Pārlūkot izejas kodu

快递鸟相关代码

zyy 1 nedēļu atpakaļ
vecāks
revīzija
adbbd41144
33 mainītis faili ar 2524 papildinājumiem un 0 dzēšanām
  1. 113 0
      fs-admin/src/main/java/com/fs/kdniao/config/KdniaoConfig.java
  2. 46 0
      fs-admin/src/main/java/com/fs/kdniao/controller/KdniaoEOrderController.java
  3. 25 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoAddService.java
  4. 47 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoCommodity.java
  5. 181 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderRequest.java
  6. 62 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderResponse.java
  7. 55 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoPerson.java
  8. 192 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoSimpleOrderRequest.java
  9. 18 0
      fs-admin/src/main/java/com/fs/kdniao/service/IKdniaoEOrderService.java
  10. 214 0
      fs-admin/src/main/java/com/fs/kdniao/service/impl/KdniaoEOrderServiceImpl.java
  11. 114 0
      fs-admin/src/main/java/com/fs/kdniao/util/KdniaoUtil.java
  12. 38 0
      fs-admin/src/main/java/com/fs/kdniaoNew/config/KdniaoUniversalConfig.java
  13. 45 0
      fs-admin/src/main/java/com/fs/kdniaoNew/controller/KdniaoUniversalEOrderController.java
  14. 25 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoAddServiceNew.java
  15. 58 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoCarrierConfig.java
  16. 47 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoCommodityNew.java
  17. 55 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoPersonNew.java
  18. 240 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoSubmitCommand.java
  19. 21 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoUniversalResponse.java
  20. 73 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/AbstractKdniaoCarrierRule.java
  21. 22 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/IKdniaoCarrierRule.java
  22. 27 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/DefaultCarrierRule.java
  23. 33 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/EmsCarrierRule.java
  24. 40 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JdKyCarrierRule.java
  25. 32 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JdsxyyCarrierRule.java
  26. 32 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JosCarrierRule.java
  27. 32 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/SfCarrierRule.java
  28. 32 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/ZtoCarrierRule.java
  29. 32 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/ZtoColdCarrierRule.java
  30. 15 0
      fs-admin/src/main/java/com/fs/kdniaoNew/service/IKdniaoUniversalEOrderService.java
  31. 253 0
      fs-admin/src/main/java/com/fs/kdniaoNew/service/impl/KdniaoUniversalEOrderServiceImpl.java
  32. 95 0
      fs-admin/src/main/java/com/fs/kdniaoNew/util/KdniaoRequestUtil.java
  33. 210 0
      fs-service/src/main/resources/application-dev.yml

+ 113 - 0
fs-admin/src/main/java/com/fs/kdniao/config/KdniaoConfig.java

@@ -0,0 +1,113 @@
+package com.fs.kdniao.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 快递鸟配置类
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "kdniao")
+public class KdniaoConfig {
+
+    /**
+     * 快递鸟用户ID
+     */
+    private String eBusinessID;
+
+    /**
+     * 快递鸟API Key
+     */
+    private String apiKey;
+
+    /**
+     * 电子面单接口地址
+     */
+    private String reqURL;
+
+    /**
+     * 默认电子面单账号配置
+     */
+    private Account account;
+
+    /**
+     * 默认发件人配置
+     */
+    private Sender sender;
+
+    /**
+     * 电子面单账号配置
+     */
+    @Data
+    public static class Account {
+
+        /**
+         * 电子面单账号
+         */
+        private String customerName;
+
+        /**
+         * 电子面单密码
+         */
+        private String customerPwd;
+
+        /**
+         * 发件网点编码
+         */
+        private String sendSite;
+
+        /**
+         * 月结号
+         */
+        private String monthCode;
+    }
+
+    /**
+     * 默认发件人配置
+     */
+    @Data
+    public static class Sender {
+
+        /**
+         * 发件公司
+         */
+        private String company;
+
+        /**
+         * 发件人姓名
+         */
+        private String name;
+
+        /**
+         * 发件人手机号
+         */
+        private String mobile;
+
+        /**
+         * 发件省
+         */
+        private String provinceName;
+
+        /**
+         * 发件市
+         */
+        private String cityName;
+
+        /**
+         * 发件区/县
+         */
+        private String expAreaName;
+
+        /**
+         * 发件详细地址
+         */
+        private String address;
+
+        /**
+         * 发件邮编
+         */
+        private String postCode;
+    }
+}

+ 46 - 0
fs-admin/src/main/java/com/fs/kdniao/controller/KdniaoEOrderController.java

@@ -0,0 +1,46 @@
+package com.fs.kdniao.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.kdniao.domain.KdniaoEOrderResponse;
+import com.fs.kdniao.domain.KdniaoSimpleOrderRequest;
+import com.fs.kdniao.service.IKdniaoEOrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 快递鸟电子面单控制器
+ */
+@RestController
+@RequestMapping("/kdniao/eorder")
+public class KdniaoEOrderController extends BaseController {
+
+    @Autowired
+    private IKdniaoEOrderService kdniaoEOrderService;
+
+    /**
+     * 简化参数下单接口
+     * 前端只需要传常用业务参数,后端自动组装 RequestData
+     */
+    @Log(title = "快递鸟电子面单", businessType = BusinessType.INSERT)
+    @PostMapping("/submit")
+    public AjaxResult submit(@RequestBody KdniaoSimpleOrderRequest request) {
+        try {
+            KdniaoEOrderResponse response = kdniaoEOrderService.submitSimpleOrder(request);
+
+            if (Boolean.TRUE.equals(response.getSuccess()) && "100".equals(response.getResultCode())) {
+                return AjaxResult.success("下单成功", response);
+            }
+
+            if ("106".equals(response.getResultCode())) {
+                return AjaxResult.error("订单号重复,快递鸟返回:该订单号已下单成功");
+            }
+
+            return AjaxResult.error("下单失败:" + response.getReason(), response);
+        } catch (Exception e) {
+            return AjaxResult.error("电子面单下单异常:" + e.getMessage());
+        }
+    }
+}

+ 25 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoAddService.java

@@ -0,0 +1,25 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+/**
+ * 增值服务(保价、代收货款等)
+ */
+@Data
+public class KdniaoAddService {
+
+    /**
+     * 服务名称(如 COD、INSURE)
+     */
+    private String Name;
+
+    /**
+     * 服务值(金额/参数)
+     */
+    private String Value;
+
+    /**
+     * 客户标识
+     */
+    private String CustomerID;
+}

+ 47 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoCommodity.java

@@ -0,0 +1,47 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 商品信息
+ */
+@Data
+public class KdniaoCommodity {
+
+    /**
+     * 商品名称
+     */
+    private String GoodsName;
+
+    /**
+     * 商品编码
+     */
+    private String GoodsCode;
+
+    /**
+     * 商品数量
+     */
+    private Integer Goodsquantity;
+
+    /**
+     * 商品价格
+     */
+    private BigDecimal GoodsPrice;
+
+    /**
+     * 商品重量
+     */
+    private BigDecimal GoodsWeight;
+
+    /**
+     * 商品描述
+     */
+    private String GoodsDesc;
+
+    /**
+     * 商品体积
+     */
+    private BigDecimal GoodsVol;
+}

+ 181 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderRequest.java

@@ -0,0 +1,181 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 快递鸟电子面单下单请求对象(RequestData)
+ */
+@Data
+public class KdniaoEOrderRequest {
+
+    /**
+     * 订单编号(必须唯一)
+     * 不能重复,否则返回106(幂等控制字段)
+     */
+    private String OrderCode;
+
+    /**
+     * 快递公司编码(如 SF、YTO、ZTO、JDKY 等)
+     */
+    private String ShipperCode;
+
+    /**
+     * 电子面单账号(部分快递必填,如顺丰、圆通等)
+     */
+    private String CustomerName;
+
+    /**
+     * 电子面单密码
+     */
+    private String CustomerPwd;
+
+    /**
+     * 发件网点编码
+     */
+    private String SendSite;
+
+    /**
+     * 发件业务员
+     */
+    private String SendStaff;
+
+    /**
+     * 月结账号
+     */
+    private String MonthCode;
+
+    /**
+     * 运费支付方式
+     * 1:现付
+     * 2:到付
+     * 3:月结
+     * 4:第三方付(部分公司支持)
+     */
+    private Integer PayType;
+
+    /**
+     * 快递业务类型(不同快递公司不同)
+     */
+    private String ExpType;
+
+    /**
+     * 发件人信息(必填)
+     */
+    private KdniaoPerson Sender;
+
+    /**
+     * 收件人信息(必填)
+     */
+    private KdniaoPerson Receiver;
+
+    /**
+     * 包裹数量(>=1)
+     */
+    private Integer Quantity;
+
+    /**
+     * 总重量(kg)
+     * 京东/快运类必填
+     */
+    private BigDecimal Weight;
+
+    /**
+     * 总体积(m³)
+     * 京东/快运类必填
+     */
+    private BigDecimal Volume;
+
+    /**
+     * 运费(部分到付场景必填)
+     */
+    private BigDecimal Cost;
+
+    /**
+     * 其他费用
+     */
+    private BigDecimal OtherCost;
+
+    /**
+     * 增值服务(保价、代收货款等)
+     */
+    private List<KdniaoAddService> AddService;
+
+    /**
+     * 备注(会打印在面单上)
+     */
+    private String Remark;
+
+    /**
+     * 商品信息(至少一个 GoodsName)
+     */
+    private List<KdniaoCommodity> Commodity;
+
+    /**
+     * 是否返回电子面单模板
+     * 0:否
+     * 1:是
+     */
+    private String IsReturnPrintTemplate;
+
+    /**
+     * 面单模板尺寸(如 130)
+     */
+    private String TemplateSize;
+
+    /**
+     * 自定义打印内容
+     */
+    private String CustomArea;
+
+    /**
+     * 是否订阅轨迹推送
+     * 1:订阅(默认)
+     * 0:不订阅(避免消耗余额)
+     */
+    private String IsSubscribe;
+
+    /**
+     * 自定义回传字段
+     */
+    private String Callback;
+
+    /**
+     * 是否通知快递员上门揽件
+     * 0:通知
+     * 1:不通知
+     */
+    private Integer IsNotice;
+
+    /**
+     * 上门揽件开始时间(格式:yyyy-MM-dd HH:mm:ss)
+     */
+    private String StartDate;
+
+    /**
+     * 上门揽件结束时间
+     */
+    private String EndDate;
+
+    /**
+     * 是否要求签回单
+     * 0:否
+     * 1:是
+     */
+    private Integer IsReturnSignBill;
+
+    /**
+     * 是否发送短信通知
+     * 0:否
+     * 1:是
+     */
+    private Integer IsSendMessage;
+
+    /**
+     * 币种(顺丰港澳台必填)
+     * CNY / HKD / NTD
+     */
+    private String CurrencyCode;
+}

+ 62 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderResponse.java

@@ -0,0 +1,62 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+/**
+ * 快递鸟电子面单返回对象
+ */
+@Data
+public class KdniaoEOrderResponse {
+
+    /**
+     * 用户ID
+     */
+    private String EBusinessID;
+
+    /**
+     * 是否成功
+     */
+    private Boolean Success;
+
+    /**
+     * 返回编码
+     */
+    private String ResultCode;
+
+    /**
+     * 返回原因
+     */
+    private String Reason;
+
+    /**
+     * 订单信息
+     */
+    private Order Order;
+
+    /**
+     * 面单模板
+     */
+    private String PrintTemplate;
+
+    /**
+     * 运单信息
+     */
+    @Data
+    public static class Order {
+
+        /**
+         * 订单编号
+         */
+        private String OrderCode;
+
+        /**
+         * 快递公司编码
+         */
+        private String ShipperCode;
+
+        /**
+         * 运单号
+         */
+        private String LogisticCode;
+    }
+}

+ 55 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoPerson.java

@@ -0,0 +1,55 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+/**
+ * 发件人 / 收件人信息
+ */
+@Data
+public class KdniaoPerson {
+
+    /**
+     * 公司名称
+     */
+    private String Company;
+
+    /**
+     * 姓名
+     */
+    private String Name;
+
+    /**
+     * 电话
+     */
+    private String Tel;
+
+    /**
+     * 手机号
+     */
+    private String Mobile;
+
+    /**
+     * 省
+     */
+    private String ProvinceName;
+
+    /**
+     * 市
+     */
+    private String CityName;
+
+    /**
+     * 区/县
+     */
+    private String ExpAreaName;
+
+    /**
+     * 详细地址
+     */
+    private String Address;
+
+    /**
+     * 邮编
+     */
+    private String PostCode;
+}

+ 192 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoSimpleOrderRequest.java

@@ -0,0 +1,192 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 前端简化下单请求对象
+ */
+@Data
+public class KdniaoSimpleOrderRequest {
+
+    /**
+     * 业务订单号
+     * 传系统自己的订单号
+     */
+    private String bizOrderNo;
+
+    /**
+     * 快递公司编码,如 SF、YTO、ZTO、JDKY、EMS
+     */
+    private String shipperCode;
+
+    /**
+     * 运费支付方式
+     * 1:现付
+     * 2:到付
+     * 3:月结
+     */
+    private Integer payType;
+
+    /**
+     * 快递业务类型
+     * 常见值:1
+     */
+    private String expType;
+
+    /**
+     * 收件人姓名
+     */
+    private String receiverName;
+
+    /**
+     * 收件人手机号
+     */
+    private String receiverMobile;
+
+    /**
+     * 收件人电话
+     */
+    private String receiverTel;
+
+    /**
+     * 收件省
+     */
+    private String receiverProvinceName;
+
+    /**
+     * 收件市
+     */
+    private String receiverCityName;
+
+    /**
+     * 收件区/县
+     */
+    private String receiverExpAreaName;
+
+    /**
+     * 收件详细地址
+     * 不要包含省市区
+     */
+    private String receiverAddress;
+
+    /**
+     * 收件邮编
+     * EMS 场景建议传
+     */
+    private String receiverPostCode;
+
+    /**
+     * 商品名称
+     * 建议传类别,如:文件、衣服、电子产品
+     */
+    private String goodsName;
+
+    /**
+     * 商品数量
+     */
+    private Integer goodsQuantity;
+
+    /**
+     * 商品价格
+     */
+    private BigDecimal goodsPrice;
+
+    /**
+     * 商品重量
+     */
+    private BigDecimal goodsWeight;
+
+    /**
+     * 商品描述
+     */
+    private String goodsDesc;
+
+    /**
+     * 包裹数量
+     */
+    private Integer quantity;
+
+    /**
+     * 总重量(kg)
+     */
+    private BigDecimal weight;
+
+    /**
+     * 总体积(m³)
+     */
+    private BigDecimal volume;
+
+    /**
+     * 运费
+     */
+    private BigDecimal cost;
+
+    /**
+     * 其他费用
+     */
+    private BigDecimal otherCost;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 是否返回面单模板
+     * 0:否
+     * 1:是
+     */
+    private String isReturnPrintTemplate;
+
+    /**
+     * 面单模板尺寸
+     */
+    private String templateSize;
+
+    /**
+     * 是否订阅轨迹推送
+     * 1:订阅
+     * 0:不订阅
+     */
+    private String isSubscribe;
+
+    /**
+     * 是否通知上门取件
+     * 0:通知
+     * 1:不通知
+     */
+    private Integer isNotice;
+
+    /**
+     * 上门取件开始时间
+     * 格式:yyyy-MM-dd HH:mm:ss
+     */
+    private String startDate;
+
+    /**
+     * 上门取件结束时间
+     */
+    private String endDate;
+
+    /**
+     * 是否要求签回单
+     * 0:否
+     * 1:是
+     */
+    private Integer isReturnSignBill;
+
+    /**
+     * 是否发送短信
+     * 0:否
+     * 1:是
+     */
+    private Integer isSendMessage;
+
+    /**
+     * 币种
+     * 特殊场景需要
+     */
+    private String currencyCode;
+}

+ 18 - 0
fs-admin/src/main/java/com/fs/kdniao/service/IKdniaoEOrderService.java

@@ -0,0 +1,18 @@
+package com.fs.kdniao.service;
+
+import com.fs.kdniao.domain.KdniaoEOrderResponse;
+import com.fs.kdniao.domain.KdniaoSimpleOrderRequest;
+
+/**
+ * 快递鸟电子面单业务接口
+ */
+public interface IKdniaoEOrderService {
+
+    /**
+     * 前端简化参数下单
+     *
+     * @param request 简化请求参数
+     * @return 下单结果
+     */
+    KdniaoEOrderResponse submitSimpleOrder(KdniaoSimpleOrderRequest request);
+}

+ 214 - 0
fs-admin/src/main/java/com/fs/kdniao/service/impl/KdniaoEOrderServiceImpl.java

@@ -0,0 +1,214 @@
+package com.fs.kdniao.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.kdniao.config.KdniaoConfig;
+import com.fs.kdniao.domain.KdniaoCommodity;
+import com.fs.kdniao.domain.KdniaoEOrderRequest;
+import com.fs.kdniao.domain.KdniaoEOrderResponse;
+import com.fs.kdniao.domain.KdniaoPerson;
+import com.fs.kdniao.domain.KdniaoSimpleOrderRequest;
+import com.fs.kdniao.service.IKdniaoEOrderService;
+import com.fs.kdniao.util.KdniaoUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.net.URLEncoder;
+import java.util.Collections;
+
+/**
+ * 快递鸟电子面单业务实现类
+ */
+@Service
+public class KdniaoEOrderServiceImpl implements IKdniaoEOrderService {
+
+    @Autowired
+    private KdniaoConfig kdniaoConfig;
+
+    /**
+     * 前端简化参数下单
+     */
+    @Override
+    public KdniaoEOrderResponse submitSimpleOrder(KdniaoSimpleOrderRequest request) {
+        validateRequest(request);
+
+        KdniaoEOrderRequest eOrderRequest = buildEOrderRequest(request);
+
+        String requestData = KdniaoUtil.toRequestDataJson(eOrderRequest);
+        String dataSign = KdniaoUtil.getDataSign(requestData, kdniaoConfig.getApiKey());
+
+        try {
+            String formData = buildFormData(requestData, dataSign);
+            String result = KdniaoUtil.doPost(kdniaoConfig.getReqURL(), formData);
+            return JSON.parseObject(result, KdniaoEOrderResponse.class);
+        } catch (Exception e) {
+            throw new RuntimeException("电子面单下单失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 组装快递鸟标准请求对象
+     */
+    private KdniaoEOrderRequest buildEOrderRequest(KdniaoSimpleOrderRequest request) {
+        KdniaoEOrderRequest eOrderRequest = new KdniaoEOrderRequest();
+
+        // 基础字段
+        eOrderRequest.setOrderCode(buildOrderCode(request.getBizOrderNo()));
+        eOrderRequest.setShipperCode(request.getShipperCode());
+        eOrderRequest.setCustomerName(kdniaoConfig.getAccount().getCustomerName());
+        eOrderRequest.setCustomerPwd(kdniaoConfig.getAccount().getCustomerPwd());
+        eOrderRequest.setSendSite(kdniaoConfig.getAccount().getSendSite());
+        eOrderRequest.setMonthCode(kdniaoConfig.getAccount().getMonthCode());
+        eOrderRequest.setPayType(request.getPayType());
+        eOrderRequest.setExpType(request.getExpType());
+
+        // 发件人
+        eOrderRequest.setSender(buildSender());
+
+        // 收件人
+        eOrderRequest.setReceiver(buildReceiver(request));
+
+        // 商品
+        eOrderRequest.setCommodity(Collections.singletonList(buildCommodity(request)));
+
+        // 包裹信息
+        eOrderRequest.setQuantity(request.getQuantity() == null ? 1 : request.getQuantity());
+        eOrderRequest.setWeight(request.getWeight());
+        eOrderRequest.setVolume(request.getVolume());
+        eOrderRequest.setCost(request.getCost());
+        eOrderRequest.setOtherCost(request.getOtherCost());
+
+        // 打印和备注
+        eOrderRequest.setRemark(trimToNull(request.getRemark()));
+        eOrderRequest.setIsReturnPrintTemplate(StringUtils.hasText(request.getIsReturnPrintTemplate()) ? request.getIsReturnPrintTemplate() : "1");
+        eOrderRequest.setTemplateSize(StringUtils.hasText(request.getTemplateSize()) ? request.getTemplateSize() : "130");
+        eOrderRequest.setIsSubscribe(StringUtils.hasText(request.getIsSubscribe()) ? request.getIsSubscribe() : "0");
+
+        // 上门取件
+        eOrderRequest.setIsNotice(request.getIsNotice());
+        eOrderRequest.setStartDate(trimToNull(request.getStartDate()));
+        eOrderRequest.setEndDate(trimToNull(request.getEndDate()));
+
+        // 其他
+        eOrderRequest.setIsReturnSignBill(request.getIsReturnSignBill());
+        eOrderRequest.setIsSendMessage(request.getIsSendMessage());
+        eOrderRequest.setCurrencyCode(trimToNull(request.getCurrencyCode()));
+
+        return eOrderRequest;
+    }
+
+    /**
+     * 构建订单号
+     * 规则:业务订单号 + 时间戳,保证唯一
+     */
+    private String buildOrderCode(String bizOrderNo) {
+        if (StringUtils.hasText(bizOrderNo)) {
+            return bizOrderNo.trim() + "-" + System.currentTimeMillis();
+        }
+        return "KD" + System.currentTimeMillis();
+    }
+
+    /**
+     * 组装默认发件人
+     */
+    private KdniaoPerson buildSender() {
+        KdniaoPerson sender = new KdniaoPerson();
+        sender.setCompany(trimToNull(kdniaoConfig.getSender().getCompany()));
+        sender.setName(kdniaoConfig.getSender().getName());
+        sender.setMobile(kdniaoConfig.getSender().getMobile());
+        sender.setProvinceName(kdniaoConfig.getSender().getProvinceName());
+        sender.setCityName(kdniaoConfig.getSender().getCityName());
+        sender.setExpAreaName(kdniaoConfig.getSender().getExpAreaName());
+        sender.setAddress(kdniaoConfig.getSender().getAddress());
+        sender.setPostCode(trimToNull(kdniaoConfig.getSender().getPostCode()));
+        return sender;
+    }
+
+    /**
+     * 组装收件人
+     */
+    private KdniaoPerson buildReceiver(KdniaoSimpleOrderRequest request) {
+        KdniaoPerson receiver = new KdniaoPerson();
+        receiver.setName(request.getReceiverName());
+        receiver.setMobile(trimToNull(request.getReceiverMobile()));
+        receiver.setTel(trimToNull(request.getReceiverTel()));
+        receiver.setProvinceName(request.getReceiverProvinceName());
+        receiver.setCityName(request.getReceiverCityName());
+        receiver.setExpAreaName(request.getReceiverExpAreaName());
+        receiver.setAddress(request.getReceiverAddress());
+        receiver.setPostCode(trimToNull(request.getReceiverPostCode()));
+        return receiver;
+    }
+
+    /**
+     * 组装商品信息
+     */
+    private KdniaoCommodity buildCommodity(KdniaoSimpleOrderRequest request) {
+        KdniaoCommodity commodity = new KdniaoCommodity();
+        commodity.setGoodsName(request.getGoodsName());
+        commodity.setGoodsquantity(request.getGoodsQuantity() == null ? 1 : request.getGoodsQuantity());
+        commodity.setGoodsPrice(request.getGoodsPrice());
+        commodity.setGoodsWeight(request.getGoodsWeight());
+        commodity.setGoodsDesc(trimToNull(request.getGoodsDesc()));
+        return commodity;
+    }
+
+    /**
+     * 构建表单请求参数
+     */
+    private String buildFormData(String requestData, String dataSign) throws Exception {
+        StringBuilder sb = new StringBuilder();
+        sb.append("RequestData=").append(URLEncoder.encode(requestData, "UTF-8"));
+        sb.append("&EBusinessID=").append(URLEncoder.encode(kdniaoConfig.getEBusinessID(), "UTF-8"));
+        sb.append("&RequestType=").append(URLEncoder.encode("1007", "UTF-8"));
+        sb.append("&DataSign=").append(dataSign);
+        sb.append("&DataType=").append(URLEncoder.encode("2", "UTF-8"));
+        return sb.toString();
+    }
+
+    /**
+     * 前端简化参数校验
+     */
+    private void validateRequest(KdniaoSimpleOrderRequest request) {
+        if (request == null) {
+            throw new IllegalArgumentException("请求参数不能为空");
+        }
+        if (!StringUtils.hasText(request.getShipperCode())) {
+            throw new IllegalArgumentException("shipperCode不能为空");
+        }
+        if (request.getPayType() == null) {
+            throw new IllegalArgumentException("payType不能为空");
+        }
+        if (!StringUtils.hasText(request.getExpType())) {
+            throw new IllegalArgumentException("expType不能为空");
+        }
+        if (!StringUtils.hasText(request.getReceiverName())) {
+            throw new IllegalArgumentException("receiverName不能为空");
+        }
+        if (!StringUtils.hasText(request.getReceiverMobile()) && !StringUtils.hasText(request.getReceiverTel())) {
+            throw new IllegalArgumentException("receiverMobile和receiverTel至少填写一个");
+        }
+        if (!StringUtils.hasText(request.getReceiverProvinceName())) {
+            throw new IllegalArgumentException("receiverProvinceName不能为空");
+        }
+        if (!StringUtils.hasText(request.getReceiverCityName())) {
+            throw new IllegalArgumentException("receiverCityName不能为空");
+        }
+        if (!StringUtils.hasText(request.getReceiverExpAreaName())) {
+            throw new IllegalArgumentException("receiverExpAreaName不能为空");
+        }
+        if (!StringUtils.hasText(request.getReceiverAddress())) {
+            throw new IllegalArgumentException("receiverAddress不能为空");
+        }
+        if (!StringUtils.hasText(request.getGoodsName())) {
+            throw new IllegalArgumentException("goodsName不能为空");
+        }
+    }
+
+    /**
+     * 去除空白,空字符串转 null
+     */
+    private String trimToNull(String value) {
+        return StringUtils.hasText(value) ? value.trim() : null;
+    }
+}

+ 114 - 0
fs-admin/src/main/java/com/fs/kdniao/util/KdniaoUtil.java

@@ -0,0 +1,114 @@
+package com.fs.kdniao.util;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.kdniao.domain.KdniaoEOrderRequest;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.util.Base64;
+
+/**
+ * 快递鸟工具类
+ */
+public class KdniaoUtil {
+
+    private KdniaoUtil() {
+    }
+
+    /**
+     * 将标准请求对象转成 RequestData JSON 字符串
+     */
+    public static String toRequestDataJson(KdniaoEOrderRequest request) {
+        return JSON.toJSONString(request);
+    }
+
+    /**
+     * 生成 DataSign
+     * 规则:Base64(MD5(RequestData + ApiKey))
+     */
+    public static String getDataSign(String requestData, String apiKey) {
+        try {
+            String md5Result = md5(requestData + apiKey);
+            String base64 = Base64.getEncoder().encodeToString(md5Result.getBytes("UTF-8"));
+            return URLEncoder.encode(base64, "UTF-8");
+        } catch (Exception e) {
+            throw new RuntimeException("生成DataSign失败", e);
+        }
+    }
+
+    /**
+     * POST 表单请求
+     */
+    public static String doPost(String reqURL, String formData) {
+        HttpURLConnection connection = null;
+        OutputStream os = null;
+        BufferedReader br = null;
+        try {
+            URL url = new URL(reqURL);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setConnectTimeout(10000);
+            connection.setReadTimeout(20000);
+            connection.setDoOutput(true);
+            connection.setDoInput(true);
+            connection.setUseCaches(false);
+            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+
+            os = connection.getOutputStream();
+            os.write(formData.getBytes("UTF-8"));
+            os.flush();
+
+            br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = br.readLine()) != null) {
+                sb.append(line);
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new RuntimeException("调用快递鸟接口失败", e);
+        } finally {
+            try {
+                if (os != null) {
+                    os.close();
+                }
+            } catch (Exception ignored) {
+            }
+            try {
+                if (br != null) {
+                    br.close();
+                }
+            } catch (Exception ignored) {
+            }
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * MD5
+     */
+    private static String md5(String text) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] digest = md.digest(text.getBytes("UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            for (byte b : digest) {
+                String hex = Integer.toHexString(b & 0xff);
+                if (hex.length() == 1) {
+                    sb.append("0");
+                }
+                sb.append(hex);
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new RuntimeException("MD5计算失败", e);
+        }
+    }
+}

+ 38 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/config/KdniaoUniversalConfig.java

@@ -0,0 +1,38 @@
+package com.fs.kdniaoNew.config;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 快递鸟统一配置
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "kdniao")
+public class KdniaoUniversalConfig {
+
+    /**
+     * 快递鸟用户ID
+     */
+    private String eBusinessId;
+
+    /**
+     * 快递鸟API Key
+     */
+    private String apiKey;
+
+    /**
+     * 请求地址
+     */
+    private String reqUrl;
+
+    /**
+     * 各快递公司独立配置
+     */
+    private Map<String, KdniaoCarrierConfig> carriers = new HashMap<>();
+}

+ 45 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/controller/KdniaoUniversalEOrderController.java

@@ -0,0 +1,45 @@
+package com.fs.kdniaoNew.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.domain.KdniaoUniversalResponse;
+import com.fs.kdniaoNew.service.IKdniaoUniversalEOrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 快递鸟统一电子面单控制器
+ */
+@RestController
+@RequestMapping("/kdniao/universal/eorder")
+public class KdniaoUniversalEOrderController extends BaseController {
+
+    @Autowired
+    private IKdniaoUniversalEOrderService kdniaoUniversalEOrderService;
+
+    /**
+     * 统一下单
+     */
+    @Log(title = "快递鸟统一电子面单", businessType = BusinessType.INSERT)
+    @PostMapping("/submit")
+    public AjaxResult submit(@RequestBody KdniaoSubmitCommand command) {
+        try {
+            KdniaoUniversalResponse response = kdniaoUniversalEOrderService.submit(command);
+
+            if (Boolean.TRUE.equals(response.getSuccess()) && "100".equals(response.getResultCode())) {
+                return AjaxResult.success("下单成功", response);
+            }
+
+            if ("106".equals(response.getResultCode())) {
+                return AjaxResult.error("订单号重复,快递鸟返回:该订单号已下单成功");
+            }
+
+            return AjaxResult.error("下单失败:" + response.getReason(), response);
+        } catch (Exception e) {
+            return AjaxResult.error("下单异常:" + e.getMessage());
+        }
+    }
+}

+ 25 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoAddServiceNew.java

@@ -0,0 +1,25 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+/**
+ * 增值服务
+ */
+@Data
+public class KdniaoAddServiceNew {
+
+    /**
+     * 服务名称
+     */
+    private String name;
+
+    /**
+     * 服务值
+     */
+    private Object value;
+
+    /**
+     * 客户标识
+     */
+    private String customerId;
+}

+ 58 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoCarrierConfig.java

@@ -0,0 +1,58 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 单个快递公司的账号配置
+ */
+@Data
+public class KdniaoCarrierConfig {
+
+    /**
+     * 电子面单账号
+     */
+    private String customerName;
+
+    /**
+     * 电子面单密码
+     */
+    private String customerPwd;
+
+    /**
+     * 发件网点编码
+     */
+    private String sendSite;
+
+    /**
+     * 发件业务员
+     */
+    private String sendStaff;
+
+    /**
+     * 月结号
+     */
+    private String monthCode;
+
+    /**
+     * 仓库编码 / 业务员编码
+     */
+    private String wareHouseId;
+
+    /**
+     * 会员ID
+     */
+    private String memberId;
+
+    /**
+     * 当前快递专属发件人
+     */
+    private KdniaoPersonNew sender;
+
+    /**
+     * 当前快递扩展字段
+     */
+    private Map<String, Object> extras = new HashMap<>();
+}

+ 47 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoCommodityNew.java

@@ -0,0 +1,47 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 商品信息
+ */
+@Data
+public class KdniaoCommodityNew {
+
+    /**
+     * 商品名称
+     */
+    private String goodsName;
+
+    /**
+     * 商品编码
+     */
+    private String goodsCode;
+
+    /**
+     * 商品数量
+     */
+    private Integer goodsQuantity;
+
+    /**
+     * 商品价格
+     */
+    private BigDecimal goodsPrice;
+
+    /**
+     * 商品重量
+     */
+    private BigDecimal goodsWeight;
+
+    /**
+     * 商品描述
+     */
+    private String goodsDesc;
+
+    /**
+     * 商品体积
+     */
+    private BigDecimal goodsVol;
+}

+ 55 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoPersonNew.java

@@ -0,0 +1,55 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+/**
+ * 发件人/收件人
+ */
+@Data
+public class KdniaoPersonNew {
+
+    /**
+     * 公司名称
+     */
+    private String company;
+
+    /**
+     * 姓名
+     */
+    private String name;
+
+    /**
+     * 电话
+     */
+    private String tel;
+
+    /**
+     * 手机号
+     */
+    private String mobile;
+
+    /**
+     * 省
+     */
+    private String provinceName;
+
+    /**
+     * 市
+     */
+    private String cityName;
+
+    /**
+     * 区/县
+     */
+    private String expAreaName;
+
+    /**
+     * 详细地址
+     */
+    private String address;
+
+    /**
+     * 邮编
+     */
+    private String postCode;
+}

+ 240 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoSubmitCommand.java

@@ -0,0 +1,240 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 快递鸟统一下单入参
+ * 设计说明:
+ * 1. 公共字段:所有快递共用
+ * 2. 专属字段:每个快递有·一个扩展对象
+ */
+@Data
+public class KdniaoSubmitCommand {
+
+    /**
+     * 业务订单号
+     */
+    private String bizOrderNo;
+
+    /**
+     * 快递公司编码
+     * 例如:SF、EMS、JDKY、JOS、JDSXYY、ZTO、ZTOCOLD
+     */
+    private String shipperCode;
+
+    /**
+     * 支付方式
+     * 1:现付
+     * 2:到付
+     * 3:月结
+     */
+    private Integer payType;
+
+    /**
+     * 业务类型
+     */
+    private String expType;
+
+    /**
+     * 收件人
+     */
+    private KdniaoPersonNew receiver;
+
+    /**
+     * 商品列表
+     */
+    private List<KdniaoCommodityNew> commodity;
+
+    /**
+     * 包裹数量
+     */
+    private Integer quantity;
+
+    /**
+     * 总重量
+     */
+    private BigDecimal weight;
+
+    /**
+     * 总体积
+     */
+    private BigDecimal volume;
+
+    /**
+     * 运费
+     */
+    private BigDecimal cost;
+
+    /**
+     * 其他费用
+     */
+    private BigDecimal otherCost;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 是否返回电子面单模板
+     * 0:否
+     * 1:是
+     */
+    private String isReturnPrintTemplate;
+
+    /**
+     * 模板尺寸
+     */
+    private String templateSize;
+
+    /**
+     * 是否订阅轨迹
+     * 0:否
+     * 1:是
+     */
+    private String isSubscribe;
+
+    /**
+     * 是否通知上门取件
+     * 某些快递需要
+     */
+    private Integer isNotice;
+
+    /**
+     * 上门取件开始时间
+     */
+    private String startDate;
+
+    /**
+     * 上门取件结束时间
+     */
+    private String endDate;
+
+    /**
+     * 增值服务
+     */
+    private List<KdniaoAddServiceNew> addService;
+
+    /**
+     * 顺丰专属参数
+     */
+    private SfExt sf;
+
+    /**
+     * EMS专属参数
+     * 当前先预留,后续扩展
+     */
+    private EmsExt ems;
+
+    /**
+     * 京东快运专属参数
+     */
+    private JdKyExt jdky;
+
+    /**
+     * 京东快递专属参数
+     */
+    private JosExt jos;
+
+    /**
+     * 京东生鲜医药专属参数
+     */
+    private JdsxyyExt jdsxyy;
+
+    /**
+     * 中通专属参数
+     */
+    private ZtoExt zto;
+
+    /**
+     * 中通冷链专属参数
+     */
+    private ZtoColdExt ztoCold;
+
+    // ================================== 不同快递专属扩展对象 ==================================
+
+    /**
+     * 顺丰专属参数
+     */
+    @Data
+    public static class SfExt {
+        /**
+         * 币种
+         * 例如:CNY、HKD、NTD、MOP
+         */
+        private String currencyCode;
+    }
+
+    /**
+     * EMS专属参数
+     */
+    @Data
+    public static class EmsExt {
+        /**
+         * 当前先预留,后续 EMS 有新增前端字段时可放这里
+         */
+        private String reserved;
+    }
+
+
+    /**
+     * 京东快运专属参数
+     */
+    @Data
+    public static class JdKyExt {
+        /**
+         * 配送方式
+         */
+        private Integer deliveryMethod;
+    }
+
+
+    /**
+     * 京东快递专属参数
+     */
+    @Data
+    public static class JosExt {
+        /**
+         * 运输类型
+         */
+        private String transType;
+    }
+
+
+    /**
+     * 京东生鲜医药专属参数
+     */
+    @Data
+    public static class JdsxyyExt {
+        /**
+         * 运输类型
+         */
+        private String transType;
+    }
+
+    /**
+     * 中通专属参数(expType 为 21,22,23 时必填)
+     */
+    @Data
+    public static class ZtoExt {
+        /**
+         * 操作指令
+         */
+        private String sendSite  ;
+    }
+
+    /**
+     * 中通冷链专属参数
+     */
+    @Data
+    public static class ZtoColdExt {
+
+        /**
+         * 运输类型
+         */
+        private String transportType;
+    }
+}

+ 21 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoUniversalResponse.java

@@ -0,0 +1,21 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 快递鸟统一返回对象
+ */
+@Data
+public class KdniaoUniversalResponse {
+
+    private String EBusinessID;
+    private Boolean Success;
+    private String ResultCode;
+    private String Reason;
+    private Map<String, Object> Order;
+    private String PrintTemplate;
+    private String UniquerRequestNumber;
+    private Integer SubCount;
+}

+ 73 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/AbstractKdniaoCarrierRule.java

@@ -0,0 +1,73 @@
+package com.fs.kdniaoNew.rule;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+/**
+ * 快递规则抽象基类
+ */
+public abstract class AbstractKdniaoCarrierRule implements IKdniaoCarrierRule {
+
+    /**
+     * 有值才放入Map
+     */
+    protected void putIfHasText(Map<String, Object> map, String key, String value) {
+        if (StringUtils.hasText(value)) {
+            map.put(key, value.trim());
+        }
+    }
+
+    /**
+     * 不为空才放入Map
+     */
+    protected void putIfNotNull(Map<String, Object> map, String key, Object value) {
+        if (value != null) {
+            map.put(key, value);
+        }
+    }
+
+    /**
+     * 条件校验
+     */
+    protected void require(boolean condition, String message) {
+        if (!condition) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * 要求文本非空
+     */
+    protected void requireText(String value, String message) {
+        require(StringUtils.hasText(value), message);
+    }
+
+    /**
+     * 要求数字大于0
+     */
+    protected void requirePositive(BigDecimal value, String message) {
+        require(value != null && value.compareTo(BigDecimal.ZERO) > 0, message);
+    }
+
+    /**
+     * 填充账号字段
+     */
+    protected void fillAccountFields(KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        putIfHasText(requestData, "CustomerName", config.getCustomerName());
+        putIfHasText(requestData, "CustomerPwd", config.getCustomerPwd());
+        putIfHasText(requestData, "SendSite", config.getSendSite());
+        putIfHasText(requestData, "SendStaff", config.getSendStaff());
+        putIfHasText(requestData, "MonthCode", config.getMonthCode());
+        putIfHasText(requestData, "WareHouseID", config.getWareHouseId());
+        putIfHasText(requestData, "MemberID", config.getMemberId());
+
+        if (config.getExtras() != null && !config.getExtras().isEmpty()) {
+            requestData.putAll(config.getExtras());
+        }
+    }
+
+}

+ 22 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/IKdniaoCarrierRule.java

@@ -0,0 +1,22 @@
+package com.fs.kdniaoNew.rule;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+
+import java.util.Map;
+
+/**
+ * 快递规则接口
+ */
+public interface IKdniaoCarrierRule {
+
+    /**
+     * 当前规则是否支持该快递
+     */
+    boolean supports(String shipperCode);
+
+    /**
+     * 应用当前快递规则
+     */
+    void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData);
+}

+ 27 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/DefaultCarrierRule.java

@@ -0,0 +1,27 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 默认规则
+ */
+@Component
+public class DefaultCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return true;
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "当前快递未配置账号信息");
+        requireText(config.getCustomerName(), "当前快递要求customerName不能为空");
+        fillAccountFields(config, requestData);
+    }
+}

+ 33 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/EmsCarrierRule.java

@@ -0,0 +1,33 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.Map;
+
+/**
+ * EMS / 邮政规则
+ */
+@Component
+public class EmsCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "EMS".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "EMS未配置账号信息");
+        requireText(config.getCustomerName(), "EMS要求customerName不能为空");
+        require(config.getSender() != null, "EMS要求发件人配置不能为空");
+        requireText(config.getSender().getPostCode(), "EMS要求发件人postCode不能为空");
+        require(command.getReceiver() != null && StringUtils.hasText(command.getReceiver().getPostCode()),
+                "EMS要求收件人postCode不能为空");
+
+        fillAccountFields(config, requestData);
+    }
+}

+ 40 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JdKyCarrierRule.java

@@ -0,0 +1,40 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 京东快运规则
+ */
+@Component
+public class JdKyCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "JDKY".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "京东快运未配置账号信息");
+        requireText(config.getCustomerName(), "京东快运要求customerName不能为空");
+        requirePositive(command.getWeight(), "京东快运要求weight必填且大于0");
+        requirePositive(command.getVolume(), "京东快运要求volume必填且大于0");
+        require(command.getIsNotice() != null, "京东快运要求isNotice必填");
+
+        if (command.getIsNotice() == 0) {
+            requireText(command.getStartDate(), "京东快运在isNotice=0时要求startDate必填");
+            requireText(command.getEndDate(), "京东快运在isNotice=0时要求endDate必填");
+        }
+
+        fillAccountFields(config, requestData);
+
+        if (command.getJdky() != null) {
+            putIfNotNull(requestData, "DeliveryMethod", command.getJdky().getDeliveryMethod());
+        }
+    }
+}

+ 32 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JdsxyyCarrierRule.java

@@ -0,0 +1,32 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 京东生鲜医药规则
+ */
+@Component
+public class JdsxyyCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "JDSXYY".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "京东生鲜医药未配置账号信息");
+        requireText(config.getCustomerName(), "京东生鲜医药要求customerName不能为空");
+
+        fillAccountFields(config, requestData);
+
+        if (command.getJdsxyy() != null) {
+            putIfHasText(requestData, "TransType", command.getJdsxyy().getTransType());
+        }
+    }
+}

+ 32 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JosCarrierRule.java

@@ -0,0 +1,32 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 京东快递规则
+ */
+@Component
+public class JosCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "JOS".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "京东快递未配置账号信息");
+        requireText(config.getCustomerName(), "京东快递要求customerName不能为空");
+
+        fillAccountFields(config, requestData);
+
+        if (command.getJos() != null) {
+            putIfHasText(requestData, "TransType", command.getJos().getTransType());
+        }
+    }
+}

+ 32 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/SfCarrierRule.java

@@ -0,0 +1,32 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 顺丰规则
+ */
+@Component
+public class SfCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "SF".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "顺丰未配置账号信息");
+        requireText(config.getMonthCode(), "顺丰要求monthCode不能为空");
+
+        fillAccountFields(config, requestData);
+
+        if (command.getSf() != null) {
+            putIfHasText(requestData, "CurrencyCode", command.getSf().getCurrencyCode());
+        }
+    }
+}

+ 32 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/ZtoCarrierRule.java

@@ -0,0 +1,32 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 中通快递规则
+ */
+@Component
+public class ZtoCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "ZTO".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "中通未配置账号信息");
+        requireText(config.getCustomerName(), "中通要求customerName不能为空");
+        requireText(config.getCustomerPwd(), "中通要求customerPwd不能为空");
+
+        fillAccountFields(config, requestData);
+
+        if (command.getZto() != null) {
+        }
+    }
+}

+ 32 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/ZtoColdCarrierRule.java

@@ -0,0 +1,32 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 中通冷链规则
+ */
+@Component
+public class ZtoColdCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "ZTOCOLD".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "中通冷链未配置账号信息");
+        requireText(config.getCustomerName(), "中通冷链要求customerName不能为空");
+
+        fillAccountFields(config, requestData);
+
+        if (command.getZtoCold() != null) {
+            putIfHasText(requestData, "TransportType", command.getZtoCold().getTransportType());
+        }
+    }
+}

+ 15 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/service/IKdniaoUniversalEOrderService.java

@@ -0,0 +1,15 @@
+package com.fs.kdniaoNew.service;
+
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.domain.KdniaoUniversalResponse;
+
+/**
+ * 快递鸟统一电子面单服务
+ */
+public interface IKdniaoUniversalEOrderService {
+
+    /**
+     * 统一下单
+     */
+    KdniaoUniversalResponse submit(KdniaoSubmitCommand command);
+}

+ 253 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/service/impl/KdniaoUniversalEOrderServiceImpl.java

@@ -0,0 +1,253 @@
+package com.fs.kdniaoNew.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.kdniaoNew.config.KdniaoUniversalConfig;
+import com.fs.kdniaoNew.domain.*;
+import com.fs.kdniaoNew.rule.IKdniaoCarrierRule;
+import com.fs.kdniaoNew.rule.impl.DefaultCarrierRule;
+import com.fs.kdniaoNew.service.IKdniaoUniversalEOrderService;
+import com.fs.kdniaoNew.util.KdniaoRequestUtil;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.net.URLEncoder;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 快递鸟统一电子面单服务实现
+ *
+ * 核心思想:
+ * 1. 公共字段统一组装
+ * 2. 每个快递规则单独类实现
+ * 3. 每个快递配置单独放在 yml
+ */
+@Service
+public class KdniaoUniversalEOrderServiceImpl implements IKdniaoUniversalEOrderService {
+
+    private final KdniaoUniversalConfig kdniaoConfig;
+    private final List<IKdniaoCarrierRule> carrierRules;
+
+    public KdniaoUniversalEOrderServiceImpl(KdniaoUniversalConfig kdniaoConfig,
+                                            List<IKdniaoCarrierRule> carrierRules) {
+        this.kdniaoConfig = kdniaoConfig;
+        this.carrierRules = carrierRules;
+    }
+
+    @Override
+    public KdniaoUniversalResponse submit(KdniaoSubmitCommand command) {
+        //校验公共参数
+        validateCommon(command);
+
+        String shipperCode = command.getShipperCode() == null ? null : command.getShipperCode().trim().toUpperCase();
+
+        if (!StringUtils.hasText(shipperCode)) {
+            throw new IllegalArgumentException("shipperCode不能为空");
+        }
+
+        if (kdniaoConfig.getCarriers() == null || kdniaoConfig.getCarriers().isEmpty()) {
+            throw new IllegalArgumentException("kdniao.carriers配置为空,请检查application.yml");
+        }
+
+        KdniaoCarrierConfig config = kdniaoConfig.getCarriers().get(shipperCode);
+        if (config == null) {
+            throw new IllegalArgumentException("未配置快递公司账号信息:" + shipperCode);
+        }
+        if (config.getSender() == null) {
+            throw new IllegalArgumentException("未配置快递公司发件人信息:" + shipperCode);
+        }
+
+        Map<String, Object> requestData = buildCommonRequestData(command, config, shipperCode);
+
+        IKdniaoCarrierRule rule = resolveRule(shipperCode);
+        rule.apply(command, config, requestData);
+
+        try {
+            String requestDataJson = JSON.toJSONString(requestData);
+            String dataSign = KdniaoRequestUtil.getDataSign(requestDataJson, kdniaoConfig.getApiKey());
+
+            StringBuilder form = new StringBuilder();
+            form.append("RequestData=").append(URLEncoder.encode(requestDataJson, "UTF-8"));
+            form.append("&EBusinessID=").append(URLEncoder.encode(kdniaoConfig.getEBusinessId(), "UTF-8"));
+            form.append("&RequestType=").append(URLEncoder.encode("1007", "UTF-8"));
+            form.append("&DataSign=").append(dataSign);
+            form.append("&DataType=").append(URLEncoder.encode("2", "UTF-8"));
+
+            String resp = KdniaoRequestUtil.doPost(kdniaoConfig.getReqUrl(), form.toString());
+            return JSON.parseObject(resp, KdniaoUniversalResponse.class);
+        } catch (Exception e) {
+            throw new RuntimeException("调用快递鸟失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 构建公共请求体
+     */
+    private Map<String, Object> buildCommonRequestData(KdniaoSubmitCommand command,
+                                                       KdniaoCarrierConfig config,
+                                                       String shipperCode) {
+        Map<String, Object> data = new LinkedHashMap<>();
+
+        data.put("ShipperCode", shipperCode);
+        data.put("OrderCode", buildOrderCode(command.getBizOrderNo()));
+        data.put("PayType", command.getPayType());
+        data.put("ExpType", command.getExpType());
+
+        data.put("Sender", toPersonMap(config.getSender()));
+        data.put("Receiver", toPersonMap(command.getReceiver()));
+        data.put("Commodity", toCommodityList(command.getCommodity()));
+        data.put("Quantity", command.getQuantity() == null ? 1 : command.getQuantity());
+
+        putIfNotNull(data, "Weight", command.getWeight());
+        putIfNotNull(data, "Volume", command.getVolume());
+        putIfNotNull(data, "Cost", command.getCost());
+        putIfNotNull(data, "OtherCost", command.getOtherCost());
+
+        putIfHasText(data, "Remark", command.getRemark());
+        putIfHasText(data, "IsReturnPrintTemplate",
+                StringUtils.hasText(command.getIsReturnPrintTemplate()) ? command.getIsReturnPrintTemplate() : "1");
+        putIfHasText(data, "TemplateSize",
+                StringUtils.hasText(command.getTemplateSize()) ? command.getTemplateSize() : "130");
+        putIfHasText(data, "IsSubscribe",
+                StringUtils.hasText(command.getIsSubscribe()) ? command.getIsSubscribe() : "0");
+
+        putIfNotNull(data, "IsNotice", command.getIsNotice());
+        putIfHasText(data, "StartDate", command.getStartDate());
+        putIfHasText(data, "EndDate", command.getEndDate());
+
+        if (command.getAddService() != null && !command.getAddService().isEmpty()) {
+            List<Map<String, Object>> addServices = new ArrayList<>();
+            for (KdniaoAddServiceNew item : command.getAddService()) {
+                Map<String, Object> map = new LinkedHashMap<>();
+                putIfHasText(map, "Name", item.getName());
+                if (item.getValue() != null) {
+                    map.put("Value", item.getValue());
+                }
+                putIfHasText(map, "CustomerID", item.getCustomerId());
+                addServices.add(map);
+            }
+            data.put("AddService", addServices);
+        }
+
+        return data;
+    }
+
+    /**
+     * 解析规则
+     */
+    private IKdniaoCarrierRule resolveRule(String shipperCode) {
+        for (IKdniaoCarrierRule rule : carrierRules) {
+            if (!(rule instanceof DefaultCarrierRule) && rule.supports(shipperCode)) {
+                return rule;
+            }
+        }
+        for (IKdniaoCarrierRule rule : carrierRules) {
+            if (rule instanceof DefaultCarrierRule) {
+                return rule;
+            }
+        }
+        throw new IllegalStateException("未找到默认规则实现");
+    }
+
+    /**
+     * 公共参数校验
+     */
+    private void validateCommon(KdniaoSubmitCommand command) {
+        if (command == null) {
+            throw new IllegalArgumentException("请求参数不能为空");
+        }
+        if (!StringUtils.hasText(command.getShipperCode())) {
+            throw new IllegalArgumentException("shipperCode不能为空");
+        }
+        if (command.getPayType() == null) {
+            throw new IllegalArgumentException("payType不能为空");
+        }
+        if (!StringUtils.hasText(command.getExpType())) {
+            throw new IllegalArgumentException("expType不能为空");
+        }
+        if (command.getReceiver() == null) {
+            throw new IllegalArgumentException("receiver不能为空");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getName())) {
+            throw new IllegalArgumentException("receiver.name不能为空");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getMobile())
+                && !StringUtils.hasText(command.getReceiver().getTel())) {
+            throw new IllegalArgumentException("receiver.mobile和receiver.tel至少填写一个");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getProvinceName())) {
+            throw new IllegalArgumentException("receiver.provinceName不能为空");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getCityName())) {
+            throw new IllegalArgumentException("receiver.cityName不能为空");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getExpAreaName())) {
+            throw new IllegalArgumentException("receiver.expAreaName不能为空");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getAddress())) {
+            throw new IllegalArgumentException("receiver.address不能为空");
+        }
+        if (command.getCommodity() == null || command.getCommodity().isEmpty()) {
+            throw new IllegalArgumentException("commodity不能为空");
+        }
+        if (!StringUtils.hasText(command.getCommodity().get(0).getGoodsName())) {
+            throw new IllegalArgumentException("commodity.goodsName不能为空");
+        }
+    }
+
+    /**
+     * 构建订单号
+     */
+    private String buildOrderCode(String bizOrderNo) {
+        if (StringUtils.hasText(bizOrderNo)) {
+            return bizOrderNo.trim() + "-" + System.currentTimeMillis();
+        }
+        return "KD" + System.currentTimeMillis();
+    }
+
+    /**
+     * 发件人/收件人转Map
+     */
+    private Map<String, Object> toPersonMap(KdniaoPersonNew p) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        putIfHasText(map, "Company", p.getCompany());
+        putIfHasText(map, "Name", p.getName());
+        putIfHasText(map, "Tel", p.getTel());
+        putIfHasText(map, "Mobile", p.getMobile());
+        putIfHasText(map, "ProvinceName", p.getProvinceName());
+        putIfHasText(map, "CityName", p.getCityName());
+        putIfHasText(map, "ExpAreaName", p.getExpAreaName());
+        putIfHasText(map, "Address", p.getAddress());
+        putIfHasText(map, "PostCode", p.getPostCode());
+        return map;
+    }
+
+    /**
+     * 商品列表转Map列表
+     */
+    private List<Map<String, Object>> toCommodityList(List<KdniaoCommodityNew> commodityList) {
+        return commodityList.stream().map(c -> {
+            Map<String, Object> map = new LinkedHashMap<>();
+            putIfHasText(map, "GoodsName", c.getGoodsName());
+            putIfHasText(map, "GoodsCode", c.getGoodsCode());
+            putIfNotNull(map, "Goodsquantity", c.getGoodsQuantity());
+            putIfNotNull(map, "GoodsPrice", c.getGoodsPrice());
+            putIfNotNull(map, "GoodsWeight", c.getGoodsWeight());
+            putIfHasText(map, "GoodsDesc", c.getGoodsDesc());
+            putIfNotNull(map, "GoodsVol", c.getGoodsVol());
+            return map;
+        }).collect(Collectors.toList());
+    }
+
+    private void putIfHasText(Map<String, Object> map, String key, String value) {
+        if (StringUtils.hasText(value)) {
+            map.put(key, value.trim());
+        }
+    }
+
+    private void putIfNotNull(Map<String, Object> map, String key, Object value) {
+        if (value != null) {
+            map.put(key, value);
+        }
+    }
+}

+ 95 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/util/KdniaoRequestUtil.java

@@ -0,0 +1,95 @@
+package com.fs.kdniaoNew.util;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.util.Base64;
+
+/**
+ * 快递鸟请求工具类
+ */
+public class KdniaoRequestUtil {
+
+    private KdniaoRequestUtil() {
+    }
+
+    /**
+     * 生成 DataSign
+     */
+    public static String getDataSign(String requestData, String apiKey) {
+        try {
+            String md5 = md5Hex(requestData + apiKey);
+            String base64 = Base64.getEncoder().encodeToString(md5.getBytes("UTF-8"));
+            return URLEncoder.encode(base64, "UTF-8");
+        } catch (Exception e) {
+            throw new RuntimeException("生成DataSign失败", e);
+        }
+    }
+
+    /**
+     * POST表单请求
+     */
+    public static String doPost(String reqUrl, String formData) {
+        HttpURLConnection conn = null;
+        OutputStream os = null;
+        BufferedReader br = null;
+        try {
+            URL url = new URL(reqUrl);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("POST");
+            conn.setConnectTimeout(10000);
+            conn.setReadTimeout(20000);
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            conn.setUseCaches(false);
+            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+
+            os = conn.getOutputStream();
+            os.write(formData.getBytes("UTF-8"));
+            os.flush();
+
+            br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = br.readLine()) != null) {
+                sb.append(line);
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new RuntimeException("调用快递鸟失败", e);
+        } finally {
+            try {
+                if (os != null) os.close();
+            } catch (Exception ignored) {
+            }
+            try {
+                if (br != null) br.close();
+            } catch (Exception ignored) {
+            }
+            if (conn != null) conn.disconnect();
+        }
+    }
+
+    /**
+     * MD5
+     */
+    private static String md5Hex(String str) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] bytes = md.digest(str.getBytes("UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            for (byte b : bytes) {
+                String hex = Integer.toHexString(b & 0xff);
+                if (hex.length() == 1) sb.append('0');
+                sb.append(hex);
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new RuntimeException("MD5失败", e);
+        }
+    }
+}

+ 210 - 0
fs-service/src/main/resources/application-dev.yml

@@ -261,3 +261,213 @@ sysconfig:
     sysVersion: v20260217
     # 是否开启登陆时选择业务组
     show-dynamic-groupid: true
+
+
+#kdniao:
+#    # 快递鸟用户ID
+#    eBusinessID: 1762981
+#
+#    # 快递鸟API Key
+#    apiKey: 024e89b1-25c7-4725-8a3c-1bf1ca3ddcab
+#
+#    # 电子面单下单接口地址
+#    reqURL: https://api.kdniao.com/api/EOrderService
+#
+#    # 默认电子面单账号配置
+#    account:
+#        # 电子面单账号
+#        customerName:
+#        # 电子面单密码
+#        customerPwd:
+#        # 网点编码
+#        sendSite:
+#        # 月结号
+#        monthCode:
+#
+#    # 默认发件人配置
+#    sender:
+#        company: 云联融智
+#        name: 夏伟
+#        mobile: 12345678901
+#        provinceName: 重庆市
+#        cityName: 重庆市
+#        expAreaName: 渝北区
+#        address: 天宫殿街道财富大厦B座
+#        postCode: 401121
+
+kdniao:
+    eBusinessId: 1762981
+    apiKey: 024e89b1-25c7-4725-8a3c-1bf1ca3ddcab
+    reqUrl: https://api.kdniao.com/api/EOrderService
+
+    carriers:
+        SF:
+            customerName: 顺丰电子面单账号
+            customerPwd: 顺丰密码
+            monthCode: 顺丰月结号
+            sender:
+                company: 顺丰发货公司
+                name: 张三
+                mobile: 13800000001
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 福田区
+                address: 顺丰专用发货地址
+                postCode: 518000
+
+        EMS:
+            customerName: EMS电子面单账号
+            monthCode: EMS月结号
+            sender:
+                company: EMS发货公司
+                name: 李四
+                mobile: 13800000002
+                provinceName: 广东省
+                cityName: 广州市
+                expAreaName: 天河区
+                address: EMS专用发货地址
+                postCode: 510000
+
+        JDKY:
+            customerName: 京东快运账号
+            customerPwd: 京东快运密码
+            sendSite: 京东快运网点
+            monthCode: 京东快运月结号
+            wareHouseId: 仓库编码
+            sender:
+                company: 京东快运发货公司
+                name: 王五
+                mobile: 13800000003
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 宝安区
+                address: 京东快运专用发货地址
+                postCode: 518101
+            extras:
+                DeliveryMethod: 1
+
+        JOS:
+            customerName: 京东快递账号
+            customerPwd: 京东快递密码
+            monthCode: 京东快递月结号
+            sender:
+                company: 京东快递发货公司
+                name: 赵六
+                mobile: 13800000004
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 南山区
+                address: 京东快递专用发货地址
+                postCode: 518052
+
+        JDSXYY:
+            customerName: 京东生鲜医药账号
+            customerPwd: 京东生鲜医药密码
+            monthCode: 京东生鲜医药月结号
+            sender:
+                company: 京东生鲜医药发货公司
+                name: 孙七
+                mobile: 13800000005
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 龙华区
+                address: 京东生鲜医药专用发货地址
+                postCode: 518109
+
+        ZTO:
+            customerName: 中通电子面单账号
+            customerPwd: 中通电子面单密码
+            sendSite: 中通网点
+            monthCode: 中通月结号
+            sender:
+                company: 中通发货公司
+                name: 周八
+                mobile: 13800000006
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 龙岗区
+                address: 中通专用发货地址
+                postCode: 518100
+
+        ZTOCOLD:
+            customerName: 中通冷链账号
+            customerPwd: 中通冷链密码
+            sendSite: 中通冷链网点
+            monthCode: 中通冷链月结号
+            sender:
+                company: 中通冷链发货公司
+                name: 吴九
+                mobile: 13800000007
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 盐田区
+                address: 中通冷链专用发货地址
+                postCode: 518083
+
+        CNCY:
+            customerName: 菜鸟橙运账号
+            customerPwd: 菜鸟橙运密码
+            sender:
+                company: 菜鸟橙运发货公司
+                name: 郑十
+                mobile: 13800000008
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 罗湖区
+                address: 菜鸟橙运专用发货地址
+                postCode: 518001
+
+        CNSD:
+            customerName: 菜鸟速递账号
+            customerPwd: 菜鸟速递密码
+            sender:
+                company: 菜鸟速递发货公司
+                name: 钱一
+                mobile: 13800000009
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 坪山区
+                address: 菜鸟速递专用发货地址
+                postCode: 518118
+
+
+        BNSY:
+            customerName: 笨鸟速运账号
+            customerPwd: 笨鸟速运密码
+            monthCode: 笨鸟速运月结号
+            sender:
+                company: 笨鸟速运发货公司
+                name: 钱三
+                mobile: 13800000011
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 大鹏新区
+                address: 笨鸟速运专用发货地址
+                postCode: 518120
+
+        FYP:
+            customerName: 丰云配账号
+            customerPwd: 丰云配密码
+            monthCode: 丰云配月结号
+            sender:
+                company: 丰云配发货公司
+                name: 钱四
+                mobile: 13800000012
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 福田区
+                address: 丰云配专用发货地址
+                postCode: 518000
+
+        YJYYLL:
+            customerName: 云集医药冷链账号
+            customerPwd: 云集医药冷链密码
+            sender:
+                company: 云集医药冷链发货公司
+                name: 钱五
+                mobile: 13800000013
+                provinceName: 广东省
+                cityName: 深圳市
+                expAreaName: 南山区
+                address: 云集医药冷链专用发货地址
+                postCode: 518052