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.account.service.PartnerAppUserAccessService; 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.doThrow; 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; @Mock private PartnerAppUserAccessService partnerAppUserAccessService; @Captor private ArgumentCaptor> pageCaptor; @Captor private ArgumentCaptor queryCaptor; @Captor private ArgumentCaptor 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); ReflectionTestUtils.setField(service, "partnerAppUserAccessService", partnerAppUserAccessService); // 默认透传(模拟非公司账号),公司账号隔离场景由具体用例覆盖打桩 lenient().when(partnerAppUserAccessService.resolveCompanyFilter(any())).thenAnswer(invocation -> invocation.getArgument(0)); lenient().when(partnerAppUserAccessService.getCurrentCompanyId()).thenReturn(null); 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 expected = new Page<>(1, 10); when(mapper.selectPageList(any(Page.class), any())).thenReturn(expected); IPage 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 expected = new Page<>(3, 20); expected.setRecords(Collections.singletonList(new TVinManagement())); when(mapper.selectPageList(any(Page.class), any())).thenReturn(expected); IPage 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 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> 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> idsCaptor = ArgumentCaptor.forClass(List.class); verify(mapper).selectBatchIds(idsCaptor.capture()); assertEquals(Arrays.asList(1L, 2L), idsCaptor.getValue()); ArgumentCaptor 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 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 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 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 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 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 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 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 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 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 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 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 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 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(); } // ---------------- 公司账号数据隔离 ---------------- @Test void selectPageListForcesCompanyFilterForCompanyAccount() { // 公司账号:前端传入的公司筛选被强制覆盖为本公司 when(partnerAppUserAccessService.resolveCompanyFilter(any())).thenReturn(2); VinManagementQueryDTO query = new VinManagementQueryDTO(); query.setCompanyId(9); query.setVin("LSV"); when(mapper.selectPageList(any(Page.class), any())).thenReturn(new Page<>(1, 10)); service.selectPageList(query); verify(mapper).selectPageList(pageCaptor.capture(), queryCaptor.capture()); assertEquals(Integer.valueOf(2), queryCaptor.getValue().getCompanyId()); } @Test void updateVinRejectsCrossCompanyRecord() { // 公司账号:编辑其他公司的 VIN 被越权校验拦截 TVinManagement old = new TVinManagement(); old.setId(5L); old.setCompanyId(9); when(mapper.selectById(5L)).thenReturn(old); doThrow(new ServiceException("无权操作其他公司的数据")).when(partnerAppUserAccessService).assertCompanyVisible(9); VinManagementSaveDTO dto = new VinManagementSaveDTO(); dto.setId(5L); dto.setVin("LSVAM4187C2184842"); ServiceException ex = assertThrows(ServiceException.class, () -> service.updateVin(dto)); assertEquals("无权操作其他公司的数据", ex.getMessage()); verify(mapper, never()).updateById(any(TVinManagement.class)); } @Test void saveVinForcesCompanyAccountOwnership() { // 公司账号:新增 VIN 强制归属本公司 when(partnerAppUserAccessService.resolveCompanyFilter(any())).thenReturn(2); when(mapper.countByVin(anyString(), isNull())).thenReturn(0); when(mapper.insert(any(TVinManagement.class))).thenReturn(1); when(groupCompanyClient.getCompanyById(2)).thenReturn(R.ok(allocationCompany(2, "本公司"))); VinManagementSaveDTO dto = new VinManagementSaveDTO(); dto.setVin("LSVAM4187C2184842"); dto.setCompanyId(9); service.saveVin(dto); verify(mapper).insert(entityCaptor.capture()); assertEquals(Integer.valueOf(2), entityCaptor.getValue().getCompanyId()); } }