--微信支付
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user