Przeglądaj źródła

feat: 支付分信息持久化到 DoorRecord,彻底避免 Redis 过期丢失

问题:payScoreOrderId/serviceId 只存 Redis (30min TTL),过期后 ORC_RESULT
      创建订单时丢失,导致 ORDER 回调无法扣费,管理员需手动找商户单号。

方案:在 scanOpenDoor 时将支付分信息写入 DoorRecord,DB 永久保存。
      后续 ORC_RESULT 创建订单时,读取优先级:
      Redis → DoorRecord(DB) → 追踪SET

改动:
- DoorRecord 新增 payScoreOrderId、serviceId 字段 + 迁移 V7
- PayScoreService 新增 getPreAuthInfo() 接口方法
- DeviceServiceImpl.scanOpenDoor 持久化支付分信息到 DoorRecord
- DoorRecordService/Mapper 新增 updatePayScorePreAuth
- HahaCallbackServiceImpl 三处恢复流程加入 DoorRecord 兜底层

Co-Authored-By: Claude <noreply@anthropic.com>
skyline 2 dni temu
rodzic
commit
485eb6802c

+ 4 - 0
haha-admin/src/main/resources/db/migration/V7__add_door_record_payscore_fields.sql

@@ -0,0 +1,4 @@
+-- V7: DoorRecord 新增支付分预授权字段,防止 Redis 过期后商户单号丢失
+ALTER TABLE `t_door_record`
+    ADD COLUMN `pay_score_order_id` VARCHAR(64) DEFAULT NULL COMMENT '微信支付分预授权订单号' AFTER `order_id`,
+    ADD COLUMN `service_id` VARCHAR(64) DEFAULT NULL COMMENT '微信支付分服务ID' AFTER `pay_score_order_id`;

+ 10 - 0
haha-entity/src/main/java/com/haha/entity/DoorRecord.java

@@ -68,6 +68,16 @@ public class DoorRecord {
     @JsonSerialize(using = ToStringSerializer.class)
     private Long orderId;
 
+    /**
+     * 微信支付分预授权订单号(outOrderNo)
+     */
+    private String payScoreOrderId;
+
+    /**
+     * 微信支付分服务 ID
+     */
+    private String serviceId;
+
     /**
      * 开门时间
      */

+ 12 - 0
haha-mapper/src/main/java/com/haha/mapper/DoorRecordMapper.java

@@ -69,6 +69,18 @@ public interface DoorRecordMapper extends BaseMapper<DoorRecord> {
      */
     int markNoConsume(@Param("activityId") String activityId);
 
+    /**
+     * 更新支付分预授权信息到开门记录
+     *
+     * @param activityId      活动 ID
+     * @param payScoreOrderId 支付分预授权订单号
+     * @param serviceId       服务 ID
+     * @return 影响行数
+     */
+    int updatePayScorePreAuth(@Param("activityId") String activityId,
+                              @Param("payScoreOrderId") String payScoreOrderId,
+                              @Param("serviceId") String serviceId);
+
     /**
      * 统计指定时间范围内的开门次数
      * 

+ 11 - 0
haha-mapper/src/main/resources/mapper/DoorRecordMapper.xml

@@ -12,6 +12,8 @@
         <result property="doorStatus" column="door_status"/>
         <result property="nobuy" column="nobuy"/>
         <result property="orderId" column="order_id"/>
+        <result property="payScoreOrderId" column="pay_score_order_id"/>
+        <result property="serviceId" column="service_id"/>
         <result property="openTime" column="open_time"/>
         <result property="closeTime" column="close_time"/>
         <result property="duration" column="duration"/>
@@ -57,6 +59,15 @@
         WHERE activity_id = #{activityId}
     </update>
 
+    <!-- 更新支付分预授权信息 -->
+    <update id="updatePayScorePreAuth">
+        UPDATE t_door_record
+        SET pay_score_order_id = #{payScoreOrderId},
+            service_id = #{serviceId},
+            update_time = NOW()
+        WHERE activity_id = #{activityId}
+    </update>
+
     <!-- 标记无消费记录 -->
     <update id="markNoConsume">
         UPDATE t_door_record 

+ 10 - 0
haha-service/src/main/java/com/haha/service/DoorRecordService.java

@@ -66,6 +66,16 @@ public interface DoorRecordService extends IService<DoorRecord> {
      */
     boolean linkOrderId(String activityId, Long orderId);
 
+    /**
+     * 将支付分预授权信息持久化到开门记录
+     *
+     * @param activityId      活动 ID
+     * @param payScoreOrderId 支付分预授权订单号
+     * @param serviceId       服务 ID
+     * @return 是否成功
+     */
+    boolean updatePayScorePreAuth(String activityId, String payScoreOrderId, String serviceId);
+
     /**
      * 获取用户最近的开门记录
      * 

+ 38 - 3
haha-service/src/main/java/com/haha/service/impl/DeviceServiceImpl.java

@@ -23,6 +23,7 @@ import com.haha.service.DeviceService;
 import com.haha.service.OrderService;
 import com.haha.service.UserService;
 import com.haha.service.DoorRecordService;
+import com.haha.service.payment.payscore.PayScoreService;
 import com.haha.common.utils.OrderUtils;
 import com.haha.common.vo.OpenDoorVO;
 import com.haha.sdk.HahaClient;
@@ -31,6 +32,7 @@ import com.haha.sdk.model.DeviceOnlineStatus;
 import com.haha.sdk.model.OpenDoorResult;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
@@ -51,6 +53,9 @@ private final HahaClient hahaClient;
     private final UserService userService;
     private final DoorRecordService doorRecordService;
 
+    @Autowired(required = false)
+    private PayScoreService payScoreService;
+
     @Override
     public IPage<Device> getPage(int page, int pageSize, String deviceId, Long shopId, Integer status) {
         LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
@@ -258,14 +263,17 @@ private final HahaClient hahaClient;
         
         // 4. 【修改】保存开门记录到数据库和 Redis(不再立即创建订单)
         doorRecordService.saveDoorOpenRecord(
-            deviceId, 
-            userId, 
+            deviceId,
+            userId,
             openResult.getActivityId(),
             doorIndex,
             openType,
             source
         );
-        
+
+        // 4.1 将支付分预授权信息持久化到 DoorRecord,防止 Redis 过期后丢失
+        persistPayScorePreAuth(deviceId, userId, openResult.getActivityId());
+
         // 5. 返回成功结果
         log.info("开门成功 - 设备:{}, 活动 ID: {}, 用户 ID: {}", deviceId, openResult.getActivityId(), openResult.getUserId());
 
@@ -511,4 +519,31 @@ private final HahaClient hahaClient;
             return null;
         }
     }
+
+    /**
+     * 从 Redis 读取支付分预授权信息,持久化到 DoorRecord
+     * 此时 Redis 数据必定存在(前端刚调用过 pre-create),写入 DB 后防止后续 Redis 过期丢失
+     */
+    private void persistPayScorePreAuth(String deviceId, Long userId, String activityId) {
+        if (payScoreService == null) {
+            return;
+        }
+        try {
+            Map<String, String> preAuth = payScoreService.getPreAuthInfo(deviceId, userId);
+            if (preAuth == null) {
+                log.debug("Redis 中无支付分预授权信息,跳过持久化 - deviceId: {}, userId: {}", deviceId, userId);
+                return;
+            }
+            String payScoreOrderId = preAuth.get("payScoreOrderId");
+            if (payScoreOrderId != null) {
+                doorRecordService.updatePayScorePreAuth(activityId, payScoreOrderId, preAuth.get("serviceId"));
+                log.info("支付分预授权信息已持久化到 DoorRecord - activityId: {}, payScoreOrderId: {}",
+                        activityId, payScoreOrderId);
+            }
+        } catch (Exception e) {
+            // 持久化失败不应阻塞开门流程,Redis 仍可作为数据源
+            log.warn("持久化支付分预授权信息到 DoorRecord 失败(非致命) - deviceId: {}, userId: {}",
+                    deviceId, userId, e);
+        }
+    }
 }

+ 18 - 0
haha-service/src/main/java/com/haha/service/impl/DoorRecordServiceImpl.java

@@ -148,6 +148,24 @@ public class DoorRecordServiceImpl extends ServiceImpl<DoorRecordMapper, DoorRec
         }
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updatePayScorePreAuth(String activityId, String payScoreOrderId, String serviceId) {
+        try {
+            int result = baseMapper.updatePayScorePreAuth(activityId, payScoreOrderId, serviceId);
+            if (result > 0) {
+                log.info("支付分预授权信息已持久化到开门记录 - activityId: {}, payScoreOrderId: {}", activityId, payScoreOrderId);
+                return true;
+            } else {
+                log.warn("更新支付分预授权信息失败(记录不存在) - activityId: {}", activityId);
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("更新支付分预授权信息异常 - activityId: {}", activityId, e);
+            return false;
+        }
+    }
+
     @Override
     public List<DoorRecord> getRecentRecords(Long userId, String deviceId, int limit) {
         return baseMapper.getRecentRecords(userId, deviceId, limit);

+ 29 - 9
haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

@@ -496,11 +496,19 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
             com.alibaba.fastjson2.JSONObject preAuth = ((com.haha.service.payment.payscore.impl.PayScoreServiceImpl) payScoreService)
                     .readPreAuthFromRedis(record.getDeviceId(), record.getUserId());
             if (preAuth != null) {
+                // 第一层:Redis 命中
                 payScoreOrderId = preAuth.getString("payScoreOrderId");
                 payScoreState = preAuth.getString("payScoreState");
                 serviceId = preAuth.getString("serviceId");
+            } else if (record.getPayScoreOrderId() != null) {
+                // 第二层:Redis 过期,从 DoorRecord(DB 持久化)恢复
+                payScoreOrderId = record.getPayScoreOrderId();
+                payScoreState = PayScoreState.CREATED.getCode();
+                serviceId = record.getServiceId();
+                log.info("从DoorRecord恢复支付分订单ID - userId: {}, deviceId: {}, payScoreOrderId: {}",
+                        record.getUserId(), record.getDeviceId(), payScoreOrderId);
             } else {
-                // Redis 预授权已过期,尝试从追踪SET恢复
+                // 第三层:兜底从追踪SET恢复
                 payScoreOrderId = ((com.haha.service.payment.payscore.impl.PayScoreServiceImpl) payScoreService)
                         .tryRecoverPayScoreOrderId(record.getUserId(), record.getDeviceId());
                 if (payScoreOrderId != null) {
@@ -509,7 +517,7 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
                     log.info("从追踪SET恢复支付分订单ID成功 - userId: {}, deviceId: {}, payScoreOrderId: {}",
                             record.getUserId(), record.getDeviceId(), payScoreOrderId);
                 } else {
-                    log.warn("Redis预授权已过期且追踪SET未找到 - userId: {}, deviceId: {}",
+                    log.warn("Redis/DoorRecord/追踪SET 均未找到支付分信息 - userId: {}, deviceId: {}",
                             record.getUserId(), record.getDeviceId());
                 }
             }
@@ -1174,20 +1182,32 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
                     com.alibaba.fastjson2.JSONObject preAuth = ((com.haha.service.payment.payscore.impl.PayScoreServiceImpl) payScoreService)
                             .readPreAuthFromRedis(deviceId, uid);
                     if (preAuth != null) {
+                        // 第一层:Redis 命中
                         payScoreOrderId = preAuth.getString("payScoreOrderId");
                         payScoreState = preAuth.getString("payScoreState");
                         serviceId = preAuth.getString("serviceId");
                     } else {
-                        // Redis 预授权已过期,尝试从追踪SET恢复
-                        payScoreOrderId = ((com.haha.service.payment.payscore.impl.PayScoreServiceImpl) payScoreService)
-                                .tryRecoverPayScoreOrderId(uid, deviceId);
-                        if (payScoreOrderId != null) {
+                        // 第二层:尝试从 DoorRecord(DB 持久化)恢复
+                        DoorRecord dr = doorRecordService.getByActivityId(activityId);
+                        if (dr != null && dr.getPayScoreOrderId() != null) {
+                            payScoreOrderId = dr.getPayScoreOrderId();
                             payScoreState = PayScoreState.CREATED.getCode();
-                            serviceId = payScoreService.getServiceId();
-                            log.info("从追踪SET恢复支付分订单ID成功 - userId: {}, deviceId: {}, payScoreOrderId: {}",
+                            serviceId = dr.getServiceId();
+                            log.info("从DoorRecord恢复支付分订单ID - userId: {}, deviceId: {}, payScoreOrderId: {}",
                                     userId, deviceId, payScoreOrderId);
                         } else {
-                            log.warn("Redis预授权已过期且追踪SET未找到 - userId: {}, deviceId: {}", userId, deviceId);
+                            // 第三层:兜底从追踪SET恢复
+                            payScoreOrderId = ((com.haha.service.payment.payscore.impl.PayScoreServiceImpl) payScoreService)
+                                    .tryRecoverPayScoreOrderId(uid, deviceId);
+                            if (payScoreOrderId != null) {
+                                payScoreState = PayScoreState.CREATED.getCode();
+                                serviceId = payScoreService.getServiceId();
+                                log.info("从追踪SET恢复支付分订单ID成功 - userId: {}, deviceId: {}, payScoreOrderId: {}",
+                                        userId, deviceId, payScoreOrderId);
+                            } else {
+                                log.warn("Redis/DoorRecord/追踪SET 均未找到支付分信息 - userId: {}, deviceId: {}",
+                                        userId, deviceId);
+                            }
                         }
                     }
                 }

+ 9 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreService.java

@@ -254,4 +254,13 @@ public interface PayScoreService {
      * @return 服务ID,未配置返回 null
      */
     String getServiceId();
+
+    /**
+     * 获取预授权信息(从 Redis 读取,只读不删)
+     *
+     * @param deviceId 设备 ID
+     * @param userId   用户 ID
+     * @return Map 包含 payScoreOrderId, payScoreState, serviceId,不存在返回 null
+     */
+    Map<String, String> getPreAuthInfo(String deviceId, Long userId);
 }

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

@@ -1117,6 +1117,19 @@ public class PayScoreServiceImpl implements PayScoreService {
         return wxPayConfig != null ? wxPayConfig.getServiceId() : null;
     }
 
+    @Override
+    public Map<String, String> getPreAuthInfo(String deviceId, Long userId) {
+        JSONObject preAuth = readPreAuthFromRedis(deviceId, userId);
+        if (preAuth == null) {
+            return null;
+        }
+        Map<String, String> result = new HashMap<>();
+        result.put("payScoreOrderId", preAuth.getString("payScoreOrderId"));
+        result.put("payScoreState", preAuth.getString("payScoreState"));
+        result.put("serviceId", preAuth.getString("serviceId"));
+        return result;
+    }
+
     /**
      * 清理用户旧的进行中支付分订单,确保不超过微信支付分 3 笔限额。
      *