微信支付-退款 V3版本
微信官方文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml
环境说明:
spring版本:4.1.7.RELEASE
jdk版本:1.8
maven版本:3.8.5
可能需要用的依赖,如果实际运行提示缺少什么包中的的什么方法,就去maven仓库中下载,我也是一点一点摸着过来的
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.7</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.3</version>
</dependency>
参数说明
微信支付商户号:merchantId
商户API证书序列号:merchantSerialNumber
商户API私钥地址:merchantPrivateKeyPath
商户apiV3秘钥:apiV3Key
这四个参数是必须的,需要设置为全局变量,会在退款方法跟回调方法中做双向验签时使用。具体如何获得四个参数参看微信支付文档即可
证书管理器: certificatesManager,该参数需要设置为全局变量,在退款方法中会下载证书并储存在这个对象中,然后在回调方法中会使用这个对象得到验证器去做验证
CertificatesManager certificatesManager = CertificatesManager.getInstance();
我写的demo使用的是微信官方提供的sdk : wechatpay-apache-httpclient
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
wechatpay-apache-httpclient,适用于使用Apache HttpClient处理HTTP的Java开发者。
过程
1,按文档中请求参数要求,获得需要的参数信息
(我只填写了必填项以及回调参数,其他参数请按实际需求填写)
@RequestMapping("/XXXXXXXXXX")
@ResponseBody
public JSONObject acquireWXParams(Long reservationId) {
return acquireWXParams(reservationId, refundNotify);
}
---------------------华丽的分界线-------------------------
public JSONObject acquireWXParams(Long reservationId, String refundNotify) {
//储存微信退款所需要的参数
Map<String, Object> wxParams = new HashMap<>();
//获取微信支付订单号
if(reservationId == null) {
return resultJSON(false, null);
}
//根据reservation获取预定信息实例
//此处为查询数据库操作,我需要根据预订信息表中的数据得到相应的参数,具体参数获得请按照实际项目需要填写
ReservationInformation reservation = reservationInformationDao.get(reservationId);
if(reservation == null) {
return resultJSON(false, null);
}
//获取微信支付订单号
String transaction_id = reservation.getOrderMchid();
if(transaction_id == null || transaction_id.isEmpty()) {
return resultJSON(false, null);
}
//生成商户退款单号
String refundNumber = "RM_TK"+System.currentTimeMillis();
//获得金额信息(包括退款金额,原订单金额,退款币种)
Map<String, Object> amount = new HashMap<>();
//退款金额
BigDecimal bigDecimal = reservation.getPrice();
if(bigDecimal == null) {
return resultJSON(false, null);
}
//微信需要的该参数单位是分,所以乘以100
long refund = bigDecimal.longValue() * 100;
//原订单金额
//我们不做一笔订单多次退款,只做一次退全款,所以退款金额跟原订单金额相同
long total = refund;
//金额信息设置-----开始
amount.put("refund", refund);
amount.put("total", total);
amount.put("currency", "CNY");
//金额信息设置-----结束
//返回参数设置-----开始
//微信支付订单号
wxParams.put("transaction_id", transaction_id);
//商户退款单号
wxParams.put("out_refund_no", refundNumber);
//金额信息
wxParams.put("amount", amount);
//回调路径
wxParams.put("notify_url", refundNotify);
//返回参数设置-----结束
return resultJSON(true, wxParams);
}
---------------------华丽的分界线-------------------------
public JSONObject resultJSON(boolean isRight, Object data) {
JSONObject json = new JSONObject();
json.put("result", isRight);
if(data != null) {
json.put("data", data);
}
return json;
}
2,接下来拿着上面得到的退款接口所需要的全部参数,向微信退款接口发起post请求
@RequestMapping("/XXXXXXXXXX")
public void WXRefundApi(String wxparams) {//wxparams即为微信退款接口所需要的数据了,就是将上边json格式的数据转换为String格式(我当时脑子可能不太好使,得到参数后向js响应,然后再用js再带着参数再请求这个接口)
//merchantPrivateKeyPath = "/apiclient_key.pem"
//我是将私钥商户API私钥放在了resource中,所以我可以读到该文件的内容,大家的私钥文件根据项目实际情况放置即可,只要能够读到文件内容即可
//也可以将私钥中的内容复制出来,存在一个字符串中,直接读这个字符串也行,wechatpay-apache-httpclient这个sdk的 README.md中也有写到:
//# 示例:私钥为String字符串
/**PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new ByteArrayInputStream(privateKey.getBytes("utf-8")));*/
try (InputStream is = new ClassPathResource(merchantPrivateKeyPath).getInputStream()) {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(is);
certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId, new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(merchantId);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier));
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
try (CloseableHttpClient httpClient = builder.build()) {
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/refund/domestic/refunds");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type","application/json; charset=utf-8");
httpPost.setEntity(new StringEntity(wxparams));
CloseableHttpResponse response = httpClient.execute(httpPost);
//这就是请求微信退款接口得到的响应数据
String bodyAsString = EntityUtils.toString(response.getEntity());
System.out.println(bodyAsString);
//接下来就可以按照实际项目逻辑做一些处理
//。。。。。。。。。。。。。
//。。。。。。。。。。。。。
}
} catch (IOException | GeneralSecurityException | HttpCodeException | NotFoundException e) {
throw new RuntimeException(e);
}
}
3,对微信端做出正确的响应
当我们向微信发起退款请求完成后,微信会调用我们之前填写的回调地址,向我们响应退款结果,如果我们没能正确给微信响应,他会反复请求我们的回调地址,下边是微信的通知规则
通知规则
商户退款完成后,微信会把相关退款结果和用户信息发送给清算机构,清算机构需要接收处理后返回应答成功,然后继续给异步通知到下游从业机构。
对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)
@RequestMapping("/XXXXXXXXX")
@ResponseBody
public void refundNotify(HttpServletRequest request, HttpServletResponse response) {
LOGGER.info("进入回调接口");
//这里设置应答状态码200以及成功的信息仅是为了对微信端做出正确的响应,有关业务逻辑问题在后边进行处理
response.setHeader("Content-Type", "application/json");
response.setHeader("Accept", "application/json");
response.setStatus(200);
response.getOutputStream().write(mapResult("SUCCESS", "成功"));
response.flushBuffer();
try{
//获得NotificationRequest对象
NotificationRequest notificationRequest = getNotificationRequest(request);
//获得微信支付通知报文解密结果
Notification notification = getParseResult(notificationRequest);
//验证通过,确认是微信官方发送的的请求
//接下来要实现业务逻辑
handle(notification);
} catch (ValidationException | NotFoundException | IOException | ParseException e) {
LOGGER.error(e.getMessage());
}
LOGGER.info("回调结束");
}
-----------------------华丽的分割线-----------------------
public byte[] mapResult(String code, String message) {
Map<String, String> map = new HashMap<>();
map.put("code", code);
map.put("message", message);
return JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8);
}
-----------------------华丽的分割线-----------------------
public NotificationRequest getNotificationRequest(HttpServletRequest request) throws IOException {
//从请求头获取验签字段
String Timestamp = request.getHeader("Wechatpay-Timestamp");
String Nonce = request.getHeader("Wechatpay-Nonce");
String Signature = request.getHeader("Wechatpay-Signature");
String Serial = request.getHeader("Wechatpay-Serial");
//读取请求体的信息
InputStream inputStream = request.getInputStream();
StringBuffer sb = new StringBuffer();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String s;
while((s = br.readLine()) != null) {
sb.append(s);
}
String requestBody = String.valueOf(sb);
return new NotificationRequest.Builder().withSerialNumber(Serial)
.withNonce(Nonce)
.withTimestamp(Timestamp)
.withSignature(Signature)
.withBody(requestBody)
.build();
}
-----------------------华丽的分割线-----------------------
public Notification getParseResult(NotificationRequest notificationRequest) throws NotFoundException, ValidationException, ParseException {
//验签和解析请求体
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(merchantId);
NotificationHandler handler = new NotificationHandler(verifier, apiV3Key.getBytes(StandardCharsets.UTF_8));
//在解析微信支付通知请求结果时会自动验签,不通过会抛出异常
return handler.parse(notificationRequest);
}
-----------------------华丽的分割线-----------------------
public void handle(Notification notification) {
//从notification中获取解密报文
String decryptData = notification.getDecryptData();
/**
解密后的明文如下
{
"mchid": "1900000100",
"transaction_id": "1008450740201411110005820873",
"out_trade_no": "20150806125346",
"refund_id": "50200207182018070300011301001",
"out_refund_no": "7752501201407033233368018",
"refund_status": "SUCCESS",
"success_time": "2018-06-08T10:34:56+08:00",
"user_received_account": "招商银行信用卡0403",
"amount" : {
"total": 999,
"refund": 999,
"payer_total": 999,
"payer_refund": 999
}
}
*/
/**
已经得到了明文,接下来就可以按照项目逻辑进行处理
*/
//...................
//...................
}