/** * 登录业务服务 * * 功能描述: * - 处理登录相关的业务逻辑和流程控制 * - 整合核心服务,提供完整的业务功能 * - 处理业务规则、数据格式化和错误处理 * * 职责分离: * - 专注于业务流程和规则实现 * - 调用核心服务完成具体功能 * - 为控制器层提供业务接口 * * @author moyin angjustinl * @version 1.0.0 * @since 2025-12-17 */ import { Injectable, Logger, Inject } from '@nestjs/common'; import { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, AuthResult, VerificationCodeLoginRequest } from '../../../core/login_core/login_core.service'; import { Users } from '../../../core/db/users/users.entity'; import { ZulipAccountService } from '../../../core/zulip/services/zulip_account.service'; import { ZulipAccountsRepository } from '../../../core/db/zulip_accounts/zulip_accounts.repository'; import { ApiKeySecurityService } from '../../../core/zulip/services/api_key_security.service'; /** * 登录响应数据接口 */ export interface LoginResponse { /** 用户信息 */ user: { id: string; username: string; nickname: string; email?: string; phone?: string; avatar_url?: string; role: number; created_at: Date; }; /** 访问令牌(实际应用中应生成JWT) */ access_token: string; /** 刷新令牌 */ refresh_token?: string; /** 是否为新用户 */ is_new_user?: boolean; /** 消息 */ message: string; } /** * 通用响应接口 */ export interface ApiResponse { /** 是否成功 */ success: boolean; /** 响应数据 */ data?: T; /** 消息 */ message: string; /** 错误代码 */ error_code?: string; } @Injectable() export class LoginService { private readonly logger = new Logger(LoginService.name); constructor( private readonly loginCoreService: LoginCoreService, private readonly zulipAccountService: ZulipAccountService, @Inject('ZulipAccountsRepository') private readonly zulipAccountsRepository: ZulipAccountsRepository, private readonly apiKeySecurityService: ApiKeySecurityService, ) {} /** * 用户登录 * * @param loginRequest 登录请求 * @returns 登录响应 */ async login(loginRequest: LoginRequest): Promise> { try { this.logger.log(`用户登录尝试: ${loginRequest.identifier}`); // 调用核心服务进行认证 const authResult = await this.loginCoreService.login(loginRequest); // 生成访问令牌(实际应用中应使用JWT) const accessToken = this.generateAccessToken(authResult.user); // 格式化响应数据 const response: LoginResponse = { user: this.formatUserInfo(authResult.user), access_token: accessToken, is_new_user: authResult.isNewUser, message: '登录成功' }; this.logger.log(`用户登录成功: ${authResult.user.username} (ID: ${authResult.user.id})`); return { success: true, data: response, message: '登录成功' }; } catch (error) { this.logger.error(`用户登录失败: ${loginRequest.identifier}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '登录失败', error_code: 'LOGIN_FAILED' }; } } /** * 用户注册 * * @param registerRequest 注册请求 * @returns 注册响应 */ async register(registerRequest: RegisterRequest): Promise> { const startTime = Date.now(); try { this.logger.log(`用户注册尝试: ${registerRequest.username}`); // 1. 初始化Zulip管理员客户端 await this.initializeZulipAdminClient(); // 2. 调用核心服务进行注册 const authResult = await this.loginCoreService.register(registerRequest); // 3. 创建Zulip账号(使用相同的邮箱和密码) let zulipAccountCreated = false; try { if (registerRequest.email && registerRequest.password) { await this.createZulipAccountForUser(authResult.user, registerRequest.password); zulipAccountCreated = true; this.logger.log(`Zulip账号创建成功: ${registerRequest.username}`, { operation: 'register', gameUserId: authResult.user.id.toString(), email: registerRequest.email, }); } else { this.logger.warn(`跳过Zulip账号创建:缺少邮箱或密码`, { operation: 'register', username: registerRequest.username, hasEmail: !!registerRequest.email, hasPassword: !!registerRequest.password, }); } } catch (zulipError) { const err = zulipError as Error; this.logger.error(`Zulip账号创建失败,回滚用户注册`, { operation: 'register', username: registerRequest.username, gameUserId: authResult.user.id.toString(), zulipError: err.message, }, err.stack); // 回滚游戏用户注册 try { await this.loginCoreService.deleteUser(authResult.user.id); this.logger.log(`用户注册回滚成功: ${registerRequest.username}`); } catch (rollbackError) { const rollbackErr = rollbackError as Error; this.logger.error(`用户注册回滚失败`, { operation: 'register', username: registerRequest.username, gameUserId: authResult.user.id.toString(), rollbackError: rollbackErr.message, }, rollbackErr.stack); } // 抛出原始错误 throw new Error(`注册失败:Zulip账号创建失败 - ${err.message}`); } // 4. 生成访问令牌 const accessToken = this.generateAccessToken(authResult.user); // 5. 格式化响应数据 const response: LoginResponse = { user: this.formatUserInfo(authResult.user), access_token: accessToken, is_new_user: true, message: zulipAccountCreated ? '注册成功,Zulip账号已同步创建' : '注册成功' }; const duration = Date.now() - startTime; this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`, { operation: 'register', gameUserId: authResult.user.id.toString(), username: authResult.user.username, zulipAccountCreated, duration, timestamp: new Date().toISOString(), }); return { success: true, data: response, message: response.message }; } catch (error) { const duration = Date.now() - startTime; const err = error as Error; this.logger.error(`用户注册失败: ${registerRequest.username}`, { operation: 'register', username: registerRequest.username, error: err.message, duration, timestamp: new Date().toISOString(), }, err.stack); return { success: false, message: err.message || '注册失败', error_code: 'REGISTER_FAILED' }; } } /** * GitHub OAuth登录 * * @param oauthRequest OAuth请求 * @returns 登录响应 */ async githubOAuth(oauthRequest: GitHubOAuthRequest): Promise> { try { this.logger.log(`GitHub OAuth登录尝试: ${oauthRequest.github_id}`); // 调用核心服务进行OAuth认证 const authResult = await this.loginCoreService.githubOAuth(oauthRequest); // 生成访问令牌 const accessToken = this.generateAccessToken(authResult.user); // 格式化响应数据 const response: LoginResponse = { user: this.formatUserInfo(authResult.user), access_token: accessToken, is_new_user: authResult.isNewUser, message: authResult.isNewUser ? 'GitHub账户绑定成功' : 'GitHub登录成功' }; this.logger.log(`GitHub OAuth成功: ${authResult.user.username} (ID: ${authResult.user.id})`); return { success: true, data: response, message: response.message }; } catch (error) { this.logger.error(`GitHub OAuth失败: ${oauthRequest.github_id}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : 'GitHub登录失败', error_code: 'GITHUB_OAUTH_FAILED' }; } } /** * 发送密码重置验证码 * * @param identifier 邮箱或手机号 * @returns 响应结果 */ async sendPasswordResetCode(identifier: string): Promise> { try { this.logger.log(`发送密码重置验证码: ${identifier}`); // 调用核心服务发送验证码 const result = await this.loginCoreService.sendPasswordResetCode(identifier); this.logger.log(`密码重置验证码已发送: ${identifier}`); // 根据是否为测试模式返回不同的状态和消息 by angjustinl 2025-12-17 if (result.isTestMode) { // 测试模式:验证码生成但未真实发送 return { success: false, // 测试模式下不算真正成功 data: { verification_code: result.code, is_test_mode: true }, message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。', error_code: 'TEST_MODE_ONLY' }; } else { // 真实发送模式 return { success: true, data: { is_test_mode: false }, message: '验证码已发送,请查收' }; } } catch (error) { this.logger.error(`发送密码重置验证码失败: ${identifier}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '发送验证码失败', error_code: 'SEND_CODE_FAILED' }; } } /** * 重置密码 * * @param resetRequest 重置请求 * @returns 响应结果 */ async resetPassword(resetRequest: PasswordResetRequest): Promise { try { this.logger.log(`密码重置尝试: ${resetRequest.identifier}`); // 调用核心服务重置密码 await this.loginCoreService.resetPassword(resetRequest); this.logger.log(`密码重置成功: ${resetRequest.identifier}`); return { success: true, message: '密码重置成功' }; } catch (error) { this.logger.error(`密码重置失败: ${resetRequest.identifier}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '密码重置失败', error_code: 'RESET_PASSWORD_FAILED' }; } } /** * 修改密码 * * @param userId 用户ID * @param oldPassword 旧密码 * @param newPassword 新密码 * @returns 响应结果 */ async changePassword(userId: bigint, oldPassword: string, newPassword: string): Promise { try { this.logger.log(`修改密码尝试: 用户ID ${userId}`); // 调用核心服务修改密码 await this.loginCoreService.changePassword(userId, oldPassword, newPassword); this.logger.log(`修改密码成功: 用户ID ${userId}`); return { success: true, message: '密码修改成功' }; } catch (error) { this.logger.error(`修改密码失败: 用户ID ${userId}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '密码修改失败', error_code: 'CHANGE_PASSWORD_FAILED' }; } } /** * 发送邮箱验证码 * * @param email 邮箱地址 * @returns 响应结果 */ async sendEmailVerification(email: string): Promise> { try { this.logger.log(`发送邮箱验证码: ${email}`); // 调用核心服务发送验证码 const result = await this.loginCoreService.sendEmailVerification(email); this.logger.log(`邮箱验证码已发送: ${email}`); // 根据是否为测试模式返回不同的状态和消息 if (result.isTestMode) { // 测试模式:验证码生成但未真实发送 return { success: false, // 测试模式下不算真正成功 data: { verification_code: result.code, is_test_mode: true }, message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。', error_code: 'TEST_MODE_ONLY' }; } else { // 真实发送模式 return { success: true, data: { is_test_mode: false }, message: '验证码已发送,请查收邮件' }; } } catch (error) { this.logger.error(`发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '发送验证码失败', error_code: 'SEND_EMAIL_VERIFICATION_FAILED' }; } } /** * 验证邮箱验证码 * * @param email 邮箱地址 * @param code 验证码 * @returns 响应结果 */ async verifyEmailCode(email: string, code: string): Promise { try { this.logger.log(`验证邮箱验证码: ${email}`); // 调用核心服务验证验证码 const isValid = await this.loginCoreService.verifyEmailCode(email, code); if (isValid) { this.logger.log(`邮箱验证成功: ${email}`); return { success: true, message: '邮箱验证成功' }; } else { return { success: false, message: '验证码错误', error_code: 'INVALID_VERIFICATION_CODE' }; } } catch (error) { this.logger.error(`邮箱验证失败: ${email}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '邮箱验证失败', error_code: 'EMAIL_VERIFICATION_FAILED' }; } } /** * 重新发送邮箱验证码 * * @param email 邮箱地址 * @returns 响应结果 */ async resendEmailVerification(email: string): Promise> { try { this.logger.log(`重新发送邮箱验证码: ${email}`); // 调用核心服务重新发送验证码 const result = await this.loginCoreService.resendEmailVerification(email); this.logger.log(`邮箱验证码已重新发送: ${email}`); // 根据是否为测试模式返回不同的状态和消息 if (result.isTestMode) { // 测试模式:验证码生成但未真实发送 return { success: false, // 测试模式下不算真正成功 data: { verification_code: result.code, is_test_mode: true }, message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。', error_code: 'TEST_MODE_ONLY' }; } else { // 真实发送模式 return { success: true, data: { is_test_mode: false }, message: '验证码已重新发送,请查收邮件' }; } } catch (error) { this.logger.error(`重新发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '重新发送验证码失败', error_code: 'RESEND_EMAIL_VERIFICATION_FAILED' }; } } /** * 格式化用户信息 * * @param user 用户实体 * @returns 格式化的用户信息 */ private formatUserInfo(user: Users) { return { id: user.id.toString(), // 将bigint转换为字符串 username: user.username, nickname: user.nickname, email: user.email, phone: user.phone, avatar_url: user.avatar_url, role: user.role, created_at: user.created_at }; } /** * 生成访问令牌 * * @param user 用户信息 * @returns 访问令牌 */ private generateAccessToken(user: Users): string { // 实际应用中应使用JWT库生成真正的JWT令牌 // 这里仅用于演示,生成一个简单的令牌 const payload = { userId: user.id.toString(), username: user.username, role: user.role, timestamp: Date.now() }; // 简单的Base64编码(实际应用中应使用JWT) return Buffer.from(JSON.stringify(payload)).toString('base64'); } /** * 验证码登录 * * @param loginRequest 验证码登录请求 * @returns 登录响应 */ async verificationCodeLogin(loginRequest: VerificationCodeLoginRequest): Promise> { try { this.logger.log(`验证码登录尝试: ${loginRequest.identifier}`); // 调用核心服务进行验证码认证 const authResult = await this.loginCoreService.verificationCodeLogin(loginRequest); // 生成访问令牌 const accessToken = this.generateAccessToken(authResult.user); // 格式化响应数据 const response: LoginResponse = { user: this.formatUserInfo(authResult.user), access_token: accessToken, is_new_user: authResult.isNewUser, message: '验证码登录成功' }; this.logger.log(`验证码登录成功: ${authResult.user.username} (ID: ${authResult.user.id})`); return { success: true, data: response, message: '验证码登录成功' }; } catch (error) { this.logger.error(`验证码登录失败: ${loginRequest.identifier}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '验证码登录失败', error_code: 'VERIFICATION_CODE_LOGIN_FAILED' }; } } /** * 发送登录验证码 * * @param identifier 邮箱或手机号 * @returns 响应结果 */ async sendLoginVerificationCode(identifier: string): Promise> { try { this.logger.log(`发送登录验证码: ${identifier}`); // 调用核心服务发送验证码 const result = await this.loginCoreService.sendLoginVerificationCode(identifier); this.logger.log(`登录验证码已发送: ${identifier}`); // 根据是否为测试模式返回不同的状态和消息 if (result.isTestMode) { // 测试模式:验证码生成但未真实发送 return { success: false, // 测试模式下不算真正成功 data: { verification_code: result.code, is_test_mode: true }, message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。', error_code: 'TEST_MODE_ONLY' }; } else { // 真实发送模式 return { success: true, data: { is_test_mode: false }, message: '验证码已发送,请查收' }; } } catch (error) { this.logger.error(`发送登录验证码失败: ${identifier}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '发送验证码失败', error_code: 'SEND_LOGIN_CODE_FAILED' }; } } /** * 调试验证码信息 * * @param email 邮箱地址 * @returns 调试信息 */ async debugVerificationCode(email: string): Promise { try { this.logger.log(`调试验证码信息: ${email}`); const debugInfo = await this.loginCoreService.debugVerificationCode(email); return { success: true, data: debugInfo, message: '调试信息获取成功' }; } catch (error) { this.logger.error(`获取验证码调试信息失败: ${email}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '获取调试信息失败', error_code: 'DEBUG_VERIFICATION_CODE_FAILED' }; } } /** * 初始化Zulip管理员客户端 * * 功能描述: * 使用环境变量中的管理员凭证初始化Zulip客户端 * * 业务逻辑: * 1. 从环境变量获取管理员配置 * 2. 验证配置完整性 * 3. 初始化ZulipAccountService的管理员客户端 * * @throws Error 当配置缺失或初始化失败时 * @private */ private async initializeZulipAdminClient(): Promise { try { // 从环境变量获取管理员配置 const adminConfig = { realm: process.env.ZULIP_SERVER_URL || '', username: process.env.ZULIP_BOT_EMAIL || '', apiKey: process.env.ZULIP_BOT_API_KEY || '', }; // 验证配置完整性 if (!adminConfig.realm || !adminConfig.username || !adminConfig.apiKey) { throw new Error('Zulip管理员配置不完整,请检查环境变量 ZULIP_SERVER_URL, ZULIP_BOT_EMAIL, ZULIP_BOT_API_KEY'); } // 初始化管理员客户端 const initialized = await this.zulipAccountService.initializeAdminClient(adminConfig); if (!initialized) { throw new Error('Zulip管理员客户端初始化失败'); } this.logger.log('Zulip管理员客户端初始化成功', { operation: 'initializeZulipAdminClient', realm: adminConfig.realm, adminEmail: adminConfig.username, }); } catch (error) { const err = error as Error; this.logger.error('Zulip管理员客户端初始化失败', { operation: 'initializeZulipAdminClient', error: err.message, }, err.stack); throw error; } } /** * 为用户创建Zulip账号 * * 功能描述: * 为新注册的游戏用户创建对应的Zulip账号并建立关联 * * 业务逻辑: * 1. 使用相同的邮箱和密码创建Zulip账号 * 2. 加密存储API Key * 3. 在数据库中建立关联关系 * 4. 处理创建失败的情况 * * @param gameUser 游戏用户信息 * @param password 用户密码(明文) * @throws Error 当Zulip账号创建失败时 * @private */ private async createZulipAccountForUser(gameUser: Users, password: string): Promise { const startTime = Date.now(); this.logger.log('开始为用户创建Zulip账号', { operation: 'createZulipAccountForUser', gameUserId: gameUser.id.toString(), email: gameUser.email, nickname: gameUser.nickname, }); try { // 1. 检查是否已存在Zulip账号关联 const existingAccount = await this.zulipAccountsRepository.findByGameUserId(gameUser.id); if (existingAccount) { this.logger.warn('用户已存在Zulip账号关联,跳过创建', { operation: 'createZulipAccountForUser', gameUserId: gameUser.id.toString(), existingZulipUserId: existingAccount.zulipUserId, }); return; } // 2. 创建Zulip账号 const createResult = await this.zulipAccountService.createZulipAccount({ email: gameUser.email, fullName: gameUser.nickname, password: password, }); if (!createResult.success) { throw new Error(createResult.error || 'Zulip账号创建失败'); } // 3. 存储API Key if (createResult.apiKey) { await this.apiKeySecurityService.storeApiKey( gameUser.id.toString(), createResult.apiKey ); } // 4. 在数据库中创建关联记录 await this.zulipAccountsRepository.create({ gameUserId: gameUser.id, zulipUserId: createResult.userId!, zulipEmail: createResult.email!, zulipFullName: gameUser.nickname, zulipApiKeyEncrypted: createResult.apiKey ? 'stored_in_redis' : '', // 标记API Key已存储在Redis中 status: 'active', }); // 5. 建立游戏账号与Zulip账号的内存关联(用于当前会话) if (createResult.apiKey) { await this.zulipAccountService.linkGameAccount( gameUser.id.toString(), createResult.userId!, createResult.email!, createResult.apiKey ); } const duration = Date.now() - startTime; this.logger.log('Zulip账号创建和关联成功', { operation: 'createZulipAccountForUser', gameUserId: gameUser.id.toString(), zulipUserId: createResult.userId, zulipEmail: createResult.email, hasApiKey: !!createResult.apiKey, duration, }); } catch (error) { const err = error as Error; const duration = Date.now() - startTime; this.logger.error('为用户创建Zulip账号失败', { operation: 'createZulipAccountForUser', gameUserId: gameUser.id.toString(), email: gameUser.email, error: err.message, duration, }, err.stack); // 清理可能创建的部分数据 try { await this.zulipAccountsRepository.deleteByGameUserId(gameUser.id); } catch (cleanupError) { this.logger.warn('清理Zulip账号关联数据失败', { operation: 'createZulipAccountForUser', gameUserId: gameUser.id.toString(), cleanupError: (cleanupError as Error).message, }); } throw error; } } }