05
2017
10

微信公众号支付开发流程与避坑手册-Java篇

最近完成了微信公众号内的未支付功能,当然开发的过程中难免遇到各种各样的问题,在这里把我开发的过程分享出来,给大家做个参考。
首先,在准备开发的时候需要进行必要的配置。

1.登录微信商户平台,在产品中心->开发配置中对支付授权目录进行配置
(注意:支付授权目录的配置规则是你使用微信支付控件页面的上一级目录,比如:你在www.xxx.cpm/wx/pay/pay.html中调用微信支付控件,那么你需要配置的目录是www.xxx.cpm/wx/pay/)
这里写图片描述

2.在账户中心->API安全中配置key值,此参数在生成签名时需要用到。
这里写图片描述

3.其他参数与配置:appid,开发者密码以及网页授权
这里写图片描述

在做完这些配置以后,我们就可以进行微信支付的开发了。在这里推荐参考微信平台提供的demo。https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1

按照文档中统一接口的实现流程,我们需要先实现发起微信支付的请求。这里的请求链接是统一下单的链接,POST请求

  /** * 发起微信支付请求 * @param requestUrl 请求链接 * @param method 请求方式 * @param param 请求参数 * @param connectTimeOut 连接超时时间 * @param readTimeOut 读取超时时间 * @return 请求结果 */
public String requestOnce(final String requestUrl, final String method, String param, boolean useCert, int connectTimeOut, int readTimeOut) {
        HttpURLConnection conn = null;
        StringBuilder builder = new StringBuilder();
        try {
            URL url = new URL(requestUrl);
            conn = (HttpURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            conn.setRequestMethod(method);
            conn.setConnectTimeout(connectTimeOut);
            conn.setReadTimeout(readTimeOut);
            OutputStream out = conn.getOutputStream();
            out.write(param.getBytes());
            out.flush();
            out.close();
            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {
                InputStream inputStream = conn.getInputStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
                String line = null;
                while ((line = reader.readLine()) != null) {
                    builder.append(line);
                }
                reader.close();
                inputStream.close();
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return builder.toString();
    }

在这里我们将微信支付所需要的参数进行填充。

 /** * 填充微信支付参数 * @param data 需要填充的Hashmap * @return 参数 */
    public Map<String, String>  fillRequestData(Map<String, String> data) throws Exception {
        data.put("appid", config.getAPPID());
        data.put("mch_id", config.getMCHID());
        data.put("nonce_str", WXPayUtil.generateNonceStr());
        data.put("notify_url",config.getNOTIFY_URL());
        data.put("spbill_create_ip", config.getSERVER_IP());
        data.put("trade_type", config.getJSAPI());
        if (WXPayConstants.SignType.MD5.equals(this.signType)) {
            data.put("sign_type", WXPayConstants.MD5);
        }
        else if (WXPayConstants.SignType.HMACSHA256.equals(this.signType)) {
            data.put("sign_type", WXPayConstants.HMACSHA256);
        }
        data.put("sign", WXPayUtil.generateSignature(data, config.getKEY(), this.signType));
        return data;
    }

在请求成功之后,微信服务器会返回xml形式的参数,我们可以将xml转换为Map(此方法在微信demo中有实现)。此外在结果返回成功以后,需要再次生成签名,此签名是由于前端页面调起微信支付控件所用

 /** * 发起微信支付认证请求 * @param requestUrl 请求链接 * @param method 请求方式 * @param param 请求参数 * @param useCert 是否使用证书 * @return 认证结果 */
    private Map<String, String> request(final String requestUrl, final String method, String param, boolean useCert) {
        String strXml = wxPayRequest.request(requestUrl, method, param, useCert, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs());
        Map<String, String> data;
        try {
            data = WXPayUtil.xmlToMap(strXml);
        } catch (Exception e) {
            data = null;
            e.printStackTrace();
        }
        return data;
    }

private Map<String, String> returnAuthParam(Map<String, String> temp, String key) {
        Map<String, String> result = new HashMap<String, String>();
        long timestamp = System.currentTimeMillis() / 1000;
        result.put("appId", temp.get("appid"));
        result.put("timeStamp", String.valueOf(timestamp));
        result.put("nonceStr", temp.get("nonce_str"));
        result.put("package", "prepay_id=" + temp.get("prepay_id"));
        result.put("signType", WXPayConstants.MD5);
        String sign = null;
        try {
            sign = WXPayUtil.generateSignature(result, key, WXPayConstants.SignType.MD5);
        } catch (Exception e) {
            e.printStackTrace();
        }
        result.put("paySign", sign);
        result.put("return_code", temp.get("return_code"));
        result.put("result_code", temp.get("result_code"));
        return result;
    }

签名生成方法:

/** * 生成签名 MD5 HMACSHA256 * @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 == signType) {
            return MD5(sb.toString()).toUpperCase();
        }
        throw new Exception(String.format("Invalid sign_type: %s", signType));
    }

在做完这些操作以后,就可以在前端实现调起微信支付控件的逻辑了。在微信支付文档中给出了一种写法:

if (typeof WeixinJSBridge == "undefined"){
                       if( document.addEventListener ){
                           document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
                       }else if (document.attachEvent){
                           document.attachEvent('WeixinJSBridgeReady', onBridgeReady); 
                           document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
                       }
                    }else{
                       onBridgeReady(data);
                    }
function onBridgeReady(data){
    orderPage.orderParam.orderId = data.orderId;
    WeixinJSBridge.invoke(
        'getBrandWCPayRequest', {
        "appId": data.appId,     //公众号名称,由商户传入 
        "timeStamp": data.timeStamp, //时间戳,自1970年以来的秒数
        "nonceStr": data.nonceStr, //随机串 
        "package": data.package,     
        "signType": data.signType,         //微信签名方式: 
        "paySign": data.paySign //微信签名 
        },
        function(res){
            alert(res.err_msg + " " + res.err_code + " " + res.err_desc);
            if(res.err_msg == "get_brand_wcpay_request:ok"){
                orderPage.orderParam.status = "WAITSEND";
                orderPage.submitOrder();
            }else if(res.err_msg == "get_brand_wcpay_request:fail"){
                alert("调用微信支付失败");
            }else if(res.err_msg == "get_brand_wcpay_request:cancel"){
                alert("已取消支付");
            }
        }
    ); 
}

在上面所使用到的参数都是服务端微信支付请求成功后获取的参数。完成这些之后,微信支付就可以使用了。
但是我们开发的过程中,如果没有相关的项目经验,难免会遇到很多坑。这里我就列举出我在开发中遇到的问题。
1.参数缺失:缺少openid,在微信公众号内嵌网页中调用微信支付需要添加用户的openid。
2.参数形式错误:在前端调起微信支付控件时,将package参数传错,package参数的正确格式为:“prepay_id=XXXXXXXXX”,而我忘了加入“prepay_id=”,这个错误着实让我找了好久(都是不仔细阅读文档的问题)。
3.签名失败:这个签名失败是在前端提示。造成的原因是我在微信支付请求成功之后,直接使用了服务端申请微信支付请求生成的签名,而正确的应该是在申请支付权限成功之后,需要将前端用到的参数拿过来重新生成一次签名才对。这个错误卡了我大半天才搞定,需要注意。
4.get_wcpay_branch_request:fail,这个错误的排查顺序是:首先查看返回参数中request_code,result_code的返回值,如果都为SUCCESS,那么说明服务端请求微信支付权限已经成功,错误就来自于前端,或者参数错误(参考3)。

微信支付的调试:
由于开发微信支付需要调用第三方接口,所以在调试的时候会遇到诸多不便。这里我们就需要使用一些工具来帮助我们完成。
1.微信web开发者工具,我们可以使用这个工具进行调试,直接输入链接地址,然后在这里可以断点调试也可以查看相关错误信息。
这里写图片描述
2。打印log。在前端我们可以打印出关键信息进行错误排查。比如说,在调用微信支付控件时,输出err_msg,err_code,err_desc,进行查看。
这里写图片描述

最后说两句
在做完微信支付以后,其实你会发现并没有太大难度,就是在开发的过程中我们需要善于发现问题。还有就是要仔细阅读文档,我遇到的一些错误有很多都是文档中所提到的,但是没有仔细看所以就花费了很多时间去排查错误,而这些都是可以避免的。

最后一张,微信支付的目录结构(仅供参考):
这里写图片描述

上一篇:Android开源日志库Logger的使用 下一篇:(二十三)Animator 实例 —— 开场动画