diff --git a/payment-demo/pom.xml b/payment-demo/pom.xml
index 4e98468bb6d95e6e35db3252aa43e3bf0c9549ba..00d8f7641f54b7b40f69ad81cc75019d948f5d97 100644
--- a/payment-demo/pom.xml
+++ b/payment-demo/pom.xml
@@ -61,6 +61,16 @@
0.3.0
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+ com.google.code.gson
+ gson
+
+
org.springframework.boot
spring-boot-starter-test
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 8616977089463f1bcf23366f10b024f4e0483681..5ea4681eb57a92f546b964f109d5b11a39061ad0 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/config/WxPayConfig.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/config/WxPayConfig.java
index c8b29c50334435bcdd3ece223075acbc0cb17869..354bcca590fd1c1b64e61c7a324280e8d29863a2 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,22 @@
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 lombok.extern.slf4j.Slf4j;
+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;
@@ -15,6 +24,7 @@ import java.security.PrivateKey;
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
+@Slf4j
public class WxPayConfig {
// 商户号
@@ -30,7 +40,7 @@ public class WxPayConfig {
private String apiV3Key;
// APPID
- private String appid;
+ private String appId;
// 微信服务器地址
private String domain;
@@ -43,7 +53,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 +61,40 @@ public class WxPayConfig {
}
}
+ /**
+ * 获取签名验证器
+ * @return
+ */
+ @Bean
+ public ScheduledUpdateCertificatesVerifier getVerifier() {
+ log.info("获取签名验证器");
+ // 获取商户私钥
+ PrivateKey privateKey = getPrivateKey(privateKeyPath);
+ // 私钥签名
+ PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
+ // 身份认证对象
+ WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
+ // 使用定时更新的签名验证器,不需要传入证书
+ return new ScheduledUpdateCertificatesVerifier(
+ wechatPay2Credentials,
+ apiV3Key.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * 获取http请求对象
+ * @param verifier
+ * @return
+ */
+ @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,会自动的处理签名和验签,并进行证书自动更新
+ return builder.build();
+ }
+
}
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 0000000000000000000000000000000000000000..a4593f3fb25117c62425bc366369c97dd48947cb
--- /dev/null
+++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/OrderInfoController.java
@@ -0,0 +1,46 @@
+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.*;
+
+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);
+ }
+
+ /**
+ * 查询订单状态
+ * @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/controller/ProductController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/ProductController.java
index 39b4b9621e14c5e157ce683e4e4de548f960f9df..128557c12d860544507bd964025160e92b8daefc 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/controller/WxPayController.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java
new file mode 100644
index 0000000000000000000000000000000000000000..78ef6939d7854e76c5588422686d7ddadfe74114
--- /dev/null
+++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/controller/WxPayController.java
@@ -0,0 +1,172 @@
+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;
+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.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+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;
+
+ @Resource
+ private Verifier verifier;
+
+ @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);
+ }
+
+ @PostMapping("/native/notify")
+ public String nativeNotify(HttpServletRequest request, HttpServletResponse response) throws IOException, GeneralSecurityException {
+ 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);
+ 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("通知验签成功");
+
+ // 处理订单
+ wxPayService.processOrder(bodyMap);
+
+ 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);
+ }
+ }
+
+ @PostMapping("/cancel/{orderNo}")
+ public R cancel(@PathVariable String orderNo) throws IOException {
+ log.info("取消订单");
+ 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();
+ }
+
+ @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/enums/OrderStatus.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/enums/OrderStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..757401cbe920637e58f924e8f5e81ff2f32f0062
--- /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 0000000000000000000000000000000000000000..b75fa3f3371162c767023de4eba7d053cb69d3c1
--- /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 0000000000000000000000000000000000000000..31b4fe8932495cb3dc956ec0893faad0ace32dd3
--- /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 0000000000000000000000000000000000000000..5980fc02675953f27a004f78710c2ad08eaa804b
--- /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 0000000000000000000000000000000000000000..9709f025f83f8bc2a8d0a4436032fe45710241f8
--- /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 0000000000000000000000000000000000000000..f7113407b262d92e7408478502b7d24eaa821d66
--- /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/service/OrderInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/OrderInfoService.java
index cd06466b964a7bf1eb2698e1e07160be6652e573..ebd9041abef26de2ec7ffec41f047932659a1fc3 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,7 +2,56 @@ 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;
public interface OrderInfoService extends IService {
+ /**
+ * 根据商品id生成订单
+ * @param productId
+ * @return
+ */
+ OrderInfo createOrderByProductById(Long productId);
+
+ /**
+ * 根据订单编号存储二维码地址
+ * @param orderNo
+ * @param codeUrl
+ */
+ void saveCodeUrl(String orderNo, String codeUrl);
+
+ /**
+ * 查询订单列表,并按时间倒序查询
+ * @return
+ */
+ List listOrderByCreateTimeDesc();
+
+ /**
+ * 根据订单号更新订单状态
+ * @param orderNo
+ * @param orderStatus
+ */
+ void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus);
+
+ /**
+ * 根据订单号查询订单状态
+ * @param orderNo
+ * @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/PaymentInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/PaymentInfoService.java
index 0d56fe9edbdf926aa459d75b7332e85784f42032..0b2e26a7b93c3f6f51bb21bf38a2a3b92a9500df 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/RefundInfoService.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/RefundInfoService.java
index ff21f5689a229ea3bda71e91e46eb3beebeee142..05c868eff171ed47834b2d128c3f87bac714ab06 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,27 @@ 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 {
+ /**
+ * 根据订单号和退款原因创建退款单记录
+ * @param orderNo
+ * @param reason
+ * @return
+ */
+ RefundInfo createRefundByOrderNo(String orderNo, String reason);
+
+ /**
+ * 更新退款单
+ * @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
new file mode 100644
index 0000000000000000000000000000000000000000..ef211380624dc8836466d6440ba8475f5a94999e
--- /dev/null
+++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/WxPayService.java
@@ -0,0 +1,82 @@
+package com.hongyi.paymentdemo.service;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+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;
+
+ 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;
+
+ /**
+ * 根据退款单号核实退款单状态
+ * @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/OrderInfoServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/OrderInfoServiceImpl.java
index c93e860757295f56db85b9434488509ffb167cb8..f7fbad3056cb2ba681a8ef7db874082a3631eb38 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,117 @@
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;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+
@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);
+ }
+
+ @Override
+ public List listOrderByCreateTimeDesc() {
+ QueryWrapper queryWrapper = new QueryWrapper<>();
+ queryWrapper.orderByDesc("create_time");
+ return orderInfoMapper.selectList(queryWrapper);
+ }
+
+ @Override
+ public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
+ log.info("更新订单状态 ====> {}", orderStatus.getType());
+ QueryWrapper queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("order_no", orderNo);
+ OrderInfo orderInfo = new OrderInfo();
+ orderInfo.setOrderStatus(orderStatus.getType());
+ orderInfoMapper.update(orderInfo, queryWrapper);
+ }
+
+ @Override
+ public String getOrderStatus(String orderNo) {
+ QueryWrapper queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("order_no", orderNo);
+ OrderInfo orderInfo = orderInfoMapper.selectOne(queryWrapper);
+ if (orderInfo == null) {
+ return null;
+ }
+ return orderInfo.getOrderStatus();
+ }
+
+ @Override
+ public List 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
+ * @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/PaymentInfoServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/PaymentInfoServiceImpl.java
index a28a68b343d4b187d958903f2d4bb0594b8c68b9..c29d8d5d307121682c20776dfafcb41373cc37b9 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/RefundInfoServiceImpl.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/RefundInfoServiceImpl.java
index ee084e3651c0b68d572aac551ff80b17f2334052..6cb75ee10cf239f1cb8b9467c4d0b70ef497a286 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
@@ -1,12 +1,82 @@
package com.hongyi.paymentdemo.service.impl;
+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;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+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
public class RefundInfoServiceImpl 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);
+ }
+
+ @Override
+ public List 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
new file mode 100644
index 0000000000000000000000000000000000000000..587d19f1d9bdb35478d4d6de4a7aa84598867e35
--- /dev/null
+++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/service/impl/WxPayServiceImpl.java
@@ -0,0 +1,494 @@
+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.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.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;
+import java.io.IOException;
+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
+ * @Date 2022/6/28 17:19
+ * @Version 1.0
+ */
+@Service
+@Slf4j
+public class WxPayServiceImpl implements WxPayService {
+
+ @Resource
+ private WxPayConfig wxPayConfig;
+
+ @Resource
+ private CloseableHttpClient httpClient;
+
+ @Resource
+ private OrderInfoService orderInfoService;
+
+ @Resource
+ private PaymentInfoService paymentInfoService;
+
+ @Resource
+ private RefundInfoService refundInfoService;
+
+ private final ReentrantLock lock = new ReentrantLock();
+
+ /**
+ * 创建订单,调用native支付接口
+ * @param productId 购买产品的id
+ * @return code_url和订单号
+ * @throws Exception
+ */
+ @Override
+ public Map nativePay(Long productId) throws Exception {
+ log.info("生成订单");
+
+ // 生成订单
+ 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请求
+ 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);
+ // 二维码
+ codeUrl = resultMap.get("code_url");
+ // 保存二维码
+ String orderNo = orderInfo.getOrderNo();
+ orderInfoService.saveCodeUrl(orderNo, codeUrl);
+ // 返回二维码
+ HashMap map = new HashMap<>();
+ map.put("codeUrl", codeUrl);
+ // 订单编号
+ map.put("orderNo", orderInfo.getOrderNo());
+ return map;
+ } finally {
+ 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"); // 订单号
+ // 数据锁:成功获取则返回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();
+ }
+ }
+ }
+
+ @Override
+ public void cancelOrder(String orderNo) throws IOException {
+ // 调用微信支付的关闭订单接口
+ this.closeOrder(orderNo);
+ // 更新商户端的订单状态
+ 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();
+ }
+ }
+
+ @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
+ */
+ 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
+ * @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/java/com/hongyi/paymentdemo/task/WxPayTask.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/task/WxPayTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..7b7b01fbc74b66ae8f6afc77d837d915241c6c50
--- /dev/null
+++ b/payment-demo/src/main/java/com/hongyi/paymentdemo/task/WxPayTask.java
@@ -0,0 +1,77 @@
+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;
+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;
+
+ @Resource
+ private RefundInfoService refundInfoService;
+
+ /**
+ * 秒 分 时 日 月 周
+ * * 每秒都执行
+ * 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);
+ }
+ }
+
+ /**
+ * 从第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);
+ }
+ }
+}
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 0000000000000000000000000000000000000000..3a4d73579df89a655f1dadaf150d782c1d482c62
--- /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 0000000000000000000000000000000000000000..dfdcacb54cc48fe9cd4620b1a65a8aaecc7c4c29
--- /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 0000000000000000000000000000000000000000..18542e0e84a5c9a304e569e5504f54010536eab3
--- /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 0000000000000000000000000000000000000000..311b86b42783feb1f9572d3ebcc114e7c445e8f2
--- /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/main/java/com/hongyi/paymentdemo/vo/R.java b/payment-demo/src/main/java/com/hongyi/paymentdemo/vo/R.java
index d92595d967121a5babb8a8be9e48ff24fa75ff22..01a013d7e41c49c1cb2016fce4f44cf6b800dc9e 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/application.yml b/payment-demo/src/main/resources/application.yml
index 19090adff740b8da62360ac1bcb5acb35f335f07..fd60c70cd7346733f3e6e8b1e283da83eed45f7a 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
diff --git a/payment-demo/src/main/resources/wxpay.properties b/payment-demo/src/main/resources/wxpay.properties
index 8fad36c55e2b3334eb9448af010d9420ab50f73c..2df1e31298ee17da5ce01bf1f6ffeb7818f2c6d8 100644
--- a/payment-demo/src/main/resources/wxpay.properties
+++ b/payment-demo/src/main/resources/wxpay.properties
@@ -8,8 +8,9 @@ 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
# 接收结果通知地址
-wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io
+# 注意:每次重新启动ngrok,都需要修改这个配置
+wxpay.notify-domain=https://4fcc-117-174-85-8.ap.ngrok.io
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 4dd25fae475b3f3ba6faa2d2e64ed764c30a808f..4fe1379c211f9291e8bc13b8a249f5c1b1797f44 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);
}
}