From 1b43670626f48266efb898ec7cd7deedcab9ba10 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期六, 17 八月 2024 15:31:23 +0800
Subject: [PATCH] Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/java/mx_charging_pile
---
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java | 58
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletPhoneEncrypteData.java | 19
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java | 10
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WebUtils.java | 48
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/JsonUtils.java | 110 +
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserDecodeData.java | 52
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/SHA1.java | 36
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/Watermark.java | 9
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WeixinProperties.java | 101 +
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxJsonUtils.java | 73 +
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCache.java | 117 +
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxAbstractPay.java | 333 +++++
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxTimeUtils.java | 164 ++
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserEncrypteData.java | 17
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/V3.java | 71 +
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentRefundModel.java | 84 +
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxJsonUtils.java | 109 +
ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java | 1
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxUtils.java | 175 ++
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/Constant/AliConstant.java | 10
ruoyi-auth/src/main/java/com/ruoyi/auth/controller/TokenController.java | 25
ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/handler/GlobalExceptionHandler.java | 10
ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/TAppUserLoginInfo.java | 79 +
ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/service/TokenService.java | 16
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/tools/AliAppletTools.java | 37
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/RespBody.java | 19
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/Watermark.java | 9
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java | 23
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/controller/WxPayController.java | 142 ++
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCacheTemplate.java | 34
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxAppletTools.java | 125 +
ruoyi-service/ruoyi-account/pom.xml | 6
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxException.java | 55
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/RefundEnum.java | 53
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/config/WxConfig.java | 33
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/model/AliProperties.java | 70 +
ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/exception/user/UserAppletException.java | 48
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/Code2SessionRespBody.java | 29
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppCouponController.java | 5
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java | 20
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCaffineCache.java | 122 +
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/AppletUserDecodeData.java | 52
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java | 7
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/WxLoginController.java | 67 +
ruoyi-service/ruoyi-payment/pom.xml | 13
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxV3Pay.java | 197 ++
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/TradeStateEnum.java | 41
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxUtils.java | 217 +++
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/resp/NotifyV3PayDecodeRespBody.java | 222 +++
ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java | 15
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resq/Code2SessionResqBody.java | 21
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java | 91 +
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/SHA1.java | 39
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/model/WeixinProperties.java | 80 +
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/AccessTokenRespBody.java | 28
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/exception/WxException.java | 55
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentInfoModel.java | 201 +++
57 files changed, 3,874 insertions(+), 29 deletions(-)
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java
new file mode 100644
index 0000000..39fae79
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java
@@ -0,0 +1,23 @@
+package com.ruoyi.chargingPile.api.vo;
+
+import com.ruoyi.chargingPile.api.model.Site;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel(value = "SiteVO对象", description = "小程序扫一扫-详情页面")
+public class SiteInfoVO {
+ @ApiModelProperty(value = "电站名称")
+ private String name;
+ @ApiModelProperty(value = "桩编号")
+ private String number;
+ @ApiModelProperty(value = "普通电价")
+ private BigDecimal electrovalence;
+ @ApiModelProperty(value = "会员电价")
+ private BigDecimal vipElectrovalence;
+ @ApiModelProperty(value = "超时占位费说明")
+ private String spaceChargeExplain;
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java
index 2f01d93..a7161a4 100644
--- a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java
@@ -25,10 +25,10 @@
/**
* 用户名id
*/
- private Integer userid;
+ private Long userId;
/**
- * 用户名
+ * 用户手机号
*/
private String phone;
/**
@@ -45,7 +45,12 @@
* 登录IP地址
*/
private String ipaddr;
-
-
-
+ /**
+ * 头像
+ */
+ private String avatar;
+ /**
+ * 地址
+ */
+ private String address;
}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/TAppUserLoginInfo.java b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/TAppUserLoginInfo.java
new file mode 100644
index 0000000..dad1f6e
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/TAppUserLoginInfo.java
@@ -0,0 +1,79 @@
+package com.ruoyi.system.api.model;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.ruoyi.common.core.web.domain.BasePojo;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ *
+ * </p>
+ *
+ * @author luodangjia
+ * @since 2024-08-06
+ */
+@Data
+public class TAppUserLoginInfo extends BasePojo {
+
+ private static final long serialVersionUID = 1L;
+ private Long id;
+
+ @ApiModelProperty(value = "用户")
+ private String name;
+
+ @ApiModelProperty(value = "手机号")
+ private String phone;
+
+ @ApiModelProperty(value = "头像")
+ private String avatar;
+
+ @ApiModelProperty(value = "会员id")
+ private Integer vipId;
+
+ @ApiModelProperty(value = "会员到期时间")
+ private LocalDateTime vipEndTime;
+
+ @ApiModelProperty(value = "单位id")
+ private Integer companyId;
+
+ @ApiModelProperty(value = "身份证号")
+ private String idCard;
+
+ @ApiModelProperty(value = "认证状态(0=否,1=是)")
+ private Integer authStatus;
+
+ @ApiModelProperty(value = "微信openid")
+ private String wxOpenid;
+
+ @ApiModelProperty(value = "支付宝openid")
+ private String aliOpenid;
+
+ @ApiModelProperty(value = "积分")
+ private Integer points;
+
+ @ApiModelProperty(value = "省名称")
+ private String province;
+
+ @ApiModelProperty(value = "省区划代码")
+ private String provinceCode;
+
+ @ApiModelProperty(value = "市名称")
+ private String city;
+
+ @ApiModelProperty(value = "市区划代码")
+ private String cityCode;
+
+ @ApiModelProperty(value = "状态(1=正常,2=冻结,3=注销)")
+ private Integer status;
+
+ @ApiModelProperty(value = "最后一次登录时间")
+ private LocalDateTime lastLoginTime;
+
+}
diff --git a/ruoyi-auth/src/main/java/com/ruoyi/auth/controller/TokenController.java b/ruoyi-auth/src/main/java/com/ruoyi/auth/controller/TokenController.java
index 41c1531..8a06c5d 100644
--- a/ruoyi-auth/src/main/java/com/ruoyi/auth/controller/TokenController.java
+++ b/ruoyi-auth/src/main/java/com/ruoyi/auth/controller/TokenController.java
@@ -3,15 +3,16 @@
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.system.api.domain.SysRole;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.feignClient.SysUserClient;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
-import org.springframework.web.bind.annotation.DeleteMapping;
-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.bind.annotation.*;
import com.ruoyi.auth.form.LoginBody;
import com.ruoyi.auth.form.RegisterBody;
import com.ruoyi.auth.service.SysLoginService;
@@ -30,6 +31,7 @@
*
* @author ruoyi
*/
+@Slf4j
@RestController
public class TokenController
{
@@ -67,10 +69,6 @@
userClient.updateSysUser(sysUser);
return R.ok(map);
}
-
-
-
-
@DeleteMapping("logout")
public R<?> logout(HttpServletRequest request) {
@@ -86,6 +84,17 @@
return R.ok();
}
+ @DeleteMapping("logoutApplet")
+ public R<?> logoutApplet(HttpServletRequest request) {
+ String token = SecurityUtils.getToken(request);
+ if (StringUtils.isNotEmpty(token))
+ {
+ // 删除用户缓存记录
+ AuthUtil.logoutByToken(token);
+ }
+ return R.ok();
+ }
+
@PostMapping("refresh")
public R<?> refresh(HttpServletRequest request)
{
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java
index 1d2510e..0748dd4 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java
@@ -11,6 +11,7 @@
* 缓存有效期,默认720(分钟)
*/
public final static long EXPIRATION = 720;
+ public final static long EXPIRATION_APPLET = 7*24*60*60;
/**
* 缓存刷新时间,默认120(分钟)
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/exception/user/UserAppletException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/exception/user/UserAppletException.java
new file mode 100644
index 0000000..b8da2e9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/exception/user/UserAppletException.java
@@ -0,0 +1,48 @@
+package com.ruoyi.common.core.exception.user;
+
+/**
+ * 学生端登录异常信息
+ *
+ * @author HJL
+ * @version 1.0
+ * @since 2024-05-24 11:35
+ */
+public class UserAppletException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ /**
+ * 错误码
+ */
+ private int code;
+
+ public UserAppletException() {
+ }
+
+ public UserAppletException(String message, Integer code) {
+ this.message = message;
+ this.code = code;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public void setCode(int code) {
+ this.code = code;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/handler/GlobalExceptionHandler.java b/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/handler/GlobalExceptionHandler.java
index a9cb1b7..262a05f 100644
--- a/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/handler/GlobalExceptionHandler.java
+++ b/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/handler/GlobalExceptionHandler.java
@@ -3,6 +3,8 @@
import javax.naming.SizeLimitExceededException;
import javax.servlet.http.HttpServletRequest;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.exception.user.UserAppletException;
import org.apache.commons.fileupload.FileUploadBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -151,4 +153,12 @@
log.error("上传文件异常 => : {}", e.getMessage());
return AjaxResult.error("文件识别大小超出限制,允许的大小在" + maxFileSize);
}
+
+ /**
+ * 学生端单点登录-异常信息
+ */
+ @ExceptionHandler(UserAppletException.class)
+ public AjaxResult<String> studyLoginExceptionHandler(UserAppletException e) {
+ return AjaxResult.error(e.getCode(), e.getMessage());
+ }
}
diff --git a/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/service/TokenService.java b/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/service/TokenService.java
index 2d7cae5..2735380 100644
--- a/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/service/TokenService.java
+++ b/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/service/TokenService.java
@@ -5,6 +5,7 @@
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
+import com.ruoyi.common.core.exception.user.UserAppletException;
import com.ruoyi.system.api.model.LoginUserApplet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -35,6 +36,7 @@
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private final static long expireTime = CacheConstants.EXPIRATION;
+ private final static long expireAppletTime = CacheConstants.EXPIRATION_APPLET;
private final static String ACCESS_TOKEN = CacheConstants.LOGIN_TOKEN_KEY;
@@ -71,7 +73,7 @@
*/
public Map<String, Object> createTokenApplet(LoginUserApplet loginUser) {
String token = IdUtils.fastUUID();
- Integer userId = loginUser.getUserid();
+ Long userId = loginUser.getUserId();
String name = loginUser.getName();
loginUser.setToken(token);
loginUser.setIpaddr(IpUtils.getIpAddr());
@@ -84,11 +86,15 @@
// 接口返回信息
Map<String, Object> rspMap = new HashMap<String, Object>();
rspMap.put("access_token", JwtUtils.createToken(claimsMap));
- rspMap.put("expires_in", expireTime);
+ rspMap.put("expires_in", expireAppletTime);
return rspMap;
}
public LoginUserApplet getLoginUserApplet() {
- return getLoginUserAppletToken(ServletUtils.getRequest());
+ LoginUserApplet loginUserAppletToken = getLoginUserAppletToken(ServletUtils.getRequest());
+ if (loginUserAppletToken == null){
+ throw new UserAppletException("登录失效,请重新登录!", 401);
+ }
+ return loginUserAppletToken;
}
public LoginUserApplet getLoginUserAppletToken(HttpServletRequest request) {
// 获取请求携带的令牌
@@ -104,8 +110,8 @@
LoginUserApplet user = null;
try {
if (StringUtils.isNotEmpty(token)) {
- String userkey = JwtUtils.getUserKeyApplet(token);
- user = redisService.getCacheObject(getTokenKey(userkey));
+ String userKey = JwtUtils.getUserKeyApplet(token);
+ user = redisService.getCacheObject(getTokenKey(userKey));
return user;
}
} catch (Exception e) {
diff --git a/ruoyi-service/ruoyi-account/pom.xml b/ruoyi-service/ruoyi-account/pom.xml
index 435d4bc..0390101 100644
--- a/ruoyi-service/ruoyi-account/pom.xml
+++ b/ruoyi-service/ruoyi-account/pom.xml
@@ -137,6 +137,12 @@
<scope>compile</scope>
</dependency>
+ <dependency>
+ <groupId>com.alipay.sdk</groupId>
+ <artifactId>alipay-sdk-java</artifactId>
+ <version>4.38.10.ALL</version>
+ </dependency>
+
</dependencies>
<build>
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/Constant/AliConstant.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/Constant/AliConstant.java
new file mode 100644
index 0000000..8cb6198
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/Constant/AliConstant.java
@@ -0,0 +1,10 @@
+package com.ruoyi.account.ali.Constant;
+
+public class AliConstant {
+
+ /**
+ * 支付宝配置
+ */
+ public static final String GRANT_TYPE = "authorization_code";
+
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/model/AliProperties.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/model/AliProperties.java
new file mode 100644
index 0000000..679e7dc
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/model/AliProperties.java
@@ -0,0 +1,70 @@
+package com.ruoyi.account.ali.model;
+
+import lombok.ToString;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author xiaochen
+ * @ClassName ALiProperties
+ * @Description
+ * @date 2024-08-14 13:55
+ */
+@ToString
+@Component
+@ConfigurationProperties(prefix = "ali.conf")
+public class AliProperties {
+
+
+ /**
+ * 商户私钥,您的PKCS8格式RSA2私钥
+ */
+ private String privateKey;
+ /**
+ * 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
+ */
+ private String alipayPublicKey;
+ /**
+ * 应用ID,您的APPID。
+ */
+ private String appId;
+
+ /**
+ * HTTP(S) 连接超时时间,单位毫秒
+ *
+ */
+ public int getHttpConnectTimeoutMs() {
+ return 6 * 1000;
+ }
+
+ /**
+ * HTTP(S) 读数据超时时间,单位毫秒
+ */
+ public int getHttpReadTimeoutMs() {
+ return 8 * 1000;
+ }
+
+ public String getPrivateKey() {
+ return privateKey;
+ }
+
+ public void setPrivateKey(String privateKey) {
+ this.privateKey = privateKey;
+ }
+
+ public String getAlipayPublicKey() {
+ return alipayPublicKey;
+ }
+
+ public void setAlipayPublicKey(String alipayPublicKey) {
+ this.alipayPublicKey = alipayPublicKey;
+ }
+
+ public String getAppId() {
+ return appId;
+ }
+
+ public void setAppId(String appId) {
+ this.appId = appId;
+ }
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/tools/AliAppletTools.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/tools/AliAppletTools.java
new file mode 100644
index 0000000..52d54d7
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/ali/tools/AliAppletTools.java
@@ -0,0 +1,37 @@
+package com.ruoyi.account.ali.tools;
+
+import com.alipay.api.AlipayConfig;
+import com.ruoyi.account.ali.model.AliProperties;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author xiaochen
+ * @ClassName WxAppletTools
+ * @Description
+ * @date 2024-8-04 13:55
+ */
+@Slf4j
+public class AliAppletTools {
+
+ private static final String SERVER_URL = "https://openapi.alipay.com/gateway.do";
+
+ private AliProperties aliProperties;
+
+ public AliAppletTools(AliProperties aliProperties) {
+ this.aliProperties = aliProperties;
+ }
+
+ /**
+ * 初始化支付宝配置
+ * @return
+ */
+ public AlipayConfig getAlipayConfig() {
+ AlipayConfig alipayConfig = new AlipayConfig();
+ alipayConfig.setServerUrl(SERVER_URL);
+ alipayConfig.setAppId(aliProperties.getAppId());
+ alipayConfig.setPrivateKey(aliProperties.getPrivateKey());
+ alipayConfig.setAlipayPublicKey(aliProperties.getAlipayPublicKey());
+ return alipayConfig;
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java
new file mode 100644
index 0000000..b574502
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java
@@ -0,0 +1,91 @@
+package com.ruoyi.account.controller;
+
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.AlipayConfig;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.diagnosis.DiagnosisUtils;
+import com.alipay.api.request.AlipaySystemOauthTokenRequest;
+import com.alipay.api.response.AlipaySystemOauthTokenResponse;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.ruoyi.account.ali.Constant.AliConstant;
+import com.ruoyi.account.ali.model.AliProperties;
+import com.ruoyi.account.ali.tools.AliAppletTools;
+import com.ruoyi.account.api.model.TAppUser;
+import com.ruoyi.account.service.TAppUserService;
+import com.ruoyi.common.core.exception.ServiceException;
+import com.ruoyi.common.core.web.domain.AjaxResult;
+import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.system.api.model.LoginUserApplet;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * <p>
+ * 支付宝小程序登录 前端控制器
+ * </p>
+ *
+ * @author xiaochen
+ * @since 2024-08-06
+ */
+@Slf4j
+@RestController
+@RequestMapping("/aliLogin")
+public class AliLoginController {
+
+ @Autowired
+ private AliProperties aliProperties;
+ @Autowired
+ private TAppUserService appUserService;
+ @Autowired
+ private TokenService tokenService;
+ @ApiOperation(value = "通过code获得openid",tags = {"支付宝小程序登录"})
+ @GetMapping("/openIdByJsCode")
+ public AjaxResult<Map<String, Object>> openIdByJsCode(@RequestParam(name = "code")@ApiParam(value = "code", required = true) String code) throws AlipayApiException {
+ log.info("<<<<<<<<换取openid开始<<<<<<<<:{}", code);
+ // 初始化SDK
+ AlipayClient alipayClient = new DefaultAlipayClient(new AliAppletTools(aliProperties).getAlipayConfig());
+ // 构造请求参数以调用接口
+ AlipaySystemOauthTokenRequest request = new AlipaySystemOauthTokenRequest();
+ // 设置授权码
+ request.setCode(code);
+ // 设置授权方式
+ request.setGrantType(AliConstant.GRANT_TYPE);
+ AlipaySystemOauthTokenResponse response = alipayClient.execute(request);
+ TAppUser appUser = null;
+ if (response.isSuccess()) {
+ String openId = response.getOpenId();
+ appUser = appUserService.getOne(Wrappers.lambdaQuery(TAppUser.class).eq(TAppUser::getAliOpenid, openId).last("limit 1"));
+ if (Objects.isNull(appUser)) {
+ appUser = new TAppUser();
+ appUser.setWxOpenid(openId);
+ appUserService.save(appUser);
+ }
+ log.info("支付宝小程序登录调用成功");
+ } else {
+ String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
+ log.warn("诊断结果:{}",diagnosisUrl);
+ throw new ServiceException("支付宝小程序登录失败");
+ }
+ LoginUserApplet loginUserApplet = new LoginUserApplet();
+ if(ObjectUtils.isNotNull(appUser)){
+ loginUserApplet.setUserId(appUser.getId());
+ }
+ HashMap<String, Object> tokenInfos = new HashMap<>();
+ tokenInfos.put("token",tokenService.createTokenApplet(loginUserApplet));
+ tokenInfos.put("info",loginUserApplet);
+ return AjaxResult.ok(tokenInfos);
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppCouponController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppCouponController.java
index a85603f..2bf8d35 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppCouponController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppCouponController.java
@@ -16,6 +16,7 @@
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.BasePage;
import com.ruoyi.common.core.web.page.PageInfo;
+import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.other.api.domain.TCoupon;
import com.ruoyi.other.api.feignClient.OtherClient;
import io.swagger.annotations.ApiModelProperty;
@@ -48,6 +49,8 @@
@Autowired
private TAppUserCarService appUserCarService;
+ @Autowired
+ private TokenService tokenService;
/**
* 查询用户可用优惠券数量
* @param dto
@@ -68,7 +71,7 @@
public AjaxResult<List<TAppUserCar>> carList() {
// todo 用户id
List<TAppUserCar> appUserId = appUserCarService.list(new QueryWrapper<TAppUserCar>()
- .eq("app_user_id", 11));
+ .eq("app_user_id",tokenService.getLoginUserApplet().getUserid() ));
return AjaxResult.ok(appUserId);
}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
index f8237ff..217799e 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -3,18 +3,25 @@
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.api.dto.*;
import com.ruoyi.account.api.model.*;
import com.ruoyi.account.api.vo.CouponListVOVO;
import com.ruoyi.account.service.*;
+import com.ruoyi.account.wx.body.resp.Code2SessionRespBody;
+import com.ruoyi.account.wx.body.resq.Code2SessionResqBody;
+import com.ruoyi.account.wx.model.WeixinProperties;
+import com.ruoyi.account.wx.tools.WxAppletTools;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.bean.BeanUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.domain.BasePojo;
import com.ruoyi.common.core.web.page.PageInfo;
+import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.common.security.annotation.RequiresPermissions;
+import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.order.api.feignClient.ChargingOrderClient;
import com.ruoyi.order.api.feignClient.ExchangeOrderClient;
import com.ruoyi.order.api.model.TChargingOrder;
@@ -22,18 +29,20 @@
import com.ruoyi.other.api.domain.TCompany;
import com.ruoyi.other.api.domain.TUserTag;
import com.ruoyi.other.api.feignClient.OtherClient;
+import com.ruoyi.system.api.domain.SysRole;
+import com.ruoyi.system.api.model.LoginUserApplet;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.stream.Collectors;
/**
@@ -44,6 +53,7 @@
* @author luodangjia
* @since 2024-08-06
*/
+@Slf4j
@RestController
@RequestMapping("/t-app-user")
public class TAppUserController {
@@ -68,8 +78,6 @@
@Resource
private ExchangeOrderClient exchangeOrderClient;
-
-
@ApiOperation(value = "管理后台-根据手机号查询用户ids", tags = {"管理后台-活动费用统计"})
@PostMapping(value = "/user/getUserIdsByPhone")
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/WxLoginController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/WxLoginController.java
new file mode 100644
index 0000000..e3444f7
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/WxLoginController.java
@@ -0,0 +1,67 @@
+package com.ruoyi.account.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.ruoyi.account.api.model.TAppUser;
+import com.ruoyi.account.service.TAppUserService;
+import com.ruoyi.account.wx.body.resp.Code2SessionRespBody;
+import com.ruoyi.account.wx.body.resq.Code2SessionResqBody;
+import com.ruoyi.account.wx.model.WeixinProperties;
+import com.ruoyi.account.wx.pojo.AppletUserDecodeData;
+import com.ruoyi.account.wx.pojo.AppletUserEncrypteData;
+import com.ruoyi.account.wx.tools.WxAppletTools;
+import com.ruoyi.account.wx.tools.WxUtils;
+import com.ruoyi.common.core.exception.ServiceException;
+import com.ruoyi.common.core.web.domain.AjaxResult;
+import com.ruoyi.common.redis.service.RedisService;
+import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.system.api.model.LoginUserApplet;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+/**
+ * <p>
+ * 微信小程序登录 前端控制器
+ * </p>
+ *
+ * @author xiaochen
+ * @since 2024-08-06
+ */
+@Slf4j
+@RestController
+@RequestMapping("/wxLogin")
+public class WxLoginController {
+ @Autowired
+ private TAppUserService appUserService;
+ @Autowired
+ private WeixinProperties wxConfig;
+ @Autowired
+ private RestTemplate wxRestTemplate;
+ @ApiOperation(value = "通过code获得openid,获取用户信息",tags = {"微信小程序登录"})
+ @PostMapping("/openIdByJsCode")
+ public AjaxResult<Map<String, Object>> openIdByJsCode(@RequestBody AppletUserEncrypteData data) {
+ log.info("<<<<<<<<换取openid开始<<<<<<<<:{}", data.getCode());
+ WxAppletTools appletTools = new WxAppletTools(wxRestTemplate, wxConfig);
+ 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());
+ }
+ AppletUserDecodeData appletUserDecodeData = WxUtils.encryptedData(data.getEncryptedData(), sessionKey, data.getIv());
+ appletUserDecodeData.setOpenId(openid);
+ return AjaxResult.ok(appUserService.login(appletUserDecodeData));
+ }
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java
index 600895f..11fec43 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java
@@ -2,6 +2,9 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.account.api.model.TAppUser;
+import com.ruoyi.account.wx.pojo.AppletUserDecodeData;
+
+import java.util.Map;
/**
* <p>
@@ -13,4 +16,11 @@
*/
public interface TAppUserService extends IService<TAppUser> {
+ /**
+ * 微信小程序登录用户封装
+ * @param appletUserDecodeData
+ * @return
+ */
+ Map<String, Object> login(AppletUserDecodeData appletUserDecodeData);
+
}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java
index 3e3b665..912a5e0 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java
@@ -1,10 +1,23 @@
package com.ruoyi.account.service.impl;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.model.TAppUser;
import com.ruoyi.account.mapper.TAppUserMapper;
import com.ruoyi.account.service.TAppUserService;
+import com.ruoyi.account.wx.model.WeixinProperties;
+import com.ruoyi.account.wx.pojo.AppletUserDecodeData;
+import com.ruoyi.common.security.service.TokenService;
+import com.ruoyi.system.api.model.LoginUserApplet;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
/**
* <p>
@@ -16,5 +29,48 @@
*/
@Service
public class TAppUserServiceImpl extends ServiceImpl<TAppUserMapper, TAppUser> implements TAppUserService {
-
+ @Autowired
+ private TokenService tokenService;
+ @Override
+ public Map<String, Object> login(AppletUserDecodeData appletUserDecodeData) {
+ // 通过手机号查询用户,是否已存在手动导入用户
+ TAppUser appUser = this.getOne(Wrappers.lambdaQuery(TAppUser.class)
+ .eq(TAppUser::getPhone, appletUserDecodeData.getPhoneNumber())
+ .isNull(TAppUser::getWxOpenid)
+ .last("LIMIT 1"));
+ LambdaQueryWrapper<TAppUser> wrapper = Wrappers.lambdaQuery(TAppUser.class)
+ .eq(TAppUser::getWxOpenid, appletUserDecodeData.getOpenId());
+ if(Objects.isNull(appUser)){
+ // 先使用openId和当前手机号进行查询
+ wrapper.eq(TAppUser::getPhone, appletUserDecodeData.getPhoneNumber())
+ .last("LIMIT 1");
+ appUser = this.getOne(wrapper);
+ if(Objects.isNull(appUser)){
+ appUser = new TAppUser();
+ appUser.setPhone(appletUserDecodeData.getPhoneNumber());
+ }
+ }else {
+ wrapper.last("LIMIT 1");
+ // 删除小程序原有授权用户
+ this.remove(wrapper);
+ }
+ appUser.setAvatar(appletUserDecodeData.getAvatarUrl());
+ appUser.setCity(appletUserDecodeData.getCity());
+ appUser.setName(appletUserDecodeData.getNickName());
+ appUser.setProvince(appletUserDecodeData.getProvince());
+ appUser.setWxOpenid(appletUserDecodeData.getOpenId());
+ this.saveOrUpdate(appUser);
+ LoginUserApplet loginUserApplet = new LoginUserApplet();
+ if(ObjectUtils.isNotNull(appUser)){
+ loginUserApplet.setUserId(appUser.getId());
+ loginUserApplet.setName(appUser.getName());
+ loginUserApplet.setPhone(appUser.getPhone());
+ loginUserApplet.setAvatar(appUser.getAvatar());
+ loginUserApplet.setAddress(appUser.getProvince()+appUser.getCity());
+ }
+ Map<String, Object> tokenInfos = new HashMap<>();
+ tokenInfos.put("token",tokenService.createTokenApplet(loginUserApplet));
+ tokenInfos.put("info",loginUserApplet);
+ return tokenInfos;
+ }
}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/AccessTokenRespBody.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/AccessTokenRespBody.java
new file mode 100644
index 0000000..abc6b3c
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/AccessTokenRespBody.java
@@ -0,0 +1,28 @@
+package com.ruoyi.account.wx.body.resp;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * AccessToken 全局唯一
+ *
+ * @author xiaochen
+ */
+@Data
+public class AccessTokenRespBody extends RespBody implements Serializable {
+
+ /**
+ * 获取到的凭证
+ */
+ @JsonProperty("access_token")
+ private String accessToken;
+ /**
+ * 凭证有效时间,单位:秒
+ */
+ @JsonProperty("expires_in")
+ private int expiresIn;
+
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/Code2SessionRespBody.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/Code2SessionRespBody.java
new file mode 100644
index 0000000..0dcd69a
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/Code2SessionRespBody.java
@@ -0,0 +1,29 @@
+package com.ruoyi.account.wx.body.resp;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * @author xiaochen
+ * @ClassName Code2SessionRespBody
+ * @Description
+ * @date 2021-07-28 12:35
+ */
+@Data
+public class Code2SessionRespBody extends RespBody {
+ /**
+ * 用户唯一标识
+ */
+ @JsonProperty("openid")
+ private String openid;
+ /**
+ * 会话密钥
+ */
+ @JsonProperty("session_key")
+ private String sessionKey;
+ /**
+ * 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
+ */
+ @JsonProperty("unionid")
+ private String unionid;
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/RespBody.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/RespBody.java
new file mode 100644
index 0000000..4056d18
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/RespBody.java
@@ -0,0 +1,19 @@
+package com.ruoyi.account.wx.body.resp;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * @author xiaochen
+ * @ClassName RespBody
+ * @Description
+ * @date 2021-07-28 11:44
+ */
+@Data
+public class RespBody {
+ @JsonProperty("errcode")
+ private Integer errorCode;
+
+ @JsonProperty("errmsg")
+ private String errorMsg;
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resq/Code2SessionResqBody.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resq/Code2SessionResqBody.java
new file mode 100644
index 0000000..193f47f
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resq/Code2SessionResqBody.java
@@ -0,0 +1,21 @@
+package com.ruoyi.account.wx.body.resq;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * @author xiaochen
+ * @ClassName Code2SessionResqBody
+ * @Description
+ * @date 2021-07-28 11:47
+ */
+@Data
+public class Code2SessionResqBody {
+ @JsonProperty("js_code")
+ private String jsCode;
+
+ public Code2SessionResqBody build(String jsCode) {
+ this.jsCode = jsCode;
+ return this;
+ }
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/model/WeixinProperties.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/model/WeixinProperties.java
new file mode 100644
index 0000000..f760c14
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/model/WeixinProperties.java
@@ -0,0 +1,80 @@
+package com.ruoyi.account.wx.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
+ * @date 2024-08-14 13:55
+ */
+@ToString
+@Component
+@ConfigurationProperties(prefix = "wx.conf")
+public class WeixinProperties {
+ /**
+ * 默认开启
+ */
+ 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;
+
+ public String getSecretId() {
+ return secretId;
+ }
+
+ public void setSecretId(String secretId) {
+ this.secretId = secretId;
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletPhoneEncrypteData.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletPhoneEncrypteData.java
new file mode 100644
index 0000000..8e48fcc
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletPhoneEncrypteData.java
@@ -0,0 +1,19 @@
+package com.ruoyi.account.wx.pojo;
+
+import lombok.Data;
+
+/**
+ * @author xiaochen
+ * @ClassName AppletUserDecodeData
+ * @Description
+ * @date 2021-08-13 17:46
+ * 小程序加密数据体
+ *
+ */
+@Data
+public class AppletPhoneEncrypteData {
+ private String encryptedData;
+ private String openid;
+ private String unionid;
+ private String iv;
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserDecodeData.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserDecodeData.java
new file mode 100644
index 0000000..6040fe9
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserDecodeData.java
@@ -0,0 +1,52 @@
+package com.ruoyi.account.wx.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-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserEncrypteData.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserEncrypteData.java
new file mode 100644
index 0000000..45884a8
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserEncrypteData.java
@@ -0,0 +1,17 @@
+package com.ruoyi.account.wx.pojo;
+
+import lombok.Data;
+
+/**
+ * @author xiaochen
+ * @ClassName AppletUserDecodeData
+ * @Description
+ * 小程序加密数据体
+ *
+ */
+@Data
+public class AppletUserEncrypteData extends AppletPhoneEncrypteData {
+ private String rawData;
+ private String signature;
+ private String code;
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/Watermark.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/Watermark.java
new file mode 100644
index 0000000..58df6cc
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/Watermark.java
@@ -0,0 +1,9 @@
+package com.ruoyi.account.wx.pojo;
+
+import lombok.Data;
+
+@Data
+public class Watermark {
+ private String appid;
+ private String timestamp;
+}
\ No newline at end of file
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/JsonUtils.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/JsonUtils.java
new file mode 100644
index 0000000..0b9fe63
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/JsonUtils.java
@@ -0,0 +1,110 @@
+package com.ruoyi.account.wx.tools;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.hotel.config.JacksonConfig;
+import com.hotel.exception.ServiceException;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Json转换工具类
+ * 参考:https://blog.csdn.net/weixin_38413579/article/details/82562634
+ * @author madman
+ */
+@Slf4j
+public final class JsonUtils {
+
+ private static final ObjectMapper OM = new ObjectMapper();
+ private static final JavaTimeModule timeModule = new JavaTimeModule();
+
+ /**
+ * 转换LocalDateTime
+ */
+ static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
+ @Override
+ public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
+ jsonGenerator.writeString(localDateTime.format(DateTimeFormatter.ofPattern(JacksonConfig.dateTimeFormat)));
+ }
+ }
+
+ /**
+ * 转换LocalDate
+ */
+ static class LocalDateSerializer extends JsonSerializer<LocalDate> {
+ @Override
+ public void serialize(LocalDate localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
+ jsonGenerator.writeString(localDate.format(DateTimeFormatter.ofPattern(JacksonConfig.dateFormat)));
+ }
+ }
+
+ /**
+ * 设置 ObjectMapper
+ *
+ * @return
+ */
+ private static ObjectMapper getObjectMapper() {
+ // 序列化
+ timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
+ timeModule.addSerializer(LocalDate.class, new LocalDateSerializer());
+ // 反序列化
+ timeModule.addDeserializer(LocalDateTime.class,
+ new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(JacksonConfig.dateTimeFormat)));
+ timeModule.addDeserializer(LocalDate.class,
+ new LocalDateDeserializer(DateTimeFormatter.ofPattern(JacksonConfig.dateFormat)));
+ // 允许对象忽略json中不存在的属性
+ OM.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ OM.registerModule(timeModule);
+ return OM;
+ }
+
+ /**
+ * 将对象序列化
+ */
+ public static <T> String toJsonString(T obj) {
+ try {
+ ObjectMapper om = getObjectMapper();
+ return om.writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ log.error("转json字符串失败:{}", obj);
+ return null;
+ }
+ }
+
+ /**
+ * 反序列化对象字符串
+ */
+ public static <T> T parseObject(String json, Class<T> clazz) {
+ try {
+ ObjectMapper om = getObjectMapper();
+ return om.readValue(json, clazz);
+ } catch (JsonProcessingException e) {
+ throw new ServiceException("反序列化对象字符串失败");
+ }
+ }
+
+ /**
+ * 反序列化字符串成为对象
+ */
+ public static <T> T parseObject(String json, TypeReference<T> valueTypeRef) {
+ try {
+ ObjectMapper om = getObjectMapper();
+ return om.readValue(json, valueTypeRef);
+ } catch (JsonProcessingException e) {
+ throw new ServiceException("反序列化字符串成为对象失败");
+ }
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/SHA1.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/SHA1.java
new file mode 100644
index 0000000..47c0181
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/SHA1.java
@@ -0,0 +1,36 @@
+package com.ruoyi.account.wx.tools;
+
+import java.security.MessageDigest;
+
+public class SHA1 {
+
+
+ /**
+ * 用SHA1算法生成安全签名
+ *
+ * @param str
+ * @return
+ * @throws WxException
+ */
+ public static String getSHA1(String str) throws WxException {
+ try {
+ // SHA1签名生成
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ md.update(str.getBytes());
+ byte[] digest = md.digest();
+ StringBuffer hexstr = new StringBuffer();
+ String shaHex;
+ for (int i = 0; i < digest.length; i++) {
+ shaHex = Integer.toHexString(digest[i] & 0xFF);
+ if (shaHex.length() < 2) {
+ hexstr.append(0);
+ }
+ hexstr.append(shaHex);
+ }
+ return hexstr.toString();
+ } catch (Exception e) {
+ throw new WxException(WxException.ComputeSignatureError);
+ }
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WebUtils.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WebUtils.java
new file mode 100644
index 0000000..69966eb
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WebUtils.java
@@ -0,0 +1,48 @@
+package com.ruoyi.account.wx.tools;
+
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+/**
+ * @Author xiaochen
+ * @Date 2019/08/26 10:28 AM
+ * @Description
+ */
+public final class WebUtils {
+
+ private WebUtils() {
+ }
+
+ /**
+ * 当前请求
+ */
+ public static HttpServletRequest request() {
+ return contextHolder() == null ? null : contextHolder().getRequest();
+ }
+
+ /**
+ * 当前响应
+ */
+ public static HttpServletResponse response() {
+ return contextHolder() == null ? null : contextHolder().getResponse();
+ }
+
+ /**
+ * 当前session
+ */
+ public static HttpSession session() {
+ return request() == null ? null : request().getSession();
+ }
+
+ /**
+ * 当前ServletRequest
+ */
+ public static ServletRequestAttributes contextHolder() {
+ return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxAppletTools.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxAppletTools.java
new file mode 100644
index 0000000..01feebf
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxAppletTools.java
@@ -0,0 +1,125 @@
+package com.ruoyi.account.wx.tools;
+
+import com.ruoyi.account.wx.body.resp.AccessTokenRespBody;
+import com.ruoyi.account.wx.body.resp.Code2SessionRespBody;
+import com.ruoyi.account.wx.body.resq.Code2SessionResqBody;
+import com.ruoyi.account.wx.model.WeixinProperties;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+
+import java.text.MessageFormat;
+
+/**
+ * @author xiaochen
+ * @ClassName WxAppletTools
+ * @Description
+ * @date 2024-8-04 13:55
+ */
+@Slf4j
+public class WxAppletTools {
+ 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 错误信息
+ */
+ public static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
+ private WeixinProperties wxConfig;
+ private RestTemplate wxRestTemplate;
+ private WxCacheTemplate<String> wxCacheTemplate;
+
+ public WxAppletTools(RestTemplate wxRestTemplate, WeixinProperties wxConfig, WxCaffineCache wxCacheTemplate) {
+ this.wxRestTemplate = wxRestTemplate;
+ this.wxCacheTemplate = wxCacheTemplate;
+ this.wxConfig = wxConfig;
+ }
+
+ public WxAppletTools(RestTemplate wxRestTemplate, WeixinProperties wxConfig) {
+ this.wxRestTemplate = wxRestTemplate;
+ this.wxConfig = wxConfig;
+ }
+
+ /**
+ * 自定义部分数据
+ *
+ * @param wxConfig
+ * @return
+ */
+ public WxAppletTools build(WeixinProperties wxConfig) {
+ this.wxConfig = wxConfig;
+ return this;
+ }
+
+ /**
+ * @param resqBody
+ * @return
+ */
+ public Code2SessionRespBody getOpenIdByJscode2session(Code2SessionResqBody resqBody) {
+ long start = System.currentTimeMillis();
+ String requestUrl = MessageFormat.format(JSCODE_2_SESSION_URL, wxConfig.getAppId(), wxConfig.getSecretId(), resqBody.getJsCode());
+ long end = System.currentTimeMillis();
+ log.info("code换取sessionKey时间:{}", (end - start));
+ String respBody = wxRestTemplate.getForEntity(requestUrl, String.class).getBody();
+ end = System.currentTimeMillis();
+ log.info("code换取sessionKey时间:{}", (end - start));
+ log.info("Jscode2session:{}", respBody);
+ Code2SessionRespBody code2SessionRespBody = WxJsonUtils.parseObject(respBody, Code2SessionRespBody.class);
+ // 判断有误异常
+ if (StringUtils.hasLength(code2SessionRespBody.getErrorMsg())) {
+ // 抛出错误
+ throw new WxException(code2SessionRespBody.getErrorCode() + ":" + code2SessionRespBody.getErrorMsg());
+ }
+ return code2SessionRespBody;
+ }
+
+ /**
+ * @return
+ */
+ public String getAccessToken(String version) {
+ String accessToken = wxCacheTemplate.getKey(ACCESSTOKEN_CACHE_KEY + version);
+ if (StringUtils.hasLength(accessToken)) {
+ return accessToken;
+ }
+ String requestUrl = MessageFormat.format(ACCESS_TOKEN_URL, wxConfig.getAppId(), wxConfig.getSecretId());
+ String respBody = wxRestTemplate.getForEntity(requestUrl, String.class).getBody();
+ AccessTokenRespBody accessTokenRespBody = WxJsonUtils.parseObject(respBody, AccessTokenRespBody.class);
+ // 判断有误异常
+ if (StringUtils.hasLength(accessTokenRespBody.getErrorMsg())) {
+ // 抛出错误
+ throw new WxException(accessTokenRespBody.getErrorCode() + ":" + accessTokenRespBody.getErrorMsg());
+ }
+ wxCacheTemplate.setKey(ACCESSTOKEN_CACHE_KEY + version, accessTokenRespBody.getAccessToken());
+ return accessTokenRespBody.getAccessToken();
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCache.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCache.java
new file mode 100644
index 0000000..a8b3560
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCache.java
@@ -0,0 +1,117 @@
+package com.ruoyi.account.wx.tools;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 缓存
+ *
+ * @author xiaochen
+ */
+class WxCache {
+ /**
+ * 缓存的初始化容量
+ */
+ private int initialCapacity = 50;
+ /**
+ * 缓存最大容量
+ */
+ private long maximumSize = 200L;
+ /**
+ * 缓存时长
+ */
+ private long duration = 7000L;
+ /**
+ * 时长单位,自动转换
+ * 支持:
+ * 时
+ * 分
+ * 秒
+ * 天
+ */
+ private TimeUnit timeunit = TimeUnit.SECONDS;
+
+ public int getInitialCapacity() {
+ return initialCapacity;
+ }
+
+ public void setInitialCapacity(int initialCapacity) {
+ this.initialCapacity = initialCapacity;
+ }
+
+ public long getMaximumSize() {
+ return maximumSize;
+ }
+
+ public void setMaximumSize(long maximumSize) {
+ this.maximumSize = maximumSize;
+ }
+
+
+ public long getDuration() {
+ return duration;
+ }
+
+ public void setDuration(long duration) {
+ this.duration = duration;
+ }
+
+ public TimeUnit getTimeunit() {
+ return timeunit;
+ }
+
+ public void setTimeunit(TimeUnit timeunit) {
+ this.timeunit = timeunit;
+ }
+
+ public static class Builder {
+ private int initialCapacity;
+ private long maximumSize;
+ private long duration;
+ private TimeUnit timeunit;
+
+ public Builder setInitialCapacity(int initialCapacity) {
+ this.initialCapacity = initialCapacity;
+ return this;
+ }
+
+ public Builder setMaximumSize(long maximumSize) {
+ this.maximumSize = maximumSize;
+ return this;
+ }
+
+ public Builder setDuration(long duration) {
+ this.duration = duration;
+ return this;
+ }
+
+ public Builder setTimeUnit(TimeUnit timeunit) {
+ this.timeunit = timeunit;
+ return this;
+ }
+
+ public WxCache build() {
+ return new WxCache(this);
+ }
+ }
+
+ public static Builder options() {
+ return new Builder();
+ }
+
+ private WxCache(Builder builder) {
+ this.initialCapacity = 0 == builder.initialCapacity ? this.initialCapacity : builder.initialCapacity;
+ this.maximumSize = 0L == builder.maximumSize ? this.maximumSize : builder.maximumSize;
+ this.duration = 0L == builder.duration ? this.duration : builder.duration;
+ this.timeunit = null == builder.timeunit ? this.timeunit : builder.timeunit;
+ }
+
+ @Override
+ public String toString() {
+ return "WxCache{" +
+ "initialCapacity=" + initialCapacity +
+ ", maximumSize=" + maximumSize +
+ ", duration=" + duration +
+ ", timeunit=" + timeunit +
+ '}';
+ }
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCacheTemplate.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCacheTemplate.java
new file mode 100644
index 0000000..fa43614
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCacheTemplate.java
@@ -0,0 +1,34 @@
+package com.ruoyi.account.wx.tools;
+
+/**
+ * @author xiaochen
+ * @ClassName WxCacheTemplate
+ * @Description
+ * @date 2021-01-11 11:27
+ */
+public interface WxCacheTemplate<T> {
+ /**
+ * 保存key
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ boolean setKey(String key, T value);
+
+ /**
+ * 获取缓存
+ *
+ * @param key
+ * @return
+ */
+ T getKey(String key);
+
+ /**
+ * 删除
+ *
+ * @param key
+ * @return
+ */
+ boolean delKey(String key);
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCaffineCache.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCaffineCache.java
new file mode 100644
index 0000000..5c40a14
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCaffineCache.java
@@ -0,0 +1,122 @@
+package com.ruoyi.account.wx.tools;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author xiaochen
+ * @ClassName AbstractCaffineCache
+ * @Description
+ * @date 2021-01-11 11:27
+ */
+@Slf4j
+class WxCaffineCache<T> implements WxCacheTemplate<T> {
+ /**
+ * 缓存环境
+ */
+ private String env = "wx";
+
+ /**
+ * 本地缓存实例
+ */
+ private LoadingCache<String, Object> loadingCache;
+
+ /**
+ * 构造函数
+ *
+ */
+ public WxCaffineCache() {
+ WxCache cache = WxCache.options().setTimeUnit(TimeUnit.SECONDS).build();
+ // 构建本地缓存实例
+ this.loadingCache = caffineCacheManage(cache);
+ }
+
+
+ @Override
+ public boolean setKey(String key, T value) {
+ if (Objects.isNull(this.loadingCache)) {
+ return Boolean.FALSE;
+ }
+ if (StringUtils.hasLength(this.env)) {
+ this.loadingCache.put(this.env + ":" + key, value);
+ } else {
+ this.loadingCache.put(key, value);
+ }
+ return Boolean.TRUE;
+ }
+
+ @Override
+ public T getKey(String key) {
+ if (Objects.isNull(this.loadingCache)) {
+ return null;
+ }
+ try {
+ if (StringUtils.hasLength(this.env)) {
+ return (T) this.loadingCache.get(this.env + ":" + key);
+ } else {
+ return (T) this.loadingCache.get(key);
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean delKey(String key) {
+ if (Objects.isNull(this.loadingCache)) {
+ return Boolean.FALSE;
+ }
+ if (StringUtils.hasLength(this.env)) {
+ this.loadingCache.invalidate(this.env + ":" + key);
+ } else {
+ this.loadingCache.invalidate(key);
+ }
+ return Boolean.TRUE;
+ }
+
+ /**
+ * 缓存管理
+ *
+ * @param cache
+ * @param <T>
+ * @return
+ */
+ private static <T> LoadingCache<String, T> caffineCacheManage(WxCache cache) {
+ log.info("初始化缓存的实体数据:{}", cache);
+ if (Objects.isNull(cache)) {
+ throw new NullPointerException("请实例化一个Cache对象!");
+ }
+ LoadingCache<String, T> localcache =
+ // 构建本地缓存,调用链的方式
+ // ,1000是设置缓存的初始化容量,maximumSize是设置缓存最大容量,当超过了最大容量,guava将使用LRU算法(最少使用算法),来移除缓存项
+ // expireAfterAccess(12,TimeUnit.HOURS)设置缓存有效期为12个小时
+ Caffeine.newBuilder().initialCapacity(cache.getInitialCapacity()).maximumSize(cache.getMaximumSize())
+ // 设置写缓存后n秒钟过期
+ // .expireAfterWrite(30, TimeUnit.SECONDS)
+ .expireAfterWrite(cache.getDuration(), cache.getTimeunit())
+ // 设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
+ //.expireAfterAccess(googleCache.getDuration(), googleCache.getTimeunit())
+ // 只阻塞当前数据加载线程,其他线程返回旧值
+ //.refreshAfterWrite(10, TimeUnit.SECONDS)
+ // 设置缓存的移除通知//用户手动移除EXPLICIT,
+ // //用户手动替换REPLACED,//被垃圾回收COLLECTED,//超时过期EXPIRED,//SIZE由于缓存大小限制
+ .removalListener(new RemovalListener<String, T>() {
+ @Override
+ public void onRemoval(String key, Object value, RemovalCause cause) {
+ log.info(key + ":" + value + ":" + cause.name());
+ }
+ })
+ // build里面要实现一个匿名抽象类
+ .build(new CacheLoader<String, T>() {
+ // 这个方法是默认的数据加载实现,get的时候,如果key没有对应的值,就调用这个方法进行加载。此处是没有默认值则返回null
+ @Override
+ public T load(String key) throws Exception {
+ return null;
+ }
+ });
+ return localcache;
+ }
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxException.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxException.java
new file mode 100644
index 0000000..eb510a9
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxException.java
@@ -0,0 +1,55 @@
+package com.ruoyi.account.wx.tools;
+
+/**
+ * @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;
+ }
+
+ WxException(int code) {
+ super(getMessage(code));
+ this.code = code;
+ }
+
+ public WxException(String message) {
+ super(message);
+ }
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxJsonUtils.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxJsonUtils.java
new file mode 100644
index 0000000..83b871d
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxJsonUtils.java
@@ -0,0 +1,109 @@
+package com.ruoyi.account.wx.tools;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Json转换工具类
+ * 参考:https://blog.csdn.net/weixin_38413579/article/details/82562634
+ * @author madman
+ */
+@Slf4j
+public final class WxJsonUtils {
+ public static final String dateFormat = "yyyy-MM-dd";
+ public static final String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
+ private static final ObjectMapper OM = new ObjectMapper();
+ private static final JavaTimeModule timeModule = new JavaTimeModule();
+
+ /**
+ * 转换LocalDateTime
+ */
+ static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
+ @Override
+ public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
+ jsonGenerator.writeString(localDateTime.format(DateTimeFormatter.ofPattern(dateTimeFormat)));
+ }
+ }
+
+ /**
+ * 转换LocalDate
+ */
+ static class LocalDateSerializer extends JsonSerializer<LocalDate> {
+ @Override
+ public void serialize(LocalDate localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
+ jsonGenerator.writeString(localDate.format(DateTimeFormatter.ofPattern(dateFormat)));
+ }
+ }
+
+ /**
+ * 设置 ObjectMapper
+ *
+ * @return
+ */
+ private static ObjectMapper getObjectMapper() {
+ // 序列化
+ timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
+ timeModule.addSerializer(LocalDate.class, new LocalDateSerializer());
+ // 反序列化
+ timeModule.addDeserializer(LocalDateTime.class,
+ new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
+ timeModule.addDeserializer(LocalDate.class,
+ new LocalDateDeserializer(DateTimeFormatter.ofPattern(dateFormat)));
+ // 允许对象忽略json中不存在的属性
+ OM.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ OM.registerModule(timeModule);
+ return OM;
+ }
+
+ /**
+ * 将对象序列化
+ */
+ public static <T> String toJsonString(T obj) {
+ try {
+ ObjectMapper om = getObjectMapper();
+ return om.writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ log.error("转json字符串失败:{}", obj);
+ return null;
+ }
+ }
+
+ /**
+ * 反序列化对象字符串
+ */
+ public static <T> T parseObject(String json, Class<T> clazz) {
+ try {
+ ObjectMapper om = getObjectMapper();
+ return om.readValue(json, clazz);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("反序列化对象字符串失败");
+ }
+ }
+
+ /**
+ * 反序列化字符串成为对象
+ */
+ public static <T> T parseObject(String json, TypeReference<T> valueTypeRef) {
+ try {
+ ObjectMapper om = getObjectMapper();
+ return om.readValue(json, valueTypeRef);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("反序列化字符串成为对象失败");
+ }
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxUtils.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxUtils.java
new file mode 100644
index 0000000..47bd8c8
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxUtils.java
@@ -0,0 +1,175 @@
+package com.ruoyi.account.wx.tools;
+
+import com.ruoyi.account.wx.pojo.AppletUserDecodeData;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.CharEncoding;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.util.encoders.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+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.Security;
+import java.util.Arrays;
+
+/**
+ * @Description 获取用户信息工具类
+ * @Author xiaochen
+ * @Date 2021/8/12 15:45
+ */
+@Slf4j
+public class WxUtils {
+
+ /**
+ * 微信小程序API 用户数据的解密
+ *
+ * @param encryptedData
+ * @param sessionKey
+ * @param iv
+ * @return
+ */
+ public static AppletUserDecodeData encryptedData(String encryptedData, String sessionKey, String iv) {
+ // 被加密的数据
+ byte[] dataByte = Base64.decode(encryptedData);
+ // 加密秘钥
+ byte[] keyByte = Base64.decode(sessionKey);
+ // 偏移量
+ byte[] ivByte = Base64.decode(iv);
+ try {
+ // 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
+ int base = 16;
+ if (keyByte.length % base != 0) {
+ int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
+ byte[] temp = new byte[groups * base];
+ Arrays.fill(temp, (byte) 0);
+ System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
+ keyByte = temp;
+ }
+ // 初始化
+ Security.addProvider(new BouncyCastleProvider());
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
+ SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
+ AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
+ parameters.init(new IvParameterSpec(ivByte));
+ cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
+ byte[] resultByte = cipher.doFinal(dataByte);
+ if (null != resultByte && resultByte.length > 0) {
+ String result = new String(resultByte, CharEncoding.UTF_8);
+ log.info("解密原串:{}", result);
+ return WxJsonUtils.parseObject(result, AppletUserDecodeData.class);
+ }
+ throw new RuntimeException("解密的数据为空");
+ } catch (Exception e) {
+ log.error("解密失败. error = {}", e.getMessage(), e);
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+
+ /**
+ * 微信小程序API 用户数据的签名验证
+ * signature = sha1( rawData + session_key )
+ *
+ * @param rawData 不包括敏感信息的原始数据字符串,用于计算签名。
+ * @param sessionKey
+ */
+ public static void verifySignature(String rawData, String sessionKey, String signature) {
+ String serverSignature = SHA1.getSHA1(rawData + sessionKey);
+ log.info(rawData + ">>>>>>:" + sessionKey + " === " + serverSignature + " ======" + signature);
+ if (!signature.equals(serverSignature)) {
+ throw new RuntimeException("数据验签不通过");
+ }
+ }
+
+ /**
+ * 根据流接收请求数据
+ *
+ * @param request
+ * @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);
+ }
+ String data = buffer.toString();
+ reader.close();
+ inputStream.close();
+ log.info("微信异步回调数据:{}", data);
+ return data;
+ }
+
+ /**
+ * 日志
+ *
+ * @return
+ */
+ public static Logger getLogger() {
+ Logger logger = LoggerFactory.getLogger("wxpay java sdk");
+ return logger;
+ }
+
+ /**
+ * debug
+ *
+ * @param msg
+ * @param args
+ */
+ public static void debug(String msg, Object... args) {
+ Logger log = getLogger();
+ if (log.isDebugEnabled()) {
+ log.debug(msg, args);
+ }
+ }
+
+ /**
+ * info
+ *
+ * @param msg
+ * @param args
+ */
+ public static void info(String msg, Object... args) {
+ Logger log = getLogger();
+ if (log.isInfoEnabled()) {
+ log.info(msg, args);
+ }
+ }
+
+ /**
+ * warn
+ *
+ * @param msg
+ * @param args
+ */
+ public static void warn(String msg, Object... args) {
+ Logger log = getLogger();
+ if (log.isWarnEnabled()) {
+ log.warn(msg, args);
+ }
+ }
+
+ /**
+ * error
+ *
+ * @param msg
+ * @param args
+ */
+ public static void error(String msg, Object... args) {
+ Logger log = getLogger();
+ if (log.isErrorEnabled()) {
+ log.error(msg, args);
+ }
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
index 14971f7..9b405c1 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.BasePage;
import com.ruoyi.common.core.web.page.PageInfo;
@@ -12,6 +13,7 @@
import com.ruoyi.order.dto.MyChargingOrderInfo;
import com.ruoyi.order.service.TChargingOrderService;
import io.swagger.annotations.Api;
+import org.springframework.beans.factory.annotation.Autowired;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
@@ -35,6 +37,8 @@
@Resource
private TChargingOrderService chargingOrderService;
+ @Autowired
+ private TokenService tokenService;
/**
* 查询用户最近一次充电记录使用的车辆
* @param
@@ -42,9 +46,8 @@
*/
@PostMapping(value = "/getCar")
public R<Long> getCar() {
- // todo 获取用户id
List<TChargingOrder> list = chargingOrderService.list(new LambdaQueryWrapper<TChargingOrder>()
- .eq(TChargingOrder::getAppUserId, 11)
+ .eq(TChargingOrder::getAppUserId, tokenService.getLoginUserApplet().getUserid())
.isNotNull(TChargingOrder::getAppUserCarId));
if (!list.isEmpty()){
// 最近使用的车辆id
diff --git a/ruoyi-service/ruoyi-payment/pom.xml b/ruoyi-service/ruoyi-payment/pom.xml
index 5741c73..639a394 100644
--- a/ruoyi-service/ruoyi-payment/pom.xml
+++ b/ruoyi-service/ruoyi-payment/pom.xml
@@ -100,7 +100,18 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
-
+ <!-- wx sdk -->
+ <!-- https://mvnrepository.com/artifact/com.github.wechatpay-apiv3/wechatpay-apache-httpclient -->
+ <dependency>
+ <groupId>com.github.wechatpay-apiv3</groupId>
+ <artifactId>wechatpay-apache-httpclient</artifactId>
+ <version>0.4.3</version>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ <version>2.10.10</version>
+ </dependency>
</dependencies>
<build>
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/config/WxConfig.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/config/WxConfig.java
new file mode 100644
index 0000000..3aafb85
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/config/WxConfig.java
@@ -0,0 +1,33 @@
+package com.ruoyi.payment.wx.config;
+
+import com.ruoyi.payment.wx.model.WeixinProperties;
+import com.ruoyi.payment.wx.utils.WxV3Pay;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 项目中需继承此类
+ *
+ * @author lihen
+ */
+@ConditionalOnProperty(name = "wx.conf.enabled")
+@Configuration
+public class WxConfig {
+
+ private final WeixinProperties weixinProperties;
+
+ @Autowired
+ public WxConfig(WeixinProperties weixinProperties) {
+ this.weixinProperties = weixinProperties;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(name = "wxV3Pay")
+ public WxV3Pay wxSpV3Pay() {
+ return new WxV3Pay(weixinProperties);
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/controller/WxPayController.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/controller/WxPayController.java
new file mode 100644
index 0000000..30c22cb
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/controller/WxPayController.java
@@ -0,0 +1,142 @@
+package com.ruoyi.payment.wx.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.ruoyi.common.core.web.domain.AjaxResult;
+import com.ruoyi.payment.wx.enums.RefundEnum;
+import com.ruoyi.payment.wx.model.WxPaymentRefundModel;
+import com.ruoyi.payment.wx.utils.WxV3Pay;
+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 java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 微信相关接口
+ */
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/wx/")
+@Api(tags = {"微信支付相关接口"})
+public class WxPayController {
+ @Autowired
+ private WxV3Pay wxV3Pay;
+
+
+ /**
+ * 按实际修改
+ */
+ @PostMapping("order")
+ @ApiOperation("订单支付")
+ public AjaxResult<Map<String, Object>> orderPay(@RequestParam Long orderId) {
+ // 查询订单
+ // 0元订单不走支付
+ // 价格
+ Integer totalPrice = 0;
+ // 生成订单号
+ String orderNo = "";
+ // 查询用户信息 用户openid
+ String openId = "";
+ // 订单做修改
+ // 调用支付方法
+ Map<String, Object> result = wxV3Pay.jsApi(orderNo, totalPrice, openId,"");
+ log.info("支付参数:{}", result);
+ return AjaxResult.ok(result);
+ }
+
+ /**
+ * 微信v3支付-订单退款
+ *
+ * @return
+ */
+ @ApiOperation("订单退款")
+ @PostMapping(value = "refund-order")
+ public AjaxResult<String> refundOrder() {
+ Map<String, Object> result = wxV3Pay.refund(new WxPaymentRefundModel());
+ log.info("退款结果:{}", result);
+ // 微信支付退款单号
+ String refund_id = result.get("refund_id").toString();
+ // 商户退款单号
+ String out_refund_no = result.get("out_refund_no").toString();
+ // 微信支付订单号
+ String transaction_id = result.get("transaction_id").toString();
+ // 商户订单号 tradeNo
+ String out_trade_no = result.get("out_trade_no").toString();
+ // 退款成功时间
+ String success_time = Objects.nonNull(result.get("success_time")) ? result.get("success_time").toString() : null;
+ // 退款状态 RefundEnum
+ String status = result.get("status").toString();
+ // TODO 退款业务处理
+ return AjaxResult.success();
+ }
+
+ /**
+ * 支付回调
+ */
+ @PostMapping("pay/notify")
+ public void payNotify(HttpServletRequest request) throws IOException {
+ try {
+ Map<String, Object> params = wxV3Pay.verifyNotify(request, new TypeReference<Map<String, Object>>() {
+ });
+ log.info("支付回调:{}", params);
+ // 商户订单号
+ String tradeNo = params.get("out_trade_no").toString();
+ // 交易状态
+ String trade_state = params.get("trade_state").toString();
+ // 交易状态描述
+ String trade_state_desc = params.get("trade_state_desc").toString();
+ // 微信支付订单号
+ String transaction_id = params.get("transaction_id").toString();
+ // 支付完成时间
+ // 时间不对的话,可以调用 WxTimeUtils.toRfc3339Date(success_time)转换一下
+ String success_time = params.get("success_time").toString();
+ // 附加数据
+ Integer attach = Integer.parseInt(params.get("attach").toString());
+ // TODO 业务处理
+ } catch (Exception e) {
+ log.error("支付回调异常:{}", e, e);
+ wxV3Pay.ack(false, e.getMessage());
+ }
+
+ }
+
+ /**
+ * 退款回调
+ */
+ @PostMapping("refund/notify")
+ public void refundNotify(HttpServletRequest request) throws IOException {
+ try {
+ Map<String, Object> params = wxV3Pay.verifyNotify(request, new TypeReference<Map<String, Object>>() {
+ });
+ // 商户订单号
+ String tradeNo = params.get("out_trade_no").toString();
+ // 商户退款单号
+ String out_refund_no = params.get("out_refund_no").toString();
+ // 微信支付订单号
+ String transaction_id = params.get("transaction_id").toString();
+ // 微信支付退款单号
+ String refund_id = params.get("refund_id").toString();
+ // 退款状态
+ String tradeState = params.get("refund_status").toString();
+ // 退款成功时间
+ // 时间不对的话,可以调用 WxTimeUtils.toRfc3339Date(success_time)转换一下
+ String success_time = params.get("success_time").toString();
+ if (tradeState.equals(RefundEnum.SUCCESS.name())) {
+ // TODO 退款成功处理
+ wxV3Pay.ack();
+ } else {
+ wxV3Pay.ack(false, "不是成功的退款状态");
+ }
+ } catch (Exception e) {
+ wxV3Pay.ack(false, e.getMessage());
+ }
+
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/RefundEnum.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/RefundEnum.java
new file mode 100644
index 0000000..fc5eabf
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/RefundEnum.java
@@ -0,0 +1,53 @@
+package com.ruoyi.payment.wx.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-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/TradeStateEnum.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/TradeStateEnum.java
new file mode 100644
index 0000000..413b434
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/TradeStateEnum.java
@@ -0,0 +1,41 @@
+package com.ruoyi.payment.wx.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-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/exception/WxException.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/exception/WxException.java
new file mode 100644
index 0000000..0367db9
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/exception/WxException.java
@@ -0,0 +1,55 @@
+package com.ruoyi.payment.wx.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-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/V3.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/V3.java
new file mode 100644
index 0000000..71eb21c
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/V3.java
@@ -0,0 +1,71 @@
+package com.ruoyi.payment.wx.model;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.util.IOUtils;
+
+import java.io.ByteArrayInputStream;
+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;
+ /**
+ * 商户证书序列号
+ */
+ 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);
+ try {
+ certData = IOUtils.toByteArray(certStream);
+ } catch (IOException e) {
+ throw new RuntimeException("私钥文件未找到");
+ }finally {
+ try {
+ certStream.close();
+ } catch (IOException e) {
+ log.error("私钥流关闭异常");
+ }
+ }
+ return new ByteArrayInputStream(certData);
+ }
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WeixinProperties.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WeixinProperties.java
new file mode 100644
index 0000000..5c1960b
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WeixinProperties.java
@@ -0,0 +1,101 @@
+package com.ruoyi.payment.wx.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 = "wx.conf")
+public class WeixinProperties {
+ /**
+ * 默认开启
+ */
+ 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;
+
+ 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;
+ }
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentInfoModel.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentInfoModel.java
new file mode 100644
index 0000000..a3c4b03
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentInfoModel.java
@@ -0,0 +1,201 @@
+package com.ruoyi.payment.wx.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-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentRefundModel.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentRefundModel.java
new file mode 100644
index 0000000..2920b93
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentRefundModel.java
@@ -0,0 +1,84 @@
+package com.ruoyi.payment.wx.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-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/AppletUserDecodeData.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/AppletUserDecodeData.java
new file mode 100644
index 0000000..f805118
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/AppletUserDecodeData.java
@@ -0,0 +1,52 @@
+package com.ruoyi.payment.wx.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-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/Watermark.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/Watermark.java
new file mode 100644
index 0000000..8044644
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/Watermark.java
@@ -0,0 +1,9 @@
+package com.ruoyi.payment.wx.pojo;
+
+import lombok.Data;
+
+@Data
+public class Watermark {
+ private String appid;
+ private String timestamp;
+}
\ No newline at end of file
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/resp/NotifyV3PayDecodeRespBody.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/resp/NotifyV3PayDecodeRespBody.java
new file mode 100644
index 0000000..94ef56e
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/resp/NotifyV3PayDecodeRespBody.java
@@ -0,0 +1,222 @@
+package com.ruoyi.payment.wx.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-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/SHA1.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/SHA1.java
new file mode 100644
index 0000000..2b8c9d9
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/SHA1.java
@@ -0,0 +1,39 @@
+package com.ruoyi.payment.wx.utils;
+
+
+import com.ruoyi.payment.wx.exception.WxException;
+
+import java.security.MessageDigest;
+
+public class SHA1 {
+
+
+ /**
+ * 用SHA1算法生成安全签名
+ *
+ * @param str
+ * @return
+ * @throws WxException
+ */
+ public static String getSHA1(String str) throws WxException {
+ try {
+ // SHA1签名生成
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ md.update(str.getBytes());
+ byte[] digest = md.digest();
+ StringBuffer hexstr = new StringBuffer();
+ String shaHex;
+ for (int i = 0; i < digest.length; i++) {
+ shaHex = Integer.toHexString(digest[i] & 0xFF);
+ if (shaHex.length() < 2) {
+ hexstr.append(0);
+ }
+ hexstr.append(shaHex);
+ }
+ return hexstr.toString();
+ } catch (Exception e) {
+ throw new WxException(WxException.ComputeSignatureError);
+ }
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxAbstractPay.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxAbstractPay.java
new file mode 100644
index 0000000..a17e3bd
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxAbstractPay.java
@@ -0,0 +1,333 @@
+package com.ruoyi.payment.wx.utils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.ruoyi.common.core.utils.WebUtils;
+import com.ruoyi.payment.wx.model.WxPaymentInfoModel;
+import com.ruoyi.payment.wx.model.WxPaymentRefundModel;
+import com.ruoyi.payment.wx.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);
+ 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);
+ String repBody = result(httpClient, httpGet);
+ NotifyV3PayDecodeRespBody body = WxJsonUtils.parseObject(repBody, NotifyV3PayDecodeRespBody.class);
+ return body;
+ }
+
+ /**
+ * 子级实现
+ *
+ * @param out_trade_no
+ * @param mchid
+ * @return
+ */
+ public abstract NotifyV3PayDecodeRespBody query(String out_trade_no, String mchid);
+
+
+ /**
+ * 订单退款
+ *
+ * @param refundModel
+ * @return
+ */
+ public abstract Map<String, Object> refund(WxPaymentRefundModel refundModel);
+
+ /**
+ * 订单退款
+ *
+ * @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();
+ }
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxJsonUtils.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxJsonUtils.java
new file mode 100644
index 0000000..648525d
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxJsonUtils.java
@@ -0,0 +1,73 @@
+package com.ruoyi.payment.wx.utils;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Json转换工具类
+ *
+ * @author madman
+ */
+@Slf4j
+public final class WxJsonUtils {
+ private static final ObjectMapper OM = new ObjectMapper();
+ private static final JavaTimeModule timeModule = new JavaTimeModule();
+
+
+ /**
+ * 设置 ObjectMapper
+ *
+ * @return
+ */
+ private static ObjectMapper getObjectMapper() {
+ // 允许对象忽略json中不存在的属性
+ OM.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ OM.registerModule(timeModule);
+ return OM;
+ }
+
+ /**
+ * 将对象序列化
+ */
+ public static <T> String toJsonString(T obj) {
+ try {
+ ObjectMapper om = getObjectMapper();
+ // 忽略空值
+ om.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
+ return om.writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ log.error("转json字符串失败:{}", obj);
+ return null;
+ }
+ }
+
+ /**
+ * 反序列化对象字符串
+ */
+ public static <T> T parseObject(String json, Class<T> clazz) {
+ try {
+ ObjectMapper om = getObjectMapper();
+ return om.readValue(json, clazz);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("反序列化对象字符串失败");
+ }
+ }
+
+ /**
+ * 反序列化字符串成为对象
+ */
+ public static <T> T parseObject(String json, TypeReference<T> valueTypeRef) {
+ try {
+ ObjectMapper om = getObjectMapper();
+ return om.readValue(json, valueTypeRef);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("反序列化字符串成为对象失败");
+ }
+ }
+
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxTimeUtils.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxTimeUtils.java
new file mode 100644
index 0000000..07476c1
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxTimeUtils.java
@@ -0,0 +1,164 @@
+package com.ruoyi.payment.wx.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-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxUtils.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxUtils.java
new file mode 100644
index 0000000..ed04671
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxUtils.java
@@ -0,0 +1,217 @@
+package com.ruoyi.payment.wx.utils;
+
+import com.ruoyi.payment.wx.pojo.AppletUserDecodeData;
+import lombok.extern.slf4j.Slf4j;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.util.encoders.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.InputStreamReader;
+import java.security.AlgorithmParameters;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * @Description 获取用户信息工具类
+ * @Author xiaochen
+ * @Date 2021/8/12 15:45
+ */
+@Slf4j
+public class WxUtils {
+ /**
+ * 随机字符
+ */
+ private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ private static final Random RANDOM = new SecureRandom();
+
+ /**
+ * 微信小程序API 用户数据的解密
+ * @param encryptedData
+ * @param sessionKey
+ * @param iv
+ * @return
+ */
+ public static AppletUserDecodeData encryptedData(String encryptedData, String sessionKey, String iv) {
+ // 被加密的数据
+ byte[] dataByte = Base64.decode(encryptedData);
+ // 加密秘钥
+ byte[] keyByte = Base64.decode(sessionKey);
+ // 偏移量
+ byte[] ivByte = Base64.decode(iv);
+ try {
+ // 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
+ int base = 16;
+ if (keyByte.length % base != 0) {
+ int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
+ byte[] temp = new byte[groups * base];
+ Arrays.fill(temp, (byte) 0);
+ System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
+ keyByte = temp;
+ }
+ // 初始化
+ Security.addProvider(new BouncyCastleProvider());
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
+ SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
+ AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
+ parameters.init(new IvParameterSpec(ivByte));
+ cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
+ byte[] resultByte = cipher.doFinal(dataByte);
+ if (null != resultByte && resultByte.length > 0) {
+ String result = new String(resultByte, "UTF-8");
+ log.info("解密原串:{}",result);
+ return WxJsonUtils.parseObject(result, AppletUserDecodeData.class);
+ }
+ throw new RuntimeException("解密的数据为空");
+ } catch (Exception e) {
+ log.error("解密失败. error = {}", e.getMessage(), e);
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+
+ /**
+ * 微信小程序API 用户数据的签名验证
+ * signature = sha1( rawData + session_key )
+ *
+ * @param rawData 不包括敏感信息的原始数据字符串,用于计算签名。
+ * @param sessionKey
+ */
+ public static void verifySignature(String rawData, String sessionKey, String signature) {
+ String serverSignature = SHA1.getSHA1(rawData + sessionKey);
+ System.out.println(rawData + sessionKey);
+ log.info(rawData + ">>>>>>:" + sessionKey + " === " + serverSignature + " ======" + signature);
+ if (!signature.equals(serverSignature)) {
+ throw new RuntimeException("数据验签不通过");
+ }
+ }
+
+ /**
+ * 根据流接收请求数据
+ *
+ * @param request
+ * @return
+ */
+ public static String streamBodyByReceive(HttpServletRequest request) throws IOException {
+ 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());
+ }
+ 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);
+ }
+
+ /**
+ * 获取当前时间戳,单位秒
+ *
+ * @return
+ */
+ public static long getCurrentTimestamp() {
+ return System.currentTimeMillis() / 1000;
+ }
+
+ /**
+ * 获取当前时间戳,单位毫秒
+ *
+ * @return
+ */
+ public static long getCurrentTimestampMs() {
+ return System.currentTimeMillis();
+ }
+
+
+ /**
+ * 日志
+ *
+ * @return
+ */
+ public static Logger getLogger() {
+ Logger logger = LoggerFactory.getLogger("wxpay java sdk --->");
+ return logger;
+ }
+
+ /**
+ * debug
+ *
+ * @param msg
+ * @param args
+ */
+ public static void debug(String msg, Object... args) {
+ Logger log = getLogger();
+ if (log.isDebugEnabled()) {
+ log.debug(msg, args);
+ }
+ }
+
+ /**
+ * info
+ *
+ * @param msg
+ * @param args
+ */
+ public static void info(String msg, Object... args) {
+ Logger log = getLogger();
+ if (log.isInfoEnabled()) {
+ log.info(msg, args);
+ }
+ }
+
+ /**
+ * warn
+ *
+ * @param msg
+ * @param args
+ */
+ public static void warn(String msg, Object... args) {
+ Logger log = getLogger();
+ if (log.isWarnEnabled()) {
+ log.warn(msg, args);
+ }
+ }
+
+ /**
+ * error
+ *
+ * @param msg
+ * @param args
+ */
+ public static void error(String msg, Object... args) {
+ Logger log = getLogger();
+ if (log.isErrorEnabled()) {
+ log.error(msg, args);
+ }
+ }
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxV3Pay.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxV3Pay.java
new file mode 100644
index 0000000..b76bbfd
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxV3Pay.java
@@ -0,0 +1,197 @@
+package com.ruoyi.payment.wx.utils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.payment.wx.model.WeixinProperties;
+import com.ruoyi.payment.wx.model.WxPaymentInfoModel;
+import com.ruoyi.payment.wx.model.WxPaymentRefundModel;
+import com.ruoyi.payment.wx.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.WechatPay2Credentials;
+import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
+import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
+import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import javax.servlet.http.HttpServletRequest;
+import java.nio.charset.StandardCharsets;
+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 WeixinProperties 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(WeixinProperties 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);
+ // 获取证书管理器实例
+ CertificatesManager certificatesManager = CertificatesManager.getInstance();
+ // 向证书管理器增加需要自动更新平台证书的商户信息
+ certificatesManager.putMerchant(config.getMchId(), new WechatPay2Credentials(config.getMchId(),
+ this.privateKeySigner), config.getV3().getApiKey().getBytes(StandardCharsets.UTF_8));
+ // 从证书管理器中获取verifier
+ this.verifier = certificatesManager.getVerifier(config.getMchId());
+ 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, 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())
+ // 分不分账
+ //.settle_info(WxPaymentInfoModel.SettleInfo.builder().profit_sharing(true).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
+ * @param mchid
+ * @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 mchid) {
+ String url =
+ String.format("/v3/pay/transactions/out-trade-no/%s", out_trade_no) + String.format("?mchid=%s", mchid);
+ 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());
+ return refund(this.httpClient, "/v3/refund/domestic/refunds", this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), refundModel);
+ }
+
+}
--
Gitblit v1.7.1