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