浏览代码

fix: 防止短时多次开柜时误取消正在进行的支付分订单

preCreatePayScoreOrder 在 Redis 追踪清理和同设备旧订单清理时,新增
isPayScoreOrderSafeToCancel 检查关联本地订单的 payScoreState:
- CREATED 或无本地订单 → 允许取消
- DOING/USER_PAYING/DONE → 禁止取消(已确认/正在扣款/已完成)
- 查询异常时保守处理,不取消

避免用户短时间内多次扫码开柜导致前序订单无法扣款的资金风险。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 1 周之前
父节点
当前提交
a1bd8480b1

+ 41 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/impl/PayScoreServiceImpl.java

@@ -984,6 +984,7 @@ public class PayScoreServiceImpl implements PayScoreService {
         }
 
         // 2. 取消 Redis 追踪的预创建订单(之前未完成购物流程留下的)
+        // 必须检查关联的本地订单状态,避免误取消已确认/正在扣款的订单
         if (stringRedisTemplate != null) {
             try {
                 String trackingKey = REDIS_TRACKING_KEY + userId;
@@ -992,6 +993,10 @@ public class PayScoreServiceImpl implements PayScoreService {
                     log.info("[支付分服务] 清理用户 {} 的 {} 笔追踪预创建订单", userId, trackedOrders.size());
                     for (String outOrderNo : trackedOrders) {
                         if (outOrderNo != null && !outOrderNo.isEmpty()) {
+                            if (!isPayScoreOrderSafeToCancel(outOrderNo)) {
+                                log.info("[支付分服务] 跳过取消已确认的支付分订单 - outOrderNo: {}", outOrderNo);
+                                continue;
+                            }
                             cancelPayScoreOrderByOutOrderNo(outOrderNo, null);
                             stringRedisTemplate.opsForSet().remove(trackingKey, outOrderNo);
                         }
@@ -1059,6 +1064,7 @@ public class PayScoreServiceImpl implements PayScoreService {
 
     /**
      * 取消同设备旧的预创建订单(已被本次新订单替代)
+     * 必须检查关联的本地订单状态,避免误取消已确认/正在扣款的订单
      */
     private void cancelOldDeviceOrders(Long userId, String deviceId, String currentOutOrderNo) {
         if (stringRedisTemplate == null) {
@@ -1072,6 +1078,10 @@ public class PayScoreServiceImpl implements PayScoreService {
             if (tracked != null) {
                 for (String outOrderNo : tracked) {
                     if (outOrderNo != null && outOrderNo.endsWith(suffix) && !outOrderNo.equals(currentOutOrderNo)) {
+                        if (!isPayScoreOrderSafeToCancel(outOrderNo)) {
+                            log.info("[支付分服务] 跳过取消同设备已确认的支付分订单 - outOrderNo: {}, deviceId: {}", outOrderNo, deviceId);
+                            continue;
+                        }
                         cancelPayScoreOrderByOutOrderNo(outOrderNo, null);
                         stringRedisTemplate.opsForSet().remove(trackingKey, outOrderNo);
                     }
@@ -1081,4 +1091,35 @@ public class PayScoreServiceImpl implements PayScoreService {
             log.warn("[支付分服务] 清理设备旧追踪失败 - deviceId: {}", deviceId, e);
         }
     }
+
+    /**
+     * 检查支付分订单是否可以安全取消
+     * 已确认(DOING)或正在扣款(USER_PAYING)或已完成(DONE)的订单不应被取消
+     *
+     * @return true 可以安全取消(本地无订单或仅 CREATED 状态),false 不可取消
+     */
+    private boolean isPayScoreOrderSafeToCancel(String outOrderNo) {
+        try {
+            Order localOrder = orderService.lambdaQuery()
+                    .eq(Order::getPayScoreOrderId, outOrderNo)
+                    .one();
+            if (localOrder == null) {
+                // 没有关联的本地订单,可以安全取消(只是预创建的支付分记录)
+                return true;
+            }
+            String state = localOrder.getPayScoreState();
+            if (PayScoreState.CREATED.getCode().equals(state)) {
+                // CREATED 状态的订单(用户从未确认),可以取消
+                return true;
+            }
+            // DOING / USER_PAYING / DONE 状态的订单不可取消
+            log.warn("[支付分服务] 支付分订单已确认不可取消 - outOrderNo: {}, payScoreState: {}, orderId: {}",
+                    outOrderNo, state, localOrder.getId());
+            return false;
+        } catch (Exception e) {
+            log.warn("[支付分服务] 检查支付分订单状态失败 - outOrderNo: {}", outOrderNo, e);
+            // 查询失败时保守处理:不取消,避免误伤
+            return false;
+        }
+    }
 }