feat(street): 公开 API 详情新增主副图分组字段
This commit is contained in:
@ -60,11 +60,19 @@ type PublicStreetSnap struct {
|
||||
Images []PublicArticleImage `json:"images"` // 列表附带的每篇前 N 张缩略图
|
||||
}
|
||||
|
||||
// PublicStreetSnapDetail 对外只读街拍详情:含完整图片集。
|
||||
// PublicStreetSnapDetail 对外只读街拍详情:含完整图片集与主副图分组。
|
||||
type PublicStreetSnapDetail struct {
|
||||
UID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Cover string `json:"cover"`
|
||||
Images []PublicArticleImage `json:"images"`
|
||||
UID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Cover string `json:"cover"`
|
||||
Images []PublicArticleImage `json:"images"` // 扁平图片集(保持不变,向后兼容)
|
||||
Groups []PublicStreetSnapGroup `json:"groups,omitempty"` // 按主副图分组(主图→其副图),与 Images 同源同序
|
||||
Favorited bool `json:"favorited,omitempty"` // 已登录时该图集是否被当前用户收藏(gallery 级)
|
||||
}
|
||||
|
||||
// PublicStreetSnapGroup 一张主图及其副图,供前端按组折叠展示。
|
||||
// Details 为空表示该主图没有副图(等价于 Images 里的一张独立图)。
|
||||
type PublicStreetSnapGroup struct {
|
||||
Image PublicArticleImage `json:"image"` // 主图
|
||||
Details []PublicArticleImage `json:"details"` // 该主图名下的副图(可为空)
|
||||
}
|
||||
|
||||
67
internal/service/street_snap_group_test.go
Normal file
67
internal/service/street_snap_group_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fashionapi/internal/model"
|
||||
"fashionapi/internal/pkg/imgurl"
|
||||
)
|
||||
|
||||
// TestGroupSnapImages 验证主副图分组与「副图的副图」上浮归并。
|
||||
//
|
||||
// 5 张图:A 主,B 副(挂 A);C 主;D 副(挂 C);E 副(挂 B → 应上浮归并到 A)
|
||||
//
|
||||
// 期望 2 组:第 0 组 = A + [B, E],第 1 组 = C + [D]。
|
||||
func TestGroupSnapImages(t *testing.T) {
|
||||
s := &streetSnapService{img: imgurl.New("", "", "")}
|
||||
imgs := []model.StreetSnapImage{
|
||||
{ID: 1, Image: "A", Name: "A", IsDetail: 0, ParentImageID: 0},
|
||||
{ID: 2, Image: "B", Name: "B", IsDetail: 1, ParentImageID: 1},
|
||||
{ID: 3, Image: "C", Name: "C", IsDetail: 0, ParentImageID: 0},
|
||||
{ID: 4, Image: "D", Name: "D", IsDetail: 1, ParentImageID: 3},
|
||||
{ID: 5, Image: "E", Name: "E", IsDetail: 1, ParentImageID: 2}, // 2 是副图 → 上浮到 A
|
||||
}
|
||||
|
||||
flat, groups := s.groupSnapImages(imgs)
|
||||
if len(flat) != 5 {
|
||||
t.Fatalf("flat 应 5 张,实际 %d", len(flat))
|
||||
}
|
||||
if len(groups) != 2 {
|
||||
t.Fatalf("groups 应为 2 组,实际 %d", len(groups))
|
||||
}
|
||||
|
||||
g0 := groups[0]
|
||||
if g0.Image.Name != "A" {
|
||||
t.Fatalf("第 0 组主图应为 A,实际 %s", g0.Image.Name)
|
||||
}
|
||||
if len(g0.Details) != 2 {
|
||||
t.Fatalf("第 0 组副图应为 2 张,实际 %d", len(g0.Details))
|
||||
}
|
||||
if g0.Details[0].Name != "B" || g0.Details[1].Name != "E" {
|
||||
t.Fatalf("第 0 组副图顺序应为 [B, E],实际 %s, %s", g0.Details[0].Name, g0.Details[1].Name)
|
||||
}
|
||||
|
||||
g1 := groups[1]
|
||||
if g1.Image.Name != "C" {
|
||||
t.Fatalf("第 1 组主图应为 C,实际 %s", g1.Image.Name)
|
||||
}
|
||||
if len(g1.Details) != 1 || g1.Details[0].Name != "D" {
|
||||
t.Fatalf("第 1 组副图应为 [D],实际 %d 张", len(g1.Details))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupSnapImagesOrphan 父引用指向失效行(不存在)时,该图按主图渲染,不丢图。
|
||||
func TestGroupSnapImagesOrphan(t *testing.T) {
|
||||
s := &streetSnapService{img: imgurl.New("", "", "")}
|
||||
imgs := []model.StreetSnapImage{
|
||||
{ID: 1, Image: "A", Name: "A", IsDetail: 1, ParentImageID: 99}, // 父 99 不存在
|
||||
{ID: 2, Image: "B", Name: "B", IsDetail: 0, ParentImageID: 0},
|
||||
}
|
||||
flat, groups := s.groupSnapImages(imgs)
|
||||
if len(flat) != 2 || len(groups) != 2 {
|
||||
t.Fatalf("孤儿副图应自成主图:flat=%d groups=%d", len(flat), len(groups))
|
||||
}
|
||||
if groups[0].Image.Name != "A" || len(groups[0].Details) != 0 {
|
||||
t.Fatalf("A 应作为主图且无副图,实际 %s details=%d", groups[0].Image.Name, len(groups[0].Details))
|
||||
}
|
||||
}
|
||||
@ -134,14 +134,72 @@ func (s *streetSnapService) Detail(ctx context.Context, id string) (*dto.PublicS
|
||||
return nil, internalErr(err.Error())
|
||||
}
|
||||
|
||||
flat, groups := s.groupSnapImages(imgs)
|
||||
return &dto.PublicStreetSnapDetail{
|
||||
UID: hashid.EncodeWithType(snap.ID, hashid.TypeSnap),
|
||||
Title: snap.Title,
|
||||
Cover: s.img.Compose(snap.Cover),
|
||||
Images: s.toPublicSnapImages(imgs),
|
||||
Images: flat,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// groupSnapImages 把街拍图片按主副图分组:主图各成一组,副图归入其(存活)主图组。
|
||||
// 父引用指向「副图」时沿链向上归并到该副图所在组的主图;父行缺失(被软删)则把该图按主图渲染(不丢图)。
|
||||
// 返回的 flat(扁平)与 groups 同源同序:groups 中每张主图对应的 Details 顺序,等于 flat 中该主图之后的副图出现顺序。
|
||||
func (s *streetSnapService) groupSnapImages(imgs []model.StreetSnapImage) ([]dto.PublicArticleImage, []dto.PublicStreetSnapGroup) {
|
||||
if len(imgs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
flat := s.toPublicSnapImages(imgs)
|
||||
|
||||
byID := make(map[uint32]*model.StreetSnapImage, len(imgs))
|
||||
for i := range imgs {
|
||||
byID[imgs[i].ID] = &imgs[i]
|
||||
}
|
||||
|
||||
// resolveRoot 返回 img 所属主图的行 id;自身是主图则返回自身;父行缺失 / 成环则按主图渲染(返回自身)。
|
||||
resolveRoot := func(start uint32) uint32 {
|
||||
cur := start
|
||||
seen := map[uint32]bool{}
|
||||
for {
|
||||
n, ok := byID[cur]
|
||||
if !ok {
|
||||
return start // 父行不存在 → 按主图渲染(返回自身)
|
||||
}
|
||||
if n.IsDetail == 0 {
|
||||
return n.ID
|
||||
}
|
||||
if seen[cur] {
|
||||
return start // 环 → 容错,自成主图
|
||||
}
|
||||
seen[cur] = true
|
||||
cur = n.ParentImageID
|
||||
}
|
||||
}
|
||||
|
||||
groups := make([]dto.PublicStreetSnapGroup, 0, len(imgs))
|
||||
index := make(map[uint32]int, len(imgs))
|
||||
for i, im := range imgs {
|
||||
root := resolveRoot(im.ID)
|
||||
isDetail := root != im.ID
|
||||
ref := flat[i]
|
||||
if !isDetail {
|
||||
groups = append(groups, dto.PublicStreetSnapGroup{Image: ref})
|
||||
index[im.ID] = len(groups) - 1
|
||||
continue
|
||||
}
|
||||
if gi, ok := index[root]; ok {
|
||||
groups[gi].Details = append(groups[gi].Details, ref)
|
||||
} else {
|
||||
// 兜底(理论不发生):主图未先出现,自成一组
|
||||
groups = append(groups, dto.PublicStreetSnapGroup{Image: ref})
|
||||
index[im.ID] = len(groups) - 1
|
||||
}
|
||||
}
|
||||
return flat, groups
|
||||
}
|
||||
|
||||
// IDs 返回全部街拍的编码 uid(供 SSG 枚举详情页路径使用,对外暴露的即编码串)。
|
||||
// Popular 返回热度前 N 个街拍(按图片数降序),供 SSG 首页热门街拍展示。
|
||||
func (s *streetSnapService) Popular(ctx context.Context, limit int) ([]dto.PublicStreetSnap, error) {
|
||||
|
||||
Reference in New Issue
Block a user