【电商多平台电子面单对接实战|第二篇】抖音代发电子面单对接:从“面条代码”到整洁架构的涅槃之路

【电商多平台电子面单对接实战·第二篇】抖音代发电子面单对接:从"面条代码"到整洁架构的涅槃之路

📢 场景说明

本文实际处理的是抖音代发订单 (供销、手工单等),而非普通抖店商家自营订单。

文中"抖店"相关描述均指代发场景 。普通抖店订单请参考系列第三篇

给您带来困惑,深表歉意。
📖 《电商多平台电子面单对接实战》系列导航


一、背景:当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);
}

八、踩坑与避坑指南

  1. access_token 有效期7天:需实现自动刷新机制,避免过期后取号失败。
  2. 发货地址必须与订购关系完全匹配 :包括标点符号、空格。建议调用 logistics.listShopNetsite 接口获取准确的地址。
  3. 重复订单场景:必须记录已申请的运单号,否则会重复申请导致浪费或接口报错。
  4. 子母件(多包裹) :抖店通过 total_pack_count 字段实现,母单在 track_no,子单在 sub_waybill_codes
  5. 顺丰 product_type :必须使用数字编码(如 12247),切勿使用 T4/T6
  6. 签名算法:抖店使用 MD5(参数升序拼接),务必实现正确的签名工具类。
  7. 日志脱敏 :不要在日志中打印 access_tokenappSecret,可用掩码。

九、设计模式运用总结

模式 应用场景
策略模式 不同订单类型使用不同的解析策略(去重 vs 不去重)
工厂方法(隐含) buildItemsJsonbuildOrderInfos 等方法负责构建特定JSON
模板方法 两个重载的 getWaybill 方法流程相同,差异通过参数和策略传递
外观模式 callDYDFApi 封装签名、序列化、HTTP调用的复杂性
值对象 ParseResult 封装多个返回值

十、给即将或正在对接抖店电子面单团队的建议

  1. 提前注册抖店开放平台 ,获取测试环境的 appKey/appSecret,并订购电子面单服务。
  2. 理解 OAuth2 授权流程 :引导商家授权应用,使用授权码换取 access_token
  3. 在沙箱环境充分测试:包括正常取号、数量不足、token过期、重复订单等场景。
  4. 单元测试先行:为每个构建和解析方法编写测试,提高代码质量。
  5. 保持发货地址配置化:将不同快递、不同出版社的地址规则抽取为配置,避免硬编码。
  6. 关注抖店接口更新:定期查阅官方文档,及时调整。

十一、系列导航与总结

本篇文章是 「电商多平台电子面单对接实战」系列的第二篇 ,聚焦抖音抖店电子面单的重构实践。我们通过分层架构、策略模式、DRY原则,将250行的"面条代码"演化为可维护、可测试、可扩展的整洁代码。

接下来:第三篇将分享抖音抖店(普通订单)电子面单的重构经验(重点:服务类型映射、子母件、重复订单),敬请期待。


📚 相关文章


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

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

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

🔔 本系列持续更新中 ,下一篇《抖音抖店(普通订单)电子面单对接》已发布,欢迎前去阅读了解详情!

相关推荐
杨同学technotes1 小时前
丰巢寄件系统基于DDD服务拆分的落地实践
架构
小赖同学啊1 小时前
基于MCP与主流AI技术架构 水利 发电 公园中的应用
人工智能·架构
葫芦和十三2 小时前
范式之变|Agent 设计,换语言了
人工智能·设计模式
●VON2 小时前
AtomGit Flutter鸿蒙客户端:首页与仓库列表
flutter·华为·架构·harmonyos·鸿蒙
ourenjiang2 小时前
【学习设计模式】原型模式
学习·设计模式·原型模式
段一凡-华北理工大学2 小时前
工业领域的Hadoop架构学习~系列文章18:制造业Hadoop应用实践 - 从数据到智能的完整闭环
大数据·人工智能·hadoop·分布式·学习·架构·高炉炼铁
贵慜_Derek2 小时前
《从零实现 Agent 系统》连载 20|MCP 与 Code Execution:协议、档位与 Sidecar
人工智能·设计模式·架构
冬奇Lab2 小时前
AI Agent 找代码:多仓库多技术栈下的代码定位工程
人工智能·agent·代码规范
Sunia2 小时前
《AgentX 专栏》08-工作流引擎:AgentWorkflow怎么把工具记忆流程串成一条流水线
java·架构