From 7617d46abee2950b7336e3379f8c7a564939cb54 Mon Sep 17 00:00:00 2001
From: yanderong <307678080@qq.com>
Date: 星期六, 10 十月 2026 17:54:56 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/2.3' into 2.3

---
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TCompanyAllocateRecordServiceImpl.java |  232 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 232 insertions(+), 0 deletions(-)

diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TCompanyAllocateRecordServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TCompanyAllocateRecordServiceImpl.java
index 0490675..794d49d 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TCompanyAllocateRecordServiceImpl.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TCompanyAllocateRecordServiceImpl.java
@@ -4,18 +4,32 @@
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.ruoyi.account.api.dto.CompanyAllocateRecordQueryDTO;
+import com.ruoyi.account.api.model.TAppUserWhitelist;
+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.account.mapper.TAppUserWhitelistMapper;
+import com.ruoyi.account.mapper.TChargingCardMapper;
 import com.ruoyi.account.mapper.TCompanyAllocateRecordMapper;
+import com.ruoyi.account.mapper.TVinManagementMapper;
 import com.ruoyi.account.service.TCompanyAllocateRecordService;
+import com.ruoyi.common.core.exception.ServiceException;
 import com.ruoyi.common.core.utils.StringUtils;
 import com.ruoyi.common.security.utils.SecurityUtils;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDateTime;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 公司额度分配记录服务实现:对外提供分页查询与统一流水写入。
@@ -34,6 +48,27 @@
     private static final int MAX_REMARK_LENGTH = 255;
     /** charging_order_code 列长度(varchar(64)) */
     private static final int MAX_ORDER_CODE_LENGTH = 64;
+
+    /** 模式切换回收锁:与额度分配同前缀、按公司串行,防止切换与分配并发导致资金不一致 */
+    private static final String RECYCLE_LOCK_PREFIX_VIN = "VIN_QUOTA:COMPANY:";
+    private static final String RECYCLE_LOCK_PREFIX_CARD = "CHARGING_CARD_QUOTA:COMPANY:";
+    private static final String RECYCLE_LOCK_PREFIX_WHITELIST = "APP_USER_WHITELIST_QUOTA:COMPANY:";
+    /** 获取锁最长等待时间(秒) */
+    private static final long LOCK_WAIT_SECONDS = 5L;
+    /** 锁自动释放时间(秒),兜住异常未解锁的场景 */
+    private static final long LOCK_LEASE_SECONDS = 30L;
+
+    @Resource
+    private RedissonClient redissonClient;
+
+    @Resource
+    private TVinManagementMapper vinManagementMapper;
+
+    @Resource
+    private TChargingCardMapper chargingCardMapper;
+
+    @Resource
+    private TAppUserWhitelistMapper appUserWhitelistMapper;
 
     @Override
     public IPage<TCompanyAllocateRecord> selectRecordPage(CompanyAllocateRecordQueryDTO query) {
@@ -131,6 +166,203 @@
         return consumed;
     }
 
+    @Override
+    public CompanyAllocateSummaryVO sumAllocatedBalance(Integer companyId) {
+        CompanyAllocateSummaryVO summary = new CompanyAllocateSummaryVO();
+        summary.setCompanyId(companyId);
+        BigDecimal totalBalance = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+        BigDecimal totalLocked = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+        int balanceUserCount = 0;
+        if (companyId == null) {
+            return summary;
+        }
+        // VIN码(delFlag 逻辑删除由 @TableLogic 自动过滤)
+        List<TVinManagement> vins = vinManagementMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinManagement>()
+                        .eq(TVinManagement::getCompanyId, companyId)
+                        .gt(TVinManagement::getBalance, BigDecimal.ZERO));
+        if (vins != null) {
+            for (TVinManagement vin : vins) {
+                totalBalance = totalBalance.add(normalizeMoney(vin.getBalance()));
+                totalLocked = totalLocked.add(normalizeMoney(vin.getLockedAmount()));
+                balanceUserCount++;
+            }
+        }
+        // 刷卡卡
+        List<TChargingCard> cards = chargingCardMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TChargingCard>()
+                        .eq(TChargingCard::getCompanyId, companyId)
+                        .gt(TChargingCard::getBalance, BigDecimal.ZERO));
+        if (cards != null) {
+            for (TChargingCard card : cards) {
+                totalBalance = totalBalance.add(normalizeMoney(card.getBalance()));
+                totalLocked = totalLocked.add(normalizeMoney(card.getLockedAmount()));
+                balanceUserCount++;
+            }
+        }
+        // 白名单手机号
+        List<TAppUserWhitelist> whitelists = appUserWhitelistMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TAppUserWhitelist>()
+                        .eq(TAppUserWhitelist::getCompanyId, companyId)
+                        .gt(TAppUserWhitelist::getBalance, BigDecimal.ZERO));
+        if (whitelists != null) {
+            for (TAppUserWhitelist whitelist : whitelists) {
+                totalBalance = totalBalance.add(normalizeMoney(whitelist.getBalance()));
+                totalLocked = totalLocked.add(normalizeMoney(whitelist.getLockedAmount()));
+                balanceUserCount++;
+            }
+        }
+        summary.setTotalBalance(totalBalance);
+        summary.setTotalLocked(totalLocked);
+        summary.setBalanceUserCount(balanceUserCount);
+        return summary;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public BigDecimal recycleAllocatedBalance(Integer companyId, String operatorName, String companyName,
+                                              BigDecimal companyBalanceBefore) {
+        if (companyId == null) {
+            throw new ServiceException("公司ID不能为空");
+        }
+        String operator = truncate(operatorName, MAX_USER_TEXT_LENGTH);
+        // 按固定顺序抢占三把公司级锁(与三类额度分配操作互斥,顺序固定避免死锁)
+        RLock vinLock = redissonClient.getLock(RECYCLE_LOCK_PREFIX_VIN + companyId);
+        RLock cardLock = redissonClient.getLock(RECYCLE_LOCK_PREFIX_CARD + companyId);
+        RLock whitelistLock = redissonClient.getLock(RECYCLE_LOCK_PREFIX_WHITELIST + companyId);
+        boolean vinLocked = tryRecycleLock(vinLock);
+        boolean cardLocked = false;
+        boolean whitelistLocked = false;
+        try {
+            cardLocked = tryRecycleLock(cardLock);
+            try {
+                whitelistLocked = tryRecycleLock(whitelistLock);
+                return doRecycleAllocatedBalance(companyId, operator, companyName, companyBalanceBefore);
+            } finally {
+                unlockRecycle(whitelistLock, whitelistLocked);
+            }
+        } finally {
+            unlockRecycle(cardLock, cardLocked);
+            unlockRecycle(vinLock, vinLocked);
+        }
+    }
+
+    /**
+     * 锁内回收实现:清零三类主体余额并写流水(与余额清零同事务,异常整体回滚),返回实际回收总额。
+     */
+    private BigDecimal doRecycleAllocatedBalance(Integer companyId, String operator, String companyName,
+                                                 BigDecimal companyBalanceBefore) {
+        Long operatorId = currentUserId();
+        BigDecimal total = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+        String remark = "账户模式切换为共享模式,回收分配余额至公司账户";
+        // VIN码
+        List<TVinManagement> vins = vinManagementMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinManagement>()
+                        .eq(TVinManagement::getCompanyId, companyId)
+                        .gt(TVinManagement::getBalance, BigDecimal.ZERO));
+        if (vins != null) {
+            for (TVinManagement vin : vins) {
+                BigDecimal before = normalizeMoney(vin.getBalance());
+                if (vinManagementMapper.clearBalance(vin.getId(), operatorId) == 1) {
+                    total = total.add(before);
+                    recordModeSwitchRecord(companyId, TCompanyAllocateRecord.USER_TYPE_VIN, vin.getVin(),
+                            vin.getPlateNumber(), operator, operatorId, before, before.negate(), remark);
+                }
+            }
+        }
+        // 刷卡卡
+        List<TChargingCard> cards = chargingCardMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TChargingCard>()
+                        .eq(TChargingCard::getCompanyId, companyId)
+                        .gt(TChargingCard::getBalance, BigDecimal.ZERO));
+        if (cards != null) {
+            for (TChargingCard card : cards) {
+                BigDecimal before = normalizeMoney(card.getBalance());
+                if (chargingCardMapper.clearBalance(card.getId(), operatorId) == 1) {
+                    total = total.add(before);
+                    recordModeSwitchRecord(companyId, TCompanyAllocateRecord.USER_TYPE_CARD, card.getCardNo(),
+                            null, operator, operatorId, before, before.negate(), remark);
+                }
+            }
+        }
+        // 白名单手机号
+        List<TAppUserWhitelist> whitelists = appUserWhitelistMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TAppUserWhitelist>()
+                        .eq(TAppUserWhitelist::getCompanyId, companyId)
+                        .gt(TAppUserWhitelist::getBalance, BigDecimal.ZERO));
+        if (whitelists != null) {
+            for (TAppUserWhitelist whitelist : whitelists) {
+                BigDecimal before = normalizeMoney(whitelist.getBalance());
+                if (appUserWhitelistMapper.clearBalance(whitelist.getId(), operator) == 1) {
+                    total = total.add(before);
+                    recordModeSwitchRecord(companyId, TCompanyAllocateRecord.USER_TYPE_WHITELIST,
+                            whitelist.getPhone(), whitelist.getName(), operator, operatorId, before,
+                            before.negate(), remark);
+                }
+            }
+        }
+        // 企业账户正数流水:公司余额加回留痕(与用户负流水同事务)
+        if (total.signum() > 0) {
+            String companyInfo = StringUtils.isNotBlank(companyName) ? companyName : "企业账户";
+            recordModeSwitchRecord(companyId, TCompanyAllocateRecord.USER_TYPE_COMPANY, companyInfo, null,
+                    operator, operatorId, normalizeMoney(companyBalanceBefore), total, remark);
+        }
+        return total;
+    }
+
+    /** 写入一条模式切换回退流水(change_source=4),写入失败抛出异常由事务回滚保证与余额清零一致 */
+    private void recordModeSwitchRecord(Integer companyId, int userType, String userInfo, String userName,
+                                        String operator, Long operatorId, BigDecimal beforeBalance,
+                                        BigDecimal amount, String remark) {
+        BigDecimal before = normalizeMoney(beforeBalance);
+        BigDecimal change = normalizeMoney(amount);
+        TCompanyAllocateRecord record = new TCompanyAllocateRecord();
+        record.setCompanyId(companyId);
+        record.setUserType(userType);
+        record.setUserInfo(truncate(userInfo, MAX_USER_TEXT_LENGTH));
+        record.setUserName(truncate(userName, MAX_USER_TEXT_LENGTH));
+        record.setChangeSource(TCompanyAllocateRecord.SOURCE_MODE_SWITCH);
+        record.setChangeType(TCompanyAllocateRecord.CHANGE_TYPE_ALLOCATE);
+        record.setBeforeBalance(before);
+        record.setAmount(change);
+        record.setAfterBalance(before.add(change));
+        record.setOperatorId(operatorId);
+        record.setOperatorName(operator);
+        record.setOperateTime(LocalDateTime.now());
+        record.setRemark(truncate(remark, MAX_REMARK_LENGTH));
+        record.setCreateTime(record.getOperateTime());
+        if (!save(record)) {
+            throw new ServiceException("模式切换回退流水写入失败");
+        }
+    }
+
+    /** 抢占回收并发锁,抢占失败抛出业务异常 */
+    private boolean tryRecycleLock(RLock lock) {
+        try {
+            if (!lock.tryLock(LOCK_WAIT_SECONDS, LOCK_LEASE_SECONDS, TimeUnit.SECONDS)) {
+                throw new ServiceException("公司额度变更操作正在处理中,请稍后重试");
+            }
+            return true;
+        } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+            throw new ServiceException("公司额度变更操作正在处理中,请稍后重试");
+        }
+    }
+
+    /** 释放回收并发锁(仅释放当前线程持有的锁,异常仅记录日志) */
+    private void unlockRecycle(RLock lock, boolean locked) {
+        if (!locked) {
+            return;
+        }
+        try {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        } catch (Exception ex) {
+            log.warn("模式切换回收锁释放失败,lockName={}", lock.getName(), ex);
+        }
+    }
+
     /**
      * 消费流水写入公共实现
      *

--
Gitblit v1.7.1