From 5e4803b97f7a0237a097e43d43cca572652750ab Mon Sep 17 00:00:00 2001 From: toom1996 Date: Tue, 15 Sep 2026 19:59:39 +0800 Subject: [PATCH] update --- internal/dto/article.go | 33 ++++++++++++++--------------- internal/handler/article_handler.go | 21 ++++++++++++------ internal/service/article_service.go | 28 +++++++++++------------- 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/internal/dto/article.go b/internal/dto/article.go index a0b7e3c..bf72200 100644 --- a/internal/dto/article.go +++ b/internal/dto/article.go @@ -78,15 +78,14 @@ type PublicArticleImage struct { Image string `json:"image"` Thumb string `json:"thumb,omitempty"` Name string `json:"name"` - IsDetail uint8 `json:"is_detail"` // 0=主图 1=细节图(详情接口 images 仅含主图;细节在 Details 分组) - LookIndex uint32 `json:"look_index"` // 细节图归属的主图序号(用于前端分组展开) - Favorited bool `json:"favorited,omitempty"` // 已登录时该图片是否被当前用户收藏(image 级) -} + IsDetail uint8 `json:"is_detail"` // 0=主图 1=细节图(详情接口 images 仅含主图,细节图挂在所属主图的 Detail 下) + LookIndex uint32 `json:"look_index"` // 该图归属的主图序号;细节图与所属主图同值 + Favorited bool `json:"favorited,omitempty"` // 已登录时该图片是否被当前用户收藏(image 级) -// RunwayLookDetails 某个主图(look)对应的细节图集合,供详情页「细节入口」按需展开。 -type RunwayLookDetails struct { - LookIndex uint32 `json:"look_index"` // 主图序号(与 images 中对应主图的 look_index 一致) - Images []PublicArticleImage `json:"images"` // 该主图的细节图列表 + // Detail 仅详情接口填充:该主图(look)下的细节图子集,元素形状与 Images 递归同构。 + // 「look → 细节」的层级与前端 UI 一致,前端直接读它即可,无需再维护一份 + // 「按 look_index 分组」的映射表。列表接口不填(omitempty 后不出现)。 + Detail []PublicArticleImage `json:"detail,omitempty"` } // PublicArticle 对外展示用的精简文章结构(走秀列表)。 @@ -110,15 +109,15 @@ type PublicArticle struct { // 与列表一致,只返回前端渲染所需的字段;后台管理用的额外元数据(季节 / 年份 / 系列等) // 见 AdminRunway,由后台列表接口单独返回。 type PublicArticleDetail struct { - UID string `json:"id"` // 编码后的文章 id(无序串) - BrandUID string `json:"brand_id"` // 编码后的品牌 id(无序串) - Title string `json:"title"` - Description string `json:"description"` - Cover string `json:"cover"` - BrandName string `json:"brand_name"` - Images []PublicArticleImage `json:"images"` // 仅主图(look 图),默认展示 - Details []RunwayLookDetails `json:"details"` // 按主图分组的细节图(「细节入口」展开用);无细节时为空 - Favorited bool `json:"favorited,omitempty"` // 已登录时该图集是否被当前用户收藏(gallery 级,r= 前缀) + UID string `json:"id"` // 编码后的文章 id(无序串) + BrandUID string `json:"brand_id"` // 编码后的品牌 id(无序串) + Title string `json:"title"` + Description string `json:"description"` + Cover string `json:"cover"` + BrandName string `json:"brand_name"` + // Images 仅含主图(look 图),默认展示;每个主图的细节图挂在它自己的 Detail 子数组下。 + Images []PublicArticleImage `json:"images"` + Favorited bool `json:"favorited,omitempty"` // 已登录时该图集是否被当前用户收藏(gallery 级,r= 前缀) } // AdminRunway 后台管理用的走秀列表项:在 PublicArticle 基础上补回管理字段, diff --git a/internal/handler/article_handler.go b/internal/handler/article_handler.go index acddcce..39b3eff 100644 --- a/internal/handler/article_handler.go +++ b/internal/handler/article_handler.go @@ -101,8 +101,9 @@ func (h *ArticleHandler) Detail(c *gin.Context) { return } - // 未登录:图片集仅返回预览上限(PreviewLimit)张,并打 preview=true, - // 前端据此在详情页展示「登录查看全部图片」门禁;已登录(带有效 Bearer)返回完整图片集。 + // 未登录:主图只返回预览上限(PreviewLimit)张并打 preview=true,前端据此展示 + // 「登录查看全部图片」门禁。细节图(Detail 子数组)不额外剥离 —— 游客也能看前几张主图的细节, + // 体验更完整;而第 PreviewLimit 张之后的主图连同其细节图会被整组截断丢弃,故无法借此绕过门禁。 anon := isAnon(c) preview := false imageTotal := len(detail.Images) @@ -111,16 +112,21 @@ func (h *ArticleHandler) Detail(c *gin.Context) { preview = true } - // 已登录:把当前用户对该图集(gallery 级)与每张图片(image 级)的收藏态随详情返回, - // 前端无需再单独调 /me/favorites/checks。gallery uid(r=)与图片 id(i=)前缀不同, - // 混传 FilterExisting 不影响查询;匿名请求不查(保持公开接口无用户态、可缓存)。 + // 已登录:把当前用户对该图集(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 := make([]string, 0, len(detail.Images)*2+1) ids = append(ids, detail.UID) for _, im := range detail.Images { if im.ID != "" { ids = append(ids, im.ID) } + for _, kid := range im.Detail { + if kid.ID != "" { + ids = append(ids, kid.ID) + } + } } if hit, err := h.favs.Check(c.Request.Context(), uid, ids); err == nil { set := make(map[string]bool, len(hit)) @@ -130,6 +136,9 @@ func (h *ArticleHandler) Detail(c *gin.Context) { detail.Favorited = set[detail.UID] for i := range detail.Images { detail.Images[i].Favorited = set[detail.Images[i].ID] + for j := range detail.Images[i].Detail { + detail.Images[i].Detail[j].Favorited = set[detail.Images[i].Detail[j].ID] + } } } } diff --git a/internal/service/article_service.go b/internal/service/article_service.go index 97d8d4e..85b5716 100644 --- a/internal/service/article_service.go +++ b/internal/service/article_service.go @@ -5,7 +5,6 @@ import ( "errors" "log" "net/http" - "sort" "fashionapi/internal/dto" "fashionapi/internal/model" @@ -168,8 +167,9 @@ func (s *articleService) Detail(ctx context.Context, id string, locale string) ( return nil, internalErr(err.Error()) } - // 默认仅主图(is_detail=0)进 Images;细节图按 look_index 分组进 Details, - // 供前端「细节入口」按需展开。这样既满足「默认只展示主图」,又不丢失细节图数据。 + // 主图(is_detail=0)进 Images,默认展示;细节图(is_detail=1)按 look_index + // 直接挂到所属主图的 Detail 子数组下 —— 「look → 细节」的层级与前端 UI 一致, + // 前端读 img.detail 即可,无需再单独维护一份「按 look_index 分组」的映射表。 mains := make([]model.BrandRunwayImage, 0, len(imgs)) byLook := make(map[uint32][]model.BrandRunwayImage) for _, im := range imgs { @@ -179,17 +179,14 @@ func (s *articleService) Detail(ctx context.Context, id string, locale string) ( byLook[im.LookIndex] = append(byLook[im.LookIndex], im) } } - details := make([]dto.RunwayLookDetails, 0, len(byLook)) - lookIdxs := make([]uint32, 0, len(byLook)) - for k := range byLook { - lookIdxs = append(lookIdxs, k) - } - sort.Slice(lookIdxs, func(i, j int) bool { return lookIdxs[i] < lookIdxs[j] }) - for _, k := range lookIdxs { - details = append(details, dto.RunwayLookDetails{ - LookIndex: k, - Images: s.toPublicImages(byLook[k]), - }) + pubMains := s.toPublicImages(mains) + for i := range pubMains { + // 挂载后即从 byLook 摘除:主图 look_index 若有重复,只归属第一张,避免同一批 + // 细节图被复制进多个主图(正常数据不会重复,此处仅作防御)。 + if kids, ok := byLook[pubMains[i].LookIndex]; ok && len(kids) > 0 { + pubMains[i].Detail = s.toPublicImages(kids) + delete(byLook, pubMains[i].LookIndex) + } } return &dto.PublicArticleDetail{ @@ -200,8 +197,7 @@ func (s *articleService) Detail(ctx context.Context, id string, locale string) ( Cover: s.img.Compose(row.Cover), // 品牌名强制优先英文("en"),不随 locale 切换,保持英文观感。 BrandName: pickLocale(row.BrandNameEn, row.BrandNameCn, "en"), - Images: s.toPublicImages(mains), - Details: details, + Images: pubMains, }, nil }