Files
backend_v2/internal/model/ingest_job.go
toom1996 e930ac41c8 feat: 爬虫页取代抓取任务/入库任务两页,抓取改全量+覆盖率跳过
一、后台只留一个「爬虫」页(/admin/spider)
抓取与入库本就是同一条流水线的上下游,分两页看谁都看不全。
新页三块:状态条(在线/卡住 + 队列数 + 待入库数)、正在抓(含进度与心跳)、
最近入库结果(品牌/场次/结果/说明)。只读,取消与重试一并下线。

二、抓取一律全量
删除品牌页的场次上限下拉与 hidden maxShows=3,入队固定 0(全量)。
crawl_tasks.max_shows 列保留,历史任务照常显示。

三、跳过判定改覆盖率
- brand_runways 增加 expected_images(spider 上报的应有张数)
- /shows/covered 返回 images + expected
- 实际张数 >= 应有 - 5 才跳过;expected=0 时回落 10 张阈值
- 实体已存在时不再直接跳过:按 key 比对把缺的图补进库(repaired)
  否则「判定不完整→重爬→后端跳过」会形成死循环,带宽白花、缺图永远补不上

四、租约 900→300 秒,且心跳算存活
只看 locked_at 会把跑了几小时的全量抓取误判成卡死;
加上 progress_at 后,真卡住 5 分钟才回收。
2026-10-25 23:59:54 +08:00

75 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package model
// IngestJob 爬虫上报的走秀入库任务队列。
//
// 设计:爬虫 POST 到 :8092 的 ingest 接口(HMAC 验签后)只做「入队 + 立即 202」,
// 真正的去重 / 补分类 / 下载图 / 写正式表 由后台 worker 异步处理,
// 因此接口本身很薄、不会被慢任务(尤其是 S4 上传)拖垮。
//
// 并发安全靠 PostgreSQL 的 `SELECT ... FOR UPDATE SKIP LOCKED`:多个 worker 同时
// Claim 不会抢到同一条,未提交的行对其它 worker 不可见。
type IngestJob struct {
ID uint32 `gorm:"primaryKey;column:id" json:"id"`
CreatedAt uint32 `gorm:"column:created_at" json:"created_at"`
UpdatedAt uint32 `gorm:"column:updated_at" json:"updated_at"`
LockedAt uint32 `gorm:"column:locked_at" json:"locked_at"` // worker 领取时间;用于超时兜底回收
Status string `gorm:"column:status;size:16;not null;default:'pending'" json:"status"` // pending|processing|done|failed
Attempts uint8 `gorm:"column:attempts;not null;default:0" json:"attempts"`
Payload string `gorm:"column:payload;type:text" json:"payload"` // 原始 JSON(RunwayIngest 或 MediaCleanupPayload)
LastError string `gorm:"column:last_error;type:text" json:"last_error"`
Kind string `gorm:"column:kind;size:16;not null;default:'crawl'" json:"kind"` // crawl|media_cleanup
NextAttemptAt uint32 `gorm:"column:next_attempt_at;not null;default:0" json:"next_attempt_at"` // 重试退避:到达该 unix 时间才可被领取
// Outcome 结果类型(见 IngestOutcome* 常量),Note 是人可读的细节。
// 只有 done 状态才有意义:同为 done,「新建 42 张图」和「实体已存在跳过」是完全不同的事,
// 后台的「品牌更新记录」靠这两个字段说清楚每次到底做了什么。
Outcome string `gorm:"column:outcome;size:24;not null;default:''" json:"outcome"`
Note string `gorm:"column:note;type:text;not null;default:''" json:"note"`
// BrandID 提交时就解出的品牌 id(街拍为 0),供品牌详情页直接按品牌查更新记录。
BrandID uint32 `gorm:"column:brand_id;not null;default:0" json:"brand_id"`
}
// 入库结果类型(ingest_jobs.outcome)。
const (
IngestOutcomeCreated = "created" // 新建入站
IngestOutcomeReused = "reused_rejected" // 复用被驳回的记录并覆盖
IngestOutcomeSkippedExisting = "skipped_existing" // 实体已存在,跳过(不重下图片)
IngestOutcomeSkippedDeleted = "skipped_deleted" // 实体已软删,跳过(删除即永久)
IngestOutcomeRepaired = "repaired" // 实体已存在,但补齐了缺失的图片(覆盖率不足时重爬)
IngestOutcomeCleaned = "cleaned" // 媒体清理任务
IngestOutcomeNoop = "noop" // 无需处理(空载荷)
)
// TableName 指定队列表名。
func (IngestJob) TableName() string { return "ingest_jobs" }
// IngestJob 状态常量。
const (
IngestStatusPending = "pending"
IngestStatusProcessing = "processing"
IngestStatusDone = "done"
IngestStatusFailed = "failed"
)
// IngestJob 任务类型(kind 列):区分「爬虫入库」与「清理S4孤儿图」两条管线。
const (
IngestKindCrawl = "crawl" // 爬虫上报的走秀/街拍入库
IngestKindMediaCleanup = "media_cleanup" // 删除图集时异步清理S4孤儿文件
)
// IngestMaxAttempts 单条任务最大处理次数(含首次)。达上限仍未成功则置 failed,需后台手动重试。
const IngestMaxAttempts = 3
// IngestStuckTimeoutSec 任务「卡死」判定阈值:worker 领取后 processing 状态持续超过该秒数,
// 视为 worker 崩溃/被杀遗留,由 Claim 兜底回收为 pending 重领。下载图/传图正常远低于此值。
const IngestStuckTimeoutSec = 300
// IngestRetryBackoff 返回第 attempts 次失败后的退避秒数(指数退避,封顶 600s)。
// attempts 从 1 起算:30s / 60s / 120s。用于失败任务的自动重试调度。
func IngestRetryBackoff(attempts int) int {
d := 30 * (1 << uint(attempts-1))
if d > 600 {
d = 600
}
return d
}