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:
@@ -14,10 +14,26 @@ const TimeLayout = time.RFC3339
|
||||
|
||||
// NowISO 返回当前 UTC 时间的字符串形式。
|
||||
// 所有写库的时间戳都要用它,不要各写各的格式。
|
||||
//
|
||||
// `[必须]` 有代码依赖它产出**定宽 UTC 格式**(如 "2026-08-07T06:10:19Z"):
|
||||
// repository.MarkCollecting 和 service 里判断"采集是否超时",
|
||||
// 靠的是直接用字符串比较 `updated_at < ?`,不解析成时间再比。
|
||||
// 定宽 + 同一时区(UTC)+ 补零,字符串的字典序才等于时间先后顺序。
|
||||
// 改成带时区偏移的本地时间(比如 "+08:00")之后,这个比较会**静默失效**
|
||||
// ——不会报错,但超时判断会全错,见 #24。
|
||||
func NowISO() string {
|
||||
return time.Now().UTC().Format(TimeLayout)
|
||||
}
|
||||
|
||||
// CollectStaleAfter 是采集任务多久没动静就当它死了。
|
||||
//
|
||||
// 采集本身几十秒到两分钟;加上排队等客户端来领,15 分钟足够宽裕。
|
||||
// 宁可短也不要长:采集是**只读**操作,多采一次没有任何副作用,
|
||||
// 而卡死的代价是这个商品永久报废、只能改数据库救,见 #24。
|
||||
//
|
||||
// 定义成有名字的常量,不要把 15 分钟当魔数散落在各处判断里。
|
||||
const CollectStaleAfter = 15 * time.Minute
|
||||
|
||||
// ParseISO 解析库里存的时间字符串。解析不了返回零值和 false。
|
||||
func ParseISO(s string) (time.Time, bool) {
|
||||
if s == "" {
|
||||
|
||||
Reference in New Issue
Block a user