puzhibing
2026-09-28 83188a6c8ea2f123519378da736382a9136a4331
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) {