摘要:前言最近应公司业务需求,把微信支付完成了,当然已经顺利上线。第三步查询订单该接口提供所有微信支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。
前言
最近应公司业务需求,把微信支付完成了,当然已经顺利上线。但是开发的过程是也是踩了很多坑,下面我就先说说开发流程,以及在开发中遇到的大大小小的坑。
开发流程首先,看一下微信开方平台关于支付的一个时序图,如下:
微信支付时序图
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 SortedMapprepareOrder(String ip, String orderId, int price) { Map oparams = ImmutableMap. 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
wx2421b1c4370ec43b 支付测试 APP支付测试10000100 1add1a30ac87aa2db72f57a2375d8fec http://wxpay.weixin.qq.com/pub_v2/pay/notify.v2.php 1415659990 14.23.150.211 1 APP 0CB01533B8C1EF103065174F50BCA001
请求统一下单地址 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 SortedMapbuildClientJson( Map 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 params = ImmutableMap. 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 sortMap = MapUtils.sortMap(params); sortMap.put("package", "Sign=WXPay"); // paySign的生成规则和Sign的生成规则同理 String paySign = PayCommonUtil.createSign("UTF-8", sortMap); sortMap.put("sign", paySign); return sortMap; }
整个统一下订单的逻辑就完成了。这里小结一下:
请求参数需要按照参数的key进行字母的ASCII码进行排序,由于我使用的是map数据结构,这里提供一个对map集合中的key元素进行排序的工具类
/** * 对map根据key进行排序 ASCII 顺序 * * @param 无序的map * @return */ public static SortedMapsortMap(Map map) { List > infoIds = new ArrayList >( map.entrySet()); // 排序 Collections.sort(infoIds, new Comparator >() { public int compare(Map.Entry o1, Map.Entry o2) { return (o1.getKey()).toString().compareTo(o2.getKey()); } }); SortedMap sortmap = new TreeMap (); for (int i = 0; i < infoIds.size(); i++) { String[] split = infoIds.get(i).toString().split("="); sortmap.put(split[0], split[1]); } return sortmap; }
对排序后的数据进行MD5签名,微信服务端会进行校验,防止数据在网络传输过程中被篡改。
拿到微信响应的数据,首先要做的事,也是对获取的数据进行签名校验,理由同上。
需要注意的一点,返回给app客户端的数据的key一定是小写,这点微信的api是没有说明白的,之前和客户端联调时耽误了很多时间,这也是微信支付被很多开发者吐槽的地方
api比较难用^-^
注意小细节:返回给客户端时时间戳要是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); Mapmap = 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"); }
小结:
当在本地做开发时,微信回调是不方便的,这里提供一种比较快速的方法,不过前提是有云服务器。用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 SortedMapprepareQueryData(String outTradeNo, String transactionId) { Map queryParams = null; // 微信的订单号,优先使用 if (null == outTradeNo || outTradeNo.length() == 0) { queryParams = ImmutableMap. builder() .put("appid", ConfigUtil.APPID) .put("mch_id", ConfigUtil.MCH_ID) .put("transaction_id", transactionId) .put("nonce_str", PayCommonUtil.CreateNoncestr()).build(); } else { queryParams = ImmutableMap. 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 sortMap = MapUtils.sortMap(queryParams); // 签名 String createSign = PayCommonUtil.createSign("UTF-8", sortMap); sortMap.put("sign", createSign); return sortMap; }
下一步对微信响应的数据进行解析,检查支付的状态
代码如下
/** * 查询订单状态 * * @param params * 订单查询参数 * @return */ public HttpResultcheckOrderStatus(SortedMap 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 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 Github thanks !
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/64855.html
摘要:相比起来,支付宝的下单动作由于是在前端调用的,因此,站点需要将自己的订单信息返回到客户端,然后又客户端发起调用支付宝的下单接口,这样一来,如果安全加密等做的不到位,很容易被恶意用户篡改信息。 作为一个具备用户交易能力的网站,丰富它的支付渠道对于获客和提高日活都有不可估量的积极作用。算起来,我接触过的支付系统也有几十个了,在这里总结一下我所接触过的支付系统对外接口的设计方案。 1. 支付...
摘要:附微信支付流程微信支付流程和小程序的支付流程基本一致,需要注意两点需要在微信商户平台配置支付目录,只有跳转到了支付目录的地址,才能发起微信支付。 我所在公司需要开发一款商城小程序,里面需要用到微信支付,我负责里面的下单功能,从小程序端到后台的支付流程都是我自己开发的,由于我们组没有人有开发微信支付的经验,很多东西都还不怎么明白,但是没办法,只能我自己琢磨,写完之后总感觉有bug,但是不...
摘要:即日起至月日公测活动期间,成功参与新版公测活动并接入支付宝小程序的用户,可获得个人版套餐个月价值元的免费使用资格。计划的第一站我们选择了支付宝小程序。支付宝以及其他平台的小程序,在这个时代里,更加需要无服务器的开发方式。 作为国内首家专注于小程序领域的后端云服务,知晓云正式开启 3.0 计划——全平台 Serverless 服务。 「知晓云」cloud.minapp.com,诞生于 2...
阅读 2744·2021-11-19 09:40
阅读 5293·2021-09-27 14:10
阅读 2098·2021-09-04 16:45
阅读 1461·2021-07-25 21:37
阅读 2993·2019-08-30 10:57
阅读 2980·2019-08-28 17:59
阅读 1054·2019-08-26 13:46
阅读 1406·2019-08-26 13:27