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