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:
@ -181,12 +181,14 @@ func TestIngestReuseRejectedRefusesPublished(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDedupIgnoresSoftDeletedImage 去重不得命中已软删的行:复用驳回行时去重先于软删旧图执行,
|
||||
// 若把「即将被软删的旧图」算进比对,重爬到的同一张图会被打上 dup_of=<旧行 id>,
|
||||
// 留痕指向一条随后公开不可见的记录。
|
||||
// TestDedupIgnoresSoftDeletedImage 去重只比对**未软删**的行(is_deleted = 0)。
|
||||
//
|
||||
// 用与探测向量完全相同的 phash(距离 0)插入软删行,保证修复前它必然是「最近一条」被返回,
|
||||
// 因此本断言在修复前确定性失败、修复后确定性通过,与库中其它数据无关。
|
||||
// 别把它误读成「复用驳回行时不会自我命中」:复用路径里去重发生在软删旧图**之前**,那一刻
|
||||
// 旧行仍是 is_deleted = 0、仍会被命中——dup_of 只是给人工看的留痕,不参与任何可见性判定。
|
||||
// 本用例锁的是另一条独立保证:**已经**软删的行不再被当作重复参照。
|
||||
//
|
||||
// 用与探测向量完全相同的 phash(距离 0)插入软删行,保证它若被纳入就必然是「最近一条」,
|
||||
// 从而确定性地验证 is_deleted = 0 过滤生效,与库中其它数据无关。
|
||||
func TestDedupIgnoresSoftDeletedImage(t *testing.T) {
|
||||
db := testDB(t)
|
||||
applyMigration(t, db, "2026-09-22-01-single-table-publish.sql")
|
||||
@ -213,3 +215,46 @@ func TestDedupIgnoresSoftDeletedImage(t *testing.T) {
|
||||
t.Fatalf("已软删的行(id=%d)不应参与近重复比对", row.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIngestImageCountMatchesAliveMainRows 入库后 image_count 必须等于该记录存活主图行数(is_detail = 0)。
|
||||
//
|
||||
// 与 service.processRunway 的 imageCount = countMainImages(images) 同口径,且与审核期
|
||||
// SoftDeleteRunwayImage 的重算口径一致:三处必须同源,否则公开卡片「N 张」角标与
|
||||
// FeaturedIDs 的 SUM(image_count) 热度会漂移(单表发布删掉了晋升重算的自愈路径)。
|
||||
func TestIngestImageCountMatchesAliveMainRows(t *testing.T) {
|
||||
db := testDB(t)
|
||||
applyMigration(t, db, "2026-09-22-01-single-table-publish.sql")
|
||||
repo := NewIngestRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
const season = "IC71"
|
||||
rw := newRunwayForIngest(70, 1, season, "rtw", "ic-main-a.jpg")
|
||||
rw.ImageCount = 2 // 2 张主图
|
||||
imgs := []model.BrandRunwayImage{
|
||||
{BrandID: 1, Image: "ic-main-a.jpg", Name: "Look 1", SortOrder: 1, IsDetail: 0},
|
||||
{BrandID: 1, Image: "ic-detail-a1.jpg", Name: "Look 1 — Detail 1", SortOrder: 2, IsDetail: 1},
|
||||
{BrandID: 1, Image: "ic-main-b.jpg", Name: "Look 2", SortOrder: 3, IsDetail: 0},
|
||||
}
|
||||
id, err := repo.CreateRunwayWithImages(ctx, rw, imgs)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRunwayWithImages 出错: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
db.Exec("DELETE FROM brand_runway_images WHERE runway_id = ?", id)
|
||||
db.Exec("DELETE FROM brand_runways WHERE id = ?", id)
|
||||
})
|
||||
|
||||
var got struct {
|
||||
ImageCount uint16 `gorm:"column:image_count"`
|
||||
AliveMain int64 `gorm:"column:alive_main"`
|
||||
}
|
||||
if err := db.Raw(`SELECT r.image_count,
|
||||
(SELECT COUNT(*) FROM brand_runway_images i
|
||||
WHERE i.runway_id = r.id AND i.is_deleted = 0 AND i.is_detail = 0) AS alive_main
|
||||
FROM brand_runways r WHERE r.id = ?`, id).Scan(&got).Error; err != nil {
|
||||
t.Fatalf("读回计数失败: %v", err)
|
||||
}
|
||||
if int64(got.ImageCount) != got.AliveMain {
|
||||
t.Fatalf("入库后 image_count=%d 必须等于存活主图行数 %d", got.ImageCount, got.AliveMain)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user