diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 6cf7c9e..9570930 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -19,16 +19,16 @@
+ * 微信小程序支付服务实现 + *
+ * + * @author songfayuan + * @date 2024/9/14 14:23 + */ +@Slf4j +@Service +public class WxMiniappPayServiceImpl implements WxMiniappPayService { + + /** + * 微信小程序的 AppID + */ + @Value("${wx.miniapp.appid}") + private String appid; + + /** + * 微信小程序的密钥 + */ + @Value("${wx.miniapp.secret}") + private String secret; + + /** + * 商户号 + */ + @Value("${wx.miniapp.merchantId}") + private String merchantId; + + /** + * 商户API私钥路径 + */ + @Value("${wx.miniapp.privateKeyPath}") + private String privateKeyPath; + + /** + * 商户证书序列号 + */ + @Value("${wx.miniapp.merchantSerialNumber}") + private String merchantSerialNumber; + + /** + * 商户APIV3密钥 + */ + @Value("${wx.miniapp.apiV3Key}") + private String apiV3Key; + + /** + * 支付通知地址 + */ + @Value("${wx.miniapp.payNotifyUrl}") + private String payNotifyUrl; + + /** + * 退款通知地址 + */ + @Value("${wx.miniapp.refundNotifyUrl}") + private String refundNotifyUrl; + + + @Autowired + @Qualifier("rsaAutoCertificateConfig") + private Config config; + @Autowired + @Qualifier("rsaAutoCertificateConfig") + private Config notificationConfig; + + + @Autowired + private IPopularizeOrderService popularizeOrderService; + + + + /** + * 退款 + *+ * 交易时间超过一年的订单无法提交退款(按支付成功时间+365天计算) + * 微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号 + * 请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次 + * 每个支付订单的部分退款次数不能超过50次 + * 如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败 + * 申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果 + * 错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次 + * 一个月之前的订单申请退款频率限制为:5000/min + * 同一笔订单多次退款的请求需相隔1分钟 + *+ * + * @param req + * @return + */ + @Override + public Response refund(RefundOrderReq req) { + + // 初始化服务 + RefundService service = new RefundService.Builder().config(config).build(); + + //根据订单标识查询订单 + PopularizeOrder payOrder = popularizeOrderService.lambdaQuery() + .eq(PopularizeOrder::getOutTradeNo, req.getOutTradeNo()) + .one(); + if (payOrder == null) { + return Response.error("订单不存在"); + } + if (payOrder.getPayPrice().compareTo(req.getRefundAmount()) < 0) { + return Response.error("退款金额不能大于支付金额"); + } + + CreateRequest request = new CreateRequest(); + // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义 + request.setOutTradeNo(req.getOutTradeNo()); + request.setOutRefundNo("REFUND_" + req.getOutTradeNo()); + AmountReq amount = new AmountReq(); + // 订单总金额,单位为分,只能为整数,详见支付金额 + amount.setTotal(decimalToLong(req.getTotalAmount())); + // 退款金额,单位为分,只能为整数,不能超过支付总额 + amount.setRefund(decimalToLong(req.getRefundAmount())); + amount.setCurrency("CNY"); + + request.setAmount(amount); + request.setNotifyUrl(refundNotifyUrl); + // 调用接口 + Refund refund = null; + try { + refund = service.create(request); + } catch (HttpException e) { + log.error("退款申请失败,发送HTTP请求失败:", e); + return Response.error("退款失败"); + } catch (MalformedMessageException e) { + log.error("退款申请失败,解析微信支付应答或回调报文异常,返回信息:", e); + return Response.error("退款失败"); + } catch (ValidationException e) { + log.error("退款申请失败,验证签名失败,返回信息:", e); + return Response.error("验证签名失败"); + } catch (ServiceException e) { + log.error("退款申请失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e); + return Response.error("退款失败:" + e.getErrorMessage()); + } catch (Exception e) { + log.error("退款申请失败,异常:", e); + return Response.error("退款失败"); + } + if (Status.SUCCESS.equals(refund.getStatus())) { + log.info("退款成功!-订单号:{}", req.getOutTradeNo()); + return Response.success("退款成功"); + } else if (Status.CLOSED.equals(refund.getStatus())) { + log.info("退款关闭!-订单号:{}", req.getOutTradeNo()); + return Response.error("退款关闭"); + } else if (Status.PROCESSING.equals(refund.getStatus())) { + log.info("退款处理中!-订单号:{}", req.getOutTradeNo()); + return Response.error("退款处理中"); + } else if (Status.ABNORMAL.equals(refund.getStatus())) { + log.info("退款异常!-订单号:{}", req.getOutTradeNo()); + return Response.error("退款异常"); + } + return Response.error("退款失败"); + } + + /** + * 查询单笔退款(通过商户退款单号) + *
+ * 提交退款申请后,通过调用该接口查询退款状态。退款有一定延时,建议查询退款状态在提交退款申请后1分钟发起,一般来说零钱支付的退款5分钟内到账,银行卡支付的退款1-3个工作日到账。 + *+ * + * @param outRefundNo 商户退款单号 + * @return + */ + @Override + public Response queryByOutRefundNo(String outRefundNo) { + // 初始化服务 + RefundService service = new RefundService.Builder().config(config).build(); + QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest(); + // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义 + request.setOutRefundNo(outRefundNo.trim()); + // 调用接口 + Refund refund = null; + try { + refund = service.queryByOutRefundNo(request); + log.info("退款查询结果:{}", JSONObject.toJSONString(refund)); + //【退款状态】退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台(pay.weixin.qq.com)-交易中心,手动处理此笔退款。可选取值:SUCCESS:退款成功CLOSED:退款关闭PROCESSING:退款处理中ABNORMAL:退款异常【退款状态】退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台(pay.weixin.qq.com)-交易中心,手动处理此笔退款。可选取值:SUCCESS:退款成功CLOSED:退款关闭PROCESSING:退款处理中ABNORMAL:退款异常 + if (Status.SUCCESS.equals(refund.getStatus())) { + log.info("退款成功!-订单号:{}", outRefundNo); + return Response.success("退款成功"); + } else if (Status.CLOSED.equals(refund.getStatus())) { + log.info("退款关闭!-订单号:{}", outRefundNo); + return Response.error("退款关闭"); + } else if (Status.PROCESSING.equals(refund.getStatus())) { + log.info("退款处理中!-订单号:{}", outRefundNo); + return Response.success("退款处理中"); + } else if (Status.ABNORMAL.equals(refund.getStatus())) { + log.info("退款异常!-订单号:{}", outRefundNo); + return Response.error("退款异常"); + } + } catch (HttpException e) { + log.error("退款查询失败,发送HTTP请求失败:", e); + return Response.error("退款查询失败"); + } catch (MalformedMessageException e) { + log.error("退款查询失败,解析微信支付应答或回调报文异常,返回信息:", e); + return Response.error("退款查询失败"); + } catch (ValidationException e) { + log.error("退款查询失败,验证签名失败,返回信息:", e); + return Response.error("退款查询失败"); + } catch (ServiceException e) { + log.error("退款查询失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e); + return Response.error("退款查询失败"); + } catch (Exception e) { + log.error("退款查询失败,异常:", e); + return Response.error("退款查询失败"); + } + + return Response.success(refund); + } + + /** + * 微信小程序退款回调 + *
+ * 注意: + * 对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功 + * + * 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。 + * 如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。 + *+ * + * @param request + * @return + */ + @Override + public String refundNotify(HttpServletRequest request) { + Map
+ * HttpServletRequest 获取请求体 + *
+ * + * @author songfayuan + * @date 2024/9/30 19:11 + */ +public class HttpServletUtils { + + /** + * 获取请求体 + * + * @param request + * @return + * @throws IOException + */ + public static String getRequestBody(HttpServletRequest request) throws IOException { + ServletInputStream stream = null; + BufferedReader reader = null; + StringBuffer sb = new StringBuffer(); + try { + stream = request.getInputStream(); + // 获取响应 + reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + throw new IOException("读取返回支付接口数据流出现异常!"); + } finally { + reader.close(); + } + return sb.toString(); + } + +} diff --git a/module-common/src/main/java/org/jeecg/api/wxUtils/PayOrderInfo.java b/module-common/src/main/java/org/jeecg/api/wxUtils/PayOrderInfo.java new file mode 100644 index 0000000..aae93ce --- /dev/null +++ b/module-common/src/main/java/org/jeecg/api/wxUtils/PayOrderInfo.java @@ -0,0 +1,31 @@ +package org.jeecg.api.wxUtils; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + *+ * 支付订单信息 + *
+ * + * @author songfayuan + * @date 2024/9/30 17:46 + */ +@Data +public class PayOrderInfo { + /** + * 订单标题 + */ + private String description; + /** + * 商户订单号 + * 只能是数字、大小写字母_-*且在同一个商户号下唯一。 + */ + private String outTradeNo; + /** + * 支付金额,单位:元 + */ + private BigDecimal amount; +} + diff --git a/module-common/src/main/java/org/jeecg/api/wxUtils/QueryOrderReq.java b/module-common/src/main/java/org/jeecg/api/wxUtils/QueryOrderReq.java new file mode 100644 index 0000000..ee2afc8 --- /dev/null +++ b/module-common/src/main/java/org/jeecg/api/wxUtils/QueryOrderReq.java @@ -0,0 +1,23 @@ +package org.jeecg.api.wxUtils; + +import lombok.Data; + +/** + *+ * 订单查询请求 + *
+ * + * @author songfayuan + * @date 2024/9/30 19:19 + */ +@Data +public class QueryOrderReq { + /** + * 订单号:业务侧的订单号 + */ + private String transactionId; + /** + * 商户订单号 + */ + private String outTradeNo; +} diff --git a/module-common/src/main/java/org/jeecg/api/wxUtils/RefundOrderReq.java b/module-common/src/main/java/org/jeecg/api/wxUtils/RefundOrderReq.java new file mode 100644 index 0000000..9f63f4f --- /dev/null +++ b/module-common/src/main/java/org/jeecg/api/wxUtils/RefundOrderReq.java @@ -0,0 +1,36 @@ +package org.jeecg.api.wxUtils; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + *+ * 退款订单请求参数 + *
+ * + * @author songfayuan + * @date 2024/9/30 19:19 + */ +@Data +public class RefundOrderReq { + /** + * 订单号:业务侧的订单号 + */ + private String transactionId; + /** + * 商户订单号 + */ + private String outTradeNo; + + /** + * 原订单金额 说明:原支付交易的订单总金额,这里单位为元。 + */ + private BigDecimal totalAmount; + + /** + * 退款金额 说明:退款金额,这里单位为元,不能超过原订单支付金额。 + */ + private BigDecimal refundAmount; +} + diff --git a/module-common/src/main/java/org/jeecg/api/wxUtils/Response.java b/module-common/src/main/java/org/jeecg/api/wxUtils/Response.java new file mode 100644 index 0000000..4eff69e --- /dev/null +++ b/module-common/src/main/java/org/jeecg/api/wxUtils/Response.java @@ -0,0 +1,164 @@ +package org.jeecg.api.wxUtils; + +import lombok.Data; + +/** + * 返回参数封装类 + * + * @param