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.CompanyAllocateRecordClient;
|
import com.ruoyi.account.api.feignClient.VinManagementClient;
|
import com.ruoyi.account.api.model.TVinManagement;
|
import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyDetailClient;
|
import com.ruoyi.chargingPile.api.feignClient.BillingTemplateClient;
|
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.order.support.ChargeAccountSupport;
|
import com.ruoyi.other.api.feignClient.CompanyPreferentialClient;
|
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;
|
@Mock
|
private AccountingStrategyDetailClient accountingStrategyDetailClient;
|
@Mock
|
private CompanyPreferentialClient companyPreferentialClient;
|
@Mock
|
private BillingTemplateClient billingTemplateClient;
|
@Mock
|
private CompanyAllocateRecordClient companyAllocateRecordClient;
|
|
@BeforeEach
|
void setUp() {
|
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
// 账户支持组件(与生产同源):授权窗口解析/停充判定/优惠装载等公共逻辑已抽取到该组件,
|
// 纯单测环境下手工装配,其远程依赖复用本类 mock(未 stubbing 时按宽松 mock 返回空值,内部均有兜底)
|
ChargeAccountSupport chargeAccountSupport = new ChargeAccountSupport();
|
ReflectionTestUtils.setField(chargeAccountSupport, "accountingStrategyDetailClient", accountingStrategyDetailClient);
|
ReflectionTestUtils.setField(chargeAccountSupport, "groupCompanyClient", groupCompanyClient);
|
ReflectionTestUtils.setField(chargeAccountSupport, "companyPreferentialClient", companyPreferentialClient);
|
ReflectionTestUtils.setField(chargeAccountSupport, "billingTemplateClient", billingTemplateClient);
|
ReflectionTestUtils.setField(chargeAccountSupport, "companyAllocateRecordClient", companyAllocateRecordClient);
|
ReflectionTestUtils.setField(service, "chargeAccountSupport", chargeAccountSupport);
|
// 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 shortAuthorizationUsesFixedSixtySecondsAheadWindow() {
|
// 授权时长 90 秒 → 提前量 min(60, 90)=60 秒,剩余 30 秒时触发停充
|
assertTrue(needStop(order(LocalDateTime.now().minusSeconds(60), LocalDateTime.now().plusSeconds(30), null)));
|
}
|
|
@Test
|
void shortAuthorizationStopsImmediatelyWithinFixedAheadWindow() {
|
// 授权时长 100 秒:固定 60 秒提前量,剩余 55 秒即触发停充(不再按授权时长的一半收缩)
|
assertTrue(needStop(order(LocalDateTime.now().minusSeconds(45), LocalDateTime.now().plusSeconds(55), null)));
|
}
|
|
@Test
|
void veryShortAuthorizationUsesWholeDurationAsAheadWindow() {
|
// 授权总时长 30 秒不足提前量 → 按整个授权时长提前,剩余 20 秒即停充
|
assertTrue(needStop(order(LocalDateTime.now().minusSeconds(10), LocalDateTime.now().plusSeconds(20), 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<PlatformStopCharging> 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<ChargeTimeRange> 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);
|
}
|
}
|