Explorar o código

feat: 补扣自动恢复商户单号,无需手动填写

- PayScoreServiceImpl: 新增 tryRecoverPayScoreOrderId,从 Redis pre-auth key
  和追踪 SET 中自动匹配恢复丢失的 payScoreOrderId
- repairPayScorePayment: 本地缺失时先自动恢复,恢复失败才提示手动输入
- 前端: outOrderNo 改为折叠的「高级选项」,默认只需填订单ID

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline hai 23 horas
pai
achega
e3046ce7a7

+ 16 - 13
haha-admin-web/src/views/order/utils/hook.tsx

@@ -712,10 +712,10 @@ export function useOrder(tableRef: Ref) {
   }
 
   async function handleRepairPayScore() {
-    const repairForm = reactive({ orderId: "", outOrderNo: "" });
+    const repairForm = reactive({ orderId: "", outOrderNo: "", showAdvanced: false });
 
     addDialog({
-      title: "修复支付分扣",
+      title: "支付分扣",
       width: "460px",
       draggable: true,
       closeOnClickModal: false,
@@ -727,15 +727,18 @@ export function useOrder(tableRef: Ref) {
             </div>
             <ElInput v-model={repairForm.orderId} placeholder="如 202607081727441260270668" />
           </div>
-          <div>
-            <div style="margin-bottom: 4px; font-size: 13px; color: var(--el-text-color-regular);">
-              outOrderNo(选填)
-            </div>
-            <ElInput v-model={repairForm.outOrderNo} placeholder="如 PS1782351340B150534,本地丢失时从微信商户后台获取" />
-            <div style="margin-top: 4px; font-size: 12px; color: #909399;">
-              仅在本地 payScoreOrderId 丢失时填写,从微信商户后台「支付分订单」中查找
-            </div>
+          <div style="margin-bottom: 8px; font-size: 12px; color: #67c23a;">
+            商户单号(PS开头)系统会自动查找,一般无需手动填写
           </div>
+          <div style="cursor: pointer; font-size: 13px; color: var(--el-color-primary); user-select: none;"
+               onClick={() => { repairForm.showAdvanced = !repairForm.showAdvanced; }}>
+            {repairForm.showAdvanced ? '▾ 收起' : '▸ 高级:手动指定商户单号'}
+          </div>
+          {repairForm.showAdvanced && (
+            <div style="margin-top: 8px;">
+              <ElInput v-model={repairForm.outOrderNo} placeholder="如 PS1782351340B150534,从微信商户后台查找" />
+            </div>
+          )}
         </div>
       ),
       beforeSure: async (done) => {
@@ -748,14 +751,14 @@ export function useOrder(tableRef: Ref) {
         try {
           const res = await repairPayScorePayment(orderId, outOrderNo);
           if (res.code === 200) {
-            message(res.message || "修复完成", { type: "success" });
+            message(res.message || "补扣成功", { type: "success" });
             onSearch();
           } else {
-            message(res.message || "修复失败", { type: "error" });
+            message(res.message || "补扣失败", { type: "error" });
           }
           done();
         } catch {
-          message("修复请求失败", { type: "error" });
+          message("补扣请求失败", { type: "error" });
         }
       }
     });

+ 14 - 2
haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

@@ -1320,11 +1320,23 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             return Map.of("success", false, "message", "订单不存在: " + orderIdOrNo);
         }
 
-        // 如果提供了 outOrderNo,先回填到本地订单
+        // 如果本地丢失 payScoreOrderId,尝试从 Redis 自动恢复
+        if ((order.getPayScoreOrderId() == null || order.getPayScoreOrderId().isEmpty())
+                && (payScoreOrderId == null || payScoreOrderId.isEmpty())) {
+            String recovered = payScoreService.tryRecoverPayScoreOrderId(order.getUserId(), order.getDeviceId());
+            if (recovered != null) {
+                payScoreOrderId = recovered;
+                log.info("[支付分修复] 自动恢复 payScoreOrderId - orderId: {}, recovered: {}", order.getId(), recovered);
+            }
+        }
+
+        // 如果提供了 outOrderNo(手动或自动恢复),先回填到本地订单
         if (payScoreOrderId != null && !payScoreOrderId.isEmpty()) {
             if (order.getPayScoreOrderId() == null || order.getPayScoreOrderId().isEmpty()) {
                 order.setPayScoreOrderId(payScoreOrderId);
                 order.setPayScoreState(PayScoreState.DOING.getCode());
+                order.setPayChannel(PaymentChannel.WECHAT_PAYSCORE.getCode());
+                order.setPayType("微信支付分");
                 this.updateById(order);
                 log.info("[支付分修复] 回填 payScoreOrderId - orderId: {}, orderNo: {}, payScoreOrderId: {}",
                         order.getId(), order.getOrderNo(), payScoreOrderId);
@@ -1339,7 +1351,7 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
 
         if (order.getPayScoreOrderId() == null || order.getPayScoreOrderId().isEmpty()) {
             return Map.of("success", false, "message",
-                    "订单未创建支付分服务订单,请在微信商户后台找到 outOrderNo 后输入");
+                    "未找到支付分商户单号,请从微信商户后台查找后手动输入");
         }
 
         if (PayScoreState.isFinished(order.getPayScoreState())) {

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

@@ -1057,6 +1057,51 @@ public class PayScoreServiceImpl implements PayScoreService {
         }
     }
 
+    /**
+     * 从 Redis 追踪 SET 中尝试恢复丢失的 payScoreOrderId
+     * outOrderNo 格式为 PS{timestamp}{deviceIdSuffix},通过 deviceId 后缀匹配
+     *
+     * @return 恢复的 payScoreOrderId,未找到返回 null
+     */
+    public String tryRecoverPayScoreOrderId(Long userId, String deviceId) {
+        if (stringRedisTemplate == null || userId == null || deviceId == null) {
+            return null;
+        }
+        try {
+            // 1. 先查 pre-auth key(最快)
+            JSONObject preAuth = readPreAuthFromRedis(deviceId, userId);
+            if (preAuth != null) {
+                String id = preAuth.getString("payScoreOrderId");
+                if (id != null) {
+                    log.info("[支付分服务] 从pre-auth恢复 payScoreOrderId - userId: {}, deviceId: {}, id: {}", userId, deviceId, id);
+                    return id;
+                }
+            }
+
+            // 2. pre-auth key 不在了,查追踪 SET
+            String trackingKey = REDIS_TRACKING_KEY + userId;
+            java.util.Set<String> tracked = stringRedisTemplate.opsForSet().members(trackingKey);
+            if (tracked != null && !tracked.isEmpty()) {
+                // outOrderNo 格式: PS{timestamp}{deviceIdSuffix}
+                // deviceId 如 B150534 → suffix 为 B150534 (转安全格式后的后10位)
+                String safeDeviceId = deviceId.replaceAll("[^A-Za-z0-9]", "");
+                String suffix = safeDeviceId.length() > 10
+                        ? safeDeviceId.substring(safeDeviceId.length() - 10) : safeDeviceId;
+
+                String matched = tracked.stream()
+                        .filter(outOrderNo -> outOrderNo != null && outOrderNo.endsWith(suffix))
+                        .findFirst().orElse(null);
+                if (matched != null) {
+                    log.info("[支付分服务] 从追踪SET恢复 payScoreOrderId - userId: {}, deviceId: {}, id: {}", userId, deviceId, matched);
+                    return matched;
+                }
+            }
+        } catch (Exception e) {
+            log.warn("[支付分服务] 恢复 payScoreOrderId 异常 - userId: {}, deviceId: {}", userId, deviceId, e);
+        }
+        return null;
+    }
+
     /**
      * 清理用户旧的进行中支付分订单
      * 生产环境下回调正常运转后,此方法只在用户多次快速扫码(测试/异常)时触发