diff --git a/server/internal/file/internal/logic/fileupload/uploadfilelogic.go b/server/internal/file/internal/logic/fileupload/uploadfilelogic.go index 564c451e..3cf4d25f 100644 --- a/server/internal/file/internal/logic/fileupload/uploadfilelogic.go +++ b/server/internal/file/internal/logic/fileupload/uploadfilelogic.go @@ -1,13 +1,20 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.9.2 - +// fileupload/upload_file_logic.go package fileupload import ( + "bytes" "context" + "fmt" + "io" + "net/http" + "path/filepath" + "sort" + "strings" + "time" "github.com/JACKYMYPERSON/hldrCenter/config" "github.com/JACKYMYPERSON/hldrCenter/internal/file/internal/types" + "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/gin-gonic/gin" "github.com/zeromicro/go-zero/core/logx" @@ -30,7 +37,203 @@ func NewUploadFileLogic(ctx context.Context, cfg *config.Config, c *gin.Context) } func (l *UploadFileLogic) UploadFile() (resp *types.UploadFileResp, err error) { - // todo: add your logic here and delete this line + // 1. 获取上传文件 + fileHeader, err := l.c.FormFile("file") + if err != nil { + l.c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "获取文件失败,请重新上传", + }) + return + } - return + // 2. 打开文件 + file, err := fileHeader.Open() + if err != nil { + l.c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "打开文件失败", + }) + return + } + defer file.Close() + + // 3. 检查文件大小 + if fileHeader.Size > l.cfg.FileUpload.MaxFileSize { + l.c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": fmt.Sprintf("文件大小不能超过 %dMB", l.cfg.FileUpload.MaxFileSize>>20), + }) + return + } + + // 4. 检查文件类型 + fileType := fileHeader.Header.Get("Content-Type") + allowTypes := strings.Split(l.cfg.FileUpload.AllowFileTypes, ",") + isAllowed := false + for _, typ := range allowTypes { + if strings.TrimSpace(typ) == fileType { + isAllowed = true + break + } + } + if !isAllowed { + l.c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": fmt.Sprintf("不支持的文件类型,仅允许:%s", l.cfg.FileUpload.AllowFileTypes), + }) + return + } + + // 5. 初始化OSS客户端 + client, err := oss.New( + l.cfg.OSS.Endpoint, + l.cfg.OSS.AccessKeyID, + l.cfg.OSS.AccessKeySecret, + ) + if err != nil { + l.c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "初始化OSS客户端失败", + }) + return + } + + // 6. 获取Bucket实例 + bucket, err := client.Bucket(l.cfg.OSS.BucketName) + if err != nil { + l.c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "获取Bucket失败", + }) + return + } + + // 7. 生成文件存储路径 + timestamp := time.Now().Format("20060102150405") + fileExt := filepath.Ext(fileHeader.Filename) + filename := fmt.Sprintf("%s_%s%s", timestamp, strings.TrimSuffix(fileHeader.Filename, fileExt), fileExt) + + var fileDir string + switch { + case strings.HasPrefix(fileType, "video/"): + fileDir = "videos/" + case fileType == "application/pdf": + fileDir = "pdfs/" + case strings.HasPrefix(fileType, "image/"): + fileDir = "images/" + case fileType == "text/plain": + fileDir = "txts/" + case fileType == "application/json": + fileDir = "jsons/" + default: + fileDir = "other/" + } + objectKey := l.cfg.OSS.ObjectPrefix + fileDir + filename + + // 8. 分片上传(100KB/片) + const partSize = 100 * 1024 // 100KB + totalSize := fileHeader.Size + partCount := (totalSize + partSize - 1) / partSize + var partETags []oss.PartETag + + // 重置文件指针 + if _, err = file.Seek(0, 0); err != nil { + l.c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "重置文件指针失败", + }) + return + } + + // 8.1 初始化分片上传 + imur, err := bucket.InitiateMultipartUpload(objectKey) + if err != nil { + l.c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "初始化分片上传失败", + }) + return + } + + // 8.2 上传每个分片 + for i := 1; i <= int(partCount); i++ { + partStart := int64((i - 1) * partSize) + partEnd := partStart + partSize + if partEnd > totalSize { + partEnd = totalSize + } + partLen := partEnd - partStart + + // 读取分片数据 + data := make([]byte, partLen) + if _, err = file.Seek(partStart, 0); err != nil { + _ = bucket.AbortMultipartUpload(imur) + l.c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": fmt.Sprintf("定位分片%d失败", i), + }) + return + } + if _, err = io.ReadFull(file, data); err != nil && err != io.EOF { + _ = bucket.AbortMultipartUpload(imur) + l.c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": fmt.Sprintf("读取分片%d失败", i), + }) + return + } + + // 上传分片(旧版:传入 imur) + partResult, err := bucket.UploadPart(imur, i, bytes.NewReader(data)) + if err != nil { + _ = bucket.AbortMultipartUpload(imur) + l.c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": fmt.Sprintf("上传分片%d失败", i), + }) + return + } + + partETags = append(partETags, oss.PartETag{ + PartNumber: i, + ETag: partResult.ETag, + }) + } + + // 8.3 按分片号排序(必须) + sort.Slice(partETags, func(i, j int) bool { + return partETags[i].PartNumber < partETags[j].PartNumber + }) + + // 8.4 完成分片上传(旧版:传入 imur) + _, err = bucket.CompleteMultipartUpload(imur, partETags) + if err != nil { + _ = bucket.AbortMultipartUpload(imur) + l.c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "合并分片失败", + }) + return + } + + // 9. 返回成功结果 + fileURL := fmt.Sprintf("https://%s.%s/%s", + l.cfg.OSS.BucketName, + strings.TrimPrefix(l.cfg.OSS.Endpoint, "https://"), + objectKey, + ) + + l.c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "文件上传成功", + "data": gin.H{ + "url": fileURL, + "filename": fileHeader.Filename, + "size": fileHeader.Size, + "type": fileType, + }, + }) + + return nil, nil }