Ver Fonte

fix: 预创建支付分订单增加持久追踪与暴力清理机制

问题:preCreatePayScoreOrder创建的微信支付分订单只存Redis(10min TTL),
回调失败后成为孤儿订单,本地无法追踪导致无法取消,微信累积"进行中订单过多"。

修复:
1. Redis SET追踪每个用户的预创建outOrderNo(24h TTL)
2. 创建前取消:DB订单 + Redis追踪的预创建订单 + 暴力扫描(60min窗口)
3. 创建失败时若为"进行中订单过多"则暴力清理后重试
4. 订单正常关联后从追踪SET移除

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline há 2 semanas atrás
pai
commit
bb30eb1d47

+ 177 - 39
haha-service/src/main/java/com/haha/service/payment/payscore/impl/PayScoreServiceImpl.java

@@ -55,6 +55,8 @@ public class PayScoreServiceImpl implements PayScoreService {
 
     private static final String REDIS_KEY_PREFIX = "payscore:preauth:";
     private static final long REDIS_TTL_MINUTES = 10;
+    private static final String REDIS_TRACKING_KEY = "payscore:preauth:tracking:";
+    private static final long REDIS_TRACKING_TTL_HOURS = 24;
 
     private static final BigDecimal DEFAULT_RISK_FUND_AMOUNT = new BigDecimal("99.00");
     private static final String DEFAULT_RISK_FUND_NAME = "DEPOSIT";
@@ -743,8 +745,8 @@ public class PayScoreServiceImpl implements PayScoreService {
             return errorData;
         }
 
-        // 清理用户旧的进行中支付分订单,避免微信返回"进行中订单过多"
-        cancelUserInProgressPayScoreOrders(userId);
+        // 取消用户旧的进行中支付分订单(DB + Redis追踪的 + 暴力清理)
+        cancelUserInProgressPayScoreOrders(userId, deviceId);
 
         // 如果没有传 openId,从用户表中获取
         if (openId == null || openId.isEmpty()) {
@@ -758,10 +760,22 @@ public class PayScoreServiceImpl implements PayScoreService {
             openId = user.getOpenid();
         }
 
-        // 生成商户服务订单号(不创建本地订单,由 AI 识别回调统一创建)
+        // 尝试创建,失败时如果是"进行中订单过多"则暴力清理后重试
+        Map<String, Object> result = doCreatePayScoreOrder(userId, deviceId, openId);
+        if (!Boolean.TRUE.equals(result.get("success"))) {
+            String errorMsg = (String) result.get("errorMsg");
+            if (errorMsg != null && (errorMsg.contains("进行中") || errorMsg.contains("过多"))) {
+                log.warn("[支付分服务] 首次创建失败(进行中订单过多),尝试暴力清理 - userId: {}", userId);
+                bruteForceCancelRecentOrders(deviceId);
+                result = doCreatePayScoreOrder(userId, deviceId, openId);
+            }
+        }
+        return result;
+    }
+
+    private Map<String, Object> doCreatePayScoreOrder(Long userId, String deviceId, String openId) {
         String outOrderNo = generatePreAuthOutOrderNo(deviceId);
 
-        // 创建微信支付分服务订单
         PayScoreCreateRequest request = new PayScoreCreateRequest();
         request.setOutOrderNo(outOrderNo);
         request.setServiceIntroduction("智能零售服务");
@@ -774,18 +788,9 @@ public class PayScoreServiceImpl implements PayScoreService {
         PayScoreResult result = payScoreStrategy.createServiceOrder(request);
 
         if (result.isSuccess()) {
-            // 将支付分信息存入 Redis,供 AI 识别回调创建订单时关联
-            if (stringRedisTemplate != null) {
-                String redisKey = REDIS_KEY_PREFIX + deviceId + ":" + userId;
-                JSONObject json = new JSONObject();
-                json.put("payScoreOrderId", result.getOutOrderNo());
-                json.put("payScoreState", result.getState());
-                if (wxPayConfig != null) {
-                    json.put("serviceId", wxPayConfig.getServiceId());
-                }
-                stringRedisTemplate.opsForValue().set(redisKey, json.toJSONString(), REDIS_TTL_MINUTES, TimeUnit.MINUTES);
-                log.info("[支付分服务] 支付分信息已存入Redis - key: {}, ttl: {}min", redisKey, REDIS_TTL_MINUTES);
-            }
+            trackPreAuthOrder(userId, outOrderNo);
+            storePreAuthToRedis(deviceId, userId, result);
+            untrackPreAuthOrdersByDevice(userId, deviceId, outOrderNo);
 
             Map<String, Object> data = new HashMap<>();
             data.put("success", true);
@@ -805,6 +810,21 @@ public class PayScoreServiceImpl implements PayScoreService {
         }
     }
 
+    private void storePreAuthToRedis(String deviceId, Long userId, PayScoreResult result) {
+        if (stringRedisTemplate == null) {
+            return;
+        }
+        String redisKey = REDIS_KEY_PREFIX + deviceId + ":" + userId;
+        JSONObject json = new JSONObject();
+        json.put("payScoreOrderId", result.getOutOrderNo());
+        json.put("payScoreState", result.getState());
+        if (wxPayConfig != null) {
+            json.put("serviceId", wxPayConfig.getServiceId());
+        }
+        stringRedisTemplate.opsForValue().set(redisKey, json.toJSONString(), REDIS_TTL_MINUTES, TimeUnit.MINUTES);
+        log.info("[支付分服务] 支付分信息已存入Redis - key: {}, ttl: {}min", redisKey, REDIS_TTL_MINUTES);
+    }
+
     /**
      * 从 Redis 获取预创建的支付分信息并关联到订单
      * 由 HahaCallbackServiceImpl 在创建订单后调用
@@ -836,6 +856,8 @@ public class PayScoreServiceImpl implements PayScoreService {
 
                 // 删除 Redis 记录,防止重复使用
                 stringRedisTemplate.delete(redisKey);
+                // 从追踪 SET 中移除,订单已正常关联不再需要清理
+                stringRedisTemplate.opsForSet().remove(REDIS_TRACKING_KEY + order.getUserId(), payScoreOrderId);
                 log.info("[支付分服务] 预授权信息已关联到订单 - orderId: {}, payScoreOrderId: {}", order.getId(), payScoreOrderId);
             }
         } catch (Exception e) {
@@ -844,10 +866,10 @@ public class PayScoreServiceImpl implements PayScoreService {
     }
 
     /**
-     * 取消用户所有进行中的支付分订单
-     * 在创建新支付分订单前调用,避免微信返回"同一实名身份下进行中的订单过多"
+     * 取消用户所有进行中的支付分订单(DB订单 + Redis追踪的预创建订单 + 暴力扫描)
      */
-    private void cancelUserInProgressPayScoreOrders(Long userId) {
+    private void cancelUserInProgressPayScoreOrders(Long userId, String deviceId) {
+        // 1. 取消 DB 中的进行中支付分订单
         try {
             List<Order> inProgressOrders = orderService.lambdaQuery()
                     .eq(Order::getUserId, userId)
@@ -857,34 +879,150 @@ public class PayScoreServiceImpl implements PayScoreService {
                     .isNotNull(Order::getPayScoreOrderId)
                     .list();
 
-            if (inProgressOrders == null || inProgressOrders.isEmpty()) {
-                return;
+            if (inProgressOrders != null && !inProgressOrders.isEmpty()) {
+                log.info("[支付分服务] 发现用户 {} 有 {} 笔DB中的进行中支付分订单,开始清理",
+                        userId, inProgressOrders.size());
+                for (Order order : inProgressOrders) {
+                    cancelSingleByOutOrderNo(order.getPayScoreOrderId(), order.getId());
+                }
+            }
+        } catch (Exception e) {
+            log.error("[支付分服务] 清理DB进行中订单失败 - userId: {}", userId, e);
+        }
+
+        // 2. 取消 Redis 追踪 SET 中记录的预创建订单
+        if (stringRedisTemplate != null) {
+            try {
+                String trackingKey = REDIS_TRACKING_KEY + userId;
+                java.util.Set<String> trackedOrders = stringRedisTemplate.opsForSet().members(trackingKey);
+                if (trackedOrders != null && !trackedOrders.isEmpty()) {
+                    log.info("[支付分服务] 发现用户 {} 有 {} 笔Redis追踪的预创建订单,开始清理",
+                            userId, trackedOrders.size());
+                    for (String outOrderNo : trackedOrders) {
+                        if (outOrderNo != null && !outOrderNo.isEmpty()) {
+                            cancelSingleByOutOrderNo(outOrderNo, null);
+                            stringRedisTemplate.opsForSet().remove(trackingKey, outOrderNo);
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                log.error("[支付分服务] 清理Redis追踪订单失败 - userId: {}", userId, e);
             }
+        }
+
+        // 3. 暴力扫描:尝试取消当前设备最近一段时间内可能存在的孤儿订单
+        bruteForceCancelRecentOrders(deviceId);
+    }
 
-            log.info("[支付分服务] 发现用户 {} 有 {} 笔进行中的支付分订单,开始清理",
-                    userId, inProgressOrders.size());
-
-            int cancelledCount = 0;
-            for (Order order : inProgressOrders) {
-                try {
-                    PayScoreResult cancelResult = cancelPayScoreOrder(order.getId(),
-                            "自动取消:创建新订单前清理旧订单");
-                    if (cancelResult.isSuccess()) {
-                        cancelledCount++;
-                    } else {
-                        log.warn("[支付分服务] 取消旧订单失败 - orderId: {}, payScoreOrderId: {}, error: {}",
-                                order.getId(), order.getPayScoreOrderId(), cancelResult.getErrorMsg());
+    /**
+     * 通过 outOrderNo 调用微信API取消单个支付分订单
+     */
+    private void cancelSingleByOutOrderNo(String outOrderNo, Long localOrderId) {
+        try {
+            PayScoreCancelRequest cancelReq = new PayScoreCancelRequest();
+            cancelReq.setOutOrderNo(outOrderNo);
+            cancelReq.setReason("自动取消:创建新订单前清理旧订单");
+            PayScoreResult cancelResult = payScoreStrategy.cancelServiceOrder(cancelReq);
+
+            if (cancelResult.isSuccess()) {
+                log.info("[支付分服务] 取消成功 - outOrderNo: {}, localOrderId: {}", outOrderNo, localOrderId);
+                // 如果有本地订单,同步更新状态
+                if (localOrderId != null) {
+                    Order order = orderService.getById(localOrderId);
+                    if (order != null) {
+                        order.setPayScoreState(PayScoreState.REVOKED.getCode());
+                        orderService.updateById(order);
                     }
-                } catch (Exception e) {
-                    log.error("[支付分服务] 取消旧订单异常 - orderId: {}, payScoreOrderId: {}",
-                            order.getId(), order.getPayScoreOrderId(), e);
+                }
+            } else {
+                // ORDER_NOT_EXIST 表示微信侧已不存在,忽略
+                if (cancelResult.getErrorMsg() != null && cancelResult.getErrorMsg().contains("ORDER_NOT_EXIST")) {
+                    log.debug("[支付分服务] 订单在微信侧已不存在 - outOrderNo: {}", outOrderNo);
+                } else {
+                    log.debug("[支付分服务] 取消失败 - outOrderNo: {}, error: {}",
+                            outOrderNo, cancelResult.getErrorMsg());
                 }
             }
+        } catch (Exception e) {
+            log.debug("[支付分服务] 取消异常 - outOrderNo: {}, error: {}", outOrderNo, e.getMessage());
+        }
+    }
 
-            log.info("[支付分服务] 用户 {} 旧订单清理完成 - 成功: {}/{}", userId, cancelledCount, inProgressOrders.size());
+    /**
+     * 暴力扫描并取消最近一段时间内的预创建订单
+     * 当微信返回"进行中订单过多"时,尝试用不同时间戳构造 outOrderNo 并取消
+     */
+    private void bruteForceCancelRecentOrders(String deviceId) {
+        if (payScoreStrategy == null) {
+            return;
+        }
+        long nowSec = System.currentTimeMillis() / 1000;
+        String safeDeviceId = deviceId.replaceAll("[^A-Za-z0-9]", "");
+        String suffix = safeDeviceId.length() > 10 ? safeDeviceId.substring(safeDeviceId.length() - 10) : safeDeviceId;
 
+        // 扫描过去60分钟内,每30秒一个时间点(最多120次尝试)
+        int cancelledCount = 0;
+        for (int i = 0; i < 120; i++) {
+            long ts = nowSec - (i * 30L);
+            String outOrderNo = "PS" + ts + suffix;
+            try {
+                PayScoreCancelRequest cancelReq = new PayScoreCancelRequest();
+                cancelReq.setOutOrderNo(outOrderNo);
+                cancelReq.setReason("自动清理孤儿订单");
+                PayScoreResult cancelResult = payScoreStrategy.cancelServiceOrder(cancelReq);
+                if (cancelResult.isSuccess()) {
+                    cancelledCount++;
+                    if (cancelledCount <= 3) {
+                        log.info("[支付分服务] 暴力清理成功 - outOrderNo: {}", outOrderNo);
+                    }
+                }
+            } catch (Exception ignored) {
+                // 忽略单次取消异常,继续尝试
+            }
+        }
+        if (cancelledCount > 0) {
+            log.info("[支付分服务] 暴力清理完成 - 成功取消 {} 笔孤儿订单", cancelledCount);
+        }
+    }
+
+    /**
+     * 将预创建的 outOrderNo 记录到 Redis SET 中,防止成为孤儿订单
+     */
+    private void trackPreAuthOrder(Long userId, String outOrderNo) {
+        if (stringRedisTemplate == null) {
+            return;
+        }
+        try {
+            String trackingKey = REDIS_TRACKING_KEY + userId;
+            stringRedisTemplate.opsForSet().add(trackingKey, outOrderNo);
+            stringRedisTemplate.expire(trackingKey, REDIS_TRACKING_TTL_HOURS, TimeUnit.HOURS);
+        } catch (Exception e) {
+            log.warn("[支付分服务] 追踪订单失败 - outOrderNo: {}", outOrderNo, e);
+        }
+    }
+
+    /**
+     * 清除同一设备旧的追踪记录(同一设备的新订单会替代旧订单)
+     */
+    private void untrackPreAuthOrdersByDevice(Long userId, String deviceId, String currentOutOrderNo) {
+        if (stringRedisTemplate == null) {
+            return;
+        }
+        try {
+            String safeDeviceId = deviceId.replaceAll("[^A-Za-z0-9]", "");
+            String suffix = safeDeviceId.length() > 10 ? safeDeviceId.substring(safeDeviceId.length() - 10) : safeDeviceId;
+            String trackingKey = REDIS_TRACKING_KEY + userId;
+            java.util.Set<String> tracked = stringRedisTemplate.opsForSet().members(trackingKey);
+            if (tracked != null) {
+                for (String outOrderNo : tracked) {
+                    if (outOrderNo != null && outOrderNo.endsWith(suffix) && !outOrderNo.equals(currentOutOrderNo)) {
+                        cancelSingleByOutOrderNo(outOrderNo, null);
+                        stringRedisTemplate.opsForSet().remove(trackingKey, outOrderNo);
+                    }
+                }
+            }
         } catch (Exception e) {
-            log.error("[支付分服务] 清理用户 {} 进行中订单失败", userId, e);
+            log.warn("[支付分服务] 清理设备旧追踪失败 - deviceId: {}", deviceId, e);
         }
     }
 }