update
This commit is contained in:
@ -92,7 +92,7 @@ func (r *brandRepository) brandFilterScope(q dto.BrandQuery, restrictIDs []uint3
|
||||
case q.Letter >= "A" && q.Letter <= "Z" && len(q.Letter) == 1:
|
||||
db = db.Where("UPPER(LEFT(name_en,1)) = ?", q.Letter)
|
||||
default:
|
||||
db = db.Where("name_en REGEXP ?", latinPrefixRegexp)
|
||||
db = db.Where("name_en ~ ?", latinPrefixRegexp)
|
||||
}
|
||||
|
||||
// 字母索引始终只返回"拥有走秀档案"的品牌(库中有 2 万+ 无档案品牌,
|
||||
|
||||
@ -44,6 +44,10 @@ type IngestRepository interface {
|
||||
CreateStreetSnapDraft(ctx context.Context, d *model.StreetSnapDraft) (uint32, error)
|
||||
// CreateStreetSnapDraftImages 批量插入街拍草稿图片行。
|
||||
CreateStreetSnapDraftImages(ctx context.Context, imgs []model.StreetSnapDraftImage) error
|
||||
// ImageExistsBySha1 在给定图片表中是否已存在相同内容哈希(精确重复);用于入库前跳过该行。
|
||||
ImageExistsBySha1(ctx context.Context, tables []string, sha1 string) (bool, 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 总条数(用于分页计算总页数)。
|
||||
@ -56,16 +60,6 @@ type IngestRepository interface {
|
||||
// ScheduleRetry 失败时调用:attempts+1,未达上限则退避后重置 pending,达上限则置 failed。
|
||||
// 用于临时失败(网络抖动 / 单图下载失败)的自动重试,区别于永久失败(payload 解析错等)直接 MarkFailed。
|
||||
ScheduleRetry(ctx context.Context, id uint32, errMsg string) error
|
||||
// ListImagePHashes 返回全部已晋升图片(runway + street)的 (id, phash, kind),
|
||||
// 跳过 is_deleted 与 phash=0/NULL(存量未计算)。供入库时与新增图做全局汉明比对(近似去重)。
|
||||
ListImagePHashes(ctx context.Context) ([]ImagePHash, error)
|
||||
}
|
||||
|
||||
// ImagePHash 已晋升图片的感知哈希摘要,供入库时全局近似去重比对。
|
||||
type ImagePHash struct {
|
||||
ID uint32 // 图片数字主键
|
||||
Phash uint64 // 感知哈希(SQL 已过滤 0/NULL)
|
||||
Kind string // "runway" | "street"(决定 dup_of 的 hashid 类型)
|
||||
}
|
||||
|
||||
type ingestRepository struct {
|
||||
@ -376,38 +370,45 @@ func (r *ingestRepository) RetryJob(ctx context.Context, id uint32) error {
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ListImagePHashes 返回全部已晋升图片(runway + street)的感知哈希摘要,供入库时全局近似去重。
|
||||
// 跳过 is_deleted 与 phash=0/NULL(存量未计算)。结果合并 runway + street 两类,
|
||||
// 用 Kind 标注类型,调用方据此把 dup_of 编码成对应 hashid 类型。
|
||||
//
|
||||
// 注:每次入库任务都会全量拉取一次(图片量当前为千级,可接受);若后续图片量到十万级,
|
||||
// 可改为按 runway_id/snap_id 分批或加内存缓存 + 定时刷新,避免每 job 一次全表扫描。
|
||||
func (r *ingestRepository) ListImagePHashes(ctx context.Context) ([]ImagePHash, error) {
|
||||
type phRow struct {
|
||||
ID uint32 `gorm:"column:id"`
|
||||
Phash uint64 `gorm:"column:phash"`
|
||||
}
|
||||
out := make([]ImagePHash, 0, 64)
|
||||
const whereActive = "is_deleted = 0 AND phash IS NOT NULL AND phash <> 0"
|
||||
|
||||
var rw []phRow
|
||||
if err := r.db.WithContext(ctx).Model(&model.BrandRunwayImage{}).
|
||||
Select("id, phash").Where(whereActive).Scan(&rw).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, x := range rw {
|
||||
out = append(out, ImagePHash{ID: x.ID, Phash: x.Phash, Kind: "runway"})
|
||||
}
|
||||
|
||||
var sn []phRow
|
||||
if err := r.db.WithContext(ctx).Model(&model.StreetSnapImage{}).
|
||||
Select("id, phash").Where(whereActive).Scan(&sn).Error; err != nil {
|
||||
return nil, err
|
||||
// ImageExistsBySha1 在给定图片表中是否已存在相同 content_sha1(精确重复)。
|
||||
func (r *ingestRepository) ImageExistsBySha1(ctx context.Context, tables []string, sha1 string) (bool, error) {
|
||||
for _, t := range tables {
|
||||
var c int64
|
||||
if err := r.db.WithContext(ctx).Table(t).
|
||||
Where("content_sha1 = ? AND is_deleted = 0", sha1).Count(&c).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
if c > 0 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
for _, x := range sn {
|
||||
out = append(out, ImagePHash{ID: x.ID, Phash: x.Phash, Kind: "street"})
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// FindNearDuplicateImage 在给定图片表中按 dHash 汉明距离(pgvector <~> 算子)检索近重复,
|
||||
// 取距离 ≤ threshold 的最近一条。phashBits 为 vector(64) 二进制向量串;NULL 的 phash 不参与比较。
|
||||
func (r *ingestRepository) FindNearDuplicateImage(ctx context.Context, tables []string, phashBits string, threshold int) (uint32, bool, error) {
|
||||
for _, t := range tables {
|
||||
var row struct {
|
||||
ID uint32 `gorm:"column:id"`
|
||||
Dist int `gorm:"column:dist"`
|
||||
}
|
||||
err := r.db.WithContext(ctx).Table(t).
|
||||
Select("id, (phash <~> ?::vector) AS dist", phashBits).
|
||||
Where("(phash <~> ?::vector) <= ?", phashBits, threshold).
|
||||
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 out, nil
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
// isDuplicateKey 兜底:gorm 的 ErrDuplicatedKey 在不同驱动下的封装不一定一致,
|
||||
|
||||
@ -244,20 +244,21 @@ func (r *reviewRepository) SaveRunwayFromDraft(ctx context.Context, draftID uint
|
||||
if len(imgs) > 0 {
|
||||
rows := make([]model.BrandRunwayImage, 0, len(imgs))
|
||||
for i, im := range imgs {
|
||||
rows = append(rows, model.BrandRunwayImage{
|
||||
RunwayID: runwayID,
|
||||
BrandID: draft.BrandID,
|
||||
Image: im.Image,
|
||||
Name: im.Name,
|
||||
SortOrder: uint32(i + 1),
|
||||
LookIndex: im.LookIndex,
|
||||
IsDetail: im.IsDetail,
|
||||
Phash: im.Phash,
|
||||
IsDuplicate: im.IsDuplicate,
|
||||
DupOf: im.DupOf,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
rows = append(rows, model.BrandRunwayImage{
|
||||
RunwayID: runwayID,
|
||||
BrandID: draft.BrandID,
|
||||
Image: im.Image,
|
||||
Name: im.Name,
|
||||
SortOrder: uint32(i + 1),
|
||||
LookIndex: im.LookIndex,
|
||||
IsDetail: im.IsDetail,
|
||||
ContentSha1: im.ContentSha1,
|
||||
Phash: im.Phash,
|
||||
IsDuplicate: im.IsDuplicate,
|
||||
DupOf: im.DupOf,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
if cErr := tx.Create(&rows).Error; cErr != nil {
|
||||
return cErr
|
||||
@ -439,17 +440,18 @@ func (r *reviewRepository) SaveStreetSnapFromDraft(ctx context.Context, draftID
|
||||
if len(imgs) > 0 {
|
||||
rows := make([]model.StreetSnapImage, 0, len(imgs))
|
||||
for i, im := range imgs {
|
||||
rows = append(rows, model.StreetSnapImage{
|
||||
SnapID: snapID,
|
||||
Image: im.Image,
|
||||
Name: im.Name,
|
||||
SortOrder: uint32(i + 1),
|
||||
Phash: im.Phash,
|
||||
IsDuplicate: im.IsDuplicate,
|
||||
DupOf: im.DupOf,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
rows = append(rows, model.StreetSnapImage{
|
||||
SnapID: snapID,
|
||||
Image: im.Image,
|
||||
Name: im.Name,
|
||||
SortOrder: uint32(i + 1),
|
||||
ContentSha1: im.ContentSha1,
|
||||
Phash: im.Phash,
|
||||
IsDuplicate: im.IsDuplicate,
|
||||
DupOf: im.DupOf,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
if cErr := tx.Create(&rows).Error; cErr != nil {
|
||||
return cErr
|
||||
|
||||
Reference in New Issue
Block a user