import { DataSource, Repository } from 'typeorm'; import { UserEntityMt } from '@d8d/core-module-mt/user-module-mt'; import { FileServiceMt } from '@d8d/core-module-mt/file-module-mt'; import { SystemConfigServiceMt } from '@d8d/core-module-mt/system-config-module-mt'; import { JWTUtil, redisUtil } from '@d8d/shared-utils'; import axios from 'axios'; import process from 'node:process' export class MiniAuthService { private userRepository: Repository; private fileService: FileServiceMt; private systemConfigService: SystemConfigServiceMt; constructor(dataSource: DataSource) { this.userRepository = dataSource.getRepository(UserEntityMt); this.fileService = new FileServiceMt(dataSource); this.systemConfigService = new SystemConfigServiceMt(dataSource); } async miniLogin(code: string, tenantId?: number): Promise<{ token: string; user: UserEntityMt; isNewUser: boolean }> { // 1. 通过code获取openid和session_key const openidInfo = await this.getOpenIdByCode(code, tenantId); // 2. 查找或创建用户 let user = await this.userRepository.findOne({ where: { openid: openidInfo.openid } }); let isNewUser = false; if (!user) { // 自动注册新用户 user = await this.createMiniUser(openidInfo, tenantId); isNewUser = true; } // 3. 保存sessionKey到Redis await redisUtil.setSessionKey(user.id, openidInfo.session_key); // 4. 生成token const token = this.generateToken(user); return { token, user, isNewUser }; } async updateUserProfile(userId: number, profile: { nickname?: string; avatarUrl?: string }): Promise { const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['avatarFile'] }); if (!user) throw new Error('用户不存在'); if (profile.nickname) user.nickname = profile.nickname; // 处理头像:如果用户没有头像且提供了小程序头像URL,则下载保存 if (profile.avatarUrl && !user.avatarFileId) { try { const avatarFileId = await this.downloadAndSaveAvatar(profile.avatarUrl, userId); if (avatarFileId) { user.avatarFileId = avatarFileId; } } catch (error) { // 头像下载失败不影响主要功能 console.error('头像下载失败:', error); } } return await this.userRepository.save(user); } private async getOpenIdByCode(code: string, tenantId?: number): Promise<{ openid: string; unionid?: string; session_key: string }> { // 优先从系统配置获取,如果配置不存在则回退到环境变量 let appId: string | null = null; let appSecret: string | null = null; if (tenantId !== undefined) { // 从系统配置获取 const configKeys = ['wx.mini.app.id', 'wx.mini.app.secret']; const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId); appId = configs['wx.mini.app.id']; appSecret = configs['wx.mini.app.secret']; } // 如果系统配置中没有找到,回退到环境变量 if (!appId) { appId = process.env.WX_MINI_APP_ID || null; } if (!appSecret) { appSecret = process.env.WX_MINI_APP_SECRET || null; } if (!appId || !appSecret) { throw new Error('微信小程序配置缺失'); } const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code`; try { const response = await axios.get(url, { timeout: 10000 }); if (response.data.errcode) { throw new Error(`微信API错误: ${response.data.errmsg}`); } return { openid: response.data.openid, unionid: response.data.unionid, session_key: response.data.session_key }; } catch (error) { if (axios.isAxiosError(error)) { throw new Error('微信服务器连接失败'); } throw error; } } private async createMiniUser(openidInfo: { openid: string; unionid?: string }, tenantId?: number): Promise { const user = this.userRepository.create({ tenantId: tenantId || 1, // 默认租户ID为1,如果未提供 username: `wx_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`, password: '', // 小程序用户不需要密码 openid: openidInfo.openid, unionid: openidInfo.unionid, nickname: '微信用户', registrationSource: 'miniapp', isDisabled: 0, isDeleted: 0 }); return await this.userRepository.save(user); } private async downloadAndSaveAvatar(avatarUrl: string, userId: number): Promise { try { const result = await this.fileService.downloadAndSaveFromUrl( avatarUrl, { uploadUserId: userId, customPath: `avatars/`, mimeType: 'image/jpeg' }, { timeout: 10000 } ); return result.file.id; } catch (error) { console.error('下载保存头像失败:', error); return null; } } private generateToken(user: UserEntityMt): string { return JWTUtil.generateToken({ id: user.id, username: user.username, roles: user.roles, openid: user.openid || undefined }); } /** * 解密小程序加密的手机号 */ async decryptPhoneNumber(encryptedData: string, iv: string, sessionKey: string): Promise { // 参数验证 if (!encryptedData || !iv || !sessionKey) { throw { code: 400, message: '加密数据或初始向量不能为空' }; } try { // 使用Node.js内置crypto模块进行AES-128-CBC解密 // 微信小程序手机号解密算法:AES-128-CBC,PKCS#7填充 const crypto = await import('node:crypto'); // 创建解密器 const decipher = crypto.createDecipheriv( 'aes-128-cbc', Buffer.from(sessionKey, 'base64'), Buffer.from(iv, 'base64') ); // 设置自动PKCS#7填充 decipher.setAutoPadding(true); // 解密数据 let decrypted = decipher.update(Buffer.from(encryptedData, 'base64')); decrypted = Buffer.concat([decrypted, decipher.final()]); // 解析解密后的JSON数据 const decryptedStr = decrypted.toString('utf8'); const phoneData = JSON.parse(decryptedStr); // 验证解密结果 if (!phoneData.phoneNumber || typeof phoneData.phoneNumber !== 'string') { throw new Error('解密数据格式不正确'); } return phoneData.phoneNumber; } catch (error) { console.error('手机号解密失败:', error); // 根据错误类型返回相应的错误信息 if (error instanceof SyntaxError) { throw { code: 400, message: '解密数据格式错误' }; } else if (error instanceof Error && error.message?.includes('wrong final block length')) { throw { code: 400, message: '解密数据长度不正确' }; } else if (error instanceof Error && error.message?.includes('bad decrypt')) { throw { code: 400, message: '解密失败,请检查sessionKey是否正确' }; } else { const errorMessage = error instanceof Error ? error.message : '未知错误'; throw { code: 400, message: '手机号解密失败: ' + errorMessage }; } } } /** * 发送微信模板消息 */ async sendTemplateMessage(params: { openid: string; templateId: string; page?: string; data: Record; miniprogramState?: 'developer' | 'trial' | 'formal'; tenantId?: number; }): Promise { const { openid, templateId, page, data, miniprogramState = 'formal', tenantId } = params; console.log("ssssssss"); // 获取微信小程序配置 let appId: string | null = null; let appSecret: string | null = null; if (tenantId !== undefined) { // 从系统配置获取 const configKeys = ['wx.mini.app.id', 'wx.mini.app.secret']; const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId); appId = configs['wx.mini.app.id']; appSecret = configs['wx.mini.app.secret']; console.log("appId:",appId) console.log("appSecret:",appSecret) } // 如果系统配置中没有找到,回退到环境变量 if (!appId) { appId = process.env.WX_MINI_APP_ID || null; } if (!appSecret) { appSecret = process.env.WX_MINI_APP_SECRET || null; } if (!appId || !appSecret) { throw new Error('微信小程序配置缺失'); } // 获取access_token const accessToken = await this.getAccessToken(appId, appSecret); // 构建模板消息请求数据 const templateMessageData = { touser: openid, template_id: templateId, page: page || 'pages/index/index', data: data, miniprogram_state: miniprogramState }; console.debug('发送微信模板消息:', { appId, openid, templateId, page, dataKeys: Object.keys(data) }); // 调用微信模板消息API const url = `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`; try { const response = await axios.post(url, templateMessageData, { timeout: 10000, headers: { 'Content-Type': 'application/json' } }); if (response.data.errcode && response.data.errcode !== 0) { throw new Error(`微信模板消息发送失败: ${response.data.errmsg} (errcode: ${response.data.errcode})`); } console.debug('微信模板消息发送成功:', response.data); return response.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`微信服务器连接失败: ${error.message}`); } throw error; } } /** * 获取微信access_token(带缓存机制) */ private async getAccessToken(appId: string, appSecret: string): Promise { // 1. 首先尝试从Redis缓存获取 const cachedToken = await redisUtil.getWechatAccessToken(appId); if (cachedToken) { console.debug(`使用缓存的微信access_token,appId: ${appId}`); return cachedToken; } console.debug(`缓存中未找到微信access_token,从API获取,appId: ${appId}`); // 2. 缓存中没有,调用微信API获取 const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`; try { const response = await axios.get(url, { timeout: 10000 }); if (response.data.errcode) { throw new Error(`获取access_token失败: ${response.data.errmsg}`); } const accessToken = response.data.access_token; const expiresIn = response.data.expires_in || 7200; // 微信默认返回7200秒(2小时) // 3. 将获取到的access_token存入Redis缓存 // 设置过期时间比微信返回的expires_in少100秒,确保安全 const cacheExpiresIn = Math.max(expiresIn - 100, 600); // 最少缓存10分钟 await redisUtil.setWechatAccessToken(appId, accessToken, cacheExpiresIn); console.debug(`微信access_token获取成功并已缓存,appId: ${appId}, 过期时间: ${cacheExpiresIn}秒`); return accessToken; } catch (error) { if (axios.isAxiosError(error)) { throw new Error('微信服务器连接失败,无法获取access_token'); } throw error; } } /** * 强制刷新微信access_token(清除缓存并重新获取) */ private async refreshAccessToken(appId: string, appSecret: string): Promise { console.debug(`强制刷新微信access_token,appId: ${appId}`); // 1. 清除缓存 await redisUtil.deleteWechatAccessToken(appId); // 2. 重新获取 return await this.getAccessToken(appId, appSecret); } /** * 检查微信access_token缓存状态 */ private async checkAccessTokenCacheStatus(appId: string): Promise<{ hasCache: boolean; ttl: number; isValid: boolean; }> { const hasCache = await redisUtil.isWechatAccessTokenValid(appId); const ttl = await redisUtil.getWechatAccessTokenTTL(appId); return { hasCache, ttl, isValid: hasCache && ttl > 60 // 剩余时间大于60秒认为有效 }; } }