319 lines
7.9 KiB
Go
319 lines
7.9 KiB
Go
package controllers
|
||
|
||
import (
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"strconv"
|
||
"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 {
|
||
return c.Ctx.Input.Bind(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 // 其他
|
||
}
|