微信小程序JSAPI支付 v3 版本 详解 附带源码

v2与v3的区别

V2版接口和V3版接口实际上是基于两种接口标准设计的两套接口。 目前大部分接口已升级为V3接口,其余V2接口后续也将逐步升级为V3接口

微信小程序JSAPI支付 v3 版本 详解 附带源码_第1张图片

总的来说,v3支付是对v2版本的升级,不过目前来看,v3的支付对接和实现对开发者更加友好,并且支付更加安全。

微信支付官方文档

请大家花点时间认真看下文档。指引文档,更新日志,sdk,api字典等,这些都很重要,避免在开发过程中,遇到很多不必要的麻烦。

v2的微信小程序支付我之前已经实现过一次了,可以看看 微信小程序支付详解

文章。

交互图

微信小程序JSAPI支付 v3 版本 详解 附带源码_第2张图片

https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_4&index=3 请看交互图,其实交互是一样的,只是v3的支付接口不一致、大致的实现流程就这些,后台的大致流程为:

  1. 请求微信统一下单接口,获取预支付交易会话标识(prepay_id)
  2. 拿prepay_id 拼接参数,生成签名,给到前端拉起支付(JSAPI调起支付API)
  3. 支付成功后,处理支付回调方法

这里就是后端需要做处理的流程,中间会涉及到 数字证书,秘钥,签名等加密解密出来,以及自己业务处理。

代码实现

官方提供了工具和SDK。先获取官方的SDK,java版本的来做项目开发。

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml

wechatpay-apache-httpclient,适用于使用Apache HttpClient处理HTTP的Java开发者。
wechatpay-php(推荐)、wechatpay-guzzle-middleware,适用于PHP开发者。
wechatpay-go,适用于Go开发者

大家根据自己的语言去下载对应的SDK。并且好好看看sdk的使用方式。

maven依赖

   
      com.github.wechatpay-apiv3
      wechatpay-apache-httpclient
      0.2.2
    

配置类:

    @Bean
    public HttpClient wxHttpClient(
            @Value("${mchid}") String mchid,
            @Value("${mchSerialNo}") String mchSerialNo,
            PrivateKey merchantPrivateKey,
            AutoUpdateCertificatesVerifier autoUpdateCertificatesVerifier) {
        return WechatPayHttpClientBuilder.create()
                .withMerchant(mchid, mchSerialNo, merchantPrivateKey)
                .withValidator(new WechatPay2Validator(autoUpdateCertificatesVerifier)).build();
    }

    /**
     * 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
     */
    @Bean
    public AutoUpdateCertificatesVerifier autoUpdateCertificatesVerifier(
            PrivateKey merchantPrivateKey,
            @Value("${mchid}") String mchid,
            @Value("${mchSerialNo}") String mchSerialNo,
            @Value("${v3Key}") String v3Key) throws UnsupportedEncodingException {
        return new AutoUpdateCertificatesVerifier(
                new WechatPay2Credentials(mchid, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),
                v3Key.getBytes("utf-8"));
    }

    /**
     * 加载商户私钥(privateKey:私钥字符串)
     */
    @Bean
    public PrivateKey merchantPrivateKey(@Value("${privateKey}") String privateKey)
            throws UnsupportedEncodingException {
        return PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
    }
  static final int KEY_LENGTH_BYTE = 32;
  static final int TAG_LENGTH_BIT = 128;

  @Value("${v3Key}")
  private String weChatV3Key;

  public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
          throws GeneralSecurityException, IOException {
    try {
      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

      SecretKeySpec key = new SecretKeySpec(weChatV3Key.getBytes(StandardCharsets.UTF_8), "AES");
      GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

      cipher.init(Cipher.DECRYPT_MODE, key, spec);
      cipher.updateAAD(associatedData);

      return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
      throw new IllegalStateException(e);
    } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
      throw new IllegalArgumentException(e);
    }
  }

请求下单

  /**
     * 请求统一下单逻辑处理:
     */
    private WxPayOrderResult unifiedOrder(WxPayOrderRequest wxPayOrderRequest) throws IOException {
        HttpPost httpPost = new HttpPost(PAYUNIFIEDORDERURL);
        // 请求body参数
        StringEntity entity = new StringEntity(new Gson().toJson(wxPayOrderRequest), "UTF-8");
        entity.setContentType("application/json;charset=UTF-8");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json;charset=UTF-8");
        HttpResponse response = httpClient.execute(httpPost);
        int code = response.getStatusLine().getStatusCode();
        String body = response.getEntity() == null ? null : EntityUtils.toString(response.getEntity());
        if (code != 200) {
            log.error("请求微信统一下单异常: code={}, body={}", code, body);
            throw new RuntimeException("请求微信统一下单异常");
        }
        return new Gson().fromJson(body,
                WxPayOrderResult.class);
    }

获取下单的预支付id生成签名

    @Override
    public String payTransactionsJsapi(WxPayOrderRequest wxPayOrderRequest) throws IOException {
        wxPayOrderRequest.setAppid(appid);
        wxPayOrderRequest.setMchid(mchid);
        WxPayOrderResult wxPayUnifiedOrderResult = this.unifiedOrder(wxPayOrderRequest);
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        WxPayMpOrderResult wxPayMpOrderResult = new WxPayMpOrderResult().setAppId(
                        wxPayOrderRequest.getAppid())
                .setNonceStr(wxPayOrderRequest.getOut_trade_no())
                .setPackageValue("prepay_id=" + wxPayUnifiedOrderResult.getPrepay_id())
                .setSignType("RSA")
                .setTimeStamp(timestamp);
        /**
         * jsapi 拉起支付文档 : https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_4.shtml
         * 一定要注意签名方式哪里
         */
        String paySign = sign(getSignStr(wxPayOrderRequest.getAppid(), timestamp,
                        wxPayOrderRequest.getOut_trade_no(), "prepay_id=" + wxPayUnifiedOrderResult.getPrepay_id())
                , privateKey);
        wxPayMpOrderResult.setPaySign(paySign);
        /**
         * 这里是为了做兼容 代码不允许有 package 的处理,所以才做转换,如果可以的话,让前端去做处理也行
         */
        Gson gson = new Gson();
        Map map = gson.fromJson(gson.toJson(wxPayMpOrderResult), Map.class);
        map.put("package", map.get("packageValue"));
        map.remove("packageValue");
        return gson.toJson(map);
    }

签名方法

    private String getSignStr(String appId, String timeStamp, String nonceStr, String packageValue) {
        return String.format("%s\n%s\n%s\n%s\n", appId, timeStamp, nonceStr, packageValue);
    }

    public static String sign(String string, PrivateKey privateKey) {
        try {
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initSign(privateKey);
            sign.update(string.getBytes());
            return Base64.getEncoder().encodeToString(sign.sign());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
        } catch (SignatureException e) {
            throw new RuntimeException("签名计算失败", e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException("无效的私钥", e);
        }
    }

这里可以看看官方文档对签名的介绍:
微信小程序JSAPI支付 v3 版本 详解 附带源码_第3张图片
微信小程序JSAPI支付 v3 版本 详解 附带源码_第4张图片

回调处理:

    @PostMapping("weChat")
    public String acceptNotify(@RequestBody WeChatNotify notify) throws Exception {
        log.info("接收到JSAPI支付通知 {}", notify.toString());
        Gson gson = new Gson();
        String decrypt = weChatAesUtil.decryptToString(notify.getResource()
                        .getAssociated_data()
                        .getBytes(StandardCharsets.UTF_8),
                notify.getResource().getNonce().getBytes(StandardCharsets.UTF_8),
                notify.getResource().getCiphertext());
        //获取微信通知数据,接下来,自己去做业务处理:
        WeChatDecryptNotifyContent content = gson
                .fromJson(decrypt, WeChatDecryptNotifyContent.class);

        return gson.toJson(new NotifyBody("SUCCESS", "成功"));
    }

完整源码:

https://gitee.com/QuRenNen/spring-boot-integrate

spring-boot-wx-miniapp-integrat 项目中感兴趣可以看看。

你可能感兴趣的:(微信小程序,微信小程序,支付,微信,java)