package handler import ( "net/http" "strconv" "strings" "fashionapi/internal/dto" "fashionapi/internal/pkg/hashid" "fashionapi/internal/pkg/response" "fashionapi/internal/service" "github.com/gin-gonic/gin" ) // ArticleHandler 对外公开的文章接口。 type ArticleHandler struct { articles service.ArticleService } // NewArticleHandler 创建文章 handler。 func NewArticleHandler(articles service.ArticleService) *ArticleHandler { return &ArticleHandler{articles: articles} } // List 文章列表(分页 + 多条件筛选 + 排序)。 // // GET /api/public/articles // // 查询参数: // - page / size 分页,size 默认 12、上限 100 // - keyword 标题模糊搜索 // - brand_id 单个品牌 id // - brand_ids 多个品牌 id,逗号分隔 // - collection_type rtw / menswear / couture / resort / pre_fall(可重复传实现多选) // - season spring / fall(可重复传) // - season_code SS26 / FW25,精确匹配 // - year 年份(可重复传) // - sort newest(默认)/ year_desc / year_asc / image_count // - with_images 附带每篇前 N 张图,上限 12 func (h *ArticleHandler) List(c *gin.Context) { q := dto.ArticleQuery{ Page: queryInt(c, "page", 1), Size: queryInt(c, "size", 12), Keyword: c.Query("keyword"), BrandID: decodeBrandID(c.Query("brand_id")), BrandIDs: decodeBrandIDs(c.Query("brand_ids")), CollectionTypes: c.QueryArray("collection_type"), Seasons: c.QueryArray("season"), SeasonCode: c.Query("season_code"), Years: parseIntSlice(c.QueryArray("year")), Sort: c.DefaultQuery("sort", "newest"), WithImages: queryInt(c, "with_images", 0), Locale: parseLocale(c), } q.Normalize() list, total, err := h.articles.List(c.Request.Context(), q) if err != nil { fail(c, err) return } response.Page(c, http.StatusOK, list, total, q.Page, q.Size) } // Detail 文章详情(含完整图片集)。 // // GET /api/public/articles/:id // :id 是对外编码串(无序),需先解码为数字主键再查库;非法串视为不存在。 func (h *ArticleHandler) Detail(c *gin.Context) { raw := strings.TrimSpace(c.Param("id")) if raw == "" { response.Error(c, http.StatusBadRequest, "缺少文章 id") return } numeric, err := hashid.Decode(raw) if err != nil { response.Error(c, http.StatusNotFound, "文章不存在") return } detail, err := h.articles.Detail(c.Request.Context(), strconv.FormatUint(uint64(numeric), 10), parseLocale(c)) if err != nil { fail(c, err) return } response.Data(c, http.StatusOK, detail) } // decodeBrandID 把对外编码串还原为数字主键;非法或为空返回 ""(表示不过滤)。 func decodeBrandID(s string) string { s = strings.TrimSpace(s) if s == "" { return "" } n, err := hashid.Decode(s) if err != nil { return "" } return strconv.FormatUint(uint64(n), 10) } // decodeBrandIDs 解析逗号分隔的编码串列表,逐个解码为数字主键,跳过非法项。 func decodeBrandIDs(s string) []uint32 { s = strings.TrimSpace(s) if s == "" { return nil } var out []uint32 for _, p := range strings.Split(s, ",") { p = strings.TrimSpace(p) if p == "" { continue } if n, err := hashid.Decode(p); err == nil { out = append(out, n) } } return out }