diff --git a/cmd/server/main.go b/cmd/server/main.go index 28c239d..1a08c41 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -144,9 +144,9 @@ func main() { engine := router.New(router.Options{ Config: cfg, JWT: jwtManager, - Article: handler.NewArticleHandler(articleSvc), + Article: handler.NewArticleHandler(articleSvc, favSvc), Brand: handler.NewBrandHandler(brandSvc), - StreetSnap: handler.NewStreetSnapHandler(snapSvc), + StreetSnap: handler.NewStreetSnapHandler(snapSvc, favSvc), Auth: handler.NewAuthHandler(authSvc), Favorite: handler.NewFavoriteHandler(favSvc), History: handler.NewHistoryHandler(histSvc), diff --git a/internal/dto/article.go b/internal/dto/article.go index 103f1c6..a0b7e3c 100644 --- a/internal/dto/article.go +++ b/internal/dto/article.go @@ -80,6 +80,7 @@ type PublicArticleImage struct { Name string `json:"name"` IsDetail uint8 `json:"is_detail"` // 0=主图 1=细节图(详情接口 images 仅含主图;细节在 Details 分组) LookIndex uint32 `json:"look_index"` // 细节图归属的主图序号(用于前端分组展开) + Favorited bool `json:"favorited,omitempty"` // 已登录时该图片是否被当前用户收藏(image 级) } // RunwayLookDetails 某个主图(look)对应的细节图集合,供详情页「细节入口」按需展开。 @@ -117,6 +118,7 @@ type PublicArticleDetail struct { BrandName string `json:"brand_name"` Images []PublicArticleImage `json:"images"` // 仅主图(look 图),默认展示 Details []RunwayLookDetails `json:"details"` // 按主图分组的细节图(「细节入口」展开用);无细节时为空 + Favorited bool `json:"favorited,omitempty"` // 已登录时该图集是否被当前用户收藏(gallery 级,r= 前缀) } // AdminRunway 后台管理用的走秀列表项:在 PublicArticle 基础上补回管理字段, diff --git a/internal/dto/street_snap.go b/internal/dto/street_snap.go index 2fb9aef..be02360 100644 --- a/internal/dto/street_snap.go +++ b/internal/dto/street_snap.go @@ -66,4 +66,5 @@ type PublicStreetSnapDetail struct { Title string `json:"title"` Cover string `json:"cover"` Images []PublicArticleImage `json:"images"` + Favorited bool `json:"favorited,omitempty"` // 已登录时该图集是否被当前用户收藏(gallery 级,s= 前缀) } diff --git a/internal/handler/article_handler.go b/internal/handler/article_handler.go index cea03b7..acddcce 100644 --- a/internal/handler/article_handler.go +++ b/internal/handler/article_handler.go @@ -17,11 +17,12 @@ import ( // ArticleHandler 对外公开的文章接口。 type ArticleHandler struct { articles service.ArticleService + favs service.FavoriteService } // NewArticleHandler 创建文章 handler。 -func NewArticleHandler(articles service.ArticleService) *ArticleHandler { - return &ArticleHandler{articles: articles} +func NewArticleHandler(articles service.ArticleService, favs service.FavoriteService) *ArticleHandler { + return &ArticleHandler{articles: articles, favs: favs} } // List 走秀列表(分页 + 单值筛选 + 排序)。 @@ -109,5 +110,29 @@ func (h *ArticleHandler) Detail(c *gin.Context) { detail.Images = detail.Images[:dto.PreviewLimit] 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) } diff --git a/internal/handler/street_snap_handler.go b/internal/handler/street_snap_handler.go index fd4b8c2..7aa9bf7 100644 --- a/internal/handler/street_snap_handler.go +++ b/internal/handler/street_snap_handler.go @@ -6,6 +6,7 @@ import ( "strings" "fashionapi/internal/dto" + "fashionapi/internal/middleware" "fashionapi/internal/pkg/hashid" "fashionapi/internal/pkg/response" "fashionapi/internal/service" @@ -15,12 +16,13 @@ import ( // StreetSnapHandler 对外公开的街拍接口。 type StreetSnapHandler struct { - svc service.StreetSnapService + svc service.StreetSnapService + favs service.FavoriteService } // NewStreetSnapHandler 创建街拍 handler。 -func NewStreetSnapHandler(svc service.StreetSnapService) *StreetSnapHandler { - return &StreetSnapHandler{svc: svc} +func NewStreetSnapHandler(svc service.StreetSnapService, favs service.FavoriteService) *StreetSnapHandler { + return &StreetSnapHandler{svc: svc, favs: favs} } // List 街拍列表(分页 + 排序 + 年份过滤)。 @@ -93,5 +95,28 @@ func (h *StreetSnapHandler) Detail(c *gin.Context) { detail.Images = detail.Images[:dto.PreviewLimit] 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) } diff --git a/internal/router/router.go b/internal/router/router.go index d058cb3..a6cd763 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -141,7 +141,6 @@ func New(opt Options) *gin.Engine { legacy.GET("/favorites", opt.Favorite.List) legacy.POST("/favorites", opt.Favorite.Add) legacy.DELETE("/favorites/:uid", opt.Favorite.Remove) - legacy.POST("/favorites/check", opt.Favorite.Check) legacy.GET("/history", opt.History.List) legacy.POST("/history", opt.History.Record)