导入依赖:
<!-- 微信支付SDK配置--> <dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-apache-httpclient</artifactId> <version>0.4.9</version> </dependency> <!-- 支付宝SDK依赖--> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.34.0.ALL</version> </dependency>
一,微信配置类
@Component @Data @ConfigurationProperties(prefix = "wxpay") public class WxPayConfig { // 商户号 private String mchId; // 商户API证书序列号 private String mchSerialNo; // 商户私钥文件 private String privateKeyPath; // APIv3密钥 private String apiV3Key; // APPID private String appid; // 微信服务器地址 private String domain; // 接收结果通知地址 private String notifyDomain; public PrivateKey getPrivateKey(String privateKeyPath) throws Exception { //传入私钥路径从而加载私钥文件 PrivateKey merchantPrivateKey = null; try { merchantPrivateKey = PemUtil.loadPrivateKey( new FileInputStream(privateKeyPath)); } catch (FileNotFoundException e) { throw new Exception("私钥文件不存在"); } return merchantPrivateKey; } @Bean public Verifier getVerifier() throws Exception{ //这个方法主要是用来获取签名验证器 PrivateKey privatekey = getPrivateKey(privateKeyPath); //加载密钥文件得到一个私钥对象 PrivateKeySigner privatekeySigner = new PrivateKeySigner(mchSerialNo, privatekey); //通过商户序列号和私钥对象得到一个私钥签名对象 WechatPay2Credentials wechatpay2credentials = new WechatPay2Credentials(mchId, privatekeySigner); //通过私钥签名对象和商户号得到了一个身份认证对象。主要用来认证是否是真正的微信服务端,而不是黑客伪造的服务端 byte[] by=apiV3Key.getBytes(StandardCharsets.UTF_8); //直接将Apiv3这个密钥字符串转换成字节数组 CertificatesManager certificatesManager = CertificatesManager.getInstance(); //获取证书管理器 certificatesManager.putMerchant(mchId,wechatpay2credentials,by); //通过证书管理器加载用户信息 Verifier verifier = certificatesManager.getVerifier(mchId); //从证书管理器中通过商户号获取签名验证器 return verifier; } //总结:先获取证书管理器,然后往里面添加信息,然后从管理器中获取签名验证器 //这里使用的是定时更新的签名验证器,所以不需要传入微信平台证书 @Bean(name="httpClient") public CloseableHttpClient getWxPayClient(Verifier verifier) throws Exception { // 这个方法主要是创建一个具有签名和验签功能的对象 PrivateKey privateKey = getPrivateKey(privateKeyPath); //加载私钥文件获取私钥 WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator(verifier)); //传入签名验签对象 CloseableHttpClient httpClient = builder.build(); return httpClient; // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的进行签名和验证签名,并进行证书自动更新 } //可以说这就是最终需要的一个最重要的对象 //可以说上面所作的一切操作,就是为了获得这样一个自动签名和验签的对象@Bean(name = "wxPayNoSignClient") public CloseableHttpClient WxPayNoSignClient() throws Exception { //该对象不具备签名验签功能, //在进行下载账单的时候,不能进行签名验签,否则会报错,所以就要使用到这个对象 //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); //用于构造HttpClient WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() //设置商户信息 .withMerchant(mchId, mchSerialNo, privateKey) .withValidator((response) -> true); //这里就是没有没有设置签名验签器,就是不会进行签名和验签 CloseableHttpClient httpClient = builder.build(); return httpClient; }httpClient的主要作用: a) 它主要对自己的数据进行私钥加密,保证微信服务器能够通过公钥识别到你, 让微信服务器确保数据是你发送的(签名) b)微信服务器对它的数据也进行私钥加密,你自己接收到数据后自动通过公钥解密, 让你确保数据是微信服务器发送过来的(验签) c) 还可以用来发送请求,自动将请求签名后发送过去,并且自动将得到的数据进行验签 比如: CloseableHttpResponse response = httpClient.execute(httpPost); //httpPost是一个请求,这里就是签名验签一体化 }
作用:
1,封装微信支付相关参数
2,创建私钥加载方法用来加载私钥文件
3,创建一个HttpClient对象,自动进行签名和验签
二,封装微信支付接口以及微信支付状态和订单状态说明的枚举类,共有五个枚举类
在PayType.enum中 @Getter @AllArgsConstructor public enum PayType { /** * 微信支付 */ WxPay("微信支付"), /** * 支付宝支付 */ ZfbPay("支付宝支付"); private final String Type; //final定义的表示常量,一旦被初始化赋值,就不能被再次修改了,也就是不能再被set方法修改了 }-
-
-
-
-
在OrderStatus.enum中
@AllArgsConstructor @Getter public enum OrderStatus { NoPay("未支付"), PaySuccess("支付成功"), Close("超时已关闭"), Cancel("用户已取消"), Refund_Processing("退款中"), Refund_Success("已退款"), Refund_Abnormal("退款异常"), ; private final String Type; }-
-
-
-
-
-
-
@Getter @AllArgsConstructor public enum WxOrderStatus { //这个是微信服务器返回的订单状态,返回的是英文字符串 NoPay("NOTPAY"), //未支付 PaySuccess("SUCCESS"), //支付成功 Close("CLOSED"), //已关闭 ; private String type; }-
-
-
-
-
-
在WxApiType.enum中
@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"), /** * 申请退款 */ DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"), /** * 查询单笔退款 */ DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"), /** * 申请交易账单 */ TRADE_BILLS("/v3/bill/tradebill"), /** * 申请资金账单 */ FUND_FLOW_BILLS("/v3/bill/fundflowbill"); /** * 类型 */ private final String type; }-
-
-
-
-
在WxNotifyType.enum中
@AllArgsConstructor @Getter public enum WxNotifyType { //封装了所有微信支付后的回调通知接口 /** * 支付通知 */ NATIVE_NOTIFY("/api/wx-pay/native/notify"), /** * 退款结果通知 */ REFUND_NOTIFY("/api/wx-pay/refunds/notify"); /** * 类型 */ private final String type; }
三,创建两个工具类,一个是用来自动生成订单号,另一个是将通知参数自动转换成字符串
在OrderNoUtils.java中 public class OrderNoUtils { //自动生成订单号的工具 /** * 获取订单编号 * @return */ public static String getOrderNo() { return "ORDER_" + getNo(); } /** * 获取退款单编号 * @return */ public static String getRefundNo() { return "REFUND_" + getNo(); } /** * 获取编号 * @return */ public static String getNo() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); String newDate = sdf.format(new Date()); String result = ""; Random random = new Random(); for (int i = 0; i < 3; i++) { result += random.nextInt(10); } return newDate + result; } }-
-
-
-
-
在HttpUtils.java中
public class HttpUtils { //主要是当微信服务器通过回调地址对我发起请求后,微信服务器回将请求数据放在请求体中的字节流中,需要通过下面这个方法转换成正常的字符串 /** * 将通知参数req转化为字符串 * @param request * @return */ public static String readData(HttpServletRequest request) { BufferedReader br = null; try { StringBuilder result = new StringBuilder(); br = request.getReader(); //读取请求体中的字节流 for (String line; (line = br.readLine()) != null; ) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } }使用方法如下:
String body = HttpUtils.readData(req); //将请求体中的数据自动转换成json格式,
//req就是微信服务器通过回调地址发送的请求体
上面这三步就是微信支付前期的一个准备工作
四,开始发送下单请求
在Controller层中:
@SneakyThrows @ApiOperation("下单生成二维码") @PostMapping("/native/{productId}") public Result NativePay( @PathVariable long productId){ //前端只用传递产品ID log.info("开始支付"); Map map= serviceImp.NativePay(productId); //从这里拿到支付二维码链接和支付订单 return Result.success().setData(map); }-
-
-
-
-
-
-
在Service业务层中:
@Resource private WxPayConfig wxPayConfig; //上面编写的配置类 @Resource private CloseableHttpClient httpClient; //这个也是自己上面编写的具有签名和验签功能的配置类 /** * 进行下单 */ @Override public Map NativePay(long productId) throws IOException { /** * 生成订单并存入数据库中 */ // 查找已经存在的订单,避免重复创建 LambdaQueryWrapper<OrderInfo> lqw=new LambdaQueryWrapper<>(); lqw.eq(OrderInfo::getProductId,productId).eq(OrderInfo::getOrderStatus,OrderStatus.NoPay.getType()); OrderInfo orderInfo= baseMapper.selectOne(lqw); if(orderInfo!=null){ return orderInfo; }log.info("开始创建订单"); OrderInfo orderInfo=new OrderInfo(); orderInfo.setTitle("你是谁"); //标题 orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号 orderInfo.setProductId(productId); //产品id orderInfo.setTotalFee(1); //订单金额,这里的金额是通过产品ID进行查询的,这里是假的金额,表示一分钱 orderInfo.setOrderStatus(OrderStatus.NoPay.getType());/** * 判断是否存在二维码并再判断是否过期,避免重复发起请求 */ if(orderInfo.getCodeUrl()!=null){ //判断订单信息中是否存在链接 long start=orderInfo.getUpdateTime().getTime(); //插入链接时候的时间 long end=new Date().getTime(); //当前时间 log.info("开始时间:"+orderInfo.getUpdateTime().toString()); log.info("结束时间:"+new Date().toString()); if((end-start)/(60*60*1000)<2){ //保证链接插入的时间在两个小时以内 log.info("订单已存在,链接已保存"); Map<String,Object> map3=new HashMap<>(); map3.put("codeUrl",orderInfo.getCodeUrl()); //二维码链接 map3.put("orderNo",orderInfo.getOrderNo()); //支付订单 return map3; } }/** * 调用下单API发起支付,得到code_url */ log.info("发起下单请求"); // HttpPost httpPost = new HttpPost(WxApiType.NATIVE_PAY.getType()); // 封装请求body参数,一定要是json格式的 Gson gson=new Gson(); Map map=new HashMap(); map.put("appid",wxPayConfig.getAppid()); //appid map.put("mchid",wxPayConfig.getMchId()); //商户号 map.put("description",orderInfo.getTitle()); //商品描述 map.put("out_trade_no",orderInfo.getOrderNo()); //订单号 map.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType())); //通知地址, 就是一个内网穿透地址和自己编写的接口进行的一个拼接,并将该地址给微信服务器 Map map1=new HashMap(); //金额主体 map1.put("total",orderInfo.getTotalFee()); //1分 map1.put("currency","CNY"); map.put("amount",map1); //金额 String bodyParams= gson.toJson(map); //将所有必备的参数转换成json格式 log.info("请求下单的参数为:"+bodyParams); StringEntity entity = new StringEntity(bodyParams,"utf-8"); //转换成utf-8格式 entity.setContentType("application/json"); //缺少这个会报错:"Http头缺少Accept或User-Agent" httpPost.setEntity(entity); //将数据添加到HttpPost请求体中 httpPost.setHeader("Accept", "application/json"); //缺少这个会报错:"Http头Content-Type值必须为application/json"//完成签名并执行请求 CloseableHttpResponse response = httpClient.execute(httpPost); //这里的httpClient就是在wxPayConfig中编写的具体签名和验签功能的配置类对象,所以需要导入进来 //这里进行签名并发送请求后,得到响应体 log.info("得到的响应体为:"+response.toString()); try { int statusCode = response.getStatusLine().getStatusCode(); //响应体中有对应的状态吗,还有对应的响应体code_url String bodyString=EntityUtils.toString(response.getEntity()); //将微信服务器的请求体中的数据转换成json格式,不然你看不懂 if (statusCode == 200) { //处理成功 log.info("success,return body = " +bodyString ); } else if (statusCode == 204) { //处理成功,无返回Body System.out.println("success"); } else { System.out.println("failed,resp code = " + statusCode+ ",return body = " + bodyString); throw new IOException("request failed"); } HashMap result=gson.fromJson(bodyString,HashMap.class); //将json字符串转换成HashMap对象 Object code_url = result.get("code_url");//获取服务器响应的二维码链接 Map<String,Object> map3=new HashMap<>(); map3.put("codeUrl",code_url); //二维码链接 map3.put("OrderNo",orderInfo.getOrderNo()); //支付订单 return map3; } finally { response.close(); } }-
-
-
-
-
-
前端方面:
el-dislog是一个弹窗,在发送请求拿到二维码codeUrl的同时改变codeDialogVisible的值为true,使弹窗展示
同时安装二维码生成器 :
import VueQriously from 'vue-qriously'
Vue.use(VueQriously) //导入这个插件
<el-dialog :visible.sync="codeDialogVisible" :show-close="false" @close="closeDialog" width="350px" center> <qriously :value="codeUrl" :size="300"/> //这个就是二维码生成器,只要将二维码链接codeUrl传入进来就可以了 使用微信扫码支付 </el-dialog>
总结:这一步主要就是:
a) 首先前端传入产品id, 创建订单信息,并存储到数据库中(订单号是通过工具类OrderNoUtils自动生成的)
b) 然后开始调用微信API,并通过自动签名进行签名后,将发送数据给微信服务器,拿到响应结果response
c) 然后解密response使其变成json格式:Stringbody String=EntityUtils.toString(response.getEntity());
d) 最后通过gson将json数据转换成map, 这样就能使用微信给我们的数据了
五,开始接收微信服务器的回调结果
/** * 微信服务器的通知地址, 将支付结果通知给你 */ @PostMapping("/native/notify") public String nativeNotify(HttpServletRequest req, HttpServletResponse res) { //微信服务器通过请求主体req将数据发送过来, 然后我们商户系统再通过响应主体res将响应结果状态码发送给微信服务器 Gson gson = new Gson(); Map<String, String> map = new HashMap<>(); // 从请求对象中获取数据: String body = HttpUtils.readData(req); //将请求体中的数据自动转换成json格式,这里我自己编写了一个工具类HttpUtils Map<String, Object> result = gson.fromJson(body, HashMap.class); //将json数据转换成hashMap集合 log.info("通知id为{}", result.get("id")); log.info("支付成功的完整数据为{}", body); //解析微信服务器发送的结果中的resource对象 Map<String,String> map= (Map<String, String>) result.get("resource"); //这个是从对象中获取带有密文的一个对象 String ciphertext= map.get("ciphertext"); //获取微信服务器发送的加密的密文 String nonce = map.get("nonce"); //获取微信服务器用来加密的随机串 String associated_data = map.get("associated_data"); //获取附加数据 AesUtil aesUtil=new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); //获取一个带有ApiV3密钥的解密工具 String plainText=aesUtil.decryptToString(associated_data.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext); //这个就是解密后的明文 //转换 Map<String,Object> plainTextMap=gson.fromJson(plainText,HashMap.class); String orderNo= (String) plainTextMap.get("out_trade_no"); //将明文转换成集合,从而获取订单号 //更行订单状态为支付成功 orderInfoService.updateOrder(orderNo,OrderStatus.PaySuccess.getType()); //响应状态码 res.setStatus(200); //通过响应体将状态码给微信服务器,200的时候,其实是可以不用发送应答报文给微信服务器的 //我将状态码改成5开头的,比如544, 微信服务器就会以为我接收失败,就会不断的发送支付结果给我 //返回应答报文 Map<String, String> map2 = new HashMap<>(); //按照格式{ "code": "FAIL","message": "失败"}, 将应答报文给微信服务器 map2.put("code", "success"); map2.put("message", "成功"); return gson.toJson(map2); //封装结果后,转成json格式发送给微信服务器 }
总结:
a)我们在支付和退款的时候,都会将对应通知地址给微信服务器进行使用,
然后只有当用户支付成功,或者退款完成后,微信服务器才会调用回调地址
比如:
在支付成功后,会通过回调地址的请求体中传入一些支付成功的信息
在退款完成后,也会通过回调地址的请求体中传入一些退款信息,比如:退款成功、退款关闭、退款异常等信息
b)微信服务器调用回调地址后,会将自己要传入的数据放到请求体的字节流中,我们可以通过自己编写的工具类HttpUtils将请求体中的字节流转换成json格式数据:
String body = HttpUtils.readData(req);
c)微信支付APIv3的下载平台证书接口以及回调通知中,为防止报文被他人其他人恶意篡改,微信服务器会对数据进行加密。商户收到报文后,要解密出明文,解密过程中用的key就是APIv3密钥。
解密过程:
1) 将从请求体的字节流中读取到的json数据通过gson转换成map进行使用,但是实际有用的数据需要通过resource中的三个参数,并和APIV3密钥结合进行解密,这样才能拿到有用的json格式的数据进行使用:
Map<String, Object> result = gson.fromJson(body, HashMap.class); //将json数据转换成hashMap集合
Map<String,String> map= (Map<String, String>) result.get("resource"); //这个是从对象result中获取带有解密参数的一个对象resource
2) 获取到resource中的三个参数,并和APIV3密钥结合进行解密:
//从resource对象中获取三个必备的解密参数:
String ciphertext= map.get("ciphertext"); //获取微信服务器发送的加密的密文
String nonce = map.get("nonce"); //获取微信服务器用来加密的随机串
String associated_data = map.get("associated_data"); //获取附加数据
2) 开始解密:
AesUtil aesUtil=new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); //获取一个带有ApiV3密钥的解密工具 String
Strin plainText=aesUtil.decryptToString(associated_data.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext); //这个就是解密后的明文
4) 拿到解密后的json数据进行使用:
Map<String,Object> plainTextMap=gson.fromJson(plainText,HashMap.class);
String orderNo= (String) plainTextMap.get("out_trade_no"); //将明文转换成集合,从而获取订单号, 这个是微信服务器给我们的数据中查询到的订单号
七, 主体流程:
1,调用微信的API得到的响应数据,需要进行解析才能拿到响应体中的json格式数据
2,微信服务器调用回调地址传过来的对象放在了请求体的字节流中,当从字节流中读取到了对象后,实际的数据是放在了该对象中的resource中,需要对resource进行解析才能得到具体的数据
八,注意事项:
在进行下载账单的时候,一定不要进行签名和验签,所以就要使用到所创建的对象:
CloseableHttpClient wxPayNoSignClient