Files
ozon-seller-kit/docs/v2/database.md
T

11 KiB
Raw Blame History

V2 数据库设计

状态:方案设计(待确认) 上游:V2 总览 · V2 架构 字段来源:Ozon ProductAPI_ImportProductsV3 + V1 docs/contracts/product-json.md + 采集/发布/店铺维度


1. 设计原则

  1. 商品主表对齐 Ozon 字段products 表按 ImportProductsV3 的字段平铺(name/description/price/offer_id/...),JSONB 存三类结构:attributes / complex_attributesOzon 动态属性)、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 统一 UUIDgen_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": "…"
}

计价公式与字段沿用 v1web/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 分区或归档。