创建采购任务:AI 调用移出事务、幂等前置与批量去重(超时仅临时缓解) #147

Closed
opened 2026-08-29 10:26:52 +08:00 by ila · 3 comments
Owner

最终实施版(2026-08-29)。 本版在全栈复核后固定实施边界:不新增迁移;并发相同 requestId 只保证单任务,不虚假承诺跨实例 provider 单次调用;批量创建仅在最终 Create 阶段执行匹配;输入指纹使用规范化结构。历史评论仅作决策记录,冲突时以本正文为准。

来源与范围

  • 来源:创建 SYB 商品 260828G6YQNX6M 的采购任务时报 timeout of 10000ms exceeded。
  • 前置于 #148;本单保留不会被异步化推翻的永久修复。
  • Server + Admin;不改页面结构、接口字段、数据库和匹配裁决口径,免原型。

已复核事实

  1. purchase.Service.Create 当前在数据库事务内调用 matcher.Resolve,并在此前锁定 SYB 行。
  2. AI 超时允许 3~600 秒,默认 15 秒;Admin Axios 全局超时 10 秒。
  3. BatchPreview 使用 allowAI=false;BatchCreate 当前先 previewOne(允许 AI)再 Create,同一条目可能调用 provider 两次。
  4. create_request_id 有唯一索引,但当前幂等检查在事务内。

目标

  1. 外部 provider HTTP 不在数据库事务内。
  2. 匹配计划携带输入指纹,写库前在短事务内重读并重校验。
  3. 已完成的 requestId 重放不调用 provider。
  4. 单个批量条目最多调用一次 provider。
  5. 前端仅对可能同步调用 provider 的创建接口做局部超时缓解。

明确非目标与并发边界

  • 不做异步化、工作项、队列或数据库迁移;这些属于 #148。
  • 不改变提示词、置信度、错误码、未解析分支或 CreateRequestID 语义。
  • 两个从未出现过的相同 requestId 若跨进程并发,数据库唯一约束只保证一条任务,可能各自调用一次 provider;本单不宣称跨实例 provider 去重。#148 的持久工作项唯一键负责最终消除此窗口。
  • 同一进程内可用 keyed singleflight 合并相同 requestId 的完整创建调用,但不得把它描述为跨实例保证。

固定实施方案

创建流程

  1. 校验请求后,事务外查询 create_request_id;存在则直接返回 replay,禁止调用 matcher。
  2. 事务外读取并形成不可变 creationPlan:任务类型、SYB/Shopee/PDD 关联、目标规格、候选规格、确认映射、本地匹配或 provider 结果,以及规范化输入指纹。
  3. Resolve 保留“本地确定性优先、必要时 provider 一次”的既有逻辑。
  4. 进入短事务,按现有顺序重新执行所有写入前约束:再次检查 requestId;锁定 SYB;重读关联商品;检查活动任务、支付与重新采购授权;检查设备能力、PDD 账号和价格;重校验指纹;创建任务。
  5. 指纹不一致返回既有 CodeMappingRequired,不写旧匹配结果、不自动重试 provider。

指纹契约

  • 指纹覆盖:task type、SYB ID/目标颜色/目标尺码/数量、Shopee ID/PDD ID/Shopee specs、PDD ID/status/specs,以及由这些输入解析出的候选颜色和尺码。
  • JSON 先解析为结构,对对象键稳定排序、候选按原始业务顺序保留,稳定序列化后计算 SHA-256;不得直接哈希原始 JSON 文本。
  • 写事务内逐项重读,并由相同函数重新计算;测试必须覆盖关联、状态和 specs 变化。

批量路径

  • 固定选择:BatchCreate 的 preview 阶段只做确定性校验和价格准备,不允许 provider;最终统一由 Create 执行至多一次匹配。
  • BatchPreview 保持不调用 provider。
  • replay 分支不得再次 preview 或调用 matcher,只返回已存在任务与可安全重建的结果字段。

前端临时缓解

  • 单条 live 创建、批量创建、批量重试使用局部 60 秒 Axios timeout;不改全局 10 秒。
  • BatchPreview、备货创建不加长。
  • 60 秒只覆盖常见默认配置,不保证覆盖服务端 600 秒上限;#148 完成后删除同步等待依赖。

验收

  • provider 等待期间写事务尚未开始,且不持有 SYB/PDD/任务行锁。
  • provider 返回后写事务短小,原有活动任务、重新采购、设备和账号约束仍在事务内执行。
  • 已完成 requestId 重放不调用 matcher/provider。
  • 同进程相同 requestId 并发只创建一条任务且通过 singleflight 最多调用一次 provider;跨进程只承诺一条任务并记录已知限制。
  • 单个批量条目最多调用一次 provider;BatchPreview 为零次。
  • 关联、PDD 状态、Shopee/PDD specs 或候选变化时返回 CodeMappingRequired,不写旧结果。
  • 上下文取消不留下半成品数据。
  • 已确认映射、本地精确匹配、错误码和提示不变。
  • 仅三个可能同步 AI 的前端接口使用 60 秒局部超时。

验证

  • go test ./app/goauto/purchase/... ./app/goauto/aimatching/...
  • 阻塞/计数 matcher;并发相同 requestId;输入变化;上下文取消;批量 provider 调用计数。
  • Admin 手工验证创建、批量创建、批量重试;不宣称覆盖 600 秒配置。

文档与风险

  • 无长期文档影响:接口、数据、状态和配置不变;跳过 Wiki。
  • 回退为还原提交,无数据影响。

状态

待实施。

> **最终实施版(2026-08-29)。** 本版在全栈复核后固定实施边界:不新增迁移;并发相同 requestId 只保证单任务,不虚假承诺跨实例 provider 单次调用;批量创建仅在最终 Create 阶段执行匹配;输入指纹使用规范化结构。历史评论仅作决策记录,冲突时以本正文为准。 ## 来源与范围 - 来源:创建 SYB 商品 `260828G6YQNX6M` 的采购任务时报 `timeout of 10000ms exceeded`。 - 前置于 #148;本单保留不会被异步化推翻的永久修复。 - Server + Admin;不改页面结构、接口字段、数据库和匹配裁决口径,免原型。 ## 已复核事实 1. `purchase.Service.Create` 当前在数据库事务内调用 `matcher.Resolve`,并在此前锁定 SYB 行。 2. AI 超时允许 3~600 秒,默认 15 秒;Admin Axios 全局超时 10 秒。 3. `BatchPreview` 使用 `allowAI=false`;`BatchCreate` 当前先 `previewOne`(允许 AI)再 `Create`,同一条目可能调用 provider 两次。 4. `create_request_id` 有唯一索引,但当前幂等检查在事务内。 ## 目标 1. 外部 provider HTTP 不在数据库事务内。 2. 匹配计划携带输入指纹,写库前在短事务内重读并重校验。 3. 已完成的 requestId 重放不调用 provider。 4. 单个批量条目最多调用一次 provider。 5. 前端仅对可能同步调用 provider 的创建接口做局部超时缓解。 ## 明确非目标与并发边界 - 不做异步化、工作项、队列或数据库迁移;这些属于 #148。 - 不改变提示词、置信度、错误码、未解析分支或 CreateRequestID 语义。 - 两个从未出现过的相同 requestId 若跨进程并发,数据库唯一约束只保证一条任务,可能各自调用一次 provider;本单不宣称跨实例 provider 去重。#148 的持久工作项唯一键负责最终消除此窗口。 - 同一进程内可用 keyed singleflight 合并相同 requestId 的完整创建调用,但不得把它描述为跨实例保证。 ## 固定实施方案 ### 创建流程 1. 校验请求后,事务外查询 `create_request_id`;存在则直接返回 replay,禁止调用 matcher。 2. 事务外读取并形成不可变 `creationPlan`:任务类型、SYB/Shopee/PDD 关联、目标规格、候选规格、确认映射、本地匹配或 provider 结果,以及规范化输入指纹。 3. `Resolve` 保留“本地确定性优先、必要时 provider 一次”的既有逻辑。 4. 进入短事务,按现有顺序重新执行所有写入前约束:再次检查 requestId;锁定 SYB;重读关联商品;检查活动任务、支付与重新采购授权;检查设备能力、PDD 账号和价格;重校验指纹;创建任务。 5. 指纹不一致返回既有 `CodeMappingRequired`,不写旧匹配结果、不自动重试 provider。 ### 指纹契约 - 指纹覆盖:task type、SYB ID/目标颜色/目标尺码/数量、Shopee ID/PDD ID/Shopee specs、PDD ID/status/specs,以及由这些输入解析出的候选颜色和尺码。 - JSON 先解析为结构,对对象键稳定排序、候选按原始业务顺序保留,稳定序列化后计算 SHA-256;不得直接哈希原始 JSON 文本。 - 写事务内逐项重读,并由相同函数重新计算;测试必须覆盖关联、状态和 specs 变化。 ### 批量路径 - 固定选择:`BatchCreate` 的 preview 阶段只做确定性校验和价格准备,不允许 provider;最终统一由 `Create` 执行至多一次匹配。 - `BatchPreview` 保持不调用 provider。 - replay 分支不得再次 preview 或调用 matcher,只返回已存在任务与可安全重建的结果字段。 ### 前端临时缓解 - 单条 live 创建、批量创建、批量重试使用局部 60 秒 Axios timeout;不改全局 10 秒。 - `BatchPreview`、备货创建不加长。 - 60 秒只覆盖常见默认配置,不保证覆盖服务端 600 秒上限;#148 完成后删除同步等待依赖。 ## 验收 - [ ] provider 等待期间写事务尚未开始,且不持有 SYB/PDD/任务行锁。 - [ ] provider 返回后写事务短小,原有活动任务、重新采购、设备和账号约束仍在事务内执行。 - [ ] 已完成 requestId 重放不调用 matcher/provider。 - [ ] 同进程相同 requestId 并发只创建一条任务且通过 singleflight 最多调用一次 provider;跨进程只承诺一条任务并记录已知限制。 - [ ] 单个批量条目最多调用一次 provider;BatchPreview 为零次。 - [ ] 关联、PDD 状态、Shopee/PDD specs 或候选变化时返回 `CodeMappingRequired`,不写旧结果。 - [ ] 上下文取消不留下半成品数据。 - [ ] 已确认映射、本地精确匹配、错误码和提示不变。 - [ ] 仅三个可能同步 AI 的前端接口使用 60 秒局部超时。 ## 验证 - `go test ./app/goauto/purchase/... ./app/goauto/aimatching/...` - 阻塞/计数 matcher;并发相同 requestId;输入变化;上下文取消;批量 provider 调用计数。 - Admin 手工验证创建、批量创建、批量重试;不宣称覆盖 600 秒配置。 ## 文档与风险 - 无长期文档影响:接口、数据、状态和配置不变;跳过 Wiki。 - 回退为还原提交,无数据影响。 ## 状态 待实施。
Author
Owner

全栈审核意见:方向成立,但正文需修订后再实施

结论

“先止血,再由 #148 异步治本”的拆分方向合理;外部 AI HTTP 位于数据库事务内也是必须处理的真实缺陷。但当前方案中的前端超时和批量路径事实不完整,不能按现文直接实施。

一、30 秒建议仍会超时倒挂

当前 AI 超时不是固定最多 15 秒:

  • 默认值为 15 秒;
  • 服务端允许配置范围为 3~600 秒;
  • Admin 设置页也允许最大 600 秒。

因此把相关前端接口设为 30 秒,仍然无法满足正文“前端超时必须大于服务端最长耗时”的硬性要求。

请在正文中删除“建议 30 秒”,并明确选择以下一种可执行策略:

  1. 若 #147 仍保持同步调用,单条接口的客户端期限必须覆盖服务端允许的 600 秒上限及服务端处理余量;或
  2. 明确收紧同步创建路径可使用的 AI 超时上限,并把该规则作为配置/契约变化处理;或
  3. 把前端超时仅作为临时缓解,不再宣称彻底解决,最终由 #148 的立即返回消除请求等待。

批量创建的最坏耗时还与商品数相关,不能用一个固定的 30 秒解决。

二、批量路径需要按真实调用链修正

当前代码事实:

  • BatchPreview 使用 previewFromDataset(..., allowAI=false),批量预检本身不调用外部 AI,因此不应无差别加长超时。
  • BatchCreate 对每个商品先调用 previewOne();该路径允许 AI。随后又调用 Create(),后者可能再次调用 AI。
  • 因此同一批量商品在当前实现中可能调用外部 AI 两次,而不只是正文描述的 N 次。

请补充:

  • 批量创建必须消除同一商品的重复 AI 调用;
  • 事务外得到的匹配计划应复用于最终创建,或批量创建预检阶段只做确定性校验;
  • 验收增加“单个批量条目最多调用一次外部 provider”;
  • 从需要延长超时的接口清单中移除不调用 AI 的批量预检,除非实施时发现其他独立慢路径并给出证据。

三、两阶段匹配必须先处理幂等

如果先在事务外调用 AI、之后才在事务内检查 create_request_id,幂等重放仍会产生无意义的外部请求;并发的相同 requestId 也可能重复调用 provider。

请明确执行顺序:

  1. 事务外先只读检查 create_request_id,已存在时立即返回,禁止调用 AI;
  2. 读取匹配输入并形成不可变输入指纹;
  3. 只在确需外部模型时调用一次;
  4. 进入短事务后再次检查 create_request_id;
  5. 锁定并重读 SYB、虾皮、PDD 关系及候选,对输入指纹逐项重校验;
  6. 指纹变化时返回既有 CodeMappingRequired,不写入过期结果。

验收需增加:

  • 幂等重放不调用 provider;
  • 两个相同 requestId 并发时只创建一条任务;
  • 匹配期间商品关联、PDD 状态或规格 JSON 变化时不落旧结果。

四、事务边界测试应覆盖真实锁行为

除代码检查外,建议使用阻塞的假 matcher 证明:

  • matcher 等待期间数据库事务尚未开始,或至少未持有 SYB/PDD/任务相关行锁;
  • 同一 SYB 的只读操作和不冲突更新不会被 provider 延迟阻塞;
  • provider 返回后,写事务保持短小;
  • 请求上下文取消时不会留下半成品,也不会破坏 requestId 幂等。

正文中“浏览器超时后服务端可能成功”属于合理风险,但实施证据需结合当前反向代理和请求上下文取消行为验证,不能只作为推断。

五、与 #148 的交付边界

#147 应保留的永久价值是:

  • 外部 HTTP 不在数据库事务内;
  • 匹配计划具有输入指纹和事务内重校验;
  • 幂等重放不调用 provider;
  • 批量路径不重复调用 AI。

单纯把前端等待时间调大属于临时缓解;#148 完成后创建接口应立即返回,不再依赖长客户端超时。请据此收紧 #147,避免实现一套很快被 #148 删除的大范围同步等待逻辑。

门禁结论

本单不涉及数据库迁移,且不改变页面结构,可继续免原型;但上述正文事实和验收项修订完成前,不建议进入实施。

## 全栈审核意见:方向成立,但正文需修订后再实施 ### 结论 “先止血,再由 #148 异步治本”的拆分方向合理;外部 AI HTTP 位于数据库事务内也是必须处理的真实缺陷。但当前方案中的前端超时和批量路径事实不完整,不能按现文直接实施。 ### 一、30 秒建议仍会超时倒挂 当前 AI 超时不是固定最多 15 秒: - 默认值为 15 秒; - 服务端允许配置范围为 3~600 秒; - Admin 设置页也允许最大 600 秒。 因此把相关前端接口设为 30 秒,仍然无法满足正文“前端超时必须大于服务端最长耗时”的硬性要求。 请在正文中删除“建议 30 秒”,并明确选择以下一种可执行策略: 1. 若 #147 仍保持同步调用,单条接口的客户端期限必须覆盖服务端允许的 600 秒上限及服务端处理余量;或 2. 明确收紧同步创建路径可使用的 AI 超时上限,并把该规则作为配置/契约变化处理;或 3. 把前端超时仅作为临时缓解,不再宣称彻底解决,最终由 #148 的立即返回消除请求等待。 批量创建的最坏耗时还与商品数相关,不能用一个固定的 30 秒解决。 ### 二、批量路径需要按真实调用链修正 当前代码事实: - `BatchPreview` 使用 `previewFromDataset(..., allowAI=false)`,批量预检本身不调用外部 AI,因此不应无差别加长超时。 - `BatchCreate` 对每个商品先调用 `previewOne()`;该路径允许 AI。随后又调用 `Create()`,后者可能再次调用 AI。 - 因此同一批量商品在当前实现中可能调用外部 AI 两次,而不只是正文描述的 N 次。 请补充: - 批量创建必须消除同一商品的重复 AI 调用; - 事务外得到的匹配计划应复用于最终创建,或批量创建预检阶段只做确定性校验; - 验收增加“单个批量条目最多调用一次外部 provider”; - 从需要延长超时的接口清单中移除不调用 AI 的批量预检,除非实施时发现其他独立慢路径并给出证据。 ### 三、两阶段匹配必须先处理幂等 如果先在事务外调用 AI、之后才在事务内检查 `create_request_id`,幂等重放仍会产生无意义的外部请求;并发的相同 requestId 也可能重复调用 provider。 请明确执行顺序: 1. 事务外先只读检查 `create_request_id`,已存在时立即返回,禁止调用 AI; 2. 读取匹配输入并形成不可变输入指纹; 3. 只在确需外部模型时调用一次; 4. 进入短事务后再次检查 `create_request_id`; 5. 锁定并重读 SYB、虾皮、PDD 关系及候选,对输入指纹逐项重校验; 6. 指纹变化时返回既有 `CodeMappingRequired`,不写入过期结果。 验收需增加: - 幂等重放不调用 provider; - 两个相同 requestId 并发时只创建一条任务; - 匹配期间商品关联、PDD 状态或规格 JSON 变化时不落旧结果。 ### 四、事务边界测试应覆盖真实锁行为 除代码检查外,建议使用阻塞的假 matcher 证明: - matcher 等待期间数据库事务尚未开始,或至少未持有 SYB/PDD/任务相关行锁; - 同一 SYB 的只读操作和不冲突更新不会被 provider 延迟阻塞; - provider 返回后,写事务保持短小; - 请求上下文取消时不会留下半成品,也不会破坏 requestId 幂等。 正文中“浏览器超时后服务端可能成功”属于合理风险,但实施证据需结合当前反向代理和请求上下文取消行为验证,不能只作为推断。 ### 五、与 #148 的交付边界 #147 应保留的永久价值是: - 外部 HTTP 不在数据库事务内; - 匹配计划具有输入指纹和事务内重校验; - 幂等重放不调用 provider; - 批量路径不重复调用 AI。 单纯把前端等待时间调大属于临时缓解;#148 完成后创建接口应立即返回,不再依赖长客户端超时。请据此收紧 #147,避免实现一套很快被 #148 删除的大范围同步等待逻辑。 ### 门禁结论 本单不涉及数据库迁移,且不改变页面结构,可继续免原型;但上述正文事实和验收项修订完成前,不建议进入实施。
ila changed title from 创建采购任务超时:AI 匹配移出事务并修正前后端超时倒挂 to 创建采购任务:AI 调用移出事务、幂等前置与批量去重(超时仅临时缓解) 2026-08-29 10:55:11 +08:00
Author
Owner

实施完成,待验收

提交:c9e9539 fix(#147): move purchase matching outside transaction,已推送 main。

实现

  • 外部 matcher 调用移到写事务前;写事务内重新读取 SYB/Shopee/PDD 并校验规范化 SHA-256 输入指纹。
  • 完成 requestId 事务外重放;同进程并发相同 requestId 通过 keyed flight 合并。跨实例仍只由数据库唯一约束保证单任务,符合工单已记录限制。
  • BatchCreate 预检改为确定性路径;最终 Create 至多调用一次 matcher;replay 不再调用 preview/matcher。
  • 单条创建、批量创建、批量重试使用 60 秒局部 Axios timeout;全局 10 秒、BatchPreview、备货创建不变。

验证

  • go test -count=1 ./app/goauto/purchase/... ./app/goauto/aimatching/...:通过。
  • 新增阻塞 matcher 测试:等待期间同一 SYB 更新成功,证明未持有写事务;同进程并发只调用 matcher 一次。
  • 新增输入变化测试:provider 等待期间 PDD 状态变化后返回 PURCHASE_SPEC_MAPPING_REQUIRED,任务数为 0。
  • npm run build:prod:通过;现有 LightningCSS :export / :deep 和 chunk size 警告未扩大。
  • git diff --check:通过。

文档

无长期文档影响:接口、数据库、状态机和配置项未变,按工单跳过 Wiki。

## 实施完成,待验收 提交:`c9e9539 fix(#147): move purchase matching outside transaction`,已推送 `main`。 ### 实现 - 外部 matcher 调用移到写事务前;写事务内重新读取 SYB/Shopee/PDD 并校验规范化 SHA-256 输入指纹。 - 完成 requestId 事务外重放;同进程并发相同 requestId 通过 keyed flight 合并。跨实例仍只由数据库唯一约束保证单任务,符合工单已记录限制。 - BatchCreate 预检改为确定性路径;最终 Create 至多调用一次 matcher;replay 不再调用 preview/matcher。 - 单条创建、批量创建、批量重试使用 60 秒局部 Axios timeout;全局 10 秒、BatchPreview、备货创建不变。 ### 验证 - `go test -count=1 ./app/goauto/purchase/... ./app/goauto/aimatching/...`:通过。 - 新增阻塞 matcher 测试:等待期间同一 SYB 更新成功,证明未持有写事务;同进程并发只调用 matcher 一次。 - 新增输入变化测试:provider 等待期间 PDD 状态变化后返回 `PURCHASE_SPEC_MAPPING_REQUIRED`,任务数为 0。 - `npm run build:prod`:通过;现有 LightningCSS `:export` / `:deep` 和 chunk size 警告未扩大。 - `git diff --check`:通过。 ### 文档 无长期文档影响:接口、数据库、状态机和配置项未变,按工单跳过 Wiki。
Author
Owner

用户于 2026-08-29 明确确认本工单通过验收。验收结论已记录,现关闭工单。没有新的长期事实变化,本次不重复同步 Wiki。

用户于 2026-08-29 明确确认本工单通过验收。验收结论已记录,现关闭工单。没有新的长期事实变化,本次不重复同步 Wiki。
ila closed this issue 2026-08-29 20:44:11 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#147