fix(publish): 最终审查修复波——迁移索引口径 / 入库计数 / 仓储证据
C1(关键):2026-09-22-03 的街拍实体键索引改用最终表达式 (city, year, COALESCE(title, '')),与体检及 StreetSnapEntityState 口径一致。 此前索引按 (city, year) 比体检更窄,同城同年多专题的合法数据会通过体检、 再由索引抛原生 23505;04 退化为幂等兜底并在注释说明真正常态由 03 承载。 新增 db/migrations/README.md,写清迁移链执行顺序与各步前提/不可逆点。 I1:processRunway 的 image_count 改为按实际落库主图行数(countMainImages), 不再用 len(p.Looks)(空主图 / sha1 重复会被跳过,导致计数偏大且不再自愈)。 补 service 单测与「入库后 image_count == 存活主图行数」的 DB 断言。 I2:三个迁移文件不再把已删除的一次性搬迁脚本写成硬前置,改为写明取回方式 (git show c4bafc5:scripts/migrate_single_table/main.go,并须在旧代码树上运行)。 I3:新增 SetRecordStatus / SetStreetRecordStatus 仓储集成测试: pending 经仓储通过后在公开视图可见、驳回后不可见、不存在的 id 返回 ErrNotFound。 Minor:修正锁不住口径的走秀 image_count 测试(改为删主图、留细节图); 修正去重注释与事实不符(含 FindNearDuplicateImage 注释); ListRecords / ListStreetRecords 改用 Scope 杜绝 Count 后复用 *gorm.DB; dbtool 视图改 CREATE OR REPLACE 并重跑 dump(仍 4 视图、无草稿表); 删除挂在 Popular 上的「IDs 返回…」注释;README 改为单表 + status 现状; SetRecordStatus 注释写明有意不校验前置状态;规格补两条已知不一致。
This commit is contained in:
@ -61,6 +61,21 @@ func NewReviewRepository(db *gorm.DB) ReviewRepository {
|
||||
return &reviewRepository{db: db}
|
||||
}
|
||||
|
||||
// reviewListScope 把「未软删 + 可选状态过滤」编译为 GORM Scope。
|
||||
//
|
||||
// 用 Scope 而非在 Count 之后复用同一个 *gorm.DB:GORM v2 中终结方法(Count 等)之后再复用
|
||||
// 同一实例会带上残留的 Statement 状态,Scope 每次作用于全新查询即可杜绝该隐患
|
||||
//(与 article_repository.filterScope 同一理由)。
|
||||
func reviewListScope(status string) func(*gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
db = db.Where("is_deleted = 0")
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
func (r *reviewRepository) ListRecords(ctx context.Context, status string, page, size int) ([]model.BrandRunway, int64, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
@ -68,19 +83,17 @@ func (r *reviewRepository) ListRecords(ctx context.Context, status string, page,
|
||||
if size < 1 || size > 100 {
|
||||
size = 20
|
||||
}
|
||||
q := r.db.WithContext(ctx).Model(&model.BrandRunway{}).Where("is_deleted = 0")
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
scope := reviewListScope(status)
|
||||
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Model(&model.BrandRunway{}).Scopes(scope).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if total == 0 {
|
||||
return []model.BrandRunway{}, 0, nil
|
||||
}
|
||||
var rows []model.BrandRunway
|
||||
if err := q.
|
||||
if err := r.db.WithContext(ctx).Model(&model.BrandRunway{}).Scopes(scope).
|
||||
Order("CASE status WHEN '" + model.StatusPending + "' THEN 0 ELSE 1 END ASC, id DESC").
|
||||
Offset((page - 1) * size).
|
||||
Limit(size).
|
||||
@ -143,6 +156,11 @@ func (r *reviewRepository) UpdateRecord(ctx context.Context, id uint32, fields m
|
||||
// SetRecordStatus 命中 0 行说明记录不存在,返回 ErrNotFound。
|
||||
// 否则「对不存在的 id 通过 / 驳回」会被当作成功:handler 只校验 id != 0,id 又来自 URL,
|
||||
// 竞态窗口(记录刚被删除)下会给出假成功 302。
|
||||
//
|
||||
// 有意**不校验前置状态**:允许把 rejected 直接置 published(或反向),不做状态机约束。
|
||||
// 列表页只对 pending 显示通过 / 驳回按钮,但 POST 端点本身不设防 —— 这是已知取舍
|
||||
//(幂等重放 / 管理端纠正误操作都依赖它);若日后要收紧,应改在 handler 或显式状态机里做,
|
||||
// 而不是悄悄在这个仓储方法里加限制。
|
||||
func (r *reviewRepository) SetRecordStatus(ctx context.Context, id uint32, status, reviewer, reason string) error {
|
||||
res := r.db.WithContext(ctx).
|
||||
Model(&model.BrandRunway{}).
|
||||
@ -183,19 +201,17 @@ func (r *reviewRepository) ListStreetRecords(ctx context.Context, status string,
|
||||
if size < 1 || size > 100 {
|
||||
size = 20
|
||||
}
|
||||
q := r.db.WithContext(ctx).Model(&model.StreetSnap{}).Where("is_deleted = 0")
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
scope := reviewListScope(status)
|
||||
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Model(&model.StreetSnap{}).Scopes(scope).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if total == 0 {
|
||||
return []model.StreetSnap{}, 0, nil
|
||||
}
|
||||
var rows []model.StreetSnap
|
||||
if err := q.
|
||||
if err := r.db.WithContext(ctx).Model(&model.StreetSnap{}).Scopes(scope).
|
||||
Order("CASE status WHEN '" + model.StatusPending + "' THEN 0 ELSE 1 END ASC, id DESC").
|
||||
Offset((page - 1) * size).
|
||||
Limit(size).
|
||||
@ -250,6 +266,7 @@ func (r *reviewRepository) UpdateStreetRecord(ctx context.Context, id uint32, fi
|
||||
}
|
||||
|
||||
// SetStreetRecordStatus 命中 0 行说明记录不存在,返回 ErrNotFound(理由同 SetRecordStatus)。
|
||||
// 同样有意不校验前置状态:允许 rejected 直接置 published,属已知取舍(见 SetRecordStatus)。
|
||||
func (r *reviewRepository) SetStreetRecordStatus(ctx context.Context, id uint32, status, reviewer, reason string) error {
|
||||
res := r.db.WithContext(ctx).
|
||||
Model(&model.StreetSnap{}).
|
||||
|
||||
Reference in New Issue
Block a user