mini-auth.service.ts 7.8 KB

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