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<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 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<TAppUserWhitelist> pageList(AppUserWhitelistQueryDTO queryDTO) {
|
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(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<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
|
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<Long> ids) {
|
if (ids == null || ids.isEmpty()) {
|
throw new ServiceException("请选择需要解绑车队的白名单用户");
|
}
|
List<Long> distinct = ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
if (distinct.isEmpty()) {
|
throw new ServiceException("请选择需要解绑车队的白名单用户");
|
}
|
List<TAppUserWhitelist> records = listByIds(distinct);
|
if (records.size() != distinct.size()) {
|
throw new ServiceException("部分白名单用户不存在或已删除");
|
}
|
for (TAppUserWhitelist record : records) {
|
assertPartnerVisible(record);
|
// 公司账号越权校验:只能解绑本公司数据
|
partnerAppUserAccessService.assertCompanyVisible(record.getCompanyId());
|
}
|
List<Long> 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<AppUserWhitelistImportDTO> util = new ExcelUtil<>(AppUserWhitelistImportDTO.class);
|
List<AppUserWhitelistImportDTO> rows = util.importExcel(file.getInputStream(), titleNum);
|
if (rows == null || rows.isEmpty()) {
|
throw new ServiceException("导入数据不能为空");
|
}
|
|
Map<String, AppUserWhitelistSaveDTO> validRows = new LinkedHashMap<>();
|
Map<String, Integer> validRowNumbers = new HashMap<>();
|
List<AppUserWhitelistImportErrorVO> errors = new ArrayList<>();
|
Set<String> importPhones = new HashSet<>();
|
for (int i = 0; i < rows.size(); i++) {
|
AppUserWhitelistImportDTO row = rows.get(i);
|
int rowNum = titleNum + i + 2;
|
List<String> 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<String, AppUserWhitelistSaveDTO> 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 BigDecimal resolveWhitelistAccountBalance(AppUserWhitelistMatchVO match) {
|
if (match == null || !Boolean.TRUE.equals(match.getMatched()) || match.getCompanyId() == null) {
|
return null;
|
}
|
try {
|
TGroupCompany company = loadCompany(match.getCompanyId());
|
Integer accountMode = company == null ? null : company.getAccountMode();
|
if (accountMode == null) {
|
// 历史数据账户模式为空,按共享模式处理
|
accountMode = ACCOUNT_MODE_SHARED;
|
}
|
return resolveAvailableBalance(accountMode, match.getBalance(),
|
company == null ? null : company.getAvailableBalance());
|
} catch (Exception ex) {
|
log.warn("解析白名单账户可用余额失败,companyId={}", match.getCompanyId(), ex);
|
return null;
|
}
|
}
|
|
@Override
|
public List<TAppUserWhitelist> 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<Long, BigDecimal> 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<TAppUserWhitelist> 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<BigDecimal> 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<List<Site>> siteResult = siteClient.getSiteByIds(Collections.singletonList(siteId));
|
List<Site> 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<AppUserWhitelistImportDTO> 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("请选择加盟商");
|
}
|
// 所属公司与所属车队必填:白名单用户归属企业,充电完全按企业优惠结算
|
if (saveDTO.getCompanyId() == null) {
|
throw new ServiceException("请选择所属公司");
|
}
|
if (saveDTO.getFleetId() == 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<Runnable> 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<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);
|
}
|
}
|
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<Runnable> compensations = new ArrayList<>();
|
// 分配记录延迟到白名单落库成功后写入:失败时公司余额已回补,不应留下流水
|
List<Runnable> 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<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);
|
}
|
}
|
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<String> validateImportRow(AppUserWhitelistImportDTO row) {
|
List<String> 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<TAppUserWhitelist> records) {
|
if (records == null || records.isEmpty()) {
|
return;
|
}
|
Map<Integer, Site> siteMap = getSiteMap(records);
|
Map<Integer, Partner> 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<TAppUserWhitelistSite> 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<TAppUserWhitelist> records) {
|
Map<Integer, TGroupCompany> 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<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) {
|
Map<Integer, String> 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<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(distinctIds);
|
if (response == null || !R.isSuccess(response) || response.getData() == null) {
|
return new HashMap<>();
|
}
|
Map<Integer, String> 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<Integer, Site> getSiteMap(List<TAppUserWhitelist> records) {
|
List<Long> whitelistIds = records.stream().map(TAppUserWhitelist::getId).collect(Collectors.toList());
|
List<TAppUserWhitelistSite> siteLinks = whitelistSiteService.lambdaQuery()
|
.in(TAppUserWhitelistSite::getWhitelistId, whitelistIds)
|
.list();
|
List<Integer> siteIds = siteLinks.stream().map(TAppUserWhitelistSite::getSiteId).distinct().collect(Collectors.toList());
|
if (siteIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
R<List<Site>> siteResult = siteClient.getSiteByIds(siteIds);
|
List<Site> 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<Integer, Partner> getPartnerMap(List<TAppUserWhitelist> records, Map<Integer, Site> siteMap) {
|
Map<Integer, Partner> partnerMap = new HashMap<>();
|
Set<Integer> 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<Partner> 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<Integer> authSiteIds = getCurrentPartnerSiteIds(currentPartnerId);
|
if (authSiteIds.isEmpty()) {
|
throw new ServiceException("当前合作商没有可用电站权限");
|
}
|
List<Integer> 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<TGroupCompany> 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<Long, BigDecimal> 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<Long, BigDecimal> 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<BigDecimal> 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<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) {
|
R<Set<Integer>> 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<List<Site>> siteResult = siteClient.getSiteByIds(Collections.singletonList(siteId));
|
List<Site> 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<SysUser> 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;
|
}
|
}
|