user-manager.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. /**
  2. * 飞书用户管理器模块
  3. *
  4. * 功能说明:
  5. * - 负责飞书OAuth2登录用户的获取或创建
  6. * - SessionToken管理
  7. * - 用户信息同步和更新
  8. *
  9. * @module user-manager
  10. * @author fmode
  11. * @date 2026
  12. */
  13. import crypto from 'crypto';
  14. import type { FeishuConfig } from './config.ts';
  15. import type { FeishuUserInfo } from './client.ts';
  16. /**
  17. * 用户会话信息接口
  18. */
  19. export interface UserSessionInfo {
  20. user: any;
  21. sessionToken: any;
  22. userInfo: {
  23. objectId: string;
  24. username: string;
  25. nickname: string;
  26. mobile?: string;
  27. email?: string;
  28. avatar: string;
  29. sessionToken: string;
  30. };
  31. }
  32. /**
  33. * 飞书用户管理器类
  34. * 负责用户的查找、创建、更新和sessionToken管理
  35. */
  36. export class FeishuUserManager {
  37. private configManager: any;
  38. /**
  39. * 构造函数
  40. * @param configManager - 配置管理器实例(必填)
  41. */
  42. constructor(configManager: FeishuConfig) {
  43. this.configManager = configManager;
  44. }
  45. /**
  46. * 根据飞书用户信息获取或创建用户,并生成sessionToken
  47. *
  48. * @async
  49. * @param userInfo - 飞书用户信息
  50. * @param appId - 飞书应用ID
  51. * @returns Promise<UserSessionInfo>
  52. * @throws {Error} 处理失败时抛出错误
  53. */
  54. async getUserInfoSessionToken(userInfo: FeishuUserInfo, appId: string): Promise<UserSessionInfo> {
  55. try {
  56. console.log(`处理飞书用户登录,unionId: ${userInfo.unionId}, openId: ${userInfo.openId}`);
  57. let user = await this.findOrCreateUser(userInfo);
  58. const sessionToken = await this.generateSessionToken(user);
  59. await this.saveUserData(user, userInfo);
  60. console.log(`用户登录成功,userId: ${user.id}, username: ${user.get('username')}`);
  61. return {
  62. user: user,
  63. sessionToken: sessionToken,
  64. userInfo: {
  65. objectId: user.id,
  66. username: user.get('username'),
  67. nickname: user.get('nickname'),
  68. mobile: user.get('mobile'),
  69. email: user.get('email'),
  70. avatar: user.get('avatar'),
  71. sessionToken: sessionToken.get('sessionToken'),
  72. }
  73. };
  74. } catch (error) {
  75. console.error('获取用户sessionToken失败:', error);
  76. throw error;
  77. }
  78. }
  79. /**
  80. * 查找或创建用户
  81. *
  82. * @async
  83. * @param userInfo - 飞书用户信息
  84. * @returns Promise<any> Parse用户对象
  85. * @throws {Error} 查找或创建失败时抛出错误
  86. */
  87. async findOrCreateUser(userInfo: FeishuUserInfo): Promise<any> {
  88. try {
  89. let username = `feishu_${userInfo.openId}`;
  90. let user = await this.findUserByUnionId(userInfo.unionId);
  91. if (!user) {
  92. user = await this.findUserByOpenId(userInfo.openId);
  93. }
  94. if (!user) {
  95. user = await this.findUserByUsername(username);
  96. }
  97. if (!user && userInfo.mobile) {
  98. user = await this.findUserByMobile(userInfo.mobile);
  99. }
  100. if (user) {
  101. console.log(`找到现有用户,更新信息: ${user.id}`);
  102. await this.updateUserInfo(user, userInfo);
  103. return user;
  104. }
  105. console.log(`创建新用户,unionId: ${userInfo.unionId}`);
  106. return await this.createNewUser(userInfo);
  107. } catch (error) {
  108. console.error('查找或创建用户失败:', error);
  109. throw error;
  110. }
  111. }
  112. /**
  113. * 生成用户默认密码
  114. *
  115. * @param username - 用户名
  116. * @returns 默认密码
  117. */
  118. generateDefaultPassword(username: string): string {
  119. if (!username || username.length < 6) {
  120. return username.padStart(6, '0');
  121. }
  122. return username.slice(-6);
  123. }
  124. /**
  125. * 通过unionId查找用户
  126. *
  127. * @async
  128. * @param unionId - 飞书用户unionId
  129. * @returns Promise<any | null>
  130. */
  131. async findUserByUnionId(unionId: string): Promise<any | null> {
  132. try {
  133. const Parse = (global as any).Parse;
  134. if (!Parse) {
  135. throw new Error('Parse不可用');
  136. }
  137. const query = new Parse.Query("_User");
  138. query.equalTo("data.feishu.unionId", unionId);
  139. return await query.first({ useMasterKey: true });
  140. } catch (error) {
  141. console.error('通过unionId查找用户失败:', error);
  142. return null;
  143. }
  144. }
  145. /**
  146. * 通过openId查找用户
  147. *
  148. * @async
  149. * @param openId - 飞书用户openId
  150. * @returns Promise<any | null>
  151. */
  152. async findUserByOpenId(openId: string): Promise<any | null> {
  153. try {
  154. const Parse = (global as any).Parse;
  155. if (!Parse) {
  156. throw new Error('Parse不可用');
  157. }
  158. const query = new Parse.Query("_User");
  159. query.equalTo("data.feishu.openId", openId);
  160. return await query.first({ useMasterKey: true });
  161. } catch (error) {
  162. console.error('通过openId查找用户失败:', error);
  163. return null;
  164. }
  165. }
  166. /**
  167. * 通过用户名查找用户
  168. *
  169. * @async
  170. * @param username - 用户名
  171. * @returns Promise<any | null>
  172. */
  173. async findUserByUsername(username: string): Promise<any | null> {
  174. try {
  175. const Parse = (global as any).Parse;
  176. if (!Parse) {
  177. throw new Error('Parse不可用');
  178. }
  179. const query = new Parse.Query("_User");
  180. query.equalTo("username", username);
  181. return await query.first({ useMasterKey: true });
  182. } catch (error) {
  183. console.error('通过username查找用户失败:', error);
  184. return null;
  185. }
  186. }
  187. /**
  188. * 通过手机号查找用户
  189. *
  190. * @async
  191. * @param mobile - 手机号
  192. * @returns Promise<any | null>
  193. */
  194. async findUserByMobile(mobile: string): Promise<any | null> {
  195. try {
  196. const Parse = (global as any).Parse;
  197. if (!Parse) {
  198. throw new Error('Parse不可用');
  199. }
  200. const query = new Parse.Query("_User");
  201. query.equalTo("mobile", mobile);
  202. return await query.first({ useMasterKey: true });
  203. } catch (error) {
  204. console.error('通过手机号查找用户失败:', error);
  205. return null;
  206. }
  207. }
  208. /**
  209. * 更新现有用户信息
  210. *
  211. * @async
  212. * @param user - Parse用户对象
  213. * @param userInfo - 飞书用户信息
  214. * @returns Promise<void>
  215. * @throws {Error} 更新失败或用户被冻结时抛出错误
  216. */
  217. async updateUserInfo(user: any, userInfo: FeishuUserInfo): Promise<void> {
  218. try {
  219. if (userInfo.name) user.set('nickname', userInfo.name);
  220. if (userInfo.mobile) user.set('mobile', userInfo.mobile);
  221. if (userInfo.avatarUrl) user.set('avatar', userInfo.avatarUrl);
  222. const data = user.get('data') || {};
  223. data.feishu = {
  224. unionId: userInfo.unionId,
  225. openId: userInfo.openId,
  226. name: userInfo.name,
  227. enName: userInfo.enName,
  228. avatarUrl: userInfo.avatarUrl,
  229. mobile: userInfo.mobile,
  230. email: userInfo.email,
  231. userId: userInfo.userId,
  232. lastLoginAt: new Date(),
  233. updatedAt: new Date()
  234. };
  235. user.set('data', data);
  236. if (user.get('status') === 'freeze') {
  237. throw new Error('该账户已被冻结');
  238. }
  239. await user.save(null, { useMasterKey: true });
  240. console.log(`用户信息更新成功: ${user.id}`);
  241. } catch (error) {
  242. console.error('更新用户信息失败:', error);
  243. throw error;
  244. }
  245. }
  246. /**
  247. * 创建新用户
  248. *
  249. * @async
  250. * @param userInfo - 飞书用户信息
  251. * @returns Promise<any> Parse用户对象
  252. * @throws {Error} 创建失败时抛出错误
  253. */
  254. async createNewUser(userInfo: FeishuUserInfo): Promise<any> {
  255. try {
  256. const Parse = (global as any).Parse;
  257. if (!Parse) {
  258. throw new Error('Parse不可用');
  259. }
  260. const UserClass = Parse.Object.extend("_User");
  261. const user = new UserClass();
  262. const username = `feishu_${userInfo.openId}`;
  263. const defaultPassword = this.generateDefaultPassword(username);
  264. user.set("username", username);
  265. user.set("password", defaultPassword);
  266. user.set("nickname", userInfo.name || `飞书用户_${userInfo.openId.slice(-6)}`);
  267. user.set("mobile", userInfo.mobile);
  268. user.set("avatar", userInfo.avatarUrl || 'https://s1-imfile.feishucdn.com/static-resource/v1/default_avatar.png');
  269. user.set("type", 'user');
  270. user.set("status", 'normal');
  271. user.set("data", {
  272. feishu: {
  273. unionId: userInfo.unionId,
  274. openId: userInfo.openId,
  275. name: userInfo.name,
  276. enName: userInfo.enName,
  277. avatarUrl: userInfo.avatarUrl,
  278. mobile: userInfo.mobile,
  279. email: userInfo.email,
  280. userId: userInfo.userId,
  281. createdAt: new Date(),
  282. lastLoginAt: new Date(),
  283. }
  284. });
  285. await user.save(null, { useMasterKey: true });
  286. console.log(`新用户创建成功: ${user.id}, username: ${username}, 默认密码: ${defaultPassword}`);
  287. return user;
  288. } catch (error) {
  289. console.error('创建新用户失败:', error);
  290. throw error;
  291. }
  292. }
  293. /**
  294. * 生成sessionToken(优化版:复用未过期的token)
  295. *
  296. * @async
  297. * @param user - Parse用户对象
  298. * @returns Promise<any> Session对象
  299. * @throws {Error} 生成失败时抛出错误
  300. */
  301. async generateSessionToken(user: any): Promise<any> {
  302. try {
  303. const Parse = (global as any).Parse;
  304. if (!Parse) {
  305. throw new Error('Parse不可用');
  306. }
  307. // 查询用户现有的有效 sessionToken(剩余有效期大于2小时)
  308. const twoHoursLater = new Date();
  309. twoHoursLater.setHours(twoHoursLater.getHours() + 2);
  310. const query = new Parse.Query('_Session');
  311. query.equalTo('user', {
  312. __type: 'Pointer',
  313. className: '_User',
  314. objectId: user.id
  315. });
  316. query.greaterThan('expiresAt', twoHoursLater);
  317. query.descending('expiresAt');
  318. const existingSession = await query.first({ useMasterKey: true });
  319. // 如果找到剩余有效期大于2小时的 session,直接复用
  320. if (existingSession) {
  321. console.log(`复用现有SessionToken for user: ${user.id}`);
  322. return existingSession;
  323. }
  324. // 创建新的 sessionToken
  325. const salt = user.id + '_' + (new Date().getTime() / 1000).toFixed();
  326. const md5 = crypto.createHash('md5').update(salt, 'utf8').digest('hex');
  327. const sessionToken = "r:" + md5;
  328. console.log(`生成新SessionToken: ${sessionToken} for user: ${user.id}`);
  329. const expiresAt = new Date();
  330. expiresAt.setFullYear(expiresAt.getFullYear() + 1);
  331. // 使用 REST API 创建 Session,绕过 SDK 的只读属性限制
  332. const serverURL = Parse.serverURL || (globalThis as any).appConfig.serverURL;
  333. const appId = Parse.applicationId || (globalThis as any)?.appConfig.appId;
  334. const masterKey = Parse.masterKey || (globalThis as any)?.appConfig.masterKey;
  335. const response = await fetch(`${serverURL}/classes/_Session`, {
  336. method: 'POST',
  337. headers: {
  338. 'X-Parse-Application-Id': appId,
  339. 'X-Parse-Master-Key': masterKey,
  340. 'Content-Type': 'application/json'
  341. },
  342. body: JSON.stringify({
  343. user: {
  344. __type: 'Pointer',
  345. className: '_User',
  346. objectId: user.id
  347. },
  348. sessionToken: sessionToken,
  349. expiresAt: {
  350. __type: 'Date',
  351. iso: expiresAt.toISOString()
  352. },
  353. createdWith: {
  354. action: "login",
  355. authProvider: "feishu"
  356. },
  357. restricted: false
  358. })
  359. });
  360. if (!response.ok) {
  361. const errorText = await response.text();
  362. throw new Error(`Session创建失败: ${response.status} ${errorText}`);
  363. }
  364. const data = await response.json();
  365. if (!data || !data.objectId) {
  366. throw new Error('Session创建失败:未返回objectId');
  367. }
  368. // 查询并返回创建的 Session 对象
  369. const sessionQuery = new Parse.Query('_Session');
  370. sessionQuery.equalTo('objectId', data.objectId);
  371. const savedSession = await sessionQuery.first({ useMasterKey: true });
  372. if (!savedSession) {
  373. throw new Error('Session查询失败');
  374. }
  375. return savedSession;
  376. } catch (error) {
  377. console.error('生成sessionToken失败:', error);
  378. throw error;
  379. }
  380. }
  381. /**
  382. * 保存用户附加信息到data字段
  383. *
  384. * @async
  385. * @param user - Parse用户对象
  386. * @param userInfo - 飞书用户信息
  387. * @returns Promise<void>
  388. */
  389. async saveUserData(user: any, userInfo: FeishuUserInfo): Promise<void> {
  390. try {
  391. const data = user.get('data') || {};
  392. if (!data.feishu) {
  393. data.feishu = {};
  394. }
  395. data.feishu[userInfo.openId] = {
  396. ...userInfo,
  397. lastLoginAt: new Date(),
  398. updatedAt: new Date()
  399. };
  400. data.feishu.unionId = userInfo.unionId;
  401. data.feishu.currentOpenId = userInfo.openId;
  402. user.set('data', data);
  403. await user.save(null, { useMasterKey: true });
  404. console.log(`用户数据保存成功: ${user.id}`);
  405. } catch (error) {
  406. console.error('保存用户数据失败:', error);
  407. }
  408. }
  409. /**
  410. * 验证sessionToken
  411. *
  412. * @async
  413. * @param sessionToken - 会话令牌
  414. * @returns Promise<any | null>
  415. */
  416. async validateSessionToken(sessionToken: string): Promise<any | null> {
  417. try {
  418. const Parse = (global as any).Parse;
  419. if (!Parse) {
  420. throw new Error('Parse不可用');
  421. }
  422. const query = new Parse.Query('_Session');
  423. query.equalTo('sessionToken', sessionToken);
  424. query.greaterThan('expiresAt', new Date());
  425. query.include('user');
  426. const session = await query.first({ useMasterKey: true });
  427. if (session && session.get('user')) {
  428. return session.get('user');
  429. }
  430. return null;
  431. } catch (error) {
  432. console.error('验证sessionToken失败:', error);
  433. return null;
  434. }
  435. }
  436. /**
  437. * 通过unionId查找所有关联用户
  438. *
  439. * @async
  440. * @param unionId - 飞书统一ID
  441. * @returns Promise<any[]>
  442. */
  443. async findUsersByUnionId(unionId: string): Promise<any[]> {
  444. try {
  445. const Parse = (global as any).Parse;
  446. if (!Parse) {
  447. throw new Error('Parse不可用');
  448. }
  449. const query = new Parse.Query("_User");
  450. query.equalTo("data.feishu.unionId", unionId);
  451. return await query.find({ useMasterKey: true });
  452. } catch (error) {
  453. console.error('通过unionId查找用户失败:', error);
  454. return [];
  455. }
  456. }
  457. }
  458. /**
  459. * 默认导出用户管理器类
  460. */
  461. export default FeishuUserManager;