这一篇博客跟着我上一篇博客的思路和流程继续,如果想看上一篇博客的童鞋,可以点击微信支付----统一下单接口对接进行阅读,本篇博客将介绍申请退款和退款通知两个接口的开发。
一,申请退款接口
注意点:
- 此处需要使用证书,Java版本的话只需要导入apiclient_cert.p12格式的证书即可,其他版本请参考微信官方文档
- 必须要当天先做交易才能退款,我的理解是每天微信都会对资金进行清算,然后将余额进行清零(此处官方文档未提及)
- 退款回调地址不可与下单回调地址一样,因为通知的内容和解密方式是不一样的。
退款请求参数封装:
//商户订单号
map.put("out_trade_no",weixinRefundDto.getOutTradeNo());
//退款订单号
map.put("out_refund_no",weixinRefundDto.getOutRefundNo());
//订单金额 ----传入元为单位,请求第三方微信以分为单位
map.put("total_fee", multiplyAmount(weixinRefundDto.getTotalFee())+"");
//退款金额 ----传入元为单位,请求第三方微信以分为单位
map.put("refund_fee",multiplyAmount(weixinRefundDto.getRefundFee())+"");
//退款通知地址
map.put("notify_url",refundNotifyUrl);
WXPayConfig wxPayConfig = WxPayConfigImpl.getInstance();
WXPay wxPay = new WXPay(wxPayConfig);
Map<String, String> map1 = wxPay.refund(map);
//获取返回状态
String returnCode = map1.get("return_code");
//获取业务状态
String resultCode = map1.get("result_code");
实际对相关参数做了配置处理,以及对金额做了转换,这里需要注意的是,不论是统一下单还是申请退款,都需要传递int类型,也就是说不能传递带小数点的值,此处附上我解决bigdecimal格式的金额代码
/**
* 统一除法运算(100)
* @param amount
* @return
*/
public BigDecimal divideAmount(String amount){
BigDecimal amt = new BigDecimal(amount).divide(new BigDecimal(100));
return amt;
}
/**
* 统一乘法运算(100)
* @param amount
* @return
*/
public BigDecimal multiplyAmount(String amount){
BigDecimal amt = new BigDecimal(amount).multiply(new BigDecimal(100)).setScale(0);
return amt;
}
调用微信退款接口如果出现
代表你当前需要新做一笔交易,昨天测试没有退出来的钱你就不要想啦!!
如果成功,我们最好记录一下微信订单号和退款订单号,根据测试发现,一笔订单可以进行多次退款,微信订单号不变的情况下退款订单号是会变化的,所以最好记录下,毕竟是和钱有关系的东西,谨慎点好。
//微信订单号
String transaction_id = map1.get("transaction_id");
//微信退款订单号
String refund_id = map1.get("refund_id");
//商户订单号
String out_trade_no = map1.get("out_trade_no");
//商户退款订单号
String out_refund_no = map1.get("out_refund_no");
//退款金额
String refund_fee = map1.get("refund_fee");
此处是获取的相关订单号和退款订单金额,一切成功就可以在微信上面看到退款金额了噢。
二,退款回调
其实退款是否成功在调用申请退款接口的时候微信就已经可以明确答复你了,但是有一些重要信息只能在退款回调中给到你,我们可以看一下微信官方文档
其中,这几个参数只能在退款通知中给到你,如果你自己要做更为细致的操作,那就需要拿到这些信息。
但是,怎么拿到这些信息呢???
我们需要对这个risk_info字段进行解密,才能拿到里面的值,解密方式如下
先解码,再md5,最后aes-256解密,其中最为重要的是aes-256解密,因为aes进行加解密时,当密钥大于128时,代码会抛出java.security.InvalidKeyException: Illegal key size or default parametersIllegal key size or default parameters是指密钥长度是受限制的,java运行时环境读到的是受限的policy文件。文件位于${java_home}/jre/lib/security(自身jdk/jre文件路径)下 这种限制是因为美国对软件出口的控制。
然而微信推荐的就是aes-256,所以我们需要修改下我们自己的jdk文件,大家可以去下载替换jar包,然后将jar包粘贴替换至自己的相应路径下,以我本地路径为例:
1.修改jdk文件路径下面的jar包
JDK:C:\Program Files\Java\jdk1.8.0_31\jre\lib\security
JRE:C:\Program Files\Java\jre1.8.0_31\lib\security
2.在C:\Program Files\Java\jre1.8.0_31\lib\security\java.security新增一条代码
security.provider.11=org.bouncycastle.jce.provider.BouncyCastleProvider
3.导入jar包---bcprov-jdk15-145.jar(可忽略)
然后再次通过微信的解密步骤进行请求即可,最终会得到一个xml文件,最后转换成map格式即可。
AESUtil
package com.youfuli.vendor.utils.weixin;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.Security;
/**
* Created by 李啸天 on 2018/11/29.
*/
public class AESUtil {
/**
* 密钥算法
*/
private static final String ALGORITHM = "AES";
/**
* 加解密算法/工作模式/填充方式
*/
private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
/**
* 生成key
*/
//微信支付API密钥设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
// private static String paySign = "微信支付API密钥";
//key
private static String paySign = WeixinConfig.getInstance().getProperty("weixin.key");
//对商户key做md5,得到32位小写key*
private static SecretKeySpec key = new SecretKeySpec(MD5Util.MD5Encode(paySign, "UTF-8").toLowerCase().getBytes(), ALGORITHM);
static {
}
/**
* AES加密
*
* @param data
* @return
* @throws Exception
*/
public static String encryptData(String data) throws Exception {
Security.addProvider(new BouncyCastleProvider());
// 创建密码器
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
// 初始化
cipher.init(Cipher.ENCRYPT_MODE, key);
return Base64Util.encode(cipher.doFinal(data.getBytes()));
}
/**
* AES解密
*
*(1)对加密串A做base64解码,得到加密串B
*(2)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
* @param base64Data
* @return
* @throws Exception
*/
public static String decryptData(String base64Data) throws Exception {
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
cipher.init(Cipher.DECRYPT_MODE, key);
return new String(cipher.doFinal(Base64Util.decode(base64Data)));
}
/* public static void main(String[] args) throws Exception {
String req_info = "nQ0oEMVFOVXsbxVsw3xb1mYZDo1YSNUQYA9hJKrKQkR1TOpADrPCQCWrTCgD+UJwFKWI9edv7UVRqLBh/1jXncv4Fa2iOxgoDKcdIANCzisIPFC5Gxo5v9sURmxti+s5aZK6cj/VC2/CPpDJrb0c1GhXx4Qek+9emTRi/rtgrjc/ui4INPMHING3Tap1Xsc2n/2O9skCYACBYP34YO0qEb0x27mz0iDJG4nPFC1Qv3lyzX5Or+5KylpP8yT5FfsMY7tZnCMbRo6mAkXNVEfIuIXQPrv+oH3LE2OzVGbPxF9/6YXKs7QxUGMyDheSCica+OibMPkvneTNjCI+3Q97oJBB8Dwv8xP346XrFXMmy+942xBISELNQr1pS+eGsvuj+4YpVEcMQjOI9M2bBjJK6YfU+TcJ0L5Yh75pk3E7R+K3TfEHmqEV50zAC2L5L5j6BGjj5GdXgpuIvxHvAtnpb7uqF3+/PmtaGvCIT9p8zebsCwT0QhtpXR/ITO7FPLUvk9EGjB7uVmy/b2lygiIJ97d0UyFnun0kCY42KVp6EpT+1F2p3j81z8FagVXkP5XFn1KAWV77fahAK8DJnzG47ozlRwMQ676GG8dnqIoP21ivu304IoBEVnNl8FPASeR5qJjTeaT/tajT+Exjg4Lo2BGlQlbSXvy9kMrNZ+Qnr+pUZ1m55qJPeUOpqbfKCst6bHBzPywpCZCTBSzJonz6fd75hPAdmmKchBKGQ/1UmC1nEwJ1JwgiMfyvTBbSiFNgLfkTgAeURm87/9lwvccSwPzxiFbHGeA/uo3SxKPUjBt0ExOpADVNr2gZ/uCO/4eWBVxtHKJJqNOm8JdvF7fhJd/SufNArysltwQkeh713MQb+phJQbSlcmI/02e9SWT6yL/e82qhrZyZ1ccoWJ+yprqvZJ4RapJq44LI1UjoHI0YARF+oY9usW9WRrh0x4RBBhv4gbGxSiaihgFeg6QAxuIXEqeB0dxyrdCrAdf7j88hinqyIjjYndCM8a3NL40suEqfHTHnqkJ1VvRxyMa/NK50rAY9hJpD9K+PdaplL";
String aa = AESUtil.decryptData(req_info);
System.out.println(aa);
}*/
}
Base64Util
package com.youfuli.vendor.utils.weixin;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
/**
* Created by 李啸天 on 2018/11/29.
*/
public class Base64Util {
private static final char S_BASE64CHAR[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
private static final byte S_DECODETABLE[];
static {
S_DECODETABLE = new byte[128];
for (int i = 0; i < S_DECODETABLE.length; i++)
S_DECODETABLE[i] = 127;
for (int i = 0; i < S_BASE64CHAR.length; i++)
S_DECODETABLE[S_BASE64CHAR[i]] = (byte) i;
}
/**
* @param ibuf
* @param obuf
* @param wp
* @return
*/
private static int decode0(char ibuf[], byte obuf[], int wp) {
int outlen = 3;
if (ibuf[3] == '=')
outlen = 2;
if (ibuf[2] == '=')
outlen = 1;
int b0 = S_DECODETABLE[ibuf[0]];
int b1 = S_DECODETABLE[ibuf[1]];
int b2 = S_DECODETABLE[ibuf[2]];
int b3 = S_DECODETABLE[ibuf[3]];
switch (outlen) {
case 1: // '\001'
obuf[wp] = (byte) (b0 << 2 & 252 | b1 >> 4 & 3);
return 1;
case 2: // '\002'
obuf[wp++] = (byte) (b0 << 2 & 252 | b1 >> 4 & 3);
obuf[wp] = (byte) (b1 << 4 & 240 | b2 >> 2 & 15);
return 2;
case 3: // '\003'
obuf[wp++] = (byte) (b0 << 2 & 252 | b1 >> 4 & 3);
obuf[wp++] = (byte) (b1 << 4 & 240 | b2 >> 2 & 15);
obuf[wp] = (byte) (b2 << 6 & 192 | b3 & 63);
return 3;
}
throw new RuntimeException("Internal error");
}
/**
* @param data
* @param off
* @param len
* @return
*/
public static byte[] decode(char data[], int off, int len) {
char ibuf[] = new char[4];
int ibufcount = 0;
byte obuf[] = new byte[(len / 4) * 3 + 3];
int obufcount = 0;
for (int i = off; i < off + len; i++) {
char ch = data[i];
if (ch != '=' && (ch >= S_DECODETABLE.length || S_DECODETABLE[ch] == 127))
continue;
ibuf[ibufcount++] = ch;
if (ibufcount == ibuf.length) {
ibufcount = 0;
obufcount += decode0(ibuf, obuf, obufcount);
}
}
if (obufcount == obuf.length) {
return obuf;
} else {
byte ret[] = new byte[obufcount];
System.arraycopy(obuf, 0, ret, 0, obufcount);
return ret;
}
}
/**
* @param data
* @return
*/
public static byte[] decode(String data) {
char ibuf[] = new char[4];
int ibufcount = 0;
byte obuf[] = new byte[(data.length() / 4) * 3 + 3];
int obufcount = 0;
for (int i = 0; i < data.length(); i++) {
char ch = data.charAt(i);
if (ch != '=' && (ch >= S_DECODETABLE.length || S_DECODETABLE[ch] == 127))
continue;
ibuf[ibufcount++] = ch;
if (ibufcount == ibuf.length) {
ibufcount = 0;
obufcount += decode0(ibuf, obuf, obufcount);
}
}
if (obufcount == obuf.length) {
return obuf;
} else {
byte ret[] = new byte[obufcount];
System.arraycopy(obuf, 0, ret, 0, obufcount);
return ret;
}
}
/**
* @param data
* @param off
* @param len
* @param ostream
* @throws IOException
*/
public static void decode(char data[], int off, int len, OutputStream ostream) throws IOException {
char ibuf[] = new char[4];
int ibufcount = 0;
byte obuf[] = new byte[3];
for (int i = off; i < off + len; i++) {
char ch = data[i];
if (ch != '=' && (ch >= S_DECODETABLE.length || S_DECODETABLE[ch] == 127))
continue;
ibuf[ibufcount++] = ch;
if (ibufcount == ibuf.length) {
ibufcount = 0;
int obufcount = decode0(ibuf, obuf, 0);
ostream.write(obuf, 0, obufcount);
}
}
}
/**
* @param data
* @param ostream
* @throws IOException
*/
public static void decode(String data, OutputStream ostream) throws IOException {
char ibuf[] = new char[4];
int ibufcount = 0;
byte obuf[] = new byte[3];
for (int i = 0; i < data.length(); i++) {
char ch = data.charAt(i);
if (ch != '=' && (ch >= S_DECODETABLE.length || S_DECODETABLE[ch] == 127))
continue;
ibuf[ibufcount++] = ch;
if (ibufcount == ibuf.length) {
ibufcount = 0;
int obufcount = decode0(ibuf, obuf, 0);
ostream.write(obuf, 0, obufcount);
}
}
}
/**
* @param data
* @return
*/
public static String encode(byte data[]) {
return encode(data, 0, data.length);
}
/**
* @param data
* @param off
* @param len
* @return
*/
public static String encode(byte data[], int off, int len) {
if (len <= 0)
return "";
char out[] = new char[(len / 3) * 4 + 4];
int rindex = off;
int windex = 0;
int rest;
for (rest = len - off; rest >= 3; rest -= 3) {
int i = ((data[rindex] & 255) << 16) + ((data[rindex + 1] & 255) << 8) + (data[rindex + 2] & 255);
out[windex++] = S_BASE64CHAR[i >> 18];
out[windex++] = S_BASE64CHAR[i >> 12 & 63];
out[windex++] = S_BASE64CHAR[i >> 6 & 63];
out[windex++] = S_BASE64CHAR[i & 63];
rindex += 3;
}
if (rest == 1) {
int i = data[rindex] & 255;
out[windex++] = S_BASE64CHAR[i >> 2];
out[windex++] = S_BASE64CHAR[i << 4 & 63];
out[windex++] = '=';
out[windex++] = '=';
} else if (rest == 2) {
int i = ((data[rindex] & 255) << 8) + (data[rindex + 1] & 255);
out[windex++] = S_BASE64CHAR[i >> 10];
out[windex++] = S_BASE64CHAR[i >> 4 & 63];
out[windex++] = S_BASE64CHAR[i << 2 & 63];
out[windex++] = '=';
}
return new String(out, 0, windex);
}
/**
* @param data
* @param off
* @param len
* @param ostream
* @throws IOException
*/
public static void encode(byte data[], int off, int len, OutputStream ostream) throws IOException {
if (len <= 0)
return;
byte out[] = new byte[4];
int rindex = off;
int rest;
for (rest = len - off; rest >= 3; rest -= 3) {
int i = ((data[rindex] & 255) << 16) + ((data[rindex + 1] & 255) << 8) + (data[rindex + 2] & 255);
out[0] = (byte) S_BASE64CHAR[i >> 18];
out[1] = (byte) S_BASE64CHAR[i >> 12 & 63];
out[2] = (byte) S_BASE64CHAR[i >> 6 & 63];
out[3] = (byte) S_BASE64CHAR[i & 63];
ostream.write(out, 0, 4);
rindex += 3;
}
if (rest == 1) {
int i = data[rindex] & 255;
out[0] = (byte) S_BASE64CHAR[i >> 2];
out[1] = (byte) S_BASE64CHAR[i << 4 & 63];
out[2] = 61;
out[3] = 61;
ostream.write(out, 0, 4);
} else if (rest == 2) {
int i = ((data[rindex] & 255) << 8) + (data[rindex + 1] & 255);
out[0] = (byte) S_BASE64CHAR[i >> 10];
out[1] = (byte) S_BASE64CHAR[i >> 4 & 63];
out[2] = (byte) S_BASE64CHAR[i << 2 & 63];
out[3] = 61;
ostream.write(out, 0, 4);
}
}
/**
* @param data
* @param off
* @param len
* @param writer
* @throws IOException
*/
public static void encode(byte data[], int off, int len, Writer writer) throws IOException {
if (len <= 0)
return;
char out[] = new char[4];
int rindex = off;
int rest = len - off;
int output = 0;
do {
if (rest < 3)
break;
int i = ((data[rindex] & 255) << 16) + ((data[rindex + 1] & 255) << 8) + (data[rindex + 2] & 255);
out[0] = S_BASE64CHAR[i >> 18];
out[1] = S_BASE64CHAR[i >> 12 & 63];
out[2] = S_BASE64CHAR[i >> 6 & 63];
out[3] = S_BASE64CHAR[i & 63];
writer.write(out, 0, 4);
rindex += 3;
rest -= 3;
if ((output += 4) % 76 == 0)
writer.write("\n");
}
while (true);
if (rest == 1) {
int i = data[rindex] & 255;
out[0] = S_BASE64CHAR[i >> 2];
out[1] = S_BASE64CHAR[i << 4 & 63];
out[2] = '=';
out[3] = '=';
writer.write(out, 0, 4);
} else if (rest == 2) {
int i = ((data[rindex] & 255) << 8) + (data[rindex + 1] & 255);
out[0] = S_BASE64CHAR[i >> 10];
out[1] = S_BASE64CHAR[i >> 4 & 63];
out[2] = S_BASE64CHAR[i << 2 & 63];
out[3] = '=';
writer.write(out, 0, 4);
}
}
}
MD5Utils
/*
* PROJECT NAME: api-webapp
* PACKAGE NAME: com.twsz.api.controller.utils
* FILE NAME: MD5Util.java
* COPYRIGHT: Copyright(c) 2016 上海共进医疗科技有限公司 All Rights Reserved.
*/
package com.youfuli.vendor.utils.weixin;
import java.security.MessageDigest;
/**
* @author lixiaotian
*/
public class MD5Utils {
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString;
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}
最后配合即可,其实退款通知最大的难点在于,微信一句轻飘飘的aes-256加密,却没有给我们提示这样那样的坑,看来还得我们开发者自己琢磨。
String reqInfo = m.get("req_info");
String infos = AESUtil.decryptData(reqInfo);
Map map = XMLUtil.doXMLParse(infos);
System.out.println(map.toString());
最终可以解密成功并成功打印即代表成功了。至此,微信退款回调也就已经完毕。