diff --git a/pom.xml b/pom.xml
index fde84309..fada46eb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -337,11 +337,11 @@
${revision}
-
-
-
-
-
+
+ org.ruoyi
+ ruoyi-wechat
+ ${revision}
+
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 1cee87a6..8ae7ca7c 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -57,6 +57,10 @@
ruoyi-generator
+
+ org.ruoyi
+ ruoyi-wechat
+
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/SysLoginService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/SysLoginService.java
index d2558b62..af88f619 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/SysLoginService.java
+++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/SysLoginService.java
@@ -19,12 +19,10 @@ import org.ruoyi.common.core.constant.TenantConstants;
import org.ruoyi.common.core.domain.dto.RoleDTO;
import org.ruoyi.common.core.domain.model.LoginUser;
import org.ruoyi.common.core.domain.model.VisitorLoginBody;
-import org.ruoyi.common.core.domain.model.VisitorLoginUser;
import org.ruoyi.common.core.enums.*;
import org.ruoyi.common.core.exception.user.CaptchaException;
import org.ruoyi.common.core.exception.user.CaptchaExpireException;
import org.ruoyi.common.core.exception.user.UserException;
-import org.ruoyi.common.core.service.ConfigService;
import org.ruoyi.common.core.utils.*;
import org.ruoyi.common.log.event.LogininforEvent;
import org.ruoyi.common.redis.utils.RedisUtils;
@@ -32,8 +30,7 @@ import org.ruoyi.common.satoken.utils.LoginHelper;
import org.ruoyi.common.tenant.exception.TenantException;
import org.ruoyi.common.tenant.helper.TenantHelper;
import org.ruoyi.system.domain.SysUser;
-import org.ruoyi.system.domain.bo.SysUserBo;
-import org.ruoyi.system.domain.vo.LoginVo;
+
import org.ruoyi.system.domain.vo.SysTenantVo;
import org.ruoyi.system.domain.vo.SysUserVo;
import org.ruoyi.system.mapper.SysUserMapper;
@@ -43,7 +40,6 @@ import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Date;
import java.util.List;
-import java.util.UUID;
import java.util.function.Supplier;
/**
@@ -60,8 +56,6 @@ public class SysLoginService {
private final WxMaService wxMaService;
private final ISysPermissionService permissionService;
private final ISysTenantService tenantService;
- private final ISysUserService userService;
- private final ConfigService configService;
@Value("${user.password.maxRetryCount}")
private Integer maxRetryCount;
@@ -69,7 +63,7 @@ public class SysLoginService {
private Integer lockTime;
/**
- * 获取微信
+ * 获取微信code
* @param xcxCode 获取xcxCode
*/
public String getOpenidFromCode(String xcxCode) {
@@ -137,9 +131,8 @@ public class SysLoginService {
return StpUtil.getTokenValue();
}
-
/**
- * 游客登录
+ * 微信小程序登录
*
* @param loginBody
* @return String
@@ -164,57 +157,6 @@ public class SysLoginService {
}
}
- public LoginVo mpLogin(String openid) {
- // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
- SysUserVo user = userService.selectUserByOpenId(openid);
- VisitorLoginUser loginUser = new VisitorLoginUser();
- if (ObjectUtil.isNull(user)) {
- SysUserBo sysUser = new SysUserBo();
- // 改为自增
- String name = "用户" + UUID.randomUUID().toString().replace("-", "");
- // 设置默认用户名
- sysUser.setUserName(name);
- // 设置默认昵称
- sysUser.setNickName(name);
- // 设置默认密码
- sysUser.setPassword(BCrypt.hashpw("123456"));
- // 设置微信openId
- sysUser.setOpenId(openid);
- String configValue = configService.getConfigValue("mail", "amount");
- // 设置默认余额
- sysUser.setUserBalance(NumberUtils.toDouble(configValue, 1));
- // 注册用户,设置默认租户为0
- SysUser registerUser = userService.registerUser(sysUser, "0");
-
- // 构建登录用户信息
- loginUser.setTenantId("0");
- loginUser.setUserId(registerUser.getUserId());
- loginUser.setUsername(registerUser.getUserName());
- loginUser.setUserType(UserType.APP_USER.getUserType());
- loginUser.setOpenid(openid);
- loginUser.setNickName(registerUser.getNickName());
-
- } else {
- // 此处可根据登录用户的数据不同 自行创建 loginUser
- loginUser.setTenantId(user.getTenantId());
- loginUser.setUserId(user.getUserId());
- loginUser.setUsername(user.getUserName());
- loginUser.setUserType(user.getUserType());
- loginUser.setNickName(user.getNickName());
- loginUser.setAvatar(user.getWxAvatar());
- loginUser.setOpenid(openid);
- }
- // 生成token
- LoginHelper.loginByDevice(loginUser, DeviceType.XCX);
- recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
- LoginVo loginVo = new LoginVo();
- // 生成令牌
- loginVo.setToken(StpUtil.getTokenValue());
- loginVo.setUserInfo(loginUser);
- return loginVo;
- }
-
-
/**
* 退出登录
*/
diff --git a/ruoyi-modules/ruoyi-wechat/pom.xml b/ruoyi-modules/ruoyi-wechat/pom.xml
index 8a716654..7e3cdfe3 100644
--- a/ruoyi-modules/ruoyi-wechat/pom.xml
+++ b/ruoyi-modules/ruoyi-wechat/pom.xml
@@ -23,6 +23,10 @@
org.ruoyi
ruoyi-chat-api
+
+ org.ruoyi
+ ruoyi-system-api
+
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpProperties.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpProperties.java
index 51077d61..05884475 100644
--- a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpProperties.java
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpProperties.java
@@ -3,12 +3,15 @@ package org.ruoyi.config;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
import java.util.List;
/**
* @author Binary Wang
*/
@Data
+@ConfigurationProperties(prefix = "wechat.cp")
public class WxCpProperties {
/**
* 设置企业微信的corpId
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinServerController.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinServerController.java
new file mode 100644
index 00000000..86767af9
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinServerController.java
@@ -0,0 +1,48 @@
+package org.ruoyi.controller;
+
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.ruoyi.service.WeixinUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author https://www.wdbyte.com
+ */
+@Slf4j
+@RestController
+public class WeixinServerController {
+
+ @Autowired
+ private WeixinUserService weixinUserService;
+
+ @GetMapping(value = "/weixin/check")
+ public String weixinCheck(HttpServletRequest request) {
+ String signature = request.getParameter("signature");
+ String timestamp = request.getParameter("timestamp");
+ String nonce = request.getParameter("nonce");
+ String echostr = request.getParameter("echostr");
+
+ if (StringUtils.isEmpty(signature) || StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(nonce)) {
+ return "";
+ }
+ weixinUserService.checkSignature(signature, timestamp, nonce);
+ return echostr;
+ }
+
+ @PostMapping(value = "/weixin/check")
+ public String weixinMsg(@RequestBody String requestBody, @RequestParam("signature") String signature,
+ @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce) {
+
+ log.debug("requestBody:{}", requestBody);
+ log.debug("signature:{}", signature);
+ log.debug("timestamp:{}", timestamp);
+ log.debug("nonce:{}", nonce);
+
+ weixinUserService.checkSignature(signature, timestamp, nonce);
+ return weixinUserService.handleWeixinMsg(requestBody);
+ }
+
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinUserController.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinUserController.java
new file mode 100644
index 00000000..777c1807
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinUserController.java
@@ -0,0 +1,52 @@
+package org.ruoyi.controller;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.ruoyi.common.core.domain.R;
+import org.ruoyi.domin.WeixinQrCode;
+import org.ruoyi.service.VxLoginService;
+import org.ruoyi.system.domain.vo.LoginVo;
+import org.ruoyi.util.WeixinApiUtil;
+import org.ruoyi.util.WeixinQrCodeCacheUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author https://www.wdbyte.com
+ */
+@Slf4j
+@RestController
+public class WeixinUserController {
+
+ @Autowired
+ private WeixinApiUtil weixinApiUtil;
+
+ @Autowired
+ private VxLoginService loginService;
+
+ @GetMapping(value = "/user/qrcode")
+ public R getQrCode() {
+ WeixinQrCode qrCode = weixinApiUtil.getQrCode();
+ qrCode.setUrl(null);
+ qrCode.setExpireSeconds(null);
+ return R.ok(qrCode);
+ }
+
+ /**
+ * 校验是否扫描完成
+ * 完成,返回 JWT
+ * 未完成,返回 check failed
+ */
+ @GetMapping(value = "/user/login/qrcode")
+ public R userLogin(String ticket) {
+ String openId = WeixinQrCodeCacheUtil.get(ticket);
+ if (StringUtils.isNotEmpty(openId)) {
+ log.info("login success,open id:{}", openId);
+ LoginVo loginVo = loginService.mpLogin(openId);
+ return R.ok(loginVo);
+ }
+ log.info("login error,ticket:{}", ticket);
+ return R.fail("check failed");
+ }
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/wxcplogin/WxPortalController.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WxPortalController.java
similarity index 98%
rename from ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/wxcplogin/WxPortalController.java
rename to ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WxPortalController.java
index 2857bc2f..aba5896e 100644
--- a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/wxcplogin/WxPortalController.java
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WxPortalController.java
@@ -1,4 +1,4 @@
-package org.ruoyi.controller.wxcplogin;
+package org.ruoyi.controller;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.cp.api.WxCpService;
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/ReceiveMessage.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/ReceiveMessage.java
new file mode 100644
index 00000000..9bc6dcb9
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/ReceiveMessage.java
@@ -0,0 +1,58 @@
+package org.ruoyi.domin;
+
+import lombok.Data;
+
+@Data
+public class ReceiveMessage {
+ /**
+ * 开发者微信号
+ */
+ private String toUserName;
+ /**
+ * 发送方账号(一个openid)
+ */
+ private String fromUserName;
+ /**
+ * 消息创建时间(整形)
+ */
+ private String createTime;
+ /**
+ * 消息类型
+ */
+ private String msgType;
+ /**
+ * 文本消息内容
+ */
+ private String content;
+ /**
+ * 消息ID 64位
+ */
+ String msgId;
+ /**
+ * 消息的数据ID 消息来自文章才有
+ */
+ private String msgDataId;
+ /**
+ * 多图文时第几篇文章,从1开始 消息如果来自文章才有
+ */
+ private String idx;
+ /**
+ * 订阅事件 subscribe 订阅 unsbscribe 取消订阅
+ */
+ private String event;
+ /**
+ * 扫码 - ticket
+ */
+ private String ticket;
+
+ public String getReplyTextMsg(String msg) {
+ String xml = "\n"
+ + " \n"
+ + " \n"
+ + " " + System.currentTimeMillis() + "\n"
+ + " \n"
+ + " \n"
+ + " ";
+ return xml;
+ }
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/WeixinQrCode.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/WeixinQrCode.java
new file mode 100644
index 00000000..0363b3bf
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/WeixinQrCode.java
@@ -0,0 +1,15 @@
+package org.ruoyi.domin;
+
+import lombok.Data;
+
+/**
+ * @author https://www.wdbyte.com
+ */
+@Data
+public class WeixinQrCode {
+
+ private String ticket;
+ private Long expireSeconds;
+ private String url;
+ private String qrCodeUrl;
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/VxLoginService.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/VxLoginService.java
new file mode 100644
index 00000000..31e6876b
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/VxLoginService.java
@@ -0,0 +1,108 @@
+package org.ruoyi.service;
+
+import cn.dev33.satoken.secure.BCrypt;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.ruoyi.common.core.constant.Constants;
+import org.ruoyi.common.core.domain.model.VisitorLoginUser;
+import org.ruoyi.common.core.enums.DeviceType;
+import org.ruoyi.common.core.enums.UserType;
+import org.ruoyi.common.core.service.ConfigService;
+import org.ruoyi.common.core.utils.MessageUtils;
+import org.ruoyi.common.core.utils.ServletUtils;
+import org.ruoyi.common.core.utils.SpringUtils;
+import org.ruoyi.common.log.event.LogininforEvent;
+import org.ruoyi.common.satoken.utils.LoginHelper;
+import org.ruoyi.system.domain.SysUser;
+import org.ruoyi.system.domain.bo.SysUserBo;
+import org.ruoyi.system.domain.vo.LoginVo;
+import org.ruoyi.system.domain.vo.SysUserVo;
+import org.ruoyi.system.service.ISysUserService;
+import org.springframework.stereotype.Service;
+
+import java.util.UUID;
+
+/**
+ * 描述:微信公众号登录
+ *
+ * @author ageerle@163.com
+ * date 2025/4/30
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class VxLoginService {
+
+ private final ISysUserService userService;
+
+ private final ConfigService configService;
+
+ public LoginVo mpLogin(String openid) {
+ // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
+ SysUserVo user = userService.selectUserByOpenId(openid);
+ VisitorLoginUser loginUser = new VisitorLoginUser();
+ if (ObjectUtil.isNull(user)) {
+ SysUserBo sysUser = new SysUserBo();
+ String name = "用户" + UUID.randomUUID().toString().replace("-", "");
+ // 设置默认用户名
+ sysUser.setUserName(name);
+ // 设置默认昵称
+ sysUser.setNickName(name);
+ // 设置默认密码
+ sysUser.setPassword(BCrypt.hashpw("123456"));
+ // 设置微信openId
+ sysUser.setOpenId(openid);
+ String configValue = configService.getConfigValue("mail", "amount");
+ // 设置默认余额
+ sysUser.setUserBalance(NumberUtils.toDouble(configValue, 1));
+ // 注册用户,设置默认租户为0
+ SysUser registerUser = userService.registerUser(sysUser, "0");
+
+ // 构建登录用户信息
+ loginUser.setTenantId("0");
+ loginUser.setUserId(registerUser.getUserId());
+ loginUser.setUsername(registerUser.getUserName());
+ loginUser.setUserType(UserType.APP_USER.getUserType());
+ loginUser.setOpenid(openid);
+ loginUser.setNickName(registerUser.getNickName());
+ } else {
+ // 此处可根据登录用户的数据不同 自行创建 loginUser
+ loginUser.setTenantId(user.getTenantId());
+ loginUser.setUserId(user.getUserId());
+ loginUser.setUsername(user.getUserName());
+ loginUser.setUserType(user.getUserType());
+ loginUser.setNickName(user.getNickName());
+ loginUser.setAvatar(user.getWxAvatar());
+ loginUser.setOpenid(openid);
+ }
+ // 生成token
+ LoginHelper.loginByDevice(loginUser, DeviceType.XCX);
+ recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
+ LoginVo loginVo = new LoginVo();
+ // 生成令牌
+ loginVo.setToken(StpUtil.getTokenValue());
+ loginVo.setUserInfo(loginUser);
+ return loginVo;
+ }
+
+ /**
+ * 记录登录信息
+ *
+ * @param tenantId 租户ID
+ * @param username 用户名
+ * @param status 状态
+ * @param message 消息内容
+ */
+ private void recordLogininfor(String tenantId, String username, String status, String message) {
+ LogininforEvent logininforEvent = new LogininforEvent();
+ logininforEvent.setTenantId(tenantId);
+ logininforEvent.setUsername(username);
+ logininforEvent.setStatus(status);
+ logininforEvent.setMessage(message);
+ logininforEvent.setRequest(ServletUtils.getRequest());
+ SpringUtils.context().publishEvent(logininforEvent);
+ }
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/WeixinUserService.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/WeixinUserService.java
new file mode 100644
index 00000000..c9996791
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/WeixinUserService.java
@@ -0,0 +1,10 @@
+package org.ruoyi.service;
+
+
+public interface WeixinUserService {
+
+ void checkSignature(String signature, String timestamp, String nonce);
+
+ String handleWeixinMsg(String body);
+
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/impl/WeixinUserServiceImpl.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/impl/WeixinUserServiceImpl.java
new file mode 100644
index 00000000..57fcdfb7
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/impl/WeixinUserServiceImpl.java
@@ -0,0 +1,65 @@
+package org.ruoyi.service.impl;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.ruoyi.domin.ReceiveMessage;
+import org.ruoyi.service.WeixinUserService;
+import org.ruoyi.util.WeixinMsgUtil;
+import org.ruoyi.util.WeixinQrCodeCacheUtil;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+
+
+@Slf4j
+@Service
+public class WeixinUserServiceImpl implements WeixinUserService {
+
+ private String token = "panda";
+
+ @Override
+ public void checkSignature(String signature, String timestamp, String nonce) {
+ String[] arr = new String[] {token, timestamp, nonce};
+ Arrays.sort(arr);
+ StringBuilder content = new StringBuilder();
+ for (String str : arr) {
+ content.append(str);
+ }
+ String tmpStr = DigestUtils.sha1Hex(content.toString());
+ if (tmpStr.equals(signature)) {
+ log.info("check success");
+ return;
+ }
+ log.error("check fail");
+ throw new RuntimeException("check fail");
+ }
+
+ @Override
+ public String handleWeixinMsg(String requestBody) {
+ ReceiveMessage receiveMessage = WeixinMsgUtil.msgToReceiveMessage(requestBody);
+ // 扫码登录
+ if (WeixinMsgUtil.isScanQrCode(receiveMessage)) {
+ return handleScanLogin(receiveMessage);
+ }
+ // 关注
+ if (WeixinMsgUtil.isEventAndSubscribe(receiveMessage)) {
+ return receiveMessage.getReplyTextMsg("感谢您的关注!");
+ }
+ return receiveMessage.getReplyTextMsg("收到(自动回复)");
+ }
+
+ /**
+ * 处理扫码登录
+ *
+ * @param receiveMessage
+ * @return
+ */
+ private String handleScanLogin(ReceiveMessage receiveMessage) {
+ String qrCodeTicket = WeixinMsgUtil.getQrCodeTicket(receiveMessage);
+ if (WeixinQrCodeCacheUtil.get(qrCodeTicket) == null) {
+ String openId = receiveMessage.getFromUserName();
+ WeixinQrCodeCacheUtil.put(qrCodeTicket, openId);
+ }
+ return receiveMessage.getReplyTextMsg("你已成功登录!");
+ }
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/KeyUtils.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/KeyUtils.java
new file mode 100644
index 00000000..6bbf917f
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/KeyUtils.java
@@ -0,0 +1,24 @@
+package org.ruoyi.util;
+
+import org.apache.commons.lang3.RandomStringUtils;
+
+import java.util.UUID;
+
+/**
+ * @author https://www.wdbyte.com
+ */
+public class KeyUtils {
+
+ public synchronized static String key6() {
+ return RandomStringUtils.randomAlphanumeric(6);
+ }
+
+ public synchronized static String key16() {
+ return RandomStringUtils.randomAlphanumeric(16);
+ }
+
+ public static String uuid32() {
+ return UUID.randomUUID().toString().replace("-", "");
+ }
+
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java
new file mode 100644
index 00000000..1aa9c395
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java
@@ -0,0 +1,81 @@
+package org.ruoyi.util;
+
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.ruoyi.common.core.service.ConfigService;
+import org.ruoyi.domin.WeixinQrCode;
+import org.springframework.stereotype.Component;
+
+import java.net.URI;
+import java.time.LocalDateTime;
+
+/**
+ * @author https://www.wdbyte.com
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class WeixinApiUtil {
+
+ private final ConfigService configService;
+
+ private static String QR_CODE_URL_PREFIX = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";
+
+ private static String ACCESS_TOKEN = null;
+ private static LocalDateTime ACCESS_TOKEN_EXPIRE_TIME = null;
+ /**
+ * 二维码 Ticket 过期时间
+ */
+ private static int QR_CODE_TICKET_TIMEOUT = 10 * 60;
+
+ /**
+ * 获取 access token
+ *
+ * @return
+ */
+ public synchronized String getAccessToken() {
+ if (ACCESS_TOKEN != null && ACCESS_TOKEN_EXPIRE_TIME.isAfter(LocalDateTime.now())) {
+ return ACCESS_TOKEN;
+ }
+ String api = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + getKey("appid") + "&secret="
+ + getKey("secret");
+ String result = HttpUtil.get(api);
+ JSONObject jsonObject = JSON.parseObject(result);
+ ACCESS_TOKEN = jsonObject.getString("access_token");
+ ACCESS_TOKEN_EXPIRE_TIME = LocalDateTime.now().plusSeconds(jsonObject.getLong("expires_in") - 10);
+ return ACCESS_TOKEN;
+ }
+
+ /**
+ * 获取二维码 Ticket
+ *
+ * https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
+ *
+ * @return
+ */
+ public WeixinQrCode getQrCode() {
+ String api = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + getAccessToken();
+ String jsonBody = String.format("{\n"
+ + " \"expire_seconds\": %d,\n"
+ + " \"action_name\": \"QR_STR_SCENE\",\n"
+ + " \"action_info\": {\n"
+ + " \"scene\": {\n"
+ + " \"scene_str\": \"%s\"\n"
+ + " }\n"
+ + " }\n"
+ + "}", QR_CODE_TICKET_TIMEOUT, KeyUtils.uuid32());
+ String result = HttpUtil.post(api, jsonBody);
+ log.info("get qr code params:{}", jsonBody);
+ log.info("get qr code result:{}", result);
+ WeixinQrCode weixinQrCode = JSON.parseObject(result, WeixinQrCode.class);
+ weixinQrCode.setQrCodeUrl(QR_CODE_URL_PREFIX + URI.create(weixinQrCode.getTicket()).toASCIIString());
+ return weixinQrCode;
+ }
+
+ public String getKey(String key) {
+ return configService.getConfigValue("weixin", key);
+ }
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinMsgUtil.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinMsgUtil.java
new file mode 100644
index 00000000..e1fd520c
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinMsgUtil.java
@@ -0,0 +1,66 @@
+package org.ruoyi.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import org.apache.commons.lang3.StringUtils;
+import org.ruoyi.domin.ReceiveMessage;
+
+/**
+ * @author https://www.wdbyte.com
+ */
+public class WeixinMsgUtil {
+
+ // 事件-关注
+ private static String EVENT_SUBSCRIBE = "subscribe";
+
+ /**
+ * 微信消息转对象
+ *
+ * @param xml
+ * @return
+ */
+ public static ReceiveMessage msgToReceiveMessage(String xml) {
+ JSONObject jsonObject = JSON.parseObject(XmlUtil.xml2json(xml));
+ ReceiveMessage receiveMessage = new ReceiveMessage();
+ receiveMessage.setToUserName(jsonObject.getString("ToUserName"));
+ receiveMessage.setFromUserName(jsonObject.getString("FromUserName"));
+ receiveMessage.setCreateTime(jsonObject.getString("CreateTime"));
+ receiveMessage.setMsgType(jsonObject.getString("MsgType"));
+ receiveMessage.setContent(jsonObject.getString("Content"));
+ receiveMessage.setMsgId(jsonObject.getString("MsgId"));
+ receiveMessage.setEvent(jsonObject.getString("Event"));
+ receiveMessage.setTicket(jsonObject.getString("Ticket"));
+ return receiveMessage;
+ }
+
+ /**
+ * 是否是订阅事件
+ *
+ * @param receiveMessage
+ * @return
+ */
+ public static boolean isEventAndSubscribe(ReceiveMessage receiveMessage) {
+ return StringUtils.equals(receiveMessage.getEvent(), EVENT_SUBSCRIBE);
+ }
+
+ /**
+ * 是否是二维码扫描事件
+ *
+ * @param receiveMessage
+ * @return
+ */
+ public static boolean isScanQrCode(ReceiveMessage receiveMessage) {
+ return StringUtils.isNotEmpty(receiveMessage.getTicket());
+ }
+
+ /**
+ * 获取扫描的二维码 Ticket
+ *
+ * @param receiveMessage
+ * @return
+ */
+ public static String getQrCodeTicket(ReceiveMessage receiveMessage) {
+ return receiveMessage.getTicket();
+ }
+
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinQrCodeCacheUtil.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinQrCodeCacheUtil.java
new file mode 100644
index 00000000..aaebd1e9
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinQrCodeCacheUtil.java
@@ -0,0 +1,34 @@
+package org.ruoyi.util;
+
+import java.util.LinkedHashMap;
+
+/**
+ * 微信二维码缓存工具类
+ *
+ * @author https://www.wdbyte.com
+ */
+public class WeixinQrCodeCacheUtil {
+ private static long MAX_CACHE_SIZE = 10000;
+ private static LinkedHashMap QR_CODE_TICKET_MAP = new LinkedHashMap<>();
+
+ /**
+ * 增加一个 Ticket
+ * 首次 put:value 为 ""
+ * 再次 put: value 有 openId,若openId已经存在,则已被扫码
+ *
+ * @param key
+ * @param value
+ */
+ public synchronized static void put(String key, String value) {
+ QR_CODE_TICKET_MAP.put(key, value);
+ if (QR_CODE_TICKET_MAP.size() > MAX_CACHE_SIZE) {
+ String first = QR_CODE_TICKET_MAP.keySet().stream().findFirst().get();
+ QR_CODE_TICKET_MAP.remove(first);
+ }
+ }
+
+ public synchronized static String get(String key) {
+ return QR_CODE_TICKET_MAP.remove(key);
+ }
+
+}
diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/XmlUtil.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/XmlUtil.java
new file mode 100644
index 00000000..d6e58f69
--- /dev/null
+++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/XmlUtil.java
@@ -0,0 +1,28 @@
+package org.ruoyi.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author https://www.wdbyte.com
+ */
+@Slf4j
+public class XmlUtil {
+
+ public static String xml2json(String requestBody) {
+ requestBody = StringUtils.trim(requestBody);
+ XmlMapper xmlMapper = new XmlMapper();
+ JsonNode node = null;
+ try {
+ node = xmlMapper.readTree(requestBody.getBytes());
+ ObjectMapper jsonMapper = new ObjectMapper();
+ return jsonMapper.writeValueAsString(node);
+ } catch (Exception e) {
+ log.error("xml 2 json error,msg:" + e.getMessage(), e);
+ }
+ return null;
+ }
+}