update backend
This commit is contained in:
@ -3,6 +3,7 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"fashionapi/internal/model"
|
||||
@ -11,13 +12,13 @@ import (
|
||||
)
|
||||
|
||||
// IngestRepository 爬虫入库管线专属仓储:任务队列(ingest_jobs)+ nonce 防重放
|
||||
// (ingest_nces)+ 走秀正式表写入(brand_runway / brand_runway_images)。
|
||||
// (ingest_nonces)+ 走秀正式表写入(brand_runways / brand_runway_images)。
|
||||
//
|
||||
// 去重由 worker 按「实体键」完成:走秀 = brand_id + season_code + collection_type,
|
||||
// 街拍 = city + year(与 review_repository 的 SaveRunwayFromDraft / SaveStreetSnapFromDraft
|
||||
// 晋升时的幂等键一致),因此一个秀/街拍只会在正式表留一行,多来源图片在晋升阶段聚合。
|
||||
//
|
||||
// 入队与领取用同一张 ingest_jobs 表,领取靠 MySQL 的 FOR UPDATE SKIP LOCKED
|
||||
// 入队与领取用同一张 ingest_jobs 表,领取靠 PostgreSQL 的 FOR UPDATE SKIP LOCKED
|
||||
// 实现「多 worker 安全并发」——同一条任务只会被一个 worker 拿到,其它 worker 跳过它。
|
||||
type IngestRepository interface {
|
||||
// Enqueue 写入一条待处理任务(payload 为原始 JSON)。
|
||||
@ -44,8 +45,6 @@ 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 控制分页。
|
||||
@ -54,8 +53,8 @@ type IngestRepository interface {
|
||||
CountJobs(ctx context.Context) (int64, error)
|
||||
// RetryJob 把一条 failed 任务重置回 pending,清 last_error/locked_at,等待 worker 重新处理。
|
||||
RetryJob(ctx context.Context, id uint32) error
|
||||
// EnqueueMediaCleanup 写入一条「清理七牛孤儿图」任务(payload 为待清理 key 的 JSON)。
|
||||
// 由删除图集的服务调用,把同步的七牛删除改为异步队列,避免阻塞删除请求。
|
||||
// EnqueueMediaCleanup 写入一条「清理S4孤儿图」任务(payload 为待清理 key 的 JSON)。
|
||||
// 由删除图集的服务调用,把同步的S4删除改为异步队列,避免阻塞删除请求。
|
||||
EnqueueMediaCleanup(ctx context.Context, payload string) error
|
||||
// ScheduleRetry 失败时调用:attempts+1,未达上限则退避后重置 pending,达上限则置 failed。
|
||||
// 用于临时失败(网络抖动 / 单图下载失败)的自动重试,区别于永久失败(payload 解析错等)直接 MarkFailed。
|
||||
@ -118,7 +117,7 @@ func (r *ingestRepository) Claim(ctx context.Context, limit int) ([]model.Ingest
|
||||
}
|
||||
|
||||
// 2) 领取 pending 且已到重试时间的任务(含刚回收的 + 新入队的 + 退避已到期的)。
|
||||
// 优先处理 media_cleanup(删除图集时异步清理七牛),避免被大批 crawl 任务排到后面、清理迟迟不触发。
|
||||
// 优先处理 media_cleanup(删除图集时异步清理S4),避免被大批 crawl 任务排到后面、清理迟迟不触发。
|
||||
var ids []uint32
|
||||
if err := tx.Raw(
|
||||
"SELECT id FROM ingest_jobs WHERE status = ? AND next_attempt_at <= ? "+
|
||||
@ -209,7 +208,7 @@ func (r *ingestRepository) ScheduleRetry(ctx context.Context, id uint32, errMsg
|
||||
}).Error
|
||||
}
|
||||
|
||||
// EnqueueMediaCleanup 写入一条「清理七牛孤儿图」任务(kind=media_cleanup),
|
||||
// EnqueueMediaCleanup 写入一条「清理S4孤儿图」任务(kind=media_cleanup),
|
||||
// payload 为待清理 key 的 JSON,由 worker 的 processMediaCleanup 按引用计数判定真孤儿后删除。
|
||||
func (r *ingestRepository) EnqueueMediaCleanup(ctx context.Context, payload string) error {
|
||||
now := uint32(time.Now().Unix())
|
||||
@ -372,32 +371,20 @@ func (r *ingestRepository) RetryJob(ctx context.Context, id uint32) error {
|
||||
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// FindNearDuplicateImage 在给定图片表中按 dHash 汉明距离(pgvector <~> 算子)检索近重复,
|
||||
// 取距离 ≤ threshold 的最近一条。phashBits 为 vector(64) 二进制向量串;NULL 的 phash 不参与比较。
|
||||
// FindNearDuplicateImage 在给定图片表中按 dHash 汉明距离检索近重复,取距离 ≤ threshold 的最近一条。
|
||||
// 算子用 pgvector 的 L2(<->);因 phash 是 {0,1}^64 向量,L2² == 汉明距离,故 L2 阈值 = sqrt(threshold)。
|
||||
// phashBits 为 vector(64) 二进制向量串;NULL 的 phash 不参与比较。
|
||||
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 int `gorm:"column:dist"`
|
||||
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, threshold).
|
||||
Select("id, (phash <-> ?::vector) AS dist", phashBits).
|
||||
Where("(phash <-> ?::vector) <= ?", phashBits, l2Limit).
|
||||
Order("dist ASC").
|
||||
Limit(1).
|
||||
Scan(&row).Error
|
||||
@ -412,13 +399,13 @@ func (r *ingestRepository) FindNearDuplicateImage(ctx context.Context, tables []
|
||||
}
|
||||
|
||||
// isDuplicateKey 兜底:gorm 的 ErrDuplicatedKey 在不同驱动下的封装不一定一致,
|
||||
// 直接命中 MySQL 1062 错误号更稳。
|
||||
// 命中驱动返回的唯一约束冲突错误信息更稳(兼容 MySQL/PostgreSQL/SQLite 不同措辞)。
|
||||
func isDuplicateKey(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := err.Error()
|
||||
return containsAny(msg, "Duplicate entry", "1062", "UNIQUE constraint failed")
|
||||
return containsAny(msg, "Duplicate entry", "1062", "duplicate key", "violates unique constraint", "UNIQUE constraint failed")
|
||||
}
|
||||
|
||||
func containsAny(s string, subs ...string) bool {
|
||||
|
||||
Reference in New Issue
Block a user