wlzboy
5 天以前 7de1396e315896dbc72a9d54e44f77434ea90f18
feat:增加企业微信自动登录
8个文件已添加
16个文件已修改
1398 ■■■■■ 已修改文件
app/App.vue 71 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/api/login.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages.json 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/login.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/qylogin.vue 323 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/permission.js 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/QyWechatLoginController.java 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/security/QyWechatAuthenticationProvider.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/security/QyWechatAuthenticationToken.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/QyWechatLoginService.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/IQyWechatAccessTokenService.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/QyWechatAccessTokenServiceImpl.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/router/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/router/modules/qywechat.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/system/qywechat/autologin.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
企业微信免登功能使用说明.md 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/App.vue
@@ -12,9 +12,9 @@
        lastToken: null // ç”¨äºŽæ£€æµ‹ token å˜åŒ–
      }
    },
    onLaunch: function() {
    onLaunch: function(options) {
      this.lastToken = getToken()
      this.initApp()
      this.initApp(options)
      
      // æ£€æŸ¥å¹¶æ¸…理存储空间
      this.checkStorage()
@@ -72,13 +72,11 @@
    },
    methods: {
      // åˆå§‹åŒ–应用
      initApp() {
      initApp(options) {
        // åˆå§‹åŒ–应用配置
        this.initConfig()
        // æ£€æŸ¥ç”¨æˆ·ç™»å½•状态
        //#ifdef H5
        this.checkLogin()
        //#endif
        // æ£€æŸ¥ç”¨æˆ·ç™»å½•状态并自动跳转到合适的登录页面
        this.checkLoginAndRedirect(options)
        
        // æ³¨æ„ï¼šä¸åœ¨åº”用启动时自动启动轮询
        // åªæœ‰åœ¨ç”¨æˆ·ä¸»åŠ¨ç™»å½•æˆåŠŸåŽæ‰å¯åŠ¨ï¼ˆé€šè¿‡ user-login äº‹ä»¶è§¦å‘)
@@ -86,9 +84,62 @@
      initConfig() {
        this.globalData.config = config
      },
      checkLogin() {
      // æ£€æŸ¥ç™»å½•状态并自动跳转到合适的登录页面
      checkLoginAndRedirect(options) {
        if (!getToken()) {
          this.$tab.reLaunch('/pages/login')
          // æ£€æŸ¥è¿è¡ŒçŽ¯å¢ƒå¹¶è·³è½¬åˆ°å¯¹åº”çš„ç™»å½•é¡µé¢
          // #ifdef MP-WEIXIN
          // åœ¨å¾®ä¿¡å°ç¨‹åºçŽ¯å¢ƒä¸­
          try {
            // èŽ·å–ç³»ç»Ÿä¿¡æ¯
            const systemInfo = uni.getSystemInfoSync()
            console.log('系统信息:', systemInfo)
            // æ£€æŸ¥environment字段是否为wxwork
            if (systemInfo.environment === 'wxwork') {
              console.log('检测到企业微信环境,跳转到企业微信免登页面')
              // æž„造带参数的URL
              let url = '/pages/qylogin'
              if (options && options.query) {
                const queryParams = Object.keys(options.query).map(key => `${key}=${encodeURIComponent(options.query[key])}`).join('&')
                if (queryParams) {
                  url += '?' + queryParams
                }
              }
              this.$tab.reLaunch(url)
              return
            } else {
              console.log('检测到普通微信环境,跳转到微信登录页面')
              // æž„造带参数的URL
              let url = '/pages/login'
              if (options && options.query) {
                const queryParams = Object.keys(options.query).map(key => `${key}=${encodeURIComponent(options.query[key])}`).join('&')
                if (queryParams) {
                  url += '?' + queryParams
                }
              }
              this.$tab.reLaunch(url)
              return
            }
          } catch (e) {
            console.error('获取系统信息失败:', e)
            // é»˜è®¤è·³è½¬åˆ°æ™®é€šç™»å½•页面
            this.$tab.reLaunch('/pages/login')
          }
          // #endif
          // #ifndef MP-WEIXIN
          // éžå¾®ä¿¡å°ç¨‹åºçŽ¯å¢ƒï¼Œè·³è½¬åˆ°æ™®é€šç™»å½•é¡µé¢
          console.log('非微信小程序环境,跳转到普通登录页面')
          let url = '/pages/login'
          if (options && options.query) {
            const queryParams = Object.keys(options.query).map(key => `${key}=${encodeURIComponent(options.query[key])}`).join('&')
            if (queryParams) {
              url += '?' + queryParams
            }
          }
          this.$tab.reLaunch(url)
          // #endif
        }
      },
      
@@ -187,4 +238,4 @@
<style lang="scss">
  @import '@/static/scss/index.scss'
</style>
</style>
app/api/login.js
@@ -101,3 +101,15 @@
    }
  })
}
// ä¼ä¸šå¾®ä¿¡å…ç™»
export function qyWechatAutoLogin(code) {
  return request({
    url: '/system/qywechat/autoLogin',
    headers: {
      isToken: false
    },
    method: 'post',
    data: { code }
  })
}
app/pages.json
@@ -5,6 +5,11 @@
      "navigationBarTitleText": "登录"
    }
  }, {
    "path": "pages/qylogin",
    "style": {
      "navigationBarTitleText": "企业微信免登"
    }
  }, {
    "path": "pages/login/wechat",
    "style": {
      "navigationBarTitleText": "微信登录"
app/pages/index.vue
@@ -276,7 +276,7 @@
    }
    // æ£€æŸ¥è®¢é˜…状态(先检查本地,后面会检查微信官方状态)
    this.hasSubscribed = subscribeManager.checkLocalSubscribeStatus();
    this.hasSubscribed = true;//subscribeManager.checkLocalSubscribeStatus();
    // è‡ªåŠ¨è®¢é˜…ï¼ˆå¦‚æžœæœªè®¢é˜…åˆ™æ˜¾ç¤ºç¡®è®¤å¼¹çª—ï¼‰
    this.autoSubscribeOnLaunch();
app/pages/login.vue
@@ -85,7 +85,13 @@
        isWechat: false, // æ˜¯å¦ä¸ºå¾®ä¿¡å°ç¨‹åºçŽ¯å¢ƒ
        wechatOpenId: '', // å¾®ä¿¡OpenID
        wechatUnionId: '', // å¾®ä¿¡UnionID
        // é¡µé¢å‚æ•°
        pageOptions: {}
      }
    },
    onLoad(options) {
      // ä¿å­˜é¡µé¢å‚æ•°
      this.pageOptions = options || {}
    },
    created() {
      this.getCode()
@@ -153,7 +159,15 @@
        this.$store.dispatch('GetInfo').then(res => {
          // è§¦å‘登录成功事件,启动消息轮询
          uni.$emit('user-login')
          this.$tab.reLaunch('/pages/index')
          // æ£€æŸ¥æ˜¯å¦æœ‰redirect参数指定跳转页面
          if (this.pageOptions.redirect) {
            // è§£ç redirect参数
            const redirectUrl = decodeURIComponent(this.pageOptions.redirect)
            this.$tab.reLaunch(redirectUrl)
          } else {
            // é»˜è®¤è·³è½¬åˆ°é¦–页
            this.$tab.reLaunch('/pages/index')
          }
        })
      },
      
@@ -237,7 +251,15 @@
                  const { setToken } = require('@/utils/auth')
                  setToken(token)
                  
                  this.loginSuccess()
                  // æ£€æŸ¥æ˜¯å¦æœ‰redirect参数指定跳转页面
                  if (this.pageOptions.redirect) {
                    // è§£ç redirect参数
                    const redirectUrl = decodeURIComponent(this.pageOptions.redirect)
                    this.$tab.reLaunch(redirectUrl)
                  } else {
                    // é»˜è®¤è·³è½¬åˆ°é¦–页
                    this.$tab.reLaunch('/pages/index')
                  }
                } else {
                  this.$modal.msgError(response.msg || '登录失败')
                }
@@ -275,7 +297,15 @@
            const { setToken } = require('@/utils/auth')
            setToken(token)
            
            this.loginSuccess()
            // æ£€æŸ¥æ˜¯å¦æœ‰redirect参数指定跳转页面
            if (this.pageOptions.redirect) {
              // è§£ç redirect参数
              const redirectUrl = decodeURIComponent(this.pageOptions.redirect)
              this.$tab.reLaunch(redirectUrl)
            } else {
              // é»˜è®¤è·³è½¬åˆ°é¦–页
              this.$tab.reLaunch('/pages/index')
            }
          } else {
            // OpenID未绑定或验证失败,需要获取手机号绑定
            console.log('该OpenID尚未绑定或验证失败,需要获取手机号')
app/pages/qylogin.vue
New file
@@ -0,0 +1,323 @@
<template>
  <view class="container">
    <view class="loading-content" v-if="loading">
      <image class="loading-icon" src="/static/images/loading.gif"></image>
      <text class="loading-text">正在企业微信免登中...</text>
    </view>
    <view class="error-content" v-else-if="error">
      <image class="error-icon" src="/static/images/error.png"></image>
      <text class="error-text">{{ errorMessage }}</text>
      <button class="retry-btn" @click="retryLogin">重新尝试</button>
    </view>
    <view class="success-content" v-else>
      <image class="success-icon" src="/static/images/success.png"></image>
      <text class="success-text">企业微信免登成功</text>
    </view>
  </view>
</template>
<script>
import { qyWechatAutoLogin } from "@/api/login";
export default {
  data() {
    return {
      loading: true,
      error: false,
      errorMessage: "",
      // ä¿å­˜é¡µé¢å‚æ•°
      pageOptions: {},
    };
  },
  onLoad(options) {
    // ä¿å­˜é¡µé¢å‚æ•°
    this.pageOptions = options || {};
    // é¡µé¢åŠ è½½æ—¶æ‰§è¡Œå…ç™»æµç¨‹
    this.qyWechatAutoLogin();
  },
  methods: {
    async getLoginCode() {
      //这里要调用wx.qy.login获取code,是否有集成到uni-app中呢
      return new Promise((resolve, reject) => {
        wx.qy.login({
          success: (res) => {
            if (res.code) {
              console.log("企业微信小程序 ---> code:", res.code);
              resolve(res.code); // è¿”回 code ç»™åŽç«¯
            } else {
              reject(new Error("获取 code å¤±è´¥ï¼š" + res.errMsg));
            }
          },
          fail: (err) => {
            console.error("wx.qy.login è°ƒç”¨å¤±è´¥ï¼š", err);
            // å¸¸è§å¤±è´¥åŽŸå› ï¼šéžä¼ä¸šå¾®ä¿¡å®¢æˆ·ç«¯æ‰“å¼€ã€å°ç¨‹åºæœªå…³è”ä¼ä¸šå¾®ä¿¡ç­‰
            reject(new Error("企业微信登录接口调用失败:" + err.errMsg));
          },
        });
      });
    },
    /**
     * ä¼ä¸šå¾®ä¿¡å…ç™»æµç¨‹
     */
    async qyWechatAutoLogin() {
      try {
        this.loading = true;
        this.error = false;
        // #ifdef MP-WEIXIN
        // åœ¨å¾®ä¿¡å°ç¨‹åºçŽ¯å¢ƒä¸­ï¼Œé€šè¿‡ä¼ä¸šå¾®ä¿¡å…ç™»
        console.log("企业微信小程序环境免登");
        // èŽ·å–URL参数中的code
        const code = await this.getLoginCode();
        if (!code) {
          // å¦‚果没有code,尝试通过企业微信API获取
          this.handleWxWorkLogin();
          return;
        }
        // è°ƒç”¨åŽç«¯æŽ¥å£è¿›è¡Œå…ç™»
        const response = await qyWechatAutoLogin(code);
        if (response.code === 200) {
          // å…ç™»æˆåŠŸï¼Œä¿å­˜token
          const token = response.data.token;
          this.$store.commit('SET_TOKEN', token)
           // å¿…须调用setToken保存到本地存储
          const { setToken } = require('@/utils/auth')
          setToken(token)
          // èŽ·å–ç”¨æˆ·ä¿¡æ¯
          await this.$store.dispatch("GetInfo");
          // è·³è½¬åˆ°é¦–页或其他指定页面
          this.redirectAfterLogin();
        } else {
          throw new Error(response.msg || "免登失败");
        }
        // #endif
        // #ifdef H5
        // æ£€æŸ¥æ˜¯å¦åœ¨ä¼ä¸šå¾®ä¿¡çŽ¯å¢ƒä¸­
        if (!this.isWxWorkEnvironment()) {
          throw new Error("请在企业微信客户端中打开");
        }
        // èŽ·å–URL参数中的code
        const codeH5 = this.getUrlParam("code");
        if (!codeH5) {
          // å¦‚果没有code,则跳转到企业微信授权页面
          this.redirectToWxWorkAuth();
          return;
        }
        // è°ƒç”¨åŽç«¯æŽ¥å£è¿›è¡Œå…ç™»
        const responseH5 = await qyWechatAutoLogin(codeH5);
        if (responseH5.code === 200) {
          // å…ç™»æˆåŠŸï¼Œä¿å­˜token
          const token = responseH5.data.token;
          this.$store.commit("SET_TOKEN", token);
          uni.setStorageSync("token", token);
          // èŽ·å–ç”¨æˆ·ä¿¡æ¯
          await this.$store.dispatch("GetInfo");
          // è·³è½¬åˆ°é¦–页或其他指定页面
          this.redirectAfterLogin();
        } else {
          throw new Error(responseH5.msg || "免登失败");
        }
        // #endif
        // #ifndef MP-WEIXIN || H5
        throw new Error("该功能仅支持在企业微信中使用");
        // #endif
      } catch (err) {
        console.error("企业微信免登失败:", err);
        this.loading = false;
        this.error = true;
        this.errorMessage = err.message || "免登失败,请稍后重试";
      }
    },
    /**
     * å¤„理企业微信登录
     */
    handleWxWorkLogin() {
      // #ifdef MP-WEIXIN
      // åœ¨ä¼ä¸šå¾®ä¿¡å°ç¨‹åºä¸­ï¼Œå¯ä»¥ç›´æŽ¥è°ƒç”¨ä¼ä¸šå¾®ä¿¡ç™»å½•API
      uni.login({
        provider: "weixin",
        success: (loginRes) => {
          console.log("企业微信登录成功", loginRes);
          // è°ƒç”¨åŽç«¯æŽ¥å£è¿›è¡Œå…ç™»
          qyWechatAutoLogin(loginRes.code)
            .then((response) => {
              if (response.code === 200) {
                // å…ç™»æˆåŠŸï¼Œä¿å­˜token
                const token = response.data.token;
                this.$store.commit("SET_TOKEN", token);
                uni.setStorageSync("token", token);
                // èŽ·å–ç”¨æˆ·ä¿¡æ¯
                this.$store.dispatch("GetInfo").then(() => {
                  // è·³è½¬åˆ°é¦–页或其他指定页面
                  this.redirectAfterLogin();
                });
              } else {
                throw new Error(response.msg || "免登失败");
              }
            })
            .catch((error) => {
              console.error("免登失败:", error);
              this.loading = false;
              this.error = true;
              this.errorMessage = error.message || "免登失败,请稍后重试";
            });
        },
        fail: (err) => {
          console.error("企业微信登录失败:", err);
          this.loading = false;
          this.error = true;
          this.errorMessage = "企业微信登录失败,请稍后重试";
        },
      });
      // #endif
    },
    /**
     * æ£€æŸ¥æ˜¯å¦åœ¨ä¼ä¸šå¾®ä¿¡çŽ¯å¢ƒ
     */
    isWxWorkEnvironment() {
      const userAgent = navigator.userAgent.toLowerCase();
      return userAgent.includes("wxwork");
    },
    /**
     * èŽ·å–URL参数
     */
    getUrlParam(name) {
      // #ifdef H5
      const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
      const r = window.location.search.substr(1).match(reg);
      if (r != null) return decodeURIComponent(r[2]);
      // #endif
      // #ifdef MP-WEIXIN
      // åœ¨å°ç¨‹åºä¸­å¯ä»¥é€šè¿‡å…¶ä»–方式获取参数
      // è¿™é‡Œç®€åŒ–处理,实际项目中可以根据需要调整
      // #endif
      return null;
    },
    /**
     * è·³è½¬åˆ°ä¼ä¸šå¾®ä¿¡æŽˆæƒé¡µé¢
     */
    redirectToWxWorkAuth() {
      // ä»Žå…¨å±€é…ç½®ä¸­èŽ·å–ä¼ä¸šå¾®ä¿¡é…ç½®
      const config = getApp().globalData.config;
      const corpId = config.qyWechatCorpId || "your_corp_id"; // ä¼ä¸šID
      const agentId = config.qyWechatAgentId || "your_agent_id"; // åº”用ID
      const redirectUri = encodeURIComponent(window.location.href);
      const state = Date.now(); // é˜²é‡æ”¾æ”»å‡»
      const authUrl = `https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=${corpId}&agentid=${agentId}&redirect_uri=${redirectUri}&state=${state}`;
      window.location.href = authUrl;
    },
    /**
     * ç™»å½•成功后的跳转处理
     */
    redirectAfterLogin() {
      // æ£€æŸ¥æ˜¯å¦æœ‰redirect参数指定跳转页面
      if (this.pageOptions.redirect) {
        // è§£ç redirect参数
        const redirectUrl = decodeURIComponent(this.pageOptions.redirect);
        this.$tab.reLaunch(redirectUrl);
      } else {
        // é»˜è®¤è·³è½¬åˆ°é¦–页
        this.$tab.reLaunch("/pages/index");
      }
    },
    /**
     * é‡æ–°å°è¯•登录
     */
    retryLogin() {
      this.qyWechatAutoLogin();
    },
  },
};
</script>
<style lang="scss" scoped>
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 40rpx;
  background-color: #f8f8f8;
}
.loading-content,
.error-content,
.success-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}
.loading-icon,
.error-icon,
.success-icon {
  width: 120rpx;
  height: 120rpx;
  margin-bottom: 30rpx;
}
.loading-text,
.error-text,
.success-text {
  font-size: 32rpx;
  color: #333;
  margin-bottom: 40rpx;
}
.error-text,
.success-text {
  font-weight: bold;
}
.error-text {
  color: #ff0000;
}
.success-text {
  color: #00cc00;
}
.retry-btn {
  width: 80%;
  height: 80rpx;
  background-color: #007aff;
  color: #fff;
  border-radius: 10rpx;
  font-size: 32rpx;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>
app/permission.js
@@ -6,6 +6,7 @@
// é¡µé¢ç™½åå•
const whiteList = [
  '/pages/login', 
  '/pages/qylogin',  // ä¼ä¸šå¾®ä¿¡å…ç™»é¡µé¢ï¼ˆåŒ¿åè®¿é—®ï¼‰
  '/pages/register', 
  '/pages/common/webview/index',
  '/pages/mine/privacy-policy/index',  // éšç§æ”¿ç­–(匿名访问)
@@ -40,4 +41,4 @@
      console.log(err)
    }
  })
})
})
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/QyWechatLoginController.java
New file
@@ -0,0 +1,210 @@
package com.ruoyi.web.controller.system;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.framework.web.service.QyWechatLoginService;
import com.ruoyi.system.service.IQyWechatAccessTokenService;
import com.ruoyi.system.service.IQyWechatService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
/**
 * ä¼ä¸šå¾®ä¿¡å…ç™»æŽ§åˆ¶å™¨
 *
 * @author ruoyi
 * @date 2025-12-14
 */
@RestController
@RequestMapping("/system/qywechat")
public class QyWechatLoginController extends BaseController {
    private static final Logger log = LoggerFactory.getLogger(QyWechatLoginController.class);
    @Autowired
    private IQyWechatAccessTokenService qyWechatAccessTokenService;
    @Autowired
    private IQyWechatService qyWechatService;
    @Autowired
    private ISysConfigService configService;
    @Autowired
    private ISysUserService userService;
    @Autowired
    private QyWechatLoginService qyWechatLoginService;
    /**
     * ä¼ä¸šå¾®ä¿¡å…ç™»æŽ¥å£
     *
     * @param params ä¼ä¸šå¾®ä¿¡æŽˆæƒcode
     * @return ç™»å½•结果
     */
    @Anonymous
    @PostMapping("/autoLogin")
    public AjaxResult autoLogin(@RequestBody Map<String, String> params) {
        try {
            String code = params.get("code");
            if (code == null || code.isEmpty()) {
                return AjaxResult.error("缺少授权code参数");
            }
            // èŽ·å–ä¼ä¸šå¾®ä¿¡é…ç½®
            String corpId = configService.selectConfigByKey("qy_wechat.corp_id");
            // èŽ·å–ä¼ä¸šå¾®ä¿¡å°ç¨‹åºçš„secret(用于获取AccessToken)
            String miniProgramSecret = configService.selectConfigByKey("qy_wechat.miniprogram_secret");
            if (corpId == null || miniProgramSecret == null) {
                return AjaxResult.error("企业微信配置不完整,请检查corp_id和miniprogram_secret配置");
            }
            // èŽ·å–AccessToken(使用小程序的secret)
            String accessToken = qyWechatAccessTokenService.getQyMiniAccessToken(corpId, miniProgramSecret);
            if (accessToken == null) {
                return AjaxResult.error("获取企业微信AccessToken失败");
            }
            // é€šè¿‡code获取用户信息
            Map<String, Object> userInfo = getUserInfoByCode(accessToken, code);
            if (!((Boolean) userInfo.get("success"))) {
                return AjaxResult.error((String) userInfo.get("message"));
            }
            // èŽ·å–ç”¨æˆ·ID
            String userId = (String) userInfo.get("userid");
            // æ ¹æ®ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID查找系统用户
            SysUser sysUser = userService.selectUserByQyWechatUserId(userId);
            if (sysUser == null) {
                return AjaxResult.error("该企业微信账号未绑定系统用户");
            }
            // æ£€æŸ¥ç”¨æˆ·çŠ¶æ€
            if ("1".equals(sysUser.getStatus())) {
                return AjaxResult.error("用户已被停用,请联系管理员");
            }
            if ("1".equals(sysUser.getDelFlag())) {
                return AjaxResult.error("用户已被删除,请联系管理员");
            }
            // ä½¿ç”¨QyWechatLoginService生成token
            String token = qyWechatLoginService.loginByQyUserId(userId, corpId);
            // æž„造返回结果
            Map<String, Object> result = new HashMap<>();
            result.put("token", token);
            result.put("user", sysUser);
            return AjaxResult.success("登录成功", result);
        } catch (Exception e) {
            log.error("企业微信免登异常", e);
            return AjaxResult.error("登录异常:" + e.getMessage());
        }
    }
    /**
     * æ ¹æ®code获取用户信息
     *
     * @param accessToken AccessToken
     * @param code æŽˆæƒcode
     * @return ç”¨æˆ·ä¿¡æ¯
     */
    private Map<String, Object> getUserInfoByCode(String accessToken, String code) {
        try {
            Map<String, Object> result = new HashMap<>();
            // æž„造请求URL - ä½¿ç”¨ä¼ä¸šå¾®ä¿¡å°ç¨‹åºä¸“用接口
            String url = "https://qyapi.weixin.qq.com/cgi-bin/miniprogram/jscode2session?access_token=" + accessToken + "&js_code=" + code + "&grant_type=authorization_code";
            // å‘送HTTP GET请求
            String response = sendHttpGetRequest(url);
            if (response == null || response.isEmpty()) {
                result.put("success", false);
                result.put("message", "获取用户信息失败,响应为空");
                return result;
            }
            // ä½¿ç”¨FastJSON解析响应
            JSONObject jsonResponse = JSON.parseObject(response);
            // æ£€æŸ¥æ˜¯å¦æœ‰é”™è¯¯
            Integer errcode = jsonResponse.getInteger("errcode");
            if (errcode != null && errcode != 0) {
                String errmsg = jsonResponse.getString("errmsg");
                result.put("success", false);
                result.put("message", "获取用户信息失败,错误码:" + errcode + ",错误信息:" + errmsg);
                return result;
            }
            // æ£€æŸ¥æ˜¯å¦åŒ…含userid字段
            String userId = jsonResponse.getString("userid");
            if (userId == null || userId.isEmpty()) {
                result.put("success", false);
                result.put("message", "获取用户信息失败,未找到用户ID");
                return result;
            }
            result.put("success", true);
            result.put("userid", userId);
            result.put("corpid", jsonResponse.getString("corpid"));
            result.put("session_key", jsonResponse.getString("session_key"));
            return result;
        } catch (Exception e) {
            log.error("获取用户信息异常", e);
            Map<String, Object> result = new HashMap<>();
            result.put("success", false);
            result.put("message", "获取用户信息异常:" + e.getMessage());
            return result;
        }
    }
    /**
     * å‘送HTTP GET请求
     *
     * @param url è¯·æ±‚URL
     * @return å“åº”内容
     */
    private String sendHttpGetRequest(String url) {
        try {
            java.net.HttpURLConnection conn = (java.net.HttpURLConnection) new java.net.URL(url).openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5000);
            conn.setReadTimeout(5000);
            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {
                java.io.BufferedReader reader = new java.io.BufferedReader(
                    new java.io.InputStreamReader(conn.getInputStream(), "UTF-8"));
                StringBuilder response = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    response.append(line);
                }
                reader.close();
                return response.toString();
            } else {
                log.error("HTTP请求失败,响应码: {}", responseCode);
                return null;
            }
        } catch (Exception e) {
            log.error("发送HTTP请求失败", e);
            return null;
        }
    }
}
ruoyi-admin/src/main/resources/application.yml
@@ -58,7 +58,7 @@
    basename: i18n/messages
  profiles:
    # çŽ¯å¢ƒ dev|test|prod
    active: prod
    active: dev
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
@@ -21,6 +21,7 @@
import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
import com.ruoyi.framework.security.WechatAuthenticationProvider;
import com.ruoyi.framework.security.QyWechatAuthenticationProvider;
import com.ruoyi.common.annotation.Anonymous;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.method.HandlerMethod;
@@ -85,6 +86,12 @@
    private WechatAuthenticationProvider wechatAuthenticationProvider;
    /**
     * ä¼ä¸šå¾®ä¿¡è®¤è¯æä¾›è€…
     */
    @Autowired
    private QyWechatAuthenticationProvider qyWechatAuthenticationProvider;
    /**
     * èŽ·å–æ‰€æœ‰æ ‡æ³¨äº†@Anonymous的URL
     */
    private Set<String> getAnonymousUrls() {
@@ -105,7 +112,7 @@
    /**
     * èº«ä»½éªŒè¯å®žçް
     * æ”¯æŒç”¨æˆ·åå¯†ç è®¤è¯å’Œå¾®ä¿¡è®¤è¯
     * æ”¯æŒç”¨æˆ·åå¯†ç è®¤è¯ã€å¾®ä¿¡è®¤è¯å’Œä¼ä¸šå¾®ä¿¡è®¤è¯
     */
    @Bean
    public AuthenticationManager authenticationManager()
@@ -116,7 +123,7 @@
        daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
        
        // è¿”回ProviderManager,支持多种认证方式
        return new ProviderManager(daoAuthenticationProvider, wechatAuthenticationProvider);
        return new ProviderManager(daoAuthenticationProvider, wechatAuthenticationProvider, qyWechatAuthenticationProvider);
    }
    /**
ruoyi-framework/src/main/java/com/ruoyi/framework/security/QyWechatAuthenticationProvider.java
New file
@@ -0,0 +1,75 @@
package com.ruoyi.framework.security;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
 * ä¼ä¸šå¾®ä¿¡ç™»å½•认证提供者
 * ç±»ä¼¼äºŽDaoAuthenticationProvider
 *
 * @author ruoyi
 */
@Component
public class QyWechatAuthenticationProvider implements AuthenticationProvider
{
    @Autowired
    private ISysUserService userService;
    @Autowired
    private SysPermissionService permissionService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException
    {
        QyWechatAuthenticationToken qyWechatToken = (QyWechatAuthenticationToken) authentication;
        String qyUserId = (String) qyWechatToken.getPrincipal();
        String corpId = (String) qyWechatToken.getCredentials();
        //qywechat__qyUserId
        //对qywechat__进行处理得到qyUserId
        qyUserId = StringUtils.substringAfter(qyUserId, "qywechat__");
        // æ ¹æ®ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID查询用户
        SysUser user = userService.selectUserByQyWechatUserId(qyUserId);
        if (user == null)
        {
            throw new BadCredentialsException("该企业微信账号尚未绑定系统用户");
        }
        // æ£€æŸ¥ç”¨æˆ·çŠ¶æ€
        if ("1".equals(user.getStatus()))
        {
            throw new BadCredentialsException("用户已被停用,请联系管理员");
        }
        if ("1".equals(user.getDelFlag()))
        {
            throw new BadCredentialsException("用户已被删除,请联系管理员");
        }
        // èŽ·å–ç”¨æˆ·æƒé™
        Set<String> permissions = permissionService.getMenuPermission(user);
        // åˆ›å»ºLoginUser对象
        LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissions);
        // è¿”回已认证的Token
        return new QyWechatAuthenticationToken(loginUser, corpId, loginUser.getAuthorities());
    }
    @Override
    public boolean supports(Class<?> authentication)
    {
        return QyWechatAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/security/QyWechatAuthenticationToken.java
New file
@@ -0,0 +1,85 @@
package com.ruoyi.framework.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
 * ä¼ä¸šå¾®ä¿¡ç™»å½•认证Token
 * ç±»ä¼¼äºŽUsernamePasswordAuthenticationToken
 *
 * @author ruoyi
 */
public class QyWechatAuthenticationToken extends AbstractAuthenticationToken
{
    private static final long serialVersionUID = 1L;
    /**
     * è®¤è¯ä¸»ä½“(登录前为企业微信用户ID,登录后为LoginUser)
     */
    private final Object principal;
    /**
     * è®¤è¯å‡­è¯(企业微信CorpID)
     */
    private Object credentials;
    /**
     * åˆ›å»ºæœªè®¤è¯çš„Token(登录前)
     *
     * @param qyUserId ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID
     * @param corpId ä¼ä¸šå¾®ä¿¡CorpID
     */
    public QyWechatAuthenticationToken(String qyUserId, String corpId)
    {
        super(null);
        this.principal = qyUserId;
        this.credentials = corpId;
        setAuthenticated(false);
    }
    /**
     * åˆ›å»ºå·²è®¤è¯çš„Token(登录后)
     *
     * @param principal ç™»å½•用户信息
     * @param credentials å‡­è¯
     * @param authorities æƒé™åˆ—表
     */
    public QyWechatAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
    {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }
    @Override
    public Object getCredentials()
    {
        return this.credentials;
    }
    @Override
    public Object getPrincipal()
    {
        return this.principal;
    }
    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException
    {
        if (isAuthenticated)
        {
            throw new IllegalArgumentException(
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }
        super.setAuthenticated(false);
    }
    @Override
    public void eraseCredentials()
    {
        super.eraseCredentials();
        credentials = null;
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/QyWechatLoginService.java
New file
@@ -0,0 +1,89 @@
package com.ruoyi.framework.web.service;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.security.QyWechatAuthenticationToken;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
/**
 * ä¼ä¸šå¾®ä¿¡ç™»å½•校验方法
 * ç±»ä¼¼äºŽSysLoginService
 *
 * @author ruoyi
 */
@Component
public class QyWechatLoginService
{
    @Autowired
    private TokenService tokenService;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private SysLoginService sysLoginService;
    @Autowired
    private ISysUserService userService;
    /**
     * ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID登录验证
     *
     * @param qyUserId ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID
     * @param corpId ä¼ä¸šå¾®ä¿¡CorpID
     * @return token
     */
    public String loginByQyUserId(String qyUserId, String corpId)
    {
        try
        {
            qyUserId = "qywechat__"+qyUserId;
            // åˆ›å»ºä¼ä¸šå¾®ä¿¡è®¤è¯Token
            QyWechatAuthenticationToken authenticationToken = new QyWechatAuthenticationToken(qyUserId, corpId);
            // ä½¿ç”¨AuthenticationManager进行认证
            Authentication authentication = authenticationManager.authenticate(authenticationToken);
            // è®¤è¯æˆåŠŸ,获取LoginUser
            LoginUser loginUser = (LoginUser) authentication.getPrincipal();
            // è®°å½•登录成功日志
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(
                loginUser.getUsername(),
                Constants.LOGIN_SUCCESS,
                "企业微信用户ID登录成功"));
            // è®°å½•登录信息(IP和时间)
            sysLoginService.recordLoginInfo(loginUser.getUserId());
            // ç”Ÿæˆtoken
            return tokenService.createToken(loginUser);
        }
        catch (BadCredentialsException e)
        {
            // è®°å½•登录失败日志
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(
                qyUserId,
                Constants.LOGIN_FAIL,
                e.getMessage()));
            throw e;
        }
        catch (Exception e)
        {
            // è®°å½•登录失败日志
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(
                qyUserId,
                Constants.LOGIN_FAIL,
                e.getMessage()));
            throw new BadCredentialsException(e.getMessage());
        }
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java
@@ -40,9 +40,16 @@
        // å°è¯•判断是手机号、openId还是用户名
        SysUser user = null;
        //qywechat__
        if (username.startsWith("qywechat__"))
        {
            //企业微信登录
            //qywechat__qyUserId
            String qyUserId =StringUtils.substringAfter(username, "qywechat__");
            user = userService.selectUserByQyWechatUserId(qyUserId);
        }
        // åˆ¤æ–­æ˜¯å¦ä¸ºå¾®ä¿¡OpenID(通常以"o"开头,28位字符)
        if (username.startsWith("o") && username.length() == 28)
        else if (username.startsWith("o") && username.length() == 28)
        {
            // å¾®ä¿¡OpenID登录
            log.info("尝试使用微信OpenID登录:{}", username);
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
@@ -153,6 +153,14 @@
    public SysUser selectUserByOpenId(@Param("openId") String openId);
    
    /**
     * é€šè¿‡ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID查询用户
     *
     * @param qyWechatUserId ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID
     * @return ç”¨æˆ·å¯¹è±¡ä¿¡æ¯
     */
    public SysUser selectUserByQyWechatUserId(@Param("qyWechatUserId") String qyWechatUserId);
    /**
     * æ ¹æ®åˆ†å…¬å¸ID列表查询用户(包含分公司及其所有子部门的用户)
     * 
     * @param branchDeptIds åˆ†å…¬å¸ID列表
ruoyi-system/src/main/java/com/ruoyi/system/service/IQyWechatAccessTokenService.java
@@ -12,19 +12,28 @@
     * èŽ·å–ä¼ä¸šå¾®ä¿¡åº”ç”¨çš„AccessToken
     * 
     * @param corpId ä¼ä¸šID
     * @param corpSecret åº”用密钥
     * @param secret åº”用密钥或小程序密钥
     * @return AccessToken
     */
    String getAppAccessToken(String corpId, String corpSecret);
    String getAppAccessToken(String corpId, String secret);
    /**
     * åˆ·æ–°ä¼ä¸šå¾®ä¿¡åº”用的AccessToken
     * 
     * @param corpId ä¼ä¸šID
     * @param corpSecret åº”用密钥
     * @param secret åº”用密钥或小程序密钥
     * @return æ–°çš„AccessToken
     */
    String refreshAppAccessToken(String corpId, String corpSecret);
    String refreshAppAccessToken(String corpId, String secret);
    /**
     * èŽ·å–ä¼ä¸šå¾®ä¿¡å°ç¨‹åºçš„AccessToken
     *
     * @param corpId ä¼ä¸šID
     * @param corpSecret å°ç¨‹åºå¯†é’¥
     * @return AccessToken
     */
    String getQyMiniAccessToken(String corpId, String corpSecret);
    /**
     * æ£€æŸ¥ä¼ä¸šå¾®ä¿¡æœåŠ¡æ˜¯å¦å¯ç”¨
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java
@@ -59,6 +59,14 @@
    public SysUser selectUserByOpenId(String openId);
    
    /**
     * é€šè¿‡ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID查询用户
     *
     * @param qyWechatUserId ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID
     * @return ç”¨æˆ·å¯¹è±¡ä¿¡æ¯
     */
    public SysUser selectUserByQyWechatUserId(String qyWechatUserId);
    /**
     * æ ¹æ®oaUserId查询用户
     * 
     * @param oaUserId SQL Server中的OA用户ID
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/QyWechatAccessTokenServiceImpl.java
@@ -31,15 +31,15 @@
     * èŽ·å–ä¼ä¸šå¾®ä¿¡åº”ç”¨çš„AccessToken
     *
     * @param corpId ä¼ä¸šID
     * @param corpSecret åº”用密钥
     * @param secret åº”用密钥或小程序密钥
     * @return AccessToken
     */
    @Override
    public String getAppAccessToken(String corpId, String corpSecret) {
    public String getAppAccessToken(String corpId, String secret) {
        try {
            // å‚数校验
            if (StringUtils.isEmpty(corpId) || StringUtils.isEmpty(corpSecret)) {
                log.warn("企业微信配置参数不完整,corpId或corpSecret为空");
            if (StringUtils.isEmpty(corpId) || StringUtils.isEmpty(secret)) {
                log.warn("企业微信配置参数不完整,corpId或secret为空");
                return null;
            }
@@ -77,7 +77,7 @@
            }
            // Token不存在或已过期,刷新Token
            return refreshAppAccessToken(corpId, corpSecret);
            return refreshAppAccessToken(corpId, secret);
        } catch (Exception e) {
            log.error("获取企业微信AccessToken失败", e);
            return null;
@@ -88,16 +88,16 @@
     * åˆ·æ–°ä¼ä¸šå¾®ä¿¡åº”用的AccessToken
     *
     * @param corpId ä¼ä¸šID
     * @param corpSecret åº”用密钥
     * @param secret åº”用密钥或小程序密钥
     * @return æ–°çš„AccessToken
     */
    @Override
    public String refreshAppAccessToken(String corpId, String corpSecret) {
    public String refreshAppAccessToken(String corpId, String secret) {
        try {
            log.info("开始刷新企业微信AccessToken");
            // æž„建请求URL
            String url = GET_ACCESS_TOKEN_URL + "?corpid=" + corpId + "&corpsecret=" + corpSecret;
            String url = GET_ACCESS_TOKEN_URL + "?corpid=" + corpId + "&corpsecret=" + secret;
            // å‘送HTTP请求获取Token
            String response = sendHttpGetRequest(url);
@@ -135,6 +135,112 @@
    }
    /**
     * èŽ·å–ä¼ä¸šå¾®ä¿¡å°ç¨‹åºçš„AccessToken
     *
     * @param corpId ä¼ä¸šID
     * @param corpSecret å°ç¨‹åºå¯†é’¥
     * @return AccessToken
     */
    @Override
    public String getQyMiniAccessToken(String corpId, String corpSecret) {
        try {
            // å‚数校验
            if (StringUtils.isEmpty(corpId) || StringUtils.isEmpty(corpSecret)) {
                log.warn("企业微信小程序配置参数不完整,corpId或corpSecret为空");
                return null;
            }
            // æ£€æŸ¥æœåŠ¡æ˜¯å¦å¯ç”¨
            if (!isEnabled()) {
                log.info("企业微信服务已禁用,无法获取小程序AccessToken");
                return null;
            }
            // æž„建配置键名(使用不同的键名以区分普通应用和小程序)
            String tokenKey = "qy_wechat.mini_access_token." + corpId;
            String expiresKey = "qy_wechat.mini_access_token_expires." + corpId;
            // ä»Žé…ç½®ä¸­èŽ·å–Token和过期时间
            String accessToken = configService.selectConfigByKey(tokenKey);
            String expiresStr = configService.selectConfigByKey(expiresKey);
            // æ£€æŸ¥Token是否存在且未过期
            if (StringUtils.isNotEmpty(accessToken) && StringUtils.isNotEmpty(expiresStr)) {
                try {
                    long expiresTime = Long.parseLong(expiresStr);
                    long currentTime = System.currentTimeMillis();
                    // é¢„ç•™60秒安全边界,避免临界点过期
                    if (currentTime < expiresTime - 60000) {
                        log.debug("使用缓存的企业微信小程序AccessToken,剩余有效时间: {}秒",
                            (expiresTime - currentTime) / 1000);
                        return accessToken;
                    } else {
                        log.info("企业微信小程序AccessToken已过期或即将过期,需要刷新");
                    }
                } catch (NumberFormatException e) {
                    log.warn("解析企业微信小程序AccessToken过期时间失败: {}", expiresStr);
                }
            }
            // Token不存在或已过期,刷新Token
            return refreshQyMiniAccessToken(corpId, corpSecret);
        } catch (Exception e) {
            log.error("获取企业微信小程序AccessToken失败", e);
            return null;
        }
    }
    /**
     * åˆ·æ–°ä¼ä¸šå¾®ä¿¡å°ç¨‹åºçš„AccessToken
     *
     * @param corpId ä¼ä¸šID
     * @param corpSecret å°ç¨‹åºå¯†é’¥
     * @return æ–°çš„AccessToken
     */
    public String refreshQyMiniAccessToken(String corpId, String corpSecret) {
        try {
            log.info("开始刷新企业微信小程序AccessToken");
            // æž„建请求URL
            String url = GET_ACCESS_TOKEN_URL + "?corpid=" + corpId + "&corpsecret=" + corpSecret;
            // å‘送HTTP请求获取Token
            String response = sendHttpGetRequest(url);
            if (StringUtils.isEmpty(response)) {
                log.error("获取企业微信小程序AccessToken失败,响应为空");
                return null;
            }
            // è§£æžå“åº”
            QyWechatTokenResponse tokenResponse = parseTokenResponse(response);
            if (tokenResponse == null || StringUtils.isEmpty(tokenResponse.getAccessToken())) {
                log.error("解析企业微信小程序AccessToken响应失败: {}", response);
                return null;
            }
            // è®¡ç®—过期时间(当前时间 + æœ‰æ•ˆæœŸ - 60秒安全边界)
            long expiresTime = System.currentTimeMillis() + (tokenResponse.getExpiresIn() * 1000L) - 60000L;
            // æž„建配置键名(使用不同的键名以区分普通应用和小程序)
            String tokenKey = "qy_wechat.mini_access_token." + corpId;
            String expiresKey = "qy_wechat.mini_access_token_expires." + corpId;
            // ä¿å­˜åˆ°ç³»ç»Ÿé…ç½®è¡¨
            configService.updateConfigValue(tokenKey, tokenResponse.getAccessToken());
            configService.updateConfigValue(expiresKey, String.valueOf(expiresTime));
            log.info("企业微信小程序AccessToken刷新成功,有效期: {}秒", tokenResponse.getExpiresIn());
            return tokenResponse.getAccessToken();
        } catch (Exception e) {
            log.error("刷新企业微信小程序AccessToken失败", e);
            return null;
        }
    }
    /**
     * æ£€æŸ¥ä¼ä¸šå¾®ä¿¡æœåŠ¡æ˜¯å¦å¯ç”¨
     *
     * @return true-启用,false-禁用
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
@@ -153,6 +153,18 @@
    }
    
    /**
     * é€šè¿‡ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID查询用户
     *
     * @param qyWechatUserId ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID
     * @return ç”¨æˆ·å¯¹è±¡ä¿¡æ¯
     */
    @Override
    public SysUser selectUserByQyWechatUserId(String qyWechatUserId)
    {
        return userMapper.selectUserByQyWechatUserId(qyWechatUserId);
    }
    /**
     * æ ¹æ®oaUserId查询用户
     * 
     * @param oaUserId SQL Server中的OA用户ID
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
@@ -263,6 +263,12 @@
        where u.open_id = #{openId} and u.del_flag = '0'
    </select>
    
    <!-- é€šè¿‡ä¼ä¸šå¾®ä¿¡ç”¨æˆ·ID查询用户 -->
    <select id="selectUserByQyWechatUserId" parameterType="String" resultMap="SysUserResult">
        <include refid="selectUserVo"/>
        where u.qy_wechat_user_id = #{qyWechatUserId} and u.del_flag = '0'
    </select>
    <!-- æ ¹æ®åˆ†å…¬å¸ID列表查询用户(包含分公司及其所有子部门的用户) -->
    <select id="selectUsersByBranchDeptIds" resultMap="SysUserResult">
        SELECT DISTINCT
ruoyi-ui/src/router/index.js
@@ -5,6 +5,7 @@
/* Layout */
import Layout from '@/layout'
import qywechatRouter from './modules/qywechat'
/**
 * Note: è·¯ç”±é…ç½®é¡¹
@@ -149,6 +150,7 @@
// åŠ¨æ€è·¯ç”±ï¼ŒåŸºäºŽç”¨æˆ·æƒé™åŠ¨æ€åŽ»åŠ è½½
export const dynamicRoutes = [
  qywechatRouter,
  {
    path: '/system/user-auth',
    component: Layout,
ruoyi-ui/src/router/modules/qywechat.js
New file
@@ -0,0 +1,25 @@
import Layout from '@/layout'
const qywechatRouter = {
  path: '/qywechat',
  component: Layout,
  redirect: 'noRedirect',
  name: 'QyWechat',
  meta: { title: '企业微信', icon: 'wechat' },
  children: [
    {
      path: 'test',
      component: () => import('@/views/system/qywechat/test'),
      name: 'QyWechatTest',
      meta: { title: '功能测试', icon: 'guide' }
    },
    {
      path: 'autologin',
      component: () => import('@/views/system/qywechat/autologin'),
      name: 'QyWechatAutoLogin',
      meta: { title: '免登测试', icon: 'lock' }
    }
  ]
}
export default qywechatRouter
ruoyi-ui/src/views/system/qywechat/autologin.vue
New file
@@ -0,0 +1,108 @@
<template>
  <div class="app-container">
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <span>企业微信免登测试</span>
      </div>
      <el-alert
        title="说明"
        type="info"
        description="此页面用于测试企业微信免登功能,请确保已在企业微信中配置好相关参数"
        show-icon
        :closable="false"
        style="margin-bottom: 20px;"
      >
      </el-alert>
      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
        <el-form-item label="授权Code" prop="code">
          <el-input v-model="form.code" placeholder="请输入企业微信授权Code" />
          <div class="form-tip">可通过企业微信扫码登录获取Code</div>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleAutoLogin">测试免登</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
      <el-divider />
      <div v-if="loginResult">
        <h4>测试结果:</h4>
        <el-card class="result-card">
          <pre>{{ loginResult }}</pre>
        </el-card>
      </div>
    </el-card>
  </div>
</template>
<script>
import { getCodeImg } from "@/api/login";
export default {
  name: "QyWechatAutoLogin",
  data() {
    return {
      form: {
        code: ""
      },
      rules: {
        code: [
          { required: true, message: "请输入授权Code", trigger: "blur" }
        ]
      },
      loginResult: null
    };
  },
  methods: {
    /** æµ‹è¯•免登 */
    handleAutoLogin() {
      this.$refs["form"].validate(valid => {
        if (valid) {
          // è°ƒç”¨ä¼ä¸šå¾®ä¿¡å…ç™»æŽ¥å£
          this.$axios
            .post("/system/qywechat/autoLogin", { code: this.form.code })
            .then(response => {
              this.loginResult = response;
              if (response.code === 200) {
                this.$modal.msgSuccess("免登成功");
              } else {
                this.$modal.msgError(response.msg || "免登失败");
              }
            })
            .catch(error => {
              this.loginResult = error;
              this.$modal.msgError("请求异常:" + error.message);
            });
        }
      });
    },
    /** é‡ç½®è¡¨å• */
    handleReset() {
      this.$refs["form"].resetFields();
      this.loginResult = null;
    }
  }
};
</script>
<style lang="scss" scoped>
.form-tip {
  font-size: 12px;
  color: #999;
  margin-top: 5px;
}
.result-card {
  background-color: #f5f5f5;
  pre {
    white-space: pre-wrap;
    word-wrap: break-word;
    margin: 0;
  }
}
</style>
Æóҵ΢ÐÅÃâµÇ¹¦ÄÜʹÓÃ˵Ã÷.md
New file
@@ -0,0 +1,155 @@
# ä¼ä¸šå¾®ä¿¡å…ç™»åŠŸèƒ½ä½¿ç”¨è¯´æ˜Ž
## åŠŸèƒ½æ¦‚è¿°
企业微信免登功能允许用户通过企业微信客户端直接登录系统,无需输入用户名和密码,提升用户体验。
## å®žçŽ°åŽŸç†
1. ç”¨æˆ·åœ¨ä¼ä¸šå¾®ä¿¡å®¢æˆ·ç«¯ä¸­è®¿é—®ç³»ç»ŸURL
2. ç³»ç»Ÿæ£€æµ‹åˆ°ä¼ä¸šå¾®ä¿¡çŽ¯å¢ƒï¼Œè‡ªåŠ¨è·³è½¬åˆ°ä¼ä¸šå¾®ä¿¡æŽˆæƒé¡µé¢
3. ç”¨æˆ·ç¡®è®¤æŽˆæƒåŽï¼Œä¼ä¸šå¾®ä¿¡è¿”回授权code
4. ç³»ç»Ÿé€šè¿‡code获取用户信息并完成登录
## æ–‡ä»¶ç»“æž„
```
前端:
- app/pages/qylogin.vue          # ç§»åŠ¨ç«¯å…ç™»é¡µé¢
- ruoyi-ui/src/views/system/qywechat/autologin.vue  # PC端免登测试页面
- ruoyi-ui/src/router/modules/qywechat.js  # è·¯ç”±é…ç½®
后端:
- ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/QyWechatLoginController.java  # å…ç™»æŽ§åˆ¶å™¨
- ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java  # ç”¨æˆ·Mapper接口
- ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml  # ç”¨æˆ·Mapper XML
- ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java  # ç”¨æˆ·æœåŠ¡æŽ¥å£
- ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java  # ç”¨æˆ·æœåŠ¡å®žçŽ°
```
## é…ç½®è¦æ±‚
### 1. ä¼ä¸šå¾®ä¿¡é…ç½®
在系统配置中添加以下配置项:
```sql
-- ä¼ä¸šå¾®ä¿¡å¯ç”¨å¼€å…³
INSERT INTO sys_config VALUES (NULL, '企业微信启用开关', 'qy_wechat.enable', 'true', 'Y', '系统内置', '是否启用企业微信功能', '1', '1', '2025-12-14 10:00:00', 'admin', '2025-12-14 10:00:00', 'admin', NULL);
-- ä¼ä¸šå¾®ä¿¡CorpID
INSERT INTO sys_config VALUES (NULL, '企业微信CorpID', 'qy_wechat.corp_id', 'your_corp_id', 'Y', '系统内置', '企业微信企业ID', '1', '1', '2025-12-14 10:00:00', 'admin', '2025-12-14 10:00:00', 'admin', NULL);
-- ä¼ä¸šå¾®ä¿¡åº”用Secret(用于获取用户信息)
INSERT INTO sys_config VALUES (NULL, '企业微信应用Secret', 'qy_wechat.corp_secret', 'your_corp_secret', 'Y', '系统内置', '企业微信应用密钥', '1', '1', '2025-12-14 10:00:00', 'admin', '2025-12-14 10:00:00', 'admin', NULL);
-- ä¼ä¸šå¾®ä¿¡å°ç¨‹åºSecret(用于获取AccessToken)
INSERT INTO sys_config VALUES (NULL, '企业微信小程序Secret', 'qy_wechat.miniprogram_secret', 'your_miniprogram_secret', 'Y', '系统内置', '企业微信关联小程序密钥', '1', '1', '2025-12-14 10:00:00', 'admin', '2025-12-14 10:00:00', 'admin', NULL);
-- ä¼ä¸šå¾®ä¿¡AgentId
INSERT INTO sys_config VALUES (NULL, '企业微信AgentId', 'qy_wechat.agent_id', 'your_agent_id', 'Y', '系统内置', '企业微信应用ID', '1', '1', '2025-12-14 10:00:00', 'admin', '2025-12-14 10:00:00', 'admin', NULL);
```
### 2. ç”¨æˆ·ç»‘定
需要将系统用户与企业微信用户进行绑定,在`sys_user`表中设置`qy_wechat_user_id`字段。
## ä½¿ç”¨æµç¨‹
### ç§»åŠ¨ç«¯ä½¿ç”¨
1. ç”¨æˆ·åœ¨ä¼ä¸šå¾®ä¿¡ä¸­æ‰“开系统URL
2. ç³»ç»Ÿè‡ªåŠ¨è·³è½¬åˆ°å…ç™»é¡µé¢ `/pages/qylogin`
3. é¡µé¢æ£€æµ‹ä¼ä¸šå¾®ä¿¡çŽ¯å¢ƒå¹¶èŽ·å–æŽˆæƒcode
4. è°ƒç”¨åŽç«¯å…ç™»æŽ¥å£å®Œæˆç™»å½•
### PC端测试
1. ç™»å½•系统后台
2. è¿›å…¥ã€ä¼ä¸šå¾®ä¿¡ã€‘->【免登测试】菜单
3. è¾“入企业微信授权code进行测试
## æŽ¥å£è¯´æ˜Ž
### å…ç™»æŽ¥å£
```
POST /system/qywechat/autoLogin
请求参数:
{
  "code": "企业微信授权code"
}
响应结果:
{
  "code": 200,
  "msg": "登录成功",
  "data": {
    "token": "登录令牌",
    "user": {
      // ç”¨æˆ·ä¿¡æ¯
    }
  }
}
```
## æ³¨æ„äº‹é¡¹
1. ä¼ä¸šå¾®ä¿¡å…ç™»åŠŸèƒ½ä»…æ”¯æŒåœ¨ä¼ä¸šå¾®ä¿¡å®¢æˆ·ç«¯ä¸­ä½¿ç”¨
2. éœ€è¦æ­£ç¡®é…ç½®ä¼ä¸šå¾®ä¿¡åº”用的可信域名
3. ç”¨æˆ·å¿…须先绑定企业微信账号才能使用免登功能
4. ç¡®ä¿æœåŠ¡å™¨èƒ½å¤Ÿè®¿é—®ä¼ä¸šå¾®ä¿¡API接口
## å¸¸è§é—®é¢˜
### 1. æŽˆæƒå¤±è´¥
**问题现象**:提示"获取用户信息失败"
**解决方案**:
- æ£€æŸ¥ä¼ä¸šå¾®ä¿¡é…ç½®æ˜¯å¦æ­£ç¡®
- ç¡®è®¤åº”用的可信域名配置
- æ£€æŸ¥ç½‘络是否能访问企业微信API
### 2. ç”¨æˆ·æœªç»‘定
**问题现象**:提示"该企业微信账号未绑定系统用户"
**解决方案**:
- åœ¨ç”¨æˆ·ç®¡ç†ä¸­æ‰¾åˆ°å¯¹åº”用户
- è®¾ç½®`qy_wechat_user_id`字段为企业微信用户ID
### 3. ç™»å½•异常
**问题现象**:提示"登录异常"
**解决方案**:
- æŸ¥çœ‹ç³»ç»Ÿæ—¥å¿—定位具体错误
- æ£€æŸ¥ç”¨æˆ·çŠ¶æ€æ˜¯å¦æ­£å¸¸
- ç¡®è®¤ç³»ç»Ÿé…ç½®æ˜¯å¦å®Œæ•´
## æ‰©å±•功能
### 1. è‡ªåŠ¨è·³è½¬é…ç½®
可以在前端页面中配置自动跳转逻辑:
```javascript
// æ£€æŸ¥ä¼ä¸šå¾®ä¿¡çŽ¯å¢ƒ
isWxWorkEnvironment() {
  const userAgent = navigator.userAgent.toLowerCase()
  return userAgent.includes('wxwork')
}
```
### 2. ç”¨æˆ·ç»‘定接口
可以开发用户绑定企业微信的接口:
```java
@PostMapping("/bind")
public AjaxResult bindQyWechatUser(@RequestBody BindRequest request) {
  // å®žçŽ°ç”¨æˆ·ç»‘å®šé€»è¾‘
}
```