经过一天颓废的战斗,终于跑通奇经八脉了;现在的我(body)比跑十圈操场还舒服......
微信退款实质上是根据商户单号和交易单号来原路返回退款的。
需要准备什么,这里就不多介绍了哈,在微信支付的基础上加上证书就好了。
微信支付篇: 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
废话少说,直接上代码
依赖
<!--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
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,商户秘钥等等,这里就不搬出来了.
微信退款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)); }
退款成功回调
/**
* 退款通知,退款成功业务处理
*
* @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("系统繁忙,请稍后重试");
}
微信退款成功回调请求解析类
注意: 退款结果对重要的数据进行了加密,商户需要用商户秘钥进行解密后才能获得结果通知的内容
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;
ok,粘贴完毕,如有哪里不明白的地方请下方留言哦!
我要去过七七了......