fix: 微信支付问题,新增补偿机制。
This commit is contained in:
@@ -221,6 +221,7 @@ xss:
|
|||||||
# 排除链接
|
# 排除链接
|
||||||
excludeUrls:
|
excludeUrls:
|
||||||
- /system/notice
|
- /system/notice
|
||||||
|
- /weixin/pay_notify
|
||||||
|
|
||||||
--- # 分布式锁 lock4j 全局配置
|
--- # 分布式锁 lock4j 全局配置
|
||||||
lock4j:
|
lock4j:
|
||||||
|
|||||||
@@ -82,4 +82,13 @@ public interface PayOrderBusinessService {
|
|||||||
* @return 是否更新成功
|
* @return 是否更新成功
|
||||||
*/
|
*/
|
||||||
boolean updatePrepayId(Long orderId, String prepayId);
|
boolean updatePrepayId(Long orderId, String prepayId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补偿未处理的支付订单
|
||||||
|
* 主动向微信支付查询状态为“未支付/支付中”但已有prepayId的订单,
|
||||||
|
* 如果微信侧已支付成功则执行回调补偿
|
||||||
|
*
|
||||||
|
* @return 补偿成功的订单数量
|
||||||
|
*/
|
||||||
|
int compensateUnpaidOrders();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,4 +61,13 @@ public interface WxPayService {
|
|||||||
* @return 解密后的明文
|
* @return 解密后的明文
|
||||||
*/
|
*/
|
||||||
String decryptCallbackData(String associatedData, String nonce, String ciphertext);
|
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;
|
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.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.intc.common.core.exception.ServiceException;
|
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.config.WxPayItemProperties;
|
||||||
import com.intc.weixin.constant.PayOrderStatus;
|
import com.intc.weixin.constant.PayOrderStatus;
|
||||||
import com.intc.weixin.service.PayOrderBusinessService;
|
import com.intc.weixin.service.PayOrderBusinessService;
|
||||||
|
import com.intc.weixin.service.WxPayService;
|
||||||
import com.intc.weixin.utils.OrderNumberGenerator;
|
import com.intc.weixin.utils.OrderNumberGenerator;
|
||||||
import com.intc.weixin.utils.PayOrderStatusUtil;
|
import com.intc.weixin.utils.PayOrderStatusUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -41,6 +43,7 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
|
|||||||
private final PayDeviceMapper payDeviceMapper;
|
private final PayDeviceMapper payDeviceMapper;
|
||||||
private final DeviceMapper deviceMapper;
|
private final DeviceMapper deviceMapper;
|
||||||
private final WxPayItemProperties wxPayItemProperties;
|
private final WxPayItemProperties wxPayItemProperties;
|
||||||
|
private final WxPayService wxPayService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单处理锁,防止并发处理同一订单
|
* 订单处理锁,防止并发处理同一订单
|
||||||
@@ -190,8 +193,28 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量关闭订单
|
// 批量处理订单:先查微信确认状态,避免错误关闭已支付订单
|
||||||
for (PayOrder order : unpaidOrders) {
|
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,
|
payOrderMapper.update(null,
|
||||||
new LambdaUpdateWrapper<PayOrder>()
|
new LambdaUpdateWrapper<PayOrder>()
|
||||||
.eq(PayOrder::getId, order.getId())
|
.eq(PayOrder::getId, order.getId())
|
||||||
@@ -394,4 +417,110 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
|
|||||||
return new Date();
|
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 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 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 WxPayProperties wxPayProperties;
|
||||||
private final WxMaProperties wxMaProperties;
|
private final WxMaProperties wxMaProperties;
|
||||||
@@ -126,6 +127,31 @@ public class WxPayServiceImpl implements WxPayService {
|
|||||||
return response.body();
|
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(无响应体,如关闭订单)
|
// 发送 HTTP POST(无响应体,如关闭订单)
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
@@ -155,7 +181,7 @@ public class WxPayServiceImpl implements WxPayService {
|
|||||||
@Override
|
@Override
|
||||||
public String createJsapiOrder(String openId, String outTradeNumber, Integer totalAmount,
|
public String createJsapiOrder(String openId, String outTradeNumber, Integer totalAmount,
|
||||||
String description, String notifyUrl) {
|
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 {
|
try {
|
||||||
String appId = wxMaProperties.getAppId();
|
String appId = wxMaProperties.getAppId();
|
||||||
String mchId = wxPayProperties.getMchId();
|
String mchId = wxPayProperties.getMchId();
|
||||||
@@ -283,4 +309,16 @@ public class WxPayServiceImpl implements WxPayService {
|
|||||||
if (value == null) return "";
|
if (value == null) return "";
|
||||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
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