From 4a5e1477afc559cf870b34e4693f41d85e9580d1 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期三, 09 九月 2026 15:39:35 +0800
Subject: [PATCH] 新增加VIN充电和刷卡充电业务逻辑

---
 ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/enums/ServiceIdMenu.java                       |    1 
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TChargingCardController.java                      |   32 
 ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml                                    |   41 
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TChargingCardService.java                            |   30 
 sql/20260909_sd_vd_order_enum_comment.sql                                                                                |   18 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinAccountingStrategyOrderMapper.java                    |   15 
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java                         |    9 
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/VinManagementFallbackFactory.java                |   15 
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TVinManagementServiceImpl.java                  |    7 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinAccountingStrategyOrderService.java                  |   13 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinChargingOrderAccountingStrategyService.java          |   13 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java                   |  582 ++++++++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinChargingOrderService.java                            |   73 +
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/ChargingCardFallbackFactory.java                 |   46 
 sql/20260909_card_password_charge_lock.sql                                                                               |   86 +
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinChargingOrderAccountingStrategy.java               |   18 
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/ChargingCardClient.java                      |   55 
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java                    |    6 
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java                                 |   14 
 ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java               |  365 ++++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinAccountingStrategyOrderServiceImpl.java         |   17 
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java                   |  187 +++
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinAccountingStrategyOrder.java                       |   18 
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardAuthDTO.java                             |   25 
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/dto/DeviceStartChargingDTO.java                              |   34 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderAccountingStrategyServiceImpl.java |   17 
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardDeductDTO.java                           |   27 
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinAccountingStrategyDetailOrder.java                 |   18 
 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/DeviceStartChargingVO.java                                |   47 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinChargingOrderAccountingStrategyMapper.java            |   15 
 ruoyi-service/ruoyi-integration/src/test/java/com/ruoyi/integration/iotda/enums/ServiceIdMenuTest.java                   |   38 
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardSaveDTO.java                             |    3 
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/vo/ChargingCardDeductVO.java                             |   33 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/TransactionStageParserTest.java                             |   88 +
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TVinManagementController.java                     |    8 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java                         |  128 +
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/mapper/TChargingCardMapper.java                              |   23 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinAccountingStrategyDetailOrderServiceImpl.java   |   30 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinAccountingStrategyDetailOrderService.java            |   20 
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TVinManagementService.java                           |    5 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java                  |  628 ++++++++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java                           |   64 +
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java                                          |   92 +
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/TransactionStageParser.java                                 |   98 +
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinOrderCompensationTest.java                   |  213 +++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinAccountingStrategyDetailOrderMapper.java              |   15 
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/VinManagementClient.java                     |    7 
 ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/TChargingCardControllerTest.java                  |   46 
 ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java         |   44 
 49 files changed, 3,403 insertions(+), 24 deletions(-)

diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardAuthDTO.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardAuthDTO.java
new file mode 100644
index 0000000..6bac6e7
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardAuthDTO.java
@@ -0,0 +1,25 @@
+package com.ruoyi.account.api.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@ApiModel(value = "充电卡刷卡启动鉴权参数")
+public class ChargingCardAuthDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "卡号", required = true)
+    private String cardNo;
+
+    /**
+     * 卡密码(设备上报明文)。
+     * 条件校验规则(V0.2 定稿):卡片管理侧配置了卡密码时必须校验且匹配;
+     * 未配置卡密码时不要求、也不校验(设备上报时忽略)。
+     */
+    @ApiModelProperty(value = "卡密码(明文,卡片配置了密码时必填并校验,未配置时忽略)")
+    private String password;
+}
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardDeductDTO.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardDeductDTO.java
new file mode 100644
index 0000000..97a4e5e
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardDeductDTO.java
@@ -0,0 +1,27 @@
+package com.ruoyi.account.api.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+@ApiModel(value = "刷卡充电订单结算扣款参数")
+public class ChargingCardDeductDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "卡片ID", required = true)
+    private Long cardId;
+
+    @ApiModelProperty(value = "刷卡订单号(SD 前缀),作为扣款幂等键", required = true)
+    private String orderCode;
+
+    @ApiModelProperty(value = "扣款金额(结算应付金额),允许大于卡当前余额,扣后余额可为负数", required = true)
+    private BigDecimal amount;
+
+    @ApiModelProperty(value = "操作人ID(设备链路传 null,默认记 0)")
+    private Long operatorId;
+}
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardSaveDTO.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardSaveDTO.java
index a3c8506..9d37341 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardSaveDTO.java
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardSaveDTO.java
@@ -19,6 +19,9 @@
     @ApiModelProperty(value = "卡号", required = true)
     private String cardNo;
 
+    @ApiModelProperty(value = "卡密码(明文,可选)。新增时配置该卡刷卡启动密码;编辑时传入则重置密码,留空表示不修改(V0.2:未配置密码的卡刷卡启动时不校验密码)")
+    private String password;
+
     @ApiModelProperty(value = "卡余额")
     private BigDecimal balance;
 
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/ChargingCardFallbackFactory.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/ChargingCardFallbackFactory.java
new file mode 100644
index 0000000..cc4e93e
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/ChargingCardFallbackFactory.java
@@ -0,0 +1,46 @@
+package com.ruoyi.account.api.factory;
+
+import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
+import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
+import com.ruoyi.account.api.feignClient.ChargingCardClient;
+import com.ruoyi.account.api.model.TChargingCard;
+import com.ruoyi.account.api.vo.ChargingCardDeductVO;
+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;
+
+/**
+ * 充电卡内部服务降级处理
+ */
+@Component
+public class ChargingCardFallbackFactory implements FallbackFactory<ChargingCardClient> {
+    private static final Logger log = LoggerFactory.getLogger(ChargingCardFallbackFactory.class);
+
+    @Override
+    public ChargingCardClient create(Throwable throwable) {
+        log.error("充电卡服务调用失败:{}", throwable.getMessage(), throwable);
+        return new ChargingCardClient() {
+            @Override
+            public R<TChargingCard> verifyCharging(ChargingCardAuthDTO dto) {
+                return R.fail("充电卡服务调用失败:" + throwable.getMessage());
+            }
+
+            @Override
+            public R<Boolean> lockForCharging(Long cardId, String orderCode) {
+                return R.fail("充电卡服务调用失败:" + throwable.getMessage());
+            }
+
+            @Override
+            public R<Boolean> unlockForCharging(Long cardId, String orderCode) {
+                return R.fail("充电卡服务调用失败:" + throwable.getMessage());
+            }
+
+            @Override
+            public R<ChargingCardDeductVO> deductForCharging(ChargingCardDeductDTO dto) {
+                return R.fail("充电卡服务调用失败:" + throwable.getMessage());
+            }
+        };
+    }
+}
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/VinManagementFallbackFactory.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/VinManagementFallbackFactory.java
index 66a9fea..c1b3fcb 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/VinManagementFallbackFactory.java
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/factory/VinManagementFallbackFactory.java
@@ -1,11 +1,14 @@
 package com.ruoyi.account.api.factory;
 
 import com.ruoyi.account.api.feignClient.VinManagementClient;
+import com.ruoyi.account.api.model.TVinManagement;
 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;
 
 /**
  * VIN 管理服务降级处理
@@ -17,6 +20,16 @@
     @Override
     public VinManagementClient create(Throwable throwable) {
         log.error("VIN 管理服务调用失败:{}", throwable.getMessage());
-        return appUserId -> R.fail("VIN 管理服务调用失败:" + throwable.getMessage());
+        return new VinManagementClient() {
+            @Override
+            public R<List<Long>> getVinIdsByAppUserId(Long appUserId) {
+                return R.fail("VIN 管理服务调用失败:" + throwable.getMessage());
+            }
+
+            @Override
+            public R<TVinManagement> getVinByCode(String vin) {
+                return R.fail("根据VIN码查询记录失败:" + throwable.getMessage());
+            }
+        };
     }
 }
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/ChargingCardClient.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/ChargingCardClient.java
new file mode 100644
index 0000000..3f65622
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/ChargingCardClient.java
@@ -0,0 +1,55 @@
+package com.ruoyi.account.api.feignClient;
+
+import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
+import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
+import com.ruoyi.account.api.factory.ChargingCardFallbackFactory;
+import com.ruoyi.account.api.model.TChargingCard;
+import com.ruoyi.account.api.vo.ChargingCardDeductVO;
+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 org.springframework.web.bind.annotation.RequestParam;
+
+/**
+ * 充电卡内部服务(刷卡启动鉴权、充电锁定、订单结算扣款)
+ * <p>仅限订单服务等内部链路调用,禁止走管理后台权限体系。</p>
+ */
+@FeignClient(contextId = "ChargingCardClient", value = ServiceNameConstants.ACCOUNT_SERVICE,
+        fallbackFactory = ChargingCardFallbackFactory.class)
+public interface ChargingCardClient {
+
+    /**
+     * 刷卡启动鉴权(V0.2 定稿:卡片配置了密码才校验密码,未配置不校验)
+     *
+     * @param dto 卡号 + 设备上报密码
+     * @return 鉴权失败时 R 的 code != 200 且 msg 为失败原因;成功时 data 为卡片信息(不含密码密文)
+     */
+    @PostMapping("/t-charging-card/inner/verify-charging")
+    R<TChargingCard> verifyCharging(@RequestBody ChargingCardAuthDTO dto);
+
+    /**
+     * 锁定卡片为充电中(订单创建成功后调用)。
+     *
+     * @return data=false 表示卡片已被其他进行中订单占用(锁定前置条件未命中),调用方应放弃建单
+     */
+    @PostMapping("/t-charging-card/inner/lock-charging")
+    R<Boolean> lockForCharging(@RequestParam("cardId") Long cardId,
+                               @RequestParam("orderCode") String orderCode);
+
+    /**
+     * 解锁卡片(订单到达终态后调用)。带订单号比对,防止误释放其他订单。
+     */
+    @PostMapping("/t-charging-card/inner/unlock-charging")
+    R<Boolean> unlockForCharging(@RequestParam("cardId") Long cardId,
+                                 @RequestParam("orderCode") String orderCode);
+
+    /**
+     * 刷卡订单最终结算扣款(幂等,允许扣成负余额)。
+     *
+     * @return data.duplicated=true 表示该订单此前已扣款成功,本次未重复扣款
+     */
+    @PostMapping("/t-charging-card/inner/deduct-charging")
+    R<ChargingCardDeductVO> deductForCharging(@RequestBody ChargingCardDeductDTO dto);
+}
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/VinManagementClient.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/VinManagementClient.java
index 2773fb1..0f904c5 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/VinManagementClient.java
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/feignClient/VinManagementClient.java
@@ -1,6 +1,7 @@
 package com.ruoyi.account.api.feignClient;
 
 import com.ruoyi.account.api.factory.VinManagementFallbackFactory;
+import com.ruoyi.account.api.model.TVinManagement;
 import com.ruoyi.common.core.constant.ServiceNameConstants;
 import com.ruoyi.common.core.domain.R;
 import org.springframework.cloud.openfeign.FeignClient;
@@ -18,4 +19,10 @@
 
     @GetMapping("/t-vin-management/inner/ids-by-app-user")
     R<List<Long>> getVinIdsByAppUserId(@RequestParam("appUserId") Long appUserId);
+
+    /**
+     * 根据VIN码查询未删除的VIN记录(订单侧VIN鉴权使用)
+     */
+    @GetMapping("/t-vin-management/inner/get-by-vin")
+    R<TVinManagement> getVinByCode(@RequestParam("vin") String vin);
 }
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java
index 70a9392..da8b468 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java
@@ -5,6 +5,7 @@
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.ruoyi.common.core.annotation.Excel;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
@@ -29,6 +30,11 @@
     @ApiModelProperty(value = "卡号")
     private String cardNo;
 
+    /** 卡密码(BCrypt 密文);为空表示未配置密码,刷卡启动时不校验密码(V0.2 定稿) */
+    @JsonIgnore
+    @ApiModelProperty(value = "卡密码(BCrypt 密文,仅内部存储与校验,不对外返回;空表示未配置,刷卡时不校验密码)")
+    private String password;
+
     @Excel(name = "卡余额", width = 16, scale = 2)
     @ApiModelProperty(value = "卡余额")
     private BigDecimal balance;
@@ -37,6 +43,14 @@
     @ApiModelProperty(value = "状态:1启用,2禁用")
     private Integer status;
 
+    /** 充电锁定标记:0 未在充电,1 充电中(进行中刷卡订单占用,管理操作互斥,V0.2 定稿) */
+    @ApiModelProperty(value = "充电锁定:0未充电,1充电中锁定")
+    private Integer chargingFlag;
+
+    /** 充电锁定订单号:解锁时必须与订单号比对,防止误释放其他订单 */
+    @ApiModelProperty(value = "充电锁定订单号")
+    private String chargingOrderCode;
+
     @TableLogic(value = "0", delval = "2")
     @ApiModelProperty(value = "删除状态:0正常,2删除")
     private Integer delFlag;
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/vo/ChargingCardDeductVO.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/vo/ChargingCardDeductVO.java
new file mode 100644
index 0000000..1848de9
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/vo/ChargingCardDeductVO.java
@@ -0,0 +1,33 @@
+package com.ruoyi.account.api.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+@ApiModel(value = "刷卡充电订单结算扣款结果")
+public class ChargingCardDeductVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "卡片ID")
+    private Long cardId;
+
+    @ApiModelProperty(value = "订单号(扣款幂等键)")
+    private String orderCode;
+
+    @ApiModelProperty(value = "扣款金额")
+    private BigDecimal amount;
+
+    @ApiModelProperty(value = "扣款前余额")
+    private BigDecimal beforeBalance;
+
+    @ApiModelProperty(value = "扣款后余额(允许为负数)")
+    private BigDecimal afterBalance;
+
+    @ApiModelProperty(value = "是否幂等重复请求(true=该订单此前已扣款成功,本次未重复扣款)")
+    private boolean duplicated;
+}
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/dto/DeviceStartChargingDTO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/dto/DeviceStartChargingDTO.java
new file mode 100644
index 0000000..c1fd404
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/dto/DeviceStartChargingDTO.java
@@ -0,0 +1,34 @@
+package com.ruoyi.order.api.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 设备主动发起充电(刷卡/VIN码)鉴权建单请求
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Data
+@ApiModel(value = "DeviceStartChargingDTO", description = "设备主动发起充电鉴权建单请求")
+public class DeviceStartChargingDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("充电桩编码")
+    private String chargingPileCode;
+
+    @ApiModelProperty("充电枪编号")
+    private String chargingGunCode;
+
+    @ApiModelProperty("启动方式(1=刷卡启动,2=账号启动(暂不支持),3=VIN码启动)")
+    private Integer startMethod;
+
+    @ApiModelProperty("账户(刷卡=卡号;VIN码=反序VIN原文)")
+    private String account;
+
+    @ApiModelProperty("卡密码(桩上送,可为空)")
+    private String password;
+}
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java
index 8773e9f..7a30556 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java
@@ -4,6 +4,7 @@
 import com.ruoyi.common.core.dto.ChargingOrderGroup;
 import com.ruoyi.common.core.dto.ChargingPercentProvinceDto;
 import com.ruoyi.order.api.dto.ChargingStatisticeDTO;
+import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
 import com.ruoyi.order.api.feignClient.ChargingOrderClient;
 import com.ruoyi.order.api.model.AddTripartitePlatformOrder;
 import com.ruoyi.order.api.model.ChargingListQuery;
@@ -253,6 +254,11 @@
             public R<Long> addChargingOrder(TChargingOrder chargingOrder) {
                 return R.fail("添加订单失败:" + throwable.getMessage());
             }
+
+            @Override
+            public R<DeviceStartChargingVO> deviceStartCharging(DeviceStartChargingDTO dto) {
+                return R.fail("设备主动发起充电鉴权建单失败:" + throwable.getMessage());
+            }
         };
     }
 }
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java
index c710110..1f09cd7 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java
@@ -6,6 +6,7 @@
 import com.ruoyi.common.core.dto.ChargingPercentProvinceDto;
 import com.ruoyi.common.core.web.domain.AjaxResult;
 import com.ruoyi.order.api.dto.ChargingStatisticeDTO;
+import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
 import com.ruoyi.order.api.factory.ChargingOrderFallbackFactory;
 import com.ruoyi.order.api.model.AddTripartitePlatformOrder;
 import com.ruoyi.order.api.model.ChargingListQuery;
@@ -364,4 +365,12 @@
      */
     @PostMapping("/t-charging-order/addChargingOrder")
     R<Long> addChargingOrder(@RequestBody TChargingOrder chargingOrder);
+
+    /**
+     * 设备主动发起充电(刷卡/VIN码)鉴权建单
+     * @param dto 启动鉴权请求(桩编码、枪编号、启动方式、账户、卡密码)
+     * @return 鉴权结果(含订单编号与失败原因)
+     */
+    @PostMapping("/t-charging-order/inner/device-start-charging")
+    R<DeviceStartChargingVO> deviceStartCharging(@RequestBody DeviceStartChargingDTO dto);
 }
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinAccountingStrategyDetailOrder.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinAccountingStrategyDetailOrder.java
new file mode 100644
index 0000000..29af5ce
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinAccountingStrategyDetailOrder.java
@@ -0,0 +1,18 @@
+package com.ruoyi.order.api.model;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * VIN码充电订单计费策略明细快照表(对应表 t_vin_accounting_strategy_detail_order)
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("t_vin_accounting_strategy_detail_order")
+public class TVinAccountingStrategyDetailOrder extends AccountingStrategyDetailOrder {
+    private static final long serialVersionUID = 1L;
+}
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinAccountingStrategyOrder.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinAccountingStrategyOrder.java
new file mode 100644
index 0000000..8a9faa8
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinAccountingStrategyOrder.java
@@ -0,0 +1,18 @@
+package com.ruoyi.order.api.model;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * VIN码充电订单计费策略快照主表(对应表 t_vin_accounting_strategy_order)
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("t_vin_accounting_strategy_order")
+public class TVinAccountingStrategyOrder extends AccountingStrategyOrder {
+    private static final long serialVersionUID = 1L;
+}
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinChargingOrderAccountingStrategy.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinChargingOrderAccountingStrategy.java
new file mode 100644
index 0000000..bd491e3
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinChargingOrderAccountingStrategy.java
@@ -0,0 +1,18 @@
+package com.ruoyi.order.api.model;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * VIN码充电订单计费明细表(对应表 t_vin_charging_order_accounting_strategy)
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("t_vin_charging_order_accounting_strategy")
+public class TVinChargingOrderAccountingStrategy extends TChargingOrderAccountingStrategy {
+    private static final long serialVersionUID = 1L;
+}
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/DeviceStartChargingVO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/DeviceStartChargingVO.java
new file mode 100644
index 0000000..a632156
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/DeviceStartChargingVO.java
@@ -0,0 +1,47 @@
+package com.ruoyi.order.api.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 设备主动发起充电鉴权建单结果
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Data
+@ApiModel(value = "DeviceStartChargingVO", description = "设备主动发起充电鉴权建单结果")
+public class DeviceStartChargingVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("鉴权结果(1=成功,0=失败)")
+    private Integer authentication;
+
+    @ApiModelProperty("失败原因(1=账户不存在,2=账户冻结,3=账户余额不足,4=该卡存在未结账记录,5=桩停用,7=密码错误,9=系统中vin码不存在,10=该桩存在未结账记录)")
+    private Integer failureCause;
+
+    @ApiModelProperty("订单编号(鉴权成功时返回,SD/VD前缀)")
+    private String orderCode;
+
+    @ApiModelProperty("账户余额(刷卡时返回,VIN码充电为空)")
+    private BigDecimal balance;
+
+    public static DeviceStartChargingVO success(String orderCode, BigDecimal balance) {
+        DeviceStartChargingVO vo = new DeviceStartChargingVO();
+        vo.setAuthentication(1);
+        vo.setOrderCode(orderCode);
+        vo.setBalance(balance);
+        return vo;
+    }
+
+    public static DeviceStartChargingVO fail(Integer failureCause) {
+        DeviceStartChargingVO vo = new DeviceStartChargingVO();
+        vo.setAuthentication(0);
+        vo.setFailureCause(failureCause);
+        return vo;
+    }
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TChargingCardController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TChargingCardController.java
index bd4e020..1b741cb 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TChargingCardController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TChargingCardController.java
@@ -1,12 +1,15 @@
 package com.ruoyi.account.controller;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
 import com.ruoyi.account.api.dto.ChargingCardBalanceDTO;
+import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
 import com.ruoyi.account.api.dto.ChargingCardExcelDTO;
 import com.ruoyi.account.api.dto.ChargingCardQueryDTO;
 import com.ruoyi.account.api.dto.ChargingCardSaveDTO;
 import com.ruoyi.account.api.dto.ChargingCardStatusDTO;
 import com.ruoyi.account.api.model.TChargingCard;
+import com.ruoyi.account.api.vo.ChargingCardDeductVO;
 import com.ruoyi.account.service.TChargingCardService;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.utils.poi.ExcelUtil;
@@ -22,6 +25,7 @@
 import org.springframework.web.bind.annotation.PutMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RequestPart;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
@@ -103,4 +107,32 @@
         List<ChargingCardExcelDTO> cards = util.importExcel(file.getInputStream());
         return R.ok(chargingCardService.importCards(cards));
     }
+
+    // ==================== 内部接口(刷卡充电链路,订单服务调用,V0.2 定稿) ====================
+
+    @ApiOperation(value = "刷卡启动鉴权(内部):卡片配置密码时才校验密码")
+    @PostMapping("/inner/verify-charging")
+    public R<TChargingCard> verifyCharging(@RequestBody ChargingCardAuthDTO dto) {
+        return R.ok(chargingCardService.verifyCardForCharging(dto));
+    }
+
+    @ApiOperation(value = "锁定卡片为充电中(内部):返回 false 表示同卡已有进行中订单")
+    @PostMapping("/inner/lock-charging")
+    public R<Boolean> lockForCharging(@RequestParam("cardId") Long cardId,
+                                      @RequestParam("orderCode") String orderCode) {
+        return R.ok(chargingCardService.lockCardForCharging(cardId, orderCode));
+    }
+
+    @ApiOperation(value = "解锁卡片(内部):订单终态后调用,带订单号比对")
+    @PostMapping("/inner/unlock-charging")
+    public R<Boolean> unlockForCharging(@RequestParam("cardId") Long cardId,
+                                        @RequestParam("orderCode") String orderCode) {
+        return R.ok(chargingCardService.unlockCardForCharging(cardId, orderCode));
+    }
+
+    @ApiOperation(value = "刷卡订单结算扣款(内部):订单号幂等、允许扣成负余额")
+    @PostMapping("/inner/deduct-charging")
+    public R<ChargingCardDeductVO> deductForCharging(@RequestBody ChargingCardDeductDTO dto) {
+        return R.ok(chargingCardService.deductBalanceForCharging(dto));
+    }
 }
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TVinManagementController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TVinManagementController.java
index f62aa5e..0f68e4f 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TVinManagementController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TVinManagementController.java
@@ -57,6 +57,14 @@
         return R.ok(service.unbindAppletVehicle(id, tokenService.getLoginUserApplet().getUserId()));
     }
 
+    /**
+     * 内部接口:根据VIN码查询未删除的VIN记录(订单侧VIN鉴权使用)
+     */
+    @GetMapping("/inner/get-by-vin")
+    public R<TVinManagement> getByVin(@RequestParam("vin") String vin) {
+        return R.ok(service.getByVin(vin));
+    }
+
     @GetMapping("/inner/ids-by-app-user")
     public R<List<Long>> getVinIdsByAppUserId(@RequestParam Long appUserId) {
         return R.ok(service.getVinIdsByAppUserId(appUserId));
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/mapper/TChargingCardMapper.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/mapper/TChargingCardMapper.java
index 3a98b6f..467258e 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/mapper/TChargingCardMapper.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/mapper/TChargingCardMapper.java
@@ -31,4 +31,27 @@
                             @Param("amount") BigDecimal amount,
                             @Param("remark") String remark,
                             @Param("createBy") Long createBy);
+
+    /**
+     * 锁定卡片为充电中(条件更新:仅空闲卡可锁定,用于防止同卡并发多单)
+     */
+    int lockCardForCharging(@Param("cardId") Long cardId, @Param("orderCode") String orderCode);
+
+    /**
+     * 解锁卡片(带订单号比对,防止误释放其他订单)
+     */
+    int unlockCardForCharging(@Param("cardId") Long cardId, @Param("orderCode") String orderCode);
+
+    /**
+     * 订单结算原子扣款:仅按卡 ID 扣减,不限制余额下限,允许扣成负数(V0.2 定稿)
+     */
+    int deductBalanceByCardId(@Param("cardId") Long cardId, @Param("amount") BigDecimal amount);
+
+    /**
+     * 写入订单结算扣款流水并占用幂等键(charging_order_code 唯一索引)
+     */
+    int insertOrderDeductRecord(@Param("cardId") Long cardId,
+                                @Param("amount") BigDecimal amount,
+                                @Param("orderCode") String orderCode,
+                                @Param("createBy") Long createBy);
 }
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TChargingCardService.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TChargingCardService.java
index 1e89501..bbdfb8f 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TChargingCardService.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TChargingCardService.java
@@ -2,12 +2,15 @@
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
 import com.ruoyi.account.api.dto.ChargingCardBalanceDTO;
+import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
 import com.ruoyi.account.api.dto.ChargingCardExcelDTO;
 import com.ruoyi.account.api.dto.ChargingCardQueryDTO;
 import com.ruoyi.account.api.dto.ChargingCardSaveDTO;
 import com.ruoyi.account.api.dto.ChargingCardStatusDTO;
 import com.ruoyi.account.api.model.TChargingCard;
+import com.ruoyi.account.api.vo.ChargingCardDeductVO;
 
 import java.util.List;
 
@@ -28,4 +31,31 @@
     List<ChargingCardExcelDTO> selectExportList(ChargingCardQueryDTO query);
 
     String importCards(List<ChargingCardExcelDTO> cards);
+
+    /**
+     * 刷卡启动鉴权(内部接口)。
+     * 条件密码校验(V0.2 定稿):卡片配置了密码时校验设备上报密码;未配置密码时不校验。
+     *
+     * @param dto 卡号与设备上报密码
+     * @return 鉴权通过返回卡片实体(不包含密码密文);失败抛出 ServiceException 携带原因
+     */
+    TChargingCard verifyCardForCharging(ChargingCardAuthDTO dto);
+
+    /**
+     * 锁定卡片为充电中(刷卡订单创建成功后调用)。同卡已有进行中订单时返回 false。
+     */
+    boolean lockCardForCharging(Long cardId, String orderCode);
+
+    /**
+     * 解锁卡片(订单终态后调用)。带订单号比对,幂等:已解锁或订单号不匹配时返回 false。
+     */
+    boolean unlockCardForCharging(Long cardId, String orderCode);
+
+    /**
+     * 刷卡订单最终结算扣款(内部接口)。
+     * 幂等(订单号为幂等键)+ 允许扣成负余额(V0.2 定稿)。
+     *
+     * @return 扣款结果;duplicated=true 表示该订单此前已扣款,本次未重复扣款
+     */
+    ChargingCardDeductVO deductBalanceForCharging(ChargingCardDeductDTO dto);
 }
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TVinManagementService.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TVinManagementService.java
index 3ebd03f..d8d2e6d 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TVinManagementService.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TVinManagementService.java
@@ -20,4 +20,9 @@
     boolean bindAppletVehicle(Long appUserId, String keyword);
     boolean unbindAppletVehicle(Long id, Long appUserId);
     List<Long> getVinIdsByAppUserId(Long appUserId);
+
+    /**
+     * 根据VIN码查询未删除的VIN记录(含禁用状态,由调用方判断)
+     */
+    TVinManagement getByVin(String vin);
 }
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java
index d0829ca..c5176b1 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java
@@ -1,14 +1,18 @@
 package com.ruoyi.account.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
 import com.ruoyi.account.api.dto.ChargingCardBalanceDTO;
+import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
 import com.ruoyi.account.api.dto.ChargingCardExcelDTO;
 import com.ruoyi.account.api.dto.ChargingCardQueryDTO;
 import com.ruoyi.account.api.dto.ChargingCardSaveDTO;
 import com.ruoyi.account.api.dto.ChargingCardStatusDTO;
 import com.ruoyi.account.api.model.TChargingCard;
+import com.ruoyi.account.api.vo.ChargingCardDeductVO;
 import com.ruoyi.account.mapper.TChargingCardMapper;
 import com.ruoyi.account.service.TChargingCardService;
 import com.ruoyi.common.core.domain.R;
@@ -19,9 +23,13 @@
 import com.ruoyi.system.api.domain.SysUser;
 import com.ruoyi.system.api.feignClient.SysUserClient;
 import com.ruoyi.system.api.model.LoginUser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionTemplate;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
@@ -33,17 +41,29 @@
 public class TChargingCardServiceImpl extends ServiceImpl<TChargingCardMapper, TChargingCard>
         implements TChargingCardService {
 
+    private static final Logger log = LoggerFactory.getLogger(TChargingCardServiceImpl.class);
+
     private static final int ENABLED = 1;
     private static final int DISABLED = 2;
     private static final int BALANCE_ADD = 1;
     private static final int BALANCE_SUBTRACT = 2;
+    /** 余额变动类型:3=刷卡订单充电结算扣款(V0.2 定稿,订单扣款专用) */
+    private static final int BALANCE_TYPE_ORDER_DEDUCT = 3;
+    /** 充电锁定标记:1=充电中(业务锁定,管理操作互斥) */
+    private static final int CHARGING_FLAG_LOCKED = 1;
+    /** 设备链路无登录操作人,流水统一记 0 */
+    private static final long SYSTEM_OPERATOR = 0L;
     private static final BigDecimal MAX_AMOUNT = new BigDecimal("9999999999.99");
+    private static final int PASSWORD_MAX_LENGTH = 64;
 
     @Autowired
     private TokenService tokenService;
 
     @Autowired
     private SysUserClient sysUserClient;
+
+    @Autowired
+    private TransactionTemplate transactionTemplate;
 
     @Override
     public IPage<TChargingCard> selectCardPage(ChargingCardQueryDTO query) {
@@ -61,6 +81,9 @@
         card.setCardNo(dto.getCardNo().trim());
         card.setBalance(dto.getBalance() == null ? BigDecimal.ZERO : dto.getBalance());
         card.setStatus(dto.getStatus() == null ? ENABLED : dto.getStatus());
+        card.setPassword(encodePasswordIfPresent(dto.getPassword()));
+        card.setChargingFlag(0);
+        card.setChargingOrderCode(null);
         card.setDelFlag(0);
         card.setCreateTime(LocalDateTime.now());
         card.setCreateBy(SecurityUtils.getUserId());
@@ -73,13 +96,20 @@
     @Override
     public boolean updateCard(ChargingCardSaveDTO dto) {
         validateCard(dto, true);
-        if (getById(dto.getId()) == null) {
+        TChargingCard existing = getById(dto.getId());
+        if (existing == null) {
             throw new ServiceException("卡片不存在或已删除");
         }
+        assertNotChargingLocked(existing, "修改卡片或重置卡密码");
         TChargingCard card = new TChargingCard();
         card.setId(dto.getId());
         card.setCardNo(dto.getCardNo().trim());
         card.setStatus(dto.getStatus() == null ? ENABLED : dto.getStatus());
+        // 仅当传入新密码时才重置卡密码;留空表示不修改(V0.2 定稿)
+        String encodedPassword = encodePasswordIfPresent(dto.getPassword());
+        if (encodedPassword != null) {
+            card.setPassword(encodedPassword);
+        }
         card.setUpdateTime(LocalDateTime.now());
         card.setUpdateBy(SecurityUtils.getUserId());
         if (!updateById(card)) {
@@ -94,9 +124,11 @@
             throw new ServiceException("请选择需要删除的卡片");
         }
         List<Long> distinctIds = new java.util.ArrayList<>(new HashSet<>(ids));
-        if (listByIds(distinctIds).size() != distinctIds.size()) {
+        List<TChargingCard> cards = listByIds(distinctIds);
+        if (cards.size() != distinctIds.size()) {
             throw new ServiceException("部分卡片不存在或已删除");
         }
+        assertNotChargingLocked(cards, "删除卡片");
         if (!removeByIds(distinctIds)) {
             throw new ServiceException("删除卡片失败");
         }
@@ -108,9 +140,11 @@
         if (dto == null || dto.getId() == null || !isValidStatus(dto.getStatus())) {
             throw new ServiceException("卡片状态参数错误");
         }
-        if (getById(dto.getId()) == null) {
+        TChargingCard existing = getById(dto.getId());
+        if (existing == null) {
             throw new ServiceException("卡片不存在或已删除");
         }
+        assertNotChargingLocked(existing, "启用/禁用卡片");
         TChargingCard card = new TChargingCard();
         card.setId(dto.getId());
         card.setStatus(dto.getStatus());
@@ -132,12 +166,13 @@
         if (cards.size() != new HashSet<>(dto.getIds()).size()) {
             throw new ServiceException("部分卡片不存在或已删除");
         }
+        assertNotChargingLocked(cards, "调整余额");
         int updated = baseMapper.adjustBalance(dto.getIds(), dto.getAmount(), dto.getType(),
                 MAX_AMOUNT, SecurityUtils.getUserId());
         if (updated != cards.size()) {
             throw new ServiceException(dto.getType() == BALANCE_SUBTRACT
-                    ? "部分卡片余额不足,无法完成扣减"
-                    : "部分卡片余额超过上限,无法完成增加");
+                    ? "部分卡片正在充电中或余额不足,无法完成扣减"
+                    : "部分卡片正在充电中或余额超过上限,无法完成增加");
         }
         for (TChargingCard card : cards) {
             baseMapper.insertBalanceRecord(card.getId(), dto.getType(), dto.getAmount(),
@@ -187,12 +222,104 @@
             card.setCardNo(item.getCardNo().trim());
             card.setBalance(item.getBalance() == null ? BigDecimal.ZERO : item.getBalance());
             card.setStatus(parseExcelStatus(item.getStatus(), 0));
+            card.setChargingFlag(0);
             card.setDelFlag(0);
             card.setCreateTime(now);
             card.setCreateBy(userId);
             save(card);
         }
         return "成功导入" + cards.size() + "条卡片数据";
+    }
+
+    @Override
+    public TChargingCard verifyCardForCharging(ChargingCardAuthDTO dto) {
+        if (dto == null || StringUtils.isEmpty(dto.getCardNo())) {
+            throw new ServiceException("卡号不能为空");
+        }
+        TChargingCard card = getOne(new LambdaQueryWrapper<TChargingCard>()
+                .eq(TChargingCard::getCardNo, dto.getCardNo().trim()));
+        if (card == null) {
+            throw new ServiceException("卡片不存在或已删除");
+        }
+        if (card.getStatus() == null || card.getStatus() != ENABLED) {
+            throw new ServiceException("卡片已禁用,无法启动充电");
+        }
+        // 条件密码校验(V0.2 定稿):卡片配置了密码才校验;未配置密码不要求、不校验
+        if (StringUtils.isNotEmpty(card.getPassword())) {
+            if (StringUtils.isEmpty(dto.getPassword())) {
+                throw new ServiceException("卡片已配置密码,请输入卡密码");
+            }
+            if (!SecurityUtils.matchesPassword(dto.getPassword(), card.getPassword())) {
+                throw new ServiceException("卡密码错误");
+            }
+        }
+        return card;
+    }
+
+    @Override
+    public boolean lockCardForCharging(Long cardId, String orderCode) {
+        if (cardId == null || StringUtils.isEmpty(orderCode)) {
+            throw new ServiceException("卡片ID与订单号不能为空");
+        }
+        return baseMapper.lockCardForCharging(cardId, orderCode.trim()) == 1;
+    }
+
+    @Override
+    public boolean unlockCardForCharging(Long cardId, String orderCode) {
+        if (cardId == null || StringUtils.isEmpty(orderCode)) {
+            throw new ServiceException("卡片ID与订单号不能为空");
+        }
+        if (baseMapper.unlockCardForCharging(cardId, orderCode.trim()) == 1) {
+            return true;
+        }
+        TChargingCard card = getById(cardId);
+        if (card == null) {
+            throw new ServiceException("卡片不存在或已删除");
+        }
+        // 已解锁(订单号已被清空)视为幂等成功;仍被其他订单占用则返回 false,防止误释放
+        return card.getChargingOrderCode() == null || orderCode.trim().equals(card.getChargingOrderCode());
+    }
+
+    @Override
+    public ChargingCardDeductVO deductBalanceForCharging(ChargingCardDeductDTO dto) {
+        validateDeductRequest(dto);
+        try {
+            return transactionTemplate.execute(status -> {
+                TChargingCard card = getById(dto.getCardId());
+                if (card == null) {
+                    throw new ServiceException("卡片不存在或已删除");
+                }
+                BigDecimal before = card.getBalance() == null ? BigDecimal.ZERO : card.getBalance();
+                // 第一步先占扣款幂等键:同一订单并发结算时,唯一索引 uk_order_code 只允许一个事务插入成功
+                baseMapper.insertOrderDeductRecord(dto.getCardId(), dto.getAmount(),
+                        dto.getOrderCode(), dto.getOperatorId() == null ? SYSTEM_OPERATOR : dto.getOperatorId());
+                // 第二步原子扣减:允许扣成负余额,禁止携带 balance >= amount 下限条件(V0.2 定稿)
+                int updated = baseMapper.deductBalanceByCardId(dto.getCardId(), dto.getAmount());
+                if (updated != 1) {
+                    throw new ServiceException("卡片不存在或已删除,扣款失败");
+                }
+                TChargingCard afterCard = getById(dto.getCardId());
+                BigDecimal after = afterCard == null || afterCard.getBalance() == null
+                        ? BigDecimal.ZERO : afterCard.getBalance();
+                ChargingCardDeductVO vo = new ChargingCardDeductVO();
+                vo.setCardId(dto.getCardId());
+                vo.setOrderCode(dto.getOrderCode());
+                vo.setAmount(dto.getAmount());
+                vo.setBeforeBalance(before);
+                vo.setAfterBalance(after);
+                vo.setDuplicated(false);
+                return vo;
+            });
+        } catch (DuplicateKeyException e) {
+            // 唯一键冲突:该订单此前已扣款成功(含并发下慢事务先提交),按幂等重复返回,不重复扣款
+            log.warn("刷卡订单结算扣款幂等命中,不重复扣款。订单号={}, 卡片ID={}", dto.getOrderCode(), dto.getCardId());
+            ChargingCardDeductVO vo = new ChargingCardDeductVO();
+            vo.setCardId(dto.getCardId());
+            vo.setOrderCode(dto.getOrderCode());
+            vo.setAmount(dto.getAmount());
+            vo.setDuplicated(true);
+            return vo;
+        }
     }
 
     private void validateCard(ChargingCardSaveDTO dto, boolean update) {
@@ -206,6 +333,11 @@
         if (cardNo.length() > 64) {
             throw new ServiceException("卡号不能超过64个字符");
         }
+        if (dto.getPassword() != null && !dto.getPassword().trim().isEmpty()) {
+            if (dto.getPassword().length() > PASSWORD_MAX_LENGTH) {
+                throw new ServiceException("卡密码不能超过" + PASSWORD_MAX_LENGTH + "个字符");
+            }
+        }
         if (!update && dto.getBalance() != null) {
             validateAmount(dto.getBalance(), true, "卡余额");
         }
@@ -214,6 +346,35 @@
         }
         if (existsCardNo(cardNo, dto.getId())) {
             throw new ServiceException("卡号已存在");
+        }
+    }
+
+    private String encodePasswordIfPresent(String rawPassword) {
+        if (rawPassword == null || rawPassword.trim().isEmpty()) {
+            return null;
+        }
+        return SecurityUtils.encryptPassword(rawPassword.trim());
+    }
+
+    /**
+     * 卡片处于充电锁定状态时拒绝管理操作(V0.2 定稿)。
+     *
+     * @param cards  待操作的卡片
+     * @param action 操作描述,用于异常提示
+     */
+    private void assertNotChargingLocked(List<TChargingCard> cards, String action) {
+        if (cards == null) {
+            return;
+        }
+        for (TChargingCard card : cards) {
+            assertNotChargingLocked(card, action);
+        }
+    }
+
+    private void assertNotChargingLocked(TChargingCard card, String action) {
+        if (card.getChargingFlag() != null && card.getChargingFlag() == CHARGING_FLAG_LOCKED) {
+            throw new ServiceException("卡片【" + card.getCardNo() + "】正在充电中,禁止" + action
+                    + ",充电结束后可重试");
         }
     }
 
@@ -237,6 +398,22 @@
         }
     }
 
+    private void validateDeductRequest(ChargingCardDeductDTO dto) {
+        if (dto == null) {
+            throw new ServiceException("扣款参数不能为空");
+        }
+        if (dto.getCardId() == null) {
+            throw new ServiceException("卡片ID不能为空");
+        }
+        if (StringUtils.isEmpty(dto.getOrderCode())) {
+            throw new ServiceException("订单号不能为空");
+        }
+        if (dto.getOrderCode().length() > 64) {
+            throw new ServiceException("订单号不能超过64个字符");
+        }
+        validateAmount(dto.getAmount(), false, "扣款金额");
+    }
+
     private void validateAmount(BigDecimal amount, boolean allowZero, String fieldName) {
         if (amount == null || (allowZero ? amount.compareTo(BigDecimal.ZERO) < 0 : amount.compareTo(BigDecimal.ZERO) <= 0)) {
             throw new ServiceException(fieldName + (allowZero ? "不能小于0" : "必须大于0"));
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TVinManagementServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TVinManagementServiceImpl.java
index 0c96fb1..2c306f3 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TVinManagementServiceImpl.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TVinManagementServiceImpl.java
@@ -92,6 +92,13 @@
         return true;
     }
     @Override public List<Long> getVinIdsByAppUserId(Long appUserId) { return baseMapper.selectIdsByAppUserId(appUserId); }
+    @Override public TVinManagement getByVin(String vin) {
+        if (StringUtils.isEmpty(vin)) return null;
+        return lambdaQuery().eq(TVinManagement::getVin, vin.trim())
+                .orderByDesc(TVinManagement::getId)
+                .last("limit 1")
+                .one();
+    }
     @Override @Transactional(rollbackFor = Exception.class) public String importVins(List<VinManagementExcelDTO> items) { if (items == null || items.isEmpty()) throw new ServiceException("导入数据不能为空"); Set<String> vins = new HashSet<>(); int row = 1; for (VinManagementExcelDTO item : items) { row++; if (item == null || StringUtils.isEmpty(item.getVin())) throw new ServiceException("第" + row + "行VIN码不能为空"); String vin = item.getVin().trim(); if (vin.length() > 64) throw new ServiceException("第" + row + "行VIN码不能超过64个字符"); if (!vins.add(vin) || baseMapper.countByVin(vin, null) > 0) throw new ServiceException("第" + row + "行VIN码已存在或重复"); parseStatus(item.getStatus(), row); } Long userId = SecurityUtils.getUserId(); LocalDateTime now = LocalDateTime.now(); for (VinManagementExcelDTO item : items) { TVinManagement v = new TVinManagement(); v.setVin(item.getVin().trim()); v.setPlateNumber(trim(item.getPlateNumber())); v.setPhone(trim(item.getPhone())); v.setCompanyName(trim(item.getCompanyName())); v.setRemark(trim(item.getRemark())); v.setStatus(parseStatus(item.getStatus(), 0)); v.setDelFlag(0); v.setCreateBy(userId); v.setCreateTime(now); save(v); } return "成功导入" + items.size() + "条VIN码数据"; }
     private void validate(VinManagementSaveDTO dto, boolean update) { if (dto == null || StringUtils.isEmpty(dto.getVin())) throw new ServiceException("VIN码不能为空"); if (update && dto.getId() == null) throw new ServiceException("VIN码ID不能为空"); String vin = dto.getVin().trim(); if (vin.length() > 64) throw new ServiceException("VIN码不能超过64个字符"); if (!validStatus(dto.getStatus() == null ? ENABLED : dto.getStatus())) throw new ServiceException("VIN码状态参数错误"); if (baseMapper.countByVin(vin, dto.getId()) > 0) throw new ServiceException("VIN码已存在"); }
     private TVinManagement from(VinManagementSaveDTO d) { TVinManagement v = new TVinManagement(); v.setVin(d.getVin().trim()); v.setPlateNumber(trim(d.getPlateNumber())); v.setPhone(trim(d.getPhone())); v.setCompanyName(trim(d.getCompanyName())); v.setRemark(trim(d.getRemark())); v.setStatus(d.getStatus() == null ? ENABLED : d.getStatus()); return v; }
diff --git a/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml b/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml
index 174516f..8be81b6 100644
--- a/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml
+++ b/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml
@@ -49,6 +49,7 @@
         </if>
     </select>
 
+    <!-- 人工调账:仍保持"增加不超过上限、扣减不低过 0"的运营约束,并叠加充电锁定条件兜底(V0.2 定稿) -->
     <update id="adjustBalance">
         UPDATE t_charging_card
         SET balance = balance
@@ -59,6 +60,7 @@
             update_time = NOW(),
             update_by = #{updateBy}
         WHERE del_flag = 0
+          AND charging_flag = 0
           AND id IN
         <foreach collection="ids" item="id" open="(" separator="," close=")">
             #{id}
@@ -80,4 +82,43 @@
             (#{cardId}, #{type}, #{amount}, #{remark}, NOW(), #{createBy})
     </insert>
 
+    <!-- 锁定:仅空闲卡可锁定,条件更新防止同卡并发多单 -->
+    <update id="lockCardForCharging">
+        UPDATE t_charging_card
+        SET charging_flag = 1,
+            charging_order_code = #{orderCode},
+            update_time = NOW()
+        WHERE id = #{cardId}
+          AND del_flag = 0
+          AND charging_flag = 0
+    </update>
+
+    <!-- 解锁:带订单号比对,防止误释放其他进行中订单;已解锁(order_code 已清空)时影响行数为 0 -->
+    <update id="unlockCardForCharging">
+        UPDATE t_charging_card
+        SET charging_flag = 0,
+            charging_order_code = NULL,
+            update_time = NOW()
+        WHERE id = #{cardId}
+          AND del_flag = 0
+          AND charging_order_code = #{orderCode}
+    </update>
+
+    <!-- 订单结算原子扣款:只按卡 ID 扣减,禁止带 balance &gt;= amount 下限条件,允许扣成负数 -->
+    <update id="deductBalanceByCardId">
+        UPDATE t_charging_card
+        SET balance = balance - #{amount},
+            update_time = NOW()
+        WHERE id = #{cardId}
+          AND del_flag = 0
+    </update>
+
+    <!-- 订单结算扣款流水:charging_order_code 唯一索引即扣款幂等键(uk_order_code) -->
+    <insert id="insertOrderDeductRecord">
+        INSERT INTO t_charging_card_balance_record
+            (card_id, change_type, amount, remark, charging_order_code, create_time, create_by)
+        VALUES
+            (#{cardId}, 3, #{amount}, #{orderCode}, #{orderCode}, NOW(), #{createBy})
+    </insert>
+
 </mapper>
diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/TChargingCardControllerTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/TChargingCardControllerTest.java
index c82f133..18e0700 100644
--- a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/TChargingCardControllerTest.java
+++ b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/controller/TChargingCardControllerTest.java
@@ -1,12 +1,15 @@
 package com.ruoyi.account.controller;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
 import com.ruoyi.account.api.dto.ChargingCardBalanceDTO;
+import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
 import com.ruoyi.account.api.dto.ChargingCardExcelDTO;
 import com.ruoyi.account.api.dto.ChargingCardQueryDTO;
 import com.ruoyi.account.api.dto.ChargingCardSaveDTO;
 import com.ruoyi.account.api.dto.ChargingCardStatusDTO;
 import com.ruoyi.account.api.model.TChargingCard;
+import com.ruoyi.account.api.vo.ChargingCardDeductVO;
 import com.ruoyi.account.service.TChargingCardService;
 import com.ruoyi.common.core.domain.R;
 import org.junit.jupiter.api.BeforeEach;
@@ -116,6 +119,49 @@
         verify(service).importCards(any());
     }
 
+    @Test
+    void innerVerifyChargingDelegatesToService() {
+        ChargingCardAuthDTO dto = new ChargingCardAuthDTO();
+        dto.setCardNo("CARD-1");
+        TChargingCard card = new TChargingCard();
+        card.setId(1L);
+        when(service.verifyCardForCharging(dto)).thenReturn(card);
+
+        R<TChargingCard> result = controller.verifyCharging(dto);
+
+        assertEquals(200, result.getCode());
+        assertEquals(card, result.getData());
+        verify(service).verifyCardForCharging(dto);
+    }
+
+    @Test
+    void innerLockAndUnlockDelegateToService() {
+        when(service.lockCardForCharging(1L, "SD20260909001")).thenReturn(true);
+        when(service.unlockCardForCharging(1L, "SD20260909001")).thenReturn(true);
+
+        assertTrue(controller.lockForCharging(1L, "SD20260909001").getData());
+        assertTrue(controller.unlockForCharging(1L, "SD20260909001").getData());
+        verify(service).lockCardForCharging(1L, "SD20260909001");
+        verify(service).unlockCardForCharging(1L, "SD20260909001");
+    }
+
+    @Test
+    void innerDeductChargingDelegatesToService() {
+        ChargingCardDeductDTO dto = new ChargingCardDeductDTO();
+        dto.setCardId(1L);
+        dto.setOrderCode("SD20260909001");
+        dto.setAmount(java.math.BigDecimal.TEN);
+        ChargingCardDeductVO vo = new ChargingCardDeductVO();
+        vo.setDuplicated(false);
+        when(service.deductBalanceForCharging(dto)).thenReturn(vo);
+
+        R<ChargingCardDeductVO> result = controller.deductForCharging(dto);
+
+        assertEquals(200, result.getCode());
+        assertEquals(vo, result.getData());
+        verify(service).deductBalanceForCharging(dto);
+    }
+
     private byte[] excelBytes() throws Exception {
         org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook();
         try {
diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java
index a2a315d..764cf47 100644
--- a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java
+++ b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java
@@ -1,17 +1,22 @@
 package com.ruoyi.account.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
 import com.ruoyi.account.api.dto.ChargingCardBalanceDTO;
+import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
 import com.ruoyi.account.api.dto.ChargingCardExcelDTO;
 import com.ruoyi.account.api.dto.ChargingCardSaveDTO;
 import com.ruoyi.account.api.dto.ChargingCardStatusDTO;
 import com.ruoyi.account.api.model.TChargingCard;
+import com.ruoyi.account.api.vo.ChargingCardDeductVO;
 import com.ruoyi.account.mapper.TChargingCardMapper;
 import com.ruoyi.common.core.context.SecurityContextHolder;
 import com.ruoyi.common.core.exception.ServiceException;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.common.security.utils.SecurityUtils;
 import com.ruoyi.system.api.domain.SysUser;
 import com.ruoyi.system.api.feignClient.SysUserClient;
 import com.ruoyi.system.api.model.LoginUser;
@@ -22,7 +27,10 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.dao.DuplicateKeyException;
 import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.transaction.support.TransactionCallback;
+import org.springframework.transaction.support.TransactionTemplate;
 
 import java.math.BigDecimal;
 import java.util.Arrays;
@@ -35,6 +43,7 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doReturn;
@@ -49,6 +58,7 @@
     @Mock private TChargingCardMapper mapper;
     @Mock private TokenService tokenService;
     @Mock private SysUserClient sysUserClient;
+    @Mock private TransactionTemplate transactionTemplate;
     private TChargingCardServiceImpl service;
 
     @BeforeEach
@@ -57,6 +67,7 @@
         ReflectionTestUtils.setField(service, "baseMapper", mapper);
         ReflectionTestUtils.setField(service, "tokenService", tokenService);
         ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient);
+        ReflectionTestUtils.setField(service, "transactionTemplate", transactionTemplate);
         SecurityContextHolder.setUserId("123");
     }
 
@@ -216,4 +227,358 @@
         assertTrue(service.selectExportList(null).isEmpty());
         verify(mapper).selectExportList(any());
     }
+
+    // ==================== V0.2 定稿:卡密码配置(仅配置密码的卡刷卡时才校验) ====================
+
+    @Test
+    void saveCardEncodesPasswordWhenProvided() {
+        when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0);
+        when(mapper.insert(any(TChargingCard.class))).thenAnswer(invocation -> 1);
+        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
+        dto.setCardNo("CARD-PWD");
+        dto.setPassword(" 123456 ");
+
+        assertTrue(service.saveCard(dto));
+
+        ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class);
+        verify(mapper).insert(card.capture());
+        assertNotNull(card.getValue().getPassword());
+        assertTrue(SecurityUtils.matchesPassword("123456", card.getValue().getPassword()));
+        assertEquals(Integer.valueOf(0), card.getValue().getChargingFlag());
+    }
+
+    @Test
+    void saveCardKeepsPasswordNullWhenNotConfigured() {
+        when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0);
+        when(mapper.insert(any(TChargingCard.class))).thenAnswer(invocation -> 1);
+        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
+        dto.setCardNo("CARD-NOPWD");
+
+        assertTrue(service.saveCard(dto));
+
+        ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class);
+        verify(mapper).insert(card.capture());
+        assertEquals(null, card.getValue().getPassword());
+    }
+
+    @Test
+    void updateCardRejectsCardInChargingLock() {
+        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
+        dto.setId(7L);
+        dto.setCardNo("CARD-7");
+        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
+        TChargingCard locked = new TChargingCard();
+        locked.setId(7L);
+        locked.setCardNo("CARD-7");
+        locked.setChargingFlag(1);
+        when(mapper.selectById(7L)).thenReturn(locked);
+
+        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateCard(dto));
+
+        assertTrue(exception.getMessage().contains("正在充电中"));
+        verify(mapper, never()).updateById(any());
+    }
+
+    @Test
+    void updateCardResetsPasswordOnlyWhenProvided() {
+        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
+        dto.setId(7L);
+        dto.setCardNo("CARD-7");
+        dto.setPassword("new-pwd");
+        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
+        TChargingCard existing = new TChargingCard();
+        existing.setId(7L);
+        existing.setCardNo("CARD-7");
+        when(mapper.selectById(7L)).thenReturn(existing);
+        when(mapper.updateById(any(TChargingCard.class))).thenReturn(1);
+
+        assertTrue(service.updateCard(dto));
+
+        ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class);
+        verify(mapper).updateById(card.capture());
+        assertNotNull(card.getValue().getPassword());
+        assertTrue(SecurityUtils.matchesPassword("new-pwd", card.getValue().getPassword()));
+    }
+
+    @Test
+    void updateCardKeepsPasswordWhenNotProvided() {
+        ChargingCardSaveDTO dto = new ChargingCardSaveDTO();
+        dto.setId(7L);
+        dto.setCardNo("CARD-7");
+        when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0);
+        TChargingCard existing = new TChargingCard();
+        existing.setId(7L);
+        existing.setCardNo("CARD-7");
+        when(mapper.selectById(7L)).thenReturn(existing);
+        when(mapper.updateById(any(TChargingCard.class))).thenReturn(1);
+
+        assertTrue(service.updateCard(dto));
+
+        ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class);
+        verify(mapper).updateById(card.capture());
+        assertEquals(null, card.getValue().getPassword());
+    }
+
+    // ==================== V0.2 定稿:充电期间卡片管理操作互斥 ====================
+
+    @Test
+    void deleteCardsRejectsCardInChargingLock() {
+        TChargingCard first = new TChargingCard();
+        first.setId(1L);
+        TChargingCard locked = new TChargingCard();
+        locked.setId(2L);
+        locked.setCardNo("CARD-2");
+        locked.setChargingFlag(1);
+        doReturn(Arrays.asList(first, locked)).when(service).listByIds(anyList());
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.deleteCards(Arrays.asList(1L, 2L)));
+
+        assertTrue(exception.getMessage().contains("正在充电中"));
+        verify(service, never()).removeByIds(anyList());
+    }
+
+    @Test
+    void updateStatusRejectsCardInChargingLock() {
+        ChargingCardStatusDTO dto = new ChargingCardStatusDTO();
+        dto.setId(1L);
+        dto.setStatus(2);
+        TChargingCard locked = new TChargingCard();
+        locked.setId(1L);
+        locked.setCardNo("CARD-1");
+        locked.setChargingFlag(1);
+        when(mapper.selectById(1L)).thenReturn(locked);
+
+        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateStatus(dto));
+
+        assertTrue(exception.getMessage().contains("正在充电中"));
+        verify(mapper, never()).updateById(any());
+    }
+
+    @Test
+    void updateBalanceRejectsCardInChargingLock() {
+        LoginUser loginUser = new LoginUser();
+        loginUser.setUserid(123L);
+        SysUser user = new SysUser();
+        user.setOperationPassword(SecurityUtils.encryptPassword("pwd"));
+        when(tokenService.getLoginUser()).thenReturn(loginUser);
+        when(sysUserClient.getSysUser(123L)).thenReturn(R.ok(user));
+        TChargingCard locked = new TChargingCard();
+        locked.setId(1L);
+        locked.setCardNo("CARD-1");
+        locked.setChargingFlag(1);
+        when(mapper.selectBatchIds(anyList())).thenReturn(Collections.singletonList(locked));
+        ChargingCardBalanceDTO dto = new ChargingCardBalanceDTO();
+        dto.setIds(Collections.singletonList(1L));
+        dto.setType(2);
+        dto.setAmount(new BigDecimal("10.00"));
+        dto.setOperationPassword("pwd");
+
+        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateBalance(dto));
+
+        assertTrue(exception.getMessage().contains("正在充电中"));
+        verify(mapper, never()).adjustBalance(anyList(), any(), any(), any(), any());
+    }
+
+    // ==================== V0.2 定稿:刷卡启动鉴权(条件密码校验) ====================
+
+    @Test
+    void verifyCardForChargingPassesWithoutPasswordConfigured() {
+        TChargingCard card = new TChargingCard();
+        card.setId(1L);
+        card.setCardNo("CARD-NOPWD");
+        card.setStatus(1);
+        card.setPassword(null);
+        when(mapper.selectOne(any(Wrapper.class))).thenReturn(card);
+
+        TChargingCard result = service.verifyCardForCharging(authDto("CARD-NOPWD", "whatever"));
+
+        assertSame(card, result);
+    }
+
+    @Test
+    void verifyCardForChargingPassesWithEmptyPasswordWhenNotConfigured() {
+        TChargingCard card = new TChargingCard();
+        card.setId(1L);
+        card.setCardNo("CARD-NOPWD");
+        card.setStatus(1);
+        card.setPassword("");
+        when(mapper.selectOne(any(Wrapper.class))).thenReturn(card);
+
+        TChargingCard result = service.verifyCardForCharging(authDto("CARD-NOPWD", null));
+
+        assertSame(card, result);
+    }
+
+    @Test
+    void verifyCardForChargingRequiresPasswordWhenConfigured() {
+        TChargingCard card = new TChargingCard();
+        card.setId(1L);
+        card.setCardNo("CARD-PWD");
+        card.setStatus(1);
+        card.setPassword(SecurityUtils.encryptPassword("123456"));
+        when(mapper.selectOne(any(Wrapper.class))).thenReturn(card);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.verifyCardForCharging(authDto("CARD-PWD", null)));
+
+        assertEquals("卡片已配置密码,请输入卡密码", exception.getMessage());
+    }
+
+    @Test
+    void verifyCardForChargingRejectsWrongPasswordWhenConfigured() {
+        TChargingCard card = new TChargingCard();
+        card.setId(1L);
+        card.setCardNo("CARD-PWD");
+        card.setStatus(1);
+        card.setPassword(SecurityUtils.encryptPassword("123456"));
+        when(mapper.selectOne(any(Wrapper.class))).thenReturn(card);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.verifyCardForCharging(authDto("CARD-PWD", "bad-pwd")));
+
+        assertEquals("卡密码错误", exception.getMessage());
+    }
+
+    @Test
+    void verifyCardForChargingRejectsDisabledCard() {
+        TChargingCard card = new TChargingCard();
+        card.setId(1L);
+        card.setCardNo("CARD-1");
+        card.setStatus(2);
+        when(mapper.selectOne(any(Wrapper.class))).thenReturn(card);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.verifyCardForCharging(authDto("CARD-1", null)));
+
+        assertEquals("卡片已禁用,无法启动充电", exception.getMessage());
+    }
+
+    @Test
+    void verifyCardForChargingRejectsMissingCard() {
+        when(mapper.selectOne(any(Wrapper.class))).thenReturn(null);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.verifyCardForCharging(authDto("NO-SUCH", null)));
+
+        assertEquals("卡片不存在或已删除", exception.getMessage());
+    }
+
+    // ==================== V0.2 定稿:充电锁定 / 解锁 ====================
+
+    @Test
+    void lockCardForChargingReturnsMapperResult() {
+        when(mapper.lockCardForCharging(1L, "SD20260909001")).thenReturn(1);
+        assertTrue(service.lockCardForCharging(1L, "SD20260909001"));
+
+        when(mapper.lockCardForCharging(1L, "SD20260909001")).thenReturn(0);
+        assertEquals(false, service.lockCardForCharging(1L, "SD20260909001"));
+    }
+
+    @Test
+    void unlockCardForChargingIsIdempotentWhenAlreadyUnlocked() {
+        when(mapper.unlockCardForCharging(1L, "SD20260909001")).thenReturn(0);
+        TChargingCard unlocked = new TChargingCard();
+        unlocked.setId(1L);
+        unlocked.setChargingOrderCode(null);
+        when(mapper.selectById(1L)).thenReturn(unlocked);
+
+        assertTrue(service.unlockCardForCharging(1L, "SD20260909001"));
+    }
+
+    @Test
+    void unlockCardForChargingRefusesWhenOccupiedByAnotherOrder() {
+        when(mapper.unlockCardForCharging(1L, "SD-OLD")).thenReturn(0);
+        TChargingCard occupied = new TChargingCard();
+        occupied.setId(1L);
+        occupied.setChargingOrderCode("SD-NEW");
+        when(mapper.selectById(1L)).thenReturn(occupied);
+
+        assertEquals(false, service.unlockCardForCharging(1L, "SD-OLD"));
+    }
+
+    // ==================== V0.2 定稿:最终结算扣款(幂等 + 允许负余额) ====================
+
+    @Test
+    void deductBalanceForChargingAllowsNegativeBalance() {
+        stubTransaction();
+        TChargingCard beforeCard = new TChargingCard();
+        beforeCard.setId(1L);
+        beforeCard.setCardNo("CARD-1");
+        beforeCard.setBalance(new BigDecimal("5.00"));
+        TChargingCard afterCard = new TChargingCard();
+        afterCard.setId(1L);
+        afterCard.setCardNo("CARD-1");
+        afterCard.setBalance(new BigDecimal("-5.00"));
+        when(mapper.selectById(1L)).thenReturn(beforeCard, afterCard);
+        when(mapper.insertOrderDeductRecord(1L, new BigDecimal("10.00"), "SD20260909001", 0L)).thenReturn(1);
+        when(mapper.deductBalanceByCardId(1L, new BigDecimal("10.00"))).thenReturn(1);
+
+        ChargingCardDeductVO result = service.deductBalanceForCharging(
+                deductDto(1L, "SD20260909001", new BigDecimal("10.00")));
+
+        assertEquals(false, result.isDuplicated());
+        assertEquals(0, result.getAfterBalance().compareTo(new BigDecimal("-5.00")));
+        verify(mapper).insertOrderDeductRecord(1L, new BigDecimal("10.00"), "SD20260909001", 0L);
+        verify(mapper).deductBalanceByCardId(1L, new BigDecimal("10.00"));
+    }
+
+    @Test
+    void deductBalanceForChargingDuplicateRequestDoesNotDeductTwice() {
+        stubTransaction();
+        TChargingCard beforeCard = new TChargingCard();
+        beforeCard.setId(1L);
+        beforeCard.setCardNo("CARD-1");
+        beforeCard.setBalance(new BigDecimal("-5.00"));
+        when(mapper.selectById(1L)).thenReturn(beforeCard);
+        when(mapper.insertOrderDeductRecord(1L, new BigDecimal("10.00"), "SD20260909001", 0L))
+                .thenThrow(new DuplicateKeyException("uk_order_code"));
+
+        ChargingCardDeductVO result = service.deductBalanceForCharging(
+                deductDto(1L, "SD20260909001", new BigDecimal("10.00")));
+
+        assertEquals(true, result.isDuplicated());
+        verify(mapper, never()).deductBalanceByCardId(anyLong(), any());
+    }
+
+    @Test
+    void deductBalanceForChargingRejectsMissingCard() {
+        stubTransaction();
+        when(mapper.selectById(1L)).thenReturn(null);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.deductBalanceForCharging(deductDto(1L, "SD20260909001", new BigDecimal("10.00"))));
+
+        assertEquals("卡片不存在或已删除", exception.getMessage());
+    }
+
+    @Test
+    void deductBalanceForChargingRejectsInvalidAmount() {
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.deductBalanceForCharging(deductDto(1L, "SD20260909001", new BigDecimal("0"))));
+
+        assertEquals("扣款金额必须大于0", exception.getMessage());
+    }
+
+    private void stubTransaction() {
+        when(transactionTemplate.execute(any(TransactionCallback.class))).thenAnswer(invocation -> {
+            TransactionCallback<Object> callback = invocation.getArgument(0);
+            return callback.doInTransaction(null);
+        });
+    }
+
+    private ChargingCardAuthDTO authDto(String cardNo, String password) {
+        ChargingCardAuthDTO dto = new ChargingCardAuthDTO();
+        dto.setCardNo(cardNo);
+        dto.setPassword(password);
+        return dto;
+    }
+
+    private ChargingCardDeductDTO deductDto(Long cardId, String orderCode, BigDecimal amount) {
+        ChargingCardDeductDTO dto = new ChargingCardDeductDTO();
+        dto.setCardId(cardId);
+        dto.setOrderCode(orderCode);
+        dto.setAmount(amount);
+        return dto;
+    }
 }
diff --git a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/enums/ServiceIdMenu.java b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/enums/ServiceIdMenu.java
index 017973a..60dff5e 100644
--- a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/enums/ServiceIdMenu.java
+++ b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/enums/ServiceIdMenu.java
@@ -100,6 +100,7 @@
 
 
 	ServiceIdMenu(String key,String value) {
+		this.key = key;
 		this.value = value;
 	}
 
diff --git a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java
index f696f2d..3e5ae6b 100644
--- a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java
+++ b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java
@@ -20,6 +20,9 @@
 import com.ruoyi.integration.iotda.utils.tools.MessageUtil;
 import com.ruoyi.integration.iotda.utils.tools.StrategyUtil;
 import com.ruoyi.integration.iotda.utils.tools.ChargingMessageUtil;
+import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
+import com.ruoyi.order.api.feignClient.ChargingOrderClient;
+import com.ruoyi.order.api.vo.DeviceStartChargingVO;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
@@ -58,6 +61,9 @@
     
     @Resource
     private ChargingMessageUtil chargingMessageUtil;
+
+    @Resource
+    private ChargingOrderClient chargingOrderClient;
 
     private final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100, 500, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
 
@@ -312,14 +318,46 @@
                 ChargingPileStartsChargingMessage chargingPileStartsChargingMessage = JSON.parseObject(content.toJSONString(),ChargingPileStartsChargingMessage.class);
                 chargingMessage.setChargingPileStartsChargingMessage(chargingPileStartsChargingMessage);
                 chargingMessageUtil.handleMessage(chargingMessage);
+                // 按启动方式执行真实业务鉴权建单(1=刷卡SD订单,2=账号暂不支持,3=VIN码VD订单)
+                DeviceStartChargingVO startVO = null;
+                try {
+                    DeviceStartChargingDTO startDTO = new DeviceStartChargingDTO();
+                    startDTO.setChargingPileCode(chargingPileStartsChargingMessage.getCharging_pile_code());
+                    startDTO.setChargingGunCode(chargingPileStartsChargingMessage.getCharging_gun_code());
+                    startDTO.setStartMethod(chargingPileStartsChargingMessage.getStart_method());
+                    // VIN码启动时协议将反序VIN放在vin字段(account置零),刷卡启动时为卡号
+                    startDTO.setAccount(chargingPileStartsChargingMessage.getStart_method() != null
+                            && chargingPileStartsChargingMessage.getStart_method() == 3
+                            ? chargingPileStartsChargingMessage.getVin()
+                            : chargingPileStartsChargingMessage.getAccount());
+                    startDTO.setPassword(chargingPileStartsChargingMessage.getPassword());
+                    R<DeviceStartChargingVO> startResult = chargingOrderClient.deviceStartCharging(startDTO);
+                    if (startResult != null && startResult.getCode() == R.SUCCESS) {
+                        startVO = startResult.getData();
+                    } else {
+                        log.error("设备主动启动充电鉴权失败(订单服务返回异常)pile={} gun={} msg={}",
+                                startDTO.getChargingPileCode(), startDTO.getChargingGunCode(),
+                                startResult == null ? "null" : startResult.getMsg());
+                    }
+                } catch (Exception ex) {
+                    log.error("设备主动启动充电鉴权建单异常 pile={} gun={}",
+                            chargingPileStartsChargingMessage.getCharging_pile_code(),
+                            chargingPileStartsChargingMessage.getCharging_gun_code(), ex);
+                }
                 // 响应硬件
                 PlatformConfirmationCharging platformConfirmationCharging = new PlatformConfirmationCharging();
                 platformConfirmationCharging.setCharging_pile_code(chargingPileStartsChargingMessage.getCharging_pile_code());
                 platformConfirmationCharging.setCharging_gun_code(chargingPileStartsChargingMessage.getCharging_gun_code());
                 platformConfirmationCharging.setCard_number(chargingPileStartsChargingMessage.getAccount());
-                platformConfirmationCharging.setAccount_balance(BigDecimal.ZERO);
-                platformConfirmationCharging.setAuthentication(1);
-                // TODO 若是失败,给出失败原因
+                if (startVO != null && Integer.valueOf(1).equals(startVO.getAuthentication())) {
+                    platformConfirmationCharging.setTransaction_serial_number(startVO.getOrderCode());
+                    platformConfirmationCharging.setAccount_balance(startVO.getBalance() == null ? BigDecimal.ZERO : startVO.getBalance());
+                    platformConfirmationCharging.setAuthentication(1);
+                } else {
+                    platformConfirmationCharging.setAccount_balance(BigDecimal.ZERO);
+                    platformConfirmationCharging.setAuthentication(0);
+                    platformConfirmationCharging.setFailure_cause(startVO == null ? null : startVO.getFailureCause());
+                }
                 iotMessageProduce.sendMessage(platformConfirmationCharging.getCharging_pile_code(), ServiceIdMenu.PLATFORM_CONFIRMATION_CHARGING.getKey(),messageUtil.platformConfirmationCharging(platformConfirmationCharging));
     
                 log.warn("CHARGING_PILE_STARTS_CHARGING消息处理:{} 毫秒", System.currentTimeMillis() - CHARGING_PILE_STARTS_CHARGING);
diff --git a/ruoyi-service/ruoyi-integration/src/test/java/com/ruoyi/integration/iotda/enums/ServiceIdMenuTest.java b/ruoyi-service/ruoyi-integration/src/test/java/com/ruoyi/integration/iotda/enums/ServiceIdMenuTest.java
new file mode 100644
index 0000000..1a871f2
--- /dev/null
+++ b/ruoyi-service/ruoyi-integration/src/test/java/com/ruoyi/integration/iotda/enums/ServiceIdMenuTest.java
@@ -0,0 +1,38 @@
+package com.ruoyi.integration.iotda.enums;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * IoTDA service_id 枚举完整性测试:
+ * 防止历史"key 恒为 null"类构造/取值错误回归,保证协议下发应答可正常匹配。
+ */
+class ServiceIdMenuTest {
+
+    @Test
+    void allKeysAndValuesPresent() {
+        for (ServiceIdMenu menu : ServiceIdMenu.values()) {
+            assertNotNull(menu.getKey(), "枚举 " + menu.name() + " 的 key(中文描述) 为空");
+            assertNotNull(menu.getValue(), "枚举 " + menu.name() + " 的 value(service_id) 为空");
+            assertFalse(menu.getKey().trim().isEmpty(), "枚举 " + menu.name() + " 的 key(中文描述) 为空白");
+            assertFalse(menu.getValue().trim().isEmpty(), "枚举 " + menu.name() + " 的 value(service_id) 为空白");
+        }
+    }
+
+    @Test
+    void serviceIdValuesAreGloballyUnique() {
+        Set<String> seen = new HashSet<>();
+        for (ServiceIdMenu menu : ServiceIdMenu.values()) {
+            assertFalse(seen.contains(menu.getValue()),
+                    "service_id 重复定义:" + menu.getValue() + "(" + menu.name() + ")");
+            seen.add(menu.getValue());
+        }
+        assertEquals(ServiceIdMenu.values().length, seen.size());
+    }
+}
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 9b1d28c..9d201e7 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
@@ -42,6 +42,7 @@
 import com.ruoyi.common.core.web.domain.AjaxResult;
 import com.ruoyi.common.core.web.page.PageInfo;
 import com.ruoyi.order.api.dto.ChargingStatisticeDTO;
+import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
 import com.ruoyi.order.api.model.*;
 import com.ruoyi.order.api.query.ChargingOrderQuery;
 import com.ruoyi.common.core.dto.MongoChargingOrderQuery;
@@ -1789,14 +1790,21 @@
     }
     
     /**
-     * 远程启动充电应答
+     * 远程启动充电应答(按订单号前缀路由:SD=刷卡订单,VD=VIN订单,其余=普通订单)
      * @param message
      */
     @ResponseBody
     @PostMapping(value = "/startChargeSuccessfully")
     public void startChargeSuccessfully(@RequestBody PlatformStartChargingReplyMessageVO message){
         log.info("-------------------远程启动充电请求应答-------------------:" + message);
-        chargingOrderService.startChargeSuccessfully(message);
+        String orderCode = message == null ? null : message.getTransaction_serial_number();
+        if (isCardOrder(orderCode)) {
+            cardChargingOrderService.cardStartChargeSuccessfully(message);
+        } else if (isVinOrder(orderCode)) {
+            vinChargingOrderService.vinStartChargeSuccessfully(message);
+        } else {
+            chargingOrderService.startChargeSuccessfully(message);
+        }
     }
     
     
@@ -1909,23 +1917,36 @@
     
     
     /**
-     * 停止充电应答处理逻辑
+     * 停止充电应答处理逻辑(普通订单原逻辑 + VIN订单按枪兜底确认)
      * @param platformStopChargingReply
      */
     @PostMapping("/terminateSuccessfulResponse")
     public void terminateSuccessfulResponse(@RequestBody PlatformStopChargingReplyVO platformStopChargingReply){
         log.info("-------------------远程停止充电请求应答-------------------:" + platformStopChargingReply);
+        // VIN订单平台停止应答兜底(按枪定位停止中订单,内部自带空判断,不影响普通订单处理)
+        try {
+            vinChargingOrderService.vinTerminateStopByReply(platformStopChargingReply);
+        } catch (Exception ex) {
+            log.error("VIN订单停止应答处理异常:", ex);
+        }
         chargingOrderService.terminateSuccessfulResponse(platformStopChargingReply);
     }
-    
-    
+
+
     /**
-     * 停止充电返回账单后计算费用
+     * 停止充电返回账单后计算费用(按订单号前缀路由:SD=刷卡订单,VD=VIN订单,其余=普通订单)
      * @param vo
      */
     @PostMapping("/endChargeBillingCharge")
     public R endChargeBillingCharge(@RequestBody TransactionRecordMessageVO vo){
         log.info("-------------------停止充电返回账单后计算费用及修改业务状态-------------------:" + vo);
+        String orderCode = vo == null ? null : vo.getTransaction_serial_number();
+        if (isCardOrder(orderCode)) {
+            return cardChargingOrderService.cardEndChargeBillingCharge(vo);
+        }
+        if (isVinOrder(orderCode)) {
+            return vinChargingOrderService.vinEndChargeBillingCharge(vo);
+        }
         return chargingOrderService.endChargeBillingCharge(vo);
     }
 
@@ -2304,22 +2325,41 @@
     }
     
     /**
-     * 处理充电订单实时监控数据相关的业务逻辑
+     * 处理充电订单实时监控数据相关的业务逻辑(按订单号前缀路由)
      * @param query
      */
     @PostMapping("/chargeMonitoring")
     public void chargeMonitoring(@RequestBody UploadRealTimeMonitoringDataQuery query){
-        chargingOrderService.chargeMonitoring(query);
+        String orderCode = query == null ? null : query.getTransaction_serial_number();
+        if (isCardOrder(orderCode)) {
+            cardChargingOrderService.cardChargeMonitoring(query);
+        } else if (isVinOrder(orderCode)) {
+            vinChargingOrderService.vinChargeMonitoring(query);
+        } else {
+            chargingOrderService.chargeMonitoring(query);
+        }
     }
     /**
-     * 通过流水号查询订单
+     * 通过流水号查询订单(统一前缀路由:SD=刷卡订单,VD=VIN订单,其余=普通订单)
+     * <p>设备链路(实时监控/BMS需求/交易记录确认等)统一经此按订单号前缀取三类订单,
+     * SD/VD 订单实体继承自 TChargingOrder,字段兼容直接返回。</p>
      * @param code
      * @return
      */
     @PostMapping(value = "/getOrderByCode/{code}")
     public R<TChargingOrder> getOrderByCode(@PathVariable("code") String code){
-        TChargingOrder one = chargingOrderService.getOne(Wrappers.lambdaQuery(TChargingOrder.class)
-                .eq(TChargingOrder::getCode, code));
+        TChargingOrder one;
+        if (isCardOrder(code)) {
+            one = cardChargingOrderService.getCardOrderByCode(code);
+        } else if (isVinOrder(code)) {
+            one = vinChargingOrderService.getVinOrderByCode(code);
+        } else {
+            one = chargingOrderService.getOne(Wrappers.lambdaQuery(TChargingOrder.class)
+                    .eq(TChargingOrder::getCode, code));
+        }
+        if (one == null) {
+            return R.ok(null);
+        }
         one.setIdStr(one.getId().toString());
         return R.ok(one);
     }
@@ -2348,6 +2388,52 @@
             return R.ok(false);
         }
         return R.ok(updated);
+    }
+
+    /**
+     * 设备链路订单号前缀判断:SD=刷卡充电订单
+     */
+    private boolean isCardOrder(String orderCode) {
+        return orderCode != null && orderCode.startsWith("SD");
+    }
+
+    /**
+     * 设备链路订单号前缀判断:VD=VIN码充电订单
+     */
+    private boolean isVinOrder(String orderCode) {
+        return orderCode != null && orderCode.startsWith("VD");
+    }
+
+    /**
+     * 设备主动发起充电(刷卡/VIN码)鉴权建单统一入口。
+     * <p>协议 start_method:1=刷卡(创建SD订单),2=账号启动(暂不支持),3=VIN码(创建VD订单)。</p>
+     */
+    @PostMapping("/inner/device-start-charging")
+    public R<DeviceStartChargingVO> deviceStartCharging(@RequestBody DeviceStartChargingDTO dto) {
+        if (dto == null || dto.getStartMethod() == null) {
+            log.warn("[设备启动] 缺少启动方式,拒绝建单 dto={}", dto);
+            return R.ok(DeviceStartChargingVO.fail(null));
+        }
+        DeviceStartChargingVO result;
+        switch (dto.getStartMethod()) {
+            case 1:
+                result = cardChargingOrderService.startCardCharging(dto);
+                break;
+            case 3:
+                result = vinChargingOrderService.startVinCharging(dto);
+                break;
+            case 2:
+                // 账号启动暂不支持(协议无对应失败码,按最接近的“账户不存在”返回)
+                log.warn("[设备启动] 账号启动方式暂不支持 pile={} account={}", dto.getChargingPileCode(), dto.getAccount());
+                result = DeviceStartChargingVO.fail(1);
+                break;
+            default:
+                log.warn("[设备启动] 未知启动方式 startMethod={} pile={} gun={}",
+                        dto.getStartMethod(), dto.getChargingPileCode(), dto.getChargingGunCode());
+                result = DeviceStartChargingVO.fail(null);
+                break;
+        }
+        return R.ok(result);
     }
     
     
@@ -3277,25 +3363,37 @@
 
 
     /**
-     * 硬件充电结束后的处理逻辑
+     * 硬件充电结束后的处理逻辑(按订单号前缀路由,结束方式2=设备自动结束)
      * @param code
      */
     @PostMapping("/endCharge")
     public void endCharge(@RequestParam("code") String code){
         log.info(code + ":-------------------充电桩自动结束充电-------------------");
-        chargingOrderService.endCharge(code, 2);
+        if (isCardOrder(code)) {
+            cardChargingOrderService.cardEndCharge(code, 2);
+        } else if (isVinOrder(code)) {
+            vinChargingOrderService.vinEndCharge(code, 2);
+        } else {
+            chargingOrderService.endCharge(code, 2);
+        }
     }
 
 
 
     /**
-     * 硬件异常结束充电后的处理逻辑
+     * 硬件异常结束充电后的处理逻辑(按订单号前缀路由,结束方式0=异常终止)
      * @param code
      */
     @PostMapping("/excelEndCharge")
     public void excelEndCharge(@RequestParam("code") String code){
         log.info(code + ":-------------------充电异常,停止充电-------------------");
-        chargingOrderService.excelEndCharge(code);
+        if (isCardOrder(code)) {
+            cardChargingOrderService.cardEndCharge(code, 0);
+        } else if (isVinOrder(code)) {
+            vinChargingOrderService.vinEndCharge(code, 0);
+        } else {
+            chargingOrderService.excelEndCharge(code);
+        }
     }
 
 
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinAccountingStrategyDetailOrderMapper.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinAccountingStrategyDetailOrderMapper.java
new file mode 100644
index 0000000..0d08b68
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinAccountingStrategyDetailOrderMapper.java
@@ -0,0 +1,15 @@
+package com.ruoyi.order.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.order.api.model.TVinAccountingStrategyDetailOrder;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * VIN码充电订单计费策略明细快照表 Mapper
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Mapper
+public interface TVinAccountingStrategyDetailOrderMapper extends BaseMapper<TVinAccountingStrategyDetailOrder> {
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinAccountingStrategyOrderMapper.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinAccountingStrategyOrderMapper.java
new file mode 100644
index 0000000..54e542c
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinAccountingStrategyOrderMapper.java
@@ -0,0 +1,15 @@
+package com.ruoyi.order.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.order.api.model.TVinAccountingStrategyOrder;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * VIN码充电订单计费策略快照主表 Mapper
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Mapper
+public interface TVinAccountingStrategyOrderMapper extends BaseMapper<TVinAccountingStrategyOrder> {
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinChargingOrderAccountingStrategyMapper.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinChargingOrderAccountingStrategyMapper.java
new file mode 100644
index 0000000..75a7094
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TVinChargingOrderAccountingStrategyMapper.java
@@ -0,0 +1,15 @@
+package com.ruoyi.order.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.order.api.model.TVinChargingOrderAccountingStrategy;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * VIN码充电订单计费明细表 Mapper
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Mapper
+public interface TVinChargingOrderAccountingStrategyMapper extends BaseMapper<TVinChargingOrderAccountingStrategy> {
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java
index c2357dc..1e21e11 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TCardChargingOrderService.java
@@ -2,11 +2,17 @@
 
 import com.ruoyi.common.core.web.page.PageInfo;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
 import com.ruoyi.order.api.model.TCardChargingOrder;
 import com.ruoyi.order.api.query.CardBalanceDetailQuery;
 import com.ruoyi.order.api.query.CardChargingOrderQuery;
+import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
 import com.ruoyi.order.api.vo.CardBalanceDetailVO;
 import com.ruoyi.order.api.vo.ChargingOrderVO;
+import com.ruoyi.order.api.vo.DeviceStartChargingVO;
+import com.ruoyi.order.api.vo.PlatformStartChargingReplyMessageVO;
+import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
 
 import java.util.List;
 import java.util.Map;
@@ -33,4 +39,62 @@
     Map<String, Object> balanceDetailSummary(CardBalanceDetailQuery query);
 
     List<CardBalanceDetailVO> balanceDetailExport(CardBalanceDetailQuery query);
+
+    // ===== 设备侧刷卡充电链路(V0.2 刷卡充电) =====
+
+    /**
+     * 设备主动发起刷卡充电:卡片鉴权 -> 建单(状态2) -> 策略快照 -> 锁卡;
+     * 锁卡失败回滚删除订单并返回失败原因4。
+     */
+    DeviceStartChargingVO startCardCharging(DeviceStartChargingDTO dto);
+
+    /**
+     * 根据订单编号查询刷卡充电订单(SD前缀)
+     */
+    TCardChargingOrder getCardOrderByCode(String code);
+
+    /**
+     * 刷卡订单收到桩启动应答:成功(结果1)置为充电中;失败置为启动失败并解锁卡片、复位枪
+     */
+    void cardStartChargeSuccessfully(PlatformStartChargingReplyMessageVO vo);
+
+    /**
+     * 刷卡订单实时监控数据更新(首次数据将订单置为充电中)
+     */
+    void cardChargeMonitoring(UploadRealTimeMonitoringDataQuery query);
+
+    /**
+     * 刷卡订单充电结束(设备自动结束/异常终止时更新结束方式)
+     */
+    void cardEndCharge(String orderCode, Integer endMode);
+
+    /**
+     * 刷卡订单最终结算(统一结算入口,SD):
+     * 订单号结算锁 -> 幂等(状态5跳过) -> 分时电量按快照计价 -> 写计费明细 -> 更新订单(状态5,允许负余额)
+     * -> 卡账户扣款(幂等) -> 解锁卡片 -> 复位枪
+     */
+    R cardEndChargeBillingCharge(TransactionRecordMessageVO vo);
+
+    // ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) =====
+
+    /**
+     * 查询启动超时(状态2超过阈值未收到启动成功应答)的刷卡订单
+     */
+    List<TCardChargingOrder> listTimeoutStartingOrders();
+
+    /**
+     * 查询充电超时(状态3超过阈值未收到交易记录)的刷卡订单
+     */
+    List<TCardChargingOrder> listTimeoutChargingOrders();
+
+    /**
+     * 启动超时关单:状态置为启动失败(6),解锁卡片并复位充电枪
+     */
+    void closeTimeoutStartingOrder(TCardChargingOrder order);
+
+    /**
+     * 充电超时兜底结算:以订单累计电量构造平段账单,复用统一结算入口
+     * cardEndChargeBillingCharge(结算锁/状态幂等/扣款幂等/解锁/复位全复用)
+     */
+    void compensationSettle(TCardChargingOrder order);
 }
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinAccountingStrategyDetailOrderService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinAccountingStrategyDetailOrderService.java
new file mode 100644
index 0000000..153a5fd
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinAccountingStrategyDetailOrderService.java
@@ -0,0 +1,20 @@
+package com.ruoyi.order.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.order.api.model.TVinAccountingStrategyDetailOrder;
+
+import java.util.List;
+
+/**
+ * VIN码充电订单计费策略明细快照表服务
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+public interface TVinAccountingStrategyDetailOrderService extends IService<TVinAccountingStrategyDetailOrder> {
+
+    /**
+     * 根据订单id查询计费策略明细快照
+     */
+    List<TVinAccountingStrategyDetailOrder> listByChargingOrderId(Long chargingOrderId);
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinAccountingStrategyOrderService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinAccountingStrategyOrderService.java
new file mode 100644
index 0000000..847e350
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinAccountingStrategyOrderService.java
@@ -0,0 +1,13 @@
+package com.ruoyi.order.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.order.api.model.TVinAccountingStrategyOrder;
+
+/**
+ * VIN码充电订单计费策略快照主表服务
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+public interface TVinAccountingStrategyOrderService extends IService<TVinAccountingStrategyOrder> {
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinChargingOrderAccountingStrategyService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinChargingOrderAccountingStrategyService.java
new file mode 100644
index 0000000..5984948
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinChargingOrderAccountingStrategyService.java
@@ -0,0 +1,13 @@
+package com.ruoyi.order.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.order.api.model.TVinChargingOrderAccountingStrategy;
+
+/**
+ * VIN码充电订单计费明细表服务
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+public interface TVinChargingOrderAccountingStrategyService extends IService<TVinChargingOrderAccountingStrategy> {
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinChargingOrderService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinChargingOrderService.java
index 507a191..7c35a40 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinChargingOrderService.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TVinChargingOrderService.java
@@ -1,11 +1,18 @@
 package com.ruoyi.order.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.web.page.PageInfo;
+import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
 import com.ruoyi.order.api.model.TVinChargingOrder;
+import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
 import com.ruoyi.order.api.query.VinChargingOrderQuery;
 import com.ruoyi.common.core.web.domain.AjaxResult;
 import com.ruoyi.order.api.vo.ChargingOrderVO;
+import com.ruoyi.order.api.vo.DeviceStartChargingVO;
+import com.ruoyi.order.api.vo.PlatformStartChargingReplyMessageVO;
+import com.ruoyi.order.api.vo.PlatformStopChargingReplyVO;
+import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
 import com.ruoyi.order.api.vo.VinChargingStatusVO;
 import com.ruoyi.order.dto.MyVinChargingOrderList;
 
@@ -31,4 +38,70 @@
     AjaxResult stopCharging(String chargingOrderId, Integer chargingGunId);
 
     List<Map<String, Object>> accountingStrategyList(Long chargingOrderId);
+
+    // ===== 设备侧VIN码充电链路(V0.2 VIN码充电) =====
+
+    /**
+     * 设备主动发起VIN码充电:VIN反序解析 -> 绑定关系校验 -> 建单(状态2) -> 策略快照
+     */
+    DeviceStartChargingVO startVinCharging(DeviceStartChargingDTO dto);
+
+    /**
+     * 根据订单编号查询VIN码充电订单(VD前缀)
+     */
+    TVinChargingOrder getVinOrderByCode(String code);
+
+    /**
+     * VIN订单收到桩启动应答:成功置为充电中;失败置为启动失败并复位枪
+     */
+    void vinStartChargeSuccessfully(PlatformStartChargingReplyMessageVO vo);
+
+    /**
+     * VIN订单实时监控数据更新(首次数据将订单置为充电中)
+     */
+    void vinChargeMonitoring(UploadRealTimeMonitoringDataQuery query);
+
+    /**
+     * VIN订单充电结束(设备自动结束/异常终止时更新结束方式)
+     */
+    void vinEndCharge(String orderCode, Integer endMode);
+
+    /**
+     * VIN订单最终结算(统一结算入口,VD):订单号结算锁 -> 幂等(状态5跳过) -> 分时电量按快照计价
+     * -> 写计费明细 -> 更新订单(状态5);不做任何账户扣款操作(后付记账模式)
+     */
+    R vinEndChargeBillingCharge(TransactionRecordMessageVO vo);
+
+    /**
+     * 查询充电枪最新停止中(状态4)的VIN订单(平台停止应答兜底处理)
+     */
+    TVinChargingOrder getLatestStoppingByGunId(Integer gunId);
+
+    /**
+     * 平台停止充电应答兜底:确认结束方式为主动终止(与stopCharging前置更新幂等合并)
+     */
+    void vinTerminateStopByReply(PlatformStopChargingReplyVO vo);
+
+    // ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) =====
+
+    /**
+     * 查询启动超时(状态2超过阈值未收到启动成功应答)的VIN订单
+     */
+    List<TVinChargingOrder> listTimeoutStartingOrders();
+
+    /**
+     * 查询充电超时(状态3/4超过阈值未收到交易记录)的VIN订单
+     */
+    List<TVinChargingOrder> listTimeoutChargingOrders();
+
+    /**
+     * 启动超时关单:状态置为启动失败(6)并复位充电枪
+     */
+    void closeTimeoutStartingOrder(TVinChargingOrder order);
+
+    /**
+     * 充电超时兜底结算:以订单累计电量构造平段账单,复用统一结算入口
+     * vinEndChargeBillingCharge(结算锁/状态幂等/枪复位全复用)
+     */
+    void compensationSettle(TVinChargingOrder order);
 }
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 e2593fb..0b69c48 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
@@ -1,46 +1,118 @@
 package com.ruoyi.order.service.impl;
 
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
+import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
 import com.ruoyi.account.api.feignClient.AppUserCarClient;
+import com.ruoyi.account.api.feignClient.ChargingCardClient;
 import com.ruoyi.account.api.model.TAppUserCar;
+import com.ruoyi.account.api.model.TChargingCard;
+import com.ruoyi.account.api.vo.ChargingCardDeductVO;
+import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyClient;
+import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyDetailClient;
 import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
 import com.ruoyi.chargingPile.api.feignClient.ChargingPileClient;
 import com.ruoyi.chargingPile.api.feignClient.SiteClient;
 import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
+import com.ruoyi.chargingPile.api.model.TAccountingStrategy;
+import com.ruoyi.chargingPile.api.model.TAccountingStrategyDetail;
 import com.ruoyi.chargingPile.api.model.TChargingGun;
+import com.ruoyi.chargingPile.api.model.TChargingPile;
+import com.ruoyi.chargingPile.api.vo.GetChargingGunByCode;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.utils.DateUtils;
+import com.ruoyi.common.core.utils.StringUtils;
+import com.ruoyi.common.core.utils.bean.BeanUtils;
 import com.ruoyi.common.core.web.page.PageInfo;
 import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
+import com.ruoyi.order.api.model.TCardAccountingStrategyDetailOrder;
+import com.ruoyi.order.api.model.TCardAccountingStrategyOrder;
 import com.ruoyi.order.api.model.TCardChargingOrder;
+import com.ruoyi.order.api.model.TCardChargingOrderAccountingStrategy;
 import com.ruoyi.order.api.query.CardBalanceDetailQuery;
 import com.ruoyi.order.api.query.CardChargingOrderQuery;
+import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
 import com.ruoyi.order.api.vo.CardBalanceDetailVO;
 import com.ruoyi.order.api.vo.ChargingOrderVO;
+import com.ruoyi.order.api.vo.DeviceStartChargingVO;
+import com.ruoyi.order.api.vo.PlatformStartChargingReplyMessageVO;
+import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
 import com.ruoyi.order.mapper.TCardChargingOrderMapper;
+import com.ruoyi.order.service.TCardAccountingStrategyDetailOrderService;
+import com.ruoyi.order.service.TCardAccountingStrategyOrderService;
+import com.ruoyi.order.service.TCardChargingOrderAccountingStrategyService;
 import com.ruoyi.order.service.TCardChargingOrderService;
 import com.ruoyi.chargingPile.api.model.Site;
+import com.ruoyi.integration.api.feignClient.ChargingMessageClient;
+import com.ruoyi.integration.api.feignClient.ChengDuJianGuanClient;
 import com.ruoyi.integration.api.model.UploadRealTimeMonitoringData;
+import com.ruoyi.integration.api.vo.ChargingOrderVo;
+import com.ruoyi.integration.api.vo.NotificationConnectorChargeStatusVo;
+import com.ruoyi.order.util.TransactionStageParser;
 import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.support.TransactionTemplate;
 
 import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 @Service
 public class TCardChargingOrderServiceImpl extends ServiceImpl<TCardChargingOrderMapper, TCardChargingOrder>
         implements TCardChargingOrderService {
 
+    private static final Logger log = LoggerFactory.getLogger(TCardChargingOrderServiceImpl.class);
+
+    /** SD 订单号前缀(设备刷卡充电) */
+    private static final String ORDER_CODE_PREFIX_SD = "SD";
+    /** 订单状态:启动中 */
+    private static final int ORDER_STATUS_STARTING = 2;
+    /** 订单状态:充电中 */
+    private static final int ORDER_STATUS_CHARGING = 3;
+    /** 订单状态:已结束 */
+    private static final int ORDER_STATUS_FINISHED = 5;
+    /** 订单状态:启动失败(沿用 t_charging_order 枚举约定:-1 为负值语义,前端按 status<0 渲染) */
+    private static final int ORDER_STATUS_START_FAILED = -1;
+    /** 充电枪状态:充电中 */
+    private static final int GUN_STATUS_CHARGING = 4;
+    /** 充电枪状态:空闲 */
+    private static final int GUN_STATUS_IDLE = 2;
+    /** SD 建单枪锁前缀(桩+枪维度,防同枪并发建单,文档13.2) */
+    private static final String LOCK_SD_START_GUN = "SD_START_GUN:";
+    /** SD 建单卡锁前缀(卡ID维度,防同卡多枪并发启动,文档13.2) */
+    private static final String LOCK_SD_START_CARD = "SD_START_CARD:";
+    /** SD 结算锁前缀(订单号维度,防并发重复结算/扣款,文档13.3) */
+    private static final String LOCK_SD_SETTLE = "SD_SETTLE:";
+    /** SD 启动中超时阈值(分钟):超时未收到启动成功应答则关单 */
+    private static final int SD_STARTING_TIMEOUT_MINUTES = 30;
+    /** SD 充电中超时阈值(小时):超时未收到交易记录则兜底结算 */
+    private static final int SD_CHARGING_TIMEOUT_HOURS = 24;
+
     @Resource
     private TokenService tokenService;
+    @Resource
+    private ChargingMessageClient chargingMessageClient;
+    @Resource
+    private ChengDuJianGuanClient chengDuJianGuanClient;
     @Resource
     private SiteClient siteClient;
     @Resource
@@ -51,6 +123,22 @@
     private AppUserCarClient appUserCarClient;
     @Resource
     private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService;
+    @Resource
+    private ChargingCardClient chargingCardClient;
+    @Resource
+    private AccountingStrategyClient accountingStrategyClient;
+    @Resource
+    private AccountingStrategyDetailClient accountingStrategyDetailClient;
+    @Resource
+    private TCardAccountingStrategyOrderService cardAccountingStrategyOrderService;
+    @Resource
+    private TCardAccountingStrategyDetailOrderService cardAccountingStrategyDetailOrderService;
+    @Resource
+    private TCardChargingOrderAccountingStrategyService cardChargingOrderAccountingStrategyService;
+    @Resource
+    private TransactionTemplate transactionTemplate;
+    @Resource
+    private RedissonClient redissonClient;
 
     @Override
     public PageInfo<ChargingOrderVO> pageList(CardChargingOrderQuery query) {
@@ -391,4 +479,544 @@
     private String[] parseTimeRange(CardChargingOrderQuery query) {
         return new String[]{query.getStartTime1(), query.getStartTime2(), query.getEndTime1(), query.getEndTime2()};
     }
+
+    // ===== 设备侧刷卡充电链路(V0.2 刷卡充电) =====
+
+    @Override
+    public DeviceStartChargingVO startCardCharging(DeviceStartChargingDTO dto) {
+        if (dto == null || StringUtils.isEmpty(dto.getChargingPileCode())
+                || StringUtils.isEmpty(dto.getChargingGunCode()) || StringUtils.isEmpty(dto.getAccount())) {
+            return DeviceStartChargingVO.fail(1);
+        }
+        // 并发防护(文档13.2):桩+枪维度分布式锁,抢不到说明同枪并发启动,快速失败
+        RLock gunLock = redissonClient.getLock(LOCK_SD_START_GUN + dto.getChargingPileCode() + ":" + dto.getChargingGunCode());
+        try {
+            if (!gunLock.tryLock(0, 30, TimeUnit.SECONDS)) {
+                return DeviceStartChargingVO.fail(10);
+            }
+            // 1. 充电枪校验(不存在->5 桩停用;充电中->10 该桩存在未结账记录)
+            GetChargingGunByCode gunQuery = new GetChargingGunByCode();
+            gunQuery.setCharging_pile_code(dto.getChargingPileCode());
+            gunQuery.setCharging_gun_code(dto.getChargingGunCode());
+            TChargingGun gun = chargingGunClient.getChargingGunByCode(gunQuery).getData();
+            if (gun == null) {
+                return DeviceStartChargingVO.fail(5);
+            }
+            if (gun.getStatus() != null && gun.getStatus() == GUN_STATUS_CHARGING) {
+                return DeviceStartChargingVO.fail(10);
+            }
+            TChargingPile pile = gun.getChargingPileId() == null ? null
+                    : chargingPileClient.getChargingPileById(gun.getChargingPileId()).getData();
+            Site site = gun.getSiteId() == null ? null
+                    : Optional.ofNullable(siteClient.getSiteByIds(Collections.singletonList(gun.getSiteId())).getData())
+                            .orElse(Collections.emptyList()).stream().findFirst().orElse(null);
+
+            // 2. 卡片鉴权(卡号存在性/禁用/密码条件校验在账户服务内完成)
+            ChargingCardAuthDTO authDTO = new ChargingCardAuthDTO();
+            authDTO.setCardNo(dto.getAccount().trim());
+            authDTO.setPassword(dto.getPassword());
+            R<TChargingCard> verifyResult = chargingCardClient.verifyCharging(authDTO);
+            TChargingCard card = verifyResult.getData();
+            if (card == null) {
+                return DeviceStartChargingVO.fail(mapAuthFailure(verifyResult.getMsg()));
+            }
+            // 3. 启动余额校验(余额>1 元方可启动,与 CD 费用不足终止阈值对齐;结算允许扣成负余额)
+            if (card.getBalance() == null || card.getBalance().compareTo(BigDecimal.ONE) <= 0) {
+                return DeviceStartChargingVO.fail(3);
+            }
+
+            // 并发防护(文档13.2):卡ID维度锁,防同卡多枪并发建单(数据库锁卡条件更新为第二道保护)
+            RLock cardLock = redissonClient.getLock(LOCK_SD_START_CARD + card.getId());
+            boolean cardLocked = false;
+            try {
+                cardLocked = cardLock.tryLock(0, 30, TimeUnit.SECONDS);
+                if (!cardLocked) {
+                    return DeviceStartChargingVO.fail(4);
+                }
+                // 4. 建单(状态2 启动中)
+                TCardChargingOrder order = new TCardChargingOrder();
+                order.setCode(ORDER_CODE_PREFIX_SD + Double.valueOf(Math.random() * 1000).intValue() + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
+                order.setOrderType(2);
+                order.setOrderSource(1);
+                order.setOrderClassification(2);
+                // 充电支付模式(沿用 CD payMethod 枚举:1=余额支付):SD 全程卡余额扣款
+                order.setPayMethod(1);
+                order.setStatus(ORDER_STATUS_STARTING);
+                order.setTitle("【充电桩充电】" + (pile != null && pile.getNumber() != null ? pile.getNumber() : dto.getChargingPileCode())
+                        + "号桩/" + dto.getChargingGunCode() + "号枪");
+                order.setSiteId(gun.getSiteId());
+                order.setChargingPileId(gun.getChargingPileId());
+                order.setChargingGunId(gun.getId());
+                order.setCardId(card.getId());
+                order.setCardNo(card.getCardNo());
+                order.setCreateTime(LocalDateTime.now());
+                save(order);
+
+                // 5. 计费策略快照(枪策略优先,站点策略兜底;无策略照常建单,结算按0价并告警)
+                Integer strategyId = gun.getAccountingStrategyId() != null
+                        ? gun.getAccountingStrategyId()
+                        : (site == null ? null : site.getAccountingStrategyId());
+                try {
+                    saveStrategySnapshot(order.getId(), strategyId);
+                } catch (Exception snapshotEx) {
+                    log.error("[SD建单] 保存计费策略快照失败 orderId={} strategyId={}", order.getId(), strategyId, snapshotEx);
+                }
+
+                // 6. 锁卡(条件更新:仅空闲卡可锁;失败回滚删除订单与快照)
+                R<Boolean> lockResult = chargingCardClient.lockForCharging(card.getId(), order.getCode());
+                if (!Boolean.TRUE.equals(lockResult.getData())) {
+                    log.warn("[SD建单] 锁卡失败回滚订单 code={} cardId={} msg={}", order.getCode(), card.getId(), lockResult.getMsg());
+                    cardAccountingStrategyDetailOrderService.remove(
+                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyDetailOrder>()
+                                    .eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, order.getId()));
+                    cardAccountingStrategyOrderService.remove(
+                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyOrder>()
+                                    .eq(TCardAccountingStrategyOrder::getChargingOrderId, order.getId()));
+                    removeById(order.getId());
+                    return DeviceStartChargingVO.fail(4);
+                }
+                log.info("[SD建单] 刷卡充电订单创建成功 code={} cardNo={} gun={}", order.getCode(), card.getCardNo(), dto.getChargingGunCode());
+                return DeviceStartChargingVO.success(order.getCode(), card.getBalance());
+            } finally {
+                if (cardLocked && cardLock.isHeldByCurrentThread()) {
+                    cardLock.unlock();
+                }
+            }
+        } catch (InterruptedException ie) {
+            Thread.currentThread().interrupt();
+            log.error("[SD建单] 建单锁获取被中断 pile={} gun={}", dto.getChargingPileCode(), dto.getChargingGunCode(), ie);
+            return DeviceStartChargingVO.fail(null);
+        } catch (Exception e) {
+            log.error("[SD建单] 刷卡充电启动异常 pile={} gun={} account={}",
+                    dto.getChargingPileCode(), dto.getChargingGunCode(), dto.getAccount(), e);
+            return DeviceStartChargingVO.fail(null);
+        } finally {
+            if (gunLock.isHeldByCurrentThread()) {
+                gunLock.unlock();
+            }
+        }
+    }
+
+    /**
+     * 保存计费策略快照(主表 + 明细表),与 CD 订单快照逻辑一致
+     */
+    private void saveStrategySnapshot(Long orderId, Integer strategyId) {
+        if (strategyId == null) {
+            return;
+        }
+        TAccountingStrategy strategy = accountingStrategyClient.getAccountingStrategyById(strategyId).getData();
+        List<TAccountingStrategyDetail> details = accountingStrategyDetailClient.getListByAccountingStrategyId(strategyId).getData();
+        if (strategy == null || details == null || details.isEmpty()) {
+            return;
+        }
+        TCardAccountingStrategyOrder strategyOrder = new TCardAccountingStrategyOrder();
+        BeanUtils.copyProperties(strategy, strategyOrder);
+        strategyOrder.setId(null);
+        strategyOrder.setChargingOrderId(orderId);
+        cardAccountingStrategyOrderService.save(strategyOrder);
+        List<TCardAccountingStrategyDetailOrder> detailOrders = new ArrayList<>();
+        for (TAccountingStrategyDetail detail : details) {
+            TCardAccountingStrategyDetailOrder detailOrder = new TCardAccountingStrategyDetailOrder();
+            BeanUtils.copyProperties(detail, detailOrder);
+            detailOrder.setId(null);
+            detailOrder.setChargingOrderId(orderId);
+            detailOrders.add(detailOrder);
+        }
+        cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
+    }
+
+    /**
+     * 账户服务鉴权失败消息 -> 协议失败原因码(1=账户不存在 2=账户冻结 7=密码错误)
+     */
+    private Integer mapAuthFailure(String msg) {
+        if (StringUtils.isNotEmpty(msg)) {
+            if (msg.contains("密码")) {
+                return 7;
+            }
+            if (msg.contains("禁用") || msg.contains("冻结")) {
+                return 2;
+            }
+            if (msg.contains("不存在")) {
+                return 1;
+            }
+        }
+        return 1;
+    }
+
+    @Override
+    public TCardChargingOrder getCardOrderByCode(String code) {
+        if (StringUtils.isEmpty(code)) {
+            return null;
+        }
+        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardChargingOrder>()
+                .eq(TCardChargingOrder::getCode, code)
+                .last("limit 1"));
+    }
+
+    @Override
+    public void cardStartChargeSuccessfully(PlatformStartChargingReplyMessageVO vo) {
+        if (vo == null || StringUtils.isEmpty(vo.getTransaction_serial_number())) {
+            return;
+        }
+        TCardChargingOrder order = getCardOrderByCode(vo.getTransaction_serial_number());
+        if (order == null) {
+            return;
+        }
+        // 幂等:已进入充电中/已结束的订单不再处理
+        if (order.getStatus() != null && (order.getStatus() >= ORDER_STATUS_CHARGING)) {
+            return;
+        }
+        TCardChargingOrder update = new TCardChargingOrder();
+        update.setId(order.getId());
+        if (vo.getStartup_result() != null && vo.getStartup_result() == 1) {
+            update.setStatus(ORDER_STATUS_CHARGING);
+            update.setStartTime(LocalDateTime.now());
+            updateById(update);
+            TChargingGun gun = new TChargingGun();
+            gun.setId(order.getChargingGunId());
+            gun.setStatus(GUN_STATUS_CHARGING);
+            chargingGunClient.updateChargingGunById(gun);
+            log.info("[SD启动] 刷卡订单进入充电中 code={}", order.getCode());
+            // 推送监管平台(复用 CD 约定:充电中推平台订单状态 + 成都监管枪状态)
+            order.setStatus(ORDER_STATUS_CHARGING);
+            pushRegulatoryAsync(order, true, false);
+        } else {
+            // 启动失败:订单关闭 + 解锁卡片 + 复位枪
+            update.setStatus(ORDER_STATUS_START_FAILED);
+            update.setEndTime(LocalDateTime.now());
+            updateById(update);
+            unlockCardQuietly(order);
+            resetGunQuietly(order);
+            log.warn("[SD启动] 刷卡订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result());
+            pushRegulatoryAsync(order, false, false);
+        }
+    }
+
+    @Override
+    public void cardChargeMonitoring(UploadRealTimeMonitoringDataQuery query) {
+        if (query == null || StringUtils.isEmpty(query.getTransaction_serial_number())) {
+            return;
+        }
+        TCardChargingOrder order = getCardOrderByCode(query.getTransaction_serial_number());
+        if (order == null) {
+            return;
+        }
+        // 已结束/已关闭的订单不再更新监控数据
+        if (order.getStatus() != null && (order.getStatus() >= ORDER_STATUS_FINISHED)) {
+            return;
+        }
+        TCardChargingOrder update = new TCardChargingOrder();
+        update.setId(order.getId());
+        // 首次监控数据:启动中 -> 充电中
+        if (order.getStatus() == null || order.getStatus() < ORDER_STATUS_CHARGING) {
+            update.setStatus(ORDER_STATUS_CHARGING);
+            if (order.getStartTime() == null) {
+                update.setStartTime(LocalDateTime.now());
+            }
+            TChargingGun gun = new TChargingGun();
+            gun.setId(order.getChargingGunId());
+            gun.setStatus(GUN_STATUS_CHARGING);
+            chargingGunClient.updateChargingGunById(gun);
+        }
+        if (query.getOutput_current() != null && query.getOutput_current().compareTo(BigDecimal.ZERO) != 0
+                && query.getOutput_voltage() != null) {
+            BigDecimal power = query.getOutput_current().multiply(query.getOutput_voltage())
+                    .divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP);
+            update.setChargingPower(power);
+            update.setCurrent(query.getOutput_current());
+            update.setVoltage(query.getOutput_voltage());
+            update.setPower(power);
+            update.setNeedElec(query.getOutput_current());
+            TChargingGun gun = new TChargingGun();
+            gun.setId(order.getChargingGunId());
+            gun.setChargingPower(power);
+            chargingGunClient.updateChargingGunById(gun);
+        }
+        if (query.getCharging_degree() != null) {
+            update.setChargingCapacity(query.getCharging_degree());
+            update.setElectricity(query.getCharging_degree());
+        }
+        updateById(update);
+    }
+
+    @Override
+    public void cardEndCharge(String orderCode, Integer endMode) {
+        if (StringUtils.isEmpty(orderCode)) {
+            return;
+        }
+        TCardChargingOrder order = getCardOrderByCode(orderCode);
+        if (order == null) {
+            return;
+        }
+        TCardChargingOrder update = new TCardChargingOrder();
+        update.setId(order.getId());
+        update.setEndMode(endMode);
+        updateById(update);
+        pushRegulatoryAsync(order, false, false);
+    }
+
+    @Override
+    public R cardEndChargeBillingCharge(TransactionRecordMessageVO vo) {
+        String code = vo == null ? null : vo.getTransaction_serial_number();
+        log.info("[SD结算] 收到交易记录 code={}", code);
+        if (StringUtils.isEmpty(code)) {
+            return R.fail("交易记录缺少交易流水号");
+        }
+        // 结算锁(文档13.3 统一结算入口第一层):订单号维度,防并发/重复账单同时进入结算;
+        // 抢不到锁说明正在结算,按重复账单语义返回成功;锁内仍有状态幂等与扣款唯一键两层兜底
+        RLock settleLock = redissonClient.getLock(LOCK_SD_SETTLE + code);
+        try {
+            if (!settleLock.tryLock(5, 60, TimeUnit.SECONDS)) {
+                log.warn("[SD结算] 并发结算进行中,跳过重复账单 code={}", code);
+                return R.ok("重复账单,结算处理中");
+            }
+            return doCardEndChargeBillingCharge(vo, code);
+        } catch (InterruptedException ie) {
+            Thread.currentThread().interrupt();
+            return R.fail("结算锁获取被中断:" + code);
+        } finally {
+            if (settleLock.isHeldByCurrentThread()) {
+                settleLock.unlock();
+            }
+        }
+    }
+
+    /**
+     * SD 结算实际执行体(调用前须持有订单号结算锁)
+     */
+    private R doCardEndChargeBillingCharge(TransactionRecordMessageVO vo, String code) {
+        TCardChargingOrder order = getCardOrderByCode(code);
+        if (order == null) {
+            return R.fail("刷卡充电订单不存在:" + code);
+        }
+        // 幂等:已结算订单直接返回成功(重复账单/重复推送)
+        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_FINISHED) {
+            log.warn("[SD结算] 重复账单跳过 code={}", code);
+            return R.ok("重复账单,已按首次账单结算");
+        }
+        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_START_FAILED) {
+            return R.ok("订单已关闭,跳过结算");
+        }
+
+        // 分时电量(以平台快照计价,V0.2 定稿)
+        Map<Integer, BigDecimal> stageElectricity = TransactionStageParser.parseStageElectricity(vo);
+        BigDecimal totalElectricity = stageElectricity.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+        if (totalElectricity.compareTo(BigDecimal.ZERO) <= 0 && vo.getTotal_electricity() != null) {
+            totalElectricity = vo.getTotal_electricity();
+        }
+
+        // 快照明细(1=尖 2=峰 3=平 4=谷)
+        Map<Integer, TCardAccountingStrategyDetailOrder> snapshotByType = cardAccountingStrategyDetailOrderService
+                .lambdaQuery().eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, order.getId())
+                .list().stream()
+                .filter(item -> item.getType() != null)
+                .collect(Collectors.toMap(TCardAccountingStrategyDetailOrder::getType, Function.identity(), (a, b) -> a));
+
+        BigDecimal electrovalenceTotal = BigDecimal.ZERO;
+        BigDecimal serviceChargeTotal = BigDecimal.ZERO;
+        List<TCardChargingOrderAccountingStrategy> details = new ArrayList<>();
+        for (Map.Entry<Integer, BigDecimal> stage : stageElectricity.entrySet()) {
+            TCardAccountingStrategyDetailOrder snapshot = snapshotByType.get(stage.getKey());
+            BigDecimal capacity = stage.getValue();
+            BigDecimal electricPrice = snapshot == null || snapshot.getElectrovalence() == null ? BigDecimal.ZERO : snapshot.getElectrovalence();
+            BigDecimal servicePrice = snapshot == null || snapshot.getServiceCharge() == null ? BigDecimal.ZERO : snapshot.getServiceCharge();
+            BigDecimal originalServicePrice = snapshot == null || snapshot.getCostServiceCharge() == null
+                    ? BigDecimal.ZERO : snapshot.getCostServiceCharge();
+            BigDecimal electricAmount = capacity.multiply(electricPrice).setScale(2, BigDecimal.ROUND_HALF_UP);
+            BigDecimal serviceAmount = capacity.multiply(servicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
+            BigDecimal originalServiceAmount = capacity.multiply(originalServicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
+            electrovalenceTotal = electrovalenceTotal.add(electricAmount);
+            serviceChargeTotal = serviceChargeTotal.add(serviceAmount);
+
+            TCardChargingOrderAccountingStrategy detail = new TCardChargingOrderAccountingStrategy();
+            detail.setChargingOrderId(order.getId());
+            detail.setType(stage.getKey());
+            detail.setElectrovalence(electricPrice);
+            detail.setServiceCharge(servicePrice);
+            detail.setCostServiceCharge(originalServicePrice);
+            detail.setChargingCapacity(capacity);
+            detail.setPeriodElectricPrice(electricAmount);
+            detail.setPeriodServicePrice(serviceAmount);
+            detail.setPeriodOriginalServicePrice(originalServiceAmount);
+            if (snapshot != null) {
+                detail.setStartTime(snapshot.getStartTime());
+                detail.setEndTime(snapshot.getEndTime());
+            }
+            details.add(detail);
+        }
+        if (snapshotByType.isEmpty()) {
+            log.error("[SD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
+        }
+        BigDecimal orderAmount = electrovalenceTotal.add(serviceChargeTotal);
+        // lambda 引用要求有效final,拷贝最终值
+        final BigDecimal totalElec = totalElectricity;
+        final BigDecimal elecTotal = electrovalenceTotal;
+        final BigDecimal svcTotal = serviceChargeTotal;
+
+        // 本地事务:写计费明细 + 更新订单(状态5,允许负余额金额记录)
+        transactionTemplate.execute(status -> {
+            cardChargingOrderAccountingStrategyService.lambdaUpdate()
+                    .eq(TCardChargingOrderAccountingStrategy::getChargingOrderId, order.getId())
+                    .remove();
+            if (!details.isEmpty()) {
+                cardChargingOrderAccountingStrategyService.saveBatch(details);
+            }
+            TCardChargingOrder update = new TCardChargingOrder();
+            update.setId(order.getId());
+            update.setTotalElectricity(totalElec);
+            update.setChargingCapacity(totalElec);
+            update.setElectrovalence(elecTotal);
+            update.setServiceCharge(svcTotal);
+            update.setOrderAmount(orderAmount);
+            update.setPaymentAmount(orderAmount);
+            update.setEndTime(LocalDateTime.now());
+            update.setStatus(ORDER_STATUS_FINISHED);
+            if (order.getEndMode() == null) {
+                update.setEndMode(1);
+            }
+            updateById(update);
+            return true;
+        });
+
+        // 卡账户扣款(订单号幂等键;允许扣成负余额;失败欠费记账不回滚订单)
+        if (orderAmount.compareTo(BigDecimal.ZERO) > 0 && order.getCardId() != null) {
+            ChargingCardDeductDTO deductDTO = new ChargingCardDeductDTO();
+            deductDTO.setCardId(order.getCardId());
+            deductDTO.setOrderCode(order.getCode());
+            deductDTO.setAmount(orderAmount);
+            R<ChargingCardDeductVO> deductResult = chargingCardClient.deductForCharging(deductDTO);
+            if (deductResult != null && deductResult.getData() != null
+                    && deductResult.getData().isDuplicated()) {
+                log.warn("[SD结算] 扣款幂等命中(重复请求不重复扣)code={}", code);
+            } else if (deductResult == null || deductResult.getCode() != R.SUCCESS) {
+                log.error("[SD结算] 扣款失败,欠费记账 code={} cardId={} amount={} msg={}",
+                        code, order.getCardId(), orderAmount, deductResult == null ? "null" : deductResult.getMsg());
+            }
+        }
+
+        // 解锁卡片(无论扣款成败必须解锁,失败人工介入)
+        unlockCardQuietly(order);
+        // 复位充电枪
+        resetGunQuietly(order);
+        // 推送成都监管(结算完成推枪状态 + 订单信息,与 CD 结算链路一致)
+        pushRegulatoryAsync(order, false, true);
+        log.info("[SD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={}",
+                code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount);
+        return R.ok();
+    }
+
+    /**
+     * 异步推送监管平台(复用 CD 约定:充电中推平台订单状态 pushOrderStatus、成都监管推枪状态
+     * notificationConnectorChargeStatus / 结算完成推订单信息 notificationOrderInfo);
+     * 推送失败仅告警不阻断主流程;单元测试环境 client 未注入时静默跳过
+     */
+    private void pushRegulatoryAsync(TCardChargingOrder order, boolean pushOrderStatus, boolean pushOrderInfo) {
+        if (chargingMessageClient == null && chengDuJianGuanClient == null) {
+            return;
+        }
+        new Thread(() -> {
+            try {
+                if (pushOrderStatus && chargingMessageClient != null) {
+                    ChargingOrderVo chargingOrderVo = new ChargingOrderVo();
+                    // 与 CD 一致使用 Spring BeanUtils(本类 ruoyi BeanUtils 同名,故用全限定名)
+                    org.springframework.beans.BeanUtils.copyProperties(order, chargingOrderVo);
+                    chargingMessageClient.pushOrderStatus(chargingOrderVo);
+                }
+                if (chengDuJianGuanClient != null) {
+                    NotificationConnectorChargeStatusVo statusVo = new NotificationConnectorChargeStatusVo();
+                    statusVo.setOrderCode(order.getCode());
+                    statusVo.setChargingGunId(order.getChargingGunId());
+                    chengDuJianGuanClient.notificationConnectorChargeStatus(statusVo);
+                    if (pushOrderInfo) {
+                        chengDuJianGuanClient.notificationOrderInfo(statusVo);
+                    }
+                }
+            } catch (Exception ex) {
+                log.error("[SD监管] 推送监管平台失败 code={}", order.getCode(), ex);
+            }
+        }, "SD-Regulatory-Push").start();
+    }
+
+    /**
+     * 解锁卡片(按订单号条件释放,失败仅告警不阻断结算流程)
+     */
+    private void unlockCardQuietly(TCardChargingOrder order) {
+        if (order.getCardId() == null) {
+            return;
+        }
+        try {
+            R<Boolean> unlockResult = chargingCardClient.unlockForCharging(order.getCardId(), order.getCode());
+            if (!Boolean.TRUE.equals(unlockResult.getData())) {
+                log.error("[SD结算] 解锁卡片失败,需人工处理 code={} cardId={} msg={}",
+                        order.getCode(), order.getCardId(), unlockResult.getMsg());
+            }
+        } catch (Exception ex) {
+            log.error("[SD结算] 解锁卡片异常 code={} cardId={}", order.getCode(), order.getCardId(), ex);
+        }
+    }
+
+    /**
+     * 复位充电枪为空闲(失败仅告警)
+     */
+    private void resetGunQuietly(TCardChargingOrder order) {
+        if (order.getChargingGunId() == null) {
+            return;
+        }
+        try {
+            TChargingGun gun = chargingGunClient.getChargingGunById(order.getChargingGunId()).getData();
+            if (gun != null) {
+                gun.setStatus(GUN_STATUS_IDLE);
+                chargingGunClient.updateChargingGunById(gun);
+            }
+        } catch (Exception ex) {
+            log.error("[SD结算] 复位充电枪异常 code={} gunId={}", order.getCode(), order.getChargingGunId(), ex);
+        }
+    }
+
+    // ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) =====
+
+    @Override
+    public List<TCardChargingOrder> listTimeoutStartingOrders() {
+        return lambdaQuery()
+                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING)
+                .lt(TCardChargingOrder::getCreateTime, LocalDateTime.now().minusMinutes(SD_STARTING_TIMEOUT_MINUTES))
+                .list();
+    }
+
+    @Override
+    public List<TCardChargingOrder> listTimeoutChargingOrders() {
+        return lambdaQuery()
+                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING)
+                .lt(TCardChargingOrder::getCreateTime, LocalDateTime.now().minusHours(SD_CHARGING_TIMEOUT_HOURS))
+                .list();
+    }
+
+    @Override
+    public void closeTimeoutStartingOrder(TCardChargingOrder order) {
+        TCardChargingOrder update = new TCardChargingOrder();
+        update.setId(order.getId());
+        update.setStatus(ORDER_STATUS_START_FAILED);
+        // 异常终止(沿用 CD 约定:启动失败/异常关单 endMode=0,避免前端结束方式显示空)
+        update.setEndMode(0);
+        update.setEndTime(LocalDateTime.now());
+        updateById(update);
+        unlockCardQuietly(order);
+        resetGunQuietly(order);
+        pushRegulatoryAsync(order, false, false);
+        log.warn("[SD补偿] 启动超时关单 code={} cardId={} gunId={}", order.getCode(), order.getCardId(), order.getChargingGunId());
+    }
+
+    @Override
+    public void compensationSettle(TCardChargingOrder order) {
+        // 无设备交易记录:以订单累计电量构造全平段账单,复用统一结算入口
+        // (结算锁/状态幂等/扣款幂等/解锁/复位全部复用,满足文档13.3"所有路径汇聚同一结算入口")
+        BigDecimal capacity = order.getChargingCapacity() != null ? order.getChargingCapacity()
+                : Optional.ofNullable(order.getElectricity()).orElse(BigDecimal.ZERO);
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setTransaction_serial_number(order.getCode());
+        vo.setFlat_charge1(capacity);
+        vo.setTotal_electricity(capacity);
+        log.warn("[SD补偿] 充电超时兜底结算 code={} 按平段计价 capacity={}", order.getCode(), capacity);
+        cardEndChargeBillingCharge(vo);
+    }
 }
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinAccountingStrategyDetailOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinAccountingStrategyDetailOrderServiceImpl.java
new file mode 100644
index 0000000..05484f1
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinAccountingStrategyDetailOrderServiceImpl.java
@@ -0,0 +1,30 @@
+package com.ruoyi.order.service.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.order.api.model.TVinAccountingStrategyDetailOrder;
+import com.ruoyi.order.mapper.TVinAccountingStrategyDetailOrderMapper;
+import com.ruoyi.order.service.TVinAccountingStrategyDetailOrderService;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * VIN码充电订单计费策略明细快照表服务实现
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Service
+public class TVinAccountingStrategyDetailOrderServiceImpl extends ServiceImpl<TVinAccountingStrategyDetailOrderMapper, TVinAccountingStrategyDetailOrder> implements TVinAccountingStrategyDetailOrderService {
+
+    @Override
+    public List<TVinAccountingStrategyDetailOrder> listByChargingOrderId(Long chargingOrderId) {
+        if (chargingOrderId == null) {
+            return Collections.emptyList();
+        }
+        return list(Wrappers.lambdaQuery(TVinAccountingStrategyDetailOrder.class)
+                .eq(TVinAccountingStrategyDetailOrder::getChargingOrderId, chargingOrderId));
+    }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinAccountingStrategyOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinAccountingStrategyOrderServiceImpl.java
new file mode 100644
index 0000000..16e3245
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinAccountingStrategyOrderServiceImpl.java
@@ -0,0 +1,17 @@
+package com.ruoyi.order.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.order.api.model.TVinAccountingStrategyOrder;
+import com.ruoyi.order.mapper.TVinAccountingStrategyOrderMapper;
+import com.ruoyi.order.service.TVinAccountingStrategyOrderService;
+import org.springframework.stereotype.Service;
+
+/**
+ * VIN码充电订单计费策略快照主表服务实现
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Service
+public class TVinAccountingStrategyOrderServiceImpl extends ServiceImpl<TVinAccountingStrategyOrderMapper, TVinAccountingStrategyOrder> implements TVinAccountingStrategyOrderService {
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderAccountingStrategyServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderAccountingStrategyServiceImpl.java
new file mode 100644
index 0000000..06a8eaa
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderAccountingStrategyServiceImpl.java
@@ -0,0 +1,17 @@
+package com.ruoyi.order.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.order.api.model.TVinChargingOrderAccountingStrategy;
+import com.ruoyi.order.mapper.TVinChargingOrderAccountingStrategyMapper;
+import com.ruoyi.order.service.TVinChargingOrderAccountingStrategyService;
+import org.springframework.stereotype.Service;
+
+/**
+ * VIN码充电订单计费明细表服务实现
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+@Service
+public class TVinChargingOrderAccountingStrategyServiceImpl extends ServiceImpl<TVinChargingOrderAccountingStrategyMapper, TVinChargingOrderAccountingStrategy> implements TVinChargingOrderAccountingStrategyService {
+}
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 2b10759..325373e 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
@@ -2,29 +2,60 @@
 
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.ruoyi.account.api.feignClient.VinManagementClient;
+import com.ruoyi.account.api.model.TVinManagement;
 import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
+import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyClient;
+import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyDetailClient;
 import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
 import com.ruoyi.chargingPile.api.feignClient.ChargingPileClient;
 import com.ruoyi.chargingPile.api.feignClient.SiteClient;
 import com.ruoyi.chargingPile.api.model.Site;
+import com.ruoyi.chargingPile.api.model.TAccountingStrategy;
+import com.ruoyi.chargingPile.api.model.TAccountingStrategyDetail;
 import com.ruoyi.chargingPile.api.model.TChargingGun;
 import com.ruoyi.chargingPile.api.model.TChargingPile;
+import com.ruoyi.chargingPile.api.vo.GetChargingGunByCode;
 import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.utils.DateUtils;
+import com.ruoyi.common.core.utils.StringUtils;
+import com.ruoyi.common.core.utils.bean.BeanUtils;
 import com.ruoyi.common.core.web.domain.AjaxResult;
 import com.ruoyi.common.core.web.page.PageInfo;
 import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.integration.api.feignClient.ChargingMessageClient;
+import com.ruoyi.integration.api.feignClient.ChengDuJianGuanClient;
 import com.ruoyi.integration.api.feignClient.SendMessageClient;
 import com.ruoyi.integration.api.model.PlatformStopCharging;
 import com.ruoyi.integration.api.model.UploadRealTimeMonitoringData;
+import com.ruoyi.integration.api.vo.ChargingOrderVo;
+import com.ruoyi.integration.api.vo.NotificationConnectorChargeStatusVo;
+import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
+import com.ruoyi.order.api.model.TVinAccountingStrategyDetailOrder;
+import com.ruoyi.order.api.model.TVinAccountingStrategyOrder;
 import com.ruoyi.order.api.model.TVinChargingOrder;
+import com.ruoyi.order.api.model.TVinChargingOrderAccountingStrategy;
+import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
 import com.ruoyi.order.api.query.VinChargingOrderQuery;
 import com.ruoyi.order.api.vo.ChargingOrderVO;
+import com.ruoyi.order.api.vo.DeviceStartChargingVO;
+import com.ruoyi.order.api.vo.PlatformStartChargingReplyMessageVO;
+import com.ruoyi.order.api.vo.PlatformStopChargingReplyVO;
+import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
 import com.ruoyi.order.api.vo.VinChargingStatusVO;
 import com.ruoyi.order.dto.MyVinChargingOrderList;
 import com.ruoyi.order.mapper.TVinChargingOrderMapper;
+import com.ruoyi.order.service.TVinAccountingStrategyDetailOrderService;
+import com.ruoyi.order.service.TVinAccountingStrategyOrderService;
+import com.ruoyi.order.service.TVinChargingOrderAccountingStrategyService;
 import com.ruoyi.order.service.TVinChargingOrderService;
+import com.ruoyi.order.util.TransactionStageParser;
 import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.support.TransactionTemplate;
 
 import javax.annotation.Resource;
 import java.math.BigDecimal;
@@ -36,19 +67,54 @@
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 @Service
 public class TVinChargingOrderServiceImpl extends ServiceImpl<TVinChargingOrderMapper, TVinChargingOrder>
         implements TVinChargingOrderService {
 
+    private static final Logger log = LoggerFactory.getLogger(TVinChargingOrderServiceImpl.class);
+
+    /** VD 订单号前缀(设备VIN码充电) */
+    private static final String ORDER_CODE_PREFIX_VD = "VD";
+    /** 订单状态:启动中 */
+    private static final int ORDER_STATUS_STARTING = 2;
+    /** 订单状态:充电中 */
+    private static final int ORDER_STATUS_CHARGING = 3;
+    /** 订单状态:停止中 */
+    private static final int ORDER_STATUS_STOPPING = 4;
+    /** 订单状态:已结束 */
+    private static final int ORDER_STATUS_FINISHED = 5;
+    /** 订单状态:启动失败(沿用 t_charging_order 枚举约定:-1 为负值语义,前端按 status<0 渲染) */
+    private static final int ORDER_STATUS_START_FAILED = -1;
+    /** 充电枪状态:充电中 */
+    private static final int GUN_STATUS_CHARGING = 4;
+    /** 充电枪状态:空闲 */
+    private static final int GUN_STATUS_IDLE = 2;
+    /** VD 建单枪锁前缀(桩+枪维度,防同枪并发建单,文档13.2) */
+    private static final String LOCK_VD_START_GUN = "VD_START_GUN:";
+    /** VD 建单VIN锁前缀(VIN ID维度,防同VIN多枪并发启动,文档13.2) */
+    private static final String LOCK_VD_START_VIN = "VD_START_VIN:";
+    /** VD 结算锁前缀(订单号维度,防并发重复结算,文档13.3) */
+    private static final String LOCK_VD_SETTLE = "VD_SETTLE:";
+    /** VD 启动中超时阈值(分钟):超时未收到启动成功应答则关单 */
+    private static final int VD_STARTING_TIMEOUT_MINUTES = 30;
+    /** VD 充电中超时阈值(小时):超时未收到交易记录则兜底结算 */
+    private static final int VD_CHARGING_TIMEOUT_HOURS = 24;
+
     @Resource
     private TokenService tokenService;
+    @Resource
+    private ChargingMessageClient chargingMessageClient;
+    @Resource
+    private ChengDuJianGuanClient chengDuJianGuanClient;
     @Resource
     private VinManagementClient vinManagementClient;
     @Resource
@@ -61,6 +127,20 @@
     private SendMessageClient sendMessageClient;
     @Resource
     private UploadRealTimeMonitoringDataService monitoringDataService;
+    @Resource
+    private AccountingStrategyClient accountingStrategyClient;
+    @Resource
+    private AccountingStrategyDetailClient accountingStrategyDetailClient;
+    @Resource
+    private TVinAccountingStrategyOrderService vinAccountingStrategyOrderService;
+    @Resource
+    private TVinAccountingStrategyDetailOrderService vinAccountingStrategyDetailOrderService;
+    @Resource
+    private TVinChargingOrderAccountingStrategyService vinChargingOrderAccountingStrategyService;
+    @Resource
+    private TransactionTemplate transactionTemplate;
+    @Resource
+    private RedissonClient redissonClient;
 
     @Override
     public PageInfo<ChargingOrderVO> pageList(VinChargingOrderQuery query) {
@@ -460,4 +540,506 @@
             item.setChargePower(power);
         }
     }
+
+    // ===== 设备侧VIN码充电链路(V0.2 VIN码充电) =====
+
+    @Override
+    public DeviceStartChargingVO startVinCharging(DeviceStartChargingDTO dto) {
+        if (dto == null || StringUtils.isEmpty(dto.getChargingPileCode())
+                || StringUtils.isEmpty(dto.getChargingGunCode()) || StringUtils.isEmpty(dto.getAccount())) {
+            return DeviceStartChargingVO.fail(9);
+        }
+        // 并发防护(文档13.2):桩+枪维度分布式锁,抢不到说明同枪并发启动,快速失败
+        RLock gunLock = redissonClient.getLock(LOCK_VD_START_GUN + dto.getChargingPileCode() + ":" + dto.getChargingGunCode());
+        try {
+            if (!gunLock.tryLock(0, 30, TimeUnit.SECONDS)) {
+                return DeviceStartChargingVO.fail(10);
+            }
+            // 1. 充电枪校验
+            GetChargingGunByCode gunQuery = new GetChargingGunByCode();
+            gunQuery.setCharging_pile_code(dto.getChargingPileCode());
+            gunQuery.setCharging_gun_code(dto.getChargingGunCode());
+            TChargingGun gun = chargingGunClient.getChargingGunByCode(gunQuery).getData();
+            if (gun == null) {
+                return DeviceStartChargingVO.fail(5);
+            }
+            if (gun.getStatus() != null && gun.getStatus() == GUN_STATUS_CHARGING) {
+                return DeviceStartChargingVO.fail(10);
+            }
+            TChargingPile pile = gun.getChargingPileId() == null ? null
+                    : chargingPileClient.getChargingPileById(gun.getChargingPileId()).getData();
+            Site site = gun.getSiteId() == null ? null
+                    : Optional.ofNullable(siteClient.getSiteByIds(Collections.singletonList(gun.getSiteId())).getData())
+                            .orElse(Collections.emptyList()).stream().findFirst().orElse(null);
+
+            // 2. VIN反序解析与绑定校验(协议要求VIN码反序上送)
+            String vin = new StringBuilder(dto.getAccount().trim()).reverse().toString();
+            TVinManagement vinRecord = vinManagementClient.getVinByCode(vin).getData();
+            if (vinRecord == null) {
+                return DeviceStartChargingVO.fail(9);
+            }
+            if (vinRecord.getStatus() == null || vinRecord.getStatus() != 1) {
+                return DeviceStartChargingVO.fail(2);
+            }
+
+            // 并发防护(文档13.2):VIN ID维度锁,防同VIN多枪并发建单
+            RLock vinLock = redissonClient.getLock(LOCK_VD_START_VIN + vinRecord.getId());
+            boolean vinLocked = false;
+            try {
+                vinLocked = vinLock.tryLock(0, 30, TimeUnit.SECONDS);
+                if (!vinLocked) {
+                    return DeviceStartChargingVO.fail(10);
+                }
+                // 3. 建单(状态2 启动中)
+                TVinChargingOrder order = new TVinChargingOrder();
+                order.setCode(ORDER_CODE_PREFIX_VD + Double.valueOf(Math.random() * 1000).intValue() + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
+                order.setOrderType(2);
+                order.setOrderSource(1);
+                order.setOrderClassification(2);
+                // VD 无支付动作(后付记账模式,不执行在线支付/扣款):payMethod 留空,
+                // 不套用 CD 余额支付枚举,避免下游按支付模式误判走余额扣款逻辑
+                order.setStatus(ORDER_STATUS_STARTING);
+                order.setTitle("【充电桩充电】" + (pile != null && pile.getNumber() != null ? pile.getNumber() : dto.getChargingPileCode())
+                        + "号桩/" + dto.getChargingGunCode() + "号枪");
+                order.setSiteId(gun.getSiteId());
+                order.setChargingPileId(gun.getChargingPileId());
+                order.setChargingGunId(gun.getId());
+                order.setVinId(vinRecord.getId());
+                order.setAppUserId(vinRecord.getAppUserId());
+                order.setCreateTime(LocalDateTime.now());
+                save(order);
+
+                // 4. 计费策略快照(枪策略优先,站点策略兜底)
+                Integer strategyId = gun.getAccountingStrategyId() != null
+                        ? gun.getAccountingStrategyId()
+                        : (site == null ? null : site.getAccountingStrategyId());
+                try {
+                    saveStrategySnapshot(order.getId(), strategyId);
+                } catch (Exception snapshotEx) {
+                    log.error("[VD建单] 保存计费策略快照失败 orderId={} strategyId={}", order.getId(), strategyId, snapshotEx);
+                }
+                log.info("[VD建单] VIN码充电订单创建成功 code={} vinId={} gun={}", order.getCode(), vinRecord.getId(), dto.getChargingGunCode());
+                return DeviceStartChargingVO.success(order.getCode(), null);
+            } finally {
+                if (vinLocked && vinLock.isHeldByCurrentThread()) {
+                    vinLock.unlock();
+                }
+            }
+        } catch (InterruptedException ie) {
+            Thread.currentThread().interrupt();
+            log.error("[VD建单] 建单锁获取被中断 pile={} gun={}", dto.getChargingPileCode(), dto.getChargingGunCode(), ie);
+            return DeviceStartChargingVO.fail(null);
+        } catch (Exception e) {
+            log.error("[VD建单] VIN码充电启动异常 pile={} gun={} account={}",
+                    dto.getChargingPileCode(), dto.getChargingGunCode(), dto.getAccount(), e);
+            return DeviceStartChargingVO.fail(null);
+        } finally {
+            if (gunLock.isHeldByCurrentThread()) {
+                gunLock.unlock();
+            }
+        }
+    }
+
+    /**
+     * 保存VIN订单计费策略快照(主表 + 明细表)
+     */
+    private void saveStrategySnapshot(Long orderId, Integer strategyId) {
+        if (strategyId == null) {
+            return;
+        }
+        TAccountingStrategy strategy = accountingStrategyClient.getAccountingStrategyById(strategyId).getData();
+        List<TAccountingStrategyDetail> details = accountingStrategyDetailClient.getListByAccountingStrategyId(strategyId).getData();
+        if (strategy == null || details == null || details.isEmpty()) {
+            return;
+        }
+        TVinAccountingStrategyOrder strategyOrder = new TVinAccountingStrategyOrder();
+        BeanUtils.copyProperties(strategy, strategyOrder);
+        strategyOrder.setId(null);
+        strategyOrder.setChargingOrderId(orderId);
+        vinAccountingStrategyOrderService.save(strategyOrder);
+        List<TVinAccountingStrategyDetailOrder> detailOrders = new ArrayList<>();
+        for (TAccountingStrategyDetail detail : details) {
+            TVinAccountingStrategyDetailOrder detailOrder = new TVinAccountingStrategyDetailOrder();
+            BeanUtils.copyProperties(detail, detailOrder);
+            detailOrder.setId(null);
+            detailOrder.setChargingOrderId(orderId);
+            detailOrders.add(detailOrder);
+        }
+        vinAccountingStrategyDetailOrderService.saveBatch(detailOrders);
+    }
+
+    @Override
+    public TVinChargingOrder getVinOrderByCode(String code) {
+        if (StringUtils.isEmpty(code)) {
+            return null;
+        }
+        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinChargingOrder>()
+                .eq(TVinChargingOrder::getCode, code)
+                .last("limit 1"));
+    }
+
+    @Override
+    public void vinStartChargeSuccessfully(PlatformStartChargingReplyMessageVO vo) {
+        if (vo == null || StringUtils.isEmpty(vo.getTransaction_serial_number())) {
+            return;
+        }
+        TVinChargingOrder order = getVinOrderByCode(vo.getTransaction_serial_number());
+        if (order == null) {
+            return;
+        }
+        if (order.getStatus() != null && order.getStatus() >= ORDER_STATUS_CHARGING) {
+            return;
+        }
+        TVinChargingOrder update = new TVinChargingOrder();
+        update.setId(order.getId());
+        if (vo.getStartup_result() != null && vo.getStartup_result() == 1) {
+            update.setStatus(ORDER_STATUS_CHARGING);
+            update.setStartTime(LocalDateTime.now());
+            updateById(update);
+            TChargingGun gun = new TChargingGun();
+            gun.setId(order.getChargingGunId());
+            gun.setStatus(GUN_STATUS_CHARGING);
+            chargingGunClient.updateChargingGunById(gun);
+            log.info("[VD启动] VIN订单进入充电中 code={}", order.getCode());
+            // 推送监管平台(复用 CD 约定:充电中推平台订单状态 + 成都监管枪状态)
+            order.setStatus(ORDER_STATUS_CHARGING);
+            pushRegulatoryAsync(order, true, false);
+        } else {
+            update.setStatus(ORDER_STATUS_START_FAILED);
+            update.setEndTime(LocalDateTime.now());
+            updateById(update);
+            resetGunQuietly(order);
+            log.warn("[VD启动] VIN订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result());
+            pushRegulatoryAsync(order, false, false);
+        }
+    }
+
+    @Override
+    public void vinChargeMonitoring(UploadRealTimeMonitoringDataQuery query) {
+        if (query == null || StringUtils.isEmpty(query.getTransaction_serial_number())) {
+            return;
+        }
+        TVinChargingOrder order = getVinOrderByCode(query.getTransaction_serial_number());
+        if (order == null) {
+            return;
+        }
+        if (order.getStatus() != null && order.getStatus() >= ORDER_STATUS_FINISHED) {
+            return;
+        }
+        TVinChargingOrder update = new TVinChargingOrder();
+        update.setId(order.getId());
+        if (order.getStatus() == null || order.getStatus() < ORDER_STATUS_CHARGING) {
+            update.setStatus(ORDER_STATUS_CHARGING);
+            if (order.getStartTime() == null) {
+                update.setStartTime(LocalDateTime.now());
+            }
+            TChargingGun gun = new TChargingGun();
+            gun.setId(order.getChargingGunId());
+            gun.setStatus(GUN_STATUS_CHARGING);
+            chargingGunClient.updateChargingGunById(gun);
+        }
+        if (query.getOutput_current() != null && query.getOutput_current().compareTo(BigDecimal.ZERO) != 0
+                && query.getOutput_voltage() != null) {
+            BigDecimal power = query.getOutput_current().multiply(query.getOutput_voltage())
+                    .divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP);
+            update.setChargingPower(power);
+            update.setCurrent(query.getOutput_current());
+            update.setVoltage(query.getOutput_voltage());
+            update.setPower(power);
+            update.setNeedElec(query.getOutput_current());
+            TChargingGun gun = new TChargingGun();
+            gun.setId(order.getChargingGunId());
+            gun.setChargingPower(power);
+            chargingGunClient.updateChargingGunById(gun);
+        }
+        if (query.getCharging_degree() != null) {
+            update.setChargingCapacity(query.getCharging_degree());
+            update.setElectricity(query.getCharging_degree());
+        }
+        updateById(update);
+    }
+
+    @Override
+    public void vinEndCharge(String orderCode, Integer endMode) {
+        if (StringUtils.isEmpty(orderCode)) {
+            return;
+        }
+        TVinChargingOrder order = getVinOrderByCode(orderCode);
+        if (order == null) {
+            return;
+        }
+        TVinChargingOrder update = new TVinChargingOrder();
+        update.setId(order.getId());
+        update.setEndMode(endMode);
+        updateById(update);
+        pushRegulatoryAsync(order, false, false);
+    }
+
+    @Override
+    public R vinEndChargeBillingCharge(TransactionRecordMessageVO vo) {
+        String code = vo == null ? null : vo.getTransaction_serial_number();
+        log.info("[VD结算] 收到交易记录 code={}", code);
+        if (StringUtils.isEmpty(code)) {
+            return R.fail("交易记录缺少交易流水号");
+        }
+        // 结算锁(文档13.3 统一结算入口第一层):订单号维度,防并发/重复账单同时进入结算;
+        // 抢不到锁说明正在结算,按重复账单语义返回成功;锁内仍有状态幂等第二层兜底
+        RLock settleLock = redissonClient.getLock(LOCK_VD_SETTLE + code);
+        try {
+            if (!settleLock.tryLock(5, 60, TimeUnit.SECONDS)) {
+                log.warn("[VD结算] 并发结算进行中,跳过重复账单 code={}", code);
+                return R.ok("重复账单,结算处理中");
+            }
+            return doVinEndChargeBillingCharge(vo, code);
+        } catch (InterruptedException ie) {
+            Thread.currentThread().interrupt();
+            return R.fail("结算锁获取被中断:" + code);
+        } finally {
+            if (settleLock.isHeldByCurrentThread()) {
+                settleLock.unlock();
+            }
+        }
+    }
+
+    /**
+     * VD 结算实际执行体(调用前须持有订单号结算锁)
+     */
+    private R doVinEndChargeBillingCharge(TransactionRecordMessageVO vo, String code) {
+        TVinChargingOrder order = getVinOrderByCode(code);
+        if (order == null) {
+            return R.fail("VIN码充电订单不存在:" + code);
+        }
+        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_FINISHED) {
+            log.warn("[VD结算] 重复账单跳过 code={}", code);
+            return R.ok("重复账单,已按首次账单结算");
+        }
+        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_START_FAILED) {
+            return R.ok("订单已关闭,跳过结算");
+        }
+
+        // 分时电量(以平台快照计价)
+        java.util.Map<Integer, BigDecimal> stageElectricity = TransactionStageParser.parseStageElectricity(vo);
+        BigDecimal totalElectricity = stageElectricity.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+        if (totalElectricity.compareTo(BigDecimal.ZERO) <= 0 && vo.getTotal_electricity() != null) {
+            totalElectricity = vo.getTotal_electricity();
+        }
+
+        Map<Integer, TVinAccountingStrategyDetailOrder> snapshotByType = vinAccountingStrategyDetailOrderService
+                .lambdaQuery().eq(TVinAccountingStrategyDetailOrder::getChargingOrderId, order.getId())
+                .list().stream()
+                .filter(item -> item.getType() != null)
+                .collect(Collectors.toMap(TVinAccountingStrategyDetailOrder::getType, java.util.function.Function.identity(), (a, b) -> a));
+
+        BigDecimal electrovalenceTotal = BigDecimal.ZERO;
+        BigDecimal serviceChargeTotal = BigDecimal.ZERO;
+        List<TVinChargingOrderAccountingStrategy> details = new ArrayList<>();
+        for (Map.Entry<Integer, BigDecimal> stage : stageElectricity.entrySet()) {
+            TVinAccountingStrategyDetailOrder snapshot = snapshotByType.get(stage.getKey());
+            BigDecimal capacity = stage.getValue();
+            BigDecimal electricPrice = snapshot == null || snapshot.getElectrovalence() == null ? BigDecimal.ZERO : snapshot.getElectrovalence();
+            BigDecimal servicePrice = snapshot == null || snapshot.getServiceCharge() == null ? BigDecimal.ZERO : snapshot.getServiceCharge();
+            BigDecimal originalServicePrice = snapshot == null || snapshot.getCostServiceCharge() == null
+                    ? BigDecimal.ZERO : snapshot.getCostServiceCharge();
+            BigDecimal electricAmount = capacity.multiply(electricPrice).setScale(2, BigDecimal.ROUND_HALF_UP);
+            BigDecimal serviceAmount = capacity.multiply(servicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
+            BigDecimal originalServiceAmount = capacity.multiply(originalServicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
+            electrovalenceTotal = electrovalenceTotal.add(electricAmount);
+            serviceChargeTotal = serviceChargeTotal.add(serviceAmount);
+
+            TVinChargingOrderAccountingStrategy detail = new TVinChargingOrderAccountingStrategy();
+            detail.setChargingOrderId(order.getId());
+            detail.setType(stage.getKey());
+            detail.setElectrovalence(electricPrice);
+            detail.setServiceCharge(servicePrice);
+            detail.setCostServiceCharge(originalServicePrice);
+            detail.setChargingCapacity(capacity);
+            detail.setPeriodElectricPrice(electricAmount);
+            detail.setPeriodServicePrice(serviceAmount);
+            detail.setPeriodOriginalServicePrice(originalServiceAmount);
+            if (snapshot != null) {
+                detail.setStartTime(snapshot.getStartTime());
+                detail.setEndTime(snapshot.getEndTime());
+            }
+            details.add(detail);
+        }
+        if (snapshotByType.isEmpty()) {
+            log.error("[VD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
+        }
+        BigDecimal orderAmount = electrovalenceTotal.add(serviceChargeTotal);
+        // lambda 引用要求有效final,拷贝最终值
+        final BigDecimal totalElec = totalElectricity;
+        final BigDecimal elecTotal = electrovalenceTotal;
+        final BigDecimal svcTotal = serviceChargeTotal;
+
+        // 本地事务:写计费明细 + 更新订单(状态5;VD后付记账模式,不做账户扣款)
+        transactionTemplate.execute(status -> {
+            vinChargingOrderAccountingStrategyService.lambdaUpdate()
+                    .eq(TVinChargingOrderAccountingStrategy::getChargingOrderId, order.getId())
+                    .remove();
+            if (!details.isEmpty()) {
+                vinChargingOrderAccountingStrategyService.saveBatch(details);
+            }
+            TVinChargingOrder update = new TVinChargingOrder();
+            update.setId(order.getId());
+            update.setTotalElectricity(totalElec);
+            update.setChargingCapacity(totalElec);
+            update.setElectrovalence(elecTotal);
+            update.setServiceCharge(svcTotal);
+            update.setOrderAmount(orderAmount);
+            update.setPaymentAmount(orderAmount);
+            update.setEndTime(LocalDateTime.now());
+            update.setStatus(ORDER_STATUS_FINISHED);
+            if (order.getEndMode() == null) {
+                update.setEndMode(1);
+            }
+            updateById(update);
+            return true;
+        });
+
+        // 复位充电枪
+        resetGunQuietly(order);
+        // 推送成都监管(结算完成推枪状态 + 订单信息,与 CD 结算链路一致)
+        pushRegulatoryAsync(order, false, true);
+        log.info("[VD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={}",
+                code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount);
+        return R.ok();
+    }
+
+    @Override
+    public TVinChargingOrder getLatestStoppingByGunId(Integer gunId) {
+        if (gunId == null) {
+            return null;
+        }
+        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinChargingOrder>()
+                .eq(TVinChargingOrder::getChargingGunId, gunId)
+                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STOPPING)
+                .orderByDesc(TVinChargingOrder::getId)
+                .last("limit 1"));
+    }
+
+    @Override
+    public void vinTerminateStopByReply(PlatformStopChargingReplyVO vo) {
+        // stopCharging 已前置更新状态4/结束方式1,此处仅兜底确认(幂等)
+        if (vo == null) {
+            return;
+        }
+        TChargingGun gun = null;
+        try {
+            GetChargingGunByCode gunQuery = new GetChargingGunByCode();
+            gunQuery.setCharging_pile_code(vo.getCharging_pile_code());
+            gunQuery.setCharging_gun_code(vo.getCharging_gun_code());
+            gun = chargingGunClient.getChargingGunByCode(gunQuery).getData();
+        } catch (Exception ex) {
+            log.error("[VD停止] 查询充电枪失败 pile={} gun={}", vo.getCharging_pile_code(), vo.getCharging_gun_code(), ex);
+            return;
+        }
+        if (gun == null) {
+            return;
+        }
+        TVinChargingOrder order = getLatestStoppingByGunId(gun.getId());
+        if (order == null) {
+            return;
+        }
+        TVinChargingOrder update = new TVinChargingOrder();
+        update.setId(order.getId());
+        update.setEndMode(1);
+        updateById(update);
+        log.info("[VD停止] 平台停止应答兜底确认 code={}", order.getCode());
+    }
+
+    /**
+     * 异步推送监管平台(复用 CD 约定:充电中推平台订单状态 pushOrderStatus、成都监管推枪状态
+     * notificationConnectorChargeStatus / 结算完成推订单信息 notificationOrderInfo);
+     * 推送失败仅告警不阻断主流程;单元测试环境 client 未注入时静默跳过
+     */
+    private void pushRegulatoryAsync(TVinChargingOrder order, boolean pushOrderStatus, boolean pushOrderInfo) {
+        if (chargingMessageClient == null && chengDuJianGuanClient == null) {
+            return;
+        }
+        new Thread(() -> {
+            try {
+                if (pushOrderStatus && chargingMessageClient != null) {
+                    ChargingOrderVo chargingOrderVo = new ChargingOrderVo();
+                    // 与 CD 一致使用 Spring BeanUtils(本类 ruoyi BeanUtils 同名,故用全限定名)
+                    org.springframework.beans.BeanUtils.copyProperties(order, chargingOrderVo);
+                    chargingMessageClient.pushOrderStatus(chargingOrderVo);
+                }
+                if (chengDuJianGuanClient != null) {
+                    NotificationConnectorChargeStatusVo statusVo = new NotificationConnectorChargeStatusVo();
+                    statusVo.setOrderCode(order.getCode());
+                    statusVo.setChargingGunId(order.getChargingGunId());
+                    chengDuJianGuanClient.notificationConnectorChargeStatus(statusVo);
+                    if (pushOrderInfo) {
+                        chengDuJianGuanClient.notificationOrderInfo(statusVo);
+                    }
+                }
+            } catch (Exception ex) {
+                log.error("[VD监管] 推送监管平台失败 code={}", order.getCode(), ex);
+            }
+        }, "VD-Regulatory-Push").start();
+    }
+
+    /**
+     * 复位充电枪为空闲(失败仅告警)
+     */
+    private void resetGunQuietly(TVinChargingOrder order) {
+        if (order.getChargingGunId() == null) {
+            return;
+        }
+        try {
+            TChargingGun gun = chargingGunClient.getChargingGunById(order.getChargingGunId()).getData();
+            if (gun != null) {
+                gun.setStatus(GUN_STATUS_IDLE);
+                chargingGunClient.updateChargingGunById(gun);
+            }
+        } catch (Exception ex) {
+            log.error("[VD结算] 复位充电枪异常 code={} gunId={}", order.getCode(), order.getChargingGunId(), ex);
+        }
+    }
+
+    // ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) =====
+
+    @Override
+    public List<TVinChargingOrder> listTimeoutStartingOrders() {
+        return lambdaQuery()
+                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING)
+                .lt(TVinChargingOrder::getCreateTime, LocalDateTime.now().minusMinutes(VD_STARTING_TIMEOUT_MINUTES))
+                .list();
+    }
+
+    @Override
+    public List<TVinChargingOrder> listTimeoutChargingOrders() {
+        return lambdaQuery()
+                .in(TVinChargingOrder::getStatus, ORDER_STATUS_CHARGING, ORDER_STATUS_STOPPING)
+                .lt(TVinChargingOrder::getCreateTime, LocalDateTime.now().minusHours(VD_CHARGING_TIMEOUT_HOURS))
+                .list();
+    }
+
+    @Override
+    public void closeTimeoutStartingOrder(TVinChargingOrder order) {
+        TVinChargingOrder update = new TVinChargingOrder();
+        update.setId(order.getId());
+        update.setStatus(ORDER_STATUS_START_FAILED);
+        // 异常终止(沿用 CD 约定:启动失败/异常关单 endMode=0,避免前端结束方式显示空)
+        update.setEndMode(0);
+        update.setEndTime(LocalDateTime.now());
+        updateById(update);
+        resetGunQuietly(order);
+        pushRegulatoryAsync(order, false, false);
+        log.warn("[VD补偿] 启动超时关单 code={} vinId={} gunId={}", order.getCode(), order.getVinId(), order.getChargingGunId());
+    }
+
+    @Override
+    public void compensationSettle(TVinChargingOrder order) {
+        // 无设备交易记录:以订单累计电量构造全平段账单,复用统一结算入口
+        // (结算锁/状态幂等/枪复位全部复用,满足文档13.3"所有路径汇聚同一结算入口")
+        BigDecimal capacity = order.getChargingCapacity() != null ? order.getChargingCapacity()
+                : Optional.ofNullable(order.getElectricity()).orElse(BigDecimal.ZERO);
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setTransaction_serial_number(order.getCode());
+        vo.setFlat_charge1(capacity);
+        vo.setTotal_electricity(capacity);
+        log.warn("[VD补偿] 充电超时兜底结算 code={} 按平段计价 capacity={}", order.getCode(), capacity);
+        vinEndChargeBillingCharge(vo);
+    }
 }
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/TransactionStageParser.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/TransactionStageParser.java
new file mode 100644
index 0000000..b35e6e9
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/TransactionStageParser.java
@@ -0,0 +1,98 @@
+package com.ruoyi.order.util;
+
+import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
+
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 设备交易记录(charging_pile_transaction_record)分时电量解析工具。
+ * <p>
+ * 协议中尖/峰/平/谷四类时段电量分组上送(spike_chargeN / peak_chargeN / flat_chargeN / valley_chargeN,N=1..12),
+ * 本工具按阶段类型汇总电量,供订单结算按平台快照单价计费使用。
+ *
+ * @author codebuddy
+ * @since 2026-09-09
+ */
+public final class TransactionStageParser {
+
+    /** 阶段类型(1=尖,2=峰,3=平,4=谷)与协议字段前缀的映射 */
+    private static final Map<Integer, String> TYPE_PREFIX;
+
+    /** 协议单类型最大分组数 */
+    private static final int MAX_GROUPS = 12;
+
+    static {
+        Map<Integer, String> prefix = new HashMap<>(8);
+        prefix.put(1, "spike");
+        prefix.put(2, "peak");
+        prefix.put(3, "flat");
+        prefix.put(4, "valley");
+        TYPE_PREFIX = Collections.unmodifiableMap(prefix);
+    }
+
+    private TransactionStageParser() {
+    }
+
+    /**
+     * 解析交易记录中各阶段(1=尖,2=峰,3=平,4=谷)的电量合计(度)。
+     * 仅返回电量大于 0 的阶段。
+     *
+     * @param vo 设备交易记录
+     * @return 阶段类型 -> 电量合计(无有效数据时返回空Map)
+     */
+    public static Map<Integer, BigDecimal> parseStageElectricity(TransactionRecordMessageVO vo) {
+        Map<Integer, BigDecimal> result = new HashMap<>(8);
+        if (vo == null) {
+            return result;
+        }
+        for (Map.Entry<Integer, String> entry : TYPE_PREFIX.entrySet()) {
+            BigDecimal total = readGroupTotal(vo, entry.getValue());
+            if (total != null && total.compareTo(BigDecimal.ZERO) > 0) {
+                result.put(entry.getKey(), total);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 读取指定前缀分组的电量合计(spike_charge1..spike_charge12)。
+     */
+    private static BigDecimal readGroupTotal(TransactionRecordMessageVO vo, String prefix) {
+        BigDecimal total = BigDecimal.ZERO;
+        boolean hasValue = false;
+        for (int i = 1; i <= MAX_GROUPS; i++) {
+            BigDecimal value = readField(vo, prefix + "_charge" + i);
+            if (value != null) {
+                hasValue = true;
+                total = total.add(value);
+            }
+        }
+        return hasValue ? total : null;
+    }
+
+    private static BigDecimal readField(TransactionRecordMessageVO vo, String fieldName) {
+        try {
+            Field field = vo.getClass().getDeclaredField(fieldName);
+            field.setAccessible(true);
+            Object value = field.get(vo);
+            if (value instanceof BigDecimal) {
+                return (BigDecimal) value;
+            }
+            if (value instanceof Number) {
+                return new BigDecimal(value.toString());
+            }
+            if (value instanceof String && ((String) value).length() > 0) {
+                return new BigDecimal((String) value);
+            }
+        } catch (NoSuchFieldException ignored) {
+            // 分组不存在说明协议未上送该组,按0处理
+        } catch (IllegalAccessException | NumberFormatException ex) {
+            // 非法数据按0处理,避免结算中断
+        }
+        return null;
+    }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java
index a2b0242..36093a4 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java
@@ -96,6 +96,12 @@
     private SiteClient siteClient;
     @Resource
     private TChargingOrderService chargingOrderService;
+
+    @Resource
+    private TCardChargingOrderService cardChargingOrderService;
+
+    @Resource
+    private TVinChargingOrderService vinChargingOrderService;
     
     @Resource
     private TChargingOrderRefundService chargingOrderRefundService;
@@ -283,6 +289,92 @@
         }
     }
 
+    /**
+     * 刷卡/VIN 订单超时补偿(文档13.3 定时任务补偿路径,10分钟一轮):
+     * 1) 启动中超时(状态2超30分钟未收到启动成功应答)→ 关单(状态6)+解锁卡片+复位枪;
+     * 2) 充电中超时(状态3/4超24小时未收到交易记录)→ 以订单累计电量构造平段账单,
+     *    复用统一结算入口兜底结算(结算锁/状态幂等/扣款幂等全复用)
+     */
+    @Scheduled(fixedRate = 1000 * 60 * 10, initialDelay = 1000 * 60)
+    public void cardVinOrderCompensationTask() {
+        if (isScheduledTaskPort(port)) {
+            compensateCardOrders();
+            compensateVinOrders();
+        }
+    }
+
+    /**
+     * SD 刷卡订单超时补偿
+     */
+    private void compensateCardOrders() {
+        try {
+            List<TCardChargingOrder> startingOrders = cardChargingOrderService.listTimeoutStartingOrders();
+            if (!startingOrders.isEmpty()) {
+                log.info("[SD补偿] 启动超时订单数量:{}", startingOrders.size());
+                for (TCardChargingOrder order : startingOrders) {
+                    try {
+                        cardChargingOrderService.closeTimeoutStartingOrder(order);
+                    } catch (Exception ex) {
+                        log.error("[SD补偿] 启动超时关单失败 code={}", order.getCode(), ex);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[SD补偿] 启动超时扫描异常", e);
+        }
+        try {
+            List<TCardChargingOrder> chargingOrders = cardChargingOrderService.listTimeoutChargingOrders();
+            if (!chargingOrders.isEmpty()) {
+                log.info("[SD补偿] 充电超时订单数量:{}", chargingOrders.size());
+                for (TCardChargingOrder order : chargingOrders) {
+                    try {
+                        cardChargingOrderService.compensationSettle(order);
+                    } catch (Exception ex) {
+                        log.error("[SD补偿] 充电超时兜底结算失败 code={}", order.getCode(), ex);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[SD补偿] 充电超时扫描异常", e);
+        }
+    }
+
+    /**
+     * VD VIN码订单超时补偿
+     */
+    private void compensateVinOrders() {
+        try {
+            List<TVinChargingOrder> startingOrders = vinChargingOrderService.listTimeoutStartingOrders();
+            if (!startingOrders.isEmpty()) {
+                log.info("[VD补偿] 启动超时订单数量:{}", startingOrders.size());
+                for (TVinChargingOrder order : startingOrders) {
+                    try {
+                        vinChargingOrderService.closeTimeoutStartingOrder(order);
+                    } catch (Exception ex) {
+                        log.error("[VD补偿] 启动超时关单失败 code={}", order.getCode(), ex);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[VD补偿] 启动超时扫描异常", e);
+        }
+        try {
+            List<TVinChargingOrder> chargingOrders = vinChargingOrderService.listTimeoutChargingOrders();
+            if (!chargingOrders.isEmpty()) {
+                log.info("[VD补偿] 充电超时订单数量:{}", chargingOrders.size());
+                for (TVinChargingOrder order : chargingOrders) {
+                    try {
+                        vinChargingOrderService.compensationSettle(order);
+                    } catch (Exception ex) {
+                        log.error("[VD补偿] 充电超时兜底结算失败 code={}", order.getCode(), ex);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[VD补偿] 充电超时扫描异常", e);
+        }
+    }
+
 
     //十分钟循环执行的定时任务
     @Scheduled(fixedRate = 1000 * 60 * 10)
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinOrderCompensationTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinOrderCompensationTest.java
new file mode 100644
index 0000000..cb879bc
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinOrderCompensationTest.java
@@ -0,0 +1,213 @@
+package com.ruoyi.order.service.impl;
+
+import com.ruoyi.account.api.feignClient.ChargingCardClient;
+import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
+import com.ruoyi.chargingPile.api.model.TChargingGun;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.order.api.model.TCardChargingOrder;
+import com.ruoyi.order.api.model.TVinChargingOrder;
+import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
+import com.ruoyi.order.mapper.TCardChargingOrderMapper;
+import com.ruoyi.order.mapper.TVinChargingOrderMapper;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.test.util.ReflectionTestUtils;
+
+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.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * SD/VD 定时补偿与结算锁行为测试(文档13.2/13.3):
+ * 1) 启动超时关单(状态-1+结束方式0+解锁卡片+复位枪,沿用 t_charging_order 枚举约定);
+ * 2) 结算入口并发锁抢占失败快速返回,不触达订单数据;
+ * 3) 已结算订单重复账单幂等跳过,不再扣款/解锁。
+ */
+class CardVinOrderCompensationTest {
+
+    @SuppressWarnings("unchecked")
+    private R<Boolean> okBoolean() {
+        R<Boolean> result = mock(R.class);
+        when(result.getData()).thenReturn(true);
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    private R<TChargingGun> gunOf(TChargingGun gun) {
+        R<TChargingGun> result = mock(R.class);
+        when(result.getData()).thenReturn(gun);
+        return result;
+    }
+
+    private RLock mockedLock(boolean tryLockResult) throws InterruptedException {
+        RLock lock = mock(RLock.class);
+        when(lock.tryLock(anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(tryLockResult);
+        when(lock.isHeldByCurrentThread()).thenReturn(false);
+        return lock;
+    }
+
+    private RedissonClient mockedRedisson(RLock lock) {
+        RedissonClient redisson = mock(RedissonClient.class);
+        when(redisson.getLock(anyString())).thenReturn(lock);
+        return redisson;
+    }
+
+    // ===== SD:启动超时关单 =====
+
+    @Test
+    void closesCardTimeoutStartingOrderWithCardUnlockAndGunReset() {
+        TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
+        TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
+        ChargingCardClient cardClient = mock(ChargingCardClient.class);
+        ChargingGunClient gunClient = mock(ChargingGunClient.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
+        ReflectionTestUtils.setField(service, "chargingGunClient", gunClient);
+        when(mapper.updateById(any(TCardChargingOrder.class))).thenReturn(1);
+        R<Boolean> unlockResult = okBoolean();
+        when(cardClient.unlockForCharging(anyLong(), anyString())).thenReturn(unlockResult);
+        TChargingGun gun = new TChargingGun();
+        gun.setId(9);
+        gun.setStatus(4);
+        R<TChargingGun> gunResult = gunOf(gun);
+        when(gunClient.getChargingGunById(any())).thenReturn(gunResult);
+
+        TCardChargingOrder order = new TCardChargingOrder();
+        order.setId(1L);
+        order.setCode("SD1");
+        order.setStatus(2);
+        order.setCardId(5L);
+        order.setChargingGunId(9);
+        service.closeTimeoutStartingOrder(order);
+
+        ArgumentCaptor<TCardChargingOrder> captor = ArgumentCaptor.forClass(TCardChargingOrder.class);
+        verify(mapper).updateById(captor.capture());
+        assertEquals(-1, captor.getValue().getStatus());
+        assertEquals(0, captor.getValue().getEndMode());
+        assertNotNull(captor.getValue().getEndTime());
+        verify(cardClient).unlockForCharging(5L, "SD1");
+        ArgumentCaptor<TChargingGun> gunCaptor = ArgumentCaptor.forClass(TChargingGun.class);
+        verify(gunClient).updateChargingGunById(gunCaptor.capture());
+        assertEquals(2, gunCaptor.getValue().getStatus());
+    }
+
+    // ===== SD:结算锁抢占失败快速返回 =====
+
+    @Test
+    void returnsOkWithoutTouchingOrderWhenSettleLockBusy() throws InterruptedException {
+        TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
+        TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(false)));
+
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setTransaction_serial_number("SD1");
+        R result = service.cardEndChargeBillingCharge(vo);
+
+        assertEquals(R.SUCCESS, result.getCode());
+        verifyNoInteractions(mapper);
+    }
+
+    // ===== SD:已结算订单重复账单幂等 =====
+
+    @Test
+    void skipsCardSettlementWhenOrderAlreadyFinished() throws InterruptedException {
+        TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
+        TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
+        ChargingCardClient cardClient = mock(ChargingCardClient.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
+        ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true)));
+
+        TCardChargingOrder finished = new TCardChargingOrder();
+        finished.setId(1L);
+        finished.setCode("SD1");
+        finished.setStatus(5);
+        finished.setCardId(5L);
+        when(mapper.selectOne(any())).thenReturn(finished);
+        when(mapper.selectList(any())).thenReturn(Collections.singletonList(finished));
+
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setTransaction_serial_number("SD1");
+        vo.setFlat_charge1(new java.math.BigDecimal("10"));
+        R result = service.cardEndChargeBillingCharge(vo);
+
+        assertEquals(R.SUCCESS, result.getCode());
+        verify(cardClient, never()).deductForCharging(any(com.ruoyi.account.api.dto.ChargingCardDeductDTO.class));
+        verify(cardClient, never()).unlockForCharging(anyLong(), anyString());
+    }
+
+    // ===== VD:启动超时关单(无卡片,仅复位枪) =====
+
+    @Test
+    void closesVinTimeoutStartingOrderWithGunReset() {
+        TVinChargingOrderServiceImpl service = new TVinChargingOrderServiceImpl();
+        TVinChargingOrderMapper mapper = mock(TVinChargingOrderMapper.class);
+        ChargingGunClient gunClient = mock(ChargingGunClient.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "chargingGunClient", gunClient);
+        when(mapper.updateById(any(TVinChargingOrder.class))).thenReturn(1);
+        TChargingGun gun = new TChargingGun();
+        gun.setId(7);
+        gun.setStatus(4);
+        R<TChargingGun> gunResult = gunOf(gun);
+        when(gunClient.getChargingGunById(any())).thenReturn(gunResult);
+
+        TVinChargingOrder order = new TVinChargingOrder();
+        order.setId(2L);
+        order.setCode("VD1");
+        order.setStatus(2);
+        order.setChargingGunId(7);
+        service.closeTimeoutStartingOrder(order);
+
+        ArgumentCaptor<TVinChargingOrder> captor = ArgumentCaptor.forClass(TVinChargingOrder.class);
+        verify(mapper).updateById(captor.capture());
+        assertEquals(-1, captor.getValue().getStatus());
+        assertEquals(0, captor.getValue().getEndMode());
+        assertNotNull(captor.getValue().getEndTime());
+        ArgumentCaptor<TChargingGun> gunCaptor = ArgumentCaptor.forClass(TChargingGun.class);
+        verify(gunClient).updateChargingGunById(gunCaptor.capture());
+        assertEquals(2, gunCaptor.getValue().getStatus());
+    }
+
+    // ===== VD:已结算订单重复账单幂等 =====
+
+    @Test
+    void skipsVinSettlementWhenOrderAlreadyFinished() throws InterruptedException {
+        TVinChargingOrderServiceImpl service = new TVinChargingOrderServiceImpl();
+        TVinChargingOrderMapper mapper = mock(TVinChargingOrderMapper.class);
+        ChargingGunClient gunClient = mock(ChargingGunClient.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "chargingGunClient", gunClient);
+        ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true)));
+
+        TVinChargingOrder finished = new TVinChargingOrder();
+        finished.setId(2L);
+        finished.setCode("VD1");
+        finished.setStatus(5);
+        finished.setChargingGunId(7);
+        when(mapper.selectOne(any())).thenReturn(finished);
+        when(mapper.selectList(any())).thenReturn(Collections.singletonList(finished));
+
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setTransaction_serial_number("VD1");
+        vo.setFlat_charge1(new java.math.BigDecimal("8"));
+        R result = service.vinEndChargeBillingCharge(vo);
+
+        assertEquals(R.SUCCESS, result.getCode());
+        verify(gunClient, never()).updateChargingGunById(any(TChargingGun.class));
+    }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/TransactionStageParserTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/TransactionStageParserTest.java
new file mode 100644
index 0000000..ac38126
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/TransactionStageParserTest.java
@@ -0,0 +1,88 @@
+package com.ruoyi.order.util;
+
+import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * 设备交易记录分时电量解析测试(尖1/峰2/平3/谷4,spike/peak/flat/valley_charge1..12 分组求和)
+ */
+class TransactionStageParserTest {
+
+    @Test
+    void parsesFourStagesAcrossGroups() {
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setSpike_charge1(new BigDecimal("1.10"));
+        vo.setPeak_charge2(new BigDecimal("2.20"));
+        vo.setFlat_charge3(new BigDecimal("3.30"));
+        vo.setValley_charge1(new BigDecimal("4.40"));
+
+        Map<Integer, BigDecimal> stages = TransactionStageParser.parseStageElectricity(vo);
+
+        assertEquals(4, stages.size());
+        assertEquals(0, new BigDecimal("1.10").compareTo(stages.get(1)));
+        assertEquals(0, new BigDecimal("2.20").compareTo(stages.get(2)));
+        assertEquals(0, new BigDecimal("3.30").compareTo(stages.get(3)));
+        assertEquals(0, new BigDecimal("4.40").compareTo(stages.get(4)));
+    }
+
+    @Test
+    void sumsMultipleGroupsPerStage() {
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setFlat_charge1(new BigDecimal("1.50"));
+        vo.setFlat_charge5(new BigDecimal("2.25"));
+
+        Map<Integer, BigDecimal> stages = TransactionStageParser.parseStageElectricity(vo);
+
+        assertEquals(1, stages.size());
+        assertEquals(0, new BigDecimal("3.75").compareTo(stages.get(3)));
+    }
+
+    @Test
+    void ignoresMissingStages() {
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setFlat_charge1(new BigDecimal("10"));
+
+        Map<Integer, BigDecimal> stages = TransactionStageParser.parseStageElectricity(vo);
+
+        assertEquals(1, stages.size());
+        assertNotNull(stages.get(3));
+    }
+
+    @Test
+    void returnsEmptyForNullVo() {
+        assertTrue(TransactionStageParser.parseStageElectricity(null).isEmpty());
+    }
+
+    @Test
+    void ignoresZeroOrMissingElectricity() {
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setFlat_charge1(BigDecimal.ZERO);
+
+        assertTrue(TransactionStageParser.parseStageElectricity(vo).isEmpty());
+    }
+
+    /**
+     * 定时补偿场景:无设备分时账单时以订单累计电量构造全平段 VO,
+     * 解析后必须落在平段(3),保证兜底结算可按平段快照计价
+     */
+    @Test
+    void compensationVoCarriesFlatStageElectricity() {
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setTransaction_serial_number("SD999");
+        vo.setFlat_charge1(new BigDecimal("17.36"));
+        vo.setTotal_electricity(new BigDecimal("17.36"));
+
+        Map<Integer, BigDecimal> stages = TransactionStageParser.parseStageElectricity(vo);
+
+        assertEquals(1, stages.size());
+        assertEquals(0, new BigDecimal("17.36").compareTo(stages.get(3)));
+        assertEquals(0, new BigDecimal("17.36").compareTo(vo.getTotal_electricity()));
+    }
+}
diff --git a/sql/20260909_card_password_charge_lock.sql b/sql/20260909_card_password_charge_lock.sql
new file mode 100644
index 0000000..dc61a39
--- /dev/null
+++ b/sql/20260909_card_password_charge_lock.sql
@@ -0,0 +1,86 @@
+-- 刷卡充电 V0.2 定稿:卡片密码配置、充电期间锁定、订单结算幂等扣款(允许负余额)
+-- 目标库:charging_pile_account(t_charging_card、t_charging_card_balance_record)
+-- 适用:MySQL(含 Mycat 代理下的 MySQL 8 后端)。Mycat 对 PREPARE/INFORMATION_SCHEMA 变量支持不稳定时,
+--       可直接执行下方「直接 DDL 小节」并回查。
+-- 脚本幂等:字段/索引已存在则自动跳过,可重复执行。
+
+USE `charging_pile_account`;
+
+-- ============ 1. t_charging_card 新增 password / charging_flag / charging_order_code ============
+SET @has_password_col := (
+  SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = 'charging_pile_account' AND TABLE_NAME = 't_charging_card' AND COLUMN_NAME = 'password'
+);
+SET @add_password_sql := IF(
+  @has_password_col = 0,
+  'ALTER TABLE `t_charging_card` ADD COLUMN `password` varchar(100) DEFAULT NULL COMMENT ''卡密码(BCrypt密文,空=未配置不校验)'' AFTER `card_no`',
+  'SELECT 1'
+);
+PREPARE add_password_stmt FROM @add_password_sql;
+EXECUTE add_password_stmt;
+DEALLOCATE PREPARE add_password_stmt;
+
+SET @has_charging_flag_col := (
+  SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = 'charging_pile_account' AND TABLE_NAME = 't_charging_card' AND COLUMN_NAME = 'charging_flag'
+);
+SET @add_charging_flag_sql := IF(
+  @has_charging_flag_col = 0,
+  'ALTER TABLE `t_charging_card` ADD COLUMN `charging_flag` tinyint NOT NULL DEFAULT 0 COMMENT ''充电锁定:0未充电,1充电中锁定'' AFTER `status`',
+  'SELECT 1'
+);
+PREPARE add_charging_flag_stmt FROM @add_charging_flag_sql;
+EXECUTE add_charging_flag_stmt;
+DEALLOCATE PREPARE add_charging_flag_stmt;
+
+SET @has_charging_order_code_col := (
+  SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = 'charging_pile_account' AND TABLE_NAME = 't_charging_card' AND COLUMN_NAME = 'charging_order_code'
+);
+SET @add_charging_order_code_sql := IF(
+  @has_charging_order_code_col = 0,
+  'ALTER TABLE `t_charging_card` ADD COLUMN `charging_order_code` varchar(64) DEFAULT NULL COMMENT ''充电锁定订单号'' AFTER `charging_flag`',
+  'SELECT 1'
+);
+PREPARE add_charging_order_code_stmt FROM @add_charging_order_code_sql;
+EXECUTE add_charging_order_code_stmt;
+DEALLOCATE PREPARE add_charging_order_code_stmt;
+
+-- ============ 2. t_charging_card_balance_record 新增充电订单号(扣款幂等键)与唯一索引 ============
+SET @has_record_order_col := (
+  SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = 'charging_pile_account' AND TABLE_NAME = 't_charging_card_balance_record' AND COLUMN_NAME = 'charging_order_code'
+);
+SET @add_record_order_sql := IF(
+  @has_record_order_col = 0,
+  'ALTER TABLE `t_charging_card_balance_record` ADD COLUMN `charging_order_code` varchar(64) DEFAULT NULL COMMENT ''关联订单号(刷卡充电订单结算扣款,幂等键)'' AFTER `remark`',
+  'SELECT 1'
+);
+PREPARE add_record_order_stmt FROM @add_record_order_sql;
+EXECUTE add_record_order_stmt;
+DEALLOCATE PREPARE add_record_order_stmt;
+
+SET @has_record_uk := (
+  SELECT COUNT(1) FROM information_schema.STATISTICS
+  WHERE TABLE_SCHEMA = 'charging_pile_account' AND TABLE_NAME = 't_charging_card_balance_record' AND INDEX_NAME = 'uk_order_code'
+);
+SET @add_record_uk_sql := IF(
+  @has_record_uk = 0,
+  'ALTER TABLE `t_charging_card_balance_record` ADD UNIQUE KEY `uk_order_code` (`charging_order_code`)',
+  'SELECT 1'
+);
+PREPARE add_record_uk_stmt FROM @add_record_uk_sql;
+EXECUTE add_record_uk_stmt;
+DEALLOCATE PREPARE add_record_uk_stmt;
+
+-- ========== 直接 DDL 小节(Mycat 下如上面 PREPARE 报错,可按序手工执行以下四条) ==========
+-- ALTER TABLE `charging_pile_account`.`t_charging_card`
+--   ADD COLUMN `password` varchar(100) DEFAULT NULL COMMENT '卡密码(BCrypt密文,空=未配置不校验)' AFTER `card_no`;
+-- ALTER TABLE `charging_pile_account`.`t_charging_card`
+--   ADD COLUMN `charging_flag` tinyint NOT NULL DEFAULT 0 COMMENT '充电锁定:0未充电,1充电中锁定' AFTER `status`;
+-- ALTER TABLE `charging_pile_account`.`t_charging_card`
+--   ADD COLUMN `charging_order_code` varchar(64) DEFAULT NULL COMMENT '充电锁定订单号' AFTER `charging_flag`;
+-- ALTER TABLE `charging_pile_account`.`t_charging_card_balance_record`
+--   ADD COLUMN `charging_order_code` varchar(64) DEFAULT NULL COMMENT '关联订单号(刷卡充电订单结算扣款,幂等键)' AFTER `remark`;
+-- ALTER TABLE `charging_pile_account`.`t_charging_card_balance_record`
+--   ADD UNIQUE KEY `uk_order_code` (`charging_order_code`);
diff --git a/sql/20260909_sd_vd_order_enum_comment.sql b/sql/20260909_sd_vd_order_enum_comment.sql
new file mode 100644
index 0000000..faeecb9
--- /dev/null
+++ b/sql/20260909_sd_vd_order_enum_comment.sql
@@ -0,0 +1,18 @@
+-- SD/VD 订单表枚举注释补全(对齐 t_charging_order 既有约定)
+-- 背景:SD/VD 代码已将"启动失败"状态从 6 修正为 -1(沿用 CD 枚举:负值语义,前端按 status<0 渲染"启动失败"),
+--       并补设 endMode(异常终止=0)与 SD payMethod(1=余额支付);本脚本仅补全列注释,不改变数据。
+-- MyCat 环境请在逻辑库 charging_pile_order 执行(分表表名会自动路由)。
+
+USE charging_pile_order;
+
+-- 刷卡充电订单(SD)
+ALTER TABLE `t_card_charging_order`
+    MODIFY COLUMN `status` int DEFAULT NULL COMMENT '订单状态(-1=启动失败,0=未支付,1=已支付待充电,2=启动中,3=充电中,4=停止中,5=已完成;沿用 t_charging_order 枚举)',
+    MODIFY COLUMN `end_mode` int DEFAULT NULL COMMENT '结束方式(0=异常终止,1=主动终止,2=满电自动终止,3=费用不足终止)',
+    MODIFY COLUMN `pay_method` int DEFAULT NULL COMMENT '充电支付模式(1=余额支付,2=余额组合支付,3=第三方支付;SD 固定 1=余额支付)';
+
+-- VIN码充电订单(VD)
+ALTER TABLE `t_vin_charging_order`
+    MODIFY COLUMN `status` int DEFAULT NULL COMMENT '订单状态(-1=启动失败,0=未支付,1=已支付待充电,2=启动中,3=充电中,4=停止中,5=已完成;沿用 t_charging_order 枚举)',
+    MODIFY COLUMN `end_mode` int DEFAULT NULL COMMENT '结束方式(0=异常终止,1=主动终止,2=满电自动终止,3=费用不足终止)',
+    MODIFY COLUMN `pay_method` int DEFAULT NULL COMMENT '充电支付模式(1=余额支付,2=余额组合支付,3=第三方支付;VD 后付记账模式留空=无支付动作)';

--
Gitblit v1.7.1