fix: 微信支付问题,新增补偿机制。
This commit is contained in:
@@ -221,6 +221,7 @@ xss:
|
||||
# 排除链接
|
||||
excludeUrls:
|
||||
- /system/notice
|
||||
- /weixin/pay_notify
|
||||
|
||||
--- # 分布式锁 lock4j 全局配置
|
||||
lock4j:
|
||||
|
||||
@@ -82,4 +82,13 @@ public interface PayOrderBusinessService {
|
||||
* @return 是否更新成功
|
||||
*/
|
||||
boolean updatePrepayId(Long orderId, String prepayId);
|
||||
|
||||
/**
|
||||
* 补偿未处理的支付订单
|
||||
* 主动向微信支付查询状态为“未支付/支付中”但已有prepayId的订单,
|
||||
* 如果微信侧已支付成功则执行回调补偿
|
||||
*
|
||||
* @return 补偿成功的订单数量
|
||||
*/
|
||||
int compensateUnpaidOrders();
|
||||
}
|
||||
|
||||
@@ -61,4 +61,13 @@ public interface WxPayService {
|
||||
* @return 解密后的明文
|
||||
*/
|
||||
String decryptCallbackData(String associatedData, String nonce, String ciphertext);
|
||||
|
||||
/**
|
||||
* 主动查询微信支付订单状态
|
||||
* 通过商户订单号向微信支付平台查询订单的实际支付状态
|
||||
*
|
||||
* @param outTradeNumber 商户订单号
|
||||
* @return 微信返回的订单JSON字符串,查询失败返回null
|
||||
*/
|
||||
String queryOrderByOutTradeNo(String outTradeNumber);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.intc.weixin.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.intc.common.core.exception.ServiceException;
|
||||
@@ -15,8 +14,11 @@ import com.intc.fishery.mapper.PayOrderMapper;
|
||||
import com.intc.weixin.config.WxPayItemProperties;
|
||||
import com.intc.weixin.constant.PayOrderStatus;
|
||||
import com.intc.weixin.service.PayOrderBusinessService;
|
||||
import com.intc.weixin.service.WxPayService;
|
||||
import com.intc.weixin.utils.OrderNumberGenerator;
|
||||
import com.intc.weixin.utils.PayOrderStatusUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -41,6 +43,7 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
|
||||
private final PayDeviceMapper payDeviceMapper;
|
||||
private final DeviceMapper deviceMapper;
|
||||
private final WxPayItemProperties wxPayItemProperties;
|
||||
private final WxPayService wxPayService;
|
||||
|
||||
/**
|
||||
* 订单处理锁,防止并发处理同一订单
|
||||
@@ -190,8 +193,28 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量关闭订单
|
||||
// 批量处理订单:先查微信确认状态,避免错误关闭已支付订单
|
||||
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;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("关闭订单前查询微信失败,直接关闭: orderId={}, error={}", order.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 微信侧确认未支付,安全关闭
|
||||
payOrderMapper.update(null,
|
||||
new LambdaUpdateWrapper<PayOrder>()
|
||||
.eq(PayOrder::getId, order.getId())
|
||||
@@ -394,4 +417,110 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compensateUnpaidOrders() {
|
||||
// 查询状态为未支付/支付中/已关闭,已有prepayId,且创建时间在5分钟前到24小时内的订单
|
||||
// 包含CLOSED是因为closeUnpaidOrders可能在微信支付成功前就关闭了订单(网络延迟导致查单返回NOTPAY)
|
||||
Date now = new Date();
|
||||
Date startTime = new Date(now.getTime() - 24L * 60 * 60 * 1000); // 24小时前
|
||||
Date endTime = new Date(now.getTime() - 5L * 60 * 1000); // 5分钟前
|
||||
|
||||
List<PayOrder> pendingOrders = payOrderMapper.selectList(
|
||||
new LambdaQueryWrapper<PayOrder>()
|
||||
.in(PayOrder::getOrderStatus, Arrays.asList(
|
||||
PayOrderStatus.NOTPAY,
|
||||
PayOrderStatus.USERPAYING,
|
||||
PayOrderStatus.CLOSED
|
||||
))
|
||||
.isNotNull(PayOrder::getPrepayId)
|
||||
.ne(PayOrder::getPrepayId, "")
|
||||
.ge(PayOrder::getCreateTime, startTime)
|
||||
.le(PayOrder::getCreateTime, endTime)
|
||||
);
|
||||
|
||||
if (pendingOrders == null || pendingOrders.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);
|
||||
}
|
||||
}
|
||||
|
||||
if (compensatedCount > 0) {
|
||||
log.info("[支付补偿] 本次补偿完成 {} 笔订单", compensatedCount);
|
||||
}
|
||||
return compensatedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对单笔订单执行支付补偿
|
||||
*
|
||||
* @param order 本地订单
|
||||
* @param wxResult 微信查询结果
|
||||
* @return 是否补偿成功
|
||||
*/
|
||||
private boolean compensateSingleOrder(PayOrder order, JSONObject wxResult) {
|
||||
String transactionId = wxResult.getStr("transaction_id");
|
||||
String tradeState = wxResult.getStr("trade_state");
|
||||
String tradeStateDesc = wxResult.getStr("trade_state_desc");
|
||||
String successTime = wxResult.getStr("success_time");
|
||||
String bankType = wxResult.getStr("bank_type");
|
||||
|
||||
JSONObject amountObj = wxResult.getJSONObject("amount");
|
||||
Integer payerTotal = amountObj != null ? amountObj.getInt("payer_total") : null;
|
||||
String payerCurrency = amountObj != null ? amountObj.getStr("payer_currency") : "CNY";
|
||||
|
||||
if (payerTotal == null) {
|
||||
log.error("[支付补偿] 微信返回金额为空: orderId={}", order.getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
return handlePaymentSuccess(
|
||||
order.getOutTradeNumber(),
|
||||
transactionId,
|
||||
payerTotal,
|
||||
payerCurrency,
|
||||
successTime,
|
||||
tradeState,
|
||||
tradeStateDesc,
|
||||
bankType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public class WxPayServiceImpl implements WxPayService {
|
||||
|
||||
private static final String JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
|
||||
private static final String CLOSE_URL_TEMPLATE = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s/close";
|
||||
private static final String QUERY_ORDER_URL_TEMPLATE = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s?mchid=%s";
|
||||
|
||||
private final WxPayProperties wxPayProperties;
|
||||
private final WxMaProperties wxMaProperties;
|
||||
@@ -126,6 +127,31 @@ public class WxPayServiceImpl implements WxPayService {
|
||||
return response.body();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 发送 HTTP GET
|
||||
// ----------------------------------------------------------------
|
||||
private String doGet(String url) throws Exception {
|
||||
URI uri = URI.create(url);
|
||||
String urlPath = uri.getPath();
|
||||
if (uri.getQuery() != null) {
|
||||
urlPath = urlPath + "?" + uri.getQuery();
|
||||
}
|
||||
String authorization = buildAuthorization("GET", urlPath, "");
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(uri)
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", authorization)
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
log.info("微信支付查询响应: status={}, body={}", response.statusCode(), response.body());
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new RuntimeException("微信支付查询失败: " + response.statusCode() + " " + response.body());
|
||||
}
|
||||
return response.body();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 发送 HTTP POST(无响应体,如关闭订单)
|
||||
// ----------------------------------------------------------------
|
||||
@@ -155,7 +181,7 @@ public class WxPayServiceImpl implements WxPayService {
|
||||
@Override
|
||||
public String createJsapiOrder(String openId, String outTradeNumber, Integer totalAmount,
|
||||
String description, String notifyUrl) {
|
||||
log.info("创建JSAPI支付订单: openId={}, outTradeNumber={}, amount={}", openId, outTradeNumber, totalAmount);
|
||||
log.info("创建JSAPI支付订单: openId={}, outTradeNumber={}, amount={}, notifyUrl={}", openId, outTradeNumber, totalAmount, notifyUrl);
|
||||
try {
|
||||
String appId = wxMaProperties.getAppId();
|
||||
String mchId = wxPayProperties.getMchId();
|
||||
@@ -283,4 +309,16 @@ public class WxPayServiceImpl implements WxPayService {
|
||||
if (value == null) return "";
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String queryOrderByOutTradeNo(String outTradeNumber) {
|
||||
log.info("主动查询订单状态: outTradeNumber={}", outTradeNumber);
|
||||
try {
|
||||
String url = String.format(QUERY_ORDER_URL_TEMPLATE, outTradeNumber, wxPayProperties.getMchId());
|
||||
return doGet(url);
|
||||
} catch (Exception e) {
|
||||
log.error("查询订单状态失败: outTradeNumber={}, error={}", outTradeNumber, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.intc.weixin.task;
|
||||
|
||||
import com.intc.weixin.service.PayOrderBusinessService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 支付订单补偿定时任务
|
||||
* 定期扫描状态为"未支付/支付中"但已有prepayId的订单,
|
||||
* 主动向微信支付查询实际状态,对已支付成功但回调丢失的订单进行补偿处理。
|
||||
*
|
||||
* 解决问题:用户实际已付款成功,但因网络/服务器等原因未收到微信支付回调通知,
|
||||
* 导致订单一直停留在未支付状态。
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(PayOrderBusinessService.class)
|
||||
public class PayOrderCompensationTask {
|
||||
|
||||
private final PayOrderBusinessService payOrderBusinessService;
|
||||
|
||||
/**
|
||||
* 每5分钟执行一次订单补偿扫描
|
||||
* 查询已创建超过5分钟但未超过24小时的未完成订单,
|
||||
* 向微信主动查单,补偿漏掉的回调。
|
||||
*/
|
||||
@Scheduled(fixedDelay = 300000)
|
||||
public void compensateOrders() {
|
||||
try {
|
||||
int count = payOrderBusinessService.compensateUnpaidOrders();
|
||||
if (count > 0) {
|
||||
log.info("[支付补偿定时任务] 本次补偿了 {} 笔订单", count);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[支付补偿定时任务] 执行异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user