# 无交易记录自动结算功能 - 实施总结 ## 版本信息 - **功能版本**: v1.0 - **开发分支**: feature/auto-settle-no-transaction - **提交哈希**: 121b357a2 - **完成日期**: 2026-08-11 - **开发者**: jsowell --- ## 实施概览 本次开发完成了"无交易记录自动结算"功能的完整实现,包括配置管理、定时任务、核心业务逻辑、幂等保护、数据校验、告警日志和单元测试。 ### 开发进度 | 任务 | 状态 | 完成时间 | |------|------|---------| | ✅ 配置开关(11个配置项) | 已完成 | 2026-08-11 | | ✅ 创建配置属性类 | 已完成 | 2026-08-11 | | ✅ 实时数据→结算数据构造方法 | 已完成 | 2026-08-11 | | ✅ 待结算订单查询 SQL | 已完成 | 2026-08-11 | | ✅ 定时任务实现 | 已完成 | 2026-08-11 | | ✅ 幂等保护(redis锁+乐观锁) | 已完成 | 2026-08-11 | | ✅ 数据校验与告警日志 | 已完成 | 2026-08-11 | | ✅ 单元测试 / 集成测试 | 已完成 | 2026-08-11 | --- ## 核心实现 ### 1. 配置管理 **文件**: `jsowell-pile/src/main/java/com/jsowell/pile/config/AutoSettleConfig.java` ```java @ConfigurationProperties(prefix = "auto-settle") public class AutoSettleConfig { private Boolean enabled = false; // 功能总开关 private Integer timeoutMinutes = 10; // 停止超时阈值(分钟) private String interval = "0 */10 * * * ?"; // 定时任务间隔 private List grayscaleStationIds; // 灰度站点ID列表 private Integer batchSize = 100; // 单次扫描订单数量上限 private Double amountThresholdRatio = 1.5; // 异常金额阈值比例 private Integer dataFreshnessMinutes = 30; // 实时数据新鲜度阈值(分钟) private Boolean alertEnabled = true; // 告警开关 private Long lockTimeout = 300L; // Redis锁超时时间(秒) } ``` **配置示例** (application.yml): ```yaml auto-settle: enabled: false # 默认关闭,生产环境需手动开启 timeout-minutes: 10 interval: "0 */10 * * * ?" grayscale-station-ids: [] # 空数组表示全量 batch-size: 100 amount-threshold-ratio: 1.5 data-freshness-minutes: 30 alert-enabled: true lock-timeout: 300 ``` ### 2. 数据转换工具 **文件**: `jsowell-pile/src/main/java/com/jsowell/pile/util/SettlementDataConverter.java` 核心功能: - 从 `RealTimeMonitorData` 提取充电数据 - 构造 `TransactionRecordsData` 结算数据 - 时间格式转换(Date ↔ String) - 默认值设置(所有电量归为平段) ### 3. 数据库层 **Mapper 接口**: `OrderBasicInfoMapper.java` ```java // 查询待结算订单 List selectPendingAutoSettleOrders( @Param("cutoffTime") LocalDateTime cutoffTime, @Param("stationIds") List stationIds, @Param("limit") int limit); // 乐观锁更新订单 int updateOrderWithOptimisticLock( @Param("order") OrderBasicInfo order, @Param("orderId") Integer orderId, @Param("expectedStatus") String expectedStatus, @Param("expectedSettlementTime") java.util.Date expectedSettlementTime); ``` **SQL 实现**: `OrderBasicInfoMapper.xml` - 查询条件:del_flag='0', order_status='3', pay_status='1', transaction_code IS NULL/空字符串, charge_end_time < 截止时间 - 灰度过滤:支持按站点ID列表过滤 - 分页限制:LIMIT 限制单次查询数量 - 乐观锁:WHERE order_status='3' AND settlement_time IS NULL ### 4. 业务逻辑 **核心方法**: `OrderBasicInfoServiceImpl.autoSettleOrdersWithoutTransactionRecord()` **执行流程**: 1. **功能开关检查** - 如果未启用直接返回 2. **计算截止时间** - 当前时间 - 超时阈值 3. **查询待结算订单** - 支持灰度站点过滤 4. **逐个处理订单**: - 获取 Redis 分布式锁(key: `settle_order_{orderId}`) - 重新查询订单最新状态(防止并发) - 检查灰度范围 - 获取充电桩连接器状态(Redis优先) - 检查充电桩在线状态 - 获取最后一条实时数据 - 检查数据新鲜度(< 30分钟) - 数据校验:电量和金额格式、非空、非零 - 构造结算数据 - 金额异常检测(chargingAmount > payAmount × 1.5倍) - 调用结算方法(`returnUpdateOrderBasicInfo` + `returnUpdateOrderDetail`) - 乐观锁更新订单状态 - 处理退款(如果结算金额 < 已支付金额) - 释放 Redis 锁 **统计信息**: - successCount - 成功结算数量 - skipCount - 跳过数量 - failCount - 失败数量 ### 5. 定时任务 **文件**: `jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java` ```java public void autoSettleOrdersWithoutTransactionRecord() { if (skipInPre("无交易记录自动结算")) { return; // 预发布环境跳过 } log.info("【无交易记录自动结算】定时任务开始执行"); try { orderBasicInfoService.autoSettleOrdersWithoutTransactionRecord(); log.info("【无交易记录自动结算】定时任务执行完成"); } catch (Exception e) { log.error("【无交易记录自动结算】定时任务执行失败", e); } } ``` **调用字符串**: `jsowellTask.autoSettleOrdersWithoutTransactionRecord()` **推荐配置**: - 执行周期:每10分钟执行一次 - Cron 表达式:`0 */10 * * * ?` ### 6. 幂等保护 #### Redis 分布式锁 - **锁键**: `settle_order_{orderId}` - **锁值**: "1" - **超时时间**: 300秒(可配置) - **获取方式**: `redisCache.setCacheObject()` + 检查返回值 #### 数据库乐观锁 - **WHERE 条件**: `order_status = '3' AND settlement_time IS NULL` - **更新失败处理**: 记录警告日志,跳过该订单 - **防止并发**: 重新查询订单最新状态 ### 7. 数据校验与告警 #### 充电桩在线检测 ```java // 从 Redis 获取充电枪状态 String connectorKey = "CONNECTOR_STATUS:" + connectorId; Integer status = redisCache.getCacheObject(connectorKey); // 状态 3=充电中,4=已连接未充电 为在线状态 if (status == null || (status != 3 && status != 4)) { logger.warn("充电桩可能已离线,跳过"); continue; } ``` #### 数据新鲜度检测 ```java long minutesSinceUpdate = Duration.between( lastUpdateTime, LocalDateTime.now() ).toMinutes(); if (minutesSinceUpdate > dataFreshnessMinutes) { logger.warn("实时数据过期({}分钟前),跳过", minutesSinceUpdate); continue; } ``` #### 金额异常告警 ```java BigDecimal threshold = payAmount.multiply(amountThresholdRatio); if (chargingAmount.compareTo(threshold) > 0) { String alertMsg = String.format( "【金额异常】订单号:%s,站点ID:%s,充电桩:%s," + "实时充电金额:%.2f 元,已支付金额:%.2f 元,阈值倍数:%.1f", orderCode, stationId, pileSn, chargingAmount, payAmount, amountThresholdRatio ); logger.error(alertMsg); // TODO: 发送告警通知(邮件/短信/钉钉/企业微信) } ``` #### 数据格式校验 - 充电电量非空、格式正确、大于0 - 充电金额非空、格式正确 - 异常时记录错误日志,跳过该订单 ### 8. 单元测试 **文件**: `jsowell-admin/src/test/java/com/jsowell/AutoSettleOrdersWithoutTransactionTest.java` **测试用例**: 1. **testSelectPendingAutoSettleOrders** - 查询待结算订单 2. **testGrayscaleConfiguration** - 灰度配置测试 3. **testAmountThresholdValidation** - 金额阈值检测 4. **testFeatureToggle** - 配置开关测试 5. **testAutoSettleMainFlow** - 完整流程测试 6. **testDataFreshness** - 数据新鲜度判断 --- ## 文件清单 ### 新增文件(3个) ``` jsowell-pile/src/main/java/com/jsowell/pile/config/AutoSettleConfig.java jsowell-pile/src/main/java/com/jsowell/pile/util/SettlementDataConverter.java jsowell-admin/src/test/java/com/jsowell/AutoSettleOrdersWithoutTransactionTest.java ``` ### 修改文件(7个) ``` docs/feature-tracker-无交易记录自动结算.md jsowell-admin/src/main/resources/application.yml jsowell-pile/src/main/java/com/jsowell/pile/service/OrderBasicInfoService.java jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java jsowell-pile/src/main/resources/mapper/pile/OrderBasicInfoMapper.xml jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java ``` ### 代码统计 - 新增代码:约 500 行(核心业务逻辑) - 配置类:95 行 - 工具类:180 行 - 单元测试:180 行 - SQL 语句:2 个 --- ## 部署指南 ### 1. 代码合并 ```bash # 切换到目标分支(通常是 dev 或 master) git checkout dev # 合并功能分支 git merge feature/auto-settle-no-transaction # 推送到远程仓库 git push origin dev ``` ### 2. 配置更新 在 `application-{env}.yml` 中添加配置: ```yaml # 无交易记录自动结算配置 auto-settle: # 功能总开关(默认关闭,需手动开启) enabled: false # 停止超时阈值(分钟)- 订单停止超过该时间才触发自动结算 timeout-minutes: 10 # 定时任务执行间隔(cron表达式) interval: "0 */10 * * * ?" # 灰度站点ID列表(为空表示全量上线) # 示例:[1001, 1002, 1003] grayscale-station-ids: [] # 单次扫描订单数量上限 batch-size: 100 # 异常金额阈值比例(chargingAmount > payAmount * 该比例时告警跳过) amount-threshold-ratio: 1.5 # 实时数据新鲜度阈值(分钟) data-freshness-minutes: 30 # 告警开关 alert-enabled: true # Redis锁超时时间(秒) lock-timeout: 300 ``` ### 3. 添加定时任务 在管理后台的"定时任务"菜单中添加任务: | 字段 | 值 | |------|-----| | 任务名称 | 无交易记录自动结算 | | 任务组名 | DEFAULT | | 调用目标字符串 | jsowellTask.autoSettleOrdersWithoutTransactionRecord() | | cron表达式 | 0 */10 * * * ? | | 执行策略 | 立即执行 | | 是否并发 | 否 | | 状态 | 暂停(灰度期间) | ### 4. 灰度发布流程 #### 阶段1:单站点灰度(第1-3天) ```yaml auto-settle: enabled: true grayscale-station-ids: [1001] # 选择1个业务量适中的站点 ``` **观察指标**: - 成功结算数量 - 跳过数量(及原因) - 失败数量(及异常) - 金额异常告警 - Redis锁竞争情况 - 数据库慢查询 #### 阶段2:小范围灰度(第4-7天) ```yaml auto-settle: enabled: true grayscale-station-ids: [1001, 1002, 1003, 1004, 1005] # 扩展到5个站点 ``` #### 阶段3:大范围灰度(第8-14天) ```yaml auto-settle: enabled: true grayscale-station-ids: [...] # 扩展到30%的站点 ``` #### 阶段4:全量上线(第15天+) ```yaml auto-settle: enabled: true grayscale-station-ids: [] # 空数组 = 全量 ``` ### 5. 监控指标 #### 日志监控 ```bash # 查看执行日志 tail -f logs/jsowell-admin.log | grep "无交易记录自动结算" # 统计成功数量 grep "【无交易记录自动结算】执行完成,成功" logs/jsowell-admin.log | tail -20 # 查看告警日志 grep "【无交易记录自动结算-金额异常】" logs/jsowell-admin.log grep "【无交易记录自动结算-处理异常】" logs/jsowell-admin.log ``` #### 数据库监控 ```sql -- 查看待结算订单数量 SELECT COUNT(*) FROM order_basic_info WHERE del_flag = '0' AND order_status = '3' AND pay_status = '1' AND (transaction_code IS NULL OR transaction_code = '') AND charge_end_time < DATE_SUB(NOW(), INTERVAL 10 MINUTE); -- 查看最近自动结算的订单 SELECT order_code, station_id, pile_sn, charge_end_time, settlement_time, order_amount, refund_amount FROM order_basic_info WHERE reason = '无交易记录自动结算' AND settlement_time > DATE_SUB(NOW(), INTERVAL 1 DAY) ORDER BY settlement_time DESC LIMIT 100; ``` #### Redis监控 ```bash # 查看当前锁数量 redis-cli KEYS "settle_order_*" | wc -l # 检查是否有死锁(TTL < 0) redis-cli KEYS "settle_order_*" | xargs -I {} redis-cli TTL {} ``` --- ## 风险点与应对 ### 1. 并发冲突 **风险**: 多个定时任务实例同时处理同一订单 **应对**: - Redis 分布式锁 + 乐观锁双重保护 - 锁超时时间设置为5分钟,防止死锁 - 乐观锁更新失败时记录日志,不抛出异常 ### 2. 金额异常 **风险**: 实时数据的充电金额远大于预付金额 **应对**: - 设置金额阈值(默认1.5倍) - 超过阈值告警跳过,人工介入 - 详细记录订单信息、站点ID、充电桩SN ### 3. 数据不新鲜 **风险**: 充电桩"假在线"(最后一条实时数据很久之前) **应对**: - 检查实时数据更新时间 - 超过30分钟的数据视为陈旧,跳过结算 - 记录警告日志 ### 4. 充电桩离线 **风险**: 充电桩实际已离线,但订单未正常结束 **应对**: - 从 Redis 检查充电枪状态 - 状态非"充电中"或"已连接"时跳过 - 依赖设备心跳机制更新状态 ### 5. 退款失败 **风险**: 结算成功但退款失败,用户损失 **应对**: - 退款失败记录错误日志 - 告警通知运营人员 - 支持后台手动发起退款 ### 6. 性能问题 **风险**: 单次扫描订单过多,影响数据库性能 **应对**: - 限制单次查询数量(默认100) - 使用索引优化查询(charge_end_time, order_status, pay_status) - 定时任务间隔可调整(默认10分钟) --- ## 后续优化建议 ### 1. 告警系统集成 - 对接现有告警系统(邮件/短信/钉钉/企业微信) - 金额异常、处理失败立即通知 - 每日汇总报告(成功数、失败数、异常数) ### 2. 监控大盘 - Grafana 可视化监控 - 实时展示待结算订单数量 - 自动结算成功率、平均处理时长 - 金额异常趋势图 ### 3. 智能调度 - 根据待结算订单数量动态调整执行频率 - 业务高峰期降低频率,低谷期提高频率 - 避免影响正常业务 ### 4. 数据分析 - 统计哪些站点、哪些充电桩频繁出现无交易记录 - 分析根本原因(设备故障、网络问题、协议异常) - 针对性优化设备通信逻辑 ### 5. 手动补结算 - 后台管理页面添加"手动自动结算"按钮 - 支持批量选择订单执行结算 - 提供详细的执行日志和结果反馈 --- ## 验收标准 ### 功能验收 - [x] 配置开关正常工作 - [x] 定时任务正常调度执行 - [x] 查询待结算订单准确 - [x] 结算逻辑正确(金额、电量、时间) - [x] 退款逻辑正确 - [x] 幂等保护有效 - [x] 数据校验完整 - [x] 告警日志记录详细 - [x] 单元测试通过 ### 性能验收 - [ ] 单次处理100个订单耗时 < 30秒 - [ ] Redis 锁无死锁现象 - [ ] 数据库慢查询 < 5% ### 稳定性验收 - [ ] 灰度期间(7天)无P0/P1故障 - [ ] 并发场景下无数据不一致 - [ ] 异常场景正常降级(记录日志、跳过订单) --- ## 附录 ### A. 相关文档 - [功能设计文档](./feature-tracker-无交易记录自动结算.md) - [功能设计附录](./feature-tracker-无交易记录自动结算-附录.md) - [项目架构文档](../CLAUDE.md) - [jsowell-pile 模块文档](../jsowell-pile/CLAUDE.md) - [jsowell-quartz 模块文档](../jsowell-quartz/CLAUDE.md) ### B. 联系方式 - 开发负责人: jsowell - Git 分支: feature/auto-settle-no-transaction - 提交哈希: 121b357a2 --- **文档版本**: v1.0 **最后更新**: 2026-08-11 **状态**: ✅ 开发完成,待灰度发布