114 lines
3.8 KiB
Go
114 lines
3.8 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"fashionapi/internal/dto"
|
||
"fashionapi/internal/middleware"
|
||
"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/v1/public/runway-looks
|
||
//
|
||
// 查询参数(均为单值,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 := h.parseListQuery(c)
|
||
q.Normalize()
|
||
list, total, err := h.articles.List(c.Request.Context(), q)
|
||
if err != nil {
|
||
fail(c, err)
|
||
return
|
||
}
|
||
// 列表首屏对游客完全公开(翻页门槛由 PublicFirstPageAuth 兜底),不做预览截断——
|
||
// 「未登录限制」已转移到详情页:每篇图集最多看 PreviewLimit 张图片,列表照常展示全部专辑。
|
||
response.Page(c, http.StatusOK, list, total, q.Size)
|
||
}
|
||
|
||
// isAnon 判断请求是否未携带有效登录态(供列表接口做未登录预览截断)。
|
||
func isAnon(c *gin.Context) bool {
|
||
_, ok := middleware.UserIDFrom(c)
|
||
return !ok
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// Detail 走秀详情(含完整图片集)。
|
||
//
|
||
// GET /api/public/runways/: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.DecodeTyped(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
|
||
}
|
||
|
||
// 未登录:图片集仅返回预览上限(PreviewLimit)张,并打 preview=true,
|
||
// 前端据此在详情页展示「登录查看全部图片」门禁;已登录(带有效 Bearer)返回完整图片集。
|
||
anon := isAnon(c)
|
||
preview := false
|
||
imageTotal := len(detail.Images)
|
||
if anon && imageTotal > dto.PreviewLimit {
|
||
detail.Images = detail.Images[:dto.PreviewLimit]
|
||
preview = true
|
||
}
|
||
response.DataPreview(c, http.StatusOK, detail, preview, imageTotal)
|
||
}
|