# WxworkAuthGuard 企微路由守卫使用指南 ## 概述 `WxworkAuthGuard` 是一个用于企业微信页面的路由守卫,提供以下核心功能: 1. **企微授权认证** - 自动处理企业微信 OAuth 授权流程 2. **用户信息同步** - 同步用户信息到 Profile(企业员工)或 UserSocial(外部用户) 3. **自动登录/注册** - 自动创建/登录 Parse `_User` 账号 4. **用户指针同步** - 自动同步 `user` 字段到 Profile/UserSocial ## 核心特性 ### ⭐ 自动登录机制 守卫会自动为通过企微认证的用户创建 Parse `_User` 账号: - **用户名**: `userid`(企业员工)或 `external_userid`(外部用户) - **密码**: userid/external_userid 的 **后6位** - **自动注册**: 用户不存在时自动注册 - **自动登录**: 用户存在时自动登录 - **用户指针**: 自动同步 `user` 字段到 Profile/UserSocial ### 📍 标记位置 守卫中的自动登录逻辑位于: - **文件**: `fmode-ng/src/lib/social/wxwork/wxwork-auth.guard.ts` - **位置**: 第 71-157 行 `autoLogin()` 函数 - **调用点**: 第 21 行和第 66 行(标记为 ⭐) ## 路由配置 ### 基础配置 ```typescript import { WxworkAuthGuard } from 'fmode-ng'; export const routes: Routes = [ { path: ":cid/auth/user-confirm", canActivate: [WxworkAuthGuard], loadComponent: () => import("./page-user-confirm.component") .then((m) => m.PageUserConfirmComponent) } ]; ``` ### 路由参数 守卫会自动从路由中提取以下参数: - **cid** (必需): 公司帐套ID(Company.objectId) - **appId** (可选): 应用ID,默认为 "crm" 支持的参数来源: - `route.paramMap.get("cid")` - 路径参数 - `route.queryParamMap.get("cid")` - 查询参数 ### 配置示例 ```typescript // 带 cid 和 appId 的路由 { path: ":cid/auth/:appId/user-confirm", canActivate: [WxworkAuthGuard], loadComponent: () => ... } // 仅带 cid 的路由(appId 默认为 "crm") { path: ":cid/auth/user-confirm", canActivate: [WxworkAuthGuard], loadComponent: () => ... } // 查询参数方式 // 访问: /some-page?cid=E4KpGvTEto { path: "some-page", canActivate: [WxworkAuthGuard], loadComponent: () => ... } ``` ## WxworkSDK 使用方法 ### 初始化 SDK ```typescript import { WxworkSDK } from 'fmode-ng/core'; // 在组件中初始化 this.wxwork = new WxworkSDK({ cid: this.cid, // 公司帐套ID appId: this.appId // 应用ID,默认 "crm" }); ``` ### ⭐ 核心方法:getCurrentUser() **最简单的方式获取当前用户信息** ```typescript // 获取当前用户的 Profile 或 UserSocial const currentUser = await this.wxwork.getCurrentUser(); if (currentUser) { console.log("用户类型:", currentUser.className); // "Profile" 或 "UserSocial" console.log("用户ID:", currentUser.id); console.log("用户名:", currentUser.get("name")); console.log("手机号:", currentUser.get("mobile")); // Profile 特有字段 if (currentUser.className === "Profile") { console.log("员工ID:", currentUser.get("userid")); console.log("部门:", currentUser.get("department")); console.log("职位:", currentUser.get("position")); } // UserSocial 特有字段 if (currentUser.className === "UserSocial") { console.log("外部用户ID:", currentUser.get("data").external_userid); console.log("外部用户类型:", currentUser.get("type")); } } ``` ### 其他常用方法 #### getUserinfo() - 获取缓存的授权信息 ```typescript // 从 localStorage 获取 USERINFO const userInfo = await this.wxwork.getUserinfo(); console.log(userInfo.userid); // 企业员工ID console.log(userInfo.external_userid); // 外部用户ID console.log(userInfo.mobile); // 手机号 console.log(userInfo.name); // 姓名 console.log(userInfo.avatar); // 头像 ``` **存储位置**: `localStorage[${cid}/USERINFO]` #### getContactOrProfile() - 查询或创建用户记录 ```typescript const userInfo = await this.wxwork.getUserinfo(); const profileOrSocial = await this.wxwork.getContactOrProfile(userInfo); console.log(profileOrSocial.className); // "Profile" 或 "UserSocial" console.log(profileOrSocial.id); ``` ## Parse.User.current() 使用 由于守卫自动登录,任何挂载守卫的页面都可以直接使用: ```typescript import { FmodeParse } from 'fmode-ng/parse'; const Parse = FmodeParse.with("nova"); // 获取当前登录的 _User const currentUser = Parse.User.current(); if (currentUser) { console.log("用户名:", currentUser.get("username")); // userid 或 external_userid console.log("手机号:", currentUser.get("mobilePhoneNumber")); console.log("邮箱:", currentUser.get("email")); // 获取关联的 Profile/UserSocial const wxwork = new WxworkSDK({ cid: this.cid, appId: "crm" }); const profileOrSocial = await wxwork.getCurrentUser(); console.log("关联用户:", profileOrSocial.get("name")); } ``` ## 页面开发最佳实践 ### 推荐方式:使用 getCurrentUser() ```typescript export class MyPageComponent implements OnInit { cid: string = ''; appId: string = ''; wxwork: WxworkSDK; currentUser: FmodeObject | null = null; constructor(private route: ActivatedRoute) {} async ngOnInit() { this.route.paramMap.subscribe(async params => { this.cid = params.get('cid') || ''; this.appId = params.get('appId') || 'crm'; // ⭐ 最简单的方式:直接获取当前用户 this.wxwork = new WxworkSDK({ cid: this.cid, appId: this.appId }); this.currentUser = await this.wxwork.getCurrentUser(); if (this.currentUser) { console.log("用户类型:", this.currentUser.className); console.log("用户姓名:", this.currentUser.get("name")); // 直接使用用户信息 this.loadUserData(); } }); } async loadUserData() { // 使用 this.currentUser 进行业务逻辑 if (this.currentUser.className === "Profile") { // 企业员工逻辑 } else { // 外部用户逻辑 } } } ``` ### 传统方式对比 ```typescript // ❌ 不推荐:手动查询(代码冗长) async loadUserInfo() { const userInfo = await this.wxwork.getUserinfo(); if (userInfo.userid) { const query = new Parse.Query('Profile'); query.equalTo('userid', userInfo.userid); const profile = await query.first(); // ... 处理 Profile } else if (userInfo.external_userid) { const query = new Parse.Query('UserSocial'); query.equalTo('data.external_userid', userInfo.external_userid); const userSocial = await query.first(); // ... 处理 UserSocial } } // ✅ 推荐:使用 getCurrentUser()(简洁高效) async loadUserInfo() { const currentUser = await this.wxwork.getCurrentUser(); // 直接使用 currentUser,无需判断类型 } ``` ## 数据结构 ### Profile 表(企业员工) | 字段 | 类型 | 说明 | |------|------|------| | userid | String | 企业员工微信ID | | company | Pointer | 所属企业 | | user | Pointer<_User> | 关联的 Parse 用户 | | name | String | 姓名 | | avatar | String | 头像URL | | mobile | String | 手机号 | | email | String | 邮箱 | | department | String | 部门 | | position | String | 职位 | | corp_name | String | 企业名称 | | isVerified | Boolean | 身份确认状态 | ### UserSocial 表(外部用户) | 字段 | 类型 | 说明 | |------|------|------| | data.external_userid | String | 外部用户微信ID | | company | Pointer | 所属企业 | | user | Pointer<_User> | 关联的 Parse 用户 | | name | String | 姓名 | | avatar | String | 头像URL | | mobile | String | 手机号 | | type | String | 外部用户类型 | | corp_name | String | 企业名称 | | isVerified | Boolean | 身份确认状态 | ### _User 表(Parse 用户) | 字段 | 类型 | 说明 | |------|------|------| | username | String | userid 或 external_userid | | password | String | userid/external_userid 后6位 | | mobilePhoneNumber | String | 手机号(同步自企微) | | email | String | 邮箱(同步自企微) | ## 授权流程 ### 1. 移动端(微信环境) ``` 用户访问页面 ↓ 检测到微信环境 ↓ OAuth 跳转授权 ↓ 回调获取 code ↓ 调用 getUserinfo(code) ↓ 同步用户信息到 Profile/UserSocial ↓ 自动登录/注册 _User ↓ 同步 user 指针 ↓ 进入页面 ``` ### 2. PC 端(非微信环境) ``` 用户访问页面 ↓ 弹出企微扫码登录 ↓ 扫码成功获取 code ↓ 调用 getUserinfo(code) ↓ 同步用户信息到 Profile/UserSocial ↓ 自动登录/注册 _User ↓ 同步 user 指针 ↓ 进入页面 ``` ### 3. 已授权用户(有缓存) ``` 用户访问页面 ↓ 从 localStorage 读取 USERINFO ↓ 验证用户信息有效 ↓ 同步用户信息到 Profile/UserSocial ↓ 自动登录(如未登录) ↓ 同步 user 指针 ↓ 直接进入页面(无需重新授权) ``` ## 常见场景示例 ### 场景1:展示当前用户信息 ```typescript async showUserInfo() { const currentUser = await this.wxwork.getCurrentUser(); if (currentUser) { const name = currentUser.get("name"); const avatar = currentUser.get("avatar"); const mobile = currentUser.get("mobile"); console.log(`用户: ${name}, 手机: ${mobile}`); } } ``` ### 场景2:判断用户类型 ```typescript async checkUserType() { const currentUser = await this.wxwork.getCurrentUser(); if (currentUser.className === "Profile") { console.log("这是企业员工"); const userid = currentUser.get("userid"); // 执行员工相关逻辑 } else if (currentUser.className === "UserSocial") { console.log("这是外部用户"); const externalUserId = currentUser.get("data").external_userid; // 执行外部用户相关逻辑 } } ``` ### 场景3:访问 Parse User ```typescript async accessParseUser() { // 方式1:通过 Parse.User.current() const parseUser = Parse.User.current(); console.log("Parse 用户名:", parseUser.get("username")); // 方式2:通过 Profile/UserSocial 的 user 指针 const currentUser = await this.wxwork.getCurrentUser(); const userPointer = currentUser.get("user"); if (userPointer) { const user = await new Parse.Query("_User").get(userPointer.id); console.log("关联的 Parse 用户:", user.get("username")); } } ``` ### 场景4:更新用户信息 ```typescript async updateUserInfo() { const currentUser = await this.wxwork.getCurrentUser(); if (currentUser) { currentUser.set("name", "新名字"); currentUser.set("mobile", "13800138000"); await currentUser.save(); console.log("用户信息更新成功"); } } ``` ### 场景5:判断用户是否已确认身份 ```typescript async checkVerified() { const currentUser = await this.wxwork.getCurrentUser(); if (currentUser.get("isVerified")) { console.log("用户已确认身份"); // 允许访问完整功能 } else { console.log("用户未确认身份"); // 跳转到身份确认页面 this.router.navigate([`/auth/${this.cid}/user-confirm`]); } } ``` ## 开发注意事项 ### 1. 守卫必须配置 cid 参数 ```typescript // ✅ 正确:路径中包含 cid { path: ":cid/my-page", canActivate: [WxworkAuthGuard], ... } // ✅ 正确:查询参数中包含 cid // 访问: /my-page?cid=E4KpGvTEto { path: "my-page", canActivate: [WxworkAuthGuard], ... } // ❌ 错误:缺少 cid 参数 { path: "my-page", canActivate: [WxworkAuthGuard], ... } ``` ### 2. 初始化 SDK 时必须提供 cid ```typescript // ✅ 正确 this.wxwork = new WxworkSDK({ cid: this.cid, appId: this.appId }); // ❌ 错误:缺少 cid this.wxwork = new WxworkSDK({ appId: this.appId }); ``` ### 3. getCurrentUser() 依赖守卫先执行 ```typescript // getCurrentUser() 会读取守卫写入的 localStorage // 必须在守卫执行后调用 async ngOnInit() { // 守卫已执行,localStorage 中有 USERINFO const currentUser = await this.wxwork.getCurrentUser(); } ``` ### 4. Parse.User.current() 可用性 ```typescript // 只有在守卫执行后,Parse.User.current() 才会有值 const currentUser = Parse.User.current(); if (currentUser) { // 用户已自动登录 } else { // 用户未登录(不应该出现在守卫保护的页面) } ``` ### 5. user 指针同步 守卫会自动同步 `user` 字段,但如果需要手动同步: ```typescript async syncUserPointer() { const parseUser = Parse.User.current(); const currentUser = await this.wxwork.getCurrentUser(); if (parseUser && currentUser && !currentUser.get("user")) { currentUser.set("user", parseUser.toPointer()); await currentUser.save(); } } ``` ## 调试技巧 ### 1. 查看 localStorage 中的 USERINFO ```typescript const userInfoStr = localStorage.getItem(`${this.cid}/USERINFO`); const userInfo = JSON.parse(userInfoStr); console.log("缓存的用户信息:", userInfo); ``` ### 2. 查看当前登录用户 ```typescript const currentUser = Parse.User.current(); console.log("Parse User:", currentUser?.toJSON()); ``` ### 3. 查看 Profile/UserSocial ```typescript const currentUser = await this.wxwork.getCurrentUser(); console.log("Profile/UserSocial:", currentUser?.toJSON()); ``` ### 4. 清除缓存重新授权 ```typescript // 清除用户信息缓存 localStorage.removeItem(`${this.cid}/USERINFO`); // 退出登录 await Parse.User.logOut(); // 刷新页面重新授权 location.reload(); ``` ## 安全性说明 ### 密码生成规则 - **规则**: 使用 userid/external_userid 的后6位作为密码 - **示例**: - userid: `ZhangSan123456` → 密码: `123456` - external_userid: `wmABCDEF123456` → 密码: `123456` ### 安全建议 1. **企业内部使用**: 该机制适用于企业内部系统,通过企微授权保证安全性 2. **不建议外部暴露**: 不要将密码规则暴露给外部用户 3. **升级密码策略**: 如需更高安全性,可修改 `autoLogin()` 函数的密码生成逻辑 4. **ACL 控制**: 为 Profile/UserSocial 配置合适的 ACL 权限 ## 总结 使用 `WxworkAuthGuard` 和 `WxworkSDK.getCurrentUser()` 可以极大简化企微页面开发: ✅ **一行代码获取用户** - `await this.wxwork.getCurrentUser()` ✅ **自动登录** - 无需手动处理 Parse 用户登录 ✅ **统一接口** - Profile 和 UserSocial 统一处理 ✅ **用户指针同步** - 自动关联 Parse _User ✅ **Parse.User.current() 可用** - 方便其他模块使用 开发企微功能页面时,只需配置守卫,然后使用 `getCurrentUser()` 即可快速开始业务逻辑开发!