mini-auth.service.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import { DataSource, Repository } from 'typeorm';
  2. import { UserEntity } from '@d8d/core-module/user-module';
  3. import { FileService } from '@d8d/core-module/file-module';
  4. // import { SystemConfigService } from '@d8d/core-module/system-config-module';
  5. import { JWTUtil, redisUtil } from '@d8d/shared-utils';
  6. import axios from 'axios';
  7. import process from 'node:process'
  8. export class MiniAuthService {
  9. private userRepository: Repository<UserEntity>;
  10. private fileService: FileService;
  11. // private systemConfigService: SystemConfigService;
  12. constructor(dataSource: DataSource) {
  13. this.userRepository = dataSource.getRepository(UserEntity);
  14. this.fileService = new FileService(dataSource);
  15. // this.systemConfigService = new SystemConfigService(dataSource);
  16. }
  17. async miniLogin(code: string): Promise<{ token: string; user: UserEntity; isNewUser: boolean }> {
  18. // 1. 通过code获取openid和session_key
  19. const openidInfo = await this.getOpenIdByCode(code);
  20. // 2. 查找或创建用户
  21. let user = await this.userRepository.findOne({
  22. where: { openid: openidInfo.openid }
  23. });
  24. let isNewUser = false;
  25. if (!user) {
  26. // 自动注册新用户
  27. user = await this.createMiniUser(openidInfo);
  28. isNewUser = true;
  29. }
  30. // 3. 保存sessionKey到Redis
  31. await redisUtil.setSessionKey(user.id, openidInfo.session_key);
  32. // 4. 生成token
  33. const token = this.generateToken(user);
  34. return { token, user, isNewUser };
  35. }
  36. async updateUserProfile(userId: number, profile: { nickname?: string; avatarUrl?: string }): Promise<UserEntity> {
  37. const user = await this.userRepository.findOne({
  38. where: { id: userId },
  39. relations: ['avatarFile']
  40. });
  41. if (!user) throw new Error('用户不存在');
  42. if (profile.nickname) user.nickname = profile.nickname;
  43. // 处理头像:如果用户没有头像且提供了小程序头像URL,则下载保存
  44. if (profile.avatarUrl && !user.avatarFileId) {
  45. try {
  46. const avatarFileId = await this.downloadAndSaveAvatar(profile.avatarUrl, userId);
  47. if (avatarFileId) {
  48. user.avatarFileId = avatarFileId;
  49. }
  50. } catch (error) {
  51. // 头像下载失败不影响主要功能
  52. console.error('头像下载失败:', error);
  53. }
  54. }
  55. return await this.userRepository.save(user);
  56. }
  57. private async getOpenIdByCode(code: string): Promise<{ openid: string; unionid?: string; session_key: string }> {
  58. // 从环境变量获取
  59. const appId = process.env.WX_MINI_APP_ID;
  60. const appSecret = process.env.WX_MINI_APP_SECRET;
  61. if (!appId || !appSecret) {
  62. throw new Error('微信小程序配置缺失');
  63. }
  64. const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code`;
  65. try {
  66. const response = await axios.get(url, { timeout: 10000 });
  67. if (response.data.errcode) {
  68. throw new Error(`微信API错误: ${response.data.errmsg}`);
  69. }
  70. return {
  71. openid: response.data.openid,
  72. unionid: response.data.unionid,
  73. session_key: response.data.session_key
  74. };
  75. } catch (error) {
  76. if (axios.isAxiosError(error)) {
  77. throw new Error('微信服务器连接失败');
  78. }
  79. throw error;
  80. }
  81. }
  82. private async createMiniUser(openidInfo: { openid: string; unionid?: string }): Promise<UserEntity> {
  83. const user = this.userRepository.create({
  84. username: `wx_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
  85. password: '', // 小程序用户不需要密码
  86. openid: openidInfo.openid,
  87. unionid: openidInfo.unionid,
  88. nickname: '微信用户',
  89. registrationSource: 'miniapp',
  90. isDisabled: 0,
  91. isDeleted: 0
  92. });
  93. return await this.userRepository.save(user);
  94. }
  95. private async downloadAndSaveAvatar(avatarUrl: string, userId: number): Promise<number | null> {
  96. try {
  97. const result = await this.fileService.downloadAndSaveFromUrl(
  98. avatarUrl,
  99. {
  100. uploadUserId: userId,
  101. customPath: `avatars/`,
  102. mimeType: 'image/jpeg'
  103. },
  104. { timeout: 10000 }
  105. );
  106. return result.file.id;
  107. } catch (error) {
  108. console.error('下载保存头像失败:', error);
  109. return null;
  110. }
  111. }
  112. private generateToken(user: UserEntity): string {
  113. return JWTUtil.generateToken({
  114. id: user.id,
  115. username: user.username,
  116. roles: user.roles,
  117. openid: user.openid || undefined
  118. });
  119. }
  120. /**
  121. * 解密小程序加密的手机号
  122. */
  123. async decryptPhoneNumber(encryptedData: string, iv: string, sessionKey: string): Promise<string> {
  124. // 参数验证
  125. if (!encryptedData || !iv || !sessionKey) {
  126. throw { code: 400, message: '加密数据或初始向量不能为空' };
  127. }
  128. try {
  129. // 使用Node.js内置crypto模块进行AES-128-CBC解密
  130. // 微信小程序手机号解密算法:AES-128-CBC,PKCS#7填充
  131. const crypto = await import('node:crypto');
  132. // 创建解密器
  133. const decipher = crypto.createDecipheriv(
  134. 'aes-128-cbc',
  135. Buffer.from(sessionKey, 'base64'),
  136. Buffer.from(iv, 'base64')
  137. );
  138. // 设置自动PKCS#7填充
  139. decipher.setAutoPadding(true);
  140. // 解密数据
  141. let decrypted = decipher.update(Buffer.from(encryptedData, 'base64'));
  142. decrypted = Buffer.concat([decrypted, decipher.final()]);
  143. // 解析解密后的JSON数据
  144. const decryptedStr = decrypted.toString('utf8');
  145. const phoneData = JSON.parse(decryptedStr);
  146. // 验证解密结果
  147. if (!phoneData.phoneNumber || typeof phoneData.phoneNumber !== 'string') {
  148. throw new Error('解密数据格式不正确');
  149. }
  150. return phoneData.phoneNumber;
  151. } catch (error) {
  152. console.error('手机号解密失败:', error);
  153. // 根据错误类型返回相应的错误信息
  154. if (error instanceof SyntaxError) {
  155. throw { code: 400, message: '解密数据格式错误' };
  156. } else if (error instanceof Error && error.message?.includes('wrong final block length')) {
  157. throw { code: 400, message: '解密数据长度不正确' };
  158. } else if (error instanceof Error && error.message?.includes('bad decrypt')) {
  159. throw { code: 400, message: '解密失败,请检查sessionKey是否正确' };
  160. } else {
  161. const errorMessage = error instanceof Error ? error.message : '未知错误';
  162. throw { code: 400, message: '手机号解密失败: ' + errorMessage };
  163. }
  164. }
  165. }
  166. }