package handler import ( "net/http" "strconv" "github.com/gin-gonic/gin" "sproutworkcollect-backend/internal/model" "sproutworkcollect-backend/internal/service" ) // PublicHandler handles all publicly accessible API endpoints. type PublicHandler struct { workSvc *service.WorkService settingsSvc *service.SettingsService rateLimiter *service.RateLimiter } // NewPublicHandler wires up a PublicHandler with its dependencies. func NewPublicHandler( workSvc *service.WorkService, settingsSvc *service.SettingsService, rateLimiter *service.RateLimiter, ) *PublicHandler { return &PublicHandler{ workSvc: workSvc, settingsSvc: settingsSvc, rateLimiter: rateLimiter, } } // GetSettings handles GET /api/settings func (h *PublicHandler) GetSettings(c *gin.Context) { settings, err := h.settingsSvc.Load() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, settings) } // GetWorks handles GET /api/works func (h *PublicHandler) GetWorks(c *gin.Context) { works, err := h.workSvc.LoadAllWorks() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) return } page, pageSize, paged := resolvePageParams(c) total := len(works) worksPage := paginateWorks(works, page, pageSize) ids := make([]string, len(worksPage)) for i, w := range worksPage { ids[i] = w.WorkID } resp := gin.H{ "success": true, "data": ids, "total": total, } if paged { resp["page"] = page resp["page_size"] = pageSize resp["total_pages"] = calcTotalPages(total, pageSize) } c.JSON(http.StatusOK, resp) } // GetWorkDetail handles GET /api/works/:work_id func (h *PublicHandler) GetWorkDetail(c *gin.Context) { workID := c.Param("work_id") work, err := h.workSvc.LoadWork(workID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "作品不存在"}) return } fp := service.Fingerprint(c.ClientIP(), c.GetHeader("User-Agent")) if h.rateLimiter.CanPerform(fp, "view", workID) { _ = h.workSvc.UpdateStats(workID, "view") // Reload to return the updated view count. if fresh, err2 := h.workSvc.LoadWork(workID); err2 == nil { work = fresh } } c.JSON(http.StatusOK, gin.H{ "success": true, "data": h.workSvc.BuildResponse(work), }) } // SearchWorks handles GET /api/search?q=...&category=... func (h *PublicHandler) SearchWorks(c *gin.Context) { works, err := h.workSvc.SearchWorks(c.Query("q"), c.Query("category")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) return } page, pageSize, paged := resolvePageParams(c) total := len(works) worksPage := paginateWorks(works, page, pageSize) ids := make([]string, len(worksPage)) for i, w := range worksPage { ids[i] = w.WorkID } resp := gin.H{ "success": true, "data": ids, "total": total, } if paged { resp["page"] = page resp["page_size"] = pageSize resp["total_pages"] = calcTotalPages(total, pageSize) } c.JSON(http.StatusOK, resp) } func resolvePageParams(c *gin.Context) (int, int, bool) { page, hasPage := parsePositiveInt(c, "page") pageSize, hasSize := parsePositiveInt(c, "page_size") if !hasSize { pageSize, hasSize = parsePositiveInt(c, "pageSize") } if !hasSize { pageSize, hasSize = parsePositiveInt(c, "limit") } if !hasPage && !hasSize { return 0, 0, false } if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 12 } if pageSize > 200 { pageSize = 200 } return page, pageSize, true } func parsePositiveInt(c *gin.Context, key string) (int, bool) { raw, ok := c.GetQuery(key) if !ok { return 0, false } val, err := strconv.Atoi(raw) if err != nil { return 0, false } return val, true } func paginateWorks(works []*model.WorkConfig, page, pageSize int) []*model.WorkConfig { if pageSize <= 0 { return works } if page <= 0 { page = 1 } start := (page - 1) * pageSize if start >= len(works) { return []*model.WorkConfig{} } end := start + pageSize if end > len(works) { end = len(works) } return works[start:end] } func calcTotalPages(total, pageSize int) int { if pageSize <= 0 { return 1 } if total == 0 { return 0 } return (total + pageSize - 1) / pageSize } // GetCategories handles GET /api/categories func (h *PublicHandler) GetCategories(c *gin.Context) { cats, err := h.workSvc.AllCategories() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": cats}) } // LikeWork handles POST /api/like/:work_id func (h *PublicHandler) LikeWork(c *gin.Context) { workID := c.Param("work_id") if _, err := h.workSvc.LoadWork(workID); err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "作品不存在"}) return } fp := service.Fingerprint(c.ClientIP(), c.GetHeader("User-Agent")) if !h.rateLimiter.CanPerform(fp, "like", workID) { c.JSON(http.StatusTooManyRequests, gin.H{ "success": false, "message": "操作太频繁,请稍后再试", }) return } if err := h.workSvc.UpdateStats(workID, "like"); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "点赞失败"}) return } work, _ := h.workSvc.LoadWork(workID) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "点赞成功", "likes": work.Likes, }) }