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