From e665bc906ba6ef715acfa55ebe3ad2370154626d Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期二, 29 九月 2026 19:18:35 +0800
Subject: [PATCH] 修改VIN码订单流水

---
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java                          |   24 ++
 ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/service/impl/TFleetServiceImplTest.java                                 |   99 +++++++++++
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FleetUnbindController.java                               |   37 ++++
 ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/FleetUnbindServiceImplTest.java                        |  157 +++++++++++++++++
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/FleetUnbindClient.java                              |   28 +++
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/FleetUnbindFallbackFactory.java                         |   29 +++
 ruoyi-api/ruoyi-api-account/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |    3 
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/FleetUnbindServiceImpl.java                            |   74 ++++++++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TFleetService.java                                              |    9 +
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TFleetServiceImpl.java                                     |   31 +++
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/FleetUnbindService.java                                     |   21 ++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TFleetController.java                                        |    2 
 12 files changed, 511 insertions(+), 3 deletions(-)

diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/FleetUnbindFallbackFactory.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/FleetUnbindFallbackFactory.java
new file mode 100644
index 0000000..4b563a1
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/FleetUnbindFallbackFactory.java
@@ -0,0 +1,29 @@
+package com.ruoyi.account.api.factory;
+
+import com.ruoyi.account.api.feignClient.FleetUnbindClient;
+import com.ruoyi.common.core.domain.R;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.openfeign.FallbackFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 车队解绑服务降级处理
+ */
+@Component
+public class FleetUnbindFallbackFactory implements FallbackFactory<FleetUnbindClient> {
+    private static final Logger log = LoggerFactory.getLogger(FleetUnbindFallbackFactory.class);
+
+    @Override
+    public FleetUnbindClient create(Throwable throwable) {
+        log.error("车队解绑服务调用失败:{}", throwable.getMessage());
+        return new FleetUnbindClient() {
+            @Override
+            public R<Integer> unbindByFleetIds(List<Integer> fleetIds) {
+                return R.fail("车队关联数据解绑失败:" + throwable.getMessage());
+            }
+        };
+    }
+}
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/FleetUnbindClient.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/FleetUnbindClient.java
new file mode 100644
index 0000000..e890d1f
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/FleetUnbindClient.java
@@ -0,0 +1,28 @@
+package com.ruoyi.account.api.feignClient;
+
+import com.ruoyi.account.api.factory.FleetUnbindFallbackFactory;
+import com.ruoyi.common.core.constant.ServiceNameConstants;
+import com.ruoyi.common.core.domain.R;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.util.List;
+
+/**
+ * 车队解绑内部服务(车队删除时批量清空 VIN码/充电卡/白名单的车队归属)
+ * <p>仅限内部服务调用(other 服务删除车队链路),禁止走管理后台权限体系。</p>
+ */
+@FeignClient(contextId = "FleetUnbindClient", value = ServiceNameConstants.ACCOUNT_SERVICE,
+        fallbackFactory = FleetUnbindFallbackFactory.class)
+public interface FleetUnbindClient {
+
+    /**
+     * 按车队ID批量解绑(单事务内清空 t_vin_management / t_charging_card / t_app_user_whitelist 的 fleet_id)
+     *
+     * @param fleetIds 车队ID列表(自动去重、过滤空值)
+     * @return 实际解绑的记录总数(三表之和,0 表示无挂靠数据)
+     */
+    @PostMapping("/fleet-unbind/inner/unbind")
+    R<Integer> unbindByFleetIds(@RequestBody List<Integer> fleetIds);
+}
diff --git a/ruoyi-api/ruoyi-api-account/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-api/ruoyi-api-account/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index eb779e9..178263a 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/ruoyi-api/ruoyi-api-account/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -9,4 +9,5 @@
 com.ruoyi.account.api.factory.JianGuanAccountFallbackFactory
 com.ruoyi.account.api.factory.VinManagementFallbackFactory
 com.ruoyi.account.api.factory.ChargingCardFallbackFactory
-com.ruoyi.account.api.factory.CompanyAllocateRecordFallbackFactory
\ No newline at end of file
+com.ruoyi.account.api.factory.CompanyAllocateRecordFallbackFactory
+com.ruoyi.account.api.factory.FleetUnbindFallbackFactory
\ No newline at end of file
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FleetUnbindController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FleetUnbindController.java
new file mode 100644
index 0000000..6ac57ef
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/FleetUnbindController.java
@@ -0,0 +1,37 @@
+package com.ruoyi.account.controller;
+
+import com.ruoyi.account.service.FleetUnbindService;
+import com.ruoyi.common.core.domain.R;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 车队解绑内部接口 前端控制器(other 服务删除车队链路使用,无管理端权限)
+ * </p>
+ *
+ * @author mxcd
+ * @since 2026-09-29
+ */
+@RestController
+@RequestMapping("/fleet-unbind")
+public class FleetUnbindController {
+
+    @Autowired
+    private FleetUnbindService fleetUnbindService;
+
+    /**
+     * 内部接口:按车队ID批量解绑 VIN码/充电卡/白名单的车队归属(单事务,返回实际解绑数量)
+     */
+    @ApiOperation(value = "内部接口:按车队批量解绑(清空三表 fleet_id,返回实际解绑数量)")
+    @PostMapping("/inner/unbind")
+    public R<Integer> unbindByFleetIds(@RequestBody List<Integer> fleetIds) {
+        return R.ok(fleetUnbindService.unbindByFleetIds(fleetIds));
+    }
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/FleetUnbindService.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/FleetUnbindService.java
new file mode 100644
index 0000000..e8645d5
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/FleetUnbindService.java
@@ -0,0 +1,21 @@
+package com.ruoyi.account.service;
+
+import java.util.List;
+
+/**
+ * 车队解绑服务(车队删除时批量清空 VIN码/充电卡/白名单的车队归属)
+ *
+ * @author mxcd
+ * @since 2026-09-29
+ */
+public interface FleetUnbindService {
+
+    /**
+     * 按车队ID批量解绑:单事务内清空 t_vin_management / t_charging_card / t_app_user_whitelist 三表的 fleet_id,
+     * 仅清空车队归属,公司、余额、锁定额度、状态等不受影响;逻辑删除数据自动过滤
+     *
+     * @param fleetIds 车队ID列表(自动去重、过滤空值,空列表直接返回 0)
+     * @return 实际解绑的记录总数(三表影响行数之和)
+     */
+    int unbindByFleetIds(List<Integer> fleetIds);
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/FleetUnbindServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/FleetUnbindServiceImpl.java
new file mode 100644
index 0000000..8a64cfa
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/FleetUnbindServiceImpl.java
@@ -0,0 +1,74 @@
+package com.ruoyi.account.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.ruoyi.account.api.model.TAppUserWhitelist;
+import com.ruoyi.account.api.model.TChargingCard;
+import com.ruoyi.account.api.model.TVinManagement;
+import com.ruoyi.account.mapper.TAppUserWhitelistMapper;
+import com.ruoyi.account.mapper.TChargingCardMapper;
+import com.ruoyi.account.mapper.TVinManagementMapper;
+import com.ruoyi.account.service.FleetUnbindService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * 车队解绑服务实现(内部链路:other 服务删除车队时触发,无管理端用户上下文)
+ * </p>
+ *
+ * @author mxcd
+ * @since 2026-09-29
+ */
+@Service
+public class FleetUnbindServiceImpl implements FleetUnbindService {
+
+    @Resource
+    private TVinManagementMapper vinManagementMapper;
+
+    @Resource
+    private TChargingCardMapper chargingCardMapper;
+
+    @Resource
+    private TAppUserWhitelistMapper appUserWhitelistMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int unbindByFleetIds(List<Integer> fleetIds) {
+        List<Integer> distinct = normalize(fleetIds);
+        if (distinct.isEmpty()) {
+            return 0;
+        }
+        LocalDateTime now = LocalDateTime.now();
+        int total = 0;
+        // 三表均在单事务内执行,任一失败整体回滚(调用方按失败中止车队删除,两侧数据保持一致)
+        // 显式 set null 清空车队归属(MyBatis-Plus 字段策略会忽略实体 null,需走 UpdateWrapper)
+        total += vinManagementMapper.update(null, new LambdaUpdateWrapper<TVinManagement>()
+                .in(TVinManagement::getFleetId, distinct)
+                .set(TVinManagement::getFleetId, null)
+                .set(TVinManagement::getUpdateTime, now));
+        total += chargingCardMapper.update(null, new LambdaUpdateWrapper<TChargingCard>()
+                .in(TChargingCard::getFleetId, distinct)
+                .set(TChargingCard::getFleetId, null)
+                .set(TChargingCard::getUpdateTime, now));
+        // 白名单实体无 updateTime 映射字段(表有 update_time 列),沿用现有解绑实现走 setSql
+        total += appUserWhitelistMapper.update(null, new LambdaUpdateWrapper<TAppUserWhitelist>()
+                .in(TAppUserWhitelist::getFleetId, distinct)
+                .set(TAppUserWhitelist::getFleetId, null)
+                .setSql("update_time = now()"));
+        return total;
+    }
+
+    private List<Integer> normalize(List<Integer> fleetIds) {
+        if (fleetIds == null || fleetIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return fleetIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
+    }
+}
diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/FleetUnbindServiceImplTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/FleetUnbindServiceImplTest.java
new file mode 100644
index 0000000..1138136
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/FleetUnbindServiceImplTest.java
@@ -0,0 +1,157 @@
+package com.ruoyi.account.service.impl;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.ruoyi.account.api.model.TAppUserWhitelist;
+import com.ruoyi.account.api.model.TChargingCard;
+import com.ruoyi.account.api.model.TVinManagement;
+import com.ruoyi.account.mapper.TAppUserWhitelistMapper;
+import com.ruoyi.account.mapper.TChargingCardMapper;
+import com.ruoyi.account.mapper.TVinManagementMapper;
+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.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * 车队解绑聚合服务单测(覆盖三表清空 fleet_id、去重过滤与空入参短路)。
+ * Mapper 以 Mock 替代,不依赖 Spring 容器与数据库。
+ */
+@ExtendWith(MockitoExtension.class)
+class FleetUnbindServiceImplTest {
+
+    @Mock
+    private TVinManagementMapper vinMapper;
+
+    @Mock
+    private TChargingCardMapper cardMapper;
+
+    @Mock
+    private TAppUserWhitelistMapper whitelistMapper;
+
+    private FleetUnbindServiceImpl service;
+
+    @BeforeAll
+    static void initTableInfo() {
+        // LambdaUpdateWrapper 的 lambda 列名解析依赖 TableInfo 元数据,
+        // 单元测试不启动 Spring,需要手动初始化三个实体的 TableInfo,否则 NPE
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TVinManagement.class);
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TChargingCard.class);
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TAppUserWhitelist.class);
+    }
+
+    @BeforeEach
+    void setUp() {
+        service = new FleetUnbindServiceImpl();
+        ReflectionTestUtils.setField(service, "vinManagementMapper", vinMapper);
+        ReflectionTestUtils.setField(service, "chargingCardMapper", cardMapper);
+        ReflectionTestUtils.setField(service, "appUserWhitelistMapper", whitelistMapper);
+    }
+
+    @Test
+    void unbindsAllThreeTablesAndSumsAffectedRows() {
+        when(vinMapper.update(isNull(), any())).thenReturn(2);
+        when(cardMapper.update(isNull(), any())).thenReturn(1);
+        when(whitelistMapper.update(isNull(), any())).thenReturn(3);
+
+        int result = service.unbindByFleetIds(Arrays.asList(4, 5));
+
+        assertEquals(6, result);
+        verify(vinMapper).update(isNull(), any());
+        verify(cardMapper).update(isNull(), any());
+        verify(whitelistMapper).update(isNull(), any());
+    }
+
+    @Test
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    void clearsFleetIdWithUpdateWrapperInAllTables() {
+        when(vinMapper.update(isNull(), any())).thenReturn(0);
+        when(cardMapper.update(isNull(), any())).thenReturn(0);
+        when(whitelistMapper.update(isNull(), any())).thenReturn(0);
+
+        service.unbindByFleetIds(Collections.singletonList(9));
+
+        // 三表均显式 set fleet_id = null(字段策略会忽略实体 null,必须走 UpdateWrapper set)
+        ArgumentCaptor<Wrapper> vinCaptor = ArgumentCaptor.forClass(Wrapper.class);
+        verify(vinMapper).update(isNull(), vinCaptor.capture());
+        LambdaUpdateWrapper<TVinManagement> vinWrapper = (LambdaUpdateWrapper<TVinManagement>) vinCaptor.getValue();
+        assertTrue(vinWrapper.getSqlSet().contains("fleet_id="), vinWrapper.getSqlSet());
+        assertTrue(vinWrapper.getSqlSegment().contains("IN"), vinWrapper.getSqlSegment());
+
+        ArgumentCaptor<Wrapper> cardCaptor = ArgumentCaptor.forClass(Wrapper.class);
+        verify(cardMapper).update(isNull(), cardCaptor.capture());
+        LambdaUpdateWrapper<TChargingCard> cardWrapper = (LambdaUpdateWrapper<TChargingCard>) cardCaptor.getValue();
+        assertTrue(cardWrapper.getSqlSet().contains("fleet_id="), cardWrapper.getSqlSet());
+        assertTrue(cardWrapper.getSqlSegment().contains("IN"), cardWrapper.getSqlSegment());
+
+        ArgumentCaptor<Wrapper> whitelistCaptor = ArgumentCaptor.forClass(Wrapper.class);
+        verify(whitelistMapper).update(isNull(), whitelistCaptor.capture());
+        LambdaUpdateWrapper<TAppUserWhitelist> whitelistWrapper = (LambdaUpdateWrapper<TAppUserWhitelist>) whitelistCaptor.getValue();
+        assertTrue(whitelistWrapper.getSqlSet().contains("fleet_id="), whitelistWrapper.getSqlSet());
+        assertTrue(whitelistWrapper.getSqlSegment().contains("IN"), whitelistWrapper.getSqlSegment());
+        // 白名单实体无 updateTime 映射,沿用 setSql 方式更新时间
+        assertTrue(whitelistWrapper.getSqlSet().contains("update_time = now()"), whitelistWrapper.getSqlSet());
+    }
+
+    @Test
+    void deduplicatesAndFiltersNullFleetIds() {
+        when(vinMapper.update(isNull(), any())).thenReturn(0);
+        when(cardMapper.update(isNull(), any())).thenReturn(0);
+        when(whitelistMapper.update(isNull(), any())).thenReturn(0);
+
+        service.unbindByFleetIds(Arrays.asList(4, null, 4, 5));
+
+        // 去重过滤后仅 4、5 两个绑定值参与 IN 条件
+        @SuppressWarnings({"unchecked", "rawtypes"})
+        ArgumentCaptor<Wrapper> captor = ArgumentCaptor.forClass(Wrapper.class);
+        verify(vinMapper).update(isNull(), captor.capture());
+        LambdaUpdateWrapper<TVinManagement> wrapper = (LambdaUpdateWrapper<TVinManagement>) captor.getValue();
+        assertEquals(2, wrapper.getParamNameValuePairs().size());
+    }
+
+    @Test
+    void returnsZeroWithoutTouchingTablesForNullOrEmptyInput() {
+        assertEquals(0, service.unbindByFleetIds(null));
+        assertEquals(0, service.unbindByFleetIds(Collections.emptyList()));
+        assertEquals(0, service.unbindByFleetIds(Arrays.asList(null, null)));
+
+        verifyNoInteractions(vinMapper, cardMapper, whitelistMapper);
+    }
+
+    @Test
+    void returnsZeroWhenNoBoundRecords() {
+        when(vinMapper.update(isNull(), any())).thenReturn(0);
+        when(cardMapper.update(isNull(), any())).thenReturn(0);
+        when(whitelistMapper.update(isNull(), any())).thenReturn(0);
+
+        List<Integer> fleetIds = Collections.singletonList(7);
+        int result = service.unbindByFleetIds(fleetIds);
+
+        assertEquals(0, result);
+        // 无挂靠数据时仍对三表各执行一次解绑语句(0 行受影响),保证幂等清理
+        verify(vinMapper).update(isNull(), any());
+        verify(cardMapper).update(isNull(), any());
+        verify(whitelistMapper).update(isNull(), any());
+        verify(vinMapper, never()).selectList(any());
+    }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
index 0372da3..4e260c5 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
@@ -911,7 +911,7 @@
                 //    避免无快照订单在结算时按 0 价计费):关单(状态-1)并释放锁定额度
                 if (!saveStrategySnapshot(order.getId(), strategyId)) {
                     log.error("[VD建单] 计费策略快照保存失败,终止本次启动 code={} strategyId={}", order.getCode(), strategyId);
-                    closeTimeoutStartingOrder(order);
+                    closeStartingOrderWithoutGunReset(order);
                     return DeviceStartChargingVO.fail(null);
                 }
                 log.info("[VD建单] VIN码充电订单创建成功 code={} vinId={} gun={} accountMode={} lockAmount={} maxEndTime={}",
@@ -2017,6 +2017,28 @@
         log.warn("[VD补偿] 启动超时关单 code={} vinId={} gunId={}", order.getCode(), order.getVinId(), order.getChargingGunId());
     }
 
+    /**
+     * 严格模式启动失败关单:仅关闭订单并释放锁定额度,不复位充电枪
+     * (枪可能处于离线/故障等业务态,避免误复位为可用;订单已关单,不会残留占用)
+     */
+    private void closeStartingOrderWithoutGunReset(TVinChargingOrder order) {
+        TVinChargingOrder update = new TVinChargingOrder();
+        update.setStatus(ORDER_STATUS_START_FAILED);
+        update.setEndMode(0);
+        update.setEndTime(LocalDateTime.now());
+        boolean closed = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TVinChargingOrder>()
+                .eq(TVinChargingOrder::getId, order.getId())
+                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING));
+        if (!closed) {
+            log.info("[VD建单] 严格模式关单跳过,订单状态已变化 code={}", order.getCode());
+            return;
+        }
+        // 释放申请时锁定的账户额度(未产生消费)
+        unlockVinChargeContextQuietly(order, "启动关单");
+        pushRegulatoryAsync(order, false, false);
+        log.warn("[VD建单] 严格模式启动失败关单(不复位充电枪) code={} vinId={} gunId={}", order.getCode(), order.getVinId(), order.getChargingGunId());
+    }
+
     @Override
     public void compensationSettle(TVinChargingOrder order) {
         // 无设备交易记录:以订单累计电量构造全平段账单,复用统一结算入口
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TFleetController.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TFleetController.java
index 02431bc..372c8fb 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TFleetController.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TFleetController.java
@@ -138,7 +138,7 @@
         if (idList.isEmpty()) {
             return AjaxResult.error("车队ID不能为空");
         }
-        fleetService.removeByIds(idList);
+        fleetService.deleteFleets(idList);
         return AjaxResult.success();
     }
 
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TFleetService.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TFleetService.java
index 9ad4a85..ce8cc48 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TFleetService.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TFleetService.java
@@ -54,4 +54,13 @@
      */
     void fillAggregates(List<TFleet> fleets);
 
+    /**
+     * 删除车队(先远程解绑该车队下的 VIN码/充电卡/白名单车队归属,解绑成功后再删除车队)。
+     * 解绑失败时抛出异常并中止删除,车队与绑定关系均保持一致。
+     *
+     * @param ids 车队ID列表(自动去重、过滤空值)
+     * @return 是否删除成功
+     */
+    boolean deleteFleets(List<Integer> ids);
+
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TFleetServiceImpl.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TFleetServiceImpl.java
index 4015c97..1fec705 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TFleetServiceImpl.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TFleetServiceImpl.java
@@ -3,6 +3,7 @@
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.api.feignClient.FleetUnbindClient;
 import com.ruoyi.account.api.feignClient.VinManagementClient;
 import com.ruoyi.account.api.dto.VinManagementQueryDTO;
 import com.ruoyi.account.api.model.TVinManagement;
@@ -62,6 +63,9 @@
 
     @Resource
     private TChargeAuthorizationMapper chargeAuthorizationMapper;
+
+    @Resource
+    private FleetUnbindClient fleetUnbindClient;
 
     @Override
     public Page<TVinManagement> selectVinPage(Integer fleetId, Integer current, Integer size) {
@@ -276,4 +280,31 @@
         throw new ServiceException((row > 0 ? "第" + row + "行" : "") + "启用状态错误");
     }
 
+    @Override
+    public boolean deleteFleets(List<Integer> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new ServiceException("请选择需要删除的车队");
+        }
+        List<Integer> distinct = ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        if (distinct.isEmpty()) {
+            throw new ServiceException("请选择需要删除的车队");
+        }
+        if (listByIds(distinct).size() != distinct.size()) {
+            throw new ServiceException("部分车队不存在或已删除");
+        }
+        // 先解绑该车队下的 VIN码/充电卡/白名单(跨服务无分布式事务:解绑失败即中止删除,两侧数据保持一致;
+        // 解绑成功后删除失败仅剩车队未删,可重试删除,不会产生已删车队挂靠的脏引用)
+        try {
+            R<Integer> response = fleetUnbindClient.unbindByFleetIds(distinct);
+            if (response == null || !R.isSuccess(response)) {
+                throw new ServiceException("车队关联数据解绑失败:" + (response == null ? "服务无响应" : response.getMsg()));
+            }
+        } catch (ServiceException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            throw new ServiceException("车队关联数据解绑失败,请稍后重试");
+        }
+        return removeByIds(distinct);
+    }
+
 }
\ No newline at end of file
diff --git a/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/service/impl/TFleetServiceImplTest.java b/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/service/impl/TFleetServiceImplTest.java
index dacff6a..4cddd3d 100644
--- a/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/service/impl/TFleetServiceImplTest.java
+++ b/ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/service/impl/TFleetServiceImplTest.java
@@ -2,6 +2,7 @@
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.account.api.dto.VinManagementQueryDTO;
+import com.ruoyi.account.api.feignClient.FleetUnbindClient;
 import com.ruoyi.account.api.feignClient.VinManagementClient;
 import com.ruoyi.account.api.model.TVinManagement;
 import com.ruoyi.common.core.domain.R;
@@ -21,6 +22,7 @@
 import java.math.BigDecimal;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -280,6 +282,103 @@
         return service;
     }
 
+    @Test
+    void deleteFleetsUnbindsThenRemoves() {
+        FleetUnbindClient unbindClient = Mockito.mock(FleetUnbindClient.class);
+        Mockito.when(unbindClient.unbindByFleetIds(Mockito.anyList())).thenReturn(R.ok(3));
+
+        TFleetServiceImpl service = Mockito.spy(new TFleetServiceImpl());
+        ReflectionTestUtils.setField(service, "fleetUnbindClient", unbindClient);
+        Mockito.doReturn(Arrays.asList(fleet(1), fleet(2))).when(service).listByIds(Mockito.anyList());
+        Mockito.doReturn(true).when(service).removeByIds(Mockito.anyList());
+
+        assertTrue(service.deleteFleets(Arrays.asList(1, 2)));
+
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<List<Integer>> idsCaptor = ArgumentCaptor.forClass(List.class);
+        Mockito.verify(unbindClient).unbindByFleetIds(idsCaptor.capture());
+        assertEquals(Arrays.asList(1, 2), idsCaptor.getValue());
+        Mockito.verify(service).removeByIds(Mockito.anyList());
+    }
+
+    @Test
+    void deleteFleetsDeduplicatesAndFiltersNullIds() {
+        FleetUnbindClient unbindClient = Mockito.mock(FleetUnbindClient.class);
+        Mockito.when(unbindClient.unbindByFleetIds(Mockito.anyList())).thenReturn(R.ok(0));
+
+        TFleetServiceImpl service = Mockito.spy(new TFleetServiceImpl());
+        ReflectionTestUtils.setField(service, "fleetUnbindClient", unbindClient);
+        Mockito.doReturn(Collections.singletonList(fleet(1))).when(service).listByIds(Mockito.anyList());
+        Mockito.doReturn(true).when(service).removeByIds(Mockito.anyList());
+
+        assertTrue(service.deleteFleets(Arrays.asList(1, null, 1)));
+
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<List<Integer>> idsCaptor = ArgumentCaptor.forClass(List.class);
+        Mockito.verify(unbindClient).unbindByFleetIds(idsCaptor.capture());
+        assertEquals(Collections.singletonList(1), idsCaptor.getValue());
+        Mockito.verify(service).removeByIds(Collections.singletonList(1));
+    }
+
+    @Test
+    void deleteFleetsRejectsNullOrEmptyIds() {
+        TFleetServiceImpl service = Mockito.spy(new TFleetServiceImpl());
+        ReflectionTestUtils.setField(service, "fleetUnbindClient", Mockito.mock(FleetUnbindClient.class));
+
+        ServiceException nullEx = assertThrows(ServiceException.class, () -> service.deleteFleets(null));
+        assertTrue(nullEx.getMessage().contains("请选择需要删除的车队"), nullEx.getMessage());
+        ServiceException emptyEx = assertThrows(ServiceException.class,
+                () -> service.deleteFleets(Arrays.asList(null, null)));
+        assertTrue(emptyEx.getMessage().contains("请选择需要删除的车队"), emptyEx.getMessage());
+    }
+
+    @Test
+    void deleteFleetsRejectsMissingFleetWithoutUnbinding() {
+        FleetUnbindClient unbindClient = Mockito.mock(FleetUnbindClient.class);
+
+        TFleetServiceImpl service = Mockito.spy(new TFleetServiceImpl());
+        ReflectionTestUtils.setField(service, "fleetUnbindClient", unbindClient);
+        Mockito.doReturn(Collections.emptyList()).when(service).listByIds(Mockito.anyList());
+
+        ServiceException ex = assertThrows(ServiceException.class, () -> service.deleteFleets(Collections.singletonList(9)));
+
+        assertTrue(ex.getMessage().contains("部分车队不存在"), ex.getMessage());
+        Mockito.verifyNoInteractions(unbindClient);
+        Mockito.verify(service, Mockito.never()).removeByIds(Mockito.anyList());
+    }
+
+    @Test
+    void deleteFleetsAbortsWhenUnbindFails() {
+        FleetUnbindClient unbindClient = Mockito.mock(FleetUnbindClient.class);
+        Mockito.when(unbindClient.unbindByFleetIds(Mockito.anyList())).thenReturn(R.fail("车队关联数据解绑失败:超时"));
+
+        TFleetServiceImpl service = Mockito.spy(new TFleetServiceImpl());
+        ReflectionTestUtils.setField(service, "fleetUnbindClient", unbindClient);
+        Mockito.doReturn(Collections.singletonList(fleet(1))).when(service).listByIds(Mockito.anyList());
+
+        ServiceException ex = assertThrows(ServiceException.class, () -> service.deleteFleets(Collections.singletonList(1)));
+
+        // 解绑失败中止删除:车队与绑定关系均保留,两侧数据保持一致
+        assertTrue(ex.getMessage().contains("车队关联数据解绑失败"), ex.getMessage());
+        Mockito.verify(service, Mockito.never()).removeByIds(Mockito.anyList());
+    }
+
+    @Test
+    void deleteFleetsAbortsWhenRemoteThrows() {
+        FleetUnbindClient unbindClient = Mockito.mock(FleetUnbindClient.class);
+        Mockito.when(unbindClient.unbindByFleetIds(Mockito.anyList()))
+                .thenThrow(new RuntimeException("connect refused"));
+
+        TFleetServiceImpl service = Mockito.spy(new TFleetServiceImpl());
+        ReflectionTestUtils.setField(service, "fleetUnbindClient", unbindClient);
+        Mockito.doReturn(Collections.singletonList(fleet(1))).when(service).listByIds(Mockito.anyList());
+
+        ServiceException ex = assertThrows(ServiceException.class, () -> service.deleteFleets(Collections.singletonList(1)));
+
+        assertTrue(ex.getMessage().contains("车队关联数据解绑失败"), ex.getMessage());
+        Mockito.verify(service, Mockito.never()).removeByIds(Mockito.anyList());
+    }
+
     private TFleet fleet(Integer id) {
         TFleet fleet = new TFleet();
         fleet.setId(id);

--
Gitblit v1.7.1