| | |
| | | import com.ruoyi.account.api.dto.ChargingCardSaveDTO; |
| | | import com.ruoyi.account.api.dto.ChargingCardStatusDTO; |
| | | import com.ruoyi.account.api.model.TChargingCard; |
| | | import com.ruoyi.account.api.model.TCompanyAllocateRecord; |
| | | import com.ruoyi.account.api.vo.ChargingCardAllocateVO; |
| | | import com.ruoyi.account.api.vo.ChargingCardDeductVO; |
| | | import com.ruoyi.account.mapper.TChargingCardMapper; |
| | | import com.ruoyi.account.service.TChargingCardService; |
| | | import com.ruoyi.account.service.TCompanyAllocateRecordService; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.exception.ServiceException; |
| | | import com.ruoyi.common.core.utils.StringUtils; |
| | |
| | | private static final int PASSWORD_MAX_LENGTH = 64; |
| | | /** 公司账户模式:2=分配模式(可设置初始分配额度并扣减公司可用余额) */ |
| | | private static final int ACCOUNT_MODE_ALLOCATION = 2; |
| | | /** 公司账户模式: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; |
| | | /** 金额展示精度 */ |
| | | private static final int AMOUNT_SCALE = 2; |
| | | /** 初始分配额度上限(对应 decimal(12,2)) */ |
| | | private static final BigDecimal MAX_INIT_QUOTA = new BigDecimal("99999999.99"); |
| | | /** 额度分配:单次最多分配的卡片数量,防止单次请求过大 */ |
| | |
| | | @Autowired |
| | | private RedissonClient redissonClient; |
| | | |
| | | @Autowired |
| | | private TCompanyAllocateRecordService companyAllocateRecordService; |
| | | |
| | | @Override |
| | | public IPage<TChargingCard> selectCardPage(ChargingCardQueryDTO query) { |
| | | ChargingCardQueryDTO safeQuery = query == null ? new ChargingCardQueryDTO() : query; |
| | |
| | | long pageSize = safeQuery.getPageSize() == null || safeQuery.getPageSize() < 1 ? 10 : safeQuery.getPageSize(); |
| | | Page<TChargingCard> page = new Page<>(pageNum, pageSize); |
| | | IPage<TChargingCard> result = baseMapper.selectCardPage(page, safeQuery); |
| | | fillRelationNames(result == null ? null : result.getRecords()); |
| | | List<TChargingCard> records = result == null ? null : result.getRecords(); |
| | | fillFleetNames(records); |
| | | fillAccountInfo(records); |
| | | return result; |
| | | } |
| | | |
| | |
| | | public boolean saveCard(ChargingCardSaveDTO dto) { |
| | | validateCard(dto, false); |
| | | String cardNo = dto.getCardNo().trim(); |
| | | BigDecimal quota = normalizeQuota(dto.getInitQuota()); |
| | | BigDecimal quota = normalizeQuota(dto.getBalance()); |
| | | // 同卡号并发新增串行化,卡号重复校验与额度扣减在同一临界区内 |
| | | RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + "CARD_NO:" + cardNo); |
| | | boolean locked = tryQuotaLock(lock, "该卡号正在处理中,请稍后重试"); |
| | |
| | | try { |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setCardNo(cardNo); |
| | | card.setBalance(dto.getBalance() == null ? BigDecimal.ZERO : dto.getBalance()); |
| | | card.setBalance(quota.signum() > 0 ? quota : BigDecimal.ZERO); |
| | | card.setStatus(dto.getStatus() == null ? ENABLED : dto.getStatus()); |
| | | card.setPassword(encodePasswordIfPresent(dto.getPassword())); |
| | | card.setChargingFlag(0); |
| | |
| | | deductBalance(card.getCompanyId(), quota); |
| | | compensations.add(() -> refundQuietly(card.getCompanyId(), quota)); |
| | | } |
| | | card.setInitQuota(quota.signum() > 0 ? quota : null); |
| | | if (!save(card)) { |
| | | throw new ServiceException("新增卡片失败"); |
| | | } |
| | | recordCardAllocate(card.getCardNo(), card.getCompanyId(), BigDecimal.ZERO, quota, |
| | | TCompanyAllocateRecord.SOURCE_CREATE, "新增卡片初始分配额度"); |
| | | return true; |
| | | } catch (RuntimeException ex) { |
| | | compensate(compensations); |
| | |
| | | RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + dto.getId()); |
| | | boolean locked = tryQuotaLock(lock, "该卡片额度正在处理中,请稍后重试"); |
| | | List<Runnable> compensations = new ArrayList<>(); |
| | | // 分配记录延迟到卡片落库成功后写入:失败时公司余额已回补,不应留下流水 |
| | | List<Runnable> recordTasks = new ArrayList<>(); |
| | | try { |
| | | TChargingCard latest = getById(dto.getId()); |
| | | if (latest == null) { |
| | |
| | | card.setUpdateTime(LocalDateTime.now()); |
| | | card.setUpdateBy(SecurityUtils.getUserId()); |
| | | applyRelation(card, dto); |
| | | BigDecimal newQuota = normalizeQuota(dto.getInitQuota()); |
| | | BigDecimal newQuota = normalizeQuota(dto.getBalance()); |
| | | if (newQuota.signum() > 0) { |
| | | requireAllocationCompany(card.getCompanyId()); |
| | | } |
| | | Integer oldCompanyId = latest.getCompanyId(); |
| | | BigDecimal oldQuota = normalizeQuota(latest.getInitQuota()); |
| | | BigDecimal oldQuota = normalizeQuota(latest.getBalance()); |
| | | if (Objects.equals(oldCompanyId, card.getCompanyId())) { |
| | | // 同一公司按差额调整:调增扣差额,调减退差额 |
| | | BigDecimal diff = newQuota.subtract(oldQuota); |
| | | if (diff.signum() > 0 && card.getCompanyId() != null) { |
| | | deductBalance(card.getCompanyId(), diff); |
| | | compensations.add(() -> refundQuietly(card.getCompanyId(), diff)); |
| | | recordTasks.add(() -> recordCardAllocate(card.getCardNo(), card.getCompanyId(), oldQuota, diff, |
| | | TCompanyAllocateRecord.SOURCE_EDIT, "编辑卡片调整初始分配额度")); |
| | | } else if (diff.signum() < 0 && card.getCompanyId() != null) { |
| | | BigDecimal refund = diff.negate(); |
| | | refundBalance(card.getCompanyId(), refund); |
| | | compensations.add(() -> deductQuietly(card.getCompanyId(), refund)); |
| | | recordTasks.add(() -> recordCardAllocate(card.getCardNo(), card.getCompanyId(), oldQuota, diff, |
| | | TCompanyAllocateRecord.SOURCE_EDIT, "编辑卡片调减初始分配额度")); |
| | | } else if (diff.signum() != 0) { |
| | | log.warn("卡片{}额度变化但未关联公司,跳过公司余额处理,oldQuota={}, newQuota={}", |
| | | dto.getId(), oldQuota, newQuota); |
| | |
| | | if (oldCompanyId != null && oldQuota.signum() > 0) { |
| | | refundBalance(oldCompanyId, oldQuota); |
| | | compensations.add(() -> deductQuietly(oldCompanyId, oldQuota)); |
| | | recordTasks.add(() -> recordCardAllocate(card.getCardNo(), oldCompanyId, oldQuota, |
| | | oldQuota.negate(), TCompanyAllocateRecord.SOURCE_EDIT, "编辑卡片调整所属公司(原公司退还)")); |
| | | } |
| | | if (card.getCompanyId() != null && newQuota.signum() > 0) { |
| | | deductBalance(card.getCompanyId(), newQuota); |
| | | compensations.add(() -> refundQuietly(card.getCompanyId(), newQuota)); |
| | | recordTasks.add(() -> recordCardAllocate(card.getCardNo(), card.getCompanyId(), BigDecimal.ZERO, |
| | | newQuota, TCompanyAllocateRecord.SOURCE_EDIT, "编辑卡片调整所属公司")); |
| | | } |
| | | } |
| | | card.setInitQuota(newQuota.signum() > 0 ? newQuota : null); |
| | | card.setBalance(newQuota.signum() > 0 ? newQuota : null); |
| | | String encodedPassword = encodePasswordIfPresent(dto.getPassword()); |
| | | if (!updateCardRecord(card, encodedPassword, latest)) { |
| | | throw new ServiceException("修改卡片失败"); |
| | | } |
| | | recordTasks.forEach(Runnable::run); |
| | | return true; |
| | | } catch (RuntimeException ex) { |
| | | compensate(compensations); |
| | |
| | | throw new ServiceException("删除卡片失败"); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * 批量解绑车队:仅清空车队归属(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<TChargingCard> records = listByIds(distinct); |
| | | if (records.size() != distinct.size()) { |
| | | throw new ServiceException("部分卡片不存在或已删除"); |
| | | } |
| | | List<Long> boundIds = records.stream() |
| | | .filter(card -> card.getFleetId() != null) |
| | | .map(TChargingCard::getId) |
| | | .collect(Collectors.toList()); |
| | | if (boundIds.isEmpty()) { |
| | | return 0; |
| | | } |
| | | // 显式 set null 清空车队归属(MyBatis-Plus 字段策略会忽略实体 null,需走 UpdateWrapper) |
| | | boolean updated = lambdaUpdate() |
| | | .in(TChargingCard::getId, boundIds) |
| | | .set(TChargingCard::getFleetId, null) |
| | | .set(TChargingCard::getUpdateTime, LocalDateTime.now()) |
| | | .set(TChargingCard::getUpdateBy, SecurityUtils.getUserId()) |
| | | .update(); |
| | | if (!updated) { |
| | | throw new ServiceException("卡片车队解绑失败,请重试"); |
| | | } |
| | | return boundIds.size(); |
| | | } |
| | | |
| | | @Override |
| | |
| | | try { |
| | | for (TChargingCard card : cards) { |
| | | BigDecimal amount = amounts.get(card.getId()); |
| | | BigDecimal beforeBalance = card.getBalance() == null ? BigDecimal.ZERO : card.getBalance(); |
| | | int updated = baseMapper.adjustBalance(Collections.singletonList(card.getId()), amount, |
| | | BALANCE_ADD, MAX_AMOUNT, SecurityUtils.getUserId()); |
| | | if (updated != 1) { |
| | |
| | | } |
| | | baseMapper.insertBalanceRecord(card.getId(), BALANCE_ADD, amount, remark, |
| | | SecurityUtils.getUserId()); |
| | | recordCardAllocate(card.getCardNo(), companyId, beforeBalance, amount, |
| | | TCompanyAllocateRecord.SOURCE_ALLOCATE, remark); |
| | | } |
| | | } catch (RuntimeException ex) { |
| | | // 卡片改动随事务回滚,公司余额为远程扣减需显式退还 |
| | |
| | | |
| | | @Override |
| | | public List<ChargingCardExcelDTO> selectExportList(ChargingCardQueryDTO query) { |
| | | return baseMapper.selectExportList(query == null ? new ChargingCardQueryDTO() : query); |
| | | List<ChargingCardExcelDTO> items = baseMapper.selectExportList(query == null ? new ChargingCardQueryDTO() : query); |
| | | if (items == null || items.isEmpty()) { |
| | | return items; |
| | | } |
| | | fillExportFleetNames(items); |
| | | fillExportAccountInfo(items); |
| | | return items; |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | |
| | | @Override |
| | | public List<TChargingCard> listByCompany(Integer companyId) { |
| | | if (companyId == null) { |
| | | return java.util.Collections.emptyList(); |
| | | } |
| | | return lambdaQuery().eq(TChargingCard::getCompanyId, companyId) |
| | | .orderByDesc(TChargingCard::getId) |
| | | .list(); |
| | | } |
| | | |
| | | @Override |
| | | public ChargingCardDeductVO deductBalanceForCharging(ChargingCardDeductDTO dto) { |
| | | validateDeductRequest(dto); |
| | | try { |
| | |
| | | vo.setDuplicated(true); |
| | | return vo; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public BigDecimal lockBalance(Long cardId, BigDecimal amount) { |
| | | if (cardId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new ServiceException("锁定金额参数错误"); |
| | | } |
| | | // 实扣冻结:余额立即扣减并计入锁定金额,余额不足时条件更新命中 0 行 |
| | | boolean updated = lambdaUpdate() |
| | | .eq(TChargingCard::getId, cardId) |
| | | .ge(TChargingCard::getBalance, amount) |
| | | .setSql("balance = IFNULL(balance, 0) - " + amount.toPlainString() |
| | | + ", locked_amount = IFNULL(locked_amount, 0) + " + amount.toPlainString()) |
| | | .update(); |
| | | if (!updated) { |
| | | throw new ServiceException("卡片余额不足"); |
| | | } |
| | | TChargingCard latest = getById(cardId); |
| | | return latest == null || latest.getBalance() == null ? BigDecimal.ZERO : latest.getBalance(); |
| | | } |
| | | |
| | | @Override |
| | | public ChargingCardDeductVO settleBalanceForCharging(ChargingCardDeductDTO dto) { |
| | | validateDeductRequest(dto); |
| | | BigDecimal locked = dto.getLockedAmount() == null ? BigDecimal.ZERO : dto.getLockedAmount(); |
| | | if (locked.compareTo(BigDecimal.ZERO) < 0) { |
| | | throw new ServiceException("锁定额度参数错误"); |
| | | } |
| | | try { |
| | | return transactionTemplate.execute(status -> { |
| | | TChargingCard card = getById(dto.getCardId()); |
| | | if (card == null) { |
| | | throw new ServiceException("卡片不存在或已删除"); |
| | | } |
| | | BigDecimal before = card.getBalance() == null ? BigDecimal.ZERO : card.getBalance(); |
| | | // 第一步先占扣款幂等键:与旧扣款接口共用流水唯一索引 uk_order_code, |
| | | // 同一订单并发结算时只允许一个事务插入成功 |
| | | baseMapper.insertOrderDeductRecord(dto.getCardId(), dto.getAmount(), |
| | | dto.getOrderCode(), dto.getOperatorId() == null ? SYSTEM_OPERATOR : dto.getOperatorId()); |
| | | // 第二步原子结算:先释放锁定额,再按实际消费扣减,允许扣成负余额(V0.2 定稿) |
| | | boolean updated = lambdaUpdate() |
| | | .eq(TChargingCard::getId, dto.getCardId()) |
| | | .setSql("balance = IFNULL(balance, 0) + " + locked.toPlainString() |
| | | + " - " + dto.getAmount().toPlainString() |
| | | + ", locked_amount = GREATEST(IFNULL(locked_amount, 0) - " |
| | | + locked.toPlainString() + ", 0)") |
| | | .update(); |
| | | if (!updated) { |
| | | throw new ServiceException("卡片不存在或已删除,结算失败"); |
| | | } |
| | | TChargingCard afterCard = getById(dto.getCardId()); |
| | | BigDecimal after = afterCard == null || afterCard.getBalance() == null |
| | | ? BigDecimal.ZERO : afterCard.getBalance(); |
| | | ChargingCardDeductVO vo = new ChargingCardDeductVO(); |
| | | vo.setCardId(dto.getCardId()); |
| | | vo.setOrderCode(dto.getOrderCode()); |
| | | vo.setAmount(dto.getAmount()); |
| | | vo.setBeforeBalance(before); |
| | | vo.setAfterBalance(after); |
| | | vo.setDuplicated(false); |
| | | return vo; |
| | | }); |
| | | } catch (DuplicateKeyException e) { |
| | | // 唯一键冲突:该订单此前已结算成功(含并发下慢事务先提交),按幂等重复返回,不重复结算 |
| | | log.warn("刷卡订单结算幂等命中,不重复结算。订单号={}, 卡片ID={}", dto.getOrderCode(), dto.getCardId()); |
| | | ChargingCardDeductVO vo = new ChargingCardDeductVO(); |
| | | vo.setCardId(dto.getCardId()); |
| | | vo.setOrderCode(dto.getOrderCode()); |
| | | vo.setAmount(dto.getAmount()); |
| | | vo.setDuplicated(true); |
| | | return vo; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public BigDecimal unlockBalance(Long cardId, BigDecimal amount) { |
| | | if (cardId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new ServiceException("释放金额参数错误"); |
| | | } |
| | | // 释放锁定额:余额回补,锁定额度扣减,允许余额超过锁定额(异常数据兜底) |
| | | boolean updated = lambdaUpdate() |
| | | .eq(TChargingCard::getId, cardId) |
| | | .setSql("balance = IFNULL(balance, 0) + " + amount.toPlainString() |
| | | + ", locked_amount = GREATEST(IFNULL(locked_amount, 0) - " + amount.toPlainString() + ", 0)") |
| | | .update(); |
| | | if (!updated) { |
| | | throw new ServiceException("卡片不存在或已删除"); |
| | | } |
| | | TChargingCard latest = getById(cardId); |
| | | return latest == null || latest.getBalance() == null ? BigDecimal.ZERO : latest.getBalance(); |
| | | } |
| | | |
| | | /** |
| | |
| | | return amounts; |
| | | } |
| | | |
| | | /** 写入卡片分配流水(分配为正数、回收为负数;公司为空或金额为0时自动忽略) */ |
| | | private void recordCardAllocate(String cardNo, Integer companyId, BigDecimal beforeBalance, BigDecimal amount, |
| | | int changeSource, String remark) { |
| | | try { |
| | | companyAllocateRecordService.recordAllocate(TCompanyAllocateRecord.USER_TYPE_CARD, cardNo, null, companyId, |
| | | changeSource, beforeBalance, amount, remark); |
| | | } catch (RuntimeException ex) { |
| | | // 流水为审计数据,写入失败不阻断卡片额度变更主流程,仅记录日志便于排查 |
| | | log.error("卡片{}分配流水写入失败:{}", cardNo, ex.getMessage(), ex); |
| | | } |
| | | } |
| | | |
| | | /** 组装额度分配流水备注(含来源公司,超长截断以适配 varchar(255)) */ |
| | | private String buildAllocateRemark(TGroupCompany company, String userRemark) { |
| | | String companyName = company == null || company.getName() == null ? "" : company.getName(); |
| | |
| | | } |
| | | } |
| | | |
| | | /** 列表回填公司/车队名称(远程失败不影响列表查询) */ |
| | | private void fillRelationNames(List<TChargingCard> records) { |
| | | /** 列表回填车队名称(远程失败不影响列表查询) */ |
| | | private void fillFleetNames(List<TChargingCard> records) { |
| | | if (records == null || records.isEmpty()) { |
| | | return; |
| | | } |
| | | fillFleetNames(records); |
| | | fillCompanyNames(records); |
| | | } |
| | | |
| | | private void fillFleetNames(List<TChargingCard> records) { |
| | | List<Integer> fleetIds = records.stream() |
| | | .map(TChargingCard::getFleetId) |
| | | .filter(Objects::nonNull) |
| | |
| | | } |
| | | } |
| | | |
| | | private void fillCompanyNames(List<TChargingCard> records) { |
| | | Map<Integer, String> nameMap = new HashMap<>(); |
| | | for (TChargingCard 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 fillAccountInfo(List<TChargingCard> records) { |
| | | if (records == null || records.isEmpty()) { |
| | | return; |
| | | } |
| | | Map<Integer, TGroupCompany> companyMap = loadCompanyMap(records.stream() |
| | | .map(TChargingCard::getCompanyId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList())); |
| | | for (TChargingCard 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)); |
| | | } |
| | | } |
| | | |
| | | /** 导出回填车队名称(远程失败不影响导出) */ |
| | | private void fillExportFleetNames(List<ChargingCardExcelDTO> items) { |
| | | List<Integer> fleetIds = items.stream() |
| | | .map(ChargingCardExcelDTO::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()); |
| | | } |
| | | } |
| | | items.forEach(item -> item.setFleetName(nameMap.get(item.getFleetId()))); |
| | | } catch (Exception ex) { |
| | | log.warn("导出回填车队名称失败,fleetIds={}", fleetIds, ex); |
| | | } |
| | | } |
| | | |
| | | /** 导出回填所属公司名称与账户信息(与列表同口径) */ |
| | | private void fillExportAccountInfo(List<ChargingCardExcelDTO> items) { |
| | | Map<Integer, TGroupCompany> companyMap = loadCompanyMap(items.stream() |
| | | .map(ChargingCardExcelDTO::getCompanyId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList())); |
| | | for (ChargingCardExcelDTO item : items) { |
| | | TGroupCompany company = companyMap.get(item.getCompanyId()); |
| | | Integer accountMode = company == null ? null : company.getAccountMode(); |
| | | BigDecimal balance = resolveAvailableBalance(accountMode, item.getBalance(), |
| | | company == null ? null : company.getAvailableBalance()); |
| | | item.setCompanyName(company == null ? null : company.getName()); |
| | | item.setAccountMode(accountMode); |
| | | item.setAvailableBalance(balance); |
| | | item.setOverrun(resolveOverrun(balance)); |
| | | item.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 cardBalance, BigDecimal companyBalance) { |
| | | if (Integer.valueOf(ACCOUNT_MODE_ALLOCATION).equals(accountMode)) { |
| | | return cardBalance; |
| | | } |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | if (!updateById(update)) { |
| | | return false; |
| | | } |
| | | BigDecimal newBalance = card.getBalance(); |
| | | boolean balanceChanged = newBalance != null && newBalance.signum() > 0 |
| | | && !quotaEquals(existing.getBalance(), newBalance); |
| | | boolean relationChanged = !Objects.equals(existing.getCompanyId(), card.getCompanyId()) |
| | | || !Objects.equals(existing.getFleetId(), card.getFleetId()) |
| | | || !quotaEquals(existing.getInitQuota(), card.getInitQuota()); |
| | | || balanceChanged; |
| | | if (!relationChanged) { |
| | | return true; |
| | | } |
| | | UpdateWrapper<TChargingCard> relationUpdate = new UpdateWrapper<>(); |
| | | relationUpdate.eq("id", card.getId()) |
| | | .set("company_id", card.getCompanyId()) |
| | | .set("fleet_id", card.getFleetId()) |
| | | .set("init_quota", card.getInitQuota()); |
| | | .set("fleet_id", card.getFleetId()); |
| | | if (balanceChanged) { |
| | | relationUpdate.set("balance", newBalance); |
| | | } |
| | | return update(null, relationUpdate); |
| | | } |
| | | |