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> 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> 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 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); } }