//go:build integration // 集成测试:去重核心查询需要真实 PostgreSQL + pgvector 环境。 // // 运行方式(先起库,见 scripts/pgvector): // // docker compose -f scripts/pgvector/docker-compose.yml up -d // go test -tags integration ./internal/repository/ -run TestDedup -v // // 默认 `go test ./...` 不会编译本文件(build tag 隔离),CI 无库环境不受影响。 package repository import ( "context" "database/sql" "testing" "fashionapi/internal/config" "fashionapi/internal/database" "fashionapi/internal/model" "fashionapi/internal/pkg/phash" "gorm.io/gorm" ) // testDB 连真实库并校验去重所需的结构已就绪。 // // 结构不再由 AutoMigrate 自动创建(已移除),须先自行准备好库,例如: // // ./bin/dbtool dump -out db_dump.sql // psql -U fashion -d fashion -v ON_ERROR_STOP=1 -f db_dump.sql // // 这里只做存在性校验并给出明确提示,避免后续查询报出难懂的错。 func testDB(t *testing.T) *gorm.DB { t.Helper() cfg, err := config.Load("") if err != nil { t.Fatalf("加载配置失败: %v", err) } db, err := database.New(cfg.Database) if err != nil { t.Fatalf("连接数据库失败: %v", err) } t.Cleanup(func() { _ = database.Close(db) }) if err := db.Exec("SELECT 1 FROM brand_runway_images LIMIT 1").Error; err != nil { t.Fatalf("brand_runway_images 不可用(库结构未就绪?先 dbtool dump 再 psql -f 灌库): %v", err) } return db } // insertRunwayImage 直接插入一条走秀图片行(无外键约束,runway_id / brand_id 可填占位值)。 func insertRunwayImage(t *testing.T, db *gorm.DB, image, phashBits string) { t.Helper() var ph sql.NullString if phashBits != "" { ph = sql.NullString{String: phashBits, Valid: true} } row := model.BrandRunwayImage{ RunwayID: 1, BrandID: 1, Image: image, Name: "look", SortOrder: 1, Phash: ph, } if err := db.Create(&row).Error; err != nil { t.Fatalf("插入图片失败: %v", err) } t.Cleanup(func() { db.Where("image = ?", image).Delete(&model.BrandRunwayImage{}) }) } // TestDedupNearDuplicate 近重复:dHash 汉明距离 ≤ 阈值应被 FindNearDuplicateImage 命中。 func TestDedupNearDuplicate(t *testing.T) { db := testDB(t) repo := NewIngestRepository(db) ctx := context.Background() // 插入一张已有图片,phash 为 64 维全 1 向量。 insertRunwayImage(t, db, "runway/near-dup.jpg", phash.ToVectorBits(^uint64(0))) tables := []string{"brand_runway_images"} // 探测向量:仅第 0 位为 0、其余为 1 → 与全 1 向量汉明距离 = 1,应命中。 probe := ^uint64(0) &^ 1 _, found, err := repo.FindNearDuplicateImage(ctx, tables, phash.ToVectorBits(probe), phash.DefaultThreshold) if err != nil { t.Fatalf("FindNearDuplicateImage 出错: %v", err) } if !found { t.Fatalf("汉明距离 1 的近重复应被命中") } // 全 1 向量(与库中那条距离 0)的取反不是合法探针(会有空串问题), // 这里改用与库中那条完全相同的全 1 探针(非空、合法)并先断言查询本身不报错。 probeSame, foundSame, errSame := repo.FindNearDuplicateImage(ctx, tables, phash.ToVectorBits(^uint64(0)), phash.DefaultThreshold) if errSame != nil { t.Fatalf("去重查询不应报错: %v", errSame) } if !foundSame { t.Fatalf("与库中那条完全相同的 phash 应命中") } if probeSame == 0 { t.Fatalf("命中的行 id 不应为 0") } }