package com.ruoyi.account.service.impl; 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; import com.ruoyi.account.api.model.TCompanyAllocateRecord; import com.ruoyi.account.api.vo.AppUserWhitelistAllocateVO; import com.ruoyi.account.api.vo.AppUserWhitelistMatchVO; 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; import com.ruoyi.account.service.TCompanyAllocateRecordService; import com.ruoyi.chargingPile.api.domain.SiteMenu; import com.ruoyi.chargingPile.api.feignClient.PartnerClient; import com.ruoyi.chargingPile.api.feignClient.SiteClient; import com.ruoyi.chargingPile.api.model.Partner; import com.ruoyi.chargingPile.api.model.Site; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.common.core.utils.StringUtils; import com.ruoyi.common.core.utils.poi.ExcelUtil; 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; import org.apache.poi.ss.usermodel.Row; 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; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.multipart.MultipartFile; 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; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; 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; /** * 白名单用户服务实现 */ @Service public class TAppUserWhitelistServiceImpl extends ServiceImpl 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 int SCOPE_ALL_SITE = 1; 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; /** 共享模式(公司账户模式 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; /** 初始分配额度上限(对应 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 int MAX_ALLOCATE_WHITELIST_COUNT = 200; /** 白名单额度分配并发锁前缀:按公司串行,防止同公司并发分配重复占用可分配余额 */ private static final String ALLOCATE_LOCK_PREFIX = "APP_USER_WHITELIST_QUOTA:COMPANY:"; /** 获取锁最长等待时间(秒) */ private static final long LOCK_WAIT_SECONDS = 5L; /** 锁自动释放时间(秒),兜住异常未解锁的场景 */ private static final long LOCK_LEASE_SECONDS = 30L; @Resource private TAppUserWhitelistSiteService whitelistSiteService; @Resource private TAppUserService appUserService; @Resource private SiteClient siteClient; @Resource private PartnerClient partnerClient; @Resource private TokenService tokenService; @Resource private SysUserClient sysUserClient; @Resource private PlatformTransactionManager transactionManager; @Autowired private GroupCompanyClient groupCompanyClient; @Autowired private RedissonClient redissonClient; @Resource private TCompanyAllocateRecordService companyAllocateRecordService; @Resource private PartnerAppUserAccessService partnerAppUserAccessService; @Override public Page pageList(AppUserWhitelistQueryDTO queryDTO) { AppUserWhitelistQueryDTO safeQuery = queryDTO == null ? new AppUserWhitelistQueryDTO() : queryDTO; Page page = buildListQuery(safeQuery) .orderByDesc(BasePojo::getCreateTime) .page(Page.of(safeQuery.getPageCurr(), safeQuery.getPageSize())); fillDisplay(page.getRecords()); return page; } /** * 列表与导出共用的查询条件(含合作商数据范围):条件为空则不参与过滤; * 合作商账号只能看到自身合作商录入或本人创建的白名单数据 */ private LambdaQueryChainWrapper buildListQuery(AppUserWhitelistQueryDTO queryDTO) { AppUserWhitelistQueryDTO safeQuery = queryDTO == null ? new AppUserWhitelistQueryDTO() : queryDTO; // 公司账号强制按所属公司过滤,覆盖前端传入的公司筛选 safeQuery.setCompanyId(partnerAppUserAccessService.resolveCompanyFilter(safeQuery.getCompanyId())); Integer currentPartnerId = getCurrentPartnerId(); String username = getUsername(); LambdaQueryChainWrapper query = this.lambdaQuery() .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)) { query.and(wrapper -> wrapper.eq(TAppUserWhitelist::getPartnerId, currentPartnerId) .or() .eq(TAppUserWhitelist::getCreateBy, username)); } else { query.eq(TAppUserWhitelist::getPartnerId, currentPartnerId); } } return query; } @Override public List selectExportList(AppUserWhitelistQueryDTO queryDTO) { List records = buildListQuery(queryDTO) .orderByDesc(BasePojo::getCreateTime) .list(); if (records == null || records.isEmpty()) { return Collections.emptyList(); } Map fleetNameMap = loadFleetNameMap(records.stream() .map(TAppUserWhitelist::getFleetId) .filter(Objects::nonNull) .collect(Collectors.toList())); Map companyMap = loadCompanyMap(records.stream() .map(TAppUserWhitelist::getCompanyId) .filter(Objects::nonNull) .collect(Collectors.toList())); List 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 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 public TAppUserWhitelist detail(Long id) { TAppUserWhitelist whitelist = this.getById(id); if (whitelist == null) { throw new ServiceException("白名单用户不存在"); } assertPartnerVisible(whitelist); // 公司账号越权校验:只能查看本公司数据 partnerAppUserAccessService.assertCompanyVisible(whitelist.getCompanyId()); fillDisplay(Collections.singletonList(whitelist)); return whitelist; } @Override @Transactional(rollbackFor = Exception.class) public void saveWhitelist(AppUserWhitelistSaveDTO saveDTO) { validateSaveDTO(saveDTO); if (saveDTO.getId() == null) { createWhitelist(saveDTO); } else { updateWhitelist(saveDTO); } } @Override @Transactional(rollbackFor = Exception.class) public void deleteByIds(String ids) { if (StringUtils.isBlank(ids)) { return; } for (String id : ids.split(",")) { if (StringUtils.isBlank(id)) { continue; } Long whitelistId = Long.valueOf(id); TAppUserWhitelist whitelist = this.getById(whitelistId); if (whitelist == null) { continue; } assertPartnerVisible(whitelist); // 公司账号越权校验:只能删除本公司数据 partnerAppUserAccessService.assertCompanyVisible(whitelist.getCompanyId()); this.removeById(whitelistId); whitelistSiteService.remove(Wrappers.lambdaQuery(TAppUserWhitelistSite.class) .eq(TAppUserWhitelistSite::getWhitelistId, whitelistId)); } } /** * 批量解绑车队:仅清空车队归属(fleet_id 置空),公司、余额、状态等不受影响; * 未绑定车队的选中记录忽略不计 */ @Override @Transactional(rollbackFor = Exception.class) public int unbindFleet(List ids) { if (ids == null || ids.isEmpty()) { throw new ServiceException("请选择需要解绑车队的白名单用户"); } List distinct = ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (distinct.isEmpty()) { throw new ServiceException("请选择需要解绑车队的白名单用户"); } List records = listByIds(distinct); if (records.size() != distinct.size()) { throw new ServiceException("部分白名单用户不存在或已删除"); } for (TAppUserWhitelist record : records) { assertPartnerVisible(record); // 公司账号越权校验:只能解绑本公司数据 partnerAppUserAccessService.assertCompanyVisible(record.getCompanyId()); } List boundIds = records.stream() .filter(record -> record.getFleetId() != null) .map(TAppUserWhitelist::getId) .collect(Collectors.toList()); if (boundIds.isEmpty()) { return 0; } // 显式 set null 清空车队归属(MyBatis-Plus 字段策略会忽略实体 null,需走 UpdateWrapper) boolean updated = lambdaUpdate() .in(TAppUserWhitelist::getId, boundIds) .set(TAppUserWhitelist::getFleetId, null) .set(TAppUserWhitelist::getUpdateBy, getUsername()) .setSql("update_time = now()") .update(); if (!updated) { throw new ServiceException("白名单用户车队解绑失败,请重试"); } return boundIds.size(); } @Override public AppUserWhitelistImportResultVO importWhitelist(MultipartFile file) throws Exception { if (file == null || file.isEmpty()) { throw new ServiceException("导入文件不能为空"); } int titleNum = getImportTitleNum(file); ExcelUtil util = new ExcelUtil<>(AppUserWhitelistImportDTO.class); List rows = util.importExcel(file.getInputStream(), titleNum); if (rows == null || rows.isEmpty()) { throw new ServiceException("导入数据不能为空"); } Map validRows = new LinkedHashMap<>(); Map validRowNumbers = new HashMap<>(); List errors = new ArrayList<>(); Set importPhones = new HashSet<>(); for (int i = 0; i < rows.size(); i++) { AppUserWhitelistImportDTO row = rows.get(i); int rowNum = titleNum + i + 2; List rowErrors = validateImportRow(row); if (!rowErrors.isEmpty()) { errors.add(new AppUserWhitelistImportErrorVO(rowNum, normalize(row.getPhone()), String.join(";", rowErrors))); continue; } AppUserWhitelistSaveDTO dto = buildSaveDTO(row); if (!importPhones.add(dto.getPhone())) { errors.add(new AppUserWhitelistImportErrorVO(rowNum, dto.getPhone(), "导入文件中手机号重复")); continue; } if (getByPhone(dto.getPhone()) != null) { errors.add(new AppUserWhitelistImportErrorVO(rowNum, dto.getPhone(), "该手机号已存在白名单配置")); continue; } validRows.put(dto.getPhone(), dto); validRowNumbers.put(dto.getPhone(), rowNum); } TransactionTemplate rowTransaction = new TransactionTemplate(transactionManager); rowTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); int successCount = 0; for (Map.Entry entry : validRows.entrySet()) { try { rowTransaction.execute(status -> { createWhitelist(entry.getValue()); return null; }); successCount++; } catch (Exception exception) { errors.add(new AppUserWhitelistImportErrorVO(validRowNumbers.get(entry.getKey()), entry.getKey(), getImportSaveErrorMessage(exception))); } } AppUserWhitelistImportResultVO result = new AppUserWhitelistImportResultVO(); result.setTotalCount(rows.size()); result.setErrorCount(errors.size()); result.setSuccessCount(successCount); result.setErrorMessage(errors); return result; } private String getImportSaveErrorMessage(Exception exception) { Throwable current = exception; while (current != null) { if (current instanceof ServiceException && StringUtils.isNotBlank(current.getMessage())) { return current.getMessage(); } current = current.getCause(); } return "数据保存失败"; } /** * 判断指定用户在指定站点是否命中启用状态的白名单规则 * * @param appUserId APP用户ID * @param siteId 站点ID * @return 白名单命中结果 */ @Override public AppUserWhitelistMatchVO matchWhitelist(Long appUserId, Integer siteId) { AppUserWhitelistMatchVO emptyResult = new AppUserWhitelistMatchVO(); if (appUserId == null || siteId == null) { return emptyResult; } TAppUser appUser = appUserService.getById(appUserId); if (appUser == null || StringUtils.isBlank(appUser.getPhone())) { return emptyResult; } TAppUserWhitelist whitelist = this.lambdaQuery() .eq(TAppUserWhitelist::getPhone, appUser.getPhone()) .eq(TAppUserWhitelist::getStatus, STATUS_ENABLED) .eq(TAppUserWhitelist::getDelFlag, false) .last("LIMIT 1") .one(); if (whitelist == null || !isWhitelistScopeMatched(whitelist, siteId)) { return emptyResult; } AppUserWhitelistMatchVO result = new AppUserWhitelistMatchVO(); result.setMatched(true); result.setWhitelistId(whitelist.getId()); // 白名单权益已下线(充电完全按企业优惠结算),命中结果不再返回权益类型与名称 result.setScopeType(whitelist.getScopeType()); result.setCompanyId(whitelist.getCompanyId()); result.setFleetId(whitelist.getFleetId()); //企业账户分配模式:返回白名单账户可用余额,供建单侧计算下发金额与锁定额度 result.setBalance(whitelist.getBalance()); return result; } @Override public List listByCompany(Integer companyId) { if (companyId == null) { return Collections.emptyList(); } return lambdaQuery() .eq(TAppUserWhitelist::getCompanyId, companyId) .eq(TAppUserWhitelist::getDelFlag, false) .orderByDesc(BasePojo::getCreateTime) .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("请选择公司"); } TGroupCompany company = loadCompany(companyId); if (company == null) { throw new ServiceException("公司不存在或已删除"); } return buildAllocateOverview(company); } @Override @Transactional(rollbackFor = Exception.class) public AppUserWhitelistAllocateVO allocateQuota(AppUserWhitelistAllocateDTO allocateDTO) { // 公司账号只能给本公司分配额度,覆盖前端传入的公司 allocateDTO.setCompanyId(partnerAppUserAccessService.resolveCompanyFilter(allocateDTO.getCompanyId())); Map amounts = validateAllocateRequest(allocateDTO); Integer companyId = allocateDTO.getCompanyId(); // 公司必须存在、启用且为分配模式(共享模式不支持额度分配) TGroupCompany company = loadAllocationCompany(companyId); BigDecimal total = amounts.values().stream() .reduce(BigDecimal.ZERO, BigDecimal::add) .setScale(2, RoundingMode.HALF_UP); // 同公司分配串行化,避免并发下重复占用可分配余额 RLock lock = redissonClient.getLock(ALLOCATE_LOCK_PREFIX + companyId); boolean locked = tryQuotaLock(lock, "该公司额度分配正在处理中,请稍后重试"); try { List whitelists = listByIds(amounts.keySet()); if (whitelists == null || whitelists.size() != amounts.size()) { throw new ServiceException("部分白名单用户不存在或已删除"); } for (TAppUserWhitelist whitelist : whitelists) { // 合作商账号仅能分配自身可见的白名单记录 assertPartnerVisible(whitelist); BigDecimal amount = amounts.get(whitelist.getId()); if (!companyId.equals(whitelist.getCompanyId())) { throw new ServiceException("白名单用户【" + whitelist.getPhone() + "】不属于所选公司,无法分配额度"); } if (!Integer.valueOf(STATUS_ENABLED).equals(whitelist.getStatus())) { throw new ServiceException("白名单用户【" + whitelist.getPhone() + "】已停用,禁止分配额度"); } BigDecimal balance = normalizeQuota(whitelist.getBalance()); if (balance.add(amount).compareTo(MAX_INIT_QUOTA) > 0) { throw new ServiceException("白名单用户【" + whitelist.getPhone() + "】分配后余额超过上限" + MAX_INIT_QUOTA.toPlainString()); } } // 先扣公司可分配余额(条件更新,余额不足直接失败),失败无需补偿 R deductResponse = groupCompanyClient.deductBalance(companyId, total); if (deductResponse == null || !R.isSuccess(deductResponse)) { throw new ServiceException(buildDeductFailureMessage(deductResponse)); } try { String operator = getUsername(); for (TAppUserWhitelist whitelist : whitelists) { BigDecimal amount = amounts.get(whitelist.getId()); BigDecimal before = normalizeQuota(whitelist.getBalance()); if (baseMapper.addBalance(whitelist.getId(), amount, operator) != 1) { throw new ServiceException("白名单用户【" + whitelist.getPhone() + "】额度分配失败,请重试"); } recordWhitelistAllocate(whitelist.getPhone(), whitelist.getName(), companyId, before, amount, TCompanyAllocateRecord.SOURCE_ALLOCATE, buildWhitelistAllocateRemark(company)); } } catch (RuntimeException ex) { // 白名单余额变更随事务回滚,公司余额为远程扣减需显式退还 refundQuietly(companyId, total); throw ex; } AppUserWhitelistAllocateVO vo = buildAllocateOverview(company); vo.setAvailableBalance(normalizeMoney(deductResponse.getData())); return vo; } finally { unlockQuota(lock, locked); } } /** * 根据白名单范围类型判断当前站点是否在可用范围内 * * @param whitelist 白名单配置 * @param siteId 站点ID * @return true=命中范围 */ private boolean isWhitelistScopeMatched(TAppUserWhitelist whitelist, Integer siteId) { if (Objects.equals(whitelist.getScopeType(), SCOPE_ALL_SITE)) { return whitelist.getPartnerId() == null || isSiteBelongsToPartner(siteId, whitelist.getPartnerId()); } if (Objects.equals(whitelist.getScopeType(), SCOPE_SELECTED_SITE)) { return whitelistSiteService.lambdaQuery() .eq(TAppUserWhitelistSite::getWhitelistId, whitelist.getId()) .eq(TAppUserWhitelistSite::getSiteId, siteId) .count() > 0; } if (Objects.equals(whitelist.getScopeType(), SCOPE_PARTNER)) { R> siteResult = siteClient.getSiteByIds(Collections.singletonList(siteId)); List sites = siteResult == null || siteResult.getData() == null ? Collections.emptyList() : siteResult.getData(); return !sites.isEmpty() && Objects.equals(sites.get(0).getPartnerId(), whitelist.getPartnerId()); } return false; } private int getImportTitleNum(MultipartFile file) throws Exception { try (InputStream inputStream = file.getInputStream(); Workbook workbook = WorkbookFactory.create(inputStream)) { Sheet sheet = workbook.getSheetAt(0); if (sheet == null) { throw new ServiceException("导入文件sheet不存在"); } int maxRow = Math.min(sheet.getLastRowNum(), 5); for (int i = 0; i <= maxRow; i++) { Row row = sheet.getRow(i); if (isImportHeaderRow(row)) { return i; } } } throw new ServiceException("导入文件表头不正确,请下载最新模板后填写"); } private boolean isImportHeaderRow(Row row) { if (row == null) { return false; } ExcelUtil util = new ExcelUtil<>(AppUserWhitelistImportDTO.class); return IMPORT_HEADER_PHONE.equals(normalize(String.valueOf(util.getCellValue(row, 0)))); } private void validateSaveDTO(AppUserWhitelistSaveDTO saveDTO) { if (saveDTO == null) { throw new ServiceException("请求参数不能为空"); } saveDTO.setPhone(normalize(saveDTO.getPhone())); if (!isValidPhone(saveDTO.getPhone())) { throw new ServiceException("请输入正确的用户手机号"); } 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); // 白名单权益已下线:白名单用户充电完全按企业优惠结算,忽略入参中的享受权益 saveDTO.setBenefitType(null); // 白名单电站不传默认全部电站,传了仍按原规则校验 Integer scopeType = saveDTO.getScopeType() == null ? SCOPE_ALL_SITE : saveDTO.getScopeType(); if (!isValidScope(scopeType)) { throw new ServiceException("请选择正确的白名单电站范围"); } saveDTO.setScopeType(scopeType); if (Objects.equals(scopeType, SCOPE_SELECTED_SITE) && (saveDTO.getSiteIds() == null || saveDTO.getSiteIds().isEmpty())) { throw new ServiceException("请选择白名单电站"); } if (Objects.equals(scopeType, SCOPE_PARTNER) && saveDTO.getPartnerId() == null) { throw new ServiceException("请选择加盟商"); } validatePartnerDataScope(saveDTO); TAppUserWhitelist exists = getByPhone(saveDTO.getPhone()); if (saveDTO.getId() == null && exists != null) { throw new ServiceException("该手机号已存在白名单配置"); } if (saveDTO.getId() != null && exists != null && !Objects.equals(exists.getId(), saveDTO.getId())) { throw new ServiceException("该手机号已存在白名单配置"); } } private void createWhitelist(AppUserWhitelistSaveDTO saveDTO) { if (getByPhone(saveDTO.getPhone()) != null) { throw new ServiceException("该手机号已存在白名单配置"); } // 同手机号并发新增串行化:手机号重复校验与额度扣减在同一临界区内 RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + "PHONE:" + saveDTO.getPhone()); boolean locked = tryQuotaLock(lock, "该手机号正在处理中,请稍后重试"); List compensations = new ArrayList<>(); try { if (getByPhone(saveDTO.getPhone()) != null) { throw new ServiceException("该手机号已存在白名单配置"); } BigDecimal quota = normalizeQuota(saveDTO.getBalance()); 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.setScopeType(saveDTO.getScopeType()); whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO)); whitelist.setCompanyId(companyId); whitelist.setFleetId(saveDTO.getFleetId()); whitelist.setBalance(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 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); } } recordWhitelistAllocate(whitelist.getPhone(), whitelist.getName(), companyId, BigDecimal.ZERO, quota, TCompanyAllocateRecord.SOURCE_CREATE, "新增白名单初始分配额度"); } catch (RuntimeException ex) { compensate(compensations); throw ex; } finally { unlockQuota(lock, locked); } } private void updateWhitelist(AppUserWhitelistSaveDTO saveDTO) { TAppUserWhitelist existing = this.getById(saveDTO.getId()); if (existing == null) { throw new ServiceException("白名单用户不存在"); } assertPartnerVisible(existing); // 公司账号越权校验:只能编辑本公司数据 partnerAppUserAccessService.assertCompanyVisible(existing.getCompanyId()); // 同白名单并发编辑串行化:额度差额计算、公司余额扣减/退还必须在临界区内,防止重复扣减或重复退还 RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + saveDTO.getId()); boolean locked = tryQuotaLock(lock, "该白名单额度正在处理中,请稍后重试"); List compensations = new ArrayList<>(); // 分配记录延迟到白名单落库成功后写入:失败时公司余额已回补,不应留下流水 List recordTasks = new ArrayList<>(); try { TAppUserWhitelist whitelist = this.getById(saveDTO.getId()); if (whitelist == null) { throw new ServiceException("白名单用户不存在"); } assertPartnerVisible(whitelist); BigDecimal newQuota = normalizeQuota(saveDTO.getBalance()); Integer newCompanyId = resolveRelationCompanyId(saveDTO); if (newQuota.signum() > 0) { requireAllocationCompany(newCompanyId); } Integer oldCompanyId = whitelist.getCompanyId(); BigDecimal oldQuota = normalizeQuota(whitelist.getBalance()); if (Objects.equals(oldCompanyId, newCompanyId)) { // 同一公司按差额调整:调增扣差额,调减退差额 BigDecimal diff = newQuota.subtract(oldQuota); if (diff.signum() > 0 && newCompanyId != null) { deductBalance(newCompanyId, diff); compensations.add(() -> refundQuietly(newCompanyId, diff)); recordTasks.add(() -> recordWhitelistAllocate(saveDTO.getPhone(), saveDTO.getName(), newCompanyId, oldQuota, diff, TCompanyAllocateRecord.SOURCE_EDIT, "编辑白名单调整初始分配额度")); } else if (diff.signum() < 0 && newCompanyId != null) { BigDecimal refund = diff.negate(); refundBalance(newCompanyId, refund); compensations.add(() -> deductQuietly(newCompanyId, refund)); recordTasks.add(() -> recordWhitelistAllocate(saveDTO.getPhone(), saveDTO.getName(), newCompanyId, oldQuota, diff, TCompanyAllocateRecord.SOURCE_EDIT, "编辑白名单调减初始分配额度")); } } else { // 跨公司:旧公司全额退还,新公司全额扣减 if (oldCompanyId != null && oldQuota.signum() > 0) { refundBalance(oldCompanyId, oldQuota); compensations.add(() -> deductQuietly(oldCompanyId, oldQuota)); recordTasks.add(() -> recordWhitelistAllocate(saveDTO.getPhone(), saveDTO.getName(), oldCompanyId, oldQuota, oldQuota.negate(), TCompanyAllocateRecord.SOURCE_EDIT, "编辑白名单调整所属公司(原公司退还)")); } if (newCompanyId != null && newQuota.signum() > 0) { deductBalance(newCompanyId, newQuota); compensations.add(() -> refundQuietly(newCompanyId, newQuota)); recordTasks.add(() -> recordWhitelistAllocate(saveDTO.getPhone(), saveDTO.getName(), newCompanyId, BigDecimal.ZERO, newQuota, TCompanyAllocateRecord.SOURCE_EDIT, "编辑白名单调整所属公司")); } } 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.setScopeType(saveDTO.getScopeType()); whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO)); whitelist.setCompanyId(newCompanyId); whitelist.setFleetId(saveDTO.getFleetId()); whitelist.setBalance(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 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); } } recordTasks.forEach(Runnable::run); } catch (RuntimeException ex) { compensate(compensations); throw ex; } finally { unlockQuota(lock, locked); } } /** * 查询手机号对应的有效白名单配置,逻辑删除记录允许重新新增。 * * @param phone 用户手机号 * @return 有效白名单配置 */ private TAppUserWhitelist getByPhone(String phone) { return this.baseMapper.selectValidByPhone(phone); } /** 写入白名单分配流水(分配为正数、回收为负数;公司为空或金额为0时自动忽略) */ private void recordWhitelistAllocate(String phone, String name, Integer companyId, BigDecimal beforeBalance, BigDecimal amount, int changeSource, String remark) { try { companyAllocateRecordService.recordAllocate(TCompanyAllocateRecord.USER_TYPE_WHITELIST, phone, name, companyId, changeSource, beforeBalance, amount, remark); } catch (RuntimeException ex) { // 流水为审计数据,写入失败不阻断白名单额度变更主流程,仅记录日志便于排查 log.error("白名单{}分配流水写入失败:{}", phone, ex.getMessage(), ex); } } private List validateImportRow(AppUserWhitelistImportDTO row) { List errors = new ArrayList<>(); if (row == null) { errors.add("空行"); return errors; } String phone = normalize(row.getPhone()); if (!isValidPhone(phone)) { errors.add("用户手机号格式错误"); } return errors; } /** 导入数据统一按默认值处理:全部电站 + 启用(权益已下线,不再写入享受权益) */ private AppUserWhitelistSaveDTO buildSaveDTO(AppUserWhitelistImportDTO row) { AppUserWhitelistSaveDTO dto = new AppUserWhitelistSaveDTO(); dto.setPhone(normalize(row.getPhone())); dto.setScopeType(SCOPE_ALL_SITE); dto.setSiteIds(Collections.emptyList()); dto.setStatus(STATUS_ENABLED); return dto; } private void fillDisplay(List records) { if (records == null || records.isEmpty()) { return; } Map siteMap = getSiteMap(records); Map partnerMap = getPartnerMap(records, siteMap); for (TAppUserWhitelist record : records) { record.setUid(record.getId() == null ? null : record.getId().toString()); // 白名单权益已下线,列表不再填充享受权益名称 record.setScopeName(getScopeName(record.getScopeType())); if (record.getPartnerId() != null) { Partner partner = partnerMap.get(record.getPartnerId()); record.setPartnerName(partner == null ? null : partner.getName()); } List sites = whitelistSiteService.lambdaQuery() .eq(TAppUserWhitelistSite::getWhitelistId, record.getId()) .list(); for (TAppUserWhitelistSite siteItem : sites) { Site site = siteMap.get(siteItem.getSiteId()); if (site != null) { siteItem.setSiteCode(site.getCode()); siteItem.setSiteName(site.getName()); siteItem.setSiteType(site.getSiteType()); siteItem.setBusinessCategory(site.getBusinessCategory()); siteItem.setStatus(site.getStatus()); Partner partner = partnerMap.get(site.getPartnerId()); siteItem.setPartnerName(partner == null ? null : partner.getName()); } } record.setSiteList(sites); record.setSiteIds(sites.stream().map(TAppUserWhitelistSite::getSiteId).collect(Collectors.toList())); } fillCompanyInfo(records); fillFleetNames(records); } /** * 列表回填所属公司名称与账户信息(账户模式、可用余额、超额、账户状态)。 * 按公司ID去重后一次批量远程查询,远程失败仅记录日志并留空,不影响列表查询 */ private void fillCompanyInfo(List records) { Map companyMap = loadCompanyMap(records.stream() .map(TAppUserWhitelist::getCompanyId) .filter(Objects::nonNull) .collect(Collectors.toList())); 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()); 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 loadCompanyMap(List companyIds) { List distinctIds = companyIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (distinctIds.isEmpty()) { return new HashMap<>(); } try { R> response = groupCompanyClient.listCompaniesByIds(distinctIds); if (response == null || !R.isSuccess(response) || response.getData() == null) { return new HashMap<>(); } Map 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 records) { Map nameMap = loadFleetNameMap(records.stream() .map(TAppUserWhitelist::getFleetId) .filter(Objects::nonNull) .collect(Collectors.toList())); records.forEach(record -> record.setFleetName(nameMap.get(record.getFleetId()))); } /** 按ID批量加载车队名称(远程失败返回空Map,由调用方按未知处理) */ private Map loadFleetNameMap(List fleetIds) { List distinctIds = fleetIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (distinctIds.isEmpty()) { return new HashMap<>(); } try { R> response = groupCompanyClient.listFleetsByIds(distinctIds); if (response == null || !R.isSuccess(response) || response.getData() == null) { return new HashMap<>(); } Map nameMap = new HashMap<>(); for (TFleet fleet : response.getData()) { if (fleet != null && fleet.getId() != null) { nameMap.put(fleet.getId(), fleet.getFleetName()); } } return nameMap; } catch (Exception ex) { log.warn("回填车队名称失败,fleetIds={}", distinctIds, ex); return new HashMap<>(); } } private Map getSiteMap(List records) { List whitelistIds = records.stream().map(TAppUserWhitelist::getId).collect(Collectors.toList()); List siteLinks = whitelistSiteService.lambdaQuery() .in(TAppUserWhitelistSite::getWhitelistId, whitelistIds) .list(); List siteIds = siteLinks.stream().map(TAppUserWhitelistSite::getSiteId).distinct().collect(Collectors.toList()); if (siteIds.isEmpty()) { return Collections.emptyMap(); } R> siteResult = siteClient.getSiteByIds(siteIds); List sites = siteResult == null || siteResult.getData() == null ? Collections.emptyList() : siteResult.getData(); return sites.stream().collect(Collectors.toMap(Site::getId, site -> site, (a, b) -> a)); } private Map getPartnerMap(List records, Map siteMap) { Map partnerMap = new HashMap<>(); Set partnerIds = new HashSet<>(); for (TAppUserWhitelist record : records) { if (record.getPartnerId() != null) { partnerIds.add(record.getPartnerId()); } } if (siteMap != null) { for (Site site : siteMap.values()) { if (site != null && site.getPartnerId() != null) { partnerIds.add(site.getPartnerId()); } } } for (Integer partnerId : partnerIds) { R partnerResult = partnerClient.getPartnerById(partnerId); if (partnerResult != null && partnerResult.getData() != null) { partnerMap.put(partnerId, partnerResult.getData()); } } return partnerMap; } private void validatePartnerDataScope(AppUserWhitelistSaveDTO saveDTO) { Integer currentPartnerId = getCurrentPartnerId(); if (currentPartnerId == null) { return; } if (Objects.equals(saveDTO.getScopeType(), SCOPE_PARTNER) && !Objects.equals(saveDTO.getPartnerId(), currentPartnerId)) { throw new ServiceException("合作商账号只能选择本合作商"); } if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)) { Set authSiteIds = getCurrentPartnerSiteIds(currentPartnerId); if (authSiteIds.isEmpty()) { throw new ServiceException("当前合作商没有可用电站权限"); } List unauthorizedSiteIds = saveDTO.getSiteIds().stream() .filter(Objects::nonNull) .filter(siteId -> !authSiteIds.contains(siteId)) .collect(Collectors.toList()); if (!unauthorizedSiteIds.isEmpty()) { throw new ServiceException("只能选择当前合作商负责的电站"); } } } private void assertPartnerVisible(TAppUserWhitelist whitelist) { Integer currentPartnerId = getCurrentPartnerId(); if (currentPartnerId == null || whitelist == null) { return; } if (Objects.equals(whitelist.getPartnerId(), currentPartnerId)) { return; } if (StringUtils.isNotBlank(whitelist.getCreateBy()) && Objects.equals(whitelist.getCreateBy(), getUsername())) { return; } throw new ServiceException("无权操作该白名单数据"); } private Integer resolveWhitelistPartnerId(AppUserWhitelistSaveDTO saveDTO) { Integer currentPartnerId = getCurrentPartnerId(); if (currentPartnerId != null) { return currentPartnerId; } return Objects.equals(saveDTO.getScopeType(), SCOPE_PARTNER) ? saveDTO.getPartnerId() : null; } /** 校验并回填所属公司与车队:车队必须归属所选公司;未选公司时按车队自动带出公司 */ private Integer resolveRelationCompanyId(AppUserWhitelistSaveDTO saveDTO) { // 公司账号新增/编辑白名单用户时强制归属本公司,覆盖前端传入的公司 saveDTO.setCompanyId(partnerAppUserAccessService.resolveCompanyFilter(saveDTO.getCompanyId())); Integer companyId = saveDTO.getCompanyId(); if (companyId != null && loadCompany(companyId) == null) { throw new ServiceException("所属公司不存在或已删除"); } 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("所属车队不属于所选公司"); } } 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 response = groupCompanyClient.getCompanyById(companyId); if (response == null || !R.isSuccess(response)) { throw new ServiceException("公司信息查询失败:" + (response == null ? "服务无响应" : response.getMsg())); } return response.getData(); } /** * 额度分配:加载公司并校验为启用且分配模式。 * 与初始分配额度校验分开,便于按场景返回更准确的提示,且不影响既有链路。 */ private TGroupCompany loadAllocationCompany(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("公司已停用,无法分配额度"); } return company; } /** 组装额度分配概览(可分配余额、总账户余额) */ private AppUserWhitelistAllocateVO buildAllocateOverview(TGroupCompany company) { AppUserWhitelistAllocateVO vo = new AppUserWhitelistAllocateVO(); vo.setCompanyId(company.getId()); vo.setCompanyName(company.getName()); vo.setAccountMode(company.getAccountMode()); vo.setAvailableBalance(normalizeMoney(company.getAvailableBalance())); vo.setTotalRechargeAmount(normalizeMoney(company.getTotalRechargeAmount())); return vo; } /** 金额归一化:null 视为 0,统一保留2位小数 */ private BigDecimal normalizeMoney(BigDecimal amount) { return (amount == null ? BigDecimal.ZERO : amount).setScale(2, RoundingMode.HALF_UP); } /** 校验额度分配请求并返回 白名单记录ID → 本次分配金额(保持提交顺序) */ private Map validateAllocateRequest(AppUserWhitelistAllocateDTO dto) { if (dto == null) { throw new ServiceException("分配参数不能为空"); } if (dto.getCompanyId() == null) { throw new ServiceException("请选择公司"); } if (dto.getItems() == null || dto.getItems().isEmpty()) { throw new ServiceException("请至少为一条白名单用户填写本次分配金额"); } if (dto.getItems().size() > MAX_ALLOCATE_WHITELIST_COUNT) { throw new ServiceException("单次最多分配" + MAX_ALLOCATE_WHITELIST_COUNT + "条白名单用户"); } Map amounts = new LinkedHashMap<>(); for (AppUserWhitelistAllocateItemDTO item : dto.getItems()) { if (item == null || item.getWhitelistId() == null) { throw new ServiceException("分配明细中存在无效白名单用户"); } if (amounts.containsKey(item.getWhitelistId())) { throw new ServiceException("分配明细中存在重复白名单用户"); } validateAmount(item.getAmount(), "分配金额"); if (item.getAmount().compareTo(MAX_INIT_QUOTA) > 0) { throw new ServiceException("单条白名单用户分配金额不能超过" + MAX_INIT_QUOTA.toPlainString()); } amounts.put(item.getWhitelistId(), item.getAmount().setScale(2, RoundingMode.HALF_UP)); } return amounts; } /** 校验金额:必填、大于0、最多2位小数 */ private void validateAmount(BigDecimal amount, String fieldName) { if (amount == null) { throw new ServiceException(fieldName + "不能为空"); } if (amount.signum() <= 0) { throw new ServiceException(fieldName + "必须大于0"); } if (amount.stripTrailingZeros().scale() > 2) { throw new ServiceException(fieldName + "最多保留2位小数"); } } /** 公司余额扣减失败提示:余额不足场景给出可读文案 */ private String buildDeductFailureMessage(R response) { if (response == null) { return "公司可用余额扣减失败:服务无响应"; } String msg = response.getMsg(); if (msg != null && msg.contains("余额不足")) { return "公司可分配余额不足,无法完成本次分配"; } return "公司可用余额扣减失败:" + msg; } /** 组装额度分配流水备注(含来源公司) */ private String buildWhitelistAllocateRemark(TGroupCompany company) { String companyName = company == null || company.getName() == null ? "" : company.getName(); String remark = "额度分配(公司:" + companyName + ")"; return remark.length() > 255 ? remark.substring(0, 255) : remark; } private TFleet loadFleet(Integer fleetId) { R 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 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 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 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 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 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 getCurrentPartnerSiteIds(Integer partnerId) { R> siteResult = partnerClient.authSite(partnerId, SiteMenu.SITE_LIST.getValue()); return siteResult == null || siteResult.getData() == null ? Collections.emptySet() : siteResult.getData(); } private boolean isSiteBelongsToPartner(Integer siteId, Integer partnerId) { if (siteId == null || partnerId == null) { return false; } R> siteResult = siteClient.getSiteByIds(Collections.singletonList(siteId)); List sites = siteResult == null || siteResult.getData() == null ? Collections.emptyList() : siteResult.getData(); return !sites.isEmpty() && Objects.equals(sites.get(0).getPartnerId(), partnerId); } private Integer getCurrentPartnerId() { SysUser sysUser = getCurrentSysUser(); if (sysUser == null || !Objects.equals(sysUser.getRoleType(), 2)) { return null; } return sysUser.getObjectId(); } private SysUser getCurrentSysUser() { LoginUser loginUser = null; try { loginUser = tokenService.getLoginUser(); } catch (Exception e) { // ignore and try security context fallback } if (loginUser == null) { try { loginUser = SecurityUtils.getLoginUser(); } catch (Exception e) { // ignore and fall through to null } } if (loginUser == null) { return null; } if (loginUser.getSysUser() != null && loginUser.getSysUser().getRoleType() != null) { return loginUser.getSysUser(); } if (loginUser.getUserid() == null) { return loginUser.getSysUser(); } R userResult = sysUserClient.getSysUser(loginUser.getUserid()); if (userResult != null && userResult.getData() != null) { return userResult.getData(); } return loginUser.getSysUser(); } private boolean isValidPhone(String phone) { return StringUtils.isNotBlank(phone) && PHONE_PATTERN.matcher(phone).matches(); } private boolean isValidScope(Integer scopeType) { return Objects.equals(scopeType, SCOPE_ALL_SITE) || Objects.equals(scopeType, SCOPE_SELECTED_SITE) || Objects.equals(scopeType, SCOPE_PARTNER); } private String normalize(String value) { return value == null ? null : value.trim(); } private String getScopeName(Integer scopeType) { if (Objects.equals(scopeType, SCOPE_ALL_SITE)) { return "全部电站"; } if (Objects.equals(scopeType, SCOPE_SELECTED_SITE)) { return "部分电站"; } if (Objects.equals(scopeType, SCOPE_PARTNER)) { return "指定合作商"; } return ""; } private String getUsername() { try { String username = SecurityUtils.getUsername(); if (StringUtils.isNotBlank(username)) { return username; } } catch (Exception e) { // ignore and try token context fallback } try { LoginUser loginUser = tokenService.getLoginUser(); if (loginUser != null) { if (StringUtils.isNotBlank(loginUser.getUsername())) { return loginUser.getUsername(); } if (loginUser.getSysUser() != null && StringUtils.isNotBlank(loginUser.getSysUser().getUserName())) { return loginUser.getSysUser().getUserName(); } } } catch (Exception e) { // ignore and fall through to null } return null; } }