Series Article

Day05 · PIM + OMS 核心业务面试准备

核心业务流程

商品管理(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:

  1. 黑色-无线版
  2. 黑色-有线版
  3. 白色-无线版
  4. 白色-有线版
  5. 蓝色-无线版
  6. 蓝色-有线版

笛卡尔积算法实现:

@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(语言代码)三字段唯一索引。

实现流程:

  1. 运营人员录入中文内容

    • 在 SaaS 平台的商品管理页面,运营人员只需要录入一种语言的商品信息(通常是中文)
    • 填写商品标题、描述、卖点等内容
    • 保存到 product_i18n 表,lang_code='zh-CN'
  2. 选择需要翻译的语言

    • 录入成功后,运营人员可以选择是否需要进行 AI 翻译
    • 勾选需要翻译的语种:英语、日语、韩语、德语等
    • 点击”一键翻译”按钮
  3. 调用 DeepSeek API 生成翻译

    • SaaS 平台从数据库查询中文内容
    • 构建翻译 Prompt(包含翻译要求和商品信息)
    • 调用 DeepSeek API(或 OpenAI API)进行翻译
    • 解析返回的 JSON 结果
  4. 保存翻译内容到数据库

    • 将翻译后的标题、描述、关键词保存到 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 是在代码中动态生成的,包含以下部分:

  1. 系统提示词:定义 AI 的角色(专业翻译员)
  2. 翻译要求:标题、描述、关键词的具体要求
  3. 商品信息:从数据库查询的中文内容
  4. 输出格式:要求返回 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 系统中修改价格后,系统会自动同步到各个电商平台:

同步流程:

  1. 运营人员在 SaaS 后台修改价格(比如把 Amazon 美国站的价格从 19.99 美元改成 17.99 美元)
  2. 系统保存新价格到数据库
  3. 系统调用对应平台的 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 接口
  4. 平台返回更新结果,系统记录同步状态
  5. 如果同步失败,系统会重试 3 次,仍然失败则发送告警通知运营人员

同步时机:

  • 实时同步:运营人员点击”保存并同步”按钮,立即同步到平台
  • 定时同步:每天凌晨 2 点,系统自动检查所有价格变更,批量同步到平台
  • 手动同步:运营人员可以在后台手动触发同步,用于修复同步失败的情况

4. 价格计算逻辑

当客户在电商平台下单时,系统需要计算最终售价:

计算步骤:

  1. 根据订单的 SKU ID、平台、国家,查询对应的价格配置
  2. 判断是否有促销价:
    • 如果有促销价且在有效期内,使用促销价
    • 否则使用基础价格
  3. 计算含税价格:
    • 最终售价 = 价格 × (1 + 税率)
    • 比如基础价格 100 美元,美国税率 7%,最终售价 = 100 × 1.07 = 107 美元
  4. 四舍五入到 2 位小数

举例说明:

假设一个蓝牙耳机的成本价是 50 元人民币:

平台国家货币加价率基础价格税率最终售价
Amazon美国USD20%$8.577%$9.17
Shopify美国USD50%$10.717%$11.46
Amazon日本JPY20%¥1,02810%¥1,131
Amazon德国EUR20%€7.7119%€9.17

可以看到,同一个商品在不同平台、不同国家的售价是完全不同的。

这个定价策略对租户有什么用?

  1. 提高定价效率:不需要在每个平台单独设置价格,在 SaaS 系统中统一管理,一键同步到所有平台
  2. 灵活的定价策略:可以根据不同平台的竞争情况,设置不同的加价率,最大化利润
  3. 自动计算含税价格:不需要手动计算税率,系统自动根据国家计算含税价格,避免出错
  4. 支持促销活动:可以设置促销价格和有效期,到期后自动恢复原价
  5. 价格变更可追溯:所有价格变更都有记录,可以查看历史价格,分析价格对销量的影响

如果 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:风控检查

对订单进行风险评估,防止欺诈订单。

我们的风控规则引擎会检查以下几个维度:

  1. 地址有效性验证:收货地址是否真实存在,是否是高风险地区
  2. 金额异常检测:订单金额是否异常(比如单价 10 元的商品,下单 1000 件)
  3. 高频下单检测:同一个买家在短时间内下了多个订单(可能是刷单)
  4. 支付方式检测:是否使用高风险支付方式(比如货到付款)
  5. 买家信用检测:买家是否有退款、拒收等不良记录

每个规则会给出一个风险分数(0-100),最终汇总成总风险分数:

  • 风险分数 ≤ 70:自动放行,进入下一步
  • 风险分数 > 70:标记为”风控审核”状态,需要人工审核

步骤 6:订单状态流转

根据风控结果,订单进入不同的状态:

  • 如果风控通过,订单状态变为”待备货”
  • 如果需要人工审核,订单状态变为”风控审核”
  • 如果库存不足,订单状态变为”缺货挂起”

步骤 7:推送到 WMS

如果订单通过了库存检查和风控检查,就可以开始备货了。

  • 系统发送 MQ 消息给 WMS(仓储管理系统)
  • 消息内容包含:订单号、SKU、数量、收货地址、仓库 ID
  • WMS 收到消息后,创建出库单,开始拣货流程
  • 拣货完成后,WMS 通知 OMS 订单状态变为”待发货”

整个流程的时间线:

  • 订单同步:实时(Webhook)或 5 分钟内(定时拉取)
  • 签名验证 + 幂等校验 + 格式转换:< 100ms
  • 库存检查 + 风控检查:< 500ms
  • 推送到 WMS:< 100ms(异步消息)
  • 总耗时:< 1 秒(不包括人工审核)

为什么要这样设计?

  1. 签名验证:防止恶意请求,保证订单来源的真实性
  2. 幂等校验:防止重复订单,避免重复发货和库存错乱
  3. 格式转换:统一数据格式,方便后续处理,同时保留原始数据方便排查问题
  4. 库存检查:避免超卖,保证订单能够正常发货
  5. 风控检查:防止欺诈订单,降低损失
  6. 异步处理:Webhook 必须在 3 秒内返回,所以业务逻辑必须异步处理
  7. 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),任何人都可以访问。如果不验证签名,会有以下风险:

  1. 恶意用户伪造订单:攻击者可以伪造大量订单,导致系统创建虚假订单,库存被错误冻结
  2. 数据被篡改:中间人攻击可能修改订单金额、收货地址等信息
  3. 重放攻击:攻击者截获真实的 Webhook 请求,反复发送,导致重复订单
  4. 系统资源浪费:大量恶意请求会消耗系统资源,影响正常业务

所以我们必须验证每个 Webhook 请求确实来自平台,且内容没有被篡改。

签名验证的完整交互流程:

第一步:平台配置(在电商平台后台)

运营人员在电商平台的后台配置 Webhook:

  1. 填写 Webhook URL:https://our-saas.com/webhook/shopify
  2. 选择要订阅的事件:订单创建、订单支付、订单取消等
  3. 平台生成一个 App Secret(密钥),比如:sk_abc123xyz456
  4. 运营人员把这个 App Secret 保存到我们的系统配置中

第二步:平台发送 Webhook(平台端)

当订单创建时,平台会执行以下步骤:

  1. 准备请求体:把订单数据转换成 JSON 格式

    {
      "id": "12345678",
      "email": "customer@example.com",
      "total_price": "99.99",
      "currency": "USD",
      ...
    }
  2. 计算签名:用 App Secret 对请求体进行 HMAC-SHA256 加密

    • 输入:请求体的原始字符串(不能格式化,必须是原始的 JSON 字符串)
    • 密钥:App Secret(sk_abc123xyz456
    • 算法:HMAC-SHA256
    • 输出:签名字符串(Base64 编码),比如:a3f5b2c8d1e9f4a7b6c5d8e1f2a3b4c5...
  3. 发送 HTTP 请求

    • URL:https://our-saas.com/webhook/shopify
    • Method:POST
    • Headers:
      • Content-Type: application/json
      • X-Shopify-Hmac-SHA256: a3f5b2c8d1e9f4a7b6c5d8e1f2a3b4c5...(签名放在请求头)
    • Body:订单 JSON 数据

第三步:我们接收 Webhook(我们的系统)

我们的系统收到 Webhook 请求后,执行以下步骤:

  1. 提取签名:从请求头中提取 X-Shopify-Hmac-SHA256 的值,这是平台发送的签名

    received_signature = "a3f5b2c8d1e9f4a7b6c5d8e1f2a3b4c5..."
  2. 读取请求体:读取 HTTP 请求的 Body,得到原始 JSON 字符串

    raw_body = "{\"id\":\"12345678\",\"email\":\"customer@example.com\",...}"
  3. 计算期望签名:用相同的 App Secret 和算法,对请求体进行 HMAC-SHA256 加密

    • 输入:raw_body(必须是原始字符串,不能解析成对象后再转回字符串)
    • 密钥:App Secret(从配置中读取:sk_abc123xyz456
    • 算法:HMAC-SHA256
    • 输出:expected_signature
  4. 对比签名

    if (received_signature == expected_signature) {
        // 签名验证通过,请求合法
        立即返回 200 OK
        把请求体放到 MQ,异步处理
    } else {
        // 签名验证失败,请求非法
        记录安全日志(包含 IP、时间、签名)
        返回 400 Bad Request
    }

第四步:异步处理业务逻辑(MQ 消费者)

为什么要异步处理?因为平台要求我们必须在 3 秒内返回 200,否则认为失败并重发。如果在 HTTP 请求内处理业务逻辑(查询数据库、调用其他服务),可能超过 3 秒,导致平台重发,产生重复订单。

所以我们的做法是:

  1. 签名验证通过后,立即返回 200(耗时 < 100ms)
  2. 把请求体放到 MQ(RocketMQ)
  3. 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. 不同平台的签名算法差异:

平台签名算法签名位置编码方式
ShopifyHMAC-SHA256X-Shopify-Hmac-SHA256Base64
TikTok ShopHMAC-SHA256AuthorizationHex
PayPalSHA-256Paypal-Transmission-SigBase64

所以我们需要为每个平台实现不同的签名验证逻辑。

实际案例: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 分钟的请求。


订单幂等设计(防止重复订单)

为什么需要幂等?

在分布式系统中,订单重复创建是一个非常常见的问题。导致重复订单的原因有很多:

  1. Webhook 重发:平台推送订单时,如果我们的响应超时(超过 3 秒),平台会认为失败并重发
  2. 网络抖动:网络不稳定导致请求重复发送
  3. 平台故障:平台系统故障可能导致同一个订单被推送多次
  4. 定时任务重复拉取:定时任务拉取订单时,可能因为时间窗口重叠,拉取到重复的订单
  5. 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 万个订单(实际上不会有这么高的并发)

幂等设计的关键点:

  1. 唯一标识的选择

    • 不能只用平台订单号,因为不同平台可能有相同的订单号
    • 必须用”平台 + 平台订单号”作为唯一标识
  2. 数据库唯一索引是最后一道防线

    • 即使业务层幂等校验失败,数据库唯一索引也能保证不会插入重复数据
    • 唯一索引冲突时,捕获异常,返回成功(因为订单已存在)
  3. 状态同步很重要

    • 不能简单地忽略重复订单,要检查状态是否有更新
    • 平台订单状态变化时,我们也要同步更新
  4. 分布式锁的必要性

    • 如果并发量不高(每秒几百个订单),可以不用分布式锁,唯一索引就够了
    • 如果并发量很高(每秒几千个订单),建议用分布式锁,减少数据库压力

实际案例:

假设 Shopify 推送了一个订单,订单号是 SHP-12345

第一次推送(订单创建):

  • 平台:SHOPIFY
  • 平台订单号:SHP-12345
  • 订单状态:待支付
  • 处理结果:查询数据库,订单不存在,创建新订单,内部订单号 ORD20260522143025123456

第二次推送(订单支付):

  • 平台:SHOPIFY
  • 平台订单号:SHP-12345
  • 订单状态:已支付
  • 处理结果:查询数据库,订单已存在,检查状态,发现状态从”待支付”变成”已支付”,更新本地状态

第三次推送(网络重发):

  • 平台:SHOPIFY
  • 平台订单号:SHP-12345
  • 订单状态:已支付
  • 处理结果:查询数据库,订单已存在,检查状态,状态没有变化,直接返回成功

面试时怎么讲:

我们用平台加平台订单号作为唯一标识,在数据库中创建唯一索引,保证同一个平台订单号只会创建一次。

收到订单后,先查询数据库,如果订单已存在,检查状态是否有更新。如果状态有更新,比如从待支付变成已支付,我们就同步更新本地状态。如果状态没有变化,直接返回成功。如果订单不存在,才创建新订单。

这样可以保证幂等性,即使平台重复推送,也不会创建重复订单。我们还会生成内部订单号,格式是 ORD 加时间戳加 6 位随机数,方便统一管理。

如果并发量很高,我们还会用 Redis 分布式锁,保证同一时刻只有一个线程处理同一个订单,减少数据库压力。锁的 Key 是订单的平台加平台订单号,过期时间 10 秒,处理完成后释放锁。

数据库唯一索引是最后一道防线,即使业务层幂等校验失败,数据库也能保证不会插入重复数据。


订单状态机设计

为什么需要状态机?

订单从创建到完成,会经历多个状态:待处理 → 风控审核 → 待备货 → 备货中 → 待发货 → 已发货 → 运输中 → 已签收 → 已完成。

如果不用状态机,状态流转逻辑会散落在各个业务方法中:

  • 创建订单的方法里,要判断是否需要风控审核
  • 风控审核的方法里,要判断审核通过后应该变成什么状态
  • 发货的方法里,要判断当前状态是否允许发货
  • 取消订单的方法里,要判断哪些状态可以取消

这样会导致:

  1. 状态流转逻辑分散:同一个状态的流转规则可能出现在多个方法中,难以维护
  2. 容易出现非法状态:比如订单从”待处理”直接跳到”已发货”,跳过了备货环节
  3. 难以扩展:新增一个状态或流转规则,需要修改多个方法
  4. 难以追溯:无法清晰地看到订单的完整状态流转路径

状态机的核心思想:

状态机是一种设计模式,用于管理对象的状态流转。它有三个核心概念:

  1. 状态(State):对象可能处于的各种状态,比如订单的 10 个状态
  2. 事件(Event):触发状态流转的动作,比如”通过风控”、“开始备货”、“发货”
  3. 转换(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 后置动作
  • 更新订单状态到数据库

状态机的优势:

  1. 集中管理:所有状态流转规则都在一个配置类中,一目了然
  2. 保证合法性:状态机会自动拦截非法流转,比如从”待处理”直接跳到”已发货”
  3. 易于扩展:新增状态或流转规则,只需要修改配置,不需要改业务代码
  4. 自动化操作:状态流转时自动执行 Action,比如发货时自动扣减库存、创建运单
  5. 可追溯:可以记录每次状态流转的时间、操作人、原因,方便排查问题

实际案例:订单从创建到完成的完整流程

步骤 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 + 仓库 + 平台”维护三个库存数量:

  1. 分配数量(allocated_qty):分配给该平台的总库存

    • 比如 Amazon 分配了 200 件
  2. 冻结数量(frozen_qty):已下单但未发货的库存

    • 比如有 50 件订单正在备货,还没发货
  3. 可售数量(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 件(实物出库)

步骤 7:动态调整分配(可选)

  • 运营人员发现 Amazon 卖得很快,库存快用完了
  • 而 eBay 卖得慢,还有很多库存
  • 运营人员可以把 eBay 的 50 件调给 Amazon:
    • Amazon: 350 + 50 = 400 件
    • eBay: 200 - 50 = 150 件
  • 系统自动同步到各平台的 API

库存同步到电商平台的机制:

同步时机:

  1. 初次分配:运营人员首次为平台分配库存时,立即同步
  2. 动态调整:运营人员调整分配比例时,立即同步
  3. 定时同步:每小时自动同步一次,作为兜底机制
  4. 手动同步:运营人员可以手动触发同步,用于修复同步失败的情况

同步逻辑:

系统会计算每个平台的”可售数量”,然后调用平台 API 更新:

可售数量 = 分配数量 - 冻结数量

比如:
- Amazon 分配了 400 件
- 有 50 件订单正在备货(冻结)
- 可售数量 = 400 - 50 = 350 件
- 调用 Amazon SP-API 更新库存为 350 件

同步接口:

平台API 接口说明
AmazonSP-API updateInventory更新 FBA 或 FBM 库存
ShopifyREST API PUT /admin/api/2024-01/inventory_levels/set.json更新库存数量
eBayTrading API ReviseInventoryStatus更新 Listing 库存
TikTok ShopOpen 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 异步同步到电商平台,如果同步失败会自动重试。

这样做的好处是:避免超卖,灵活调整分配策略,统一管理所有平台的库存,实时监控库存情况,数据可追溯。


订单风控规则引擎(策略模式)

为什么需要风控?

跨境电商存在大量欺诈订单和风险订单:

  1. 虚假地址:收货地址不存在,货物无法送达,导致退货损失
  2. 盗刷信用卡:使用盗刷的信用卡下单,货物发出后银行拒付,商家损失货款和货物
  3. 恶意刷单:刷手大量下单刷好评,被平台发现后会处罚商家
  4. 恶意退款:买家收到货后恶意申请退款,商家损失货物
  5. 测试订单:竞争对手下测试订单,探测商家的价格和库存

如果不做风控,会导致:

  • 货物发出后无法签收,退货成本高
  • 信用卡拒付,商家损失货款和货物
  • 平台处罚,店铺被封
  • 恶意退款,商家损失

所以我们需要在订单创建后、发货前进行风控检查,识别高风险订单,人工审核后再决定是否发货。

风控规则设计(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;
    }
}

策略模式的优势:

  1. 规则可插拔:新增规则只需实现 RiskRule 接口,不需要修改风控服务
  2. 规则可配置:每个规则的分值可以动态调整,不需要改代码
  3. 规则可测试:每个规则都是独立的类,可以单独编写单元测试
  4. 规则可追溯:每次风控检查都会记录命中的规则和分值,方便排查问题
  5. 规则可扩展:可以轻松添加新的规则类型,比如”买家信用评分”、“设备指纹识别”

实际案例:

假设一个订单的风控检查过程:

订单信息:

  • 买家邮箱:test@example.com
  • 收货地址:美国加州,邮编 12345(无效邮编)
  • 订单金额:$999.99
  • 历史平均金额:$50
  • 1 小时内下单次数:6 次

风控检查过程:

  1. 地址有效性规则:邮编 12345 无效(加州邮编应该是 9xxxx),返回 20 分
  2. 金额异常规则:订单金额 $999.99 是历史均值 $50 的 20 倍,返回 30 分
  3. 高频下单规则:1 小时内下单 6 次,返回 25 分
  4. 同地址大量下单规则:该地址 24 小时内只有 1 单,返回 0 分
  5. 高退款率买家规则:该买家历史退款率 10%,返回 0 分

风控结果:

  • 总分:20 + 30 + 25 = 75 分
  • 风险等级:高风险(> 70 分)
  • 命中规则:地址有效性(20分)、金额异常(30分)、高频下单(25分)
  • 处理方式:进入人工审核队列,运营专员审核后决定是否发货

面试时怎么讲:

我们设计了 5 类风控规则:地址有效性、金额异常、高频下单、同地址大量下单、高退款率买家。每个规则对应一个分值,总分 0-30 分为低风险,31-70 分为中风险,71 分以上为高风险。高风险订单会进入人工审核队列。

技术实现上,我们用策略模式设计风控规则引擎。定义一个 RiskRule 接口,每个规则实现这个接口,evaluate 方法返回风险分值。风控服务通过 Spring 自动注入所有规则,遍历执行,累加分值,最终得到总风险评分。

这样做的好处是:规则可插拔,新增规则只需实现接口,不需要修改风控服务;规则可配置,每个规则的分值可以动态调整;规则可测试,每个规则都是独立的类,可以单独编写单元测试;规则可追溯,每次风控检查都会记录命中的规则和分值。


订单超期预警系统

为什么需要超期预警?

订单在某个状态停留时间过长,说明可能出现了问题:

  1. 待处理超时:订单创建后长时间没有进入备货流程,可能是系统故障或库存不足
  2. 风控审核超时:订单在风控审核队列中长时间没有处理,可能是运营人员忘记审核
  3. 待发货超时:订单拣货完成后长时间没有发货,可能是仓库忘记扫码发货
  4. 运输中超时:订单发货后长时间没有签收,可能是物流异常或地址错误

如果不及时预警,会导致:

  • 客户投诉,影响店铺评分
  • 平台处罚,扣除保证金
  • 订单取消,损失销售额
  • 客户流失,影响复购率

所以我们需要及时预警,让运营人员及时处理异常订单。

预警规则设计:

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 个订单,处理完再查询下一批
  • 索引优化:在 statusstatus_update_time 字段上创建联合索引
  • 异步处理:把超期订单放到 MQ,异步发送通知

3. 避免重复预警

如果一个订单已经发送过预警,不应该每小时都发送一次。

解决方案:

  • order_timeout_log 表中记录已发送的预警
  • 查询时排除已发送过相同级别预警的订单
  • 或者:只在预警级别升级时发送(比如从”预警”升级到”紧急”)

4. 预警通知的降级

如果短信服务故障,不应该影响整个预警系统。

解决方案:

  • 邮件发送失败:记录日志,继续发送短信
  • 短信发送失败:记录日志,继续发送电话
  • 所有通知方式都失败:记录到数据库,人工处理

实际案例:

假设有一个订单在”待发货”状态超期了:

订单信息:

  • 订单号:ORD20260522143025123456
  • 当前状态:待发货
  • 状态更新时间:2026-05-20 10:00:00
  • 当前时间:2026-05-22 14:00:00
  • 停留时长:52 小时

超期检查过程:

  1. 定时任务触发:2026-05-22 14:00:00,定时任务开始执行
  2. 查询未完成订单:查询到该订单,状态是”待发货”
  3. 计算停留时长:52 小时
  4. 判断是否超期:待发货的阈值是 24 小时,52 > 24,超期
  5. 判断预警级别:52 小时在 48-72 小时之间,紧急级别
  6. 发送预警通知:发送邮件 + 短信给运营人员
  7. 记录预警日志:保存到 order_timeout_log

运营人员处理:

  • 收到短信后,登录后台查看订单详情
  • 发现仓库已经拣货完成,但忘记扫码发货
  • 联系仓库,立即扫码发货
  • 订单状态变为”已发货”,不再超期

面试时怎么讲:

我们设计了订单超期预警系统,定时检查订单在当前状态的停留时长。

每个状态都有超期阈值,比如待处理 2 小时、待发货 24 小时、运输中 15 天。超过阈值后,根据停留时长分为 3 级预警:24-48 小时是预警级别,48-72 小时是紧急级别,72 小时以上是严重级别。

预警级别越高,通知方式越强:预警级别发邮件,紧急级别发邮件加短信,严重级别发邮件加短信加电话。

技术实现上,我们用 XXL-Job 定时任务每小时执行一次检查,查询所有未完成订单,计算停留时长,判断是否超期。超期订单会记录到预警日志表,方便后续分析。为了避免重复预警,我们会查询预警日志,排除已发送过相同级别预警的订单。

这样做的好处是:及时发现异常订单,避免客户投诉;提高订单处理效率,减少人工巡检成本;可以分析哪些状态容易超期,优化流程。


订单拆单与合单规则

为什么需要拆单?

一个订单可能包含多个商品,这些商品可能面临以下情况:

  1. 分布在不同仓库:商品 A 在北京仓,商品 B 在上海仓,如果等两个仓库都备货完成再发货,会延长发货时间
  2. 物流限制:含电池商品不能空运,只能海运或陆运,需要和普通商品分开发货
  3. 超重或超体积:单个包裹超过 30kg 或体积超过 1 立方米,物流公司不接收,需要拆分成多个包裹
  4. 平台要求:某些平台要求不同类目的商品分开发货

如果不拆单,会导致:

  • 发货时间延长,影响客户体验
  • 物流成本增加,需要跨仓调拨
  • 物流公司拒收,无法发货
  • 违反平台规则,被处罚

拆单的 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 立方米,需要拆分

拆分算法(贪心算法):

  1. 按商品重量从大到小排序
  2. 依次把商品放入当前包裹
  3. 如果放入后超过限制,创建新包裹
  4. 重复步骤 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
  • 三个包裹独立发货,互不影响

为什么需要合单?

合单是拆单的反向操作,适用于以下场景:

  1. 同一买家多次下单:买家在短时间内下了多个订单,商品都在同一个仓库,可以合并发货,降低物流成本
  2. 平台促销活动:买家参加”满减”活动,分多次下单凑单,可以合并发货
  3. 降低物流成本:多个小订单合并成一个大订单,物流费用更低

合单的触发条件:

  • 同一买家
  • 同一收货地址
  • 同一仓库
  • 订单状态都是”待备货”(还没开始拣货)
  • 订单创建时间间隔 < 2 小时

合单的流程:

  1. 定时任务每 10 分钟扫描一次”待备货”订单
  2. 按买家 + 收货地址 + 仓库分组
  3. 如果同一组有多个订单,且创建时间间隔 < 2 小时,触发合单
  4. 创建一个新的父订单,包含所有商品
  5. 原订单标记为”已合单”,关联到新的父订单
  6. 新的父订单进入备货流程

拆单与合单的技术实现关键点:

1. 拆单时机

  • 在订单创建后、备货前进行拆单
  • 如果已经开始备货,不能拆单(会导致库存混乱)

2. 子订单的独立性

  • 每个子订单有独立的订单号、独立的发货流程
  • 子订单可以独立取消、独立退款
  • 子订单的状态变更不影响其他子订单

3. 父订单的状态同步

  • 子订单状态变更时,自动更新父订单状态
  • 父订单状态 = 所有子订单中最落后的状态
  • 用数据库触发器或 MQ 消息实现自动同步

4. 拆单算法的优化

  • 贪心算法:优先选择距离最近的仓库,降低物流成本
  • 动态规划:如果有多个仓库都有库存,选择最优组合,使总物流成本最低
  • 缓存优化:仓库距离、商品重量等信息缓存到 Redis,避免重复查询

面试时怎么讲:

订单拆单有 4 种触发规则:跨仓库、物流限制、超重超体积、平台要求。

拆单流程是:先按仓库分组,然后检查每组是否需要进一步拆分。比如含电池商品不能和普通商品一起空运,需要单独发货;单个包裹超过 30kg,需要拆分成多个包裹。

拆单后,系统会创建多个子订单,每个子订单有独立的订单号、独立的发货流程。子订单通过 parent_order_id 关联父订单。父订单状态等于所有子订单中最落后的状态,比如有 3 个子订单,2 个已发货,1 个待发货,父订单状态就是待发货。

合单是拆单的反向操作,适用于同一买家短时间内下了多个订单的场景。定时任务每 10 分钟扫描一次待备货订单,按买家加收货地址加仓库分组,如果同一组有多个订单且创建时间间隔小于 2 小时,就触发合单。

这样做的好处是:提高发货效率,不同仓库可以并行发货;降低物流成本,避免跨仓调拨;满足物流限制,保证合规发货。


模块间调用技术

为什么需要模块间调用?

OMS(订单管理系统)不是孤立的,它需要和其他系统协作完成业务:

  1. 和 WMS(仓储管理)交互

    • 订单创建时,需要冻结库存(防止超卖)
    • 订单发货时,需要扣减库存
    • 订单取消时,需要释放库存
  2. 和 TMS(物流管理)交互

    • 订单发货时,需要创建运单
    • 物流状态更新时,需要同步订单状态
  3. 和 FMS(财务管理)交互

    • 订单完成时,需要触发财务结算
    • 订单退款时,需要创建退款单
  4. 和 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 模式的原理:

  1. 阶段一(Try):执行业务操作

    • OMS 创建订单,Seata 自动记录 undo_log(回滚日志)
    • WMS 冻结库存,Seata 自动记录 undo_log
    • 如果任何一步失败,Seata 自动回滚
  2. 阶段二(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"}

具体实现:

  1. SPU 表设计:存储 SPU ID、品牌、材质、HS 编码等共性属性
  2. SKU 表设计:存储 SKU ID、SPU ID、规格值(JSON)、价格、库存等
  3. 笛卡尔积算法:递归遍历所有规格项,生成所有可能的组合
  4. 批量插入:一次性插入所有 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 接口是公开的,存在被恶意请求伪造订单的风险。如果不验证签名,黑客可以伪造订单,导致库存被恶意占用、发货地址被篡改。

方案:

  1. 设计统一的订单接入适配层

    • 定义统一的内部订单模型
    • 每个平台实现一个适配器,负责格式转换
    • 支持主动拉取(定时任务)和被动接收(Webhook)两种方式
  2. 实现 Webhook 签名验证机制

    • 使用 HMAC-SHA256 算法验证签名
    • 每个平台有独立的密钥(Secret)
    • 签名验证失败,直接拒绝请求

具体实现:

  1. 适配器模式:每个平台实现 OrderAdapter 接口,包含 parse()verify() 方法
  2. 签名验证流程
    • 从请求头获取平台签名(比如 X-Shopify-Hmac-SHA256
    • 用平台密钥对请求体进行 HMAC-SHA256 加密
    • 对比计算出的签名和平台签名,一致则通过
  3. 幂等性保证:用”平台 + 平台订单号”作为唯一标识,数据库唯一索引防止重复插入
  4. 异步处理: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 个状态(待处理、风控审核、待备货、备货中、待发货、已发货、运输中、已签收、已完成、已取消),状态流转逻辑复杂,容易出现非法状态流转。比如订单从”待处理”直接跳到”已发货”,跳过了备货环节,导致库存没有扣减。

同时跨境电商存在大量欺诈订单(盗刷信用卡、虚假地址、恶意下单),如果不进行风控检查,会导致货物发出后无法签收,造成损失。

方案:

  1. 使用 Spring State Machine 实现订单状态机

    • 定义 10 个状态和 12 个事件
    • 配置状态流转规则(哪些状态可以流转到哪些状态)
    • 每个流转规则包含前置条件(Guard)和后置动作(Action)
    • 状态机自动拦截非法流转
  2. 设计风控规则引擎

    • 支持 5 类风控规则:地址有效性、金额异常、高频下单、黑名单、设备指纹
    • 采用策略模式实现规则可插拔
    • 每个规则返回风险分值,累加后判断是否需要人工审核

具体实现:

  1. 状态机配置

    • 定义状态枚举:PENDINGRISK_REVIEWWAIT_PREPARE
    • 定义事件枚举:PASS_RISK_CHECKSTART_PREPARESHIP
    • 配置流转规则:PENDINGRISK_REVIEW(事件:TRIGGER_RISK_REVIEW
    • 配置 Guard:判断订单金额是否超过 1000 元
    • 配置 Action:发货时自动调用 WMS 扣减库存、调用 TMS 创建运单
  2. 风控规则引擎

    • 定义规则接口 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 在上海仓,如果等两个仓库都备货完成再发货,会延长发货时间,影响客户体验。

同时订单拆单涉及多个操作:创建子订单、更新父订单状态、冻结库存。这些操作必须同时成功或同时失败,否则会导致数据不一致。

方案:

  1. 设计订单拆单算法

    • 按仓库分组:先按仓库分组,同一仓库的商品放在一起
    • 检查物流限制:含电池商品不能和普通商品一起空运,需要单独发货
    • 检查重量限制:单个包裹超过 30kg,需要拆分成多个包裹
    • 创建子订单:每个分组创建一个子订单,子订单通过 parent_order_id 关联父订单
  2. 使用 Seata AT 模式保证分布式事务

    • OMS 创建子订单(本地事务)
    • WMS 冻结库存(远程调用)
    • 如果任何操作失败,Seata 自动回滚所有操作

具体实现:

  1. 拆单流程

    • 步骤 1:按仓库分组
    • 步骤 2:检查每组是否需要进一步拆分(物流限制、重量限制)
    • 步骤 3:为每个分组创建子订单
    • 步骤 4:调用 WMS 冻结库存(Feign + Seata)
    • 步骤 5:更新父订单状态
  2. 分布式事务

    • 在拆单方法上加 @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 翻译后,运营可以手动调整

具体实现:

  1. 翻译 Prompt 设计

    你是一个专业的跨境电商文案翻译专家。请将以下商品信息翻译成{目标语言},
    要求:
    1. 保持电商文案风格,突出卖点
    2. 标题控制在 200 字符以内
    3. 描述要详细,包含使用场景
    4. 卖点要简洁有力,每条不超过 50 字符
    
    商品信息:
    标题:{原标题}
    描述:{原描述}
    卖点:{原卖点}
  2. 调用 OpenAI API

    • 使用 gpt-4 模型(翻译质量更高)
    • 设置 temperature=0.3(降低随机性,保证翻译稳定)
    • 设置 max_tokens=2000(限制返回长度)
  3. 异步翻译

    • 翻译任务放到 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 和算法计算期望签名,对比收到的签名和期望签名是否一致。

如果一致,说明:

  1. 请求确实来自平台(因为只有平台和我们知道 App Secret)
  2. 请求内容没有被篡改(因为签名是基于请求体计算的)

如果不一致,直接拒绝请求,返回 400 错误。

追问:签名验证失败后怎么处理?

答:记录安全日志(包含请求 IP、请求时间、请求体),返回 400 错误,拒绝请求。同时发送告警通知,提醒运营人员检查是否有恶意攻击。

如果短时间内出现大量签名验证失败(比如 1 分钟内 100 次),可能是:

  1. App Secret 泄露,需要立即更换
  2. 遭受 DDoS 攻击,需要启用 IP 黑名单
  3. 平台更新了签名算法,需要更新代码

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 创建运单,不需要在业务代码中手动调用。

好处:

  1. 状态流转逻辑集中管理,代码可维护性高
  2. 避免出现非法状态流转,保证数据一致性
  3. Guard 和 Action 解耦业务逻辑,提高代码复用性

追问:订单状态机如何持久化?

答:每次状态流转都会更新 order_main 表的 status 字段,同时写入 order_log 表记录操作日志。状态机本身是无状态的,状态存储在数据库中。这样即使服务重启,订单状态也不会丢失。


Q7: 供应链系统和电商平台是什么关系?

标准答案:

供应链系统是库存的”真实来源”,电商平台只是”展示渠道”。

商家在多个平台开店(Amazon、Shopify、eBay),但库存是统一管理的。我们的做法是固定分配库存,比如仓库有 500 件,分配给 Amazon 200 件、Shopify 150 件、eBay 100 件。

用户在 Amazon 下单后,Amazon 平台扣减自己的库存,然后推送订单到我们系统。我们接收订单后,冻结平台库存,等发货后再扣减分配库存和仓库库存。

供应链系统的价值:

  1. 多平台统一库存管理
  2. 全局库存可见性
  3. 智能库存分配
  4. 智能补货预警

追问:为什么不实时同步库存给平台?

答:如果把仓库的 500 件实时同步给每个平台,那么每个平台都显示 500 件,用户可能在多个平台同时下单,总订单量超过 500 件,导致仓库发不出货。所以我们采用固定分配策略,根据各平台销售速度,提前分配好每个平台的库存配额。


Q8: 退款是在哪里处理的?

标准答案:

退款是在电商平台处理的,不是在供应链平台。

买家在 Amazon 申请退款,商家在 Amazon 后台审核,Amazon 执行退款。我们的供应链系统只是接收退款通知,然后处理库存调整。

退款类型处理:

  1. 仅退款(未发货):释放冻结库存,让库存恢复可售
  2. 仅退款(已发货):货物不回来,我们不处理库存
  3. 退货退款:等待退货入库,质检后增加可售库存

所以供应链系统的职责是:接收退款通知 → 调整库存状态 → 记录退款信息。

追问:退货入库后如何处理?

答:退货入库后,WMS 会进行质检。质检合格的商品,增加可售库存;质检不合格的商品(破损、使用过),标记为次品,不能再销售。质检结果会同步到 OMS,更新退款记录。


Q9: 多语言内容如何管理?

标准答案:

我们用 pim_product_i18n 表存储多语言内容,表结构是 ref_type + ref_id + lang_code 三字段唯一索引。

  • ref_type 表示关联类型(SPU 或 SKU)
  • ref_id 表示关联 ID
  • lang_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

授权流程:

  1. 首次授权时,引导卖家到 Amazon 授权页面
  2. 授权后回调我们的接口,附带 code
  3. 我们用 code 换取 refresh_token,永久保存
  4. 每次调用 API 前,用 refresh_token 换取 access_token(有效期 1 小时)
  5. 如果 access_token 过期,自动刷新

拉取订单:

调用 GetOrders 接口,传入时间范围(CreatedAfter)和订单状态(OrderStatuses)。一次最多返回 100 条,需要用 NextToken 循环翻页。

追问:如果 refresh_token 过期怎么办?

答:refresh_token 理论上是永久有效的,但如果卖家在 Amazon 后台撤销授权,refresh_token 会失效。我们会捕获 401 错误,然后通知卖家重新授权。


Q11: 订单风控规则有哪些?

标准答案:

我们设计了 5 类风控规则:

  1. 地址有效性:邮政编码是否存在,国家-州组合是否合法
  2. 金额异常:单笔订单金额超过历史均值 10 倍,或单价明显低于成本价
  3. 高频下单:同一买家 1 小时内下 5 单以上(可能是刷单)
  4. 同地址大量下单:同一收货地址 24 小时内收到 10 单以上
  5. 高退款率买家:历史退款率超过 50% 的买家

每个规则对应一个分值,总分 0-30 分为低风险,31-70 分为中风险,71 分以上为高风险。

高风险订单会进入人工审核队列,运营专员 24 小时内处理。

追问:风控规则如何扩展?

答:我们用策略模式设计风控规则引擎。每个规则实现 RiskRule 接口,evaluate 方法返回风险分值。新增规则只需实现接口,注册为 Spring Bean,风控服务会自动扫描并执行。这样规则可插拔,无需修改核心代码。


Q12: 如何实现订单拆单?

标准答案:

拆单有 4 种触发规则:

  1. 跨仓库:订单商品分布在不同仓库,需要分开发货
  2. 物流限制:含电池商品不能与普通商品走同一渠道
  3. 超重/超体积:单个包裹超过物流商重量上限(30kg)
  4. 平台要求:某些平台要求每个包裹单独运单

拆单算法:

先按仓库分组,然后检查每组是否需要进一步拆分。比如含电池商品不能和普通商品一起空运,需要单独发货;单个包裹超过 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):执行业务操作

  1. OMS 创建订单,Seata 自动记录 undo_log(回滚日志)
  2. WMS 冻结库存,Seata 自动记录 undo_log
  3. 如果任何一步失败,Seata 自动回滚

阶段二(Confirm/Cancel):提交或回滚

  1. 如果所有操作都成功,Seata 提交全局事务,删除 undo_log
  2. 如果任何操作失败,Seata 根据 undo_log 自动回滚所有操作

undo_log 的作用:

undo_log 记录了操作前的数据快照。比如库存冻结前是 100,冻结后是 90,undo_log 记录”库存从 90 改回 100”。如果需要回滚,Seata 会执行 undo_log 中的 SQL,把数据恢复到操作前的状态。

AT 模式的优点:

  1. 侵入性低,只需要在方法上加 @GlobalTransactional 注解
  2. 自动记录 undo_log,不需要手动实现回滚逻辑
  3. 支持大部分数据库(MySQL、PostgreSQL、Oracle)

追问:AT 模式和 TCC 模式有什么区别?

答:AT 模式是自动化的,Seata 自动记录 undo_log,自动回滚。TCC 模式需要手动实现 Try、Confirm、Cancel 三个方法,侵入性高,但性能更好。

我们选择 AT 模式,因为开发成本低,性能满足业务需求。如果是高并发场景(比如秒杀),可以考虑 TCC 模式。


面试准备建议

必须能讲清楚的

  1. SPU/SKU 的区别:用生活化的例子讲清楚(比如蓝牙耳机)
  2. Webhook 签名验证原理:讲清楚 HMAC-SHA256 算法,讲清楚为什么要验证签名
  3. 为什么要立即返回 200:讲清楚超时重发的问题,讲清楚异步处理的好处
  4. 订单幂等性设计:讲清楚唯一索引、分布式锁、状态同步
  5. 订单状态机的好处:讲清楚集中管理、避免非法流转、Guard 和 Action
  6. 风控规则引擎的设计:讲清楚策略模式、规则可插拔、风险分值计算
  7. 订单拆单的触发规则:讲清楚 4 种规则,讲清楚拆单算法
  8. Feign vs MQ 的选择标准:讲清楚什么时候用 Feign,什么时候用 MQ
  9. Seata AT 模式的原理:讲清楚两阶段提交,讲清楚 undo_log 的作用
  10. 供应链系统的价值:不是解决超卖,而是统一管理多平台库存

表达技巧

  1. 先讲业务场景,再讲技术方案:不要上来就讲技术,先让面试官理解业务背景
  2. 用数据说话:效果提升 80%、成本降低 90%、日均处理 5 万+、拦截欺诈订单 3000+ 单/月
  3. 讲清楚为什么这样设计:不要只讲怎么做,要讲为什么这样做,有什么好处
  4. 准备追问:每个技术点都要准备 1-2 个追问,展示技术深度
  5. 用对比说明:比如 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+ 国家地区。

核心业绩:

  1. 使用 SPU + SKU 两级结构 + 笛卡尔积算法 实现了商品规格的批量化录入 达到了商品录入效率提升 80%,运营人员录入 100 个 SKU 的时间从 2 小时降到 20 分钟

    • 设计 SPU(标准产品单元)和 SKU(库存量单位)两级模型,SPU 维护共性属性(标题、品牌、类目、详情),SKU 维护差异属性(颜色、尺寸、价格、库存)
    • 用笛卡尔积算法自动生成 SKU 组合,比如颜色 5 种 × 尺寸 4 种 = 20 个 SKU,运营只需勾选规格即可批量生成
    • 支持手动调整:自动生成后可删除不存在的组合,逐个设置 SKU 属性,也可以批量设置默认值
  2. 使用 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 秒
  3. 使用 Webhook 签名验证 + HMAC-SHA256 + 异步消费 实现了多平台订单的安全接入 达到了订单接收成功率 99.95%,零安全事故

    • 实现 Amazon SP-API、Shopify、eBay、TikTok Shop 四大平台的 Webhook 接入,统一适配器模式
    • 签名验证:用 HMAC-SHA256 算法,每个平台独立 secret,原始请求体计算签名,防止请求伪造
    • 同步快速返回:Webhook 接收后 100ms 内返回 200,签名验证通过即把请求体放入 MQ
    • 异步处理:MQ 消费者做幂等校验、格式转换、入库,平台不重发的同时保证数据可靠性
  4. 使用 Redis 分布式锁 + 平台订单号唯一索引 实现了订单创建的幂等性保证 达到了重复订单率 0%,平台重发请求零误处理

    • 用”平台 + 平台订单号”作为唯一标识,建立数据库唯一索引,从存储层保证幂等
    • Redis 分布式锁(SETNX + 10 秒过期):防止同一订单并发处理,加锁失败等待 100ms 重试
    • 双重检查:加锁成功后再查数据库,订单已存在则比对状态,状态不同则更新,状态相同直接返回
    • 内部订单号生成:ORD + yyyyMMddHHmmss + 6 位随机数,时间戳保证可读性,随机数防止同秒冲突
  5. 使用 Spring State Machine 实现了订单状态机管理 达到了订单状态流转零异常,业务逻辑解耦 70%

    • 设计 10 个订单状态(待处理、风控审核、待备货、备货中、待发货、已发货、运输中、已签收、已完成、已取消)
    • 配置 State、Event、Transition、Guard、Action 五要素,比如待处理 + 通过风控 → 待备货
    • Guard 前置条件:订单金额超 1000 元自动触发风控审核,金额低则跳过
    • Action 后置动作:状态流转到已发货时自动调用 WMS 扣减库存、调用 TMS 创建运单
    • 状态机集中管理流转规则,避免业务代码中散落的 if-else 判断,新增状态只需修改配置
  6. 使用 策略模式 + 责任链 实现了订单风控规则引擎 达到了风控通过率 95%,欺诈订单识别率 92%

    • 设计 5 类风控规则:黑名单检查、订单金额阈值、收货地址异常、下单频率、IP 风险
    • 策略模式:每条规则实现 RiskRule 接口,独立计算风险评分(0-100 分),互不依赖
    • 责任链组合:所有规则串行执行,累加评分,超过 80 分触发人工审核,低于 30 分自动通过
    • 规则可配置:风控阈值存数据库,运营可在后台调整,无需改代码重新发布
    • 风控日志记录:每条规则的命中情况都记录到日志表,便于追溯和优化
  7. 使用 Feign + Seata AT 模式 实现了订单创建与库存冻结的分布式事务 达到了数据一致性 100%,零超卖

    • OMS 创建订单后通过 Feign 同步调用 WMS 冻结库存,Seata 全局事务保证原子性
    • AT 模式原理:阶段一执行业务并记录 undo_log,阶段二根据成功失败决定提交或回滚
    • 全局锁:Seata 在执行 update 前获取全局锁,防止脏写
    • 超时控制:默认 60 秒超时自动回滚,特殊业务可通过 @GlobalTransactional(timeoutMills) 调整
  8. 使用 RocketMQ 异步消息 实现了订单发货与库存扣减的解耦 达到了发货接口响应时间从 800ms 降到 50ms

    • 订单发货时 OMS 立即更新状态返回成功,发送 OrderShippedEvent 到 MQ
    • WMS 异步消费消息扣减库存,主流程不受 WMS 性能影响
    • 重试机制:消费失败重试 16 次,仍失败进入死信队列人工处理
    • 消息幂等:用订单号作为唯一标识,重复消息只处理一次
  9. 使用 XXL-Job 定时任务 + 三级预警 实现了订单超期预警系统 达到了订单超时率从 5% 降到 0.8%

    • 定时任务每 30 分钟扫描一次订单,按当前状态匹配预警规则
    • 三级预警:黄色(接近超时 80%)发邮件,橙色(已超时)发企业微信,红色(超时 2 倍)电话通知主管
    • 不同状态不同 SLA:待发货 24 小时、运输中 7 天、待签收 15 天
    • 预警记录:每次预警记录到 order_alert_log 表,便于复盘和考核
  10. 使用 拆单合单规则引擎 实现了多仓发货与买家合并发货 达到了物流成本降低 25%

    • 拆单触发:多仓库存、不同发货时效、平台限制(如 SKU 上限)、超大件商品
    • 合单触发:同一买家短时间多笔订单(30 分钟内)、同一收货地址、同一仓库发货
    • 拆单算法:贪心算法分配 SKU 到仓库,优先离收货地址近的仓库,最小化运费
    • 合单后用一个运单号、一个面单,物流成本从平均 25 元降到 18 元

最后提醒:

面试前把这份笔记看 2-3 遍,重点记住:

  • 商品的 SPU/SKU 模型和笛卡尔积算法
  • 订单接入的 7 个步骤(签名 → 幂等 → 转换 → 库存 → 风控 → 状态 → 推送)
  • 订单幂等的三层保证(唯一索引 + Redis 锁 + 双重检查)
  • 订单状态机的 5 要素(State、Event、Transition、Guard、Action)
  • 风控规则引擎的策略模式 + 责任链
  • 分布式事务的选型(Seata 强一致 vs MQ 最终一致)

面试时不要背书,要像讲故事一样自然地表达。如果面试官追问细节,可以展开讲技术实现。准备好画图,订单状态机图、订单幂等流程图、Seata 事务流程图、MQ 异步流程图一定要能画出来。