PDD 商品替换(一):替换关系与分项匹配状态数据模型 #129

Closed
opened 2026-08-28 15:03:56 +08:00 by ila · 6 comments
Owner

本正文为最终版(2026-08-28 重写)。 此前的历史评论仅作决策过程记录;如与本正文冲突,一律以本正文为准。

所属与来源

  • 关联工单:#130 Agent 发起替代商品采集、#131 替换生效与规格匹配、#132 续做入口改造、#46 服务端规格匹配裁决。
  • 来源:用户于 2026-08-28 提出:Admin 用 PDD 链接创建任务后,Agent 采集或采购时才发现商品失效或已售罄,希望在 PDD 选好替代商品后从 Agent 发起替换。后续确认:采购员本人使用 Agent 并创建任务,取消人工审批环节,替换即时生效;售罄同样纳入可替换范围。
  • 类型:Server / PDD 商品替换关系数据模型与登记接口。
  • 设计证据:不涉及界面与导航,无需原型。
  • 工具回退说明:本工单通过 Gitea API 创建与维护;当前会话未提供 Gitea MCP 工具,按 AGENTS.md「Gitea 交互与工单最小读取」记录回退原因。

当前事实(提交 30d8238 复核)

  • models.PDDProduct.status 三态 pending / active / disabled(models/schema.go:59),可表达失效而无需删除。
  • 采购任务已冻结完整历史快照(models/purchase.go:91-103),含 PDDURLSnapshot、PDDGoodsIDSnapshot(带索引)、ShopeeOrderNoSnapshot、Target* / Mapped* / SpecDecisionSnapshot。外键改指不会丢失下单时的商品身份。
  • 一个虾皮商品最多挂一个 PDD 商品,但多个虾皮商品可共用同一个 PDD 商品(models/schema.go:297-300)。
  • 规格映射权威源为 shopee_product.specs_json(models/schema.go:315);采购任务中的 Mapped* 只是建任务时由 confirmedMappings()(purchase/service.go:282)抄取的副本。
  • models.AgentDevice(models/schema.go:28-46)只有设备身份,无操作人绑定。

目标

  1. 用独立表记录「旧 PDD 商品 → 替代 PDD 商品」的替换关系,创建即生效,无人工审批环节。
  2. 支持来源为失败采集任务或失败采购任务两种入口。
  3. 按受影响的每个虾皮商品分项记录规格匹配状态,供 #130 展示与 #132 判定使用。
  4. 记录替代商品的采集证据与本次实际影响范围,保证可审计、可纠错。

非目标

  • 不实现生效逻辑与规格匹配(#131)。
  • 不实现 Agent 入口(#130)与续做入口(#132)。
  • 不提供审批、拒绝与删除入口;替换记录是审计事实。
  • 不涉及支付、下单与地址。

实施方案

一、主表 pdd_product_replacement

列 说明
id 主键
source_product_id 失效的 PDD 商品,外键 OnDelete:RESTRICT
target_product_id 替代 PDD 商品,外键 OnDelete:RESTRICT
origin_type collection / purchase,check 约束
origin_task_id 来源失败任务 ID,与 origin_type 组合解释
target_collection_task_id 证明替代商品已采集的采集任务 ID
status active / superseded,check 约束
mapping_status 总体进度:matching / completed / completed_partial
created_by_device_id 发起设备(命名明确为设备,不得叫 decided_by)
create_request_id 创建幂等键,唯一索引
created_at / mapping_updated_at 时间戳

要点:

  1. origin_type + origin_task_id 组合表达来源。不得只用 replacesTaskId——采集任务与采购任务是两张表,ID 可能重复。
  2. 不设软删除、不提供删除接口:替换记录是审计事实。
  3. target_collection_task_id 必须指向一条已 completed 或 completed_partial 且商品为 target 的采集任务,作为「哪次采集证明了替代商品」的证据。
  4. 同一 source_product_id 同时只允许一条 status = active 的记录,用可空守卫列 + 复合唯一索引实现(参照 collection_task.ActiveSlot,models/schema.go:128),保证跨 SQLite / MySQL / PostgreSQL 一致。
  5. created_by_device_id 的已知局限必须写入实施结论:AgentDevice 无操作人绑定,只能定位到设备,不能追溯到采购员账号。多人多机场景需先做「设备绑定操作员」,另行建单。

二、分项表 pdd_product_replacement_item

一个 PDD 商品可被多个虾皮商品共用,替换后各自的匹配结果可能不同(S1 已匹配、S2 需人工、S3 失败)。因此必须分项:

列 说明
replacement_id 外键
shopee_product_id 本次实际受影响的虾皮商品
mapping_status matching / matched / manual_required
source ai_match / exact_match / manual_mapping
confidence 可空
reason 可空、脱敏、限长
attempt_count / last_error_code / last_error_at 供 #131 的持久化 worker 使用
updated_at

要点:

  1. 分项记录同时承担本次实际影响集合的职责——纠错时据此只处理原影响范围。
  2. 主表的总体 mapping_status 不得用于判定某条采购任务能否续做;#130 展示与 #132 判定一律读取该任务对应虾皮商品的分项状态。
  3. 分项在 #131 的生效事务内创建,本工单提供模型与写入能力。

三、登记接口

  1. 由 #130 在替代商品采集成功入库后调用;接口语义为「登记并触发生效」,生效逻辑在 #131。
  2. 必须拒绝并返回可读错误码的情况:
    • target_product_id == source_product_id;
    • source_product_id 已存在 status = active 的记录;
    • 任一商品不存在,或 target 不是 active;
    • 来源任务不存在、不属于该设备、状态不是失败,或其商品与 source_product_id 不一致;
    • target_collection_task_id 缺失或不满足第 3 条;
    • 替换链成环(例如已存在 B→A 时再建 A→B)。
  3. 幂等冲突校验:相同 create_request_id 重放时必须核对 source、target、origin_type、origin_task_id 全部一致,返回既有记录;任一不一致返回幂等冲突错误,不得静默覆盖。
  4. 只读查询接口返回主表状态与分项状态,供 #130 / #132 使用。

四、纠错语义

  1. 连续替换不等于撤销:A→B 选错后执行 B→C,会把 B 原本关联的其他虾皮商品一并迁到 C。因此纠错的正确做法是:把原记录置为 superseded,新建 A→C 并只处理原记录 replacement_item 中记录的影响范围。
  2. 本工单提供 superseded 状态与影响集合数据;纠错动作的实施在 #131。

五、权限

  1. 在 access/purchaser.go 单独登记权限点。Agent 不直接获得 Admin 写权限:Agent 只在采集流程中提交替换上下文,登记与生效由服务端内部领域服务完成。

安全边界

  • 不删除任何记录;失效一律用状态表达。
  • 替换记录与分项不含颜色文案、价格、链接原文与个人数据。
  • 不涉及创建订单、支付与地址。
  • 数据库迁移属高风险改动,实施前需再次人工确认;迁移只新增表,不改动既有表结构与数据。

验收标准

  • 两张表创建成功,迁移可重复执行,既有表与数据不受影响。
  • origin_type = collection 与 purchase 两种来源均可正确登记与查询。
  • 同一失效商品已有 active 记录时,再次登记被拒绝。
  • target 等于 source、target 非 active、替换链成环,三种情况均被拒绝。
  • target_collection_task_id 缺失或不满足要求时被拒绝。
  • 相同 create_request_id 携带不同内容时返回幂等冲突,不静默覆盖;内容一致时返回既有记录。
  • 分项表可为同一替换记录写入多个虾皮商品并各自维护状态。
  • 唯一性约束在 SQLite 与 MySQL 上行为一致(含并发创建测试)。
  • 无删除接口;软删除未被引入。
  • 实施结论如实记录 created_by_device_id 只能定位到设备的局限。

验证方式

  • go test ./app/goauto/...
  • 迁移在空库与既有库两种前提下各执行一次。
  • 并发创建同一 source 的替换记录,断言只有一条成功。
  • 不涉及 Agent 与前端,不需要真机验证;如实记录该结论。

依赖、并行与风险

  • 无前置依赖,是 #130 / #131 / #132 的共同前置。
  • 风险:唯一性若只在应用层实现,并发下会漏。缓解:数据库唯一索引 + 并发测试。
  • 回退:还原提交并保留空表即可,无数据影响。

文档影响

  • Wiki Architecture-and-Code-Map:新增替换关系与分项表及其边界。
  • Wiki Business-Rules-and-Glossary:商品替换的业务定义、即时生效口径、纠错语义。
  • 按 Wiki-first 门禁:先改线上页面并回读 revision,再执行一轮 sync 与一轮 sync --check,把页面与 revision 写回本工单。

状态

待实施(数据库迁移需实施前再次人工确认)。

> **本正文为最终版(2026-08-28 重写)。** 此前的历史评论仅作决策过程记录;如与本正文冲突,一律以本正文为准。 ## 所属与来源 - 关联工单:#130 Agent 发起替代商品采集、#131 替换生效与规格匹配、#132 续做入口改造、#46 服务端规格匹配裁决。 - 来源:用户于 2026-08-28 提出:Admin 用 PDD 链接创建任务后,Agent 采集或采购时才发现商品失效或已售罄,希望在 PDD 选好替代商品后从 Agent 发起替换。后续确认:**采购员本人使用 Agent 并创建任务,取消人工审批环节,替换即时生效**;**售罄同样纳入可替换范围**。 - 类型:Server / PDD 商品替换关系数据模型与登记接口。 - 设计证据:不涉及界面与导航,无需原型。 - 工具回退说明:本工单通过 Gitea API 创建与维护;当前会话未提供 Gitea MCP 工具,按 `AGENTS.md`「Gitea 交互与工单最小读取」记录回退原因。 ## 当前事实(提交 30d8238 复核) - `models.PDDProduct.status` 三态 `pending` / `active` / `disabled`(`models/schema.go:59`),可表达失效而无需删除。 - 采购任务已冻结完整历史快照(`models/purchase.go:91-103`),含 `PDDURLSnapshot`、`PDDGoodsIDSnapshot`(带索引)、`ShopeeOrderNoSnapshot`、`Target*` / `Mapped*` / `SpecDecisionSnapshot`。外键改指不会丢失下单时的商品身份。 - 一个虾皮商品最多挂一个 PDD 商品,但**多个虾皮商品可共用同一个 PDD 商品**(`models/schema.go:297-300`)。 - 规格映射权威源为 `shopee_product.specs_json`(`models/schema.go:315`);采购任务中的 `Mapped*` 只是建任务时由 `confirmedMappings()`(`purchase/service.go:282`)抄取的副本。 - `models.AgentDevice`(`models/schema.go:28-46`)只有设备身份,无操作人绑定。 ## 目标 1. 用独立表记录「旧 PDD 商品 → 替代 PDD 商品」的替换关系,创建即生效,无人工审批环节。 2. 支持来源为**失败采集任务**或**失败采购任务**两种入口。 3. 按受影响的**每个虾皮商品**分项记录规格匹配状态,供 #130 展示与 #132 判定使用。 4. 记录替代商品的采集证据与本次实际影响范围,保证可审计、可纠错。 ## 非目标 - 不实现生效逻辑与规格匹配(#131)。 - 不实现 Agent 入口(#130)与续做入口(#132)。 - 不提供审批、拒绝与删除入口;替换记录是审计事实。 - 不涉及支付、下单与地址。 ## 实施方案 ### 一、主表 `pdd_product_replacement` | 列 | 说明 | |---|---| | `id` | 主键 | | `source_product_id` | 失效的 PDD 商品,外键 `OnDelete:RESTRICT` | | `target_product_id` | 替代 PDD 商品,外键 `OnDelete:RESTRICT` | | `origin_type` | `collection` / `purchase`,check 约束 | | `origin_task_id` | 来源失败任务 ID,与 `origin_type` 组合解释 | | `target_collection_task_id` | 证明替代商品已采集的采集任务 ID | | `status` | `active` / `superseded`,check 约束 | | `mapping_status` | 总体进度:`matching` / `completed` / `completed_partial` | | `created_by_device_id` | 发起设备(**命名明确为设备**,不得叫 `decided_by`) | | `create_request_id` | 创建幂等键,唯一索引 | | `created_at` / `mapping_updated_at` | 时间戳 | 要点: 1. `origin_type + origin_task_id` 组合表达来源。**不得只用 `replacesTaskId`**——采集任务与采购任务是两张表,ID 可能重复。 2. **不设软删除、不提供删除接口**:替换记录是审计事实。 3. `target_collection_task_id` 必须指向一条已 `completed` 或 `completed_partial` 且商品为 `target` 的采集任务,作为「哪次采集证明了替代商品」的证据。 4. 同一 `source_product_id` 同时只允许一条 `status = active` 的记录,用可空守卫列 + 复合唯一索引实现(参照 `collection_task.ActiveSlot`,`models/schema.go:128`),保证跨 SQLite / MySQL / PostgreSQL 一致。 5. `created_by_device_id` 的已知局限必须写入实施结论:`AgentDevice` 无操作人绑定,只能定位到设备,不能追溯到采购员账号。多人多机场景需先做「设备绑定操作员」,另行建单。 ### 二、分项表 `pdd_product_replacement_item` 一个 PDD 商品可被多个虾皮商品共用,替换后各自的匹配结果可能不同(S1 已匹配、S2 需人工、S3 失败)。因此必须分项: | 列 | 说明 | |---|---| | `replacement_id` | 外键 | | `shopee_product_id` | 本次实际受影响的虾皮商品 | | `mapping_status` | `matching` / `matched` / `manual_required` | | `source` | `ai_match` / `exact_match` / `manual_mapping` | | `confidence` | 可空 | | `reason` | 可空、脱敏、限长 | | `attempt_count` / `last_error_code` / `last_error_at` | 供 #131 的持久化 worker 使用 | | `updated_at` | | 要点: 6. 分项记录同时承担**本次实际影响集合**的职责——纠错时据此只处理原影响范围。 7. **主表的总体 `mapping_status` 不得用于判定某条采购任务能否续做**;#130 展示与 #132 判定一律读取该任务对应虾皮商品的分项状态。 8. 分项在 #131 的生效事务内创建,本工单提供模型与写入能力。 ### 三、登记接口 9. 由 #130 在替代商品采集成功入库后调用;接口语义为「登记并触发生效」,生效逻辑在 #131。 10. 必须拒绝并返回可读错误码的情况: - `target_product_id == source_product_id`; - `source_product_id` 已存在 `status = active` 的记录; - 任一商品不存在,或 `target` 不是 `active`; - 来源任务不存在、不属于该设备、状态不是失败,或其商品与 `source_product_id` 不一致; - `target_collection_task_id` 缺失或不满足第 3 条; - 替换链成环(例如已存在 B→A 时再建 A→B)。 11. **幂等冲突校验**:相同 `create_request_id` 重放时必须核对 source、target、`origin_type`、`origin_task_id` 全部一致,返回既有记录;任一不一致返回幂等冲突错误,不得静默覆盖。 12. 只读查询接口返回主表状态与分项状态,供 #130 / #132 使用。 ### 四、纠错语义 13. **连续替换不等于撤销**:A→B 选错后执行 B→C,会把 B 原本关联的其他虾皮商品一并迁到 C。因此纠错的正确做法是:把原记录置为 `superseded`,新建 A→C 并**只处理原记录 `replacement_item` 中记录的影响范围**。 14. 本工单提供 `superseded` 状态与影响集合数据;纠错动作的实施在 #131。 ### 五、权限 15. 在 `access/purchaser.go` 单独登记权限点。**Agent 不直接获得 Admin 写权限**:Agent 只在采集流程中提交替换上下文,登记与生效由服务端内部领域服务完成。 ## 安全边界 - 不删除任何记录;失效一律用状态表达。 - 替换记录与分项不含颜色文案、价格、链接原文与个人数据。 - 不涉及创建订单、支付与地址。 - 数据库迁移属高风险改动,实施前需再次人工确认;迁移只新增表,不改动既有表结构与数据。 ## 验收标准 - [ ] 两张表创建成功,迁移可重复执行,既有表与数据不受影响。 - [ ] `origin_type = collection` 与 `purchase` 两种来源均可正确登记与查询。 - [ ] 同一失效商品已有 `active` 记录时,再次登记被拒绝。 - [ ] target 等于 source、target 非 `active`、替换链成环,三种情况均被拒绝。 - [ ] `target_collection_task_id` 缺失或不满足要求时被拒绝。 - [ ] 相同 `create_request_id` 携带不同内容时返回幂等冲突,不静默覆盖;内容一致时返回既有记录。 - [ ] 分项表可为同一替换记录写入多个虾皮商品并各自维护状态。 - [ ] 唯一性约束在 SQLite 与 MySQL 上行为一致(含并发创建测试)。 - [ ] 无删除接口;软删除未被引入。 - [ ] 实施结论如实记录 `created_by_device_id` 只能定位到设备的局限。 ## 验证方式 - `go test ./app/goauto/...` - 迁移在空库与既有库两种前提下各执行一次。 - 并发创建同一 source 的替换记录,断言只有一条成功。 - 不涉及 Agent 与前端,不需要真机验证;如实记录该结论。 ## 依赖、并行与风险 - 无前置依赖,是 #130 / #131 / #132 的共同前置。 - 风险:唯一性若只在应用层实现,并发下会漏。缓解:数据库唯一索引 + 并发测试。 - 回退:还原提交并保留空表即可,无数据影响。 ## 文档影响 - Wiki `Architecture-and-Code-Map`:新增替换关系与分项表及其边界。 - Wiki `Business-Rules-and-Glossary`:商品替换的业务定义、即时生效口径、纠错语义。 - 按 Wiki-first 门禁:先改线上页面并回读 revision,再执行一轮 `sync` 与一轮 `sync --check`,把页面与 revision 写回本工单。 ## 状态 待实施(数据库迁移需实施前再次人工确认)。
Author
Owner

范围修订:取消人工确认环节,替换即时生效

用户于 2026-08-28 与采购员沟通后确认:Agent 由采购员本人使用,采集与采购任务也由其创建,因此原设计中「先记录待确认、再由采购员在 Admin 批准」的双人复核在同一人身上属纯摩擦,予以取消。用户进一步明确:「尽量简单易用地用 PDD 代替商品替换失效的 PDD 商品,不要增加繁琐和误人心智的操作流程和文字。」

本评论修订 #129 的状态机与约束,覆盖正文中与 pending 审核流程相关的部分。

一、状态机简化

  • 替换记录创建即生效,status 直接落 approved,不再经过 pending。
  • 保留 status 列与 rejected 值以便将来扩展,但本工单不产生这两种状态,也不提供任何审核接口。
  • decided_at 等同于 requested_at;decided_by 见第三节。

二、唯一性约束调整

正文第 2 项「同一 source_product_id 同时只允许一条 pending」调整为:

  • 同一 source_product_id 只允许一条生效中的替换记录(即一个失效商品只能被替换一次)。
  • 仍使用可空守卫列 + 复合唯一索引实现,跨 SQLite / MySQL / PostgreSQL 行为一致。
  • 说明:替换是可重复纠错的——若替代品选错,采购任务此时已指向 B 商品,采购员用同一入口把 B 再换成 C 即可,形成 A→B、B→C 两条独立记录,不与本约束冲突。

三、审计与已知局限

  • 记录表保留,其定位由「待办队列」改为审计台账:用于回答「某商品何时被替换为哪个、从哪个失败采集任务发起」。
  • 已知局限(必须写入实施结论):models.AgentDevice(models/schema.go:28-46)只有设备身份(InstallID、型号、token),没有绑定到具体操作人。因此 decided_by 实际只能记录到发起设备,不能追溯到采购员账号。当前单人单机场景下等价,多人多机时需要先做「设备绑定操作员」,属另行建单范围,本工单不实现。

四、接口调整

  • 保留登记接口(由 #130 在替代商品采集成功后调用),但其语义由「登记待确认」变为「登记并触发生效」,生效逻辑本身在 #131。
  • 删除正文第 7 项中面向审核界面的按状态查询接口需求;仅保留供审计查询的只读接口。
  • 第 5 项的拒绝条件全部保留(替代品等于原商品、已存在生效替换、商品不存在、来源任务与源商品不一致),并继续返回可读错误码。
  • 幂等要求不变。

五、验收标准调整

  • 原「同一失效商品重复发起时第二条被拒绝」改为:同一失效商品已存在生效替换时,再次发起被拒绝。
  • 原「登记记录后各类数据均无变化」一条作废——生效逻辑在 #131,本工单单独交付时仍不产生副作用,但整体链路上线后会立即生效。本工单的验收改为:登记接口按预期写入 approved 记录并正确拒绝非法输入。
  • 新增:实施结论中如实记录 decided_by 只能定位到设备这一局限。

正文的数据模型(除状态与唯一性约束外)、权限、安全边界、文档影响不变。

## 范围修订:取消人工确认环节,替换即时生效 用户于 2026-08-28 与采购员沟通后确认:**Agent 由采购员本人使用,采集与采购任务也由其创建**,因此原设计中「先记录待确认、再由采购员在 Admin 批准」的双人复核在同一人身上属纯摩擦,予以取消。用户进一步明确:「尽量简单易用地用 PDD 代替商品替换失效的 PDD 商品,不要增加繁琐和误人心智的操作流程和文字。」 本评论修订 #129 的状态机与约束,覆盖正文中与 `pending` 审核流程相关的部分。 ### 一、状态机简化 - 替换记录**创建即生效**,`status` 直接落 `approved`,不再经过 `pending`。 - 保留 `status` 列与 `rejected` 值以便将来扩展,但本工单不产生这两种状态,也不提供任何审核接口。 - `decided_at` 等同于 `requested_at`;`decided_by` 见第三节。 ### 二、唯一性约束调整 正文第 2 项「同一 `source_product_id` 同时只允许一条 `pending`」调整为: - **同一 `source_product_id` 只允许一条生效中的替换记录**(即一个失效商品只能被替换一次)。 - 仍使用可空守卫列 + 复合唯一索引实现,跨 SQLite / MySQL / PostgreSQL 行为一致。 - 说明:替换是可重复纠错的——若替代品选错,采购任务此时已指向 B 商品,采购员用同一入口把 B 再换成 C 即可,形成 A→B、B→C 两条独立记录,不与本约束冲突。 ### 三、审计与已知局限 - 记录表保留,其定位由「待办队列」改为**审计台账**:用于回答「某商品何时被替换为哪个、从哪个失败采集任务发起」。 - **已知局限(必须写入实施结论)**:`models.AgentDevice`(`models/schema.go:28-46`)只有设备身份(InstallID、型号、token),**没有绑定到具体操作人**。因此 `decided_by` 实际只能记录到发起设备,不能追溯到采购员账号。当前单人单机场景下等价,多人多机时需要先做「设备绑定操作员」,属另行建单范围,本工单不实现。 ### 四、接口调整 - 保留登记接口(由 #130 在替代商品采集成功后调用),但其语义由「登记待确认」变为「登记并触发生效」,生效逻辑本身在 #131。 - **删除**正文第 7 项中面向审核界面的按状态查询接口需求;仅保留供审计查询的只读接口。 - 第 5 项的拒绝条件全部保留(替代品等于原商品、已存在生效替换、商品不存在、来源任务与源商品不一致),并继续返回可读错误码。 - 幂等要求不变。 ### 五、验收标准调整 - 原「同一失效商品重复发起时第二条被拒绝」改为:同一失效商品已存在生效替换时,再次发起被拒绝。 - 原「登记记录后各类数据均无变化」一条**作废**——生效逻辑在 #131,本工单单独交付时仍不产生副作用,但整体链路上线后会立即生效。本工单的验收改为:登记接口按预期写入 `approved` 记录并正确拒绝非法输入。 - 新增:实施结论中如实记录 `decided_by` 只能定位到设备这一局限。 正文的数据模型(除状态与唯一性约束外)、权限、安全边界、文档影响不变。
Author
Owner

修订:增加匹配状态字段,串起替换后的规格匹配流程

用户于 2026-08-28 确认整体流程为「替换 → 自动 AI 匹配规格 → Agent 刷新看状态 → 继续采购」,并选定 AI 结果按 B 方案处理(高置信自动确认,低置信留人工)。本评论在 #129 的数据模型上补充承载该流程所需的状态字段。

一、替换记录新增匹配状态

pdd_product_replacement 增加:

  • mapping_status:matching / matched / manual_required,check 约束
    • matching:替换已生效,AI 匹配进行中或待发起
    • matched:规格映射已可用(AI 高置信自动确认,或人工已确认)
    • manual_required:AI 关闭、AI 无结果、置信度不足或缺失,需人工在 Admin 匹配
  • mapping_updated_at:状态最后变更时间

该字段是 Agent 刷新时的唯一判据:matched 显示可继续采购,其余显示需人工匹配。没有它,Agent 只能靠猜。

二、状态流转

  • #131 生效完成时写入 matching;
  • 匹配流程(见 #131 修订)完成后写入 matched 或 manual_required;
  • 人工在 Admin 完成映射确认后,允许由 manual_required 转为 matched;
  • 状态变更需幂等,重复写入同一状态不报错。

三、只读查询接口补充

审计只读接口需返回 mapping_status,供 Agent 的采购/采集任务详情展示与按钮显隐判断使用。

四、验收标准补充

  • mapping_status 三态可正确写入与查询,非法值被 check 约束拒绝。
  • 生效后初始状态为 matching。
  • 状态变更幂等。

正文其余部分(表结构主体、唯一性约束、审计定位、decided_by 只能定位到设备的局限)不变。

## 修订:增加匹配状态字段,串起替换后的规格匹配流程 用户于 2026-08-28 确认整体流程为「替换 → 自动 AI 匹配规格 → Agent 刷新看状态 → 继续采购」,并选定 AI 结果按 **B 方案**处理(高置信自动确认,低置信留人工)。本评论在 #129 的数据模型上补充承载该流程所需的状态字段。 ### 一、替换记录新增匹配状态 `pdd_product_replacement` 增加: - `mapping_status`:`matching` / `matched` / `manual_required`,check 约束 - `matching`:替换已生效,AI 匹配进行中或待发起 - `matched`:规格映射已可用(AI 高置信自动确认,或人工已确认) - `manual_required`:AI 关闭、AI 无结果、置信度不足或缺失,需人工在 Admin 匹配 - `mapping_updated_at`:状态最后变更时间 该字段是 Agent 刷新时的唯一判据:`matched` 显示可继续采购,其余显示需人工匹配。没有它,Agent 只能靠猜。 ### 二、状态流转 - #131 生效完成时写入 `matching`; - 匹配流程(见 #131 修订)完成后写入 `matched` 或 `manual_required`; - 人工在 Admin 完成映射确认后,允许由 `manual_required` 转为 `matched`; - 状态变更需幂等,重复写入同一状态不报错。 ### 三、只读查询接口补充 审计只读接口需返回 `mapping_status`,供 Agent 的采购/采集任务详情展示与按钮显隐判断使用。 ### 四、验收标准补充 - [ ] `mapping_status` 三态可正确写入与查询,非法值被 check 约束拒绝。 - [ ] 生效后初始状态为 `matching`。 - [ ] 状态变更幂等。 正文其余部分(表结构主体、唯一性约束、审计定位、`decided_by` 只能定位到设备的局限)不变。
Author
Owner

Codex 全栈复核:数据模型与跨工单契约待补全

基于当前代码 30d8238 复核,#129 → #131 → #130 → #132 的总体顺序可以保留,但本工单模型还不能完整承载最新版流程。以下内容建议由 Claude Code 审核并合并进最终正文后再实施。

阻塞项

  1. 来源模型需同时支持采集任务与采购任务

    • #130 已把入口扩展到失败采集、失败采购两个 Tab,但本工单仍只有 origin_collection_task_id。
    • 建议改为互斥的 origin_type + origin_task_id,或 origin_collection_task_id / origin_purchase_task_id 两个可空字段并加 check 约束。
    • Agent 请求不能只传有歧义的 replacesTaskId,因为两个任务表可能出现相同 ID。
  2. 单条 mapping_status 无法表达多个虾皮商品的不同结果

    • 一个 PDD 商品可以被多个 shopee_product 共用;替换后可能出现 S1 已匹配、S2 需人工、S3 匹配失败。
    • 建议增加替换明细表,例如 pdd_product_replacement_item(replacement_id, shopee_product_id, mapping_status, source, confidence, reason, updated_at)。
    • 主表状态只表达总体进度;Agent 查看采购任务时必须读取该任务对应虾皮商品的明细状态,不能使用全局状态决定“继续采购”。
  3. 需要记录替代商品的采集证据

    • 建议增加 target_collection_task_id,并要求其已完成或部分完成、商品为 target,保证审计能够回答“哪次成功采集证明了替代商品”。
  4. 连续替换不是可靠的撤销

    • A→B 选错后执行 B→C,会把 B 原本关联的其他虾皮商品一起迁到 C,不等价于撤销 A→B。
    • 建议记录本次实际影响的虾皮商品集合;纠错时把原记录置为 superseded 并执行 A→C,只处理原记录影响范围。
    • 至少需要拒绝 target 已停用、替换链成环,以及相同 source 已存在有效记录时的模糊覆盖。

契约补充

  • request_id 重放时必须核对 source、target、来源类型和来源任务均一致;相同 UUID 携带不同内容应返回幂等冲突。
  • 如果保留决定字段,应区分创建幂等键与生效幂等键,不能混用。
  • 替换记录是审计事实,不建议设置软删除或提供删除入口。
  • decided_by 只能定位设备的限制可以保留,但建议字段明确命名为设备身份,避免被误解为采购员账号。
  • Agent 不应直接获得 Admin 写权限;Agent 只在采集任务中提交替换上下文,成功结果事务由服务端内部领域服务登记并触发生效。
  • 数据库迁移、索引、循环校验、目标商品有效性和并发创建需要补 SQLite/MySQL 测试。

工单一致性

当前正文仍描述 pending 审核模型,评论改为即时生效;建议实施前把最终状态机、字段、接口和验收标准合并回正文,避免实现者漏读覆盖评论。

## Codex 全栈复核:数据模型与跨工单契约待补全 基于当前代码 `30d8238` 复核,`#129 → #131 → #130 → #132` 的总体顺序可以保留,但本工单模型还不能完整承载最新版流程。以下内容建议由 Claude Code 审核并合并进最终正文后再实施。 ### 阻塞项 1. **来源模型需同时支持采集任务与采购任务** - #130 已把入口扩展到失败采集、失败采购两个 Tab,但本工单仍只有 `origin_collection_task_id`。 - 建议改为互斥的 `origin_type + origin_task_id`,或 `origin_collection_task_id / origin_purchase_task_id` 两个可空字段并加 check 约束。 - Agent 请求不能只传有歧义的 `replacesTaskId`,因为两个任务表可能出现相同 ID。 2. **单条 `mapping_status` 无法表达多个虾皮商品的不同结果** - 一个 PDD 商品可以被多个 `shopee_product` 共用;替换后可能出现 S1 已匹配、S2 需人工、S3 匹配失败。 - 建议增加替换明细表,例如 `pdd_product_replacement_item(replacement_id, shopee_product_id, mapping_status, source, confidence, reason, updated_at)`。 - 主表状态只表达总体进度;Agent 查看采购任务时必须读取该任务对应虾皮商品的明细状态,不能使用全局状态决定“继续采购”。 3. **需要记录替代商品的采集证据** - 建议增加 `target_collection_task_id`,并要求其已完成或部分完成、商品为 target,保证审计能够回答“哪次成功采集证明了替代商品”。 4. **连续替换不是可靠的撤销** - A→B 选错后执行 B→C,会把 B 原本关联的其他虾皮商品一起迁到 C,不等价于撤销 A→B。 - 建议记录本次实际影响的虾皮商品集合;纠错时把原记录置为 `superseded` 并执行 A→C,只处理原记录影响范围。 - 至少需要拒绝 target 已停用、替换链成环,以及相同 source 已存在有效记录时的模糊覆盖。 ### 契约补充 - `request_id` 重放时必须核对 source、target、来源类型和来源任务均一致;相同 UUID 携带不同内容应返回幂等冲突。 - 如果保留决定字段,应区分创建幂等键与生效幂等键,不能混用。 - 替换记录是审计事实,不建议设置软删除或提供删除入口。 - `decided_by` 只能定位设备的限制可以保留,但建议字段明确命名为设备身份,避免被误解为采购员账号。 - Agent 不应直接获得 Admin 写权限;Agent 只在采集任务中提交替换上下文,成功结果事务由服务端内部领域服务登记并触发生效。 - 数据库迁移、索引、循环校验、目标商品有效性和并发创建需要补 SQLite/MySQL 测试。 ### 工单一致性 当前正文仍描述 pending 审核模型,评论改为即时生效;建议实施前把最终状态机、字段、接口和验收标准合并回正文,避免实现者漏读覆盖评论。
ila changed title from PDD 商品替换(一):服务端替换关系记录与发起接口 to PDD 商品替换(一):替换关系与分项匹配状态数据模型 2026-08-28 16:59:02 +08:00
Author
Owner

高风险实施授权记录

用户于 2026-08-28 明确授权:#129 涉及的数据库迁移可按已确认工单方案进入实施。

授权边界:仅覆盖本工单所需的数据模型/迁移实施与验证;不自动授权生产发布、删除数据、不可逆生产迁移或支付。开始实施时仍须按工单依赖、备份/回退方案和迁移验证门禁执行。当前回合仅制作 #130 + #132 原型,尚未实施 #129。

## 高风险实施授权记录 用户于 2026-08-28 明确授权:#129 涉及的数据库迁移可按已确认工单方案进入实施。 授权边界:仅覆盖本工单所需的数据模型/迁移实施与验证;不自动授权生产发布、删除数据、不可逆生产迁移或支付。开始实施时仍须按工单依赖、备份/回退方案和迁移验证门禁执行。当前回合仅制作 #130 + #132 原型,尚未实施 #129。
Author
Owner

实施完成,待验收

已按最终正文完成 #129,并推送到 main。

实现

  • 新增 pdd_product_replacement 与 pdd_product_replacement_item,迁移版本 1787885000000_pdd_product_replacement.go;只新增表,不改既有表和数据。
  • 主表支持 collection/purchase 两种来源、采集证据、设备审计、active/superseded、总体匹配进度和跨数据库可空唯一守卫。
  • 分项表冻结实际影响的虾皮商品集合并独立维护 matching/matched/manual_required、来源、置信度与 worker 错误字段。
  • 内部登记服务完成目标有效性、来源归属/失败状态、采集证据、同商品、环、active 唯一性和严格幂等冲突校验。
  • 新增管理端只读审计列表/详情;采购员只有只读权限,Agent 没有直接写接口。
  • 代码知识图谱 MCP 在当前会话未提供,按仓库规则回退到 rg 和定点源码读取;Gitea 与 Wiki 全程使用 Gitea MCP。

验证

  • go test ./app/goauto/replacement ./app/goauto/migrations ./app/goauto/access ./app/admin/router ./cmd/migrate/migration/version-local:通过。
  • go test ./app/goauto/...:通过。
  • .\\scripts\\verify.ps1 -Component server:通过(go test ./... + go build)。
  • 空库、既有库、重复迁移、SQLite 并发创建、两种来源、幂等冲突、同商品、非 active 目标、无效采集证据、环和分项独立状态均有自动化覆盖。
  • 未验证:没有可安全使用的 MySQL 集成实例,因此 MySQL 真并发未执行;实现使用与现有任务槽一致的可空复合唯一索引,并由迁移/模型测试覆盖结构,但不把结构检查冒充 MySQL 真机结果。
  • 不需要 Android/真机验证。

文档

  • Architecture-and-Code-Map revision:d585a906857a46752788c9f25d3ac0361739dada
  • Business-Rules-and-Glossary revision:b31ebf8888da7c3d9b71520938b104ff9e66511f
  • 已执行一轮 harness.py sync 与一轮 sync --check,一致性通过。

提交

  • 696e0bc feat(#129): add PDD product replacement model
  • 已推送:origin/main

已知局限

created_by_device_id 只能定位 Agent 设备,不能追溯采购员账号;多人多机场景的设备绑定操作员仍需独立工单。本工单保持打开,状态为待验收。

## 实施完成,待验收 已按最终正文完成 #129,并推送到 `main`。 ### 实现 - 新增 `pdd_product_replacement` 与 `pdd_product_replacement_item`,迁移版本 `1787885000000_pdd_product_replacement.go`;只新增表,不改既有表和数据。 - 主表支持 collection/purchase 两种来源、采集证据、设备审计、active/superseded、总体匹配进度和跨数据库可空唯一守卫。 - 分项表冻结实际影响的虾皮商品集合并独立维护 matching/matched/manual_required、来源、置信度与 worker 错误字段。 - 内部登记服务完成目标有效性、来源归属/失败状态、采集证据、同商品、环、active 唯一性和严格幂等冲突校验。 - 新增管理端只读审计列表/详情;采购员只有只读权限,Agent 没有直接写接口。 - 代码知识图谱 MCP 在当前会话未提供,按仓库规则回退到 `rg` 和定点源码读取;Gitea 与 Wiki 全程使用 Gitea MCP。 ### 验证 - `go test ./app/goauto/replacement ./app/goauto/migrations ./app/goauto/access ./app/admin/router ./cmd/migrate/migration/version-local`:通过。 - `go test ./app/goauto/...`:通过。 - `.\\scripts\\verify.ps1 -Component server`:通过(`go test ./...` + `go build`)。 - 空库、既有库、重复迁移、SQLite 并发创建、两种来源、幂等冲突、同商品、非 active 目标、无效采集证据、环和分项独立状态均有自动化覆盖。 - 未验证:没有可安全使用的 MySQL 集成实例,因此 MySQL 真并发未执行;实现使用与现有任务槽一致的可空复合唯一索引,并由迁移/模型测试覆盖结构,但不把结构检查冒充 MySQL 真机结果。 - 不需要 Android/真机验证。 ### 文档 - Architecture-and-Code-Map revision:`d585a906857a46752788c9f25d3ac0361739dada` - Business-Rules-and-Glossary revision:`b31ebf8888da7c3d9b71520938b104ff9e66511f` - 已执行一轮 `harness.py sync` 与一轮 `sync --check`,一致性通过。 ### 提交 - `696e0bc feat(#129): add PDD product replacement model` - 已推送:`origin/main` ### 已知局限 `created_by_device_id` 只能定位 Agent 设备,不能追溯采购员账号;多人多机场景的设备绑定操作员仍需独立工单。本工单保持打开,状态为待验收。
Author
Owner

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

用户于 2026-08-29 明确确认本工单通过验收。验收结论已记录,现关闭工单。没有新的长期事实变化,本次不重复同步 Wiki。
ila closed this issue 2026-08-29 20:43:37 +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#129