微信支付及案例说明

导入依赖:

<!--   微信支付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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值