From 67d25f3d9eb0124c7aab544bf8f77031d6d8adc3 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期三, 30 九月 2026 10:36:18 +0800
Subject: [PATCH] 增加公司账号权限功能
---
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java | 152 ++++++++++++++++++++++++++++++++++----------------
1 files changed, 104 insertions(+), 48 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 fc2a9f5..439d08a 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
@@ -673,37 +673,40 @@
order.setCreateTime(LocalDateTime.now());
save(order);
- // 5. 锁定账户额度(实扣冻结:共享=企业账户,分配=卡账户;锁定失败则关单并回告余额不足)
+ // 5. 计费策略快照(枪策略优先,站点策略兜底,strategyId 在授权校验前已解析):
+ // 无可用策略(未配置/已被删除、明细为空)或保存异常时无法计费,直接回滚删除订单并回告启动失败,
+ // 避免产生无策略订单(结算侧的价格兜底仅用于历史订单)
+ try {
+ if (!saveStrategySnapshot(order.getId(), strategyId)) {
+ log.warn("[SD建单] 无可用计费策略,回滚删除订单 code={} strategyId={}", order.getCode(), strategyId);
+ removeOrderWithSnapshot(order.getId());
+ return DeviceStartChargingVO.fail(null);
+ }
+ } catch (Exception snapshotEx) {
+ log.error("[SD建单] 保存计费策略快照异常,回滚删除订单 orderId={} strategyId={}", order.getId(),
+ strategyId, snapshotEx);
+ removeOrderWithSnapshot(order.getId());
+ return DeviceStartChargingVO.fail(null);
+ }
+
+ // 6. 锁定账户额度(实扣冻结:共享=企业账户,分配=卡账户;锁定失败则关单并回告余额不足)
if (!lockCardChargeContext(card, chargeContext)) {
- removeById(order.getId());
+ removeOrderWithSnapshot(order.getId());
return DeviceStartChargingVO.fail(3);
}
- // 5.1 回填锁定额度(订单列表/详情展示与结算释放使用),同步内存对象,保证后续回滚能按额度释放
+ // 6.1 回填锁定额度(订单列表/详情展示与结算释放使用),同步内存对象,保证后续回滚能按额度释放
TCardChargingOrder accountUpdate = new TCardChargingOrder();
accountUpdate.setId(order.getId());
accountUpdate.setLockedAmount(chargeContext.getDispatchAmount());
updateById(accountUpdate);
order.setLockedAmount(chargeContext.getDispatchAmount());
- // 6. 计费策略快照(枪策略优先,站点策略兜底,strategyId 在授权校验前已解析)
- try {
- saveStrategySnapshot(order.getId(), strategyId);
- } catch (Exception snapshotEx) {
- log.error("[SD建单] 保存计费策略快照失败 orderId={} strategyId={}", order.getId(), strategyId, snapshotEx);
- }
-
// 7. 锁卡(条件更新:仅空闲卡可锁;失败释放账户锁定额并回滚删除订单与快照)
R<Boolean> lockResult = chargingCardClient.lockForCharging(card.getId(), order.getCode());
if (!Boolean.TRUE.equals(lockResult.getData())) {
log.warn("[SD建单] 锁卡失败回滚订单 code={} cardId={} msg={}", order.getCode(), card.getId(), lockResult.getMsg());
unlockCardChargeContextQuietly(order, "锁卡失败");
- cardAccountingStrategyDetailOrderService.remove(
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyDetailOrder>()
- .eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, order.getId()));
- cardAccountingStrategyOrderService.remove(
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyOrder>()
- .eq(TCardAccountingStrategyOrder::getChargingOrderId, order.getId()));
- removeById(order.getId());
+ removeOrderWithSnapshot(order.getId());
return DeviceStartChargingVO.fail(4);
}
log.info("[SD建单] 刷卡充电订单创建成功 code={} cardNo={} gun={} accountMode={} lockAmount={} maxEndTime={}",
@@ -830,16 +833,34 @@
}
/**
- * 释放账户锁定额(建单失败/回滚场景):共享=企业账户,分配=卡账户;失败仅告警,留待补偿任务/人工处理
+ * 释放账户锁定额(建单失败/回滚/关单场景):共享=企业账户,分配=卡账户;
+ * 订单侧 lock_released 条件更新抢占,保证同一订单的锁定额在多路径/重复补偿下只释放一次;
+ * 释放失败时把标记回滚为未释放,留待补偿任务/人工重试
*/
private void unlockCardChargeContextQuietly(TCardChargingOrder order, String scene) {
- if (order == null || order.getCompanyId() == null) {
+ if (order == null || order.getId() == null || order.getCompanyId() == null) {
return;
}
BigDecimal amount = order.getLockedAmount();
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
+ boolean claimed;
+ try {
+ claimed = lambdaUpdate()
+ .eq(TCardChargingOrder::getId, order.getId())
+ .eq(TCardChargingOrder::getLockReleased, 0)
+ .set(TCardChargingOrder::getLockReleased, 1)
+ .update();
+ } catch (Exception ex) {
+ log.error("[SD建单] 抢占锁定额释放标记异常 scene={} code={}", scene, order.getCode(), ex);
+ return;
+ }
+ if (!claimed) {
+ log.info("[SD建单] 锁定额已释放,跳过重复释放 scene={} code={}", scene, order.getCode());
+ return;
+ }
+ boolean unlocked = false;
try {
R<BigDecimal> unlockResult;
if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode())) {
@@ -847,31 +868,61 @@
} else {
unlockResult = groupCompanyClient.unlockBalance(order.getCompanyId(), amount);
}
- if (unlockResult == null || unlockResult.getCode() != R.SUCCESS) {
+ unlocked = unlockResult != null && unlockResult.getCode() == R.SUCCESS;
+ if (!unlocked) {
log.error("[SD建单] 释放账户锁定额失败 scene={} code={} amount={} msg={}", scene, order.getCode(),
amount, unlockResult == null ? "null" : unlockResult.getMsg());
}
} catch (Exception ex) {
log.error("[SD建单] 释放账户锁定额异常 scene={} code={} amount={}", scene, order.getCode(), amount, ex);
}
+ if (!unlocked) {
+ lambdaUpdate()
+ .eq(TCardChargingOrder::getId, order.getId())
+ .eq(TCardChargingOrder::getLockReleased, 1)
+ .set(TCardChargingOrder::getLockReleased, 0)
+ .update();
+ }
+ }
+
+ /**
+ * 回滚删除订单及其计费策略快照(建单失败场景;快照可能尚未写入,删除为幂等操作)
+ */
+ private void removeOrderWithSnapshot(Long orderId) {
+ if (orderId == null) {
+ return;
+ }
+ try {
+ cardAccountingStrategyDetailOrderService.remove(
+ new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyDetailOrder>()
+ .eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, orderId));
+ cardAccountingStrategyOrderService.remove(
+ new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyOrder>()
+ .eq(TCardAccountingStrategyOrder::getChargingOrderId, orderId));
+ } catch (Exception ex) {
+ log.error("[SD建单] 清理计费策略快照异常 orderId={}", orderId, ex);
+ }
+ removeById(orderId);
}
/**
* 保存计费策略快照(主表 + 明细表),与 CD 订单快照逻辑一致
+ *
+ * @return 是否成功写入快照(策略ID为空、策略/明细不存在或明细为空、持久化失败均返回 false,调用方需回滚建单)
*/
- private void saveStrategySnapshot(Long orderId, Integer strategyId) {
+ private boolean saveStrategySnapshot(Long orderId, Integer strategyId) {
if (strategyId == null) {
- return;
+ return false;
}
TAccountingStrategy strategy = accountingStrategyClient.getAccountingStrategyById(strategyId).getData();
List<TAccountingStrategyDetail> details = accountingStrategyDetailClient.getListByAccountingStrategyId(strategyId).getData();
if (strategy == null || details == null || details.isEmpty()) {
- return;
+ return false;
}
TCardAccountingStrategyOrder strategyOrder = new TCardAccountingStrategyOrder();
BeanUtils.copyProperties(strategy, strategyOrder);
strategyOrder.setChargingOrderId(orderId);
- cardAccountingStrategyOrderService.save(strategyOrder);
+ boolean mainSaved = cardAccountingStrategyOrderService.save(strategyOrder);
List<TCardAccountingStrategyDetailOrder> detailOrders = new ArrayList<>();
for (TAccountingStrategyDetail detail : details) {
TCardAccountingStrategyDetailOrder detailOrder = new TCardAccountingStrategyDetailOrder();
@@ -879,7 +930,8 @@
detailOrder.setChargingOrderId(orderId);
detailOrders.add(detailOrder);
}
- cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
+ boolean detailSaved = cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
+ return mainSaved && detailSaved;
}
/**
@@ -1049,7 +1101,7 @@
/**
* 判断是否触发授权时间到期主动停充(提前 AUTHORIZE_STOP_AHEAD_SECONDS 秒下发,规避数据传输延迟):
- * 授权时长不足提前量时按授权时长的一半提前,避免刚启动即被停充;
+ * 授权总时长不足固定提前量时按整个授权时长提前(启动后即触发停充),确保授权截止时间前结束充电;
* maxEndTime 为空(全天授权/未开启自动停充)时不介入。
*/
private boolean needAuthorizeStop(TCardChargingOrder order) {
@@ -1383,10 +1435,10 @@
}
afterBalance = settleResult.getData().getAfterBalance();
} else {
- // 订单侧自行写消费流水,此处不传订单号(账户侧仅在订单号非空时做幂等与流水,避免重复记账)
+ // 共享模式:传订单号启用账户侧结算幂等(账户侧按订单号+变更类型落企业消费流水并作为已结算标记)
R<BigDecimal> settleResult =
groupCompanyClient.settleBalance(order.getCompanyId(), lockedAmount, actual,
- null, null, null, null);
+ code, order.getCardNo(), null, "刷卡充电结算");
if (settleResult == null || settleResult.getCode() != R.SUCCESS) {
log.error("[SD结算] 企业账户结算失败 code={} companyId={} locked={} actual={} msg={}", code,
order.getCompanyId(), lockedAmount, actual,
@@ -1400,7 +1452,10 @@
lockedAmount, actual, ex);
return false;
}
- recordCardConsumeAllocate(order, actual, lockedAmount, afterBalance, code);
+ if (allocated) {
+ // 分配模式消费流水记卡账户;共享模式消费流水已由账户侧 settleBalance 按订单号写入,避免重复记账
+ recordCardConsumeAllocate(order, actual, lockedAmount, afterBalance, code);
+ }
log.info("[SD结算] 账户结算完成 code={} accountMode={} locked={} actual={} balance={}", code,
order.getAccountMode(), lockedAmount, actual, afterBalance);
return true;
@@ -1438,7 +1493,8 @@
}
/**
- * 写企业额度分配记录-消费流水(分配模式记该卡账户;共享模式记企业账户):
+ * 写企业额度分配记录-消费流水(仅卡侧扣款场景:分配模式结算与历史订单卡扣款,记该卡账户):
+ * 共享模式消费流水由账户侧 settleBalance 按订单号写入(同一订单号+变更类型只能有一个写入方,避免重复记账);
* 消费前余额 = 结算后余额 + 实际消费 - 锁定额度(实扣冻结口径反推);失败不影响结算主流程
*/
private void recordCardConsumeAllocate(TCardChargingOrder order, BigDecimal actual, BigDecimal lockedAmount,
@@ -1448,21 +1504,8 @@
}
BigDecimal after = afterBalance == null ? BigDecimal.ZERO : afterBalance;
BigDecimal before = after.add(actual).subtract(lockedAmount);
- boolean allocated = Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode());
- String userInfo;
- String userName;
- if (allocated) {
- userInfo = order.getCardNo();
- userName = order.getCardNo();
- } else {
- TGroupCompany company = chargeAccountSupport.queryCompanyQuietly(order.getCompanyId());
- userInfo = company == null || StringUtils.isEmpty(company.getName())
- ? String.valueOf(order.getCompanyId()) : company.getName();
- userName = company == null ? null : company.getName();
- }
- chargeAccountSupport.recordConsume(order.getCompanyId(),
- allocated ? TCompanyAllocateRecord.USER_TYPE_CARD : TCompanyAllocateRecord.USER_TYPE_COMPANY,
- userInfo, userName, before, actual.negate(), code, "刷卡充电结算");
+ chargeAccountSupport.recordConsume(order.getCompanyId(), TCompanyAllocateRecord.USER_TYPE_CARD,
+ order.getCardNo(), order.getCardNo(), before, actual.negate(), code, "刷卡充电结算");
}
private boolean rollbackFinishedToStopping(Long orderId, String code) {
@@ -1622,14 +1665,21 @@
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={}",
+ log.error("[SD扣款补偿] 查询卡侧扣款流水失败,本轮不修改订单 msg={}",
deductedResult == null ? "null" : deductedResult.getMsg());
+ return 0;
+ }
+ java.util.Set<String> consumedCodes = chargeAccountSupport.listConsumedOrderCodesQuietly(orderCodes);
+ if (consumedCodes == null) {
+ log.error("[SD扣款补偿] 查询企业侧消费流水失败,本轮不修改订单");
return 0;
}
java.util.Set<String> deductedCodes = new java.util.HashSet<>(deductedResult.getData());
+ deductedCodes.addAll(consumedCodes);
int recovered = 0;
for (TCardChargingOrder candidate : candidates) {
if (StringUtils.isEmpty(candidate.getCode()) || deductedCodes.contains(candidate.getCode())) {
@@ -1642,8 +1692,14 @@
}
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())) {
+ if (recheck == null || recheck.getCode() != R.SUCCESS || recheck.getData() == null) {
+ continue;
+ }
+ // 双判据复核:任一侧查询失败或存在流水即不回退(保守处理,避免重复扣款)
+ java.util.Set<String> consumedRecheck = chargeAccountSupport.listConsumedOrderCodesQuietly(
+ Collections.singletonList(candidate.getCode()));
+ if (consumedRecheck == null || recheck.getData().contains(candidate.getCode())
+ || consumedRecheck.contains(candidate.getCode())) {
continue;
}
if (rollbackFinishedToStopping(candidate.getId(), candidate.getCode())) {
--
Gitblit v1.7.1