diff --git a/controllers/qiniu_upload.go b/controllers/qiniu_upload.go new file mode 100644 index 0000000..826574a --- /dev/null +++ b/controllers/qiniu_upload.go @@ -0,0 +1,274 @@ +package controllers + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "strconv" + "strings" + "time" + + "server/models" + "server/services" + + "github.com/qiniu/go-sdk/v7/auth/qbox" + "github.com/qiniu/go-sdk/v7/storage" +) + +// QiniuUploadController 七牛云上传控制器 +type QiniuUploadController struct { + BaseController +} + +// 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 := getFileExt(req.Name) + fileType := detectFileType(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() { + claims, 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" // 默认华东 + } +} + +// getFileExt 获取文件扩展名 +func getFileExt(filename string) string { + parts := strings.Split(filename, ".") + if len(parts) > 1 { + return strings.ToLower(parts[len(parts)-1]) + } + return "" +} + +// detectFileType 检测文件类型 +func detectFileType(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 // 其他 +} diff --git a/docs/服务端启动命令.md b/docs/服务端启动命令.md index 98d0458..f9322a4 100644 --- a/docs/服务端启动命令.md +++ b/docs/服务端启动命令.md @@ -13,6 +13,12 @@ chmod +x install-systemd-service.sh # 运行安装脚本 sudo bash install-systemd-service.sh + +或者 + +sudo env PATH=$PATH:/usr/local/btgo/bin bash install-systemd-service.sh + + ``` 脚本会自动: diff --git a/routers/platform/platform.go b/routers/platform/platform.go index ae790f9..a18d212 100644 --- a/routers/platform/platform.go +++ b/routers/platform/platform.go @@ -152,4 +152,9 @@ func Register() { beego.Router("/platform/batchdeletefiles", &controllers.PlatformFileController{}, "post:BatchDeleteFiles") beego.Router("/platform/batchDeleteFilesPermanently", &controllers.PlatformFileController{}, "post:BatchDeleteFilesPermanently") beego.Router("/platform/batchMoveFiles", &controllers.PlatformFileController{}, "post:BatchMoveFiles") + + // 七牛云直传相关 + beego.Router("/platform/storage/config", &controllers.QiniuUploadController{}, "get:GetStorageConfig") + beego.Router("/platform/qiniu/token", &controllers.QiniuUploadController{}, "get:GetUploadToken") + beego.Router("/platform/qiniu/save", &controllers.QiniuUploadController{}, "post:SaveFileRecord") }