fix: 控制器没有塘口时,也能打电话产生报警。

This commit is contained in:
tianyongbao
2026-06-06 09:21:51 +08:00
parent 266fd0587a
commit bf2b2045c8
5 changed files with 436 additions and 238 deletions

View File

@@ -1535,18 +1535,7 @@ public class DeviceDataHandler {
return;
}
if (device.getPondId() == null) {
log.debug("[设备故障] 设备未绑定塘口,跳过: {}", deviceName);
return;
}
Pond pond = pondMapper.selectById(device.getPondId());
if (pond == null) {
log.debug("[设备故障] 塘口不存在,跳过: {}", deviceName);
return;
}
String pondName = pond.getPondName();
String pondName = getPondName(device.getPondId());
String displayName = device.getDeviceName() != null ? device.getDeviceName() : deviceName;
// 解析事件参数获取故障码
@@ -1557,8 +1546,8 @@ public class DeviceDataHandler {
errorDesc = "设备故障(code=" + errorCode + ")";
}
String warnMsg = String.format("%s塘口中%s(%s)发生故障:%s",
pondName, displayName, deviceName, errorDesc);
String warnMsg = formatPondWarnMessage(pondName,
String.format("%s(%s)发生故障:%s", displayName, deviceName, errorDesc));
log.error("[设备故障] 设备: {}, 故障信息: {}", deviceName, warnMsg);
// 获取告警手机号
@@ -1728,23 +1717,18 @@ public class DeviceDataHandler {
}
// 查询塘口(如果设备在塘口中)
String pondName = null;
if (device.getPondId() != null) {
Pond pond = pondMapper.selectById(device.getPondId());
if (pond != null) {
pondName = pond.getPondName();
}
}
String pondName = getPondName(device.getPondId());
String displayName = device.getDeviceName() != null ? device.getDeviceName() : device.getSerialNum();
// 设备在塘口中且非山机才触发告警
// 非离线状态才触发告警;未绑定塘口时仍告警,告警内容不带塘口前缀
boolean isOffline = device.getWarnCode() != null
&& (device.getWarnCode() & DefineDeviceWarnCode.DeviceOffline) != 0;
boolean warnTrigger = !isOffline && pondName != null;
boolean warnTrigger = !isOffline;
// 根据故障码范围分类生成告警内容
String title = null;
String message = null;
String messagePondName = pondName;
int switchIndex = 0;
// 告警类型:默认为控制器告警(3),开关相关故障使用开关告警(4)
int warnType = WARN_TYPE_CONTROLLER;
@@ -1758,8 +1742,8 @@ public class DeviceDataHandler {
}
if (warnTrigger) {
title = "控制器输入缺相";
message = String.format("%s塘口中%s(%s)输入缺%s相",
pondName, displayName, device.getSerialNum(), phaseName);
message = formatPondWarnMessage(messagePondName,
String.format("%s(%s)输入缺%s相", displayName, device.getSerialNum(), phaseName));
}
} else if (errorCode >= DefineDeviceErrorCode.Three_PhaseA_OverVoltage
&& errorCode <= DefineDeviceErrorCode.Three_PhaseC_UnderVoltage) {
@@ -1769,8 +1753,9 @@ public class DeviceDataHandler {
String phaseName = DefineDeviceErrorCode.getPhaseName(phaseIndex);
if (warnTrigger) {
title = isOver ? "控制器输入过压" : "控制器输入欠压";
message = String.format("%s塘口中%s(%s)%s相%s",
pondName, displayName, device.getSerialNum(), phaseName, isOver ? "过压" : "欠压");
message = formatPondWarnMessage(messagePondName,
String.format("%s(%s)%s相%s",
displayName, device.getSerialNum(), phaseName, isOver ? "过压" : "欠压"));
}
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1LosePhaseA
&& errorCode <= DefineDeviceErrorCode.Three_Switch4LosePhaseC) {
@@ -1785,10 +1770,12 @@ public class DeviceDataHandler {
if (warnTrigger) {
String swPondName = (sw != null && sw.getPondId() != null)
? getPondName(sw.getPondId()) : pondName;
messagePondName = swPondName;
String swName = sw != null && sw.getSwitchName() != null ? sw.getSwitchName() : ("开关" + switchIndex);
title = "控制器开关输出缺相";
message = String.format("%s塘口中%s(%s)的开关%d(%s)输出缺%s相",
swPondName, displayName, device.getSerialNum(), switchIndex, swName, phaseName);
message = formatPondWarnMessage(messagePondName,
String.format("%s(%s)的开关%d(%s)输出缺%s相",
displayName, device.getSerialNum(), switchIndex, swName, phaseName));
}
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1OverElectricA
&& errorCode <= DefineDeviceErrorCode.Three_Switch4OverElectricC) {
@@ -1802,10 +1789,12 @@ public class DeviceDataHandler {
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
messagePondName = swPondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("开关" + switchIndex);
title = "控制器开关输出过流";
message = String.format("%s塘口中%s(%s)的开关%d(%s)输出%s相过流",
swPondName, displayName, device.getSerialNum(), switchIndex, swName, phaseName);
message = formatPondWarnMessage(messagePondName,
String.format("%s(%s)的开关%d(%s)输出%s相过流",
displayName, device.getSerialNum(), switchIndex, swName, phaseName));
}
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1UnderElectricA
&& errorCode <= DefineDeviceErrorCode.Three_Switch4UnderElectricC) {
@@ -1819,10 +1808,12 @@ public class DeviceDataHandler {
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
messagePondName = swPondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("开关" + switchIndex);
title = "控制器开关输出欠流";
message = String.format("%s塘口中%s(%s)的开关%d(%s)输出%s相欠流",
swPondName, displayName, device.getSerialNum(), switchIndex, swName, phaseName);
message = formatPondWarnMessage(messagePondName,
String.format("%s(%s)的开关%d(%s)输出%s相欠流",
displayName, device.getSerialNum(), switchIndex, swName, phaseName));
}
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1ElectricEmpty
&& errorCode <= DefineDeviceErrorCode.Three_Switch4ElectricEmpty) {
@@ -1834,10 +1825,12 @@ public class DeviceDataHandler {
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
messagePondName = swPondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("开关" + switchIndex);
title = "控制器开关输出空载";
message = String.format("%s塘口中%s(%s)的开关%d(%s)输出空载",
swPondName, displayName, device.getSerialNum(), switchIndex, swName);
message = formatPondWarnMessage(messagePondName,
String.format("%s(%s)的开关%d(%s)输出空载",
displayName, device.getSerialNum(), switchIndex, swName));
}
} else if (errorCode >= DefineDeviceErrorCode.One_Switch1OverElectric
&& errorCode <= DefineDeviceErrorCode.One_Switch4OverElectric) {
@@ -1849,10 +1842,12 @@ public class DeviceDataHandler {
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
messagePondName = swPondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("开关" + switchIndex);
title = "控制器开关输出过流";
message = String.format("%s塘口中%s(%s)的开关%d(%s)输出过流",
swPondName, displayName, device.getSerialNum(), switchIndex, swName);
message = formatPondWarnMessage(messagePondName,
String.format("%s(%s)的开关%d(%s)输出过流",
displayName, device.getSerialNum(), switchIndex, swName));
}
} else if (errorCode >= DefineDeviceErrorCode.One_Switch1UnderElectric
&& errorCode <= DefineDeviceErrorCode.One_Switch4UnderElectric) {
@@ -1864,10 +1859,12 @@ public class DeviceDataHandler {
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
messagePondName = swPondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("开关" + switchIndex);
title = "控制器开关输出欠流";
message = String.format("%s塘口中%s(%s)的开关%d(%s)输出欠流",
swPondName, displayName, device.getSerialNum(), switchIndex, swName);
message = formatPondWarnMessage(messagePondName,
String.format("%s(%s)的开关%d(%s)输出欠流",
displayName, device.getSerialNum(), switchIndex, swName));
}
} else if (errorCode >= DefineDeviceErrorCode.One_Switch1ElectricEmpty
&& errorCode <= DefineDeviceErrorCode.One_Switch4ElectricEmpty) {
@@ -1879,10 +1876,12 @@ public class DeviceDataHandler {
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
messagePondName = swPondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("开关" + switchIndex);
title = "控制器开关输出空载";
message = String.format("%s塘口中%s(%s)的开关%d(%s)输出空载",
swPondName, displayName, device.getSerialNum(), switchIndex, swName);
message = formatPondWarnMessage(messagePondName,
String.format("%s(%s)的开关%d(%s)输出空载",
displayName, device.getSerialNum(), switchIndex, swName));
}
} else if (errorCode == DefineDeviceErrorCode.PowerOff) {
// 断电:已存在离线状态则不触发断电告警
@@ -1893,8 +1892,8 @@ public class DeviceDataHandler {
setDeviceWarnCodeBit(device.getSerialNum(), DefineDeviceWarnCode.ErrorPowerOff, true);
if (warnTrigger) {
title = "控制器断电";
message = String.format("%s塘口中%s(%s)断电",
pondName, displayName, device.getSerialNum());
message = formatPondWarnMessage(messagePondName,
String.format("%s(%s)断电", displayName, device.getSerialNum()));
}
} else {
warnTrigger = false;
@@ -1904,11 +1903,11 @@ public class DeviceDataHandler {
if (warnTrigger && title != null && message != null) {
Long userId = device.getUserId();
if (userId != null) {
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, title, warnType, message);
MessageWarn warn = createMessageWarn(deviceId, userId, messagePondName, title, warnType, message);
messageWarnMapper.insert(warn);
// 创建电话通知记录(全局开关控制)
if (aliyunIotProperties.isCallNoticeEnabled()) {
createCallNotices(device, pondName, warn);
createCallNotices(device, messagePondName, warn);
}
}
}
@@ -1990,6 +1989,16 @@ public class DeviceDataHandler {
}
}
/**
* 格式化带塘口前缀的告警内容。未绑定塘口时只保留设备告警内容。
*/
private String formatPondWarnMessage(String pondName, String content) {
if (StrUtil.isBlank(pondName)) {
return content;
}
return String.format("%s塘口中%s", pondName, content);
}
/**
* 根据塘口ID获取塘口名称
*/

View File

@@ -281,13 +281,22 @@ public class WeixinController extends BaseController {
@Operation(summary = "主动确认微信支付订单")
@PostMapping("/pay/confirm_order")
public R<Boolean> confirmOrder(@RequestBody Map<String, String> request) {
public R<Boolean> confirmOrder(@RequestBody Map<String, Object> request) {
try {
if (payOrderBusinessService == null) {
return R.fail("支付服务未启用");
}
String outTradeNumber = request != null ? request.get("outTradeNumber") : null;
String outTradeNumber = request != null && request.get("outTradeNumber") != null
? String.valueOf(request.get("outTradeNumber"))
: null;
if ((outTradeNumber == null || outTradeNumber.isEmpty()) && request != null && request.get("orderId") != null) {
Long orderId = Long.valueOf(String.valueOf(request.get("orderId")));
com.intc.fishery.domain.PayOrder order = payOrderBusinessService.queryById(orderId);
if (order != null) {
outTradeNumber = order.getOutTradeNumber();
}
}
if (outTradeNumber == null || outTradeNumber.isEmpty()) {
return R.fail("商户订单号不能为空");
}

View File

@@ -85,7 +85,7 @@ public interface PayOrderBusinessService {
/**
* 补偿未处理的支付订单
* 主动向微信支付查询状态为“未支付/支付中”但已有prepayId的订单,
* 主动向微信支付查询状态为“未支付/支付中/支付失败/已关闭”且有商户订单号的订单,
* 如果微信侧已支付成功则执行回调补偿
*
* @return 补偿成功的订单数量

View File

@@ -2,6 +2,7 @@ package com.intc.weixin.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.intc.common.core.exception.ServiceException;
import com.intc.common.tenant.helper.TenantHelper;
import com.intc.fishery.constant.DefineDeviceWarnCode;
@@ -28,6 +29,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
/**
@@ -51,6 +53,13 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
* 订单处理锁,防止并发处理同一订单
*/
private static final ConcurrentHashMap<Long, Long> ORDER_PROCESSING_LOCK = new ConcurrentHashMap<>();
private static final AtomicLong COMPENSATION_HISTORY_CURSOR_ID = new AtomicLong(0L);
private static final AtomicLong REPAIR_HISTORY_CURSOR_ID = new AtomicLong(0L);
private static final int RECENT_SCAN_SIZE = 200;
private static final int HISTORY_SCAN_SIZE = 200;
private static final int ORDER_UPDATE_FAILED = -1;
private static final int ORDER_ALREADY_SUCCESS = 0;
private static final int ORDER_UPDATED = 1;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -196,48 +205,45 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
// 批量处理订单:先查微信确认状态,避免错误关闭已支付订单
for (PayOrder order : unpaidOrders) {
// 如果订单有prepayId先向微信查询实际状态
if (order.getPrepayId() != null && !order.getPrepayId().isEmpty()) {
try {
String queryResult = wxPayService.queryOrderByOutTradeNo(order.getOutTradeNumber());
if (queryResult != null) {
JSONObject resultJson = JSONUtil.parseObj(queryResult);
String tradeState = resultJson.getStr("trade_state");
if ("SUCCESS".equalsIgnoreCase(tradeState)) {
// 微信侧已支付成功,执行补偿而不是关闭
log.info("关闭订单前查询发现已支付: orderId={}, outTradeNumber={}", order.getId(), order.getOutTradeNumber());
compensateSingleOrder(order, resultJson);
continue;
}
if ("CLOSED".equalsIgnoreCase(tradeState)) {
payOrderMapper.update(null,
new LambdaUpdateWrapper<PayOrder>()
.eq(PayOrder::getId, order.getId())
.set(PayOrder::getOrderStatus, PayOrderStatus.CLOSED)
.set(PayOrder::getTradeState, "CLOSED")
.set(PayOrder::getTradeStateDescription, "微信侧已关闭")
);
log.info("旧订单微信侧已关闭,同步本地状态: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
continue;
}
if ("USERPAYING".equalsIgnoreCase(tradeState)) {
log.info("旧订单仍在支付中,跳过关闭: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
continue;
}
try {
String queryResult = wxPayService.queryOrderByOutTradeNo(order.getOutTradeNumber());
if (queryResult != null) {
JSONObject resultJson = JSONUtil.parseObj(queryResult);
String tradeState = resultJson.getStr("trade_state");
if ("SUCCESS".equalsIgnoreCase(tradeState)) {
// 微信侧已支付成功,执行补偿而不是关闭
log.info("关闭订单前查询发现已支付: orderId={}, outTradeNumber={}", order.getId(), order.getOutTradeNumber());
compensateSingleOrder(order, resultJson);
continue;
}
if ("CLOSED".equalsIgnoreCase(tradeState)) {
payOrderMapper.update(null,
new LambdaUpdateWrapper<PayOrder>()
.eq(PayOrder::getId, order.getId())
.set(PayOrder::getOrderStatus, PayOrderStatus.CLOSED)
.set(PayOrder::getTradeState, "CLOSED")
.set(PayOrder::getTradeStateDescription, "微信侧已关闭")
);
log.info("旧订单微信侧已关闭,同步本地状态: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
continue;
}
if ("USERPAYING".equalsIgnoreCase(tradeState)) {
log.info("旧订单仍在支付中,跳过关闭: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
continue;
}
} catch (Exception e) {
log.warn("关闭订单前查询微信失败,跳过关闭避免误关已支付订单: orderId={}, error={}", order.getId(), e.getMessage());
continue;
}
} catch (Exception e) {
log.warn("关闭订单前查询微信失败,跳过关闭避免误关已支付订单: orderId={}, error={}", order.getId(), e.getMessage());
continue;
}
boolean closed = wxPayService.closeOrder(order.getOutTradeNumber());
if (!closed) {
log.warn("微信关单失败,跳过本地关闭: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
continue;
}
boolean closed = wxPayService.closeOrder(order.getOutTradeNumber());
if (!closed) {
log.warn("微信关单失败,跳过本地关闭: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
continue;
}
// 微信侧确认未支付,安全关闭
@@ -254,7 +260,6 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean handlePaymentSuccess(String outTradeNumber, String transactionId, Integer payerTotal,
String payerCurrency, String successTime, String tradeState,
String tradeStateDescription, String bankType) {
@@ -267,7 +272,9 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
// 2. 检查订单状态,如果已经是成功状态,直接返回
if (PayOrderStatus.SUCCESS.equals(order.getOrderStatus())) {
log.info("订单已处理过: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
int repairCount = repairOrderDeviceDeadTime(order);
log.info("订单已处理过: orderId={}, outTradeNumber={}, repairDeviceCount={}",
order.getId(), outTradeNumber, repairCount);
return true;
}
@@ -291,33 +298,55 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
// 7. 更新订单状态
Integer orderStatus = PayOrderStatusUtil.convertTradeStateToOrderStatus(tradeState);
int updateRows = payOrderMapper.update(null,
new LambdaUpdateWrapper<PayOrder>()
.eq(PayOrder::getId, order.getId())
.ne(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS)
.set(PayOrder::getTransactionId, transactionId)
.set(PayOrder::getPayerTotal, payerTotal)
.set(PayOrder::getPayerCurrency, payerCurrency)
.set(PayOrder::getSuccessTime, successDate)
.set(PayOrder::getTradeState, tradeState)
.set(PayOrder::getTradeStateDescription, tradeStateDescription)
.set(PayOrder::getBankType, bankType)
.set(PayOrder::getOrderStatus, orderStatus)
);
Integer updateResult = transactionTemplate.execute(status -> {
int updateRows = payOrderMapper.update(null,
new LambdaUpdateWrapper<PayOrder>()
.eq(PayOrder::getId, order.getId())
.ne(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS)
.set(PayOrder::getTransactionId, transactionId)
.set(PayOrder::getPayerTotal, payerTotal)
.set(PayOrder::getPayerCurrency, payerCurrency)
.set(PayOrder::getSuccessTime, successDate)
.set(PayOrder::getTradeState, tradeState)
.set(PayOrder::getTradeStateDescription, tradeStateDescription)
.set(PayOrder::getBankType, bankType)
.set(PayOrder::getOrderStatus, orderStatus)
);
if (updateRows <= 0) {
PayOrder latestOrder = payOrderMapper.selectById(order.getId());
if (latestOrder != null && PayOrderStatus.SUCCESS.equals(latestOrder.getOrderStatus())) {
log.info("订单已由其他回调处理成功: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
return true;
if (updateRows <= 0) {
PayOrder latestOrder = payOrderMapper.selectById(order.getId());
if (latestOrder != null && PayOrderStatus.SUCCESS.equals(latestOrder.getOrderStatus())) {
log.info("订单已由其他回调处理成功: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
return ORDER_ALREADY_SUCCESS;
}
log.warn("订单状态更新失败: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
return ORDER_UPDATE_FAILED;
}
log.warn("订单状态更新失败: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
return ORDER_UPDATED;
});
if (updateResult == null || ORDER_UPDATE_FAILED == updateResult) {
return false;
}
// 8. 如果支付成功,更新设备到期时间
if (PayOrderStatus.SUCCESS.equals(orderStatus)) {
updateDeviceDeadTime(order.getId());
if (ORDER_UPDATED == updateResult) {
try {
int updatedDevices = updateDeviceDeadTime(order.getId());
if (updatedDevices <= 0) {
log.warn("订单已支付成功,但本次未更新任何设备有效期,等待修复任务兜底: orderId={}, outTradeNumber={}",
order.getId(), outTradeNumber);
}
} catch (Exception e) {
log.error("订单已支付成功,但更新设备有效期异常,等待修复任务兜底: orderId={}, outTradeNumber={}, error={}",
order.getId(), outTradeNumber, e.getMessage(), e);
}
} else {
int repairCount = repairOrderDeviceDeadTime(order);
log.info("订单已由其他实例处理成功,本次只做设备有效期修复: orderId={}, outTradeNumber={}, repairDeviceCount={}",
order.getId(), outTradeNumber, repairCount);
}
}
log.info("订单支付处理完成: orderId={}, outTradeNumber={}, status={}",
@@ -386,58 +415,101 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
*
* @param orderId 订单ID
*/
private void updateDeviceDeadTime(Long orderId) {
private int updateDeviceDeadTime(Long orderId) {
// 查询订单关联的设备充值记录
List<PayDevice> payDevices = queryPayDevicesByOrderId(orderId);
if (payDevices == null || payDevices.isEmpty()) {
log.warn("订单没有关联的设备充值记录: orderId={}", orderId);
return;
return 0;
}
int updatedCount = 0;
// 批量更新设备到期时间忽略租户过滤因为设备可能由IOT模块写入租户ID可能不一致
for (PayDevice payDevice : payDevices) {
// 仅通过 serialNum 查找设备,不限制 userId因为允许代充支付者可能与设备所有者不同
Device device = TenantHelper.ignore(() ->
deviceMapper.selectOne(
try {
if (payDevice.getAddMonth() == null) {
log.error("设备充值记录缺少续费月数,跳过设备有效期更新: payDeviceId={}, orderId={}, serialNum={}",
payDevice.getId(), orderId, payDevice.getSerialNum());
continue;
}
Device device = queryDeviceByPayDevice(payDevice);
if (device == null) {
log.error("设备不存在,订单成功状态已保留,等待人工核对设备编号: orderId={}, payDeviceId={}, serialNum={}",
orderId, payDevice.getId(), payDevice.getSerialNum());
continue;
}
// 计算新的到期时间
Date newDeadTime = calculateNewDeadTime(device.getDeadTime(), payDevice.getAddMonth());
payDeviceMapper.update(null,
new LambdaUpdateWrapper<PayDevice>()
.eq(PayDevice::getId, payDevice.getId())
.set(PayDevice::getDeadTime, newDeadTime)
);
// 清除 warnCode 中的「设备到期」标志位(续费后恢复正常状态)
Integer oldWarnCode = device.getWarnCode();
Integer newWarnCode = oldWarnCode != null
? (oldWarnCode & ~DefineDeviceWarnCode.DeviceTimeDead)
: 0;
// 更新设备到期时间,并同步清除到期告警位
int updateRows = TenantHelper.ignore(() -> deviceMapper.update(null,
new LambdaUpdateWrapper<Device>()
.eq(Device::getId, device.getId())
.set(Device::getDeadTime, newDeadTime)
.set(Device::getWarnCode, newWarnCode)
));
if (updateRows <= 0) {
log.error("更新设备有效期失败,订单成功状态已保留,等待修复任务兜底: orderId={}, payDeviceId={}, serialNum={}, deviceId={}",
orderId, payDevice.getId(), payDevice.getSerialNum(), device.getId());
continue;
}
updatedCount++;
log.info("续费成功,更新设备到期时间并清除到期告警: deviceId={}, serialNum={}, newDeadTime={}, oldWarnCode={}, newWarnCode={}",
device.getId(), device.getSerialNum(), newDeadTime, oldWarnCode, newWarnCode);
} catch (Exception e) {
log.error("更新单台设备有效期异常,订单成功状态已保留: orderId={}, payDeviceId={}, serialNum={}, error={}",
orderId, payDevice.getId(), payDevice.getSerialNum(), e.getMessage(), e);
}
}
return updatedCount;
}
private Device queryDeviceByPayDevice(PayDevice payDevice) {
return TenantHelper.ignore(() -> {
if (payDevice.getTenantId() != null && !payDevice.getTenantId().isEmpty()) {
List<Device> sameTenantDevices = deviceMapper.selectList(
new LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, payDevice.getSerialNum())
)
);
if (device == null) {
throw new ServiceException("设备不存在: " + payDevice.getSerialNum());
.eq(Device::getTenantId, payDevice.getTenantId())
.orderByDesc(Device::getUpdateTime)
.orderByDesc(Device::getCreateTime)
);
if (sameTenantDevices != null && !sameTenantDevices.isEmpty()) {
return sameTenantDevices.get(0);
}
}
// 计算新的到期时间
Date newDeadTime = calculateNewDeadTime(device.getDeadTime(), payDevice.getAddMonth());
// 清除 warnCode 中的「设备到期」标志位(续费后恢复正常状态)
Integer oldWarnCode = device.getWarnCode();
Integer newWarnCode = oldWarnCode != null
? (oldWarnCode & ~DefineDeviceWarnCode.DeviceTimeDead)
: 0;
// 更新设备到期时间,并同步清除到期告警位
int updateRows = TenantHelper.ignore(() -> deviceMapper.update(null,
new LambdaUpdateWrapper<Device>()
.eq(Device::getId, device.getId())
.set(Device::getDeadTime, newDeadTime)
.set(Device::getWarnCode, newWarnCode)
));
if (updateRows <= 0) {
throw new ServiceException("更新设备有效期失败: " + payDevice.getSerialNum());
}
payDeviceMapper.update(null,
new LambdaUpdateWrapper<PayDevice>()
.eq(PayDevice::getId, payDevice.getId())
.set(PayDevice::getDeadTime, newDeadTime)
List<Device> devices = deviceMapper.selectList(
new LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, payDevice.getSerialNum())
.orderByDesc(Device::getUpdateTime)
.orderByDesc(Device::getCreateTime)
);
log.info("续费成功,更新设备到期时间并清除到期告警: deviceId={}, serialNum={}, newDeadTime={}, oldWarnCode={}, newWarnCode={}",
device.getId(), device.getSerialNum(), newDeadTime, oldWarnCode, newWarnCode);
}
if (devices == null || devices.isEmpty()) {
return null;
}
if (devices.size() > 1) {
log.warn("根据设备编号查到多台设备,已取最近更新的一台: serialNum={}, selectedDeviceId={}, matchedCount={}",
payDevice.getSerialNum(), devices.get(0).getId(), devices.size());
}
return devices.get(0);
});
}
/**
@@ -464,76 +536,143 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
@Override
public int compensateUnpaidOrders() {
// 查询状态为未支付/支付中/支付失败/已关闭已有prepayId且创建时间在1分钟前到48小时内的订单
// 包含CLOSED是因为closeUnpaidOrders可能在微信支付成功前就关闭了订单网络延迟导致查单返回NOTPAY
// 最新订单优先处理,历史订单用游标慢慢补扫,避免老异常数据被固定时间窗口或分页上限漏掉。
Date now = new Date();
Date startTime = new Date(now.getTime() - 48L * 60 * 60 * 1000); // 48小时前
Date endTime = new Date(now.getTime() - 1L * 60 * 1000); // 1分钟前
List<PayOrder> pendingOrders = payOrderMapper.selectList(
new LambdaQueryWrapper<PayOrder>()
.in(PayOrder::getOrderStatus, Arrays.asList(
PayOrderStatus.NOTPAY,
PayOrderStatus.USERPAYING,
PayOrderStatus.PAYERROR,
PayOrderStatus.CLOSED
))
.isNotNull(PayOrder::getPrepayId)
.ne(PayOrder::getPrepayId, "")
.ge(PayOrder::getCreateTime, startTime)
.le(PayOrder::getCreateTime, endTime)
);
Map<Long, PayOrder> candidateMap = new LinkedHashMap<>();
scanRecentCompensationOrders(endTime).forEach(order -> candidateMap.put(order.getId(), order));
scanHistoryCompensationOrders(endTime).forEach(order -> candidateMap.putIfAbsent(order.getId(), order));
if (pendingOrders == null || pendingOrders.isEmpty()) {
if (candidateMap.isEmpty()) {
return 0;
}
log.info("[支付补偿] 扫描到 {} 笔待补偿订单", pendingOrders.size());
int compensatedCount = 0;
for (PayOrder order : pendingOrders) {
try {
String queryResult = wxPayService.queryOrderByOutTradeNo(order.getOutTradeNumber());
if (queryResult == null) {
log.warn("[支付补偿] 查询微信订单失败: outTradeNumber={}", order.getOutTradeNumber());
continue;
}
JSONObject resultJson = JSONUtil.parseObj(queryResult);
String tradeState = resultJson.getStr("trade_state");
if ("SUCCESS".equalsIgnoreCase(tradeState)) {
// 微信侧已支付成功,执行补偿
boolean success = compensateSingleOrder(order, resultJson);
if (success) {
compensatedCount++;
log.info("[支付补偿] 补偿成功: orderId={}, outTradeNumber={}", order.getId(), order.getOutTradeNumber());
}
} else if ("CLOSED".equalsIgnoreCase(tradeState)) {
// 微信侧已关闭,同步关闭本地订单
payOrderMapper.update(null,
new LambdaUpdateWrapper<PayOrder>()
.eq(PayOrder::getId, order.getId())
.set(PayOrder::getOrderStatus, PayOrderStatus.CLOSED)
.set(PayOrder::getTradeState, "CLOSED")
.set(PayOrder::getTradeStateDescription, "微信侧已关闭")
);
log.info("[支付补偿] 订单微信侧已关闭,同步关闭: orderId={}", order.getId());
}
// NOTPAY/USERPAYING 状态不处理,等待下次扫描
} catch (Exception e) {
log.error("[支付补偿] 处理订单异常: orderId={}, outTradeNumber={}, error={}",
order.getId(), order.getOutTradeNumber(), e.getMessage(), e);
for (PayOrder order : candidateMap.values()) {
if (compensateOrderFromWechat(order)) {
compensatedCount++;
}
}
log.info("[支付补偿] 本次扫描 {} 笔待补偿订单", candidateMap.size());
if (compensatedCount > 0) {
log.info("[支付补偿] 本次补偿完成 {} 笔订单", compensatedCount);
}
return compensatedCount;
}
private List<PayOrder> scanRecentCompensationOrders(Date endTime) {
Page<PayOrder> page = payOrderMapper.selectPage(
new Page<>(1, RECENT_SCAN_SIZE, false),
buildCompensationOrderQuery(endTime)
.orderByDesc(PayOrder::getCreateTime)
.orderByDesc(PayOrder::getId)
);
return page.getRecords() != null ? page.getRecords() : Collections.emptyList();
}
private List<PayOrder> scanHistoryCompensationOrders(Date endTime) {
long cursorId = COMPENSATION_HISTORY_CURSOR_ID.get();
LambdaQueryWrapper<PayOrder> queryWrapper = buildCompensationOrderQuery(endTime);
if (cursorId > 0) {
queryWrapper.gt(PayOrder::getId, cursorId);
}
Page<PayOrder> page = payOrderMapper.selectPage(
new Page<>(1, HISTORY_SCAN_SIZE, false),
queryWrapper.orderByAsc(PayOrder::getId)
);
List<PayOrder> records = page.getRecords();
if (records == null || records.isEmpty()) {
if (cursorId > 0) {
COMPENSATION_HISTORY_CURSOR_ID.set(0L);
log.info("[支付补偿] 历史扫描游标已到末尾,下次从最早订单重新扫描");
}
return Collections.emptyList();
}
COMPENSATION_HISTORY_CURSOR_ID.set(records.get(records.size() - 1).getId());
return records;
}
private LambdaQueryWrapper<PayOrder> buildCompensationOrderQuery(Date endTime) {
return new LambdaQueryWrapper<PayOrder>()
.and(wrapper -> wrapper
.in(PayOrder::getOrderStatus, Arrays.asList(
PayOrderStatus.NOTPAY,
PayOrderStatus.USERPAYING,
PayOrderStatus.PAYERROR
))
.or()
.and(closed -> closed
.eq(PayOrder::getOrderStatus, PayOrderStatus.CLOSED)
.and(unconfirmedClosed -> unconfirmedClosed
.isNull(PayOrder::getTradeStateDescription)
.or()
.ne(PayOrder::getTradeStateDescription, "微信侧已关闭")
)
)
.or()
.isNull(PayOrder::getOrderStatus)
)
.isNotNull(PayOrder::getOutTradeNumber)
.ne(PayOrder::getOutTradeNumber, "")
.and(time -> time
.isNull(PayOrder::getCreateTime)
.or()
.le(PayOrder::getCreateTime, endTime)
);
}
private boolean compensateOrderFromWechat(PayOrder order) {
try {
String queryResult = wxPayService.queryOrderByOutTradeNo(order.getOutTradeNumber());
if (queryResult == null) {
log.warn("[支付补偿] 查询微信订单失败: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
return false;
}
JSONObject resultJson = JSONUtil.parseObj(queryResult);
String tradeState = resultJson.getStr("trade_state");
log.info("[支付补偿] 微信查单结果: orderId={}, outTradeNumber={}, localStatus={}, tradeState={}, prepayId={}, createTime={}",
order.getId(), order.getOutTradeNumber(), order.getOrderStatus(), tradeState, order.getPrepayId(), order.getCreateTime());
if ("SUCCESS".equalsIgnoreCase(tradeState)) {
boolean success = compensateSingleOrder(order, resultJson);
if (success) {
log.info("[支付补偿] 补偿成功: orderId={}, outTradeNumber={}", order.getId(), order.getOutTradeNumber());
} else {
log.warn("[支付补偿] 微信已支付但本地补偿失败: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
}
return success;
}
if ("CLOSED".equalsIgnoreCase(tradeState)) {
syncLocalClosedOrder(order.getId(), "微信侧已关闭");
}
return false;
} catch (Exception e) {
log.error("[支付补偿] 处理订单异常: orderId={}, outTradeNumber={}, error={}",
order.getId(), order.getOutTradeNumber(), e.getMessage(), e);
return false;
}
}
private void syncLocalClosedOrder(Long orderId, String description) {
payOrderMapper.update(null,
new LambdaUpdateWrapper<PayOrder>()
.eq(PayOrder::getId, orderId)
.ne(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS)
.set(PayOrder::getOrderStatus, PayOrderStatus.CLOSED)
.set(PayOrder::getTradeState, "CLOSED")
.set(PayOrder::getTradeStateDescription, description)
);
log.info("[支付补偿] 订单微信侧已关闭,同步关闭: orderId={}", orderId);
}
/**
* 对单笔订单执行支付补偿
*
@@ -557,7 +696,7 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
return false;
}
Boolean success = transactionTemplate.execute(status -> handlePaymentSuccess(
return handlePaymentSuccess(
order.getOutTradeNumber(),
transactionId,
payerTotal,
@@ -566,8 +705,7 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
tradeState,
tradeStateDesc,
bankType
));
return Boolean.TRUE.equals(success);
);
}
@Override
@@ -582,7 +720,9 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
return false;
}
if (PayOrderStatus.SUCCESS.equals(order.getOrderStatus())) {
log.info("[支付确认] 订单已成功: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
int repairCount = repairOrderDeviceDeadTime(order);
log.info("[支付确认] 订单已成功: orderId={}, outTradeNumber={}, repairDeviceCount={}",
order.getId(), outTradeNumber, repairCount);
return true;
}
@@ -616,41 +756,83 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
@Override
@Transactional(rollbackFor = Exception.class)
public int repairPaidOrderDeviceDeadTime() {
Date now = new Date();
Date startTime = new Date(now.getTime() - 30L * 24 * 60 * 60 * 1000); // 30天内
Map<Long, PayOrder> paidOrderMap = new LinkedHashMap<>();
scanRecentPaidOrdersForRepair().forEach(order -> paidOrderMap.put(order.getId(), order));
scanHistoryPaidOrdersForRepair().forEach(order -> paidOrderMap.putIfAbsent(order.getId(), order));
List<PayOrder> paidOrders = payOrderMapper.selectList(
new LambdaQueryWrapper<PayOrder>()
.eq(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS)
.ge(PayOrder::getCreateTime, startTime)
);
if (paidOrders == null || paidOrders.isEmpty()) {
if (paidOrderMap.isEmpty()) {
return 0;
}
int repairCount = 0;
for (PayOrder order : paidOrders) {
List<PayDevice> payDevices = queryPayDevicesByOrderId(order.getId());
if (payDevices == null || payDevices.isEmpty()) {
log.warn("[支付有效期修复] 支付成功订单没有设备充值记录: orderId={}", order.getId());
continue;
}
for (PayOrder order : paidOrderMap.values()) {
repairCount += repairOrderDeviceDeadTime(order);
}
for (PayDevice payDevice : payDevices) {
if (repairCount > 0) {
log.warn("[支付有效期修复] 本次修复 {} 台设备", repairCount);
} else {
log.info("[支付有效期修复] 本次扫描 {} 笔支付成功订单,无需修复", paidOrderMap.size());
}
return repairCount;
}
private List<PayOrder> scanRecentPaidOrdersForRepair() {
Page<PayOrder> page = payOrderMapper.selectPage(
new Page<>(1, RECENT_SCAN_SIZE, false),
buildPaidOrderRepairQuery()
.orderByDesc(PayOrder::getCreateTime)
.orderByDesc(PayOrder::getId)
);
return page.getRecords() != null ? page.getRecords() : Collections.emptyList();
}
private List<PayOrder> scanHistoryPaidOrdersForRepair() {
long cursorId = REPAIR_HISTORY_CURSOR_ID.get();
LambdaQueryWrapper<PayOrder> queryWrapper = buildPaidOrderRepairQuery();
if (cursorId > 0) {
queryWrapper.gt(PayOrder::getId, cursorId);
}
Page<PayOrder> page = payOrderMapper.selectPage(
new Page<>(1, HISTORY_SCAN_SIZE, false),
queryWrapper.orderByAsc(PayOrder::getId)
);
List<PayOrder> records = page.getRecords();
if (records == null || records.isEmpty()) {
if (cursorId > 0) {
REPAIR_HISTORY_CURSOR_ID.set(0L);
log.info("[支付有效期修复] 历史扫描游标已到末尾,下次从最早成功订单重新扫描");
}
return Collections.emptyList();
}
REPAIR_HISTORY_CURSOR_ID.set(records.get(records.size() - 1).getId());
return records;
}
private LambdaQueryWrapper<PayOrder> buildPaidOrderRepairQuery() {
return new LambdaQueryWrapper<PayOrder>()
.eq(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS);
}
private int repairOrderDeviceDeadTime(PayOrder order) {
List<PayDevice> payDevices = queryPayDevicesByOrderId(order.getId());
if (payDevices == null || payDevices.isEmpty()) {
log.warn("[支付有效期修复] 支付成功订单没有设备充值记录: orderId={}", order.getId());
return 0;
}
int repairCount = 0;
for (PayDevice payDevice : payDevices) {
try {
if (payDevice.getDeadTime() == null) {
log.warn("[支付有效期修复] 设备充值记录没有目标到期时间: payDeviceId={}, orderId={}",
payDevice.getId(), order.getId());
continue;
}
Device device = TenantHelper.ignore(() ->
deviceMapper.selectOne(
new LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, payDevice.getSerialNum())
)
);
Device device = queryDeviceByPayDevice(payDevice);
if (device == null) {
log.warn("[支付有效期修复] 设备不存在: orderId={}, serialNum={}",
order.getId(), payDevice.getSerialNum());
@@ -678,12 +860,11 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
log.warn("[支付有效期修复] 已修复设备有效期: orderId={}, deviceId={}, serialNum={}, oldDeadTime={}, targetDeadTime={}",
order.getId(), device.getId(), device.getSerialNum(), device.getDeadTime(), payDevice.getDeadTime());
}
} catch (Exception e) {
log.error("[支付有效期修复] 修复单台设备异常: orderId={}, payDeviceId={}, serialNum={}, error={}",
order.getId(), payDevice.getId(), payDevice.getSerialNum(), e.getMessage(), e);
}
}
if (repairCount > 0) {
log.warn("[支付有效期修复] 本次修复 {} 台设备", repairCount);
}
return repairCount;
}
}

View File

@@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
/**
* 支付订单补偿定时任务
* 定期扫描状态为"未支付/支付中/支付失败"但已有prepayId的订单,
* 定期扫描状态为"未支付/支付中/支付失败/已关闭"且有商户订单号的订单,
* 主动向微信支付查询实际状态,对已支付成功但回调丢失的订单进行补偿处理。
*
* 解决问题:用户实际已付款成功,但因网络/服务器等原因未收到微信支付回调通知,
@@ -27,10 +27,9 @@ public class PayOrderCompensationTask {
/**
* 每1分钟执行一次订单补偿扫描
* 查询已创建超过1分钟但未超过48小时的未完成订单
* 向微信主动查单,补偿漏掉的回调。
* 优先扫描最近订单,同时通过历史游标持续补扫老异常订单。
*/
@Scheduled(fixedDelay = 60000, initialDelay = 60000)
// @Scheduled(fixedDelay = 60000, initialDelay = 60000)
public void compensateOrders() {
try {
int count = payOrderBusinessService.compensateUnpaidOrders();
@@ -44,9 +43,9 @@ public class PayOrderCompensationTask {
/**
* 每10分钟修复一次已支付订单的设备有效期。
* 用于兜底处理历史数据或极端情况下订单成功但设备有效期未落库的问题
* 优先扫描最近成功订单,同时通过历史游标持续补扫老成功订单
*/
@Scheduled(fixedDelay = 600000, initialDelay = 120000)
// @Scheduled(fixedDelay = 600000, initialDelay = 120000)
public void repairDeviceDeadTime() {
try {
int count = payOrderBusinessService.repairPaidOrderDeviceDeadTime();