puzhibing
2026-09-09 4a5e1477afc559cf870b34e4693f41d85e9580d1
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java
@@ -1,17 +1,22 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.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;
@@ -22,7 +27,10 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.util.Arrays;
@@ -35,6 +43,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
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;
@@ -49,6 +58,7 @@
    @Mock private TChargingCardMapper mapper;
    @Mock private TokenService tokenService;
    @Mock private SysUserClient sysUserClient;
    @Mock private TransactionTemplate transactionTemplate;
    private TChargingCardServiceImpl service;
    @BeforeEach
@@ -57,6 +67,7 @@
        ReflectionTestUtils.setField(service, "baseMapper", mapper);
        ReflectionTestUtils.setField(service, "tokenService", tokenService);
        ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient);
        ReflectionTestUtils.setField(service, "transactionTemplate", transactionTemplate);
        SecurityContextHolder.setUserId("123");
    }
@@ -216,4 +227,358 @@
        assertTrue(service.selectExportList(null).isEmpty());
        verify(mapper).selectExportList(any());
    }
    // ==================== V0.2 定稿:卡密码配置(仅配置密码的卡刷卡时才校验) ====================
    @Test
    void saveCardEncodesPasswordWhenProvided() {
        when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0);
        when(mapper.insert(any(TChargingCard.class))).thenAnswer(invocation -> 1);
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setCardNo("CARD-PWD");
        dto.setPassword(" 123456 ");
        assertTrue(service.saveCard(dto));
        ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class);
        verify(mapper).insert(card.capture());
        assertNotNull(card.getValue().getPassword());
        assertTrue(SecurityUtils.matchesPassword("123456", card.getValue().getPassword()));
        assertEquals(Integer.valueOf(0), card.getValue().getChargingFlag());
    }
    @Test
    void saveCardKeepsPasswordNullWhenNotConfigured() {
        when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0);
        when(mapper.insert(any(TChargingCard.class))).thenAnswer(invocation -> 1);
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setCardNo("CARD-NOPWD");
        assertTrue(service.saveCard(dto));
        ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class);
        verify(mapper).insert(card.capture());
        assertEquals(null, card.getValue().getPassword());
    }
    @Test
    void updateCardRejectsCardInChargingLock() {
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setId(7L);
        dto.setCardNo("CARD-7");
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        TChargingCard locked = new TChargingCard();
        locked.setId(7L);
        locked.setCardNo("CARD-7");
        locked.setChargingFlag(1);
        when(mapper.selectById(7L)).thenReturn(locked);
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateCard(dto));
        assertTrue(exception.getMessage().contains("正在充电中"));
        verify(mapper, never()).updateById(any());
    }
    @Test
    void updateCardResetsPasswordOnlyWhenProvided() {
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setId(7L);
        dto.setCardNo("CARD-7");
        dto.setPassword("new-pwd");
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        TChargingCard existing = new TChargingCard();
        existing.setId(7L);
        existing.setCardNo("CARD-7");
        when(mapper.selectById(7L)).thenReturn(existing);
        when(mapper.updateById(any(TChargingCard.class))).thenReturn(1);
        assertTrue(service.updateCard(dto));
        ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class);
        verify(mapper).updateById(card.capture());
        assertNotNull(card.getValue().getPassword());
        assertTrue(SecurityUtils.matchesPassword("new-pwd", card.getValue().getPassword()));
    }
    @Test
    void updateCardKeepsPasswordWhenNotProvided() {
        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
        dto.setId(7L);
        dto.setCardNo("CARD-7");
        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
        TChargingCard existing = new TChargingCard();
        existing.setId(7L);
        existing.setCardNo("CARD-7");
        when(mapper.selectById(7L)).thenReturn(existing);
        when(mapper.updateById(any(TChargingCard.class))).thenReturn(1);
        assertTrue(service.updateCard(dto));
        ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class);
        verify(mapper).updateById(card.capture());
        assertEquals(null, card.getValue().getPassword());
    }
    // ==================== V0.2 定稿:充电期间卡片管理操作互斥 ====================
    @Test
    void deleteCardsRejectsCardInChargingLock() {
        TChargingCard first = new TChargingCard();
        first.setId(1L);
        TChargingCard locked = new TChargingCard();
        locked.setId(2L);
        locked.setCardNo("CARD-2");
        locked.setChargingFlag(1);
        doReturn(Arrays.asList(first, locked)).when(service).listByIds(anyList());
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.deleteCards(Arrays.asList(1L, 2L)));
        assertTrue(exception.getMessage().contains("正在充电中"));
        verify(service, never()).removeByIds(anyList());
    }
    @Test
    void updateStatusRejectsCardInChargingLock() {
        ChargingCardStatusDTO dto = new ChargingCardStatusDTO();
        dto.setId(1L);
        dto.setStatus(2);
        TChargingCard locked = new TChargingCard();
        locked.setId(1L);
        locked.setCardNo("CARD-1");
        locked.setChargingFlag(1);
        when(mapper.selectById(1L)).thenReturn(locked);
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateStatus(dto));
        assertTrue(exception.getMessage().contains("正在充电中"));
        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");
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateBalance(dto));
        assertTrue(exception.getMessage().contains("正在充电中"));
        verify(mapper, never()).adjustBalance(anyList(), any(), any(), any(), any());
    }
    // ==================== V0.2 定稿:刷卡启动鉴权(条件密码校验) ====================
    @Test
    void verifyCardForChargingPassesWithoutPasswordConfigured() {
        TChargingCard card = new TChargingCard();
        card.setId(1L);
        card.setCardNo("CARD-NOPWD");
        card.setStatus(1);
        card.setPassword(null);
        when(mapper.selectOne(any(Wrapper.class))).thenReturn(card);
        TChargingCard result = service.verifyCardForCharging(authDto("CARD-NOPWD", "whatever"));
        assertSame(card, result);
    }
    @Test
    void verifyCardForChargingPassesWithEmptyPasswordWhenNotConfigured() {
        TChargingCard card = new TChargingCard();
        card.setId(1L);
        card.setCardNo("CARD-NOPWD");
        card.setStatus(1);
        card.setPassword("");
        when(mapper.selectOne(any(Wrapper.class))).thenReturn(card);
        TChargingCard result = service.verifyCardForCharging(authDto("CARD-NOPWD", null));
        assertSame(card, result);
    }
    @Test
    void verifyCardForChargingRequiresPasswordWhenConfigured() {
        TChargingCard card = new TChargingCard();
        card.setId(1L);
        card.setCardNo("CARD-PWD");
        card.setStatus(1);
        card.setPassword(SecurityUtils.encryptPassword("123456"));
        when(mapper.selectOne(any(Wrapper.class))).thenReturn(card);
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.verifyCardForCharging(authDto("CARD-PWD", null)));
        assertEquals("卡片已配置密码,请输入卡密码", exception.getMessage());
    }
    @Test
    void verifyCardForChargingRejectsWrongPasswordWhenConfigured() {
        TChargingCard card = new TChargingCard();
        card.setId(1L);
        card.setCardNo("CARD-PWD");
        card.setStatus(1);
        card.setPassword(SecurityUtils.encryptPassword("123456"));
        when(mapper.selectOne(any(Wrapper.class))).thenReturn(card);
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.verifyCardForCharging(authDto("CARD-PWD", "bad-pwd")));
        assertEquals("卡密码错误", exception.getMessage());
    }
    @Test
    void verifyCardForChargingRejectsDisabledCard() {
        TChargingCard card = new TChargingCard();
        card.setId(1L);
        card.setCardNo("CARD-1");
        card.setStatus(2);
        when(mapper.selectOne(any(Wrapper.class))).thenReturn(card);
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.verifyCardForCharging(authDto("CARD-1", null)));
        assertEquals("卡片已禁用,无法启动充电", exception.getMessage());
    }
    @Test
    void verifyCardForChargingRejectsMissingCard() {
        when(mapper.selectOne(any(Wrapper.class))).thenReturn(null);
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.verifyCardForCharging(authDto("NO-SUCH", null)));
        assertEquals("卡片不存在或已删除", exception.getMessage());
    }
    // ==================== V0.2 定稿:充电锁定 / 解锁 ====================
    @Test
    void lockCardForChargingReturnsMapperResult() {
        when(mapper.lockCardForCharging(1L, "SD20260909001")).thenReturn(1);
        assertTrue(service.lockCardForCharging(1L, "SD20260909001"));
        when(mapper.lockCardForCharging(1L, "SD20260909001")).thenReturn(0);
        assertEquals(false, service.lockCardForCharging(1L, "SD20260909001"));
    }
    @Test
    void unlockCardForChargingIsIdempotentWhenAlreadyUnlocked() {
        when(mapper.unlockCardForCharging(1L, "SD20260909001")).thenReturn(0);
        TChargingCard unlocked = new TChargingCard();
        unlocked.setId(1L);
        unlocked.setChargingOrderCode(null);
        when(mapper.selectById(1L)).thenReturn(unlocked);
        assertTrue(service.unlockCardForCharging(1L, "SD20260909001"));
    }
    @Test
    void unlockCardForChargingRefusesWhenOccupiedByAnotherOrder() {
        when(mapper.unlockCardForCharging(1L, "SD-OLD")).thenReturn(0);
        TChargingCard occupied = new TChargingCard();
        occupied.setId(1L);
        occupied.setChargingOrderCode("SD-NEW");
        when(mapper.selectById(1L)).thenReturn(occupied);
        assertEquals(false, service.unlockCardForCharging(1L, "SD-OLD"));
    }
    // ==================== V0.2 定稿:最终结算扣款(幂等 + 允许负余额) ====================
    @Test
    void deductBalanceForChargingAllowsNegativeBalance() {
        stubTransaction();
        TChargingCard beforeCard = new TChargingCard();
        beforeCard.setId(1L);
        beforeCard.setCardNo("CARD-1");
        beforeCard.setBalance(new BigDecimal("5.00"));
        TChargingCard afterCard = new TChargingCard();
        afterCard.setId(1L);
        afterCard.setCardNo("CARD-1");
        afterCard.setBalance(new BigDecimal("-5.00"));
        when(mapper.selectById(1L)).thenReturn(beforeCard, afterCard);
        when(mapper.insertOrderDeductRecord(1L, new BigDecimal("10.00"), "SD20260909001", 0L)).thenReturn(1);
        when(mapper.deductBalanceByCardId(1L, new BigDecimal("10.00"))).thenReturn(1);
        ChargingCardDeductVO result = service.deductBalanceForCharging(
                deductDto(1L, "SD20260909001", new BigDecimal("10.00")));
        assertEquals(false, result.isDuplicated());
        assertEquals(0, result.getAfterBalance().compareTo(new BigDecimal("-5.00")));
        verify(mapper).insertOrderDeductRecord(1L, new BigDecimal("10.00"), "SD20260909001", 0L);
        verify(mapper).deductBalanceByCardId(1L, new BigDecimal("10.00"));
    }
    @Test
    void deductBalanceForChargingDuplicateRequestDoesNotDeductTwice() {
        stubTransaction();
        TChargingCard beforeCard = new TChargingCard();
        beforeCard.setId(1L);
        beforeCard.setCardNo("CARD-1");
        beforeCard.setBalance(new BigDecimal("-5.00"));
        when(mapper.selectById(1L)).thenReturn(beforeCard);
        when(mapper.insertOrderDeductRecord(1L, new BigDecimal("10.00"), "SD20260909001", 0L))
                .thenThrow(new DuplicateKeyException("uk_order_code"));
        ChargingCardDeductVO result = service.deductBalanceForCharging(
                deductDto(1L, "SD20260909001", new BigDecimal("10.00")));
        assertEquals(true, result.isDuplicated());
        verify(mapper, never()).deductBalanceByCardId(anyLong(), any());
    }
    @Test
    void deductBalanceForChargingRejectsMissingCard() {
        stubTransaction();
        when(mapper.selectById(1L)).thenReturn(null);
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.deductBalanceForCharging(deductDto(1L, "SD20260909001", new BigDecimal("10.00"))));
        assertEquals("卡片不存在或已删除", exception.getMessage());
    }
    @Test
    void deductBalanceForChargingRejectsInvalidAmount() {
        ServiceException exception = assertThrows(ServiceException.class,
                () -> service.deductBalanceForCharging(deductDto(1L, "SD20260909001", new BigDecimal("0"))));
        assertEquals("扣款金额必须大于0", exception.getMessage());
    }
    private void stubTransaction() {
        when(transactionTemplate.execute(any(TransactionCallback.class))).thenAnswer(invocation -> {
            TransactionCallback<Object> callback = invocation.getArgument(0);
            return callback.doInTransaction(null);
        });
    }
    private ChargingCardAuthDTO authDto(String cardNo, String password) {
        ChargingCardAuthDTO dto = new ChargingCardAuthDTO();
        dto.setCardNo(cardNo);
        dto.setPassword(password);
        return dto;
    }
    private ChargingCardDeductDTO deductDto(Long cardId, String orderCode, BigDecimal amount) {
        ChargingCardDeductDTO dto = new ChargingCardDeductDTO();
        dto.setCardId(cardId);
        dto.setOrderCode(orderCode);
        dto.setAmount(amount);
        return dto;
    }
}