Files
atlas-seller-kit/docs/architecture/platform-catalog-cache-refresh-plan.md
T

264 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 平台目录缓存、新鲜度与按需刷新方案
> 方案日期: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 天;后台只配置类型属性和属性字典的两组新鲜/过期阈值。