134 lines
4.8 KiB
Go
134 lines
4.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"
|
||
)
|
||
|
||
// StreetSnapHandler 对外公开的街拍接口。
|
||
type StreetSnapHandler struct {
|
||
svc service.StreetSnapService
|
||
favs service.FavoriteService
|
||
}
|
||
|
||
// NewStreetSnapHandler 创建街拍 handler。
|
||
func NewStreetSnapHandler(svc service.StreetSnapService, favs service.FavoriteService) *StreetSnapHandler {
|
||
return &StreetSnapHandler{svc: svc, favs: favs}
|
||
}
|
||
|
||
// List 街拍列表(分页 + 排序 + 年份过滤)。
|
||
//
|
||
// GET /api/v1/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/v1/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
|
||
}
|
||
// 类型由路由提供(/street-snaps 即 snap)。类型已揉进密码:非 snap 编码的串
|
||
// 在此解出的是错误主键,查库自然 404,从而强制前端按类型化路由分流(无需靠前缀嗅探)。
|
||
numeric, err := hashid.DecodeWithType(raw, hashid.TypeSnap)
|
||
if err != nil {
|
||
response.Error(c, http.StatusNotFound, "街拍不存在")
|
||
return
|
||
}
|
||
|
||
detail, err := h.svc.Detail(c.Request.Context(), strconv.FormatUint(uint64(numeric), 10), parseLocale(c))
|
||
if err != nil {
|
||
// 向后兼容:迁移前收藏/历史/旧外链可能仍带 s= 前缀(旧 EncodeTyped)。
|
||
// 主线按类型化密码解码不到实体时,按旧编码再解一次回退(仅当旧类型确为 snap)。
|
||
if ltyp, lnum, lerr := hashid.DecodeTyped(raw); lerr == nil && ltyp == hashid.TypeSnap {
|
||
if d2, e2 := h.svc.Detail(c.Request.Context(), strconv.FormatUint(uint64(lnum), 10), parseLocale(c)); e2 == nil {
|
||
detail, err = d2, nil
|
||
}
|
||
}
|
||
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
|
||
}
|
||
|
||
// 已登录:图集(gallery 级 TypeSnap)与每张图片(image 级 TypeSnapImage)收藏态随详情返回,
|
||
// 前端无需再单独调 /me/favorites/checks。
|
||
if uid, ok := middleware.UserIDFrom(c); ok {
|
||
ids := make([]string, 0, len(detail.Images)+1)
|
||
ids = append(ids, detail.UID)
|
||
for _, im := range detail.Images {
|
||
if im.ID != "" {
|
||
ids = append(ids, im.ID)
|
||
}
|
||
}
|
||
if hit, err := h.favs.Check(c.Request.Context(), uid, ids); err == nil {
|
||
set := make(map[string]bool, len(hit))
|
||
for _, id := range hit {
|
||
set[id] = true
|
||
}
|
||
detail.Favorited = set[detail.UID]
|
||
for i := range detail.Images {
|
||
detail.Images[i].Favorited = set[detail.Images[i].ID]
|
||
}
|
||
}
|
||
}
|
||
|
||
response.DataPreview(c, http.StatusOK, detail, preview, imageTotal)
|
||
}
|