| | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.account.api.dto.VinManagementAllocateDTO; |
| | | import com.ruoyi.account.api.dto.VinManagementAllocateItemDTO; |
| | | import com.ruoyi.account.api.dto.VinManagementExcelDTO; |
| | | import com.ruoyi.account.api.dto.VinManagementQueryDTO; |
| | | import com.ruoyi.account.api.dto.VinManagementSaveDTO; |
| | | import com.ruoyi.account.api.model.TCompanyAllocateRecord; |
| | | import com.ruoyi.account.api.model.TVinManagement; |
| | | import com.ruoyi.account.api.vo.VinManagementAllocateVO; |
| | | import com.ruoyi.account.mapper.TVinManagementMapper; |
| | | import com.ruoyi.account.service.TCompanyAllocateRecordService; |
| | | import com.ruoyi.account.service.TVinManagementService; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.exception.ServiceException; |
| | |
| | | import com.ruoyi.common.security.utils.SecurityUtils; |
| | | import com.ruoyi.order.api.feignClient.ChargingOrderClient; |
| | | import com.ruoyi.order.api.vo.VinChargingStatusVO; |
| | | import com.ruoyi.other.api.domain.TFleet; |
| | | import com.ruoyi.other.api.domain.TGroupCompany; |
| | | import com.ruoyi.other.api.feignClient.GroupCompanyClient; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.redisson.api.RLock; |
| | | import org.redisson.api.RedissonClient; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.LocalDateTime; |
| | | import java.util.ArrayList; |
| | | 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.stream.Collectors; |
| | | |
| | | @Slf4j |
| | | @Service |
| | |
| | | private static final int ENABLED = 1; |
| | | private static final int DISABLED = 2; |
| | | |
| | | /** 分配模式(公司账户模式 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"); |
| | | /** 额度分配:单次最多分配的VIN数量,防止单次请求过大 */ |
| | | private static final int MAX_ALLOCATE_VIN_COUNT = 200; |
| | | /** VIN额度分配并发锁前缀:按公司串行,防止同公司并发分配重复占用可分配余额 */ |
| | | private static final String ALLOCATE_LOCK_PREFIX = "VIN_QUOTA:COMPANY:"; |
| | | /** 获取锁最长等待时间(秒) */ |
| | | private static final long LOCK_WAIT_SECONDS = 5L; |
| | | /** 锁自动释放时间(秒),兜住异常未解锁的场景 */ |
| | | private static final long LOCK_LEASE_SECONDS = 30L; |
| | | |
| | | @Resource |
| | | private ChargingOrderClient chargingOrderClient; |
| | | |
| | | @Resource |
| | | private GroupCompanyClient groupCompanyClient; |
| | | |
| | | @Resource |
| | | private RedissonClient redissonClient; |
| | | |
| | | @Resource |
| | | private TCompanyAllocateRecordService companyAllocateRecordService; |
| | | |
| | | @Override public IPage<TVinManagement> selectPageList(VinManagementQueryDTO query) { |
| | | VinManagementQueryDTO safe = query == null ? new VinManagementQueryDTO() : query; |
| | | long page = safe.getPageCurr() == null || safe.getPageCurr() < 1 ? 1 : safe.getPageCurr(); |
| | | long size = safe.getPageSize() == null || safe.getPageSize() < 1 ? 10 : safe.getPageSize(); |
| | | return baseMapper.selectPageList(new Page<>(page, size), safe); |
| | | IPage<TVinManagement> result = baseMapper.selectPageList(new Page<>(page, size), safe); |
| | | List<TVinManagement> records = result == null ? null : result.getRecords(); |
| | | fillFleetNames(records); |
| | | fillAccountInfo(records); |
| | | return result; |
| | | } |
| | | @Override public boolean saveVin(VinManagementSaveDTO dto) { validate(dto, false); TVinManagement item = from(dto); item.setDelFlag(0); item.setCreateTime(LocalDateTime.now()); item.setCreateBy(SecurityUtils.getUserId()); return save(item); } |
| | | @Override public boolean updateVin(VinManagementSaveDTO dto) { validate(dto, true); if (getById(dto.getId()) == null) throw new ServiceException("VIN码不存在或已删除"); TVinManagement item = from(dto); item.setId(dto.getId()); item.setUpdateTime(LocalDateTime.now()); item.setUpdateBy(SecurityUtils.getUserId()); return updateById(item); } |
| | | @Override public boolean saveVin(VinManagementSaveDTO dto) { |
| | | validate(dto, false); |
| | | TVinManagement item = from(dto); |
| | | applyRelation(item, dto, null); |
| | | BigDecimal quota = normalizeQuota(dto.getBalance()); |
| | | List<Runnable> compensations = new ArrayList<>(); |
| | | try { |
| | | if (quota.signum() > 0) { |
| | | requireAllocationCompany(item.getCompanyId()); |
| | | deductBalance(item.getCompanyId(), quota); |
| | | compensations.add(() -> refundQuietly(item.getCompanyId(), quota)); |
| | | } |
| | | item.setBalance(quota.signum() > 0 ? quota : null); |
| | | item.setDelFlag(0); |
| | | item.setCreateTime(LocalDateTime.now()); |
| | | item.setCreateBy(SecurityUtils.getUserId()); |
| | | if (!save(item)) { |
| | | throw new ServiceException("VIN码保存失败"); |
| | | } |
| | | recordVinAllocate(item.getVin(), item.getCompanyId(), BigDecimal.ZERO, quota, |
| | | TCompanyAllocateRecord.SOURCE_CREATE, "新增VIN码初始分配额度"); |
| | | return true; |
| | | } catch (RuntimeException ex) { |
| | | compensate(compensations); |
| | | throw ex; |
| | | } |
| | | } |
| | | @Override public boolean updateVin(VinManagementSaveDTO dto) { |
| | | validate(dto, true); |
| | | TVinManagement old = getById(dto.getId()); |
| | | if (old == null) { |
| | | throw new ServiceException("VIN码不存在或已删除"); |
| | | } |
| | | TVinManagement item = from(dto); |
| | | item.setId(dto.getId()); |
| | | item.setUpdateTime(LocalDateTime.now()); |
| | | item.setUpdateBy(SecurityUtils.getUserId()); |
| | | applyRelation(item, dto, old); |
| | | BigDecimal newQuota = normalizeQuota(dto.getBalance()); |
| | | BigDecimal oldQuota = normalizeQuota(old.getBalance()); |
| | | if (newQuota.signum() > 0) { |
| | | requireAllocationCompany(item.getCompanyId()); |
| | | } |
| | | Integer oldCompanyId = old.getCompanyId(); |
| | | Integer newCompanyId = item.getCompanyId(); |
| | | item.setBalance(newQuota.signum() > 0 ? newQuota : null); |
| | | List<Runnable> compensations = new ArrayList<>(); |
| | | // 分配记录延迟到 VIN 记录落库成功后写入:失败时公司余额已回补,不应留下流水 |
| | | List<Runnable> recordTasks = new ArrayList<>(); |
| | | try { |
| | | 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(() -> recordVinAllocate(item.getVin(), newCompanyId, oldQuota, diff, |
| | | TCompanyAllocateRecord.SOURCE_EDIT, "编辑VIN码调整初始分配额度")); |
| | | } else if (diff.signum() < 0 && newCompanyId != null) { |
| | | BigDecimal refund = diff.negate(); |
| | | refundBalance(newCompanyId, refund); |
| | | compensations.add(() -> deductQuietly(newCompanyId, refund)); |
| | | recordTasks.add(() -> recordVinAllocate(item.getVin(), newCompanyId, oldQuota, diff, |
| | | TCompanyAllocateRecord.SOURCE_EDIT, "编辑VIN码调减初始分配额度")); |
| | | } else if (diff.signum() != 0) { |
| | | log.warn("VIN码{}额度变化但未关联公司,跳过余额处理,oldQuota={}, newQuota={}", dto.getId(), oldQuota, newQuota); |
| | | } |
| | | } else { |
| | | if (oldCompanyId != null && oldQuota.signum() > 0) { |
| | | refundBalance(oldCompanyId, oldQuota); |
| | | compensations.add(() -> deductQuietly(oldCompanyId, oldQuota)); |
| | | recordTasks.add(() -> recordVinAllocate(item.getVin(), oldCompanyId, oldQuota, oldQuota.negate(), |
| | | TCompanyAllocateRecord.SOURCE_EDIT, "编辑VIN码调整所属公司(原公司退还)")); |
| | | } |
| | | if (newCompanyId != null && newQuota.signum() > 0) { |
| | | deductBalance(newCompanyId, newQuota); |
| | | compensations.add(() -> refundQuietly(newCompanyId, newQuota)); |
| | | recordTasks.add(() -> recordVinAllocate(item.getVin(), newCompanyId, BigDecimal.ZERO, newQuota, |
| | | TCompanyAllocateRecord.SOURCE_EDIT, "编辑VIN码调整所属公司")); |
| | | } |
| | | } |
| | | if (!updateVinRecord(item)) { |
| | | throw new ServiceException("VIN码保存失败"); |
| | | } |
| | | recordTasks.forEach(Runnable::run); |
| | | return true; |
| | | } catch (RuntimeException ex) { |
| | | compensate(compensations); |
| | | throw ex; |
| | | } |
| | | } |
| | | @Override public boolean deleteVins(List<Long> ids) { if (ids == null || ids.isEmpty()) throw new ServiceException("请选择需要删除的VIN码"); List<Long> distinct = new ArrayList<>(new HashSet<>(ids)); if (listByIds(distinct).size() != distinct.size()) throw new ServiceException("部分VIN码不存在或已删除"); return removeByIds(distinct); } |
| | | |
| | | /** |
| | | * 批量解绑车队:仅清空车队归属(fleet_id 置空),公司、初始分配额度、状态等不受影响; |
| | | * 未绑定车队的选中记录忽略不计 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int unbindFleet(List<Long> ids) { |
| | | if (ids == null || ids.isEmpty()) { |
| | | throw new ServiceException("请选择需要解绑车队的VIN码"); |
| | | } |
| | | List<Long> distinct = ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); |
| | | if (distinct.isEmpty()) { |
| | | throw new ServiceException("请选择需要解绑车队的VIN码"); |
| | | } |
| | | List<TVinManagement> records = listByIds(distinct); |
| | | if (records.size() != distinct.size()) { |
| | | throw new ServiceException("部分VIN码不存在或已删除"); |
| | | } |
| | | List<Long> boundIds = records.stream() |
| | | .filter(record -> record.getFleetId() != null) |
| | | .map(TVinManagement::getId) |
| | | .collect(Collectors.toList()); |
| | | if (boundIds.isEmpty()) { |
| | | return 0; |
| | | } |
| | | // 显式 set null 清空车队归属(MyBatis-Plus 字段策略会忽略实体 null,需走 UpdateWrapper) |
| | | boolean updated = lambdaUpdate() |
| | | .in(TVinManagement::getId, boundIds) |
| | | .set(TVinManagement::getFleetId, null) |
| | | .set(TVinManagement::getUpdateTime, LocalDateTime.now()) |
| | | .set(TVinManagement::getUpdateBy, SecurityUtils.getUserId()) |
| | | .update(); |
| | | if (!updated) { |
| | | throw new ServiceException("VIN码车队解绑失败,请重试"); |
| | | } |
| | | return boundIds.size(); |
| | | } |
| | | @Override public boolean updateStatus(Long id, Integer status) { if (id == null || !validStatus(status)) throw new ServiceException("VIN码状态参数错误"); if (getById(id) == null) throw new ServiceException("VIN码不存在或已删除"); TVinManagement item = new TVinManagement(); item.setId(id); item.setStatus(status); item.setUpdateTime(LocalDateTime.now()); item.setUpdateBy(SecurityUtils.getUserId()); return updateById(item); } |
| | | @Override public List<VinManagementExcelDTO> selectExportList(VinManagementQueryDTO query) { return baseMapper.selectExportList(query == null ? new VinManagementQueryDTO() : query); } |
| | | @Override public List<TVinManagement> selectAppletVehicleList(Long apppUserId) { |
| | | List<TVinManagement> vehicles = baseMapper.selectAppletVehicleList(apppUserId); |
| | | @Override |
| | | public List<VinManagementExcelDTO> selectExportList(VinManagementQueryDTO query) { |
| | | List<VinManagementExcelDTO> items = baseMapper.selectExportList(query == null ? new VinManagementQueryDTO() : query); |
| | | if (items == null || items.isEmpty()) { |
| | | return items; |
| | | } |
| | | fillExportFleetNames(items); |
| | | fillExportAccountInfo(items); |
| | | return items; |
| | | } |
| | | @Override public List<TVinManagement> selectAppletVehicleList(Long appUserId) { |
| | | List<TVinManagement> vehicles = baseMapper.selectAppletVehicleList(appUserId); |
| | | if (vehicles == null || vehicles.isEmpty()) return vehicles; |
| | | vehicles.forEach(vehicle -> vehicle.setCharging(false)); |
| | | List<Long> vinIds = new ArrayList<>(); |
| | |
| | | } |
| | | } |
| | | } catch (Exception ex) { |
| | | log.warn("查询 VIN 车辆充电状态失败,按未充电返回,apppUserId={}", apppUserId, ex); |
| | | log.warn("查询 VIN 车辆充电状态失败,按未充电返回,appUserId={}", appUserId, ex); |
| | | } |
| | | return vehicles; |
| | | } |
| | | @Override @Transactional(rollbackFor = Exception.class) public boolean bindAppletVehicle(Long apppUserId, String keyword) { |
| | | @Override @Transactional(rollbackFor = Exception.class) public boolean bindAppletVehicle(Long appUserId, String keyword) { |
| | | String exactKeyword = trim(keyword); |
| | | if (StringUtils.isEmpty(exactKeyword)) throw new ServiceException("请输入VIN码或车牌号"); |
| | | TVinManagement vehicle = baseMapper.selectEnabledVehicle(exactKeyword); |
| | | if (vehicle == null) throw new ServiceException("该车辆不存在"); |
| | | if (apppUserId.equals(vehicle.getApppUserId())) throw new ServiceException("该车辆已绑定当前用户"); |
| | | if (vehicle.getApppUserId() != null) throw new ServiceException("该车辆已被其他用户绑定"); |
| | | if (baseMapper.bindAppletVehicle(vehicle.getId(), apppUserId) > 0) return true; |
| | | if (appUserId.equals(vehicle.getAppUserId())) throw new ServiceException("该车辆已绑定当前用户"); |
| | | if (vehicle.getAppUserId() != null) throw new ServiceException("该车辆已被其他用户绑定"); |
| | | if (baseMapper.bindAppletVehicle(vehicle.getId(), appUserId) > 0) return true; |
| | | TVinManagement latest = baseMapper.selectById(vehicle.getId()); |
| | | if (latest != null && apppUserId.equals(latest.getApppUserId())) throw new ServiceException("该车辆已绑定当前用户"); |
| | | if (latest != null && appUserId.equals(latest.getAppUserId())) throw new ServiceException("该车辆已绑定当前用户"); |
| | | throw new ServiceException("该车辆已被其他用户绑定"); |
| | | } |
| | | @Override @Transactional(rollbackFor = Exception.class) public boolean unbindAppletVehicle(Long id, Long apppUserId) { |
| | | if (id == null || baseMapper.unbindAppletVehicle(id, apppUserId) == 0) throw new ServiceException("该车辆不存在"); |
| | | @Override @Transactional(rollbackFor = Exception.class) public boolean unbindAppletVehicle(Long id, Long appUserId) { |
| | | if (id == null || baseMapper.unbindAppletVehicle(id, appUserId) == 0) throw new ServiceException("该车辆不存在"); |
| | | return true; |
| | | } |
| | | @Override public List<Long> getVinIdsByApppUserId(Long apppUserId) { return baseMapper.selectIdsByApppUserId(apppUserId); } |
| | | @Override @Transactional(rollbackFor = Exception.class) public String importVins(List<VinManagementExcelDTO> items) { if (items == null || items.isEmpty()) throw new ServiceException("导入数据不能为空"); Set<String> vins = new HashSet<>(); int row = 1; for (VinManagementExcelDTO item : items) { row++; if (item == null || StringUtils.isEmpty(item.getVin())) throw new ServiceException("第" + row + "行VIN码不能为空"); String vin = item.getVin().trim(); if (vin.length() > 64) throw new ServiceException("第" + row + "行VIN码不能超过64个字符"); if (!vins.add(vin) || baseMapper.countByVin(vin, null) > 0) throw new ServiceException("第" + row + "行VIN码已存在或重复"); parseStatus(item.getStatus(), row); } Long userId = SecurityUtils.getUserId(); LocalDateTime now = LocalDateTime.now(); for (VinManagementExcelDTO item : items) { TVinManagement v = new TVinManagement(); v.setVin(item.getVin().trim()); v.setPlateNumber(trim(item.getPlateNumber())); v.setPhone(trim(item.getPhone())); v.setCompanyName(trim(item.getCompanyName())); v.setRemark(trim(item.getRemark())); v.setStatus(parseStatus(item.getStatus(), 0)); v.setDelFlag(0); v.setCreateBy(userId); v.setCreateTime(now); save(v); } return "成功导入" + items.size() + "条VIN码数据"; } |
| | | @Override public List<Long> getVinIdsByAppUserId(Long appUserId) { return baseMapper.selectIdsByAppUserId(appUserId); } |
| | | @Override public TVinManagement getByVin(String vin) { |
| | | if (StringUtils.isEmpty(vin)) return null; |
| | | return lambdaQuery().eq(TVinManagement::getVin, vin.trim()) |
| | | .orderByDesc(TVinManagement::getId) |
| | | .last("limit 1") |
| | | .one(); |
| | | } |
| | | @Override public List<TVinManagement> listByCompany(Integer companyId) { |
| | | if (companyId == null) return new ArrayList<>(); |
| | | return lambdaQuery().eq(TVinManagement::getCompanyId, companyId) |
| | | .orderByDesc(TVinManagement::getId) |
| | | .list(); |
| | | } |
| | | @Override public long countByFleet(Integer fleetId) { |
| | | if (fleetId == null) return 0L; |
| | | return baseMapper.countByFleet(fleetId); |
| | | } |
| | | @Override |
| | | public VinManagementAllocateVO getAllocateOverview(Integer 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 VinManagementAllocateVO allocateQuota(VinManagementAllocateDTO dto) { |
| | | Map<Long, BigDecimal> amounts = validateAllocateRequest(dto); |
| | | Integer companyId = dto.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<TVinManagement> vehicles = listByIds(amounts.keySet()); |
| | | if (vehicles.size() != amounts.size()) { |
| | | throw new ServiceException("部分VIN码不存在或已删除"); |
| | | } |
| | | for (TVinManagement vehicle : vehicles) { |
| | | BigDecimal amount = amounts.get(vehicle.getId()); |
| | | if (!companyId.equals(vehicle.getCompanyId())) { |
| | | throw new ServiceException("VIN码【" + vehicle.getVin() + "】不属于所选公司,无法分配额度"); |
| | | } |
| | | if (!Integer.valueOf(ENABLED).equals(vehicle.getStatus())) { |
| | | throw new ServiceException("VIN码【" + vehicle.getVin() + "】已禁用,禁止分配额度"); |
| | | } |
| | | BigDecimal quota = normalizeQuota(vehicle.getBalance()); |
| | | if (quota.add(amount).compareTo(MAX_INIT_QUOTA) > 0) { |
| | | throw new ServiceException("VIN码【" + vehicle.getVin() + "】分配后初始分配额度超过上限" |
| | | + MAX_INIT_QUOTA.toPlainString()); |
| | | } |
| | | } |
| | | // 先扣公司可分配余额(条件更新,余额不足直接失败),失败无需补偿 |
| | | R<BigDecimal> deductResponse = groupCompanyClient.deductBalance(companyId, total); |
| | | if (deductResponse == null || !R.isSuccess(deductResponse)) { |
| | | throw new ServiceException(buildDeductFailureMessage(deductResponse)); |
| | | } |
| | | try { |
| | | Long operator = SecurityUtils.getUserId(); |
| | | for (TVinManagement vehicle : vehicles) { |
| | | BigDecimal amount = amounts.get(vehicle.getId()); |
| | | BigDecimal before = normalizeQuota(vehicle.getBalance()); |
| | | if (baseMapper.addBalance(vehicle.getId(), amount, operator) != 1) { |
| | | throw new ServiceException("VIN码【" + vehicle.getVin() + "】额度分配失败,请重试"); |
| | | } |
| | | recordVinAllocate(vehicle.getVin(), companyId, before, amount, |
| | | TCompanyAllocateRecord.SOURCE_ALLOCATE, buildVinAllocateRemark(company)); |
| | | } |
| | | } catch (RuntimeException ex) { |
| | | // VIN额度变更随事务回滚,公司余额为远程扣减需显式退还 |
| | | refundQuietly(companyId, total); |
| | | throw ex; |
| | | } |
| | | VinManagementAllocateVO vo = buildAllocateOverview(company); |
| | | vo.setAvailableBalance(normalizeMoney(deductResponse.getData())); |
| | | return vo; |
| | | } finally { |
| | | unlockQuota(lock, locked); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public BigDecimal lockBalance(Long vinId, BigDecimal amount) { |
| | | if (vinId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new ServiceException("锁定金额参数错误"); |
| | | } |
| | | boolean updated = lambdaUpdate() |
| | | .eq(TVinManagement::getId, vinId) |
| | | .eq(TVinManagement::getDelFlag, 0) |
| | | .ge(TVinManagement::getBalance, amount) |
| | | .setSql("balance = balance - " + amount.toPlainString() |
| | | + ", locked_amount = IFNULL(locked_amount, 0) + " + amount.toPlainString()) |
| | | .update(); |
| | | if (!updated) { |
| | | throw new ServiceException("VIN码账户余额不足"); |
| | | } |
| | | TVinManagement latest = getById(vinId); |
| | | return latest == null ? BigDecimal.ZERO : normalizeQuota(latest.getBalance()); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public BigDecimal settleBalance(Long vinId, BigDecimal lockedAmount, BigDecimal actualAmount, String orderCode, |
| | | String userInfo, String userName, String remark) { |
| | | BigDecimal lock = lockedAmount == null ? BigDecimal.ZERO : lockedAmount; |
| | | BigDecimal actual = actualAmount == null ? BigDecimal.ZERO : actualAmount; |
| | | if (vinId == null || lock.compareTo(BigDecimal.ZERO) < 0 || actual.compareTo(BigDecimal.ZERO) < 0) { |
| | | throw new ServiceException("结算金额参数错误"); |
| | | } |
| | | boolean hasOrderCode = StringUtils.isNotEmpty(orderCode); |
| | | // 幂等:同一订单已结算过(账户侧已落消费流水)直接返回当前余额,结算失败回退后的补偿重试不会重复结算 |
| | | if (hasOrderCode && companyAllocateRecordService.existsConsumeRecord(orderCode)) { |
| | | TVinManagement existed = getById(vinId); |
| | | log.info("[VIN结算] 订单已结算,幂等跳过:orderCode={}, vinId={}", orderCode, vinId); |
| | | return existed == null ? BigDecimal.ZERO : normalizeQuota(existed.getBalance()); |
| | | } |
| | | boolean updated = lambdaUpdate() |
| | | .eq(TVinManagement::getId, vinId) |
| | | .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("VIN码账户不存在或已删除"); |
| | | } |
| | | TVinManagement latest = getById(vinId); |
| | | BigDecimal after = latest == null ? BigDecimal.ZERO : normalizeQuota(latest.getBalance()); |
| | | if (hasOrderCode) { |
| | | if (latest == null || latest.getCompanyId() == null) { |
| | | log.error("[VIN结算] 缺少公司信息,无法写入消费流水(幂等标记缺失):orderCode={}, vinId={}", |
| | | orderCode, vinId); |
| | | } else { |
| | | // 消费流水与余额变更同事务写入:既是对账台账,也是结算幂等标记 |
| | | BigDecimal before = after.add(actual).subtract(lock); |
| | | boolean recorded = companyAllocateRecordService.recordSettleConsume( |
| | | latest.getCompanyId(), TCompanyAllocateRecord.USER_TYPE_VIN, |
| | | StringUtils.isNotEmpty(userInfo) ? userInfo : latest.getVin(), |
| | | StringUtils.isNotEmpty(userName) ? userName : latest.getPlateNumber(), |
| | | before, actual.negate(), orderCode, remark); |
| | | if (!recorded) { |
| | | throw new ServiceException("消费流水写入失败"); |
| | | } |
| | | } |
| | | } |
| | | return after; |
| | | } |
| | | |
| | | @Override |
| | | public BigDecimal unlockBalance(Long vinId, BigDecimal amount) { |
| | | if (vinId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new ServiceException("释放金额参数错误"); |
| | | } |
| | | boolean updated = lambdaUpdate() |
| | | .eq(TVinManagement::getId, vinId) |
| | | .setSql("balance = IFNULL(balance, 0) + " + amount.toPlainString() |
| | | + ", locked_amount = GREATEST(IFNULL(locked_amount, 0) - " + amount.toPlainString() + ", 0)") |
| | | .update(); |
| | | if (!updated) { |
| | | throw new ServiceException("VIN码账户不存在或已删除"); |
| | | } |
| | | TVinManagement latest = getById(vinId); |
| | | return latest == null ? BigDecimal.ZERO : normalizeQuota(latest.getBalance()); |
| | | } |
| | | @Override @Transactional(rollbackFor = Exception.class) public String importVins(List<VinManagementExcelDTO> items) { if (items == null || items.isEmpty()) throw new ServiceException("导入数据不能为空"); Set<String> vins = new HashSet<>(); int row = 1; for (VinManagementExcelDTO item : items) { row++; if (item == null || StringUtils.isEmpty(item.getVin())) throw new ServiceException("第" + row + "行VIN码不能为空"); String vin = item.getVin().trim(); if (vin.length() > 64) throw new ServiceException("第" + row + "行VIN码不能超过64个字符"); if (!vins.add(vin) || baseMapper.countByVin(vin, null) > 0) throw new ServiceException("第" + row + "行VIN码已存在或重复"); parseStatus(item.getStatus(), row); } Long userId = SecurityUtils.getUserId(); LocalDateTime now = LocalDateTime.now(); for (VinManagementExcelDTO item : items) { TVinManagement v = new TVinManagement(); v.setVin(item.getVin().trim()); v.setPlateNumber(trim(item.getPlateNumber())); v.setPhone(trim(item.getPhone())); v.setCompanyName(trim(item.getCompanyName())); v.setStatus(parseStatus(item.getStatus(), 0)); v.setDelFlag(0); v.setCreateBy(userId); v.setCreateTime(now); save(v); } return "成功导入" + items.size() + "条VIN码数据"; } |
| | | private void validate(VinManagementSaveDTO dto, boolean update) { if (dto == null || StringUtils.isEmpty(dto.getVin())) throw new ServiceException("VIN码不能为空"); if (update && dto.getId() == null) throw new ServiceException("VIN码ID不能为空"); String vin = dto.getVin().trim(); if (vin.length() > 64) throw new ServiceException("VIN码不能超过64个字符"); if (!validStatus(dto.getStatus() == null ? ENABLED : dto.getStatus())) throw new ServiceException("VIN码状态参数错误"); if (baseMapper.countByVin(vin, dto.getId()) > 0) throw new ServiceException("VIN码已存在"); } |
| | | private TVinManagement from(VinManagementSaveDTO d) { TVinManagement v = new TVinManagement(); v.setVin(d.getVin().trim()); v.setPlateNumber(trim(d.getPlateNumber())); v.setPhone(trim(d.getPhone())); v.setCompanyName(trim(d.getCompanyName())); v.setRemark(trim(d.getRemark())); v.setStatus(d.getStatus() == null ? ENABLED : d.getStatus()); return v; } |
| | | private TVinManagement from(VinManagementSaveDTO d) { TVinManagement v = new TVinManagement(); v.setVin(d.getVin().trim()); v.setPlateNumber(trim(d.getPlateNumber())); v.setPhone(trim(d.getPhone())); v.setCompanyName(trim(d.getCompanyName())); v.setStatus(d.getStatus() == null ? ENABLED : d.getStatus()); return v; } |
| | | private String trim(String value) { return value == null ? null : value.trim(); } |
| | | |
| | | /** 写入VIN码分配流水(分配为正数、回收为负数;公司为空或金额为0时自动忽略) */ |
| | | private void recordVinAllocate(String vin, Integer companyId, BigDecimal beforeBalance, BigDecimal amount, |
| | | int changeSource, String remark) { |
| | | try { |
| | | companyAllocateRecordService.recordAllocate(TCompanyAllocateRecord.USER_TYPE_VIN, vin, null, companyId, |
| | | changeSource, beforeBalance, amount, remark); |
| | | } catch (RuntimeException ex) { |
| | | // 流水为审计数据,写入失败不阻断 VIN 额度变更主流程,仅记录日志便于排查 |
| | | log.error("VIN码{}分配流水写入失败:{}", vin, ex.getMessage(), ex); |
| | | } |
| | | } |
| | | |
| | | /** 组装额度分配流水备注(含来源公司) */ |
| | | private String buildVinAllocateRemark(TGroupCompany company) { |
| | | String companyName = company == null || company.getName() == null ? "" : company.getName(); |
| | | String remark = "额度分配(公司:" + companyName + ")"; |
| | | return remark.length() > 255 ? remark.substring(0, 255) : remark; |
| | | } |
| | | private boolean validStatus(Integer status) { return status != null && (status == ENABLED || status == DISABLED); } |
| | | private Integer parseStatus(String value, int row) { if (StringUtils.isEmpty(value) || "1".equals(value.trim()) || "启用".equals(value.trim())) return ENABLED; if ("2".equals(value.trim()) || "禁用".equals(value.trim())) return DISABLED; throw new ServiceException((row > 0 ? "第" + row + "行" : "") + "启用状态错误"); } |
| | | |
| | | /** |
| | | * 校验并回填所属公司与车队:车队必须归属所选公司;未选公司时按车队自动带出公司;公司名称以公司档案为准 |
| | | */ |
| | | private void applyRelation(TVinManagement item, VinManagementSaveDTO dto, TVinManagement old) { |
| | | Integer companyId = dto.getCompanyId(); |
| | | TGroupCompany company = null; |
| | | if (companyId != null) { |
| | | company = loadCompany(companyId); |
| | | if (company == null) { |
| | | throw new ServiceException("所属公司不存在或已删除"); |
| | | } |
| | | } |
| | | Integer fleetId = dto.getFleetId(); |
| | | if (fleetId != null) { |
| | | TFleet fleet = loadFleet(fleetId); |
| | | if (fleet == null) { |
| | | throw new ServiceException("所属车队不存在或已删除"); |
| | | } |
| | | if (companyId == null) { |
| | | companyId = fleet.getCompanyId(); |
| | | company = companyId == null ? null : loadCompany(companyId); |
| | | } else if (!companyId.equals(fleet.getCompanyId())) { |
| | | throw new ServiceException("所属车队不属于所选公司"); |
| | | } |
| | | } |
| | | item.setCompanyId(companyId); |
| | | item.setFleetId(fleetId); |
| | | if (company != null) { |
| | | item.setCompanyName(company.getName()); |
| | | } else if (dto.getCompanyName() != null) { |
| | | item.setCompanyName(trim(dto.getCompanyName())); |
| | | } else { |
| | | item.setCompanyName(old == null ? null : old.getCompanyName()); |
| | | } |
| | | } |
| | | |
| | | 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(); |
| | | } |
| | | |
| | | /** 规范化初始分配额度: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 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 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 VinManagementAllocateVO buildAllocateOverview(TGroupCompany company) { |
| | | VinManagementAllocateVO vo = new VinManagementAllocateVO(); |
| | | 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); |
| | | } |
| | | |
| | | /** 校验额度分配请求并返回 VIN记录ID → 本次分配金额(保持提交顺序) */ |
| | | private Map<Long, BigDecimal> validateAllocateRequest(VinManagementAllocateDTO dto) { |
| | | if (dto == null) { |
| | | throw new ServiceException("分配参数不能为空"); |
| | | } |
| | | if (dto.getCompanyId() == null) { |
| | | throw new ServiceException("请选择公司"); |
| | | } |
| | | if (dto.getItems() == null || dto.getItems().isEmpty()) { |
| | | throw new ServiceException("请至少为一条VIN码填写本次分配金额"); |
| | | } |
| | | if (dto.getItems().size() > MAX_ALLOCATE_VIN_COUNT) { |
| | | throw new ServiceException("单次最多分配" + MAX_ALLOCATE_VIN_COUNT + "条VIN码"); |
| | | } |
| | | Map<Long, BigDecimal> amounts = new LinkedHashMap<>(); |
| | | for (VinManagementAllocateItemDTO item : dto.getItems()) { |
| | | if (item == null || item.getVinId() == null) { |
| | | throw new ServiceException("分配明细中存在无效VIN码"); |
| | | } |
| | | if (amounts.containsKey(item.getVinId())) { |
| | | throw new ServiceException("分配明细中存在重复VIN码"); |
| | | } |
| | | validateAmount(item.getAmount(), "分配金额"); |
| | | if (item.getAmount().compareTo(MAX_INIT_QUOTA) > 0) { |
| | | throw new ServiceException("单条VIN码分配金额不能超过" + MAX_INIT_QUOTA.toPlainString()); |
| | | } |
| | | amounts.put(item.getVinId(), 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 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("VIN额度分配锁释放失败,lockName={}", lock.getName(), 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 void fillFleetNames(List<TVinManagement> records) { |
| | | if (records == null || records.isEmpty()) { |
| | | return; |
| | | } |
| | | List<Integer> fleetIds = records.stream() |
| | | .map(TVinManagement::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); |
| | | } |
| | | } |
| | | |
| | | /** 导出回填车队名称(远程失败不影响导出) */ |
| | | private void fillExportFleetNames(List<VinManagementExcelDTO> items) { |
| | | List<Integer> fleetIds = items.stream() |
| | | .map(VinManagementExcelDTO::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 fillAccountInfo(List<TVinManagement> records) { |
| | | if (records == null || records.isEmpty()) { |
| | | return; |
| | | } |
| | | Map<Integer, TGroupCompany> companyMap = loadCompanyMap(records.stream() |
| | | .map(TVinManagement::getCompanyId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList())); |
| | | for (TVinManagement 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.setAccountMode(accountMode); |
| | | record.setAvailableBalance(balance); |
| | | record.setOverrun(resolveOverrun(balance)); |
| | | record.setAccountStatus(resolveAccountStatus(balance)); |
| | | } |
| | | } |
| | | |
| | | /** 导出回填账户信息:账户模式、可用余额、超额、账户状态(与列表同口径) */ |
| | | private void fillExportAccountInfo(List<VinManagementExcelDTO> items) { |
| | | Map<Integer, TGroupCompany> companyMap = loadCompanyMap(items.stream() |
| | | .map(VinManagementExcelDTO::getCompanyId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList())); |
| | | for (VinManagementExcelDTO 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.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<>(); |
| | | } |
| | | } |
| | | |
| | | /** 计算可用余额:分配模式=VIN码自身余额,共享模式=公司可用余额,账户模式未知时留空 */ |
| | | private BigDecimal resolveAvailableBalance(Integer accountMode, BigDecimal vinBalance, BigDecimal companyBalance) { |
| | | if (Integer.valueOf(ACCOUNT_MODE_ALLOCATION).equals(accountMode)) { |
| | | return vinBalance; |
| | | } |
| | | 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; |
| | | } |
| | | |
| | | /** 更新VIN记录(显式设置表单字段,保证公司/车队/额度可被清空;备注已下线,历史值保持不动) */ |
| | | private boolean updateVinRecord(TVinManagement item) { |
| | | return lambdaUpdate() |
| | | .eq(TVinManagement::getId, item.getId()) |
| | | .set(TVinManagement::getVin, item.getVin()) |
| | | .set(TVinManagement::getPlateNumber, item.getPlateNumber()) |
| | | .set(TVinManagement::getPhone, item.getPhone()) |
| | | .set(TVinManagement::getCompanyName, item.getCompanyName()) |
| | | .set(TVinManagement::getCompanyId, item.getCompanyId()) |
| | | .set(TVinManagement::getFleetId, item.getFleetId()) |
| | | .set(TVinManagement::getBalance, item.getBalance()) |
| | | .set(TVinManagement::getStatus, item.getStatus()) |
| | | .set(TVinManagement::getUpdateTime, item.getUpdateTime()) |
| | | .set(TVinManagement::getUpdateBy, item.getUpdateBy()) |
| | | .update(); |
| | | } |
| | | } |