From a0e03b646f2c444b0f09687d24e9995112b99017 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期六, 10 十月 2026 17:30:11 +0800
Subject: [PATCH] 修改bug

---
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java |  248 +++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 228 insertions(+), 20 deletions(-)

diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java
index 28eb07a..3ce22d4 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java
@@ -10,10 +10,13 @@
 import com.ruoyi.account.api.model.TChargingCard;
 import com.ruoyi.account.api.model.TCompanyAllocateRecord;
 import com.ruoyi.account.api.model.TVinManagement;
+import com.ruoyi.account.api.vo.CompanyAllocateSummaryVO;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.exception.ServiceException;
 import com.ruoyi.common.core.utils.StringUtils;
 import com.ruoyi.common.core.web.domain.AjaxResult;
+import com.ruoyi.common.security.utils.SecurityUtils;
+import com.ruoyi.order.api.feignClient.ChargingOrderClient;
 import com.ruoyi.other.api.domain.TFleet;
 import com.ruoyi.other.api.domain.TGroupCompany;
 import com.ruoyi.other.api.model.TCompanyRechargeRecord;
@@ -30,8 +33,11 @@
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
@@ -41,6 +47,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.stream.Collectors;
 
 /**
@@ -85,6 +92,21 @@
     /** 账户状态:欠款 */
     private static final int ACCOUNT_STATUS_OVERDUE = 2;
 
+    /** 公司登录账号前缀:admin */
+    private static final String ACCOUNT_PREFIX = "admin";
+
+    /** 公司登录账号随机数字位数 */
+    private static final int ACCOUNT_RANDOM_DIGITS = 4;
+
+    /** 账号生成查重重试上限 */
+    private static final int ACCOUNT_GENERATE_MAX_ATTEMPTS = 20;
+
+    /** 新增公司初始密码(明文,落库前按登录口径 HMAC-MD5 处理) */
+    private static final String DEFAULT_INITIAL_PASSWORD = "123456";
+
+    /** 密码传输 HMAC 密钥(与前端 CryptoJS.HmacMD5(password, 'password') 口径一致) */
+    private static final String PASSWORD_HMAC_KEY = "password";
+
     /**
      * 车队 Mapper。
      * 此处注入 Mapper 而非 TFleetService:TFleetServiceImpl 已依赖 TGroupCompanyService,
@@ -120,6 +142,10 @@
     /** 企业额度分配记录远程客户端(结算时写消费流水、幂等判定使用) */
     @Resource
     private CompanyAllocateRecordClient companyAllocateRecordClient;
+
+    /** 充电订单远程客户端(账户模式切换前进行中订单校验使用) */
+    @Resource
+    private ChargingOrderClient chargingOrderClient;
 
     @Override
     public TGroupCompany getDetail(Integer id) {
@@ -380,30 +406,21 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult addCompany(TGroupCompany company) {
-        if (!StringUtils.hasText(company.getAccount())) {
-            return AjaxResult.error("登录账号不能为空");
-        }
+        // 登录账号由系统自动生成:admin + 4位随机数字,前端不可指定、不可修改
+        String account = generateCompanyAccount();
+        company.setAccount(account);
         // 添加企业弹窗不再配置账户模式:未指定时按共享模式建档,后续可在“账户模式”弹窗切换
         if (company.getAccountMode() == null) {
             company.setAccountMode(ACCOUNT_MODE_SHARED);
         }
-        if (!StringUtils.hasText(company.getPassword())) {
-            return AjaxResult.error("登录密码不能为空");
-        }
-        if (accountExists(company.getAccount(), null)) {
-            return AjaxResult.error("登录账号不能重复");
-        }
-        R<SysUser> existR = sysUserClient.queryUserByUserName(company.getAccount());
-        SysUser existUser = existR == null ? null : existR.getData();
-        if (existUser != null) {
-            return AjaxResult.error("登录账号不能重复");
-        }
+        // 初始密码固定 123456,按登录口径 HMAC-MD5 后由 system 侧 BCrypt 落库
+        String password = hmacMd5(DEFAULT_INITIAL_PASSWORD, PASSWORD_HMAC_KEY);
         Long roleId = resolveCompanyRoleId();
         SysUser user = new SysUser();
-        user.setUserName(company.getAccount());
+        user.setUserName(account);
         user.setNickName(company.getName());
         user.setPhonenumber(company.getContactPhone());
-        user.setPassword(company.getPassword());
+        user.setPassword(password);
         user.setStatus("0");
         user.setDelFlag("0");
         user.setRoleId(roleId);
@@ -421,7 +438,47 @@
             created.setObjectId(company.getId());
             sysUserClient.updateUser(created);
         }
-        return AjaxResult.success();
+        // 返回生成的登录账号,供添加页回显(初始密码为固定 123456)
+        return AjaxResult.success("操作成功", account);
+    }
+
+    /**
+     * 生成公司登录账号:admin + 4位随机数字,公司表与 sys_user 双查重,冲突重试
+     */
+    private String generateCompanyAccount() {
+        int start = (int) Math.pow(10, ACCOUNT_RANDOM_DIGITS - 1);
+        int bound = (int) Math.pow(10, ACCOUNT_RANDOM_DIGITS) - start;
+        for (int i = 0; i < ACCOUNT_GENERATE_MAX_ATTEMPTS; i++) {
+            String candidate = ACCOUNT_PREFIX + (start + ThreadLocalRandom.current().nextInt(bound));
+            if (accountExists(candidate, null)) {
+                continue;
+            }
+            R<SysUser> existR = sysUserClient.queryUserByUserName(candidate);
+            SysUser existUser = existR == null ? null : existR.getData();
+            if (existUser == null) {
+                return candidate;
+            }
+        }
+        throw new ServiceException("登录账号生成失败,请稍后重试");
+    }
+
+    /**
+     * HMAC-MD5(小写 Hex),与前端 CryptoJS.HmacMD5(text, key) 口径一致
+     */
+    private String hmacMd5(String text, String key) {
+        try {
+            Mac mac = Mac.getInstance("HmacMD5");
+            mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacMD5"));
+            byte[] bytes = mac.doFinal(text.getBytes(StandardCharsets.UTF_8));
+            StringBuilder hex = new StringBuilder(bytes.length * 2);
+            for (byte b : bytes) {
+                hex.append(Character.forDigit((b >> 4) & 0xF, 16));
+                hex.append(Character.forDigit(b & 0xF, 16));
+            }
+            return hex.toString();
+        } catch (Exception e) {
+            throw new ServiceException("密码处理失败: " + e.getMessage());
+        }
     }
 
     @Override
@@ -548,11 +605,162 @@
         if (company == null) {
             return AjaxResult.error("公司不存在");
         }
+        // 目标模式与当前一致:幂等直接返回
+        if (Objects.equals(company.getAccountMode(), accountMode)) {
+            return AjaxResult.success();
+        }
+        // 统一校验:无论切换方向,存在进行中充电订单均拒绝切换
+        AjaxResult chargingCheck = ensureNoActiveChargingOrder(id);
+        if (chargingCheck != null) {
+            return chargingCheck;
+        }
+        // 共享 → 分配:仅切换模式,不涉及资金回退
+        if (accountMode == ACCOUNT_MODE_ALLOCATION) {
+            TGroupCompany update = new TGroupCompany();
+            update.setId(id);
+            update.setAccountMode(accountMode);
+            updateById(update);
+            return AjaxResult.success();
+        }
+        // 分配 → 共享:校验在途业务后回收全部分配余额
+        return switchAllocationToShared(id, company);
+    }
+
+    /**
+     * 校验公司无进行中充电订单(三表合计,状态 2=启动中/3=充电中/4=停止中)。
+     * 通过返回 null;不通过返回可直接回给前端的错误结果。
+     * 账户模式双向切换均须先通过本校验。
+     */
+    private AjaxResult ensureNoActiveChargingOrder(Integer id) {
+        R<Integer> activeCountResult;
+        try {
+            activeCountResult = chargingOrderClient.countActiveByCompany(id);
+        } catch (RuntimeException ex) {
+            log.error("[模式切换] 统计进行中充电订单异常 companyId={}", id, ex);
+            return AjaxResult.error("校验充电状态失败,请稍后重试");
+        }
+        if (activeCountResult == null || R.SUCCESS != activeCountResult.getCode()) {
+            return AjaxResult.error("校验充电状态失败,请稍后重试");
+        }
+        Integer activeCount = activeCountResult.getData();
+        if (activeCount != null && activeCount > 0) {
+            return AjaxResult.error("该公司存在正在充电的订单,暂不允许切换账户模式");
+        }
+        return null;
+    }
+
+    /**
+     * 分配模式切换为共享模式:
+     * <ol>
+     *   <li>校验公司与三类主体均无未释放充电锁定额</li>
+     *   <li>公司余额预加回回收总额 + 切换为共享模式(本地事务)</li>
+     *   <li>远程回收三类主体全部余额并写双向流水(account 侧锁内事务,幂等)</li>
+     *   <li>按实际回收额与预加回额差额校准公司余额(并发分配竞态兜底)</li>
+     * </ol>
+     * 进行中充电订单校验已在 updateAccountMode 公共入口完成。
+     */
+    private AjaxResult switchAllocationToShared(Integer id, TGroupCompany company) {
+        // 1. 公司锁定额校验(在途充电锁定未释放)
+        BigDecimal companyLocked = company.getLockedAmount();
+        if (companyLocked != null && companyLocked.signum() > 0) {
+            return AjaxResult.error("公司存在未释放的充电锁定金额,暂不允许切换为共享模式");
+        }
+        // 2. 三类主体余额与锁定额统计
+        R<CompanyAllocateSummaryVO> summaryResult;
+        try {
+            summaryResult = companyAllocateRecordClient.sumAllocatedBalance(id);
+        } catch (RuntimeException ex) {
+            log.error("[模式切换] 统计分配余额异常 companyId={}", id, ex);
+            return AjaxResult.error("查询分配余额失败,请稍后重试");
+        }
+        if (summaryResult == null || R.SUCCESS != summaryResult.getCode() || summaryResult.getData() == null) {
+            return AjaxResult.error("查询分配余额失败,请稍后重试");
+        }
+        CompanyAllocateSummaryVO summary = summaryResult.getData();
+        if (summary.getTotalLocked() != null && summary.getTotalLocked().signum() > 0) {
+            return AjaxResult.error("存在充电锁定金额未释放,暂不允许切换为共享模式");
+        }
+        BigDecimal total = summary.getTotalBalance() == null ? BigDecimal.ZERO : summary.getTotalBalance();
+        BigDecimal companyBalanceBefore = company.getAvailableBalance() == null
+                ? BigDecimal.ZERO : company.getAvailableBalance();
+        // 3. 公司余额加回 + 模式切换(本地事务)
+        if (!updateCompanyToShared(id, total)) {
+            throw new ServiceException("公司余额更新失败");
+        }
+        // 4. 远程回收三类主体余额(account 侧三把公司级锁串行、事务、幂等),失败回滚公司余额与模式
+        BigDecimal actual;
+        try {
+            actual = recycleAllocatedBalanceRemote(id, company, companyBalanceBefore);
+        } catch (RuntimeException ex) {
+            log.error("[模式切换] 远程回收分配余额异常 companyId={}", id, ex);
+            rollbackModeSwitch(id, total);
+            throw new ServiceException("回收分配余额失败,请稍后重试");
+        }
+        // 5. 差额调整:回收与统计间存在并发分配时,按实际回收额校准公司余额
+        BigDecimal diff = actual.subtract(total);
+        if (diff.signum() != 0) {
+            boolean adjusted = lambdaUpdate()
+                    .eq(TGroupCompany::getId, id)
+                    .setSql("available_balance = IFNULL(available_balance, 0) + (" + diff.toPlainString() + ")")
+                    .update();
+            if (!adjusted) {
+                log.error("[模式切换] 公司余额差额调整失败 companyId={} diff={}", id, diff.toPlainString());
+            }
+        }
+        log.info("[模式切换] 分配→共享成功 companyId={} 回收总额={}", id, actual.toPlainString());
+        return AjaxResult.success("账户模式已切换为共享模式,回收分配余额 " + actual.toPlainString() + " 元");
+    }
+
+    /** 更新公司余额(加回回收额)并切换为共享模式,返回是否更新成功 */
+    private boolean updateCompanyToShared(Integer id, BigDecimal total) {
+        if (total.signum() > 0) {
+            return lambdaUpdate()
+                    .eq(TGroupCompany::getId, id)
+                    .setSql("available_balance = IFNULL(available_balance, 0) + " + total.toPlainString())
+                    .set(TGroupCompany::getAccountMode, ACCOUNT_MODE_SHARED)
+                    .update();
+        }
         TGroupCompany update = new TGroupCompany();
         update.setId(id);
-        update.setAccountMode(accountMode);
-        updateById(update);
-        return AjaxResult.success();
+        update.setAccountMode(ACCOUNT_MODE_SHARED);
+        return updateById(update);
+    }
+
+    /** 远程回收三类主体分配余额,失败(远程失败码/返回体异常)抛出 ServiceException 由调用方补偿 */
+    private BigDecimal recycleAllocatedBalanceRemote(Integer id, TGroupCompany company,
+                                                     BigDecimal companyBalanceBefore) {
+        String operatorName = null;
+        try {
+            operatorName = SecurityUtils.getLoginUser().getUsername();
+        } catch (Exception ignored) {
+            // 无安全上下文时(内部调用)操作人留空
+        }
+        R<BigDecimal> recycleResult = companyAllocateRecordClient.recycleAllocatedBalance(id, operatorName,
+                company.getName(), companyBalanceBefore);
+        if (recycleResult == null || R.SUCCESS != recycleResult.getCode() || recycleResult.getData() == null) {
+            String msg = recycleResult == null ? "远程调用失败" : recycleResult.getMsg();
+            log.error("[模式切换] 回收分配余额失败 companyId={} msg={}", id, msg);
+            throw new ServiceException("回收分配余额失败:" + msg);
+        }
+        return recycleResult.getData();
+    }
+
+    /**
+     * 回收失败补偿:公司余额减回预加回额、模式恢复为分配模式。
+     * 仅当模式仍为共享时执行恢复,避免覆盖后续并发操作;恢复失败仅记录日志(下轮对账/重试兜底)。
+     */
+    private void rollbackModeSwitch(Integer id, BigDecimal total) {
+        try {
+            boolean rollbacked = lambdaUpdate()
+                    .eq(TGroupCompany::getId, id)
+                    .eq(TGroupCompany::getAccountMode, ACCOUNT_MODE_SHARED)
+                    .setSql("available_balance = IFNULL(available_balance, 0) - " + total.toPlainString())
+                    .set(TGroupCompany::getAccountMode, ACCOUNT_MODE_ALLOCATION)
+                    .update();
+            log.warn("[模式切换] 回收失败已回滚 companyId={} rollbacked={}", id, rollbacked);
+        } catch (RuntimeException ex) {
+            log.error("[模式切换] 回收失败回滚异常 companyId={}", id, ex);
+        }
     }
 
     @Override

--
Gitblit v1.7.1