java实现微信小程序退款

请仔细阅读微信退款文档 微信退款文档

申请退款

退款需要有证书,参照:安全规范第三条 API证书

/**
     * @DESCRIPTION: 调用微信退款
     */
    public void payOrderRefund() {
        try {
            //查询需要退款的金额
            double refundMoney = 0.0;

            if (验证退款金额,不验证也可以) {
                //封装参数
                Map<String, String> data = this.createData(refundMoney);

                //MD5运算生成签名,这里是第一次签名,用于调用统一下单接口
                //WeChat.KEY:微信支付的商户密钥,需要在微信后台设置,包括后面用到的 KEY 都是这个值
                String mySign = this.sign(this.createLinkString(data), WeChat.KEY, "utf-8").toUpperCase();
                data.put("sign", mySign);
                log.info("退款 签名:" + mySign);
                log.info("向微信服务器发送退款请求...");

                //支付结果通知的xml格式数据
                //退款的post请求方法需要有证书验证
                String xmlStr = httpUtils.postData(""https://api.mch.weixin.qq.com/secapi/pay/refund"", this.GetMapToXML(data));
                log.info("请求数据:" + xmlStr);

                Map notifyMap = this.doXMLParse(xmlStr);

                if (WeChat.SUCCESS.equals(notifyMap.get("return_code"))) {
                    if (WeChat.SUCCESS.equals(notifyMap.get("result_code"))) {
                        log.info("退款请求成功");
                    } else {
                        log.info("退款失败!原因:" + notifyMap.get("err_code_des"));
                    }
                } else {
                    log.info("退款失败!原因:" + notifyMap.get("return_msg"));
                }
            }
        } catch (Exception e) {
            log.info("退款异常:" + e.getMessage());
            e.printStackTrace();
        } 
    }

下面的工具方法可以和上面的放在一起也可以单独写一个工具类

封装退款参数

//封装退款请求需要的数据
    private Map<String, String> createData(OrderItemDto orderItemDto, double refundMoney) {

        Map<String, String> data = new HashMap<>();

        //随机字符串
        String nonce_str = PayUtil.getRandomStringByLength(32);
        //商户订单号
        String out_trade_no = orderItemDto.getOrderNo();
        //商户退款订单号
        String out_refund_no = PayUtil.createOrderNoOrRefundNo(orderItemDto.getHotelId());
        //订单总金额 微信支付提交的金额是不能带小数点的,且是以分为单位,这边需要转成字符串类型,否则后面的签名会失败
        String total_fee = String.valueOf((int) (orderItemDto.getAmount() * 100));
        //退款总金额
        String refund_fee = String.valueOf((int) (refundMoney * 100));

        data.put("appid", appid);
        data.put("mch_id", WeChat.MCH_ID);
        data.put("nonce_str", nonce_str);
        data.put("sign_type", WeChat.SIGNTYPE);
        data.put("out_trade_no", out_trade_no);
        data.put("out_refund_no", out_refund_no);
        data.put("total_fee", total_fee);
        data.put("refund_fee", refund_fee);
        data.put("notify_url", WeChat.REFUND_NOTIFY_URL);
        data.put("refund_desc", WeChat.REFUND_DESC);

        return data;
    }

签名

/**
     * 签名字符串
     *
     * @param text          需要签名的字符串
     * @param key           商户平台设置的密钥
     * @param input_charset 编码格式
     * @return 签名结果
     */
    public static String sign(String text, String key, String input_charset) {
        text = text + "&key=" + key;
        return DigestUtils.md5Hex(getContentBytes(text, input_charset));
    }

转换xml

public static String GetMapToXML(Map<String, String> param) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        for (Map.Entry<String, String> entry : param.entrySet()) {
            sb.append("<" + entry.getKey() + ">");
            sb.append(entry.getValue());
            sb.append("</" + entry.getKey() + ">");
        }
        sb.append("</xml>");
        return sb.toString();
    }

解析xml

/**
     * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
     *
     * @param strxml
     * @return
     * @throws IOException
     */
    public static Map doXMLParse(String strxml) throws Exception {
        if (stringUtils.isEmpty(strxml)) {
            return null;
        }
        Map m = new HashMap();
        InputStream in = String2Inputstream(strxml);
        SAXBuilder builder = new SAXBuilder();
        Document doc = builder.build(in);
        Element root = doc.getRootElement();
        List list = root.getChildren();
        Iterator it = list.iterator();
        while (it.hasNext()) {
            Element e = (Element) it.next();
            String k = e.getName();
            String v = "";
            List children = e.getChildren();
            if (children.isEmpty()) {
                v = e.getTextNormalize();
            } else {
                v = getChildrenText(children);
            }
            m.put(k, v);
        }
        //关闭流
        in.close();
        return m;
    }

排序并按规则拼接

/**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     */
    public static String createLinkString(Map<String, String> params) {
        List<String> keys = new ArrayList<>(params.keySet());
        Collections.sort(keys);
        String prestr = "";
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);
            if (i == keys.size() - 1) {
                // 拼接时,不包括最后一个&字符
                prestr = prestr + key + "=" + value;
            } else {
                prestr = prestr + key + "=" + value + "&";
            }
        }
        return prestr;
    }

http工具类

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;

@Slf4j
public class httpUtils {

    // 连接超时时间,默认10秒
    private static int socketTimeout = 10000;
    // 传输超时时间,默认30秒
    private static int connectTimeout = 30000;
    // HTTP请求器
    private static CloseableHttpClient httpClient;
    // 请求器的配置
    private static RequestConfig requestConfig;

    /**
     * 加载证书
     */
    private static void initCert() throws Exception {
        // 证书密码,默认为商户ID
        String key = "";
        // 商户证书的路径
        //public static final String CERT_PATH = "E:/workspace/apiclient_cert.p12"; //本地路径
        String CERT_PATH = "/usr/local/dev/guns/cert/"; // 服务器路径
        String path = CERT_PATH + "qcdr_cert.p12";

        InputStream instream = null;
        try {
            // 读取本机存放的PKCS12证书文件
            instream = new FileInputStream(new File(path));

            /*ClassPathResource classPathResource = new ClassPathResource(path);
            //获取文件流
            instream = classPathResource.getInputStream();
            instream = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);*/
        } catch (Exception e) {
            log.error("商户证书不正确-->>" + e);
        }
        try {
            // 指定读取证书格式为PKCS12
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            // 指定PKCS12的密码(商户ID)
            keyStore.load(instream, key.toCharArray());

            SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, key.toCharArray()).build();
            // 指定TLS版本
            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
            // 设置httpclient的SSLSocketFactory
            httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        } catch (Exception e) {
            log.error("商户秘钥不正确-->>" + e);
        } finally {
            instream.close();
        }
    }

    /**
     * 通过Https往API post xml数据
     *
     * @param url    退款地址
     * @param xmlObj 要提交的XML数据对象
     * @return
     */
    public static String postData(String url, String xmlObj) {
        // 加载证书
        try {
            initCert();
        } catch (Exception e) {
            e.printStackTrace();
        }
        String result = null;
        HttpPost httpPost = new HttpPost(url);
        // 得指明使用UTF-8编码,否则到API服务器XML的中文不能被成功识别
        StringEntity postEntity = new StringEntity(xmlObj, "UTF-8");
        httpPost.addHeader("Content-Type", "text/xml");
        httpPost.setEntity(postEntity);
        // 根据默认超时限制初始化requestConfig
        requestConfig = RequestConfig.custom()
                .setSocketTimeout(socketTimeout)
                .setConnectTimeout(connectTimeout)
                .build();
        // 设置请求器的配置
        httpPost.setConfig(requestConfig);
        try {
            HttpResponse response = null;
            try {
                response = httpClient.execute(httpPost);
            } catch (IOException e) {
                e.printStackTrace();
            }
            HttpEntity entity = response.getEntity();
            try {
                result = EntityUtils.toString(entity, "UTF-8");
            } catch (IOException e) {
                e.printStackTrace();
            }
        } finally {
            httpPost.abort();
        }
        return result;
    }

    /**
     * @param requestUrl    请求地址
     * @param requestMethod 请求方法
     * @param outputStr     参数
     */
    public static String httpRequest(String requestUrl, String requestMethod, String outputStr) {
        // 创建SSLContext
        StringBuffer buffer = null;
        try {
            URL url = new URL(requestUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(requestMethod);
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.connect();
            //往服务器端写内容
            if (null != outputStr) {
                OutputStream os = conn.getOutputStream();
                os.write(outputStr.getBytes("utf-8"));
                os.close();
            }
            // 读取服务器端返回的内容
            InputStream is = conn.getInputStream();
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            BufferedReader br = new BufferedReader(isr);
            buffer = new StringBuffer();
            String line = null;
            while ((line = br.readLine()) != null) {
                buffer.append(line);
            }
            br.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return buffer.toString();
    }
}

下面是小程序退款回调函数

退款回调函数

/**
     * 微信退款回调函数
     */
    public void payOrderRefundCallback(HttpServletRequest request, HttpServletResponse response) {

        log.info("======================= 微信退款回调开始 =======================");
        String inputLine = "";
        String notityXml = "";
        StringBuffer buffer = new StringBuffer();
        buffer.append("<xml>");

        try {
            while ((inputLine = request.getReader().readLine()) != null) {
                notityXml += inputLine;
            }
            //关闭流
            request.getReader().close();
            log.info("退款  微信回调内容信息:" + notityXml);

            //解析成Map
            Map map = this.doXMLParse(notityXml);

            //判断 退款是否成功
            if ("SUCCESS".equals(map.get("return_code").toString())) {

                log.info("开始解密回调信息...");

                //解密返回的加密信息
                String passMap = AESUtil.decryptData(map.get("req_info").toString());

                log.info("解密成功!");
                //拿到解密信息
                map = this.doXMLParse(passMap);

                if ("SUCCESS".equals(map.get("refund_status"))) {

                    //退款回调成功,添加业务逻辑,可以修改订退款成功

                    buffer.append("<return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg>");

                } else {
                    log.info("REFUNDCLOSE".equals(map.get("refund_status")) == true ? "退款关闭" : "退款异常");
                    buffer.append("<return_code><![CDATA[FAIL]]></return_code>");
                    buffer.append("<return_msg><![CDATA[退款异常]]></return_msg>");
                }

            } else {
                log.info("退款 微信回调返回失败");
                buffer.append("<return_code><![CDATA[FAIL]]></return_code>");
                buffer.append("<return_msg><![CDATA[请求退款回调函数失败]]></return_msg>");
            }

            buffer.append("</xml>");
            //给微信服务器返回 成功标识 否则会一直询问是否回调成功
            PrintWriter writer = response.getWriter();
            writer.print(buffer.toString());

            log.info("退款回调通知微信的xml【" + buffer.toString() + "】");

        } catch (Exception e) {
            log.info("退款异常:" + e.getMessage());
            System.out.println(e);
            e.printStackTrace();
        }

        log.info("======================= 微信退款回调结束 =======================");
    }

解密回调字符串

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class AESUtil {
    /**
     * 密钥算法
     */
    private static final String ALGORITHM = "AES";
    /**
     * 加解密算法/工作模式/填充方式
     */
    private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS5Padding";
    /**
     * 生成key
     */
    private static SecretKeySpec key = new SecretKeySpec(PayUtil.MD5Encode(WeChat.KEY, "UTF-8").toLowerCase().getBytes(), ALGORITHM);

    /**
     * AES加密
     *
     * @param data
     * @return
     * @throws Exception
     */
    public static String encryptData(String data) throws Exception {
        // 创建密码器
        Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
        // 初始化
        cipher.init(Cipher.ENCRYPT_MODE, key);
        return PayUtil.encode(cipher.doFinal(data.getBytes()));
    }

    /**
     * AES解密
     *
     * @param base64Data
     * @return
     * @throws Exception
     */
    public static String decryptData(String base64Data) throws Exception {
        Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
        cipher.init(Cipher.DECRYPT_MODE, key);
        return new String(cipher.doFinal(PayUtil.decode(base64Data)));
    }
}

如果微信支付(退款)业务出现AES解密失败(Illegal key size or default parameters)
请参照:AES解密失败

至此整个微信小程序支付退款完成,如果有什么问题欢迎指出

微信小程序 支付 详细代码地址:微信小程序支付