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); } }