# 实现前待决与待落地事项 > 本文记录 API 契约和数据库 DDL 评审后尚未闭环的事项。它不是新的产品需求;除明确标为“已决策”的条目外,实施对应模块前必须先确认或补齐。最后复核:2026-09-02。 ## 使用规则 - **已决策,待落地**:产品或技术方向已确认,但 DDL、接口契约或实现规则尚未同步,编码前补齐即可。 - **待确认**:会改变用户可见行为或数据语义,不能由开发自行猜测。 - **实现约束**:已经确认,后续代码、测试和文档必须遵守。 ## P0:开始实现前必须闭环 ### 1. 幂等请求与 Transactional Outbox **状态:已决策,DDL 与基础架构文档已落地;待 NestJS 实现和集成测试。** 所有可重试的关键写操作需要幂等保护;所有会触发异步工作的业务写入需要 Transactional Outbox。基础迁移已新增以下两张工作空间隔离表,并已纳入 RLS、清理策略和 Worker 设计: - `idempotency_requests`:按用户、工作空间、规范化请求路径和 `Idempotency-Key` 保存请求摘要、处理状态、HTTP 响应摘要、关联资源及过期时间。至少保留 24 小时;建议 48 小时。相同键而请求摘要不同返回 `409 IDEMPOTENCY_KEY_REUSED`。 - `outbox_events`:在**同一数据库事务**中随业务数据和 `operations` 记录写入待投递事件;独立 Dispatcher 读取、投递 BullMQ 并重试。事件投递必须按“至少一次”设计,消费者必须可重入;不得将 API Key、Cookie、预签名 URL 等秘密写入事件载荷。 首批必须接入的场景:店铺创建/凭据验证、上传完成后的媒体处理、采集素材转存、首次上架发布、在线商品同步、AI 生成任务。普通草稿保存可在首期只使用乐观锁。完整设计和验收项见 [`../architecture/transactional-outbox-and-idempotency.md`](../architecture/transactional-outbox-and-idempotency.md)。 ### 2. `offer_id`(Ozon 货号)策略 **状态:已决策,待接口和数据校验落地。** - 采集进入采集箱时,型号(`model`)和 `offer_id` 都允许为空。 - 用户在商品编辑页填写:`model` 是商品通用字段;`offer_id` 是当前平台下、每个 SKU 的字段。 - 系统**不**从采集标题、规格或来源 SKU 自动生成 `offer_id`。 - 首次上架前,`model` 必填;每个被选中 SKU 的 `offer_id` 必填,且在目标门店内唯一。 - 导入成功后,该次平台 SKU 发布记录使用的 `offer_id` 作为历史事实,不允许通过“重新上架”改变;后续编辑在线商品属于另一套平台商品更新能力。 `docs/api/product-editor-and-publishing.md`、商品域 DDL 和发布前校验必须以本节为准,不再保留“自动生成可编辑货号”的备选逻辑。 ## P1:实现对应模块前修正 ### 1. SKU 与平台发布配置的同商品归属 **状态:M3.0 已落地数据库约束(2026-09-09);发布任务、在线镜像等后续表必须复用该归属链。** `product_variant_platform_listings` 同时引用平台发布配置(profile)和通用 SKU(variant),但必须强制:`variant.product_id = profile.product_id`。同一条归属链也适用于属性、报价、发布任务项和在线镜像映射。 实施时优先选用可维护的复合外键/冗余 `product_id` 加一致性约束;如 PostgreSQL 外键表达能力不足,再使用约束触发器。不得只依赖服务层校验。 ### 2. 类目切换后未映射属性的持久化 **状态:M3.0 已落地持久化表(2026-09-09);类目切换写入与恢复操作随编辑草稿保存实现。** 切换目标平台类目后,旧类目中无法映射的属性值必须作为 Atlas 草稿保留,供用户查看、复用或手动处理,不能只出现在一次 API response 的 warning 中。 可选落地方式: - 新增 `product_unmapped_attributes`,保存原类目/属性标识、名称快照、原值、原因、保留时间;或 - 在 `product_platform_profiles` 增加有明确 JSON Schema 的 `unmapped_attributes_draft`。 选择后需要同步商品编辑 API 的读取、保存和类目切换响应。 ### 3. 乐观锁的统一实施 **状态:M3.1 已使用产品/profile 双 `revision` 条件更新、事务回滚和集成测试落地;后续编辑资源不得绕过该规则。** `revision` 不依赖数据库触发器自动递增。所有可编辑资源的更新必须由统一 Repository/数据访问层执行: ```sql UPDATE ... SET ..., revision = revision + 1 WHERE id = :id AND revision = :expected_revision; ``` 受影响行数为 `0` 时,服务层读取当前版本摘要并返回 `409 VERSION_CONFLICT`。必须为并发更新和重复提交编写集成测试;禁止由各业务开发者手写、遗漏该规则。 ### 4. 来源平台、发布目标平台与页头上下文 **状态:已决策,待平台能力模型落地。** `platforms` 需要显式表达以下能力,不能由名称或零散 JSON 约定猜测: - `can_be_source`:可作为采集来源,例如 1688、淘宝、天猫、Ozon; - `can_be_target`:可作为商品发布目标,例如首期 Ozon; - `can_be_header_context`:可出现在 ERP 页头平台切换器。 页头只展示已启用、且 `can_be_header_context = true` 的目标平台;来源信息仍可保存其平台标识。实现时可以使用三个布尔列,或统一的 `capabilities` JSONB,但后者必须在架构文档中给出固定 JSON Schema、默认值和校验规则。 ### 5. 在线商品同步中的“未见”与归档 **状态:待确认。** 建议规则如下,待产品确认后写入接口和同步 Worker: 1. 一次**成功的全量同步**中未见某在线 SKU:仅首次写入 `missing_since`;不归档。 2. 之后连续第二次成功全量同步仍未见,或 `missing_since` 已超过 7 天:写入 `archived_at`,列表默认隐藏但可筛选查看。 3. 同步失败、部分同步或被取消:绝不修改 `missing_since`、`archived_at`。 4. 该 SKU 随后的成功同步再次出现:清空 `missing_since` 与 `archived_at`,恢复正常展示。 ## P2:契约一致性优化 ### 1. 删除的同步与异步响应 **状态:已决策,待统一约定更新。** - 同步软删除成功返回 `204 No Content`。 - 涉及大量素材/衍生文件清理、需要异步任务的删除或清空操作允许返回 `202 Accepted` 与 `operation`。 应将此例外写入 `docs/api/conventions.md`,并在每个异步删除接口明确说明。 ### 2. 平台市场、语言和币种能力的 JSON Schema **状态:待技术设计确认。** 目前平台 API 返回的 `default_market_code`、`default_locale` 等字段可暂存于 `platforms.capabilities`,但必须定义统一 Schema,至少覆盖:默认市场、可用市场、默认语言、可用语言、可用币种,以及上述来源/目标/页头能力。 如平台能力会被频繁查询、筛选或由后台维护,后续可拆分为平台市场/语言/币种关联表;首期不得让各模块自行解释任意 JSON 字段。 ### 3. 修改门店 Client ID 的边界 **状态:待产品确认。** 当前 API 契约禁止编辑 `Client ID`,这比最初“编辑店铺”需求更严格。二选一: - 保持禁止:Client ID 变化视为新增授权,用户新建门店后停用/删除旧门店; - 允许替换:要求同时提交新的 `Client ID` 与 API Key,服务端原子加密替换、清除验证状态并重新验证,验证失败不得使旧密钥与新 Client ID 形成混合状态。 无论选哪种,都不得只允许单独改 `Client ID`。 ## 关闭记录 事项关闭时,在对应小节补充“确认日期、最终决策、影响的 SQL/API/测试文件”。不要直接删除历史结论,以保留设计决策轨迹。