This commit is contained in:
toom1996
2026-10-18 17:42:10 +08:00
parent 1b06bd5bda
commit 1de1ec46a6
58 changed files with 5281 additions and 19191 deletions

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"math"
"strings"
"time"
@ -63,8 +62,6 @@ type IngestRepository interface {
CreateStreetSnapWithImages(ctx context.Context, snap *model.StreetSnap, imgs []model.StreetSnapImage) (uint32, error)
// ReuseRejectedStreetSnap 复用一条 rejected 街拍记录(事务内完成)。
ReuseRejectedStreetSnap(ctx context.Context, id uint32, snap *model.StreetSnap, imgs []model.StreetSnapImage) error
// FindNearDuplicateImage 在给定图片表中按 dHash 汉明距离检索近重复,返回命中行 id(仅取最近一条)。
FindNearDuplicateImage(ctx context.Context, tables []string, phashBits string, threshold int) (dupID uint32, found bool, err error)
// ListJobs 按 id 倒序分页列出入库任务(用于后台监控页)。offset/limit 控制分页。
ListJobs(ctx context.Context, offset, limit int) ([]model.IngestJob, error)
// CountJobs 返回 ingest_jobs 总条数(用于分页计算总页数)。
@ -409,13 +406,16 @@ func (r *ingestRepository) BackfillRunwayPublishedAt(ctx context.Context, runway
return nil
}
// 仅当目标 season_code 不与同品牌其它行冲突时才补全 year / season_code。
// 注意:占位符顺序必须是 (runwayID, targetSeasonCode) —— 本 Where 有两个 ?,
// 漏传 runwayID 会让 season_code 落到 id 槽位,生成 `WHERE id = 'FW27'`
// 这种必然语法错误的 SQL(该缺陷曾导致「重爬已存在走秀」的任务无限重试)。
return r.db.WithContext(ctx).
Model(&model.BrandRunway{}).
Where("id = ? AND year = 0 AND is_deleted = 0 "+
"AND NOT EXISTS (SELECT 1 FROM brand_runways b "+
"WHERE b.brand_id = brand_runways.brand_id AND b.season_code = ? "+
"AND b.collection_type = brand_runways.collection_type AND b.id <> brand_runways.id AND b.is_deleted = 0)",
targetSeasonCode).
runwayID, targetSeasonCode).
Updates(map[string]any{
"year": year,
"season_code": targetSeasonCode,
@ -570,39 +570,6 @@ func (r *ingestRepository) RetryJob(ctx context.Context, id uint32) error {
}).Error
}
// FindNearDuplicateImage 在给定图片表中按 dHash 汉明距离检索近重复,取距离 ≤ threshold 的最近一条。
// 算子用 pgvector 的 L2(<->);因 phash 是 {0,1}^64 向量,L2² == 汉明距离,故 L2 阈值 = sqrt(threshold)。
// phashBits 为 vector(64) 二进制向量串;NULL 的 phash 不参与比较。
//
// 只比对未软删的行(is_deleted = 0):已被软删(审核剔除 / 复用重审)的旧图不应再作为
// 「重复」的参照。注意复用驳回行的时序 —— 去重发生在软删旧图**之前**,那一刻旧行仍是
// is_deleted = 0、会被正常命中;dup_of 只是人工留痕,不承担可见性语义。
// 本方法被 runway / street 两条入库路径共用,过滤对两者语义一致。
func (r *ingestRepository) FindNearDuplicateImage(ctx context.Context, tables []string, phashBits string, threshold int) (uint32, bool, error) {
// 汉明阈值转 L2 阈值:phash 为 {0,1}^64 向量,L2² == 汉明距离,故 L2 阈值 = sqrt(汉明阈值)。
l2Limit := math.Sqrt(float64(threshold))
for _, t := range tables {
var row struct {
ID uint32 `gorm:"column:id"`
Dist float64 `gorm:"column:dist"`
}
err := r.db.WithContext(ctx).Table(t).
Select("id, (phash <-> ?::vector) AS dist", phashBits).
Where("(phash <-> ?::vector) <= ?", phashBits, l2Limit).
Where("is_deleted = 0").
Order("dist ASC").
Limit(1).
Scan(&row).Error
if err != nil {
return 0, false, err
}
if row.ID != 0 {
return row.ID, true, nil
}
}
return 0, false, nil
}
// isDuplicateKey 兜底:gorm 的 ErrDuplicatedKey 在不同驱动下的封装不一定一致,
// 命中驱动返回的唯一约束冲突错误信息更稳(兼容 MySQL/PostgreSQL/SQLite 不同措辞)。
func isDuplicateKey(err error) bool {