对接微信v3支付改版了,按证书的方式运行,结果在构建商户配置的时候报错了,微信那边也没给具体错误信息,问了客服才知道:由证书验证商户身份改成公钥验证身份!以下是技术客服原话:
微信推出了平台证书的替代公钥方案,两者使用场景完全相同,但公钥不会过期,后续新申请的商户号将使用公钥进行验签与敏感信息加密。
微信支付公钥使用介绍:https://pay.weixin.qq.com/docs/merchant/products/platform-certificate/wxp-pub-key-guide.html
平台证书切换微信支付公钥指引:https://pay.weixin.qq.com/docs/merchant/products/platform-certificate/update-pub-key.html
新申请的商户不能再用平台证书,请申请公钥
对接SDK的代码要改成如下:
以上的意思是说,对接微信支付,有两种方式,一种是自己封装参数,自己加密解密。而另一种方式就简便很多了,Maven引入微信支付对应jar包,然后调用相应接口即可。上图是对象调用方法前,对这个对象初始化。
还不明白参考下面的,配好就能使用(除了需要商户相关资料,还需要在公众号和商户各自后台配置合法域名啊,白名单,商户关联公众号这些,这些都配置好后,下面的代码就能用)
我对接SDK的方式如下:
1、Maven引入
<!-- 微信支付apiv3版本 -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.14</version>
</dependency>
2、springboot的yml配置
wx:
#公众号
public:
appId: wx556****
sercet: 3******
redirectUri: https://******/ #后面要拼项目名和具体接口名
#企业微信
corporation:
corpId: ww**** #企业ID
agentId: 100**** #自建应用ID
corpsecret: nzmw0JmTF***** #自建应用密钥
#微信支付 ,本系统示例地址:https://****/terminal/static/pay.html?myCallBack=1
pay:
#合作伙伴(服务商)
partner:
merchantId: 1314*** #商户号
privateKeyPath: D:/hwj/aaa/普通商户1314*****/apiclient_key.pem #商户API私钥路径
apiV3Key: 64Thi56nWE****** #商户APIV3密钥
merchantSerialNumber: 194EB00A******* #商户证书序列号
notifyUrl: https://*******/ #微信支付回调地址
sub_mchid: 160***** #子商户号
#普通商户
general:
merchantId: 13****** #商户号
privateKeyPath: D:/hwj/aaa/普通商户13*****/apiclient_key.pem #商户API私钥路径
apiV3Key: 6reo35WVjr****** #商户APIV3密钥
merchantSerialNumber: 6D519C633E**** #商户证书序列号
notifyUrl: https://****/ #微信支付回调地址
publicKeyId: PUB_KE****** #公钥ID
publicKeyFromPath: D:/****/pub_key.pem #公钥路径
3、普通商户支付工具类(本文要讲的重点如下:v3新版和旧版)
package com.yema.utils;
import com.alibaba.fastjson.JSON;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.RSAPublicKeyConfig;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.AmountReq;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 微信普通商户支付工具类
* @ClassName WXPayUtil
* @Description TODO
* @Author orison
* @Date 2024-08-12 17:09
* @Version 1.0
**/
@Component
@Slf4j
public class WXPayGeneralUtil implements ApplicationContextAware {
/** 应用Id,如公众号、小程序等 */
public static String appId;
/** 商户号 */
public static String merchantId;
/** 商户API私钥路径 */
public static String privateKeyPath;
/** 商户证书序列号 */
public static String merchantSerialNumber;
/** 商户APIV3密钥 */
public static String apiV3Key;
/** 支付回调地址 */
public static String notifyUrl;
/**
* 公钥路径
* 【微信推出了平台证书的替代公钥方案,两者使用场景完全相同,但公钥不会过期,后续新申请的商户号将使用公钥进行验签与敏感信息加密。
* 微信支付公钥使用介绍:https://pay.weixin.qq.com/docs/merchant/products/platform-certificate/wxp-pub-key-guide.html
* 平台证书切换微信支付公钥指引:https://pay.weixin.qq.com/docs/merchant/products/platform-certificate/update-pub-key.html
* 新申请的商户不能再用平台证书,请申请公钥】
* */
public static String publicKeyFromPath;
/** 公钥ID */
public static String publicKeyId;
/** 项目名 */
public static String contextPath;
public static JsapiService service;//jsapi支付服务
public static JsapiServiceExtension jsapiServiceExtension; //jsapi支付服务增强接口(强烈推荐)
public static NotificationConfig notificationConfig;//回调配置
public static RefundService refundService;//退款服务
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
Environment environment = applicationContext.getEnvironment();
appId = environment.getProperty("wx.public.appId");
merchantId = environment.getProperty("wx.pay.general.merchantId");
merchantSerialNumber = environment.getProperty("wx.pay.general.merchantSerialNumber");
privateKeyPath = environment.getProperty("wx.pay.general.privateKeyPath");
apiV3Key = environment.getProperty("wx.pay.general.apiV3Key");
notifyUrl = environment.getProperty("wx.pay.general.notifyUrl");
publicKeyFromPath = environment.getProperty("wx.pay.general.publicKeyFromPath");
publicKeyId = environment.getProperty("wx.pay.general.publicKeyId");
contextPath = environment.getProperty("server.servlet.context-path");
if (StringUtils.isBlank(merchantId) || StringUtils.isBlank(privateKeyPath) || StringUtils.isBlank(merchantSerialNumber) || StringUtils.isBlank(apiV3Key)){
log.warn("由于初始化普通商户配置时缺少某些必要参数,所以不允许初始化该普通商户配置");
return;
}
// 初始化商户配置
Config config =
null;
try {
if (StringUtils.isBlank(publicKeyFromPath)){
//v3旧版
config = new RSAAutoCertificateConfig.Builder()
.merchantId(merchantId)
// 使用 com.wechat.pay.java.core.util 中的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
}else{
//v3新版
config = new RSAPublicKeyConfig.Builder()
.merchantId(merchantId)
.privateKeyFromPath(privateKeyPath)
.publicKeyFromPath(publicKeyFromPath)
.publicKeyId(publicKeyId)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
}
} catch (Exception e) {
e.printStackTrace();
}
// 初始化服务
notificationConfig = (NotificationConfig)config;
service = new JsapiService.Builder().config(config).build();
refundService = new RefundService.Builder().config(config).build();
jsapiServiceExtension = new JsapiServiceExtension.Builder().config(config).build();
}
/**
* JSAPI 支付下单,并返回 JSAPI 调起支付数据(这些数据直接给前端唤起微信收银台的)。推荐使用!
* 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/direct-jsons/jsapi-prepay.html
* <p>请求成功后,该方法返回预支付交易会话标识 prepay_id 和客户端 JSAPI 调起支付所需参数。 它相比 JsApiService.prepay
* 更简单易用,因为无需开发者自行计算调起支付签名。
*
* @return PrepayWithRequestPaymentResponse
* HttpException 发送HTTP请求失败。例如构建请求参数失败、发送请求失败、I/O错误等。包含请求信息。
* ValidationException 发送HTTP请求成功,验证微信支付返回签名失败。
* ServiceException 发送HTTP请求成功,服务返回异常。例如返回状态码小于200或大于等于300。
* MalformedMessageException 服务返回成功,content-type不为application/json、解析返回体失败。
*/
public static PrepayWithRequestPaymentResponse jsapiPrepayExtension(int total,String desc,String outTradeNo,String openId,String attach,String uri){
PrepayRequest request = new PrepayRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
Amount amount = new Amount();
amount.setTotal(total);
request.setAmount(amount);
request.setAppid(appId);
request.setMchid(merchantId);
request.setDescription(desc); //"测试商品标题"
request.setNotifyUrl(notifyUrl+contextPath+uri);
request.setAttach(attach);
request.setOutTradeNo(outTradeNo);
Payer payer = new Payer();
payer.setOpenid(openId);
request.setPayer(payer);
return jsapiServiceExtension.prepayWithRequestPayment(request);
}
/**
* 根据退款单号查询退款单信息
* 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/query-by-out-refund-no.html
* @param outRefundNo
* @return
*/
public static Result queryRefund(String outRefundNo) {
try {
QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
request.setOutRefundNo(outRefundNo);
Refund refund = refundService.queryByOutRefundNo(request);
// status
// 可选取值:
// SUCCESS: 退款成功
// CLOSED: 退款关闭
// PROCESSING: 退款处理中
// ABNORMAL: 退款异常
return Result.success(refund);
} catch (ServiceException e) {
log.error("普通商户-根据退款单号查询退款单信息异常",e);
return Result.fail(e.getErrorMessage());
}
}
/**
* 关闭订单
* 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/close-order.html
* @param outTradeNo
* @return
*/
public static Result closeOrder(String outTradeNo) {
try {
CloseOrderRequest request = new CloseOrderRequest();
request.setMchid(merchantId);
request.setOutTradeNo(outTradeNo);
service.closeOrder(request);
return Result.success();
} catch (ServiceException e) {
log.error("普通商户-关闭订单异常",e);
return Result.fail(e.getErrorMessage());
}
}
/**
* 根据系统业务单号查询订单
* (用于防止订单支付后微信没有回调支付接口,导致订单还是处于待支付,解决方案如:定时任务查询当天处于待支付订单)
* 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/query-by-out-trade-no.html
* @param outTradeNo
*
* trade_state
* 【交易状态】 交易状态,枚举值:
* * SUCCESS:支付成功
* * REFUND:转入退款
* * NOTPAY:未支付
* * CLOSED:已关闭
* * REVOKED:已撤销(仅付款码支付会返回)
* * USERPAYING:用户支付中(仅付款码支付会返回)
* * PAYERROR:支付失败(仅付款码支付会返回)
*/
public static Result queryOrderByOutTradeNo(String outTradeNo){
try {
QueryOrderByOutTradeNoRequest query = new QueryOrderByOutTradeNoRequest();
query.setMchid(merchantId);
query.setOutTradeNo(outTradeNo);
Transaction transaction = service.queryOrderByOutTradeNo(query);
return Result.success(transaction);
} catch (ServiceException e) {
log.error("普通商户-根据系统业务号查询订单异常",e);
return Result.fail(e.getErrorMessage());
}
}
/**
* 微信支付单号查询订单
* 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/query-by-wx-trade-no.html
* @param transactionId 【微信支付订单号】 微信支付系统生成的订单号
* @return
*/
public static Transaction queryOrderByTransactionId(String transactionId) {
QueryOrderByIdRequest request = new QueryOrderByIdRequest();
request.setMchid(merchantId);
request.setTransactionId(transactionId);
return service.queryOrderById(request);
}
/**
* 退款
* 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/create.html
* 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
* 注意:
* 交易时间超过一年的订单无法提交退款(按支付成功时间+365天计算)
* 微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
* 请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次
* 每个支付订单的部分退款次数不能超过50次
* 如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
* 申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
* 错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
* 一个月之前的订单申请退款频率限制为:5000/min
* 同一笔订单多次退款的请求需相隔1分钟
* @param outTradeNo
*/
public static Result refunds(String outTradeNo,long total,long refund,String uri) {
try {
String symbol = StringUtils.join(WXPayGeneralUtil.randomNum(6, 10).toArray(), ",").replace(",", "");
String str = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + symbol;
String out_refund_no = str;//商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
CreateRequest request = new CreateRequest();
request.setOutTradeNo(outTradeNo);
request.setOutRefundNo(out_refund_no);
request.setNotifyUrl(notifyUrl+contextPath+uri);
AmountReq amountReq = new AmountReq();
amountReq.setRefund(refund);
amountReq.setTotal(total);
amountReq.setCurrency("CNY");
request.setAmount(amountReq);
refundService.create(request);
return Result.success("已申请退款",out_refund_no);
} catch (ServiceException e) {
log.error("普通商户-退款",e);
return Result.fail(e.getErrorMessage());
}
}
/**
* 对退款回调预处理
* 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/refund-result-notice.html
* @param request
* @param response
* @return
*/
public static Map refundsCallbackHandle(HttpServletRequest request, HttpServletResponse response){
try {
//获取报文
String body = IOUtils.toString(request.getInputStream(), "UTF-8");
//随机串
String nonceStr = request.getHeader("Wechatpay-Nonce");
//微信传递过来的签名
String signature = request.getHeader("Wechatpay-Signature");
//证书序列号(微信平台)
String serialNo = request.getHeader("Wechatpay-Serial");
//时间戳
String timestamp = request.getHeader("Wechatpay-Timestamp");
// 签名方式
String signType = request.getHeader("Wechatpay-Signature-Type");
// 构造 RequestParam
com.wechat.pay.java.core.notification.RequestParam requestParam = new com.wechat.pay.java.core.notification.RequestParam.Builder()
.serialNumber(serialNo)
.nonce(nonceStr)
.signature(signature)
.timestamp(timestamp)
.signType(signType)
.body(body)
.build();
// 如果已经初始化了 RSAAutoCertificateConfig,可以直接使用 config
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(notificationConfig);
// 验签、解密并转换成 Transaction
Map transaction = parser.parse(requestParam, Map.class);
return transaction;
} catch (Exception e) {
e.printStackTrace();
myWriter(response,500);
log.error("微信支付回调Exception",e);
}
return null;
}
/**
* 生成指定范围的随机数字
* @param scope 需要生成的随机数字的个数
* @param total 数字范围
* @return
*/
public static List<Integer> randomNum(int scope, int total) {
List<Integer> mylist = new ArrayList<>(); // 用于储存不重复的随机数
Random rd = new Random();
while (mylist.size() < scope) {
int myNum = rd.nextInt(total);
if (!mylist.contains(myNum += 1)) { // 判断容器中是否包含指定的数字
mylist.add(myNum); // 往集合里面添加数据。
}
}
return mylist;
}
/**
* 对支付回调预处理
* 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/payment-notice.html
* @param request
* @param response
* @return
*/
public static Map paymentCallbackHandle(HttpServletRequest request, HttpServletResponse response){
try {
//获取报文
String body = IOUtils.toString(request.getInputStream(), "UTF-8");
//随机串
String nonceStr = request.getHeader("Wechatpay-Nonce");
//微信传递过来的签名
String signature = request.getHeader("Wechatpay-Signature");
//证书序列号(微信平台)
String serialNo = request.getHeader("Wechatpay-Serial");
//时间戳
String timestamp = request.getHeader("Wechatpay-Timestamp");
// 签名方式
String signType = request.getHeader("Wechatpay-Signature-Type");
// 构造 RequestParam
com.wechat.pay.java.core.notification.RequestParam requestParam = new com.wechat.pay.java.core.notification.RequestParam.Builder()
.serialNumber(serialNo)
.nonce(nonceStr)
.signature(signature)
.timestamp(timestamp)
.signType(signType)
.body(body)
.build();
// 如果已经初始化了 RSAAutoCertificateConfig,可以直接使用 config
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(notificationConfig);
// 验签、解密并转换成 Transaction
Map transaction = parser.parse(requestParam, Map.class);
return transaction;
} catch (Exception e) {
e.printStackTrace();
myWriter(response,500);
log.error("微信支付回调Exception",e);
return null;
}
}
private static void myWriter(HttpServletResponse response,int code) {
try {
// 设置HTTP应答状态码,例如200 OK
Map map = new HashMap();
if (code == 200){
response.setStatus(200);
map.put("code","SUCCESS");
map.put("message","成功");
}else{
response.setStatus(500);
map.put("code","FAIL");
map.put("message","失败");
}
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
PrintWriter writer = response.getWriter();
String resultStr = JSON.toJSONString(map);
writer.print(resultStr);
writer.close();
} catch (IOException e) {
log.error("Exception",e);
}
}
}