mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-08-13 17:53:45 +08:00
核心功能: - 配置开关:11个配置项,支持灰度发布 - 定时任务:Quartz调度,可配置执行间隔 - 幂等保护:Redis分布式锁 + 数据库乐观锁 - 数据校验:充电桩在线检测、数据新鲜度、金额异常告警 - 结算逻辑:实时数据转换为结算数据,自动处理退款 - 单元测试:6个测试用例,覆盖主要场景 新增文件: - AutoSettleConfig.java - 配置属性类 - SettlementDataConverter.java - 数据转换工具 - AutoSettleOrdersWithoutTransactionTest.java - 单元测试 修改文件: - OrderBasicInfoService.java - 添加自动结算接口 - OrderBasicInfoServiceImpl.java - 实现自动结算逻辑 - OrderBasicInfoMapper.java - 添加查询和乐观锁更新方法 - OrderBasicInfoMapper.xml - 添加SQL实现 - JsowellTask.java - 添加定时任务方法 - application.yml - 添加配置项示例 详细设计:见 docs/feature-tracker-无交易记录自动结算.md
600 lines
25 KiB
Markdown
600 lines
25 KiB
Markdown
# 功能开发追踪:无交易记录时按实时检测数据自动结算
|
||
|
||
**功能编号**: TBD
|
||
**版本**: v0.4
|
||
**日期**: 2026-08-11
|
||
**项目**: 万车充运营管理平台
|
||
**状态**: ✅ 方案已确认,待开发
|
||
**负责模块**: jsowell-pile / jsowell-netty / jsowell-quartz
|
||
|
||
---
|
||
|
||
## 变更记录
|
||
|
||
| 版本 | 日期 | 变更内容 |
|
||
|------|------|---------|
|
||
| v0.1 | 2026-08-11 | 初稿:需求分析、风险点、实现方案、开发进度追踪框架 |
|
||
| v0.2 | 2026-08-11 | 确认决策:金额以桩端 chargingAmount 为准;双依据判定都启用;特定站点灰度 |
|
||
| v0.3 | 2026-08-11 | 新增8个待明确问题(第六章);扩充测试计划至29个场景(第七章);新增附录 |
|
||
| v0.4 | 2026-08-11 | ✅ 完成8个问题的决策回答;更新已决策问题列表(11个决策);状态更新为"方案已确认,待开发" |
|
||
|
||
---
|
||
|
||
## 一、背景与目标
|
||
|
||
### 背景
|
||
|
||
当前自动结算**唯一入口**是收到交易记录帧。充电桩在线、充电已停止,但**若未收到交易记录**(桩端漏发、网络丢帧、交易记录帧异常等),订单将永远停留在「待结算」状态,无法自动结算,导致:
|
||
- 用户预付资金长期冻结,体验差
|
||
- 后台需要人工介入结算,运维成本高
|
||
|
||
### 目标
|
||
|
||
充电桩**在线**且**停止充电超过 10 分钟**仍未收到交易记录时,平台**按最后一条实时检测记录中的耗电量(充电度数)自动结算**,替代人工结算,缩短资金冻结周期。
|
||
|
||
### 非目标(本期不做)
|
||
|
||
- 桩离线场景的自动结算(离线时数据不可信,需人工确认)
|
||
- 尖峰平谷分时明细的补全(实时数据本身不含分时信息)
|
||
|
||
---
|
||
|
||
## 二、需求分析
|
||
|
||
### 2.1 当前逻辑(现状)
|
||
|
||
| 环节 | 现状 | 代码位置 |
|
||
|------|------|---------|
|
||
| 结算触发 | 仅收到交易记录帧时触发 `settleOrder` | `TransactionRecordsRequestHandler.processOrder()` → jsowell-netty/.../TransactionRecordsRequestHandler.java:683 |
|
||
| 结算数据来源 | 交易记录 `TransactionRecordsData`(含尖峰平谷分时电量、单价、金额) | 同上 |
|
||
| 无交易记录时 | 订单停在「待结算」,不自动结算 | — |
|
||
| 桩离线 | 订单置为异常 `updateOrderStatusAsAbnormal` | YKCBusinessServiceImpl.java:166 |
|
||
| 人工结算 | 已支持「无交易记录 → 用最后一条实时数据构造结算数据」 | `OrderService.manualSettlementOrder()` → jsowell-admin/.../OrderService.java:1008-1027 |
|
||
|
||
### 2.2 目标逻辑
|
||
|
||
```
|
||
定时任务(周期可配,默认如每 10 分钟)扫描待结算订单:
|
||
├─ 条件 1:订单状态 = 待结算 (STAY_SETTLEMENT)
|
||
├─ 条件 2:充电桩在线(connector status ≠ "0" 离线)
|
||
├─ 条件 3:充电已停止超过 10 分钟
|
||
│ ├─ 优先依据:chargeEndTime 非空 且 距当前 > 10 分钟(桩上报过 0x19 充电结束)
|
||
│ └─ 兜底依据:最后一条实时检测数据 dateTime 距当前 > 10 分钟(桩未上报结束/交易记录)
|
||
└─ 条件 4:存在实时检测数据(chargingDegree > 0)
|
||
|
||
命中后:
|
||
1. 复用人工结算逻辑,从最后一条实时检测数据构造 TransactionRecordsData
|
||
2. 走 orderLogic.settleOrder(data, orderBasicInfo)
|
||
```
|
||
|
||
### 2.3 关键数据来源
|
||
|
||
| 数据 | 字段 | 说明 |
|
||
|------|------|------|
|
||
| 累计充电度数 | `RealTimeMonitorData.chargingDegree` | 精确到 4 位小数,待机置零 |
|
||
| 已充金额 | `RealTimeMonitorData.chargingAmount` | 桩端计算(电费+服务费)*计损度数 |
|
||
| 实时数据落库 | `order_monitor_data` 表 / redis `PILE_REAL_TIME_MONITOR_DATA` | 充电中每 10s 上报一次,按分钟保留最后一条 |
|
||
| 桩在线状态 | `PileConnectorDataBaseStatusEnum` | `"0"`=离线,`1/2/3/4`=在线 |
|
||
| 充电结束信号 | `ChargeEndHandler` (0x19) | 更新 `chargeEndTime`、`endSoc` |
|
||
|
||
---
|
||
|
||
## 三、风险点与应对
|
||
|
||
| # | 风险 | 等级 | 说明 | 应对 |
|
||
|---|------|------|------|------|
|
||
| R1 | **金额准确性依赖桩端上报** | 高 | 实时数据无尖峰平谷分时明细,结算走「以交易记录金额为准」分支,即完全信任桩端 `chargingAmount`。若桩端该值有误(充电中途拔枪、数据异常),直接导致扣费/退款不准确 | **已决策:以桩端 `chargingAmount` 为准**(与人工结算一致)。降低影响:① 异常金额阈值(chargingAmount 远超 payAmount)时跳过并告警;② 特定站点灰度上线,逐步观察 |
|
||
| R2 | **退款联动** | 中 | `chargingAmount` < 预付款 `payAmount` 时走退款流程,金额不准会连带退款不准 | 结算金额以 `chargingAmount` 为准,与人工结算一致;金额异常(远超 payAmount)时跳过并告警 |
|
||
| R3 | **幂等 / 并发** | 中 | 定时扫描与「交易记录恰好到达」可能并发重复结算 | 复用现有 `settle_order_`+transactionCode redis 锁(TransactionRecordsRequestHandler.java:602);结算前校验订单状态与结算时间 |
|
||
| R4 | **判「停止」时间点模糊** | 中 | 桩只拔枪不上报 0x19 时,`chargeEndTime` 为空,只能依赖实时数据 `dateTime` 判定;若桩同时离线则数据停更 | **已决策:双依据都启用**(chargeEndTime 优先,最后实时数据 dateTime 兜底)。条件 2(在线)为安全底线,离线订单不结算 |
|
||
| R5 | **影响面扩散** | 中 | 该逻辑影响所有待结算订单,改动结算主链路风险高 | 不修改结算核心 `settleOrder`,仅新增定时扫描入口,复用人工结算构造逻辑 |
|
||
| R6 | **灰度与回滚** | 中 | 新逻辑上线初期存在未覆盖场景 | **已决策:特定站点灰度**(`auto-settle.grayscale-station-ids`)。开关关闭即回退到现有行为 |
|
||
|
||
---
|
||
|
||
## 四、实现方案
|
||
|
||
### 4.1 总体思路
|
||
|
||
不修改现有结算核心链路,新增**定时扫描任务**,命中条件后复用人工结算的「实时数据 → TransactionRecordsData」构造逻辑,再走既有的 `settleOrder`。
|
||
|
||
### 4.2 实现步骤
|
||
|
||
| # | 步骤 | 说明 | 模块 |
|
||
|---|------|------|------|
|
||
| 1 | 配置开关 | 新增自动结算开关 + 站点灰度白名单:`auto-settle.enabled`、`auto-settle.timeout-minutes=10`、`auto-settle.interval`、`auto-settle.grayscale-station-ids`(特定站点灰度)。默认关闭或仅灰度站点开启 | jsowell-pile / application.yml |
|
||
| 2 | 构造结算数据方法 | 在 `OrderService`(或抽到 pile 的公共 Service)新增「从最后一条实时数据构造 TransactionRecordsData」方法,与人工结算复用同一逻辑 | jsowell-admin / jsowell-pile |
|
||
| 3 | 查询待结算订单 | 新增 mapper 查询:状态=待结算 + 桩在线 + 满足停止条件(chargeEndTime/最后实时数据时间 < 当前-10min)的订单列表 | jsowell-pile |
|
||
| 4 | 定时任务 | 新增 Quartz Job,周期执行扫描+结算,含 redis 分布式锁防并发 | jsowell-quartz |
|
||
| 5 | 结算执行 | 循环命中订单,构造数据 → `orderLogic.settleOrder`,异常单独捕获不影响整体 | jsowell-pile |
|
||
| 6 | 幂等保护 | 结算前校验订单状态未变更、`settle_order_` 锁未占用 | jsowell-pile |
|
||
| 7 | 告警日志 | 结算异常、金额异常(chargingAmount 远大于 payAmount)时输出 ERROR 日志/告警 | 各模块 |
|
||
|
||
### 4.3 涉及文件清单(预估)
|
||
|
||
| 文件 | 变更 |
|
||
|------|------|
|
||
| `jsowell-pile/.../OrderBasicInfoService.java` | 新增查询待结算订单方法 |
|
||
| `jsowell-pile/.../OrderBasicInfoServiceImpl.java` | 实现查询逻辑 |
|
||
| `jsowell-pile/.../OrderBasicInfoMapper.xml` | 新增 SQL |
|
||
| `jsowell-admin/.../OrderService.java` | 抽取「实时数据→结算数据」构造方法(或下沉到 pile) |
|
||
| `jsowell-quartz/.../RyTask.java` 或新增 Task | 新增自动结算定时任务 |
|
||
| `jsowell-admin/src/main/resources/application*.yml` | 新增配置项 |
|
||
|
||
---
|
||
|
||
## 五、开发进度追踪
|
||
|
||
> 状态:`⬜ 未开始` / `🟡 进行中` / `✅ 已完成` / `🔴 阻塞`
|
||
|
||
| # | 任务 | 模块 | 负责人 | 状态 | 备注 |
|
||
|---|------|------|--------|------|------|
|
||
| 1 | 需求评审确认(含 R1/R4 决策) | - | | ✅ 已完成 | 2026-08-11 确认:金额以桩端 chargingAmount 为准;双依据判定;特定站点灰度 |
|
||
| 1.1 | 8个待明确问题决策 | - | | ✅ 已完成 | 2026-08-11 完成全部8个问题的决策回答 |
|
||
| 2 | 配置开关(11个配置项) | jsowell-pile | | ⬜ 未开始 | application.yml 新增 auto-settle 配置块 |
|
||
| 3 | 实时数据→结算数据构造方法 | jsowell-admin/pile | | ⬜ 未开始 | 从Redis获取实时数据,构造 TransactionRecordsData |
|
||
| 4 | 待结算订单查询 SQL | jsowell-pile | | ⬜ 未开始 | 支持Redis查询桩在线状态,降级JOIN查询 |
|
||
| 5 | 定时任务 | jsowell-quartz | | ⬜ 未开始 | Cron表达式:0 */10 * * * ? |
|
||
| 6 | 幂等保护(redis锁+乐观锁) | jsowell-pile | | ⬜ 未开始 | settle_order_{orderId} + UPDATE WHERE条件 |
|
||
| 7 | 数据校验与告警日志 | jsowell-pile | | ⬜ 未开始 | 度数、金额、阈值三层校验 |
|
||
| 8 | 单元测试 / 集成测试 | jsowell-admin | | ⬜ 未开始 | 覆盖29个测试场景(P0优先) |
|
||
| 9 | 灰度上线(特定站点) | - | | ⬜ 未开始 | 先对灰度站点开启,观察稳定后逐步放开 |
|
||
| 10 | 上线后监控与复盘 | - | | ⬜ 未开始 | 监控Prometheus指标,分析自动结算效果 |
|
||
|
||
### 已决策问题
|
||
|
||
| # | 问题 | 决策 | 决策日期 |
|
||
|---|------|------|---------|
|
||
| 1 | 金额策略 | **以桩端 `chargingAmount` 为准**(与人工结算一致) | 2026-08-11 |
|
||
| 2 | 停止判定依据 | **双依据都启用**:chargeEndTime 优先,最后实时数据 dateTime 兜底 | 2026-08-11 |
|
||
| 3 | 灰度范围 | **特定站点灰度**,先对指定站点开启,观察稳定后再逐步放开 | 2026-08-11 |
|
||
| 4 | 并发幂等锁key | 采用方案A:基于orderId构造 `"settle_order_" + orderId` | 2026-08-11 |
|
||
| 5 | 异常金额阈值 | 倍率阈值策略:`amount-threshold-ratio: 1.5` | 2026-08-11 |
|
||
| 6 | 桩在线查询 | 优先Redis缓存(方案C),降级JOIN查询(方案D) | 2026-08-11 |
|
||
| 7 | 最后实时数据 | 直接从Redis取(方案B),失败降级DB查询(方案A) | 2026-08-11 |
|
||
| 8 | 状态校验原子性 | UPDATE带WHERE条件(乐观锁,方案A)+ redis分布式锁双重保障 | 2026-08-11 |
|
||
| 9 | 数据新鲜度 | 增加判断,阈值30分钟 `data-freshness-minutes: 30` | 2026-08-11 |
|
||
| 10 | 异常数据拦截 | 采用三层校验:度数>0、金额>0、金额<阈值 | 2026-08-11 |
|
||
| 11 | 配置项 | 采用完整配置项(11个配置) | 2026-08-11 |
|
||
|
||
---
|
||
|
||
## 六、待明确问题
|
||
|
||
> **状态说明**:`⬜ 待回答` / `✅ 已解决` / `🔴 阻塞中`
|
||
|
||
### 6.1 并发幂等保护的锁key构造(对应风险R3)
|
||
|
||
**问题描述**:
|
||
文档中提到复用 `settle_order_+transactionCode` redis锁,但**无交易记录场景下没有transactionCode**,锁key应如何构造?
|
||
|
||
**影响**:
|
||
- 若锁key不正确,可能导致定时任务与交易记录到达的并发冲突
|
||
- 多个定时任务实例同时扫描时可能重复结算
|
||
|
||
**建议方案**:
|
||
```java
|
||
// 方案A:基于orderId构造锁key
|
||
String lockKey = "settle_order_" + orderId;
|
||
|
||
// 方案B:保持现有逻辑,构造虚拟transactionCode
|
||
String transactionCode = "AUTO_" + orderId + "_" + System.currentTimeMillis();
|
||
String lockKey = "settle_order_" + transactionCode;
|
||
```
|
||
|
||
**回答**:
|
||
```
|
||
状态:✅ 已解决
|
||
|
||
决策:采用方案A,基于orderId构造锁key
|
||
理由:transactionCode在创建订单的时候就已经生成了,但无交易记录场景下不会再生成新的,
|
||
因此统一使用 "settle_order_" + orderId 作为锁key,与交易记录到达时使用同一把锁。
|
||
实现:String lockKey = "settle_order_" + orderId;
|
||
```
|
||
|
||
---
|
||
|
||
### 6.2 异常金额阈值的量化定义(对应风险R1/R2)
|
||
|
||
**问题描述**:
|
||
文档多次提到"chargingAmount 远超 payAmount 时告警跳过",但**"远超"没有具体定义**。
|
||
|
||
**影响**:
|
||
- 无明确阈值会导致实现时判断标准不统一
|
||
- 无法准确识别异常金额并拦截
|
||
|
||
**建议方案**:
|
||
```yaml
|
||
auto-settle:
|
||
amount-threshold-ratio: 1.5 # chargingAmount > payAmount * 1.5 时告警跳过
|
||
# 或者使用绝对差值
|
||
amount-threshold-diff: 100.0 # chargingAmount - payAmount > 100元时告警
|
||
```
|
||
|
||
**示例**:
|
||
- payAmount = 50元,chargingAmount = 76元 → 1.52倍 → 告警跳过
|
||
- payAmount = 50元,chargingAmount = 72元 → 1.44倍 → 正常结算
|
||
|
||
**回答**:
|
||
```
|
||
状态:✅ 已解决
|
||
|
||
决策:采用倍率阈值策略
|
||
配置:amount-threshold-ratio: 1.5
|
||
理由:倍率阈值比绝对差值更通用,适应不同金额区间的订单。
|
||
例如:10元订单和100元订单的异常判断标准应该成比例,而非固定差值。
|
||
实现:if (chargingAmount > payAmount * 1.5) { 告警跳过 }
|
||
```
|
||
|
||
---
|
||
|
||
### 6.3 SQL查询"桩在线"条件的实现效率
|
||
|
||
**问题描述**:
|
||
定时任务需要查询"桩在线"的待结算订单,但`order_basic_info`表可能没有直接存储connector_status字段,需要JOIN `pile_connector_info`表,**大批量查询可能有性能问题**。
|
||
|
||
**影响**:
|
||
- 每次扫描都需要多表JOIN,数据量大时查询慢
|
||
- 定时任务执行时间过长可能影响下一轮扫描
|
||
|
||
**建议方案**:
|
||
|
||
| 方案 | 优点 | 缺点 | 实施难度 |
|
||
|------|------|------|---------|
|
||
| A. 订单表增加冗余字段`connector_status` | 查询快,单表索引 | 需要实时同步状态,数据冗余 | 中 |
|
||
| B. 分批查询(先查订单,再批量查桩状态) | 不改表结构 | 需要两次查询,逻辑复杂 | 低 |
|
||
| C. 从Redis缓存判断(`PILE_REAL_TIME_MONITOR_DATA`) | 最实时,无DB压力 | Redis数据可能缺失 | 低 |
|
||
| D. 直接JOIN查询(当前方案) | 实现简单 | 性能问题 | 最低 |
|
||
|
||
**回答**:
|
||
```
|
||
状态:✅ 已解决
|
||
|
||
决策:优先使用方案C(Redis缓存),失败时降级到方案D(JOIN查询)
|
||
理由:Redis缓存最实时且无DB压力,适合高频扫描场景。
|
||
实现逻辑:
|
||
1. 先从 Redis PILE_REAL_TIME_MONITOR_DATA:{connectorId} 获取数据
|
||
2. 若Redis中有数据,直接判断connector在线状态
|
||
3. 若Redis中无数据或异常,降级到JOIN pile_connector_info表查询
|
||
4. 记录降级日志,便于监控Redis缓存命中率
|
||
```
|
||
|
||
---
|
||
|
||
### 6.4 "最后一条实时数据"的精确定义
|
||
|
||
**问题描述**:
|
||
条件4要求"存在实时检测数据(chargingDegree > 0)",但**如何确定"最后一条"数据**?
|
||
- 按`dateTime`排序?如果同一分钟有多条怎么办?
|
||
- 按主键ID排序?
|
||
- Redis缓存里的是不是一定是最后一条?
|
||
|
||
**影响**:
|
||
- 排序规则不明确可能导致取错数据
|
||
- 影响结算金额准确性
|
||
|
||
**建议方案**:
|
||
```sql
|
||
-- 方案A:按时间+主键双重排序
|
||
SELECT * FROM order_monitor_data
|
||
WHERE order_id = ?
|
||
ORDER BY date_time DESC, id DESC
|
||
LIMIT 1;
|
||
|
||
-- 方案B:直接从Redis取
|
||
String key = "PILE_REAL_TIME_MONITOR_DATA:" + connectorId;
|
||
RealTimeMonitorData data = redis.get(key);
|
||
|
||
-- 方案C:取settle_time前的最后一条
|
||
SELECT * FROM order_monitor_data
|
||
WHERE order_id = ?
|
||
AND date_time <= NOW()
|
||
ORDER BY date_time DESC, id DESC
|
||
LIMIT 1;
|
||
```
|
||
|
||
**回答**:
|
||
```
|
||
状态:✅ 已解决
|
||
|
||
决策:采用方案B(直接从Redis取)
|
||
理由:Redis中的 PILE_REAL_TIME_MONITOR_DATA 缓存已经是最新的实时数据,
|
||
按充电桩上报机制,每次上报都会更新该缓存为最新值。
|
||
实现:
|
||
String key = "PILE_REAL_TIME_MONITOR_DATA:" + connectorId;
|
||
RealTimeMonitorData data = redis.get(key);
|
||
备注:若Redis获取失败,可降级查询DB,使用方案A(按date_time DESC, id DESC排序)
|
||
```
|
||
|
||
---
|
||
|
||
### 6.5 状态校验的原子性保证(对应风险R3)
|
||
|
||
**问题描述**:
|
||
文档中提到"结算前校验订单状态未变更",但**校验→结算之间仍有并发窗口**,可能导致:
|
||
- 线程A校验通过 → 线程B同时校验通过 → 两者同时结算
|
||
|
||
**影响**:
|
||
- 重复结算导致资金异常
|
||
- 数据一致性问题
|
||
|
||
**建议方案**:
|
||
```sql
|
||
-- 方案A:UPDATE语句带WHERE条件(乐观锁)
|
||
UPDATE order_basic_info
|
||
SET order_status = 'COMPLETED',
|
||
settle_time = NOW(),
|
||
...
|
||
WHERE order_id = ?
|
||
AND order_status = 'STAY_SETTLEMENT' -- 确保状态未变
|
||
AND settle_time IS NULL; -- 确保未结算过
|
||
|
||
-- 检查受影响行数
|
||
if (affectedRows == 0) {
|
||
log.warn("订单{}状态已变更,跳过结算", orderId);
|
||
return;
|
||
}
|
||
|
||
-- 方案B:SELECT FOR UPDATE(悲观锁)
|
||
SELECT * FROM order_basic_info
|
||
WHERE order_id = ?
|
||
FOR UPDATE;
|
||
-- 然后再UPDATE
|
||
|
||
-- 方案C:依赖redis分布式锁
|
||
if (!redisLock.tryLock("settle_order_" + orderId)) {
|
||
return;
|
||
}
|
||
try {
|
||
// 结算逻辑
|
||
} finally {
|
||
redisLock.unlock();
|
||
}
|
||
```
|
||
|
||
**回答**:
|
||
```
|
||
状态:✅ 已解决
|
||
|
||
决策:采用方案A(UPDATE语句带WHERE条件,乐观锁)
|
||
理由:乐观锁适合并发冲突较少的场景,性能优于悲观锁,且实现简单。
|
||
结合redis分布式锁(6.1中的方案),形成双重保障。
|
||
实现:
|
||
UPDATE order_basic_info
|
||
SET order_status = 'COMPLETED',
|
||
settle_time = NOW(),
|
||
...
|
||
WHERE order_id = ?
|
||
AND order_status = 'STAY_SETTLEMENT' -- 确保状态未变
|
||
AND settle_time IS NULL; -- 确保未结算过
|
||
|
||
if (affectedRows == 0) {
|
||
log.warn("订单{}状态已变更,跳过结算", orderId);
|
||
return;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6.6 "桩假在线"场景的判断逻辑(对应风险R4补充)
|
||
|
||
**问题描述**:
|
||
真实场景可能出现:
|
||
- 桩TCP连接还在(connector_status ≠ 0)
|
||
- 但实际已停止上报数据(最后实时数据很久没更新)
|
||
|
||
这种"假在线"状态下,实时数据不可信,不应该触发自动结算。
|
||
|
||
**影响**:
|
||
- 可能使用过时数据结算
|
||
- 金额不准确
|
||
|
||
**建议方案**:
|
||
```yaml
|
||
条件2补充为:
|
||
- connector_status ≠ 0(连接在线)
|
||
AND
|
||
- 最后一条实时数据的dateTime距当前 < 30分钟(数据新鲜度)
|
||
```
|
||
|
||
**示例**:
|
||
- 当前时间:2026-08-11 14:00:00
|
||
- 最后实时数据时间:2026-08-11 13:55:00 → 新鲜度5分钟 → 通过
|
||
- 最后实时数据时间:2026-08-11 13:20:00 → 新鲜度40分钟 → 不通过(桩假在线)
|
||
|
||
**回答**:
|
||
```
|
||
状态:✅ 已解决
|
||
|
||
决策:增加数据新鲜度判断,阈值设为30分钟
|
||
配置:data-freshness-minutes: 30
|
||
理由:30分钟是合理的阈值,既能覆盖正常充电停止后的数据延迟,
|
||
又能有效过滤"桩假在线"场景(连接在但不上报数据)。
|
||
实现条件:
|
||
条件2更新为:
|
||
- connector_status ≠ 0(连接在线)
|
||
AND
|
||
- 最后一条实时数据的dateTime距当前 < 30分钟(数据新鲜度)
|
||
示例:NOW() - dateTime < 30分钟
|
||
```
|
||
|
||
---
|
||
|
||
### 6.7 异常数据场景的处理策略
|
||
|
||
**问题描述**:
|
||
实时数据可能出现异常情况:
|
||
- `chargingDegree > 0` 但 `chargingAmount = 0`(金额未计算)
|
||
- `chargingDegree = 0` 但 `chargingAmount > 0`(数据矛盾)
|
||
- `chargingDegree` 为负数(数据异常)
|
||
|
||
**影响**:
|
||
- 异常数据直接结算会导致金额错误
|
||
- 需要明确拦截规则
|
||
|
||
**建议方案**:
|
||
```java
|
||
// 数据校验规则
|
||
if (chargingDegree <= 0) {
|
||
log.warn("订单{}充电度数异常:{}", orderId, chargingDegree);
|
||
return; // 跳过
|
||
}
|
||
|
||
if (chargingAmount <= 0) {
|
||
log.warn("订单{}充电金额异常:{}", orderId, chargingAmount);
|
||
return; // 跳过
|
||
}
|
||
|
||
if (chargingAmount > payAmount * amountThresholdRatio) {
|
||
log.error("订单{}金额异常:实际{}元 > 预付{}元 * {}",
|
||
orderId, chargingAmount, payAmount, amountThresholdRatio);
|
||
// 发送告警
|
||
return; // 跳过
|
||
}
|
||
```
|
||
|
||
**回答**:
|
||
```
|
||
状态:✅ 已解决
|
||
|
||
决策:采用建议的数据校验规则
|
||
实现:
|
||
// 1. 充电度数校验
|
||
if (chargingDegree <= 0) {
|
||
log.warn("订单{}充电度数异常:{}", orderId, chargingDegree);
|
||
return; // 跳过
|
||
}
|
||
|
||
// 2. 充电金额校验
|
||
if (chargingAmount <= 0) {
|
||
log.warn("订单{}充电金额异常:{}", orderId, chargingAmount);
|
||
return; // 跳过
|
||
}
|
||
|
||
// 3. 金额阈值校验(结合6.2的决策)
|
||
if (chargingAmount > payAmount * amountThresholdRatio) {
|
||
log.error("订单{}金额异常:实际{}元 > 预付{}元 * {}",
|
||
orderId, chargingAmount, payAmount, amountThresholdRatio);
|
||
// 发送告警
|
||
return; // 跳过
|
||
}
|
||
|
||
补充场景:暂无其他需要拦截的异常场景,后续如发现新场景再补充。
|
||
```
|
||
|
||
---
|
||
|
||
### 6.8 配置项完整性补充
|
||
|
||
**问题描述**:
|
||
当前配置项不完整,缺少以下关键配置:
|
||
|
||
**建议补充**:
|
||
```yaml
|
||
auto-settle:
|
||
# 现有配置
|
||
enabled: false # 总开关
|
||
timeout-minutes: 10 # 停止超时阈值
|
||
interval: '0 */10 * * * ?' # 扫描周期(Cron表达式)
|
||
grayscale-station-ids: [1001,1002] # 灰度站点白名单
|
||
|
||
# 🆕 建议补充
|
||
amount-threshold-ratio: 1.5 # 金额异常阈值倍数(对应6.2)
|
||
batch-size: 100 # 单次扫描订单数量上限(防止一次查太多)
|
||
data-freshness-minutes: 30 # 实时数据新鲜度阈值(对应6.6)
|
||
alert-enabled: true # 告警开关
|
||
lock-timeout-seconds: 60 # redis锁超时时间
|
||
retry-on-failure: false # 失败是否重试(建议false,等下一轮)
|
||
```
|
||
|
||
**回答**:
|
||
```
|
||
状态:✅ 已解决
|
||
|
||
决策:采用建议的所有配置项
|
||
完整配置:
|
||
auto-settle:
|
||
# 基础配置
|
||
enabled: false # 总开关,默认关闭
|
||
timeout-minutes: 10 # 停止超时阈值
|
||
interval: '0 */10 * * * ?' # 扫描周期(Cron表达式)
|
||
grayscale-station-ids: [1001,1002] # 灰度站点白名单
|
||
|
||
# 新增配置
|
||
amount-threshold-ratio: 1.5 # 金额异常阈值倍数(对应6.2)
|
||
batch-size: 100 # 单次扫描订单数量上限
|
||
data-freshness-minutes: 30 # 实时数据新鲜度阈值(对应6.6)
|
||
alert-enabled: true # 告警开关
|
||
lock-timeout-seconds: 60 # redis锁超时时间
|
||
retry-on-failure: false # 失败是否重试(建议false,等下一轮)
|
||
|
||
备注:所有配置项均已明确,可直接用于开发。
|
||
```
|
||
|
||
---
|
||
|
||
## 七、测试计划
|
||
|
||
### 7.1 基础功能测试
|
||
|
||
| # | 场景 | 预期 | 优先级 |
|
||
|---|------|------|--------|
|
||
| 1 | 桩在线 + 有 chargeEndTime + 停止>10min + 有实时数据 | 自动结算成功,金额=实时数据 chargingAmount | P0 |
|
||
| 2 | 桩在线 + 无 chargeEndTime + 最后实时数据>10min + 有实时数据 | 兜底依据触发,自动结算 | P0 |
|
||
| 3 | 桩离线(connector_status=0) | 不触发(条件 2 拦截) | P0 |
|
||
| 4 | 停止 < 10min | 不触发 | P1 |
|
||
| 5 | 停止 = 10min 0秒(边界值) | 应触发(>= 10min) | P1 |
|
||
| 6 | 无实时检测数据(chargingDegree=0) | 不触发 | P1 |
|
||
| 7 | 配置开关关闭(enabled=false) | 保持现有行为(不自动结算) | P0 |
|
||
|
||
### 7.2 异常数据测试
|
||
|
||
| # | 场景 | 预期 | 优先级 |
|
||
|---|------|------|--------|
|
||
| 8 | chargingAmount > payAmount * threshold(异常值) | 跳过并告警,不结算 | P0 |
|
||
| 9 | chargingDegree > 0 但 chargingAmount = 0 | 跳过并告警(数据异常) | P1 |
|
||
| 10 | chargingDegree = 0 但 chargingAmount > 0 | 跳过并告警(数据矛盾) | P1 |
|
||
| 11 | chargingDegree 为负数 | 跳过并告警(数据异常) | P1 |
|
||
| 12 | chargingAmount < payAmount(正常退款场景) | 正常结算并退款差额 | P0 |
|
||
|
||
### 7.3 并发与幂等测试
|
||
|
||
| # | 场景 | 预期 | 优先级 |
|
||
|---|------|------|--------|
|
||
| 13 | 扫描期间交易记录帧到达(并发) | 仅一方结算成功,另一方被幂等拦截 | P0 |
|
||
| 14 | 多个定时任务实例同时扫描同一订单 | 仅一个实例处理成功(分布式锁生效) | P0 |
|
||
| 15 | 订单在校验后、结算前状态被其他线程修改 | 被原子性校验拦截,不重复结算 | P0 |
|
||
|
||
### 7.4 灰度与配置测试
|
||
|
||
| # | 场景 | 预期 | 优先级 |
|
||
|---|------|------|--------|
|
||
| 16 | 灰度站点内的订单 | 触发自动结算 | P0 |
|
||
| 17 | 非灰度站点的订单 | 不触发(即使满足条件) | P0 |
|
||
| 18 | 混合:部分灰度、部分非灰度站点 | 仅灰度站点订单被处理 | P1 |
|
||
| 19 | batch-size限制(如设为10,实际符合20单) | 仅处理前10单,下次扫描处理剩余 | P1 |
|
||
|
||
### 7.5 边界与容错测试
|
||
|
||
| # | 场景 | 预期 | 优先级 |
|
||
|---|------|------|--------|
|
||
| 20 | 桩"假在线"(连接在线但数据>30分钟未更新) | 不触发(数据新鲜度检查拦截) | P0 |
|
||
| 21 | 数据库查询订单列表为空 | 正常退出,无异常 | P1 |
|
||
| 22 | Redis获取实时数据失败 | 降级查DB,或跳过并记录日志 | P1 |
|
||
| 23 | 结算过程中DB连接超时 | 捕获异常,不影响其他订单处理 | P1 |
|
||
| 24 | 单个订单结算失败(业务异常) | 记录失败日志,继续处理其他订单 | P0 |
|
||
|
||
### 7.6 压力测试
|
||
|
||
| # | 场景 | 预期 | 优先级 |
|
||
|---|------|------|--------|
|
||
| 25 | 1000个待结算订单同时满足条件 | batch-size分批处理,不超时 | P2 |
|
||
| 26 | 定时任务执行时间 > 扫描周期(如>10min) | 下一轮等待当前轮完成,或跳过 | P2 |
|
||
|
||
### 7.7 监控与告警测试
|
||
|
||
| # | 场景 | 预期 | 优先级 |
|
||
|---|------|------|--------|
|
||
| 27 | 金额异常告警触发 | ERROR日志输出,告警系统收到通知 | P1 |
|
||
| 28 | 自动结算成功 | INFO日志记录订单号、金额、耗时 | P1 |
|
||
| 29 | 查看Prometheus指标 | 统计自动结算成功数、失败数、跳过数 | P2 | |