ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -1912,6 +1912,18 @@ }) public AjaxResult stopCharging(@PathVariable String id) { log.info("小程序主动发起停止充电,订单ID:{}", id); // VIN 码充电订单路由:VD 订单存于 t_vin_charging_order,不在 t_charging_order; // VD 表存在该 ID 订单时优先走 VIN 停充(内部按状态返回准确的“不能重复操作/还未开始充电”), // 其余情况回落普通停充。雪花 ID 两表同源生成,撞号概率可忽略 TVinChargingOrder vinOrder = null; try { vinOrder = vinChargingOrderService.getById(Long.valueOf(id)); } catch (NumberFormatException ignored) { // 非数字订单ID,按普通停充处理 } if (vinOrder != null) { return vinChargingOrderService.stopChargingByApplet(id); } return chargingOrderService.stopCharging(id); } ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinChargingOrderService.java
@@ -37,6 +37,13 @@ AjaxResult stopCharging(String chargingOrderId, Integer chargingGunId); /** * 小程序端停止VIN码充电订单(正在充电界面调用): * 复用 getAuthorizedAppletOrder 鉴权(VIN当前归属+下单用户一致),仅状态3允许停止, * 置状态4/结束方式1/结束时间并下发平台停机指令 */ AjaxResult stopChargingByApplet(String chargingOrderId); List<Map<String, Object>> accountingStrategyList(Long chargingOrderId); // ===== 设备侧VIN码充电链路(V0.2 VIN码充电) ===== ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -56,6 +56,7 @@ import com.ruoyi.order.mapper.TChargingOrderMapper; import com.ruoyi.order.mapper.TChargingOrderShareRecordMapper; import com.ruoyi.order.mapper.TSettlementConfirmMapper; import com.ruoyi.order.mapper.TVinChargingOrderMapper; import com.ruoyi.order.service.TChargingOrderAccountingStrategyService; import com.ruoyi.order.service.TChargingOrderRefundService; import com.ruoyi.order.service.TChargingOrderService; @@ -251,6 +252,9 @@ @Resource private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService; @Resource private TVinChargingOrderMapper vinChargingOrderMapper; @Resource private TCECClient tcecClient; @@ -2767,7 +2771,19 @@ .orderByDesc(TChargingOrder::getCreateTime) .last(" limit 0, 1")); if (null == one || !one.getAppUserId().equals(userId)) { return null; // VIN 码充电订单兜底:VD 订单存于 t_vin_charging_order,且后付模式无支付动作, // 不按 rechargePaymentStatus 过滤;TVinChargingOrder 继承 TChargingOrder,后续组装原样复用 TVinChargingOrder vinOrder = vinChargingOrderMapper.selectOne(new LambdaQueryWrapper<TVinChargingOrder>() .eq(TVinChargingOrder::getAppUserId, userId) .eq(TVinChargingOrder::getChargingGunId, id) .in(TVinChargingOrder::getStatus, Arrays.asList(2, 3, 4)) .eq(TVinChargingOrder::getDelFlag, 0) .orderByDesc(TVinChargingOrder::getCreateTime) .last(" limit 0, 1")); if (null == vinOrder) { return null; } one = vinOrder; } ChargingDetails chargingDetails = new ChargingDetails(); chargingDetails.setId(one.getId().toString()); @@ -8058,6 +8074,14 @@ public EndOfChargePageInfo getEndOfChargePageInfo(String id) { EndOfChargePageInfo info = new EndOfChargePageInfo(); TChargingOrder chargingOrder = this.getById(id); if (chargingOrder == null) { // VIN 码充电订单兜底:VD 订单存于 t_vin_charging_order(继承 TChargingOrder,字段兼容; // 结算已写 paymentAmount/startTime/endTime,后续组装原样复用) chargingOrder = vinChargingOrderMapper.selectById(id); } if (chargingOrder == null) { throw new RuntimeException("充电订单不存在"); } Long userId = tokenService.getLoginUserApplet().getUserId(); if (!chargingOrder.getAppUserId().equals(userId)) { throw new RuntimeException("权限不足"); ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
@@ -432,6 +432,42 @@ } @Override public AjaxResult stopChargingByApplet(String chargingOrderId) { Long orderId; try { orderId = Long.valueOf(chargingOrderId); } catch (NumberFormatException e) { return AjaxResult.error("VIN码充电订单ID无效"); } TVinChargingOrder order = getAuthorizedAppletOrder(orderId); if (order == null) { return AjaxResult.error("VIN码充电订单不存在或无权操作"); } Integer status = order.getStatus(); if (status == null || !status.equals(3)) { return AjaxResult.error(status != null && (status.equals(4) || status.equals(5)) ? "不能重复操作" : "还未开始充电"); } TChargingGun chargingGun = chargingGunClient.getChargingGunById(order.getChargingGunId()).getData(); TChargingPile chargingPile = order.getChargingPileId() == null ? null : chargingPileClient.getChargingPileById(order.getChargingPileId()).getData(); if (chargingGun == null || chargingPile == null) { return AjaxResult.error("未找到订单对应的充电设备"); } order.setStatus(4); order.setEndMode(1); order.setEndTime(LocalDateTime.now()); if (!updateById(order)) { return AjaxResult.error("更新VIN码充电订单状态失败"); } PlatformStopCharging command = new PlatformStopCharging(); command.setCharging_pile_code(chargingPile.getCode()); command.setCharging_gun_code(chargingGun.getCode()); sendMessageClient.platformStopCharging(command); return AjaxResult.success(); } @Override public List<Map<String, Object>> accountingStrategyList(Long chargingOrderId) { TVinChargingOrder order = getAuthorizedOrder(chargingOrderId); return order == null ? Collections.emptyList() : baseMapper.accountingStrategyList(chargingOrderId); ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplVinFallbackTest.java
New file @@ -0,0 +1,194 @@ 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.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.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.Arrays; import java.util.Collections; 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.mockito.ArgumentMatchers.any; 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 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()); } } ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TVinChargingOrderAppletServiceTest.java
@@ -2,9 +2,15 @@ import com.ruoyi.account.api.feignClient.VinManagementClient; 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.TChargingGun; import com.ruoyi.chargingPile.api.model.TChargingPile; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.web.domain.AjaxResult; import com.ruoyi.common.core.web.page.PageInfo; import com.ruoyi.integration.api.feignClient.SendMessageClient; import com.ruoyi.integration.api.model.PlatformStopCharging; import com.ruoyi.system.api.model.LoginUserApplet; import com.ruoyi.common.security.service.TokenService; import com.ruoyi.order.api.model.TVinChargingOrder; @@ -25,7 +31,10 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -48,6 +57,10 @@ private SiteClient siteClient; @Mock private ChargingGunClient chargingGunClient; @Mock private ChargingPileClient chargingPileClient; @Mock private SendMessageClient sendMessageClient; @Mock private UploadRealTimeMonitoringDataService monitoringDataService; @@ -149,4 +162,72 @@ assertTrue(service.appletDetail(1001L).isEmpty()); verify(mapper, never()).accountingStrategyList(1001L); } @Test void stopChargingByAppletStopsChargingOrderForBoundVinOwner() { LoginUserApplet loginUser = new LoginUserApplet(); loginUser.setUserId(88L); when(tokenService.getLoginUserApplet()).thenReturn(loginUser); when(vinManagementClient.getVinIdsByAppUserId(88L)).thenReturn(R.ok(Collections.singletonList(11L))); TVinChargingOrder order = new TVinChargingOrder(); order.setId(1001L); order.setVinId(11L); order.setAppUserId(88L); order.setStatus(3); order.setChargingGunId(5); order.setChargingPileId(7); when(mapper.selectById(1001L)).thenReturn(order); TChargingGun gun = new TChargingGun(); gun.setCode("GUN_5"); when(chargingGunClient.getChargingGunById(5)).thenReturn(R.ok(gun)); TChargingPile pile = new TChargingPile(); pile.setCode("PILE_7"); when(chargingPileClient.getChargingPileById(7)).thenReturn(R.ok(pile)); when(mapper.updateById(order)).thenReturn(1); AjaxResult result = service.stopChargingByApplet("1001"); assertTrue(result.isSuccess()); assertEquals(4, order.getStatus()); assertEquals(1, order.getEndMode()); assertNotNull(order.getEndTime()); verify(sendMessageClient).platformStopCharging(any(PlatformStopCharging.class)); } @Test void stopChargingByAppletRejectsWhenVinNotBoundToCurrentUser() { LoginUserApplet loginUser = new LoginUserApplet(); loginUser.setUserId(88L); when(tokenService.getLoginUserApplet()).thenReturn(loginUser); when(vinManagementClient.getVinIdsByAppUserId(88L)).thenReturn(R.ok(Collections.emptyList())); TVinChargingOrder order = new TVinChargingOrder(); order.setId(1001L); order.setVinId(11L); when(mapper.selectById(1001L)).thenReturn(order); AjaxResult result = service.stopChargingByApplet("1001"); assertFalse(result.isSuccess()); verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class)); verify(mapper, never()).updateById(any(TVinChargingOrder.class)); } @Test void stopChargingByAppletRejectsFinishedOrder() { LoginUserApplet loginUser = new LoginUserApplet(); loginUser.setUserId(88L); when(tokenService.getLoginUserApplet()).thenReturn(loginUser); when(vinManagementClient.getVinIdsByAppUserId(88L)).thenReturn(R.ok(Collections.singletonList(11L))); TVinChargingOrder order = new TVinChargingOrder(); order.setId(1001L); order.setVinId(11L); order.setAppUserId(88L); order.setStatus(5); when(mapper.selectById(1001L)).thenReturn(order); AjaxResult result = service.stopChargingByApplet("1001"); assertFalse(result.isSuccess()); verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class)); } }