Files
jsowell-charger-web/docs/feature-tracker-无交易记录自动结算.md
2026-08-11 14:49:42 +08:00

162 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 功能开发追踪:无交易记录时按实时检测数据自动结算
**功能编号**: TBD
**版本**: v0.2
**日期**: 2026-08-11
**项目**: 万车充运营管理平台
**状态**: 方案已确认,开发中
**负责模块**: jsowell-pile / jsowell-netty / jsowell-quartz
---
## 变更记录
| 版本 | 日期 | 变更内容 |
|------|------|---------|
| v0.1 | 2026-08-11 | 初稿:需求分析、风险点、实现方案、开发进度追踪框架 |
| v0.2 | 2026-08-11 | 确认决策:金额以桩端 chargingAmount 为准;双依据判定都启用;特定站点灰度 |
---
## 一、背景与目标
### 背景
当前自动结算**唯一入口**是收到交易记录帧。充电桩在线、充电已停止,但**若未收到交易记录**(桩端漏发、网络丢帧、交易记录帧异常等),订单将永远停留在「待结算」状态,无法自动结算,导致:
- 用户预付资金长期冻结,体验差
- 后台需要人工介入结算,运维成本高
### 目标
充电桩**在线**且**停止充电超过 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 为准;双依据判定;特定站点灰度 |
| 2 | 配置开关 | jsowell-pile | | ⬜ 未开始 | |
| 3 | 实时数据→结算数据构造方法 | jsowell-admin/pile | | ⬜ 未开始 | |
| 4 | 待结算订单查询 SQL | jsowell-pile | | ⬜ 未开始 | |
| 5 | 定时任务 | jsowell-quartz | | ⬜ 未开始 | |
| 6 | 幂等保护 | jsowell-pile | | ⬜ 未开始 | |
| 7 | 告警日志 | 各模块 | | ⬜ 未开始 | |
| 8 | 单元测试 / 集成测试 | jsowell-admin | | ⬜ 未开始 | |
| 9 | 灰度上线(特定站点) | - | | ⬜ 未开始 | 先对灰度站点开启,观察稳定后逐步放开 |
| 10 | 上线后监控与复盘 | - | | ⬜ 未开始 | |
### 已决策问题
| # | 问题 | 决策 | 决策日期 |
|---|------|------|---------|
| 1 | 金额策略 | **以桩端 `chargingAmount` 为准**(与人工结算一致) | 2026-08-11 |
| 2 | 停止判定依据 | **双依据都启用**chargeEndTime 优先,最后实时数据 dateTime 兜底 | 2026-08-11 |
| 3 | 灰度范围 | **特定站点灰度**,先对指定站点开启,观察稳定后再逐步放开 | 2026-08-11 |
---
## 六、测试计划
| # | 场景 | 预期 |
|---|------|------|
| 1 | 桩在线 + 有 chargeEndTime + 停止>10min + 有实时数据 | 自动结算成功,金额=实时数据 chargingAmount |
| 2 | 桩在线 + 无 chargeEndTime + 最后实时数据>10min + 有实时数据 | 兜底依据触发,自动结算 |
| 3 | 桩离线 | 不触发(条件 2 拦截) |
| 4 | 停止 < 10min | 不触发 |
| 5 | 无实时检测数据chargingDegree=0 | 不触发 |
| 6 | 扫描期间交易记录到达 | 不重复结算(幂等) |
| 7 | chargingAmount > payAmount 异常值 | 跳过并告警 |
| 8 | 配置开关关闭 | 保持现有行为(不自动结算) |