Files
backend_v2/internal/repository/image_id_stability_integration_test.go
toom1996 f04b5707cb 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/
2026-09-23 16:27:14 +08:00

141 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//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)
}
}