Java对接第三方支付渠道之微信支付APIV3版本
Java对接第三方支付渠道之微信支付APIV3版本提示微信支付APIV3版本对接流程梳理,目前微信支付提供APIV3和APIV2两个版本,简而言之,V3版本的安全性比V2更高。
- 一、接入指引
- 1.获取商户号
- 2.获取AppID
- 3.申请商户证书
- 4.获取微信的证书
- 5.获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
- 二、导入依赖
- 三、书写配置类
- 四、书写工具类
- 1.订单状态枚举类
- 2.支付类型枚举类
- 3.微信支付相关地址枚举类
- 五、调用相关支付接口
- 1.流程图
- 2.发起支付
- 3.支付通知
- 4.查询订单
- 5.取消支付
一、接入指引
是为了获取标识商户身份的信息、商户的证书和私钥、微信支付的证书、微信支付API的URL
1.获取商户号微信商户平台https://pay.eixin.qq./ 步骤申请成为商户 => 提交资料 => 签署协议 => 获取商户号
2.获取AppID微信公众平台https://mp.eixin.qq./ 步骤注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
3.申请商户证书步骤登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥
4.获取微信的证书可以预先下载,也可以通过编程的方式获取
我这里是直接下载在本地的
步骤登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥
二、导入依赖
.github.echatpay-apiv3 echatpay-apache-httpclient0.4.5
三、书写配置类
这里导入支付秘钥的时候,我直接选择的是导入秘钥字符串,没有选择导入秘钥文件
@Configuration @PropertySource("classpath:xpay.properties") //读取配置文件 @ConfigurationProperties(prefix="xpay") //读取xpay节点 @Data //使用set方法将xpay节点中的值填充到当前类的属性中 @Slf4j public class WxPayConfig { private String mchId; private String mchSerialNo; private String privateKeyString; private String apiV3Key; private String appid; private String domain; private String notifyDomain; private PrivateKey getPrivateKey(String privateKeyString){ log.info("开始获取私钥"); try { //PrivateKey privateKey = PemUtil.loadPrivateKey(ne FileInputStream(privateKeyString)); PrivateKey privateKey = PemUtil.loadPrivateKey( ne ByteArrayInputStream(privateKeyString.getBytes("utf-8"))); log.info("获取私钥成功"); return privateKey; } catch (UnsupportedEncodingException e) { e.printStackTrace(); log.error("获取私钥文件失败", e); thro ne ServiceErrorException(ResultCode.ERROR, "私钥不存在"); } } @Bean public Verifier getVerifier(){ log.info("开始获取签名验证器"); //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyString); //微信证书校验器 Verifier verifier=null; try{ //获取证书管理器实例 CertificatesManager certificatesManager=CertificatesManager.getInstance(); //私钥签名对象(加密) PrivateKeySigner privateKeySigner = ne PrivateKeySigner(mchSerialNo, privateKey); //身份认证对象(解密) WechatPay2Credentials echatPay2Credentials = ne WechatPay2Credentials(mchId, privateKeySigner); //向证书管理器增加需要自动更新平台证书的商户信息(默认时间间隔:24小时) certificatesManager.putMerchant(mchId,echatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8)); //从证书管理器中获取verifier verifier=certificatesManager.getVerifier(mchId); } catch(Exception e){ log.error("获取签名验证器失败", e); thro ne ServiceErrorException(ResultCode.ERROR, "获取签名验证器失败"); } log.info("获取签名验证器成功"); return verifier; } @Bean public HttpClient httpClientWithSign(){ log.info("开始获取httpClientWithSign"); //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyString); //微信证书校验器 Verifier verifier=getVerifier(); WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create() //设置商户信息 .ithMerchant(mchId,mchSerialNo,privateKey) .ithValidator(ne WechatPay2Validator(verifier)); CloseableHttpClient httpClient=builder.build(); log.info("获取httpClientWithSign结束"); return httpClient; } @Bean public HttpClient httpClientWithNoSign(){ log.info("开始获取httpClientWithNoSign"); //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyString); //用于构造HttpClient WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create() //设置商户信息 .ithMerchant(mchId,mchSerialNo,privateKey) //无需进行签名验证、通过ithValidator((response) -> true)实现 .ithValidator(response->true); CloseableHttpClient httpClient=builder.build(); log.info("获取httpClientWithNoSign结束"); return httpClient; } }
四、书写工具类 1.订单状态枚举类
@AllArgsConstructor @Getter public enum OrderStatus { NOTPAY("未支付"), SUCCESS("支付成功"), CLOSED("超时已关闭"), CANCEL("用户已取消"), private final String type; }2.支付类型枚举类
之所以书写这个枚举类,是因为我们在对接第三方支付的时候,一般会对接微信支付和支付包支付两种方式。
@AllArgsConstructor @Getter public enum PayType { WXPAY("微信"), ALIPAY("支付宝"); private final String type; }3.微信支付相关地址枚举类
package .ruiya.mons.enums.xpay; import lombok.AllArgsConstructor; import lombok.Getter; @AllArgsConstructor @Getter public enum WxApiType { NATIVE_PAY("/v3/pay/transactions/native"), ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"), CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"), private final String type; }五、调用相关支付接口 1.流程图 2.发起支付
https://pay.eixin.qq./iki/doc/apiv3/apis/chapter3_4_1.shtml
@Override public String nativePay(String orderNo, String uid) thros Exception{ //根据orderNo获取orderInfo OrderInfo orderInfo = orderInfoService.getOne(ne LambdaQueryWrapper3.支付通知() .eq(OrderInfo::getOrderNo, orderNo) .eq(OrderInfo::getUid, uid)); if (Objects.isNull(orderInfo)){ thro ne ServiceErrorException(ResultCode.ERROR, "发起支付请求失败 - 订单不存在"); } String codeUrl = orderInfo.getCodeUrl(); String orderStatus = orderInfo.getOrderStatus(); //支付二维码不为空 &&订单的支付状态为支付中 if(!StringUtils.isEmpty(codeUrl) && OrderStatus.NOTPAY.getType().equals(orderStatus)){ log.info("订单已存在,二维码已保存"); //返回二维码 return codeUrl; } //更新订单的支付类型 orderInfoService.updatePayTypeByOrderNo(orderInfo.getOrderNo(), PayType.WXPAY); //调用统一下单API HttpPost httpPost=ne HttpPost(xPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType())); // 请求body参数 Gson gson = ne Gson(); Map paramsMap = ne HashMap(); paramsMap.put("appid", xPayConfig.getAppid()); paramsMap.put("mchid", xPayConfig.getMchId()); paramsMap.put("description", orderInfo.getTitle()); paramsMap.put("out_trade_no", orderInfo.getOrderNo()); paramsMap.put("notify_url", xPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType())); Map amountMap = ne HashMap(); BigDecimal payMoney = orderInfo.getTotalFee(); BigDecimal total = payMoney.multiply(ne BigDecimal("100")); total = total.setScale(0,BigDecimal.ROUND_UP); //支付金额单位分 amountMap.put("total", total.intValue()); amountMap.put("currency", "CNY"); paramsMap.put("amount", amountMap); //将参数转换成json字符串 String jsonParams = gson.toJson(paramsMap); log.info("请求参数" + jsonParams); StringEntity entity = ne StringEntity(jsonParams,"utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Aept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = (CloseableHttpResponse) httpClientWithSign.execute(httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity());//响应体 int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 log.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204) { //处理成功,无返回Body log.info("成功"); } else { log.info("Native下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString); thro ne ServiceErrorException(ResultCode.ERROR, "发起支付请求失败 - Native下单失败" + bodyAsString); } //响应结果 Map resultMap = gson.fromJson(bodyAsString, HashMap.class); //获取二维码 codeUrl = resultMap.get("code_url"); //保存支付二维码 orderInfoService.saveCodeUrl(orderNo, codeUrl); return codeUrl; }finally { response.close(); } }
https://pay.eixin.qq./iki/doc/apiv3/apis/chapter3_4_5.shtml
@PostMapping("/native/notify") public String nativeNotify(HttpServletRequest request, HttpServletResponse response){ log.info("收到微信回调"); Gson gson = ne Gson(); Map4.查询订单map = ne HashMap<>();//应答对象 try { //处理通知参数 String body = HttpUtils.readData(request); Map bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String)bodyMap.get("id"); log.info("支付通知的id ===> {}", requestId); //签名的验证 String serialNumber = request.getHeader("Wechatpay-Serial"); String nonce = request.getHeader("Wechatpay-Nonce"); String timestamp = request.getHeader("Wechatpay-Timestamp"); String signature = request.getHeader("Wechatpay-Signature");// 请求头Wechatpay-Signature // 构造微信请求体 NotificationRequest xRequest = ne NotificationRequest.Builder().ithSerialNumber(serialNumber) .ithNonce(nonce) .ithTimestamp(timestamp) .ithSignature(signature) .ithBody(body) .build(); NotificationHandler handler = ne NotificationHandler(xPayConfig.getVerifier(), xPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); Notification notification = null; try { // 验签和解析请求体 notification = handler.parse(xRequest); Assert.assertNotNull(notification); } catch (Exception e) { log.error("通知验签失败"); //失败应答 response.setStatus(500); map.put("code", "ERROR"); map.put("message", "通知验签失败"); return JSON.toJSONString(map); } log.info("通知验签成功"); // 从notification中获取解密报文,并解析为HashMap String plainText = notification.getDecryptData(); log.info("解密报文{}",plainText); //支付成功后 - 处理订单 xPayService.processOrder(plainText); //成功应答 response.setStatus(200); return gson.toJson(map); } catch (Exception e) { e.printStackTrace(); log.error("失败应答"); //失败应答 response.setStatus(500); map.put("code", "FALL"); map.put("message", "失败"); return gson.toJson(map); } } @Override @Transactional(rollbackFor = Exception.class) public void processOrder(String plainText) { log.info("用户支付成功 - 开始处理订单!"); Gson gson = ne Gson(); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); //订单号 String orderNo = (String)plainTextMap.get("out_trade_no"); //尝试获取锁 // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放 if(lock.tryLock()){ try { //1.处理重复的通知 //接口调用的幂等性无论接口被调用多少次,产生的结果是一致的。 String orderStatus = orderInfoService.getOrderStatus(orderNo); if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){ log.info("重复订单"); return; } //2.更新订单状态 orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS); //3.记录支付日志 paymentInfoService.createPaymentInfo(plainText); //根据订单号获取订单信息 OrderInfo orderInfo = orderInfoService.getUidByOrderInfo(orderNo); String title = orderInfo.getTitle(); String uid = orderInfo.getUid(); //4.保存购买课程成功的消息提示 noticeService.saveBuyNotice(title,uid); //5.移除延时队列中的订单信息 log.info("延时队列中移除订单信息"); orderDelayQueue.removeToOrderDelayQueue(orderNo); //6.发送支付成功的消息给前端用户 String msg = uid+"|"+title; ebSocket.sendToUser(msg); } finally { //要主动释放锁 lock.unlock(); } } log.info("用户支付成功 - 处理订单结束!"); }
https://pay.eixin.qq./iki/doc/apiv3/apis/chapter3_4_2.shtml
@Override @Transactional(rollbackFor = Exception.class) public boolean queryOrder(String orderNo) thros Exception{ log.info("查单接口调用 ===> {}", orderNo); String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo); url = xPayConfig.getDomain().concat(url).concat("?mchid=").concat(xPayConfig.getMchId()); HttpGet httpGet = ne HttpGet(url); httpGet.setHeader("Aept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = (CloseableHttpResponse) httpClientWithSign.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity());//响应体 int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 log.info("成功, 返回结果 = " + bodyAsString); Gson gson = ne Gson(); Map5.取消支付resultMap = gson.fromJson(bodyAsString, HashMap.class); //获取微信支付端的订单状态 String tradeState = resultMap.get("trade_state"); //判断订单状态 if(WxTradeState.SUCCESS.getType().equals(tradeState)){ log.arn("查询订单已支付 ===> {}", orderNo); //如果确认订单已支付则更新本地订单状态 orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); //记录支付日志 paymentInfoService.createPaymentInfo(bodyAsString); //根据订单号获取订单信息 OrderInfo orderInfo = orderInfoService.getUidByOrderInfo(orderNo); String title = orderInfo.getTitle(); String uid = orderInfo.getUid(); //保存购买课程成功的消息提示 noticeService.saveBuyNotice(title,uid); //移除延时队列中的订单信息 log.info("延时队列中移除订单信息"); orderDelayQueue.removeToOrderDelayQueue(orderNo); //发送支付成功的消息给前端用户 String msg = uid+"|"+title; ebSocket.sendToUser(msg); return true; } if(WxTradeState.NOTPAY.getType().equals(tradeState)){ log.info("微信支付 - 支付未完成 - 请扫码支付"); return false; } } else if (statusCode == 204) { //处理成功,无返回Body log.info("成功"); return false; } else { log.info("查单接口调用,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString); thro ne ServiceErrorException(ResultCode.ERROR, "微信支付 - 查询订单接口调用失败"); } } finally { response.close(); } return false; }
https://pay.eixin.qq./iki/doc/apiv3/apis/chapter3_4_3.shtml
@Override public void closeOrder(String orderNo){ log.info("关单接口的调用,订单号 ===> {}", orderNo); //创建远程请求对象 String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo); url = xPayConfig.getDomain().concat(url); HttpPost httpPost = ne HttpPost(url); //组装json请求体 Gson gson = ne Gson(); MapparamsMap = ne HashMap<>(); paramsMap.put("mchid", xPayConfig.getMchId()); String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}", jsonParams); //将请求参数设置到请求对象中 StringEntity entity = ne StringEntity(jsonParams,"utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Aept", "application/json"); try { //完成签名并执行请求 CloseableHttpResponse response = (CloseableHttpResponse) httpClientWithSign.execute(httpPost); try { int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 log.info("用户取消订单成功 - 200"); } else if (statusCode == 204) { //处理成功,无返回Body log.info("用户取消订单成功 - 204"); } else { log.info("用户取消订单失败,响应码 = " + statusCode); thro ne ServiceErrorException(ResultCode.ERROR, "用户取消订单失败"); } } finally { response.close(); } } catch (IOException e) { log.error("调用微信支付关闭订单接口失败 - {}", e); e.printStackTrace(); thro ne ServiceErrorException(ResultCode.ERROR, "调用微信支付关闭订单接口失败"); } }
我这边的JDK版本是jdk1.8.0_333,如果有开发者在读取秘钥的时候获取不到系统路径,获取秘钥文件是没存地址以及提示读取了非法长度的字符串等报错提示,证明当前jdk的版本过低,需要升级jdk的版本