puzhibing
2026-09-15 5d45ba0c31bf1e861cfc3bc1e5343488f6ea23ab
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);