Files
backend_v2/internal/model/ingest_job.go
toom1996 c8a70760cc feat: 入库结果落库 + 品牌详情展示更新记录
背景:任务只有 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 供断言
2026-10-25 11:06:16 +08:00

74 lines
4.0 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" // 实体已软删,跳过(删除即永久)
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
}