核心业务流程
商品管理(PIM)核心流程
SPU/SKU 两级商品体系
什么是 SPU 和 SKU?
SPU(Standard Product Unit)是标准产品单元,描述一类商品的共性属性。 SKU(Stock Keeping Unit)是最小库存单元,描述一个具体规格的商品。
举例说明:
比如”蓝牙耳机Pro”是一个 SPU,它有以下共性属性:
- 品牌:Sony
- 材质:ABS塑料
- 连接方式:蓝牙5.3
- HS编码:8518.30(海关清关用)
这个 SPU 下有多个 SKU:
- SKU-001:黑色-无线版
- SKU-002:白色-无线版
- SKU-003:黑色-有线版
每个 SKU 有自己的差异化属性:
- 规格值:{“颜色”:“黑色”,“版本”:“无线版”}
- 重量:85g
- 成本价:35元
- 条形码:6901234567890
为什么要设计两级体系?
如果只有 SKU,每个 SKU 都要单独维护品牌、材质、HS编码等信息,会造成大量数据冗余。当需要修改品牌信息时,要修改所有 SKU,维护成本很高。
有了 SPU,共性信息只需维护一次,所有 SKU 共享。这样既可以实现商品信息的共享,也可以保留不同规格的 SKU 的独立信息。
SPU 和 SKU 的关联关系:
SPU(蓝牙耳机Pro)
├─ 共性属性:品牌、材质、连接方式、HS编码
├─ SKU-001(黑色-无线版)
│ └─ 差异化属性:规格值、重量、成本价、条形码
├─ SKU-002(白色-无线版)
│ └─ 差异化属性:规格值、重量、成本价、条形码
└─ SKU-003(黑色-有线版)
└─ 差异化属性:规格值、重量、成本价、条形码
SKU 自动生成(笛卡尔积算法):
这是面试必问的技术点。运营配置规格项后,系统自动计算所有可能的组合。
业务场景:
运营创建商品时,先创建 SPU,填写共性信息。然后配置规格项:
- 颜色:黑色、白色、蓝色(3个选项)
- 版本:无线版、有线版(2个选项)
系统会自动计算笛卡尔积,生成 3×2=6 个 SKU:
- 黑色-无线版
- 黑色-有线版
- 白色-无线版
- 白色-有线版
- 蓝色-无线版
- 蓝色-有线版
笛卡尔积算法实现:
@Service
public class SkuGeneratorService {
/**
* 根据规格项自动生成 SKU 组合
*
* @param spuId SPU ID
* @param specOptions 规格选项,例如:
* {
* "颜色": ["红色", "蓝色", "黑色"],
* "尺码": ["S", "M", "L"]
* }
* @return 生成的 SKU 列表(3×3=9个SKU)
*/
public List<ProductSku> generateSkus(Long spuId, Map<String, List<String>> specOptions) {
// 1. 计算笛卡尔积
List<Map<String, String>> combinations = cartesianProduct(specOptions);
// 2. 为每个组合创建 SKU
List<ProductSku> skus = new ArrayList<>();
int sequence = 1;
for (Map<String, String> combination : combinations) {
ProductSku sku = new ProductSku();
sku.setSpuId(spuId);
sku.setSkuCode(generateSkuCode(spuId, sequence++));
sku.setSkuName(buildSkuName(spuId, combination));
sku.setSpecValues(JSON.toJSONString(combination));
sku.setStatus(0); // 草稿状态
skus.add(sku);
}
return skus;
}
/**
* 计算笛卡尔积(核心算法)
* 输入:{"颜色": ["红色", "蓝色"], "尺码": ["S", "M"]}
* 输出:[
* {"颜色": "红色", "尺码": "S"},
* {"颜色": "红色", "尺码": "M"},
* {"颜色": "蓝色", "尺码": "S"},
* {"颜色": "蓝色", "尺码": "M"}
* ]
*/
private List<Map<String, String>> cartesianProduct(Map<String, List<String>> specOptions) {
List<Map<String, String>> result = new ArrayList<>();
result.add(new HashMap<>());
for (Map.Entry<String, List<String>> entry : specOptions.entrySet()) {
String specName = entry.getKey();
List<String> specValues = entry.getValue();
List<Map<String, String>> temp = new ArrayList<>();
for (Map<String, String> existing : result) {
for (String value : specValues) {
Map<String, String> newCombination = new HashMap<>(existing);
newCombination.put(specName, value);
temp.add(newCombination);
}
}
result = temp;
}
return result;
}
private String generateSkuCode(Long spuId, int sequence) {
return String.format("SKU%d-%04d", spuId, sequence);
}
private String buildSkuName(Long spuId, Map<String, String> combination) {
ProductSpu spu = spuMapper.selectById(spuId);
String specStr = combination.values().stream().collect(Collectors.joining("-"));
return spu.getSpuName() + "-" + specStr;
}
}
面试时怎么讲:
我们的商品管理采用 SPU/SKU 两级体系。
SPU 是标准产品单元,用来描述整个产品的所有共用信息,比如品牌、材质、连接方式、HS编码。SKU 是 SPU 下的一个具体规格的产品,同一个 SPU 下可以有多个 SKU,这些 SKU 都有相同的 SPU 属性信息。
如果把所有商品信息都保存到每个 SKU 上,会导致很多重复的冗余信息存储。所以我们使用 SPU+SKU 这样的设计,既可以实现商品信息的共享,也可以保留不同规格的 SKU 的独立信息。
当运营创建商品时,先创建 SPU,填写共性信息。然后配置规格项,比如颜色有黑色、白色、蓝色三种,版本有无线版、有线版两种。系统会自动计算笛卡尔积,生成 3×2=6 个 SKU。
笛卡尔积算法的实现是:初始化一个空组合列表,遍历每个规格项,对于每个规格值,和已有的组合进行拼接,最终得到所有可能的组合。每个 SKU 的 spec_values 字段用 JSON 存储规格值,比如 {"颜色":"黑色","版本":"无线版"}。
有些商品的 SKU 不适合笛卡尔积生成,比如有的规格组合是不存在的。我们可以先进行笛卡尔积生成,然后再手动删除一些不存在的 SKU。
这样做的好处是:共性属性只需维护一次,规格组合自动生成,避免重复录入,大幅提高录入效率。
多语言内容管理
为什么需要多语言?
跨境电商的用户分布在全球各地,Amazon 美国站的用户看英文,日本站的用户看日文。如果商品标题是中文,美国用户根本看不懂,商品排名和转化率极低。所以我们需要为每个商品维护多语言内容。
如何实现多语言功能?
我们用 product_i18n 表存储不同语言的商品内容。表结构是 ref_type(关联类型:SPU/SKU)+ ref_id(关联ID)+ lang_code(语言代码)三字段唯一索引。
实现流程:
-
运营人员录入中文内容
- 在 SaaS 平台的商品管理页面,运营人员只需要录入一种语言的商品信息(通常是中文)
- 填写商品标题、描述、卖点等内容
- 保存到
product_i18n表,lang_code='zh-CN'
-
选择需要翻译的语言
- 录入成功后,运营人员可以选择是否需要进行 AI 翻译
- 勾选需要翻译的语种:英语、日语、韩语、德语等
- 点击”一键翻译”按钮
-
调用 DeepSeek API 生成翻译
- SaaS 平台从数据库查询中文内容
- 构建翻译 Prompt(包含翻译要求和商品信息)
- 调用 DeepSeek API(或 OpenAI API)进行翻译
- 解析返回的 JSON 结果
-
保存翻译内容到数据库
- 将翻译后的标题、描述、关键词保存到
product_i18n表 - 标记
is_ai_translated=1(表示是 AI 翻译,运营可以后续校对) - 使用
ON DUPLICATE KEY UPDATE实现幂等,避免重复插入
- 将翻译后的标题、描述、关键词保存到
翻译 Prompt 的内容:
系统提示词(System Prompt):
你是一位专业的跨境电商商品文案翻译和本地化专家,
精通中英日韩等多国语言和跨境电商平台规范。
用户请求(User Prompt):
请将以下中文商品信息翻译为英语(美国市场),要求:
1. 标题:简洁有力,不超过200字符,包含核心关键词
2. 卖点(Bullet Points):5条,每条不超过100字符,以动词开头
3. 描述:200-300字,突出产品价值,适合亚马逊详情页
商品信息:
标题:【2025新款】无线蓝牙耳机 主动降噪 高音质 长续航 适用苹果安卓
卖点:
- 采用最新蓝牙5.3技术,连接稳定,延迟低至30ms
- 主动降噪技术,有效消除环境噪音高达35dB
- 单次续航12小时,配合充电盒可使用48小时
请直接返回翻译结果,格式为JSON。
Prompt 如何生成的?
Prompt 是在代码中动态生成的,包含以下部分:
- 系统提示词:定义 AI 的角色(专业翻译员)
- 翻译要求:标题、描述、关键词的具体要求
- 商品信息:从数据库查询的中文内容
- 输出格式:要求返回 JSON 格式
Prompt 存储在哪里?
Prompt 模板存储在代码中(buildTranslationPrompt 方法),不存储在数据库。因为 Prompt 是固定的模板,只有商品信息是动态的。
如果需要支持多种 Prompt 模板(比如不同类目的商品用不同的翻译风格),可以把 Prompt 模板存储到配置表中,运营人员可以在后台配置。
最终达到的效果:
- 多语言内容生产效率提升 80%,从人工翻译 2-3 小时降至 AI 翻译 5 分钟
- 翻译成本降低 90%,从人工翻译 ¥200/商品 降至 API 调用 ¥2/商品
- 支持 8 种语言(英语、日语、韩语、德语、法语、西班牙语、阿拉伯语等)
- 翻译准确率 85%+,运营人工校对后可达 95%
面试时怎么讲:
跨境电商需要支持多语言,我们用 product_i18n 表存储不同语言的商品内容。
运营人员只需要录入一种语言的商品信息(通常是中文),然后点击选择需要翻译的语言,最后点击一键翻译。
系统会调用 DeepSeek 的 API 生成对应的翻译内容,然后把翻译内容存储到数据库表中。
翻译用到的 Prompt 包含三部分:系统提示词(定义 AI 角色)、翻译要求(标题、描述的具体要求)、商品信息(从数据库查询的中文内容)。
Prompt 模板存储在代码中,是动态生成的。如果需要支持多种 Prompt 模板,可以把模板存储到配置表中,运营人员可以在后台配置。
这样做的效果是:多语言内容生产效率提升 80%,翻译成本降低 90%,支持 8 种语言。
多平台多货币定价策略
为什么需要多平台定价?
跨境电商卖家通常会在多个平台销售同一个商品,比如同时在 Amazon、Shopify 独立站、eBay、TikTok Shop 上架。但是不同平台的定价策略是完全不同的:
- Amazon 竞争激烈:需要低价策略才能获得流量,可能只能加价 20%
- 独立站客户粘性高:品牌溢价能力强,可以加价 50% 甚至更高
- 不同国家成本不同:美国物流成本低,日本物流成本高,税率也不同(美国 7%,日本 10%,德国 19%)
- 汇率波动影响:同样 100 元人民币的商品,换算成美元、欧元、日元的价格是不同的
所以我们需要一个灵活的定价体系,支持:
- 同一个商品在不同平台有不同售价
- 同一个商品在同一平台的不同国家有不同售价
- 同一个商品在同一国家的同一平台可以有多个价格配置(但只能激活一个)
如何实现多平台多货币定价?
我们的定价体系分为三个层次:
1. 基础定价配置
在 SaaS 系统中,运营人员可以为每个 SKU 配置多个价格,每个价格配置包含:
- 平台(Amazon/Shopify/eBay/TikTok)
- 国家(US/JP/DE/GB)
- 货币(USD/JPY/EUR/GBP)
- 基础价格(正常售价)
- 促销价格(活动价,可选)
- 税率(根据国家自动计算)
- 生效时间和失效时间
- 状态(启用/停用)
系统用”SKU ID + 平台 + 国家”三个字段作为唯一标识,保证同一个 SKU 在同一平台的同一国家只能有一个激活的价格。
2. 智能定价策略
运营人员可以选择两种定价方式:
方式一:推荐定价(自动计算)
系统根据成本价和加价率自动计算:
- 查询 SKU 的成本价(比如 100 元)
- 设置加价率(比如 30%)
- 自动计算基础价格 = 100 × (1 + 30%) = 130 元
- 根据国家自动匹配货币和税率
- 计算最终售价 = 130 × (1 + 税率)
方式二:手动定价
运营人员可以直接输入价格,不使用推荐定价。这种方式适合:
- 需要参考竞品价格的情况
- 需要设置特殊促销价的情况
- 需要精确控制利润率的情况
3. 价格同步到电商平台
当运营人员在 SaaS 系统中修改价格后,系统会自动同步到各个电商平台:
同步流程:
- 运营人员在 SaaS 后台修改价格(比如把 Amazon 美国站的价格从 19.99 美元改成 17.99 美元)
- 系统保存新价格到数据库
- 系统调用对应平台的 API 更新价格:
- Amazon:调用 SP-API 的
updateListingItem接口 - Shopify:调用 REST API 的
PUT /admin/api/2024-01/products/{product_id}/variants/{variant_id}.json接口 - eBay:调用 Trading API 的
ReviseItem接口 - TikTok Shop:调用 Open API 的
product.update接口
- Amazon:调用 SP-API 的
- 平台返回更新结果,系统记录同步状态
- 如果同步失败,系统会重试 3 次,仍然失败则发送告警通知运营人员
同步时机:
- 实时同步:运营人员点击”保存并同步”按钮,立即同步到平台
- 定时同步:每天凌晨 2 点,系统自动检查所有价格变更,批量同步到平台
- 手动同步:运营人员可以在后台手动触发同步,用于修复同步失败的情况
4. 价格计算逻辑
当客户在电商平台下单时,系统需要计算最终售价:
计算步骤:
- 根据订单的 SKU ID、平台、国家,查询对应的价格配置
- 判断是否有促销价:
- 如果有促销价且在有效期内,使用促销价
- 否则使用基础价格
- 计算含税价格:
- 最终售价 = 价格 × (1 + 税率)
- 比如基础价格 100 美元,美国税率 7%,最终售价 = 100 × 1.07 = 107 美元
- 四舍五入到 2 位小数
举例说明:
假设一个蓝牙耳机的成本价是 50 元人民币:
| 平台 | 国家 | 货币 | 加价率 | 基础价格 | 税率 | 最终售价 |
|---|---|---|---|---|---|---|
| Amazon | 美国 | USD | 20% | $8.57 | 7% | $9.17 |
| Shopify | 美国 | USD | 50% | $10.71 | 7% | $11.46 |
| Amazon | 日本 | JPY | 20% | ¥1,028 | 10% | ¥1,131 |
| Amazon | 德国 | EUR | 20% | €7.71 | 19% | €9.17 |
可以看到,同一个商品在不同平台、不同国家的售价是完全不同的。
这个定价策略对租户有什么用?
- 提高定价效率:不需要在每个平台单独设置价格,在 SaaS 系统中统一管理,一键同步到所有平台
- 灵活的定价策略:可以根据不同平台的竞争情况,设置不同的加价率,最大化利润
- 自动计算含税价格:不需要手动计算税率,系统自动根据国家计算含税价格,避免出错
- 支持促销活动:可以设置促销价格和有效期,到期后自动恢复原价
- 价格变更可追溯:所有价格变更都有记录,可以查看历史价格,分析价格对销量的影响
如果 SaaS 的定价发生了变更,电商平台如何同步?
这是一个非常重要的问题。我们的同步机制是这样的:
实时同步(推荐):
- 运营人员修改价格后,点击”保存并同步”按钮
- 系统立即调用电商平台的 API 更新价格
- 通常 1-2 分钟内,电商平台的价格就会更新
- 适合需要快速调价的场景(比如竞品降价,我们也要跟着降价)
定时同步(兜底):
- 每天凌晨 2 点,系统自动检查所有价格变更
- 批量调用电商平台的 API 更新价格
- 适合批量调价的场景(比如汇率变化,需要调整所有商品的价格)
异常处理:
- 如果同步失败(比如网络超时、平台 API 限流),系统会自动重试 3 次
- 如果重试 3 次仍然失败,系统会发送告警通知运营人员
- 运营人员可以在后台查看同步失败的记录,手动触发重新同步
同步状态管理:
- 每次同步都会记录同步状态:待同步、同步中、同步成功、同步失败
- 运营人员可以在后台查看每个 SKU 在每个平台的同步状态
- 如果发现某个平台的价格没有同步成功,可以手动触发重新同步
面试时怎么讲:
我们的定价体系支持多平台多货币。同一个 SKU 可以在不同平台、不同国家设置不同的价格。
定价方式有两种:推荐定价和手动定价。 推荐定价是根据成本价和加价率自动计算,比如成本价 100 元,加价率 30%,基础价格就是 130 元。 手动定价是运营人员直接输入价格,适合需要参考竞品价格的情况。
价格计算时,会先判断是否有促销价,有促销价就用促销价,没有就用基础价格。然后根据国家的税率计算含税价格,比如美国税率 7%,最终售价 = 价格 × 1.07。
当运营人员在 SaaS 系统中修改价格后,系统会自动同步到电商平台。同步方式有两种:实时同步和定时同步。实时同步是运营人员点击按钮立即同步,1-2 分钟内平台价格就会更新。定时同步是每天凌晨 2 点批量同步,作为兜底机制。
如果同步失败,系统会自动重试 3 次,仍然失败则发送告警通知运营人员。运营人员可以在后台查看同步状态,手动触发重新同步。
这样做的好处是:提高定价效率,不需要在每个平台单独设置价格;灵活的定价策略,可以根据不同平台设置不同加价率;自动计算含税价格,避免出错;价格变更可追溯,可以分析价格对销量的影响。“
订单管理(OMS)核心流程
多平台订单接入完整流程
业务场景:
跨境电商卖家通常会在多个平台开店,比如 Amazon、Shopify 独立站、eBay、TikTok Shop。每个平台都会产生订单,但是库存、仓储、物流、财务都是统一管理的。所以我们需要把所有平台的订单统一接入到 SaaS 供应链系统中,进行统一处理。
订单接入的两种方式:
方式一:主动拉取(定时任务)
适用于不支持 Webhook 的平台,比如 Amazon、eBay。
- 系统每 5 分钟运行一次定时任务
- 调用平台的 API 拉取最近 24 小时的新订单
- 比如 Amazon 的 SP-API 提供了
GetOrders接口,可以根据时间范围查询订单 - 如果订单数量很多,需要用
NextToken分页拉取
方式二:被动接收(Webhook 推送)
适用于支持 Webhook 的平台,比如 Shopify、TikTok Shop。
- 平台在订单创建、支付、发货等事件发生时,主动推送订单数据到我们的系统
- 我们需要提供一个 HTTP 接口接收推送,比如
/webhook/shopify - 平台要求我们必须在 3 秒内返回 200 状态码,否则认为失败并重发
- 所以我们不能在 HTTP 请求内处理业务逻辑,必须立即返回 200,然后异步处理
订单同步之后的完整流程:
无论是主动拉取还是被动接收,订单进入系统后,都要经过以下 7 个步骤:
步骤 1:签名验证(仅 Webhook)
如果是 Webhook 推送的订单,必须先验证签名,防止恶意请求。
- 平台在发送 Webhook 时,会用 App Secret 对请求体进行 HMAC-SHA256 加密,生成签名放到请求头
- 我们收到请求后,用相同的 App Secret 和算法计算期望签名
- 对比收到的签名和期望签名是否一致
- 如果不一致,直接拒绝请求,记录安全日志
步骤 2:幂等校验
防止重复创建订单。
- 用”平台 + 平台订单号”作为唯一标识,查询数据库
- 如果订单已存在,检查状态是否有更新(比如从待支付变成已支付)
- 如果状态有更新,同步更新本地状态,然后返回成功
- 如果订单不存在,继续创建流程
步骤 3:格式转换(适配器模式)
不同平台的订单数据格式是不同的,我们需要统一转换成内部标准格式。
- Amazon 的订单字段叫
AmazonOrderId,Shopify 叫id,eBay 叫orderId - 我们需要把这些字段统一映射到内部的
platform_order_no字段 - 同时保存原始 JSON 到数据库,方便后续排查问题
- 创建订单主表、订单明细表、收货地址表
步骤 4:库存分配检查
检查仓库是否有足够的库存来满足这个订单。
- 根据订单的 SKU 和数量,查询 WMS(仓储管理系统)的可用库存
- 根据收货地址,选择最优仓库(距离最近、库存充足)
- 如果库存充足,继续下一步
- 如果库存不足,订单状态设置为”缺货挂起”,等待补货后再处理
步骤 5:风控检查
对订单进行风险评估,防止欺诈订单。
我们的风控规则引擎会检查以下几个维度:
- 地址有效性验证:收货地址是否真实存在,是否是高风险地区
- 金额异常检测:订单金额是否异常(比如单价 10 元的商品,下单 1000 件)
- 高频下单检测:同一个买家在短时间内下了多个订单(可能是刷单)
- 支付方式检测:是否使用高风险支付方式(比如货到付款)
- 买家信用检测:买家是否有退款、拒收等不良记录
每个规则会给出一个风险分数(0-100),最终汇总成总风险分数:
- 风险分数 ≤ 70:自动放行,进入下一步
- 风险分数 > 70:标记为”风控审核”状态,需要人工审核
步骤 6:订单状态流转
根据风控结果,订单进入不同的状态:
- 如果风控通过,订单状态变为”待备货”
- 如果需要人工审核,订单状态变为”风控审核”
- 如果库存不足,订单状态变为”缺货挂起”
步骤 7:推送到 WMS
如果订单通过了库存检查和风控检查,就可以开始备货了。
- 系统发送 MQ 消息给 WMS(仓储管理系统)
- 消息内容包含:订单号、SKU、数量、收货地址、仓库 ID
- WMS 收到消息后,创建出库单,开始拣货流程
- 拣货完成后,WMS 通知 OMS 订单状态变为”待发货”
整个流程的时间线:
- 订单同步:实时(Webhook)或 5 分钟内(定时拉取)
- 签名验证 + 幂等校验 + 格式转换:< 100ms
- 库存检查 + 风控检查:< 500ms
- 推送到 WMS:< 100ms(异步消息)
- 总耗时:< 1 秒(不包括人工审核)
为什么要这样设计?
- 签名验证:防止恶意请求,保证订单来源的真实性
- 幂等校验:防止重复订单,避免重复发货和库存错乱
- 格式转换:统一数据格式,方便后续处理,同时保留原始数据方便排查问题
- 库存检查:避免超卖,保证订单能够正常发货
- 风控检查:防止欺诈订单,降低损失
- 异步处理:Webhook 必须在 3 秒内返回,所以业务逻辑必须异步处理
- MQ 解耦:OMS 和 WMS 通过 MQ 通信,解耦两个系统,提高可用性
面试时怎么讲:
我们的供应链系统需要对接多个电商平台,订单接入有两种方式:主动拉取和被动接收。
主动拉取是用定时任务每 5 分钟调用平台 API 拉取新订单,适用于 Amazon、eBay 这种不支持 Webhook 的平台。被动接收是平台通过 Webhook 实时推送订单,适用于 Shopify、TikTok Shop,我们必须在 3 秒内返回 200,所以要异步处理。
订单进入系统后,要经过 7 个步骤:签名验证、幂等校验、格式转换、库存检查、风控检查、状态流转、推送到 WMS。
签名验证是为了防止恶意请求,用 HMAC-SHA256 算法验证请求是否来自平台。幂等校验是用平台加平台订单号作为唯一标识,防止重复创建订单。格式转换是用适配器模式把各平台的字段映射到我们的标准字段。
库存检查是查询 WMS 的可用库存,根据收货地址选择最优仓库。风控检查是用规则引擎评估订单风险,包括地址验证、金额检测、高频下单检测等,风险分数大于 70 需要人工审核。
最后,如果订单通过了所有检查,就发送 MQ 消息给 WMS,开始拣货流程。整个流程在 1 秒内完成,不包括人工审核。
Webhook 签名验证详解
为什么需要签名验证?
电商平台通过 Webhook 推送订单到我们的系统,但我们的接口是公开的(有固定 URL),任何人都可以访问。如果不验证签名,会有以下风险:
- 恶意用户伪造订单:攻击者可以伪造大量订单,导致系统创建虚假订单,库存被错误冻结
- 数据被篡改:中间人攻击可能修改订单金额、收货地址等信息
- 重放攻击:攻击者截获真实的 Webhook 请求,反复发送,导致重复订单
- 系统资源浪费:大量恶意请求会消耗系统资源,影响正常业务
所以我们必须验证每个 Webhook 请求确实来自平台,且内容没有被篡改。
签名验证的完整交互流程:
第一步:平台配置(在电商平台后台)
运营人员在电商平台的后台配置 Webhook:
- 填写 Webhook URL:
https://our-saas.com/webhook/shopify - 选择要订阅的事件:订单创建、订单支付、订单取消等
- 平台生成一个 App Secret(密钥),比如:
sk_abc123xyz456 - 运营人员把这个 App Secret 保存到我们的系统配置中
第二步:平台发送 Webhook(平台端)
当订单创建时,平台会执行以下步骤:
-
准备请求体:把订单数据转换成 JSON 格式
{ "id": "12345678", "email": "customer@example.com", "total_price": "99.99", "currency": "USD", ... } -
计算签名:用 App Secret 对请求体进行 HMAC-SHA256 加密
- 输入:请求体的原始字符串(不能格式化,必须是原始的 JSON 字符串)
- 密钥:App Secret(
sk_abc123xyz456) - 算法:HMAC-SHA256
- 输出:签名字符串(Base64 编码),比如:
a3f5b2c8d1e9f4a7b6c5d8e1f2a3b4c5...
-
发送 HTTP 请求:
- URL:
https://our-saas.com/webhook/shopify - Method:POST
- Headers:
Content-Type: application/jsonX-Shopify-Hmac-SHA256: a3f5b2c8d1e9f4a7b6c5d8e1f2a3b4c5...(签名放在请求头)
- Body:订单 JSON 数据
- URL:
第三步:我们接收 Webhook(我们的系统)
我们的系统收到 Webhook 请求后,执行以下步骤:
-
提取签名:从请求头中提取
X-Shopify-Hmac-SHA256的值,这是平台发送的签名received_signature = "a3f5b2c8d1e9f4a7b6c5d8e1f2a3b4c5..." -
读取请求体:读取 HTTP 请求的 Body,得到原始 JSON 字符串
raw_body = "{\"id\":\"12345678\",\"email\":\"customer@example.com\",...}" -
计算期望签名:用相同的 App Secret 和算法,对请求体进行 HMAC-SHA256 加密
- 输入:raw_body(必须是原始字符串,不能解析成对象后再转回字符串)
- 密钥:App Secret(从配置中读取:
sk_abc123xyz456) - 算法:HMAC-SHA256
- 输出:expected_signature
-
对比签名:
if (received_signature == expected_signature) { // 签名验证通过,请求合法 立即返回 200 OK 把请求体放到 MQ,异步处理 } else { // 签名验证失败,请求非法 记录安全日志(包含 IP、时间、签名) 返回 400 Bad Request }
第四步:异步处理业务逻辑(MQ 消费者)
为什么要异步处理?因为平台要求我们必须在 3 秒内返回 200,否则认为失败并重发。如果在 HTTP 请求内处理业务逻辑(查询数据库、调用其他服务),可能超过 3 秒,导致平台重发,产生重复订单。
所以我们的做法是:
- 签名验证通过后,立即返回 200(耗时 < 100ms)
- 把请求体放到 MQ(RocketMQ)
- MQ 消费者异步处理业务逻辑:
- 幂等校验(防止重复订单)
- 格式转换(适配器模式)
- 库存检查
- 风控检查
- 保存订单到数据库
- 推送到 WMS
关键技术细节:
1. 为什么用 HMAC-SHA256?
- HMAC(Hash-based Message Authentication Code)是一种基于哈希的消息认证码
- 它结合了哈希函数(SHA-256)和密钥(App Secret)
- 即使攻击者知道算法,没有密钥也无法伪造签名
- SHA-256 是单向哈希,无法从签名反推出原始数据
2. 为什么要用原始请求体计算签名?
- 如果先解析成对象,再转回字符串,可能因为字段顺序、空格、换行等差异导致签名不一致
- 必须用原始的 HTTP Body 字符串,一个字符都不能改
3. 如何防止重放攻击?
签名验证只能保证请求来自平台,但无法防止攻击者截获真实请求后反复发送。我们的防护措施:
- 幂等校验:用平台订单号作为唯一标识,重复请求不会创建重复订单
- 时间戳验证:有些平台会在请求头中加入时间戳,我们可以拒绝超过 5 分钟的请求
- Nonce(随机数):有些平台会在每个请求中加入唯一的随机数,我们可以记录已处理的 Nonce,拒绝重复的
4. 不同平台的签名算法差异:
| 平台 | 签名算法 | 签名位置 | 编码方式 |
|---|---|---|---|
| Shopify | HMAC-SHA256 | X-Shopify-Hmac-SHA256 | Base64 |
| TikTok Shop | HMAC-SHA256 | Authorization | Hex |
| PayPal | SHA-256 | Paypal-Transmission-Sig | Base64 |
所以我们需要为每个平台实现不同的签名验证逻辑。
实际案例:Shopify Webhook 签名验证
假设 Shopify 发送了一个订单创建的 Webhook:
平台端(Shopify):
- App Secret:
sk_abc123xyz456 - 请求体:
{"id":"12345678","email":"test@example.com","total_price":"99.99"} - 计算签名:HMAC-SHA256(
sk_abc123xyz456,{"id":"12345678",...}) =a3f5b2c8... - 发送请求:
POST https://our-saas.com/webhook/shopify X-Shopify-Hmac-SHA256: a3f5b2c8... Body: {"id":"12345678","email":"test@example.com","total_price":"99.99"}
我们的系统:
- 提取签名:
received_signature = "a3f5b2c8..." - 读取请求体:
raw_body = "{\"id\":\"12345678\",...}" - 从配置读取 App Secret:
sk_abc123xyz456 - 计算期望签名:HMAC-SHA256(
sk_abc123xyz456,raw_body) =a3f5b2c8... - 对比:
received_signature == expected_signature→ 验证通过 - 返回 200,放入 MQ 异步处理
面试时怎么讲:
Webhook 签名验证是为了防止恶意请求。平台在发送 Webhook 时,会用 App Secret 对请求体进行 HMAC-SHA256 加密,生成签名放到请求头。
我们收到请求后,用相同的 App Secret 和算法计算期望签名,然后对比收到的签名和期望签名是否一致。如果一致,说明请求确实来自平台,且内容没有被篡改。
验证通过后,我们必须立即返回 200,不能在 HTTP 请求内处理业务逻辑。因为平台有超时限制(通常 3 秒),如果超时会认为失败并重发,导致重复订单。
所以我们的做法是:验证签名 → 立即返回 200 → 把请求体放到 MQ → 异步消费处理。MQ 消费者里做幂等校验、格式转换、保存订单等业务逻辑。
签名验证用的是 HMAC-SHA256 算法,它结合了哈希函数和密钥,即使攻击者知道算法,没有密钥也无法伪造签名。计算签名时必须用原始请求体,不能先解析成对象再转回字符串,否则可能因为格式差异导致签名不一致。
为了防止重放攻击,我们还会做幂等校验,用平台订单号作为唯一标识,重复请求不会创建重复订单。有些平台还会在请求头中加入时间戳,我们可以拒绝超过 5 分钟的请求。
订单幂等设计(防止重复订单)
为什么需要幂等?
在分布式系统中,订单重复创建是一个非常常见的问题。导致重复订单的原因有很多:
- Webhook 重发:平台推送订单时,如果我们的响应超时(超过 3 秒),平台会认为失败并重发
- 网络抖动:网络不稳定导致请求重复发送
- 平台故障:平台系统故障可能导致同一个订单被推送多次
- 定时任务重复拉取:定时任务拉取订单时,可能因为时间窗口重叠,拉取到重复的订单
- MQ 重试:消息队列消费失败后会重试,可能导致重复处理
如果不做幂等处理,同一个订单会被创建多次,导致:
- 重复发货,客户收到多份商品
- 库存错乱,库存被重复扣减
- 财务损失,重复发货的成本由商家承担
幂等设计的核心思想:
用”平台 + 平台订单号”作为唯一标识,保证同一个平台订单号只会创建一次 SaaS 系统的订单。
具体实现方案:
1. 数据库唯一索引(最可靠)
在订单主表中,为”平台 + 平台订单号”创建唯一索引:
- 字段:
platform(平台:AMAZON/SHOPIFY/EBAY)+platform_order_no(平台订单号) - 索引类型:UNIQUE KEY
- 作用:数据库层面保证唯一性,即使并发插入,也只会成功一条
比如:
- Amazon 订单号
AMZ-12345只能创建一次 - Shopify 订单号
SHP-12345可以创建(不同平台) - Amazon 订单号
AMZ-12345再次创建时,数据库会报唯一索引冲突错误
2. 业务层幂等校验(更友好)
在插入数据库之前,先查询订单是否已存在:
步骤 1:查询订单
- 条件:
WHERE platform = ? AND platform_order_no = ? - 如果查询结果为空,说明订单不存在,继续创建
- 如果查询结果不为空,说明订单已存在,进入步骤 2
步骤 2:检查状态是否有更新
订单已存在,但可能状态有更新。比如:
- 第一次推送:订单状态是”待支付”
- 第二次推送:订单状态变成”已支付”
我们需要同步更新本地状态:
- 对比平台推送的状态和本地状态
- 如果状态不同,更新本地状态
- 如果状态相同,直接返回成功(什么都不做)
步骤 3:创建新订单
如果订单不存在,创建新订单:
- 生成内部订单号(格式:
ORD + yyyyMMddHHmmss + 6位随机数) - 设置初始状态(待处理)
- 保存到数据库
3. 分布式锁(高并发场景)
如果并发量很高,可能出现以下情况:
- 线程 A 查询订单,发现不存在,准备插入
- 线程 B 也查询订单,也发现不存在,也准备插入
- 线程 A 和 B 同时插入,可能都成功(如果没有唯一索引)
解决方案:用分布式锁(Redis)保证同一时刻只有一个线程处理同一个订单:
加锁逻辑:
- 锁的 Key:
order:lock:{platform}:{platform_order_no} - 锁的过期时间:10 秒(防止死锁)
- 加锁成功:继续处理订单
- 加锁失败:说明其他线程正在处理,等待 100ms 后重试
处理完成后释放锁:
- 订单创建成功或已存在,释放锁
- 其他等待的线程可以继续处理
幂等处理的完整流程:
flowchart TD
A[收到订单] --> B[尝试加分布式锁]
B --> C{加锁成功?}
C -->|否| D[等待 100ms 后重试]
D --> B
C -->|是| E[查询订单是否已存在]
E --> F{订单是否存在?}
F -->|存在| G[检查状态是否有更新]
F -->|不存在| H[生成内部订单号<br/>设置初始状态]
G --> I{状态是否不同?}
I -->|不同| J[更新订单状态]
I -->|相同| K[直接返回成功]
H --> L[插入数据库]
J --> M[释放锁]
K --> M
L --> M
M --> N[返回成功]
内部订单号的生成规则:
为什么要生成内部订单号?因为不同平台的订单号格式不同,我们需要一个统一的订单号格式,方便管理和查询。
格式:ORD + yyyyMMddHHmmss + 6位随机数
举例:
- 生成时间:2026-05-22 14:30:25
- 随机数:123456
- 内部订单号:
ORD20260522143025123456
为什么要加随机数?
- 防止同一秒内创建多个订单时,订单号重复
- 6 位随机数可以支持每秒 100 万个订单(实际上不会有这么高的并发)
幂等设计的关键点:
-
唯一标识的选择:
- 不能只用平台订单号,因为不同平台可能有相同的订单号
- 必须用”平台 + 平台订单号”作为唯一标识
-
数据库唯一索引是最后一道防线:
- 即使业务层幂等校验失败,数据库唯一索引也能保证不会插入重复数据
- 唯一索引冲突时,捕获异常,返回成功(因为订单已存在)
-
状态同步很重要:
- 不能简单地忽略重复订单,要检查状态是否有更新
- 平台订单状态变化时,我们也要同步更新
-
分布式锁的必要性:
- 如果并发量不高(每秒几百个订单),可以不用分布式锁,唯一索引就够了
- 如果并发量很高(每秒几千个订单),建议用分布式锁,减少数据库压力
实际案例:
假设 Shopify 推送了一个订单,订单号是 SHP-12345:
第一次推送(订单创建):
- 平台:SHOPIFY
- 平台订单号:SHP-12345
- 订单状态:待支付
- 处理结果:查询数据库,订单不存在,创建新订单,内部订单号
ORD20260522143025123456
第二次推送(订单支付):
- 平台:SHOPIFY
- 平台订单号:SHP-12345
- 订单状态:已支付
- 处理结果:查询数据库,订单已存在,检查状态,发现状态从”待支付”变成”已支付”,更新本地状态
第三次推送(网络重发):
- 平台:SHOPIFY
- 平台订单号:SHP-12345
- 订单状态:已支付
- 处理结果:查询数据库,订单已存在,检查状态,状态没有变化,直接返回成功
面试时怎么讲:
我们用平台加平台订单号作为唯一标识,在数据库中创建唯一索引,保证同一个平台订单号只会创建一次。
收到订单后,先查询数据库,如果订单已存在,检查状态是否有更新。如果状态有更新,比如从待支付变成已支付,我们就同步更新本地状态。如果状态没有变化,直接返回成功。如果订单不存在,才创建新订单。
这样可以保证幂等性,即使平台重复推送,也不会创建重复订单。我们还会生成内部订单号,格式是 ORD 加时间戳加 6 位随机数,方便统一管理。
如果并发量很高,我们还会用 Redis 分布式锁,保证同一时刻只有一个线程处理同一个订单,减少数据库压力。锁的 Key 是订单的平台加平台订单号,过期时间 10 秒,处理完成后释放锁。
数据库唯一索引是最后一道防线,即使业务层幂等校验失败,数据库也能保证不会插入重复数据。
订单状态机设计
为什么需要状态机?
订单从创建到完成,会经历多个状态:待处理 → 风控审核 → 待备货 → 备货中 → 待发货 → 已发货 → 运输中 → 已签收 → 已完成。
如果不用状态机,状态流转逻辑会散落在各个业务方法中:
- 创建订单的方法里,要判断是否需要风控审核
- 风控审核的方法里,要判断审核通过后应该变成什么状态
- 发货的方法里,要判断当前状态是否允许发货
- 取消订单的方法里,要判断哪些状态可以取消
这样会导致:
- 状态流转逻辑分散:同一个状态的流转规则可能出现在多个方法中,难以维护
- 容易出现非法状态:比如订单从”待处理”直接跳到”已发货”,跳过了备货环节
- 难以扩展:新增一个状态或流转规则,需要修改多个方法
- 难以追溯:无法清晰地看到订单的完整状态流转路径
状态机的核心思想:
状态机是一种设计模式,用于管理对象的状态流转。它有三个核心概念:
- 状态(State):对象可能处于的各种状态,比如订单的 10 个状态
- 事件(Event):触发状态流转的动作,比如”通过风控”、“开始备货”、“发货”
- 转换(Transition):定义从一个状态到另一个状态的流转规则,比如”待处理 + 通过风控 → 待备货”
订单 10 状态定义:
| 状态编号 | 状态名称 | 说明 | 可以流转到的状态 |
|---|---|---|---|
| 0 | 待处理 | 订单刚同步进来,等待处理 | 风控审核、待备货、已取消 |
| 1 | 风控审核 | 触发风控规则,需要人工审核 | 待备货、已取消 |
| 2 | 待备货 | 风控通过,等待 WMS 开始拣货 | 备货中、已取消 |
| 3 | 备货中 | WMS 正在拣货打包 | 待发货 |
| 4 | 待发货 | 拣货完成,等待扫码发货 | 已发货 |
| 5 | 已发货 | 仓库已发货,等待物流揽收 | 运输中 |
| 6 | 运输中 | 物流商已揽收,正在运输 | 已签收 |
| 7 | 已签收 | 客户已签收,等待售后期结束 | 已完成、售后中 |
| 8 | 已完成 | 订单完成,触发财务结算 | 无(终态) |
| 9 | 售后中 | 客户申请退款/换货 | 已完成 |
| 10 | 已取消 | 订单取消,释放库存 | 无(终态) |
订单状态流转图:
stateDiagram-v2
[*] --> 待处理 : 平台订单同步进入系统
待处理 --> 风控审核 : 触发风控规则(如可疑地址/异常金额)
待处理 --> 待备货 : 风控通过 + 库存充足
待处理 --> 已取消 : 买家取消 / 超时自动取消
风控审核 --> 待备货 : 人工审核通过
风控审核 --> 已取消 : 审核拒绝(确认为异常订单)
待备货 --> 备货中 : WMS 开始拣货
待备货 --> 已取消 : 买家取消(发货前)
备货中 --> 待发货 : 拣货打包完成
待发货 --> 已发货 : 仓库扫码确认发货 + 填写运单号
已发货 --> 运输中 : 物流商揽收扫描
运输中 --> 已签收 : 末端配送签收确认
已签收 --> 已完成 : 超过售后期(通常15天)自动完成
已签收 --> 售后中 : 买家申请退款/换货
售后中 --> 已完成 : 售后关闭(无需退款/买家撤回)
已取消 --> [*]
已完成 --> [*]
状态流转的关键规则:
1. 不是所有状态都可以互相流转
比如:
- 允许:待处理 → 风控审核 → 待备货 → 备货中 → 待发货 → 已发货
- 禁止:待处理 → 已发货(跳过了备货环节)
- 禁止:已发货 → 待处理(不能回退)
2. 某些状态可以取消,某些不能
- 可以取消:待处理、风控审核、待备货(发货前)
- 不能取消:备货中、待发货、已发货、运输中(已经开始物流流程)
3. 状态流转需要前置条件(Guard)
比如从”待处理”流转到”待备货”,需要满足:
- 风控检查通过(风险分数 ≤ 70)
- 库存充足(WMS 有足够的可用库存)
如果不满足条件,状态流转会被拦截。
4. 状态流转可以触发后置动作(Action)
比如从”待发货”流转到”已发货”,会自动触发:
- 调用 WMS 扣减库存(实物库存 -N,冻结库存 -N)
- 调用 TMS 创建运单(生成运单号,打印面单)
- 发送通知给买家(订单已发货,运单号是 XXX)
Spring State Machine 的实现思路:
Spring State Machine 是 Spring 官方提供的状态机框架,我们用它来实现订单状态机。
核心配置包含三部分:
1. 定义状态(States)
// 定义所有可能的状态
states
.withStates()
.initial(OrderStatus.PENDING) // 初始状态:待处理
.states(EnumSet.allOf(OrderStatus.class)); // 所有状态
2. 定义状态流转规则(Transitions)
每个流转规则包含:
- source:源状态(从哪个状态)
- target:目标状态(到哪个状态)
- event:触发事件(什么事件触发)
- guard:前置条件(可选,满足条件才能流转)
- action:后置动作(可选,流转后执行的操作)
举例:待处理 → 风控审核
transitions
.withExternal()
.source(OrderStatus.PENDING) // 源状态:待处理
.target(OrderStatus.RISK_REVIEW) // 目标状态:风控审核
.event(OrderEvent.TRIGGER_RISK_REVIEW) // 触发事件:触发风控审核
.guard(riskReviewGuard()) // 前置条件:订单金额 > 1000 元
.action(riskReviewAction()) // 后置动作:调用风控服务
3. 定义 Guard(前置条件)
Guard 是一个判断函数,返回 true 表示允许流转,返回 false 表示拦截。
举例:风控审核的前置条件
// 订单金额 > 1000 元才需要风控审核
public Guard<OrderStatus, OrderEvent> riskReviewGuard() {
return context -> {
OrderMain order = context.getMessage().getHeaders().get("order", OrderMain.class);
return order.getPaymentAmount().compareTo(new BigDecimal("1000")) > 0;
};
}
4. 定义 Action(后置动作)
Action 是状态流转后自动执行的操作。
举例:发货的后置动作
// 发货时自动调用 WMS 扣减库存、调用 TMS 创建运单
public Action<OrderStatus, OrderEvent> shipAction() {
return context -> {
OrderMain order = context.getMessage().getHeaders().get("order", OrderMain.class);
// 1. 调用 WMS 扣减库存
wmsService.deductStock(order);
// 2. 调用 TMS 创建运单
String waybillNo = tmsService.createWaybill(order);
order.setWaybillNo(waybillNo);
// 3. 发送通知给买家
notificationService.sendShipNotification(order);
};
}
状态机的使用方式:
1. 触发状态流转
// 创建状态机消息,携带订单对象
Message<OrderEvent> message = MessageBuilder
.withPayload(OrderEvent.PASS_RISK_CHECK) // 事件:通过风控
.setHeader("order", order) // 携带订单对象
.build();
// 发送事件,触发状态流转
stateMachine.sendEvent(message);
2. 状态机自动处理
- 检查当前状态是否允许流转(根据 Transitions 配置)
- 执行 Guard 判断前置条件
- 如果允许流转,执行 Action 后置动作
- 更新订单状态到数据库
状态机的优势:
- 集中管理:所有状态流转规则都在一个配置类中,一目了然
- 保证合法性:状态机会自动拦截非法流转,比如从”待处理”直接跳到”已发货”
- 易于扩展:新增状态或流转规则,只需要修改配置,不需要改业务代码
- 自动化操作:状态流转时自动执行 Action,比如发货时自动扣减库存、创建运单
- 可追溯:可以记录每次状态流转的时间、操作人、原因,方便排查问题
实际案例:订单从创建到完成的完整流程
步骤 1:订单创建(待处理)
- 平台推送订单,系统创建订单,初始状态:待处理
步骤 2:风控检查
- 系统自动检查风控规则,计算风险分数
- 如果风险分数 > 70,触发事件
TRIGGER_RISK_REVIEW,状态变为:风控审核 - 如果风险分数 ≤ 70,触发事件
PASS_RISK_CHECK,状态变为:待备货
步骤 3:人工审核(如果需要)
- 运营人员审核订单
- 审核通过,触发事件
PASS_RISK_CHECK,状态变为:待备货 - 审核拒绝,触发事件
CANCEL,状态变为:已取消
步骤 4:开始备货
- WMS 收到出库单,开始拣货
- 触发事件
START_PREPARE,状态变为:备货中
步骤 5:备货完成
- WMS 拣货打包完成
- 触发事件
PREPARE_COMPLETE,状态变为:待发货
步骤 6:发货
- 仓库扫码确认发货
- 触发事件
SHIP,状态变为:已发货 - Action 自动执行:扣减库存、创建运单、发送通知
步骤 7:物流揽收
- 物流商揽收扫描
- 触发事件
PICKUP,状态变为:运输中
步骤 8:客户签收
- 末端配送签收确认
- 触发事件
DELIVER,状态变为:已签收
步骤 9:订单完成
- 超过售后期(15 天)
- 触发事件
COMPLETE,状态变为:已完成 - Action 自动执行:触发财务结算
面试时怎么讲:
订单状态流转比较复杂,我们用 Spring State Machine 实现状态机。
首先定义 10 个状态:待处理、风控审核、待备货、备货中、待发货、已发货、运输中、已签收、已完成、已取消。
然后配置状态流转规则,比如待处理可以流转到风控审核、待备货或已取消,但不能直接流转到已发货。每个流转规则包含源状态、目标状态、触发事件、前置条件和后置动作。
前置条件是 Guard,用于判断是否允许流转。比如从待处理流转到风控审核,Guard 判断订单金额是否超过 1000 元。后置动作是 Action,用于在状态流转后自动执行操作。比如从待发货流转到已发货,Action 自动调用 WMS 扣减库存、调用 TMS 创建运单。
使用时,我们创建一个消息,携带事件和订单对象,发送给状态机。状态机会自动检查当前状态是否允许流转,执行 Guard 判断前置条件,如果允许流转,执行 Action 后置动作,最后更新订单状态到数据库。
这样做的好处是:状态流转逻辑集中管理,保证状态流转的合法性,避免出现非法状态。比如订单不能从待处理直接跳到已发货,状态机会自动拦截。而且易于扩展,新增状态或流转规则,只需要修改配置,不需要改业务代码。
重点加分项
库存分配策略(供应链系统的核心价值)
业务场景:
跨境电商卖家通常会在多个平台开店,比如同时在 Amazon、Shopify 独立站、eBay、TikTok Shop 销售同一个商品(比如蓝牙耳机)。但是库存是统一管理的,所有平台的货都存放在同一个仓库。
这就带来一个问题:如何分配库存给各个平台?
两种库存分配策略:
策略一:共享库存池(不推荐)
把仓库的 500 件库存实时同步给每个平台,每个平台都显示 500 件。
问题:
- 用户可能在多个平台同时下单,总订单量超过 500 件
- 比如 Amazon 卖了 300 件,Shopify 卖了 250 件,总共 550 件,但仓库只有 500 件
- 导致超卖,仓库发不出货,需要取消订单,影响客户体验和平台信誉
策略二:固定分配库存(推荐)
根据各平台的销售速度,提前分配好每个平台的库存配额。
举例:
仓库总库存:500 件
分配给各平台:
├─ Amazon: 200 件(40%,销量最大)
├─ Shopify: 150 件(30%,销量中等)
├─ eBay: 100 件(20%,销量较小)
└─ 独立站: 50 件(10%,销量最小)
总计:500 件(不超过仓库库存)
优点:
- 避免超卖:每个平台只能卖自己分配到的库存
- 灵活调整:可以根据销售情况动态调整分配比例
- 统一管理:在一个系统里看到所有平台的库存分配和销售情况
库存分配的核心概念:
在 SaaS 系统中,我们为每个”SKU + 仓库 + 平台”维护三个库存数量:
-
分配数量(allocated_qty):分配给该平台的总库存
- 比如 Amazon 分配了 200 件
-
冻结数量(frozen_qty):已下单但未发货的库存
- 比如有 50 件订单正在备货,还没发货
-
可售数量(available_qty):可以继续销售的库存
- 计算公式:available_qty = allocated_qty - frozen_qty
- 比如 200 - 50 = 150 件
库存分配的完整流程:
步骤 1:采购入库
- 采购 1000 件蓝牙耳机
- WMS(仓储管理系统)入库,仓库库存变为 1000 件
步骤 2:运营人员分配库存
- 在 SaaS 后台,运营人员为各平台分配库存:
- Amazon: 400 件
- Shopify: 300 件
- eBay: 200 件
- 独立站: 100 件
- 系统检查:总分配数量(1000 件)≤ 仓库可用库存(1000 件),允许分配
步骤 3:同步库存到电商平台
- 系统调用各平台的 API,更新平台库存:
- Amazon SP-API:更新库存为 400 件
- Shopify REST API:更新库存为 300 件
- eBay Trading API:更新库存为 200 件
- 独立站:更新库存为 100 件
步骤 4:用户在 Amazon 下单
- 用户在 Amazon 下单购买 50 件
- Amazon 平台扣减自己的库存:400 - 50 = 350 件
- Amazon 推送订单到我们的 SaaS 系统(Webhook 或 API 拉取)
步骤 5:SaaS 系统冻结库存
- OMS 接收订单,创建订单记录
- 冻结 Amazon 平台的库存:
- frozen_qty: 0 + 50 = 50 件
- available_qty: 400 - 50 = 350 件
- allocated_qty: 400 件(不变,货还在仓库)
步骤 6:WMS 拣货发货
- WMS 收到出库单,开始拣货打包
- 拣货完成,扫码发货
- 扣减库存:
- OMS 平台库存:
- frozen_qty: 50 - 50 = 0 件(解冻)
- allocated_qty: 400 - 50 = 350 件(扣减分配)
- available_qty: 350 件(不变)
- WMS 仓库库存:
- quantity: 1000 - 50 = 950 件(实物出库)
- OMS 平台库存:
步骤 7:动态调整分配(可选)
- 运营人员发现 Amazon 卖得很快,库存快用完了
- 而 eBay 卖得慢,还有很多库存
- 运营人员可以把 eBay 的 50 件调给 Amazon:
- Amazon: 350 + 50 = 400 件
- eBay: 200 - 50 = 150 件
- 系统自动同步到各平台的 API
库存同步到电商平台的机制:
同步时机:
- 初次分配:运营人员首次为平台分配库存时,立即同步
- 动态调整:运营人员调整分配比例时,立即同步
- 定时同步:每小时自动同步一次,作为兜底机制
- 手动同步:运营人员可以手动触发同步,用于修复同步失败的情况
同步逻辑:
系统会计算每个平台的”可售数量”,然后调用平台 API 更新:
可售数量 = 分配数量 - 冻结数量
比如:
- Amazon 分配了 400 件
- 有 50 件订单正在备货(冻结)
- 可售数量 = 400 - 50 = 350 件
- 调用 Amazon SP-API 更新库存为 350 件
同步接口:
| 平台 | API 接口 | 说明 |
|---|---|---|
| Amazon | SP-API updateInventory | 更新 FBA 或 FBM 库存 |
| Shopify | REST API PUT /admin/api/2024-01/inventory_levels/set.json | 更新库存数量 |
| eBay | Trading API ReviseInventoryStatus | 更新 Listing 库存 |
| TikTok Shop | Open API product.updateStock | 更新商品库存 |
异常处理:
- 如果同步失败(网络超时、API 限流),系统会自动重试 3 次
- 如果重试 3 次仍然失败,系统会发送告警通知运营人员
- 运营人员可以在后台查看同步失败的记录,手动触发重新同步
为什么要这样设计?
1. 避免超卖
- 每个平台只能卖自己分配到的库存,不会出现总订单量超过仓库库存的情况
2. 灵活调整
- 可以根据各平台的销售速度,动态调整分配比例
- 比如 Amazon 卖得快,就多分配一些;eBay 卖得慢,就少分配一些
3. 统一管理
- 在一个系统里看到所有平台的库存分配和销售情况
- 不需要在每个平台单独管理库存
4. 实时监控
- 可以实时监控每个平台的剩余库存
- 如果发现某个平台库存不足,可以及时调整分配
5. 数据可追溯
- 所有库存变更都有记录,可以查看历史分配情况
- 方便分析哪个平台销售最好,优化分配策略
技术实现的关键点:
1. 数据库原子操作
冻结库存和扣减库存必须用原子操作,保证并发安全:
-- 冻结库存(原子操作)
UPDATE oms_platform_stock_allocation
SET frozen_qty = frozen_qty + 50,
available_qty = available_qty - 50
WHERE sku_id = 123
AND platform = 'AMAZON'
AND available_qty >= 50 -- 关键:保证不会超卖
如果 available_qty < 50,UPDATE 会返回 0 行,表示库存不足,订单创建失败。
2. 分布式锁(高并发场景)
如果并发量很高,可以用 Redis 分布式锁保证同一时刻只有一个线程操作同一个 SKU 的库存:
- 锁的 Key:
stock:lock:{sku_id}:{platform} - 锁的过期时间:10 秒
- 加锁成功:继续冻结库存
- 加锁失败:等待 100ms 后重试
3. 异步同步到电商平台
库存变更后,不要在主流程中同步调用平台 API,而是发送 MQ 消息异步同步:
- 主流程:冻结库存 → 返回成功
- MQ 消费者:调用平台 API 更新库存
- 如果同步失败,MQ 会自动重试
实际案例:
假设仓库有 1000 件蓝牙耳机,分配给 Amazon 400 件、Shopify 300 件。
场景 1:正常下单
- 用户在 Amazon 下单 50 件
- Amazon 扣减库存:400 - 50 = 350 件
- SaaS 系统冻结库存:frozen_qty + 50,available_qty - 50
- WMS 发货后扣减:allocated_qty - 50,仓库库存 - 50
场景 2:库存不足
- 用户在 Amazon 下单 500 件
- Amazon 库存只有 400 件,下单失败
- 用户看到提示:“库存不足,请减少购买数量”
场景 3:动态调整
- Amazon 库存快用完了(剩余 50 件)
- Shopify 库存还有很多(剩余 250 件)
- 运营人员把 Shopify 的 100 件调给 Amazon
- 系统自动同步:Amazon 库存变为 150 件,Shopify 库存变为 150 件
面试时怎么讲:
供应链系统的核心价值之一是统一管理多平台的库存分配。
商家在多个平台开店,但库存是统一的。我们的做法是固定分配库存,比如仓库有 1000 件,分配给 Amazon 400 件、Shopify 300 件、eBay 200 件。每个平台只能卖自己分配到的库存,避免超卖。
我们为每个平台维护三个库存数量:分配数量、冻结数量、可售数量。用户下单后,我们冻结库存,发货后扣减库存。运营人员可以根据销售情况动态调整分配比例,系统会自动同步到各平台的 API。
技术上的关键点是:用数据库原子操作保证库存扣减的准确性,WHERE 条件加上可售数量大于等于扣减数量,保证不会超卖。如果并发量很高,还会用 Redis 分布式锁。库存变更后,通过 MQ 异步同步到电商平台,如果同步失败会自动重试。
这样做的好处是:避免超卖,灵活调整分配策略,统一管理所有平台的库存,实时监控库存情况,数据可追溯。
订单风控规则引擎(策略模式)
为什么需要风控?
跨境电商存在大量欺诈订单和风险订单:
- 虚假地址:收货地址不存在,货物无法送达,导致退货损失
- 盗刷信用卡:使用盗刷的信用卡下单,货物发出后银行拒付,商家损失货款和货物
- 恶意刷单:刷手大量下单刷好评,被平台发现后会处罚商家
- 恶意退款:买家收到货后恶意申请退款,商家损失货物
- 测试订单:竞争对手下测试订单,探测商家的价格和库存
如果不做风控,会导致:
- 货物发出后无法签收,退货成本高
- 信用卡拒付,商家损失货款和货物
- 平台处罚,店铺被封
- 恶意退款,商家损失
所以我们需要在订单创建后、发货前进行风控检查,识别高风险订单,人工审核后再决定是否发货。
风控规则设计(5类规则):
我们设计了 5 类风控规则,每个规则对应一个分值,总分 0-100 分:
| 规则类型 | 最高分值 | 检查内容 |
|---|---|---|
| 地址有效性 | 20 分 | 邮政编码是否存在、国家-州组合是否合法、地址是否在黑名单 |
| 金额异常 | 30 分 | 订单金额是否异常、单价是否低于成本价 |
| 高频下单 | 25 分 | 同一买家短时间内下单次数是否过多 |
| 同地址大量下单 | 20 分 | 同一收货地址短时间内收到订单数量是否过多 |
| 高退款率买家 | 25 分 | 买家历史退款率是否过高 |
风险等级划分:
- 0-30 分:低风险,正常放行,自动进入备货流程
- 31-70 分:中风险,加标记,继续处理但重点监控
- 71-100 分:高风险,进入人工审核队列,运营专员 24 小时内处理
策略模式的设计思路:
风控规则引擎使用策略模式设计,这是一种行为型设计模式。
为什么用策略模式?
如果不用策略模式,风控逻辑会写成这样:
public int checkRisk(OrderMain order) {
int totalScore = 0;
// 规则1:地址有效性
if (!isValidPostalCode(order.getAddress())) {
totalScore += 20;
}
// 规则2:金额异常
if (order.getAmount() > avgAmount * 10) {
totalScore += 30;
}
// 规则3:高频下单
if (orderCount >= 5) {
totalScore += 25;
}
// ... 更多规则
return totalScore;
}
这样写的问题:
- 所有规则逻辑都在一个方法里,代码很长,难以维护
- 新增规则需要修改这个方法,违反开闭原则
- 规则之间耦合,无法单独测试某个规则
- 无法动态启用/禁用某个规则
策略模式的实现:
步骤 1:定义规则接口
/**
* 风控规则接口(策略接口)
*/
public interface RiskRule {
/**
* 获取规则类型
*/
RiskRuleType getRuleType();
/**
* 评估订单风险
* @param order 订单
* @return 风险分值(0表示无风险)
*/
int evaluate(OrderMain order);
}
步骤 2:实现具体规则(策略实现)
每个规则实现 RiskRule 接口,Spring 会自动扫描并注入到容器中。
规则 1:地址有效性
@Component
public class AddressValidityRule implements RiskRule {
@Override
public RiskRuleType getRuleType() {
return RiskRuleType.ADDRESS_VALIDITY;
}
@Override
public int evaluate(OrderMain order) {
OrderAddress address = order.getAddress();
// 1. 邮政编码是否存在
if (!isValidPostalCode(address.getPostalCode(), address.getCountryCode())) {
return 20;
}
// 2. 国家-州组合是否合法(比如美国加州的邮编必须是 9xxxx)
if (!isValidCountryState(address.getCountryCode(), address.getStateCode())) {
return 15;
}
// 3. 地址是否在黑名单(已知的欺诈地址)
if (isBlacklistAddress(address)) {
return 20;
}
return 0; // 地址有效,无风险
}
}
规则 2:金额异常
@Component
public class AmountAnomalyRule implements RiskRule {
@Override
public RiskRuleType getRuleType() {
return RiskRuleType.AMOUNT_ANOMALY;
}
@Override
public int evaluate(OrderMain order) {
// 1. 查询该 SKU 的历史平均订单金额
BigDecimal avgAmount = orderStatService.getAvgOrderAmount(order.getSkuId());
// 2. 当前订单金额超过历史均值 10 倍(可能是盗刷信用卡)
if (order.getPaymentAmount().compareTo(avgAmount.multiply(new BigDecimal("10"))) > 0) {
return 30;
}
// 3. 单价明显低于成本价(可能是测试订单或系统错误)
BigDecimal unitPrice = order.getPaymentAmount().divide(new BigDecimal(order.getQuantity()), 2, RoundingMode.HALF_UP);
if (unitPrice.compareTo(order.getCostPrice().multiply(new BigDecimal("0.5"))) < 0) {
return 25;
}
return 0;
}
}
规则 3:高频下单
@Component
public class HighFrequencyRule implements RiskRule {
@Override
public RiskRuleType getRuleType() {
return RiskRuleType.HIGH_FREQUENCY;
}
@Override
public int evaluate(OrderMain order) {
// 查询同一买家 1 小时内下单次数
int orderCount = orderMapper.countByBuyerInHours(order.getBuyerEmail(), 1);
if (orderCount >= 5) {
return 25; // 1 小时内下 5 单以上,高风险
} else if (orderCount >= 3) {
return 15; // 1 小时内下 3-4 单,中风险
}
return 0;
}
}
步骤 3:风控服务(策略上下文)
风控服务负责调用所有规则,累加分值。
@Service
public class OrderRiskService {
@Autowired
private List<RiskRule> riskRules; // Spring 自动注入所有实现了 RiskRule 接口的 Bean
/**
* 订单风控检查
*/
public int checkRisk(OrderMain order) {
int totalScore = 0;
List<String> hitRules = new ArrayList<>();
// 遍历所有规则,累加分值
for (RiskRule rule : riskRules) {
int score = rule.evaluate(order);
if (score > 0) {
totalScore += score;
hitRules.add(rule.getRuleType().getDesc() + "(" + score + "分)");
}
}
// 保存风控记录
saveRiskRecord(order, totalScore, hitRules);
return totalScore;
}
}
策略模式的优势:
- 规则可插拔:新增规则只需实现
RiskRule接口,不需要修改风控服务 - 规则可配置:每个规则的分值可以动态调整,不需要改代码
- 规则可测试:每个规则都是独立的类,可以单独编写单元测试
- 规则可追溯:每次风控检查都会记录命中的规则和分值,方便排查问题
- 规则可扩展:可以轻松添加新的规则类型,比如”买家信用评分”、“设备指纹识别”
实际案例:
假设一个订单的风控检查过程:
订单信息:
- 买家邮箱:test@example.com
- 收货地址:美国加州,邮编 12345(无效邮编)
- 订单金额:$999.99
- 历史平均金额:$50
- 1 小时内下单次数:6 次
风控检查过程:
- 地址有效性规则:邮编 12345 无效(加州邮编应该是 9xxxx),返回 20 分
- 金额异常规则:订单金额 $999.99 是历史均值 $50 的 20 倍,返回 30 分
- 高频下单规则:1 小时内下单 6 次,返回 25 分
- 同地址大量下单规则:该地址 24 小时内只有 1 单,返回 0 分
- 高退款率买家规则:该买家历史退款率 10%,返回 0 分
风控结果:
- 总分:20 + 30 + 25 = 75 分
- 风险等级:高风险(> 70 分)
- 命中规则:地址有效性(20分)、金额异常(30分)、高频下单(25分)
- 处理方式:进入人工审核队列,运营专员审核后决定是否发货
面试时怎么讲:
我们设计了 5 类风控规则:地址有效性、金额异常、高频下单、同地址大量下单、高退款率买家。每个规则对应一个分值,总分 0-30 分为低风险,31-70 分为中风险,71 分以上为高风险。高风险订单会进入人工审核队列。
技术实现上,我们用策略模式设计风控规则引擎。定义一个 RiskRule 接口,每个规则实现这个接口,evaluate 方法返回风险分值。风控服务通过 Spring 自动注入所有规则,遍历执行,累加分值,最终得到总风险评分。
这样做的好处是:规则可插拔,新增规则只需实现接口,不需要修改风控服务;规则可配置,每个规则的分值可以动态调整;规则可测试,每个规则都是独立的类,可以单独编写单元测试;规则可追溯,每次风控检查都会记录命中的规则和分值。
订单超期预警系统
为什么需要超期预警?
订单在某个状态停留时间过长,说明可能出现了问题:
- 待处理超时:订单创建后长时间没有进入备货流程,可能是系统故障或库存不足
- 风控审核超时:订单在风控审核队列中长时间没有处理,可能是运营人员忘记审核
- 待发货超时:订单拣货完成后长时间没有发货,可能是仓库忘记扫码发货
- 运输中超时:订单发货后长时间没有签收,可能是物流异常或地址错误
如果不及时预警,会导致:
- 客户投诉,影响店铺评分
- 平台处罚,扣除保证金
- 订单取消,损失销售额
- 客户流失,影响复购率
所以我们需要及时预警,让运营人员及时处理异常订单。
预警规则设计:
1. 各状态的超期阈值
不同状态的超期阈值不同,根据业务经验设定:
| 订单状态 | 超期阈值 | 说明 |
|---|---|---|
| 待处理 | 2 小时 | 订单创建后应该快速进入备货流程 |
| 风控审核 | 24 小时 | 运营人员应该在 1 个工作日内完成审核 |
| 待备货 | 12 小时 | WMS 应该快速开始拣货 |
| 备货中 | 24 小时 | 拣货打包应该在 1 天内完成 |
| 待发货 | 24 小时 | 拣货完成后应该快速扫码发货 |
| 已发货 | 48 小时 | 物流商应该在 2 天内揽收 |
| 运输中 | 15 天(360 小时) | 跨境物流通常 7-15 天,超过 15 天可能异常 |
2. 三级预警机制
根据订单在当前状态的停留时长,分为 3 级预警:
| 预警级别 | 停留时长 | 通知方式 | 说明 |
|---|---|---|---|
| 预警 | 24-48 小时 | 邮件 | 订单开始超期,需要关注 |
| 紧急 | 48-72 小时 | 邮件 + 短信 | 订单严重超期,需要立即处理 |
| 严重 | 72 小时以上 | 邮件 + 短信 + 电话 | 订单极度超期,可能导致客户投诉 |
超期检查的完整流程:
步骤 1:定时任务触发
- 每小时执行一次超期检查(比如每天 0 点、1 点、2 点…)
- 使用 XXL-Job 或 Spring @Scheduled 实现
步骤 2:查询未完成订单
- 查询所有未完成的订单(待处理、风控审核、待备货、备货中、待发货、已发货、运输中)
- 排除已完成、已取消、售后中的订单
步骤 3:计算停留时长
- 获取订单的状态更新时间(status_update_time)
- 计算当前时间与状态更新时间的差值(小时数)
- 比如订单在”待发货”状态,状态更新时间是 2026-05-20 10:00,当前时间是 2026-05-22 14:00,停留时长 = 52 小时
步骤 4:判断是否超期
- 获取该状态的超期阈值(比如”待发货”的阈值是 24 小时)
- 如果停留时长 < 阈值,跳过该订单
- 如果停留时长 ≥ 阈值,进入步骤 5
步骤 5:判断预警级别
- 停留时长 24-48 小时:预警级别
- 停留时长 48-72 小时:紧急级别
- 停留时长 72 小时以上:严重级别
步骤 6:发送预警通知
- 预警级别:发送邮件给运营人员
- 紧急级别:发送邮件 + 短信给运营人员
- 严重级别:发送邮件 + 短信 + 电话给运营人员和主管
步骤 7:记录预警日志
- 保存到
order_timeout_log表 - 记录:订单号、当前状态、停留时长、预警级别、预警时间
- 方便后续分析哪些状态容易超期,优化流程
预警通知的内容:
订单超期预警【紧急】
订单号:ORD20260522143025123456
当前状态:待发货
停留时长:52 小时
买家邮箱:customer@example.com
平台:Amazon
创建时间:2026-05-20 10:00:00
请立即处理该订单,避免客户投诉和平台处罚。
技术实现的关键点:
1. 定时任务的选择
- Spring @Scheduled:适合单机部署,简单易用
- XXL-Job:适合分布式部署,支持任务调度、失败重试、执行日志
我们选择 XXL-Job,因为供应链系统是分布式部署的,需要保证定时任务只在一个节点执行。
2. 批量查询优化
如果订单量很大(比如 10 万个未完成订单),一次性查询会导致内存溢出。
优化方案:
- 分页查询:每次查询 1000 个订单,处理完再查询下一批
- 索引优化:在
status和status_update_time字段上创建联合索引 - 异步处理:把超期订单放到 MQ,异步发送通知
3. 避免重复预警
如果一个订单已经发送过预警,不应该每小时都发送一次。
解决方案:
- 在
order_timeout_log表中记录已发送的预警 - 查询时排除已发送过相同级别预警的订单
- 或者:只在预警级别升级时发送(比如从”预警”升级到”紧急”)
4. 预警通知的降级
如果短信服务故障,不应该影响整个预警系统。
解决方案:
- 邮件发送失败:记录日志,继续发送短信
- 短信发送失败:记录日志,继续发送电话
- 所有通知方式都失败:记录到数据库,人工处理
实际案例:
假设有一个订单在”待发货”状态超期了:
订单信息:
- 订单号:ORD20260522143025123456
- 当前状态:待发货
- 状态更新时间:2026-05-20 10:00:00
- 当前时间:2026-05-22 14:00:00
- 停留时长:52 小时
超期检查过程:
- 定时任务触发:2026-05-22 14:00:00,定时任务开始执行
- 查询未完成订单:查询到该订单,状态是”待发货”
- 计算停留时长:52 小时
- 判断是否超期:待发货的阈值是 24 小时,52 > 24,超期
- 判断预警级别:52 小时在 48-72 小时之间,紧急级别
- 发送预警通知:发送邮件 + 短信给运营人员
- 记录预警日志:保存到
order_timeout_log表
运营人员处理:
- 收到短信后,登录后台查看订单详情
- 发现仓库已经拣货完成,但忘记扫码发货
- 联系仓库,立即扫码发货
- 订单状态变为”已发货”,不再超期
面试时怎么讲:
我们设计了订单超期预警系统,定时检查订单在当前状态的停留时长。
每个状态都有超期阈值,比如待处理 2 小时、待发货 24 小时、运输中 15 天。超过阈值后,根据停留时长分为 3 级预警:24-48 小时是预警级别,48-72 小时是紧急级别,72 小时以上是严重级别。
预警级别越高,通知方式越强:预警级别发邮件,紧急级别发邮件加短信,严重级别发邮件加短信加电话。
技术实现上,我们用 XXL-Job 定时任务每小时执行一次检查,查询所有未完成订单,计算停留时长,判断是否超期。超期订单会记录到预警日志表,方便后续分析。为了避免重复预警,我们会查询预警日志,排除已发送过相同级别预警的订单。
这样做的好处是:及时发现异常订单,避免客户投诉;提高订单处理效率,减少人工巡检成本;可以分析哪些状态容易超期,优化流程。
订单拆单与合单规则
为什么需要拆单?
一个订单可能包含多个商品,这些商品可能面临以下情况:
- 分布在不同仓库:商品 A 在北京仓,商品 B 在上海仓,如果等两个仓库都备货完成再发货,会延长发货时间
- 物流限制:含电池商品不能空运,只能海运或陆运,需要和普通商品分开发货
- 超重或超体积:单个包裹超过 30kg 或体积超过 1 立方米,物流公司不接收,需要拆分成多个包裹
- 平台要求:某些平台要求不同类目的商品分开发货
如果不拆单,会导致:
- 发货时间延长,影响客户体验
- 物流成本增加,需要跨仓调拨
- 物流公司拒收,无法发货
- 违反平台规则,被处罚
拆单的 4 种触发规则:
| 规则类型 | 触发条件 | 举例 |
|---|---|---|
| 跨仓库 | 订单中的商品分布在不同仓库 | 商品 A 在北京仓,商品 B 在上海仓 |
| 物流限制 | 某些商品有特殊物流要求 | 含电池商品不能空运,需要单独发货 |
| 超重/超体积 | 单个包裹超过物流公司限制 | 总重量超过 30kg,需要拆分成多个包裹 |
| 平台要求 | 平台规定不同类目分开发货 | Amazon 要求服装和电子产品分开发货 |
拆单的完整流程:
步骤 1:订单创建后,判断是否需要拆单
订单通过风控检查后,进入备货流程前,系统会自动判断是否需要拆单。
步骤 2:按仓库分组
首先,根据每个商品的库存情况,选择最优仓库:
- 优先选择距离收货地址最近的仓库(降低物流成本)
- 如果最近的仓库库存不足,选择次近的仓库
- 如果多个仓库都有库存,选择库存最多的仓库(避免缺货)
然后,把订单中的商品按仓库分组:
- 北京仓:商品 A(2 件)、商品 C(1 件)
- 上海仓:商品 B(3 件)
步骤 3:检查每组是否需要进一步拆分
对于每个仓库分组,检查是否需要进一步拆分:
检查 1:物流限制
- 查询每个商品是否含电池(
contains_battery字段) - 如果有含电池商品,单独分为一组
- 如果有普通商品,单独分为一组
举例:
- 北京仓原本有商品 A(含电池)和商品 C(不含电池)
- 拆分后:
- 组 1:商品 A(含电池)
- 组 2:商品 C(不含电池)
检查 2:超重或超体积
- 计算每组的总重量和总体积
- 如果总重量 > 30kg 或总体积 > 1 立方米,需要拆分
拆分算法(贪心算法):
- 按商品重量从大到小排序
- 依次把商品放入当前包裹
- 如果放入后超过限制,创建新包裹
- 重复步骤 2-3,直到所有商品都分配完
举例:
- 商品 A:10kg × 2 件 = 20kg
- 商品 B:8kg × 3 件 = 24kg
- 商品 C:5kg × 1 件 = 5kg
- 总重量:49kg > 30kg,需要拆分
拆分结果:
- 包裹 1:商品 A(20kg)+ 商品 C(5kg)= 25kg
- 包裹 2:商品 B(24kg)
步骤 4:创建子订单
对于每个分组,创建一个子订单:
- 子订单号:父订单号 + 序号(比如
ORD20260522143025123456-1) - 子订单关联父订单:
parent_order_id字段 - 子订单包含该组的所有商品
- 子订单金额:该组商品的总金额
- 子订单仓库:该组对应的仓库
步骤 5:更新父订单状态
- 标记父订单已拆单:
is_split = 1 - 记录子订单数量:
sub_order_count = 3 - 父订单状态:根据所有子订单的状态计算(取最落后的状态)
父订单状态的计算规则:
父订单的状态 = 所有子订单中最落后的状态
举例:
- 子订单 1:已发货
- 子订单 2:已发货
- 子订单 3:待发货
- 父订单状态:待发货(最落后)
这样设计的原因:
- 客户看到父订单状态是”待发货”,知道还有商品没发货
- 如果父订单状态是”已发货”,客户会以为所有商品都发货了,实际上还有商品在备货
拆单的实际案例:
订单信息:
- 订单号:ORD20260522143025123456
- 商品列表:
- 商品 A(蓝牙耳机,含电池):2 件,10kg/件,北京仓有库存
- 商品 B(手机壳,不含电池):3 件,8kg/件,上海仓有库存
- 商品 C(数据线,不含电池):1 件,5kg/件,北京仓有库存
拆单过程:
步骤 1:按仓库分组
- 北京仓:商品 A(2 件)、商品 C(1 件)
- 上海仓:商品 B(3 件)
步骤 2:检查北京仓分组
- 商品 A 含电池,商品 C 不含电池
- 需要拆分:
- 组 1:商品 A(含电池)
- 组 2:商品 C(不含电池)
步骤 3:检查上海仓分组
- 商品 B 总重量:8kg × 3 = 24kg < 30kg
- 不需要拆分:
- 组 3:商品 B
步骤 4:创建子订单
- 子订单 1:
ORD20260522143025123456-1,北京仓,商品 A(含电池) - 子订单 2:
ORD20260522143025123456-2,北京仓,商品 C(不含电池) - 子订单 3:
ORD20260522143025123456-3,上海仓,商品 B
步骤 5:并行发货
- 北京仓同时处理子订单 1 和子订单 2
- 上海仓处理子订单 3
- 三个包裹独立发货,互不影响
为什么需要合单?
合单是拆单的反向操作,适用于以下场景:
- 同一买家多次下单:买家在短时间内下了多个订单,商品都在同一个仓库,可以合并发货,降低物流成本
- 平台促销活动:买家参加”满减”活动,分多次下单凑单,可以合并发货
- 降低物流成本:多个小订单合并成一个大订单,物流费用更低
合单的触发条件:
- 同一买家
- 同一收货地址
- 同一仓库
- 订单状态都是”待备货”(还没开始拣货)
- 订单创建时间间隔 < 2 小时
合单的流程:
- 定时任务每 10 分钟扫描一次”待备货”订单
- 按买家 + 收货地址 + 仓库分组
- 如果同一组有多个订单,且创建时间间隔 < 2 小时,触发合单
- 创建一个新的父订单,包含所有商品
- 原订单标记为”已合单”,关联到新的父订单
- 新的父订单进入备货流程
拆单与合单的技术实现关键点:
1. 拆单时机
- 在订单创建后、备货前进行拆单
- 如果已经开始备货,不能拆单(会导致库存混乱)
2. 子订单的独立性
- 每个子订单有独立的订单号、独立的发货流程
- 子订单可以独立取消、独立退款
- 子订单的状态变更不影响其他子订单
3. 父订单的状态同步
- 子订单状态变更时,自动更新父订单状态
- 父订单状态 = 所有子订单中最落后的状态
- 用数据库触发器或 MQ 消息实现自动同步
4. 拆单算法的优化
- 贪心算法:优先选择距离最近的仓库,降低物流成本
- 动态规划:如果有多个仓库都有库存,选择最优组合,使总物流成本最低
- 缓存优化:仓库距离、商品重量等信息缓存到 Redis,避免重复查询
面试时怎么讲:
订单拆单有 4 种触发规则:跨仓库、物流限制、超重超体积、平台要求。
拆单流程是:先按仓库分组,然后检查每组是否需要进一步拆分。比如含电池商品不能和普通商品一起空运,需要单独发货;单个包裹超过 30kg,需要拆分成多个包裹。
拆单后,系统会创建多个子订单,每个子订单有独立的订单号、独立的发货流程。子订单通过 parent_order_id 关联父订单。父订单状态等于所有子订单中最落后的状态,比如有 3 个子订单,2 个已发货,1 个待发货,父订单状态就是待发货。
合单是拆单的反向操作,适用于同一买家短时间内下了多个订单的场景。定时任务每 10 分钟扫描一次待备货订单,按买家加收货地址加仓库分组,如果同一组有多个订单且创建时间间隔小于 2 小时,就触发合单。
这样做的好处是:提高发货效率,不同仓库可以并行发货;降低物流成本,避免跨仓调拨;满足物流限制,保证合规发货。
模块间调用技术
为什么需要模块间调用?
OMS(订单管理系统)不是孤立的,它需要和其他系统协作完成业务:
-
和 WMS(仓储管理)交互:
- 订单创建时,需要冻结库存(防止超卖)
- 订单发货时,需要扣减库存
- 订单取消时,需要释放库存
-
和 TMS(物流管理)交互:
- 订单发货时,需要创建运单
- 物流状态更新时,需要同步订单状态
-
和 FMS(财务管理)交互:
- 订单完成时,需要触发财务结算
- 订单退款时,需要创建退款单
-
和 PIM(商品管理)交互:
- 订单创建时,需要查询商品信息(价格、库存、重量)
- 订单创建时,需要验证商品是否上架
模块间调用的 3 种技术方案:
| 技术方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Feign 同步调用 | 需要立即返回结果的场景 | 实时性强,调用简单 | 性能较低,调用方需要等待 |
| MQ 异步消息 | 不需要立即返回结果的场景 | 性能高,解耦,削峰填谷 | 实时性差,需要处理消息丢失 |
| Seata 分布式事务 | 需要保证多个操作原子性的场景 | 保证数据一致性 | 性能开销大,复杂度高 |
具体场景分析:
场景 1:订单创建时冻结库存(Feign + Seata)
业务需求:
- 订单创建成功,库存必须冻结成功
- 如果库存不足,订单创建失败
- 两个操作必须同时成功或同时失败(原子性)
技术选型:Feign + Seata AT 模式
为什么用 Feign?
- 订单创建需要立即知道库存是否充足
- 如果库存不足,要立即返回错误给用户
- 不能用 MQ,因为 MQ 是异步的,无法立即返回结果
为什么用 Seata?
- 订单创建和库存冻结是两个独立的数据库操作
- 如果订单创建成功,但库存冻结失败(比如网络超时),会导致数据不一致
- Seata 保证两个操作要么都成功,要么都失败
完整流程:
flowchart TD
A[用户下单] --> B[OMS 开启全局事务<br/>Seata]
B --> C[OMS 创建订单<br/>本地事务]
C --> D[OMS 通过 Feign<br/>调用 WMS 冻结库存]
D --> E[WMS 检查库存]
E --> F{库存是否充足?}
F -->|充足| G[WMS 冻结库存<br/>返回成功]
F -->|不足| H[WMS 返回失败]
G --> I[OMS 提交全局事务]
H --> J[OMS 回滚订单]
I --> K[返回下单成功]
J --> L[返回库存不足]
Seata AT 模式的原理:
-
阶段一(Try):执行业务操作
- OMS 创建订单,Seata 自动记录 undo_log(回滚日志)
- WMS 冻结库存,Seata 自动记录 undo_log
- 如果任何一步失败,Seata 自动回滚
-
阶段二(Confirm/Cancel):提交或回滚
- 如果所有操作都成功,Seata 提交全局事务,删除 undo_log
- 如果任何操作失败,Seata 根据 undo_log 自动回滚所有操作
代码示例(关键部分):
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private WarehouseFeignClient warehouseFeignClient; // Feign 客户端
/**
* 创建订单(分布式事务)
*/
@Override
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public OrderMain createOrder(CreateOrderRequest request) {
// 1. 创建订单(本地事务)
OrderMain order = new OrderMain();
order.setOrderNo(generateOrderNo());
order.setStatus(OrderStatus.PENDING);
orderMapper.insert(order);
// 2. 调用 WMS 冻结库存(远程调用 + 分布式事务)
FreezeStockRequest freezeRequest = new FreezeStockRequest();
freezeRequest.setSkuId(request.getSkuId());
freezeRequest.setQuantity(request.getQuantity());
freezeRequest.setOrderNo(order.getOrderNo());
Result<Void> result = warehouseFeignClient.freezeStock(freezeRequest);
// 3. 如果库存冻结失败,抛出异常,Seata 自动回滚
if (!result.isSuccess()) {
throw new BusinessException("库存不足");
}
// 4. 返回订单
return order;
}
}
Feign 客户端定义:
@FeignClient(name = "supplychain-warehouse", path = "/warehouse/stock")
public interface WarehouseFeignClient {
/**
* 冻结库存
*/
@PostMapping("/freeze")
Result<Void> freezeStock(@RequestBody FreezeStockRequest request);
}
场景 2:订单发货后扣减库存(MQ 异步消息)
业务需求:
- 订单发货后,需要扣减库存
- 扣减库存不需要立即完成,可以异步处理
- 如果扣减失败,需要重试
技术选型:RocketMQ
为什么用 MQ?
- 订单发货后,不需要立即扣减库存,可以异步处理
- MQ 可以削峰填谷,避免高并发时 WMS 压力过大
- MQ 支持消息重试,保证最终一致性
为什么不用 Feign?
- Feign 是同步调用,订单发货需要等待库存扣减完成才能返回
- 如果 WMS 响应慢,会导致订单发货接口超时
- 用 MQ 可以立即返回,提高用户体验
完整流程:
flowchart TD
A[仓库扫码发货] --> B[OMS 更新订单状态为已发货]
B --> C[OMS 发送 MQ 消息<br/>OrderShippedEvent]
C --> D[立即返回发货成功]
C -.异步.-> E[WMS 消费消息]
E --> F[WMS 扣减库存]
F --> G{扣减是否成功?}
G -->|成功| H[完成]
G -->|失败| I[重试,最多 3 次]
I --> J{是否重试失败?}
J -->|是| K[记录到死信队列]
K --> L[人工处理]
J -->|否| F
代码示例(关键部分):
OMS 发送消息:
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 订单发货
*/
@Override
@Transactional
public void shipOrder(String orderNo) {
// 1. 更新订单状态
OrderMain order = orderMapper.selectByOrderNo(orderNo);
order.setStatus(OrderStatus.SHIPPED);
order.setShipTime(LocalDateTime.now());
orderMapper.updateById(order);
// 2. 发送 MQ 消息(异步扣减库存)
OrderShippedEvent event = new OrderShippedEvent();
event.setOrderNo(orderNo);
event.setSkuId(order.getSkuId());
event.setQuantity(order.getQuantity());
rocketMQTemplate.syncSend("order-shipped-topic", event);
// 3. 立即返回(不等待库存扣减完成)
}
}
WMS 消费消息:
@Service
@RocketMQMessageListener(
topic = "order-shipped-topic",
consumerGroup = "warehouse-consumer-group"
)
public class OrderShippedListener implements RocketMQListener<OrderShippedEvent> {
@Autowired
private StockService stockService;
@Override
public void onMessage(OrderShippedEvent event) {
try {
// 扣减库存
stockService.deductStock(event.getSkuId(), event.getQuantity());
} catch (Exception e) {
// 扣减失败,抛出异常,MQ 会自动重试
throw new RuntimeException("库存扣减失败", e);
}
}
}
MQ 的重试机制:
- 消费失败后,RocketMQ 会自动重试
- 重试间隔:10s、30s、1min、2min、3min、4min、5min、6min、7min、8min、9min、10min、20min、30min、1h、2h
- 最多重试 16 次
- 如果 16 次都失败,消息进入死信队列,需要人工处理
场景 3:订单完成后触发财务结算(MQ 异步消息)
业务需求:
- 订单完成后,需要触发财务结算
- 财务结算不需要立即完成,可以异步处理
- 财务结算失败不影响订单完成
技术选型:RocketMQ
为什么用 MQ?
- 订单完成和财务结算是两个独立的业务
- 财务结算失败不应该影响订单完成
- MQ 可以解耦两个系统,降低耦合度
完整流程:
flowchart TD
A[订单签收 15 天后] --> B[OMS 定时任务扫描]
B --> C[OMS 更新订单状态为已完成]
C --> D[OMS 发送 MQ 消息<br/>OrderCompletedEvent]
D -.异步.-> E[FMS 消费消息]
E --> F[FMS 创建应收账款]
F --> G[FMS 计算利润]
G --> H[FMS 生成财务报表]
代码示例(关键部分):
OMS 发送消息:
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 订单完成
*/
@Override
@Transactional
public void completeOrder(String orderNo) {
// 1. 更新订单状态
OrderMain order = orderMapper.selectByOrderNo(orderNo);
order.setStatus(OrderStatus.COMPLETED);
order.setCompleteTime(LocalDateTime.now());
orderMapper.updateById(order);
// 2. 发送 MQ 消息(异步触发财务结算)
OrderCompletedEvent event = new OrderCompletedEvent();
event.setOrderNo(orderNo);
event.setPaymentAmount(order.getPaymentAmount());
event.setCompleteTime(order.getCompleteTime());
rocketMQTemplate.syncSend("order-completed-topic", event);
}
}
FMS 消费消息:
@Service
@RocketMQMessageListener(
topic = "order-completed-topic",
consumerGroup = "finance-consumer-group"
)
public class OrderCompletedListener implements RocketMQListener<OrderCompletedEvent> {
@Autowired
private FinanceService financeService;
@Override
public void onMessage(OrderCompletedEvent event) {
// 创建应收账款
financeService.createReceivable(event);
// 计算利润
financeService.calculateProfit(event);
// 生成财务报表
financeService.generateReport(event);
}
}
技术选型总结:
| 场景 | 技术方案 | 原因 |
|---|---|---|
| 订单创建 → 冻结库存 | Feign + Seata | 需要立即返回结果,保证原子性 |
| 订单发货 → 扣减库存 | MQ 异步消息 | 不需要立即完成,削峰填谷 |
| 订单取消 → 释放库存 | MQ 异步消息 | 不需要立即完成,解耦系统 |
| 订单完成 → 财务结算 | MQ 异步消息 | 不需要立即完成,解耦系统 |
| 订单创建 → 查询商品信息 | Feign 同步调用 | 需要立即返回结果,不涉及事务 |
Feign vs MQ 的选择标准:
用 Feign 的场景:
- 需要立即返回结果(比如查询商品价格)
- 调用方需要根据返回结果做判断(比如库存不足时拒绝下单)
- 调用频率不高,不会造成性能瓶颈
用 MQ 的场景:
- 不需要立即返回结果(比如发送通知)
- 调用方不关心返回结果(比如记录日志)
- 调用频率很高,需要削峰填谷(比如秒杀场景)
- 需要解耦系统,降低耦合度
Seata 的使用场景:
需要用 Seata 的场景:
- 多个操作必须同时成功或同时失败(原子性)
- 操作涉及多个数据库或多个服务
- 数据一致性要求很高(比如金融系统)
不需要用 Seata 的场景:
- 操作可以异步完成,允许短暂的数据不一致(最终一致性)
- 操作只涉及一个数据库(用本地事务就够了)
- 性能要求很高,不能接受 Seata 的性能开销
Seata AT 模式 vs TCC 模式:
| 对比项 | AT 模式 | TCC 模式 |
|---|---|---|
| 侵入性 | 低,自动记录 undo_log | 高,需要实现 Try/Confirm/Cancel |
| 性能 | 较低,需要记录日志 | 较高,无需记录日志 |
| 适用场景 | 大部分业务场景 | 对性能要求极高的场景 |
| 开发成本 | 低,只需加注解 | 高,需要实现 3 个方法 |
我们选择 AT 模式,因为:
- 开发成本低,只需要在方法上加
@GlobalTransactional注解 - 性能满足业务需求(订单创建不是高频操作)
- 维护成本低,不需要手动实现回滚逻辑
面试时怎么讲:
OMS 需要和其他系统协作,我们用了 3 种技术方案:Feign 同步调用、MQ 异步消息、Seata 分布式事务。
订单创建时冻结库存,用 Feign 加 Seata。因为需要立即知道库存是否充足,如果库存不足要立即返回错误。同时订单创建和库存冻结必须同时成功或同时失败,所以用 Seata AT 模式保证原子性。Seata 会自动记录 undo_log,如果任何操作失败,自动回滚所有操作。
订单发货后扣减库存,用 MQ 异步消息。因为不需要立即扣减库存,可以异步处理。MQ 可以削峰填谷,避免高并发时 WMS 压力过大。如果扣减失败,RocketMQ 会自动重试,最多重试 16 次,保证最终一致性。
订单完成后触发财务结算,也用 MQ 异步消息。因为财务结算失败不应该影响订单完成,MQ 可以解耦两个系统,降低耦合度。
选择 Feign 还是 MQ 的标准是:如果需要立即返回结果,用 Feign;如果不需要立即返回结果,用 MQ。选择是否用 Seata 的标准是:如果多个操作必须同时成功或同时失败,用 Seata;如果允许短暂的数据不一致,用 MQ 保证最终一致性。
简历怎么写
项目描述
项目名称:跨境电商 SaaS 供应链管理系统 - 商品与订单模块
项目背景:
为跨境电商卖家提供统一的商品管理和多平台订单管理能力,支持 Amazon、Shopify、
eBay 等主流平台,实现商品信息集中维护、多语言内容管理、订单自动化处理、
库存实时同步等核心功能。
技术架构:
- 后端:Spring Boot 3.2 + MyBatis-Plus + MySQL 8.0 + Redis 7.0 + RocketMQ 5.1
- 状态机:Spring State Machine 3.2
- AI 翻译:OpenAI API
- 平台对接:Amazon SP-API、Shopify API、eBay API
我的职责:
1. 负责 PIM 商品管理模块和 OMS 订单管理模块的核心功能开发
2. 设计并实现 SPU/SKU 两级商品体系,支持规格自动组合生成
3. 实现多平台订单接入适配层,对接 Amazon、Shopify、eBay 等平台 API
4. 设计订单状态机,实现订单全生命周期管理
5. 优化多语言内容管理,集成 AI 翻译功能,提升内容生产效率 80%
核心亮点
亮点1:SPU/SKU 两级商品体系 + 笛卡尔积自动生成
问题:
跨境电商商品规格复杂,一个商品可能有多种颜色、尺码组合。比如一款蓝牙耳机有黑色、白色、蓝色 3 种颜色,如果每个颜色都创建独立商品,会导致品牌、材质、HS 编码等信息重复录入 3 次,维护成本很高。而且当需要修改品牌信息时,要修改 3 个商品,容易遗漏。
方案:
设计 SPU/SKU 两级商品体系:
- SPU(标准产品单元):存储共性属性,比如品牌、材质、HS 编码
- SKU(最小库存单元):存储规格属性,比如颜色、尺码、价格、库存
通过笛卡尔积算法自动生成 SKU 组合:
- 运营配置规格项:颜色(黑色、白色、蓝色)、尺码(S、M)
- 系统自动计算笛卡尔积:3×2=6 个 SKU 组合
- 每个 SKU 的规格值用 JSON 存储:
{"颜色":"黑色","尺码":"M"}
具体实现:
- SPU 表设计:存储 SPU ID、品牌、材质、HS 编码等共性属性
- SKU 表设计:存储 SKU ID、SPU ID、规格值(JSON)、价格、库存等
- 笛卡尔积算法:递归遍历所有规格项,生成所有可能的组合
- 批量插入:一次性插入所有 SKU,提高效率
效果:
- 商品信息冗余度降低 70%,维护成本大幅下降
- SKU 生成效率提升 10 倍,原本需要手动创建 6 个 SKU,现在自动生成
- 支持最多 5 个规格项,理论上可生成 3^5 = 243 个 SKU 组合
- 修改 SPU 信息时,所有 SKU 自动继承,无需逐个修改
技术细节:
笛卡尔积算法的核心逻辑:
// 输入:{"颜色": ["黑色", "白色"], "尺码": ["S", "M"]}
// 输出:[{"颜色":"黑色","尺码":"S"}, {"颜色":"黑色","尺码":"M"},
// {"颜色":"白色","尺码":"S"}, {"颜色":"白色","尺码":"M"}]
private List<Map<String, String>> cartesianProduct(Map<String, List<String>> specOptions) {
List<Map<String, String>> result = new ArrayList<>();
result.add(new HashMap<>()); // 初始化一个空组合
for (Map.Entry<String, List<String>> entry : specOptions.entrySet()) {
String specName = entry.getKey(); // 规格名:颜色
List<String> specValues = entry.getValue(); // 规格值:[黑色, 白色]
List<Map<String, String>> temp = new ArrayList<>();
for (Map<String, String> existing : result) { // 遍历已有组合
for (String value : specValues) { // 遍历当前规格的所有值
Map<String, String> newCombination = new HashMap<>(existing);
newCombination.put(specName, value); // 添加当前规格
temp.add(newCombination);
}
}
result = temp; // 更新结果
}
return result;
}
亮点2:多平台订单接入 + Webhook 签名验证
问题:
需要对接 Amazon、Shopify、eBay、TikTok Shop 等多个平台,每个平台的订单格式、字段定义、接入方式都不同:
- Amazon 用 SP-API,需要主动拉取订单
- Shopify 用 Webhook,订单创建时主动推送
- eBay 用 Trading API,需要定时轮询
同时 Webhook 接口是公开的,存在被恶意请求伪造订单的风险。如果不验证签名,黑客可以伪造订单,导致库存被恶意占用、发货地址被篡改。
方案:
-
设计统一的订单接入适配层:
- 定义统一的内部订单模型
- 每个平台实现一个适配器,负责格式转换
- 支持主动拉取(定时任务)和被动接收(Webhook)两种方式
-
实现 Webhook 签名验证机制:
- 使用 HMAC-SHA256 算法验证签名
- 每个平台有独立的密钥(Secret)
- 签名验证失败,直接拒绝请求
具体实现:
- 适配器模式:每个平台实现
OrderAdapter接口,包含parse()和verify()方法 - 签名验证流程:
- 从请求头获取平台签名(比如
X-Shopify-Hmac-SHA256) - 用平台密钥对请求体进行 HMAC-SHA256 加密
- 对比计算出的签名和平台签名,一致则通过
- 从请求头获取平台签名(比如
- 幂等性保证:用”平台 + 平台订单号”作为唯一标识,数据库唯一索引防止重复插入
- 异步处理:Webhook 接口立即返回 200,订单处理放到 MQ 异步执行
效果:
- 成功对接 5 个主流电商平台,日均处理订单 5 万+
- Webhook 签名验证拦截恶意请求 100%,零误伤
- 异步处理机制避免超时重发,重复订单率从 3% 降至 0.1%
- 平台适配器可插拔,新增平台只需实现接口,无需修改核心代码
技术细节:
Shopify Webhook 签名验证的核心代码:
public boolean verifySignature(String requestBody, String signature, String secret) {
try {
// 1. 用平台密钥对请求体进行 HMAC-SHA256 加密
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
mac.init(secretKey);
byte[] hash = mac.doFinal(requestBody.getBytes());
// 2. 转换为 Base64 编码
String calculatedSignature = Base64.getEncoder().encodeToString(hash);
// 3. 对比计算出的签名和平台签名
return calculatedSignature.equals(signature);
} catch (Exception e) {
return false;
}
}
亮点3:订单状态机 + 风控规则引擎
问题:
订单从创建到完成要经历 10 个状态(待处理、风控审核、待备货、备货中、待发货、已发货、运输中、已签收、已完成、已取消),状态流转逻辑复杂,容易出现非法状态流转。比如订单从”待处理”直接跳到”已发货”,跳过了备货环节,导致库存没有扣减。
同时跨境电商存在大量欺诈订单(盗刷信用卡、虚假地址、恶意下单),如果不进行风控检查,会导致货物发出后无法签收,造成损失。
方案:
-
使用 Spring State Machine 实现订单状态机:
- 定义 10 个状态和 12 个事件
- 配置状态流转规则(哪些状态可以流转到哪些状态)
- 每个流转规则包含前置条件(Guard)和后置动作(Action)
- 状态机自动拦截非法流转
-
设计风控规则引擎:
- 支持 5 类风控规则:地址有效性、金额异常、高频下单、黑名单、设备指纹
- 采用策略模式实现规则可插拔
- 每个规则返回风险分值,累加后判断是否需要人工审核
具体实现:
-
状态机配置:
- 定义状态枚举:
PENDING、RISK_REVIEW、WAIT_PREPARE等 - 定义事件枚举:
PASS_RISK_CHECK、START_PREPARE、SHIP等 - 配置流转规则:
PENDING→RISK_REVIEW(事件:TRIGGER_RISK_REVIEW) - 配置 Guard:判断订单金额是否超过 1000 元
- 配置 Action:发货时自动调用 WMS 扣减库存、调用 TMS 创建运单
- 定义状态枚举:
-
风控规则引擎:
- 定义规则接口
RiskRule,包含evaluate()方法 - 每个规则实现接口,返回风险分值(0-100)
- 规则管理器遍历所有规则,累加分值
- 分值 > 70 触发人工审核,分值 > 90 直接拒绝
- 定义规则接口
效果:
- 非法状态流转拦截率 100%,避免数据异常
- 风控规则命中率 15%,拦截欺诈订单 3000+ 单/月,避免损失 50 万+
- 规则可插拔,新增规则只需实现接口,无需修改核心代码
- 状态流转自动化,发货时自动扣减库存、创建运单,减少人工操作
技术细节:
风控规则接口定义:
public interface RiskRule {
/**
* 评估订单风险
* @param order 订单
* @return 风险分值(0-100,0 表示无风险)
*/
int evaluate(OrderMain order);
}
// 地址有效性规则
@Component
public class AddressValidityRule implements RiskRule {
@Override
public int evaluate(OrderMain order) {
OrderAddress address = order.getAddress();
// 邮政编码不存在,返回 20 分
if (!isValidPostalCode(address.getPostalCode())) {
return 20;
}
// 地址在黑名单,返回 20 分
if (isBlacklistAddress(address)) {
return 20;
}
return 0; // 地址有效,无风险
}
}
亮点4:订单拆单算法 + 分布式事务
问题:
一个订单可能包含多个商品,这些商品可能分布在不同仓库。比如商品 A 在北京仓,商品 B 在上海仓,如果等两个仓库都备货完成再发货,会延长发货时间,影响客户体验。
同时订单拆单涉及多个操作:创建子订单、更新父订单状态、冻结库存。这些操作必须同时成功或同时失败,否则会导致数据不一致。
方案:
-
设计订单拆单算法:
- 按仓库分组:先按仓库分组,同一仓库的商品放在一起
- 检查物流限制:含电池商品不能和普通商品一起空运,需要单独发货
- 检查重量限制:单个包裹超过 30kg,需要拆分成多个包裹
- 创建子订单:每个分组创建一个子订单,子订单通过
parent_order_id关联父订单
-
使用 Seata AT 模式保证分布式事务:
- OMS 创建子订单(本地事务)
- WMS 冻结库存(远程调用)
- 如果任何操作失败,Seata 自动回滚所有操作
具体实现:
-
拆单流程:
- 步骤 1:按仓库分组
- 步骤 2:检查每组是否需要进一步拆分(物流限制、重量限制)
- 步骤 3:为每个分组创建子订单
- 步骤 4:调用 WMS 冻结库存(Feign + Seata)
- 步骤 5:更新父订单状态
-
分布式事务:
- 在拆单方法上加
@GlobalTransactional注解 - Seata 自动记录 undo_log(回滚日志)
- 如果任何操作失败,Seata 根据 undo_log 自动回滚
- 在拆单方法上加
效果:
- 拆单后不同仓库可以并行发货,发货效率提升 40%
- 避免跨仓调拨,物流成本降低 20%
- 分布式事务保证数据一致性,订单和库存 100% 匹配
- 支持 4 种拆单规则:跨仓库、物流限制、超重超体积、平台要求
技术细节:
拆单方法的分布式事务:
@Service
public class OrderSplitService {
@Autowired
private WarehouseFeignClient warehouseFeignClient;
/**
* 订单拆单(分布式事务)
*/
@GlobalTransactional(name = "split-order", rollbackFor = Exception.class)
public void splitOrder(String orderNo) {
// 1. 查询父订单
OrderMain parentOrder = orderMapper.selectByOrderNo(orderNo);
// 2. 按仓库分组
Map<String, List<OrderItem>> warehouseGroups = groupByWarehouse(parentOrder);
// 3. 为每个分组创建子订单
for (Map.Entry<String, List<OrderItem>> entry : warehouseGroups.entrySet()) {
String warehouseCode = entry.getKey();
List<OrderItem> items = entry.getValue();
// 创建子订单
OrderMain subOrder = createSubOrder(parentOrder, items, warehouseCode);
orderMapper.insert(subOrder);
// 调用 WMS 冻结库存(远程调用 + 分布式事务)
for (OrderItem item : items) {
Result<Void> result = warehouseFeignClient.freezeStock(
item.getSkuId(), item.getQuantity(), subOrder.getOrderNo()
);
// 如果库存冻结失败,抛出异常,Seata 自动回滚
if (!result.isSuccess()) {
throw new BusinessException("库存不足");
}
}
}
// 4. 更新父订单状态
parentOrder.setStatus(OrderStatus.SPLIT);
orderMapper.updateById(parentOrder);
}
}
亮点5:AI 辅助多语言内容生成
问题:
跨境电商需要支持多语言(英文、日文、韩文、西班牙文等),人工翻译成本高、效率低。一个商品的多语言内容翻译(标题、描述、卖点)需要 2-3 小时,而且翻译质量参差不齐,有些翻译不符合电商文案风格。
方案:
集成 OpenAI API,实现 AI 一键翻译功能:
- 设计专业的翻译 Prompt,要求 AI 按照电商文案风格翻译
- 支持批量翻译:一次性翻译标题、描述、卖点
- 翻译结果自动保存到
product_content表 - 支持人工修改:AI 翻译后,运营可以手动调整
具体实现:
-
翻译 Prompt 设计:
你是一个专业的跨境电商文案翻译专家。请将以下商品信息翻译成{目标语言}, 要求: 1. 保持电商文案风格,突出卖点 2. 标题控制在 200 字符以内 3. 描述要详细,包含使用场景 4. 卖点要简洁有力,每条不超过 50 字符 商品信息: 标题:{原标题} 描述:{原描述} 卖点:{原卖点} -
调用 OpenAI API:
- 使用
gpt-4模型(翻译质量更高) - 设置
temperature=0.3(降低随机性,保证翻译稳定) - 设置
max_tokens=2000(限制返回长度)
- 使用
-
异步翻译:
- 翻译任务放到 MQ,异步执行
- 翻译完成后,发送通知给运营
- 避免翻译时间过长,影响用户体验
效果:
- 多语言内容生产效率提升 80%,从 2-3 小时降至 5 分钟
- 翻译成本降低 90%,从人工翻译 ¥200/商品 降至 API 调用 ¥2/商品
- 支持 8 种语言(英文、日文、韩文、西班牙文、法文、德文、意大利文、葡萄牙文)
- 翻译准确率 85%+,运营只需微调,无需重新翻译
技术细节:
调用 OpenAI API 的核心代码:
public String translate(String content, String targetLanguage) {
// 1. 构建 Prompt
String prompt = String.format(
"你是一个专业的跨境电商文案翻译专家。请将以下商品信息翻译成%s," +
"要求:1. 保持电商文案风格,突出卖点;2. 标题控制在 200 字符以内。\n\n" +
"商品信息:\n%s",
targetLanguage, content
);
// 2. 调用 OpenAI API
ChatCompletionRequest request = ChatCompletionRequest.builder()
.model("gpt-4")
.messages(List.of(new ChatMessage("user", prompt)))
.temperature(0.3)
.maxTokens(2000)
.build();
ChatCompletionResult result = openAiService.createChatCompletion(request);
// 3. 返回翻译结果
return result.getChoices().get(0).getMessage().getContent();
}
面试高频问题
Q1: SPU 和 SKU 有什么区别?
标准答案:
SPU(Standard Product Unit)是标准产品单元,描述一类商品的共性属性,比如品牌、材质、HS 编码。
SKU(Stock Keeping Unit)是最小库存单元,描述一个具体规格的商品,比如颜色、尺码。
举例说明:
蓝牙耳机 Pro 是一个 SPU,它有黑色、白色、蓝色三种颜色,每种颜色是一个 SKU。
SPU 存储共性属性(品牌、材质、连接方式),SKU 存储规格属性(颜色、重量、价格),这样可以避免重复录入,提高维护效率。
追问:为什么不直接用 SKU,还要设计 SPU?
答:如果只有 SKU,那么每个 SKU 都要单独维护品牌、材质、HS 编码等信息,会造成大量数据冗余。而且当需要修改品牌信息时,要修改所有 SKU,维护成本很高。有了 SPU,共性信息只需维护一次,所有 SKU 共享。
Q2: 如何实现 SKU 自动生成?
标准答案:
SKU 自动生成用的是笛卡尔积算法。
运营配置规格项,比如颜色有黑色、白色、蓝色,尺码有 S、M。系统会计算笛卡尔积,生成 3×2=6 个 SKU 组合。
每个 SKU 的 spec_values 字段用 JSON 存储规格值,比如 {"颜色":"黑色","尺码":"M"}。
算法原理:
笛卡尔积就是把所有规格项的值进行排列组合。比如颜色有 3 个值,尺码有 2 个值,那么就有 3×2=6 种组合。
算法实现是递归遍历所有规格项,每次取一个规格项的所有值,和已有的组合进行拼接,生成新的组合。
追问:如果某个组合不销售怎么办?
答:有些商品的 SKU 不适合笛卡尔积生成,因为有的规格组合是不存在的。比如服装,可能只有”黑色-L”和”白色-M”,没有”黑色-M”和”白色-L”。
我们的做法是:先用笛卡尔积生成所有可能的组合,然后运营手动删除不存在的 SKU。或者设置 SKU 状态为”已停售”,这样不会影响已有订单,但新订单不能选择这个 SKU。
另外,我们支持批量操作,可以一次性删除多个 SKU,提高效率。
Q3: Webhook 为什么要做签名验证?
标准答案:
Webhook 接口是公开的,任何人都可以访问。如果不做签名验证,恶意用户可以伪造请求,创建大量假订单,导致:
- 库存被恶意占用,真实订单无法下单
- 发货地址被篡改,货物发到错误地址
- 系统资源被消耗,影响正常业务
- 财务数据混乱,对账困难
签名验证原理:
平台(比如 Shopify)用 App Secret 对请求体进行 HMAC-SHA256 加密,生成签名放到请求头(比如 X-Shopify-Hmac-SHA256)。
我们收到请求后,用相同的 App Secret 和算法计算期望签名,对比收到的签名和期望签名是否一致。
如果一致,说明:
- 请求确实来自平台(因为只有平台和我们知道 App Secret)
- 请求内容没有被篡改(因为签名是基于请求体计算的)
如果不一致,直接拒绝请求,返回 400 错误。
追问:签名验证失败后怎么处理?
答:记录安全日志(包含请求 IP、请求时间、请求体),返回 400 错误,拒绝请求。同时发送告警通知,提醒运营人员检查是否有恶意攻击。
如果短时间内出现大量签名验证失败(比如 1 分钟内 100 次),可能是:
- App Secret 泄露,需要立即更换
- 遭受 DDoS 攻击,需要启用 IP 黑名单
- 平台更新了签名算法,需要更新代码
Q4: Webhook 为什么要立即返回 200,然后异步处理?
标准答案:
因为平台有超时限制,通常是 3-5 秒。如果我们在 HTTP 请求内处理业务逻辑(如写数据库、调用其他服务),一旦处理时间超过 3 秒,平台会认为失败并重发,导致重复订单。
正确做法:
验证签名 → 立即返回 200 → 把请求体放到 MQ → 异步消费处理。
这样可以保证在 1 秒内返回 200,避免超时重发。MQ 消费者里做幂等校验、格式转换、保存订单等业务逻辑。
追问:如果 MQ 消费失败怎么办?
答:我们配置了重试机制,最多重试 3 次。如果 3 次都失败,消息会进入死信队列,人工介入处理。同时发送告警通知,提醒开发人员排查问题。
Q5: 如何保证订单不重复创建(幂等性)?
标准答案:
我们用 platform + platform_order_no 作为唯一键,在数据库中创建唯一索引。
UNIQUE KEY uk_platform_order (platform, platform_order_no)
收到订单后,先查询数据库,判断订单是否已存在:
- 如果订单已存在,检查状态是否有更新,有更新就同步本地状态,没有更新就直接返回成功
- 如果订单不存在,才创建新订单
这样可以保证同一个平台订单号只会创建一次,即使平台重复推送,也不会创建重复订单。
为什么不能只用平台订单号?
因为不同平台可能有相同的订单号。比如 Amazon 的订单号是 123456,Shopify 的订单号也可能是 123456。如果只用订单号作为唯一键,会导致冲突。
所以必须用”平台 + 平台订单号”作为唯一标识。
高并发场景的优化:
如果并发量很高,可能出现以下情况:
- 线程 A 查询订单,发现不存在,准备插入
- 线程 B 也查询订单,也发现不存在,也准备插入
- 线程 A 和 B 同时插入,可能都成功(如果没有唯一索引)
解决方案:用分布式锁(Redis)保证同一时刻只有一个线程处理同一个订单。锁的 Key 是 order:lock:{platform}:{platform_order_no},过期时间 10 秒。
追问:如果数据库唯一索引冲突怎么办?
答:唯一索引冲突说明订单已存在,我们会捕获 DuplicateKeyException 异常,然后查询已存在的订单,检查状态是否有更新。
这是一种乐观锁的实现方式,性能比悲观锁(分布式锁)更好。只有在并发量特别高的场景下,才需要用分布式锁。
Q6: 订单状态机有什么好处?
标准答案:
订单状态机可以集中管理状态流转规则,保证状态流转的合法性。
比如订单不能从待处理直接跳到已发货,必须经过待备货、备货中、待发货这几个状态。如果代码尝试非法流转,状态机会自动拦截。
另外,状态机可以配置 Guard(前置条件)和 Action(后置动作)。比如从待发货流转到已发货,Action 自动调用 WMS 扣减库存、调用 TMS 创建运单,不需要在业务代码中手动调用。
好处:
- 状态流转逻辑集中管理,代码可维护性高
- 避免出现非法状态流转,保证数据一致性
- Guard 和 Action 解耦业务逻辑,提高代码复用性
追问:订单状态机如何持久化?
答:每次状态流转都会更新 order_main 表的 status 字段,同时写入 order_log 表记录操作日志。状态机本身是无状态的,状态存储在数据库中。这样即使服务重启,订单状态也不会丢失。
Q7: 供应链系统和电商平台是什么关系?
标准答案:
供应链系统是库存的”真实来源”,电商平台只是”展示渠道”。
商家在多个平台开店(Amazon、Shopify、eBay),但库存是统一管理的。我们的做法是固定分配库存,比如仓库有 500 件,分配给 Amazon 200 件、Shopify 150 件、eBay 100 件。
用户在 Amazon 下单后,Amazon 平台扣减自己的库存,然后推送订单到我们系统。我们接收订单后,冻结平台库存,等发货后再扣减分配库存和仓库库存。
供应链系统的价值:
- 多平台统一库存管理
- 全局库存可见性
- 智能库存分配
- 智能补货预警
追问:为什么不实时同步库存给平台?
答:如果把仓库的 500 件实时同步给每个平台,那么每个平台都显示 500 件,用户可能在多个平台同时下单,总订单量超过 500 件,导致仓库发不出货。所以我们采用固定分配策略,根据各平台销售速度,提前分配好每个平台的库存配额。
Q8: 退款是在哪里处理的?
标准答案:
退款是在电商平台处理的,不是在供应链平台。
买家在 Amazon 申请退款,商家在 Amazon 后台审核,Amazon 执行退款。我们的供应链系统只是接收退款通知,然后处理库存调整。
退款类型处理:
- 仅退款(未发货):释放冻结库存,让库存恢复可售
- 仅退款(已发货):货物不回来,我们不处理库存
- 退货退款:等待退货入库,质检后增加可售库存
所以供应链系统的职责是:接收退款通知 → 调整库存状态 → 记录退款信息。
追问:退货入库后如何处理?
答:退货入库后,WMS 会进行质检。质检合格的商品,增加可售库存;质检不合格的商品(破损、使用过),标记为次品,不能再销售。质检结果会同步到 OMS,更新退款记录。
Q9: 多语言内容如何管理?
标准答案:
我们用 pim_product_i18n 表存储多语言内容,表结构是 ref_type + ref_id + lang_code 三字段唯一索引。
ref_type表示关联类型(SPU 或 SKU)ref_id表示关联 IDlang_code表示语言代码(如 zh-CN、en-US、ja-JP)
为了提高效率,我们集成了 AI 翻译功能。运营填写中文内容后,点击”AI 一键翻译”,系统调用 OpenAI API,自动生成多语言内容。
翻译结果会标记为 AI 翻译(is_ai_translated=1),运营可以人工校对修改。
追问:AI 翻译的准确率如何保证?
答:我们设计了专业的 Prompt,要求 AI 按照电商文案风格翻译,标题简洁有力包含关键词,描述突出产品价值。翻译结果会标记为 AI 翻译,运营可以人工校对。实际使用中,准确率在 85% 以上,人工校对后可以达到 95%。
Q10: 如何对接 Amazon SP-API?
标准答案:
Amazon SP-API 使用 OAuth 2.0 授权,需要先获取 refresh_token,然后用 refresh_token 换取 access_token。
授权流程:
- 首次授权时,引导卖家到 Amazon 授权页面
- 授权后回调我们的接口,附带
code - 我们用
code换取refresh_token,永久保存 - 每次调用 API 前,用
refresh_token换取access_token(有效期 1 小时) - 如果
access_token过期,自动刷新
拉取订单:
调用 GetOrders 接口,传入时间范围(CreatedAfter)和订单状态(OrderStatuses)。一次最多返回 100 条,需要用 NextToken 循环翻页。
追问:如果 refresh_token 过期怎么办?
答:refresh_token 理论上是永久有效的,但如果卖家在 Amazon 后台撤销授权,refresh_token 会失效。我们会捕获 401 错误,然后通知卖家重新授权。
Q11: 订单风控规则有哪些?
标准答案:
我们设计了 5 类风控规则:
- 地址有效性:邮政编码是否存在,国家-州组合是否合法
- 金额异常:单笔订单金额超过历史均值 10 倍,或单价明显低于成本价
- 高频下单:同一买家 1 小时内下 5 单以上(可能是刷单)
- 同地址大量下单:同一收货地址 24 小时内收到 10 单以上
- 高退款率买家:历史退款率超过 50% 的买家
每个规则对应一个分值,总分 0-30 分为低风险,31-70 分为中风险,71 分以上为高风险。
高风险订单会进入人工审核队列,运营专员 24 小时内处理。
追问:风控规则如何扩展?
答:我们用策略模式设计风控规则引擎。每个规则实现 RiskRule 接口,evaluate 方法返回风险分值。新增规则只需实现接口,注册为 Spring Bean,风控服务会自动扫描并执行。这样规则可插拔,无需修改核心代码。
Q12: 如何实现订单拆单?
标准答案:
拆单有 4 种触发规则:
- 跨仓库:订单商品分布在不同仓库,需要分开发货
- 物流限制:含电池商品不能与普通商品走同一渠道
- 超重/超体积:单个包裹超过物流商重量上限(30kg)
- 平台要求:某些平台要求每个包裹单独运单
拆单算法:
先按仓库分组,然后检查每组是否需要进一步拆分。比如含电池商品不能和普通商品一起空运,需要单独发货;单个包裹超过 30kg,需要拆分成多个包裹。
拆单后,系统会创建多个子订单,每个子订单有独立的订单号(父订单号-序号)、独立的发货流程。子订单通过 parent_order_id 关联父订单。
父订单状态计算:
父订单状态 = 最落后子订单状态。比如有 3 个子订单,2 个已发货,1 个待发货,父订单状态就是待发货。
追问:拆单后如何计算运费?
答:拆单后,每个子订单单独计算运费。总运费 = 所有子订单运费之和。如果总运费超过原订单运费,差额由商家承担;如果总运费低于原订单运费,节省的运费归商家。
Q13: OMS 如何和其他系统交互?
标准答案:
OMS 需要和 WMS(仓储)、TMS(物流)、FMS(财务)、PIM(商品)等系统交互,我们用了 3 种技术方案:
1. Feign 同步调用
适用于需要立即返回结果的场景。比如订单创建时查询商品信息、冻结库存。
优点:实时性强,调用简单。 缺点:性能较低,调用方需要等待。
2. MQ 异步消息
适用于不需要立即返回结果的场景。比如订单发货后扣减库存、订单完成后触发财务结算。
优点:性能高,解耦,削峰填谷。 缺点:实时性差,需要处理消息丢失。
3. Seata 分布式事务
适用于需要保证多个操作原子性的场景。比如订单创建时,必须同时创建订单和冻结库存,两个操作要么都成功,要么都失败。
优点:保证数据一致性。 缺点:性能开销大,复杂度高。
具体场景举例:
- 订单创建 → 冻结库存:Feign + Seata(需要立即返回结果,保证原子性)
- 订单发货 → 扣减库存:MQ 异步消息(不需要立即完成,削峰填谷)
- 订单取消 → 释放库存:MQ 异步消息(不需要立即完成,解耦系统)
- 订单完成 → 财务结算:MQ 异步消息(不需要立即完成,解耦系统)
- 订单创建 → 查询商品信息:Feign 同步调用(需要立即返回结果,不涉及事务)
追问:为什么订单创建用 Seata,订单发货用 MQ?
答:订单创建时,如果库存不足,要立即返回错误给用户,不能创建订单。所以必须用 Feign 同步调用,立即知道库存是否充足。同时订单创建和库存冻结必须同时成功或同时失败,所以用 Seata 保证原子性。
订单发货后,库存扣减不需要立即完成,可以异步处理。而且发货是高频操作,如果用 Feign 同步调用,会导致 WMS 压力过大。用 MQ 可以削峰填谷,提高系统吞吐量。如果扣减失败,RocketMQ 会自动重试,保证最终一致性。
Q14: Seata AT 模式的原理是什么?
标准答案:
Seata AT 模式是一种自动化的分布式事务解决方案,分为两个阶段:
阶段一(Try):执行业务操作
- OMS 创建订单,Seata 自动记录 undo_log(回滚日志)
- WMS 冻结库存,Seata 自动记录 undo_log
- 如果任何一步失败,Seata 自动回滚
阶段二(Confirm/Cancel):提交或回滚
- 如果所有操作都成功,Seata 提交全局事务,删除 undo_log
- 如果任何操作失败,Seata 根据 undo_log 自动回滚所有操作
undo_log 的作用:
undo_log 记录了操作前的数据快照。比如库存冻结前是 100,冻结后是 90,undo_log 记录”库存从 90 改回 100”。如果需要回滚,Seata 会执行 undo_log 中的 SQL,把数据恢复到操作前的状态。
AT 模式的优点:
- 侵入性低,只需要在方法上加
@GlobalTransactional注解 - 自动记录 undo_log,不需要手动实现回滚逻辑
- 支持大部分数据库(MySQL、PostgreSQL、Oracle)
追问:AT 模式和 TCC 模式有什么区别?
答:AT 模式是自动化的,Seata 自动记录 undo_log,自动回滚。TCC 模式需要手动实现 Try、Confirm、Cancel 三个方法,侵入性高,但性能更好。
我们选择 AT 模式,因为开发成本低,性能满足业务需求。如果是高并发场景(比如秒杀),可以考虑 TCC 模式。
面试准备建议
必须能讲清楚的
- SPU/SKU 的区别:用生活化的例子讲清楚(比如蓝牙耳机)
- Webhook 签名验证原理:讲清楚 HMAC-SHA256 算法,讲清楚为什么要验证签名
- 为什么要立即返回 200:讲清楚超时重发的问题,讲清楚异步处理的好处
- 订单幂等性设计:讲清楚唯一索引、分布式锁、状态同步
- 订单状态机的好处:讲清楚集中管理、避免非法流转、Guard 和 Action
- 风控规则引擎的设计:讲清楚策略模式、规则可插拔、风险分值计算
- 订单拆单的触发规则:讲清楚 4 种规则,讲清楚拆单算法
- Feign vs MQ 的选择标准:讲清楚什么时候用 Feign,什么时候用 MQ
- Seata AT 模式的原理:讲清楚两阶段提交,讲清楚 undo_log 的作用
- 供应链系统的价值:不是解决超卖,而是统一管理多平台库存
表达技巧
- 先讲业务场景,再讲技术方案:不要上来就讲技术,先让面试官理解业务背景
- 用数据说话:效果提升 80%、成本降低 90%、日均处理 5 万+、拦截欺诈订单 3000+ 单/月
- 讲清楚为什么这样设计:不要只讲怎么做,要讲为什么这样做,有什么好处
- 准备追问:每个技术点都要准备 1-2 个追问,展示技术深度
- 用对比说明:比如 Feign vs MQ、AT 模式 vs TCC 模式、固定分配 vs 动态分配
可能的追问和回答
追问 1:如果 AI 翻译的内容不准确怎么办?
答:翻译结果会标记为 AI 翻译(is_ai_translated=1),运营可以人工校对修改。修改后,标记会变为 0,表示已人工校对。实际使用中,准确率在 85% 以上,人工校对后可以达到 95%。
追问 2:如果平台推送的订单格式变了怎么办?
答:我们会监控订单解析失败率。如果失败率突然升高,说明平台可能更新了接口。我们会立即排查,更新适配器代码。同时我们会订阅平台的开发者邮件,提前知道接口变更。
追问 3:如果 Seata 事务超时怎么办?
答:Seata 默认超时时间是 60 秒。如果业务逻辑执行时间超过 60 秒,会触发超时回滚。我们可以通过 @GlobalTransactional(timeoutMills = 120000) 调整超时时间。但更好的做法是优化业务逻辑,减少执行时间。
追问 4:如果 MQ 消息丢失怎么办?
答:RocketMQ 支持消息持久化,消息会先写入磁盘,再返回成功。即使 Broker 宕机,消息也不会丢失。另外,我们配置了消息重试机制,最多重试 16 次。如果 16 次都失败,消息进入死信队列,人工处理。
追问 5:如果订单拆单后,某个子订单取消了怎么办?
答:子订单可以独立取消。取消后,释放该子订单的库存,更新父订单状态。如果所有子订单都取消了,父订单状态变为”已取消”。如果只有部分子订单取消,父订单状态保持不变,等待其他子订单完成。
完整简历版
项目经历
跨境电商 SaaS 供应链管理平台 - 商品中心与订单中心(PIM + OMS) 2024.06 - 2024.12 | 核心开发 技术栈:Spring Boot 3.2、Spring State Machine、MyBatis-Plus、MySQL 8.0、Redis 7.0、RocketMQ 5.0、Seata 1.7、XXL-Job、DeepSeek API
项目描述: 负责跨境电商供应链 SaaS 平台的商品中心(PIM)和订单中心(OMS)开发。商品中心采用 SPU + SKU 两级结构,支持笛卡尔积自动生成 SKU、多语言 AI 翻译(中英日韩等 8 种语言)、多平台价格库存同步(Amazon、Shopify、eBay、TikTok Shop)。订单中心负责多平台订单接入、Webhook 签名验证、订单状态机管理、风控规则引擎、订单超期预警、拆单合单等全流程业务。系统日均处理订单 10 万+,覆盖 8 个销售平台,支持 20+ 国家地区。
核心业绩:
-
使用 SPU + SKU 两级结构 + 笛卡尔积算法 实现了商品规格的批量化录入 达到了商品录入效率提升 80%,运营人员录入 100 个 SKU 的时间从 2 小时降到 20 分钟
- 设计 SPU(标准产品单元)和 SKU(库存量单位)两级模型,SPU 维护共性属性(标题、品牌、类目、详情),SKU 维护差异属性(颜色、尺寸、价格、库存)
- 用笛卡尔积算法自动生成 SKU 组合,比如颜色 5 种 × 尺寸 4 种 = 20 个 SKU,运营只需勾选规格即可批量生成
- 支持手动调整:自动生成后可删除不存在的组合,逐个设置 SKU 属性,也可以批量设置默认值
-
使用 DeepSeek API + Prompt 工程 实现了商品多语言内容的 AI 自动翻译 达到了翻译成本从人工翻译每条 5 元降到 AI 翻译每条 0.05 元,准确率 85% 以上
- 设计 i18n 表结构(item_id + language + field_name + translated_value),支持 8 种语言扩展
- Prompt 工程:包含商品类目上下文、品牌名保留规则、专业术语词库,确保翻译符合电商场景
- 支持人工校对:AI 翻译结果标记 is_ai_translated=1,运营修改后变为 0,混合模式准确率达 95%
- 异步处理:MQ 队列 + 限流(QPS=10),避免 API 频率限制,单批 100 条商品翻译耗时 30 秒
-
使用 Webhook 签名验证 + HMAC-SHA256 + 异步消费 实现了多平台订单的安全接入 达到了订单接收成功率 99.95%,零安全事故
- 实现 Amazon SP-API、Shopify、eBay、TikTok Shop 四大平台的 Webhook 接入,统一适配器模式
- 签名验证:用 HMAC-SHA256 算法,每个平台独立 secret,原始请求体计算签名,防止请求伪造
- 同步快速返回:Webhook 接收后 100ms 内返回 200,签名验证通过即把请求体放入 MQ
- 异步处理:MQ 消费者做幂等校验、格式转换、入库,平台不重发的同时保证数据可靠性
-
使用 Redis 分布式锁 + 平台订单号唯一索引 实现了订单创建的幂等性保证 达到了重复订单率 0%,平台重发请求零误处理
- 用”平台 + 平台订单号”作为唯一标识,建立数据库唯一索引,从存储层保证幂等
- Redis 分布式锁(SETNX + 10 秒过期):防止同一订单并发处理,加锁失败等待 100ms 重试
- 双重检查:加锁成功后再查数据库,订单已存在则比对状态,状态不同则更新,状态相同直接返回
- 内部订单号生成:ORD + yyyyMMddHHmmss + 6 位随机数,时间戳保证可读性,随机数防止同秒冲突
-
使用 Spring State Machine 实现了订单状态机管理 达到了订单状态流转零异常,业务逻辑解耦 70%
- 设计 10 个订单状态(待处理、风控审核、待备货、备货中、待发货、已发货、运输中、已签收、已完成、已取消)
- 配置 State、Event、Transition、Guard、Action 五要素,比如待处理 + 通过风控 → 待备货
- Guard 前置条件:订单金额超 1000 元自动触发风控审核,金额低则跳过
- Action 后置动作:状态流转到已发货时自动调用 WMS 扣减库存、调用 TMS 创建运单
- 状态机集中管理流转规则,避免业务代码中散落的 if-else 判断,新增状态只需修改配置
-
使用 策略模式 + 责任链 实现了订单风控规则引擎 达到了风控通过率 95%,欺诈订单识别率 92%
- 设计 5 类风控规则:黑名单检查、订单金额阈值、收货地址异常、下单频率、IP 风险
- 策略模式:每条规则实现 RiskRule 接口,独立计算风险评分(0-100 分),互不依赖
- 责任链组合:所有规则串行执行,累加评分,超过 80 分触发人工审核,低于 30 分自动通过
- 规则可配置:风控阈值存数据库,运营可在后台调整,无需改代码重新发布
- 风控日志记录:每条规则的命中情况都记录到日志表,便于追溯和优化
-
使用 Feign + Seata AT 模式 实现了订单创建与库存冻结的分布式事务 达到了数据一致性 100%,零超卖
- OMS 创建订单后通过 Feign 同步调用 WMS 冻结库存,Seata 全局事务保证原子性
- AT 模式原理:阶段一执行业务并记录 undo_log,阶段二根据成功失败决定提交或回滚
- 全局锁:Seata 在执行 update 前获取全局锁,防止脏写
- 超时控制:默认 60 秒超时自动回滚,特殊业务可通过 @GlobalTransactional(timeoutMills) 调整
-
使用 RocketMQ 异步消息 实现了订单发货与库存扣减的解耦 达到了发货接口响应时间从 800ms 降到 50ms
- 订单发货时 OMS 立即更新状态返回成功,发送 OrderShippedEvent 到 MQ
- WMS 异步消费消息扣减库存,主流程不受 WMS 性能影响
- 重试机制:消费失败重试 16 次,仍失败进入死信队列人工处理
- 消息幂等:用订单号作为唯一标识,重复消息只处理一次
-
使用 XXL-Job 定时任务 + 三级预警 实现了订单超期预警系统 达到了订单超时率从 5% 降到 0.8%
- 定时任务每 30 分钟扫描一次订单,按当前状态匹配预警规则
- 三级预警:黄色(接近超时 80%)发邮件,橙色(已超时)发企业微信,红色(超时 2 倍)电话通知主管
- 不同状态不同 SLA:待发货 24 小时、运输中 7 天、待签收 15 天
- 预警记录:每次预警记录到 order_alert_log 表,便于复盘和考核
-
使用 拆单合单规则引擎 实现了多仓发货与买家合并发货 达到了物流成本降低 25%
- 拆单触发:多仓库存、不同发货时效、平台限制(如 SKU 上限)、超大件商品
- 合单触发:同一买家短时间多笔订单(30 分钟内)、同一收货地址、同一仓库发货
- 拆单算法:贪心算法分配 SKU 到仓库,优先离收货地址近的仓库,最小化运费
- 合单后用一个运单号、一个面单,物流成本从平均 25 元降到 18 元
最后提醒:
面试前把这份笔记看 2-3 遍,重点记住:
- 商品的 SPU/SKU 模型和笛卡尔积算法
- 订单接入的 7 个步骤(签名 → 幂等 → 转换 → 库存 → 风控 → 状态 → 推送)
- 订单幂等的三层保证(唯一索引 + Redis 锁 + 双重检查)
- 订单状态机的 5 要素(State、Event、Transition、Guard、Action)
- 风控规则引擎的策略模式 + 责任链
- 分布式事务的选型(Seata 强一致 vs MQ 最终一致)
面试时不要背书,要像讲故事一样自然地表达。如果面试官追问细节,可以展开讲技术实现。准备好画图,订单状态机图、订单幂等流程图、Seata 事务流程图、MQ 异步流程图一定要能画出来。