微信支付服务端开发总结

来源:互联网 发布:自瞄程序源码 编辑:程序博客网 时间:2024/06/08 08:02

前言

最近应公司业务需求,把微信支付完成了,当然已经顺利上线。但是开发的过程是也是踩了很多坑,下面我就先说说开发流程,以及在开发中遇到的大大小小的坑。

开发流程

首先,看一下微信开方平台关于支付的一个时序图,如下:


微信支付时序图
https://pay.weixin.qq.com/wiki/doc/api/app/app.php

商户系统和微信支付系统主要交互说明:步骤1:用户在商户APP中选择商品,提交订单,选择微信支付。步骤2:商户后台收到用户支付单,调用微信支付统一下单接口。参见【统一下单API】。步骤3:统一下单接口返回正常的prepay_id,再按签名规范重新生成签名后,将数据传输给APP。参与签名的字段名为appId,partnerId,prepayId,nonceStr,timeStamp,package。注意:package的值格式为Sign=WXPay步骤4:商户APP调起微信支付。api参见本章节【app端开发步骤说明】步骤5:商户后台接收支付通知。api参见【支付结果通知API】步骤6:商户后台查询支付结果。,api参见【查询订单API】

这里我讲解的服务端的开发,那我们就看服务端需要做什么工作。

第一步 统一下单

商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易回话标识后再在APP里面调起支付。
首先,准备请求的参数
代码如下:

private SortedMap<String, Object> prepareOrder(String ip, String orderId,            int price) {        Map<String, Object> oparams = ImmutableMap.<String, Object> builder()                .put("appid", ConfigUtil.APPID)//应用号                .put("body", WeixinConstant.PRODUCT_BODY)// 商品描述                .put("mch_id", ConfigUtil.MCH_ID)// 商户号                .put("nonce_str", PayCommonUtil.CreateNoncestr())// 16随机字符串(大小写字母加数字)                .put("out_trade_no", orderId)// 商户订单号                .put("total_fee", "1")// 银行币种支付的钱钱啦                .put("spbill_create_ip", ip)// IP地址                .put("notify_url", ConfigUtil.NOTIFY_URL) // 微信回调地址                .put("trade_type", ConfigUtil.TRADE_TYPE)// 支付类型 APP                .build();        return MapUtils.sortMap(oparams);    }

接下来将这些请求参数格式化成XML格式的数据 like this

<xml>   <appid>wx2421b1c4370ec43b</appid>   <attach>支付测试</attach>   <body>APP支付测试</body>   <mch_id>10000100</mch_id>   <nonce_str>1add1a30ac87aa2db72f57a2375d8fec</nonce_str>   <notify_url>http://wxpay.weixin.qq.com/pub_v2/pay/notify.v2.php</notify_url>   <out_trade_no>1415659990</out_trade_no>   <spbill_create_ip>14.23.150.211</spbill_create_ip>   <total_fee>1</total_fee>   <trade_type>APP</trade_type>   <sign>0CB01533B8C1EF103065174F50BCA001</sign></xml>

请求统一下单地址 https://api.mch.weixin.qq.com/pay/unifiedorder
代码(部分代码,完整的代码请见我的)github

String requestXML = PayCommonUtil.getRequestXml(parameters);// 生成xml格式字符串String responseStr = HttpUtil.httpsRequest(ConfigUtil.UNIFIED_ORDER_URL, "POST", requestXML);// 带上post

完成之后将微信返回的数据进行解析,取出APP客户端需要的数据,用于唤起微信支付。
代码

    /**     * 生成订单完成,返回给android,ios唤起微信所需要的参数。     *      * @param resutlMap     * @return     * @throws UnsupportedEncodingException     */    private SortedMap<String, Object> buildClientJson(            Map<String, Object> resutlMap) throws UnsupportedEncodingException {        // 获取微信返回的签名        /**         * backObject.put("appid", appid);         *          * backObject.put("noncestr", payParams.get("noncestr"));         *          * backObject.put("package", "Sign=WXPay");         *          * backObject.put("partnerid", payParams.get("partnerid"));         *          * backObject.put("prepayid", payParams.get("prepayid"));         *          * backObject.put("appkey", this.appkey);         *          * backObject.put("timestamp",payParams.get("timestamp"));         *          * backObject.put("sign",payParams.get("sign"));         */        Map<String, Object> params = ImmutableMap.<String, Object> builder()                .put("appid", ConfigUtil.APPID)                .put("noncestr", PayCommonUtil.CreateNoncestr())                .put("package", "Sign=WXPay")                .put("partnerid", ConfigUtil.MCH_ID)                .put("prepayid", resutlMap.get("prepay_id"))                .put("timestamp", DateUtils.getTimeStamp()).build();//取10位时间戳        // key ASCII排序        SortedMap<String, Object> sortMap = MapUtils.sortMap(params);        sortMap.put("package", "Sign=WXPay");        // paySign的生成规则和Sign的生成规则同理        String paySign = PayCommonUtil.createSign("UTF-8", sortMap);        sortMap.put("sign", paySign);        return sortMap;    }

整个统一下订单的逻辑就完成了。这里小结一下:

  1. 请求参数需要按照参数的key进行字母的ASCII码进行排序,由于我使用的是map数据结构,这里提供一个对map集合中的key元素进行排序的工具类

/**     * 对map根据key进行排序 ASCII 顺序     *      * @param 无序的map     * @return     */    public static SortedMap<String, Object> sortMap(Map<String, Object> map) {        List<Map.Entry<String, Object>> infoIds = new ArrayList<Map.Entry<String, Object>>(                map.entrySet());        // 排序        Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() {            public int compare(Map.Entry<String, Object> o1,                    Map.Entry<String, Object> o2) {                return (o1.getKey()).toString().compareTo(o2.getKey());            }        });        SortedMap<String, Object> sortmap = new TreeMap<String, Object>();        for (int i = 0; i < infoIds.size(); i++) {            String[] split = infoIds.get(i).toString().split("=");            sortmap.put(split[0], split[1]);        }        return sortmap;    }
  1. 对排序后的数据进行MD5签名,微信服务端会进行校验,防止数据在网络传输过程中被篡改。

  2. 拿到微信响应的数据,首先要做的事,也是对获取的数据进行签名校验,理由同上。

  3. 需要注意的一点,返回给app客户端的数据的key一定是小写,这点微信的api是没有说明白的,之前和客户端联调时耽误了很多时间,这也是微信支付被很多开发者吐槽的地方
    api比较难用^-^

  4. 注意小细节:返回给客户端时时间戳要是10位的,太长ios那边会越界,支付不成功。

第二步 调起支付

支付成功后,微信就会调用你填写的notify_url的方法,本人微信支付的开发配置中说明了我的notify_url为http://ip:port/weixin
/pay/callback/pay.action
对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略(如 30 分钟共 8 次)定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。由于存在重新収送后台通知的情况,因此同样的通知可能会多次収送给商户系统。 商户系统必须能够正确处理重复的通知。推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行幵収控制,以避免凼数重入造成的数据混乱。判断完成后,我们需要通知微信,我们收到信息了,不然微信就会通过一定的策略定期重新发起通知。

/**     * 微信回调告诉微信支付结果 注意:同样的通知可能会多次发送给此接口,注意处理重复的通知。     * 对于支付结果通知的内容做签名验证,防止数据泄漏导致出现“假通知”,造成资金损失。     *      * @param params     * @return     */    public String callback(HttpRequest request) {        try {            String responseStr = parseWeixinCallback(request);            Map<String, Object> map = XMLUtil.doXMLParse(responseStr);            // 校验签名 防止数据泄漏导致出现“假通知”,造成资金损失            if (!PayCommonUtil.checkIsSignValidFromResponseString(responseStr)) {                logger.error("微信回调失败,签名可能被篡改");                return PayCommonUtil.setXML("FAIL", "invalid sign");            }            if (WeixinConstant.FAIL.equalsIgnoreCase(map.get("result_code")                    .toString())) {                logger.error("微信回调失败");                return PayCommonUtil.setXML("FAIL", "weixin pay fail");            }            if (WeixinConstant.SUCCESS.equalsIgnoreCase(map.get("result_code")                    .toString())) {                //获取应用服务器需要的数据进行持久化操作                String outTradeNo = (String) map.get("out_trade_no");                String transactionId = (String) map.get("transaction_id");                String totlaFee = (String) map.get("total_fee");                Integer totalPrice = Integer.valueOf(totlaFee);                if (PayApp.theApp.isDebug()) {// 测试时候支付一分钱,买入价值6块的20分钟语音                    totalPrice = 6;                }                boolean isOk = updateDB(outTradeNo, transactionId, totalPrice,                        2);                // 告诉微信服务器,我收到信息了,不要在调用回调action了                if (isOk) {                    return PayCommonUtil.setXML(WeixinConstant.SUCCESS, "OK");                } else {                    return PayCommonUtil                            .setXML(WeixinConstant.FAIL, "pay fail");                }            }        } catch (Exception e) {            logger.debug("支付失败" + e.getMessage());            return PayCommonUtil.setXML(WeixinConstant.FAIL,                    "weixin pay server exception");        }        return PayCommonUtil.setXML(WeixinConstant.FAIL, "weixin pay fail");    }

小结:

  1. 当在本地做开发时,微信回调是不方便的,这里提供一种比较快速的方法,不过前提是有云服务器。用ssh建立反向通道。

步骤如下:(1) ssh -R 9999:localhost:9000 ubuntu@myserver_ip_address,输入密码;(2) server上查看一下是否监听了9999端口,netstat -anltp | grep 9999;ubuntu@VM-39-45-ubuntu:~$ netstat -anltp | grep 9999(Not all processes could be identified, non-owned process infowill not be shown, you would have to be root to see it all.)tcp        0      0 127.0.0.1:9999          0.0.0.0:*               LISTEN      -tcp6       0      0 ::1:9999                :::*                    LISTEN      -(3) 在本地9000上开启web服务;(4) 当微信回调公网服务器时就会被代理到本地9000端口对应的web服务;

这样就可以在本地调试了,是不是很方便呢。

2.回调逻辑中记得,将重要数据在应用服务器进行持久化哦。

第三步 查询订单

该接口提供所有微信支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。

需要调用查询接口的情况:◆ 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知;◆ 调用支付接口后,返回系统错误或未知交易状态情况;◆ 调用被扫支付API,返回USERPAYING的状态;◆ 调用关单或撤销接口API之前,需确认支付状态;

需要提供两个参数
outTradeNo 商户订单号
transactionId 微信订单号 
二选一
请求接口 https://api.mch.weixin.qq.com/pay/orderquery
代码:

/**     * 封装查询请求数据     * @param outTradeNo      * @param transactionId     * @return     */    private SortedMap<String, Object> prepareQueryData(String outTradeNo,            String transactionId) {        Map<String, Object> queryParams = null;        // 微信的订单号,优先使用        if (null == outTradeNo || outTradeNo.length() == 0) {            queryParams = ImmutableMap.<String, Object> builder()                    .put("appid", ConfigUtil.APPID)                    .put("mch_id", ConfigUtil.MCH_ID)                    .put("transaction_id", transactionId)                    .put("nonce_str", PayCommonUtil.CreateNoncestr()).build();        } else {            queryParams = ImmutableMap.<String, Object> builder()                    .put("appid", ConfigUtil.APPID)                    .put("mch_id", ConfigUtil.MCH_ID)                    .put("out_trade_no", outTradeNo)                    .put("nonce_str", PayCommonUtil.CreateNoncestr()).build();        }        // key ASCII 排序        SortedMap<String, Object> sortMap = MapUtils.sortMap(queryParams);        // 签名        String createSign = PayCommonUtil.createSign("UTF-8", sortMap);        sortMap.put("sign", createSign);        return sortMap;    }

下一步对微信响应的数据进行解析,检查支付的状态
代码如下

/**     * 查询订单状态     *      * @param params     *            订单查询参数     * @return     */    public HttpResult<String> checkOrderStatus(SortedMap<String, Object> params) {        if (params == null) {            return HttpResult.error(1, "查询订单参数不能为空");        }        try {            String requestXML = PayCommonUtil.getRequestXml(params);// 生成xml格式字符串            String responseStr = HttpUtil.httpsRequest(                    ConfigUtil.CHECK_ORDER_URL, "POST", requestXML);// 带上post            SortedMap<String, Object> responseMap = XMLUtil                    .doXMLParse(responseStr);// 解析响应xml格式字符串            // 校验响应结果return_code            if (WeixinConstant.FAIL.equalsIgnoreCase(responseMap.get(                    "return_code").toString())) {                return HttpResult.error(1, responseMap.get("return_msg")                        .toString());            }            // 校验业务结果result_code            if (WeixinConstant.FAIL.equalsIgnoreCase(responseMap.get(                    "result_code").toString())) {                return HttpResult.error(2, responseMap.get("err_code")                        .toString() + "=" + responseMap.get("err_code_des"));            }            // 校验签名            if (!PayCommonUtil.checkIsSignValidFromResponseString(responseStr)) {                logger.error("订单查询失败,签名可能被篡改");                return HttpResult.error(3, "签名错误");            }            // 判断支付状态            String tradeState = responseMap.get("trade_state").toString();            if (tradeState != null && tradeState.equals("SUCCESS")) {                return HttpResult.success(0, "订单支付成功");            } else if (tradeState == null) {                return HttpResult.error(4, "获取订单状态失败");            } else if (tradeState.equals("REFUND")) {                return HttpResult.error(5, "转入退款");            } else if (tradeState.equals("NOTPAY")) {                return HttpResult.error(6, "未支付");            } else if (tradeState.equals("CLOSED")) {                return HttpResult.error(7, "已关闭");            } else if (tradeState.equals("REVOKED")) {                return HttpResult.error(8, "已撤销(刷卡支付");            } else if (tradeState.equals("USERPAYING")) {                return HttpResult.error(9, "用户支付中");            } else if (tradeState.equals("PAYERROR")) {                return HttpResult.error(10, "支付失败");            } else {                return HttpResult.error(11, "未知的失败状态");            }        } catch (Exception e) {            logger.error("订单查询失败,查询参数 = {}", JSONObject.toJSONString(params));            return HttpResult.success(1, "订单查询失败");        }    }

整个流程就是这样的,呵呵呵...好久没写博客有点手生了。对于代码中很多工具类,这里就不一一贴出来了. Fork me on Githubthanks !

转载地址:https://segmentfault.com/a/1190000005795580

阅读全文
'); })();
0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 开口垫圈 防松动垫圈 垫圈标准 平垫圈标准 垫圈流体低 茶杯垫圈 垫圈图片 硅胶防水垫圈 弹簧垫圈标准 双叠自锁垫圈 金属缠绕垫圈 橡胶密封垫圈 弹簧垫圈的作用 毛毡密封垫圈 垫圈生产厂家 硅胶垫圈厂家 不锈钢垫圈规格 镀锌弹簧垫圈 锥形弹簧垫圈 不锈钢弹簧垫圈标准 尼龙垫片垫圈 硅胶垫圈价格 垫片种类 金属垫片 垫片的作用 垫片规格 医用气垫圈 榻榻米垫子 马桶垫子 海绵垫子 老板椅垫子 打坐垫子 大鼠标垫子 海棉垫子 泡沫垫子 小垫子 垫子图片 垫子英文 塌塌米垫子 羊毛毡垫子 练功垫子