feat(street): 公开 API 详情新增主副图分组字段
This commit is contained in:
@ -60,11 +60,19 @@ type PublicStreetSnap struct {
|
|||||||
Images []PublicArticleImage `json:"images"` // 列表附带的每篇前 N 张缩略图
|
Images []PublicArticleImage `json:"images"` // 列表附带的每篇前 N 张缩略图
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicStreetSnapDetail 对外只读街拍详情:含完整图片集。
|
// PublicStreetSnapDetail 对外只读街拍详情:含完整图片集与主副图分组。
|
||||||
type PublicStreetSnapDetail struct {
|
type PublicStreetSnapDetail struct {
|
||||||
UID string `json:"id"`
|
UID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Cover string `json:"cover"`
|
Cover string `json:"cover"`
|
||||||
Images []PublicArticleImage `json:"images"`
|
Images []PublicArticleImage `json:"images"` // 扁平图片集(保持不变,向后兼容)
|
||||||
|
Groups []PublicStreetSnapGroup `json:"groups,omitempty"` // 按主副图分组(主图→其副图),与 Images 同源同序
|
||||||
Favorited bool `json:"favorited,omitempty"` // 已登录时该图集是否被当前用户收藏(gallery 级)
|
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())
|
return nil, internalErr(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flat, groups := s.groupSnapImages(imgs)
|
||||||
return &dto.PublicStreetSnapDetail{
|
return &dto.PublicStreetSnapDetail{
|
||||||
UID: hashid.EncodeWithType(snap.ID, hashid.TypeSnap),
|
UID: hashid.EncodeWithType(snap.ID, hashid.TypeSnap),
|
||||||
Title: snap.Title,
|
Title: snap.Title,
|
||||||
Cover: s.img.Compose(snap.Cover),
|
Cover: s.img.Compose(snap.Cover),
|
||||||
Images: s.toPublicSnapImages(imgs),
|
Images: flat,
|
||||||
|
Groups: groups,
|
||||||
}, nil
|
}, 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 枚举详情页路径使用,对外暴露的即编码串)。
|
// IDs 返回全部街拍的编码 uid(供 SSG 枚举详情页路径使用,对外暴露的即编码串)。
|
||||||
// Popular 返回热度前 N 个街拍(按图片数降序),供 SSG 首页热门街拍展示。
|
// Popular 返回热度前 N 个街拍(按图片数降序),供 SSG 首页热门街拍展示。
|
||||||
func (s *streetSnapService) Popular(ctx context.Context, limit int) ([]dto.PublicStreetSnap, error) {
|
func (s *streetSnapService) Popular(ctx context.Context, limit int) ([]dto.PublicStreetSnap, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user