Java对接第三方支付渠道之微信支付APIV3版本
一、接入指引
1.获取商户号
2.获取AppID
3.申请商户证书
4.获取微信的证书
5.获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
二、导入依赖
三、书写配置类
四、书写工具类
1.订单状态枚举类
2.支付类型枚举类
3.微信支付相关地址枚举类
五、调用相关支付接口
1.流程图
2.发起支付
3.支付通知
4.查询订单
5.取消支付
总结
一、接入指引
是为了获取:标识商户身份的信息、商户的证书和私钥、微信支付的证书、微信支付API的URL
1.获取商户号
微信商户平台:https://pay.weixin.qq.com/ 步骤:申请成为商户 => 提交资料 => 签署协议 => 获取商户号
2.获取AppID
微信公众平台:https://mp.weixin.qq.com/ 步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
3.申请商户证书
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥
4.获取微信的证书
可以预先下载,也可以通过编程的方式获取
我这里是直接下载在本地的
5.获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥
二、导入依赖
com.github.wechatpay-apiv3
wechatpay-apache-httpclient
0.4.5
三、书写配置类
这里导入支付秘钥的时候,我直接选择的是导入秘钥字符串,没有选择导入秘钥文件
/**
* description:微信支付配置文件
* author:maozl
* date:2022/7/19
*/
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
/**
* 商户号
*/
private String mchId;
/**
* 商户API证书序列号
*/
private String mchSerialNo;
/**
* 商户私钥字符串
*/
private String privateKeyString;
/**
* APIv3密钥
*/
private String apiV3Key;
/**
* APPID
*/
private String appid;
/**
* 微信服务器地址
*/
private String domain;
/**
* 回调地址
*/
private String notifyDomain;
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
private PrivateKey getPrivateKey(String privateKeyString){
log.info("开始获取私钥");
try {
//PrivateKey privateKey = PemUtil.loadPrivateKey(new FileInputStream(privateKeyString));
PrivateKey privateKey = PemUtil.loadPrivateKey(
new ByteArrayInputStream(privateKeyString.getBytes("utf-8")));
log.info("获取私钥成功");
return privateKey;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
log.error("获取私钥文件失败", e);
throw new ServiceErrorException(ResultCode.ERROR, "私钥不存在");
}
}
/**
* 获取微信签名验证器 (定时更新平台证书功能)
* @return
*/
@Bean
public Verifier getVerifier(){
log.info("开始获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyString);
//微信证书校验器
Verifier verifier=null;
try{
//获取证书管理器实例
CertificatesManager certificatesManager=CertificatesManager.getInstance();
//私钥签名对象(加密)
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象(解密)
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
//向证书管理器增加需要自动更新平台证书的商户信息(默认时间间隔:24小时)
certificatesManager.putMerchant(mchId,wechatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8));
//从证书管理器中获取verifier
verifier=certificatesManager.getVerifier(mchId);
}
catch(Exception e){
log.error("获取签名验证器失败", e);
throw new ServiceErrorException(ResultCode.ERROR, "获取签名验证器失败");
}
log.info("获取签名验证器成功");
return verifier;
}
/**
* 获取http请求对象
* 给容器中加入WechatPay的HttpClient,虽然它是WechatPay的,
* 但可以用它给任何外部发请求,因为它只对发给WechatPay的请求做处理而不对发给别的的请求做处理.
* @return
*/
@Bean
public HttpClient httpClientWithSign(){
log.info("开始获取httpClientWithSign");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyString);
//微信证书校验器
Verifier verifier=getVerifier();
WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId,mchSerialNo,privateKey)
.withValidator(new WechatPay2Validator(verifier));
CloseableHttpClient httpClient=builder.build();
log.info("获取httpClientWithSign结束");
return httpClient;
}
/**
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
* @return
*/
@Bean
public HttpClient httpClientWithNoSign(){
log.info("开始获取httpClientWithNoSign");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyString);
//用于构造HttpClient
WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId,mchSerialNo,privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator(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 com.ruiya.commons.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum WxApiType {
/**
* Native下单
*/
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.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
/**
* 统一下单 - 生成支付二维码
* @param orderNo 订单编号
* @param uid 用户id
* @return
*/
@Override
public String nativePay(String orderNo, String uid) throws Exception{
//根据orderNo获取orderInfo
OrderInfo orderInfo = orderInfoService.getOne(new LambdaQueryWrapper()
.eq(OrderInfo::getOrderNo, orderNo)
.eq(OrderInfo::getUid, uid));
if (Objects.isNull(orderInfo)){
throw new 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=new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", orderInfo.getTitle());
paramsMap.put("out_trade_no", orderInfo.getOrderNo());
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
Map amountMap = new HashMap();
BigDecimal payMoney = orderInfo.getTotalFee();
BigDecimal total = payMoney.multiply(new 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 = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "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);
throw new 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();
}
}
3.支付通知
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
/**
* 支付通知
* 微信支付通过支付通知接口将用户支付成功消息通知给商户
* @param request
* @param response
* @return
*/
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
log.info("收到微信回调");
Gson gson = new Gson();
Map map = new 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 wxRequest = new NotificationRequest.Builder().withSerialNumber(serialNumber)
.withNonce(nonce)
.withTimestamp(timestamp)
.withSignature(signature)
.withBody(body)
.build();
NotificationHandler handler = new
NotificationHandler(wxPayConfig.getVerifier(), wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
Notification notification = null;
try {
// 验签和解析请求体
notification = handler.parse(wxRequest);
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);
//支付成功后 - 处理订单
wxPayService.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);
}
}
/**
* 支付成功后 - 处理订单
* @param plainText
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void processOrder(String plainText) {
log.info("用户支付成功 - 开始处理订单!");
Gson gson = new 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;
webSocket.sendToUser(msg);
} finally {
//要主动释放锁
lock.unlock();
}
}
log.info("用户支付成功 - 处理订单结束!");
}
4.查询订单
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml
/**
* 查询订单
* @param orderNo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean queryOrder(String orderNo) throws Exception{
log.info("查单接口调用 ===> {}", orderNo);
String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept", "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 = new Gson();
Map resultMap = gson.fromJson(bodyAsString, HashMap.class);
//获取微信支付端的订单状态
String tradeState = resultMap.get("trade_state");
//判断订单状态
if(WxTradeState.SUCCESS.getType().equals(tradeState)){
log.warn("查询订单已支付 ===> {}", 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;
webSocket.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);
throw new ServiceErrorException(ResultCode.ERROR, "微信支付 - 查询订单接口调用失败");
}
} finally {
response.close();
}
return false;
}
5.取消支付
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml
/**
* 微信支付关闭订单
* @param orderNo
*/
@Override
public void closeOrder(String orderNo){
log.info("关单接口的调用,订单号 ===> {}", orderNo);
//创建远程请求对象
String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
url = wxPayConfig.getDomain().concat(url);
HttpPost httpPost = new HttpPost(url);
//组装json请求体
Gson gson = new Gson();
Map paramsMap = new HashMap<>();
paramsMap.put("mchid", wxPayConfig.getMchId());
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数 ===> {}", jsonParams);
//将请求参数设置到请求对象中
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "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);
throw new ServiceErrorException(ResultCode.ERROR, "用户取消订单失败");
}
} finally {
response.close();
}
} catch (IOException e) {
log.error("调用微信支付关闭订单接口失败 - {}", e);
e.printStackTrace();
throw new ServiceErrorException(ResultCode.ERROR, "调用微信支付关闭订单接口失败");
}
}
总结
我这边的JDK版本是jdk1.8.0_333,如果有开发者在读取秘钥的时候获取不到系统路径,获取秘钥文件是没存地址以及提示读取了非法长度的字符串等报错提示,证明当前jdk的版本过低,需要升级jdk的版本
以上就是就是全部内容,本文仅仅简单介绍了微信支付的部分API的使用,还有退款以及账单等相关操作后续使用到了会继续更新。
文章评论