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