11 KiB
V2 数据库设计
状态:方案设计(待确认) 上游:V2 总览 · V2 架构 字段来源:Ozon
ProductAPI_ImportProductsV3+ V1docs/contracts/product-json.md+ 采集/发布/店铺维度
1. 设计原则
- 商品主表对齐 Ozon 字段:
products表按ImportProductsV3的字段平铺(name/description/price/offer_id/...),JSONB 存三类结构:attributes/complex_attributes(Ozon 动态属性)、raw(采集原文)、pricing(计价结果)。 - 素材与发布字段分离:采集来的源图(分组/变体/源站 URL/七牛 URL)放
product_assets表;products.images只存「将提交给 Ozon 的有序公网 URL 数组」。 - 店铺密钥加密落库:
shops.client_id_enc/api_key_enc用服务端密钥 AES-GCM 加密,前端永不回显明文。 - 发布异步化:发布请求与结果存
publish_tasks,商品状态由轮询结果回写。 - 类目字典可重建:
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)
{
"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)
{
"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)
[ { "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 分区或归档。