/** * 注册业务服务 * * 功能描述: * - 处理用户注册相关的业务逻辑和流程控制 * - 整合核心服务,提供完整的注册功能 * - 处理业务规则、数据格式化和错误处理 * - 集成Zulip账号创建和关联 * * 职责分离: * - 专注于注册业务流程和规则实现 * - 调用核心服务完成具体功能 * - 为控制器层提供注册业务接口 * - 处理注册相关的邮箱验证和Zulip集成 * * 最近修改: * - 2026-01-15: 代码规范优化 - 清理未使用的导入TokenPair,增强userId非空验证 (修改者: moyin) * - 2026-01-12: 代码分离 - 从login.service.ts中分离注册相关业务逻辑 * * @author moyin * @version 1.0.1 * @since 2026-01-12 * @lastModified 2026-01-15 */ import { Injectable, Logger, Inject, Optional } from '@nestjs/common'; import { LoginCoreService, RegisterRequest } 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 { AccountProfilePayload, AccountProfileService } from './account_profile.service'; import { InvitationCodesService } from '../invitation/invitation_codes.service'; import { InvitationCode } from '../invitation/invitation_code.entity'; // 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: '注册成功', 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; }; /** 游戏内账号资料 */ profile?: AccountProfilePayload['profile']; /** 访问令牌 */ 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, private readonly accountProfileService: AccountProfileService, @Optional() private readonly invitationCodesService?: InvitationCodesService, ) {} /** * 用户注册 * * @param registerRequest 注册请求 * @returns 注册响应 */ async register(registerRequest: RegisterRequest): Promise> { const startTime = Date.now(); const operationId = `register_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; let reservation: InvitationCode | undefined; let userCreated = false; try { if (this.invitationCodesService) { reservation = await this.invitationCodesService.reserve(registerRequest.invitation_code || ''); } this.logger.log(`开始用户注册流程`, { operation: 'register', operationId, username: registerRequest.username, email: registerRequest.email, timestamp: new Date().toISOString(), }); // 1. 初始化Zulip管理员客户端 const zulipUnavailableForLocalDebug = this.isZulipUnavailableForLocalDebug(); if (!zulipUnavailableForLocalDebug) { await this.initializeZulipAdminClient(); } else { this.logger.warn('本地调试模式跳过Zulip管理员客户端初始化', { operation: 'register', operationId, }); } // 2. 调用核心服务进行注册 const authResult = await this.loginCoreService.register(registerRequest); userCreated = true; if (reservation) { try { await this.invitationCodesService!.recordUsage(reservation.id, authResult.user.id, registerRequest.email || ''); } catch (error) { this.logger.error('记录邀请码使用明细失败,不中断已完成的用户注册', { operation: 'register', operationId, invitationCodeId: reservation.id.toString(), gameUserId: authResult.user.id.toString(), error: error instanceof Error ? error.message : String(error), }); } } // 3. 创建Zulip账号(使用相同的邮箱和密码)- 异步处理,不影响注册流程 if (registerRequest.email && registerRequest.password && !zulipUnavailableForLocalDebug) { // 异步创建Zulip账号,不阻塞注册流程 this.createZulipAccountWithRetry( authResult.user, registerRequest.password, operationId ).then(success => { if (success) { this.logger.log(`Zulip账号异步创建成功`, { operation: 'register', operationId, gameUserId: authResult.user.id.toString(), email: registerRequest.email, }); } }).catch(err => { // 错误已在重试方法中记录,这里只是确保Promise不会未处理 this.logger.warn(`Zulip账号异步创建最终失败`, { operation: 'register', operationId, gameUserId: authResult.user.id.toString(), email: registerRequest.email, }); }); this.logger.log(`Zulip账号创建已提交到后台异步处理`, { operation: 'register', operationId, gameUserId: authResult.user.id.toString(), email: registerRequest.email, }); } else { this.logger.log(`跳过Zulip账号创建:缺少邮箱或密码`, { operation: 'register', username: registerRequest.username, hasEmail: !!registerRequest.email, hasPassword: !!registerRequest.password, zulipUnavailableForLocalDebug, }); } // 4. 生成JWT令牌对 const tokenPair = await this.loginCoreService.generateTokenPair(authResult.user); const profile = await this.accountProfileService.ensureProfile(authResult.user.id, registerRequest.skin_id); await this.accountProfileService.sendWelcomeEmailAfterInitialCharacterCreation(authResult.user, profile); // 5. 格式化响应数据 const response: RegisterResponse = { user: this.formatUserInfo(authResult.user), profile: (await this.accountProfileService.formatAccountProfileAsync(authResult.user, profile)).profile, 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: 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, duration, timestamp: new Date().toISOString(), }); return { success: true, data: response, message: response.message }; } catch (error) { if (reservation && !userCreated) { await this.invitationCodesService?.release(reservation.id).catch(() => undefined); } 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, invitationCode: string): Promise> { try { if (this.invitationCodesService) await this.invitationCodesService.validate(invitationCode); 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服务。 * * @private */ private isZulipUnavailableForLocalDebug(): boolean { const serverUrl = process.env.ZULIP_SERVER_URL || process.env.ZULIP_REALM || ''; const apiKey = process.env.ZULIP_BOT_API_KEY || process.env.ZULIP_ADMIN_API_KEY || ''; const degradedModeEnabled = process.env.ZULIP_DEGRADED_MODE_ENABLED === 'true'; const hasPlaceholderServer = serverUrl.includes('your-zulip-server.com'); const hasPlaceholderApiKey = !apiKey || apiKey === 'your_bot_api_key'; return degradedModeEnabled || hasPlaceholderServer || hasPlaceholderApiKey; } /** * 带重试机制的异步创建Zulip账号 * * 功能描述: * 异步创建Zulip账号,失败时自动重试最多3次 * 所有错误只记录日志,不影响用户注册流程 * * @param gameUser 游戏用户信息 * @param password 用户密码 * @param operationId 操作ID(用于日志追踪) * @param maxRetries 最大重试次数,默认3次 * @returns Promise 是否创建成功 * @private */ private async createZulipAccountWithRetry( gameUser: Users, password: string, operationId: string, maxRetries: number = 3 ): Promise { let lastError: Error | null = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { this.logger.log(`尝试创建Zulip账号 (第${attempt}/${maxRetries}次)`, { operation: 'createZulipAccountWithRetry', operationId, attempt, maxRetries, gameUserId: gameUser.id.toString(), email: gameUser.email, }); await this.createZulipAccountForUser(gameUser, password); this.logger.log(`Zulip账号创建成功 (第${attempt}次尝试)`, { operation: 'createZulipAccountWithRetry', operationId, attempt, gameUserId: gameUser.id.toString(), email: gameUser.email, }); return true; } catch (error) { lastError = error as Error; this.logger.warn(`Zulip账号创建失败 (第${attempt}/${maxRetries}次尝试)`, { operation: 'createZulipAccountWithRetry', operationId, attempt, maxRetries, gameUserId: gameUser.id.toString(), email: gameUser.email, error: lastError.message, }); // 如果不是最后一次尝试,等待后重试 if (attempt < maxRetries) { const delayMs = attempt * 1000; // 递增延迟:1秒、2秒、3秒 this.logger.log(`等待${delayMs}ms后重试`, { operation: 'createZulipAccountWithRetry', operationId, attempt, delayMs, }); await this.delay(delayMs); } } } // 所有重试都失败 this.logger.error(`Zulip账号创建最终失败,已尝试${maxRetries}次`, { operation: 'createZulipAccountWithRetry', operationId, maxRetries, gameUserId: gameUser.id.toString(), email: gameUser.email, finalError: lastError?.message, note: '用户注册已成功,但Zulip账号创建失败。用户可以正常使用游戏,但无法使用聊天功能。', }, lastError?.stack); return false; } /** * 延迟工具方法 * * @param ms 延迟毫秒数 * @returns Promise * @private */ private delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 为用户创建或绑定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账号创建/绑定失败'); } // 验证必须获取到 userId(数据库字段 NOT NULL) if (createResult.userId === undefined || createResult.userId === null) { throw new Error('Zulip账号创建成功但未能获取用户ID,无法建立关联'); } // 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, // 已在上面验证不为 undefined zulipEmail: createResult.email!, zulipFullName: gameUser.nickname, zulipApiKeyEncrypted: finalApiKey ? 'stored_in_redis' : '', status: 'active', }); // 注意:不在注册时建立内存关联,Zulip客户端将在WebSocket连接时创建 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; } } }