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.TableInfoHelper; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.ruoyi.account.api.dto.AppUserWhitelistAllocateDTO; import com.ruoyi.account.api.dto.AppUserWhitelistAllocateItemDTO; import com.ruoyi.account.api.dto.AppUserWhitelistStatusDTO; import com.ruoyi.account.api.model.TAppUser; import com.ruoyi.account.api.model.TAppUserWhitelist; import com.ruoyi.account.api.model.TCompanyAllocateRecord; import com.ruoyi.account.api.vo.AppUserWhitelistAllocateVO; import com.ruoyi.account.api.vo.AppUserWhitelistImportResultVO; import com.ruoyi.account.api.vo.AppUserWhitelistMatchVO; import com.ruoyi.account.mapper.TAppUserWhitelistMapper; import com.ruoyi.account.service.TAppUserService; import com.ruoyi.account.service.TAppUserWhitelistSiteService; import com.ruoyi.account.service.TCompanyAllocateRecordService; import com.ruoyi.chargingPile.api.feignClient.PartnerClient; import com.ruoyi.chargingPile.api.feignClient.SiteClient; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.common.security.service.TokenService; import com.ruoyi.other.api.domain.TGroupCompany; import com.ruoyi.other.api.feignClient.GroupCompanyClient; import com.ruoyi.system.api.feignClient.SysUserClient; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.apache.poi.xssf.usermodel.XSSFWorkbook; 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.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import java.io.ByteArrayOutputStream; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import static org.junit.jupiter.api.Assertions.assertEquals; 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.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class TAppUserWhitelistServiceImplTest { @Mock private TAppUserWhitelistMapper mapper; @Mock private TAppUserWhitelistSiteService whitelistSiteService; @Mock private TAppUserService appUserService; @Mock private SiteClient siteClient; @Mock private PartnerClient partnerClient; @Mock private TokenService tokenService; @Mock private SysUserClient sysUserClient; @Mock private PlatformTransactionManager transactionManager; @Mock private GroupCompanyClient groupCompanyClient; @Mock private RedissonClient redissonClient; @Mock private TCompanyAllocateRecordService companyAllocateRecordService; private TAppUserWhitelistServiceImpl service; @BeforeAll static void initTableInfo() { // MP 的 lambdaUpdate 会通过 TableInfoHelper.getTableInfo 取表元数据, // 单元测试不启动 Spring,需要手动初始化 TableInfo,否则抛 MybatisPlusException TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TAppUserWhitelist.class); } @BeforeEach void setUp() { service = new TAppUserWhitelistServiceImpl(); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "whitelistSiteService", whitelistSiteService); ReflectionTestUtils.setField(service, "appUserService", appUserService); ReflectionTestUtils.setField(service, "siteClient", siteClient); ReflectionTestUtils.setField(service, "partnerClient", partnerClient); ReflectionTestUtils.setField(service, "tokenService", tokenService); ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient); ReflectionTestUtils.setField(service, "transactionManager", transactionManager); ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient); ReflectionTestUtils.setField(service, "redissonClient", redissonClient); ReflectionTestUtils.setField(service, "companyAllocateRecordService", companyAllocateRecordService); RLock lock = mock(RLock.class); // 并发锁仅在额度相关场景使用,宽松桩避免严格模式下未使用即报错 lenient().when(redissonClient.getLock(any())).thenReturn(lock); try { lenient().when(lock.tryLock(anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(true); } catch (InterruptedException exception) { throw new IllegalStateException(exception); } } @Test void importsValidRowsAndReturnsOnlyInvalidRows() throws Exception { when(mapper.selectValidByPhone(any())).thenReturn(null); TransactionStatus transactionStatus = mock(TransactionStatus.class); when(transactionManager.getTransaction(any())).thenReturn(transactionStatus); LambdaQueryChainWrapper appUserQuery = mock(LambdaQueryChainWrapper.class); when(appUserService.lambdaQuery()).thenReturn(appUserQuery); when(appUserQuery.eq(any(), any())).thenReturn(appUserQuery); when(appUserQuery.last(any())).thenReturn(appUserQuery); when(appUserQuery.one()).thenReturn(null); AtomicLong ids = new AtomicLong(1L); doAnswer(invocation -> { TAppUserWhitelist whitelist = invocation.getArgument(0); whitelist.setId(ids.getAndIncrement()); return 1; }).when(mapper).insert(any(TAppUserWhitelist.class)); AppUserWhitelistImportResultVO result = service.importWhitelist(importFile()); assertEquals(4, result.getTotalCount()); assertEquals(3, result.getSuccessCount()); assertEquals(1, result.getErrorCount()); assertEquals("1234", result.getErrorMessage().get(0).getPhone()); org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(TAppUserWhitelist.class); verify(mapper, org.mockito.Mockito.times(3)).insert(captor.capture()); for (TAppUserWhitelist saved : captor.getAllValues()) { // 导入统一按默认值写入:全部电站 + 启用;白名单权益已下线,不再写入享受权益,忽略模板中的享受权益/白名单电站列 assertEquals(null, saved.getBenefitType()); assertEquals(1, saved.getScopeType().intValue()); assertEquals(1, saved.getStatus().intValue()); } } // ---------------- unbindFleet ---------------- @Test void unbindFleetRejectsNullOrEmptyIds() { ServiceException nullEx = assertThrows(ServiceException.class, () -> service.unbindFleet(null)); assertEquals("请选择需要解绑车队的白名单用户", nullEx.getMessage()); ServiceException emptyEx = assertThrows(ServiceException.class, () -> service.unbindFleet(Collections.emptyList())); assertEquals("请选择需要解绑车队的白名单用户", emptyEx.getMessage()); } @Test void unbindFleetRejectsMissingRecords() { TAppUserWhitelist existing = new TAppUserWhitelist(); existing.setId(1L); when(mapper.selectBatchIds(anyCollection())).thenReturn(Collections.singletonList(existing)); ServiceException exception = assertThrows(ServiceException.class, () -> service.unbindFleet(Arrays.asList(1L, 2L))); assertEquals("部分白名单用户不存在或已删除", exception.getMessage()); verify(mapper, never()).update(isNull(), any(Wrapper.class)); } @Test void unbindFleetClearsFleetIdAndIgnoresUnboundRecords() { TAppUserWhitelist bound = new TAppUserWhitelist(); bound.setId(1L); bound.setFleetId(5); TAppUserWhitelist unbound = new TAppUserWhitelist(); 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); 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_by=")); // 实现使用 setSql("update_time = now()"),SQL 片段不带列名与等号紧邻的写法 assertTrue(sqlSet.contains("update_time = now()")); } @Test void unbindFleetReturnsZeroWhenAllUnbound() { TAppUserWhitelist unbound = new TAppUserWhitelist(); unbound.setId(1L); when(mapper.selectBatchIds(anyCollection())).thenReturn(Collections.singletonList(unbound)); int result = service.unbindFleet(Collections.singletonList(1L)); assertEquals(0, result); verify(mapper, never()).update(isNull(), any(Wrapper.class)); } // ---------------- 白名单匹配(含公司/车队归属) ---------------- @Test void matchWhitelistReturnsCompanyAndFleetOwnership() { TAppUser appUser = new TAppUser(); appUser.setId(100L); appUser.setPhone("13540211236"); when(appUserService.getById(100L)).thenReturn(appUser); TAppUserWhitelist whitelist = new TAppUserWhitelist(); whitelist.setId(88L); whitelist.setPhone("13540211236"); whitelist.setStatus(1); whitelist.setBenefitType(1); whitelist.setScopeType(1); whitelist.setCompanyId(12); whitelist.setFleetId(3); when(mapper.selectOne(any(Wrapper.class))).thenReturn(whitelist); AppUserWhitelistMatchVO result = service.matchWhitelist(100L, 2001); assertTrue(result.getMatched()); assertEquals(12, result.getCompanyId()); assertEquals(3, result.getFleetId()); // 白名单权益已下线:历史数据的享受权益不再回传给调用方,充电完全按企业优惠结算 assertEquals(null, result.getBenefitType()); assertEquals(null, result.getBenefitName()); } @Test void matchWhitelistReturnsNullOwnershipWhenUnassigned() { TAppUser appUser = new TAppUser(); appUser.setId(100L); appUser.setPhone("13540211236"); when(appUserService.getById(100L)).thenReturn(appUser); TAppUserWhitelist whitelist = new TAppUserWhitelist(); whitelist.setId(88L); whitelist.setPhone("13540211236"); whitelist.setStatus(1); whitelist.setBenefitType(2); whitelist.setScopeType(1); when(mapper.selectOne(any(Wrapper.class))).thenReturn(whitelist); AppUserWhitelistMatchVO result = service.matchWhitelist(100L, 2001); assertTrue(result.getMatched()); assertEquals(null, result.getCompanyId()); assertEquals(null, result.getFleetId()); } // ---------------- 额度分配 ---------------- @Test void getAllocateOverviewNormalizesAmounts() { TGroupCompany company = allocationCompany(9, "测试公司"); company.setAvailableBalance(new BigDecimal("8420.5")); company.setTotalRechargeAmount(null); when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(company)); AppUserWhitelistAllocateVO overview = service.getAllocateOverview(9); assertEquals(Integer.valueOf(9), overview.getCompanyId()); assertEquals("测试公司", overview.getCompanyName()); assertEquals(Integer.valueOf(2), overview.getAccountMode()); assertEquals(new BigDecimal("8420.50"), overview.getAvailableBalance()); assertEquals(new BigDecimal("0.00"), overview.getTotalRechargeAmount()); } @Test void getAllocateOverviewRejectsMissingCompanyId() { ServiceException exception = assertThrows(ServiceException.class, () -> service.getAllocateOverview(null)); assertEquals("请选择公司", exception.getMessage()); } @Test void allocateQuotaAddsWhitelistBalanceAndDeductsCompanyBalance() { when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); when(mapper.selectBatchIds(anyCollection())).thenReturn(Arrays.asList( allocateWhitelist(1L, 9, "13540211236", "10.00"), allocateWhitelist(2L, 9, "13540211237", "20.00"))); when(mapper.addBalance(anyLong(), any(BigDecimal.class), any())).thenReturn(1); when(groupCompanyClient.deductBalance(9, new BigDecimal("30.00"))).thenReturn(R.ok(new BigDecimal("970.00"))); AppUserWhitelistAllocateVO overview = service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00"), allocateItem(2L, "20.00"))); assertEquals(new BigDecimal("970.00"), overview.getAvailableBalance()); assertEquals("测试公司", overview.getCompanyName()); verify(redissonClient).getLock("APP_USER_WHITELIST_QUOTA:COMPANY:9"); verify(groupCompanyClient).deductBalance(9, new BigDecimal("30.00")); verify(mapper).addBalance(eq(1L), eq(new BigDecimal("10.00")), any()); verify(mapper).addBalance(eq(2L), eq(new BigDecimal("20.00")), any()); verify(companyAllocateRecordService).recordAllocate(TCompanyAllocateRecord.USER_TYPE_WHITELIST, "13540211236", null, 9, TCompanyAllocateRecord.SOURCE_ALLOCATE, new BigDecimal("10.00"), new BigDecimal("10.00"), "额度分配(公司:测试公司)"); } @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 allocateQuotaRejectsWhitelistOfAnotherCompany() { when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); when(mapper.selectBatchIds(anyCollection())) .thenReturn(Collections.singletonList(allocateWhitelist(1L, 8, "13540211236", "10.00"))); 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 allocateQuotaRejectsDisabledWhitelist() { when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); TAppUserWhitelist disabled = allocateWhitelist(1L, 9, "13540211236", "10.00"); disabled.setStatus(2); when(mapper.selectBatchIds(anyCollection())).thenReturn(Collections.singletonList(disabled)); 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 allocateQuotaRefundsCompanyBalanceWhenWhitelistUpdateFails() { when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); when(mapper.selectBatchIds(anyCollection())) .thenReturn(Collections.singletonList(allocateWhitelist(1L, 9, "13540211236", "0"))); when(mapper.addBalance(anyLong(), any(BigDecimal.class), any())).thenReturn(0); when(groupCompanyClient.deductBalance(9, new BigDecimal("10.00"))).thenReturn(R.ok(new BigDecimal("990.00"))); 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")))); assertTrue(exception.getMessage().contains("额度分配失败")); verify(groupCompanyClient).refundBalance(9, new BigDecimal("10.00")); } @Test void allocateQuotaRejectsAmountExceedingLimit() { ServiceException exception = assertThrows(ServiceException.class, () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "999999999.00")))); assertTrue(exception.getMessage().contains("不能超过")); verify(groupCompanyClient, never()).getCompanyById(any(Integer.class)); } @Test void allocateQuotaRejectsDuplicateWhitelistItems() { ServiceException exception = assertThrows(ServiceException.class, () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00"), allocateItem(1L, "5.00")))); assertEquals("分配明细中存在重复白名单用户", exception.getMessage()); } // ---------------- updateStatus ---------------- @Test void updateStatusRejectsInvalidParams() { assertThrows(ServiceException.class, () -> service.updateStatus(null)); AppUserWhitelistStatusDTO noId = new AppUserWhitelistStatusDTO(); noId.setStatus(1); assertThrows(ServiceException.class, () -> service.updateStatus(noId)); AppUserWhitelistStatusDTO badStatus = new AppUserWhitelistStatusDTO(); badStatus.setId(1L); badStatus.setStatus(3); ServiceException exception = assertThrows(ServiceException.class, () -> service.updateStatus(badStatus)); assertEquals("白名单用户状态参数错误", exception.getMessage()); verify(mapper, never()).update(isNull(), any(Wrapper.class)); } @Test void updateStatusRejectsMissingRecord() { when(mapper.selectById(1L)).thenReturn(null); AppUserWhitelistStatusDTO dto = new AppUserWhitelistStatusDTO(); dto.setId(1L); dto.setStatus(2); ServiceException exception = assertThrows(ServiceException.class, () -> service.updateStatus(dto)); assertEquals("白名单用户不存在或已删除", exception.getMessage()); verify(mapper, never()).update(isNull(), any(Wrapper.class)); } @Test void updateStatusKeepsUnchangedStatusWithoutUpdate() { TAppUserWhitelist existing = new TAppUserWhitelist(); existing.setId(1L); existing.setStatus(1); when(mapper.selectById(1L)).thenReturn(existing); AppUserWhitelistStatusDTO dto = new AppUserWhitelistStatusDTO(); dto.setId(1L); dto.setStatus(1); assertTrue(service.updateStatus(dto)); // 状态未变化时视为幂等成功,不产生更新语句,避免污染 update_time / update_by verify(mapper, never()).update(isNull(), any(Wrapper.class)); } @Test void updateStatusDisablesWhitelist() { TAppUserWhitelist existing = new TAppUserWhitelist(); existing.setId(1L); existing.setStatus(1); when(mapper.selectById(1L)).thenReturn(existing); when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1); AppUserWhitelistStatusDTO dto = new AppUserWhitelistStatusDTO(); dto.setId(1L); dto.setStatus(2); assertTrue(service.updateStatus(dto)); ArgumentCaptor wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class); verify(mapper).update(isNull(), wrapperCaptor.capture()); String sqlSet = wrapperCaptor.getValue().getSqlSet(); assertTrue(sqlSet.contains("status")); assertTrue(sqlSet.contains("update_time")); } @Test void updateStatusThrowsWhenUpdateAffectsNoRow() { TAppUserWhitelist existing = new TAppUserWhitelist(); existing.setId(1L); existing.setStatus(1); when(mapper.selectById(1L)).thenReturn(existing); when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(0); AppUserWhitelistStatusDTO dto = new AppUserWhitelistStatusDTO(); dto.setId(1L); dto.setStatus(2); ServiceException exception = assertThrows(ServiceException.class, () -> service.updateStatus(dto)); assertEquals("白名单用户状态更新失败,请重试", exception.getMessage()); } private TGroupCompany allocationCompany(Integer id, String name) { TGroupCompany company = new TGroupCompany(); company.setId(id); company.setName(name); company.setAccountMode(2); company.setStatus(1); return company; } private TAppUserWhitelist allocateWhitelist(Long id, Integer companyId, String phone, String balance) { TAppUserWhitelist whitelist = new TAppUserWhitelist(); whitelist.setId(id); whitelist.setCompanyId(companyId); whitelist.setPhone(phone); whitelist.setStatus(1); whitelist.setBalance(new BigDecimal(balance)); return whitelist; } private AppUserWhitelistAllocateDTO allocateDto(Integer companyId, AppUserWhitelistAllocateItemDTO... items) { AppUserWhitelistAllocateDTO dto = new AppUserWhitelistAllocateDTO(); dto.setCompanyId(companyId); dto.setItems(Arrays.asList(items)); return dto; } private AppUserWhitelistAllocateItemDTO allocateItem(Long whitelistId, String amount) { AppUserWhitelistAllocateItemDTO item = new AppUserWhitelistAllocateItemDTO(); item.setWhitelistId(whitelistId); item.setAmount(new BigDecimal(amount)); return item; } private MockMultipartFile importFile() throws Exception { try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("白名单用户"); org.apache.poi.ss.usermodel.Row header = sheet.createRow(0); header.createCell(0).setCellValue("用户手机号"); header.createCell(1).setCellValue("享受权益"); header.createCell(2).setCellValue("白名单电站"); addRow(sheet, 1, "13540211236", "1", "all"); addRow(sheet, 2, "13540211238", "2", "all"); addRow(sheet, 3, "13540211237", "", "all"); addRow(sheet, 4, "1234", "", "all"); workbook.write(output); return new MockMultipartFile("file", "whitelist.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", output.toByteArray()); } } private void addRow(org.apache.poi.ss.usermodel.Sheet sheet, int index, String phone, String benefitType, String whitelistSite) { org.apache.poi.ss.usermodel.Row row = sheet.createRow(index); row.createCell(0).setCellValue(phone); row.createCell(1).setCellValue(benefitType); row.createCell(2).setCellValue(whitelistSite); } }