服务端:按地址后缀批量回填采购任务的订单号与下单时间 #241

Open
opened 2026-09-08 11:45:39 +08:00 by ila · 3 comments
Owner

2026-09-08 评审更新:内部系统精简方案

用户要求“内部系统,简单易用,不要复杂门禁”,并授权更新本工单正文。本版收敛为人工点击回填、输入天数、自动扫描、小批提交、显示结果;不增加产品内审批、二次确认、后台任务系统或复杂配置。保留设备归属、冲突不覆盖、设备互斥和禁止支付/确认收货等必要边界。

本次只更新方案,不代表批准实施、原型确认、迁移或真机操作。代码核验基线 main fdd26af;Android 已发布分支 7ac1355 的互斥锁与历史缓存相关文件与此基线一致。原工单真机现象与积压数量是创建时记录,本轮未重新验证,不作为实时统计。

Gitea MCP 当前不可用,回退项目安全配置提供凭据的 Gitea API;未输出或写入凭据。

来源与目标

2026-09-08 用户提出:Agent 侧扫描 PDD「我的订单」,把收货地址含采购任务编号的订单的订单号与下单时间回填到采购任务。原话摘录:

  • 「现在要查看我的订单-全部,获取下单地址包含采购编号的订单的订单号(必须)和下单时间,把这两个数据更新到admin」
  • 「地址里的"_"后面的是采购任务编号,用这个来更新对应的采购任务数据.」
  • 「下单时间读页面,读不到回落irreversible_at.」

本工单只做服务端接收与回填,Agent 端入口与扫描由后续工单实现。

目标:提供一个 Agent 可调用的批量回填接口,按地址后缀定位采购任务,写入 pdd_order_no 与 order_submitted_at,并把 order_result_unknown 推进到 order_created。

当前事实(2026-09-08 核验)

积压数据:

status                  n    有irreversible_at   有pdd_order_no
order_result_unknown    21         21                 0
order_created            3          3                 3

21 个任务全部具备 irreversible_at,全部缺 pdd_order_no,正是本工单的回填对象。

Admin 展示层已完全就绪,本工单不需要改前端:

  • web/src/views/goauto/purchase-tasks/index.vue 列表列「订单 / 支付」已显示 pddOrderNo,空值占位「尚未取得订单号」
  • 详情「订单」卡片已显示 pddOrderNo 与 orderSubmittedAt
  • 查询条件已支持 pdd_order_no LIKE 搜索
  • admin_query.go 已返回这两个字段

缺口一:没有后缀反解函数

server/app/goauto/purchasecontract/contract.go:361 只有生成方向:

func AddressSuffix(taskID uint64) string { return fmt.Sprintf("_cg%d", taskID) }

无反解实现。

缺口二:Agent 没有可用的提交端点

现有 POST /api/agent/v1/purchase-tasks/:taskId/result 绑定在 attempt 生命周期上(需先 claim、start),离线批量回填无法使用。

缺口三:缺少独立的 Agent 回填写入路径

现有人工解除未知结果路径 ResolveUnknown 挂在 admin 路由下,handler.go 要求:

req.OperatorID = operatorID(c)
if req.OperatorID == 0 {
    writeError(c, fail(CodeInvalidRequest, "无法识别当前操作人"))
    return
}

需要 JWT 与 AuthCheckRole(),Agent 不具备,且不应为此放宽 admin 权限模型。

下单时间回落依据(实测):

任务 63 对照,irreversible_at 与页面读到的真实下单时间仅差 140 毫秒:

irreversible_at     2026-09-03 17:01:04.140
order_submitted_at  2026-09-03 17:01:04.000

结构上也一致:markOrderSubmitStarted 就在 submitOrderOnce() 之前一步。仅一个样本,记为假设。

范围

  1. 仅修改 Server 采购域、Agent 路由及共享契约。新增人工触发的小批回填接口;条目使用精确地址后缀、必填订单号、可空页面下单时间。requestId 与批大小上限在 API 契约中明确,不引入批任务表或调度器。
  2. 后缀完整解析为正整数任务号,区分 _cg7 与 _cg72;拒绝零、溢出和歧义。不得照搬 Go 不支持的负向前瞻正则,可完整解析后比较既有 AddressSuffix。请求不含地址全文。
  3. 沿用 Device Token 与既有 Agent HTTP/HTTPS 部署策略,服务端从认证取得 deviceId,仅回填该设备自己的正式采购任务;不开放跨设备,不依赖 Admin JWT,也不新增权限审批。
  4. 单条事务锁定任务后检查当前状态。首版允许 order_result_unknown 回填为 order_created;已为 order_created 且订单号一致视为已回填、不重复更新。其他状态明确拒绝,不自动把运行中、失败、取消或演练任务改成功。
  5. 代码事实修正:models.PurchaseTask.SetStatus 仅设置状态并同步 guard slots,不检查旧状态到新状态的合法转换。新服务负责来源状态校验,再复用 SetStatus;同一事务更新订单字段、statusVersion、statusChangedAt,清理已解除的主任务当前错误,保留历史 attempt 与原始执行结果,不伪造一次采购执行。支付、物流、SYB 回填状态不变。
  6. 页面下单时间优先;缺失则采用该任务 irreversible_at,两者均无则该条失败。统一 Asia/Shanghai 页面时间到 API 时间格式;回落值属于估算,不宣称真实精确时间。本次响应可返回时间来源,不为此新增字段迁移或 Admin 界面;已回填记录不做时间自动校正。
  7. 相同任务、相同订单号重复提交视为已完成,不重复变更状态或版本。已有不同订单号、同一批同任务多个不同订单号、同一订单号对应其他任务时明确冲突并保留原值,不静默覆盖。事务/并发测试须覆盖与人工解除未知状态和旧结果补提交竞争;同 requestId 不同内容不得绕过冲突检查。不为本功能重构全局幂等系统。
  8. 每条独立事务、独立结果;合法条目不因其他条目失败回滚。响应含任务号、成功/已回填/冲突/失败及稳定原因,并返回服务端最终状态和订单时间,供 Agent 刷新缓存。网络结果不明时重复提交仍安全。
  9. 首版不增加“缺订单号任务总数”查询或展示。这是范围裁剪,不是能力否定:服务端可由单条 SQL 给出准确总数,本次仅为先走通主流程而延后。若后续加入,总数必须来自服务端实时查询,不得用本地缓存或当前页数量替代。

非目标

  • 不修改 admin 前端(展示层已就绪)。
  • 不修改 ResolveUnknown 及 admin 权限模型。
  • 不实现 Agent 端 UI、扫描流程或本地缓存(后续工单)。
  • 不新增任务类型、任务表或任务调度机制;本次回填是人工触发的一次性提交,不进任务系统。
  • 不改动采购状态机中与本次无关的其他转换。
  • 不执行支付、不创建订单。
  • 回填范围严格限定为 pdd_order_no 与 order_submitted_at 两个订单字段(2026-09-08 用户确认)。不同步 PDD 订单的商品、金额、订单状态、物流或其他任何信息。此处的“两个字段”指业务回填范围;实现上仍需同步更新采购状态与必要状态元数据(见范围第 5 条),二者不矛盾。

方案与设计证据

后端功能不要求 UI 原型;以本方案、现有采购模型、manual.go、agent_history.go 及单元测试作为设计与核验依据。实现前确定请求/逐条响应和轻量并发处理方式,不引入新任务类型。

数据变更范围为订单字段、对应采购状态及必要状态元数据;并非“只写两个字段”。订单号只进入授权业务字段和响应,日志仅记录任务号、设备号、requestId、结果分类等脱敏元数据,沿任务保留规则快照关联;不记录完整地址、收件人、手机号、请求正文或原始树。

验收

  • _cg7/_cg72、非法后缀、零/溢出与歧义测试通过。
  • 当前设备的正式未知结果任务可回填;其他设备、非法状态和无时间来源拒绝且不改数据。
  • 正确维护主任务状态及版本/占用字段,历史 attempt 保留,支付/物流状态不变。
  • 重复提交为已回填,订单号冲突不覆盖;并发人工操作及旧结果补提交不造成串单或覆盖。
  • 混合批次逐条返回结果,失败不回滚成功条目;响应丢失后安全重放。
  • 设备鉴权与现有协议兼容策略不变,无新审批或任务表。
  • 相关 Go 单测及 go build ./... 通过;真实数据回填另需范围明确的授权,不以单测冒充真机验收。

文档影响

实施时必须更新线上 Android-Agent-API-Contract、Business-Rules-and-Glossary、Architecture-and-Code-Map:新增 Agent 回填入口、设备归属、未知状态解除和时间回落是长期事实变化。在线回读后按仓库流程同步并检查 docs 镜像,不直接编辑镜像。本次仅工单方案更新,不修改 Wiki。

保留原目录无关改动,必要时用干净工作区执行文档同步,不覆盖 docs/12-syb-erp-interface.md。

依赖与后续

无阻塞依赖。后续工单(Agent 回填入口与扫描)依赖本工单的端点先行落地。

实施分工

本工单已建单,用户明确要求暂不实施(原话「建工单,先别做」)。实施启动前需用户再次确认。

## 2026-09-08 评审更新:内部系统精简方案 用户要求“内部系统,简单易用,不要复杂门禁”,并授权更新本工单正文。本版收敛为人工点击回填、输入天数、自动扫描、小批提交、显示结果;不增加产品内审批、二次确认、后台任务系统或复杂配置。保留设备归属、冲突不覆盖、设备互斥和禁止支付/确认收货等必要边界。 本次只更新方案,不代表批准实施、原型确认、迁移或真机操作。代码核验基线 main `fdd26af`;Android 已发布分支 `7ac1355` 的互斥锁与历史缓存相关文件与此基线一致。原工单真机现象与积压数量是创建时记录,本轮未重新验证,不作为实时统计。 Gitea MCP 当前不可用,回退项目安全配置提供凭据的 Gitea API;未输出或写入凭据。 ## 来源与目标 2026-09-08 用户提出:Agent 侧扫描 PDD「我的订单」,把收货地址含采购任务编号的订单的订单号与下单时间回填到采购任务。原话摘录: - 「现在要查看我的订单-全部,获取下单地址包含采购编号的订单的订单号(必须)和下单时间,把这两个数据更新到admin」 - 「地址里的"_"后面的是采购任务编号,用这个来更新对应的采购任务数据.」 - 「下单时间读页面,读不到回落irreversible_at.」 本工单只做**服务端接收与回填**,Agent 端入口与扫描由后续工单实现。 目标:提供一个 Agent 可调用的批量回填接口,按地址后缀定位采购任务,写入 `pdd_order_no` 与 `order_submitted_at`,并把 `order_result_unknown` 推进到 `order_created`。 ## 当前事实(2026-09-08 核验) **积压数据:** ``` status n 有irreversible_at 有pdd_order_no order_result_unknown 21 21 0 order_created 3 3 3 ``` 21 个任务全部具备 `irreversible_at`,全部缺 `pdd_order_no`,正是本工单的回填对象。 **Admin 展示层已完全就绪,本工单不需要改前端:** - `web/src/views/goauto/purchase-tasks/index.vue` 列表列「订单 / 支付」已显示 `pddOrderNo`,空值占位「尚未取得订单号」 - 详情「订单」卡片已显示 `pddOrderNo` 与 `orderSubmittedAt` - 查询条件已支持 `pdd_order_no LIKE` 搜索 - `admin_query.go` 已返回这两个字段 **缺口一:没有后缀反解函数** `server/app/goauto/purchasecontract/contract.go:361` 只有生成方向: ```go func AddressSuffix(taskID uint64) string { return fmt.Sprintf("_cg%d", taskID) } ``` 无反解实现。 **缺口二:Agent 没有可用的提交端点** 现有 `POST /api/agent/v1/purchase-tasks/:taskId/result` 绑定在 attempt 生命周期上(需先 claim、start),离线批量回填无法使用。 **缺口三:缺少独立的 Agent 回填写入路径** 现有人工解除未知结果路径 `ResolveUnknown` 挂在 admin 路由下,`handler.go` 要求: ```go req.OperatorID = operatorID(c) if req.OperatorID == 0 { writeError(c, fail(CodeInvalidRequest, "无法识别当前操作人")) return } ``` 需要 JWT 与 `AuthCheckRole()`,Agent 不具备,且不应为此放宽 admin 权限模型。 **下单时间回落依据(实测):** 任务 63 对照,`irreversible_at` 与页面读到的真实下单时间仅差 140 毫秒: ``` irreversible_at 2026-09-03 17:01:04.140 order_submitted_at 2026-09-03 17:01:04.000 ``` 结构上也一致:`markOrderSubmitStarted` 就在 `submitOrderOnce()` 之前一步。仅一个样本,记为假设。 ## 范围 1. 仅修改 Server 采购域、Agent 路由及共享契约。新增人工触发的小批回填接口;条目使用精确地址后缀、必填订单号、可空页面下单时间。requestId 与批大小上限在 API 契约中明确,不引入批任务表或调度器。 2. 后缀完整解析为正整数任务号,区分 `_cg7` 与 `_cg72`;拒绝零、溢出和歧义。不得照搬 Go 不支持的负向前瞻正则,可完整解析后比较既有 `AddressSuffix`。请求不含地址全文。 3. 沿用 Device Token 与既有 Agent HTTP/HTTPS 部署策略,服务端从认证取得 deviceId,仅回填该设备自己的正式采购任务;不开放跨设备,不依赖 Admin JWT,也不新增权限审批。 4. 单条事务锁定任务后检查当前状态。首版允许 `order_result_unknown` 回填为 `order_created`;已为 `order_created` 且订单号一致视为已回填、不重复更新。其他状态明确拒绝,不自动把运行中、失败、取消或演练任务改成功。 5. 代码事实修正:`models.PurchaseTask.SetStatus` 仅设置状态并同步 guard slots,不检查旧状态到新状态的合法转换。新服务负责来源状态校验,再复用 SetStatus;同一事务更新订单字段、statusVersion、statusChangedAt,清理已解除的主任务当前错误,保留历史 attempt 与原始执行结果,不伪造一次采购执行。支付、物流、SYB 回填状态不变。 6. 页面下单时间优先;缺失则采用该任务 irreversible_at,两者均无则该条失败。统一 Asia/Shanghai 页面时间到 API 时间格式;回落值属于估算,不宣称真实精确时间。本次响应可返回时间来源,不为此新增字段迁移或 Admin 界面;已回填记录不做时间自动校正。 7. 相同任务、相同订单号重复提交视为已完成,不重复变更状态或版本。已有不同订单号、同一批同任务多个不同订单号、同一订单号对应其他任务时明确冲突并保留原值,不静默覆盖。事务/并发测试须覆盖与人工解除未知状态和旧结果补提交竞争;同 requestId 不同内容不得绕过冲突检查。不为本功能重构全局幂等系统。 8. 每条独立事务、独立结果;合法条目不因其他条目失败回滚。响应含任务号、成功/已回填/冲突/失败及稳定原因,并返回服务端最终状态和订单时间,供 Agent 刷新缓存。网络结果不明时重复提交仍安全。 9. 首版不增加“缺订单号任务总数”查询或展示。这是**范围裁剪,不是能力否定**:服务端可由单条 SQL 给出准确总数,本次仅为先走通主流程而延后。若后续加入,总数必须来自服务端实时查询,不得用本地缓存或当前页数量替代。 ## 非目标 - 不修改 admin 前端(展示层已就绪)。 - 不修改 `ResolveUnknown` 及 admin 权限模型。 - 不实现 Agent 端 UI、扫描流程或本地缓存(后续工单)。 - 不新增任务类型、任务表或任务调度机制;本次回填是人工触发的一次性提交,不进任务系统。 - 不改动采购状态机中与本次无关的其他转换。 - 不执行支付、不创建订单。 - **回填范围严格限定为 `pdd_order_no` 与 `order_submitted_at` 两个订单字段**(2026-09-08 用户确认)。不同步 PDD 订单的商品、金额、订单状态、物流或其他任何信息。此处的“两个字段”指业务回填范围;实现上仍需同步更新采购状态与必要状态元数据(见范围第 5 条),二者不矛盾。 ## 方案与设计证据 后端功能不要求 UI 原型;以本方案、现有采购模型、manual.go、agent_history.go 及单元测试作为设计与核验依据。实现前确定请求/逐条响应和轻量并发处理方式,不引入新任务类型。 数据变更范围为订单字段、对应采购状态及必要状态元数据;并非“只写两个字段”。订单号只进入授权业务字段和响应,日志仅记录任务号、设备号、requestId、结果分类等脱敏元数据,沿任务保留规则快照关联;不记录完整地址、收件人、手机号、请求正文或原始树。 ## 验收 - `_cg7`/`_cg72`、非法后缀、零/溢出与歧义测试通过。 - 当前设备的正式未知结果任务可回填;其他设备、非法状态和无时间来源拒绝且不改数据。 - 正确维护主任务状态及版本/占用字段,历史 attempt 保留,支付/物流状态不变。 - 重复提交为已回填,订单号冲突不覆盖;并发人工操作及旧结果补提交不造成串单或覆盖。 - 混合批次逐条返回结果,失败不回滚成功条目;响应丢失后安全重放。 - 设备鉴权与现有协议兼容策略不变,无新审批或任务表。 - 相关 Go 单测及 go build ./... 通过;真实数据回填另需范围明确的授权,不以单测冒充真机验收。 ## 文档影响 实施时必须更新线上 Android-Agent-API-Contract、Business-Rules-and-Glossary、Architecture-and-Code-Map:新增 Agent 回填入口、设备归属、未知状态解除和时间回落是长期事实变化。在线回读后按仓库流程同步并检查 docs 镜像,不直接编辑镜像。本次仅工单方案更新,不修改 Wiki。 保留原目录无关改动,必要时用干净工作区执行文档同步,不覆盖 docs/12-syb-erp-interface.md。 ## 依赖与后续 无阻塞依赖。后续工单(Agent 回填入口与扫描)依赖本工单的端点先行落地。 ## 实施分工 本工单已建单,**用户明确要求暂不实施**(原话「建工单,先别做」)。实施启动前需用户再次确认。
ila self-assigned this 2026-09-08 11:45:39 +08:00
Author
Owner

2026-09-08 用户确认后的调整(Claude 复核 Codex 评审版)

Codex 评审提出的四处代码事实纠错,已逐条在基线 main fdd26af 上独立验证,全部成立:

  1. models.PurchaseTask.SetStatus 确实只做 task.Status = status 加 syncPurchaseGuardSlots(),不校验来源状态到目标状态的合法性。原工单「复用既有 SetStatus 校验」的表述是错的,已由评审版修正。
  2. Go 的 regexp(RE2) 确实不支持负向前瞻,实测 regexp.Compile("_cg(\d+)(?![0-9])") 报 invalid or unsupported Perl syntax: (?!。原工单直接搬用了 Kotlin 侧写法,是错的。
  3. TaskHistoryCache 使用 getSharedPreferences,不是 SQLite;SQLite 的是 PurchaseTaskStore。
  4. TaskExecutionMutex.tryAcquire 含 || activeTask.get() == taskId 分支,对同一 ID 可重入。固定预留 ID 确实防不住重复启动,必须另加原子防重入标记。

用户已确认「内部系统,简单易用,不要复杂门禁」为其本人对 Codex 的原话,评审版的精简方向成立。

本次调整

  • 范围第 9 条:保持首版不做「缺订单号任务总数」,但改写为范围裁剪而非能力否定。原理由「避免把缓存/当前页数量冒充总数」不成立——服务端可由单条 SQL 给出准确总数。后续若加入,须明确要求总数来自服务端实时查询。
  • 非目标新增:回填范围严格限定为 pdd_order_no 与 order_submitted_at 两个订单字段,不同步商品、金额、订单状态、物流等其他信息(用户 2026-09-08 明确确认)。

已撤回的意见

  • 原提「7 天用户可见上限」已撤回:这属于用户明确不要的门禁,且评审版已有内部扫描条数/耗时上限兜底。
  • 原提「弹窗显示缺订单号任务数」已撤回:非走通主流程所必需,可后续按需增加。

本次仅调整工单方案,不实施。

## 2026-09-08 用户确认后的调整(Claude 复核 Codex 评审版) Codex 评审提出的四处代码事实纠错,已逐条在基线 main `fdd26af` 上独立验证,**全部成立**: 1. `models.PurchaseTask.SetStatus` 确实只做 `task.Status = status` 加 `syncPurchaseGuardSlots()`,不校验来源状态到目标状态的合法性。原工单「复用既有 SetStatus 校验」的表述是错的,已由评审版修正。 2. Go 的 `regexp`(RE2) 确实不支持负向前瞻,实测 `regexp.Compile("_cg(\d+)(?![0-9])")` 报 `invalid or unsupported Perl syntax: (?!`。原工单直接搬用了 Kotlin 侧写法,是错的。 3. `TaskHistoryCache` 使用 `getSharedPreferences`,不是 SQLite;SQLite 的是 `PurchaseTaskStore`。 4. `TaskExecutionMutex.tryAcquire` 含 `|| activeTask.get() == taskId` 分支,对同一 ID 可重入。固定预留 ID 确实防不住重复启动,必须另加原子防重入标记。 用户已确认「内部系统,简单易用,不要复杂门禁」为其本人对 Codex 的原话,评审版的精简方向成立。 ### 本次调整 - 范围第 9 条:保持首版不做「缺订单号任务总数」,但改写为**范围裁剪而非能力否定**。原理由「避免把缓存/当前页数量冒充总数」不成立——服务端可由单条 SQL 给出准确总数。后续若加入,须明确要求总数来自服务端实时查询。 - 非目标新增:回填范围严格限定为 `pdd_order_no` 与 `order_submitted_at` 两个订单字段,不同步商品、金额、订单状态、物流等其他信息(用户 2026-09-08 明确确认)。 ### 已撤回的意见 - 原提「7 天用户可见上限」已撤回:这属于用户明确不要的门禁,且评审版已有内部扫描条数/耗时上限兜底。 - 原提「弹窗显示缺订单号任务数」已撤回:非走通主流程所必需,可后续按需增加。 本次仅调整工单方案,**不实施**。
Author
Owner

2026-09-08 Claude 审核 290a17e + 用户确认追加范围

审核通过的部分

独立核验(worktree D:/OPC/goauto-worktrees/issue-241,基线 fdd26af):

  • 范围未越界:9 个文件全在服务端与契约文档,git diff --name-only fdd26af HEAD 无 android/ 或 web/ 命中。
  • 后缀反解正确:ParseAddressSuffix 用 AddressSuffix(id) != suffix 回比替代负向前瞻,天然拒绝 _cg07、_cg+7 等非规范形式;测试覆盖 _cg7/_cg72 区分与 16 个拒绝用例(含前导零、溢出、全角数字、内嵌、首尾空白)。
  • 来源状态校验由服务自己完成,未依赖 SetStatus(符合评审版指出的代码事实)。
  • 设备归属、订单号格式、冲突不覆盖、already_backfilled 幂等、时间回落与来源标记均已实现。
  • 复用 UnknownResolveRequestID 槽写入 backfill:<来源>:<UUIDv5>,记录来源且无需迁移。
  • go build ./...、go test -count=1 ./app/goauto/purchase/... ./app/goauto/purchasecontract/... 强制重跑通过。
  • 实施方主动披露「MySQL 多进程行锁、生产数据及真机回填未验证」,测试使用 SQLite。属实,予以记录。

审核发现

实施将订单号唯一性校验挂入 models.PurchaseTask.BeforeSave,影响 7 个既有 tx.Save 调用点(lifecycle.go:226/256/528、manual.go:83/169、reset.go:132、order_backfill.go:158),超出原工单声明范围。

具体问题:

  1. ErrPurchaseOrderNumberUsed 仅在 order_backfill.go:171 映射为业务错误码,ResolveUnknown / Cancel / lifecycle 三条路径无映射,会向管理员暴露裸 Go 错误。
  2. lifecycle.go:378 的 order_created 结果回传路径处于不可逆边界之后(真单已创建)。该处校验失败会导致整笔结果提交失败、任务滞留 order_submit_started、订单号丢失——正是 #210 已修复的故障形态。
  3. purchase_rule_setting 成为订单号分配的全局互斥锁,存在隐性耦合;单例行缺失时 fail-closed 将影响所有带订单号的保存。该行由迁移 1787983600000_purchase_rules.go 种下,当前库存在,实际风险低。

已核实、推翻的担忧

Agent 侧 PurchaseOutboxUploader.flush() 中 markUploaded 只在 submit 成功后调用,失败条目留在 pending 队列并在下次 flush 重试。因此瞬时数据库失败(锁等待、超时)可自愈,不会丢单。审核初期基于「真单丢失」反对保留钩子的理由不成立,已撤回。

用户决策与追加范围

用户确认:方向一(同一订单号被多个任务使用)应当报错并提醒采购员人工处理,不得静默放过;并确认相关改动放入本工单一起完成,不拆分、不留半成品。

因此本工单范围追加:

  1. 保留 BeforeSave 全局唯一性校验(本次经用户确认,不再视为超范围)。
  2. ErrPurchaseOrderNumberUsed 必须在 ResolveUnknown、Cancel、lifecycle 三条路径映射为明确业务错误码,错误信息需指明冲突对方任务号,供采购员定位。不得暴露裸 Go 错误。
  3. lifecycle.go 的 order_created 路径撞到订单号冲突时必须降级,不得整笔失败:将任务置为 order_result_unknown,并在 error_message 写明「读到订单号 X,但该号已属于任务 CG-yy」。理由是该时刻真单已在 PDD 创建,首要目标是保住「订单已存在」这一事实并交由人工核对;order_result_unknown + admin「处理订单结果未知」弹窗正是既有的人工处理通道,无需新增界面。
  4. 补充测试覆盖第 11、12 条:三条路径的错误码映射、order_created 冲突降级后的状态与 error_message 内容。

记录:相邻问题,不在本工单处理

PurchaseOutboxUploader.flush() 对永久性业务错误也会无限重试(submit 抛出即停在该条,下次继续)。瞬时错误如此处理正确,但永久性业务冲突会卡死队列。属 Agent 侧问题,另行处理,不混入本工单。

## 2026-09-08 Claude 审核 `290a17e` + 用户确认追加范围 ### 审核通过的部分 独立核验(worktree `D:/OPC/goauto-worktrees/issue-241`,基线 `fdd26af`): - 范围未越界:9 个文件全在服务端与契约文档,`git diff --name-only fdd26af HEAD` 无 android/ 或 web/ 命中。 - 后缀反解正确:`ParseAddressSuffix` 用 `AddressSuffix(id) != suffix` 回比替代负向前瞻,天然拒绝 `_cg07`、`_cg+7` 等非规范形式;测试覆盖 `_cg7`/`_cg72` 区分与 16 个拒绝用例(含前导零、溢出、全角数字、内嵌、首尾空白)。 - 来源状态校验由服务自己完成,未依赖 `SetStatus`(符合评审版指出的代码事实)。 - 设备归属、订单号格式、冲突不覆盖、`already_backfilled` 幂等、时间回落与来源标记均已实现。 - 复用 `UnknownResolveRequestID` 槽写入 `backfill:<来源>:<UUIDv5>`,记录来源且无需迁移。 - `go build ./...`、`go test -count=1 ./app/goauto/purchase/... ./app/goauto/purchasecontract/...` 强制重跑通过。 - 实施方主动披露「MySQL 多进程行锁、生产数据及真机回填未验证」,测试使用 SQLite。属实,予以记录。 ### 审核发现 实施将订单号唯一性校验挂入 `models.PurchaseTask.BeforeSave`,影响 7 个既有 `tx.Save` 调用点(`lifecycle.go:226/256/528`、`manual.go:83/169`、`reset.go:132`、`order_backfill.go:158`),超出原工单声明范围。 具体问题: 1. `ErrPurchaseOrderNumberUsed` 仅在 `order_backfill.go:171` 映射为业务错误码,`ResolveUnknown` / `Cancel` / `lifecycle` 三条路径无映射,会向管理员暴露裸 Go 错误。 2. `lifecycle.go:378` 的 `order_created` 结果回传路径处于不可逆边界之后(真单已创建)。该处校验失败会导致整笔结果提交失败、任务滞留 `order_submit_started`、订单号丢失——正是 #210 已修复的故障形态。 3. `purchase_rule_setting` 成为订单号分配的全局互斥锁,存在隐性耦合;单例行缺失时 fail-closed 将影响所有带订单号的保存。该行由迁移 `1787983600000_purchase_rules.go` 种下,当前库存在,实际风险低。 ### 已核实、推翻的担忧 Agent 侧 `PurchaseOutboxUploader.flush()` 中 `markUploaded` 只在 `submit` 成功后调用,失败条目留在 pending 队列并在下次 flush 重试。因此**瞬时**数据库失败(锁等待、超时)可自愈,不会丢单。审核初期基于「真单丢失」反对保留钩子的理由不成立,已撤回。 ### 用户决策与追加范围 用户确认:方向一(同一订单号被多个任务使用)应当报错并提醒采购员人工处理,不得静默放过;并确认相关改动放入本工单一起完成,不拆分、不留半成品。 因此本工单范围追加: 10. **保留** `BeforeSave` 全局唯一性校验(本次经用户确认,不再视为超范围)。 11. `ErrPurchaseOrderNumberUsed` 必须在 `ResolveUnknown`、`Cancel`、`lifecycle` 三条路径映射为明确业务错误码,错误信息需指明冲突对方任务号,供采购员定位。不得暴露裸 Go 错误。 12. **`lifecycle.go` 的 `order_created` 路径撞到订单号冲突时必须降级,不得整笔失败**:将任务置为 `order_result_unknown`,并在 `error_message` 写明「读到订单号 X,但该号已属于任务 CG-yy」。理由是该时刻真单已在 PDD 创建,首要目标是保住「订单已存在」这一事实并交由人工核对;`order_result_unknown` + admin「处理订单结果未知」弹窗正是既有的人工处理通道,无需新增界面。 13. 补充测试覆盖第 11、12 条:三条路径的错误码映射、`order_created` 冲突降级后的状态与 `error_message` 内容。 ### 记录:相邻问题,不在本工单处理 `PurchaseOutboxUploader.flush()` 对永久性业务错误也会无限重试(`submit` 抛出即停在该条,下次继续)。瞬时错误如此处理正确,但永久性业务冲突会卡死队列。属 Agent 侧问题,另行处理,不混入本工单。
Author
Owner

已完成并推送,待验收。

实现

分支 feat/241-order-backfill-endpoint(独立 worktree D:/OPC/goauto-worktrees/issue-241),基于 fdd26af。

  • 290a17e feat(purchase): add agent order backfill endpoint (#241) —— 端点主体、后缀反解、逐条事务与结果、幂等、时间回落。
  • 72b8b5d fix(purchase): preserve conflicting order results for manual resolution (#241) —— 审核后追加范围。
  • ca7f768 / 482ba34 —— Wiki 镜像同步。

追加范围的实现:

  • 错误类型由 sentinel 升级为 *models.PurchaseOrderNumberUsedError,携带冲突对方 TaskID,并实现 Unwrap() 返回原 sentinel,故 order_backfill.go 既有 errors.Is 判定不受影响。
  • 新增 PURCHASE_ORDER_NUMBER_ALREADY_USED,覆盖 ResolveUnknown、Cancel、lifecycle 三条路径,HTTP 409、retryable=false,消息含冲突对方任务号。回填路径继续使用原有 PURCHASE_BACKFILL_ORDER_ALREADY_USED。
  • lifecycle.go 的 order_created 路径改为降级而非失败:仅当错误确为订单号冲突时(errors.As 判定,其他错误仍硬失败)把任务降为 order_result_unknown,pdd_order_no 留空以维持唯一性,冲突号以「读到订单号 X,但该号已属于任务 CG-yy」写入任务与 attempt 的 error_message,保留 order_submitted_at 与 irreversible_at,释放租约与运行槽。

审核验证(Claude 独立执行)

  • go build ./...、go test -count=1 ./app/goauto/... 强制重跑通过。
  • git diff --check、git diff fdd26af HEAD --check clean。
  • 范围核对:git diff --name-only fdd26af HEAD 无 android/ 或 web/ 命中;290a17e 哈希未被改写。
  • 主工作区与本工单无关的既有改动(docs/12-syb-erp-interface.md、server/config/settings.yml 及未跟踪文件)全程未被触碰——本工单在独立 worktree 中完成,正是为此。

关键测试覆盖已逐项核对:SubmitResult(order_created) 撞冲突后状态为 order_result_unknown、StatusVersion 恰好 +1、pdd_order_no 为空、错误证据含对方任务号、下单时间与不可逆时间保留、attempt 标记失败并保留原始 order_created 结果类型、重复提交返回 Replayed 且版本不变、对方任务未被修改、全库 pdd_order_no 该值仅 1 条,并最终通过 ResolveUnknown 用正确订单号人工核销为 order_created——闭环成立。

文档(Wiki-first 已补正)

实施阶段曾按指示直接编辑镜像 docs/08-agent-api-contract.md,这违反「先改线上 Wiki」的规则,责任在派工指令,实施方已在文中自行标注该偏离。审核阶段已补正:先更新线上页面并在线回读,再由 harness.py sync 从线上重建镜像,未反向覆盖 Wiki。

三个页面均已更新并回读:

  • Android-Agent-API-Contract revision 1f5ee1b29c66773fa571d862241b63f02dae283b
  • Business-Rules-and-Glossary revision afb3eb896e41d9aea0f15ad2682ec70b7a3924ea
  • Architecture-and-Code-Map revision d547c17924ac53422232ad9d6c55a34c8cd8d63c

harness.py sync 与 sync --check 均通过,全部镜像一致。

未验证 / 未执行

  • 测试使用 SQLite,MySQL 多进程行锁、生产数据回填、真机行为未验证(实施方主动披露,审核确认属实)。
  • 未合并到 main,未部署。分支已推送至 origin/feat/241-order-backfill-endpoint。
  • 未执行支付、未创建订单、未连接手机、未执行 adb。

相邻问题(已记录,不在本工单处理)

PurchaseOutboxUploader.flush() 对永久性业务错误也会无限重试(submit 抛出即停在该条,下次继续)。瞬时错误如此处理正确,但永久性业务冲突会卡死上传队列。属 Agent 侧,建议并入 #242 或另建工单。

已完成并推送,待验收。 ## 实现 分支 `feat/241-order-backfill-endpoint`(独立 worktree `D:/OPC/goauto-worktrees/issue-241`),基于 `fdd26af`。 - `290a17e feat(purchase): add agent order backfill endpoint (#241)` —— 端点主体、后缀反解、逐条事务与结果、幂等、时间回落。 - `72b8b5d fix(purchase): preserve conflicting order results for manual resolution (#241)` —— 审核后追加范围。 - `ca7f768` / `482ba34` —— Wiki 镜像同步。 追加范围的实现: - 错误类型由 sentinel 升级为 `*models.PurchaseOrderNumberUsedError`,携带冲突对方 `TaskID`,并实现 `Unwrap()` 返回原 sentinel,故 `order_backfill.go` 既有 `errors.Is` 判定不受影响。 - 新增 `PURCHASE_ORDER_NUMBER_ALREADY_USED`,覆盖 `ResolveUnknown`、`Cancel`、`lifecycle` 三条路径,HTTP 409、`retryable=false`,消息含冲突对方任务号。回填路径继续使用原有 `PURCHASE_BACKFILL_ORDER_ALREADY_USED`。 - `lifecycle.go` 的 `order_created` 路径改为降级而非失败:仅当错误确为订单号冲突时(`errors.As` 判定,其他错误仍硬失败)把任务降为 `order_result_unknown`,`pdd_order_no` 留空以维持唯一性,冲突号以「读到订单号 X,但该号已属于任务 CG-yy」写入任务与 attempt 的 `error_message`,保留 `order_submitted_at` 与 `irreversible_at`,释放租约与运行槽。 ## 审核验证(Claude 独立执行) - `go build ./...`、`go test -count=1 ./app/goauto/...` 强制重跑通过。 - `git diff --check`、`git diff fdd26af HEAD --check` clean。 - 范围核对:`git diff --name-only fdd26af HEAD` 无 `android/` 或 `web/` 命中;`290a17e` 哈希未被改写。 - 主工作区与本工单无关的既有改动(`docs/12-syb-erp-interface.md`、`server/config/settings.yml` 及未跟踪文件)全程未被触碰——本工单在独立 worktree 中完成,正是为此。 关键测试覆盖已逐项核对:`SubmitResult(order_created)` 撞冲突后状态为 `order_result_unknown`、`StatusVersion` 恰好 +1、`pdd_order_no` 为空、错误证据含对方任务号、下单时间与不可逆时间保留、attempt 标记失败并保留原始 `order_created` 结果类型、重复提交返回 `Replayed` 且版本不变、对方任务未被修改、全库 `pdd_order_no` 该值仅 1 条,并最终通过 `ResolveUnknown` 用正确订单号人工核销为 `order_created`——闭环成立。 ## 文档(Wiki-first 已补正) 实施阶段曾按指示直接编辑镜像 `docs/08-agent-api-contract.md`,这违反「先改线上 Wiki」的规则,责任在派工指令,实施方已在文中自行标注该偏离。审核阶段已补正:先更新线上页面并在线回读,再由 `harness.py sync` 从线上重建镜像,未反向覆盖 Wiki。 三个页面均已更新并回读: - `Android-Agent-API-Contract` revision `1f5ee1b29c66773fa571d862241b63f02dae283b` - `Business-Rules-and-Glossary` revision `afb3eb896e41d9aea0f15ad2682ec70b7a3924ea` - `Architecture-and-Code-Map` revision `d547c17924ac53422232ad9d6c55a34c8cd8d63c` `harness.py sync` 与 `sync --check` 均通过,全部镜像一致。 ## 未验证 / 未执行 - 测试使用 SQLite,**MySQL 多进程行锁、生产数据回填、真机行为未验证**(实施方主动披露,审核确认属实)。 - 未合并到 main,未部署。分支已推送至 `origin/feat/241-order-backfill-endpoint`。 - 未执行支付、未创建订单、未连接手机、未执行 adb。 ## 相邻问题(已记录,不在本工单处理) `PurchaseOutboxUploader.flush()` 对永久性业务错误也会无限重试(`submit` 抛出即停在该条,下次继续)。瞬时错误如此处理正确,但永久性业务冲突会卡死上传队列。属 Agent 侧,建议并入 #242 或另建工单。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#241