Kaynağa Gözat

Merge remote-tracking branch 'origin/master'

yuhongqi 4 gün önce
ebeveyn
işleme
03885537a8
49 değiştirilmiş dosya ile 2938 ekleme ve 44 silme
  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. 44 5
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyController.java
  34. 25 24
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  35. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiQwAddWxTaskNode.java
  36. 6 0
      fs-service/src/main/java/com/fs/company/vo/WorkflowExecRecordVo.java
  37. 29 0
      fs-service/src/main/java/com/fs/course/domain/FsPublicCourseTrafficLog.java
  38. 42 0
      fs-service/src/main/java/com/fs/course/mapper/FsPublicCourseTrafficLogMapper.java
  39. 3 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  40. 62 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  41. 8 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerProperty.java
  42. 14 2
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java
  43. 1 1
      fs-service/src/main/java/com/fs/live/mapper/LiveCouponMapper.java
  44. 40 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveAutoTaskServiceImpl.java
  45. 8 7
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  46. 210 0
      fs-service/src/main/resources/application-dev.yml
  47. 102 0
      fs-service/src/main/resources/mapper/course/FsPublicCourseTrafficLogMapper.xml
  48. 17 4
      fs-service/src/main/resources/mapper/crm/CrmCustomerPropertyMapper.xml
  49. 12 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

+ 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);
+        }
+    }
+}

+ 44 - 5
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyController.java

@@ -18,6 +18,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -136,25 +137,63 @@ public class CrmCustomerPropertyController extends BaseController {
         return toAjax(crmCustomerPropertyService.updateCrmCustomerProperty(property));
     }
 
+//    @ApiOperation("删除客户属性标签")
+//    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+//    @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
+//    @DeleteMapping("/{ids}")
+//    public AjaxResult remove(@PathVariable Long[] ids) {
+//        return toAjax(crmCustomerPropertyService.deleteCrmCustomerPropertyByIds(ids));
+//    }
+
+//    @ApiOperation("删除客户单个属性标签")
+//    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+//    @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
+//    @DeleteMapping("/deleteByPropertyId")
+//    public AjaxResult deleteByPropertyId(
+//            @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
+//            @ApiParam(required = true, name = "propertyId", value = "属性模板 ID") @RequestParam Long propertyId) {
+//        return toAjax(crmCustomerPropertyService.lambdaUpdate()
+//                .eq(CrmCustomerProperty::getCustomerId, customerId)
+//                .eq(CrmCustomerProperty::getPropertyId, propertyId)
+//                .remove());
+//    }
+
     @ApiOperation("删除客户属性标签")
-    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @PreAuthorize("@ss.hasPermi('crm:customerProperty:delete')")
     @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
     @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids) {
-        return toAjax(crmCustomerPropertyService.deleteCrmCustomerPropertyByIds(ids));
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Date now = new Date();
+        boolean success = crmCustomerPropertyService.lambdaUpdate()
+                .in(CrmCustomerProperty::getId, ids)
+                .eq(CrmCustomerProperty::getDeleted, 0)
+                .set(CrmCustomerProperty::getDeleted, 1)
+                .set(CrmCustomerProperty::getDeleteBy, loginUser.getUsername())
+                .set(CrmCustomerProperty::getDeleteTime, now)
+                .update();
+
+        return toAjax(success);
     }
 
     @ApiOperation("删除客户单个属性标签")
-    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @PreAuthorize("@ss.hasPermi('crm:customerProperty:delete')")
     @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
     @DeleteMapping("/deleteByPropertyId")
     public AjaxResult deleteByPropertyId(
             @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
             @ApiParam(required = true, name = "propertyId", value = "属性模板 ID") @RequestParam Long propertyId) {
-        return toAjax(crmCustomerPropertyService.lambdaUpdate()
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        boolean success = crmCustomerPropertyService.lambdaUpdate()
                 .eq(CrmCustomerProperty::getCustomerId, customerId)
                 .eq(CrmCustomerProperty::getPropertyId, propertyId)
-                .remove());
+                .eq(CrmCustomerProperty::getDeleted, 0)
+                .set(CrmCustomerProperty::getDeleted, 1)
+                .set(CrmCustomerProperty::getDeleteBy, loginUser.getUsername())
+                .set(CrmCustomerProperty::getDeleteTime, new Date())
+                .update();
+
+        return toAjax(success);
     }
 
     @ApiOperation("导出客户属性标签")

+ 25 - 24
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -1541,30 +1541,31 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 .filter(Objects::nonNull)
                 .collect(Collectors.toList());
 
-//        //拿取业务id
-//        List<Long> businessIds = records.stream()
-//                .map(WorkflowExecRecordVo::getBusinessId)
-//                .filter(Objects::nonNull)
-//                .collect(Collectors.toList());
-//
-//        if (!businessIds.isEmpty()) {
-//            List<CompanyVoiceRoboticBusiness> businesses = companyVoiceRoboticBusinessMapper.selectList(new LambdaQueryWrapper<CompanyVoiceRoboticBusiness>()
-//                    .in(CompanyVoiceRoboticBusiness::getId, businessIds));
-//            if (ObjectUtil.isNotEmpty(businesses)) {
-//                Map<Long, CompanyVoiceRoboticBusiness> businessMap = businesses.stream().collect(Collectors.toMap(CompanyVoiceRoboticBusiness::getId, Function.identity()));
-//                records.forEach(record -> {
-//                    if (record.getBusinessId() != null && businessMap.containsKey(record.getBusinessId())) {
-//                        CompanyVoiceRoboticBusiness business = businessMap.get(record.getBusinessId());
-//                        CompanyVoiceRoboticCallLogCallphone callLogCallphone = companyVoiceRoboticCallLogCallphoneMapper.selectOne(new LambdaQueryWrapper<CompanyVoiceRoboticCallLogCallphone>()
-//                                .eq(CompanyVoiceRoboticCallLogCallphone::getRoboticId, business.getRoboticId())
-//                                .eq(CompanyVoiceRoboticCallLogCallphone::getCallerId, business.getCalleeId()));
-//                        if (ObjectUtil.isNotEmpty(callLogCallphone)) {
-//                            record.setContentList(callLogCallphone.getContentList());
-//                        }
-//                    }
-//                });
-//            }
-//        }
+        //拿取业务id
+        List<Long> businessIds = records.stream()
+                .map(WorkflowExecRecordVo::getBusinessId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        if (!businessIds.isEmpty()) {
+            List<CompanyVoiceRoboticBusiness> businesses = companyVoiceRoboticBusinessMapper.selectList(new LambdaQueryWrapper<CompanyVoiceRoboticBusiness>()
+                    .in(CompanyVoiceRoboticBusiness::getId, businessIds));
+            if (ObjectUtil.isNotEmpty(businesses)) {
+                Map<Long, CompanyVoiceRoboticBusiness> businessMap = businesses.stream().collect(Collectors.toMap(CompanyVoiceRoboticBusiness::getId, Function.identity()));
+                records.forEach(record -> {
+                    if (record.getBusinessId() != null && businessMap.containsKey(record.getBusinessId())) {
+                        CompanyVoiceRoboticBusiness business = businessMap.get(record.getBusinessId());
+                        CompanyVoiceRoboticCallLogCallphone callLogCallphone = companyVoiceRoboticCallLogCallphoneMapper.selectOne(new LambdaQueryWrapper<CompanyVoiceRoboticCallLogCallphone>()
+                                .eq(CompanyVoiceRoboticCallLogCallphone::getRoboticId, business.getRoboticId())
+                                .eq(CompanyVoiceRoboticCallLogCallphone::getCallerId, business.getCalleeId()));
+                        if (ObjectUtil.isNotEmpty(callLogCallphone)) {
+                            record.setContentList(callLogCallphone.getContentList());
+                            record.setIntention(callLogCallphone.getIntention());
+                        }
+                    }
+                });
+            }
+        }
 
         if (!instanceIds.isEmpty()) {
             List<CompanyAiWorkflowExecLog> allLogs = companyAiWorkflowExecLogMapper.selectByInstanceIds(instanceIds);

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiQwAddWxTaskNode.java

@@ -31,7 +31,7 @@ public class AiQwAddWxTaskNode extends AbstractWorkflowNode {
     private static final CompanyWxClientMapper companyWxClientMapper = SpringUtils.getBean(CompanyWxClientMapper.class);
     @SuppressWarnings("unchecked")
     private static final RedisCacheT<String> redisCache = SpringUtils.getBean(RedisCacheT.class);
-    public static final String DELAY_QW_ADD_WX_KEY = "qwAddWxTask:delay:%s:%s:";
+    public static final String DELAY_QW_ADD_WX_KEY = "qwAddWxTask:delay:%s:%s:%s:";
     /**
      * 默认加微超时时间(分钟)
      */

+ 6 - 0
fs-service/src/main/java/com/fs/company/vo/WorkflowExecRecordVo.java

@@ -129,6 +129,12 @@ public class WorkflowExecRecordVo {
      */
     private String contentList;
 
+    /**
+     * 意向度
+     */
+    private String intention;
+
+
     /**
      * 节点执行日志VO
      */

+ 29 - 0
fs-service/src/main/java/com/fs/course/domain/FsPublicCourseTrafficLog.java

@@ -0,0 +1,29 @@
+package com.fs.course.domain;
+
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 公开课流量记录对象 fs_public_course_traffic_log
+ */
+@Data
+public class FsPublicCourseTrafficLog extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    private Long logId;
+
+    private String uuId;
+
+    private Long userId;
+
+    private Long courseId;
+
+    private Long videoId;
+
+    private Long internetTraffic;
+
+    private Integer status;
+
+    private  Long projectId;
+}

+ 42 - 0
fs-service/src/main/java/com/fs/course/mapper/FsPublicCourseTrafficLogMapper.java

@@ -0,0 +1,42 @@
+package com.fs.course.mapper;
+
+import com.fs.course.domain.FsPublicCourseTrafficLog;
+import com.fs.course.param.FsCourseTrafficLogParam;
+import com.fs.course.vo.FsCourseTrafficLogListVO;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * 公开课流量记录Mapper接口
+ */
+@Repository
+public interface FsPublicCourseTrafficLogMapper {
+
+    FsPublicCourseTrafficLog selectFsPublicCourseTrafficLogByLogId(Long logId);
+
+    int insertFsPublicCourseTrafficLog(FsPublicCourseTrafficLog fsPublicCourseTrafficLog);
+
+    int updateFsPublicCourseTrafficLog(FsPublicCourseTrafficLog fsPublicCourseTrafficLog);
+
+    @Select("select * from fs_public_course_traffic_log where uu_id = #{uuId}")
+    FsPublicCourseTrafficLog selectFsPublicCourseTrafficLogByUuId(@Param("uuId") String uuId);
+
+    void insertOrUpdateTrafficLog(FsPublicCourseTrafficLog trafficLog);
+
+    @Select("SELECT IFNULL(SUM(internet_traffic), 0) FROM fs_public_course_traffic_log " +
+            "WHERE DATE(create_time) = DATE(CURDATE()) AND company_id = #{companyId}")
+    Long getTodayTrafficLogCompanyId(@Param("companyId") Long companyId);
+
+    @Select("SELECT IFNULL(SUM(internet_traffic), 0) FROM fs_public_course_traffic_log " +
+            "WHERE DATE(create_time) = DATE(CURDATE() - INTERVAL 1 DAY) AND company_id = #{companyId}")
+    Long getYesterdayTrafficLogCompanyId(@Param("companyId") Long companyId);
+
+    @Select("SELECT IFNULL(SUM(internet_traffic), 0) FROM fs_public_course_traffic_log " +
+            "WHERE YEAR(create_time) = YEAR(CURDATE()) AND MONTH(create_time) = MONTH(CURDATE()) AND company_id = #{companyId}")
+    Long getMonthTrafficLogCompanyId(@Param("companyId") Long companyId);
+
+    List<FsCourseTrafficLogListVO> selectTrafficNew(FsCourseTrafficLogParam param);
+}

+ 3 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java

@@ -99,6 +99,9 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
 
     R getInternetTraffic(FsUserCourseVideoFinishUParam param);
 
+    //公开课流量统计
+    R getPublicCourseInternetTraffic(FsUserCourseVideoFinishUParam param);
+
     R getIntegralByH5Video(FsUserCourseVideoFinishUParam param);
 
     R sendReward(FsCourseSendRewardUParam param);

+ 62 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -194,6 +194,8 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Autowired
     private FsCourseTrafficLogMapper fsCourseTrafficLogMapper;
     @Autowired
+    private FsPublicCourseTrafficLogMapper fsPublicCourseTrafficLogMapper;
+    @Autowired
     private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
     @Autowired
     private FsUserMapper fsUserMapper;
@@ -1329,6 +1331,66 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         return R.ok();
     }
 
+    @Override
+    public R getPublicCourseInternetTraffic(FsUserCourseVideoFinishUParam param) {
+        try {
+            if (param.getBufferRate() == null) {
+                logger.error("【公开课缓冲值空】参数: {}", param);
+                return R.error("缓冲值空");
+            }
+            FsPublicCourseTrafficLog trafficLog = new FsPublicCourseTrafficLog();
+            trafficLog.setCreateTime(new Date());
+            BeanUtils.copyProperties(param, trafficLog);
+
+            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+            if (video == null) {
+                return R.error("视频不存在");
+            }
+            FsUserCourse fsUserCourse = fsUserCourseMapper.selectFsUserCourseByCourseId(param.getCourseId());
+            if (fsUserCourse != null) {
+                trafficLog.setProjectId(fsUserCourse.getProject());
+            }
+            BigDecimal result = param.getBufferRate().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP);
+            BigDecimal longAsBigDecimal = BigDecimal.valueOf(video.getFileSize());
+            long roundedResult = result.multiply(longAsBigDecimal).setScale(0, RoundingMode.HALF_UP).longValue();
+            trafficLog.setInternetTraffic(roundedResult);
+
+            if (StringUtils.isNotEmpty(trafficLog.getUuId())) {
+                fsPublicCourseTrafficLogMapper.insertOrUpdateTrafficLog(trafficLog);
+//                asyncDeductPublicCourseTraffic(company, trafficLog);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            logger.error("【公开课插入或更新流量失败】参数: {}, 错误信息:{}", param, e.getMessage(), e);
+            return R.error();
+        }
+        return R.ok();
+    }
+
+//    public void asyncDeductPublicCourseTraffic(Company company, FsPublicCourseTrafficLog trafficLog) {
+//        try {
+//            FsPublicCourseTrafficLog existingLog = fsPublicCourseTrafficLogMapper.selectFsPublicCourseTrafficLogByUuId(trafficLog.getUuId());
+//            long recordedTraffic;
+//            if (existingLog != null) {
+//                recordedTraffic = trafficLog.getInternetTraffic() - existingLog.getInternetTraffic();
+//            } else {
+//                recordedTraffic = trafficLog.getInternetTraffic();
+//            }
+//            if (recordedTraffic <= 0) {
+//                return;
+//            }
+//            Long remainingTraffic = updateRedisCache(company, recordedTraffic / 1024);
+//
+//            if ("1".equals(configUtil.generateConfigByKey("watch.course.config").getString("doNotPlay")) && remainingTraffic <= 0) {
+//                logger.warn("公开课公司ID: {} 流量不足,当前剩余: {}", company.getCompanyId(), remainingTraffic);
+//                throw new Exception("流量不足");
+//            }
+//        } catch (Exception e) {
+//            logger.error("公开课异步扣除流量失败 - 公司ID: {}, 错误信息: {}",
+//                    company.getCompanyId(), e.getMessage(), e);
+//        }
+//    }
+
     public void asyncDeductTraffic(Company company, FsCourseTrafficLog trafficLog) {
         try {
             //根据uuid查询

+ 8 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomerProperty.java

@@ -5,6 +5,8 @@ import com.fs.common.core.domain.BaseEntityTow;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.util.Date;
+
 @Data
 @EqualsAndHashCode(callSuper = true)
 public class CrmCustomerProperty extends BaseEntityTow {
@@ -35,4 +37,10 @@ public class CrmCustomerProperty extends BaseEntityTow {
 
     @Excel(name = "喜欢占比")
     private Integer likeRatio;
+
+    private Integer deleted;
+
+    private String deleteBy;
+
+    private Date deleteTime;
 }

+ 14 - 2
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java

@@ -23,6 +23,7 @@ import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -230,12 +231,23 @@ public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPrope
                 property.setTradeType("3");
                 property.setIntention("medium");
                 property.setLikeRatio(50);
+                property.setDeleted(0);
+                property.setDeleteBy(null);
+                property.setDeleteTime(null);
                 return property;
             }).collect(Collectors.toList());
-            remove(new LambdaQueryWrapper<CrmCustomerProperty>()
+//            remove(new LambdaQueryWrapper<CrmCustomerProperty>()
+//                    .eq(CrmCustomerProperty::getCustomerId, companyVoiceRoboticCallees.getUserId())
+//                    .in(CrmCustomerProperty::getPropertyId, ids)
+//            );
+            lambdaUpdate()
                     .eq(CrmCustomerProperty::getCustomerId, companyVoiceRoboticCallees.getUserId())
                     .in(CrmCustomerProperty::getPropertyId, ids)
-            );
+                    .eq(CrmCustomerProperty::getDeleted, 0)
+                    .set(CrmCustomerProperty::getDeleted, 1)
+                    .set(CrmCustomerProperty::getDeleteBy, "system")
+                    .set(CrmCustomerProperty::getDeleteTime, DateUtils.getNowDate())
+                    .update();
             saveBatch(propertyList);
         } catch (JsonProcessingException e) {
             throw new RuntimeException(e);

+ 1 - 1
fs-service/src/main/java/com/fs/live/mapper/LiveCouponMapper.java

@@ -79,7 +79,7 @@ public interface LiveCouponMapper
 
     @Select("<script>" +
             "select lc.*,lci.id from live_coupon_issue lci left join live_coupon lc on lc.coupon_id=lci.coupon_id " +
-            "where lci.status=1" +
+            "where lci.status=1 and lc.status=1 " +
             " and lc.title like concat('%', #{couponName}, '%')" +
             " and id not in (select coupon_issue_id as id from live_coupon_issue_relation where live_id = #{liveId})" +
             "</script>"

+ 40 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveAutoTaskServiceImpl.java

@@ -4,6 +4,7 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.ZoneId;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
@@ -21,6 +22,7 @@ import com.fs.live.vo.LiveLotteryProductListVo;
 import org.checkerframework.checker.units.qual.A;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -393,6 +395,44 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
         if (!updateList.isEmpty()) {
             baseMapper.batchUpdateLiveAutoTask(updateList);
         }
+
+        // 重新计算后,需要清理并重建Redis缓存
+        // 因为任务的触发时间(score)已经改变,旧的缓存已失效
+        try {
+            String cacheKey = "live:auto_task:" + live.getLiveId();
+
+            // 1. 删除旧的缓存
+            redisCache.deleteObject(cacheKey);
+
+            // 2. 如果直播间正在直播,重新构建缓存
+            if (live.getStatus() != null && live.getStatus() == 2) {
+                for (LiveAutoTask task : liveAutoTasks) {
+                    // 只缓存状态为启用且未完成的任务
+                    if (task.getStatus() != null && task.getStatus() == 1L
+                            && task.getFinishStatus() != null && task.getFinishStatus() == 0L) {
+                        LiveAutoTask cacheTask = new LiveAutoTask();
+                        BeanUtils.copyProperties(task, cacheTask);
+                        cacheTask.setCreateTime(null);
+                        cacheTask.setUpdateTime(null);
+
+                        redisCache.zSetAdd(cacheKey, JSON.toJSONString(cacheTask), task.getAbsValue().getTime());
+                    }
+                }
+
+                // 设置过期时间
+                if (!liveAutoTasks.isEmpty()) {
+                    redisCache.expire(cacheKey, 1, TimeUnit.DAYS);
+                }
+
+                log.info("直播间开始时间变化,已重新构建自动化任务缓存: liveId={}, 任务数={}",
+                        live.getLiveId(), liveAutoTasks.size());
+            } else {
+                log.info("直播间未开播,已清理自动化任务缓存: liveId={}", live.getLiveId());
+            }
+        } catch (Exception e) {
+            log.error("重新构建自动化任务缓存失败: liveId={}, error={}",
+                    live.getLiveId(), e.getMessage(), e);
+        }
     }
 
     @Override

+ 8 - 7
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -1183,13 +1183,14 @@ public class LiveServiceImpl implements ILiveService
         baseMapper.updateLive(exist);
         // 清除缓存
         clearLiveCache(live.getLiveId());
-        List<LiveAutoTask> liveAutoTasks = liveAutoTaskService.selectNoActivedByLiveId(exist.getLiveId(), new Date());
-        liveAutoTasks.forEach(liveAutoTask -> {
-            liveAutoTask.setCreateTime(null);
-            liveAutoTask.setUpdateTime(null);
-            redisCache.redisTemplate.opsForZSet().add("live:auto_task:" + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
-            redisCache.redisTemplate.expire("live:auto_task:"+live.getLiveId(), 1, TimeUnit.DAYS);
-        });
+//        List<LiveAutoTask> liveAutoTasks = liveAutoTaskService.selectNoActivedByLiveId(exist.getLiveId(), new Date());
+//        liveAutoTasks.forEach(liveAutoTask -> {
+//            liveAutoTask.setCreateTime(null);
+//            liveAutoTask.setUpdateTime(null);
+//            redisCache.redisTemplate.opsForZSet().add("live:auto_task:" + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
+//            redisCache.redisTemplate.expire("live:auto_task:"+live.getLiveId(), 1, TimeUnit.DAYS);
+//        });
+        liveAutoTaskService.recalcLiveAutoTask(exist);
         String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
         redisCache.deleteObject(cacheKey);
         String cacheKey2 = String.format(LiveKeysConstant.LIVE_FLAG_CACHE, live.getLiveId());

+ 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

+ 102 - 0
fs-service/src/main/resources/mapper/course/FsPublicCourseTrafficLogMapper.xml

@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.course.mapper.FsPublicCourseTrafficLogMapper">
+
+    <resultMap type="com.fs.course.domain.FsPublicCourseTrafficLog" id="FsPublicCourseTrafficLogResult">
+        <result property="logId" column="log_id"/>
+        <result property="uuId" column="uu_id"/>
+        <result property="userId" column="user_id"/>
+        <result property="courseId" column="course_id"/>
+        <result property="videoId" column="video_id"/>
+        <result property="internetTraffic" column="internet_traffic"/>
+        <result property="status" column="status"/>
+        <result property="createTime" column="create_time"/>
+        <result property="project_id" column="projectId"/>
+    </resultMap>
+
+    <sql id="selectFsPublicCourseTrafficLogVo">
+        select log_id,
+               uu_id,
+               user_id,
+               course_id,
+               video_id,
+               company_id,
+               internet_traffic,
+               status,
+               create_time,
+               project_id
+        from fs_public_course_traffic_log
+    </sql>
+
+    <select id="selectFsPublicCourseTrafficLogByLogId" parameterType="Long" resultMap="FsPublicCourseTrafficLogResult">
+        <include refid="selectFsPublicCourseTrafficLogVo"/>
+        where log_id = #{logId}
+    </select>
+
+    <insert id="insertFsPublicCourseTrafficLog" parameterType="com.fs.course.domain.FsPublicCourseTrafficLog"
+            useGeneratedKeys="true" keyProperty="logId">
+        insert into fs_public_course_traffic_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="uuId != null">uu_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="courseId != null">course_id,</if>
+            <if test="videoId != null">video_id,</if>
+            <if test="internetTraffic != null">internet_traffic,</if>
+            <if test="status != null">status,</if>
+            create_time,
+            <if test="projectId != null">project_id,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="uuId != null">#{uuId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="courseId != null">#{courseId},</if>
+            <if test="videoId != null">#{videoId},</if>
+            <if test="internetTraffic != null">#{internetTraffic},</if>
+            <if test="status != null">#{status},</if>
+            sysdate(),
+            <if test="projectId != null">#{projectId},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsPublicCourseTrafficLog" parameterType="com.fs.course.domain.FsPublicCourseTrafficLog">
+        update fs_public_course_traffic_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="uuId != null">uu_id = #{uuId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="courseId != null">course_id = #{courseId},</if>
+            <if test="videoId != null">video_id = #{videoId},</if>
+            <if test="internetTraffic != null">internet_traffic = #{internetTraffic},</if>
+            <if test="status != null">status = #{status},</if>
+        </trim>
+        where log_id = #{logId}
+    </update>
+
+    <insert id="insertOrUpdateTrafficLog" parameterType="com.fs.course.domain.FsPublicCourseTrafficLog">
+        insert into fs_public_course_traffic_log (uu_id, user_id, course_id, video_id,
+                                                  internet_traffic, status, create_time, project_id)
+        values (#{uuId}, #{userId}, #{courseId}, #{videoId}, #{internetTraffic}, #{status}, sysdate(),
+                #{projectId}) ON DUPLICATE KEY
+        UPDATE
+            internet_traffic = internet_traffic + #{internetTraffic}
+    </insert>
+
+    <select id="selectTrafficNew" resultType="com.fs.course.vo.FsCourseTrafficLogListVO">
+        select
+        log.course_id,
+        SUM(log.internet_traffic) AS total_internet_traffic,
+        DATE_FORMAT(log.create_time, '%Y-%m-%d') AS `month`
+        FROM fs_public_course_traffic_log log
+        <where>
+            <if test="startDate != null and endDate != null">
+                and DATE_FORMAT(log.create_time, '%Y-%m-%d') between #{startDate} AND #{endDate}
+            </if>
+            <if test="courseId != null">
+                and log.course_id = ${courseId}
+            </if>
+        </where>
+        group by log.course_id,`month`
+    </select>
+
+</mapper>

+ 17 - 4
fs-service/src/main/resources/mapper/crm/CrmCustomerPropertyMapper.xml

@@ -20,15 +20,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="updateTime"    column="update_time"    />
         <result property="updateBy"    column="update_by"    />
         <result property="remark"    column="remark"    />
+        <result property="deleted" column="deleted"/>
+        <result property="deleteBy" column="delete_by"/>
+        <result property="deleteTime" column="delete_time"/>
     </resultMap>
 
     <sql id="selectCrmCustomerPropertyVo">
-        select id, customer_id, property_id, property_name, property_value, property_value_type, trade_type, ai_analysis, intention, like_ratio, create_time, create_by, update_time, update_by, remark from crm_customer_property
+        select id, customer_id, property_id, property_name, property_value, property_value_type, trade_type, ai_analysis, intention, like_ratio, create_time, create_by, update_time, update_by, remark, deleted, delete_by, delete_time from crm_customer_property
     </sql>
 
     <select id="selectCrmCustomerPropertyList" parameterType="CrmCustomerProperty" resultMap="CrmCustomerPropertyResult">
         <include refid="selectCrmCustomerPropertyVo"/>
         <where>
+            deleted = 0
             <if test="customerId != null"> and customer_id = #{customerId}</if>
             <if test="propertyId != null"> and property_id = #{propertyId}</if>
             <if test="propertyName != null and propertyName != ''"> and property_name like concat('%', #{propertyName}, '%')</if>
@@ -41,7 +45,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectCrmCustomerPropertyById" parameterType="Long" resultMap="CrmCustomerPropertyResult">
         <include refid="selectCrmCustomerPropertyVo"/>
-        where id = #{id}
+        where id = #{id} and deleted = 0
     </select>
 
     <insert id="insertCrmCustomerProperty" parameterType="CrmCustomerProperty" useGeneratedKeys="true" keyProperty="id">
@@ -61,6 +65,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">update_time,</if>
             <if test="updateBy != null">update_by,</if>
             <if test="remark != null">remark,</if>
+            <if test="deleted != null">deleted,</if>
+            <if test="deleteBy != null">delete_by,</if>
+            <if test="deleteTime != null">delete_time,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="customerId != null">#{customerId},</if>
@@ -77,6 +84,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">#{updateTime},</if>
             <if test="updateBy != null">#{updateBy},</if>
             <if test="remark != null">#{remark},</if>
+            <if test="deleted != null">#{deleted},</if>
+            <if test="deleteBy != null">#{deleteBy},</if>
+            <if test="deleteTime != null">#{deleteTime},</if>
         </trim>
     </insert>
 
@@ -97,6 +107,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">update_time = #{updateTime},</if>
             <if test="updateBy != null">update_by = #{updateBy},</if>
             <if test="remark != null">remark = #{remark},</if>
+            <if test="deleted != null">deleted = #{deleted},</if>
+            <if test="deleteBy != null">delete_by = #{deleteBy},</if>
+            <if test="deleteTime != null">delete_time = #{deleteTime},</if>
         </trim>
         where id = #{id}
     </update>
@@ -114,13 +127,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectCrmCustomerPropertyByCustomerId" parameterType="Long" resultMap="CrmCustomerPropertyResult">
         <include refid="selectCrmCustomerPropertyVo"/>
-        where customer_id = #{customerId}
+        where customer_id = #{customerId} and deleted = 0
         order by id desc
     </select>
 
     <select id="selectByCustomerIdAndPropertyId" resultMap="CrmCustomerPropertyResult">
         <include refid="selectCrmCustomerPropertyVo"/>
-        where customer_id = #{customerId} and property_id = #{propertyId}
+        where customer_id = #{customerId} and property_id = #{propertyId} and deleted = 0
         limit 1
     </select>
 

+ 12 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

@@ -236,6 +236,18 @@ public class CourseFsUserController extends AppBaseController {
         return courseVideoService.getInternetTraffic(param);
     }
 
+    /**
+     * 获取公开课流量
+     * @return
+     */
+    @ApiOperation("获取公开课缓冲流量")
+    @PostMapping("/getPublicCourseInternetTraffic")
+    @Login
+    public R getPublicCourseInternetTraffic(@RequestBody FsUserCourseVideoFinishUParam param ){
+        param.setUserId(Long.parseLong(getUserId()));
+        return courseVideoService.getPublicCourseInternetTraffic(param);
+    }
+
 
     @ApiOperation("答题")
     @PostMapping("/courseAnswer")