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" ) // StreetSnapHandler 对外公开的街拍接口。 type StreetSnapHandler struct { svc service.StreetSnapService } // NewStreetSnapHandler 创建街拍 handler。 func NewStreetSnapHandler(svc service.StreetSnapService) *StreetSnapHandler { return &StreetSnapHandler{svc: svc} } // List 街拍列表(分页 + 排序 + 年份过滤)。 // // GET /api/public/street-snaps // // 查询参数: // - page 页码,默认 1(服务端固定每页 StreetSnapFixedSize=24 条) // - year 年份(整数),0 表示不过滤 // - city 地区/城市(精确匹配 city 列),'' 表示不过滤 // - sort image_count(默认,按图片数降序)/ year_desc / year_asc // - locale cn / en // // 每条附带前 StreetSnapFixedImages=6 张缩略图(服务端固定,不进参数)。 // 列表接口挂在 public 组,受 ClientSign 中间件保护(与 runway-looks / brands 同批批量接口)。 func (h *StreetSnapHandler) List(c *gin.Context) { q := h.parseListQuery(c) q.Normalize() list, total, err := h.svc.List(c.Request.Context(), q) if err != nil { fail(c, err) return } // 列表首屏对游客完全公开(翻页门槛由 PublicFirstPageAuth 兜底),不做预览截断—— // 「未登录限制」已转移到详情页:每篇图集最多看 PreviewLimit 张图片,列表照常展示全部专辑。 response.Page(c, http.StatusOK, list, total, q.Size) } func (h *StreetSnapHandler) parseListQuery(c *gin.Context) dto.StreetSnapQuery { return dto.StreetSnapQuery{ Page: queryInt(c, "page", 1), Size: dto.StreetSnapFixedSize, // 服务端固定,前端不传 Year: queryInt(c, "year", 0), City: c.DefaultQuery("city", ""), Sort: c.DefaultQuery("sort", "image_count"), Images: dto.StreetSnapFixedImages, // 服务端固定,前端不传 Locale: parseLocale(c), } } // Detail 街拍详情(含完整图片集)。 // // GET /api/public/street-snaps/:id // :id 是对外编码串(无序),需先解码为数字主键再查库;非法串视为不存在。 // 不走前端 JS 签名,单独挂载(与 runway 详情一致),单篇枚举防护由 HashID 承担。 func (h *StreetSnapHandler) 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.svc.Detail(c.Request.Context(), strconv.FormatUint(uint64(numeric), 10), middleware.IsVIP(middleware.TierFrom(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) }