一、背景:那年写下的“能跑就行”

在我们的电商WMS系统中,发货环节需要通过菜鸟奇门电子面单接口向顺丰等快递公司申请运单号。这段核心代码写于多年前,当时的业务需求比较简单:只支持淘宝/天猫订单,快递也只有顺丰。随着业务爆炸式增长(新增京东、抖音、拼多多、小红书等渠道,快递扩展至中通、圆通、申通、京东快递等),这段“祖传代码”逐渐成了团队的心病。

痛点直击

  • 长方法:单个方法超过200行,getQiMenWaybillByProductCode 里充斥着 obj1~obj16 的变量名,阅读时仿佛在玩“猜谜游戏”。
  • 重复代码:普通订单和重复订单两个重载方法,60%以上的逻辑相同;顺丰与非顺丰的循环体也高度相似。
  • 性能杀手:每个包裹循环内都去数据库查询订单明细,10个包裹就是10次查询。
  • 硬编码满天飞:电话号码、月结卡号、网点编码、地址字符串直接写在代码中,修改一次要全局搜索。
  • 扩展困难:每次新增快递公司,都要在巨型方法里添加 else if,一不小心就改出Bug。

今年5月,业务要求支持快递产品代码(如顺丰的标快、特快、电商标快),我们终于下定决心,对这段代码进行彻底重构。本文记录了重构过程中的思考、步骤和踩坑经验,并附上一份可直接运行的Java对接测试样例,希望能为同样对接奇门电子面单的开发者提供借鉴。


二、重构目标与原则

  1. 行为保持:重构后的代码必须与原有业务逻辑完全等价,不能改变任何功能。
  2. 单一职责:每个方法只做一件事,长度控制在50行以内。
  3. 消除重复:提取公共逻辑,复用于普通订单和重复订单场景。
  4. 性能优化:将循环内数据库查询提升到循环外。
  5. 可读性优先:用有意义的命名,消除魔法值。
  6. 便于扩展:新增快递公司或平台时,只需添加常量和少量分支。

三、重构步骤详解

3.1 拆分长方法,职责单一

原始代码中,一个方法同时做了:获取平台配置、构建发件人、构建收件人、查询订单明细、循环生成面单、调用接口、处理异常……我们将其拆分为多个小方法:

职责 提取的方法名
获取平台App配置 getTocPlatFormAppByCode
构建顶层请求对象 buildWaybillCloudPrintApplyNewRequest
构建发件人信息 buildSenderUserInfoDto
构建收件人信息 buildRecipientInfoDto
构建订单渠道和交易单号 buildOrderInfoDto
构建包裹信息 buildPackageInfoDto
构建商品明细 buildItemList / buildItemListWithMaxCount
设置网点编码等公共参数 setCommonApplyRequestParams
统一平台分发 callPlatformWaybillMethod

效果:主方法从200+行缩减到约60行,每个子方法都可以独立理解和测试。

3.2 提取常量,告别魔法值

创建常量接口 TocWmsExpressType,集中管理所有快递相关配置:

public interface TocWmsExpressType {
    // 快递编码
    String SF_CODE = "SF";
    String ZTO_CODE = "ZTO";
    String JD_CODE = "JD";
    
    // 顺丰专用
    String SF_BRAND_CODE = "SF";
    String SF_CUSTOMER_CODE = "010*******";   // 月结卡号脱敏
    
    // 京东专用
    String JD_CUSTOMER_CODE = "010K******";   // 月结卡号脱敏
    
    // 网点编码
    String ZTO_BRANCH_CODE = "3****";
    String JD_BRANCH_CODE = "1566****";
    
    // 默认值
    String DEFAULT_SENDER_NAME = "张**";
    String DEFAULT_SENDER_PHONE = "138****0000";
    String DEFAULT_GOODS_NAME = "书籍";
}

3.3 消除循环内数据库查询

原始代码(N次查询):

for (int i = 1; i <= jianNum; i++) {
    List<Detail> details = dao.findByQuery("FROM Detail WHERE ...");
    // 使用 details
}

优化后(1次查询):

List<Detail> allDetails = getPickTicketDetails(ticketId);
for (int i = 1; i <= jianNum; i++) {
    buildPackageInfo(i, allDetails);
}

3.4 移除循环内的冗余设置

原代码在循环内反复执行 obj1.setBrandCode("SF")obj1.setCustomerCode(...)。由于 obj1 是同一个请求对象,在循环外设置一次完全等价,且避免了重复操作。

3.5 利用重载方法复用公共逻辑

普通订单和重复订单(带 exsitJianNum)共用同一套构建方法,仅通过参数传递差异(循环起始索引、商品明细最大条数)。

3.6 统一平台分发逻辑

将原来散落在多个方法中的 if-else 平台判断,统一到 callPlatformWaybillMethod 中,方便后续新增渠道。


四、优化前后对比

维度 优化前 优化后
代码行数 单个方法200+行 主方法~60行,子方法平均20行
重复代码 两个重载重复率>60% 共用10+私有方法,重复率<10%
数据库查询 每个包裹查询1次 全局1次
可读性 obj1~obj16 语义化命名,如 applyRequestrecipient
维护成本 修改需同步多处 改常量或私有方法即可
扩展性 新增快递需改大方法 增加常量+分支,调用公共构件

五、对接奇门顺丰电子面单的必要步骤

如果您是初次对接,以下步骤可供参考:

5.1 准备工作

  1. 注册菜鸟开放平台https://open.taobao.com)并创建应用,获取 App KeyApp Secret
  2. 订购电子面单服务:在菜鸟服务市场订购顺丰等快递公司的电子面单服务,获取 月结卡号
  3. 获取模板ID:根据快递公司、纸张规格(如一联单76mm*130mm)获取对应的电子面单模板URL。
  4. 开通顺丰品牌:顺丰需要额外配置 brandCode = "SF",并在联调时联系顺丰技术确认。

5.2 接口调用流程

  1. 构建请求对象 CainiaoWaybillIiGetRequest
  2. 填充 WaybillCloudPrintApplyNewRequest,包括:
    • cpCode:快递公司编码(如 SF
    • productCode:顺丰专用,指定服务类型(产品编码,如 1 代表顺丰特快、2 代表顺丰标快)
    • sender / recipient:发件人/收件人信息(注意OAID隐私面单)
    • tradeOrderInfoDtos:包裹列表(支持多包裹,但顺丰超过10件需走子母件接口)
  3. 调用 client.execute(req, sessionKey) 获取响应。
  4. modules 中提取 waybill_codeprint_data

5.3 核心参数说明

参数 说明 注意事项
cpCode 快递公司编码 顺丰SF,中通ZTO
productCode 产品编码(顺丰必填) T4特快,需向顺丰获取映射表
brandCode 品牌编码(顺丰必填) 固定 SF
customerCode 月结卡号 顺丰和京东都需要
oaid 隐私面单标识 淘宝订单传入后可隐藏明文信息
needEncrypt 是否加密打印报文 oaid配合使用

5.4 常见错误码及处理

错误现象 可能原因 解决方案
isv.waybill-apply-error 月结卡号无效或未订购服务 检查 customerCode 和订购关系
产品编码不支持 productCode 错误 确认顺丰产品编码(如T4T6
发货地址没有匹配的电子面单服务 发件人地址未与月结卡号绑定 联系快递公司配置
运单号不足 账户余额不足 充值或检查订购量

六、实战:Java对接测试样例(可复制运行)

以下示例基于 菜鸟沙箱环境 编写,使用脱敏数据。您只需替换 AppKeyAppSecret月结卡号 即可运行验证。

6.1 Maven依赖(pom.xml)

<dependency>
    <groupId>com.taobao.api</groupId>
    <artifactId>taobao-sdk-java-auto</artifactId>
    <version>20240601</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

6.2 测试代码:单个顺丰包裹申请运单号

import com.taobao.api.DefaultTaobaoClient;
import com.taobao.api.TaobaoClient;
import com.taobao.api.request.CainiaoWaybillIiGetRequest;
import com.taobao.api.request.CainiaoWaybillIiGetRequest.*;
import com.taobao.api.response.CainiaoWaybillIiGetResponse;

public class SFWaybillTest {

    // 沙箱环境配置(请替换为您的真实沙箱账号)
    private static final String SANDBOX_URL = "http://qimen.api.taobao.com/router/qmtest";
    private static final String APP_KEY = "your_app_key";
    private static final String APP_SECRET = "your_app_secret";
    private static final String SESSION_KEY = "your_session_key"; // 通常从授权获取

    // 脱敏的客户信息
    private static final String SF_CUSTOMER_CODE = "010*******";   // 顺丰月结卡号
    private static final String SF_BRAND_CODE = "SF";
    private static final String SF_PRODUCT_CODE_T6 = "2";         // 顺丰标快,产品编码2,时效T6

    public static void main(String[] args) {
        try {
            // 1. 构建客户端
            TaobaoClient client = new DefaultTaobaoClient(SANDBOX_URL, APP_KEY, APP_SECRET);

            // 2. 创建请求对象
            CainiaoWaybillIiGetRequest request = new CainiaoWaybillIiGetRequest();
            WaybillCloudPrintApplyNewRequest applyRequest = new WaybillCloudPrintApplyNewRequest();

            // 3. 基础参数
            applyRequest.setCpCode("SF");
            applyRequest.setProductCode(SF_PRODUCT_CODE_T6);
            applyRequest.setBrandCode(SF_BRAND_CODE);
            applyRequest.setCustomerCode(SF_CUSTOMER_CODE);
            applyRequest.setNeedEncrypt(false);
            applyRequest.setMultiPackagesShipment(false);

            // 4. 发件人信息(脱敏)
            UserInfoDto sender = new UserInfoDto();
            AddressDto senderAddr = new AddressDto();
            senderAddr.setProvince("北京市");
            senderAddr.setCity("北京市");
            senderAddr.setDistrict("通州区");
            senderAddr.setDetail("科创十三街18号院");
            sender.setAddress(senderAddr);
            sender.setName("王先生");
            sender.setMobile("13912345678");
            applyRequest.setSender(sender);

            // 5. 订单信息列表(单包裹)
            java.util.List<TradeOrderInfoDto> tradeOrderList = new java.util.ArrayList<>();

            TradeOrderInfoDto order = new TradeOrderInfoDto();
            order.setObjectId("1");
            order.setTemplateUrl("https://example.com/template?id=123"); // 沙箱可使用任意合法URL

            // 5.1 订单渠道
            OrderInfoDto orderInfo = new OrderInfoDto();
            orderInfo.setOrderChannelsType("TM"); // 天猫
            order.setOrderInfo(orderInfo);

            // 5.2 包裹信息
            PackageInfoDto pkg = new PackageInfoDto();
            pkg.setId("1");
            pkg.setTotalPackagesCount(1L);
            pkg.setWeight(500L);   // 克
            pkg.setVolume(1000L);  // 立方厘米
            pkg.setGoodsDescription("图书");

            java.util.List<Item> items = new java.util.ArrayList<>();
            Item item = new Item();
            item.setCount(2L);
            item.setName("Java编程思想");
            items.add(item);
            pkg.setItems(items);
            order.setPackageInfo(pkg);

            // 5.3 收件人信息(脱敏)
            RecipientInfoDto recipient = new RecipientInfoDto();
            AddressDto recAddr = new AddressDto();
            recAddr.setProvince("上海市");
            recAddr.setCity("上海市");
            recAddr.setDistrict("浦东新区");
            recAddr.setDetail("世纪大道100号");
            recipient.setAddress(recAddr);
            recipient.setName("李女士");
            recipient.setPhone("15987654321");
            order.setRecipient(recipient);

            tradeOrderList.add(order);
            applyRequest.setTradeOrderInfoDtos(tradeOrderList);

            // 6. 其他公共参数
            applyRequest.setCallDoorPickUp(false);
            applyRequest.setDmsSorting(false);

            request.setParamWaybillCloudPrintApplyNewRequest(applyRequest);

            // 7. 发起调用
            CainiaoWaybillIiGetResponse response = client.execute(request, SESSION_KEY);

            // 8. 处理响应
            if (response.isSuccess()) {
                java.util.List<WaybillCloudPrintResponse> modules = response.getModules();
                if (modules != null && !modules.isEmpty()) {
                    String waybillCode = modules.get(0).getWaybillCode();
                    System.out.println("✅ 申请成功!运单号:" + waybillCode);
                    System.out.println("打印数据:" + modules.get(0).getPrintData());
                } else {
                    System.out.println("⚠️ 返回成功但modules为空");
                }
            } else {
                System.out.println("❌ 申请失败:" + response.getSubMsg());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6.3 测试多包裹场景(批量申请)

// 如需同时申请多个运单号(例如子母件),可在 tradeOrderList 中添加多个 TradeOrderInfoDto
// 每个包裹的 objectId 不同,且 totalPackagesCount 设置为总数
for (int i = 1; i <= 3; i++) {
    TradeOrderInfoDto subOrder = new TradeOrderInfoDto();
    subOrder.setObjectId(String.valueOf(i));
    // 其他构建逻辑相同...
    tradeOrderList.add(subOrder);
}

6.4 常用断言验证(单元测试风格)

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class SFWaybillApiTest {

    @Test
    public void testGetWaybillSuccess() {
        String waybillCode = callSFWaybillAPI(); // 封装上面逻辑
        Assertions.assertNotNull(waybillCode);
        Assertions.assertTrue(waybillCode.startsWith("SF"));
    }

    @Test
    public void testInvalidProductCode() {
        // 故意传错误 productCode
        Exception exception = Assertions.assertThrows(BusinessException.class, () -> {
            callSFWaybillAPIWithProductCode("INVALID");
        });
        Assertions.assertTrue(exception.getMessage().contains("产品编码不支持"));
    }
}

6.5 沙箱环境注意事项

  • 沙箱地址:http://qimen.api.taobao.com/router/qmtest
  • 沙箱不会真实发快递,但会返回模拟运单号(如SF1234567890)。
  • 沙箱环境下,productCode 传任意值都能成功,但正式环境必须正确。
  • 第一次调用沙箱需要确保已订购电子面单服务(沙箱免费)。

七、重构中保留的特殊业务细节

重构不是“想当然”地简化,必须严格保留原始逻辑。以下是几个容易忽略的点:

  1. 地址字段映射:原代码将 town(街道)赋值给了 district(区县),虽然奇怪但业务上已固化,保留。
  2. 随机订单号生成:仅当“顺丰 + 新媒体场景”时才生成10位随机串,用于填充交易单号。
  3. 商品明细条数限制
    • 普通订单:最多取前6条明细(奇门接口限制10条,此处取6条)。
    • 重复订单中的顺丰分支:只取1条明细;非顺丰分支:取全部明细。
  4. 发件人默认值:当 specialShipName 为空时,使用脱敏后的默认姓名“张**”和电话“138****0000”。
  5. 线下单跳过isOffLine 为 true 时不申请运单号。

八、踩坑与避坑指南

8.1 顺丰 brandCodecustomerCode 不能省略

即使已经在月结卡号中关联了品牌,调用电子面单接口时仍然需要显式传入 brandCode = "SF"customerCode,否则会报“未找到品牌”。

8.2 重复订单的已有运单号要正确扣除

重复订单场景下,需要先查询已存在的运单数量(exsitJianNum),然后只申请新增包裹的运单号,否则会导致运单号数量不足或浪费。

8.3 超过10件的订单只能走顺丰子母件

菜鸟奇门接口限制每个请求最多10个包裹,超过10件时必须使用顺丰子母件模式(调用 getQiMenWaybillSFMoreTen)。

8.4 模板URL不可用会直接导致取号失败

必须在调用前校验 standardTemplateUrl 是否为 null,否则接口会返回“模板不存在”错误。

8.5 隐私面单的 oaidneedEncrypt 需同时设置

传入 oaid 后,必须设置 needEncrypt = true,否则面单上仍会显示明文信息。


九、参考资料与文档

注:以上链接为官方入口,具体参数以最新文档为准。


十、总结

通过这次重构,我们不仅消除了“祖传代码”的技术债务,还建立了一套可复用的对接模式:

  • 性能提升:消除N+1查询,接口响应时间降低50%以上。
  • 可维护性飞跃:新人接手时不再需要忍受 obj1~obj16 的折磨。
  • 扩展能力增强:后续新增极兔、德邦等快递,只需在常量中添加编码,并在 callPlatformWaybillMethod 中增加一个分支。

最后,送给所有正在维护老代码的开发者一句话:重构不是炫技,而是为了让代码更好地表达业务。保持行为不变,提升可理解性,是对自己和团队最大的负责。

如果您也在对接奇门电子面单,欢迎留言交流。如果本文对您有帮助,请点赞、收藏、分享,让更多同行少走弯路。


本文系原创,首发于CSDN。转载需注明出处,并保持内容完整。
附:示例代码已脱敏,可直接复制到沙箱环境运行验证。
👉 点击关注我,更新后第一时间收到推送相关文章!

Logo

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

更多推荐