范围:src/business/auth/ - 统一命名规范和注释格式 - 完善文件头部注释和修改记录 - 分离登录和注册业务逻辑到独立服务 - 添加缺失的测试文件(JWT守卫、控制器测试) - 清理未使用的测试文件 - 优化代码结构和依赖关系
578 lines
18 KiB
TypeScript
578 lines
18 KiB
TypeScript
/**
|
||
* 注册业务服务
|
||
*
|
||
* 功能描述:
|
||
* - 处理用户注册相关的业务逻辑和流程控制
|
||
* - 整合核心服务,提供完整的注册功能
|
||
* - 处理业务规则、数据格式化和错误处理
|
||
* - 集成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<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: '注册成功',
|
||
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<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,
|
||
) {}
|
||
|
||
/**
|
||
* 用户注册
|
||
*
|
||
* @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)}`;
|
||
|
||
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<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||
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<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账号
|
||
*
|
||
* 功能描述:
|
||
* 为新注册的游戏用户创建对应的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账号创建/绑定失败');
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
}
|
||
} |