From 13449157f6f409b4cfb0b1a8fd94cbb7b795c4ee Mon Sep 17 00:00:00 2001
From: xuhy <3313886187@qq.com>
Date: 星期四, 06 十一月 2025 10:08:04 +0800
Subject: [PATCH] 微信小程序支付,小程序登录
---
ruoyi-admin/src/main/resources/application-test.yml | 25
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java | 53 +
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java | 41
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java | 108 ++
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java | 122 ++
ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java | 80 +
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java | 118 ++
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java | 113 ++
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java | 164 +++
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java | 2
ruoyi-admin/pom.xml | 6
ruoyi-system/pom.xml | 11
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java | 28
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java | 53
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java | 400 ++++++++
ruoyi-applet/pom.xml | 6
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java | 31
ruoyi-applet/src/main/resources/application-test.yml | 25
ruoyi-common/pom.xml | 6
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java | 222 ++++
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java | 20
/dev/null | 77 -
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java | 201 ++++
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java | 55 +
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java | 9
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java | 52 +
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java | 84 +
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java | 441 +++++++++
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java | 235 +++++
29 files changed, 2,671 insertions(+), 117 deletions(-)
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 18b4c52..a53beab 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -155,6 +155,12 @@
<version>2.0.10</version>
</dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ <version>4.9.3</version>
+ </dependency>
+
</dependencies>
<build>
diff --git a/ruoyi-admin/src/main/resources/application-test.yml b/ruoyi-admin/src/main/resources/application-test.yml
index d5ca74f..ffe4765 100644
--- a/ruoyi-admin/src/main/resources/application-test.yml
+++ b/ruoyi-admin/src/main/resources/application-test.yml
@@ -194,4 +194,27 @@
wx:
conf:
appId:
- secretId:
\ No newline at end of file
+ secretId:
+payment:
+ wx:
+ # 微信appid
+ appId:
+ # 微信商户号
+ mchId:
+ # 秘钥
+ secretId:
+ v3:
+ # 加签串
+ apiKey:
+ # 加签证书地址
+ privateKeyPath:
+ # 微信支付公钥id
+ publicKeyId:
+ # 微信支付公钥证书
+ publicKeyPath:
+ # 证书序列号
+ mchSerialNo:
+ # 支付成功回调地址
+ notifyPayUrl:
+ # 支付退款回调地址
+ notifyRefundUrl:
\ No newline at end of file
diff --git a/ruoyi-applet/pom.xml b/ruoyi-applet/pom.xml
index 335f596..2e585f6 100644
--- a/ruoyi-applet/pom.xml
+++ b/ruoyi-applet/pom.xml
@@ -170,6 +170,12 @@
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ <version>4.9.3</version>
+ </dependency>
+
</dependencies>
<build>
diff --git a/ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java b/ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java
new file mode 100644
index 0000000..b18935b
--- /dev/null
+++ b/ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java
@@ -0,0 +1,122 @@
+package com.ruoyi.web.controller.api;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.framework.web.service.TokenService;
+import com.ruoyi.system.wxPay.enums.TradeStateEnum;
+import com.ruoyi.system.wxPay.model.WeixinPayProperties;
+import com.ruoyi.system.wxPay.utils.WxV3Pay;
+import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
+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.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedReader;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * 微信相关接口
+ */
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/wx/")
+@Api(tags = {"微信支付相关接口"})
+public class WxPayController {
+ @Autowired
+ private WxV3Pay wxV3Pay;
+ @Autowired
+ private TokenService tokenService;
+ @Autowired
+ private WeixinPayProperties weixinPayProperties;
+
+
+
+ /**
+ * 按实际修改
+ */
+ @ApiOperation("订单支付")
+ @PostMapping("orderPay")
+ public R<Map<String, Object>> orderPay(@RequestParam(value = "inspectionId") String inspectionId) {
+
+// TSysOtherConfig sysOtherConfig = sysOtherConfigService.getById(1);
+// if (Objects.isNull(sysOtherConfig.getAiPrice())) {
+// return R.fail("系统未配置AI支付价格");
+// }
+//
+// String userId = tokenService.getLoginUserApplet().getUserId();
+// TSysAppUser sysAppUser = sysAppUserService.getById(userId);
+// // 价格
+// Long totalPrice = sysOtherConfig.getAiPrice().multiply(new BigDecimal(100)).longValue();
+// // 生成订单号
+// String orderNo = CodeGenerateUtils.generateOrderSn();
+// // 查询用户信息 用户openid
+// String openId = sysAppUser.getOpenId();
+// // 存储支付记录
+// sysPayRecordService.saveData(orderNo, userId, sysOtherConfig.getAiPrice(), 1);
+// // 调用支付方法
+// Map<String, Object> result = wxV3Pay.jsApi(orderNo, totalPrice, openId,"AI检测报告支付","");
+// log.info("支付参数:{}", result);
+ return R.ok();
+ }
+
+ /**
+ * 支付回调
+ */
+ @PostMapping("pay/notify")
+ @ApiOperation("订单回调")
+ public void payNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ try {
+ System.err.println("微信回调");
+ BufferedReader reader = request.getReader();
+ StringBuilder requestBody = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ requestBody.append(line);
+ }
+ System.err.println("全部请求体" + requestBody);
+ JSONObject jsonObject = JSONObject.parseObject(requestBody.toString());
+ JSONObject resource = jsonObject.getJSONObject("resource");
+
+ AesUtil aesUtil = new AesUtil(weixinPayProperties.getV3().getApiKey().getBytes(StandardCharsets.UTF_8));
+ String decryptedData = aesUtil.decryptToString(resource.getString("associated_data").getBytes(StandardCharsets.UTF_8), resource.getString("nonce").getBytes(StandardCharsets.UTF_8),
+ resource.getString("ciphertext"));
+ System.err.println("微信解密的字符串信息" + decryptedData);
+ JSONObject jsonInfo = JSONObject.parse(decryptedData);
+ String out_trade_no = jsonInfo.getString("out_trade_no");
+ String transaction_id = jsonInfo.getString("transaction_id");
+ String trade_state = jsonInfo.getString("trade_state");
+ String success_time = jsonInfo.getString("success_time");
+ // 查询订单
+// TSysPayRecord sysPayRecord = sysPayRecordService.getOne(Wrappers.lambdaQuery(TSysPayRecord.class)
+// .eq(TSysPayRecord::getOrderNo, out_trade_no).last("LIMIT 1"));
+ if (trade_state.equals(TradeStateEnum.SUCCESS.name())) {
+ log.info("回调成功");
+ // 订单号查询订单
+// sysPayRecord.setPayState(2);
+// sysPayRecord.setPayTime(WxTimeUtils.dateToLocalDateTime(WxTimeUtils.toRfc3339Date(success_time)));
+// sysPayRecord.setTransactionId(transaction_id);
+// sysPayRecordService.updateById(sysPayRecord);
+//
+// // 处理检测报告为可见
+// TSysInspection sysInspection = sysInspectionService.getById(sysPayRecord.getInspectionId());
+// sysInspection.setIsPay(1);
+// sysInspectionService.updateById(sysInspection);
+
+ PrintWriter out = response.getWriter();
+ out.print("SUCCESS");
+ out.flush();
+ out.close();
+ }
+ } catch (Exception e) {
+ log.error("支付回调异常:{}", e, e);
+ wxV3Pay.ack(false, e.getMessage());
+ }
+ }
+}
diff --git a/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
index 78126f5..3936557 100644
--- a/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
+++ b/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
@@ -1,29 +1,48 @@
package com.ruoyi.web.controller.system;
import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.entity.TAppUserResp;
import com.ruoyi.common.core.domain.model.LoginAppBody;
import com.ruoyi.common.core.domain.model.LoginUserApplet;
import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.framework.web.service.TokenService;
+import com.ruoyi.system.model.TAppUser;
import com.ruoyi.system.service.ISysMenuService;
import com.ruoyi.system.service.ISysRoleService;
+import com.ruoyi.system.service.TAppUserService;
+import com.ruoyi.system.utils.wx.body.resp.Code2SessionRespBody;
+import com.ruoyi.system.utils.wx.body.resq.Code2SessionResqBody;
+import com.ruoyi.system.utils.wx.model.WeixinProperties;
+import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData;
+import com.ruoyi.system.utils.wx.pojo.AppletUserEncrypteData;
+import com.ruoyi.system.utils.wx.tools.WxAppletTools;
+import com.ruoyi.system.utils.wx.tools.WxUtils;
import com.ruoyi.web.controller.tool.MsgUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -31,6 +50,7 @@
*
* @author ruoyi
*/
+@Slf4j
@Api(tags = "登录")
@RestController
public class SysLoginController
@@ -51,7 +71,14 @@
private ISysRoleService roleService;
@Autowired
private MsgUtils msgUtils;
-
+ @Autowired
+ private RestTemplate wxRestTemplate;
+ @Autowired
+ private WeixinProperties wxConfig;
+ @Autowired
+ private RedisService redisService;
+ @Autowired
+ private TAppUserService appUserService;
/**
* 账号密码登录
*
@@ -59,7 +86,7 @@
* @return 结果
*/
@ApiOperation(value = "账号密码登录",notes = "账号密码登录")
- @PostMapping("/api/loginApp")
+ @PostMapping("/loginApp")
public AjaxResult login(@RequestBody String param)
{
LoginAppBody loginAppBody = JSON.parseObject(param, LoginAppBody.class);
@@ -68,10 +95,57 @@
LoginUserApplet loginUser = loginService.loginCodeApplet(loginAppBody.getAccount(), loginAppBody.getPassword());
ajax.put(Constants.TOKEN, tokenService.createTokenApplet(loginUser));
ajax.put("data",loginUser);
-
return ajax;
}
+ @ApiOperation(value = "通过code获得openid,获取用户信息",tags = {"微信小程序登录"})
+ @PostMapping("/openIdByJsCode")
+ public R<Map<String, Object>> openIdByJsCode(@RequestBody AppletUserEncrypteData data) {
+ log.info("<<<<<<<<换取openid开始<<<<<<<<:{}", data.getCode());
+ WxAppletTools appletTools = new WxAppletTools(wxRestTemplate, wxConfig, redisService);
+ Code2SessionRespBody body = appletTools.getOpenIdByJscode2session(new Code2SessionResqBody().build(data.getCode()));
+ String openid = body.getOpenid();
+ String sessionKey = body.getSessionKey();
+ // 用户信息解密 数据验签
+// if (StringUtils.isNotBlank(data.getSignature())) {
+// WxUtils.verifySignature(data.getRawData(), sessionKey, data.getSignature());
+// }
+ if(StringUtils.isEmpty(data.getEncryptedData()) || StringUtils.isEmpty(data.getIv())){
+ return R.fail("已拒绝授权");
+ }
+ AppletUserDecodeData appletUserDecodeData = WxUtils.encryptedData(data.getEncryptedData(), sessionKey, data.getIv());
+ appletUserDecodeData.setOpenId(openid);
+ // 先使用openId和当前手机号进行查询
+ TAppUser sysAppUser = appUserService.getOne(Wrappers.lambdaQuery(TAppUser.class)
+ .and(e->e.eq(TAppUser::getOpenId, appletUserDecodeData.getOpenId()).or()
+ .eq(TAppUser::getPhoneNumber, appletUserDecodeData.getPhoneNumber())));
+ if (sysAppUser==null){
+ sysAppUser = new TAppUser();
+ sysAppUser.setPhoneNumber(appletUserDecodeData.getPhoneNumber());
+ sysAppUser.setOpenId(appletUserDecodeData.getOpenId());
+ // 手机号中间四位替换为*
+ sysAppUser.setNickName(appletUserDecodeData.getPhoneNumber().replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
+ appUserService.save(sysAppUser);
+ }else {
+ // 判断账号是否被冻结
+ if (sysAppUser.getStatus() == 2){
+ return R.fail("账号已冻结,请联系管理员");
+ }
+ // 手机号相同但openId为空的情况
+ sysAppUser.setOpenId(appletUserDecodeData.getOpenId());
+ appUserService.updateById(sysAppUser);
+ }
+ LoginUserApplet loginUserApplet = new LoginUserApplet();
+ TAppUserResp sysUserResp = new TAppUserResp();
+ BeanUtils.copyProperties(sysAppUser, sysUserResp);
+ loginUserApplet.setUser(sysUserResp);
+ loginUserApplet.setUserId(sysAppUser.getId());
+ Map<String, Object> tokenInfos = new HashMap<>();
+ tokenInfos.put("token",tokenService.createTokenApplet(loginUserApplet));
+ tokenInfos.put("info",loginUserApplet);
+ return R.ok(tokenInfos);
+ }
+
/**
* 获取用户信息
diff --git a/ruoyi-applet/src/main/resources/application-test.yml b/ruoyi-applet/src/main/resources/application-test.yml
index d451e0f..4fcc7b6 100644
--- a/ruoyi-applet/src/main/resources/application-test.yml
+++ b/ruoyi-applet/src/main/resources/application-test.yml
@@ -194,4 +194,27 @@
wx:
conf:
appId:
- secretId:
\ No newline at end of file
+ secretId:
+payment:
+ wx:
+ # 微信appid
+ appId:
+ # 微信商户号
+ mchId:
+ # 秘钥
+ secretId:
+ v3:
+ # 加签串
+ apiKey:
+ # 加签证书地址
+ privateKeyPath:
+ # 微信支付公钥id
+ publicKeyId:
+ # 微信支付公钥证书
+ publicKeyPath:
+ # 证书序列号
+ mchSerialNo:
+ # 支付成功回调地址
+ notifyPayUrl:
+ # 支付退款回调地址
+ notifyRefundUrl:
\ No newline at end of file
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
index 4d6fbd9..047f099 100644
--- a/ruoyi-common/pom.xml
+++ b/ruoyi-common/pom.xml
@@ -219,6 +219,12 @@
<!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api -->
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ <version>4.9.3</version>
+ </dependency>
+
</dependencies>
</project>
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java
deleted file mode 100644
index 770412a..0000000
--- a/ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.ruoyi.common.config;
-
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-
-
-/**
- * 项目中需继承此类
- *
- * @author lihen
- */
-@Data
-@Component
-@ConfigurationProperties(prefix = "wx.config")
-public class WxConfig {
-
- /**
- * 获取 App ID
- *
- * @return App ID
- */
- @JsonProperty("appId")
- private String appId;
-
- /**
- * 获取 Secret
- *
- * @return Secret
- */
- @JsonProperty("secret")
- private String secret;
-
-
-}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java
index d1bd1e8..abf49de 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java
@@ -6,6 +6,8 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
+import java.time.LocalDate;
+
/**
* <p>
* 租户
@@ -28,30 +30,18 @@
private String nickName;
@ApiModelProperty(value = "手机号")
- private String phone;
+ private String phoneNumber;
- @ApiModelProperty(value = "登录账号")
- private String account;
+ @ApiModelProperty(value = "头像")
+ private String avatar;
- @ApiModelProperty(value = "密码")
- private String password;
+ @ApiModelProperty(value = "性别 1=男 2=女")
+ private Integer gender;
- @ApiModelProperty(value = "分队名称 注册提交时使用")
- private String teamName;
-
- @ApiModelProperty(value = "分队id")
- private String teamId;
+ @ApiModelProperty(value = "生日")
+ private LocalDate birthDate;
@ApiModelProperty(value = "状态 1=启用 2=禁用")
private Integer status;
-
- @ApiModelProperty(value = "实训次数")
- private Integer practicalTrainCount;
-
- @ApiModelProperty(value = "其他次数")
- private Integer otherCount;
-
- @ApiModelProperty(value = "审核状态 0=待审核 1=通过 2=拒绝")
- private Integer state;
}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java
index bf66bf3..f2d8b61 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java
@@ -127,7 +127,7 @@
@Override
public String getPassword()
{
- return user.getPassword();
+ return null;
}
@Override
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java
deleted file mode 100644
index 7c6ee85..0000000
--- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.ruoyi.common.utils;
-
-import com.alibaba.fastjson2.JSONObject;
-import com.ruoyi.common.config.WxConfig;
-import com.ruoyi.common.core.redis.RedisCache;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
-import org.springframework.web.client.RestTemplate;
-
-import java.text.MessageFormat;
-
-/**
- * @author liheng
- * @ClassName WxAppletTools
- * @Description
- * @date 2020-12-04 13:55
- */
-@Slf4j
-@Component
-public class WxAppletTools {
- @Autowired
- private RedisCache redisCache;
- @Autowired
- private RestTemplate restTemplate;
- @Autowired
- private WxConfig wxConfig;
- private final static String ACCESSTOKEN_CACHE_KEY = "accessToken";
- /**
- * 请求参数
- * 属性 类型 默认值 必填 说明
- * appid string 是 小程序 appId
- * secret string 是 小程序 appSecret
- * js_code string 是 登录时获取的 code
- * grant_type string 是 授权类型,此处只需填写 authorization_cod
- * <p>
- * 返回值:
- * <p>
- * 属性 类型 说明
- * openid string 用户唯一标识
- * session_key string 会话密钥
- * unionid string 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
- * errcode number 错误码
- * errmsg string 错误信息
- */
- private static final String JSCODE_2_SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
-
-
- /**
- * 请求参数
- * 属性 类型 默认值 必填 说明
- * grant_type string 是 填写 client_credential
- * appid string 是 小程序唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得。(需要已经成为开发者,且帐号没有异常状态)
- * secret string 是 小程序唯一凭证密钥,即 AppSecret,获取方式同 appid
- * 返回值
- * Object
- * 返回的 JSON 数据包
- * <p>
- * 属性 类型 说明
- * access_token string 获取到的凭证
- * expires_in number 凭证有效时间,单位:秒。目前是7200秒之内的值。
- * errcode number 错误码
- * errmsg string 错误信息
- */
- private static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
-
- /**
- * @return
- */
- public String getAccessToken() {
- String requestUrl = MessageFormat.format(ACCESS_TOKEN_URL, wxConfig.getAppId(), wxConfig.getSecret());
- String respBody = restTemplate.getForEntity(requestUrl, String.class).getBody();
- JSONObject jsonObject = JSONObject.parseObject(respBody);
- return jsonObject.getString("access_token");
- }
-}
diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml
index 5adce03..a19e805 100644
--- a/ruoyi-system/pom.xml
+++ b/ruoyi-system/pom.xml
@@ -88,6 +88,17 @@
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.github.wechatpay-apiv3</groupId>
+ <artifactId>wechatpay-java-core</artifactId>
+ <version>0.2.12</version>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.github.wechatpay-apiv3</groupId>
+ <artifactId>wechatpay-apache-httpclient</artifactId>
+ <version>0.4.9</version>
+ </dependency>
</dependencies>
</project>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java b/ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java
index 6287358..3692b6c 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java
@@ -1,7 +1,7 @@
package com.ruoyi.system.utils.wx.tools;
-import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData;
import com.ruoyi.common.utils.sign.Base64;
+import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.CharEncoding;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -11,14 +11,16 @@
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
+import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.AlgorithmParameters;
+import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
+import java.util.Random;
/**
* @Description 获取用户信息工具类
@@ -27,7 +29,12 @@
*/
@Slf4j
public class WxUtils {
+ /**
+ * 随机字符
+ */
+ private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ private static final Random RANDOM = new SecureRandom();
/**
* 微信小程序API 用户数据的解密
*
@@ -95,20 +102,36 @@
* @return
*/
public static String streamBodyByReceive(HttpServletRequest request) throws IOException {
- log.info("微信异步回调地址:{}", request.getRequestURL());
- StringBuffer buffer = new StringBuffer();
- InputStream inputStream = request.getInputStream();
- InputStreamReader reader = new InputStreamReader(inputStream);
- BufferedReader bufferedReader = new BufferedReader(reader);
- String body = null;
- while ((body = bufferedReader.readLine()) != null) {
- buffer.append(body);
+ BufferedReader reader = null;
+ StringBuffer sb = new StringBuffer();
+ try {
+ ServletInputStream stream = request.getInputStream();
+ // 获取响应
+ reader = new BufferedReader(new InputStreamReader(stream));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("读取微信支付接口数据流出现异常!");
+ } finally {
+ reader.close();
+ WxUtils.info(sb.toString());
}
- String data = buffer.toString();
- reader.close();
- inputStream.close();
- log.info("微信异步回调数据:{}", data);
- return data;
+ return sb.toString();
+ }
+
+ /**
+ * 获取随机字符串 Nonce Str
+ *
+ * @return String 随机字符串
+ */
+ public static String generateNonceStr() {
+ char[] nonceChars = new char[32];
+ for (int index = 0; index < nonceChars.length; ++index) {
+ nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
+ }
+ return new String(nonceChars);
}
/**
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java
new file mode 100644
index 0000000..925ab1e
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java
@@ -0,0 +1,31 @@
+package com.ruoyi.system.wxPay.config;
+
+import com.ruoyi.system.wxPay.model.WeixinPayProperties;
+import com.ruoyi.system.wxPay.utils.WxV3Pay;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 项目中需继承此类
+ *
+ * @author lihen
+ */
+@Configuration
+public class WxConfig {
+
+ private final WeixinPayProperties weixinPayProperties;
+
+ @Autowired
+ public WxConfig(WeixinPayProperties weixinPayProperties) {
+ this.weixinPayProperties = weixinPayProperties;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(name = "wxV3Pay")
+ public WxV3Pay wxSpV3Pay() {
+ return new WxV3Pay(weixinPayProperties);
+ }
+
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java
new file mode 100644
index 0000000..61e3d2d
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java
@@ -0,0 +1,53 @@
+package com.ruoyi.system.wxPay.enums;
+
+import lombok.Getter;
+
+import java.util.stream.Stream;
+
+/**
+ * @author xiaochen
+ * @ClassName ProfitSharingEnum
+ * @Description
+ * @date 2021-11-21 11:15
+ */
+public enum RefundEnum {
+ /**
+ * 退款成功
+ */
+ SUCCESS("SUCCESS", "退款成功"),
+ /**
+ * 退款关闭
+ */
+ CLOSED("CLOSED", "退款关闭"),
+ /**
+ * 退款处理中
+ */
+ PROCESSING("PROCESSING", "退款处理中"),
+ /**
+ * 退款异常
+ */
+ ABNORMAL("ABNORMAL", "退款异常"),
+ ;
+
+ @Getter
+ private String code;
+ @Getter
+ private String desc;
+
+ RefundEnum(String code, String desc) {
+ this.code = code;
+ this.desc = desc;
+ }
+
+ /**
+ * 通过交易类型执行具体的交易方法
+ *
+ * @param code
+ * @return
+ */
+ public static RefundEnum fromValue(String code) {
+ return Stream.of(RefundEnum.values()).filter(fileType ->
+ fileType.getCode().toLowerCase().equals(code.toLowerCase())
+ ).findFirst().orElse(null);
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java
new file mode 100644
index 0000000..c78c222
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java
@@ -0,0 +1,41 @@
+package com.ruoyi.system.wxPay.enums;
+
+import lombok.Getter;
+
+import java.util.stream.Stream;
+
+/**
+ * @author xiaochen
+ * @ClassName AliTradeStateEnum
+ * @Description
+ * @date 2022-01-07 11:56
+ */
+public enum TradeStateEnum {
+ SUCCESS("支付成功"),
+ RETURN("已分账回退"),
+ FAIL("已失败"),
+ PROCESSING("处理中"),
+ FINISHED("分账完成"),
+ REFUND("转入退款"),
+ PAYERROR("支付失败(其他原因,如银行返回失败)"),
+ USERPAYING("用户支付中"),
+ CLOSED("已关闭"),
+ NOTPAY("未支付"),
+ UNKNOWN("未知"),
+ DONE("服务订单完成"),
+ // ...
+ ;
+ @Getter
+ private String desc;
+
+ TradeStateEnum(String desc) {
+ this.desc = desc;
+ }
+
+ public static TradeStateEnum tradeState(String code) {
+ return Stream.of(TradeStateEnum.values()).filter(fileType ->
+ fileType.name().equals(code)
+ ).findFirst().orElse(UNKNOWN);
+ }
+
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java
new file mode 100644
index 0000000..d79804a
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java
@@ -0,0 +1,55 @@
+package com.ruoyi.system.wxPay.exception;
+
+/**
+ * @author lihen
+ */
+public class WxException extends RuntimeException {
+
+ private final static int OK = 0;
+ private final static int ValidateSignatureError = -40001;
+ private final static int ParseXmlError = -40002;
+ public final static int ComputeSignatureError = -40003;
+ private final static int IllegalAesKey = -40004;
+ private final static int ValidateAppidError = -40005;
+ private final static int EncryptAESError = -40006;
+ private final static int DecryptAESError = -40007;
+ private final static int IllegalBuffer = -40008;
+
+ private int code;
+
+ private static String getMessage(int code) {
+ switch (code) {
+ case ValidateSignatureError:
+ return "签名验证错误";
+ case ParseXmlError:
+ return "xml解析失败";
+ case ComputeSignatureError:
+ return "sha加密生成签名失败";
+ case IllegalAesKey:
+ return "SymmetricKey非法";
+ case ValidateAppidError:
+ return "appid校验失败";
+ case EncryptAESError:
+ return "aes加密失败";
+ case DecryptAESError:
+ return "aes解密失败";
+ case IllegalBuffer:
+ return "解密后得到的buffer非法";
+ default:
+ return null;
+ }
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public WxException(int code) {
+ super(getMessage(code));
+ this.code = code;
+ }
+
+ public WxException(String message) {
+ super(message);
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java
new file mode 100644
index 0000000..18062cb
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java
@@ -0,0 +1,108 @@
+package com.ruoyi.system.wxPay.model;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.util.IOUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author xiaochen
+ * @ClassName V3
+ * @Description
+ */
+@Slf4j
+@Data
+public class V3 {
+ /**
+ * 获取 API 密钥
+ *
+ * @return API密钥
+ */
+ private String apiKey;
+ /**
+ * 秘钥路径,apiclient_key.pem
+ */
+ private String privateKeyPath;
+ /**
+ * 微信支付公钥id
+ */
+ private String publicKeyId;
+ /**
+ * 微信支付公钥证书 apiclient_cert.pem
+ */
+ private String publicKeyPath;
+ /**
+ * 商户证书序列号
+ */
+ private String mchSerialNo;
+
+ /**
+ * 支付回调地址
+ *
+ * @return
+ */
+ private String notifyPayUrl;
+
+ /**
+ * 退款回调地址
+ *
+ * @return
+ */
+ private String notifyRefundUrl;
+
+ /**
+ * 退款回调地址
+ */
+ private String notifyTravelRefundUrl;
+
+
+ public InputStream getPrivateKeyStream() {
+ // 需要证书释放
+ byte[] certData;
+// InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(this.privateKeyPath);
+ InputStream certStream = null;
+ try {
+ certStream = new FileInputStream(this.privateKeyPath);
+ certData = IOUtils.toByteArray(certStream);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new RuntimeException("私钥文件未找到");
+ }finally {
+ if(null != certStream){
+ try {
+ certStream.close();
+ } catch (IOException e) {
+ log.error("私钥流关闭异常");
+ }
+ }
+ }
+ return new ByteArrayInputStream(certData);
+ }
+
+ public InputStream getPublicKeyStream() {
+ // 需要证书释放
+ byte[] certData;
+// InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(this.privateKeyPath);
+ InputStream certStream = null;
+ try {
+ certStream = new FileInputStream(this.publicKeyPath);
+ certData = IOUtils.toByteArray(certStream);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new RuntimeException("公钥文件未找到");
+ }finally {
+ if(null != certStream){
+ try {
+ certStream.close();
+ } catch (IOException e) {
+ log.error("公钥流关闭异常");
+ }
+ }
+ }
+ return new ByteArrayInputStream(certData);
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java
new file mode 100644
index 0000000..8463cfc
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java
@@ -0,0 +1,113 @@
+package com.ruoyi.system.wxPay.model;
+
+import lombok.ToString;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author xiaochen
+ * @ClassName WeixinProperties
+ * @Description
+ */
+@ToString
+@Component
+@ConfigurationProperties(prefix = "payment.wx")
+public class WeixinPayProperties {
+ /**
+ * 默认开启
+ */
+ private boolean enabled = true;
+ /**
+ * 获取 App ID
+ *
+ * @return App ID
+ */
+ private String appId;
+ /**
+ * 获取 Mch ID
+ *
+ * @return Mch ID
+ */
+ private String mchId;
+
+ /**
+ * 获取 secret ID
+ *
+ * @return secret ID
+ */
+ private String secretId;
+ /**
+ * 回调地址
+ */
+ private String callBackUrl;
+
+ public String getSecretId() {
+ return secretId;
+ }
+
+ public void setSecretId(String secretId) {
+ this.secretId = secretId;
+ }
+
+ /**
+ * v3
+ */
+ @NestedConfigurationProperty
+ private V3 v3;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public V3 getV3() {
+ return v3;
+ }
+
+ public void setV3(V3 v3) {
+ this.v3 = v3;
+ }
+
+ /**
+ * HTTP(S) 连接超时时间,单位毫秒
+ *
+ */
+ public int getHttpConnectTimeoutMs() {
+ return 6 * 1000;
+ }
+
+ /**
+ * HTTP(S) 读数据超时时间,单位毫秒
+ */
+ public int getHttpReadTimeoutMs() {
+ return 8 * 1000;
+ }
+
+ public String getAppId() {
+ return appId;
+ }
+
+ public void setAppId(String appId) {
+ this.appId = appId;
+ }
+
+ public String getMchId() {
+ return mchId;
+ }
+
+ public void setMchId(String mchId) {
+ this.mchId = mchId;
+ }
+
+ public String getCallBackUrl() {
+ return callBackUrl;
+ }
+
+ public void setCallBackUrl(String callBackUrl) {
+ this.callBackUrl = callBackUrl;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java
new file mode 100644
index 0000000..cc4c82c
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java
@@ -0,0 +1,20 @@
+package com.ruoyi.system.wxPay.model;
+
+import lombok.*;
+
+/**
+ * @author xiaochen
+ * @ClassName WxPaymentRefundModel
+ * @Description
+ */
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+@ToString
+public class WxCloseOrderModel {
+ private String mchid;
+
+ private String out_trade_no;
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java
new file mode 100644
index 0000000..af98ef9
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java
@@ -0,0 +1,201 @@
+package com.ruoyi.system.wxPay.model;
+
+import lombok.*;
+
+import java.util.List;
+
+/**
+ * @author xiaochen
+ * @ClassName WxPaymentInfoModel
+ * @Description
+ */
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+@ToString
+public class WxPaymentInfoModel {
+ /**
+ * 合单商户appid
+ */
+ private String combine_appid;
+ /**
+ * 合单商户号
+ */
+ private String combine_mchid;
+ /**
+ * 合单商户订单号
+ */
+ private String combine_out_trade_no;
+ /**
+ * 合单--子单信息
+ */
+ private List<SubOrders> sub_orders;
+ /**
+ * 合单--支付者
+ */
+ private CombinePayerInfo combine_payer_info;
+
+ private String appid;
+ private String sp_appid;
+ private String mchid;
+ private String sp_mchid;
+ private String sub_appid;
+ private String sub_mchid;
+ private String description;
+ private String out_trade_no;
+ private String time_expire;
+ private String attach;
+ private String notify_url;
+ private String goods_tag;
+ private SettleInfo settle_info;
+ private Amount amount;
+ private Payer payer;
+ private Detail detail;
+ private SceneInfo scene_info;
+
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class SettleInfo {
+ private Boolean profit_sharing;
+ private Integer subsidy_amount;
+ }
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class Amount {
+ private Integer total;
+ /**
+ * 合单支付时需要
+ */
+ private Integer total_amount;
+ @Builder.Default
+ private String currency = "CNY";
+ }
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class Payer {
+ private String openid;
+ private String sp_openid;
+ private String sub_openid;
+ }
+
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class Detail {
+ private int cost_price;
+ private String invoice_id;
+ private List<GoodsDetail> goods_detail;
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class GoodsDetail {
+ private String merchant_goods_id;
+ private String wechatpay_goods_id;
+ private String goods_name;
+ private int quantity;
+ private int unit_price;
+ }
+ }
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class SceneInfo {
+ private String payer_client_ip;
+ private String device_id;
+ private StoreInfo store_info;
+ private H5Info h5_info;
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class StoreInfo {
+ private String id;
+ private String name;
+ private String area_code;
+ private String address;
+ }
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class H5Info {
+ private String type;
+ private String app_name;
+ private String app_url;
+ private String bundle_id;
+ private String package_name;
+ }
+ }
+
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class SubOrders {
+
+ private String out_trade_no;
+
+
+ private Amount amount;
+
+ private String mchid;
+
+ private String sub_mchid;
+
+ private String attach;
+
+ private String description;
+ private String goods_tag;
+
+ private SettleInfo settle_info;
+ }
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class CombinePayerInfo {
+ private String openid;
+ }
+
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java
new file mode 100644
index 0000000..636acf5
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java
@@ -0,0 +1,84 @@
+package com.ruoyi.system.wxPay.model;
+
+import lombok.*;
+
+import java.util.List;
+
+/**
+ * @author xiaochen
+ * @ClassName WxPaymentRefundModel
+ * @Description
+ */
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+@ToString
+public class WxPaymentRefundModel {
+ /**
+ * 子商户,二级商户号
+ */
+ private String sub_mchid;
+ /**
+ * 电商平台APPID
+ */
+ private String sp_appid;
+ private String transaction_id;
+ private String out_trade_no;
+ /**
+ * 商户退款单号
+ */
+ private String out_refund_no;
+ /**
+ * 退款原因
+ */
+ private String reason;
+ private String notify_url;
+ /**
+ * 资金账户,否
+ */
+ private String funds_account;
+ /**
+ * 退款金额信息
+ */
+ private RefundAmount amount;
+ /**
+ * 退款商品
+ */
+ private List<RefundGoodsDetail> goods_detail;
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class RefundAmount {
+ /**
+ * 原订单金额
+ */
+ private int total;
+ @Builder.Default
+ private String currency = "CNY";
+ /**
+ * 退款金额
+ */
+ private int refund;
+ }
+
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Getter
+ @Setter
+ @ToString
+ public static class RefundGoodsDetail {
+ private String merchant_goods_id;
+ private String wechatpay_goods_id;
+ private String goods_name;
+ private int unit_price;
+ private int refund_amount;
+ private int refund_quantity;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java
new file mode 100644
index 0000000..ea6f700
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java
@@ -0,0 +1,52 @@
+package com.ruoyi.system.wxPay.pojo;
+
+import lombok.Data;
+
+/**
+ * @author xiaochen
+ * @ClassName AppletUserDecodeData
+ * @Description
+ * 用户主体信息部分
+ * {
+ * "openId": "OPENID",
+ * "nickName": "NICKNAME",
+ * "gender": GENDER,
+ * "city": "CITY",
+ * "province": "PROVINCE",
+ * "country": "COUNTRY",
+ * "avatarUrl": "AVATARURL",
+ * "unionId": "UNIONID",
+ * "watermark":
+ * {
+ * "appid":"APPID",
+ * "timestamp":TIMESTAMP
+ * }
+ * }
+ * 电话部分
+ * {
+ * "phoneNumber": "13580006666",
+ * "purePhoneNumber": "13580006666",
+ * "countryCode": "86",
+ * "watermark":
+ * {
+ * "appid":"APPID",
+ * "timestamp": TIMESTAMP
+ * }
+ * }
+ *
+ */
+@Data
+public class AppletUserDecodeData {
+ private String openId;
+ private String unionId;
+ private String nickName;
+ private int gender;
+ private String city;
+ private String province;
+ private String country;
+ private String avatarUrl;
+ private Watermark watermark;
+ private String phoneNumber;
+ private String purePhoneNumber;
+ private String countryCode;
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java
new file mode 100644
index 0000000..2f92e2b
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java
@@ -0,0 +1,9 @@
+package com.ruoyi.system.wxPay.pojo;
+
+import lombok.Data;
+
+@Data
+public class Watermark {
+ private String appid;
+ private String timestamp;
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java
new file mode 100644
index 0000000..b119786
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java
@@ -0,0 +1,222 @@
+package com.ruoyi.system.wxPay.resp;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author xiaochen
+ * @ClassName FacilV3PayNotifyRespBody
+ * @Description
+ */
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class NotifyV3PayDecodeRespBody implements Serializable {
+ // 合单--开始
+ private String combine_appid;
+ private String combine_mchid;
+ private String combine_out_trade_no;
+ private List<SubOrders> sub_orders;
+ // 合单--结束
+ /**
+ * 服务商应用ID
+ */
+ private String sp_appid;
+ /**
+ * 服务商户号
+ */
+ private String sp_mchid;
+ /**
+ * 商户号
+ */
+ private String mchid;
+ /**
+ * 子商户应用ID
+ */
+ private String sub_appid;
+ /**
+ * 子商户号
+ */
+ private String sub_mchid;
+ /**
+ * 商户订单号
+ */
+ private String out_trade_no;
+ /**
+ * 交易状态描述
+ */
+ private String trade_state_desc;
+ /**
+ * 交易类型,枚举值:
+ * JSAPI:公众号支付
+ * NATIVE:扫码支付
+ * APP:APP支付
+ * MICROPAY:付款码支付
+ * MWEB:H5支付
+ * FACEPAY:刷脸支付
+ */
+ private String trade_type;
+ /**
+ * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
+ */
+ private String attach;
+ /**
+ * 微信支付订单号
+ */
+ private String transaction_id;
+ /**
+ * 交易状态,枚举值:
+ * SUCCESS:支付成功
+ * REFUND:转入退款
+ * NOTPAY:未支付
+ * CLOSED:已关闭
+ * REVOKED:已撤销(付款码支付)
+ * USERPAYING:用户支付中(付款码支付)
+ * PAYERROR:支付失败(其他原因,如银行返回失败)
+ */
+ private String trade_state;
+ /**
+ * 银行类型,采用字符串类型的银行标识。银行标识请参考《银行类型对照表》
+ * https://pay.weixin.qq.com/wiki/doc/apiv3_partner/terms_definition/chapter1_1_3.shtml#part-6
+ */
+ private String bank_type;
+ /**
+ * 支付完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,
+ * YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,
+ * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。
+ * 例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
+ * 示例值:2018-06-08T10:34:56+08:00
+ */
+ private String success_time;
+ /**
+ * 支付者信息
+ */
+ private Payer payer;
+
+ /**
+ * 支付者
+ */
+ private Payer combine_payer_info;
+
+ /**
+ * 订单金额信息
+ */
+ private Amount amount;
+ /**
+ * 场景信息
+ */
+ private SceneInfo scene_info;
+ /**
+ * 优惠功能,享受优惠时返回该字段
+ */
+ private List<PromotionDetail> promotion_detail;
+
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Data
+ public static class Amount implements Serializable{
+ /**
+ * 用户支付金额
+ */
+ private int payer_total;
+ /**
+ * 总金额
+ */
+ private int total;
+ /**
+ * 标价金额
+ */
+ private int total_amount;
+ /**
+ * 现金支付金额
+ */
+ private int payer_amount;
+ private String currency;
+ private String payer_currency;
+ }
+
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Data
+ public static class GoodsDetail implements Serializable{
+ private String goods_id;
+ private int quantity;
+ private int unit_price;
+ private int discount_amount;
+ private String goods_remark;
+
+ }
+
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Data
+ public static class Payer implements Serializable{
+ private String openid;
+ private String sp_openid;
+ private String sub_openid;
+ }
+
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Data
+ public static class PromotionDetail implements Serializable{
+ private String coupon_id;
+ private String name;
+ private String scope;
+ private String type;
+ private int amount;
+ private String stock_id;
+ private int wechatpay_contribute;
+
+ private int merchant_contribute;
+ private int other_contribute;
+ private String currency;
+
+ private List<GoodsDetail> goods_detail;
+
+ }
+
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Data
+ public static class SceneInfo implements Serializable{
+ /**
+ * 商户端设备号
+ */
+ private String device_id;
+ }
+
+
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Data
+ public static class SubOrders implements Serializable{
+ private String mchid;
+ private String trade_type;
+ private String trade_state;
+ private String trade_state_desc;
+ private String bank_type;
+ private String attach;
+ private String success_time;
+ private String transaction_id;
+ private String out_trade_no;
+ private String sub_mchid;
+ private Amount amount;
+ /**
+ * 优惠功能,享受优惠时返回该字段
+ */
+ private List<PromotionDetail> promotion_detail;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java
new file mode 100644
index 0000000..ded7b52
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java
@@ -0,0 +1,118 @@
+package com.ruoyi.system.wxPay.utils;
+
+
+import com.google.gson.annotations.SerializedName;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+/**
+ * JSAPI下单
+ */
+public class JsapiPrepay {
+ private static String HOST = "https://api.mch.weixin.qq.com";
+ private static String METHOD = "POST";
+ private static String PATH = "/v3/pay/transactions/jsapi";
+ private final String mchId;
+ private final String certificateSerialNo;
+ private final PrivateKey privateKey;
+ private final String wechatPayPublicKeyId;
+ private final PublicKey wechatPayPublicKey;
+
+ public JsapiPrepay(String mchId, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
+ this.mchId = mchId;
+ this.certificateSerialNo = certificateSerialNo;
+ this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
+ this.wechatPayPublicKeyId = wechatPayPublicKeyId;
+ this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
+ }
+
+ public DirectAPIv3JsapiPrepayResponse run(DirectAPIv3JsapiPrepayRequest request) {
+ String uri = PATH;
+ String reqBody = WXPayUtility.toJson(request);
+
+ Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
+ reqBuilder.addHeader("Accept", "application/json");
+ reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+ reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchId, certificateSerialNo,privateKey, METHOD, uri, reqBody));
+ reqBuilder.addHeader("Content-Type", "application/json");
+ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
+ reqBuilder.method(METHOD, requestBody);
+ Request httpRequest = reqBuilder.build();
+
+ // 发送HTTP请求
+ OkHttpClient client = new OkHttpClient.Builder().build();
+ try (Response httpResponse = client.newCall(httpRequest).execute()) {
+ String respBody = WXPayUtility.extractBody(httpResponse);
+ if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+ // 2XX 成功,验证应答签名
+ WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
+ httpResponse.headers(), respBody);
+
+ // 从HTTP应答报文构建返回数据
+ return WXPayUtility.fromJson(respBody, DirectAPIv3JsapiPrepayResponse.class);
+ } else {
+ throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
+ }
+ }
+ public static class DirectAPIv3JsapiPrepayRequest {
+ @SerializedName("appid")
+ public String appid;
+
+ @SerializedName("mchid")
+ public String mchid;
+
+ @SerializedName("description")
+ public String description;
+
+ @SerializedName("out_trade_no")
+ public String outTradeNo;
+
+ @SerializedName("time_expire")
+ public String timeExpire;
+
+ @SerializedName("attach")
+ public String attach;
+
+ @SerializedName("notify_url")
+ public String notifyUrl;
+
+ @SerializedName("goods_tag")
+ public String goodsTag;
+
+ @SerializedName("support_fapiao")
+ public Boolean supportFapiao;
+
+ @SerializedName("amount")
+ public CommonAmountInfo amount;
+
+ @SerializedName("payer")
+ public JsapiReqPayerInfo payer;
+
+ }
+
+ public static class DirectAPIv3JsapiPrepayResponse {
+ @SerializedName("prepay_id")
+ public String prepayId;
+ }
+
+ public static class CommonAmountInfo {
+ @SerializedName("total")
+ public Long total;
+
+ @SerializedName("currency")
+ public String currency;
+ }
+
+ public static class JsapiReqPayerInfo {
+ @SerializedName("openid")
+ public String openid;
+ }
+
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java
new file mode 100644
index 0000000..834edae
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java
@@ -0,0 +1,441 @@
+package com.ruoyi.system.wxPay.utils;
+
+import com.google.gson.*;
+import com.google.gson.annotations.Expose;
+import com.wechat.pay.java.core.util.GsonUtil;
+import okhttp3.Headers;
+import okhttp3.Response;
+import okio.BufferedSource;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Objects;
+
+public class WXPayUtility {
+ private static final Gson gson = new GsonBuilder()
+ .disableHtmlEscaping()
+ .addSerializationExclusionStrategy(new ExclusionStrategy() {
+ @Override
+ public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+ final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+ return expose != null && !expose.serialize();
+ }
+
+ @Override
+ public boolean shouldSkipClass(Class<?> aClass) {
+ return false;
+ }
+ })
+ .addDeserializationExclusionStrategy(new ExclusionStrategy() {
+ @Override
+ public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+ final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+ return expose != null && !expose.deserialize();
+ }
+
+ @Override
+ public boolean shouldSkipClass(Class<?> aClass) {
+ return false;
+ }
+ })
+ .create();
+ private static final char[] SYMBOLS =
+ "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
+ private static final SecureRandom random = new SecureRandom();
+
+ /**
+ * 将 Object 转换为 JSON 字符串
+ */
+ public static String toJson(Object object) {
+ return gson.toJson(object);
+ }
+
+ /**
+ * 将 JSON 字符串解析为特定类型的实例
+ */
+ public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
+ return gson.fromJson(json, classOfT);
+ }
+
+ /**
+ * 从公私钥文件路径中读取文件内容
+ *
+ * @param keyPath 文件路径
+ * @return 文件内容
+ */
+ private static String readKeyStringFromPath(String keyPath) {
+ try {
+ return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
+ * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象
+ *
+ * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头
+ * @return PrivateKey 对象
+ */
+ public static PrivateKey loadPrivateKeyFromString(String keyString) {
+ try {
+ keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
+ .replace("-----END PRIVATE KEY-----", "")
+ .replaceAll("\\s+", "");
+ return KeyFactory.getInstance("RSA").generatePrivate(
+ new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+ } catch (NoSuchAlgorithmException e) {
+ throw new UnsupportedOperationException(e);
+ } catch (InvalidKeySpecException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * 从 PKCS#8 格式的私钥文件中加载私钥
+ *
+ * @param keyPath 私钥文件路径
+ * @return PrivateKey 对象
+ */
+ public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
+ return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
+ }
+
+ /**
+ * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象
+ *
+ * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头
+ * @return PublicKey 对象
+ */
+ public static PublicKey loadPublicKeyFromString(String keyString) {
+ try {
+ keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
+ .replace("-----END PUBLIC KEY-----", "")
+ .replaceAll("\\s+", "");
+ return KeyFactory.getInstance("RSA").generatePublic(
+ new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+ } catch (NoSuchAlgorithmException e) {
+ throw new UnsupportedOperationException(e);
+ } catch (InvalidKeySpecException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * 从 PKCS#8 格式的公钥文件中加载公钥
+ *
+ * @param keyPath 公钥文件路径
+ * @return PublicKey 对象
+ */
+ public static PublicKey loadPublicKeyFromPath(String keyPath) {
+ return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
+ }
+
+ /**
+ * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途
+ */
+ public static String createNonce(int length) {
+ char[] buf = new char[length];
+ for (int i = 0; i < length; ++i) {
+ buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
+ }
+ return new String(buf);
+ }
+
+ /**
+ * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密
+ *
+ * @param publicKey 加密用公钥对象
+ * @param plaintext 待加密明文
+ * @return 加密后密文
+ */
+ public static String encrypt(PublicKey publicKey, String plaintext) {
+ final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
+
+ try {
+ Cipher cipher = Cipher.getInstance(transformation);
+ cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+ return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
+ } catch (InvalidKeyException e) {
+ throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
+ } catch (BadPaddingException | IllegalBlockSizeException e) {
+ throw new IllegalArgumentException("Plaintext is too long", e);
+ }
+ }
+
+ /**
+ * 使用私钥按照指定算法进行签名
+ *
+ * @param message 待签名串
+ * @param algorithm 签名算法,如 SHA256withRSA
+ * @param privateKey 签名用私钥对象
+ * @return 签名结果
+ */
+ public static String sign(String message, String algorithm, PrivateKey privateKey) {
+ byte[] sign;
+ try {
+ Signature signature = Signature.getInstance(algorithm);
+ signature.initSign(privateKey);
+ signature.update(message.getBytes(StandardCharsets.UTF_8));
+ sign = signature.sign();
+ } catch (NoSuchAlgorithmException e) {
+ throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
+ } catch (InvalidKeyException e) {
+ throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
+ } catch (SignatureException e) {
+ throw new RuntimeException("An error occurred during the sign process.", e);
+ }
+ return Base64.getEncoder().encodeToString(sign);
+ }
+
+ /**
+ * 使用公钥按照特定算法验证签名
+ *
+ * @param message 待签名串
+ * @param signature 待验证的签名内容
+ * @param algorithm 签名算法,如:SHA256withRSA
+ * @param publicKey 验签用公钥对象
+ * @return 签名验证是否通过
+ */
+ public static boolean verify(String message, String signature, String algorithm,
+ PublicKey publicKey) {
+ try {
+ Signature sign = Signature.getInstance(algorithm);
+ sign.initVerify(publicKey);
+ sign.update(message.getBytes(StandardCharsets.UTF_8));
+ return sign.verify(Base64.getDecoder().decode(signature));
+ } catch (SignatureException e) {
+ return false;
+ } catch (InvalidKeyException e) {
+ throw new IllegalArgumentException("verify uses an illegal publickey.", e);
+ } catch (NoSuchAlgorithmException e) {
+ throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
+ }
+ }
+
+ /**
+ * 根据微信支付APIv3请求签名规则构造 Authorization 签名
+ *
+ * @param mchid 商户号
+ * @param certificateSerialNo 商户API证书序列号
+ * @param privateKey 商户API证书私钥
+ * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE
+ * @param uri 请求接口的URL
+ * @param body 请求接口的Body
+ * @return 构造好的微信支付APIv3 Authorization 头
+ */
+ public static String buildAuthorization(String mchid, String certificateSerialNo,
+ PrivateKey privateKey,
+ String method, String uri, String body) {
+ String nonce = createNonce(32);
+ long timestamp = Instant.now().getEpochSecond();
+
+ String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
+ body == null ? "" : body);
+
+ String signature = sign(message, "SHA256withRSA", privateKey);
+
+ return String.format(
+ "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
+ "timestamp=\"%d\",serial_no=\"%s\"",
+ mchid, nonce, signature, timestamp, certificateSerialNo);
+ }
+
+ /**
+ * 对参数进行 URL 编码
+ *
+ * @param content 参数内容
+ * @return 编码后的内容
+ */
+ public static String urlEncode(String content) {
+ try {
+ return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 对参数Map进行 URL 编码,生成 QueryString
+ *
+ * @param params Query参数Map
+ * @return QueryString
+ */
+ public static String urlEncode(Map<String, Object> params) {
+ if (params == null || params.isEmpty()) {
+ return "";
+ }
+
+ int index = 0;
+ StringBuilder result = new StringBuilder();
+ for (Map.Entry<String, Object> entry : params.entrySet()) {
+ result.append(entry.getKey())
+ .append("=")
+ .append(urlEncode(entry.getValue().toString()));
+ index++;
+ if (index < params.size()) {
+ result.append("&");
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * 从应答中提取 Body
+ *
+ * @param response HTTP 请求应答对象
+ * @return 应答中的Body内容,Body为空时返回空字符串
+ */
+ public static String extractBody(Response response) {
+ if (response.body() == null) {
+ return "";
+ }
+
+ try {
+ BufferedSource source = response.body().source();
+ return source.readUtf8();
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e);
+ }
+ }
+
+ /**
+ * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常
+ *
+ * @param wechatpayPublicKeyId 微信支付公钥ID
+ * @param wechatpayPublicKey 微信支付公钥对象
+ * @param headers 微信支付应答 Header 列表
+ * @param body 微信支付应答 Body
+ */
+ public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
+ Headers headers,
+ String body) {
+ String timestamp = headers.get("Wechatpay-Timestamp");
+ try {
+ Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
+ // 拒绝过期请求
+ if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
+ throw new IllegalArgumentException(
+ String.format("Validate http response,timestamp[%s] of httpResponse is expires, "
+ + "request-id[%s]",
+ timestamp, headers.get("Request-ID")));
+ }
+ } catch (DateTimeException | NumberFormatException e) {
+ throw new IllegalArgumentException(
+ String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " +
+ "request-id[%s]", timestamp,
+ headers.get("Request-ID")));
+ }
+ String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
+ body == null ? "" : body);
+ String serialNumber = headers.get("Wechatpay-Serial");
+ if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
+ throw new IllegalArgumentException(
+ String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId,
+ serialNumber));
+ }
+ String signature = headers.get("Wechatpay-Signature");
+
+ boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
+ if (!success) {
+ throw new IllegalArgumentException(
+ String.format("Validate response failed,the WechatPay signature is incorrect.%n"
+ + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
+ headers.get("Request-ID"), headers, body));
+ }
+ }
+
+ /**
+ * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
+ */
+ public static class ApiException extends RuntimeException {
+ private static final long serialVersionUID = 2261086748874802175L;
+
+ private final int statusCode;
+ private final String body;
+ private final Headers headers;
+ private final String errorCode;
+ private final String errorMessage;
+
+ public ApiException(int statusCode, String body, Headers headers) {
+ super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, body, headers));
+ this.statusCode = statusCode;
+ this.body = body;
+ this.headers = headers;
+
+ if (body != null && !body.isEmpty()) {
+ JsonElement code;
+ JsonElement message;
+
+ try {
+ JsonObject jsonObject = GsonUtil.getGson().fromJson(body, JsonObject.class);
+ code = jsonObject.get("code");
+ message = jsonObject.get("message");
+ } catch (JsonSyntaxException ignored) {
+ code = null;
+ message = null;
+ }
+ this.errorCode = code == null ? null : code.getAsString();
+ this.errorMessage = message == null ? null : message.getAsString();
+ } else {
+ this.errorCode = null;
+ this.errorMessage = null;
+ }
+ }
+
+ /**
+ * 获取 HTTP 应答状态码
+ */
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ /**
+ * 获取 HTTP 应答包体内容
+ */
+ public String getBody() {
+ return body;
+ }
+
+ /**
+ * 获取 HTTP 应答 Header
+ */
+ public Headers getHeaders() {
+ return headers;
+ }
+
+ /**
+ * 获取 错误码 (错误应答中的 code 字段)
+ */
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ /**
+ * 获取 错误消息 (错误应答中的 message 字段)
+ */
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java
new file mode 100644
index 0000000..a888f1b
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java
@@ -0,0 +1,400 @@
+package com.ruoyi.system.wxPay.utils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.ruoyi.system.utils.wx.tools.WebUtils;
+import com.ruoyi.system.utils.wx.tools.WxJsonUtils;
+import com.ruoyi.system.utils.wx.tools.WxUtils;
+import com.ruoyi.system.wxPay.model.WxCloseOrderModel;
+import com.ruoyi.system.wxPay.model.WxPaymentInfoModel;
+import com.ruoyi.system.wxPay.model.WxPaymentRefundModel;
+import com.ruoyi.system.wxPay.resp.NotifyV3PayDecodeRespBody;
+import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
+import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
+import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
+import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
+import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.*;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
+
+/**
+ * @author xiaochen
+ * @ClassName WxWifiV3Pay
+ * @Description
+ * @date 2021-11-13 21:10
+ */
+@Slf4j
+public abstract class WxAbstractPay {
+ /**
+ * 请求成功相应码
+ */
+ private static final int STATUS_CODE = 200;
+ /**
+ * 请求成功相应码
+ */
+ private static final int OTHER_STATUS_CODE = 204;
+ /**
+ * 请求根地址
+ */
+ private static final String HOST = "https://api.mch.weixin.qq.com";
+
+ private static RuntimeException parameterError(String message, Object... args) {
+ message = String.format(message, args);
+ return new IllegalArgumentException("parameter error: " + message);
+ }
+
+ /**
+ * 封装基础数据
+ *
+ * @param requestBody
+ * @param notifyUrl
+ * @return
+ */
+ protected String buildBaseParam(WxPaymentInfoModel requestBody, String notifyUrl) {
+ // 封装基础数据
+ requestBody.setNotify_url(notifyUrl + requestBody.getNotify_url());
+ String reqBody = WxJsonUtils.toJsonString(requestBody);
+ return reqBody;
+ }
+
+ /**
+ * 微信调起支付参数
+ * 返回参数如有不理解 请访问微信官方文档
+ * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
+ *
+ * @param prepayId 微信下单返回的prepay_id
+ * @param appId 应用ID(appid)
+ * @return 当前调起支付所需的参数
+ * @throws Exception
+ */
+ protected Map<String, Object> wxTuneUp(PrivateKeySigner privateKeySigner, String appId, String prepayId) {
+ String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
+ String nonceStr = WxUtils.generateNonceStr();
+ String packageStr = "prepay_id=" + prepayId;
+ //加载签名
+ String signStr = Stream.of(appId, timeStamp, nonceStr, packageStr).collect(Collectors.joining("\n", "", "\n"));
+ String packageSign = privateKeySigner.sign(signStr.getBytes(StandardCharsets.UTF_8)).getSign();
+ Map<String, Object> map = new HashMap<>(6);
+ map.put("appId", appId);
+ map.put("timeStamp", timeStamp);
+ map.put("nonceStr", nonceStr);
+ map.put("package", packageStr);
+ map.put("signType", "RSA");
+ map.put("paySign", packageSign);
+ return map;
+ }
+
+ /**
+ * 构建方法请求
+ *
+ * @param uri
+ * @param socketTimeout
+ * @param connectTimeout
+ * @return
+ */
+ protected HttpGet requestGet(String uri, int socketTimeout, int connectTimeout) {
+ //请求URL
+ HttpGet httpGet = new HttpGet(HOST + uri);
+ RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
+ .setConnectTimeout(connectTimeout).build();
+ httpGet.setConfig(requestConfig);
+ httpGet.setHeader("Content-type", "application/json");
+ httpGet.setHeader("Accept", "application/json");
+ return httpGet;
+ }
+
+ /**
+ * 构建方法请求
+ *
+ * @param uri
+ * @param socketTimeout
+ * @param connectTimeout
+ * @param reqdata
+ * @return
+ */
+ protected HttpPost requestPost(String uri, int socketTimeout, int connectTimeout, String reqdata) {
+ //请求URL
+ HttpPost httpPost = new HttpPost(HOST + uri);
+ RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
+ .setConnectTimeout(connectTimeout).build();
+ httpPost.setConfig(requestConfig);
+ StringEntity entity = new StringEntity(reqdata, StandardCharsets.UTF_8);
+ entity.setContentType("application/json");
+ httpPost.setEntity(entity);
+ httpPost.setHeader("Accept", "application/json");
+ return httpPost;
+ }
+
+ public abstract <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception;
+
+ /**
+ * 接收回调
+ *
+ * @param request
+ * @return
+ * @throws Exception
+ */
+ public <T> T verifyNotify(HttpServletRequest request, Verifier verifier, String apiKey, TypeReference<T> valueTypeRef) throws Exception {
+ String body = WxUtils.streamBodyByReceive(request);
+ String requestId = request.getHeader(REQUEST_ID);
+ String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
+ String value;
+ // 验签必须参数检验
+ for (String headerName : headers) {
+ value = request.getHeader(headerName);
+ if (value == null || "".equals(value)) {
+ throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
+ }
+ }
+ String serial = request.getHeader(WECHAT_PAY_SERIAL);
+ String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
+ String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
+ String nonce = request.getHeader(WECHAT_PAY_NONCE);
+ // 构建request,传入必要参数
+ NotificationRequest notificationRequest = new NotificationRequest.Builder().withSerialNumber(serial)
+ .withNonce(nonce)
+ .withTimestamp(timestamp)
+ .withSignature(signature)
+ .withBody(body)
+ .build();
+ NotificationHandler handler = new NotificationHandler(verifier, apiKey.getBytes(StandardCharsets.UTF_8));
+ // 验签和解析请求体
+ Notification notification = handler.parse(notificationRequest);
+ assert notification != null;
+ T respBody = WxJsonUtils.parseObject(notification.getDecryptData(), valueTypeRef);
+ return respBody;
+ }
+
+ /**
+ * 订单查询
+ *
+ * @param httpClient
+ * @param socketTimeout
+ * @param connectTimeout
+ * @param url
+ * @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody
+ * @author xiaochen
+ * @date 2021-12-20 17:12
+ */
+ protected NotifyV3PayDecodeRespBody query(CloseableHttpClient httpClient, int socketTimeout, int connectTimeout, String url) {
+ //请求URL
+ HttpGet httpGet = requestGet(
+ url
+ , socketTimeout
+ , connectTimeout);
+ CloseableHttpResponse response = null;
+ try {
+ response = httpClient.execute(httpGet);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+ if (WxUtils.getLogger().isDebugEnabled()) {
+ WxUtils.debug("请求成功:{}", respBodyStr);
+ }
+ if(404 == statusCode){
+ return null;
+ }
+ // 成功相应
+ if (STATUS_CODE == statusCode || OTHER_STATUS_CODE == statusCode ) {
+ NotifyV3PayDecodeRespBody body = WxJsonUtils.parseObject(respBodyStr, NotifyV3PayDecodeRespBody.class);
+ return body;
+ } else {
+ WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr);
+ throw new RuntimeException(respBodyStr);
+ }
+ } catch (ConnectTimeoutException e) {
+ e.printStackTrace();
+ throw new RuntimeException("接口超时");
+ } catch (SocketTimeoutException e) {
+ e.printStackTrace();
+ throw new RuntimeException("读取接口数据超时");
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new RuntimeException("接口请求失败,请尝试检查网络环境或请求接口是否能正常访问");
+ } finally {
+ // 关闭响应
+ try {
+ if (response != null) {
+ //关闭结果集
+ response.getEntity().getContent().close();
+ response.close();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("关闭流异常");
+ }
+ }
+ }
+
+ /**
+ * 子级实现
+ *
+ * @param out_trade_no
+ * @return
+ */
+ public abstract NotifyV3PayDecodeRespBody query(String out_trade_no);
+
+
+ /**
+ * 订单退款
+ *
+ * @param refundModel
+ * @return
+ */
+ public abstract Map<String, Object> refund(WxPaymentRefundModel refundModel);
+
+
+
+ public abstract String close(String out_trade_no);
+
+ /**
+ * 订单退款
+ *
+ * @param httpClient
+ * @param uri
+ * @param httpReadTimeoutMs
+ * @param httpConnectTimeoutMs
+ * @param refundModel
+ * @return
+ */
+ public Map<String, Object> refund(CloseableHttpClient httpClient,
+ String uri,
+ int httpReadTimeoutMs,
+ int httpConnectTimeoutMs,
+ WxPaymentRefundModel refundModel) {
+ String reqBody = WxJsonUtils.toJsonString(refundModel);
+ //请求URL
+ HttpEntityEnclosingRequestBase httpPost = requestPost(
+ uri
+ , httpReadTimeoutMs
+ , httpConnectTimeoutMs, reqBody);
+ String repBody = result(httpClient, httpPost);
+ Map<String, Object> body = WxJsonUtils.parseObject(repBody, Map.class);
+ return body;
+ }
+
+ /**
+ * 请求结果
+ *
+ * @param request
+ * @return
+ */
+ protected String result(CloseableHttpClient httpClient, HttpRequestBase request) {
+ CloseableHttpResponse response = null;
+ try {
+ response = httpClient.execute(request);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+ if (WxUtils.getLogger().isDebugEnabled()) {
+ WxUtils.debug("请求成功:{}", respBodyStr);
+ }
+ // 成功相应
+ if (STATUS_CODE == statusCode || OTHER_STATUS_CODE == statusCode) {
+ return respBodyStr;
+ } else {
+ WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr);
+ throw new RuntimeException(respBodyStr);
+ }
+ } catch (ConnectTimeoutException e) {
+ e.printStackTrace();
+ throw new RuntimeException("接口超时");
+ } catch (SocketTimeoutException e) {
+ e.printStackTrace();
+ throw new RuntimeException("读取接口数据超时");
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new RuntimeException("接口请求失败,请尝试检查网络环境或请求接口是否能正常访问");
+ } finally {
+ // 关闭响应
+ try {
+ if (response != null) {
+ //关闭结果集
+ response.getEntity().getContent().close();
+ response.close();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("关闭流异常");
+ }
+ }
+ }
+
+ /**
+ * 微信结果确认应答
+ * 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
+ *
+ * @param
+ * @throws IOException
+ */
+ public void ack() throws IOException {
+ ack(true, null);
+ }
+
+ /**
+ * 微信结果确认应答
+ * 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
+ *
+ * @param
+ * @throws IOException
+ */
+ public void ack(boolean ackSucc, String erroMsg) throws IOException {
+ HttpServletResponse response = WebUtils.response();
+ PrintWriter writer = response.getWriter();
+ if (ackSucc) {
+ log.info("响应微信回调成功!");
+ response.setStatus(200);
+ writer.write("{\"code\": \"SUCCESS\",\"message\": \"成功\"}");
+
+ } else {
+ log.info("响应微信回调失败:{}!", erroMsg);
+ response.setStatus(500);
+ writer.write("{\"code\": \"FAIL\",\"message\": " + (StringUtils.hasLength(erroMsg) ? erroMsg : "业务处理失败") + "}");
+ }
+ // 关闭流
+ if (Objects.nonNull(writer)) {
+ writer.close();
+ }
+ }
+
+
+ /**
+ * 关闭订单
+ * @param httpClient
+ * @param uri
+ * @param httpReadTimeoutMs
+ * @param httpConnectTimeoutMs
+ * @param closeModel
+ * @return
+ */
+ public String close(CloseableHttpClient httpClient,
+ String uri,
+ int httpReadTimeoutMs,
+ int httpConnectTimeoutMs,
+ WxCloseOrderModel closeModel) {
+ String reqBody = WxJsonUtils.toJsonString(closeModel);
+ //请求URL
+ HttpEntityEnclosingRequestBase httpPost = requestPost(
+ uri
+ , httpReadTimeoutMs
+ , httpConnectTimeoutMs, reqBody);
+ String repBody = result(httpClient, httpPost);
+ return repBody;
+ }
+
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java
new file mode 100644
index 0000000..160e952
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java
@@ -0,0 +1,164 @@
+package com.ruoyi.system.wxPay.utils;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.springframework.util.StringUtils;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.Objects;
+import java.util.TimeZone;
+
+/**
+ * @author xiaochen
+ * @ClassName WxTimeUtils
+ * @Description
+ * @date 2021-12-16 16:07
+ */
+public class WxTimeUtils {
+ /**
+ * 系统默认时区
+ */
+ private static final ZoneId ZONE = ZoneId.systemDefault();
+ /**
+ * yyyy-MM-dd'T'HH:mm:ssxxx 比如:2020-05-23T17:06:30+08:00 0时区时末尾 为+00:00
+ */
+ public static final DateTimeFormatter YYYY_MM_DD_T_HH_MM_SS_XXX_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx");
+ /**
+ * yyyy-MM-dd HH:mm:ss 比如:2020-05-23 17:06:30
+ */
+ public static final DateTimeFormatter YYYY_MM_DD_HH_MM_SS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZONE);
+
+ /**
+ * 时间转 TimeZone
+ *
+ * @param date
+ * @return
+ * @throws Exception
+ */
+ public static String dateToTimeZone(Date date) throws Exception {
+ String time;
+ if (date == null) {
+ throw new Exception("date is not null");
+ }
+ ZonedDateTime zonedDateTime = toZonedDateTime(date);
+ time = format(zonedDateTime, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
+ return time;
+ }
+
+ /**
+ * Date转ZonedDateTime,时区为系统默认时区
+ *
+ * @param date Date
+ * @return ZonedDateTime
+ */
+ public static ZonedDateTime toZonedDateTime(Date date) {
+ Objects.requireNonNull(date, "date");
+ return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault());
+ }
+
+ /**
+ * 根据 formatter格式化 zonedDateTime
+ *
+ * @param zonedDateTime ZonedDateTime
+ * @param formatter DateTimeFormatter
+ * @return String
+ */
+ public static String format(ZonedDateTime zonedDateTime, DateTimeFormatter formatter) {
+ Objects.requireNonNull(zonedDateTime, "zonedDateTime");
+ Objects.requireNonNull(formatter, "formatter");
+ return zonedDateTime.format(formatter);
+ }
+
+
+ /**
+ * TimeZone 时间转标准时间
+ *
+ * @param date
+ * @return
+ * @throws Exception
+ */
+ public static String toTimeZoneStr(String date) {
+ String time;
+ if (!StringUtils.hasLength(date)) {
+ throw new RuntimeException("str is not null");
+ }
+ ZonedDateTime zonedDateTime = parseToZonedDateTime(date, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
+ if (zonedDateTime == null) {
+ throw new RuntimeException("str to zonedDateTime fail");
+ }
+ time = zonedDateTime.format(YYYY_MM_DD_HH_MM_SS_FMT);
+ return time;
+ }
+
+ /**
+ * 转date
+ *
+ * @param date
+ * @return
+ * @throws Exception
+ */
+ public static Date toDate(String date) {
+ String time;
+ if (!StringUtils.hasLength(date)) {
+ throw new RuntimeException("str is not null");
+ }
+ ZonedDateTime zonedDateTime = parseToZonedDateTime(date, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
+ if (zonedDateTime == null) {
+ throw new RuntimeException("str to zonedDateTime fail");
+ }
+ return Date.from(zonedDateTime.toInstant());
+ }
+
+ /**
+ * str --> Date
+ *
+ * @param date
+ * @return java.util.Date
+ * @author xiaochen
+ * @date 2022-01-20 18:20
+ */
+ public static Date toRfc3339Date(String date) {
+ DateTime dt2 = new DateTime(date);
+ return dt2.toDate();
+ }
+
+ /**
+ * 将 Date 转为 LocalDateTime
+ *
+ * @param date
+ * @return java.time.LocalDateTime;
+ */
+ public static LocalDateTime dateToLocalDateTime(Date date) {
+ return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+ }
+
+
+ /**
+ * str --> Date
+ *
+ * @param date
+ * @return java.util.Date
+ * @author xiaochen
+ * @date 2022-01-20 18:20
+ */
+ public static String toRfc3339Str(Date date) {
+ DateTime dt1 = new DateTime(new Date(), DateTimeZone.forTimeZone(TimeZone.getTimeZone("Asia/Shanghai")));
+ return dt1.toString();
+ }
+
+ /**
+ * 根据 formatter解析为 ZonedDateTime
+ *
+ * @param text 待解析字符串
+ * @param formatter DateTimeFormatter
+ * @return ZonedDateTime
+ */
+ public static ZonedDateTime parseToZonedDateTime(String text, DateTimeFormatter formatter) {
+ return ZonedDateTime.parse(text, formatter);
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java
new file mode 100644
index 0000000..2ba770d
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java
@@ -0,0 +1,235 @@
+package com.ruoyi.system.wxPay.utils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.ruoyi.system.utils.wx.tools.WxUtils;
+import com.ruoyi.system.wxPay.model.WeixinPayProperties;
+import com.ruoyi.system.wxPay.model.WxCloseOrderModel;
+import com.ruoyi.system.wxPay.model.WxPaymentRefundModel;
+import com.ruoyi.system.wxPay.resp.NotifyV3PayDecodeRespBody;
+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.Verifier;
+import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
+import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import javax.servlet.http.HttpServletRequest;
+import java.security.PrivateKey;
+import java.util.Map;
+
+/**
+ * @author xiaochen
+ * @ClassName WxWifiV3Pay
+ * @Description
+ * @date 2021-11-13 21:10
+ */
+@Slf4j
+public class WxV3Pay extends WxAbstractPay {
+ @Getter
+ private WeixinPayProperties config;
+ @Getter
+ private Verifier verifier;
+ private WechatPayHttpClientBuilder builder;
+ @Getter
+ private CloseableHttpClient httpClient;
+ private PrivateKeySigner privateKeySigner;
+ private WechatPay2Validator validator;
+ @Getter
+ private PrivateKey privateKey;
+
+ /**
+ * 初始化
+ *
+ * @param config
+ */
+ public WxV3Pay(WeixinPayProperties config) {
+ // 检查v3支付配置信息
+ if (WxUtils.getLogger().isDebugEnabled()) {
+ WxUtils.debug("开始检查v3支付配置信息....");
+ }
+ try {
+ this.config = config;
+ checkWxConfig();
+ this.privateKey = PemUtil.loadPrivateKey(config.getV3().getPrivateKeyStream());
+ this.privateKeySigner = new PrivateKeySigner(config.getV3().getMchSerialNo(), privateKey);
+ this.validator = new WechatPay2Validator(verifier);
+ this.builder = WechatPayHttpClientBuilder.create()
+ .withMerchant(config.getMchId(), config.getV3().getMchSerialNo(), this.privateKey)
+ .withValidator(this.validator);
+ this.httpClient = this.builder.build();
+ } catch (Exception e) {
+ // 打印异常信息
+ e.printStackTrace();
+ WxUtils.warn("检查v3支付配置信息出现错误,直连商户商户号:{},{}", config.getMchId(), e.getMessage());
+ return;
+ }
+ if (WxUtils.getLogger().isDebugEnabled()) {
+ WxUtils.debug("检查v3支付配置信息完成,未出现异常....");
+ }
+ }
+
+ /**
+ * 检查支付配置信息
+ *
+ * @throws Exception
+ */
+ private void checkWxConfig() throws Exception {
+ if (this.config == null) {
+ throw new Exception("config is null");
+ }
+ if (config.getMchId() == null || config.getMchId().trim().length() == 0) {
+ throw new Exception("MchID in config is empty");
+ }
+ if (config.getV3().getMchSerialNo() == null) {
+ throw new Exception("mchSerialNo in config is empty");
+ }
+ if (config.getV3().getPrivateKeyStream() == null) {
+ throw new Exception("cert stream in config is empty");
+ }
+ if (this.config.getHttpConnectTimeoutMs() < 10) {
+ throw new Exception("http connect timeout is too small");
+ }
+ if (this.config.getHttpReadTimeoutMs() < 10) {
+ throw new Exception("http read timeout is too small");
+ }
+ }
+
+ /**
+ * jsApi下单
+ *
+ * @param tradeNo 订单号
+ * @param amount 金额 分
+ * @param openid openid
+ * @param description 订单描述
+ * @return java.util.Map<java.lang.String, java.lang.Object>
+ * @author xiaochen
+ * @date 2022-03-22 12:47
+ */
+ public Map<String, Object> jsApi(String tradeNo, Long amount, String openid, String description,String attach) {
+ JsapiPrepay client = new JsapiPrepay(
+ this.config.getMchId(), // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
+ this.config.getV3().getMchSerialNo(), // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
+ this.config.getV3().getPrivateKeyPath(), // 商户API证书私钥文件路径,本地文件路径
+ this.config.getV3().getPublicKeyId(), // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
+ this.config.getV3().getPublicKeyPath() // 微信支付公钥文件路径,本地文件路径
+ );
+ JsapiPrepay.DirectAPIv3JsapiPrepayRequest request = new JsapiPrepay.DirectAPIv3JsapiPrepayRequest();
+ request.appid = this.config.getAppId();
+ request.mchid = this.config.getMchId();
+ request.description = description;
+ request.outTradeNo = tradeNo;
+// request.timeExpire = "2018-06-08T10:34:56+08:00";
+ request.attach = attach;
+ request.notifyUrl = this.config.getCallBackUrl();
+ request.goodsTag = "WXG";
+ request.supportFapiao = false;
+ request.amount = new JsapiPrepay.CommonAmountInfo();
+ request.amount.total = amount;
+ request.amount.currency = "CNY";
+ request.payer = new JsapiPrepay.JsapiReqPayerInfo();
+ request.payer.openid = openid;
+ try {
+ JsapiPrepay.DirectAPIv3JsapiPrepayResponse response = client.run(request);
+ return wxTuneUp(this.privateKeySigner, this.config.getAppId(), response.prepayId);
+ } catch (WXPayUtility.ApiException e) {
+ throw new RuntimeException("获取支付数据错误!");
+ }
+ }
+// /**
+// * jsApi下单
+// *
+// * @param tradeNo 订单号
+// * @param amount 金额 分
+// * @param openid openid
+// * @param description 订单描述
+// * @return java.util.Map<java.lang.String, java.lang.Object>
+// * @author xiaochen
+// * @date 2022-03-22 12:47
+// */
+// public Map<String, Object> jsApi(String tradeNo, Integer amount, String openid, String description) {
+// WxPaymentInfoModel requestBody = WxPaymentInfoModel.builder()
+// .mchid(this.config.getMchId())
+// .appid(this.config.getAppId())
+// .description(description)
+// .out_trade_no(tradeNo)
+//// .attach("")
+// .amount(WxPaymentInfoModel.Amount.builder().total(amount).build())
+// .payer(WxPaymentInfoModel.Payer.builder().openid(openid).build())
+// .build();
+// // 封装基础数据
+// String reqBody = buildBaseParam(requestBody
+// , this.config.getV3().getNotifyPayUrl());
+// //请求URL
+// HttpEntityEnclosingRequestBase httpPost = requestPost(
+// "/v3/pay/transactions/jsapi"
+// , this.config.getHttpReadTimeoutMs()
+// , this.config.getHttpConnectTimeoutMs()
+// , reqBody);
+// String repBody = result(httpClient, httpPost);
+// ObjectMapper om = new ObjectMapper();
+// try {
+// JsonNode rootNode = om.readTree(repBody);
+// String prepayId = rootNode.path("prepay_id").asText();
+// return wxTuneUp(this.privateKeySigner, requestBody.getAppid(), prepayId);
+// } catch (JsonProcessingException e) {
+// throw new RuntimeException("获取支付数据错误!");
+// }
+// }
+
+
+ /**
+ * 接收回调
+ *
+ * @param request
+ * @return
+ * @throws Exception
+ */
+ @Override
+ public <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception {
+ return verifyNotify(request, this.verifier, this.config.getV3().getApiKey(), valueTypeRef);
+ }
+
+ /**
+ * 订单查询
+ *
+ * @param out_trade_no
+ * @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody
+ * @author xiaochen
+ * @date 2021-12-20 16:47
+ */
+ @Override
+ public NotifyV3PayDecodeRespBody query(String out_trade_no) {
+ String url =
+ String.format("/v3/pay/transactions/out-trade-no/%s", out_trade_no) + String.format("?mchid=%s", this.getConfig().getMchId());
+ return query(this.httpClient, this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), url);
+ }
+
+ /**
+ * 退款
+ *
+ * @param refundModel
+ * @return java.util.Map<java.lang.String, java.lang.Object>
+ * @author xiaochen
+ */
+ @Override
+ public Map<String, Object> refund(WxPaymentRefundModel refundModel) {
+ refundModel.setNotify_url(this.config.getV3().getNotifyRefundUrl() + refundModel.getNotify_url());
+ return refund(this.httpClient, "/v3/refund/domestic/refunds", this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), refundModel);
+ }
+
+
+ /**
+ * 关闭订单
+ * @param out_trade_no
+ * @return
+ */
+ @Override
+ public String close(String out_trade_no) {
+ String uri = String.format("/v3/pay/transactions/out-trade-no/%s/close", out_trade_no);
+ WxCloseOrderModel wxCloseOrderModel = new WxCloseOrderModel();
+ wxCloseOrderModel.setMchid(this.config.getMchId());
+ return close(this.httpClient, uri, this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), wxCloseOrderModel);
+ }
+}
--
Gitblit v1.7.1