feat: 抓取任务队列 + 媒体机直传 + 向量回填
crawl(品牌抓取任务队列) - /admin/internal/crawl/tasks 领取 / :id/result 回报,spider 可常驻拉任务 - 后台品牌列表支持「加入抓取队列」,队列页可看状态与重试 - 租约超时的 running 自动回收为 queued,避免 worker 崩溃后任务卡死 media(媒体机直传) - ingest 载荷新增 main_key/detail_keys/main_size/detail_sizes: 有 key 就直接落库,跳过下载与上传;与 URL 形态可逐图混用 - 细节图按 Details/DetailKeys 两者较长者遍历(只给 key 时不再丢图) - 媒体机上传的 key 不进失败回滚清单(内容寻址、可能与他人共享) - /admin/internal/upload-url 签发 S4 PUT 预签名(spider 不持有 AK/SK) embeddings(向量回填) - GET /admin/internal/embeddings/pending 派发缺向量的图(含可直接下载的 url) - POST /admin/internal/embeddings 回写向量并重算近邻;失败项计入 image_embedding_failures,连续失败到上限后退出队列(坏图不占位) - 近邻重算改为异步串行:同步会让 100 张的批次 POST 超时(实测 >60s) - 新增 key 分支单测:混用 key/url、细节图只给 key、字节数、回滚清单、向量占位
This commit is contained in:
@ -52,6 +52,16 @@ type BrandRepository interface {
|
||||
// 规则:先按品牌总图片数取前 limit 个热门品牌,再为每个品牌取最新一篇走秀。
|
||||
// 返回的是仓储层裸数据(数字主键),对外编码由 service 层负责。
|
||||
PopularWithCover(ctx context.Context, limit int) ([]PopularBrandRow, error)
|
||||
|
||||
// StatsByIDs 批量统计若干品牌的「图集数 / 图片数」,供后台列表展示数据量。
|
||||
// 只查当前页的 id,避免为了两列统计把整张品牌表都聚合一遍。
|
||||
StatsByIDs(ctx context.Context, ids []uint32) (map[uint32]BrandStats, error)
|
||||
}
|
||||
|
||||
// BrandStats 品牌数据量统计(后台列表用)。
|
||||
type BrandStats struct {
|
||||
Runways int64 // 未删除的图集数
|
||||
Images int64 // 未删除的图片数
|
||||
}
|
||||
|
||||
// PopularBrandRow 热门品牌裸数据:品牌数字主键 + 中英文名称 + 代表走秀封面/中英文标题/文章 ID。
|
||||
@ -174,11 +184,30 @@ func (r *brandRepository) list(ctx context.Context, q dto.BrandQuery, restrictID
|
||||
return []model.Brand{}, 0, nil
|
||||
}
|
||||
|
||||
var items []model.Brand
|
||||
if err := r.db.WithContext(ctx).
|
||||
// 后台可按「最近有动静」排序:走秀图集的 updated_at 与抓取任务的 updated_at 取大。
|
||||
//
|
||||
// 性能注意:不要写成逐行的相关子查询(2.3 万品牌 × 2 个子查询实测 2.7 秒)。
|
||||
// 这里先把两张小表各自按品牌聚合(brand_runways / crawl_tasks 都只有几百行),
|
||||
// 再 LEFT JOIN 到品牌表排序 —— 一次哈希连接 + 一次排序,量级降到百毫秒。
|
||||
// 公开侧不传 Sort,保持原有 name_en ASC,前台顺序不变。
|
||||
itemsQ := r.db.WithContext(ctx).
|
||||
Model(&model.Brand{}).
|
||||
Scopes(scope).
|
||||
Order("name_en ASC").
|
||||
Scopes(scope)
|
||||
if admin && q.Sort == "updated" {
|
||||
itemsQ = itemsQ.
|
||||
Joins(`LEFT JOIN (SELECT brand_id, max(updated_at) AS u
|
||||
FROM brand_runways WHERE is_deleted = 0 GROUP BY brand_id) br
|
||||
ON br.brand_id = brands.id`).
|
||||
Joins(`LEFT JOIN (SELECT brand_id, max(updated_at) AS u
|
||||
FROM crawl_tasks GROUP BY brand_id) ct
|
||||
ON ct.brand_id = brands.id`).
|
||||
Order("GREATEST(coalesce(br.u, 0), coalesce(ct.u, 0)) DESC, name_en ASC")
|
||||
} else {
|
||||
itemsQ = itemsQ.Order("name_en ASC")
|
||||
}
|
||||
|
||||
var items []model.Brand
|
||||
if err := itemsQ.
|
||||
Offset(q.Offset()).
|
||||
Limit(q.Size).
|
||||
Find(&items).Error; err != nil {
|
||||
@ -319,6 +348,36 @@ func (r *brandRepository) CrawlBrands(ctx context.Context, brandID uint32) ([]mo
|
||||
// 3. 结果按热门品牌排名顺序返回
|
||||
//
|
||||
// 注意:created_at/updated_at 是数据导入时间,不能代表走秀新旧,因此必须用 year + season_code。
|
||||
// StatsByIDs 批量统计图集数 / 图片数(只针对传入的 id,通常是一页 30 个)。
|
||||
func (r *brandRepository) StatsByIDs(ctx context.Context, ids []uint32) (map[uint32]BrandStats, error) {
|
||||
out := make(map[uint32]BrandStats, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
var rows []struct {
|
||||
BrandID uint32
|
||||
Runways int64
|
||||
Images int64
|
||||
}
|
||||
// 一次查询同时拿两个计数:按品牌聚合图集,再左连图片聚合,避免 N+1。
|
||||
err := r.db.WithContext(ctx).Raw(`
|
||||
SELECT r.brand_id,
|
||||
count(DISTINCT r.id) AS runways,
|
||||
count(i.id) AS images
|
||||
FROM brand_runways r
|
||||
LEFT JOIN brand_runway_images i
|
||||
ON i.runway_id = r.id AND i.is_deleted = 0
|
||||
WHERE r.is_deleted = 0 AND r.brand_id IN ?
|
||||
GROUP BY r.brand_id`, ids).Scan(&rows).Error
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
out[row.BrandID] = BrandStats{Runways: row.Runways, Images: row.Images}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *brandRepository) PopularWithCover(ctx context.Context, limit int) ([]PopularBrandRow, error) {
|
||||
if limit < 1 {
|
||||
limit = 20
|
||||
|
||||
Reference in New Issue
Block a user