Explorar o código

中车API对接

Lijingyang hai 2 meses
pai
achega
529bf2386e
Modificáronse 33 ficheiros con 1951 adicións e 405 borrados
  1. 6 0
      pom.xml
  2. 4 2
      ruoyi-api/ruoyi-api-external/pom.xml
  3. 11 40
      ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/ZCR.java
  4. 19 0
      ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/bo/DeliveryTrackBo.java
  5. 6 6
      ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/vo/CatalogsVo.java
  6. 15 0
      ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/vo/GoodsStockVo.java
  7. 1 1
      ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/vo/TrackVo.java
  8. 19 0
      ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/vo/ZCLoginBusinessRespVo.java
  9. 5 0
      ruoyi-api/ruoyi-api-order/pom.xml
  10. 12 0
      ruoyi-api/ruoyi-api-order/src/main/java/org/dromara/product/api/RemoteExternalOrderService.java
  11. 1 1
      ruoyi-api/ruoyi-api-product/src/main/java/org/dromara/product/api/RemoteProductService.java
  12. 16 0
      ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteComLogisticsCompanyService.java
  13. 123 2
      ruoyi-auth/src/main/java/org/dromara/auth/controller/Auth2Controller.java
  14. 128 0
      ruoyi-auth/src/main/java/org/dromara/auth/util/SM2SignatureUtils.java
  15. 111 0
      ruoyi-auth/src/main/java/org/dromara/auth/util/SignParamUtils.java
  16. 94 0
      ruoyi-auth/src/main/java/org/dromara/auth/util/ZCApiUtils.java
  17. 4 2
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/zhongche/domain/DeliveryTrack.java
  18. 0 1
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/controller/SupplierInfoController.java
  19. 1 1
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/ISupplierInfoService.java
  20. 8 8
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SupplierContactServiceImpl.java
  21. 21 8
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SupplierInfoServiceImpl.java
  22. 152 41
      ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/controller/zhongche/ZhongChePullController.java
  23. 504 247
      ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/controller/zhongche/ZhongChePushController.java
  24. 85 0
      ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/enums/MallMessageTypeEnum.java
  25. 128 0
      ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/util/SM2SignatureUtils.java
  26. 109 0
      ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/util/SignParamUtils.java
  27. 93 0
      ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/util/ZCApiUtils.java
  28. 131 0
      ruoyi-modules/ruoyi-order/src/main/java/org/dromara/order/dubbo/RemoteExternalOrderServiceImpl.java
  29. 3 0
      ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/controller/ProductCategoryController.java
  30. 108 45
      ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/dubbo/RemoteProductServiceImpl.java
  31. 2 0
      ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/service/IProductWarehouseInventoryService.java
  32. 6 0
      ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/service/impl/ProductWarehouseInventoryServiceImpl.java
  33. 25 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/dubbo/RemoteComLogisticsCompanyServiceImpl.java

+ 6 - 0
pom.xml

@@ -144,6 +144,12 @@
     <dependencyManagement>
         <dependencies>
 
+            <dependency>
+                <groupId>org.bouncycastle</groupId>
+                <artifactId>bcprov-jdk18on</artifactId>
+                <version>1.78.1</version>
+            </dependency>
+
             <!-- SpringCloud 微服务 -->
             <dependency>
                 <groupId>org.springframework.cloud</groupId>

+ 4 - 2
ruoyi-api/ruoyi-api-external/pom.xml

@@ -26,11 +26,13 @@
 
         <dependency>
             <groupId>org.bouncycastle</groupId>
-            <artifactId>bcprov-jdk15on</artifactId>
-            <version>1.70</version>
+            <artifactId>bcprov-jdk18on</artifactId>
+            <version>1.78.1</version>
         </dependency>
 
 
+
+
     </dependencies>
 
 </project>

+ 11 - 40
ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/ZCR.java

@@ -12,7 +12,7 @@ import java.io.Serializable;
  */
 @Data
 @NoArgsConstructor
-public class ZCR<T> implements Serializable {
+public class ZCR implements Serializable {
     @Serial
     private static final long serialVersionUID = 1L;
     /**
@@ -32,65 +32,36 @@ public class ZCR<T> implements Serializable {
     /**
      * 业务响应参数    N
      */
-    private T data;
+    private String data;
 
     /**
      * 签名    Y
      */
     private String sign;
 
-    /**
-     * token
-     */
-    private String accessToken;
 
-    /**
-     * 有效期  访问令牌有效期,单位秒,建议24小时 24小时返回值为
-     * 24*60*60 = 86400
-     */
-    private Integer expiresIn;
 
-    public static <T> ZCR<T> tokenOk(String accessToken, Integer expiresIn) {
-        return resetRToken(SUCCESS, "",null,"", accessToken,expiresIn);
+    public static  ZCR ok(String data,String sign) {
+        return resetR(SUCCESS, null, data, sign);
     }
 
-
-
-    public static <T> ZCR<T> ok(T data,String sign) {
-        return resetR(SUCCESS, "", data, sign);
-    }
-
-    public static <T> ZCR<T> ok(T data) {
-        return resetR(SUCCESS, "", data, "" );
+    public static  ZCR ok(String data) {
+        return resetR(SUCCESS, null, data, null );
     }
-    public static <T> ZCR<T> ok(String respMsg, T data,String sign) {
+    public static  ZCR ok(String respMsg, String data,String sign) {
         return resetR(SUCCESS, respMsg, data, sign);
     }
 
-    public static <T> ZCR<T> fail(String code, String msg) {
-        return resetR(code, msg,null,"");
-    }
-
-    public static <T> ZCR<T> resetR(String respCode, String respMsg, T data, String sign) {
-        ZCR ZCR = new ZCR<>();
-        ZCR.setRespCode(respCode);
-        ZCR.setRespMsg(respMsg);
-        ZCR.setData(data);
-        ZCR.setSign(sign);
-        return ZCR;
+    public static  ZCR fail(String code, String msg) {
+        return resetR(code, msg,null,null);
     }
 
-    public static <T> ZCR<T> resetRToken(String respCode, String respMsg, T data, String sign, String accessToken, Integer expiresIn) {
-        ZCR ZCR = new ZCR<>();
+    public static  ZCR resetR(String respCode, String respMsg, String data, String sign) {
+        ZCR ZCR = new ZCR();
         ZCR.setRespCode(respCode);
         ZCR.setRespMsg(respMsg);
         ZCR.setData(data);
         ZCR.setSign(sign);
-        ZCR.setAccessToken(accessToken);
-        ZCR.setExpiresIn(expiresIn);
         return ZCR;
     }
-
-
-
 }

+ 19 - 0
ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/bo/DeliveryTrackBo.java

@@ -0,0 +1,19 @@
+package org.dromara.external.api.zhongche.domain.bo;
+
+import lombok.Data;
+
+/**
+ * 物流查询请求 - 业务参数根节点
+ */
+@Data
+public class DeliveryTrackBo {
+    /**
+     * 中车电子商城发货单编号(必填,长度20)
+     */
+    private String outgoingCode;
+
+    /**
+     * 运单类型(必填,长度5;0=订单,1=换新单)
+     */
+    private String waybillType;
+}

+ 6 - 6
ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/vo/CatalogsVo.java

@@ -3,15 +3,15 @@ package org.dromara.external.api.zhongche.domain.vo;
 import lombok.Data;
 import org.dromara.external.api.zhongche.domain.Catalog;
 
-import java.io.Serializable;
 import java.util.List;
 
+/**
+ * 品目查询业务响应(对应文档 4.2.6.1 + 4.2.6 业务参数)
+ */
 @Data
-public class CatalogsVo implements Serializable {
-    private static final long serialVersionUID = 1L;
-
+public class CatalogsVo {
     /**
-     * 电商品目列表(必填)
+     * 电商品目列表(文档参数名:catalogs,必填,List类型)
      */
-    private List<Catalog> catalogs;
+    private List<Catalog> catalogs; // 对应文档 4.2.6.1 的参数 2(catalogs)
 }

+ 15 - 0
ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/vo/GoodsStockVo.java

@@ -0,0 +1,15 @@
+package org.dromara.external.api.zhongche.domain.vo;
+
+import lombok.Data;
+import org.dromara.external.api.zhongche.domain.Stocks;
+
+import java.util.List;
+
+/**
+ * author
+ * 时间:2026/2/2,17:49
+ */
+@Data
+public class GoodsStockVo {
+    private List<Stocks> stocks;
+}

+ 1 - 1
ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/vo/TrackVo.java

@@ -3,7 +3,7 @@ package org.dromara.external.api.zhongche.domain.vo;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.dromara.external.api.zhongche.domain.DeliveryTrack;
+import org.dromara.common.core.domain.zhongche.domain.DeliveryTrack;
 
 import java.util.List;
 

+ 19 - 0
ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/vo/ZCLoginBusinessRespVo.java

@@ -0,0 +1,19 @@
+package org.dromara.external.api.zhongche.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 中车登录业务响应参数(data字段编码前对应的实体)
+ */
+@Data
+public class ZCLoginBusinessRespVo {
+    /**
+     * 访问令牌,用于业务接口调用  Y
+     */
+    private String accessToken;
+
+    /**
+     * 访问令牌有效期,单位秒  Y
+     */
+    private Integer expiresIn;
+}

+ 5 - 0
ruoyi-api/ruoyi-api-order/pom.xml

@@ -23,6 +23,11 @@
             <artifactId>ruoyi-common-core</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-api-external</artifactId>
+        </dependency>
+
 
     </dependencies>
 

+ 12 - 0
ruoyi-api/ruoyi-api-order/src/main/java/org/dromara/product/api/RemoteExternalOrderService.java

@@ -1,9 +1,13 @@
 package org.dromara.product.api;
 
+import org.dromara.common.core.domain.zhongche.domain.DeliveryTrack;
+import org.dromara.external.api.zhongche.domain.vo.TrackVo;
 import org.dromara.product.api.domain.dto.OrderNoDto;
 import org.dromara.product.api.domain.dto.OrderNoticeDto;
 import org.dromara.product.api.domain.dto.OrderPushDto;
 
+import java.util.List;
+
 /**
  * @author
  * @date 2025/12/30 下午7:09
@@ -32,4 +36,12 @@ public interface RemoteExternalOrderService {
     String getOrderNo(String ZCorderNo);
 
 
+    //查询物流单号物流公司
+    TrackVo queryLogisticsTrack(String outgoingCode, String waybillType);
+
+    //获取物流轨迹
+    List<DeliveryTrack> queryDeliverTrack(String logisticsCompanyCode,String logisticNo ,String phone);
+
+    //获取退货订单号
+    String getReturnOrderNo(String ZCorderNo);
 }

+ 1 - 1
ruoyi-api/ruoyi-api-product/src/main/java/org/dromara/product/api/RemoteProductService.java

@@ -70,7 +70,7 @@ public interface RemoteProductService {
 
     StocksResultDto queryProductStock(Map<String, Integer> goods, String areaId);
 
-    List<Prices> queryProductPrice(List<String> goodsIds);
+    Map<String, Prices> queryProductPrice(List<String> goodsIds);
 
     /**
      * 分页查询站点产品列表(供远程调用)

+ 16 - 0
ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteComLogisticsCompanyService.java

@@ -0,0 +1,16 @@
+package org.dromara.system.api;
+
+import org.dromara.common.core.domain.zhongche.domain.DeliveryTrack;
+import org.dromara.common.core.validate.enumd.EnumPattern;
+
+import java.util.List;
+
+/**
+ * author
+ * 时间:2026/2/2,19:12
+ */
+public interface RemoteComLogisticsCompanyService {
+    String selectLogisticsCompanyNameById(Long ids);
+
+
+}

+ 123 - 2
ruoyi-auth/src/main/java/org/dromara/auth/controller/Auth2Controller.java

@@ -10,18 +10,31 @@ import cn.hutool.crypto.SecureUtil;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpUtil;
 import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.PostConstruct;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.auth.domain.vo.LoginVo;
 import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.util.SM2SignatureUtils;
+import org.dromara.auth.util.SignParamUtils;
+import org.dromara.auth.util.ZCApiUtils;
 import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.external.api.zhongche.domain.ZCR;
+import org.dromara.external.api.zhongche.domain.bo.UserLoginBo;
+import org.dromara.external.api.zhongche.domain.bo.ZCTokenBo;
+import org.dromara.external.api.zhongche.domain.vo.ZCLoginBusinessRespVo;
 import org.dromara.external.api.zhongzhi.domain.Result;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.time.Duration;
 import java.time.format.DateTimeFormatter;
+import java.util.Base64;
 import java.util.Map;
 import java.util.Objects;
 
@@ -38,15 +51,23 @@ import static org.dromara.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
 @RequestMapping("/auth2")
 public class Auth2Controller {
 
+    private final ObjectMapper objectMapper;
+
+    // ========== 读取yml中的真实公私钥(直接复制使用) ==========
+
+    private final String DEVELOPER_PRIVATE_KEY = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1YybOl0QDE2e9humlm4AgI3wJ1tI+UfVRZx8kk4hfPtZjorHN8Tjq/cP07t4Yscy+R9oFci8xw0VpBbcnlaq1w=="; // 电商提供的私钥
+
+    private final String DEVELOPER_PUBLIC_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgpQdXwMi21Mg1FhWad2AQLOwfNiDHgwhootau0YerQbagCgYIKoEcz1UBgi2hRANCAATVjJs6XRAMTZ72G6aWbgCAjfAnW0j5R9VFnHySTiF8+1mOisc3xOOr9w/Tu3hixzL5H2gVyLzHDRWkFtyeVqrX";  // 电商提供的公钥
+
 
     /**
-     * 获取 Access Token
+     * 获取 Access Token  ZHONGZHI
      * @param username 用户名
      * @param password 密码(明文,生产环境建议加密后再传)
      * @return 包含 access_token 和 expires_at 的 Result,失败则 success=false
      */
     @PostMapping("/zhongzhi/access_token")
-    public Result getAccessToken(String timestamp, String username, String password, String sign) {
+    public Result getAccessTokenZhongZhi(String timestamp, String username, String password, String sign) {
         // 2. 生成 sign = MD5(username + password + timestamp + password).toLowerCase()
         String signStr = username + password + timestamp + password;
         String sign1 = SecureUtil.md5(signStr).toLowerCase();
@@ -66,6 +87,106 @@ public class Auth2Controller {
             return Result.fail(5004,"签名错误,请检查后重试");
         }
     }
+    /**
+     * 获取 Access Token  ZHONGChe
+     * @return 包含 access_token 和 expires_at 的 Result,失败则 success=false
+     */
+    @PostMapping("/zhongche/access_token")
+    public ZCR getAccessTokenZhongChe(@RequestBody ZCTokenBo zcTokenBo) {
+        if (zcTokenBo == null) {
+            return ZCR.fail("1001", "请求参数不能为空");
+        }
+        if (zcTokenBo.getVersion() == null || zcTokenBo.getVersion().trim().isEmpty()) {
+            return ZCR.fail("1001", "接口版本version不能为空");
+        }
+        if (zcTokenBo.getTimestamp() == null || zcTokenBo.getTimestamp().trim().isEmpty()) {
+            return ZCR.fail("1001", "时间戳timestamp不能为空");
+        }
+        if (zcTokenBo.getClientId() == null || zcTokenBo.getClientId().trim().isEmpty()) {
+            return ZCR.fail("1001", "开发者id clientId不能为空");
+        }
+        if (zcTokenBo.getData() == null || zcTokenBo.getData().trim().isEmpty()) {
+            return ZCR.fail("1001", "业务请求参数data不能为空");
+        }
+        if (zcTokenBo.getSign() == null || zcTokenBo.getSign().trim().isEmpty()) {
+            return ZCR.fail("1001", "签名sign不能为空");
+        }
+        //sign值校验
+        // ========== 步骤2:签名校验(核心!复用工具类,无需修改工具类) ==========
+        boolean signVerifyResult = false;
+        try {
+            signVerifyResult = SignParamUtils.verifyRequestSign(zcTokenBo, DEVELOPER_PUBLIC_KEY);
+        } catch (JsonProcessingException e) {
+            return ZCR.fail("1001", "接口签名错误(验签异常)");
+        }
+
+        if (!signVerifyResult) {
+            try {
+                log.error("中车电商接口验签失败,请求参数:{}", objectMapper.writeValueAsString(zcTokenBo));
+            } catch (JsonProcessingException e) {
+                return ZCR.fail("1001", "接口签名错误(验签异常)");
+            }
+            return ZCR.fail("2002", "接口签名错误(验签失败)");
+        }
+
+        // ========== 步骤3:解码data字段,获取username和password(复用 ZCApiUtils 更简洁) ==========
+        UserLoginBo userLoginBo = null;
+        try {
+            userLoginBo = ZCApiUtils.base64JsonToObject(zcTokenBo.getData(), UserLoginBo.class);
+        } catch (JsonProcessingException e) {
+            return ZCR.fail("1001", "账号密码解析异常");
+        }
+        // 校验业务参数非空
+        if (userLoginBo.getUsername() == null || userLoginBo.getUsername().trim().isEmpty()) {
+            return ZCR.fail("1007", "账户名称username不能为空");
+        }
+        if (userLoginBo.getPassword() == null || userLoginBo.getPassword().trim().isEmpty()) {
+            return ZCR.fail("1008", "账户密码password不能为空");
+        }
+
+        // ========== 步骤4:业务逻辑校验( ==========
+        //TODO 这里用的肖哥的逻辑 待完成吧
+        String username = userLoginBo.getUsername();
+        String password = userLoginBo.getPassword();
+        LoginVo loginVo = IAuthStrategy.getAccessToken(username, password);
+
+        if(ObjectUtil.isEmpty(loginVo)){
+            return ZCR.fail("1001","授权失败");
+        }
+        if(ObjectUtil.isNotEmpty(loginVo.getMsg())){
+            return ZCR.fail(loginVo.getCode().toString(),loginVo.getMsg());
+        }
+
+        // ========== 步骤5:封装业务响应参数(accessToken + expiresIn) ==========
+        String accessToken = loginVo.getAccessToken();
+        // 获取令牌有效期(秒),与 zhongzhi 逻辑一致
+        Integer expiresIn = (int) StpUtil.getTokenTimeout(accessToken);
+        ZCLoginBusinessRespVo loginBusinessRespBo = new ZCLoginBusinessRespVo();
+        loginBusinessRespBo.setAccessToken(accessToken);
+        loginBusinessRespBo.setExpiresIn(expiresIn);
+
+        // ========== 步骤6:封装响应 data 字段(Base64 编码,复用 ZCApiUtils) ==========
+        String respData = null;
+        try {
+            respData = ZCApiUtils.objectToBase64Json(loginBusinessRespBo);
+        } catch (JsonProcessingException e) {
+            return ZCR.fail("1001", "接口响应数据编码异常");
+        }
+
+        // ========== 步骤7:生成响应签名(正常响应必须返回 sign,复用工具类) ==========
+        ZCR successZcr = ZCR.ok(respData, ""); // 先填充 data,暂不填 sign
+        String respSignContent = null;
+        try {
+            respSignContent = SignParamUtils.getSignContent(successZcr);
+        } catch (JsonProcessingException e) {
+            return ZCR.fail("1001", "接口响应签名错误(验签异常)");
+        }
+        String respSign = SM2SignatureUtils.sign(respSignContent, DEVELOPER_PRIVATE_KEY);
+        // ========== 步骤8:返回成功响应(填充最终签名) ==========
+        log.info("中车电商登录授权成功,用户名:{},accessToken:{}", username, accessToken);
+        return ZCR.ok(respData, respSign);
+
+    }
 
 
 

+ 128 - 0
ruoyi-auth/src/main/java/org/dromara/auth/util/SM2SignatureUtils.java

@@ -0,0 +1,128 @@
+package org.dromara.auth.util;
+
+import org.bouncycastle.asn1.gm.GMNamedCurves;
+import org.bouncycastle.asn1.x9.X9ECParameters;
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
+import org.bouncycastle.crypto.params.ECDomainParameters;
+import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.params.ECPublicKeyParameters;
+import org.bouncycastle.crypto.signers.SM2Signer;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.util.encoders.Base64;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.security.Security;
+
+/**
+ * 国密 SM2withSM3 签名工具类
+ * 注意:开发者私钥/公钥需由电商平台提供,此处提供生成示例(实际使用平台分配的密钥)
+ */
+public class SM2SignatureUtils {
+    // 注册 BouncyCastle 安全提供者
+    static {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+
+    // SM2 曲线参数(固定,国密标准)
+    private static final X9ECParameters SM2_CURVE_PARAMS = GMNamedCurves.getByName("sm2p256v1");
+    private static final ECDomainParameters EC_DOMAIN_PARAMETERS = new ECDomainParameters(
+            SM2_CURVE_PARAMS.getCurve(),
+            SM2_CURVE_PARAMS.getG(),
+            SM2_CURVE_PARAMS.getN(),
+            SM2_CURVE_PARAMS.getH()
+    );
+
+    /**
+     * 生成 SM2 密钥对(仅用于测试,实际使用电商平台提供的开发者私钥/公钥)
+     * @return 密钥对(私钥在前,公钥在后,均为 Base64 编码)
+     */
+    public static String[] generateSM2KeyPair() {
+        ECKeyPairGenerator generator = new ECKeyPairGenerator();
+        ECKeyGenerationParameters genParams = new ECKeyGenerationParameters(EC_DOMAIN_PARAMETERS, new SecureRandom());
+        generator.init(genParams);
+
+        AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
+        ECPrivateKeyParameters privateKeyParams = (ECPrivateKeyParameters) keyPair.getPrivate();
+        ECPublicKeyParameters publicKeyParams = (ECPublicKeyParameters) keyPair.getPublic();
+
+        // 转换为 Base64 编码字符串
+        String privateKeyBase64 = Base64.toBase64String(privateKeyParams.getD().toByteArray());
+        String publicKeyBase64 = Base64.toBase64String(publicKeyParams.getQ().getEncoded(false));
+
+        return new String[]{privateKeyBase64, publicKeyBase64};
+    }
+
+    /**
+     * SM2withSM3 签名(私钥签名,生成待提交的 sign 字段)
+     * @param content 待签名字符串(筛选排序后的 JSON 字符串)
+     * @param privateKeyBase64 开发者私钥(Base64 编码,平台提供)
+     * @return 签名结果(Base64 编码,可直接赋值给 sign 字段)
+     */
+    public static String sign(String content, String privateKeyBase64) {
+        if (content == null || content.trim().isEmpty() || privateKeyBase64 == null || privateKeyBase64.trim().isEmpty()) {
+            throw new IllegalArgumentException("待签名字符串和私钥不能为空");
+        }
+
+        try {
+            // 1. 解码私钥
+            byte[] privateKeyBytes = Base64.decode(privateKeyBase64);
+            ECPrivateKeyParameters privateKeyParams = new ECPrivateKeyParameters(
+                    new java.math.BigInteger(1, privateKeyBytes),
+                    EC_DOMAIN_PARAMETERS
+            );
+
+            // 2. 初始化 SM2 签名器
+            SM2Signer signer = new SM2Signer();
+            signer.init(true, privateKeyParams);
+
+            // 3. 传入待签名内容(UTF-8 编码)
+            byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
+            signer.update(contentBytes, 0, contentBytes.length);
+
+            // 4. 生成签名并进行 Base64 编码
+            byte[] signatureBytes = signer.generateSignature();
+            return Base64.toBase64String(signatureBytes);
+        } catch (Exception e) {
+            throw new RuntimeException("SM2 签名失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * SM2withSM3 验签(公钥校验,验证请求传入的 sign 是否合法)
+     * @param content 待签名字符串(与签名时一致的 JSON 字符串)
+     * @param signBase64 请求传入的 sign 字段(Base64 编码)
+     * @param publicKeyBase64 开发者公钥(Base64 编码,平台提供)
+     * @return true=验签通过,false=验签失败
+     */
+    public static boolean verify(String content, String signBase64, String publicKeyBase64) {
+        if (content == null || content.trim().isEmpty() || signBase64 == null || signBase64.trim().isEmpty()
+                || publicKeyBase64 == null || publicKeyBase64.trim().isEmpty()) {
+            throw new IllegalArgumentException("待签名字符串、签名和公钥不能为空");
+        }
+
+        try {
+            // 1. 解码公钥和签名
+            byte[] publicKeyBytes = Base64.decode(publicKeyBase64);
+            byte[] signatureBytes = Base64.decode(signBase64);
+
+            // 2. 构建公钥参数
+            org.bouncycastle.math.ec.ECPoint ecPoint = SM2_CURVE_PARAMS.getCurve().decodePoint(publicKeyBytes);
+            ECPublicKeyParameters publicKeyParams = new ECPublicKeyParameters(ecPoint, EC_DOMAIN_PARAMETERS);
+
+            // 3. 初始化 SM2 签名器
+            SM2Signer signer = new SM2Signer();
+            signer.init(false, publicKeyParams);
+
+            // 4. 传入待签名内容并验签
+            byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
+            signer.update(contentBytes, 0, contentBytes.length);
+
+            return signer.verifySignature(signatureBytes);
+        } catch (Exception e) {
+            throw new RuntimeException("SM2 验签失败:" + e.getMessage(), e);
+        }
+    }
+}

+ 111 - 0
ruoyi-auth/src/main/java/org/dromara/auth/util/SignParamUtils.java

@@ -0,0 +1,111 @@
+package org.dromara.auth.util;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.dromara.external.api.zhongche.domain.bo.ZCTokenBo;
+
+import java.lang.reflect.Field;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * 签名参数处理工具类(筛选、排序、拼接JSON)
+ */
+public class SignParamUtils {
+    /**
+     * JSON 转换工具(保持与 ZCApiUtils 一致)
+     */
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    /**
+     * 处理签名参数(核心方法)
+     * @param paramObj 请求/响应参数实体(如 ZCTokenBo、ZCR)
+     * @return 待签名字符串(筛选排序后的 JSON 字符串)
+     * @throws JsonProcessingException JSON 转换异常
+     */
+    public static String getSignContent(Object paramObj) throws JsonProcessingException {
+        if (paramObj == null) {
+            throw new IllegalArgumentException("参数实体不能为空");
+        }
+
+        // 步骤 1:反射获取参数实体的所有字段(键值对)
+        Map<String, Object> paramMap = new HashMap<>();
+        Field[] fields = paramObj.getClass().getDeclaredFields();
+        for (Field field : fields) {
+            field.setAccessible(true); // 允许访问私有字段
+            String fieldName = field.getName();
+            Object fieldValue = null;
+
+            try {
+                fieldValue = field.get(paramObj);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException("获取字段值失败:" + fieldName, e);
+            }
+
+            // 步骤 2:筛选字段(剔除 sign、剔除 null 值、剔除字节数组)
+            if ("sign".equals(fieldName)) {
+                continue; // 剔除 sign 字段
+            }
+            if (fieldValue == null) {
+                continue; // 剔除 null 值
+            }
+            if (fieldValue instanceof byte[]) {
+                continue; // 剔除字节类型字段(当前场景无,预留)
+            }
+
+            // 符合条件的字段存入 Map
+            paramMap.put(fieldName, fieldValue);
+        }
+
+        // 步骤 3:按字段名(键)ASCII 码升序排序
+        Map<String, Object> sortedParamMap = new TreeMap<>(new Comparator<String>() {
+            @Override
+            public int compare(String key1, String key2) {
+                // 按 ASCII 码升序排序(逐个字符对比)
+                return key1.compareTo(key2);
+            }
+        });
+        sortedParamMap.putAll(paramMap);
+
+        // 步骤 4:拼接为 JSON 字符串(待签名字符串)
+        return OBJECT_MAPPER.writeValueAsString(sortedParamMap);
+    }
+
+    /**
+     * 快捷方法:生成请求签名(ZCTokenBo → 待签名内容 → SM2 签名)
+     * @param zcTokenBo 请求参数实体
+     * @param privateKeyBase64 开发者私钥(Base64 编码)
+     * @return 签名结果(Base64 编码,可赋值给 sign 字段)
+     * @throws JsonProcessingException JSON 转换异常
+     */
+    public static String generateRequestSign(ZCTokenBo zcTokenBo, String privateKeyBase64) throws JsonProcessingException {
+        // 生成待签名字符串
+        String signContent = getSignContent(zcTokenBo);
+        // SM2 签名并返回
+        return SM2SignatureUtils.sign(signContent, privateKeyBase64);
+    }
+
+    /**
+     * 快捷方法:验证请求签名(ZCTokenBo → 待签名内容 → 校验 sign 是否合法)
+     * @param zcTokenBo 请求参数实体
+     * @param publicKeyBase64 开发者公钥(Base64 编码)
+     * @return true=验签通过,false=验签失败
+     * @throws JsonProcessingException JSON 转换异常
+     */
+    public static boolean verifyRequestSign(ZCTokenBo zcTokenBo, String publicKeyBase64) throws JsonProcessingException {
+        // 提取请求传入的 sign
+        String requestSign = zcTokenBo.getSign();
+        if (requestSign == null || requestSign.trim().isEmpty()) {
+            return false;
+        }
+
+        // 生成待签名字符串(与签名时一致)
+        String signContent = getSignContent(zcTokenBo);
+
+        // SM2 验签
+        return SM2SignatureUtils.verify(signContent, requestSign, publicKeyBase64);
+    }
+}

+ 94 - 0
ruoyi-auth/src/main/java/org/dromara/auth/util/ZCApiUtils.java

@@ -0,0 +1,94 @@
+package org.dromara.auth.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.util.Base64;
+
+/**
+ * 中车接口工具类(Base64 + JSON 转换)
+ */
+public class ZCApiUtils {
+    /**
+     * JSON 转换工具(Jackson)
+     */
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    /**
+     * Base64 解码(字符串 → 原始字符串)
+     * @param base64Str Base64编码字符串
+     * @return 解码后的原始字符串
+     */
+    public static String base64Decode(String base64Str) {
+        if (base64Str == null || base64Str.trim().isEmpty()) {
+            throw new IllegalArgumentException("Base64编码字符串不能为空");
+        }
+        byte[] decodeBytes = Base64.getDecoder().decode(base64Str);
+        return new String(decodeBytes);
+    }
+
+    /**
+     * Base64 编码(原始字符串 → Base64编码字符串)
+     * @param rawStr 原始字符串
+     * @return Base64编码字符串
+     */
+    public static String base64Encode(String rawStr) {
+        if (rawStr == null || rawStr.trim().isEmpty()) {
+            throw new IllegalArgumentException("原始字符串不能为空");
+        }
+        byte[] encodeBytes = rawStr.getBytes();
+        return Base64.getEncoder().encodeToString(encodeBytes);
+    }
+
+    /**
+     * JSON 字符串 → Java 对象
+     * @param jsonStr JSON字符串
+     * @param clazz 目标对象类型
+     * @param <T> 泛型
+     * @return 目标Java对象
+     * @throws JsonProcessingException JSON转换异常
+     */
+    public static <T> T jsonToObject(String jsonStr, Class<T> clazz) throws JsonProcessingException {
+        if (jsonStr == null || jsonStr.trim().isEmpty()) {
+            throw new IllegalArgumentException("JSON字符串不能为空");
+        }
+        return OBJECT_MAPPER.readValue(jsonStr, clazz);
+    }
+
+    /**
+     * Java 对象 → JSON 字符串
+     * @param obj Java对象
+     * @return JSON字符串
+     * @throws JsonProcessingException JSON转换异常
+     */
+    public static String objectToJson(Object obj) throws JsonProcessingException {
+        if (obj == null) {
+            throw new IllegalArgumentException("Java对象不能为空");
+        }
+        return OBJECT_MAPPER.writeValueAsString(obj);
+    }
+
+    /**
+     * 快捷方法:Java对象 → Base64编码的JSON字符串(响应data字段专用)
+     * @param obj Java对象
+     * @return Base64编码的JSON字符串
+     * @throws JsonProcessingException JSON转换异常
+     */
+    public static String objectToBase64Json(Object obj) throws JsonProcessingException {
+        String jsonStr = objectToJson(obj);
+        return base64Encode(jsonStr);
+    }
+
+    /**
+     * 快捷方法:Base64编码的JSON字符串 → Java对象(请求data字段专用)
+     * @param base64Json Base64编码的JSON字符串
+     * @param clazz 目标对象类型
+     * @param <T> 泛型
+     * @return 目标Java对象
+     * @throws JsonProcessingException JSON转换异常
+     */
+    public static <T> T base64JsonToObject(String base64Json, Class<T> clazz) throws JsonProcessingException {
+        String jsonStr = base64Decode(base64Json);
+        return jsonToObject(jsonStr, clazz);
+    }
+}

+ 4 - 2
ruoyi-api/ruoyi-api-external/src/main/java/org/dromara/external/api/zhongche/domain/DeliveryTrack.java → ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/zhongche/domain/DeliveryTrack.java

@@ -1,14 +1,16 @@
-package org.dromara.external.api.zhongche.domain;
+package org.dromara.common.core.domain.zhongche.domain;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
+import java.io.Serializable;
+
 // 1. 物流信息详情 (deliveryTrack 列表项)
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
-public class DeliveryTrack {
+public class DeliveryTrack implements Serializable {
     /**
      * 物流详情
      * 必填

+ 0 - 1
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/controller/SupplierInfoController.java

@@ -202,7 +202,6 @@ public class SupplierInfoController extends BaseController {
     @RepeatSubmit()
     @PutMapping("/edit")
     @Transactional(rollbackFor = Exception.class)
-
     public R<Void> srmEdit(@RequestBody SupplierInfoBo bo) throws JsonProcessingException {
         return toAjax(supplierInfoService.srmUpdateByBo(bo));
     }

+ 1 - 1
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/ISupplierInfoService.java

@@ -98,5 +98,5 @@ public interface ISupplierInfoService extends IService<SupplierInfo>{
 
     Long getSupplierStatus(Long id);
 
-    boolean srmUpdateByBo(SupplierInfoBo bo);
+    boolean srmUpdateByBo(SupplierInfoBo bo) throws JsonProcessingException;
 }

+ 8 - 8
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SupplierContactServiceImpl.java

@@ -165,8 +165,15 @@ public class SupplierContactServiceImpl  extends ServiceImpl<SupplierContactMapp
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Boolean insertByBo(SupplierContactBo bo) {
+        //首先判断是否是待审核状态
+        //1.如果是待审核 得记录联系人 不能登录  user是不能登录的 status = 1 停用
+
+        //2.如果是正式供应商  能登录  user是能登录的 status = 0 正常
+
+        //3.如果是停用的供应商  不能登录  user是不能登录的 status = 1 停用
+
+        //4.如果是待修改审核  能登录  user是能登录的 status = 0
         SupplierContact add = MapstructUtils.convert(bo, SupplierContact.class);
-        validEntityBeforeSave(add);
         RemoteUserBo remoteUserBo = new RemoteUserBo();
         remoteUserBo.setNickName(bo.getUserName());
         remoteUserBo.setUserName(bo.getPhone());
@@ -201,7 +208,6 @@ public class SupplierContactServiceImpl  extends ServiceImpl<SupplierContactMapp
     @Transactional(rollbackFor = Exception.class)
     public Boolean updateByBo(SupplierContactBo bo) {
         SupplierContact update = MapstructUtils.convert(bo, SupplierContact.class);
-        validEntityBeforeSave(update);
         if (update.getUserId() != null){
             RemoteUserBo remoteUserBo = new RemoteUserBo();
             remoteUserBo.setNickName(bo.getUserName());
@@ -218,12 +224,6 @@ public class SupplierContactServiceImpl  extends ServiceImpl<SupplierContactMapp
         return baseMapper.updateById(update) > 0;
     }
 
-    /**
-     * 保存前的数据校验
-     */
-    private void validEntityBeforeSave(SupplierContact entity){
-        //TODO 做一些数据校验,如唯一约束
-    }
 
     /**
      * 校验并批量删除联系人信息

+ 21 - 8
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SupplierInfoServiceImpl.java

@@ -68,6 +68,8 @@ public class SupplierInfoServiceImpl  extends ServiceImpl<SupplierInfoMapper, Su
 
     private final ISupplierInfoTemporaryService supplierInfoTemporaryService;
 
+    private final ObjectMapper objectMapper;
+
     @DubboReference
     private final RemoteComStaffService remoteComStaffService;
 
@@ -123,7 +125,6 @@ public class SupplierInfoServiceImpl  extends ServiceImpl<SupplierInfoMapper, Su
             return;
         }
         try {
-            ObjectMapper objectMapper = new ObjectMapper();
             // 解析JSON到SupplierBusinessInfo对象
             SupplierBusinessInfo businessInfo = objectMapper.readValue(otherCustomersJson, SupplierBusinessInfo.class);
 
@@ -184,7 +185,6 @@ public class SupplierInfoServiceImpl  extends ServiceImpl<SupplierInfoMapper, Su
             supplyAreaBo.setSupplierId(bo.getId());
             PageQuery pageQuery = new PageQuery();
             TableDataInfo<SupplyAreaVo> supplyAreaVoTableDataInfo = supplyAreaService.queryPageList(supplyAreaBo, pageQuery);
-            ObjectMapper objectMapper = new ObjectMapper();
             String areaListJson = objectMapper.writeValueAsString(supplyAreaVoTableDataInfo);
             supplierInfoTemporary.setAreaListJson(areaListJson);
             boolean save = supplierInfoTemporaryService.save(supplierInfoTemporary);
@@ -233,7 +233,6 @@ public class SupplierInfoServiceImpl  extends ServiceImpl<SupplierInfoMapper, Su
                 // 无区域数据,直接更新主表即可
                 return baseMapper.updateById(supplierInfoVo) > 0;
             }
-            ObjectMapper objectMapper = new ObjectMapper();
             try {
                 // 1. 解析JSON字符串为 TableDataInfo<SupplyAreaVo>(和存储时的类型一致)
                 // 注意:泛型解析需要用 TypeReference 明确类型,否则会解析为 LinkedHashMap
@@ -329,11 +328,11 @@ public class SupplierInfoServiceImpl  extends ServiceImpl<SupplierInfoMapper, Su
     }
 
     @Override
-    public boolean srmUpdateByBo(SupplierInfoBo bo) {
+    public boolean srmUpdateByBo(SupplierInfoBo bo) throws JsonProcessingException {
         Long id = bo.getId();
         SupplierInfo supplierInfo = baseMapper.selectById(id);
-        //如果是待审核 随便改
-        if (bo.getSupplyStatus() == SupplierStatusEnum.PENDING_REVIEW.getCode()){
+        //如果是待审核和审核不通过 随便改
+        if (bo.getSupplyStatus() == SupplierStatusEnum.PENDING_REVIEW.getCode() || bo.getSupplyStatus() == SupplierStatusEnum.REVIEW_FAILED.getCode()){
             SupplierInfo update = MapstructUtils.convert(bo, SupplierInfo.class);
             return baseMapper.updateById(update) > 0;
         }
@@ -341,13 +340,23 @@ public class SupplierInfoServiceImpl  extends ServiceImpl<SupplierInfoMapper, Su
         if (supplierInfo.getSupplyStatus() == SupplierStatusEnum.OFFICIAL_SUPPLIER.getCode()){
             SupplierInfoTemporary supplierInfoTemporary = supplierInfoTemporaryService.querBySupplierId(id);
             if (supplierInfoTemporary == null){
-                BeanUtils.copyProperties(bo, supplierInfoTemporary,"id");
+                supplierInfoTemporary = new SupplierInfoTemporary();
+                BeanUtils.copyProperties(supplierInfo, supplierInfoTemporary,"id");
                 supplierInfoTemporary.setSupplierId(id);
+                //通过传参在set临时表
+                //查询供应地址 存入  areaListJson  为了回显
+                SupplyAreaBo supplyAreaBo = new SupplyAreaBo();
+                supplyAreaBo.setSupplierId(bo.getId());
+                PageQuery pageQuery = new PageQuery();
+                TableDataInfo<SupplyAreaVo> supplyAreaVoTableDataInfo = supplyAreaService.queryPageList(supplyAreaBo, pageQuery);
+                String areaListJson = objectMapper.writeValueAsString(supplyAreaVoTableDataInfo);
+                supplierInfoTemporary.setAreaListJson(areaListJson);
                 supplierInfoTemporary.setSupplyStatus(SupplierStatusEnum.REVIEW_UPDATED.getCode());
                 boolean save = supplierInfoTemporaryService.save(supplierInfoTemporary);
                 if (save == false){
                     throw new RuntimeException("保存供应商待审核数据失败");
                 }
+
             }
             //一共三个地方
             boolean flag =false;
@@ -371,7 +380,7 @@ public class SupplierInfoServiceImpl  extends ServiceImpl<SupplierInfoMapper, Su
             boolean update =baseMapper.updateById(supplierInfo) > 0;
             return update && flag;
         }
-        //如果是待审核状态,就变成待修改审核状态
+        //如果是待审核状态,依旧是待修改审核状态
         if (supplierInfo.getSupplyStatus() == SupplierStatusEnum.REVIEW_UPDATED.getCode()){
             SupplierInfoTemporary supplierInfoTemporary = supplierInfoTemporaryService.querBySupplierId(id);
             //一共三个地方
@@ -393,6 +402,10 @@ public class SupplierInfoServiceImpl  extends ServiceImpl<SupplierInfoMapper, Su
             }
             return flag;
         }
+        // 停用呢  不能修改了
+        if (supplierInfo.getSupplyStatus() == SupplierStatusEnum.DISABLED.getCode()){
+            throw new RuntimeException("供应商已停用,不能修改");
+        }
         return false;
     }
 

+ 152 - 41
ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/controller/zhongche/ZhongChePullController.java

@@ -3,10 +3,13 @@ package org.dromara.external.controller.zhongche;
 import cn.hutool.core.codec.Base64;
 import cn.hutool.core.date.DateUtil;
 
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpResponse;
 import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.external.api.zhongche.domain.*;
 import org.dromara.external.api.zhongche.domain.aftersale.bo.*;
 import org.dromara.external.api.zhongche.domain.aftersale.vo.*;
@@ -21,6 +24,9 @@ import org.dromara.external.api.zhongche.domain.settlement.vo.SettlementDetailVo
 import org.dromara.external.api.zhongche.domain.settlement.vo.SettlementPaymentDetailVo;
 import org.dromara.external.api.zhongche.domain.vo.*;
 import org.dromara.external.util.SM2SignUtil;
+import org.dromara.external.util.SM2SignatureUtils;
+import org.dromara.external.util.SignParamUtils;
+import org.dromara.external.util.ZCApiUtils;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -28,11 +34,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.nio.charset.StandardCharsets;
+import java.util.List;
 
 /**
  * author
  * 时间:2026/1/5,19:03
  */
+@Slf4j
 @Validated
 @RequiredArgsConstructor
 @RestController
@@ -40,14 +48,12 @@ import java.nio.charset.StandardCharsets;
 public class ZhongChePullController {
 
     // 中车地区查询接口地址(替换为真实域名)
-    private static final String AREA_QUERY_URL = "https://{中车域名}";
-
+    private static final String AREA_QUERY_URL = "https://supply-test.crrcgo.cc/mallapi/";
     // 中车提供的配置(替换为真实值)
-    private static final String CLIENT_ID = "KFZiA6EknYf";
-    private static final String ACCESS_TOKEN = "1261127442808451075";
-    private static final String PRIVATE_KEY = "你的私钥(16进制)"; // 电商平台私钥
-    private static final String ZC_PUBLIC_KEY = "中车公钥(16进制)"; // 中车公钥
-    private static final String VERSION = "1.0";
+    private static final String CLIENT_ID = "KFZAVuIyC56";
+    private static final String PRIVATE_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgpQdXwMi21Mg1FhWad2AQLOwfNiDHgwhootau0YerQbagCgYIKoEcz1UBgi2hRANCAATVjJs6XRAMTZ72G6aWbgCAjfAnW0j5R9VFnHySTiF8+1mOisc3xOOr9w/Tu3hixzL5H2gVyLzHDRWkFtyeVqrX"; // 电商平台私钥
+    private static final String ZC_PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEmUrB5ByAeb8jHayC7vbddqBFDIEsf1cpNO1qJttZ17xlDagVB/tBFasPr/x0+OWf2kimTKah2NGCYarymD1R5Q==\n"; // 中车公钥
+    private static final String VERSION = "1.0.0";
 
     private final SM2SignUtil sm2SignUtil;
 
@@ -56,49 +62,126 @@ public class ZhongChePullController {
 
     //5.1.1	地区查询
     @PostMapping("/area/query")
-    public ZCR<AreaVo> areaQuery(@RequestBody AreaQueryBo bo) {
+    public ZCR areaQuery(@RequestBody AreaQueryBo bo) {
+        // 1. 校验业务请求参数(自身先校验,避免无效调用电商平台)
+        //1 省级,2市级,3县级, 4区级
+        //父级地址id	当查询省级时填0
+        if (bo.getLevel() == null || !List.of(1, 2, 3, 4).contains(bo.getLevel())) {
+            return ZCR.fail("5051", "级次(level)必填,且仅支持1/2/3/4");
+        }
+        if (StrUtil.isBlank(bo.getPid())) {
+            return ZCR.fail("5052", "父级地址id(pid)不能为空");
+        }
         //获取response.body
-        ZCR<String> responseDto = doZcPost("/api/area/query", bo);
-        //TODO 2 签名校验
-        Boolean aBoolean = verifyResponseSign(responseDto);
-        if (!aBoolean){
-            return ZCR.fail("5001", "响应签名验证失败");
+        ZCR responseDto = doZcPost("/api/area/query", bo);
+        if (!"0".equals(responseDto.getRespCode())) {
+            log.error("地区查询 - 电商平台返回失败,错误码:{},错误信息:{}", responseDto.getRespCode(), responseDto.getRespMsg());
+            return ZCR.fail(responseDto.getRespCode(), responseDto.getRespMsg());
         }
-        //解析业务响应参数
-        ZCR<AreaVo> zcr = parseZcResponse(responseDto, AreaVo.class);
-        return zcr;
+        // 3. 复用工具类校验签名
+        Boolean signValid = verifyResponseSign(responseDto);
+        if (!signValid) {
+            return ZCR.fail("5053", "响应签名验证失败");
+        }
+        // 4. 复用工具类解析响应
+        return parseZcResponse(responseDto, AreaVo.class);
     }
 
-    private Boolean verifyResponseSign(ZCR<String> responseDto) {
-        String sign = responseDto.getSign();
-        if (sign == null){
+    private Boolean verifyResponseSign(ZCR responseDto) {
+        // 1. 空值防护
+        if (responseDto == null || responseDto.getSign() == null || responseDto.getSign().trim().isEmpty()) {
+            log.warn("通用签名校验 - 电商平台响应签名为空");
+            return false;
+        }
+
+        try {
+            // 2. 生成响应签名原文(复用 SignParamUtils.getSignContent,剔除 sign 字段)
+            String signContent = SignParamUtils.getSignContent(responseDto);
+
+            // 3. SM2 验签(复用 SM2SignatureUtils.verify,传入电商平台公钥)
+            return SM2SignatureUtils.verify(
+                signContent,
+                responseDto.getSign(),
+                ZC_PUBLIC_KEY
+            );
+        } catch (Exception e) {
+            log.error("通用签名校验 - 校验响应签名异常", e);
             return false;
         }
-        return true;
     }
 
 
-    private <B> ZCR<String> doZcPost(String apiPath, B bo) {
-        // 1. 业务参数 -> JSON -> Base64
-        String bizJson = JSONUtil.toJsonStr(bo);
-        String dataBase64 = Base64.encode(bizJson, StandardCharsets.UTF_8);
+    private <B> ZCR doZcPost(String apiPath, B bo) {
+        // 1. 业务 BO → JSON 字符串(复用 ZCApiUtils)
+        String bizJson;
+        try {
+            bizJson = ZCApiUtils.objectToJson(bo);
+        } catch (JsonProcessingException e) {
+            log.error("通用请求 - 业务 BO 转换 JSON 失败", e);
+            return ZCR.fail("5000", "业务参数转换失败:" + e.getMessage());
+        }
+
+        // 2. JSON 字符串 → Base64 编码(复用 ZCApiUtils,适配 UTF-8)
+        String dataBase64;
+        try {
+            // 注意:你的 ZCApiUtils.base64Encode 内部用 getBytes(),默认是系统编码,补充 UTF-8 编码保证一致性
+            dataBase64 = ZCApiUtils.base64Encode(new String(bizJson.getBytes(StandardCharsets.UTF_8)));
+        } catch (IllegalArgumentException e) {
+            log.error("通用请求 - 业务 JSON Base64 编码失败", e);
+            return ZCR.fail("5000", "业务参数编码失败:" + e.getMessage());
+        }
 
-        // 2. 构建 token 请求体
+        //TODO 3. 构建 ZCTokenBo 请求体
         ZCTokenBo zcTokenBo = getZcTokenBo(dataBase64);
-        String requestJson = JSONUtil.toJsonStr(zcTokenBo);
 
-        // 3. HTTP POST
-        HttpResponse httpResponse = HttpRequest
-            .post(AREA_QUERY_URL + apiPath)
+        // 4. 生成请求签名(复用 SignParamUtils + SM2SignatureUtils)
+        String requestSign;
+        try {
+            requestSign = SignParamUtils.generateRequestSign(zcTokenBo, PRIVATE_KEY);
+        } catch (Exception e) {
+            log.error("通用请求 - 生成请求签名失败", e);
+            return ZCR.fail("5000", "生成请求签名失败:" + e.getMessage());
+        }
+        zcTokenBo.setSign(requestSign);
+        // 5. ZCTokenBo → JSON 字符串(复用 ZCApiUtils)
+        String requestJson;
+        try {
+            requestJson = ZCApiUtils.objectToJson(zcTokenBo);
+        } catch (JsonProcessingException e) {
+            log.error("通用请求 - ZCTokenBo 转换 JSON 失败", e);
+            return ZCR.fail("5000", "请求体转换失败:" + e.getMessage());
+        }
+
+        // 6. 发送 HTTP POST 请求(保持 hutool HTTP 工具,保证稳定性)
+        String fullUrl = AREA_QUERY_URL + apiPath;
+        HttpResponse httpResponse = cn.hutool.http.HttpRequest
+            .post(fullUrl)
             .charset(StandardCharsets.UTF_8)
+            .contentType("application/json")
             .body(requestJson)
             .timeout(60000)
             .execute();
 
-        // 4. 响应体转 DTO
+        // 7. 校验 HTTP 响应状态
+        if (!httpResponse.isOk()) {
+            log.error("通用请求 - 电商平台接口调用失败,接口路径:{},HTTP 状态码:{}", apiPath, httpResponse.getStatus());
+            return ZCR.fail("5000", "电商平台接口调用失败,HTTP 响应异常");
+        }
+        // 8. 响应体 → ZCR(复用 ZCApiUtils 转换 JSON)
         String responseBody = httpResponse.body();
-        ZCR<String> responseDto = JSONUtil.toBean(responseBody, ZCR.class);
+        ZCR responseDto;
+        try {
+            responseDto = ZCApiUtils.jsonToObject(responseBody, ZCR.class);
+        } catch (JsonProcessingException e) {
+            log.error("通用请求 - 响应体转换 ZCR 失败", e);
+            return ZCR.fail("5000", "响应体解析失败:" + e.getMessage());
+        }
 
+        // 6. 校验HTTP响应状态
+        if (!httpResponse.isOk()) {
+            log.error("通用请求 - 电商平台接口调用失败,接口路径:{},HTTP状态码:{}", apiPath, httpResponse.getStatus());
+            return ZCR.fail("5000", "电商平台接口调用失败,HTTP响应异常");
+        }
         // 5. 响应码校验
         if (!"0".equals(responseDto.getRespCode())) {
             return ZCR.fail(responseDto.getRespCode(), responseDto.getRespMsg());
@@ -107,11 +190,12 @@ public class ZhongChePullController {
         return responseDto;
     }
 
+    //TODO 怎么获取TOKEN
     private ZCTokenBo getZcTokenBo(String data) {
         ZCTokenBo zcTokenBo = new ZCTokenBo();
         zcTokenBo.setVersion(VERSION);
         zcTokenBo.setTimestamp(DateUtil.format(DateUtil.date(), "yyyyMMddHHmmss"));
-        zcTokenBo.setAccessToken(ACCESS_TOKEN);
+        zcTokenBo.setAccessToken(null);
         zcTokenBo.setClientId(CLIENT_ID);
         zcTokenBo.setData(data);
         zcTokenBo.setSign("");
@@ -136,17 +220,44 @@ public class ZhongChePullController {
         return zcr;
 
     }
-    private <V> ZCR<V> parseZcResponse(ZCR<String> responseDto, Class<V> voClass) {
-        // Base64 解码 data
-        String respBizJson = Base64.decodeStr(
-            responseDto.getData(),
-            StandardCharsets.UTF_8
-        );
+    private <V> ZCR parseZcResponse(ZCR responseDto, Class<V> voClass) {
+        // 1. 空值防护
+        if (responseDto == null || responseDto.getData() == null || responseDto.getData().trim().isEmpty()) {
+            log.warn("通用解析 - 电商平台响应数据为空");
+            return ZCR.fail("5001", "解析响应数据失败:响应数据为空");
+        }
+
+        // 2. Base64 解码 data 字段(复用 ZCApiUtils)
+        String respBizJson;
+        try {
+            respBizJson = ZCApiUtils.base64Decode(responseDto.getData());
+            // 补充 UTF-8 编码转换,保证中文不乱码
+            respBizJson = new String(respBizJson.getBytes(), StandardCharsets.UTF_8);
+        } catch (IllegalArgumentException e) {
+            log.warn("通用解析 - 电商平台响应数据 Base64 解码失败", e);
+            return ZCR.fail("5001", "解析响应数据失败:Base64 解码失败");
+        }
 
-        // JSON → VO
-        V vo = JSONUtil.toBean(respBizJson, voClass);
+        // 3. JSON → 业务 VO(复用 ZCApiUtils)
+        V bizVo;
+        try {
+            bizVo = ZCApiUtils.jsonToObject(respBizJson, voClass);
+        } catch (JsonProcessingException e) {
+            log.error("通用解析 - 响应 JSON 转换业务 VO 失败", e);
+            return ZCR.fail("5001", "解析响应数据失败:JSON 转换失败");
+        }
+
+        // 4. 业务 VO → Base64 JSON,封装回 ZCR
+        String respDataBase64;
+        try {
+            String bizVoJson = ZCApiUtils.objectToJson(bizVo);
+            respDataBase64 = ZCApiUtils.base64Encode(bizVoJson);
+        } catch (Exception e) {
+            log.error("通用解析 - 业务 VO 转换 Base64 JSON 失败", e);
+            return ZCR.fail("5001", "解析响应数据失败:结果封装失败");
+        }
 
-        return ZCR.ok(vo);
+        return ZCR.ok(respDataBase64, "");
     }
 
 

+ 504 - 247
ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/controller/zhongche/ZhongChePushController.java

@@ -1,36 +1,40 @@
 package org.dromara.external.controller.zhongche;
 
+import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.codec.Base64;
-import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.config.annotation.DubboReference;
 import org.dromara.common.core.domain.zhongche.domain.Goods;
 import org.dromara.common.core.domain.zhongche.domain.Prices;
 import org.dromara.common.core.domain.zhongche.vo.PricesVo;
-import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.external.api.zhongche.domain.*;
 import org.dromara.external.api.zhongche.domain.bo.AreaStockBo;
 import org.dromara.external.api.zhongche.domain.bo.*;
 import org.dromara.external.api.zhongche.domain.vo.*;
 import org.dromara.external.api.zhongche.domain.Catalog;
-import org.dromara.external.service.IExternalProductCategoryService;
-import org.dromara.external.util.SM2SignUtil;
+import org.dromara.external.util.SM2SignatureUtils;
+import org.dromara.external.util.SignParamUtils;
 import org.dromara.product.api.RemoteExternalOrderService;
 import org.dromara.product.api.RemoteProductService;
 import org.dromara.product.api.domain.ProductCategoryRemoteVo;
+import org.dromara.product.api.domain.zhongche.dto.StocksResult;
 import org.dromara.product.api.domain.zhongche.dto.StocksResultDto;
+import org.springframework.beans.BeanUtils;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.dromara.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
@@ -39,14 +43,15 @@ import static org.dromara.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
  * author
  * 时间:2026/1/5,19:03
  */
+@Slf4j
 @Validated
 @RequiredArgsConstructor
 @RestController
 @RequestMapping("/api/mall/auth")
 public class ZhongChePushController {
-    private final String url = "";
     private final String key = GLOBAL_REDIS_KEY+"external:zhongche:token:";
-    private final String CLIENT_ID = "ZC_CLIENT_ID";
+    private final String CLIENT_ID = "KFZAVuIyC56";
+    private final String VERSION = "1.0.0";
 
     @DubboReference
     private final RemoteProductService remoteProductService;
@@ -54,9 +59,9 @@ public class ZhongChePushController {
     @DubboReference
     private final RemoteExternalOrderService remoteOrderService;
 
-    private final IExternalProductCategoryService externalProductCategoryService;
+    private final String DEVELOPER_PRIVATE_KEY = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1YybOl0QDE2e9humlm4AgI3wJ1tI+UfVRZx8kk4hfPtZjorHN8Tjq/cP07t4Yscy+R9oFci8xw0VpBbcnlaq1w=="; // 电商提供的私钥
 
-    private final SM2SignUtil sm2SignUtil;
+    private final String DEVELOPER_PUBLIC_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgpQdXwMi21Mg1FhWad2AQLOwfNiDHgwhootau0YerQbagCgYIKoEcz1UBgi2hRANCAATVjJs6XRAMTZ72G6aWbgCAjfAnW0j5R9VFnHySTiF8+1mOisc3xOOr9w/Tu3hixzL5H2gVyLzHDRWkFtyeVqrX";  // 电商提供的公钥
 
 
 
@@ -71,43 +76,52 @@ public class ZhongChePushController {
 
     // 获取品目列表
     @PostMapping("/catalog/query")
-    public ZCR<String> catelogQuery(@RequestBody ZCTokenBo zcTokenBo) {
-            //TODO  1. 公共请求参数校验
-            ZCR<Void> checkResult = checkPublicParams(zcTokenBo);
-            if (!"0".equals(checkResult.getRespCode())) {
-                // 校验失败,直接返回错误响应
-                return ZCR.fail(checkResult.getRespCode(), checkResult.getRespMsg());
-            }
-            // 2.业务参数校验
-            String bizJson = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
-            if (!"{}".equals(bizJson.trim())){
-                return ZCR.fail("5007", "业务参数data格式错误,需为空JSON的Base64编码");
-            }
-
-            // TODO 3. AccessToken有效性校验
-            if (!checkAccessTokenValid(zcTokenBo.getAccessToken())) {
-                return ZCR.fail("5007", "accessToken无效或已过期");
-            }
-
-            // 4. 核心业务:查询品目列表并转换为文档要求的结构
-            List<Catalog> catalogVoList = remoteProductService.queryList()
-                .stream()
-                .map(this::convertToCatalogVo)
-                .collect(Collectors.toList());
-
-            // 5. 构造响应业务参数并Base64编码(放入ZCR的data字段)
-            CatalogsVo catalogVo = new CatalogsVo();
-            catalogVo.setCatalogs(catalogVoList);
-
-            // 业务参数→JSON→Base64编码
-            String respBizJson = JSONUtil.toJsonStr(catalogVo);
-            String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
-
-            //TODO 6. 生成响应签名(复用你的ZCR的sign字段)
-            String respSign = "";
-
-            // 7. 封装最终响应(严格复用你的ZCR.resetR)
-            return ZCR.ok(respDataBase64,respSign);
+    public ZCR catelogQuery(@RequestBody ZCTokenBo zcTokenBo) {
+        //  1. 公共请求参数校验
+        ZCR checkResult = checkPublicParams(zcTokenBo);
+        if (!"0".equals(checkResult.getRespCode())) {
+            // 校验失败,直接返回错误响应
+            return ZCR.fail(checkResult.getRespCode(), checkResult.getRespMsg());
+        }
+        // 2.业务参数校验
+        String bizJson = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
+        if (!"{}".equals(bizJson.trim())){
+            return ZCR.fail("5007", "业务参数data格式错误,需为空JSON的Base64编码");
+        }
+        //3. AccessToken有效性校验
+        ZCR zcr = checkAccessTokenValid(zcTokenBo.getAccessToken());
+        if (!zcr.getRespCode().equals("0")){
+            return zcr;
+        }
+        // 4. 核心业务:查询品目列表并转换为文档要求的结构  TODO这里标准品目没有做
+        List<Catalog> catalogVoList = remoteProductService.queryList()
+            .stream()
+            .map(this::convertToCatalogVo)
+            .collect(Collectors.toList());
+        // 5. 构造响应业务参数并Base64编码(放入ZCR的data字段)
+        CatalogsVo catalogVo = new CatalogsVo();
+        catalogVo.setCatalogs(catalogVoList);
+        // 业务参数→JSON→Base64编码
+        String respBizJson = JSONUtil.toJsonStr(catalogVo);
+        String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
+        // 6. 生成响应签名(核心:复用工具类,完成SM2withSM3签名,补全TODO)
+        String respSign = "";
+        try {
+            // 步骤6.1:封装空签名的成功响应实体(用于生成待签名字符串)
+            ZCR successZcr = ZCR.ok(respDataBase64, "");
+            // 步骤6.2:生成待签名字符串(筛选、排序、拼接JSON,严格贴合文档)
+            String respSignContent = SignParamUtils.getSignContent(successZcr);
+            // 步骤6.3:用电商私钥生成SM2签名(Base64编码,直接赋值给sign)
+            respSign = SM2SignatureUtils.sign(respSignContent, DEVELOPER_PRIVATE_KEY);
+        } catch (JsonProcessingException e) {
+            log.error("中车电商品目查询接口 - 待签名字符串生成失败,业务JSON:{}", respBizJson, e);
+            return ZCR.fail("5009", "接口响应签名生成异常(JSON转换失败)");
+        } catch (Exception e) {
+            log.error("中车电商品目查询接口 - 响应签名生成失败,业务JSON:{}", respBizJson, e);
+            return ZCR.fail("5010", "接口响应签名生成失败,请稍后重试");
+        }
+        // 7. 封装最终响应(严格复用你的ZCR.resetR)
+        return ZCR.ok(respDataBase64,respSign);
     }
 
     /**
@@ -116,7 +130,7 @@ public class ZhongChePushController {
     private Catalog convertToCatalogVo(ProductCategoryRemoteVo category) {
         Catalog catalog = new Catalog();
         // 以下字段映射需根据你的实体实际字段调整
-        catalog.setId(category.getCategoryNo());          // 品目id
+        catalog.setId(category.getId().toString());          // 品目id
         catalog.setPid(category.getParentId().toString());   // 父id
         catalog.setName(category.getCategoryName());      // 品目名称
         catalog.setLevel(category.getClassLevel().toString()); // 品目级次
@@ -150,77 +164,100 @@ public class ZhongChePushController {
      * @return
      */
     @PostMapping("/egoods/stock/query")
-    public ZCR<String> egoodsStockQuery(@RequestBody ZCTokenBo zcTokenBo) {
-            //TODO  1. 公共请求参数校验
-            ZCR<Void> checkResult = checkPublicParams(zcTokenBo);
-            if (!"0".equals(checkResult.getRespCode())) {
-                return ZCR.fail(checkResult.getRespCode(), checkResult.getRespMsg());
+    public ZCR egoodsStockQuery(@RequestBody ZCTokenBo zcTokenBo) {
+
+        //1. 公共请求参数校验
+        ZCR checkResult = checkPublicParams(zcTokenBo);
+        if (!"0".equals(checkResult.getRespCode())) {
+            return checkResult;
+        }
+        //2. AccessToken有效性校验
+        ZCR zcr = checkAccessTokenValid(zcTokenBo.getAccessToken());
+        if (!zcr.getRespCode().equals("0")){
+            return zcr;
+        }
+        //  3. 业务参数解析
+        AreaStockBo areaStockBo;
+        try {
+            // 解码data字段(Base64→JSON→业务BO)
+            String decodedData = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
+            if (StrUtil.isBlank(decodedData)) {
+                return ZCR.fail("5011", "业务参数data不能为空");
             }
-            // TODO 2. AccessToken有效性校验
-            if (!checkAccessTokenValid(zcTokenBo.getAccessToken())) {
-                return ZCR.fail("5007", "accessToken无效或已过期");
+            areaStockBo = JSONUtil.toBean(decodedData, AreaStockBo.class);
+            if (areaStockBo == null) {
+                return ZCR.fail("5012", "业务参数data格式错误,无法解析");
             }
-            //  3. 业务参数解析
-            AreaStockBo areaStockBo;
-            try {
-                // 解码data字段(Base64→JSON→业务BO)
-                String decodedData = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
-                areaStockBo = JSONUtil.toBean(decodedData, AreaStockBo.class);
-            } catch (Exception e) {
-                return ZCR.fail("5007", "业务参数data解析失败:" + e.getMessage());
+            // 3.3 业务参数必填项校验
+            if (areaStockBo.getGoods() == null || areaStockBo.getGoods().isEmpty()) {
+                return ZCR.fail("5013", "商品信息列表goods不能为空");
             }
-            //  4. 业务参数校验
-            String validateMsg = validateBizParams(areaStockBo);
-            if (StrUtil.isNotBlank(validateMsg)) {
-                return ZCR.fail("5007", validateMsg);
+            if (areaStockBo.getGoods().size() > 50) {
+                return ZCR.fail("5014", "商品信息列表goods最多支持50条");
             }
-            // 5.核心业务:库存查询
-            String areaId = areaStockBo.getAreaId();
-            Map<String, Integer> collect = areaStockBo.getGoods().stream()
-                .collect(Collectors.toMap(Goods::getGoodsId,
-                    Goods::getGoodsNum));
-            StocksResultDto stocksResultDto = remoteProductService.queryProductStock(collect,areaId);
-
-            // 6. 构造响应业务参数
-            // JSON→Base64编码(响应data字段值)
-            String respBizJson = JSONUtil.toJsonStr(stocksResultDto);
-            String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
-            // 6. 生成响应签名
-            String respSign = "";
-
-            // 7. 封装最终响应
-            return ZCR.ok(respDataBase64, respSign);
-    }
-    private String validateBizParams(AreaStockBo areaStockBo) {
-        // 1. goods列表校验
-        if (CollUtil.isEmpty(areaStockBo.getGoods())) {
-            return "商品信息goods列表不能为空";
+            // 3.4 单个商品信息校验(避免无效数据)
+            for (Goods goods : areaStockBo.getGoods()) {
+                if (StrUtil.isBlank(goods.getGoodsId()) || goods.getGoodsNum() == null || goods.getGoodsNum() <= 0) {
+                    return ZCR.fail("5015", "商品sku(goodsId)不能为空,所需库存(goodsNum)必须为正整数");
+                }
+            }
+        } catch (Exception e) {
+            return ZCR.fail("5007", "业务参数data解析失败:" + e.getMessage());
         }
-        if (areaStockBo.getGoods().size() > 50) {
-            return "商品信息goods列表最多支持50个商品";
+
+        GoodsStockVo goodsStockVo = new GoodsStockVo();
+        // 4. 核心业务:查询商品库存(封装业务逻辑,映射文档返回格式)
+        List<Stocks> stockRespList = queryGoodsStock(areaStockBo);
+        goodsStockVo.setStocks(stockRespList);
+        // 7. 业务参数→JSON→Base64编码(响应data字段要求)
+        String respBizJson = JSONUtil.toJsonStr(goodsStockVo);
+        String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
+
+        // 8. 生成响应签名(贴合签名机制,复用工具类)
+        String respSign = "";
+        try {
+            ZCR successZcr = ZCR.ok(respDataBase64, "");
+            String respSignContent = SignParamUtils.getSignContent(successZcr);
+            respSign = SM2SignatureUtils.sign(respSignContent, DEVELOPER_PRIVATE_KEY);
+        } catch (Exception e) {
+            log.error("响应签名生成失败,业务JSON:{}", respBizJson, e);
+            return ZCR.fail("5017", "接口响应签名生成失败");
         }
 
-        // 2. 遍历校验每个商品
-        for (int i = 0; i < areaStockBo.getGoods().size(); i++) {
-            Goods goods = areaStockBo.getGoods().get(i);
-            if (StrUtil.isBlank(goods.getGoodsId())) {
-                return "第" + (i + 1) + "个商品的goodsId(商品sku)不能为空";
-            }
-            if (goods.getGoodsNum() == null || goods.getGoodsNum() <= 0) {
-                return "第" + (i + 1) + "个商品的goodsNum(所需库存)必须大于0";
-            }
+        // 9. 封装最终响应(符合文档要求)
+        return ZCR.ok(respDataBase64, respSign);
+    }
+
+    /**
+     * 核心业务:查询商品库存,映射文档返回格式(包含库存状态、剩余数量等规则)
+     */
+    private List<Stocks> queryGoodsStock(AreaStockBo bo) {
+        String areaId = bo.getAreaId() == null ? "" : bo.getAreaId();
+
+        Map<String, Integer> goodsMap = new HashMap<>();
+        for (Goods goodsReq : bo.getGoods()) {
+            String goodsId = goodsReq.getGoodsId();
+            Integer goodsNum = goodsReq.getGoodsNum();
+            goodsMap.put(goodsId, goodsNum); // 对应Dubbo服务要求:key=商品ID,value=所需库存
         }
 
-        //TODO 3. areaId校验(虚拟库存可不填,有地区库存则必填)
-        //
-        /*boolean isVirtualStock = false; // 假设从配置获取:是否虚拟库存
-        if (!isVirtualStock && StrUtil.isBlank(areaStockBo.getAreaId())) {
-            return "非虚拟库存模式下,地区id areaId不能为空";
-        }*/
+        // 3. 调用Dubbo服务,获取库存结果(你的remoteProductService)
+        StocksResultDto stocksResultDto = remoteProductService.queryProductStock(goodsMap, areaId);
+
+        List<Stocks> goodsStockRespList = new ArrayList<>();
+        if (stocksResultDto == null || stocksResultDto.getStocks() == null || stocksResultDto.getStocks().isEmpty()) {
+            // 无库存数据返回,遍历入参商品,按「未查询到」处理
+            for (StocksResult stocksResult : stocksResultDto.getStocks()) {
+                Stocks stocks =new Stocks();
+                BeanUtils.copyProperties(stocksResult, stocks);
+                goodsStockRespList.add(stocks);
+            }
+        }
 
-        return null; // 校验通过
+        return goodsStockRespList;
     }
 
+
     //TODO 查询商品价格
     /*
         //请求业务参数
@@ -230,91 +267,174 @@ public class ZhongChePushController {
         PricesVo pricesVo = new PricesVo();
      */
     @PostMapping("/egoods/price/query")
-    public ZCR<String> egoodsPriceQuery(@RequestBody ZCTokenBo zcTokenBo) {
-            //TODO  1. 公共请求参数校验
-            ZCR<Void> checkResult = checkPublicParams(zcTokenBo);
-            if (!"0".equals(checkResult.getRespCode())) {
-                return ZCR.fail(checkResult.getRespCode(), checkResult.getRespMsg());
-            }
+    public ZCR egoodsPriceQuery(@RequestBody ZCTokenBo zcTokenBo) {
+        //1. 公共请求参数校验
+        ZCR checkResult = checkPublicParams(zcTokenBo);
+        if (!"0".equals(checkResult.getRespCode())) {
+            return ZCR.fail(checkResult.getRespCode(), checkResult.getRespMsg());
+        }
+        //2. AccessToken有效性校验
+        ZCR zcr = checkAccessTokenValid(zcTokenBo.getAccessToken());
+        if (!zcr.getRespCode().equals("0")){
+            return zcr;
+        }
+        // 3. 业务参数解析
+        GoodsPrieceBo goods;
+        List<String> goodsIdList;
+        // 解码data字段(Base64→JSON→业务BO)
+        String decodedData = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
+        if (StrUtil.isBlank(decodedData)) {
+            return ZCR.fail("5021", "业务参数data不能为空");
+        }
+        goods = JSONUtil.toBean(decodedData, GoodsPrieceBo.class);
+        if (goods == null || StrUtil.isBlank(goods.getGoodsIds())) {
+            return ZCR.fail("5022", "商品sku(goodsIds)不能为空");
+        }
+        // 3.3 分割商品sku,转换为列表(多个用,分隔)
+        goodsIdList = Arrays.stream(goods.getGoodsIds().split(","))
+            .map(String::trim)
+            .filter(StrUtil::isNotBlank)
+            .collect(Collectors.toList());
+        if (goodsIdList.isEmpty()) {
+            return ZCR.fail("5023", "商品sku(goodsIds)格式错误,有效sku不能为空");
+        }
+        // 4. 核心业务:调用Dubbo服务查询商品价格(适配你的业务逻辑)
+        PricesVo pricesVo = queryGoodsPrice(goodsIdList);
 
-            // TODO 2. AccessToken有效性校验
-            if (!checkAccessTokenValid(zcTokenBo.getAccessToken())) {
-                return ZCR.fail("5007", "accessToken无效或已过期");
-            }
+        // 5. 构造响应业务参数并Base64编码(符合文档data字段要求)
+        String respBizJson = JSONUtil.toJsonStr(pricesVo);
+        String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
 
-            // 3. 业务参数解析
-            GoodsPrieceBo goods;
-            // 解码data字段(Base64→JSON→业务BO)
-            String decodedData = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
-            goods = JSONUtil.toBean(decodedData, GoodsPrieceBo.class);
+        // 6. 生成响应签名(复用工具类,贴合签名机制)
+        String respSign = "";
+        try {
+            ZCR successZcr = ZCR.ok(respDataBase64, "");
+            String respSignContent = SignParamUtils.getSignContent(successZcr);
+            respSign = SM2SignatureUtils.sign(respSignContent, DEVELOPER_PRIVATE_KEY);
+        } catch (Exception e) {
+            log.error("商品价格查询 - 响应签名生成失败,业务JSON:{}", respBizJson, e);
+            return ZCR.fail("5025", "接口响应签名生成失败");
+        }
+        // 7. 封装最终响应(符合文档要求)
+        return ZCR.ok(respDataBase64, respSign);
+    }
 
-            // 4. 业务参数校验
-            String validateMsg = validateGoodsPriceParams(goods);
-            if (StrUtil.isNotBlank(validateMsg)) {
-                return ZCR.fail("5007", validateMsg);
+    /**
+     * 核心业务:查询商品价格,映射文档返回格式
+     */
+    private PricesVo queryGoodsPrice(List<String> goodsIdList) {
+        // 1. 初始化响应结果
+        PricesVo pricesVo = new PricesVo();
+        pricesVo.setPrices(new ArrayList<>());
+
+        // 2. 调用Dubbo服务查询商品价格(替换为你的真实Dubbo服务方法)
+        // 示例:假设你的Dubbo服务提供了查询商品价格的方法
+        Map<String, Prices> priceMap = remoteProductService.queryProductPrice(goodsIdList);
+
+        // 3. 遍历商品sku,封装价格信息(贴合文档规则)
+        for (String goodsId : goodsIdList) {
+            Prices priceResp = new Prices();
+            priceResp.setGoodsId(goodsId);
+
+            // 3.1 获取Dubbo查询结果,判断是否查询到
+            Prices productPrice = priceMap.get(goodsId);
+            if (productPrice == null) {
+                // 未查询到,按文档规则赋值-1(BigDecimal类型)
+                priceResp.setDsPrice(new BigDecimal(-1));
+                priceResp.setPrice(new BigDecimal(-1));
+                priceResp.setTaxFreePrice(null);
+                priceResp.setTax(null);
+                priceResp.setTaxCode("");
+                pricesVo.getPrices().add(priceResp);
+                continue;
             }
-            // 5. 查询商品价格
-            List<String> goodsIds = List.of(goods.toString().split(","));
-            List<Prices> prices = remoteProductService.queryProductPrice(goodsIds);
 
+            // 3.2 填充查询到的价格信息(保证精度,最多精确到分)
+            // 电商价格(dsPrice)
+            BigDecimal dsPrice = productPrice.getDsPrice() == null ? new BigDecimal(-1) : productPrice.getDsPrice();
+            priceResp.setDsPrice(dsPrice); // 保留2位小数,四舍五入
+
+            // 协议价格(price)
+            BigDecimal protocolPrice = productPrice.getPrice() == null ? new BigDecimal(-1) : productPrice.getPrice();
+            priceResp.setPrice(protocolPrice);
+            priceResp.setTaxFreePrice(null);
+            priceResp.setTax(productPrice.getTax());
+            // 税收编码(非必填)
+            priceResp.setTaxCode(null);
+
+            // 3.3 添加到响应列表
+            pricesVo.getPrices().add(priceResp);
+        }
 
-            // 6. 构造响应业务参数
-            // 封装响应根对象(包含prices列表)
-            PricesVo pricesVo = new PricesVo();
-            pricesVo.setPrices(prices);
-            System.out.println(pricesVo);
+        return pricesVo;
+    }
 
-            // JSON→Base64编码(响应data字段值)
-            String respBizJson = JSONUtil.toJsonStr(pricesVo);
-            String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
+    // 4.5查询物流信息(物流信息尚未实现)
+    @PostMapping("/get/track")
+    public ZCR getTrack(@RequestBody ZCTokenBo zcTokenBo) {
+        // 1. 公共请求参数校验(含签名、版本、clientId等,复用已有逻辑)
+        ZCR checkResult = checkPublicParams(zcTokenBo);
+        if (!"0".equals(checkResult.getRespCode())) {
+            return checkResult;
+        }
 
-            //  7. 生成响应签名
-            Map<String, String> map = new HashMap<>();
-            map.put("respCode", "0");
-            map.put("data", respBizJson);
-            String respSign = JsonUtils.toJsonString(map);
+        //2. AccessToken有效性校验
+        ZCR zcr = checkAccessTokenValid(zcTokenBo.getAccessToken());
+        if (!zcr.getRespCode().equals("0")){
+            return zcr;
+        }
 
-            //8. 封装最终响应
-            return ZCR.ok(respDataBase64, respSign);
-    }
-    /**
-     * 业务参数校验(匹配4.4.4.1文档规则)
-     */
-    private String validateGoodsPriceParams(GoodsPrieceBo goods) {
-        // 1. goodsIds非空校验
-        if (StrUtil.isBlank(goods.getGoodsIds())) {
-            return "商品sku(goodsIds)不能为空";
-        }
-
-        // 2. 拆分goodsIds并校验格式/数量
-        String[] goodsIdArray = goods.getGoodsIds().split(",");
-        if (goodsIdArray.length == 0) {
-            return "商品sku(goodsIds)格式错误,多个sku需用英文逗号分隔";
-        }
-        // 3. 校验每个sku非空
-        for (int i = 0; i < goodsIdArray.length; i++) {
-            String goodsId = goodsIdArray[i].trim();
-            if (StrUtil.isBlank(goodsId)) {
-                return "第" + (i + 1) + "个商品sku为空,请检查goodsId格式";
+        // 3. 业务参数解析与校验(贴合文档要求)
+        DeliveryTrackBo bizReq;
+        try {
+            // 3.1 Base64解码data字段
+            String decodedData = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
+            if (StrUtil.isBlank(decodedData)) {
+                return ZCR.fail("5031", "业务参数data不能为空");
+            }
+
+            // 3.2 JSON转业务请求实体
+            bizReq = JSONUtil.toBean(decodedData, DeliveryTrackBo.class);
+            if (bizReq == null) {
+                return ZCR.fail("5032", "业务参数data格式错误,无法解析");
+            }
+            // 3.3 业务参数必填项校验
+            if (StrUtil.isBlank(bizReq.getOutgoingCode()) || bizReq.getOutgoingCode().length() > 20) {
+                return ZCR.fail("5033", "发货单编号(outgoingCode)不能为空,且长度不超过20");
             }
+            if (StrUtil.isBlank(bizReq.getWaybillType()) || !List.of("0", "1").contains(bizReq.getWaybillType())) {
+                return ZCR.fail("5034", "运单类型(waybillType)必填,且仅支持0(订单)、1(换新单)");
+            }
+        } catch (Exception e) {
+            log.error("物流查询 - 业务参数解析/校验失败,data:{}", zcTokenBo.getData(), e);
+            return ZCR.fail("5035", "业务参数data解析失败:" + e.getMessage());
         }
-        return null; // 校验通过
-    }
 
-    //TODO 4.5查询物流信息(物流信息尚未实现)
-    @PostMapping("/get/track")
-    public ZCR<TrackVo> getTrack(@RequestBody ZCTokenBo zcTokenBo) {
-        //请求业务参数
-        DeliveryTrack deliveryTrack = new DeliveryTrack();
+        //******4. 核心业务:调用Dubbo服务查询物流信息   我们没有换新单 所以第二个参数先抛弃掉
+        TrackVo trackVo = remoteOrderService.queryLogisticsTrack(bizReq.getOutgoingCode(), bizReq.getWaybillType());
+        if (trackVo == null){
+            return ZCR.fail("5035", "物流信息为空");
+        }
+        // 6. 业务参数→JSON→Base64编码(符合文档data字段要求)
+        String respBizJson = JSONUtil.toJsonStr(trackVo);
+        String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
 
-        //响应业务参数
-        TrackVo trackVo = new TrackVo();
-        DeliveryTrack deliveryTrack1 = new DeliveryTrack();
+        // 7. 生成响应签名(复用工具类,贴合签名机制)
+        String respSign = "";
+        try {
+            ZCR successZcr = ZCR.ok(respDataBase64, "");
+            String respSignContent = SignParamUtils.getSignContent(successZcr);
+            respSign = SM2SignatureUtils.sign(respSignContent, DEVELOPER_PRIVATE_KEY);
+        } catch (Exception e) {
+            log.error("物流查询 - 响应签名生成失败,业务JSON:{}", respBizJson, e);
+            return ZCR.fail("5036", "接口响应签名生成失败");
+        }
 
-        return null;
+        // 8. 封装最终响应(符合文档要求)
+        return ZCR.ok(respDataBase64, respSign);
     }
 
-    //TODO 4.6 查询电商平台订单号
+    //4.6 查询电商平台订单号
     /*
         //请求业务参数
         MallOrderNoQueryBo mallOrderNoQueryBo = new MallOrderNoQueryBo();
@@ -323,87 +443,215 @@ public class ZhongChePushController {
         MallOrderNoVo mallOrderNoVo = new MallOrderNoVo();
      */
     @PostMapping("/get/mallOrderNo")
-    public ZCR<String> getMallOrderNo(@RequestBody ZCTokenBo zcTokenBo) {
+    public ZCR getMallOrderNo(@RequestBody ZCTokenBo zcTokenBo) {
         //1. 公共请求参数校验
-        ZCR<Void> checkResult = checkPublicParams(zcTokenBo);
+        ZCR checkResult = checkPublicParams(zcTokenBo);
         if (!"0".equals(checkResult.getRespCode())) {
             return ZCR.fail(checkResult.getRespCode(), checkResult.getRespMsg());
         }
-
-        // TODO 2. AccessToken有效性校验
-        if (!checkAccessTokenValid(zcTokenBo.getAccessToken())) {
-            return ZCR.fail("5007", "accessToken无效或已过期");
+        // 2. 请求签名校验(必须)
+        boolean verifyResult;
+        try {
+            verifyResult = SignParamUtils.verifyRequestSign(
+                zcTokenBo,
+                DEVELOPER_PUBLIC_KEY   // 中车给你的公钥
+            );
+        } catch (Exception e) {
+            log.error("查询电商订单号 - 请求验签异常", e);
+            return ZCR.fail("5005", "请求签名校验异常");
         }
 
-        //2. 业务参数解析(Base64→JSON→BO)
+        if (!verifyResult) {
+            return ZCR.fail("5006", "请求签名校验失败");
+        }
+        //3. AccessToken 有效性校验
+        ZCR zcr = checkAccessTokenValid(zcTokenBo.getAccessToken());
+        if (!zcr.getRespCode().equals("0")){
+            return zcr;
+        }
+        // 4. 业务参数解析(Base64 → JSON → BO)
         MallOrderNoQueryBo orderNoQueryBo;
-
-        // 解码data字段(核心:Base64解码为JSON字符串)
-        String decodedData = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
-        // JSON转业务BO(你已创建的MallOrderNoQueryBo)
-        orderNoQueryBo = JSONUtil.toBean(decodedData, MallOrderNoQueryBo.class);
-
-        //3. 业务参数校验
-        String validateMsg = validateOrderParams(orderNoQueryBo);
-        if (StrUtil.isNotBlank(validateMsg)) {
-            return ZCR.fail("5007", validateMsg);
+        try {
+            String decodedData = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
+            orderNoQueryBo = JSONUtil.toBean(decodedData, MallOrderNoQueryBo.class);
+        } catch (Exception e) {
+            log.error("查询电商订单号 - 业务参数解析失败,data={}", zcTokenBo.getData(), e);
+            return ZCR.fail("5008", "业务参数解析失败");
         }
 
-        //5.查询电商订单号
+        // 5. 查询电商平台订单号(你的内部逻辑)
         String orderNo = orderNoQueryBo.getOrderNo();
-        String mallOrderNo = remoteOrderService.getOrderNo(orderNo);
-        // 6. 构造响应业务参数
+        String mallOrderNo;
+        try {
+            mallOrderNo = remoteOrderService.getOrderNo(orderNo);
+        } catch (Exception e) {
+            log.error("查询电商订单号 - 查询失败,orderNo={}", orderNo, e);
+            return ZCR.fail("5010", "查询电商订单号失败");
+        }
+        // 6. 构造业务响应 data(VO → JSON → Base64)
         MallOrderNoVo orderNoVo = new MallOrderNoVo();
         orderNoVo.setMallOrderNo(mallOrderNo);
-        // VO→JSON→Base64编码(响应的data字段值)
+
         String respBizJson = JSONUtil.toJsonStr(orderNoVo);
         String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
-        //7. 生成响应签名
-        String respSign = "";
-        //8. 封装最终响应
-        return ZCR.ok(respDataBase64, respSign);
-    }
-    private String validateOrderParams(MallOrderNoQueryBo orderNoQueryBo) {
-        // 1. orderNo非空校验
-        if (StrUtil.isBlank(orderNoQueryBo.getOrderNo())) {
-            return "中车电子商城订单编号(orderNo)不能为空";
-        }
 
-        // 2. orderNo长度校验(≤20)
-        if (orderNoQueryBo.getOrderNo().length() > 20) {
-            return "中车电子商城订单编号(orderNo)长度不能超过20位";
+        // 7. 生成响应签名
+        ZCR resp = ZCR.ok(respDataBase64);
+        String respSign;
+        try {
+            String respSignContent = SignParamUtils.getSignContent(resp);
+            respSign = SM2SignatureUtils.sign(respSignContent, DEVELOPER_PRIVATE_KEY);
+        } catch (Exception e) {
+            log.error("查询电商订单号 - 响应签名生成失败,业务JSON={}", respBizJson, e);
+            return ZCR.fail("5036", "接口响应签名生成失败");
         }
 
-        return null; // 校验通过
+        // 9. 回填 sign 并返回
+        resp.setSign(respSign);
+        return resp;
     }
 
-    //TODO 4.7 查询电商平台售后单号
+    //4.7 查询电商平台售后单号
     @PostMapping("/get/mallAfterSaleNo")
-    public ZCR<MallAfterSaleNoVo> getMallAfterSaleNo(@RequestBody ZCTokenBo zcTokenBo) {
-        //请求业务参数
-        MallAfterSaleNoQueryBo mallAfterSaleNoQueryBo = new MallAfterSaleNoQueryBo();
+    public ZCR getMallAfterSaleNo(@RequestBody ZCTokenBo zcTokenBo) {
+        // 1. 公共请求参数校验
+        ZCR checkResult = checkPublicParams(zcTokenBo);
+        if (!"0".equals(checkResult.getRespCode())) {
+            return ZCR.fail(checkResult.getRespCode(), checkResult.getRespMsg());
+        }
 
-        //响应业务参数
-        MallAfterSaleNoVo mallAfterSaleNoVo = new MallAfterSaleNoVo();
+        //2. AccessToken 有效性校验
+        ZCR zcr = checkAccessTokenValid(zcTokenBo.getAccessToken());
+        if (!zcr.getRespCode().equals("0")){
+            return zcr;
+        }
 
-        return null;
+        // 3. 业务参数解析(Base64 → JSON → BO)
+        MallAfterSaleNoQueryBo queryBo;
+        try {
+            String decodedData = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
+            queryBo = JSONUtil.toBean(decodedData, MallAfterSaleNoQueryBo.class);
+        } catch (Exception e) {
+            log.error("售后单号查询 - 业务参数解析失败", e);
+            return ZCR.fail("5007", "业务参数解析失败");
+        }
+
+        // 4. 业务参数校验
+        if (queryBo == null || StrUtil.isBlank(queryBo.getAfterSaleNo())) {
+            return ZCR.fail("5007", "afterSaleNo不能为空");
+        }
+
+        // 5. 查询电商平台售后单号(内部逻辑你自己实现)
+        String afterSaleNo = queryBo.getAfterSaleNo();
+        String mallAfterSaleNo = remoteOrderService.getReturnOrderNo(afterSaleNo);
+
+        if (StrUtil.isBlank(mallAfterSaleNo)) {
+            return ZCR.fail("5004", "未查询到对应的电商平台售后单号");
+        }
+
+        // 6. 构造响应业务参数
+        MallAfterSaleNoVo vo = new MallAfterSaleNoVo();
+        vo.setMallAfterSaleNo(mallAfterSaleNo);
+
+        String respBizJson = JSONUtil.toJsonStr(vo);
+        String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
+
+        // 7. 生成响应签名(关键补全点)
+        String respSign;
+        try {
+            // 先构造不带 sign 的响应对象
+            ZCR successZcr = ZCR.ok(respDataBase64);
+
+            // 按平台规则生成待签名字符串
+            String respSignContent = SignParamUtils.getSignContent(successZcr);
+
+            // 使用【电商私钥】进行 SM2 签名
+            respSign = SM2SignatureUtils.sign(respSignContent, DEVELOPER_PRIVATE_KEY);
+        } catch (Exception e) {
+            log.error("售后单号查询 - 响应签名生成失败,业务JSON:{}", respBizJson, e);
+            return ZCR.fail("5036", "接口响应签名生成失败");
+        }
+
+        // 8. 返回最终响应
+        return ZCR.ok(respDataBase64, respSign);
     }
 
     //TODO 4.8 消息监听     消息类型枚举没做
     @PostMapping("/message/listening")
-    public ZCR<MessageVo> listener(@RequestBody ZCTokenBo zcTokenBo) {
-        //请求业务参数
-        MessageBo messageBo = new MessageBo();
+    public ZCR listener(@RequestBody ZCTokenBo zcTokenBo) {
+        // 1. 公共请求参数校验
+        ZCR checkResult = checkPublicParams(zcTokenBo);
+        if (!"0".equals(checkResult.getRespCode())) {
+            return ZCR.fail(checkResult.getRespCode(), checkResult.getRespMsg());
+        }
 
-        //响应业务参数
+        // 2. accessToken 校验
+        ZCR tokenCheck = checkAccessTokenValid(zcTokenBo.getAccessToken());
+        if (!"0".equals(tokenCheck.getRespCode())) {
+            return tokenCheck;
+        }
+        // 3. 业务参数解析
+        MessageBo messageBo;
+        try {
+            String decodedData = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
+            messageBo = JSONUtil.toBean(decodedData, MessageBo.class);
+        } catch (Exception e) {
+            log.error("消息监听 - 业务参数解析失败", e);
+            return ZCR.fail("5007", "业务参数解析失败");
+        }
+        // 4. 业务参数必填校验
+        if (messageBo == null
+            || StrUtil.isBlank(messageBo.getId())
+            || StrUtil.isBlank(messageBo.getType())
+            || messageBo.getContent() == null
+            || StrUtil.isBlank(messageBo.getTime())) {
+            return ZCR.fail("5007", "消息业务参数不完整");
+        }
+
+        // 5. 按消息类型处理业务
+        try {
+            MessageTypeEnum typeEnum = MessageTypeEnum.of(messageBo.getType());
+            switch (typeEnum) {
+                case NEW_ORDER:
+                    handleNewOrder(messageBo);
+                    break;
+                case AFTER_SALE_APPLY:
+                    handleAfterSale(messageBo);
+                    break;
+                default:
+                    log.warn("未处理的消息类型:{}", messageBo.getType());
+            }
+        } catch (Exception e) {
+            log.error("消息监听 - 业务处理失败,msgId={}", messageBo.getId(), e);
+            return ZCR.fail("5006", "消息处理失败");
+        }
+        // 6. 构造响应业务参数
         MessageVo messageVo = new MessageVo();
+        messageVo.setResult("1");
+        messageVo.setMessage("success");
+
+        String respBizJson = JSONUtil.toJsonStr(messageVo);
+        String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
+
+        // 7. 生成响应签名(和 4.7 一模一样)
+        String respSign;
+        try {
+            ZCR successZcr = ZCR.ok(respDataBase64);
+            String respSignContent = SignParamUtils.getSignContent(successZcr);
+            respSign = SM2SignatureUtils.sign(respSignContent, DEVELOPER_PRIVATE_KEY);
+        } catch (Exception e) {
+            log.error("消息监听 - 响应签名生成失败,业务JSON:{}", respBizJson, e);
+            return ZCR.fail("5036", "接口响应签名生成失败");
+        }
+
+        // 8. 返回响应
+        return ZCR.ok(respDataBase64, respSign);
 
-        return null;
     }
 
     //TODO 4.9 非必接模块接入校验
     @PostMapping("/notRequireApi/check")
-    public ZCR<ModuleCheckVo> notRequireApiCheck(@RequestBody ZCTokenBo zcTokenBo) {
+    public ZCR notRequireApiCheck(@RequestBody ZCTokenBo zcTokenBo) {
         //请求业务参数
         ModuleCheckBo moduleCheckBo = new ModuleCheckBo();
 
@@ -415,7 +663,7 @@ public class ZhongChePushController {
 
     //TODO 4.10 	查询运费
     @PostMapping("egoods/getfreight")
-    public ZCR<FreightVo> getFreight(@RequestBody ZCTokenBo zcTokenBo) {
+    public ZCR getFreight(@RequestBody ZCTokenBo zcTokenBo) {
         //请求业务参数
         AreaStockBo areaStock = new AreaStockBo();
 
@@ -429,7 +677,7 @@ public class ZhongChePushController {
     /**
      * 校验公共请求参数(基于你的ZCTokenBo)
      */
-    private ZCR<Void> checkPublicParams(ZCTokenBo zcTokenBo) {
+    private ZCR checkPublicParams(ZCTokenBo zcTokenBo) {
         // 1. 必填项非空校验(严格按文档4.2.4)
         if (StrUtil.isBlank(zcTokenBo.getVersion())) {
             return ZCR.fail("5007", "接口版本version不能为空");
@@ -447,32 +695,36 @@ public class ZhongChePushController {
             return ZCR.fail("5007", "签名sign不能为空");
         }
 
-        // 2. TODO 版本校验(固定1.0.0)
-        /*if (!"1.0.0".equals(zcTokenBo.getVersion())) {
+        // 2.版本校验(固定1.0.0)
+        if (!VERSION.equals(zcTokenBo.getVersion())) {
             return ZCR.fail("1008", "仅支持接口版本1.0.0");
-        }*/
+        }
 
         // 3. 时间戳格式校验(14位)
-        if (zcTokenBo.getTimestamp().length() != 14) {
-            return ZCR.fail("5007", "时间戳格式错误,需为14位YYYYMMDDHHMMSS");
+        if (!ReUtil.isMatch("\\d{14}", zcTokenBo.getTimestamp())) {
+            return ZCR.fail("5007", "时间戳格式错误,需为14位数字YYYYMMDDHHMMSS");
         }
-        if (zcTokenBo.getClientId() !=CLIENT_ID){
+        // 4. clientId 校验
+        if (!CLIENT_ID.equals(zcTokenBo.getClientId())){
             return ZCR.fail("5007", "clientId错误");
         }
 
-        // 4.TODO  签名校验(验证中车请求的签名)
-        /*try {
-            boolean signValid = sm2SignUtil.sm2Verify(
-                JSONUtil.toJsonStr(SM2SignUtil.beanToFilteredMap(zcTokenBo)).getBytes(),
-                zcTokenBo.getSign(),
-                "中车提供的公钥(16进制)" // 替换为真实公钥
-            );
+        //
+        // 5. 签名校验
+        try {
+            // 防御性 Base64 校验
+            Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
+            // 调用 SignParamUtils 验证请求签名,直接复用之前的逻辑
+            boolean signValid = SignParamUtils.verifyRequestSign(zcTokenBo, DEVELOPER_PUBLIC_KEY);
             if (!signValid) {
-                return ZCR.fail("1010", "签名验证失败");
+                // 验签失败,返回错误响应
+                return ZCR.fail("5007", "接口签名错误(验签失败)");
             }
         } catch (Exception e) {
-            return ZCR.fail("1011", "签名校验异常:" + e.getMessage());
-        }*/
+            // 捕获验签过程中的异常(如JSON转换、Base64解码、SM2算法异常等)
+            log.error("中车电商接口签名校验异常,请求参数:{}", JSONUtil.toJsonStr(zcTokenBo), e);
+            return ZCR.fail("5007", "接口签名校验异常,请稍后重试");
+        }
 
         // 校验通过
         return ZCR.ok(null);
@@ -481,11 +733,16 @@ public class ZhongChePushController {
 
 
     /**
-     * 校验AccessToken有效性(替换为你的真实逻辑)
+     * 校验AccessToken有效性
      */
-    private boolean checkAccessTokenValid(String accessToken) {
-        // 临时占位,需替换
-        return true;
+    private ZCR checkAccessTokenValid(String accessToken) {
+        if(ObjectUtil.isEmpty(accessToken)){
+            return ZCR.fail("5006","token不能为空");
+        }
+        if(StpUtil.getTokenTimeout(accessToken) == -2l){
+            return ZCR.fail("5005","token_expired");
+        }
+        return ZCR.ok( null);
     }
 
 

+ 85 - 0
ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/enums/MallMessageTypeEnum.java

@@ -0,0 +1,85 @@
+package org.dromara.external.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum MallMessageTypeEnum {
+
+    /* ======================= 订单 ======================= */
+    ORDER_CREATE("2001", "订单", "新订单消息"),
+    ORDER_CANCEL("2002", "订单", "取消订单消息"),
+    ORDER_RECEIVE_CONFIRM("2003", "订单", "确认收货消息"),
+    ORDER_PRE_CONFIRM("2004", "订单", "确认预购单"),
+    ORDER_PAY_NOTICE("2005", "订单", "订单支付信息通知"),
+
+    /* ======================= 备货单 ======================= */
+    PREPARE_CREATE("2006", "备货单", "新备货单消息"),
+    PREPARE_CONFIRM("2007", "备货单", "确认备货单消息"),
+    PREPARE_CANCEL("2008", "备货单", "取消备货单消息"),
+
+    /* ======================= 订单取消申请 ======================= */
+    ORDER_CANCEL_APPLY("2009", "订单", "取消订单申请消息"),
+
+    /* ======================= 售后 ======================= */
+    AFTER_SALE_APPLY("2101", "售后单", "申请售后消息"),
+    AFTER_SALE_CANCEL("2102", "售后单", "取消售后消息"),
+    AFTER_SALE_DELIVER("2103", "售后单", "退货发运消息"),
+    AFTER_SALE_RECEIVE_CONFIRM("2104", "售后单", "确认收货"),
+    AFTER_SALE_REFUND_NOTICE("2105", "售后单", "售后退款信息通知"),
+
+    /* ======================= 账户 ======================= */
+    ACCOUNT_ACTIVE("2201", "账户", "电商账户已生效"),
+    NOTICE("2202", "账户", "通知公告信息"),
+
+    /* ======================= 对账 ======================= */
+    BILL_RULE_CREATE("2301", "对账", "生成对账规则消息"),
+    BILL_CONFIRM("2302", "对账", "采购确认账单"),
+    BILL_MODIFY("2303", "对账", "采购修改账单"),
+    BILL_WAIT_INVOICE("2304", "对账", "待开票订单消息"),
+    BILL_CREATE("2305", "对账", "采购生成对账单消息"),
+    BILL_FINISH("2306", "对账", "采购完成对账单消息"),
+    ABNORMAL_PASS("2307", "对账", "异议单审核通过消息"),
+    ABNORMAL_REJECT("2308", "对账", "异议单审核不通过消息"),
+
+    /* ======================= 开票 ======================= */
+    INVOICE_APPLY("2401", "开票", "开票申请消息"),
+    INVOICE_CONFIRM("2402", "开票", "采购确认收票消息"),
+    INVOICE_REFUND_APPLY("2403", "开票", "退票申请消息"),
+    INVOICE_RETURN("2405", "开票", "采购邮寄退票消息"),
+
+    /* ======================= 库存 ======================= */
+    STOCK_LOCK("2501", "购物车", "库存锁定消息"),
+    STOCK_UNLOCK("2502", "购物车", "库存解定消息"),
+
+    /* ======================= 商品 ======================= */
+    GOODS_IMPORT_BLOCK("2601", "商品", "商品导入拦截消息"),
+    GOODS_WAIT_AUDIT("2602", "商品", "商品待审核消息"),
+    GOODS_AUDIT_PASS("2603", "商品", "商品审核通过消息"),
+    GOODS_AUDIT_REJECT("2604", "商品", "商品审核不通过消息"),
+    GOODS_ON_SHELF_FAIL("2605", "商品", "商品上架失败消息"),
+    GOODS_OFF_SHELF("2606", "商品", "采购主动下架商品消息"),
+
+    /* ======================= 结算 ======================= */
+    SETTLEMENT_CREATE("2701", "结算", "采购生成结算单消息"),
+    SETTLEMENT_PAYED("2702", "结算", "采购结算单已付款消息"),
+    SETTLEMENT_INVOICE_RETURN("2703", "结算", "采购结算单开票退回消息");
+
+    private final String code;
+    private final String module;
+    private final String desc;
+
+    /**
+     * 根据 type 查枚举
+     */
+    public static MallMessageTypeEnum of(String code) {
+        for (MallMessageTypeEnum type : values()) {
+            if (type.code.equals(code)) {
+                return type;
+            }
+        }
+        return null;
+    }
+}

+ 128 - 0
ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/util/SM2SignatureUtils.java

@@ -0,0 +1,128 @@
+package org.dromara.external.util;
+
+import org.bouncycastle.asn1.gm.GMNamedCurves;
+import org.bouncycastle.asn1.x9.X9ECParameters;
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
+import org.bouncycastle.crypto.params.ECDomainParameters;
+import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.params.ECPublicKeyParameters;
+import org.bouncycastle.crypto.signers.SM2Signer;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.util.encoders.Base64;
+
+import java.security.SecureRandom;
+import java.security.Security;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 国密 SM2withSM3 签名工具类
+ * 注意:开发者私钥/公钥需由电商平台提供,此处提供生成示例(实际使用平台分配的密钥)
+ */
+public class SM2SignatureUtils {
+    // 注册 BouncyCastle 安全提供者
+    static {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+
+    // SM2 曲线参数(固定,国密标准)
+    private static final X9ECParameters SM2_CURVE_PARAMS = GMNamedCurves.getByName("sm2p256v1");
+    private static final ECDomainParameters EC_DOMAIN_PARAMETERS = new ECDomainParameters(
+            SM2_CURVE_PARAMS.getCurve(),
+            SM2_CURVE_PARAMS.getG(),
+            SM2_CURVE_PARAMS.getN(),
+            SM2_CURVE_PARAMS.getH()
+    );
+
+    /**
+     * 生成 SM2 密钥对(仅用于测试,实际使用电商平台提供的开发者私钥/公钥)
+     * @return 密钥对(私钥在前,公钥在后,均为 Base64 编码)
+     */
+    public static String[] generateSM2KeyPair() {
+        ECKeyPairGenerator generator = new ECKeyPairGenerator();
+        ECKeyGenerationParameters genParams = new ECKeyGenerationParameters(EC_DOMAIN_PARAMETERS, new SecureRandom());
+        generator.init(genParams);
+
+        AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
+        ECPrivateKeyParameters privateKeyParams = (ECPrivateKeyParameters) keyPair.getPrivate();
+        ECPublicKeyParameters publicKeyParams = (ECPublicKeyParameters) keyPair.getPublic();
+
+        // 转换为 Base64 编码字符串
+        String privateKeyBase64 = Base64.toBase64String(privateKeyParams.getD().toByteArray());
+        String publicKeyBase64 = Base64.toBase64String(publicKeyParams.getQ().getEncoded(false));
+
+        return new String[]{privateKeyBase64, publicKeyBase64};
+    }
+
+    /**
+     * SM2withSM3 签名(私钥签名,生成待提交的 sign 字段)
+     * @param content 待签名字符串(筛选排序后的 JSON 字符串)
+     * @param privateKeyBase64 开发者私钥(Base64 编码,平台提供)
+     * @return 签名结果(Base64 编码,可直接赋值给 sign 字段)
+     */
+    public static String sign(String content, String privateKeyBase64) {
+        if (content == null || content.trim().isEmpty() || privateKeyBase64 == null || privateKeyBase64.trim().isEmpty()) {
+            throw new IllegalArgumentException("待签名字符串和私钥不能为空");
+        }
+
+        try {
+            // 1. 解码私钥
+            byte[] privateKeyBytes = Base64.decode(privateKeyBase64);
+            ECPrivateKeyParameters privateKeyParams = new ECPrivateKeyParameters(
+                    new java.math.BigInteger(1, privateKeyBytes),
+                    EC_DOMAIN_PARAMETERS
+            );
+
+            // 2. 初始化 SM2 签名器
+            SM2Signer signer = new SM2Signer();
+            signer.init(true, privateKeyParams);
+
+            // 3. 传入待签名内容(UTF-8 编码)
+            byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
+            signer.update(contentBytes, 0, contentBytes.length);
+
+            // 4. 生成签名并进行 Base64 编码
+            byte[] signatureBytes = signer.generateSignature();
+            return Base64.toBase64String(signatureBytes);
+        } catch (Exception e) {
+            throw new RuntimeException("SM2 签名失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * SM2withSM3 验签(公钥校验,验证请求传入的 sign 是否合法)
+     * @param content 待签名字符串(与签名时一致的 JSON 字符串)
+     * @param signBase64 请求传入的 sign 字段(Base64 编码)
+     * @param publicKeyBase64 开发者公钥(Base64 编码,平台提供)
+     * @return true=验签通过,false=验签失败
+     */
+    public static boolean verify(String content, String signBase64, String publicKeyBase64) {
+        if (content == null || content.trim().isEmpty() || signBase64 == null || signBase64.trim().isEmpty()
+                || publicKeyBase64 == null || publicKeyBase64.trim().isEmpty()) {
+            throw new IllegalArgumentException("待签名字符串、签名和公钥不能为空");
+        }
+
+        try {
+            // 1. 解码公钥和签名
+            byte[] publicKeyBytes = Base64.decode(publicKeyBase64);
+            byte[] signatureBytes = Base64.decode(signBase64);
+
+            // 2. 构建公钥参数
+            org.bouncycastle.math.ec.ECPoint ecPoint = SM2_CURVE_PARAMS.getCurve().decodePoint(publicKeyBytes);
+            ECPublicKeyParameters publicKeyParams = new ECPublicKeyParameters(ecPoint, EC_DOMAIN_PARAMETERS);
+
+            // 3. 初始化 SM2 签名器
+            SM2Signer signer = new SM2Signer();
+            signer.init(false, publicKeyParams);
+
+            // 4. 传入待签名内容并验签
+            byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
+            signer.update(contentBytes, 0, contentBytes.length);
+
+            return signer.verifySignature(signatureBytes);
+        } catch (Exception e) {
+            throw new RuntimeException("SM2 验签失败:" + e.getMessage(), e);
+        }
+    }
+}

+ 109 - 0
ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/util/SignParamUtils.java

@@ -0,0 +1,109 @@
+package org.dromara.external.util;
+
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.dromara.external.api.zhongche.domain.bo.ZCTokenBo;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+/**
+ * 签名参数处理工具类(筛选、排序、拼接JSON)
+ */
+public class SignParamUtils {
+    /**
+     * JSON 转换工具(保持与 ZCApiUtils 一致)
+     */
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    /**
+     * 处理签名参数(核心方法)
+     * @param paramObj 请求/响应参数实体(如 ZCTokenBo、ZCR)
+     * @return 待签名字符串(筛选排序后的 JSON 字符串)
+     * @throws JsonProcessingException JSON 转换异常
+     */
+    public static String getSignContent(Object paramObj) throws JsonProcessingException {
+        if (paramObj == null) {
+            throw new IllegalArgumentException("参数实体不能为空");
+        }
+
+        // 步骤 1:反射获取参数实体的所有字段(键值对)
+        Map<String, Object> paramMap = new HashMap<>();
+        Field[] fields = paramObj.getClass().getDeclaredFields();
+        for (Field field : fields) {
+            field.setAccessible(true); // 允许访问私有字段
+            String fieldName = field.getName();
+            Object fieldValue = null;
+
+            try {
+                fieldValue = field.get(paramObj);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException("获取字段值失败:" + fieldName, e);
+            }
+
+            // 步骤 2:筛选字段(剔除 sign、剔除 null 值、剔除字节数组)
+            if ("sign".equals(fieldName)) {
+                continue; // 剔除 sign 字段
+            }
+            if (fieldValue == null) {
+                continue; // 剔除 null 值
+            }
+            if (fieldValue instanceof byte[]) {
+                continue; // 剔除字节类型字段(当前场景无,预留)
+            }
+
+            // 符合条件的字段存入 Map
+            paramMap.put(fieldName, fieldValue);
+        }
+
+        // 步骤 3:按字段名(键)ASCII 码升序排序
+        Map<String, Object> sortedParamMap = new TreeMap<>(new Comparator<String>() {
+            @Override
+            public int compare(String key1, String key2) {
+                // 按 ASCII 码升序排序(逐个字符对比)
+                return key1.compareTo(key2);
+            }
+        });
+        sortedParamMap.putAll(paramMap);
+
+        // 步骤 4:拼接为 JSON 字符串(待签名字符串)
+        return OBJECT_MAPPER.writeValueAsString(sortedParamMap);
+    }
+
+    /**
+     * 快捷方法:生成请求签名(ZCTokenBo → 待签名内容 → SM2 签名)
+     * @param zcTokenBo 请求参数实体
+     * @param privateKeyBase64 开发者私钥(Base64 编码)
+     * @return 签名结果(Base64 编码,可赋值给 sign 字段)
+     * @throws JsonProcessingException JSON 转换异常
+     */
+    public static String generateRequestSign(ZCTokenBo zcTokenBo, String privateKeyBase64) throws JsonProcessingException {
+        // 生成待签名字符串
+        String signContent = getSignContent(zcTokenBo);
+        // SM2 签名并返回
+        return SM2SignatureUtils.sign(signContent, privateKeyBase64);
+    }
+
+    /**
+     * 快捷方法:验证请求签名(ZCTokenBo → 待签名内容 → 校验 sign 是否合法)
+     * @param zcTokenBo 请求参数实体
+     * @param publicKeyBase64 开发者公钥(Base64 编码)
+     * @return true=验签通过,false=验签失败
+     * @throws JsonProcessingException JSON 转换异常
+     */
+    public static boolean verifyRequestSign(ZCTokenBo zcTokenBo, String publicKeyBase64) throws JsonProcessingException {
+        // 提取请求传入的 sign
+        String requestSign = zcTokenBo.getSign();
+        if (requestSign == null || requestSign.trim().isEmpty()) {
+            return false;
+        }
+
+        // 生成待签名字符串(与签名时一致)
+        String signContent = getSignContent(zcTokenBo);
+
+        // SM2 验签
+        return SM2SignatureUtils.verify(signContent, requestSign, publicKeyBase64);
+    }
+}

+ 93 - 0
ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/util/ZCApiUtils.java

@@ -0,0 +1,93 @@
+package org.dromara.external.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Base64;
+
+/**
+ * 中车接口工具类(Base64 + JSON 转换)
+ */
+public class ZCApiUtils {
+    /**
+     * JSON 转换工具(Jackson)
+     */
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    /**
+     * Base64 解码(字符串 → 原始字符串)
+     * @param base64Str Base64编码字符串
+     * @return 解码后的原始字符串
+     */
+    public static String base64Decode(String base64Str) {
+        if (base64Str == null || base64Str.trim().isEmpty()) {
+            throw new IllegalArgumentException("Base64编码字符串不能为空");
+        }
+        byte[] decodeBytes = Base64.getDecoder().decode(base64Str);
+        return new String(decodeBytes);
+    }
+
+    /**
+     * Base64 编码(原始字符串 → Base64编码字符串)
+     * @param rawStr 原始字符串
+     * @return Base64编码字符串
+     */
+    public static String base64Encode(String rawStr) {
+        if (rawStr == null || rawStr.trim().isEmpty()) {
+            throw new IllegalArgumentException("原始字符串不能为空");
+        }
+        byte[] encodeBytes = rawStr.getBytes();
+        return Base64.getEncoder().encodeToString(encodeBytes);
+    }
+
+    /**
+     * JSON 字符串 → Java 对象
+     * @param jsonStr JSON字符串
+     * @param clazz 目标对象类型
+     * @param <T> 泛型
+     * @return 目标Java对象
+     * @throws JsonProcessingException JSON转换异常
+     */
+    public static <T> T jsonToObject(String jsonStr, Class<T> clazz) throws JsonProcessingException {
+        if (jsonStr == null || jsonStr.trim().isEmpty()) {
+            throw new IllegalArgumentException("JSON字符串不能为空");
+        }
+        return OBJECT_MAPPER.readValue(jsonStr, clazz);
+    }
+
+    /**
+     * Java 对象 → JSON 字符串
+     * @param obj Java对象
+     * @return JSON字符串
+     * @throws JsonProcessingException JSON转换异常
+     */
+    public static String objectToJson(Object obj) throws JsonProcessingException {
+        if (obj == null) {
+            throw new IllegalArgumentException("Java对象不能为空");
+        }
+        return OBJECT_MAPPER.writeValueAsString(obj);
+    }
+
+    /**
+     * 快捷方法:Java对象 → Base64编码的JSON字符串(响应data字段专用)
+     * @param obj Java对象
+     * @return Base64编码的JSON字符串
+     * @throws JsonProcessingException JSON转换异常
+     */
+    public static String objectToBase64Json(Object obj) throws JsonProcessingException {
+        String jsonStr = objectToJson(obj);
+        return base64Encode(jsonStr);
+    }
+
+    /**
+     * 快捷方法:Base64编码的JSON字符串 → Java对象(请求data字段专用)
+     * @param base64Json Base64编码的JSON字符串
+     * @param clazz 目标对象类型
+     * @param <T> 泛型
+     * @return 目标Java对象
+     * @throws JsonProcessingException JSON转换异常
+     */
+    public static <T> T base64JsonToObject(String base64Json, Class<T> clazz) throws JsonProcessingException {
+        String jsonStr = base64Decode(base64Json);
+        return jsonToObject(jsonStr, clazz);
+    }
+}

+ 131 - 0
ruoyi-modules/ruoyi-order/src/main/java/org/dromara/order/dubbo/RemoteExternalOrderServiceImpl.java

@@ -3,17 +3,36 @@ package org.dromara.order.dubbo;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
 import org.apache.dubbo.config.annotation.DubboService;
+import org.dromara.common.core.domain.zhongche.domain.DeliveryTrack;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.external.api.zhongche.domain.vo.TrackVo;
+import org.dromara.order.domain.OrderDeliver;
 import org.dromara.order.domain.OrderMain;
+import org.dromara.order.domain.OrderReturn;
 import org.dromara.order.domain.bo.OrderProductBo;
+import org.dromara.order.service.IOrderDeliverService;
 import org.dromara.order.service.IOrderMainService;
 import org.dromara.order.service.IOrderProductService;
+import org.dromara.order.service.IOrderReturnService;
+import org.dromara.order.utils.kd100.Kd100Util;
+import org.dromara.order.utils.kd100.domain.QueryTrackDTO;
+import org.dromara.order.utils.kd100.domain.TrackData;
+import org.dromara.order.utils.kd100.domain.TrackVO;
 import org.dromara.product.api.RemoteExternalOrderService;
 import org.dromara.product.api.domain.dto.OrderNoDto;
 import org.dromara.product.api.domain.dto.OrderNoticeDto;
 import org.dromara.product.api.domain.dto.OrderPushDto;
+import org.dromara.system.api.RemoteComLogisticsCompanyService;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 /**
  * @author
  * @date 2025/12/30 下午7:12
@@ -28,6 +47,20 @@ public class RemoteExternalOrderServiceImpl implements RemoteExternalOrderServic
 
      private final IOrderMainService orderMainService;
 
+     private final IOrderDeliverService orderDeliverService;
+
+     @DubboReference
+     private final RemoteComLogisticsCompanyService remoteComLogisticsCompanyService;
+
+     private final IOrderReturnService orderReturnService;
+
+    // 通用正则:匹配 快递员 + 任意分隔符(无/冒号/空格) + 姓名 + (手机号)
+    // 正则说明:
+    // 快递员      :固定匹配关键字
+    // [::\\s]*   :匹配0个或多个 全角冒号/半角冒号/任意空格(兼容无分隔符的情况)
+    // ([^()]+)    :匹配任意不含括号的字符(提取姓名,排除后面的手机号括号部分)
+    // \\(         :匹配左括号(作为姓名结束标识)
+    private static final Pattern COURIER_PATTERN = Pattern.compile("快递员[::\\s]*([^()]+)\\(");
     /**
      * 推送订单
      *
@@ -78,4 +111,102 @@ public class RemoteExternalOrderServiceImpl implements RemoteExternalOrderServic
         return orderMain.getOrderNo();
 
     }
+
+    @Override
+    public TrackVo queryLogisticsTrack(String outgoingCode, String waybillType) {
+        //目前忽略第二个参数
+        OrderDeliver one = orderDeliverService.lambdaQuery()
+            .eq(OrderDeliver::getDeliverCode, outgoingCode)
+            .last("limit 1")
+            .one();
+        // 新增:空值防护(查询不到发货单直接返回null,避免空指针)
+        if (one == null) {
+            log.warn("物流查询 - 未查询到发货单,outgoingCode:{}", outgoingCode);
+            return null;
+        }
+
+        String name = remoteComLogisticsCompanyService.selectLogisticsCompanyNameById(one.getLogisticsCompanyId());
+        List<DeliveryTrack> deliveryTracks = queryDeliverTrack(one.getLogisticsCompanyCode(), one.getLogisticNo(), one.getConsigneePhone());
+        TrackVo trackVo = new TrackVo();
+        trackVo.setExpressCompanyName(name);
+        trackVo.setDeliveryTrack(deliveryTracks);
+        trackVo.setExpressCode(one.getLogisticNo());
+        return trackVo;
+    }
+
+    @Override
+    public List<DeliveryTrack> queryDeliverTrack(String logisticsCompanyCode, String logisticNo, String consigneePhone) {
+        // 1. 参数校验
+        if (!StringUtils.isNotBlank(logisticsCompanyCode)) {
+            log.warn("物流单号不能为空,bo={}", logisticsCompanyCode);
+            throw new IllegalArgumentException("物流单号不能为空");
+        }
+        List<DeliveryTrack> deliveryTracks = new ArrayList<>();
+
+        // 2. 构建查询 DTO
+        QueryTrackDTO dto = QueryTrackDTO.builder()
+            .com(logisticsCompanyCode)
+            .num(logisticNo)
+            .phone(consigneePhone)
+            .build();
+        // 3. 调用物流查询
+        try {
+            log.info("开始查询物流信息,单号: {}, 公司代码: {}", dto.getNum(), dto.getCom());
+            TrackVO trackVO = Kd100Util.queryTrack(dto);
+            List<TrackData> data = trackVO.getData();
+            for (TrackData trackData : data) {
+                DeliveryTrack deliveryTrack = new DeliveryTrack();
+                deliveryTrack.setExpressDetail(trackData.getContext());
+                deliveryTrack.setExpressTime(trackData.getFtime());
+                //操作员怎么设置
+                // 核心:提取快递员姓名并设置到 optName
+                String courierName = extractCourierName(trackData.getContext());
+                deliveryTrack.setOptName(courierName);
+                deliveryTracks.add(deliveryTrack);
+            }
+            return deliveryTracks;
+        } catch (Exception e) {
+            log.error("查询物流信息失败,dto={}", dto, e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取退货单编号
+     * @param ZCorderNo
+     * @return
+     */
+    @Override
+    public String getReturnOrderNo(String ZCorderNo) {
+        OrderReturn one = orderReturnService.lambdaQuery()
+            .eq(OrderReturn::getReturnNo, ZCorderNo)
+            .select(OrderReturn::getReturnNo)
+            .one();
+
+        return one.getReturnNo();
+    }
+
+    /**
+     * 从物流描述中提取快递员姓名
+     * @param context 物流描述字符串
+     * @return 快递员姓名(提取不到返回 "未知")
+     */
+    private String extractCourierName(String context) {
+        // 1. 先判断上下文是否为空,避免空指针
+        if (StringUtils.isBlank(context)) {
+            return "";
+        }
+
+        // 2. 使用正则表达式匹配快递员姓名
+        Matcher matcher = COURIER_PATTERN.matcher(context);
+        if (matcher.find()) {
+            // 提取姓名并去除前后空格(处理姓名前后的冗余空格)
+            String courierName = StringUtils.trim(matcher.group(1));
+            // 避免提取到空字符串(极端情况)
+            return StringUtils.isNotBlank(courierName) ? courierName : "";
+        }
+
+        // 3. 没有匹配到 "快递员" 关键字,返回默认值
+        return "";
+    }
 }

+ 3 - 0
ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/controller/ProductCategoryController.java

@@ -59,6 +59,9 @@ public class ProductCategoryController extends BaseController {
         ProductCategoryBo bo = new ProductCategoryBo();
         bo.setClassLevel(1L);
         List<ProductCategoryVo> productCategoryVos = productCategoryService.queryList(bo);
+        if (productCategoryVos == null){
+            throw new RuntimeException("商品类目为空");
+        }
         return R.ok(productCategoryVos);
     }
 

+ 108 - 45
ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/dubbo/RemoteProductServiceImpl.java

@@ -20,24 +20,20 @@ import org.dromara.product.api.domain.zhongche.dto.StocksResult;
 import org.dromara.product.api.domain.zhongche.dto.StocksResultDto;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.product.domain.ProductWarehouseInventory;
 import org.dromara.product.domain.bo.SiteProductBo;
-import org.dromara.product.domain.vo.SiteProductVo;
+import org.dromara.product.domain.vo.*;
 import org.dromara.product.domain.ProductPriceInventory;
 import org.dromara.product.domain.bo.ProductCategoryBo;
 import org.dromara.product.api.domain.RemoteProductBrand;
 import org.dromara.product.domain.ProductBrand;
 import org.dromara.product.domain.bo.ProductBaseBo;
 import org.dromara.product.domain.bo.ProductChangeLogBo;
-import org.dromara.product.domain.vo.ProductBaseVo;
-import org.dromara.product.domain.vo.ProductCategoryVo;
-import org.dromara.product.domain.vo.ProductChangeLogVo;
-import org.dromara.product.service.IProductBaseService;
-import org.dromara.product.service.IProductCategoryService;
-import org.dromara.product.service.IProductBrandService;
-import org.dromara.product.service.IProductChangeLogService;
-import org.dromara.product.service.IProductPriceInventoryService;
+import org.dromara.product.service.*;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -62,6 +58,8 @@ public class RemoteProductServiceImpl implements RemoteProductService {
 
     private final IProductPriceInventoryService productPriceInventoryService;
 
+    private final IProductWarehouseInventoryService productWarehouseInventoryService;
+
     /**
      * 获取商品详情
      *
@@ -165,59 +163,124 @@ public class RemoteProductServiceImpl implements RemoteProductService {
 
     @Override
     public StocksResultDto queryProductStock(Map<String, Integer> goods, String areaId) {
-        List<String> goodsIds = goods.keySet().stream()
-            .collect(Collectors.toList());
+        // 1. 入参防护:避免goods为null导致后续异常
+        if (goods == null || goods.isEmpty()) {
+            return new StocksResultDto();
+        }
+
+        // 2. 提取商品ID列表,准备查询库存
+        List<String> goodsIds = goods.keySet().stream().collect(Collectors.toList());
 
-        List<ProductPriceInventory> productPriceInventories= productPriceInventoryService.queryProductStock(goodsIds);
+        // 3. 调用服务查询库存数据
+        List<ProductWarehouseInventory> productWarehouseInventories = productWarehouseInventoryService.queryProductStock(goodsIds);
 
+        // 4. 初始化返回结果(提前初始化列表,避免后续多次判断null)
         StocksResultDto stocksResultDto = new StocksResultDto();
-        productPriceInventories.forEach(productPriceInventory -> {
-            StocksResult stocksResult= new StocksResult();
-            stocksResult.setGoodsId(productPriceInventory.getProductId().toString());
-            stocksResult.setAreaId(areaId);
-            stocksResult.setStockState("1");
-            stocksResult.setStockStateDesc("有货");
-
-            //TODO 支持无库存下单商品返回(-999)
-            //当值为-1时,为未查询到。
-            //stockState=1或stockState=2时,入参goodsNum<50,该值为实际库存。
-            //入参50<=goodsNum<=100时,该值为-1。
-            //入参goodsNum>100,该值等于goodsNum。(这种情况并未返回真实库存)
-            long nowInventory   = productPriceInventory.getNowInventory()+productPriceInventory.getVirtualInventory();
-            Integer goodsNum = goods.get(productPriceInventory.getProductId().toString());
-            if (goodsNum<50){
-                stocksResult.setRemainNum((int)nowInventory);
-            }else if(goodsNum<=100){
-                stocksResult.setRemainNum(-1);
-            }else if (goodsNum>100){
-                stocksResult.setRemainNum(goodsNum);
-            }
-            //DateTime now = DateTime.now();
-            //DateTime after15Days = DateUtil.offsetDay(now, 15);
-            //stocksResult.setEstimatedShippingTime("");
-            if (stocksResultDto.getStocks() == null) {
-                stocksResultDto.setStocks(new ArrayList<>());
+
+        stocksResultDto.setStocks(new ArrayList<>());
+
+        // 5. 遍历查询结果,封装返回数据
+        productWarehouseInventories.forEach(productWarehouseInventorie -> {
+            // 5.1 提取商品核心信息
+            String productIdStr = productWarehouseInventorie.getProductId().toString();
+            Integer goodsNum = goods.get(productIdStr); // 入参的所需库存数量
+            //可用库存
+            Long nowInventory = productWarehouseInventorie.getNowInventory();
+
+            // 5.2 初始化单个商品库存结果
+            StocksResult stocksResult = new StocksResult();
+            stocksResult.setGoodsId(productIdStr);
+            stocksResult.setAreaId(areaId == null ? "" : areaId); // 地区id为空时赋值空字符串,保证格式统一
+
+            if (nowInventory > 0){
+                //有货
+                stocksResult.setStockState("1");
+                stocksResult.setStockStateDesc("下单立即发货");
+                if (goodsNum < 50){
+                    stocksResult.setRemainNum(nowInventory.intValue());
+                }else if (goodsNum <= 100){
+                    stocksResult.setRemainNum(-1);
+                }else {
+                    stocksResult.setRemainNum(goodsNum);
+                }
+            }else {
+                //无货
+                stocksResult.setStockState("5");
+                stocksResult.setStockStateDesc("无货");
+                stocksResult.setRemainNum(-999);
             }
+            // 5.6 添加到结果列表(已提前初始化,无需判断null)
             stocksResultDto.getStocks().add(stocksResult);
         });
 
+        // 6. 补充:处理「未查询到库存」的商品(即 goods 中有,但 productPriceInventories 中没有的商品)
+        // 提取已查询到的商品ID
+        Set<String> queriedGoodsIds = productWarehouseInventories.stream()
+            .map(p -> p.getProductId().toString())
+            .collect(Collectors.toSet());
+        // 遍历入参goods,补充未查询到的商品,返回 remainNum=-1
+        goods.keySet().stream()
+            .filter(goodsId -> !queriedGoodsIds.contains(goodsId))
+            .forEach(goodsId -> {
+                StocksResult noStockResult = new StocksResult();
+                noStockResult.setGoodsId(goodsId);
+                noStockResult.setAreaId(areaId == null ? "" : areaId);
+                noStockResult.setStockState(""); // 未查询到,库存状态为空
+                noStockResult.setStockStateDesc("未查询到商品库存");
+                noStockResult.setRemainNum(-1); // 文档规定:未查询到返回 -1
+                stocksResultDto.getStocks().add(noStockResult);
+            });
+
         return stocksResultDto;
     }
 
     @Override
-    public List<Prices> queryProductPrice(List<String> goodsIds) {
+    public Map<String, Prices> queryProductPrice(List<String> goodsIds) {
+        // 1. 入参防护:避免goodsIds为null/空,返回空Map
+        if (goodsIds == null || goodsIds.isEmpty()) {
+            return new HashMap<>();
+        }
         List<ProductPriceInventory> productPriceInventories= productPriceInventoryService.queryProductPrice(goodsIds);
-        return productPriceInventories.stream().map(productPriceInventory -> {
+        // 3. 初始化返回Map(key=商品sku,value=价格信息,方便快速查找)
+        Map<String, Prices> priceMap = new HashMap<>();
+
+        // 4. 遍历查询结果,封装Prices(贴合文档要求)
+        productPriceInventories.forEach(productPriceInventory -> {
+            // 4.1 提取商品sku(转换为字符串,保证与入参格式一致)
+            String goodsId = productPriceInventory.getProductId().toString();
             Prices prices = new Prices();
-            prices.setGoodsId(productPriceInventory.getProductId().toString());
-            prices.setPrice(productPriceInventory.getMemberPrice());
-            prices.setDsPrice(productPriceInventory.getMarketPrice());
+
+            // 4.2 填充必填字段
+            prices.setGoodsId(goodsId);
+
+            // 4.3 电商价格(dsPrice=市场价):处理null,保留2位小数,未查询到(此处是有数据,null则赋值-1)
+            BigDecimal marketPrice = productPriceInventory.getMarketPrice();
+
+            // 4.4 协议价格(price=会员价):处理null,保留2位小数,未查询到赋值-1
+            BigDecimal memberPrice = productPriceInventory.getMemberPrice();
+
+            // 4.5 填充非必填字段(文档允许为null/空,按当前逻辑赋值)
             prices.setTaxFreePrice(null);
             prices.setTax(productPriceInventory.getTaxRate());
             prices.setTaxCode(null);
-            return prices;
-        }).collect(Collectors.toList());
+            // 4.6 放入Map中
+            priceMap.put(goodsId, prices);
+        });
+        // 5. 补充:处理「入参有但查询结果无」的商品,按文档规则赋值-1
+        for (String goodsId : goodsIds) {
+            if (!priceMap.containsKey(goodsId)) {
+                Prices noPriceResult = new Prices();
+                noPriceResult.setGoodsId(goodsId);
+                noPriceResult.setDsPrice(new BigDecimal(-1).setScale(2, RoundingMode.HALF_UP));
+                noPriceResult.setPrice(new BigDecimal(-1).setScale(2, RoundingMode.HALF_UP));
+                noPriceResult.setTaxFreePrice(null);
+                noPriceResult.setTax(null);
+                noPriceResult.setTaxCode(null);
+                priceMap.put(goodsId, noPriceResult);
+            }
+        }
 
+        return priceMap;
     }
 
     /**

+ 2 - 0
ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/service/IProductWarehouseInventoryService.java

@@ -67,4 +67,6 @@ public interface IProductWarehouseInventoryService extends IService<ProductWareh
      * @return 是否删除成功
      */
     Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    List<ProductWarehouseInventory> queryProductStock(List<String> goodsIds);
 }

+ 6 - 0
ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/service/impl/ProductWarehouseInventoryServiceImpl.java

@@ -137,4 +137,10 @@ public class ProductWarehouseInventoryServiceImpl  extends ServiceImpl<ProductWa
         }
         return baseMapper.deleteByIds(ids) > 0;
     }
+
+    @Override
+    public List<ProductWarehouseInventory> queryProductStock(List<String> goodsIds) {
+        List<ProductWarehouseInventory> productWarehouseInventories = baseMapper.selectList(new LambdaQueryWrapper<ProductWarehouseInventory>().in(ProductWarehouseInventory::getProductId, goodsIds));
+        return productWarehouseInventories;
+    }
 }

+ 25 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/dubbo/RemoteComLogisticsCompanyServiceImpl.java

@@ -0,0 +1,25 @@
+package org.dromara.system.dubbo;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.dromara.system.api.RemoteComLogisticsCompanyService;
+import org.dromara.system.domain.ComLogisticsCompany;
+import org.dromara.system.service.IComLogisticsCompanyService;
+import org.springframework.stereotype.Service;
+
+/**
+ * author
+ * 时间:2026/2/2,19:13
+ */
+@RequiredArgsConstructor
+@Service
+@DubboService
+public class RemoteComLogisticsCompanyServiceImpl implements RemoteComLogisticsCompanyService {
+
+    private final IComLogisticsCompanyService comLogisticsCompanyService;
+    @Override
+    public String selectLogisticsCompanyNameById(Long ids) {
+        ComLogisticsCompany one = comLogisticsCompanyService.lambdaQuery().eq(ComLogisticsCompany::getId, ids).select(ComLogisticsCompany::getLogisticsName).one();
+        return one == null ? "" : one.getLogisticsName();
+    }
+}