还是接上文:订单查询 、我们获取到订单详情之后、接下来就要开始 退款接口了、在我们调运退款接口的时候、我们可以先调运订单查询接口、看订单状态是否正常或者订单是否存在,如果订单存在并且订单状态正常、那我们就可以调运退款借口了。
如下
退款API
业务功能
商户针对某一个已经成功支付的订单发起退款,操作结果在同一会话中同步返回。
一、退款方式
目前只支持原路返回退款
说明:退到银行卡则是非实时的,每个银行的处理速度不同,一般发起退款后1-3个工作日内到账。
同一笔单的部分退款需要设置相同的订单号和不同的 out_refund_no 。一笔退款失败后重新提交,要采用原来 的out_refund_no。总退款金额不能超过用户实际支付金额(现金券金额不能退款)
二、退款限制
商户在退款操作时应该注意退款限制,避免发起不会成功的退款请求,下面是主要的退款限制:
1.在平台中,只要退款累计金额不超过交易单支付总额,一笔交易单可以多次退款,退款申请单号(退款接口中有此参数)唯一确定一次退 款,而不是交易单号确定一次退款。退款申请单号由商户生成,所以商户一定要保证退款申请单的唯一性。商家在退款过程中要特别注 意,只有在能确定退款失败的情况下,才能重新发起另一笔退款。一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号。
2.请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次。
3.错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次。
4.每个支付订单的部分退款次数不能超过50次。
交互模式
后台系统调用交互模式
请求参数列表
请求url:https://pay.swiftpass.cn/pay/gateway
POST XML 内容体进行请求
字段名 | 变量名 | 必填 | 类型 | 说明 |
接口类型 | service | 是 | String(32) | 接口类型:unified.trade.refund |
版本号 | version | 否 | String(8) | 版本号,version默认值是1.0。 |
字符集 | charset | 否 | String(8) | 可选值 UTF-8 ,默认为 UTF-8。 |
签名方式 | sign_type | 否 | String(8) | 签名类型,取值:MD5默认:MD5 |
商户号 | mch_id | 是 | String(32) | 商户号,由平台分配 |
商户订单号 | out_trade_no | 否 | String(32) | 商户系统内部的订单号, out_trade_no和transaction_id至少一个必填,同时存在时transaction_id优先 |
平台订单号 | transaction_id | 否 | String(32) | 平台单号, out_trade_no和transaction_id至少一个必填,同时存在时transaction_id优先 |
商户退款单号 | out_refund_no | 是 | String(32) | 商户退款单号,32个字符内、可包含字母,确保在商户系统唯一。同个退款单号多次请求,平台当一个单处理,只会退一次款。如果出现退款不成功,请采用原退款单号重新发起,避免出现重复退款。 |
总金额 | total_fee | 是 | Int | 订单总金额,单位为分 |
退款金额 | refund_fee | 是 | Int | 退款总金额,单位为分,可以做部分退款 |
操作员 | op_user_id | 是 | String(32) | 操作员帐号,默认为商户号 |
退款渠道 | refund_channel | 否 | String(16) | ORIGINAL-原路退款,默认 |
随机字符串 | nonce_str | 是 | String(32) | 随机字符串,不长于 32 位 |
签名 | sign | 是 | String(32) | MD5签名结果,详见“安全规范” |
返回结果
数据按XML的格式实时返回
字段名 | 变量名 | 必填 | 类型 | 说明 |
版本号 | version | 是 | String(8) | 版本号,version默认值是2.0。 |
字符集 | charset | 是 | String(8) | 可选值 UTF-8 ,默认为 UTF-8。 |
签名方式 | sign_type | 是 | String(8) | 签名类型,取值:MD5默认:MD5 |
返回状态码 | status | 是 | String(16) | 0表示成功,非0表示失败此字段是通信标识,非交易标识,交易是否成功需要查看 result_code 来判断 |
返回信息 | message | 否 | String(128) | 异常或错误时返回信息,具体描述请看文档最后返回信息列表 |
以下字段在 status 为 0的时候有返回 | ||||
业务结果 | result_code | 是 | String(16) | 0表示成功,非0表示失败 注:此处返回0表示退款申请接收成功,实际的退款结果根据退款查询接口查询 |
商户号 | mch_id | 是 | String(32) | 商户号,由平台分配 |
设备号 | device_info | 否 | String(32) | 平台分配的终端设备号 |
随机字符串 | nonce_str | 是 | String(32) | 随机字符串,不长于 32 位 |
错误代码 | err_code | 否 | String(32) | 具体错误码请看文档最后错误码列表 |
签名 | sign | 是 | String(32) | MD5签名结果,详见“安全规范” |
以下字段在 status 和 result_code 都为 0的时候有返回 | ||||
平台订单号 | transaction_id | 是 | String(32) | 平台订单号。 |
商户订单号 | out_trade_no | 是 | String(32) | 商户系统内部的订单号 |
商户退款单号 | out_refund_no | 是 | String(32) | 商户退款单号 |
平台退款单号 | refund_id | 是 | String(32) | 平台退款单号 |
退款渠道 | refund_channel | 是 | String(16) | ORIGINAL—原路退款,默认 |
退款金额 | refund_fee | 是 | Int | 退款总金额,单位为分,可以做部分退款 |
现金券退款金额 | coupon_refund_fee | 否 | Int | 现金券退款金额 <= 退款金额, 退款金额-现金券退款金额为现金 |
由文档我们可以获取到我们需要传的参数和返回的参数、所以、最重要的、、突然想到、、要有一个自己的商户才好啊 ,,,, 不闲扯了
注意:文档中的必传参数
下面我们看demo
1 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
2 req.setCharacterEncoding("utf-8");
3 resp.setCharacterEncoding("utf-8");
4
5 SortedMap<String,String> map = XmlUtils.getParameterMap(req);
6 System.out.println(XmlUtils.toXml(map));
7 String key = SwiftpassConfig.key;
8 String res = null;
9 String reqUrl = SwiftpassConfig.req_url;
10 map.put("mch_id", SwiftpassConfig.mch_id);
11 map.put("op_user_id", SwiftpassConfig.mch_id);
12 map.put("nonce_str", String.valueOf(new Date().getTime()));
13
14 Map<String,String> params = SignUtils.paraFilter(map);
15 StringBuilder buf = new StringBuilder((params.size() +1) * 10);
16 SignUtils.buildPayParams(buf,params,false);
17 String preStr = buf.toString();
18 String sign = MD5.sign(preStr, "&key=" + key, "utf-8");
19 map.put("sign", sign);
20
21 System.out.println("reqUrl:" + reqUrl);
22
23 CloseableHttpResponse response = null;
24 CloseableHttpClient client = null;
25 try {
26 HttpPost httpPost = new HttpPost(reqUrl);
27 StringEntity entityParams = new StringEntity(XmlUtils.parseXML(map),"utf-8");
28 httpPost.setEntity(entityParams);
29 httpPost.setHeader("Content-Type", "text/xml;charset=ISO-8859-1");
30 client = HttpClients.createDefault();
31 response = client.execute(httpPost);
32 if(response != null && response.getEntity() != null){
33 Map<String,String> resultMap = XmlUtils.toMap(EntityUtils.toByteArray(response.getEntity()), "utf-8");
34 res = XmlUtils.toXml(resultMap);
35 System.out.println("请求结果:" + res);
36
37 if(resultMap.containsKey("sign") && !SignUtils.checkParam(resultMap, key)){
38 res = "验证签名不通过";
39 }
40 }else{
41 res = "操作失败!";
42 }
43 } catch (Exception e) {
44 e.printStackTrace();
45 res = "操作失败";
46 } finally {
47 if(response != null){
48 response.close();
49 }
50 if(client != null){
51 client.close();
52 }
53 }
54 if(res.startsWith("<")){
55 resp.setHeader("Content-type", "text/xml;charset=UTF-8");
56 }else{
57 resp.setHeader("Content-type", "text/html;charset=UTF-8");
58 }
59 resp.getWriter().write(res);
60 }
resultMap 这里面就是返回的各种数据 -- 上面文档中的返回数据
注意:1. 第5行的那个map 、那里面已经从req里面获取到了很多参数、比如service之类的、如果我们要开发的话、我们就要把那儿改一下、下面会讲到:
现在是我们本地的demo:
1 public Pair<Boolean, Map<String,String>> doOrderRefund(Payparams paramss) throws Exception {
2 boolean result = false;
3 Map<String,String> resultMap = new HashMap<String, String>();
4 HashMap<String, String> rspData = new HashMap<String,String>();
5 logger.info("---退款开始---");
6 String orderId = paramss.getOrderId();
7 String merId = paramss.getMerId();
8 String merOrderId = paramss.getMerOrderId();
9 String txnAmt = paramss.getTxnAmt();
10 String refundOrderId = "TK" + orderId; //退款单号 = TK + 订单号
11
12 //组织请求报文
13 SortedMap<String,String> map = new TreeMap<String, String>(); // 标志02 下面解释
14 System.out.println(XmlUtils.toXml(map));
15 String key = SwiftpassConfig.key;
16 String res = null;
17 String reqUrl = SwiftpassConfig.req_url;
18 map.put("out_trade_no",orderId); //订单号
19 map.put("out_refund_no", refundOrderId); //商户退单号
20 map.put("service", "unified.trade.refund"); //接口类型
21 map.put("mch_id", merId);
22 map.put("op_user_id", merId);
23 map.put("total_fee", txnAmt); //int 总金额 单位是分
24 map.put("refund_fee", txnAmt); //int 退款金额 单位是分
25
26 map.put("nonce_str", String.valueOf(new Date().getTime())); //随机字符串
27
28 Map<String,String> params = SignUtils.paraFilter(map);
29 StringBuilder buf = new StringBuilder((params.size() +1) * 10);
30 SignUtils.buildPayParams(buf,params,false);
31 String preStr = buf.toString();
32 String sign = MD5.sign(preStr, "&key=" + key, "utf-8");
33 map.put("sign", sign);
34
35 logger.info("reqUrl:" + reqUrl);
36
37 CloseableHttpResponse response = null;
38 CloseableHttpClient client = null;
39 try {
40 HttpPost httpPost = new HttpPost(reqUrl);
41 StringEntity entityParams = new StringEntity(XmlUtils.parseXML(map),"utf-8");
42 httpPost.setEntity(entityParams);
43 httpPost.setHeader("Content-Type", "text/xml;charset=ISO-8859-1");
44 client = HttpClients.createDefault();
45 response = client.execute(httpPost);
46 if(response != null && response.getEntity() != null){
47 resultMap = XmlUtils.toMap(EntityUtils.toByteArray(response.getEntity()), "utf-8");
48 res = XmlUtils.toXml(resultMap);
49 System.out.println("请求结果:" + res);
50
51 if(resultMap.containsKey("sign") && !SignUtils.checkParam(resultMap, key)){
52 res = "验证签名不通过";
53 }
54 }else{
55 res = "操作失败!";
56 }
57 } catch (Exception e) {
58 e.printStackTrace();
59 res = "操作失败";
60 } finally {
61 if(response != null){
62 response.close();
63 }
64 if(client != null){
65 client.close();
66 }
67 }
68
69 //转换成json对象
70 JSONObject respJson = JSONObject.fromObject(resultMap); //标志 03 下面解释
71 Assert.notNull(respJson, "微信退款返回信息解析 json 字串为空");
72 //解析json
73 Assert.notNull(respJson.get("result_code"), "微信退款返回码异常");
74 String respCode = respJson.getString("result_code");
75
76 PayRes payRes = PayReturnCode.getPayRes(Contents.WX_CODE, respCode);
77 logger.info("退款结果,返回码:" + respCode + ", 退款信息:" + payRes.getMsg());
78
79 if(respCode.equals("SUCCESS")){
80 result=true;
81 }
82
91 return new Pair<Boolean, Map<String,String>>(result, rspData);
92 }
注释:标识02 : 因为在demo示例中那个sortMap是将request的参数排序取出来了、所以在这里我们没有request的情况下、我们就new一个TreeMap(); 然后将下面红色的那部分参数传进来、放到map里面、请求微信的接口
标识03 : 我们获取到的是一个map<String , String> 类型的map、而微信的整个参数都在这个map中、我们在这里把map转换为json之后、就方便了我们下面对数据的处理。
今天在开发的时候、微信测试的商家都超过测试限额了、、、尴尬的啊、明天再说吧。