微信支付退款功能

微信支付前的准备

微信支付退款功能需要证书,需要在微信支付平台的账号管理–>api安全中申请证书。

后台开发

话不多说,直接上代码吧,如果想细了解的请看官方文档
微信退款
1.实体类

  @ApiModel("微信退款实体类")
@Data
public class RefundDomain {

    /*
     * orderId       商户订单号
     * */
    @JsonProperty(value = "orderId")
    @ApiModelProperty(value = "商户订单号",required=true)
    private String orderId;
    /*
     * totalFee      订单金额/退款金额 也可以分为两个字段,退款金额不能大于订单金额
     * */
    @JsonProperty(value = "money")
    @ApiModelProperty(value = "订单金额/退款金额",required=true)
    private Double money;

    /*
     * refundAccount 退款资金来源(默认传 "REFUND_SOURCE_UNSETTLED_FUNDS")
     * */
    @JsonProperty(value = "refundAccount")
    @ApiModelProperty(value = "退款资金来源",required=false)
    private String refundAccount = "REFUND_SOURCE_UNSETTLED_FUNDS";
}

  1. 控制层
    /**
     * 申请退款
     */
    @ApiOperation(value="微信支付退款", notes="微信支付退款功能,前端传用户支付的金额以及当前用户退款的订单号")
    @PostMapping("/refund")
    public Map<String, String> refund(@RequestBody RefundDomain refundDomain) {
        System.out.println("money-------->:"+refundDomain.getMoney()); 
        System.out.println("OrderId-------->:"+refundDomain.getOrderId());
        System.out.println("getRefundAccount-------->:"+refundDomain.getRefundAccount());
        // 生成唯一退款订单号
        Integer out_trade_no = (int) (System.currentTimeMillis() / 1000 + 970516);
        // 获取退款的金额 微信支付中的金额必须为正整数,所以需要把小数去掉。
        Double money = refundDomain.getMoney();
        Integer total_fee = money.intValue();

        SortedMap<String, Object> params = new TreeMap<String, Object>();
        params.put("appid", Parm.APPID); // 微信分配的小程序ID
        params.put("mch_id", Parm.MCH_ID); // 微信支付分配的商务号
        params.put("nonce_str", randomString); // 随机字符串,不长于32位。
        params.put("out_trade_no", refundDomain.getOrderId()); //商户订单号和微信订单号二选一(我这里选的是商户订单号)
        params.put("out_refund_no", out_trade_no); // 商户退款单号
        params.put("total_fee", total_fee); // 订单金额
        params.put("refund_fee", total_fee); // 退款金额
        params.put("refund_account", refundDomain.getRefundAccount()); // 退款资金来源
        params.put("sign_type", "MD5"); // 签名类型
        String preStr = PayCommonUtil.createLinkString(params);

        //签名算法
        String sign = (PayCommonUtil.sign(preStr, Parm.KEY, "utf-8")).toUpperCase();
        System.out.println("sign-------->:"+sign);
        params.put("sign", sign);

        Map<String, String> map = new HashMap<>();
        try {
            System.out.println("params-------->:"+params);
            String xml = PayCommonUtil.getRequestXml(params);
            System.out.println("xml-------->:"+xml);
            String xmlStr = PayCommonUtil.doRefund("https://api.mch.weixin.qq.com/secapi/pay/refund", xml);
            System.out.println("xmlStr-------->:"+xmlStr);
            map = PayCommonUtil.doXMLParse(xmlStr);
        } catch (Exception e) {
        }
        return map;
    }

3.微信常量类

package com.studies.wx.util;

import org.springframework.stereotype.Component;

/**
 * @Description: 微信常量类
 */
@Component
public class Parm {
    /**
     * 微信接入token,自定义但是一定要与公众平台上的保持一致
     */
    public static final String TOKEN = "微信公众号平台中设置的,小程序不需要";

    public static final String APPID = "微信公众号或者小程序平台中开发者模式里面的";

    public static final String APPSECRET = "微信公众号或者小程序平台中开发者模式里面的";

    public static final String REDIRECT_URI = "http://localhost:8090/PcText(随意写的)";

    public static byte [] certData;
    /*
     * 商务号api秘银
     * */
    public static String KEY = "微信支付平台中的秘银";
    /*
     * 微信商务号
     * */
    public static String MCH_ID = "微信支付平台中给的商务号";
    /**
     * EncodingAESKey 公众平台上面自己填写的43位EncodingAESKey(服务号的)
     */
    public static final String EncodingAESKey = "自己填写的";
    /**
     * 获取access_token的接口地址(GET) 限200(次/天)
     */
    public static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";

    /**
     * 获取JS_SDK_TICKET
     */
    public static final String JS_SDK_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi";

    /**
     * 获取网页授权的access_token的接口地址
     */
    public static final String OAUTH2_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";

    /**
     * 自定义菜单删除接口
     */
    public static final String MENU_DELETE_URL = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN";

    /**
     * 自定义菜单的创建接口
     */
    public static final String MENU_CREATE_URL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
    /**
     * 自定义菜单的查询接口
     */
    public static final String MENU_GET_URL = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN";
    /**
     * 客服接口-发消息接口
     */
    public static final String CUSTOM_SERVICE_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN";

    /**
     * 发送模板消息接口
     */
    public static final String SEND_TEMPLATE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN";

    /**
     * 创建标签接口
     *
     * @Method :POST
     */
    public static final String USE_TAG_CREATE_URL = "https://api.weixin.qq.com/cgi-bin/tags/create?access_token=ACCESS_TOKEN";

    /**
     * 获取用户身上的标签
     *
     * @Method:POST
     */
    public static final String GET_INUSER_TAG_URL = "https://api.weixin.qq.com/cgi-bin/tags/getidlist?access_token=ACCESS_TOKEN";

    /**
     * 批量为用户取消标签
     *
     * @Method:POST
     */
    public static final String UNTAGGING_USER_BATCH_URL = "https://api.weixin.qq.com/cgi-bin/tags/members/batchuntagging?access_token=ACCESS_TOKEN";

    /**
     * 创建个性化菜单
     *
     * @Method :POST
     */
    public static final String CREATE_PERSONALIZED_MENU_URL = "https://api.weixin.qq.com/cgi-bin/menu/addconditional?access_token=ACCESS_TOKEN";

    /**
     * 删除个性化菜单
     *
     * @Method:
     */
    public static final String DELETE_PERSONAL_MENU_URL = "https://api.weixin.qq.com/cgi-bin/menu/delconditional?access_token=ACCESS_TOKEN";

    /**
     * 给用户打标签的姐接口
     */
    public static final String CREATE_USERTAG_URL = "https://api.weixin.qq.com/cgi-bin/tags/members/batchtagging?access_token=ACCESS_TOKEN";

    /**
     * 网页授权获取用户详细信息的的接口
     */
    public static final String GET_USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";

    /**
     * openid获取用户的基本信息的接口
     */
    public static final String OPENID_USERINFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";

}


4.工具类

   public class PayCommonUtil {
    //微信参数配置
    public static String API_KEY = Parm.KEY;

    //随机字符串生成
    public static String getRandomString(int length) { //length表示生成字符串的长度
        String base = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }

    //请求xml组装
    public static String getRequestXml(SortedMap<String, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        Set es = parameters.entrySet();
        Iterator it = es.iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            String key = (String) entry.getKey();
            Object value = entry.getValue();
            if ("attach".equalsIgnoreCase(key) || "body".equalsIgnoreCase(key) || "sign".equalsIgnoreCase(key)) {
                sb.append("<" + key + ">" + "<![CDATA[" + value + "]]></" + key + ">");
            } else {
                sb.append("<" + key + ">" + value + "</" + key + ">");
            }
        }
        sb.append("</xml>");
        return sb.toString();
    }

    //xml解析
    public static Map doXMLParse(String strxml) throws JDOMException, IOException {
        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
        if (null == strxml || "".equals(strxml)) {
            return null;
        }
        Map m = new HashMap();
        InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
        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;
    }

    //生成签名
    public static String createSign(String characterEncoding, SortedMap<String, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        Set es = parameters.entrySet();
        Iterator it = es.iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            String k = (String) entry.getKey();
            Object v = entry.getValue();
            if (null != v && !"".equals(v)
                    && !"sign".equals(k) && !"key".equals(k)) {
                sb.append(k + "=" + v + "&");
            }
        }
        sb.append("key=" + API_KEY);
        System.out.println(sb.toString());
        String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
        return sign;
    }

    /**
     * 签名字符串
     *
     * @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));
    }

    private static byte[] getContentBytes(String content, String charset) {
        if (charset == null || "".equals(charset)) {
            return content.getBytes();
        }
        try {
            return content.getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset);
        }
    }

    /**
     * 验证回调签名
     *
     * @return
     */
    public static boolean isTenpaySign(Map<String, String> map) {
        String characterEncoding = "utf-8";
        String charset = "utf-8";
        String signFromAPIResponse = map.get("sign");
        if (signFromAPIResponse == null || signFromAPIResponse.equals("")) {
            System.out.println("API返回的数据签名数据不存在,有可能被第三方篡改!!!");
            return false;
        }
        System.out.println("服务器回包里面的签名是:" + signFromAPIResponse);
        //过滤空 设置 TreeMap
        SortedMap<String, String> packageParams = new TreeMap();

        for (String parameter : map.keySet()) {
            String parameterValue = map.get(parameter);
            String v = "";
            if (null != parameterValue) {
                v = parameterValue.trim();
            }
            packageParams.put(parameter, v);
        }

        StringBuffer sb = new StringBuffer();
        Set es = packageParams.entrySet();
        Iterator it = es.iterator();

        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            String k = (String) entry.getKey();
            String v = (String) entry.getValue();
            if (!"sign".equals(k) && null != v && !"".equals(v)) {
                sb.append(k + "=" + v + "&");
            }
        }
        sb.append("key=" + API_KEY);

        //将API返回的数据根据用签名算法进行计算新的签名,用来跟API返回的签名进行比较
        //算出签名
        String resultSign = "";
        String tobesign = sb.toString();

        if (null == charset || "".equals(charset)) {
            resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
        } else {
            try {
                resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
            } catch (Exception e) {
                resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
            }
        }

        String tenpaySign = ((String) packageParams.get("sign")).toUpperCase();
        return tenpaySign.equals(resultSign);
    }

    //请求方法
    public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
        try {

            URL url = new URL(requestUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 设置请求方式(GET/POST)
            conn.setRequestMethod(requestMethod);
            conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
            // 当outputStr不为null时向输出流写数据
            if (null != outputStr) {
                OutputStream outputStream = conn.getOutputStream();
                // 注意编码格式
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }
            // 从输入流读取返回内容
            InputStream inputStream = conn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str = null;
            StringBuffer buffer = new StringBuffer();
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            // 释放资源
            bufferedReader.close();
            inputStreamReader.close();
            inputStream.close();
            inputStream = null;
            conn.disconnect();
            return buffer.toString();
        } catch (ConnectException ce) {
            System.out.println("连接超时:{}" + ce);
        } catch (Exception e) {
            System.out.println("https请求异常:{}" + e);
        }
        return null;
    }

    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     */
    public static String createLinkString(SortedMap<String, Object> 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).toString();
            if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
                preStr = preStr + key + "=" + value;
            } else {
                preStr = preStr + key + "=" + value + "&";
            }
        }
        return preStr;
    }

    public static String getChildrenText(List children) {
        StringBuffer sb = new StringBuffer();
        if (!children.isEmpty()) {
            Iterator it = children.iterator();
            while (it.hasNext()) {
                Element e = (Element) it.next();
                String name = e.getName();
                String value = e.getTextNormalize();
                List list = e.getChildren();
                sb.append("<" + name + ">");
                if (!list.isEmpty()) {
                    sb.append(getChildrenText(list));
                }
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }

        return sb.toString();
    }

    /**
     * 调用微信退款接口
     */
    public static String doRefund(String url, String data) throws Exception {
        KeyStore keyStore = KeyStore.getInstance("PKCS12"); //证书格式
        try {
            InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("apiclient_cert.p12");
            System.out.println("certStream----->:"+certStream);
            Parm.certData = IOUtils.toByteArray(certStream);
            certStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        ByteArrayInputStream is = new ByteArrayInputStream(Parm.certData);
        try {
            keyStore.load(is, Parm.MCH_ID.toCharArray());
        } finally {
            is.close();
        }
        // Trust own CA and all self-signed certs
        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(
                keyStore,
                Parm.MCH_ID.toCharArray())
                .build();
        // Allow TLSv1 protocol only
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                sslcontext,
                new String[]{"TLSv1"},
                null,
                SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
        );
        CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        try {
            HttpPost httpost = new HttpPost(url); // 设置响应头信息
            httpost.addHeader("Connection", "keep-alive");
            httpost.addHeader("Accept", "*/*");
            httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
            httpost.addHeader("Host", "api.mch.weixin.qq.com");
            httpost.addHeader("X-Requested-With", "XMLHttpRequest");
            httpost.addHeader("Cache-Control", "max-age=0");
            httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
            httpost.setEntity(new StringEntity(data, "UTF-8"));
            CloseableHttpResponse response = httpclient.execute(httpost);
            try {
                HttpEntity entity = response.getEntity();

                String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
                EntityUtils.consume(entity);
                return jsonStr;
            } finally {
                response.close();
            }
        } finally {
            httpclient.close();
        }
    }

    // 获取设备ip
    public static String getRemoteHost(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;
    }
}

vue前端

// 微信退款
   let param = {
		orderId:this.data.out_trade_no, // 支付的商户订单号
		money:this.data.total_fee, // 用户支付的金额
	};
	console.log(param);
	// 前端我使用了axios接口统一管理模式,refundController后台接口
	refundController(param).then(ref =>{
		console.log(ref);
		// 如果退款成功 result_code 返回success,如果退款失败 result_code 返回fall
		if(ref.data.result_code === "SUCCESS"){
			uni.showToast({
				icon: 'none',
				title: "退款成功",
				duration: 2000
			})
		}else{
			uni.showToast({
				icon: 'none',
				title: ref.data.err_code_des, // 退款失败,err_code_des才会返回,退款成功则不会返回这个字段
				duration: 2000
			})
		}
	})

微信支付退款就到此结束了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值