puzhibing
3 小时以前 26795c6ce013bb204fa57a8324db7a7c77aea5b8
修改bug
1个文件已添加
3个文件已修改
337 ■■■■■ 已修改文件
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TCardChargingOrderController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderStopChargingTest.java 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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("卡片收支明细列表")
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);
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();
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderStopChargingTest.java
New file
@@ -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));
    }
}