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 供断言
This commit is contained in:
@ -28,8 +28,13 @@ type IngestRepository interface {
|
||||
// Claim 原子领取最多 limit 条 pending 任务并置为 processing,返回这些任务。
|
||||
// 用 SKIP LOCKED 保证多 worker 不抢同一条。
|
||||
Claim(ctx context.Context, limit int) ([]model.IngestJob, error)
|
||||
// MarkDone 标记任务成功。
|
||||
MarkDone(ctx context.Context, id uint32) error
|
||||
// MarkDone 标记任务成功,并记录结果类型与人可读细节。
|
||||
//
|
||||
// outcome 取 model.IngestOutcome* 常量,note 例如「新建 42 张图」「已存在(status=published)」。
|
||||
// 二者是后台「品牌更新记录」的全部信息源:只有 status=done 看不出这次到底新建还是跳过。
|
||||
MarkDone(ctx context.Context, id uint32, outcome, note string) error
|
||||
// ListByBrand 取某品牌的入库记录(按 id 倒序),供品牌详情页展示更新历史。
|
||||
ListByBrand(ctx context.Context, brandID uint32, limit int) ([]BrandIngestRecord, error)
|
||||
// MarkFailed 标记任务失败并记录错误(attempts 自增)。
|
||||
MarkFailed(ctx context.Context, id uint32, errMsg string) error
|
||||
// ReserveNonce 写入一次性随机串;若已存在(重放)返回 ok=false。
|
||||
@ -165,17 +170,51 @@ func (r *ingestRepository) Claim(ctx context.Context, limit int) ([]model.Ingest
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (r *ingestRepository) MarkDone(ctx context.Context, id uint32) error {
|
||||
func (r *ingestRepository) MarkDone(ctx context.Context, id uint32, outcome, note string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&model.IngestJob{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]any{
|
||||
"status": model.IngestStatusDone,
|
||||
"outcome": outcome,
|
||||
"note": note,
|
||||
"last_error": "",
|
||||
"updated_at": uint32(time.Now().Unix()),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// BrandIngestRecord 是品牌详情页需要的「一条更新记录」。
|
||||
//
|
||||
// 只取必要列:payload 可能几十 KB(含全部 look 的 key),一次拉 20 条会传上兆,
|
||||
// 所以标题直接在 SQL 里从 JSON 取出,不回传整段 payload。
|
||||
type BrandIngestRecord struct {
|
||||
ID uint32 `gorm:"column:id"`
|
||||
Title string `gorm:"column:title"`
|
||||
Status string `gorm:"column:status"`
|
||||
Outcome string `gorm:"column:outcome"`
|
||||
Note string `gorm:"column:note"`
|
||||
LastError string `gorm:"column:last_error"`
|
||||
Attempts uint8 `gorm:"column:attempts"`
|
||||
UpdatedAt uint32 `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
// ListByBrand 取某品牌的入库记录(按 id 倒序),供品牌详情页展示更新历史。
|
||||
func (r *ingestRepository) ListByBrand(ctx context.Context, brandID uint32, limit int) ([]BrandIngestRecord, error) {
|
||||
if brandID == 0 || limit <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var rows []BrandIngestRecord
|
||||
if err := r.db.WithContext(ctx).Raw(
|
||||
"SELECT id, status, coalesce(outcome, '') AS outcome, coalesce(note, '') AS note, "+
|
||||
"coalesce(last_error, '') AS last_error, attempts, updated_at, "+
|
||||
"coalesce(payload::json->>'title_en', '') AS title "+
|
||||
"FROM ingest_jobs WHERE brand_id = ? ORDER BY id DESC LIMIT ?",
|
||||
brandID, limit).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *ingestRepository) MarkFailed(ctx context.Context, id uint32, errMsg string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&model.IngestJob{}).
|
||||
|
||||
Reference in New Issue
Block a user