互斥条件必须带租约。我交接时给的是 WHERE id = ? AND status = 'pending',这是错的:Claim 领取任务时并不改 status,只写 device_id / lease_expires_at / claim_request_id。照此实现会把刚被领走、即将 start 的任务误取消。实施方由自写的并发用例 TestCancelAfterClaimFailsInsteadOfMiscancelling 抓到并修正为 WHERE id = ? AND status = 'pending' AND (lease_expires_at IS NULL OR lease_expires_at <= now),与 Claim 完全对齐。
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
问题
批量创建的采集任务一旦建出来就无法取消。删除只放行
failed:于是批量建出 N 个
pending任务后,中途发现搜错商品、AI 服务不可用或图源不对,唯一的办法是等它们逐个跑完。把设备下线也只是暂停——设备恢复后继续从队列领取。实测图搜成功率并不高(2026-09-16 本地六次四成功),批量越大,中途需要叫停的概率越高。
方案
放开
pending状态的取消,提供单条与批量两个入口,批量入口按条件筛选(来源、状态)。[必须]running仍然禁止取消。它正在设备上操作拼多多,中途打断后页面停在哪一步不可控,下一个任务的归位会受影响——今天已经因为「手机停在深层页面」踩过一次(#292)。所以语义是「停止后续,当前这个跑完」,不是「立刻停止」。取消后的任务进入终态,不再被设备领取;是复用现有状态还是新增取消态,按既有状态机的一致性决定,不得让已取消的任务再被 claim。
非目标
running任务。failed任务的现有行为。验收
pending任务可以取消,取消后不再被任何设备领取。running任务仍然拒绝取消,错误可读。验证
go test ./app/goauto/task/...;本地构造批量 pending 后取消并确认设备不再领取。文档影响
新增取消接口与任务状态语义变化,需更新
docs/08-agent-api-contract.md与状态机相关 Wiki 页面。方案确认:新增
cancelled终态,不需要迁移建单时留的问题(复用
failed还是新增取消态)已定:新增cancelled。两条新查到的事实改变了这个决定的成本:
models/purchase.go:25的PurchaseTaskStatusCancelled = "cancelled"。采集侧新增不是发明新概念,是与既有约定对齐。collection_task.status是varchar(24),cancelled为 9 字符,放得下。建单时预估的"要加迁移"不成立。因此选
cancelled的代价远低于建单时的估计,而复用failed会把"采购员主动取消"和"设备执行失败"混成同一个值,日后拆分需要迁移历史数据。#294 刚证明过把两类不同的东西记成同一个值,排查时要付出代价。并发方案
取消与领取用同一种条件更新模式,不得先读后写:
领取逻辑(
task/service.go约 213 行)本就是这个写法并校验RowsAffected != 1。因此任务一旦被取消,领取的条件更新自然命中 0 行而安全失败;反之任务已被领取时取消也命中 0 行,返回状态冲突而非误取消。范围补充
[必须]需逐一排查cancelled对既有"终态/非终态"判定的影响:任务领取查询、重置的终态判定、删除的状态判定、设备忙碌判定。不创建迁移文件。
状态
实施中。
实施
提交
7c43dc6。由 sonnet 子代理实施,我独立复核并重跑验证。建单与派单时两处事实判断被实施过程纠正
varchar(24)的长度就断言「不需要迁移」,漏查了ck_collection_task_status——它只认旧五个状态,数据库会直接拒绝cancelled。已按同文件ensureMySQLDirectSelectConstraint的手法在migrations/migrate.go补幂等 DROP/ADD(先查约束是否已含cancelled,含则跳过)。不是新建迁移文件。WHERE id = ? AND status = 'pending',这是错的:Claim领取任务时并不改 status,只写device_id/lease_expires_at/claim_request_id。照此实现会把刚被领走、即将 start 的任务误取消。实施方由自写的并发用例TestCancelAfterClaimFailsInsteadOfMiscancelling抓到并修正为WHERE id = ? AND status = 'pending' AND (lease_expires_at IS NULL OR lease_expires_at <= now),与 Claim 完全对齐。另有连锁影响:
cancelled须并入syncGuardSlots终态分支清空active_slot/device_run_slot,否则ck_collection_task_active_slot与ck_collection_task_device_run_slot会拒绝写入。复核阶段补入
BatchCancelResponse.HasMore:超出单批上限时如实上报,不静默截断。采购员界面显示实时数量,截断而不告知会让「取消了 N 个」与眼前数字对不上。已验证
gofmt/go vet/go build干净;task/purchase/access/shopeeproduct全绿;sybimport仍为 12 条失败,与 #285 基线一致。新增 9 个用例,含并发误取消与上限上报。未验证
未在真实环境构造「批量建单后中途取消」的端到端场景。
部署影响
[必须]本次含collection_task.status的 CHECK 约束变更,随服务启动自动执行。上线需按高风险操作单独确认。状态
待验收。