From 0e2e91c0f48127254c4ae6af20b413933166321c Mon Sep 17 00:00:00 2001 From: zenghongyi <277382367@qq.com> Date: Tue, 28 Jun 2022 17:09:22 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E7=AD=BE=E5=90=8D=E9=AA=8C=E8=AF=81=E5=99=A8?= =?UTF-8?q?=E5=92=8CHttpClient=20API=E5=AD=97=E5=85=B8=E5=92=8C=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=B1=BB=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- payment-demo/pom.xml | 5 + .../paymentdemo/config/WxPayConfig.java | 46 ++++- .../controller/ProductController.java | 1 + .../hongyi/paymentdemo/enums/OrderStatus.java | 49 +++++ .../com/hongyi/paymentdemo/enums/PayType.java | 24 +++ .../paymentdemo/enums/wxpay/WxApiType.java | 55 ++++++ .../paymentdemo/enums/wxpay/WxNotifyType.java | 30 ++++ .../enums/wxpay/WxRefundStatus.java | 34 ++++ .../paymentdemo/enums/wxpay/WxTradeState.java | 34 ++++ .../paymentdemo/util/HttpClientUtils.java | 168 ++++++++++++++++++ .../hongyi/paymentdemo/util/HttpUtils.java | 39 ++++ .../hongyi/paymentdemo/util/OrderNoUtils.java | 46 +++++ .../util/WechatPay2ValidatorForRequest.java | 114 ++++++++++++ .../PaymentDemoApplicationTests.java | 6 +- 14 files changed, 647 insertions(+), 4 deletions(-) create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/enums/OrderStatus.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/enums/PayType.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxApiType.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxNotifyType.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxRefundStatus.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxTradeState.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/util/HttpClientUtils.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/util/HttpUtils.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/util/OrderNoUtils.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/util/WechatPay2ValidatorForRequest.java diff --git a/payment-demo/pom.xml b/payment-demo/pom.xml index 4e98468..aabdd14 100644 --- a/payment-demo/pom.xml +++ b/payment-demo/pom.xml @@ -61,6 +61,11 @@ 0.3.0 + + com.google.code.gson + gson + + org.springframework.boot spring-boot-starter-test diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java index c8b29c5..3509f37 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java @@ -1,13 +1,21 @@ package com.hongyi.paymentdemo.config; +import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; +import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; +import com.wechat.pay.contrib.apache.httpclient.auth.ScheduledUpdateCertificatesVerifier; +import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; +import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; import lombok.Data; +import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.nio.charset.StandardCharsets; import java.security.PrivateKey; @@ -43,7 +51,7 @@ public class WxPayConfig { * @param filename * @return */ - public PrivateKey getPrivateKey(String filename) { + private PrivateKey getPrivateKey(String filename) { try { return PemUtil.loadPrivateKey(new FileInputStream(filename)); } catch (FileNotFoundException e) { @@ -51,4 +59,40 @@ public class WxPayConfig { } } + /** + * 获取签名验证器 + * @return + */ + @Bean + public ScheduledUpdateCertificatesVerifier getVerifier() { + // 获取商户私钥 + PrivateKey privateKey = getPrivateKey(privateKeyPath); + // 私钥签名 + PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey); + // 身份认证对象 + WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); + // 使用定时更新的签名验证器,不需要传入证书 + ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier( + wechatPay2Credentials, + apiV3Key.getBytes(StandardCharsets.UTF_8)); + return verifier; + } + + /** + * 获取http请求对象 + * @param verifier + * @return + */ + @Bean + public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) { + // 获取商户私钥 + PrivateKey privateKey = getPrivateKey(privateKeyPath); + WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() + .withMerchant(mchId, mchSerialNo, privateKey) + .withValidator(new WechatPay2Validator(verifier)); + // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 + CloseableHttpClient httpClient = builder.build(); + return httpClient; + } + } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/ProductController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/ProductController.java index 39b4b96..128557c 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/ProductController.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/ProductController.java @@ -19,6 +19,7 @@ import java.util.List; * @Date 2022/5/31 18:18 * @Version 1.0 */ + @Api(tags = "商品管理") @RestController @RequestMapping("/api/product") diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/OrderStatus.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/OrderStatus.java new file mode 100644 index 0000000..757401c --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/OrderStatus.java @@ -0,0 +1,49 @@ +package com.hongyi.paymentdemo.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum OrderStatus { + /** + * 未支付 + */ + NOTPAY("未支付"), + + + /** + * 支付成功 + */ + SUCCESS("支付成功"), + + /** + * 已关闭 + */ + CLOSED("超时已关闭"), + + /** + * 已取消 + */ + CANCEL("用户已取消"), + + /** + * 退款中 + */ + REFUND_PROCESSING("退款中"), + + /** + * 已退款 + */ + REFUND_SUCCESS("已退款"), + + /** + * 退款异常 + */ + REFUND_ABNORMAL("退款异常"); + + /** + * 类型 + */ + private final String type; +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/PayType.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/PayType.java new file mode 100644 index 0000000..b75fa3f --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/PayType.java @@ -0,0 +1,24 @@ +package com.hongyi.paymentdemo.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum PayType { + /** + * 微信 + */ + WXPAY("微信"), + + + /** + * 支付宝 + */ + ALIPAY("支付宝"); + + /** + * 类型 + */ + private final String type; +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxApiType.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxApiType.java new file mode 100644 index 0000000..31b4fe8 --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxApiType.java @@ -0,0 +1,55 @@ +package com.hongyi.paymentdemo.enums.wxpay; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum WxApiType { + + /** + * Native下单 + */ + NATIVE_PAY("/v3/pay/transactions/native"), + + /** + * 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/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxNotifyType.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxNotifyType.java new file mode 100644 index 0000000..5980fc0 --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxNotifyType.java @@ -0,0 +1,30 @@ +package com.hongyi.paymentdemo.enums.wxpay; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum WxNotifyType { + + /** + * 支付通知 + */ + NATIVE_NOTIFY("/api/wx-pay/native/notify"), + + /** + * 支付通知 + */ + NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"), + + + /** + * 退款结果通知 + */ + REFUND_NOTIFY("/api/wx-pay/refunds/notify"); + + /** + * 类型 + */ + private final String type; +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxRefundStatus.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxRefundStatus.java new file mode 100644 index 0000000..9709f02 --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxRefundStatus.java @@ -0,0 +1,34 @@ +package com.hongyi.paymentdemo.enums.wxpay; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum WxRefundStatus { + + /** + * 退款成功 + */ + SUCCESS("SUCCESS"), + + /** + * 退款关闭 + */ + CLOSED("CLOSED"), + + /** + * 退款处理中 + */ + PROCESSING("PROCESSING"), + + /** + * 退款异常 + */ + ABNORMAL("ABNORMAL"); + + /** + * 类型 + */ + private final String type; +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxTradeState.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxTradeState.java new file mode 100644 index 0000000..f711340 --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/wxpay/WxTradeState.java @@ -0,0 +1,34 @@ +package com.hongyi.paymentdemo.enums.wxpay; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum WxTradeState { + + /** + * 支付成功 + */ + SUCCESS("SUCCESS"), + + /** + * 未支付 + */ + NOTPAY("NOTPAY"), + + /** + * 已关闭 + */ + CLOSED("CLOSED"), + + /** + * 转入退款 + */ + REFUND("REFUND"); + + /** + * 类型 + */ + private final String type; +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/util/HttpClientUtils.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/util/HttpClientUtils.java new file mode 100644 index 0000000..3a4d735 --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/util/HttpClientUtils.java @@ -0,0 +1,168 @@ +package com.hongyi.paymentdemo.util; + +import org.apache.http.Consts; +import org.apache.http.HttpEntity; +import org.apache.http.NameValuePair; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.*; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContextBuilder; +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.text.ParseException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * http请求客户端 + */ +public class HttpClientUtils { + private String url; + private Map param; + private int statusCode; + private String content; + private String xmlParam; + private boolean isHttps; + + public boolean isHttps() { + return isHttps; + } + + public void setHttps(boolean isHttps) { + this.isHttps = isHttps; + } + + public String getXmlParam() { + return xmlParam; + } + + public void setXmlParam(String xmlParam) { + this.xmlParam = xmlParam; + } + + public HttpClientUtils(String url, Map param) { + this.url = url; + this.param = param; + } + + public HttpClientUtils(String url) { + this.url = url; + } + + public void setParameter(Map map) { + param = map; + } + + public void addParameter(String key, String value) { + if (param == null) + param = new HashMap(); + param.put(key, value); + } + + public void post() throws ClientProtocolException, IOException { + HttpPost http = new HttpPost(url); + setEntity(http); + execute(http); + } + + public void put() throws ClientProtocolException, IOException { + HttpPut http = new HttpPut(url); + setEntity(http); + execute(http); + } + + public void get() throws ClientProtocolException, IOException { + if (param != null) { + StringBuilder url = new StringBuilder(this.url); + boolean isFirst = true; + for (String key : param.keySet()) { + if (isFirst) { + url.append("?"); + isFirst = false; + }else { + url.append("&"); + } + url.append(key).append("=").append(param.get(key)); + } + this.url = url.toString(); + } + HttpGet http = new HttpGet(url); + execute(http); + } + + /** + * set http post,put param + */ + private void setEntity(HttpEntityEnclosingRequestBase http) { + if (param != null) { + List nvps = new LinkedList(); + for (String key : param.keySet()) + nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数 + http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数 + } + if (xmlParam != null) { + http.setEntity(new StringEntity(xmlParam, Consts.UTF_8)); + } + } + + private void execute(HttpUriRequest http) throws ClientProtocolException, + IOException { + CloseableHttpClient httpClient = null; + try { + if (isHttps) { + SSLContext sslContext = new SSLContextBuilder() + .loadTrustMaterial(null, new TrustStrategy() { + // 信任所有 + public boolean isTrusted(X509Certificate[] chain, + String authType) + throws CertificateException { + return true; + } + }).build(); + SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( + sslContext); + httpClient = HttpClients.custom().setSSLSocketFactory(sslsf) + .build(); + } else { + httpClient = HttpClients.createDefault(); + } + CloseableHttpResponse response = httpClient.execute(http); + try { + if (response != null) { + if (response.getStatusLine() != null) + statusCode = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + // 响应内容 + content = EntityUtils.toString(entity, Consts.UTF_8); + } + } finally { + response.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + httpClient.close(); + } + } + + public int getStatusCode() { + return statusCode; + } + + public String getContent() throws ParseException, IOException { + return content; + } + +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/util/HttpUtils.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/util/HttpUtils.java new file mode 100644 index 0000000..dfdcacb --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/util/HttpUtils.java @@ -0,0 +1,39 @@ +package com.hongyi.paymentdemo.util; + +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; + + +public class HttpUtils { + + /** + * 将通知参数转化为字符串 + * @param request + * @return + */ + public static String readData(HttpServletRequest request) { + BufferedReader br = null; + try { + StringBuilder result = new StringBuilder(); + br = request.getReader(); + for (String line; (line = br.readLine()) != null; ) { + if (result.length() > 0) { + result.append("\n"); + } + result.append(line); + } + return result.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} \ No newline at end of file diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/util/OrderNoUtils.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/util/OrderNoUtils.java new file mode 100644 index 0000000..18542e0 --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/util/OrderNoUtils.java @@ -0,0 +1,46 @@ +package com.hongyi.paymentdemo.util; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Random; + +/** + * 订单号工具类 + * + * @author qy + * @since 1.0 + */ +public class OrderNoUtils { + + /** + * 获取订单编号 + * @return + */ + public static String getOrderNo() { + return "ORDER_" + getNo(); + } + + /** + * 获取退款单编号 + * @return + */ + public static String getRefundNo() { + return "REFUND_" + getNo(); + } + + /** + * 获取编号 + * @return + */ + public static String getNo() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + String newDate = sdf.format(new Date()); + String result = ""; + Random random = new Random(); + for (int i = 0; i < 3; i++) { + result += random.nextInt(10); + } + return newDate + result; + } + +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/util/WechatPay2ValidatorForRequest.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/util/WechatPay2ValidatorForRequest.java new file mode 100644 index 0000000..311b86b --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/util/WechatPay2ValidatorForRequest.java @@ -0,0 +1,114 @@ +package com.hongyi.paymentdemo.util; + + +import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; +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.*; + +/** + * @author xy-peng + */ +public class WechatPay2ValidatorForRequest { + + protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class); + /** + * 应答超时时间,单位为分钟 + */ + protected static final long RESPONSE_EXPIRED_MINUTES = 5; + protected final Verifier verifier; + protected final String requestId; + protected final String body; + + + public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) { + this.verifier = verifier; + this.requestId = requestId; + this.body = body; + } + + 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, requestId); + } + } 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"; + } + + protected final String getResponseBody(CloseableHttpResponse response) throws IOException { + HttpEntity entity = response.getEntity(); + return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : ""; + } + +} diff --git a/payment-demo/src/test/java/com/hongyi/paymentdemo/PaymentDemoApplicationTests.java b/payment-demo/src/test/java/com/hongyi/paymentdemo/PaymentDemoApplicationTests.java index 4dd25fa..4fe1379 100644 --- a/payment-demo/src/test/java/com/hongyi/paymentdemo/PaymentDemoApplicationTests.java +++ b/payment-demo/src/test/java/com/hongyi/paymentdemo/PaymentDemoApplicationTests.java @@ -15,9 +15,9 @@ class PaymentDemoApplicationTests { @Test void testGetPrivateKey() { - String privateKeyPath = wxPayConfig.getPrivateKeyPath(); - PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath); - System.out.println(privateKey); +// String privateKeyPath = wxPayConfig.getPrivateKeyPath(); +// PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath); +// System.out.println(privateKey); } } -- Gitee From 9f13cb2c214a3cd893b8fb64ff357c34015f377c Mon Sep 17 00:00:00 2001 From: zenghongyi <277382367@qq.com> Date: Tue, 28 Jun 2022 20:50:49 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=B8=8B=E5=8D=95api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- payment-demo/pom.xml | 5 + .../paymentdemo/config/WxPayConfig.java | 2 +- .../controller/WxPayController.java | 37 ++++++ .../paymentdemo/service/WxPayService.java | 13 ++ .../service/impl/WxPayServiceImpl.java | 113 ++++++++++++++++++ .../java/com/hongyi/paymentdemo/vo/R.java | 2 + .../src/main/resources/wxpay.properties | 2 +- 7 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java diff --git a/payment-demo/pom.xml b/payment-demo/pom.xml index aabdd14..00d8f76 100644 --- a/payment-demo/pom.xml +++ b/payment-demo/pom.xml @@ -61,6 +61,11 @@ 0.3.0 + + org.springframework.boot + spring-boot-devtools + + com.google.code.gson gson diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java index 3509f37..e5200a7 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java @@ -38,7 +38,7 @@ public class WxPayConfig { private String apiV3Key; // APPID - private String appid; + private String appId; // 微信服务器地址 private String domain; diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java new file mode 100644 index 0000000..6c988cb --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java @@ -0,0 +1,37 @@ +package com.hongyi.paymentdemo.controller; + +import com.hongyi.paymentdemo.service.WxPayService; +import com.hongyi.paymentdemo.vo.R; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * @Author Kisugi Takumi + * @Date 2022/6/28 17:18 + * @Version 1.0 + */ +@CrossOrigin +@RestController +@RequestMapping("/api/wx-pay") +@Api(tags = "网站微信支付api") +@Slf4j +public class WxPayController { + + @Resource // 这个是java规范的注解,Spring的为@Autowired + private WxPayService wxPayService; + + @ApiOperation("调用统一下单API,生成支付二维码") + @PostMapping("/native/{productId}") + public R nativePay(@PathVariable Long productId) throws Exception { + log.info("发起支付请求"); + // 返回支付二维码连接和订单号 + Map map = wxPayService.nativePay(productId); + return R.ok().setData(map); + } +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java new file mode 100644 index 0000000..b09ad7c --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java @@ -0,0 +1,13 @@ +package com.hongyi.paymentdemo.service; + +import java.io.IOException; +import java.util.Map; + +/** + * @Author Kisugi Takumi + * @Date 2022/6/28 17:19 + * @Version 1.0 + */ +public interface WxPayService { + Map nativePay(Long productId) throws Exception; +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java new file mode 100644 index 0000000..d0fb8f4 --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java @@ -0,0 +1,113 @@ +package com.hongyi.paymentdemo.service.impl; + +import com.google.gson.Gson; +import com.hongyi.paymentdemo.config.WxPayConfig; +import com.hongyi.paymentdemo.entity.OrderInfo; +import com.hongyi.paymentdemo.enums.OrderStatus; +import com.hongyi.paymentdemo.enums.wxpay.WxApiType; +import com.hongyi.paymentdemo.enums.wxpay.WxNotifyType; +import com.hongyi.paymentdemo.service.WxPayService; +import com.hongyi.paymentdemo.util.OrderNoUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.client.methods.CloseableHttpResponse; +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.Service; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @Author Kisugi Takumi + * @Date 2022/6/28 17:19 + * @Version 1.0 + */ +@Service +@Slf4j +public class WxPayServiceImpl implements WxPayService { + + @Resource + private WxPayConfig wxPayConfig; + + @Resource + private CloseableHttpClient httpClient; + + /** + * 创建订单,调用native支付接口 + * @param productId 购买产品的id + * @return code_url和订单号 + * @throws Exception + */ + @Override + public Map nativePay(Long productId) throws Exception { + log.info("生成订单"); + // 生成订单 + OrderInfo orderInfo = new OrderInfo(); + orderInfo.setTitle("test"); + orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); // 订单号 + orderInfo.setProductId(productId); + orderInfo.setTotalFee(99); // 价格,单位为分 + orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); + // TODO: 存入数据库 + + log.info("调用统一下单API"); + // 调用统一下单API,构造Post请求 + HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType())); + + // 请求body参数 + Gson gson = new Gson(); + Map paramsMap = new HashMap<>(); + paramsMap.put("appid", wxPayConfig.getAppId()); + paramsMap.put("mchid", wxPayConfig.getMchId()); + paramsMap.put("description", orderInfo.getTitle()); + paramsMap.put("out_trade_no", orderInfo.getOrderNo()); + paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType())); + // 注意查阅文档得知,amount为嵌套结构 + Map amountMap = new HashMap<>(); + amountMap.put("total", orderInfo.getTotalFee()); + amountMap.put("currency", "CNY"); + paramsMap.put("amount", amountMap); + // 将Map对象转换为json + String jsonParams = gson.toJson(paramsMap); + log.info("请求参数: " + jsonParams); + + // 将请求参数放入请求体中,设置请求和响应类型为json + StringEntity entity = new StringEntity(jsonParams,"utf-8"); + entity.setContentType("application/json"); + httpPost.setEntity(entity); + httpPost.setHeader("Accept", "application/json"); + + // 完成签名并执行请求 + CloseableHttpResponse response = httpClient.execute(httpPost); + + try { + // 获取响应体 + String bodyAsString = EntityUtils.toString(response.getEntity()); + // 获取响应状态码 + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200) { //处理成功 + log.info("success,return body = " + bodyAsString); + } else if (statusCode == 204) { //处理成功,无返回Body + log.info("success"); + } else { + log.info("failed,resp code = " + statusCode+ ",return body = " + bodyAsString); + throw new IOException("request failed"); + } + // 响应结果 + HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class); + // 二维码 + String codeUrl = resultMap.get("code_url"); + HashMap map = new HashMap<>(); + map.put("codeUrl", codeUrl); + // 订单编号 + map.put("orderNo", orderInfo.getOrderNo()); + return map; + } finally { + response.close(); + } + } +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/vo/R.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/vo/R.java index d92595d..01a013d 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/vo/R.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/vo/R.java @@ -1,6 +1,7 @@ package com.hongyi.paymentdemo.vo; import lombok.Data; +import lombok.experimental.Accessors; import java.util.HashMap; import java.util.Map; @@ -11,6 +12,7 @@ import java.util.Map; * @Version 1.0 */ @Data // 生成get和set方法 +@Accessors(chain = true) public class R { // 响应码 private Integer code; diff --git a/payment-demo/src/main/resources/wxpay.properties b/payment-demo/src/main/resources/wxpay.properties index 8fad36c..c7bbe01 100644 --- a/payment-demo/src/main/resources/wxpay.properties +++ b/payment-demo/src/main/resources/wxpay.properties @@ -8,7 +8,7 @@ wxpay.private-key-path=apiclient_key.pem # APIv3密钥 wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B # APPID -wxpay.appid=wx74862e0dfcf69954 +wxpay.app-id=wx74862e0dfcf69954 # 微信服务器地址 wxpay.domain=https://api.mch.weixin.qq.com # 接收结果通知地址 -- Gitee From 5e87d578f053a6186e5517459d5a4443d5936667 Mon Sep 17 00:00:00 2001 From: zenghongyi <277382367@qq.com> Date: Wed, 29 Jun 2022 15:28:37 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E8=AE=A2=E5=8D=95=20=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E4=BA=8C=E7=BB=B4=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../paymentdemo/config/WxPayConfig.java | 10 ++-- .../paymentdemo/service/OrderInfoService.java | 12 ++++ .../service/impl/OrderInfoServiceImpl.java | 56 +++++++++++++++++++ .../service/impl/WxPayServiceImpl.java | 30 +++++++--- .../src/main/resources/application.yml | 5 +- 5 files changed, 99 insertions(+), 14 deletions(-) diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java index e5200a7..354bcca 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java @@ -7,6 +7,7 @@ import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -23,6 +24,7 @@ import java.security.PrivateKey; @PropertySource("classpath:wxpay.properties") //读取配置文件 @ConfigurationProperties(prefix="wxpay") //读取wxpay节点 @Data //使用set方法将wxpay节点中的值填充到当前类的属性中 +@Slf4j public class WxPayConfig { // 商户号 @@ -65,6 +67,7 @@ public class WxPayConfig { */ @Bean public ScheduledUpdateCertificatesVerifier getVerifier() { + log.info("获取签名验证器"); // 获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); // 私钥签名 @@ -72,10 +75,9 @@ public class WxPayConfig { // 身份认证对象 WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); // 使用定时更新的签名验证器,不需要传入证书 - ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier( + return new ScheduledUpdateCertificatesVerifier( wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8)); - return verifier; } /** @@ -85,14 +87,14 @@ public class WxPayConfig { */ @Bean public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) { + log.info("获取HttpClient"); // 获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator(verifier)); // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 - CloseableHttpClient httpClient = builder.build(); - return httpClient; + return builder.build(); } } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java index cd06466..1b7794c 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java @@ -4,5 +4,17 @@ import com.hongyi.paymentdemo.entity.OrderInfo; import com.baomidou.mybatisplus.extension.service.IService; public interface OrderInfoService extends IService { + /** + * 根据商品id生成订单 + * @param productId + * @return + */ + OrderInfo createOrderByProductById(Long productId); + /** + * 根据订单编号存储二维码地址 + * @param orderNo + * @param codeUrl + */ + void saveCodeUrl(String orderNo, String codeUrl); } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java index c93e860..6c39492 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java @@ -1,12 +1,68 @@ package com.hongyi.paymentdemo.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.hongyi.paymentdemo.entity.OrderInfo; +import com.hongyi.paymentdemo.entity.Product; +import com.hongyi.paymentdemo.enums.OrderStatus; import com.hongyi.paymentdemo.mapper.OrderInfoMapper; +import com.hongyi.paymentdemo.mapper.ProductMapper; import com.hongyi.paymentdemo.service.OrderInfoService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.hongyi.paymentdemo.util.OrderNoUtils; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import javax.annotation.Resource; + @Service +@Slf4j public class OrderInfoServiceImpl extends ServiceImpl implements OrderInfoService { + @Resource + private ProductMapper productMapper; + + @Resource + private OrderInfoMapper orderInfoMapper; + + @Override + public OrderInfo createOrderByProductById(Long productId) { + // 查找已存在但未支付的订单 + OrderInfo orderInfo = this.getNoPayOrderByProductById(productId); + if (orderInfo != null) { + return orderInfo; + } + // 获取商品信息 + Product product = productMapper.selectById(productId); + // 生成订单 + orderInfo = new OrderInfo(); + orderInfo.setTitle(product.getTitle()); + orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); // 订单号 + orderInfo.setProductId(productId); + orderInfo.setTotalFee(product.getPrice()); // 价格,单位为分 + orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); + orderInfoMapper.insert(orderInfo); + return orderInfo; + } + + @Override + public void saveCodeUrl(String orderNo, String codeUrl) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("order_no", orderNo); + OrderInfo orderInfo = new OrderInfo(); + orderInfo.setCodeUrl(codeUrl); + orderInfoMapper.update(orderInfo, queryWrapper); + } + + /** + * 根据商品id查询未支付的订单 + * @param productId + * @return + */ + private OrderInfo getNoPayOrderByProductById(Long productId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("product_id", productId) + .eq("order_status", OrderStatus.NOTPAY.getType()); + return orderInfoMapper.selectOne(queryWrapper); + } + } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java index d0fb8f4..5d20638 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java @@ -6,6 +6,7 @@ import com.hongyi.paymentdemo.entity.OrderInfo; import com.hongyi.paymentdemo.enums.OrderStatus; import com.hongyi.paymentdemo.enums.wxpay.WxApiType; import com.hongyi.paymentdemo.enums.wxpay.WxNotifyType; +import com.hongyi.paymentdemo.service.OrderInfoService; import com.hongyi.paymentdemo.service.WxPayService; import com.hongyi.paymentdemo.util.OrderNoUtils; import lombok.extern.slf4j.Slf4j; @@ -15,6 +16,7 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.io.IOException; @@ -36,6 +38,9 @@ public class WxPayServiceImpl implements WxPayService { @Resource private CloseableHttpClient httpClient; + @Resource + private OrderInfoService orderInfoService; + /** * 创建订单,调用native支付接口 * @param productId 购买产品的id @@ -45,14 +50,19 @@ public class WxPayServiceImpl implements WxPayService { @Override public Map nativePay(Long productId) throws Exception { log.info("生成订单"); + // 生成订单 - OrderInfo orderInfo = new OrderInfo(); - orderInfo.setTitle("test"); - orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); // 订单号 - orderInfo.setProductId(productId); - orderInfo.setTotalFee(99); // 价格,单位为分 - orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); - // TODO: 存入数据库 + OrderInfo orderInfo = orderInfoService.createOrderByProductById(productId); + String codeUrl = orderInfo.getCodeUrl(); + if (orderInfo != null && !StringUtils.isEmpty(codeUrl)) { + log.info("订单已存在,二维码已保存"); + // 返回二维码 + HashMap map = new HashMap<>(); + map.put("codeUrl", codeUrl); + // 订单编号 + map.put("orderNo", orderInfo.getOrderNo()); + return map; + } log.info("调用统一下单API"); // 调用统一下单API,构造Post请求 @@ -100,7 +110,11 @@ public class WxPayServiceImpl implements WxPayService { // 响应结果 HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class); // 二维码 - String codeUrl = resultMap.get("code_url"); + codeUrl = resultMap.get("code_url"); + // 保存二维码 + String orderNo = orderInfo.getOrderNo(); + orderInfoService.saveCodeUrl(orderNo, codeUrl); + // 返回二维码 HashMap map = new HashMap<>(); map.put("codeUrl", codeUrl); // 订单编号 diff --git a/payment-demo/src/main/resources/application.yml b/payment-demo/src/main/resources/application.yml index 19090ad..fd60c70 100644 --- a/payment-demo/src/main/resources/application.yml +++ b/payment-demo/src/main/resources/application.yml @@ -15,5 +15,6 @@ mybatis-plus: configuration: #sql日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:com/hongyi/paymentdemo/mapper/xml/*.xml - - +#logging: +# level: +# root: debug -- Gitee From e35a89d73e61fcfd4110a5a97fb7aea955562838 Mon Sep 17 00:00:00 2001 From: zenghongyi <277382367@qq.com> Date: Wed, 29 Jun 2022 17:33:59 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E9=80=9A=E7=9F=A5api=20=E5=86=85=E7=BD=91?= =?UTF-8?q?=E7=A9=BF=E9=80=8F=20=E9=80=9A=E7=9F=A5=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/OrderInfoController.java | 33 +++++++++++++++++++ .../controller/WxPayController.java | 27 +++++++++++++++ .../paymentdemo/service/OrderInfoService.java | 8 +++++ .../service/impl/OrderInfoServiceImpl.java | 8 +++++ .../src/main/resources/wxpay.properties | 3 +- 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/controller/OrderInfoController.java diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/OrderInfoController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/OrderInfoController.java new file mode 100644 index 0000000..afe8b32 --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/OrderInfoController.java @@ -0,0 +1,33 @@ +package com.hongyi.paymentdemo.controller; + +import com.hongyi.paymentdemo.entity.OrderInfo; +import com.hongyi.paymentdemo.service.OrderInfoService; +import com.hongyi.paymentdemo.vo.R; +import io.swagger.annotations.Api; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @Author Kisugi Takumi + * @Date 2022/6/29 15:33 + * @Version 1.0 + */ +@Api(tags = "商品订单管理") +@RestController +@CrossOrigin +@RequestMapping("/api/order-info") +public class OrderInfoController { + @Resource + private OrderInfoService orderInfoService; + + @GetMapping("/list") + public R list() { + List list = orderInfoService.listOrderByCreateTimeDesc(); + return R.ok().data("list", list); + } +} diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java index 6c988cb..e03be91 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java @@ -1,6 +1,8 @@ package com.hongyi.paymentdemo.controller; +import com.google.gson.Gson; import com.hongyi.paymentdemo.service.WxPayService; +import com.hongyi.paymentdemo.util.HttpUtils; import com.hongyi.paymentdemo.vo.R; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -9,6 +11,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; import java.util.Map; /** @@ -34,4 +39,26 @@ public class WxPayController { Map map = wxPayService.nativePay(productId); return R.ok().setData(map); } + + @PostMapping("/native/notify") + public String nativeNotify(HttpServletRequest request, HttpServletResponse response) { + Gson gson = new Gson(); + // 应答对象 + Map map = new HashMap<>(); + // 处理通知参数 + String body = HttpUtils.readData(request); + Map bodyMap = gson.fromJson(body, HashMap.class); + log.info("支付通知的id ====> {}", bodyMap.get("id")); + log.info("支付通知完整的数据 ====> {}", body); + Object id = bodyMap.get("id"); + // TODO: 签名的验证 + + // TODO: 处理订单 + + // 成功应答 + response.setStatus(200); + map.put("code", "SUCCESS"); + map.put("message", "成功"); + return gson.toJson(map); + } } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java index 1b7794c..ae670d5 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java @@ -3,6 +3,8 @@ package com.hongyi.paymentdemo.service; import com.hongyi.paymentdemo.entity.OrderInfo; import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + public interface OrderInfoService extends IService { /** * 根据商品id生成订单 @@ -17,4 +19,10 @@ public interface OrderInfoService extends IService { * @param codeUrl */ void saveCodeUrl(String orderNo, String codeUrl); + + /** + * 查询订单列表,并按时间倒序查询 + * @return + */ + List listOrderByCreateTimeDesc(); } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java index 6c39492..41ff390 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java @@ -13,6 +13,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; +import java.util.List; @Service @Slf4j @@ -53,6 +54,13 @@ public class OrderInfoServiceImpl extends ServiceImpl listOrderByCreateTimeDesc() { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.orderByDesc("create_time"); + return orderInfoMapper.selectList(queryWrapper); + } + /** * 根据商品id查询未支付的订单 * @param productId diff --git a/payment-demo/src/main/resources/wxpay.properties b/payment-demo/src/main/resources/wxpay.properties index c7bbe01..dbae34f 100644 --- a/payment-demo/src/main/resources/wxpay.properties +++ b/payment-demo/src/main/resources/wxpay.properties @@ -12,4 +12,5 @@ wxpay.app-id=wx74862e0dfcf69954 # 微信服务器地址 wxpay.domain=https://api.mch.weixin.qq.com # 接收结果通知地址 -wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io +# 注意:每次重新启动ngrok,都需要修改这个配置 +wxpay.notify-domain=https://c257-117-174-85-8.ap.ngrok.io -- Gitee From ca481af37145a53845b36e9c6447080406f03ecc Mon Sep 17 00:00:00 2001 From: zenghongyi <277382367@qq.com> Date: Fri, 1 Jul 2022 16:17:11 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E9=80=9A=E7=9F=A5=20=E9=AA=8C=E7=AD=BE=20?= =?UTF-8?q?=E8=A7=A3=E5=AF=86=20=E5=A4=84=E7=90=86=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WxPayController.java | 45 ++++++++++++++---- .../paymentdemo/service/OrderInfoService.java | 8 ++++ .../service/PaymentInfoService.java | 8 +++- .../paymentdemo/service/WxPayService.java | 3 ++ .../service/impl/OrderInfoServiceImpl.java | 10 ++++ .../service/impl/PaymentInfoServiceImpl.java | 40 ++++++++++++++++ .../service/impl/WxPayServiceImpl.java | 46 +++++++++++++++++++ .../src/main/resources/wxpay.properties | 2 +- 8 files changed, 151 insertions(+), 11 deletions(-) diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java index e03be91..b81aa2e 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java @@ -3,7 +3,9 @@ package com.hongyi.paymentdemo.controller; import com.google.gson.Gson; import com.hongyi.paymentdemo.service.WxPayService; import com.hongyi.paymentdemo.util.HttpUtils; +import com.hongyi.paymentdemo.util.WechatPay2ValidatorForRequest; import com.hongyi.paymentdemo.vo.R; +import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @@ -13,6 +15,8 @@ import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; @@ -31,6 +35,9 @@ public class WxPayController { @Resource // 这个是java规范的注解,Spring的为@Autowired private WxPayService wxPayService; + @Resource + private Verifier verifier; + @ApiOperation("调用统一下单API,生成支付二维码") @PostMapping("/native/{productId}") public R nativePay(@PathVariable Long productId) throws Exception { @@ -41,7 +48,7 @@ public class WxPayController { } @PostMapping("/native/notify") - public String nativeNotify(HttpServletRequest request, HttpServletResponse response) { + public String nativeNotify(HttpServletRequest request, HttpServletResponse response) throws IOException, GeneralSecurityException { Gson gson = new Gson(); // 应答对象 Map map = new HashMap<>(); @@ -50,15 +57,35 @@ public class WxPayController { Map bodyMap = gson.fromJson(body, HashMap.class); log.info("支付通知的id ====> {}", bodyMap.get("id")); log.info("支付通知完整的数据 ====> {}", body); - Object id = bodyMap.get("id"); - // TODO: 签名的验证 + String requestId = (String) bodyMap.get("id"); + // 签名的验证 + WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest + = new WechatPay2ValidatorForRequest(verifier, requestId, body); + if (!wechatPay2ValidatorForRequest.validate(request)) { + log.info("通知验签失败"); + // 失败应答 + response.setStatus(500); + map.put("code", "ERROR"); + map.put("message", "通知验签失败"); + return gson.toJson(map); + } + log.info("通知验签成功"); - // TODO: 处理订单 + // 处理订单 + wxPayService.processOrder(bodyMap); - // 成功应答 - response.setStatus(200); - map.put("code", "SUCCESS"); - map.put("message", "成功"); - return gson.toJson(map); + try { + // 成功应答 + response.setStatus(200); + map.put("code", "SUCCESS"); + map.put("message", "成功"); + return gson.toJson(map); + } catch (Exception e) { + // 失败应答 + response.setStatus(500); + map.put("code", "ERROR"); + map.put("message", "失败"); + return gson.toJson(map); + } } } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java index ae670d5..b2fd0ae 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java @@ -2,6 +2,7 @@ package com.hongyi.paymentdemo.service; import com.hongyi.paymentdemo.entity.OrderInfo; import com.baomidou.mybatisplus.extension.service.IService; +import com.hongyi.paymentdemo.enums.OrderStatus; import java.util.List; @@ -25,4 +26,11 @@ public interface OrderInfoService extends IService { * @return */ List listOrderByCreateTimeDesc(); + + /** + * 根据订单号更新订单状态 + * @param orderNo + * @param orderStatus + */ + void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus); } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/PaymentInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/PaymentInfoService.java index 0d56fe9..0b2e26a 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/PaymentInfoService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/PaymentInfoService.java @@ -1,5 +1,11 @@ package com.hongyi.paymentdemo.service; -public interface PaymentInfoService { +import java.util.HashMap; +public interface PaymentInfoService { + /** + * 根据解密的明文创建支付日志 + * @param plainTextMap + */ + void createPaymentInfo(HashMap plainTextMap); } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java index b09ad7c..7775038 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java @@ -1,6 +1,7 @@ package com.hongyi.paymentdemo.service; import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.Map; /** @@ -10,4 +11,6 @@ import java.util.Map; */ public interface WxPayService { Map nativePay(Long productId) throws Exception; + + void processOrder(Map bodyMap) throws GeneralSecurityException; } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java index 41ff390..422ef61 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java @@ -61,6 +61,16 @@ public class OrderInfoServiceImpl extends ServiceImpl {}", orderStatus.getType()); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("order_no", orderNo); + OrderInfo orderInfo = new OrderInfo(); + orderInfo.setOrderStatus(orderStatus.getType()); + orderInfoMapper.update(orderInfo, queryWrapper); + } + /** * 根据商品id查询未支付的订单 * @param productId diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/PaymentInfoServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/PaymentInfoServiceImpl.java index a28a68b..c29d8d5 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/PaymentInfoServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/PaymentInfoServiceImpl.java @@ -1,12 +1,52 @@ package com.hongyi.paymentdemo.service.impl; +import com.google.gson.Gson; import com.hongyi.paymentdemo.entity.PaymentInfo; +import com.hongyi.paymentdemo.enums.PayType; import com.hongyi.paymentdemo.mapper.PaymentInfoMapper; import com.hongyi.paymentdemo.service.PaymentInfoService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + @Service +@Slf4j public class PaymentInfoServiceImpl extends ServiceImpl implements PaymentInfoService { + @Resource + private PaymentInfoMapper paymentInfoMapper; + + + @Override + public void createPaymentInfo(HashMap plainTextMap) { + log.info("创建支付日志"); + // 订单号 + String orderNo = (String)plainTextMap.get("out_trade_no"); + // 微信端的业务编号 + String transactionId = (String)plainTextMap.get("transaction_id"); + // 交易类型 + String tradeType = (String)plainTextMap.get("trade_type"); + // 交易状态 + String tradeState = (String)plainTextMap.get("trade_state"); + // 用户支付的金额 + Map amount = (Map)plainTextMap.get("amount"); + Integer payerTotal = ((Double) amount.get("payer_total")).intValue(); + // 新建支付订单 + PaymentInfo paymentInfo = new PaymentInfo(); + paymentInfo.setOrderNo(orderNo); + paymentInfo.setPaymentType(PayType.WXPAY.getType()); + paymentInfo.setTransactionId(transactionId); + paymentInfo.setTradeType(tradeType); + paymentInfo.setTradeState(tradeState); + paymentInfo.setPayerTotal(payerTotal); + // 将所有数据存在一个字段里 + Gson gson = new Gson(); + String plainText = gson.toJson(plainTextMap); + paymentInfo.setContent(plainText); + paymentInfoMapper.insert(paymentInfo); + } } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java index 5d20638..ddc6fd2 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java @@ -7,8 +7,10 @@ import com.hongyi.paymentdemo.enums.OrderStatus; import com.hongyi.paymentdemo.enums.wxpay.WxApiType; import com.hongyi.paymentdemo.enums.wxpay.WxNotifyType; import com.hongyi.paymentdemo.service.OrderInfoService; +import com.hongyi.paymentdemo.service.PaymentInfoService; import com.hongyi.paymentdemo.service.WxPayService; import com.hongyi.paymentdemo.util.OrderNoUtils; +import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; @@ -20,6 +22,8 @@ import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; @@ -41,6 +45,9 @@ public class WxPayServiceImpl implements WxPayService { @Resource private OrderInfoService orderInfoService; + @Resource + private PaymentInfoService paymentInfoService; + /** * 创建订单,调用native支付接口 * @param productId 购买产品的id @@ -124,4 +131,43 @@ public class WxPayServiceImpl implements WxPayService { response.close(); } } + + @Override + public void processOrder(Map bodyMap) throws GeneralSecurityException { + log.info("处理订单"); + // 解密报文 + String plainText = decryptFromResource(bodyMap); + // 将明文转换为map + Gson gson = new Gson(); + HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); + String orderNo = (String)plainTextMap.get("out_trade_no"); // 订单号 + // 更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); + // 记录支付日志 + paymentInfoService.createPaymentInfo(plainTextMap); + } + + /** + * 对称解密 + * @param bodyMap + * @return + */ + private String decryptFromResource(Map bodyMap) throws GeneralSecurityException { + log.info("密文解密"); + // 通知数据 + Map resourceMap = (Map) bodyMap.get("resource"); + // 数据密文 + String ciphertext = resourceMap.get("ciphertext"); + // 随机串 + String nonce = resourceMap.get("nonce"); + // 附加数据 + String associatedData = resourceMap.get("associated_data"); + AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); + String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), + nonce.getBytes(StandardCharsets.UTF_8), + ciphertext); + log.info("密文 ===> {}", ciphertext); + log.info("明文 ===> {}", plainText); + return plainText; + } } diff --git a/payment-demo/src/main/resources/wxpay.properties b/payment-demo/src/main/resources/wxpay.properties index dbae34f..eb68ba0 100644 --- a/payment-demo/src/main/resources/wxpay.properties +++ b/payment-demo/src/main/resources/wxpay.properties @@ -13,4 +13,4 @@ wxpay.app-id=wx74862e0dfcf69954 wxpay.domain=https://api.mch.weixin.qq.com # 接收结果通知地址 # 注意:每次重新启动ngrok,都需要修改这个配置 -wxpay.notify-domain=https://c257-117-174-85-8.ap.ngrok.io +wxpay.notify-domain=https://d892-101-206-166-241.jp.ngrok.io -- Gitee From 5362828bce5bb199ae0f66ddb9d8333f8ae50c00 Mon Sep 17 00:00:00 2001 From: zenghongyi <277382367@qq.com> Date: Fri, 1 Jul 2022 17:48:32 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=87=8D=E5=A4=8D=E9=80=9A=E7=9F=A5=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=94=81=20=E5=AE=9A=E6=97=B6=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E8=AE=A2=E5=8D=95=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/OrderInfoController.java | 21 ++++++++++++--- .../paymentdemo/service/OrderInfoService.java | 7 +++++ .../service/impl/OrderInfoServiceImpl.java | 11 ++++++++ .../service/impl/WxPayServiceImpl.java | 27 ++++++++++++++++--- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/OrderInfoController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/OrderInfoController.java index afe8b32..a4593f3 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/OrderInfoController.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/OrderInfoController.java @@ -1,13 +1,11 @@ package com.hongyi.paymentdemo.controller; import com.hongyi.paymentdemo.entity.OrderInfo; +import com.hongyi.paymentdemo.enums.OrderStatus; import com.hongyi.paymentdemo.service.OrderInfoService; import com.hongyi.paymentdemo.vo.R; import io.swagger.annotations.Api; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; @@ -30,4 +28,19 @@ public class OrderInfoController { List list = orderInfoService.listOrderByCreateTimeDesc(); return R.ok().data("list", list); } + + /** + * 查询订单状态 + * @param orderNo + * @return + */ + @GetMapping("/query-order-status/{orderNo}") + public R queryOrderStatus(@PathVariable String orderNo) { + String orderStatus = orderInfoService.getOrderStatus(orderNo); + if (OrderStatus.SUCCESS.getType().equals(orderStatus)) { + return R.ok().setMessage("支付成功"); //支付成功 + } + // code: 1为支付成功,0为支付失败,101为任意值 + return R.ok().setCode(101).setMessage("用户支付中...."); + } } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java index b2fd0ae..b92626a 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java @@ -33,4 +33,11 @@ public interface OrderInfoService extends IService { * @param orderStatus */ void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus); + + /** + * 根据订单号查询订单状态 + * @param orderNo + * @return + */ + String getOrderStatus(String orderNo); } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java index 422ef61..2c292b1 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java @@ -71,6 +71,17 @@ public class OrderInfoServiceImpl extends ServiceImpl queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("order_no", orderNo); + OrderInfo orderInfo = orderInfoMapper.selectOne(queryWrapper); + if (orderInfo == null) { + return null; + } + return orderInfo.getOrderStatus(); + } + /** * 根据商品id查询未支付的订单 * @param productId diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java index ddc6fd2..d1625e8 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java @@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; /** * @Author Kisugi Takumi @@ -48,6 +49,8 @@ public class WxPayServiceImpl implements WxPayService { @Resource private PaymentInfoService paymentInfoService; + private final ReentrantLock lock = new ReentrantLock(); + /** * 创建订单,调用native支付接口 * @param productId 购买产品的id @@ -141,10 +144,26 @@ public class WxPayServiceImpl implements WxPayService { Gson gson = new Gson(); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no"); // 订单号 - // 更新订单状态 - orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); - // 记录支付日志 - paymentInfoService.createPaymentInfo(plainTextMap); + // 数据锁:成功获取则返回true,失败获取则立即返回false,不会一直等待锁的释放 + if (lock.tryLock()) { + try { + // 处理重复的通知 + // 保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的 + String orderStatus = orderInfoService.getOrderStatus(orderNo); + // 如果当前订单已经支付,则不做处理 + if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) { + return ; + } + // 如果用户尚未订单,则将数据插入数据库 + // 更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); + // 记录支付日志 + paymentInfoService.createPaymentInfo(plainTextMap); + } finally { + // 释放锁 + lock.unlock(); + } + } } /** -- Gitee From df735e622e08e4690684115ace1fd6d9cbf05a33 Mon Sep 17 00:00:00 2001 From: zenghongyi <277382367@qq.com> Date: Mon, 4 Jul 2022 11:03:11 +0800 Subject: [PATCH 7/9] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WxPayController.java | 7 +++ .../paymentdemo/service/WxPayService.java | 2 + .../service/impl/WxPayServiceImpl.java | 49 +++++++++++++++++++ .../src/main/resources/wxpay.properties | 2 +- 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java index b81aa2e..2b55166 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java @@ -88,4 +88,11 @@ public class WxPayController { return gson.toJson(map); } } + + @PostMapping("/cancel/{orderNo}") + public R cancel(@PathVariable String orderNo) throws IOException { + log.info("取消订单"); + wxPayService.cancelOrder(orderNo); + return R.ok().setMessage("订单已取消"); + } } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java index 7775038..a4de494 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java @@ -13,4 +13,6 @@ public interface WxPayService { Map nativePay(Long productId) throws Exception; void processOrder(Map bodyMap) throws GeneralSecurityException; + + void cancelOrder(String orderNo) throws IOException; } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java index d1625e8..6084ae5 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java @@ -166,6 +166,55 @@ public class WxPayServiceImpl implements WxPayService { } } + @Override + public void cancelOrder(String orderNo) throws IOException { + // 调用微信支付的关闭订单接口 + this.closeOrder(orderNo); + // 更新商户端的订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL); + } + + /** + * 关闭订单接口的调用 + * @param orderNo + */ + private void closeOrder(String orderNo) throws IOException { + log.info("关单接口的调用,订单号 ====> {}", orderNo); + // 创建远程请求对象 + String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo); + url = wxPayConfig.getDomain().concat(url); + HttpPost httpPost = new HttpPost(url); + // 组装json请求体 + Gson gson = new Gson(); + HashMap paramsMap = new HashMap<>(); + paramsMap.put("mchid", wxPayConfig.getMchId()); + String jsonParams = gson.toJson(paramsMap); + log.info("请求参数 ====> {}", jsonParams); + // 将请求参数设置到请求对象中 + StringEntity entity = new StringEntity(jsonParams,"utf-8"); + entity.setContentType("application/json"); + httpPost.setEntity(entity); + httpPost.setHeader("Accept", "application/json"); + + // 完成签名并执行请求 + CloseableHttpResponse response = httpClient.execute(httpPost); + + try { + // 获取响应状态码 + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200) { //处理成功 + log.info("success200"); + } else if (statusCode == 204) { //处理成功,无返回Body + log.info("success204"); + } else { + log.info("failed,resp code = " + statusCode); + throw new IOException("request failed"); + } + } finally { + response.close(); + } + } + /** * 对称解密 * @param bodyMap diff --git a/payment-demo/src/main/resources/wxpay.properties b/payment-demo/src/main/resources/wxpay.properties index eb68ba0..a25d3e7 100644 --- a/payment-demo/src/main/resources/wxpay.properties +++ b/payment-demo/src/main/resources/wxpay.properties @@ -13,4 +13,4 @@ wxpay.app-id=wx74862e0dfcf69954 wxpay.domain=https://api.mch.weixin.qq.com # 接收结果通知地址 # 注意:每次重新启动ngrok,都需要修改这个配置 -wxpay.notify-domain=https://d892-101-206-166-241.jp.ngrok.io +wxpay.notify-domain=https://84bb-117-174-85-8.ap.ngrok.io -- Gitee From 7fe376b322e3af44b96968b26830811a94c38aa1 Mon Sep 17 00:00:00 2001 From: zenghongyi <277382367@qq.com> Date: Mon, 4 Jul 2022 17:22:20 +0800 Subject: [PATCH 8/9] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?=E6=9F=A5=E5=8D=95API=20=E8=B6=85=E6=97=B6=E8=AE=A2=E5=8D=95=20?= =?UTF-8?q?=E7=94=B3=E8=AF=B7=E9=80=80=E6=AC=BEAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../paymentdemo/PaymentDemoApplication.java | 3 + .../controller/WxPayController.java | 14 +++ .../paymentdemo/service/OrderInfoService.java | 14 +++ .../service/RefundInfoService.java | 12 ++ .../paymentdemo/service/WxPayService.java | 28 +++++ .../service/impl/OrderInfoServiceImpl.java | 20 ++++ .../service/impl/RefundInfoServiceImpl.java | 55 +++++++++ .../service/impl/WxPayServiceImpl.java | 111 ++++++++++++++++++ .../hongyi/paymentdemo/task/WxPayTask.java | 56 +++++++++ .../src/main/resources/wxpay.properties | 2 +- 10 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 payment-demo/src/main/java/com/hongyi/paymentdemo/task/WxPayTask.java diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/PaymentDemoApplication.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/PaymentDemoApplication.java index 8616977..5ea4681 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/PaymentDemoApplication.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/PaymentDemoApplication.java @@ -2,8 +2,11 @@ package com.hongyi.paymentdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +// 引入Spring Task +@EnableScheduling public class PaymentDemoApplication { public static void main(String[] args) { diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java index 2b55166..5c69971 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java @@ -95,4 +95,18 @@ public class WxPayController { wxPayService.cancelOrder(orderNo); return R.ok().setMessage("订单已取消"); } + + @GetMapping("/query/{orderNo}") + public R queryOrder(@PathVariable String orderNo) throws IOException { + log.info("查询订单"); + String result = wxPayService.queryOrder(orderNo); + return R.ok().setMessage("查询成功").data("result", result); + } + + @PostMapping("/refunds/{orderNo}/{reason}") + public R refunds(@PathVariable String orderNo, @PathVariable String reason) throws IOException { + log.info("申请退款"); + wxPayService.refund(orderNo, reason); + return R.ok(); + } } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java index b92626a..ebd9041 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java @@ -40,4 +40,18 @@ public interface OrderInfoService extends IService { * @return */ String getOrderStatus(String orderNo); + + /** + * 查询超过minutes分钟的未支付订单 + * @param minutes + * @return + */ + List getNoPayOrderByDuration(int minutes); + + /** + * 根据订单号获得订单 + * @param orderNo + * @return + */ + OrderInfo getOrderByOrderNo(String orderNo); } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/RefundInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/RefundInfoService.java index ff21f56..ce4c550 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/RefundInfoService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/RefundInfoService.java @@ -4,5 +4,17 @@ import com.hongyi.paymentdemo.entity.RefundInfo; import com.baomidou.mybatisplus.extension.service.IService; public interface RefundInfoService extends IService { + /** + * 根据订单号和退款原因创建退款单记录 + * @param orderNo + * @param reason + * @return + */ + RefundInfo createRefundByOrderNo(String orderNo, String reason); + /** + * 更新退款单 + * @param content + */ + void updateRefund(String content); } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java index a4de494..3cd8224 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java @@ -14,5 +14,33 @@ public interface WxPayService { void processOrder(Map bodyMap) throws GeneralSecurityException; + /** + * 通过订单号取消订单 + * @param orderNo + * @throws IOException + */ void cancelOrder(String orderNo) throws IOException; + + /** + * 通过订单号查询订单 + * @param orderNo + * @return + * @throws IOException + */ + String queryOrder(String orderNo) throws IOException; + + /** + * 根据订单号查询微信支付查单接口,核实订单状态 + * 如果订单已经支付,则更新商户端订单状态 + * 如果订单未支付,则调用关单接口,并更新商户端订单状态 + * @param orderNo + */ + void checkOrderStatus(String orderNo) throws IOException; + + /** + * 根据订单号退款 + * @param orderNo + * @param reason + */ + void refund(String orderNo, String reason) throws IOException; } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java index 2c292b1..f7fbad3 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java @@ -13,6 +13,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; +import java.time.Duration; +import java.time.Instant; import java.util.List; @Service @@ -82,6 +84,22 @@ public class OrderInfoServiceImpl extends ServiceImpl getNoPayOrderByDuration(int minutes) { + Instant instant = Instant.now().minus(Duration.ofMinutes(minutes)); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType()) + .le("create_time", instant); + return orderInfoMapper.selectList(queryWrapper); + } + + @Override + public OrderInfo getOrderByOrderNo(String orderNo) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("order_no", orderNo); + return orderInfoMapper.selectOne(queryWrapper); + } + /** * 根据商品id查询未支付的订单 * @param productId @@ -94,4 +112,6 @@ public class OrderInfoServiceImpl extends ServiceImpl implements RefundInfoService { + @Resource + private OrderInfoService orderInfoService; + + @Resource + private RefundInfoMapper refundInfoMapper; + + @Override + public RefundInfo createRefundByOrderNo(String orderNo, String reason) { + // 根据订单号获取订单信息 + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); + // 根据订单号生成退款单 + RefundInfo refundInfo = new RefundInfo(); + refundInfo.setOrderNo(orderNo); + refundInfo.setRefundNo(OrderNoUtils.getRefundNo()); + refundInfo.setTotalFee(orderInfo.getTotalFee()); + refundInfo.setRefund(orderInfo.getTotalFee()); + refundInfo.setReason(reason); + // 保存 + refundInfoMapper.insert(refundInfo); + return refundInfo; + } + + @Override + public void updateRefund(String content) { + //将json字符串转换成Map + Gson gson = new Gson(); + Map resultMap = gson.fromJson(content, HashMap.class); + //根据退款单编号修改退款单 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("refund_no", resultMap.get("out_refund_no")); + //设置要修改的字段 + RefundInfo refundInfo = new RefundInfo(); + refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号 + //查询退款和申请退款中的返回参数 + if(resultMap.get("status") != null){ + refundInfo.setRefundStatus(resultMap.get("status"));//退款状态 + refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段 + } + //退款回调中的回调参数 + if(resultMap.get("refund_status") != null){ + refundInfo.setRefundStatus(resultMap.get("refund_status")); //退款状态 + refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段 + } + //更新退款单 + refundInfoMapper.update(refundInfo, queryWrapper); + } } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java index 6084ae5..8c6603a 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java @@ -3,9 +3,11 @@ package com.hongyi.paymentdemo.service.impl; import com.google.gson.Gson; import com.hongyi.paymentdemo.config.WxPayConfig; import com.hongyi.paymentdemo.entity.OrderInfo; +import com.hongyi.paymentdemo.entity.RefundInfo; import com.hongyi.paymentdemo.enums.OrderStatus; import com.hongyi.paymentdemo.enums.wxpay.WxApiType; import com.hongyi.paymentdemo.enums.wxpay.WxNotifyType; +import com.hongyi.paymentdemo.enums.wxpay.WxTradeState; import com.hongyi.paymentdemo.service.OrderInfoService; import com.hongyi.paymentdemo.service.PaymentInfoService; import com.hongyi.paymentdemo.service.WxPayService; @@ -13,11 +15,13 @@ import com.hongyi.paymentdemo.util.OrderNoUtils; import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; 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.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import javax.annotation.Resource; @@ -49,6 +53,9 @@ public class WxPayServiceImpl implements WxPayService { @Resource private PaymentInfoService paymentInfoService; + @Resource + private RefundInfoServiceImpl refundInfoService; + private final ReentrantLock lock = new ReentrantLock(); /** @@ -174,6 +181,110 @@ public class WxPayServiceImpl implements WxPayService { orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL); } + @Override + public String queryOrder(String orderNo) throws IOException { + log.info("查单接口调用 ====> {}", orderNo); + String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo); + url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId()); + HttpGet httpGet = new HttpGet(url); + httpGet.setHeader("Accept", "application/json"); + // 完成签名并执行请求 + CloseableHttpResponse response = httpClient.execute(httpGet); + try { + // 获取响应体 + String bodyAsString = EntityUtils.toString(response.getEntity()); + // 获取响应状态码 + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200) { //处理成功 + log.info("success,return body = " + bodyAsString); + } else if (statusCode == 204) { //处理成功,无返回Body + log.info("success"); + } else { + log.info("failed,resp code = " + statusCode+ ",return body = " + bodyAsString); + throw new IOException("request failed"); + } + return bodyAsString; + } finally { + response.close(); + } + } + + @Override + public void checkOrderStatus(String orderNo) throws IOException { + log.warn("根据订单号核实订单状态 ====> {}", orderNo); + // 调用查单接口 + String result = this.queryOrder(orderNo); + Gson gson = new Gson(); + Map resultMap = gson.fromJson(result, HashMap.class); + // 获取微信支付端的订单状态 + Object tradeState = resultMap.get("trade_state"); + // 判断订单状态 + if (WxTradeState.SUCCESS.getType().equals(tradeState)) { + log.warn("核实订单已支付 ====> {}", orderNo); + // 更新本地的订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); + // 记录支付日志 + paymentInfoService.createPaymentInfo(gson.fromJson(result, HashMap.class)); + } + if (WxTradeState.NOTPAY.getType().equals(tradeState)) { + log.info("核实订单未支付 ====> {}", tradeState); + // 调用关闭订单接口 + this.closeOrder(orderNo); + // 更新本地的订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void refund(String orderNo, String reason) throws IOException { + log.info("创建退款单记录"); + //根据订单编号创建退款单 + RefundInfo refundsInfo = refundInfoService.createRefundByOrderNo(orderNo, reason); + log.info("调用退款API"); + //调用统一下单API + String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType()); + HttpPost httpPost = new HttpPost(url); + // 请求body参数 + Gson gson = new Gson(); + Map paramsMap = new HashMap(); + paramsMap.put("out_trade_no", orderNo);//订单编号 + paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号 + paramsMap.put("reason",reason);//退款原因 + paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址 + Map amountMap = new HashMap(); + amountMap.put("refund", refundsInfo.getRefund());//退款金额 + amountMap.put("total", refundsInfo.getTotalFee());//原订单金额 + amountMap.put("currency", "CNY");//退款币种 + paramsMap.put("amount", amountMap); //将参数转换成json字符串 + String jsonParams = gson.toJson(paramsMap); + log.info("请求参数 ===> {}" + jsonParams); + StringEntity entity = new StringEntity(jsonParams,"utf-8"); + entity.setContentType("application/json");//设置请求报文格式 + httpPost.setEntity(entity);//将请求报文放入请求对象 + httpPost.setHeader("Accept", "application/json");//设置响应报文格式 + //完成签名并执行请求,并完成验签 + CloseableHttpResponse response = httpClient.execute(httpPost); + try { + //解析响应结果 + String bodyAsString = EntityUtils.toString(response.getEntity()); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200) { + log.info("成功, 退款返回结果 = " + bodyAsString); + } else if (statusCode == 204) { + log.info("成功"); + } else { + throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款 返回结果 = " + bodyAsString); + } + //更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING); + //更新退款单 + refundInfoService.updateRefund(bodyAsString); + } finally { + response.close(); + } + } + /** * 关闭订单接口的调用 * @param orderNo diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/task/WxPayTask.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/task/WxPayTask.java new file mode 100644 index 0000000..4f62794 --- /dev/null +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/task/WxPayTask.java @@ -0,0 +1,56 @@ +package com.hongyi.paymentdemo.task; + +import com.hongyi.paymentdemo.entity.OrderInfo; +import com.hongyi.paymentdemo.service.OrderInfoService; +import com.hongyi.paymentdemo.service.WxPayService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.List; + +/** + * @Author Kisugi Takumi + * @Date 2022/7/4 14:34 + * @Version 1.0 + */ +@Component +@Slf4j +public class WxPayTask { + + @Resource + private OrderInfoService orderInfoService; + + @Resource + private WxPayService wxPayService; + + /** + * 秒 分 时 日 月 周 + * * 每秒都执行 + * 1-3: 从第1s开始执行,到第3s结束执行 + * 0/3: 从第0s开始,每隔3s执行1次 + * 1,2,3: 在指定的第1,2,3s执行1次 + * 日和周不能同时指定,?为不指定 + */ +// @Scheduled(cron = "0/3 * * * * ?") +// public void task1() { +// log.info("task1被执行..."); +// } + + /** + * 每隔30s执行一次,查询创建超过5分钟,并且未支付的订单 + */ + @Scheduled(cron = "0/30 * * * * ?") + public void orderConfirm() throws IOException { + log.info("orderConfirm被执行..."); + List orderInfoList = orderInfoService.getNoPayOrderByDuration(1); + for (OrderInfo orderInfo : orderInfoList) { + String orderNo = orderInfo.getOrderNo(); + log.warn("超时订单 ====> {}", orderNo); + // 核实订单状态:调用微信支付查单接口 + wxPayService.checkOrderStatus(orderNo); + } + } +} diff --git a/payment-demo/src/main/resources/wxpay.properties b/payment-demo/src/main/resources/wxpay.properties index a25d3e7..2df1e31 100644 --- a/payment-demo/src/main/resources/wxpay.properties +++ b/payment-demo/src/main/resources/wxpay.properties @@ -13,4 +13,4 @@ wxpay.app-id=wx74862e0dfcf69954 wxpay.domain=https://api.mch.weixin.qq.com # 接收结果通知地址 # 注意:每次重新启动ngrok,都需要修改这个配置 -wxpay.notify-domain=https://84bb-117-174-85-8.ap.ngrok.io +wxpay.notify-domain=https://4fcc-117-174-85-8.ap.ngrok.io -- Gitee From 7b9b61d58e9faf7c5fe5ce61687a64deeb9b3d2f Mon Sep 17 00:00:00 2001 From: zenghongyi <277382367@qq.com> Date: Mon, 4 Jul 2022 20:46:25 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?=E9=80=80=E6=AC=BE=E9=80=9A=E7=9F=A5=20=E8=B4=A6=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WxPayController.java | 60 +++++++ .../service/RefundInfoService.java | 9 ++ .../paymentdemo/service/WxPayService.java | 36 +++++ .../service/impl/RefundInfoServiceImpl.java | 15 ++ .../service/impl/WxPayServiceImpl.java | 146 +++++++++++++++++- .../hongyi/paymentdemo/task/WxPayTask.java | 21 +++ 6 files changed, 285 insertions(+), 2 deletions(-) diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java index 5c69971..78ef693 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java @@ -109,4 +109,64 @@ public class WxPayController { wxPayService.refund(orderNo, reason); return R.ok(); } + + @GetMapping("/query-refund/{refundNo}") + public R queryRefund(@PathVariable String refundNo) throws Exception { + log.info("查询退款"); + String result = wxPayService.queryRefund(refundNo); + return R.ok().setMessage("查询成功").data("result", result); + } + + @PostMapping("/refunds/notify") + public String refundsNotify(HttpServletRequest request, HttpServletResponse response){ + log.info("退款通知执行"); + Gson gson = new Gson(); + Map map = new HashMap<>(); + //应答对象 + try { + //处理通知参数 + String body = HttpUtils.readData(request); + Map bodyMap = gson.fromJson(body, HashMap.class); + String requestId = (String)bodyMap.get("id"); + log.info("支付通知的id ===> {}", requestId); + //签名的验证 + WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, requestId, body); + if(!wechatPay2ValidatorForRequest.validate(request)){ + log.error("通知验签失败"); + //失败应答 + response.setStatus(500); + map.put("code", "ERROR"); + map.put("message", "通知验签失败"); + return gson.toJson(map); + } + log.info("通知验签成功"); + //处理退款单 + wxPayService.processRefund(bodyMap); + //成功应答 + response.setStatus(200); + map.put("code", "SUCCESS"); + map.put("message", "成功"); + return gson.toJson(map); + } catch (Exception e) { + e.printStackTrace(); + //失败应答 + response.setStatus(500); + map.put("code", "ERROR"); + map.put("message", "失败"); return gson.toJson(map); + } + } + + @GetMapping("/querybill/{billDate}/{type}") + public R queryTradeBill( @PathVariable String billDate, @PathVariable String type) throws Exception { + log.info("获取账单url"); + String downloadUrl = wxPayService.queryBill(billDate, type); + return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl); + } + + @GetMapping("/downloadbill/{billDate}/{type}") + public R downloadBill( @PathVariable String billDate, @PathVariable String type) throws Exception { + log.info("下载账单"); + String result = wxPayService.downloadBill(billDate, type); + return R.ok().data("result", result); + } } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/RefundInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/RefundInfoService.java index ce4c550..05c868e 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/RefundInfoService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/RefundInfoService.java @@ -3,6 +3,8 @@ package com.hongyi.paymentdemo.service; import com.hongyi.paymentdemo.entity.RefundInfo; import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + public interface RefundInfoService extends IService { /** * 根据订单号和退款原因创建退款单记录 @@ -17,4 +19,11 @@ public interface RefundInfoService extends IService { * @param content */ void updateRefund(String content); + + /** + * 找出申请退款超过minutes分钟并且未成功的退款单 + * @param minutes + * @return + */ + List getNoRefundOrderByDuration(int minutes); } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java index 3cd8224..ef21138 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java @@ -43,4 +43,40 @@ public interface WxPayService { * @param reason */ void refund(String orderNo, String reason) throws IOException; + + /** + * 根据退款单号核实退款单状态 + * @param refundNo + * @return + */ + String queryRefund(String refundNo) throws IOException; + + /** + * 根据退款单号核实退款单状态 + * @param refundNo + */ + void checkRefundStatus(String refundNo) throws IOException; + + /** + * 退款结果通知 + * 退款状态改变后,微信会把相关退款结果发送给商户。 + * @param bodyMap + */ + void processRefund(Map bodyMap) throws Exception; + + /** + * 申请账单 + * @param billDate + * @param type + * @return + */ + String queryBill(String billDate, String type) throws Exception; + + /** + * 下载账单 + * @param billDate + * @param type + * @return + */ + String downloadBill(String billDate, String type) throws Exception; } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/RefundInfoServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/RefundInfoServiceImpl.java index f443e9b..6cb75ee 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/RefundInfoServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/RefundInfoServiceImpl.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.google.gson.Gson; import com.hongyi.paymentdemo.entity.OrderInfo; import com.hongyi.paymentdemo.entity.RefundInfo; +import com.hongyi.paymentdemo.enums.wxpay.WxRefundStatus; import com.hongyi.paymentdemo.mapper.RefundInfoMapper; import com.hongyi.paymentdemo.service.OrderInfoService; import com.hongyi.paymentdemo.service.RefundInfoService; @@ -12,7 +13,10 @@ import com.hongyi.paymentdemo.util.OrderNoUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; +import java.time.Duration; +import java.time.Instant; import java.util.HashMap; +import java.util.List; import java.util.Map; @Service @@ -64,4 +68,15 @@ public class RefundInfoServiceImpl extends ServiceImpl getNoRefundOrderByDuration(int minutes) { + //minutes分钟之前的时间 + Instant instant = Instant.now().minus(Duration.ofMinutes(minutes)); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType()); + queryWrapper.le("create_time", instant); + List refundInfoList = refundInfoMapper.selectList(queryWrapper); + return refundInfoList; + } } diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java index 8c6603a..587d19f 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java @@ -7,11 +7,12 @@ import com.hongyi.paymentdemo.entity.RefundInfo; import com.hongyi.paymentdemo.enums.OrderStatus; import com.hongyi.paymentdemo.enums.wxpay.WxApiType; import com.hongyi.paymentdemo.enums.wxpay.WxNotifyType; +import com.hongyi.paymentdemo.enums.wxpay.WxRefundStatus; import com.hongyi.paymentdemo.enums.wxpay.WxTradeState; import com.hongyi.paymentdemo.service.OrderInfoService; import com.hongyi.paymentdemo.service.PaymentInfoService; +import com.hongyi.paymentdemo.service.RefundInfoService; import com.hongyi.paymentdemo.service.WxPayService; -import com.hongyi.paymentdemo.util.OrderNoUtils; import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.CloseableHttpResponse; @@ -54,7 +55,7 @@ public class WxPayServiceImpl implements WxPayService { private PaymentInfoService paymentInfoService; @Resource - private RefundInfoServiceImpl refundInfoService; + private RefundInfoService refundInfoService; private final ReentrantLock lock = new ReentrantLock(); @@ -285,6 +286,147 @@ public class WxPayServiceImpl implements WxPayService { } } + @Override + public String queryRefund(String refundNo) throws IOException { + log.info("查询退款接口调用 ===> {}", refundNo); + String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo); + url = wxPayConfig.getDomain().concat(url); + //创建远程Get 请求对象 + HttpGet httpGet = new HttpGet(url); httpGet.setHeader("Accept", "application/json"); + //完成签名并执行请求 + CloseableHttpResponse response = httpClient.execute(httpGet); + try { + String bodyAsString = EntityUtils.toString(response.getEntity()); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200) { + log.info("成功, 查询退款返回结果 = " + bodyAsString); + } else if (statusCode == 204) { + log.info("成功"); + } else { + throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString); + } + return bodyAsString; + } finally { + response.close(); + } + } + + @Override + public void checkRefundStatus(String refundNo) throws IOException { + log.warn("根据退款单号核实退款单状态 ===> {}", refundNo); + //调用查询退款单接口 + String result = this.queryRefund(refundNo); + //组装json请求体字符串 + Gson gson = new Gson(); + Map resultMap = gson.fromJson(result, HashMap.class); + //获取微信支付端退款状态 + String status = resultMap.get("status"); + String orderNo = resultMap.get("out_trade_no"); + if (WxRefundStatus.SUCCESS.getType().equals(status)) { + log.warn("核实订单已退款成功 ===> {}", refundNo); + //如果确认退款成功,则更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); //更新退款单 + refundInfoService.updateRefund(result); + } + if (WxRefundStatus.ABNORMAL.getType().equals(status)) { + log.warn("核实订单退款异常 ===> {}", refundNo); + //如果确认退款成功,则更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL); + //更新退款单 + refundInfoService.updateRefund(result); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void processRefund(Map bodyMap) throws Exception { + log.info("退款单"); + //解密报文 + String plainText = decryptFromResource(bodyMap); + //将明文转换成map + Gson gson = new Gson(); + HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); + String orderNo = (String)plainTextMap.get("out_trade_no"); + if(lock.tryLock()){ + try { + String orderStatus = orderInfoService.getOrderStatus(orderNo); + if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) { + return; + } + //更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); + //更新退款单 + refundInfoService.updateRefund(plainText); + } finally { + //要主动释放锁 + lock.unlock(); + } + } + } + + @Override + public String queryBill(String billDate, String type) throws Exception { + log.warn("申请账单接口调用 {}", billDate); + String url = ""; + if("tradebill".equals(type)){ + url = WxApiType.TRADE_BILLS.getType(); + }else if("fundflowbill".equals(type)){ + url = WxApiType.FUND_FLOW_BILLS.getType(); + }else{ + throw new RuntimeException("不支持的账单类型"); + } + url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate); + //创建远程Get 请求对象 + HttpGet httpGet = new HttpGet(url); + httpGet.addHeader("Accept", "application/json"); + //使用wxPayClient发送请求得到响应 + CloseableHttpResponse response = httpClient.execute(httpGet); + try { + String bodyAsString = EntityUtils.toString(response.getEntity()); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200) { + log.info("成功, 申请账单返回结果 = " + bodyAsString); + } else if (statusCode == 204) { + log.info("成功"); + } + else { + throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString); + } + //获取账单下载地址 + Gson gson = new Gson(); + Map resultMap = gson.fromJson(bodyAsString, HashMap.class); + return resultMap.get("download_url"); + } finally { + response.close(); + } + } + + @Override + public String downloadBill(String billDate, String type) throws Exception { + log.warn("下载账单接口调用 {}, {}", billDate, type); + //获取账单url地址 + String downloadUrl = this.queryBill(billDate, type); + //创建远程Get 请求对象 + HttpGet httpGet = new HttpGet(downloadUrl); + httpGet.addHeader("Accept", "application/json"); + //使用wxPayClient发送请求得到响应 + CloseableHttpResponse response = httpClient.execute(httpGet); + try { + String bodyAsString = EntityUtils.toString(response.getEntity()); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200) { + log.info("成功, 下载账单返回结果 = " + bodyAsString); + } else if (statusCode == 204) { + log.info("成功"); + } else { + throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString); + } + return bodyAsString; + } finally { + response.close(); + } + } + /** * 关闭订单接口的调用 * @param orderNo diff --git a/payment-demo/src/main/java/com/hongyi/paymentdemo/task/WxPayTask.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/task/WxPayTask.java index 4f62794..7b7b01f 100644 --- a/payment-demo/src/main/java/com/hongyi/paymentdemo/task/WxPayTask.java +++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/task/WxPayTask.java @@ -1,7 +1,9 @@ package com.hongyi.paymentdemo.task; import com.hongyi.paymentdemo.entity.OrderInfo; +import com.hongyi.paymentdemo.entity.RefundInfo; import com.hongyi.paymentdemo.service.OrderInfoService; +import com.hongyi.paymentdemo.service.RefundInfoService; import com.hongyi.paymentdemo.service.WxPayService; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; @@ -26,6 +28,9 @@ public class WxPayTask { @Resource private WxPayService wxPayService; + @Resource + private RefundInfoService refundInfoService; + /** * 秒 分 时 日 月 周 * * 每秒都执行 @@ -53,4 +58,20 @@ public class WxPayTask { wxPayService.checkOrderStatus(orderNo); } } + + /** + * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单 + */ + @Scheduled(cron = "0/30 * * * * ?") + public void refundConfirm() throws Exception { + log.info("refundConfirm 被执行......"); + //找出申请退款超过5分钟并且未成功的退款单 + List refundInfoList = refundInfoService.getNoRefundOrderByDuration(5); + for (RefundInfo refundInfo : refundInfoList) { + String refundNo = refundInfo.getRefundNo(); + log.warn("超时未退款的退款单号 ===> {}", refundNo); + //核实订单状态:调用微信支付查询退款接口 + wxPayService.checkRefundStatus(refundNo); + } + } } -- Gitee