Files
36Wisdom/biz/service/admin_user.go
T
2026-08-14 16:11:10 +08:00

121 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"crypto/rand"
"encoding/hex"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"golang.org/x/crypto/bcrypt"
"36wisdom/biz/consts"
"36wisdom/biz/dao"
"36wisdom/biz/model/dto"
"36wisdom/common/auth"
)
type adminUser struct{}
var AdminUser = &adminUser{}
// Login 管理员登录:用户名 + bcrypt 密码校验,签发 admin 角色 token。
// token 有效期读 auth.expire 配置(≤0 回退 consts 默认)。
func (s *adminUser) Login(ctx context.Context, req *dto.AdminLoginReq) (*dto.AdminLoginRes, error) {
rec, err := dao.AdminUser.GetByUsername(ctx, req.Username)
if err != nil {
return nil, err
}
if rec == nil || rec.Status != consts.StatusEnabled {
return nil, gerror.New("用户名或密码错误")
}
if err = bcrypt.CompareHashAndPassword([]byte(rec.Password), []byte(req.Password)); err != nil {
return nil, gerror.New("用户名或密码错误")
}
expire := g.Cfg().MustGet(ctx, "auth.expire", consts.AuthExpireSeconds).Int()
if expire <= 0 {
expire = consts.AuthExpireSeconds
}
token, err := auth.GenerateToken(auth.Secret(ctx), rec.Id, consts.RoleAdmin, expire)
if err != nil {
return nil, err
}
return &dto.AdminLoginRes{Token: token, AdminId: rec.Id}, nil
}
// uploadAllow 扩展名白名单 → 素材分类(技术设计.md 4.17)。
var uploadAllow = map[string]string{
".png": "image", ".jpg": "image", ".jpeg": "image", ".webp": "image", ".gif": "image",
".mp3": "audio", ".wav": "audio", ".m4a": "audio", ".ogg": "audio",
}
// Upload 素材上传:白名单 + 内容嗅探双校验,落盘 workspace/uploads/{分类}/YYYYMMDD_<随机>.ext
// 返回 /uploads/... 访问路径。
func (s *adminUser) Upload(ctx context.Context, req *dto.AdminUploadReq) (*dto.AdminUploadRes, error) {
f := req.File
if f == nil || f.FileHeader == nil {
return nil, gerror.New("未收到上传文件")
}
ext := strings.ToLower(filepath.Ext(f.FileHeader.Filename))
cat, ok := uploadAllow[ext]
if !ok {
return nil, gerror.New("不支持的文件类型(png/jpg/jpeg/webp/gif/mp3/wav/m4a/ogg")
}
maxBytes := int64(consts.UploadMaxImageBytes)
if cat == "audio" {
maxBytes = consts.UploadMaxAudioBytes
}
if f.FileHeader.Size > maxBytes {
return nil, gerror.Newf("文件超过大小限制(%dMB", maxBytes>>20)
}
fh, err := f.Open()
if err != nil {
return nil, gerror.Wrap(err, "读取上传文件失败")
}
data, err := io.ReadAll(fh)
if err != nil {
return nil, gerror.Wrap(err, "读取上传文件失败")
}
if err = fh.Close(); err != nil {
return nil, gerror.Wrap(err, "关闭上传文件失败")
}
if len(data) == 0 {
return nil, gerror.New("上传文件为空")
}
if mime := http.DetectContentType(data); !contentTypeMatch(cat, mime) {
return nil, gerror.New("文件内容与扩展名不符")
}
randBytes := make([]byte, 6)
if _, err = rand.Read(randBytes); err != nil {
return nil, err
}
name := time.Now().Format("20060102") + "_" + hex.EncodeToString(randBytes) + ext
dir := filepath.Join("workspace", "uploads", cat)
if err = os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
if err = os.WriteFile(filepath.Join(dir, name), data, 0o644); err != nil {
return nil, err
}
return &dto.AdminUploadRes{Url: "/uploads/" + cat + "/" + name}, nil
}
// contentTypeMatch 内容嗅探校验:image 分类须 image/*audio 分类须 audio/* 或 video/*m4a 被识别为 video/mp4)。
func contentTypeMatch(cat, mime string) bool {
if mime == "" || mime == "application/octet-stream" {
return false
}
if cat == "image" {
return strings.HasPrefix(mime, "image/")
}
return strings.HasPrefix(mime, "audio/") || strings.HasPrefix(mime, "video/")
}