配套面试准备:完成本篇后,可以继续阅读:Day05_PIM+OMS核心业务面试准备 Day05_PIM+OMS商品订单管理系统完整面试指南
课程目标:完整实现商品信息从创建到上架的全生命周期管理,以及多平台订单从接入到完成的全链路闭环设计,掌握 SPU/SKU 两级商品体系、超卖防护、订单状态机等核心业务设计。
今日交付物:
- 商品模块 7 张数据库表完整 SQL
- SPU / SKU 两级商品体系设计
- 多语言内容管理方案
- 多平台多货币价格体系
- 商品状态机完整实现
- 订单模块 6 张数据库表完整 SQL
- 多平台订单聚合接入设计
- Redis 原子操作防超卖核心实现
- 订单 10 状态状态机完整设计
- 拆单 / 合单业务规则
- 售后退款逆向流程
- 两模块完整接口清单与错误码
第一节 商品管理业务全解析(PIM)
1.1 PIM 在系统中的定位
PIM(Product Information Management,商品信息管理)是整个供应链系统的”商品中台”,它不负责库存,只负责商品的描述性信息。
graph LR
subgraph PIM商品管理
A["📝 商品基础信息<br>名称/描述/图片/规格"]
B["💰 价格体系<br>多平台定价"]
C["🌍 多语言内容<br>中/英/日/韩"]
D["📊 HS编码/合规<br>清关信息"]
end
subgraph 依赖PIM的模块
E["WMS<br>sku_id 关联库存"]
F["OMS<br>sku_id 关联订单"]
G["PMS<br>sku_id 关联采购"]
H["TMS<br>物流系统<br>商品重量/尺寸"]
I["FMS<br>财务系统<br>商品成本/利润"]
end
A & B & C & D --> E & F & G & H & I
style A fill:#e8f5e9
style B fill:#e8f5e9
style C fill:#e8f5e9
style D fill:#e8f5e9
关键理解:PIM 是商品信息的”唯一真相来源”(Single Source of Truth)。所有其他模块需要商品名称、规格、图片时,都从 PIM 取,而不是各自维护一份。这样修改商品名称只需改一处,全系统生效。
1.2 跨境商品信息的特殊性
跨境场景下的商品信息,比国内电商复杂得多:
| 复杂维度 | 说明 | 举例 |
|---|---|---|
| 多语言标题 | 同一商品在不同市场显示不同语言 | 中文”蓝牙耳机”,英文”Bluetooth Earphone”,日文”ブルートゥースイヤホン” |
| 多货币定价 | 美国站标美元,欧洲站标欧元,日本站标日元 | 同款耳机:$29.99 / €27.99 / ¥3,999 |
| HS 海关编码 | 清关时必须申报商品品类编码 | 耳机的 HS Code:8518.30 |
| 合规认证要求 | 不同目的国有不同认证 | 美国 FCC、欧盟 CE、德国 GS |
| 尺寸重量精准 | 影响物流分类和运费计算 | 净重 85g,含包装 120g,盒装尺寸 15×8×5cm |
| 多平台类目映射 | 各平台类目体系不同 | 亚马逊:Electronics > Headphones;Shopee:电子产品 > 耳机耳麦 |
1.3 商品管理的参与角色
| 角色 | 职责 | 核心操作 |
|---|---|---|
| 运营专员 | 商品日常维护 | 创建商品、维护描述、调整价格、上下架 |
| 运营负责人 | 商品策略决策 | 审核高价值新品、批量价格调整审批 |
| 采购专员 | 成本信息维护 | 维护成本价、HS 编码、重量尺寸 |
| 设计师 | 视觉内容 | 上传商品图片、详情页设计图 |
第二节 商品数据库表设计
2.1 商品模块表结构总览
erDiagram
product_category {
bigint id PK
bigint parent_id "父分类ID"
varchar category_name "分类名称"
int level "层级深度"
varchar path "层级路径"
}
product_spu {
bigint id PK
bigint tenant_id
varchar spu_code "SPU编码"
varchar spu_name "商品名称"
bigint category_id FK
varchar brand "品牌"
varchar hs_code "HS海关编码"
tinyint status "状态"
}
product_sku {
bigint id PK
bigint spu_id FK
varchar sku_code "SKU编码"
varchar sku_name "完整SKU名"
json spec_values "规格值JSON"
decimal weight_g "重量克"
tinyint status "状态"
tinyint abc_class "ABC分类"
}
product_attr_template {
bigint id PK
bigint category_id FK
varchar attr_name "属性名"
tinyint attr_type "属性类型"
tinyint is_sku_spec "是否SKU规格项"
}
product_sku_price {
bigint id PK
bigint sku_id FK
tinyint price_type "价格类型"
varchar platform "平台"
varchar country_code "国家"
decimal price "价格"
varchar currency "货币"
}
product_i18n {
bigint id PK
bigint ref_id FK
varchar ref_type "关联类型SPU/SKU"
varchar lang_code "语言代码"
varchar title "标题"
text description "详情描述"
}
product_image {
bigint id PK
bigint spu_id FK
bigint sku_id FK
tinyint image_type "图片类型"
varchar image_url "图片URL"
int sort_order "排序"
}
product_category ||--o{ product_spu : "分类下有多个SPU"
product_spu ||--o{ product_sku : "一个SPU有多个SKU"
product_spu ||--o{ product_i18n : "多语言信息"
product_sku ||--o{ product_sku_price : "多平台定价"
product_spu ||--o{ product_image : "商品图片"
product_category ||--o{ product_attr_template : "分类属性模板"
2.2 商品分类表
-- ============================================================
-- 商品分类表(无限级树形结构)
-- 最多支持 5 级分类
-- 使用"路径枚举"模式,方便查询某分类的所有子分类
-- ============================================================
CREATE TABLE `product_category`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`parent_id` BIGINT NOT NULL DEFAULT 0 COMMENT '父分类ID,0表示顶级分类',
`category_name` VARCHAR(64) NOT NULL COMMENT '分类名称(中文)',
`category_name_en` VARCHAR(64) NULL COMMENT '分类名称(英文)',
`level` TINYINT NOT NULL DEFAULT 1 COMMENT '层级深度,从1开始,最大5',
`path` VARCHAR(256) NOT NULL DEFAULT '/' COMMENT '层级路径,如 /1/5/12/ 表示三级分类,方便查所有子分类',
`icon_url` VARCHAR(512) NULL COMMENT '分类图标URL',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '同级排序序号',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_tenant_parent` (`tenant_id`, `parent_id`) COMMENT '查询某分类的直接子分类',
KEY `idx_path` (`path`(128)) COMMENT '按路径前缀查询所有子分类'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '商品分类表(无限级树形)';
2.3 SPU 商品主表
-- ============================================================
-- SPU 商品标准单元主表
-- SPU(Standard Product Unit)= 一个商品的标准化描述
-- 如"蓝牙耳机Pro"是一个SPU,它有黑色/白色两个SKU
-- ============================================================
CREATE TABLE `product_spu`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID,雪花算法',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`spu_code` VARCHAR(32) NOT NULL COMMENT 'SPU编码,系统自动生成,格式 SPU-YYYYMMDD-XXXX',
`spu_name` VARCHAR(256) NOT NULL COMMENT '商品名称(主语言,中文)',
`category_id` BIGINT NOT NULL COMMENT '所属分类ID',
`category_path` VARCHAR(256) NOT NULL COMMENT '分类路径(冗余,如 /1/5/12/)',
`brand` VARCHAR(64) NULL COMMENT '品牌名称',
`hs_code` VARCHAR(16) NULL COMMENT 'HS 海关编码(6-10位数字,用于报关)',
`origin_country` CHAR(2) NULL COMMENT '原产地国家代码,如 CN/US',
`material` VARCHAR(128) NULL COMMENT '主要材质,如:ABS塑料、铝合金',
`certifications` JSON NULL COMMENT '产品认证列表,如 ["CE","FCC","RoHS"]',
`status` TINYINT NOT NULL DEFAULT 0
COMMENT '状态:0=草稿 1=待审核 2=已上架 3=已下架 4=已停售',
`publish_time` DATETIME NULL COMMENT '上架时间(定时上架时设置)',
`shelf_off_time` DATETIME NULL COMMENT '定时下架时间(促销结束自动下架)',
`spu_desc` TEXT NULL COMMENT '商品基础描述(中文,其他语言见 i18n 表)',
`package_desc` VARCHAR(512) NULL COMMENT '包装内容描述,如:耳机×1、充电线×1、说明书×1',
`remark` VARCHAR(512) NULL COMMENT '内部备注(不对外展示)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`update_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_spu_code` (`tenant_id`, `spu_code`),
KEY `idx_tenant_category` (`tenant_id`, `category_id`),
KEY `idx_tenant_status` (`tenant_id`, `status`),
KEY `idx_category_path` (`category_path`(128))
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = 'SPU商品标准单元表';
2.4 SKU 规格单元表
-- ============================================================
-- SKU 最小库存单元表
-- SKU(Stock Keeping Unit)= 一个可销售、可库存的最小单位
-- 如"蓝牙耳机Pro-黑色"是一个SKU
-- ============================================================
CREATE TABLE `product_sku`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`spu_id` BIGINT NOT NULL COMMENT '所属SPU ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码,自动生成或手动设置,全租户唯一',
`sku_name` VARCHAR(256) NOT NULL COMMENT 'SKU完整名称,如:蓝牙耳机Pro-黑色-无线版',
`barcode` VARCHAR(64) NULL COMMENT '条形码(EAN/UPC),仓库扫码用',
`fnsku` VARCHAR(64) NULL COMMENT 'FNSKU(亚马逊仓储条码,FBA必须)',
`spec_values` JSON NOT NULL DEFAULT '{}' COMMENT '规格组合JSON,如 {"颜色":"黑色","尺码":"XL"}',
`spec_values_en` JSON NULL COMMENT '英文规格,如 {"Color":"Black","Size":"XL"}',
-- 物理属性(物流和清关必需)
`net_weight_g` DECIMAL(10, 2) NULL COMMENT '净重(克),商品本身重量(不含包装)',
`gross_weight_g` DECIMAL(10, 2) NULL COMMENT '毛重(克),含包装重量,计算运费用',
`length_mm` DECIMAL(10, 2) NULL COMMENT '包装长度(毫米)',
`width_mm` DECIMAL(10, 2) NULL COMMENT '包装宽度(毫米)',
`height_mm` DECIMAL(10, 2) NULL COMMENT '包装高度(毫米)',
`is_battery` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否含电池:1=含电池(影响物流渠道选择,航空有限制)',
`is_liquid` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否液体:1=液体(影响物流渠道)',
`is_powder` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否粉末:1=粉末(影响物流渠道)',
-- 成本信息
`cost_price` DECIMAL(12, 4) NULL COMMENT '当前采购成本价(元),人工维护或从PMS同步',
`cost_currency` CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '成本价货币',
-- ABC 分类(仓储盘点用)
`abc_class` CHAR(1) NULL COMMENT 'ABC库存分类:A=高频 B=中频 C=低频,用于盘点频率',
`status` TINYINT NOT NULL DEFAULT 0
COMMENT 'SKU状态:0=草稿 1=已上架 2=已下架 3=已停售',
`remark` VARCHAR(256) NULL COMMENT '备注',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`update_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_sku_code` (`tenant_id`, `sku_code`),
KEY `idx_spu_id` (`spu_id`),
KEY `idx_tenant_status` (`tenant_id`, `status`),
KEY `idx_barcode` (`barcode`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = 'SKU最小库存单元表';
2.5 分类属性模板表
-- ============================================================
-- 分类属性模板表
-- 定义每个商品分类下有哪些属性(公共属性)和规格项(SKU维度属性)
-- 如:耳机分类 → 属性有[材质/连接方式/佩戴方式],规格有[颜色/型号]
-- ============================================================
CREATE TABLE `product_attr_template`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID(0=平台公共模板)',
`category_id` BIGINT NOT NULL COMMENT '所属分类ID',
`attr_name` VARCHAR(64) NOT NULL COMMENT '属性名称,如:颜色、材质、佩戴方式',
`attr_name_en` VARCHAR(64) NULL COMMENT '属性英文名',
`attr_type` TINYINT NOT NULL DEFAULT 1
COMMENT '属性类型:1=文本输入 2=单选(枚举) 3=多选 4=数字(含单位)',
`attr_options` JSON NULL COMMENT '枚举选项列表(type=2/3时使用),如 ["黑色","白色","蓝色"]',
`attr_unit` VARCHAR(16) NULL COMMENT '属性单位(type=4时使用),如 cm、g、mAh',
`is_sku_spec` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是SKU规格维度:1=是(会产生SKU组合)0=否(SPU公共属性)',
`is_required` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否必填:1=必填',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序序号',
`status` TINYINT NOT NULL DEFAULT 1,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_category_id` (`category_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '分类属性模板表';
2.6 SKU 价格表
-- ============================================================
-- SKU 多平台价格表
-- 同一 SKU 在不同平台、不同国家可以有不同的售价
-- ============================================================
CREATE TABLE `product_sku_price`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`sku_id` BIGINT NOT NULL COMMENT '关联SKU ID',
`price_type` TINYINT NOT NULL
COMMENT '价格类型:1=成本价 2=建议售价 3=平台售价 4=会员价 5=活动价',
`platform` VARCHAR(32) NULL
COMMENT '平台代码:ALL=通用 AMAZON=亚马逊 TIKTOK=TikTok Shop SHOPEE=Shopee TEMU=Temu SHOPIFY=独立站',
`country_code` CHAR(2) NULL COMMENT '国家代码,NULL表示该平台全部市场',
`price` DECIMAL(12, 4) NOT NULL COMMENT '价格数值',
`currency` CHAR(3) NOT NULL COMMENT '货币代码,如 USD/EUR/CNY/JPY',
`min_qty` INT NOT NULL DEFAULT 1 COMMENT '适用最小购买数量(阶梯定价用)',
`max_qty` INT NULL COMMENT '适用最大购买数量(NULL表示无上限)',
`effective_time` DATETIME NULL COMMENT '活动价生效时间',
`expire_time` DATETIME NULL COMMENT '活动价失效时间',
`is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否生效中',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_sku_id` (`sku_id`),
KEY `idx_sku_platform` (`sku_id`, `platform`, `country_code`, `price_type`)
COMMENT '查询某SKU某平台某国的价格'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = 'SKU多平台价格表';
2.7 多语言内容表与商品图片表
-- ============================================================
-- 商品多语言内容表
-- 存储商品标题、描述在不同语言下的内容
-- ============================================================
CREATE TABLE `product_i18n`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`ref_type` VARCHAR(16) NOT NULL COMMENT '关联类型:SPU 或 SKU',
`ref_id` BIGINT NOT NULL COMMENT '关联SPU或SKU的ID',
`lang_code` VARCHAR(8) NOT NULL
COMMENT '语言代码(BCP47标准):zh-CN=简体中文 en-US=英语 ja-JP=日语 ko-KR=韩语 ar=阿拉伯语 de-DE=德语 fr-FR=法语',
`title` VARCHAR(512) NULL COMMENT '商品标题(该语言)',
`subtitle` VARCHAR(256) NULL COMMENT '副标题 / 卖点短语(5个以内卖点)',
`bullet_points` JSON NULL COMMENT '卖点列表(亚马逊Bullet Points),如["长续航","Hi-Fi音质"]',
`description` TEXT NULL COMMENT '商品详情描述(该语言,支持富文本HTML)',
`keywords` VARCHAR(1024) NULL COMMENT '搜索关键词,逗号分隔',
`search_terms` VARCHAR(1024) NULL COMMENT '平台后台搜索词(亚马逊Search Terms)',
`is_ai_translated` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否由AI翻译(非人工校对,内容质量可能低)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ref_lang` (`ref_type`, `ref_id`, `lang_code`) COMMENT '同一商品同一语言只有一条记录',
KEY `idx_ref_id` (`ref_id`),
KEY `idx_lang_code` (`lang_code`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '商品多语言内容表';
-- ============================================================
-- 商品图片表
-- ============================================================
CREATE TABLE `product_image`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`spu_id` BIGINT NOT NULL COMMENT '关联SPU ID',
`sku_id` BIGINT NULL COMMENT '关联SKU ID(规格图时需要,主图可为NULL)',
`image_type` TINYINT NOT NULL
COMMENT '图片类型:1=主图(封面) 2=轮播图 3=详情图 4=SKU规格图(如不同颜色的图)5=尺码表',
`image_url` VARCHAR(512) NOT NULL COMMENT '图片存储URL(OSS地址)',
`thumb_url` VARCHAR(512) NULL COMMENT '缩略图URL(自动生成的小尺寸图)',
`image_width` INT NULL COMMENT '图片宽度(像素),方便前端展示计算',
`image_height` INT NULL COMMENT '图片高度(像素)',
`file_size` BIGINT NULL COMMENT '文件大小(字节)',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '同类型图片排序,0最靠前',
`alt_text` VARCHAR(128) NULL COMMENT '图片 alt 文字(有助于平台SEO)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_spu_id` (`spu_id`),
KEY `idx_sku_id` (`sku_id`),
KEY `idx_image_type` (`image_type`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '商品图片表';
第三节 SPU 与 SKU 核心业务
3.1 SPU 与 SKU 的本质区别
这是电商系统中最基础也最容易混淆的概念,必须彻底搞清楚。
graph TD
A["SPU:蓝牙耳机Pro<br>(标准产品单元)<br>描述这款耳机的共性属性<br>品牌/材质/连接方式/HS编码"] --> B["SKU-001<br>黑色 + 无线版<br>sku_code: SKU-EARPHONE-BK-W"]
A --> C["SKU-002<br>白色 + 无线版<br>sku_code: SKU-EARPHONE-WH-W"]
A --> D["SKU-003<br>黑色 + 有线版<br>sku_code: SKU-EARPHONE-BK-C"]
B --> E["独立库存<br>WMS单独管理黑色无线款库存"]
C --> F["独立库存<br>WMS单独管理白色无线款库存"]
D --> G["独立库存<br>WMS单独管理黑色有线款库存"]
B --> H["独立定价<br>黑色无线款:$29.99"]
C --> I["独立定价<br>白色无线款:$31.99(白色溢价)"]
D --> J["独立定价<br>黑色有线款:$19.99(有线更便宜)"]
一句话理解:
- SPU = 你去商场问导购”有没有蓝牙耳机Pro?“——导购能找到对应的产品区域
- SKU = 你问导购”要黑色的无线版”——导购从货架上精确取出那个具体商品
3.2 SKU 自动生成机制
当运营专员为一个 SPU 配置规格属性时,系统自动计算笛卡尔积生成所有 SKU 组合:
flowchart TD
A["运营专员配置规格"] --> B["规格项1:颜色<br>选项:黑色、白色、蓝色"]
A --> C["规格项2:版本<br>选项:无线版、有线版"]
B & C --> D["系统计算笛卡尔积<br>3种颜色 × 2种版本 = 6个SKU"]
D --> E["SKU-001:黑色-无线版"]
D --> F["SKU-002:黑色-有线版"]
D --> G["SKU-003:白色-无线版"]
D --> H["SKU-004:白色-有线版"]
D --> I["SKU-005:蓝色-无线版"]
D --> J["SKU-006:蓝色-有线版"]
E & F & G & H & I & J --> K["运营专员逐个SKU填写:<br>重量/尺寸/成本价/条形码"]
K --> L["批量操作:<br>可以选中多个SKU一次性填写相同值<br>如所有SKU的毛重都是120g"]
spec_values 字段的 JSON 结构:
{
"颜色": "黑色",
"版本": "无线版"
}
英文版(用于海外平台):
{
"Color": "Black",
"Type": "Wireless"
}
3.3 SKU 编码设计规则
SKU 编码规则(可自定义,也可系统生成):
格式:{SPU代码}-{规格缩写}-{序号}
示例:
SPU: EARPHONE-PRO
黑色无线版 → EARPHONE-PRO-BKW-001
白色有线版 → EARPHONE-PRO-WHC-002
系统自动生成(无自定义时):
格式:{SPU_ID前8位}-{4位序号}
如:18742912-0001,18742912-0002
条形码(barcode):
国际通用 EAN-13(13位数字)或 UPC-A(12位数字)
由商家自行申请或在系统中录入
FNSKU(亚马逊仓储编码):
格式:X00XXXXXXX(10位,X开头+9位字母数字)
由亚马逊 Seller Central 生成,每个 SKU+ASIN 组合唯一
3.4 商品品类与属性模板的关系
flowchart TD
A["运营选择商品分类:<br>电子产品 > 音频设备 > 耳机"] --> B["系统加载该分类的属性模板"]
B --> C["公共属性(SPU级别,非规格项):<br>✏️ 连接方式(单选:蓝牙/有线/无线)<br>✏️ 电池容量(数字 + mAh)<br>✏️ 防水等级(单选:IPX4/IPX5/IPX7)<br>✏️ 包含功能(多选:主动降噪/通话/游戏模式)"]
B --> D["SKU规格项(会产生SKU组合):<br>🔴 颜色(枚举:黑色/白色/蓝色/红色)<br>🔴 版本(枚举:无线版/有线版)"]
C --> E["填写公共属性值<br>所有SKU共享这些属性"]
D --> F["选择规格组合<br>系统自动生成SKU列表"]
第四节 多语言内容管理
4.1 为什么需要多语言
跨境电商的用户分布在全球各地,亚马逊美国站的用户看英文,日本站的用户看日文,德国站的用户看德文。如果商品标题是中文,美国用户根本看不懂,商品排名和转化率极低。
多语言内容的优先级:
graph LR
A["系统展示商品标题"] --> B{"是否有目标语言内容?"}
B -- "有 zh-CN 内容" --> C["显示中文内容"]
B -- "有 en-US 内容" --> D["显示英文内容"]
B -- "无该语言" --> E{"是否有默认语言内容?"}
E -- "有默认(zh-CN)" --> F["显示中文内容<br>并标注:未完成翻译"]
E -- "无任何内容" --> G["显示 SKU 编码<br>兜底处理"]
4.2 AI 辅助翻译流程
flowchart TD
A["运营专员填写中文内容:<br>标题/卖点/描述"] --> B["点击'AI一键翻译'按钮"]
B --> C["选择目标语言:<br>☑ 英语(en-US)<br>☑ 日语(ja-JP)<br>☑ 韩语(ko-KR)"]
C --> D["系统调用 AI 大模型 API<br>(DeepSeek/OpenAI)"]
D --> E{"翻译质量控制 Prompt:<br>你是专业的跨境电商文案翻译员,<br>将以下中文商品信息翻译为{目标语言},<br>保持营销语气,符合当地消费者习惯,<br>标题不超过200个字符..."}
E --> F["AI 返回翻译结果"]
F --> G["系统将结果写入 product_i18n<br>is_ai_translated = 1(标记为AI翻译)"]
G --> H["运营专员人工校对<br>修改不准确的内容"]
H --> I["确认后更新 is_ai_translated = 0<br>(标记为人工已校对)"]
AI 翻译的 Prompt 设计示例:
系统提示词(System Prompt):
你是一位专业的跨境电商商品文案翻译和本地化专家,
精通中英日韩等多国语言和跨境电商平台规范。
用户请求(User Prompt):
请将以下中文商品信息翻译为英语(美国市场),要求:
1. 标题:简洁有力,不超过200字符,包含核心关键词
2. 卖点(Bullet Points):5条,每条不超过100字符,以动词开头
3. 描述:200-300字,突出产品价值,适合亚马逊详情页
商品信息:
标题:【2025新款】无线蓝牙耳机 主动降噪 高音质 长续航 适用苹果安卓
卖点:
- 采用最新蓝牙5.3技术,连接稳定,延迟低至30ms
- 主动降噪技术,有效消除环境噪音高达35dB
- 单次续航12小时,配合充电盒可使用48小时
- 支持快充,10分钟充电可使用2小时
- 通用接口,兼容iPhone和安卓设备
请直接返回翻译结果,格式为JSON。
4.3 多语言内容完整性检查
-- 查询哪些商品的多语言内容不完整(缺少英文内容)
-- 上架到亚马逊的商品必须有英文内容
SELECT
s.spu_code,
s.spu_name,
s.status,
COUNT(CASE WHEN i.lang_code = 'en-US' AND i.title IS NOT NULL THEN 1 END) AS has_en_title,
COUNT(CASE WHEN i.lang_code = 'en-US' AND i.description IS NOT NULL THEN 1 END) AS has_en_desc,
COUNT(CASE WHEN i.lang_code = 'ja-JP' THEN 1 END) AS has_ja,
COUNT(CASE WHEN i.lang_code = 'ko-KR' THEN 1 END) AS has_ko
FROM product_spu s
LEFT JOIN product_i18n i ON s.id = i.ref_id AND i.ref_type = 'SPU' AND i.is_deleted = 0
WHERE s.tenant_id = #{tenantId} AND s.is_deleted = 0
GROUP BY s.id, s.spu_code, s.spu_name, s.status
HAVING has_en_title = 0 -- 没有英文标题的商品
ORDER BY s.create_time DESC;
第五节 价格体系设计
5.1 跨境电商的多维度定价
跨境卖家的定价策略远比国内复杂,需要考虑多个维度:
graph TD
A["最终售价确定"] --> B["成本价<br>采购成本(含税)"]
A --> C["目标利润率<br>通常25%-40%"]
A --> D["平台佣金<br>亚马逊15% / TikTok8%"]
A --> E["物流成本<br>首重+续重+关税"]
A --> F["汇率因素<br>CNY换算USD/EUR"]
A --> G["市场竞争<br>对手定价参考"]
A --> H["平台活动<br>Prime Day折扣要求"]
B & C & D & E & F & G & H --> I["综合决策<br>确定各市场最终价格"]
最低售价保护计算公式:
最低可售价(美国亚马逊)=
(采购成本 × (1 + 采购税率) + 头程物流费)/ 汇率
÷ (1 - 平台佣金率) / (1 - 目标利润率)
示例:
采购成本:35元(含税)
头程物流费:15元(分摊到每件)
汇率:7.2 (USD/CNY)
亚马逊佣金率:15%
目标利润率:25%
最低可售价 = (35 + 15) / 7.2 ÷ (1 - 15%) ÷ (1 - 25%)
= 50 / 7.2 / 0.85 / 0.75
= 6.94 / 0.85 / 0.75
= $10.89
建议售价:在最低价基础上再上浮10%-20%留出降价促销空间
→ $11.99 - $12.99
5.2 价格查询优先级规则
当前端需要展示某商品某平台的价格时,按以下优先级查询:
flowchart TD
A["查询 SKU 在 亚马逊美国站 的售价"] --> B["第1优先:活动价<br>price_type=5<br>platform=AMAZON, country=US<br>且当前时间在 effective_time 到 expire_time 内"]
B --> C{"有有效活动价?"}
C -- 有 --> D["使用活动价 ✅"]
C -- 无 --> E["第2优先:平台国家售价<br>price_type=3<br>platform=AMAZON, country=US"]
E --> F{"有该平台该国售价?"}
F -- 有 --> G["使用平台国家售价 ✅"]
F -- 无 --> H["第3优先:平台通用售价<br>price_type=3<br>platform=AMAZON, country=NULL"]
H --> I{"有该平台通用售价?"}
I -- 有 --> J["使用平台通用售价 ✅"]
I -- 无 --> K["第4优先:建议售价<br>price_type=2,platform=ALL"]
K --> L["兜底显示建议售价"]
5.3 汇率同步机制
flowchart TD
A(["定时任务:每天 00:30 执行"]) --> B["调用汇率 API<br>(OpenExchangeRates / 国家银行接口)"]
B --> C["获取最新汇率<br>以 CNY 为基准的所有货币汇率"]
C --> D["写入汇率表<br>exchange_rate<br>记录日期+币种+汇率值"]
D --> E{"与前一日汇率对比<br>波动是否超过2%?"}
E -- 是 --> F["发送汇率大幅波动告警<br>通知财务专员关注"]
E -- 否 --> G["正常更新,无需告警"]
F & G --> H["更新所有以外币定价的商品<br>重新计算 CNY 等值价格(展示用)"]
H --> I(["定时任务结束"])
第六节 商品状态机与上下架
6.1 商品状态机
stateDiagram-v2
[*] --> 草稿 : 运营创建商品
草稿 --> 待审核 : 提交审核(高价值商品)
草稿 --> 已上架 : 直接上架(无需审核)
待审核 --> 已上架 : 审核通过
待审核 --> 草稿 : 审核拒绝,退回修改
已上架 --> 已下架 : 手动下架 / 定时下架 / 自动下架
已下架 --> 已上架 : 重新上架
已下架 --> 已停售 : 永久停止销售
已停售 --> [*] : 归档
草稿 --> [*] : 删除(逻辑删除)
6.2 自动下架触发规则
flowchart TD
A(["定时任务:每30分钟执行"]) --> B["检查所有已上架商品"]
B --> C{"是否满足任意自动下架条件?"}
C --> D["条件1:所有仓库的可售库存 = 0<br>AND 租户配置了零库存自动下架"]
C --> E["条件2:当前时间 > shelf_off_time<br>(定时下架时间已到)"]
C --> F["条件3:关联的产品认证(CE/FCC)已过期<br>继续销售可能违规"]
D -- 满足 --> G["执行自动下架<br>status = 3(已下架)"]
E -- 满足 --> G
F -- 满足 --> G
G --> H["发送下架通知:<br>站内信通知运营专员<br>注明下架原因"]
H --> I["记录下架操作日志"]
6.3 商品上架前完整性校验
提交上架前,系统检查以下必填项,任意一项不满足则阻止上架:
| 校验项 | 验证规则 | 错误提示 |
|---|---|---|
| 商品主图 | 必须有至少 1 张主图 | ”请上传至少1张主图” |
| 英文标题 | 如商品需要发布到英文市场,en-US 标题必填 | ”请填写英文标题” |
| SKU 数量 | SPU 下至少有 1 个已上架的 SKU | ”至少有1个SKU才能上架” |
| SKU 重量 | 每个 SKU 的毛重必须填写 | ”SKU重量未填写,无法计算运费” |
| HS 编码 | 若销售目的地需要清关,HS Code 必填 | ”跨境销售必须填写HS编码” |
| 售价 | 每个 SKU 必须有至少 1 条有效价格 | ”请设置商品售价” |
| 成本价 | 成本价不能为 0 或空 | ”成本价为空,无法计算利润” |
我们这里的 PIM 不是电商平台前台商品系统,而是卖家的商品主数据中心。 它负责维护 SPU/SKU、成本价、平台售价、多语言资料、平台映射、库存同步和发布状态。 真正展示给消费者的是 Amazon、TikTok、Shopee; 我们的系统负责把正确的商品资料和库存价格同步过去,并在供应链风险出现时提醒或触发上下架。
供应链系统可以做:
- 维护商品主数据
- 维护多平台价格
- 计算最低售价
- 同步平台价格和库存
- 标记内部可售/停售状态
- 发起平台上架/下架请求
- 记录平台发布结果
供应链系统不应该做:
- 替代 Amazon/TikTok/Shopee 的消费者前台
- 绕过平台审核直接控制平台商品
- 自己决定平台最终是否展示商品
- 随意干涉卖家的运营策略
第七节 订单管理业务全解析(OMS)
7.1 OMS 在系统中的核心地位
OMS(Order Management System)是整个 SaaS 平台的业务中枢,是驱动所有其他模块联动的核心:
graph TD
subgraph 上游: 订单来源
A1["亚马逊 Amazon"]
A2["TikTok Shop"]
A3["Shopee"]
A4["Temu"]
A5["Shopify 独立站"]
end
OMS["📋 OMS 订单管理<br>统一接入 / 聚合管理<br>今日核心"]
subgraph 下游: 驱动执行
D1["WMS 仓储<br>库存扣减 + 拣货出库"]
D2["TMS 物流<br>创建运单 + 面单打印"]
D3["FMS 财务<br>收款对账 + 利润核算"]
end
A1 & A2 & A3 & A4 & A5 --> OMS
OMS --> D1 & D2 & D3
style OMS fill:#fff3e0,stroke:#ff9800,stroke-width:3px
7.2 跨平台订单的挑战
不同平台的订单格式和字段定义完全不同,OMS 必须做标准化处理:
我们在编码的时候 要实现一个通用的适配器 当我们在不同个电商平台 拉取到了订单的信息后 要先把这个订单的信息 经过适配器 进行数据的格式化 再把格式化后的数据 存储到我们自己的数据中
| 字段 | 亚马逊 | TikTok Shop | Shopee | 我们的标准字段 |
|---|---|---|---|---|
| 订单号 | AmazonOrderId | order_id | ordersn | platform_order_no |
| 商品标识 | ASIN + SKU | product_id | item_id | 映射为我们的 sku_id |
| 买家收货地址 | ShippingAddress | recipient_address | recipient_address | shipping_address |
| 订单状态 | OrderStatus | order_status | order_status | 统一映射为内部状态 |
| 支付金额 | OrderTotal | payment_amount | total_amount | payment_amount |
| 货币 | CurrencyCode | currency | currency | currency |
| 平台手续费 | 从结算报告获取 | commission_fee | commission_fee | platform_fee |
7.3 OMS 业务核心挑战
graph TD
A["OMS 要解决的核心挑战"] --> B["1️⃣ 超卖防护<br>智能库存配置建议<br>多平台同时接单<br>同一SKU库存不能超卖"]
A --> C["2️⃣ 数据幂等<br>同一订单可能被推送两次<br>不能重复创建"]
A --> D["3️⃣ 状态同步<br>发货后要回传物流单号<br>平台才能更新状态"]
A --> E["4️⃣ 异常处理<br>地址无效/库存不足/支付失败<br>各类异常的处理方案"]
A --> F["5️⃣ 售后管理<br>退款退货/换货/仅退款<br>逆向物流处理"]
第八节 订单数据库表设计
8.1 订单模块表结构总览
erDiagram
order_main {
bigint id PK
bigint tenant_id
varchar order_no "内部订单号"
varchar platform_order_no "平台订单号"
varchar platform "来源平台"
tinyint status "订单状态0-10"
decimal payment_amount "支付金额"
varchar currency "货币"
}
order_item {
bigint id PK
bigint order_id FK
bigint sku_id FK
int quantity "购买数量"
decimal unit_price "销售单价"
decimal amount "小计"
}
order_address {
bigint id PK
bigint order_id FK
varchar receiver_name "收件人"
varchar country_code "国家"
varchar full_address "完整地址"
varchar phone "电话"
}
order_log {
bigint id PK
bigint order_id FK
tinyint from_status "变更前状态"
tinyint to_status "变更后状态"
varchar action "操作说明"
bigint operator_id "操作人"
}
order_refund {
bigint id PK
bigint order_id FK
varchar refund_no "退款单号"
tinyint refund_type "退款类型"
decimal refund_amount "退款金额"
tinyint status "退款状态"
}
order_platform_raw {
bigint id PK
bigint order_id FK
varchar platform "平台"
text raw_data "原始JSON数据"
}
order_main ||--o{ order_item : "含多个商品明细"
order_main ||--|| order_address : "一个收货地址"
order_main ||--o{ order_log : "多条操作日志"
order_main ||--o{ order_refund : "可有多条退款"
order_main ||--|| order_platform_raw : "原始平台数据"
8.2 订单主表
-- ============================================================
-- 订单主表
-- 每一笔销售订单(无论来自哪个平台)都在这里记录
-- ============================================================
CREATE TABLE `order_main`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID,雪花算法(内部ID)',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`order_no` VARCHAR(32) NOT NULL COMMENT '内部订单号,格式 ORD-YYYYMMDD-XXXX,全系统唯一',
`platform` VARCHAR(32) NOT NULL
COMMENT '来源平台:AMAZON/TIKTOK/SHOPEE/TEMU/SHOPIFY/MANUAL(手动创建)',
`platform_order_no` VARCHAR(128) NOT NULL COMMENT '平台侧订单号,如亚马逊的 Amazon Order ID',
`store_id` BIGINT NULL COMMENT '关联店铺ID(同一租户可有多个店铺)',
-- 商品与金额
`total_amount` DECIMAL(12, 2) NOT NULL COMMENT '订单总金额(商品金额合计)',
`discount_amount` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '优惠金额(平台优惠/折扣码)',
`shipping_fee` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '买家支付的运费',
`payment_amount` DECIMAL(12, 2) NOT NULL COMMENT '买家实际支付金额 = total_amount - discount_amount + shipping_fee',
`currency` CHAR(3) NOT NULL COMMENT '订单货币,如USD/EUR/JPY',
`exchange_rate` DECIMAL(10, 6) NOT NULL DEFAULT 1 COMMENT '当时汇率(折算CNY用)',
`cny_amount` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '折合人民币金额 = payment_amount × exchange_rate',
`platform_fee` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '平台手续费(从结算报告同步)',
-- 状态
`status` TINYINT NOT NULL DEFAULT 0
COMMENT '订单状态:0=待处理 1=风控审核 2=待备货 3=备货中 4=待发货 5=已发货 6=运输中 7=已签收 8=已完成 9=售后中 10=已取消',
`cancel_reason` VARCHAR(256) NULL COMMENT '取消原因',
`is_abnormal` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否标记为异常订单:1=异常(需人工处理)',
`abnormal_reason` VARCHAR(256) NULL COMMENT '异常原因描述',
-- 仓库与物流
`warehouse_id` BIGINT NULL COMMENT '发货仓库ID(分配后填入)',
`logistics_channel` VARCHAR(64) NULL COMMENT '物流渠道(如顺丰、EMS、DHL)',
`waybill_no` VARCHAR(128) NULL COMMENT '物流运单号',
`ship_time` DATETIME NULL COMMENT '实际发货时间',
`delivery_deadline` DATE NULL COMMENT '平台要求的最晚发货日期(超期处罚)',
`signed_time` DATETIME NULL COMMENT '签收时间',
-- 平台信息
`platform_order_time` DATETIME NOT NULL COMMENT '买家在平台下单时间',
`platform_pay_time` DATETIME NULL COMMENT '买家在平台支付时间',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`update_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_order_no` (`tenant_id`, `order_no`),
UNIQUE KEY `uk_platform_order_no` (`tenant_id`, `platform`, `platform_order_no`) COMMENT '防止同一平台同一订单号重复导入(幂等键)',
KEY `idx_tenant_status` (`tenant_id`, `status`),
KEY `idx_warehouse_id` (`warehouse_id`),
KEY `idx_platform_order_time` (`platform_order_time`),
KEY `idx_delivery_deadline` (`delivery_deadline`) COMMENT '查询即将超期的订单'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '订单主表';
8.3 订单明细与收货地址表
-- 订单明细表
CREATE TABLE `order_item`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`order_id` BIGINT NOT NULL COMMENT '关联订单ID',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单号(冗余,方便查询)',
`sku_id` BIGINT NOT NULL COMMENT '我们系统的SKU ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码(冗余)',
`sku_name` VARCHAR(256) NOT NULL COMMENT 'SKU名称(下单时快照,防止后续修改影响历史)',
`platform_sku_id` VARCHAR(128) NULL COMMENT '平台侧SKU标识(如亚马逊的ASIN)',
`quantity` INT NOT NULL COMMENT '购买数量',
`unit_price` DECIMAL(12, 4) NOT NULL COMMENT '下单时的单价(快照)',
`discount` DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '该商品的优惠金额',
`amount` DECIMAL(12, 2) NOT NULL COMMENT '小计 = unit_price × quantity - discount',
`currency` CHAR(3) NOT NULL COMMENT '货币',
`refunded_qty` INT NOT NULL DEFAULT 0 COMMENT '已退款数量(部分退款时使用)',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_sku_id` (`sku_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '订单明细表';
-- 收货地址表
CREATE TABLE `order_address`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`order_id` BIGINT NOT NULL COMMENT '关联订单ID',
`receiver_name` VARCHAR(128) NOT NULL COMMENT '收件人姓名',
`phone` VARCHAR(32) NULL COMMENT '收件人电话',
`email` VARCHAR(128) NULL COMMENT '收件人邮箱',
`country_code` CHAR(2) NOT NULL COMMENT '国家代码,如US/GB/DE/JP',
`country_name` VARCHAR(64) NULL COMMENT '国家名称',
`state` VARCHAR(64) NULL COMMENT '州/省',
`city` VARCHAR(64) NULL COMMENT '城市',
`address_line1` VARCHAR(256) NOT NULL COMMENT '地址第一行(街道+门牌号)',
`address_line2` VARCHAR(256) NULL COMMENT '地址第二行(公寓号/单元号)',
`zip_code` VARCHAR(16) NOT NULL COMMENT '邮政编码',
`full_address` VARCHAR(512) NOT NULL COMMENT '完整地址拼接(用于展示和打单)',
`is_verified` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '地址是否已验证:1=有效地址',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_id` (`order_id`) COMMENT '一个订单只有一个收货地址'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '订单收货地址表';
8.4 订单操作日志与退款表
-- 订单操作日志表(只增不改不删)
CREATE TABLE `order_log`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`order_id` BIGINT NOT NULL COMMENT '关联订单ID',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单号',
`from_status` TINYINT NULL COMMENT '变更前状态(NULL表示首次创建)',
`to_status` TINYINT NOT NULL COMMENT '变更后状态',
`action` VARCHAR(128) NOT NULL COMMENT '操作说明,如:系统自动同步/运营审核通过/确认发货',
`operator_type` TINYINT NOT NULL DEFAULT 1 COMMENT '操作人类型:1=系统自动 2=内部用户 3=平台回调',
`operator_id` BIGINT NULL COMMENT '操作人用户ID(系统操作时为NULL)',
`operator_name` VARCHAR(64) NULL COMMENT '操作人姓名',
`remark` VARCHAR(512) NULL COMMENT '操作备注',
`operate_time` DATETIME NOT NULL COMMENT '操作时间',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_operate_time` (`operate_time`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '订单操作日志表(只增不改不删)';
-- 退款单表
CREATE TABLE `order_refund`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`refund_no` VARCHAR(32) NOT NULL COMMENT '退款单号,格式 REF-YYYYMMDD-XXXX',
`order_id` BIGINT NOT NULL COMMENT '关联订单ID',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单号',
`platform_refund_no` VARCHAR(128) NULL COMMENT '平台侧退款单号',
`refund_type` TINYINT NOT NULL
COMMENT '退款类型:1=仅退款(无需退货)2=退货退款 3=换货 4=平台强制退款',
`refund_reason` VARCHAR(32) NOT NULL COMMENT '退款原因分类',
`reason_detail` VARCHAR(512) NULL COMMENT '详细原因说明',
`refund_amount` DECIMAL(12, 2) NOT NULL COMMENT '申请退款金额',
`actual_refund_amount` DECIMAL(12, 2) NULL COMMENT '实际退款金额(审核后可调整)',
`currency` CHAR(3) NOT NULL COMMENT '货币',
`status` TINYINT NOT NULL DEFAULT 0
COMMENT '状态:0=待审核 1=审核通过(等待退货) 2=已收到退货 3=退款完成 4=审核拒绝 5=已关闭',
`apply_time` DATETIME NOT NULL COMMENT '退款申请时间',
`audit_time` DATETIME NULL COMMENT '审核时间',
`complete_time` DATETIME NULL COMMENT '退款完成时间',
`evidence_urls` JSON NULL COMMENT '证据图片列表(买家上传)',
`return_tracking_no` VARCHAR(128) NULL COMMENT '退货运单号',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_refund_no` (`tenant_id`, `refund_no`),
KEY `idx_order_id` (`order_id`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '退款单表';
-- 平台原始数据存储表(用于调试和对账)
CREATE TABLE `order_platform_raw`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`order_id` BIGINT NOT NULL COMMENT '关联订单ID(解析后关联)',
`platform` VARCHAR(32) NOT NULL COMMENT '平台代码',
`platform_order_no` VARCHAR(128) NOT NULL COMMENT '平台订单号',
`raw_data` MEDIUMTEXT NOT NULL COMMENT '平台原始 JSON 数据(完整保存,方便排查问题)',
`sync_time` DATETIME NOT NULL COMMENT '同步时间',
`sync_type` TINYINT NOT NULL DEFAULT 1 COMMENT '同步方式:1=定时拉取 2=Webhook实时推送',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_platform_order_no` (`platform_order_no`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '平台订单原始数据表(只增不改)';
第九节 多平台订单接入
9.1 订单接入整体架构
flowchart TD
subgraph 主动拉取-定时轮询
A1["定时任务<br>每5分钟执行"] --> A2["调用亚马逊 SP-API<br>GetOrders 接口"]
A1 --> A3["调用 Shopee Open API<br>GetEscrowOrders 接口"]
A2 & A3 --> A4["按时间范围拉取<br>新增和更新的订单"]
end
subgraph 被动接收-Webhook实时推送
B1["TikTok Shop<br>Webhook 推送"] --> B2["POST /webhook/tiktok<br>系统接收推送"]
B3["Shopify<br>Webhook 推送"] --> B4["POST /webhook/shopify<br>系统接收推送"]
end
subgraph 统一处理层
C["订单接入适配层<br>PlatformOrderAdapter"]
C --> D["格式转换<br>各平台字段 → 内部标准字段"]
D --> E["幂等校验<br>UK(tenant_id, platform, platform_order_no)<br>已存在则跳过"]
E --> F{"是新订单?"}
F -- 是 --> G["写入 order_platform_raw<br>保存原始数据"]
G --> H["解析并创建 order_main<br>order_item<br>order_address"]
H --> I["写入 order_log<br>操作:系统自动同步"]
I --> J["推送异步处理消息<br>触发后续业务逻辑"]
F -- "否(已存在)" --> K["检查状态是否有更新<br>如平台订单从待付款→已付款"]
K --> L["更新订单状态<br>写入 order_log"]
end
A4 & B2 & B4 --> C
9.2 亚马逊订单对接详解
亚马逊是对接最复杂的平台,需要处理 LWA(Login With Amazon)OAuth 授权:
sequenceDiagram
participant System as 我们的系统
participant LWA as 亚马逊 LWA 授权服务
participant SPAPI as 亚马逊 SP-API
Note over System: 首次授权(卖家操作)
System->>LWA: 引导卖家到亚马逊授权页面
LWA->>System: 回调 redirect_uri,附带 code
System->>LWA: 用 code 换取 refresh_token
LWA->>System: 返回 refresh_token(永久保存)
Note over System: 每次 API 调用前
System->>LWA: 用 refresh_token 换取 access_token
LWA->>System: 返回 access_token(有效期1小时)
Note over System: 正常业务调用
System->>SPAPI: GET /orders/v0/orders<br>Header: x-amz-access-token: {access_token}
SPAPI->>System: 返回订单列表 JSON
Note over System: Token 过期自动刷新
System->>SPAPI: API 调用返回 403
System->>LWA: 自动刷新 access_token
LWA->>System: 新的 access_token
System->>SPAPI: 重新调用成功
亚马逊订单拉取关键参数:
GET /orders/v0/orders
参数:
MarketplaceIds: ATVPDKIKX0DER(美国站)
CreatedAfter: 2025-01-17T00:00:00Z(上次同步时间)
OrderStatuses: Unshipped,PartiallyShipped,Shipped
MaxResultsPerPage: 100(每次最多返回100条)
NextToken: {上次返回的分页Token}(分页用)
注意事项:
1. 不能全量拉取,必须有时间范围(CreatedAfter)
2. 一次最多返回100条,需要用 NextToken 循环翻页
3. 拉取间隔不能太频繁,亚马逊有限流(Throttle)机制
4. 订单状态更新也需要拉取(如已发货→已签收)
9.3 Webhook 接收与签名验证
TikTok Shop 和 Shopify 使用 Webhook 主动推送订单,系统必须验证签名防止伪造。
为什么需要签名验证:
Webhook 接口是公开的(有固定 URL),任何人都可以伪造请求。如果不验证签名,恶意用户可以伪造大量订单,导致系统混乱。
签名验证原理:
-
平台生成签名(发送方)
- 平台用 App Secret 对请求体进行 HMAC-SHA256 加密
- 得到签名字符串(如:a3f5b2c8d1e9…)
- 把签名放到请求头:X-Shopify-Hmac-SHA256
-
我们验证签名(接收方)
- 提取请求头中的签名:received_signature(收到的签名)
- 用相同的 App Secret 对请求体进行 HMAC-SHA256 加密
- 得到期望签名:expected_signature(期望签名)
- 对比:received_signature == expected_signature
- 相等 → 签名有效,请求合法
- 不相等 → 签名无效,拒绝请求
什么是”期望签名”和”收到的签名”:
- 收到的签名(received_signature):从请求头中提取的签名,是平台发送过来的
- 期望签名(expected_signature):我们用相同的算法和密钥计算出来的签名
如果两者一致,说明请求确实来自平台,且内容没有被篡改。
代码示例:
@PostMapping("/webhook/shopify")
public ResponseEntity<String> receiveShopifyWebhook(
@RequestHeader("X-Shopify-Hmac-SHA256") String receivedSignature,
@RequestBody String rawBody) {
// 1. 计算期望签名
String expectedSignature = calculateHmacSha256(rawBody, appSecret);
// 2. 对比签名
if (!expectedSignature.equals(receivedSignature)) {
log.warn("Shopify webhook signature verification failed");
return ResponseEntity.status(400).body("Invalid signature");
}
// 3. 签名验证通过,立即返回 200(必须在 3 秒内)
// 4. 异步处理业务逻辑(放到 MQ)
rocketMQTemplate.asyncSend("order-webhook-topic", rawBody, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("Webhook message sent to MQ successfully");
}
@Override
public void onException(Throwable e) {
log.error("Failed to send webhook message to MQ", e);
}
});
return ResponseEntity.ok("OK");
}
private String calculateHmacSha256(String data, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
mac.init(secretKey);
byte[] hash = mac.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
}
}
流程图:
flowchart TD
A["TikTok Shop 推送 Webhook<br>POST /webhook/tiktok<br>Header 含签名"] --> B["提取签名<br>Request Header: X-Tiktok-Signature<br>这是收到的签名(received_signature)"]
B --> C["用 App Secret 对请求体进行 HMAC-SHA256 计算<br>得到期望签名(expected_signature)"]
C --> D{"期望签名 == 收到的签名?"}
D -- "不一致(可能是伪造请求)" --> E["返回 400 Bad Request<br>记录安全日志"]
D -- "一致(合法来源)" --> F["返回 200 OK(必须立即返回)<br>不能让 TikTok 等待太久(超时会重发)"]
F --> G["将 Webhook 数据放入消息队列<br>异步处理(不在 HTTP 请求内处理)"]
G --> H["消息队列消费者<br>异步解析处理订单"]
重要:收到 Webhook 时必须立即返回 200,然后异步处理业务逻辑。如果在 HTTP 请求内处理业务逻辑(如写数据库),一旦处理时间超过平台超时(通常 3-5 秒),平台会认为失败并重复发送,导致重复订单。
9.4 订单去重(幂等)机制
flowchart LR
A["收到订单数据<br>platform=AMAZON<br>platform_order_no=112-3456789-0123456"] --> B["查询数据库:<br>SELECT * FROM order_main<br>WHERE tenant_id=101<br>AND platform='AMAZON'<br>AND platform_order_no='112-3456789-0123456'"]
B --> C{"查询结果"}
C -- "不存在" --> D["正常创建新订单<br>INSERT INTO order_main..."]
C -- "已存在(重复推送)" --> E{"平台订单状态有无更新?<br>对比平台状态 vs 本地状态"}
E -- "有更新" --> F["更新本地订单状态<br>写操作日志"]
E -- "无更新" --> G["直接返回成功<br>忽略重复数据"]
第十节 多平台库存管理与超卖防护
这是供应链系统最核心的业务价值,也是面试必问的技术难点。
10.1 多平台超卖问题的本质
问题场景:
仓库实际库存:10件蓝牙耳机
Amazon 店铺显示:10件可售
Shopify 店铺显示:10件可售
eBay 店铺显示:10件可售
同一时刻:
- 买家A在Amazon下单8件
- 买家B在Shopify下单5件
- 买家C在eBay下单3件
供应链系统拉取订单:总需求 8+5+3=16件 > 实际库存10件
问题:如何处理?取消哪个订单?
这个问题的根本原因:
- 库存分配策略缺失:没有给各平台分配固定配额,所有平台都显示全部库存
- 实时库存同步延迟:订单产生到供应链系统拉取有时间差(可能5-10分钟)
- 平台库存扣减时机:买家下单时平台已扣减库存,但供应链系统还不知道
10.2 解决方案:固定配额 + 动态调整 + 超卖处理
方案一:固定配额分配(推荐,最常用)
商家根据各平台销售情况,预先分配固定库存配额:
仓库实际库存:100件
固定分配:
- Amazon:40件(40%,销量最大)
- Shopify:30件(30%,销量中等)
- eBay:20件(20%,销量较小)
- 独立站:10件(10%,销量最小)
总计:40+30+20+10 = 100件(不超过仓库库存)
优点:
- 各平台库存独立,不会互相抢占
- 平台侧库存准确,买家下单时不会超卖
- 供应链系统只需定期同步各平台销售情况
缺点:
- 需要人工调整配额(可以做成自动化)
- 某个平台卖完了,其他平台还有库存,需要手动调配
方案二:共享池 + 先到先得 + 超卖补偿(备选方案)
所有平台共享同一个库存池,订单按到达时间先到先得:
仓库实际库存:10件
所有平台都显示:10件可售
订单到达顺序:
1. Amazon订单(8件)→ 扣减成功,剩余2件
2. Shopify订单(5件)→ 扣减失败,库存不足
3. eBay订单(3件)→ 扣减失败,库存不足
处理方式:
- Amazon订单:正常发货
- Shopify订单:系统自动取消,通知买家"库存不足"
- eBay订单:系统自动取消,通知买家"库存不足"
优点:
- 不需要预先分配配额
- 库存利用率最高
缺点:
- 会出现订单取消,影响买家体验
- 平台可能会因为取消率高而处罚店铺
- 需要自动取消订单的能力(不是所有平台都支持)
如果所有的店铺都共享一个库存 出现了超卖问题 店铺 要么取消买家的订单 要么 就是延迟发货 等待新的采购入库之后 才能重新发货
10.3 固定配额方案的完整实现
数据库设计:增加平台库存分配表
-- 平台库存分配表
CREATE TABLE `inventory_platform_allocation`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`sku_id` BIGINT NOT NULL COMMENT 'SKU ID',
`warehouse_id` BIGINT NOT NULL COMMENT '仓库ID',
`platform` VARCHAR(32) NOT NULL COMMENT '平台:AMAZON/SHOPIFY/EBAY/INDEPENDENT',
`allocated_qty` INT NOT NULL DEFAULT 0 COMMENT '分配数量',
`sold_qty` INT NOT NULL DEFAULT 0 COMMENT '已售数量',
`available_qty` INT NOT NULL DEFAULT 0 COMMENT '可售数量 = allocated_qty - sold_qty',
`last_sync_time` DATETIME NULL COMMENT '最后同步到平台的时间',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sku_warehouse_platform` (`tenant_id`, `sku_id`, `warehouse_id`, `platform`),
KEY `idx_sku_id` (`sku_id`),
KEY `idx_warehouse_id` (`warehouse_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
COMMENT = '平台库存分配表';
核心业务流程:
SaaS后台可以自动获取该租户的所以店铺信息 也可以获取该租户的所有的采购信息 同时给该租户提供一个分配的策略录入功能 默认分配策略是 平均分配 数据库保存当前录入的分配比例 给当前分配比例 配置一个分配策略名称 当其他的SKU需要进行分配库存的时候 直接选择该 分配策略 即可 自动分配 如果某个店铺销售比较快 而其他的店铺销售比较慢 我们供应链系统也可以自动按照之前指定的分配策略 去重新平衡这个库存
flowchart TD
A["商家设置库存分配策略"] --> B["仓库总库存:100件"]
B --> C["分配给Amazon:40件"]
B --> D["分配给Shopify:30件"]
B --> E["分配给eBay:20件"]
B --> F["分配给独立站:10件"]
C --> G["调用Amazon API<br>更新店铺库存为40件"]
D --> H["调用Shopify API<br>更新店铺库存为30件"]
E --> I["调用eBay API<br>更新店铺库存为20件"]
F --> J["独立站库存设置为10件"]
G & H & I & J --> K["各平台独立销售<br>互不影响"]
K --> L["Amazon卖出8件"]
L --> M["供应链系统接收订单<br>扣减Amazon配额:40-8=32"]
M --> N["调用Amazon API<br>更新店铺库存为32件"]
K --> O["Shopify卖出5件"]
O --> P["供应链系统接收订单<br>扣减Shopify配额:30-5=25"]
P --> Q["调用Shopify API<br>更新店铺库存为25件"]
代码实现:库存分配服务
@Service
public class InventoryAllocationService {
@Autowired
private InventoryPlatformAllocationMapper allocationMapper;
@Autowired
private InventoryMapper inventoryMapper;
@Autowired
private PlatformApiClientFactory platformApiClientFactory;
/**
* 设置库存分配策略
*
* @param skuId SKU ID
* @param warehouseId 仓库ID
* @param allocations 各平台分配数量
* {
* "AMAZON": 40,
* "SHOPIFY": 30,
* "EBAY": 20,
* "INDEPENDENT": 10
* }
*/
@Transactional
public void setAllocation(Long skuId, Long warehouseId, Map<String, Integer> allocations) {
// 1. 查询仓库总库存
Inventory inventory = inventoryMapper.selectBySkuAndWarehouse(skuId, warehouseId);
if (inventory == null) {
throw new BusinessException("库存记录不存在");
}
int totalQty = inventory.getQuantity();
// 2. 计算分配总量
int totalAllocation = allocations.values().stream().mapToInt(Integer::intValue).sum();
// 3. 校验:分配总量不能超过仓库库存
if (totalAllocation > totalQty) {
throw new BusinessException(
String.format("分配总量(%d)不能超过仓库库存(%d)", totalAllocation, totalQty)
);
}
// 4. 更新各平台库存分配
for (Map.Entry<String, Integer> entry : allocations.entrySet()) {
String platform = entry.getKey();
Integer allocatedQty = entry.getValue();
// 4.1 查询或创建分配记录
InventoryPlatformAllocation allocation = allocationMapper.selectBySkuWarehousePlatform(
skuId, warehouseId, platform
);
if (allocation == null) {
// 新建分配记录
allocation = new InventoryPlatformAllocation();
allocation.setSkuId(skuId);
allocation.setWarehouseId(warehouseId);
allocation.setPlatform(platform);
allocation.setAllocatedQty(allocatedQty);
allocation.setSoldQty(0);
allocation.setAvailableQty(allocatedQty);
allocationMapper.insert(allocation);
} else {
// 更新分配数量
allocation.setAllocatedQty(allocatedQty);
allocation.setAvailableQty(allocatedQty - allocation.getSoldQty());
allocationMapper.updateById(allocation);
}
// 4.2 同步到平台
syncInventoryToPlatform(skuId, platform, allocation.getAvailableQty());
}
log.info("库存分配完成:skuId={}, warehouseId={}, allocations={}",
skuId, warehouseId, allocations);
}
/**
* 订单创建时扣减平台配额
*/
@Transactional
public void deductPlatformAllocation(Long skuId, Long warehouseId, String platform, Integer quantity) {
// 1. 查询平台分配记录(加乐观锁)
InventoryPlatformAllocation allocation = allocationMapper.selectBySkuWarehousePlatform(
skuId, warehouseId, platform
);
if (allocation == null) {
throw new BusinessException("该平台未分配库存");
}
// 2. 检查可售库存是否充足
if (allocation.getAvailableQty() < quantity) {
throw new BusinessException(
String.format("平台%s库存不足:需要%d,可用%d",
platform, quantity, allocation.getAvailableQty())
);
}
// 3. 扣减可售库存(乐观锁更新)
int updated = allocationMapper.deductAvailableQty(
allocation.getId(),
quantity,
allocation.getVersion()
);
if (updated == 0) {
throw new BusinessException("库存扣减失败,请重试");
}
// 4. 增加已售数量
allocation.setSoldQty(allocation.getSoldQty() + quantity);
allocation.setAvailableQty(allocation.getAvailableQty() - quantity);
// 5. 同步到平台(异步)
CompletableFuture.runAsync(() -> {
syncInventoryToPlatform(skuId, platform, allocation.getAvailableQty());
});
log.info("平台库存扣减成功:skuId={}, platform={}, quantity={}, remaining={}",
skuId, platform, quantity, allocation.getAvailableQty());
}
/**
* 同步库存到平台
*/
private void syncInventoryToPlatform(Long skuId, String platform, Integer quantity) {
try {
PlatformApiClient client = platformApiClientFactory.getClient(platform);
client.updateInventory(skuId, quantity);
// 更新最后同步时间
allocationMapper.updateLastSyncTime(skuId, platform, LocalDateTime.now());
log.info("库存同步到平台成功:skuId={}, platform={}, quantity={}",
skuId, platform, quantity);
} catch (Exception e) {
log.error("库存同步到平台失败:skuId={}, platform={}, quantity={}",
skuId, platform, quantity, e);
// 记录同步失败日志,后续重试
}
}
}
乐观锁扣减SQL:
<!-- InventoryPlatformAllocationMapper.xml -->
<update id="deductAvailableQty">
UPDATE inventory_platform_allocation
SET available_qty = available_qty - #{quantity},
sold_qty = sold_qty + #{quantity},
version = version + 1,
update_time = NOW()
WHERE id = #{id}
AND version = #{version}
AND available_qty >= #{quantity}
</update>
10.4 共享池方案的超卖处理
如果采用共享池方案,当多个平台订单同时到达导致库存不足时,需要自动取消部分订单:
@Service
public class OrderOversellHandler {
@Autowired
private InventoryService inventoryService;
@Autowired
private OrderMainMapper orderMainMapper;
@Autowired
private PlatformApiClientFactory platformApiClientFactory;
/**
* 处理超卖订单
*
* 场景:同一时刻收到多个平台的订单,总需求超过库存
*/
@Transactional
public void handleOversell(Long skuId, Long warehouseId, List<OrderMain> orders) {
// 1. 查询当前可用库存
Inventory inventory = inventoryService.getInventory(skuId, warehouseId);
int availableQty = inventory.getQuantity() - inventory.getFrozenQty();
// 2. 计算总需求
int totalDemand = orders.stream()
.mapToInt(order -> {
return orderItemMapper.selectByOrderId(order.getId()).stream()
.filter(item -> item.getSkuId().equals(skuId))
.mapToInt(OrderItem::getQuantity)
.sum();
})
.sum();
log.warn("检测到超卖:skuId={}, 可用库存={}, 总需求={}",
skuId, availableQty, totalDemand);
// 3. 按订单时间排序(先到先得)
orders.sort(Comparator.comparing(OrderMain::getPlatformOrderTime));
// 4. 分配库存
int remainingQty = availableQty;
List<OrderMain> cancelOrders = new ArrayList<>();
for (OrderMain order : orders) {
int orderQty = orderItemMapper.selectByOrderId(order.getId()).stream()
.filter(item -> item.getSkuId().equals(skuId))
.mapToInt(OrderItem::getQuantity)
.sum();
if (remainingQty >= orderQty) {
// 库存充足,正常处理
remainingQty -= orderQty;
log.info("订单分配成功:orderNo={}, quantity={}, remaining={}",
order.getOrderNo(), orderQty, remainingQty);
} else {
// 库存不足,加入取消列表
cancelOrders.add(order);
log.warn("订单库存不足,待取消:orderNo={}, quantity={}, remaining={}",
order.getOrderNo(), orderQty, remainingQty);
}
}
// 5. 取消库存不足的订单
for (OrderMain order : cancelOrders) {
cancelOrderDueToOversell(order);
}
}
/**
* 取消订单(库存不足)
*/
private void cancelOrderDueToOversell(OrderMain order) {
try {
// 1. 更新订单状态
order.setStatus(10); // 已取消
order.setCancelReason("库存不足,系统自动取消");
orderMainMapper.updateById(order);
// 2. 记录操作日志
OrderLog log = new OrderLog();
log.setOrderId(order.getId());
log.setOrderNo(order.getOrderNo());
log.setFromStatus(order.getStatus());
log.setToStatus(10);
log.setAction("系统自动取消:库存不足");
log.setOperatorType(1); // 系统自动
log.setOperateTime(LocalDateTime.now());
orderLogMapper.insert(log);
// 3. 调用平台API取消订单
PlatformApiClient client = platformApiClientFactory.getClient(order.getPlatform());
client.cancelOrder(order.getPlatformOrderNo(), "Out of stock");
// 4. 发送通知给运营
notificationService.sendOversellAlert(order);
log.info("订单取消成功:orderNo={}, platform={}, reason=库存不足",
order.getOrderNo(), order.getPlatform());
} catch (Exception e) {
log.error("订单取消失败:orderNo={}", order.getOrderNo(), e);
// 标记为异常订单,人工处理
order.setIsAbnormal(1);
order.setAbnormalReason("自动取消失败:" + e.getMessage());
orderMainMapper.updateById(order);
}
}
}
10.5 两种方案对比
需要租户自己去配置相关的策略 默认的策略是 平均分配
| 维度 | 固定配额方案 | 共享池方案 |
|---|---|---|
| 实现复杂度 | 中等(需要分配管理) | 较低(先到先得) |
| 超卖风险 | 低(各平台独立) | 高(需要处理冲突) |
| 库存利用率 | 中等(可能有闲置) | 高(充分利用) |
| 买家体验 | 好(很少取消) | 差(可能被取消) |
| 平台处罚风险 | 低 | 高(取消率高) |
| 运营工作量 | 需要定期调整配额 | 无需人工干预 |
| 适用场景 | 多平台稳定销售 | 单平台或测试期 |
推荐方案:==固定配额 + 智能调整==
- 日常使用固定配额,保证各平台库存准确
- 系统根据销售速度自动建议调整配额
- 运营人员定期(如每周)审核并调整
- 紧急情况下可以手动调配库存
10.6 库存冻结机制
什么是冻结库存:
冻结库存(frozen_qty)是为了区分”仓库实际库存”和”已分配给订单的库存”,让商家清楚看到库存的不同状态。
inventory 表字段:
- quantity: 实际库存(仓库里真的有多少货)
- frozen_qty: 冻结库存(已分配给订单但未发货的库存)
- available_qty: 可售库存 = quantity - frozen_qty - defective_qty - reserved_qty
冻结库存的作用:
- 库存状态可见性:商家可以看到有多少库存已经分配给订单,有多少库存可以继续销售
- 防止重复分配:已冻结的库存不能再分配给其他订单
- 支持订单取消:订单取消时,释放冻结库存,让库存恢复可售
冻结库存的流转:
1. 订单创建
frozen_qty + N
quantity 不变(货还在仓库)
2. 订单发货
quantity - N(货物出库)
frozen_qty - N(解冻)
3. 订单取消
frozen_qty - N(释放冻结)
quantity 不变(货还在仓库)
10.7 完整的订单库存扣减流程
flowchart TD
A["平台订单推送到供应链系统"] --> B["OMS接收订单"]
B --> C{"采用哪种库存策略?"}
C -- "固定配额" --> D["检查平台配额是否充足"]
D --> E{"配额充足?"}
E -- "是" --> F["扣减平台配额<br>inventory_platform_allocation.available_qty -= N"]
E -- "否" --> G["订单标记为缺货<br>通知运营调配库存"]
C -- "共享池" --> H["检查仓库总库存是否充足"]
H --> I{"库存充足?"}
I -- "是" --> J["Redis原子扣减<br>DECRBY sku:stock:101:1000001:4001 N"]
I -- "否" --> K["订单进入超卖处理队列<br>按时间排序,先到先得"]
F --> L["冻结仓库库存<br>inventory.frozen_qty += N"]
J --> L
L --> M["订单状态:待备货<br>等待WMS拣货"]
M --> N["WMS拣货完成,确认发货"]
N --> O["扣减实际库存<br>inventory.quantity -= N"]
O --> P["释放冻结库存<br>inventory.frozen_qty -= N"]
P --> Q["订单状态:已发货"]
G --> R["运营调配库存后<br>重新处理订单"]
K --> S["库存不足的订单<br>自动取消并通知平台"]
代码实现:完整的库存扣减
@Service
public class OrderInventoryService {
@Autowired
private InventoryAllocationService allocationService;
@Autowired
private InventoryService inventoryService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 订单创建时扣减库存
*
* @param order 订单
* @param strategy 库存策略:FIXED_ALLOCATION=固定配额,SHARED_POOL=共享池
*/
@Transactional
public void deductInventoryForOrder(OrderMain order, String strategy) {
List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
for (OrderItem item : items) {
if ("FIXED_ALLOCATION".equals(strategy)) {
// 方案一:固定配额
deductByFixedAllocation(order, item);
} else {
// 方案二:共享池
deductBySharedPool(order, item);
}
}
}
/**
* 固定配额扣减
*/
private void deductByFixedAllocation(OrderMain order, OrderItem item) {
try {
// 1. 扣减平台配额
allocationService.deductPlatformAllocation(
item.getSkuId(),
order.getWarehouseId(),
order.getPlatform(),
item.getQuantity()
);
// 2. 冻结仓库库存
inventoryService.freezeStock(
item.getSkuId(),
order.getWarehouseId(),
item.getQuantity()
);
log.info("固定配额扣减成功:orderNo={}, skuId={}, quantity={}",
order.getOrderNo(), item.getSkuId(), item.getQuantity());
} catch (BusinessException e) {
// 配额不足,标记订单为缺货
order.setStatus(0); // 待处理(缺货挂起)
order.setIsAbnormal(1);
order.setAbnormalReason("平台库存配额不足");
orderMainMapper.updateById(order);
log.warn("平台配额不足:orderNo={}, platform={}, skuId={}",
order.getOrderNo(), order.getPlatform(), item.getSkuId());
throw e;
}
}
/**
* 共享池扣减(Redis原子操作)
*/
private void deductBySharedPool(OrderMain order, OrderItem item) {
String redisKey = String.format("sku:stock:%d:%d:%d",
order.getTenantId(), item.getSkuId(), order.getWarehouseId());
// 1. Redis原子扣减
Long remaining = redisTemplate.opsForValue().decrement(redisKey, item.getQuantity());
if (remaining < 0) {
// 扣减失败,回滚
redisTemplate.opsForValue().increment(redisKey, item.getQuantity());
log.warn("共享池库存不足:orderNo={}, skuId={}, remaining={}",
order.getOrderNo(), item.getSkuId(), remaining);
throw new BusinessException("库存不足");
}
// 2. 冻结仓库库存(异步同步到DB)
CompletableFuture.runAsync(() -> {
inventoryService.freezeStock(
item.getSkuId(),
order.getWarehouseId(),
item.getQuantity()
);
});
log.info("共享池扣减成功:orderNo={}, skuId={}, remaining={}",
order.getOrderNo(), item.getSkuId(), remaining);
}
/**
* 订单发货时扣减实际库存
*/
@Transactional
public void deductActualInventory(OrderMain order) {
List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
for (OrderItem item : items) {
// 1. 扣减实际库存
inventoryService.deductStock(
item.getSkuId(),
order.getWarehouseId(),
item.getQuantity()
);
// 2. 释放冻结库存
inventoryService.releaseFrozenStock(
item.getSkuId(),
order.getWarehouseId(),
item.getQuantity()
);
log.info("实际库存扣减成功:orderNo={}, skuId={}, quantity={}",
order.getOrderNo(), item.getSkuId(), item.getQuantity());
}
}
/**
* 订单取消时释放库存
*/
@Transactional
public void releaseInventoryForOrder(OrderMain order, String strategy) {
List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
for (OrderItem item : items) {
if ("FIXED_ALLOCATION".equals(strategy)) {
// 恢复平台配额
allocationService.restorePlatformAllocation(
item.getSkuId(),
order.getWarehouseId(),
order.getPlatform(),
item.getQuantity()
);
} else {
// 恢复共享池
String redisKey = String.format("sku:stock:%d:%d:%d",
order.getTenantId(), item.getSkuId(), order.getWarehouseId());
redisTemplate.opsForValue().increment(redisKey, item.getQuantity());
}
// 释放冻结库存
inventoryService.releaseFrozenStock(
item.getSkuId(),
order.getWarehouseId(),
item.getQuantity()
);
log.info("库存释放成功:orderNo={}, skuId={}, quantity={}",
order.getOrderNo(), item.getSkuId(), item.getQuantity());
}
}
}
10.8 库存分配的智能调整建议
系统可以根据各平台的销售速度,自动计算最优的库存分配方案:
关于智能调整方案实现:
- 让租户自己去配置不同的平台的库存的比例 SaaS系统只按照租户自己的配置 去监控各个平台的剩余库存 然后动态的去调整这些库存 使其达到平衡状态(租户指定的比例)
- 租户让SaaS自动去进行分析调整 刚开始分配库存的时候 先按照平均分配 然后SaaS会自动获取各个平台的剩余库存 哪个平台销售比较快 然后进行动态的调整
我们给不同的电商平台去动态设置库存 都要需要电商平台的支持的 如果个别平台不支持API的操作 我们可以实现预警 当某个平台的库存不足的时候 但是我们的供应链仓库中的库存还有很多的时候 就可以发送预警 让运营人员手动的去配置这个平台的库存
如果是手动的去电商平台后端进行库存的设置 可能会忘记 在SaaS平台进行记录 如果是SaaS自动去进行配置的 那么每次的配置 都会记录到SaaS系统中
@Service
public class InventoryAllocationOptimizer {
/**
* 计算最优库存分配
*
* 根据过去30天的销售数据,计算各平台的销售占比,
* 并建议新的库存分配方案
*/
public Map<String, Integer> calculateOptimalAllocation(Long skuId, Long warehouseId) {
// 1. 查询过去30天各平台销售数量
LocalDateTime startTime = LocalDateTime.now().minusDays(30);
List<PlatformSalesData> salesData = orderMainMapper.selectPlatformSales(
skuId, startTime, LocalDateTime.now()
);
// 2. 计算总销量
int totalSales = salesData.stream()
.mapToInt(PlatformSalesData::getSalesQty)
.sum();
if (totalSales == 0) {
// 没有销售数据,使用默认分配
return getDefaultAllocation();
}
// 3. 查询当前总库存
Inventory inventory = inventoryMapper.selectBySkuAndWarehouse(skuId, warehouseId);
int totalQty = inventory.getQuantity();
// 4. 按销售占比分配
Map<String, Integer> allocation = new HashMap<>();
int allocatedQty = 0;
for (PlatformSalesData data : salesData) {
double ratio = (double) data.getSalesQty() / totalSales;
int qty = (int) Math.floor(totalQty * ratio);
allocation.put(data.getPlatform(), qty);
allocatedQty += qty;
}
// 5. 剩余库存分配给销量最大的平台
if (allocatedQty < totalQty) {
String topPlatform = salesData.get(0).getPlatform();
allocation.put(topPlatform, allocation.get(topPlatform) + (totalQty - allocatedQty));
}
log.info("计算最优分配:skuId={}, totalQty={}, allocation={}",
skuId, totalQty, allocation);
return allocation;
}
private Map<String, Integer> getDefaultAllocation() {
Map<String, Integer> allocation = new HashMap<>();
allocation.put("AMAZON", 40);
allocation.put("SHOPIFY", 30);
allocation.put("EBAY", 20);
allocation.put("INDEPENDENT", 10);
return allocation;
}
}
第十一节 订单状态机与全流程
11.1 订单 10 状态机全景
stateDiagram-v2
[*] --> 待处理 : 平台订单同步进入系统
待处理 --> 风控审核 : 触发风控规则(如可疑地址/异常金额)
待处理 --> 待备货 : 风控通过 + 库存充足
待处理 --> 缺货挂起 : 库存不足(缺货状态=2中的特殊子状态)
待处理 --> 已取消 : 买家取消 / 超时自动取消
风控审核 --> 待备货 : 人工审核通过
风控审核 --> 已取消 : 审核拒绝(确认为异常订单)
缺货挂起 --> 待备货 : 补货到位,库存恢复
待备货 --> 备货中 : WMS 开始拣货
待备货 --> 已取消 : 买家取消(发货前)
备货中 --> 待发货 : 拣货打包完成
待发货 --> 已发货 : 仓库扫码确认发货 + 填写运单号
已发货 --> 运输中 : 物流商揽收扫描
运输中 --> 已签收 : 末端配送签收确认
已签收 --> 已完成 : 超过售后期(通常15天)自动完成
已签收 --> 售后中 : 买家申请退款/换货
售后中 --> 已完成 : 售后关闭(无需退款/买家撤回)
售后中 --> 已完成 : 退款/换货完成
已取消 --> [*]
已完成 --> [*]
11.2 订单全流程详解
flowchart TD
A(["平台新订单进入"]) --> B["OMS 接收订单<br>格式标准化 + 幂等校验"]
B --> C["Redis 原子扣减库存<br>frozen_qty 增加"]
C --> D{"扣减结果"}
D -- "库存不足" --> E["订单状态:缺货挂起<br>通知采购补货<br>等待库存补充后重新处理"]
D -- "库存充足" --> F["风控规则检查"]
F --> G{"风控结果"}
G -- "触发高风险规则" --> H["进入人工审核队列<br>运营专员 24 小时内处理"]
G -- "正常通过" --> I["自动分配发货仓库<br>根据:库存所在仓 / 离目的地近 / 仓库优先级"]
H --> J{"人工审核结果"}
J -- "确认异常,拒绝" --> K["取消订单,释放库存"]
J -- "审核通过" --> I
I --> L["推送给 WMS<br>创建出库单"]
L --> M["WMS 生成拣货单<br>按库位路径排序"]
M --> N["仓管员拣货 → 打包 → 复核"]
N --> O["系统确认发货<br>实物库存 -N,冻结库存 -N(解冻)"]
O --> P["推送给 TMS<br>创建运单,打印面单"]
P --> Q["交给物流商揽收"]
Q --> R["TMS 追踪物流轨迹<br>实时更新订单状态"]
R --> S["买家签收"]
S --> T{"15天内是否有售后?"}
T -- 有 --> U["进入售后流程"]
T -- "无(超过售后期)" --> V["订单自动完成<br>触发财务结算"]
V & U --> W(["订单归档"])
E --> X(["待补货"])
K --> Y(["已取消"])
11.3 风控规则设计
为什么需要风控:
跨境电商存在大量欺诈订单、刷单订单、恶意退款等风险。如果不做风控,商家可能会遭受损失。
风控规则:
我们设计了 5 类风控规则,每个规则对应一个分值,总分 0-100 分:
-
地址有效性(20 分)
- 邮政编码是否存在
- 国家-州组合是否合法
- 地址格式是否规范
-
金额异常(30 分)
- 单笔订单金额超过历史均值 10 倍
- 单价明显低于成本价(可能是测试订单)
- 大额订单但买家账号新注册
-
新买家高频下单(20 分)
- 同一买家 1 小时内下 5 单以上(可能是刷单)
- 新注册账号立即下大额订单
-
同地址大量下单(15 分)
- 同一收货地址 24 小时内收到 10 单以上
- 可能是转运仓或刷单地址
-
高退款率买家(15 分)
- 历史退款率超过 50% 的买家
- 可能是恶意退款
风险等级:
- 0-30 分:低风险,正常放行
- 31-70 分:中风险,加标记,继续处理但重点监控
- 71 分以上:高风险,进入人工审核队列,挂起等待处理
代码实现:
@Service
public class OrderRiskService {
/**
* 风控检查
*
* @param order 订单
* @return 风险评分(0-100)
*/
public int checkRisk(OrderMain order) {
int totalScore = 0;
// 规则1:地址有效性
totalScore += checkAddressValidity(order);
// 规则2:金额异常
totalScore += checkAmountAnomaly(order);
// 规则3:新买家高频下单
totalScore += checkHighFrequencyOrder(order);
// 规则4:同地址大量下单
totalScore += checkSameAddressOrders(order);
// 规则5:高退款率买家
totalScore += checkRefundRate(order);
return totalScore;
}
/**
* 检查地址有效性
*/
private int checkAddressValidity(OrderMain order) {
OrderAddress address = orderAddressMapper.selectByOrderId(order.getId());
// 检查邮政编码是否存在
if (!zipCodeService.isValid(address.getCountryCode(), address.getZipCode())) {
return 20; // 地址无效,20 分
}
// 检查国家-州组合是否合法
if (!regionService.isValidCombination(address.getCountryCode(), address.getState())) {
return 20;
}
return 0; // 地址有效
}
/**
* 检查金额异常
*/
private int checkAmountAnomaly(OrderMain order) {
// 查询该租户的历史订单平均金额
BigDecimal avgAmount = orderMainMapper.selectAvgAmount(order.getTenantId());
// 如果订单金额超过历史均值 10 倍
if (order.getPaymentAmount().compareTo(avgAmount.multiply(BigDecimal.TEN)) > 0) {
return 30;
}
// 检查单价是否低于成本价
List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
for (OrderItem item : items) {
ProductSku sku = productSkuMapper.selectById(item.getSkuId());
if (item.getUnitPrice().compareTo(sku.getCostPrice()) < 0) {
return 30; // 单价低于成本价,可能是测试订单
}
}
return 0;
}
/**
* 检查新买家高频下单
*/
private int checkHighFrequencyOrder(OrderMain order) {
// 查询该买家 1 小时内的订单数量
int orderCount = orderMainMapper.countByBuyerInLastHour(
order.getTenantId(),
order.getPlatform(),
order.getPlatformOrderNo()
);
if (orderCount >= 5) {
return 20; // 1 小时内下 5 单以上
}
return 0;
}
/**
* 检查同地址大量下单
*/
private int checkSameAddressOrders(OrderMain order) {
OrderAddress address = orderAddressMapper.selectByOrderId(order.getId());
// 查询该地址 24 小时内的订单数量
int orderCount = orderMainMapper.countByAddressInLast24Hours(
order.getTenantId(),
address.getFullAddress()
);
if (orderCount >= 10) {
return 15; // 24 小时内 10 单以上
}
return 0;
}
/**
* 检查退款率
*/
private int checkRefundRate(OrderMain order) {
// 查询该买家的历史订单和退款订单
int totalOrders = orderMainMapper.countByBuyer(order.getTenantId(), order.getPlatform());
int refundOrders = orderRefundMapper.countByBuyer(order.getTenantId(), order.getPlatform());
if (totalOrders > 0) {
double refundRate = (double) refundOrders / totalOrders;
if (refundRate > 0.5) {
return 15; // 退款率超过 50%
}
}
return 0;
}
}
流程图:
graph TD
A["订单进入风控检查"] --> B["规则1:地址有效性<br>邮政编码是否存在<br>国家-州组合是否合法"]
A --> C["规则2:金额异常<br>单笔订单金额超过历史均值10倍<br>或单价明显低于成本价"]
A --> D["规则3:新买家高频下单<br>同一买家1小时内下5单以上<br>(可能是刷单)"]
A --> E["规则4:同地址大量下单<br>同一收货地址24小时内收到10单以上"]
A --> F["规则5:高退款率买家<br>历史退款率超过50%的买家"]
B & C & D & E & F --> G["风控评分计算<br>每个规则对应分值"]
G --> H{"总分"}
H -- "0-30分" --> I["低风险:正常放行"]
H -- "31-70分" --> J["中风险:加标记,继续处理<br>但重点监控"]
H -- "71分以上" --> K["高风险:进入人工审核队列<br>挂起等待处理"]
11.4 订单超期发货预警
为什么需要超期预警:
每个电商平台都有最晚发货时间要求(delivery_deadline),超期会导致:
- 店铺扣分
- 订单自动取消
- 买家投诉
- 严重时可能导致店铺被封
预警规则:
我们设计了三级预警机制:
- 24 小时预警(黄色):站内信通知运营专员
- 48 小时预警(红色):站内信 + 邮件通知运营负责人
- 已超期告警(深红色):标记订单为异常,通知多级负责人
代码实现:
@Service
public class OrderDeadlineWarningService {
@Autowired
private OrderMainMapper orderMainMapper;
@Autowired
private NotificationService notificationService;
/**
* 定时任务:每小时执行一次
*/
@Scheduled(cron = "0 0 * * * ?")
public void checkDeadlineWarning() {
// 1. 查询所有待备货/备货中的订单,且有最晚发货时间
List<OrderMain> orders = orderMainMapper.selectList(
new LambdaQueryWrapper<OrderMain>()
.in(OrderMain::getStatus, Arrays.asList(2, 3)) // 待备货、备货中
.isNotNull(OrderMain::getDeliveryDeadline)
);
LocalDateTime now = LocalDateTime.now();
for (OrderMain order : orders) {
LocalDateTime deadline = order.getDeliveryDeadline().atStartOfDay();
long hoursUntilDeadline = ChronoUnit.HOURS.between(now, deadline);
if (hoursUntilDeadline < 0) {
// 已超期
handleOverdueOrder(order, Math.abs(hoursUntilDeadline));
} else if (hoursUntilDeadline <= 24) {
// 24 小时内
handleUrgentWarning(order, hoursUntilDeadline);
} else if (hoursUntilDeadline <= 48) {
// 48 小时内
handleNormalWarning(order, hoursUntilDeadline);
}
}
}
/**
* 处理已超期订单
*/
private void handleOverdueOrder(OrderMain order, long overdueHours) {
// 1. 标记订单为异常
order.setIsAbnormal(1);
order.setAbnormalReason("发货超期 " + overdueHours + " 小时");
orderMainMapper.updateById(order);
// 2. 记录操作日志
OrderLog log = new OrderLog();
log.setOrderId(order.getId());
log.setOrderNo(order.getOrderNo());
log.setFromStatus(order.getStatus());
log.setToStatus(order.getStatus());
log.setAction("系统自动标记为异常:发货超期");
log.setOperatorType(1); // 系统自动
log.setOperateTime(LocalDateTime.now());
orderLogMapper.insert(log);
// 3. 发送告警通知(多级负责人)
notificationService.sendOverdueAlert(order, overdueHours);
log.warn("订单超期告警:orderNo={}, overdueHours={}", order.getOrderNo(), overdueHours);
}
/**
* 处理紧急预警(24 小时内)
*/
private void handleUrgentWarning(OrderMain order, long hoursUntilDeadline) {
// 发送站内信 + 邮件通知运营负责人
notificationService.sendUrgentWarning(order, hoursUntilDeadline);
log.warn("订单紧急预警:orderNo={}, hoursUntilDeadline={}",
order.getOrderNo(), hoursUntilDeadline);
}
/**
* 处理普通预警(48 小时内)
*/
private void handleNormalWarning(OrderMain order, long hoursUntilDeadline) {
// 发送站内信通知运营专员
notificationService.sendNormalWarning(order, hoursUntilDeadline);
log.info("订单预警:orderNo={}, hoursUntilDeadline={}",
order.getOrderNo(), hoursUntilDeadline);
}
}
通知服务实现:
@Service
public class NotificationService {
@Autowired
private SysMessageMapper sysMessageMapper;
@Autowired
private EmailService emailService;
/**
* 发送超期告警(多级负责人)
*/
public void sendOverdueAlert(OrderMain order, long overdueHours) {
String title = "【紧急】订单发货超期告警";
String content = String.format(
"订单号:%s\n" +
"平台:%s\n" +
"超期时长:%d 小时\n" +
"最晚发货时间:%s\n" +
"当前状态:%s\n" +
"请立即处理!",
order.getOrderNo(),
order.getPlatform(),
overdueHours,
order.getDeliveryDeadline(),
getStatusName(order.getStatus())
);
// 发送站内信给运营负责人
sendInternalMessage(order.getTenantId(), "OPERATION_MANAGER", title, content);
// 发送邮件给运营负责人
emailService.sendAlert(order.getTenantId(), "OPERATION_MANAGER", title, content);
}
/**
* 发送紧急预警(24 小时内)
*/
public void sendUrgentWarning(OrderMain order, long hoursUntilDeadline) {
String title = "【紧急】订单即将超期预警";
String content = String.format(
"订单号:%s\n" +
"平台:%s\n" +
"距离最晚发货时间:%d 小时\n" +
"最晚发货时间:%s\n" +
"当前状态:%s\n" +
"请尽快处理!",
order.getOrderNo(),
order.getPlatform(),
hoursUntilDeadline,
order.getDeliveryDeadline(),
getStatusName(order.getStatus())
);
// 发送站内信 + 邮件
sendInternalMessage(order.getTenantId(), "OPERATION_MANAGER", title, content);
emailService.sendWarning(order.getTenantId(), "OPERATION_MANAGER", title, content);
}
/**
* 发送普通预警(48 小时内)
*/
public void sendNormalWarning(OrderMain order, long hoursUntilDeadline) {
String title = "订单发货预警";
String content = String.format(
"订单号:%s\n" +
"平台:%s\n" +
"距离最晚发货时间:%d 小时\n" +
"最晚发货时间:%s\n" +
"当前状态:%s",
order.getOrderNo(),
order.getPlatform(),
hoursUntilDeadline,
order.getDeliveryDeadline(),
getStatusName(order.getStatus())
);
// 发送站内信
sendInternalMessage(order.getTenantId(), "OPERATION_STAFF", title, content);
}
private void sendInternalMessage(Long tenantId, String roleCode, String title, String content) {
SysMessage message = new SysMessage();
message.setTenantId(tenantId);
message.setTitle(title);
message.setContent(content);
message.setReceiverRole(roleCode);
message.setMessageType(1); // 系统通知
message.setIsRead(0);
sysMessageMapper.insert(message);
}
private String getStatusName(Integer status) {
switch (status) {
case 0: return "待处理";
case 1: return "风控审核";
case 2: return "待备货";
case 3: return "备货中";
case 4: return "待发货";
case 5: return "已发货";
case 6: return "运输中";
case 7: return "已签收";
case 8: return "已完成";
case 9: return "售后中";
case 10: return "已取消";
default: return "未知状态";
}
}
}
流程图:
flowchart TD
A(["定时任务:每小时执行"]) --> B["查询所有状态为待备货/备货中的订单<br>且 delivery_deadline IS NOT NULL"]
B --> C["计算距最晚发货时间的剩余小时数"]
C --> D{"剩余时间判断"}
D -- "≤ 24小时" --> E["🔴 紧急预警<br>站内信 + 邮件通知运营负责人"]
D -- "≤ 48小时" --> F["🟡 预警<br>站内信通知运营专员"]
D -- "已超期" --> G["🔴🔴 超期告警<br>标记订单为异常<br>通知多级负责人"]
E & F & G --> H["在系统 Dashboard 中<br>突出显示这些订单"]
第十二节 拆单与合单规则
12.1 为什么需要拆单
graph TD
A["买家下了一个订单<br>购买了3件商品"] --> B["商品1:蓝牙耳机<br>在广州总仓有货"]
A --> C["商品2:手机支架<br>在美国FBA仓有货"]
A --> D["商品3:数据线(含电池)<br>只能走海运"]
B & C & D --> E["3件商品无法合并发货!<br>需要拆单处理"]
E --> F["子订单1:蓝牙耳机<br>广州总仓 → 国际物流 → 美国"]
E --> G["子订单2:手机支架<br>FBA仓 → 本地配送 → 美国"]
E --> H["子订单3:数据线<br>广州总仓 → 海运 → 美国(较慢)"]
F & G & H --> I["买家收到3个包裹<br>系统展示3个运单号<br>但对应同一个原始订单"]
12.2 拆单触发规则
flowchart TD
A["原始订单进入拆单引擎"] --> B["规则1:跨仓库<br>明细商品分布在不同仓库"]
A --> C["规则2:物流限制<br>含电池商品不能与普通商品走同一渠道"]
A --> D["规则3:超重/超体积<br>单个包裹超过物流商重量上限"]
A --> E["规则4:平台要求<br>某些平台要求每个包裹单独运单"]
B & C & D & E --> F["拆单决策引擎<br>计算最优拆分方案"]
F --> G["生成多个子订单<br>父子关系通过 parent_order_id 关联"]
G --> H["各子订单独立流转<br>父订单状态 = 最落后子订单状态"]
父子订单字段设计补充:
-- 在 order_main 表增加以下字段(初始设计未包含,作为补充)
ALTER TABLE `order_main`
ADD COLUMN `is_parent_order` TINYINT(1) NOT NULL DEFAULT 0
COMMENT '是否为父订单(被拆分的原始订单):1=是父订单' AFTER `is_abnormal`,
ADD COLUMN `parent_order_id` BIGINT NULL
COMMENT '父订单ID,是子订单时填写,是父订单时为NULL' AFTER `is_parent_order`,
ADD COLUMN `split_reason` VARCHAR(128) NULL
COMMENT '拆单原因,如:跨仓库/物流限制/超重' AFTER `parent_order_id`;
-- 创建子订单查询索引
ALTER TABLE `order_main`
ADD KEY `idx_parent_order_id` (`parent_order_id`);
12.3 合单规则(减少物流成本)
合单(Order Merging):同一买家在短时间内下了多个订单,合并成一个包裹发出,节省运费。
flowchart TD
A["同一买家短时间内下了多个订单"] --> B["合单检查条件(全部满足才可合单):<br>1. 同一买家、同一收货地址<br>2. 下单时间间隔 ≤ 2小时<br>3. 所有订单状态为待备货(未开始拣货)<br>4. 合并后总重量未超过物流上限<br>5. 买家未明确要求分开发货"]
B --> C{"是否满足合单条件?"}
C -- 是 --> D["系统提示运营专员:<br>建议合单(节省运费 ¥15.3)"]
D --> E{"运营专员确认合单"}
E -- 确认 --> F["将多个订单合并为一张出库单<br>一次拣货发出"]
E -- 不合并 --> G["保持独立发货"]
C -- 否 --> G
第十三节 售后退款处理
13.1 退款业务类型
| 退款类型 | 说明 | 库存处理 | 难点 |
|---|---|---|---|
| 仅退款(未发货) | 买家取消,货还在仓库 | 释放冻结库存,库存恢复可售 | 时间窗口判断(已发货则不适用) |
| 仅退款(已发货) | 货已发出,买家无需退回 | 库存已减少,不需要增加(货物不回来) | 财务处理,损失记录 |
| 退货退款 | 买家退回商品,收到后退款 | 退货到仓后二次质检,好品增加可售库存 | 退货物流,二次质检 |
| 换货 | 退旧品发新品 | 退货入库 + 新品出库 | 等于一进一出 |
| 平台强制退款 | 平台介入裁决,强制退款给买家 | 按退款类型处理 | 无从反驳,按平台裁决 |
13.2 退款退货完整流程
核心理解:退款发生在电商平台,供应链系统只处理库存调整
退款流程分为两个层面:
- 电商平台层面:买家在 Amazon 申请退款 → 商家在 Amazon 后台审核 → Amazon 执行退款
- 供应链系统层面:接收退款通知 → 调整库存状态 → 记录退款信息
详细流程:
步骤 1:买家申请退款
- 买家在电商平台(如 Amazon)申请退款
- 填写退款原因、上传证据图片
- 平台创建退款申请
步骤 2:平台通知供应链系统
- 平台通过 Webhook 或 API 推送退款信息
- 供应链系统接收通知,创建退款单(order_refund 表)
步骤 3:商家审核退款申请
- 运营专员在供应链系统中查看退款申请
- 查看商品状态、退款原因、证据图片
- 决定是否批准退款
步骤 4:处理不同类型的退款
情况 A:仅退款(未发货)
// 释放冻结库存
UPDATE inventory
SET frozen_qty = frozen_qty - #{quantity}
WHERE sku_id = #{skuId} AND warehouse_id = #{warehouseId};
// quantity 不变,因为货还在仓库
情况 B:仅退款(已发货)
// 不处理库存,因为货物不回来
// 只记录财务损失
INSERT INTO finance_loss (order_id, loss_amount, loss_reason)
VALUES (#{orderId}, #{refundAmount}, '仅退款-已发货');
情况 C:退货退款
// 1. 等待买家寄回商品
// 2. 货物到达仓库,进行二次质检
// 3. 根据质检结果处理库存
// 好品入库
UPDATE inventory
SET quantity = quantity + #{goodQty}
WHERE sku_id = #{skuId} AND warehouse_id = #{warehouseId};
// 差品入库
UPDATE inventory
SET defective_qty = defective_qty + #{badQty}
WHERE sku_id = #{skuId} AND warehouse_id = #{warehouseId};
// 写入库流水
INSERT INTO inventory_log (sku_id, warehouse_id, type, quantity, remark)
VALUES (#{skuId}, #{warehouseId}, 7, #{goodQty}, '退货入库-好品');
步骤 5:执行退款
- 供应链系统通知财务部门
- 财务部门在电商平台后台执行退款操作
- 退款金额返还买家账户(3-7 个工作日)
如果是在电商平台的退款期内 当商家同意退款后 电商平台是可以直接把钱返还给买家的 不需要从商家的账户里面再进行转账了 但是如果这个订单 已经过了很长的时间 或者 商家在电商平台没有开通相关的自动扣款的权限 那么就需要商家给买家进行退款操作 正常来说 退款的操作是不需要商家介入的 只需要电商平台处理就可以了 SaaS平台只负责退款的记录和库存的变更
步骤 6:更新状态
- 更新退款单状态:退款完成
- 更新订单状态:已完成或售后关闭
代码实现:
@Service
public class OrderRefundService {
@Autowired
private OrderRefundMapper orderRefundMapper;
@Autowired
private InventoryService inventoryService;
@Autowired
private OrderMainMapper orderMainMapper;
/**
* 处理退款申请
*/
@Transactional
public void processRefund(Long refundId, Integer auditResult, String auditRemark) {
// 1. 查询退款单
OrderRefund refund = orderRefundMapper.selectById(refundId);
if (refund == null) {
throw new BusinessException("退款单不存在");
}
// 2. 查询订单
OrderMain order = orderMainMapper.selectById(refund.getOrderId());
if (auditResult == 1) {
// 审核通过
if (refund.getRefundType() == 1) {
// 仅退款(未发货)
handleRefundOnly(refund, order);
} else if (refund.getRefundType() == 2) {
// 退货退款
handleRefundWithReturn(refund, order);
}
// 更新退款单状态
refund.setStatus(1); // 审核通过
refund.setAuditTime(LocalDateTime.now());
orderRefundMapper.updateById(refund);
} else {
// 审核拒绝
refund.setStatus(4); // 审核拒绝
refund.setAuditTime(LocalDateTime.now());
refund.setReasonDetail(auditRemark);
orderRefundMapper.updateById(refund);
}
}
/**
* 处理仅退款(未发货)
*/
private void handleRefundOnly(OrderRefund refund, OrderMain order) {
// 查询订单明细
List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
for (OrderItem item : items) {
// 释放冻结库存
inventoryService.releaseFrozenStock(
item.getSkuId(),
order.getWarehouseId(),
item.getQuantity()
);
}
// 更新订单状态
order.setStatus(10); // 已取消
orderMainMapper.updateById(order);
}
/**
* 处理退货退款
*/
private void handleRefundWithReturn(OrderRefund refund, OrderMain order) {
// 更新退款单状态:等待退货
refund.setStatus(1); // 审核通过,等待退货
orderRefundMapper.updateById(refund);
// 订单状态更新为售后中
order.setStatus(9); // 售后中
orderMainMapper.updateById(order);
}
/**
* 确认收到退货
*/
@Transactional
public void confirmReturnReceived(Long refundId, Integer goodQty, Integer badQty) {
// 1. 查询退款单
OrderRefund refund = orderRefundMapper.selectById(refundId);
OrderMain order = orderMainMapper.selectById(refund.getOrderId());
List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
for (OrderItem item : items) {
// 2. 好品入库
if (goodQty > 0) {
inventoryService.returnGoodStock(
item.getSkuId(),
order.getWarehouseId(),
goodQty
);
}
// 3. 差品入库
if (badQty > 0) {
inventoryService.returnDefectiveStock(
item.getSkuId(),
order.getWarehouseId(),
badQty
);
}
}
// 4. 更新退款单状态
refund.setStatus(2); // 已收到退货
orderRefundMapper.updateById(refund);
}
/**
* 完成退款
*/
@Transactional
public void completeRefund(Long refundId) {
// 1. 更新退款单状态
OrderRefund refund = orderRefundMapper.selectById(refundId);
refund.setStatus(3); // 退款完成
refund.setCompleteTime(LocalDateTime.now());
orderRefundMapper.updateById(refund);
// 2. 更新订单状态
OrderMain order = orderMainMapper.selectById(refund.getOrderId());
order.setStatus(8); // 已完成
orderMainMapper.updateById(order);
}
}
流程图:
flowchart TD
A(["买家在电商平台申请退款"]) --> B["电商平台接收申请<br>(Amazon/Shopify/eBay)"]
B --> C["平台通知供应链系统<br>(Webhook 或 API)"]
C --> D["供应链系统创建退款单<br>order_refund"]
D --> E["运营专员审核退款申请<br>查看:商品状态/退款原因/证据图片"]
E --> F{"是否批准退款?"}
F -- "拒绝" --> G["填写拒绝原因<br>通过平台消息回复买家"]
F -- "批准仅退款" --> H{"货物状态"}
H -- "未发货" --> I["释放冻结库存<br>inventory.frozen_qty -= N<br>inventory.quantity 不变(货还在)"]
H -- "已发货" --> J["财务登记损失<br>库存不增加,货物归属买家"]
F -- "批准退货退款" --> K["告知买家退货地址<br>等待买家寄回"]
K --> L["买家发货,填写运单号"]
L --> M["货物到达仓库<br>收货 + 二次质检"]
M --> N{"质检结果"}
N -- "好品" --> O["入可售库存<br>inventory.quantity += 合格数量<br>写流水:退货入库(7)"]
N -- "差品" --> P["入不良品库存<br>inventory.defective_qty += 不合格数量"]
O & P & I & J --> Q["供应链系统通知财务"]
Q --> R["财务在电商平台后台执行退款<br>退款金额返还买家(3-7 个工作日)"]
R --> S["更新退款单状态:退款完成"]
S --> T["更新订单状态:已完成或售后关闭"]
T --> U(["退款流程结束"])
G --> V(["买家可继续申诉"])
13.3 退款对各模块的影响
graph TD
A["退款单处理完成"] --> B["OMS:<br>订单状态更新<br>退款记录完成"]
A --> C["WMS:<br>退货入库(好品)<br>不良品登记"]
A --> D["FMS:<br>退款金额记录<br>影响本期利润核算"]
A --> E["SRM:<br>若是质量问题退款<br>计入供应商质量评分<br>影响绩效评级"]
第十四节 接口设计规范
14.1 商品模块接口清单
基础路径前缀: /api/pim
| 接口名称 | HTTP方法 | 路径 | 权限标识 |
|---|---|---|---|
| 分类树查询 | GET | /categories/tree | pim:category:list |
| 新增分类 | POST | /categories | pim:category:add |
| SPU列表 | GET | /spus | pim:spu:list |
| SPU详情 | GET | /spus/{id} | pim:spu:list |
| 新增SPU | POST | /spus | pim:spu:add |
| 编辑SPU | PUT | /spus/{id} | pim:spu:edit |
| 删除SPU | DELETE | /spus/{id} | pim:spu:delete |
| 提交审核 | PUT | /spus/{id}/submit | pim:spu:submit |
| 上架 | PUT | /spus/{id}/on-sale | pim:spu:publish |
| 下架 | PUT | /spus/{id}/off-sale | pim:spu:publish |
| SKU列表(按SPU) | GET | /spus/{spuId}/skus | pim:sku:list |
| 批量创建SKU | POST | /spus/{spuId}/skus/batch | pim:sku:add |
| 编辑SKU | PUT | /skus/{id} | pim:sku:edit |
| SKU价格设置 | PUT | /skus/{id}/prices | pim:sku:price |
| 多语言内容 | GET | /spus/{id}/i18n | pim:spu:list |
| 保存多语言内容 | PUT | /spus/{id}/i18n/{langCode} | pim:spu:edit |
| AI一键翻译 | POST | /spus/{id}/i18n/translate | pim:spu:edit |
| 图片上传 | POST | /spus/{id}/images | pim:spu:edit |
| 分类属性模板 | GET | /categories/{id}/attrs | pim:category:list |
| SKU下拉选项 | GET | /skus/options | pim:sku:list |
14.2 订单模块接口清单
基础路径前缀: /api/oms
| 接口名称 | HTTP方法 | 路径 | 权限标识 |
|---|---|---|---|
| 订单列表 | GET | /orders | oms:order:list |
| 订单详情 | GET | /orders/{id} | oms:order:list |
| 手动创建订单 | POST | /orders | oms:order:add |
| 手动取消订单 | PUT | /orders/{id}/cancel | oms:order:cancel |
| 审核通过(风控) | PUT | /orders/{id}/approve | oms:order:audit |
| 审核拒绝(风控) | PUT | /orders/{id}/reject | oms:order:audit |
| 标记异常 | PUT | /orders/{id}/flag | oms:order:edit |
| 同步平台状态 | POST | /orders/{id}/sync | oms:order:sync |
| 订单操作日志 | GET | /orders/{id}/logs | oms:order:list |
| 拆单 | POST | /orders/{id}/split | oms:order:split |
| 合单 | POST | /orders/merge | oms:order:merge |
| 退款单列表 | GET | /refunds | oms:refund:list |
| 创建退款单 | POST | /refunds | oms:refund:add |
| 审核退款 | PUT | /refunds/{id}/audit | oms:refund:audit |
| 确认收到退货 | PUT | /refunds/{id}/received | oms:refund:receive |
| 完成退款 | PUT | /refunds/{id}/complete | oms:refund:complete |
| 订单统计看板 | GET | /report/overview | oms:report:view |
| 今日订单实时 | GET | /report/today | oms:report:view |
| 平台同步日志 | GET | /sync/logs | oms:sync:view |
| Webhook接收(亚马逊) | POST | /webhook/amazon | 无需权限(需签名验证) |
| Webhook接收(TikTok) | POST | /webhook/tiktok | 无需权限(需签名验证) |
14.3 关键接口示例
① 订单列表接口(支持多维度筛选):
GET /api/oms/orders?page=1&size=20&status=4&platform=AMAZON&startDate=2025-01-01&endDate=2025-01-17
响应:
{
"code": 200,
"data": {
"total": 856,
"pages": 43,
"records": [
{
"id": "1748291234567900",
"orderNo": "ORD-20250117-0001",
"platform": "AMAZON",
"platformOrderNo": "112-3456789-0123456",
"status": 4,
"statusName": "待发货",
"totalAmount": 29.99,
"currency": "USD",
"cnyAmount": 215.93,
"itemCount": 1,
"items": [
{
"skuCode": "SKU-EARPHONE-B",
"skuName": "蓝牙耳机Pro-黑色",
"quantity": 1,
"unitPrice": 29.99
}
],
"receiverName": "John Smith",
"countryCode": "US",
"deliveryDeadline": "2025-01-19",
"daysUntilDeadline": 2,
"isUrgent": true,
"platformOrderTime": "2025-01-17 08:30:00",
"warehouseName": "广州总仓"
}
]
}
}
② 订单看板统计接口:
GET /api/oms/report/today
响应:
{
"code": 200,
"data": {
"todayOrderCount": 342,
"todayOrderAmount": 12486.50,
"currency": "USD",
"pendingCount": 12,
"waitingShipCount": 87,
"shippedCount": 198,
"abnormalCount": 5,
"urgentShipCount": 3,
"platformBreakdown": [
{ "platform": "AMAZON", "count": 201, "amount": 7823.45 },
{ "platform": "TIKTOK", "count": 89, "amount": 2943.20 },
{ "platform": "SHOPEE", "count": 52, "amount": 1719.85 }
],
"hourlyTrend": [
{ "hour": "08:00", "count": 23 },
{ "hour": "09:00", "count": 41 }
]
}
}
14.4 模块专用错误码
PIM 商品模块(14000-14099):
| 错误码 | 含义 |
|---|---|
| 14001 | SPU 不存在 |
| 14002 | SKU 不存在或已停售 |
| 14003 | 商品编码已存在 |
| 14004 | 分类不存在 |
| 14005 | 上架前必须完善商品信息 |
| 14006 | 必须至少有1个SKU才能上架 |
| 14007 | HS编码格式不正确(需6-10位数字) |
| 14008 | 该分类下还有商品,不能删除 |
| 14009 | AI翻译服务暂时不可用 |
| 14010 | 语言代码不支持 |
OMS 订单模块(15000-15099):
| 错误码 | 含义 |
|---|---|
| 15001 | 订单不存在 |
| 15002 | 订单状态不允许此操作 |
| 15003 | 库存不足,订单无法处理 |
| 15004 | 该订单已存在(幂等拦截) |
| 15005 | 退款金额超过订单金额 |
| 15006 | 订单已发货,不能取消 |
| 15007 | 平台订单号重复 |
| 15008 | 拆单失败,明细不满足拆单条件 |
| 15009 | 合单失败,订单状态不一致 |
| 15010 | Webhook 签名验证失败 |
今日总结与作业
今日知识点回顾
PIM 商品模块:
- SPU vs SKU 核心区别:SPU 是产品标准单元(一类商品),SKU 是最小可销售单元(一个具体规格)
- SKU 自动生成:规格项的笛卡尔积(颜色3×版本2=6个SKU)
- 多语言内容管理:
product_i18n表按ref_type + ref_id + lang_code三字段唯一 - 多维度定价:成本价/建议售价/平台售价/活动价,查询时有优先级顺序
- 最低售价保护公式:涵盖采购成本、物流、汇率、平台佣金、利润率
- 商品上架前完整性校验:7 项必填校验,任一不满足阻止上架
- 自动下架规则:零库存/定时下架/认证过期三种触发条件
OMS 订单模块:
- 多平台接入:标准化适配层,各平台字段映射到内部统一格式
- 幂等设计:
UNIQUE KEY(tenant_id, platform, platform_order_no)防止重复导入 - Webhook 处理:立即返回 200,异步处理业务(不能在 HTTP 请求内处理耗时操作)
- 超卖防护核心:Redis
DECRBY原子操作 → 失败立即INCRBY回滚 → 异步同步 DB - 订单 10 状态机:待处理→风控→待备货→备货中→待发货→已发货→运输中→已签收→已完成/售后中
- 拆单规则:跨仓库/物流限制/超重/平台要求四种触发条件
- 合单规则:同买家同地址2小时内,且总重量不超限
- 退款 5 种类型及各自的库存处理方式
今日作业
作业 1:数据库执行(必做)
- 执行 PIM 模块全部建表 SQL(7 张表)
- 执行 OMS 模块全部建表 SQL(6 张表)
- 插入测试数据并验证:
-- 插入商品分类
INSERT INTO `product_category` (`id`, `tenant_id`, `parent_id`, `category_name`, `category_name_en`, `level`, `path`, `sort_order`, `status`)
VALUES
(10, 101, 0, '电子产品', 'Electronics', 1, '/10/', 1, 1),
(11, 101, 10, '音频设备', 'Audio', 2, '/10/11/', 1, 1),
(12, 101, 11, '蓝牙耳机', 'Bluetooth Earphones', 3, '/10/11/12/', 1, 1);
-- 插入 SPU
INSERT INTO `product_spu` (
`id`, `tenant_id`, `spu_code`, `spu_name`, `category_id`, `category_path`,
`brand`, `hs_code`, `origin_country`, `status`
) VALUES (
8001, 101, 'SPU-20250117-0001', '蓝牙耳机Pro', 12, '/10/11/12/',
'FlexBrand', '8518300090', 'CN', 2 -- 已上架
);
-- 插入 SKU(两个颜色)
INSERT INTO `product_sku` (
`id`, `tenant_id`, `spu_id`, `sku_code`, `sku_name`,
`barcode`, `spec_values`, `gross_weight_g`, `length_mm`, `width_mm`, `height_mm`,
`is_battery`, `cost_price`, `abc_class`, `status`
) VALUES
(1000001, 101, 8001, 'SKU-EARPHONE-BK', '蓝牙耳机Pro-黑色',
'6901234567890', '{"颜色":"黑色"}', 120.00, 150, 80, 50, 1, 35.00, 'A', 1),
(1000002, 101, 8001, 'SKU-EARPHONE-WH', '蓝牙耳机Pro-白色',
'6901234567891', '{"颜色":"白色"}', 120.00, 150, 80, 50, 1, 35.00, 'A', 1);
-- 插入多语言内容
INSERT INTO `product_i18n` (
`id`, `tenant_id`, `ref_type`, `ref_id`, `lang_code`,
`title`, `description`
) VALUES
(9001, 101, 'SPU', 8001, 'zh-CN',
'【2025新款】蓝牙耳机Pro 主动降噪 高音质',
'采用最新蓝牙5.3技术,主动降噪效果出色...'),
(9002, 101, 'SPU', 8001, 'en-US',
'Bluetooth Earphone Pro - Active Noise Cancelling Hi-Fi',
'Equipped with Bluetooth 5.3 technology, outstanding ANC performance...');
-- 插入价格(亚马逊美国站售价)
INSERT INTO `product_sku_price` (
`id`, `tenant_id`, `sku_id`, `price_type`, `platform`, `country_code`, `price`, `currency`
) VALUES
(7001, 101, 1000001, 3, 'AMAZON', 'US', 29.99, 'USD'),
(7002, 101, 1000001, 3, 'TIKTOK', NULL, 27.99, 'USD'),
(7003, 101, 1000002, 3, 'AMAZON', 'US', 31.99, 'USD');
-- 插入测试订单
INSERT INTO `order_main` (
`id`, `tenant_id`, `order_no`, `platform`, `platform_order_no`,
`total_amount`, `payment_amount`, `currency`, `exchange_rate`, `cny_amount`,
`status`, `warehouse_id`,
`platform_order_time`, `delivery_deadline`
) VALUES (
7001, 101, 'ORD-20250117-0001', 'AMAZON', '112-3456789-0123456',
29.99, 29.99, 'USD', 7.20, 215.93,
4, 4001,
'2025-01-17 08:30:00', '2025-01-19'
);
-- 订单明细
INSERT INTO `order_item` (
`id`, `tenant_id`, `order_id`, `order_no`, `sku_id`, `sku_code`, `sku_name`,
`platform_sku_id`, `quantity`, `unit_price`, `amount`, `currency`
) VALUES (
7101, 101, 7001, 'ORD-20250117-0001', 1000001, 'SKU-EARPHONE-BK', '蓝牙耳机Pro-黑色',
'B0XXXXXX', 1, 29.99, 29.99, 'USD'
);
-- 收货地址
INSERT INTO `order_address` (
`id`, `tenant_id`, `order_id`, `receiver_name`, `phone`,
`country_code`, `country_name`, `state`, `city`,
`address_line1`, `zip_code`, `full_address`, `is_verified`
) VALUES (
7201, 101, 7001, 'John Smith', '+1-310-555-0001',
'US', 'United States', 'California', 'Los Angeles',
'123 Main Street Apt 4B', '90001',
'John Smith, 123 Main Street Apt 4B, Los Angeles, California 90001, US', 1
);
-- 验证:查询订单完整信息
SELECT
o.order_no,
o.platform,
o.platform_order_no,
o.payment_amount,
o.currency,
CASE o.status
WHEN 0 THEN '待处理'
WHEN 4 THEN '待发货'
WHEN 8 THEN '已完成'
ELSE CONCAT('状态:', o.status)
END AS status_name,
a.receiver_name,
a.country_code,
a.city,
i.sku_name,
i.quantity,
i.unit_price
FROM order_main o
LEFT JOIN order_address a ON o.id = a.order_id
LEFT JOIN order_item i ON o.id = i.order_id
WHERE o.tenant_id = 101 AND o.is_deleted = 0;
作业 2:业务分析题(必做)
-
SKU 自动生成:一个 SPU 配置了 3 个规格项:颜色(黑/白/蓝/红=4个选项)、尺码(S/M/L/XL=4个选项)、版本(基础版/Pro版=2个选项),请问:
- 理论上会生成多少个 SKU?
- 如果其中”蓝色 + XL + Pro版”这个组合实际上不销售,应该怎么处理?(库存层面和商品层面各怎么操作?)
-
超卖防护:假设 Redis 中某 SKU 的库存是 1 件,同时有 3 个来自不同平台的订单在同一毫秒内到达,分别执行
DECRBY sku:stock:101:1000001:4001 1:- Redis 的单线程模型如何保证这 3 次 DECRBY 不会同时拿到”成功”?
- 第 1 次 DECRBY 结果为 0,第 2 次结果为 -1,第 3 次结果为 -2,请描述后两次应如何处理?
-
Webhook 的正确处理:为什么 Webhook 接口一定要”立即返回 200,异步处理业务”?如果不这样做,会出现什么问题?请描述一个具体的故障场景。
-
退款对库存的影响:一个订单,买家购买了 2 件蓝牙耳机,订单发货后买家申请”退货退款”。请描述从申请退款到退款完成,
inventory表的quantity、frozen_qty字段在哪些步骤发生了什么变化?
参考答案:
-
理论 SKU 数量是
4 × 4 × 2 = 32个。如果“蓝色 + XL + Pro版”不销售,商品层面不要生成这个 SKU,或者生成后设置为停用/不可售,避免同步到平台。库存层面不应该为这个组合创建可售库存,也不能让订单匹配到这个 SKU。如果这个组合曾经销售过,不能物理删除,只能下架或禁用,避免影响历史订单。 -
Redis 单线程执行命令,同一个 key 的 3 次
DECRBY会排队顺序执行,不会同时读到库存 1。第 1 次结果为 0,表示扣减成功且库存刚好卖完;第 2 次和第 3 次结果为负数,说明库存不足,业务层必须立即执行补偿,把 Redis 库存加回去,或者使用 Lua 脚本在扣减前判断库存是否足够,库存不足直接返回失败。后两个订单应进入缺货、取消、待人工处理或切换仓库流程,不能继续创建正常待发货订单。 -
Webhook 是第三方平台主动推送订单或状态变化的接口,必须快速返回 200,否则平台会认为推送失败并重复重试。如果接口里直接做复杂业务,比如查库存、创建订单、调用 WMS、写财务数据,一旦某一步耗时超过平台超时时间,就会导致平台重复推送。正确做法是先校验签名、记录原始消息、判断幂等,然后立即返回 200,后续业务通过 MQ 或异步任务处理。
-
订单发货后,库存已经从可售库存扣减,
quantity已减少,frozen_qty已释放为 0。买家申请退货退款时,如果货还没有退回仓库,quantity和frozen_qty都不应增加,只记录售后单状态。仓库收到退货并质检合格后,quantity增加 2;如果质检不合格,则不进入可售库存,可以增加defective_qty或进入不良品库位。退款完成只影响财务金额,不应该直接改变库存,库存变化必须以仓库实际收货质检为准。
作业 3:SQL 练习(必做)
-- 练习1:查询某SPU(id=8001)的所有SKU,
-- 以及每个SKU在广州总仓(warehouse_id=4001)的可售库存
-- 要求:显示 sku_code、sku_name、spec_values、可售库存量
-- 提示:需要 JOIN product_sku 和 inventory 表
-- 你的答案:
SELECT
ps.sku_code,
ps.sku_name,
ps.spec_values,
COALESCE(i.quantity - i.frozen_qty - i.defective_qty - i.reserved_qty, 0) AS available_qty
FROM product_sku ps
LEFT JOIN inventory i ON ps.id = i.sku_id
AND i.tenant_id = ps.tenant_id
AND i.warehouse_id = 4001
AND i.location_id IS NULL
AND i.is_deleted = 0
WHERE ps.tenant_id = 101
AND ps.spu_id = 8001
AND ps.is_deleted = 0
ORDER BY ps.sku_code;
-- 练习2:统计当日各平台订单数量和总金额(按平台分组)
-- 要求显示:platform、订单数、总金额(USD)、折合人民币总额
-- 你的答案:
SELECT
platform,
COUNT(*) AS order_count,
SUM(payment_amount) AS total_amount_usd,
SUM(cny_amount) AS total_amount_cny
FROM order_main
WHERE tenant_id = 101
AND is_deleted = 0
AND platform_order_time >= CURDATE()
AND platform_order_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY)
GROUP BY platform
ORDER BY order_count DESC;
-- 练习3:查询所有待发货(status=4)且距离最晚发货截止日期
-- 不足 48 小时的紧急订单,按截止日期升序排列
-- 要求显示:order_no、platform、delivery_deadline、距截止剩余小时数
-- 你的答案:
SELECT
order_no,
platform,
delivery_deadline,
TIMESTAMPDIFF(HOUR, NOW(), delivery_deadline) AS hours_left
FROM order_main
WHERE tenant_id = 101
AND is_deleted = 0
AND status = 4
AND delivery_deadline >= NOW()
AND delivery_deadline < DATE_ADD(NOW(), INTERVAL 48 HOUR)
ORDER BY delivery_deadline ASC;
-- 练习4:查询某SKU(id=1000001)在各平台各国家的全部价格配置
-- 按 price_type 和 platform 排序
-- 你的答案:
SELECT
sku_id,
price_type,
platform,
country_code,
price,
currency,
effective_start,
effective_end,
status
FROM product_sku_price
WHERE tenant_id = 101
AND sku_id = 1000001
AND is_deleted = 0
ORDER BY price_type, platform, country_code;
作业 4:系统设计题(选做)
新需求:支持”商品组合销售”(Bundle)。即把多个独立 SKU 打包成一个组合商品,如”耳机 + 充电线 + 保护套 = 节日礼盒套装”,以一个独立的 SKU 形式销售和库存管理。
请设计:
- 需要新增什么表来存储 Bundle 与其包含的 SKU 的关系?(给出建表 SQL)
- Bundle 商品的库存如何计算?(当组成 Bundle 的某个子 SKU 库存为 0 时,Bundle 的可售数量应该是多少?)
- 买家购买 Bundle 后,发货时如何处理?(一个 Bundle 对应出库单里的几条明细?)
- Bundle 下单时,超卖防护如何设计?(需要同时扣减多个 SKU 的库存,如何保证原子性?)
参考答案:
- 可以把 Bundle 本身也建成一个独立 SKU,然后新增关系表记录 Bundle SKU 由哪些子 SKU 组成。
CREATE TABLE `product_bundle_item` (
`id` BIGINT PRIMARY KEY COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`bundle_sku_id` BIGINT NOT NULL COMMENT '组合商品SKU ID',
`child_sku_id` BIGINT NOT NULL COMMENT '子商品SKU ID',
`child_sku_code` VARCHAR(64) NOT NULL COMMENT '子商品SKU编码',
`quantity` INT NOT NULL COMMENT '每个Bundle需要的子SKU数量',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
UNIQUE KEY `uk_bundle_child` (`tenant_id`, `bundle_sku_id`, `child_sku_id`),
KEY `idx_child_sku` (`tenant_id`, `child_sku_id`)
) COMMENT='组合商品明细表';
-
Bundle 可售库存取决于所有子 SKU 能组成多少套,计算公式是:
MIN(子SKU可售库存 / 每套需要数量)。例如耳机库存 100、充电线库存 40、保护套库存 60,每套各需要 1 个,则 Bundle 可售数量是 40。如果任意一个子 SKU 库存为 0,Bundle 可售数量就是 0。 -
买家购买 1 个 Bundle 后,订单明细可以展示一条 Bundle 商品,但发货出库单需要拆成多条子 SKU 明细,例如耳机 1 件、充电线 1 件、保护套 1 件。仓库实际拣货和扣库存时扣的是子 SKU,不是只扣 Bundle SKU。
-
Bundle 下单需要同时扣减多个子 SKU。Redis 防超卖建议用 Lua 脚本实现原子校验和扣减:先检查所有子 SKU 库存是否都足够,只要有一个不足,全部不扣;全部足够时,再一次性扣减所有子 SKU。数据库落库时也要有订单幂等和库存流水,避免 Redis 成功但订单创建失败后无法补偿。
明日预告
第六天:物流管理系统(TMS)+ 跨境合规
明天将完整实现跨境国际物流的全链路管理,包括:
- 物流商与渠道管理(含物流类型对比:空运/海运/专线)
- 运单智能分配规则引擎(按国家/重量/商品类型/时效要求自动选渠道)
- 面单打印与批量发货(对接顺丰/EMS/DHL等物流商 API)
- 物流轨迹追踪与异常预警(停滞/扣押/拒收)
- 物流费用核算(首重/续重/材积重/附加费)
- 跨境合规:HS 编码/VAT/DDP/DDU
预习建议:
- 了解国际物流的基本流程:揽收→出口清关→国际运输→进口清关→末端派送→签收
- 了解什么是”材积重”(体积重)及其计算方式
- 思考:为什么同一家快递公司,发往不同国家、不同重量的包裹,价格差异会这么大?
学习提示:今天内容量是整个课程中最大的一天(PIM + OMS 两个模块),学完可能会感到有些密集,这完全正常。
今天最核心的 2 个知识点,必须反复理解到能清楚讲出来:
- Redis DECRBY 防超卖:面试必考,要能画出时序图解释为什么原子操作能防超卖
- Webhook 的正确处理方式:立即返回 200 + 异步处理,这是所有对接外部平台的通用最佳实践
其次是 SPU/SKU 两级体系和订单状态机,这两个是做电商系统的基础认知,面试官会假设你已经掌握。