| | |
| | | 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.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; |
| | |
| | | /** 姓名最大长度(对应 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)) */ |
| | |
| | | @Resource |
| | | private TCompanyAllocateRecordService companyAllocateRecordService; |
| | | |
| | | @Resource |
| | | private PartnerAppUserAccessService partnerAppUserAccessService; |
| | | |
| | | @Override |
| | | public Page<TAppUserWhitelist> pageList(AppUserWhitelistQueryDTO queryDTO) { |
| | | if (queryDTO == null) { |
| | | queryDTO = new AppUserWhitelistQueryDTO(); |
| | | } |
| | | AppUserWhitelistQueryDTO safeQuery = queryDTO == null ? new AppUserWhitelistQueryDTO() : queryDTO; |
| | | Page<TAppUserWhitelist> page = buildListQuery(safeQuery) |
| | | .orderByDesc(BasePojo::getCreateTime) |
| | | .page(Page.of(safeQuery.getPageCurr(), safeQuery.getPageSize())); |
| | | fillDisplay(page.getRecords()); |
| | | return page; |
| | | } |
| | | |
| | | /** |
| | | * 列表与导出共用的查询条件(含合作商数据范围):条件为空则不参与过滤; |
| | | * 合作商账号只能看到自身合作商录入或本人创建的白名单数据 |
| | | */ |
| | | private LambdaQueryChainWrapper<TAppUserWhitelist> buildListQuery(AppUserWhitelistQueryDTO queryDTO) { |
| | | AppUserWhitelistQueryDTO safeQuery = queryDTO == null ? new AppUserWhitelistQueryDTO() : queryDTO; |
| | | // 公司账号强制按所属公司过滤,覆盖前端传入的公司筛选 |
| | | safeQuery.setCompanyId(partnerAppUserAccessService.resolveCompanyFilter(safeQuery.getCompanyId())); |
| | | Integer currentPartnerId = getCurrentPartnerId(); |
| | | String username = getUsername(); |
| | | LambdaQueryChainWrapper<TAppUserWhitelist> query = this.lambdaQuery() |
| | | .like(StringUtils.isNotBlank(queryDTO.getPhone()), TAppUserWhitelist::getPhone, queryDTO.getPhone()) |
| | | .like(StringUtils.isNotBlank(queryDTO.getName()), TAppUserWhitelist::getName, queryDTO.getName()) |
| | | .eq(queryDTO.getCompanyId() != null, TAppUserWhitelist::getCompanyId, queryDTO.getCompanyId()) |
| | | .eq(queryDTO.getFleetId() != null, TAppUserWhitelist::getFleetId, queryDTO.getFleetId()) |
| | | .eq(queryDTO.getStatus() != null, TAppUserWhitelist::getStatus, queryDTO.getStatus()) |
| | | .like(StringUtils.isNotBlank(safeQuery.getPhone()), TAppUserWhitelist::getPhone, safeQuery.getPhone()) |
| | | .like(StringUtils.isNotBlank(safeQuery.getName()), TAppUserWhitelist::getName, safeQuery.getName()) |
| | | .eq(safeQuery.getCompanyId() != null, TAppUserWhitelist::getCompanyId, safeQuery.getCompanyId()) |
| | | .eq(safeQuery.getFleetId() != null, TAppUserWhitelist::getFleetId, safeQuery.getFleetId()) |
| | | .eq(safeQuery.getStatus() != null, TAppUserWhitelist::getStatus, safeQuery.getStatus()) |
| | | .eq(TAppUserWhitelist::getDelFlag, false); |
| | | if (currentPartnerId != null) { |
| | | if (StringUtils.isNotBlank(username)) { |
| | |
| | | query.eq(TAppUserWhitelist::getPartnerId, currentPartnerId); |
| | | } |
| | | } |
| | | Page<TAppUserWhitelist> page = query.orderByDesc(BasePojo::getCreateTime) |
| | | .page(Page.of(queryDTO.getPageCurr(), queryDTO.getPageSize())); |
| | | fillDisplay(page.getRecords()); |
| | | return page; |
| | | return query; |
| | | } |
| | | |
| | | @Override |
| | | public List<AppUserWhitelistExcelDTO> selectExportList(AppUserWhitelistQueryDTO queryDTO) { |
| | | List<TAppUserWhitelist> records = buildListQuery(queryDTO) |
| | | .orderByDesc(BasePojo::getCreateTime) |
| | | .list(); |
| | | if (records == null || records.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | Map<Integer, String> fleetNameMap = loadFleetNameMap(records.stream() |
| | | .map(TAppUserWhitelist::getFleetId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList())); |
| | | Map<Integer, TGroupCompany> companyMap = loadCompanyMap(records.stream() |
| | | .map(TAppUserWhitelist::getCompanyId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList())); |
| | | List<AppUserWhitelistExcelDTO> items = new ArrayList<>(records.size()); |
| | | for (TAppUserWhitelist record : records) { |
| | | TGroupCompany company = companyMap.get(record.getCompanyId()); |
| | | Integer accountMode = company == null ? null : company.getAccountMode(); |
| | | BigDecimal balance = resolveAvailableBalance(accountMode, record.getBalance(), |
| | | company == null ? null : company.getAvailableBalance()); |
| | | AppUserWhitelistExcelDTO item = new AppUserWhitelistExcelDTO(); |
| | | item.setPhone(record.getPhone()); |
| | | item.setName(record.getName()); |
| | | item.setCompanyName(company == null ? null : company.getName()); |
| | | item.setFleetName(fleetNameMap.get(record.getFleetId())); |
| | | item.setAccountMode(accountMode); |
| | | item.setAvailableBalance(balance); |
| | | item.setOverrun(resolveOverrun(balance)); |
| | | item.setAccountStatus(resolveAccountStatus(balance)); |
| | | item.setStatus(record.getStatus()); |
| | | item.setCompanyId(record.getCompanyId()); |
| | | item.setFleetId(record.getFleetId()); |
| | | item.setBalance(record.getBalance()); |
| | | items.add(item); |
| | | } |
| | | return items; |
| | | } |
| | | |
| | | @Override |
| | | public boolean updateStatus(AppUserWhitelistStatusDTO statusDTO) { |
| | | if (statusDTO == null || statusDTO.getId() == null |
| | | || (!Objects.equals(statusDTO.getStatus(), STATUS_ENABLED) |
| | | && !Objects.equals(statusDTO.getStatus(), STATUS_DISABLED))) { |
| | | throw new ServiceException("白名单用户状态参数错误"); |
| | | } |
| | | TAppUserWhitelist existing = this.getById(statusDTO.getId()); |
| | | if (existing == null) { |
| | | throw new ServiceException("白名单用户不存在或已删除"); |
| | | } |
| | | assertPartnerVisible(existing); |
| | | // 公司账号越权校验:只能操作本公司数据 |
| | | partnerAppUserAccessService.assertCompanyVisible(existing.getCompanyId()); |
| | | if (Objects.equals(existing.getStatus(), statusDTO.getStatus())) { |
| | | // 状态未变化视为幂等成功,避免无意义的更新人与更新时间变更 |
| | | return true; |
| | | } |
| | | LambdaUpdateChainWrapper<TAppUserWhitelist> updateWrapper = lambdaUpdate() |
| | | .eq(TAppUserWhitelist::getId, statusDTO.getId()) |
| | | .set(TAppUserWhitelist::getStatus, statusDTO.getStatus()) |
| | | .setSql("update_time = now()"); |
| | | String username = getUsername(); |
| | | if (StringUtils.isNotBlank(username)) { |
| | | updateWrapper.set(TAppUserWhitelist::getUpdateBy, username); |
| | | } |
| | | if (!updateWrapper.update()) { |
| | | throw new ServiceException("白名单用户状态更新失败,请重试"); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | |
| | | throw new ServiceException("白名单用户不存在"); |
| | | } |
| | | assertPartnerVisible(whitelist); |
| | | // 公司账号越权校验:只能查看本公司数据 |
| | | partnerAppUserAccessService.assertCompanyVisible(whitelist.getCompanyId()); |
| | | fillDisplay(Collections.singletonList(whitelist)); |
| | | return whitelist; |
| | | } |
| | |
| | | continue; |
| | | } |
| | | assertPartnerVisible(whitelist); |
| | | // 公司账号越权校验:只能删除本公司数据 |
| | | partnerAppUserAccessService.assertCompanyVisible(whitelist.getCompanyId()); |
| | | this.removeById(whitelistId); |
| | | whitelistSiteService.remove(Wrappers.lambdaQuery(TAppUserWhitelistSite.class) |
| | | .eq(TAppUserWhitelistSite::getWhitelistId, whitelistId)); |
| | |
| | | } |
| | | for (TAppUserWhitelist record : records) { |
| | | assertPartnerVisible(record); |
| | | // 公司账号越权校验:只能解绑本公司数据 |
| | | partnerAppUserAccessService.assertCompanyVisible(record.getCompanyId()); |
| | | } |
| | | List<Long> boundIds = records.stream() |
| | | .filter(record -> record.getFleetId() != null) |
| | |
| | | |
| | | @Override |
| | | public AppUserWhitelistAllocateVO getAllocateOverview(Integer companyId) { |
| | | // 公司账号只能查看本公司额度概览,覆盖前端传入的公司 |
| | | companyId = partnerAppUserAccessService.resolveCompanyFilter(companyId); |
| | | if (companyId == null) { |
| | | throw new ServiceException("请选择公司"); |
| | | } |
| | |
| | | @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(); |
| | | // 公司必须存在、启用且为分配模式(共享模式不支持额度分配) |
| | |
| | | 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("白名单用户不存在"); |
| | | } |
| | | assertPartnerVisible(existing); |
| | | // 公司账号越权校验:只能编辑本公司数据 |
| | | partnerAppUserAccessService.assertCompanyVisible(existing.getCompanyId()); |
| | | // 同白名单并发编辑串行化:额度差额计算、公司余额扣减/退还必须在临界区内,防止重复扣减或重复退还 |
| | | RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + saveDTO.getId()); |
| | | boolean locked = tryQuotaLock(lock, "该白名单额度正在处理中,请稍后重试"); |
| | |
| | | record.setSiteList(sites); |
| | | record.setSiteIds(sites.stream().map(TAppUserWhitelistSite::getSiteId).collect(Collectors.toList())); |
| | | } |
| | | fillCompanyNames(records); |
| | | fillCompanyInfo(records); |
| | | fillFleetNames(records); |
| | | } |
| | | |
| | | /** 列表回填所属公司名称(远程失败仅记录日志,不影响列表查询) */ |
| | | private void fillCompanyNames(List<TAppUserWhitelist> records) { |
| | | Map<Integer, String> nameMap = new HashMap<>(); |
| | | /** |
| | | * 列表回填所属公司名称与账户信息(账户模式、可用余额、超额、账户状态)。 |
| | | * 按公司ID去重后一次批量远程查询,远程失败仅记录日志并留空,不影响列表查询 |
| | | */ |
| | | private void fillCompanyInfo(List<TAppUserWhitelist> records) { |
| | | Map<Integer, TGroupCompany> companyMap = loadCompanyMap(records.stream() |
| | | .map(TAppUserWhitelist::getCompanyId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList())); |
| | | for (TAppUserWhitelist record : records) { |
| | | Integer companyId = record.getCompanyId(); |
| | | if (companyId == null) { |
| | | continue; |
| | | } |
| | | if (nameMap.containsKey(companyId)) { |
| | | record.setCompanyName(nameMap.get(companyId)); |
| | | continue; |
| | | } |
| | | String companyName = null; |
| | | try { |
| | | TGroupCompany company = loadCompany(companyId); |
| | | companyName = company == null ? null : company.getName(); |
| | | } catch (Exception ex) { |
| | | log.warn("回填公司名称失败,companyId={}", companyId, ex); |
| | | } |
| | | nameMap.put(companyId, companyName); |
| | | record.setCompanyName(companyName); |
| | | TGroupCompany company = companyMap.get(record.getCompanyId()); |
| | | Integer accountMode = company == null ? null : company.getAccountMode(); |
| | | BigDecimal balance = resolveAvailableBalance(accountMode, record.getBalance(), |
| | | company == null ? null : company.getAvailableBalance()); |
| | | record.setCompanyName(company == null ? null : company.getName()); |
| | | record.setAccountMode(accountMode); |
| | | record.setAvailableBalance(balance); |
| | | record.setOverrun(resolveOverrun(balance)); |
| | | record.setAccountStatus(resolveAccountStatus(balance)); |
| | | } |
| | | } |
| | | |
| | | /** 按ID批量加载公司(远程失败或服务降级时返回空Map,由调用方按未知处理) */ |
| | | private Map<Integer, TGroupCompany> loadCompanyMap(List<Integer> companyIds) { |
| | | List<Integer> distinctIds = companyIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); |
| | | if (distinctIds.isEmpty()) { |
| | | return new HashMap<>(); |
| | | } |
| | | try { |
| | | R<List<TGroupCompany>> response = groupCompanyClient.listCompaniesByIds(distinctIds); |
| | | if (response == null || !R.isSuccess(response) || response.getData() == null) { |
| | | return new HashMap<>(); |
| | | } |
| | | Map<Integer, TGroupCompany> companyMap = new HashMap<>(); |
| | | for (TGroupCompany company : response.getData()) { |
| | | if (company != null && company.getId() != null) { |
| | | companyMap.put(company.getId(), company); |
| | | } |
| | | } |
| | | return companyMap; |
| | | } catch (Exception ex) { |
| | | log.warn("回填公司账户信息失败,companyIds={}", distinctIds, ex); |
| | | return new HashMap<>(); |
| | | } |
| | | } |
| | | |
| | | /** 计算可用余额:分配模式=白名单用户自身余额,共享模式=公司可用余额,账户模式未知时留空 */ |
| | | private BigDecimal resolveAvailableBalance(Integer accountMode, BigDecimal whitelistBalance, BigDecimal companyBalance) { |
| | | if (Integer.valueOf(ACCOUNT_MODE_ALLOCATION).equals(accountMode)) { |
| | | return whitelistBalance; |
| | | } |
| | | if (Integer.valueOf(ACCOUNT_MODE_SHARED).equals(accountMode)) { |
| | | return companyBalance; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 超额:可用余额为负时取其绝对值,其余情况为空 */ |
| | | private BigDecimal resolveOverrun(BigDecimal balance) { |
| | | if (balance == null || balance.signum() >= 0) { |
| | | return null; |
| | | } |
| | | return balance.abs().setScale(AMOUNT_SCALE, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | /** 账户状态:可用余额为负=欠款,非负=正常,可用余额未知时为空 */ |
| | | private Integer resolveAccountStatus(BigDecimal balance) { |
| | | if (balance == null) { |
| | | return null; |
| | | } |
| | | return balance.signum() < 0 ? ACCOUNT_STATUS_OVERDUE : ACCOUNT_STATUS_NORMAL; |
| | | } |
| | | |
| | | /** 列表回填所属车队名称(批量查询,远程失败仅记录日志,不影响列表查询) */ |
| | | private void fillFleetNames(List<TAppUserWhitelist> records) { |
| | | List<Integer> fleetIds = records.stream() |
| | | Map<Integer, String> nameMap = loadFleetNameMap(records.stream() |
| | | .map(TAppUserWhitelist::getFleetId) |
| | | .filter(Objects::nonNull) |
| | | .distinct() |
| | | .collect(Collectors.toList()); |
| | | if (fleetIds.isEmpty()) { |
| | | return; |
| | | .collect(Collectors.toList())); |
| | | records.forEach(record -> record.setFleetName(nameMap.get(record.getFleetId()))); |
| | | } |
| | | |
| | | /** 按ID批量加载车队名称(远程失败返回空Map,由调用方按未知处理) */ |
| | | private Map<Integer, String> loadFleetNameMap(List<Integer> fleetIds) { |
| | | List<Integer> distinctIds = fleetIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); |
| | | if (distinctIds.isEmpty()) { |
| | | return new HashMap<>(); |
| | | } |
| | | try { |
| | | R<List<TFleet>> response = groupCompanyClient.listFleetsByIds(fleetIds); |
| | | R<List<TFleet>> response = groupCompanyClient.listFleetsByIds(distinctIds); |
| | | if (response == null || !R.isSuccess(response) || response.getData() == null) { |
| | | return; |
| | | return new HashMap<>(); |
| | | } |
| | | Map<Integer, String> nameMap = new HashMap<>(); |
| | | for (TFleet fleet : response.getData()) { |
| | |
| | | nameMap.put(fleet.getId(), fleet.getFleetName()); |
| | | } |
| | | } |
| | | records.forEach(record -> record.setFleetName(nameMap.get(record.getFleetId()))); |
| | | return nameMap; |
| | | } catch (Exception ex) { |
| | | log.warn("回填车队名称失败,fleetIds={}", fleetIds, ex); |
| | | log.warn("回填车队名称失败,fleetIds={}", distinctIds, ex); |
| | | return new HashMap<>(); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | /** 校验并回填所属公司与车队:车队必须归属所选公司;未选公司时按车队自动带出公司 */ |
| | | private Integer resolveRelationCompanyId(AppUserWhitelistSaveDTO saveDTO) { |
| | | // 公司账号新增/编辑白名单用户时强制归属本公司,覆盖前端传入的公司 |
| | | saveDTO.setCompanyId(partnerAppUserAccessService.resolveCompanyFilter(saveDTO.getCompanyId())); |
| | | Integer companyId = saveDTO.getCompanyId(); |
| | | if (companyId != null && loadCompany(companyId) == null) { |
| | | throw new ServiceException("所属公司不存在或已删除"); |