- Add ZulipAccountsEntity, repository, and module for persistent Zulip account storage - Create ZulipAccountService in core layer for managing Zulip account lifecycle - Integrate Zulip account creation into login flow via LoginService - Add comprehensive test suite for Zulip account creation during user registration - Create quick test script for validating registered user Zulip integration - Update UsersEntity to support Zulip account associations - Update auth module to include Zulip and ZulipAccounts dependencies - Fix WebSocket connection protocol from ws:// to wss:// in API documentation - Enhance LoginCoreService to coordinate Zulip account provisioning during authentication
839 lines
26 KiB
TypeScript
839 lines
26 KiB
TypeScript
/**
|
||
* 登录业务服务
|
||
*
|
||
* 功能描述:
|
||
* - 处理登录相关的业务逻辑和流程控制
|
||
* - 整合核心服务,提供完整的业务功能
|
||
* - 处理业务规则、数据格式化和错误处理
|
||
*
|
||
* 职责分离:
|
||
* - 专注于业务流程和规则实现
|
||
* - 调用核心服务完成具体功能
|
||
* - 为控制器层提供业务接口
|
||
*
|
||
* @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<T = any> {
|
||
/** 是否成功 */
|
||
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<ApiResponse<LoginResponse>> {
|
||
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<ApiResponse<LoginResponse>> {
|
||
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<ApiResponse<LoginResponse>> {
|
||
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<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||
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<ApiResponse> {
|
||
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<ApiResponse> {
|
||
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<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||
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<ApiResponse> {
|
||
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<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||
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<ApiResponse<LoginResponse>> {
|
||
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<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||
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<any> {
|
||
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<void> {
|
||
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<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.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;
|
||
}
|
||
}
|
||
} |