还是接上文:订单查询 、我们获取到订单详情之后、接下来就要开始 退款接口了、在我们调运退款接口的时候、我们可以先调运订单查询接口、看订单状态是否正常或者订单是否存在,如果订单存在并且订单状态正常、那我们就可以调运退款借口了。
业务功能
商户针对某一个已经成功支付的订单发起退款,操作结果在同一会话中同步返回。
一、退款方式
目前只支持原路返回退款
说明:退到银行卡则是非实时的,每个银行的处理速度不同,一般发起退款后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 | 现金券退款金额 <= 退款金额, 退款金额-现金券退款金额为现金 |
由文档我们可以获取到我们需要传的参数和返回的参数、所以、最重要的、、突然想到、、要有一个自己的商户才好啊 ,,,, 不闲扯了
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 }
demo中直接是把返回结果打印在了页面、response掉了、所以、我们需要拿到那个 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 }