From 4a4babb58fb64badd85668265c15e7787b2cf43e Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期一, 14 九月 2026 17:42:37 +0800
Subject: [PATCH] 修改SD和VD获取计费策略错误的问题

---
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java       |   77 ++++--
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java      |   80 ++++--
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/TransactionStageParserTest.java                 |   41 +++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/TransactionStageParser.java                     |  106 +++++++-
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinOrderCompensationTest.java       |   76 ++++++
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinSettlementTimeAlignmentTest.java |  282 ++++++++++++++++++++++
 6 files changed, 580 insertions(+), 82 deletions(-)

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 e4e24dd..7a821d6 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
@@ -78,7 +78,6 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 
 @Service
@@ -852,29 +851,36 @@
             return R.ok("订单已关闭,跳过结算");
         }
 
-        // 分时电量(以平台快照计价,V0.2 定稿)
-        Map<Integer, BigDecimal> stageElectricity = TransactionStageParser.parseStageElectricity(vo);
-        BigDecimal totalElectricity = stageElectricity.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+        List<TransactionStageParser.PeriodElectricity> reportedPeriods =
+                TransactionStageParser.parsePeriodElectricity(vo);
+        BigDecimal totalElectricity = reportedPeriods.stream()
+                .map(TransactionStageParser.PeriodElectricity::getElectricity)
+                .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
+        List<TCardAccountingStrategyDetailOrder> snapshots = new ArrayList<>(cardAccountingStrategyDetailOrderService
                 .lambdaQuery().eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, order.getId())
-                .list().stream()
-                .filter(item -> item.getType() != null)
-                .collect(Collectors.toMap(TCardAccountingStrategyDetailOrder::getType, Function.identity(), (a, b) -> a));
+                .list());
+        snapshots.removeIf(item -> item.getType() == null);
+        snapshots.sort(Comparator.comparing(TCardAccountingStrategyDetailOrder::getStartTime,
+                Comparator.nullsLast(String::compareTo)));
 
         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
+        // 严格对齐 CD:遍历按 start_time 排序后的策略,第 N 段只读取该策略类型对应的 chargeN。
+        for (int snapshotIndex = 0; snapshotIndex < snapshots.size(); snapshotIndex++) {
+            TCardAccountingStrategyDetailOrder snapshot = snapshots.get(snapshotIndex);
+            BigDecimal capacity = TransactionStageParser.getPeriodElectricity(
+                    vo, snapshot.getType(), snapshotIndex + 1);
+            if (capacity == null || capacity.compareTo(BigDecimal.ZERO) <= 0) {
+                continue;
+            }
+            BigDecimal electricPrice = snapshot.getElectrovalence() == null ? BigDecimal.ZERO : snapshot.getElectrovalence();
+            BigDecimal servicePrice = snapshot.getServiceCharge() == null ? BigDecimal.ZERO : snapshot.getServiceCharge();
+            BigDecimal originalServicePrice = 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);
@@ -884,7 +890,7 @@
 
             TCardChargingOrderAccountingStrategy detail = new TCardChargingOrderAccountingStrategy();
             detail.setChargingOrderId(order.getId());
-            detail.setType(stage.getKey());
+            detail.setType(snapshot.getType());
             detail.setElectrovalence(electricPrice);
             detail.setServiceCharge(servicePrice);
             detail.setCostServiceCharge(originalServicePrice);
@@ -904,21 +910,20 @@
             detail.setCreateTime(LocalDateTime.now());
             details.add(detail);
         }
-        if (snapshotByType.isEmpty()) {
+        if (snapshots.isEmpty()) {
             log.error("[SD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
         }
-        // 与 CD 充电 TaskUtil.endOrder 同口径:details 按 type 升序对齐业务时段顺序,
-        // 首段 start_time 用订单实际开始时间的 HH:mm 覆盖(对应 CD 的时段内首条监控数据 HH:mm),
-        // 末段 end_time 用订单实际结束时间的 HH:mm 覆盖(对应 CD 的时段内末条监控数据 HH:mm)。
-        // 若 order.startTime/endTime 为空(结算事务内才回填),则跳过覆盖以保留快照时段位兜底。
+        // 首段使用订单实际开始时间;订单尚未写入结束时间时,末段使用交易记录设备结束时间。
+        LocalDateTime settlementStartTime = order.getStartTime() != null
+                ? order.getStartTime() : parseDeviceTime(vo.getStart_time(), null);
+        LocalDateTime settlementEndTime = parseDeviceTime(vo.getEnd_time(), order.getEndTime());
         if (!details.isEmpty()) {
-            details.sort(Comparator.comparingInt(TCardChargingOrderAccountingStrategy::getType));
             DateTimeFormatter hhmm = DateTimeFormatter.ofPattern("HH:mm");
-            if (order.getStartTime() != null) {
-                details.get(0).setStartTime(order.getStartTime().format(hhmm));
+            if (settlementStartTime != null) {
+                details.get(0).setStartTime(settlementStartTime.format(hhmm));
             }
-            if (order.getEndTime() != null) {
-                details.get(details.size() - 1).setEndTime(order.getEndTime().format(hhmm));
+            if (settlementEndTime != null) {
+                details.get(details.size() - 1).setEndTime(settlementEndTime.format(hhmm));
             }
         }
         BigDecimal orderAmount = electrovalenceTotal.add(serviceChargeTotal);
@@ -1160,11 +1165,30 @@
         // (结算锁/状态幂等/扣款幂等/解锁/复位全部复用,满足文档13.3"所有路径汇聚同一结算入口")
         BigDecimal capacity = order.getChargingCapacity() != null ? order.getChargingCapacity()
                 : Optional.ofNullable(order.getElectricity()).orElse(BigDecimal.ZERO);
+        int flatPeriodIndex = resolveCompensationFlatPeriodIndex(order.getId());
         TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
         vo.setTransaction_serial_number(order.getCode());
-        vo.setFlat_charge1(capacity);
+        TransactionStageParser.setPeriodElectricity(vo, 3, flatPeriodIndex, capacity);
         vo.setTotal_electricity(capacity);
-        log.warn("[SD补偿] 充电超时兜底结算 code={} 按平段计价 capacity={}", order.getCode(), capacity);
+        log.warn("[SD补偿] 充电超时兜底结算 code={} 按平段计价 periodIndex={} capacity={}",
+                order.getCode(), flatPeriodIndex, capacity);
         cardEndChargeBillingCharge(vo);
     }
+
+    private int resolveCompensationFlatPeriodIndex(Long orderId) {
+        List<TCardAccountingStrategyDetailOrder> snapshots = Optional.ofNullable(cardAccountingStrategyDetailOrderService
+                        .lambdaQuery().eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, orderId).list())
+                .orElse(Collections.emptyList()).stream()
+                .filter(item -> item.getType() != null)
+                .sorted(Comparator.comparing(TCardAccountingStrategyDetailOrder::getStartTime,
+                        Comparator.nullsLast(String::compareTo)))
+                .collect(Collectors.toList());
+        for (int i = 0; i < snapshots.size(); i++) {
+            if (Integer.valueOf(3).equals(snapshots.get(i).getType())) {
+                return i + 1;
+            }
+        }
+        log.error("[SD补偿] 订单计费策略无平段,回退构造 flat_charge1 orderId={}", orderId);
+        return 1;
+    }
 }
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 9cfc34e..408089f 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
@@ -988,28 +988,36 @@
             return R.ok("订单已关闭,跳过结算");
         }
 
-        // 分时电量(以平台快照计价)
-        java.util.Map<Integer, BigDecimal> stageElectricity = TransactionStageParser.parseStageElectricity(vo);
-        BigDecimal totalElectricity = stageElectricity.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+        List<TransactionStageParser.PeriodElectricity> reportedPeriods =
+                TransactionStageParser.parsePeriodElectricity(vo);
+        BigDecimal totalElectricity = reportedPeriods.stream()
+                .map(TransactionStageParser.PeriodElectricity::getElectricity)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
         if (totalElectricity.compareTo(BigDecimal.ZERO) <= 0 && vo.getTotal_electricity() != null) {
             totalElectricity = vo.getTotal_electricity();
         }
 
-        Map<Integer, TVinAccountingStrategyDetailOrder> snapshotByType = vinAccountingStrategyDetailOrderService
+        List<TVinAccountingStrategyDetailOrder> snapshots = new ArrayList<>(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));
+                .list());
+        snapshots.removeIf(item -> item.getType() == null);
+        snapshots.sort(Comparator.comparing(TVinAccountingStrategyDetailOrder::getStartTime,
+                Comparator.nullsLast(String::compareTo)));
 
         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
+        // 严格对齐 CD:遍历按 start_time 排序后的策略,第 N 段只读取该策略类型对应的 chargeN。
+        for (int snapshotIndex = 0; snapshotIndex < snapshots.size(); snapshotIndex++) {
+            TVinAccountingStrategyDetailOrder snapshot = snapshots.get(snapshotIndex);
+            BigDecimal capacity = TransactionStageParser.getPeriodElectricity(
+                    vo, snapshot.getType(), snapshotIndex + 1);
+            if (capacity == null || capacity.compareTo(BigDecimal.ZERO) <= 0) {
+                continue;
+            }
+            BigDecimal electricPrice = snapshot.getElectrovalence() == null ? BigDecimal.ZERO : snapshot.getElectrovalence();
+            BigDecimal servicePrice = snapshot.getServiceCharge() == null ? BigDecimal.ZERO : snapshot.getServiceCharge();
+            BigDecimal originalServicePrice = 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);
@@ -1019,7 +1027,7 @@
 
             TVinChargingOrderAccountingStrategy detail = new TVinChargingOrderAccountingStrategy();
             detail.setChargingOrderId(order.getId());
-            detail.setType(stage.getKey());
+            detail.setType(snapshot.getType());
             detail.setElectrovalence(electricPrice);
             detail.setServiceCharge(servicePrice);
             detail.setCostServiceCharge(originalServicePrice);
@@ -1039,20 +1047,20 @@
             detail.setCreateTime(LocalDateTime.now());
             details.add(detail);
         }
-        if (snapshotByType.isEmpty()) {
+        if (snapshots.isEmpty()) {
             log.error("[VD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
         }
-        // 与 CD 充电 TaskUtil.endOrder 同口径:details 按 type 升序对齐业务时段顺序,
-        // 首段 start_time 用订单实际开始时间的 HH:mm 覆盖(对应 CD 的时段内首条监控数据 HH:mm),
-        // 末段 end_time 用订单实际结束时间的 HH:mm 覆盖(对应 CD 的时段内末条监控数据 HH:mm)。
+        // 首段使用订单实际开始时间;订单尚未写入结束时间时,末段使用交易记录设备结束时间。
+        LocalDateTime settlementStartTime = order.getStartTime() != null
+                ? order.getStartTime() : parseDeviceTime(vo.getStart_time(), null);
+        LocalDateTime settlementEndTime = parseDeviceTime(vo.getEnd_time(), order.getEndTime());
         if (!details.isEmpty()) {
-            details.sort(Comparator.comparingInt(TVinChargingOrderAccountingStrategy::getType));
             DateTimeFormatter hhmm = DateTimeFormatter.ofPattern("HH:mm");
-            if (order.getStartTime() != null) {
-                details.get(0).setStartTime(order.getStartTime().format(hhmm));
+            if (settlementStartTime != null) {
+                details.get(0).setStartTime(settlementStartTime.format(hhmm));
             }
-            if (order.getEndTime() != null) {
-                details.get(details.size() - 1).setEndTime(order.getEndTime().format(hhmm));
+            if (settlementEndTime != null) {
+                details.get(details.size() - 1).setEndTime(settlementEndTime.format(hhmm));
             }
         }
         BigDecimal orderAmount = electrovalenceTotal.add(serviceChargeTotal);
@@ -1275,11 +1283,30 @@
         // (结算锁/状态幂等/枪复位全部复用,满足文档13.3"所有路径汇聚同一结算入口")
         BigDecimal capacity = order.getChargingCapacity() != null ? order.getChargingCapacity()
                 : Optional.ofNullable(order.getElectricity()).orElse(BigDecimal.ZERO);
+        int flatPeriodIndex = resolveCompensationFlatPeriodIndex(order.getId());
         TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
         vo.setTransaction_serial_number(order.getCode());
-        vo.setFlat_charge1(capacity);
+        TransactionStageParser.setPeriodElectricity(vo, 3, flatPeriodIndex, capacity);
         vo.setTotal_electricity(capacity);
-        log.warn("[VD补偿] 充电超时兜底结算 code={} 按平段计价 capacity={}", order.getCode(), capacity);
+        log.warn("[VD补偿] 充电超时兜底结算 code={} 按平段计价 periodIndex={} capacity={}",
+                order.getCode(), flatPeriodIndex, capacity);
         vinEndChargeBillingCharge(vo);
     }
+
+    private int resolveCompensationFlatPeriodIndex(Long orderId) {
+        List<TVinAccountingStrategyDetailOrder> snapshots = Optional.ofNullable(vinAccountingStrategyDetailOrderService
+                        .lambdaQuery().eq(TVinAccountingStrategyDetailOrder::getChargingOrderId, orderId).list())
+                .orElse(Collections.emptyList()).stream()
+                .filter(item -> item.getType() != null)
+                .sorted(Comparator.comparing(TVinAccountingStrategyDetailOrder::getStartTime,
+                        Comparator.nullsLast(String::compareTo)))
+                .collect(Collectors.toList());
+        for (int i = 0; i < snapshots.size(); i++) {
+            if (Integer.valueOf(3).equals(snapshots.get(i).getType())) {
+                return i + 1;
+            }
+        }
+        log.error("[VD补偿] 订单计费策略无平段,回退构造 flat_charge1 orderId={}", orderId);
+        return 1;
+    }
 }
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
index b35e6e9..381afd3 100644
--- 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
@@ -4,15 +4,17 @@
 
 import java.lang.reflect.Field;
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
 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
@@ -26,7 +28,7 @@
     private static final int MAX_GROUPS = 12;
 
     static {
-        Map<Integer, String> prefix = new HashMap<>(8);
+        Map<Integer, String> prefix = new LinkedHashMap<>(8);
         prefix.put(1, "spike");
         prefix.put(2, "peak");
         prefix.put(3, "flat");
@@ -45,33 +47,99 @@
      * @return 阶段类型 -> 电量合计(无有效数据时返回空Map)
      */
     public static Map<Integer, BigDecimal> parseStageElectricity(TransactionRecordMessageVO vo) {
-        Map<Integer, BigDecimal> result = new HashMap<>(8);
+        Map<Integer, BigDecimal> result = new LinkedHashMap<>(8);
+        for (PeriodElectricity period : parsePeriodElectricity(vo)) {
+            result.merge(period.getType(), period.getElectricity(), BigDecimal::add);
+        }
+        return result;
+    }
+
+    /**
+     * 按协议计费时段序号解析电量。chargeN 中的 N 对应平台计费模型按开始时间排序后的第 N 段,
+     * 不能只按阶段类型汇总,否则同一种类型在一天内出现多次时会匹配到错误价格。
+     *
+     * @param vo 设备交易记录
+     * @return 按时段序号升序排列的有效电量
+     */
+    public static List<PeriodElectricity> parsePeriodElectricity(TransactionRecordMessageVO vo) {
+        List<PeriodElectricity> result = new ArrayList<>();
         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);
+        for (int periodIndex = 1; periodIndex <= MAX_GROUPS; periodIndex++) {
+            for (Integer type : TYPE_PREFIX.keySet()) {
+                BigDecimal value = getPeriodElectricity(vo, type, periodIndex);
+                if (value != null && value.compareTo(BigDecimal.ZERO) > 0) {
+                    result.add(new PeriodElectricity(periodIndex, type, value));
+                }
             }
         }
         return result;
     }
 
     /**
-     * 读取指定前缀分组的电量合计(spike_charge1..spike_charge12)。
+     * 读取指定策略类型在指定计费时段序号上的电量。
+     * CD 订单结算按排序后策略的类型读取同序号 chargeN,VD、SD 必须使用相同口径。
+     *
+     * @param vo 设备交易记录
+     * @param type 阶段类型(1=尖,2=峰,3=平,4=谷)
+     * @param periodIndex 计费时段序号(1..12)
+     * @return 对应电量;字段无值或参数无效时返回 null
      */
-    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);
-            }
+    public static BigDecimal getPeriodElectricity(TransactionRecordMessageVO vo, Integer type, int periodIndex) {
+        String prefix = TYPE_PREFIX.get(type);
+        if (vo == null || prefix == null || periodIndex < 1 || periodIndex > MAX_GROUPS) {
+            return null;
         }
-        return hasValue ? total : null;
+        return readField(vo, prefix + "_charge" + periodIndex);
+    }
+
+    /**
+     * 向指定策略类型和计费时段序号写入电量,供无设备账单的补偿结算构造与策略位置一致的 chargeN。
+     *
+     * @return 写入成功返回 true,参数无效或字段不存在返回 false
+     */
+    public static boolean setPeriodElectricity(TransactionRecordMessageVO vo, Integer type, int periodIndex,
+                                               BigDecimal electricity) {
+        String prefix = TYPE_PREFIX.get(type);
+        if (vo == null || prefix == null || periodIndex < 1 || periodIndex > MAX_GROUPS) {
+            return false;
+        }
+        try {
+            Field field = vo.getClass().getDeclaredField(prefix + "_charge" + periodIndex);
+            field.setAccessible(true);
+            field.set(vo, electricity);
+            return true;
+        } catch (NoSuchFieldException | IllegalAccessException ex) {
+            return false;
+        }
+    }
+
+    /**
+     * 单个协议计费时段的电量。
+     */
+    public static final class PeriodElectricity {
+        private final int periodIndex;
+        private final int type;
+        private final BigDecimal electricity;
+
+        private PeriodElectricity(int periodIndex, int type, BigDecimal electricity) {
+            this.periodIndex = periodIndex;
+            this.type = type;
+            this.electricity = electricity;
+        }
+
+        public int getPeriodIndex() {
+            return periodIndex;
+        }
+
+        public int getType() {
+            return type;
+        }
+
+        public BigDecimal getElectricity() {
+            return electricity;
+        }
     }
 
     private static BigDecimal readField(TransactionRecordMessageVO vo, String fieldName) {
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
index 5b8a92e..d811c52 100644
--- 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
@@ -35,6 +35,7 @@
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -46,8 +47,10 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
@@ -56,7 +59,8 @@
  * SD/VD 定时补偿与结算锁行为测试(文档13.2/13.3):
  * 1) 启动超时关单(状态-1+结束方式0+解锁卡片+复位枪,沿用 t_charging_order 枚举约定);
  * 2) 结算入口并发锁抢占失败快速返回,不触达订单数据;
- * 3) 已结算订单重复账单幂等跳过,不再扣款/解锁。
+ * 3) 已结算订单重复账单幂等跳过,不再扣款/解锁;
+ * 4) 超时补偿按排序后平段的真实序号构造 flat_chargeN。
  */
 class CardVinOrderCompensationTest {
 
@@ -456,4 +460,74 @@
         assertEquals("00:00", saved.get(0).getStartTime());
         assertEquals("06:00", saved.get(0).getEndTime());
     }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void cardCompensationWritesElectricityToActualFlatPeriodIndex() {
+        TCardChargingOrderServiceImpl service = spy(new TCardChargingOrderServiceImpl());
+        TCardAccountingStrategyDetailOrderService detailOrderService = mock(TCardAccountingStrategyDetailOrderService.class);
+        ReflectionTestUtils.setField(service, "cardAccountingStrategyDetailOrderService", detailOrderService);
+
+        TCardAccountingStrategyDetailOrder valley = new TCardAccountingStrategyDetailOrder();
+        valley.setType(4);
+        valley.setStartTime("00:00");
+        TCardAccountingStrategyDetailOrder flat = new TCardAccountingStrategyDetailOrder();
+        flat.setType(3);
+        flat.setStartTime("08:00");
+        LambdaQueryChainWrapper<TCardAccountingStrategyDetailOrder> queryWrapper = mock(LambdaQueryChainWrapper.class);
+        when(detailOrderService.lambdaQuery()).thenReturn(queryWrapper);
+        when(queryWrapper.eq(any(), any())).thenReturn(queryWrapper);
+        when(queryWrapper.list()).thenReturn(Arrays.asList(flat, valley));
+        doReturn(R.ok()).when(service).cardEndChargeBillingCharge(any(TransactionRecordMessageVO.class));
+
+        TCardChargingOrder order = new TCardChargingOrder();
+        order.setId(1L);
+        order.setCode("SD-COMPENSATION-SLOT-2");
+        order.setChargingCapacity(new BigDecimal("17.36"));
+
+        service.compensationSettle(order);
+
+        ArgumentCaptor<TransactionRecordMessageVO> captor = ArgumentCaptor.forClass(TransactionRecordMessageVO.class);
+        verify(service).cardEndChargeBillingCharge(captor.capture());
+        TransactionRecordMessageVO settlement = captor.getValue();
+        assertEquals("SD-COMPENSATION-SLOT-2", settlement.getTransaction_serial_number());
+        assertEquals(null, settlement.getFlat_charge1());
+        assertEquals(0, new BigDecimal("17.36").compareTo(settlement.getFlat_charge2()));
+        assertEquals(0, new BigDecimal("17.36").compareTo(settlement.getTotal_electricity()));
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void vinCompensationWritesElectricityToActualFlatPeriodIndex() {
+        TVinChargingOrderServiceImpl service = spy(new TVinChargingOrderServiceImpl());
+        TVinAccountingStrategyDetailOrderService detailOrderService = mock(TVinAccountingStrategyDetailOrderService.class);
+        ReflectionTestUtils.setField(service, "vinAccountingStrategyDetailOrderService", detailOrderService);
+
+        TVinAccountingStrategyDetailOrder valley = new TVinAccountingStrategyDetailOrder();
+        valley.setType(4);
+        valley.setStartTime("00:00");
+        TVinAccountingStrategyDetailOrder flat = new TVinAccountingStrategyDetailOrder();
+        flat.setType(3);
+        flat.setStartTime("08:00");
+        LambdaQueryChainWrapper<TVinAccountingStrategyDetailOrder> queryWrapper = mock(LambdaQueryChainWrapper.class);
+        when(detailOrderService.lambdaQuery()).thenReturn(queryWrapper);
+        when(queryWrapper.eq(any(), any())).thenReturn(queryWrapper);
+        when(queryWrapper.list()).thenReturn(Arrays.asList(flat, valley));
+        doReturn(R.ok()).when(service).vinEndChargeBillingCharge(any(TransactionRecordMessageVO.class));
+
+        TVinChargingOrder order = new TVinChargingOrder();
+        order.setId(2L);
+        order.setCode("VD-COMPENSATION-SLOT-2");
+        order.setChargingCapacity(new BigDecimal("9.87"));
+
+        service.compensationSettle(order);
+
+        ArgumentCaptor<TransactionRecordMessageVO> captor = ArgumentCaptor.forClass(TransactionRecordMessageVO.class);
+        verify(service).vinEndChargeBillingCharge(captor.capture());
+        TransactionRecordMessageVO settlement = captor.getValue();
+        assertEquals("VD-COMPENSATION-SLOT-2", settlement.getTransaction_serial_number());
+        assertEquals(null, settlement.getFlat_charge1());
+        assertEquals(0, new BigDecimal("9.87").compareTo(settlement.getFlat_charge2()));
+        assertEquals(0, new BigDecimal("9.87").compareTo(settlement.getTotal_electricity()));
+    }
 }
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinSettlementTimeAlignmentTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinSettlementTimeAlignmentTest.java
index 3a482d2..5299faf 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinSettlementTimeAlignmentTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinSettlementTimeAlignmentTest.java
@@ -55,7 +55,7 @@
 /**
  * VIN/刷卡充电结算明细开始/结束时间口径对齐测试(与 CD 充电 TaskUtil.endOrder 一致):
  * 1) 循环内:start_time/end_time 截取快照时段位 HH:mm;
- * 2) 循环外:details 按 type 升序排序,首段 start_time 用订单实际开始时间 HH:mm 覆盖,
+ * 2) 循环外:details 按策略开始时间顺序生成,首段 start_time 用订单实际开始时间 HH:mm 覆盖,
  *    末段 end_time 用订单实际结束时间 HH:mm 覆盖;
  * 3) 订单实际时间缺失时保持快照时段位兜底。
  *
@@ -145,6 +145,50 @@
         valley.setCostServiceCharge(new BigDecimal("0.4000"));
 
         return Arrays.asList(spike, flat, valley);
+    }
+
+    private List<TVinAccountingStrategyDetailOrder> vinSixPeriodSnapshots() {
+        return Arrays.asList(
+                vinSnapshot(948524, 4, "00:00", "08:00"),
+                vinSnapshot(948525, 3, "08:00", "10:00"),
+                vinSnapshot(948526, 2, "10:00", "12:00"),
+                vinSnapshot(948527, 3, "12:00", "17:00"),
+                vinSnapshot(948528, 2, "17:00", "22:00"),
+                vinSnapshot(948529, 4, "22:00", "00:00"));
+    }
+
+    private TVinAccountingStrategyDetailOrder vinSnapshot(int id, int type, String start, String end) {
+        TVinAccountingStrategyDetailOrder snapshot = new TVinAccountingStrategyDetailOrder();
+        snapshot.setId(id);
+        snapshot.setType(type);
+        snapshot.setStartTime(start);
+        snapshot.setEndTime(end);
+        snapshot.setElectrovalence(new BigDecimal("1.0000"));
+        snapshot.setServiceCharge(new BigDecimal("1.0000"));
+        snapshot.setCostServiceCharge(new BigDecimal("1.0000"));
+        return snapshot;
+    }
+
+    private List<TCardAccountingStrategyDetailOrder> cardSixPeriodSnapshots() {
+        return Arrays.asList(
+                cardSnapshot(31, 4, "00:00", "08:00"),
+                cardSnapshot(32, 3, "08:00", "10:00"),
+                cardSnapshot(33, 2, "10:00", "12:00"),
+                cardSnapshot(34, 3, "12:00", "17:00"),
+                cardSnapshot(35, 2, "17:00", "22:00"),
+                cardSnapshot(36, 4, "22:00", "00:00"));
+    }
+
+    private TCardAccountingStrategyDetailOrder cardSnapshot(int id, int type, String start, String end) {
+        TCardAccountingStrategyDetailOrder snapshot = new TCardAccountingStrategyDetailOrder();
+        snapshot.setId(id);
+        snapshot.setType(type);
+        snapshot.setStartTime(start);
+        snapshot.setEndTime(end);
+        snapshot.setElectrovalence(new BigDecimal("1.0000"));
+        snapshot.setServiceCharge(new BigDecimal("1.0000"));
+        snapshot.setCostServiceCharge(new BigDecimal("1.0000"));
+        return snapshot;
     }
 
     private void stubVinStrategyQuerySave(TVinAccountingStrategyDetailOrderService detailOrderService,
@@ -252,8 +296,8 @@
         TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
         vo.setTransaction_serial_number("VD1");
         vo.setSpike_charge1(new BigDecimal("3"));
-        vo.setFlat_charge1(new BigDecimal("5"));
-        vo.setValley_charge1(new BigDecimal("8"));
+        vo.setFlat_charge2(new BigDecimal("5"));
+        vo.setValley_charge3(new BigDecimal("8"));
 
         R result = service.vinEndChargeBillingCharge(vo);
         assertEquals(R.SUCCESS, result.getCode());
@@ -318,8 +362,8 @@
         TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
         vo.setTransaction_serial_number("VD1");
         vo.setSpike_charge1(new BigDecimal("3"));
-        vo.setFlat_charge1(new BigDecimal("5"));
-        vo.setValley_charge1(new BigDecimal("8"));
+        vo.setFlat_charge2(new BigDecimal("5"));
+        vo.setValley_charge3(new BigDecimal("8"));
 
         R result = service.vinEndChargeBillingCharge(vo);
         assertEquals(R.SUCCESS, result.getCode());
@@ -375,8 +419,8 @@
         TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
         vo.setTransaction_serial_number("SD1");
         vo.setSpike_charge1(new BigDecimal("3"));
-        vo.setFlat_charge1(new BigDecimal("5"));
-        vo.setValley_charge1(new BigDecimal("8"));
+        vo.setFlat_charge2(new BigDecimal("5"));
+        vo.setValley_charge3(new BigDecimal("8"));
 
         R result = service.cardEndChargeBillingCharge(vo);
         assertEquals(R.SUCCESS, result.getCode());
@@ -445,8 +489,8 @@
         TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
         vo.setTransaction_serial_number("SD1");
         vo.setSpike_charge1(new BigDecimal("3"));
-        vo.setFlat_charge1(new BigDecimal("5"));
-        vo.setValley_charge1(new BigDecimal("8"));
+        vo.setFlat_charge2(new BigDecimal("5"));
+        vo.setValley_charge3(new BigDecimal("8"));
 
         R result = service.cardEndChargeBillingCharge(vo);
         assertEquals(R.SUCCESS, result.getCode());
@@ -521,4 +565,224 @@
         assertEquals("00:00", saved.get(0).getStartTime());
         assertEquals("06:00", saved.get(0).getEndTime());
     }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void vinFlatCharge4UsesSnapshot948527ForReportedOrder() throws InterruptedException {
+        TVinChargingOrderServiceImpl service = new TVinChargingOrderServiceImpl();
+        TVinChargingOrderMapper mapper = mock(TVinChargingOrderMapper.class);
+        ChargingGunClient gunClient = mock(ChargingGunClient.class);
+        TVinAccountingStrategyDetailOrderService detailOrderService = mock(TVinAccountingStrategyDetailOrderService.class);
+        TVinChargingOrderAccountingStrategyService strategyService = mock(TVinChargingOrderAccountingStrategyService.class);
+        TransactionTemplate txTemplate = mock(TransactionTemplate.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "chargingGunClient", gunClient);
+        ReflectionTestUtils.setField(service, "vinAccountingStrategyDetailOrderService", detailOrderService);
+        ReflectionTestUtils.setField(service, "vinChargingOrderAccountingStrategyService", strategyService);
+        ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate);
+        ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true)));
+
+        TVinChargingOrder charging = new TVinChargingOrder();
+        charging.setId(2099403373421596674L);
+        charging.setCode("VD10220260914154223913");
+        charging.setStatus(4);
+        charging.setChargingGunId(7);
+        charging.setStartTime(LocalDateTime.of(2026, 9, 14, 15, 41, 49));
+        when(mapper.selectOne(any())).thenReturn(charging);
+        when(mapper.updateById(any())).thenReturn(1);
+
+        stubGunAndUpdate(gunClient, 7);
+        List<TVinAccountingStrategyDetailOrder> snapshots = new ArrayList<>(vinSixPeriodSnapshots());
+        snapshots.get(1).setElectrovalence(new BigDecimal("0.1000"));
+        snapshots.get(3).setElectrovalence(new BigDecimal("1.0000"));
+        Collections.reverse(snapshots);
+        stubVinStrategyQuerySave(detailOrderService, strategyService, snapshots);
+        stubTxExecute(txTemplate);
+
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setTransaction_serial_number("VD10220260914154223913");
+        vo.setFlat_charge4(new BigDecimal("0.167"));
+        vo.setTotal_electricity(new BigDecimal("0.167"));
+        vo.setStart_time("2026-09-14 15:41:49.00");
+        vo.setEnd_time("2026-09-14 15:43:45.00");
+
+        R result = service.vinEndChargeBillingCharge(vo);
+        assertEquals(R.SUCCESS, result.getCode());
+
+        ArgumentCaptor<Collection<TVinChargingOrderAccountingStrategy>> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(strategyService).saveBatch(captor.capture());
+        List<TVinChargingOrderAccountingStrategy> saved = new ArrayList<>(captor.getValue());
+        assertEquals(1, saved.size());
+        assertEquals(Integer.valueOf(948527), saved.get(0).getAccountingStrategyDetailId());
+        assertEquals(Integer.valueOf(3), saved.get(0).getType());
+        assertEquals("15:41", saved.get(0).getStartTime());
+        assertEquals("15:43", saved.get(0).getEndTime());
+        assertEquals(0, new BigDecimal("0.167").compareTo(saved.get(0).getChargingCapacity()));
+        assertEquals(0, new BigDecimal("1.0000").compareTo(saved.get(0).getElectrovalence()));
+        assertEquals(0, new BigDecimal("0.17").compareTo(saved.get(0).getPeriodElectricPrice()));
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void cardFlatCharge4UsesFourthSnapshotInsteadOfFirstFlatSnapshot() throws InterruptedException {
+        TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
+        TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
+        ChargingCardClient cardClient = mock(ChargingCardClient.class);
+        ChargingGunClient gunClient = mock(ChargingGunClient.class);
+        TCardAccountingStrategyDetailOrderService detailOrderService = mock(TCardAccountingStrategyDetailOrderService.class);
+        TCardChargingOrderAccountingStrategyService strategyService = mock(TCardChargingOrderAccountingStrategyService.class);
+        TransactionTemplate txTemplate = mock(TransactionTemplate.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
+        ReflectionTestUtils.setField(service, "chargingGunClient", gunClient);
+        ReflectionTestUtils.setField(service, "cardAccountingStrategyDetailOrderService", detailOrderService);
+        ReflectionTestUtils.setField(service, "cardChargingOrderAccountingStrategyService", strategyService);
+        ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate);
+        ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true)));
+
+        TCardChargingOrder charging = new TCardChargingOrder();
+        charging.setId(1L);
+        charging.setCode("SD-SLOT-4");
+        charging.setStatus(4);
+        charging.setCardId(5L);
+        charging.setChargingGunId(9);
+        charging.setChargeAmount(new BigDecimal("100.00"));
+        charging.setStartTime(LocalDateTime.of(2026, 9, 14, 15, 41, 49));
+        when(mapper.selectOne(any())).thenReturn(charging);
+        when(mapper.updateById(any())).thenReturn(1);
+
+        stubGunAndUpdate(gunClient, 9);
+        stubCardDeductAndUnlock(cardClient);
+        List<TCardAccountingStrategyDetailOrder> snapshots = new ArrayList<>(cardSixPeriodSnapshots());
+        snapshots.get(1).setElectrovalence(new BigDecimal("0.1000"));
+        snapshots.get(3).setElectrovalence(new BigDecimal("1.2000"));
+        Collections.reverse(snapshots);
+        stubCardStrategyQuerySave(detailOrderService, strategyService, snapshots);
+        stubTxExecute(txTemplate);
+
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setTransaction_serial_number("SD-SLOT-4");
+        vo.setFlat_charge4(new BigDecimal("0.167"));
+        vo.setTotal_electricity(new BigDecimal("0.167"));
+        vo.setStart_time("2026-09-14 15:41:49.00");
+        vo.setEnd_time("2026-09-14 15:43:45.00");
+
+        R result = service.cardEndChargeBillingCharge(vo);
+        assertEquals(R.SUCCESS, result.getCode());
+
+        ArgumentCaptor<Collection<TCardChargingOrderAccountingStrategy>> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(strategyService).saveBatch(captor.capture());
+        List<TCardChargingOrderAccountingStrategy> saved = new ArrayList<>(captor.getValue());
+        assertEquals(1, saved.size());
+        assertEquals(Integer.valueOf(34), saved.get(0).getAccountingStrategyDetailId());
+        assertEquals(Integer.valueOf(3), saved.get(0).getType());
+        assertEquals("15:41", saved.get(0).getStartTime());
+        assertEquals("15:43", saved.get(0).getEndTime());
+        assertEquals(0, new BigDecimal("0.167").compareTo(saved.get(0).getChargingCapacity()));
+        assertEquals(0, new BigDecimal("1.2000").compareTo(saved.get(0).getElectrovalence()));
+        assertEquals(0, new BigDecimal("0.20").compareTo(saved.get(0).getPeriodElectricPrice()));
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void vinPeakCharge5UsesFifthSnapshotAndDeviceEndTime() throws InterruptedException {
+        TVinChargingOrderServiceImpl service = new TVinChargingOrderServiceImpl();
+        TVinChargingOrderMapper mapper = mock(TVinChargingOrderMapper.class);
+        ChargingGunClient gunClient = mock(ChargingGunClient.class);
+        TVinAccountingStrategyDetailOrderService detailOrderService = mock(TVinAccountingStrategyDetailOrderService.class);
+        TVinChargingOrderAccountingStrategyService strategyService = mock(TVinChargingOrderAccountingStrategyService.class);
+        TransactionTemplate txTemplate = mock(TransactionTemplate.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "chargingGunClient", gunClient);
+        ReflectionTestUtils.setField(service, "vinAccountingStrategyDetailOrderService", detailOrderService);
+        ReflectionTestUtils.setField(service, "vinChargingOrderAccountingStrategyService", strategyService);
+        ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate);
+        ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true)));
+
+        TVinChargingOrder charging = new TVinChargingOrder();
+        charging.setId(2L);
+        charging.setCode("VD-SLOT-5");
+        charging.setStatus(4);
+        charging.setChargingGunId(7);
+        charging.setStartTime(LocalDateTime.of(2026, 9, 11, 19, 46, 15));
+        when(mapper.selectOne(any())).thenReturn(charging);
+        when(mapper.updateById(any())).thenReturn(1);
+
+        stubGunAndUpdate(gunClient, 7);
+        stubVinStrategyQuerySave(detailOrderService, strategyService, vinSixPeriodSnapshots());
+        stubTxExecute(txTemplate);
+
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setTransaction_serial_number("VD-SLOT-5");
+        vo.setPeak_charge5(new BigDecimal("0.182"));
+        vo.setStart_time("2026-09-11 19:46:15.00");
+        vo.setEnd_time("2026-09-11 19:53:44.00");
+
+        R result = service.vinEndChargeBillingCharge(vo);
+        assertEquals(R.SUCCESS, result.getCode());
+
+        ArgumentCaptor<Collection<TVinChargingOrderAccountingStrategy>> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(strategyService).saveBatch(captor.capture());
+        List<TVinChargingOrderAccountingStrategy> saved = new ArrayList<>(captor.getValue());
+        assertEquals(1, saved.size());
+        assertEquals(Integer.valueOf(948528), saved.get(0).getAccountingStrategyDetailId());
+        assertEquals(Integer.valueOf(2), saved.get(0).getType());
+        assertEquals("19:46", saved.get(0).getStartTime());
+        assertEquals("19:53", saved.get(0).getEndTime());
+        assertEquals(0, new BigDecimal("0.182").compareTo(saved.get(0).getChargingCapacity()));
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void cardPeakCharge5UsesFifthSnapshotAndDeviceEndTime() throws InterruptedException {
+        TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
+        TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
+        ChargingCardClient cardClient = mock(ChargingCardClient.class);
+        ChargingGunClient gunClient = mock(ChargingGunClient.class);
+        TCardAccountingStrategyDetailOrderService detailOrderService = mock(TCardAccountingStrategyDetailOrderService.class);
+        TCardChargingOrderAccountingStrategyService strategyService = mock(TCardChargingOrderAccountingStrategyService.class);
+        TransactionTemplate txTemplate = mock(TransactionTemplate.class);
+        ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
+        ReflectionTestUtils.setField(service, "chargingGunClient", gunClient);
+        ReflectionTestUtils.setField(service, "cardAccountingStrategyDetailOrderService", detailOrderService);
+        ReflectionTestUtils.setField(service, "cardChargingOrderAccountingStrategyService", strategyService);
+        ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate);
+        ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true)));
+
+        TCardChargingOrder charging = new TCardChargingOrder();
+        charging.setId(1L);
+        charging.setCode("SD-SLOT-5");
+        charging.setStatus(4);
+        charging.setCardId(5L);
+        charging.setChargingGunId(9);
+        charging.setChargeAmount(new BigDecimal("100.00"));
+        charging.setStartTime(LocalDateTime.of(2026, 9, 11, 20, 16, 48));
+        when(mapper.selectOne(any())).thenReturn(charging);
+        when(mapper.updateById(any())).thenReturn(1);
+
+        stubGunAndUpdate(gunClient, 9);
+        stubCardDeductAndUnlock(cardClient);
+        stubCardStrategyQuerySave(detailOrderService, strategyService, cardSixPeriodSnapshots());
+        stubTxExecute(txTemplate);
+
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setTransaction_serial_number("SD-SLOT-5");
+        vo.setPeak_charge5(new BigDecimal("0.079"));
+        vo.setStart_time("2026-09-11 20:16:48.00");
+        vo.setEnd_time("2026-09-11 20:20:54.00");
+
+        R result = service.cardEndChargeBillingCharge(vo);
+        assertEquals(R.SUCCESS, result.getCode());
+
+        ArgumentCaptor<Collection<TCardChargingOrderAccountingStrategy>> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(strategyService).saveBatch(captor.capture());
+        List<TCardChargingOrderAccountingStrategy> saved = new ArrayList<>(captor.getValue());
+        assertEquals(1, saved.size());
+        assertEquals(Integer.valueOf(35), saved.get(0).getAccountingStrategyDetailId());
+        assertEquals(Integer.valueOf(2), saved.get(0).getType());
+        assertEquals("20:16", saved.get(0).getStartTime());
+        assertEquals("20:20", saved.get(0).getEndTime());
+        assertEquals(0, new BigDecimal("0.079").compareTo(saved.get(0).getChargingCapacity()));
+    }
 }
\ No newline at end of file
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
index ac38126..a244604 100644
--- 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
@@ -4,6 +4,7 @@
 import org.junit.jupiter.api.Test;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -56,8 +57,48 @@
     }
 
     @Test
+    void preservesProtocolPeriodIndexForRepeatedStageType() {
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+        vo.setPeak_charge3(new BigDecimal("1.25"));
+        vo.setPeak_charge5(new BigDecimal("2.75"));
+
+        List<TransactionStageParser.PeriodElectricity> periods =
+                TransactionStageParser.parsePeriodElectricity(vo);
+
+        assertEquals(2, periods.size());
+        assertEquals(3, periods.get(0).getPeriodIndex());
+        assertEquals(2, periods.get(0).getType());
+        assertEquals(0, new BigDecimal("1.25").compareTo(periods.get(0).getElectricity()));
+        assertEquals(5, periods.get(1).getPeriodIndex());
+        assertEquals(2, periods.get(1).getType());
+        assertEquals(0, new BigDecimal("2.75").compareTo(periods.get(1).getElectricity()));
+    }
+
+    @Test
+    void readsAndWritesElectricityByExactTypeAndPeriodIndex() {
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+
+        assertTrue(TransactionStageParser.setPeriodElectricity(vo, 3, 4, new BigDecimal("0.167")));
+        assertEquals(0, new BigDecimal("0.167").compareTo(
+                TransactionStageParser.getPeriodElectricity(vo, 3, 4)));
+        assertEquals(null, TransactionStageParser.getPeriodElectricity(vo, 3, 1));
+        assertEquals(null, TransactionStageParser.getPeriodElectricity(vo, 2, 4));
+    }
+
+    @Test
+    void rejectsInvalidTypeOrPeriodIndex() {
+        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
+
+        assertTrue(!TransactionStageParser.setPeriodElectricity(vo, 3, 0, BigDecimal.ONE));
+        assertTrue(!TransactionStageParser.setPeriodElectricity(vo, 5, 1, BigDecimal.ONE));
+        assertEquals(null, TransactionStageParser.getPeriodElectricity(vo, 3, 13));
+    }
+
+    @Test
     void returnsEmptyForNullVo() {
         assertTrue(TransactionStageParser.parseStageElectricity(null).isEmpty());
+        assertTrue(TransactionStageParser.parsePeriodElectricity(null).isEmpty());
+        assertEquals(null, TransactionStageParser.getPeriodElectricity(null, 3, 1));
     }
 
     @Test

--
Gitblit v1.7.1