前言

微信新出的V3支付接口,还没玩过,正好有做支付的需求,于是就看看怎么搞了,下面是集成第三方jar来调用的微信支付,如何使用可以查看我的另一篇文章,不过方式跟v2也是有些区别的。

微信jar V2支付(含内网穿透工具)


V3接口规则(个人理解)

微信文档

发送请求前,构造签名串,使用证书和密钥文件签名
将生成签名按照规则组装后,作为请求头,发送请求


整合binarywang.weixin-java-pay实现v3支付
jar
<dependency>
  <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-pay</artifactId>
    <version>4.0.0</version>
</dependency>

下单

该库现在支持使用V3方式构造Http Client,从而使用该Client发送请求时,我们就无需关心签名的生成,只需要设置接口所需参数即可,不过这个Client也需要我们来生成!!

生成Http Client
/**
 * <p>
 *  生成v3 http client并返回WxPayConfig
 * </p>
 * @param mchId         商户号
 * @param apiV3Key      api V3 密钥
 * @param privateKey    私钥
 * @param privateCert   证书
 * @param isUrl         私钥和证书是否为url资源,false则表示文件资源
 * @return com.github.binarywang.wxpay.config.WxPayConfig
 * @author guohaibin
 * @date 2020-12-02 13:53:14
 */
private WxPayConfig createV3WxPayConfig(String mchId, String apiV3Key,
                                        String privateKey, String privateCert, boolean isUrl) {
    WxPayConfig wxPayConfig = new WxPayConfig();
    if (isUrl) {
        //  手动构建http client - 通过url的方式来读取证书/密钥
        Credentials credentials = new WxPayCredentials(
                mchId,
                new PrivateKeySigner(
                        PemUtils.loadCertificate(URLUtil.getStream(URLUtil.url(privateCert))).getSerialNumber().toString(16),
                        PemUtils.loadPrivateKey(URLUtil.getStream(URLUtil.url(privateKey)))
                )
        );
        Validator validator = new WxPayValidator(
                new AutoUpdateCertificatesVerifier(
                        credentials, apiV3Key.getBytes()
                )
        );
        CloseableHttpClient client = WxPayV3HttpClientBuilder.create()
                .withCredentials(credentials)
                .withValidator(validator)
                .build();
        wxPayConfig.setApiV3HttpClient(client);
    } else {
        //  自动构建apiV3HttpClient时以下参数必须设置
        //  com.github.binarywang.wxpay.config.WxPayConfig.initApiV3HttpClient
        wxPayConfig.setMchId(mchId);
        wxPayConfig.setPrivateKeyPath(privateKey);
        wxPayConfig.setPrivateCertPath(privateCert);
        wxPayConfig.setApiV3Key(apiV3Key);
    }
    return wxPayConfig;
}

这里我使用两种方式来生成,一种是读文件的方式,一种是文件是url时的方式,因为我们项目时代小程序开发,所以如果用了V3,证书文件就会存在服务器上,因此考虑两种方式;但实际不适用,因为回调无法满足需求。
一般单独小程序使用,读文件的方式就足以使用了!

下单

微信文档

/**
 * <p>
 *  JSAPI支付
 * </p>
 * @param appId         小程序appId
 * @param mchId         小程序绑定商户号
 * @param apiV3Key      api V3 密钥
 * @param description   商品描述(128)
 * @param total         订单总金额(分)
 * @param outTradeNo    订单号(需要生成)
 * @param openId        用户openId
 * @param privateKey    私钥
 * @param privateCert   证书
 * @param isUrl         私钥和证书是否为url资源,false则表示文件资源
 * @return com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult
 * @author guohaibin
 * @date 2020-12-02 13:56:02
 */
public WxPayMpOrderResult createOrderJSAPI(String appId, String mchId, String apiV3Key,
                                             String description, BigDecimal total, String outTradeNo,
                                             String openId, String privateKey, String privateCert, boolean isUrl) throws Exception {
    WxPayService wxPayService = new WxPayServiceImpl();
    WxPayConfig wxPayConfig = createV3WxPayConfig(mchId, apiV3Key, privateKey, privateCert, isUrl);
    wxPayService.setConfig(wxPayConfig);
    //  设置参数
    JSONObject data = new JSONObject();
    data.set("appid", appId);
    data.set("mchid", mchId);
    data.set("description", description);
    data.set("out_trade_no", outTradeNo);
    data.set("notify_url", v3PayNotifyUrl);
    JSONObject amount = new JSONObject();
    amount.set("total", BaseWxPayRequest.yuanToFen(total.toString()));
    data.set("amount", amount);
    JSONObject payer = new JSONObject();
    payer.set("openid", openId);
    data.set("payer", payer);
    //  调接口
    String result = wxPayService.postV3("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi",
            data.toString());
    JSONObject prepayObject = JSONUtil.parseObj(result);
    //  获取paySign
    if (prepayObject.containsKey("prepay_id")) {
        //  预支付签名
        String packageStr = "prepay_id=" + JSONUtil.parseObj(result).getStr("prepay_id");
        String nonceStr = SignUtils.genRandomStr();
        String timeStamp = String.valueOf(System.currentTimeMillis());
        String signType = "RSA";
        //  构造paySign
        String signStr = appId + "\n" + timeStamp + "\n" + nonceStr + "\n" + packageStr + "\n";
        String paySign = SignUtils.sign(signStr, getPrivateKey(privateKey, isUrl));
        return new WxPayMpOrderResult(null, timeStamp, nonceStr, packageStr, signType, paySign);
    } else {
        //  TODO 报错并打印有用信息
        throw new WxPayException("v3获取预支付异常");
    }
}

后续的流程就是设置参数,调接口,再签名出paySign,和V2类似,不过这里再次签名方式不一样,要先构造签名串,用V3的方式签名。

签名文档


支付回调

controller
因为V3都是JSON做参和返回值了,所以直接接收即可。

@PostMapping("/payNotify")
public String payNotify(@RequestBody PayV3Notify payV3Notify) {
    //  需要考虑恶意调用,非法调用可以不考虑,因为有签名验证,但是要避免多次恶意调用
    return payV3Service.payNotify(payV3Notify);
}

service
V2:返回值中会包含信息,然后再用商户密钥去校验签名,可利用信息
V3:返回值中不包含任何明文,需要ApiV3密钥解密后,才可解析出订单号等信息
因此,在我们项目中,我们可以通过appId来获取商户信息,但由于V3不再提供信息,导致我们无法根据商户号/订单号来查找商户信息,那么就无法使用V3来实现我们的逻辑,并且,V3暂未提供退款接口,因此后续的退款我们也实现不了,综上,最后我还是选择使用V2来实现支付。

public String payNotify(PayV3Notify payV3Notify) {
    try {
        //  解密
        String decryptToString = AesUtils.decryptToString(payV3Notify.getResource().getAssociated_data(),
                payV3Notify.getResource().getNonce(), payV3Notify.getResource().getCiphertext(),
                wxConfig.getApiV3Key());
        PayV3NotifyDecrypt payV3NotifyDecrypt = JSONUtil.toBean(decryptToString, PayV3NotifyDecrypt.class);
        //  订单号
        String outTradeNo = payV3NotifyDecrypt.getOut_trade_no();
        //  判断订单状态

        //  修改订单状态
        return WxPayNotifyResponse.success("处理成功!");
    } catch (Exception e) {
        log.error("微信回调结果异常,异常原因{}", e.getMessage());
        return WxPayNotifyResponse.fail(e.getMessage());
    }
}

小结

就接口来说,V3调用的方式复杂了些,每次都需要构造签名,有点麻烦,不过配置好通用一下就行;
参数方面比V2好些,就不需要string -> xml或是xml -> string了,可以自定义对象来接收回调数据了
就支付回调来说,V2是先拿到数据,再校验数据,而V3是先解密,才能获取数据(个人感觉这样在代小程序开发中就不好用V3来搞,因为小程序是动态的,V2就能获取appId再连查密钥,而V3需要密钥才能获取信息,就不符合需求感觉!)
最后,用别人封装好的还是香啊,这个库后续估计还会对V3进行优化,到时候用起来就更滋润了!!
最后附上自己的demo,也包含V2接口!

demo