From d4d70fe3a3a20f5fc29c9f37a1f89854a376597f Mon Sep 17 00:00:00 2001
From: yanderong <307678080@qq.com>
Date: 星期三, 16 九月 2026 18:42:36 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/2.3' into 2.3

---
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java |  104 +++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 94 insertions(+), 10 deletions(-)

diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
index 7a821d6..0b54e6b 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
@@ -112,6 +112,10 @@
     private static final int SD_STARTING_TIMEOUT_MINUTES = 30;
     /** SD 充电中超时阈值(小时):超时未收到交易记录则兜底结算 */
     private static final int SD_CHARGING_TIMEOUT_HOURS = 24;
+    /** 状态5未扣款扫描保护窗,避免与正在提交的扣款事务竞态 */
+    private static final int SD_DEDUCT_COMPENSATION_DELAY_MINUTES = 10;
+    /** 单轮最多检查的已结束订单数,避免大批量远程查询 */
+    private static final int SD_DEDUCT_COMPENSATION_BATCH_SIZE = 100;
 
     @Resource
     private TokenService tokenService;
@@ -914,8 +918,7 @@
             log.error("[SD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
         }
         // 首段使用订单实际开始时间;订单尚未写入结束时间时,末段使用交易记录设备结束时间。
-        LocalDateTime settlementStartTime = order.getStartTime() != null
-                ? order.getStartTime() : parseDeviceTime(vo.getStart_time(), null);
+        LocalDateTime settlementStartTime = parseDeviceTime(vo.getStart_time(), order.getStartTime());
         LocalDateTime settlementEndTime = parseDeviceTime(vo.getEnd_time(), order.getEndTime());
         if (!details.isEmpty()) {
             DateTimeFormatter hhmm = DateTimeFormatter.ofPattern("HH:mm");
@@ -975,23 +978,33 @@
         order.setOrderAmount(orderAmount);
         order.setPaymentAmount(orderAmount);
 
-        // 卡账户扣款(订单号幂等键;允许扣成负余额;失败欠费记账不回滚订单)
+        // 卡账户扣款(订单号幂等键;允许扣成负余额);无法确认扣款成功时回退状态4,交由补偿任务重试
         if (orderAmount.compareTo(BigDecimal.ZERO) > 0 && order.getCardId() != null) {
             ChargingCardDeductDTO deductDTO = new ChargingCardDeductDTO();
             deductDTO.setCardId(order.getCardId());
             deductDTO.setOrderCode(order.getCode());
             deductDTO.setAmount(orderAmount);
-            R<ChargingCardDeductVO> deductResult = chargingCardClient.deductForCharging(deductDTO);
-            if (deductResult != null && deductResult.getData() != null
-                    && deductResult.getData().isDuplicated()) {
-                log.warn("[SD结算] 扣款幂等命中(重复请求不重复扣)code={}", code);
-            } else if (deductResult == null || deductResult.getCode() != R.SUCCESS) {
-                log.error("[SD结算] 扣款失败,欠费记账 code={} cardId={} amount={} msg={}",
+            R<ChargingCardDeductVO> deductResult;
+            try {
+                deductResult = chargingCardClient.deductForCharging(deductDTO);
+            } catch (Exception ex) {
+                rollbackFinishedToStopping(order.getId(), code);
+                log.error("[SD结算] 扣款调用异常,订单回退停止中 code={} cardId={} amount={}",
+                        code, order.getCardId(), orderAmount, ex);
+                return R.fail("充电卡扣款失败,订单已进入补偿队列");
+            }
+            if (deductResult == null || deductResult.getCode() != R.SUCCESS || deductResult.getData() == null) {
+                rollbackFinishedToStopping(order.getId(), code);
+                log.error("[SD结算] 扣款失败,订单回退停止中 code={} cardId={} amount={} msg={}",
                         code, order.getCardId(), orderAmount, deductResult == null ? "null" : deductResult.getMsg());
+                return R.fail("充电卡扣款失败,订单已进入补偿队列");
+            }
+            if (deductResult.getData().isDuplicated()) {
+                log.warn("[SD结算] 扣款幂等命中(重复请求不重复扣)code={}", code);
             }
         }
 
-        // 解锁卡片(无论扣款成败必须解锁,失败人工介入)
+        // 扣款成功后解锁卡片
         unlockCardQuietly(order);
         // 复位充电枪
         resetGunQuietly(order);
@@ -1000,6 +1013,18 @@
         log.info("[SD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={}",
                 code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount);
         return R.ok();
+    }
+
+    private boolean rollbackFinishedToStopping(Long orderId, String code) {
+        boolean rolledBack = lambdaUpdate()
+                .eq(TCardChargingOrder::getId, orderId)
+                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_FINISHED)
+                .set(TCardChargingOrder::getStatus, ORDER_STATUS_STOPPING)
+                .update();
+        if (!rolledBack) {
+            log.warn("[SD结算] 状态5回退4未命中,订单状态可能已变化 code={}", code);
+        }
+        return rolledBack;
     }
 
     /**
@@ -1132,6 +1157,65 @@
     }
 
     @Override
+    public int recoverFinishedOrdersWithoutDeduct() {
+        List<TCardChargingOrder> candidates = lambdaQuery()
+                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_FINISHED)
+                .isNotNull(TCardChargingOrder::getCardId)
+                .gt(TCardChargingOrder::getOrderAmount, BigDecimal.ZERO)
+                .lt(TCardChargingOrder::getEndTime, LocalDateTime.now().minusMinutes(SD_DEDUCT_COMPENSATION_DELAY_MINUTES))
+                .orderByAsc(TCardChargingOrder::getEndTime)
+                .last("LIMIT " + SD_DEDUCT_COMPENSATION_BATCH_SIZE)
+                .list();
+        if (candidates.isEmpty()) {
+            return 0;
+        }
+
+        List<String> orderCodes = candidates.stream().map(TCardChargingOrder::getCode)
+                .filter(StringUtils::isNotEmpty).collect(Collectors.toList());
+        R<List<String>> deductedResult = chargingCardClient.listDeductedOrderCodes(orderCodes);
+        if (deductedResult == null || deductedResult.getCode() != R.SUCCESS || deductedResult.getData() == null) {
+            log.error("[SD扣款补偿] 查询扣款流水失败,本轮不修改订单 msg={}",
+                    deductedResult == null ? "null" : deductedResult.getMsg());
+            return 0;
+        }
+
+        java.util.Set<String> deductedCodes = new java.util.HashSet<>(deductedResult.getData());
+        int recovered = 0;
+        for (TCardChargingOrder candidate : candidates) {
+            if (StringUtils.isEmpty(candidate.getCode()) || deductedCodes.contains(candidate.getCode())) {
+                continue;
+            }
+            RLock settleLock = redissonClient.getLock(LOCK_SD_SETTLE + candidate.getCode());
+            try {
+                if (!settleLock.tryLock(0, 60, TimeUnit.SECONDS)) {
+                    continue;
+                }
+                R<List<String>> recheck = chargingCardClient.listDeductedOrderCodes(
+                        Collections.singletonList(candidate.getCode()));
+                if (recheck == null || recheck.getCode() != R.SUCCESS || recheck.getData() == null
+                        || recheck.getData().contains(candidate.getCode())) {
+                    continue;
+                }
+                if (rollbackFinishedToStopping(candidate.getId(), candidate.getCode())) {
+                    recovered++;
+                    log.warn("[SD扣款补偿] 已结束订单无扣款流水,状态回退停止中 code={}", candidate.getCode());
+                }
+            } catch (InterruptedException ex) {
+                Thread.currentThread().interrupt();
+                log.warn("[SD扣款补偿] 获取结算锁被中断 code={}", candidate.getCode());
+                break;
+            } catch (Exception ex) {
+                log.error("[SD扣款补偿] 订单处理异常,保留原状态 code={}", candidate.getCode(), ex);
+            } finally {
+                if (settleLock.isHeldByCurrentThread()) {
+                    settleLock.unlock();
+                }
+            }
+        }
+        return recovered;
+    }
+
+    @Override
     public void closeTimeoutStartingOrder(TCardChargingOrder order) {
         TCardChargingOrder update = new TCardChargingOrder();
         update.setStatus(ORDER_STATUS_START_FAILED);

--
Gitblit v1.7.1