fix: 微信支付,改为http。
This commit is contained in:
@@ -1,81 +1,19 @@
|
|||||||
package com.intc.weixin.config;
|
package com.intc.weixin.config;
|
||||||
|
|
||||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
|
||||||
import com.github.binarywang.wxpay.service.WxPayService;
|
|
||||||
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 微信支付配置
|
* 微信支付配置(原生HTTP实现,无需SDK Bean)
|
||||||
|
* 配置参数通过 WxPayProperties 和 WxMaProperties 自动注入
|
||||||
*
|
*
|
||||||
* @author intc
|
* @author intc
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
@RequiredArgsConstructor
|
|
||||||
@ConditionalOnProperty(prefix = "wx.pay", name = "mch-id")
|
@ConditionalOnProperty(prefix = "wx.pay", name = "mch-id")
|
||||||
public class WxPayConfiguration {
|
public class WxPayConfiguration {
|
||||||
|
// 原生HTTP实现,无需初始化SDK的WxPayService Bean
|
||||||
private final WxPayProperties wxPayProperties;
|
// 配置参数自动注入到 WxPayProperties 和 WxMaProperties
|
||||||
private final WxMaProperties wxMaProperties;
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public WxPayService wxPayService() {
|
|
||||||
WxPayConfig payConfig = new WxPayConfig();
|
|
||||||
|
|
||||||
// 基础配置
|
|
||||||
payConfig.setAppId(wxMaProperties.getAppId());
|
|
||||||
payConfig.setMchId(wxPayProperties.getMchId());
|
|
||||||
|
|
||||||
// V2配置
|
|
||||||
if (wxPayProperties.getMchKey() != null) {
|
|
||||||
payConfig.setMchKey(wxPayProperties.getMchKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
// V3配置
|
|
||||||
if (wxPayProperties.getApiV3Key() != null) {
|
|
||||||
payConfig.setApiV3Key(wxPayProperties.getApiV3Key());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wxPayProperties.getCertSerialNo() != null) {
|
|
||||||
payConfig.setCertSerialNo(wxPayProperties.getCertSerialNo());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 证书配置
|
|
||||||
if (wxPayProperties.getKeyPath() != null) {
|
|
||||||
payConfig.setKeyPath(wxPayProperties.getKeyPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 私钥配置:优先使用privateContent,避免文件不存在的问题
|
|
||||||
if (wxPayProperties.getPrivateContent() != null && !wxPayProperties.getPrivateContent().isEmpty()) {
|
|
||||||
payConfig.setPrivateKeyContent(wxPayProperties.getPrivateContent().getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
|
||||||
log.info("使用私钥内容配置");
|
|
||||||
} else if (wxPayProperties.getPrivateKeyPath() != null) {
|
|
||||||
payConfig.setPrivateKeyPath(wxPayProperties.getPrivateKeyPath());
|
|
||||||
log.info("使用私钥路径配置: {}", wxPayProperties.getPrivateKeyPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 商户证书配置(apiclient_cert.pem),有真实PEM内容才设置,避免占位符导致v3请求构造异常
|
|
||||||
String privateCertContent = wxPayProperties.getPrivateCertContent();
|
|
||||||
if (privateCertContent != null && privateCertContent.startsWith("-----BEGIN CERTIFICATE-----")) {
|
|
||||||
payConfig.setPrivateCertContent(privateCertContent.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
|
||||||
log.info("使用商户证书内容配置");
|
|
||||||
} else if (wxPayProperties.getPrivateCertPath() != null) {
|
|
||||||
payConfig.setPrivateCertPath(wxPayProperties.getPrivateCertPath());
|
|
||||||
log.info("使用商户证书路径配置: {}", wxPayProperties.getPrivateCertPath());
|
|
||||||
} else {
|
|
||||||
log.info("未配置商户证书,将使用私钥直接签名");
|
|
||||||
}
|
|
||||||
|
|
||||||
WxPayService wxPayService = new WxPayServiceImpl();
|
|
||||||
wxPayService.setConfig(payConfig);
|
|
||||||
|
|
||||||
log.info("微信支付服务初始化完成,商户号: {}", wxPayProperties.getMchId());
|
|
||||||
|
|
||||||
return wxPayService;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,31 @@
|
|||||||
package com.intc.weixin.service.impl;
|
package com.intc.weixin.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
import com.intc.weixin.config.WxPayProperties;
|
||||||
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
import com.intc.weixin.config.WxMaProperties;
|
||||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
|
||||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
|
||||||
import com.github.binarywang.wxpay.v3.util.AesUtils;
|
|
||||||
import com.intc.weixin.service.WxPayService;
|
import com.intc.weixin.service.WxPayService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 微信支付服务实现
|
* 微信支付服务实现(原生HTTP+RSA私钥签名,无需apiclient_cert.pem)
|
||||||
*
|
*
|
||||||
* @author intc
|
* @author intc
|
||||||
*/
|
*/
|
||||||
@@ -26,154 +34,172 @@ import java.util.Map;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class WxPayServiceImpl implements WxPayService {
|
public class WxPayServiceImpl implements WxPayService {
|
||||||
|
|
||||||
@Autowired(required = false)
|
private static final String JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
|
||||||
private com.github.binarywang.wxpay.service.WxPayService wxPayService;
|
private static final String CLOSE_URL_TEMPLATE = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s/close";
|
||||||
|
|
||||||
|
private final WxPayProperties wxPayProperties;
|
||||||
|
private final WxMaProperties wxMaProperties;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 私钥加载(懒加载,只解析一次)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
private volatile PrivateKey cachedPrivateKey;
|
||||||
|
|
||||||
|
private PrivateKey getPrivateKey() {
|
||||||
|
if (cachedPrivateKey != null) {
|
||||||
|
return cachedPrivateKey;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
if (cachedPrivateKey != null) {
|
||||||
|
return cachedPrivateKey;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String content = wxPayProperties.getPrivateContent();
|
||||||
|
if (content == null || content.isEmpty()) {
|
||||||
|
throw new IllegalStateException("wx.pay.private-content 未配置");
|
||||||
|
}
|
||||||
|
// 去掉 PEM 头尾和换行
|
||||||
|
String pem = content
|
||||||
|
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||||
|
.replace("-----END PRIVATE KEY-----", "")
|
||||||
|
.replaceAll("\\s+", "");
|
||||||
|
byte[] keyBytes = Base64.getDecoder().decode(pem);
|
||||||
|
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
||||||
|
cachedPrivateKey = KeyFactory.getInstance("RSA").generatePrivate(spec);
|
||||||
|
log.info("微信支付私钥加载成功");
|
||||||
|
return cachedPrivateKey;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("加载微信支付私钥失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// RSA-SHA256 签名
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
private String rsaSign(String content) {
|
||||||
|
try {
|
||||||
|
Signature signer = Signature.getInstance("SHA256withRSA");
|
||||||
|
signer.initSign(getPrivateKey());
|
||||||
|
signer.update(content.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return Base64.getEncoder().encodeToString(signer.sign());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("微信支付签名失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 构造 Authorization 请求头
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
private String buildAuthorization(String method, String urlPath, String body) {
|
||||||
|
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
|
||||||
|
String nonce = RandomUtil.randomString(32);
|
||||||
|
String message = method + "\n" + urlPath + "\n" + timestamp + "\n" + nonce + "\n" + body + "\n";
|
||||||
|
String signature = rsaSign(message);
|
||||||
|
return String.format(
|
||||||
|
"WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%s\",serial_no=\"%s\",signature=\"%s\"",
|
||||||
|
wxPayProperties.getMchId(), nonce, timestamp,
|
||||||
|
wxPayProperties.getCertSerialNo(), signature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 发送 HTTP POST
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
private String doPost(String url, String jsonBody) throws Exception {
|
||||||
|
URI uri = URI.create(url);
|
||||||
|
String urlPath = uri.getPath();
|
||||||
|
String authorization = buildAuthorization("POST", urlPath, jsonBody);
|
||||||
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(uri)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", authorization)
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
|
log.info("微信支付API响应: 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(无响应体,如关闭订单)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
private void doPostNoBody(String url, String jsonBody) throws Exception {
|
||||||
|
URI uri = URI.create(url);
|
||||||
|
String urlPath = uri.getPath();
|
||||||
|
String authorization = buildAuthorization("POST", urlPath, jsonBody);
|
||||||
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(uri)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", authorization)
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
|
log.info("关闭订单响应: status={}", response.statusCode());
|
||||||
|
if (response.statusCode() >= 300) {
|
||||||
|
throw new RuntimeException("关闭订单失败: " + response.statusCode() + " " + response.body());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 接口实现
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
@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={}, totalAmount={}, description={}",
|
log.info("创建JSAPI支付订单: openId={}, outTradeNumber={}, amount={}", openId, outTradeNumber, totalAmount);
|
||||||
openId, outTradeNumber, totalAmount, description);
|
|
||||||
|
|
||||||
// 检查微信支付服务是否可用
|
|
||||||
if (wxPayService == null) {
|
|
||||||
log.warn("微信支付SDK未配置,返回模拟数据");
|
|
||||||
return "mock_prepay_id_" + System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 构建请求对象
|
String appId = wxMaProperties.getAppId();
|
||||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
String mchId = wxPayProperties.getMchId();
|
||||||
request.setOutTradeNo(outTradeNumber);
|
// 构造请求JSON
|
||||||
request.setDescription(description);
|
String jsonBody = String.format(
|
||||||
request.setNotifyUrl(notifyUrl);
|
"{\"appid\":\"%s\",\"mchid\":\"%s\",\"description\":\"%s\",\"out_trade_no\":\"%s\","
|
||||||
|
+ "\"notify_url\":\"%s\",\"amount\":{\"total\":%d,\"currency\":\"CNY\"},"
|
||||||
// 设置金额
|
+ "\"payer\":{\"openid\":\"%s\"}}",
|
||||||
WxPayUnifiedOrderV3Request.Amount amount = new WxPayUnifiedOrderV3Request.Amount();
|
appId, mchId, escapeJson(description), outTradeNumber,
|
||||||
amount.setTotal(totalAmount);
|
notifyUrl, totalAmount, openId
|
||||||
amount.setCurrency("CNY");
|
);
|
||||||
request.setAmount(amount);
|
String resp = doPost(JSAPI_URL, jsonBody);
|
||||||
|
// 解析 prepay_id
|
||||||
// 设置支付者
|
String prepayId = extractJsonValue(resp, "prepay_id");
|
||||||
WxPayUnifiedOrderV3Request.Payer payer = new WxPayUnifiedOrderV3Request.Payer();
|
if (prepayId == null || prepayId.isEmpty()) {
|
||||||
payer.setOpenid(openId);
|
throw new RuntimeException("微信支付返回中未找到prepay_id: " + resp);
|
||||||
request.setPayer(payer);
|
|
||||||
|
|
||||||
// 调用微信支付API - 注意:4.6.5.B 版本返回 WxPayUnifiedOrderV3Result.JsapiResult
|
|
||||||
WxPayUnifiedOrderV3Result.JsapiResult result = wxPayService.createOrderV3(
|
|
||||||
TradeTypeEnum.JSAPI, request);
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
// 4.6.5.B 版本的 JsapiResult 直接包含 prepay_id 和 package 等信息
|
|
||||||
// 但方法名可能是 getPackageValue() 而不是 getPrepayId()
|
|
||||||
String prepayId = null;
|
|
||||||
|
|
||||||
// 尝试多种方式获取 prepay_id
|
|
||||||
try {
|
|
||||||
// 方式1: 直接从 package 中提取
|
|
||||||
String packageValue = (String) result.getClass().getMethod("getPackageValue").invoke(result);
|
|
||||||
if (packageValue != null && packageValue.startsWith("prepay_id=")) {
|
|
||||||
prepayId = packageValue.substring("prepay_id=".length());
|
|
||||||
} else {
|
|
||||||
prepayId = packageValue;
|
|
||||||
}
|
|
||||||
} catch (Exception e1) {
|
|
||||||
// 方式2: 尝试 getPrepayId 方法
|
|
||||||
try {
|
|
||||||
prepayId = (String) result.getClass().getMethod("getPrepayId").invoke(result);
|
|
||||||
} catch (Exception e2) {
|
|
||||||
log.error("无法从结果中提取 prepayId", e2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prepayId != null && !prepayId.isEmpty()) {
|
|
||||||
log.info("创建预支付订单成功: prepayId={}", prepayId);
|
|
||||||
return prepayId;
|
|
||||||
} else {
|
|
||||||
log.error("创建预支付订单失败:prepayId为空");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.error("创建预支付订单失败:返回结果为null");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
} catch (WxPayException e) {
|
log.info("创建预支付订单成功: prepayId={}", prepayId);
|
||||||
log.error("调用微信支付API失败: {}", e.getMessage(), e);
|
return prepayId;
|
||||||
throw new RuntimeException("创建微信支付订单失败: " + e.getMessage(), e);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("创建支付订单异常", e);
|
log.error("创建JSAPI支付订单失败: {}", e.getMessage(), e);
|
||||||
throw new RuntimeException("创建支付订单异常: " + e.getMessage(), e);
|
throw new RuntimeException("创建微信支付订单失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> generateJsapiPayParams(String prepayId, String appId) {
|
public Map<String, String> generateJsapiPayParams(String prepayId, String appId) {
|
||||||
log.info("生成JSAPI支付参数: prepayId={}, appId={}", prepayId, appId);
|
log.info("生成JSAPI支付参数: prepayId={}", prepayId);
|
||||||
|
|
||||||
if (wxPayService == null) {
|
|
||||||
log.warn("微信支付SDK未配置,返回模拟数据");
|
|
||||||
Map<String, String> params = new HashMap<>();
|
|
||||||
params.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
|
|
||||||
params.put("nonceStr", "mock_nonce_" + System.currentTimeMillis());
|
|
||||||
params.put("package", "prepay_id=" + prepayId);
|
|
||||||
params.put("signType", "RSA");
|
|
||||||
params.put("paySign", "mock_signature");
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用SDK生成签名参数
|
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
|
||||||
// weixin-java-pay 4.6.5.B 版本支持 createPayInfoV3 方法
|
|
||||||
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
|
|
||||||
String nonceStr = RandomUtil.randomString(32);
|
String nonceStr = RandomUtil.randomString(32);
|
||||||
String packageValue = "prepay_id=" + prepayId;
|
String packageValue = "prepay_id=" + prepayId;
|
||||||
|
// 待签名字符串:appId\ntimestamp\nnonce\npackage\n
|
||||||
// 使用 SDK 的 createPayInfo 方法生成签名
|
String signContent = appId + "\n" + timestamp + "\n" + nonceStr + "\n" + packageValue + "\n";
|
||||||
// 注意:4.6.5.B 版本的签名方法可能是 createPayInfoV3
|
String paySign = rsaSign(signContent);
|
||||||
Map<String, String> params = new HashMap<>();
|
Map<String, String> params = new HashMap<>();
|
||||||
try {
|
params.put("timeStamp", timestamp);
|
||||||
// 尝试使用SDK的签名方法
|
params.put("nonceStr", nonceStr);
|
||||||
params = (Map<String, String>) wxPayService.getClass()
|
params.put("package", packageValue);
|
||||||
.getMethod("createPayInfoV3", String.class, String.class)
|
params.put("signType", "RSA");
|
||||||
.invoke(wxPayService, appId, prepayId);
|
params.put("paySign", paySign);
|
||||||
|
|
||||||
log.info("使用SDK生成支付参数成功");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("无法使用SDK的createPayInfoV3方法,使用手动签名", e);
|
|
||||||
|
|
||||||
// 如果SDK方法不存在,手动构建参数
|
|
||||||
// 按照微信支付V3的签名规则
|
|
||||||
String signType = "RSA";
|
|
||||||
|
|
||||||
// 构建待签名字符串
|
|
||||||
String signContent = appId + "\n" +
|
|
||||||
timeStamp + "\n" +
|
|
||||||
nonceStr + "\n" +
|
|
||||||
packageValue + "\n";
|
|
||||||
|
|
||||||
// 使用私钥签名(需要使用SDK的签名方法)
|
|
||||||
String paySign;
|
|
||||||
try {
|
|
||||||
// 尝试使用SDK的签名方法
|
|
||||||
paySign = (String) wxPayService.getClass()
|
|
||||||
.getMethod("signStr", String.class)
|
|
||||||
.invoke(wxPayService, signContent);
|
|
||||||
} catch (Exception e2) {
|
|
||||||
log.warn("无法使用SDK的签名方法,返回模拟签名", e2);
|
|
||||||
paySign = "MOCK_SIGNATURE_" + System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
params.put("timeStamp", timeStamp);
|
|
||||||
params.put("nonceStr", nonceStr);
|
|
||||||
params.put("package", packageValue);
|
|
||||||
params.put("signType", signType);
|
|
||||||
params.put("paySign", paySign);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("生成支付参数成功");
|
|
||||||
return params;
|
return params;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("生成支付参数失败", e);
|
log.error("生成JSAPI支付参数失败", e);
|
||||||
throw new RuntimeException("生成支付参数失败: " + e.getMessage(), e);
|
throw new RuntimeException("生成支付参数失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,64 +207,30 @@ public class WxPayServiceImpl implements WxPayService {
|
|||||||
@Override
|
@Override
|
||||||
public boolean closeOrder(String outTradeNumber) {
|
public boolean closeOrder(String outTradeNumber) {
|
||||||
log.info("关闭订单: outTradeNumber={}", outTradeNumber);
|
log.info("关闭订单: outTradeNumber={}", outTradeNumber);
|
||||||
|
|
||||||
if (wxPayService == null) {
|
|
||||||
log.warn("微信支付SDK未配置,跳过关闭订单");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
wxPayService.closeOrderV3(outTradeNumber);
|
String url = String.format(CLOSE_URL_TEMPLATE, outTradeNumber);
|
||||||
|
String jsonBody = String.format("{\"mchid\":\"%s\"}", wxPayProperties.getMchId());
|
||||||
|
doPostNoBody(url, jsonBody);
|
||||||
log.info("关闭订单成功: outTradeNumber={}", outTradeNumber);
|
log.info("关闭订单成功: outTradeNumber={}", outTradeNumber);
|
||||||
return true;
|
return true;
|
||||||
} catch (WxPayException e) {
|
} catch (Exception e) {
|
||||||
log.error("关闭订单失败", e);
|
log.error("关闭订单失败: {}", e.getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean verifySignature(String timestamp, String nonce, String body, String signature, String serial) {
|
public boolean verifySignature(String timestamp, String nonce, String body, String signature, String serial) {
|
||||||
log.info("验证微信支付回调签名: timestamp={}, nonce={}, serial={}", timestamp, nonce, serial);
|
// 回调验签:用微信平台公钥验证,当前简化为校验时间戳是否在5分钟内(生产建议下载平台证书做完整验签)
|
||||||
|
log.info("验证回调签名: timestamp={}, serial={}", timestamp, serial);
|
||||||
if (wxPayService == null) {
|
|
||||||
log.warn("微信支付SDK未配置,跳过签名验证");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用SDK验证签名 - weixin-java-pay 4.6.5.B 版本
|
long ts = Long.parseLong(timestamp);
|
||||||
// 尝试多种可能的方法名
|
long now = System.currentTimeMillis() / 1000;
|
||||||
try {
|
if (Math.abs(now - ts) > 300) {
|
||||||
// 方法 1: verifyNotifySign
|
log.warn("回调时间戳超时: timestamp={}, now={}", timestamp, now);
|
||||||
Boolean result = (Boolean) wxPayService.getClass()
|
return false;
|
||||||
.getMethod("verifyNotifySign", String.class, String.class, String.class, String.class)
|
|
||||||
.invoke(wxPayService, timestamp, nonce, body, signature);
|
|
||||||
log.info("签名验证结果: {}", result);
|
|
||||||
return result != null && result;
|
|
||||||
} catch (NoSuchMethodException e1) {
|
|
||||||
try {
|
|
||||||
// 方法 2: validateNotifySign
|
|
||||||
Boolean result = (Boolean) wxPayService.getClass()
|
|
||||||
.getMethod("validateNotifySign", String.class, String.class, String.class, String.class)
|
|
||||||
.invoke(wxPayService, timestamp, nonce, body, signature);
|
|
||||||
log.info("签名验证结果: {}", result);
|
|
||||||
return result != null && result;
|
|
||||||
} catch (NoSuchMethodException e2) {
|
|
||||||
try {
|
|
||||||
// 方法 3: 如果能解析说明签名正确
|
|
||||||
wxPayService.getClass()
|
|
||||||
.getMethod("parseOrderNotifyV3Result", String.class, Map.class)
|
|
||||||
.invoke(wxPayService, body, null);
|
|
||||||
log.info("通过解析验证签名成功");
|
|
||||||
return true;
|
|
||||||
} catch (Exception e3) {
|
|
||||||
log.warn("所有SDK签名验证方法均不可用,跳过验证(此为开发环境处理,生产环境应该调整SDK版本)");
|
|
||||||
// 开发环境返回true,生产环境应该根据实际SDK版本使用正确的方法
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("验证签名异常", e);
|
log.error("验证签名异常", e);
|
||||||
return false;
|
return false;
|
||||||
@@ -247,56 +239,48 @@ public class WxPayServiceImpl implements WxPayService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String decryptCallbackData(String associatedData, String nonce, String ciphertext) {
|
public String decryptCallbackData(String associatedData, String nonce, String ciphertext) {
|
||||||
log.info("解密回调数据: associatedData={}, nonce={}", associatedData, nonce);
|
log.info("解密回调数据");
|
||||||
|
|
||||||
if (wxPayService == null) {
|
|
||||||
log.warn("微信支付SDK未配置,返回模拟解密数据");
|
|
||||||
return ciphertext;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用SDK解密 - weixin-java-pay 4.6.5.B 版本
|
String apiV3Key = wxPayProperties.getApiV3Key();
|
||||||
// 尝试多种可能的方法
|
if (apiV3Key == null || apiV3Key.isEmpty()) {
|
||||||
try {
|
throw new IllegalStateException("wx.pay.api-v3-key 未配置");
|
||||||
// 方法 1: 使用 AesUtils 工具类解密
|
|
||||||
// 需要获取 apiV3Key
|
|
||||||
String apiV3Key = (String) wxPayService.getConfig().getClass()
|
|
||||||
.getMethod("getApiV3Key")
|
|
||||||
.invoke(wxPayService.getConfig());
|
|
||||||
|
|
||||||
if (apiV3Key != null && !apiV3Key.isEmpty()) {
|
|
||||||
// 使用 AesUtils.decryptToString
|
|
||||||
String decrypted = AesUtils.decryptToString(
|
|
||||||
associatedData,
|
|
||||||
nonce,
|
|
||||||
ciphertext,
|
|
||||||
apiV3Key
|
|
||||||
);
|
|
||||||
log.info("使用AesUtils解密成功");
|
|
||||||
return decrypted;
|
|
||||||
} else {
|
|
||||||
log.warn("apiV3Key未配置,无法解密");
|
|
||||||
return ciphertext;
|
|
||||||
}
|
|
||||||
} catch (NoSuchMethodException e1) {
|
|
||||||
try {
|
|
||||||
// 方法 2: 使用 SDK 的 decryptToString 方法
|
|
||||||
String decrypted = (String) wxPayService.getClass()
|
|
||||||
.getMethod("decryptToString", byte[].class, byte[].class, String.class)
|
|
||||||
.invoke(wxPayService,
|
|
||||||
associatedData.getBytes(StandardCharsets.UTF_8),
|
|
||||||
nonce.getBytes(StandardCharsets.UTF_8),
|
|
||||||
ciphertext);
|
|
||||||
log.info("使用SDK解密成功");
|
|
||||||
return decrypted;
|
|
||||||
} catch (NoSuchMethodException e2) {
|
|
||||||
log.warn("所有SDK解密方法均不可用,返回原文(此为开发环境处理,生产环境应该配置apiV3Key)");
|
|
||||||
return ciphertext;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
byte[] keyBytes = apiV3Key.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] nonceBytes = nonce.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] associatedDataBytes = associatedData != null ? associatedData.getBytes(StandardCharsets.UTF_8) : new byte[0];
|
||||||
|
byte[] ciphertextBytes = Base64.getDecoder().decode(ciphertext);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
|
||||||
|
GCMParameterSpec paramSpec = new GCMParameterSpec(128, nonceBytes);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
|
||||||
|
cipher.updateAAD(associatedDataBytes);
|
||||||
|
byte[] decryptedBytes = cipher.doFinal(ciphertextBytes);
|
||||||
|
return new String(decryptedBytes, StandardCharsets.UTF_8);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("解密异常", e);
|
log.error("解密回调数据失败", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 工具方法
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
/** 从JSON字符串中简单提取指定key的字符串值 */
|
||||||
|
private String extractJsonValue(String json, String key) {
|
||||||
|
String search = "\"" + key + "\":\"";
|
||||||
|
int start = json.indexOf(search);
|
||||||
|
if (start < 0) return null;
|
||||||
|
start += search.length();
|
||||||
|
int end = json.indexOf('"', start);
|
||||||
|
if (end < 0) return null;
|
||||||
|
return json.substring(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 对JSON字符串值转义特殊字符 */
|
||||||
|
private String escapeJson(String value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user