package com.ruoyi.payment.wx.utils;
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.ruoyi.common.core.utils.WebUtils;
|
import com.ruoyi.payment.wx.model.WxPaymentInfoModel;
|
import com.ruoyi.payment.wx.model.WxPaymentRefundModel;
|
import com.ruoyi.payment.wx.resp.NotifyV3PayDecodeRespBody;
|
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
|
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
|
import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
|
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
|
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.http.client.config.RequestConfig;
|
import org.apache.http.client.methods.*;
|
import org.apache.http.conn.ConnectTimeoutException;
|
import org.apache.http.entity.StringEntity;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.util.EntityUtils;
|
import org.springframework.util.StringUtils;
|
|
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletResponse;
|
import java.io.IOException;
|
import java.io.PrintWriter;
|
import java.net.SocketTimeoutException;
|
import java.nio.charset.StandardCharsets;
|
import java.util.HashMap;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.stream.Collectors;
|
import java.util.stream.Stream;
|
|
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
|
|
/**
|
* @author xiaochen
|
* @ClassName WxWifiV3Pay
|
* @Description
|
* @date 2021-11-13 21:10
|
*/
|
@Slf4j
|
public abstract class WxAbstractPay {
|
/**
|
* 请求成功相应码
|
*/
|
private static final int STATUS_CODE = 200;
|
/**
|
* 请求成功相应码
|
*/
|
private static final int OTHER_STATUS_CODE = 204;
|
/**
|
* 请求根地址
|
*/
|
private static final String HOST = "https://api.mch.weixin.qq.com";
|
|
private static RuntimeException parameterError(String message, Object... args) {
|
message = String.format(message, args);
|
return new IllegalArgumentException("parameter error: " + message);
|
}
|
|
/**
|
* 封装基础数据
|
*
|
* @param requestBody
|
* @param notifyUrl
|
* @return
|
*/
|
protected String buildBaseParam(WxPaymentInfoModel requestBody, String notifyUrl) {
|
// 封装基础数据
|
requestBody.setNotify_url(notifyUrl);
|
String reqBody = WxJsonUtils.toJsonString(requestBody);
|
return reqBody;
|
}
|
|
/**
|
* 微信调起支付参数
|
* 返回参数如有不理解 请访问微信官方文档
|
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
|
*
|
* @param prepayId 微信下单返回的prepay_id
|
* @param appId 应用ID(appid)
|
* @return 当前调起支付所需的参数
|
* @throws Exception
|
*/
|
protected Map<String, Object> wxTuneUp(PrivateKeySigner privateKeySigner, String appId, String prepayId) {
|
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
|
String nonceStr = WxUtils.generateNonceStr();
|
String packageStr = "prepay_id=" + prepayId;
|
//加载签名
|
String signStr = Stream.of(appId, timeStamp, nonceStr, packageStr).collect(Collectors.joining("\n", "", "\n"));
|
String packageSign = privateKeySigner.sign(signStr.getBytes(StandardCharsets.UTF_8)).getSign();
|
Map<String, Object> map = new HashMap<>(6);
|
map.put("appId", appId);
|
map.put("timeStamp", timeStamp);
|
map.put("nonceStr", nonceStr);
|
map.put("package", packageStr);
|
map.put("signType", "RSA");
|
map.put("paySign", packageSign);
|
return map;
|
}
|
|
/**
|
* 构建方法请求
|
*
|
* @param uri
|
* @param socketTimeout
|
* @param connectTimeout
|
* @return
|
*/
|
protected HttpGet requestGet(String uri, int socketTimeout, int connectTimeout) {
|
//请求URL
|
HttpGet httpGet = new HttpGet(HOST + uri);
|
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
|
.setConnectTimeout(connectTimeout).build();
|
httpGet.setConfig(requestConfig);
|
httpGet.setHeader("Content-type", "application/json");
|
httpGet.setHeader("Accept", "application/json");
|
return httpGet;
|
}
|
|
/**
|
* 构建方法请求
|
*
|
* @param uri
|
* @param socketTimeout
|
* @param connectTimeout
|
* @param reqdata
|
* @return
|
*/
|
protected HttpPost requestPost(String uri, int socketTimeout, int connectTimeout, String reqdata) {
|
//请求URL
|
HttpPost httpPost = new HttpPost(HOST + uri);
|
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
|
.setConnectTimeout(connectTimeout).build();
|
httpPost.setConfig(requestConfig);
|
StringEntity entity = new StringEntity(reqdata, StandardCharsets.UTF_8);
|
entity.setContentType("application/json");
|
httpPost.setEntity(entity);
|
httpPost.setHeader("Accept", "application/json");
|
return httpPost;
|
}
|
|
public abstract <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception;
|
|
/**
|
* 接收回调
|
*
|
* @param request
|
* @return
|
* @throws Exception
|
*/
|
public <T> T verifyNotify(HttpServletRequest request, Verifier verifier, String apiKey, TypeReference<T> valueTypeRef) throws Exception {
|
String body = WxUtils.streamBodyByReceive(request);
|
String requestId = request.getHeader(REQUEST_ID);
|
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
|
String value;
|
// 验签必须参数检验
|
for (String headerName : headers) {
|
value = request.getHeader(headerName);
|
if (value == null || "".equals(value)) {
|
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
|
}
|
}
|
String serial = request.getHeader(WECHAT_PAY_SERIAL);
|
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
|
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
|
String nonce = request.getHeader(WECHAT_PAY_NONCE);
|
// 构建request,传入必要参数
|
NotificationRequest notificationRequest = new NotificationRequest.Builder().withSerialNumber(serial)
|
.withNonce(nonce)
|
.withTimestamp(timestamp)
|
.withSignature(signature)
|
.withBody(body)
|
.build();
|
NotificationHandler handler = new NotificationHandler(verifier, apiKey.getBytes(StandardCharsets.UTF_8));
|
// 验签和解析请求体
|
Notification notification = handler.parse(notificationRequest);
|
assert notification != null;
|
T respBody = WxJsonUtils.parseObject(notification.getDecryptData(), valueTypeRef);
|
return respBody;
|
}
|
|
/**
|
* 订单查询
|
*
|
* @param httpClient
|
* @param socketTimeout
|
* @param connectTimeout
|
* @param url
|
* @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody
|
* @author xiaochen
|
* @date 2021-12-20 17:12
|
*/
|
protected NotifyV3PayDecodeRespBody query(CloseableHttpClient httpClient, int socketTimeout, int connectTimeout, String url) {
|
//请求URL
|
HttpGet httpGet = requestGet(
|
url
|
, socketTimeout
|
, connectTimeout);
|
String repBody = result(httpClient, httpGet);
|
NotifyV3PayDecodeRespBody body = WxJsonUtils.parseObject(repBody, NotifyV3PayDecodeRespBody.class);
|
return body;
|
}
|
|
/**
|
* 子级实现
|
*
|
* @param out_trade_no
|
* @param mchid
|
* @return
|
*/
|
public abstract NotifyV3PayDecodeRespBody query(String out_trade_no, String mchid);
|
|
|
/**
|
* 订单退款
|
*
|
* @param refundModel
|
* @return
|
*/
|
public abstract Map<String, Object> refund(WxPaymentRefundModel refundModel);
|
|
/**
|
* 订单退款
|
*
|
* @param httpClient
|
* @param uri
|
* @param httpReadTimeoutMs
|
* @param httpConnectTimeoutMs
|
* @param refundModel
|
* @return
|
*/
|
public Map<String, Object> refund(CloseableHttpClient httpClient,
|
String uri,
|
int httpReadTimeoutMs,
|
int httpConnectTimeoutMs,
|
WxPaymentRefundModel refundModel) {
|
String reqBody = WxJsonUtils.toJsonString(refundModel);
|
//请求URL
|
HttpEntityEnclosingRequestBase httpPost = requestPost(
|
uri
|
, httpReadTimeoutMs
|
, httpConnectTimeoutMs, reqBody);
|
String repBody = result(httpClient, httpPost);
|
Map<String, Object> body = WxJsonUtils.parseObject(repBody, Map.class);
|
return body;
|
}
|
|
|
/**
|
* 请求结果
|
*
|
* @param request
|
* @return
|
*/
|
protected String result(CloseableHttpClient httpClient, HttpRequestBase request) {
|
CloseableHttpResponse response = null;
|
try {
|
response = httpClient.execute(request);
|
int statusCode = response.getStatusLine().getStatusCode();
|
String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
if (WxUtils.getLogger().isDebugEnabled()) {
|
WxUtils.debug("请求成功:{}", respBodyStr);
|
}
|
// 成功相应
|
if (STATUS_CODE == statusCode || OTHER_STATUS_CODE == statusCode) {
|
return respBodyStr;
|
} else {
|
WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr);
|
throw new RuntimeException(respBodyStr);
|
}
|
} catch (ConnectTimeoutException e) {
|
e.printStackTrace();
|
throw new RuntimeException("接口超时");
|
} catch (SocketTimeoutException e) {
|
e.printStackTrace();
|
throw new RuntimeException("读取接口数据超时");
|
} catch (IOException e) {
|
e.printStackTrace();
|
throw new RuntimeException("接口请求失败,请尝试检查网络环境或请求接口是否能正常访问");
|
} finally {
|
// 关闭响应
|
try {
|
if (response != null) {
|
//关闭结果集
|
response.getEntity().getContent().close();
|
response.close();
|
}
|
} catch (IOException e) {
|
throw new RuntimeException("关闭流异常");
|
}
|
}
|
}
|
|
/**
|
* 微信结果确认应答
|
* 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
|
*
|
* @param
|
* @throws IOException
|
*/
|
public void ack() throws IOException {
|
ack(true, null);
|
}
|
|
/**
|
* 微信结果确认应答
|
* 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
|
*
|
* @param
|
* @throws IOException
|
*/
|
public void ack(boolean ackSucc, String erroMsg) throws IOException {
|
HttpServletResponse response = WebUtils.response();
|
PrintWriter writer = response.getWriter();
|
if (ackSucc) {
|
log.info("响应微信回调成功!");
|
response.setStatus(200);
|
writer.write("{\"code\": \"SUCCESS\",\"message\": \"成功\"}");
|
|
} else {
|
log.info("响应微信回调失败:{}!", erroMsg);
|
response.setStatus(500);
|
writer.write("{\"code\": \"FAIL\",\"message\": " + (StringUtils.hasLength(erroMsg) ? erroMsg : "业务处理失败") + "}");
|
}
|
// 关闭流
|
if (Objects.nonNull(writer)) {
|
writer.close();
|
}
|
}
|
|
}
|