package com.ruoyi.order.service.impl;
|
|
import com.ruoyi.account.api.feignClient.AppUserIntegralChangeClient;
|
import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
|
import com.ruoyi.chargingPile.api.feignClient.ChargingPileClient;
|
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
|
import com.ruoyi.chargingPile.api.model.Site;
|
import com.ruoyi.chargingPile.api.model.TChargingGun;
|
import com.ruoyi.chargingPile.api.model.TChargingPile;
|
import com.ruoyi.common.core.domain.R;
|
import com.ruoyi.common.security.service.TokenService;
|
import com.ruoyi.integration.api.model.UploadRealTimeMonitoringData;
|
import com.ruoyi.order.api.model.TChargingOrder;
|
import com.ruoyi.order.api.model.TVinChargingOrder;
|
import com.ruoyi.order.dto.ChargingDetails;
|
import com.ruoyi.order.mapper.TChargingOrderMapper;
|
import com.ruoyi.order.mapper.TVinChargingOrderMapper;
|
import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
|
import com.ruoyi.order.vo.EndOfChargePageInfo;
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
import com.ruoyi.system.api.model.LoginUserApplet;
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
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.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.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Collections;
|
import java.util.List;
|
import java.util.regex.Matcher;
|
import java.util.regex.Pattern;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
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.verify;
|
import static org.mockito.Mockito.when;
|
|
/**
|
* TChargingOrderServiceImpl 小程序接口对 VIN 码充电订单(t_vin_charging_order)的兜底逻辑测试:
|
* getChargingDetails(正在充电详情)、getEndOfChargePageInfo(结束页详情)。
|
*/
|
@ExtendWith(MockitoExtension.class)
|
class TChargingOrderServiceImplVinFallbackTest {
|
|
@InjectMocks
|
private TChargingOrderServiceImpl service;
|
@Mock
|
private TChargingOrderMapper mapper;
|
@Mock
|
private TVinChargingOrderMapper vinChargingOrderMapper;
|
@Mock
|
private TokenService tokenService;
|
@Mock
|
private ChargingGunClient chargingGunClient;
|
@Mock
|
private ChargingPileClient chargingPileClient;
|
@Mock
|
private SiteClient siteClient;
|
@Mock
|
private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService;
|
@Mock
|
private AppUserIntegralChangeClient appUserIntegralChangeClient;
|
|
/**
|
* 纯单测无 Spring 容器,MyBatis-Plus 的 lambda 列缓存需手工初始化,
|
* 否则 LambdaQueryWrapper 解析 getter 引用时抛 "can not find lambda cache"
|
*/
|
@BeforeAll
|
static void initMybatisPlusTableInfo() {
|
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
TableInfoHelper.initTableInfo(assistant, TChargingOrder.class);
|
TableInfoHelper.initTableInfo(assistant, TVinChargingOrder.class);
|
}
|
|
@BeforeEach
|
void setUp() {
|
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
}
|
|
@Test
|
void getChargingDetailsFallsBackToVinOrderAndAssemblesRealTimeData() {
|
LoginUserApplet loginUser = new LoginUserApplet();
|
loginUser.setUserId(88L);
|
when(tokenService.getLoginUserApplet()).thenReturn(loginUser);
|
// 普通订单表查不到
|
when(mapper.selectOne(any())).thenReturn(null);
|
TVinChargingOrder vinOrder = new TVinChargingOrder();
|
vinOrder.setId(1001L);
|
vinOrder.setAppUserId(88L);
|
vinOrder.setChargingGunId(5);
|
vinOrder.setStatus(3);
|
vinOrder.setCode("VD20261013001");
|
vinOrder.setResidualAmount(new BigDecimal("66.60"));
|
when(vinChargingOrderMapper.selectOne(any())).thenReturn(vinOrder);
|
TChargingGun gun = new TChargingGun();
|
gun.setChargingPileId(7);
|
when(chargingGunClient.getChargingGunById(5)).thenReturn(R.ok(gun));
|
TChargingPile pile = new TChargingPile();
|
pile.setName("1号桩");
|
pile.setSiteId(9);
|
when(chargingPileClient.getChargingPileById(7)).thenReturn(R.ok(pile));
|
Site site = new Site();
|
site.setName("城东站");
|
when(siteClient.getSiteByIds(Arrays.asList(9))).thenReturn(R.ok(Collections.singletonList(site)));
|
UploadRealTimeMonitoringData data = new UploadRealTimeMonitoringData();
|
data.setOutput_current(new BigDecimal("10"));
|
data.setOutput_voltage(new BigDecimal("220"));
|
data.setSoc(50);
|
data.setTime_remaining(30);
|
data.setCharging_degree(new BigDecimal("5.5"));
|
data.setCumulative_charging_time(75);
|
when(uploadRealTimeMonitoringDataService.getLastDataById("VD20261013001")).thenReturn(data);
|
|
ChargingDetails details = service.getChargingDetails(5);
|
|
assertEquals("1001", details.getId());
|
assertEquals("城东站-1号桩", details.getName());
|
assertEquals("VD20261013001", details.getCode());
|
assertEquals(Integer.valueOf(3), details.getStatus());
|
assertEquals(0, new BigDecimal("66.60").compareTo(details.getChargingCost()));
|
assertEquals(0, new BigDecimal("10").compareTo(details.getChargeCurrent()));
|
assertEquals(0, new BigDecimal("220").compareTo(details.getChargeVoltage()));
|
assertEquals(0, new BigDecimal("2.200").compareTo(details.getChargePower()));
|
assertEquals(Integer.valueOf(50), details.getCompletionRatio());
|
assertEquals(Integer.valueOf(30), details.getRemainingChargeTime());
|
assertEquals(0, new BigDecimal("5.5").compareTo(details.getChargedDegrees()));
|
assertEquals("01:15", details.getChargedTime());
|
}
|
|
@Test
|
void getChargingDetailsUsesPaidAmountAsUsedCostWhenRealTimeDataPresent() {
|
LoginUserApplet loginUser = new LoginUserApplet();
|
loginUser.setUserId(88L);
|
when(tokenService.getLoginUserApplet()).thenReturn(loginUser);
|
when(mapper.selectOne(any())).thenReturn(null);
|
TVinChargingOrder vinOrder = new TVinChargingOrder();
|
vinOrder.setId(1001L);
|
vinOrder.setAppUserId(88L);
|
vinOrder.setChargingGunId(5);
|
vinOrder.setStatus(3);
|
vinOrder.setCode("VD20261013001");
|
vinOrder.setResidualAmount(new BigDecimal("66.60"));
|
when(vinChargingOrderMapper.selectOne(any())).thenReturn(vinOrder);
|
TChargingGun gun = new TChargingGun();
|
gun.setChargingPileId(7);
|
when(chargingGunClient.getChargingGunById(5)).thenReturn(R.ok(gun));
|
TChargingPile pile = new TChargingPile();
|
pile.setName("1号桩");
|
pile.setSiteId(9);
|
when(chargingPileClient.getChargingPileById(7)).thenReturn(R.ok(pile));
|
Site site = new Site();
|
site.setName("城东站");
|
when(siteClient.getSiteByIds(Arrays.asList(9))).thenReturn(R.ok(Collections.singletonList(site)));
|
UploadRealTimeMonitoringData data = new UploadRealTimeMonitoringData();
|
data.setOutput_current(new BigDecimal("10"));
|
data.setOutput_voltage(new BigDecimal("220"));
|
data.setSoc(50);
|
data.setTime_remaining(30);
|
data.setCharging_degree(new BigDecimal("5.5"));
|
data.setCumulative_charging_time(75);
|
// 设备上报已充金额为 4 位小数,展示口径统一保留 2 位
|
data.setPaid_amount(new BigDecimal("12.3456"));
|
when(uploadRealTimeMonitoringDataService.getLastDataById("VD20261013001")).thenReturn(data);
|
|
ChargingDetails details = service.getChargingDetails(5);
|
|
assertEquals(0, new BigDecimal("12.35").compareTo(details.getChargingCost()));
|
}
|
|
@Test
|
void getChargingDetailsReturnsNullWhenNeitherNormalNorVinOrderExists() {
|
LoginUserApplet loginUser = new LoginUserApplet();
|
loginUser.setUserId(88L);
|
when(tokenService.getLoginUserApplet()).thenReturn(loginUser);
|
when(mapper.selectOne(any())).thenReturn(null);
|
when(vinChargingOrderMapper.selectOne(any())).thenReturn(null);
|
|
assertNull(service.getChargingDetails(5));
|
}
|
|
@Test
|
void getEndOfChargePageInfoFallsBackToVinOrder() {
|
LoginUserApplet loginUser = new LoginUserApplet();
|
loginUser.setUserId(88L);
|
when(tokenService.getLoginUserApplet()).thenReturn(loginUser);
|
when(mapper.selectById("1001")).thenReturn(null);
|
TVinChargingOrder vinOrder = new TVinChargingOrder();
|
vinOrder.setId(1001L);
|
vinOrder.setAppUserId(88L);
|
vinOrder.setCode("VD20261013001");
|
vinOrder.setPaymentAmount(new BigDecimal("18.50"));
|
vinOrder.setRechargeAmount(new BigDecimal("50.00"));
|
vinOrder.setRefundAmount(new BigDecimal("31.50"));
|
vinOrder.setStartTime(LocalDateTime.of(2026, 10, 13, 10, 0));
|
vinOrder.setEndTime(LocalDateTime.of(2026, 10, 13, 11, 30));
|
when(vinChargingOrderMapper.selectById("1001")).thenReturn(vinOrder);
|
when(appUserIntegralChangeClient.getTAppUserIntegralChangeByOrderCode("VD20261013001"))
|
.thenReturn(R.ok(null));
|
|
EndOfChargePageInfo info = service.getEndOfChargePageInfo("1001");
|
|
assertEquals("1001", info.getId());
|
assertEquals("VD20261013001", info.getCode());
|
assertEquals(0, new BigDecimal("18.50").compareTo(info.getPaymentAmount()));
|
assertEquals(0, new BigDecimal("50.00").compareTo(info.getRechargeAmount()));
|
assertEquals(0, new BigDecimal("31.50").compareTo(info.getRefundAmount()));
|
assertEquals(Integer.valueOf(0), info.getIntegral());
|
assertEquals("10:00", info.getStartHour());
|
assertEquals("11:30", info.getEndHour());
|
assertEquals("1小时30分钟", info.getDuration());
|
}
|
|
@Test
|
void getEndOfChargePageInfoThrowsWhenOrderMissingInBothTables() {
|
// 两表均查不到订单时在获取登录用户之前即抛异常,tokenService 无需 stub
|
when(mapper.selectById("1001")).thenReturn(null);
|
when(vinChargingOrderMapper.selectById("1001")).thenReturn(null);
|
|
RuntimeException exception = null;
|
try {
|
service.getEndOfChargePageInfo("1001");
|
} catch (RuntimeException ex) {
|
exception = ex;
|
}
|
assertNotNull(exception);
|
assertEquals("充电订单不存在", exception.getMessage());
|
}
|
|
@Test
|
void getChargingDetailsReturnsFinishedNormalOrderAndQueriesStatus5() {
|
// 小程序主动结束充电后订单已结束(5),充电中页面仍需拿到该笔订单,
|
// 用于关闭「请勿拔枪,正在结束充电」并展示「充电完成,可拔枪」
|
LoginUserApplet loginUser = new LoginUserApplet();
|
loginUser.setUserId(88L);
|
when(tokenService.getLoginUserApplet()).thenReturn(loginUser);
|
TChargingOrder order = new TChargingOrder();
|
order.setId(3003L);
|
order.setAppUserId(88L);
|
order.setChargingGunId(5);
|
order.setStatus(5);
|
order.setCode("CH20261014001");
|
order.setResidualAmount(new BigDecimal("20.00"));
|
when(mapper.selectOne(any())).thenReturn(order);
|
stubGunPileAndSite();
|
when(uploadRealTimeMonitoringDataService.getLastDataById("CH20261014001")).thenReturn(null);
|
|
ChargingDetails details = service.getChargingDetails(5);
|
|
assertNotNull(details, "订单已结束时应返回完成态而非 null");
|
assertEquals("3003", details.getId());
|
assertEquals(Integer.valueOf(5), details.getStatus());
|
assertEquals("CH20261014001", details.getCode());
|
assertEquals(0, new BigDecimal("20.00").compareTo(details.getChargingCost()));
|
|
@SuppressWarnings("unchecked")
|
ArgumentCaptor<Wrapper<TChargingOrder>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
verify(mapper).selectOne(captor.capture());
|
assertStatusFilterCoversFinished(captor.getValue());
|
}
|
|
@Test
|
void getChargingDetailsReturnsFinishedVinOrderAndQueriesStatus5() {
|
// VIN 码订单结束后同样需要返回完成态,兜底查询的状态过滤必须与主查询一致
|
LoginUserApplet loginUser = new LoginUserApplet();
|
loginUser.setUserId(88L);
|
when(tokenService.getLoginUserApplet()).thenReturn(loginUser);
|
when(mapper.selectOne(any())).thenReturn(null);
|
TVinChargingOrder vinOrder = new TVinChargingOrder();
|
vinOrder.setId(1001L);
|
vinOrder.setAppUserId(88L);
|
vinOrder.setChargingGunId(5);
|
vinOrder.setStatus(5);
|
vinOrder.setCode("VD20261014001");
|
vinOrder.setResidualAmount(new BigDecimal("66.60"));
|
when(vinChargingOrderMapper.selectOne(any())).thenReturn(vinOrder);
|
stubGunPileAndSite();
|
when(uploadRealTimeMonitoringDataService.getLastDataById("VD20261014001")).thenReturn(null);
|
|
ChargingDetails details = service.getChargingDetails(5);
|
|
assertNotNull(details, "VIN 订单已结束时应返回完成态而非 null");
|
assertEquals(Integer.valueOf(5), details.getStatus());
|
assertEquals("VD20261014001", details.getCode());
|
|
@SuppressWarnings("unchecked")
|
ArgumentCaptor<Wrapper<TVinChargingOrder>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
verify(vinChargingOrderMapper).selectOne(captor.capture());
|
assertStatusFilterCoversFinished(captor.getValue());
|
}
|
|
/** getChargingDetails 组装链路依赖的充电枪/充电桩/站点远程调用 stub */
|
private void stubGunPileAndSite() {
|
TChargingGun gun = new TChargingGun();
|
gun.setChargingPileId(7);
|
when(chargingGunClient.getChargingGunById(5)).thenReturn(R.ok(gun));
|
TChargingPile pile = new TChargingPile();
|
pile.setName("1号桩");
|
pile.setSiteId(9);
|
when(chargingPileClient.getChargingPileById(7)).thenReturn(R.ok(pile));
|
Site site = new Site();
|
site.setName("城东站");
|
when(siteClient.getSiteByIds(Arrays.asList(9))).thenReturn(R.ok(Collections.singletonList(site)));
|
}
|
|
/**
|
* 断言订单查询条件中的 status 过滤覆盖 2、3、4、5。
|
* 已结束(5) 必须在过滤范围内,否则小程序结束充电后拿不到订单,会一直停留在「正在结束充电」提示。
|
*/
|
private void assertStatusFilterCoversFinished(Wrapper<?> wrapper) {
|
AbstractWrapper<?, ?, ?> queryWrapper = (AbstractWrapper<?, ?, ?>) wrapper;
|
Matcher inMatcher = Pattern.compile("status\\s+in\\s*\\(([^)]*)\\)", Pattern.CASE_INSENSITIVE)
|
.matcher(queryWrapper.getSqlSegment());
|
assertTrue(inMatcher.find(), "查询条件中应存在 status IN 过滤");
|
List<Object> statusValues = new ArrayList<>();
|
Matcher placeholderMatcher = Pattern.compile("MPGENVAL\\d+").matcher(inMatcher.group(1));
|
while (placeholderMatcher.find()) {
|
statusValues.add(queryWrapper.getParamNameValuePairs().get(placeholderMatcher.group()));
|
}
|
assertTrue(statusValues.containsAll(Arrays.asList(2, 3, 4, 5)),
|
"status 过滤应包含 2,3,4,5,实际为:" + statusValues);
|
}
|
}
|