فهرست منبع

feat(external): 新增同济推送接口并完善外部产品服务

- 新增同济推送相关控制器 TongJiController,实现品目查询、库存查询、价格查询等功能
- 添加图片链接迁移脚本 ImageUrlMigration,处理产品基础表和产品照片表的域名替换
- 更新用户子类型定义,新增企业用户和个人用户类型
- 添加产品查询类型字段,支持供应商和伙伴商查询过滤
- 修复外部产品服务中的策略调用,启用图片更新功能
- 优化协议产品池审核服务,通过协议信息获取客户名称
- 完善产品基础服务中的查询条件构建,支持基于查询类型的过滤
- 调整数据映射关系,将协议ID字段名统一为protocolId
肖路 1 هفته پیش
والد
کامیت
d496b2014a
15فایلهای تغییر یافته به همراه2035 افزوده شده و 14 حذف شده
  1. 1 1
      ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/LoginUser.java
  2. 919 0
      ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/controller/tongji/TongJiController.java
  3. 772 0
      ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/controller/tongji/TongJiPullController.java
  4. 89 0
      ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/handler/impl/TongjiPushStrategy.java
  5. 2 2
      ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/service/impl/ExternalProductServiceImpl.java
  6. 224 0
      ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/ImageUrlMigration.java
  7. 5 0
      ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/domain/bo/ProductBaseBo.java
  8. 1 1
      ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/domain/vo/ProductListVo.java
  9. 4 0
      ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/service/impl/ProductBaseServiceImpl.java
  10. 10 4
      ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/service/impl/ProductPoolAuditServiceImpl.java
  11. 1 1
      ruoyi-modules/ruoyi-product/src/main/resources/mapper/product/ProductBaseMapper.xml
  12. 1 1
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java
  13. 1 1
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java
  14. 1 1
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java
  15. 4 2
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/dubbo/RemoteUserServiceImpl.java

+ 1 - 1
ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/LoginUser.java

@@ -136,7 +136,7 @@ public class LoginUser implements Serializable {
     private String deviceType;
 
     /**
-     * 用户子类型 用户子类型(0=平台用户,1=供应商,2=伙伴商,3=商城用户)
+     * 用户子类型 用户子类型(0=平台用户,1=供应商,2=伙伴商,3=企业用户,4=个人用户)
      */
     private String userSonType;
     /**

+ 919 - 0
ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/controller/tongji/TongJiController.java

@@ -0,0 +1,919 @@
+package org.dromara.external.controller.tongji;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.codec.Base64;
+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.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+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.R;
+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.core.domain.zhongche.vo.ZhongCheTrackVo;
+import org.dromara.external.api.zhongche.domain.Catalog;
+import org.dromara.external.api.zhongche.domain.StandardCatalog;
+import org.dromara.external.api.zhongche.domain.Stocks;
+import org.dromara.external.api.zhongche.domain.ZCR;
+import org.dromara.external.api.zhongche.domain.bo.*;
+import org.dromara.external.api.zhongche.domain.vo.*;
+import org.dromara.external.controller.zhongche.handle.MallMessageDispatcher;
+import org.dromara.external.domain.ExternalItem;
+import org.dromara.external.domain.ExternalProduct;
+import org.dromara.external.domain.ExternalProductCategory;
+import org.dromara.external.service.IExternalItemService;
+import org.dromara.external.service.IExternalProductCategoryService;
+import org.dromara.external.service.IExternalProductService;
+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.*;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static org.dromara.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
+
+/**
+ * 同济推送接口
+ * 时间:2026/1/5,19:03
+ */
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/tongji/api/mall")
+public class TongJiController {
+    private final String key = GLOBAL_REDIS_KEY+"external:zhongche:token:";
+    private final String CLIENT_ID = "KFZnKGiDsJ7";
+    private final String VERSION = "1.0.0";
+
+    @DubboReference
+    private final RemoteProductService remoteProductService;
+
+    @DubboReference
+    private final RemoteExternalOrderService remoteOrderService;
+
+    private final MallMessageDispatcher mallMessageDispatcher;
+
+    private final IExternalProductCategoryService externalProductCategoryService;
+
+    private final IExternalProductService externalProductService;
+
+    private final IExternalItemService externalItemService;
+
+    //电商私钥
+    private final static String DEVELOPER_PRIVATE_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgQu0H97EPqkgz1YS5LkzZNmkG3mS5Er8rJ2LSoJtuOlGgCgYIKoEcz1UBgi2hRANCAARP6NYwTHpW2QTL8A2f2hpgunEpDVkJBhErBQPLqNS/Si5Q+9I9wUpCYdk1EvB5Hw6yzkE4bYk5IZM1j+/SnNFn";
+    //企采公钥
+    private final static String DEVELOPER_PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE9ITEKJdH9o1K9AeQYY7zNMo/q5/cdce+9jbawURTPEpBKAx4VkB+lRkb5e5YL+Be4pPM464rPvLyfqGNJvL6uQ==";
+
+
+    //测试环境
+//    private static final String DEVELOPER_PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1YybOl0QDE2e9humlm4AgI3wJ1tI+UfVRZx8kk4hfPtZjorHN8Tjq/cP07t4Yscy+R9oFci8xw0VpBbcnlaq1w=="; // 电商提供的私钥
+//
+//    private static final String DEVELOPER_PRIVATE_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgpQdXwMi21Mg1FhWad2AQLOwfNiDHgwhootau0YerQbagCgYIKoEcz1UBgi2hRANCAATVjJs6XRAMTZ72G6aWbgCAjfAnW0j5R9VFnHySTiF8+1mOisc3xOOr9w/Tu3hixzL5H2gVyLzHDRWkFtyeVqrX";  // 电商提供的公钥
+
+
+
+
+    //登录
+    @PostMapping("/login")
+    public ZCR login(@RequestBody ZCTokenBo zcTokenBo) {
+        //业务参数
+        UserLoginBo zcUserLoginBo = new UserLoginBo();
+
+        return null;
+    }
+
+    // 获取品目列表
+    @PostMapping("/auth/catalog/query")
+    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<ExternalProductCategory> externalProductCategoryList = externalProductCategoryService.lambdaQuery()
+            .select(ExternalProductCategory::getProductCategoryId)
+            .list();
+        List<Long> collect = externalProductCategoryList.stream()
+            .map(ExternalProductCategory::getProductCategoryId)
+            .collect(Collectors.toList());
+        List<Catalog> catalogVoList = remoteProductService.queryListByIds(collect)
+            .stream()
+            .map(this::convertToCatalogVo)
+            .collect(Collectors.toList());
+        StandardCatalog standardCatalog = new StandardCatalog();
+        standardCatalog.setStandardCatalogId("1374013891398471680");
+        standardCatalog.setStandardCatalogName("福利套餐");
+        catalogVoList.forEach(catalog -> catalog.setStandardCatalog(standardCatalog));
+        // 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签名,补全)
+        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);
+    }
+
+    /**
+     * 数据库实体 → 同济品目VO(ProductCategory字段)
+     */
+    private Catalog convertToCatalogVo(ProductCategoryRemoteVo category) {
+        Catalog catalog = new Catalog();
+        // 以下字段映射需根据你的实体实际字段调整
+        catalog.setId(category.getId().toString());          // 品目id
+        catalog.setPid(category.getParentId().toString());   // 父id
+        catalog.setName(category.getCategoryName());      // 品目名称
+        catalog.setLevel(category.getClassLevel().toString()); // 品目级次
+        catalog.setState(category.getIsShow() == 1 ? 1 : 0); // 状态:1有效 0无效
+        // 标准品目(仅最细级填充)
+        /*if (StrUtil.isNotBlank(category.getStandardCatalogId())) {
+            StandardCatalogVo standardVo = new StandardCatalogVo();
+            standardVo.setStandardCatalogId(category.getStandardCatalogId());
+            standardVo.setStandardCatalogName(category.getStandardCatalogName());
+            vo.setStandardCatalog(standardVo);
+        }*/
+        catalog.setStandardCatalog(null);
+        return catalog;
+    }
+
+
+
+
+    //查询商品的库存情况
+
+    /**
+     * 请求业务参数
+     * AreaStockBo areaStock = new AreaStockBo();
+     * Goods goods = new Goods();
+     *
+     * 响应业务参数
+     * Stocks stocks = new Stocks();
+     * @param zcTokenBo
+     * @return
+     */
+    @PostMapping("/auth/egoods/stock/query")
+    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不能为空");
+            }
+            areaStockBo = JSONUtil.toBean(decodedData, AreaStockBo.class);
+            if (areaStockBo == null) {
+                return ZCR.fail("5012", "业务参数data格式错误,无法解析");
+            }
+            // 3.3 业务参数必填项校验
+            if (areaStockBo.getGoods() == null || areaStockBo.getGoods().isEmpty()) {
+                return ZCR.fail("5013", "商品信息列表goods不能为空");
+            }
+            if (areaStockBo.getGoods().size() > 50) {
+                return ZCR.fail("5014", "商品信息列表goods最多支持50条");
+            }
+            // 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());
+        }
+
+        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", "接口响应签名生成失败");
+        }
+
+        // 9. 封装最终响应(符合文档要求)
+        return ZCR.ok(respDataBase64, respSign);
+    }
+
+    @GetMapping("testQueryGoodsStock")
+    public R<List<Stocks>> testQueryGoodsStock() {
+        AreaStockBo areaStockBo = new AreaStockBo();
+        areaStockBo.setAreaId("1");
+        List<Goods> goods = new ArrayList<>();
+        Goods goods1 = new Goods();
+        goods1.setGoodsId("002171765");
+        goods1.setGoodsNum(2);
+        goods.add(goods1);
+        Goods goods2 = new Goods();
+        goods2.setGoodsId("002171772");
+        goods2.setGoodsNum(2);
+        goods.add(goods2);
+        areaStockBo.setGoods(goods);
+        List<Stocks> stocks = queryGoodsStock(areaStockBo);
+        return R.ok(stocks);
+    }
+
+    /**
+     * 核心业务:查询商品库存,映射文档返回格式(包含库存状态、剩余数量等规则)
+     */
+    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=所需库存
+        }
+
+
+        // 3. 调用Dubbo服务,获取库存结果(你的remoteProductService)
+        StocksResultDto stocksResultDto = new StocksResultDto();
+        stocksResultDto.setStocks(new ArrayList<>());
+
+        // 2. 提取商品ID列表,准备查询库存
+        List<String> goodsIds = goodsMap.keySet().stream().collect(Collectors.toList());
+        ExternalItem one = externalItemService.getOne(Wrappers.lambdaQuery(ExternalItem.class).eq(ExternalItem::getItemKey, "zhongche").last("LIMIT 1"));
+        List<ExternalProduct> externalProducts = externalProductService.queryProductStock(one.getId(),goodsIds);
+        // 5. 遍历查询结果,封装返回数据
+        externalProducts.forEach(product -> {
+            // 5.1 提取商品核心信息
+            String productIdStr = product.getProductNo();
+            Integer nowInventory = product.getAvailableInventory();
+            Integer goodsNum = goodsMap.get(productIdStr); // 入参的所需库存数量
+            //可用库存
+
+
+            // 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 = externalProducts.stream()
+            .map(p -> p.getProductNo())
+            .collect(Collectors.toSet());
+        // 遍历入参goods,补充未查询到的商品,返回 remainNum=-1
+        goodsMap.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);
+            });
+
+        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 goodsStockRespList;
+    }
+
+
+    //查询商品价格
+    /*
+        //请求业务参数
+        Goods goods = new Goods();
+
+        //响应业务参数
+        PricesVo pricesVo = new PricesVo();
+     */
+    @PostMapping("/auth/egoods/price/query")
+    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);
+
+        // 5. 构造响应业务参数并Base64编码(符合文档data字段要求)
+        String respBizJson = JSONUtil.toJsonStr(pricesVo);
+        String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
+
+        // 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);
+    }
+
+    /**
+     * 核心业务:查询商品价格,映射文档返回格式
+     */
+    public 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(BigDecimal.valueOf(0.13));
+                priceResp.setTaxCode("107022301");
+                pricesVo.getPrices().add(priceResp);
+                continue;
+            }
+
+            // 3.2 填充查询到的价格信息(保证精度,最多精确到分)
+            // 电商价格(dsPrice)
+            BigDecimal dsPrice = productPrice.getDsPrice() == null ? new BigDecimal(-1) : productPrice.getDsPrice();
+            priceResp.setDsPrice(dsPrice); // 保留2位小数,四舍五入
+
+            // 协议价格(price)
+            ExternalProduct one = externalProductService.getOne(new LambdaQueryWrapper<>(ExternalProduct.class)
+                .eq(ExternalProduct::getProductNo, goodsId)
+                .last("LIMIT 1")
+            );
+            if (one == null) {
+                // 未找到协议价格,使用 Dubbo 返回的价格
+                BigDecimal protocolPrice = productPrice.getPrice() == null ? new BigDecimal(-1) : productPrice.getPrice();
+                priceResp.setPrice(protocolPrice);
+            } else {
+                // 找到协议价格,使用数据库中的协议价格
+                priceResp.setPrice(one.getExternalPrice());
+            }
+            priceResp.setTaxFreePrice(null);
+            priceResp.setTax(BigDecimal.valueOf(0.13));
+            // 税收编码(非必填)
+            priceResp.setTaxCode("107022301");
+            // 3.3 添加到响应列表
+            pricesVo.getPrices().add(priceResp);
+        }
+        return pricesVo;
+    }
+    @GetMapping("/get/queryGoodsPrice")
+    public R getMallOrderNo() {
+
+        PricesVo pricesVo = queryGoodsPrice(List.of("002171604"));
+        return R.ok(pricesVo);
+    }
+
+
+    // 4.5查询物流信息(物流信息尚未实现)
+    @PostMapping("/get/track")
+    public ZCR getTrack(@RequestBody ZCTokenBo zcTokenBo) {
+        // 1. 公共请求参数校验(含签名、版本、clientId等,复用已有逻辑)
+        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. 业务参数解析与校验(贴合文档要求)
+        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());
+        }
+
+        //******4. 核心业务:调用Dubbo服务查询物流信息   TODO我们没有换新单 所以第二个参数先抛弃掉
+        ZhongCheTrackVo 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);
+
+        // 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);
+    }
+
+    //4.6 查询电商平台订单号
+    @PostMapping("/get/mallOrderNo")
+    public ZCR getMallOrderNo(@RequestBody ZCTokenBo zcTokenBo) {
+        //1. 公共请求参数校验
+        ZCR checkResult = checkPublicParams(zcTokenBo);
+        if (!"0".equals(checkResult.getRespCode())) {
+            return ZCR.fail(checkResult.getRespCode(), checkResult.getRespMsg());
+        }
+        // 2. 请求签名校验(必须)
+        boolean verifyResult;
+        try {
+            verifyResult = SignParamUtils.verifyRequestSign(
+                zcTokenBo,
+                DEVELOPER_PUBLIC_KEY   // 同济给你的公钥
+            );
+        } catch (Exception e) {
+            log.error("查询电商订单号 - 请求验签异常", e);
+            return ZCR.fail("5005", "请求签名校验异常");
+        }
+
+        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;
+        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. 查询电商平台订单号(你的内部逻辑)
+        String orderNo = orderNoQueryBo.getOrderNo();
+        String mallOrderNo = remoteOrderService.getOrderNo(orderNo);
+        if (!ObjectUtil.isNotEmpty(mallOrderNo)){
+            return ZCR.fail("5010", "查询电商订单号失败");
+        }
+        // 6. 构造业务响应 data(VO → JSON → Base64)
+        MallOrderNoVo orderNoVo = new MallOrderNoVo();
+        orderNoVo.setMallOrderNo(mallOrderNo);
+
+        String respBizJson = JSONUtil.toJsonStr(orderNoVo);
+        String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
+
+        // 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", "接口响应签名生成失败");
+        }
+
+        // 9. 回填 sign 并返回
+        resp.setSign(respSign);
+        return resp;
+    }
+
+    //4.7 查询电商平台售后单号
+    @PostMapping("/get/mallAfterSaleNo")
+    public ZCR getMallAfterSaleNo(@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. 业务参数解析(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 returnOrderNo = remoteOrderService.getReturnOrderNo(afterSaleNo);
+        if (StrUtil.isBlank(returnOrderNo)) {
+            return ZCR.fail("5004", "未查询到对应的电商平台售后单号");
+        }
+
+        // 6. 构造响应业务参数
+        MallAfterSaleNoVo vo = new MallAfterSaleNoVo();
+        vo.setMallAfterSaleNo(returnOrderNo);
+
+        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);
+    }
+
+    //4.8 消息监听
+    @PostMapping("/message/listening")
+    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. 按消息类型处理业务
+        MessageVo messageVo;
+        try {
+            messageVo = mallMessageDispatcher.dispatch(messageBo);
+        } catch (Exception e) {
+            log.error("消息处理异常,messageId={}", messageBo.getId(), e);
+            return ZCR.fail("5007", "消息处理失败");
+        }
+        // 防御式兜底
+        if (messageVo == null) {
+            messageVo = new MessageVo("0", "消息处理无返回结果");
+        }
+
+        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);
+    }
+
+    //4.9 非必接模块接入校验
+    @PostMapping("/notRequireApi/check")
+    public ZCR notRequireApiCheck(@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. 业务参数解析
+        ModuleCheckBo moduleCheckBo;
+        try {
+            String decodedData = Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
+            moduleCheckBo = JSONUtil.toBean(decodedData, ModuleCheckBo.class);
+        } catch (Exception e) {
+            log.error("非必接模块校验 - 业务参数解析失败", e);
+            return ZCR.fail("5007", "业务参数解析失败");
+        }
+        // 4. 业务参数必填校验
+        if (moduleCheckBo == null
+            || StrUtil.isBlank(moduleCheckBo.getAccount())
+            || StrUtil.isBlank(moduleCheckBo.getModuleType())) {
+            return ZCR.fail("5007", "模块校验业务参数不完整");
+        }
+
+        // 5. 核心业务逻辑
+        // 当前阶段:所有非必接模块【均未接入】
+        CheckResultVo checkResultVo = new CheckResultVo();
+        checkResultVo.setCheckResult("1"); // 0 = 未接入
+
+        // 6. 业务响应转 Base64
+        String respBizJson = JSONUtil.toJsonStr(checkResultVo);
+        String respDataBase64 = Base64.encode(respBizJson, StandardCharsets.UTF_8);
+
+        // 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);
+    }
+
+    //TODO 4.10 	查询运费
+    @PostMapping("/auth/egoods/getfreight")
+    public ZCR getFreight(@RequestBody ZCTokenBo zcTokenBo) {
+        //请求业务参数
+        AreaStockBo areaStock = new AreaStockBo();
+
+        //响应业务参数
+        FreightVo freightVo = new FreightVo();
+
+        return null;
+    }
+
+
+    /**
+     * 校验公共请求参数(基于你的ZCTokenBo)
+     */
+    private ZCR checkPublicParams(ZCTokenBo zcTokenBo) {
+        // 1. 必填项非空校验(严格按文档4.2.4)
+        if (StrUtil.isBlank(zcTokenBo.getVersion())) {
+            return ZCR.fail("5007", "接口版本version不能为空");
+        }
+        if (StrUtil.isBlank(zcTokenBo.getTimestamp())) {
+            return ZCR.fail("5007", "时间戳timestamp不能为空(格式:YYYYMMDDHHMMSS)");
+        }
+        if (StrUtil.isBlank(zcTokenBo.getClientId())) {
+            return ZCR.fail("5007", "开发者id clientId不能为空");
+        }
+        if (StrUtil.isBlank(zcTokenBo.getData())) {
+            return ZCR.fail("5007", "业务参数data不能为空");
+        }
+        if (StrUtil.isBlank(zcTokenBo.getSign())) {
+            return ZCR.fail("5007", "签名sign不能为空");
+        }
+
+        // 2.版本校验(固定1.0.0)
+        if (!VERSION.equals(zcTokenBo.getVersion())) {
+            return ZCR.fail("1008", "仅支持接口版本1.0.0");
+        }
+
+        // 3. 时间戳格式校验(14位)
+        if (!ReUtil.isMatch("\\d{14}", zcTokenBo.getTimestamp())) {
+            return ZCR.fail("5007", "时间戳格式错误,需为14位数字YYYYMMDDHHMMSS");
+        }
+        // 4. clientId 校验
+        if (!CLIENT_ID.equals(zcTokenBo.getClientId())){
+            return ZCR.fail("5007", "clientId错误");
+        }
+
+        //
+        // 5. 签名校验
+        try {
+            // 防御性 Base64 校验
+//            Base64.decodeStr(zcTokenBo.getData(), StandardCharsets.UTF_8);
+            // 调用 SignParamUtils 验证请求签名,直接复用之前的逻辑
+            boolean signValid = SignParamUtils.verifyRequestSign(zcTokenBo, DEVELOPER_PUBLIC_KEY);
+            if (!signValid) {
+                // 验签失败,返回错误响应
+                return ZCR.fail("5007", "接口签名错误(验签失败)");
+            }
+        } catch (Exception e) {
+            // 捕获验签过程中的异常(如JSON转换、Base64解码、SM2算法异常等)
+            log.error("同济电商接口签名校验异常,请求参数:{}", JSONUtil.toJsonStr(zcTokenBo), e);
+            return ZCR.fail("5007", "接口签名校验异常,请稍后重试");
+        }
+
+        // 校验通过
+        return ZCR.ok(null);
+    }
+
+
+
+    /**
+     * 校验AccessToken有效性
+     */
+    private ZCR checkAccessTokenValid(String accessToken) {
+        if(ObjectUtil.isEmpty(accessToken)){
+            return ZCR.fail("5006","token不能为空");
+        }
+        System.out.println(StpUtil.getTokenTimeout(accessToken));
+        if(StpUtil.getTokenTimeout(accessToken) == -2l){
+            return ZCR.fail("5005","token_expired");
+        }
+        return ZCR.ok( null);
+    }
+
+    public static void main(String[] args) throws JsonProcessingException {
+        // 1️⃣ 构造请求对象(和你 controller 里的一模一样)
+        ZCTokenBo bo = new ZCTokenBo();
+        bo.setVersion("1.0.0");
+        bo.setTimestamp("20260326160413");
+        bo.setClientId("KFZAVuIyC56");
+        bo.setData("eyJpZCI6IjM1ODdjOGRhLTAxMWItNGUyOS1iYmQzLTIzODA0MDYyZDc1NyIsInR5cGUiOiIyMjAxIiwiY29udGVudCI6eyJ1c2VybmFtZSI6IjIwMjQwMzE2MDAxIn0sInRpbWUiOiIyMDI2MDMyNjE2MDQxMzU3NyJ9");
+        bo.setAccessToken( "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJzeXNfdXNlcjoyMDM3MDc2OTc3MjQ2MzE0NDk3Iiwicm5TdHIiOiJVTm84azZISzFLQXZ0eXJZOG9KZUtZV0dQbHd1MnlMZCIsImNsaWVudGlkIjoiZTVjZDdlNDg5MWJmOTVkMWQxOTIwNmNlMjRhN2IzMmUiLCJ0ZW5hbnRJZCI6IjAwMDAwMCIsInVzZXJJZCI6MjAzNzA3Njk3NzI0NjMxNDQ5NywidXNlck5hbWUiOiIyMDI0MDMxNjAwMSIsImRlcHRJZCI6MTk5NjgyODcyMjQwMDg5OTA3NCwiZGVwdE5hbWUiOiJBUEnlr7nmjqXnrqHnkIblubPlj7AiLCJkZXB0Q2F0ZWdvcnkiOiIifQ.BSrQtYQQIcy2chvcza4qVjj7x6wXw2OA5zAJb0vI-rk");
+        // 注意:此时不要 setSign
+
+        // 2️⃣ 打印待签名内容(用于和同济排查)
+        String signContent = SignParamUtils.getSignContent(bo);
+        System.out.println("待签名字符串:");
+        System.out.println(signContent);
+
+        // 3️⃣ 生成 SM2 签名
+        String sign = SignParamUtils.generateRequestSign(bo, DEVELOPER_PRIVATE_KEY);
+
+        // 4️⃣ 输出 sign
+        System.out.println("生成的 sign:");
+        System.out.println(sign);
+    }
+
+}

+ 772 - 0
ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/controller/tongji/TongJiPullController.java

@@ -0,0 +1,772 @@
+package org.dromara.external.controller.tongji;
+
+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.common.core.domain.zhongche.aftersale.bo.*;
+import org.dromara.common.core.domain.zhongche.aftersale.vo.*;
+import org.dromara.common.core.domain.zhongche.vo.OrderDetailVo;
+import org.dromara.common.core.domain.zhongche.vo.PrepareOrderDetailRespVo;
+import org.dromara.common.core.exception.api.ZhongcheException;
+import org.dromara.external.api.zhongche.domain.ZCR;
+import org.dromara.external.api.zhongche.domain.bo.*;
+import org.dromara.external.api.zhongche.domain.invoice.bo.*;
+import org.dromara.external.api.zhongche.domain.invoice.vo.InvoiceApplyDetailVo;
+import org.dromara.external.api.zhongche.domain.invoice.vo.InvoiceApplyOrdersVo;
+import org.dromara.external.api.zhongche.domain.invoice.vo.InvoiceApplyWaitlistVo;
+import org.dromara.external.api.zhongche.domain.settlement.bo.*;
+import org.dromara.external.api.zhongche.domain.settlement.vo.SettlementApplyOrdersVo;
+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.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;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * 同济拉取接口
+ * 时间:2026/1/5,19:03
+ */
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/tongji/api")
+public class TongJiPullController {
+
+    //正式环境
+    // 同济地区查询接口地址(替换为真实域名)
+    private static final String AREA_QUERY_URL = "https://supply.crrcgo.cc/mallapi";
+    // 同济提供的配置(替换为真实值)
+    private static final String CLIENT_ID = "KFZnKGiDsJ7";
+    private static final String PRIVATE_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgQu0H97EPqkgz1YS5LkzZNmkG3mS5Er8rJ2LSoJtuOlGgCgYIKoEcz1UBgi2hRANCAARP6NYwTHpW2QTL8A2f2hpgunEpDVkJBhErBQPLqNS/Si5Q+9I9wUpCYdk1EvB5Hw6yzkE4bYk5IZM1j+/SnNFn"; // 电商平台私钥
+    private static final String ZC_PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE9ITEKJdH9o1K9AeQYY7zNMo/q5/cdce+9jbawURTPEpBKAx4VkB+lRkb5e5YL+Be4pPM464rPvLyfqGNJvL6uQ=="; // 同济公钥
+    //测试环境
+    // 同济地区查询接口地址(替换为真实域名)
+//    private static final String AREA_QUERY_URL = "https://supply-test.crrcgo.cc/mallapi/";
+//    // 同济提供的配置(替换为真实值)
+//    private static final String CLIENT_ID = "KFZAVuIyC56";
+//    private static final String PRIVATE_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgpQdXwMi21Mg1FhWad2AQLOwfNiDHgwhootau0YerQbagCgYIKoEcz1UBgi2hRANCAATVjJs6XRAMTZ72G6aWbgCAjfAnW0j5R9VFnHySTiF8+1mOisc3xOOr9w/Tu3hixzL5H2gVyLzHDRWkFtyeVqrX"; // 电商平台私钥
+//    private static final String ZC_PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1YybOl0QDE2e9humlm4AgI3wJ1tI+UfVRZx8kk4hfPtZjorHN8Tjq/cP07t4Yscy+R9oFci8xw0VpBbcnlaq1w=="; // 同济公钥
+    //企采公钥
+    private static final String VERSION = "1.0.0";
+
+
+    //5	同济电子商城提供服务
+    //5.1	基础数据服务
+
+    //5.1.1	地区查询
+    @PostMapping("/area/query")
+    public AreaVo areaQuery(@RequestBody AreaQueryBo bo) {
+        // 1. 校验业务请求参数(自身先校验,避免无效调用电商平台)
+        //1 省级,2市级,3县级, 4区级
+        //父级地址id	当查询省级时填0
+        if (bo.getLevel() == null || !List.of(1, 2, 3, 4).contains(bo.getLevel())) {
+            throw new RuntimeException("级次(level)必填,且仅支持1/2/3/4");
+        }
+        if (StrUtil.isBlank(bo.getPid())) {
+            throw new RuntimeException("父级地址id(pid)不能为空");
+        }
+        //获取response.body
+        ZCR responseDto = doZcPost("/api/area/query", bo);
+        // 4. 复用工具类解析响应
+        return parseZcResponse(responseDto, AreaVo.class);
+    }
+
+    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;
+        }
+    }
+
+
+    private <B> ZCR doZcPost(String apiPath, B bo) {
+        // 1.BO → JSON
+        String bizJson;
+        try {
+            bizJson = ZCApiUtils.objectToJson(bo);
+        } catch (JsonProcessingException e) {
+            return ZCR.fail("5000", "业务参数转换失败:" + e.getMessage());
+        }
+
+        // 2. JSON→ Base64
+        String dataBase64;
+        try {
+            // 注意:你的 ZCApiUtils.base64Encode 内部用 getBytes(),默认是系统编码,补充 UTF-8 编码保证一致性
+            dataBase64 = ZCApiUtils.base64Encode(bizJson);
+        } catch (IllegalArgumentException e) {
+            log.error("通用请求 - 业务 JSON Base64 编码失败", e);
+            return ZCR.fail("5000", "业务参数编码失败:" + e.getMessage());
+        }
+
+        //3. 构建 ZCTokenBo 请求体
+        ZCTokenBo zcTokenBo = getZcTokenBo(dataBase64);
+        // 4. 生成请求签名
+        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 字符串
+        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 = HttpRequest
+            .post(fullUrl)
+            .charset(StandardCharsets.UTF_8)
+            .contentType("application/json")
+            .body(requestJson)
+            .timeout(60000)
+            .execute();
+
+        // 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 responseDto;
+        try {
+            responseDto = ZCApiUtils.jsonToObject(responseBody, ZCR.class);
+        } catch (JsonProcessingException e) {
+            log.error("通用请求 - 响应体转换 ZCR 失败", e);
+            return ZCR.fail("5000", "响应体解析失败:" + e.getMessage());
+        }
+
+        // 9. 响应码校验
+        if (!"0".equals(responseDto.getRespCode())) {
+            return ZCR.fail(responseDto.getRespCode(), responseDto.getRespMsg());
+        }
+
+        // 10. 响应签名校验(同济强要求)
+        if (!verifyResponseSign(responseDto)) {
+            log.error("通用请求 - 响应签名校验失败,apiPath={}", apiPath);
+            return ZCR.fail("5001", "响应签名验证失败");
+        }
+
+        return responseDto;
+    }
+    private ZCTokenBo getZcTokenBo(String data) {
+        ZCTokenBo zcTokenBo = new ZCTokenBo();
+        zcTokenBo.setVersion(VERSION);
+        zcTokenBo.setTimestamp(DateUtil.format(DateUtil.date(), "yyyyMMddHHmmss"));
+        zcTokenBo.setClientId(CLIENT_ID);
+        zcTokenBo.setData(data);
+        zcTokenBo.setSign(null);
+        return zcTokenBo;
+    }
+
+
+    //ZhongCheOrderDeliverVo(outgoingCode=1471093529601249280)
+    //查询发货单详情
+    /*public static void main(String[] args) {
+        ZhongChePullController zhongChePullController = new ZhongChePullController();
+        OutgoingQueryBo queryBo = new OutgoingQueryBo();
+        queryBo.setOrderNo("20260210110174131");
+        OutgoingVo outgoingVo = zhongChePullController.mallOrderQueryOutgoing(queryBo);
+        System.out.println(outgoingVo);
+        //OutgoingVo(outgoingList=[OutgoingList(orderNo=20260210110174131, outgoingCode=1470843870811131904, outgoingStatus=0, deliveryType=1, expressCode=123456, expressCompanyName=韵达, outgoingGoods=[OutgoingGoods(goodsId=362032, num=7.0000)])])
+    }*/
+
+//OrderDetailVo(orderNo=20260211110174132, purchaserOrderNo=2026021110452506, preOrder=0, mallOrderNo=null, orderStatus=1, orderAmount=205.24, freight=0.00, orderGoods=[OrderGoodsItem(goodsId=362032, num=7.0000, price=29.3200, subAmount=205.24, wishDeliveryDate=null)], name=111, provinceId=1, cityId=2800, countyId=55835, townId=0, address=同济智程, zip=null, mobile=13381115928, phone=null, email=service_yg@bosssoft.com.cn, memo=, orderTime=2026-02-11 10:17:40, buyer=常恒瑀, buyerMobile=18801195315, purchaserName=中国同济, purchaserDeptName=中国同济-同济科技园发展有限公司-同济智程文化科技(北京)有限公司, paymentType=01, orderInvoice=ZhongCheOrderInvoice(invoiceType=2, selectedInvoiceTitle=5, companyName=同济智程文化科技(北京), invoiceContent=1, invoiceName=王琳, invoicePhone=15249101907, invoiceFullAddress=北京海淀区羊坊店街道北京市海淀区羊坊店路11号, regCompanyName=同济智程文化科技(北京), regCode=91110108MAC4385K6J, regAddr=北京市海淀区羊坊店路11号四层428室测试, regPhone=010-51891021, regBank=中国工商银行股份有限公司北京玉渊潭支行测试, regBankAccount=111111111), voucher=0, payClient=0, paySerialNum=null, payAccount=null, payMoney=null, payPrestore=null, files=null)
+
+    //他们售后发货了
+//{"id":"d0094ec3-e032-43df-b95a-7cfb4ac91261","type":"2103","content":{"afterSaleNo":"1471137739549315072","expressCode":"21312321","expressCompanyName":"顺丰快递","freight":0,"deliverTime":"20260211135600"},"time":"20260211135601149","protocolId":null}
+//{"id":"d0094ec3-e032-43df-b95a-7cfb4ac91261","type":"2001","content":{"orderNo":"20260210110174130"},"time":"20260211135601149"}
+//{"id":"d0094ec3-e032-43df-b95a-7cfb4ac91261","type":"2101","content":{"afterSaleNo":"1471137739549315072"},"time":"20260211135601149"}
+//{"id":"d0094ec3-e032-43df-b95a-7cfb4ac91261","type":"2002","content":{"orderNo":"20260210110174130","cancelReason":"不想要了"},"time":"20260211135601149"}
+//{"id":"d0094ec3-e032-43df-b95a-7cfb4ac91261","type":"2003","content":{"orderNo":"20260210110174131","outgoingCode":"123456789"},"time":"20260211135601149"}
+//{"outgoingCode":"123456789","waybillType":"1"}
+
+    //查询订单详情
+    /*public static void main(String[] args) {
+        OrderDetailBo bo = new OrderDetailBo();
+        bo.setOrderNo("20260210110174131");
+        ZhongChePullController zhongChePullController = new ZhongChePullController();
+        OrderDetailVo orderDetailVo =  zhongChePullController.mallOrderDetail(bo);
+        System.out.println(orderDetailVo);
+        //待签名字符串:{"clientId":"KFZAVuIyC56","data":"eyJvcmRlck5vIjoiMjAyNjAyMTAxMTAxNzQxMzAifQ==","timestamp":"20260210174806","version":"1.0.0"}
+        //OrderDetailVo(orderNo=20260210110174130, purchaserOrderNo=2026021010452504, preOrder=0, mallOrderNo=null, orderStatus=1, orderAmount=205.24, freight=0.00, orderGoods=[OrderGoodsItem(goodsId=362032, num=7.0000, price=29.3200, subAmount=205.24, wishDeliveryDate=null)], name=111, provinceId=1, cityId=2800, countyId=55835, townId=0, address=同济智程, zip=null, mobile=13381115928, phone=null, email=service_yg@bosssoft.com.cn, memo=, orderTime=2026-02-10 17:25:18, buyer=常恒瑀, buyerMobile=18801195315, purchaserName=中国同济, purchaserDeptName=中国同济-同济科技园发展有限公司-同济智程文化科技(北京)有限公司, paymentType=01, orderInvoice=ZhongCheOrderInvoice(invoiceType=2, selectedInvoiceTitle=5, companyName=同济智程文化科技(北京), invoiceContent=1, invoiceName=王琳, invoicePhone=15249101907, invoiceFullAddress=北京海淀区羊坊店街道北京市海淀区羊坊店路11号, regCompanyName=同济智程文化科技(北京), regCode=91110108MAC4385K6J, regAddr=北京市海淀区羊坊店路11号四层428室测试, regPhone=010-51891021, regBank=中国工商银行股份有限公司北京玉渊潭支行测试, regBankAccount=111111111), voucher=0, payClient=0, paySerialNum=null, payAccount=null, payMoney=null, payPrestore=null, files=null)
+    }*/
+
+    //导入商品
+    /*public static void main(String[] args) {
+        String username = "admin";
+        List<GoodsImportItem> batchGoods = new ArrayList<>();
+        GoodsImportItem item = new GoodsImportItem();
+        item.setGoodsId("362031");
+        item.setCatalogId("13012");
+        item.setCatalogName("原装墨盒");
+        item.setStandardCatalogId("1750717233748832257");
+        item.setStandardCatalogName("铁圈装订机耗材");
+        item.setBrandName("得力");
+        item.setName("得力");
+        item.setDsPrice(new BigDecimal(41));
+        item.setPrice(new BigDecimal(34));
+        item.setUnit("个");
+        item.setStock(100);
+        item.setBarImgUrls("[\"https://img1.com\",\"https://img2.com\"]");
+        item.setDescription("得力商品");
+        item.setProperties("{\"颜色\":\"红色\",\"尺寸\":\"XL\",\"材质\":\"棉|涤纶\"}");
+        item.setIsSelfOperated(1);
+        item.setTax(new BigDecimal(0.05));
+        item.setTaxCode("123456");
+        batchGoods.add(item);
+        GoodsImportBo bo = new GoodsImportBo();
+        bo.setAccount(username);
+        bo.setGoods(batchGoods);
+        ZhongChePullController zhongChePullController = new ZhongChePullController();
+        GoodsImportVo resp = zhongChePullController.egoodsImport(bo);
+        System.out.println(resp);
+    }*/
+
+    //发货
+    /*public static void main(String[] args) {
+        ZhongCheOrderDeliverBo bo = new ZhongCheOrderDeliverBo();
+        bo.setOrderNo("20260211110174132");
+        bo.setDeliveryType("1");
+        bo.setExpressCode("434848895994511");
+        bo.setExpressCompanyName("韵达");
+        List<OrderDeliverGoods> orderDeliverGoods1 = new ArrayList<>();
+        OrderDeliverGoods orderDeliverGoods = new OrderDeliverGoods();
+        orderDeliverGoods.setGoodsId("362032");
+        orderDeliverGoods.setNum(new BigDecimal(7.0000));
+        orderDeliverGoods.setVoucher("[\"imgUrl1\",\"imgUrl2\"]");
+        orderDeliverGoods1.add(orderDeliverGoods);
+        bo.setOrderDeliverGoods(orderDeliverGoods1);
+        ZhongChePullController zhongChePullController = new ZhongChePullController();
+        ZhongCheOrderDeliverVo vo =  zhongChePullController.mallOrderDeliverGoods(bo);
+        System.out.println(vo);
+    }*/
+
+    //接单
+    /*public static void main(String[] args) {
+        OrderConfirmBo bo = new OrderConfirmBo();
+        bo.setOrderNo("20260211110174132");
+        bo.setMallOrderNo("RS20260002");
+        ZhongChePullController zhongChePullController = new ZhongChePullController();
+        GoodsUpdateVo goodsUpdateVo =  zhongChePullController.mallOrderConfirm(bo);
+        System.out.println(goodsUpdateVo);
+        //待签名字符串:{"clientId":"KFZAVuIyC56","data":"eyJvcmRlck5vIjoiMjAyNjAyMTAxMTAxNzQxMzEiLCJtYWxsT3JkZXJObyI6IlJTMjAyNjAwMDEifQ==","timestamp":"20260210175937","version":"1.0.0"}
+        //GoodsUpdateVo(result=1, message=null)
+    }*/
+
+    //查询售后单详情
+    /*public static void main(String[] args) {
+        AfterSaleDetailBo bo = new AfterSaleDetailBo();
+        bo.setAfterSaleNo("1471151366025121792");
+        ZhongChePullController zhongChePullController = new ZhongChePullController();
+        AfterSaleDetailVo vo =zhongChePullController.mallAftersaleDetail(bo);
+        System.out.println(vo);
+        //仅退款
+        //AfterSaleDetailVo(afterSaleNo=1471151366025121792, purchaserAfterSaleNo=1471151366243225600, mallAfterSaleNo=null, orderNo=20260211110174132, afterSaleType=4, memo=null, afterSaleStatus=10, goodsReturnStatus=1, applyTime=20260211143018, afterSaleReason=AfterSaleReason(reasonTypeName=收货商品与商品描述不符, reasonTypeCode=1, reason=2312321, imageList=[]), afterSaleGoods=AfterSaleGoods(goodsId=362032, num=1.0000, needDetectionReport=null, hasPackage=null, packageDesc=null), afterSalePick=null, afterSaleReturn=null, refundAccount=null, refundMoney=null, refundGoodsStatus=1, refundPrestore=null)
+        //换货
+        //AfterSaleDetailVo(afterSaleNo=1471137739549315072, purchaserAfterSaleNo=1471137739675144192, mallAfterSaleNo=null, orderNo=20260211110174132, afterSaleType=1, memo=null, afterSaleStatus=10, goodsReturnStatus=1, applyTime=20260211133610, afterSaleReason=AfterSaleReason(reasonTypeName=错发, reasonTypeCode=1, reason=1111, imageList=[]), afterSaleGoods=AfterSaleGoods(goodsId=362032, num=1.0000, needDetectionReport=0, hasPackage=1, packageDesc=10), afterSalePick=AfterSalePick(pickType=3, name=null, provinceId=null, cityId=null, countyId=null, townId=0, address=null, zip=null, mobile=null, phone=null, email=null), afterSaleReturn=null, refundAccount=null, refundMoney=null, refundGoodsStatus=null, refundPrestore=null)
+    }*/
+
+    //同意售后
+    /*public static void main(String[] args) {
+        AfterSaleConfirmBo bo = new AfterSaleConfirmBo();
+        bo.setAfterSaleNo("1471151366025121792");
+        bo.setMallAfterSaleNo("RT202602110003");
+        //bo.setName("李景阳");
+        //bo.setProvinceId("1");
+        //bo.setCityId("2800");
+        //bo.setCountyId("55835");
+        //bo.setAddress("湖北省武汉市武昌区中南国际城C12207");
+        //bo.setZip("430000");
+        //bo.setMobile("18327041620");
+        //bo.setPhone("18327041620");
+        //bo.setEmail("18327041620@163.com");
+        ZhongChePullController zhongChePullController = new ZhongChePullController();
+        GoodsUpdateVo vo =zhongChePullController.mallAftersaleConfirm(bo);
+        System.out.println(vo);
+    }*/
+
+    //换货
+    //待签名字符串:{"clientId":"KFZAVuIyC56","data":"eyJhZnRlclNhbGVObyI6IjE0NzExNDUwNTAwMjgyNDkwODgifQ==","timestamp":"20260211140847","version":"1.0.0"}
+    //AfterSaleDetailVo(afterSaleNo=1471145050028249088, purchaserAfterSaleNo=1471145050934218752, mallAfterSaleNo=null, orderNo=20260211110174132, afterSaleType=2, memo=null, afterSaleStatus=10, goodsReturnStatus=1, applyTime=20260211140512, afterSaleReason=AfterSaleReason(reasonTypeName=错发, reasonTypeCode=1, reason=123123, imageList=[]), afterSaleGoods=AfterSaleGoods(goodsId=362032, num=1.0000, needDetectionReport=0, hasPackage=1, packageDesc=10), afterSalePick=AfterSalePick(pickType=1, name=111, provinceId=1, cityId=2800, countyId=55835, townId=0, address=同济智程, zip=null, mobile=13381115928, phone=null, email=null), afterSaleReturn=AfterSaleReturn(name=111, provinceId=1, cityId=2800, countyId=55835, townId=null, address=同济智程, zip=null, mobile=13381115928, phone=null, email=null), refundAccount=null, refundMoney=null, refundGoodsStatus=null, refundPrestore=null)
+
+    //收到商品确认
+    /*public static void main(String[] args) {
+        AfterSaleReturnReceivedBo bo = new AfterSaleReturnReceivedBo();
+        bo.setAfterSaleNo("1471145050028249088");
+        ZhongChePullController zhongChePullController = new ZhongChePullController();
+        GoodsUpdateVo vo =zhongChePullController. mallAftersaleReturnGoodsReceived(bo);
+        System.out.println(vo);
+    }*/
+
+    //换货新品发货
+    /*public static void main(String[] args) {
+        AfterSaleDeliverGoodsBo bo = new AfterSaleDeliverGoodsBo();
+        bo.setAfterSaleNo("1471145050028249088");
+        bo.setDeliveryType("1");
+        bo.setExpressCode("434848895994511");
+        bo.setExpressCompanyName("韵达");
+        List<AfterSaleDeliverGoodsItem> afterSaleDeliverGoods = new ArrayList<>();
+        AfterSaleDeliverGoodsItem afterSaleDeliverGoodsItem = new AfterSaleDeliverGoodsItem();
+        afterSaleDeliverGoodsItem.setGoodsId("362032");
+        afterSaleDeliverGoodsItem.setNum(new BigDecimal(1.0000));
+        afterSaleDeliverGoods.add(afterSaleDeliverGoodsItem);
+        bo.setAfterSaleDeliverGoods(afterSaleDeliverGoods);
+        ZhongChePullController zhongChePullController = new ZhongChePullController();
+        AfterSaleDeliverGoodsVo vo =  zhongChePullController.mallAftersaleDeliverGoods(bo);
+        System.out.println(vo);
+        //待签名字符串:{"clientId":"KFZAVuIyC56","data":"eyJhZnRlclNhbGVObyI6IjE0NzExNDUwNTAwMjgyNDkwODgiLCJkZWxpdmVyeVR5cGUiOiIxIiwiZXhwcmVzc0NvZGUiOiI0MzQ4NDg4OTU5OTQ1MTEiLCJleHByZXNzQ29tcGFueU5hbWUiOiLpn7Xovr4iLCJhZnRlclNhbGVEZWxpdmVyR29vZHMiOlt7Imdvb2RzSWQiOiIzNjIwMzIiLCJudW0iOjF9XX0=","timestamp":"20260211142325","version":"1.0.0"}
+        //AfterSaleDeliverGoodsVo(outgoingCode=1471149642388475904)
+    }*/
+
+    //确认退款
+    /*public static void main(String[] args) {
+        AfterSaleRefundBo bo = new AfterSaleRefundBo();
+        bo.setAfterSaleNo("1471151366025121792");
+        ZhongChePullController zhongChePullController = new ZhongChePullController();
+        GoodsUpdateVo vo = zhongChePullController.mallAftersaleRefund(bo);
+        System.out.println(vo);
+    }*/
+    //5.2.1	商品导入
+    @PostMapping("/egoods/import")
+    public GoodsImportVo egoodsImport(@RequestBody GoodsImportBo bo) throws ZhongcheException {
+        ZCR responseDto = doZcPost("/api/egoods/import", bo);
+        log.info(JSONUtil.toJsonStr(bo));
+        //解析业务响应参数
+        GoodsImportVo zcr = parseZcResponse(responseDto, GoodsImportVo.class);
+        return zcr;
+    }
+    private <V> V parseZcResponse(ZCR responseDto, Class<V> voClass) throws ZhongcheException {
+        // 1. 基础校验
+        if (responseDto == null) {
+            throw new ZhongcheException("同济响应为空");
+        }
+
+        if (!"0".equals(responseDto.getRespCode())) {
+            throw new ZhongcheException("同济接口返回失败:" + responseDto.getRespCode() + " - " + responseDto.getRespMsg());
+        }
+
+        if (StrUtil.isBlank(responseDto.getData())) {
+            throw new ZhongcheException("同济响应 data 为空");
+        }
+        try {
+            // 2. Base64 → JSON
+            String bizJson = ZCApiUtils.base64Decode(responseDto.getData());
+
+            // 3. JSON → VO(Jackson 自动处理嵌套对象 / List)
+//            return ZCApiUtils.jsonToObject(bizJson, voClass);
+            return JSONUtil.toBean(bizJson, voClass);
+        } catch (Exception e) {
+            log.error("解析同济响应失败,response={}", responseDto, e);
+            throw new ZhongcheException("同济响应解析失败 : " + e.getMessage());
+        }
+    }
+
+
+    //TODO 5.2.2	商品价格变更
+    @PostMapping("/egoods/price/update")
+    public GoodsPriceUpdateVo egoodsPriceUpdate(@RequestBody GoodsImportBo bo) {
+        ZCR responseDto = doZcPost("/api/egoods/price/update", bo);
+        log.info("商品价格变更参数:{}",JSONUtil.toJsonStr(bo));
+        GoodsPriceUpdateVo zcr = parseZcResponse(responseDto, GoodsPriceUpdateVo.class);
+        log.info("商品价格变更结果:{}", zcr);
+        return zcr;
+    }
+
+    // 5.2.3	商品上下架状态变更
+    @PostMapping("/egoods/status/update")
+    public GoodsStatusUpdateVo egoodsStatusUpdate(@RequestBody GoodsStatusUpdateBo bo) {
+
+        ZCR responseDto = doZcPost("/api/egoods/status/update", bo);
+        log.info("商品上下架状态变更参数:{}",JSONUtil.toJsonStr(bo));
+        GoodsStatusUpdateVo zcr = parseZcResponse(responseDto, GoodsStatusUpdateVo.class);
+        log.info("商品上下架状态变更结果:{}", zcr);
+        return zcr;
+    }
+
+    // 5.2.4	商品图片变更
+    @PostMapping("/egoods/imgs/update")
+    public GoodsImageUpdateVo egoodsImgsUpdate(@RequestBody GoodsImageUpdateBo bo) {
+        ZCR responseDto = doZcPost("/api/egoods/imgs/update", bo);
+        log.info("商品图片变更参数:{}",JSONUtil.toJsonStr(bo));
+        GoodsImageUpdateVo zcr = parseZcResponse(responseDto, GoodsImageUpdateVo.class);
+        log.info("商品图片变更结果:{}", zcr);
+        return zcr;
+    }
+
+    // 5.2.5	商品规格信息变更
+    @PostMapping("/egoods/properties/update")
+    public GoodsUpdateVo egoodsPropertiesUpdate(@RequestBody GoodsPropertiesUpdateBo bo) {
+        ZCR responseDto = doZcPost("/api/egoods/properties/update", bo);
+        log.info("商品规格信息变更参数:{}",JSONUtil.toJsonStr(bo));
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        log.info("商品规格信息变更结果:{}", zcr);
+        return zcr;
+    }
+
+    //TODO 5.2.6	商品详情信息变更
+    @PostMapping("/egoods/detail/update")
+    public GoodsUpdateVo egoodsDetailUpdate(@RequestBody GoodsDetailBo bo) {
+        ZCR responseDto = doZcPost("/api/egoods/detail/update", bo);
+        log.info("商品详情信息变更参数:{}",JSONUtil.toJsonStr(bo));
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        log.info("商品详情信息变更结果:{}", zcr);
+        return zcr;
+    }
+
+    //5.3	订单服务
+
+    // 5.3.1	查询订单详情
+    @PostMapping("/mall/order/detail")
+    public OrderDetailVo mallOrderDetail(@RequestBody OrderDetailBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/order/detail", bo);
+        OrderDetailVo orderDetailVo = parseZcResponse(responseDto, OrderDetailVo.class);
+        return orderDetailVo;
+    }
+
+    //TODO 5.3.2	接单
+    @PostMapping("/mall/order/confirm")
+    public GoodsUpdateVo mallOrderConfirm(@RequestBody OrderConfirmBo bo) throws ZhongcheException {
+        ZCR responseDto = doZcPost("/api/mall/order/confirm", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //TODO 5.3.3	同步商品协商发货时间
+    @PostMapping("/mall/order/goods/append")
+    public GoodsUpdateVo mallOrderGoodsAppend(@RequestBody OrderGoodsAppendBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/order/goods/append", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //TODO 5.3.4	拒单
+    @PostMapping("/mall/order/reject")
+    public GoodsUpdateVo mallOrderReject(@RequestBody OrderRejectBo bo) throws ZhongcheException {
+        ZCR responseDto = doZcPost("/api/mall/order/reject", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //TODO 5.3.5	发货
+    @PostMapping("/mall/order/deliver/goods")
+    public ZhongCheOrderDeliverVo mallOrderDeliverGoods(@RequestBody ZhongCheOrderDeliverBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/order/deliver/goods", bo);
+        ZhongCheOrderDeliverVo zcr = parseZcResponse(responseDto, ZhongCheOrderDeliverVo.class);
+        return zcr;
+    }
+
+    //TODO 5.3.6	查询待处理订单列表
+    @PostMapping("/mall/order/pending/list")
+    public PendingOrderListVo mallOrderPendingList(@RequestBody ZCTokenBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/order/pending/list", bo);
+        PendingOrderListVo zcr = parseZcResponse(responseDto, PendingOrderListVo.class);
+        return zcr;
+    }
+
+    //TODO 5.3.7	妥投通知
+    @PostMapping("/mall/order/delivered/notice")
+    public GoodsUpdateVo mallOrderDeliveredNotice(@RequestBody OrderDeliveredNoticeBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/order/delivered/notice", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //TODO 5.3.8	查询发货单信息
+    @PostMapping("/mall/order/query/outgoing")
+    public OutgoingVo mallOrderQueryOutgoing(@RequestBody OutgoingQueryBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/order/query/outgoing", bo);
+        OutgoingVo zcr = parseZcResponse(responseDto, OutgoingVo.class);
+        return zcr;
+    }
+
+    //5.3.9	查询备货单详情
+    @PostMapping("/mall/prepare/order/detail")
+    public PrepareOrderDetailRespVo mallPrepareOrderDetail(@RequestBody PrepareOrderDetailReqBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/prepare/order/detail", bo);
+        PrepareOrderDetailRespVo zcr = parseZcResponse(responseDto, PrepareOrderDetailRespVo.class);
+        return zcr;
+    }
+
+    //5.3.10	备货
+    @PostMapping("/mall/prepare/order/goods")
+    public GoodsUpdateVo mallPrepareOrderGoods(@RequestBody PrepareOrderDataBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/prepare/order/goods", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+
+    //5.3.11	确认取消备货单
+    @PostMapping("/mall/prepare/order/confirm/cancel")
+    public GoodsUpdateVo mallPrepareOrderConfirmCancel(@RequestBody PrepareOrderDetailReqBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/prepare/order/confirm/cancel", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+
+
+    //TODO 5.3.12	审核订单取消
+    /*
+        //请求业务参数
+        OrderCancelAuditBo orderCancelAuditBo = new OrderCancelAuditBo();
+        //响应业务参数
+        GoodsUpdateVo goodsUpdateVo = new GoodsUpdateVo();
+     */
+    @PostMapping("/mall/order/cancel/audit/judge")
+    public GoodsUpdateVo mallOrderCancelAuditJudge(@RequestBody OrderCancelAuditBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/order/cancel/audit/judge", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+    //5.4.1	查询售后单详情
+    /*
+        //请求业务参数
+        AfterSaleDetailBo afterSaleDetailBo = new AfterSaleDetailBo();
+        //响应业务参数
+        AfterSaleDetailVo afterSaleDetailVo = new AfterSaleDetailVo();
+        AfterSaleReason afterSaleReason = new AfterSaleReason();
+        AfterSaleGoods afterSaleGoods = new AfterSaleGoods();
+        AfterSalePick afterSalePick = new AfterSalePick();
+        ImageList imageList = new ImageList();
+        AfterSaleReturn afterSaleReturn = new AfterSaleReturn();
+     */
+    @PostMapping("/mall/aftersale/detail")
+    public AfterSaleDetailVo mallAftersaleDetail(@RequestBody AfterSaleDetailBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/aftersale/detail", bo);
+        AfterSaleDetailVo zcr = parseZcResponse(responseDto, AfterSaleDetailVo.class);
+        return zcr;
+    }
+
+    //5.4.2	接受售后
+    @PostMapping("/mall/aftersale/confirm")
+    public GoodsUpdateVo mallAftersaleConfirm(@RequestBody AfterSaleConfirmBo bo) throws ZhongcheException{
+        ZCR responseDto = doZcPost("/api/mall/aftersale/confirm", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //5.4.3	拒绝售后
+    @PostMapping("/mall/aftersale/reject")
+    public GoodsUpdateVo mallAftersaleReject(@RequestBody AfterSaleRejectBo bo) throws ZhongcheException{
+        ZCR responseDto = doZcPost("/api/mall/aftersale/reject", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //5.4.4	确认收到退货
+    @PostMapping("/mall/aftersale/return/goods/received")
+    public GoodsUpdateVo mallAftersaleReturnGoodsReceived(@RequestBody AfterSaleReturnReceivedBo bo) throws ZhongcheException{
+        ZCR responseDto = doZcPost("/api/mall/aftersale/return/goods/received", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //5.4.5	换货新品发货
+    @PostMapping("/mall/aftersale/deliver/goods")
+    public AfterSaleDeliverGoodsVo mallAftersaleDeliverGoods(@RequestBody AfterSaleDeliverGoodsBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/aftersale/deliver/goods", bo);
+        AfterSaleDeliverGoodsVo zcr = parseZcResponse(responseDto, AfterSaleDeliverGoodsVo.class);
+        return zcr;
+    }
+
+    //TODO 5.4.6	查询待处理售后单列表
+    @PostMapping("/mall/aftersale/pending/list")
+    public AfterSalePendingListVo mallAftersalePendingList(@RequestBody AfterSalePendingListBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/aftersale/pending/list", bo);
+        AfterSalePendingListVo zcr = parseZcResponse(responseDto, AfterSalePendingListVo.class);
+        return zcr;
+    }
+
+    //5.4.7	查询换货新品发货单信息
+    @PostMapping("/mall/aftersale/query/outgoing")
+    public AfterSaleQueryOutgoingVo mallAftersaleQueryOutgoing(@RequestBody AfterSaleQueryOutgoingBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/aftersale/query/outgoing", bo);
+        AfterSaleQueryOutgoingVo zcr = parseZcResponse(responseDto, AfterSaleQueryOutgoingVo.class);
+        return zcr;
+    }
+
+    //5.4.8	线下售后
+    @PostMapping("/mall/aftersale/offline/create")
+    public AfterSaleOfflineCreateVo mallAftersaleOfflineCreate(@RequestBody AfterSaleOfflineCreateBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/aftersale/offline/create", bo);
+        AfterSaleOfflineCreateVo zcr = parseZcResponse(responseDto, AfterSaleOfflineCreateVo.class);
+        return zcr;
+    }
+
+    //5.4.9	确认退款
+    @PostMapping("/mall/aftersale/refund")
+    public GoodsUpdateVo mallAftersaleRefund(@RequestBody AfterSaleRefundBo bo) throws ZhongcheException{
+        ZCR responseDto = doZcPost("/api/mall/aftersale/refund", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+
+    //5.7.1	查询开票信息
+    @PostMapping("/mall/invoice/apply/detail")
+    public InvoiceApplyDetailVo mallInvoiceApplyDetail(@RequestBody InvoiceApplyDetailBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/invoice/apply/detail", bo);
+        InvoiceApplyDetailVo zcr = parseZcResponse(responseDto, InvoiceApplyDetailVo.class);
+        return zcr;
+
+    }
+
+    //5.7.2	查询开票申请订单列表
+    @PostMapping("/mall/invoice/apply/orders")
+    public InvoiceApplyOrdersVo mallInvoiceApplyOrders(@RequestBody InvoiceApplyOrdersBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/invoice/apply/orders", bo);
+        InvoiceApplyOrdersVo zcr = parseZcResponse(responseDto, InvoiceApplyOrdersVo.class);
+        return zcr;
+
+    }
+
+    //5.7.3    拒绝开票
+    @PostMapping("/mall/invoice/apply/reject")
+    public GoodsUpdateVo mallInvoiceApplyReject(@RequestBody InvoiceApplyRejectBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/invoice/apply/reject", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //5.7.4	电商同步开票信息
+    @PostMapping("/mall/invoice/sync/invoiceinfos")
+    public GoodsUpdateVo mallInvoiceSyncInvoiceInfos(@RequestBody InvoiceSyncInvoiceInfosBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/invoice/sync/invoiceinfos", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //5.7.5 同步纸质发票邮寄信息
+
+    @PostMapping("/mall/invoice/sync/waybill")
+    public GoodsUpdateVo mallInvoiceSyncWaybill(@RequestBody InvoiceSyncWaybillBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/invoice/sync/waybill", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //5.7.6 发票妥投通知
+    @PostMapping("/mall/invoice/delivered/notice")
+    public GoodsUpdateVo mallInvoiceDeliveredNotice(@RequestBody InvoiceDeliveredNoticeBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/invoice/delivered/notice", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //5.7.7	查询待开票申请单列表
+    @PostMapping("/mall/invoice/apply/waitlist")
+    public InvoiceApplyWaitlistVo mallInvoiceApplyWaitList(@RequestBody InvoiceApplyWaitlistBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/invoice/apply/waitlist", bo);
+        InvoiceApplyWaitlistVo zcr = parseZcResponse(responseDto, InvoiceApplyWaitlistVo.class);
+        return zcr;
+    }
+
+    //5.7.8	电商同意退票
+    @PostMapping("/mall/invoice/refund/agree")
+    public GoodsUpdateVo mallInvoiceRefundAgree(@RequestBody InvoiceRefundAgreeBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/invoice/refund/agree", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //5.7.9	电商确认完成退票
+
+    @PostMapping("/mall/invoice/refund/finish")
+    public GoodsUpdateVo mallInvoiceRefundFinish(@RequestBody InvoiceRefundFinishBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/invoice/refund/finish", bo);
+        GoodsUpdateVo zcr = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return zcr;
+    }
+
+    //5.7.10 电商拒绝退票申请
+    @PostMapping("/mall/invoice/refund/reject")
+    public GoodsUpdateVo mallInvoiceRefundReject(@RequestBody InvoiceRefundRejectBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/invoice/refund/reject", bo);
+        GoodsUpdateVo goodsUpdateVo = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return goodsUpdateVo;
+    }
+
+    //5.9.1	查询结算单详情
+    @PostMapping("/mall/settlement/detail")
+    public SettlementDetailVo mallSettlementDetail(@RequestBody SettlementDetailBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/settlement/detail", bo);
+        SettlementDetailVo settlementDetailVo = parseZcResponse(responseDto, SettlementDetailVo.class);
+        return settlementDetailVo;
+    }
+
+    //5.9.2	查询结算单订单列表
+    @PostMapping("/mall/settlement/apply/orders")
+    public SettlementApplyOrdersVo mallSettlementApplyOrders(@RequestBody SettlementApplyOrdersBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/settlement/apply/orders", bo);
+        SettlementApplyOrdersVo settlementApplyOrdersVo = parseZcResponse(responseDto, SettlementApplyOrdersVo.class);
+        return settlementApplyOrdersVo;
+    }
+
+
+    //5.9.3	结算单确认结算
+    @PostMapping("/mall/settlement/confirm")
+    public GoodsUpdateVo mallSettlementConfirm(@RequestBody SettlementConfirmBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/settlement/confirm", bo);
+        GoodsUpdateVo goodsUpdateVo = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return goodsUpdateVo;
+    }
+
+    //5.9.4	结算单付款详情
+    @PostMapping("/mall/settlement/payment/detail")
+    public SettlementPaymentDetailVo mallSettlementPaymentDetail( @RequestBody SettlementPaymentDetailBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/settlement/payment/detail", bo);
+        SettlementPaymentDetailVo settlementPaymentDetailVo = parseZcResponse(responseDto, SettlementPaymentDetailVo.class);
+        return settlementPaymentDetailVo;
+
+    }
+
+    //5.9.5	结算单确认收款
+    @PostMapping("/mall/settlement/confirm/payment")
+    public GoodsUpdateVo mallSettlementConfirmPayment( @RequestBody SettlementConfirmPaymentBo bo) {
+        ZCR responseDto = doZcPost("/api/mall/settlement/confirm/payment", bo);
+        GoodsUpdateVo goodsUpdateVo = parseZcResponse(responseDto, GoodsUpdateVo.class);
+        return goodsUpdateVo;
+    }
+
+}

+ 89 - 0
ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/handler/impl/TongjiPushStrategy.java

@@ -0,0 +1,89 @@
+package org.dromara.external.handler.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.external.api.zhongche.domain.vo.GoodsImageUpdateVo;
+import org.dromara.external.api.zhongche.domain.vo.GoodsPriceUpdateVo;
+import org.dromara.external.api.zhongche.domain.vo.GoodsStatusUpdateVo;
+import org.dromara.external.api.zhongche.domain.vo.GoodsUpdateVo;
+import org.dromara.external.domain.ExternalProduct;
+import org.dromara.external.handler.ProductPushStrategy;
+import org.dromara.product.api.domain.ProductVo;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * @author
+ * @date 2026/4/21 下午5:20
+ */
+@Component("tongjiPushStrategy")
+@RequiredArgsConstructor
+@Slf4j
+public class TongjiPushStrategy  implements ProductPushStrategy {
+    /**
+     * 推送商品
+     *
+     * @param itemId
+     * @param products
+     */
+    @Override
+    public void push(Long itemId, List<ExternalProduct> products) {
+
+    }
+
+    /**
+     * 商品价格变更
+     *
+     * @param itemId
+     * @param products
+     */
+    @Override
+    public GoodsPriceUpdateVo updatePrice(Long itemId, List<ProductVo> products) {
+        return null;
+    }
+
+    /**
+     * 商品上下架状态变更
+     *
+     * @param productNos
+     * @param status
+     */
+    @Override
+    public GoodsStatusUpdateVo updateStatus(List<String> productNos, Integer status) {
+        return null;
+    }
+
+    /**
+     * 商品图片变更
+     *
+     * @param itemId
+     * @param products
+     */
+    @Override
+    public GoodsImageUpdateVo updateImages(Long itemId, List<ProductVo> products) {
+        return null;
+    }
+
+    /**
+     * 商品规格信息变更
+     *
+     * @param itemId
+     * @param products
+     */
+    @Override
+    public GoodsUpdateVo updateProperties(Long itemId, List<ProductVo> products) {
+        return null;
+    }
+
+    /**
+     * 商品详情信息变更
+     *
+     * @param itemId
+     * @param products
+     */
+    @Override
+    public GoodsUpdateVo updateDetail(Long itemId, List<ProductVo> products) {
+        return null;
+    }
+}

+ 2 - 2
ruoyi-modules/ruoyi-external/src/main/java/org/dromara/external/service/impl/ExternalProductServiceImpl.java

@@ -256,10 +256,10 @@ public class ExternalProductServiceImpl  extends ServiceImpl<ExternalProductMapp
         if (ObjectUtil.isNotEmpty(productIds)) {
             //获取需要推送的商品数据
             List<ProductVo> productDetails = remoteProductService.getProductDetails(productIds);
-            strategy.updateDetail(itemId,productDetails);
+//            strategy.updateDetail(itemId,productDetails);
 //            strategy.updatePrice(itemId,productDetails);
 //            strategy.updateProperties(itemId,productDetails);
-//            strategy.updateImages(itemId,productDetails);
+            strategy.updateImages(itemId,productDetails);
         }
         return baseMapper.update(Wrappers.lambdaUpdate(ExternalProduct.class)
             .set(ExternalProduct::getPushStatus, 1)

+ 224 - 0
ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/ImageUrlMigration.java

@@ -0,0 +1,224 @@
+package org.dromara.product;
+
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ImageUrlMigration {
+
+    // 数据库配置
+    private static final String URL = "jdbc:mysql://119.97.180.88:3306/yoe_product_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true";
+    private static final String USER = "root";
+    private static final String PASSWORD = "8b1595a453c9d355";
+
+    // 批次大小:每次处理 500 条,避免内存溢出和锁表
+    private static final int BATCH_SIZE = 500;
+
+    // 目标域名配置
+    private static final String DOMAIN_PC = "https://pc.yoe365.com:8044";
+    private static final String DOMAIN_YUNYING = "https://yunying.yoe365.com:8045";
+
+    public static void main(String[] args) {
+        System.out.println("🚀 开始执行图片链接迁移脚本...");
+
+        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
+            // 关闭自动提交,手动控制事务
+            conn.setAutoCommit(false);
+
+            // 1. 查询 product_base 表
+            updateProductBaseTable(conn);
+
+            // 2. 查询 product_photos 表 (包含 image_url 和 product_details_pc)
+            updateProductPhotosTable(conn);
+
+            System.out.println("✅ 所有任务执行完毕!");
+
+        } catch (SQLException e) {
+            e.printStackTrace();
+            System.err.println("❌ 发生数据库错误,请检查日志。");
+        }
+    }
+
+    /**
+     * 处理 product_base 表
+     */
+    private static void updateProductBaseTable(Connection conn) throws SQLException {
+        String selectSql = "SELECT id, product_image FROM product_base WHERE del_flag = '0' AND product_image NOT LIKE 'http%' AND (product_image LIKE '%/BankPhotos/%' OR product_image LIKE '%/mro/%' OR product_image LIKE '%/upload/%' OR product_image LIKE '%/Photos/%' OR product_image LIKE '%/marketing/%')";
+        String updateSql = "UPDATE product_base SET product_image = ? WHERE id = ?";
+
+        List<Long> ids = new ArrayList<>();
+        try (Statement stmt = conn.createStatement();
+             ResultSet rs = stmt.executeQuery(selectSql)) {
+            while (rs.next()) {
+                ids.add(rs.getLong("id"));
+            }
+        }
+
+        if (ids.isEmpty()) {
+            System.out.println("ℹ️  [product_base] 没有需要更新的数据。");
+            return;
+        }
+
+        System.out.println("ℹ️  [product_base] 共找到 " + ids.size() + " 条待更新数据,开始分批处理...");
+//        processBatches(conn, ids, updateSql, (id, conn1) -> {
+            // 这里只是为了演示结构,product_base 比较简单,可以直接在 SQL 层做,
+            // 但为了统一逻辑,我们在 Java 层也可以做,或者针对 product_base 直接用 SQL 批量更新更简单。
+            // 由于 product_base 只有 product_image 一个字段,直接用 SQL 替换效率更高。
+//            return null;
+//        });
+
+        // 针对 product_base 这种单字段更新,直接用 SQL 批量执行效率更高,不走 Java 内存处理
+        executeDirectSqlUpdate(conn, ids, BATCH_SIZE);
+    }
+
+    /**
+     * 针对 product_base 的直接 SQL 更新 (因为只涉及一个字段,不需要查出来再塞回去)
+     */
+    private static void executeDirectSqlUpdate(Connection conn, List<Long> ids, int batchSize) throws SQLException {
+        String sql = "UPDATE product_base SET product_image = CASE " +
+                "WHEN product_image LIKE '%/Photos/%' THEN CONCAT(?, product_image) " +
+                "WHEN product_image LIKE '%/marketing/%' THEN CONCAT(?, product_image) " +
+                "WHEN product_image LIKE '%/BankPhotos/%' THEN CONCAT(?, product_image) " +
+                "WHEN product_image LIKE '%/mro/%' THEN CONCAT(?, product_image) " +
+                "WHEN product_image LIKE '%/upload/%' THEN CONCAT(?, product_image) " +
+                "ELSE product_image END WHERE id = ?";
+
+        int total = ids.size();
+        for (int i = 0; i < total; i += batchSize) {
+            int end = Math.min(i + batchSize, total);
+            List<Long> subList = ids.subList(i, end);
+
+            try (PreparedStatement ps = conn.prepareStatement(sql)) {
+                // 设置参数顺序:先设置 5 个域名参数,再设置 ID
+                for (Long id : subList) {
+                    ps.setString(1, DOMAIN_YUNYING); // /Photos/
+                    ps.setString(2, DOMAIN_YUNYING); // /marketing/
+                    ps.setString(3, DOMAIN_PC);      // /BankPhotos/
+                    ps.setString(4, DOMAIN_PC);      // /mro/
+                    ps.setString(5, DOMAIN_PC);      // /upload/
+                    ps.setLong(6, id);
+                    ps.addBatch();
+                }
+                ps.executeBatch();
+                conn.commit();
+            }
+            System.out.println("   -> 批次完成: " + end + " / " + total);
+        }
+    }
+
+    /**
+     * 处理 product_photos 表
+     * 包含 image_url 和 product_details_pc
+     */
+    private static void updateProductPhotosTable(Connection conn) throws SQLException {
+        // 1. 先查出所有符合条件的 ID
+        String selectIdsSql = "SELECT product_id FROM product_photos WHERE del_flag = '0' AND image_url NOT LIKE 'http%' AND (image_url LIKE '%/BankPhotos/%' OR image_url LIKE '%/mro/%' OR image_url LIKE '%/upload/%' OR image_url LIKE '%/Photos/%' OR image_url LIKE '%/marketing/%')";
+
+        List<Long> ids = new ArrayList<>();
+        try (Statement stmt = conn.createStatement();
+             ResultSet rs = stmt.executeQuery(selectIdsSql)) {
+            while (rs.next()) {
+                ids.add(rs.getLong("product_id"));
+            }
+        }
+
+        if (ids.isEmpty()) {
+            System.out.println("ℹ️  [product_photos] 没有需要更新的数据。");
+            return;
+        }
+
+        System.out.println("ℹ️  [product_photos] 共找到 " + ids.size() + " 条待更新数据,开始分批处理...");
+
+        // 2. 分批处理
+        int total = ids.size();
+        for (int i = 0; i < total; i += BATCH_SIZE) {
+            int end = Math.min(i + BATCH_SIZE, total);
+            List<Long> subList = ids.subList(i, end);
+
+            // 2.1 构建 IN 查询语句
+            String inSql = String.join(",", subList.stream().map(String::valueOf).toArray(String[]::new));
+            String selectDataSql = "SELECT product_id, image_url, product_details_pc FROM product_photos WHERE product_id IN (" + inSql + ")";
+
+            List<Object[]> updateBatch = new ArrayList<>();
+
+            try (Statement stmt = conn.createStatement();
+                 ResultSet rs = stmt.executeQuery(selectDataSql)) {
+
+                while (rs.next()) {
+                    Long id = rs.getLong("product_id");
+                    String imageUrl = rs.getString("image_url");
+                    String detailsPc = rs.getString("product_details_pc");
+
+                    // 2.2 在 Java 内存中进行替换 (逻辑更清晰,避免 SQL 复杂度)
+                    String newImageUrl = replaceUrl(imageUrl);
+                    String newDetailsPc = replaceUrl(detailsPc);
+
+                    // 只有当内容确实发生变化时才加入更新队列
+                    if (!imageUrl.equals(newImageUrl) || !detailsPc.equals(newDetailsPc)) {
+                        updateBatch.add(new Object[]{newImageUrl, newDetailsPc, id});
+                    }
+                }
+            }
+
+            // 2.3 执行批量更新
+            if (!updateBatch.isEmpty()) {
+                String updateSql = "UPDATE product_photos SET image_url = ?, product_details_pc = ? WHERE product_id = ?";
+                try (PreparedStatement ps = conn.prepareStatement(updateSql)) {
+                    for (Object[] params : updateBatch) {
+                        ps.setString(1, (String) params[0]);
+                        ps.setString(2, (String) params[1]);
+                        ps.setLong(3, (Long) params[2]);
+                        ps.addBatch();
+                    }
+                    ps.executeBatch();
+                }
+                conn.commit(); // 提交当前批次
+            } else {
+                conn.commit(); // 即使没有数据变更也要提交事务以保持游标移动
+            }
+
+            System.out.println("   -> 批次完成: " + end + " / " + total);
+        }
+    }
+
+    /**
+     * 通用的 URL 替换逻辑
+     * 修改重点:在执行替换前,先检查是否已经包含目标域名,防止重复替换
+     */
+    private static String replaceUrl(String content) {
+        if (content == null || content.isEmpty()) {
+            return content;
+        }
+
+        // 【关键修复】如果内容已经包含目标域名,直接返回,不做任何处理
+        // 这能防止重复运行脚本导致的数据损坏
+        if (content.contains("pc.yoe365.com:8044") || content.contains("yunying.yoe365.com:8045")) {
+            return content;
+        }
+
+        // 只有当不包含 http 时才处理(排除已经是其他 http 链接的情况)
+        if (content.contains("http")) {
+            return content;
+        }
+
+        // 执行替换
+        // 注意:这里依然使用 contains 检查旧路径,但上面已经拦截了新域名,所以是安全的
+        if (content.contains("/Photos/")) {
+            content = content.replace("/Photos/", "https://yunying.yoe365.com:8045/Photos/");
+        }
+        if (content.contains("/marketing/")) {
+            content = content.replace("/marketing/", "https://yunying.yoe365.com:8045/marketing/");
+        }
+        if (content.contains("/BankPhotos/")) {
+            content = content.replace("/BankPhotos/", "https://pc.yoe365.com:8044/BankPhotos/");
+        }
+        if (content.contains("/mro/")) {
+            content = content.replace("/mro/", "https://pc.yoe365.com:8044/mro/");
+        }
+        if (content.contains("/upload/")) {
+            content = content.replace("/upload/", "https://pc.yoe365.com:8044/upload/");
+        }
+
+        return content;
+    }
+}

+ 5 - 0
ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/domain/bo/ProductBaseBo.java

@@ -457,6 +457,11 @@ public class ProductBaseBo extends BaseEntity {
     * */
     private Long customerId;
 
+    /**
+    * 查询类型  1=供应商查询,2=伙伴商查询
+    * */
+    private Integer queryType;
+
 
 
 }

+ 1 - 1
ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/domain/vo/ProductListVo.java

@@ -225,7 +225,7 @@ public class ProductListVo implements Serializable {
     * 协议id
     * */
     @ExcelProperty(value = "协议id")
-    private Long agreementId;
+    private Long protocolId;
     /**
     * 客户Id
     * */

+ 4 - 0
ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/service/impl/ProductBaseServiceImpl.java

@@ -452,6 +452,10 @@ public class ProductBaseServiceImpl extends ServiceImpl<ProductBaseMapper, Produ
         if (ObjectUtil.isNotEmpty(bo.getCategoryIds())){
             productBaseVoLambdaEsQueryWrapper.in(ProductBaseVo::getBottomCategoryId, bo.getCategoryIds().split(","));
         }
+        productBaseVoLambdaEsQueryWrapper.eq(bo.getQueryType()==1,ProductBaseVo::getCreateByType,1);
+        productBaseVoLambdaEsQueryWrapper.eq(bo.getQueryType()==1,ProductBaseVo::getCreateById,LoginHelper.getLoginUser().getSupplierId());
+        productBaseVoLambdaEsQueryWrapper.eq(bo.getQueryType()==2,ProductBaseVo::getCreateByType,2);
+        productBaseVoLambdaEsQueryWrapper.eq(bo.getQueryType()==2,ProductBaseVo::getCreateById,LoginHelper.getLoginUser().getPartnerId());
         return productBaseVoLambdaEsQueryWrapper;
     }
 

+ 10 - 4
ruoyi-modules/ruoyi-product/src/main/java/org/dromara/product/service/impl/ProductPoolAuditServiceImpl.java

@@ -637,10 +637,16 @@ public class ProductPoolAuditServiceImpl  extends ServiceImpl<ProductPoolAuditMa
             }else if(Objects.equals(product.getPoolType(),1)){
                 name = "标准产品池";
             }else if(Objects.equals(product.getPoolType(),2)){
-                //获取客户信息
-                CustomerInfoDTO customerInfoDTO = remoteCustomerService.selectCustomerInfoById(product.getCustomerId());
-                if(customerInfoDTO != null){
-                    name = customerInfoDTO.getCustomerName();
+                //先获取协议信息
+                ProtocolInfo  protocolInfo= protocolInfoMapper.selectById(product.getProtocolId());
+                if (ObjectUtil.isNotEmpty(protocolInfo)){
+                    //获取客户信息
+                    CustomerInfoDTO customerInfoDTO = remoteCustomerService.selectCustomerInfoById(protocolInfo.getCustomerId());
+                    if(customerInfoDTO != null){
+                        name = customerInfoDTO.getCustomerName();
+                    }else{
+                        name = "未知协议产品池";
+                    }
                 }else{
                     name = "未知协议产品池";
                 }

+ 1 - 1
ruoyi-modules/ruoyi-product/src/main/resources/mapper/product/ProductBaseMapper.xml

@@ -374,9 +374,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             c.now_inventory AS nowInventory,
             c.virtual_inventory AS virtualInventory,
             c.min_order_quantity AS minOrderQuantity,
-            c.purchasing_price AS purchasingPrice,
             d.pool_id AS poolId,
             d.item_id AS itemId,
+            d.protocol_id AS protocol_id,
             d.type AS poolType,
             d.protocol_id AS protocolId,
             e.brand_name AS brandName,

+ 1 - 1
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java

@@ -104,7 +104,7 @@ public class SysUser extends TenantEntity {
     private String remark;
 
     /**
-     * 用户子类型 用户子类型(0=平台用户,1=供应商,2=伙伴商,3=商城用户)
+     * 用户子类型 用户子类型(0=平台用户,1=供应商,2=伙伴商,3=企业用户,4=个人用户)
      * */
     private String userSonType;
 

+ 1 - 1
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java

@@ -96,7 +96,7 @@ public class SysUserBo extends BaseEntity {
     private String remark;
 
     /**
-     * 用户子类型 用户子类型(0=平台用户,1=供应商,2=伙伴商,3=商城用户)
+     * 用户子类型 用户子类型(0=平台用户,1=供应商,2=伙伴商,3=企业用户,4=个人用户)
      */
     private String userSonType;
 

+ 1 - 1
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java

@@ -109,7 +109,7 @@ public class SysUserVo implements Serializable {
     private String remark;
 
     /**
-     * 用户子类型 用户子类型(0=平台用户,1=供应商,2=伙伴商,3=商城用户)
+     * 用户子类型 用户子类型(0=平台用户,1=供应商,2=伙伴商,3=企业用户,4=个人用户)
      * */
     private String userSonType;
 

+ 4 - 2
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/dubbo/RemoteUserServiceImpl.java

@@ -296,7 +296,7 @@ public class RemoteUserServiceImpl implements RemoteUserService {
         List<SysPostVo> posts = postService.selectPostsByUserId(userId);
         loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
         loginUser.setPosts(BeanUtil.copyToList(posts, PostDTO.class));
-        //用户子类型 用户子类型(0=平台用户,1=供应商,2=伙伴商,3=商城用户)
+        //用户子类型 用户子类型(0=平台用户,1=供应商,2=伙伴商,3=企业用户,4=个人用户)
         if(Objects.equals(userVo.getUserSonType(),"1")){
             Long supplierId = remoteSupplierInfoService.selectSupplierIdByUserId(userVo.getUserId());
 
@@ -511,7 +511,9 @@ public class RemoteUserServiceImpl implements RemoteUserService {
         SysUserBo sysUserBo = MapstructUtils.convert(remoteUserBo, SysUserBo.class);
         String username = sysUserBo.getUserName();
         boolean exist = userMapper.exists(new LambdaQueryWrapper<SysUser>()
-            .eq(SysUser::getUserName, sysUserBo.getUserName()));
+            .eq(SysUser::getUserName, sysUserBo.getUserName())
+            .eq(SysUser::getUserSonType, sysUserBo.getUserSonType())
+        );
         if (exist) {
             throw new ServiceException("账号已经存在:" + username);
         }