| | |
| | | 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.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; |
| | |
| | | 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"); |
| | | } |
| | |
| | | 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<TCompanyAllocateRecord> captor = ArgumentCaptor.forClass(TCompanyAllocateRecord.class); |
| | | verify(mapper, times(4)).insert(captor.capture()); |
| | | List<TCompanyAllocateRecord> 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++) { |