feat: 实现无交易记录自动结算功能

核心功能:
- 配置开关: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
This commit is contained in:
jsowell
2026-08-11 16:40:35 +08:00
parent 33b48f2ad4
commit 121b357a20
10 changed files with 1416 additions and 23 deletions

View File

@@ -0,0 +1,95 @@
package com.jsowell.pile.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 无交易记录自动结算配置类
*
* @author jsowell
*/
@Data
@Component
@ConfigurationProperties(prefix = "auto-settle")
public class AutoSettleConfig {
/**
* 功能总开关
* 默认false关闭
*/
private Boolean enabled = false;
/**
* 停止超时阈值(分钟)
* 订单停止超过该时间才触发自动结算
* 默认10分钟
*/
private Integer timeoutMinutes = 10;
/**
* 定时任务执行间隔cron表达式
* 默认每10分钟执行一次
*/
private String interval = "0 */10 * * * ?";
/**
* 灰度站点ID列表
* 为空表示全量上线
*/
private List<Long> grayscaleStationIds;
/**
* 单次扫描订单数量上限
* 防止一次性处理过多订单
* 默认100
*/
private Integer batchSize = 100;
/**
* 异常金额阈值比例
* chargingAmount > payAmount * 该比例时告警跳过
* 默认1.5倍
*/
private Double amountThresholdRatio = 1.5;
/**
* 实时数据新鲜度阈值(分钟)
* 最后一条实时数据距当前时间超过该阈值则认为桩"假在线"
* 默认30分钟
*/
private Integer dataFreshnessMinutes = 30;
/**
* 告警开关
* 异常情况是否发送告警
* 默认true
*/
private Boolean alertEnabled = true;
/**
* Redis锁超时时间
* 防止分布式锁死锁
* 默认300秒5分钟
*/
private Long lockTimeout = 300L;
/**
* 是否启用灰度(根据灰度站点列表判断)
*/
public boolean isGrayscaleEnabled() {
return grayscaleStationIds != null && !grayscaleStationIds.isEmpty();
}
/**
* 判断站点是否在灰度范围内
*/
public boolean isStationInGrayscale(Long stationId) {
if (!isGrayscaleEnabled()) {
return true; // 未启用灰度,所有站点都算在范围内
}
return grayscaleStationIds.contains(stationId);
}
}