序:
公司业务涉及到打通微信支付,网上查了很多,发现要么太老,要么不可用,踩了无数坑之后终于搞定,只需要自己去商户后台申请之后替换配置,复制代码即可使用。
微信小程序支付本文使用【微信支付 APIv3 Java SDK(wechatpay-java) 】实现。
官方微信支付 APIv3 Java SDK GitHub - wechatpay-apiv3/wechatpay-java: 微信支付 APIv3 的官方 Java Library官网接口文档:开发指引_小程序支付|微信支付商户文档中心
该sdk无需进行繁琐的验签,httpClint等,就适合我这种懒人。
前置条件:
Java 1.8+。
成为微信支付商户。
商户 API 证书:指由商户申请的,包含证书序列号、商户的商户号、公司名称、公钥信息的证书。
商户 API 私钥:商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件 apiclient_key.pem 中。
APIv3 密钥:为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了 AES-256-GCM 加密。APIv3 密钥是加密时使用的对称密钥。
apiv3公钥id:
apiv3公钥证书
支付打通流程图
重点步骤说明:
步骤4: 用户下单发起支付,商户可通过JSAPI下单创建支付订单。
步骤9: 商户小程序内使用小程序调起支付API(wx.requestPayment)发起微信支付,详见小程序API文档 (opens new window)。
步骤16: 用户支付成功后,商户可接收到微信支付支付结果通知支付通知API。
步骤21: 商户在没有接收到微信支付结果通知的情况下需要主动调用查询订单API查询支付结果。
具体实现步骤如下
1.前端弹框发起支付,调用【预支付订单/统一下单(/wxMiniappPay/createOrder)】接口取得预支付参数。
2.前端将上一步获得的参数,在小程序中调用支付API(wx.requestPayment)发起微信支付,用户输入支付密码后即可成功支付。
3.用户成功支付后,微信将通过【支付回调(/wxMiniappPay/payNotify)】接口推送支付成功的信息给后端,后端根据业务进行操作即可。
4.前端在用户输入密码支付后,调用【根据商户订单号查询订单(/wxMiniappPay/queryOrderByOutTradeNo)】接口,查看支付信息,按照自己的业务进行信息展示。
5.后端通过定时任务,调用【关闭订单(/wxMiniappPay/closeOrder)】接口,对未成功支付的订单进行关闭。
6.如果需要退款操作,前端调用【退款申请(/wxMiniappPay/refund)】接口,进行退款操作。
7.成功退款后,微信会通过【微信小程序退款回调】接口,推送退款状态信息,后端同步退款状态。
8.后端通过定时任务,调用【查询单笔退款(通过商户退款单号)(/wxMiniappPay/queryByOutRefundNo)】接口,同步退款状态。
微信小程序支付实现
pom.xml加入以下依赖 (注:sdk包版本尽量一致,0.2.12一下不支持apiv3)
<!--微信支付-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.15</version>
</dependency>
application.yml配置
证书路径如图
wx:
miniapp:
appid: wx7*******451 # 微信小程序appid
secret: a4******0bacb86870 # 微信小程序密钥
merchantId: 16******* # 商户号
privateKeyPath: *****\apiclient_key.pem # 商户API私钥路径(测试环境)绝对路径
merchantSerialNumber: 3**************** # 商户API证书序列号
apiV3Key: *************** # 商户APIV3密钥
payNotifyUrl:yuming/app/order/pay/CallBack # 支付通知地址(测试环境)
refundNotifyUrl: yuming/wxMiniappPay/refundNotify # 退款通知地址(测试环境)
publicKeyId: 'PUB_KEY_ID_*********' # 商户API公钥ID
publicKeyPath: ********\pub_key.pem # 商户API公钥路径
微信小程序支付配置WxPayConfig
package com.ruoyi.common.weixin;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* <p>
* 微信小程序支付配置
* </p>
*
* @author songfayuan
* @date 2024/9/30 15:59
*/
@Data
@Component
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxPayConfig {
/**
* 微信小程序的 AppID
*/
private String appid;
/**
* 微信小程序的密钥
*/
private String secret;
/**
* 商户号
*/
private String merchantId;
/**
* 商户API私钥路径
*/
private String privateKeyPath;
/**
* 商户证书序列号
*/
private String merchantSerialNumber;
/**
* 商户APIV3密钥
*/
private String apiV3Key;
/**
* 支付通知地址
*/
private String payNotifyUrl;
/**
* 退款通知地址
*/
private String refundNotifyUrl;
private String publicKeyId;
private String publicKeyPath;
}
微信支付证书自动更新配置WxPayAutoCertificateConfig
注意:启动时,该配置类如果出现【无可用的平台证书,请在商户平台-API安全申请使用微信支付公钥。可查看指引】异常,GitHub - wechatpay-apiv3/wechatpay-java: 微信支付 APIv3 的官方 Java Library
可去查看微信官方SDK代码,注意这个类
package com.ruoyi.common.weixin;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.RSAPublicKeyConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* <p>
* 微信支付证书自动更新配置
* 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
* </p>
*
* @author songfayuan
* @date 2024/9/30 15:57
*/
@Slf4j
@Configuration
public class WxPayAutoCertificateConfig {
@Resource
private WxPayConfig wxPayConfig;
/**
* 初始化商户配置
*
* @return
*/
@Bean
public Config rsaAutoCertificateConfig() {
// 这里把Config作为配置Bean是为了避免多次创建资源,一般项目运行的时候这些东西都确定了
// 具体的参数改为申请的数据,可以通过读配置文件的形式获取
Config config = new RSAPublicKeyConfig.Builder()
.merchantId(wxPayConfig.getMerchantId())
.privateKeyFromPath(wxPayConfig.getPrivateKeyPath())
.publicKeyFromPath(wxPayConfig.getPublicKeyPath())
.publicKeyId(wxPayConfig.getPublicKeyId())
.merchantSerialNumber(wxPayConfig.getMerchantSerialNumber())
.apiV3Key(wxPayConfig.getApiV3Key())
.build();
// Config config = new RSAAutoCertificateConfig.Builder()
// .merchantId(wxPayConfig.getMerchantId())
// .privateKeyFromPath(wxPayConfig.getPrivateKeyPath())
// .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber())
// .apiV3Key(wxPayConfig.getApiV3Key())
// .build();
log.info("初始化微信支付商户配置完成...");
return config;
}
}
业务代码数据敏感,这里就不贴了,贴个相关的示例调用代码吧,根据自己业务集成一下
package com.wechat.pay.java.service.payments.jsapi;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.service.payments.jsapi.model.CloseOrderRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByIdRequest;
import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByOutTradeNoRequest;
import com.wechat.pay.java.service.payments.model.Transaction;
public class JsapiServiceExtensionExample {
/** 商户号 */
public static String merchantId = "190000****";
/** 商户API私钥路径 */
public static String privateKeyPath = "/Users/yourname/your/path/apiclient_key.pem";
/** 商户证书序列号 */
public static String merchantSerialNumber = "5157F09EFDC096DE15EBE81A47057A72********";
/** 商户APIV3密钥 */
public static String apiV3Key = "...";
public static JsapiServiceExtension service;
public static void main(String[] args) {
// 初始化商户配置
Config config =
new RSAAutoCertificateConfig.Builder()
.merchantId(merchantId)
// 使用 com.wechat.pay.java.core.util 中的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
// 初始化服务
service =
new JsapiServiceExtension.Builder()
.config(config)
.signType("RSA") // 不填默认为RSA
.build();
try {
// ... 调用接口
PrepayWithRequestPaymentResponse response = prepayWithRequestPayment();
System.out.println(response);
} catch (HttpException e) { // 发送HTTP请求失败
// 调用e.getHttpRequest()获取请求打印日志或上报监控,更多方法见HttpException定义
} catch (ServiceException e) { // 服务返回状态小于200或大于等于300,例如500
// 调用e.getResponseBody()获取返回体打印日志或上报监控,更多方法见ServiceException定义
} catch (MalformedMessageException e) { // 服务返回成功,返回体类型不合法,或者解析返回体失败
// 调用e.getMessage()获取信息打印日志或上报监控,更多方法见MalformedMessageException定义
}
}
/** 关闭订单 */
public static void closeOrder() {
CloseOrderRequest request = new CloseOrderRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
// 调用接口
service.closeOrder(request);
}
/** JSAPI支付下单,并返回JSAPI调起支付数据 */
public static PrepayWithRequestPaymentResponse prepayWithRequestPayment() {
PrepayRequest request = new PrepayRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
// 调用接口
return service.prepayWithRequestPayment(request);
}
/** 微信支付订单号查询订单 */
public static Transaction queryOrderById() {
QueryOrderByIdRequest request = new QueryOrderByIdRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
// 调用接口
return service.queryOrderById(request);
}
/** 商户订单号查询订单 */
public static Transaction queryOrderByOutTradeNo() {
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
// 调用接口
return service.queryOrderByOutTradeNo(request);
}
}
相关工具类
package com.ruoyi.common.weixin;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
public class OrderNoGenerator {
// 平台标识(WX-微信,ALI-支付宝)
private static final String PLATFORM_PREFIX = "WX_REFUND_";
// 时间戳格式(精确到毫秒)
private static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("yyyyMMddHHmmssSSS");
private static final String ORDER_NO_PREFIX = "WP_"; // 微信预支付订单号前缀
private static final int RANDOM_NUMBER_LENGTH = 4; // 随机数长度
// 序列号(1000-9999循环)
private static final AtomicInteger SEQ = new AtomicInteger(0);
// 日期格式:两位年+月日+时分秒+两位毫秒(共14位)
private static final DateTimeFormatter DATE_FMT =
DateTimeFormatter.ofPattern("yyMMddHHmmssSS");
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
/**
* 生成微信预支付下单入参 orderNo
*
* @return 生成的唯一订单号
*/
public static String generateOrderNo() {
// 获取当前时间戳
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
String timestamp = dateFormat.format(new Date());
// 生成随机数
Random random = new Random();
StringBuilder randomNumber = new StringBuilder();
for (int i = 0; i < RANDOM_NUMBER_LENGTH; i++) {
randomNumber.append(random.nextInt(10));
}
// 组合前缀、时间戳和随机数
return ORDER_NO_PREFIX + timestamp + randomNumber.toString();
}
/**
* 生成全局唯一退款单号
*
* @param platform 支付平台标识(如:WX/ALI)
*/
public static String generateOutRefundNo(String platform) {
// 1. 平台标识(防多平台冲突)
String prefix = platform.toUpperCase() + "_";
// 2. 时间戳(17位)
String timestamp = DATE_FORMAT.format(new Date());
// 3. 随机数(3位防并发重复)
String random = String.format("%03d",
ThreadLocalRandom.current().nextInt(1000));
return prefix + timestamp + random;
}
public static String generate() {
LocalDateTime now = LocalDateTime.now(ZONE);
String datePart = now.format(DATE_FMT); // 示例:25030615304512(14位)
// 序列号取模运算保证范围1000-9999
int seq = SEQ.getAndIncrement() % 9000 + 1000;
return datePart + String.format("%04d", seq);
}
}
业务逻辑相对就简单了,自己结合实际业务调用,这个sdk集成了验签等,方便很多。
大佬们有指正的地方,欢迎评论区交流~