fix: 商品卡在「采集中」无法恢复 (#24)
MarkCollecting 原本只在 pending/failed 时成功,而全库只有客户端提交结果 才会把 collecting 改走。客户端离线、崩溃、任务被删都是常态——一旦发生, 这个商品就永久报废,界面上没有任何入口能救,只能改数据库。 改成 collecting 超过 15 分钟视为已超时,允许重新创建采集任务。 判定在读取那一刻现算,仍是同一条原子 UPDATE,不加后台清理协程: 后台扫要处理"扫到一半客户端正好提交了"的竞态,本项目已经在并发上 栽过两次,能不引入并发就不引入。 15 分钟写成有名字的常量并注明理由:采集本身几十秒到两分钟,加上排队 等客户端来领。宁可短也不要长——采集是只读的,多采一次没有副作用, 而卡死的代价是商品永久报废。 已知取舍:超时后老任务还在队列里,客户端上线可能两个都领走、采两次。 可接受(只读,后一次覆盖前一次),已写进代码注释和 03-data-model.md, 免得后来人当成 bug 去"修"成加锁或加租约——租约和心跳是被明确移除的设计。 时间比较用字符串,依赖 NowISO 产出定宽 UTC。已在 NowISO 上加注释: 改成带时区偏移的本地时间会让这个比较静默失效,不报错但判断全错。 界面两处:超时的显示「采集中(超时)」,否则操作员盯着「采集中」 不知道它已经死了;跳过原因按正在采集/已采集/已删除分类计数, 一个都没建成时告诉操作员还要等多久。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+31
-6
@@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
)
|
||||
@@ -350,17 +351,41 @@ func SetCollectFailed(q Execer, pddGoodsID, msg, artifactRef string) error {
|
||||
|
||||
// MarkCollecting 把商品置为"采集中"。创建采集任务时调。
|
||||
//
|
||||
// 只有 pending / failed 状态才允许发起采集:
|
||||
// 已经是 collecting 的说明有任务在跑,再建一个就是重复采集,浪费一次。
|
||||
// 返回 false 表示当前状态不允许,调用方应跳过并告诉操作员。
|
||||
// 允许发起采集的状态:
|
||||
// - pending / failed —— 没有任务在跑;
|
||||
// - collecting **且已经超时**(`updated_at` 早于 `now - model.CollectStaleAfter`)
|
||||
// —— 客户端离线、崩溃或任务被删都会让一个 collecting 卡住不动,
|
||||
// 这些是常态不是异常,界面上必须有出口,见 #24。
|
||||
//
|
||||
// `[必须]` 超时判定在**读取的这一刻现算**(直接拼进这条 UPDATE 的 WHERE 里),
|
||||
// 不是靠后台协程定期把超时的状态改回 pending。本项目已经在并发上栽过两次
|
||||
// (PRAGMA 没作用到连接池、`BEGIN DEFERRED` 死锁),能不引入并发就不引入——
|
||||
// 现算没有调度、没有窗口期,天然不会有"扫到一半客户端正好提交了结果"这种竞态。
|
||||
//
|
||||
// `[必须]` 用**字符串比较** `updated_at < ?`,不解析成 time.Time 再比。
|
||||
// 这是安全的,但依赖 model.NowISO() 永远产出定宽 UTC 格式,见那里的注释。
|
||||
//
|
||||
// `[已知取舍]` 超时后允许重新建任务,但**老任务还留在队列里**,
|
||||
// 客户端上线后可能把新旧两个任务都领走,同一个商品被采两次。
|
||||
// 这是可以接受的:采集是只读操作,没有任何副作用,后一次的结果覆盖前一次,
|
||||
// 数据仍然正确。**不要看到"可能采两次"就加锁或加租约去"修"它**
|
||||
// ——租约和心跳是被明确移除过的设计,见 docs/client/00-glossary.md
|
||||
// 「为什么没有租约和心跳」一节;这条取舍详见 docs/admin/03-data-model.md §4.1。
|
||||
//
|
||||
// 返回 false 表示当前状态不允许发起采集(真的在采集且没超时、已采集、
|
||||
// 或商品不存在/已删除),调用方应跳过并把原因告诉操作员。
|
||||
func MarkCollecting(q Execer, pddGoodsID string) (bool, error) {
|
||||
now := model.NowISO()
|
||||
now := time.Now()
|
||||
nowISO := now.UTC().Format(model.TimeLayout)
|
||||
staleBefore := now.Add(-model.CollectStaleAfter).UTC().Format(model.TimeLayout)
|
||||
|
||||
res, err := q.Exec(`
|
||||
UPDATE pdd_products
|
||||
SET collect_status = 'collecting', updated_at = ?
|
||||
WHERE goods_id = ? AND deleted_at IS NULL
|
||||
AND collect_status IN ('pending', 'failed')`,
|
||||
now, pddGoodsID)
|
||||
AND ( collect_status IN ('pending', 'failed')
|
||||
OR (collect_status = 'collecting' AND updated_at < ?) )`,
|
||||
nowISO, pddGoodsID, staleBefore)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("标记 PDD 商品 %s 采集中失败: %w", pddGoodsID, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user