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