diff --git a/src/main/java/com/peanut/modules/pay/weChatPay/dto/WeChatBasePayData.java b/src/main/java/com/peanut/modules/pay/weChatPay/dto/WeChatBasePayData.java new file mode 100644 index 00000000..b929a532 --- /dev/null +++ b/src/main/java/com/peanut/modules/pay/weChatPay/dto/WeChatBasePayData.java @@ -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; + + +} diff --git a/src/main/java/com/peanut/modules/pay/weChatPay/dto/WechatDto.java b/src/main/java/com/peanut/modules/pay/weChatPay/dto/WechatDto.java new file mode 100644 index 00000000..9995a3cf --- /dev/null +++ b/src/main/java/com/peanut/modules/pay/weChatPay/dto/WechatDto.java @@ -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; + } +} diff --git a/src/main/java/com/peanut/modules/pay/weChatPay/dto/WxchatCallbackRefundData.java b/src/main/java/com/peanut/modules/pay/weChatPay/dto/WxchatCallbackRefundData.java new file mode 100644 index 00000000..a6f1cc39 --- /dev/null +++ b/src/main/java/com/peanut/modules/pay/weChatPay/dto/WxchatCallbackRefundData.java @@ -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); + } + +} diff --git a/src/main/java/com/peanut/modules/pay/weChatPay/enums/WxApiType.java b/src/main/java/com/peanut/modules/pay/weChatPay/enums/WxApiType.java new file mode 100644 index 00000000..7bf99ce6 --- /dev/null +++ b/src/main/java/com/peanut/modules/pay/weChatPay/enums/WxApiType.java @@ -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; +} diff --git a/src/main/java/com/peanut/modules/pay/weChatPay/enums/WxNotifyType.java b/src/main/java/com/peanut/modules/pay/weChatPay/enums/WxNotifyType.java new file mode 100644 index 00000000..e9ecbec7 --- /dev/null +++ b/src/main/java/com/peanut/modules/pay/weChatPay/enums/WxNotifyType.java @@ -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; +} diff --git a/src/main/java/com/peanut/modules/pay/weChatPay/service/WxpayService.java b/src/main/java/com/peanut/modules/pay/weChatPay/service/WxpayService.java new file mode 100644 index 00000000..4d7e91d8 --- /dev/null +++ b/src/main/java/com/peanut/modules/pay/weChatPay/service/WxpayService.java @@ -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 { +} diff --git a/src/main/java/com/peanut/modules/pay/weChatPay/service/impl/WxpayServiceImpl.java b/src/main/java/com/peanut/modules/pay/weChatPay/service/impl/WxpayServiceImpl.java new file mode 100644 index 00000000..58e53416 --- /dev/null +++ b/src/main/java/com/peanut/modules/pay/weChatPay/service/impl/WxpayServiceImpl.java @@ -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 implements WxpayService { +} diff --git a/src/main/java/com/peanut/modules/pay/weChatPay/util/WechatPayValidatorForRequest.java b/src/main/java/com/peanut/modules/pay/weChatPay/util/WechatPayValidatorForRequest.java new file mode 100644 index 00000000..d57b3b05 --- /dev/null +++ b/src/main/java/com/peanut/modules/pay/weChatPay/util/WechatPayValidatorForRequest.java @@ -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"; + } + +} diff --git a/src/main/java/com/peanut/modules/pay/weChatPay/util/WechatRefundCallback.java b/src/main/java/com/peanut/modules/pay/weChatPay/util/WechatRefundCallback.java new file mode 100644 index 00000000..e8f7a0d9 --- /dev/null +++ b/src/main/java/com/peanut/modules/pay/weChatPay/util/WechatRefundCallback.java @@ -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); + + + + + + +} diff --git a/src/main/java/com/peanut/modules/pay/weChatPay/util/WxPayUtil.java b/src/main/java/com/peanut/modules/pay/weChatPay/util/WxPayUtil.java new file mode 100644 index 00000000..ad7e3020 --- /dev/null +++ b/src/main/java/com/peanut/modules/pay/weChatPay/util/WxPayUtil.java @@ -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 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()); + } + } + +