go-platform/controllers/qiniu_upload.go

328 lines
8.2 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 controllers
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"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 > 0 {
return uint64(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() {
_, 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)
// 调试:打印请求体
body := c.Ctx.Input.RequestBody
fmt.Println("SaveFileRecord 请求体长度:", len(body))
fmt.Println("SaveFileRecord 请求体内容:", string(body))
// 解析请求参数
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 // 其他
}