/** * 飞书用户管理器模块 * * 功能说明: * - 负责飞书OAuth2登录用户的获取或创建 * - SessionToken管理 * - 用户信息同步和更新 * * @module user-manager * @author fmode * @date 2026 */ import crypto from 'crypto'; import type { FeishuConfig } from './config.ts'; import type { FeishuUserInfo } from './client.ts'; /** * 用户会话信息接口 */ export interface UserSessionInfo { user: any; sessionToken: any; userInfo: { objectId: string; username: string; nickname: string; mobile?: string; email?: string; avatar: string; sessionToken: string; }; } /** * 飞书用户管理器类 * 负责用户的查找、创建、更新和sessionToken管理 */ export class FeishuUserManager { private configManager: any; /** * 构造函数 * @param configManager - 配置管理器实例(必填) */ constructor(configManager: FeishuConfig) { this.configManager = configManager; } /** * 根据飞书用户信息获取或创建用户,并生成sessionToken * * @async * @param userInfo - 飞书用户信息 * @param appId - 飞书应用ID * @returns Promise * @throws {Error} 处理失败时抛出错误 */ async getUserInfoSessionToken(userInfo: FeishuUserInfo, appId: string): Promise { try { console.log(`处理飞书用户登录,unionId: ${userInfo.unionId}, openId: ${userInfo.openId}`); let user = await this.findOrCreateUser(userInfo); const sessionToken = await this.generateSessionToken(user); await this.saveUserData(user, userInfo); console.log(`用户登录成功,userId: ${user.id}, username: ${user.get('username')}`); return { user: user, sessionToken: sessionToken, userInfo: { objectId: user.id, username: user.get('username'), nickname: user.get('nickname'), mobile: user.get('mobile'), email: user.get('email'), avatar: user.get('avatar'), sessionToken: sessionToken.get('sessionToken'), } }; } catch (error) { console.error('获取用户sessionToken失败:', error); throw error; } } /** * 查找或创建用户 * * @async * @param userInfo - 飞书用户信息 * @returns Promise Parse用户对象 * @throws {Error} 查找或创建失败时抛出错误 */ async findOrCreateUser(userInfo: FeishuUserInfo): Promise { try { let username = `feishu_${userInfo.openId}`; let user = await this.findUserByUnionId(userInfo.unionId); if (!user) { user = await this.findUserByOpenId(userInfo.openId); } if (!user) { user = await this.findUserByUsername(username); } if (!user && userInfo.mobile) { user = await this.findUserByMobile(userInfo.mobile); } if (user) { console.log(`找到现有用户,更新信息: ${user.id}`); await this.updateUserInfo(user, userInfo); return user; } console.log(`创建新用户,unionId: ${userInfo.unionId}`); return await this.createNewUser(userInfo); } catch (error) { console.error('查找或创建用户失败:', error); throw error; } } /** * 生成用户默认密码 * * @param username - 用户名 * @returns 默认密码 */ generateDefaultPassword(username: string): string { if (!username || username.length < 6) { return username.padStart(6, '0'); } return username.slice(-6); } /** * 通过unionId查找用户 * * @async * @param unionId - 飞书用户unionId * @returns Promise */ async findUserByUnionId(unionId: string): Promise { try { const Parse = (global as any).Parse; if (!Parse) { throw new Error('Parse不可用'); } const query = new Parse.Query("_User"); query.equalTo("data.feishu.unionId", unionId); return await query.first({ useMasterKey: true }); } catch (error) { console.error('通过unionId查找用户失败:', error); return null; } } /** * 通过openId查找用户 * * @async * @param openId - 飞书用户openId * @returns Promise */ async findUserByOpenId(openId: string): Promise { try { const Parse = (global as any).Parse; if (!Parse) { throw new Error('Parse不可用'); } const query = new Parse.Query("_User"); query.equalTo("data.feishu.openId", openId); return await query.first({ useMasterKey: true }); } catch (error) { console.error('通过openId查找用户失败:', error); return null; } } /** * 通过用户名查找用户 * * @async * @param username - 用户名 * @returns Promise */ async findUserByUsername(username: string): Promise { try { const Parse = (global as any).Parse; if (!Parse) { throw new Error('Parse不可用'); } const query = new Parse.Query("_User"); query.equalTo("username", username); return await query.first({ useMasterKey: true }); } catch (error) { console.error('通过username查找用户失败:', error); return null; } } /** * 通过手机号查找用户 * * @async * @param mobile - 手机号 * @returns Promise */ async findUserByMobile(mobile: string): Promise { try { const Parse = (global as any).Parse; if (!Parse) { throw new Error('Parse不可用'); } const query = new Parse.Query("_User"); query.equalTo("mobile", mobile); return await query.first({ useMasterKey: true }); } catch (error) { console.error('通过手机号查找用户失败:', error); return null; } } /** * 更新现有用户信息 * * @async * @param user - Parse用户对象 * @param userInfo - 飞书用户信息 * @returns Promise * @throws {Error} 更新失败或用户被冻结时抛出错误 */ async updateUserInfo(user: any, userInfo: FeishuUserInfo): Promise { try { if (userInfo.name) user.set('nickname', userInfo.name); if (userInfo.mobile) user.set('mobile', userInfo.mobile); if (userInfo.avatarUrl) user.set('avatar', userInfo.avatarUrl); const data = user.get('data') || {}; data.feishu = { unionId: userInfo.unionId, openId: userInfo.openId, name: userInfo.name, enName: userInfo.enName, avatarUrl: userInfo.avatarUrl, mobile: userInfo.mobile, email: userInfo.email, userId: userInfo.userId, lastLoginAt: new Date(), updatedAt: new Date() }; user.set('data', data); if (user.get('status') === 'freeze') { throw new Error('该账户已被冻结'); } await user.save(null, { useMasterKey: true }); console.log(`用户信息更新成功: ${user.id}`); } catch (error) { console.error('更新用户信息失败:', error); throw error; } } /** * 创建新用户 * * @async * @param userInfo - 飞书用户信息 * @returns Promise Parse用户对象 * @throws {Error} 创建失败时抛出错误 */ async createNewUser(userInfo: FeishuUserInfo): Promise { try { const Parse = (global as any).Parse; if (!Parse) { throw new Error('Parse不可用'); } const UserClass = Parse.Object.extend("_User"); const user = new UserClass(); const username = `feishu_${userInfo.openId}`; const defaultPassword = this.generateDefaultPassword(username); user.set("username", username); user.set("password", defaultPassword); user.set("nickname", userInfo.name || `飞书用户_${userInfo.openId.slice(-6)}`); user.set("mobile", userInfo.mobile); user.set("avatar", userInfo.avatarUrl || 'https://s1-imfile.feishucdn.com/static-resource/v1/default_avatar.png'); user.set("type", 'user'); user.set("status", 'normal'); user.set("data", { feishu: { unionId: userInfo.unionId, openId: userInfo.openId, name: userInfo.name, enName: userInfo.enName, avatarUrl: userInfo.avatarUrl, mobile: userInfo.mobile, email: userInfo.email, userId: userInfo.userId, createdAt: new Date(), lastLoginAt: new Date(), } }); await user.save(null, { useMasterKey: true }); console.log(`新用户创建成功: ${user.id}, username: ${username}, 默认密码: ${defaultPassword}`); return user; } catch (error) { console.error('创建新用户失败:', error); throw error; } } /** * 生成sessionToken(优化版:复用未过期的token) * * @async * @param user - Parse用户对象 * @returns Promise Session对象 * @throws {Error} 生成失败时抛出错误 */ async generateSessionToken(user: any): Promise { try { const Parse = (global as any).Parse; if (!Parse) { throw new Error('Parse不可用'); } // 查询用户现有的有效 sessionToken(剩余有效期大于2小时) const twoHoursLater = new Date(); twoHoursLater.setHours(twoHoursLater.getHours() + 2); const query = new Parse.Query('_Session'); query.equalTo('user', { __type: 'Pointer', className: '_User', objectId: user.id }); query.greaterThan('expiresAt', twoHoursLater); query.descending('expiresAt'); const existingSession = await query.first({ useMasterKey: true }); // 如果找到剩余有效期大于2小时的 session,直接复用 if (existingSession) { console.log(`复用现有SessionToken for user: ${user.id}`); return existingSession; } // 创建新的 sessionToken const salt = user.id + '_' + (new Date().getTime() / 1000).toFixed(); const md5 = crypto.createHash('md5').update(salt, 'utf8').digest('hex'); const sessionToken = "r:" + md5; console.log(`生成新SessionToken: ${sessionToken} for user: ${user.id}`); const expiresAt = new Date(); expiresAt.setFullYear(expiresAt.getFullYear() + 1); // 使用 REST API 创建 Session,绕过 SDK 的只读属性限制 const serverURL = Parse.serverURL || (globalThis as any).appConfig.serverURL; const appId = Parse.applicationId || (globalThis as any)?.appConfig.appId; const masterKey = Parse.masterKey || (globalThis as any)?.appConfig.masterKey; const response = await fetch(`${serverURL}/classes/_Session`, { method: 'POST', headers: { 'X-Parse-Application-Id': appId, 'X-Parse-Master-Key': masterKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ user: { __type: 'Pointer', className: '_User', objectId: user.id }, sessionToken: sessionToken, expiresAt: { __type: 'Date', iso: expiresAt.toISOString() }, createdWith: { action: "login", authProvider: "feishu" }, restricted: false }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Session创建失败: ${response.status} ${errorText}`); } const data = await response.json(); if (!data || !data.objectId) { throw new Error('Session创建失败:未返回objectId'); } // 查询并返回创建的 Session 对象 const sessionQuery = new Parse.Query('_Session'); sessionQuery.equalTo('objectId', data.objectId); const savedSession = await sessionQuery.first({ useMasterKey: true }); if (!savedSession) { throw new Error('Session查询失败'); } return savedSession; } catch (error) { console.error('生成sessionToken失败:', error); throw error; } } /** * 保存用户附加信息到data字段 * * @async * @param user - Parse用户对象 * @param userInfo - 飞书用户信息 * @returns Promise */ async saveUserData(user: any, userInfo: FeishuUserInfo): Promise { try { const data = user.get('data') || {}; if (!data.feishu) { data.feishu = {}; } data.feishu[userInfo.openId] = { ...userInfo, lastLoginAt: new Date(), updatedAt: new Date() }; data.feishu.unionId = userInfo.unionId; data.feishu.currentOpenId = userInfo.openId; user.set('data', data); await user.save(null, { useMasterKey: true }); console.log(`用户数据保存成功: ${user.id}`); } catch (error) { console.error('保存用户数据失败:', error); } } /** * 验证sessionToken * * @async * @param sessionToken - 会话令牌 * @returns Promise */ async validateSessionToken(sessionToken: string): Promise { try { const Parse = (global as any).Parse; if (!Parse) { throw new Error('Parse不可用'); } const query = new Parse.Query('_Session'); query.equalTo('sessionToken', sessionToken); query.greaterThan('expiresAt', new Date()); query.include('user'); const session = await query.first({ useMasterKey: true }); if (session && session.get('user')) { return session.get('user'); } return null; } catch (error) { console.error('验证sessionToken失败:', error); return null; } } /** * 通过unionId查找所有关联用户 * * @async * @param unionId - 飞书统一ID * @returns Promise */ async findUsersByUnionId(unionId: string): Promise { try { const Parse = (global as any).Parse; if (!Parse) { throw new Error('Parse不可用'); } const query = new Parse.Query("_User"); query.equalTo("data.feishu.unionId", unionId); return await query.find({ useMasterKey: true }); } catch (error) { console.error('通过unionId查找用户失败:', error); return []; } } } /** * 默认导出用户管理器类 */ export default FeishuUserManager;