This commit is contained in:
toom1996
2026-09-13 21:38:32 +08:00
parent 580697f17a
commit 0185ecd0df
6 changed files with 60 additions and 8 deletions

View File

@ -144,9 +144,9 @@ func main() {
engine := router.New(router.Options{ engine := router.New(router.Options{
Config: cfg, Config: cfg,
JWT: jwtManager, JWT: jwtManager,
Article: handler.NewArticleHandler(articleSvc), Article: handler.NewArticleHandler(articleSvc, favSvc),
Brand: handler.NewBrandHandler(brandSvc), Brand: handler.NewBrandHandler(brandSvc),
StreetSnap: handler.NewStreetSnapHandler(snapSvc), StreetSnap: handler.NewStreetSnapHandler(snapSvc, favSvc),
Auth: handler.NewAuthHandler(authSvc), Auth: handler.NewAuthHandler(authSvc),
Favorite: handler.NewFavoriteHandler(favSvc), Favorite: handler.NewFavoriteHandler(favSvc),
History: handler.NewHistoryHandler(histSvc), History: handler.NewHistoryHandler(histSvc),

View File

@ -80,6 +80,7 @@ type PublicArticleImage struct {
Name string `json:"name"` Name string `json:"name"`
IsDetail uint8 `json:"is_detail"` // 0=主图 1=细节图(详情接口 images 仅含主图;细节在 Details 分组) IsDetail uint8 `json:"is_detail"` // 0=主图 1=细节图(详情接口 images 仅含主图;细节在 Details 分组)
LookIndex uint32 `json:"look_index"` // 细节图归属的主图序号(用于前端分组展开) LookIndex uint32 `json:"look_index"` // 细节图归属的主图序号(用于前端分组展开)
Favorited bool `json:"favorited,omitempty"` // 已登录时该图片是否被当前用户收藏(image 级)
} }
// RunwayLookDetails 某个主图(look)对应的细节图集合,供详情页「细节入口」按需展开。 // RunwayLookDetails 某个主图(look)对应的细节图集合,供详情页「细节入口」按需展开。
@ -117,6 +118,7 @@ type PublicArticleDetail struct {
BrandName string `json:"brand_name"` BrandName string `json:"brand_name"`
Images []PublicArticleImage `json:"images"` // 仅主图(look 图),默认展示 Images []PublicArticleImage `json:"images"` // 仅主图(look 图),默认展示
Details []RunwayLookDetails `json:"details"` // 按主图分组的细节图(「细节入口」展开用);无细节时为空 Details []RunwayLookDetails `json:"details"` // 按主图分组的细节图(「细节入口」展开用);无细节时为空
Favorited bool `json:"favorited,omitempty"` // 已登录时该图集是否被当前用户收藏(gallery 级,r= 前缀)
} }
// AdminRunway 后台管理用的走秀列表项:在 PublicArticle 基础上补回管理字段, // AdminRunway 后台管理用的走秀列表项:在 PublicArticle 基础上补回管理字段,

View File

@ -66,4 +66,5 @@ type PublicStreetSnapDetail struct {
Title string `json:"title"` Title string `json:"title"`
Cover string `json:"cover"` Cover string `json:"cover"`
Images []PublicArticleImage `json:"images"` Images []PublicArticleImage `json:"images"`
Favorited bool `json:"favorited,omitempty"` // 已登录时该图集是否被当前用户收藏(gallery 级,s= 前缀)
} }

View File

@ -17,11 +17,12 @@ import (
// ArticleHandler 对外公开的文章接口。 // ArticleHandler 对外公开的文章接口。
type ArticleHandler struct { type ArticleHandler struct {
articles service.ArticleService articles service.ArticleService
favs service.FavoriteService
} }
// NewArticleHandler 创建文章 handler。 // NewArticleHandler 创建文章 handler。
func NewArticleHandler(articles service.ArticleService) *ArticleHandler { func NewArticleHandler(articles service.ArticleService, favs service.FavoriteService) *ArticleHandler {
return &ArticleHandler{articles: articles} return &ArticleHandler{articles: articles, favs: favs}
} }
// List 走秀列表(分页 + 单值筛选 + 排序)。 // List 走秀列表(分页 + 单值筛选 + 排序)。
@ -109,5 +110,29 @@ func (h *ArticleHandler) Detail(c *gin.Context) {
detail.Images = detail.Images[:dto.PreviewLimit] detail.Images = detail.Images[:dto.PreviewLimit]
preview = true preview = true
} }
// 已登录:把当前用户对该图集(gallery 级)与每张图片(image 级)的收藏态随详情返回,
// 前端无需再单独调 /me/favorites/checks。gallery uid(r=)与图片 id(i=)前缀不同,
// 混传 FilterExisting 不影响查询;匿名请求不查(保持公开接口无用户态、可缓存)。
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) response.DataPreview(c, http.StatusOK, detail, preview, imageTotal)
} }

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"fashionapi/internal/dto" "fashionapi/internal/dto"
"fashionapi/internal/middleware"
"fashionapi/internal/pkg/hashid" "fashionapi/internal/pkg/hashid"
"fashionapi/internal/pkg/response" "fashionapi/internal/pkg/response"
"fashionapi/internal/service" "fashionapi/internal/service"
@ -15,12 +16,13 @@ import (
// StreetSnapHandler 对外公开的街拍接口。 // StreetSnapHandler 对外公开的街拍接口。
type StreetSnapHandler struct { type StreetSnapHandler struct {
svc service.StreetSnapService svc service.StreetSnapService
favs service.FavoriteService
} }
// NewStreetSnapHandler 创建街拍 handler。 // NewStreetSnapHandler 创建街拍 handler。
func NewStreetSnapHandler(svc service.StreetSnapService) *StreetSnapHandler { func NewStreetSnapHandler(svc service.StreetSnapService, favs service.FavoriteService) *StreetSnapHandler {
return &StreetSnapHandler{svc: svc} return &StreetSnapHandler{svc: svc, favs: favs}
} }
// List 街拍列表(分页 + 排序 + 年份过滤)。 // List 街拍列表(分页 + 排序 + 年份过滤)。
@ -93,5 +95,28 @@ func (h *StreetSnapHandler) Detail(c *gin.Context) {
detail.Images = detail.Images[:dto.PreviewLimit] detail.Images = detail.Images[:dto.PreviewLimit]
preview = true preview = true
} }
// 已登录:图集(gallery 级 s=)与每张图片(image 级 j=)收藏态随详情返回,
// 前端无需再单独调 /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) response.DataPreview(c, http.StatusOK, detail, preview, imageTotal)
} }

View File

@ -141,7 +141,6 @@ func New(opt Options) *gin.Engine {
legacy.GET("/favorites", opt.Favorite.List) legacy.GET("/favorites", opt.Favorite.List)
legacy.POST("/favorites", opt.Favorite.Add) legacy.POST("/favorites", opt.Favorite.Add)
legacy.DELETE("/favorites/:uid", opt.Favorite.Remove) legacy.DELETE("/favorites/:uid", opt.Favorite.Remove)
legacy.POST("/favorites/check", opt.Favorite.Check)
legacy.GET("/history", opt.History.List) legacy.GET("/history", opt.History.List)
legacy.POST("/history", opt.History.Record) legacy.POST("/history", opt.History.Record)