8.6 KiB
平台级商品参考目录的归属与维护决策
决策日期:2026-09-18。状态:平台参考目录、系统管理员维护 API/页面及定时同步链路已经实现;商品编辑器的门店级引用尚未迁移。当前商品编辑契约仍以
product-editor-and-publishing.md为准。本决策取代 2026-09-10「每个门店完整保存一份类目、属性和字典」的目标设计,不抹去其实现历史。
1. 依据与边界
Ozon 的类目树接口请求体仅以 language 选择返回语言;属性接口以 description_category_id + type_id + language 查询属性;字典值接口还要求 attribute_id,并用 last_value_id 分页。属性响应的 category_dependent 指示字典值是否随类目变化。三个接口都需要卖家 Client-Id + Api-Key。
Atlas 本阶段明确采用平台级参考数据:同一 platform_id 下的所有门店共用一套类目/商品类型树、类型属性定义和字典候选值,不再按门店复制,也不建立或维护门店差异层。Ozon 文档未保证所有卖家账号返回完全一致;“同平台门店共用”是本阶段产品假设,而不是 Ozon 官方保证。若实际发现账号差异,再评审下文保留的扩展方案,不提前实现。
2. 数据归属
| 数据 | 目标归属与键 | 约束 |
|---|---|---|
| 类目/商品类型树 | platform_id 下的一份平台目录,名称按语言保存;Ozon 可发布叶子使用 description_category_id + type_id 标识 |
同平台门店读取相同的目录节点、父子关系与 disabled;不以名称或单独 type_id 判等,也不复制到各门店 |
| 类型属性定义 | platform_id + 平台类目/类型键 + 属性键 + 语言/版本 |
Ozon 的 is_required、is_aspect、dictionary_id、类型和数量限制属于其平台适配数据;其他平台由各自适配器定义外部标识和规则 |
| 字典候选值 | platform_id + 平台类目/类型键 + 属性键 + 字典键 + 语言 + 值键;记录完整/部分同步状态 |
Ozon 的 category_dependent=false 可供后续去重,但不能仅以 dictionary_id + value_id 跨类目或跨平台合并 |
| 门店连接与商品/SKU 已填属性 | 原工作空间、门店、商品、profile、SKU | 凭据、上架结果、用户填报值、确认状态和发布快照仍是业务数据,按工作空间隔离;它们不是平台参考目录 |
平台参考表以 platform_id 隔离 Ozon、未来的 Wildberries 等平台,只保存对应平台可复用的参考元数据;不存客户凭据、商品、价格或店铺专有数据,也不使用 store_id 作为目录分区键。所有门店连接和商品引用仍须满足 development-collaboration-contract.md 的工作空间外键与 RLS 约束。读 API 依页头平台上下文过滤,不能跨平台混用 ID,也不能据此枚举其他店铺信息。
目标交互上,切换到同一平台的另一个门店不应清空已选类目和属性;切换平台则必须重新选择该平台的类目并按新平台 schema 校验。新门店凭据验证只处理该门店连接,不为其复制或重新同步整套平台目录;若平台目录尚未准备好,由平台级任务补齐。当前门店级外键尚未迁移前仍维持现有切换行为。
3. 同步方案:自动为主、人工为辅
统一的后台同步服务按 platform_id 负责定时任务、首次缺失时的补齐和管理员手动请求。三种触发方式都创建有权限校验、幂等去重的 platform_catalog_sync_runs + platform_catalog_outbox_events;这是不带 workspace_id 的全局任务事实,不能借用租户 operations/outbox_events。Worker 通过平台适配器读取服务端安全存储的参考源凭据并调用对应平台。浏览器、日志、run 和 outbox 载荷不包含 API Key。管理员手动同步不得在 HTTP 请求内直接访问平台或绕过同一套限流、审计与事务发布流程。
- 同步来源:每个平台分别配置 Atlas 专用的参考源凭据及适配器;Ozon 首期使用专用参考门店凭据作为技术采集身份,而非普通客户工作空间门店。设置最小授权、密钥轮换、失效告警与备用源,不能未经授权借用客户门店密钥。若暂无专用凭据,保持现有门店级链路,待来源配置完成后迁移。
- 基线调度:当前实现为类目树和类型属性成功后 24 小时内不重复、字典成功后 7 天内不重复;调度器每 60 秒扫描,每个平台最多 6 个活动任务。BullMQ 最多尝试 5 次并从 5 秒开始指数退避。当前固定按树、属性、字典排序,在数千类型下会造成字典饥饿;后续必须改为属性/字典配额轮转,并增加近期访问优先级。具体间隔是 Atlas 运营参数,不是 Ozon 文档承诺的 TTL。
- 管理员页面:按平台展示参考源状态、树、属性、字典候选值、最后成功时间、失败详情、排队任务和「同步类目树 / 指定类目属性 / 指定字典」操作。手动动作必须受系统管理员权限控制、记录操作者,并返回异步任务;页面不展示密钥明文。当前实现位于
/system/platform-catalog,全局系统管理员授权独立于工作空间角色。 - 读路径:正常用户只读 Atlas 缓存。首次缺失时可排队补齐并显示准备中;已有数据过期时优先提供上次成功快照并后台更新。不能在每次打开编辑器或下拉框时同步请求 Ozon。大字典搜索按规范化关键词及上述字典作用域共享缓存,并去重并发补齐。
- 原子性与监控:P0 按平台及树、类型属性、字典作用域在单事务内 upsert,提交前读取方继续看到旧数据;失败保留上次成功缓存。仅在确认完整分页完成后才处理消失的字典值,部分同步记录
is_complete=false。当前尚未实现不可变版本快照、内容哈希、next_refresh_at、连续失败告警和异常删减保护,这些属于上线前可靠性加固,不能在文档中误记为已完成。
Ozon 字典分页每次最多 2000 个值;搜索接口要求至少两个字符、每次最多 100 个值。搜索命中只是局部候选,不能证明字典已完整同步。当前平台任务单次最多读取 5 页(10000 项);超过上限会正确保留为部分缓存,但续传游标尚未实现。
4. 门店差异层的未来预案(本阶段不实施)
本阶段按同平台所有门店使用相同目录、属性定义和候选值实施:不创建门店差异表,不按门店同步参考目录,不在读取或发布校验时叠加门店覆盖规则。门店凭据状态和某次发布的失败原因仍按门店记录,但不据此自动改写平台共享目录。
如果联调或线上证据表明确有账号专属的可售状态、属性或字典差异,再单独提出迁移:可按 workspace_id + store_id 建门店覆盖层,或按市场/卖家分组收窄共享范围,并制定差异检测与回退策略。该预案只保留扩展方向,不是当前开发任务或上线前置条件。
5. 从现有实现迁移
当前 022_store_catalog_cache.sql 的 store_product_categories、属性和字典表,以及 profile 的 store_product_category_id 外键都是已上线的门店级事实,本次文档决策不改变它们。旧 001_foundation.sql / 021_product_category_attribute_schema.sql 虽已有平台级表,但原字典唯一键不足以表达按类目/类型变化的字典;不能直接当成最终系统目录复用。
迁移 024_platform_catalog_management.sql 已补齐以 platform_id 为作用域的参考来源、候选值和同步任务表,没有建立门店差异表;API、Worker 和管理页面已经按该作用域工作。下一步仍需以 Ozon 外部 ID 对照旧门店记录、回填平台目录。草稿/接口迁移应先提供兼容读写与可回滚映射,逐步改为平台目录引用,最后经校验和维护者批准再清理冗余门店缓存。现有商品编辑 HTTP 路径、UUID 含义和门店同步 operation 尚未改变;任何契约变更须先更新 docs/api/、共享类型和测试。Wildberries 等新平台应新增平台适配器及自己的外部 ID 映射,不复用 Ozon 的 description_category_id + type_id 语义。