微信支付服务端开发

前言

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

开发流程

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

微信支付时序图
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 info
will 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, "订单查询失败");
        }
    }
时间: 2024-10-23 19:43:59

微信支付服务端开发的相关文章

微信APP支付服务端开发Java版(一)

一.准备工作 去微信开发者中心下载(扫码支付,里面的大部分代码是可以用的) https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=11_1 选择UTF-8的版本copy到你的项目里面 找到Configure.java的类修改成public static String PAY_API = "https://api.mch.weixin.qq.com/pay/unifiedorder"; 下面正式进入代码部分 1 //初始化

支付宝支付与微信支付服务端回调notify_url数据的区别

这两天优化了一下支付宝支付和微信支付订单回调的问题,之前我们的订单都是用手动回调给服务器,现在改成支付宝和微信原生的异步回调,结果并没有像我们想象的那么简单,支付宝是很顺利的解决回调,用一般的方式接收即可,但是微信接收时用普通的接收方式是不行的必须用另一种方式,如下且看: 支付宝和微信的回调地址 notifyUrl = RequestUrl.BASE_URL+"order/order_payment"+"?order_code="+orderCode; 服务器端是P

现代Java服务端开发核心技术之开发工具箱

现代Java服务端开发核心技术之开发工具箱 现代Java服务端开发核心技术 2.1 开发工具概述 俗话说,工欲善其事必先利其器,掌握一些日常开中常用的工具软件能够大大提开发效率,工具本身的目的也是解放生产力.在安装各种软件时注意如果没有特殊需要不必使用最新版本,尤其是操作系统,例如当前(2018/10/12)最新版的macOS是10.14,但是运行在macOS之上的其他应用软件可能还没有及时做兼容新系统的版本,可能在系统升级之后无法正常使用,因此推荐在新系统正式推出半年后再升级最为稳妥. 而且软

个人公众号服务端开发Demo

公众号出来很久了,也可以个人申请.知道公众号的服务端开发其实很简单,接口调用封装,数据存取,不外如是. 人一旦懒了,真的是 "无可救药" 了...现简单描述晚到的公众号HelloWorld 思路 公众号里面简单的文章展示,静态博客展示这种好像没啥特别的,就跳过了. 在聊天框输入参数, 传到云服务器处理,然后请求个三方API获取信息,封装返回.嗯,这就算个基本的Demo了 准备 1, 得申请个公众号,个人的,免费 2, 最好买一台云服务器,双十一的阿里云最低100¥/Year,heihe

google支付服务端订单验证PHP代码

之前有转发一则关于google支付服务端验证的文章,今天把之前研究出得服务端订单支付验证代码(PHP版本)贴出来大家分享 在进行服务端交易验证之前,需要到google api consle后台https://console.developers.google.com开通google play developer api并获取请求api证书priket.p12文件: 交易状态API官方文档:https://developers.google.com/android-publisher/api-re

微信公众服务号开发

1 公司提供接口,微信平台可以通过访问我们的接口,进行数据交互,通过xml. 2 http://mp.weixin.qq.com/ 微信服务号平台,输入用户名密码进入高级功能-开发者模式, 3 申请服务号之后,微信会提供appid和AppSecret两个参数,根据这两个参数可以获取token,token作为你服务号和微信交互的证明. 4 接下里就是完成自己的接口部分的开发,然后接口的服务器配置地址输入微信服务号的服务器配置url,本地的项目地址是不行的,得是能访问的地址 5 接口的开发,接受xm

如何进行SuperMap iServer服务端开发

有时候在进行地图应用开发时,可能单纯的客户端无法满足要求(如功能和性能等因素),这时就需要进行iServer的服务端开发.SuperMap iServer 6R/7c的服务端开发步骤如下: 一.在Eclipse中新建一个Dynamic Web Project 此过程截图略去 二.引用iServer所需的各类jar包 经测试后发现,必须引用iServer/WEB-INF/lib下的所有jar包,否则运行时会出现一些奇怪的错误.这些jar包所在的位置为[iServer安装目录]\webapps\is

在线教学、视频会议软件 Webus Fox(2) 服务端开发手册

上次在<在线教学.视频会议软件 Webus Fox(1)文本.语音.视频聊天及电子白板基本用法>里介绍了软件的基本用法.本文主要介绍服务器端如何配置.开发. 1配置 1.1 IIS配置 Fox支持最基本的.net Framework4.0和IIS6/IIS7. 在IIS7中,对应应用程序池,需要配置为经典模式,支持.net4.0 1.2 web.config配置 Fox服务器端是暂时是host在IIS上,将来将Host在Windows Service上.对于IIS的配置,web.config是

微信公共服务平台开发(.Net的实现)1 认证“成为开发者”

http://www.cnblogs.com/freeliver54/p/3725979.html http://www.it165.net/pro/html/201402/9459.html 这些代码也就开始认证的时候用一次,以后就不用了: view sourceprint? 01.const string Token = "XXXXX";//你的token 02.protected void Page_Load(object sender, EventArgs e) 03.{ 04