mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-08-14 10:07:35 +08:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,38 @@ public interface OrderBasicInfoMapper {
|
||||
*/
|
||||
int deleteByPrimaryKey(Integer id);
|
||||
|
||||
/**
|
||||
* 查询待自动结算的订单列表
|
||||
* 条件:
|
||||
* 1. 订单状态为待结算
|
||||
* 2. 无交易流水号(transactionCode为空)
|
||||
* 3. 充电结束时间在指定时间之前(stopTime < cutoffTime)
|
||||
* 4. 支付状态为已支付
|
||||
* 5. 灰度站点范围内(如果指定)
|
||||
*
|
||||
* @param cutoffTime 截止时间(当前时间 - 超时阈值)
|
||||
* @param stationIds 灰度站点ID列表,为空则不限制站点
|
||||
* @param limit 查询数量限制
|
||||
* @return 待自动结算的订单列表
|
||||
*/
|
||||
List<OrderBasicInfo> selectPendingAutoSettleOrders(@Param("cutoffTime") LocalDateTime cutoffTime,
|
||||
@Param("stationIds") List<Long> stationIds,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 使用乐观锁更新订单(用于自动结算)
|
||||
*
|
||||
* @param order 订单信息
|
||||
* @param orderId 订单ID
|
||||
* @param expectedStatus 期望的订单状态
|
||||
* @param expectedSettlementTime 期望的结算时间(null表示未结算)
|
||||
* @return 更新行数
|
||||
*/
|
||||
int updateOrderWithOptimisticLock(@Param("order") OrderBasicInfo order,
|
||||
@Param("orderId") Integer orderId,
|
||||
@Param("expectedStatus") String expectedStatus,
|
||||
@Param("expectedSettlementTime") java.util.Date expectedSettlementTime);
|
||||
|
||||
/**
|
||||
* insert record to table
|
||||
*
|
||||
|
||||
@@ -753,10 +753,16 @@ public interface OrderBasicInfoService{
|
||||
List<String> merchantIdList, List<String> stationIdList);
|
||||
|
||||
/**
|
||||
* 大数据平台-今日充电电量(Java汇总)
|
||||
* 大数据平台-今日充电电量(Java汇总)
|
||||
*
|
||||
* @return 今日已完成订单的充电电量合计(kWh)
|
||||
* @return 今日已完成订单的充电电量合计(kWh)
|
||||
*/
|
||||
java.math.BigDecimal getTodayElectricity(String startTime, String endTime,
|
||||
List<String> merchantIdList, List<String> stationIdList);
|
||||
|
||||
/**
|
||||
* 无交易记录自动结算
|
||||
* 扫描停止超时的待结算订单,如果有实时数据但无交易记录,则自动结算
|
||||
*/
|
||||
void autoSettleOrdersWithoutTransactionRecord();
|
||||
}
|
||||
|
||||
@@ -213,6 +213,9 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
|
||||
@Autowired
|
||||
private com.jsowell.pile.mq.producer.PointsRewardProducer pointsRewardProducer;
|
||||
|
||||
@Autowired
|
||||
private com.jsowell.pile.config.AutoSettleConfig autoSettleConfig;
|
||||
|
||||
@Override
|
||||
public int deleteByPrimaryKey(Integer id) {
|
||||
return orderBasicInfoMapper.deleteByPrimaryKey(id);
|
||||
@@ -7435,4 +7438,327 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void autoSettleOrdersWithoutTransactionRecord() {
|
||||
// 1. 检查功能总开关
|
||||
if (!autoSettleConfig.getEnabled()) {
|
||||
logger.debug("【无交易记录自动结算】功能未启用,跳过执行");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("【无交易记录自动结算】开始执行,超时阈值:{}分钟,批次大小:{}",
|
||||
autoSettleConfig.getTimeoutMinutes(), autoSettleConfig.getBatchSize());
|
||||
|
||||
// 2. 计算截止时间
|
||||
LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(autoSettleConfig.getTimeoutMinutes());
|
||||
|
||||
// 3. 获取灰度站点列表(若启用灰度)
|
||||
List<Long> grayscaleStationIds = autoSettleConfig.isGrayscaleEnabled() ?
|
||||
autoSettleConfig.getGrayscaleStationIds() : null;
|
||||
|
||||
// 4. 查询待结算订单
|
||||
List<OrderBasicInfo> pendingOrders = orderBasicInfoMapper.selectPendingAutoSettleOrders(
|
||||
cutoffTime, grayscaleStationIds, autoSettleConfig.getBatchSize());
|
||||
|
||||
if (pendingOrders == null || pendingOrders.isEmpty()) {
|
||||
logger.info("【无交易记录自动结算】本次扫描未发现符合条件的订单");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("【无交易记录自动结算】本次扫描到 {} 条待处理订单", pendingOrders.size());
|
||||
|
||||
int successCount = 0;
|
||||
int skipCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
// 5. 逐单处理
|
||||
for (OrderBasicInfo order : pendingOrders) {
|
||||
String lockKey = "settle_order_" + order.getId();
|
||||
boolean lockAcquired = false;
|
||||
|
||||
try {
|
||||
// 5.1 获取分布式锁
|
||||
redisCache.setCacheObject(lockKey, "1",
|
||||
autoSettleConfig.getLockTimeout().intValue(), TimeUnit.SECONDS);
|
||||
|
||||
// 尝试获取锁(检查是否成功设置)
|
||||
String lockValue = redisCache.getCacheObject(lockKey);
|
||||
lockAcquired = "1".equals(lockValue);
|
||||
|
||||
if (!lockAcquired) {
|
||||
logger.warn("【无交易记录自动结算】订单 {} 正在被其他线程处理,跳过", order.getOrderCode());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5.2 重新查询订单最新状态(防止并发问题 + 乐观锁)
|
||||
OrderBasicInfo latestOrder = orderBasicInfoMapper.selectOrderBasicInfoById(Long.valueOf(order.getId()));
|
||||
if (latestOrder == null || !"3".equals(latestOrder.getOrderStatus()) || latestOrder.getSettlementTime() != null) {
|
||||
logger.warn("【无交易记录自动结算】订单 {} 状态已变更或已结算,跳过", order.getOrderCode());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5.3 检查灰度范围
|
||||
Long stationIdLong = latestOrder.getStationId() != null ? Long.parseLong(latestOrder.getStationId()) : null;
|
||||
if (stationIdLong != null && !autoSettleConfig.isStationInGrayscale(stationIdLong)) {
|
||||
logger.debug("【无交易记录自动结算】订单 {} 所属站点不在灰度范围内,跳过", order.getOrderCode());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5.4 检查充电桩状态(从 Redis 获取实时状态)
|
||||
String pileStatusKey = "pile_status_" + latestOrder.getPileSn();
|
||||
Object pileStatusObj = redisCache.getCacheObject(pileStatusKey);
|
||||
if (pileStatusObj != null && "0".equals(pileStatusObj.toString())) {
|
||||
logger.warn("【无交易记录自动结算】订单 {} 对应充电桩 {} 当前离线,跳过",
|
||||
order.getOrderCode(), latestOrder.getPileSn());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5.5 获取实时数据(OrderMonitorData,不是RealTimeMonitorData)
|
||||
List<RealTimeMonitorData> realTimeDataList = getChargingRealTimeData(latestOrder.getOrderCode());
|
||||
if (realTimeDataList == null || realTimeDataList.isEmpty()) {
|
||||
logger.warn("【无交易记录自动结算】订单 {} 未找到实时数据,跳过", order.getOrderCode());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 取最后一条实时数据
|
||||
RealTimeMonitorData lastRealTimeData = realTimeDataList.get(realTimeDataList.size() - 1);
|
||||
|
||||
// 5.6 检查数据新鲜度 - 暂时跳过,因为RealTimeMonitorData没有createTime字段
|
||||
// 使用dateTime字段判断
|
||||
if (lastRealTimeData.getDateTime() != null) {
|
||||
try {
|
||||
LocalDateTime dataTime = LocalDateTime.parse(lastRealTimeData.getDateTime(),
|
||||
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
long minutesSinceLastData = java.time.Duration.between(dataTime, LocalDateTime.now()).toMinutes();
|
||||
if (minutesSinceLastData > autoSettleConfig.getDataFreshnessMinutes()) {
|
||||
logger.warn("【无交易记录自动结算】订单 {} 实时数据不新鲜({}分钟前),疑似桩假在线,跳过",
|
||||
order.getOrderCode(), minutesSinceLastData);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("【无交易记录自动结算】订单 {} 解析实时数据时间失败,跳过", order.getOrderCode(), e);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 5.7 构造结算数据(从实时数据转换为交易记录格式)
|
||||
TransactionRecordsData settlementData = new TransactionRecordsData();
|
||||
settlementData.setStartTime(com.jsowell.common.util.DateUtils.parseDateToStr(
|
||||
com.jsowell.common.util.DateUtils.YYYY_MM_DD_HH_MM_SS,
|
||||
latestOrder.getChargeStartTime()));
|
||||
settlementData.setEndTime(com.jsowell.common.util.DateUtils.parseDateToStr(
|
||||
com.jsowell.common.util.DateUtils.YYYY_MM_DD_HH_MM_SS,
|
||||
latestOrder.getChargeEndTime()));
|
||||
|
||||
// 从实时数据中提取充电信息
|
||||
String chargingDegree = lastRealTimeData.getChargingDegree();
|
||||
String lossDegree = lastRealTimeData.getLossDegree();
|
||||
String chargingAmount = lastRealTimeData.getChargingAmount();
|
||||
|
||||
// 数据校验:检查充电电量是否为空或0
|
||||
if (chargingDegree == null || chargingDegree.trim().isEmpty()) {
|
||||
logger.warn("【无交易记录自动结算】订单 {} 充电电量为空,跳过", order.getOrderCode());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
BigDecimal chargingDegreeDecimal = new BigDecimal(chargingDegree);
|
||||
if (chargingDegreeDecimal.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
logger.warn("【无交易记录自动结算】订单 {} 充电电量为0或负数({}),跳过",
|
||||
order.getOrderCode(), chargingDegree);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
logger.error("【无交易记录自动结算】订单 {} 充电电量格式错误({}),跳过",
|
||||
order.getOrderCode(), chargingDegree, e);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 数据校验:检查充电金额格式
|
||||
if (chargingAmount == null || chargingAmount.trim().isEmpty()) {
|
||||
logger.warn("【无交易记录自动结算】订单 {} 充电金额为空,跳过", order.getOrderCode());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
new BigDecimal(chargingAmount);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.error("【无交易记录自动结算】订单 {} 充电金额格式错误({}),跳过",
|
||||
order.getOrderCode(), chargingAmount, e);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 设置总电量和消费金额
|
||||
settlementData.setTotalElectricity(chargingDegree);
|
||||
settlementData.setPlanLossTotalElectricity(lossDegree != null ? lossDegree : chargingDegree);
|
||||
settlementData.setConsumptionAmount(chargingAmount);
|
||||
|
||||
// 将所有电量归为平段
|
||||
settlementData.setFlatUsedElectricity(chargingDegree);
|
||||
settlementData.setFlatPlanLossElectricity(lossDegree != null ? lossDegree : chargingDegree);
|
||||
settlementData.setFlatAmount(chargingAmount);
|
||||
|
||||
// 其他时段设为0
|
||||
settlementData.setSharpUsedElectricity("0.0000");
|
||||
settlementData.setSharpAmount("0.0000");
|
||||
settlementData.setPeakUsedElectricity("0.0000");
|
||||
settlementData.setPeakAmount("0.0000");
|
||||
settlementData.setValleyUsedElectricity("0.0000");
|
||||
settlementData.setValleyAmount("0.0000");
|
||||
|
||||
// 停止原因
|
||||
settlementData.setStopReasonMsg("无交易记录自动结算");
|
||||
|
||||
// 5.9 金额异常检测
|
||||
BigDecimal chargingAmountDecimal = new BigDecimal(chargingAmount);
|
||||
if (latestOrder.getPayAmount() != null) {
|
||||
BigDecimal threshold = latestOrder.getPayAmount()
|
||||
.multiply(BigDecimal.valueOf(autoSettleConfig.getAmountThresholdRatio()));
|
||||
if (chargingAmountDecimal.compareTo(threshold) > 0) {
|
||||
String alertMsg = String.format(
|
||||
"【无交易记录自动结算-金额异常】订单号:%s,站点ID:%s,充电桩:%s," +
|
||||
"实时充电金额:%.2f 元,已支付金额:%.2f 元,阈值倍数:%.1f," +
|
||||
"充电电量:%s kWh,充电时长:%d 分钟",
|
||||
order.getOrderCode(),
|
||||
latestOrder.getStationId(),
|
||||
latestOrder.getPileSn(),
|
||||
chargingAmountDecimal,
|
||||
latestOrder.getPayAmount(),
|
||||
autoSettleConfig.getAmountThresholdRatio(),
|
||||
chargingDegree,
|
||||
java.time.Duration.between(
|
||||
java.time.LocalDateTime.ofInstant(
|
||||
latestOrder.getChargeStartTime().toInstant(),
|
||||
java.time.ZoneId.systemDefault()),
|
||||
java.time.LocalDateTime.ofInstant(
|
||||
latestOrder.getChargeEndTime().toInstant(),
|
||||
java.time.ZoneId.systemDefault())
|
||||
).toMinutes()
|
||||
);
|
||||
logger.error(alertMsg);
|
||||
if (autoSettleConfig.getAlertEnabled()) {
|
||||
// TODO: 发送告警通知(对接现有告警系统)
|
||||
// 可以在这里发送邮件、短信、钉钉、企业微信等告警
|
||||
}
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 5.10 执行结算(使用乐观锁)
|
||||
logger.info("【无交易记录自动结算】开始结算订单:{},充电电量:{}kWh,结算金额:{}元",
|
||||
order.getOrderCode(), chargingDegree, chargingAmount);
|
||||
|
||||
// 更新订单基础信息
|
||||
returnUpdateOrderBasicInfo(latestOrder, settlementData);
|
||||
|
||||
// 更新订单明细
|
||||
returnUpdateOrderDetail(latestOrder, settlementData);
|
||||
|
||||
// 使用 MyBatis 传统方式更新(带乐观锁条件)
|
||||
OrderBasicInfo updateOrder = new OrderBasicInfo();
|
||||
updateOrder.setId(latestOrder.getId());
|
||||
updateOrder.setOrderStatus(latestOrder.getOrderStatus());
|
||||
updateOrder.setOrderAmount(latestOrder.getOrderAmount());
|
||||
updateOrder.setVirtualAmount(latestOrder.getVirtualAmount());
|
||||
updateOrder.setSettleAmount(latestOrder.getSettleAmount());
|
||||
updateOrder.setActualReceivedAmount(latestOrder.getActualReceivedAmount());
|
||||
updateOrder.setReason(latestOrder.getReason());
|
||||
updateOrder.setSettlementTime(latestOrder.getSettlementTime());
|
||||
updateOrder.setRefundAmount(latestOrder.getRefundAmount());
|
||||
|
||||
int updateCount = orderBasicInfoMapper.updateOrderWithOptimisticLock(
|
||||
updateOrder,
|
||||
latestOrder.getId(),
|
||||
"3", // 必须是待结算状态
|
||||
null); // 结算时间必须为null
|
||||
|
||||
if (updateCount == 0) {
|
||||
logger.warn("【无交易记录自动结算】订单 {} 乐观锁更新失败,可能已被其他线程结算,跳过", order.getOrderCode());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5.11 处理退款(如果结算金额 < 已支付金额)
|
||||
BigDecimal consumptionAmountDecimal = new BigDecimal(settlementData.getConsumptionAmount());
|
||||
if (consumptionAmountDecimal != null && latestOrder.getPayAmount() != null) {
|
||||
BigDecimal refundAmount = latestOrder.getPayAmount().subtract(consumptionAmountDecimal);
|
||||
if (refundAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
logger.info("【无交易记录自动结算】订单 {} 需退款:{}元", order.getOrderCode(), refundAmount);
|
||||
// 调用退款逻辑(与正常结算流程一致)
|
||||
handleAutoSettleRefund(latestOrder, refundAmount);
|
||||
}
|
||||
}
|
||||
|
||||
successCount++;
|
||||
logger.info("【无交易记录自动结算】订单 {} 结算成功", order.getOrderCode());
|
||||
|
||||
} catch (Exception e) {
|
||||
failCount++;
|
||||
String errorMsg = String.format(
|
||||
"【无交易记录自动结算-处理异常】订单号:%s,站点ID:%s,充电桩:%s,异常信息:%s",
|
||||
order.getOrderCode(),
|
||||
order.getStationId(),
|
||||
order.getPileSn(),
|
||||
e.getMessage()
|
||||
);
|
||||
logger.error(errorMsg, e);
|
||||
if (autoSettleConfig.getAlertEnabled()) {
|
||||
// TODO: 发送告警通知
|
||||
// 建议包含异常堆栈信息前3行,方便快速定位问题
|
||||
}
|
||||
} finally {
|
||||
// 5.12 释放分布式锁
|
||||
if (lockAcquired) {
|
||||
redisCache.deleteObject(lockKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("【无交易记录自动结算】执行完成,成功:{},跳过:{},失败:{}",
|
||||
successCount, skipCount, failCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自动结算的退款
|
||||
*/
|
||||
private void handleAutoSettleRefund(OrderBasicInfo order, BigDecimal refundAmount) {
|
||||
try {
|
||||
// 根据支付方式调用不同的退款接口
|
||||
String payMode = order.getPayMode();
|
||||
if ("1".equals(payMode)) {
|
||||
// 微信支付退款
|
||||
// TODO: 调用微信退款接口
|
||||
logger.info("【无交易记录自动结算】订单 {} 微信退款:{}元", order.getOrderCode(), refundAmount);
|
||||
} else if ("2".equals(payMode)) {
|
||||
// 支付宝退款
|
||||
// TODO: 调用支付宝退款接口
|
||||
logger.info("【无交易记录自动结算】订单 {} 支付宝退款:{}元", order.getOrderCode(), refundAmount);
|
||||
} else if ("3".equals(payMode)) {
|
||||
// 汇付支付退款
|
||||
// TODO: 调用汇付退款接口
|
||||
logger.info("【无交易记录自动结算】订单 {} 汇付支付退款:{}元", order.getOrderCode(), refundAmount);
|
||||
} else if ("4".equals(payMode)) {
|
||||
// 余额支付退款(直接返还会员钱包)
|
||||
logger.info("【无交易记录自动结算】订单 {} 余额退款:{}元", order.getOrderCode(), refundAmount);
|
||||
// TODO: 调用钱包退款接口
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("【无交易记录自动结算】订单 {} 退款处理失败:{}元", order.getOrderCode(), refundAmount, e);
|
||||
throw new RuntimeException("退款处理失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package com.jsowell.pile.util;
|
||||
|
||||
import com.jsowell.common.core.domain.ykc.RealTimeMonitorData;
|
||||
import com.jsowell.common.core.domain.ykc.TransactionRecordsData;
|
||||
import com.jsowell.common.util.DateUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 结算数据转换工具类
|
||||
* 用于将实时监测数据转换为交易记录数据
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
public class SettlementDataConverter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SettlementDataConverter.class);
|
||||
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* 将实时监测数据转换为结算用的交易记录数据
|
||||
*
|
||||
* @param realTimeData 最后一条实时监测数据
|
||||
* @param startTime 订单开始时间
|
||||
* @return 交易记录数据
|
||||
*/
|
||||
public static TransactionRecordsData convertToTransactionData(RealTimeMonitorData realTimeData, LocalDateTime startTime) {
|
||||
if (realTimeData == null) {
|
||||
logger.warn("实时监测数据为空,无法转换为交易记录");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(realTimeData.getTransactionCode())) {
|
||||
logger.warn("实时监测数据缺少交易流水号,无法转换");
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info("开始转换实时数据为结算数据, transactionCode: {}, chargingDegree: {}, chargingAmount: {}",
|
||||
realTimeData.getTransactionCode(),
|
||||
realTimeData.getChargingDegree(),
|
||||
realTimeData.getChargingAmount());
|
||||
|
||||
TransactionRecordsData transactionData = new TransactionRecordsData();
|
||||
|
||||
// 基础信息
|
||||
transactionData.setTransactionCode(realTimeData.getTransactionCode());
|
||||
transactionData.setPileSn(realTimeData.getPileSn());
|
||||
transactionData.setConnectorCode(realTimeData.getConnectorCode());
|
||||
|
||||
// 时间信息
|
||||
String startTimeStr = startTime != null ? startTime.format(FORMATTER) : null;
|
||||
String endTimeStr = StringUtils.isNotBlank(realTimeData.getDateTime())
|
||||
? realTimeData.getDateTime()
|
||||
: LocalDateTime.now().format(FORMATTER);
|
||||
|
||||
transactionData.setStartTime(startTimeStr);
|
||||
transactionData.setEndTime(endTimeStr);
|
||||
transactionData.setTransactionTime(endTimeStr);
|
||||
|
||||
// 电量和金额信息(使用实时数据的累计值)
|
||||
String chargingDegree = defaultIfBlank(realTimeData.getChargingDegree(), "0.0000");
|
||||
String lossDegree = defaultIfBlank(realTimeData.getLossDegree(), chargingDegree);
|
||||
String chargingAmount = defaultIfBlank(realTimeData.getChargingAmount(), "0.0000");
|
||||
|
||||
// 设置总电量(不区分时段,全部归为平段)
|
||||
transactionData.setTotalElectricity(chargingDegree);
|
||||
transactionData.setPlanLossTotalElectricity(lossDegree);
|
||||
transactionData.setConsumptionAmount(chargingAmount);
|
||||
|
||||
// 平段数据(将所有数据归为平段)
|
||||
transactionData.setFlatUsedElectricity(chargingDegree);
|
||||
transactionData.setFlatPlanLossElectricity(lossDegree);
|
||||
transactionData.setFlatAmount(chargingAmount);
|
||||
|
||||
// 计算平段单价(避免除零)
|
||||
if (isPositive(lossDegree) && isPositive(chargingAmount)) {
|
||||
BigDecimal price = new BigDecimal(chargingAmount).divide(new BigDecimal(lossDegree), 5, BigDecimal.ROUND_HALF_UP);
|
||||
transactionData.setFlatPrice(price.toPlainString());
|
||||
} else {
|
||||
transactionData.setFlatPrice("0.00000");
|
||||
}
|
||||
|
||||
// 其他时段设为0
|
||||
transactionData.setSharpUsedElectricity("0.0000");
|
||||
transactionData.setSharpPlanLossElectricity("0.0000");
|
||||
transactionData.setSharpAmount("0.0000");
|
||||
transactionData.setSharpPrice("0.00000");
|
||||
|
||||
transactionData.setPeakUsedElectricity("0.0000");
|
||||
transactionData.setPeakPlanLossElectricity("0.0000");
|
||||
transactionData.setPeakAmount("0.0000");
|
||||
transactionData.setPeakPrice("0.00000");
|
||||
|
||||
transactionData.setValleyUsedElectricity("0.0000");
|
||||
transactionData.setValleyPlanLossElectricity("0.0000");
|
||||
transactionData.setValleyAmount("0.0000");
|
||||
transactionData.setValleyPrice("0.00000");
|
||||
|
||||
// 电表读数(无实际数据)
|
||||
transactionData.setAmmeterTotalStart("0.0000");
|
||||
transactionData.setAmmeterTotalEnd(chargingDegree);
|
||||
|
||||
// 停止原因:无交易记录自动结算
|
||||
transactionData.setStopReasonCode("0xFF");
|
||||
transactionData.setStopReasonMsg("无交易记录自动结算");
|
||||
|
||||
// 交易标识(未知)
|
||||
transactionData.setTransactionIdentifier("0x00");
|
||||
|
||||
// VIN码和物理卡号(无)
|
||||
transactionData.setVinCode("");
|
||||
transactionData.setLogicCard("");
|
||||
|
||||
// 电费和服务费(无法拆分,设为空由业务层处理)
|
||||
transactionData.setTotalElectricityAmount(null);
|
||||
transactionData.setTotalServiceAmount(null);
|
||||
|
||||
logger.info("实时数据转换为结算数据完成, transactionCode: {}, totalElectricity: {}, consumptionAmount: {}",
|
||||
transactionData.getTransactionCode(),
|
||||
transactionData.getTotalElectricity(),
|
||||
transactionData.getConsumptionAmount());
|
||||
|
||||
return transactionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回默认值(如果为空)
|
||||
*/
|
||||
private static String defaultIfBlank(String value, String defaultValue) {
|
||||
return StringUtils.isNotBlank(value) ? value : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串数值是否为正数
|
||||
*/
|
||||
private static boolean isPositive(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(value).compareTo(BigDecimal.ZERO) > 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验实时数据是否有效(用于自动结算前的数据校验)
|
||||
*
|
||||
* @param realTimeData 实时监测数据
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean isValidForSettlement(RealTimeMonitorData realTimeData) {
|
||||
if (realTimeData == null) {
|
||||
logger.warn("实时数据为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(realTimeData.getTransactionCode())) {
|
||||
logger.warn("实时数据缺少交易流水号");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 充电度数必须大于0
|
||||
String chargingDegree = realTimeData.getChargingDegree();
|
||||
if (StringUtils.isBlank(chargingDegree) || !isPositive(chargingDegree)) {
|
||||
logger.warn("充电度数无效, transactionCode: {}, chargingDegree: {}",
|
||||
realTimeData.getTransactionCode(), chargingDegree);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 充电金额必须 >= 0(可以为0,如白名单免费)
|
||||
String chargingAmount = realTimeData.getChargingAmount();
|
||||
if (StringUtils.isBlank(chargingAmount)) {
|
||||
logger.warn("充电金额为空, transactionCode: {}", realTimeData.getTransactionCode());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
BigDecimal amount = new BigDecimal(chargingAmount);
|
||||
if (amount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
logger.warn("充电金额为负数, transactionCode: {}, chargingAmount: {}",
|
||||
realTimeData.getTransactionCode(), chargingAmount);
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("充电金额格式错误, transactionCode: {}, chargingAmount: {}",
|
||||
realTimeData.getTransactionCode(), chargingAmount, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user