package com.ruoyi.account.service.impl; 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.TVinManagement; import com.ruoyi.account.api.vo.VinManagementAllocateVO; import com.ruoyi.account.mapper.TVinManagementMapper; import com.ruoyi.account.service.TVinManagementService; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.common.core.utils.StringUtils; import com.ruoyi.common.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 public class TVinManagementServiceImpl extends ServiceImpl implements TVinManagementService { private static final int ENABLED = 1; private static final int DISABLED = 2; /** 分配模式(公司账户模式 2=分配模式,可设置初始分配额度并扣减公司可用余额) */ private static final int ACCOUNT_MODE_ALLOCATION = 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; @Override public IPage 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(); IPage result = baseMapper.selectPageList(new Page<>(page, size), safe); fillFleetNames(result == null ? null : result.getRecords()); return result; } @Override public boolean saveVin(VinManagementSaveDTO dto) { validate(dto, false); TVinManagement item = from(dto); applyRelation(item, dto, null); BigDecimal quota = normalizeQuota(dto.getInitQuota()); List compensations = new ArrayList<>(); try { if (quota.signum() > 0) { requireAllocationCompany(item.getCompanyId()); deductBalance(item.getCompanyId(), quota); compensations.add(() -> refundQuietly(item.getCompanyId(), quota)); } item.setInitQuota(quota.signum() > 0 ? quota : null); item.setDelFlag(0); item.setCreateTime(LocalDateTime.now()); item.setCreateBy(SecurityUtils.getUserId()); if (!save(item)) { throw new ServiceException("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.getInitQuota()); BigDecimal oldQuota = normalizeQuota(old.getInitQuota()); if (newQuota.signum() > 0) { requireAllocationCompany(item.getCompanyId()); } Integer oldCompanyId = old.getCompanyId(); Integer newCompanyId = item.getCompanyId(); item.setInitQuota(newQuota.signum() > 0 ? newQuota : null); List compensations = 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)); } else if (diff.signum() < 0 && newCompanyId != null) { BigDecimal refund = diff.negate(); refundBalance(newCompanyId, refund); compensations.add(() -> deductQuietly(newCompanyId, refund)); } 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)); } if (newCompanyId != null && newQuota.signum() > 0) { deductBalance(newCompanyId, newQuota); compensations.add(() -> refundQuietly(newCompanyId, newQuota)); } } if (!updateVinRecord(item)) { throw new ServiceException("VIN码保存失败"); } return true; } catch (RuntimeException ex) { compensate(compensations); throw ex; } } @Override public boolean deleteVins(List ids) { if (ids == null || ids.isEmpty()) throw new ServiceException("请选择需要删除的VIN码"); List 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 ids) { if (ids == null || ids.isEmpty()) { throw new ServiceException("请选择需要解绑车队的VIN码"); } List distinct = ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (distinct.isEmpty()) { throw new ServiceException("请选择需要解绑车队的VIN码"); } List records = listByIds(distinct); if (records.size() != distinct.size()) { throw new ServiceException("部分VIN码不存在或已删除"); } List 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 selectExportList(VinManagementQueryDTO query) { return baseMapper.selectExportList(query == null ? new VinManagementQueryDTO() : query); } @Override public List selectAppletVehicleList(Long appUserId) { List vehicles = baseMapper.selectAppletVehicleList(appUserId); if (vehicles == null || vehicles.isEmpty()) return vehicles; vehicles.forEach(vehicle -> vehicle.setCharging(false)); List vinIds = new ArrayList<>(); for (TVinManagement vehicle : vehicles) { if (vehicle != null && vehicle.getId() != null) vinIds.add(vehicle.getId()); } if (vinIds.isEmpty()) return vehicles; try { R> response = chargingOrderClient.getChargingVinStatus(vinIds); if (response == null || !R.isSuccess(response) || response.getData() == null) return vehicles; Map chargingGunMap = new HashMap<>(); for (VinChargingStatusVO status : response.getData()) { if (status != null && status.getVinId() != null) chargingGunMap.put(status.getVinId(), status.getChargingGunId()); } for (TVinManagement vehicle : vehicles) { if (vehicle != null && chargingGunMap.containsKey(vehicle.getId())) { vehicle.setCharging(true); vehicle.setChargingGunId(chargingGunMap.get(vehicle.getId())); } } } catch (Exception ex) { log.warn("查询 VIN 车辆充电状态失败,按未充电返回,appUserId={}", appUserId, ex); } return vehicles; } @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 (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 && appUserId.equals(latest.getAppUserId())) throw new ServiceException("该车辆已绑定当前用户"); 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 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 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 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 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.getInitQuota()); if (quota.add(amount).compareTo(MAX_INIT_QUOTA) > 0) { throw new ServiceException("VIN码【" + vehicle.getVin() + "】分配后初始分配额度超过上限" + MAX_INIT_QUOTA.toPlainString()); } } // 先扣公司可分配余额(条件更新,余额不足直接失败),失败无需补偿 R 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()); if (baseMapper.addInitQuota(vehicle.getId(), amount, operator) != 1) { throw new ServiceException("VIN码【" + vehicle.getVin() + "】额度分配失败,请重试"); } } } 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 @Transactional(rollbackFor = Exception.class) public String importVins(List items) { if (items == null || items.isEmpty()) throw new ServiceException("导入数据不能为空"); Set 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码数据"; } 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 String trim(String value) { return value == null ? null : value.trim(); } 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 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 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 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 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 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 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 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 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 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 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 records) { if (records == null || records.isEmpty()) { return; } List fleetIds = records.stream() .map(TVinManagement::getFleetId) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); if (fleetIds.isEmpty()) { return; } try { R> response = groupCompanyClient.listFleetsByIds(fleetIds); if (response == null || !R.isSuccess(response) || response.getData() == null) { return; } Map 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); } } /** 更新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::getInitQuota, item.getInitQuota()) .set(TVinManagement::getRemark, item.getRemark()) .set(TVinManagement::getStatus, item.getStatus()) .set(TVinManagement::getUpdateTime, item.getUpdateTime()) .set(TVinManagement::getUpdateBy, item.getUpdateBy()) .update(); } }