From 976ff3fe092ee2f9515013dbfab77d25b2944b75 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期三, 14 十月 2026 11:18:29 +0800
Subject: [PATCH] 修改bug

---
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java                          |    2 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java                |  179 +++++++++++++++----
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java            |    4 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplVinFallbackTest.java |  103 +++++++++++
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java                              |    1 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderStartupTimeoutTest.java         |  187 ++++++++++++++++++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java                   |   22 ++
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java                                |    1 
 8 files changed, 452 insertions(+), 47 deletions(-)

diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java
index 66b259d..8ec6b3a 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java
@@ -2,6 +2,7 @@
 
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -33,6 +34,7 @@
     private Integer fleetId;
 
     @ApiModelProperty("最大充电结束时间(授权截止时间,全天授权或未开启自动停充时为空)")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @TableField("max_end_time")
     private LocalDateTime maxEndTime;
 
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java
index 19e9da4..3f92186 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java
@@ -233,6 +233,7 @@
     private Integer fleetId;
 
     @ApiModelProperty(value = "最大充电结束时间(企业授权截止时间,全天授权或未开启自动停充时为空)")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @TableField("max_end_time")
     private LocalDateTime maxEndTime;
 
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
index 8057588..863b390 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
@@ -85,6 +85,7 @@
     @ApiModelProperty(value = "账户模式(1=共享企业账户,2=分配卡账户;VIN/刷卡订单建单快照)")
     private Integer accountMode;
     @ApiModelProperty(value = "授权结束时间(企业政策授权截止时间,全天授权为空)")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime maxEndTime;
     @ApiModelProperty(value = "锁定额度(申请时冻结金额,实扣冻结口径)")
     private BigDecimal lockedAmount;
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
index a2002a6..df80466 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -10,6 +10,7 @@
 import java.math.RoundingMode;
 import java.text.DecimalFormat;
 import java.time.*;
+import java.util.concurrent.TimeUnit;
 
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -1871,6 +1872,12 @@
     
     
     
+    /**
+     * 安全检测等待窗口(秒):设备在此时长内未上报有效安全检测时,前端按「未插枪」提示用户,
+     * 超时订单由启动超时兜底任务自动取消,避免订单永久停留在启动中状态。
+     */
+    private static final long STARTUP_WAIT_SECONDS = 90L;
+
     @ResponseBody
     @GetMapping(value = "/preChargeCheck/{id}")
     @ApiOperation(value = "获取安全检测数据", tags = {"小程序-扫一扫"})
@@ -1880,10 +1887,17 @@
     public AjaxResult<PreChargeCheck> preChargeCheck(@PathVariable Integer id) {
         String key = "AQJC_" + id;
         PreChargeCheck cacheObject = redisService.getCacheObject(key);
-        if(null != cacheObject){
-            if(LocalDateTime.now().minusMinutes(1).isAfter(cacheObject.getAddTime())){
-                redisService.deleteObject(key);
-                cacheObject = null;
+        if (null != cacheObject) {
+            LocalDateTime addTime = cacheObject.getAddTime();
+            // 等待安全检测超出窗口且启动指令尚未下发时,按「未插枪」提示前端;
+            // 缓存必须保留:设备安全检测上报依赖该缓存推进启动,删除会导致订单永久卡在启动中
+            if (Integer.valueOf(1).equals(cacheObject.getStartupSuccess())
+                    && !Boolean.TRUE.equals(cacheObject.getStartCommandSent())
+                    && null != addTime
+                    && LocalDateTime.now().isAfter(addTime.plusSeconds(STARTUP_WAIT_SECONDS))) {
+                cacheObject.setStartupSuccess(3);
+                cacheObject.setFailureCause(5);
+                redisService.setCacheObject(key, cacheObject, 24L, TimeUnit.HOURS);
             }
         }
         return AjaxResult.success(cacheObject);
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 5429658..8194c22 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
@@ -306,7 +306,9 @@
         result.put("discountServiceCharge", order.getDiscountServiceCharge());
         // 账户模式/授权截止时间/申请锁定额度(刷卡链路改造)
         result.put("accountMode", order.getAccountMode());
-        result.put("maxEndTime", order.getMaxEndTime());
+        // Map 值不走实体字段上的 @JsonFormat,显式格式化,避免前端展示 ISO 格式(含 T)
+        result.put("maxEndTime", order.getMaxEndTime() == null ? null
+                : order.getMaxEndTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
         result.put("lockedAmount", order.getLockedAmount());
         result.put("total", defaultDecimal(order.getElectrovalence()).add(defaultDecimal(order.getServiceCharge())));
         result.put("realTotal", order.getPaymentAmount());
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
index eaeb5b5..80c4878 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -1936,13 +1936,7 @@
         chargingOrder.setPayTime(callbackPayTime);
 
         //添加安全检测数据到缓存中,每步安全检测完成后需要更新缓存数据
-        PreChargeCheck preChargeCheck = new PreChargeCheck();
-        preChargeCheck.setElectronicLockLock(false);
-        preChargeCheck.setInsulationTesting(false);
-        preChargeCheck.setSecureConnectionDetection(false);
-        preChargeCheck.setStartupSuccess(1);
-        preChargeCheck.setStartCommandSent(false);
-        preChargeCheck.setAddTime(chargingOrder.getReservationTime() != null ? chargingOrder.getReservationTime() : LocalDateTime.now());
+        PreChargeCheck preChargeCheck = newPreChargeCheck(chargingOrder);
         String key = "AQJC_" + chargingOrder.getChargingGunId();
         redisService.setCacheObject(key, preChargeCheck, 24L, TimeUnit.HOURS);
 
@@ -2082,6 +2076,33 @@
                 && Boolean.TRUE.equals(check.getSecureConnectionDetection());
     }
 
+    /**
+     * 查询充电枪进行中的待启动订单(支付完成、启动中)
+     */
+    private TChargingOrder getPendingStartOrder(Integer chargingGunId) {
+        return this.getOne(new LambdaQueryWrapper<TChargingOrder>()
+                .eq(TChargingOrder::getChargingGunId, chargingGunId)
+                .eq(TChargingOrder::getRechargePaymentStatus, 2)
+                .eq(TChargingOrder::getStatus, 2)
+                .eq(TChargingOrder::getDelFlag, 0)
+                .orderByDesc(TChargingOrder::getCreateTime)
+                .last("limit 1"));
+    }
+
+    /**
+     * 初始化安全检测等待对象(与建单/支付完成时的初始状态保持一致)
+     */
+    private PreChargeCheck newPreChargeCheck(TChargingOrder chargingOrder) {
+        PreChargeCheck preChargeCheck = new PreChargeCheck();
+        preChargeCheck.setElectronicLockLock(false);
+        preChargeCheck.setInsulationTesting(false);
+        preChargeCheck.setSecureConnectionDetection(false);
+        preChargeCheck.setStartupSuccess(1);
+        preChargeCheck.setStartCommandSent(false);
+        preChargeCheck.setAddTime(chargingOrder.getReservationTime() != null ? chargingOrder.getReservationTime() : LocalDateTime.now());
+        return preChargeCheck;
+    }
+
     private String normalizeVin(String vin) {
         return StringUtils.hasLength(vin) ? vin.replaceAll("\\s+", "").toUpperCase(Locale.ROOT) : null;
     }
@@ -2119,6 +2140,44 @@
         }
     }
 
+    /**
+     * 启动超时兜底:订单支付完成后长时间未真正开始充电(等待安全检测无进展、启动指令未生效),
+     * 自动取消订单并释放锁定额度,避免订单永久停留在「启动中」导致该枪无法再次启动。
+     */
+    @Scheduled(fixedDelay = 30000L)
+    public void cancelStartupTimeoutOrders() {
+        LocalDateTime deadline = LocalDateTime.now().minusMinutes(3);
+        List<TChargingOrder> timeoutOrders = this.list(new LambdaQueryWrapper<TChargingOrder>()
+                .eq(TChargingOrder::getRechargePaymentStatus, 2)
+                .eq(TChargingOrder::getStatus, 2)
+                .eq(TChargingOrder::getDelFlag, 0)
+                .isNull(TChargingOrder::getStartTime)
+                .isNull(TChargingOrder::getReservationTime)
+                .lt(TChargingOrder::getPayTime, deadline)
+                .last("limit 100"));
+        for (TChargingOrder order : timeoutOrders) {
+            try {
+                PreChargeCheck check = redisService.getCacheObject("AQJC_" + order.getChargingGunId());
+                if (null != check && Boolean.TRUE.equals(check.getStartCommandSent())) {
+                    // 已下发启动指令,交给既有启动结果检测(timingDetection)处理,避免误取消
+                    continue;
+                }
+                log.warn("订单编号={}:等待安全检测超时,自动取消订单并释放额度,充电枪id={}", order.getCode(), order.getChargingGunId());
+                refund(order.getCode());
+                TChargingOrder finish = new TChargingOrder();
+                finish.setId(order.getId());
+                finish.setAppUserId(order.getAppUserId());
+                finish.setStatus(5);
+                finish.setEndMode(0);
+                finish.setEndTime(LocalDateTime.now());
+                this.updateById(finish);
+                redisService.deleteObject("AQJC_" + order.getChargingGunId());
+            } catch (Exception e) {
+                log.error("启动超时订单自动取消失败,orderCode={}", order.getCode(), e);
+            }
+        }
+    }
+
     private void startChargingAfterSafety(TChargingOrder chargingOrder, PreChargeCheck check) {
         RLock lock = redissonClient.getLock("START_AFTER_SAFETY:" + chargingOrder.getId());
         try {
@@ -2152,12 +2211,18 @@
             command.setCharging_gun_code(gun.getCode());
             command.setCard_number(latest.getId().toString());
             command.setAccount_balance(latest.getChargeAmount());
-            check.setStartCommandSent(true);
             latest.setActualVin(actualVin);
             this.updateById(latest);
-            redisService.setCacheObject("AQJC_" + latest.getChargingGunId(), check, 24L, TimeUnit.HOURS);
             log.info(latest.getCode() + ":安全检测及VIN校验通过,下发启动充电命令:" + command);
-            sendMessageClient.platformStartCharging(command);
+            try {
+                sendMessageClient.platformStartCharging(command);
+            } catch (Exception e) {
+                // 下发失败不置 startCommandSent:保留后续安全检测上报重试机会,超时订单由兜底任务取消
+                log.error(latest.getCode() + ":启动充电命令下发失败,等待下次安全检测上报重试", e);
+                return;
+            }
+            check.setStartCommandSent(true);
+            redisService.setCacheObject("AQJC_" + latest.getChargingGunId(), check, 24L, TimeUnit.HOURS);
             ScheduledExecutorService resultScheduler = Executors.newScheduledThreadPool(1);
             resultScheduler.scheduleAtFixedRate(() -> {
                 if (timingDetection(latest.getId())) {
@@ -2196,8 +2261,13 @@
             Integer failure_cause = platformStartChargingReply.getFailure_cause();
             Integer counter = counter_map.get(code);
             PreChargeCheck preChargeCheck1 = redisService.getCacheObject(key);
-            //15分钟内还未插枪则取消充电,退回金额。
-            if (failure_cause == 5 && (null == counter || counter < 60 * 15)) {
+            if (null == preChargeCheck1) {
+                // 缓存缺失时用临时对象推进,避免启动结果补偿因空缓存中断
+                preChargeCheck1 = new PreChargeCheck();
+                preChargeCheck1.setStartupSuccess(1);
+            }
+            //15分钟内还未插枪则取消充电,退回金额。按10秒探测一次,90次约15分钟
+            if (failure_cause == 5 && (null == counter || counter < 90)) {
                 counter = (null == counter ? 0 : counter) + 1;
                 counter_map.put(code, counter);
                 //启动失败
@@ -2248,6 +2318,11 @@
             return true;
         } else {
             PreChargeCheck preChargeCheck1 = redisService.getCacheObject(key);
+            if (null == preChargeCheck1) {
+                // 缓存缺失时用临时对象推进,避免启动结果补偿因空缓存中断
+                preChargeCheck1 = new PreChargeCheck();
+                preChargeCheck1.setStartupSuccess(1);
+            }
             UploadRealTimeMonitoringData uploadRealTimeMonitoringData = uploadRealTimeMonitoringDataService.getDataByOrderCode(code, "desc");
             if (null != uploadRealTimeMonitoringData) {
                 //启动成功
@@ -2279,8 +2354,8 @@
             } else {
                 Integer counter = boot_failed_map.get(code);
                 log.info(code + ":-------------------未上传开启充电结果-------------------" + counter);
-                //15分钟内未启动成功,退回金额。
-                if (null == counter || counter < 60 * 15) {
+                //15分钟内未启动成功,退回金额。按10秒探测一次,90次约15分钟
+                if (null == counter || counter < 90) {
                     counter = (null == counter ? 0 : counter) + 1;
                     boot_failed_map.put(code, counter);
                     //启动失败
@@ -2332,25 +2407,43 @@
         GetChargingGunByCode code = new GetChargingGunByCode();
         code.setCharging_pile_code(securityDetection.getCharging_pile_code());
         code.setCharging_gun_code(securityDetection.getCharging_gun_code());
-        TChargingGun chargingGun = chargingGunClient.getChargingGunByCode(code).getData();
-        PreChargeCheck preChargeCheck1 = redisService.getCacheObject("AQJC_" + chargingGun.getId());
-        if (null != preChargeCheck1) {
-            preChargeCheck1.setElectronicLockLock(Integer.valueOf(1).equals(securityDetection.getElectronic_lock_lock()));
-            preChargeCheck1.setInsulationTesting(Integer.valueOf(0).equals(securityDetection.getNsulation_detection()));
-            preChargeCheck1.setSecureConnectionDetection(Integer.valueOf(1).equals(securityDetection.getSecure_connection()));
-            preChargeCheck1.setActualVin(normalizeVin(securityDetection.getVin()));
-            redisService.setCacheObject("AQJC_" + chargingGun.getId(), preChargeCheck1, 24L, TimeUnit.HOURS);
-            if (isSafetyCheckPassed(preChargeCheck1)) {
-                TChargingOrder pendingOrder = this.getOne(new LambdaQueryWrapper<TChargingOrder>()
-                        .eq(TChargingOrder::getChargingGunId, chargingGun.getId())
-                        .eq(TChargingOrder::getRechargePaymentStatus, 2)
-                        .eq(TChargingOrder::getStatus, 2)
-                        .eq(TChargingOrder::getDelFlag, 0)
-                        .orderByDesc(TChargingOrder::getCreateTime)
-                        .last("limit 1"));
-                if (pendingOrder != null) {
-                    startChargingAfterSafety(pendingOrder, preChargeCheck1);
-                }
+        TChargingGun chargingGun = null;
+        try {
+            chargingGun = chargingGunClient.getChargingGunByCode(code).getData();
+        } catch (Exception e) {
+            log.error("安全检测上报-查询充电枪异常,charging_pile_code={},charging_gun_code={}",
+                    securityDetection.getCharging_pile_code(), securityDetection.getCharging_gun_code(), e);
+        }
+        if (null == chargingGun) {
+            log.warn("安全检测上报-未匹配到充电枪,忽略,charging_pile_code={},charging_gun_code={}",
+                    securityDetection.getCharging_pile_code(), securityDetection.getCharging_gun_code());
+            return;
+        }
+        String key = "AQJC_" + chargingGun.getId();
+        PreChargeCheck preChargeCheck1 = redisService.getCacheObject(key);
+        if (null == preChargeCheck1) {
+            // 缓存缺失(等待窗口内未收到上报、缓存被清理)时按该枪进行中的待启动订单重建,
+            // 保证设备后续安全检测上报仍能推进启动,避免订单永久卡在启动中
+            TChargingOrder pendingOrder = getPendingStartOrder(chargingGun.getId());
+            if (null == pendingOrder) {
+                log.info("安全检测上报-无待启动订单,忽略,充电枪id={},charging_pile_code={},charging_gun_code={}",
+                        chargingGun.getId(), securityDetection.getCharging_pile_code(), securityDetection.getCharging_gun_code());
+                return;
+            }
+            preChargeCheck1 = newPreChargeCheck(pendingOrder);
+            log.warn("安全检测上报-AQJC缓存缺失,已按待启动订单重建,订单编号={},充电枪id={}", pendingOrder.getCode(), chargingGun.getId());
+        }
+        preChargeCheck1.setElectronicLockLock(Integer.valueOf(1).equals(securityDetection.getElectronic_lock_lock()));
+        preChargeCheck1.setInsulationTesting(Integer.valueOf(0).equals(securityDetection.getNsulation_detection()));
+        preChargeCheck1.setSecureConnectionDetection(Integer.valueOf(1).equals(securityDetection.getSecure_connection()));
+        preChargeCheck1.setActualVin(normalizeVin(securityDetection.getVin()));
+        // 刷新等待基准时间,避免前端等待窗口/超时判断误伤正在推进的安全检测
+        preChargeCheck1.setAddTime(LocalDateTime.now());
+        redisService.setCacheObject(key, preChargeCheck1, 24L, TimeUnit.HOURS);
+        if (isSafetyCheckPassed(preChargeCheck1)) {
+            TChargingOrder pendingOrder = getPendingStartOrder(chargingGun.getId());
+            if (pendingOrder != null) {
+                startChargingAfterSafety(pendingOrder, preChargeCheck1);
             }
         }
     }
@@ -2375,6 +2468,11 @@
             preChargeCheck1.setElectronicLockLock(true);
             preChargeCheck1.setInsulationTesting(true);
             preChargeCheck1.setStartupSuccess(1);
+        } else {
+            // 缓存缺失时用临时对象推进订单状态,避免设备已启动成功而订单因缓存为空无法流转
+            preChargeCheck1 = new PreChargeCheck();
+            preChargeCheck1.setElectronicLockLock(true);
+            preChargeCheck1.setInsulationTesting(true);
         }
         if (0 == startup_result) {
             //启动失败
@@ -2769,19 +2867,22 @@
     @Override
     public ChargingDetails getChargingDetails(Integer id) {
         Long userId = tokenService.getLoginUserApplet().getUserId();
+        // 状态包含已结束(5):订单结束后小程序需继续拿到该笔订单,用于关闭「请勿拔枪,正在结束充电」
+        // 并展示「充电完成,可拔枪」;仅取最近一笔,避免历史完成订单长期滞留结束态
         TChargingOrder one = this.getOne(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getAppUserId, userId).eq(TChargingOrder::getChargingGunId, id)
                 .eq(TChargingOrder::getRechargePaymentStatus, 2)
-                .in(TChargingOrder::getStatus, Arrays.asList(2, 3, 4))
+                .in(TChargingOrder::getStatus, Arrays.asList(2, 3, 4, 5))
                 .eq(TChargingOrder::getDelFlag, 0)
                 .orderByDesc(TChargingOrder::getCreateTime)
                 .last(" limit 0, 1"));
         if (null == one || !one.getAppUserId().equals(userId)) {
             // VIN 码充电订单兜底:VD 订单存于 t_vin_charging_order,且后付模式无支付动作,
             // 不按 rechargePaymentStatus 过滤;TVinChargingOrder 继承 TChargingOrder,后续组装原样复用
+            // 状态同样包含已结束(5),保证 VIN 订单结束后前端仍能拿到完成态
             TVinChargingOrder vinOrder = vinChargingOrderMapper.selectOne(new LambdaQueryWrapper<TVinChargingOrder>()
                     .eq(TVinChargingOrder::getAppUserId, userId)
                     .eq(TVinChargingOrder::getChargingGunId, id)
-                    .in(TVinChargingOrder::getStatus, Arrays.asList(2, 3, 4))
+                    .in(TVinChargingOrder::getStatus, Arrays.asList(2, 3, 4, 5))
                     .eq(TVinChargingOrder::getDelFlag, 0)
                     .orderByDesc(TVinChargingOrder::getCreateTime)
                     .last(" limit 0, 1"));
@@ -8240,13 +8341,7 @@
 
 
         //添加安全检测数据到缓存中,每步安全检测完成后需要更新缓存数据
-        PreChargeCheck preChargeCheck = new PreChargeCheck();
-        preChargeCheck.setElectronicLockLock(false);
-        preChargeCheck.setInsulationTesting(false);
-        preChargeCheck.setSecureConnectionDetection(false);
-        preChargeCheck.setStartupSuccess(1);
-        preChargeCheck.setStartCommandSent(false);
-        preChargeCheck.setAddTime(chargingOrder.getReservationTime() != null ? chargingOrder.getReservationTime() : LocalDateTime.now());
+        PreChargeCheck preChargeCheck = newPreChargeCheck(chargingOrder);
         String key = "AQJC_" + chargingOrder.getChargingGunId();
         redisService.setCacheObject(key, preChargeCheck, 24L, TimeUnit.HOURS);
 
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplVinFallbackTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplVinFallbackTest.java
index 090cb4a..3b32ec4 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplVinFallbackTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplVinFallbackTest.java
@@ -18,6 +18,8 @@
 import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
 import com.ruoyi.order.vo.EndOfChargePageInfo;
 import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import com.ruoyi.system.api.model.LoginUserApplet;
 import org.apache.ibatis.builder.MapperBuilderAssistant;
@@ -25,6 +27,7 @@
 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;
@@ -32,13 +35,19 @@
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 /**
@@ -231,4 +240,98 @@
         assertNotNull(exception);
         assertEquals("充电订单不存在", exception.getMessage());
     }
+
+    @Test
+    void getChargingDetailsReturnsFinishedNormalOrderAndQueriesStatus5() {
+        // 小程序主动结束充电后订单已结束(5),充电中页面仍需拿到该笔订单,
+        // 用于关闭「请勿拔枪,正在结束充电」并展示「充电完成,可拔枪」
+        LoginUserApplet loginUser = new LoginUserApplet();
+        loginUser.setUserId(88L);
+        when(tokenService.getLoginUserApplet()).thenReturn(loginUser);
+        TChargingOrder order = new TChargingOrder();
+        order.setId(3003L);
+        order.setAppUserId(88L);
+        order.setChargingGunId(5);
+        order.setStatus(5);
+        order.setCode("CH20261014001");
+        order.setResidualAmount(new BigDecimal("20.00"));
+        when(mapper.selectOne(any())).thenReturn(order);
+        stubGunPileAndSite();
+        when(uploadRealTimeMonitoringDataService.getLastDataById("CH20261014001")).thenReturn(null);
+
+        ChargingDetails details = service.getChargingDetails(5);
+
+        assertNotNull(details, "订单已结束时应返回完成态而非 null");
+        assertEquals("3003", details.getId());
+        assertEquals(Integer.valueOf(5), details.getStatus());
+        assertEquals("CH20261014001", details.getCode());
+        assertEquals(0, new BigDecimal("20.00").compareTo(details.getChargingCost()));
+
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<Wrapper<TChargingOrder>> captor = ArgumentCaptor.forClass(Wrapper.class);
+        verify(mapper).selectOne(captor.capture());
+        assertStatusFilterCoversFinished(captor.getValue());
+    }
+
+    @Test
+    void getChargingDetailsReturnsFinishedVinOrderAndQueriesStatus5() {
+        // VIN 码订单结束后同样需要返回完成态,兜底查询的状态过滤必须与主查询一致
+        LoginUserApplet loginUser = new LoginUserApplet();
+        loginUser.setUserId(88L);
+        when(tokenService.getLoginUserApplet()).thenReturn(loginUser);
+        when(mapper.selectOne(any())).thenReturn(null);
+        TVinChargingOrder vinOrder = new TVinChargingOrder();
+        vinOrder.setId(1001L);
+        vinOrder.setAppUserId(88L);
+        vinOrder.setChargingGunId(5);
+        vinOrder.setStatus(5);
+        vinOrder.setCode("VD20261014001");
+        vinOrder.setResidualAmount(new BigDecimal("66.60"));
+        when(vinChargingOrderMapper.selectOne(any())).thenReturn(vinOrder);
+        stubGunPileAndSite();
+        when(uploadRealTimeMonitoringDataService.getLastDataById("VD20261014001")).thenReturn(null);
+
+        ChargingDetails details = service.getChargingDetails(5);
+
+        assertNotNull(details, "VIN 订单已结束时应返回完成态而非 null");
+        assertEquals(Integer.valueOf(5), details.getStatus());
+        assertEquals("VD20261014001", details.getCode());
+
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<Wrapper<TVinChargingOrder>> captor = ArgumentCaptor.forClass(Wrapper.class);
+        verify(vinChargingOrderMapper).selectOne(captor.capture());
+        assertStatusFilterCoversFinished(captor.getValue());
+    }
+
+    /** getChargingDetails 组装链路依赖的充电枪/充电桩/站点远程调用 stub */
+    private void stubGunPileAndSite() {
+        TChargingGun gun = new TChargingGun();
+        gun.setChargingPileId(7);
+        when(chargingGunClient.getChargingGunById(5)).thenReturn(R.ok(gun));
+        TChargingPile pile = new TChargingPile();
+        pile.setName("1号桩");
+        pile.setSiteId(9);
+        when(chargingPileClient.getChargingPileById(7)).thenReturn(R.ok(pile));
+        Site site = new Site();
+        site.setName("城东站");
+        when(siteClient.getSiteByIds(Arrays.asList(9))).thenReturn(R.ok(Collections.singletonList(site)));
+    }
+
+    /**
+     * 断言订单查询条件中的 status 过滤覆盖 2、3、4、5。
+     * 已结束(5) 必须在过滤范围内,否则小程序结束充电后拿不到订单,会一直停留在「正在结束充电」提示。
+     */
+    private void assertStatusFilterCoversFinished(Wrapper<?> wrapper) {
+        AbstractWrapper<?, ?, ?> queryWrapper = (AbstractWrapper<?, ?, ?>) wrapper;
+        Matcher inMatcher = Pattern.compile("status\\s+in\\s*\\(([^)]*)\\)", Pattern.CASE_INSENSITIVE)
+                .matcher(queryWrapper.getSqlSegment());
+        assertTrue(inMatcher.find(), "查询条件中应存在 status IN 过滤");
+        List<Object> statusValues = new ArrayList<>();
+        Matcher placeholderMatcher = Pattern.compile("MPGENVAL\\d+").matcher(inMatcher.group(1));
+        while (placeholderMatcher.find()) {
+            statusValues.add(queryWrapper.getParamNameValuePairs().get(placeholderMatcher.group()));
+        }
+        assertTrue(statusValues.containsAll(Arrays.asList(2, 3, 4, 5)),
+                "status 过滤应包含 2,3,4,5,实际为:" + statusValues);
+    }
 }
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderStartupTimeoutTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderStartupTimeoutTest.java
new file mode 100644
index 0000000..1aac8ce
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderStartupTimeoutTest.java
@@ -0,0 +1,187 @@
+package com.ruoyi.order.service.impl;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
+import com.ruoyi.chargingPile.api.model.TChargingGun;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.redis.service.RedisService;
+import com.ruoyi.order.api.model.TChargingOrder;
+import com.ruoyi.order.api.vo.SecurityDetectionVO;
+import com.ruoyi.order.dto.PreChargeCheck;
+import com.ruoyi.order.mapper.TChargingOrderMapper;
+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.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 启动等待链路单测:安全检测上报缓存自愈重建、启动超时订单兜底取消
+ */
+class TChargingOrderStartupTimeoutTest {
+
+    private static final Integer GUN_ID = 1967;
+    private static final String ORDER_CODE = "CD3720261014104949469";
+
+    /**
+     * 纯单测无 Spring 容器,MyBatis-Plus 的 lambda 列缓存需手工初始化
+     */
+    @BeforeAll
+    static void initMybatisPlusTableInfo() {
+        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, TChargingOrder.class);
+    }
+
+    private TChargingOrderServiceImpl service;
+    private TChargingOrderMapper mapper;
+    private RedisService redisService;
+    private ChargingGunClient chargingGunClient;
+
+    @BeforeEach
+    void setUp() {
+        service = spy(new TChargingOrderServiceImpl());
+        mapper = mock(TChargingOrderMapper.class);
+        redisService = mock(RedisService.class);
+        chargingGunClient = mock(ChargingGunClient.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "redisService", redisService);
+        ReflectionTestUtils.setField(service, "chargingGunClient", chargingGunClient);
+    }
+
+    /**
+     * 缓存缺失时按待启动订单重建,保证设备安全检测上报仍能推进启动
+     */
+    @Test
+    void securityDetectionRebuildsCacheWhenCacheMissing() {
+        when(chargingGunClient.getChargingGunByCode(any())).thenReturn(R.ok(chargingGun()));
+        when(redisService.getCacheObject("AQJC_" + GUN_ID)).thenReturn(null);
+        when(mapper.selectOne(any())).thenReturn(pendingOrder());
+
+        // 绝缘检测未通过(1=不安全)时仅更新缓存,不进入启动下发
+        service.securityDetection(securityDetectionVO(1, 1, 1));
+
+        ArgumentCaptor<PreChargeCheck> captor = ArgumentCaptor.forClass(PreChargeCheck.class);
+        verify(redisService).setCacheObject(eq("AQJC_" + GUN_ID), captor.capture(), eq(24L), eq(TimeUnit.HOURS));
+        PreChargeCheck saved = captor.getValue();
+        assertEquals(Integer.valueOf(1), saved.getStartupSuccess(), "重建后应回到等待启动状态");
+        assertEquals(Boolean.TRUE, saved.getElectronicLockLock());
+        assertEquals(Boolean.FALSE, saved.getInsulationTesting());
+        assertNotNull(saved.getAddTime(), "重建后应刷新等待基准时间");
+    }
+
+    /**
+     * 缓存缺失且无待启动订单时忽略上报,不产生脏缓存
+     */
+    @Test
+    void securityDetectionIgnoresWhenNoPendingOrder() {
+        when(chargingGunClient.getChargingGunByCode(any())).thenReturn(R.ok(chargingGun()));
+        when(redisService.getCacheObject("AQJC_" + GUN_ID)).thenReturn(null);
+        when(mapper.selectOne(any())).thenReturn(null);
+
+        service.securityDetection(securityDetectionVO(1, 0, 1));
+
+        verify(redisService, never()).setCacheObject(anyString(), any(), anyLong(), any(TimeUnit.class));
+    }
+
+    /**
+     * 等待安全检测超时的订单自动取消:退款、推进为已结束并清理缓存
+     */
+    @Test
+    void startupTimeoutOrderIsCancelled() {
+        when(mapper.selectList(any())).thenReturn(Collections.singletonList(pendingOrder()));
+        when(redisService.getCacheObject("AQJC_" + GUN_ID)).thenReturn(null);
+        doNothing().when(service).refund(ORDER_CODE);
+
+        service.cancelStartupTimeoutOrders();
+
+        ArgumentCaptor<TChargingOrder> captor = ArgumentCaptor.forClass(TChargingOrder.class);
+        verify(mapper).updateById(captor.capture());
+        TChargingOrder finished = captor.getValue();
+        assertEquals(Integer.valueOf(5), finished.getStatus());
+        assertEquals(Integer.valueOf(0), finished.getEndMode());
+        assertNotNull(finished.getEndTime());
+        verify(service).refund(ORDER_CODE);
+        verify(redisService).deleteObject("AQJC_" + GUN_ID);
+    }
+
+    /**
+     * 已下发启动指令的订单交给启动结果检测处理,兜底任务不得误取消
+     */
+    @Test
+    void startupTimeoutOrderWithCommandSentIsSkipped() {
+        when(mapper.selectList(any())).thenReturn(Collections.singletonList(pendingOrder()));
+        PreChargeCheck check = new PreChargeCheck();
+        check.setStartCommandSent(true);
+        when(redisService.getCacheObject("AQJC_" + GUN_ID)).thenReturn(check);
+
+        service.cancelStartupTimeoutOrders();
+
+        verify(service, never()).refund(anyString());
+        verify(mapper, never()).updateById(any());
+        verify(redisService, never()).deleteObject(anyString());
+    }
+
+    /**
+     * 初始化安全检测等待对象:预约订单以预约时间为等待基准
+     */
+    @Test
+    void newPreChargeCheckUsesReservationTimeAsWaitBase() {
+        LocalDateTime reservationTime = LocalDateTime.now().plusMinutes(5);
+        TChargingOrder order = pendingOrder();
+        order.setReservationTime(reservationTime);
+
+        PreChargeCheck check = ReflectionTestUtils.invokeMethod(service, "newPreChargeCheck", order);
+
+        assertNotNull(check);
+        assertEquals(reservationTime, check.getAddTime());
+        assertEquals(Integer.valueOf(1), check.getStartupSuccess());
+        assertEquals(Boolean.FALSE, check.getStartCommandSent());
+    }
+
+    private static TChargingGun chargingGun() {
+        TChargingGun gun = new TChargingGun();
+        gun.setId(GUN_ID);
+        return gun;
+    }
+
+    private static TChargingOrder pendingOrder() {
+        TChargingOrder order = new TChargingOrder();
+        order.setId(2110201381176229890L);
+        order.setCode(ORDER_CODE);
+        order.setChargingGunId(GUN_ID);
+        order.setStatus(2);
+        order.setRechargePaymentStatus(2);
+        order.setDelFlag(false);
+        order.setPayTime(LocalDateTime.now().minusMinutes(10));
+        return order;
+    }
+
+    private static SecurityDetectionVO securityDetectionVO(Integer electronicLock, Integer insulation, Integer secureConnection) {
+        SecurityDetectionVO vo = new SecurityDetectionVO();
+        vo.setCharging_pile_code("8832010600495288");
+        vo.setCharging_gun_code("02");
+        vo.setElectronic_lock_lock(electronicLock);
+        vo.setNsulation_detection(insulation);
+        vo.setSecure_connection(secureConnection);
+        return vo;
+    }
+}

--
Gitblit v1.7.1