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<TCompanyAllocateRecord> 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<TCompanyAllocateRecord> result = service.selectRecordPage(query);
|
|
ArgumentCaptor<Page<TCompanyAllocateRecord>> 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<Page<TCompanyAllocateRecord>> 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<Page<TCompanyAllocateRecord>> 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<TCompanyAllocateRecord> 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<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++) {
|
builder.append(value);
|
}
|
return builder.toString();
|
}
|
}
|