puzhibing
5 小时以前 d1e725ce1c6a6588fb02a660124c668fcf860d32
修改bug
6个文件已修改
252 ■■■■■ 已修改文件
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TVinManagementServiceImpl.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TVinManagementServiceImplTest.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
@@ -599,7 +599,7 @@
                if (!Integer.valueOf(STATUS_ENABLED).equals(whitelist.getStatus())) {
                    throw new ServiceException("白名单用户【" + whitelist.getPhone() + "】已停用,禁止分配额度");
                }
                BigDecimal balance = normalizeQuota(whitelist.getBalance());
                BigDecimal balance = normalizeBalance(whitelist.getBalance());
                if (balance.add(amount).compareTo(MAX_INIT_QUOTA) > 0) {
                    throw new ServiceException("白名单用户【" + whitelist.getPhone() + "】分配后余额超过上限"
                            + MAX_INIT_QUOTA.toPlainString());
@@ -614,7 +614,7 @@
                String operator = getUsername();
                for (TAppUserWhitelist whitelist : whitelists) {
                    BigDecimal amount = amounts.get(whitelist.getId());
                    BigDecimal before = normalizeQuota(whitelist.getBalance());
                    BigDecimal before = normalizeBalance(whitelist.getBalance());
                    if (baseMapper.addBalance(whitelist.getId(), amount, operator) != 1) {
                        throw new ServiceException("白名单用户【" + whitelist.getPhone() + "】额度分配失败,请重试");
                    }
@@ -825,13 +825,13 @@
                throw new ServiceException("白名单用户不存在");
            }
            assertPartnerVisible(whitelist);
            BigDecimal newQuota = normalizeQuota(saveDTO.getBalance());
            Integer oldCompanyId = whitelist.getCompanyId();
            BigDecimal oldQuota = normalizeBalance(whitelist.getBalance());
            BigDecimal newQuota = normalizeQuota(saveDTO.getBalance(), oldQuota);
            Integer newCompanyId = resolveRelationCompanyId(saveDTO);
            if (newQuota.signum() > 0) {
                requireAllocationCompany(newCompanyId);
            }
            Integer oldCompanyId = whitelist.getCompanyId();
            BigDecimal oldQuota = normalizeQuota(whitelist.getBalance());
            if (Objects.equals(oldCompanyId, newCompanyId)) {
                // 同一公司按差额调整:调增扣差额,调减退差额
                BigDecimal diff = newQuota.subtract(oldQuota);
@@ -874,7 +874,7 @@
            whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO));
            whitelist.setCompanyId(newCompanyId);
            whitelist.setFleetId(saveDTO.getFleetId());
            whitelist.setBalance(newQuota.signum() > 0 ? newQuota : null);
            whitelist.setBalance(newQuota.signum() == 0 ? null : newQuota);
            whitelist.setStatus(saveDTO.getStatus());
            whitelist.setUpdateBy(getUsername());
            whitelist.setRemark(saveDTO.getRemark());
@@ -1219,6 +1219,35 @@
        return normalized;
    }
    /**
     * 规范化余额(读取库中余额专用):null 视为 0,允许欠款(负数),不限制上限。
     * 与入参校验的 normalizeQuota 区分,避免欠款账户被"初始分配额度不能小于0"误拦。
     */
    private BigDecimal normalizeBalance(BigDecimal balance) {
        if (balance == null) {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        return balance.setScale(2, RoundingMode.HALF_UP);
    }
    /**
     * 规范化编辑提交的初始分配额度,兼容欠款账户:
     * null 视为不调整额度(保持当前余额);等于当前余额的负值放行(前端欠款原值回显);其余负数仍拒绝。
     */
    private BigDecimal normalizeQuota(BigDecimal quota, BigDecimal currentBalance) {
        if (quota == null) {
            return normalizeBalance(currentBalance);
        }
        if (quota.signum() < 0) {
            BigDecimal current = normalizeBalance(currentBalance);
            if (quota.setScale(2, RoundingMode.HALF_UP).compareTo(current) == 0) {
                return current;
            }
            throw new ServiceException("初始分配额度不能小于0");
        }
        return normalizeQuota(quota);
    }
    /** 校验公司为启用且分配模式(可设置初始分配额度) */
    private void requireAllocationCompany(Integer companyId) {
        if (companyId == null) {
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java
@@ -196,12 +196,12 @@
            card.setUpdateTime(LocalDateTime.now());
            card.setUpdateBy(SecurityUtils.getUserId());
            applyRelation(card, dto);
            BigDecimal newQuota = normalizeQuota(dto.getBalance());
            Integer oldCompanyId = latest.getCompanyId();
            BigDecimal oldQuota = normalizeBalance(latest.getBalance());
            BigDecimal newQuota = normalizeQuota(dto.getBalance(), oldQuota);
            if (newQuota.signum() > 0) {
                requireAllocationCompany(card.getCompanyId());
            }
            Integer oldCompanyId = latest.getCompanyId();
            BigDecimal oldQuota = normalizeQuota(latest.getBalance());
            if (Objects.equals(oldCompanyId, card.getCompanyId())) {
                // 同一公司按差额调整:调增扣差额,调减退差额
                BigDecimal diff = newQuota.subtract(oldQuota);
@@ -235,7 +235,7 @@
                            newQuota, TCompanyAllocateRecord.SOURCE_EDIT, "编辑卡片调整所属公司"));
                }
            }
            card.setBalance(newQuota.signum() > 0 ? newQuota : null);
            card.setBalance(newQuota.signum() == 0 ? null : newQuota);
            String encodedPassword = encodePasswordIfPresent(dto.getPassword());
            if (!updateCardRecord(card, encodedPassword, latest)) {
                throw new ServiceException("修改卡片失败");
@@ -744,6 +744,35 @@
        return normalized;
    }
    /**
     * 规范化余额(读取库中余额专用):null 视为 0,允许欠款(负数),不限制上限。
     * 与入参校验的 normalizeQuota 区分,避免欠款账户被"初始分配额度不能小于0"误拦。
     */
    private BigDecimal normalizeBalance(BigDecimal balance) {
        if (balance == null) {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        return balance.setScale(2, RoundingMode.HALF_UP);
    }
    /**
     * 规范化编辑提交的初始分配额度,兼容欠款账户:
     * null 视为不调整额度(保持当前余额);等于当前余额的负值放行(前端欠款原值回显);其余负数仍拒绝。
     */
    private BigDecimal normalizeQuota(BigDecimal quota, BigDecimal currentBalance) {
        if (quota == null) {
            return normalizeBalance(currentBalance);
        }
        if (quota.signum() < 0) {
            BigDecimal current = normalizeBalance(currentBalance);
            if (quota.setScale(2, RoundingMode.HALF_UP).compareTo(current) == 0) {
                return current;
            }
            throw new ServiceException("初始分配额度不能小于0");
        }
        return normalizeQuota(quota);
    }
    /** 校验公司为启用且分配模式(可设置初始分配额度) */
    private void requireAllocationCompany(Integer companyId) {
        if (companyId == null) {
@@ -1099,8 +1128,8 @@
            return false;
        }
        BigDecimal newBalance = card.getBalance();
        boolean balanceChanged = newBalance != null && newBalance.signum() > 0
                && !quotaEquals(existing.getBalance(), newBalance);
        // 余额与库中不一致即需落库(含欠款负数与清零场景:null 与 0 等价)
        boolean balanceChanged = !quotaEquals(existing.getBalance(), newBalance);
        boolean relationChanged = !Objects.equals(existing.getCompanyId(), card.getCompanyId())
                || !Objects.equals(existing.getFleetId(), card.getFleetId())
                || balanceChanged;
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TVinManagementServiceImpl.java
@@ -139,14 +139,14 @@
        item.setUpdateTime(LocalDateTime.now());
        item.setUpdateBy(SecurityUtils.getUserId());
        applyRelation(item, dto, old);
        BigDecimal newQuota = normalizeQuota(dto.getBalance());
        BigDecimal oldQuota = normalizeQuota(old.getBalance());
        BigDecimal oldQuota = normalizeBalance(old.getBalance());
        BigDecimal newQuota = normalizeQuota(dto.getBalance(), oldQuota);
        if (newQuota.signum() > 0) {
            requireAllocationCompany(item.getCompanyId());
        }
        Integer oldCompanyId = old.getCompanyId();
        Integer newCompanyId = item.getCompanyId();
        item.setBalance(newQuota.signum() > 0 ? newQuota : null);
        item.setBalance(newQuota.signum() == 0 ? null : newQuota);
        List<Runnable> compensations = new ArrayList<>();
        // 分配记录延迟到 VIN 记录落库成功后写入:失败时公司余额已回补,不应留下流水
        List<Runnable> recordTasks = new ArrayList<>();
@@ -351,7 +351,7 @@
                if (!Integer.valueOf(ENABLED).equals(vehicle.getStatus())) {
                    throw new ServiceException("VIN码【" + vehicle.getVin() + "】已禁用,禁止分配额度");
                }
                BigDecimal quota = normalizeQuota(vehicle.getBalance());
                BigDecimal quota = normalizeBalance(vehicle.getBalance());
                if (quota.add(amount).compareTo(MAX_INIT_QUOTA) > 0) {
                    throw new ServiceException("VIN码【" + vehicle.getVin() + "】分配后初始分配额度超过上限"
                            + MAX_INIT_QUOTA.toPlainString());
@@ -366,7 +366,7 @@
                Long operator = SecurityUtils.getUserId();
                for (TVinManagement vehicle : vehicles) {
                    BigDecimal amount = amounts.get(vehicle.getId());
                    BigDecimal before = normalizeQuota(vehicle.getBalance());
                    BigDecimal before = normalizeBalance(vehicle.getBalance());
                    if (baseMapper.addBalance(vehicle.getId(), amount, operator) != 1) {
                        throw new ServiceException("VIN码【" + vehicle.getVin() + "】额度分配失败,请重试");
                    }
@@ -402,7 +402,7 @@
            throw new ServiceException("VIN码账户余额不足");
        }
        TVinManagement latest = getById(vinId);
        return latest == null ? BigDecimal.ZERO : normalizeQuota(latest.getBalance());
        return latest == null ? BigDecimal.ZERO : normalizeBalance(latest.getBalance());
    }
    @Override
@@ -419,7 +419,7 @@
        if (hasOrderCode && companyAllocateRecordService.existsConsumeRecord(orderCode)) {
            TVinManagement existed = getById(vinId);
            log.info("[VIN结算] 订单已结算,幂等跳过:orderCode={}, vinId={}", orderCode, vinId);
            return existed == null ? BigDecimal.ZERO : normalizeQuota(existed.getBalance());
            return existed == null ? BigDecimal.ZERO : normalizeBalance(existed.getBalance());
        }
        boolean updated = lambdaUpdate()
                .eq(TVinManagement::getId, vinId)
@@ -430,7 +430,7 @@
            throw new ServiceException("VIN码账户不存在或已删除");
        }
        TVinManagement latest = getById(vinId);
        BigDecimal after = latest == null ? BigDecimal.ZERO : normalizeQuota(latest.getBalance());
        BigDecimal after = latest == null ? BigDecimal.ZERO : normalizeBalance(latest.getBalance());
        if (hasOrderCode) {
            if (latest == null || latest.getCompanyId() == null) {
                log.error("[VIN结算] 缺少公司信息,无法写入消费流水(幂等标记缺失):orderCode={}, vinId={}",
@@ -465,7 +465,7 @@
            throw new ServiceException("VIN码账户不存在或已删除");
        }
        TVinManagement latest = getById(vinId);
        return latest == null ? BigDecimal.ZERO : normalizeQuota(latest.getBalance());
        return latest == null ? BigDecimal.ZERO : normalizeBalance(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(); Integer lockedCompanyId = partnerAppUserAccessService.getCurrentCompanyId(); TGroupCompany lockedCompany = lockedCompanyId == null ? null : loadCompany(lockedCompanyId); for (VinManagementExcelDTO item : items) { TVinManagement v = new TVinManagement(); v.setVin(item.getVin().trim()); v.setPlateNumber(trim(item.getPlateNumber())); v.setPhone(trim(item.getPhone())); if (lockedCompanyId != null) { v.setCompanyId(lockedCompanyId); v.setCompanyName(lockedCompany == null ? null : lockedCompany.getName()); } else { 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码已存在"); }
@@ -565,6 +565,35 @@
        return normalized;
    }
    /**
     * 规范化余额(读取库中余额专用):null 视为 0,允许欠款(负数),不限制上限。
     * 与入参校验的 normalizeQuota 区分,避免欠款账户被"初始分配额度不能小于0"误拦。
     */
    private BigDecimal normalizeBalance(BigDecimal balance) {
        if (balance == null) {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        return balance.setScale(2, RoundingMode.HALF_UP);
    }
    /**
     * 规范化编辑提交的初始分配额度,兼容欠款账户:
     * null 视为不调整额度(保持当前余额);等于当前余额的负值放行(前端欠款原值回显);其余负数仍拒绝。
     */
    private BigDecimal normalizeQuota(BigDecimal quota, BigDecimal currentBalance) {
        if (quota == null) {
            return normalizeBalance(currentBalance);
        }
        if (quota.signum() < 0) {
            BigDecimal current = normalizeBalance(currentBalance);
            if (quota.setScale(2, RoundingMode.HALF_UP).compareTo(current) == 0) {
                return current;
            }
            throw new ServiceException("初始分配额度不能小于0");
        }
        return normalizeQuota(quota);
    }
    /** 校验公司为启用且分配模式(可设置初始分配额度) */
    private void requireAllocationCompany(Integer companyId) {
        if (companyId == null) {
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java
@@ -457,6 +457,24 @@
        assertEquals("分配明细中存在重复白名单用户", exception.getMessage());
    }
    @Test
    void allocateQuotaAllowsWhitelistWithOverdueBalance() {
        // 欠款账户(余额为负):分配额度按累加计算,分配前余额以负值写入分配流水
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        when(mapper.selectBatchIds(anyCollection()))
                .thenReturn(Collections.singletonList(allocateWhitelist(1L, 9, "13540211236", "-30.00")));
        when(mapper.addBalance(anyLong(), any(BigDecimal.class), any())).thenReturn(1);
        when(groupCompanyClient.deductBalance(9, new BigDecimal("100.00"))).thenReturn(R.ok(new BigDecimal("900.00")));
        AppUserWhitelistAllocateVO overview = service.allocateQuota(allocateDto(9, allocateItem(1L, "100.00")));
        assertEquals(new BigDecimal("900.00"), overview.getAvailableBalance());
        verify(mapper).addBalance(eq(1L), eq(new BigDecimal("100.00")), any());
        verify(companyAllocateRecordService).recordAllocate(TCompanyAllocateRecord.USER_TYPE_WHITELIST,
                "13540211236", null, 9, TCompanyAllocateRecord.SOURCE_ALLOCATE,
                new BigDecimal("-30.00"), new BigDecimal("100.00"), "额度分配(公司:测试公司)");
    }
    // ---------------- updateStatus ----------------
    @Test
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java
@@ -172,6 +172,28 @@
    }
    @Test
    void updateCardAllowsOverdueBalanceUnchanged() {
        // 欠款卡片:编辑按原值回传余额(差额为 0),不触发公司余额收退
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setId(7L);
        dto.setCardNo("CARD-7");
        dto.setBalance(new BigDecimal("-30.00"));
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        TChargingCard existing = new TChargingCard();
        existing.setId(7L);
        existing.setBalance(new BigDecimal("-30.00"));
        when(mapper.selectById(7L)).thenReturn(existing);
        when(mapper.updateById(any(TChargingCard.class))).thenReturn(1);
        assertTrue(service.updateCard(dto));
        verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class));
        verify(groupCompanyClient, never()).refundBalance(any(Integer.class), any(BigDecimal.class));
        // 余额未变化时不产生额外余额更新
        verify(mapper, never()).update(isNull(), any(Wrapper.class));
    }
    @Test
    void deleteCardsRemovesDistinctIds() {
        TChargingCard first = new TChargingCard();
        first.setId(1L);
@@ -511,6 +533,26 @@
    }
    @Test
    void updateCardClearsOverdueBalanceWhenQuotaReset() {
        // 欠款卡片:额度改为 0 时公司补足欠款,余额显式清零
        ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 9, BigDecimal.ZERO);
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("-30.00")));
        when(mapper.updateById(any(TChargingCard.class))).thenReturn(1);
        when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        when(groupCompanyClient.deductBalance(9, new BigDecimal("30.00")))
                .thenReturn(R.ok(new BigDecimal("970.00")));
        assertTrue(service.updateCard(dto));
        verify(groupCompanyClient).deductBalance(9, new BigDecimal("30.00"));
        ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(mapper).update(isNull(), wrapperCaptor.capture());
        assertTrue(wrapperCaptor.getValue().getSqlSet().contains("balance="));
    }
    @Test
    void updateCardSwitchesCompanyRefundsOldAndDeductsNew() {
        ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 11, new BigDecimal("60.00"));
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TVinManagementServiceImplTest.java
@@ -11,10 +11,12 @@
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.PartnerAppUserAccessService;
import com.ruoyi.account.service.TCompanyAllocateRecordService;
import com.ruoyi.common.core.context.SecurityContextHolder;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.exception.ServiceException;
@@ -88,6 +90,9 @@
    @Mock
    private PartnerAppUserAccessService partnerAppUserAccessService;
    @Mock
    private TCompanyAllocateRecordService companyAllocateRecordService;
    @Captor
    private ArgumentCaptor<Page<TVinManagement>> pageCaptor;
@@ -114,6 +119,7 @@
        ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient);
        ReflectionTestUtils.setField(service, "redissonClient", redissonClient);
        ReflectionTestUtils.setField(service, "partnerAppUserAccessService", partnerAppUserAccessService);
        ReflectionTestUtils.setField(service, "companyAllocateRecordService", companyAllocateRecordService);
        // 默认透传(模拟非公司账号),公司账号隔离场景由具体用例覆盖打桩
        lenient().when(partnerAppUserAccessService.resolveCompanyFilter(any())).thenAnswer(invocation -> invocation.getArgument(0));
        lenient().when(partnerAppUserAccessService.getCurrentCompanyId()).thenReturn(null);
@@ -940,6 +946,63 @@
        verify(groupCompanyClient).refundBalance(9, new BigDecimal("10.00"));
    }
    @Test
    void allocateQuotaAllowsOverdueVinBalance() {
        // 欠款账户(余额为负):分配额度按累加计算,分配前余额以负值写入分配流水
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        doReturn(Collections.singletonList(allocateVehicle(1L, 9, "VIN-1", "-30.00")))
                .when(service).listByIds(anyCollection());
        when(groupCompanyClient.deductBalance(9, new BigDecimal("100.00"))).thenReturn(R.ok(new BigDecimal("900.00")));
        when(mapper.addBalance(1L, new BigDecimal("100.00"), CURRENT_USER)).thenReturn(1);
        VinManagementAllocateVO vo = service.allocateQuota(allocateDto(9, allocateItem(1L, "100.00")));
        assertEquals(new BigDecimal("900.00"), vo.getAvailableBalance());
        verify(mapper).addBalance(1L, new BigDecimal("100.00"), CURRENT_USER);
        verify(companyAllocateRecordService).recordAllocate(eq(TCompanyAllocateRecord.USER_TYPE_VIN),
                eq("VIN-1"), isNull(), eq(9), eq(TCompanyAllocateRecord.SOURCE_ALLOCATE),
                eq(new BigDecimal("-30.00")), eq(new BigDecimal("100.00")), anyString());
    }
    @Test
    void updateVinKeepsOverdueBalanceUnchanged() {
        // 欠款账户编辑:余额原值回传(差额为 0),不触发公司余额收退且负数原样落库
        TVinManagement existing = new TVinManagement();
        existing.setId(9L);
        existing.setBalance(new BigDecimal("-30.00"));
        when(mapper.selectById(9L)).thenReturn(existing);
        when(mapper.countByVin(anyString(), eq(9L))).thenReturn(0);
        when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
        VinManagementSaveDTO dto = new VinManagementSaveDTO();
        dto.setId(9L);
        dto.setVin("LSVAM4187C2184842");
        dto.setStatus(1);
        dto.setBalance(new BigDecimal("-30.00"));
        assertTrue(service.updateVin(dto));
        ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(mapper).update(isNull(), wrapperCaptor.capture());
        assertTrue(wrapperCaptor.getValue().getSqlSet().contains("balance="));
        verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class));
        verify(groupCompanyClient, never()).refundBalance(any(Integer.class), any(BigDecimal.class));
    }
    @Test
    void settleBalanceReturnsOverdueBalance() {
        // 欠款账户结算:余额扣成负数后仍正常返回,不再因负数校验抛异常
        when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
        TVinManagement latest = new TVinManagement();
        latest.setBalance(new BigDecimal("-12.50"));
        when(mapper.selectById(9L)).thenReturn(latest);
        BigDecimal after = service.settleBalance(9L, new BigDecimal("10.00"), new BigDecimal("22.50"),
                null, null, null, null);
        assertEquals(new BigDecimal("-12.50"), after);
    }
    private TGroupCompany allocationCompany(Integer id, String name) {
        TGroupCompany company = new TGroupCompany();
        company.setId(id);