原创 Dr Hydra 码农参上 2020-11-29 11:00

收录于合集#微信开发技术3个

在上一篇中我们介绍了微信小程序的支付流程,这一篇接着讲一下小程序的退款流程,首先看一下官方给出的介绍:

当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。

和付款流程不同,退款流程不再需要在前端页面额外调用微信接口,可由后端独立完成。可分为以下3步:

  • 服务后端发送退款请求

  • 接收微信同步返回结果

  • 接收微信调用回调接口返回异步消息

1

调用微信退款接口

生成退款参数及发送请求方法如下,和付款的统一支付接口相同,首先需要对请求中的参数进行签名,之后再发送http请求:

public String refund(String orderNumber, String refundNumber, double totalFee, double refundFee, String notifyUrl) {    int totalMoney = new Double(Math.ceil(totalFee * 100)).intValue();   //转换为分    int refundMoney = new Double(Math.ceil(refundFee * 100)).intValue();   //转换为分
    Map<String, String> wxMap = new HashMap<>();    wxMap.put("appid", mpCommonProperty.getAppid());    wxMap.put("mch_id", mpCommonProperty.getMuchId());    wxMap.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", ""));    wxMap.put("notify_url", mpCommonProperty.getServerDomain() + notifyUrl);    wxMap.put("out_refund_no", refundNumber);    wxMap.put("out_trade_no", orderNumber);    wxMap.put("refund_fee", String.valueOf(refundMoney));    wxMap.put("total_fee", String.valueOf(totalMoney));    wxMap.put("sign", mpPayUtil.signCommon(wxMap));
    String refundXml = XmlUtil.generateXmlFromMap(wxMap);    String url = https://api.mch.weixin.qq.com/secapi/pay/refund";
    String xmlResult = null;    try {        xmlResult = mpCertificateUtil.doWxpayRequest(url, refundXml);    } catch (Exception e) {        e.printStackTrace();    }    log.info("xmlResult:" + xmlResult);    return xmlResult;}

参数说明:

orderNumber:需要执行退款的订单号

refundNumber:业务系统生成的退款单号

totalFee:订单总金额,如果业务系统单位为元,需要在发送请求前转化为分

refundFee:本次退款金额,同上

notifyUrl:接收通知的回调接口地址

微信退款支持一笔订单分多次退款,上面的方法可以用于执行部分退款操作,如果是执行一次性全部退款的话,那么可以重载上面的方法,减少传入的参数:

public String refund(String orderNumber,double totalFee,String notifyUrl){    return refund(orderNumber,orderNumber,totalFee,totalFee,notifyUrl);}

需要注意,和付款发送请求不同的是这里不能直接发起http请求,需要使用微信商户平台生成的证书。证书的申请流程也不复杂,登录商户平台,在账户中心点击申请API证书,下载证书工具后通过验证商户信息可以自动生成。在生成完pkcs12证书后,在每次发送退款请求时需要携带证书的信息。

下面是证书工具类,提供加载证书及发送携带证书的请求功能:

public class MPCertificateUtil {    @Autowired    MPCommonProperty mpCommonProperty;    /**     * 加载证书文件流,通过hex解析为16进制存到静态变量里    */    public String parseCertificateFile(){        String haxString=null;        try {            ClassPathResource classPathResource=new ClassPathResource(mpCommonProperty.getCertFilePath());            InputStream inputStream=classPathResource.getStream();            haxString = Hex.encodeHexString(StreamUtils.copyToByteArray(inputStream));        } catch (Exception e) {            log.error("fileError:"+e.getMessage());            e.printStackTrace();        }        return haxString;    }
    /**     * 发送微信携带证书请求    */    public String doWxpayRequest(String httpurl, String strxml) throws Exception {        String cert = parseCertificateFile();        if(StringUtils.isEmpty(cert)){            throw new RuntimeException("cert is null");        }
        CloseableHttpClient client = null;        HttpPost httpPost = null;        try {            // 解密出16进制原证书文件内容为字节数组            byte[] bytes = Hex.decodeHex(cert.toCharArray());            ByteArrayInputStream input = new ByteArrayInputStream(bytes);            KeyStore clientTrustKeyStore = KeyStore.getInstance("PKCS12");            clientTrustKeyStore.load(input, mpCommonProperty.getMuchId().toCharArray());            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());            kmf.init(clientTrustKeyStore, mpCommonProperty.getMuchId().toCharArray());            TrustManager[] tm = {new MyX509TrustManager()};            SSLContext sslContext = SSLContext.getInstance("TLSv1");            sslContext.init(kmf.getKeyManagers(), tm, new java.security.SecureRandom());            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);            client = HttpClients.custom().setSSLSocketFactory(sslsf).build();
            httpPost = new HttpPost(httpurl);            httpPost.setEntity(new StringEntity(strxml, "utf-8"));
            CloseableHttpResponse response = client.execute(httpPost);            StatusLine statusLine = response.getStatusLine();            HttpEntity entity = response.getEntity();            if (statusLine.getStatusCode() == 200) {                return EntityUtils.toString(entity, "utf-8");            }        } catch (Exception e) {            e.printStackTrace();            throw e;        } finally {            if (client != null) {                client.close();            }        }        return null;    }}

在上面的代码中,读取了项目目录下的pkcs12证书文件,但是微信在官方文档中更加推荐不要将证书放在web服务器的虚拟目录下,通过放在有权限控制的目录中,防止被他人下载。

2

接收同步返回结果

在发送完携带证书的http请求后,同步返回结果格式如下所示,接收到下面的信息后可以根据业务需求存入数据库中进行备案:

<xml>   <return_code><![CDATA[SUCCESS]]></return_code>   <return_msg><![CDATA[OK]]></return_msg>   <appid><![CDATA[wx2421b1c4370ec43b]]></appid>   <mch_id><![CDATA[10000100]]></mch_id>   <nonce_str><![CDATA[NfsMFbUFpdbEhPXP]]></nonce_str>   <sign><![CDATA[B7274EB9F8925EB93100DD2085FA56C0]]></sign>   <result_code><![CDATA[SUCCESS]]></result_code>   <transaction_id><![CDATA[1008450740201411110005820873]]></transaction_id>   <out_trade_no><![CDATA[1415757673]]></out_trade_no>   <out_refund_no><![CDATA[1415701182]]></out_refund_no>   <refund_id><![CDATA[2008450740201411110000174436]]></refund_id>   <refund_channel><![CDATA[]]></refund_channel>   <refund_fee>1</refund_fee></xml>

注意返回结果中的return_code为SUCCESS时,只表示退款申请被微信服务器接收成功,并不是退款执行成功,退款的结果会在回调接口中被返回。

3

接收异步通知结果

在退款执行成功或因某种原因执行失败后,微信会调用之前在发起请求时我们填写的回调接口地址,会把退款的结果以异步通知的形式发送给我们:

@PostMapping("refundFallBack")public void refundFallBack(HttpServletRequest request,HttpServletResponse response) throws IOException {    StringBuilder sb = new StringBuilder();    BufferedReader reader = null;    try (InputStream inputStream = request.getInputStream()) {        reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));        String line = "";        while ((line = reader.readLine()) != null) {            sb.append(line);        }    } catch (IOException e) {        log.error("getBodyString错误!");    } finally {        if (reader != null) {            try {                reader.close();            } catch (IOException e) {                log.error(ExceptionUtils.getMessage(e));            }        }    }        String resultXml;    String bodyXml = sb.toString();    Map<String, String> xmlResult = XmlUtil.parseXmlToMap(bodyXml);    if ("SUCCESS".equals(xmlResult.get("return_code"))) {        String reqInfo = xmlResult.get("req_info");        byte[] decode = Base64.decode(reqInfo);        String md5Hash = Md5Utils.hash(WechatConstants.muchSecret);        try {            //AES解密            String result = AESUtils.decryptData(decode, md5Hash);            Map<String, String> resultMap = XmlUtil.parseXmlToMap(result);            //执行业务逻辑...            } catch (Exception e) {              log.error(e.getMessage());            }        resultXml=WechatConstants.FALLBACK_SUCCESS_XML;    }else{        resultXml= WechatConstants.FALLBACK_FAIL_XML;    }    ServletOutputStream outputStream = response.getOutputStream();    outputStream.println(resultXml);    outputStream.close();}

可以看出,异步返回通知并不能够被直接拿来解析使用,在使用过程中还进行了一次解密,这是因为返回的报文格式如下:

<xml><return_code>SUCCESS</return_code>   <appid><![CDATA[wx2421b1c4370ec43b]]></appid>   <mch_id><![CDATA[10000100]]></mch_id>   <nonce_str><![CDATA[TeqClE3i0mvn3DrK]]></nonce_str>   <req_info><![CDATA[T87GAHG17TGAHG1TGHAHAHA1Y1CIOA9UGJH1GAHV871HAGAGQYQQPOOJMXNBCXBVNMNMAJAA]]></req_info></xml>

其中req_info为加密信息,需要对其进行解密,解密步骤如下:

  • 对加密串A做base64解码,得到加密串B

  • 对商户key做md5加密,得到32位小写key*

  • 用key*对加密串B做AES-256-ECB解密

AES解密工具类实现如下:

import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;import java.security.Security;public class AESUtils {    public static String decryptData(byte[] base64Data,String md5Hash) throws Exception {        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());        //加解密算法/工作模式/填充方式        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");        SecretKeySpec key = new SecretKeySpec(md5Hash.toLowerCase().getBytes(), "AES");        cipher.init(Cipher.DECRYPT_MODE, key);        return new String(cipher.doFinal(base64Data), "UTF-8");    }}

解密完成后,得到真正包含退款信息的xml报文:

<root>  <out_refund_no><![CDATA[131811191610442717309]]></out_refund_no>  <out_trade_no><![CDATA[71106718111915575302817]]></out_trade_no>  <refund_account><![CDATA[REFUND_SOURCE_RECHARGE_FUNDS]]></refund_account>  <refund_fee><![CDATA[3960]]></refund_fee>  <refund_id><![CDATA[50000408942018111907145868882]]></refund_id>  <refund_recv_accout><![CDATA[支付用户零钱]]></refund_recv_accout>  <refund_request_source><![CDATA[API]]></refund_request_source>  <refund_status><![CDATA[SUCCESS]]></refund_status>  <settlement_refund_fee><![CDATA[3960]]></settlement_refund_fee>  <settlement_total_fee><![CDATA[3960]]></settlement_total_fee>  <success_time><![CDATA[2018-11-19 16:24:13]]></success_time>  <total_fee><![CDATA[3960]]></total_fee>  <transaction_id><![CDATA[4200000215201811190261405420]]></transaction_id></root>

再对上面的报文进行解析,执行业务系统中对退款流程的后续处理即可。同样,我们需要按照微信规定的格式返回接收成功的报文,并在每次处理前验证通知消息的幂等性。

在退款中还踩到了一个坑,如果在微信的商户平台中,开启了自动提现功能,那么会自动将基本账户内的资金全提现至结算银行卡中,隔日到账。这样如果被结算过的订单在被退款时商户平台也没有基本余额,就会报错提示“基本余额不足,请充值后重新发起退款”,所以最好先关闭自动提现,待订单超过退款周期后再对其进行结算,避免发起退款不成功的情况。

微信小程序退款流程详解相关推荐

  1. 微信小程序退款功能(详解完整)

    微信小程序支付->退款 微信小程序退款的时候如果是线上,就会涉及到Linux读取打包后项目存放文件路径失败问题,获取不到其中的微信退款证书,在这里就需要使用流的方式进行读取路径,经大佬指点才最终 ...

  2. 【迷宫】地下迷宫游戏-微信小程序开发流程详解

    可曾记得,小时候上学路边买的透明铅笔盒,里面内嵌了一个小球,它用重力可从起点滚动到终点,对小朋友来说是感觉有趣的,在这个游戏的基础上,弄一款微信小程序的迷宫探索游戏试试,在不同关卡的迷宫中解开机关与谜 ...

  3. 微信小程序支付流程详解

    原创 Dr Hydra 码农参上 2020-11-22 11:00 收录于合集#微信开发技术3个 最近在工作中接入了一下微信小程序支付的功能,虽然说官方文档已经比较详细了,但在使用过程中还是踩了不少的 ...

  4. Python-Flask微信小程序登录流程详解及后台实现

    文章目录 登录流程图及个人理解 登录接口源码 登录流程图及个人理解 1.前端将由wx.login()方法获取到的用户临时登录凭证code(只能使用一次)传给后台服务器(即登录接口) 2.后台利用微信小 ...

  5. 【数独】数独游戏-微信小程序开发流程详解

    有没有玩过数独游戏呢,听说,它是一个能训练大脑思维的棋盘类游戏,游戏规则很简单,通过小程序来实现很容易,非常适合对数独游戏逻辑感兴趣的同学,选择它开发入门吧. 准备 会使用微信开发者工具, 有Java ...

  6. 【答题】在线答卷-答题系统的微信小程序开发流程详解

    用死记硬背的方法学习的学生,面对桌上堆积成厚厚的书本,是否感觉鸭梨山大呢,想着教育却面临着学习成本不小问题,是否感觉各种不便呢,如果对编程代码有感兴趣,不妨试试做一个自己的在线答题系统,这里可以用微信 ...

  7. 【拼图】拼图游戏-微信小程序开发流程详解

    还记得小时候玩过的经典拼图游戏吗,上小学时,在路边摊用买个玩具,是一个正方形盒子形状,里面装的是图片分割成的很多块,还差一块,怎么描述好呢,和魔方玩具差不多,有没有听说叫二维的魔方,这里用小程序把它实 ...

  8. 【飞行棋】多人游戏-微信小程序开发流程详解

    可曾记得小时候玩过的飞行棋游戏,是90后的都有玩过吧,现在重温一下,这是一个可以二到四个人参与的游戏,通过投骰子走棋,一开始靠运气,后面还靠自己选择,谁抢占先机才能赢,还可以和小伙伴们一起玩,狭路相逢 ...

  9. 【跳棋】跳棋游戏-多人游戏-微信小程序开发流程详解

    看到跳棋游戏,一个2到6人可一起玩的游戏,于是联想起,自己上小学时候陪同学们玩过的弹珠游戏,是不是跟跳棋游戏很像呢,看了跳棋玩法,有兴趣就研究,这里把跳棋游戏给弄出来了,想知道地图怎么画,对此感兴趣的 ...

最新文章

  1. Struts2 自己定义下拉框标签Tag
  2. 用Gvim建立IDE编程环境 (Windows篇)
  3. windows配置solr5.5.2(不通过tomcat,使用内置jetty)
  4. kalman filter卡尔曼滤波器- 数学推导和原理理解-----网上讲的比较好的kalman filter和整理、将预测值和观测值融和...
  5. 如何自己写xuetr(一) 每次改变的驱动名和服务名
  6. android 倒计时 动画下载,倒计时器app下载-倒计时器安卓最新版-幻想游戏网
  7. 在eclipse中引入mybatis和spring的约束文件
  8. Struts2详细执行流程自己总结
  9. java jsp聊天系统_jsp 在线客服聊天源码(websocket)
  10. 制定可用性测试计划(1)
  11. 【指标异动】贡献度定量归因之法
  12. 黄小宁罪大恶极!!!!!!!!!!黄小宁罪大恶极!!!!!!!!!!
  13. 做PPT只会用黑体和宋体?这些可商用字体瞬间提升你的PPT档次
  14. 如何将证件照片打印在A4纸上
  15. HDU - 2859 Phalanx (DP)
  16. Python_pgzero小球抛物线运动
  17. Tomcat中如何配置使用APR
  18. 电子设计教程47:流水灯电路-74HC245驱动器
  19. iOS15.2 注册相册变化通知未给相册权限导致崩溃 [PHPhotoLibrary.sharedPhotoLibrary registerChangeObserver:self]
  20. mib browse

热门文章

  1. python常见的语法错误_python编程中常见错误
  2. 2021年全国研究生数学建模竞赛华为杯C题帕金森病的脑深部电刺激治疗建模研究求解全过程文档及程序
  3. 记录一次解决蓝牙音箱时断时续问题---蓝牙发射端接一根USB延长线,靠近蓝牙音箱
  4. CodeIgniter源码解读
  5. ubuntu突然显卡消失
  6. 国旗检测 pytorch yolov3
  7. python语言使用不需要付费不存在商业风险_中国大学MOOC慕课_Python语言基础与应用_答案...
  8. Python网络爬虫全网资源汇总
  9. Excel——VBA之移动文本框圆形操作
  10. 苹果或推出自家搜索引擎;阿里推出阿里云网盘对标百度云盘;腾讯云公布5G产品矩阵 | EA周报...