From 26795c6ce013bb204fa57a8324db7a7c77aea5b8 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期三, 14 十月 2026 14:07:09 +0800
Subject: [PATCH] 修改bug

---
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TCardChargingOrderController.java         |   10 +
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java      |   83 +++++++++++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java               |    7 +
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderStopChargingTest.java |  237 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 337 insertions(+), 0 deletions(-)

diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TCardChargingOrderController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TCardChargingOrderController.java
index 30c0077..4ecbfc3 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TCardChargingOrderController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TCardChargingOrderController.java
@@ -5,6 +5,7 @@
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.utils.DateUtils;
 import com.ruoyi.common.core.utils.WebUtils;
+import com.ruoyi.common.core.web.domain.AjaxResult;
 import com.ruoyi.common.core.web.page.PageInfo;
 import com.ruoyi.common.log.annotation.Log;
 import com.ruoyi.common.log.enums.BusinessType;
@@ -128,6 +129,15 @@
         return R.ok(Collections.singletonMap("list", list));
     }
 
+    @RequiresPermissions("/chargingGun/end_charging")
+    @PutMapping("/stopCharging")
+    @ApiOperation("停止刷卡充电")
+    @Log(title = "刷卡充电订单停止充电", businessType = BusinessType.OTHER)
+    public AjaxResult stopCharging(@RequestParam("chargingOrderId") String chargingOrderId,
+                                   @RequestParam("chargingGunId") Integer chargingGunId) {
+        return cardChargingOrderService.stopCharging(chargingOrderId, chargingGunId);
+    }
+
     @RequiresPermissions("/chargingCardBalanceDetail/list")
     @GetMapping("/balance-detail/list")
     @ApiOperation("卡片收支明细列表")
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java
index fc391b0..b0c2b41 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java
@@ -1,5 +1,6 @@
 package com.ruoyi.order.service;
 
+import com.ruoyi.common.core.web.domain.AjaxResult;
 import com.ruoyi.common.core.web.page.PageInfo;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.ruoyi.common.core.domain.R;
@@ -34,6 +35,12 @@
 
     List<Map<String, Object>> accountingStrategyList(Long chargingOrderId);
 
+    /**
+     * 管理后台停止刷卡充电:校验订单(数据范围/充电枪/状态),
+     * 条件更新为停止中(4)+结束方式1,再下发平台停止充电命令。
+     */
+    AjaxResult stopCharging(String chargingOrderId, Integer chargingGunId);
+
     PageInfo<CardBalanceDetailVO> balanceDetailPage(CardBalanceDetailQuery query);
 
     Map<String, Object> balanceDetailSummary(CardBalanceDetailQuery query);
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
index 8194c22..ca6c00f 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
@@ -26,6 +26,7 @@
 import com.ruoyi.common.core.utils.DateUtils;
 import com.ruoyi.common.core.utils.StringUtils;
 import com.ruoyi.common.core.utils.bean.BeanUtils;
+import com.ruoyi.common.core.web.domain.AjaxResult;
 import com.ruoyi.common.core.web.page.PageInfo;
 import com.ruoyi.common.security.service.TokenService;
 import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
@@ -370,6 +371,65 @@
         return baseMapper.accountingStrategyList(chargingOrderId);
     }
 
+    /**
+     * 管理后台停止刷卡充电:与 VIN 码停充口径一致,
+     * 状态推进使用条件更新(仅当前状态为充电中时生效),防止重复点击/设备回调并发。
+     */
+    @Override
+    public AjaxResult stopCharging(String chargingOrderId, Integer chargingGunId) {
+        Long orderId;
+        try {
+            orderId = Long.valueOf(chargingOrderId);
+        } catch (NumberFormatException e) {
+            return AjaxResult.error("刷卡充电订单ID无效");
+        }
+        TCardChargingOrder order = getAuthorizedOrder(orderId);
+        if (order == null) {
+            return AjaxResult.error("刷卡充电订单不存在或无权操作");
+        }
+        if (order.getChargingGunId() == null || !order.getChargingGunId().equals(chargingGunId)) {
+            return AjaxResult.error("充电枪与订单不匹配");
+        }
+        Integer status = order.getStatus();
+        if (status == null || status != ORDER_STATUS_CHARGING) {
+            return AjaxResult.error(status != null && (status == ORDER_STATUS_STOPPING || status == ORDER_STATUS_FINISHED)
+                    ? "不能重复操作" : "还未开始充电");
+        }
+        TChargingGun chargingGun;
+        TChargingPile chargingPile;
+        try {
+            R<TChargingGun> gunResult = chargingGunClient.getChargingGunById(chargingGunId);
+            chargingGun = gunResult == null ? null : gunResult.getData();
+            R<TChargingPile> pileResult = order.getChargingPileId() == null ? null
+                    : chargingPileClient.getChargingPileById(order.getChargingPileId());
+            chargingPile = pileResult == null ? null : pileResult.getData();
+        } catch (Exception ex) {
+            log.error("[SD停充] 管理后台停充查询设备异常 code={} gunId={}", order.getCode(), chargingGunId, ex);
+            return AjaxResult.error("查询充电设备失败");
+        }
+        if (chargingGun == null || chargingPile == null) {
+            return AjaxResult.error("未找到订单对应的充电设备");
+        }
+        boolean updated = lambdaUpdate()
+                .eq(TCardChargingOrder::getId, order.getId())
+                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING)
+                .set(TCardChargingOrder::getStatus, ORDER_STATUS_STOPPING)
+                .set(TCardChargingOrder::getEndMode, 1)
+                .set(TCardChargingOrder::getEndTime, LocalDateTime.now())
+                .update();
+        if (!updated) {
+            return AjaxResult.error("订单状态已变化,请刷新后重试");
+        }
+        order.setStatus(ORDER_STATUS_STOPPING);
+        order.setEndMode(1);
+        PlatformStopCharging command = new PlatformStopCharging();
+        command.setCharging_pile_code(chargingPile.getCode());
+        command.setCharging_gun_code(chargingGun.getCode());
+        sendMessageClient.platformStopCharging(command);
+        log.warn("[SD停充] 管理后台主动停充 code={} gunId={}", order.getCode(), chargingGunId);
+        return AjaxResult.success();
+    }
+
     @Override
     public PageInfo<CardBalanceDetailVO> balanceDetailPage(CardBalanceDetailQuery query) {
         int pageCurr = query.getPageCurr() == null || query.getPageCurr() < 1 ? 1 : query.getPageCurr();
@@ -437,6 +497,29 @@
         query.setSiteIds(siteIds);
     }
 
+    /**
+     * 按订单ID查询并校验数据范围(订单所属站点须在当前登录用户授权站点内),与 VIN 码停充校验口径一致
+     */
+    private TCardChargingOrder getAuthorizedOrder(Long chargingOrderId) {
+        if (chargingOrderId == null) {
+            return null;
+        }
+        TCardChargingOrder order = getById(chargingOrderId);
+        if (order == null || order.getSiteId() == null || !getAuthorizedSiteIds().contains(order.getSiteId())) {
+            return null;
+        }
+        return order;
+    }
+
+    private List<Integer> getAuthorizedSiteIds() {
+        Long userId = tokenService.getLoginUser().getUserid();
+        if (userId == null) {
+            return Collections.emptyList();
+        }
+        return Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData())
+                .orElse(Collections.emptyList()).stream().map(GetSiteListDTO::getId).collect(Collectors.toList());
+    }
+
     private void setAuthSiteIds(CardBalanceDetailQuery query) {
         List<Integer> siteIds = new ArrayList<>();
         Long userId = tokenService.getLoginUser().getUserid();
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderStopChargingTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderStopChargingTest.java
new file mode 100644
index 0000000..faabfc1
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderStopChargingTest.java
@@ -0,0 +1,237 @@
+package com.ruoyi.order.service.impl;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
+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.security.service.TokenService;
+import com.ruoyi.integration.api.feignClient.SendMessageClient;
+import com.ruoyi.integration.api.model.PlatformStopCharging;
+import com.ruoyi.order.api.model.TCardChargingOrder;
+import com.ruoyi.order.mapper.TCardChargingOrderMapper;
+import com.ruoyi.system.api.model.LoginUser;
+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.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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;
+
+/**
+ * 管理后台刷卡充电订单「停止充电」单测。
+ * 覆盖:数据范围校验、充电枪匹配、状态拦截、条件更新并发保护、停机指令下发。
+ */
+@ExtendWith(MockitoExtension.class)
+class TCardChargingOrderStopChargingTest {
+
+    private static final Long LOGIN_USER_ID = 9L;
+    private static final Integer AUTHORIZED_SITE_ID = 7;
+    private static final Long ORDER_ID = 11L;
+    private static final Integer GUN_ID = 22;
+    private static final Integer PILE_ID = 33;
+
+    @InjectMocks
+    private TCardChargingOrderServiceImpl service;
+    @Mock
+    private TCardChargingOrderMapper mapper;
+    @Mock
+    private TokenService tokenService;
+    @Mock
+    private SiteClient siteClient;
+    @Mock
+    private ChargingGunClient chargingGunClient;
+    @Mock
+    private ChargingPileClient chargingPileClient;
+    @Mock
+    private SendMessageClient sendMessageClient;
+
+    @BeforeEach
+    void setUp() {
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        // lambdaUpdate 解析 getter 引用依赖 MyBatis-Plus 实体元信息缓存,单测环境需显式初始化
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
+                TCardChargingOrder.class);
+        // 默认:条件更新成功
+        lenient().when(mapper.update(any(), any())).thenReturn(1);
+    }
+
+    // ===== 一、正常停充 =====
+
+    @Test
+    void stopChargingAdvancesStatusAndSendsCommand() {
+        TCardChargingOrder order = order(3);
+        when(mapper.selectById(ORDER_ID)).thenReturn(order);
+        mockAuthorizedLogin();
+        mockDevice(order);
+
+        AjaxResult result = service.stopCharging(String.valueOf(ORDER_ID), GUN_ID);
+
+        assertEquals(Integer.valueOf(200), result.get("code"));
+        ArgumentCaptor<PlatformStopCharging> captor = ArgumentCaptor.forClass(PlatformStopCharging.class);
+        verify(sendMessageClient).platformStopCharging(captor.capture());
+        assertEquals("PILE-33", captor.getValue().getCharging_pile_code());
+        assertEquals("GUN-22", captor.getValue().getCharging_gun_code());
+        assertEquals(Integer.valueOf(4), order.getStatus());
+        assertEquals(Integer.valueOf(1), order.getEndMode());
+        verify(mapper).update(any(), any());
+    }
+
+    // ===== 二、入参与订单校验 =====
+
+    @Test
+    void rejectsInvalidOrderId() {
+        AjaxResult result = service.stopCharging("abc", GUN_ID);
+
+        assertEquals(Integer.valueOf(500), result.get("code"));
+        assertEquals("刷卡充电订单ID无效", result.get("msg"));
+        verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class));
+    }
+
+    @Test
+    void rejectsOrderNotFound() {
+        when(mapper.selectById(ORDER_ID)).thenReturn(null);
+
+        AjaxResult result = service.stopCharging(String.valueOf(ORDER_ID), GUN_ID);
+
+        assertEquals(Integer.valueOf(500), result.get("code"));
+        assertEquals("刷卡充电订单不存在或无权操作", result.get("msg"));
+        verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class));
+    }
+
+    @Test
+    void rejectsOrderOutsideAuthorizationScope() {
+        TCardChargingOrder order = order(3);
+        order.setSiteId(AUTHORIZED_SITE_ID + 1);
+        when(mapper.selectById(ORDER_ID)).thenReturn(order);
+        mockAuthorizedLogin();
+
+        AjaxResult result = service.stopCharging(String.valueOf(ORDER_ID), GUN_ID);
+
+        assertEquals(Integer.valueOf(500), result.get("code"));
+        assertEquals("刷卡充电订单不存在或无权操作", result.get("msg"));
+        verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class));
+    }
+
+    @Test
+    void rejectsMismatchedGun() {
+        TCardChargingOrder order = order(3);
+        when(mapper.selectById(ORDER_ID)).thenReturn(order);
+        mockAuthorizedLogin();
+
+        AjaxResult result = service.stopCharging(String.valueOf(ORDER_ID), GUN_ID + 1);
+
+        assertEquals(Integer.valueOf(500), result.get("code"));
+        assertEquals("充电枪与订单不匹配", result.get("msg"));
+        verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class));
+    }
+
+    // ===== 三、状态拦截 =====
+
+    @Test
+    void rejectsOrderNotStartedCharge() {
+        TCardChargingOrder order = order(2);
+        when(mapper.selectById(ORDER_ID)).thenReturn(order);
+        mockAuthorizedLogin();
+
+        AjaxResult result = service.stopCharging(String.valueOf(ORDER_ID), GUN_ID);
+
+        assertEquals(Integer.valueOf(500), result.get("code"));
+        assertEquals("还未开始充电", result.get("msg"));
+        verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class));
+    }
+
+    @Test
+    void rejectsRepeatedStopOperation() {
+        TCardChargingOrder order = order(4);
+        when(mapper.selectById(ORDER_ID)).thenReturn(order);
+        mockAuthorizedLogin();
+
+        AjaxResult result = service.stopCharging(String.valueOf(ORDER_ID), GUN_ID);
+
+        assertEquals(Integer.valueOf(500), result.get("code"));
+        assertEquals("不能重复操作", result.get("msg"));
+        verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class));
+    }
+
+    // ===== 四、设备与并发 =====
+
+    @Test
+    void rejectsWhenDeviceMissing() {
+        TCardChargingOrder order = order(3);
+        when(mapper.selectById(ORDER_ID)).thenReturn(order);
+        mockAuthorizedLogin();
+
+        AjaxResult result = service.stopCharging(String.valueOf(ORDER_ID), GUN_ID);
+
+        assertEquals(Integer.valueOf(500), result.get("code"));
+        assertEquals("未找到订单对应的充电设备", result.get("msg"));
+        verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class));
+    }
+
+    @Test
+    void rejectsWhenStatusChangedConcurrently() {
+        lenient().when(mapper.update(any(), any())).thenReturn(0);
+        TCardChargingOrder order = order(3);
+        when(mapper.selectById(ORDER_ID)).thenReturn(order);
+        mockAuthorizedLogin();
+        mockDevice(order);
+
+        AjaxResult result = service.stopCharging(String.valueOf(ORDER_ID), GUN_ID);
+
+        assertEquals(Integer.valueOf(500), result.get("code"));
+        assertEquals("订单状态已变化,请刷新后重试", result.get("msg"));
+        verify(sendMessageClient, never()).platformStopCharging(any(PlatformStopCharging.class));
+    }
+
+    // ===== 辅助方法 =====
+
+    private TCardChargingOrder order(Integer status) {
+        TCardChargingOrder order = new TCardChargingOrder();
+        order.setId(ORDER_ID);
+        order.setCode("SD202610140001");
+        order.setSiteId(AUTHORIZED_SITE_ID);
+        order.setChargingGunId(GUN_ID);
+        order.setChargingPileId(PILE_ID);
+        order.setStatus(status);
+        return order;
+    }
+
+    private void mockAuthorizedLogin() {
+        LoginUser loginUser = new LoginUser();
+        loginUser.setUserid(LOGIN_USER_ID);
+        lenient().when(tokenService.getLoginUser()).thenReturn(loginUser);
+        GetSiteListDTO site = new GetSiteListDTO();
+        site.setId(AUTHORIZED_SITE_ID);
+        lenient().when(siteClient.getSiteListByUserId(LOGIN_USER_ID))
+                .thenReturn(R.ok(Collections.singletonList(site)));
+    }
+
+    private void mockDevice(TCardChargingOrder order) {
+        TChargingGun gun = new TChargingGun();
+        gun.setId(order.getChargingGunId());
+        gun.setCode("GUN-" + order.getChargingGunId());
+        lenient().when(chargingGunClient.getChargingGunById(order.getChargingGunId())).thenReturn(R.ok(gun));
+        TChargingPile pile = new TChargingPile();
+        pile.setId(order.getChargingPileId());
+        pile.setCode("PILE-" + order.getChargingPileId());
+        lenient().when(chargingPileClient.getChargingPileById(order.getChargingPileId())).thenReturn(R.ok(pile));
+    }
+}

--
Gitblit v1.7.1