基于编排器+策略模式的多平台电子面单架构设计
基于编排器+策略模式的多平台电子面单架构设计
📖 《电商多平台电子面单对接实战》系列导航
- 系列开篇:从“能跑就行”到“整洁架构”
- 第一篇:奇门对接顺丰电子面单
- 第二篇:抖音代发电子面单对接
- 第三篇:抖音普通订单电子面单对接
- 第四篇:多平台统一架构设计(本文)
- 第五篇:策略工厂复合Key路由改造
- 第六篇:快递公司前置校验改造
- 第七篇:解析器职责分离改造
- 第八篇:模板方法的组合与继承抉择
- 第九篇:API调用调度层Handler分组设计
- 第十篇:奇门 trade_order_list 排查实录
- 第十一篇:数据库查询优化让多包裹取号快一倍
- 第十二篇:两次架构升级完整复盘
- 第十三篇:常量与配置集中管控改造
- 后续:京东、拼多多等平台专项篇
文章目录
关于历史代码的说明
本系列所展示的原始代码,诞生于公司业务爆发式增长期。那时的首要目标是快速上线、稳定支撑业务,代码的简洁性和扩展性在时间压力下做出了一些合理的妥协。正是这些“历史代码”撑起了公司数年的发货业务,离不开前辈们的智慧和付出。
我接手系统后,业务提出了新需求(顺丰多产品编码),同时团队核心成员发生变动。为了让新同事能快速理解系统、安全地扩展功能,也为了让系统能适应未来更多平台(京东、拼多多、快手等),我决定在保持所有业务逻辑不变的前提下,对代码结构进行优化。
这不是对历史的否定,而是站在巨人肩膀上的演进。 如果老同事看到这篇文章,请理解这只是在技术债务和业务需求之间的务实选择,绝非对你工作的否定。你的付出,是这个系统的基石。
一、缘起:200行历史代码的痛
在电商WMS系统中,电子面单获取是发货环节的核心。最初的代码只有几十行,但随着业务膨胀(支持顺丰特快/标快/电商标快、重复订单、子母件、多平台……),一个方法悄然膨胀到250+行,内部变量名从 obj1 排到 obj16,阅读代码如同考古。
// 伪代码示例(原始“上帝方法”)
private void getQiMenWaybillByProductCode(...) {
// 1. 校验、获取平台配置
// 2. 构建发件人、收件人
// 3. 循环每个包裹(循环内查询数据库获取商品明细)
// 4. 调用奇门接口
// 5. 解析响应
// 6. 保存运单号、绑定工作单
// 7. 异常处理...
}
痛点直击:
- 长方法,职责爆炸:一个方法做了所有事,改一处动全身。
- 重复代码:普通订单与重复订单两个重载方法60%逻辑相同;顺丰与非顺丰的循环体也高度相似。
- 性能杀手:每个包裹循环内都查询数据库(N+1次),10个包裹就是10次查询。
- 硬编码满天飞:月结卡号、网点编码、地址字符串散落各处,修改一次全局搜索。
- 扩展困难:新增平台需要复制粘贴200+行代码,极易出错。
- 测试缺失:无法对独立逻辑编写单元测试。
业务要求支持顺丰多产品编码(特快=1、标快=2、电商标快=247),且未来还要对接抖音、京东、拼多多等平台。我们决定彻底重构,采用编排器 + 策略模式。
二、设计模式选型:从“模板方法”到“编排器+策略”
最初我们尝试了模板方法模式:定义一个抽象基类,子类实现 buildRequest、callApi、parseResponse 三个方法。这解决了平台内重复,但仍有不足:
- 继承强耦合:子类必须继承基类,无法灵活组合不同逻辑。
- 流程固化:一旦需要修改通用流程(如增加重试、缓存),必须修改基类,影响所有子类。
最终我们选择编排器 + 策略模式:
- 编排器(
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 多平台电子面单核心类图

(图片说明:展示编排器、三大策略接口、奇门/抖音策略实现、策略工厂、上下文、基础设施的静态关系)
设计说明:
- 编排器 (
WaybillFetchTemplate):流程的“总指挥”,固定执行顺序。它不关心具体平台差异,通过策略接口调用具体实现。 - 策略接口:三大接口分离了请求构建、响应解析、异常判断的职责,符合接口隔离原则。
- 策略实现:每个平台提供三个独立的实现类,例如
QiMenRequestStrategy、QiMenParseStrategy、QiMenExceptionStrategy。这样每个类只负责一项任务,易于单元测试和独立修改。 - 策略工厂 (
StrategyFactory):根据平台编码(如TM、DY)返回对应的策略实例。工厂内部使用Map存储策略,消除if-else。 - 上下文 (
WaybillContext):封装订单、已有件数、产品代码及扩展属性(如sessionKey),避免在策略方法中传递大量参数。 - 基础设施:
ApiInvoker负责网络调用,WaybillPersistence负责数据持久化,PlatformConfigCache负责缓存。这些组件被编排器调用,与策略独立。
扩展性:新增平台(如京东)只需创建 JingdongRequestStrategy、JingdongParseStrategy、JingdongExceptionStrategy 三个类,并在 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 直接透传底层订单实体,各平台订单信息差异向上污染业务逻辑。
规划:在编排器与策略之间增加防腐层,将各平台异构数据模型收敛为内部 StandardOrder 和 StandardWaybillRequest,实现上层业务与第三方接口解耦。
9.2 异步削峰与高可用容灾(高优先级)
问题:取号流程同步阻塞,大促期间高并发可能拖垮发货链路。
规划:
- 引入消息队列(如 RocketMQ)异步化取号,订单取号后立即返回“处理中”,通过回调或轮询更新运单号。
- 增加熔断降级机制:当某快递公司接口错误率或超时率超过阈值时,自动熔断,降级为“本地生成占位面单 + 人工审核”或切换备选物流渠道。
9.3 策略工厂自动化装配(中优先级)
问题:当前 StrategyFactory 需手动注册策略实例,新增平台要修改工厂类,违背开闭原则。
规划:利用 Spring 依赖注入,在 @PostConstruct 阶段自动扫描所有实现了 RequestStrategy 等接口的 Bean,并通过策略类内部的 getPlatform() 方法自动注册到 Map,实现零侵入扩展。
其他规划方向(延后实施):
- 全链路安全与隐私合规(统一脱敏、oaid 集成)
- 规则引擎处理动态校验(剥离字段校验规则,支持热更新)
- 错误码分级与重试补偿(4xx/5xx 分级处理)
- 监控埋点增强(成功率、RT 大盘、资费预警)
- 缓存防击穿(DCL 或分布式锁)
- JDK 版本升级(长期目标)
十、系列导航与参考
本篇文章是「电商多平台电子面单对接实战」的第四篇(架构设计篇),它统一了前几篇中奇门、抖音等平台的重构模式,提炼出一套可复用的多平台扩展架构。后续新增平台(快手、得物等)均可按此模式快速接入。
系列文章目录:
- 开篇:从“能跑就行”到“整洁架构”
- 第一篇:奇门对接顺丰电子面单
- 第二篇:抖音代发电子面单对接
- 第三篇:抖音普通订单电子面单对接
- 第四篇:多平台统一架构设计(本文)
- 第五篇:策略工厂复合Key路由改造
- 第六篇:快递公司前置校验改造
- 第七篇:解析器职责分离改造
- 第八篇:模板方法的组合与继承抉择
- 第九篇:API调用调度层Handler分组设计
- 第十篇:奇门 trade_order_list 排查实录
- 第十一篇:数据库查询优化让多包裹取号快一倍
- 第十二篇:两次架构升级完整复盘
- 第十三篇:常量与配置集中管控改造
- 后续:京东、拼多多、微信视频号等平台专项篇
延伸阅读:Java 23种设计模式实战系列
本文中架构设计的核心——编排器+策略模式、模板方法模式的选型对比、组合优于继承等设计理念,在《Java 23种设计模式:从踩坑到精通》系列中有更体系化的讲解。如果你对以下问题感兴趣,推荐延伸阅读:
- 策略模式 vs 模板方法模式:何时用策略替换算法,何时用模板固定骨架?
- 工厂模式:简单工厂、工厂方法、抽象工厂,如何在架构演进中逐步升级?
- 组合优于继承:为什么我们的编排器选择了组合而非继承?
📖 《Java 23 种设计模式:从踩坑到精通》
💡 学习建议:电子面单系列侧重业务落地与架构演进,设计模式系列侧重理论体系与设计思维。两者搭配阅读,既能看懂一个系统如何从“能跑”进化到“可插拔”,又能掌握指导每次升级的设计原则,形成“实战→理论→反哺实战”的闭环。
十一、一起交流,共同进步
技术之路,一个人走得快,一群人走得远。
如果您的团队也在为多平台对接头疼,希望本文的架构设计能给您带来启发。欢迎留言交流。
- 📌 关注我:点击上方“关注”,第一时间获取系列更新推送。
- 💬 留言讨论:如果您在实际对接中遇到问题,或对文章有任何建议,欢迎在评论区留言,我会定期回复。
- 🔗 分享转发:如果本文对您有帮助,请 点赞、收藏、分享,让更多同行看到。
更多推荐




所有评论(0)