一、业务概览
1.1 系统定位
PIM+OMS 是供应链 SaaS 平台的核心业务系统,由两个紧密关联的子系统组成:
PIM(Product Information Management)商品信息管理系统
- 管理商品的基础信息、规格属性、多语言内容、多平台定价
- 支持 SPU/SKU 两级商品体系,自动生成 SKU 组合
- 支持多语言内容管理,AI 自动翻译商品标题和描述
- 支持多平台多币种定价,自动计算税费
OMS(Order Management System)订单管理系统
- 统一管理来自多个电商平台的订单
- 支持订单自动创建、状态流转、风险控制、超时预警
- 支持订单拆分合并、库存分配、物流对接
- 支持分布式事务,保证订单和库存的强一致性
1.2 在供应链中的位置
上游系统:
- SRM 供应商管理系统:提供商品的供应商信息、采购成本
- WMS 仓储管理系统:提供商品的库存数据
下游系统:
- WMS 仓储管理系统:订单创建时冻结库存,订单发货时扣减库存
- TMS 物流管理系统:订单发货后创建运单,跟踪物流状态
- FMS 财务结算系统:订单完成后生成财务流水,计算利润
外部系统:
- 电商平台:Amazon、Shopify、eBay、Walmart 等
- AI 翻译服务:DeepSeek API、OpenAI API
1.3 数据规模
- 商品数据:管理 50 万+ SPU,300 万+ SKU
- 订单数据:日均处理订单 10 万+,峰值 QPS 5000+
- 多平台对接:对接 10+ 电商平台,支持 20+ 国家和地区
- 多语言支持:支持 15+ 语言,AI 翻译准确率 95%+
- 数据库规模:订单表按月分表,单表 500 万行
二、技术亮点
2.1 SPU/SKU 两级商品体系 + 笛卡尔积算法
SPU:
SKU:
问题背景
跨境电商的商品管理面临三个核心问题:
第一,商品规格组合爆炸。 一件 T恤有 3 种颜色(红、蓝、绿)× 3 种尺码(S、M、L)= 9 个 SKU。如果手动创建,商家要填 9 次表单,效率低,容易出错。
第二,公共属性冗余存储。 品牌、材质、HS 编码、产地这些属性对所有 SKU 都一样,如果每个 SKU 都存一遍,数据冗余严重,修改时要改 9 次。
第三,多平台同步困难。 商家在 Amazon、Shopify、eBay 都开店,同一个商品要同步到多个平台,如果没有统一的商品主数据,容易出现数据不一致。
解决方案
我们设计了 SPU/SKU 两级商品体系 + 笛卡尔积自动生成算法。
SPU(Standard Product Unit)标准产品单元:存储商品的公共属性,如品牌、材质、HS 编码、产地、商品描述等。一个 SPU 代表一类商品。
SKU(Stock Keeping Unit)库存量单位:存储商品的规格属性,如颜色、尺码、价格、库存、条形码等。一个 SKU 代表一个具体的可售商品。
笛卡尔积算法:商家只需要在 SPU 层面配置规格选项(颜色:红、蓝、绿;尺码:S、M、L),系统自动生成所有 SKU 组合(红-S、红-M、红-L、蓝-S、蓝-M、蓝-L、绿-S、绿-M、绿-L)。
技术实现
第一步,商家创建 SPU。 填写品牌、材质、HS 编码、产地、商品描述等公共属性。
第二步,商家配置规格选项。 选择规格类型(颜色、尺码),填写规格值(红、蓝、绿;S、M、L)。
第三步,系统自动生成 SKU。 使用笛卡尔积算法,生成所有规格组合。
核心代码实现:
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;
}
第四步,商家补充 SKU 信息。 系统生成 SKU 后,商家补充每个 SKU 的价格、库存、条形码、SKU 图片等信息。
第五步,数据持久化。 在一个事务中插入 SPU、SKU、规格选项、规格值。
效果
- 效率提升:商家创建商品的时间从 10 分钟降到 2 分钟,效率提升 80%
- 数据一致性:公共属性只存一份,修改时只需要改 SPU,所有 SKU 自动生效
- 扩展性强:支持任意数量的规格类型和规格值,3 种颜色 × 3 种尺码 = 9 个 SKU,5 种颜色 × 4 种尺码 × 2 种材质 = 40 个 SKU
自动生成SKU的前提: 我们的数据库中有一个用来描述 SPU 属性模板的表
在这个表中 可以给某个SPU配置属性的名称 属性的值 然后系统就可以根据这些属性的名称 属性的值 自动计算笛卡尔积 生成对应的SKU
2.2 多语言内容管理 + AI 自动翻译
问题背景
跨境电商需要在多个国家销售,每个国家的语言不同。商家需要为每个商品准备多语言的标题、描述、关键词。
第一,人工翻译成本高。 一个商品要翻译成 10 种语言,人工翻译费用 100 元/语言,10 种语言就是 1000 元。商家有 1000 个商品,翻译费用就是 100 万。
第二,翻译质量不稳定。 不同翻译人员的水平不同,翻译质量参差不齐。有些翻译不专业,翻译出来的内容不符合电商场景。
第三,翻译周期长。 人工翻译需要 1-2 天,商家上新速度慢,错过销售时机。
解决方案
我们设计了 多语言内容管理 + AI 自动翻译。
多语言内容管理:设计 product_i18n 表,存储商品的多语言内容。每个商品的每种语言都是一条记录,通过 spu_id + language_code 唯一标识。
AI 自动翻译:集成 DeepSeek API 或 OpenAI API,商家填写中文标题和描述后,系统自动翻译成英语、西班牙语、法语、德语、日语等 15+ 语言。
专业 Prompt 设计:针对电商场景设计专业 Prompt,提升翻译质量。
有一张多语言的数据库表 在这个表中存储了该SKU各个国家的语言翻译
租户在进行录入SKU信息的时候 只需要使用一种语言录入即可 然后在录入界面上 我们可以让租户自己选择是否需要进行AI的翻译 如果需要则让租户选择都需要哪些语言 当租户选择好语言后 我们可以通过AI自动进行翻译 并且把翻译后的数据 存储到这个多语言的数据库表中
技术实现
第一步,商家填写中文内容。 填写商品标题、描述、关键词。
第二步,系统调用 AI 翻译 API。 使用 DeepSeek API 或 OpenAI API,传入中文内容和目标语言,返回翻译结果。
核心 Prompt 设计:
你是一个专业的跨境电商翻译专家。请将以下商品信息翻译成{目标语言}。
要求:
1. 保持电商营销风格,突出卖点
2. 使用目标市场的常用表达
3. 保留品牌名、型号等专有名词
4. 标题控制在 200 字符以内
5. 描述保持原文的段落结构
商品标题:{title}
商品描述:{description}
请返回 JSON 格式:
{
"title": "翻译后的标题",
"description": "翻译后的描述"
}
第三步,系统保存翻译结果。 将翻译结果保存到 product_i18n 表,language_code 为目标语言代码(en-US、es-ES、fr-FR 等)。
第四步,商家审核和修改。 商家可以查看翻译结果,如果不满意可以手动修改。
第五步,同步到电商平台。 商家发布商品时,系统根据平台的目标市场,选择对应语言的内容同步到平台。 如果电商平台支持批量的导入 或者 支持API的导入 SaaS平台就可以直接读取数据库中的多语言 直接对接到电商平台 把SKU发布到电商平台
效果
- 成本降低:翻译成本从 100 元/语言降到 0.1 元/语言,降低 99.9%
- 效率提升:翻译时间从 1-2 天降到 10 秒,效率提升 10000 倍
- 质量稳定:AI 翻译质量稳定,准确率 95%+,商家满意度 90%+
2.3 多平台订单集成 + Webhook 签名验证
问题背景
跨境电商商家在多个平台开店(Amazon、Shopify、eBay、Walmart),每个平台都有自己的订单系统。商家需要统一管理所有平台的订单。
第一,订单来源分散。 商家要登录多个平台后台查看订单,效率低,容易漏单。
第二,订单数据格式不统一。 每个平台的订单数据结构不同,字段名不同,需要逐个适配。
第三,订单同步时效性差。 如果用定时任务拉取订单,延迟 5-10 分钟,影响发货速度。
第四,安全性问题。 如果用 Webhook 接收订单,需要验证请求来源,防止恶意请求。
解决方案
我们设计了 多平台订单集成 + Webhook 签名验证。
两种集成方式:
- 主动拉取(Polling):定时任务每 5 分钟调用平台 API 拉取新订单,适用于不支持 Webhook 的平台(如 eBay)
- 被动接收(Webhook):平台主动推送订单到我们的回调接口,实时性高,适用于支持 Webhook 的平台(如 Shopify)
Webhook 签名验证:使用 HMAC-SHA256 算法验证请求签名,防止恶意请求。 绑定Webhook的时候 需要指定密钥
技术实现
主动拉取方式:
第一步,配置定时任务。使用 XXL-Job 配置定时任务,每 5 分钟执行一次。
第二步,调用平台 API。使用平台提供的 SDK 或 HTTP 客户端,调用订单查询 API,传入查询条件(时间范围、订单状态)。
第三步,解析订单数据。将平台返回的订单数据转换为统一的订单模型。 要使用到适配器模式
第四步,保存订单。调用订单创建接口,保存订单到数据库。
被动接收方式(Webhook):
第一步,平台推送订单。商家在平台后台配置 Webhook URL(如 https://api.example.com/webhook/shopify),平台创建订单时自动推送到这个 URL。
第二步,验证签名。平台在 HTTP Header 中传入签名(如 X-Shopify-Hmac-SHA256),我们使用相同的算法计算签名,对比是否一致。
核心代码实现:
public boolean verifySignature(String requestBody, String signature, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
mac.init(secretKey);
byte[] hash = mac.doFinal(requestBody.getBytes());
String calculatedSignature = Base64.getEncoder().encodeToString(hash);
return calculatedSignature.equals(signature);
} catch (Exception e) {
log.error("签名验证失败", e);
return false;
}
}
第三步,解析订单数据。将平台推送的订单数据转换为统一的订单模型。
第四步,保存订单。调用订单创建接口,保存订单到数据库。
第五步,返回响应。返回 HTTP 200,告诉平台接收成功。如果返回非 200,平台会重试推送。
效果
- 订单同步实时性:Webhook 方式实时接收订单,延迟从 5 分钟降到 1 秒
- 安全性提升:签名验证防止恶意请求,拦截率 100%
- 统一管理:商家在一个后台查看所有平台订单,效率提升 80%
主动拉取 推荐的做法 Webhook 快/及时 并发
2.4 订单幂等性设计 + 分布式锁
问题背景
订单创建面临三个幂等性问题:
第一,平台重复推送。 Webhook 推送失败时,平台会重试推送,可能导致同一个订单被创建多次。
第二,定时任务重复拉取。 定时任务拉取订单时,可能因为时间窗口重叠,拉取到重复的订单。
如果不做幂等性控制,会导致:
- 订单重复创建,商家发货两次,造成损失
- 库存重复冻结,可售库存减少,影响销售
- 财务数据错误,订单金额重复计算
解决方案
我们设计了 订单幂等性设计 + 分布式锁。
核心思路: 使用”平台 + 平台订单号”作为唯一标识,保证同一个平台的同一个订单只能创建一次。
三层防护:
- 数据库唯一索引:在
order_main表创建唯一索引uk_platform_order_no(platform, platform_order_no),数据库层面防止重复插入 - Redis 分布式锁:在创建订单前,先获取分布式锁
order:create:{platform}:{platform_order_no},防止并发创建- Webhook 被动接收 + 重复推送 需要分布式锁
- 完全使用数据库的唯一约束 也可以解决幂等性问题
- 如果完全依赖数据库的唯一约束 会让更多的请求 只有达到数据库之后才知道是否冲突 每个请求都需要走完所有的流程 订单同步的流程
- 库存
- 日志
- 订单
- 订单详情表
- 回滚 如果没有分布式锁 冲突的数据 会走完整个流程后 才回滚 如果有分布式锁 在走这些流程之前就已经拦截返回
- API 定时拉取 可能会拉取到相同的订单数据 但是不会出现并发的问题 但是需要保证幂等性
- Webhook 被动接收 + 重复推送 需要分布式锁
- 业务层查重:在创建订单前,先查询订单是否已存在,如果存在直接返回
当前的这个业务实现的分布式锁 我们不用使用Redisson 因为Redisson主要是为了解决并发的问题 和 锁竞争的问题
我们这个业务主要是为了解决 幂等性问题 所以我们可以通过 SETNX (platform + platform_order_no) 同时设置过期时间 15分钟
如果拉取到数据或者推送过来的数据 先进行 SETNX 如果成功 就进行保存订单的逻辑 如果失败 就直接返回
获取数据有两种方式
一旦获取到数据后 —> 分布式锁(S E T N X) —> 做数据的转换(适配器) —> 操作数据库表
分布式的粒度: platform + platform_order_no 针对的是某个具体的SKU 并发是非常高(只有 Webhook 重复推送)
为什么要使用分布式锁 ?? 我们的主要目的是为了防止 幂等性 但是Webhook也确实可能会出现重复推送的问题 分布式锁的粒度 ?? 分布式锁的实现过程 ??
- 先获取订单的数据
- 遍历订单的数据
- 获取 platform + platform_order_no
- 获取分布式锁
- 如果获取失败 说明 这个平台中的这个订单 已经存在了 直接返回 没有抢锁的等待时间 也没有抢锁的重试
- 如果获取成功 就直接走插入订单的逻辑
技术实现
第一步,获取分布式锁。
String lockKey = "order:create:" + platform + ":" + platformOrderNo;
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("订单创建中,请勿重复提交");
}
第二步,查询订单是否已存在。
OrderMain existingOrder = orderMapper.selectOne(
new LambdaQueryWrapper<OrderMain>()
.eq(OrderMain::getPlatform, platform)
.eq(OrderMain::getPlatformOrderNo, platformOrderNo)
);
if (existingOrder != null) {
return existingOrder;
}
第三步,创建订单。
OrderMain order = new OrderMain();
order.setOrderNo(generateOrderNo());
order.setPlatform(platform);
order.setPlatformOrderNo(platformOrderNo);
order.setStatus(OrderStatus.PENDING);
orderMapper.insert(order);
第四步,释放分布式锁。
千万不能手动释放 要等着锁自动过期 因为这个锁的主要目的是为了保证在这段时间内不允许重复插入
数据库唯一索引:
数据库的唯一约束是一个兜底方案 假设 Redis 失效了 最差的情况是 每个订单都会执行一遍 重复的订单会被回滚
ALTER TABLE order_main ADD UNIQUE INDEX uk_platform_order_no(platform, platform_order_no);
效果
- 幂等性保证:同一个订单重复推送 100 次,只创建 1 次,幂等性 100%
- 并发安全:分布式锁防止并发创建,冲突率 0%
- 性能优化:Redis 分布式锁响应时间 < 10ms,不影响订单创建性能
2.5 订单状态机 + Spring State Machine
问题背景
订单的状态流转非常复杂,涉及 10+ 种状态和 20+ 种状态转换。
第一,状态转换规则复杂。 订单从”待支付”到”已完成”,中间要经过”待发货”、“已发货”、“已签收”等多个状态。每个状态只能转换到特定的下一个状态,不能随意跳转。
第二,状态转换前后需要执行业务逻辑。 比如订单从”待发货”转换到”已发货”时,需要调用 WMS 出库、调用 TMS 创建运单。
第三,状态转换失败需要回滚。 如果 WMS 出库失败,订单状态不能变成”已发货”,需要保持”待发货”。
第四,代码可维护性差。 如果用 if-else 判断状态转换,代码会非常臃肿,难以维护。
解决方案
我们使用 Spring State Machine 实现订单状态机。
核心概念:
- State(状态):订单的 10 种状态(待支付、待发货、已发货、已签收、已完成、已取消等)
- Event(事件):触发状态转换的事件(支付成功、发货、签收、取消等)
- Transition(转换):定义从一个状态到另一个状态的转换规则
- Guard(守卫):状态转换的前置条件,返回 true 才允许转换
- Action(动作):状态转换时执行的业务逻辑
技术实现
第一步,定义状态和事件。
public enum OrderState {
PENDING_PAYMENT, // 待支付
PENDING_SHIP, // 待发货
SHIPPED, // 已发货
DELIVERED, // 已签收
COMPLETED, // 已完成
CANCELLED // 已取消
}
public enum OrderEvent {
PAY, // 支付
SHIP, // 发货
DELIVER, // 签收
COMPLETE, // 完成
CANCEL // 取消
}
第二步,配置状态机。
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderState, OrderEvent> {
@Override
public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> states) throws Exception {
states
.withStates()
.initial(OrderState.PENDING_PAYMENT)
.states(EnumSet.allOf(OrderState.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions) throws Exception {
transitions
// 待支付 -> 待发货
.withExternal()
.source(OrderState.PENDING_PAYMENT)
.target(OrderState.PENDING_SHIP)
.event(OrderEvent.PAY)
.guard(paymentGuard())
.action(freezeStockAction())
.and()
// 待发货 -> 已发货
.withExternal()
.source(OrderState.PENDING_SHIP)
.target(OrderState.SHIPPED)
.event(OrderEvent.SHIP)
.guard(stockGuard())
.action(outboundAction())
.and()
// 已发货 -> 已签收
.withExternal()
.source(OrderState.SHIPPED)
.target(OrderState.DELIVERED)
.event(OrderEvent.DELIVER)
.action(deliverAction());
}
}
第三步,实现 Guard 和 Action。
@Component
public class PaymentGuard implements Guard<OrderState, OrderEvent> {
@Override
public boolean evaluate(StateContext<OrderState, OrderEvent> context) {
Long orderId = context.getMessage().getHeaders().get("orderId", Long.class);
// 检查订单是否已支付
return paymentService.isPaid(orderId);
}
}
@Component
public class FreezeStockAction implements Action<OrderState, OrderEvent> {
@Override
public void execute(StateContext<OrderState, OrderEvent> context) {
Long orderId = context.getMessage().getHeaders().get("orderId", Long.class);
// 冻结库存
warehouseService.freezeStock(orderId);
}
}
第四步,触发状态转换。
public void payOrder(Long orderId) {
Message<OrderEvent> message = MessageBuilder
.withPayload(OrderEvent.PAY)
.setHeader("orderId", orderId)
.build();
stateMachine.sendEvent(message);
}
效果
- 代码可维护性:状态转换规则集中管理,代码清晰,易于维护
- 业务逻辑解耦:Guard 和 Action 独立实现,职责单一,易于测试
- 状态转换安全:Guard 保证状态转换的前置条件,防止非法转换
2.6 订单风险控制规则引擎
问题背景
跨境电商订单面临多种风险:
第一,欺诈订单。 使用盗刷信用卡下单,商家发货后买家拒付,商家损失货款和货物。
第二,恶意刷单。 竞争对手恶意下单后取消,占用库存,影响正常销售。
第三,地址异常。 收货地址不完整或不存在,导致物流无法配送,增加退货成本。
第四,金额异常。 订单金额远超正常范围,可能是系统漏洞或恶意攻击。
第五,高频下单。 同一用户短时间内下单多次,可能是黄牛或恶意用户。
如果不做风险控制,会导致:
- 欺诈订单造成直接经济损失
- 恶意订单占用库存,影响正常销售
- 异常订单增加客服和物流成本
解决方案
我们设计了 订单风险控制规则引擎,使用策略模式实现多种风险规则。
五大风险规则:
- 地址有效性规则:检查收货地址是否完整、是否在配送范围内 只限于: 国家
- 金额异常规则:检查订单金额是否超过正常范围(判断订单单价金额 和 SKU的均价 对比)
- 高频下单规则:检查同一用户 1 小时内对于同一个SKU下单次数是否超过 5 次
- 黑名单规则:检查用户是否在黑名单中(历史欺诈用户)
- 设备指纹规则:检查设备指纹是否异常(同一设备多账号下单) 如果是开启 活动 的时候 可以进行判断 平时正常的订单可以不用判断
风险等级:
- 低风险:通过所有规则,自动审核通过
- 中风险:触发 1-2 条规则,使用站内信通知运营人员
- 高风险:触发 3+ 条规则,使用 短信/办公软件 通知运营人员
技术实现
第一步,定义规则接口。
public interface RiskRule {
/**
* 评估订单风险
* @return 风险分数(0-100,越高越危险)
*/
int evaluate(OrderMain order);
/**
* 规则名称
*/
String getRuleName();
}
第二步,实现具体规则。
@Component
public class AddressValidityRule implements RiskRule {
@Override
public int evaluate(OrderMain order) {
String address = order.getShippingAddress();
// 地址为空或长度 < 10
if (StringUtils.isBlank(address) || address.length() < 10) {
return 50;
}
// 地址不包含门牌号
if (!address.matches(".*\\d+.*")) {
return 30;
}
return 0;
}
@Override
public String getRuleName() {
return "地址有效性规则";
}
}
@Component
public class AmountAnomalyRule implements RiskRule {
@Override
public int evaluate(OrderMain order) {
BigDecimal amount = order.getTotalAmount();
// 订单金额 > 10000 美元
// 如果订单单价 和 采购的均价 对比 低于 30% 50分 低于50% 80 低于 70% 90分
if (amount.compareTo(new BigDecimal("10000")) > 0) {
return 80;
}
// 订单金额 > 5000 美元
if (amount.compareTo(new BigDecimal("5000")) > 0) {
return 40;
}
return 0;
}
@Override
public String getRuleName() {
return "金额异常规则";
}
}
@Component
public class HighFrequencyRule implements RiskRule {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public int evaluate(OrderMain order) {
String key = "order:frequency:" + order.getUserId();
Long count = redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, 1, TimeUnit.HOURS);
// 1 小时内下单 > 10 次
if (count > 10) {
return 90;
}
// 1 小时内下单 > 5 次
if (count > 5) {
return 50;
}
return 0;
}
@Override
public String getRuleName() {
return "高频下单规则";
}
}
第三步,规则引擎执行。
@Service
public class RiskControlService {
// 如果List<接口类型> Spring boot 在进行注入的时候 会把这个Spring Boot容器中所有的该接口的实现类 都自动帮你注入到这个集合中
@Autowired
private List<RiskRule> riskRules;
public RiskResult evaluateRisk(OrderMain order) {
int totalScore = 0;
List<String> triggeredRules = new ArrayList<>();
for (RiskRule rule : riskRules) {
int score = rule.evaluate(order);
if (score > 0) {
totalScore += score;
triggeredRules.add(rule.getRuleName() + "(" + score + "分)");
}
}
RiskLevel level;
if (totalScore >= 100) {
level = RiskLevel.HIGH;
} else if (totalScore >= 50) {
level = RiskLevel.MEDIUM;
} else {
level = RiskLevel.LOW;
}
return new RiskResult(level, totalScore, triggeredRules);
}
}
第四步,订单创建时执行风险控制。
@Transactional(rollbackFor = Exception.class)
public OrderMain createOrder(CreateOrderRequest request) {
// 创建订单
OrderMain order = buildOrder(request);
orderMapper.insert(order);
// 风险控制
RiskResult riskResult = riskControlService.evaluateRisk(order);
order.setRiskLevel(riskResult.getLevel());
order.setRiskScore(riskResult.getScore());
// 高风险订单自动拒绝
if (riskResult.getLevel() == RiskLevel.HIGH) {
order.setStatus(OrderStatus.REJECTED);
order.setRejectReason("触发风险规则:" + String.join(", ", riskResult.getTriggeredRules()));
}
// 中风险订单人工审核
else if (riskResult.getLevel() == RiskLevel.MEDIUM) {
order.setStatus(OrderStatus.PENDING_REVIEW);
}
// 低风险订单自动通过
else {
order.setStatus(OrderStatus.PENDING_PAYMENT);
}
orderMapper.updateById(order);
return order;
}
效果
- 欺诈订单拦截率:拦截欺诈订单 95%+,减少损失 100 万+/年
- 规则扩展性:新增规则只需实现 RiskRule 接口,无需修改引擎代码
- 性能优化:规则并行执行,风险评估耗时 < 50ms
规则: 有哪些规则校验 规则校验的实现过程
2.7 订单超时预警系统
问题背景
订单处理有严格的时效要求:
第一,待发货订单超时。 订单支付后 72 小时未发货,影响客户体验,可能被平台处罚。
第二,已发货订单超时。 订单发货后 30 天未签收,可能是物流丢件或地址错误,需要人工介入。
如果不做超时预警,会导致:
- 库存长期被冻结,影响销售
- 客户投诉增加,影响店铺评分
- 平台处罚,影响店铺权重
解决方案
我们设计了 订单超时预警系统,使用 XXL-Job 定时任务 + 三级预警机制。
三级预警机制:
- 一级预警(提前 2 小时):发送站内信 + 邮件提醒商家
- 二级预警(提前 30 分钟):发送短信 + 钉钉消息提醒商家
- 三级预警(超时):自动执行超时处理(取消订单、标记异常等)
技术实现
第一步,配置定时任务。
使用 XXL-Job 配置定时任务,每 10 分钟执行一次。
第二步,查询超时订单。
@XxlJob("orderTimeoutWarningJob")
public void execute() {
// 查询待支付超时订单(创建时间 > 30 分钟)
List<OrderMain> pendingPaymentOrders = orderMapper.selectList(
new LambdaQueryWrapper<OrderMain>()
.eq(OrderMain::getStatus, OrderStatus.PENDING_PAYMENT)
.lt(OrderMain::getCreateTime, LocalDateTime.now().minusMinutes(30))
);
for (OrderMain order : pendingPaymentOrders) {
handlePendingPaymentTimeout(order);
}
// 查询待发货超时订单(支付时间 > 72 小时)
List<OrderMain> pendingShipOrders = orderMapper.selectList(
new LambdaQueryWrapper<OrderMain>()
.eq(OrderMain::getStatus, OrderStatus.PENDING_SHIP)
.lt(OrderMain::getPayTime, LocalDateTime.now().minusHours(72))
);
for (OrderMain order : pendingShipOrders) {
handlePendingShipTimeout(order);
}
}
第三步,执行超时处理。
private void handlePendingPaymentTimeout(OrderMain order) {
// 自动取消订单
order.setStatus(OrderStatus.CANCELLED);
order.setCancelReason("超时未支付");
orderMapper.updateById(order);
// 释放冻结库存
warehouseService.unfreezeStock(order.getId());
// 发送通知
notificationService.send(order.getUserId(), "订单已取消", "订单超时未支付,已自动取消");
}
private void handlePendingShipTimeout(OrderMain order) {
LocalDateTime payTime = order.getPayTime();
LocalDateTime now = LocalDateTime.now();
long hours = ChronoUnit.HOURS.between(payTime, now);
// 一级预警(70 小时,提前 2 小时)
if (hours >= 70 && hours < 71) {
notificationService.sendEmail(order.getMerchantId(), "订单即将超时", "订单 " + order.getOrderNo() + " 将在 2 小时后超时");
}
// 二级预警(71.5 小时,提前 30 分钟)
else if (hours >= 71.5 && hours < 72) {
notificationService.sendSms(order.getMerchantId(), "订单即将超时,请尽快发货");
notificationService.sendDingTalk(order.getMerchantId(), "订单 " + order.getOrderNo() + " 将在 30 分钟后超时");
}
// 三级预警(72 小时,超时)
else if (hours >= 72) {
order.setStatus(OrderStatus.TIMEOUT);
orderMapper.updateById(order);
notificationService.sendSms(order.getMerchantId(), "订单已超时,请尽快处理");
}
}
效果
- 超时订单自动处理:未支付订单自动取消,释放库存,库存周转率提升 20%
- 预警及时性:三级预警机制,商家响应时间从 4 小时降到 30 分钟
- 客户满意度提升:发货及时率从 85% 提升到 95%,客户投诉减少 50%
2.8 订单拆分与合并策略
问题背景
跨境电商订单经常需要拆分或合并:
第一,跨仓库发货。 用户下单购买 3 件商品,但这 3 件商品分别在国内仓、FBA 仓、海外仓,需要拆分成 3 个子订单分别发货。
第二,物流限制。 某些国家禁止某些商品一起发货(如电池和化妆品),需要拆分成多个包裹。
第三,超重限制。 订单总重量超过物流限制(如 30kg),需要拆分成多个包裹。
第四,平台要求。 Amazon FBA 要求每个包裹只能包含一个 SKU,需要拆分订单。
第五,合并发货。 同一用户在 1 小时内下了 3 个订单,可以合并成 1 个包裹发货,节省物流成本。
解决方案
我们设计了 订单拆分与合并策略。
四种拆分规则:
- 跨仓库拆分:按商品所在仓库拆分,每个仓库生成一个子订单
- 物流限制拆分:按物流规则拆分,禁止一起发货的商品分开
- 超重拆分:按重量拆分,每个包裹不超过 30kg
- 平台要求拆分:按平台规则拆分,如 FBA 要求每个包裹只能包含一个 SKU
合并规则:
- 同一用户、同一收货地址、1 小时内下单、总重量 < 30kg,可以合并发货
技术实现
第一步,判断是否需要拆分。
public boolean needSplit(OrderMain order) {
List<OrderItem> items = order.getItems();
// 检查是否跨仓库
Set<Long> warehouseIds = items.stream()
.map(OrderItem::getWarehouseId)
.collect(Collectors.toSet());
if (warehouseIds.size() > 1) {
return true;
}
// 检查是否超重
BigDecimal totalWeight = items.stream()
.map(item -> item.getWeight().multiply(new BigDecimal(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (totalWeight.compareTo(new BigDecimal("30")) > 0) {
return true;
}
// 检查物流限制
if (hasLogisticsRestriction(items)) {
return true;
}
return false;
}
第二步,执行拆分。
我们创建不同的运单 TMS 订单
但是这多个运单一定要关联同一个 OMS 订单
@Transactional(rollbackFor = Exception.class)
public List<OrderMain> splitOrder(OrderMain order) {
List<OrderMain> subOrders = new ArrayList<>();
// 按仓库分组
Map<Long, List<OrderItem>> warehouseGroups = order.getItems().stream()
.collect(Collectors.groupingBy(OrderItem::getWarehouseId));
for (Map.Entry<Long, List<OrderItem>> entry : warehouseGroups.entrySet()) {
Long warehouseId = entry.getKey();
List<OrderItem> items = entry.getValue();
// 创建子订单
OrderMain subOrder = new OrderMain();
subOrder.setOrderNo(generateSubOrderNo(order.getOrderNo()));
subOrder.setParentOrderNo(order.getOrderNo());
subOrder.setWarehouseId(warehouseId);
subOrder.setStatus(OrderStatus.PENDING_SHIP);
subOrder.setItems(items);
// 计算子订单金额
BigDecimal subTotal = items.stream()
.map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
subOrder.setTotalAmount(subTotal);
orderMapper.insert(subOrder);
subOrders.add(subOrder);
}
// 更新主订单状态
order.setStatus(OrderStatus.SPLIT);
order.setSplitCount(subOrders.size());
orderMapper.updateById(order);
return subOrders;
}
第三步,判断是否可以合并。
public boolean canMerge(List<OrderMain> orders) {
if (orders.size() < 2) {
return false;
}
// 检查是否同一用户
Long userId = orders.get(0).getUserId();
if (!orders.stream().allMatch(o -> o.getUserId().equals(userId))) {
return false;
}
// 检查是否同一收货地址
String address = orders.get(0).getShippingAddress();
if (!orders.stream().allMatch(o -> o.getShippingAddress().equals(address))) {
return false;
}
// 检查是否 1 小时内下单
LocalDateTime firstOrderTime = orders.get(0).getCreateTime();
LocalDateTime lastOrderTime = orders.get(orders.size() - 1).getCreateTime();
if (ChronoUnit.HOURS.between(firstOrderTime, lastOrderTime) > 1) {
return false;
}
// 检查总重量是否 < 30kg
BigDecimal totalWeight = orders.stream()
.flatMap(o -> o.getItems().stream())
.map(item -> item.getWeight().multiply(new BigDecimal(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (totalWeight.compareTo(new BigDecimal("30")) > 0) {
return false;
}
return true;
}
第四步,执行合并。
多个OMS订单 对应 同一个TMS运单
@Transactional(rollbackFor = Exception.class)
public OrderMain mergeOrders(List<OrderMain> orders) {
// 创建合并订单
OrderMain mergedOrder = new OrderMain();
mergedOrder.setOrderNo(generateOrderNo());
mergedOrder.setUserId(orders.get(0).getUserId());
mergedOrder.setShippingAddress(orders.get(0).getShippingAddress());
mergedOrder.setStatus(OrderStatus.PENDING_SHIP);
// 合并订单明细
List<OrderItem> allItems = orders.stream()
.flatMap(o -> o.getItems().stream())
.collect(Collectors.toList());
mergedOrder.setItems(allItems);
// 计算合并订单金额
BigDecimal totalAmount = orders.stream()
.map(OrderMain::getTotalAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
mergedOrder.setTotalAmount(totalAmount);
orderMapper.insert(mergedOrder);
// 更新原订单状态
for (OrderMain order : orders) {
order.setStatus(OrderStatus.MERGED);
order.setMergedOrderNo(mergedOrder.getOrderNo());
orderMapper.updateById(order);
}
return mergedOrder;
}
效果
- 拆分准确率:自动拆分准确率 100%,无需人工干预
- 物流成本降低:合并发货减少包裹数量 30%,物流成本降低 25%
- 发货效率提升:拆分后每个仓库独立发货,发货时间从 48 小时降到 24 小时
2.9 分布式事务 Seata AT 模式
问题背景
订单创建涉及多个系统的数据操作:
第一,OMS 创建订单。 在 order_main 表插入订单记录。
第二,WMS 冻结库存。 在 inventory 表更新 frozen_qty。
第三,PMS 更新商品销量。 在 product_spu 表更新 sales_count。
这三个操作必须保证原子性:要么全部成功,要么全部失败。如果 OMS 创建订单成功,但 WMS 冻结库存失败,就会出现”有订单但没库存”的情况,导致无法发货。
传统的本地事务只能保证单个数据库的原子性,无法保证跨系统的原子性。
解决方案
我们使用 Seata AT 模式 实现分布式事务。
Seata AT 模式原理:
- 一阶段:执行业务 SQL,Seata 自动记录 undo_log(回滚日志)
- 每个子事务都提交了 不会有数据库的锁占用
- 二阶段提交:删除 undo_log,释放全局锁
- 二阶段回滚:根据 undo_log 自动回滚数据
核心优势:
- 无侵入:业务代码无需修改,只需加
@GlobalTransactional注解 - 高性能:一阶段直接提交本地事务,不阻塞业务
- 自动回滚:二阶段回滚时自动生成反向 SQL,无需手动编写补偿逻辑
技术实现
第一步,引入 Seata 依赖。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
第二步,配置 Seata。
seata:
enabled: true
application-id: oms-service
tx-service-group: my_tx_group
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: seata
group: SEATA_GROUP
第三步,创建 undo_log 表。
CREATE TABLE undo_log (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
branch_id BIGINT(20) NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第四步,使用 @GlobalTransactional 注解。
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private WarehouseFeignClient warehouseFeignClient;
@Autowired
private ProductFeignClient productFeignClient;
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public OrderMain createOrder(CreateOrderRequest request) {
// 1. 创建订单
OrderMain order = new OrderMain();
order.setOrderNo(generateOrderNo());
order.setStatus(OrderStatus.PENDING_PAYMENT);
orderMapper.insert(order);
// 2. 冻结库存(远程调用 WMS)
FreezeStockRequest freezeRequest = new FreezeStockRequest();
freezeRequest.setSkuId(request.getSkuId());
freezeRequest.setQuantity(request.getQuantity());
Result<Void> freezeResult = warehouseFeignClient.freezeStock(freezeRequest);
if (!freezeResult.isSuccess()) {
throw new BusinessException("库存不足");
}
// 3. 更新商品销量(远程调用 PMS)
UpdateSalesRequest salesRequest = new UpdateSalesRequest();
salesRequest.setSpuId(request.getSpuId());
salesRequest.setQuantity(request.getQuantity());
Result<Void> salesResult = productFeignClient.updateSales(salesRequest);
if (!salesResult.isSuccess()) {
throw new BusinessException("更新销量失败");
}
return order;
}
}
第五步,WMS 和 PMS 的本地事务方法。
// WMS 冻结库存
@Transactional(rollbackFor = Exception.class)
public void freezeStock(FreezeStockRequest request) {
int updated = inventoryMapper.update(null,
new LambdaUpdateWrapper<Inventory>()
.eq(Inventory::getSkuId, request.getSkuId())
.apply("quantity - frozen_qty >= {0}", request.getQuantity())
.setSql("frozen_qty = frozen_qty + " + request.getQuantity())
);
if (updated == 0) {
throw new BusinessException("库存不足");
}
}
// PMS 更新销量
@Transactional(rollbackFor = Exception.class)
public void updateSales(UpdateSalesRequest request) {
productMapper.update(null,
new LambdaUpdateWrapper<ProductSpu>()
.eq(ProductSpu::getId, request.getSpuId())
.setSql("sales_count = sales_count + " + request.getQuantity())
);
}
执行流程:
- OMS 调用
createOrder(),Seata 开启全局事务,生成全局事务 ID(XID) - OMS 插入订单,Seata 记录 undo_log,提交本地事务
- OMS 调用 WMS
freezeStock(),Seata 传递 XID - WMS 更新库存,Seata 记录 undo_log,提交本地事务
- OMS 调用 PMS
updateSales(),Seata 传递 XID - PMS 更新销量,Seata 记录 undo_log,提交本地事务
- 所有操作成功,Seata 二阶段提交,删除所有 undo_log
- 如果任何一步失败,Seata 二阶段回滚,根据 undo_log 自动回滚所有数据
效果
- 数据一致性:订单和库存强一致性,一致性保证 100%
- 性能优化:一阶段直接提交,不阻塞业务,TPS 5000+
- 开发效率:无需手动编写补偿逻辑,开发效率提升 80%
面试追问:什么时候用 Seata,什么时候用 RocketMQ?
使用 Seata(强一致性)的场景:
- 业务要求强一致性:订单创建 + 库存冻结,必须同时成功或同时失败
- 用户体验要求实时反馈:用户下单后立即知道是否成功,不能延迟
- 数据关联性强:多个操作之间有强依赖关系,一个失败其他必须回滚
使用 RocketMQ(最终一致性)的场景:
- 业务允许短暂延迟:订单状态通知、运单创建,延迟几秒钟可以接受
- 解耦服务:上游不关心下游的执行结果,只需要通知到位
- 削峰填谷:高并发场景下,用 MQ 缓冲流量
本项目的实际应用:
- 订单创建 → 冻结库存:用 Seata(必须强一致,用户需要实时反馈)
- 订单发货 → 创建运单:用 RocketMQ(允许延迟,解耦 OMS 和 TMS)
- 订单完成 → 更新商品销量:用 Seata(保证销量统计准确)
2.10 库存分配策略
问题背景
跨境电商商家在多个平台开店,但库存是共享的。如何分配库存到各个平台,避免超卖?
第一,库存共享导致超卖。 仓库有 100 件货,Amazon 和 Shopify 都配置 100 件,两个平台同时卖出 60 件,总共卖了 120 件,超卖 20 件。
第二,库存分配不合理。 Amazon 销量好,Shopify 销量差,如果平均分配库存,会导致 Amazon 缺货,Shopify 积压。
第三,库存同步延迟。 库存变动后,需要同步到各个平台,如果同步延迟,可能导致超卖。
解决方案
我们设计了 库存分配策略。
两种分配模式:
- 固定分配:商家手动配置每个平台的库存数量,如 Amazon 50 件、Shopify 30 件、eBay 20 件
- 动态分配:系统根据各平台的销量占比,自动分配库存
- SaaS 可以获取到每个平台的库存数据 如果拿不到平台的库存数据 只能根据往期的订单数据进行分析 然后给商家一个建议
库存同步机制:
- 推送模式:库存变动后,主动推送到各平台 如果电商平台支持推送 优先推荐使用推送 webhook
- 拉取模式:平台定期查询供应链系统的库存
技术实现
第一步,商家配置库存分配。
public class InventoryAllocation {
private Long skuId;
private Long warehouseId;
private String platform; // Amazon, Shopify, eBay
private Integer allocatedQty; // 分配数量
private AllocationMode mode; // FIXED(固定), DYNAMIC(动态)
}
第二步,计算可售库存。
public Integer getAvailableQty(Long skuId, Long warehouseId, String platform) {
// 查询库存
Inventory inventory = inventoryMapper.selectOne(
new LambdaQueryWrapper<Inventory>()
.eq(Inventory::getSkuId, skuId)
.eq(Inventory::getWarehouseId, warehouseId)
);
// 查询分配配置
InventoryAllocation allocation = allocationMapper.selectOne(
new LambdaQueryWrapper<InventoryAllocation>()
.eq(InventoryAllocation::getSkuId, skuId)
.eq(InventoryAllocation::getWarehouseId, warehouseId)
.eq(InventoryAllocation::getPlatform, platform)
);
if (allocation == null) {
return 0;
}
// 固定分配模式
if (allocation.getMode() == AllocationMode.FIXED) {
return allocation.getAllocatedQty();
}
// 动态分配模式
// 查询所有平台的销量占比
Map<String, Integer> salesRatio = getSalesRatio(skuId);
Integer ratio = salesRatio.getOrDefault(platform, 0);
// 可售库存 = 实物库存 - 冻结库存 - 不良品库存
Integer availableQty = inventory.getQuantity()
- inventory.getFrozenQty()
- inventory.getDefectiveQty();
// 按销量占比分配
return availableQty * ratio / 100;
}
第三步,库存变动后同步到平台。
@Async
public void syncInventoryToPlatform(Long skuId, Long warehouseId) {
// 查询所有平台的分配配置
List<InventoryAllocation> allocations = allocationMapper.selectList(
new LambdaQueryWrapper<InventoryAllocation>()
.eq(InventoryAllocation::getSkuId, skuId)
.eq(InventoryAllocation::getWarehouseId, warehouseId)
);
for (InventoryAllocation allocation : allocations) {
String platform = allocation.getPlatform();
Integer availableQty = getAvailableQty(skuId, warehouseId, platform);
// 调用平台 API 更新库存
if ("Amazon".equals(platform)) {
amazonApiClient.updateInventory(skuId, availableQty);
} else if ("Shopify".equals(platform)) {
shopifyApiClient.updateInventory(skuId, availableQty);
} else if ("eBay".equals(platform)) {
ebayApiClient.updateInventory(skuId, availableQty);
}
}
}
效果
- 超卖率降低:从 5% 降到 0.1%,超卖订单减少 98%
- 库存周转率提升:动态分配模式下,库存周转率提升 30%
- 同步及时性:推送模式下,库存同步延迟从 15 分钟降到 10 秒
我们的库存总数是固定的 那么给每个电商平台的每个店铺 进行库存分配的时候 要保证 不能超过库存总数
但是给每个店铺分配多少库存 ??
- 让租户手动去填写 但是我们要给出填写的具体的数量
- SaaS 系统自动分配 自动推送到各个电商平台(最推荐的做法)
- 可以自动配置库存到各个电商平台的各个店铺 不需要租户做任何的操作
- 电商平台的库存 可以 根据实际的库存 动态去调整 不会出现超卖
- 可以把卖的慢的店铺的库存 随时 转移到 卖的快的店铺的库存上
三、数据库设计
3.1 商品表设计
主键策略说明:
所有业务表统一使用 MyBatis-Plus 的 ASSIGN_ID 策略(雪花算法),而不是数据库的 AUTO_INCREMENT。
原因:
- 分布式友好: 雪花算法生成的 ID 全局唯一,多个服务并发插入不会冲突
- 支持分库分表: AUTO_INCREMENT 在分库分表场景下会产生重复 ID
- 性能更好: 不依赖数据库自增,减少数据库压力
- 趋势递增: 雪花算法生成的 ID 趋势递增,对 B+ 树索引友好
实体类定义:
@TableName("product_spu")
public class ProductSpu {
@TableId(type = IdType.ASSIGN_ID) // 使用雪花算法
private Long id;
// 其他字段...
}
product_spu(SPU 表)
CREATE TABLE product_spu (
id BIGINT PRIMARY KEY COMMENT 'SPU ID(雪花算法生成)',
spu_no VARCHAR(50) NOT NULL COMMENT 'SPU 编号',
spu_name VARCHAR(200) NOT NULL COMMENT 'SPU 名称',
brand_id BIGINT COMMENT '品牌 ID',
category_id BIGINT COMMENT '类目 ID',
hs_code VARCHAR(20) COMMENT 'HS 编码',
material VARCHAR(100) COMMENT '材质',
origin_country VARCHAR(50) COMMENT '产地',
description TEXT COMMENT '商品描述',
sales_count INT DEFAULT 0 COMMENT '销量',
status TINYINT DEFAULT 1 COMMENT '状态:1-上架,2-下架',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_spu_no(spu_no),
KEY idx_brand_id(brand_id),
KEY idx_category_id(category_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SPU 表';
product_sku(SKU 表)
CREATE TABLE product_sku (
id BIGINT PRIMARY KEY COMMENT 'SKU ID(雪花算法生成)',
sku_no VARCHAR(50) NOT NULL COMMENT 'SKU 编号',
spu_id BIGINT NOT NULL COMMENT 'SPU ID',
sku_name VARCHAR(200) NOT NULL COMMENT 'SKU 名称',
barcode VARCHAR(50) COMMENT '条形码',
spec_json JSON COMMENT '规格属性 JSON',
price DECIMAL(10,2) NOT NULL COMMENT '价格',
cost DECIMAL(10,2) COMMENT '成本',
weight DECIMAL(10,2) COMMENT '重量(kg)',
length DECIMAL(10,2) COMMENT '长度(cm)',
width DECIMAL(10,2) COMMENT '宽度(cm)',
height DECIMAL(10,2) COMMENT '高度(cm)',
image_url VARCHAR(500) COMMENT 'SKU 图片',
status TINYINT DEFAULT 1 COMMENT '状态:1-上架,2-下架',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_sku_no(sku_no),
KEY idx_spu_id(spu_id),
KEY idx_barcode(barcode)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SKU 表';
product_spec(规格表)
CREATE TABLE product_spec (
id BIGINT PRIMARY KEY COMMENT '规格 ID(雪花算法生成)',
spu_id BIGINT NOT NULL COMMENT 'SPU ID',
spec_name VARCHAR(50) NOT NULL COMMENT '规格名称(如:颜色、尺码)',
spec_values JSON NOT NULL COMMENT '规格值列表(如:["红色","蓝色","绿色"])',
sort_order INT DEFAULT 0 COMMENT '排序',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_spu_id(spu_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规格表';
product_i18n(多语言表)
CREATE TABLE product_i18n (
id BIGINT PRIMARY KEY COMMENT 'ID(雪花算法生成)',
spu_id BIGINT NOT NULL COMMENT 'SPU ID',
language_code VARCHAR(10) NOT NULL COMMENT '语言代码(如:en-US, zh-CN)',
title VARCHAR(500) NOT NULL COMMENT '商品标题',
description TEXT COMMENT '商品描述',
keywords VARCHAR(500) COMMENT '关键词',
is_ai_translated TINYINT DEFAULT 0 COMMENT '是否 AI 翻译:0-否,1-是',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_spu_language(spu_id, language_code),
KEY idx_language_code(language_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品多语言表';
product_price(多平台定价表)
CREATE TABLE product_price (
id BIGINT PRIMARY KEY COMMENT 'ID',
sku_id BIGINT NOT NULL COMMENT 'SKU ID',
platform VARCHAR(50) NOT NULL COMMENT '平台(Amazon, Shopify, eBay)',
country_code VARCHAR(10) NOT NULL COMMENT '国家代码(US, UK, DE)',
currency VARCHAR(10) NOT NULL COMMENT '币种(USD, GBP, EUR)',
price DECIMAL(10,2) NOT NULL COMMENT '售价',
tax_rate DECIMAL(5,2) DEFAULT 0 COMMENT '税率(%)',
tax_amount DECIMAL(10,2) DEFAULT 0 COMMENT '税额',
final_price DECIMAL(10,2) NOT NULL COMMENT '最终价格(含税)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_sku_platform_country(sku_id, platform, country_code),
KEY idx_platform(platform)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='多平台定价表';
3.2 订单表设计
order_main(订单主表)
CREATE TABLE order_main (
id BIGINT PRIMARY KEY COMMENT '订单 ID',
order_no VARCHAR(50) NOT NULL COMMENT '订单号',
parent_order_no VARCHAR(50) COMMENT '父订单号(拆单时使用)',
platform VARCHAR(50) NOT NULL COMMENT '平台(Amazon, Shopify, eBay)',
platform_order_no VARCHAR(100) NOT NULL COMMENT '平台订单号',
user_id BIGINT NOT NULL COMMENT '用户 ID',
merchant_id BIGINT NOT NULL COMMENT '商家 ID',
warehouse_id BIGINT COMMENT '仓库 ID',
-- 收货信息
receiver_name VARCHAR(100) NOT NULL COMMENT '收货人',
receiver_phone VARCHAR(50) NOT NULL COMMENT '收货电话',
receiver_email VARCHAR(100) COMMENT '收货邮箱',
shipping_address VARCHAR(500) NOT NULL COMMENT '收货地址',
country_code VARCHAR(10) NOT NULL COMMENT '国家代码',
-- 金额信息
currency VARCHAR(10) NOT NULL COMMENT '币种',
goods_amount DECIMAL(10,2) NOT NULL COMMENT '商品金额',
shipping_fee DECIMAL(10,2) DEFAULT 0 COMMENT '运费',
tax_amount DECIMAL(10,2) DEFAULT 0 COMMENT '税费',
discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总额',
-- 状态信息
status TINYINT NOT NULL COMMENT '订单状态:1-待支付,2-待发货,3-已发货,4-已签收,5-已完成,6-已取消',
pay_status TINYINT DEFAULT 0 COMMENT '支付状态:0-未支付,1-已支付',
ship_status TINYINT DEFAULT 0 COMMENT '发货状态:0-未发货,1-已发货',
-- 风险控制
risk_level VARCHAR(20) COMMENT '风险等级:LOW, MEDIUM, HIGH',
risk_score INT DEFAULT 0 COMMENT '风险分数',
-- 时间信息
pay_time DATETIME COMMENT '支付时间',
ship_time DATETIME COMMENT '发货时间',
deliver_time DATETIME COMMENT '签收时间',
complete_time DATETIME COMMENT '完成时间',
cancel_time DATETIME COMMENT '取消时间',
cancel_reason VARCHAR(200) COMMENT '取消原因',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_order_no(order_no),
UNIQUE KEY uk_platform_order_no(platform, platform_order_no),
KEY idx_user_id(user_id),
KEY idx_merchant_id(merchant_id),
KEY idx_status(status),
KEY idx_create_time(create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';
分表策略: 按月分表,表名格式 order_main_202601、order_main_202602。
order_item(订单明细表)
CREATE TABLE order_item (
id BIGINT PRIMARY KEY COMMENT '明细 ID',
order_id BIGINT NOT NULL COMMENT '订单 ID',
order_no VARCHAR(50) NOT NULL COMMENT '订单号',
sku_id BIGINT NOT NULL COMMENT 'SKU ID',
sku_no VARCHAR(50) NOT NULL COMMENT 'SKU 编号',
sku_name VARCHAR(200) NOT NULL COMMENT 'SKU 名称',
sku_image VARCHAR(500) COMMENT 'SKU 图片',
spec_json JSON COMMENT '规格属性 JSON',
price DECIMAL(10,2) NOT NULL COMMENT '单价',
quantity INT NOT NULL COMMENT '数量',
total_amount DECIMAL(10,2) NOT NULL COMMENT '小计',
warehouse_id BIGINT COMMENT '仓库 ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_order_id(order_id),
KEY idx_order_no(order_no),
KEY idx_sku_id(sku_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
order_log(订单日志表)
CREATE TABLE order_log (
id BIGINT PRIMARY KEY COMMENT '日志 ID',
order_id BIGINT NOT NULL COMMENT '订单 ID',
order_no VARCHAR(50) NOT NULL COMMENT '订单号',
operate_type VARCHAR(50) NOT NULL COMMENT '操作类型',
before_status TINYINT COMMENT '变更前状态',
after_status TINYINT COMMENT '变更后状态',
remark VARCHAR(500) COMMENT '备注',
operator_id BIGINT COMMENT '操作人 ID',
operator_name VARCHAR(100) COMMENT '操作人姓名',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_order_id(order_id),
KEY idx_order_no(order_no),
KEY idx_create_time(create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单日志表';
四、核心业务流程
4.1 商品创建流程(SPU/SKU 自动生成)
业务场景
商家要上架一款 T恤,有 3 种颜色(红、蓝、绿)× 3 种尺码(S、M、L)= 9 个 SKU。
完整流程
第一步,商家创建 SPU。
商家在后台填写 SPU 信息:
- SPU 名称:纯棉圆领 T恤
- 品牌:Nike
- 类目:服装 > T恤
- HS 编码:6109100021
- 材质:100% 纯棉
- 产地:中国
- 商品描述:舒适透气,适合日常穿着
技术上,系统插入 product_spu 表,生成 SPU ID。
第二步,商家配置规格选项。
商家选择规格类型并填写规格值:
- 规格 1:颜色 → 红色、蓝色、绿色
- 规格 2:尺码 → S、M、L
技术上,系统插入 product_spec 表,每个规格一条记录。
第三步,系统自动生成 SKU。
系统使用笛卡尔积算法,生成所有规格组合:
- SKU 1:红色-S
- SKU 2:红色-M
- SKU 3:红色-L
- SKU 4:蓝色-S
- SKU 5:蓝色-M
- SKU 6:蓝色-L
- SKU 7:绿色-S
- SKU 8:绿色-M
- SKU 9:绿色-L
技术上,系统调用笛卡尔积算法,生成 SKU 列表,插入 product_sku 表。每个 SKU 的 spec_json 字段存储规格属性:
{
"颜色": "红色",
"尺码": "S"
}
第四步,商家补充 SKU 信息。
商家为每个 SKU 填写:
- 价格:29.99 美元
- 成本:10.00 美元
- 重量:0.2 kg
- 条形码:123456789012
- SKU 图片:https://cdn.example.com/red-s.jpg
技术上,系统更新 product_sku 表。
第五步,商家配置多语言内容。
商家填写中文标题和描述,系统自动翻译成英语、西班牙语、法语等 15+ 语言。
技术上,系统调用 DeepSeek API,传入中文内容和目标语言,返回翻译结果,插入 product_i18n 表。
第六步,商家配置多平台定价。
商家为不同平台和国家配置价格:
- Amazon 美国:29.99 USD(税率 10%,最终价格 32.99 USD)
- Shopify 英国:24.99 GBP(税率 20%,最终价格 29.99 GBP)
- eBay 德国:27.99 EUR(税率 19%,最终价格 33.31 EUR)
技术上,系统插入 product_price 表,自动计算税额和最终价格。
第七步,商家发布商品。
商家点击”发布”按钮,系统同步商品到各个平台。
技术上,系统调用平台 API,传入商品信息、多语言内容、定价信息。
关键代码
@Transactional(rollbackFor = Exception.class)
public ProductSpu createProduct(CreateProductRequest request) {
// 1. 创建 SPU
ProductSpu spu = new ProductSpu();
spu.setSpuNo(generateSpuNo());
spu.setSpuName(request.getSpuName());
spu.setBrandId(request.getBrandId());
spu.setCategoryId(request.getCategoryId());
spu.setHsCode(request.getHsCode());
spu.setMaterial(request.getMaterial());
spu.setOriginCountry(request.getOriginCountry());
spu.setDescription(request.getDescription());
spuMapper.insert(spu);
// 2. 保存规格选项
for (SpecOption specOption : request.getSpecOptions()) {
ProductSpec spec = new ProductSpec();
spec.setSpuId(spu.getId());
spec.setSpecName(specOption.getSpecName());
spec.setSpecValues(JSON.toJSONString(specOption.getSpecValues()));
specMapper.insert(spec);
}
// 3. 生成 SKU
Map<String, List<String>> specOptions = request.getSpecOptions().stream()
.collect(Collectors.toMap(SpecOption::getSpecName, SpecOption::getSpecValues));
List<Map<String, String>> skuCombinations = cartesianProduct(specOptions);
for (Map<String, String> combination : skuCombinations) {
ProductSku sku = new ProductSku();
sku.setSkuNo(generateSkuNo());
sku.setSpuId(spu.getId());
sku.setSkuName(spu.getSpuName() + " " + String.join(" ", combination.values()));
sku.setSpecJson(JSON.toJSONString(combination));
skuMapper.insert(sku);
}
// 4. AI 翻译多语言内容
for (String languageCode : SUPPORTED_LANGUAGES) {
TranslateResult result = aiTranslateService.translate(
request.getTitle(),
request.getDescription(),
languageCode
);
ProductI18n i18n = new ProductI18n();
i18n.setSpuId(spu.getId());
i18n.setLanguageCode(languageCode);
i18n.setTitle(result.getTitle());
i18n.setDescription(result.getDescription());
i18n.setIsAiTranslated(1);
i18nMapper.insert(i18n);
}
return spu;
}
4.2 订单创建流程(Webhook 接收 + 分布式事务)
业务场景
用户在 Shopify 下单购买 2 件蓝牙耳机,Shopify 通过 Webhook 推送订单到供应链系统。
完整流程
第一步,Shopify 推送订单。
用户在 Shopify 完成支付,Shopify 自动推送订单到供应链系统的 Webhook 接口:
POST https://api.example.com/webhook/shopify
Headers:
X-Shopify-Hmac-SHA256: base64_encoded_signature
Body:
{
"id": 123456789,
"order_number": "SO-2024-001",
"customer": {
"name": "John Doe",
"email": "john@example.com",
"phone": "+1234567890"
},
"shipping_address": {
"address1": "123 Main St",
"city": "New York",
"country": "US",
"zip": "10001"
},
"line_items": [
{
"sku": "SKU-BT-001",
"name": "蓝牙耳机",
"price": "29.99",
"quantity": 2
}
],
"total_price": "59.98",
"currency": "USD"
}
第二步,验证签名。
系统接收到请求后,首先验证签名,防止恶意请求。
技术上,系统从 HTTP Header 中获取签名,使用 HMAC-SHA256 算法计算签名,对比是否一致。
String signature = request.getHeader("X-Shopify-Hmac-SHA256");
String requestBody = request.getBody();
boolean valid = verifySignature(requestBody, signature, SHOPIFY_SECRET);
if (!valid) {
throw new BusinessException("签名验证失败");
}
第三步,幂等性检查。
系统检查订单是否已存在,防止重复创建。
技术上,系统先获取分布式锁,再查询订单是否已存在。
String lockKey = "order:create:Shopify:" + platformOrderNo;
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("订单创建中,请勿重复提交");
}
OrderMain existingOrder = orderMapper.selectOne(
new LambdaQueryWrapper<OrderMain>()
.eq(OrderMain::getPlatform, "Shopify")
.eq(OrderMain::getPlatformOrderNo, platformOrderNo)
);
if (existingOrder != null) {
return existingOrder;
}
第四步,创建订单(分布式事务)。
系统使用 Seata AT 模式,在一个全局事务中完成三个操作:
- OMS 创建订单
- WMS 冻结库存
- PMS 更新商品销量
技术上,系统使用 @GlobalTransactional 注解,Seata 自动管理分布式事务。
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public OrderMain createOrder(CreateOrderRequest request) {
// 1. 创建订单
OrderMain order = new OrderMain();
order.setOrderNo(generateOrderNo());
order.setPlatform("Shopify");
order.setPlatformOrderNo(request.getPlatformOrderNo());
order.setUserId(request.getUserId());
order.setStatus(OrderStatus.PENDING_SHIP);
orderMapper.insert(order);
// 2. 创建订单明细
for (OrderItemRequest itemRequest : request.getItems()) {
OrderItem item = new OrderItem();
item.setOrderId(order.getId());
item.setSkuId(itemRequest.getSkuId());
item.setQuantity(itemRequest.getQuantity());
orderItemMapper.insert(item);
}
// 3. 冻结库存(远程调用 WMS)
FreezeStockRequest freezeRequest = new FreezeStockRequest();
freezeRequest.setOrderNo(order.getOrderNo());
freezeRequest.setItems(request.getItems());
Result<Void> freezeResult = warehouseFeignClient.freezeStock(freezeRequest);
if (!freezeResult.isSuccess()) {
throw new BusinessException("库存不足");
}
// 4. 更新商品销量(远程调用 PMS)
UpdateSalesRequest salesRequest = new UpdateSalesRequest();
salesRequest.setItems(request.getItems());
Result<Void> salesResult = productFeignClient.updateSales(salesRequest);
if (!salesResult.isSuccess()) {
throw new BusinessException("更新销量失败");
}
return order;
}
第五步,风险控制。
系统执行风险控制规则,评估订单风险等级。
技术上,系统调用风险控制服务,执行多个风险规则,计算风险分数。
RiskResult riskResult = riskControlService.evaluateRisk(order);
order.setRiskLevel(riskResult.getLevel());
order.setRiskScore(riskResult.getScore());
if (riskResult.getLevel() == RiskLevel.HIGH) {
order.setStatus(OrderStatus.REJECTED);
order.setRejectReason("触发风险规则:" + String.join(", ", riskResult.getTriggeredRules()));
} else if (riskResult.getLevel() == RiskLevel.MEDIUM) {
order.setStatus(OrderStatus.PENDING_REVIEW);
}
orderMapper.updateById(order);
第七步,返回响应。
系统返回 HTTP 200,告诉 Shopify 接收成功。
return ResponseEntity.ok().build();
4.3 订单发货流程(状态机 + 库存扣减)
业务场景
商家在后台点击”发货”按钮,系统调用 WMS 出库,调用 TMS 创建运单。
完整流程
第一步,商家点击发货。
商家在后台选择订单,点击”发货”按钮。
第二步,触发状态机事件。
系统触发订单状态机的 SHIP 事件。
技术上,系统调用 Spring State Machine,传入订单 ID 和事件类型。
Message<OrderEvent> message = MessageBuilder
.withPayload(OrderEvent.SHIP)
.setHeader("orderId", orderId)
.build();
stateMachine.sendEvent(message);
第三步,状态机执行 Guard。
状态机执行 Guard,检查订单是否可以发货。
技术上,Guard 检查订单状态是否为”待发货”,检查库存是否充足。
@Component
public class StockGuard implements Guard<OrderState, OrderEvent> {
@Override
public boolean evaluate(StateContext<OrderState, OrderEvent> context) {
Long orderId = context.getMessage().getHeaders().get("orderId", Long.class);
// 检查库存是否充足
return warehouseService.hasEnoughStock(orderId);
}
}
第四步,状态机执行 Action。
状态机执行 Action,调用 WMS 出库。
技术上,Action 调用 WMS 的 Feign 接口,传入订单号和 SKU 列表。
@Component
public class OutboundAction implements Action<OrderState, OrderEvent> {
@Override
public void execute(StateContext<OrderState, OrderEvent> context) {
Long orderId = context.getMessage().getHeaders().get("orderId", Long.class);
// 调用 WMS 出库
OutboundRequest request = new OutboundRequest();
request.setOrderId(orderId);
Result<Void> result = warehouseFeignClient.outbound(request);
if (!result.isSuccess()) {
throw new BusinessException("出库失败");
}
}
}
第五步,WMS 出库。
WMS 接收到出库请求,执行出库操作:
- 扣减库存:
quantity = quantity - N, frozen_qty = frozen_qty - N - 写入库存流水
- 更新出库单状态
技术上,WMS 在一个事务中完成这三个操作。
第六步,状态机更新订单状态。
状态机将订单状态从”待发货”更新为”已发货”。
技术上,状态机自动更新订单状态,记录发货时间。
order.setStatus(OrderStatus.SHIPPED);
order.setShipTime(LocalDateTime.now());
orderMapper.updateById(order);
第七步,通知 TMS 创建运单。
系统通过 MQ 异步通知 TMS 创建运单。
技术上,系统发送 MQ 消息,包含订单号、收货地址、物流方式。
CreateWaybillMessage message = new CreateWaybillMessage();
message.setOrderNo(order.getOrderNo());
message.setReceiverName(order.getReceiverName());
message.setShippingAddress(order.getShippingAddress());
message.setLogisticsMethod("DHL");
rocketMQTemplate.convertAndSend("topic-create-waybill", message);
第八步,记录订单日志。
系统记录订单日志,记录操作人、操作时间、状态变更。
OrderLog log = new OrderLog();
log.setOrderId(order.getId());
log.setOrderNo(order.getOrderNo());
log.setOperateType("发货");
log.setBeforeStatus(OrderStatus.PENDING_SHIP);
log.setAfterStatus(OrderStatus.SHIPPED);
log.setOperatorId(operatorId);
log.setOperatorName(operatorName);
orderLogMapper.insert(log);
五、面试问答
5.1 业务面试题(10 个)
Q1: 为什么需要 SPU/SKU 两级商品体系?直接用 SKU 不行吗?
A1: 不行。如果只用 SKU,会出现三个问题:
第一,公共属性冗余存储。品牌、材质、HS 编码、产地这些属性对所有 SKU 都一样,如果每个 SKU 都存一遍,数据冗余严重。比如一件 T恤有 9 个 SKU,品牌信息就要存 9 次。修改品牌时要改 9 次,容易出错。
第二,商品管理困难。商家想查看某个商品的所有 SKU,需要通过商品名称模糊查询,效率低。如果有 SPU,直接通过 SPU ID 查询所有 SKU,效率高。
第三,数据分析困难。商家想统计某个商品的总销量,需要聚合所有 SKU 的销量。如果有 SPU,直接在 SPU 层面记录总销量,查询效率高。
用 SPU/SKU 两级体系后,公共属性只存一份,规格属性分开存储,数据结构清晰,管理方便。
Q2: 笛卡尔积算法的时间复杂度是多少?如果规格很多会不会性能问题?
A2: 笛卡尔积算法的时间复杂度是 O(n1 × n2 × … × nk),其中 n1、n2、…、nk 是每个规格的选项数量。
举个例子:3 种颜色 × 3 种尺码 = 9 个 SKU,时间复杂度是 O(3 × 3) = O(9)。5 种颜色 × 4 种尺码 × 2 种材质 = 40 个 SKU,时间复杂度是 O(5 × 4 × 2) = O(40)。
实际场景中,规格数量一般不会超过 5 个,每个规格的选项数量一般不会超过 10 个,所以 SKU 数量一般在 100 以内,性能完全没问题。
如果真的有极端场景,比如 10 种颜色 × 10 种尺码 × 10 种材质 = 1000 个 SKU,我们可以用异步任务生成 SKU,避免阻塞用户操作。
Q3: AI 翻译的准确率如何保证?如果翻译错了怎么办?
A3: 我们通过三个方式保证翻译质量:
第一,专业 Prompt 设计。我们针对电商场景设计了专业 Prompt,要求保持电商营销风格,突出卖点,使用目标市场的常用表达,保留品牌名和型号等专有名词。这样翻译出来的内容更符合电商场景。
第二,人工审核机制。AI 翻译完成后,商家可以查看翻译结果,如果不满意可以手动修改。我们在 product_i18n 表有 is_ai_translated 字段,标记是否 AI 翻译,方便商家筛选和审核。
第三,翻译质量反馈。商家可以对翻译结果打分,我们收集反馈数据,持续优化 Prompt,提升翻译质量。
实际使用中,AI 翻译准确率 95%+,商家满意度 90%+。即使有 5% 的翻译不准确,商家也可以手动修改,总体效率还是比人工翻译高很多。
商家在录入SPU/SKU的时候 如果选择了AI翻译 那么在翻译成功后 我们可以发出一个通知 提醒商家翻译完成 让其进行人工审核(提供查询翻译结果的接口)
Q4: 为什么需要多平台多币种定价?不能统一定价吗?
A4: 不能统一定价,因为不同平台、不同国家的市场环境不同。
第一,税率不同。美国的销售税率是 10%,英国的增值税率是 20%,德国的增值税率是 19%。如果统一定价,商家要么承担税费差异,要么客户支付的最终价格不同。
第二,汇率波动。美元、英镑、欧元的汇率每天都在变化,如果统一定价,商家可能因为汇率波动亏损。
第三,市场竞争。不同平台、不同国家的竞争程度不同,定价策略也不同。比如 Amazon 竞争激烈,价格要低一些;Shopify 是独立站,价格可以高一些。
第四,平台佣金。不同平台的佣金比例不同,Amazon 佣金 15%,Shopify 佣金 2.9%,定价要考虑佣金成本。
所以,我们设计了多平台多币种定价表,商家可以为每个平台、每个国家配置不同的价格,系统自动计算税费和最终价格。
Q5: Webhook 签名验证的原理是什么?为什么要验证签名?
A5: Webhook 签名验证的原理是使用 HMAC-SHA256 算法。

平台在推送订单时,会用一个密钥(Secret)对请求体进行 HMAC-SHA256 加密,生成签名,放在 HTTP Header 中。我们接收到请求后,用相同的密钥和算法计算签名,对比是否一致。如果一致,说明请求来自平台;如果不一致,说明请求被篡改或来自恶意攻击者。
为什么要验证签名?因为 Webhook 接口是公开的,任何人都可以访问。如果不验证签名,攻击者可以伪造订单推送到我们的系统,导致:
- 创建虚假订单,占用库存
- 修改订单金额,造成财务损失
- 触发业务逻辑,消耗系统资源
验证签名后,只有持有密钥的平台才能推送订单,保证了请求的真实性和完整性。
Q6: 订单幂等性为什么要用三层防护?一层不够吗?
A6: 一层不够,因为每一层都有局限性。
第一层,Redis 分布式锁。在 Webhoook 并发推送的情况下可以防止并发, 在其他情况下也可以实现一个幂等性校验, 不用每次都查询数据进行判断, 所以此处不只是作为分布式锁使用更是作为幂等性校验的Key使用, 所以这个Redis的Key不能手动删除, 而是要等待自动过期, 否则幂等性校验的功能就失效了。
第二层,业务层查重。如果Redis不能进行判断, 在真正进行业务之前还可以先通过MySQL进行判断, 此处判断也是优于直接执行业务后遇到MySQL唯一约束的校验, 但缺点是如果两个请求同时查询,都查不到订单,就会同时创建,无法防止并发。
第三层,数据库唯一索引。优点是数据库层面保证唯一性,即使前两层失效,数据库也会拒绝重复插入。也是作为幂等性的兜底策略。
三层防护结合起来,既保证了幂等性,又保证了性能和用户体验。正常情况下,Redis 分布式锁就能防止重复创建。如果 Redis 挂了,业务层查重可以防止重复创建。如果业务层查重失败,数据库唯一索引可以兜底。
Q7: 订单状态机有什么好处?用 if-else 判断状态转换不行吗?
A7: 用 if-else 判断状态转换有三个问题:
第一,代码臃肿。订单有 10+ 种状态,20+ 种状态转换,如果用 if-else 判断,代码会非常臃肿,难以维护。比如:
if (order.getStatus() == OrderStatus.PENDING_PAYMENT && event == OrderEvent.PAY) {
order.setStatus(OrderStatus.PENDING_SHIP);
freezeStock(order);
} else if (order.getStatus() == OrderStatus.PENDING_SHIP && event == OrderEvent.SHIP) {
order.setStatus(OrderStatus.SHIPPED);
outbound(order);
} else if (...) {
...
}
第二,状态转换规则分散。状态转换规则散落在各个业务方法中,难以统一管理。如果要修改状态转换规则,需要找到所有相关代码,容易遗漏。
第三,业务逻辑耦合。状态转换和业务逻辑耦合在一起,职责不清晰,难以测试。
用状态机后,状态转换规则集中管理,代码清晰,易于维护。Guard 和 Action 独立实现,职责单一,易于测试。新增状态或转换规则时,只需要修改状态机配置,不需要修改业务代码。
Q8: 订单风险控制规则引擎如何扩展?如果要新增规则怎么办?
A8: 我们使用策略模式实现风险控制规则引擎,扩展性非常好。
如果要新增规则,只需要三步:
第一步,实现 RiskRule 接口。
@Component
public class NewRule implements RiskRule {
@Override
public int evaluate(OrderMain order) {
// 实现规则逻辑
return score;
}
@Override
public String getRuleName() {
return "新规则";
}
}
第二步,Spring 自动注入。因为我们用 @Component 注解,Spring 会自动扫描并注入到 List<RiskRule> 中。
第三步,规则引擎自动执行。规则引擎会遍历所有规则,自动执行新规则。
不需要修改规则引擎的代码,不需要修改配置文件,完全符合开闭原则(对扩展开放,对修改关闭)。
Q9: 订单超时预警为什么要用三级预警?一次预警不够吗?
A9: 一次预警不够,因为商家可能没有及时看到预警。
第一级预警(提前 2 小时):发送站内信 + 邮件。这是最温和的提醒,商家可能在忙,没有及时查看。
第二级预警(提前 30 分钟):发送短信 + 钉钉消息。这是更紧急的提醒,短信和钉钉消息的到达率更高,商家更容易看到。
第三级预警(超时):自动执行超时处理。如果商家还是没有处理,系统自动执行超时处理,避免订单一直挂着。
三级预警机制,既给了商家充足的时间处理订单,又保证了订单不会一直超时。实际使用中,大部分商家在第一级或第二级预警时就会处理订单,第三级预警很少触发。
Q10: 订单拆分和合并的业务价值是什么?
A10: 订单拆分和合并有三个业务价值:
订单拆分的价值:
第一,提高发货效率。如果订单中的商品分别在国内仓、FBA 仓、海外仓,拆分后每个仓库独立发货,不需要等待其他仓库,发货时间从 48 小时降到 24 小时。
第二,满足物流限制。某些国家禁止某些商品一起发货(如电池和化妆品),拆分后可以分开发货,避免物流拒收。
第三,满足平台要求。Amazon FBA 要求每个包裹只能包含一个 SKU,拆分后可以满足平台要求,避免被平台拒收。
订单合并的价值:
第一,降低物流成本。同一用户在 1 小时内下了 3 个订单,合并成 1 个包裹发货,物流成本降低 60%。
第二,提升客户体验。客户收到 1 个包裹比收到 3 个包裹更方便,减少签收次数。
第三,减少包装成本。1 个包裹的包装成本比 3 个包裹的包装成本低。
实际使用中,订单拆分提升发货效率 50%,订单合并降低物流成本 30%。
5.2 技术面试题(10 个)
Q1: 笛卡尔积算法如何实现?
A1: 笛卡尔积算法的核心思路是:从第一个规格开始,逐个规格展开,每次展开都生成新的组合。
具体实现:
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;
}
举个例子:颜色(红、蓝)× 尺码(S、M)
第一轮:result = [{}]
第二轮(展开颜色):
- existing = {},value = 红 → newCombination = {颜色: 红}
- existing = {},value = 蓝 → newCombination = {颜色: 蓝}
- result = [{颜色: 红}, {颜色: 蓝}]
第三轮(展开尺码):
- existing = {颜色: 红},value = S → newCombination = {颜色: 红, 尺码: S}
- existing = {颜色: 红},value = M → newCombination = {颜色: 红, 尺码: M}
- existing = {颜色: 蓝},value = S → newCombination = {颜色: 蓝, 尺码: S}
- existing = {颜色: 蓝},value = M → newCombination = {颜色: 蓝, 尺码: M}
- result = [{颜色: 红, 尺码: S}, {颜色: 红, 尺码: M}, {颜色: 蓝, 尺码: S}, {颜色: 蓝, 尺码: M}]
Q2: Webhook 签名验证如何实现?
A2: Webhook 签名验证使用 HMAC-SHA256 算法。
具体实现:
public boolean verifySignature(String requestBody, String signature, String secret) {
try {
// 1. 创建 HMAC-SHA256 实例
Mac mac = Mac.getInstance("HmacSHA256");
// 2. 初始化密钥
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
mac.init(secretKey);
// 3. 计算签名
byte[] hash = mac.doFinal(requestBody.getBytes());
String calculatedSignature = Base64.getEncoder().encodeToString(hash);
// 4. 对比签名
return calculatedSignature.equals(signature);
} catch (Exception e) {
log.error("签名验证失败", e);
return false;
}
}
使用方式:
@PostMapping("/webhook/shopify")
public ResponseEntity<Void> receiveShopifyOrder(@RequestBody String requestBody,
@RequestHeader("X-Shopify-Hmac-SHA256") String signature) {
// 验证签名
boolean valid = verifySignature(requestBody, signature, SHOPIFY_SECRET);
if (!valid) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// 处理订单
processOrder(requestBody);
return ResponseEntity.ok().build();
}
Q3: 订单幂等性如何实现?
A3: 订单幂等性通过三层防护实现:
第一层,Redis 分布式锁。
String lockKey = "order:create:" + platform + ":" + platformOrderNo;
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("订单创建中,请勿重复提交");
}
第二层,业务层查重。
OrderMain existingOrder = orderMapper.selectOne(
new LambdaQueryWrapper<OrderMain>()
.eq(OrderMain::getPlatform, platform)
.eq(OrderMain::getPlatformOrderNo, platformOrderNo)
);
if (existingOrder != null) {
return existingOrder;
}
第三层,数据库唯一索引。
ALTER TABLE order_main ADD UNIQUE INDEX uk_platform_order_no(platform, platform_order_no);
完整流程:
public OrderMain createOrder(CreateOrderRequest request) {
String platform = request.getPlatform();
String platformOrderNo = request.getPlatformOrderNo();
// 1. 获取分布式锁
String lockKey = "order:create:" + platform + ":" + platformOrderNo;
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("订单创建中,请勿重复提交");
}
try {
// 2. 查询订单是否已存在
OrderMain existingOrder = orderMapper.selectOne(
new LambdaQueryWrapper<OrderMain>()
.eq(OrderMain::getPlatform, platform)
.eq(OrderMain::getPlatformOrderNo, platformOrderNo)
);
if (existingOrder != null) {
return existingOrder;
}
// 3. 创建订单
OrderMain order = new OrderMain();
order.setOrderNo(generateOrderNo());
order.setPlatform(platform);
order.setPlatformOrderNo(platformOrderNo);
orderMapper.insert(order);
return order;
} finally {
// 4. 释放分布式锁
redisTemplate.delete(lockKey);
}
}
Q4: Spring State Machine 如何配置?
A4: Spring State Machine 配置分为三步:
第一步,定义状态和事件。
public enum OrderState {
PENDING_PAYMENT, // 待支付
PENDING_SHIP, // 待发货
SHIPPED, // 已发货
DELIVERED, // 已签收
COMPLETED, // 已完成
CANCELLED // 已取消
}
public enum OrderEvent {
PAY, // 支付
SHIP, // 发货
DELIVER, // 签收
COMPLETE, // 完成
CANCEL // 取消
}
第二步,配置状态机。
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderState, OrderEvent> {
@Autowired
private PaymentGuard paymentGuard;
@Autowired
private FreezeStockAction freezeStockAction;
@Override
public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> states) throws Exception {
states
.withStates()
.initial(OrderState.PENDING_PAYMENT)
.states(EnumSet.allOf(OrderState.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions) throws Exception {
transitions
// 待支付 -> 待发货
.withExternal()
.source(OrderState.PENDING_PAYMENT)
.target(OrderState.PENDING_SHIP)
.event(OrderEvent.PAY)
.guard(paymentGuard)
.action(freezeStockAction)
.and()
// 待发货 -> 已发货
.withExternal()
.source(OrderState.PENDING_SHIP)
.target(OrderState.SHIPPED)
.event(OrderEvent.SHIP)
.action(outboundAction);
}
}
第三步,实现 Guard 和 Action。
@Component
public class PaymentGuard implements Guard<OrderState, OrderEvent> {
@Override
public boolean evaluate(StateContext<OrderState, OrderEvent> context) {
Long orderId = context.getMessage().getHeaders().get("orderId", Long.class);
return paymentService.isPaid(orderId);
}
}
@Component
public class FreezeStockAction implements Action<OrderState, OrderEvent> {
@Override
public void execute(StateContext<OrderState, OrderEvent> context) {
Long orderId = context.getMessage().getHeaders().get("orderId", Long.class);
warehouseService.freezeStock(orderId);
}
}
Q5: 风险控制规则引擎如何实现?
A5: 风险控制规则引擎使用策略模式实现。
第一步,定义规则接口。
public interface RiskRule {
int evaluate(OrderMain order);
String getRuleName();
}
第二步,实现具体规则。
@Component
public class AddressValidityRule implements RiskRule {
@Override
public int evaluate(OrderMain order) {
String address = order.getShippingAddress();
if (StringUtils.isBlank(address) || address.length() < 10) {
return 50;
}
if (!address.matches(".*\\d+.*")) {
return 30;
}
return 0;
}
@Override
public String getRuleName() {
return "地址有效性规则";
}
}
@Component
public class HighFrequencyRule implements RiskRule {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public int evaluate(OrderMain order) {
String key = "order:frequency:" + order.getUserId();
Long count = redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, 1, TimeUnit.HOURS);
if (count > 10) {
return 90;
}
if (count > 5) {
return 50;
}
return 0;
}
@Override
public String getRuleName() {
return "高频下单规则";
}
}
第三步,规则引擎执行。
@Service
public class RiskControlService {
@Autowired
private List<RiskRule> riskRules;
public RiskResult evaluateRisk(OrderMain order) {
int totalScore = 0;
List<String> triggeredRules = new ArrayList<>();
for (RiskRule rule : riskRules) {
int score = rule.evaluate(order);
if (score > 0) {
totalScore += score;
triggeredRules.add(rule.getRuleName() + "(" + score + "分)");
}
}
RiskLevel level;
if (totalScore >= 100) {
level = RiskLevel.HIGH;
} else if (totalScore >= 50) {
level = RiskLevel.MEDIUM;
} else {
level = RiskLevel.LOW;
}
return new RiskResult(level, totalScore, triggeredRules);
}
}
Q6: Seata AT 模式的原理是什么?
A6: Seata AT 模式是一种无侵入的分布式事务解决方案,原理是两阶段提交 + 自动回滚。
一阶段(执行业务 SQL):
- 解析 SQL,查询前镜像(before image)
- 执行业务 SQL
- 查询后镜像(after image)
- 生成 undo_log,记录前后镜像
- 提交本地事务
- 向 TC(事务协调器)注册分支事务
二阶段提交:
- TC 通知所有分支事务提交
- 删除 undo_log
- 释放锁
二阶段回滚:
- TC 通知所有分支事务回滚
- 根据 undo_log 生成反向 SQL
- 执行反向 SQL,恢复数据
- 删除 undo_log
举个例子:
业务 SQL:UPDATE inventory SET frozen_qty = frozen_qty + 10 WHERE sku_id = 1001
一阶段:
- 前镜像:
{sku_id: 1001, frozen_qty: 50} - 执行 SQL:
frozen_qty变成 60 - 后镜像:
{sku_id: 1001, frozen_qty: 60} - 生成 undo_log:
{before: {frozen_qty: 50}, after: {frozen_qty: 60}}
二阶段回滚:
- 根据 undo_log 生成反向 SQL:
UPDATE inventory SET frozen_qty = 50 WHERE sku_id = 1001 - 执行反向 SQL,
frozen_qty恢复成 50
Q7: Seata AT 模式如何使用?
A7: Seata AT 模式使用非常简单,只需要三步:
第一步,引入依赖。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
第二步,配置 Seata。
seata:
enabled: true
application-id: oms-service
tx-service-group: my_tx_group
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
第三步,创建 undo_log 表。
CREATE TABLE undo_log (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
branch_id BIGINT(20) NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第四步,使用 @GlobalTransactional 注解。
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public OrderMain createOrder(CreateOrderRequest request) {
// 1. 创建订单
orderMapper.insert(order);
// 2. 冻结库存(远程调用 WMS)
warehouseFeignClient.freezeStock(freezeRequest);
// 3. 更新商品销量(远程调用 PMS)
productFeignClient.updateSales(salesRequest);
return order;
}
就这么简单,不需要手动编写补偿逻辑,Seata 自动管理分布式事务。
Q8: 为什么订单表要按月分表?
A8: 订单表按月分表有三个原因:
第一,数据量大。 日均订单 10 万+,一年就是 3600 万+。如果不分表,单表数据量太大,查询性能下降。MySQL 单表超过 1000 万行,查询性能明显下降。
查询性能不能只看 表的记录的行数 还跟 物理存储的 文件大小有关
我们是一个SaaS系统 也就是说 所有商家的所有店铺的所有订单 都统一放到了一张表中
分表: 最简单的是 按月分表 —> 不需要使用 分库分表的 插件 只使用 MyBatis-Plus 就可以实现了
每个月初 创建下个月的订单表
然后在进行查询的时候 根据查询的时间 去找到对应的表进行查询 使用 ${} 表名拼接 去查询就可以了
如果需要进行跨月查询 多次查询 上个月 本月 在业务层 进行数据的合并
按照不同的租户ID 去分表 每个租户一个表 每个租户下 每个月再分表 —> 就需要使用分库分表的插件 进行查询的路由
第二,查询特点。 订单查询大部分是按时间范围查询,比如查询最近 7 天的订单、查询本月的订单。按月分表后,查询只需要扫描对应月份的表,不需要扫描全表,查询性能提升。
第三,数据归档。 历史订单可以归档到冷存储,节省成本。比如 1 年前的订单,查询频率很低,可以归档到 OSS 或 S3,释放数据库空间。
分表策略:
- 表名格式:
order_main_202601、order_main_202602 - 路由规则:根据
create_time的年月路由到对应表 - 查询时,根据时间范围确定要查询哪些表
Q9: 如何实现订单按月分表的路由?
A9: 订单按月分表的路由使用 ShardingSphere 实现。
第一步,引入依赖。
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
</dependency>
第二步,配置分表规则。
spring:
shardingsphere:
datasource:
names: ds0
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/oms
username: root
password: root
rules:
sharding:
tables:
order_main:
actual-data-nodes: ds0.order_main_$->{202401..202412}
table-strategy:
standard:
sharding-column: create_time
sharding-algorithm-name: order-month-algorithm
sharding-algorithms:
order-month-algorithm:
type: CLASS_BASED
props:
strategy: STANDARD
algorithmClassName: com.example.OrderMonthShardingAlgorithm
第三步,实现分片算法。
public class OrderMonthShardingAlgorithm implements StandardShardingAlgorithm<Date> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Date> shardingValue) {
Date createTime = shardingValue.getValue();
String suffix = new SimpleDateFormat("yyyyMM").format(createTime);
String tableName = "order_main_" + suffix;
if (availableTargetNames.contains(tableName)) {
return tableName;
}
throw new IllegalArgumentException("表不存在: " + tableName);
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Date> shardingValue) {
Range<Date> range = shardingValue.getValueRange();
Date start = range.lowerEndpoint();
Date end = range.upperEndpoint();
List<String> result = new ArrayList<>();
Calendar calendar = Calendar.getInstance();
calendar.setTime(start);
while (!calendar.getTime().after(end)) {
String suffix = new SimpleDateFormat("yyyyMM").format(calendar.getTime());
String tableName = "order_main_" + suffix;
if (availableTargetNames.contains(tableName)) {
result.add(tableName);
}
calendar.add(Calendar.MONTH, 1);
}
return result;
}
}
Q10: 如何保证 MQ 消息的可靠性和幂等性?
A10: MQ 消息的可靠性和幂等性分别保证。
可靠性(保证消息不丢失):
第一,生产者发送失败重试。
rocketMQTemplate.syncSend("topic-create-waybill", message, 3000, 3);
第二,Broker 持久化。RocketMQ 主从同步 + 同步刷盘,消息写入磁盘后才返回成功。
第三,消费者手动 ACK。
@RocketMQMessageListener(topic = "topic-create-waybill", consumerGroup = "tms-consumer")
public class CreateWaybillConsumer implements RocketMQListener<CreateWaybillMessage> {
@Override
public void onMessage(CreateWaybillMessage message) {
try {
// 处理业务逻辑
waybillService.createWaybill(message);
// 手动 ACK(RocketMQ 默认自动 ACK,这里只是示意)
} catch (Exception e) {
// 抛异常,消息会自动重试
throw new RuntimeException("处理失败", e);
}
}
}
幂等性(保证消息不重复消费):
用 Redis SETNX 实现。
@Override
public void onMessage(CreateWaybillMessage message) {
String orderNo = message.getOrderNo();
String key = "waybill:create:" + orderNo;
// 幂等性检查
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "1", 7, TimeUnit.DAYS);
if (!success) {
log.info("订单 {} 已处理,跳过", orderNo);
return;
}
// 处理业务逻辑
waybillService.createWaybill(message);
}
为什么 TTL 是 7 天?因为消息可能延迟投递,7 天足够长,防止消息延迟投递后被重复消费。
六、简历模板
6.1 详细版简历
项目描述
项目:跨境电商 SaaS 供应链管理平台 - 商品订单管理模块(PIM+OMS)
时间:2024.06 - 2024.12
角色:核心开发
技术:Spring Boot 3.2、MyBatis-Plus、MySQL 8.0、Redis 7.0、RocketMQ 5.0、Seata 1.6、Spring State Machine 3.2
这是个跨境电商的供应链 SaaS 平台,我负责商品信息管理(PIM)和订单管理(OMS)两个核心模块。
PIM 模块管理商品的基础信息、规格属性、多语言内容、多平台定价,支持 SPU/SKU 两级商品体系,自动生成 SKU 组合,支持 AI 自动翻译商品标题和描述到 15+ 语言。
OMS 模块统一管理来自 Amazon、Shopify、eBay 等 10+ 电商平台的订单,支持订单自动创建、状态流转、风险控制、超时预警、拆分合并、库存分配、物流对接。
日均处理订单 10 万+,峰值 QPS 5000+,管理 50 万+ SPU、300 万+ SKU,支持 20+ 国家和地区,AI 翻译准确率 95%+。
核心难点:
1. SPU/SKU 两级商品体系设计,笛卡尔积自动生成 SKU 组合
2. 多语言内容管理,AI 自动翻译,翻译成本降低 99.9%
3. 多平台订单集成,Webhook 签名验证,保证请求安全性
4. 订单幂等性设计,三层防护,幂等性 100%
5. 订单状态机设计,Spring State Machine 管理状态转换
6. 订单风险控制规则引擎,策略模式实现,欺诈订单拦截率 95%+
7. 订单超时预警系统,三级预警机制,发货及时率提升到 95%
8. 订单拆分与合并策略,提升发货效率 50%,降低物流成本 30%
9. 分布式事务 Seata AT 模式,保证订单和库存强一致性
10. 库存分配策略,固定分配 + 动态分配,超卖率降低到 0.1%
核心亮点(详细版)
1. SPU/SKU 两级商品体系 + 笛卡尔积算法
问题背景: 跨境电商的商品管理面临三个核心问题:第一,商品规格组合爆炸,一件 T恤有 3 种颜色 × 3 种尺码 = 9 个 SKU,手动创建效率低,容易出错。第二,公共属性冗余存储,品牌、材质、HS 编码这些属性对所有 SKU 都一样,如果每个 SKU 都存一遍,数据冗余严重。第三,多平台同步困难,同一个商品要同步到多个平台,如果没有统一的商品主数据,容易出现数据不一致。
解决方案: 设计了 SPU/SKU 两级商品体系 + 笛卡尔积自动生成算法。SPU 存储商品的公共属性(品牌、材质、HS 编码、产地、商品描述),SKU 存储商品的规格属性(颜色、尺码、价格、库存、条形码)。商家只需要在 SPU 层面配置规格选项(颜色:红、蓝、绿;尺码:S、M、L),系统自动生成所有 SKU 组合(红-S、红-M、红-L、蓝-S、蓝-M、蓝-L、绿-S、绿-M、绿-L)。
技术实现: 使用笛卡尔积算法,从第一个规格开始,逐个规格展开,每次展开都生成新的组合。时间复杂度是 O(n1 × n2 × … × nk),实际场景中 SKU 数量一般在 100 以内,性能完全没问题。商家创建 SPU 后,系统自动生成 SKU,插入 product_sku 表,每个 SKU 的 spec_json 字段存储规格属性 JSON。
效果: 商家创建商品的时间从 10 分钟降到 2 分钟,效率提升 80%。公共属性只存一份,修改时只需要改 SPU,所有 SKU 自动生效。支持任意数量的规格类型和规格值,扩展性强。
2. 多语言内容管理 + AI 自动翻译
问题背景: 跨境电商需要在多个国家销售,每个国家的语言不同。人工翻译成本高(100 元/语言),翻译质量不稳定,翻译周期长(1-2 天)。
解决方案: 设计了多语言内容管理 + AI 自动翻译。设计 product_i18n 表,存储商品的多语言内容,每个商品的每种语言都是一条记录,通过 spu_id + language_code 唯一标识。集成 DeepSeek API 或 OpenAI API,商家填写中文标题和描述后,系统自动翻译成英语、西班牙语、法语、德语、日语等 15+ 语言。针对电商场景设计专业 Prompt,要求保持电商营销风格,突出卖点,使用目标市场的常用表达,保留品牌名和型号等专有名词。
技术实现: 商家填写中文内容后,系统调用 DeepSeek API,传入中文内容和目标语言,返回翻译结果,插入 product_i18n 表。Prompt 设计:你是一个专业的跨境电商翻译专家,请将以下商品信息翻译成{目标语言},保持电商营销风格,突出卖点,使用目标市场的常用表达,保留品牌名、型号等专有名词,标题控制在 200 字符以内,描述保持原文的段落结构。商家可以查看翻译结果,如果不满意可以手动修改。
效果: 翻译成本从 100 元/语言降到 0.1 元/语言,降低 99.9%。翻译时间从 1-2 天降到 10 秒,效率提升 10000 倍。AI 翻译质量稳定,准确率 95%+,商家满意度 90%+。
3. 多平台订单集成 + Webhook 签名验证
问题背景: 跨境电商商家在多个平台开店,订单来源分散,订单数据格式不统一,订单同步时效性差,安全性问题(需要验证请求来源,防止恶意请求)。
解决方案: 设计了多平台订单集成 + Webhook 签名验证。两种集成方式:主动拉取(定时任务每 5 分钟调用平台 API 拉取新订单)和被动接收(平台主动推送订单到我们的回调接口)。Webhook 签名验证使用 HMAC-SHA256 算法,平台在推送订单时,用密钥对请求体进行加密,生成签名,放在 HTTP Header 中。我们接收到请求后,用相同的密钥和算法计算签名,对比是否一致。
技术实现: 主动拉取方式使用 XXL-Job 配置定时任务,每 5 分钟执行一次,调用平台 API,解析订单数据,保存订单。被动接收方式,商家在平台后台配置 Webhook URL,平台创建订单时自动推送。系统验证签名,使用 Mac.getInstance("HmacSHA256") 计算签名,对比是否一致。验证通过后,解析订单数据,保存订单,返回 HTTP 200。
效果: 订单同步实时性,Webhook 方式实时接收订单,延迟从 5 分钟降到 1 秒。签名验证防止恶意请求,拦截率 100%。商家在一个后台查看所有平台订单,效率提升 80%。
4. 订单幂等性设计 + 分布式锁
问题背景: 订单创建面临三个幂等性问题:平台重复推送、用户重复提交、定时任务重复拉取。如果不做幂等性控制,会导致订单重复创建、库存重复冻结、财务数据错误。
解决方案: 设计了订单幂等性设计 + 分布式锁。使用”平台 + 平台订单号”作为唯一标识,保证同一个平台的同一个订单只能创建一次。三层防护:Redis 分布式锁(防止并发创建)、业务层查重(查询订单是否已存在)、数据库唯一索引(数据库层面保证唯一性)。
技术实现: 创建订单前,先获取分布式锁 order:create:{platform}:{platform_order_no},使用 redisTemplate.opsForValue().setIfAbsent(),TTL 10 秒。获取锁成功后,查询订单是否已存在,如果存在直接返回。如果不存在,创建订单,插入 order_main 表。数据库创建唯一索引 uk_platform_order_no(platform, platform_order_no),防止重复插入。最后释放分布式锁。
效果: 同一个订单重复推送 100 次,只创建 1 次,幂等性 100%。分布式锁防止并发创建,冲突率 0%。Redis 分布式锁响应时间 < 10ms,不影响订单创建性能。
5. 订单状态机 + Spring State Machine
问题背景: 订单的状态流转非常复杂,涉及 10+ 种状态和 20+ 种状态转换。状态转换规则复杂,状态转换前后需要执行业务逻辑,状态转换失败需要回滚,代码可维护性差(如果用 if-else 判断状态转换,代码会非常臃肿)。
解决方案: 使用 Spring State Machine 实现订单状态机。定义 State(订单的 10 种状态)、Event(触发状态转换的事件)、Transition(定义从一个状态到另一个状态的转换规则)、Guard(状态转换的前置条件)、Action(状态转换时执行的业务逻辑)。
技术实现: 定义状态枚举 OrderState(PENDING_PAYMENT、PENDING_SHIP、SHIPPED、DELIVERED、COMPLETED、CANCELLED)和事件枚举 OrderEvent(PAY、SHIP、DELIVER、COMPLETE、CANCEL)。配置状态机,使用 @EnableStateMachine 注解,配置状态转换规则,比如”待支付 -> 待发货”需要 PAY 事件,Guard 检查订单是否已支付,Action 冻结库存。实现 Guard 和 Action,Guard 返回 true 才允许转换,Action 执行业务逻辑(如调用 WMS 冻结库存)。触发状态转换时,使用 stateMachine.sendEvent(message),传入事件和订单 ID。
效果: 代码可维护性,状态转换规则集中管理,代码清晰,易于维护。业务逻辑解耦,Guard 和 Action 独立实现,职责单一,易于测试。状态转换安全,Guard 保证状态转换的前置条件,防止非法转换。
6. 订单风险控制规则引擎
问题背景: 跨境电商订单面临多种风险:欺诈订单(使用盗刷信用卡下单)、恶意刷单(竞争对手恶意下单后取消)、地址异常(收货地址不完整或不存在)、金额异常(订单金额远超正常范围)、高频下单(同一用户短时间内下单多次)。如果不做风险控制,会导致欺诈订单造成直接经济损失、恶意订单占用库存、异常订单增加客服和物流成本。
解决方案: 设计了订单风险控制规则引擎,使用策略模式实现多种风险规则。五大风险规则:地址有效性规则(检查收货地址是否完整)、金额异常规则(检查订单金额是否超过正常范围)、高频下单规则(检查同一用户 1 小时内下单次数是否超过 5 次)、黑名单规则(检查用户是否在黑名单中)、设备指纹规则(检查设备指纹是否异常)。风险等级:低风险(通过所有规则,自动审核通过)、中风险(触发 1-2 条规则,人工审核)、高风险(触发 3+ 条规则,自动拒绝订单)。
技术实现: 定义规则接口 RiskRule,包含 evaluate() 方法(返回风险分数 0-100)和 getRuleName() 方法。实现具体规则,比如 AddressValidityRule(地址为空或长度 < 10 返回 50 分,地址不包含门牌号返回 30 分)、HighFrequencyRule(使用 Redis 记录用户下单次数,1 小时内下单 > 10 次返回 90 分,> 5 次返回 50 分)。规则引擎执行,使用 @Autowired List<RiskRule> riskRules 自动注入所有规则,遍历执行,累加风险分数,根据总分判断风险等级。订单创建时执行风险控制,高风险订单自动拒绝,中风险订单人工审核,低风险订单自动通过。
效果: 欺诈订单拦截率 95%+,减少损失 100 万+/年。规则扩展性,新增规则只需实现 RiskRule 接口,无需修改引擎代码。性能优化,规则并行执行,风险评估耗时 < 50ms。
7. 订单超时预警系统
问题背景: 订单处理有严格的时效要求:未支付订单超时(用户下单后 30 分钟未支付,需要自动取消订单)、待发货订单超时(订单支付后 72 小时未发货,影响客户体验)、已发货订单超时(订单发货后 30 天未签收,可能是物流丢件)。如果不做超时预警,会导致库存长期被冻结、客户投诉增加、平台处罚。
解决方案: 设计了订单超时预警系统,使用 XXL-Job 定时任务 + 三级预警机制。三级预警机制:一级预警(提前 2 小时,发送站内信 + 邮件提醒商家)、二级预警(提前 30 分钟,发送短信 + 钉钉消息提醒商家)、三级预警(超时,自动执行超时处理,如取消订单、标记异常)。
技术实现: 使用 XXL-Job 配置定时任务,每 10 分钟执行一次。查询超时订单,比如查询待支付超时订单(创建时间 > 30 分钟),查询待发货超时订单(支付时间 > 72 小时)。执行超时处理,待支付超时订单自动取消,释放冻结库存,发送通知。待发货超时订单,根据超时时长执行不同级别的预警:70 小时发送邮件(一级预警),71.5 小时发送短信 + 钉钉消息(二级预警),72 小时标记超时状态(三级预警)。
效果: 超时订单自动处理,未支付订单自动取消,释放库存,库存周转率提升 20%。预警及时性,三级预警机制,商家响应时间从 4 小时降到 30 分钟。客户满意度提升,发货及时率从 85% 提升到 95%,客户投诉减少 50%。
8. 订单拆分与合并策略
问题背景: 跨境电商订单经常需要拆分或合并:跨仓库发货(用户下单购买 3 件商品,但这 3 件商品分别在国内仓、FBA 仓、海外仓)、物流限制(某些国家禁止某些商品一起发货)、超重限制(订单总重量超过物流限制)、平台要求(Amazon FBA 要求每个包裹只能包含一个 SKU)、合并发货(同一用户在 1 小时内下了 3 个订单,可以合并成 1 个包裹发货)。
解决方案: 设计了订单拆分与合并策略。四种拆分规则:跨仓库拆分(按商品所在仓库拆分)、物流限制拆分(按物流规则拆分)、超重拆分(按重量拆分,每个包裹不超过 30kg)、平台要求拆分(按平台规则拆分)。合并规则:同一用户、同一收货地址、1 小时内下单、总重量 < 30kg,可以合并发货。
技术实现: 判断是否需要拆分,检查是否跨仓库(商品所在仓库 ID 不同)、是否超重(总重量 > 30kg)、是否有物流限制。执行拆分,按仓库分组,为每个仓库创建子订单,计算子订单金额,更新主订单状态为”已拆分”。判断是否可以合并,检查是否同一用户、同一收货地址、1 小时内下单、总重量 < 30kg。执行合并,创建合并订单,合并订单明细,计算合并订单金额,更新原订单状态为”已合并”。
效果: 拆分准确率 100%,无需人工干预。物流成本降低,合并发货减少包裹数量 30%,物流成本降低 25%。发货效率提升,拆分后每个仓库独立发货,发货时间从 48 小时降到 24 小时。
9. 分布式事务 Seata AT 模式
问题背景: 订单创建涉及多个系统的数据操作:OMS 创建订单、WMS 冻结库存、PMS 更新商品销量。这三个操作必须保证原子性:要么全部成功,要么全部失败。传统的本地事务只能保证单个数据库的原子性,无法保证跨系统的原子性。
解决方案: 使用 Seata AT 模式实现分布式事务。Seata AT 模式原理是两阶段提交 + 自动回滚。一阶段:执行业务 SQL,Seata 自动记录 undo_log(回滚日志),提交本地事务。二阶段提交:删除 undo_log,释放锁。二阶段回滚:根据 undo_log 自动回滚数据。核心优势:无侵入(业务代码无需修改,只需加 @GlobalTransactional 注解)、高性能(一阶段直接提交本地事务,不阻塞业务)、自动回滚(二阶段回滚时自动生成反向 SQL)。
技术实现: 引入 Seata 依赖,配置 Seata(application-id、tx-service-group、registry),创建 undo_log 表。使用 @GlobalTransactional 注解,在订单创建方法上加注解,Seata 自动管理分布式事务。OMS 创建订单,Seata 记录 undo_log,提交本地事务。OMS 调用 WMS 冻结库存,Seata 传递全局事务 ID(XID),WMS 记录 undo_log,提交本地事务。OMS 调用 PMS 更新销量,Seata 传递 XID,PMS 记录 undo_log,提交本地事务。所有操作成功,Seata 二阶段提交,删除所有 undo_log。如果任何一步失败,Seata 二阶段回滚,根据 undo_log 自动回滚所有数据。
效果: 数据一致性,订单和库存强一致性,一致性保证 100%。性能优化,一阶段直接提交,不阻塞业务,TPS 5000+。开发效率,无需手动编写补偿逻辑,开发效率提升 80%。
10. 库存分配策略
问题背景: 跨境电商商家在多个平台开店,但库存是共享的。如何分配库存到各个平台,避免超卖?库存共享导致超卖、库存分配不合理、库存同步延迟。
解决方案: 设计了库存分配策略。两种分配模式:固定分配(商家手动配置每个平台的库存数量)、动态分配(系统根据各平台的销量占比,自动分配库存)。库存同步机制:推送模式(库存变动后,主动推送到各平台)、拉取模式(平台定期查询供应链系统的库存)。
技术实现: 商家配置库存分配,设计 inventory_allocation 表,存储每个 SKU 在每个平台的分配数量和分配模式。计算可售库存,固定分配模式直接返回配置的数量,动态分配模式根据销量占比计算。库存变动后同步到平台,查询所有平台的分配配置,计算每个平台的可售库存,调用平台 API 更新库存。
效果: 超卖率降低,从 5% 降到 0.1%,超卖订单减少 98%。库存周转率提升,动态分配模式下,库存周转率提升 30%。同步及时性,推送模式下,库存同步延迟从 15 分钟降到 10 秒。
6.2 简化版简历
项目描述
项目:跨境电商 SaaS 供应链管理平台 - 商品订单管理模块(PIM+OMS)
时间:2024.06 - 2024.12
角色:核心开发
技术:Spring Boot、MyBatis-Plus、MySQL、Redis、RocketMQ、Seata、Spring State Machine
负责商品信息管理(PIM)和订单管理(OMS)两个核心模块。PIM 支持 SPU/SKU 两级商品体系,自动生成 SKU 组合,AI 自动翻译商品内容到 15+ 语言。OMS 统一管理来自 10+ 电商平台的订单,支持订单自动创建、状态流转、风险控制、超时预警、拆分合并。
日均处理订单 10 万+,峰值 QPS 5000+,管理 50 万+ SPU、300 万+ SKU。
核心亮点(简化版)
1. SPU/SKU 两级商品体系 + 笛卡尔积算法 设计了 SPU/SKU 两级商品体系,SPU 存储公共属性,SKU 存储规格属性。使用笛卡尔积算法自动生成 SKU 组合,商家创建商品的时间从 10 分钟降到 2 分钟,效率提升 80%。
2. 多语言内容管理 + AI 自动翻译 集成 DeepSeek API,商家填写中文内容后,系统自动翻译成 15+ 语言。翻译成本从 100 元/语言降到 0.1 元/语言,降低 99.9%,翻译时间从 1-2 天降到 10 秒。
3. 多平台订单集成 + Webhook 签名验证 支持主动拉取和被动接收两种集成方式。Webhook 签名验证使用 HMAC-SHA256 算法,防止恶意请求,拦截率 100%。订单同步延迟从 5 分钟降到 1 秒。
4. 订单幂等性设计 + 分布式锁 使用”平台 + 平台订单号”作为唯一标识,三层防护:Redis 分布式锁、业务层查重、数据库唯一索引。同一个订单重复推送 100 次,只创建 1 次,幂等性 100%。
5. 订单状态机 + Spring State Machine 使用 Spring State Machine 实现订单状态机,定义 10 种状态和 20+ 种状态转换。状态转换规则集中管理,代码清晰,易于维护。
6. 订单风险控制规则引擎 使用策略模式实现风险控制规则引擎,支持地址有效性、金额异常、高频下单、黑名单、设备指纹等 5 种规则。欺诈订单拦截率 95%+,减少损失 100 万+/年。
7. 订单超时预警系统 使用 XXL-Job 定时任务 + 三级预警机制。一级预警(提前 2 小时)发送邮件,二级预警(提前 30 分钟)发送短信,三级预警(超时)自动处理。发货及时率从 85% 提升到 95%。
8. 订单拆分与合并策略 支持跨仓库拆分、物流限制拆分、超重拆分、平台要求拆分。支持同一用户 1 小时内订单合并发货。拆分准确率 100%,物流成本降低 25%,发货时间从 48 小时降到 24 小时。
9. 分布式事务 Seata AT 模式 使用 Seata AT 模式实现分布式事务,保证订单创建、库存冻结、销量更新的强一致性。一阶段直接提交本地事务,不阻塞业务,TPS 5000+。
10. 库存分配策略 支持固定分配和动态分配两种模式。固定分配由商家手动配置,动态分配根据销量占比自动分配。超卖率从 5% 降到 0.1%,库存周转率提升 30%。
七、面试逐字稿
7.1 系统介绍
面试官:介绍一下你负责的 PIM+OMS 系统。
我的回答:
我负责的是 PIM+OMS 商品订单管理系统,这个系统由两个紧密关联的子系统组成。
PIM 是商品信息管理系统,主要解决跨境电商的商品管理问题。我们支持 SPU/SKU 两级商品体系,商家只需要配置规格选项,系统自动生成所有 SKU 组合。我们还集成了 AI 翻译,商家填写中文内容后,系统自动翻译成 15+ 语言,翻译成本降低 99.9%。
OMS 是订单管理系统,主要解决多平台订单统一管理的问题。商家在 Amazon、Shopify、eBay 等 10+ 平台开店,我们通过 Webhook 和定时任务两种方式集成订单,统一管理。我们支持订单自动创建、状态流转、风险控制、超时预警、拆分合并等全流程业务。
这个系统在整个供应链中的位置是核心。上游是 SRM 供应商管理系统,提供商品的供应商信息和采购成本。下游是 WMS 仓储管理系统,订单创建时冻结库存,订单发货时扣减库存。还有 TMS 物流管理系统,订单发货后创建运单。
日均处理订单 10 万+,峰值 QPS 5000+,管理 50 万+ SPU、300 万+ SKU,支持 20+ 国家和地区。
7.2 技术亮点
面试官:说说这个系统有什么技术亮点。
我的回答:
这个系统有三个核心技术亮点:SPU/SKU 两级商品体系 + 笛卡尔积算法、订单幂等性设计 + 分布式锁、分布式事务 Seata AT 模式。
第一个亮点是 SPU/SKU 两级商品体系 + 笛卡尔积算法。
问题是跨境电商的商品规格组合爆炸,一件 T恤有 3 种颜色 × 3 种尺码 = 9 个 SKU,手动创建效率低。而且公共属性冗余存储,品牌、材质、HS 编码这些属性对所有 SKU 都一样,如果每个 SKU 都存一遍,数据冗余严重。
我们的方案是设计了 SPU/SKU 两级商品体系。SPU 存储公共属性,SKU 存储规格属性。商家只需要在 SPU 层面配置规格选项,系统使用笛卡尔积算法自动生成所有 SKU 组合。
具体实现是从第一个规格开始,逐个规格展开,每次展开都生成新的组合。时间复杂度是 O(n1 × n2 × … × nk),实际场景中 SKU 数量一般在 100 以内,性能完全没问题。
效果是商家创建商品的时间从 10 分钟降到 2 分钟,效率提升 80%。公共属性只存一份,修改时只需要改 SPU,所有 SKU 自动生效。
第二个亮点是订单幂等性设计 + 分布式锁。
问题是订单创建面临三个幂等性问题:平台重复推送、用户重复提交、定时任务重复拉取。如果不做幂等性控制,会导致订单重复创建、库存重复冻结、财务数据错误。
我们的方案是使用”平台 + 平台订单号”作为唯一标识,三层防护:Redis 分布式锁、业务层查重、数据库唯一索引。
具体实现是创建订单前,先获取分布式锁,使用 Redis SETNX,TTL 10 秒。获取锁成功后,查询订单是否已存在,如果存在直接返回。如果不存在,创建订单。数据库创建唯一索引,防止重复插入。最后释放分布式锁。
效果是同一个订单重复推送 100 次,只创建 1 次,幂等性 100%。分布式锁防止并发创建,冲突率 0%。
第三个亮点是分布式事务 Seata AT 模式。
问题是订单创建涉及多个系统的数据操作:OMS 创建订单、WMS 冻结库存、PMS 更新商品销量。这三个操作必须保证原子性:要么全部成功,要么全部失败。
我们的方案是使用 Seata AT 模式实现分布式事务。Seata AT 模式原理是两阶段提交 + 自动回滚。一阶段执行业务 SQL,Seata 自动记录 undo_log,提交本地事务。二阶段提交时删除 undo_log,二阶段回滚时根据 undo_log 自动回滚数据。
具体实现是在订单创建方法上加 @GlobalTransactional 注解,Seata 自动管理分布式事务。OMS 创建订单,WMS 冻结库存,PMS 更新销量,Seata 自动记录 undo_log。所有操作成功,Seata 二阶段提交。如果任何一步失败,Seata 二阶段回滚。
效果是订单和库存强一致性,一致性保证 100%。一阶段直接提交,不阻塞业务,TPS 5000+。无需手动编写补偿逻辑,开发效率提升 80%。