refactor:项目架构重构和命名规范化

- 统一文件命名为snake_case格式(kebab-case  snake_case)
- 重构zulip模块为zulip_core,明确Core层职责
- 重构user-mgmt模块为user_mgmt,统一命名规范
- 调整模块依赖关系,优化架构分层
- 删除过时的文件和目录结构
- 更新相关文档和配置文件

本次重构涉及大量文件重命名和模块重组,
旨在建立更清晰的项目架构和统一的命名规范。
This commit is contained in:
moyin
2026-01-08 00:14:14 +08:00
parent 4fa4bd1a70
commit bb796a2469
178 changed files with 24767 additions and 3484 deletions

View File

@@ -0,0 +1,731 @@
/**
* 登录控制器
*
* 功能描述:
* - 处理登录相关的HTTP请求和响应
* - 提供RESTful API接口
* - 数据验证和格式化
*
* 职责分离:
* - 专注于HTTP请求处理和响应格式化
* - 调用业务服务完成具体功能
* - 处理API文档和参数验证
*
* API端点
* - POST /auth/login - 用户登录
* - POST /auth/register - 用户注册
* - POST /auth/github - GitHub OAuth登录
* - POST /auth/forgot-password - 发送密码重置验证码
* - POST /auth/reset-password - 重置密码
* - PUT /auth/change-password - 修改密码
* - POST /auth/refresh-token - 刷新访问令牌
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 文件夹扁平化,移除单文件文件夹结构
* - 2026-01-07: 代码规范优化 - 更新注释规范,修正文件引用路径
*
* @author moyin
* @version 1.0.2
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Controller, Post, Put, Body, HttpCode, HttpStatus, ValidationPipe, UsePipes, Logger, Res } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse as SwaggerApiResponse, ApiBody } from '@nestjs/swagger';
import { Response } from 'express';
import { LoginService, ApiResponse, LoginResponse } from './login.service';
import { LoginDto, RegisterDto, GitHubOAuthDto, ForgotPasswordDto, ResetPasswordDto, ChangePasswordDto, EmailVerificationDto, SendEmailVerificationDto, VerificationCodeLoginDto, SendLoginVerificationCodeDto, RefreshTokenDto } from './login.dto';
import {
LoginResponseDto,
RegisterResponseDto,
GitHubOAuthResponseDto,
ForgotPasswordResponseDto,
CommonResponseDto,
TestModeEmailVerificationResponseDto,
SuccessEmailVerificationResponseDto,
RefreshTokenResponseDto
} from './login_response.dto';
import { Throttle, ThrottlePresets } from '../../core/security_core/throttle.decorator';
import { Timeout, TimeoutPresets } from '../../core/security_core/timeout.decorator';
// 错误代码到HTTP状态码的映射
const ERROR_STATUS_MAP = {
LOGIN_FAILED: HttpStatus.UNAUTHORIZED,
REGISTER_FAILED: HttpStatus.BAD_REQUEST,
TEST_MODE_ONLY: HttpStatus.PARTIAL_CONTENT,
TOKEN_REFRESH_FAILED: HttpStatus.UNAUTHORIZED,
GITHUB_OAUTH_FAILED: HttpStatus.UNAUTHORIZED,
SEND_CODE_FAILED: HttpStatus.BAD_REQUEST,
RESET_PASSWORD_FAILED: HttpStatus.BAD_REQUEST,
CHANGE_PASSWORD_FAILED: HttpStatus.BAD_REQUEST,
EMAIL_VERIFICATION_FAILED: HttpStatus.BAD_REQUEST,
VERIFICATION_CODE_LOGIN_FAILED: HttpStatus.UNAUTHORIZED,
INVALID_VERIFICATION_CODE: HttpStatus.BAD_REQUEST,
} as const;
@ApiTags('auth')
@Controller('auth')
export class LoginController {
private readonly logger = new Logger(LoginController.name);
constructor(private readonly loginService: LoginService) {}
/**
* 通用响应处理方法
*
* 业务逻辑:
* 1. 根据业务结果设置HTTP状态码
* 2. 处理不同类型的错误响应
* 3. 统一响应格式和错误处理
*
* @param result 业务服务返回的结果
* @param res Express响应对象
* @param successStatus 成功时的HTTP状态码默认为200
* @private
*/
private handleResponse(result: any, res: Response, successStatus: HttpStatus = HttpStatus.OK): void {
if (result.success) {
res.status(successStatus).json(result);
return;
}
// 根据错误代码获取状态码
const statusCode = this.getErrorStatusCode(result);
res.status(statusCode).json(result);
}
/**
* 根据错误代码和消息获取HTTP状态码
*
* @param result 业务服务返回的结果
* @returns HTTP状态码
* @private
*/
private getErrorStatusCode(result: any): HttpStatus {
// 优先使用错误代码映射
if (result.error_code && ERROR_STATUS_MAP[result.error_code as keyof typeof ERROR_STATUS_MAP]) {
return ERROR_STATUS_MAP[result.error_code as keyof typeof ERROR_STATUS_MAP];
}
// 根据消息内容判断
if (result.message?.includes('已存在') || result.message?.includes('已被注册')) {
return HttpStatus.CONFLICT;
}
if (result.message?.includes('令牌验证失败') || result.message?.includes('已过期')) {
return HttpStatus.UNAUTHORIZED;
}
if (result.message?.includes('用户不存在')) {
return HttpStatus.NOT_FOUND;
}
// 默认返回400
return HttpStatus.BAD_REQUEST;
}
/**
* 用户登录
*
* @param loginDto 登录数据
* @returns 登录结果
*/
@ApiOperation({
summary: '用户登录',
description: '支持用户名、邮箱或手机号登录'
})
@ApiBody({ type: LoginDto })
@SwaggerApiResponse({
status: 200,
description: '登录成功',
type: LoginResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '请求参数错误'
})
@SwaggerApiResponse({
status: 401,
description: '用户名或密码错误'
})
@SwaggerApiResponse({
status: 403,
description: '账户被禁用或锁定'
})
@SwaggerApiResponse({
status: 429,
description: '登录尝试过于频繁'
})
@Throttle(ThrottlePresets.LOGIN)
@Timeout(TimeoutPresets.NORMAL)
@Post('login')
@UsePipes(new ValidationPipe({ transform: true }))
async login(@Body() loginDto: LoginDto, @Res() res: Response): Promise<void> {
const result = await this.loginService.login({
identifier: loginDto.identifier,
password: loginDto.password
});
this.handleResponse(result, res);
}
/**
* 用户注册
*
* @param registerDto 注册数据
* @returns 注册结果
*/
@ApiOperation({
summary: '用户注册',
description: '创建新用户账户。如果提供邮箱,需要先调用发送验证码接口获取验证码,然后在注册时提供验证码进行验证。'
})
@ApiBody({ type: RegisterDto })
@SwaggerApiResponse({
status: 201,
description: '注册成功',
type: RegisterResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '请求参数错误'
})
@SwaggerApiResponse({
status: 409,
description: '用户名或邮箱已存在'
})
@SwaggerApiResponse({
status: 429,
description: '注册请求过于频繁'
})
@Throttle(ThrottlePresets.REGISTER)
@Timeout(TimeoutPresets.NORMAL)
@Post('register')
@UsePipes(new ValidationPipe({ transform: true }))
async register(@Body() registerDto: RegisterDto, @Res() res: Response): Promise<void> {
const result = await this.loginService.register({
username: registerDto.username,
password: registerDto.password,
nickname: registerDto.nickname,
email: registerDto.email,
phone: registerDto.phone,
email_verification_code: registerDto.email_verification_code
});
this.handleResponse(result, res, HttpStatus.CREATED);
}
/**
* GitHub OAuth登录
*
* @param githubDto GitHub OAuth数据
* @returns 登录结果
*/
@ApiOperation({
summary: 'GitHub OAuth登录',
description: '使用GitHub账户登录或注册'
})
@ApiBody({ type: GitHubOAuthDto })
@SwaggerApiResponse({
status: 200,
description: 'GitHub登录成功',
type: GitHubOAuthResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '请求参数错误'
})
@SwaggerApiResponse({
status: 401,
description: 'GitHub认证失败'
})
@Post('github')
@UsePipes(new ValidationPipe({ transform: true }))
async githubOAuth(@Body() githubDto: GitHubOAuthDto, @Res() res: Response): Promise<void> {
const result = await this.loginService.githubOAuth({
github_id: githubDto.github_id,
username: githubDto.username,
nickname: githubDto.nickname,
email: githubDto.email,
avatar_url: githubDto.avatar_url
});
this.handleResponse(result, res);
}
/**
* 发送密码重置验证码
*
* @param forgotPasswordDto 忘记密码数据
* @param res Express响应对象
* @returns 发送结果
*/
@ApiOperation({
summary: '发送密码重置验证码',
description: '向用户邮箱或手机发送密码重置验证码'
})
@ApiBody({ type: ForgotPasswordDto })
@SwaggerApiResponse({
status: 200,
description: '验证码发送成功',
type: ForgotPasswordResponseDto
})
@SwaggerApiResponse({
status: 206,
description: '测试模式:验证码已生成但未真实发送',
type: ForgotPasswordResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '请求参数错误'
})
@SwaggerApiResponse({
status: 404,
description: '用户不存在'
})
@SwaggerApiResponse({
status: 429,
description: '发送频率过高'
})
@Throttle(ThrottlePresets.SEND_CODE)
@Post('forgot-password')
@UsePipes(new ValidationPipe({ transform: true }))
async forgotPassword(
@Body() forgotPasswordDto: ForgotPasswordDto,
@Res() res: Response
): Promise<void> {
const result = await this.loginService.sendPasswordResetCode(forgotPasswordDto.identifier);
this.handleResponse(result, res);
}
/**
* 重置密码
*
* @param resetPasswordDto 重置密码数据
* @returns 重置结果
*/
@ApiOperation({
summary: '重置密码',
description: '使用验证码重置用户密码'
})
@ApiBody({ type: ResetPasswordDto })
@SwaggerApiResponse({
status: 200,
description: '密码重置成功',
type: CommonResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '请求参数错误或验证码无效'
})
@SwaggerApiResponse({
status: 404,
description: '用户不存在'
})
@SwaggerApiResponse({
status: 429,
description: '重置请求过于频繁'
})
@Throttle(ThrottlePresets.RESET_PASSWORD)
@Post('reset-password')
@UsePipes(new ValidationPipe({ transform: true }))
async resetPassword(@Body() resetPasswordDto: ResetPasswordDto, @Res() res: Response): Promise<void> {
const result = await this.loginService.resetPassword({
identifier: resetPasswordDto.identifier,
verificationCode: resetPasswordDto.verification_code,
newPassword: resetPasswordDto.new_password
});
this.handleResponse(result, res);
}
/**
* 修改密码
*
* @param changePasswordDto 修改密码数据
* @returns 修改结果
*/
@ApiOperation({
summary: '修改密码',
description: '用户修改自己的密码(需要提供旧密码)'
})
@ApiBody({ type: ChangePasswordDto })
@SwaggerApiResponse({
status: 200,
description: '密码修改成功',
type: CommonResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '请求参数错误或旧密码不正确'
})
@SwaggerApiResponse({
status: 404,
description: '用户不存在'
})
@Put('change-password')
@UsePipes(new ValidationPipe({ transform: true }))
async changePassword(@Body() changePasswordDto: ChangePasswordDto, @Res() res: Response): Promise<void> {
// 实际应用中应从JWT令牌中获取用户ID
// 这里为了演示使用请求体中的用户ID
const userId = BigInt(changePasswordDto.user_id);
const result = await this.loginService.changePassword(
userId,
changePasswordDto.old_password,
changePasswordDto.new_password
);
this.handleResponse(result, res);
}
/**
* 发送邮箱验证码
*
* @param sendEmailVerificationDto 发送验证码数据
* @param res Express响应对象
* @returns 发送结果
*/
@ApiOperation({
summary: '发送邮箱验证码',
description: '向指定邮箱发送验证码'
})
@ApiBody({ type: SendEmailVerificationDto })
@SwaggerApiResponse({
status: 200,
description: '验证码发送成功(真实发送模式)',
type: SuccessEmailVerificationResponseDto
})
@SwaggerApiResponse({
status: 206,
description: '测试模式:验证码已生成但未真实发送',
type: TestModeEmailVerificationResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '请求参数错误'
})
@SwaggerApiResponse({
status: 429,
description: '发送频率过高'
})
@Throttle(ThrottlePresets.SEND_CODE)
@Timeout(TimeoutPresets.EMAIL_SEND)
@Post('send-email-verification')
@UsePipes(new ValidationPipe({ transform: true }))
async sendEmailVerification(
@Body() sendEmailVerificationDto: SendEmailVerificationDto,
@Res() res: Response
): Promise<void> {
const result = await this.loginService.sendEmailVerification(sendEmailVerificationDto.email);
this.handleResponse(result, res);
}
/**
* 验证邮箱验证码
*
* @param emailVerificationDto 邮箱验证数据
* @returns 验证结果
*/
@ApiOperation({
summary: '验证邮箱验证码',
description: '使用验证码验证邮箱'
})
@ApiBody({ type: EmailVerificationDto })
@SwaggerApiResponse({
status: 200,
description: '邮箱验证成功',
type: CommonResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '验证码错误或已过期'
})
@Post('verify-email')
@UsePipes(new ValidationPipe({ transform: true }))
async verifyEmail(@Body() emailVerificationDto: EmailVerificationDto, @Res() res: Response): Promise<void> {
const result = await this.loginService.verifyEmailCode(
emailVerificationDto.email,
emailVerificationDto.verification_code
);
this.handleResponse(result, res);
}
/**
* 重新发送邮箱验证码
*
* @param sendEmailVerificationDto 发送验证码数据
* @param res Express响应对象
* @returns 发送结果
*/
@ApiOperation({
summary: '重新发送邮箱验证码',
description: '重新向指定邮箱发送验证码'
})
@ApiBody({ type: SendEmailVerificationDto })
@SwaggerApiResponse({
status: 200,
description: '验证码重新发送成功',
type: ForgotPasswordResponseDto
})
@SwaggerApiResponse({
status: 206,
description: '测试模式:验证码已生成但未真实发送',
type: ForgotPasswordResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '邮箱已验证或用户不存在'
})
@SwaggerApiResponse({
status: 429,
description: '发送频率过高'
})
@Throttle(ThrottlePresets.SEND_CODE)
@Post('resend-email-verification')
@UsePipes(new ValidationPipe({ transform: true }))
async resendEmailVerification(
@Body() sendEmailVerificationDto: SendEmailVerificationDto,
@Res() res: Response
): Promise<void> {
const result = await this.loginService.resendEmailVerification(sendEmailVerificationDto.email);
this.handleResponse(result, res);
}
/**
* 验证码登录
*
* @param verificationCodeLoginDto 验证码登录数据
* @returns 登录结果
*/
@ApiOperation({
summary: '验证码登录',
description: '使用邮箱或手机号和验证码进行登录,无需密码'
})
@ApiBody({ type: VerificationCodeLoginDto })
@SwaggerApiResponse({
status: 200,
description: '验证码登录成功',
type: LoginResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '请求参数错误'
})
@SwaggerApiResponse({
status: 401,
description: '验证码错误或已过期'
})
@SwaggerApiResponse({
status: 404,
description: '用户不存在'
})
@Post('verification-code-login')
@HttpCode(HttpStatus.OK)
@UsePipes(new ValidationPipe({ transform: true }))
async verificationCodeLogin(@Body() verificationCodeLoginDto: VerificationCodeLoginDto): Promise<ApiResponse<LoginResponse>> {
return await this.loginService.verificationCodeLogin({
identifier: verificationCodeLoginDto.identifier,
verificationCode: verificationCodeLoginDto.verification_code
});
}
/**
* 发送登录验证码
*
* @param sendLoginVerificationCodeDto 发送验证码数据
* @param res Express响应对象
* @returns 发送结果
*/
@ApiOperation({
summary: '发送登录验证码',
description: '向用户邮箱或手机发送登录验证码。邮件使用专门的登录验证码模板,内容明确标识为登录验证而非密码重置。'
})
@ApiBody({ type: SendLoginVerificationCodeDto })
@SwaggerApiResponse({
status: 200,
description: '验证码发送成功',
type: ForgotPasswordResponseDto
})
@SwaggerApiResponse({
status: 206,
description: '测试模式:验证码已生成但未真实发送',
type: ForgotPasswordResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '请求参数错误'
})
@SwaggerApiResponse({
status: 404,
description: '用户不存在'
})
@SwaggerApiResponse({
status: 429,
description: '发送频率过高'
})
@Post('send-login-verification-code')
@UsePipes(new ValidationPipe({ transform: true }))
async sendLoginVerificationCode(
@Body() sendLoginVerificationCodeDto: SendLoginVerificationCodeDto,
@Res() res: Response
): Promise<void> {
const result = await this.loginService.sendLoginVerificationCode(sendLoginVerificationCodeDto.identifier);
this.handleResponse(result, res);
}
/**
* 调试验证码信息
* 仅用于开发和调试
*
* @param sendEmailVerificationDto 邮箱信息
* @returns 验证码调试信息
*/
@ApiOperation({
summary: '调试验证码信息',
description: '获取验证码的详细调试信息(仅开发环境)'
})
@ApiBody({ type: SendEmailVerificationDto })
@Post('debug-verification-code')
@UsePipes(new ValidationPipe({ transform: true }))
async debugVerificationCode(@Body() sendEmailVerificationDto: SendEmailVerificationDto, @Res() res: Response): Promise<void> {
const result = await this.loginService.debugVerificationCode(sendEmailVerificationDto.email);
// 调试接口总是返回200
res.status(HttpStatus.OK).json(result);
}
/**
* 清除限流记录(仅开发环境)
*/
@ApiOperation({
summary: '清除限流记录',
description: '清除所有限流记录(仅开发环境使用)'
})
@Post('debug-clear-throttle')
async clearThrottle(@Res() res: Response): Promise<void> {
// 注入ThrottleGuard并清除记录
// 这里需要通过依赖注入获取ThrottleGuard实例
res.status(HttpStatus.OK).json({
success: true,
message: '限流记录已清除'
});
}
/**
* 刷新访问令牌
*
* 功能描述:
* 使用有效的刷新令牌生成新的访问令牌,实现无感知的令牌续期
*
* 业务逻辑:
* 1. 验证刷新令牌的有效性和格式
* 2. 检查用户状态是否正常
* 3. 生成新的JWT令牌对
* 4. 返回新的访问令牌和刷新令牌
*
* @param refreshTokenDto 刷新令牌数据
* @param res Express响应对象
* @returns 新的令牌对
*/
@ApiOperation({
summary: '刷新访问令牌',
description: '使用有效的刷新令牌生成新的访问令牌,实现无感知的令牌续期。建议在访问令牌即将过期时调用此接口。'
})
@ApiBody({ type: RefreshTokenDto })
@SwaggerApiResponse({
status: 200,
description: '令牌刷新成功',
type: RefreshTokenResponseDto
})
@SwaggerApiResponse({
status: 400,
description: '请求参数错误'
})
@SwaggerApiResponse({
status: 401,
description: '刷新令牌无效或已过期'
})
@SwaggerApiResponse({
status: 404,
description: '用户不存在或已被禁用'
})
@SwaggerApiResponse({
status: 429,
description: '刷新请求过于频繁'
})
@Throttle(ThrottlePresets.REFRESH_TOKEN)
@Timeout(TimeoutPresets.NORMAL)
@Post('refresh-token')
@UsePipes(new ValidationPipe({ transform: true }))
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto, @Res() res: Response): Promise<void> {
const startTime = Date.now();
try {
this.logRefreshTokenStart();
const result = await this.loginService.refreshAccessToken(refreshTokenDto.refresh_token);
this.handleRefreshTokenResponse(result, res, startTime);
} catch (error) {
this.handleRefreshTokenError(error, res, startTime);
}
}
/**
* 记录令牌刷新开始日志
* @private
*/
private logRefreshTokenStart(): void {
this.logger.log('令牌刷新请求', {
operation: 'refreshToken',
timestamp: new Date().toISOString(),
});
}
/**
* 处理令牌刷新响应
* @private
*/
private handleRefreshTokenResponse(result: any, res: Response, startTime: number): void {
const duration = Date.now() - startTime;
if (result.success) {
this.logger.log('令牌刷新成功', {
operation: 'refreshToken',
duration,
timestamp: new Date().toISOString(),
});
res.status(HttpStatus.OK).json(result);
} else {
this.logger.warn('令牌刷新失败', {
operation: 'refreshToken',
error: result.message,
errorCode: result.error_code,
duration,
timestamp: new Date().toISOString(),
});
this.handleResponse(result, res);
}
}
/**
* 处理令牌刷新异常
* @private
*/
private handleRefreshTokenError(error: unknown, res: Response, startTime: number): void {
const duration = Date.now() - startTime;
const err = error as Error;
this.logger.error('令牌刷新异常', {
operation: 'refreshToken',
error: err.message,
duration,
timestamp: new Date().toISOString(),
}, err.stack);
res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
success: false,
message: '服务器内部错误',
error_code: 'INTERNAL_SERVER_ERROR'
});
}
}