forked from xiangwang25/whale-town-end-v2
714 lines
23 KiB
TypeScript
714 lines
23 KiB
TypeScript
/**
|
||
* 注册业务服务
|
||
*
|
||
* 功能描述:
|
||
* - 处理用户注册相关的业务逻辑和流程控制
|
||
* - 整合核心服务,提供完整的注册功能
|
||
* - 处理业务规则、数据格式化和错误处理
|
||
* - 集成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<any>;
|
||
create(createDto: any): Promise<any>;
|
||
deleteByGameUserId(gameUserId: string): Promise<boolean>;
|
||
}
|
||
|
||
// 常量定义
|
||
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<T = any> {
|
||
/** 是否成功 */
|
||
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<ApiResponse<RegisterResponse>> {
|
||
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<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||
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<ApiResponse> {
|
||
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<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||
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<void> {
|
||
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<boolean> 是否创建成功
|
||
* @private
|
||
*/
|
||
private async createZulipAccountWithRetry(
|
||
gameUser: Users,
|
||
password: string,
|
||
operationId: string,
|
||
maxRetries: number = 3
|
||
): Promise<boolean> {
|
||
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<void>
|
||
* @private
|
||
*/
|
||
private delay(ms: number): Promise<void> {
|
||
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<void> {
|
||
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;
|
||
}
|
||
}
|
||
}
|