Files
SmyWorkCollect/SproutWorkCollect-Backend-Golang/internal/handler/admin.go

318 lines
9.3 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 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": "删除成功"})
}