فهرست منبع

fix: 公众号登录功能同步

ageer 1 ماه پیش
والد
کامیت
5476a4b0b7
18فایلهای تغییر یافته به همراه609 افزوده شده و 67 حذف شده
  1. 5 5
      pom.xml
  2. 4 0
      ruoyi-admin/pom.xml
  3. 3 61
      ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/SysLoginService.java
  4. 4 0
      ruoyi-modules/ruoyi-wechat/pom.xml
  5. 3 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpProperties.java
  6. 48 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinServerController.java
  7. 52 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinUserController.java
  8. 1 1
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WxPortalController.java
  9. 58 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/ReceiveMessage.java
  10. 15 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/WeixinQrCode.java
  11. 108 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/VxLoginService.java
  12. 10 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/WeixinUserService.java
  13. 65 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/impl/WeixinUserServiceImpl.java
  14. 24 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/KeyUtils.java
  15. 81 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java
  16. 66 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinMsgUtil.java
  17. 34 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinQrCodeCacheUtil.java
  18. 28 0
      ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/XmlUtil.java

+ 5 - 5
pom.xml

@@ -337,11 +337,11 @@
                 <version>${revision}</version>
             </dependency>
 
-<!--            <dependency>-->
-<!--                <groupId>org.ruoyi</groupId>-->
-<!--                <artifactId>ruoyi-demo</artifactId>-->
-<!--                <version>${revision}</version>-->
-<!--            </dependency>-->
+            <dependency>
+                <groupId>org.ruoyi</groupId>
+                <artifactId>ruoyi-wechat</artifactId>
+                <version>${revision}</version>
+            </dependency>
 
         </dependencies>
     </dependencyManagement>

+ 4 - 0
ruoyi-admin/pom.xml

@@ -57,6 +57,10 @@
             <artifactId>ruoyi-generator</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.ruoyi</groupId>
+            <artifactId>ruoyi-wechat</artifactId>
+        </dependency>
 
     </dependencies>
 

+ 3 - 61
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;
-    }
-
-
     /**
      * 退出登录
      */

+ 4 - 0
ruoyi-modules/ruoyi-wechat/pom.xml

@@ -23,6 +23,10 @@
             <groupId>org.ruoyi</groupId>
             <artifactId>ruoyi-chat-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.ruoyi</groupId>
+            <artifactId>ruoyi-system-api</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 3 - 0
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 <a href="https://github.com/binarywang">Binary Wang</a>
  */
 @Data
+@ConfigurationProperties(prefix = "wechat.cp")
 public class WxCpProperties {
   /**
    * 设置企业微信的corpId

+ 48 - 0
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);
+    }
+
+}

+ 52 - 0
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<WeixinQrCode> 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<LoginVo> 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");
+    }
+}

+ 1 - 1
ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/wxcplogin/WxPortalController.java → 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;

+ 58 - 0
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 = "<xml>\n"
+            + "       <ToUserName><![CDATA[" + getFromUserName() + "]]></ToUserName>\n"
+            + "       <FromUserName><![CDATA[" + getToUserName() + "]]></FromUserName>\n"
+            + "       <CreateTime>" + System.currentTimeMillis() + "</CreateTime>\n"
+            + "       <MsgType><![CDATA[text]]></MsgType>\n"
+            + "       <Content><![CDATA[" + msg + "]]></Content>\n"
+            + "     </xml>";
+        return xml;
+    }
+}

+ 15 - 0
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;
+}

+ 108 - 0
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);
+    }
+}

+ 10 - 0
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);
+
+}

+ 65 - 0
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("你已成功登录!");
+    }
+}

+ 24 - 0
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("-", "");
+    }
+
+}

+ 81 - 0
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);
+    }
+}

+ 66 - 0
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();
+    }
+
+}

+ 34 - 0
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<String, String> 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);
+    }
+
+}

+ 28 - 0
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;
+    }
+}