From 83188a6c8ea2f123519378da736382a9136a4331 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期一, 28 九月 2026 14:47:09 +0800
Subject: [PATCH] 新增加功能和修改已有功能

---
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java |  561 ++++++++++++++++++++++++++++++++++++++++---------------
 1 files changed, 408 insertions(+), 153 deletions(-)

diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
index db281a9..5d32587 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
@@ -29,6 +29,9 @@
 import com.ruoyi.common.core.web.domain.BasePojo;
 import com.ruoyi.common.security.service.TokenService;
 import com.ruoyi.common.security.utils.SecurityUtils;
+import com.ruoyi.other.api.domain.TFleet;
+import com.ruoyi.other.api.domain.TGroupCompany;
+import com.ruoyi.other.api.feignClient.GroupCompanyClient;
 import com.ruoyi.system.api.domain.SysUser;
 import com.ruoyi.system.api.feignClient.SysUserClient;
 import com.ruoyi.system.api.model.LoginUser;
@@ -36,6 +39,11 @@
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.PlatformTransactionManager;
 import org.springframework.transaction.TransactionDefinition;
@@ -45,6 +53,8 @@
 
 import javax.annotation.Resource;
 import java.io.InputStream;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -54,6 +64,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -63,10 +74,10 @@
 @Service
 public class TAppUserWhitelistServiceImpl extends ServiceImpl<TAppUserWhitelistMapper, TAppUserWhitelist> implements TAppUserWhitelistService {
 
+    private static final Logger log = LoggerFactory.getLogger(TAppUserWhitelistServiceImpl.class);
+
     private static final Pattern PHONE_PATTERN = Pattern.compile("^1\\d{10}$");
     private static final String IMPORT_HEADER_PHONE = "用户手机号";
-    private static final String IMPORT_HEADER_BENEFIT_TYPE = "享受权益";
-    private static final String IMPORT_HEADER_WHITELIST_SITE = "白名单电站";
     private static final int BENEFIT_ALL_FREE = 1;
     private static final int BENEFIT_ELECTRIC_FREE = 2;
     private static final int BENEFIT_SERVICE_FREE = 3;
@@ -74,6 +85,20 @@
     private static final int SCOPE_SELECTED_SITE = 2;
     private static final int SCOPE_PARTNER = 3;
     private static final int STATUS_ENABLED = 1;
+    private static final int STATUS_DISABLED = 2;
+    /** 姓名最大长度(对应 varchar(64)) */
+    private static final int MAX_NAME_LENGTH = 64;
+
+    /** 公司账户模式:2=分配模式(可设置初始分配额度并扣减公司可用余额) */
+    private static final int ACCOUNT_MODE_ALLOCATION = 2;
+    /** 初始分配额度上限(对应 decimal(12,2)) */
+    private static final BigDecimal MAX_INIT_QUOTA = new BigDecimal("99999999.99");
+    /** 初始分配额度并发锁前缀:新增按手机号、编辑按ID串行,防止并发下重复扣减或退还公司余额 */
+    private static final String QUOTA_LOCK_PREFIX = "APP_USER_WHITELIST_QUOTA:";
+    /** 获取锁最长等待时间(秒) */
+    private static final long LOCK_WAIT_SECONDS = 5L;
+    /** 锁自动释放时间(秒),兜住异常未解锁的场景 */
+    private static final long LOCK_LEASE_SECONDS = 30L;
 
     @Resource
     private TAppUserWhitelistSiteService whitelistSiteService;
@@ -89,6 +114,10 @@
     private SysUserClient sysUserClient;
     @Resource
     private PlatformTransactionManager transactionManager;
+    @Autowired
+    private GroupCompanyClient groupCompanyClient;
+    @Autowired
+    private RedissonClient redissonClient;
 
     @Override
     public Page<TAppUserWhitelist> pageList(AppUserWhitelistQueryDTO queryDTO) {
@@ -99,6 +128,10 @@
         String username = getUsername();
         LambdaQueryChainWrapper<TAppUserWhitelist> query = this.lambdaQuery()
                 .like(StringUtils.isNotBlank(queryDTO.getPhone()), TAppUserWhitelist::getPhone, queryDTO.getPhone())
+                .like(StringUtils.isNotBlank(queryDTO.getName()), TAppUserWhitelist::getName, queryDTO.getName())
+                .eq(queryDTO.getCompanyId() != null, TAppUserWhitelist::getCompanyId, queryDTO.getCompanyId())
+                .eq(queryDTO.getFleetId() != null, TAppUserWhitelist::getFleetId, queryDTO.getFleetId())
+                .eq(queryDTO.getStatus() != null, TAppUserWhitelist::getStatus, queryDTO.getStatus())
                 .eq(TAppUserWhitelist::getDelFlag, false);
         if (currentPartnerId != null) {
             if (StringUtils.isNotBlank(username)) {
@@ -171,7 +204,6 @@
             throw new ServiceException("导入数据不能为空");
         }
 
-        Map<String, Site> siteCodeMap = getSiteCodeMap();
         Map<String, AppUserWhitelistSaveDTO> validRows = new LinkedHashMap<>();
         Map<String, Integer> validRowNumbers = new HashMap<>();
         List<AppUserWhitelistImportErrorVO> errors = new ArrayList<>();
@@ -179,12 +211,12 @@
         for (int i = 0; i < rows.size(); i++) {
             AppUserWhitelistImportDTO row = rows.get(i);
             int rowNum = titleNum + i + 2;
-            List<String> rowErrors = validateImportRow(row, siteCodeMap);
+            List<String> rowErrors = validateImportRow(row);
             if (!rowErrors.isEmpty()) {
                 errors.add(new AppUserWhitelistImportErrorVO(rowNum, normalize(row.getPhone()), String.join(";", rowErrors)));
                 continue;
             }
-            AppUserWhitelistSaveDTO dto = buildSaveDTO(row, siteCodeMap);
+            AppUserWhitelistSaveDTO dto = buildSaveDTO(row);
             if (!importPhones.add(dto.getPhone())) {
                 errors.add(new AppUserWhitelistImportErrorVO(rowNum, dto.getPhone(), "导入文件中手机号重复"));
                 continue;
@@ -314,9 +346,7 @@
             return false;
         }
         ExcelUtil<AppUserWhitelistImportDTO> util = new ExcelUtil<>(AppUserWhitelistImportDTO.class);
-        return IMPORT_HEADER_PHONE.equals(normalize(String.valueOf(util.getCellValue(row, 0))))
-                && IMPORT_HEADER_BENEFIT_TYPE.equals(normalize(String.valueOf(util.getCellValue(row, 1))))
-                && IMPORT_HEADER_WHITELIST_SITE.equals(normalize(String.valueOf(util.getCellValue(row, 2))));
+        return IMPORT_HEADER_PHONE.equals(normalize(String.valueOf(util.getCellValue(row, 0))));
     }
 
     private void validateSaveDTO(AppUserWhitelistSaveDTO saveDTO) {
@@ -327,17 +357,36 @@
         if (!isValidPhone(saveDTO.getPhone())) {
             throw new ServiceException("请输入正确的用户手机号");
         }
-        if (!isValidBenefit(saveDTO.getBenefitType())) {
+        saveDTO.setName(normalize(saveDTO.getName()));
+        if (saveDTO.getName() != null && saveDTO.getName().isEmpty()) {
+            saveDTO.setName(null);
+        }
+        if (StringUtils.isNotBlank(saveDTO.getName()) && saveDTO.getName().length() > MAX_NAME_LENGTH) {
+            throw new ServiceException("姓名长度不能超过" + MAX_NAME_LENGTH + "个字符");
+        }
+        // 状态不传默认启用,仅允许1=启用、2=停用
+        Integer status = saveDTO.getStatus() == null ? STATUS_ENABLED : saveDTO.getStatus();
+        if (!Objects.equals(status, STATUS_ENABLED) && !Objects.equals(status, STATUS_DISABLED)) {
+            throw new ServiceException("请选择正确的状态");
+        }
+        saveDTO.setStatus(status);
+        // 享受权益不传默认全部免费,传了仍按原规则校验
+        Integer benefitType = saveDTO.getBenefitType() == null ? BENEFIT_ALL_FREE : saveDTO.getBenefitType();
+        if (!isValidBenefit(benefitType)) {
             throw new ServiceException("请选择正确的享受权益");
         }
-        if (!isValidScope(saveDTO.getScopeType())) {
+        saveDTO.setBenefitType(benefitType);
+        // 白名单电站不传默认全部电站,传了仍按原规则校验
+        Integer scopeType = saveDTO.getScopeType() == null ? SCOPE_ALL_SITE : saveDTO.getScopeType();
+        if (!isValidScope(scopeType)) {
             throw new ServiceException("请选择正确的白名单电站范围");
         }
-        if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)
+        saveDTO.setScopeType(scopeType);
+        if (Objects.equals(scopeType, SCOPE_SELECTED_SITE)
                 && (saveDTO.getSiteIds() == null || saveDTO.getSiteIds().isEmpty())) {
             throw new ServiceException("请选择白名单电站");
         }
-        if (Objects.equals(saveDTO.getScopeType(), SCOPE_PARTNER) && saveDTO.getPartnerId() == null) {
+        if (Objects.equals(scopeType, SCOPE_PARTNER) && saveDTO.getPartnerId() == null) {
             throw new ServiceException("请选择加盟商");
         }
         validatePartnerDataScope(saveDTO);
@@ -354,81 +403,155 @@
         if (getByPhone(saveDTO.getPhone()) != null) {
             throw new ServiceException("该手机号已存在白名单配置");
         }
-        TAppUserWhitelist whitelist = new TAppUserWhitelist();
-        TAppUser appUser = appUserService.lambdaQuery()
-                .eq(TAppUser::getPhone, saveDTO.getPhone())
-                .last("LIMIT 1")
-                .one();
-        whitelist.setCreateBy(getUsername());
-        whitelist.setAppUserId(appUser == null ? null : appUser.getId());
-        whitelist.setPhone(saveDTO.getPhone());
-        whitelist.setBenefitType(saveDTO.getBenefitType());
-        whitelist.setScopeType(saveDTO.getScopeType());
-        whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO));
-        whitelist.setStatus(STATUS_ENABLED);
-        whitelist.setUpdateBy(getUsername());
-        whitelist.setRemark(saveDTO.getRemark());
-        whitelist.setDelFlag(false);
-        if (whitelist.getId() == null) {
-            this.save(whitelist);
-        }
-
-        whitelistSiteService.deleteByWhitelistId(whitelist.getId());
-        if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)) {
-            Long whitelistId = whitelist.getId();
-            List<TAppUserWhitelistSite> sites = saveDTO.getSiteIds().stream()
-                    .filter(Objects::nonNull)
-                    .distinct()
-                    .map(siteId -> {
-                        TAppUserWhitelistSite site = new TAppUserWhitelistSite();
-                        site.setWhitelistId(whitelistId);
-                        site.setSiteId(siteId);
-                        site.setDelFlag(false);
-                        return site;
-                    }).collect(Collectors.toList());
-            if (!sites.isEmpty()) {
-                whitelistSiteService.saveBatch(sites);
+        // 同手机号并发新增串行化:手机号重复校验与额度扣减在同一临界区内
+        RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + "PHONE:" + saveDTO.getPhone());
+        boolean locked = tryQuotaLock(lock, "该手机号正在处理中,请稍后重试");
+        List<Runnable> compensations = new ArrayList<>();
+        try {
+            if (getByPhone(saveDTO.getPhone()) != null) {
+                throw new ServiceException("该手机号已存在白名单配置");
             }
+            BigDecimal quota = normalizeQuota(saveDTO.getInitQuota());
+            Integer companyId = resolveRelationCompanyId(saveDTO);
+            if (quota.signum() > 0) {
+                requireAllocationCompany(companyId);
+                deductBalance(companyId, quota);
+                compensations.add(() -> refundQuietly(companyId, quota));
+            }
+            TAppUserWhitelist whitelist = new TAppUserWhitelist();
+            TAppUser appUser = appUserService.lambdaQuery()
+                    .eq(TAppUser::getPhone, saveDTO.getPhone())
+                    .last("LIMIT 1")
+                    .one();
+            whitelist.setCreateBy(getUsername());
+            whitelist.setAppUserId(appUser == null ? null : appUser.getId());
+            whitelist.setPhone(saveDTO.getPhone());
+            whitelist.setName(saveDTO.getName());
+            whitelist.setBenefitType(saveDTO.getBenefitType());
+            whitelist.setScopeType(saveDTO.getScopeType());
+            whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO));
+            whitelist.setCompanyId(companyId);
+            whitelist.setFleetId(saveDTO.getFleetId());
+            whitelist.setInitQuota(quota.signum() > 0 ? quota : null);
+            whitelist.setStatus(saveDTO.getStatus());
+            whitelist.setUpdateBy(getUsername());
+            whitelist.setRemark(saveDTO.getRemark());
+            whitelist.setDelFlag(false);
+            if (whitelist.getId() == null) {
+                this.save(whitelist);
+            }
+
+            whitelistSiteService.deleteByWhitelistId(whitelist.getId());
+            if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)) {
+                Long whitelistId = whitelist.getId();
+                List<TAppUserWhitelistSite> sites = saveDTO.getSiteIds().stream()
+                        .filter(Objects::nonNull)
+                        .distinct()
+                        .map(siteId -> {
+                            TAppUserWhitelistSite site = new TAppUserWhitelistSite();
+                            site.setWhitelistId(whitelistId);
+                            site.setSiteId(siteId);
+                            site.setDelFlag(false);
+                            return site;
+                        }).collect(Collectors.toList());
+                if (!sites.isEmpty()) {
+                    whitelistSiteService.saveBatch(sites);
+                }
+            }
+        } catch (RuntimeException ex) {
+            compensate(compensations);
+            throw ex;
+        } finally {
+            unlockQuota(lock, locked);
         }
     }
 
     private void updateWhitelist(AppUserWhitelistSaveDTO saveDTO) {
-        TAppUserWhitelist whitelist = this.getById(saveDTO.getId());
-        if (whitelist == null) {
+        TAppUserWhitelist existing = this.getById(saveDTO.getId());
+        if (existing == null) {
             throw new ServiceException("白名单用户不存在");
         }
-        assertPartnerVisible(whitelist);
-        TAppUser appUser = appUserService.lambdaQuery()
-                .eq(TAppUser::getPhone, saveDTO.getPhone())
-                .last("LIMIT 1")
-                .one();
-        whitelist.setAppUserId(appUser == null ? null : appUser.getId());
-        whitelist.setPhone(saveDTO.getPhone());
-        whitelist.setBenefitType(saveDTO.getBenefitType());
-        whitelist.setScopeType(saveDTO.getScopeType());
-        whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO));
-        whitelist.setStatus(STATUS_ENABLED);
-        whitelist.setUpdateBy(getUsername());
-        whitelist.setRemark(saveDTO.getRemark());
-        whitelist.setDelFlag(false);
-        this.baseMapper.updateByIdIgnoreLogic(whitelist);
-
-        whitelistSiteService.deleteByWhitelistId(whitelist.getId());
-        if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)) {
-            Long whitelistId = whitelist.getId();
-            List<TAppUserWhitelistSite> sites = saveDTO.getSiteIds().stream()
-                    .filter(Objects::nonNull)
-                    .distinct()
-                    .map(siteId -> {
-                        TAppUserWhitelistSite site = new TAppUserWhitelistSite();
-                        site.setWhitelistId(whitelistId);
-                        site.setSiteId(siteId);
-                        site.setDelFlag(false);
-                        return site;
-                    }).collect(Collectors.toList());
-            if (!sites.isEmpty()) {
-                whitelistSiteService.saveBatch(sites);
+        assertPartnerVisible(existing);
+        // 同白名单并发编辑串行化:额度差额计算、公司余额扣减/退还必须在临界区内,防止重复扣减或重复退还
+        RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + saveDTO.getId());
+        boolean locked = tryQuotaLock(lock, "该白名单额度正在处理中,请稍后重试");
+        List<Runnable> compensations = new ArrayList<>();
+        try {
+            TAppUserWhitelist whitelist = this.getById(saveDTO.getId());
+            if (whitelist == null) {
+                throw new ServiceException("白名单用户不存在");
             }
+            assertPartnerVisible(whitelist);
+            BigDecimal newQuota = normalizeQuota(saveDTO.getInitQuota());
+            Integer newCompanyId = resolveRelationCompanyId(saveDTO);
+            if (newQuota.signum() > 0) {
+                requireAllocationCompany(newCompanyId);
+            }
+            Integer oldCompanyId = whitelist.getCompanyId();
+            BigDecimal oldQuota = normalizeQuota(whitelist.getInitQuota());
+            if (Objects.equals(oldCompanyId, newCompanyId)) {
+                // 同一公司按差额调整:调增扣差额,调减退差额
+                BigDecimal diff = newQuota.subtract(oldQuota);
+                if (diff.signum() > 0 && newCompanyId != null) {
+                    deductBalance(newCompanyId, diff);
+                    compensations.add(() -> refundQuietly(newCompanyId, diff));
+                } else if (diff.signum() < 0 && newCompanyId != null) {
+                    BigDecimal refund = diff.negate();
+                    refundBalance(newCompanyId, refund);
+                    compensations.add(() -> deductQuietly(newCompanyId, refund));
+                }
+            } else {
+                // 跨公司:旧公司全额退还,新公司全额扣减
+                if (oldCompanyId != null && oldQuota.signum() > 0) {
+                    refundBalance(oldCompanyId, oldQuota);
+                    compensations.add(() -> deductQuietly(oldCompanyId, oldQuota));
+                }
+                if (newCompanyId != null && newQuota.signum() > 0) {
+                    deductBalance(newCompanyId, newQuota);
+                    compensations.add(() -> refundQuietly(newCompanyId, newQuota));
+                }
+            }
+            TAppUser appUser = appUserService.lambdaQuery()
+                    .eq(TAppUser::getPhone, saveDTO.getPhone())
+                    .last("LIMIT 1")
+                    .one();
+            whitelist.setAppUserId(appUser == null ? null : appUser.getId());
+            whitelist.setPhone(saveDTO.getPhone());
+            whitelist.setName(saveDTO.getName());
+            whitelist.setBenefitType(saveDTO.getBenefitType());
+            whitelist.setScopeType(saveDTO.getScopeType());
+            whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO));
+            whitelist.setCompanyId(newCompanyId);
+            whitelist.setFleetId(saveDTO.getFleetId());
+            whitelist.setInitQuota(newQuota.signum() > 0 ? newQuota : null);
+            whitelist.setStatus(saveDTO.getStatus());
+            whitelist.setUpdateBy(getUsername());
+            whitelist.setRemark(saveDTO.getRemark());
+            whitelist.setDelFlag(false);
+            this.baseMapper.updateByIdIgnoreLogic(whitelist);
+
+            whitelistSiteService.deleteByWhitelistId(whitelist.getId());
+            if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)) {
+                Long whitelistId = whitelist.getId();
+                List<TAppUserWhitelistSite> sites = saveDTO.getSiteIds().stream()
+                        .filter(Objects::nonNull)
+                        .distinct()
+                        .map(siteId -> {
+                            TAppUserWhitelistSite site = new TAppUserWhitelistSite();
+                            site.setWhitelistId(whitelistId);
+                            site.setSiteId(siteId);
+                            site.setDelFlag(false);
+                            return site;
+                        }).collect(Collectors.toList());
+                if (!sites.isEmpty()) {
+                    whitelistSiteService.saveBatch(sites);
+                }
+            }
+        } catch (RuntimeException ex) {
+            compensate(compensations);
+            throw ex;
+        } finally {
+            unlockQuota(lock, locked);
         }
     }
 
@@ -442,7 +565,7 @@
         return this.baseMapper.selectValidByPhone(phone);
     }
 
-    private List<String> validateImportRow(AppUserWhitelistImportDTO row, Map<String, Site> siteCodeMap) {
+    private List<String> validateImportRow(AppUserWhitelistImportDTO row) {
         List<String> errors = new ArrayList<>();
         if (row == null) {
             errors.add("空行");
@@ -452,57 +575,17 @@
         if (!isValidPhone(phone)) {
             errors.add("用户手机号格式错误");
         }
-        Integer benefitType = parseInteger(row.getBenefitType());
-        if (!isValidBenefit(benefitType)) {
-            errors.add("享受权益只能填写1、2、3");
-        }
-        String whitelistSite = normalize(row.getWhitelistSite());
-        if (StringUtils.isBlank(whitelistSite)) {
-            errors.add("白名单电站不能为空");
-        } else if (!"all".equalsIgnoreCase(whitelistSite)) {
-            for (String code : whitelistSite.split(",")) {
-                String siteCode = normalize(code);
-                if (StringUtils.isBlank(siteCode)) {
-                    errors.add("白名单电站存在空编号");
-                } else if (!siteCodeMap.containsKey(siteCode)) {
-                    errors.add("站点编号不存在:" + siteCode);
-                }
-            }
-        }
         return errors;
     }
 
-    private AppUserWhitelistSaveDTO buildSaveDTO(AppUserWhitelistImportDTO row, Map<String, Site> siteCodeMap) {
+    /** 导入数据统一按默认值处理:全部免费 + 全部电站 */
+    private AppUserWhitelistSaveDTO buildSaveDTO(AppUserWhitelistImportDTO row) {
         AppUserWhitelistSaveDTO dto = new AppUserWhitelistSaveDTO();
         dto.setPhone(normalize(row.getPhone()));
-        dto.setBenefitType(parseInteger(row.getBenefitType()));
-        String whitelistSite = normalize(row.getWhitelistSite());
-        if ("all".equalsIgnoreCase(whitelistSite)) {
-            dto.setScopeType(SCOPE_ALL_SITE);
-            dto.setSiteIds(Collections.emptyList());
-        } else {
-            dto.setScopeType(SCOPE_SELECTED_SITE);
-            List<Integer> siteIds = new ArrayList<>();
-            for (String code : whitelistSite.split(",")) {
-                Site site = siteCodeMap.get(normalize(code));
-                if (site != null) {
-                    siteIds.add(site.getId());
-                }
-            }
-            dto.setSiteIds(siteIds.stream().distinct().collect(Collectors.toList()));
-        }
+        dto.setBenefitType(BENEFIT_ALL_FREE);
+        dto.setScopeType(SCOPE_ALL_SITE);
+        dto.setSiteIds(Collections.emptyList());
         return dto;
-    }
-
-    private Map<String, Site> getSiteCodeMap() {
-        List<Site> sites = getCurrentSelectableSites();
-        Map<String, Site> siteCodeMap = new HashMap<>();
-        for (Site site : sites) {
-            if (site != null && StringUtils.isNotBlank(site.getCode())) {
-                siteCodeMap.put(site.getCode(), site);
-            }
-        }
-        return siteCodeMap;
     }
 
     private void fillDisplay(List<TAppUserWhitelist> records) {
@@ -536,6 +619,59 @@
             }
             record.setSiteList(sites);
             record.setSiteIds(sites.stream().map(TAppUserWhitelistSite::getSiteId).collect(Collectors.toList()));
+        }
+        fillCompanyNames(records);
+        fillFleetNames(records);
+    }
+
+    /** 列表回填所属公司名称(远程失败仅记录日志,不影响列表查询) */
+    private void fillCompanyNames(List<TAppUserWhitelist> records) {
+        Map<Integer, String> nameMap = new HashMap<>();
+        for (TAppUserWhitelist record : records) {
+            Integer companyId = record.getCompanyId();
+            if (companyId == null) {
+                continue;
+            }
+            if (nameMap.containsKey(companyId)) {
+                record.setCompanyName(nameMap.get(companyId));
+                continue;
+            }
+            String companyName = null;
+            try {
+                TGroupCompany company = loadCompany(companyId);
+                companyName = company == null ? null : company.getName();
+            } catch (Exception ex) {
+                log.warn("回填公司名称失败,companyId={}", companyId, ex);
+            }
+            nameMap.put(companyId, companyName);
+            record.setCompanyName(companyName);
+        }
+    }
+
+    /** 列表回填所属车队名称(批量查询,远程失败仅记录日志,不影响列表查询) */
+    private void fillFleetNames(List<TAppUserWhitelist> records) {
+        List<Integer> fleetIds = records.stream()
+                .map(TAppUserWhitelist::getFleetId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (fleetIds.isEmpty()) {
+            return;
+        }
+        try {
+            R<List<TFleet>> response = groupCompanyClient.listFleetsByIds(fleetIds);
+            if (response == null || !R.isSuccess(response) || response.getData() == null) {
+                return;
+            }
+            Map<Integer, String> nameMap = new HashMap<>();
+            for (TFleet fleet : response.getData()) {
+                if (fleet != null && fleet.getId() != null) {
+                    nameMap.put(fleet.getId(), fleet.getFleetName());
+                }
+            }
+            records.forEach(record -> record.setFleetName(nameMap.get(record.getFleetId())));
+        } catch (Exception ex) {
+            log.warn("回填车队名称失败,fleetIds={}", fleetIds, ex);
         }
     }
 
@@ -623,21 +759,155 @@
         return Objects.equals(saveDTO.getScopeType(), SCOPE_PARTNER) ? saveDTO.getPartnerId() : null;
     }
 
-    private List<Site> getCurrentSelectableSites() {
-        Integer currentPartnerId = getCurrentPartnerId();
-        if (currentPartnerId == null) {
-            R<List<Site>> siteResult = siteClient.getSiteAll();
-            return siteResult == null || siteResult.getData() == null ? Collections.emptyList() : siteResult.getData();
+    /** 校验并回填所属公司与车队:车队必须归属所选公司;未选公司时按车队自动带出公司 */
+    private Integer resolveRelationCompanyId(AppUserWhitelistSaveDTO saveDTO) {
+        Integer companyId = saveDTO.getCompanyId();
+        if (companyId != null && loadCompany(companyId) == null) {
+            throw new ServiceException("所属公司不存在或已删除");
         }
-        Set<Integer> siteIds = getCurrentPartnerSiteIds(currentPartnerId);
-        if (siteIds.isEmpty()) {
-            return Collections.emptyList();
+        Integer fleetId = saveDTO.getFleetId();
+        if (fleetId != null) {
+            TFleet fleet = loadFleet(fleetId);
+            if (fleet == null) {
+                throw new ServiceException("所属车队不存在或已删除");
+            }
+            if (companyId == null) {
+                companyId = fleet.getCompanyId();
+            } else if (!companyId.equals(fleet.getCompanyId())) {
+                throw new ServiceException("所属车队不属于所选公司");
+            }
         }
-        R<List<Site>> siteResult = siteClient.getSiteByIds(new ArrayList<>(siteIds));
-        List<Site> sites = siteResult == null || siteResult.getData() == null ? Collections.emptyList() : siteResult.getData();
-        return sites.stream()
-                .filter(site -> site != null && Objects.equals(site.getPartnerId(), currentPartnerId))
-                .collect(Collectors.toList());
+        return companyId;
+    }
+
+    /** 规范化初始分配额度:null 视为 0,不允许负数、超过2位小数或超出上限 */
+    private BigDecimal normalizeQuota(BigDecimal quota) {
+        if (quota == null) {
+            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+        }
+        if (quota.signum() < 0) {
+            throw new ServiceException("初始分配额度不能小于0");
+        }
+        if (quota.stripTrailingZeros().scale() > 2) {
+            throw new ServiceException("初始分配额度最多保留2位小数");
+        }
+        BigDecimal normalized = quota.setScale(2, RoundingMode.HALF_UP);
+        if (normalized.compareTo(MAX_INIT_QUOTA) > 0) {
+            throw new ServiceException("初始分配额度不能超过" + MAX_INIT_QUOTA.toPlainString());
+        }
+        return normalized;
+    }
+
+    /** 校验公司为启用且分配模式(可设置初始分配额度) */
+    private void requireAllocationCompany(Integer companyId) {
+        if (companyId == null) {
+            throw new ServiceException("请选择所属公司后再填写初始分配额度");
+        }
+        TGroupCompany company = loadCompany(companyId);
+        if (company == null) {
+            throw new ServiceException("所属公司不存在或已删除");
+        }
+        if (!Integer.valueOf(ACCOUNT_MODE_ALLOCATION).equals(company.getAccountMode())) {
+            throw new ServiceException("仅分配模式的公司可填写初始分配额度");
+        }
+        if (!Integer.valueOf(1).equals(company.getStatus())) {
+            throw new ServiceException("所属公司已停用,无法填写初始分配额度");
+        }
+    }
+
+    private TGroupCompany loadCompany(Integer companyId) {
+        R<TGroupCompany> response = groupCompanyClient.getCompanyById(companyId);
+        if (response == null || !R.isSuccess(response)) {
+            throw new ServiceException("公司信息查询失败:" + (response == null ? "服务无响应" : response.getMsg()));
+        }
+        return response.getData();
+    }
+
+    private TFleet loadFleet(Integer fleetId) {
+        R<TFleet> response = groupCompanyClient.getFleetById(fleetId);
+        if (response == null || !R.isSuccess(response)) {
+            throw new ServiceException("车队信息查询失败:" + (response == null ? "服务无响应" : response.getMsg()));
+        }
+        return response.getData();
+    }
+
+    /** 扣减公司可用余额(余额不足或服务异常时抛出业务异常) */
+    private void deductBalance(Integer companyId, BigDecimal amount) {
+        R<BigDecimal> response = groupCompanyClient.deductBalance(companyId, amount);
+        if (response == null || !R.isSuccess(response)) {
+            throw new ServiceException("公司可用余额扣减失败:" + (response == null ? "服务无响应" : response.getMsg()));
+        }
+    }
+
+    /** 退还公司可用余额(失败时抛出业务异常) */
+    private void refundBalance(Integer companyId, BigDecimal amount) {
+        R<BigDecimal> response = groupCompanyClient.refundBalance(companyId, amount);
+        if (response == null || !R.isSuccess(response)) {
+            throw new ServiceException("公司可用余额退还失败:" + (response == null ? "服务无响应" : response.getMsg()));
+        }
+    }
+
+    /** 静默退还(补偿场景使用,失败仅记录日志) */
+    private void refundQuietly(Integer companyId, BigDecimal amount) {
+        try {
+            R<BigDecimal> response = groupCompanyClient.refundBalance(companyId, amount);
+            if (response == null || !R.isSuccess(response)) {
+                log.error("公司可用余额退还失败,需人工核对,companyId={}, amount={}, msg={}",
+                        companyId, amount, response == null ? "服务无响应" : response.getMsg());
+            }
+        } catch (Exception ex) {
+            log.error("公司可用余额退还异常,需人工核对,companyId={}, amount={}", companyId, amount, ex);
+        }
+    }
+
+    /** 静默扣减(补偿场景使用,失败仅记录日志) */
+    private void deductQuietly(Integer companyId, BigDecimal amount) {
+        try {
+            R<BigDecimal> response = groupCompanyClient.deductBalance(companyId, amount);
+            if (response == null || !R.isSuccess(response)) {
+                log.error("公司可用余额回滚扣减失败,需人工核对,companyId={}, amount={}, msg={}",
+                        companyId, amount, response == null ? "服务无响应" : response.getMsg());
+            }
+        } catch (Exception ex) {
+            log.error("公司可用余额回滚扣减异常,需人工核对,companyId={}, amount={}", companyId, amount, ex);
+        }
+    }
+
+    /** 保存/更新失败时反向补偿已执行的余额操作(倒序执行) */
+    private void compensate(List<Runnable> compensations) {
+        for (int i = compensations.size() - 1; i >= 0; i--) {
+            try {
+                compensations.get(i).run();
+            } catch (Exception ex) {
+                log.error("公司可用余额补偿失败,需人工核对", ex);
+            }
+        }
+    }
+
+    /** 抢占并发锁,抢占失败抛出业务异常 */
+    private boolean tryQuotaLock(RLock lock, String busyMessage) {
+        try {
+            if (!lock.tryLock(LOCK_WAIT_SECONDS, LOCK_LEASE_SECONDS, TimeUnit.SECONDS)) {
+                throw new ServiceException(busyMessage);
+            }
+            return true;
+        } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+            throw new ServiceException(busyMessage);
+        }
+    }
+
+    private void unlockQuota(RLock lock, boolean locked) {
+        if (!locked) {
+            return;
+        }
+        try {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        } catch (Exception ex) {
+            log.warn("释放白名单额度并发锁失败", ex);
+        }
     }
 
     private Set<Integer> getCurrentPartnerSiteIds(Integer partnerId) {
@@ -706,21 +976,6 @@
         return Objects.equals(scopeType, SCOPE_ALL_SITE)
                 || Objects.equals(scopeType, SCOPE_SELECTED_SITE)
                 || Objects.equals(scopeType, SCOPE_PARTNER);
-    }
-
-    private Integer parseInteger(String value) {
-        try {
-            String normalizedValue = normalize(value);
-            if (StringUtils.isBlank(normalizedValue)) {
-                return null;
-            }
-            if (normalizedValue.matches("^\\d+\\.0+$")) {
-                normalizedValue = normalizedValue.substring(0, normalizedValue.indexOf("."));
-            }
-            return Integer.valueOf(normalizedValue);
-        } catch (Exception e) {
-            return null;
-        }
     }
 
     private String normalize(String value) {

--
Gitblit v1.7.1