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