V3版 jsapi微信支付流程(Springboot)
一、开发准备
jsapi微信支付需要信息如下:
1.微信公众平台申请:AppId
2.微信支付平台申请:
(1)商户ID(mcdid)绑定APPID及mchid;
(2)商户API秘钥(apiV3Key):API v3密钥主要用于平台证书解密、回调信息解密;
登录微信商户平台,进入【账户中心 > API安全 > API安全】目录,点击【设置密钥】。
(3)商户Api证书、证书秘钥。商户API证书具体使用说明可参见接口规则文档中私钥和证书章节
商户可登录微信商户平台,在【账户中心】->【API安全】->【API证书】目录下载证书
详情请阅读微信支付光放文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml
二、开发步骤
1.引入库
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.2.2</version>
</dependency>
源码地址:https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
2.初始化httpClient
配置httpClient之后再调用之后的下单接口、查询接口后不需要配置签名可直接调用。
/**
* 初始化微信支付httpClient,此方法通过自动更新微信支付平台证书实现(自动更新证书功能),无需下载微信支付平台证书
* privateKey 商户API私钥,加载证书获取
* apiv3 API v3密钥
* mchid 商户id
* serialNumber 商户API证书的证书序列号,加载证书获取
*使用完成之后记得调用httpClient.close()方法。
* @return
*/
public CloseableHttpClient initWChatClient() {
String apiv3 = ConstantWxPayPropertiesUtils.KEY;
String mchid = ConstantWxPayPropertiesUtils.MCH_ID;
//api证书序列号
String serialNumber = ConstantWxPayPropertiesUtils.SERIAL_NO;
//获取证书信息
KeyPair keyPair = createPKCS12(ConstantWxPayPropertiesUtils.CERT_PATH, mchid);
PrivateKey merchantPrivateKey = keyPair.getPrivate();
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(mchid, new PrivateKeySigner(serialNumber, merchantPrivateKey)),
apiv3.getBytes(StandardCharsets.UTF_8));
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchid, serialNumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier));
return builder.build();
}
/**
* //获取KeyPair
*
* @param keyPath 微信支付Api证书路径
* @param keyPass 初始密码为商户号mchid
* @return
*/
public KeyPair createPKCS12(String keyPath, String keyPass) {
String keyAlias = "Tenpay Certificate";// 证书的别名,固定为Tenpay Certificate
ClassPathResource resource = new ClassPathResource(keyPath);
char[] pem = keyPass.toCharArray();
try {
synchronized (lock) {
if (store == null) {
synchronized (lock) {
store = KeyStore.getInstance("PKCS12");
store.load(resource.getInputStream(), pem);
}
}
}
X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias);
certificate.checkValidity();
//证书序列号,需要的可以自己写方法获取
String serialNo = certificate.getSerialNumber().toString(16).toLowerCase();
// 证书的 公钥
PublicKey publicKey = certificate.getPublicKey();
PrivateKey privateKey = (PrivateKey) store.getKey(keyAlias, pem);
return new KeyPair(publicKey, privateKey);
} catch (Exception e) {
throw new IllegalStateException("Cannot load keys from store: " + resource, e);
}
}
证书存放路径,可通过配置文件加载
3.jsapi调用下单请求
请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
请求方式:POST
/**
* 微信支付,请求下单
*/
@Override
public CloseableHttpResponse createWxOrder(BigDecimal price, String desc, String openId,String orderNo) {
//初始化httpClient
CloseableHttpClient httpClient = rsaUtil.initWChatClient();
try {
//请求微信服务器下单接口,生成预付单
//存入订单到数据库中
//微信下单请求URL
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
MapUtils requestBody = new MapUtils()
.put("appid",ConstantWxPropertiesUtils.WX_OPEN_APP_ID)
.put("mchid", ConstantWxPayPropertiesUtils.MCH_ID)
.put("description", desc)
.put("out_trade_no", orderNo)
.put("notify_url", ConstantWxPayPropertiesUtils.PAY_NOTIFY_URL);
MapUtils amountMap = new MapUtils()
.put("total", 1)
.put("currency", "CNY");
requestBody.put("amount", amountMap);
MapUtils payerMap = new MapUtils().put("openid", openId);
requestBody.put("payer", payerMap);
String jsonString = JSONObject.toJSONString(requestBody);
// 请求body参数
StringEntity entity = new StringEntity(jsonString);
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
rsaUtil.closeHttp(httpClient);
return response;
} catch (IOException e) {
rsaUtil.closeHttp(httpClient);
throw new ApiException("请求微信下单失败",500);
}
}
请求下单接口完成之后需返回如下信息:appId,timeStamp,nonceStr,package,signType,paySign供前端调起微信支付。注意:以下字段名字母大小写敏感
/**
* 生成签名
* @param signStr 需要签名的字符串
* @return
*/
private String generateSign(String signStr) {
try {
String mchid = ConstantWxPayPropertiesUtils.MCH_ID;
KeyPair keyPair = createPKCS12(ConstantWxPayPropertiesUtils.CERT_PATH, mchid);
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(keyPair.getPrivate());
sign.update(signStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(sign.sign());
} catch (Exception e) {
throw new ApiException("签名异常错误", 500);
}
}
/**
* 下单签名
* @param packageStr
* @return
*/
public String paySign(String packageStr,long timestamp,String nonceStr) {
String appId = ConstantWxPropertiesUtils.WX_OPEN_APP_ID;
String signatureStr =
Stream.of(appId, String.valueOf(timestamp), nonceStr, packageStr).collect(Collectors.joining("\n", ""
, "\n"));
return generateSign(signatureStr);
}
下单请求参与签名的字段如下:
4.jsapi调起支付(此过程无需后台参与)
请求参数就是第三部中下单操作返回的结果。
5.支付结果通知
@PostMapping("callback")
public Map<String, String> callback(HttpServletRequest request) {
log.debug("========================支付回调开始===============================");
try {
String requestBody = HttpRequestHelper.getRequestBody(request);
if (verifiedSign(request, requestBody)) {
WxPayCallBackDataVo wxPayCallBackDataVo = JSONObject.parseObject(requestBody,
WxPayCallBackDataVo.class);
//如果支付成功
if ("TRANSACTION.SUCCESS".equals(wxPayCallBackDataVo.getEvent_type())) {
//通知资源数据
WxPayCallBackDataVo.WxPayResourceDataVo resource = wxPayCallBackDataVo.getResource();
//解密后资源数据
String notifyResponseBody = rsaUtil.decryptResponseBody(ConstantWxPayPropertiesUtils.KEY,
resource.getAssociated_data(), resource.getNonce(), resource.getCiphertext());
//微信回调通知数据
NotifyResultVo resultVo = JSONObject.parseObject(notifyResponseBody, NotifyResultVo.class);
String outTradeNo = resultVo.getOut_trade_no();
OrderEntity orderEntity = orderService.getOrderInfoByOutTradeNo(outTradeNo);
if (orderEntity != null) {
if (orderEntity.getOrderStatus().equals(OrderStatusEnum.NOT_PAID.getStatus())) {
//根据微信之后返回的结果对订单进行不同的处理
if ("SUCCESS".equals(resultVo.getTrade_state())) {
orderService.updateOrderPaySuccess(orderEntity, resultVo);
} else {
orderService.updateOrderPayFail(orderEntity, resultVo);
}
}
}
} else {
log.error("微信返回支付错误摘要:" + wxPayCallBackDataVo.getSummary());
}
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("callback...");
Map<String, String> mapUtils = new HashMap<>();
mapUtils.put("code", "SUCCESS");
mapUtils.put("message", "");
log.debug("========================支付回调结束===============================");
return mapUtils;
}
/**
* 获取请求文体
* @param request
* @return
* @throws IOException
*/
public static String getRequestBody(HttpServletRequest request) throws IOException {
BufferedReader reader = null;
StringBuffer sb = new StringBuffer();
try {
ServletInputStream stream = request.getInputStream();
// 获取响应
reader = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
throw new ApiException("读取返回支付接口数据流出现异常!");
} finally {
reader.close();
}
return sb.toString();
}
/**
* 解密响应体.
*
* @param apiV3Key API V3 KEY API v3密钥 商户平台设置的32位字符串
* @param associatedData response.body.data[i].encrypt_certificate.associated_data
* @param nonce response.body.data[i].encrypt_certificate.nonce
* @param ciphertext response.body.data[i].encrypt_certificate.ciphertext
* @return the string
* @throws GeneralSecurityException the general security exception
*/
public String decryptResponseBody(String apiV3Key,String associatedData, String nonce, String ciphertext) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
byte[] bytes;
try {
bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
} catch (GeneralSecurityException e) {
throw new IllegalArgumentException(e);
}
return new String(bytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
验证签名:
// 定义全局容器 保存微信平台证书公钥 注意线程安全
public static Map<String, X509Certificate> certificateMap = new ConcurrentHashMap<>();
/**
* 验证微信签名,必须要验证签名哦
*
* @param request
* @param body
* @return
* @throws GeneralSecurityException
* @throws IOException
* @throws InstantiationException
* @throws IllegalAccessException
* @throws ParseException
*/
private boolean verifiedSign(HttpServletRequest request, String body) throws GeneralSecurityException,
ParseException {
//微信返回的证书序列号
String serialNo = request.getHeader("Wechatpay-Serial");
//微信返回的随机字符串
String nonceStr = request.getHeader("Wechatpay-Nonce");
//微信返回的时间戳
String timestamp = request.getHeader("Wechatpay-Timestamp");
//微信返回的签名
String wechatSign = request.getHeader("Wechatpay-Signature");
//组装签名字符串
String signStr = Stream.of(timestamp, nonceStr, body)
.collect(Collectors.joining("\n", "", "\n"));
//当证书容器为空 或者 响应提供的证书序列号不在容器中时 就应该刷新了
if (ConstantWxPayPropertiesUtils.certificateMap.isEmpty() || !ConstantWxPayPropertiesUtils.certificateMap.containsKey(serialNo)) {
rsaUtil.refreshCertificate();
}
//根据序列号获取平台证书
X509Certificate certificate = ConstantWxPayPropertiesUtils.certificateMap.get(serialNo);
//获取失败 验证失败
if (certificate == null) {
return false;
}
//SHA256withRSA签名
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(certificate);
signature.update(signStr.getBytes());
//返回验签结果
return signature.verify(Base64Utils.decodeFromString(wechatSign));
}
6.订单状态查询
由于网络异常或者系统的波动,可能会导致用户支付成功,但是商户侧未能成功接收到支付结果通知,进而显示订单未支付的情况。商户侧的订单状态更新不及时,容易造成用户投诉,甚至是重复支付的情况发生。
1、商户APP或者前端页面收到支付返回时,商户需要调用商户查单接口确认订单状态,并把查询结果展示给用户。
2、商户后台需要准确、高效地处理微信支付发送的异步支付结果通知,并按接口规范把处理结果返回给微信支付。
3、商户后台未收到异步支付结果通知时,商户应该主动调用《微信支付查单接口》,同步订单状态。
4、商户在T+1日从微信支付侧获取T日的交易账单,并与商户系统中的订单核对。如出现订单在微信支付侧成功,但是在商户侧未成功的情况,商户需要给用户补发货或者退款处理。
后端服务处理:
方案一
以订单下单成功时间为基准(或者以前端支付返回成功或者报错后,第一次调用商户查单接口未成功的时间为基准),每隔5秒/30秒/1分钟/3分钟/5分钟/10分钟/30分钟调用《微信支付查单接口》查询一次,最后一次查询还是未返回支付成功状态,则停止后续查询,并调用《关单接口》关闭订单。(轮询时间间隔和次数,商户可以根据自身业务场景灵活设置)
方案二
定时任务每隔30秒启动一次,找出最近10分钟内创建并且未支付的订单,调用《微信支付查单接口》核实订单状态。系统记录订单查询的次数,在10次查询之后状态还是未支付成功,则停止后续查询,并调用《关单接口》关闭订单。(轮询时间间隔和次数,商户可以根据自身业务场景灵活设置)
此处采用第二种方式完成订单轮训查询方式:
/**
* 微信订单状态定时查询
*/
@Scheduled(cron = "0/30 * * * * ?")//定时查询时间可根据自身业务进行修改
public void queryPayStatus(){
log.debug("===============订单状态查询定时任务已启动======================");
int count = 5;查询次数,在订单表中新增字段用于记录查询次数
int timeMin = 10; //查询10分钟之内的订单。
//查询创建十分钟之内并且未未支付的订单,并且查单次数不超过5的数据
List<OrderEntity> list = orderService.list(new QueryWrapper<OrderEntity>().eq("order_status",
OrderStatusEnum.NOT_PAID.getStatus()).gt("create_dt", DateUtils.addMinute(new Date(), -timeMin)).lt("check_status_count",count));
log.debug("未支付订单数据量"+list.size());
list.stream().forEach(orderInfo->{
log.debug("订单号:=================="+orderInfo.getOrderNo());
NotifyResultVo notifyResultVo = orderService.queryOrderStatus(orderInfo);
//如果付款成功
if ("SUCCESS".equals(notifyResultVo.getTrade_state())) {
//修改成功结果参数
orderService.updateOrderPaySuccess(orderInfo, notifyResultVo);
} else{
//支付不成功则修改订单查询次数
Integer checkStatusCount = orderInfo.getCheckStatusCount();
checkStatusCount++;
orderInfo.setCheckStatusCount(checkStatusCount);
orderService.updateById(orderInfo);
if(checkStatusCount == count){
//最后一次查询扔未成功,需调用关闭订单接口
orderService.updateOrderPayFail(orderInfo, notifyResultVo);
}
}
});
log.debug("=================订单状态查询定时任务已结束===================");
}
7.关闭订单
订单超时未支付取消功能可通过redis中的key失效监听功能实现(此处采用),定时任务查询订单或者利用rabbitMq实现延迟消费
public void cancelOrder(Long id) throws IOException {
log.debug("==========================订单取消log开始=========================");
OrderEntity orderEntity = this.baseMapper.selectById(id);
if(orderEntity.getOrderStatus()==OrderStatusEnum.NOT_PAID.getStatus()){
orderEntity.setOrderStatus(OrderStatusEnum.CANCEL.getStatus());
String orderDetailJson = orderEntity.getOrderDetailJson();
JSONObject jsonObject = JSONObject.parseObject(orderDetailJson);
long venueBookId = jsonObject.getLongValue("venueBookId");
venueBookService.removeById(venueBookId);
venueBookDetailService.remove(new QueryWrapper<VenueBookDetailEntity>().eq("venue_book_id", venueBookId));
redisUtils.delete(Constant.ORDER_HEAD + id);
this.baseMapper.updateById(orderEntity);
PaymentInfoEntity paymentInfoEntity = paymentInfoDao.selectOne(new QueryWrapper<PaymentInfoEntity>().eq(
"order_id", orderEntity.getId()));
String outTradeNo = paymentInfoEntity.getOutTradeNo();
//发送微信请求取消订单
CloseableHttpClient httpClient = rsaUtil.initWChatClient();
//微信下单请求URL
HttpPost httpPost =
new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/" + outTradeNo + "/close");
MapUtils requestBody = new MapUtils()
.put("mchid", ConstantWxPayPropertiesUtils.MCH_ID);
String jsonString = JSONObject.toJSONString(requestBody);
// 请求body参数
StringEntity entity = new StringEntity(jsonString);
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
log.debug("订单取消响应码:" + statusCode);
}
log.debug("==========================订单取消log结束=========================");
}
总结
微信支付主要是签名校验之类的比较繁琐,本身实现还是比较简单,初始化好httpClient,后面只要凭借参数直接调用微信接口就好了。
注意一个大坑:
如果签名中出现:
InvalidKeyException: Illegal key size错误,
解决方式是替换jdk里面的local_policy.jar,US_export_policy.jar,不过我发现只要jdk版本高于java8-251以上的基本上不会有这个问题,低于这个版本就老老实实修改local_policy.jar,US_export_policy.jar。