Jelajahi Sumber

fix: scan-open 消费 Redis preauth,彻底解决连续开门 payScoreOrderId 错配

问题根源:
- persistPayScorePreAuth 只读 Redis 不删除,旧 preauth 残留
- 用户连续购物时,下次 scan-open 读到的是上一个未被消费的旧 preauth
- DoorRecord 错配 oldPayScoreOrderId,导致 cancelOldDeviceOrders 误取消、complete 失败

修改:
1. PayScoreServiceImpl 新增 consumePreAuthFromRedis():
   scan-open 持久化到 DoorRecord 后立即删除 Redis key
2. PayScoreServiceImpl.storePreAuthToRedis():
   旧 Fix2 无条件阻止覆盖 → 改为 isPreAuthStillPending() 智能判断
   - 旧 preauth 已有对应本地订单 → ORC_RESULT 已消费 → 允许覆盖
   - 旧 preauth 无本地订单 → 可能还在等 ORC_RESULT → 保守不覆盖
3. DeviceServiceImpl.persistPayScorePreAuth():
   持久化成功后调用 consumePreAuthFromRedis 清理 Redis

效果:
- 每次开门后 Redis 立即清空,下次 pre-create 从干净状态写入
- ORC_RESULT 通过 DoorRecord 层恢复(不依赖 Redis 层)

Co-Authored-By: Claude <noreply@anthropic.com>
skyline 2 minggu lalu
induk
melakukan
e2ce506e95

+ 7 - 2
haha-service/src/main/java/com/haha/service/impl/DeviceServiceImpl.java

@@ -529,8 +529,8 @@ private final HahaClient hahaClient;
     }
 
     /**
-     * 从 Redis 读取支付分预授权信息,持久化到 DoorRecord
-     * 此时 Redis 数据必定存在(前端刚调用过 pre-create),写入 DB 后防止后续 Redis 过期丢失
+     * 从 Redis 读取支付分预授权信息,持久化到 DoorRecord,然后立即消费(删除)Redis key。
+     * 消费后该 key 置空,下一次 pre-create 可以从干净状态写入,避免连续开门时读到旧 preauth。
      */
     private void persistPayScorePreAuth(String deviceId, Long userId, String activityId) {
         if (payScoreService == null) {
@@ -547,6 +547,11 @@ private final HahaClient hahaClient;
                 doorRecordService.updatePayScorePreAuth(activityId, payScoreOrderId, preAuth.get("serviceId"));
                 log.info("支付分预授权信息已持久化到 DoorRecord - activityId: {}, payScoreOrderId: {}",
                         activityId, payScoreOrderId);
+
+                // 消费 Redis key:已成功持久化到 DoorRecord,后续 ORC_RESULT 通过 DoorRecord 层恢复即可
+                // 删除后下一次 pre-create 可写入新的 outOrderNo,避免连续购物时读错
+                ((com.haha.service.payment.payscore.impl.PayScoreServiceImpl) payScoreService)
+                        .consumePreAuthFromRedis(deviceId, userId);
             }
         } catch (Exception e) {
             // 持久化失败不应阻塞开门流程,Redis 仍可作为数据源

+ 46 - 4
haha-service/src/main/java/com/haha/service/payment/payscore/impl/PayScoreServiceImpl.java

@@ -1002,13 +1002,17 @@ public class PayScoreServiceImpl implements PayScoreService {
         }
         String redisKey = REDIS_KEY_PREFIX + deviceId + ":" + userId;
 
-        // 防止覆盖已有的未消费预授权:如果同一设备+用户已有 preauth key,不覆盖
-        // 新的 outOrderNo 已在 tracking SET 中,后续可通过三层恢复机制找到
+        // 检查是否已有未消费的预授权。如果旧预授权关联的 DoorRecord 尚未被 ORC_RESULT 处理
+        // (即本地无对应订单),说明购物流程正在进行中,不覆盖;否则可以安全覆盖
         String existing = stringRedisTemplate.opsForValue().get(redisKey);
         if (existing != null && !existing.isEmpty()) {
-            log.warn("[支付分服务] Redis中已有未消费的预授权信息,不覆盖 - key: {}, 已有: {}, 新: {}",
+            if (isPreAuthStillPending(existing)) {
+                log.warn("[支付分服务] Redis中已有进行中的预授权信息,不覆盖 - key: {}, 已有: {}, 新: {}",
+                        redisKey, existing, result.getOutOrderNo());
+                return;
+            }
+            log.info("[支付分服务] Redis中旧预授权已消费完毕,覆盖 - key: {}, 旧: {}, 新: {}",
                     redisKey, existing, result.getOutOrderNo());
-            return;
         }
 
         JSONObject json = new JSONObject();
@@ -1021,6 +1025,44 @@ public class PayScoreServiceImpl implements PayScoreService {
         log.info("[支付分服务] 支付分信息已存入Redis - key: {}, ttl: {}min", redisKey, REDIS_TTL_MINUTES);
     }
 
+    /**
+     * 检查 Redis 中已有的预授权是否仍在进行中(未被 ORC_RESULT 消费)
+     * 如果对应 payScoreOrderId 的本地订单已存在 → 已消费 → 可覆盖
+     * 如果本地无订单 → 可能仍在等待 ORC_RESULT → 不可覆盖
+     */
+    private boolean isPreAuthStillPending(String existingJson) {
+        try {
+            JSONObject json = JSON.parseObject(existingJson);
+            String oldOutOrderNo = json.getString("payScoreOrderId");
+            if (oldOutOrderNo == null) {
+                return false;
+            }
+            // 检查是否已有本地订单关联此支付分订单号
+            // 有 → ORC_RESULT 已处理完毕 → 可安全覆盖
+            // 无 → 可能还在等 ORC_RESULT → 保守不覆盖
+            long count = orderService.lambdaQuery()
+                    .eq(Order::getPayScoreOrderId, oldOutOrderNo)
+                    .count();
+            return count == 0;
+        } catch (Exception e) {
+            log.warn("[支付分服务] 检查预授权状态失败,保守不覆盖", e);
+            return true; // 解析失败保守处理
+        }
+    }
+
+    /**
+     * 消费(读取并删除)Redis 中的预授权信息
+     * 由 scan-open 流程调用,确保每次开门后 Redis 清零,防止下次 scan-open 读错
+     */
+    public void consumePreAuthFromRedis(String deviceId, Long userId) {
+        if (stringRedisTemplate == null) {
+            return;
+        }
+        String redisKey = REDIS_KEY_PREFIX + deviceId + ":" + userId;
+        stringRedisTemplate.delete(redisKey);
+        log.debug("[支付分服务] Redis预授权已消费 - key: {}", redisKey);
+    }
+
     /**
      * 从 Redis 获取预创建的支付分信息并关联到订单
      * 由 HahaCallbackServiceImpl 在创建订单后调用