最近公司财务爸爸提需求,把三个支付退款api集成到公司系统里面去,由于之前api文档看的不够仔细,遇到很多坑,特此记录,分享给同样遇到坑的小伙伴:
商户能提供的是
appid 你的appid 也就是对于微信来说的唯一标示
appsecret 通过你的微信商户号进入就可以看到一个32位加密
key 商户的秘钥 这秘钥不是一开始就有的。需要你自己去设置,在设置的时候还需要与本商户号绑定的手机发下验证码
mchid 商户id不解释
强烈建议仔细阅读,微信和支付宝的开发文档可以避免少踩很多坑;
微信app开发文档地址:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_4&index=6
微信jsapi开发文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
支付宝退款文档地址:https://docs.open.alipay.com/api_1/alipay.trade.refund
支付宝相关参数获取页面:
微信证书及apiKey获取页面:
1.微信jsapi退款、微信app退款:两个接口看官方文档参数基本一致,不同在于商户号(mch_id)、商户秘钥(API_KEY)、应用id即微信标识(APPID)
import com.cdy.common.util.DisconfDataGetter;
import com.cdy.common.util.MD5Util;
import com.cdy.lx.common.constant.CommonConstant;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import java.io.*;
import java.math.BigDecimal;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.System.out;
/**
* @author liuxue
* @ClassName WxPayRefundUtil
* @ProjectName cdy-ccb
* @Description: TODO 微信退款工具类
* @date 2019/5/20 0020上午 10:35
* @Version 1.0
*/
public class WxPayRefundUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(WxPayRefundUtil.class);
//微信appid (注*以下配置文件,我是使用百度的分布式配置文件配置,实际替换成你们自己的配置)
private static final String WX_APPID = (String) DisconfDataGetter.getByFileItem("wxconfig.properties", "AppID");
//微信商户号
private static final String WX_MCH_ID = (String) DisconfDataGetter.getByFileItem("wxconfig.properties", "WX_MCH_ID");
//微信api秘钥
private static final String API_KEY = (String) DisconfDataGetter.getByFileItem("wxconfig.properties", "WECHAT_MCH_SECRET");
//环境版本控制(1表示测试环境,2表示生产环境)
private static final String VERSION_CONTRO = (String) DisconfDataGetter.getByFileItem("wxconfig.properties", "VERSION_CONTRO");
//微信公众号支付退款回调地址
private static final String WX_REFUND_NOTIFY_URL = (String) DisconfDataGetter.getByFileItem("wxconfig.properties", "WX_REFUND_NOTIFY_URL");
//微信app支付退款回调地址
private static final String WX_APP_REFUND_NOTIFY_URL = (String) DisconfDataGetter.getByFileItem("wxconfig.properties", "WX_APP_REFUND_NOTIFY_URL");
/**
* @param out_trade_no 商户订单号
* @param transaction_id 微信订单号
* @param total_fee 订单总金额
* @Author: liux
* @Description:微信退款
* @Date: 2019/5/20 0020上午 10:35
* @return:
*/
private static String wxPayRefund(String refund_account,String notify_url ,String refundNum,String out_trade_no, String transaction_id, String total_fee,String appId,String mchId,String apiKey) {
StringBuffer xml = new StringBuffer();
String data = null;
try {
String nonceStr = genNonceStr();//生成32位随机字符串
xml.append("</xml>");
Map<String, Object> parameters = new HashMap<>();
parameters.put("appid", appId);
parameters.put("mch_id", mchId);
parameters.put("nonce_str", nonceStr);
parameters.put("out_trade_no", out_trade_no);
if(transaction_id != null){
parameters.put("transaction_id", transaction_id);
}
parameters.put("out_refund_no", refundNum);
parameters.put("fee_type", "CNY");
parameters.put("total_fee", total_fee);
parameters.put("refund_fee", total_fee);
parameters.put("op_user_id", mchId);
parameters.put("notify_url", notify_url);
parameters.put("refund_account", refund_account);
parameters.put("sign", WechatPayUtil.getSign(parameters,apiKey)/*createSign(parameters, apiKey)*/);
data = SortedMaptoXml(parameters);
} catch (Exception e) {
System.err.println(e.getMessage());
return null;
}
return data;
}
/**
* @param out_trade_no 商户订单号
* @param transaction_id 微信订单号
* @param total_fee 订单总金额
* @param refund_fee 退款金额
* @Author: liux
* @Description:微信部分退款
* @Date: 2019/5/20 0020上午 10:35
* @return:
*/
private static String wxPayRefund(String refund_account,String notify_url ,String refundNum,String out_trade_no, String transaction_id, String total_fee, String refund_fee,String appId,String mchId,String apiKey) {
StringBuffer xml = new StringBuffer();
String data = null;
try {
String nonceStr = genNonceStr();//生成32位随机字符串
xml.append("</xml>");
Map<String, Object> parameters = new HashMap<>();
parameters.put("appid", appId);
parameters.put("mch_id", mchId);
parameters.put("nonce_str", nonceStr);
parameters.put("out_trade_no", out_trade_no);
if(transaction_id != null){
parameters.put("transaction_id", transaction_id);
}
parameters.put("out_refund_no", refundNum);
parameters.put("fee_type", "CNY");
parameters.put("total_fee", total_fee);
parameters.put("refund_fee", refund_fee);//部退款金额
parameters.put("op_user_id", mchId);
parameters.put("notify_url", notify_url);
parameters.put("refund_account", refund_account);//仅针对老资金流商户使用REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款)REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款
parameters.put("sign", WechatPayUtil.getSign(parameters,apiKey)/*createSign(parameters, apiKey)*/);
data = SortedMaptoXml(parameters);
} catch (Exception e) {
System.err.println(e.getMessage());
return null;
}
return data;
}
/**
* 生成32位随机数字
*/
public static String genNonceStr() {
Random random = new Random();
return MD5Util.MD5(String.valueOf(random.nextInt(10000)));
}
/**
* @param characterEncoding
* @param parameters
* @Author: liux
* @Description:支付参数生成签名
* @Date: 2019/5/20 0020上午 10:35
*/
public static String createSign(String characterEncoding, SortedMap<Object, Object> parameters) {
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序)
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);
String sign = MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
/**
* 创建md5摘要,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
*/
public static String createSign(SortedMap<String, String> packageParams, String AppKey) {
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 (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + AppKey);
String sign = MD5Util.MD5Encode(sb.toString()).toUpperCase();
return sign;
}
/**
* @param params
* @Author: liux
* @Description:请求值转换为xml格式 SortedMap转xml
* @Date:2019/5/20 0020上午 10:35
*/
private static String SortedMaptoXml(Map<String, Object> params) {
StringBuilder sb = new StringBuilder();
Set es = params.entrySet();
Iterator it = es.iterator();
sb.append("<xml>\n");
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
Object v = entry.getValue();
sb.append("<" + k + ">");
sb.append(v);
sb.append("</" + k + ">\n");
}
sb.append("</xml>");
return sb.toString();
}
/**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
public static String MD5(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
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 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];
}
private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
/**
* 证书使用
* 微信退款
*/
private static String wxPayBack(String url, String data,String fileName,String mchId) throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// FileInputStream instream = new FileInputStream(new File("F:\\wx\\apiclient_cert.p12"));
//FileInputStream instream=(FileInputStream)inputStream;
String result="";
InputStream inputStream = inputStream =new FileInputStream(new File(fileName));
if(inputStream == null){
return result;
}
try {
keyStore.load(inputStream, mchId.toCharArray());
} finally {
inputStream.close();
}
// Trust own CA and all self-signed certs
SSLContext sslcontext = SSLContexts.custom()
.loadKeyMaterial(keyStore, mchId.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 httppost = new HttpPost("https://api.mch.weixin.qq.com/secapi/pay/refund");
StringEntity entitys = new StringEntity(data);
httppost.setEntity((HttpEntity) entitys);
CloseableHttpResponse response = httpclient.execute(httppost);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent()));
String text="";
String t="";
while ((text=bufferedReader.readLine()) != null) {
t+=text;
}
byte[] temp=t.getBytes("utf-8");//这里写原编码方式
String newStr=new String(temp);//这里写转换后的编码方式
result=newStr;
}
EntityUtils.consume(entity);
} finally {
response.close();
}
} finally {
httpclient.close();
}
return result;
}
/**
* @Description: 退款接口 供外部调用
* @param ${refundNum} :商户退款${source} :订单来源(微信公众号或者app微信)
* @param ${out_trade_no} :商户订单号 ${transaction_id} :微信平台订单号
* @param ${total_fee} :订单金额 ${refund_fee} :退费金额
* @param ${refund_account} 退费钱包(默认未结算金额退款)
* @return ${return_type}
* @throws
* @author liuxue
* @date 2019/5/24 0024 下午 3:30
*/
public static Map<String,String> wxRefund(String refundNum,String source,String out_trade_no, String transaction_id, BigDecimal total_fee, BigDecimal refund_fee,String refund_account) {
String param = "" ;
String appId = "";
String mchId = "";
String apiKey = "";
String notifyUrl = "";
String totalAmount = String.valueOf(Double.valueOf(total_fee.multiply(BigDecimal.valueOf(100)).toString()).intValue());
String refundAmount = String.valueOf(Double.valueOf(refund_fee.multiply(BigDecimal.valueOf(100)).toString()).intValue());
if("app".equals(source)){
appId = CommonConstant.OPEN_WECHAT_APPID;
mchId = CommonConstant.OPEN_WECHAT_MCHID;
apiKey = CommonConstant.OPEN_WECHAT_MCH_SECRET;
notifyUrl = WX_APP_REFUND_NOTIFY_URL;//退款通知回调地址
}else{
appId = WX_APPID;
mchId = WX_MCH_ID;
apiKey = API_KEY;
notifyUrl = WX_REFUND_NOTIFY_URL;//退款通知回调地址
}
String fileName = ProjectPathUtil.getRootPath("/refundceat/test_wx_apiclient_cert.p12");
if(VERSION_CONTRO!=null && "2".equals(VERSION_CONTRO)){//2表示生产环境
fileName = ProjectPathUtil.getRootPath("/refundceat/wx_apiclient_cert.p12");
}
if ("app".equals(source)){
fileName = ProjectPathUtil.getRootPath("/refundceat/wx_app_apiclient_cert.p12");
}
if(total_fee.subtract(refund_fee).compareTo(BigDecimal.valueOf(0))==0){
LOGGER.info("微信|退款|单号|"+out_trade_no+"|全额退款:请求参数out_trade_no:"+out_trade_no+":transaction_id:"+transaction_id+":total_fee:"+total_fee+":refund_fee:"+refund_fee);
param = wxPayRefund(refund_account,notifyUrl,refundNum,out_trade_no,transaction_id,totalAmount,appId,mchId,apiKey);
}else{
LOGGER.info("微信|退款|单号|"+out_trade_no+"|微信部分退款:请求参数out_trade_no:"+out_trade_no+":transaction_id:"+transaction_id+":total_fee:"+total_fee+":refund_fee:"+refund_fee);
param = wxPayRefund(refund_account,notifyUrl,refundNum,out_trade_no,transaction_id,totalAmount,refundAmount,appId,mchId,apiKey);
}
LOGGER.info("微信|退款|单号|"+out_trade_no+"|请求xml|"+param);
String result = "";
String url = "https://api.mch.weixin.qq.com/secapi/pay/refund";
try {
result = wxPayBack(url, param,fileName,mchId);
LOGGER.info("微信|退款|单号|"+out_trade_no+"|返回结果"+result);
Map<String,String> map = WechatPayUtil.getMapStringFromXML(result);
return map;
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("微信|退款|单号|"+out_trade_no+"|出错",e);
}
return null;
}
}
上面的代码,我遇到过一个问题,就是maven编译打包会把2进制这种证书文件给改变,导致证书不可用的问题,可以在maven打包中
过滤证书文件不编译,添加一下配置即可,(实际生产环境可能会与到路径不对,无法读取证书问题,这个就没法给一个公用的解决方案了,
只能具体情况,自己去特殊处理了):
<nonFilteredFileExtensions>
<nonFilteredFileExtension>p12</nonFilteredFileExtension>
</nonFilteredFileExtensions>
WechatPayUtil工具类
import com.cdy.common.util.MD5Util;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Splitter;
import com.google.common.collect.Maps;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.*;
import static java.lang.System.out;
/**
* 微信支付签名算法
*
* @author liux
* @since 2019-05-15 14:38
*/
public class WechatPayUtil {
private static Logger log = Logger.getLogger(WechatPayUtil.class);
public static String getSign(Map<String, Object> map, String mch_secret) {
ArrayList<String> list = new ArrayList<String>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!"".equals(entry.getValue()) && entry.getValue() != null) {
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + mch_secret;
log.debug("before sign:" + result);
result = MD5Util.MD5Encode(result).toUpperCase();
log.debug("after sign:" + result);
return result;
}
}
Md5util工具类:
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import org.apache.commons.codec.digest.DigestUtils;
/**
*/
public class MD5Util {
private static final char[] hexDigest = new char[]{'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7",
"8", "9", "a", "b", "c", "d", "e", "f"};
public final static String MD5(String content) {
//用于加密的字符
char md5String[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F'};
try {
//使用平台的默认字符集将此 String 编码为 byte序列,并将结果存储到一个新的 byte数组中
byte[] btInput = content.getBytes();
//信息摘要是安全的单向哈希函数,它接收任意大小的数据,并输出固定长度的哈希值。
MessageDigest mdInst = MessageDigest.getInstance("MD5");
//MessageDigest对象通过使用 update方法处理数据, 使用指定的byte数组更新摘要
mdInst.update(btInput);
// 摘要更新之后,通过调用digest()执行哈希计算,获得密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) { // i = 0
byte byte0 = md[i]; //95
str[k++] = md5String[byte0 >>> 4 & 0xf]; // 5
str[k++] = md5String[byte0 & 0xf]; // F
}
//返回经过加密后的字符串
return new String(str);
} catch (Exception e) {
return null;
}
}
/**
*
* @Description: 生成member的token
* @param id
* @param mobilePhone
* @param password
* @return
* String
* @author liux
* @date 2017年10月16日
*/
public static String generateToken(Long id, String mobilePhone, String password) {
StringBuilder sb = new StringBuilder();
sb.append(id);
sb.append(mobilePhone);
sb.append(password);
sb.append(new Date().toString());
//生成token
return md5(sb.toString());
}
public static String md5(String content) {
try {
//1.创建消息摘要实例
MessageDigest md = MessageDigest.getInstance("MD5");
//2.获取待加密内容的字节数组
byte[] contentB = content.getBytes();
//3.使用指定的字节更新摘要
md.update(contentB);
//4.加密
byte[] newContent = md.digest();//长度为16的字节数组
//5.将加密后的16位字节数组转换为32位十六进制数字
int k=0;
char[] contentC = new char[newContent.length * 2];
for(int i=0;i<newContent.length;i++)
{
byte b = newContent[i];
contentC[k++] = hexDigest[b >>> 4 & 0xf]; //高四位
contentC[k++] = hexDigest[b & 0xf]; //低四位
}
return new String(contentC);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
/**
* MD5编码
* @param origin 原始字符串
* @return 经过MD5加密之后的结果
*/
public static String MD5Encode(String origin) {
String resultString = null;
try {
resultString = origin;
MessageDigest md = MessageDigest.getInstance("MD5");
resultString = byteArrayToHexString(md.digest(resultString.getBytes("utf-8")));
} catch (Exception e) {
e.printStackTrace();
}
return resultString;
}
/**
* 转换字节数组为16进制字串
* @param b 字节数组
* @return 16进制字串
*/
public static String byteArrayToHexString(byte[] b) {
StringBuilder resultSb = new StringBuilder();
for (byte aB : b) {
resultSb.append(byteToHexString(aB));
}
return resultSb.toString();
}
/**
* 转换byte到16进制
* @param b 要转换的byte
* @return 16进制格式
*/
private static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n = 256 + n;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
/**
*
* @Description:
* @param text 需要签名的字符串
* @param key 密钥
* @param input_charset 编码格式
* @return 签名结果
* String
* @author liux
* @date 2017年11月8日
*/
public static String sign(String text, String key, String input_charset) {
text = text + key;
System.out.println(text);
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);
}
}
public static String md5Str(String str)
{
if (str == null)return "";
return md5Str(str, 0);
}
/**
* 计算消息摘要。
* @param data 计算摘要的数据。
* @param offset 数据偏移地址。
* @param length 数据长度。
* @return 摘要结果。(16字节)
*/
public static String md5Str(String str, int offset)
{
try
{
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] b = str.getBytes("UTF8");
md5.update(b, offset, b.length);
return byteArrayToHexString(md5.digest());
}
catch (NoSuchAlgorithmException ex)
{
ex.printStackTrace();
return null;
}
catch (UnsupportedEncodingException ex)
{
ex.printStackTrace();
return null;
}
}
}
支付宝退款相关代码,这里有点注意的,代码里面用到的私钥和公钥,如果不知道可以去对应的支付宝商户平台上去查看,还有加密的方式是使用的“RSA”还是“RSA2”可以去上商户平台上查看,这个写错 也是无法退款的:
引入sdk
<dependency>
<groupId>com.alipay</groupId>
<artifactId>sdk-java</artifactId>
<version></version>
</dependency>
AlipayRefundUtil
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.cdy.common.util.DisconfDataGetter;
import com.cdy.common.util.JSONUtil;
import com.cdy.lx.common.constant.CommonConstant;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
/**
* @author liux
* @ClassName AlipayRefundUtil
* @ProjectName cdy-ccb
* @Description: TODO
* @date 2019/5/20 0020上午 10:05
* @Version 1.0
*/
public class AlipayRefundUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(AlipayRefundUtil.class);
//支付宝退款请求的网关
private static String requestUrl = "https://openapi.alipay.com/gateway.do";
//编码级别
private static String CHARSET = "UTF-8";
public static AlipayTradeRefundResponse refundOrder(String out_trade_no, String refundAmount){
System.out.println("开始调用支付宝加密。。。");
//实例化客户端
AlipayClient alipayClient = new DefaultAlipayClient(requestUrl, CommonConstant.APP_ID, CommonConstant.APP_PRIVATE_KEY, "json", CHARSET, CommonConstant.ALIPAY_PUBLIC_KEY, "RSA");
//SDK已经封装掉了公共参数,这里只需要传入业务参数。以下方法为sdk的model入参方式(model和biz_content同时存在的情况下取biz_content)。
AlipayTradeRefundModel refundModel = new AlipayTradeRefundModel();
//refundModel.setTradeNo(trade_no);
refundModel.setOutTradeNo(out_trade_no);
refundModel.setRefundAmount(refundAmount);
refundModel.setRefundReason("商品退款");
//实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称:alipay.trade.app.pay
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
request.setBizModel(refundModel);
try{
AlipayTradeRefundResponse response = alipayClient.execute(request);
LOGGER.info("支付宝|退款|单号|"+out_trade_no+"|返回结果"+ JSONUtil.toJson(response));
return response;
}catch(Exception e){
e.printStackTrace();
LOGGER.error("支付宝退款错误!",e.getMessage());
}
return null;
}
/** 支付宝退款接口(部分退款)
* @param out_trade_no 订单支付时传入的商户订单号,不能和支付宝交易号(trade_no)同时为空。
* @param trade_no 支付宝交易号
* @param refund_amount 需要退款的金额,该金额不能大于订单金额,单位为元,支持两位小数
* @return 将提示信息返回
*/
public synchronized static AlipayTradeRefundResponse alipayRefundRequest(String out_trade_no,String trade_no,BigDecimal refund_amount,String out_request_no) {
String returnStr = null;
try {
LOGGER.info("支付宝|退款|单号|"+out_trade_no+"申请退款:请求参数out_trade_no:"+out_trade_no+":trade_no:"+trade_no+":refund_amount:"+refund_amount);
AlipayClient alipayClient = new DefaultAlipayClient(requestUrl,CommonConstant.APP_ID, CommonConstant.APP_PRIVATE_KEY,"JSON", CHARSET, CommonConstant.ALIPAY_PUBLIC_KEY, "RSA");
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
request.setBizContent("{" +
"\"out_trade_no\":\"" + out_trade_no + "\"," +
"\"trade_no\":\"" + trade_no + "\"," +
"\"refund_amount\":\"" + refund_amount + "\"," +
"\"out_request_no\":\"" + out_request_no+ "\"," +
"\"refund_reason\":\"正常退款\"" +
" }");
AlipayTradeRefundResponse response;
response = alipayClient.execute(request);
LOGGER.info("支付宝|退款|单号|"+out_trade_no+"|返回结果"+ JSONUtil.toJson(response));
return response;
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("支付宝|退款|单号|"+out_trade_no+"|出错",e);
}
return null;
}
}
遇到过的错误:
1.java.security.InvalidKeyException: Illegal key size or default parameters
参考https://blog.csdn.net/cl11992/article/details/86703694 (我这边是测试环境linux jdk 1.8.0 151遇到的,通过升级jdk版本解决)
2.DER input, Integer tag error (测试环境遇到的错误,好像是秘钥配置问题,秘钥一定要和证书对应)
3.{"code":"40003","msg":"Insufficient Conditions","sub_code":"isv.missing-signature-config","sub_msg":"验签出错, 未配置对应签名算法的公钥或者证书"}(这种问题,都是支付宝的秘钥配置不正确)
4.微信退款返回签名错误(这种问题也是秘钥和证书不一致,(商户秘钥不是微信公众平台上面配置的秘钥))
浦发相关的代码就不贴了,有不清楚的可以问题;