Update seller store experience

This commit is contained in:
R524809
2026-09-08 16:19:38 +08:00
parent ceb644a3f9
commit 319887726e
25 changed files with 31417 additions and 127 deletions
+8 -6
View File
@@ -9,6 +9,7 @@
| 项目 | [`project/`](./project/) | 产品定位、交接、协作约定与已确认的长期约束 |
| 产品需求 | [`requirements/`](./requirements/) | 页面、流程、字段和待确认事项 |
| 基础架构 | [`architecture/`](./architecture/) | 多租户、平台、门店、水印、物流和定价的数据设计及 DDL |
| 图片存储策略 | [`architecture/media-storage-strategy.md`](./architecture/media-storage-strategy.md) | 采集候选图片的按需物化、AI/水印处理与发布素材边界。 |
| 商品领域设计 | [`product/`](./product/) | 已有的多 SKU 商品编辑页、领域模型与 API 草案 |
| 项目结构 | [`architecture/project-structure.md`](./architecture/project-structure.md) | pnpm monorepo 的目录、依赖关系与本地运行边界。 |
| 外部平台资料 | [`integrations/`](./integrations/) | Ozon Seller API 的整理资料与适配依据 |
@@ -32,9 +33,10 @@
12. [`architecture/database-security.md`](./architecture/database-security.md):迁移/应用数据库角色、RLS 强制策略与部署职责。
13. [`architecture/transactional-outbox-and-idempotency.md`](./architecture/transactional-outbox-and-idempotency.md):关键写请求去重、事务 Outbox、BullMQ 投递与验收规则。
14. [`guides/object-storage-object-key.md`](./guides/object-storage-object-key.md):对象存储、`object_key`、来源 URL 与预签名 URL 的使用边界。
15. [`guides/browser-extension-background-service-worker.md`](./guides/browser-extension-background-service-worker.md):浏览器扩展后台 Service Worker 的运行模型、消息调用链与调试方法。
16. [`guides/minio-local-deployment.md`](./guides/minio-local-deployment.md):MinIO 简介、本地 Docker 部署与应用侧使用方法。
17. [`guides/postgresql-local-development.md`](./guides/postgresql-local-development.md):原生 PostgreSQL / Docker Compose 本地数据库、角色、迁移与建表。
18. [`api/README.md`](./api/README.md):HTTP API 统一约定及第一批账户、门店、水印接口。
19. [`reviews/implementation-readiness-open-items.md`](./reviews/implementation-readiness-open-items.md):实现前必须闭环的数据库/API 事项与已确认约束。
19. 后续按模块补全定价、采集、商品编辑、发布和在线商品接口。
15. [`architecture/media-storage-strategy.md`](./architecture/media-storage-strategy.md):**采集图片不自动转存、按实际使用再物化的强制设计规则。**
16. [`guides/browser-extension-background-service-worker.md`](./guides/browser-extension-background-service-worker.md):浏览器扩展后台 Service Worker 的运行模型、消息调用链与调试方法。
17. [`guides/minio-local-deployment.md`](./guides/minio-local-deployment.md):MinIO 简介、本地 Docker 部署与应用侧使用方法。
18. [`guides/postgresql-local-development.md`](./guides/postgresql-local-development.md):原生 PostgreSQL / Docker Compose 本地数据库、角色、迁移与建表。
19. [`api/README.md`](./api/README.md):HTTP API 统一约定及第一批账户、门店、水印接口。
20. [`reviews/implementation-readiness-open-items.md`](./reviews/implementation-readiness-open-items.md):实现前必须闭环的数据库/API 事项与已确认约束。
21. 后续按模块补全定价、采集、商品编辑、发布和在线商品接口。
+11 -7
View File
@@ -22,24 +22,24 @@
`POST /api/extension/materials`
Headers:扩展 Bearer token 与 `Idempotency-Key` 必填。请求体为已定义的 `CollectedProductPayload`,其 `source` 至少包含 `platform`、`itemKey`、`canonicalUrl`、`collectedAt`、`parserVersion`;`variants` 仅列来源有稳定证据的真实组合,媒体只传来源 URL/关系和证据,不传图片二进制。
Headers:扩展 Bearer token 与 `Idempotency-Key` 必填。请求体为已定义的 `CollectedProductPayload`,其 `source` 至少包含 `platform`、`itemKey`、`canonicalUrl`、`collectedAt`、`parserVersion`;`variants` 仅列来源有稳定证据的真实组合,媒体只传来源 URL、`mediaType`(`image` / `video`)、可选 `posterUrl`、关系和证据,不传二进制。
请求体不得包含或传入 `workspace_id`、`target_platform_id`、`platform_id`、`store_id`、`api_key`、Cookie、授权头或来源站会话字段。服务端从扩展令牌解析工作空间,并将来源 `platform` 映射为系统已启用的来源平台。
服务端在一个事务中:检查工作空间采集箱上限 → 按 `workspace + source_platform + source_item_key` 查找活动记录 → 创建记录,或以本次页面快照覆盖已有记录的来源 URL、标题、价格、原始证据、规范化载荷、SKU 草稿与候选素材 → 再经 outbox 投递素材 Worker。重复上报不会增加采集箱配额,且会保留原有 `collection_item` 与 `product` ID;旧素材任务被取消,新素材任务使用新的来源快照版本,旧 Worker 不得回写新快照。
服务端在一个事务中:检查工作空间采集箱上限 → 按 `workspace + source_platform + source_item_key` 查找活动记录 → 创建记录,或以本次页面快照覆盖已有记录的来源 URL、标题、价格、原始证据、规范化载荷、SKU 草稿与候选素材。采集阶段的图片和视频只保存为 `pending_selection` 候选记录,**不下载媒体、不投递素材 Worker**;用户在编辑流程中选择媒体后,才调用按需转存接口。重复上报不会增加采集箱配额,且会保留原有 `collection_item` 与 `product` ID;旧快照中尚未完成的候选或任务会被取消,新快照使用新的来源快照版本,旧 Worker 不得回写新快照。
成功返回 `201`(新建)或 `200`(复用):
```json
{ "data": {
"collection_item": { "id": "uuid", "status": "materializing", "source_title": "商品标题", "variant_count": 3 },
"collection_item": { "id": "uuid", "status": "ready_for_edit", "source_title": "商品标题", "variant_count": 3 },
"product_id": "uuid", "reused": false, "refreshed": false,
"editor_entry_path": "/products/uuid",
"materialization": { "total": 12, "queued": 12, "succeeded": 0, "failed": 0 }
"materialization": { "total": 12, "pending_selection": 12, "queued": 0, "succeeded": 0, "failed": 0 }
} }
```
达到工作空间上限时返回 `422 COLLECTION_LIMIT_REACHED`,并附 `{current_count, limit}`;不能因重复点击或并发请求绕过 1000 条限制。单个来源素材失败不使采集事务失败,关键商品/SKU 解析失败才使记录进入 `failed` 并返回可展示的脱敏摘要。
达到工作空间上限时返回 `422 COLLECTION_LIMIT_REACHED`,并附 `{current_count, limit}`;不能因重复点击或并发请求绕过 1000 条限制。采集阶段不会因下载来源媒体失败而失败;关键商品/SKU 解析失败才使记录进入 `failed` 并返回可展示的脱敏摘要。
## 3. Ozon Seller 网页本地观察
@@ -80,7 +80,7 @@ Headers:扩展 Bearer token 与 `Idempotency-Key` 必填。请求体为已定
}], "meta": { "next_cursor": null, "has_more": false, "active_count": 12, "collection_limit": 1000 } }
```
`listing_summary.status` 映射 UI 的未上架/已部分上架/已上架;`collection_status` 是采集/素材处理状态,二者绝不混用。首张素材转存未完成时 thumbnail 为占位状态;商品未命名时 `title` 返回 `null`,前端展示“未命名商品”。
`listing_summary.status` 映射 UI 的未上架/已部分上架/已上架;`collection_status` 是采集/素材处理状态,二者绝不混用。采集测试页应直接使用候选的 `source_url` 预览图片,视频则用 `preload=none` 的播放器,不等待转存、也不为预览创建存储对象;商品未命名时 `title` 返回 `null`,前端展示“未命名商品”。
`GET /api/collection-items/{collection_item_id}`
@@ -90,7 +90,7 @@ Headers:扩展 Bearer token 与 `Idempotency-Key` 必填。请求体为已定
Headers:`If-Match`、`Idempotency-Key` 必填。首期仅允许修改 `note`;标题、SKU 和素材在商品编辑 API 中修改。响应返回更新后的采集记录。`If-Match` 对应记录的 `revision`,包括采集 Worker 状态更新在内的每次修改都递增;不得覆盖 Worker 的状态更新。
## 5. 手动创建、删除与素材重试
## 5. 手动创建、按需转存、删除与素材重试
`POST /api/collection-items`
@@ -102,6 +102,10 @@ Headers:`Idempotency-Key` 必填。
创建 `source_kind=manual` 的采集记录、空商品草稿和默认 SKU,返回 `201`、`collection_item`、`product_id` 与 `editor_entry_path`。它仍不绑定目标平台/店铺,也不绕过之后的类目、定价和首次发布校验。
`POST /api/collection-items/{collection_item_id}/asset-materializations`
Headers:`Idempotency-Key` 必填。请求 `{ "transfer_ids": ["uuid"] }`;只能选择当前来源快照中状态为 `pending_selection` 的候选图片或视频。服务端将所选候选改为 `queued`,创建 operation,并通过 Outbox 投递 `collection.assets.materialize.requested` 给 Worker;返回 `202` 和 operation。该接口是商品编辑、加水印、AI 处理和发布流程共用的按需转存原语,不应在采集测试页自动调用。
`POST /api/collection-items/{collection_item_id}/asset-retries`
Headers:`Idempotency-Key` 必填。请求 `{ "transfer_ids": ["uuid"] }`;只能重试该采集记录中 `failed` 或 `cancelled` 的转存任务。返回 `202` 和 operation。不能用它重试来源商品解析或已成功素材。
+102
View File
@@ -0,0 +1,102 @@
# 商品采集媒体(图片与视频)存储策略
> 状态:采集阶段的按需物化已落地;正式商品编辑、水印、AI 图片处理和发布尚未接入选择动作。后续任何相关开发必须以本文为准;接口、数据模型和 Worker 改动须先在同一任务中完成相应契约更新。
## 1. 目标与基本原则
采集阶段的绝大多数图片和视频最终不会被用户选中、编辑或发布。首期因此采用**按需物化**:先保存可审查的来源候选信息,只有用户实际使用媒体时才下载、校验并写入 Atlas 对象存储。
1. `source_url` 是来源证据与即时预览地址,不是 Atlas 控制的正式素材,也不是可靠的长期地址。
2. `media_asset` 才是经过大小、格式、安全校验并由 Atlas 控制的正式素材;编辑、发布和需要稳定重试的处理链路只能引用它。
3. 浏览器可显示来源图片不代表后端或第三方 AI 服务可稳定下载它。来源可能有防盗链、临时 URL、限流、地区限制、图片替换或失效。
4. Worker 不携带来源站 Cookie,不能为了下载图片绕过来源站的会话或防盗链机制。
5. 首期不做“已选择但未发布素材”的自动生命周期删除。自动清理需要定义引用、编辑中状态、恢复和误删处理,复杂度与风险超过当前收益。
## 2. 场景决策表
| 场景 | 是否需要先转存原图 | 原因 |
|---|---:|---|
| 采集测试页预览 | 否 | 直接显示来源 URL;视频使用 `preload=none`,最快、零存储成本 |
| 采集箱暂存 | 否 | 只保存候选 URL、媒体类型、角色、SKU 关联、视频封面与证据 |
| 用户选图/视频进入编辑 | 建议是 | 转存选中的媒体,保证后续可用、可重试 |
| 加水印 | 不一定 | 可在任务中临时下载来源图→加水印→只保存水印结果;但来源失效后无法重试 |
| AI 生图 / AI 改图 | 通常要先转存或至少临时下载 | AI 服务需要实际图片字节,不能可靠地只依赖来源 URL |
| 正式发布 | 是 | 必须使用 Atlas 控制的已验证素材,不能依赖第三方 URL |
## 3. 推荐流程
```text
来源商品页
→ 采集候选 source_url、媒体类型、图片角色、SKU/规格关联、排序、视频封面和证据
→ 采集箱 / 测试页直接加载 source_url 供人工核对
→ 用户选择图片、请求水印/AI 处理或发起发布
→ 按需下载 → HTTPS/格式/大小/内容校验 → 创建或复用 media_asset
→ 编辑、派生任务和发布仅引用 media_asset
```
### 3.1 采集与即时预览
- 采集 API 只持久化来源候选信息;不得在采集完成后自动创建“下载全部候选图”的素材任务或 Outbox 事件。
- 测试页和后续采集箱详情优先以 `source_url` 作为 `<img>` 或 `<video preload="none">` 预览。跨域媒体通常可以由浏览器显示,但来源拒绝加载时只显示明确的失败状态与“查看来源”链接。
- 来源预览不得被误写入 `media_assets`、商品正式媒体关系、发布请求或长期缓存。
### 3.2 选择图片与正式编辑
- 用户首次把候选图设为商品主图、SKU 图、详情图,或进入依赖稳定原图的编辑操作时,服务端创建按需转存任务。
- 转存完成后,界面和后续操作切换到该 `media_asset` 的短时访问 URL;来源 URL 仍保留为采集证据。
- 转存失败或来源图已不可访问时,应提示用户重新采集/重新选择,而不是尝试携带来源 Cookie 或伪造 Referer 绕过限制。
### 3.3 水印与 AI 图片处理
- 水印任务可以临时下载来源图并只存储水印派生结果,以节约原图存储;但该模式无法在来源失效后可靠重试。需要稳定重试、再次编辑或保留原图时,应先转存原图。
- AI 生图不一定需要来源图;AI 改图、图生图、抠图等需要图片输入的能力,必须获得实际图片字节。实现可选择先物化 `media_asset`,或在单次任务内临时下载后提交给 AI 提供商。
- 向 AI 提供商发送来源图片字节或可访问 URL 属于对第三方的数据传输。产品界面、隐私说明和任务审计必须在该能力立项时明确告知;不得默认将所有采集候选图发送给 AI。
- 所有水印和 AI 的成功输出都必须保存为 Atlas 控制的 `media_asset`,并记录输入素材/来源、模板或模型版本与任务证据。
### 3.4 发布
- 发布前必须验证每张已选图片是当前工作空间内已校验的 `media_asset`。
- 禁止将来源站 URL 直接提交给目标销售平台;发布任务只能使用 Atlas 对象存储中的受控素材或由平台要求的上传结果。
## 4. 首期明确不做的能力
不实现“已选择但未发布素材在 7~30 天未使用即自动删除”。实现这一策略需要追踪编辑草稿、引用关系、处理中任务、发布快照、恢复窗口和误删申诉;在没有真实成本数据前,首期保留用户已按需转存的素材。
未来如存储成本成为问题,优先考虑只清理**从未被商品媒体、派生任务或发布快照引用**的素材,并在独立需求、数据模型、Worker 与用户通知方案评审后实施。
## 5. 已落地的按需物化边界
`POST /api/extension/materials` 已将图片和视频保存为 `collection_asset_transfers` 的 `pending_selection` 候选,不会创建素材 Worker 的 Outbox 事件。候选记录带 `media_type`,视频可另带 `poster_url`。重复采集会取消旧快照中尚未完成的候选或转存任务,并以新快照候选覆盖它们。
`POST /api/collection-items/{id}/asset-materializations` 是当前唯一的按需转存入口:请求必须包含当前快照中的一个或多个 `pending_selection` 候选 ID,并携带 `Idempotency-Key`。服务端将所选行标为 `queued`、创建 operation 并写入 `collection.assets.materialize.requested` Outbox 事件;既有 Worker 继续执行安全下载、校验、去重与 `media_asset` 创建。
`/collection-test/{id}` 对图片候选直接显示 `source_url`,对视频候选使用按需播放的来源 URL;已转存的历史素材则优先显示 Atlas 短时预览 URL。该测试页只核对数据,不能触发转存。
### 5.1 首期视频格式边界
- 当前采集器只接受来源商品结构化状态中明确给出的 HTTPS `MP4` / `WebM` 直链;Ozon 读取 `webGallery` / `webVideo`,1688 读取 `gallery.fields.video|videos`,淘宝/天猫读取 `item.videos`。绝不扫描整页 `video` 元素。
- `blob:` URL、`m3u8` / HLS、DRM 和无法识别的扩展不进入候选;转存 Worker 再以文件魔数确认 MP4/WebM。MP4/WebM 单文件上限为 200MB、最长下载等待 60 秒;图片保持 20MB 限制。
- 首期不做视频转码、封面抽帧、HLS 下载、DRM 处理、视频水印或发布。视频尺寸和时长也暂不解析;`media_asset.content_type` 是后续处理时的媒体判断依据。
下一阶段必须在正式商品编辑页实现选图后调用上述接口,并补齐来源失效、防盗链、重复点击、跨工作空间访问、AI/水印输入和发布前素材校验的集成测试;不得绕过该接口直接把来源 URL 写入正式商品媒体或发布请求。
## 6. 待优化 P1:来源站下载限流与退避
### 6.1 当前风险边界
按需物化已避免“采集一次下载全部图片”,但当前 Worker 尚未按来源平台或域名限流:单个转存任务内部逐张下载,Worker 总并发为 5;多个商品同时选图、多个 Worker 实例,或用户在测试页一次打开大量候选图片时,都可能在短时间向同一来源 CDN 发出多次请求。
- 测试页的候选图片预览由用户浏览器直接请求 `source_url`,不经过 Atlas Worker;它不会消耗 Atlas 存储,但多张图片同屏会形成来源站请求突发。
- Worker 下载会限制 HTTPS 公网地址、最多 3 次重定向、60 秒超时;图片上限 20MB、视频上限 200MB。这些是安全和资源保护,**不是来源站限流**。
- 当前 `429`、`503` 等下载失败会把候选标记为失败;不会读取 `Retry-After` 后延迟重试,因此不容易持续猛撞来源站,但会降低成功率。
- 素材仅在下载后按内容 SHA-256 去重;相同的来源 URL 被多个候选选中时,当前仍可能被重复下载。
### 6.2 正式编辑页接入前必须完成
1. 建立基于 Redis 的分布式下载限流器,限流键至少覆盖来源平台和规范化域名;初始建议每域名最多 2 个并发、每秒 2~3 个请求、短时突发不超过 4。实际阈值需按来源平台压测和运行数据调整。
2. 将 `429`、带 `Retry-After` 的响应和临时 `503` 转为可调度的延迟重试;遵守 `Retry-After`,使用指数退避与抖动,不能立即重试。
3. 对归一化后的来源 URL 建立下载任务去重:同一图片仅下载一次,完成后让多个候选关联同一 `media_asset`,而不是依赖下载后的哈希去重。
4. 对连续限流或临时不可用的来源建立熔断/降速窗口,并在操作状态中给出可理解的“来源暂时限流,稍后重试”反馈。
5. 测试页和正式采集箱详情采用懒加载、分页或虚拟列表,避免首次进入即请求全部来源预览图;来源图加载失败时保留“查看来源”链接,不能尝试携带 Cookie 或伪造 Referer 绕过防盗链。
在以上能力落地前,不允许将按需转存用于自动批量选图、全量素材预热或无人值守的大规模转存。
+36 -1
View File
@@ -6,7 +6,7 @@
这是一个全新的云端产品,不是 `ozon-seller-kit` 的本地版重构。目标用户可拥有多个门店,每个门店只能绑定一个平台;平台自身可支持多个市场、国家、语言和币种。因此语言、币种、类目和发布规则不能写死为 Ozon 或中文/俄文。
当前已完成 pnpm monorepo 初始化:包含 Web、NestJS API、BullMQ Worker、WXT 浏览器扩展、共享 API 契约、数据库包、本地基础设施 Compose 与依赖锁文件。API 已实现健康/readiness、集中配置与安全基础、认证、个人工作空间上下文、平台目录/上下文、门店、水印/素材、采集箱与扩展采集入库;Worker 已具备 Ozon 凭据验证、采集记录清理和来源素材转存消费链路,仓库同步尚未接入。Web 已具备完整前端框架(布局、路由、主题)、登录/注册页、页头平台选择器、店铺管理、水印管理、真实采集箱页面和采集数据测试页;浏览器扩展已提供毛子 ERP 风格的网页登录态同步、悬浮窗及 Ozon/1688/淘宝/天猫首版页面状态解析。不要从旧项目复制 SQLite 模型、无鉴权 API、旧 `/trial/:id` 或商品编辑页组件。可选择性复用已经验证过的纯计价公式、采集扩展思路和 AI 任务经验,但必须先适配多租户和云端边界。
当前已完成 pnpm monorepo 初始化:包含 Web、NestJS API、BullMQ Worker、WXT 浏览器扩展、共享 API 契约、数据库包、本地基础设施 Compose 与依赖锁文件。API 已实现健康/readiness、集中配置与安全基础、认证、个人工作空间上下文、平台目录/上下文、门店、水印/素材、采集箱与扩展采集入库;Worker 已具备 Ozon 凭据验证、采集记录清理和按需来源素材转存消费链路,仓库同步尚未接入。Web 已具备完整前端框架(布局、路由、主题)、登录/注册页、页头平台选择器、店铺管理、水印管理、真实采集箱页面和采集数据测试页;浏览器扩展已提供毛子 ERP 风格的网页登录态同步、悬浮窗及 Ozon/1688/淘宝/天猫首版页面状态解析。不要从旧项目复制 SQLite 模型、无鉴权 API、旧 `/trial/:id` 或商品编辑页组件。可选择性复用已经验证过的纯计价公式、采集扩展思路和 AI 任务经验,但必须先适配多租户和云端边界。
当前运行时基线:Node 24.20.0、pnpm 11.25.0、PostgreSQL 18.6、Redis 7.4.11、MinIO `RELEASE.2025-09-07T16-13-09Z`;Web 默认端口为 `8100`,API 默认端口为 `8200`,扩展 dev server 默认端口为 `8110`(仅本地开发用),Worker 无 HTTP 端口。完整目录、依赖边界与版本表见 [`architecture/project-structure.md`](../architecture/project-structure.md) 和 [`architecture/technology-and-deployment.md`](../architecture/technology-and-deployment.md)。
@@ -127,6 +127,40 @@
- **兼容性提醒:** 已有采集记录不会自动重新解析历史 `normalized_payload`。例如上面的现有记录仍标记为 `ozon-public-2.1.0`,只能验证新素材预览;需重新在来源商品页执行“编辑上架”,系统会覆盖同一采集快照,届时在规范化载荷查看新版属性与描述。
- 已通过扩展 `wxt prepare + tsc + build`、Web `vite build`、`tsx --test test/*.test.mts`(5 项,包括 Ozon 页面快照中“类型/原产国/材料”等 5 个特征和富文本描述样本)以及浏览器测试页实测。
### 1.14 开发进度(2026-09-08):重采集素材任务外键修复
- 修复重复“编辑上架”覆盖来源快照时 `collection_asset_transfers.workspace_id` 为空导致的数据库非空约束错误。根因不在新任务插入:旧的复合外键 `(workspace_id, source_variant_id) ... ON DELETE SET NULL` 在删除旧 SKU 时,会按 PostgreSQL 规则同时清空两个引用列,误将不可为空的 `workspace_id` 设为 `NULL`。
- 覆盖流程现在先将历史素材任务的可空 `source_variant_id` 显式置空,再删除旧 SKU。迁移 `014_collection_asset_variant_fk.sql` 同时把外键改为 `ON DELETE SET NULL (source_variant_id)`,从 schema 层杜绝同类删除操作再次触发该错误。已在本地数据库实际执行并核验最终约束定义。
- 该错误发生在同一数据库事务内,因此报错的旧请求会完整回滚,不会留下半成品变体、转存任务或 Outbox 事件。扩展可在 API 开发进程热重载后直接再次点击“编辑上架”。
- 已通过 API TypeScript 检查、`apps/api/test/extension.e2e.test.ts`(首次采集 → 同组 SKU 重采集 → 快照覆盖)和新增的转存任务 `workspace_id` 断言。
### 1.15 已确认设计(2026-09-08):采集图片按需物化
- **新规则取代“采集即转存”:** 采集阶段仅保存候选来源 URL、图片角色、SKU/规格关联、排序与证据;测试页直接加载来源 URL,以最快速度核对且不产生对象存储和下载费用。`media_asset` 只代表 Atlas 已验证、可稳定编辑与发布的正式素材。
- 用户选图进入编辑时建议转存;加水印可在单次任务中临时下载来源图、仅存水印结果(来源失效后不可重试);AI 改图等需要图片字节,通常先转存或在受控任务中临时下载;AI 成功输出和正式发布图片必须是 Atlas 控制的 `media_asset`。不得把来源 URL 直接提交到目标平台,也不得为下载图片读取来源 Cookie 或绕过防盗链。
- 首期**不做自动删除已选择但未发布素材**。该能力涉及引用、编辑草稿、处理中任务、恢复和误删风险,当前不值得引入。以后如需控成本,只能先考虑清理从未被任何商品、派生任务或发布快照引用的素材,并单独立项。
- 完整决策、场景表、AI/水印边界与按需物化要求见 [`media-storage-strategy.md`](../architecture/media-storage-strategy.md)。后续采集、编辑、AI、水印和发布任务开始前必须先读此文;按需物化已通过 `pending_selection` 候选状态和 `POST /api/collection-items/{id}/asset-materializations` 落地,正式商品编辑页尚未接入选图动作。
### 1.16 开发进度(2026-09-08):采集图片按需转存与来源预览
- 已新增迁移 `015_collection_asset_pending_selection.sql`、`016_collection_asset_pending_selection_default.sql`:为素材处理枚举增加 `pending_selection`,并将新候选的默认状态改为该值。枚举变更和使用它的默认值/索引刻意拆为两个迁移事务,避免 PostgreSQL 新枚举值在同一事务中立即使用的限制。
- `POST /api/extension/materials` 现在只保存来源图片候选,绝不自动创建 `collection.assets.materialize.requested` Outbox 事件或下载图片;快照覆盖会取消旧快照中 `pending_selection`、`queued`、`running` 的候选/任务。返回的 `materialization` 新增 `pending_selection` 计数。
- 已新增受 `Idempotency-Key` 保护的 `POST /api/collection-items/{id}/asset-materializations`:只能选当前快照的 `pending_selection` 候选,转为 `queued` 后创建 operation 并投递现有来源素材 Worker。商品编辑、AI、水印和发布后续均复用此入口;采集测试页不会调用它。
- `/collection-test/{collectionItemId}` 现在直接显示候选的来源 URL;已转存素材仍通过短时受控 URL 预览。页面明确区分“候选,未转存”“等待转存”“转存中”“已转存”等状态,方便核对而无需等待或消耗存储。
### 1.17 开发进度(2026-09-08):采集候选视频(首期)
- 已新增并在本地开发数据库执行 `017_collection_video_candidates.sql`:`collection_asset_transfers` 新增 `media_type`(`image` / `video`)和可选 `poster_url`。共享采集契约的素材项新增 `mediaType`(默认为 `image`)及 `posterUrl`;检查读模型和测试页可区分媒体类型。
- Ozon 仅从固定 `webGallery` / `webVideo` 状态读取视频;1688 仅从旧采集器验证过的 `gallery.fields.video|videos` 状态读取;淘宝、天猫仅从旧采集器验证过的 `item.videos` 状态读取。三者都不扫描页面 `video` 标签,因此不会将评价、推荐或广告视频误采为商品视频。
- 首期候选仅接受 HTTPS 的直接 MP4/WebM URL,并保留可用封面 URL。`blob:`、HLS (`m3u8`)、DRM 与未知格式不采集;Worker 按需转存时以 MP4/WebM 文件魔数复核,图片仍限制 20MB,视频限制 200MB。暂不实现视频转码、封面抽帧、时长/尺寸解析、视频水印或发布。
- `/collection-test/{collectionItemId}` 对未转存视频显示 `preload=none` 原生播放器,对已转存视频使用 Atlas 短时预览 URL;与图片一样,采集本身不下载也不产生存储成本。完整规则见 [`media-storage-strategy.md`](../architecture/media-storage-strategy.md)。
### 1.18 开发进度(2026-09-08):旧采集器结构化路径迁移
- Ozon 解析器已对齐旧采集器的结构化合并方案:`webDescription/webRichContent` 富文本优先,`schema.org Product.description` 为兜底;首屏 `webShortCharacteristics`、默认商品页和详情页返回的 `webCharacteristics/webDetailedCharacteristics` 都会合并去重。`ozon-public-2.3.0` 是对应版本;旧快照必须重新采集。
- 1688、淘宝、天猫已加入受限 MAIN-world 商品状态桥:1688 仅投影公开商品的画廊、SKU、价格、参数等固定字段;淘宝/天猫仅投影 `__ICE_APP_CONTEXT__` 中的公开商品结果。桥不提供任意 window 读取、Cookie、请求头或通用 fetch。新采集器将这些结构化数据、JSON-LD、受限详情 DOM 与既有图片 profile 映射为 Atlas 新 `CollectedProductPayload`。
- `/collection-test/{collectionItemId}` 现单独展示商品描述、类目路径、全部特征表、规格轴、SKU 草稿、素材候选,并保留规范化载荷和原始证据,供解析核对。扩展解析器测试覆盖 JSON-LD 描述兜底、完整特征合并和图片白名单。
## 2. 已确认产品决策
| 主题 | 决策 |
@@ -204,6 +238,7 @@ User → Workspace → Membership / Role
- [商品领域数据模型与 DDL](../architecture/product-domain-data-model.md)
- [幂等与 Transactional Outbox](../architecture/transactional-outbox-and-idempotency.md)
- [数据库角色与 RLS](../architecture/database-security.md)
- [商品采集图片与存储策略](../architecture/media-storage-strategy.md)
- [实现前待决与待落地事项](../reviews/implementation-readiness-open-items.md)
- [浏览器采集插件需求与技术方案](../requirements/collector-browser-extension.md)
- [M2.2 / M2.3 扩展采集交接](./m2.2-m2.3-extension-collection.md)
@@ -1,4 +1,4 @@
# M2.2 / M2.3:扩展授权、素材转存与 Ozon 公开页采集
# M2.2 / M2.3:扩展授权、素材候选与 Ozon 公开页采集
> 状态:已完成首个可运行闭环(2026-09-05)。Seller 网页四接口观察实验不属于本批次,仍待独立任务实施。
@@ -7,14 +7,14 @@
1. 扩展按毛子 ERP 风格复用 Atlas Web 登录态:点击登录会打开 Atlas Web;Web 登录/恢复后把当前短期 access token 写入本站 localStorage,扩展每 2 秒从该 Atlas 标签读取并保存到 extension storage。未登录时用户在该页完成登录,标签不会被扩展自动关闭。
2. 扩展以同步到的 Web Bearer token 调用会话检查和采集接口;服务端同时验证 JWT 与未撤销的 Web session,并从当前用户成员关系解析工作空间。Token 失效时插件清除缓存并重新要求从 Atlas Web 登录同步。
3. 扩展上报仅携带来源、规范化 SKU/规格轴、候选素材 URL 和证据;`POST /api/extension/materials` 从 Web Bearer 身份解析工作空间,不接受目标平台、门店、Cookie 或 API Key。非 Ozon 来源按 item key 复用;Ozon 额外按已确认 `source_sku` 集合相交复用同一采集记录。
4. 上报事务创建来源采集记录、SPU、来源确认 SKU 和素材转存任务;同一来源商品再次采集时保留记录与商品 ID,并用本次来源快照替换来源字段、SKU 与候选素材。素材异步事件经 Transactional Outbox 进入 Worker,任务带快照版本;Worker 仅处理当前版本,旧任务不得回写。Worker 仅下载 HTTPS 公网地址,限制重定向、私网地址、20MB 和 PNG/JPEG/WebP,按 SHA-256 去重后写入对象存储。单张失败不会使商品草稿丢失。
4. 上报事务创建来源采集记录、SPU、来源确认 SKU 和媒体候选(`pending_selection`);同一来源商品再次采集时保留记录与商品 ID,并用本次来源快照替换来源字段、SKU 与候选素材。采集本身不下载媒体也不写素材 Outbox。编辑流程选中候选后,`POST /api/collection-items/{id}/asset-materializations` 才将指定候选置为 `queued` 并经 Transactional Outbox 进入 Worker;任务带快照版本,Worker 仅处理当前版本,旧任务不得回写。Worker 仅下载 HTTPS 公网地址,限制重定向、私网地址;图片限制 20MB 和 PNG/JPEG/WebP,视频限制 200MB 和 MP4/WebM,均按 SHA-256 去重后写入对象存储。单张失败不会使商品草稿丢失。
5. Ozon (`ru/kz/by`) 商品详情页显示 Shadow DOM 小浮窗。未登录只显示“登录 Atlas ERP”;已登录显示“编辑上架 / 采集商品 / 进入 ERP”。商品编辑模块尚未完成时,“编辑上架”成功后打开 `/collection-test/{collectionItemId}` 供审查采集结果;采集商品只提示进入采集箱。
6. Ozon 解析器保留当前公开 SKU 作为入口证据,优先解析 `state-webAspects-*.data-state.aspects[].variants[]` 的真实变体;不会扫描整页 SKU 或把规格轴做笛卡尔积。来源未给可确认组合时,仍保留当前 SKU。
7. Ozon 多 SKU 补全通过受限 MAIN world bridge 调用同源公开页面 JSON:先读取 `modal/aspectsNew` 的 `webAspectsModal`,再以并发 3、上限 100 的队列补全 `/product/<sku>/` 中的标题、价格、画廊和关联 SKU。每个已补全 SKU 的画廊图片使用 `variant_direct` 关联;单个请求失败保留已发现 SKU 与失败证据。
8. Ozon 公开响应没有可依赖 SPU/组 ID。服务端在同一来源平台下按新旧 `source_sku` 集合相交覆盖快照;若命中多条历史记录返回 `SOURCE_VARIANT_GROUP_AMBIGUOUS`,不静默合并。
9. 图片采集沿用旧采集插件已验证的分组 profile:1688、淘宝、天猫只从主图画廊、规格选择器和商品详情容器读取图片,绝不扫描全页 `document.images`。淘宝/天猫详情图排除评论和推荐卡片;URL 会在入库前还原 CDN 原图并按归一化 URL 去重。
10. Ozon 只从 `webGallery` 读取商品画廊,`coverImage` 与 `images[].src` 归为该真实 SKU 的 `source_main + variant_direct`,不会将第二张及以后的画廊图误标为 SKU 图;`webAspects.data.coverImage` 作为 `source_sku + axis_value`。受限 bridge 额外读取固定的富文本页段来获取 `webDescription` / `webRichContent` / `webFeatures` 内的详情图,仍不接触评价或推荐组件。
11. 采集数据测试页会为转存成功且已生成 `asset_id` 的素材按 4 路并发申请短时预览 URL,直接显示缩略图,并展示其关联来源 SKU;预览 URL 不保存到业务数据。Ozon 解析器 `ozon-public-2.2.0` 现额外读取短/完整 `web*Characteristics.characteristics[]` 为公共属性,并将 `webDescription.richAnnotationJson` 转为公共描述文本、原 JSON 放入 `raw_value`。该桥接仍只允许固定 Ozon 页面段和 widget 白名单;已有旧版载荷必须重采集才会拥有这些字段。
11. 采集数据测试页对未转存候选直接加载 `source_url`;图片展示缩略图,视频采用不预加载的原生播放器,最快核对且不产生对象存储/Worker 下载费用;已经生成 `asset_id` 的历史素材才按 4 路并发申请短时预览 URL。页面会展示关联来源 SKU;预览 URL 不保存到业务数据。Ozon 解析器 `ozon-public-2.3.0` 现额外读取短/完整 `web*Characteristics.characteristics[]` 为公共属性,并将 `webDescription.richAnnotationJson` 转为公共描述文本、原 JSON 放入 `raw_value`。该桥接仍只允许固定 Ozon 页面段和 widget 白名单;已有旧版载荷必须重采集才会拥有这些字段。
## 配置与安全前提