背景:任务只有 status=done,分不清「新建」「已存在跳过」「已删除跳过」, 品牌更新历史只能在进程日志里翻,日志一滚就没了。 - ingest_jobs 新增 outcome / note / brand_id(迁移 2026-10-25-01) outcome 记结果类型,note 记人话细节(新建 N 张图 / 已存在(status=published)) - 提交任务时解出 brand_id 落库,品牌详情页按 (brand_id, id DESC) 直接查 - 处理时优先用 job.brand_id,payload 里的 hashid 只作老任务兜底: 避免「别的实例盐不同 → 解出错品牌 → 整场 brand not found」再发生 - 品牌编辑页新增「更新记录」区块:时间 / 场次 / 结果 / 明细(含失败原因与尝试次数) - 测试:fake 仓储补齐新接口,capture 仓储记录 outcome 供断言
74 lines
4.0 KiB
Go
74 lines
4.0 KiB
Go
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" // 实体已软删,跳过(删除即永久)
|
||
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
|
||
}
|