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

@ -13,12 +13,16 @@ import (
// BrandRepository 品牌数据访问接口。
type BrandRepository interface {
// List 按字母索引 / 关键词分页查询品牌。
// List 按字母索引 / 关键词分页查询品牌(公开侧:字母索引只认已发布走秀档案)。
//
// restrictIDs 用于 featured 模式:非 nil 时结果被限制在这些品牌 id 内;
// 若 q.Featured 为 true 而 restrictIDs 为空,则返回空结果——绝不能退化成"全部品牌"。
List(ctx context.Context, q dto.BrandQuery, restrictIDs []uint32) ([]model.Brand, int64, error)
// ListAdmin 后台列表:与 List 同构,但字母索引不要求走秀已发布,
// 使只有待审 / 驳回走秀的品牌仍出现在后台(公开侧保护不受影响)。
ListAdmin(ctx context.Context, q dto.BrandQuery, restrictIDs []uint32) ([]model.Brand, int64, error)
// FeaturedIDs 返回"代表品牌"的 id 集合:在拥有走秀档案的品牌中,
// 按 metric(images = 图片总数 / shows = 发布会数)排名取前 limit 个。
FeaturedIDs(ctx context.Context, metric string, limit int) ([]uint32, error)
@ -71,7 +75,7 @@ func NewBrandRepository(db *gorm.DB) BrandRepository {
// 排序与索引统一基于 name_en(拉丁规范名);name_en 为空的纯中文品牌归入 OTHER 桶。
const latinPrefixRegexp = "^[A-Za-z]"
// hasArticlesSubQuery 子查询:所有「至少有一篇已发布走秀档案」的品牌 id。
// hasArticlesSubQuery 子查询(公开侧):所有「至少有一篇已发布走秀档案」的品牌 id。
// 库中存在 2 万+ 无档案品牌,索引必须靠它收敛。
// 读公开视图而非基表:否则只有待审走秀的品牌会提前出现在前台的 A-Z 索引里。
func (r *brandRepository) hasArticlesSubQuery() *gorm.DB {
@ -79,8 +83,20 @@ func (r *brandRepository) hasArticlesSubQuery() *gorm.DB {
Select("DISTINCT brand_id")
}
// hasArticlesSubQueryBase 子查询(后台侧):所有「至少有一篇未删除走秀档案」的品牌 id。
// 读基表且不看 status:后台品牌列表必须仍能看到只有待审 / 驳回走秀的品牌,
// 否则它们会从后台「消失」,审核员无处下手(与公开侧的保护并不冲突:公开走视图)。
func (r *brandRepository) hasArticlesSubQueryBase() *gorm.DB {
return r.db.Model(&model.BrandRunway{}).
Select("DISTINCT brand_id").
Where("is_deleted = 0")
}
// brandFilterScope 把品牌查询条件编译为 GORM Scope。
func (r *brandRepository) brandFilterScope(q dto.BrandQuery, restrictIDs []uint32) func(*gorm.DB) *gorm.DB {
//
// admin 为 true 时(后台列表)「必须有走秀档案」的子查询读基表且不过滤 status,
// 使只有待审 / 驳回走秀的品牌仍出现在后台;公开列表保持读视图(保护未发布内容)。
func (r *brandRepository) brandFilterScope(q dto.BrandQuery, restrictIDs []uint32, admin bool) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
db = db.Where("is_deleted = 0")
@ -96,12 +112,19 @@ func (r *brandRepository) brandFilterScope(q dto.BrandQuery, restrictIDs []uint3
}
// 字母索引始终只返回"拥有走秀档案"的品牌(库中有 2 万+ 无档案品牌,
// 不过滤会污染 A-Z 索引)。原 only_with_articles 参数已删除,此过滤无条件生效。
// 不过滤会污染 A-Z 索引)。原 only_with_articles 参数已删除,此过滤无条件生效;
// 唯一区别是"档案"的口径:公开=已发布(视图),后台=未删除(基表,含待审)。
// 例外:关键词搜索时不限制"必须有走秀档案"(用户按名字找品牌,
// 即使该品牌暂无走秀档案也应被命中,如 A Bathing Ape / 猿人头);
// 中文分桶(OTHER)同样列出全部中文名品牌便于检索。
if q.Keyword == "" && q.Letter != "OTHER" {
db = db.Where("id IN (?)", r.hasArticlesSubQuery())
if admin {
// 后台:只要求「存在未删除走秀档案」,待审 / 驳回的品牌也带出来。
db = db.Where("id IN (?)", r.hasArticlesSubQueryBase())
} else {
// 公开:只认已发布走秀(视图),未发布内容不得出现在前台 A-Z 索引。
db = db.Where("id IN (?)", r.hasArticlesSubQuery())
}
}
if q.Keyword != "" {
@ -121,8 +144,19 @@ func (r *brandRepository) brandFilterScope(q dto.BrandQuery, restrictIDs []uint3
}
}
// List 公开品牌列表:字母索引只保留「至少有一篇已发布走秀档案」的品牌。
func (r *brandRepository) List(ctx context.Context, q dto.BrandQuery, restrictIDs []uint32) ([]model.Brand, int64, error) {
scope := r.brandFilterScope(q, restrictIDs)
return r.list(ctx, q, restrictIDs, false)
}
// ListAdmin 后台品牌列表:与 List 同构,但字母索引不要求走秀已发布,
// 只有待审 / 驳回走秀的品牌仍能看到(否则它们从后台列表消失,无处审核/编辑)。
func (r *brandRepository) ListAdmin(ctx context.Context, q dto.BrandQuery, restrictIDs []uint32) ([]model.Brand, int64, error) {
return r.list(ctx, q, restrictIDs, true)
}
func (r *brandRepository) list(ctx context.Context, q dto.BrandQuery, restrictIDs []uint32, admin bool) ([]model.Brand, int64, error) {
scope := r.brandFilterScope(q, restrictIDs, admin)
var total int64
if err := r.db.WithContext(ctx).