/** * 注册业务服务 * * 功能描述: * - 处理用户注册相关的业务逻辑和流程控制 * - 整合核心服务,提供完整的注册功能 * - 处理业务规则、数据格式化和错误处理 * - 集成Zulip账号创建和关联 * * 职责分离: * - 专注于注册业务流程和规则实现 * - 调用核心服务完成具体功能 * - 为控制器层提供注册业务接口 * - 处理注册相关的邮箱验证和Zulip集成 * * 最近修改: * - 2026-01-12: 代码分离 - 从login.service.ts中分离注册相关业务逻辑 * * @author moyin * @version 1.0.0 * @since 2026-01-12 * @lastModified 2026-01-12 */ import { Injectable, Logger, Inject } from '@nestjs/common'; import { LoginCoreService, RegisterRequest, TokenPair } from '../../core/login_core/login_core.service'; import { Users } from '../../core/db/users/users.entity'; import { ZulipAccountService } from '../../core/zulip_core/services/zulip_account.service'; import { ApiKeySecurityService } from '../../core/zulip_core/services/api_key_security.service'; // Import the interface types we need interface IZulipAccountsService { findByGameUserId(gameUserId: string, includeGameUser?: boolean): Promise; create(createDto: any): Promise; deleteByGameUserId(gameUserId: string): Promise; } // 常量定义 const ERROR_CODES = { REGISTER_FAILED: 'REGISTER_FAILED', SEND_EMAIL_VERIFICATION_FAILED: 'SEND_EMAIL_VERIFICATION_FAILED', EMAIL_VERIFICATION_FAILED: 'EMAIL_VERIFICATION_FAILED', RESEND_EMAIL_VERIFICATION_FAILED: 'RESEND_EMAIL_VERIFICATION_FAILED', TEST_MODE_ONLY: 'TEST_MODE_ONLY', INVALID_VERIFICATION_CODE: 'INVALID_VERIFICATION_CODE', } as const; const MESSAGES = { REGISTER_SUCCESS: '注册成功', REGISTER_SUCCESS_WITH_ZULIP: '注册成功,Zulip账号已同步创建', EMAIL_VERIFICATION_SUCCESS: '邮箱验证成功', CODE_SENT: '验证码已发送,请查收', EMAIL_CODE_SENT: '验证码已发送,请查收邮件', EMAIL_CODE_RESENT: '验证码已重新发送,请查收邮件', VERIFICATION_CODE_ERROR: '验证码错误', TEST_MODE_WARNING: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。', } as const; /** * 注册响应数据接口 */ export interface RegisterResponse { /** 用户信息 */ user: { id: string; username: string; nickname: string; email?: string; phone?: string; avatar_url?: string; role: number; created_at: Date; }; /** 访问令牌 */ access_token: string; /** 刷新令牌 */ refresh_token: string; /** 访问令牌过期时间(秒) */ expires_in: number; /** 令牌类型 */ token_type: string; /** 是否为新用户 */ is_new_user?: boolean; /** 消息 */ message: string; } /** * 通用响应接口 */ export interface ApiResponse { /** 是否成功 */ success: boolean; /** 响应数据 */ data?: T; /** 消息 */ message: string; /** 错误代码 */ error_code?: string; } @Injectable() export class RegisterService { private readonly logger = new Logger(RegisterService.name); constructor( private readonly loginCoreService: LoginCoreService, private readonly zulipAccountService: ZulipAccountService, @Inject('ZulipAccountsService') private readonly zulipAccountsService: IZulipAccountsService, private readonly apiKeySecurityService: ApiKeySecurityService, ) {} /** * 用户注册 * * @param registerRequest 注册请求 * @returns 注册响应 */ async register(registerRequest: RegisterRequest): Promise> { const startTime = Date.now(); const operationId = `register_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; try { this.logger.log(`开始用户注册流程`, { operation: 'register', operationId, username: registerRequest.username, email: registerRequest.email, timestamp: new Date().toISOString(), }); // 1. 初始化Zulip管理员客户端 await this.initializeZulipAdminClient(); // 2. 调用核心服务进行注册 const authResult = await this.loginCoreService.register(registerRequest); // 3. 创建Zulip账号(使用相同的邮箱和密码) let zulipAccountCreated = false; if (registerRequest.email && registerRequest.password) { try { await this.createZulipAccountForUser(authResult.user, registerRequest.password); zulipAccountCreated = true; this.logger.log(`Zulip账号创建成功`, { operation: 'register', operationId, gameUserId: authResult.user.id.toString(), email: registerRequest.email, }); } catch (zulipError) { const err = zulipError as Error; this.logger.error(`Zulip账号创建失败,开始回滚`, { operation: 'register', operationId, username: registerRequest.username, gameUserId: authResult.user.id.toString(), zulipError: err.message, }, err.stack); // 回滚游戏用户注册 try { await this.loginCoreService.deleteUser(authResult.user.id); this.logger.log(`游戏用户注册回滚成功`, { operation: 'register', operationId, username: registerRequest.username, gameUserId: authResult.user.id.toString(), }); } catch (rollbackError) { const rollbackErr = rollbackError as Error; this.logger.error(`游戏用户注册回滚失败`, { operation: 'register', operationId, username: registerRequest.username, gameUserId: authResult.user.id.toString(), rollbackError: rollbackErr.message, }, rollbackErr.stack); } // 抛出原始错误 throw new Error(`注册失败:Zulip账号创建失败 - ${err.message}`); } } else { this.logger.log(`跳过Zulip账号创建:缺少邮箱或密码`, { operation: 'register', username: registerRequest.username, hasEmail: !!registerRequest.email, hasPassword: !!registerRequest.password, }); } // 4. 生成JWT令牌对 const tokenPair = await this.loginCoreService.generateTokenPair(authResult.user); // 5. 格式化响应数据 const response: RegisterResponse = { user: this.formatUserInfo(authResult.user), access_token: tokenPair.access_token, refresh_token: tokenPair.refresh_token, expires_in: tokenPair.expires_in, token_type: tokenPair.token_type, is_new_user: true, message: zulipAccountCreated ? MESSAGES.REGISTER_SUCCESS_WITH_ZULIP : MESSAGES.REGISTER_SUCCESS }; const duration = Date.now() - startTime; this.logger.log(`用户注册成功`, { operation: 'register', operationId, gameUserId: authResult.user.id.toString(), username: authResult.user.username, email: authResult.user.email, 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(`用户注册失败`, { operation: 'register', operationId, username: registerRequest.username, email: registerRequest.email, error: err.message, duration, timestamp: new Date().toISOString(), }, err.stack); return { success: false, message: err.message || '注册失败', error_code: ERROR_CODES.REGISTER_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}`); return this.handleTestModeResponse(result, MESSAGES.CODE_SENT, MESSAGES.EMAIL_CODE_SENT); } catch (error) { this.logger.error(`发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '发送验证码失败', error_code: ERROR_CODES.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: MESSAGES.EMAIL_VERIFICATION_SUCCESS }; } else { return { success: false, message: MESSAGES.VERIFICATION_CODE_ERROR, error_code: ERROR_CODES.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: ERROR_CODES.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}`); return this.handleTestModeResponse(result, MESSAGES.CODE_SENT, MESSAGES.EMAIL_CODE_RESENT); } catch (error) { this.logger.error(`重新发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '重新发送验证码失败', error_code: ERROR_CODES.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 result 核心服务返回的结果 * @param successMessage 成功时的消息 * @param emailMessage 邮件发送成功时的消息 * @returns 格式化的响应 * @private */ private handleTestModeResponse( result: { code: string; isTestMode: boolean }, successMessage: string, emailMessage?: string ): ApiResponse<{ verification_code?: string; is_test_mode?: boolean }> { if (result.isTestMode) { return { success: false, data: { verification_code: result.code, is_test_mode: true }, message: MESSAGES.TEST_MODE_WARNING, error_code: ERROR_CODES.TEST_MODE_ONLY }; } else { return { success: true, data: { is_test_mode: false }, message: emailMessage || successMessage }; } } /** * 初始化Zulip管理员客户端 * * 功能描述: * 使用环境变量中的管理员凭证初始化Zulip客户端 * * 业务逻辑: * 1. 从环境变量获取管理员配置 * 2. 验证配置完整性 * 3. 初始化ZulipAccountService的管理员客户端 * * @throws Error 当配置缺失或初始化失败时 * @private */ private async initializeZulipAdminClient(): Promise { try { // 从环境变量获取管理员配置 const adminConfig = { realm: process.env.ZULIP_SERVER_URL || process.env.ZULIP_REALM || '', username: process.env.ZULIP_BOT_EMAIL || process.env.ZULIP_ADMIN_EMAIL || '', apiKey: process.env.ZULIP_BOT_API_KEY || process.env.ZULIP_ADMIN_API_KEY || '', }; // 验证配置完整性 if (!adminConfig.realm || !adminConfig.username || !adminConfig.apiKey) { throw new Error('Zulip管理员配置不完整,请检查环境变量'); } // 初始化管理员客户端 const initialized = await this.zulipAccountService.initializeAdminClient(adminConfig); if (!initialized) { throw new Error('Zulip管理员客户端初始化失败'); } } 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. 尝试创建Zulip账号(如果已存在则自动绑定) * 3. 获取或生成API Key并存储到Redis * 4. 在数据库中创建关联记录 * 5. 建立内存关联(用于当前会话) * * @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.zulipAccountsService.findByGameUserId(gameUser.id.toString()); 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 let finalApiKey = createResult.apiKey; // 如果是绑定已有账号但没有API Key,尝试重新获取 if (createResult.isExistingUser && !finalApiKey) { const apiKeyResult = await this.zulipAccountService.generateApiKeyForUser( createResult.email!, password ); if (apiKeyResult.success) { finalApiKey = apiKeyResult.apiKey; } else { this.logger.warn('无法获取已有Zulip账号的API Key', { operation: 'createZulipAccountForUser', gameUserId: gameUser.id.toString(), zulipEmail: createResult.email, error: apiKeyResult.error, }); } } // 4. 存储API Key到Redis if (finalApiKey) { await this.apiKeySecurityService.storeApiKey( gameUser.id.toString(), finalApiKey ); } // 5. 在数据库中创建关联记录 await this.zulipAccountsService.create({ gameUserId: gameUser.id.toString(), zulipUserId: createResult.userId!, zulipEmail: createResult.email!, zulipFullName: gameUser.nickname, zulipApiKeyEncrypted: finalApiKey ? 'stored_in_redis' : '', status: 'active', }); // 6. 建立游戏账号与Zulip账号的内存关联(用于当前会话) if (finalApiKey) { await this.zulipAccountService.linkGameAccount( gameUser.id.toString(), createResult.userId!, createResult.email!, finalApiKey ); } const duration = Date.now() - startTime; this.logger.log('Zulip账号创建/绑定和关联成功', { operation: 'createZulipAccountForUser', gameUserId: gameUser.id.toString(), zulipUserId: createResult.userId, zulipEmail: createResult.email, isExistingUser: createResult.isExistingUser, hasApiKey: !!finalApiKey, 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.zulipAccountsService.deleteByGameUserId(gameUser.id.toString()); } catch (cleanupError) { this.logger.warn('清理Zulip账号关联数据失败', { operation: 'createZulipAccountForUser', gameUserId: gameUser.id.toString(), cleanupError: (cleanupError as Error).message, }); } throw error; } } }