update
This commit is contained in:
@ -23,49 +23,59 @@ func NewArticleHandler(articles service.ArticleService) *ArticleHandler {
|
||||
return &ArticleHandler{articles: articles}
|
||||
}
|
||||
|
||||
// List 文章列表(分页 + 多条件筛选 + 排序)。
|
||||
// List 走秀列表(分页 + 单值筛选 + 排序)。
|
||||
//
|
||||
// GET /api/public/articles
|
||||
// GET /api/v1/public/runway-looks
|
||||
//
|
||||
// 查询参数:
|
||||
// - 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
|
||||
// 查询参数(均为单值,size / images 由服务端固定,前端不传):
|
||||
// - page 页码,默认 1(服务端固定每页 ArticleFixedSize=24 条)
|
||||
// - collection rtw / menswear / couture / resort / pre_fall
|
||||
// - season spring / fall
|
||||
// - year 年份(整数),0 表示不过滤
|
||||
// - sort newest(默认)/ year_desc / year_asc / image_count
|
||||
// - brand_id hashid 编码串;传入则只返回该品牌走秀(查询参数,取代原嵌套路由 /brands/:brandId/runway-looks)
|
||||
// - locale cn / en
|
||||
//
|
||||
// 每篇附带前 ArticleFixedImages=6 张缩略图(服务端固定,不进参数)。
|
||||
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()
|
||||
h.respondRunways(c, h.parseListQuery(c))
|
||||
}
|
||||
|
||||
// parseListQuery 从 query string 解析走秀列表的筛选条件。
|
||||
// brand_id 为 hashid 编码串;非法串忽略(当作不过滤,返回空结果集而非 404)。
|
||||
func (h *ArticleHandler) parseListQuery(c *gin.Context) dto.ArticleQuery {
|
||||
q := dto.ArticleQuery{
|
||||
Page: queryInt(c, "page", 1),
|
||||
Size: dto.ArticleFixedSize, // 服务端固定,前端不传
|
||||
Collection: c.Query("collection"),
|
||||
Season: c.Query("season"),
|
||||
Year: queryInt(c, "year", 0),
|
||||
Sort: c.DefaultQuery("sort", "newest"),
|
||||
Images: dto.ArticleFixedImages, // 服务端固定,前端不传
|
||||
Locale: parseLocale(c),
|
||||
}
|
||||
if raw := strings.TrimSpace(c.Query("brand_id")); raw != "" {
|
||||
if n, err := hashid.Decode(raw); err == nil {
|
||||
q.BrandIDs = []uint32{n}
|
||||
}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// respondRunways 统一执行列表查询并回包(分页)。
|
||||
func (h *ArticleHandler) respondRunways(c *gin.Context, q dto.ArticleQuery) {
|
||||
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)
|
||||
response.Page(c, http.StatusOK, list, total, q.Size)
|
||||
}
|
||||
|
||||
// Detail 文章详情(含完整图片集)。
|
||||
// Detail 走秀详情(含完整图片集)。
|
||||
//
|
||||
// GET /api/public/articles/:id
|
||||
// GET /api/public/runways/:id
|
||||
// :id 是对外编码串(无序),需先解码为数字主键再查库;非法串视为不存在。
|
||||
func (h *ArticleHandler) Detail(c *gin.Context) {
|
||||
raw := strings.TrimSpace(c.Param("id"))
|
||||
@ -86,35 +96,3 @@ func (h *ArticleHandler) Detail(c *gin.Context) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ package handler
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"fashionapi/internal/dto"
|
||||
"fashionapi/internal/middleware"
|
||||
"fashionapi/internal/pkg/response"
|
||||
"fashionapi/internal/service"
|
||||
@ -21,46 +20,6 @@ func NewAuthHandler(auth service.AuthService) *AuthHandler {
|
||||
return &AuthHandler{auth: auth}
|
||||
}
|
||||
|
||||
// Register 注册。
|
||||
//
|
||||
// POST /api/auth/register
|
||||
// 请求体:{ username, email, password }
|
||||
// 成功:201 { token, user }
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req dto.AuthRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.auth.Register(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, result)
|
||||
}
|
||||
|
||||
// Login 登录。
|
||||
//
|
||||
// POST /api/auth/login
|
||||
// 请求体:{ account, password },account 可填邮箱或用户名
|
||||
// 成功:200 { token, user }
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req dto.AuthRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.auth.Login(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Me 当前登录用户,需 Authorization: Bearer <token>。
|
||||
//
|
||||
// GET /api/auth/me
|
||||
@ -79,5 +38,3 @@ func (h *AuthHandler) Me(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"user": user})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -27,23 +27,23 @@ func NewBrandHandler(brands service.BrandService) *BrandHandler {
|
||||
//
|
||||
// 查询参数:
|
||||
// - letter A-Z 字母;other 表示中文分桶;缺省返回全部拉丁字母开头的品牌
|
||||
// - keyword 品牌名 / 展示名模糊搜索
|
||||
// - page / size 分页,size 默认 200、上限 500
|
||||
// - only_with_articles 默认 1,只返回有走秀档案的品牌;传 0 返回全部
|
||||
// - keyword 品牌名 / 展示名模糊搜索(不过滤"必须有走秀档案")
|
||||
// - page 页码(size 已写死 30,前端不传)
|
||||
// - featured 传 1 时只返回"代表品牌"精选集合
|
||||
// - metric 精选排名指标:images(默认)/ shows
|
||||
// - limit 精选集合大小,默认 200、上限 500
|
||||
//
|
||||
// 注:原 only_with_articles 参数已删除——字母索引始终只返回有走秀档案的品牌,
|
||||
// 该过滤不再可由调用方关闭。
|
||||
func (h *BrandHandler) List(c *gin.Context) {
|
||||
q := dto.BrandQuery{
|
||||
Letter: strings.ToUpper(strings.TrimSpace(c.Query("letter"))),
|
||||
Keyword: strings.TrimSpace(c.Query("keyword")),
|
||||
Page: queryInt(c, "page", 1),
|
||||
Size: queryInt(c, "size", 200),
|
||||
OnlyWithArticles: queryBool(c, "only_with_articles", true),
|
||||
Featured: queryBool(c, "featured", false),
|
||||
Metric: strings.ToLower(strings.TrimSpace(c.Query("metric"))),
|
||||
Limit: queryInt(c, "limit", 200),
|
||||
Locale: parseLocale(c),
|
||||
Letter: strings.ToUpper(strings.TrimSpace(c.Query("letter"))),
|
||||
Keyword: strings.TrimSpace(c.Query("keyword")),
|
||||
Page: queryInt(c, "page", 1),
|
||||
Featured: queryBool(c, "featured", false),
|
||||
Metric: strings.ToLower(strings.TrimSpace(c.Query("metric"))),
|
||||
Limit: queryInt(c, "limit", 200),
|
||||
Locale: parseLocale(c),
|
||||
}
|
||||
q.Normalize()
|
||||
|
||||
@ -52,5 +52,5 @@ func (h *BrandHandler) List(c *gin.Context) {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
response.Page(c, http.StatusOK, list, total, q.Page, q.Size)
|
||||
response.Page(c, http.StatusOK, list, total, q.Size)
|
||||
}
|
||||
|
||||
@ -45,10 +45,8 @@ func queryInt(c *gin.Context, key string, def int) int {
|
||||
// queryBool 读取布尔查询参数。
|
||||
//
|
||||
// def 为该参数缺失时的取值:
|
||||
// - def=true(如 only_with_articles):只有显式传 0 / false 才关闭;
|
||||
// - def=true(如某开关默认开启):只有显式传 0 / false 才关闭;
|
||||
// - def=false(如 featured):只有显式传 1 / true 才开启。
|
||||
//
|
||||
// 这两种语义都来自原项目,必须保持,否则前端 A-Z 索引的数据量会突变。
|
||||
func queryBool(c *gin.Context, key string, def bool) bool {
|
||||
v := strings.ToLower(strings.TrimSpace(c.Query(key)))
|
||||
if v == "" {
|
||||
|
||||
@ -19,11 +19,12 @@ import (
|
||||
type SSGHandler struct {
|
||||
brands service.BrandService
|
||||
index service.IndexService
|
||||
snaps service.StreetSnapService
|
||||
}
|
||||
|
||||
// NewSSGHandler 创建 SSG handler。
|
||||
func NewSSGHandler(brands service.BrandService, index service.IndexService) *SSGHandler {
|
||||
return &SSGHandler{brands: brands, index: index}
|
||||
func NewSSGHandler(brands service.BrandService, index service.IndexService, snaps service.StreetSnapService) *SSGHandler {
|
||||
return &SSGHandler{brands: brands, index: index, snaps: snaps}
|
||||
}
|
||||
|
||||
// IndexRunway 首页(/index/)runway 区块数据。
|
||||
@ -58,4 +59,15 @@ func (h *SSGHandler) HotBrands(c *gin.Context) {
|
||||
response.Data(c, http.StatusOK, list)
|
||||
}
|
||||
|
||||
|
||||
// StreetSnapPopular 首页热门街拍(按图片数前 N,返回完整对象含封面)。
|
||||
//
|
||||
// GET /api/v1/ssg/street-snaps/popular
|
||||
func (h *SSGHandler) StreetSnapPopular(c *gin.Context) {
|
||||
const limit = 20
|
||||
list, err := h.snaps.Popular(c.Request.Context(), limit)
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
response.Data(c, http.StatusOK, list)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user