--微信支付

This commit is contained in:
yc13649764453
2023-05-25 09:07:17 +08:00
parent 12af476515
commit 880114cb3a
10 changed files with 537 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
package com.peanut.modules.pay.weChatPay.dto;
import com.peanut.modules.pay.weChatPay.enums.WxNotifyType;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class WeChatBasePayData {
/**
* 商品描述
*/
private String title;
/**
* 商家订单号,对应 out_trade_no
*/
private String orderId;
/**
* 订单金额
*/
private BigDecimal price;
/**
* 回调地址
*/
private WxNotifyType notify;
private Integer buyOrderId;
}

View File

@@ -0,0 +1,33 @@
package com.peanut.modules.pay.weChatPay.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class WechatDto implements Serializable {
private String orderSn;
private Integer buyOrderId;
public String getOrderSn() {
return orderSn;
}
public void setOrderSn(String orderSn) {
this.orderSn = orderSn;
}
public Integer getBuyOrderId() {
return buyOrderId;
}
public void setBuyOrderId(Integer buyOrderId) {
this.buyOrderId = buyOrderId;
}
}

View File

@@ -0,0 +1,79 @@
package com.peanut.modules.pay.weChatPay.dto;
import cn.hutool.core.date.DateUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.util.Date;
/**
* @author
* @version 1.0
* @description 微信支付退款回调返回的数据
* @date
*/
@Data
@Slf4j
public class WxchatCallbackRefundData {
/**
* 商户订单号
*/
private String orderId;
/**
* 商户退款单号,out_refund_no
*/
private String refundId;
/**
* 微信支付系统生成的订单号
*/
private String transactionId;
/**
* 微信支付系统生成的退款订单号
*/
private String transactionRefundId;
/**
* 退款渠道
* ORIGINAL原路退款
* BALANCE退回到余额
* OTHER_BALANCE原账户异常退到其他余额账户
* OTHER_BANKCARD原银行卡异常退到其他银行卡
*/
private String channel;
/**
* 退款成功时间
* 当前退款成功时才有此返回值
*/
private Date successTime;
/**
* 退款状态
* 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。
* SUCCESS退款成功
* CLOSED退款关闭
* PROCESSING退款处理中
* ABNORMAL退款异常
*/
private String status;
/**
* 退款金额
*/
private BigDecimal refundMoney;
public Date getSuccessTime() {
return successTime;
}
public void setSuccessTime(String successTime) {
// Hutool工具包的方法自动识别一些常用格式的日期字符串
this.successTime = DateUtil.parse(successTime);
}
}

View File

@@ -0,0 +1,56 @@
package com.peanut.modules.pay.weChatPay.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum WxApiType {
/**
* APP下单
*/
APP_PAY("/v3/pay/transactions/app"),
/**
* Native下单
*/
NATIVE_PAY_V2("/pay/unifiedorder"),
/**
* 查询订单
*/
ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),
/**
* 关闭订单
*/
CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),
/**
* 申请退款
*/
DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),
/**
* 查询单笔退款
*/
DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),
/**
* 申请交易账单
*/
TRADE_BILLS("/v3/bill/tradebill"),
/**
* 申请资金账单
*/
FUND_FLOW_BILLS("/v3/bill/fundflowbill");
/**
* 类型
*/
private final String type;
}

View File

@@ -0,0 +1,31 @@
package com.peanut.modules.pay.weChatPay.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum WxNotifyType {
/**
* 支付通知
* https://192.168.110.100:9100/pb/weChat/payNotify
*/
NATIVE_NOTIFY("/pay/payNotify"),
/**
* 支付通知
*/
NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),
/**
* 退款结果通知
*/
REFUND_NOTIFY("/api/wx-pay/refunds/notify");
/**
* 类型
*/
private final String type;
}

View File

@@ -0,0 +1,9 @@
package com.peanut.modules.pay.weChatPay.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.peanut.modules.book.entity.PayWechatOrderEntity;
import org.springframework.stereotype.Service;
@Service
public interface WxpayService extends IService<PayWechatOrderEntity> {
}

View File

@@ -0,0 +1,11 @@
package com.peanut.modules.pay.weChatPay.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.peanut.modules.book.dao.PayWechatOrderDao;
import com.peanut.modules.book.entity.PayWechatOrderEntity;
import com.peanut.modules.pay.weChatPay.service.WxpayService;
import org.springframework.stereotype.Service;
@Service
public class WxpayServiceImpl extends ServiceImpl<PayWechatOrderDao ,PayWechatOrderEntity> implements WxpayService {
}

View File

@@ -0,0 +1,97 @@
package com.peanut.modules.pay.weChatPay.util;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
public class WechatPayValidatorForRequest {
protected static final Logger log = LoggerFactory.getLogger(WechatPayValidatorForRequest.class);
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
protected final Verifier verifier;
protected final String body;
protected final String requestId;
public WechatPayValidatorForRequest(Verifier verifier, String body, String requestId) {
this.verifier = verifier;
this.body = body;
this.requestId = requestId;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
public final boolean validate(HttpServletRequest request) throws IOException {
try {
validateParameters(request);
String message = buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, request.getHeader(REQUEST_ID));
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
protected final void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期应答
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
}

View File

@@ -0,0 +1,42 @@
package com.peanut.modules.pay.weChatPay.util;
import com.peanut.modules.pay.weChatPay.dto.WxchatCallbackRefundData;
import java.lang.reflect.Type;
/**
* 退款处理接口,为了防止项目开发人员,不手动判断退款失败的情况
* 退款失败:退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款
*/
public interface WechatRefundCallback {
/*
* WxchatCallbackRefundData是微信支付退款回调数据的格式在退款请求完成后微信支付会将退款结果发送给商户的服务器
* 以此通知商户退款结果。refundData表示退款回调的数据包含退款结果的各种信息如退款金额、退款状态、退款时间等等。
* */
/**
* 退款成功处理情况
*/
void success(WxchatCallbackRefundData refundData);
/**
* 退款失败处理情况
*/
void find(WxchatCallbackRefundData refundData);
}

View File

@@ -0,0 +1,147 @@
package com.peanut.modules.pay.weChatPay.util;
import com.alibaba.fastjson.JSONObject;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
@Component
@Data
public class WxPayUtil {
public static final String mchId = "1612860909"; // 商户号
public static final String appId = "wx47134a8f15083734"; // appId
public static final String apiV3Key = "4aYFklzaULeGlr7oJPZ6rHWKcxjihZUF"; // apiV3秘钥
//商户私钥路径
public static final String privateKeyUrl = "C:\\Users\\Administrator\\IdeaProjects\\peanut_book\\src\\main\\resources\\cent\\apiclient_key.pem";
//平台证书路径
public static final String wechatPayCertificateUrl = "C:\\Users\\Administrator\\IdeaProjects\\peanut_book\\src\\main\\resources\\cent\\wechatpay_7B5676E3CDF56680D0414A009CE501C844DBE2D6.pem";
//第一步申请完证书后在API证书哪里点击管理证书就能看到
public static final String mchSerialNo = "679AECB2F7AC4183033F713828892BA640E4EEE3"; // 商户证书序列号
private CloseableHttpClient httpClient;
public void setup() {
// PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey);
PrivateKey merchantPrivateKey = null;
X509Certificate wechatPayCertificate = null;
try {
merchantPrivateKey = PemUtil.loadPrivateKey(
new FileInputStream(privateKeyUrl));
wechatPayCertificate = PemUtil.loadCertificate(
new FileInputStream(wechatPayCertificateUrl));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
ArrayList<X509Certificate> listCertificates = new ArrayList<>();
listCertificates.add(wechatPayCertificate);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
.withWechatPay(listCertificates);
httpClient = builder.build();
}
/**
* wxMchid商户号
* wxCertno证书编号
* wxCertPath证书地址
* wxPaternerKey v3秘钥
* url 下单地址
* body 构造好的消息体
*/
public JSONObject doPostWexinV3(String url, String body) {
if (httpClient == null) {
setup();
}
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader("Content-Type", "application/json;chartset=utf-8");
httpPost.addHeader("Accept", "application/json");
try {
if (body == null) {
throw new IllegalArgumentException("data参数不能为空");
}
StringEntity stringEntity = new StringEntity(body, "utf-8");
httpPost.setEntity(stringEntity);
// 直接执行execute方法官方会自动处理签名和验签并进行证书自动更新
HttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
if (httpResponse.getStatusLine().getStatusCode() == 200) {
String jsonResult = EntityUtils.toString(httpEntity);
return JSONObject.parseObject(jsonResult);
} else {
System.err.println("微信支付错误信息" + EntityUtils.toString(httpEntity));
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//获取签名
public String getSign(String appId, long timestamp, String nonceStr, String pack){
String message = buildMessage(appId, timestamp, nonceStr, pack);
String paySign= null;
try {
paySign = sign(message.getBytes("utf-8"));
} catch (Exception e) {
e.printStackTrace();
}
return paySign;
}
private String buildMessage(String appId, long timestamp, String nonceStr, String pack) {
return appId + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ pack + "\n";
}
private String sign(byte[] message) throws Exception{
PrivateKey merchantPrivateKey = null;
X509Certificate wechatPayCertificate = null;
try {
merchantPrivateKey = PemUtil.loadPrivateKey(
new FileInputStream(privateKeyUrl));
wechatPayCertificate = PemUtil.loadCertificate(
new FileInputStream(wechatPayCertificateUrl));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Signature sign = Signature.getInstance("SHA256withRSA");
//这里需要一个PrivateKey类型的参数就是商户的私钥。
sign.initSign(merchantPrivateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
}