微信支付
最近接入了微信小程序的微信支付, 之前用的是 V2 版本,这两天用的V3 版本。也搞了不少时间这里就记录一下。
首先是调用下单接口,拿到对应的预支付id,然后通过预支付id ,获取到对应的支付参数,最后将参数返回到前端。

导入对应的sdk
<!-- 微信支付SDK -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.7</version> <!-- 使用最新稳定版 -->
</dependency>
初始化配置微信支付
package com.trailer.common.config.wechat;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.InputStream;
import java.security.PrivateKey;
/**
* 微信支付配置属性类,用于绑定yml配置文件
*/
@Configuration
public class WechatPayConfig {
@Value("${wx.pay.merchant-id}")
private String merchantId;
@Value("${wx.pay.private-key-path:classpath:apiclient_key.pem}")
private String privateKeyPath;
@Value("${wx.pay.merchant-serial-number}")
private String merchantSerialNumber;
@Value("${wx.pay.api-v3-key}")
private String apiV3Key;
@Value("${wx.pay.app-id}")
private String appId;
@Value("${wx.pay.mch-key}")
private String mchKey;
@Value("${wx.pay.cert-path:classpath:/apiclient_cert.p12}")
private String certPath;
@Value("${wx.pay.notify-url}")
private String notifyUrl;
@Value("${wx.pay.v3-base-url:https://api.mch.weixin.qq.com/v3}")
private String v3BaseUrl;
// Getter和Setter方法
public String getMerchantId() {
return merchantId;
}
public void setMerchantId(String merchantId) {
this.merchantId = merchantId;
}
public String getPrivateKeyPath() {
return privateKeyPath;
}
public void setPrivateKeyPath(String privateKeyPath) {
this.privateKeyPath = privateKeyPath;
}
public String getMerchantSerialNumber() {
return merchantSerialNumber;
}
public void setMerchantSerialNumber(String merchantSerialNumber) {
this.merchantSerialNumber = merchantSerialNumber;
}
public String getApiV3Key() {
return apiV3Key;
}
public void setApiV3Key(String apiV3Key) {
this.apiV3Key = apiV3Key;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getMchKey() {
return mchKey;
}
public void setMchKey(String mchKey) {
this.mchKey = mchKey;
}
public String getCertPath() {
return certPath;
}
public void setCertPath(String certPath) {
this.certPath = certPath;
}
public String getNotifyUrl() {
return notifyUrl;
}
public void setNotifyUrl(String notifyUrl) {
this.notifyUrl = notifyUrl;
}
public String getV3BaseUrl() {
return v3BaseUrl;
}
public void setV3BaseUrl(String v3BaseUrl) {
this.v3BaseUrl = v3BaseUrl;
}
/**
* 初始化商户私钥
*/
@Bean
public PrivateKey privateKey() throws Exception {
Resource resource = new ClassPathResource(privateKeyPath.replace("classpath:", ""));
try (InputStream inputStream = resource.getInputStream()) {
return PemUtil.loadPrivateKey(inputStream);
}
}
/**
* 初始化HTTP客户端
*/
@Bean
public CloseableHttpClient httpClient(PrivateKey privateKey) throws Exception {
// 加载平台证书
CertificatesManager certificatesManager = CertificatesManager.getInstance();
certificatesManager.putMerchant(merchantId,
new WechatPay2Credentials(merchantId, new PrivateKeySigner(merchantSerialNumber, privateKey)),
apiV3Key.getBytes("UTF-8"));
// 获取证书验证器
Verifier verifier = certificatesManager.getVerifier(merchantId);
// 构建HTTP客户端
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(merchantId, merchantSerialNumber, privateKey)
.withValidator(new WechatPay2Validator(verifier));
return builder.build();
}
}
配置微信支付的加密和解密、签名
package com.trailer.client.service;
import com.alibaba.fastjson2.JSONObject;
import com.trailer.client.dto.request.TransferSceneReportInfo;
import com.trailer.common.config.wechat.WechatPayConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.Base64;
import java.util.List;
import java.util.Random;
import java.util.UUID;
/**
* 微信支付服务类,封装微信支付相关操作
*/
@Service
public class WechatPayService {
@Autowired
private WechatPayConfig wechatPayConfig;
@Autowired
private CloseableHttpClient httpClient;
/**
* 生成随机字符串
*/
public String generateNonceStr() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
/**
* 生成商户订单号
*/
public String generateOutTradeNo() {
return "ORD" + System.currentTimeMillis() + new Random().nextInt(1000);
}
/**
* 小程序 v3 接口
*
*/
public String signV3MiniProgramSign(String appid, long timestamp, String nonceStr, String body) throws Exception{
//从下往上依次生成
String signatureStr = String.format("%s\n%d\n%s\n%s\n",
appid, timestamp, nonceStr, body);
//签名
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(wechatPayConfig.privateKey());
signature.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
}
/**
* 调用微信支付V3接口
*/
public String callV3Api(String path, JSONObject params) throws Exception {
String url = wechatPayConfig.getV3BaseUrl() + path;
HttpPost httpPost = new HttpPost(url);
// 设置请求头
long timestamp = System.currentTimeMillis() / 1000;
String nonceStr = generateNonceStr();
String body = params.toJSONString();
String signature = signV3("POST", path, timestamp, nonceStr, body);
System.out.println("签名结果:" + signature);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("Wechatpay-Timestamp", String.valueOf(timestamp));
httpPost.setHeader("Wechatpay-Nonce", nonceStr);
httpPost.setHeader("Wechatpay-Signature", signature);
httpPost.setHeader("Wechatpay-Serial", wechatPayConfig.getMerchantSerialNumber());
// 设置请求体
httpPost.setEntity(new StringEntity(body, "UTF-8"));
// 发送请求
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String responseBody = EntityUtils.toString(response.getEntity());
if (response.getStatusLine().getStatusCode() != 200) {
throw new RuntimeException("调用微信支付接口失败: " + responseBody);
}
return responseBody;
}
}
/**
* 解密微信支付通知数据
*/
public String decryptNotifyData(String associatedData, String nonce, String ciphertext) throws Exception {
SecretKeySpec key = new SecretKeySpec(
wechatPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8),
"AES"
);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] nonceBytes = nonce.getBytes(StandardCharsets.UTF_8);
byte[] associatedDataBytes = associatedData.getBytes(StandardCharsets.UTF_8);
byte[] ciphertextBytes = Base64.getDecoder().decode(ciphertext);
// GCM参数
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, nonceBytes);
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
cipher.updateAAD(associatedDataBytes);
return new String(cipher.doFinal(ciphertextBytes), StandardCharsets.UTF_8);
}
/**
* 小程序支付下单
*/
public JSONObject createMiniProgramOrder(String description, String outTradeNo,
int totalAmount, String openid) throws Exception {
JSONObject params = new JSONObject();
params.put("appid", wechatPayConfig.getAppId());
params.put("mchid", wechatPayConfig.getMerchantId());
params.put("description", description);
params.put("out_trade_no", outTradeNo);
params.put("notify_url", wechatPayConfig.getNotifyUrl());
JSONObject amount = new JSONObject();
amount.put("total", totalAmount); // 单位:分
amount.put("currency", "CNY");
params.put("amount", amount);
JSONObject payer = new JSONObject();
payer.put("openid", openid);
params.put("payer", payer);
String response = callV3Api("/pay/transactions/jsapi", params);
return JSONObject.parseObject(response);
}
/**
* 申请退款
*/
public JSONObject refund(String outTradeNo, String outRefundNo,
int totalAmount, int refundAmount) throws Exception {
JSONObject params = new JSONObject();
params.put("out_trade_no", outTradeNo);
params.put("out_refund_no", outRefundNo);
JSONObject amount = new JSONObject();
amount.put("total", totalAmount);
amount.put("refund", refundAmount);
amount.put("currency", "CNY");
params.put("amount", amount);
params.put("notify_url", wechatPayConfig.getNotifyUrl());
String response = callV3Api("/refund/domestic/refunds", params);
return JSONObject.parseObject(response);
}
/**
* 商家转账到零钱
*/
public JSONObject transferToBalance(String outBatchNo, String outDetailNo,
String openid ,String userName , int amount, String description) throws Exception {
JSONObject params = new JSONObject();
params.put("appid", wechatPayConfig.getAppId());
params.put("out_batch_no", outBatchNo);
params.put("batch_name", "商家转账");
params.put("batch_remark", description);
params.put("total_amount", amount);
params.put("total_num", 1);
params.put("notify_url", wechatPayConfig.getNotifyUrl());
JSONObject transferDetail = new JSONObject();
transferDetail.put("out_detail_no", outDetailNo);
transferDetail.put("transfer_amount", amount);
transferDetail.put("transfer_remark", description);
transferDetail.put("openid", openid);
if (shouldIncludeUserName(amount, userName)) {
// 对用户姓名进行加密处理
String encryptedName = encryptUserName(userName);
transferDetail.put("user_name", encryptedName);
}
params.put("transfer_detail_list", new Object[]{transferDetail});
String response = callV3Api("/fund-app/mch-transfer/transfer-bills", params);
return JSONObject.parseObject(response);
}
/**
* 商家转账到零钱
*/
public JSONObject transferToBalance(String outBillNo, String transferSceneId,
String openid, String userName, int amount,
String description, String userRecvPerception,
List<TransferSceneReportInfo> reportInfos) throws Exception {
JSONObject params = new JSONObject();
params.put("appid", wechatPayConfig.getAppId());
params.put("out_bill_no", outBillNo);
params.put("transfer_scene_id", transferSceneId);
params.put("openid", openid);
params.put("transfer_amount", amount);
params.put("transfer_remark", description);
params.put("notify_url", wechatPayConfig.getNotifyUrl());
if (userRecvPerception != null) {
params.put("user_recv_perception", userRecvPerception);
}
if (shouldIncludeUserName(amount, userName)) {
String encryptedName = encryptUserName(userName);
params.put("user_name", encryptedName);
}
params.put("transfer_scene_report_infos", reportInfos);
String response = callV3Api("/fund-app/mch-transfer/transfer-bills", params);
return JSONObject.parseObject(response);
}
/**
* 判断是否应该包含用户姓名
*
* @param amount 转账金额(分)
* @param userName 用户姓名
* @return 是否应该包含用户姓名
*/
private boolean shouldIncludeUserName(int amount, String userName) {
// 如果没有用户提供姓名,则不包含
if (userName == null || userName.trim().isEmpty()) {
return false;
}
// 金额小于30分(0.3元)时,不允许填写收款用户姓名
if (amount < 30) {
return false;
}
// 金额大于等于200000分(2000元)时,必须填写收款用户姓名
if (amount >= 200000) {
return true;
}
// 0.3元 <= 金额 < 2000元时,可选填写收款用户姓名
// 这里我们选择填写,以提高安全性
return true;
}
/**
* 对用户姓名进行加密
*
* @param userName 用户姓名
* @return 加密后的用户姓名
* @throws Exception 加密异常
*/
private String encryptUserName(String userName) throws Exception {
// 微信支付要求使用平台证书对敏感信息进行加密
// 这里需要实现具体的加密逻辑
// 暂时返回原值,实际项目中需要根据微信支付文档实现加密
return encryptWithWechatPublicKey(userName);
}
/**
* 使用微信支付公钥加密用户姓名
*
* @param data 待加密数据
* @return 加密后的数据
* @throws Exception 加密异常
*/
private String encryptWithWechatPublicKey(String data) throws Exception {
// 实际实现应该:
// 1. 获取微信支付平台证书
// 2. 使用证书中的公钥对数据进行RSA加密
// 3. 返回Base64编码的加密结果
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(wechatPayConfig.privateKey());
signature.update(data.getBytes(StandardCharsets.UTF_8));
// 临时返回原值,避免空值导致的错误
if (data == null || data.isEmpty()) {
throw new IllegalArgumentException("用户姓名不能为空");
}
return Base64.getEncoder().encodeToString(signature.sign());// 实际项目中需要替换为加密后的值
}
}
有了上面的代码下单和支付的流程就很简单了。
JSAPI/小程序下单
创建订单获取预支付 ID
private OrderSubmitResponse handlePayment(UserOrders userOrders, String openid) {
try {
// 1. 转换金额单位(元 -> 分)
int totalAmount = userOrders.getTotalAmount().intValue();
// 2. 调用工具类创建小程序支付订单
JSONObject payResult = wechatPayUtil.createMiniProgramOrder(
"拖车服务-" + userOrders.getStartAddress() + "至" + userOrders.getEndAddress(),
userOrders.getOrderSn(),
totalAmount,
openid
);
// 3. 获取预支付ID
String prepayId = payResult.getString("prepay_id");
if (prepayId == null || prepayId.isEmpty()) {
throw new GlobalException("获取预支付ID失败");
}
// 4. 生成小程序调起支付的签名参数
Map<String, String> payParams = generateMiniProgramPayParams(prepayId);
// 5. 构建返回结果
return OrderSubmitResponse.builder()
.orderNo(userOrders.getOrderSn())
.prepayment(userOrders.getPrepayment())
.payParams(payParams)
.build();
}catch (Exception e){
throw new GlobalException("支付失败");
}
}
JSAPI调起支付
/**
* 生成小程序调起支付的参数
*/
private Map<String, String> generateMiniProgramPayParams(String prepayId) throws Exception {
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonceStr = generateRandomString(32);
String packageValue = "prepay_id=" + prepayId;
String signType = "RSA";
// 调用工具类生成签名
String paySign = wechatPayUtil.signV3MiniProgramSign(
wechatPayConfig.getAppId(),
Long.parseLong(timeStamp),
nonceStr,
packageValue
);
Map<String, String> params = new HashMap<>();
params.put("appId", wechatPayConfig.getAppId());
params.put("timeStamp", timeStamp);
params.put("nonceStr", nonceStr);
params.put("package", packageValue);
params.put("signType", signType);
params.put("paySign", paySign);
return params;
}
前端只要拿到对应的支付参数调用支付即可了。

调用后在页面上会出现对应的说明成功了。

踩坑:
签名出错:V2 和 V3 的签名方式不一样。=,所以要注意看是调用的是V 几的接口。
签名证书是否配置正确。

1万+

被折叠的 条评论
为什么被折叠?



