【电商多平台电子面单对接实战·第二篇】抖音代发电子面单对接:从"面条代码"到整洁架构的涅槃之路
📢 场景说明
本文实际处理的是抖音代发订单 (供销、手工单等),而非普通抖店商家自营订单。
文中"抖店"相关描述均指代发场景 。普通抖店订单请参考系列第三篇。
给您带来困惑,深表歉意。
📖 《电商多平台电子面单对接实战》系列导航
- 系列开篇:从"能跑就行"到"整洁架构"------WMS多平台发货系统重构手记
- 上一篇:奇门对接顺丰电子面单:从200行"祖传代码"到优雅重构的经验分享
- 本文:【电商多平台电子面单对接实战·第二篇】抖音代发电子面单对接
- 下一篇:抖音抖店(普通订单)电子面单对接(已发布)
🔔 订阅提醒:本系列将陆续发布淘宝/天猫、京东、拼多多、抖音、快手、微信视频号、小红书、得物等平台电子面单对接实战。点击右上角"关注",不错过每一篇干货。
一、背景:当200行代码遇上"抖音速度"
抖音电商的崛起速度远超预期,我们的WMS系统在接入抖店电子面单时,延续了"先跑通再说"的战术。最初的实现仅支持普通订单,随着业务发展(多包裹、重复订单、顺丰产品码、出版社特殊地址规则),一个方法悄然膨胀到250行,内部遍布条件判断、字符串拼接、JSON硬编码、重复解析逻辑......每次需求变更,都像在拆弹。
典型症状:
- 单方法违背单一职责:JSON构建、HTTP签名、响应解析、数据库存储混在一起。
- 重复代码泛滥:普通订单和重复订单两个重载方法,大量逻辑重复。
- 硬编码失控:物流编码映射(
"SF"→"shunfeng")、出版社特殊地址、默认发件人信息散布各处。 - 测试几乎不可能:无法对独立的解析逻辑编写单元测试。
- 扩展举步维艰:新增一个快递类型或地址规则,需要修改多处。
📌 一句话:代码的复杂度已经超过了业务复杂度,重构刻不容缓。
二、抖音抖店电子面单业务流程(业务视角)
在深入技术重构之前,先理清抖店电子面单的完整链路(以下为脱敏流程):

关键业务规则(根据抖店官方文档及踩坑总结):
- 发货地址一致性 :
sender_info必须与抖店后台订购的"电子面单网点"地址完全一致,否则取号失败。 - 顺丰产品码 :需将前端选择的"特快/标快/电商标快"映射为抖店要求的
product_type值(数字编码,非T4/T6)。 - 重复订单场景:原订单已部分发货(已有部分运单号),新申请时需增量获取,不能重复申请已有运单号。
- 店铺共享token :如果订单来自共享店铺,需要查询对应商家的
access_token。 - 多包裹 :一次请求最多支持10个包裹,超过需分批或使用子母件(抖店子母件通过
total_pack_count字段实现)。 - 错误处理 :接口可能返回
err_infos(错误信息)和顶层message(如token过期),需取最后一条错误信息。
三、重构目标与设计原则
我们以开闭原则、单一职责、DRY为核心,制定了以下重构目标:
| 原则 | 落地手法 |
|---|---|
| 单一职责 | 每个方法只做一件事:物流映射、JSON构建、签名、解析、存储各司其职 |
| 开闭原则 | 新增快递类型或地址规则,只需修改映射Map或扩展策略,不修改核心流程 |
| 依赖倒置 | 高层业务逻辑不依赖具体的HTTP实现,通过统一方法签名调用 |
| 接口隔离 | 不同场景(普通订单/重复订单)使用不同的解析策略,不强迫实现不需要的去重逻辑 |
| DRY | 抽取公共的JSON构建、商品清洗、错误解析,两个重载方法复用 |
同时,我们引入了清晰的分层架构(详见后文)。
四、分层架构设计
我们将电子面单功能划分为以下层次,每层独立演进:

为什么这样分层?
- 降低耦合:每一层只依赖下层的抽象,未来接口升级(如抖店更换签名算法)只改Client层。
- 提高复用:Builder层和Parser层的逻辑同时服务于普通订单和重复订单。
- 便于测试:每一层都可以编写独立的单元测试,无需启动数据库或外部服务。
- 扩展性:新增快递类型只需扩展物流编码映射;新增地址规则只需修改Builder层。
五、重构核心:拆解与封装
下面展示关键代码片段(已脱敏),突出重构前后的对比。
5.1 物流编码映射:从if-else到静态Map
重构前(散落在各处):
java
if ("SF".equals(logisticsCode)) {
DYlogisticsCode = "shunfeng";
} else if ("ZTO".equals(logisticsCode)) {
DYlogisticsCode = "zhongtong";
}
重构后:
java
private static final Map<String, String> DY_LOGISTICS_CODE_MAP = new HashMap<>();
static {
DY_LOGISTICS_CODE_MAP.put("SF", "shunfeng");
DY_LOGISTICS_CODE_MAP.put("ZTO", "zhongtong");
DY_LOGISTICS_CODE_MAP.put("POSTB", "youzhengguonei");
DY_LOGISTICS_CODE_MAP.put("STO", "shentong");
DY_LOGISTICS_CODE_MAP.put("YTO", "yuantong");
}
private String getDYLogisticsCode(String logisticsCode) {
String dyCode = DY_LOGISTICS_CODE_MAP.get(logisticsCode);
return dyCode != null ? dyCode : logisticsCode;
}
5.2 商品明细JSON构建:独立方法 + 清洗
重构前(长串拼接,难以维护):
java
items += "{";
items += "\"item_name\":\"" + name + "\",";
items += "\"item_count\":\"" + count + "\"";
if (i == size) items += "}"; else items += "},";
重构后:
java
private String buildDYDFItemsJson(TocWmsPickTicket ticket) {
StringBuilder items = new StringBuilder("[");
int idx = 0;
for (TocWmsPickTicketDetail detail : ticket.getDetails()) {
idx++;
String name = sanitizeItemName(detail.getItemName()); // 清洗逻辑独立
items.append("{")
.append("\"item_name\":\"").append(escapeJson(name)).append("\",")
.append("\"item_count\":\"").append(detail.getQuantityBU().intValue()).append("\"")
.append("}");
if (idx < details.size()) items.append(",");
}
items.append("]");
return items.toString();
}
5.3 发件人地址规则:策略化处理
原代码中地址规则复杂(中通固定地址、非特定出版社的邮政/顺丰用另一地址)。我们将其独立为方法:
java
private String resolveShipAddress(TocWmsTicket ticket) {
String baseAddr = getCompanyShipAddress(ticket);
String logisticsCode = ticket.getLogisticsCode();
// 中通使用固定地址
if ("ZTO".equals(logisticsCode)) {
return DEFAULT_ZTO_ADDRESS;
}
// 非"A出版社"(脱敏)的邮政或顺丰使用另一固定地址
if (!"A_PUBLISHER_CODE".equals(ticket.getCompanyCode())
&& ("POSTB".equals(logisticsCode) || "SF".equals(logisticsCode))) {
return DEFAULT_OTHER_PUBLISHER_SF_POSTB_ADDRESS;
}
return baseAddr;
}
5.4 响应解析:策略模式 + 结果对象
根据是否去重,设计了两个解析方法,返回统一的 ParseResult:
java
private static class ParseResult {
private final List<TocTicketWayBillDetails> newBillDetails;
private final boolean hasWaybill;
// getters...
}
// 重复订单:去重
private ParseResult parseEbillInfos(TocWmsTicket ticket, JSONObject jsonResponse,
int totalPackages, int exsitJianNum,
String dyLogisticsCode, String accessToken,
Set<String> existingBillCodes) {
// ... 遍历 ebill_infos,检查 existingBillCodes,仅新增未存在的
}
// 普通订单:不去重
private ParseResult parseEbillInfosWithoutDuplicate(TocWmsTicket ticket, JSONObject jsonResponse,
int totalPackages, String dyLogisticsCode,
String accessToken) {
// ... 每次都新增
}
5.5 错误解析独立
java
private String parseErrInfos(JSONObject jsonResponse) {
String errMessage = "";
JSONObject data = jsonResponse.getJSONObject("data");
if (data != null) {
JSONArray errInfos = data.getJSONArray("err_infos");
if (errInfos != null && !errInfos.isEmpty()) {
for (Object obj : errInfos) {
JSONObject err = (JSONObject) obj;
String msg = err.getString("err_msg");
if (StringUtils.isNotBlank(msg)) errMessage = msg;
}
}
}
return errMessage;
}
5.6 凭证管理统一封装
java
private String getDYDFAccessToken(TocWmsTicket ticket) {
String accessToken = ticket.getCompany().getDYDFAccessToken();
if (DYDFUtils.isContainShopNames(ticket.getShopNick())) {
WmsDepartment customerOrg = (WmsDepartment ) commonDao.findByQueryUniqueResult(
"FROM WmsDepartment o where o.name=:name", "name", ticket.getShopNick());
if (customerOrg != null) accessToken = customerOrg.getDYDFAccessToken();
}
return accessToken;
}
六、最终效果对比
| 维度 | 重构前 | 重构后 |
|---|---|---|
| 主方法代码行数 | 250+ 行 | 约 50 行 |
| 重复代码 | 两个重载方法重复率 >60% | 共用 Builder/Parser,重复率 <10% |
| 可测试性 | 几乎不可测 | 每个子方法可独立单元测试 |
| 扩展快递 | 需改多处 | 只需扩展物流编码 Map |
| 错误处理 | 分散且不完整 | 统一错误解析,取最后一条 |
七、单元测试示例
基于 JUnit 5 + Mockito,测试关键的构建和解析逻辑。
7.1 测试物流编码映射
java
@Test
void testGetDYLogisticsCode() {
assertEquals("shunfeng", getDYLogisticsCode("SF"));
assertEquals("yuantong", getDYLogisticsCode("YTO"));
assertEquals("unknown", getDYLogisticsCode("unknown"));
}
7.2 测试商品明细JSON构建
java
@Test
void testBuildItemsJson() {
Order order = mockOrderWithDetails(2);
String json = buildItemsJson(order);
assertTrue(json.startsWith("["));
assertTrue(json.endsWith("]"));
assertFalse(json.matches(".*},\\s*\\]$")); // 最后一项后没有逗号
}
7.3 测试错误解析
java
@Test
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);
}
7.4 测试顶层过期消息覆盖
java
@Test
void testCheckTopLevelMessage() {
JSONObject resp = new JSONObject();
resp.put("message", "access_token已过期");
String finalErr = checkTopLevelMessage(resp, "原始错误");
assertEquals("access_token已过期", finalErr);
}
八、踩坑与避坑指南
- access_token 有效期7天:需实现自动刷新机制,避免过期后取号失败。
- 发货地址必须与订购关系完全匹配 :包括标点符号、空格。建议调用
logistics.listShopNetsite接口获取准确的地址。 - 重复订单场景:必须记录已申请的运单号,否则会重复申请导致浪费或接口报错。
- 子母件(多包裹) :抖店通过
total_pack_count字段实现,母单在track_no,子单在sub_waybill_codes。 - 顺丰 product_type :必须使用数字编码(如
1、2、247),切勿使用 T4/T6。 - 签名算法:抖店使用 MD5(参数升序拼接),务必实现正确的签名工具类。
- 日志脱敏 :不要在日志中打印
access_token、appSecret,可用掩码。
九、设计模式运用总结
| 模式 | 应用场景 |
|---|---|
| 策略模式 | 不同订单类型使用不同的解析策略(去重 vs 不去重) |
| 工厂方法(隐含) | buildItemsJson、buildOrderInfos 等方法负责构建特定JSON |
| 模板方法 | 两个重载的 getWaybill 方法流程相同,差异通过参数和策略传递 |
| 外观模式 | callDYDFApi 封装签名、序列化、HTTP调用的复杂性 |
| 值对象 | ParseResult 封装多个返回值 |
十、给即将或正在对接抖店电子面单团队的建议
- 提前注册抖店开放平台 ,获取测试环境的
appKey/appSecret,并订购电子面单服务。 - 理解 OAuth2 授权流程 :引导商家授权应用,使用授权码换取
access_token。 - 在沙箱环境充分测试:包括正常取号、数量不足、token过期、重复订单等场景。
- 单元测试先行:为每个构建和解析方法编写测试,提高代码质量。
- 保持发货地址配置化:将不同快递、不同出版社的地址规则抽取为配置,避免硬编码。
- 关注抖店接口更新:定期查阅官方文档,及时调整。
十一、系列导航与总结
本篇文章是 「电商多平台电子面单对接实战」系列的第二篇 ,聚焦抖音抖店电子面单的重构实践。我们通过分层架构、策略模式、DRY原则,将250行的"面条代码"演化为可维护、可测试、可扩展的整洁代码。
接下来:第三篇将分享抖音抖店(普通订单)电子面单的重构经验(重点:服务类型映射、子母件、重复订单),敬请期待。
📚 相关文章
十二、一起交流,共同进步
技术之路,一个人走得快,一群人走得远。
- 📌 关注我 :点击上方"关注",第一时间获取系列更新推送。
- 💬 留言讨论:如果您在实际对接中遇到问题,或对文章有任何建议,欢迎在评论区留言,我会定期回复。
- 🔗 分享转发 :如果本文对您有帮助,请 点赞 、收藏 、分享,让更多同行看到。
🔔 本系列持续更新中 ,下一篇《抖音抖店(普通订单)电子面单对接》已发布,欢迎前去阅读了解详情!