【Java必备技能五】微信支付之JSAPI方式(公众号支付)超详细实现步骤

目录

一、JSAPI支付概述

二、名词解释

三、签名算法

四、设置支付目录和授权域名

五、JSAPI支付流程

六、JSAPI支付下单

       第一步:用户同意授权,获取code

       第二步:通过code换取网页授权access_token

       第三步:调用接口统一下单

       第四步:微信内H5调起支付

七、支付结果通知

八、支付常见问题

九、总结


一、JSAPI支付概述

微信官方提供了很多种支付方式,如扫码支付、JSAPI支付、H5支付、NATIVE支付等等,支付接入文档地址:https://pay.weixin.qq.com/wiki/doc/api/index.html。在这几种支付方式中,JSAPI支付应该是稍微复杂点的支付,配置以及流程相对复杂,本文总结一下JSAPI支付的详细实现步骤。

JSAPI支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。应用场景有:

  1. ◆ 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付;
  2. ◆ 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付;
  3. ◆ 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付;

注意:JSAPI支付只能用微信浏览器打开。如果需要使用外部浏览器,可以对接H5支付这种方式。

JSAPI支付交互细节:

(1)用户打开商户网页选购商品,发起支付,在网页通过JavaScript调用getBrandWCPayRequest接口,发起微信支付请求,用户进入支付流程。

(2)用户成功支付点击完成按钮后,商户的前端会收到JavaScript的返回值。商户可直接跳转到支付成功的静态页面进行展示。

(3)商户后台收到来自微信开放平台的支付成功回调通知,标志该笔订单支付成功。

注:(2)和(3)的触发不保证遵循严格的时序。JS API返回值作为触发商户网页跳转的标志,但商户后台应该只在收到微信后台的支付成功回调通知后,才做真正的支付成功的处理。

二、名词解释

  • 签名

商户后台和微信支付后台根据相同的密钥和算法生成一个结果,用于校验双方身份合法性。签名的算法由微信支付制定并公开,常用的签名方式有:MD5、SHA1、SHA256、HMAC等。

  • JSAPI网页支付

JSAPI网页支付即前文说的公众号支付,可在微信公众号、朋友圈、聊天会话中点击页面链接,或者用微信“扫一扫”扫描页面地址二维码在微信中打开商户HTML5页面,在页面内下单完成支付。

  • Openid

用户在公众号内的身份标识,不同公众号拥有不同的openid。商户后台系统通过登录授权、支付通知、查询订单等API可获取到用户的openid。主要用途是判断同一个用户,对用户发送客服消息、模版消息等。企业号用户需要使用企业号userid转openid接口将企业成员的userid转换成openid。

三、签名算法

签名生成的通用步骤如下:

第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。

特别注意以下重要规则:

  1. ◆ 参数名ASCII码从小到大排序(字典序);
  2. ◆ 如果参数的值为空不参与签名;
  3. ◆ 参数名区分大小写;
  4. ◆ 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
  5. ◆ 微信接口可能增加字段,验证签名时必须支持增加的扩展字段

第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。

◆ key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置

注意:参与签名的key是微信商户平台支付的API密钥,并不是微信公众平台的appSecret,否则会报"签名错误",API密钥设置路径:微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全-->密钥设置,如下图所示:

在调试的时候,我们可以使用https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=20_1提供的签名校验工具,此工具旨在帮助开发者检测调用【微信支付接口API】时发送的请求参数中生成的签名是否正确,提交相关信息后可获得签名校验结果。

具体使用参照下图:

 签名常见问题:

  1. 注意参数是否区分大小写,参数大小写不正确将会导致签名错误;
  2. 检查所有参数是否与文档完全一致;
  3. 请求数据的编码是否正确,微信支付接口编码要求统一为UTF-8;
  4. 签名原串是否存在被URLencode编码的参数,微信支付的签名原串要求使用参数的原值进行签名;
  5. 请求参数是否存在特殊字符,或者字段长度不符的情况;

四、设置支付目录和授权域名

【a】微信商户平台设置支付目录

支付授权目录说明:

  • 1、商户最后请求拉起微信支付收银台的页面地址我们称之为“支付目录”,例如:https://www.weixin.com/pay.php;
  • 2、商户实际的支付目录必须和在微信支付商户平台设置的一致,否则会报错“当前页面的URL未注册:”;

支付授权目录设置说明:

登录微信支付商户平台(pay.weixin.qq.com)-->产品中心-->开发配置,设置后一般5分钟内生效。

支付授权目录校验规则说明:

1、如果支付授权目录设置为顶级域名(例如:https://www.weixin.com/ ),那么只校验顶级域名,不校验后缀;

2、如果支付授权目录设置为多级目录,就会进行全匹配,例如设置支付授权目录为https://www.weixin.com/abc/123/,则实际请求页面目录不能为https://www.weixin.com/abc/,也不能为https://www.weixin.com/abc/123/pay/,必须为https://www.weixin.com/abc/123/

下图是笔者的配置,可以看到开放了:http://wwtt.utools.club/ 这个支付目录。

JSAPI支付在请求支付的时候会校验请求来源是否有在商户平台做了配置,所以必须确保支付目录已经正确的被配置,否则将验证失败,请求支付不成功。

 【b】微信公众平台设置授权域名

开发JSAPI支付时,在统一下单接口中要求必传用户openid,而获取openid则需要您在公众平台设置获取openid的域名,只有被设置过的域名才是一个有效的获取openid的域名,否则将获取失败。具体界面如图所示:

网页授权域名是用来向微信获取code,根据code获取openId和access_token。

 五、JSAPI支付流程

微信内网页支付时序图如下所示:

大体流程步骤如下:

1. 商户后台系统生成图文支付消息或二维码;

2. 用户点击消息或者扫描二维码在微信浏览器打开我们的业务H5网页;

3. 微信客户端网页内请求生成支付订单;

4. 商户后台系统生成商户订单;

5. 商户后台系统调用统一下单API, 微信支付系统生成预付单,并返回预付单信息(prepay_id)等信息;

6. 商户后台系统生成JSAPI页面调用的支付参数并签名,返回支付参数prepay_id、paySign等给微信客户端;

7. 用户点击发起支付,微信客户端JSAPI接口请求微信支付系统支付;

8. 微信支付系统检查参数是否合法和授权域权限等信息,并返回验证结果,并要求支付授权;

9. 微信客户端提示输入密码,用户确认支付,输入支付密码;

10. 微信支付系统验证授权,异步通知商户后台系统支付结果;

11. 商户后台系统告知微信支付系统,微信通知处理结果;

12. 微信支付系统返回支付结果,并发送微信消息提示,微信跳转回商户H5页面;

 

商户系统和微信支付系统主要交互:

  • 1、商户后台(即我们的业务系统)调用统一下单接口请求订单,api参见公共api【统一下单API
  • 2、商户后台(即我们的业务系统)接收支付通知,api参见公共api【支付结果通知API
  • 3、商户后台(即我们的业务系统)查询支付结果,api参见公共api【查询订单API

六、JSAPI支付下单

通过前面的介绍,相信小伙伴们都已经对微信支付--JSAPI方式的流程、需要获取的参数、签名的算法都有了一定的了解,下面我们就以一个案例详细说明如何实现JSAPI支付,由于需要在微信公众号内完成支付,此操作需要进行网页授权。

【a】微信网页授权

如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。

网页授权文档地址:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html

具体而言,网页授权流程分为四步:

1、引导用户进入授权页面同意授权,获取code;

2、通过code换取网页授权access_token(与基础支持中的access_token不同);

3、如果需要,开发者可以刷新网页授权access_token,避免过期;

注意:这里由于微信 JSAPI下单需要获取openid才行,并不需要获取到用户基本信息(用户昵称,头像等信息),所以此次案例,只需要到第三步获取网页授权access_token后,微信服务器已经返回openid了,我们直接获取即可。

4、通过网页授权access_token和openid获取用户基本信息(支持UnionID机制);

第一步:用户同意授权,获取code

引导关注者打开如下页面:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

参数说明: 

下面是前端部分代码:当用户没有进行授权,引导其进入授权页面

if (!wxCode) { //如果微信支付,且未获取code值,此时需要先获取code值
   this.props.dispatch({
      type: 'payMain/getWxAppid', 
      params: { zfshPkid: zfshPkid }, 
      callback: data => {
          //data其实就是查询后台设置的appId
         var urlencode = require('urlencode');
          //redirect_uri需要进行urlencode
         let location = urlencode(window.location.href);
         let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${data}&redirect_uri=${location}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect`;
         window.location = url;
      },
   });
   return;
}

用户如果同意授权后,页面将跳转至 redirect_uri/?code=CODE&state=STATE。

code说明 : code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。

 第二步:通过code换取网页授权access_token

公众号可通过下述接口来获取网页授权access_token。如果网页授权的作用域为snsapi_base,则本步骤中获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。

请求方法:

获取code后,请求以下链接获取access_token:https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

参数说明:

正确时返回的JSON数据包如下:

{
  "access_token":"ACCESS_TOKEN",
  "expires_in":7200,
  "refresh_token":"REFRESH_TOKEN",
  "openid":"OPENID",
  "scope":"SCOPE" 
}

第三步:调用接口统一下单

微信支付只需要openid,获取到access_token之后只需要里面openid参数, 拿到openid之后,我们调用微信统一下单接口生成预付单。

文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1

 

下面来看看具体的实现代码:

前端代码:

getQueryString = (name) => {
   return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(window.location.href) || [, ''])[1].replace(/\+/g, '%20')) || null;
};

// 组件渲染后调用
componentDidMount() {
    //获取用户同意授权后拼接在redirect_uri上面的code
   let code = this.getQueryString('code');
   if (code) {
      this.setState({
         wxCode: code,
         payWay: '微信支付',
      }, () => {
         //已经获取了code,开始执行支付操作
         //此事件就是确认支付按钮事件
         this.handleSaveBtn();
      });
   }
   
//点击支付按钮事件
handleSaveBtn = () => {
   //获取勾选的支付收费项目
   //...........

   if (!wxCode) { //如果微信支付,且未获取code值,此时需要先获取code值
      this.props.dispatch({
         type: 'payMain/getWxAppid',
         params: { zfshPkid: zfshPkid },
         callback: data => {
            var urlencode = require('urlencode');
            let location = urlencode(window.location.href);
            let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${data}&redirect_uri=${location}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect`;
            window.location = url;
         },
      });
      return;
   }

   //需要支付的收费项目订单信息
   let params = {};
   //...........

   //创建订单消息保存到业务系统
   //业务系统保存支付订单、订单详情
   //我们需要在我们的业务系统先保存此次支付的订单、订单明细,并记录日志,只有我们业务系统保存成功之后,我们才请求微信服务器发起一个预付单请求。
   this.props.dispatch({
      type: 'payMain/saveAliPayOrder',
      params: params,
      callback: data => {
         if (data.code == '1') {
            let orderPkid = data.data || '';
            if (orderPkid) { //支付订单主键ID
            //通过code去后台查询openID
              this.goToWxPay(orderPkid, paymentAmount, zfshPkid, wxCode, id); 
            }
         } else {
            Toast.fail('保存订单失败,请稍后重试!');
         }
      },
   });
}

/**
 * 微信支付
 */
goToWxPay = (orderPkid, paymentAmount, zfshPkid, code, id) => {
   paymentAmount = parseInt(paymentAmount * 100) + '';//转换成分
   this.props.dispatch({
      type: 'payMain/goToWxPay',  //此处对应controller的/toWxPay接口
      params: { ddid: orderPkid, paymentAmount: paymentAmount, zfshPkid: zfshPkid, code: code },
      callback: data => {
         if (data && data.code == 1) {
            //打开微信支付页面
            //................
      },
   });
};

后端请求接口:

请求微信下单Controller:

@ApiOperation(value = "跳转微信支付界面", notes = "跳转微信支付界面-", httpMethod = "POST")
@ApiImplicitParams({
        @ApiImplicitParam(paramType = "Map", name = "params", dataType = "Map", required = true, value = "微信支付参数信息")})
@RequestMapping(value = "/toWxPay", method = RequestMethod.POST)
public JsonResult toWxPay(@RequestBody Map<String, Object> params, HttpServletRequest request) {
    try {
        return JsonResult.success(wxPayService.toWxPay(params, request));
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        return JsonResult.failure(e.getMessage());
    }
}

Service业务层接口:

/**
 * 微信支付添加订单
 *
 * @param params
 * @param request
 * @return
 */
Map<String, String> toWxPay(Map<String, Object> params, HttpServletRequest request) throws Exception;

请求微信下单ServiceImpl:

@Override
public Map<String, String> toWxPay(Map<String, Object> params, HttpServletRequest request) {
    Map<String, String> result = new HashMap<>(20);
    try {
        if (null == params || params.isEmpty()) {
            logger.error("【微信支付】参数缺失,请检查!");
            result.put("code", "0");
            result.put("msg", "参数缺失,请检查!");
            return result;
        }

        //订单ID
        String ddid = null != params.get("ddid") ? params.get("ddid").toString() : "";
        //支付商号ID
        String zfshPkid = null != params.get("zfshPkid") ? params.get("zfshPkid").toString() : "";
        //支付金额
        String paymentAmount = null != params.get("paymentAmount") ? params.get("paymentAmount").toString() : "";
        //用户微信授权后获取的code,用于获取access_token和openId
        String code = null != params.get("code") ? params.get("code").toString() : "";

        //1、获取支付商户信息
        ZfglZfshbVO zfglZfshbVO = openapiMapper.getZfshInfo(zfshPkid);
        if (null == zfglZfshbVO) {
            logger.error("【微信支付】支付商户信息参数缺失,请检查!");
            result.put("code", "0");
            result.put("msg", "支付商户信息参数缺失,请检查!");
            return result;
        }

        //2. 根据订单ID查询是否存在未支付的订单信息
        PayOrderDTO payOrderDTO = payOrderPOMapper.getOrderInfoByPkid(ddid, "1");
        if (null == payOrderDTO) {
            logger.error("【微信支付】暂未查询到未支付订单!");
            result.put("code", "0");
            result.put("msg", "暂未查询到未支付订单!");
            return result;
        }

        //3、通过code获取openid信息
        String openId = getOpenId(zfglZfshbVO.getAppid(), zfglZfshbVO.getSyxx(), code);
        if (StringUtils.isBlank(openId)) {
            logger.error("【微信支付】openId参数缺失,请检查!");
            result.put("code", "0");
            result.put("msg", "【微信支付】openId参数缺失,请检查!");
            return result;
        }

        /**
         * 生成签名参考文档: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3
         * 签名校验工具: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=20_1
         */
        //微信商户支付API密钥,用于生成签名
        String apiKey = zfglZfshbVO.getApikey();

        /**
         * 统一下单参考文档: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
         */
        //4. 组装微信生成预付单所需参数
        Map<String, String> paymentParamsMap = new HashMap<>(30);
        //公众账号ID
        paymentParamsMap.put("appid", zfglZfshbVO.getAppid());
        //商户号
        paymentParamsMap.put("mch_id", zfglZfshbVO.getShwyyhh());
        //随机字符串,不长于32位
        paymentParamsMap.put("nonce_str", WXPayUtil.generateNonceStr());
        //商户订单号
        paymentParamsMap.put("out_trade_no", payOrderDTO.getDdh().split("@")[0]);
        //附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
        Map<String, String> customParamsMap = new HashMap<>(10);
        customParamsMap.put("zfshbPkid", zfshPkid);
        paymentParamsMap.put("attach", JSON.toJSONString(customParamsMap));
        //设备号,终端设备号(门店号或收银设备ID),注意:PC网页或公众号内支付请传"WEB"
        paymentParamsMap.put("device_info", "WEB");
        //货币类型,符合ISO 4217标准的三位字母代码,默认人民币:CNY
        paymentParamsMap.put("fee_type", "CNY");
        //总金额,订单总金额,单位为分
        paymentParamsMap.put("total_fee", paymentAmount);
        //商品简单描述,该字段须严格按照规范传递
        paymentParamsMap.put("body", zfglZfshbVO.getMc() + "-微信支付");
        //终端IP
        paymentParamsMap.put("spbill_create_ip", WXPayUtil.getIpAddress(request));
        //异步通知地址
        paymentParamsMap.put("notify_url", zfglZfshbVO.getYbtzurl());
        //交易类型,JSAPI -JSAPI支付  NATIVE -Native支付  APP -APP支付
        paymentParamsMap.put("trade_type", "JSAPI");
        //用户标识, trade_type=JSAPI时(即JSAPI支付),此参数必传,此参数为微信用户在商户对应appid下的唯一标识。
        paymentParamsMap.put("openid", openId);
        //场景信息
        paymentParamsMap.put("scene_info", "{\"h5_info\": {\"type\":\"Wap\",\"wap_url\": \"\",\"wap_name\": \"微信支付\"}}");
        //生成签名,MD5方式
        paymentParamsMap.put("sign", WXPayUtil.generateSignature(paymentParamsMap, apiKey, WXPayConstants.SignType.MD5));
        String payInfoXml = WXPayUtil.mapToXml(paymentParamsMap);
        logger.info("【微信支付】支付参数XML: {}", payInfoXml);
        //5.发送POST请求微信生成预付单
        String response = HttpUtil.doPost("https://api.mch.weixin.qq.com/pay/unifiedorder", payInfoXml, "UTF-8", 3000, 5000);
        if (StringUtils.isBlank(response)) {
            logger.error("【微信支付】请求微信生成预付单失败!");
            result.put("code", "0");
            result.put("msg", "请求微信生成预付单失败!");
            return result;
        }

        Map<String, String> resMap = WXPayUtil.xmlToMap(response);
        String returnCode = resMap.get("return_code");
        //解析微信返回的支付预付单信息,注意需要二次签名.
        if (SUCCESS.equalsIgnoreCase(returnCode)) {
            String resultCode = resMap.get("result_code");
            if (SUCCESS.equalsIgnoreCase(resultCode)) {
                //参数需要重新进行签名计算,参与签名的参数为:appId、timeStamp、nonceStr、package、signType,参数区分大小写。
                result.put("appId", zfglZfshbVO.getAppid());
                result.put("timeStamp", WXPayUtil.getCurrentTimestamp() + "");
                result.put("nonceStr", resMap.get("nonce_str"));
                result.put("package", "prepay_id=" + resMap.get("prepay_id"));
                result.put("signType", WXPayConstants.MD5);
                //第二次签名方式跟第一次一致,也是需要传入API密钥参与生成
                String paySign = WXPayUtil.generateSignature(result, apiKey, WXPayConstants.SignType.MD5);
                result.put("paySign", paySign);
                result.put("code", "1");
                return result;
            } else {
                logger.error("【微信支付失败】>> errMsg: {}", resMap.get("err_code_des"));
                result.put("code", "0");
                result.put("msg", resMap.get("err_code_des"));
            }
        } else {
            logger.error("【微信支付失败】>> errMsg: {}", resMap.get("return_msg"));
            result.put("code", "0");
            result.put("msg", resMap.get("return_msg"));
        }
    } catch (Exception e) {
        logger.error("【微信支付失败】: {}", e.getMessage());
        e.printStackTrace();
    }
    return result;
}

/**
 * 获取OpenId
 *
 * @param appId  公众号唯一标识
 * @param secret 公众号秘钥
 * @param code   用户同意授权后获取的code
 * @return </br>
 * 说明: 通过code换取网页授权access_token,由于微信支付需要openId生成预付单,此接口同时返回了openId,故直接获取即可;
 * 如需获取用户信息,还需要根据access_token获取, 发送GET请求: https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
 */
@SuppressWarnings("unchecked")
private String getOpenId(String appId, String secret, String code) {
    Map<String, String> map = new HashMap(10);
    String url = "https://api.weixin.qq.com/sns/oauth2/access_token";
    //公众号的唯一标识
    map.put("appid", appId);
    //公众号的appsecret
    map.put("secret", secret);
    //用户同意授权后获取的code参数
    map.put("code", code);
    //填写为authorization_code
    map.put("grant_type", "authorization_code");
    try {
        String tokenStr = HttpUtil.doGet(url, map, "UTF-8", 3000, 5000);
        Map<String, Object> codeMap = JSON.parseObject(tokenStr, Map.class);
        if (null != codeMap && !codeMap.isEmpty() && codeMap.containsKey("errcode")) {
            //获取openId出错
            String errCode = null != codeMap.get("errcode") ? codeMap.get("errcode").toString() : "";
            String errMsg = null != codeMap.get("errmsg") ? codeMap.get("errmsg").toString() : "";
            logger.error("【微信支付】获取openId失败 >> errcode:{}, errmsg: {}", errCode, errMsg);
            return null;
        } else {
            String openId = null != codeMap.get("openid") ? codeMap.get("openid").toString() : "";
            return StringUtils.isNotBlank(openId) ? openId : "";
        }
    } catch (Exception exception) {
        exception.printStackTrace();
        return null;
    }
}

WXPayUtil微信支付工具类:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.*;


public class WXPayUtil {

    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private static final Random RANDOM = new SecureRandom();

    /**
     * XML格式字符串转换为Map
     *
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<String, String>();
            DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
            throw ex;
        }

    }

    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        org.w3c.dom.Document document = WXPayXmlUtil.newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key: data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        }
        catch (Exception ex) {
        }
        return output;
    }


    /**
     * 生成带有 sign 的 XML 格式字符串
     *
     * @param data Map类型数据
     * @param key API密钥
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
        return generateSignedXml(data, key, WXPayConstants.SignType.MD5);
    }

    /**
     * 生成带有 sign 的 XML 格式字符串
     *
     * @param data Map类型数据
     * @param key API密钥
     * @param signType 签名类型
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
        String sign = generateSignature(data, key, signType);
        data.put(WXPayConstants.FIELD_SIGN, sign);
        return mapToXml(data);
    }


    /**
     * 判断签名是否正确
     *
     * @param xmlStr XML格式数据
     * @param key API密钥
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
        Map<String, String> data = xmlToMap(xmlStr);
        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key).equals(sign);
    }

    /**
     * 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
     *
     * @param data Map类型数据
     * @param key API密钥
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
        return isSignatureValid(data, key, WXPayConstants.SignType.MD5);
    }

    /**
     * 判断签名是否正确,必须包含sign字段,否则返回false。
     *
     * @param data Map类型数据
     * @param key API密钥
     * @param signType 签名方式
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key, signType).equals(sign);
    }

    /**
     * 生成签名
     *
     * @param data 待签名数据
     * @param key API密钥
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key) throws Exception {
        return generateSignature(data, key, WXPayConstants.SignType.MD5);
    }

    /**
     * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
     *
     * @param data 待签名数据
     * @param key API密钥
     * @param signType 签名方式
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals(WXPayConstants.FIELD_SIGN)) {
                continue;
            }
            if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
        }
        sb.append("key=").append(key);
        if (WXPayConstants.SignType.MD5.equals(signType)) {
            return MD5(sb.toString()).toUpperCase();
        }
        else if (WXPayConstants.SignType.HMACSHA256.equals(signType)) {
            return HMACSHA256(sb.toString(), key);
        }
        else {
            throw new Exception(String.format("Invalid sign_type: %s", signType));
        }
    }


    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }


    /**
     * 生成 MD5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public static String MD5(String data) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] array = md.digest(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 生成 HMACSHA256
     * @param data 待处理数据
     * @param key 密钥
     * @return 加密结果
     * @throws Exception
     */
    public static String HMACSHA256(String data, String key) throws Exception {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 日志
     * @return
     */
    public static Logger getLogger() {
        Logger logger = LoggerFactory.getLogger("wxpay java sdk");
        return logger;
    }

    /**
     * 获取当前时间戳,单位秒
     * @return
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis()/1000;
    }

    /**
     * 获取当前时间戳,单位毫秒
     * @return
     */
    public static long getCurrentTimestampMs() {
        return System.currentTimeMillis();
    }


    /**
     * 从request中获取请求方IP
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            // 若以上方式均为获取到ip则证明获得客户端并没有采用反向代理直接使用getRemoteAddr()获取客户端的ip地址
            ip = request.getRemoteAddr();
        }
        // 多个路由时,取第一个非unknown的ip
        final String[] arr = ip.split(",");
        for (final String str : arr) {
            if (!"unknown".equalsIgnoreCase(str)) {
                ip = str;
                break;
            }
        }
        return ip;
    }
}

WXPayXmlUtil解析XML工具类: 

import org.w3c.dom.Document;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * 2018/7/3
 */
public final class WXPayXmlUtil {
    public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        documentBuilderFactory.setXIncludeAware(false);
        documentBuilderFactory.setExpandEntityReferences(false);

        return documentBuilderFactory.newDocumentBuilder();
    }

    public static Document newDocument() throws ParserConfigurationException {
        return newDocumentBuilder().newDocument();
    }
}

WXPayConstants常量类: 

import org.apache.http.client.HttpClient;

/**
 * 常量
 */
public class WXPayConstants {

    public enum SignType {
        MD5, HMACSHA256
    }

    public static final String DOMAIN_API = "api.mch.weixin.qq.com";
    public static final String DOMAIN_API2 = "api2.mch.weixin.qq.com";
    public static final String DOMAIN_APIHK = "apihk.mch.weixin.qq.com";
    public static final String DOMAIN_APIUS = "apius.mch.weixin.qq.com";


    public static final String FAIL     = "FAIL";
    public static final String SUCCESS  = "SUCCESS";
    public static final String HMACSHA256 = "HMAC-SHA256";
    public static final String MD5 = "MD5";

    public static final String FIELD_SIGN = "sign";
    public static final String FIELD_SIGN_TYPE = "sign_type";

    public static final String WXPAYSDK_VERSION = "WXPaySDK/3.0.9";
    public static final String USER_AGENT = WXPAYSDK_VERSION +
            " (" + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version") +
            ") Java/" + System.getProperty("java.version") + " HttpClient/" + HttpClient.class.getPackage().getImplementationVersion();

    public static final String MICROPAY_URL_SUFFIX     = "/pay/micropay";
    public static final String UNIFIEDORDER_URL_SUFFIX = "/pay/unifiedorder";
    public static final String ORDERQUERY_URL_SUFFIX   = "/pay/orderquery";
    public static final String REVERSE_URL_SUFFIX      = "/secapi/pay/reverse";
    public static final String CLOSEORDER_URL_SUFFIX   = "/pay/closeorder";
    public static final String REFUND_URL_SUFFIX       = "/secapi/pay/refund";
    public static final String REFUNDQUERY_URL_SUFFIX  = "/pay/refundquery";
    public static final String DOWNLOADBILL_URL_SUFFIX = "/pay/downloadbill";
    public static final String REPORT_URL_SUFFIX       = "/payitil/report";
    public static final String SHORTURL_URL_SUFFIX     = "/tools/shorturl";
    public static final String AUTHCODETOOPENID_URL_SUFFIX = "/tools/authcodetoopenid";

    // sandbox
    public static final String SANDBOX_MICROPAY_URL_SUFFIX     = "/sandboxnew/pay/micropay";
    public static final String SANDBOX_UNIFIEDORDER_URL_SUFFIX = "/sandboxnew/pay/unifiedorder";
    public static final String SANDBOX_ORDERQUERY_URL_SUFFIX   = "/sandboxnew/pay/orderquery";
    public static final String SANDBOX_REVERSE_URL_SUFFIX      = "/sandboxnew/secapi/pay/reverse";
    public static final String SANDBOX_CLOSEORDER_URL_SUFFIX   = "/sandboxnew/pay/closeorder";
    public static final String SANDBOX_REFUND_URL_SUFFIX       = "/sandboxnew/secapi/pay/refund";
    public static final String SANDBOX_REFUNDQUERY_URL_SUFFIX  = "/sandboxnew/pay/refundquery";
    public static final String SANDBOX_DOWNLOADBILL_URL_SUFFIX = "/sandboxnew/pay/downloadbill";
    public static final String SANDBOX_REPORT_URL_SUFFIX       = "/sandboxnew/payitil/report";
    public static final String SANDBOX_SHORTURL_URL_SUFFIX     = "/sandboxnew/tools/shorturl";
    public static final String SANDBOX_AUTHCODETOOPENID_URL_SUFFIX = "/sandboxnew/tools/authcodetoopenid";

}

第四步:微信内H5调起支付

在微信浏览器里面打开H5网页中执行JS调起支付。接口输入输出数据格式为JSON。

注意:WeixinJSBridge内置对象在其他浏览器中无效。

getBrandWCPayRequest参数以及返回值定义:

1、网页端接口请求参数列表(参数需要重新进行签名计算,参与签名的参数为:appId、timeStamp、nonceStr、package、signType,参数区分大小写。)

如下代码是微信成功生成预付单并返回到商户系统之后的回调: 请求打开微信支付弹窗进行支付。

if (data && data.code == 1) {
   //打开微信支付页面
   let resData = {};
   resData['appId'] = data.appId;   //公众号名称,由商户传入
   resData['timeStamp'] = data.timeStamp;  //时间戳,自1970年以来的秒数
   resData['nonceStr'] = data.nonceStr;  //随机串
   resData['package'] = data.package;   //统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
   resData['signType'] = data.signType;  //微信签名方式
   resData['paySign'] = data.paySign;  //微信签名
   if (typeof WeixinJSBridge == 'undefined') {
      if (document.addEventListener) {
         document.addEventListener('WeixinJSBridgeReady', function() {
            this.onBridgeReady(resData, id);
         }, false);
      } else if (document.attachEvent) {
         document.attachEvent('WeixinJSBridgeReady', function() {
            this.onBridgeReady(resData, id);
         });
         document.attachEvent('onWeixinJSBridgeReady', function() {
            this.onBridgeReady(resData, id);
         });
      }
   } else {
      this.onBridgeReady(resData, id);
   }
} else {
   Toast.fail(data.msg);
}

//微信内H5调起支付
onBridgeReady = (data, id) => {
   //注意: WeixinJSBridge内置对象在其他浏览器中无效
   //网页端接口请求参数列表(参数需要重新进行签名计算,参与签名的参数为:appId、timeStamp、nonceStr、package、signType,参数区分大小写。)
   WeixinJSBridge.invoke(
      'getBrandWCPayRequest',
      {
         'appId': data.appId,     //公众号id,由商户传入
         'timeStamp': data.timeStamp, //时间戳,自1970年以来的秒数
         'nonceStr': data.nonceStr, //随机串
         'package': data.package,  //统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
         'signType': data.signType, //微信签名方式
         'paySign': data.paySign, //微信签名
      },
      function(res) {
         /**
          * 返回结果值说明:
          * get_brand_wcpay_request:ok    支付成功
          * get_brand_wcpay_request:cancel    支付过程中用户取消
          * get_brand_wcpay_request:fail    支付失败
          */
         if (res.err_msg === 'get_brand_wcpay_request:ok') {
            Toast.success('支付成功', 1, () => {
               window.location = `${window.location.origin}/#/UnifiedPaymentPlatform/payMain?id=${id}`;
            });
         } else {
            Toast.fail('用户支付遇到错误或者取消支付');
         }
      },
   );
};

七、支付结果通知

参考文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8

应用场景:

支付完成后,微信会把相关支付结果及用户信息通过数据流的形式发送给商户,商户需要接收处理,并按文档规范返回应答。首先我们会在配置支付请求参数的时候,配置了回调地址(notify_url),支付完成微信服务器就会将结果推送到这个地址上,我们只是需要获取那些数据就可以了,然后根据处理相关业务。

注意:

  • 1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
  • 2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起多次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
  • 3、在订单状态不明或者没有收到微信支付结果通知的情况下,建议商户主动调用微信支付【查询订单API】确认订单状态。

特别提醒:

  • 1、商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。
  • 2、当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。

接口链接:

该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。

通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”。

 

微信支付异步通知接口代码:

/**
 * 微信支付异步通知结果
 *
 * @return 说明:
 * 支付完成后,微信会把相关支付结果及用户信息通过数据流的形式发送给商户,商户需要接收处理,并按文档规范返回应答。
 * <p>
 * 注意:
 * 1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
 * <p>
 * 2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起多次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
 * <p>
 * 3、在订单状态不明或者没有收到微信支付结果通知的情况下,建议商户主动调用微信支付【查询订单API】确认订单状态。
 */
@Override
@Transactional
@SuppressWarnings("unchecked")
public String wxPaySyncNotification(HttpServletRequest request, HttpServletResponse response) {
    try {
        ServletInputStream servletInputStream = request.getInputStream();
        BufferedReader in;
        StringBuilder result = new StringBuilder();
        in = new BufferedReader(new InputStreamReader(servletInputStream));
        String line;
        while ((line = in.readLine()) != null) {
            result.append(line);
        }
        //1. 解析微信异步通知结果
        Map<String, String> resultMap = WXPayUtil.xmlToMap(result.toString());
        if (null == resultMap || resultMap.isEmpty()) {
            return returnFailInfo("异步通知参数不能为空!");
        }
        logger.info("【微信支付】异步通知, 通知参数: {}", resultMap);

        //2. 验证return_code是否为SUCCESS
        String returnCode = resultMap.get("return_code");
        if (SUCCESS.equalsIgnoreCase(returnCode)) {
            //3. 获取微信下单时传递的自定义参数(支付商户表PKID)
            String attach = resultMap.get("attach");
            Map<String, Object> attachMap = (Map<String, Object>) JSON.parse(attach);
            String zfshbPkid = null != attachMap.get("zfshbPkid") ? attachMap.get("zfshbPkid").toString() : "";

            if (StringUtils.isBlank(zfshbPkid)) {
                logger.error("微信支付自定义参数attach不能为空");
                return returnFailInfo("微信支付自定义参数attach不能为空");
            }

            //4. 根据支付商户PKID查询相关设置信息
            ZfglZfshbVO zfshInfo = openapiMapper.getZfshInfo(zfshbPkid);
            if (null == zfshInfo) {
                return returnFailInfo("支付商户信息为空");
            }

            //商户订单号
            String outTradeNo = resultMap.get("out_trade_no");
            //5、验证商户订单号是否存在
            if (StringUtils.isBlank(outTradeNo)) {
                logger.error("out_trade_no(商户订单号)不能为空");
                return returnFailInfo("out_trade_no(商户订单号)不能为空");
            }

            //6. 查询业务系统订单信息
            String ddh = openapiMapper.selectDdhByWxOutTradeNo(outTradeNo);
            PayOrderPO payOrderPO = payOrderPOMapper.getPayOrderDetailByTradeNo(ddh);
            if (null == payOrderPO) {
                //订单不存在,返回success,反馈微信不需要继续异步通知
                return returnSuccessInfo("订单不存在,无需通知");
            }

            //7. 判断订单是否是等待付款状态,如果不是则不需要通知
            if (!"1".equals(payOrderPO.getJyzt())) {
                return returnSuccessInfo("交易状态非等待付款,无需通知");
            }

            //8. 记录通知日志
            //.......

            //商户号
            String mchId = resultMap.get("mch_id");
            //9. 验证商户号是否存在
            if (StringUtils.isBlank(mchId)) {
                logger.error("mch_id(商户号)不能为空");
                return returnFailInfo("mch_id(商户号)不能为空");
            }

            //订单总金额,单位为分
            String totalFee = resultMap.get("total_fee");
            if (StringUtils.isBlank(totalFee)) {
                logger.error("totalFee(订单总金额)不能为空");
                return returnFailInfo("totalFee(订单总金额)不能为空");
            }

            String shwyyhh = zfshInfo.getShwyyhh();
            //10. 判断订单里面的商户号与交易金额是否与微信通知过来的商户号、交易金额相同
            BigDecimal jyje = payOrderPO.getJyje();
            //乘以100(单位:分)
            BigDecimal newJyje = jyje.multiply(new BigDecimal(100));
            if (mchId.equals(shwyyhh) && String.valueOf(newJyje.doubleValue()).equals(String.valueOf(Double.parseDouble(totalFee)))) {
                //11. 根据订单ID查询出对应的订单明细
                //.............
                
                //12. 查询订单是否已通知成功
                String jyzt = payOrderPOMapper.getJyzt(payOrderPO.getPkid());
                if (!(StringUtils.isNotBlank(jyzt) && "3".equals(jyzt))) {
                    //交易支付成功,更新该订单状态为已支付
                    //..................
                    logger.info("【微信支付】交易支付成功,修改订单为已支付");

                    //修改订单状态
                    //..................
                    //批量保存实收数据
                    //..................
                    //批量保存实收数据明细
                    //..................
                }
            }
            return returnSuccessInfo("微信支付成功");
        } else {
            logger.error("【微信异步通知】>>> return_code为false");
            return returnFailInfo("【微信异步通知】>>> return_code为false");
        }
    } catch (Exception e) {
        e.printStackTrace();
        logger.error("微信支付回调出现未知错误");
        return returnFailInfo("微信支付回调出现未知错误");
    }
}

private String returnSuccessInfo(String returnMsg) {
    try {
        Map<String, String> map = new HashMap<>();
        map.put("return_code", SUCCESS);
        map.put("return_msg", returnMsg);
        return WXPayUtil.mapToXml(map);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

private String returnFailInfo(String returnMsg) {
    try {
        Map<String, String> map = new HashMap<>();
        map.put("return_code", FAIL);
        map.put("return_msg", returnMsg);
        return WXPayUtil.mapToXml(map);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

测试结果:

由于微信异步通知接口等都需要外网可以访问,所以这里介绍一个内网穿透的工具utools来进行测试,可以免费获得一个外网访问的域名,通过映射我们本地启动的项目,进行微信开发调试。如下图所示:

支付结果测试如下图:

八、支付常见问题

下面分享一些笔者调试遇到的几个问题,并给出了相应的解决方法,当然可能还有其他原因导致,得具体问题具体分析。

【a】{"errcode":40125,"errmsg":"invalid appsecret, view more at http:\/\/t.cn\/RAEkdVq, hints: [ req_id: GhFEBK4FE-.yDWiA ]"}

如果报以上错误,我们需要检查一下appsecret是否正确,如果不正确,需要到微信公众平台重置一下。

【b】<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名错误]]></return_msg></xml>

笔者调试时遇到这个错误,是因为参与签名的key写错了,应该是微信商户平台设置的API密钥才对。

【c】errMsg: "the permission value is offline verifying"

如果报以上错误,可能是因为微信开发者工具调取不了支付弹窗,我们可以使用真机进行测试即可。

【d】当前页面的URL未注册:http://wwtt.utools.club/

如果报上述错误,说明我们在微信商户凭条配置的授权域名有问题,需要跟实际支付的域名保持一致。

【e】调用支付JSAPI缺少参数:appId

笔者调试时遇到这个错误,真的是微信的大坑。如下图说明:

 

上述第二个参数文档说要是json,然后笔者前端一开始使用:JSON.stringfy(params)传递进去,直接就报错缺少appId,明明已经传了还说缺失。

后来,在下面的文章中找到解决方法:https://www.cnblogs.com/danlis/p/5566740.html

居然是JSON格式问题,后来直接写成下面的写法,直接传一个对象过去,就成功弹出支付页面:

WeixinJSBridge.invoke(
   'getBrandWCPayRequest',
   {
      'appId': data.appId,     //公众号id,由商户传入
      'timeStamp': data.timeStamp, //时间戳,自1970年以来的秒数
      'nonceStr': data.nonceStr, //随机串
      'package': data.package,  //统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
      'signType': data.signType, //微信签名方式
      'paySign': data.paySign, //微信签名
   },
   function(res) {
      /**
       * 返回结果值说明:
       * get_brand_wcpay_request:ok    支付成功
       * get_brand_wcpay_request:cancel    支付过程中用户取消
       * get_brand_wcpay_request:fail    支付失败
       */
      if (res.err_msg === 'get_brand_wcpay_request:ok') {
         Toast.success('支付成功', 1, () => {
            window.location = `${window.location.origin}/#/UnifiedPaymentPlatform/payMain?id=${id}`;
         });
      } else {
         Toast.fail('用户支付遇到错误或者取消支付');
      }
   },
);

【f】redirect_uri参数错误

笔者调试时遇到的问题是redirect_uri参数错误是因为获取用户授权的outh2路径写错了:

正确格式大体如下:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxf0e81c3bee622d60&redirect_uri=http%3A%2F%2Fnba.bluewebgame.com%2Foauth_response.php&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect

其他可能原因:

支付授权目录是否设置正确;

网页授权域名是否设置正确;

参数错误;

九、总结

微信JSAPI支付方式较其他方式稍复杂一些,我们总结一下大体的流程:

  1. 获取微信支付四大参数: 公众APPID,APPSECEPT ,微信商户平台商户ID, 微信商户平台支付API密钥;
  2. 在微信公众平台设置好授权域名,在微信商户平台设置好授权支付目录信息;
  3. 用户访问我们的业务系统,进行微信网页授权,用户同意授权后获取code;
  4. 先保存订单信息到业务系统中,保存成功后发起微信下单接口;
  5. 根据code获取openid,获取成功后调用微信统一下单接口,获取预付单信息;
  6. 解析微信返回的预付单信息,解析出prepay_id;
  7. 根据appId、timeStamp、nonceStr、package、signType进行第二次签名,返回给前台;
  8. 商户网页前端使用getBrandWCPayRequest接口调起微信支付,传入上一步返回的参数;
  9. 弹出支付弹窗,用户支付成功后,在该接口里面会返回结果,同时微信也会发送异步通知到统一下单时候填写的notify_url;
  10. 回调支付成功通知,通知微信支付结果,并进行我们的后续的业务处理,如修改订单状态,记录日志,生成支付记录等等。

以上就是关于微信支付之JSAPI方式详细的实现步骤,对接微信支付可能会遇到一些奇奇怪怪的问题,查阅官方文档以及度娘一般都可以解决,由于笔者水平有限,文中难免有些没考虑周全的地方,还望大家指正,相互学习,一起进步!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值