puzhibing
4 天以前 3650ce7f5d42d1bef58abea9693f20d1f209d3b0
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);
        }
    }
    /**
     * 消费流水写入公共实现
     *