feat: 开发采集、采集箱和商品编辑功能
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# Ozon Seller Kit V2 方案总览
|
||||
|
||||
> 状态:方案设计(待确认)
|
||||
> 最后更新:2026-08-14
|
||||
> 定位:本文是 V2 全部设计文档的入口与决策总表。先读本文,再按需读分册。
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话定位
|
||||
|
||||
V2 把 Ozon Seller Kit 从「**本地文件夹 + 单机工具**」升级为「**云端数据库 + 多店铺工作台**」:
|
||||
|
||||
```
|
||||
V1(现状) V2(目标)
|
||||
插件 ──写本地文件夹──> studio 读 插件 ──上传──> 服务端落库(采集箱)
|
||||
用户 ──> studio 工作台:看采集箱 → 编辑 → 发布
|
||||
服务端 ──> Ozon Seller API(多店铺)
|
||||
发布结果落库 → 支持 CSV 导出
|
||||
```
|
||||
|
||||
核心变化只有一条:**契约真源从「磁盘上的商品文件夹」换成「数据库 + 七牛对象存储」**。本地文件夹不再承载主流程,降级为可选的导入/导出格式。
|
||||
|
||||
这是原架构文档(`docs/architecture.md` §7)里早已规划的 **S4 阶段**:商品库落库,本地文件夹降级。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状盘点(V1 资产)
|
||||
|
||||
| 部分 | 现状 | V2 处置 |
|
||||
|---|---|---|
|
||||
| `web/` 工具台 v1 | ✅ 在用(计价/登记/水印/俄文文案),冻结 | **只读**。计价公式、水印算法、文案交互被抄进 studio,不改原文件 |
|
||||
| `extension-v1` | 1688/淘宝采集(SSR/DOM) | 保留为素材补充来源(二期) |
|
||||
| `extension-v2` | Ozon 采集,**写本地文件夹**(File System Access) | 改造成**上传服务端落库**,删除本地写盘主路径 |
|
||||
| `studio/` | React + antd,**仅「AI 图生图」一页**(wanx2.1-imageedit) | 扩为**多页工作台**:采集箱 / 商品编辑 / 发布 / 店铺 / 导出 |
|
||||
| `server/` | FastAPI,无 DB,无 Ozon 对接;有 `/api/ai/*`、`/api/image/edit` | 加 DB + 七牛 + Ozon 对接 + 鉴权 + 异步任务 |
|
||||
|
||||
现状能力与可复用清单详见 [`capability-inventory.md`](./capability-inventory.md)(V2 设计输入稿)。
|
||||
|
||||
---
|
||||
|
||||
## 3. V2 决策总表(D 系列)
|
||||
|
||||
| 编号 | 决策 | 内容 | 理由 |
|
||||
|---|---|---|---|
|
||||
| **D1** | 契约云端化 | 商品数据落 **PostgreSQL**,图片落 **七牛**;本地文件夹降级为导入/导出格式 | 多设备、多店铺、可发布、可导出,单机文件夹做不到 |
|
||||
| **D2** | 插件上传 | 插件采集结果走 `POST /api/materials` 上传落库,不再写本地 | 复用 `docs/extension/plan.md` §14 已定好的契约 |
|
||||
| **D3** | 数据库选型 | PostgreSQL 16(腾讯云 CDB)+ SQLAlchemy 2.0 + Alembic | 单库覆盖结构化字段 + JSONB(attributes/raw),运维成熟 |
|
||||
| **D4** | 图片存储 | 七牛云:源图由服务端代下转存,生成图也转存;Ozon 发布用七牛公网 URL | Ozon `images` 只收公网 URL(见 `architecture.md` §4) |
|
||||
| **D5** | 图片方案 | **方案 B(高低搭配)✅ 已拍板**:集成 ecommerce-image-suite「电商套图」+ 保留 wanx2.1-imageedit(改名「智能修图」) | 二者是不同能力、共用 DASHSCOPE Key,互补不互斥;详见 [`image-strategy.md`](./image-strategy.md) |
|
||||
| **D6** | 工作台化 | studio 从单页扩为:采集箱 → 商品编辑(计价+文案+图片+类目/属性)→ 发布 → 店铺 → 导出 | 对齐「采集 → 编辑 → 发布」主链路 |
|
||||
| **D7** | 鉴权 | MVP 用长期 Bearer Token(单用户自用);预留 `users` 表升级多用户 | 自用阶段不做 OAuth,与插件 options 页一致 |
|
||||
| **D8** | 部署 | 腾讯云:FastAPI + nginx + PostgreSQL + 七牛;studio 静态托管;插件/前端指向公网后端 | 用户明确要部署腾讯云 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据流(端到端)
|
||||
|
||||
```
|
||||
① 浏览 Ozon 竞品页(或 1688/淘宝补素材)
|
||||
│ 点插件 → 侧边栏 → 采集 → 勾选
|
||||
▼
|
||||
② 插件 POST /api/materials 上传(texts + images 的 URL + source)
|
||||
│ 服务端立即落库为「采集箱商品」,异步排队下载源图 → 转存七牛
|
||||
▼
|
||||
③ studio「采集箱」列表:查看/筛选/删除商品
|
||||
│ 进入商品编辑页
|
||||
▼
|
||||
④ 编辑:offer_id / 计价 / 俄文文案 / 图片(水印·智能修图·套图)/ 类目选择 / 属性映射
|
||||
│ 每一步落库(Draft),可随时回来继续
|
||||
▼
|
||||
⑤ 发布:绑定店铺 → 服务端组装 ImportProductsV3 items[0] → POST /v3/product/import
|
||||
│ 轮询 /v1/product/import/info 直到 imported / moderation / failed
|
||||
▼
|
||||
⑥ 落库:ozon_product_id / product_id / 状态 / 发布任务记录
|
||||
│
|
||||
▼
|
||||
⑦ CSV 导出:采集箱 + 已发布商品的字段导出(含 product_id 回填)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 分册索引
|
||||
|
||||
| 文档 | 内容 | 什么时候读 |
|
||||
|---|---|---|
|
||||
| [`architecture.md`](./architecture.md) | V2 总体架构:组件、技术栈、目录、鉴权、部署拓扑 | 先读这个,建立全局 |
|
||||
| [`database.md`](./database.md) | PostgreSQL 表结构(按 Ozon 字段 + 采集/编辑/发布/店铺维度) | 做数据层时读 |
|
||||
| [`api.md`](./api.md) | 后端 REST 接口契约(采集入库 / 商品 / 类目 / 店铺 / 发布 / 导出 / 图片 / 汇率) | 前后端联调时读 |
|
||||
| [`image-strategy.md`](./image-strategy.md) | 图片处理:方案 A/B 对比、推荐、七牛存储、套图集成方式 | 图片这块没想明白时读 |
|
||||
| [`ozon-publish.md`](./ozon-publish.md) | Ozon Seller API 集成:鉴权、店铺绑定、类目/属性、发布、任务状态、CSV 导出字段 | 做发布链路时读 |
|
||||
| [`migration.md`](./migration.md) | 分阶段落地计划、改动点清单、风险 | 开工前读 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 与 V1 文档的关系
|
||||
|
||||
- `docs/architecture.md`(V1 总架构)仍有效,V2 是其 **S4 阶段**的具体化;S1–S3 的结论(后端收进 `server/`、契约对齐 ImportProductsV3、`_` 前缀剥离、图床公网 URL 硬约束)全部沿用。
|
||||
- `docs/contracts/product-json.md` 的**字段结构**在 V2 成为 `products` 表 + `product_assets` 表的设计蓝本;「文件夹」语义换成「商品记录」。
|
||||
- `docs/extension/plan.md` §9/§13/§14 的**消息层、`/api/materials` 契约、鉴权、重试队列**在 V2 原样采纳,只把「写文件夹」换成「上传落库」。
|
||||
- 若存在分歧,以 `docs/v2/` 为准。
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键风险(先立 flag,细节见 migration.md)
|
||||
|
||||
| 风险 | 级别 | 对策 |
|
||||
|---|---|---|
|
||||
| Ozon 类目/属性字典大且需实时性 | 🟡 中 | 服务端缓存 + 按类目按需拉取,见 [`ozon-publish.md`](./ozon-publish.md) §3 |
|
||||
| 采集属性 → Ozon 属性 id 的映射工作量大 | 🔴 高 | 自动匹配 + 人工确认 UI,见 `ozon-publish.md` §4 |
|
||||
| ecommerce-image-suite 是「脚本+Skill」形态,非服务 | 🟡 中 | 把 generate.py 的 prompt 引擎抽成服务端能力,见 `image-strategy.md` §5 |
|
||||
| 发布是异步(task_id 轮询),用户不能干等 | 🟡 中 | 任务表 + 轮询 + 状态回显,见 `ozon-publish.md` §5 |
|
||||
| 密钥落库(店铺 Client-Id/Api-Key) | 🔴 高 | 服务端 AES 加密存储,前端永不回显明文,见 `database.md` §2.6 |
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
# V2 后端 API 设计
|
||||
|
||||
> 状态:方案设计(待确认)
|
||||
> 上游:[V2 总览](./README.md) · [V2 架构](./architecture.md) · [数据库](./database.md)
|
||||
|
||||
统一约定:
|
||||
- 前缀 `/api`;鉴权 `Authorization: Bearer <JWT>`(除 `/auth/login`、`/health` 外)。
|
||||
- 错误:FastAPI 语义状态码;`detail` 为可读中文;校验错误 `422`。
|
||||
- 列表分页:`?page=&page_size=` 或 `?limit=&last_id=`(Ozon 风格,仅对代理 Ozon 的接口)。
|
||||
- 时间一律 ISO 8601 UTC。
|
||||
|
||||
---
|
||||
|
||||
## 1. 鉴权 `/api/auth`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| POST | `/auth/login` | `{token}` → `{access_token, expires_at}`。MVP:token 等于 `.env` 的 `APP_TOKEN`;升级后:用户名密码 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 采集入库 `/api/materials`(插件 → 服务端)
|
||||
|
||||
沿用 `docs/extension/plan.md` §14 的契约,**把「文件夹」换成「商品」**。
|
||||
|
||||
### `POST /api/materials` ★ 主接口
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"product": { // 对应「文件夹」;首次上传可空=新建
|
||||
"id": "uuid-or-null" // 传了=追加到已有商品(跨平台补素材)
|
||||
},
|
||||
"source": {
|
||||
"platform": "ozon", // ozon | 1688 | taobao
|
||||
"itemId": "123456789",
|
||||
"url": "https://www.ozon.ru/product/…",
|
||||
"collectedAt": 1710000000000
|
||||
},
|
||||
"texts": [
|
||||
{ "kind": "title", "content": "…" },
|
||||
{ "kind": "params", "content": "…", "pairs": [{ "key": "…", "value": "…" }] }
|
||||
],
|
||||
"images": [
|
||||
{ "groupKey": "main", "groupName": "主图", "variantName": null,
|
||||
"url": "https://…原图…", "index": 0, "type": "img", "dedupeKey": "…" }
|
||||
],
|
||||
"refererOrigin": "https://www.ozon.ru" // 该站图片下载需带的 Referer
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```jsonc
|
||||
{ "product": { "id": "…", "stage": "collected" }, "assetsQueued": 12 }
|
||||
```
|
||||
|
||||
**语义**:服务端立即落库(product + texts 进 raw + assets 置 `pending`),**响应不等图片下载完成**;后台协程按 `refererOrigin` 下载源图 → 转存七牛 → 更新 asset 状态。插件无需等待。
|
||||
|
||||
### 其余
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| POST | `/materials/bytes` | 字节兜底:`multipart/form-data`(`product_id` + `meta`(JSON) + `file`),供源图带登录态、插件在页面上下文抓字节上传 |
|
||||
| GET | `/products/:id/fingerprints` | 跨页去重:返回已采集 `dedupeKey` 列表 |
|
||||
| GET | `/collected?platform=&itemId=` | 状态回显:`{collected, count}`(页面徽标用) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 商品 `/api/products`
|
||||
|
||||
### 列表(采集箱 / 发布列表)
|
||||
|
||||
`GET /api/products?stage=&q=&page=&page_size=`
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"total": 128,
|
||||
"items": [
|
||||
{
|
||||
"id": "…", "stage": "collected",
|
||||
"name": "…", "offer_id": "", "price": null,
|
||||
"source_platform": "ozon", "source_url": "…",
|
||||
"asset_count": { "main": 6, "sku": 4, "detail": 9 },
|
||||
"ozon_product_id": null,
|
||||
"created_at": "…", "updated_at": "…"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 详情 / 编辑 / 删除
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/products/:id` | 完整商品(含 raw / pricing / copy / attributes / assets) |
|
||||
| PATCH | `/products/:id` | 部分更新(编辑页 autosave)。可改字段见 database.md §2.3 |
|
||||
| POST | `/products` | 手动新建商品(不经过插件) |
|
||||
| DELETE | `/products/:id` | 软删(stage→archived)或硬删(采集箱未发布项) |
|
||||
| POST | `/products/:id/stage` | `{stage}` 流转(editing→ready 前校验必填项) |
|
||||
|
||||
> 编辑页的计价、文案、图片、类目/属性都是「编辑 `products` 的某几列」,统一走 `PATCH /products/:id` 或细分子资源(见 §6–§8),不新增独立存储。
|
||||
|
||||
---
|
||||
|
||||
## 4. 汇率 `/api/fx`
|
||||
|
||||
`GET /api/fx` → `{ "rate": 11.84, "source": "cbr", "updated_at": "…" }`
|
||||
|
||||
服务端抓取(FloatRates → 俄央行 → er-api 三级降级,沿用 v1 数据源但**移到服务端**),缓存 + 脏数据过滤(5~25 区间)。前端计价用它,也可在计价时快照进 `products.fx_rate`。
|
||||
|
||||
---
|
||||
|
||||
## 5. AI 文案 `/api/ai`(沿用现有,零改动)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/ai/models` | 模型列表(models.yaml) |
|
||||
| POST | `/ai/copy` | 中文采买信息 → 俄文标题/描述/标签 + 中文对照 |
|
||||
|
||||
V2 编辑页的「文案」面板直接消费这两个接口,生成结果写入 `products.copy` + `products.name/description`(用户确认后)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 图片 `/api/image`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| POST | `/image/edit` | **智能修图**(wanx2.1-imageedit,改名,沿用现有)——单图换背景/去水印/局部重绘/加文字 |
|
||||
| POST | `/image/suite` | **电商套图**(集成 ecommerce-image-suite,新增)——原图 + 卖点 → 套图 |
|
||||
| POST | `/image/upload-token` | 获取七牛直传 token(若走前端直传;一期可省,走服务端中转) |
|
||||
|
||||
`/image/edit` 与 `/image/suite` 的结果图由服务端**下载 → 转存七牛 → 返回七牛 URL**(改造现状 image_edit.py 已预留的扩展点)。
|
||||
|
||||
详见 [`image-strategy.md`](./image-strategy.md)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 店铺 `/api/shops`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/shops` | 列表(脱敏:不返回 key) |
|
||||
| POST | `/shops` | `{name, client_id, api_key, currency_code}` 绑定(加密落库) |
|
||||
| PATCH | `/shops/:id` | 更新(可只更新 name/currency,或换 key) |
|
||||
| DELETE | `/shops/:id` | 删除(级联校验是否有进行中发布) |
|
||||
| POST | `/shops/:id/test` | **连通性校验**:用该店铺凭证调 Ozon `/v1/roles`,成功→`{ok:true, roles:[…]}`,失败→`status=invalid` 并返回原因 |
|
||||
|
||||
> `test` 用 `/v1/roles`(返回该 key 的角色与方法权限),既验凭证又验权限范围,成本为零。
|
||||
|
||||
---
|
||||
|
||||
## 8. 类目与属性 `/api/categories`(服务端代理 Ozon)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/categories/tree?lang=RU` | 类目树(代理 `/v1/description-category/tree`,服务端缓存) |
|
||||
| GET | `/categories/:category_id/attributes?type_id=` | 属性列表(代理 `/v1/description-category/attribute`) |
|
||||
| GET | `/categories/attribute/:attribute_id/values?category_id=&type_id=&q=` | 属性值字典(代理 `/values` 或 `/values/search` 按需) |
|
||||
|
||||
> 这些接口需要店铺凭证(Client-Id/Api-Key)。请求体里带 `shop_id`,服务端用对应店铺凭证调 Ozon。类目树可全局缓存(与店铺无关);属性/值按类目缓存。详见 `ozon-publish.md` §3。
|
||||
|
||||
---
|
||||
|
||||
## 9. 发布 `/api/publish`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| POST | `/products/:id/publish` | `{shop_id}` 发布:组装 items[0] → `/v3/product/import` → 建 `publish_tasks` → 返回 `{task_id}` |
|
||||
| GET | `/publish/:taskId` | 查询发布任务状态(服务端已轮询回写,直接读库) |
|
||||
| GET | `/products/:id/publish-history` | 该商品历史发布记录 |
|
||||
|
||||
**发布请求体组装**(服务端职责,见 `ozon-publish.md` §5):
|
||||
1. 取 `products` 平铺字段 + `attributes`/`complex_attributes`;
|
||||
2. 剥离 `_` 前缀扩展字段(DB 里已天然分层,无需剥离);
|
||||
3. 校验必填(name/description/category/尺寸重量/offer_id/images);
|
||||
4. `POST /v3/product/import`(头 `Client-Id`/`Api-Key`)→ 得 `task_id`;
|
||||
5. 后台轮询 `POST /v1/product/import/info` → 回写 `products.stage` + `ozon_product_id`。
|
||||
|
||||
---
|
||||
|
||||
## 10. CSV 导出 `/api/export`
|
||||
|
||||
`GET /api/export/products.csv?stage=&ids=` → 流式返回带 BOM 的 UTF-8 CSV。
|
||||
|
||||
字段(默认全量,`fields=` 可指定子集):
|
||||
|
||||
```
|
||||
offer_id, product_id, name, description_category_id, price, old_price,
|
||||
currency_code, vat, weight, weight_unit, depth, width, height, dimension_unit,
|
||||
barcode, primary_image, images(join "|"), source_platform, source_item_id, source_url,
|
||||
stage, published_at, created_at
|
||||
```
|
||||
|
||||
已发布商品含 `product_id`;未发布留空。支持按 `stage`(collected/published/全部)与 `ids`(勾选导出)筛选。CSV 字段明细与公式见 [`ozon-publish.md`](./ozon-publish.md) §6。
|
||||
|
||||
---
|
||||
|
||||
## 11. 插件运维接口(沿用 plan.md §14)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/ext/profiles` | 远程采集配置下发(选择器热更) |
|
||||
| POST | `/ext/logs` | 埋点批量上报(抗改版看板) |
|
||||
|
||||
---
|
||||
|
||||
## 12. 接口 → 现有代码映射(改造量)
|
||||
|
||||
| V2 接口 | 现状 | 改造 |
|
||||
|---|---|---|
|
||||
| `/auth/*` | 无 | 新建 |
|
||||
| `/materials` 系列 | 无(plan 里有设计) | 新建 |
|
||||
| `/products` 系列 | 无 | 新建 |
|
||||
| `/fx` | 无(v1 前端直连第三方) | 新建(搬 v1 数据源到服务端) |
|
||||
| `/ai/*` | ✅ 有 | 复用 |
|
||||
| `/image/edit` | ✅ 有 | 复用 + 加七牛转存 |
|
||||
| `/image/suite` | 无 | 新建(集成 ecommerce-image-suite) |
|
||||
| `/shops`、`/categories`、`/publish`、`/export` | 无(`api/ozon.py` 占位) | 新建 |
|
||||
| `/ext/*` | 无 | 新建 |
|
||||
@@ -0,0 +1,182 @@
|
||||
# V2 总体架构
|
||||
|
||||
> 状态:方案设计(待确认)
|
||||
> 上游:[V2 总览](./README.md) · V1 [`architecture.md`](../architecture.md)
|
||||
> 前置阅读:建议先读 [`capability-inventory.md`](./capability-inventory.md) 了解现状资产。
|
||||
|
||||
---
|
||||
|
||||
## 1. 组件与职责
|
||||
|
||||
```
|
||||
┌───────────────┐ Bearer Token ┌──────────────────────────────────────────────┐
|
||||
│ Chrome 插件 │ ────────────────▶ │ 服务端 server/(FastAPI) │
|
||||
│ extension-v2 │ POST /api/materials│ │
|
||||
│ (Ozon/1688 采集)│ │ ├─ api/ 采集/商品/类目/店铺/发布/图片/汇率 │
|
||||
└───────────────┘ │ ├─ services/ Ozon / DeepSeek / 套图 / 七牛 │
|
||||
▲ │ ├─ models/ SQLAlchemy ORM + Alembic │
|
||||
│ 远程配置 / 埋点 │ └─ jobs/ 异步:图下载转存 / 发布轮询 │
|
||||
┌───────┴───────┐ │ │
|
||||
│ 采集配置文件 │ └──────┬───────────────┬───────────────┬────────┘
|
||||
│(可热更) │ │ │ │
|
||||
└───────────────┘ ┌──────────▼──┐ ┌───────▼────────┐ ┌─────▼─────────┐
|
||||
│ PostgreSQL │ │ 七牛对象存储 │ │ Ozon Seller API│
|
||||
│ 商品/店铺/任务│ │ 源图/生成图/水印 │ │ Client-Id+Api-Key│
|
||||
└─────────────┘ └────────────────┘ └───────────────┘
|
||||
▲
|
||||
│ REST /api/*
|
||||
┌─────────────┴──────────────┐
|
||||
│ studio/(React + Vite + antd)│
|
||||
│ 采集箱 / 商品编辑 / 发布 / 店铺 / 导出 │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
四个部分与 V1 一致(插件 / studio / server / 采集配置),但 **studio 的职责显著扩大**(从单页图生图 → 完整工作台),**server 从无状态代理 → 有状态业务中枢**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心边界(延续 V1,补充 V2)
|
||||
|
||||
1. **密钥只放服务端**:DeepSeek / DASHSCOPE / 七牛 / Ozon 店铺 Client-Id+Api-Key 全部只在 server 侧;插件和 studio 只持有一个长期 Bearer Token。
|
||||
2. **插件仍做纯采集**:不调 LLM、不做图片处理、不碰 Ozon API;只是把「写本地文件夹」换成「上传落库」。
|
||||
3. **服务端是唯一出网口(对 Ozon/云厂商)**:插件 background 与服务端通信,studio 与服务端通信;谁都不直连 Ozon。
|
||||
4. **商品数据单点真源 = `products` 表**:`_` 前缀的本地扩展字段(`_raw`/`_pricing`/`_images`)仍保留在 JSONB 里,提交 Ozon 前按 V1 契约剥离。
|
||||
5. **图片一律七牛公网 URL**:数据库里存七牛 URL,不存本地路径、不存源站 URL(源站 URL 仅存 `product_assets.source_url` 做溯源)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 技术栈
|
||||
|
||||
| 部分 | 技术栈 | 说明 |
|
||||
|---|---|---|
|
||||
| server | FastAPI + SQLAlchemy 2.0(async)+ Alembic + httpx | 沿用现状 FastAPI;ORM 用 SQLAlchemy 2.0 async |
|
||||
| DB | PostgreSQL 16(腾讯云 CDB) | JSONB 存 attributes/raw/pricing |
|
||||
| 任务 | 轻量:先 DB 轮询 + asyncio 后台任务;量大再上 Redis/Celery | 图下载转存、发布轮询都是 IO 密集 |
|
||||
| 对象存储 | 七牛云 Kodo | 源图转存 + 生成图 + 水印结果 |
|
||||
| 缓存 | 类目/属性字典 → PostgreSQL 表 + 内存 LRU;可选 Redis | 见 `ozon-publish.md` §3 |
|
||||
| studio | React 19 + Vite 7 + antd 6 + react-router 7 | 沿用现状;加 react-query 或 zustand 管状态 |
|
||||
| extension | WXT + React + TS | 沿用;改造 export → upload |
|
||||
| 鉴权 | JWT(短期)+ Bearer Token;MVP 单用户,预留 `users` 表 | 见 §6 |
|
||||
|
||||
**与 V1 的差异**:唯一新增重依赖是 **SQLAlchemy + Alembic** 和 **七牛 SDK(qiniu)**。任务队列一期不引入 Redis/Celery,用「DB 状态机 + 后台协程」即可(单人自用规模)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 目录结构(目标)
|
||||
|
||||
```
|
||||
ozon-seller-kit/
|
||||
├── server/
|
||||
│ ├── main.py # 应用入口,挂载路由 + studio 静态
|
||||
│ ├── api/ # 按域拆:collection / products / categories /
|
||||
│ │ │ # shops / publish / export / image / ai / fx / auth
|
||||
│ ├── services/ # ozon_client / deepseek / image_suite / qiniu / pricing
|
||||
│ ├── models/ # SQLAlchemy 模型(见 database.md)
|
||||
│ ├── schemas/ # Pydantic(接口契约真源)
|
||||
│ ├── jobs/ # 后台协程:下载转存 / 发布轮询
|
||||
│ ├── migrations/ # Alembic
|
||||
│ └── config/ # settings.py + models.yaml(沿用)
|
||||
├── studio/ # 工作台(多页)
|
||||
│ └── src/
|
||||
│ ├── pages/
|
||||
│ │ ├── collection/ # 采集箱列表
|
||||
│ │ ├── product/ # 商品编辑(核心)
|
||||
│ │ │ └── components/ # PricingPanel / CopyPanel / ImagePanel /
|
||||
│ │ │ # CategoryPicker / AttributeMapper / PublishPanel
|
||||
│ │ ├── publish/ # 发布任务 / 状态
|
||||
│ │ ├── shops/ # 店铺管理(Client-Id / Api-Key)
|
||||
│ │ ├── export/ # CSV 导出
|
||||
│ │ └── ai-image/ # 保留:智能修图(wanx2.1-imageedit,改名)
|
||||
│ ├── services/ # 与 /api/* 对齐的客户端
|
||||
│ ├── stores/ # zustand:商品编辑态 / 采集箱筛选
|
||||
│ └── pricing/ # 从 v1 抄来的计价纯函数(不改原文件)
|
||||
├── extension-v2/ # 采集插件(改造:上传落库)
|
||||
│ └── src/
|
||||
│ ├── messaging/ # 消息层(plan.md §9)
|
||||
│ ├── api/ # 后端客户端(仅 background)
|
||||
│ └── collector/ profiles/ # 沿用采集引擎
|
||||
├── web/ # v1 工具台,冻结
|
||||
├── docs/
|
||||
│ ├── v2/ # ★ 本文档集
|
||||
│ └── ...(V1 文档)
|
||||
└── .env / .env.example
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态机:商品生命周期
|
||||
|
||||
V1 是 `collected → edited → published`。V2 因为「落库 + 异步发布」,扩展为:
|
||||
|
||||
```
|
||||
collected ──(进入编辑)──> editing ──(填写完整)──> ready ──(点发布)──> publishing
|
||||
│
|
||||
┌───────────────────────────────────────┤
|
||||
▼ ▼
|
||||
imported(成功) failed(失败,可改后重发)
|
||||
│ ▲
|
||||
└────── published ──(可归档)──> archived ─┘
|
||||
|
||||
collected 插件刚上传,只有素材与原文
|
||||
editing 用户正在编辑(计价/文案/图片/类目)
|
||||
ready 必填项齐全,可发布
|
||||
publishing 已提交 ImportProductsV3,拿到 task_id,等待轮询
|
||||
published 轮询 imported 成功,回填 product_id
|
||||
failed 轮询返回 errors / 校验失败;可回到 editing 修复后重发
|
||||
archived 手动归档(软删)
|
||||
```
|
||||
|
||||
- 每步都落库,刷新/换设备不丢。
|
||||
- `publishing` 由发布任务表(`publish_tasks`)驱动,服务端轮询 `/v1/product/import/info` 更新状态。
|
||||
- 状态定义详见 [`database.md`](./database.md) §2.2。
|
||||
|
||||
---
|
||||
|
||||
## 6. 鉴权与多租户
|
||||
|
||||
**MVP(单人自用)**:`.env` 里配一个 `APP_TOKEN`,插件 options 页和 studio 登录页填同一个值,请求头 `Authorization: Bearer <APP_TOKEN>`。服务端校验后签发短期 JWT,后续请求用 JWT。
|
||||
|
||||
**预留升级路径(不影响 MVP)**:`users` 表 + `shops.user_id` 已留好外键,未来要做多用户 SaaS 只需补注册/登录 + 按 `user_id` 过滤查询,schema 不用改。
|
||||
|
||||
| 层 | MVP | 升级 |
|
||||
|---|---|---|
|
||||
| 身份 | 单个 `APP_TOKEN` | `users` 表 + 密码哈希 |
|
||||
| 会话 | 短期 JWT(`Authorization: Bearer`) | 同左,加刷新令牌 |
|
||||
| 店铺归属 | 全部归当前用户 | 按 `user_id` 隔离 |
|
||||
| 密钥保护 | 店铺 Api-Key 服务端 AES-GCM 加密落库 | 同左 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 部署拓扑(腾讯云)
|
||||
|
||||
```
|
||||
┌────────────── nginx (443) ──────────────┐
|
||||
│ /api/* → uvicorn (127.0.0.1:8800) │
|
||||
浏览器/插件 ─────▶ │ / → studio 静态资源(构建产物)│
|
||||
│ /ozonSeller.html → web/(v1,可选保留) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
▼ ▼ ▼
|
||||
PostgreSQL(CDB) 七牛 Kodo(对象存储) 外部 API(Ozon/DeepSeek/DashScope)
|
||||
```
|
||||
|
||||
- **单进程部署**:FastAPI 同源托管 studio 构建产物(与 V1 托管 web/ 同思路),`/api` 走 nginx 反代到 uvicorn。
|
||||
- **环境变量**:`.env` 在服务器上维护(不入 git),新增 `APP_TOKEN` / `DATABASE_URL` / `QINIU_*` / `APP_BASE_URL`。
|
||||
- **CORS**:同源托管时 `CORS_ORIGINS` 留空;开发期 studio 跑 8900 时用 Vite 代理 `/api`,无需 CORS。
|
||||
- 详见 [`migration.md`](./migration.md) §5。
|
||||
|
||||
---
|
||||
|
||||
## 8. 与 V1 的差异小结
|
||||
|
||||
| 维度 | V1 | V2 |
|
||||
|---|---|---|
|
||||
| 契约真源 | 磁盘「商品文件夹」 | `products` 表 + 七牛 |
|
||||
| 插件出口 | File System Access 写盘 | `POST /api/materials` 落库 |
|
||||
| studio | 单页图生图 | 多页工作台 |
|
||||
| server | 无状态代理(ai/image) | 有状态业务中枢(DB/七牛/Ozon/任务) |
|
||||
| 发布 | 预留 `/api/ozon/*` 占位 | 完整发布链路 + 任务轮询 |
|
||||
| 图片 | 图生图(wanx2.1)+ 前端水印 | 智能修图 + 电商套图 + 七牛托管 |
|
||||
| 数据导出 | v1 登记表 CSV(前端) | 服务端统一 CSV 导出 |
|
||||
| 部署 | 本机 127.0.0.1 | 腾讯云公网 |
|
||||
@@ -0,0 +1,218 @@
|
||||
# Ozon Seller Kit 现有能力清单(V2 方案设计输入)
|
||||
|
||||
> 依据代码逐文件核对生成(server/ 与 studio/,忽略 node_modules/.venv/__pycache__/.output)。
|
||||
> 数据来源文件:`ozon-seller-kit/server/**` 与 `ozon-seller-kit/studio/src/**`,全部行号以当前工作区为准。
|
||||
|
||||
---
|
||||
|
||||
## 一、后端 API 清单
|
||||
|
||||
FastAPI 应用入口 `server/main.py`:
|
||||
|
||||
- 应用名 `Ozon Seller Kit` v0.1.0(main.py:13)。
|
||||
- CORS 中间件:仅当 `cors_origin_list` 非空时启用,`allow_credentials=True`,方法/请求头全放行(main.py:16-23)。
|
||||
- 挂载三个路由:`api/ai`、`api/image`、`api/ozon`(main.py:25-27)。
|
||||
- `GET /api/health` → `{"status":"ok"}`(main.py:30-32)。
|
||||
- 若仓库根 `web/`(v1 工具台,冻结)存在,则 `app.mount("/", StaticFiles(html=True))` 静态托管(main.py:11, 35-36)——即生产形态下 FastAPI 同源托管前端。
|
||||
|
||||
| 方法 | 路径 | 功能 | 关键入参 | 关键出参 |
|
||||
|---|---|---|---|---|
|
||||
| GET | `/api/health` | 健康检查(main.py:30-32) | 无 | `{status: "ok"}` |
|
||||
| GET | `/api/ai/models` | 列出可用模型(ai.py:10-12 → models_catalog.list_model_options) | 无 | `{default: str, models: [{id, label}]}` |
|
||||
| POST | `/api/ai/copy` | 生成 Ozon 俄文商品文案(ai.py:15-17 → deepseek.generate_copy) | `CopyRequest`:`source_text`(≥10字符)、`product_name`、`model_code`、`model`(可选) | `CopyResponse`:`titles_ru/zh[2]`、`description_ru/zh`、`tags_ru/zh`、`model`、`usage{prompt_tokens,completion_tokens}` |
|
||||
| POST | `/api/image/edit` | AI 图生图/图像编辑(image.py:9-11 → image_edit.edit_image) | `ImageEditRequest`:`base_image`(dataURL/公网URL)、`prompt`、`model`(白名单)、`mask_image?`、`function?`、`n`(1-4)、`size?`、`seed?`、`style?`、`prompt_extend`、`strength?` | `ImageEditResponse`:`task_id`、`results[{url(24h), image_base64(dataURL)}]`、`image_count`、`request_id` |
|
||||
| — | `/api/ozon/*` | **空占位**(ozon.py:3-5):仅定义前缀与注释 `# Phase 3: Ozon Seller API product upload`,无任何端点 | — | — |
|
||||
|
||||
前端调用面:`studio/src/services/image.ts` 只调 `/api/image/edit`;`/api/ai/*` 目前只有 v1 工具台 `web/js/ai-copy.js`(233 行 models、299 行 copy,用原生 fetch)在消费。
|
||||
|
||||
---
|
||||
|
||||
## 二、配置与模型目录机制
|
||||
|
||||
### 2.1 环境变量(server/config/settings.py + 仓库根 `.env`)
|
||||
|
||||
- `load_dotenv` 与 pydantic-settings 均指向仓库根 `.env`(settings.py:8-9, 16-17,`parents[2]` 上跳两级)。
|
||||
- 字段(settings.py:21-29):
|
||||
- `deepseek_api_key` / `openai_api_key` / `dashscope_api_key`
|
||||
- `dashscope_base_http_api_url`(华北2北京业务空间专用,普通 API Key 留空)
|
||||
- `host=127.0.0.1`、`port=8800`(uvicorn 启动参数)
|
||||
- `cors_origins`(逗号分隔)→ 属性 `cors_origin_list`(settings.py:31-35)
|
||||
- `get_settings()` 带 `@lru_cache`(settings.py:38-40)。
|
||||
- `.env.example` 给出全部变量名:`DEEPSEEK_API_KEY`、`DASHSCOPE_API_KEY`、`DASHSCOPE_BASE_HTTP_API_URL`、`HOST`、`PORT`、`CORS_ORIGINS`。
|
||||
|
||||
### 2.2 模型目录(server/config/models.yaml + services/models_catalog.py)
|
||||
|
||||
- YAML 结构:`default`(默认模型 id)+ `models[]`,每项含 `id/label/provider/api_model/base_url/api_key_env/max_tokens/params`。
|
||||
- 密钥**不写入 yaml**,只引用环境变量名(`api_key_env: DEEPSEEK_API_KEY`)。
|
||||
- 当前仅两个 deepseek 模型(deepseek-v4-flash 默认、deepseek-v4-pro),均 `base_url=https://api.deepseek.com`、`max_tokens=4000`、`params.thinking.type=disabled`(关闭思维链,防止推理耗尽 token 正文为空)。
|
||||
- `models_catalog.py`:
|
||||
- `ModelSpec` Pydantic 模型(16-25 行),`params` 为任意 dict、直接并入请求体。
|
||||
- `load_models_file()` 带 `@lru_cache`(43-54 行),校验 default 在列表中、列表非空;文件缺失抛 RuntimeError。
|
||||
- `list_model_options()`(57-62 行)→ 前端下拉用 `{id,label}`。
|
||||
- `get_model_spec(model_id)`(65-74 行):空值回落 default,未知 id 抛 400。
|
||||
- `resolve_api_key(spec)`(77-83 行):按 `api_key_env` 读环境变量,缺失抛 500。
|
||||
- 设计要点:**模型即配置**——新增模型只需改 yaml + 加环境变量,代码零改动(deepseek 类);这是 V2 可直接继承的机制。
|
||||
|
||||
### 2.3 依赖(server/requirements.txt)
|
||||
|
||||
`fastapi>=0.115`、`uvicorn[standard]>=0.32`、`httpx>=0.27`、`pydantic-settings>=2.6`、`python-dotenv`、`PyYAML`、`dashscope>=1.23.8`。
|
||||
|
||||
---
|
||||
|
||||
## 三、AI 文案服务细节(services/deepseek.py + prompts/copy_ru.py)
|
||||
|
||||
### 3.1 调用链
|
||||
|
||||
`POST /api/ai/copy` → `generate_copy(req)`(deepseek.py:144-181):
|
||||
|
||||
1. `get_model_spec(req.model)` 取模型规格,未传用默认。
|
||||
2. messages = system(`SYSTEM_PROMPT`) + user(`build_user_prompt(...)`),system 完整文本见 copy_ru.py:1-65。
|
||||
3. `_chat_once(spec, messages)`(deepseek.py:93-141):
|
||||
- URL = `base_url + /chat/completions`;Bearer 认证。
|
||||
- payload:`model=api_model`、`temperature=0.45`(事实稳定、营销留少量变化)、`max_tokens`、`response_format={type:"json_object"}`、`**spec.params`。
|
||||
- httpx 超时 90s;网络错误/HTTP≥400 一律 502(detail 截断 500 字符)。
|
||||
- 解析 `body["choices"][0]["message"]["content"]`;若 `finish_reason=="length"` 且正文为空 → 502 并提示调 max_tokens 或关思维链(131-138 行,对应 yaml 中 thinking disabled 的注释)。
|
||||
4. **重试机制**(159-177 行):最多 2 次;解析失败时把上一次输出以 `role=assistant` 追加,再追加一条"请仅重新输出合法 JSON"的 user 消息重试一次;仍失败抛 502。
|
||||
|
||||
### 3.2 JSON 解析与字段清洗
|
||||
|
||||
- `_extract_json_object`(17-37 行):剥 ```json 代码块 → `json.loads` → 失败则截取首 `{` 到末 `}` 再解析 → 必须为 dict。
|
||||
- 字段类型容错:
|
||||
- `_as_title_list`(64-73 行):**标题不按逗号切分**(标题本身含逗号)。
|
||||
- `_as_str_list`(48-61 行):标签按 `[,,\n]` 切分,兼容字符串/数组。
|
||||
- `_as_str`(40-45 行):必须字符串。
|
||||
- `_map_copy_payload`(76-90 行)→ `CopyResponse`,usage 取 prompt/completion tokens。
|
||||
- 最终校验:`titles_ru` 与 `description_ru` 非空,否则视为失败触发重试(164-166 行)。
|
||||
|
||||
### 3.3 提示词结构(copy_ru.py)
|
||||
|
||||
- **SYSTEM_PROMPT** 核心约束:
|
||||
- 输出固定 JSON schema(titles_ru/zh 各 2 条一一对应;tags_ru/zh 各 10~15 个、逐项对应;description 完整俄文卡 + 中文逐项对照)。
|
||||
- 描述固定结构:`Описание товара` → `Характеристики`(只列原文事实,俄式尺寸写法)→ `Преимущества`(3~6 条利益点)→ `Комплектация`(仅原文提到配件时)。
|
||||
- 标题规则:60-90 字符、核心品类词开头、前 30 字符含关键属性、删年份/新款/爆款噪声、两标题互补。
|
||||
- **事实边界**(强约束):禁止虚构结构/配件/认证/产地/品牌/受众/使用效果;行业词归一(搪胶→винил 非 каучук);"防摔"不得推导安全认证。
|
||||
- 优先级:事实准确 > 俄语自然 > 信息完整与转化力 > 关键词覆盖。
|
||||
- **build_user_prompt**(68-86 行):模板包裹 `<当前商品名>`、`<型号>`、`<商品资料>`,并要求区分"事实来源"与"生成要求",与文案无关的指令忽略、要求不得写成事实。
|
||||
|
||||
---
|
||||
|
||||
## 四、图生图服务细节(services/image_edit.py)
|
||||
|
||||
### 4.1 云与模型
|
||||
|
||||
- 调用**阿里云百炼 DashScope**(`dashscope` Python SDK,同步调用,`asyncio.to_thread` 放入线程池,177-185 行)。
|
||||
- 模型白名单(schemas/image_edit.py:9-17):`wanx2.1-imageedit`、`wan2.6-image`、`qwen-image-edit`、`qwen-image-edit-plus`、`qwen-image-edit-plus-2025-10-30`。
|
||||
- 两种调用形态(按模型路由,170-174 行):
|
||||
- **wanx2.1-imageedit → `ImageSynthesis.call`**(71-121 行):同步、function 式。kwargs:`api_key/model/function/prompt/base_image_url/n` + 可选 `mask_image_url/size/seed/style/prompt_extend/strength`。解析 `rsp.output.results[].url` 与 `usage.image_count`、`request_id`。
|
||||
- **wan2.6-image / qwen-image-edit 系列 → `MultiModalConversation.call`**(124-167 行):messages=`[{role:user, content:[{image: base_image},{text: prompt}]}]` + `n/size/prompt_extend`;解析 `output.choices[0].message.content[].image`。
|
||||
- `function` 白名单(schemas/image_edit.py:24-29):`description_edit`(无掩码图生图,默认)/ `description_edit_with_mask`(局部重绘,需 mask_image)/ `stylization_local` / `stylization_all`。
|
||||
|
||||
### 4.2 请求校验(schemas/image_edit.py)
|
||||
|
||||
- `base_image` / `mask_image`:data URL(`data:image/...`)或公网 http(s) URL;data URL 编码后 ≤ 15MB(`_MAX_BASE64_LENGTH`,32 行);mask 可空。
|
||||
- `prompt` 非空;`model` 白名单校验;`n` 1~4;`strength` 0.0~1.0(默认 0.5,加文字/大改建议 0.8);`prompt_extend` 默认 True。
|
||||
|
||||
### 4.3 关键工程决策
|
||||
|
||||
- **服务端代理下载 → data URL**(`_download_to_data_url`,29-48 行):阿里云结果 URL 未开放 CORS,前端直接绘 canvas 会污染画布无法导出;服务端下载后转 `data:{content-type};base64,...`,上限 20MB(`_MAX_PROXY_BYTES`,23 行),失败回退空串、前端用 `url`。
|
||||
- 无密钥 → 500;SDK 异常/HTTP 非 200 → 502(`_fail`,60-64 行)。
|
||||
- `_apply_base_url`(51-57 行):仅北京业务空间需设 `dashscope.base_http_api_url`。
|
||||
- 文件头注释明确:**当前不落盘、不存储图片**,将来接七牛云可在返回 URL 后加"下载并转存"步骤,api 层与前端契约不动(image_edit.py:1-9)——这是刻意的扩展点。
|
||||
|
||||
---
|
||||
|
||||
## 五、studio 前端页面/组件/功能清单与交互流程
|
||||
|
||||
### 5.1 技术栈(studio/package.json + vite.config.ts)
|
||||
|
||||
- React 19.2、react-router 7(createBrowserRouter)、antd 6.1(zhCN)、axios、Vite 7(SWC 插件)、TS 5.9;`@` 别名 → `src`。
|
||||
- dev server:端口 8900(strictPort、open),`/api` 代理到 `http://127.0.0.1:8800`(vite.config.ts:17-22)。
|
||||
|
||||
### 5.2 页面与路由
|
||||
|
||||
| 文件 | 内容 |
|
||||
|---|---|
|
||||
| src/main.tsx | createRoot 挂载 |
|
||||
| src/App.tsx | ConfigProvider(zhCN、主色 #8b5cf6、圆角 8)+ AntdApp + RouterProvider |
|
||||
| src/router/index.tsx | `/` 与 `/ai-image` → AiImagePage(MainLayout 内);`*` → Navigate `/` |
|
||||
| src/layouts/MainLayout.tsx | 固定 Sider(240, 深色, 可折叠) + Header(页面标题/副标题) + Content(Outlet);<768px 切 Drawer 移动菜单 |
|
||||
| src/layouts/SidebarMenu.tsx | antd Menu,"主要功能"分组,点击 navigate |
|
||||
| src/layouts/menuConfig.tsx | **仅一个菜单项**:`/ai-image`「AI 图生图」(subtitle:上传图片、加水印、用万相模型进行图生图编辑);`getPageInfo` 路径→标题映射 |
|
||||
|
||||
### 5.3 AiImagePage(pages/ai-image/AiImagePage.tsx)——主页面
|
||||
|
||||
状态:图片列表 `ImageItem[]`(id/fileName/image/dataUrl/state)、全局水印设置(type=image|text、text 默认 'Panda Store'、opacity 默认 30、水印图 `/imgs/watermark.jpg` 预加载为 HTMLImageElement)、编辑弹窗(modalOpen/editing)。
|
||||
|
||||
区块与流程:
|
||||
1. **水印设置面板**:类型 Radio(图片/文字)→ 文字输入 或 水印图预览(圆形贴图);透明度 Slider(0-100)。
|
||||
2. **上传**:`Upload.Dragger` 多图、`accept=image/*`、`beforeUpload` 返回 false(阻止自动上传,纯前端读文件)→ `fileToImage`(FileReader → dataURL → Image)→ 追加进列表,按图片尺寸算默认水印位置(右下角)。
|
||||
3. **预览网格**:每张图一个 `WatermarkCanvas`(可拖拽水印,坐标相对原图、`clampPos` 限制不越界);卡片操作:加水印/清除水印(`toggleWatermark`)、**AI 生图**(`openEdit`:`renderFullRes` 全分辨率合成水印图 → dataURL → 打开弹窗)、导出 PNG(`downloadCanvas`,文件名加 `_watermark` 后缀)、删除;顶部"全部加水印"(`applyToAll` 重建所有 state)与"清空所有"。
|
||||
4. **AI 生图弹窗** = ImageEditModal(见下)。
|
||||
|
||||
### 5.4 ImageEditModal(pages/ai-image/components/ImageEditModal.tsx)——编辑工作台 Drawer
|
||||
|
||||
右抽屉(`min(1240px, 96vw)`),每次打开重置状态;三段式布局 + 底部 AI 指令条:
|
||||
|
||||
- **左:底图切换 Thumb 列表**——「原图」+ 每次 AI 生成的结果图(`AiResult{id,image,dataUrl,url}`),点击切换当前底图(标注可叠加在 AI 结果上继续编辑)。
|
||||
- **中:AnnotationCanvas**(预览最大宽 900)——标注画布,Pointer 交互:拖拽移动、旋转手柄旋转、四角手柄缩放(文字改 fontSize、标尺等比改 length/tSize/lineWidth/labelFontSize);命中检测 `hitTest`(旋转手柄 → 四角 → body);`onBeginInteraction` 每次交互开始时压撤销快照。
|
||||
- **右:属性编辑**——Tabs(文字/标尺);添加文字/标尺按钮;选中元素属性面板 ElementPropsPanel;**样式预设**:选中元素可"保存预设"(localStorage key `ozon_annotation_presets_v1`,只存样式不含内容/位置),新建元素/选中元素可套用预设;撤销/清空/导出 PNG。
|
||||
- **底部 AI 条**:TextArea 指令(Enter 快捷生成)+ 模型 Select(5 个模型带中文说明)+「AI 生成」按钮。
|
||||
- **runAi**(195-228 行):`editImage({base_image: currentDataUrl, prompt, model, n:1, strength:0.8, prompt_extend:true})` → 取 `results[0]`,优先 `image_base64` 否则 `url` → 解码为 Image 追加为结果底图并自动切换。出错用 `apiErrorMessage` 提示。
|
||||
- **exportImage**(230-240 行):新 canvas 合成底图 + 全部标注元素 → PNG 下载。
|
||||
|
||||
### 5.5 组件与工具细节
|
||||
|
||||
| 文件 | 能力 |
|
||||
|---|---|
|
||||
| components/WatermarkCanvas.tsx | 单图水印预览(PREVIEW_MAX_WIDTH=360),命中水印区域拖拽,坐标映射回原图,`clampPos` 防越界 |
|
||||
| components/AnnotationCanvas.tsx | 标注画布(预览最大宽 900):绘制底图+元素+选中框;Pointer 交互(move/rotate/resize);交互开始回调存撤销 |
|
||||
| components/ElementPropsPanel.tsx | 文字:内容/字体(9 种)/字号/加粗/文字色/描边色/描边宽;标尺:长度/线色/线宽/T字大小/标签文字/标签色/标签字号;公共:透明度/旋转 |
|
||||
| utils/watermark.ts | `WATERMARK_SCALE=0.15`(图片水印直径比)、`WATERMARK_MARGIN=10`、`WATERMARK_TEXT_SCALE=0.051`;`getWatermarkSize/clampPos/defaultPos`;`drawWatermarked`:图片水印圆形裁剪+全局透明度,文字水印先绘离屏层再合成(避免描边透出);`renderFullRes` 全分辨率合成 |
|
||||
| utils/annotation.ts | `FONT_FAMILIES`、`HANDLE_RADIUS=7`、`ROTATE_GAP=26`;`measureText/elementBox/toLocal/hitTest`;`drawElement/drawText/drawRuler/drawSelectionHandles`;`createTextElement/createRulerElement` 工厂(默认值随图宽缩放) |
|
||||
| utils/image.ts | `fileToImage`、`canvasToDataUrl`、`downloadCanvas` |
|
||||
| types/image.ts | `WatermarkType/WatermarkState/ImageItem/ImageEditRequest/ImageEditResponse`(与后端 schema 字段一致) |
|
||||
| types/annotation.ts | `TextElement/RulerElement`(中心点/旋转/透明度 + 各自样式字段) |
|
||||
| services/api.ts | axios 实例:baseURL=`envConfig.apiBaseUrl`(默认 `/api`)、**timeout 120s**;`api.get/post` 解包 `res.data`;`apiErrorMessage` 提取 FastAPI `detail` |
|
||||
| services/image.ts | `editImage(payload)` → `POST /image/edit` |
|
||||
| config/env.ts | `VITE_API_BASE_URL`(默认 `/api`)、`VITE_APP_NAME`、`debug=import.meta.env.DEV` |
|
||||
|
||||
### 5.6 前后端衔接方式
|
||||
|
||||
- 前端**只调 `/api/image/edit`**(services/image.ts);`/api/ai/*` 暂无 studio 页面(v1 `web/js/ai-copy.js` 在消费,v1 已冻结)。
|
||||
- 图片以 data URL 在请求体内传输(15MB 上限),响应以服务端代理的 `image_base64` 为主、`url` 兜底——专为规避阿里云结果 URL 无 CORS 的画布污染问题。
|
||||
|
||||
---
|
||||
|
||||
## 六、可直接复用到 V2 vs 需要重写的部分
|
||||
|
||||
### 6.1 可直接复用(成熟、结构清晰、低耦合)
|
||||
|
||||
| 资产 | 理由 |
|
||||
|---|---|
|
||||
| 模型目录机制(models.yaml + models_catalog.py) | "模型即配置":换/加 LLM 只改 yaml+env,代码零改动;`resolve_api_key` 按 env 名取密钥,安全;V2 加多厂商直接扩展 |
|
||||
| DeepSeek 调用骨架(deepseek.py 的 `_chat_once` + `_extract_json_object` + 重试纠错循环) | 通用性强:JSON 输出强制、代码块剥离、容错截取、失败追加修正消息重试一次;可抽象为通用 "JSON 任务 LLM 调用器" 供 V2 任意生成任务复用 |
|
||||
| 文案 schema 与提示词(schemas/copy.py + prompts/copy_ru.py) | 领域逻辑已打磨(事实边界、俄语标题/描述结构、中俄对照);V2 若保留文案生成,整套平移 |
|
||||
| 图生图服务(services/image_edit.py + schemas/image_edit.py) | 多模型路由(ImageSynthesis vs MultiModalConversation)、服务端代理下载规避 CORS、线程池隔离同步 SDK、响应契约(task_id/request_id 已预留异步字段);注释已规划"返回后接七牛转存"扩展点,与 V2 存储需求天然衔接 |
|
||||
| 前端 API 层(services/api.ts + services/image.ts + config/env.ts) | axios 封装(120s 超时、detail 提取)与端点封装可原样复用;新增端点照 image.ts 模式加即可 |
|
||||
| 水印/图片/标注工具集(utils/watermark.ts、utils/image.ts、utils/annotation.ts) | 纯 canvas 数学、框架无关、按原图坐标系建模(预览缩放/全分辨率导出分离),可直接搬入 V2 |
|
||||
| 标注数据模型(types/annotation.ts:TextElement/RulerElement) | 字段设计合理(中心点+旋转+透明度+样式),可直接作为 V2 标注/叠加元素的数据契约种子,甚至与后端共享 schema |
|
||||
| 布局壳(MainLayout + SidebarMenu + menuConfig) | 菜单配置驱动、移动端适配完整;V2 加页面只改 menuConfig + router |
|
||||
| 水印批处理主流程(AiImagePage 上传→全局水印→批量套用→导出) | 完整闭环,可整体作为 V2 的一个功能模块迁入 |
|
||||
|
||||
### 6.2 需要重写/新建(当前缺失或形态不适配 V2)
|
||||
|
||||
| 资产 | 现状与重写理由 |
|
||||
|---|---|
|
||||
| Ozon 对接(api/ozon.py) | 纯占位,V2 若做"发布到 Ozon"需从零实现 Seller API(token 管理、类目树、商品上传、图片上传等) |
|
||||
| 前端 AI 文案页 | studio 完全没有文案 UI;后端 `/api/ai/copy` 就绪,V2 需新建页面(可参考 v1 web/js/ai-copy.js 的表单交互) |
|
||||
| 服务端文件/任务持久化 | 现状:图不落盘、无上传端点、无 DB、无任务队列;图生图同步等待(前端 120s 超时)。V2 若引入"商品文件夹"契约(README 所述四部分衔接点)与异步任务,需新增上传/资产/任务/持久化体系 |
|
||||
| ImageEditModal 状态管理 | 编辑+AI 生成+撤销+预设全在组件本地 useState;V2 若需跨步骤工作流/历史/多页共享,需抽出 store(如 zustand/redux)与后端任务状态同步 |
|
||||
| AiImagePage 批量状态 | 纯内存 useState,刷新即失;V2 按商品文件夹组织时需要持久化/恢复会话 |
|
||||
| 鉴权与错误治理 | 无任何鉴权、无统一错误码规范(全部 HTTPException detail 字符串);V2 多用户/上线需补齐 |
|
||||
| 水印可配置性 | 水印图硬编码 `/imgs/watermark.jpg`、比例/字号为常量;V2 应支持自定义水印资源与配置 |
|
||||
| 路由/菜单 | 仅 1 页;V2 多页面需按功能域重组(当前结构太薄,扩展时建议直接重构而非继续堆叠) |
|
||||
| 测试与文档链 | 无自动化测试;契约靠 README/docs 维护。V2 建议引入 schema 共享(README 中 `packages/schema` 待建项)避免前后端字段漂移 |
|
||||
|
||||
### 6.3 一句话结论
|
||||
|
||||
**后端资产(模型目录、LLM 调用骨架、图生图多模型服务、schema)质量高且刻意留了扩展点(异步字段、七牛转存注释),可大幅平移;前端可平移的是纯 canvas 工具层与布局壳,而所有"产品化"能力——Ozon 对接、文案 UI、持久化/资产、异步任务、鉴权、状态管理——目前为空或雏形,是 V2 的主要建设量。**
|
||||
@@ -0,0 +1,278 @@
|
||||
# V2 数据库设计
|
||||
|
||||
> 状态:方案设计(待确认)
|
||||
> 上游:[V2 总览](./README.md) · [V2 架构](./architecture.md)
|
||||
> 字段来源:Ozon `ProductAPI_ImportProductsV3` + V1 `docs/contracts/product-json.md` + 采集/发布/店铺维度
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计原则
|
||||
|
||||
1. **商品主表对齐 Ozon 字段**:`products` 表按 `ImportProductsV3` 的字段平铺(`name/description/price/offer_id/...`),JSONB 存三类结构:`attributes` / `complex_attributes`(Ozon 动态属性)、`raw`(采集原文)、`pricing`(计价结果)。
|
||||
2. **素材与发布字段分离**:采集来的源图(分组/变体/源站 URL/七牛 URL)放 `product_assets` 表;`products.images` 只存「将提交给 Ozon 的有序公网 URL 数组」。
|
||||
3. **店铺密钥加密落库**:`shops.client_id_enc` / `api_key_enc` 用服务端密钥 AES-GCM 加密,前端永不回显明文。
|
||||
4. **发布异步化**:发布请求与结果存 `publish_tasks`,商品状态由轮询结果回写。
|
||||
5. **类目字典可重建**:`category_*` 三张表是 Ozon 字典的本地缓存,可随时清空重拉,不作为业务真源。
|
||||
|
||||
数据库:**PostgreSQL 16**。ID 统一 `UUID`(`gen_random_uuid()`)或 Ozon 原生 `BIGINT`(类目/属性 id 用 BIGINT 保持与 Ozon 一致)。时间统一 `timestamptz`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 表结构
|
||||
|
||||
### 2.1 `users` —— 用户(预留,MVP 单用户可空置)
|
||||
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | UUID PK | |
|
||||
| username | varchar(64) UNIQUE | 登录名 |
|
||||
| password_hash | varchar(255) | Argon2/bcrypt |
|
||||
| created_at | timestamptz | |
|
||||
|
||||
MVP 用 `APP_TOKEN` 时此表可留空;升级多用户时启用。
|
||||
|
||||
### 2.2 `shops` —— Ozon 店铺
|
||||
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | UUID PK | |
|
||||
| user_id | UUID FK → users | 归属(MVP 可为空) |
|
||||
| name | varchar(128) | 店铺显示名 |
|
||||
| client_id_enc | text | Client-Id 密文 |
|
||||
| api_key_enc | text | Api-Key 密文 |
|
||||
| currency_code | varchar(3) DEFAULT 'RUB' | 店铺结算币种(RUB/CNY) |
|
||||
| status | enum('active','invalid','disabled') DEFAULT 'active' | invalid=连通性校验失败 |
|
||||
| last_checked_at | timestamptz | 最近一次校验时间 |
|
||||
| created_at / updated_at | timestamptz | |
|
||||
|
||||
> **密钥安全**:`client_id` / `api_key` 用服务端 `SECRET_KEY` 做 AES-GCM 加密后存 `*_enc`。列表接口只返回 `id/name/currency/status/last_checked_at` 与**打码**的 client_id 后四位,永不返回明文 key。
|
||||
|
||||
### 2.3 `products` —— 商品(采集箱 + 编辑 + 发布一体化)
|
||||
|
||||
| 列 | 类型 | 说明 | 对应 Ozon 字段 |
|
||||
|---|---|---|---|
|
||||
| id | UUID PK | 内部主键 | — |
|
||||
| user_id | UUID FK → users | 归属(MVP 可空) | — |
|
||||
| stage | enum | `collected/editing/ready/publishing/published/failed/archived` | — |
|
||||
| source_platform | varchar(16) | `ozon/1688/taobao` | — |
|
||||
| source_item_id | varchar(64) | 源平台商品 ID(去重) | — |
|
||||
| source_url | text | 采集来源 URL | — |
|
||||
| offer_id | varchar(255) | **自己的货号**(采集恒空,编辑必填) | offer_id |
|
||||
| ozon_product_id | bigint | 发布成功后回填 | — |
|
||||
| ozon_sku | bigint | 跟卖(import-by-sku)用,可空 | — |
|
||||
| name | text | 商品名(俄文,最终) | name |
|
||||
| description | text | 商品描述(俄文,最终) | description |
|
||||
| description_category_id | bigint | 类目 | description_category_id |
|
||||
| type_id | bigint | 商品类型 | type_id |
|
||||
| price | numeric(20,2) | 销售价 | price |
|
||||
| old_price | numeric(20,2) | 划线价 | old_price |
|
||||
| currency_code | varchar(3) DEFAULT 'RUB' | | currency_code |
|
||||
| vat | varchar(8) DEFAULT '0' | 0 / 0.1 / 0.2 | vat |
|
||||
| depth / width / height | numeric(12,3) | 尺寸 | depth/width/height |
|
||||
| dimension_unit | varchar(4) DEFAULT 'mm' | mm / cm | dimension_unit |
|
||||
| weight | numeric(12,3) | 重量 | weight |
|
||||
| weight_unit | varchar(4) DEFAULT 'g' | g / kg | weight_unit |
|
||||
| barcode | varchar(64) | 条码 | barcode |
|
||||
| images | jsonb | 有序公网 URL(七牛)数组,≤15 | images |
|
||||
| primary_image | text | 主图 URL | primary_image |
|
||||
| images360 | jsonb | 360 图 URL 数组 | images360 |
|
||||
| color_image | text | 营销色图 URL | color_image |
|
||||
| pdf_list | jsonb | | pdf_list |
|
||||
| attributes | jsonb | `[{complex_id,id,values:[{dictionary_value_id,value}]}]` | attributes |
|
||||
| complex_attributes | jsonb | 视频/尺码表等 | complex_attributes |
|
||||
| promotions | jsonb | | promotions |
|
||||
| raw | jsonb | 采集原文:`{title,price,params[],desc,sellingPoints,brand,texts[]}` | —(`_raw`) |
|
||||
| pricing | jsonb | 计价结果(见 §3) | —(`_pricing`) |
|
||||
| copy | jsonb | AI 文案结果:`{titles_ru/zh,description_ru/zh,tags_ru/zh,model}` | — |
|
||||
| fx_rate | numeric(12,4) | 计价时快照的汇率 | — |
|
||||
| published_at | timestamptz | 发布成功时间 | — |
|
||||
| created_at / updated_at | timestamptz | | |
|
||||
|
||||
索引:
|
||||
- `(user_id, stage)` —— 采集箱/发布列表主查询
|
||||
- `(source_platform, source_item_id)` UNIQUE(可空)—— 采集去重
|
||||
- `offer_id` —— 货号查重
|
||||
- `ozon_product_id`
|
||||
|
||||
### 2.4 `product_assets` —— 采集素材(图片/视频)
|
||||
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | UUID PK | |
|
||||
| product_id | UUID FK → products ON DELETE CASCADE | |
|
||||
| group_key | varchar(16) | `main/sku/detail/video/param` |
|
||||
| variant_name | varchar(128) | SKU 规格名(俄文原样) |
|
||||
| sort_order | int | 组内顺序(1 起,对应命名 `main-001`) |
|
||||
| type | varchar(8) | `img/video` |
|
||||
| source_url | text | 源站原图 URL(溯源) |
|
||||
| qiniu_url | text | 七牛公网 URL(转存成功后) |
|
||||
| status | enum('pending','downloading','uploaded','failed') | 转存状态 |
|
||||
| dedupe_key | varchar(512) | URL 归一化指纹(去重) |
|
||||
| width / height | int | |
|
||||
| error | text | 失败原因 |
|
||||
| created_at | timestamptz | |
|
||||
|
||||
索引:`(product_id, group_key, sort_order)`。
|
||||
|
||||
> **与 `products.images` 的关系**:`product_assets` 是「素材库」(编辑期勾选、分组、去重);用户从素材库选出 ≤15 张主图后,按顺序写 `products.images`(七牛 URL)。这两层解耦,跟卖换主图不改素材库。
|
||||
|
||||
### 2.5 `product_texts` —— 采集文本(可选,也可并进 raw)
|
||||
|
||||
> 一期建议**并进 `products.raw`**(JSONB),不必单开表。若后续要按「卖点/参数」检索,再拆此表:
|
||||
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| product_id | UUID FK | |
|
||||
| kind | varchar(16) | `title/params/selling_point/desc/price/brand` |
|
||||
| content | text | 文本 |
|
||||
| pairs | jsonb | `table` 模式的 kv |
|
||||
|
||||
### 2.6 `publish_tasks` —— 发布任务
|
||||
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | UUID PK | |
|
||||
| product_id | UUID FK → products | |
|
||||
| shop_id | UUID FK → shops | 发布到哪个店铺 |
|
||||
| ozon_task_id | bigint | `/v3/product/import` 返回的 task_id |
|
||||
| status | enum('pending','processing','moderation','imported','failed') | 轮询结果 |
|
||||
| request_payload | jsonb | 实际发给 Ozon 的 items[0](脱敏后) |
|
||||
| response | jsonb | `/v1/product/import/info` 原始结果 |
|
||||
| errors | jsonb | 失败原因数组 |
|
||||
| created_at / completed_at | timestamptz | |
|
||||
|
||||
索引:`(product_id, created_at DESC)`、`ozon_task_id`。
|
||||
|
||||
### 2.7 类目字典缓存(三张,可重建)
|
||||
|
||||
#### `category_tree`
|
||||
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| description_category_id | bigint PK | 类目 ID |
|
||||
| parent_id | bigint | 父类目 |
|
||||
| category_name | varchar(255) | |
|
||||
| type_id | bigint | 商品类型 ID |
|
||||
| type_name | varchar(255) | |
|
||||
| disabled | boolean | 不可建品 |
|
||||
| level | int | 层级 |
|
||||
| lang | varchar(8) | DEFAULT/RU/EN/ZH_HANS |
|
||||
| updated_at | timestamptz | 缓存时间 |
|
||||
|
||||
#### `category_attributes`
|
||||
|
||||
主键 `(description_category_id, type_id, attribute_id)`。
|
||||
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| description_category_id / type_id / attribute_id | bigint | 复合主键 |
|
||||
| name | varchar(255) | 属性名 |
|
||||
| description | text | |
|
||||
| type | varchar(32) | 属性值类型 |
|
||||
| dictionary_id | bigint | 0=无字典 |
|
||||
| group_id / group_name | bigint / varchar | 属性分组 |
|
||||
| is_required | boolean | 必填 |
|
||||
| is_aspect | boolean | 变体属性(颜色/尺码) |
|
||||
| is_collection | boolean | 多值 |
|
||||
| max_value_count | int | |
|
||||
| attribute_complex_id | bigint | 复杂属性 |
|
||||
| complex_is_collection | boolean | |
|
||||
| category_dependent | boolean | 字典值是否依赖类目 |
|
||||
| lang | varchar(8) | |
|
||||
| updated_at | timestamptz | |
|
||||
|
||||
#### `attribute_values`
|
||||
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | bigint | 字典值 ID |
|
||||
| attribute_id | bigint | |
|
||||
| description_category_id / type_id | bigint | |
|
||||
| value | varchar(512) | 字典值文本 |
|
||||
| picture | text | 值配图 |
|
||||
| info | text | |
|
||||
| lang | varchar(8) | |
|
||||
| updated_at | timestamptz | |
|
||||
|
||||
> 字典值可能很大(一个类目数万条),**按需拉取**:用户选了类目+属性后才拉该属性字典,且只缓存用过的属性(见 `ozon-publish.md` §3)。
|
||||
|
||||
---
|
||||
|
||||
## 3. JSONB 结构约定
|
||||
|
||||
### 3.1 `products.raw`(采集原文,对齐 V1 `_raw` + texts)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"title": "Термокружка детская 316",
|
||||
"price": "1 290 ₽",
|
||||
"params": [{ "key": "Материал", "value": "Нержавеющая сталь" }],
|
||||
"desc": "…",
|
||||
"sellingPoints": "…",
|
||||
"brand": "…",
|
||||
"texts": [ // 插件 texts[] 原样
|
||||
{ "kind": "params", "content": "…", "pairs": [{ "key": "…", "value": "…" }] }
|
||||
],
|
||||
"images": { "main": [...], "sku": [...], "detail": [...], "video": [...] } // 采集快照(可选)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 `products.pricing`(对齐 V1 `_pricing`)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"purchasePrice": 18.5, // 进货价 ¥
|
||||
"profitRate": 30, // 净利率 %
|
||||
"logisticsLevel": "high", // low | high | high2
|
||||
"weightG": 320,
|
||||
"dims": { "l": 12, "w": 8, "h": 20 },
|
||||
"logisticsFee": 0,
|
||||
"fullCommission": 0,
|
||||
"totalCost": 0,
|
||||
"sellingPriceCny": 0,
|
||||
"sellingPriceRub": 0,
|
||||
"discountReserve": 50,
|
||||
"fxRate": 11.8,
|
||||
"calculatedAt": "…"
|
||||
}
|
||||
```
|
||||
|
||||
> 计价公式与字段沿用 v1(`web/js/app.js`),**只抄不改**,见 `migration.md` §3。`products.price` 最终取 `sellingPriceRub`(预留折扣后售价)。
|
||||
|
||||
### 3.3 `products.attributes`(对齐 Ozon)
|
||||
|
||||
```jsonc
|
||||
[ { "complex_id": 0, "id": 5076, "values": [ { "dictionary_value_id": 971082156, "value": "Speaker stand" } ] } ]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 关系图
|
||||
|
||||
```
|
||||
users 1─n shops 1─n publish_tasks n─1 products
|
||||
1─n product_assets
|
||||
1─n product_texts(可选)
|
||||
products n─1 category_tree(弱关联,仅存 id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 迁移(Alembic)约定
|
||||
|
||||
- 首个迁移建全部表;后续 schema 变更走 Alembic revision。
|
||||
- JSONB 字段的 schema 演进靠应用层版本号(`raw.schemaVersion` / `pricing.schemaVersion`)而非 DB 迁移,避免频繁 ALTER。
|
||||
- `shops.client_id_enc/api_key_enc` 的加密密钥 `SECRET_KEY` 放 `.env`,**换环境(本地/腾讯云)需保证一致或做好密文重写**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 规模预估(单人自用 → 小团队)
|
||||
|
||||
| 表 | 量级 | 说明 |
|
||||
|---|---|---|
|
||||
| products | 万级 | 每商品数十素材,主表轻 |
|
||||
| product_assets | 十万级 | 每商品 10~30 图 |
|
||||
| publish_tasks | 万级 | 每发布一次一条 |
|
||||
| category_* | 类目数万 / 属性数百万 / 值可能上亿(按需缓存) | 只缓存用过的 |
|
||||
|
||||
该量级单机 PostgreSQL 绰绰有余,无需分库分表;`product_assets` 后续可考虑按 product_id 分区或归档。
|
||||
@@ -0,0 +1,148 @@
|
||||
# V2 图片处理方案
|
||||
|
||||
> 状态:方案设计(**图片方案 B 已拍板** 2026-08-15;套图落地节奏待定)
|
||||
> 上游:[V2 总览](./README.md) · [V2 架构](./architecture.md)
|
||||
> 相关:V1 [`docs/studio/image-edit.md`](../studio/image-edit.md) · `ecommerce-image-suite/` 源码与 `SKILL.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 先厘清:这是「两种不同的能力」,不是二选一的替代品
|
||||
|
||||
用户提出的两个方案,本质是把两种**不同粒度**的图片能力放在一起比了:
|
||||
|
||||
| | 现有 studio「AI 生图」 | ecommerce-image-suite「电商套图」 |
|
||||
|---|---|---|
|
||||
| 模型 | 万相 `wanx2.1-imageedit`(+ qwen-image-edit 系列) | `wan2.7-image-pro` / 豆包 `doubao-seedream` / GPT-image 等 |
|
||||
| 形态 | **单图编辑**:换背景/去水印/局部重绘/加文字/风格化 | **套图生成**:原图 → 8~9 张营销图 |
|
||||
| 输入 | 一张底图 + 一句指令 | 商品原图(1~3 张)+ 卖点文案 |
|
||||
| 输出 | 1 张改好的图 | 白底主图/核心卖点图/卖点图/材质图/场景图/模特图/多场景拼图/详情图/三角度 |
|
||||
| 适用 | 「修一张图」:白底、去水印、补字 | 「产出一整套」:营销素材、详情页 |
|
||||
| 成本 | 单次编辑(百炼按张计) | 一套 8~9 张 × 单张价 + 1 次视觉分析 |
|
||||
|
||||
**结论先行**:这两个是**互补**的,不是谁替代谁。所以不存在真正的「方案一 vs 方案二」,而是「只保留一个」还是「两个都要」。
|
||||
|
||||
---
|
||||
|
||||
## 2. 对 Ozon 跟卖场景,真正刚需是什么
|
||||
|
||||
跟卖(从竞品页采集 → 自己上品)的图片痛点很具体:
|
||||
|
||||
1. **白底主图**:Ozon 主图要求纯白底、无文字、无水印。竞品图往往带背景/水印/营销字。
|
||||
2. **去水印/去字**:采集来的图常带竞品水印。
|
||||
3. **补充营销图**:主图之外,详情页要卖点图、场景图、模特图(服饰类)。
|
||||
|
||||
| 痛点 | 最合适的工具 | 成本 |
|
||||
|---|---|---|
|
||||
| 白底 / 去水印 / 去字 / 换背景 | **`wanx2.1-imageedit`(单图编辑)** | 单张,便宜,可控 |
|
||||
| 整套营销图 / 模特图 / 场景图 / 详情图 | **ecommerce-image-suite(套图)** | 一套多张,按需 |
|
||||
|
||||
**单图编辑是高频刚需**(几乎每个商品都要做白底),**套图是选配**(服饰/需要营销图的类目才用,且部分类目 Ozon 主图够用)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 方案对比
|
||||
|
||||
### 方案 A:只集成 ecommerce-image-suite,去掉 wanx2.1-imageedit
|
||||
|
||||
- ✅ 简单:图片能力一个入口,前端一套 UI。
|
||||
- ❌ **贵且不划算**:白底/去水印这种单图需求,也要走整套生成(8~9 张),大量浪费。
|
||||
- ❌ **产出不完全对口**:套图里只有「白底主图」一张符合 Ozon 主图规范;卖点图/场景图**带营销文字**,不能当 Ozon 主图(Ozon 主图禁文字水印),只能进详情/补充。
|
||||
- ❌ 丢掉了「改一张图」的精细控制(局部重绘、加字、改背景强度),这些 wanx2.1-imageedit 已经做好且便宜。
|
||||
|
||||
### 方案 B:套图 + 保留 wanx2.1-imageedit(改名「智能修图」),高低搭配 ✅ **已选(2026-08-15)**
|
||||
|
||||
- 高频单图需求(白底/去水印/换背景/加字)→ **智能修图**(wanx2.1-imageedit,已有代码,复用改名)。
|
||||
- 低频整套需求(卖点图/场景图/模特图/详情图)→ **电商套图**(ecommerce-image-suite)。
|
||||
- ✅ 两者共用 `DASHSCOPE_API_KEY`,无额外对接成本。
|
||||
- ✅ 现有 `server/api/image.py` + `services/image_edit.py` + studio `AiImagePage` 整套**原样保留**,只是改个名字和菜单。
|
||||
- ✅ 成本可控:默认用便宜的修图,需要时再整套。
|
||||
|
||||
**成本佐证**(来自 ecommerce-image-suite `references/providers.md`):
|
||||
|
||||
| 供应商/模型 | 单价 | 参考图 | 国内直连 |
|
||||
|---|---|---|---|
|
||||
| 千问 `wan2.7-image-pro` | ¥0.14/张 | ✅ | ✅ |
|
||||
| 豆包 `doubao-seedream-4-5` | ¥0.12/张 | ✅ | ✅ |
|
||||
| Gemini 3.1-flash-image | $0.03/张 | ✅ | 需代理 |
|
||||
| GPT-image-1.5 | $0.04~0.2/张 | ✅ | 需代理 |
|
||||
| Stability core | $0.03/张 | ❌ | 需代理 |
|
||||
|
||||
一套 8 张 ≈ ¥0.96~1.12(国内直连),加一次视觉分析(qwen-vl-max)。单图编辑是「按需 1 张」,成本远低于整套。
|
||||
|
||||
> **纠正一个直觉**:套图「贵」不在单张价,而在「一次要生成一整套」。单张价其实比很多平台便宜。所以「方案 B 更贵」不成立——方案 B 反而因为默认走单图编辑而更省。
|
||||
|
||||
---
|
||||
|
||||
## 4. 命名与入口(方案 B 落定后的 UI)
|
||||
|
||||
studio 编辑页「图片」面板里,每张图/每个插槽提供两类操作:
|
||||
|
||||
| 菜单 | 能力 | 底层 |
|
||||
|---|---|---|
|
||||
| **智能修图** | 白底、去水印、换背景、局部重绘、加文字、风格化 | `wanx2.1-imageedit`(`/api/image/edit`) |
|
||||
| **电商套图** | 从商品原图生成整套营销图(可勾选图型) | ecommerce-image-suite(`/api/image/suite`) |
|
||||
|
||||
现有的「AI 图生图」独立页保留,改名「智能修图」,作为单图精修工作台;「电商套图」作为编辑页图片面板里的一个按钮/抽屉。
|
||||
|
||||
---
|
||||
|
||||
## 5. 电商套图的集成方式(ecommerce-image-suite 是「脚本+Skill」,不是服务)
|
||||
|
||||
`ecommerce-image-suite` 目前是给 Agent/人用的 **脚本 + Skill** 形态(`analyze.py` + `generate.py`,Apache-2.0),不是现成 API。要集成进 studio,有三档:
|
||||
|
||||
| 档 | 做法 | 代价 | 建议 |
|
||||
|---|---|---|---|
|
||||
| L1 快速 | 服务端 subprocess 调 `analyze.py`/`generate.py` | 依赖 Python 环境、脚本路径、退出码解析;无并发控制 | 验证期可用 |
|
||||
| L2 正式 ✅ | 把 `generate.py` 的 **prompt 引擎 + 供应商调用**抽成 `services/image_suite.py`(纯 Python 模块,直接在 FastAPI 里调 DashScope/豆包) | 移植 prompt 模板与参数(图型 9 种、6 套视觉模板、平台规范),约 1~2 天 | **推荐** |
|
||||
| L3 独立服务 | 套图做成独立微服务,HTTP 调用 | 重,单人项目不值 | 不建议 |
|
||||
|
||||
**L2 的关键取舍**:ecommerce-image-suite 里有大量「Agent 交互」逻辑(模特选择、模板推荐、场景推荐、确认步骤),这些在 studio 里**不该照搬**。V2 只取它的**生成引擎**(图型 Prompt 模板 + 供应商 API 调用),把交互简化成 studio 表单:
|
||||
|
||||
- 输入:选 1~3 张商品原图(素材库已有)+ 卖点文案(可自动从 `products.raw`/`copy` 取,可手改)+ 勾选图型(白底主图/卖点图/场景图/…)+ 目标语言(俄文)。
|
||||
- 输出:生成结果逐张进素材库(`product_assets`,group_key 标 `generated`),用户挑图再进 `products.images`。
|
||||
- 视觉分析(analyze 那步):一期跳过(直接让用户填卖点),二期可调 qwen-vl-max 自动提炼卖点。
|
||||
|
||||
> 注意:ecommerce-image-suite 的图型 Prompt 模板当前是为**国内平台/Amazon**写的(中文/英文文案、平台字体规范)。Ozon 是俄文市场,**俄文文案渲染需要新增一套俄文 Prompt 约束**(或先出英文/无文字图,俄文文案靠前端叠加,见 §7)。这是集成里唯一需要新做的实质工作。
|
||||
|
||||
### 5.1 已知坑(源码级核对,服务端集成必须处理)
|
||||
|
||||
| 坑 | 说明 | 对策 |
|
||||
|---|---|---|
|
||||
| `generate.py` 全局禁用 SSL 校验 | 脚本为方便本地跑图关闭了 TLS 验证 | 服务端集成(L1/L2)**必须移除**,恢复正常 TLS,否则是安全漏洞 |
|
||||
| 退出码恒 0,单张失败不中断 | 成败不反映在退出码上 | 以 `generate_result.json` 为**唯一真源**逐张判成败;失败图重试或标记 |
|
||||
| 文档与代码不一致 | Gemini 端点、豆包/视频模型版本、README 称"无 LICENSE"但实为 Apache-2.0 等 | 以 `generate.py` 实际调用为准,逐供应商核对后再落地 |
|
||||
| 输出固定中文文件名 | `白底主图.jpg` 等中文命名 | 转存七牛时改用英文/序号命名,避免 Ozon 与跨平台文件名问题 |
|
||||
|
||||
### 5.2 用 SKILL.md 当 studio 向导蓝图(可选)
|
||||
|
||||
`SKILL.md` 里那段「上传原图 → 分析卖点 → 选平台/图型/模板/模特 → 生成 → 确认」的对话流,本身是经过打磨的**交互蓝图**。做 studio「电商套图」抽屉时可直接参照它,把多步向导固化成表单步骤(原图选择 → 卖点确认 → 图型勾选 → 模板/模特 → 生成),省去重新设计交互的成本。
|
||||
|
||||
---
|
||||
|
||||
## 6. 七牛存储(贯穿所有图片路径)
|
||||
|
||||
| 来源 | 处理 |
|
||||
|---|---|
|
||||
| 采集源图 | 插件传 URL → 服务端下载(带 Referer)→ 转存七牛 → `product_assets.qiniu_url` |
|
||||
| 智能修图结果 | DashScope 返回 URL(24h)→ 服务端下载 → 转存七牛 → 返回七牛 URL |
|
||||
| 套图结果 | 同上 |
|
||||
| 前端水印合成 | studio canvas 合成 → 上传七牛(服务端中转或直传 token) |
|
||||
|
||||
**为什么必须转存**:Ozon `images` 只收公网可访问 URL(Ozon 服务器主动拉取);阿里云结果 URL 24h 失效且无 CORS;源站 URL 可能防盗链/失效。七牛是稳定公网源。
|
||||
|
||||
一期建议**服务端中转上传**(改动小、无前端直传的 token 复杂度);量大后再切前端直传 + 上传 token。
|
||||
|
||||
---
|
||||
|
||||
## 7. 推荐落地顺序(务实版)
|
||||
|
||||
1. **一期只做「智能修图」**(已有代码):白底/去水印/换背景。这是最高频、最省、复用度最高的部分。`/api/image/edit` 加七牛转存即可。
|
||||
2. **二期集成「电商套图」**:按 §5 L2 抽 `services/image_suite.py`,先支持 `white_bg / key_features / selling_pt / material / lifestyle / model / multi_scene` 几个高频图型,俄文文案先出英文/无字版本。
|
||||
3. **三期**:俄文文案渲染(新增俄文 Prompt 约束或前端叠字)、视觉分析自动提炼卖点、模特库接入(45 位内置模特)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 决策记录
|
||||
|
||||
- ✅ **图片方案:已选 B(高低搭配)**(2026-08-15):集成 ecommerce-image-suite「电商套图」+ 保留 wanx2.1-imageedit(改名「智能修图」)。
|
||||
- ⏳ 待定:**电商套图一期就做,还是先只交付「智能修图」跑通闭环、套图二期再加**(见 [`migration.md`](./migration.md) §7)。
|
||||
@@ -0,0 +1,139 @@
|
||||
# V2 落地计划与改动清单
|
||||
|
||||
> 状态:方案设计(待确认)
|
||||
> 上游:[V2 总览](./README.md) · 其余各分册
|
||||
|
||||
---
|
||||
|
||||
## 1. 分阶段里程碑(建议顺序)
|
||||
|
||||
每个里程碑都可独立验收,且不破坏 V1 正在用的部分。
|
||||
|
||||
| # | 里程碑 | 内容 | 产出/验收 | 估时 |
|
||||
|---|---|---|---|---|
|
||||
| **M0** | 数据层与骨架 | 建 DB + SQLAlchemy 模型 + Alembic 首迁移;`/api/health` 接 DB;`.env` 加 `DATABASE_URL`/`APP_TOKEN`/`SECRET_KEY`/`QINIU_*` | 服务能连库、能 `alembic upgrade` | 1d |
|
||||
| **M1** | 插件上传落库 | extension-v2 加消息层 + api client + options;`POST /api/materials` + 素材下载转存七牛(后台协程) | 插件点「上传」,采集箱能看到商品与图 | 2d |
|
||||
| **M2** | 采集箱列表 + 商品编辑骨架 | studio 加「采集箱」页 + 「商品编辑」页(表单 + autosave 落库);计价面板(抄 v1 公式);文案面板(接 `/api/ai/copy`) | 能看采集箱、编辑保存、算价、生成文案 | 3d |
|
||||
| **M3** | 图片:智能修图 + 七牛 | `/api/image/edit` 加七牛转存;编辑页图片面板(水印沿用 canvas + 智能修图入口) | 图片能转存七牛、能白底/去水印 | 1d |
|
||||
| **M4** | 店铺 + 类目 + 属性 | `shops` CRUD + 连通校验;`/api/categories/*` 代理 + 缓存;属性映射 UI | 能绑店铺、选类目、映射属性 | 3d |
|
||||
| **M5** | 发布链路 | `POST /products/:id/publish` + 组装 items[0] + 轮询回填 + 发布结果页 | 商品成功进 Ozon 后台,product_id 回填 | 2d |
|
||||
| **M6** | CSV 导出 + 打磨 | `/api/export/products.csv` + 导出页;错误处理/限流/日志 | 能导出 CSV | 1d |
|
||||
| **M7** | 部署腾讯云 | nginx + systemd + PostgreSQL + 七牛配置;插件/studio 指向公网 | 公网可访问,闭环 | 1d |
|
||||
| **M8** | (二期)电商套图 | 集成 ecommerce-image-suite(`/api/image/suite`);俄文 Prompt | 按方案 B 决策而定 | 2~3d |
|
||||
|
||||
> M2/M4 是最大的两块(编辑页 + 属性映射),也是最值得先用静态样例打磨 UI 的部分。
|
||||
|
||||
---
|
||||
|
||||
## 2. 各端改动清单
|
||||
|
||||
### 2.1 extension-v2(采集插件)
|
||||
|
||||
1. 加 `src/messaging/`(`MessageMap` + client)与 `src/api/client.ts`(Bearer 鉴权,仅 background)。
|
||||
2. `background.ts` 从「单个 fetchImage 分支」改为 handler 表路由;新增 `collect`/`product-*`/`health` 等消息。
|
||||
3. options 页:后端地址 + Token + 「测试连接」。
|
||||
4. sidepanel「导出到本地」旁边加「上传到服务端」:复用 `buildProduct()` 产物 → `POST /api/materials`。
|
||||
5. 删除/降级 File System Access 写盘主路径(保留为可选本地备份)。
|
||||
6. `SH_PENDING_QUEUE` 重试队列 + `GET /products/:id/fingerprints` 跨页去重接线(v2 已写 builder 但未读回比对)。
|
||||
7. manifest 加后端域名 `host_permissions`。
|
||||
|
||||
> 详细契约沿用 `docs/extension/plan.md` §9/§13/§14,已在 `api.md` §2 落地。
|
||||
|
||||
### 2.2 server(服务端)
|
||||
|
||||
1. 依赖加:`sqlalchemy[asyncio]`、`asyncpg`、`alembic`、`qiniu`、`python-jose`(或 pyjwt)、`cryptography`。
|
||||
2. 新增 `models/`、`migrations/`、`jobs/`(下载转存协程、发布轮询协程)。
|
||||
3. 新增 api 文件:`collection/products/categories/shops/publish/export/fx/auth`。
|
||||
4. 复用不改:`ai.py`/`image.py`/`deepseek.py`/`image_edit.py`/`models_catalog.py`(`image_edit.py` 加七牛转存一步)。
|
||||
5. 新增 `services/ozon_client.py`(Ozon 通用调用)、`services/qiniu.py`(上传/下载转存)、`services/pricing.py`(把 v1 公式实现为服务端校验/计算,供「ready 校验」与 CSV)。
|
||||
6. 鉴权中间件:JWT 校验 + `APP_TOKEN` 换发。
|
||||
|
||||
### 2.3 studio(工作台)
|
||||
|
||||
1. 菜单从单页扩为多页:`采集箱 / 商品编辑 / 发布 / 店铺 / 导出 / 智能修图(原 AI 图生图)`。
|
||||
2. 新建 `pages/product/` 及子组件(PricingPanel/CopyPanel/ImagePanel/CategoryPicker/AttributeMapper/PublishPanel)。
|
||||
3. 计价纯函数从 `web/js/app.js` **抄**进 `src/pricing/`(不改原文件),补单测锁定 v1 数值。
|
||||
4. 文案面板复用 `/api/ai/copy`(参考 `web/js/ai-copy.js` 交互)。
|
||||
5. 状态管理引入 zustand(编辑页跨面板共享);axios 客户端对齐 `/api/*`。
|
||||
6. 复用不动:`utils/watermark.ts`、`annotation.ts`、`image.ts`、布局壳、`AiImagePage`(改名「智能修图」)。
|
||||
|
||||
### 2.4 web(v1 工具台)
|
||||
|
||||
**冻结,零改动**。只被读(抄公式、抄文案交互)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 计价公式迁移(v1 → 服务端 + studio)
|
||||
|
||||
来源:`web/js/app.js`(`calculateLogisticsFee` / `calculateAndDisplay` / `validateDimensions` / `validateLogisticsLevel` / `validatePriceRange` / `updateDerivedPrices`)。
|
||||
|
||||
迁移方式:
|
||||
- **studio 侧**:抽成 TS 纯函数(输入字段 + 汇率 + 预留% → 输出全部结果字段),展示在编辑页计价面板。
|
||||
- **server 侧**:抽成 `services/pricing.py`(同样公式的 Python 版),用于「ready 校验」、`products.price` 最终写入、CSV 导出的一致性。
|
||||
|
||||
> **为什么两端各一份**:计价是高频纯前端交互(实时算),不需要每次走后端;但发布前校验和导出需要服务端有权威结果。约定:**以服务端 `services/pricing.py` 为真源**,前端 TS 版照抄并对齐,用同一组 fixture 测两端一致性(沿用 V1 契约测试思路)。
|
||||
|
||||
关键常量(照抄不改):
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 物流费 | low/high/high2 三档 × 两重量段(公式见 app.js:959) |
|
||||
| 净到手比例 netRate | low=0.845;high/high2=0.785 |
|
||||
| 完全抽成 | 15.5%(low)/ 21.5%(其他),含约 3.5% 其它费 |
|
||||
| 汇率源 | FloatRates → 俄央行 → er-api,5~25 区间过滤,兜底 11.5 |
|
||||
| 预留折扣 | 默认 50%,0~95 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置(`.env` 新增项)
|
||||
|
||||
```env
|
||||
# 现有
|
||||
DEEPSEEK_API_KEY=…
|
||||
DASHSCOPE_API_KEY=…
|
||||
# V2 新增
|
||||
APP_TOKEN=… # 单用户登录 token(MVP)
|
||||
SECRET_KEY=… # 店铺凭证 AES-GCM 加密密钥
|
||||
DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/ozon_seller
|
||||
# 七牛
|
||||
QINIU_ACCESS_KEY=…
|
||||
QINIU_SECRET_KEY=…
|
||||
QINIU_BUCKET=…
|
||||
QINIU_DOMAIN=https://cdn.example.com # 七牛绑定域名(Ozon 拉取用)
|
||||
APP_BASE_URL=https://api.example.com # 插件/studio 回写、生成图回调用
|
||||
```
|
||||
|
||||
`.env.example` 同步补占位并注释。
|
||||
|
||||
---
|
||||
|
||||
## 5. 部署(腾讯云)
|
||||
|
||||
1. **资源**:轻量应用服务器 / CVM + CDB PostgreSQL + 七牛(域名需备案,Ozon 拉取的是公网 URL,务必用已备案域名)。
|
||||
2. **应用**:`uvicorn main:app --app-dir server --host 127.0.0.1 --port 8800 --workers 2`,systemd 守护;nginx 反代 `/api`,托管 studio 构建产物。
|
||||
3. **数据**:`alembic upgrade head`;`.env` 放服务器(不入 git);`SECRET_KEY` 换环境时注意密文一致性(见 database.md §5)。
|
||||
4. **七牛**:配置 bucket + 绑定 CDN 域名 + 证书;Ozon 服务器需能公网访问该域名。
|
||||
5. **健康检查**:`/api/health`(含 DB ping)给运维探活。
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与对策
|
||||
|
||||
| 风险 | 级别 | 对策 |
|
||||
|---|---|---|
|
||||
| 属性映射工作量大、体验差 | 🔴 高 | 自动匹配 + 人工确认;先做基础版,迭代智能匹配 |
|
||||
| Ozon 改版/限额/风控 | 🟡 中 | 采集端已有四路径 + 埋点热更;发布端错误透传 + 退避 |
|
||||
| ecommerce-image-suite 集成是脚本非服务 | 🟡 中 | 抽 prompt 引擎为服务模块(image-strategy §5 L2) |
|
||||
| 店铺密钥泄露 | 🔴 高 | AES-GCM 加密落库 + 前端打码 + 永不回显明文 + 日志脱敏 |
|
||||
| 任务异步(下载/发布)状态不可见 | 🟡 中 | 素材/发布都有状态表 + 前端轮询回显 |
|
||||
| 本地 → 云上环境不一致 | 🟡 中 | 十二要素:配置全走 `.env`,Alembic 管 schema |
|
||||
|
||||
---
|
||||
|
||||
## 7. 待确认项(开工前拍板)
|
||||
|
||||
1. ~~图片方案 A / B~~ ✅ **已定 B(高低搭配)**(2026-08-15,见 [`image-strategy.md`](./image-strategy.md) §8)。
|
||||
2. **电商套图是否进一期**:建议一期先只交付「智能修图」跑通闭环,套图二期。
|
||||
3. **单用户还是预留多用户**:schema 已按多用户预留,MVP 用 `APP_TOKEN` 即可。
|
||||
4. **库存是否自动设置**:一期发布到「已创建/审核」,库存去后台补(或二期接 `/v2/products/stocks`)。
|
||||
5. **数据库选型**:已定 PostgreSQL;若想更省事可换 SQLite(本地)→ 但 JSONB/并发/腾讯云部署建议直接用 PostgreSQL。
|
||||
@@ -0,0 +1,138 @@
|
||||
# V2 多 SKU(多变体)配置方案调研
|
||||
|
||||
> 状态:调研结论(待实现)
|
||||
> 来源:Ozon Seller API 官方文档 + `reference/maozi-plugin-3.2.3`(毛子ERP 逆向)+ `reference/AI编辑 - 毛子ERP.html`
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论一句话
|
||||
|
||||
Ozon 的多变体商品**不是"一个商品带多个 SKU 子结构"**,而是 **N 个独立商品(各自 `offer_id` / `product_id`),通过同一个「型号名称」属性(attribute id = `9048`)自动合并成一张卡**。变体之间的差异只能体现在「aspect 属性」(颜色/尺码等)上。
|
||||
|
||||
---
|
||||
|
||||
## 2. Ozon 官方机制
|
||||
|
||||
### 2.1 合并规则(官方文档原文)
|
||||
|
||||
`/v3/product/import` 文档明确写:
|
||||
|
||||
> To merge two product description pages, pass `9048` in the `attributes` array for each product. **All attributes except size or color must match** in these description pages.
|
||||
|
||||
即:
|
||||
|
||||
- 每个变体 = `items[]` 里的一个独立 item(独立 `offer_id`)。
|
||||
- 每个 item 的 `attributes` 里都带上 `id=9048`(型号名称 / Название модели),且**值相同**。
|
||||
- 除「尺寸/颜色」这类 aspect 属性外,其它属性必须完全一致。
|
||||
- Ozon 会把同型号、仅 aspect 不同的商品**自动合并成一张带变体选择器的卡片**。
|
||||
|
||||
### 2.2 aspect 属性(`is_aspect`)
|
||||
|
||||
`/v1/description-category/attribute` 返回的属性里,`is_aspect=true` 表示该属性是「区分同型号商品的变体维度」(官方定义:颜色、尺码这类)。这就是多变体的"轴":
|
||||
|
||||
- 变体轴 = 类目下 `is_aspect=true` 的属性(通常 `颜色`、`尺码`/`尺寸`)。
|
||||
- 其它属性(品牌、材料、型号名称…)在各变体间必须一致。
|
||||
|
||||
### 2.3 每个变体的图片
|
||||
|
||||
- 主图 `images`:每个变体传自己的主图(通常是该 SKU 的图)。
|
||||
- 采集端已支持"SKU 图带规格名"(`sku-001-синий.jpg`),正好一一对应。
|
||||
|
||||
---
|
||||
|
||||
## 3. 采集侧(毛子ERP 的 SKU 组合逻辑)
|
||||
|
||||
毛子ERP(1688 采集,`content-scripts/content.js` 里的 `生成SKU组合` 函数)的做法:
|
||||
|
||||
```
|
||||
规格选项(颜色×尺寸…) × SKU详情 → 笛卡尔积 → 变体列表
|
||||
红色, 蓝色 M, L 红色-M, 红色-L, 蓝色-M, 蓝色-L
|
||||
```
|
||||
|
||||
关键点:
|
||||
1. 从 `webAspects` 拿到规格维度(aspect)+ 每个维度的可选值(含 SKU 图)。
|
||||
2. 若有独立 SKU 详情(价格/图),做笛卡尔积;否则用默认价格 + 规格图。
|
||||
3. 每个变体:`{ name: "红色-M", price, primary_image, sku }`。
|
||||
|
||||
对我们的映射:`webAspects` 采集回来的 `skuVariants`(已有 `variantName` + `image`)就是变体轴的数据源。
|
||||
|
||||
---
|
||||
|
||||
## 4. 发布侧(Ozon 多 SKU 实现方案)
|
||||
|
||||
### 4.1 数据模型
|
||||
|
||||
在现有 `products` 表基础上,多 SKU 用**一张卡对应多个 product 记录**来表示:
|
||||
|
||||
- 每个变体是一条 `products` 记录,`offer_id` 唯一。
|
||||
- 变体间共享 `型号名称`(存 `raw.model_name`,映射到 attribute `9048`)。
|
||||
- 变体差异在 `attributes`(aspect 属性填不同字典值)。
|
||||
|
||||
### 4.2 发布流程
|
||||
|
||||
```
|
||||
① 主商品:确定类目 → 拉属性 → 找 is_aspect=true 的属性(如 颜色/尺码)
|
||||
② 变体配置 UI:
|
||||
颜色 ▾ [红/蓝/绿](字典值)
|
||||
尺码 ▾ [M/L/XL](字典值)
|
||||
→ 笛卡尔积生成变体列表(可编辑每个变体的 offer_id / 价格 / 主图)
|
||||
③ 对每个变体,组装 items[i]:
|
||||
offer_id = 变体自己的货号(如 SKU-001-RED-M)
|
||||
attributes = 同型号名称(9048) + 变体自己的颜色/尺码字典值 + 其它共同属性
|
||||
images = 变体自己的主图
|
||||
④ 一次 /v3/product/import 提交所有变体(≤100 个 item)
|
||||
⑤ 轮询回填每个变体的 product_id,Ozon 自动合并成一张卡
|
||||
```
|
||||
|
||||
### 4.3 关键字段映射
|
||||
|
||||
| 概念 | 来源 | 落点 |
|
||||
|---|---|---|
|
||||
| 型号名称 | 编辑页「型号名称」输入 | `attributes[{id:9048, values:[{value}]}]` |
|
||||
| 变体轴 | 类目 `is_aspect=true` 属性 | `attributes`(不同变体填不同 `dictionary_value_id`) |
|
||||
| 变体货号 | 用户/自动 | 每个变体 `offer_id` |
|
||||
| 变体图 | 采集 `skuVariants[].image` | 每个变体 `images[0]` |
|
||||
| 共同属性 | 属性映射结果 | 每个变体相同 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 待实现的 UI(studio 新增「SKU 配置」面板)
|
||||
|
||||
```
|
||||
[型号名称] 儿童保温杯 316
|
||||
[变体维度] 颜色(字典下拉,多选) [红][蓝][绿]
|
||||
尺码(字典下拉,多选) [M][L][XL]
|
||||
[生成变体] ← 笛卡尔积
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 颜色 尺码 货号 价格 主图 │
|
||||
│ 红 M SKU-001-RED-M [ ] [图] │
|
||||
│ 红 L SKU-001-RED-L [ ] [图] │
|
||||
│ 蓝 M SKU-001-BLU-M [ ] [图] │
|
||||
│ … │
|
||||
└──────────────────────────────────────────────┘
|
||||
[保存变体] → 生成 N 条 products 记录(或发布时展开)
|
||||
```
|
||||
|
||||
实现上两种选择:
|
||||
- **A. 落库展开**:保存时直接生成 N 条 `products`(每个变体一条),发布时各自提交。
|
||||
- **B. 发布时展开**:主商品存一份变体配置 JSON,发布时动态展开成 N 个 item 提交。
|
||||
|
||||
建议 **A**(落库展开)——与现有"采集箱 → 单商品编辑 → 发布"链路一致,每个变体可独立查看/编辑/重发。
|
||||
|
||||
---
|
||||
|
||||
## 6. 注意点
|
||||
|
||||
1. **变体必须同型号**:`9048` 值必须完全一致,否则 Ozon 不合并,会生成 N 张独立卡片。
|
||||
2. **非 aspect 属性必须一致**:品牌/材料/描述等若不同,Ozon 拒绝合并。
|
||||
3. **变体图数量**:每个变体各自 ≤15 张主图;`skuVariants` 采集的规格图作为变体主图候选。
|
||||
4. **`complex_attributes` 不是用来做 SKU 的**:它用于视频/尺码表等富内容,别混用。
|
||||
5. **单次 ≤100 item**:变体数超过 100 需分批提交。
|
||||
|
||||
---
|
||||
|
||||
## 7. 结论
|
||||
|
||||
- 多 SKU = **同型号名称(9048) + aspect 属性区分 + 多 product 记录**,靠 Ozon 自动合并,无需额外的"组合商品" API。
|
||||
- 采集端已具备变体轴数据(`webAspects` 的 `skuVariants`),发布端只需一个「变体配置 + 展开提交」的 UI 即可落地。
|
||||
- 推荐实现路径:studio 增加「SKU 配置」面板 → 落库展开为多条 product → 复用现有发布链路。
|
||||
@@ -0,0 +1,175 @@
|
||||
# V2 Ozon 发布集成
|
||||
|
||||
> 状态:方案设计(待确认)
|
||||
> 上游:[V2 总览](./README.md) · [V2 架构](./architecture.md) · [数据库](./database.md) · [API](./api.md)
|
||||
> 官方文档:[Ozon Seller API(中文)](https://docs.ozon.ru/api/seller/zh/)
|
||||
|
||||
---
|
||||
|
||||
## 1. 鉴权与店铺凭证
|
||||
|
||||
Ozon Seller API 用 **两个请求头** 鉴权(不是 OAuth):
|
||||
|
||||
```
|
||||
Client-Id: <你的 Client ID>
|
||||
Api-Key: <你的 API Key>
|
||||
```
|
||||
|
||||
- 获取:Ozon 卖家后台 → 设置 → Seller API → 生成 Key(可选权限级别)。
|
||||
- **凭证归属店铺**:V2 里每店铺一条 `shops` 记录,`client_id`/`api_key` 加密落库,调用时解密拼头。
|
||||
- **连通性校验**:`POST /api/shops/:id/test` 调 [`/v1/roles`](https://docs.ozon.ru/api/seller/zh/#operation/AccessAPI_RolesByToken)(返回该 key 的角色与可用方法),既验证凭证又看权限范围,零成本。
|
||||
|
||||
服务端封装 `services/ozon_client.py`:统一 base URL(`https://api-seller.ozon.ru`)、拼头、超时、错误映射(400/403/409/500 → 语义化 detail)、限流退避。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心接口(本项目用到)
|
||||
|
||||
| 用途 | 方法 | 说明 |
|
||||
|---|---|---|
|
||||
| 类目树 | `POST /v1/description-category/tree` | 返回 `description_category_id / type_id / category_name / type_name / disabled / children`;**只有末级类目可建品** |
|
||||
| 类目属性 | `POST /v1/description-category/attribute` | 入参 `description_category_id + type_id`;返回属性含 `is_required / is_aspect / is_collection / dictionary_id / type / max_value_count` |
|
||||
| 属性值字典 | `POST /v1/description-category/attribute/values` | 入参 `attribute_id + category_id + type_id + limit(≤2000) + last_value_id`(分页) |
|
||||
| 属性值搜索 | `POST /v1/description-category/attribute/values/search` | 按 `value` 模糊匹配参考值(≥2 字符,limit≤100) |
|
||||
| **发布/更新商品** | `POST /v3/product/import` | 一次 ≤100 个 item;返回 `task_id` |
|
||||
| **发布状态** | `POST /v1/product/import/info` | 入参 `task_id`;返回 `items[{offer_id, product_id, status, errors[]}]` |
|
||||
| 商品列表/回填 | `POST /v3/product/list` | 用 `offer_id/product_id` 过滤取 `product_id`,或分页拉全部 |
|
||||
| 跟卖复制 PDP | `POST /v1/product/import-by-sku` | 入参 `sku + name + offer_id + price...`;返回 `task_id + unmatched_sku_list` |
|
||||
| 图片更新 | `POST /v1/product/pictures/import` | 按 `product_id` 覆盖 `images/images360/color_image` |
|
||||
| 商品详情(含图片/审核错误) | `POST /v3/product/info/list` | 回读已发布商品的图片/状态/错误 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 类目与属性字典(采集属性 → Ozon 属性的关键)
|
||||
|
||||
### 3.1 数据流
|
||||
|
||||
```
|
||||
类目树(全局缓存)
|
||||
└─ 用户选类目 → 得 description_category_id + type_id
|
||||
└─ 拉该类目属性(按 category+type 缓存)
|
||||
└─ 对每个「有字典」的属性,按需拉值(/values 或 /values/search)
|
||||
```
|
||||
|
||||
- **类目树全局缓存**:与店铺无关(虽然接口要凭证),服务端拉一次存 `category_tree` 表 + 内存 LRU,TTL 24h。
|
||||
- **属性按类目缓存**:`category_attributes` 表,按 `(category_id, type_id)` 缓存。
|
||||
- **属性值按需拉取**:值目录可能非常大,只在用户映射到某个属性时才拉,且用 `/values/search`(按关键词搜)而非全量拉。
|
||||
|
||||
### 3.2 属性映射(采集的 `raw.params` → Ozon `attributes[]`)
|
||||
|
||||
这是发布链路**最重的工作**。流程:
|
||||
|
||||
```
|
||||
采集 raw.params: [{key:"Материал", value:"Нержавеющая сталь"}, …]
|
||||
│ ① 自动匹配:key 与属性 name 模糊匹配(归一化 + 词干)
|
||||
│ ② 有字典的属性:value 去 /values/search 找 dictionary_value_id
|
||||
▼
|
||||
属性映射 UI:自动匹配结果 + 人工确认未匹配项 + 必填项高亮
|
||||
▼
|
||||
products.attributes = [{complex_id:0, id, values:[{dictionary_value_id, value}]}]
|
||||
```
|
||||
|
||||
**必填项校验**:服务端在「ready 校验」和「发布前」两次校验:`category_attributes` 里 `is_required=true` 的属性必须已映射,否则阻断发布并指出缺哪些。
|
||||
|
||||
---
|
||||
|
||||
## 4. 发布请求体组装(对齐 ImportProductsV3)
|
||||
|
||||
`items[0]` 字段(已核对官方示例):
|
||||
|
||||
| 字段 | 来源 | 说明 |
|
||||
|---|---|---|
|
||||
| offer_id | `products.offer_id` | **自己的货号**,跟卖不能用竞品的 |
|
||||
| name / description | `products.name/description`(俄文) | |
|
||||
| description_category_id / type_id | `products.*` | 从类目树选 |
|
||||
| price / old_price / currency_code / vat | `products.*` | currency 须与店铺设置一致(默认 RUB) |
|
||||
| depth/width/height/dimension_unit/weight/weight_unit | `products.*` | **必填且不能为 0**(官方硬约束) |
|
||||
| barcode | `products.barcode` | 可选 |
|
||||
| images | `products.images`(七牛 URL,≤15) | 顺序即展示顺序;首张为主图。**必须 https 直链**(实测 Ozon 不接受 http,见 §7) |
|
||||
| primary_image | `products.primary_image` | 用 primary_image 则 images ≤14 |
|
||||
| images360 / color_image | `products.*` | 可选 |
|
||||
| attributes | `products.attributes` | 映射结果 |
|
||||
| complex_attributes | `products.complex_attributes` | 视频/尺码表等 |
|
||||
| pdf_list / promotions | 可选 | 一般留空 |
|
||||
|
||||
**跟卖场景可选优化**:若竞品允许复制 PDP,走 `/v1/product/import-by-sku`(只需 sku + 基本信息),更快且继承竞品详情——但受「卖家是否允许复制」限制,且不能更新,故作为**可选快捷路径**,主路径仍是 `import`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 发布状态机与轮询
|
||||
|
||||
`/v3/product/import` 是异步的,返回 `task_id`。流程:
|
||||
|
||||
```
|
||||
POST /v3/product/import → { task_id }
|
||||
│ 建 publish_tasks(status=pending, ozon_task_id)
|
||||
▼
|
||||
后台协程轮询 POST /v1/product/import/info { task_id }
|
||||
│ items[0].status ∈ imported | moderation | failed(+errors[])
|
||||
▼
|
||||
imported → products.stage=published, ozon_product_id=items[0].product_id
|
||||
moderation → products.stage=publishing(继续轮询,通常 <1 天)
|
||||
failed → products.stage=failed, publish_tasks.errors=items[0].errors
|
||||
```
|
||||
|
||||
- 轮询间隔:先 5s,退避到 30s;`moderation` 状态降低频率到分钟级。
|
||||
- 用 `/v3/product/list`(filter by offer_id)回读 `product_id` 兜底(轮询遗漏时)。
|
||||
- **上架还需设置库存**:`import` 成功后商品进入后台但不自动上架(`architecture.md` 与官方文档均明确「只有设置库存后才开售」)。V2 一期发布到「已创建/审核」即可,库存设置(`/v2/products/stocks`)作为二期可选,或提示用户去后台补库存。
|
||||
|
||||
---
|
||||
|
||||
## 6. CSV 导出字段
|
||||
|
||||
服务端 `GET /api/export/products.csv`,带 BOM 的 UTF-8,Excel 直接打开不乱码。字段:
|
||||
|
||||
```
|
||||
offer_id, product_id, name, description_category_id, type_id,
|
||||
price, old_price, currency_code, vat,
|
||||
weight, weight_unit, depth, width, height, dimension_unit,
|
||||
barcode, primary_image, images, source_platform, source_item_id, source_url,
|
||||
stage, published_at, created_at, updated_at
|
||||
```
|
||||
|
||||
- `images` 用 `|` 拼接七牛 URL。
|
||||
- 未发布商品 `product_id` 为空。
|
||||
- 支持筛选 `stage`(collected/ready/published/failed/全部)与 `ids`(勾选导出)。
|
||||
|
||||
> 对齐 V1:v1 登记表导出的 `sku + 卢布预留价` 组合码,V2 里 `offer_id + price`(卢布)即等价物;若要完全兼容 v1 组合码,可加一列 `combo`(`offer_id + 卢布预留价`)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理与限流
|
||||
|
||||
| Ozon 错误 | 含义 | 处理 |
|
||||
|---|---|---|
|
||||
| 400 Invalid parameter | 参数错误 | 把 detail 透传前端,定位字段 |
|
||||
| 403 Access denied | 权限不足 | 提示检查 Api-Key 权限级别 |
|
||||
| 409 Request conflict | 冲突(如 offer_id 重复) | 提示改 offer_id 或走更新 |
|
||||
| 429 / 限流 | 频率超限 | 指数退避重试 |
|
||||
| `item_limit_exceeded` | 超过当日建/更新商品限额 | 提示限额,可查 `/v4/product/info/limit` |
|
||||
|
||||
`publish_tasks.errors` 完整保存 Ozon 返回的 errors 数组,前端发布结果页展示中文解读。
|
||||
|
||||
---
|
||||
|
||||
## 8. 店铺绑定交互
|
||||
|
||||
1. 店铺管理页「新增店铺」:填 `名称 + Client ID + API Key + 结算币种`。
|
||||
2. 点「测试连接」→ `/api/shops/:id/test` → 调 `/v1/roles` → 显示 `ok` 与角色列表,或失败原因(凭证错/权限不足/网络)。
|
||||
3. 保存后 `client_id` 只显示尾号打码(如 `…1234`),key 永不回显。
|
||||
4. 发布时从店铺下拉选择目标店铺。
|
||||
|
||||
---
|
||||
|
||||
## 9. 一期范围 vs 二期
|
||||
|
||||
| 能力 | 一期 | 二期 |
|
||||
|---|---|---|
|
||||
| 店铺绑定 + 连通性校验 | ✅ | |
|
||||
| 类目树 + 属性 + 值字典(缓存) | ✅ | |
|
||||
| 属性映射 UI(自动 + 人工) | ✅ 基础版 | 智能匹配优化 |
|
||||
| `/v3/product/import` 发布 + 轮询回填 | ✅ | |
|
||||
| `/v1/product/import-by-sku` 跟卖复制 | 🟡 可选 | |
|
||||
| 库存设置(上架) | ❌(提示去后台) | `/v2/products/stocks` |
|
||||
| 价格/库存批量更新 | ❌ | `/v1/product/import/prices`、`/v2/products/stocks` |
|
||||
| 图片更新(换图) | ❌ | `/v1/product/pictures/import` |
|
||||
Reference in New Issue
Block a user