从API报错到本地拦截:电子面单快递公司前置校验改造

从API报错到本地拦截:电子面单快递公司前置校验改造

摘要:在多平台电子面单系统中,不同平台支持的快递公司各不相同。若运营选错快递公司,过去只能等到API调用后收到平台返回的模糊报错,排查费时费力。本文记录了如何通过引入 LogisticsSupportable 接口,在请求构建前实现前置校验,将错误拦截在本地,并将报错信息从"非法的参数"优化为"平台抖音不支持快递公司京东"的清晰中文提示。
📖 系列导航


文章目录


一、事故:一次"非法参数"引发的排查

那天测试抖音普通订单的取号流程,系统却报出了一个令人困惑的错误:

复制代码
获取运单号失败: 非法的参数

查看日志,抖音平台返回的是 "不支持该物流服务商"。顺着订单查下去,发现这个测试订单的快递公司选的是 京东物流(JD) ,而它走的却是 抖音取号 流程。

抖音平台压根不支持京东快递,这个请求从一开始就注定会失败。但我们的系统并没有在发出请求前检查这一点,傻傻地拼好 JSON、签好名,调用完 API 之后才拿到这个模糊的错误。

运营看到这样的提示,只能找开发:"帮我看看这个订单为什么失败?"------于是开发查日志、查平台文档、查订单数据......一次本可避免的排查浪费了半小时。


二、问题根源:缺少平台与快递的匹配校验

在多平台电子面单架构中,每个电商平台支持的快递公司列表是不同的。例如:

平台 支持的快递公司
抖音 顺丰、中通、圆通、申通、邮政
奇门(淘宝) 几乎所有快递
京东 京东物流

但改造前的代码中,从订单数据到请求构建,没有任何地方校验"这个平台能不能发这个快递"。错误只有在快递公司的 API 返回报错时才会暴露,而此时已经浪费了一次网络调用,且错误信息往往是英文或技术化的,业务人员完全看不懂。


三、改造方案:在请求构建前增加前置校验

3.1 设计思路

校验的核心是回答一个问题:当前平台是否支持订单指定的快递公司?

我们希望:

  • 校验逻辑与平台绑定:每个平台策略自己声明支持哪些快递。
  • 校验时机尽可能早:在请求构建之前就拦截,不浪费 API 调用。
  • 错误信息友好:用中文平台名称和快递名称,运营一眼能看懂。

但有一个现实约束:项目运行在 JDK 1.6 上,接口不能定义 default 方法。因此我们不能直接在 RequestStrategy 接口中加一个默认方法,需要另辟蹊径。

3.2 JDK 1.6 兼容方案:独立接口 + instanceof

我们设计一个独立的接口 LogisticsSupportable,只包含一个 supports 方法:

java 复制代码
public interface LogisticsSupportable {
    boolean supports(String logisticsCode);
}
  • 需要校验的平台 (如抖音普通、抖音代发):实现 LogisticsSupportable,在 supports 方法中返回支持的快递公司列表。
  • 无需校验的平台(如奇门,几乎所有快递都支持):不实现该接口,门面层默认放行。

门面层 WaybillFetchService 在获取策略后,通过 instanceof 判断是否需要校验:

java 复制代码
if (req instanceof LogisticsSupportable) {
    LogisticsSupportable checker = (LogisticsSupportable) req;
    if (!checker.supports(logisticsCode)) {
        // 抛出友好的业务异常
    }
}

这个方案完全兼容 JDK 1.6,不修改现有 RequestStrategy 接口,未来升级 JDK 后可将 supports 直接合并进接口的 default 方法。

🏭 设计模式视角LogisticsSupportable 接口的设计体现了接口隔离原则(ISP) ------它只定义了一个"是否支持某快递"的独立能力,没有强迫所有策略类都实现它。这种"可选接口"的模式,是策略模式的一种灵活变体,在《Java 23种设计模式:从踩坑到精通》系列的第22篇(策略模式)中有更深入的拆解,欢迎延伸阅读。


四、改造实施

4.1 新增 LogisticsSupportable 接口

java 复制代码
public interface LogisticsSupportable {
    boolean supports(String logisticsCode);
}

4.2 抖音策略实现 LogisticsSupportable

java 复制代码
public class DouYinRequestStrategy implements RequestStrategy, LogisticsSupportable {

    @Override
    public boolean supports(String logisticsCode) {
        return TocWmsSourcePlatFormType.PLAT_DY_SUPPORTED_LOGISTICS.contains(logisticsCode);
    }

    @Override
    public Object buildRequest(WaybillContext ctx) {
        return DouYinWaybillBuilder.buildRequest(ctx);
    }
}

其中支持的快递公司列表统一维护在常量类 TocWmsSourcePlatFormType 中:

java 复制代码
public static final Set<String> PLAT_DY_SUPPORTED_LOGISTICS = new HashSet<String>(
    Arrays.asList(
        TocWmsExpressType.SF_CODE,      // 顺丰
        TocWmsExpressType.ZTO_CODE,     // 中通
        TocWmsExpressType.YTO_CODE,     // 圆通
        TocWmsExpressType.STO_CODE,     // 申通
        TocWmsExpressType.POSTB_CODE    // 邮政
    )
);

4.3 门面层增加前置校验

WaybillFetchService.fetchWaybill 中,获取策略后立即校验:

java 复制代码
RequestStrategy req = strategyFactory.getRequestStrategy(platFormCode, platFormOriginal);
ParseStrategy parse = strategyFactory.getParseStrategy(platFormCode, platFormOriginal);
ExceptionStrategy ex = strategyFactory.getExceptionStrategy(platFormCode, platFormOriginal);

// 前置校验:检查平台是否支持该快递公司
if (req instanceof LogisticsSupportable) {
    LogisticsSupportable checker = (LogisticsSupportable) req;
    String logisticsCode = ticket.getLogisticsCode();
    if (!checker.supports(logisticsCode)) {
        String platFormName = TocWmsSourcePlatFormType.getPlatFormName(platFormCode);
        String logisticsName = TocWmsExpressType.getLogisticsName(logisticsCode);
        String errorMsg = "平台[" + platFormName + "]不支持快递公司[" + logisticsName + "]";
        logger.warn(errorMsg);
        throw new BusinessException(errorMsg);
    }
}

4.4 快递公司与平台名称中文化

为了让错误信息更友好,我们在 TocWmsExpressTypeTocWmsSourcePlatFormType 中分别增加了编码到中文名称的映射方法:

java 复制代码
// TocWmsExpressType 中
public static String getLogisticsName(String logisticsCode) {
    String name = LOGISTICS_NAME_MAP.get(logisticsCode);
    return name != null ? name : logisticsCode;
}

// TocWmsSourcePlatFormType 中
public static String getPlatFormName(String platFormCode) {
    String name = PLAT_FORM_NAME_MAP.get(platFormCode);
    return name != null ? name : platFormCode;
}

五、改造效果

用一个抖音订单、快递选京东的错误数据测试,改造前后对比如下:

维度 改造前 改造后
错误信息 "非法的参数" "平台抖音不支持快递公司京东"
拦截时机 API 调用后 请求构建前,本地拦截
API 调用浪费 每次错误都调用 零浪费
排查方式 开发介入查日志 运营看提示即可修正

日志输出:

复制代码
平台[抖音]不支持快递公司[京东]

简洁、准确、无需翻译,业务人员一看就明白:订单选错快递了,改一下就好。


六、工程权衡与后续演进

当前取舍

1. 独立接口而非修改 RequestStrategy

受 JDK 1.6 限制,无法使用 default 方法,我们选择用 instanceof 进行能力检查。这比直接修改基础接口更安全,因为奇门等无需校验的平台完全不受影响。

2. 快递列表硬编码在常量类中

当前支持的快递列表写死在 TocWmsSourcePlatFormType 中,新增快递公司需修改常量类。在渠道快速接入阶段,这种集中管理方式反而更容易维护。后续可迁移至配置中心,实现动态调整。

后续优化路线图

阶段 优化项 触发条件
短期 快递公司列表迁移到 PlatformConfigCache,支持动态刷新 快递公司变更频繁
中期 升级 JDK 8+ 后,将 supports 合并到 RequestStrategydefault 方法 技术栈升级
长期 运营后台增加"平台-快递"绑定配置页面,自助管理 业务自助化需求

七、总结

这次改造的核心价值在于:将错误从"事后被动响应"变为"事前主动拦截"

通过引入 LogisticsSupportable 接口,我们为需要校验的平台提供了统一的能力声明机制;通过门面层的 instanceof 检查,在不修改核心接口的前提下实现了前置拦截;通过快递公司和平台名称中文化,让错误信息从技术语言变成了业务语言。

代码改动量很小,但带来的运维收益和用户体验提升是显著的。这也是我们在整个电子面单系列中反复强调的理念:好的架构不一定是技术最先进的,但一定是问题发生时最能快速定位、最容易修复的。


八、系列导航与参考

本篇文章是「电商多平台电子面单对接实战」的第六篇(防御性设计篇),聚焦快递公司前置校验的落地实践。

系列文章目录


延伸阅读:Java 23种设计模式实战系列

本文中前置校验改造的核心,巧妙地运用了接口隔离原则(ISP)策略模式 的变体------通过一个独立的 LogisticsSupportable 接口,在不修改原有策略接口的前提下,为部分平台赋予了快递校验能力。在《Java 23种设计模式:从踩坑到精通》系列中,这些设计原则和模式有更体系化的拆解。如果你对以下问题感兴趣,推荐延伸阅读:

  • 接口隔离原则:如何设计小而专一的接口,避免"胖接口"?
  • 策略模式:如何定义算法族并动态切换,与工厂模式配合?
  • 单一职责原则:如何判断一个类是否承担了过多职责?

📖 《Java 23 种设计模式:从踩坑到精通》


九、一起交流,共同进步

技术之路,一个人走得快,一群人走得远。

如果您的团队也在为多平台对接中的数据校验头疼,希望本文的设计能给您带来启发。

  • 📌 关注我 :点击上方"关注",第一时间获取系列更新推送。
  • 💬 留言讨论:如果您在实际对接中遇到类似问题,或对文章有任何建议,欢迎在评论区留言。
  • 🔗 分享转发 :如果本文对您有帮助,请 点赞收藏分享,让更多同行看到。