package handler import ( "fmt" "net/http" "os" "path/filepath" "strings" "time" "github.com/gin-gonic/gin" "sproutworkcollect-backend/internal/model" "sproutworkcollect-backend/internal/service" ) const maxUploadBytes = 5000 << 20 // 5 000 MB // AdminHandler handles admin-only API endpoints (protected by AdminAuth middleware). type AdminHandler struct { workSvc *service.WorkService } // NewAdminHandler wires up an AdminHandler. func NewAdminHandler(workSvc *service.WorkService) *AdminHandler { return &AdminHandler{workSvc: workSvc} } // GetWorks handles GET /api/admin/works func (h *AdminHandler) GetWorks(c *gin.Context) { works, err := h.workSvc.LoadAllWorks() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) return } responses := make([]*model.WorkResponse, len(works)) for i, w := range works { responses[i] = h.workSvc.BuildResponse(w) } c.JSON(http.StatusOK, gin.H{"success": true, "data": responses, "total": len(responses)}) } // CreateWork handles POST /api/admin/works func (h *AdminHandler) CreateWork(c *gin.Context) { var data model.WorkConfig if err := c.ShouldBindJSON(&data); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "请求数据格式错误"}) return } if data.WorkID == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "作品ID不能为空"}) return } ts := time.Now().Format("2006-01-02T15:04:05.000000") data.UploadTime = ts data.UpdateTime = ts data.UpdateCount = 0 data.Downloads = 0 data.Views = 0 data.Likes = 0 data.Normalize() if err := h.workSvc.CreateWork(&data); err != nil { status := http.StatusInternalServerError if strings.Contains(err.Error(), "已存在") { status = http.StatusConflict } c.JSON(status, gin.H{"success": false, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "创建成功", "work_id": data.WorkID}) } // UpdateWork handles PUT /api/admin/works/:work_id func (h *AdminHandler) UpdateWork(c *gin.Context) { workID := c.Param("work_id") var data model.WorkConfig if err := c.ShouldBindJSON(&data); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "请求数据格式错误"}) return } if err := h.workSvc.UpdateWork(workID, &data); err != nil { status := http.StatusInternalServerError if strings.Contains(err.Error(), "不存在") { status = http.StatusNotFound } c.JSON(status, gin.H{"success": false, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "更新成功"}) } // DeleteWork handles DELETE /api/admin/works/:work_id func (h *AdminHandler) DeleteWork(c *gin.Context) { if err := h.workSvc.DeleteWork(c.Param("work_id")); err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "删除成功"}) } // UploadFile handles POST /api/admin/upload/:work_id/:file_type // file_type: "image" | "video" | "platform" // For "platform", the form field "platform" must specify the target platform name. func (h *AdminHandler) UploadFile(c *gin.Context) { workID := c.Param("work_id") fileType := c.Param("file_type") if !h.workSvc.WorkExists(workID) { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "作品不存在"}) return } fh, err := c.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "没有文件"}) return } if fh.Size > maxUploadBytes { c.JSON(http.StatusRequestEntityTooLarge, gin.H{ "success": false, "message": fmt.Sprintf("文件太大,最大支持 %dMB,当前 %dMB", maxUploadBytes>>20, fh.Size>>20), }) return } originalName := fh.Filename safeName := service.SafeFilename(originalName) ext := strings.ToLower(filepath.Ext(safeName)) allowed := map[string]bool{ ".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".mp4": true, ".avi": true, ".mov": true, ".zip": true, ".rar": true, ".apk": true, ".exe": true, ".dmg": true, } if !allowed[ext] { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "不支持的文件格式"}) return } // Determine destination directory and resolve a unique filename. // ModifyWork (called later) re-checks uniqueness under a write lock to avoid races. work, err := h.workSvc.LoadWork(workID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "作品配置不存在"}) return } var saveDir string var platform string switch fileType { case "image": saveDir = filepath.Join(h.workSvc.WorksDir(), workID, "image") case "video": saveDir = filepath.Join(h.workSvc.WorksDir(), workID, "video") case "platform": platform = c.PostForm("platform") if platform == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "平台参数缺失"}) return } saveDir = filepath.Join(h.workSvc.WorksDir(), workID, "platform", platform) default: c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "不支持的文件类型"}) return } if err := os.MkdirAll(saveDir, 0755); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "创建目录失败"}) return } // Pre-compute unique filename based on current state. // The authoritative assignment happens inside ModifyWork below. var previewName string switch fileType { case "image": previewName = service.UniqueFilename(safeName, work.Screenshots) case "video": previewName = service.UniqueFilename(safeName, work.VideoFiles) case "platform": previewName = service.UniqueFilename(safeName, work.FileNames[platform]) } destPath := filepath.Join(saveDir, previewName) if err := c.SaveUploadedFile(fh, destPath); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": fmt.Sprintf("保存文件失败: %v", err), }) return } // Atomically update the work config under a write lock. var finalName string modErr := h.workSvc.ModifyWork(workID, func(w *model.WorkConfig) { // Re-derive the unique name inside the lock to handle concurrent uploads. switch fileType { case "image": finalName = service.UniqueFilename(safeName, w.Screenshots) case "video": finalName = service.UniqueFilename(safeName, w.VideoFiles) case "platform": finalName = service.UniqueFilename(safeName, w.FileNames[platform]) } // Rename the file on disk if the finalName differs from the pre-computed one. if finalName != previewName { newDest := filepath.Join(saveDir, finalName) _ = os.Rename(destPath, newDest) } if w.OriginalNames == nil { w.OriginalNames = make(map[string]string) } w.OriginalNames[finalName] = originalName switch fileType { case "image": if !service.ContainsString(w.Screenshots, finalName) { w.Screenshots = append(w.Screenshots, finalName) } if w.Cover == "" { w.Cover = finalName } case "video": if !service.ContainsString(w.VideoFiles, finalName) { w.VideoFiles = append(w.VideoFiles, finalName) } case "platform": if w.FileNames == nil { w.FileNames = make(map[string][]string) } if !service.ContainsString(w.FileNames[platform], finalName) { w.FileNames[platform] = append(w.FileNames[platform], finalName) } } }) if modErr != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "更新配置失败"}) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "上传成功", "filename": finalName, "file_size": fh.Size, }) } // DeleteFile handles DELETE /api/admin/delete-file/:work_id/:file_type/:filename func (h *AdminHandler) DeleteFile(c *gin.Context) { workID := c.Param("work_id") fileType := c.Param("file_type") filename := c.Param("filename") if _, err := h.workSvc.LoadWork(workID); err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "作品不存在"}) return } platform := c.Query("platform") var filePath string switch fileType { case "image": filePath = filepath.Join(h.workSvc.WorksDir(), workID, "image", filename) case "video": filePath = filepath.Join(h.workSvc.WorksDir(), workID, "video", filename) case "platform": if platform == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "平台参数缺失"}) return } filePath = filepath.Join(h.workSvc.WorksDir(), workID, "platform", platform, filename) default: c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "不支持的文件类型"}) return } _ = os.Remove(filePath) modErr := h.workSvc.ModifyWork(workID, func(w *model.WorkConfig) { if w.OriginalNames != nil { delete(w.OriginalNames, filename) } switch fileType { case "image": w.Screenshots = service.RemoveString(w.Screenshots, filename) if w.Cover == filename { if len(w.Screenshots) > 0 { w.Cover = w.Screenshots[0] } else { w.Cover = "" } } case "video": w.VideoFiles = service.RemoveString(w.VideoFiles, filename) case "platform": if w.FileNames != nil { w.FileNames[platform] = service.RemoveString(w.FileNames[platform], filename) } } }) if modErr != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "更新配置失败"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "删除成功"}) }