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