feat(category): 类目树、类目属性、属性字典维护系统构建完成

This commit is contained in:
R524809
2026-09-20 15:11:32 +08:00
parent 388429dbd0
commit 880e441bc6
48 changed files with 5127 additions and 1159 deletions
@@ -0,0 +1,263 @@
# 平台目录缓存、新鲜度与按需刷新方案
> 方案日期:2026-09-20。状态:**第 1~4 步已实现**。
> 迁移 `025`~`027`、系统管理 API/页面、Worker 调度、商品编辑器平台级读取及首次发布预检均已落地。真正的 Ozon 首次发布 job 尚未开发;“第 4 步完成”仅指发布前目录刷新与最终本地校验已具备稳定复用入口。
## 1. 目标与非目标
本方案解决三个问题:
1. 类目树、类型属性和属性字典只保存一份平台级参考数据,同平台所有门店和用户共同使用。
2. 类目树通过低频自动同步维持基线;类型属性和属性字典不做周期性全量自动同步,改为管理员发起、用户访问触发和发布前校验。
3. 用户读取缓存时按新鲜度决定直接返回、后台刷新或等待高优先级刷新;Ozon 请求始终由 Worker 执行,不在 API 进程同步直连。
本阶段不实现门店差异层,不依赖 Ozon 聊天通知,也不假设 Ozon 提供类目变更 Feed。商品/SKU 已填值、门店凭据、发布结果和错误仍属于工作空间业务数据,不迁入平台参考目录。
## 2. 平台级唯一归属
| 参考数据 | 归属与读取键 | 禁止行为 |
|---|---|---|
| 类目/商品类型树 | `platform_id + external_category_id + external_type_id` | 不按 `store_id` 复制,不用名称匹配,不因切换同平台门店重新同步 |
| 类型属性定义 | `platform_id + platform_product_category_id + platform_attribute_id + complex_id` | 不保存门店级属性定义,不使用门店凭据刷新普通商品编辑缓存 |
| 属性字典候选值 | `platform_id + platform_product_category_id + attribute_id + external_value_id` | 不只按 `dictionary_id` 跨类型合并,不按门店保存同一候选值副本 |
所有普通读路径、发布校验和系统后台都读取平台级表。商品编辑器迁移时以 Ozon `description_category_id + type_id` 将现有门店类目引用映射到平台类目 UUID,再把属性和字典读取切换到平台级;迁移完成前保留旧表用于兼容和回滚,不能混用新旧 UUID。
平台目录只使用系统后台为该平台配置的专用参考源凭据。普通用户不会选择同步门店,API 响应和任务载荷不包含 `Api-Key`;只有 Worker 在调用平台时短暂解密凭据。
## 3. 同步触发规则
### 3.1 类目树
- 每个平台每 7 天自动同步一次完整类目/商品类型树。
- 周期从最近一次成功完成时间计算;失败不更新成功时间,按 Worker 重试策略处理。
- 系统后台保留“同步类目树”按钮。管理员手动同步绕过 7 天周期,但复用相同平台上已有的 queued/running 树任务,不重复调用 Ozon。
- 首次配置或更换平台参考源凭据时,自动创建一次树同步任务。
- 树同步完成后只更新节点、父子关系、活动状态和树级同步状态,不自动展开为全部类型属性或字典任务。
类目树自动同步是唯一固定周期同步。类型属性和属性字典均不再由定时器按 TTL 扫描并自动创建任务。
### 3.2 类型属性
类型属性有三种触发来源:
1. 普通用户读取商品编辑数据时发现缓存为 stale、expired 或 missing。
2. 发布任务在校验当前商品类型时发现缓存为 expired 或 missing。
3. 系统管理员在后台按所需范围启动一次类型属性同步批次。
系统管理员不需要逐个类型点击同步。后台提供三个批次入口:
| 按钮 | 批次快照包含范围 | 不包含范围 |
|---|---|---|
| 同步未缓存和过期类型属性 | `missing`、`expired`(`age > 过期阈值`) | `fresh`、`stale` |
| 同步未缓存和不新鲜类型属性 | `missing`、`stale`、`expired`(`age ≥ 新鲜阈值`,以及从未成功同步) | `fresh` |
| 同步全部类型属性 | 全部活动且可发布类型,不考虑缓存年龄 | 无 |
“同步全部类型属性”必须使用红色危险按钮;点击后弹出二次确认,明确展示将处理的类型数量、任务为低速后台批次且可能持续较长时间。用户确认前不得创建批次或子任务。前两个按钮使用普通按钮,但仍需在创建前展示本次命中的类型数量;命中数量为 0 时禁用操作并提示无需同步。
任一批次启动后:
- 固定一份当时活动且可发布的类型快照;后续树同步新增的类型不悄悄加入已经开始的批次。
- 创建父批次记录,记录总数、等待数、运行数、成功数、失败数和取消数。
- Worker/调度器按限流能力逐步生成或领取类型子任务,不把数千任务一次性全部压入 Redis。
- 已有相同 `scope_key` 的 queued/running 任务时直接复用,并计入批次结果。
- 批次只处理对应按钮命中的快照范围;“同步全部类型属性”会请求 fresh 类型。每个子任务的优先级仍按第 5 节的资源年龄计算,管理员批次本身不把 fresh/stale 任务提升为高优先级。
- 批次失败不回滚其他已成功类型,也不清空任何旧缓存。
### 3.3 属性字典
- 不做周期性全平台或全属性自动同步。
- 系统后台保留选择某个类型属性后“同步此字典”的单项操作。
- 普通用户打开字典控件或搜索候选值时,先读取 Atlas 平台缓存,再按第 4 节状态决定是否创建刷新任务。
- 类型属性刷新发现新增字典、`dictionary_id` 变化或属性作用域变化时,只把受影响字典标记为 missing/stale,不自动同步该类型的全部字典。
- 相同平台、类型、属性和规范化搜索词的并发补齐请求必须合并,不因用户或门店不同重复请求 Ozon。
字典的完整性与新鲜度是两个独立维度:
- `is_complete=true` 表示最近一次完整分页已经结束,可以据此处理 Ozon 已删除的旧值。
- `is_complete=false` 表示当前只有部分候选值;已有值仍可按新鲜度使用,但本地搜索未命中不能证明 Ozon 没有该值。
- 部分字典的“查询词补齐”作为独立缓存作用域记录成功时间;该查询词从未请求成功时视为 missing,按高优先级处理。搜索命中不会把整个字典改成完整。
## 4. 新鲜度状态机
类型属性和属性字典默认都使用:
- 新鲜时间:14 天;
- 过期时间:21 天。
资源年龄使用 `now - last_success_at` 计算,`last_success_at` 只在 Ozon 请求成功且新缓存事务提交后更新。排队、运行、失败和取消都不能延长新鲜期。全部比较使用 UTC 实时时长,不按自然日零点取整。
| 状态 | 精确条件 | Atlas 数据能否使用 | Ozon 任务 | 用户读取行为 |
|---|---|---|---|---|
| `fresh` | `age < 14 天` | 可以 | 不创建 | 立即返回 Atlas 缓存 |
| `stale` | `14 天 ≤ age ≤ 21 天` | 可以 | 创建/复用低优先级任务 | 立即返回旧缓存,并标记后台刷新中 |
| `expired` | `age > 21 天` | 不作为当前有效 schema/候选值使用 | 创建/复用高优先级任务 | 等待 Worker;成功后返回新缓存 |
| `missing` | 从未成功同步,或当前作用域无可用快照 | 不可以 | 创建/复用高优先级任务 | 与 expired 相同 |
边界规则:恰好 14 天进入 stale;恰好 21 天仍是 stale 和低优先级;超过 21 天才进入 expired 和高优先级。
失败处理:
- fresh/stale 的后台刷新失败时保留旧快照;在 21 天内仍可继续使用,并把失败状态提供给管理后台观察。
- expired/missing 刷新失败时不能把旧数据伪装为有效结果。API 返回可重试错误和任务状态;已有商品草稿值必须保留,但相关控件和发布校验等待有效 schema/字典后继续。
- 更新必须按资源作用域在单个事务中发布。读取方只能看到上一次完整成功快照或本次完整提交后的新快照,不能看到写入一半的数据。
## 5. Worker 优先级、等待和任务合并
Ozon 调用仍统一通过 Transactional Outbox 和 Worker:
| 资源状态 | Worker 优先级 | 是否允许返回旧缓存 | API 等待策略 |
|---|---|---|---|
| fresh | 不创建任务 | 是 | 不等待 |
| stale(≤21 天) | 低 | 是 | 不等待,立即返回缓存 |
| expired(>21 天) | 高 | 否 | 最多等待短窗口,未完成则返回操作状态 |
| missing | 高 | 否 | 与 expired 相同 |
“等待 Ozon 数据”不代表 API 自己调用 Ozon。推荐交互:
1. API 在事务内创建或复用平台级同步 run 和 outbox。
2. 高优先级目录队列优先于管理员全量批次和 stale 刷新任务。
3. API/前端可等待最多 5 秒;Worker 在窗口内成功时立即重新读取并返回新缓存。
4. 5 秒内未完成时返回 `202`、`operation_id` 和 `cache_status=expired|missing`,前端保持加载状态并轮询或等待推送,不能退回门店缓存。
5. stale 请求立即返回 `200` 和旧缓存,可同时返回 `refresh_operation_id`,前端无需阻塞。
每个平台和资源 `scope_key` 同时最多有一个活动任务。低优先级任务已经 queued、随后资源超过 21 天时,将原任务提升为高优先级并复用同一个 run;若任务已经 running,则不启动重复任务,调用方等待同一个任务。高优先级任务只能插队尚未运行的低优先级任务,不能中断正在执行的 Ozon 请求。
目录交互任务应使用独立的 Worker 并发/限流通道,避免和素材处理等任务争用;高、低优先级共享同一平台级 Ozon 限流器、429 退避、最多尝试次数和审计记录。优先级不能绕过平台限流。
## 6. 普通用户读取流程
普通用户不直接操作系统管理 API。产品 API 根据当前平台和平台类目 UUID读取共享目录,并返回缓存元数据。
### 6.1 类型属性
```text
请求当前商品类型属性
↓
读取平台级属性快照与 last_success_at
↓
fresh ───────────────→ 200 最新 Atlas 缓存
stale ──→ 低优先级刷新 → 200 旧缓存 + refreshing
expired/missing ─────→ 高优先级刷新 → 短暂等待
├─ 成功:200 新缓存
└─ 未完成:202 operation
```
类型属性刷新只调用当前 `description_category_id + type_id` 的属性接口,不刷新类目树、其他类型或全部字典。刷新后比较属性 ID、复杂属性 ID、必填、SKU 维度、类型、数量、字典 ID 和依赖信息,并只失效受影响的字典缓存。
### 6.2 属性字典
1. 打开下拉框时先读取平台级字典缓存。
2. fresh 时立即返回;complete 字典的本地未命中直接视为无结果。
3. stale 时立即返回旧候选值并创建低优先级刷新任务。
4. expired/missing 时创建高优先级任务,更新 Atlas 后才能作为当前候选值返回。
5. partial 字典本地搜索未命中时,检查规范化查询词缓存;查询词 missing/expired 时创建高优先级搜索任务,成功结果先写入平台缓存再返回。
6. 前端可以先显示 stale/partial 的已有候选值,但不能把“部分缓存未命中”展示成 Ozon 确认不存在。
浏览器永远不接收 Ozon 原始凭据,也不使用 Ozon 响应作为未落库的临时事实。任何返回给用户的新候选值都必须已经成功写入 Atlas 平台缓存。
## 7. 发布前校验
- 发布任务只检查商品当前引用的类型属性,不扫描其他类型。
- fresh/stale 类型属性可以用于发布校验;stale 同时创建低优先级刷新,但不阻止本次发布进入后续校验。
- expired/missing 类型属性必须先通过高优先级 Worker 刷新,再使用新 schema 校验必填项、scope 和数量限制。
- 发布使用到的字典值必须存在于该平台、类型和属性作用域的有效缓存。相关字典 expired/missing 时先刷新对应字典或目标查询作用域,不刷新该类型下所有字典。
- Ozon 最终仍可能返回平台校验错误;这类错误按商品/门店记录,必要时将对应平台参考资源标记为需要重新检查,但不能自动生成门店差异层。
## 8. 系统后台调整
「平台类目与属性」页面保留现有平台选择、同步来源、类目树、属性和字典浏览能力,并调整为:
### 8.1 操作区
- “同步类目树”:立即创建/复用平台树任务。
- “同步未缓存和过期类型属性”:只为 missing、expired 类型创建批次。
- “同步未缓存和不新鲜类型属性”:只为 missing、stale、expired 类型创建批次。
- “同步全部类型属性”:红色危险按钮,为所有活动可发布类型创建批次;二次确认中显示命中数量和预计耗时,确认后才能创建任务。
- 类型详情可继续显示单类型最近成功时间和状态;本方案不要求管理员逐个点击才能完成全量属性同步。
- 字典 Drawer 保留“同步此字典”,不增加全平台字典批次。
### 8.2 新鲜度设置
按平台配置以下四个整数天数:
- 类型属性新鲜天数,默认 14;
- 类型属性过期天数,默认 21;
- 属性字典新鲜天数,默认 14;
- 属性字典过期天数,默认 21。
约束:取值建议为 `1..365`,且同一资源的过期天数必须大于新鲜天数。类目树的每周周期首期固定为 7 天,不放入该配置区。
设置保存后立即用于后续读取判断,但不主动扫描并创建数千任务。例如把过期天数从 30 改为 21 后,只有下次用户读取、发布校验或管理员批次触达该资源时才创建任务。修改必须记录操作者、修改时间和前后值。
### 8.3 状态展示
管理页至少展示:
- 类目树最近成功时间和下次计划时间;
- 类型属性批次的总数、等待、运行、成功、失败和完成百分比;
- 当前类型属性的 `fresh/stale/expired/missing`、最近成功时间和最近错误;
- 字典的相同新鲜度状态、完整/部分状态、候选值数量和最近错误;
- 高低优先级、触发来源(weekly/manual/admin_batch/user_access/publish)和任务复用关系。
## 9. 建议的数据与契约调整
确认后至少需要补充以下模型,具体 DDL 和字段名在实现阶段冻结:
1. 平台同步策略:保存属性、字典的新鲜和过期天数以及审计字段。
2. 资源同步状态:按 tree、类型属性、属性字典和字典搜索作用域保存 `last_success_at`、`is_complete`、最新任务及错误;不能仅依赖数据行的 `updated_at` 推断是否成功检查过。
3. 属性批次:保存管理员批次快照和聚合进度,子任务继续使用既有平台 run/outbox 事实。
4. 任务优先级与触发来源:区分 high/low 及 weekly、manual、admin_batch、user_access、publish。
5. 商品编辑引用:从门店类目 UUID 迁移为平台类目 UUID;属性和值仍按当前商品/SKU 草稿保存,但校验引用平台 schema。
普通产品读取响应建议统一带:
```json
{
"data": [],
"cache": {
"status": "fresh",
"last_success_at": "2026-09-20T00:00:00Z",
"fresh_until": "2026-10-04T00:00:00Z",
"expires_at": "2026-10-11T00:00:00Z",
"is_complete": true,
"refresh_run_id": null,
"refresh_status": null,
"retry_after_ms": null
}
}
```
管理 API 需要增加同步策略读取/修改和类型属性批次创建/读取;现有单项 `POST .../syncs` 保留给树和字典。最终端点、状态码和错误码必须在实现前更新 [`platform-catalog-admin.md`](../api/platform-catalog-admin.md) 与 [`product-editor-and-publishing.md`](../api/product-editor-and-publishing.md)。
## 10. 实施顺序与验收
建议分四步实施,避免继续扩大门店级缓存:
1. **平台状态与调度**:增加策略、资源状态、批次和优先级;把树改为每周自动同步,关闭属性/字典周期自动扫描。
2. **后台能力**:加入新鲜度设置、三种范围的类型属性批次入口及统一批次进度,保留树及单字典手动同步。
3. **商品编辑迁移**:通过 Ozon 外部 ID 回填平台类目引用,切换属性和字典读取、按需刷新与任务等待;保留兼容回滚路径。
4. **发布迁移**:发布前使用平台 schema/字典状态执行定向高优先级刷新和最终校验,验证稳定后再讨论清理门店级目录表。已通过 `POST .../publish-preflights` 落地;未来 `publish-jobs` 必须先调用相同服务,不能自行绕过。
当前完成度:第 1~4 步已完成。第 3 步由 `027_product_editor_platform_catalog.sql` 回填平台类目引用并保留旧门店列;编辑器返回统一 cache 元数据,stale 创建低优先级任务并立即返回,expired/missing 创建高优先级任务、最多等待 5 秒后返回 `202`;部分字典的未命中关键词使用独立 `dictionary_search` 任务。第 4 步新增首次发布预检:只接受所选未导入 SKU,先刷新当前 schema,再从已确认草稿识别实际使用的字典;所有 high 任务共用 5 秒窗口,stale 继续 low 后台刷新,最终校验同时检查字典缓存有效期和候选值仍有效。
验收至少覆盖:
- 同平台不同门店读取相同类目、属性和字典记录,不产生门店副本。
- 13 天 23:59:59、恰好 14 天、恰好 21 天和超过 21 天四个边界行为正确。
- stale 立即返回旧缓存并只创建一个低优先级任务;expired/missing 复用一个高优先级任务并等待新缓存。
- queued 低优先级任务可提升,高并发用户不会重复调用 Ozon。
- 每周树任务、管理员树任务和来源变更任务正确去重。
- 三种类型属性批次分别准确覆盖 missing+expired、missing+stale+expired、全部活动类型;支持部分失败和准确进度,不自动创建字典任务。
- “同步全部类型属性”在二次确认前不产生批次、run 或 outbox,另外两个按钮在无命中类型时不能创建空批次。
- 字典完整/部分状态与查询词补齐不混淆;只有完整同步才能停用消失的旧值。
- 调整策略后不立即制造全量任务,下一次访问按新阈值生效。
- Ozon 失败、超时、429 和 Worker 重启均保留上次成功缓存、任务审计与可恢复状态。
## 11. 已确认的产品规则
1. 管理员属性批次有三种范围:missing+expired、missing+stale+expired、全部活动类型;全量批次会刷新 fresh 类型,并使用红色按钮和二次确认。
2. `age = 21 天` 仍属于 stale/低优先级;只有严格大于 21 天才属于 expired/高优先级。
3. missing 包括从未同步成功的整个资源,以及 partial 字典中从未成功补齐的具体查询作用域,均按高优先级处理。
4. expired 数据可以用于保留和展示已有草稿值,但不能作为当前可选 schema/候选值或发布校验事实。
5. 类目树周期首期固定 7 天;后台只配置类型属性和属性字典的两组新鲜/过期阈值。
@@ -1,6 +1,6 @@
# 平台级商品参考目录的归属与维护决策
> 决策日期:2026-09-18。状态:平台参考目录、系统管理员维护 API/页面及定时同步链路已经实现;商品编辑器的门店级引用尚未迁移。当前商品编辑契约仍以 [`product-editor-and-publishing.md`](../api/product-editor-and-publishing.md) 为准。本决策取代 2026-09-10「每个门店完整保存一份类目、属性和字典」的目标设计,不抹去其实现历史。
> 决策日期:2026-09-18。状态:平台参考目录、系统管理员维护 API/页面、同步链路、商品编辑器平台级读取及首次发布预检已实现;真正的 Ozon 发布 job 尚未开发。当前商品编辑契约以 [`product-editor-and-publishing.md`](../api/product-editor-and-publishing.md) 为准。本决策取代 2026-09-10「每个门店完整保存一份类目、属性和字典」的目标设计,不抹去其实现历史。
## 1. 依据与边界
@@ -19,14 +19,14 @@ Atlas 本阶段明确采用**平台级参考数据**:同一 `platform_id` 下
平台参考表以 `platform_id` 隔离 Ozon、未来的 Wildberries 等平台,只保存对应平台可复用的参考元数据;不存客户凭据、商品、价格或店铺专有数据,也不使用 `store_id` 作为目录分区键。所有门店连接和商品引用仍须满足 [`development-collaboration-contract.md`](../project/development-collaboration-contract.md) 的工作空间外键与 RLS 约束。读 API 依页头平台上下文过滤,不能跨平台混用 ID,也不能据此枚举其他店铺信息。
目标交互上,切换到**同一平台**的另一个门店不应清空已选类目和属性;切换平台则必须重新选择该平台的类目并按新平台 schema 校验。新门店凭据验证只处理该门店连接,不为其复制或重新同步整套平台目录;若平台目录尚未准备好,由平台级任务补齐。当前门店级外键尚未迁移前仍维持现有切换行为。
切换到**同一平台**的另一个门店不会清空已选类目和属性;切换平台则必须重新选择该平台的类目并按新平台 schema 校验。新门店凭据验证只处理该门店连接,不为其复制或重新同步整套平台目录;若平台目录尚未准备好,由平台级任务补齐。
## 3. 同步方案:自动为主、人工为辅
## 3. 同步方案:当前实现与已确认目标
统一的后台同步服务按 `platform_id` 负责定时任务、首次缺失时的补齐和管理员手动请求。三种触发方式都创建有权限校验、幂等去重的 `platform_catalog_sync_runs + platform_catalog_outbox_events`;这是不带 `workspace_id` 的全局任务事实,不能借用租户 `operations/outbox_events`。Worker 通过平台适配器读取服务端安全存储的参考源凭据并调用对应平台。浏览器、日志、run 和 outbox 载荷不包含 API Key。管理员手动同步不得在 HTTP 请求内直接访问平台或绕过同一套限流、审计与事务发布流程。
1. **同步来源**:每个平台分别配置 Atlas 专用的参考源凭据及适配器;Ozon 首期使用专用参考门店凭据作为技术采集身份,而非普通客户工作空间门店。设置最小授权、密钥轮换、失效告警与备用源,不能未经授权借用客户门店密钥。若暂无专用凭据,保持现有门店级链路,待来源配置完成后迁移。
2. **基线调度**:当前实现为类目树和类型属性成功后 24 小时内不重复、字典成功后 7 天内不重复;调度器每 60 秒扫描,每个平台最多 6 个活动任务。BullMQ 最多尝试 5 次并从 5 秒开始指数退避。当前固定按树、属性、字典排序,在数千类型下会造成字典饥饿;后续必须改为属性/字典配额轮转,并增加近期访问优先级。具体间隔是 Atlas 运营参数,不是 Ozon 文档承诺的 TTL。
2. **基线调度(当前代码事实)**:类目树最近成功时间达到 7 天后由调度器创建低优先级周任务;管理员仍可手动创建高优先级树任务。类型属性和字典已经退出周期自动候选,类型属性只从管理员显式创建的三种批次中逐批领取,字典只保留单项手动入口。普通商品读路径已按 14/21 天执行 fresh/stale/expired/missing 状态机,并创建或复用 user_access 平台任务。
3. **管理员页面**:按平台展示参考源状态、树、属性、字典候选值、最后成功时间、失败详情、排队任务和「同步类目树 / 指定类目属性 / 指定字典」操作。手动动作必须受系统管理员权限控制、记录操作者,并返回异步任务;页面不展示密钥明文。当前实现位于 `/system/platform-catalog`,全局系统管理员授权独立于工作空间角色。
4. **读路径**:正常用户只读 Atlas 缓存。首次缺失时可排队补齐并显示准备中;已有数据过期时优先提供上次成功快照并后台更新。不能在每次打开编辑器或下拉框时同步请求 Ozon。大字典搜索按规范化关键词及上述字典作用域共享缓存,并去重并发补齐。
5. **原子性与监控**:P0 按平台及树、类型属性、字典作用域在单事务内 upsert,提交前读取方继续看到旧数据;失败保留上次成功缓存。仅在确认完整分页完成后才处理消失的字典值,部分同步记录 `is_complete=false`。当前尚未实现不可变版本快照、内容哈希、`next_refresh_at`、连续失败告警和异常删减保护,这些属于上线前可靠性加固,不能在文档中误记为已完成。
@@ -41,6 +41,6 @@ Ozon 字典分页每次最多 2000 个值;[搜索接口](https://docs.ozon.ru/
## 5. 从现有实现迁移
当前 `022_store_catalog_cache.sql` 的 `store_product_categories`、属性和字典表,以及 profile 的 `store_product_category_id` 外键都是已上线的门店级事实,**本次文档决策不改变它们**。旧 `001_foundation.sql` / `021_product_category_attribute_schema.sql` 虽已有平台级表,但原字典唯一键不足以表达按类目/类型变化的字典;不能直接当成最终系统目录复用。
`022_store_catalog_cache.sql` 的门店级表和 profile 的 `store_product_category_id` 仍保留为兼容/回滚数据,但商品编辑器不再读取它们。当前事实来源是平台级类目、属性和 `platform_catalog_dictionary_values`。
迁移 `024_platform_catalog_management.sql` 已补齐以 `platform_id` 为作用域的参考来源、候选值和同步任务表,**没有建立门店差异表**;API、Worker 和管理页面已经按该作用域工作。下一步仍需以 Ozon 外部 ID 对照旧门店记录、回填平台目录。草稿/接口迁移应先提供兼容读写与可回滚映射,逐步改为平台目录引用,最后经校验和维护者批准再清理冗余门店缓存。现有商品编辑 HTTP 路径、UUID 含义和门店同步 operation 尚未改变;任何契约变更须先更新 `docs/api/`、共享类型和测试。Wildberries 等新平台应新增平台适配器及自己的外部 ID 映射,不复用 Ozon 的 `description_category_id + type_id` 语义。
迁移 `024`~`026` 建立平台参考来源、候选值、同步任务、策略和资源状态;`027_product_editor_platform_catalog.sql` 以 Ozon 外部类目/类型 ID 回填商品的 `platform_product_category_id`,并为部分字典增加关键词搜索任务作用域。旧门店列和表暂不删除,新保存会尽量反写可映射的门店类目 UUID,供回滚使用。清理旧表必须在发布链路迁移、稳定性验证和维护者批准后另行执行。Wildberries 等新平台应新增平台适配器及自己的外部 ID 映射,不复用 Ozon 的 `description_category_id + type_id` 语义。