fix(publish): 入库实体键读错误 fail-closed,复用加 rejected 守卫,去重排除软删行

This commit is contained in:
toom1996
2026-09-23 10:41:16 +08:00
parent dba615d636
commit 8f8b06def7
3 changed files with 117 additions and 4 deletions

View File

@ -5,9 +5,12 @@ package repository
import (
"context"
"database/sql"
"testing"
"time"
"fashionapi/internal/model"
"fashionapi/internal/pkg/phash"
)
func newRunwayForIngest(jobID uint32, brandID uint32, seasonCode, collectionType string, cover string) *model.BrandRunway {
@ -129,3 +132,84 @@ func TestIngestReusesRejectedRecord(t *testing.T) {
t.Fatalf("旧图应被软删")
}
}
// TestIngestReuseRejectedRefusesPublished 复用守卫:实体键状态是读阶段拿到的,而写库发生在整批
// 图片下载/上传之后(可能数十秒窗口)。若该行在这段窗口内已被改成 published,复用必须失败,
// 绝不能把它置回 pending——那等于把已发布内容从公开视图上撤下。
func TestIngestReuseRejectedRefusesPublished(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 = "SS95"
id, err := repo.CreateRunwayWithImages(ctx, newRunwayForIngest(80, 1, season, "rtw", "pub.jpg"), newRunwayImages("pub.jpg"))
if err != nil {
t.Fatalf("预置记录失败: %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)
})
// 模拟「读阶段之后、写库之前」该行被审核通过。
if err := db.Model(&model.BrandRunway{}).Where("id = ?", id).
Update("status", model.StatusPublished).Error; err != nil {
t.Fatalf("置为已发布失败: %v", err)
}
newRW := newRunwayForIngest(81, 1, season, "rtw", "pub2.jpg")
newRW.TitleEn = "ingest-test-should-not-apply"
if err := repo.ReuseRejectedRunway(ctx, id, newRW, newRunwayImages("pub2.jpg")); err == nil {
t.Fatalf("对已发布行复用应报错,实际返回 nil")
}
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("已发布行不得被复用改回 pending,实际 %s", rw.Status)
}
if rw.TitleEn != "ingest-test" || rw.Cover != "pub.jpg" {
t.Fatalf("复用失败时不应覆盖内容,实际 title=%s cover=%s", rw.TitleEn, rw.Cover)
}
publicCount := int64(0)
db.Table("public_brand_runways").Where("id = ?", id).Count(&publicCount)
if publicCount != 1 {
t.Fatalf("已发布行应仍在公开视图,实际 %d", publicCount)
}
}
// TestDedupIgnoresSoftDeletedImage 去重不得命中已软删的行:复用驳回行时去重先于软删旧图执行,
// 若把「即将被软删的旧图」算进比对,重爬到的同一张图会被打上 dup_of=<旧行 id>,
// 留痕指向一条随后公开不可见的记录。
//
// 用与探测向量完全相同的 phash(距离 0)插入软删行,保证修复前它必然是「最近一条」被返回,
// 因此本断言在修复前确定性失败、修复后确定性通过,与库中其它数据无关。
func TestDedupIgnoresSoftDeletedImage(t *testing.T) {
db := testDB(t)
applyMigration(t, db, "2026-09-22-01-single-table-publish.sql")
repo := NewIngestRepository(db)
ctx := context.Background()
bits := phash.ToVectorBits(^uint64(0))
now := uint32(time.Now().Unix())
row := model.BrandRunwayImage{
RunwayID: 1, BrandID: 1, Image: "ingest/soft-deleted.jpg", Name: "look",
SortOrder: 1, IsDeleted: 1, CreatedAt: now, UpdatedAt: now,
Phash: sql.NullString{String: bits, Valid: true},
}
if err := db.Create(&row).Error; err != nil {
t.Fatalf("插入软删图片失败: %v", err)
}
t.Cleanup(func() { db.Where("id = ?", row.ID).Delete(&model.BrandRunwayImage{}) })
dupID, found, err := repo.FindNearDuplicateImage(ctx, []string{"brand_runway_images"}, bits, phash.DefaultThreshold)
if err != nil {
t.Fatalf("FindNearDuplicateImage 出错: %v", err)
}
if found && dupID == row.ID {
t.Fatalf("已软删的行(id=%d)不应参与近重复比对", row.ID)
}
}