style(auth):优化auth模块代码规范和测试覆盖

范围:src/business/auth/
- 统一命名规范和注释格式
- 完善文件头部注释和修改记录
- 分离登录和注册业务逻辑到独立服务
- 添加缺失的测试文件(JWT守卫、控制器测试)
- 清理未使用的测试文件
- 优化代码结构和依赖关系
This commit is contained in:
moyin
2026-01-12 18:04:33 +08:00
parent 16ae78ed12
commit 267f1b2263
14 changed files with 1908 additions and 1739 deletions

View File

@@ -0,0 +1,578 @@
/**
* 注册业务服务
*
* 功能描述:
* - 处理用户注册相关的业务逻辑和流程控制
* - 整合核心服务,提供完整的注册功能
* - 处理业务规则、数据格式化和错误处理
* - 集成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;
}
}
}