fix(publish): 最终审查补修(编辑页删图重算计数、迁移编号后置、图片 id 稳定断言)

D1 编辑页删图:DeleteRunwayImage/DeleteSnapImage 在单事务内取父 id→软删→按存活行重算 image_count,口径与审核侧 SoftDelete*Image 完全一致(走秀只计主图、街拍计全部),并补集成测试(含重复删同一张)

D2 不可逆删表迁移 02→05(git mv),使文件名顺序 01→01b→03→04→05 与必需执行顺序一致;同步 README/文件头/规格/计划全部引用

D3 新增「通过审核后图片行 id 集合完全一致」的集成断言,锁住单表发布头号收益

D4 修正 01 迁移文件头「只做三件事」自相矛盾表述;D5 删除受版本控制的陈旧产物 cmd/dbtool/db_dump.json 并加 gitignore 规则;D6 清理遗留空目录 scripts/tmp_verify/
This commit is contained in:
toom1996
2026-09-23 16:27:14 +08:00
parent be4db0bf96
commit f04b5707cb
10 changed files with 411 additions and 37116 deletions

View File

@ -0,0 +1,140 @@
//go:build integration
// 集成测试:单表发布模型的头号收益 —— 审核通过不再重建图片,**图片行 id 终身不变**。
//
// 旧流程(草稿表 → 晋升)会在通过时把草稿图片重新插入正式图片表,图片行 id 必然改变;
// 任何按 id 引用的下游(CDN 缓存键、分享链接、埋点)都会随之失效。改造把「通过」降级为
// 只改 status,本文件用「id 集合一字不差」把这条收益钉死。
//
// 为什么现有测试看不到它:
// - router/backstage_test.go 的桩只断言 op == "approve",根本看不到图片行 id;
// - 公开视图测试都是直接 Update("status"),绕过了 SetRecordStatus / SetStreetRecordStatus。
//
// 反证(本断言为何能区分「重建图片」的假实现):若把 SetRecordStatus 换成任何会重建图片的实现
// (先删旧图再插入、或从别处拷贝重建),通过前记下的 id 与通过后必然不同(旧 id 消失、出现新 id),
// 本断言立即变红。断言只比对「同一记录存活图片行的 id 序列」,不依赖计数或其它副作用,
// 因此不会被与重建无关的改动误伤,也不会因无关改动静默通过。
//
// 运行:go test -tags integration ./internal/repository/ -run TestApproveKeepsImageRowIDs -v
package repository
import (
"context"
"testing"
"fashionapi/internal/model"
)
// sameUint32IDs 逐元素比较两个 id 序列(长度 + 顺序)。
func sameUint32IDs(a, b []uint32) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// TestApproveKeepsImageRowIDs 走秀:pending 记录(3 张图)经 SetRecordStatus(published) 通过后,
// status 变为 published,且图片行 id 集合与通过前完全一致。
func TestApproveKeepsImageRowIDs(t *testing.T) {
db := testDB(t)
applyMigration(t, db, "2026-09-22-01-single-table-publish.sql")
repo := NewReviewRepository(db)
ctx := context.Background()
id := seedRunway(t, db, model.StatusPending, 1992) // seedRunway 已插 1 张主图
now := uint32(1_800_000_000)
for i, name := range []string{"second", "third"} {
img := model.BrandRunwayImage{
RunwayID: id, BrandID: 1, Image: name + ".jpg", Name: name,
SortOrder: uint32(i + 2), CreatedAt: now, UpdatedAt: now,
}
if err := db.Create(&img).Error; err != nil {
t.Fatalf("插入走秀图片失败: %v", err)
}
}
imageIDs := func() []uint32 {
var ids []uint32
if err := db.Model(&model.BrandRunwayImage{}).
Where("runway_id = ?", id).Order("id ASC").Pluck("id", &ids).Error; err != nil {
t.Fatalf("读图片行 id 失败: %v", err)
}
return ids
}
before := imageIDs()
if len(before) != 3 {
t.Fatalf("前置应恰有 3 行图片,实际 %d", len(before))
}
if err := repo.SetRecordStatus(ctx, id, model.StatusPublished, "admin", ""); err != nil {
t.Fatalf("SetRecordStatus(published) 出错: %v", err)
}
var rw model.BrandRunway
if err := db.Where("id = ?", id).First(&rw).Error; err != nil {
t.Fatalf("读回记录失败: %v", err)
}
if rw.Status != model.StatusPublished {
t.Fatalf("status 应为 published,实际 %s", rw.Status)
}
if after := imageIDs(); !sameUint32IDs(before, after) {
t.Fatalf("通过审核不得重建图片:图片行 id 应完全一致,通过前 %v,通过后 %v", before, after)
}
}
// TestApproveKeepsSnapImageRowIDs 街拍:与走秀同形,覆盖另一组记录/图片表与方法。
func TestApproveKeepsSnapImageRowIDs(t *testing.T) {
db := testDB(t)
applyMigration(t, db, "2026-09-22-01-single-table-publish.sql")
repo := NewReviewRepository(db)
ctx := context.Background()
id := seedStreetSnap(t, db, model.StatusPending, 1986) // 已插 1 张图
now := uint32(1_800_000_000)
for i, name := range []string{"second", "third"} {
img := model.StreetSnapImage{
SnapID: id, Image: name + ".jpg", Name: name,
SortOrder: uint32(i + 2), CreatedAt: now, UpdatedAt: now,
}
if err := db.Create(&img).Error; err != nil {
t.Fatalf("插入街拍图片失败: %v", err)
}
}
imageIDs := func() []uint32 {
var ids []uint32
if err := db.Model(&model.StreetSnapImage{}).
Where("snap_id = ?", id).Order("id ASC").Pluck("id", &ids).Error; err != nil {
t.Fatalf("读图片行 id 失败: %v", err)
}
return ids
}
before := imageIDs()
if len(before) != 3 {
t.Fatalf("前置应恰有 3 行图片,实际 %d", len(before))
}
if err := repo.SetStreetRecordStatus(ctx, id, model.StatusPublished, "admin", ""); err != nil {
t.Fatalf("SetStreetRecordStatus(published) 出错: %v", err)
}
var snap model.StreetSnap
if err := db.Where("id = ?", id).First(&snap).Error; err != nil {
t.Fatalf("读回记录失败: %v", err)
}
if snap.Status != model.StatusPublished {
t.Fatalf("status 应为 published,实际 %s", snap.Status)
}
if after := imageIDs(); !sameUint32IDs(before, after) {
t.Fatalf("通过审核不得重建图片:图片行 id 应完全一致,通过前 %v,通过后 %v", before, after)
}
}