package controllers import ( "crypto/md5" "encoding/hex" "encoding/json" "fmt" "io" "strings" "time" "server/models" "server/pkg/jwtutil" beego "github.com/beego/beego/v2/server/web" "github.com/qiniu/go-sdk/v7/auth/qbox" "github.com/qiniu/go-sdk/v7/storage" ) // QiniuUploadController 七牛云上传控制器 type QiniuUploadController struct { beego.Controller } // platformClaims 获取平台端 JWT claims func (c *QiniuUploadController) platformClaims() (*jwtutil.Claims, error) { auth := c.Ctx.Request.Header.Get("Authorization") if auth == "" { return nil, fmt.Errorf("未登录") } parts := strings.Split(auth, " ") if len(parts) != 2 || parts[0] != "Bearer" { return nil, fmt.Errorf("token 格式错误") } claims, err := jwtutil.ParseToken(parts[1]) if err != nil { return nil, fmt.Errorf("token 无效") } return claims, nil } // effectiveTid 获取有效的租户 ID func (c *QiniuUploadController) effectiveTid(claims *jwtutil.Claims) uint64 { if claims.TenantId != nil && *claims.TenantId > 0 { return *claims.TenantId } return 0 } // jsonErr 返回错误响应 func (c *QiniuUploadController) jsonErr(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} _ = c.ServeJSON() } // jsonOK 返回成功响应 func (c *QiniuUploadController) jsonOK(data interface{}) { c.Data["json"] = map[string]interface{}{"code": 200, "data": data} _ = c.ServeJSON() } // ParseJSON 解析 JSON 请求体 func (c *QiniuUploadController) ParseJSON(v interface{}) error { body := c.Ctx.Input.RequestBody if len(body) == 0 { return fmt.Errorf("请求体为空") } return json.Unmarshal(body, v) } // GetUploadToken 获取上传凭证 // GET /platform/qiniu/token func (c *QiniuUploadController) GetUploadToken() { claims, err := c.platformClaims() if err != nil { c.jsonErr(401, 401, err.Error()) return } // 获取存储配置 cfg, err := models.GetStorageConfig() if err != nil || cfg.StorageType != "qiniu" { c.jsonErr(400, 400, "当前未配置七牛云存储") return } // 检查配置完整性 if cfg.QiniuAccessKey == "" || cfg.QiniuSecretKey == "" || cfg.QiniuBucket == "" { c.jsonErr(500, 500, "七牛云配置不完整") return } // 生成文件 key(前端可以覆盖) datePath := time.Now().Format("2006/01/02") timestamp := time.Now().UnixNano() keyPrefix := fmt.Sprintf("%s/%d", datePath, timestamp) // 创建上传策略 mac := qbox.NewMac(cfg.QiniuAccessKey, cfg.QiniuSecretKey) putPolicy := storage.PutPolicy{ Scope: cfg.QiniuBucket, ReturnBody: `{"key":"$(key)","hash":"$(etag)","size":$(fsize),"mimeType":"$(mimeType)"}`, Expires: 3600, // 1小时有效期 } upToken := putPolicy.UploadToken(mac) // 返回上传凭证和配置 c.jsonOK(map[string]interface{}{ "token": upToken, "domain": cfg.QiniuDomain, "bucket": cfg.QiniuBucket, "region": cfg.QiniuRegion, "keyPrefix": keyPrefix, "expires": time.Now().Add(time.Hour).Unix(), "uploadUrl": getQiniuUploadURL(cfg.QiniuRegion), }) } // SaveFileRecord 保存文件记录 // POST /platform/qiniu/save func (c *QiniuUploadController) SaveFileRecord() { claims, err := c.platformClaims() if err != nil { c.jsonErr(401, 401, err.Error()) return } tid := c.effectiveTid(claims) // 解析请求参数 type SaveRequest struct { Key string `json:"key"` // 七牛云文件 key Hash string `json:"hash"` // 文件 hash (etag) Size int64 `json:"size"` // 文件大小 Name string `json:"name"` // 原始文件名 MimeType string `json:"mimeType"` // 文件类型 Cate uint64 `json:"cate"` // 分类 ID } var req SaveRequest if err := c.ParseJSON(&req); err != nil { c.jsonErr(400, 400, "参数解析失败: "+err.Error()) return } // 验证必填字段 if req.Key == "" || req.Name == "" { c.jsonErr(400, 400, "缺少必填参数") return } // 获取存储配置 cfg, err := models.GetStorageConfig() if err != nil || cfg.StorageType != "qiniu" { c.jsonErr(400, 400, "当前未配置七牛云存储") return } // 构建完整 URL domain := strings.TrimRight(cfg.QiniuDomain, "/") fileURL := fmt.Sprintf("%s/%s", domain, req.Key) // 计算 MD5(使用 hash 作为 MD5,或者重新计算) md5Sum := req.Hash if md5Sum == "" { // 如果没有 hash,使用 key 生成一个唯一标识 h := md5.New() h.Write([]byte(req.Key)) md5Sum = hex.EncodeToString(h.Sum(nil)) } // 检查文件是否已存在(通过 MD5) var exist models.SystemFile err = models.Orm.QueryTable(new(models.SystemFile)). Filter("md5", md5Sum). Filter("tid", tid). Filter("delete_time__isnull", true). One(&exist) if err == nil { // 文件已存在,返回已有记录 c.Data["json"] = map[string]interface{}{ "code": 201, "msg": "文件已存在", "data": map[string]interface{}{ "url": exist.Src, "id": exist.ID, "name": exist.Name, }, } _ = c.ServeJSON() return } // 检测文件类型 ext := getQiniuFileExt(req.Name) fileType := detectQiniuFileType(ext) // 保存文件记录 adminID := uint64(claims.UserID) row := &models.SystemFile{ Tid: tid, Uid: &adminID, Name: req.Name, Type: fileType, Cate: req.Cate, Size: uint64(req.Size), Src: fileURL, Uploader: adminID, Md5: md5Sum, } id, err := models.Orm.Insert(row) if err != nil { c.jsonErr(500, 500, "保存文件记录失败: "+err.Error()) return } c.jsonOK(map[string]interface{}{ "url": fileURL, "id": uint64(id), "name": req.Name, "key": req.Key, }) } // GetStorageConfig 获取存储配置(前端用于判断上传方式) // GET /platform/storage/config func (c *QiniuUploadController) GetStorageConfig() { _, err := c.platformClaims() if err != nil { c.jsonErr(401, 401, err.Error()) return } cfg, err := models.GetStorageConfig() if err != nil { c.jsonOK(map[string]interface{}{ "storageType": "local", }) return } // 只返回必要的配置信息,不返回密钥 c.jsonOK(map[string]interface{}{ "storageType": cfg.StorageType, "qiniuDomain": cfg.QiniuDomain, "qiniuRegion": cfg.QiniuRegion, }) } // getQiniuUploadURL 根据区域获取上传地址 func getQiniuUploadURL(region string) string { switch region { case "z0": return "https://up-z0.qiniup.com" case "z1": return "https://up-z1.qiniup.com" case "z2": return "https://up-z2.qiniup.com" case "na0": return "https://up-na0.qiniup.com" case "as0": return "https://up-as0.qiniup.com" case "cn-east-2": return "https://up-cn-east-2.qiniup.com" default: return "https://up-z0.qiniup.com" // 默认华东 } } // getQiniuFileExt 获取文件扩展名 func getQiniuFileExt(filename string) string { parts := strings.Split(filename, ".") if len(parts) > 1 { return strings.ToLower(parts[len(parts)-1]) } return "" } // detectQiniuFileType 检测文件类型 func detectQiniuFileType(ext string) uint8 { imageExts := map[string]bool{ "jpg": true, "jpeg": true, "png": true, "gif": true, "bmp": true, "webp": true, "svg": true, "ico": true, } videoExts := map[string]bool{ "mp4": true, "avi": true, "mov": true, "wmv": true, "flv": true, "mkv": true, "webm": true, "m4v": true, } audioExts := map[string]bool{ "mp3": true, "wav": true, "flac": true, "aac": true, "ogg": true, "m4a": true, "wma": true, } docExts := map[string]bool{ "doc": true, "docx": true, "xls": true, "xlsx": true, "ppt": true, "pptx": true, "pdf": true, "txt": true, "md": true, } archiveExts := map[string]bool{ "zip": true, "rar": true, "7z": true, "tar": true, "gz": true, "bz2": true, "xz": true, } executableExts := map[string]bool{ "exe": true, "msi": true, "dmg": true, "pkg": true, "deb": true, "rpm": true, "apk": true, "msix": true, } if imageExts[ext] { return 1 // 图片 } if videoExts[ext] { return 2 // 视频 } if audioExts[ext] { return 3 // 音频 } if docExts[ext] { return 4 // 文档 } if archiveExts[ext] || executableExts[ext] { return 5 // 压缩包/安装包 } return 0 // 其他 }