| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- 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<UserEntityMt>;
- 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<UserEntityMt> {
- 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<UserEntityMt> {
- 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<number | null> {
- 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<string> {
- // 参数验证
- 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<string, { value: string }>;
- miniprogramState?: 'developer' | 'trial' | 'formal';
- tenantId?: number;
- }): Promise<any> {
- 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<string> {
- // 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<string> {
- 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秒认为有效
- };
- }
- }
|