diff --git a/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/controller/PayNotifyRecordController.java b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/controller/PayNotifyRecordController.java index 6a3100c48..886edae93 100755 --- a/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/controller/PayNotifyRecordController.java +++ b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/controller/PayNotifyRecordController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2026, lengleng All rights reserved. + * Copyright (c) 2018-2025, lengleng All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -19,6 +19,7 @@ package com.pig4cloud.pigx.pay.controller; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -32,9 +33,14 @@ import com.pig4cloud.pigx.common.log.annotation.SysLog; import com.pig4cloud.pigx.common.security.annotation.HasPermission; import com.pig4cloud.pigx.common.security.annotation.Inner; import com.pig4cloud.pigx.common.xss.core.XssCleanIgnore; +import com.pig4cloud.pigx.pay.entity.PayChannel; import com.pig4cloud.pigx.pay.entity.PayNotifyRecord; import com.pig4cloud.pigx.pay.handler.PayNotifyCallbakHandler; +import com.pig4cloud.pigx.pay.service.PayChannelService; import com.pig4cloud.pigx.pay.service.PayNotifyRecordService; +import com.pig4cloud.pigx.pay.utils.PayChannelNameEnum; +import com.pig4cloud.pigx.pay.utils.WeChatPayV3Config; +import com.pig4cloud.pigx.pay.utils.WeChatPayV3NotifyParser; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -44,7 +50,9 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter; import org.springframework.web.bind.annotation.*; @@ -67,6 +75,8 @@ public class PayNotifyRecordController { private final PayNotifyRecordService payNotifyRecordService; + private final PayChannelService payChannelService; + private final PayNotifyCallbakHandler alipayCallback; private final PayNotifyCallbakHandler weChatCallback; @@ -179,11 +189,25 @@ public class PayNotifyRecordController { @XssCleanIgnore @PostMapping("/wx/callbak") @Operation(summary = "微信渠道支付回调", description = "微信渠道支付回调") - public String wxCallbak(HttpServletRequest request) { - String xmlMsg = HttpKit.readData(request); - log.info("微信订单回调信息:{}", xmlMsg); - Map params = WxPayKit.xmlToMap(xmlMsg); - return weChatCallback.handle(params); + public ResponseEntity wxCallbak(HttpServletRequest request) { + String body = HttpKit.readData(request); + log.info("微信 APIV3 订单回调信息:{}", body); + try { + PayChannel channel = payChannelService.getOne(Wrappers.lambdaQuery() + .eq(PayChannel::getChannelId, PayChannelNameEnum.WEIXIN_MP.name()), false); + WeChatPayV3Config config = WeChatPayV3Config.from(channel); + String decryptData = WxPayKit.verifyNotify(request.getHeader("Wechatpay-Serial"), body, + request.getHeader("Wechatpay-Nonce"), request.getHeader("Wechatpay-Timestamp"), + request.getHeader("Wechatpay-Signature"), config.getApiV3Key(), config.platformCertInputStream()); + Map params = WeChatPayV3NotifyParser.parse(decryptData); + weChatCallback.handle(params); + return ResponseEntity.noContent().build(); + } catch (Exception ex) { + log.warn("微信 APIV3 订单回调处理失败", ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON) + .body(JSONUtil.createObj().set("code", "FAIL").set("message", ex.getMessage()).toString()); + } } /** diff --git a/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/AbstractPayOrderHandler.java b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/AbstractPayOrderHandler.java index a86c2e8ac..eb705ddf2 100644 --- a/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/AbstractPayOrderHandler.java +++ b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/AbstractPayOrderHandler.java @@ -60,13 +60,16 @@ public abstract class AbstractPayOrderHandler implements PayOrderHandler { PayChannel payChannel = preparePayParams(); ChannelPayApiConfigKit.put(payChannel); - createGoodsOrder(payGoodsOrder); - PayTradeOrder tradeOrder = createTradeOrder(payGoodsOrder); - Object result = pay(payGoodsOrder, tradeOrder); - updateOrder(payGoodsOrder, tradeOrder); - // 情况ttl - ChannelPayApiConfigKit.remove(); - return result; + try { + createGoodsOrder(payGoodsOrder); + PayTradeOrder tradeOrder = createTradeOrder(payGoodsOrder); + Object result = pay(payGoodsOrder, tradeOrder); + updateOrder(payGoodsOrder, tradeOrder); + return result; + } finally { + // 清理 ThreadLocal + ChannelPayApiConfigKit.remove(); + } } } diff --git a/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/WeChatMpPayOrderHandler.java b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/WeChatMpPayOrderHandler.java index 79859d981..0fbfb25b7 100644 --- a/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/WeChatMpPayOrderHandler.java +++ b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/WeChatMpPayOrderHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2026, lengleng All rights reserved. + * Copyright (c) 2018-2025, lengleng All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -21,13 +21,15 @@ import cn.hutool.extra.servlet.JakartaServletUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.ijpay.core.enums.SignType; -import com.ijpay.core.enums.TradeType; +import com.ijpay.core.IJPayHttpResponse; +import com.ijpay.core.enums.RequestMethodEnum; import com.ijpay.core.kit.WxPayKit; import com.ijpay.wxpay.WxPayApi; -import com.ijpay.wxpay.WxPayApiConfig; -import com.ijpay.wxpay.WxPayApiConfigKit; -import com.ijpay.wxpay.model.UnifiedOrderModel; +import com.ijpay.wxpay.enums.WxDomainEnum; +import com.ijpay.wxpay.enums.v3.BasePayApiEnum; +import com.ijpay.wxpay.model.v3.Amount; +import com.ijpay.wxpay.model.v3.Payer; +import com.ijpay.wxpay.model.v3.UnifiedOrderModel; import com.pig4cloud.pigx.common.core.util.WebUtils; import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; import com.pig4cloud.pigx.pay.entity.PayChannel; @@ -39,13 +41,12 @@ import com.pig4cloud.pigx.pay.mapper.PayTradeOrderMapper; import com.pig4cloud.pigx.pay.utils.ChannelPayApiConfigKit; import com.pig4cloud.pigx.pay.utils.OrderStatusEnum; import com.pig4cloud.pigx.pay.utils.PayChannelNameEnum; +import com.pig4cloud.pigx.pay.utils.WeChatPayV3Config; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.Map; - /** * @author lengleng * @date 2019-05-31 @@ -78,14 +79,8 @@ public class WeChatMpPayOrderHandler extends AbstractPayOrderHandler { throw new IllegalArgumentException("微信公众号支付渠道配置为空"); } - JSONObject params = JSONUtil.parseObj(channel.getParam()); - WxPayApiConfig wx = WxPayApiConfig.builder() - .appId(channel.getAppId()) - .mchId(channel.getChannelMchId()) - .partnerKey(params.getStr("partnerKey")) - .build(); - - WxPayApiConfigKit.setThreadLocalWxPayApiConfig(wx); + // 提前验证 APIV3 配置完整性,避免在写入订单后才抛出配置异常 + WeChatPayV3Config.from(channel); return channel; } @@ -100,7 +95,7 @@ public class WeChatMpPayOrderHandler extends AbstractPayOrderHandler { tradeOrder.setOrderId(goodsOrder.getPayOrderId()); tradeOrder.setAmount(goodsOrder.getAmount()); tradeOrder.setChannelId(PayChannelNameEnum.WEIXIN_MP.getName()); - tradeOrder.setChannelMchId(WxPayApiConfigKit.getWxPayApiConfig().getMchId()); + tradeOrder.setChannelMchId(ChannelPayApiConfigKit.get().getChannelMchId()); tradeOrder.setClientIp(JakartaServletUtil.getClientIP(request)); tradeOrder.setCurrency("CNY"); tradeOrder.setStatus(OrderStatusEnum.INIT.getStatus()); @@ -116,32 +111,38 @@ public class WeChatMpPayOrderHandler extends AbstractPayOrderHandler { */ @Override public Object pay(PayGoodsOrder goodsOrder, PayTradeOrder tradeOrder) { - String ip = JakartaServletUtil.getClientIP(request); - WxPayApiConfig wxPayApiConfig = WxPayApiConfigKit.getWxPayApiConfig(); - - // 判断是否是单体架构 - Map params = UnifiedOrderModel.builder() - .appid(wxPayApiConfig.getAppId()) - .mch_id(wxPayApiConfig.getMchId()) - .nonce_str(WxPayKit.generateStr()) - .body(goodsOrder.getGoodsName()) + WeChatPayV3Config config = WeChatPayV3Config.from(ChannelPayApiConfigKit.get()); + String notifyUrl = String.format("%s/api/%s/notify/wx/callbak", ChannelPayApiConfigKit.get().getNotifyUrl(), + WebUtils.isMicro() ? "pay" : "admin"); + UnifiedOrderModel orderModel = UnifiedOrderModel.builder() + .appid(config.getAppId()) + .mchid(config.getMchId()) + .description(goodsOrder.getGoodsName()) .attach(TenantContextHolder.getTenantId().toString()) .out_trade_no(String.valueOf(tradeOrder.getOrderId())) - .total_fee(goodsOrder.getAmount()) - .spbill_create_ip(ip) - .notify_url(String.format("%s/api/%s/notify/wx/callbak", ChannelPayApiConfigKit.get().getNotifyUrl(), - WebUtils.isMicro() ? "pay" : "admin")) - .trade_type(TradeType.JSAPI.getTradeType()) - .openid(goodsOrder.getUserId()) - .build() - .createSign(wxPayApiConfig.getPartnerKey(), SignType.HMACSHA256); + .notify_url(notifyUrl) + .amount(Amount.builder().total(Integer.parseInt(goodsOrder.getAmount())).currency("CNY").build()) + .payer(Payer.builder().openid(goodsOrder.getUserId()).build()) + .build(); - String xmlResult = WxPayApi.pushOrder(false, params); - log.info("微信统一下单返回 {}", xmlResult); - Map resultMap = WxPayKit.xmlToMap(xmlResult); - String prepayId = resultMap.get("prepay_id"); - return WxPayKit.prepayIdCreateSign(prepayId, wxPayApiConfig.getAppId(), wxPayApiConfig.getPartnerKey(), - SignType.HMACSHA256); + try { + IJPayHttpResponse response = WxPayApi.v3(RequestMethodEnum.POST, WxDomainEnum.CHINA.getDomain(), + BasePayApiEnum.JS_API_PAY.getUrl(), config.getMchId(), config.getMerchantSerialNo(), null, + config.getPrivateKey(), JSONUtil.toJsonStr(orderModel)); + log.info("微信 APIV3 JSAPI 下单返回: status={}, body={}", response.getStatus(), response.getBody()); + if (response.getStatus() < 200 || response.getStatus() >= 300) { + throw new IllegalStateException("微信支付 APIV3 下单失败: " + response.getBody()); + } + + JSONObject result = JSONUtil.parseObj(response.getBody()); + String prepayId = result.getStr("prepay_id"); + if (prepayId == null) { + throw new IllegalStateException("微信支付 APIV3 下单未返回 prepay_id"); + } + return WxPayKit.jsApiCreateSign(config.getAppId(), prepayId, config.getPrivateKey()); + } catch (Exception ex) { + throw new IllegalStateException("微信支付 APIV3 下单失败", ex); + } } /** diff --git a/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/YungouosMergePayOrderHandler.java b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/YungouosMergePayOrderHandler.java index 23d3cdc63..018e2b99d 100644 --- a/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/YungouosMergePayOrderHandler.java +++ b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/handler/impl/YungouosMergePayOrderHandler.java @@ -100,7 +100,7 @@ public class YungouosMergePayOrderHandler extends AbstractPayOrderHandler { return MergePay.nativePay(String.valueOf(tradeOrder.getOrderId()), money, channel.getChannelMchId(), tradeOrder.getBody(), "1", TenantContextHolder.getTenantId().toString(), String.format("%s/api/%s/notify/merge/callbak", ChannelPayApiConfigKit.get().getNotifyUrl(), WebUtils.isMicro() ? "pay" : "admin"), - ChannelPayApiConfigKit.get().getReturnUrl(), "", "", "0", "pay", null, key); + ChannelPayApiConfigKit.get().getReturnUrl(), "", "", "0", null, null, key); } /** diff --git a/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/utils/WeChatPayV3Config.java b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/utils/WeChatPayV3Config.java new file mode 100644 index 000000000..3cada2929 --- /dev/null +++ b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/utils/WeChatPayV3Config.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.pig4cloud.pigx.pay.utils; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.ijpay.core.enums.AuthTypeEnum; +import com.ijpay.core.kit.PayKit; +import com.pig4cloud.pigx.pay.entity.PayChannel; +import lombok.Getter; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; + +/** + * 微信支付 APIV3 渠道配置 + * + * @author lengleng + * @date 2026-05-10 + */ +@Getter +public class WeChatPayV3Config { + + /** + * 公众号 AppID。 + */ + private final String appId; + + /** + * 微信支付商户号(channelMchId)。 + */ + private final String mchId; + + /** + * APIv3 密钥,32 位字符串,用于回调解密。 + */ + private final String apiV3Key; + + /** + * 商户 API 证书序列号,用于签名请求头。 + */ + private final String merchantSerialNo; + + /** + * 商户私钥 PEM 文本,换行符已从 \n 还原为真实换行。 + */ + private final String privateKeyContent; + + /** + * 微信平台证书 PEM 文本,换行符已从 \n 还原为真实换行。 + */ + private final String platformCertContent; + + /** + * 解析后的商户私钥对象,用于 APIV3 签名。 + */ + private final PrivateKey privateKey; + + private WeChatPayV3Config(String appId, String mchId, String apiV3Key, String merchantSerialNo, + String privateKeyContent, String platformCertContent, PrivateKey privateKey) { + this.appId = appId; + this.mchId = mchId; + this.apiV3Key = apiV3Key; + this.merchantSerialNo = merchantSerialNo; + this.privateKeyContent = privateKeyContent; + this.platformCertContent = platformCertContent; + this.privateKey = privateKey; + } + + /** + * 从渠道配置构建 APIV3 配置对象,并验证必填字段。 + * + * @param channel 支付渠道配置,不允许为空;param JSON 需含 apiV3Key / merchantSerialNo / privateKey / platformCert + * @return 解析完成的配置对象 + * @throws IllegalArgumentException 配置缺失或私钥解析失败 + */ + public static WeChatPayV3Config from(PayChannel channel) { + if (channel == null) { + throw new IllegalArgumentException("微信公众号支付渠道配置为空"); + } + + JSONObject params = JSONUtil.parseObj(channel.getParam()); + String appId = require(channel.getAppId(), "appId"); + String mchId = require(channel.getChannelMchId(), "channelMchId"); + String apiV3Key = require(params.getStr("apiV3Key"), "param.apiV3Key"); + String merchantSerialNo = require(params.getStr("merchantSerialNo"), "param.merchantSerialNo"); + String privateKeyContent = normalizePem(require(params.getStr("privateKey"), "param.privateKey")); + String platformCertContent = normalizePem(require(params.getStr("platformCert"), "param.platformCert")); + + try { + PrivateKey privateKey = PayKit.getPrivateKeyByKeyContent(privateKeyContent, AuthTypeEnum.RSA.getCode()); + return new WeChatPayV3Config(appId, mchId, apiV3Key, merchantSerialNo, privateKeyContent, + platformCertContent, privateKey); + } catch (Exception ex) { + throw new IllegalArgumentException("微信支付 APIV3 私钥解析失败", ex); + } + } + + /** + * 返回平台证书内容的字节流,用于传入 WxPayKit.verifyNotify 验签。 + * + * @return 平台证书 UTF-8 字节流,每次调用返回新实例 + */ + public InputStream platformCertInputStream() { + return new ByteArrayInputStream(platformCertContent.getBytes(StandardCharsets.UTF_8)); + } + + static String normalizePem(String value) { + return value.replace("\\n", "\n").trim(); + } + + private static String require(String value, String field) { + if (StrUtil.isBlank(value)) { + throw new IllegalArgumentException("微信支付 APIV3 配置缺失: " + field); + } + return value; + } + +} diff --git a/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/utils/WeChatPayV3NotifyParser.java b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/utils/WeChatPayV3NotifyParser.java new file mode 100644 index 000000000..0daa631f0 --- /dev/null +++ b/pigx-visual/pigx-pay-platform/src/main/java/com/pig4cloud/pigx/pay/utils/WeChatPayV3NotifyParser.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.pig4cloud.pigx.pay.utils; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 微信支付 APIV3 回调报文适配器 + * + * @author lengleng + * @date 2026-05-10 + */ +public final class WeChatPayV3NotifyParser { + + private WeChatPayV3NotifyParser() { + } + + /** + * 将微信 APIV3 回调解密后的 JSON 转换为旧版回调处理器兼容的 Map。 + *

trade_state 为 SUCCESS 时 result_code=SUCCESS,否则为 FAIL。 + * + * @param decryptData WxPayKit.verifyNotify 解密后的 JSON 字符串,不允许为空 + * @return 包含 out_trade_no / result_code / openid / total_fee 等字段的参数 Map + */ + public static Map parse(String decryptData) { + JSONObject data = JSONUtil.parseObj(decryptData); + Map params = new LinkedHashMap<>(); + String tradeState = data.getStr("trade_state"); + + params.put(PayConstants.OUT_TRADE_NO, data.getStr("out_trade_no")); + params.put("transaction_id", data.getStr("transaction_id")); + params.put("attach", data.getStr("attach")); + params.put("trade_state", tradeState); + params.put("trade_state_desc", data.getStr("trade_state_desc")); + params.put("success_time", data.getStr("success_time")); + params.put(PayConstants.RESULT_CODE, StrUtil.equals("SUCCESS", tradeState) ? "SUCCESS" : "FAIL"); + params.put("err_code", tradeState); + params.put("err_code_des", data.getStr("trade_state_desc")); + + JSONObject payer = data.getJSONObject("payer"); + if (payer != null) { + params.put("openid", payer.getStr("openid")); + } + + JSONObject amount = data.getJSONObject("amount"); + if (amount != null) { + params.put("total_fee", amount.getStr("total")); + params.put("payer_total", amount.getStr("payer_total")); + params.put("currency", amount.getStr("currency")); + params.put("payer_currency", amount.getStr("payer_currency")); + } + + return params; + } + +} diff --git a/pigx-visual/pigx-pay-platform/src/main/resources/mapper/PayChannelMapper.xml b/pigx-visual/pigx-pay-platform/src/main/resources/mapper/PayChannelMapper.xml index 05befc3a8..718a49b9f 100755 --- a/pigx-visual/pigx-pay-platform/src/main/resources/mapper/PayChannelMapper.xml +++ b/pigx-visual/pigx-pay-platform/src/main/resources/mapper/PayChannelMapper.xml @@ -2,7 +2,7 @@