From 9cf15af6723173ddcc6e83d5c5aeeed5e1976cd7 Mon Sep 17 00:00:00 2001 From: tianyongbao Date: Mon, 1 Jun 2026 00:17:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=8C=E6=96=B0=E5=A2=9E=E8=A1=A5=E5=81=BF?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- intc-admin/src/main/resources/application.yml | 1 + .../service/PayOrderBusinessService.java | 9 ++ .../com/intc/weixin/service/WxPayService.java | 9 ++ .../impl/PayOrderBusinessServiceImpl.java | 133 +++++++++++++++++- .../weixin/service/impl/WxPayServiceImpl.java | 40 +++++- .../weixin/task/PayOrderCompensationTask.java | 44 ++++++ 6 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 intc-modules/intc-weixin/src/main/java/com/intc/weixin/task/PayOrderCompensationTask.java diff --git a/intc-admin/src/main/resources/application.yml b/intc-admin/src/main/resources/application.yml index c8e6981..860b173 100644 --- a/intc-admin/src/main/resources/application.yml +++ b/intc-admin/src/main/resources/application.yml @@ -221,6 +221,7 @@ xss: # 排除链接 excludeUrls: - /system/notice + - /weixin/pay_notify --- # 分布式锁 lock4j 全局配置 lock4j: diff --git a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/PayOrderBusinessService.java b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/PayOrderBusinessService.java index 78a2fbc..e381902 100644 --- a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/PayOrderBusinessService.java +++ b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/PayOrderBusinessService.java @@ -82,4 +82,13 @@ public interface PayOrderBusinessService { * @return 是否更新成功 */ boolean updatePrepayId(Long orderId, String prepayId); + + /** + * 补偿未处理的支付订单 + * 主动向微信支付查询状态为“未支付/支付中”但已有prepayId的订单, + * 如果微信侧已支付成功则执行回调补偿 + * + * @return 补偿成功的订单数量 + */ + int compensateUnpaidOrders(); } diff --git a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/WxPayService.java b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/WxPayService.java index 1b700e0..24b6644 100644 --- a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/WxPayService.java +++ b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/WxPayService.java @@ -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); } diff --git a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/PayOrderBusinessServiceImpl.java b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/PayOrderBusinessServiceImpl.java index 71fdc9c..4d4b410 100644 --- a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/PayOrderBusinessServiceImpl.java +++ b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/PayOrderBusinessServiceImpl.java @@ -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() .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 pendingOrders = payOrderMapper.selectList( + new LambdaQueryWrapper() + .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() + .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 + ); + } } diff --git a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/WxPayServiceImpl.java b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/WxPayServiceImpl.java index a56f995..b9867d1 100644 --- a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/WxPayServiceImpl.java +++ b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/WxPayServiceImpl.java @@ -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 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; + } + } } diff --git a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/task/PayOrderCompensationTask.java b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/task/PayOrderCompensationTask.java new file mode 100644 index 0000000..898c492 --- /dev/null +++ b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/task/PayOrderCompensationTask.java @@ -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); + } + } +}