puzhibing
2026-09-30 e03f9a706ba46df72c1914870a0a707d6e5e676b
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java
@@ -1,32 +1,39 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.api.dto.ChargingCardAllocateDTO;
import com.ruoyi.account.api.dto.ChargingCardAllocateItemDTO;
import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
import com.ruoyi.account.api.dto.ChargingCardBalanceDTO;
import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
import com.ruoyi.account.api.dto.ChargingCardExcelDTO;
import com.ruoyi.account.api.dto.ChargingCardSaveDTO;
import com.ruoyi.account.api.dto.ChargingCardStatusDTO;
import com.ruoyi.account.api.model.TChargingCard;
import com.ruoyi.account.api.vo.ChargingCardAllocateVO;
import com.ruoyi.account.api.vo.ChargingCardDeductVO;
import com.ruoyi.account.mapper.TChargingCardMapper;
import com.ruoyi.common.core.context.SecurityContextHolder;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.feignClient.SysUserClient;
import com.ruoyi.system.api.model.LoginUser;
import com.ruoyi.other.api.domain.TFleet;
import com.ruoyi.other.api.domain.TGroupCompany;
import com.ruoyi.other.api.feignClient.GroupCompanyClient;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.support.TransactionCallback;
@@ -35,18 +42,23 @@
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -56,18 +68,29 @@
class TChargingCardServiceImplTest {
    @Mock private TChargingCardMapper mapper;
    @Mock private TokenService tokenService;
    @Mock private SysUserClient sysUserClient;
    @Mock private TransactionTemplate transactionTemplate;
    @Mock private GroupCompanyClient groupCompanyClient;
    @Mock private RedissonClient redissonClient;
    @Mock private RLock quotaLock;
    private TChargingCardServiceImpl service;
    @BeforeAll
    static void initTableInfo() {
        // MP 的 lambdaUpdate 会通过 TableInfoHelper.getTableInfo 取表元数据,
        // 单元测试不启动 Spring,需要手动初始化 TableInfo,否则抛 MybatisPlusException
        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TChargingCard.class);
    }
    @BeforeEach
    void setUp() {
    void setUp() throws InterruptedException {
        service = spy(new TChargingCardServiceImpl());
        ReflectionTestUtils.setField(service, "baseMapper", mapper);
        ReflectionTestUtils.setField(service, "tokenService", tokenService);
        ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient);
        ReflectionTestUtils.setField(service, "transactionTemplate", transactionTemplate);
        ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient);
        ReflectionTestUtils.setField(service, "redissonClient", redissonClient);
        lenient().when(redissonClient.getLock(anyString())).thenReturn(quotaLock);
        lenient().when(quotaLock.tryLock(anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(true);
        lenient().when(quotaLock.isHeldByCurrentThread()).thenReturn(true);
        SecurityContextHolder.setUserId("123");
    }
@@ -126,7 +149,6 @@
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setId(7L);
        dto.setCardNo(" CARD-7 ");
        dto.setBalance(new BigDecimal("999.99"));
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        TChargingCard existing = new TChargingCard();
        existing.setId(7L);
@@ -164,47 +186,6 @@
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateStatus(dto));
        assertEquals("卡片状态参数错误", exception.getMessage());
    }
    @Test
    void updateBalanceAdjustsAndRecordsForAllCards() {
        LoginUser loginUser = new LoginUser();
        loginUser.setUserid(123L);
        SysUser user = new SysUser();
        user.setOperationPassword(com.ruoyi.common.security.utils.SecurityUtils.encryptPassword("pwd"));
        when(tokenService.getLoginUser()).thenReturn(loginUser);
        when(sysUserClient.getSysUser(123L)).thenReturn(R.ok(user));
        TChargingCard first = new TChargingCard();
        first.setId(1L);
        TChargingCard second = new TChargingCard();
        second.setId(2L);
        when(mapper.selectBatchIds(anyList())).thenReturn(Arrays.asList(first, second));
        when(mapper.adjustBalance(anyList(), any(BigDecimal.class), any(Integer.class), any(BigDecimal.class), any(Long.class)))
                .thenReturn(2);
        ChargingCardBalanceDTO dto = new ChargingCardBalanceDTO();
        dto.setIds(Arrays.asList(1L, 2L));
        dto.setType(1);
        dto.setAmount(new BigDecimal("10.00"));
        dto.setOperationPassword("pwd");
        dto.setRemark(" test ");
        assertTrue(service.updateBalance(dto));
        verify(mapper).insertBalanceRecord(1L, 1, new BigDecimal("10.00"), "test", 123L);
        verify(mapper).insertBalanceRecord(2L, 1, new BigDecimal("10.00"), "test", 123L);
    }
    @Test
    void updateBalanceRejectsMissingPasswordBeforeDatabaseUpdate() {
        ChargingCardBalanceDTO dto = new ChargingCardBalanceDTO();
        dto.setIds(Collections.singletonList(1L));
        dto.setType(1);
        dto.setAmount(BigDecimal.ONE);
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateBalance(dto));
        assertEquals("请输入操作密码", exception.getMessage());
        verify(mapper, never()).adjustBalance(anyList(), any(), any(), any(), any());
    }
    @Test
@@ -338,6 +319,63 @@
        verify(service, never()).removeByIds(anyList());
    }
    // ==================== 批量解绑车队 ====================
    @Test
    void unbindFleetRejectsNullOrEmptyIds() {
        ServiceException nullEx = assertThrows(ServiceException.class, () -> service.unbindFleet(null));
        assertEquals("请选择需要解绑车队的卡片", nullEx.getMessage());
        ServiceException emptyEx = assertThrows(ServiceException.class, () -> service.unbindFleet(Collections.emptyList()));
        assertEquals("请选择需要解绑车队的卡片", emptyEx.getMessage());
    }
    @Test
    void unbindFleetRejectsMissingRecords() {
        TChargingCard existing = new TChargingCard();
        existing.setId(1L);
        doReturn(Collections.singletonList(existing)).when(service).listByIds(anyList());
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.unbindFleet(Arrays.asList(1L, 2L)));
        assertEquals("部分卡片不存在或已删除", exception.getMessage());
        verify(mapper, never()).update(isNull(), any(Wrapper.class));
    }
    @Test
    void unbindFleetClearsFleetIdAndIgnoresUnboundRecords() {
        TChargingCard bound = new TChargingCard();
        bound.setId(1L);
        bound.setFleetId(5);
        TChargingCard unbound = new TChargingCard();
        unbound.setId(2L);
        doReturn(Arrays.asList(bound, unbound)).when(service).listByIds(anyList());
        when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
        int result = service.unbindFleet(Arrays.asList(1L, 2L, 2L, null));
        assertEquals(1, result);
        ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(mapper).update(isNull(), wrapperCaptor.capture());
        String sqlSet = wrapperCaptor.getValue().getSqlSet();
        assertTrue(sqlSet.contains("fleet_id="));
        assertTrue(sqlSet.contains("update_time="));
        assertTrue(sqlSet.contains("update_by="));
    }
    @Test
    void unbindFleetReturnsZeroWhenAllUnbound() {
        TChargingCard unbound = new TChargingCard();
        unbound.setId(1L);
        doReturn(Collections.singletonList(unbound)).when(service).listByIds(anyList());
        int result = service.unbindFleet(Collections.singletonList(1L));
        assertEquals(0, result);
        verify(mapper, never()).update(isNull(), any(Wrapper.class));
    }
    @Test
    void updateStatusRejectsCardInChargingLock() {
        ChargingCardStatusDTO dto = new ChargingCardStatusDTO();
@@ -355,29 +393,302 @@
        verify(mapper, never()).updateById(any());
    }
    // ==================== 卡片所属公司/车队 + 初始分配额度 ====================
    @Test
    void updateBalanceRejectsCardInChargingLock() {
        LoginUser loginUser = new LoginUser();
        loginUser.setUserid(123L);
        SysUser user = new SysUser();
        user.setOperationPassword(SecurityUtils.encryptPassword("pwd"));
        when(tokenService.getLoginUser()).thenReturn(loginUser);
        when(sysUserClient.getSysUser(123L)).thenReturn(R.ok(user));
        TChargingCard locked = new TChargingCard();
        locked.setId(1L);
        locked.setCardNo("CARD-1");
        locked.setChargingFlag(1);
        when(mapper.selectBatchIds(anyList())).thenReturn(Collections.singletonList(locked));
        ChargingCardBalanceDTO dto = new ChargingCardBalanceDTO();
        dto.setIds(Collections.singletonList(1L));
        dto.setType(2);
        dto.setAmount(new BigDecimal("10.00"));
        dto.setOperationPassword("pwd");
    void saveCardDeductsCompanyBalanceWhenAllocationQuotaProvided() {
        when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0);
        when(mapper.insert(any(TChargingCard.class))).thenReturn(1);
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        when(groupCompanyClient.getFleetById(3)).thenReturn(R.ok(fleet(3, 9, "测试车队")));
        when(groupCompanyClient.deductBalance(9, new BigDecimal("100.00")))
                .thenReturn(R.ok(new BigDecimal("900.00")));
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setCardNo(" CARD-Q1 ");
        dto.setCompanyId(9);
        dto.setFleetId(3);
        dto.setBalance(new BigDecimal("100"));
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateBalance(dto));
        assertTrue(service.saveCard(dto));
        assertTrue(exception.getMessage().contains("正在充电中"));
        verify(mapper, never()).adjustBalance(anyList(), any(), any(), any(), any());
        ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class);
        verify(mapper).insert(card.capture());
        assertEquals(Integer.valueOf(9), card.getValue().getCompanyId());
        assertEquals(Integer.valueOf(3), card.getValue().getFleetId());
        assertEquals(new BigDecimal("100.00"), card.getValue().getBalance());
        verify(groupCompanyClient).deductBalance(9, new BigDecimal("100.00"));
    }
    @Test
    void saveCardRejectsQuotaForShareModeCompany() {
        when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0);
        TGroupCompany shareCompany = allocationCompany(9, "共享公司");
        shareCompany.setAccountMode(1);
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(shareCompany));
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setCardNo("CARD-Q2");
        dto.setCompanyId(9);
        dto.setBalance(new BigDecimal("10.00"));
        ServiceException exception = assertThrows(ServiceException.class, () -> service.saveCard(dto));
        assertEquals("仅分配模式的公司可填写初始分配额度", exception.getMessage());
        verify(groupCompanyClient, never()).deductBalance(any(), any());
        verify(mapper, never()).insert(any(TChargingCard.class));
    }
    @Test
    void saveCardKeepsQuotaEmptyAndSkipsBalanceWhenCompanyMissing() {
        when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0);
        when(mapper.insert(any(TChargingCard.class))).thenReturn(1);
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setCardNo("CARD-Q3");
        assertTrue(service.saveCard(dto));
        ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class);
        verify(mapper).insert(card.capture());
        assertEquals(null, card.getValue().getCompanyId());
        assertEquals(0, card.getValue().getBalance().compareTo(BigDecimal.ZERO));
        verify(groupCompanyClient, never()).deductBalance(any(), any());
    }
    @Test
    void saveCardRejectsFleetNotBelongingToCompany() {
        when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0);
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        when(groupCompanyClient.getFleetById(3)).thenReturn(R.ok(fleet(3, 11, "他司车队")));
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setCardNo("CARD-Q4");
        dto.setCompanyId(9);
        dto.setFleetId(3);
        ServiceException exception = assertThrows(ServiceException.class, () -> service.saveCard(dto));
        assertEquals("所属车队不属于所选公司", exception.getMessage());
    }
    @Test
    void updateCardDeductsQuotaDiffWhenQuotaIncreased() {
        ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 9, new BigDecimal("150.00"));
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("100.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("50.00")))
                .thenReturn(R.ok(new BigDecimal("50.00")));
        assertTrue(service.updateCard(dto));
        verify(groupCompanyClient).deductBalance(9, new BigDecimal("50.00"));
        verify(groupCompanyClient, never()).refundBalance(any(), any());
    }
    @Test
    void updateCardRefundsQuotaDiffWhenQuotaReduced() {
        ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 9, new BigDecimal("80.00"));
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("150.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.refundBalance(9, new BigDecimal("70.00")))
                .thenReturn(R.ok(new BigDecimal("70.00")));
        assertTrue(service.updateCard(dto));
        verify(groupCompanyClient).refundBalance(9, new BigDecimal("70.00"));
        verify(groupCompanyClient, never()).deductBalance(any(), any());
    }
    @Test
    void updateCardSwitchesCompanyRefundsOldAndDeductsNew() {
        ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 11, new BigDecimal("60.00"));
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("100.00")));
        when(mapper.updateById(any(TChargingCard.class))).thenReturn(1);
        when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
        when(groupCompanyClient.getCompanyById(11)).thenReturn(R.ok(allocationCompany(11, "新公司")));
        when(groupCompanyClient.refundBalance(9, new BigDecimal("100.00")))
                .thenReturn(R.ok(new BigDecimal("100.00")));
        when(groupCompanyClient.deductBalance(11, new BigDecimal("60.00")))
                .thenReturn(R.ok(new BigDecimal("60.00")));
        assertTrue(service.updateCard(dto));
        verify(groupCompanyClient).refundBalance(9, new BigDecimal("100.00"));
        verify(groupCompanyClient).deductBalance(11, new BigDecimal("60.00"));
    }
    @Test
    void updateCardCompensatesRefundWhenUpdateFails() {
        ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 9, new BigDecimal("150.00"));
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("100.00")));
        when(mapper.updateById(any(TChargingCard.class))).thenReturn(0);
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        when(groupCompanyClient.deductBalance(9, new BigDecimal("50.00")))
                .thenReturn(R.ok(new BigDecimal("100.00")));
        when(groupCompanyClient.refundBalance(9, new BigDecimal("50.00")))
                .thenReturn(R.ok(new BigDecimal("150.00")));
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateCard(dto));
        assertEquals("修改卡片失败", exception.getMessage());
        verify(groupCompanyClient).refundBalance(9, new BigDecimal("50.00"));
    }
    @Test
    void updateCardRejectsWhenQuotaLockUnavailable() throws InterruptedException {
        ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 9, new BigDecimal("150.00"));
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("100.00")));
        when(quotaLock.tryLock(anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(false);
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateCard(dto));
        assertEquals("该卡片额度正在处理中,请稍后重试", exception.getMessage());
        verify(groupCompanyClient, never()).deductBalance(any(), any());
    }
    @Test
    void selectCardPageFillsCompanyAndFleetNames() {
        TChargingCard record = cardWithQuota(1L, 9, new BigDecimal("10.00"));
        record.setFleetId(3);
        Page<TChargingCard> page = new Page<>(1, 10);
        page.setRecords(Collections.singletonList(record));
        when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page);
        when(groupCompanyClient.listCompaniesByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(allocationCompany(9, "测试公司"))));
        when(groupCompanyClient.listFleetsByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(fleet(3, 9, "测试车队"))));
        IPage<TChargingCard> result = service.selectCardPage(null);
        TChargingCard filled = result.getRecords().get(0);
        assertEquals("测试公司", filled.getCompanyName());
        assertEquals("测试车队", filled.getFleetName());
        // 分配模式:可用余额取卡片自身余额,非负为正常、无超额
        assertEquals(2, filled.getAccountMode());
        assertEquals(0, new BigDecimal("10.00").compareTo(filled.getAvailableBalance()));
        assertNull(filled.getOverrun());
        assertEquals(1, filled.getAccountStatus());
    }
    @Test
    void selectCardPageUsesCompanyBalanceForSharedModeCard() {
        TChargingCard record = cardWithQuota(2L, 9, null);
        Page<TChargingCard> page = new Page<>(1, 10);
        page.setRecords(Collections.singletonList(record));
        when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page);
        TGroupCompany shared = new TGroupCompany();
        shared.setId(9);
        shared.setName("共享公司");
        shared.setAccountMode(1);
        shared.setAvailableBalance(new BigDecimal("100.00"));
        when(groupCompanyClient.listCompaniesByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(shared)));
        IPage<TChargingCard> result = service.selectCardPage(null);
        TChargingCard filled = result.getRecords().get(0);
        assertEquals(1, filled.getAccountMode());
        assertEquals(0, new BigDecimal("100.00").compareTo(filled.getAvailableBalance()));
        assertNull(filled.getOverrun());
        assertEquals(1, filled.getAccountStatus());
    }
    @Test
    void selectCardPageComputesOverrunForNegativeCardBalance() {
        TChargingCard record = cardWithQuota(3L, 9, new BigDecimal("-5.50"));
        Page<TChargingCard> page = new Page<>(1, 10);
        page.setRecords(Collections.singletonList(record));
        when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page);
        when(groupCompanyClient.listCompaniesByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(allocationCompany(9, "分配公司"))));
        IPage<TChargingCard> result = service.selectCardPage(null);
        TChargingCard filled = result.getRecords().get(0);
        assertEquals(0, new BigDecimal("-5.50").compareTo(filled.getAvailableBalance()));
        assertEquals(0, new BigDecimal("5.50").compareTo(filled.getOverrun()));
        assertEquals(2, filled.getAccountStatus());
    }
    @Test
    void selectCardPageLeavesAccountFieldsEmptyWhenCompanyUnavailable() {
        TChargingCard record = cardWithQuota(4L, 9, new BigDecimal("8.00"));
        Page<TChargingCard> page = new Page<>(1, 10);
        page.setRecords(Collections.singletonList(record));
        when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page);
        when(groupCompanyClient.listCompaniesByIds(anyList())).thenReturn(R.fail("远程不可用"));
        IPage<TChargingCard> result = service.selectCardPage(null);
        TChargingCard filled = result.getRecords().get(0);
        assertNull(filled.getCompanyName());
        assertNull(filled.getAccountMode());
        assertNull(filled.getAvailableBalance());
        assertNull(filled.getOverrun());
        assertNull(filled.getAccountStatus());
    }
    @Test
    void selectExportListFillsCompanyFleetAndAccountInfo() {
        ChargingCardExcelDTO item = new ChargingCardExcelDTO();
        item.setCardNo("CARD-9");
        item.setCompanyId(9);
        item.setFleetId(3);
        item.setBalance(new BigDecimal("20.00"));
        when(mapper.selectExportList(any())).thenReturn(Collections.singletonList(item));
        when(groupCompanyClient.listCompaniesByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(allocationCompany(9, "测试公司"))));
        when(groupCompanyClient.listFleetsByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(fleet(3, 9, "测试车队"))));
        List<ChargingCardExcelDTO> result = service.selectExportList(null);
        assertEquals("测试公司", result.get(0).getCompanyName());
        assertEquals("测试车队", result.get(0).getFleetName());
        assertEquals(2, result.get(0).getAccountMode());
        assertEquals(0, new BigDecimal("20.00").compareTo(result.get(0).getAvailableBalance()));
        assertNull(result.get(0).getOverrun());
        assertEquals(1, result.get(0).getAccountStatus());
    }
    private TGroupCompany allocationCompany(Integer id, String name) {
        TGroupCompany company = new TGroupCompany();
        company.setId(id);
        company.setName(name);
        company.setAccountMode(2);
        company.setStatus(1);
        return company;
    }
    private TFleet fleet(Integer id, Integer companyId, String name) {
        TFleet fleet = new TFleet();
        fleet.setId(id);
        fleet.setCompanyId(companyId);
        fleet.setFleetName(name);
        return fleet;
    }
    private TChargingCard cardWithQuota(Long id, Integer companyId, BigDecimal quota) {
        TChargingCard card = new TChargingCard();
        card.setId(id);
        card.setCompanyId(companyId);
        card.setBalance(quota);
        return card;
    }
    private ChargingCardSaveDTO quotaUpdateDto(Long id, String cardNo, Integer companyId, BigDecimal quota) {
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setId(id);
        dto.setCardNo(cardNo);
        dto.setCompanyId(companyId);
        dto.setBalance(quota);
        return dto;
    }
    // ==================== V0.2 定稿:刷卡启动鉴权(条件密码校验) ====================
@@ -597,4 +908,165 @@
        dto.setAmount(amount);
        return dto;
    }
    // ==================== 卡片额度分配 ====================
    @Test
    void getAllocateOverviewNormalizesAmounts() {
        TGroupCompany company = allocationCompany(9, "测试公司");
        company.setAvailableBalance(new BigDecimal("8420.5"));
        company.setTotalRechargeAmount(null);
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(company));
        ChargingCardAllocateVO vo = service.getAllocateOverview(9);
        assertEquals(Integer.valueOf(9), vo.getCompanyId());
        assertEquals("测试公司", vo.getCompanyName());
        assertEquals(new BigDecimal("8420.50"), vo.getAvailableBalance());
        assertEquals(new BigDecimal("0.00"), vo.getTotalRechargeAmount());
    }
    @Test
    void getAllocateOverviewRejectsMissingCompanyId() {
        ServiceException exception = assertThrows(ServiceException.class, () -> service.getAllocateOverview(null));
        assertEquals("请选择公司", exception.getMessage());
    }
    @Test
    void allocateQuotaAddsCardBalanceAndDeductsCompanyBalance() {
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        TChargingCard first = allocateCard(1L, 9, "CARD-1", "10.00");
        TChargingCard second = allocateCard(2L, 9, "CARD-2", "20.00");
        doReturn(Arrays.asList(first, second)).when(service).listByIds(anyCollection());
        when(mapper.adjustBalance(anyList(), any(BigDecimal.class), any(Integer.class), any(BigDecimal.class),
                any(Long.class))).thenReturn(1);
        when(mapper.insertBalanceRecord(anyLong(), any(Integer.class), any(BigDecimal.class), any(), any(Long.class)))
                .thenReturn(1);
        when(groupCompanyClient.deductBalance(9, new BigDecimal("30.00"))).thenReturn(R.ok(new BigDecimal("970.00")));
        ChargingCardAllocateVO vo = service.allocateQuota(allocateDto(9,
                allocateItem(1L, "10.00"), allocateItem(2L, "20.00")));
        assertEquals(new BigDecimal("970.00"), vo.getAvailableBalance());
        assertEquals("测试公司", vo.getCompanyName());
        verify(groupCompanyClient).deductBalance(9, new BigDecimal("30.00"));
        verify(mapper).insertBalanceRecord(1L, 1, new BigDecimal("10.00"), "额度分配(公司:测试公司)", 123L);
        verify(mapper).insertBalanceRecord(2L, 1, new BigDecimal("20.00"), "额度分配(公司:测试公司)", 123L);
        verify(redissonClient).getLock("CHARGING_CARD_QUOTA:COMPANY:9");
    }
    @Test
    void allocateQuotaRejectsSharedModeCompany() {
        TGroupCompany shared = allocationCompany(9, "共享公司");
        shared.setAccountMode(1);
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(shared));
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00"))));
        assertEquals("该公司为共享模式,不支持额度分配", exception.getMessage());
        verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class));
    }
    @Test
    void allocateQuotaRejectsCardOfAnotherCompany() {
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        TChargingCard foreign = allocateCard(1L, 8, "CARD-1", "10.00");
        doReturn(Collections.singletonList(foreign)).when(service).listByIds(anyCollection());
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00"))));
        assertTrue(exception.getMessage().contains("不属于所选公司"));
        verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class));
    }
    @Test
    void allocateQuotaRefundsCompanyBalanceWhenCardUpdateFails() {
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        TChargingCard card = allocateCard(1L, 9, "CARD-1", "10.00");
        doReturn(Collections.singletonList(card)).when(service).listByIds(anyCollection());
        when(groupCompanyClient.deductBalance(9, new BigDecimal("10.00")))
                .thenReturn(R.ok(new BigDecimal("990.00")));
        when(mapper.adjustBalance(anyList(), any(BigDecimal.class), any(Integer.class), any(BigDecimal.class),
                any(Long.class))).thenReturn(0);
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00"))));
        assertTrue(exception.getMessage().contains("余额分配失败"));
        verify(groupCompanyClient).refundBalance(9, new BigDecimal("10.00"));
    }
    @Test
    void allocateQuotaReportsInsufficientCompanyBalance() {
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        TChargingCard card = allocateCard(1L, 9, "CARD-1", "0.00");
        doReturn(Collections.singletonList(card)).when(service).listByIds(anyCollection());
        when(groupCompanyClient.deductBalance(9, new BigDecimal("500.00"))).thenReturn(R.fail("公司可用余额不足"));
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "500.00"))));
        assertEquals("公司可分配余额不足,无法完成本次分配", exception.getMessage());
        verify(mapper, never()).adjustBalance(anyList(), any(BigDecimal.class), any(Integer.class),
                any(BigDecimal.class), any(Long.class));
    }
    @Test
    void allocateQuotaRejectsInvalidAmount() {
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "0"))));
        assertEquals("分配金额必须大于0", exception.getMessage());
        verify(groupCompanyClient, never()).getCompanyById(any(Integer.class));
    }
    @Test
    void allocateQuotaRejectsDuplicateCards() {
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00"), allocateItem(1L, "5.00"))));
        assertEquals("分配明细中存在重复卡片", exception.getMessage());
    }
    @Test
    void allocateQuotaRejectsChargingLockedCard() {
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        TChargingCard card = allocateCard(1L, 9, "CARD-1", "10.00");
        card.setChargingFlag(1);
        doReturn(Collections.singletonList(card)).when(service).listByIds(anyCollection());
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00"))));
        assertTrue(exception.getMessage().contains("充电中"));
        verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class));
    }
    private TChargingCard allocateCard(Long id, Integer companyId, String cardNo, String balance) {
        TChargingCard card = new TChargingCard();
        card.setId(id);
        card.setCompanyId(companyId);
        card.setCardNo(cardNo);
        card.setBalance(new BigDecimal(balance));
        card.setStatus(1);
        card.setChargingFlag(0);
        return card;
    }
    private ChargingCardAllocateItemDTO allocateItem(Long cardId, String amount) {
        ChargingCardAllocateItemDTO item = new ChargingCardAllocateItemDTO();
        item.setCardId(cardId);
        item.setAmount(new BigDecimal(amount));
        return item;
    }
    private ChargingCardAllocateDTO allocateDto(Integer companyId, ChargingCardAllocateItemDTO... items) {
        ChargingCardAllocateDTO dto = new ChargingCardAllocateDTO();
        dto.setCompanyId(companyId);
        dto.setItems(Arrays.asList(items));
        return dto;
    }
}