fix(publish): 后台详情与品牌列表改读基表,修复未发布内容在后台不可达

This commit is contained in:
toom1996
2026-09-23 10:16:09 +08:00
parent f5144038ab
commit 4169ebe1ed
9 changed files with 287 additions and 52 deletions

View File

@ -24,6 +24,8 @@ type MediaCleanupEnqueuer interface {
type ArticleService interface {
List(ctx context.Context, q dto.ArticleQuery) ([]dto.PublicArticle, int64, error)
Detail(ctx context.Context, id string, locale string) (*dto.PublicArticleDetail, error)
// DetailAdmin 后台详情:读基表,能渲染 pending / rejected(供后台详情页复用)。
DetailAdmin(ctx context.Context, id string, locale string) (*dto.PublicArticleDetail, error)
// AdminList 后台管理列表:返回含季节 / 年份 / 系列等管理字段的走秀项。
AdminList(ctx context.Context, q dto.ArticleQuery) ([]dto.AdminRunway, int64, error)
// GetForEdit 取原始走秀记录与图片,供后台编辑表单预填。
@ -151,22 +153,46 @@ func excludeCover(imgs []dto.PublicArticleImage, cover string) []dto.PublicArtic
return kept
}
// Detail 查询文章详情,含完整图片集。locale 决定标题/描述/品牌名的展示语言;
// 图片统一走展示样式(VIP 与免费同质量)。
// Detail 公开详情:读公开视图,pending / rejected 视同不存在(ErrArticleNotFound)。
// locale 决定标题/描述/品牌名的展示语言;图片统一走展示样式(VIP 与免费同质量)。
func (s *articleService) Detail(ctx context.Context, id string, locale string) (*dto.PublicArticleDetail, error) {
row, err := s.articles.FindByID(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, ErrArticleNotFound
}
return nil, NewError(http.StatusInternalServerError, err.Error())
return nil, s.detailErr(err)
}
imgs, err := s.articles.ListImages(ctx, id)
if err != nil {
return nil, internalErr(err.Error())
}
return s.buildDetail(row, imgs, locale), nil
}
// DetailAdmin 后台详情:读基表,能渲染 pending / rejected。
//
// 后台列表(AdminList)故意把待审行链到详情页,且详情页是通往编辑页的唯一入口;
// 若后台详情也走公开视图,待审项会 404、审核无从下手。
func (s *articleService) DetailAdmin(ctx context.Context, id string, locale string) (*dto.PublicArticleDetail, error) {
row, err := s.articles.FindByIDBase(ctx, id)
if err != nil {
return nil, s.detailErr(err)
}
imgs, err := s.articles.ListImagesBase(ctx, id)
if err != nil {
return nil, internalErr(err.Error())
}
return s.buildDetail(row, imgs, locale), nil
}
// detailErr 统一详情取数失败的错误映射(公开 Detail 与 DetailAdmin 共用)。
func (s *articleService) detailErr(err error) error {
if errors.Is(err, repository.ErrNotFound) {
return ErrArticleNotFound
}
return NewError(http.StatusInternalServerError, err.Error())
}
// buildDetail 把查询行与图片集映射为详情 DTO(公开/后台共用,差异只在数据来源)。
func (s *articleService) buildDetail(row *model.RunwayRow, imgs []model.BrandRunwayImage, locale string) *dto.PublicArticleDetail {
// 主图(is_detail=0)进 Images,默认展示;细节图(is_detail=1)按 look_index
// 直接挂到所属主图的 Detail 子数组下 —— 「look → 细节」的层级与前端 UI 一致,
// 前端读 img.detail 即可,无需再单独维护一份「按 look_index 分组」的映射表。
@ -198,7 +224,7 @@ func (s *articleService) Detail(ctx context.Context, id string, locale string) (
// 品牌名强制优先英文("en"),不随 locale 切换,保持英文观感。
BrandName: pickLocale(row.BrandNameEn, row.BrandNameCn, "en"),
Images: pubMains,
}, nil
}
}
// ── 后台编辑支持 ──────────────────────────────────────────────────────────────

View File

@ -16,6 +16,9 @@ import (
// BrandService 品牌业务接口。
type BrandService interface {
List(ctx context.Context, q dto.BrandQuery) ([]dto.PublicBrand, int64, error)
// AdminList 后台品牌列表:字母索引不要求走秀已发布,
// 使只有待审 / 驳回走秀的品牌仍出现在后台(公开 List 读视图,会挡掉它们)。
AdminList(ctx context.Context, q dto.BrandQuery) ([]dto.PublicBrand, int64, error)
// Hot 返回热门品牌(按走秀图片数排名前 limit 个)并附带各品牌档案数。
// 供 SSG 构建期(RunwayLooks 侧栏 / 品牌弹窗 HOT 标签)一次性取前 N 个热门品牌;
// 复用 List 的 featured 收窄逻辑(FeaturedIDs("images", limit)),与首页精选口径一致。
@ -70,6 +73,18 @@ func (s *brandService) List(ctx context.Context, q dto.BrandQuery) ([]dto.Public
return s.toPublicBrands(items, q.Locale), total, nil
}
// AdminList 后台品牌列表:与 List 同形,但走 ListAdmin —— 字母索引不过滤走秀发布态,
// 只有待审 / 驳回走秀的品牌仍可见。后台不做 featured 收窄,故 restrictIDs 传 nil。
func (s *brandService) AdminList(ctx context.Context, q dto.BrandQuery) ([]dto.PublicBrand, int64, error) {
q.Normalize()
items, total, err := s.brands.ListAdmin(ctx, q, nil)
if err != nil {
return nil, 0, internalErr(err.Error())
}
return s.toPublicBrands(items, q.Locale), total, nil
}
// Hot 返回热门品牌:按走秀图片总数排名取前 limit 个,结果按热度顺序(而非字母序)排列。
//
// 供 SSG 构建期(走秀页侧栏品牌 filter / 品牌弹窗 HOT 标签)一次性取用——

View File

@ -20,6 +20,8 @@ type StreetSnapService interface {
// AdminList 后台街拍列表:读基表,包含 pending / rejected(否则审核员看不到待审内容)。
AdminList(ctx context.Context, q dto.StreetSnapQuery) ([]dto.PublicStreetSnap, int64, error)
Detail(ctx context.Context, id string) (*dto.PublicStreetSnapDetail, error)
// DetailAdmin 后台详情:读基表,能渲染 pending / rejected(供后台详情页复用)。
DetailAdmin(ctx context.Context, id string) (*dto.PublicStreetSnapDetail, error)
// Popular 返回热度前 N 个街拍(按图片数降序),供 SSG 首页热门街拍展示。
Popular(ctx context.Context, limit int) ([]dto.PublicStreetSnap, error)
// GetForEdit 取原始街拍记录与图片,供后台编辑表单预填。
@ -144,27 +146,52 @@ func excludeCoverSnap(imgs []dto.PublicArticleImage, cover string) []dto.PublicA
return kept
}
// Detail 查询街拍详情,含完整图片集。图片统一走展示样式(VIP 与免费同质量)。
// Detail 公开详情:读公开视图,pending / rejected 视同不存在(ErrStreetSnapNotFound)。
// 图片统一走展示样式(VIP 与免费同质量)。
func (s *streetSnapService) Detail(ctx context.Context, id string) (*dto.PublicStreetSnapDetail, error) {
snap, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, ErrStreetSnapNotFound
}
return nil, internalErr(err.Error())
return nil, s.detailErr(err)
}
imgs, err := s.repo.ListImages(ctx, id)
if err != nil {
return nil, internalErr(err.Error())
}
return s.buildSnapDetail(snap, imgs), nil
}
// DetailAdmin 后台详情:读基表,能渲染 pending / rejected。
//
// 后台列表(AdminList)故意把待审行链到详情页,且详情页是通往编辑页的唯一入口;
// 若后台详情也走公开视图,待审项会 404、审核无从下手。
func (s *streetSnapService) DetailAdmin(ctx context.Context, id string) (*dto.PublicStreetSnapDetail, error) {
snap, err := s.repo.FindByIDBase(ctx, id)
if err != nil {
return nil, s.detailErr(err)
}
imgs, err := s.repo.ListImagesBase(ctx, id)
if err != nil {
return nil, internalErr(err.Error())
}
return s.buildSnapDetail(snap, imgs), nil
}
// detailErr 统一详情取数失败的错误映射(公开 Detail 与 DetailAdmin 共用)。
func (s *streetSnapService) detailErr(err error) error {
if errors.Is(err, repository.ErrNotFound) {
return ErrStreetSnapNotFound
}
return internalErr(err.Error())
}
// buildSnapDetail 把街拍行与图片集映射为详情 DTO(公开/后台共用,差异只在数据来源)。
func (s *streetSnapService) buildSnapDetail(snap *model.StreetSnap, imgs []model.StreetSnapImage) *dto.PublicStreetSnapDetail {
return &dto.PublicStreetSnapDetail{
UID: hashid.EncodeWithType(snap.ID, hashid.TypeSnap),
Title: snap.Title,
Cover: s.img.Compose(snap.Cover),
Images: s.buildSnapImages(imgs),
}, nil
}
}
// buildSnapImages 把街拍图片组装成「主图 + 其副图」的嵌套结构(与走秀详情同形):