From 77a29cf159b6638b74a98256003a63643028fe39 Mon Sep 17 00:00:00 2001 From: ila Date: Fri, 7 Aug 2026 16:36:57 +0800 Subject: [PATCH] docs: import wiki at afc651f75a3a --- T-249.-.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 T-249.-.md diff --git a/T-249.-.md b/T-249.-.md new file mode 100644 index 0000000..3405cb7 --- /dev/null +++ b/T-249.-.md @@ -0,0 +1,119 @@ + +> 同步来源:[`docs/tasks/T-249.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-249.md) · commit `afc651f75a3a` + +--- +id: T-249 +title: 采购任务安全重试与重复采购防护 +phase: 2 +deps: + - T-218 + - T-223 + - T-248 +status: DONE +created: 2026-07-30 +context_ref: 773a3cd +work_branch: null +write_paths: + - docs/tasks/T-249.md + - docs/current-state.md + - docs/api.md + - backend-api/migrations/** + - backend-api/internal/domain/** + - backend-api/internal/usecase/** + - backend-api/internal/repository/sqlite/** + - backend-api/internal/transport/httpapi/** + - backend-api/internal/transport/webui/** +--- + +## 需求 / 背景 + +ERP 货运商品当前按 `(freight_order_item_id, source_revision)` 只允许创建一条采购需求, +同一采购需求也只关联一个采购任务。前一个唯一约束可以防止页面双击、网络重试和多人操作 +形成重复采购,必须保留;但采购任务因设备中断、自动化失败或下单前人工取消进入 +`FAILED/CANCELED` 后没有恢复入口,采购人员只能看到历史任务,无法继续采购。 + +本任务不开放“再次创建相同采购订单”。同一 ERP 商品 revision 继续只有一个业务采购需求 +和一个采购任务身份;符合安全条件时,将原任务重新排入 `PENDING`,由 Roubao 领取后创建 +新的 execution attempt。既有候选、证据、人工选择、失败结果和事件全部保留。 + +## 修订契约 + +### 需求和任务身份 + +- `UNIQUE (freight_order_item_id, source_revision)` 保持不变;重复创建采购需求继续幂等返回 + 原需求。 +- `procurement_requests.purchase_task_id` 与 `purchase_task_sources` 的一对一来源关系保持 + 不变;安全重试不复制参考图、不创建第二个任务 ID,也不改写 ERP 来源快照。 +- Admin 文案使用“重新执行采购任务”,不得表示已创建新的拼多多订单。 + +### 可重试状态和下单围栏 + +- 只允许 `FAILED` 或 `CANCELED` 任务重新排队;`PENDING/CLAIMED/RUNNING/ + WAITING_CONFIRMATION/SUCCEEDED` 一律拒绝。 +- 事务内检查该任务不存在任何 `order_submissions` 记录,且历史 execution/outcome 均未 + 标记 `order_submitted=true`。只要已创建 `FENCED/RECONCILED/MANUAL_REVIEW` 提交记录, + 或是否下单无法确认,就保持禁止重试。 +- 重试成功将任务改为 `PENDING`、增加 `version`、清除当前 claim 和取消控制字段;任务 + 标题、SKU、数量、预算、参考图、来源关联和全部历史执行数据保持不变。 +- Roubao 再次领取时沿用既有 claim generation 和 execution attempt 递增机制;旧 claim、 + authorization、command 和 execution 不能授权新一轮动作。 + +### 原因、审计和幂等 + +- 重试必须提交原因码:`AUTOMATION_FAILED`、`DEVICE_INTERRUPTED`、 + `ADMIN_CANCELED` 或 `OTHER`。`OTHER` 必须填写说明,说明去除首尾空格后最多 500 个 + UTF-8 字节。 +- v20 保存不可变重试审计:任务、重试序号、重试前状态、原因、操作人和时间;同时产生 + `TASK_REQUEUED` 任务事件。 +- Admin API 和 SSR 都要求幂等键。相同键和相同请求返回当前任务;相同键不同请求返回 + 幂等冲突。并发请求最多一次从终态切换到 `PENDING`。 +- 稳定错误区分状态冲突和下单不确定:前者为 `TASK_RETRY_NOT_ALLOWED`,后者为 + `TASK_RETRY_ORDER_UNCERTAIN`,不得用通用 500 掩盖。 + +### Admin 交互 + +- 任务详情仅在状态和下单历史表明可重试时显示“重新执行”入口;表单要求选择原因, + `OTHER` 提供说明并在服务端再次校验。 +- 有提交围栏或待付款订单时不显示可执行按钮,明确提示必须先核对拼多多订单,禁止重复 + 下单。 +- 重试成功返回同一任务详情并显示“任务已重新排队”;货运详情继续链接同一采购任务。 + +## 方案 + +1. v20 增加 `task_retries` 审计表,并重建 `task_events` 约束以允许 + `TASK_REQUEUED`;Down 在已有重试记录或事件时 fail-closed。 +2. `TaskService` 增加受校验的 retry command;领域层集中维护原因码、状态判断和长度限制。 +3. SQLite immediate 写事务完成幂等读取、安全门禁、审计插入、任务状态更新和事件写入, + 并同时检查 `task_executions`、`execution_outcomes` 与 `order_submissions`。 +4. Admin JSON API 增加 `POST /api/v1/tasks/:id/retry`;SSR 增加 + `POST /tasks/:id/retry` 和任务详情重试表单。 +5. 任务详情返回是否可重试及阻塞原因;最终安全裁决始终在仓储事务内完成,页面判断只 + 用于交互提示。 + +## 验收要点 + +- [x] 同一货运商品 revision 仍不能创建第二条采购需求或第二个任务 ID。 +- [x] `FAILED/CANCELED` 且无下单痕迹的任务可原子恢复为 `PENDING`。 +- [x] 新一次 Roubao 领取创建递增的 execution attempt,旧证据和结果仍可审计。 +- [x] 活跃、成功、已围栏、待人工复核或待付款任务均不能重试。 +- [x] 重试原因、操作人、原状态和时间可查询;`OTHER` 无说明时拒绝。 +- [x] API/SSR 幂等、并发和稳定错误码测试通过。 +- [x] v20 上下迁移、Go 单测、race、vet 和入口构建通过。 + +## 边界 + +- 本任务不实现拆单、部分数量补采或同一需求多任务;这些场景必须先扩展拼多多订单的 + 已付款、取消、关闭、退款和实购数量状态,不能仅凭任务失败状态猜测。 +- 不删除或覆盖旧 execution、候选、授权、命令、证据、结果、提交围栏和订单编号。 +- 不绕过拼多多登录、验证码、风险控制或支付边界。 + +## 执行记录 + +- 2026-07-30:冻结“单一采购需求/任务身份,多次受控 execution”的恢复契约;已产生 + 下单围栏或下单状态不确定时 fail-closed。 +- 2026-07-30:新增 v20 `task_retries` 审计和 `TASK_REQUEUED` 事件;Admin API/SSR + 支持带幂等键和原因的原任务重新排队,SQLite 事务同时检查 execution outcome 与 + order submission 下单信号。 +- 2026-07-30:`go test ./...`、`go test -race ./...`、`go vet ./...` 和三个 Go + 入口构建通过;真实 Gin/SQLite + Playwright 验证 `CANCELED -> PENDING`、同一任务 + ID、重试历史和成功提示,1440px 与 390px 视口均无横向溢出。