抖音普通订单电子面单对接:从重复代码到整洁分层

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



关于历史代码的说明

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

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

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


一、背景:两套独立方法,却重复造了两次轮子

在我们的WMS系统中,抖音平台订单分为两种场景:代发订单(供销、代发、手工单)和普通抖店订单(商家直接在抖店后台产生的自营订单)。两者虽然都调用抖音电子面单接口,但使用的API、请求字段、解析逻辑完全不同,因此代码中很早就分成了两套独立的重载方法组。

然而,这两套方法虽然在业务逻辑上互不干扰,却存在大量机械重复的底层代码:

  • 物流编码映射(SF → shunfeng、ZTO → zhongtong 等)
  • 商品名称清洗(移除特殊敏感词、反斜杠等)
  • JSON 字符串转义
  • 错误信息解析(err_infos 取最后一条)
  • 顶层 message 的“已过期”检查
  • 数据库查询已存在运单号的 SQL 逻辑

这些重复代码散落在两个独立的方法族中,导致了严重的维护灾难:

  1. 修改一处映射(如新增快递类型),需要同时改两个地方;
  2. 修复一个解析 Bug,要同步两套代码;
  3. 代码行数翻倍(代发 250 行 + 普通 250 行 = 500 行重复逻辑)。

📌本文聚焦于抖音平台普通订单场景的重构,旨在消除其内部有件数/无件数版本的重复,并与代发场景共享底层公共能力。


二、原始代码的典型问题

普通订单无件数版本为例,原始代码约350行,堪称遗留系统的典型缩影,存在以下严重问题:

  • 长方法,职责爆炸:一个方法同时做参数校验、商品明细拼装、收件人JSON构建、user_id逻辑、order_channel判断、发件人地址获取、签名、HTTP调用、响应解析、数据库存储……任何一处修改都可能引发连锁反应。
  • 重复代码泛滥:有件数版本与无件数版本有80%的代码相同,仅循环起始索引和 exsitJianNum 不同;且与代发场景的底层逻辑完全重复。
  • 硬编码散落各处:地址字符串、渠道码 "1"/"101"、默认发件人姓名电话等直接写在代码里。
  • 性能低下:每个包裹循环内都去数据库查询商品明细(WmsItem),10个包裹就是10次查询。
  • 可测试性为零:无法对单独的JSON构建、响应解析编写单元测试,必须启动完整容器才能验证。
  • 错误处理脆弱:数量校验不足、错误信息只取第一条、token过期未单独处理等。

三、重构目标

本次重构立下6个军令状:

  1. 行为保持:任何优化不能改变原有业务逻辑,包括那些看似“奇怪”的细节。
  2. 消除重复:提取公共底层能力,与代发场景共享;统一有件数/无件数版本的流程。
  3. 提升可维护性:拆分长方法,让每个方法只做一件事。
  4. 性能优化:彻底消除循环内的数据库查询。
  5. 增强可测试性:让JSON构建、响应解析等模块可独立测试。
  6. 为后续扩展奠基:方便新增其他快递产品(如顺丰冷链)或平台。

🏭 设计模式视角:这次重构虽然没有显式引入GoF设计模式,但其中“参数化消除重复”、“提取公共能力”等手法,本质上是单一职责原则开闭原则的落地实践——让每个方法只做一件事,让公共逻辑的变更只影响一处。在《Java 23种设计模式:从踩坑到精通》系列的**第2篇(策略模式)**中,我详细拆解了如何用策略接口进一步替代参数化分支,欢迎延伸阅读。


四、重构步骤与技术细节

4.1 拆分构建与解析,形成分层

我们保留了原有的“请求构建 → 接口调用 → 响应解析 → 保存”主线,但将每个步骤独立为私有方法:

方法 职责
buildDYWaybillItemsJson 构建商品明细JSON,同时返回最后一个明细的 sourceOrderCode 作为 order_id
buildDYWaybillOrderInfos 构建包裹数组(order_infos),支持有件数/无件数两种模式
buildDYWaybillUserIdJson 处理 user_id 字段的特殊逻辑(特定公司不传,否则传店铺ID或-1)
buildDYWaybillOrderChannel 动态生成 order_channel(普通订单默认"1",供销平台覆盖为"101")
buildDYWaybillRequestJson 组装最终请求体(sender_info + logistics_code + order_infos)
callDYWaybillApi 封装签名、HTTP调用、重试机制
parseDYWaybillResponse 解析响应,支持有件数(需计算 needCount)和无件数版本

📌重构效果:主方法从350+行缩减到约50行,每个子方法职责单一,可独立阅读和测试。

4.2 提取公共底层能力,与代发共享

以下工具方法被提升到公共工具类,同时供代发和普通订单使用:

// 物流编码映射
public static String getDYLogisticsCode(String logisticsCode) { ... }

// 商品名称清洗(过滤特殊敏感词、转义反斜杠等)
public static String sanitizeItemName(String originalName) { ... }

// JSON 转义
public static String escapeJson(String s) {
    return s.replace("\\", "\\\\").replace("\"", "\\\"");
}

// 解析错误信息(取最后一条)
public static String parseErrInfos(JSONObject jsonResponse) { ... }

// 检查顶层 message 是否过期
public static String checkTopLevelMessage(JSONObject jsonResponse, String currentErrMessage) { ... }

// 绑定工作单
public static void bindWaybillToWorkDoc(TocWmsPickTicket ticket, List<WaybillDetail> details) { ... }

📌重构效果:原本在两个场景中各自实现的约200行重复逻辑,现集中维护一处。

4.3 消除有件数/无件数版本的重复

有件数版本与无件数版本仅在以下三点不同:

  1. 总包裹数计算方式(totalPackages = customerBoxNum vs + exsitJianNum
  2. order_infos 构建时包裹ID起始索引(packId = 1 vs = exsitJianNum+1
  3. 解析响应时需要的运单数量(totalPackages vs totalPackages - exsitJianNum

我们通过参数化统一了两个版本:

  • exsitJianNum 作为显式参数传入,无件数版本调用时传 0
  • 构建方法接收该参数,内部循环从 exsitJianNum+1 开始;
  • 解析方法增加该参数,精准计算 needCount

📌重构效果:有件数/无件数版本共用同一套构建和解析逻辑,彻底避免了代码复制。

4.4 性能优化:一次查询,多次复用

原始代码(在每个包裹循环内执行):

WmsItem wmsItem = commonDao.get(WmsItem.class, detail.getItem().getId());

优化后:在 buildDYWaybillItemsJson 方法中只遍历一次明细集合,同时完成商品名称清洗、JSON拼接,并提取最后一个明细的 sourceOrderCode 作为 order_id。不再有循环内查询。实测10个包裹的场景下,面单获取器内部循环内数据库查询次数从10次降为0次(明细数据已由调度层预先批量加载并传入)。

📌重构备注:性能统计仅针对面单获取器内部的 N+1 查询问题,不包括调度层预先加载明细和最终保存运单号的写入操作。

4.5 保留所有“业务暗知识”

重构不是抹杀历史,而是让历史在阳光下运行。所有细节都体现在代码注释中,防止未来优化时误删:

// 1. 随机订单号兜底
if (ticket.getIsAppointGetLogitic() && orderId == null) {
    orderId = RandomStringUtils.randomAlphanumeric(10);
}

// 2. user_id 特殊规则
if (companyName.contains("特定标识") && shopNick.contains("特定标识")) {
    return "";  // 不传 user_id
} else {
    return ",\"user_id\": \"" + (userId != null ? userId : "-1") + "\"";
}

// 3. order_channel 动态生成
String orderChannel = ",\"order_channel\": \"1\"";
if ("DYGX".equals(ticket.getTocPlatFormOriginal())) {
    orderChannel = ",\"order_channel\": \"101\"";
}

// 4. 中通固定地址
if ("ZTO".equals(logisticsCode)) {
    shipAddress = DEFAULT_DY_SHIP_ADDRESS;
}

4.6 核心代码优化前后直观对比

优化前:泥球式长方法(职责爆炸 & 性能杀手)

优化前代码

优化后:单一职责 & 参数化复用(整洁架构)

优化后代码片段

【重构后代码片段】职责清晰,支持有件数/无件数动态切换

📌对比总结:优化后的代码不仅消除了 N+1 查询和脆弱的字符串拼接,还通过引入 exsitJianNum 参数,让一套代码同时支撑两种截然不同的业务形态。

4.7 当前重构成果小结

经过上述改造,我们实现了:

  • 分层拆分:Builder、Client、Parser 三层职责清晰。
  • 公共能力提取:物流映射、字符串清洗、转义、错误解析等沉淀为工具类,供代发和普通场景共享。
  • 参数化统一:有件数/无件数版本共用一套构建和解析逻辑,彻底消灭重复。
  • 性能提升:消除 N+1 查询,明细数据仅在调度层查询一次。
  • 可测试性:每个构建和解析方法均可独立编写单元测试。

📌重构备注:本阶段尚未引入统一的流程编排器(Template)和显式的策略接口(Strategy),这些属于下一阶段的架构规划,详见第十章。


五、设计模式运用

在本次重构中,我们灵活运用了以下设计思想(仅限于当前实现,未超前引入策略接口):

模式 应用场景
参数化切换 通过 exsitJianNum 参数统一有件数/无件数版本,为未来策略模式预留扩展点
工厂方法 buildDYWaybillOrderInfosInlinebuildDYWaybillOrderInfos 分别构建不同场景的包裹数组
值对象 DYItemsAndOrderId 封装商品JSON和订单号,避免多返回值污染
分层架构 Builder、Client、Parser 三层分离,与代发保持一致

六、踩坑与避坑指南

  1. 普通订单无去重,必须业务层控制:与代发不同,普通抖音订单的重复订单场景下,每次调用都会为所有包裹重新生成运单号。调用有件数版本时,必须传入正确的 exsitJianNum,前端只能展示新增的运单号。
  2. order_id 可能为空,需兜底:当明细中的 sourceOrderCode 为空且订单为指定取号时,必须生成随机订单号,否则接口报错。
  3. 中通地址必须固定:原代码对中通使用了 DEFAULT_DY_SHIP_ADDRESS,这个地址必须与抖店后台订购的网点地址完全一致(包括标点符号)
  4. access_token 来源切勿混淆:普通渠道使用 getDouDianAccessToken(),代发使用 getDYDFAccessToken(),两者不通用。
  5. 顺丰产品编码使用数字product_type 必须传入数字编码(如 "1""2""247"),而不是 "T4"/"T6"
  6. 公共底层修改需回归双场景:修改 getDYLogisticsCodeescapeJson 等公共方法时,务必同时回归测试代发和普通订单两个渠道。

⚠️ 以上为普通订单血泪经验总结,建议收藏!


七、重构成果与性能验证

7.1 代码质量量化

指标 重构前 重构后
主方法代码行数 350+ 行 约 50 行
重复代码(有/无件数) 80% 重复 0%(参数化)
循环内DB查询次数(10包裹) 10 次 0 次*
单元测试覆盖 0% 80%+(构建、解析、工具方法)
新增快递产品类型成本 需修改多处 只需扩展 buildDYProductTypeJson

📌重构备注:此处统计的是面单获取器内部循环内的数据库查询次数(N+1问题)。明细数据的预先加载和最终保存的写入操作不计入此指标。

7.2 性能压测实测

为了验证消除循环内数据库查询以及 JSON 构建优化带来的实际收益,我们在预发布环境进行了针对性的接口压测。

📌说明:以下压测数据基于特定的硬件配置、数据库版本及网络环境,实际性能会因服务器负载、数据库性能、网络延迟等因素而有所差异。本文数据仅用于展示重构前后的相对变化趋势,不作为绝对性能承诺。

7.2.1 测试环境与方法
  • 硬件配置:4核 CPU / 8GB 内存 / 500GB SSD
  • 中间件:Oracle 11g, JDK 1.6
  • 测试工具:JMeter 5.x
  • 测试场景:模拟大促期间的批量发货请求,分别构造包含 1个包裹10个包裹50个包裹 的订单进行并发调用(并发线程数=20)。
  • 核心指标:平均响应时间(RT)、吞吐量(TPS)、面单获取器内部循环内 DB 交互次数
7.2.2 压测结果对比
订单包裹数 版本 平均响应时间 (RT) 吞吐量 (TPS) 循环内 DB 交互次数/单 P99 延迟
1 个包裹 重构前 120 ms 150 1 次 180 ms
重构后 45 ms 380 0 次 65 ms
10 个包裹 重构前 850 ms 22 10 次 1200 ms
重构后 60 ms 350 0 次 85 ms
50 个包裹 重构前 3800 ms 5 50 次 4500 ms
重构后 110 ms 320 0 次 150 ms
7.2.3 数据解读与瓶颈分析
  1. N+1 查询是绝对的性能杀手:在重构前的代码中,随着包裹数量的增加,RT 呈线性甚至指数级增长。当包裹数达到50个时,单次请求耗时逼近4秒,极易引发Tomcat线程池耗尽和HTTP网关超时。
  2. 内存计算 vs IO等待:重构后,我们将明细数据的加载前置到了调度层,面单获取器内部完全基于内存中的集合进行操作。无论包裹数量如何翻倍,RT 始终稳定在百毫秒级别。
  3. JSON库替代字符串拼接:使用 JSONArray 替代了原始的 StringBuilder.append(),不仅彻底杜绝了因漏加逗号导致的JSON解析异常,还减少了大量临时String对象的创建,降低了GC频率。

📌业务价值:在“6.18”、“双11”等大促高峰期,系统能够从容应对多包裹大单的冲击,彻底消除了因发货接口超时导致的“打单卡顿”和“漏发”风险,为前端仓储作业节省了宝贵的工时。


八、与代发文章的区别及系列完整性

对比点 代发文章(第二篇) 普通订单文章(本文)
接口字段 distr_order_id, user_id, company_code order_id, sender_info, 无 company_code
解析策略 去重(isCreate 标志) 不去重(每次都新增)
order_channel 仅指定取号时传 "1" 默认 "1",供销 "101"
user_id 逻辑 复杂(共享店铺查询) 简化(特定公司不传,否则传店铺ID或 -1
地址特殊规则 中通、顺丰、邮政多规则 仅中通固定地址
重复订单处理 增量取号 + 去重 增量取号 + 不去重

📌两篇文章互为补充,共同构成抖音电商电子面单对接的完整实践指南。


九、单元测试示例

9.1 测试商品明细 JSON 构建

@Test
public void testBuildDYWaybillItemsJson() {
    TocWmsPickTicket ticket = mockTicketWithTwoDetails();
    DYItemsAndOrderId result = buildDYWaybillItemsJson(ticket);
    String json = result.getItemsJson();
    
    assertTrue(json.startsWith("["));
    assertTrue(json.endsWith("]"));
    // 验证最后一项没有逗号
    assertFalse(json.matches(".*},\\s*\\]$"));
    assertNotNull(result.getOrderId());
}

9.2 测试 order_channel 动态生成

@Test
public void testBuildDYWaybillOrderChannel() {
    TocWmsTicket normal = mockNormalTicket();
    assertEquals(",\"order_channel\": \"1\"", buildDYWaybillOrderChannel(normal));
    
    TocWmsTicket supply = mockSupplyTicket(); // tocPlatFormOriginal = "DYGX"
    assertEquals(",\"order_channel\": \"101\"", buildDYWaybillOrderChannel(supply));
}

9.3 测试错误解析

@Test
public void testParseErrInfos() {
    String json = "{\"data\":{\"err_infos\":[{\"err_msg\":\"错误1\"},{\"err_msg\":\"错误2\"}]}}";
    JSONObject resp = JSON.parseObject(json);
    String err = parseErrInfos(resp);
    assertEquals("错误2", err);
}

📌测试验证错误信息是否为获取到最后一条信息。


十、总结与展望

本篇文章完成了抖音普通订单电子面单的重构,实现了:

  • 将四个重载方法拆分为分层清晰的 Builder、Client、Parser;
  • 提取了公共底层能力,与代发场景共享,消除了大量重复代码;
  • 普通订单内部有件数/无件数版本复用相同构建逻辑,仅通过参数 exsitJianNum 区分;
  • 提供了完整的单元测试示例,确保行为不变;
  • 通过性能压测数据验证了重构的实际成效。

系列文章目录


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

本文中“参数化消除重复”、“公共能力提取”等重构手法,背后体现了单一职责原则开闭原则的设计思想。在《Java 23种设计模式:从踩坑到精通》系列中,这些原则与模式有更体系化的拆解。如果你对以下问题感兴趣,推荐延伸阅读:

  • 策略模式:如何用接口替代参数化分支,实现更优雅的扩展?
  • 模板方法模式:如何用固定骨架统一流程,子类只填差异?
  • 单一职责原则:如何判断一个方法是否承担了过多职责?

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

💡 学习建议:电子面单系列侧重业务落地与重构实践,设计模式系列侧重理论体系与设计思维。两者搭配阅读,既能掌握具体重构手法,又能理解背后的设计原则,形成“实战→理论→反哺实战”的闭环。


十一、下一阶段架构规划

当前重构已完成分层拆分和公共能力提取,为下一步演进奠定了坚实基础。接下来,我们将引入统一流程编排器 + 策略模式,将日志、异常、重试、保存等流程彻底标准化,支撑未来10+个平台的快速扩展。

下图是我们计划下一阶段引入的目标架构(尚未在代码中实现):
统一流程编排器 + 策略模式

📌架构解读WaybillFetchTemplate 充当纯粹的“指挥官”,不关心具体字段组装或响应结构;所有差异化逻辑下沉到 Strategy 和 Util 中。未来新增顺丰冷链产品或拼多多平台,只需新增对应的 Strategy 实现类,主干流程无需改动。

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

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

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

🔔 本系列持续更新中,下一篇《多平台统一架构设计》已发布,欢迎前去阅读!


参考文档

Logo

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

更多推荐