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