package com.ruoyi.other.service.impl; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.ruoyi.account.api.feignClient.AppUserWhitelistClient; import com.ruoyi.account.api.feignClient.ChargingCardClient; import com.ruoyi.account.api.feignClient.VinManagementClient; import com.ruoyi.account.api.model.TVinManagement; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.web.domain.AjaxResult; import com.ruoyi.other.api.domain.TGroupCompany; import com.ruoyi.other.api.model.TCompanyRechargeRecord; import com.ruoyi.other.mapper.TCompanyRechargeRecordMapper; import com.ruoyi.other.mapper.TFleetMapper; import com.ruoyi.system.api.domain.SysUser; import com.ruoyi.system.api.feignClient.SysRoleClient; import com.ruoyi.system.api.feignClient.SysUserClient; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.test.util.ReflectionTestUtils; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** * 公司详情(车队数量、本月充值金额)统计逻辑单测,不依赖 Spring 容器与数据库。 */ class TGroupCompanyServiceImplTest { /** 编辑公司用例共享的远程账号客户端 mock,供 verify 使用 */ private SysUserClient editUserClient; @BeforeAll static void initTableInfo() { // 初始化 MyBatis-Plus 表元数据,使 LambdaQueryWrapper 可在无 Spring/DB 环境下解析列名 TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TCompanyRechargeRecord.class); } @Test void fillsFleetCountAndCurrentMonthRechargeAmount() { TGroupCompany company = new TGroupCompany(); company.setId(1); TFleetMapper fleetMapper = Mockito.mock(TFleetMapper.class); Mockito.when(fleetMapper.selectCount(Mockito.any())).thenReturn(3L); TCompanyRechargeRecordMapper rechargeMapper = Mockito.mock(TCompanyRechargeRecordMapper.class); Mockito.when(rechargeMapper.selectList(Mockito.any())) .thenReturn(Arrays.asList(record("100.00"), record(null), record("50.50"))); TGroupCompany detail = buildService(company, fleetMapper, rechargeMapper).getDetail(1); assertEquals(3, detail.getFleetCount()); assertEquals(new BigDecimal("150.50"), detail.getMonthRechargeAmount()); } @Test void returnsZeroAmountWhenNoRechargeRecordInCurrentMonth() { TGroupCompany company = new TGroupCompany(); company.setId(1); TFleetMapper fleetMapper = Mockito.mock(TFleetMapper.class); Mockito.when(fleetMapper.selectCount(Mockito.any())).thenReturn(null); TCompanyRechargeRecordMapper rechargeMapper = Mockito.mock(TCompanyRechargeRecordMapper.class); Mockito.when(rechargeMapper.selectList(Mockito.any())).thenReturn(Collections.emptyList()); TGroupCompany detail = buildService(company, fleetMapper, rechargeMapper).getDetail(1); assertEquals(0, detail.getFleetCount()); assertEquals(0, new BigDecimal("0.00").compareTo(detail.getMonthRechargeAmount())); assertEquals(2, detail.getMonthRechargeAmount().scale()); } /** * 公司详情(带充电用户聚合)统计 VIN码车辆数量:与 VIN 列表接口同口径, * 且聚合充电用户时复用同一远程结果,不重复调用远程接口。 */ @Test void fillsVinCountAndReusesRemoteVinList() { TGroupCompany company = new TGroupCompany(); company.setId(1); company.setAccountMode(1); company.setAvailableBalance(new BigDecimal("100.00")); TVinManagement vin1 = new TVinManagement(); vin1.setId(11L); vin1.setVin("TESTVIN0001"); vin1.setBalance(new BigDecimal("10.00")); vin1.setStatus(1); TVinManagement vin2 = new TVinManagement(); vin2.setId(12L); vin2.setVin("TESTVIN0002"); vin2.setBalance(new BigDecimal("20.00")); vin2.setStatus(1); TFleetMapper fleetMapper = Mockito.mock(TFleetMapper.class); Mockito.when(fleetMapper.selectCount(Mockito.any())).thenReturn(0L); Mockito.when(fleetMapper.selectList(Mockito.any())).thenReturn(Collections.emptyList()); TCompanyRechargeRecordMapper rechargeMapper = Mockito.mock(TCompanyRechargeRecordMapper.class); Mockito.when(rechargeMapper.selectList(Mockito.any())).thenReturn(Collections.emptyList()); VinManagementClient vinClient = Mockito.mock(VinManagementClient.class); Mockito.when(vinClient.listByCompany(1)).thenReturn(R.ok(Arrays.asList(vin1, vin2))); ChargingCardClient cardClient = Mockito.mock(ChargingCardClient.class); Mockito.when(cardClient.listByCompany(1)).thenReturn(R.ok(Collections.emptyList())); AppUserWhitelistClient whitelistClient = Mockito.mock(AppUserWhitelistClient.class); Mockito.when(whitelistClient.listByCompany(1)).thenReturn(R.ok(Collections.emptyList())); TGroupCompanyServiceImpl service = buildService(company, fleetMapper, rechargeMapper); ReflectionTestUtils.setField(service, "vinManagementClient", vinClient); ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); ReflectionTestUtils.setField(service, "appUserWhitelistClient", whitelistClient); TGroupCompany detail = service.getDetailWithChargingUsers(1); assertEquals(2, detail.getVinCount()); assertEquals(2, detail.getChargingUsers().size()); // VIN 远程接口仅调用一次:vinCount 统计与充电用户聚合复用同一结果 Mockito.verify(vinClient, Mockito.times(1)).listByCompany(1); } /** * VIN 远程接口异常降级:vinCount 返回 0,详情主流程不受影响 */ @Test void vinCountZeroWhenVinRemoteFails() { TGroupCompany company = new TGroupCompany(); company.setId(1); company.setAccountMode(1); TFleetMapper fleetMapper = Mockito.mock(TFleetMapper.class); Mockito.when(fleetMapper.selectCount(Mockito.any())).thenReturn(0L); Mockito.when(fleetMapper.selectList(Mockito.any())).thenReturn(Collections.emptyList()); TCompanyRechargeRecordMapper rechargeMapper = Mockito.mock(TCompanyRechargeRecordMapper.class); Mockito.when(rechargeMapper.selectList(Mockito.any())).thenReturn(Collections.emptyList()); VinManagementClient vinClient = Mockito.mock(VinManagementClient.class); Mockito.when(vinClient.listByCompany(1)).thenThrow(new RuntimeException("remote down")); ChargingCardClient cardClient = Mockito.mock(ChargingCardClient.class); Mockito.when(cardClient.listByCompany(1)).thenReturn(R.ok(Collections.emptyList())); AppUserWhitelistClient whitelistClient = Mockito.mock(AppUserWhitelistClient.class); Mockito.when(whitelistClient.listByCompany(1)).thenReturn(R.ok(Collections.emptyList())); TGroupCompanyServiceImpl service = buildService(company, fleetMapper, rechargeMapper); ReflectionTestUtils.setField(service, "vinManagementClient", vinClient); ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); ReflectionTestUtils.setField(service, "appUserWhitelistClient", whitelistClient); TGroupCompany detail = service.getDetailWithChargingUsers(1); assertEquals(0, detail.getVinCount()); assertTrue(detail.getChargingUsers().isEmpty()); } @Test void returnsNullAndSkipsStatisticsWhenCompanyNotExists() { TFleetMapper fleetMapper = Mockito.mock(TFleetMapper.class); TCompanyRechargeRecordMapper rechargeMapper = Mockito.mock(TCompanyRechargeRecordMapper.class); TGroupCompany detail = buildService(null, fleetMapper, rechargeMapper).getDetail(9); assertNull(detail); Mockito.verifyNoInteractions(fleetMapper, rechargeMapper); } @Test @SuppressWarnings("unchecked") void queriesCurrentMonthRechargeRecordsByCompanyAndTimeRange() { TGroupCompany company = new TGroupCompany(); company.setId(2); TFleetMapper fleetMapper = Mockito.mock(TFleetMapper.class); Mockito.when(fleetMapper.selectCount(Mockito.any())).thenReturn(0L); TCompanyRechargeRecordMapper rechargeMapper = Mockito.mock(TCompanyRechargeRecordMapper.class); Mockito.when(rechargeMapper.selectList(Mockito.any())).thenReturn(Collections.emptyList()); buildService(company, fleetMapper, rechargeMapper).getDetail(2); ArgumentCaptor> captor = ArgumentCaptor.forClass(Wrapper.class); // getDetail 会查询两次充值记录:本月 + 累计;首次调用为本月查询 Mockito.verify(rechargeMapper, Mockito.times(2)).selectList(captor.capture()); // 过滤条件:company_id = 2,且 recharge_time ∈ [本月1日 00:00:00, 次月1日 00:00:00) String sqlSegment = captor.getAllValues().get(0).getSqlSegment(); assertTrue(sqlSegment.contains("company_id"), sqlSegment); assertTrue(sqlSegment.contains("recharge_time"), sqlSegment); LocalDateTime start = TGroupCompanyServiceImpl.monthStart(LocalDate.now()); Map paramValues = (Map) ReflectionTestUtils.getField(captor.getAllValues().get(0), "paramNameValuePairs"); assertTrue(paramValues != null && paramValues.containsValue(2), String.valueOf(paramValues)); assertTrue(paramValues != null && paramValues.containsValue(start), String.valueOf(paramValues)); assertTrue(paramValues != null && paramValues.containsValue(start.plusMonths(1)), String.valueOf(paramValues)); } @Test void monthStartIsTheFirstDayOfNaturalMonth() { assertEquals(LocalDateTime.of(2026, 9, 1, 0, 0, 0), TGroupCompanyServiceImpl.monthStart(LocalDate.of(2026, 9, 24))); assertEquals(LocalDateTime.of(2026, 9, 1, 0, 0, 0), TGroupCompanyServiceImpl.monthStart(LocalDate.of(2026, 9, 1))); assertEquals(LocalDateTime.of(2027, 1, 1, 0, 0, 0), TGroupCompanyServiceImpl.monthStart(LocalDate.of(2026, 12, 31)).plusMonths(1)); } @Test void sumArrivalAmountReturnsZeroWithScaleForEmptyList() { assertEquals(new BigDecimal("0.00"), TGroupCompanyServiceImpl.sumArrivalAmount(new ArrayList<>())); } private TGroupCompanyServiceImpl buildService(TGroupCompany company, TFleetMapper fleetMapper, TCompanyRechargeRecordMapper rechargeMapper) { TGroupCompanyServiceImpl service = Mockito.spy(new TGroupCompanyServiceImpl()); Mockito.doReturn(company).when(service).getById(Mockito.any()); ReflectionTestUtils.setField(service, "fleetMapper", fleetMapper); ReflectionTestUtils.setField(service, "companyRechargeRecordMapper", rechargeMapper); return service; } private TCompanyRechargeRecord record(String arrivalAmount) { TCompanyRechargeRecord record = new TCompanyRechargeRecord(); record.setArrivalAmount(arrivalAmount == null ? null : new BigDecimal(arrivalAmount)); return record; } /** * 添加/编辑企业弹窗不再配置账户模式:未传账户模式时按共享模式(1)建档 */ @Test void addCompanyDefaultsAccountModeToSharedWhenAbsent() { TGroupCompany company = new TGroupCompany(); company.setAccount("company01"); company.setPassword("pwd"); company.setName("测试公司"); TGroupCompanyServiceImpl service = buildAddCompanyService(); assertEquals(200, code(service.addCompany(company))); ArgumentCaptor captor = ArgumentCaptor.forClass(TGroupCompany.class); Mockito.verify(service).save(captor.capture()); assertEquals(1, captor.getValue().getAccountMode()); } /** * 显式传入账户模式时不被兜底值覆盖(兼容后台/脚本直接建档) */ @Test void addCompanyKeepsExplicitAccountMode() { TGroupCompany company = new TGroupCompany(); company.setAccount("company02"); company.setPassword("pwd"); company.setAccountMode(2); TGroupCompanyServiceImpl service = buildAddCompanyService(); assertEquals(200, code(service.addCompany(company))); ArgumentCaptor captor = ArgumentCaptor.forClass(TGroupCompany.class); Mockito.verify(service).save(captor.capture()); assertEquals(2, captor.getValue().getAccountMode()); } @Test void updateAccountModeRejectsIllegalArgs() { TGroupCompany company = new TGroupCompany(); company.setId(1); TGroupCompanyServiceImpl service = buildService(company, Mockito.mock(TFleetMapper.class), Mockito.mock(TCompanyRechargeRecordMapper.class)); assertEquals(500, code(service.updateAccountMode(null, 1))); assertEquals(500, code(service.updateAccountMode(1, null))); assertEquals(500, code(service.updateAccountMode(1, 3))); Mockito.verify(service, Mockito.never()).updateById(Mockito.any()); } @Test void updateAccountModeRejectsUnknownCompany() { TGroupCompanyServiceImpl service = buildService(null, Mockito.mock(TFleetMapper.class), Mockito.mock(TCompanyRechargeRecordMapper.class)); assertEquals(500, code(service.updateAccountMode(9, 2))); Mockito.verify(service, Mockito.never()).updateById(Mockito.any()); } @Test void updateAccountModeOnlyUpdatesAccountModeColumn() { TGroupCompany company = new TGroupCompany(); company.setId(2); TGroupCompanyServiceImpl service = buildService(company, Mockito.mock(TFleetMapper.class), Mockito.mock(TCompanyRechargeRecordMapper.class)); Mockito.doReturn(true).when(service).updateById(Mockito.any()); assertEquals(200, code(service.updateAccountMode(2, 2))); ArgumentCaptor captor = ArgumentCaptor.forClass(TGroupCompany.class); Mockito.verify(service).updateById(captor.capture()); TGroupCompany update = captor.getValue(); assertEquals(2, update.getId()); assertEquals(2, update.getAccountMode()); // 只更新账户模式列,其余字段保持 null,避免覆盖公司其它资料 assertNull(update.getName()); assertNull(update.getAccount()); assertNull(update.getStatus()); } /** * 构建关注建档字段的 spy:远程账号相关调用全部打桩,不依赖 Spring 容器与数据库 */ private TGroupCompanyServiceImpl buildAddCompanyService() { SysUserClient sysUserClient = Mockito.mock(SysUserClient.class); Mockito.when(sysUserClient.queryUserByUserName(Mockito.any())).thenReturn(R.ok()); Mockito.when(sysUserClient.addSysUser(Mockito.any())).thenReturn(R.ok(100L)); Mockito.when(sysUserClient.getSysUser(Mockito.anyLong())).thenReturn(R.ok()); SysRoleClient sysRoleClient = Mockito.mock(SysRoleClient.class); Mockito.when(sysRoleClient.getRoleIdByRoleKey(Mockito.any())).thenReturn(R.ok(74L)); TGroupCompanyServiceImpl service = Mockito.spy(new TGroupCompanyServiceImpl()); Mockito.doReturn(0L).when(service).count(Mockito.any()); Mockito.doReturn(true).when(service).save(Mockito.any()); ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient); ReflectionTestUtils.setField(service, "sysRoleClient", sysRoleClient); return service; } /** * 编辑公司修改登录账号:按旧账号定位用户后把 userName 更新为新账号, * 不误走补建分支(避免双账号残留) */ @Test void editCompanyUpdatesUserNameWhenAccountChanged() { TGroupCompany old = persistedCompany("company01"); TGroupCompany edit = editCompanyBody(); edit.setAccount("company02"); TGroupCompanyServiceImpl service = buildEditCompanyService(old, "company01", owner(100L), "company02", null); assertEquals(200, code(service.editCompany(edit))); ArgumentCaptor captor = ArgumentCaptor.forClass(SysUser.class); Mockito.verify(editUserClient).updateSysUser(captor.capture()); assertEquals("company02", captor.getValue().getUserName()); Mockito.verify(editUserClient, Mockito.never()).addSysUser(Mockito.any()); assertEquals("company02", edit.getAccount()); } /** * 编辑公司修改登录密码:调用 resetPassword 且仅携带 userId 与密码(前端已 HMAC-MD5) */ @Test void editCompanyResetsPasswordWhenProvided() { TGroupCompany old = persistedCompany("company01"); TGroupCompany edit = editCompanyBody(); edit.setAccount("company01"); edit.setPassword("hmac-value"); TGroupCompanyServiceImpl service = buildEditCompanyService(old, "company01", owner(100L), null, null); assertEquals(200, code(service.editCompany(edit))); ArgumentCaptor pwdCaptor = ArgumentCaptor.forClass(SysUser.class); Mockito.verify(editUserClient).resetPassword(pwdCaptor.capture()); assertEquals(Long.valueOf(100L), pwdCaptor.getValue().getUserId()); assertEquals("hmac-value", pwdCaptor.getValue().getPassword()); // 资料更新调用不携带密码,避免哈希/脱敏值写回 ArgumentCaptor userCaptor = ArgumentCaptor.forClass(SysUser.class); Mockito.verify(editUserClient).updateSysUser(userCaptor.capture()); assertNull(userCaptor.getValue().getPassword()); } /** * 新账号已被其他用户占用:返回错误且不更新任何数据 */ @Test void editCompanyRejectsAccountOccupiedByOtherUser() { TGroupCompany old = persistedCompany("company01"); TGroupCompany edit = editCompanyBody(); edit.setAccount("company02"); TGroupCompanyServiceImpl service = buildEditCompanyService(old, "company01", owner(100L), "company02", owner(999L)); assertEquals(500, code(service.editCompany(edit))); Mockito.verify(editUserClient, Mockito.never()).updateSysUser(Mockito.any()); Mockito.verify(editUserClient, Mockito.never()).resetPassword(Mockito.any()); Mockito.verify(service, Mockito.never()).updateById(Mockito.any()); } /** * 编辑时密码留空:不调用 resetPassword,密码保持不变 */ @Test void editCompanyKeepsPasswordWhenAbsent() { TGroupCompany old = persistedCompany("company01"); TGroupCompany edit = editCompanyBody(); edit.setAccount("company01"); TGroupCompanyServiceImpl service = buildEditCompanyService(old, "company01", owner(100L), null, null); assertEquals(200, code(service.editCompany(edit))); Mockito.verify(editUserClient, Mockito.never()).resetPassword(Mockito.any()); Mockito.verify(editUserClient).updateSysUser(Mockito.any(SysUser.class)); } private TGroupCompany persistedCompany(String account) { TGroupCompany company = new TGroupCompany(); company.setId(1); company.setAccount(account); company.setName("旧公司名"); return company; } private TGroupCompany editCompanyBody() { TGroupCompany company = new TGroupCompany(); company.setId(1); company.setName("新公司名"); company.setContactPhone("13800000000"); return company; } private SysUser owner(Long userId) { SysUser user = new SysUser(); user.setUserId(userId); user.setPassword("hashed"); return user; } /** * 构建编辑公司场景的 spy:getById 返回库里旧记录,updateById 打桩; * queryUserByUserName 按账号返回不同用户(newAccountUser 非 null 时用于模拟新账号占用) */ private TGroupCompanyServiceImpl buildEditCompanyService(TGroupCompany old, String oldAccount, SysUser oldAccountUser, String newAccount, SysUser newAccountUser) { SysUserClient sysUserClient = Mockito.mock(SysUserClient.class); Mockito.when(sysUserClient.queryUserByUserName(oldAccount)).thenReturn(R.ok(oldAccountUser)); if (newAccount != null) { Mockito.when(sysUserClient.queryUserByUserName(newAccount)).thenReturn(R.ok(newAccountUser)); } Mockito.when(sysUserClient.updateSysUser(Mockito.any())).thenReturn(R.ok(true)); Mockito.when(sysUserClient.resetPassword(Mockito.any())).thenReturn(R.ok()); this.editUserClient = sysUserClient; TGroupCompanyServiceImpl service = Mockito.spy(new TGroupCompanyServiceImpl()); Mockito.doReturn(old).when(service).getById(Mockito.any()); Mockito.doReturn(true).when(service).updateById(Mockito.any()); Mockito.doReturn(0L).when(service).count(Mockito.any()); ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient); return service; } private int code(AjaxResult result) { return ((Number) result.get("code")).intValue(); } }