package com.ruoyi.order.service.impl; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.ruoyi.account.api.feignClient.VinManagementClient; import com.ruoyi.account.api.model.TVinManagement; import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient; import com.ruoyi.chargingPile.api.feignClient.ChargingPileClient; import com.ruoyi.chargingPile.api.model.TChargingGun; import com.ruoyi.chargingPile.api.model.TChargingPile; import com.ruoyi.common.core.domain.R; import com.ruoyi.integration.api.feignClient.SendMessageClient; import com.ruoyi.integration.api.model.PlatformStopCharging; import com.ruoyi.order.api.model.TVinChargingOrder; import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery; import com.ruoyi.order.mapper.TVinChargingOrderMapper; import com.ruoyi.other.api.domain.TGroupCompany; import com.ruoyi.other.api.dto.ChargeAuthorizationDTO; import com.ruoyi.other.api.dto.ChargeTimeRange; import com.ruoyi.other.api.feignClient.ChargeAuthorizationClient; import com.ruoyi.other.api.feignClient.GroupCompanyClient; import org.apache.ibatis.builder.MapperBuilderAssistant; 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.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * VIN 码充电「授权校验 + 授权到期提前 1 分钟主动停充」单测。 * 说明:授权时段判定依赖运行时钟,时段类用例统一使用与当前时刻无关的时段构造(rangesExcludingNow)。 */ @ExtendWith(MockitoExtension.class) class TVinChargingOrderAuthorizeStopTest { @InjectMocks private TVinChargingOrderServiceImpl service; @Mock private TVinChargingOrderMapper mapper; @Mock private ChargingGunClient chargingGunClient; @Mock private ChargingPileClient chargingPileClient; @Mock private SendMessageClient sendMessageClient; @Mock private GroupCompanyClient groupCompanyClient; @Mock private ChargeAuthorizationClient chargeAuthorizationClient; @Mock private VinManagementClient vinManagementClient; @BeforeEach void setUp() { ReflectionTestUtils.setField(service, "baseMapper", mapper); // lambdaUpdate 解析 getter 引用依赖 MyBatis-Plus 实体元信息缓存,单测环境需显式初始化 TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TVinChargingOrder.class); // lambdaUpdate 链式更新统一走 mapper.update(null, wrapper) lenient().when(mapper.update(any(), any())).thenReturn(1); } // ===== 一、授权到期提前停充判定(needAuthorizeStop) ===== @Test void doesNotStopWhenMaxEndTimeMissing() { assertFalse(needStop(order(LocalDateTime.now().minusHours(1), null, null))); } @Test void doesNotStopWhenRemainBeyondAheadWindow() { assertFalse(needStop(order(LocalDateTime.now().minusHours(1), LocalDateTime.now().plusMinutes(2), null))); } @Test void stopsOneMinuteAheadOfDeadline() { assertTrue(needStop(order(LocalDateTime.now().minusHours(1), LocalDateTime.now().plusSeconds(59), null))); } @Test void stopsWhenAuthorizationAlreadyExpired() { assertTrue(needStop(order(LocalDateTime.now().minusHours(1), LocalDateTime.now().minusSeconds(30), null))); } @Test void shortAuthorizationUsesHalfOfDurationAsAheadWindow() { // 授权时长 90 秒 → 提前量 min(60, 45)=45 秒,剩余 30 秒时触发停充 assertTrue(needStop(order(LocalDateTime.now().minusSeconds(60), LocalDateTime.now().plusSeconds(30), null))); } @Test void shortAuthorizationIsNotStoppedImmediately() { // 授权时长 100 秒 → 提前量 min(60, 50)=50 秒,剩余 55 秒时仍继续充电 assertFalse(needStop(order(LocalDateTime.now().minusSeconds(45), LocalDateTime.now().plusSeconds(55), null))); } @Test void fallsBackToCreateTimeWhenStartTimeMissing() { assertTrue(needStop(order(null, LocalDateTime.now().plusSeconds(30), LocalDateTime.now().minusHours(1)))); } @Test void doesNotStopBeforeChargeStarted() { assertFalse(needStop(order(LocalDateTime.now().plusSeconds(30), LocalDateTime.now().plusHours(2), null))); } // ===== 二、授权到期停充落库与下发 ===== @Test void stopByAuthorizeExpiredAdvancesStatusAndSendsCommand() { TVinChargingOrder order = order(LocalDateTime.now().minusHours(1), LocalDateTime.now().minusSeconds(5), null); mockDevice(order); ReflectionTestUtils.invokeMethod(service, "stopByAuthorizeExpired", order); ArgumentCaptor captor = ArgumentCaptor.forClass(PlatformStopCharging.class); verify(sendMessageClient).platformStopCharging(captor.capture()); assertEquals("PILE-22", captor.getValue().getCharging_pile_code()); assertEquals("GUN-11", captor.getValue().getCharging_gun_code()); assertEquals(Integer.valueOf(4), order.getStatus()); assertEquals(Integer.valueOf(1), order.getEndMode()); } @Test void doesNotSendStopCommandWhenStatusChangedConcurrently() { lenient().when(mapper.update(any(), any())).thenReturn(0); TVinChargingOrder order = order(LocalDateTime.now().minusHours(1), LocalDateTime.now().minusSeconds(5), null); mockDevice(order); ReflectionTestUtils.invokeMethod(service, "stopByAuthorizeExpired", order); verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class)); } @Test void monitoringTriggersAuthorizeStopInsteadOfRealtimeUpdate() { TVinChargingOrder order = order(LocalDateTime.now().minusHours(1), LocalDateTime.now().plusSeconds(20), null); mockDevice(order); when(mapper.selectOne(any())).thenReturn(order); UploadRealTimeMonitoringDataQuery query = new UploadRealTimeMonitoringDataQuery(); query.setTransaction_serial_number(order.getCode()); query.setOutput_current(new BigDecimal("10.0")); query.setOutput_voltage(new BigDecimal("500.0")); query.setCharging_degree(new BigDecimal("1.0000")); service.vinChargeMonitoring(query); verify(sendMessageClient).platformStopCharging(any(PlatformStopCharging.class)); // 已下发停充,实时数据不再落库 verify(mapper, never()).updateById(any(TVinChargingOrder.class)); } // ===== 三、申请充电前的授权与额度校验(resolveVinChargeContext) ===== @Test void rejectsSiteOutsideAuthorizationScope() { mockCompany(1, 1, 1, "1000"); ChargeAuthorizationDTO authorization = new ChargeAuthorizationDTO(); authorization.setSiteIds(Collections.singletonList(7)); authorization.setTimeMode(1); when(chargeAuthorizationClient.getDetailByCompanyId(1)).thenReturn(R.ok(authorization)); Object context = resolveContext(9, 100, vin(9001L, 1, "500")); assertEquals(Integer.valueOf(5), failCode(context)); } @Test void allowsAllDayAuthorizationWithDispatchCappedByBalance() { mockCompany(1, 1, 1, "1000"); ChargeAuthorizationDTO authorization = new ChargeAuthorizationDTO(); authorization.setTimeMode(1); authorization.setDispatchAmount(new BigDecimal("50")); when(chargeAuthorizationClient.getDetailByCompanyId(1)).thenReturn(R.ok(authorization)); Object context = resolveContext(9, 100, vin(9001L, 1, "500")); assertNull(failCode(context)); assertEquals(Integer.valueOf(1), ReflectionTestUtils.invokeMethod(context, "getAccountMode")); assertEquals(0, new BigDecimal("50").compareTo(ReflectionTestUtils.invokeMethod(context, "getDispatchAmount"))); assertNull(ReflectionTestUtils.invokeMethod(context, "getMaxEndTime")); } @Test void allowsUnconfiguredAuthorizationWithDefaultDispatchAmount() { mockCompany(1, 1, 1, "100"); when(chargeAuthorizationClient.getDetailByCompanyId(1)).thenReturn(R.ok((ChargeAuthorizationDTO) null)); Object context = resolveContext(9, 100, vin(9001L, 1, "500")); assertNull(failCode(context)); assertEquals(0, new BigDecimal("100").compareTo(ReflectionTestUtils.invokeMethod(context, "getDispatchAmount"))); assertNull(ReflectionTestUtils.invokeMethod(context, "getMaxEndTime")); } @Test void rejectsCurrentTimeOutsideAuthorizedTimeRange() { mockCompany(1, 1, 1, "1000"); ChargeAuthorizationDTO authorization = new ChargeAuthorizationDTO(); authorization.setTimeMode(2); authorization.setTimeRanges(rangesExcludingNow()); authorization.setAutoCharge(true); when(chargeAuthorizationClient.getDetailByCompanyId(1)).thenReturn(R.ok(authorization)); Object context = resolveContext(9, 100, vin(9001L, 1, "500")); assertEquals(Integer.valueOf(2), failCode(context)); } @Test void allowsTimeRangeAuthorizationWithoutAutoStop() { mockCompany(1, 1, 1, "1000"); ChargeAuthorizationDTO authorization = new ChargeAuthorizationDTO(); authorization.setTimeMode(2); // 未配置可用时段:按放行处理,避免误拦 authorization.setTimeRanges(Collections.emptyList()); when(chargeAuthorizationClient.getDetailByCompanyId(1)).thenReturn(R.ok(authorization)); Object context = resolveContext(9, 100, vin(9001L, 1, "500")); assertNull(failCode(context)); assertNull(ReflectionTestUtils.invokeMethod(context, "getMaxEndTime")); } @Test void rejectsAllocatedVinWithEmptyBalance() { mockCompany(1, 1, 2, "1000"); when(chargeAuthorizationClient.getDetailByCompanyId(1)).thenReturn(R.ok((ChargeAuthorizationDTO) null)); Object context = resolveContext(9, 100, vin(9001L, 1, "0")); assertEquals(Integer.valueOf(3), failCode(context)); } @Test void rejectsDisabledCompany() { mockCompany(1, 2, 1, "1000"); Object context = resolveContext(9, 100, vin(9001L, 1, "500")); assertEquals(Integer.valueOf(2), failCode(context)); verify(chargeAuthorizationClient, never()).getDetailByCompanyId(any()); } @Test void rejectsVinWithoutCompany() { Object context = resolveContext(9, 100, vin(9001L, null, "500")); assertEquals(Integer.valueOf(2), failCode(context)); } // ===== 用例支撑 ===== private Boolean needStop(TVinChargingOrder order) { return ReflectionTestUtils.invokeMethod(service, "needAuthorizeStop", order); } private Object resolveContext(Integer siteId, Integer strategyId, TVinManagement vinRecord) { return ReflectionTestUtils.invokeMethod(service, "resolveVinChargeContext", siteId, strategyId, vinRecord); } private Integer failCode(Object context) { return ReflectionTestUtils.invokeMethod(context, "getFailCode"); } private TVinChargingOrder order(LocalDateTime startTime, LocalDateTime maxEndTime, LocalDateTime createTime) { TVinChargingOrder order = new TVinChargingOrder(); order.setId(3001L); order.setCode("VD20260929001"); order.setStatus(3); order.setChargingGunId(11); order.setChargingPileId(22); order.setStartTime(startTime); order.setMaxEndTime(maxEndTime); order.setCreateTime(createTime); return order; } private void mockDevice(TVinChargingOrder order) { TChargingGun gun = new TChargingGun(); gun.setId(order.getChargingGunId()); gun.setCode("GUN-11"); TChargingPile pile = new TChargingPile(); pile.setId(order.getChargingPileId()); pile.setCode("PILE-22"); lenient().when(chargingGunClient.getChargingGunById(order.getChargingGunId())).thenReturn(R.ok(gun)); lenient().when(chargingPileClient.getChargingPileById(order.getChargingPileId())).thenReturn(R.ok(pile)); } private void mockCompany(Integer companyId, Integer status, Integer accountMode, String availableBalance) { TGroupCompany company = new TGroupCompany(); company.setId(companyId); company.setStatus(status); company.setAccountMode(accountMode); company.setAvailableBalance(new BigDecimal(availableBalance)); lenient().when(groupCompanyClient.getCompanyById(companyId)).thenReturn(R.ok(company)); } private TVinManagement vin(Long id, Integer companyId, String balance) { TVinManagement vinRecord = new TVinManagement(); vinRecord.setId(id); vinRecord.setCompanyId(companyId); vinRecord.setBalance(new BigDecimal(balance)); return vinRecord; } /** * 构造不覆盖当前时刻的可用时段(跨 00:00 的窗口合并为单段),使用例与运行时钟无关 */ private static List rangesExcludingNow() { LocalTime now = LocalTime.now(); LocalTime gapStart = now.minusMinutes(5); LocalTime gapEnd = now.plusMinutes(5); if (gapStart.isBefore(gapEnd)) { return Arrays.asList(new ChargeTimeRange(hhmm(LocalTime.MIN), hhmm(gapStart)), new ChargeTimeRange(hhmm(gapEnd), "23:59")); } return Collections.singletonList(new ChargeTimeRange(hhmm(gapEnd), hhmm(gapStart))); } private static String hhmm(LocalTime time) { return time.toString().substring(0, 5); } }