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 org.apache.ibatis.builder.MapperBuilderAssistant;
|
import com.ruoyi.account.api.dto.VinManagementAllocateDTO;
|
import com.ruoyi.account.api.dto.VinManagementAllocateItemDTO;
|
import com.ruoyi.account.api.dto.VinManagementExcelDTO;
|
import com.ruoyi.account.api.dto.VinManagementQueryDTO;
|
import com.ruoyi.account.api.dto.VinManagementSaveDTO;
|
import com.ruoyi.account.api.model.TVinManagement;
|
import com.ruoyi.account.api.vo.VinManagementAllocateVO;
|
import com.ruoyi.account.mapper.TVinManagementMapper;
|
import com.ruoyi.common.core.context.SecurityContextHolder;
|
import com.ruoyi.common.core.domain.R;
|
import com.ruoyi.common.core.exception.ServiceException;
|
import com.ruoyi.order.api.feignClient.ChargingOrderClient;
|
import com.ruoyi.order.api.vo.VinChargingStatusVO;
|
import com.ruoyi.other.api.domain.TFleet;
|
import com.ruoyi.other.api.domain.TGroupCompany;
|
import com.ruoyi.other.api.feignClient.GroupCompanyClient;
|
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.Captor;
|
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.anyCollection;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.eq;
|
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.times;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verifyNoInteractions;
|
import static org.mockito.Mockito.when;
|
|
@ExtendWith(MockitoExtension.class)
|
class TVinManagementServiceImplTest {
|
|
private static final Long CURRENT_USER = 123L;
|
|
@Mock
|
private TVinManagementMapper mapper;
|
|
@Mock
|
private ChargingOrderClient chargingOrderClient;
|
|
@Mock
|
private GroupCompanyClient groupCompanyClient;
|
|
@Mock
|
private RedissonClient redissonClient;
|
|
@Mock
|
private RLock quotaLock;
|
|
@Captor
|
private ArgumentCaptor<Page<TVinManagement>> pageCaptor;
|
|
@Captor
|
private ArgumentCaptor<VinManagementQueryDTO> queryCaptor;
|
|
@Captor
|
private ArgumentCaptor<TVinManagement> entityCaptor;
|
|
private TVinManagementServiceImpl service;
|
|
@BeforeAll
|
static void initTableInfo() {
|
// MP 的 removeByIds 会通过 TableInfoHelper.getTableInfo 取表元数据,
|
// 单元测试不启动 Spring,需要手动初始化 TableInfo,否则返回 null 导致 NPE
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TVinManagement.class);
|
}
|
|
@BeforeEach
|
void setUp() throws InterruptedException {
|
service = spy(new TVinManagementServiceImpl());
|
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
ReflectionTestUtils.setField(service, "chargingOrderClient", chargingOrderClient);
|
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(String.valueOf(CURRENT_USER));
|
}
|
|
@AfterEach
|
void tearDown() {
|
SecurityContextHolder.remove();
|
}
|
|
// ---------------- selectPageList ----------------
|
|
@Test
|
void selectPageListWithNullQueryUsesDefaultPagination() {
|
Page<TVinManagement> expected = new Page<>(1, 10);
|
when(mapper.selectPageList(any(Page.class), any())).thenReturn(expected);
|
|
IPage<TVinManagement> result = service.selectPageList(null);
|
|
verify(mapper).selectPageList(pageCaptor.capture(), queryCaptor.capture());
|
assertEquals(1, pageCaptor.getValue().getCurrent());
|
assertEquals(50, pageCaptor.getValue().getSize()); // BasePage 默认 pageSize=50
|
assertNotNull(queryCaptor.getValue());
|
assertSame(expected, result);
|
}
|
|
@Test
|
void selectPageListWithInvalidParamsFallsBackToDefaults() {
|
VinManagementQueryDTO query = new VinManagementQueryDTO();
|
query.setPageCurr(0);
|
query.setPageSize(-1);
|
when(mapper.selectPageList(any(Page.class), any())).thenReturn(new Page<>());
|
|
service.selectPageList(query);
|
|
verify(mapper).selectPageList(pageCaptor.capture(), any());
|
assertEquals(1, pageCaptor.getValue().getCurrent());
|
assertEquals(10, pageCaptor.getValue().getSize());
|
}
|
|
@Test
|
void selectPageListPassesQueryAndPagination() {
|
VinManagementQueryDTO query = new VinManagementQueryDTO();
|
query.setPageCurr(3);
|
query.setPageSize(20);
|
query.setVin("LSV");
|
Page<TVinManagement> expected = new Page<>(3, 20);
|
expected.setRecords(Collections.singletonList(new TVinManagement()));
|
when(mapper.selectPageList(any(Page.class), any())).thenReturn(expected);
|
|
IPage<TVinManagement> result = service.selectPageList(query);
|
|
verify(mapper).selectPageList(pageCaptor.capture(), queryCaptor.capture());
|
assertEquals(3, pageCaptor.getValue().getCurrent());
|
assertEquals(20, pageCaptor.getValue().getSize());
|
assertSame(query, queryCaptor.getValue());
|
assertEquals(1, result.getRecords().size());
|
}
|
|
// ---------------- saveVin ----------------
|
|
@Test
|
void saveVinSucceedsAndSetsDefaults() {
|
when(mapper.countByVin(anyString(), isNull())).thenReturn(0);
|
when(mapper.insert(any(TVinManagement.class))).thenReturn(1);
|
|
VinManagementSaveDTO dto = new VinManagementSaveDTO();
|
dto.setVin(" LSVAM4187C2184842 ");
|
dto.setPlateNumber(" 川A12345 ");
|
dto.setPhone("13800138000");
|
dto.setCompanyName(" 测试公司 ");
|
|
boolean result = service.saveVin(dto);
|
|
assertTrue(result);
|
verify(mapper).insert(entityCaptor.capture());
|
TVinManagement saved = entityCaptor.getValue();
|
assertEquals("LSVAM4187C2184842", saved.getVin());
|
assertEquals("川A12345", saved.getPlateNumber());
|
assertEquals("13800138000", saved.getPhone());
|
assertEquals("测试公司", saved.getCompanyName());
|
assertNull(saved.getRemark());
|
assertEquals(Integer.valueOf(1), saved.getStatus());
|
assertEquals(Integer.valueOf(0), saved.getDelFlag());
|
assertNotNull(saved.getCreateTime());
|
assertEquals(CURRENT_USER, saved.getCreateBy());
|
}
|
|
@Test
|
void saveVinRejectsNullVin() {
|
VinManagementSaveDTO dto = new VinManagementSaveDTO();
|
dto.setVin(null);
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.saveVin(dto));
|
|
assertEquals("VIN码不能为空", ex.getMessage());
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void saveVinRejectsTooLongVin() {
|
VinManagementSaveDTO dto = new VinManagementSaveDTO();
|
dto.setVin(longVin(65));
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.saveVin(dto));
|
|
assertEquals("VIN码不能超过64个字符", ex.getMessage());
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void saveVinRejectsInvalidStatus() {
|
VinManagementSaveDTO dto = new VinManagementSaveDTO();
|
dto.setVin("LSVAM4187C2184842");
|
dto.setStatus(3);
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.saveVin(dto));
|
|
assertEquals("VIN码状态参数错误", ex.getMessage());
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void saveVinRejectsDuplicateVin() {
|
when(mapper.countByVin(anyString(), isNull())).thenReturn(1);
|
|
VinManagementSaveDTO dto = new VinManagementSaveDTO();
|
dto.setVin("LSVAM4187C2184842");
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.saveVin(dto));
|
|
assertEquals("VIN码已存在", ex.getMessage());
|
verify(mapper, never()).insert(any());
|
}
|
|
// ---------------- updateVin ----------------
|
|
@Test
|
void updateVinSucceedsAndSetsAuditFields() {
|
when(mapper.selectById(9L)).thenReturn(new TVinManagement());
|
when(mapper.countByVin(anyString(), eq(9L))).thenReturn(0);
|
// 记录更新走 lambdaUpdate(显式 set 保证公司/车队/额度可被清空),最终调用 mapper.update(entity=null, wrapper)
|
when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
|
|
VinManagementSaveDTO dto = new VinManagementSaveDTO();
|
dto.setId(9L);
|
dto.setVin("LSVAM4187C2184842");
|
dto.setPlateNumber("川B88888");
|
dto.setStatus(2);
|
|
boolean result = service.updateVin(dto);
|
|
assertTrue(result);
|
ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
|
verify(mapper).update(isNull(), wrapperCaptor.capture());
|
String sqlSet = wrapperCaptor.getValue().getSqlSet();
|
assertTrue(sqlSet.contains("vin="));
|
assertTrue(sqlSet.contains("plate_number="));
|
assertTrue(sqlSet.contains("status="));
|
assertTrue(sqlSet.contains("update_time="));
|
assertTrue(sqlSet.contains("update_by="));
|
// 备注已从表单下线,编辑不再写入 remark,历史备注不会被清空
|
assertFalse(sqlSet.contains("remark="));
|
}
|
|
@Test
|
void updateVinRejectsMissingId() {
|
VinManagementSaveDTO dto = new VinManagementSaveDTO();
|
dto.setVin("LSVAM4187C2184842");
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.updateVin(dto));
|
|
assertEquals("VIN码ID不能为空", ex.getMessage());
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void updateVinRejectsNotExist() {
|
when(mapper.selectById(9L)).thenReturn(null);
|
|
VinManagementSaveDTO dto = new VinManagementSaveDTO();
|
dto.setId(9L);
|
dto.setVin("LSVAM4187C2184842");
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.updateVin(dto));
|
|
assertEquals("VIN码不存在或已删除", ex.getMessage());
|
verify(mapper, never()).updateById(any());
|
}
|
|
@Test
|
void updateVinRejectsDuplicateVin() {
|
// validate 中 countByVin 命中重复即抛异常,selectById 不会被执行
|
when(mapper.countByVin(anyString(), eq(9L))).thenReturn(1);
|
|
VinManagementSaveDTO dto = new VinManagementSaveDTO();
|
dto.setId(9L);
|
dto.setVin("LSVAM4187C2184842");
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.updateVin(dto));
|
|
assertEquals("VIN码已存在", ex.getMessage());
|
verify(mapper, never()).updateById(any());
|
}
|
|
// ---------------- deleteVins ----------------
|
|
@Test
|
void deleteVinsRejectsNullOrEmptyIds() {
|
ServiceException ex1 = assertThrows(ServiceException.class, () -> service.deleteVins(null));
|
assertEquals("请选择需要删除的VIN码", ex1.getMessage());
|
|
ServiceException ex2 = assertThrows(ServiceException.class, () -> service.deleteVins(Collections.emptyList()));
|
assertEquals("请选择需要删除的VIN码", ex2.getMessage());
|
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void deleteVinsRejectsMissingRecords() {
|
when(mapper.selectBatchIds(anyCollection())).thenReturn(Collections.singletonList(new TVinManagement()));
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.deleteVins(Arrays.asList(1L, 2L)));
|
|
assertEquals("部分VIN码不存在或已删除", ex.getMessage());
|
verify(mapper, never()).deleteBatchIds(any());
|
}
|
|
@Test
|
void deleteVinsSucceedsWithDistinctIds() {
|
when(mapper.selectBatchIds(anyCollection()))
|
.thenReturn(Arrays.asList(new TVinManagement(), new TVinManagement()));
|
when(mapper.deleteBatchIds(anyCollection())).thenReturn(1);
|
|
boolean result = service.deleteVins(Arrays.asList(1L, 2L, 2L));
|
|
assertTrue(result);
|
@SuppressWarnings("unchecked")
|
ArgumentCaptor<List<Long>> idsCaptor = ArgumentCaptor.forClass(List.class);
|
verify(mapper).deleteBatchIds(idsCaptor.capture());
|
assertEquals(2, idsCaptor.getValue().size());
|
assertEquals(Long.valueOf(1L), idsCaptor.getValue().get(0));
|
assertEquals(Long.valueOf(2L), idsCaptor.getValue().get(1));
|
}
|
|
// ---------------- unbindFleet ----------------
|
|
@Test
|
void unbindFleetRejectsNullOrEmptyIds() {
|
ServiceException ex1 = assertThrows(ServiceException.class, () -> service.unbindFleet(null));
|
assertEquals("请选择需要解绑车队的VIN码", ex1.getMessage());
|
|
ServiceException ex2 = assertThrows(ServiceException.class, () -> service.unbindFleet(Collections.emptyList()));
|
assertEquals("请选择需要解绑车队的VIN码", ex2.getMessage());
|
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void unbindFleetRejectsMissingRecords() {
|
TVinManagement existing = new TVinManagement();
|
existing.setId(1L);
|
when(mapper.selectBatchIds(anyCollection())).thenReturn(Collections.singletonList(existing));
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.unbindFleet(Arrays.asList(1L, 2L)));
|
|
assertEquals("部分VIN码不存在或已删除", ex.getMessage());
|
verify(mapper, never()).update(isNull(), any(Wrapper.class));
|
}
|
|
@Test
|
void unbindFleetClearsFleetIdAndIgnoresUnboundRecords() {
|
TVinManagement bound = new TVinManagement();
|
bound.setId(1L);
|
bound.setFleetId(5);
|
TVinManagement unbound = new TVinManagement();
|
unbound.setId(2L);
|
when(mapper.selectBatchIds(anyCollection())).thenReturn(Arrays.asList(bound, unbound));
|
when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
|
|
int result = service.unbindFleet(Arrays.asList(1L, 2L, 2L, null));
|
|
assertEquals(1, result);
|
@SuppressWarnings("unchecked")
|
ArgumentCaptor<List<Long>> idsCaptor = ArgumentCaptor.forClass(List.class);
|
verify(mapper).selectBatchIds(idsCaptor.capture());
|
assertEquals(Arrays.asList(1L, 2L), idsCaptor.getValue());
|
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="));
|
}
|
|
// ---------------- updateStatus ----------------
|
|
@Test
|
void updateStatusRejectsNullId() {
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.updateStatus(null, 1));
|
|
assertEquals("VIN码状态参数错误", ex.getMessage());
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void updateStatusRejectsInvalidStatus() {
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.updateStatus(9L, 0));
|
|
assertEquals("VIN码状态参数错误", ex.getMessage());
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void updateStatusRejectsNotExist() {
|
when(mapper.selectById(9L)).thenReturn(null);
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> service.updateStatus(9L, 2));
|
|
assertEquals("VIN码不存在或已删除", ex.getMessage());
|
verify(mapper, never()).updateById(any());
|
}
|
|
@Test
|
void updateStatusSucceeds() {
|
when(mapper.selectById(9L)).thenReturn(new TVinManagement());
|
when(mapper.updateById(any(TVinManagement.class))).thenReturn(1);
|
|
boolean result = service.updateStatus(9L, 2);
|
|
assertTrue(result);
|
verify(mapper).updateById(entityCaptor.capture());
|
TVinManagement updated = entityCaptor.getValue();
|
assertEquals(Long.valueOf(9L), updated.getId());
|
assertEquals(Integer.valueOf(2), updated.getStatus());
|
assertNotNull(updated.getUpdateTime());
|
assertEquals(CURRENT_USER, updated.getUpdateBy());
|
}
|
|
// ---------------- selectExportList ----------------
|
|
@Test
|
void selectExportListReturnsMapperResult() {
|
VinManagementExcelDTO item = new VinManagementExcelDTO();
|
item.setVin("LSVAM4187C2184842");
|
when(mapper.selectExportList(any())).thenReturn(Collections.singletonList(item));
|
|
VinManagementQueryDTO query = new VinManagementQueryDTO();
|
query.setStatus(1);
|
|
List<VinManagementExcelDTO> result = service.selectExportList(query);
|
|
assertEquals(1, result.size());
|
assertEquals("LSVAM4187C2184842", result.get(0).getVin());
|
verify(mapper).selectExportList(queryCaptor.capture());
|
assertSame(query, queryCaptor.getValue());
|
}
|
|
@Test
|
void selectExportListWithNullQueryUsesEmptyDto() {
|
when(mapper.selectExportList(any())).thenReturn(Collections.emptyList());
|
|
List<VinManagementExcelDTO> result = service.selectExportList(null);
|
|
assertTrue(result.isEmpty());
|
verify(mapper).selectExportList(queryCaptor.capture());
|
assertNotNull(queryCaptor.getValue());
|
}
|
|
// ---------------- 账户信息回填(列表/导出) ----------------
|
|
@Test
|
void selectPageListFillsSharedAccountInfoFromCompanyBalance() {
|
TVinManagement record = new TVinManagement();
|
record.setId(1L);
|
record.setCompanyId(8);
|
record.setBalance(new BigDecimal("5.00"));
|
Page<TVinManagement> expected = new Page<>(1, 10);
|
expected.setRecords(Collections.singletonList(record));
|
when(mapper.selectPageList(any(Page.class), any())).thenReturn(expected);
|
TGroupCompany company = new TGroupCompany();
|
company.setId(8);
|
company.setAccountMode(1);
|
company.setAvailableBalance(new BigDecimal("88.60"));
|
when(groupCompanyClient.listCompaniesByIds(Collections.singletonList(8)))
|
.thenReturn(R.ok(Collections.singletonList(company)));
|
|
IPage<TVinManagement> result = service.selectPageList(new VinManagementQueryDTO());
|
|
TVinManagement filled = result.getRecords().get(0);
|
assertEquals(Integer.valueOf(1), filled.getAccountMode());
|
assertEquals(new BigDecimal("88.60"), filled.getAvailableBalance());
|
assertNull(filled.getOverrun());
|
assertEquals(Integer.valueOf(1), filled.getAccountStatus());
|
}
|
|
@Test
|
void selectPageListUsesOwnBalanceAndFlagsOverrunForAllocationMode() {
|
TVinManagement record = new TVinManagement();
|
record.setId(2L);
|
record.setCompanyId(9);
|
record.setBalance(new BigDecimal("-12.345"));
|
Page<TVinManagement> expected = new Page<>(1, 10);
|
expected.setRecords(Collections.singletonList(record));
|
when(mapper.selectPageList(any(Page.class), any())).thenReturn(expected);
|
TGroupCompany company = new TGroupCompany();
|
company.setId(9);
|
company.setAccountMode(2);
|
company.setAvailableBalance(new BigDecimal("1000.00"));
|
when(groupCompanyClient.listCompaniesByIds(Collections.singletonList(9)))
|
.thenReturn(R.ok(Collections.singletonList(company)));
|
|
IPage<TVinManagement> result = service.selectPageList(new VinManagementQueryDTO());
|
|
TVinManagement filled = result.getRecords().get(0);
|
assertEquals(Integer.valueOf(2), filled.getAccountMode());
|
assertEquals(new BigDecimal("-12.345"), filled.getAvailableBalance());
|
assertEquals(new BigDecimal("12.35"), filled.getOverrun());
|
assertEquals(Integer.valueOf(2), filled.getAccountStatus());
|
}
|
|
@Test
|
void selectPageListLeavesAccountFieldsEmptyWhenCompanyLookupFails() {
|
TVinManagement record = new TVinManagement();
|
record.setId(3L);
|
record.setCompanyId(10);
|
record.setBalance(new BigDecimal("1.00"));
|
Page<TVinManagement> expected = new Page<>(1, 10);
|
expected.setRecords(Collections.singletonList(record));
|
when(mapper.selectPageList(any(Page.class), any())).thenReturn(expected);
|
when(groupCompanyClient.listCompaniesByIds(Collections.singletonList(10)))
|
.thenReturn(R.fail("other service unavailable"));
|
|
IPage<TVinManagement> result = service.selectPageList(new VinManagementQueryDTO());
|
|
TVinManagement filled = result.getRecords().get(0);
|
assertNull(filled.getAccountMode());
|
assertNull(filled.getAvailableBalance());
|
assertNull(filled.getOverrun());
|
assertNull(filled.getAccountStatus());
|
}
|
|
@Test
|
void selectExportListFillsFleetNameAndAccountInfo() {
|
VinManagementExcelDTO item = new VinManagementExcelDTO();
|
item.setVin("LSVAM4187C2184842");
|
item.setCompanyId(8);
|
item.setFleetId(3);
|
item.setBalance(new BigDecimal("7.00"));
|
when(mapper.selectExportList(any())).thenReturn(Collections.singletonList(item));
|
TFleet fleet = new TFleet();
|
fleet.setId(3);
|
fleet.setFleetName("测试车队");
|
when(groupCompanyClient.listFleetsByIds(Collections.singletonList(3)))
|
.thenReturn(R.ok(Collections.singletonList(fleet)));
|
TGroupCompany company = new TGroupCompany();
|
company.setId(8);
|
company.setAccountMode(1);
|
company.setAvailableBalance(new BigDecimal("66.00"));
|
when(groupCompanyClient.listCompaniesByIds(Collections.singletonList(8)))
|
.thenReturn(R.ok(Collections.singletonList(company)));
|
|
List<VinManagementExcelDTO> result = service.selectExportList(new VinManagementQueryDTO());
|
|
assertEquals("测试车队", result.get(0).getFleetName());
|
assertEquals(Integer.valueOf(1), result.get(0).getAccountMode());
|
assertEquals(new BigDecimal("66.00"), result.get(0).getAvailableBalance());
|
assertNull(result.get(0).getOverrun());
|
assertEquals(Integer.valueOf(1), result.get(0).getAccountStatus());
|
}
|
|
@Test
|
void selectExportListKeepsRowsWhenRemoteLookupsFail() {
|
VinManagementExcelDTO item = new VinManagementExcelDTO();
|
item.setVin("LSVAM4187C2184842");
|
item.setCompanyId(8);
|
item.setFleetId(3);
|
when(mapper.selectExportList(any())).thenReturn(Collections.singletonList(item));
|
when(groupCompanyClient.listFleetsByIds(Collections.singletonList(3)))
|
.thenThrow(new ServiceException("fleet service unavailable"));
|
when(groupCompanyClient.listCompaniesByIds(Collections.singletonList(8)))
|
.thenThrow(new ServiceException("company service unavailable"));
|
|
List<VinManagementExcelDTO> result = service.selectExportList(new VinManagementQueryDTO());
|
|
assertEquals(1, result.size());
|
assertNull(result.get(0).getFleetName());
|
assertNull(result.get(0).getAccountMode());
|
assertNull(result.get(0).getAvailableBalance());
|
}
|
|
// ---------------- importVins ----------------
|
|
@Test
|
void importVinsRejectsNullOrEmptyItems() {
|
ServiceException ex1 = assertThrows(ServiceException.class, () -> service.importVins(null));
|
assertEquals("导入数据不能为空", ex1.getMessage());
|
|
ServiceException ex2 = assertThrows(ServiceException.class, () -> service.importVins(Collections.emptyList()));
|
assertEquals("导入数据不能为空", ex2.getMessage());
|
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void importVinsRejectsEmptyVinAtRow() {
|
VinManagementExcelDTO item = new VinManagementExcelDTO();
|
item.setVin("");
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> service.importVins(Collections.singletonList(item)));
|
|
assertEquals("第2行VIN码不能为空", ex.getMessage());
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void importVinsRejectsTooLongVinAtRow() {
|
VinManagementExcelDTO item = new VinManagementExcelDTO();
|
item.setVin(longVin(65));
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> service.importVins(Collections.singletonList(item)));
|
|
assertEquals("第2行VIN码不能超过64个字符", ex.getMessage());
|
verifyNoInteractions(mapper);
|
}
|
|
@Test
|
void importVinsRejectsDuplicateVinWithinFile() {
|
VinManagementExcelDTO first = new VinManagementExcelDTO();
|
first.setVin("LSVAM4187C2184842");
|
VinManagementExcelDTO second = new VinManagementExcelDTO();
|
second.setVin("LSVAM4187C2184842");
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> service.importVins(Arrays.asList(first, second)));
|
|
assertEquals("第3行VIN码已存在或重复", ex.getMessage());
|
// 第一行校验时已调用过 countByVin,仅断言不执行 insert
|
verify(mapper, never()).insert(any());
|
}
|
|
@Test
|
void importVinsRejectsVinExistingInDb() {
|
when(mapper.countByVin(anyString(), isNull())).thenReturn(1);
|
|
VinManagementExcelDTO item = new VinManagementExcelDTO();
|
item.setVin("LSVAM4187C2184842");
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> service.importVins(Collections.singletonList(item)));
|
|
assertEquals("第2行VIN码已存在或重复", ex.getMessage());
|
verify(mapper, never()).insert(any());
|
}
|
|
@Test
|
void importVinsRejectsInvalidStatusAtRow() {
|
VinManagementExcelDTO item = new VinManagementExcelDTO();
|
item.setVin("LSVAM4187C2184842");
|
item.setStatus("3");
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> service.importVins(Collections.singletonList(item)));
|
|
assertEquals("第2行启用状态错误", ex.getMessage());
|
verify(mapper, never()).insert(any());
|
}
|
|
@Test
|
void importVinsSucceedsAndParsesStatus() {
|
when(mapper.countByVin(anyString(), isNull())).thenReturn(0);
|
when(mapper.insert(any(TVinManagement.class))).thenReturn(1);
|
|
VinManagementExcelDTO first = new VinManagementExcelDTO();
|
first.setVin(" LSVAM4187C2184842 ");
|
first.setPlateNumber(" 川A12345 ");
|
first.setPhone("13800138000");
|
first.setCompanyName(" 公司A ");
|
first.setStatus("启用");
|
|
VinManagementExcelDTO second = new VinManagementExcelDTO();
|
second.setVin("LSVAM4187C2184843");
|
second.setStatus("2");
|
|
VinManagementExcelDTO third = new VinManagementExcelDTO();
|
third.setVin("LSVAM4187C2184844");
|
third.setStatus(null);
|
|
String result = service.importVins(Arrays.asList(first, second, third));
|
|
assertEquals("成功导入3条VIN码数据", result);
|
verify(mapper, times(3)).insert(entityCaptor.capture());
|
List<TVinManagement> saved = entityCaptor.getAllValues();
|
assertEquals("LSVAM4187C2184842", saved.get(0).getVin());
|
assertEquals("川A12345", saved.get(0).getPlateNumber());
|
assertEquals("公司A", saved.get(0).getCompanyName());
|
assertNull(saved.get(0).getRemark());
|
assertEquals(Integer.valueOf(1), saved.get(0).getStatus());
|
assertEquals(Integer.valueOf(2), saved.get(1).getStatus());
|
assertEquals(Integer.valueOf(1), saved.get(2).getStatus());
|
assertNotNull(saved.get(0).getCreateTime());
|
assertEquals(CURRENT_USER, saved.get(0).getCreateBy());
|
}
|
|
@Test
|
void selectAppletVehicleListFillsChargingStatusInOneBatch() {
|
TVinManagement chargingVehicle = new TVinManagement();
|
chargingVehicle.setId(11L);
|
TVinManagement idleVehicle = new TVinManagement();
|
idleVehicle.setId(22L);
|
when(mapper.selectAppletVehicleList(88L)).thenReturn(Arrays.asList(chargingVehicle, idleVehicle));
|
VinChargingStatusVO status = new VinChargingStatusVO();
|
status.setVinId(11L);
|
status.setChargingGunId(33);
|
when(chargingOrderClient.getChargingVinStatus(Arrays.asList(11L, 22L)))
|
.thenReturn(R.ok(Collections.singletonList(status)));
|
|
List<TVinManagement> result = service.selectAppletVehicleList(88L);
|
|
assertEquals(Boolean.TRUE, result.get(0).getCharging());
|
assertEquals(Integer.valueOf(33), result.get(0).getChargingGunId());
|
assertEquals(Boolean.FALSE, result.get(1).getCharging());
|
verify(chargingOrderClient).getChargingVinStatus(Arrays.asList(11L, 22L));
|
}
|
|
@Test
|
void selectAppletVehicleListTreatsFeignFailureAsIdle() {
|
TVinManagement vehicle = new TVinManagement();
|
vehicle.setId(11L);
|
when(mapper.selectAppletVehicleList(88L)).thenReturn(Collections.singletonList(vehicle));
|
when(chargingOrderClient.getChargingVinStatus(Collections.singletonList(11L)))
|
.thenReturn(R.fail("order unavailable"));
|
|
List<TVinManagement> result = service.selectAppletVehicleList(88L);
|
|
assertEquals(Boolean.FALSE, result.get(0).getCharging());
|
assertEquals(null, result.get(0).getChargingGunId());
|
}
|
|
@Test
|
void selectAppletVehicleListTreatsFeignExceptionAsIdle() {
|
TVinManagement vehicle = new TVinManagement();
|
vehicle.setId(11L);
|
when(mapper.selectAppletVehicleList(88L)).thenReturn(Collections.singletonList(vehicle));
|
when(chargingOrderClient.getChargingVinStatus(Collections.singletonList(11L)))
|
.thenThrow(new RuntimeException("timeout"));
|
|
assertEquals(Boolean.FALSE, service.selectAppletVehicleList(88L).get(0).getCharging());
|
}
|
|
@Test
|
void bindAppletVehicleByVinUsesAtomicConditionalUpdate() {
|
TVinManagement vehicle = new TVinManagement();
|
vehicle.setId(9L);
|
vehicle.setVin("LSVAM4187C2184842");
|
when(mapper.selectEnabledVehicle("LSVAM4187C2184842")).thenReturn(vehicle);
|
when(mapper.bindAppletVehicle(9L, 88L)).thenReturn(1);
|
assertTrue(service.bindAppletVehicle(88L, " LSVAM4187C2184842 "));
|
verify(mapper).bindAppletVehicle(9L, 88L);
|
}
|
|
@Test
|
void bindAppletVehicleRejectsMissingDisabledOrDeletedVehicle() {
|
when(mapper.selectEnabledVehicle("UNKNOWN")).thenReturn(null);
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> service.bindAppletVehicle(88L, "UNKNOWN"));
|
assertEquals("该车辆不存在", ex.getMessage());
|
}
|
|
@Test
|
void bindAppletVehicleRejectsVehicleAlreadyBoundToCurrentUser() {
|
TVinManagement vehicle = new TVinManagement();
|
vehicle.setId(9L);
|
vehicle.setAppUserId(88L);
|
when(mapper.selectEnabledVehicle("川A12345")).thenReturn(vehicle);
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> service.bindAppletVehicle(88L, "川A12345"));
|
assertEquals("该车辆已绑定当前用户", ex.getMessage());
|
verify(mapper, never()).bindAppletVehicle(any(), any());
|
}
|
|
@Test
|
void bindAppletVehicleDoesNotOverwriteOtherUser() {
|
TVinManagement vehicle = new TVinManagement();
|
vehicle.setId(9L);
|
vehicle.setAppUserId(99L);
|
when(mapper.selectEnabledVehicle("川A12345")).thenReturn(vehicle);
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> service.bindAppletVehicle(88L, "川A12345"));
|
assertEquals("该车辆已被其他用户绑定", ex.getMessage());
|
verify(mapper, never()).bindAppletVehicle(any(), any());
|
}
|
|
@Test
|
void unbindAppletVehicleRequiresIdAndOwner() {
|
when(mapper.unbindAppletVehicle(9L, 88L)).thenReturn(1);
|
assertTrue(service.unbindAppletVehicle(9L, 88L));
|
when(mapper.unbindAppletVehicle(10L, 88L)).thenReturn(0);
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> service.unbindAppletVehicle(10L, 88L));
|
assertEquals("该车辆不存在", ex.getMessage());
|
}
|
|
@Test
|
void getVinIdsByAppUserIdReturnsMapperResult() {
|
when(mapper.selectIdsByAppUserId(88L)).thenReturn(Arrays.asList(1L, 2L));
|
assertEquals(Arrays.asList(1L, 2L), service.getVinIdsByAppUserId(88L));
|
}
|
|
// ---------------- getAllocateOverview / allocateQuota ----------------
|
|
@Test
|
void getAllocateOverviewReturnsCompanyBalances() {
|
when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
|
|
VinManagementAllocateVO vo = service.getAllocateOverview(9);
|
|
assertEquals(Integer.valueOf(9), vo.getCompanyId());
|
assertEquals("测试公司", vo.getCompanyName());
|
assertEquals(new BigDecimal("1000.00"), vo.getAvailableBalance());
|
assertEquals(new BigDecimal("5000.00"), vo.getTotalRechargeAmount());
|
}
|
|
@Test
|
void getAllocateOverviewRejectsMissingCompanyId() {
|
ServiceException exception = assertThrows(ServiceException.class,
|
() -> service.getAllocateOverview(null));
|
|
assertEquals("请选择公司", exception.getMessage());
|
verifyNoInteractions(groupCompanyClient);
|
}
|
|
@Test
|
void allocateQuotaAccumulatesVinQuotaAndDeductsCompanyBalance() {
|
when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
|
doReturn(Arrays.asList(allocateVehicle(1L, 9, "VIN-1", "10.00"), allocateVehicle(2L, 9, "VIN-2", "20.00")))
|
.when(service).listByIds(anyCollection());
|
when(groupCompanyClient.deductBalance(9, new BigDecimal("30.00"))).thenReturn(R.ok(new BigDecimal("970.00")));
|
when(mapper.addBalance(1L, new BigDecimal("10.00"), CURRENT_USER)).thenReturn(1);
|
when(mapper.addBalance(2L, new BigDecimal("20.00"), CURRENT_USER)).thenReturn(1);
|
|
VinManagementAllocateVO vo = service.allocateQuota(allocateDto(9,
|
allocateItem(1L, "10.00"), allocateItem(2L, "20.00")));
|
|
assertEquals(new BigDecimal("970.00"), vo.getAvailableBalance());
|
assertEquals("测试公司", vo.getCompanyName());
|
verify(mapper).addBalance(1L, new BigDecimal("10.00"), CURRENT_USER);
|
verify(mapper).addBalance(2L, new BigDecimal("20.00"), CURRENT_USER);
|
verify(redissonClient).getLock("VIN_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 allocateQuotaRejectsVehicleOfOtherCompany() {
|
when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
|
doReturn(Collections.singletonList(allocateVehicle(1L, 8, "VIN-1", "0.00")))
|
.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 allocateQuotaRejectsDisabledVehicle() {
|
when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
|
TVinManagement vehicle = allocateVehicle(1L, 9, "VIN-1", "0.00");
|
vehicle.setStatus(2);
|
doReturn(Collections.singletonList(vehicle)).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 allocateQuotaRejectsQuotaOverflow() {
|
when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
|
doReturn(Collections.singletonList(allocateVehicle(1L, 9, "VIN-1", "99999999.00")))
|
.when(service).listByIds(anyCollection());
|
|
ServiceException exception = assertThrows(ServiceException.class,
|
() -> service.allocateQuota(allocateDto(9, allocateItem(1L, "100.00"))));
|
|
assertTrue(exception.getMessage().contains("超过上限"));
|
verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class));
|
}
|
|
@Test
|
void allocateQuotaRefundsCompanyBalanceWhenQuotaUpdateFails() {
|
when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
|
doReturn(Collections.singletonList(allocateVehicle(1L, 9, "VIN-1", "10.00")))
|
.when(service).listByIds(anyCollection());
|
when(groupCompanyClient.deductBalance(9, new BigDecimal("10.00"))).thenReturn(R.ok(new BigDecimal("990.00")));
|
when(mapper.addBalance(1L, new BigDecimal("10.00"), CURRENT_USER)).thenReturn(0);
|
when(groupCompanyClient.refundBalance(9, new BigDecimal("10.00"))).thenReturn(R.ok(new BigDecimal("1000.00")));
|
|
ServiceException exception = assertThrows(ServiceException.class,
|
() -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00"))));
|
|
assertEquals("VIN码【VIN-1】额度分配失败,请重试", exception.getMessage());
|
verify(groupCompanyClient).refundBalance(9, new BigDecimal("10.00"));
|
}
|
|
private TGroupCompany allocationCompany(Integer id, String name) {
|
TGroupCompany company = new TGroupCompany();
|
company.setId(id);
|
company.setName(name);
|
company.setAccountMode(2);
|
company.setStatus(1);
|
company.setAvailableBalance(new BigDecimal("1000.00"));
|
company.setTotalRechargeAmount(new BigDecimal("5000.00"));
|
return company;
|
}
|
|
private TVinManagement allocateVehicle(Long id, Integer companyId, String vin, String balance) {
|
TVinManagement vehicle = new TVinManagement();
|
vehicle.setId(id);
|
vehicle.setCompanyId(companyId);
|
vehicle.setVin(vin);
|
vehicle.setBalance(new BigDecimal(balance));
|
vehicle.setStatus(1);
|
return vehicle;
|
}
|
|
private VinManagementAllocateItemDTO allocateItem(Long vinId, String amount) {
|
VinManagementAllocateItemDTO item = new VinManagementAllocateItemDTO();
|
item.setVinId(vinId);
|
item.setAmount(new BigDecimal(amount));
|
return item;
|
}
|
|
private VinManagementAllocateDTO allocateDto(Integer companyId, VinManagementAllocateItemDTO... items) {
|
VinManagementAllocateDTO dto = new VinManagementAllocateDTO();
|
dto.setCompanyId(companyId);
|
dto.setItems(Arrays.asList(items));
|
return dto;
|
}
|
|
private String longVin(int length) {
|
StringBuilder sb = new StringBuilder();
|
for (int i = 0; i < length; i++) {
|
sb.append('L');
|
}
|
return sb.toString();
|
}
|
}
|