基于编排器+策略模式的多平台电子面单架构设计

📖 《电商多平台电子面单对接实战》系列导航



关于历史代码的说明

本系列所展示的原始代码,诞生于公司业务爆发式增长期。那时的首要目标是快速上线、稳定支撑业务,代码的简洁性和扩展性在时间压力下做出了一些合理的妥协。正是这些“历史代码”撑起了公司数年的发货业务,离不开前辈们的智慧和付出。

我接手系统后,业务提出了新需求(顺丰多产品编码),同时团队核心成员发生变动。为了让新同事能快速理解系统、安全地扩展功能,也为了让系统能适应未来更多平台(京东、拼多多、快手等),我决定在保持所有业务逻辑不变的前提下,对代码结构进行优化。

这不是对历史的否定,而是站在巨人肩膀上的演进。 如果老同事看到这篇文章,请理解这只是在技术债务和业务需求之间的务实选择,绝非对你工作的否定。你的付出,是这个系统的基石。


一、缘起:200行历史代码的痛

在电商WMS系统中,电子面单获取是发货环节的核心。最初的代码只有几十行,但随着业务膨胀(支持顺丰特快/标快/电商标快、重复订单、子母件、多平台……),一个方法悄然膨胀到250+行,内部变量名从 obj1 排到 obj16,阅读代码如同考古。

// 伪代码示例(原始“上帝方法”)
private void getQiMenWaybillByProductCode(...) {
    // 1. 校验、获取平台配置
    // 2. 构建发件人、收件人
    // 3. 循环每个包裹(循环内查询数据库获取商品明细)
    // 4. 调用奇门接口
    // 5. 解析响应
    // 6. 保存运单号、绑定工作单
    // 7. 异常处理...
}

痛点直击

  • 长方法,职责爆炸:一个方法做了所有事,改一处动全身。
  • 重复代码:普通订单与重复订单两个重载方法60%逻辑相同;顺丰与非顺丰的循环体也高度相似。
  • 性能杀手:每个包裹循环内都查询数据库(N+1次),10个包裹就是10次查询。
  • 硬编码满天飞:月结卡号、网点编码、地址字符串散落各处,修改一次全局搜索。
  • 扩展困难:新增平台需要复制粘贴200+行代码,极易出错。
  • 测试缺失:无法对独立逻辑编写单元测试。

业务要求支持顺丰多产品编码(特快=1、标快=2、电商标快=247),且未来还要对接抖音、京东、拼多多等平台。我们决定彻底重构,采用编排器 + 策略模式


二、设计模式选型:从“模板方法”到“编排器+策略”

最初我们尝试了模板方法模式:定义一个抽象基类,子类实现 buildRequestcallApiparseResponse 三个方法。这解决了平台内重复,但仍有不足:

  • 继承强耦合:子类必须继承基类,无法灵活组合不同逻辑。
  • 流程固化:一旦需要修改通用流程(如增加重试、缓存),必须修改基类,影响所有子类。

最终我们选择编排器 + 策略模式

  • 编排器(WaybillFetchTemplate:作为纯粹的“指挥官”,固定取号流程(构建请求 → 调用API → 解析响应 → 保存绑定),不关心任何平台细节。
  • 策略接口RequestStrategy(请求构建)、ParseStrategy(响应解析)、ExceptionStrategy(业务异常判断)。
  • 上下文对象 WaybillContext:封装订单、已有件数、产品代码等参数,以及扩展属性 Map<String,Object>,避免策略方法参数膨胀。

这样,新增平台只需实现三个策略类,无需改动编排器及任何现有代码

🏭 设计模式视角:编排器+策略模式的核心优势在于组合优于继承——WaybillFetchTemplate 通过组合三个策略接口来实现流程控制,而非让每个平台继承一个抽象模板类。这种设计避免了类爆炸,同时保留了运行时动态替换策略的灵活性。在《Java 23种设计模式:从踩坑到精通》系列中,我详细拆解了策略模式与模板方法模式的选型边界,欢迎延伸阅读。


三、多平台电子面单整体架构设计

3.1 多平台电子面单分层结构

在这里插入图片描述

(图片说明:从上到下依次为业务服务层、取号服务层、编排器、策略接口、API调用器/持久化组件、缓存)

分层职责说明

层次 组件 职责 通俗解释
业务服务层 TocOrderExpressModifyService 接收前端请求(修改快递),校验权限、更新订单快递信息、调用取号服务、处理批量结果(支持部分成功)。 就像快递公司的前台:你拿着订单去修改快递公司,前台检查资格、改单子、然后告诉后台去申请新单号。
取号服务层 WaybillFetchService 创建取号上下文、获取平台配置(token)、组装策略,调用编排器。 调度员:把订单信息整理成标准工单,并决定今天哪个快递员(平台)来取货。
编排器 WaybillFetchTemplate 固定流程:构建请求 → 调用API → 判断成功 → 解析响应 → 保存绑定。它是“指挥官”,不干具体活。 像工厂的流水线控制员:不管生产什么产品,都按“取料→加工→质检→包装”的顺序来。
策略接口 RequestStrategy, ParseStrategy, ExceptionStrategy 定义平台差异的行为:如何构造请求、如何解析响应、如何判断业务成功。每个平台(顺丰、抖音等)有自己的一套策略。 就像不同快递公司的快递员:有的要填电子单,有的要打印纸单,但总得有人把包裹取走。
API调用器 ApiInvoker 封装所有外部接口的HTTP通信、签名、超时、重试。 类似快递员的运输工具(货车、飞机),负责把包裹送到目的地。
持久化组件 WaybillPersistence 将获取的运单号保存到数据库,并绑定到具体工作单(包裹)。 好比仓库管理员:把快递单号录入系统,并贴在对应的货品上。
平台缓存 PlatformConfigCache 缓存各平台的配置信息(appKey、token等),减少重复查询数据库。 像是前台的小抄本:记着各个快递公司的联系电话和地址,不用每次都翻电话本。

关键设计原则

  • 单向依赖:上层只依赖下层,下层不感知上层,修改某一层不影响其他层。
  • 策略与流程分离:编排器固定流程,策略实现细节,新增平台只需增加策略类,不修改编排器。
  • 配置外置:平台敏感信息(token、appKey)通过缓存统一管理,避免散落各处。

3.2 多平台电子面单核心类图

多平台电子面单核心类图

(图片说明:展示编排器、三大策略接口、奇门/抖音策略实现、策略工厂、上下文、基础设施的静态关系)

设计说明

  1. 编排器 (WaybillFetchTemplate):流程的“总指挥”,固定执行顺序。它不关心具体平台差异,通过策略接口调用具体实现。
  2. 策略接口:三大接口分离了请求构建、响应解析、异常判断的职责,符合接口隔离原则
  3. 策略实现:每个平台提供三个独立的实现类,例如 QiMenRequestStrategyQiMenParseStrategyQiMenExceptionStrategy。这样每个类只负责一项任务,易于单元测试和独立修改。
  4. 策略工厂 (StrategyFactory):根据平台编码(如 TMDY)返回对应的策略实例。工厂内部使用 Map 存储策略,消除 if-else
  5. 上下文 (WaybillContext):封装订单、已有件数、产品代码及扩展属性(如 sessionKey),避免在策略方法中传递大量参数。
  6. 基础设施ApiInvoker 负责网络调用,WaybillPersistence 负责数据持久化,PlatformConfigCache 负责缓存。这些组件被编排器调用,与策略独立。

扩展性:新增平台(如京东)只需创建 JingdongRequestStrategyJingdongParseStrategyJingdongExceptionStrategy 三个类,并在 StrategyFactory 中注册,无需修改编排器或其他现有代码。


四、多平台电子面单核心流程时序图

核心流程时序图

(图片说明:业务层→服务层→编排器→策略→API调用器→持久化组件的交互顺序)

时序图关键点

  • 策略获取:通过 StrategyFactory 动态获取三个策略实例,实现平台解耦。
  • isFirst 计算:根据 exsitJianNum == 0 决定是否首次申请,用于子母件标识。
  • 异常短路:业务失败时直接返回,不执行解析和持久化,提高效率。
  • 持久化原子性saveAndBind 内部先清理旧数据再插入新数据,确保数据一致性。

五、关键代码实现(JDK 1.6 兼容)

5.1 上下文对象 WaybillContext

public class WaybillContext {
    private final TocWmsPickTicket ticket;
    private final int exsitJianNum;
    private final String productCode;
    private final Map<String, Object> ext = new HashMap<String, Object>();
    // 构造器、getters/setters...
}

5.2 编排器 WaybillFetchTemplate

public class WaybillFetchTemplate {
    // 构造器注入策略等
    public boolean execute(WaybillContext ctx) {
        String traceId = ctx.getTicket().getCode() + "_" + System.currentTimeMillis();
        try {
            Object request = requestStrategy.buildRequest(ctx);
            logger.info("请求: " + request);
            String response = apiInvoker.invoke(request, ctx);
            logger.info("响应: " + response);
            if (!exceptionStrategy.isBusinessSuccess(response)) {
                String errMsg = exceptionStrategy.extractErrorMsg(response);
                markException(ctx.getTicket(), errMsg);
                return false;
            }
            List<WaybillDetail> details = parseStrategy.parseResponse(response, ctx);
            if (details == null || details.isEmpty()) {
                markException(ctx.getTicket(), "未获取到运单号");
                return false;
            }
            persistence.saveAndBind(ctx.getTicket(), details, ctx.getExsitJianNum() == 0);
            return true;
        } catch (Exception e) {
            logger.error("取号失败", e);
            markException(ctx.getTicket(), "系统异常: " + e.getMessage());
            return false;
        }
    }
}

5.3 奇门平台策略实现

public class QiMenRequestStrategy implements RequestStrategy {
    @Override
    public Object buildRequest(WaybillContext ctx) {
        return QiMenRequestBuilder.buildRequest(ctx.getTicket(), ctx.getExsitJianNum(), ctx.getProductCode());
    }
}

public class QiMenParseStrategy implements ParseStrategy {
    @Override
    public List<WaybillDetail> parseResponse(String response, WaybillContext ctx) {
        return QiMenResponseParser.parse(response);
    }
}

public class QiMenExceptionStrategy implements ExceptionStrategy {
    @Override
    public boolean isBusinessSuccess(String response) {
        return !response.contains("\"error\"") && response.contains("\"waybill_cloud_print_response\"");
    }
    @Override
    public String extractErrorMsg(String response) {
        // 解析奇门错误信息
    }
}

5.4 缓存组件 PlatformConfigCache

public class PlatformConfigCache extends DefaultBaseManager {
    private ConcurrentHashMap<String, TocPlatFormApp> appCache;
    private ConcurrentHashMap<String, WmsOrganization> tokenCache;
    // 定时自动刷新(默认1小时)
    public TocPlatFormApp getPlatApp(String platCode) { ... }
    public WmsOrganization getPlatAppTokenConfig(String sourcePlatformCode, WmsOrganization company) { ... }
}

5.5 持久化组件 WaybillPersistence

public class WaybillPersistence extends DefaultBaseManager {
    public void saveAndBind(TocWmsPickTicket ticket, List<WaybillDetail> details, boolean isFirst) {
        // 1. 清理历史数据(清空工作单引用、删除旧明细)
        // 2. 遍历新明细,按工作单件数分配运单号
        // 3. 处理子母件标识(isFirst)
        // 4. 更新订单 billCodes
    }
}

5.6 业务服务层

public class TocOrderExpressModifyService extends DefaultBaseManager {
    public BatchResult editTocLogistic(List<Long> ids, String express, String productCode) {
        // 校验 + 循环每个订单(独立事务)
    }
    public void getTocWayBillCodeByProductCode(TocWmsPickTicket ticket, String productCode) {
        // 新媒体场景处理、订单状态校验
        // 重新计算 customerBoxNum(基于工作单,若为0则抛异常)
        int exsitJianNum = ticket.getIsTocRepeatOrder() ? queryExistingWaybillCount(ticket) : 0;
        fetchService.fetchWaybill(ticket, exsitJianNum, productCode);
        ticket.setGetBillCodeDate(new Date());
    }
}

六、性能压测与成果

6.1 压测环境

  • 4核CPU / 8GB内存 / Oracle 11g / JDK 1.6
  • JMeter 5.x,并发20线程

6.2 结果对比

包裹数 重构前平均RT(ms) 重构后平均RT(ms) 循环内DB查询次数 吞吐量(TPS)提升
1 120 45 1 → 0 150 → 380
10 850 60 10 → 0 22 → 350
50 3800 110 50 → 0 5 → 320

6.3 代码质量提升

指标 重构前 重构后
主方法代码行数 250+ 行 编排器80行 + 策略类60行
重复代码(跨平台) 60% 0%
单元测试覆盖 <5% 80%+
新增平台接入时间 2-3天 0.5天

七、踩坑与避坑指南

异常 原因 解决
Invalid session 硬编码 mock sessionKey 通过 WaybillContext.ext 传递动态获取的 sessionKey
Missing required arguments: sender 请求未设置发件人 完整迁移 buildSenderUserInfoDto 逻辑
ORA-00001 唯一约束冲突 历史运单明细未删除 saveAndBind 中先物理删除旧明细
ORA-02292 外键约束冲突 工作单仍引用旧明细 先清空工作单 wayBillDetailNew 再删明细
ConcurrentHashMap NPE 缓存了 null token 仅当 token != null 时缓存
customerBoxNum=0 无包裹 订单未同步工作单件数 根据工作单重新计算,若为0则抛异常
sourcePlatformCode 为 null 数据缺失 转换为 "OTHER"

八、总结

通过“编排器 + 策略模式”重构多平台电子面单模块,我们实现了:

  • 流程与策略完全解耦:编排器不关心平台差异,新增平台只需实现策略。
  • 性能飞跃:消除N+1查询,缓存配置,10包裹响应时间从850ms降至60ms。
  • 代码复用:通用日志、异常、保存逻辑集中,跨平台重复代码归零。
  • 高可靠:彻底解决唯一约束、外键约束问题,事务支持部分成功。
  • 易扩展:后续接入抖音、京东、拼多多等平台,只需编写三个策略类,半天即可完成。

该架构已成功支撑奇门、抖音代发、抖音普通等平台平稳运行,线上故障率归零,并为后续新平台接入提供了统一范式。


九、未来架构演进方向

为应对未来业务增长,我们规划了以下优先级最高的演进方向(部分已进入设计阶段)。

9.1 防腐层(ACL)与统一数据契约(高优先级)

问题WaybillContext 直接透传底层订单实体,各平台订单信息差异向上污染业务逻辑。
规划:在编排器与策略之间增加防腐层,将各平台异构数据模型收敛为内部 StandardOrderStandardWaybillRequest,实现上层业务与第三方接口解耦。

9.2 异步削峰与高可用容灾(高优先级)

问题:取号流程同步阻塞,大促期间高并发可能拖垮发货链路。
规划

  • 引入消息队列(如 RocketMQ)异步化取号,订单取号后立即返回“处理中”,通过回调或轮询更新运单号。
  • 增加熔断降级机制:当某快递公司接口错误率或超时率超过阈值时,自动熔断,降级为“本地生成占位面单 + 人工审核”或切换备选物流渠道。

9.3 策略工厂自动化装配(中优先级)

问题:当前 StrategyFactory 需手动注册策略实例,新增平台要修改工厂类,违背开闭原则。
规划:利用 Spring 依赖注入,在 @PostConstruct 阶段自动扫描所有实现了 RequestStrategy 等接口的 Bean,并通过策略类内部的 getPlatform() 方法自动注册到 Map,实现零侵入扩展。

其他规划方向(延后实施)

  • 全链路安全与隐私合规(统一脱敏、oaid 集成)
  • 规则引擎处理动态校验(剥离字段校验规则,支持热更新)
  • 错误码分级与重试补偿(4xx/5xx 分级处理)
  • 监控埋点增强(成功率、RT 大盘、资费预警)
  • 缓存防击穿(DCL 或分布式锁)
  • JDK 版本升级(长期目标)

十、系列导航与参考

本篇文章是「电商多平台电子面单对接实战」的第四篇(架构设计篇),它统一了前几篇中奇门、抖音等平台的重构模式,提炼出一套可复用的多平台扩展架构。后续新增平台(快手、得物等)均可按此模式快速接入。

系列文章目录


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

本文中架构设计的核心——编排器+策略模式模板方法模式的选型对比组合优于继承等设计理念,在《Java 23种设计模式:从踩坑到精通》系列中有更体系化的讲解。如果你对以下问题感兴趣,推荐延伸阅读:

  • 策略模式 vs 模板方法模式:何时用策略替换算法,何时用模板固定骨架?
  • 工厂模式:简单工厂、工厂方法、抽象工厂,如何在架构演进中逐步升级?
  • 组合优于继承:为什么我们的编排器选择了组合而非继承?

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

💡 学习建议:电子面单系列侧重业务落地与架构演进,设计模式系列侧重理论体系与设计思维。两者搭配阅读,既能看懂一个系统如何从“能跑”进化到“可插拔”,又能掌握指导每次升级的设计原则,形成“实战→理论→反哺实战”的闭环。


十一、一起交流,共同进步

技术之路,一个人走得快,一群人走得远。
如果您的团队也在为多平台对接头疼,希望本文的架构设计能给您带来启发。欢迎留言交流。

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

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐