微信退款
微信退款实质上是根据商户单号和交易单号来原路返回退款的(支持部分退款)。
需要准备如下东东:
微信公appid、微信秘钥、商户号id、商户号秘钥、微信支付证书
方式一V2
1.导入依赖
<!--WXPay api-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>${weixin.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>${weixin.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>${weixin.version}</version>
</dependency>
<!--微信小程序 解密依赖-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
<!-- 过滤后缀为pem、pfx的证书文件 -->
<nonFilteredFileExtensions>
<nonFilteredFileExtension>pem</nonFilteredFileExtension>
<nonFilteredFileExtension>pfx</nonFilteredFileExtension>
<nonFilteredFileExtension>p12</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
版本统一为:3.5.0
2.业务层
service层
/**
* 微信退款
*
* @param wxRefundParam 微信退款参数类
* @return
*/
public Map weChatRefund(WxRefundParam wxRefundParam);
service实现类
注意: 若订单退款金额≤1元,且属于部分退款,则不会在退款消息中体现退款原因,这里也可以在逻辑中做判断就好了。
/**
* 微信退款
*
* @param wxRefundParam 微信退款参数类
* @return
*/
@Override
public Map weChatRefund(WxRefundParam wxRefundParam) {
System.out.println("----------进入微信退款业务层--------");
//随机字符串
String nonce_str = PayUtil.getRandomStringByLength(32);
SortedMap<String, String> params = new TreeMap<>();
params.put("appid", WxPayConfig.appID);
params.put("mch_id", WxPayConfig.MCH_ID);
params.put("nonce_str", nonce_str);
params.put("out_trade_no", wxRefundParam.getOutTradeNo());
params.put("transaction_id", wxRefundParam.getTransactionId());
//生成退款单号
String returnNo = String.valueOf(snowflake.nextId());
params.put("out_refund_no", returnNo);
params.put("refund_desc", wxRefundParam.getRefundDesc());
//把元转化成分, 金额*100, 注意:要将金额保留整数,否则参数无法转换
params.put("total_fee", String.valueOf(df.format(wxRefundParam.getTotalFee().doubleValue() * 100)));
String refundFee = String.valueOf(df.format(wxRefundParam.getRefundFee().doubleValue() * 100));
params.put("refund_fee", refundFee);
//退款回调地址
params.put("notify_url", apiConfig.getDomainName() + "/paySuccess/refundSuccess");
//签名算法
String stringA = PayUtil.createLinkString(params);
//第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。(签名)
String sign = PayUtil.sign(stringA, WxPayConfig.mchKey, "utf-8").toUpperCase();
params.put("sign", sign);
try {
String xml = PayUtil.GetMapToXML(params);
String xmlStr = doRefund("https://api.mch.weixin.qq.com/secapi/pay/refund", xml);
Map map = PayUtil.doXMLParse(xmlStr);
log.info("返回的前端数据-->{}", map);
if (map == null || !"SUCCESS".equals(map.get("return_code"))) {
//消息通知
log.info("退款发起失败-->{}", map);
throw new CustomException("退款发起失败,请稍后重试");
}
//成功的话就在下面写自己的逻辑吧
log.info("退款成功,退款金额为:{}", refundFee + "分");
return map;
} catch (Exception e) {
//微信退款接口异常
log.info("微信退款接口异常");
}
throw new CustomException("系统繁忙,请稍后重试");
}
/**
* 处理退款
*
* @param url 微信商户退款url
* @param data xml数据
* @return
* @throws Exception
*/
public static String doRefund(String url, String data){
StringBuilder sb = new StringBuilder();
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
//证书放好哦,我这个是linux的路径,相信乖巧的你也肯定知道windows该怎么写
// /usr/local/tomcat/webapps/cert/apiclient_cert.p12
//FileInputStream instream = new FileInputStream(new File("classpath:apiclient_cert.p12"));
File file = ResourceUtils.getFile("classpath:apiclient_cert.p12");
FileInputStream certStream = new FileInputStream(file);
String mchid = WxPayConfig.MCH_ID;
try {
keyStore.load(certStream, mchid.toCharArray());
} finally {
certStream.close();
}
// 证书
SSLContext sslcontext = SSLContexts.custom()
.loadKeyMaterial(keyStore, mchid.toCharArray())
.build();
// 只允许TLSv1协议
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslcontext,
new String[]{"TLSv1"},
null,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
//创建基于证书的httpClient,后面要用到
CloseableHttpClient client = HttpClients.custom()
.setSSLSocketFactory(sslsf)
.build();
HttpPost httpPost = new HttpPost(url);
//这里加入utf-8编码解决退款原因为中文的错误
StringEntity reqEntity = new StringEntity(data, "UTF-8");
// 设置类型
reqEntity.setContentType("application/x-www-form-urlencoded");
httpPost.setEntity(reqEntity);
CloseableHttpResponse response = client.execute(httpPost);
try {
HttpEntity entity = response.getEntity();
System.out.println(response.getStatusLine());
if (entity != null) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));
String text = "";
while ((text = bufferedReader.readLine()) != null) {
sb.append(text);
}
}
EntityUtils.consume(entity);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return sb.toString();
}
WxPayConfig为位置配置常量,主要装appid,商户id,商户秘钥等等,这里就不搬出来了.
3.controller层
/**
* 申请退款
*
* @param refundParam 微信退款参数类
*/
@PostMapping("refundMargin")
public AjaxResult refundMargin(@RequestBody WxRefundParam refundParam) {
if (refundParam.getTransactionId() == null || refundParam.getTransactionId() == "") {
return AjaxResult.error("微信支付单号不能为空");
}
if (refundParam.getOutTradeNo() == null || refundParam.getOutTradeNo() == "") {
return AjaxResult.error("商户号不能为空");
}
if (refundParam.getTotalFee() == null || refundParam.getRefundFee() == null) {
return AjaxResult.error("总金额或者退款金额不能为空");
}
return AjaxResult.success(payService.weChatRefund(refundParam));
}
4.退款成功回调
/**
* 退款通知,退款成功业务处理
*
* @param xmlData 回调信息
* @return
*/
@RequestMapping("refundSuccess")
public String refundSuccessfully(@RequestBody String xmlData) {
log.info("微信退款通知-->{}:" + xmlData);
try {
Map<String, String> params = WXPayUtil.xmlToMap(xmlData);
String returnCode = params.get("return_code");
if (WxPayKit.codeIsOk(returnCode)) {
String reqInfo = params.get("req_info");
if (returnCode != null || "SUCCESS".equals(returnCode)) {
log.info("退款成功");
}
//reqInfo解析
String decryptData = ParseReqInfo.reqInfoDecryption(reqInfo);
log.info("退款通知解密后的数据-->{}" + decryptData);
// 更新订单信息
// 发送通知等
Map<String, String> xml = new HashMap<String, String>(2);
xml.put("return_code", returnCode);
xml.put("return_msg", "OK");
return WxPayKit.toXml(xml);
}
} catch (Exception e) {
e.printStackTrace();
}
throw new CustomException("系统繁忙,请稍后重试");
}
5.附加
微信退款成功回调请求解析类
注意: 退款结果对重要的数据进行了加密,商户需要用商户秘钥进行解密后才能获得结果通知的内容
import com.cainaer.common.core.exception.CustomException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.Base64;
/**
* 微信退款请求解析类
*
* @author serence
* @date 2021/8/13 17:49
*/
public class ParseReqInfo {
//解码器
private static Cipher cipher = null;
//商户秘钥
private static String mchkey = "微信商户秘钥";
/**
* reqInfo解析
*
* @param reqInfo 请求信息
* @return
*/
public static String reqInfoDecryption(String reqInfo) {
init();
try {
return parseReqInfo(reqInfo);
} catch (Exception e) {
e.printStackTrace();
}
throw new CustomException("系统繁忙,请稍后重试");
}
/**
* 解析请求信息
*
* @param reqInfo 请求信息
* @return
* @throws Exception
*/
public static String parseReqInfo(String reqInfo) throws Exception {
Base64.Decoder decoder = Base64.getDecoder();
byte[] base64ByteArr = decoder.decode(reqInfo);
return new String(cipher.doFinal(base64ByteArr));
}
public static void init() {
String key = getMD5(mchkey);
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "AES");
Security.addProvider(new BouncyCastleProvider());
try {
cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}
public static String getMD5(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
String result = MD5(str, md);
return result;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
}
public static String MD5(String strSrc, MessageDigest md) {
byte[] bt = strSrc.getBytes();
md.update(bt);
String strDes = bytes2Hex(md.digest());
return strDes;
}
public static String bytes2Hex(byte[] bts) {
StringBuffer des = new StringBuffer();
String tmp = null;
for (int i = 0; i < bts.length; i++) {
tmp = (Integer.toHexString(bts[i] & 0xFF));
if (tmp.length() == 1) {
des.append("0");
}
des.append(tmp);
}
return des.toString();
}
微信传参类
/**
* 商户订单号 支付时的订单号
*/
private String outTradeNo;
/**
* 微信支付订单号
*/
private String transactionId;
/**
* 商户退款单号 新生成
*/
private String outRefundNo;
/**
* 订单总金额 单位为分
*/
private BigDecimal totalFee;
/**
* 退款金额 单位为分
*/
private BigDecimal refundFee;
/**
* 退款原因
*/
private String refundDesc;
查询退款订单
controller
/**
* 获取微信退款详情
* <p>
* 微信订单号查询的优先级是: refund_id > out_refund_no > transaction_id > out_trade_no
*
* @param wxRefundParam 微信退款参数类
* @return 微信参数数据
*/
@PostMapping("weChatQueryRefund")
public AjaxResult weChatQueryRefund(@RequestBody WxRefundParam wxRefundParam) {
Map<String, String> map = payService.weChatQueryRefund(wxRefundParam);
String errCode = map.get("err_code");
if (errCode != null) {
String errCodeDes = map.get("err_code_des");
return AjaxResult.error(errCodeDes);
}
return AjaxResult.success(map);
}
service实现层
/**
* 获取微信退款详情
*
* @param wxRefundParam 微信退款参数类
* @return
*/
@Override
public Map<String, String> weChatQueryRefund(WxRefundParam wxRefundParam) {
Map<String, String> params = RefundQueryModel.builder()
.appid(WxPayConfig.APP_ID)
.mch_id(WxPayConfig.MCH_ID)
.nonce_str(WeChatPayKit.generateStr())
.transaction_id(wxRefundParam.getTransactionId())
.out_trade_no(wxRefundParam.getOutTradeNo())
.out_refund_no(wxRefundParam.getOutRefundNo())
.refund_id(wxRefundParam.getRefundId())
.build()
.createSign(WxPayConfig.API_SECRET, WxSignType.MD5);
String resultStr = WeChatPayApi.orderRefundQuery(false, params);
Map<String, String> map = null;
try {
map = WXPayUtil.xmlToMap(resultStr);
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
方式二V2
1.导入依赖
<!--IJPay-WxPay-->
<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-WxPay</artifactId>
<version>2.7.4</version>
</dependency>
2.业务层
/**
* 微信退款处理V2 方式二
*
* @param wxRefundParam 微信退款参数类
* @return
*/
@Override
public Map<String, String> weChatRefundDealWith(WxRefundParam wxRefundParam) {
try {
//订单总金额
String totalFee = String.valueOf(wxRefundParam.getTotalFee().multiply(new BigDecimal(100)).intValue());
//退款金额
String refundFee = String.valueOf(wxRefundParam.getRefundFee().multiply(new BigDecimal(100)).intValue());
Map<String, String> params = com.ijpay.wxpay.model.RefundModel.builder()
.appid(WeChatPayConfig.APP_ID)
.mch_id(WeChatPayConfig.MCH_ID)
.nonce_str(WxPayKit.generateStr())
.transaction_id(wxRefundParam.getTransactionId())
.out_trade_no(wxRefundParam.getOutTradeNo())
.out_refund_no(WxPayKit.generateStr())
.total_fee(totalFee)
.refund_fee(refundFee)
.notify_url(WeChatPayConfig.DomainName + WeChatPayConfig.Refund_Callback)
.build()
.createSign(WeChatPayConfig.API_SECRET, SignType.MD5);
File file = ResourceUtils.getFile(WeChatPayConfig.CERT_PATH_P12);
FileInputStream certPath = new FileInputStream(file);
String refundStr = WxPayApi.orderRefund(false, params, certPath, WeChatPayConfig.MCH_ID);
log.info("refundStr: {}", refundStr);
//XML格式字符串转换为Map
return WXPayUtil.xmlToMap(refundStr);
} catch (Exception e) {
e.printStackTrace();
}
throw new CustomException("系统繁忙,请稍后重试");
}
3.异步通知
/**
* 微信退款成功回调通知 方式二
*
* @param xmlData 回调信息
* @return
*/
@Override
public String weChatRefundNotify(String xmlData) {
log.info("退款通知: {}" + xmlData);
Map<String, String> params = WxPayKit.xmlToMap(xmlData);
Map<String, String> xml = new HashMap<String, String>(2);
String returnCode = params.get("return_code");
// 注意重复通知的情况,同一订单号可能收到多次通知,请注意一定先判断订单状态
if (WxPayKit.codeIsOk(returnCode)) {
String reqInfo = params.get("req_info");
//解密数据
String decryptData = WxPayKit.decryptData(reqInfo, WxPayApiConfigKit.getWxPayApiConfig().getPartnerKey());
log.info("退款通知解密后的数据: {}" + decryptData);
//xml解析map
Map<String, String> aesMap = WxPayKit.xmlToMap(decryptData);
//商户退款单号
String outRefundNo = aesMap.get("out_refund_no");
//商户单号
String outTradeNo = aesMap.get("out_trade_no");
//交易单号
String transactionId = aesMap.get("transaction_id");
//微信内部退款单号
String refundId = aesMap.get("refund_id");
//退款时间
String refundTime = aesMap.get("success_time");
//退款状态
String refundStatus = aesMap.get("refund_status");
if (WxPayKit.codeIsOk(refundStatus)) {
// 更新订单信息
// 发送通知等
}
xml.put("return_code", "SUCCESS");
xml.put("return_msg", "OK");
} else {
xml.put("return_code", "fail");
}
return WxPayKit.toXml(xml);
}
方式二的这种方法代码比较清晰,就是添加退款原因的时候判断比较麻烦,因为微信的退款金额需大于1和退款金额等于总金额的情况下(非部分退款)才能添加退款原因。
微信支付篇: https://www.cnblogs.com/ckfeng/p/14953135.html
微信退款官方文档: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
证书获取方法: https://kf.qq.com/faq/161222NneAJf161222U7fARv.html
微信自带的sdk代码demo: pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1