Browse Source

合并0507分支

yousongbo 1 week ago
parent
commit
641a4c85ab

+ 2 - 0
suishenbang-admin/src/main/java/com/dgtly/DgtlyApplication.java

@@ -9,6 +9,7 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
 import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.retry.annotation.EnableRetry;
 import org.springframework.web.client.RestTemplate;
 
 import javax.servlet.MultipartConfigElement;
@@ -20,6 +21,7 @@ import java.util.List;
  * 
  * @author ruoyi
  */
+@EnableRetry
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
 public class DgtlyApplication
 {

+ 1 - 0
suishenbang-admin/src/main/resources/application-prod.yml

@@ -117,6 +117,7 @@ spring:
     shopStoneLikePaint: http://esbgateway.nipponpaint.com.cn/NPeportal/api/diydt/v3/texturewall/staff/list #http://esbgateway-test.nipponpaint.com.cn/NPeportal/api/diydt/v3/texturewall/staff/list
     goldShopUser: http://esbgateway.nipponpaint.com.cn/NPeportal/api/diydt/v3/goldshop/staff/list
     esbShopStoneLikePaintUser: http://esb.nipponpaint.com.cn/esb/get/comm/api/body
+    esbOrderCancelUrl: http://esb.nipponpaint.com.cn/esb/erp/api
 
   mail:
     #邮件服务器地址

+ 1 - 0
suishenbang-admin/src/main/resources/application-uat.yml

@@ -120,6 +120,7 @@ spring:
     shopStoneLikePaint: http://esbgateway.nipponpaint.com.cn/NPeportal/api/diydt/v3/texturewall/staff/list #http://esbgateway-test.nipponpaint.com.cn/NPeportal/api/diydt/v3/texturewall/staff/list
     goldShopUser: http://esbgateway.nipponpaint.com.cn/NPeportal/api/diydt/v3/goldshop/staff/list
     esbShopStoneLikePaintUser: http://esb-test.nipponpaint.com.cn/esb/get/comm/api/body
+    esbOrderCancelUrl: http://esb-test.nipponpaint.com.cn/esb/erp/api
 
   mail:
     #邮件服务器地址

+ 1 - 0
suishenbang-api/src/main/resources/application-uat.yml

@@ -97,6 +97,7 @@ spring:
     shopStoneLikePaint: http://esbgateway.nipponpaint.com.cn/NPeportal/api/diydt/v3/texturewall/staff/list #http://esbgateway-test.nipponpaint.com.cn/NPeportal/api/diydt/v3/texturewall/staff/list
     goldShopUser: http://esbgateway.nipponpaint.com.cn/NPeportal/api/diydt/v3/goldshop/staff/list
     esbShopStoneLikePaintUser: http://esb-test.nipponpaint.com.cn/esb/get/comm/api/body
+    esbOrderCancelUrl: http://esb-test.nipponpaint.com.cn/esb/get/comm/api/body
   mail:
     #邮件服务器地址
     host: mail.dgtis.com

+ 20 - 0
suishenbang-quartz/src/main/java/com/dgtly/quartz/task/RyTask.java

@@ -13,6 +13,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
 
 /**
  * 定时任务调度测试
@@ -227,6 +229,24 @@ public class RyTask
     }*/
 
 
+    /**
+     * @description: 交货冻结取货定时任务
+     *               从 meta_hana_not_freeze_customer 白名单确认当前所有已冻结经销商,
+     *               在 sales_order_base 中查询满足以下条件的要货记录执行逻辑删除(取消):
+     *               1. 经销商属于已冻结名单(不在白名单中)
+     *               2. 订单状态满足任一条件:未复核金额≠0、或冻结≠0、或信用状态不为空
+     *               3. 订单尚未完成发货(NO_DELIVER_AMT != 0,已发货订单不取消)
+     *               取消完成后记录操作日志,并通过企业微信向经销商负责人、要货人、对应销售推送通知。
+     * @author: njs
+     */
+    public void cancelFreezeCustomerOrder() {
+        List<Map<String, Object>> canceledList = HanaSalesOrderService.cancelFreezeCustomerOrder();
+//        if (canceledList != null && !canceledList.isEmpty()) {
+//            wxSendMessageService.sendFreezeCustomerCancelOrderMessage(canceledList);
+//        }
+    }
+
+
     /**
      * @description: 业绩进度指标预警邮件/企微消息通知
      * @param:

+ 10 - 0
suishenbang-system/pom.xml

@@ -27,6 +27,10 @@
             <artifactId>mysql-connector-java</artifactId>
             <version>5.1.47</version>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.retry</groupId>
+            <artifactId>spring-retry</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>com.sap.cloud.db.jdbc</groupId>
@@ -38,6 +42,12 @@
             <groupId>com.dgtly</groupId>
             <artifactId>suishenbang-common</artifactId>
         </dependency>
+        <!-- Gson已不再需要,因为使用Spring RestTemplate + Fastjson -->
+        <!--<dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.8.6</version>
+        </dependency>-->
         <!--mail-->
         <dependency>
             <groupId>javax.mail</groupId>

+ 65 - 0
suishenbang-system/src/main/java/com/dgtly/system/domain/ApplicationContextProvider.java

@@ -0,0 +1,65 @@
+package com.dgtly.system.domain;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+/**
+ * @Author: csz
+ * @Date: 2020/7/2 19:13
+ */
+@Component
+public class ApplicationContextProvider implements ApplicationContextAware {
+    /**
+     * 上下文对象实例
+     */
+    private static ApplicationContext applicationContext;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+
+    /**
+     * 获取applicationContext
+     *
+     * @return
+     */
+    public static ApplicationContext getApplicationContext() {
+        return applicationContext;
+    }
+
+    /**
+     * 通过name获取 Bean.
+     *
+     * @param name
+     * @return
+     */
+    public static Object getBean(String name) {
+        return getApplicationContext().getBean(name);
+    }
+
+    /**
+     * 通过class获取Bean.
+     *
+     * @param clazz
+     * @param <T>
+     * @return
+     */
+    public static <T> T getBean(Class<T> clazz) {
+        return getApplicationContext().getBean(clazz);
+    }
+
+    /**
+     * 通过name,以及Clazz返回指定的Bean
+     *
+     * @param name
+     * @param clazz
+     * @param <T>
+     * @return
+     */
+    public static <T> T getBean(String name, Class<T> clazz) {
+        return getApplicationContext().getBean(name, clazz);
+    }
+}

+ 22 - 0
suishenbang-system/src/main/java/com/dgtly/system/mapper/HanaSalesOrderMapper.java

@@ -3,6 +3,7 @@ package com.dgtly.system.mapper;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
+import java.util.Map;
 
 public interface HanaSalesOrderMapper {
     /**
@@ -15,4 +16,25 @@ public interface HanaSalesOrderMapper {
     List<String> selectOrderBaseList();
 
     int  deleteLogic(@Param("list") List<String> list);
+
+    /**
+     * @description: 查询已冻结经销商(不在白名单中)且订单状态满足条件、尚未完成发货的要货记录 id 集合
+     * @return: java.util.List<java.lang.String>
+     */
+    List<String> selectFreezeCustomerOrderBaseList();
+
+    /**
+     * @description: 查询已冻结经销商(不在白名单中)且订单状态满足条件的要货记录详情列表,
+     *               每条记录包含 id、customerCode(经销商编码)、orderNumber(订单号)、confirmBy(要货人)
+     * @return: java.util.List<java.util.Map<java.lang.String, java.lang.Object>>
+     */
+    List<Map<String, Object>> selectFreezeCustomerOrderDetailList();
+
+    /**
+     * @description: 逻辑删除(取消)已冻结经销商的要货记录
+     * @param: list 要货记录 id 集合
+     * @return: int 影响行数
+     */
+    int cancelFreezeCustomerOrderLogic(@Param("list") List<String> list);
 }
+

+ 20 - 0
suishenbang-system/src/main/java/com/dgtly/system/service/HanaSalesOrderService.java

@@ -1,6 +1,9 @@
 package com.dgtly.system.service;
 
 
+import java.util.List;
+import java.util.Map;
+
 public interface HanaSalesOrderService {
     /**
      * @description: 未过信的经销商订单的要货记录id(做逻辑删除,重新要货)
@@ -10,4 +13,21 @@ public interface HanaSalesOrderService {
      * @date: 2023/6/16 14:35
      */
     void updateSalesOrderBaseNoCrditStatus();
+
+    /**
+     * @description: 交货冻结取货定时任务
+     *               对已冻结经销商(不在 meta_hana_not_freeze_customer 白名单中)
+     *               且满足以下任一条件、尚未完全发货的要货记录执行逻辑删除(取消):
+     *               1. 未复核金额 (NO_DELIVER_AMT) != 0
+     *               2. 冻结标识 (ZCMFRE) != ''
+     *               3. 信用状态 (ZCMGST) != ''
+     *               同时要求订单未完全发货(NO_DELIVER_AMT != 0),已完全发货的订单不处理。
+     *               执行后写入操作日志(经销商编码、订单号、要货人、取消时间)。
+     *               逻辑删除完成后,逐条调用 ESB 接口 ZSD_SSB_DELIV_INFO_IF 通知 SAP 取消要货,
+     *               TRANSPORT 字段固定传"交货冻结";单条 ESB 失败不影响整体流程,仅记录错误日志。
+     * @return: 已取消的要货记录详情列表(含 customerCode/orderNumber/confirmBy),供调用方做企微通知
+     */
+    List<Map<String, Object>> cancelFreezeCustomerOrder();
 }
+
+

+ 211 - 1
suishenbang-system/src/main/java/com/dgtly/system/service/impl/HanaSalesOrderServiceImpl.java

@@ -1,17 +1,54 @@
 package com.dgtly.system.service.impl;
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.dgtly.common.utils.StringUtils;
+import com.dgtly.common.utils.http.HttpUtils;
+import com.dgtly.system.domain.ApplicationContextProvider;
 import com.dgtly.system.mapper.HanaSalesOrderMapper;
 import com.dgtly.system.service.HanaSalesOrderService;
+import com.dgtly.system.util.HttpUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.retry.support.RetryTemplate;
 import org.springframework.stereotype.Service;
 
-import java.util.List;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static org.springframework.jdbc.datasource.init.DatabasePopulatorUtils.execute;
 
 @Service
 public class HanaSalesOrderServiceImpl implements HanaSalesOrderService {
 
+    private static final Logger log = LoggerFactory.getLogger(HanaSalesOrderServiceImpl.class);
+
+    /** 批量删除每批最大条数,避免 IN 语句过长 */
+    private static final int BATCH_SIZE = 500;
+
+    private String sapResult;
+
+    /** 通知 SAP 取消要货的 ESB 接口名称(固定值) */
+    private static final String ESB_SERVICE_NAME = "ZSD_SSB_DELIV_INFO_IF";
+
+    /** 取消要货时传给 SAP 的 TRANSPORT 字段值 */
+    private static final String CANCEL_TRANSPORT = "交货冻结";
+
     @Autowired
     private HanaSalesOrderMapper hanaSalesOrderMapper;
+
+    /** Spring Retry 重试模板 */
+    @Autowired
+    private RetryTemplate retryTemplate;
+
+    /** ESB 通用接口地址(从配置文件读取) */
+    @Value("${spring.esb.esbOrderCancelUrl}")
+    private String esbOrderCancelUrl;
+
     /**
      * @description: 未过信的经销商订单的要货记录id(做逻辑删除,重新要货)
      * @param: []
@@ -26,4 +63,177 @@ public class HanaSalesOrderServiceImpl implements HanaSalesOrderService {
             hanaSalesOrderMapper.deleteLogic(orderBaseList);
         }
     }
+
+    public String getSapResult() {
+        return sapResult;
+    }
+
+    /**
+     * @description: 交货冻结取货定时任务
+     *               查询已冻结经销商(不在白名单中)且订单状态满足条件、尚未完全发货的要货记录,
+     *               执行逻辑删除(is_delete = '1')以取消要货,并记录操作日志。
+     *               取消后逐条调用 ESB 接口 ZSD_SSB_DELIV_INFO_IF 通知 SAP,TRANSPORT 字段传"交货冻结"。
+     * @return: 已取消的要货记录详情列表(含 customerCode/orderNumber/confirmBy),供调用方做企微通知
+     */
+    @Override
+    public List<Map<String, Object>> cancelFreezeCustomerOrder() {
+        log.info("【交货冻结取货】定时任务开始执行");
+
+        // 先查详情,用于日志记录和企微通知(含 customerCode/orderNumber/confirmBy)
+        List<Map<String, Object>> detailList = hanaSalesOrderMapper.selectFreezeCustomerOrderDetailList();
+        if (detailList == null || detailList.isEmpty()) {
+            log.info("【交货冻结取货】未查询到需要取消的要货记录,任务结束");
+            return Collections.emptyList();
+        }
+
+        // 提取 id 列表
+        List<String> orderIdList = detailList.stream()
+                .map(m -> String.valueOf(m.get("id")))
+                .collect(Collectors.toList());
+
+        log.info("【交货冻结取货】共查询到需要取消的要货记录 {} 条,开始分批执行逻辑删除", orderIdList.size());
+        int total = orderIdList.size();
+        int deleted = 0;
+        for (int i = 0; i < total; i += BATCH_SIZE) {
+            List<String> batch = orderIdList.subList(i, Math.min(i + BATCH_SIZE, total));
+            int rows = hanaSalesOrderMapper.cancelFreezeCustomerOrderLogic(batch);
+            deleted += rows;
+            log.info("【交货冻结取货】已处理批次 {}/{}, 本批影响行数: {}",
+                    (i / BATCH_SIZE + 1), (int) Math.ceil((double) total / BATCH_SIZE), rows);
+        }
+        log.info("【交货冻结取货】逻辑删除完成,共取消要货记录 {} 条", deleted);
+
+        // ---- 操作日志:按经销商分组输出结构化日志 ----
+        String cancelTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
+        Map<String, List<String>> customerOrderMap = new LinkedHashMap<>();
+        Map<String, Set<String>> customerConfirmByMap = new LinkedHashMap<>();
+        for (Map<String, Object> detail : detailList) {
+            String customerCode = String.valueOf(detail.get("customerCode"));
+            String orderNumber  = String.valueOf(detail.get("orderNumber"));
+            String confirmBy    = detail.get("confirmBy") != null ? String.valueOf(detail.get("confirmBy")) : "";
+            customerOrderMap.computeIfAbsent(customerCode, k -> new ArrayList<>()).add(orderNumber);
+            customerConfirmByMap.computeIfAbsent(customerCode, k -> new LinkedHashSet<>()).add(confirmBy);
+        }
+        for (Map.Entry<String, List<String>> entry : customerOrderMap.entrySet()) {
+            String customerCode = entry.getKey();
+            List<String> orderNumbers = entry.getValue();
+            Set<String> confirmBys = customerConfirmByMap.get(customerCode);
+            log.info("【交货冻结取货-操作日志】 取消时间={}, 经销商编码={}, 要货人={}, 取消订单号={}",
+                    cancelTime, customerCode,
+                    String.join(",", confirmBys),
+                    String.join(",", orderNumbers));
+        }
+
+        // ---- 逐条通知 SAP:调用 ESB 接口 ZSD_SSB_DELIV_INFO_IF ----
+        log.info("【交货冻结取货】开始通知 SAP,共 {} 条,ESB接口={}", detailList.size(), ESB_SERVICE_NAME);
+        int esbSuccess = 0;
+        int esbFail    = 0;
+        Set<String> orderNumberSet = new HashSet<>();
+        for (Map<String, Object> detail : detailList) {
+            String orderNumber = String.valueOf(detail.get("orderNumber"));
+            try {
+                //调用sap不重复发
+                if (orderNumberSet.add(orderNumber)){
+                    notifySapCancelOrder(orderNumber);
+                    esbSuccess++;
+                }
+            } catch (Exception e) {
+                esbFail++;
+                log.error("【交货冻结取货】ESB 通知 SAP 失败,订单号={}", orderNumber, e);
+            }
+        }
+        log.info("【交货冻结取货】ESB 通知 SAP 完成,成功={} 条,失败={} 条", esbSuccess, esbFail);
+
+        return detailList;
+    }
+
+
+
+    /**
+     * 调用 ESB 接口 ZSD_SSB_DELIV_INFO_IF 通知 SAP 取消指定要货订单。
+     * TRANSPORT 字段固定传"交货冻结"。
+     *
+     * @param orderNumber 要取消的订单号(sales_order_base.order_number)
+     */
+    private  void notifySapCancelOrder(String orderNumber) {
+        String uuidStr = UUID.randomUUID().toString();
+
+        // 请求头
+        Map<String, String> headers = new HashMap<>(8);
+        headers.put("Content-Type", "application/json");
+        headers.put("requestId",    uuidStr);
+        headers.put("trackId",      uuidStr);
+        headers.put("sourceSystem", "SSB");
+        headers.put("serviceName",  "S_SSB_ERP_ZsdSsbDelivInfoIf_S");
+
+        List<Map<String, String>> params =new ArrayList<>();
+        Map<String, String> maps = new HashMap<>(7);
+        maps.put("VBELN", orderNumber);
+        maps.put("POSNR", "000000");
+        maps.put("KWMENG_2", "0");
+        maps.put("KWMENG_1", "0");
+        maps.put("ZYZH", "system");
+        maps.put("DATE", format(new Date(), "yyyyMMdd"));
+        maps.put("TIME", format(new Date(), "HHmmss"));
+        maps.put("TRANSPORT",  CANCEL_TRANSPORT);
+        params.add( maps);
+        Map<String, List<Map<String, String>>> item = new HashMap<>(1);
+        item.put("item", params);
+        Map<String, Map<String, List<Map<String, String>>>> itData = new HashMap<>(1);
+        itData.put("IT_DATA", item);
+        Map<String, Map<String, Map<String, List<Map<String, String>>>>> zsdSsbDelivInfoIf = new HashMap<>(1);
+        zsdSsbDelivInfoIf.put("ZSD_SSB_DELIV_INFO_IF", itData);
+
+        log.info("【交货冻结取货】ESB 通知 SAP,订单号={},请求报文={}", orderNumber, params);
+        boolean execute = false;
+        try {
+            execute = retry("http://esb-test.nipponpaint.com.cn/esb/erp/api", headers, zsdSsbDelivInfoIf);
+        } catch (Exception e) {
+            log.error("[{}] retry error,  headers is [{}], params is [{}]", "http://esb-test.nipponpaint.com.cn/esb/erp/api", JSON.toJSONString(headers), JSON.toJSONString(zsdSsbDelivInfoIf), e);
+        }
+        log.info("【交货冻结取货】ESB 响应,订单号={},响应={}", orderNumber, execute);
+
+//        // 解析响应,非成功时抛出异常由调用方记录
+//        JSONObject jsonResult = JSONObject.parseObject(result);
+//        if (jsonResult != null && jsonResult.containsKey("code")) {
+//            Object code = jsonResult.get("code");
+//            if (!"0".equals(String.valueOf(code)) && !"200".equals(String.valueOf(code))) {
+//                throw new RuntimeException("ESB 返回非成功状态,code=" + code
+//                        + ",msg=" + jsonResult.getString("msg"));
+//            }
+//        }
+    }
+    public static String format(Date date, String format) {
+        if (null != date && !StringUtils.isEmpty(format)) {
+            SimpleDateFormat sdf = new SimpleDateFormat(format);
+            return sdf.format(date);
+        } else {
+            return null;
+        }
+    }
+    public boolean retry(String url, Map<String, String> headers, Map<String, Map<String, Map<String, List<Map<String, String>>>>> params) throws Exception {
+        return retryTemplate.execute(
+                context -> execute(url, headers, params),
+                context -> {
+                    log.error("[{}] final request retry fail , headers is [{}], params is [{}]", url, JSON.toJSONString(headers), JSON.toJSONString(params), context.getLastThrowable());
+                    return false;
+                });
+    }
+    private boolean execute(String url, Map<String, String> headers, Map<String, Map<String, Map<String, List<Map<String, String>>>>> params) throws Exception {
+        String returnString = HttpUtil.executePostBodyJsonParamRetry(url, headers, params);
+        log.info("SendOrderExpectedTimeToSapTask headers = [{}],  params = [{}], returnString = [{}]", headers, params, returnString);
+        this.sapResult = returnString;
+        JSONObject jsonObject = JSON.parseObject(returnString);
+        JSONObject zsdSsbDelivInfoIfResponse = jsonObject.getJSONObject("ZSD_SSB_DELIV_INFO_IFResponse");
+        String code = zsdSsbDelivInfoIfResponse.getString("EV_FLAG");
+        return org.apache.commons.lang3.StringUtils.equals("S", code);
+    }
+
+//    public static void main(String[] args) {
+//        notifySapCancelOrder("1231111111");
+//    }
+
+
 }
+
+

+ 165 - 0
suishenbang-system/src/main/java/com/dgtly/system/util/HttpUtil.java

@@ -0,0 +1,165 @@
+package com.dgtly.system.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.*;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @Author: csz
+ * @Date: 2020/7/15 16:39
+ * @Modified: 2026/05/09 使用Spring RestTemplate替代OkHttp,解决NoClassDefFoundError问题
+ */
+public class HttpUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(HttpUtil.class);
+    private static RestTemplate restTemplate;
+
+    static {
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(30000);  // 连接超时30秒
+        factory.setReadTimeout(30000);     // 读取超时30秒
+        restTemplate = new RestTemplate(factory);
+    }
+
+    public static String executePostBodyJsonParamList(String url, Map<String, String> headers, Map<String, String> params) throws Exception {
+        List<Map<String, String>> maps = Arrays.asList(params);
+        return executePostBodyJsonParam(url, headers, maps);
+    }
+
+    public static <T> String executePostBodyJsonParam(String url, Map<String, String> headers, T params) throws Exception {
+        HttpHeaders httpHeaders = new HttpHeaders();
+        if (headers != null && headers.size() > 0) {
+            for (Map.Entry<String, String> entry : headers.entrySet()) {
+                httpHeaders.add(entry.getKey(), entry.getValue());
+            }
+        }
+        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+
+        String jsonBody = JSON.toJSONString(params, SerializerFeature.WriteMapNullValue);
+        HttpEntity<String> entity = new HttpEntity<>(jsonBody, httpHeaders);
+
+        log.info("ESB请求 url = [{}], headers = [{}], params = [{}]", url, JSON.toJSONString(headers), jsonBody);
+        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
+        String body = response.getBody();
+        log.info("ESB响应 returnString = [{}]", body);
+        return body;
+    }
+
+    public static <T> String executePostBodyJsonParamRetry(String url, Map<String, String> headers, T params) throws Exception {
+        // 简单重试逻辑:重试3次
+        int maxRetries = 3;
+        Exception lastException = null;
+        
+        for (int i = 0; i < maxRetries; i++) {
+            try {
+                log.info("ESB重发次数 headers = [{}], params = [{}], count = [{}]", headers, params, i + 1);
+                return executePostBodyJsonParam(url, headers, params);
+            } catch (Exception e) {
+                lastException = e;
+                log.warn("ESB请求失败,第{}次重试", i + 1, e);
+                if (i < maxRetries - 1) {
+                    Thread.sleep(1000); // 重试间隔1秒
+                }
+            }
+        }
+        throw new Exception("ESB请求失败,已重试" + maxRetries + "次", lastException);
+    }
+
+    public static String executePostFormParam(String url, Map<String, String> headers, Map<String, String> params) {
+        HttpHeaders httpHeaders = new HttpHeaders();
+        if (headers != null && headers.size() > 0) {
+            for (Map.Entry<String, String> entry : headers.entrySet()) {
+                httpHeaders.add(entry.getKey(), entry.getValue());
+            }
+        }
+        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+
+        // 构建form参数
+        String formBody = params.entrySet().stream()
+                .map(entry -> entry.getKey() + "=" + entry.getValue())
+                .collect(Collectors.joining("&"));
+
+        HttpEntity<String> entity = new HttpEntity<>(formBody, httpHeaders);
+
+        String returnString = null;
+        try {
+            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
+            returnString = response.getBody();
+            log.info("onResponse is {}", returnString);
+        } catch (Exception e) {
+            log.error("[{}] request error , heads is [{}], params is [{}]", url, JSON.toJSONString(headers), JSON.toJSONString(params), e);
+        }
+        return returnString;
+    }
+
+
+    public static String executePostJsonParam(String url, Map<String, String> headers, Map<String, String> params) {
+        HttpHeaders httpHeaders = new HttpHeaders();
+        if (headers != null && headers.size() > 0) {
+            for (Map.Entry<String, String> entry : headers.entrySet()) {
+                httpHeaders.add(entry.getKey(), entry.getValue());
+            }
+        }
+        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+
+        String paramJson = JSON.toJSONString(params, SerializerFeature.WriteMapNullValue);
+        HttpEntity<String> entity = new HttpEntity<>(paramJson, httpHeaders);
+
+        String returnString = null;
+        try {
+            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
+            returnString = response.getBody();
+            log.info("onResponse is {}", returnString);
+        } catch (Exception e) {
+            log.error("[{}] request error , heads is [{}], params is [{}]", url, JSON.toJSONString(headers), JSON.toJSONString(params), e);
+        }
+        return returnString;
+    }
+
+    /**
+     * @description: get方法
+     * @param: [url, headers, params]
+     * @return: java.lang.String
+     * @author: njs     
+     * @date: 2025/6/17 13:26
+     */
+    public static String executeGet(String url, Map<String, String> headers, Map<String, String> params) {
+        // 拼接GET参数
+        if (params != null && params.size() > 0) {
+            List<String> noticeParams = params.entrySet().stream()
+                    .map(entry -> entry.getKey() + "=" + entry.getValue())
+                    .collect(Collectors.toList());
+            url = url + "?" + StringUtils.join(noticeParams, "&");
+        }
+        log.info("url is [{}]", url);
+
+        HttpHeaders httpHeaders = new HttpHeaders();
+        if (headers != null && headers.size() > 0) {
+            for (Map.Entry<String, String> entry : headers.entrySet()) {
+                httpHeaders.add(entry.getKey(), entry.getValue());
+            }
+        }
+
+        HttpEntity<String> entity = new HttpEntity<>("", httpHeaders);
+
+        String returnString = null;
+        try {
+            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
+            returnString = response.getBody();
+            log.info("onResponse is {}", returnString);
+        } catch (Exception e) {
+            log.warn("request is error, url =[{}]", url, e);
+        }
+        return returnString;
+    }
+}

+ 78 - 0
suishenbang-system/src/main/resources/mapper/system/HanaSalesOrderMapper.xml

@@ -24,4 +24,82 @@
             </foreach>
         </if>
     </update>
+
+    <!--
+        查询已冻结经销商(不在白名单 meta_hana_not_freeze_customer 中)
+        且满足以下任一条件的要货记录 id(未完成发货):
+          1. 未复核金额(NO_DELIVER_AMT) != 0
+          2. 未清(NO_DELIVER_AMT 对应已进入未清状态) 通过 ZCMFRE(冻结标识) != '' 判断
+          3. 冻结(ZCMFRE) != ''
+        且订单尚未完全发货(NO_DELIVER_AMT != 0,仍有未发货金额)
+        对已完全发货的订单(NO_DELIVER_AMT = 0)不处理
+    -->
+    <select id="selectFreezeCustomerOrderBaseList" resultType="java.lang.String">
+        SELECT
+            ob.id
+        FROM
+            sales_order_base ob
+        INNER JOIN meta_hana_sales_order so ON so.DOC_NUMBER = ob.order_number
+        LEFT JOIN meta_hana_deliver_order sod ON sod.DOC_NUMBER = so.DOC_NUMBER
+        WHERE
+            ob.is_delete = '0'
+            AND ob.belong_to NOT IN (
+                SELECT customer_code FROM meta_hana_not_freeze_customer
+            )
+            AND (
+                (so.NO_DELIVER_AMT IS NOT NULL AND so.NO_DELIVER_AMT != 0)
+                OR (sod.NET_VALUE IS NOT NULL AND sod.NET_VALUE != 0)
+                OR (so.NO_DELIVER_AMT IS NOT NULL AND so.NO_DELIVER_AMT != 0)
+            )
+            AND so.NO_DELIVER_AMT != 0
+        GROUP BY
+            ob.id
+    </select>
+
+    <!--
+        查询已冻结经销商(不在白名单中)且订单状态满足条件的要货记录详情列表,
+        返回经销商编码、订单号、要货人,用于操作日志和企业微信通知
+
+        业务条件(任一满足即可):
+          1. NO_DELIVER_AMT != 0  —— 仍有未发货金额
+          2. NET_VALUE != 0        —— 交付单已复核(进入已清状态)
+        且 NO_DELIVER_AMT != 0(额外兜底,防止已完全发货的订单被误处理)
+
+        优化点:
+        1. NOT IN 子查询改为 LEFT JOIN + WHERE IS NULL(anti-join,可利用索引)
+        2. 去掉 GROUP BY(id 是主键,JOIN 结果天然唯一,无需分组)
+        3. 合并重复的 NO_DELIVER_AMT 条件
+        4. 去掉 GROUP BY 后的 WHERE 末尾 AND so.NO_DELIVER_AMT != 0 条件仍然保留(逻辑要求)
+    -->
+    <select id="selectFreezeCustomerOrderDetailList" resultType="java.util.Map">
+        SELECT
+            ob.id,
+            ob.belong_to   AS customerCode,
+            ob.order_number AS orderNumber,
+            ob.confirm_by  AS confirmBy
+        FROM
+            sales_order_base ob
+        INNER JOIN meta_hana_sales_order so ON so.DOC_NUMBER = ob.order_number
+        LEFT JOIN meta_hana_deliver_order sod ON sod.DOC_NUMBER = so.DOC_NUMBER
+        LEFT JOIN meta_hana_not_freeze_customer b ON b.customer_code = ob.belong_to
+        WHERE
+            ob.is_delete = '0'
+            AND b.customer_code IS NULL
+            AND (
+                (so.NO_DELIVER_AMT IS NOT NULL AND so.NO_DELIVER_AMT != 0)
+                OR (sod.NET_VALUE IS NOT NULL AND sod.NET_VALUE != 0)
+            )
+    </select>
+
+    <update id="cancelFreezeCustomerOrderLogic">
+        UPDATE sales_order_base
+        SET is_delete = '1'
+        WHERE 1=1
+        <if test="list != null and list.size() > 0"> AND id IN
+            <foreach collection="list" item="id" index="index" open="(" close=")" separator=",">
+                #{id}
+            </foreach>
+        </if>
+    </update>
+
 </mapper>

+ 12 - 0
suishenbang-wxportal/suishenbang-wxportal-common/src/main/java/com/dgtly/wxportal/service/IWxSendMessageService.java

@@ -2,6 +2,7 @@ package com.dgtly.wxportal.service;
 
 import com.dgtly.wxportal.domain.WxSendMessage;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 企业微信发送消息历史记录Service接口
@@ -76,4 +77,15 @@ public interface IWxSendMessageService
       * @date: 2024/2/4 9:21
       */
      void sendMailAndWxEarlyTucMessage();
+
+    /**
+     * @description: 交货冻结取消要货企微消息通知
+     *               取消后向经销商负责人、要货人、对应销售推送通知:
+     *               "因经销商交货冻结,已取消以下订单的要货,如需重新要货,请解冻后再操作:[订单号列表]"
+     * @param canceledOrderList 已取消的要货记录详情列表(含 customerCode/orderNumber/confirmBy)
+     * @return: void
+     * @author: njs
+     */
+     void sendFreezeCustomerCancelOrderMessage(List<Map<String, Object>> canceledOrderList);
 }
+

+ 95 - 26
suishenbang-wxportal/suishenbang-wxportal-common/src/main/java/com/dgtly/wxportal/service/impl/WxSendMessageServiceImpl.java

@@ -3,17 +3,14 @@ package com.dgtly.wxportal.service.impl;
 import java.math.BigDecimal;
 import java.util.*;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 import com.dgtly.common.utils.DateUtils;
 import com.dgtly.system.domain.Mail;
 import com.dgtly.system.domain.OrderTucEarlyWarning;
 import com.dgtly.system.domain.SysDictData;
-import com.dgtly.system.domain.UserVO;
 import com.dgtly.system.mapper.OrderTucEarlyWarningMapper;
 import com.dgtly.system.mapper.SysBatchSignForMapper;
 import com.dgtly.system.mapper.SysUserMapper;
-import com.dgtly.system.service.IOrderTucEarlyWarningService;
 import com.dgtly.system.service.ISysConfigService;
 import com.dgtly.system.service.ISysDictDataService;
 import com.dgtly.system.util.MailUtils;
@@ -64,6 +61,7 @@ public class WxSendMessageServiceImpl implements IWxSendMessageService
     public  String myEmailPassword;
     @Value(value = "${spring.mail.host}")
     public  String myEmailSMTPHost;
+
     /**
      * 查询企业微信发送消息历史记录
      * 
@@ -155,7 +153,6 @@ public class WxSendMessageServiceImpl implements IWxSendMessageService
                  ) {
                 String user =null;
                 String deliverNumber =null;
-                String orderFormat =null;
                 String customerName = userMapper.getCustomNameByCode(customerId);
                 Map<String, Map<String,Object>> maps = userMapper.selectLoginNamesByOrgCodeSelf(customerId);
                 Iterator<Map.Entry<String, Object>> iterator = tmsMap.get(customerId).entrySet().iterator();
@@ -261,6 +258,90 @@ public class WxSendMessageServiceImpl implements IWxSendMessageService
         }
     }
 
+    /**
+     * @description: 交货冻结取消要货企微消息通知
+     *               取消后按经销商分组,向经销商侧负责人/要货人 + 对应销售推送通知。
+     *               消息内容:
+     *               "因经销商交货冻结,已取消以下订单的要货,如需重新要货,请解冻后再操作:[订单号列表]"
+     * @param canceledOrderList 已取消的要货记录详情列表(含 customerCode/orderNumber/confirmBy)
+     */
+    @Override
+    public void sendFreezeCustomerCancelOrderMessage(List<Map<String, Object>> canceledOrderList) {
+        if (canceledOrderList == null || canceledOrderList.isEmpty()) {
+            return;
+        }
+        // 按经销商编码分组,收集订单号和要货人
+        Map<String, List<String>> customerOrderMap = new LinkedHashMap<>();
+        Map<String, Set<String>> customerConfirmByMap = new LinkedHashMap<>();
+        for (Map<String, Object> detail : canceledOrderList) {
+            String customerCode = String.valueOf(detail.get("customerCode"));
+            String orderNumber  = String.valueOf(detail.get("orderNumber"));
+            String confirmBy    = detail.get("confirmBy") != null ? String.valueOf(detail.get("confirmBy")) : "";
+            customerOrderMap.computeIfAbsent(customerCode, k -> new ArrayList<>()).add(orderNumber);
+            customerConfirmByMap.computeIfAbsent(customerCode, k -> new LinkedHashSet<>()).add(confirmBy);
+        }
+
+        for (Map.Entry<String, List<String>> entry : customerOrderMap.entrySet()) {
+            String customerCode  = entry.getKey();
+            List<String> orderNumbers = entry.getValue();
+            Set<String> confirmBys    = customerConfirmByMap.get(customerCode);
+
+            // 拼装通知内容
+            String orderList = String.join("、", orderNumbers);
+            String msgContent = "因经销商交货冻结,已取消以下订单的要货,如需重新要货,请解冻后再操作:" + orderList;
+
+            // 收集收件人(用 | 分隔,企微支持多人合并)
+            Set<String> toUserSet = new LinkedHashSet<>();
+
+            try {
+                // 1. 经销商侧负责人
+                Map<String, Map<String, Object>> cusUserMap =
+                        userMapper.selectLoginNamesByOrgCodeSelf(customerCode);
+                if (cusUserMap != null && cusUserMap.containsKey(customerCode)) {
+                    Object touser = cusUserMap.get(customerCode).get("touser");
+                    if (touser != null && StringUtils.isNotBlank(touser.toString())) {
+                        Arrays.stream(touser.toString().split("\\|"))
+                              .filter(StringUtils::isNotBlank)
+                              .forEach(toUserSet::add);
+                    }
+                }
+
+                // 2. 要货人(confirm_by 即 sys_user.login_name)
+                for (String confirmBy : confirmBys) {
+                    if (StringUtils.isNotBlank(confirmBy)) {
+                        toUserSet.add(confirmBy);
+                    }
+                }
+
+                // 3. 对应销售
+                String salesLoginName = userMapper.selectLoginNameByCustomerCode(customerCode);
+                if (StringUtils.isNotBlank(salesLoginName)) {
+                    Arrays.stream(salesLoginName.split("\\|"))
+                          .filter(StringUtils::isNotBlank)
+                          .forEach(toUserSet::add);
+                }
+            } catch (Exception e) {
+                // 查询收件人失败不阻断主流程
+                org.slf4j.LoggerFactory.getLogger(WxSendMessageServiceImpl.class)
+                    .error("【交货冻结取货】查询企微通知收件人失败,经销商编码={}", customerCode, e);
+            }
+
+            if (toUserSet.isEmpty()) {
+                org.slf4j.LoggerFactory.getLogger(WxSendMessageServiceImpl.class)
+                    .warn("【交货冻结取货】经销商 {} 未找到任何企微收件人,跳过通知", customerCode);
+                continue;
+            }
+
+            // 发送(|分隔多人)
+            String toUserStr = String.join("|", toUserSet);
+            qyWxSendMessageUtil.sendMsgSelfToCustomer(toUserStr, msgContent, "50");
+
+            org.slf4j.LoggerFactory.getLogger(WxSendMessageServiceImpl.class)
+                .info("【交货冻结取货】已向经销商 {} 相关人员 [{}] 发送企微通知,订单:{}",
+                        customerCode, toUserStr, orderList);
+        }
+    }
+
     public Set<String> getConfigValueSet(String dictType){
         Set<String> res = new HashSet<>();
         List<SysDictData> dictDatas = sysDictDataService.selectSimpleDictDataByType(dictType);
@@ -284,6 +365,7 @@ public class WxSendMessageServiceImpl implements IWxSendMessageService
         }
         return res;
     }
+
     void sendQw(BigDecimal tucValue,BigDecimal oldValue,int type,String orgCode,String orgName){
         String sendQwUser = getConfigValueUserAccount("sendQwEarlyWarning");
         if(sendQwUser !=null && !("").equals(sendQwUser) ){
@@ -296,41 +378,32 @@ public class WxSendMessageServiceImpl implements IWxSendMessageService
                 qyWxSendMessageUtil.sendMsgSelfToCustomer(sendQwUser,messageDetail, "66");
             }
         }
-
     }
+
     void sendMail(BigDecimal tucValue,BigDecimal oldValue,int type,String orgCode,String orgName){
         // 1. 创建参数配置, 用于连接邮件服务器的参数配置
-        Properties props = new Properties();                    // 参数配置
-        props.setProperty("mail.transport.protocol", "smtp");   // 使用的协议(JavaMail规范要求)
-        props.setProperty("mail.smtp.host", myEmailSMTPHost);   // 发件人的邮箱的 SMTP 服务器地址
+        Properties props = new Properties();
+        props.setProperty("mail.transport.protocol", "smtp");
+        props.setProperty("mail.smtp.host", myEmailSMTPHost);
         props.setProperty("mail.smtp.port", "25");
-        props.setProperty("mail.smtp.auth", "true");            // 需要请求认证
-        props.setProperty("mail.smtp.starttls.enable", "false");            // 需要请求认证
-        props.setProperty("mail.smtp.ssl.enable", "false");            // 需要请求认证
+        props.setProperty("mail.smtp.auth", "true");
+        props.setProperty("mail.smtp.starttls.enable", "false");
+        props.setProperty("mail.smtp.ssl.enable", "false");
         Set<String> sendEmailUser = getConfigValueSet("sendMailEarlyWarning");
         Session session = Session.getDefaultInstance(props);
         session.setDebug(true);
 
-        for (String email : sendEmailUser
-        ) {
+        for (String email : sendEmailUser) {
             Mail mail = new Mail();
             try {
                 mail.setFrom(myEmailAccount);
                 mail.setTo(email);
                 mail.setSubject("立邦随身邦业绩进度数据源预警");
                 mail.setContent(getHtmlUserContextList(tucValue,oldValue,type,orgCode,orgName));
-                //mailUtils.sendMailHtml(mailFromUsername, email, "用户账号重复通知", "您好,重复用户信息如下<br>" + userList);
                 MimeMessage message = mailUtils.createMimeMessage(session, myEmailAccount, email, mail);
-
-                // 4. 根据 Session 获取邮件传输对象
                 Transport transport = session.getTransport();
-
                 transport.connect(myEmailAccount, myEmailPassword);
-
-                // 6. 发送邮件, 发到所有的收件地址, message.getAllRecipients() 获取到的是在创建邮件对象时添加的所有收件人, 抄送人, 密送人
                 transport.sendMessage(message, message.getAllRecipients());
-
-                // 7. 关闭连接
                 transport.close();
             } catch (NoSuchProviderException e) {
                 e.printStackTrace();
@@ -340,11 +413,9 @@ public class WxSendMessageServiceImpl implements IWxSendMessageService
                 e.printStackTrace();
             }
         }
-
-}
+    }
 
     public String getHtmlUserContextList(BigDecimal tucValue,BigDecimal oldValue,int type,String orgCode,String orgName) {
-
         String str = "";
         String html ="";
         if(type == 1){
@@ -366,7 +437,6 @@ public class WxSendMessageServiceImpl implements IWxSendMessageService
                     "    </tr>" +
                     str +
                     "</table>";
-
         }
 
         if(type == 2){
@@ -388,7 +458,6 @@ public class WxSendMessageServiceImpl implements IWxSendMessageService
                     "    </tr>" +
                     str +
                     "</table>";
-
         }
 
         return html;