本文是【浅析微信支付】系列文章的第八篇,主要讲解商户如何处理微信申请退款、退款回调、查询退款接口,其中有一些坑的地方,会着重强调。php
浅析微信支付系列已经更新七篇了哟~,没有看过的朋友们能够看一下哦。html
浅析微信支付:查询订单和关闭订单java
浅析微信支付:统一下单接口github
在实际场景中,申请退款和退款回调接口是比较经常使用到的微信支付接口,这里咱们会讲原路返回
方式的退款,还有的是使用直接为用户付款到零钱
、现金红包
等方式来退款,此种状况主要会出如今客服退款时,不是所有退款的状况,也有的会出如今使用了微信代金券-单品券
的时候,由于单品券不能部分退款,因此只能走企业付款用户的方式,如下咱们主要讲原路返回
退款。算法
PS:原路返回的意思就是,从你支付时的关联支付单中扣款,微信会记录相关数据,能够在客户端通知中展现。小程序
如下为微信官方的申请退款
文档:api
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
当交易发生以后一段时间内,因为买家或者卖家的缘由须要退款时,卖家能够经过退款接口将支付款退还给买家,微信支付将在收到退款请求而且验证成功以后,按照退款规则将支付款按原路退到买家账号上。安全
注意: 一、交易时间超过一年的订单没法提交退款 二、微信支付退款支持单笔交易分屡次退款,屡次退款须要提交原支付订单的商户订单号和设置不一样的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后从新提交,请不要更换退款单号,请使用原商户退款单号 三、请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次 错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次 四、每一个支付订单的部分退款次数不能超过50次
PS:以上限制通常状况下不会出现,但咱们也必须写入系统异常场景处理中,请求频率可使用队列或增长延迟等方式来处理,部分退款此时不要超过微信的限制。微信
https://api.mch.weixin.qq.com/secapi/pay/refund
请求须要双向证书。
PS:关于微信证书,能够在 [商户平台-帐户中心-API安全] 去下载,此证书不少支付接口均须要使用,请将证书地址配置为常量,具体实现能够参考做者github源码。
先看源码,以下:
/** * [微信退款接口] - 保存调用的相关记录 * @param refundPayment 退款订单的支付记录 * @param tradePayment 历史付款单 * @return map * @throws Exception e * * @author yclimb * @date 2018/6/21 */ public Map<String,String> saveWxPayRefund(Payment refundPayment, Payment tradePayment) throws Exception { if (refundPayment == null || tradePayment == null) { return null; } // 微信订单号/商户订单号,必须传入其中一个,此处默认传入商户订单号 // 微信订单号,微信生成的订单号,在支付通知中有返回 // String transaction_id = null; // 商户订单号,商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下惟一。 String out_trade_no = tradePayment.getFlowNumer(); // 商户退款单号,商户系统内部的退款单号,商户系统内部惟一,只能是数字、大小写字母_-|*@ ,同一退款单号屡次请求只退一笔。 String out_refund_no = refundPayment.getFlowNumer(); // 订单总金额,传入参数单位为:元 String total_fee = String.valueOf(tradePayment.getAmount()); // 退款总金额,订单总金额,传入参数单位为:元 String refund_fee = String.valueOf(refundPayment.getAmount()); // 退款缘由,若商户传入,会在下发给用户的退款消息中体现退款缘由 String refund_desc = refundPayment.getBody(); // 微信支付对象 WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); // 微信退款接口 Map<String, String> resultMap = wxPay.refund(refundUrl, null, out_trade_no, out_refund_no, total_fee, refund_fee, refund_desc); logger.info("saveWxPayRefund:resultMap:" + resultMap.toString()); // 记录付款流水 // 下单失败,进行处理 if (WXPayConstants.FAIL.equals(resultMap.get(WXPayConstants.RETURN_CODE)) || WXPayConstants.FAIL.equals(resultMap.get(WXPayConstants.RESULT_CODE))) { // 处理结果返回,无需继续执行 resultMap.put(WXPayConstants.RESULT_CODE, WXPayConstants.FAIL); resultMap.put(WXPayConstants.ERR_CODE_DES, resultMap.get(WXPayConstants.RETURN_MSG)); return resultMap; } return resultMap; }
以上为sdk退款调用示例代码,有几个参数须要咱们注意:
字段名 | 变量名 | 必填 | 类型 | 描述 |
---|---|---|---|---|
微信订单号 | transaction_id | 是 | String(32) | 微信生成的订单号,在支付通知中有返回 |
商户订单号 | out_trade_no | 是 | String(32) | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_- |
商户退款单号 | out_refund_no | 是 | String(64) | 商户系统内部的退款单号,商户系统内部惟一,只能是数字、大小写字母_- |
退款金额 | refund_fee | 是 | Int | 退款总金额,订单总金额,单位为分,只能为整数 |
退款结果通知url | notify_url | 否 | String(256) | 异步接收微信支付退款结果通知的回调地址,通知URL必须为外网可访问的url,不容许带参数,若是参数中传了notify_url,则商户平台上配置的回调地址将不会生效。 |
PS:推荐以上的参数都必填,notify_url
参数可配置为环境常量,根据环境的不一样配置调用不会的回调地址。
下面为具体的实际sdkwxPay.refund
调用代码:
/** * 做用:申请退款<br> * 场景:当交易发生以后一段时间内,因为买家或者卖家的缘由须要退款时,卖家能够经过退款接口将支付款退还给买家, * 微信支付将在收到退款请求而且验证成功以后,按照退款规则将支付款按原路退到买家账号上。 * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_4 * * @param notify_url 回调地址 * @param transaction_id 微信生成的订单号,在支付通知中有返回 * @param out_trade_no 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下惟一。 * @param out_refund_no 商户系统内部的退款单号,商户系统内部惟一,只能是数字、大小写字母_-|*@ ,同一退款单号屡次请求只退一笔。 * @param total_fee 订单总金额,传入参数单位为:元 * @param refund_fee 退款总金额,订单总金额,传入参数单位为:元 * @param refund_desc 退款缘由,若商户传入,会在下发给用户的退款消息中体现退款缘由 * @return API返回数据 * @throws Exception e */ public Map<String, String> refund(String notify_url, String transaction_id, String out_trade_no, String out_refund_no, String total_fee, String refund_fee, String refund_desc) throws Exception { /** 构造请求参数数据 **/ Map<String, String> data = new HashMap<>(); // 变量名 字段名 必填 类型 示例值 描述 // 微信订单号 二选一 String(32) 1.21775E+27 微信生成的订单号,在支付通知中有返回 if (transaction_id != null) { data.put("transaction_id", transaction_id); } // 商户订单号 String(32) 1.21775E+27 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下惟一。 data.put("out_trade_no", out_trade_no); // 商户退款单号 是 String(64) 1.21775E+27 商户系统内部的退款单号,商户系统内部惟一,只能是数字、大小写字母_-|*@ ,同一退款单号屡次请求只退一笔。 data.put("out_refund_no", out_refund_no); // 订单金额 是 Int 100 订单总金额,单位为分,只能为整数,详见支付金额 data.put("total_fee", String.valueOf(new BigDecimal(total_fee).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue())); // 退款金额 是 Int 100 退款总金额,订单总金额,单位为分,只能为整数,详见支付金额 // 默认单位为分,系统是元,因此须要*100 data.put("refund_fee", String.valueOf(new BigDecimal(refund_fee).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue())); // 退款缘由 否 String(80) 商品已售完 若商户传入,会在下发给用户的退款消息中体现退款缘由 data.put("refund_desc", refund_desc); // 货币种类 否 String(8) CNY 货币类型,符合ISO 4217标准的三位字母代码,默认人民币:CNY,其余值列表详见货币类型 data.put("refund_fee_type", WXPayConstants.FEE_TYPE_CNY); // 退款结果通知url 否 String(256) https://weixin.qq.com/notify/ 异步接收微信支付退款结果通知的回调地址,通知URL必须为外网可访问的url,不容许带参数,若是参数中传了notify_url,则商户平台上配置的回调地址将不会生效。 data.put("notify_url", notify_url); /** 如下参数为非必填参数 **/ // 退款资金来源 否 String(30) REFUND_SOURCE_RECHARGE_FUNDS 仅针对老资金流商户使用;REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款);REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款 // data.put("refund_account", null); /** 如下五个参数,在 this.fillRequestData 方法中会自动赋值 **/ /*// 小程序ID appid 是 String(32) wxd678efh567hg6787 微信分配的小程序ID data.put("appid", WXPayConstants.APP_ID); // 商户号 mch_id 是 String(32) 1230000109 微信支付分配的商户号 data.put("mch_id", WXPayConstants.MCH_ID); // 随机字符串 nonce_str 是 String(32) 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 随机字符串,长度要求在32位之内。推荐随机数生成算法 data.put("nonce_str", nonce_str); // 签名类型 sign_type 否 String(32) MD5 签名类型,默认为MD5,支持HMAC-SHA256和MD5。 data.put("sign_type", WXPayConstants.MD5); // 签名 sign 是 String(32) C380BEC2BFD727A4B6845133519F3AD6 经过签名算法计算得出的签名值,详见签名生成算法 data.put("sign", sign);*/ // 微信退款接口 Map<String, String> resultMap = this.refund(data); WXPayUtil.getLogger().info("wxPay.refund:" + resultMap); return resultMap; }
以上已经详细说明的具体的字段含义,有不明白的同窗能够查看微信的官方文档,具体的源码能够查看做者的github。
这里有一个比较须要注意的点,在咱们调用退款以后,会返回一些异常处理状况,官方文档中收录了一系列错误码code,咱们能够在系统中对其进行处理,这里就不细说了。
如下为微信官方的退款结果通知
文档:
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=10
当商户申请的退款有结果后,微信会把相关结果发送给商户,商户须要接收处理,并返回应答。
对后台通知交互时,若是微信收到商户的应答不是成功或超时,微信认为通知失败,微信会经过必定的策略按期从新发起通知,尽量提升通知的成功率,但微信不保证通知最终能成功。
(通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)
注意:一样的通知可能会屡次发送给商户系统。商户系统必须可以正确处理重复的通知。
推荐的作法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,若是没有处理过再进行处理,若是处理过直接返回结果成功。在对业务数据进行状态检查和处理以前,要采用数据锁进行并发控制,以免函数重入形成的数据混乱。
特别说明:退款结果对重要的数据进行了加密,商户须要用商户秘钥进行解密后才能得到结果通知的内容
在申请退款接口中上传参数“notify_url”以开通该功能
若是连接没法访问,商户将没法接收到微信通知。
通知url必须为直接可访问的url,不能携带参数。
示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”
解密步骤以下: (1)对加密串A作base64解码,获得加密串B (2)对商户key作md5,获得32位小写key* ( key设置路径:微信商户平台-->帐户设置-->API安全-->密钥设置 ) (3)用key*对加密串B作AES-256-ECB解密(PKCS7Padding)
PS:特别注意,若是要进行微信AES解密,由于GJ的进口管制限制,Java发布的运行环境包中的加解密有必定的限制。默认不容许256位密钥的AES加解密,解决方法就是修改策略文件,咱们须要从官方网站下载无限制权限策略文件,注意本身JDK的版本别下错了。
jdk8的jce下载地址:https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
将local_policy.jar和US_export_policy.jar这两个文件替换%JRE_HOME%\lib\security和%JDK_HOME%\jre\lib\security下原来的文件,注意先备份原文件。
若是是jdk8,可能会遇到安全目录下有policy
文件夹的状况,拿做者的电脑举例,jdk路径为/opt/jdk1.8.0_152/jre/lib/security/policy
,此目录下有两个子文件夹limited
、limited
,须要替换limited
文件夹下的文件 local_policy.jar
、US_export_policy.jar
这两个,最好先备份哦~!!!替换后重启项目便可。
由于退款回调接口是咋们系统被动接收微信的消息,因此此处和支付回调接口一致,也是使用了流的方式,格式为xml,下面咱们来看代码:
/** * 退款结果通知 * <p> * 在申请退款接口中上传参数“notify_url”以开通该功能 * 若是连接没法访问,商户将没法接收到微信通知。 * 通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action” * <p> * 当商户申请的退款有结果后,微信会把相关结果发送给商户,商户须要接收处理,并返回应答。 * 对后台通知交互时,若是微信收到商户的应答不是成功或超时,微信认为通知失败,微信会经过必定的策略按期从新发起通知,尽量提升通知的成功率,但微信不保证通知最终能成功。 * (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒) * 注意:一样的通知可能会屡次发送给商户系统。商户系统必须可以正确处理重复的通知。 * 推荐的作法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,若是没有处理过再进行处理,若是处理过直接返回结果成功。在对业务数据进行状态检查和处理以前,要采用数据锁进行并发控制,以免函数重入形成的数据混乱。 * 特别说明:退款结果对重要的数据进行了加密,商户须要用商户秘钥进行解密后才能得到结果通知的内容 * @param request req * @param response resp * @return res xml * * @author yclimb * @date 2018/6/21 */ @ApiOperation(value = "微信支付|微信退款回调接口", httpMethod = "POST", notes = "该连接是经过【微信退款API】中提交的参数notify_url设置,若是参数中传了notify_url,则商户平台上配置的回调地址将不会生效。") @RequestMapping("/refund") public void refund(HttpServletRequest request, HttpServletResponse response) { String resXml = ""; InputStream inStream; try { inStream = request.getInputStream(); ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while ((len = inStream.read(buffer)) != -1) { outSteam.write(buffer, 0, len); } WXPayUtil.getLogger().info("refund:微信退款----start----"); // 获取微信调用咱们notify_url的返回信息 String result = new String(outSteam.toByteArray(), "utf-8"); WXPayUtil.getLogger().info("refund:微信退款----result----=" + result); // 关闭流 outSteam.close(); inStream.close(); // xml转换为map Map<String, String> map = WXPayUtil.xmlToMap(result); if (WXPayConstants.SUCCESS.equalsIgnoreCase(map.get(WXPayConstants.RETURN_CODE))) { WXPayUtil.getLogger().info("refund:微信退款----返回成功"); /** 如下字段在return_code为SUCCESS的时候有返回: **/ // 加密信息:加密信息请用商户秘钥进行解密,详看法密方式 String req_info = map.get("req_info"); /** * 解密方式 * 解密步骤以下: * (1)对加密串A作base64解码,获得加密串B * (2)对商户key作md5,获得32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->帐户设置-->API安全-->密钥设置 ) * (3)用key*对加密串B作AES-256-ECB解密(PKCS7Padding) */ String resultStr = AESUtil.decryptData(req_info); // WXPayUtil.getLogger().info("refund:解密后的字符串:" + resultStr); Map<String, String> aesMap = WXPayUtil.xmlToMap(resultStr); /** 如下为返回的加密字段: **/ // 商户退款单号 是 String(64) 1.21775E+27 商户退款单号 String out_refund_no = aesMap.get("out_refund_no"); // 退款状态 是 String(16) SUCCESS SUCCESS-退款成功、CHANGE-退款异常、REFUNDCLOSE—退款关闭 String refund_status = aesMap.get("refund_status"); // 商户订单号 是 String(32) 1.21775E+27 商户系统内部的订单号 String out_trade_no = aesMap.get("out_trade_no"); /*// 微信订单号 是 String(32) 1.21775E+27 微信订单号 String transaction_id = null; // 微信退款单号 是 String(32) 1.21775E+27 微信退款单号 String refund_id = null; // 订单金额 是 Int 100 订单总金额,单位为分,只能为整数,详见支付金额 String total_fee = null; // 应结订单金额 否 Int 100 当该订单有使用非充值券时,返回此字段。应结订单金额=订单金额-非充值代金券金额,应结订单金额<=订单金额。 String settlement_total_fee = null; // 申请退款金额 是 Int 100 退款总金额,单位为分 String refund_fee = null; // 退款金额 是 Int 100 退款金额=申请退款金额-非充值代金券退款金额,退款金额<=申请退款金额 String settlement_refund_fee = null;*/ // 退款是否成功 if (!WXPayConstants.SUCCESS.equals(refund_status)) { resXml = resFailXml; } else { // 通知微信.异步确认成功.必写.否则会一直通知后台.八次以后就认为交易失败了. resXml = resSuccessXml; isSuccess = true; } // 根据付款单号查询付款记录 out_refund_no // 付款记录修改 & 记录付款日志 if (payment != null) { WXPayUtil.getLogger().error("refund:微信支付回调:修改支付单"); } else { WXPayUtil.getLogger().error("refund:微信支付回调:找不到对应的支付单"); } } else { WXPayUtil.getLogger().error("refund:支付失败,错误信息:" + map.get(WXPayConstants.RETURN_MSG)); resXml = resFailXml; } } catch (Exception e) { WXPayUtil.getLogger().error("refund:微信退款回调发布异常:", e); } finally { try { // 处理业务完毕 BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()); out.write(resXml.getBytes()); out.flush(); out.close(); } catch (IOException e) { WXPayUtil.getLogger().error("refund:微信退款回调发布异常:out:", e); } } }
以上代码详细解释了如何接收微信回调数据和解码数据,具体的AESUtil.decryptData(req_info)
请参考做者源码,文末有地址,这里就不细讲了。
具体的退款接收参数请参考微信官方文档,须要注意的是商户退款单号
和微信退款单号
,此两个参数是修改和记录退款的必要凭证。
如下为微信官方的查询退款
文档:
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_5
提交退款申请后,经过调用该接口查询退款状态。退款有必定延时,用零钱支付的退款20分钟内到帐,银行卡支付的退款3个工做往后从新查询退款状态。
注意:若是单个支付订单部分退款次数超过20次请使用退款单号查询
https://api.mch.weixin.qq.com/pay/refundquery
不须要
注意:当一个订单部分退款超过10笔后,商户用微信订单号或商户订单号调退款查询API查询退款时,默认返回前10笔和total_refund_count
(订单总退款次数)。商户须要查询同一订单下超过10笔的退款单时,可传入订单号及offset来查询,微信支付会返回offset及后面的10笔,以此类推。当商户传入的offset超过total_refund_count
,则系统会返回报错PARAM_ERROR
。
举例:
一笔订单下的退款单有36笔,当商户想查询第25笔时,可传入订单号及offset=24,微信支付平台会返回第25笔到第35笔的退款单信息,或商户可直接传入退款单号查询退款
如下为调用方式:
private void doRefundQuery() { // 四选一,微信订单号查询的优先级是: refund_id > out_refund_no > transaction_id > out_trade_no HashMap<String, String> data = new HashMap<String, String>(); // 商户订单号 data.put("out_trade_no", out_trade_no); // 微信订单号 data.put("transaction_id", out_trade_no); // 商户退款单号 data.put("out_refund_no", out_trade_no); // 微信退款单号 data.put("refund_id", out_trade_no); try { Map<String, String> r = wxpay.refundQuery(data); System.out.println(r); } catch (Exception e) { e.printStackTrace(); } }
PS:微信订单号查询的优先级是: refund_id > out_refund_no > transaction_id > out_trade_no
须要注意的是,查询退款时,须要注意退款返回的错误码,若是出现错误,须要及时同步商户系统中的退款数据。
以上为申请退款、退款回调接口、查询退款
相关的解释和源码,特别须要注意的是接收退款时的解密方式和替换安全文件,小伙伴们必定要注意哦,具体的源码能够看做者的github,里面对每一个方法有详细的注释。
预告:下一篇文章 下载对帐单和资金帐单
,敬请期待!!!
若是想要提早一览源码的小伙伴,能够先看看个人 github,地址以下:
https://github.com/YClimb/wxpay-sdk/blob/master/README.md
加做者私人微信,做者微信号以下 yclimb
,标明 微信支付
可拉入微信支付讨论群与小伙伴一块儿探讨哦,必定要标明 微信支付
哦~
到此本文就结束了,关注公众号查看更多推送!!!