puzhibing
4 天以前 a0e03b646f2c444b0f09687d24e9995112b99017
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
@@ -3,12 +3,15 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.dto.AppUserWhitelistAllocateDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistAllocateItemDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistExcelDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistImportDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistQueryDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistSaveDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistStatusDTO;
import com.ruoyi.account.api.model.TAppUser;
import com.ruoyi.account.api.model.TAppUserWhitelist;
import com.ruoyi.account.api.model.TAppUserWhitelistSite;
@@ -18,6 +21,7 @@
import com.ruoyi.account.api.vo.AppUserWhitelistImportErrorVO;
import com.ruoyi.account.api.vo.AppUserWhitelistImportResultVO;
import com.ruoyi.account.mapper.TAppUserWhitelistMapper;
import com.ruoyi.account.service.PartnerAppUserAccessService;
import com.ruoyi.account.service.TAppUserService;
import com.ruoyi.account.service.TAppUserWhitelistService;
import com.ruoyi.account.service.TAppUserWhitelistSiteService;
@@ -83,9 +87,6 @@
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1\\d{10}$");
    private static final String IMPORT_HEADER_PHONE = "用户手机号";
    private static final int BENEFIT_ALL_FREE = 1;
    private static final int BENEFIT_ELECTRIC_FREE = 2;
    private static final int BENEFIT_SERVICE_FREE = 3;
    private static final int SCOPE_ALL_SITE = 1;
    private static final int SCOPE_SELECTED_SITE = 2;
    private static final int SCOPE_PARTNER = 3;
@@ -93,6 +94,15 @@
    private static final int STATUS_DISABLED = 2;
    /** 姓名最大长度(对应 varchar(64)) */
    private static final int MAX_NAME_LENGTH = 64;
    /** 共享模式(公司账户模式 1=共享模式,可用余额取公司可用余额) */
    private static final int ACCOUNT_MODE_SHARED = 1;
    /** 账户状态:正常 */
    private static final int ACCOUNT_STATUS_NORMAL = 1;
    /** 账户状态:欠款 */
    private static final int ACCOUNT_STATUS_OVERDUE = 2;
    /** 金额小数位(元,对应 decimal(12,2)) */
    private static final int AMOUNT_SCALE = 2;
    /** 公司账户模式:2=分配模式(可设置初始分配额度并扣减公司可用余额) */
    private static final int ACCOUNT_MODE_ALLOCATION = 2;
@@ -131,19 +141,35 @@
    @Resource
    private TCompanyAllocateRecordService companyAllocateRecordService;
    @Resource
    private PartnerAppUserAccessService partnerAppUserAccessService;
    @Override
    public Page<TAppUserWhitelist> pageList(AppUserWhitelistQueryDTO queryDTO) {
        if (queryDTO == null) {
            queryDTO = new AppUserWhitelistQueryDTO();
        }
        AppUserWhitelistQueryDTO safeQuery = queryDTO == null ? new AppUserWhitelistQueryDTO() : queryDTO;
        Page<TAppUserWhitelist> page = buildListQuery(safeQuery)
                .orderByDesc(BasePojo::getCreateTime)
                .page(Page.of(safeQuery.getPageCurr(), safeQuery.getPageSize()));
        fillDisplay(page.getRecords());
        return page;
    }
    /**
     * 列表与导出共用的查询条件(含合作商数据范围):条件为空则不参与过滤;
     * 合作商账号只能看到自身合作商录入或本人创建的白名单数据
     */
    private LambdaQueryChainWrapper<TAppUserWhitelist> buildListQuery(AppUserWhitelistQueryDTO queryDTO) {
        AppUserWhitelistQueryDTO safeQuery = queryDTO == null ? new AppUserWhitelistQueryDTO() : queryDTO;
        // 公司账号强制按所属公司过滤,覆盖前端传入的公司筛选
        safeQuery.setCompanyId(partnerAppUserAccessService.resolveCompanyFilter(safeQuery.getCompanyId()));
        Integer currentPartnerId = getCurrentPartnerId();
        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())
                .like(StringUtils.isNotBlank(safeQuery.getPhone()), TAppUserWhitelist::getPhone, safeQuery.getPhone())
                .like(StringUtils.isNotBlank(safeQuery.getName()), TAppUserWhitelist::getName, safeQuery.getName())
                .eq(safeQuery.getCompanyId() != null, TAppUserWhitelist::getCompanyId, safeQuery.getCompanyId())
                .eq(safeQuery.getFleetId() != null, TAppUserWhitelist::getFleetId, safeQuery.getFleetId())
                .eq(safeQuery.getStatus() != null, TAppUserWhitelist::getStatus, safeQuery.getStatus())
                .eq(TAppUserWhitelist::getDelFlag, false);
        if (currentPartnerId != null) {
            if (StringUtils.isNotBlank(username)) {
@@ -154,10 +180,79 @@
                query.eq(TAppUserWhitelist::getPartnerId, currentPartnerId);
            }
        }
        Page<TAppUserWhitelist> page = query.orderByDesc(BasePojo::getCreateTime)
                .page(Page.of(queryDTO.getPageCurr(), queryDTO.getPageSize()));
        fillDisplay(page.getRecords());
        return page;
        return query;
    }
    @Override
    public List<AppUserWhitelistExcelDTO> selectExportList(AppUserWhitelistQueryDTO queryDTO) {
        List<TAppUserWhitelist> records = buildListQuery(queryDTO)
                .orderByDesc(BasePojo::getCreateTime)
                .list();
        if (records == null || records.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Integer, String> fleetNameMap = loadFleetNameMap(records.stream()
                .map(TAppUserWhitelist::getFleetId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        Map<Integer, TGroupCompany> companyMap = loadCompanyMap(records.stream()
                .map(TAppUserWhitelist::getCompanyId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        List<AppUserWhitelistExcelDTO> items = new ArrayList<>(records.size());
        for (TAppUserWhitelist record : records) {
            TGroupCompany company = companyMap.get(record.getCompanyId());
            Integer accountMode = company == null ? null : company.getAccountMode();
            BigDecimal balance = resolveAvailableBalance(accountMode, record.getBalance(),
                    company == null ? null : company.getAvailableBalance());
            AppUserWhitelistExcelDTO item = new AppUserWhitelistExcelDTO();
            item.setPhone(record.getPhone());
            item.setName(record.getName());
            item.setCompanyName(company == null ? null : company.getName());
            item.setFleetName(fleetNameMap.get(record.getFleetId()));
            item.setAccountMode(accountMode);
            item.setAvailableBalance(balance);
            item.setOverrun(resolveOverrun(balance));
            item.setAccountStatus(resolveAccountStatus(balance));
            item.setStatus(record.getStatus());
            item.setCompanyId(record.getCompanyId());
            item.setFleetId(record.getFleetId());
            item.setBalance(record.getBalance());
            items.add(item);
        }
        return items;
    }
    @Override
    public boolean updateStatus(AppUserWhitelistStatusDTO statusDTO) {
        if (statusDTO == null || statusDTO.getId() == null
                || (!Objects.equals(statusDTO.getStatus(), STATUS_ENABLED)
                && !Objects.equals(statusDTO.getStatus(), STATUS_DISABLED))) {
            throw new ServiceException("白名单用户状态参数错误");
        }
        TAppUserWhitelist existing = this.getById(statusDTO.getId());
        if (existing == null) {
            throw new ServiceException("白名单用户不存在或已删除");
        }
        assertPartnerVisible(existing);
        // 公司账号越权校验:只能操作本公司数据
        partnerAppUserAccessService.assertCompanyVisible(existing.getCompanyId());
        if (Objects.equals(existing.getStatus(), statusDTO.getStatus())) {
            // 状态未变化视为幂等成功,避免无意义的更新人与更新时间变更
            return true;
        }
        LambdaUpdateChainWrapper<TAppUserWhitelist> updateWrapper = lambdaUpdate()
                .eq(TAppUserWhitelist::getId, statusDTO.getId())
                .set(TAppUserWhitelist::getStatus, statusDTO.getStatus())
                .setSql("update_time = now()");
        String username = getUsername();
        if (StringUtils.isNotBlank(username)) {
            updateWrapper.set(TAppUserWhitelist::getUpdateBy, username);
        }
        if (!updateWrapper.update()) {
            throw new ServiceException("白名单用户状态更新失败,请重试");
        }
        return true;
    }
    @Override
@@ -167,6 +262,8 @@
            throw new ServiceException("白名单用户不存在");
        }
        assertPartnerVisible(whitelist);
        // 公司账号越权校验:只能查看本公司数据
        partnerAppUserAccessService.assertCompanyVisible(whitelist.getCompanyId());
        fillDisplay(Collections.singletonList(whitelist));
        return whitelist;
    }
@@ -198,6 +295,8 @@
                continue;
            }
            assertPartnerVisible(whitelist);
            // 公司账号越权校验:只能删除本公司数据
            partnerAppUserAccessService.assertCompanyVisible(whitelist.getCompanyId());
            this.removeById(whitelistId);
            whitelistSiteService.remove(Wrappers.lambdaQuery(TAppUserWhitelistSite.class)
                    .eq(TAppUserWhitelistSite::getWhitelistId, whitelistId));
@@ -224,6 +323,8 @@
        }
        for (TAppUserWhitelist record : records) {
            assertPartnerVisible(record);
            // 公司账号越权校验:只能解绑本公司数据
            partnerAppUserAccessService.assertCompanyVisible(record.getCompanyId());
        }
        List<Long> boundIds = records.stream()
                .filter(record -> record.getFleetId() != null)
@@ -346,9 +447,12 @@
        AppUserWhitelistMatchVO result = new AppUserWhitelistMatchVO();
        result.setMatched(true);
        result.setWhitelistId(whitelist.getId());
        result.setBenefitType(whitelist.getBenefitType());
        // 白名单权益已下线(充电完全按企业优惠结算),命中结果不再返回权益类型与名称
        result.setScopeType(whitelist.getScopeType());
        result.setBenefitName(getBenefitName(whitelist.getBenefitType()));
        result.setCompanyId(whitelist.getCompanyId());
        result.setFleetId(whitelist.getFleetId());
        //企业账户分配模式:返回白名单账户可用余额,供建单侧计算下发金额与锁定额度
        result.setBalance(whitelist.getBalance());
        return result;
    }
@@ -364,8 +468,77 @@
                .list();
    }
    /**
     * 锁定白名单账户余额(企业账户流程-分配模式,实扣冻结):
     * 余额立即扣减并计入 locked_amount,余额不足时条件更新命中 0 行直接失败;
     * 与 VIN/刷卡账户锁定口径一致(T VinManagementServiceImpl/TChargingCardServiceImpl)。
     */
    @Override
    public BigDecimal lockBalance(Long whitelistId, BigDecimal amount) {
        if (whitelistId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("锁定金额参数错误");
        }
        boolean updated = lambdaUpdate()
                .eq(TAppUserWhitelist::getId, whitelistId)
                .eq(TAppUserWhitelist::getDelFlag, false)
                .ge(TAppUserWhitelist::getBalance, amount)
                .setSql("balance = IFNULL(balance, 0) - " + amount.toPlainString()
                        + ", locked_amount = IFNULL(locked_amount, 0) + " + amount.toPlainString())
                .update();
        if (!updated) {
            throw new ServiceException("白名单账户余额不足");
        }
        TAppUserWhitelist latest = getById(whitelistId);
        return latest == null || latest.getBalance() == null ? BigDecimal.ZERO : latest.getBalance();
    }
    /**
     * 结算白名单账户余额:先释放锁定额,再按实际消费扣减,允许扣成负余额
     * (与 VIN 结算口径一致:balance = balance + locked - actual,locked_amount 归零下限保护)。
     */
    @Override
    public BigDecimal settleBalance(Long whitelistId, BigDecimal lockedAmount, BigDecimal actualAmount) {
        BigDecimal lock = lockedAmount == null ? BigDecimal.ZERO : lockedAmount;
        BigDecimal actual = actualAmount == null ? BigDecimal.ZERO : actualAmount;
        if (whitelistId == null || lock.compareTo(BigDecimal.ZERO) < 0 || actual.compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("结算金额参数错误");
        }
        boolean updated = lambdaUpdate()
                .eq(TAppUserWhitelist::getId, whitelistId)
                .setSql("balance = IFNULL(balance, 0) + " + lock.toPlainString() + " - " + actual.toPlainString()
                        + ", locked_amount = GREATEST(IFNULL(locked_amount, 0) - " + lock.toPlainString() + ", 0)")
                .update();
        if (!updated) {
            throw new ServiceException("白名单用户不存在或已删除");
        }
        TAppUserWhitelist latest = getById(whitelistId);
        return latest == null || latest.getBalance() == null ? BigDecimal.ZERO : latest.getBalance();
    }
    /**
     * 释放白名单账户锁定金额:余额回补、锁定额度扣减(未产生消费/订单回滚时使用)。
     */
    @Override
    public BigDecimal unlockBalance(Long whitelistId, BigDecimal amount) {
        if (whitelistId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("释放金额参数错误");
        }
        boolean updated = lambdaUpdate()
                .eq(TAppUserWhitelist::getId, whitelistId)
                .setSql("balance = IFNULL(balance, 0) + " + amount.toPlainString()
                        + ", locked_amount = GREATEST(IFNULL(locked_amount, 0) - " + amount.toPlainString() + ", 0)")
                .update();
        if (!updated) {
            throw new ServiceException("白名单用户不存在或已删除");
        }
        TAppUserWhitelist latest = getById(whitelistId);
        return latest == null || latest.getBalance() == null ? BigDecimal.ZERO : latest.getBalance();
    }
    @Override
    public AppUserWhitelistAllocateVO getAllocateOverview(Integer companyId) {
        // 公司账号只能查看本公司额度概览,覆盖前端传入的公司
        companyId = partnerAppUserAccessService.resolveCompanyFilter(companyId);
        if (companyId == null) {
            throw new ServiceException("请选择公司");
        }
@@ -379,6 +552,8 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AppUserWhitelistAllocateVO allocateQuota(AppUserWhitelistAllocateDTO allocateDTO) {
        // 公司账号只能给本公司分配额度,覆盖前端传入的公司
        allocateDTO.setCompanyId(partnerAppUserAccessService.resolveCompanyFilter(allocateDTO.getCompanyId()));
        Map<Long, BigDecimal> amounts = validateAllocateRequest(allocateDTO);
        Integer companyId = allocateDTO.getCompanyId();
        // 公司必须存在、启用且为分配模式(共享模式不支持额度分配)
@@ -510,12 +685,8 @@
            throw new ServiceException("请选择正确的状态");
        }
        saveDTO.setStatus(status);
        // 享受权益不传默认全部免费,传了仍按原规则校验
        Integer benefitType = saveDTO.getBenefitType() == null ? BENEFIT_ALL_FREE : saveDTO.getBenefitType();
        if (!isValidBenefit(benefitType)) {
            throw new ServiceException("请选择正确的享受权益");
        }
        saveDTO.setBenefitType(benefitType);
        // 白名单权益已下线:白名单用户充电完全按企业优惠结算,忽略入参中的享受权益
        saveDTO.setBenefitType(null);
        // 白名单电站不传默认全部电站,传了仍按原规则校验
        Integer scopeType = saveDTO.getScopeType() == null ? SCOPE_ALL_SITE : saveDTO.getScopeType();
        if (!isValidScope(scopeType)) {
@@ -528,6 +699,13 @@
        }
        if (Objects.equals(scopeType, SCOPE_PARTNER) && saveDTO.getPartnerId() == null) {
            throw new ServiceException("请选择加盟商");
        }
        // 所属公司与所属车队必填:白名单用户归属企业,充电完全按企业优惠结算
        if (saveDTO.getCompanyId() == null) {
            throw new ServiceException("请选择所属公司");
        }
        if (saveDTO.getFleetId() == null) {
            throw new ServiceException("请选择所属车队");
        }
        validatePartnerDataScope(saveDTO);
        TAppUserWhitelist exists = getByPhone(saveDTO.getPhone());
@@ -567,7 +745,6 @@
            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);
@@ -614,6 +791,8 @@
            throw new ServiceException("白名单用户不存在");
        }
        assertPartnerVisible(existing);
        // 公司账号越权校验:只能编辑本公司数据
        partnerAppUserAccessService.assertCompanyVisible(existing.getCompanyId());
        // 同白名单并发编辑串行化:额度差额计算、公司余额扣减/退还必须在临界区内,防止重复扣减或重复退还
        RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + saveDTO.getId());
        boolean locked = tryQuotaLock(lock, "该白名单额度正在处理中,请稍后重试");
@@ -671,7 +850,6 @@
            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);
@@ -744,11 +922,10 @@
        return errors;
    }
    /** 导入数据统一按默认值处理:全部免费 + 全部电站 + 启用 */
    /** 导入数据统一按默认值处理:全部电站 + 启用(权益已下线,不再写入享受权益) */
    private AppUserWhitelistSaveDTO buildSaveDTO(AppUserWhitelistImportDTO row) {
        AppUserWhitelistSaveDTO dto = new AppUserWhitelistSaveDTO();
        dto.setPhone(normalize(row.getPhone()));
        dto.setBenefitType(BENEFIT_ALL_FREE);
        dto.setScopeType(SCOPE_ALL_SITE);
        dto.setSiteIds(Collections.emptyList());
        dto.setStatus(STATUS_ENABLED);
@@ -763,7 +940,7 @@
        Map<Integer, Partner> partnerMap = getPartnerMap(records, siteMap);
        for (TAppUserWhitelist record : records) {
            record.setUid(record.getId() == null ? null : record.getId().toString());
            record.setBenefitName(getBenefitName(record.getBenefitType()));
            // 白名单权益已下线,列表不再填充享受权益名称
            record.setScopeName(getScopeName(record.getScopeType()));
            if (record.getPartnerId() != null) {
                Partner partner = partnerMap.get(record.getPartnerId());
@@ -787,48 +964,102 @@
            record.setSiteList(sites);
            record.setSiteIds(sites.stream().map(TAppUserWhitelistSite::getSiteId).collect(Collectors.toList()));
        }
        fillCompanyNames(records);
        fillCompanyInfo(records);
        fillFleetNames(records);
    }
    /** 列表回填所属公司名称(远程失败仅记录日志,不影响列表查询) */
    private void fillCompanyNames(List<TAppUserWhitelist> records) {
        Map<Integer, String> nameMap = new HashMap<>();
    /**
     * 列表回填所属公司名称与账户信息(账户模式、可用余额、超额、账户状态)。
     * 按公司ID去重后一次批量远程查询,远程失败仅记录日志并留空,不影响列表查询
     */
    private void fillCompanyInfo(List<TAppUserWhitelist> records) {
        Map<Integer, TGroupCompany> companyMap = loadCompanyMap(records.stream()
                .map(TAppUserWhitelist::getCompanyId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        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);
            TGroupCompany company = companyMap.get(record.getCompanyId());
            Integer accountMode = company == null ? null : company.getAccountMode();
            BigDecimal balance = resolveAvailableBalance(accountMode, record.getBalance(),
                    company == null ? null : company.getAvailableBalance());
            record.setCompanyName(company == null ? null : company.getName());
            record.setAccountMode(accountMode);
            record.setAvailableBalance(balance);
            record.setOverrun(resolveOverrun(balance));
            record.setAccountStatus(resolveAccountStatus(balance));
        }
    }
    /** 按ID批量加载公司(远程失败或服务降级时返回空Map,由调用方按未知处理) */
    private Map<Integer, TGroupCompany> loadCompanyMap(List<Integer> companyIds) {
        List<Integer> distinctIds = companyIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (distinctIds.isEmpty()) {
            return new HashMap<>();
        }
        try {
            R<List<TGroupCompany>> response = groupCompanyClient.listCompaniesByIds(distinctIds);
            if (response == null || !R.isSuccess(response) || response.getData() == null) {
                return new HashMap<>();
            }
            Map<Integer, TGroupCompany> companyMap = new HashMap<>();
            for (TGroupCompany company : response.getData()) {
                if (company != null && company.getId() != null) {
                    companyMap.put(company.getId(), company);
                }
            }
            return companyMap;
        } catch (Exception ex) {
            log.warn("回填公司账户信息失败,companyIds={}", distinctIds, ex);
            return new HashMap<>();
        }
    }
    /** 计算可用余额:分配模式=白名单用户自身余额,共享模式=公司可用余额,账户模式未知时留空 */
    private BigDecimal resolveAvailableBalance(Integer accountMode, BigDecimal whitelistBalance, BigDecimal companyBalance) {
        if (Integer.valueOf(ACCOUNT_MODE_ALLOCATION).equals(accountMode)) {
            return whitelistBalance;
        }
        if (Integer.valueOf(ACCOUNT_MODE_SHARED).equals(accountMode)) {
            return companyBalance;
        }
        return null;
    }
    /** 超额:可用余额为负时取其绝对值,其余情况为空 */
    private BigDecimal resolveOverrun(BigDecimal balance) {
        if (balance == null || balance.signum() >= 0) {
            return null;
        }
        return balance.abs().setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
    }
    /** 账户状态:可用余额为负=欠款,非负=正常,可用余额未知时为空 */
    private Integer resolveAccountStatus(BigDecimal balance) {
        if (balance == null) {
            return null;
        }
        return balance.signum() < 0 ? ACCOUNT_STATUS_OVERDUE : ACCOUNT_STATUS_NORMAL;
    }
    /** 列表回填所属车队名称(批量查询,远程失败仅记录日志,不影响列表查询) */
    private void fillFleetNames(List<TAppUserWhitelist> records) {
        List<Integer> fleetIds = records.stream()
        Map<Integer, String> nameMap = loadFleetNameMap(records.stream()
                .map(TAppUserWhitelist::getFleetId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (fleetIds.isEmpty()) {
            return;
                .collect(Collectors.toList()));
        records.forEach(record -> record.setFleetName(nameMap.get(record.getFleetId())));
    }
    /** 按ID批量加载车队名称(远程失败返回空Map,由调用方按未知处理) */
    private Map<Integer, String> loadFleetNameMap(List<Integer> fleetIds) {
        List<Integer> distinctIds = fleetIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (distinctIds.isEmpty()) {
            return new HashMap<>();
        }
        try {
            R<List<TFleet>> response = groupCompanyClient.listFleetsByIds(fleetIds);
            R<List<TFleet>> response = groupCompanyClient.listFleetsByIds(distinctIds);
            if (response == null || !R.isSuccess(response) || response.getData() == null) {
                return;
                return new HashMap<>();
            }
            Map<Integer, String> nameMap = new HashMap<>();
            for (TFleet fleet : response.getData()) {
@@ -836,9 +1067,10 @@
                    nameMap.put(fleet.getId(), fleet.getFleetName());
                }
            }
            records.forEach(record -> record.setFleetName(nameMap.get(record.getFleetId())));
            return nameMap;
        } catch (Exception ex) {
            log.warn("回填车队名称失败,fleetIds={}", fleetIds, ex);
            log.warn("回填车队名称失败,fleetIds={}", distinctIds, ex);
            return new HashMap<>();
        }
    }
@@ -928,6 +1160,8 @@
    /** 校验并回填所属公司与车队:车队必须归属所选公司;未选公司时按车队自动带出公司 */
    private Integer resolveRelationCompanyId(AppUserWhitelistSaveDTO saveDTO) {
        // 公司账号新增/编辑白名单用户时强制归属本公司,覆盖前端传入的公司
        saveDTO.setCompanyId(partnerAppUserAccessService.resolveCompanyFilter(saveDTO.getCompanyId()));
        Integer companyId = saveDTO.getCompanyId();
        if (companyId != null && loadCompany(companyId) == null) {
            throw new ServiceException("所属公司不存在或已删除");
@@ -1233,12 +1467,6 @@
        return StringUtils.isNotBlank(phone) && PHONE_PATTERN.matcher(phone).matches();
    }
    private boolean isValidBenefit(Integer benefitType) {
        return Objects.equals(benefitType, BENEFIT_ALL_FREE)
                || Objects.equals(benefitType, BENEFIT_ELECTRIC_FREE)
                || Objects.equals(benefitType, BENEFIT_SERVICE_FREE);
    }
    private boolean isValidScope(Integer scopeType) {
        return Objects.equals(scopeType, SCOPE_ALL_SITE)
                || Objects.equals(scopeType, SCOPE_SELECTED_SITE)
@@ -1247,19 +1475,6 @@
    private String normalize(String value) {
        return value == null ? null : value.trim();
    }
    private String getBenefitName(Integer benefitType) {
        if (Objects.equals(benefitType, BENEFIT_ALL_FREE)) {
            return "全部免费";
        }
        if (Objects.equals(benefitType, BENEFIT_ELECTRIC_FREE)) {
            return "免电费";
        }
        if (Objects.equals(benefitType, BENEFIT_SERVICE_FREE)) {
            return "免服务费";
        }
        return "";
    }
    private String getScopeName(Integer scopeType) {