fix(publish): 软删图片按实际行重算 image_count,状态写入拒绝不存在的记录
This commit is contained in:
@ -159,3 +159,162 @@ func TestStreetMainDetailReparentGrandchildren(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── image_count 重算(软删不漂移) ─────────────────────────────────────────────
|
||||
|
||||
// snapImageCount 读某街拍记录当前的 image_count。
|
||||
func snapImageCount(t *testing.T, repo ReviewRepository, snapID uint32) uint16 {
|
||||
t.Helper()
|
||||
var row struct {
|
||||
ImageCount uint16 `gorm:"column:image_count"`
|
||||
}
|
||||
if err := repo.(*reviewRepository).db.WithContext(context.Background()).
|
||||
Table("street_snaps").Select("image_count").Where("id = ?", snapID).Scan(&row).Error; err != nil {
|
||||
t.Fatalf("读街拍 image_count 失败: %v", err)
|
||||
}
|
||||
return row.ImageCount
|
||||
}
|
||||
|
||||
// runwayImageCount 读某走秀记录当前的 image_count。
|
||||
func runwayImageCount(t *testing.T, repo ReviewRepository, runwayID uint32) uint16 {
|
||||
t.Helper()
|
||||
var row struct {
|
||||
ImageCount uint16 `gorm:"column:image_count"`
|
||||
}
|
||||
if err := repo.(*reviewRepository).db.WithContext(context.Background()).
|
||||
Table("brand_runways").Select("image_count").Where("id = ?", runwayID).Scan(&row).Error; err != nil {
|
||||
t.Fatalf("读走秀 image_count 失败: %v", err)
|
||||
}
|
||||
return row.ImageCount
|
||||
}
|
||||
|
||||
// seedMainDetailRunway 插入一条走秀正式记录(status=pending)并返回其 id。
|
||||
// SeasonCode 用递增序号,避开 uq_br_entity (brand_id, season_code, collection_type) 部分唯一索引。
|
||||
func seedMainDetailRunway(t *testing.T, repo ReviewRepository) uint32 {
|
||||
t.Helper()
|
||||
db := repo.(*reviewRepository).db
|
||||
mainDetailSeedSeq++
|
||||
rw := model.BrandRunway{
|
||||
BrandID: 1,
|
||||
TitleEn: "主副图计数测试",
|
||||
Year: 2026,
|
||||
SeasonCode: fmt.Sprintf("MD%d", mainDetailSeedSeq),
|
||||
CollectionType: "rtw",
|
||||
Status: model.StatusPending,
|
||||
}
|
||||
if err := db.WithContext(context.Background()).Create(&rw).Error; err != nil {
|
||||
t.Fatalf("插入走秀记录失败: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
db.Exec("DELETE FROM brand_runway_images WHERE runway_id = ?", rw.ID)
|
||||
db.Exec("DELETE FROM brand_runways WHERE id = ?", rw.ID)
|
||||
})
|
||||
return rw.ID
|
||||
}
|
||||
|
||||
// seedMainDetailRunwayImage 插入一张走秀图(isDetail=1 表示细节图)并返回其行 id。
|
||||
func seedMainDetailRunwayImage(t *testing.T, repo ReviewRepository, runwayID uint32, name string, order uint32, isDetail uint8) uint32 {
|
||||
t.Helper()
|
||||
img := model.BrandRunwayImage{
|
||||
RunwayID: runwayID, BrandID: 1, Image: name + ".jpg", Name: name,
|
||||
SortOrder: order, IsDetail: isDetail,
|
||||
}
|
||||
if err := repo.(*reviewRepository).db.WithContext(context.Background()).Create(&img).Error; err != nil {
|
||||
t.Fatalf("插入走秀图失败: %v", err)
|
||||
}
|
||||
return img.ID
|
||||
}
|
||||
|
||||
// TestSoftDeleteStreetImageRecomputesCount 软删街拍图片后 image_count 必须按实际存活行**重算**。
|
||||
//
|
||||
// 对「已被软删」或「不属于该记录」的 imgID 重放一次,计数必须不变:
|
||||
// 旧实现是 `image_count - 1`(GREATEST 钳到 0),命中 0 行时仍会递减且永不回补;
|
||||
// 单表发布模型删掉了晋升时按实际行重算的自愈路径,漂移会永久留在公开列表卡片的「N 张」上。
|
||||
func TestSoftDeleteStreetImageRecomputesCount(t *testing.T) {
|
||||
repo := NewReviewRepository(testDB(t))
|
||||
ctx := context.Background()
|
||||
|
||||
snapID := seedMainDetailSnap(t, repo)
|
||||
keep := seedMainDetailImage(t, repo, snapID, "keep", 1)
|
||||
drop := seedMainDetailImage(t, repo, snapID, "drop", 2)
|
||||
|
||||
if err := repo.SoftDeleteStreetImage(ctx, snapID, drop); err != nil {
|
||||
t.Fatalf("软删失败: %v", err)
|
||||
}
|
||||
if got := snapImageCount(t, repo, snapID); got != 1 {
|
||||
t.Fatalf("软删一张后 image_count 应为 1,实际 %d", got)
|
||||
}
|
||||
|
||||
// 重放同一张(已被软删):命中 0 行,计数不得变化。
|
||||
if err := repo.SoftDeleteStreetImage(ctx, snapID, drop); err != nil {
|
||||
t.Fatalf("重复软删应幂等,实际报错: %v", err)
|
||||
}
|
||||
if got := snapImageCount(t, repo, snapID); got != 1 {
|
||||
t.Fatalf("重复软删不应改变 image_count,实际 %d(旧递减实现会掉到 0)", got)
|
||||
}
|
||||
|
||||
// 不属于该记录的 imgID:同样不得影响本记录计数。
|
||||
otherImg := seedMainDetailImage(t, repo, seedMainDetailSnap(t, repo), "other", 1)
|
||||
if err := repo.SoftDeleteStreetImage(ctx, snapID, otherImg); err != nil {
|
||||
t.Fatalf("跨记录软删不应报错: %v", err)
|
||||
}
|
||||
if got := snapImageCount(t, repo, snapID); got != 1 {
|
||||
t.Fatalf("跨记录 imgID 不应改变 image_count,实际 %d", got)
|
||||
}
|
||||
|
||||
imgs, err := repo.StreetRecordImages(ctx, snapID)
|
||||
if err != nil {
|
||||
t.Fatalf("读存活图失败: %v", err)
|
||||
}
|
||||
if len(imgs) != 1 || imgs[0].ID != keep {
|
||||
t.Fatalf("应只剩 keep 一行,实际 %+v", imgs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSoftDeleteRunwayImage 走秀 image_count 口径是「主图数」(与入库 imageCount = len(Looks) 一致):
|
||||
// 软删细节图不改计数;软删主图才减,且同样是重算(重放 / 跨记录不漂移)。
|
||||
func TestSoftDeleteRunwayImage(t *testing.T) {
|
||||
repo := NewReviewRepository(testDB(t))
|
||||
ctx := context.Background()
|
||||
|
||||
runwayID := seedMainDetailRunway(t, repo)
|
||||
main1 := seedMainDetailRunwayImage(t, repo, runwayID, "main1", 1, 0)
|
||||
main2 := seedMainDetailRunwayImage(t, repo, runwayID, "main2", 2, 0)
|
||||
detail := seedMainDetailRunwayImage(t, repo, runwayID, "detail1", 3, 1)
|
||||
|
||||
// 首次软删细节图:主图数仍为 2(若用 COUNT(*) 不排除 is_detail,会错算成 3)。
|
||||
if err := repo.SoftDeleteRunwayImage(ctx, runwayID, detail); err != nil {
|
||||
t.Fatalf("软删细节图失败: %v", err)
|
||||
}
|
||||
if got := runwayImageCount(t, repo, runwayID); got != 2 {
|
||||
t.Fatalf("image_count 只计主图,应为 2,实际 %d", got)
|
||||
}
|
||||
|
||||
// 软删一张主图 → 1。
|
||||
if err := repo.SoftDeleteRunwayImage(ctx, runwayID, main2); err != nil {
|
||||
t.Fatalf("软删主图失败: %v", err)
|
||||
}
|
||||
if got := runwayImageCount(t, repo, runwayID); got != 1 {
|
||||
t.Fatalf("软删一张主图后应为 1,实际 %d", got)
|
||||
}
|
||||
|
||||
// 重放(已软删)+ 跨记录:计数均不得变化。
|
||||
if err := repo.SoftDeleteRunwayImage(ctx, runwayID, main2); err != nil {
|
||||
t.Fatalf("重复软删应幂等: %v", err)
|
||||
}
|
||||
otherMain := seedMainDetailRunwayImage(t, repo, seedMainDetailRunway(t, repo), "x", 1, 0)
|
||||
if err := repo.SoftDeleteRunwayImage(ctx, runwayID, otherMain); err != nil {
|
||||
t.Fatalf("跨记录软删不应报错: %v", err)
|
||||
}
|
||||
if got := runwayImageCount(t, repo, runwayID); got != 1 {
|
||||
t.Fatalf("重放 / 跨记录不应改变 image_count,实际 %d", got)
|
||||
}
|
||||
|
||||
imgs, err := repo.RecordImages(ctx, runwayID)
|
||||
if err != nil {
|
||||
t.Fatalf("读存活主图失败: %v", err)
|
||||
}
|
||||
if len(imgs) != 1 || imgs[0].ID != main1 {
|
||||
t.Fatalf("应只剩 main1 一行,实际 %+v", imgs)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user