package com.ruoyi.account.service.impl; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.account.api.dto.CompanyAllocateRecordQueryDTO; import com.ruoyi.account.api.model.TAppUserWhitelist; import com.ruoyi.account.api.model.TChargingCard; import com.ruoyi.account.api.model.TCompanyAllocateRecord; import com.ruoyi.account.api.model.TVinManagement; import com.ruoyi.account.api.vo.CompanyAllocateSummaryVO; import com.ruoyi.account.mapper.TAppUserWhitelistMapper; import com.ruoyi.account.mapper.TChargingCardMapper; import com.ruoyi.account.mapper.TCompanyAllocateRecordMapper; import com.ruoyi.account.mapper.TVinManagementMapper; import com.ruoyi.common.core.context.SecurityContextHolder; import com.ruoyi.common.core.exception.ServiceException; import org.junit.jupiter.api.AfterEach; 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.test.util.ReflectionTestUtils; 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.assertFalse; 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.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class TCompanyAllocateRecordServiceImplTest { @Mock private TCompanyAllocateRecordMapper mapper; @Mock private TVinManagementMapper vinMapper; @Mock private TChargingCardMapper cardMapper; @Mock private TAppUserWhitelistMapper whitelistMapper; @Mock private RedissonClient redissonClient; @Mock private RLock lock; private TCompanyAllocateRecordServiceImpl service; @BeforeEach void setUp() { service = spy(new TCompanyAllocateRecordServiceImpl()); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "vinManagementMapper", vinMapper); ReflectionTestUtils.setField(service, "chargingCardMapper", cardMapper); ReflectionTestUtils.setField(service, "appUserWhitelistMapper", whitelistMapper); ReflectionTestUtils.setField(service, "redissonClient", redissonClient); lenient().when(mapper.insert(any(TCompanyAllocateRecord.class))).thenReturn(1); lenient().when(redissonClient.getLock(anyString())).thenReturn(lock); SecurityContextHolder.setUserId("123"); SecurityContextHolder.setUserName("admin"); } @AfterEach void tearDown() { SecurityContextHolder.remove(); } @Test void recordAllocateWritesBeforeAfterAndOperator() { boolean saved = service.recordAllocate(TCompanyAllocateRecord.USER_TYPE_VIN, "LVIN0001", null, 9, TCompanyAllocateRecord.SOURCE_ALLOCATE, new BigDecimal("10.5"), new BigDecimal("20.25"), "额度分配"); assertTrue(saved); TCompanyAllocateRecord record = captureInserted(); assertEquals(9, record.getCompanyId()); assertEquals(TCompanyAllocateRecord.USER_TYPE_VIN, record.getUserType()); assertEquals("LVIN0001", record.getUserInfo()); assertEquals(TCompanyAllocateRecord.SOURCE_ALLOCATE, record.getChangeSource()); assertEquals(new BigDecimal("10.50"), record.getBeforeBalance()); assertEquals(new BigDecimal("20.25"), record.getAmount()); assertEquals(new BigDecimal("30.75"), record.getAfterBalance()); assertEquals(123L, record.getOperatorId()); assertEquals("admin", record.getOperatorName()); assertNotNull(record.getOperateTime()); assertEquals(record.getOperateTime(), record.getCreateTime()); } @Test void recordAllocateSupportsNegativeAmountForRecycle() { service.recordAllocate(TCompanyAllocateRecord.USER_TYPE_WHITELIST, "13800000000", "张三", 5, TCompanyAllocateRecord.SOURCE_EDIT, new BigDecimal("100"), new BigDecimal("-40"), "编辑白名单调减初始分配额度"); TCompanyAllocateRecord record = captureInserted(); assertEquals("13800000000", record.getUserInfo()); assertEquals("张三", record.getUserName()); assertEquals(new BigDecimal("60.00"), record.getAfterBalance()); assertEquals(new BigDecimal("-40.00"), record.getAmount()); } @Test void recordAllocateSkipsWhenCompanyMissingOrAmountZero() { assertFalse(service.recordAllocate(TCompanyAllocateRecord.USER_TYPE_CARD, "CARD-1", null, null, TCompanyAllocateRecord.SOURCE_CREATE, BigDecimal.ZERO, new BigDecimal("10"), null)); assertFalse(service.recordAllocate(TCompanyAllocateRecord.USER_TYPE_CARD, "CARD-1", null, 8, TCompanyAllocateRecord.SOURCE_CREATE, BigDecimal.ZERO, BigDecimal.ZERO, null)); verify(mapper, never()).insert(any(TCompanyAllocateRecord.class)); } @Test void recordAllocateTruncatesOverlongText() { String overlong = repeat('V', 80); service.recordAllocate(TCompanyAllocateRecord.USER_TYPE_VIN, overlong, overlong, 3, TCompanyAllocateRecord.SOURCE_CREATE, BigDecimal.ZERO, new BigDecimal("1"), repeat('R', 300)); TCompanyAllocateRecord record = captureInserted(); assertEquals(64, record.getUserInfo().length()); assertEquals(64, record.getUserName().length()); assertEquals(255, record.getRemark().length()); } @Test void recordAllocateWithoutLoginKeepsOperatorEmpty() { SecurityContextHolder.remove(); assertTrue(service.recordAllocate(TCompanyAllocateRecord.USER_TYPE_VIN, "LVIN0002", null, 3, TCompanyAllocateRecord.SOURCE_CREATE, BigDecimal.ZERO, new BigDecimal("5"), "新增VIN码初始分配额度")); TCompanyAllocateRecord record = captureInserted(); assertNull(record.getOperatorId()); assertNull(record.getOperatorName()); } @Test void selectRecordPagePassesQueryAndPaging() { Page expected = new Page<>(2, 10); when(mapper.selectRecordPage(any(Page.class), any())).thenReturn(expected); CompanyAllocateRecordQueryDTO query = new CompanyAllocateRecordQueryDTO(); query.setCompanyId(7); query.setPageCurr(2); query.setPageSize(10); IPage result = service.selectRecordPage(query); ArgumentCaptor> pageCaptor = ArgumentCaptor.forClass(Page.class); verify(mapper).selectRecordPage(pageCaptor.capture(), any()); assertEquals(2, pageCaptor.getValue().getCurrent()); assertEquals(10, pageCaptor.getValue().getSize()); assertSame(expected, result); } @Test void selectRecordPageFallsBackToSafeDefaults() { when(mapper.selectRecordPage(any(Page.class), any())).thenReturn(new Page<>()); CompanyAllocateRecordQueryDTO query = new CompanyAllocateRecordQueryDTO(); query.setPageCurr(0); query.setPageSize(0); service.selectRecordPage(null); ArgumentCaptor> firstPage = ArgumentCaptor.forClass(Page.class); verify(mapper).selectRecordPage(firstPage.capture(), any()); assertEquals(1, firstPage.getValue().getCurrent()); assertEquals(50, firstPage.getValue().getSize()); service.selectRecordPage(query); ArgumentCaptor> secondPage = ArgumentCaptor.forClass(Page.class); verify(mapper, times(2)).selectRecordPage(secondPage.capture(), any()); assertEquals(1, secondPage.getAllValues().get(1).getCurrent()); assertEquals(10, secondPage.getAllValues().get(1).getSize()); } private TCompanyAllocateRecord captureInserted() { ArgumentCaptor captor = ArgumentCaptor.forClass(TCompanyAllocateRecord.class); verify(mapper).insert(captor.capture()); return captor.getValue(); } // ============ 账户模式切换回收相关 ============ @Test void sumAllocatedBalanceAggregatesThreeTypes() { TVinManagement vin = new TVinManagement(); vin.setId(1L); vin.setBalance(new BigDecimal("10.50")); vin.setLockedAmount(new BigDecimal("2.00")); TVinManagement vin2 = new TVinManagement(); vin2.setId(2L); vin2.setBalance(new BigDecimal("5.25")); vin2.setLockedAmount(BigDecimal.ZERO); when(vinMapper.selectList(any())).thenReturn(Arrays.asList(vin, vin2)); TChargingCard card = new TChargingCard(); card.setId(3L); card.setBalance(new BigDecimal("8.00")); card.setLockedAmount(new BigDecimal("1.50")); when(cardMapper.selectList(any())).thenReturn(Collections.singletonList(card)); TAppUserWhitelist whitelist = new TAppUserWhitelist(); whitelist.setId(4L); whitelist.setBalance(new BigDecimal("6.00")); whitelist.setLockedAmount(BigDecimal.ZERO); when(whitelistMapper.selectList(any())).thenReturn(Collections.singletonList(whitelist)); CompanyAllocateSummaryVO summary = service.sumAllocatedBalance(9); assertEquals(9, summary.getCompanyId()); assertEquals(new BigDecimal("29.75"), summary.getTotalBalance()); assertEquals(new BigDecimal("3.50"), summary.getTotalLocked()); assertEquals(4, summary.getBalanceUserCount()); } @Test void recycleAllocatedBalanceClearsAndWritesBothSides() throws InterruptedException { when(lock.tryLock(5L, 30L, TimeUnit.SECONDS)).thenReturn(true); when(lock.isHeldByCurrentThread()).thenReturn(true); TVinManagement vin = new TVinManagement(); vin.setId(1L); vin.setBalance(new BigDecimal("10.00")); vin.setVin("LVIN0001"); vin.setPlateNumber("粤A12345"); when(vinMapper.selectList(any())).thenReturn(Collections.singletonList(vin)); when(vinMapper.clearBalance(eq(1L), eq(123L))).thenReturn(1); TChargingCard card = new TChargingCard(); card.setId(2L); card.setBalance(new BigDecimal("20.00")); card.setCardNo("CARD0001"); when(cardMapper.selectList(any())).thenReturn(Collections.singletonList(card)); when(cardMapper.clearBalance(eq(2L), eq(123L))).thenReturn(1); TAppUserWhitelist whitelist = new TAppUserWhitelist(); whitelist.setId(3L); whitelist.setBalance(new BigDecimal("30.00")); whitelist.setPhone("13800000000"); whitelist.setName("张三"); when(whitelistMapper.selectList(any())).thenReturn(Collections.singletonList(whitelist)); when(whitelistMapper.clearBalance(eq(3L), eq("admin"))).thenReturn(1); BigDecimal recycled = service.recycleAllocatedBalance(9, "admin", "测试公司", new BigDecimal("100.00")); assertEquals(new BigDecimal("60.00"), recycled); verify(vinMapper).clearBalance(1L, 123L); verify(cardMapper).clearBalance(2L, 123L); verify(whitelistMapper).clearBalance(3L, "admin"); // 3 条用户负流水 + 1 条企业账户正流水 verify(mapper, times(4)).insert(any(TCompanyAllocateRecord.class)); ArgumentCaptor captor = ArgumentCaptor.forClass(TCompanyAllocateRecord.class); verify(mapper, times(4)).insert(captor.capture()); List records = captor.getAllValues(); TCompanyAllocateRecord companyRecord = records.get(records.size() - 1); assertEquals(TCompanyAllocateRecord.USER_TYPE_COMPANY, companyRecord.getUserType()); assertEquals("测试公司", companyRecord.getUserInfo()); assertEquals(TCompanyAllocateRecord.SOURCE_MODE_SWITCH, companyRecord.getChangeSource()); assertEquals(new BigDecimal("100.00"), companyRecord.getBeforeBalance()); assertEquals(new BigDecimal("60.00"), companyRecord.getAmount()); assertEquals(new BigDecimal("160.00"), companyRecord.getAfterBalance()); for (int i = 0; i < 3; i++) { assertEquals(TCompanyAllocateRecord.SOURCE_MODE_SWITCH, records.get(i).getChangeSource()); assertTrue(records.get(i).getAmount().signum() < 0); } } @Test void recycleAllocatedBalanceIsIdempotentWhenNoBalanceLeft() throws InterruptedException { when(lock.tryLock(5L, 30L, TimeUnit.SECONDS)).thenReturn(true); when(lock.isHeldByCurrentThread()).thenReturn(true); when(vinMapper.selectList(any())).thenReturn(Collections.emptyList()); when(cardMapper.selectList(any())).thenReturn(Collections.emptyList()); when(whitelistMapper.selectList(any())).thenReturn(Collections.emptyList()); BigDecimal recycled = service.recycleAllocatedBalance(9, "admin", "测试公司", new BigDecimal("100.00")); assertEquals(new BigDecimal("0.00"), recycled); verify(mapper, never()).insert(any(TCompanyAllocateRecord.class)); } @Test void recycleAllocatedBalanceThrowsWhenLockBusy() throws InterruptedException { when(lock.tryLock(5L, 30L, TimeUnit.SECONDS)).thenReturn(false); assertThrows(ServiceException.class, () -> service.recycleAllocatedBalance(9, "admin", "测试公司", new BigDecimal("100.00"))); verify(vinMapper, never()).selectList(any()); } @Test void recycleAllocatedBalanceRejectsMissingCompany() { assertThrows(ServiceException.class, () -> service.recycleAllocatedBalance(null, "admin", null, null)); verify(mapper, never()).insert(any(TCompanyAllocateRecord.class)); } private String repeat(char value, int count) { StringBuilder builder = new StringBuilder(count); for (int i = 0; i < count; i++) { builder.append(value); } return builder.toString(); } }