Procházet zdrojové kódy

feat(order): 增强订单过期延迟消息处理逻辑

- 优化订单过期时间解析逻辑,支持多种日期格式
- 增加空值检查和异常处理,提升系统健壮性
- 添加延迟时间溢出检查,防止int类型溢出
- 引入CompletableFuture实现异步消息发送
- 配置自定义线程池执行异步任务
- 完善日志记录,增加处理成功/失败计数统计
- 支持webp、flv、mkv、gz、json等多种文件类型白名单
- 增强文件类型检测逻辑,添加读取字节检查和异常降级处理
- 优化文件上传下载类型校验日志,提高可追踪性
wzq před 12 hodinami
rodič
revize
5f4a96b755

+ 53 - 19
national-motion-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java

@@ -34,6 +34,7 @@ public class SsrfFileTypeFilter {
         FILE_TYPE_WHITE_LIST.add("bmp");
         FILE_TYPE_WHITE_LIST.add("svg");
         FILE_TYPE_WHITE_LIST.add("ico");
+        FILE_TYPE_WHITE_LIST.add("webp");
 
         //文本文件
         FILE_TYPE_WHITE_LIST.add("txt");
@@ -51,6 +52,8 @@ public class SsrfFileTypeFilter {
         FILE_TYPE_WHITE_LIST.add("wmv");
         FILE_TYPE_WHITE_LIST.add("mp3");
         FILE_TYPE_WHITE_LIST.add("wav");
+        FILE_TYPE_WHITE_LIST.add("flv");
+        FILE_TYPE_WHITE_LIST.add("mkv");
 
         //表格文件
         FILE_TYPE_WHITE_LIST.add("xls");
@@ -61,6 +64,7 @@ public class SsrfFileTypeFilter {
         FILE_TYPE_WHITE_LIST.add("rar");
         FILE_TYPE_WHITE_LIST.add("7z");
         FILE_TYPE_WHITE_LIST.add("tar");
+        FILE_TYPE_WHITE_LIST.add("gz");
 
         //app文件后缀
         FILE_TYPE_WHITE_LIST.add("apk");
@@ -70,6 +74,14 @@ public class SsrfFileTypeFilter {
         FILE_TYPE_WHITE_LIST.add("ppt");
         FILE_TYPE_WHITE_LIST.add("pptx");
 
+        //其他常用文件类型
+        FILE_TYPE_WHITE_LIST.add("json");
+        FILE_TYPE_WHITE_LIST.add("xml");
+        FILE_TYPE_WHITE_LIST.add("html");
+        FILE_TYPE_WHITE_LIST.add("htm");
+        FILE_TYPE_WHITE_LIST.add("css");
+        FILE_TYPE_WHITE_LIST.add("js");
+        
         //设置禁止文件的头部标记
         FILE_TYPE_MAP.put("3c25402070616765206c", "jsp");
         FILE_TYPE_MAP.put("3c3f7068700a0a2f2a2a0a202a205048", "php");
@@ -148,11 +160,14 @@ public class SsrfFileTypeFilter {
     public static void checkDownloadFileType(String filePath) throws IOException {
         //文件后缀
         String suffix = getFileTypeBySuffix(filePath);
-        log.info("suffix:{}", suffix);
+        log.info("检查下载文件类型,文件路径: {}, 文件类型: {}", filePath, suffix);
         boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
         //是否允许下载的文件
         if (!isAllowExtension) {
+            log.error("下载失败,存在非法文件类型,文件路径: {}, 文件类型: {}", filePath, suffix);
             throw new IOException("下载失败,存在非法文件类型:" + suffix);
+        } else {
+            log.info("下载文件类型校验通过,文件路径: {}, 文件类型: {}", filePath, suffix);
         }
     }
 
@@ -166,11 +181,14 @@ public class SsrfFileTypeFilter {
         //获取文件真是后缀
         String suffix = getFileType(file);
 
-        log.info("suffix:{}", suffix);
+        log.info("检查上传文件类型,文件名: {}, 检测到的文件类型: {}", file.getOriginalFilename(), suffix);
         boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
         //是否允许下载的文件
         if (!isAllowExtension) {
+            log.error("上传失败,存在非法文件类型,文件名: {}, 文件类型: {}", file.getOriginalFilename(), suffix);
             throw new Exception("上传失败,存在非法文件类型:" + suffix);
+        } else {
+            log.info("文件类型校验通过,文件名: {}, 文件类型: {}", file.getOriginalFilename(), suffix);
         }
     }
 
@@ -190,36 +208,52 @@ public class SsrfFileTypeFilter {
             //is = new FileInputStream(file);
             is = file.getInputStream();
             byte[] b = new byte[10];
-            is.read(b, 0, b.length);
-            String fileTypeHex = String.valueOf(bytesToHexString(b));
-            Iterator<String> keyIter = FILE_TYPE_MAP.keySet().iterator();
-            while (keyIter.hasNext()) {
-                String key = keyIter.next();
-                // 验证前5个字符比较
-                if (key.toLowerCase().startsWith(fileTypeHex.toLowerCase().substring(0, 5))
-                        || fileTypeHex.toLowerCase().substring(0, 5).startsWith(key.toLowerCase())) {
-                    fileExtendName = FILE_TYPE_MAP.get(key);
-                    break;
+            int readBytes = is.read(b, 0, b.length);
+            
+            // 添加检查,确保读取到了数据
+            if (readBytes > 0) {
+                String fileTypeHex = String.valueOf(bytesToHexString(b));
+                Iterator<String> keyIter = FILE_TYPE_MAP.keySet().iterator();
+                while (keyIter.hasNext()) {
+                    String key = keyIter.next();
+                    // 验证前5个字符比较
+                    if (key.toLowerCase().startsWith(fileTypeHex.toLowerCase().substring(0, 5))
+                            || fileTypeHex.toLowerCase().substring(0, 5).startsWith(key.toLowerCase())) {
+                        fileExtendName = FILE_TYPE_MAP.get(key);
+                        break;
+                    }
                 }
+                log.info("-----获取到的指定文件类型------"+fileExtendName);
             }
-            log.info("-----获取到的指定文件类型------"+fileExtendName);
+            
             // 如果不是上述类型,则判断扩展名
             if (StringUtils.isBlank(fileExtendName)) {
                 String fileName = file.getOriginalFilename();
                 // 如果无扩展名,则直接返回空串
-                if (-1 == fileName.indexOf(".")) {
+                if (fileName == null || -1 == fileName.indexOf(".")) {
+                    log.warn("文件没有扩展名或文件名为空: {}", fileName);
                     return "";
                 }
                 // 如果有扩展名,则返回扩展名
-                return getFileTypeBySuffix(fileName);
+                String suffix = getFileTypeBySuffix(fileName);
+                log.info("通过文件扩展名获取到的文件类型: {}", suffix);
+                return suffix;
             }
-            log.info("-----最終的文件类型------"+fileExtendName);
-            is.close();
+            log.info("-----mercial的文件类型------"+fileExtendName);
             return fileExtendName;
         } catch (Exception e) {
-            log.error(e.getMessage(), e);
+            log.error("获取文件类型时发生异常: ", e);
+            // 即使出现异常也尝试通过文件扩展名获取类型
+            try {
+                String fileName = file.getOriginalFilename();
+                if (fileName != null && fileName.indexOf(".") != -1) {
+                    return getFileTypeBySuffix(fileName);
+                }
+            } catch (Exception ex) {
+                log.error("通过文件扩展名获取文件类型时也发生异常: ", ex);
+            }
             return "";
-        }finally {
+        } finally {
             if (is != null) {
                 is.close();
             }

+ 151 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/service/WeChatPayService.java

@@ -15,12 +15,16 @@ import org.jeecg.common.constant.CommonConstant;
 import org.jeecg.common.exception.JeecgBootException;
 import org.jeecg.modules.pay.config.*;
 import org.jeecg.modules.pay.serverPay.WXPayUtility;
+import org.jeecg.modules.rabbitmq.DelayedMessageService;
 import org.jeecg.modules.system.app.dto.receiptPaymentDetails.ReceiptPaymentDetailsInfoVo;
 import org.jeecg.modules.system.app.entity.*;
 import org.jeecg.modules.system.app.mapper.*;
 import org.jeecg.modules.system.entity.SysDepart;
 import org.jeecg.modules.system.mapper.SysDepartMapper;
 import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -38,6 +42,7 @@ import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -195,6 +200,148 @@ public class WeChatPayService {
 
     }
 
+
+    @Autowired
+    @Qualifier("businessTaskExecutor")
+    private ThreadPoolTaskExecutor businessTaskExecutor;
+
+    @Resource
+    private DelayedMessageService delayedMessageService;
+
+    public CompletableFuture<Void> executeTaskSendMessage() {
+        // 使用 runAsync 并指定自定义线程池
+        return CompletableFuture.runAsync(() -> {
+            // 异步任务逻辑
+            System.out.println("执行线程: " + Thread.currentThread().getName());
+            System.out.println("执行无返回值任务");
+        }, businessTaskExecutor);
+    }
+
+    /**
+     * 异步发送订单过期延迟消息
+     * 判断子订单过期时间是否在第二天凌晨4点之前,是则发送延迟消息
+     * @param proInfoList 子订单列表
+     * @return CompletableFuture<Void>
+     */
+    public CompletableFuture<Void> executeTaskSendMessage(List<AppOrderProInfo> proInfoList) {
+        // 使用 runAsync 并指定自定义线程池
+        return CompletableFuture.runAsync(() -> {
+            // 异步任务逻辑
+            log.info("异步发送订单过期延迟消息 - 执行线程: {}", Thread.currentThread().getName());
+
+            // 计算第二天凌晨4点的时间戳
+            Calendar tomorrow4AM = Calendar.getInstance();
+            tomorrow4AM.add(Calendar.DAY_OF_MONTH, 1);
+            tomorrow4AM.set(Calendar.HOUR_OF_DAY, 4);
+            tomorrow4AM.set(Calendar.MINUTE, 0);
+            tomorrow4AM.set(Calendar.SECOND, 0);
+            tomorrow4AM.set(Calendar.MILLISECOND, 0);
+            Date tomorrow4AMDate = tomorrow4AM.getTime();
+            long tomorrow4AMMillis = tomorrow4AMDate.getTime();
+            
+            log.info("第二天凌晨4点时间:{}", DateUtil.formatDateTime(tomorrow4AMDate));
+            
+            int totalCount = 0;
+            int sendCount = 0;
+            int skipCount = 0;
+
+            for (AppOrderProInfo orderProInfo : proInfoList) {
+                totalCount++;
+                try {
+                    String expireTime = orderProInfo.getExpireTime();
+                    
+                    // 空值检查
+                    if (expireTime == null || expireTime.trim().isEmpty()) {
+                        log.warn("订单[{}]过期时间为空,跳过处理", orderProInfo.getId());
+                        skipCount++;
+                        continue;
+                    }
+                    
+                    // 解析过期时间
+                    Date expireDate = parseExpireTime(expireTime);
+                    if (expireDate == null) {
+                        log.warn("订单[{}]过期时间格式错误:{}", orderProInfo.getId(), expireTime);
+                        skipCount++;
+                        continue;
+                    }
+                    
+                    long expireMillis = expireDate.getTime();
+                    long currentMillis = System.currentTimeMillis();
+                    
+                    // 判断过期时间是否在第二天凌晨4点之前
+                    if (expireMillis <= tomorrow4AMMillis && expireMillis > currentMillis) {
+                        // 计算延迟时间(毫秒)
+                        long delayMillis = expireMillis - currentMillis;
+                        
+                        // 检查是否超过int最大值
+                        if (delayMillis > Integer.MAX_VALUE) {
+                            log.warn("订单[{}]延迟时间过长:{}ms,超过int最大值", orderProInfo.getId(), delayMillis);
+                            skipCount++;
+                            continue;
+                        }
+                        
+                        int delayTime = (int) delayMillis;
+                        
+                        log.info("发送延迟消息 - 订单ID:{},订单编号:{},过期时间:{},延迟毫秒数:{}",
+                            orderProInfo.getId(), orderProInfo.getOrderCode(), expireTime, delayTime);
+                        
+                        delayedMessageService.sendOrderExpireMessage(
+                            orderProInfo.getId(),
+                            delayTime
+                        );
+                        sendCount++;
+                    } else {
+                        log.debug("订单[{}]过期时间不在第二天凌晨4点之前,跳过。过期时间:{}", 
+                            orderProInfo.getId(), expireTime);
+                        skipCount++;
+                    }
+                    
+                } catch (Exception e) {
+                    log.error("处理订单[{}]延迟消息时发生异常", orderProInfo.getId(), e);
+                    skipCount++;
+                }
+            }
+            
+            log.info("异步发送订单过期延迟消息完成 - 总数:{},发送:{},跳过:{}", totalCount, sendCount, skipCount);
+            
+        }, businessTaskExecutor);
+    }
+    
+    /**
+     * 解析过期时间字符串
+     * 支持格式:yyyy-MM-dd、yyyy-MM-dd HH:mm、yyyy-MM-dd HH:mm:ss
+     * @param expireTime 过期时间字符串
+     * @return Date对象,解析失败返回null
+     */
+    private Date parseExpireTime(String expireTime) {
+        try {
+            // yyyy-MM-dd 格式,转换为当天23:59:59
+            if (expireTime.matches("^\\d{4}-\\d{2}-\\d{2}$")) {
+                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+                Date date = sdf.parse(expireTime);
+                Calendar cal = Calendar.getInstance();
+                cal.setTime(date);
+                cal.set(Calendar.HOUR_OF_DAY, 23);
+                cal.set(Calendar.MINUTE, 59);
+                cal.set(Calendar.SECOND, 59);
+                return cal.getTime();
+            }
+            // yyyy-MM-dd HH:mm 格式
+            else if (expireTime.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$")) {
+                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+                return sdf.parse(expireTime);
+            }
+            // yyyy-MM-dd HH:mm:ss 格式
+            else if (expireTime.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$")) {
+                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                return sdf.parse(expireTime);
+            }
+        } catch (ParseException e) {
+            log.error("解析过期时间失败:{}", expireTime, e);
+        }
+        return null;
+    }
+
     /**
      * 小程序微信支付回调
      *
@@ -245,6 +392,10 @@ public class WeChatPayService {
                         if (appOrder.getOrProfitSharing() == 1) {
                             addProfitSharingInfos(appOrder);
                         }
+
+                        //发送延迟消息
+                        executeTaskSendMessage(proInfoList);
+
                         ShopMoney shopMoney = shopMoneyMapper.selectOne(Wrappers.<ShopMoney>lambdaQuery()
                                 .eq(ShopMoney::getOrgCode, appOrder.getOrgCode())
                                 .last("limit 1")

+ 4 - 4
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/service/impl/OrderServiceImpl.java

@@ -50,7 +50,10 @@ import org.jeecg.modules.system.entity.SysDepart;
 import org.jeecg.modules.system.entity.SysUser;
 import org.jeecg.modules.system.mapper.SysDepartMapper;
 import org.jeecg.modules.system.mapper.SysUserMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -67,10 +70,7 @@ import java.time.LocalTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
 import java.util.stream.Collectors;
 
 import static org.jeecg.modules.hikiot.HikiotTool.addFace;

+ 59 - 10
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/quartz/job/TodayExpireOrderJobService.java

@@ -45,22 +45,71 @@ public class TodayExpireOrderJobService {
     @Transactional(rollbackFor = Exception.class)
     public void execute() {
         log.info("开始执行统计24小时内即将过期的订单,并发送过期订单延迟消息定时任务");
+        int totalCount = 0;
+        int successCount = 0;
+        int errorCount = 0;
+        
         try {
-            // 查询24小时内即将过期的子订单
-            List<AppOrderProInfo> orderProInfoList = appOrderProInfoService.list(Wrappers.lambdaQuery(AppOrderProInfo.class).eq(AppOrderProInfo::getOrderStatus, 1));
+            // 查询24小时内即将过期的子订单(状态为待使用)
+            List<AppOrderProInfo> orderProInfoList = appOrderProInfoService.list(
+                Wrappers.lambdaQuery(AppOrderProInfo.class)
+                    .eq(AppOrderProInfo::getOrderStatus, 1)
+            );
+            
+            totalCount = orderProInfoList.size();
+            log.info("查询到待使用订单总数:{}", totalCount);
+            
             for (AppOrderProInfo orderProInfo : orderProInfoList) {
-                String expireTime = orderProInfo.getExpireTime();
-                //判断字符串是yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd HH:mm
-                Date date = adjustDate(expireTime);
-                long differenceIfWithin24Hours = getDifferenceIfWithin24Hours(date);
-                if (differenceIfWithin24Hours > 0) {
-                    // 发送延迟消息
-                    log.info("即将过期的子订单:{}", orderProInfo);
-                    delayedMessageService.sendOrderExpireMessage(orderProInfo.getId(), (int) differenceIfWithin24Hours);
+                try {
+                    String expireTime = orderProInfo.getExpireTime();
+                    
+                    // 增加空值检查
+                    if (expireTime == null || expireTime.trim().isEmpty()) {
+                        log.warn("订单[{}]过期时间为空,跳过处理", orderProInfo.getId());
+                        errorCount++;
+                        continue;
+                    }
+                    
+                    //判断字符串是yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd HH:mm
+                    Date date = adjustDate(expireTime);
+                    long differenceIfWithin24Hours = getDifferenceIfWithin24Hours(date);
+                    
+                    if (differenceIfWithin24Hours > 0) {
+                        // 检查延迟时间是否超过int最大值(避免溢出)
+                        if (differenceIfWithin24Hours > Integer.MAX_VALUE) {
+                            log.warn("订单[{}]延迟时间过长:{}ms,超过int最大值,跳过处理", 
+                                orderProInfo.getId(), differenceIfWithin24Hours);
+                            errorCount++;
+                            continue;
+                        }
+                        
+                        // 发送延迟消息
+                        log.info("即将过期的子订单:订单ID={}, 订单编号={}, 过期时间={}, 延迟毫秒数={}", 
+                            orderProInfo.getId(), orderProInfo.getOrderCode(), 
+                            expireTime, differenceIfWithin24Hours);
+                        
+                        delayedMessageService.sendOrderExpireMessage(
+                            orderProInfo.getId(),
+                            (int) differenceIfWithin24Hours
+                        );
+                        successCount++;
+                    }
+                    
+                } catch (IllegalArgumentException e) {
+                    log.error("订单[{}]日期格式解析失败,过期时间:{}, 错误:{}", 
+                        orderProInfo.getId(), orderProInfo.getExpireTime(), e.getMessage());
+                    errorCount++;
+                } catch (Exception e) {
+                    log.error("处理订单[{}]时发生异常", orderProInfo.getId(), e);
+                    errorCount++;
                 }
             }
+            
         } catch (Exception e) {
             log.error("统计24小时内即将过期的订单,并发送过期订单延迟消息异常", e);
+        } finally {
+            log.info("定时任务执行完毕 - 总订单数:{},成功发送消息:{},处理失败:{}", 
+                totalCount, successCount, errorCount);
         }
     }
 

+ 1 - 1
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/rabbitmq/DelayedMessageService.java

@@ -14,7 +14,7 @@ public class DelayedMessageService {
 
     public void sendOrderExpireMessage(String message,Integer time) {
         MessagePostProcessor processor = msg -> {
-            // 设置延迟时间(25天毫秒数)
+            // 设置延迟时间
             msg.getMessageProperties().setDelay(time);
             // 强制持久化消息
             msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);