Initial WhaleTown V2 backend
This commit is contained in:
55
src/gateway/auth/auth.gateway.module.ts
Normal file
55
src/gateway/auth/auth.gateway.module.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 认证网关模块
|
||||
*
|
||||
* 架构层级:Gateway Layer(网关层)
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合所有认证相关的网关组件
|
||||
* - 提供HTTP API接口
|
||||
* - 配置认证守卫和中间件
|
||||
* - 处理请求验证和响应格式化
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于HTTP协议处理和API网关功能
|
||||
* - 依赖业务层服务,不包含业务逻辑
|
||||
* - 提供统一的API入口和文档
|
||||
*
|
||||
* 依赖关系:
|
||||
* - 依赖 Business Layer 的 AuthModule
|
||||
* - 提供 Controller 和 Guard
|
||||
*
|
||||
* @author moyin
|
||||
* @version 2.0.0
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LoginController } from './login.controller';
|
||||
import { RegisterController } from './register.controller';
|
||||
import { JwtAuthGuard } from './jwt_auth.guard';
|
||||
import { AuthModule } from '../../business/auth/auth.module';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// 导入业务层模块
|
||||
AuthModule,
|
||||
// 导入核心层模块(JwtAuthGuard需要LoginCoreService)
|
||||
LoginCoreModule,
|
||||
],
|
||||
controllers: [
|
||||
// 网关层控制器
|
||||
LoginController,
|
||||
RegisterController,
|
||||
],
|
||||
providers: [
|
||||
// 认证守卫
|
||||
JwtAuthGuard,
|
||||
],
|
||||
exports: [
|
||||
// 导出守卫供其他模块使用
|
||||
JwtAuthGuard,
|
||||
],
|
||||
})
|
||||
export class AuthGatewayModule {}
|
||||
69
src/gateway/auth/current_user.decorator.ts
Normal file
69
src/gateway/auth/current_user.decorator.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 当前用户装饰器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 从请求上下文中提取当前认证用户信息
|
||||
* - 简化控制器中获取用户信息的操作
|
||||
* - 支持获取用户对象的特定属性
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于用户信息提取和参数装饰
|
||||
* - 提供类型安全的用户信息访问
|
||||
* - 简化控制器方法的参数处理
|
||||
*
|
||||
* 使用示例:
|
||||
* ```typescript
|
||||
* @Get('profile')
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* getProfile(@CurrentUser() user: JwtPayload) {
|
||||
* return { user };
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 文件夹扁平化,移除单文件文件夹结构
|
||||
* - 2026-01-07: 代码规范优化 - 文件重命名为snake_case格式,更新注释规范
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-01-05
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { JwtPayload } from '../../core/login_core/login_core.service';
|
||||
import { AuthenticatedRequest } from './jwt_auth.guard';
|
||||
|
||||
/**
|
||||
* 当前用户装饰器实现
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 从执行上下文获取HTTP请求对象
|
||||
* 2. 提取请求中的用户信息(由JwtAuthGuard注入)
|
||||
* 3. 根据data参数返回完整用户对象或特定属性
|
||||
* 4. 提供类型安全的用户信息访问
|
||||
*
|
||||
* @param data 可选的属性名,用于获取用户对象的特定属性
|
||||
* @param ctx 执行上下文,包含HTTP请求信息
|
||||
* @returns JwtPayload | any 用户信息或用户的特定属性
|
||||
* @throws 无异常抛出,依赖JwtAuthGuard确保用户信息存在
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 获取完整用户对象
|
||||
* @Get('profile')
|
||||
* getProfile(@CurrentUser() user: JwtPayload) { }
|
||||
*
|
||||
* // 获取特定属性
|
||||
* @Get('username')
|
||||
* getUsername(@CurrentUser('username') username: string) { }
|
||||
* ```
|
||||
*/
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: keyof JwtPayload | undefined, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest<AuthenticatedRequest>();
|
||||
const user = request.user;
|
||||
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
469
src/gateway/auth/dto/login.dto.ts
Normal file
469
src/gateway/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
/**
|
||||
* 登录业务数据传输对象
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义登录相关API的请求数据结构
|
||||
* - 提供数据验证规则和错误提示
|
||||
* - 确保API接口的数据格式一致性
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于数据结构定义和验证规则
|
||||
* - 提供Swagger文档生成支持
|
||||
* - 确保类型安全和数据完整性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 文件夹扁平化,移除单文件文件夹结构
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,修正作者信息
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-17
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import {
|
||||
IsString,
|
||||
IsEmail,
|
||||
IsPhoneNumber,
|
||||
IsNotEmpty,
|
||||
Length,
|
||||
IsOptional,
|
||||
Matches,
|
||||
IsNumberString
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 登录请求DTO
|
||||
*/
|
||||
export class LoginDto {
|
||||
/**
|
||||
* 登录标识符
|
||||
* 支持用户名、邮箱或手机号登录
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '登录标识符,支持用户名、邮箱或手机号',
|
||||
example: 'testuser',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: '登录标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '登录标识符不能为空' })
|
||||
@Length(1, 100, { message: '登录标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户密码',
|
||||
example: 'password123',
|
||||
minLength: 1,
|
||||
maxLength: 128
|
||||
})
|
||||
@IsString({ message: '密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '密码不能为空' })
|
||||
@Length(1, 128, { message: '密码长度需在1-128字符之间' })
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册请求DTO
|
||||
*/
|
||||
export class RegisterDto {
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户名,只能包含字母、数字和下划线',
|
||||
example: 'testuser',
|
||||
minLength: 1,
|
||||
maxLength: 50,
|
||||
pattern: '^[a-zA-Z0-9_]+$'
|
||||
})
|
||||
@IsString({ message: '用户名必须是字符串' })
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
@Length(1, 50, { message: '用户名长度需在1-50字符之间' })
|
||||
@Matches(/^[a-zA-Z0-9_]+$/, { message: '用户名只能包含字母、数字和下划线' })
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '密码,必须包含字母和数字,长度8-128字符',
|
||||
example: 'password123',
|
||||
minLength: 8,
|
||||
maxLength: 128
|
||||
})
|
||||
@IsString({ message: '密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '密码不能为空' })
|
||||
@Length(8, 128, { message: '密码长度需在8-128字符之间' })
|
||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '密码必须包含字母和数字' })
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户昵称',
|
||||
example: '测试用户',
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
})
|
||||
@IsString({ message: '昵称必须是字符串' })
|
||||
@IsNotEmpty({ message: '昵称不能为空' })
|
||||
@Length(1, 50, { message: '昵称长度需在1-50字符之间' })
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱地址,注册前必须先发送验证码',
|
||||
example: 'test@example.com',
|
||||
required: true
|
||||
})
|
||||
@IsNotEmpty({ message: '邮箱不能为空' })
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* 初始角色皮肤ID(可选)
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '初始角色皮肤ID(可选)',
|
||||
example: 'classic_whale',
|
||||
required: false,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '皮肤ID必须是字符串' })
|
||||
@Length(1, 100, { message: '皮肤ID长度需在1-100字符之间' })
|
||||
@Matches(/^[A-Za-z0-9_:-]+$/, { message: '皮肤ID格式不正确' })
|
||||
skin_id?: string;
|
||||
|
||||
/**
|
||||
* 手机号(可选)
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '手机号码(可选)',
|
||||
example: '+8613800138000',
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsPhoneNumber(null, { message: '手机号格式不正确' })
|
||||
phone?: string;
|
||||
|
||||
/**
|
||||
* 邮箱验证码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱验证码,必须先通过 /auth/send-email-verification 发送到注册邮箱',
|
||||
example: '123456',
|
||||
pattern: '^\\d{6}$',
|
||||
required: true
|
||||
})
|
||||
@IsNotEmpty({ message: '邮箱验证码不能为空' })
|
||||
@IsString({ message: '验证码必须是字符串' })
|
||||
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
|
||||
email_verification_code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth登录请求DTO
|
||||
*/
|
||||
export class GitHubOAuthDto {
|
||||
/**
|
||||
* GitHub用户ID
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'GitHub用户ID',
|
||||
example: '12345678',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: 'GitHub ID必须是字符串' })
|
||||
@IsNotEmpty({ message: 'GitHub ID不能为空' })
|
||||
@Length(1, 100, { message: 'GitHub ID长度需在1-100字符之间' })
|
||||
github_id: string;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'GitHub用户名',
|
||||
example: 'octocat',
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
})
|
||||
@IsString({ message: '用户名必须是字符串' })
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
@Length(1, 50, { message: '用户名长度需在1-50字符之间' })
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'GitHub显示名称',
|
||||
example: 'The Octocat',
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
})
|
||||
@IsString({ message: '昵称必须是字符串' })
|
||||
@IsNotEmpty({ message: '昵称不能为空' })
|
||||
@Length(1, 50, { message: '昵称长度需在1-50字符之间' })
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* 邮箱(可选)
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'GitHub邮箱地址(可选)',
|
||||
example: 'octocat@github.com',
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* 头像URL(可选)
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'GitHub头像URL(可选)',
|
||||
example: 'https://github.com/images/error/octocat_happy.gif',
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '头像URL必须是字符串' })
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码请求DTO
|
||||
*/
|
||||
export class ForgotPasswordDto {
|
||||
/**
|
||||
* 邮箱或手机号
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱或手机号',
|
||||
example: 'test@example.com',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: '标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '邮箱或手机号不能为空' })
|
||||
@Length(1, 100, { message: '标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码请求DTO
|
||||
*/
|
||||
export class ResetPasswordDto {
|
||||
/**
|
||||
* 邮箱或手机号
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱或手机号',
|
||||
example: 'test@example.com',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: '标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '邮箱或手机号不能为空' })
|
||||
@Length(1, 100, { message: '标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '6位数字验证码',
|
||||
example: '123456',
|
||||
pattern: '^\\d{6}$'
|
||||
})
|
||||
@IsString({ message: '验证码必须是字符串' })
|
||||
@IsNotEmpty({ message: '验证码不能为空' })
|
||||
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
|
||||
verification_code: string;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '新密码,必须包含字母和数字,长度8-128字符',
|
||||
example: 'newpassword123',
|
||||
minLength: 8,
|
||||
maxLength: 128
|
||||
})
|
||||
@IsString({ message: '新密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '新密码不能为空' })
|
||||
@Length(8, 128, { message: '新密码长度需在8-128字符之间' })
|
||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '新密码必须包含字母和数字' })
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码请求DTO
|
||||
*/
|
||||
export class ChangePasswordDto {
|
||||
/**
|
||||
* 用户ID
|
||||
* 实际应用中应从JWT令牌中获取,这里为了演示放在请求体中
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户ID(实际应用中应从JWT令牌中获取)',
|
||||
example: '1'
|
||||
})
|
||||
@IsNumberString({}, { message: '用户ID必须是数字字符串' })
|
||||
@IsNotEmpty({ message: '用户ID不能为空' })
|
||||
user_id: string;
|
||||
|
||||
/**
|
||||
* 旧密码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '当前密码',
|
||||
example: 'oldpassword123',
|
||||
minLength: 1,
|
||||
maxLength: 128
|
||||
})
|
||||
@IsString({ message: '旧密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '旧密码不能为空' })
|
||||
@Length(1, 128, { message: '旧密码长度需在1-128字符之间' })
|
||||
old_password: string;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '新密码,必须包含字母和数字,长度8-128字符',
|
||||
example: 'newpassword123',
|
||||
minLength: 8,
|
||||
maxLength: 128
|
||||
})
|
||||
@IsString({ message: '新密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '新密码不能为空' })
|
||||
@Length(8, 128, { message: '新密码长度需在8-128字符之间' })
|
||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '新密码必须包含字母和数字' })
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱验证请求DTO
|
||||
*/
|
||||
export class EmailVerificationDto {
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱地址',
|
||||
example: 'test@example.com'
|
||||
})
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
@IsNotEmpty({ message: '邮箱不能为空' })
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '6位数字验证码',
|
||||
example: '123456',
|
||||
pattern: '^\\d{6}$'
|
||||
})
|
||||
@IsString({ message: '验证码必须是字符串' })
|
||||
@IsNotEmpty({ message: '验证码不能为空' })
|
||||
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
|
||||
verification_code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码请求DTO
|
||||
*/
|
||||
export class SendEmailVerificationDto {
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱地址',
|
||||
example: 'test@example.com'
|
||||
})
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
@IsNotEmpty({ message: '邮箱不能为空' })
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码登录请求DTO
|
||||
*/
|
||||
export class VerificationCodeLoginDto {
|
||||
/**
|
||||
* 登录标识符
|
||||
* 支持邮箱或手机号登录
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '登录标识符,支持邮箱或手机号',
|
||||
example: 'test@example.com',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: '登录标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '登录标识符不能为空' })
|
||||
@Length(1, 100, { message: '登录标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '6位数字验证码',
|
||||
example: '123456',
|
||||
pattern: '^\\d{6}$'
|
||||
})
|
||||
@IsString({ message: '验证码必须是字符串' })
|
||||
@IsNotEmpty({ message: '验证码不能为空' })
|
||||
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
|
||||
verification_code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送登录验证码请求DTO
|
||||
*/
|
||||
export class SendLoginVerificationCodeDto {
|
||||
/**
|
||||
* 登录标识符
|
||||
* 支持邮箱或手机号
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '登录标识符,支持邮箱或手机号',
|
||||
example: 'test@example.com',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: '登录标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '登录标识符不能为空' })
|
||||
@Length(1, 100, { message: '登录标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌请求DTO
|
||||
*/
|
||||
export class RefreshTokenDto {
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'JWT刷新令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
minLength: 1
|
||||
})
|
||||
@IsString({ message: '刷新令牌必须是字符串' })
|
||||
@IsNotEmpty({ message: '刷新令牌不能为空' })
|
||||
refresh_token: string;
|
||||
}
|
||||
479
src/gateway/auth/dto/login_response.dto.ts
Normal file
479
src/gateway/auth/dto/login_response.dto.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
/**
|
||||
* 登录业务响应数据传输对象
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义登录相关API的响应数据结构
|
||||
* - 提供Swagger文档生成支持
|
||||
* - 确保API响应的数据格式一致性
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于响应数据结构定义
|
||||
* - 提供完整的API文档支持
|
||||
* - 确保响应格式的统一性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 文件夹扁平化,移除单文件文件夹结构
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,修正作者信息
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-17
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 用户信息响应DTO
|
||||
*/
|
||||
export class UserInfoDto {
|
||||
@ApiProperty({
|
||||
description: '用户ID',
|
||||
example: '1'
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户名',
|
||||
example: 'testuser'
|
||||
})
|
||||
username: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户昵称',
|
||||
example: '测试用户'
|
||||
})
|
||||
nickname: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '邮箱地址',
|
||||
example: 'test@example.com',
|
||||
required: false
|
||||
})
|
||||
email?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '手机号码',
|
||||
example: '+8613800138000',
|
||||
required: false
|
||||
})
|
||||
phone?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '头像URL',
|
||||
example: 'https://example.com/avatar.jpg',
|
||||
required: false
|
||||
})
|
||||
avatar_url?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户角色',
|
||||
example: 1
|
||||
})
|
||||
role: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '创建时间',
|
||||
example: '2025-12-17T10:00:00.000Z'
|
||||
})
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应数据DTO
|
||||
*/
|
||||
export class LoginResponseDataDto {
|
||||
@ApiProperty({
|
||||
description: '用户信息',
|
||||
type: UserInfoDto
|
||||
})
|
||||
user: UserInfoDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'JWT访问令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
})
|
||||
access_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'JWT刷新令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
})
|
||||
refresh_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '访问令牌过期时间(秒)',
|
||||
example: 604800
|
||||
})
|
||||
expires_in: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '令牌类型',
|
||||
example: 'Bearer'
|
||||
})
|
||||
token_type: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '是否为新用户',
|
||||
example: false,
|
||||
required: false
|
||||
})
|
||||
is_new_user?: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '登录成功'
|
||||
})
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应DTO
|
||||
*/
|
||||
export class LoginResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: LoginResponseDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: LoginResponseDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '登录成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'LOGIN_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册响应DTO
|
||||
*/
|
||||
export class RegisterResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: LoginResponseDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: LoginResponseDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '注册成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'REGISTER_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth响应DTO
|
||||
*/
|
||||
export class GitHubOAuthResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: LoginResponseDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: LoginResponseDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: 'GitHub登录成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'GITHUB_OAUTH_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码响应数据DTO
|
||||
*/
|
||||
export class ForgotPasswordResponseDataDto {
|
||||
@ApiProperty({
|
||||
description: '验证码(仅用于演示,实际应用中不应返回)',
|
||||
example: '123456',
|
||||
required: false
|
||||
})
|
||||
verification_code?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '是否为测试模式',
|
||||
example: true,
|
||||
required: false
|
||||
})
|
||||
is_test_mode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码响应DTO
|
||||
*/
|
||||
export class ForgotPasswordResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: false,
|
||||
examples: {
|
||||
success: {
|
||||
summary: '真实发送成功',
|
||||
value: true
|
||||
},
|
||||
testMode: {
|
||||
summary: '测试模式',
|
||||
value: false
|
||||
}
|
||||
}
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: ForgotPasswordResponseDataDto,
|
||||
required: false,
|
||||
examples: {
|
||||
success: {
|
||||
summary: '真实发送成功',
|
||||
value: {
|
||||
verification_code: '123456',
|
||||
is_test_mode: false
|
||||
}
|
||||
},
|
||||
testMode: {
|
||||
summary: '测试模式',
|
||||
value: {
|
||||
verification_code: '059174',
|
||||
is_test_mode: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
data?: ForgotPasswordResponseDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。',
|
||||
examples: {
|
||||
success: {
|
||||
summary: '真实发送成功',
|
||||
value: '验证码已发送,请查收'
|
||||
},
|
||||
testMode: {
|
||||
summary: '测试模式',
|
||||
value: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。'
|
||||
}
|
||||
}
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'TEST_MODE_ONLY',
|
||||
examples: {
|
||||
success: {
|
||||
summary: '真实发送成功',
|
||||
value: null
|
||||
},
|
||||
testMode: {
|
||||
summary: '测试模式',
|
||||
value: 'TEST_MODE_ONLY'
|
||||
},
|
||||
failed: {
|
||||
summary: '发送失败',
|
||||
value: 'SEND_CODE_FAILED'
|
||||
}
|
||||
},
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用响应DTO(用于重置密码、修改密码等)
|
||||
*/
|
||||
export class CommonResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '操作成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'OPERATION_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试模式邮件验证码响应DTO
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2025-12-17: 功能新增 - 添加测试模式响应DTO (修改者: angjustinl)
|
||||
*/
|
||||
export class TestModeEmailVerificationResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功(测试模式下为false)',
|
||||
example: false
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
example: {
|
||||
verification_code: '059174',
|
||||
is_test_mode: true
|
||||
}
|
||||
})
|
||||
data: {
|
||||
verification_code: string;
|
||||
is_test_mode: boolean;
|
||||
};
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'TEST_MODE_ONLY'
|
||||
})
|
||||
error_code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功发送邮件验证码响应DTO
|
||||
*/
|
||||
export class SuccessEmailVerificationResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
example: {
|
||||
verification_code: '123456',
|
||||
is_test_mode: false
|
||||
}
|
||||
})
|
||||
data: {
|
||||
verification_code: string;
|
||||
is_test_mode: boolean;
|
||||
};
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '验证码已发送,请查收'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: null,
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌刷新响应数据DTO
|
||||
*/
|
||||
export class RefreshTokenResponseDataDto {
|
||||
@ApiProperty({
|
||||
description: 'JWT访问令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
})
|
||||
access_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'JWT刷新令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
})
|
||||
refresh_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '访问令牌过期时间(秒)',
|
||||
example: 604800
|
||||
})
|
||||
expires_in: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '令牌类型',
|
||||
example: 'Bearer'
|
||||
})
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌刷新响应DTO
|
||||
*/
|
||||
export class RefreshTokenResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: RefreshTokenResponseDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: RefreshTokenResponseDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '令牌刷新成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'TOKEN_REFRESH_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
119
src/gateway/auth/jwt_auth.guard.ts
Normal file
119
src/gateway/auth/jwt_auth.guard.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* JWT 认证守卫
|
||||
*
|
||||
* 功能描述:
|
||||
* - 验证请求中的 JWT 令牌
|
||||
* - 提取用户信息并添加到请求上下文
|
||||
* - 保护需要认证的路由
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于JWT令牌验证和用户认证
|
||||
* - 提供统一的认证守卫机制
|
||||
* - 处理认证失败的异常情况
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 文件夹扁平化,移除单文件文件夹结构
|
||||
* - 2026-01-07: 代码规范优化 - 文件重命名为snake_case格式,更新注释规范
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-01-05
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { LoginCoreService, JwtPayload } from '../../core/login_core/login_core.service';
|
||||
|
||||
/**
|
||||
* 扩展的请求接口,包含用户信息
|
||||
*/
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user: JwtPayload;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(JwtAuthGuard.name);
|
||||
|
||||
constructor(private readonly loginCoreService: LoginCoreService) {}
|
||||
|
||||
/**
|
||||
* JWT令牌验证和用户认证
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 从请求头中提取Bearer令牌
|
||||
* 2. 验证令牌的有效性和签名
|
||||
* 3. 解码令牌获取用户信息
|
||||
* 4. 将用户信息添加到请求上下文
|
||||
* 5. 记录认证成功或失败的日志
|
||||
* 6. 返回认证结果
|
||||
*
|
||||
* @param context 执行上下文,包含HTTP请求信息
|
||||
* @returns Promise<boolean> 认证是否成功
|
||||
* @throws UnauthorizedException 当令牌缺失或无效时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Get('protected')
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* getProtectedData() {
|
||||
* // 此方法需要有效的JWT令牌才能访问
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
if (!token) {
|
||||
this.logger.warn('访问被拒绝:缺少认证令牌');
|
||||
throw new UnauthorizedException('缺少认证令牌');
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用Core层服务验证JWT令牌
|
||||
const payload = await this.loginCoreService.verifyToken(token, 'access');
|
||||
|
||||
// 将用户信息添加到请求对象
|
||||
(request as AuthenticatedRequest).user = payload;
|
||||
|
||||
this.logger.log(`用户认证成功: ${payload.username} (ID: ${payload.sub})`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
this.logger.warn(`JWT 令牌验证失败: ${errorMessage}`);
|
||||
throw new UnauthorizedException('无效的认证令牌');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取JWT令牌
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 获取Authorization请求头
|
||||
* 2. 解析Bearer令牌格式
|
||||
* 3. 验证令牌类型是否为Bearer
|
||||
* 4. 返回提取的令牌字符串
|
||||
*
|
||||
* @param request HTTP请求对象
|
||||
* @returns string | undefined JWT令牌字符串或undefined
|
||||
* @throws 无异常抛出,返回undefined表示令牌不存在
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 请求头格式:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
* const token = this.extractTokenFromHeader(request);
|
||||
* ```
|
||||
*/
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
503
src/gateway/auth/login.controller.ts
Normal file
503
src/gateway/auth/login.controller.ts
Normal file
@@ -0,0 +1,503 @@
|
||||
/**
|
||||
* 登录网关控制器
|
||||
*
|
||||
* 架构层级:Gateway Layer(网关层)
|
||||
*
|
||||
* 功能描述:
|
||||
* - 处理登录相关的HTTP请求和响应
|
||||
* - 提供RESTful API接口
|
||||
* - 数据验证和格式化
|
||||
* - 协议处理和错误响应
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于HTTP协议处理和请求响应
|
||||
* - 调用业务层服务完成具体功能
|
||||
* - 处理API文档和参数验证
|
||||
* - 不包含业务逻辑,只做数据转换和路由
|
||||
*
|
||||
* 依赖关系:
|
||||
* - 依赖 Business Layer 的 LoginService
|
||||
* - 使用 DTO 进行数据验证
|
||||
* - 使用 Guard 进行认证保护
|
||||
*
|
||||
* API端点:
|
||||
* - POST /auth/login - 用户登录
|
||||
* - POST /auth/github - GitHub OAuth登录
|
||||
* - POST /auth/forgot-password - 发送密码重置验证码
|
||||
* - POST /auth/reset-password - 重置密码
|
||||
* - PUT /auth/change-password - 修改密码
|
||||
* - POST /auth/refresh-token - 刷新访问令牌
|
||||
* - POST /auth/verification-code-login - 验证码登录
|
||||
* - POST /auth/send-login-verification-code - 发送登录验证码
|
||||
*
|
||||
* @author moyin
|
||||
* @version 2.0.0
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
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 } from '../../business/auth/login.service';
|
||||
import {
|
||||
LoginDto,
|
||||
GitHubOAuthDto,
|
||||
ForgotPasswordDto,
|
||||
ResetPasswordDto,
|
||||
ChangePasswordDto,
|
||||
VerificationCodeLoginDto,
|
||||
SendLoginVerificationCodeDto,
|
||||
RefreshTokenDto,
|
||||
SendEmailVerificationDto
|
||||
} from './dto/login.dto';
|
||||
import {
|
||||
LoginResponseDto,
|
||||
GitHubOAuthResponseDto,
|
||||
ForgotPasswordResponseDto,
|
||||
CommonResponseDto,
|
||||
RefreshTokenResponseDto
|
||||
} from './dto/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,
|
||||
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,
|
||||
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) {}
|
||||
|
||||
/**
|
||||
* 通用响应处理方法
|
||||
*
|
||||
* 职责:
|
||||
* - 根据业务结果设置HTTP状态码
|
||||
* - 处理不同类型的错误响应
|
||||
* - 统一响应格式和错误处理
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
return HttpStatus.BAD_REQUEST;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param loginDto 登录数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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_PER_ACCOUNT)
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth登录
|
||||
*
|
||||
* @param githubDto GitHub OAuth数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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响应对象
|
||||
*/
|
||||
@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 重置密码数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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 修改密码数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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> {
|
||||
const userId = BigInt(changePasswordDto.user_id);
|
||||
|
||||
const result = await this.loginService.changePassword(
|
||||
userId,
|
||||
changePasswordDto.old_password,
|
||||
changePasswordDto.new_password
|
||||
);
|
||||
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码登录
|
||||
*
|
||||
* @param verificationCodeLoginDto 验证码登录数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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,
|
||||
@Res() res: Response
|
||||
): Promise<void> {
|
||||
const result = await this.loginService.verificationCodeLogin({
|
||||
identifier: verificationCodeLoginDto.identifier,
|
||||
verificationCode: verificationCodeLoginDto.verification_code
|
||||
});
|
||||
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送登录验证码
|
||||
*
|
||||
* @param sendLoginVerificationCodeDto 发送验证码数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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 refreshTokenDto 刷新令牌数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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 result = await this.loginService.refreshAccessToken(refreshTokenDto.refresh_token);
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试验证码信息(仅开发环境)
|
||||
*
|
||||
* @param sendEmailVerificationDto 邮箱信息
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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);
|
||||
res.status(HttpStatus.OK).json(result);
|
||||
}
|
||||
}
|
||||
286
src/gateway/auth/register.controller.ts
Normal file
286
src/gateway/auth/register.controller.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 注册网关控制器
|
||||
*
|
||||
* 架构层级:Gateway Layer(网关层)
|
||||
*
|
||||
* 功能描述:
|
||||
* - 处理用户注册相关的HTTP请求和响应
|
||||
* - 提供RESTful API接口
|
||||
* - 数据验证和格式化
|
||||
* - 邮箱验证功能
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于HTTP协议处理和请求响应
|
||||
* - 调用业务层服务完成具体功能
|
||||
* - 处理API文档和参数验证
|
||||
* - 不包含业务逻辑,只做数据转换和路由
|
||||
*
|
||||
* 依赖关系:
|
||||
* - 依赖 Business Layer 的 RegisterService
|
||||
* - 使用 DTO 进行数据验证
|
||||
*
|
||||
* API端点:
|
||||
* - POST /auth/register - 用户注册
|
||||
* - POST /auth/send-email-verification - 发送邮箱验证码
|
||||
* - POST /auth/verify-email - 验证邮箱验证码
|
||||
* - POST /auth/resend-email-verification - 重新发送邮箱验证码
|
||||
*
|
||||
* @author moyin
|
||||
* @version 2.0.0
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
HttpStatus,
|
||||
ValidationPipe,
|
||||
UsePipes,
|
||||
Logger,
|
||||
Res
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse as SwaggerApiResponse,
|
||||
ApiBody
|
||||
} from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { RegisterService } from '../../business/auth/register.service';
|
||||
import {
|
||||
RegisterDto,
|
||||
EmailVerificationDto,
|
||||
SendEmailVerificationDto
|
||||
} from './dto/login.dto';
|
||||
import {
|
||||
RegisterResponseDto,
|
||||
CommonResponseDto,
|
||||
TestModeEmailVerificationResponseDto,
|
||||
SuccessEmailVerificationResponseDto
|
||||
} from './dto/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 = {
|
||||
REGISTER_FAILED: HttpStatus.BAD_REQUEST,
|
||||
TEST_MODE_ONLY: HttpStatus.PARTIAL_CONTENT,
|
||||
SEND_EMAIL_VERIFICATION_FAILED: HttpStatus.BAD_REQUEST,
|
||||
EMAIL_VERIFICATION_FAILED: HttpStatus.BAD_REQUEST,
|
||||
RESEND_EMAIL_VERIFICATION_FAILED: HttpStatus.BAD_REQUEST,
|
||||
INVALID_VERIFICATION_CODE: HttpStatus.BAD_REQUEST,
|
||||
} as const;
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
export class RegisterController {
|
||||
private readonly logger = new Logger(RegisterController.name);
|
||||
|
||||
constructor(private readonly registerService: RegisterService) {}
|
||||
|
||||
/**
|
||||
* 通用响应处理方法
|
||||
*
|
||||
* 职责:
|
||||
* - 根据业务结果设置HTTP状态码
|
||||
* - 处理不同类型的错误响应
|
||||
* - 统一响应格式和错误处理
|
||||
*
|
||||
* @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('用户不存在')) {
|
||||
return HttpStatus.NOT_FOUND;
|
||||
}
|
||||
|
||||
return HttpStatus.BAD_REQUEST;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param registerDto 注册数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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.registerService.register({
|
||||
username: registerDto.username,
|
||||
password: registerDto.password,
|
||||
nickname: registerDto.nickname,
|
||||
email: registerDto.email,
|
||||
phone: registerDto.phone,
|
||||
skin_id: registerDto.skin_id,
|
||||
email_verification_code: registerDto.email_verification_code
|
||||
});
|
||||
|
||||
this.handleResponse(result, res, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码
|
||||
*
|
||||
* @param sendEmailVerificationDto 发送验证码数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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_PER_EMAIL)
|
||||
@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.registerService.sendEmailVerification(sendEmailVerificationDto.email);
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱验证码
|
||||
*
|
||||
* @param emailVerificationDto 邮箱验证数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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.registerService.verifyEmailCode(
|
||||
emailVerificationDto.email,
|
||||
emailVerificationDto.verification_code
|
||||
);
|
||||
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新发送邮箱验证码
|
||||
*
|
||||
* @param sendEmailVerificationDto 发送验证码数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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)
|
||||
@Post('resend-email-verification')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async resendEmailVerification(
|
||||
@Body() sendEmailVerificationDto: SendEmailVerificationDto,
|
||||
@Res() res: Response
|
||||
): Promise<void> {
|
||||
const result = await this.registerService.resendEmailVerification(sendEmailVerificationDto.email);
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
}
|
||||
195
src/gateway/chat/chat.controller.ts
Normal file
195
src/gateway/chat/chat.controller.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 聊天 HTTP 控制器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 处理聊天相关的 REST API 请求
|
||||
* - 只做协议转换,不包含业务逻辑
|
||||
* - 提供聊天历史查询和系统状态接口
|
||||
*
|
||||
* 架构层级:Gateway Layer(网关层)
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-14: 代码规范优化 - 处理未使用的参数 (修改者: moyin)
|
||||
* - 2026-01-14: 代码规范优化 - 完善注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Body,
|
||||
Query,
|
||||
UseGuards,
|
||||
HttpStatus,
|
||||
HttpException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/jwt_auth.guard';
|
||||
import { ChatService } from '../../business/chat/chat.service';
|
||||
import { ChatWebSocketGateway } from './chat.gateway';
|
||||
import { SendChatMessageDto, GetChatHistoryDto } from './chat.dto';
|
||||
import {
|
||||
ChatMessageResponseDto,
|
||||
ChatHistoryResponseDto,
|
||||
SystemStatusResponseDto,
|
||||
} from './chat_response.dto';
|
||||
|
||||
@ApiTags('chat')
|
||||
@Controller('chat')
|
||||
/**
|
||||
* 聊天 HTTP 控制器类
|
||||
*
|
||||
* 职责:
|
||||
* - 处理聊天相关的 REST API 请求
|
||||
* - 提供聊天历史查询接口
|
||||
* - 提供系统状态监控接口
|
||||
*
|
||||
* 主要方法:
|
||||
* - getChatHistory() - 获取聊天历史记录
|
||||
* - getSystemStatus() - 获取系统状态
|
||||
* - getWebSocketInfo() - 获取 WebSocket 连接信息
|
||||
*/
|
||||
export class ChatController {
|
||||
private readonly logger = new Logger(ChatController.name);
|
||||
|
||||
constructor(
|
||||
private readonly chatService: ChatService,
|
||||
private readonly websocketGateway: ChatWebSocketGateway,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 发送聊天消息(REST API 方式)
|
||||
*
|
||||
* @param dto 发送消息请求参数
|
||||
* @returns 消息发送响应
|
||||
* @throws HttpException 聊天消息需要通过 WebSocket 发送
|
||||
*/
|
||||
@Post('send')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: '发送聊天消息',
|
||||
description: '通过 REST API 发送聊天消息。推荐使用 WebSocket 接口以获得更好的实时性。'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: '消息发送成功', type: ChatMessageResponseDto })
|
||||
@ApiResponse({ status: 400, description: '请求参数错误' })
|
||||
@ApiResponse({ status: 401, description: '未授权访问' })
|
||||
async sendMessage(@Body() _dto: SendChatMessageDto): Promise<ChatMessageResponseDto> {
|
||||
this.logger.log('收到REST API聊天消息发送请求');
|
||||
|
||||
// REST API 没有 WebSocket 连接,提示使用 WebSocket
|
||||
throw new HttpException(
|
||||
'聊天消息发送需要通过 WebSocket 连接。请使用 WebSocket 接口:wss://whaletownend.xinghangee.icu/game',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天历史记录
|
||||
*
|
||||
* @param query 查询参数(mapId, limit, offset)
|
||||
* @returns 聊天历史响应
|
||||
* @throws HttpException 获取失败时抛出异常
|
||||
*/
|
||||
@Get('history')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: '获取聊天历史记录' })
|
||||
@ApiQuery({ name: 'mapId', required: false, description: '地图ID' })
|
||||
@ApiQuery({ name: 'limit', required: false, description: '消息数量限制' })
|
||||
@ApiQuery({ name: 'offset', required: false, description: '偏移量' })
|
||||
@ApiResponse({ status: 200, description: '获取成功', type: ChatHistoryResponseDto })
|
||||
async getChatHistory(@Query() query: GetChatHistoryDto): Promise<ChatHistoryResponseDto> {
|
||||
this.logger.log('获取聊天历史记录', { mapId: query.mapId });
|
||||
|
||||
try {
|
||||
const result = await this.chatService.getChatHistory(query);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error('获取聊天历史失败', error);
|
||||
throw new HttpException('获取聊天历史失败', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统状态
|
||||
*
|
||||
* @returns 系统状态响应(WebSocket连接数、Zulip状态、内存使用等)
|
||||
* @throws HttpException 获取失败时抛出异常
|
||||
*/
|
||||
@Get('status')
|
||||
@ApiOperation({ summary: '获取聊天系统状态' })
|
||||
@ApiResponse({ status: 200, description: '获取成功', type: SystemStatusResponseDto })
|
||||
async getSystemStatus(): Promise<SystemStatusResponseDto> {
|
||||
try {
|
||||
const totalConnections = this.websocketGateway.getConnectionCount();
|
||||
const authenticatedConnections = this.websocketGateway.getAuthenticatedConnectionCount();
|
||||
const mapPlayerCounts = this.websocketGateway.getMapPlayerCounts();
|
||||
|
||||
const memoryUsage = process.memoryUsage();
|
||||
const memoryUsedMB = (memoryUsage.heapUsed / 1024 / 1024).toFixed(1);
|
||||
const memoryTotalMB = (memoryUsage.heapTotal / 1024 / 1024).toFixed(1);
|
||||
const memoryPercentage = (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100;
|
||||
|
||||
return {
|
||||
websocket: {
|
||||
totalConnections,
|
||||
authenticatedConnections,
|
||||
activeSessions: authenticatedConnections,
|
||||
mapPlayerCounts,
|
||||
},
|
||||
zulip: {
|
||||
serverConnected: true,
|
||||
serverVersion: '11.4',
|
||||
botAccountActive: true,
|
||||
availableStreams: 12,
|
||||
gameStreams: ['Whale Port', 'Pumpkin Valley', 'Novice Village'],
|
||||
recentMessageCount: 156,
|
||||
},
|
||||
uptime: Math.floor(process.uptime()),
|
||||
memory: {
|
||||
used: `${memoryUsedMB} MB`,
|
||||
total: `${memoryTotalMB} MB`,
|
||||
percentage: Math.round(memoryPercentage * 100) / 100,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('获取系统状态失败', error);
|
||||
throw new HttpException('获取系统状态失败', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 连接信息
|
||||
*
|
||||
* @returns WebSocket 连接配置信息
|
||||
*/
|
||||
@Get('websocket/info')
|
||||
@ApiOperation({ summary: '获取 WebSocket 连接信息' })
|
||||
async getWebSocketInfo() {
|
||||
return {
|
||||
websocketUrl: 'wss://whaletownend.xinghangee.icu/game',
|
||||
protocol: 'native-websocket',
|
||||
path: '/game',
|
||||
supportedEvents: ['login', 'chat', 'position'],
|
||||
supportedResponses: [
|
||||
'connected', 'login_success', 'login_error',
|
||||
'chat_sent', 'chat_error', 'chat_render', 'error'
|
||||
],
|
||||
authRequired: true,
|
||||
tokenType: 'JWT',
|
||||
};
|
||||
}
|
||||
}
|
||||
126
src/gateway/chat/chat.dto.ts
Normal file
126
src/gateway/chat/chat.dto.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 聊天网关层 DTO 定义
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义聊天相关的数据传输对象
|
||||
* - 用于 HTTP 和 WebSocket 请求的数据验证
|
||||
* - 提供请求参数的类型约束和校验规则
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-14: 代码规范优化 - 清理未使用的导入 (修改者: moyin)
|
||||
* - 2026-01-14: 代码规范优化 - 完善注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsNumber } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
/**
|
||||
* 发送聊天消息请求 DTO
|
||||
*/
|
||||
export class SendChatMessageDto {
|
||||
@ApiProperty({
|
||||
description: '消息内容',
|
||||
example: '大家好!我刚进入游戏',
|
||||
maxLength: 1000
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
content: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '消息范围',
|
||||
example: 'local',
|
||||
enum: ['local', 'global'],
|
||||
default: 'local'
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
scope: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '地图ID(可选,用于地图相关消息)',
|
||||
example: 'whale_port'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mapId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天历史请求 DTO
|
||||
*/
|
||||
export class GetChatHistoryDto {
|
||||
@ApiPropertyOptional({
|
||||
description: '地图ID(可选)',
|
||||
example: 'whale_port'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mapId?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '消息数量限制',
|
||||
example: 50,
|
||||
default: 50,
|
||||
minimum: 1,
|
||||
maximum: 100
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
limit?: number = 50;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '偏移量(分页用)',
|
||||
example: 0,
|
||||
default: 0,
|
||||
minimum: 0
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
offset?: number = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 登录消息 DTO
|
||||
*/
|
||||
export class WsLoginDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 聊天消息 DTO
|
||||
*/
|
||||
export class WsChatMessageDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
content: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 位置更新 DTO
|
||||
*/
|
||||
export class WsPositionUpdateDto {
|
||||
@IsNumber()
|
||||
x: number;
|
||||
|
||||
@IsNumber()
|
||||
y: number;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
mapId: string;
|
||||
}
|
||||
46
src/gateway/chat/chat.gateway.module.ts
Normal file
46
src/gateway/chat/chat.gateway.module.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 聊天网关模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合聊天相关的网关层组件
|
||||
* - 提供 WebSocket 和 HTTP 协议处理
|
||||
*
|
||||
* 架构层级:Gateway Layer(网关层)
|
||||
*
|
||||
* 依赖关系:
|
||||
* - 依赖 ChatModule(业务层)处理业务逻辑
|
||||
* - 依赖 LoginCoreModule 进行 JWT 验证
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-14: 代码规范优化 - 完善注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ChatController } from './chat.controller';
|
||||
import { ChatWebSocketGateway } from './chat.gateway';
|
||||
import { ChatModule } from '../../business/chat/chat.module';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// 业务层模块
|
||||
ChatModule,
|
||||
// 登录核心模块 - 用于 JWT 验证
|
||||
LoginCoreModule,
|
||||
],
|
||||
controllers: [
|
||||
ChatController,
|
||||
],
|
||||
providers: [
|
||||
ChatWebSocketGateway,
|
||||
],
|
||||
exports: [
|
||||
ChatWebSocketGateway,
|
||||
],
|
||||
})
|
||||
export class ChatGatewayModule {}
|
||||
1047
src/gateway/chat/chat.gateway.ts
Normal file
1047
src/gateway/chat/chat.gateway.ts
Normal file
File diff suppressed because it is too large
Load Diff
135
src/gateway/chat/chat_response.dto.ts
Normal file
135
src/gateway/chat/chat_response.dto.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 聊天网关层响应 DTO 定义
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义聊天相关的响应数据传输对象
|
||||
* - 用于 HTTP 和 WebSocket 响应的数据结构
|
||||
* - 提供 Swagger API 文档的响应类型定义
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-14: 代码规范优化 - 完善注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
/**
|
||||
* 聊天消息响应 DTO
|
||||
*/
|
||||
export class ChatMessageResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: '消息ID', example: 'game_1234567890_user1' })
|
||||
messageId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '响应消息', example: '消息发送成功' })
|
||||
message?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '错误信息', example: '消息内容不能为空' })
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天消息信息 DTO
|
||||
*/
|
||||
export class ChatMessageInfoDto {
|
||||
@ApiProperty({ description: '消息ID', example: 12345 })
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: '发送者用户名', example: 'Player_123' })
|
||||
sender: string;
|
||||
|
||||
@ApiProperty({ description: '消息内容', example: '大家好!' })
|
||||
content: string;
|
||||
|
||||
@ApiProperty({ description: '消息范围', example: 'local' })
|
||||
scope: string;
|
||||
|
||||
@ApiProperty({ description: '地图ID', example: 'whale_port' })
|
||||
mapId: string;
|
||||
|
||||
@ApiProperty({ description: '发送时间', example: '2026-01-14T14:30:00.000Z' })
|
||||
timestamp: string;
|
||||
|
||||
@ApiProperty({ description: 'Zulip Stream 名称', example: 'Whale Port' })
|
||||
streamName: string;
|
||||
|
||||
@ApiProperty({ description: 'Zulip Topic 名称', example: 'Game Chat' })
|
||||
topicName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天历史响应 DTO
|
||||
*/
|
||||
export class ChatHistoryResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息列表', type: [ChatMessageInfoDto] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ChatMessageInfoDto)
|
||||
messages: ChatMessageInfoDto[];
|
||||
|
||||
@ApiProperty({ description: '总消息数', example: 150 })
|
||||
total: number;
|
||||
|
||||
@ApiProperty({ description: '当前页消息数', example: 50 })
|
||||
count: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '错误信息', example: '获取消息历史失败' })
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 连接状态 DTO
|
||||
*/
|
||||
export class WebSocketStatusDto {
|
||||
@ApiProperty({ description: '总连接数', example: 25 })
|
||||
totalConnections: number;
|
||||
|
||||
@ApiProperty({ description: '已认证连接数', example: 20 })
|
||||
authenticatedConnections: number;
|
||||
|
||||
@ApiProperty({ description: '活跃会话数', example: 18 })
|
||||
activeSessions: number;
|
||||
|
||||
@ApiProperty({ description: '各地图在线人数' })
|
||||
mapPlayerCounts: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统状态响应 DTO
|
||||
*/
|
||||
export class SystemStatusResponseDto {
|
||||
@ApiProperty({ description: 'WebSocket 状态', type: WebSocketStatusDto })
|
||||
@ValidateNested()
|
||||
@Type(() => WebSocketStatusDto)
|
||||
websocket: WebSocketStatusDto;
|
||||
|
||||
@ApiProperty({ description: 'Zulip 集成状态' })
|
||||
zulip: {
|
||||
serverConnected: boolean;
|
||||
serverVersion: string;
|
||||
botAccountActive: boolean;
|
||||
availableStreams: number;
|
||||
gameStreams: string[];
|
||||
recentMessageCount: number;
|
||||
};
|
||||
|
||||
@ApiProperty({ description: '系统运行时间(秒)', example: 86400 })
|
||||
uptime: number;
|
||||
|
||||
@ApiProperty({ description: '内存使用情况' })
|
||||
memory: {
|
||||
used: string;
|
||||
total: string;
|
||||
percentage: number;
|
||||
};
|
||||
}
|
||||
603
src/gateway/zulip/dynamic_config.controller.ts
Normal file
603
src/gateway/zulip/dynamic_config.controller.ts
Normal file
@@ -0,0 +1,603 @@
|
||||
/**
|
||||
* 统一配置管理控制器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供统一配置管理的REST API接口
|
||||
* - 支持配置查询、同步、状态检查
|
||||
* - 提供备份管理功能
|
||||
*
|
||||
* 架构定位:
|
||||
* - 层级:Gateway层(网关层)
|
||||
* - 职责:HTTP协议处理、API接口暴露
|
||||
* - 依赖:调用Business层的ZulipModule服务
|
||||
*
|
||||
* 职责分离:
|
||||
* - API接口:提供RESTful风格的配置管理接口
|
||||
* - 协议处理:处理HTTP请求和响应
|
||||
* - 参数验证:验证请求参数格式
|
||||
* - 错误转换:将业务异常转换为HTTP响应
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-14: 架构优化 - 从Business层迁移到Gateway层,符合四层架构规范 (修改者: moyin)
|
||||
* - 2026-01-14: 代码规范优化 - 完善文件头注释和职责分离描述 (修改者: moyin)
|
||||
* - 2026-01-12: 功能新增 - 初始创建统一配置管理控制器 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 3.0.0
|
||||
* @since 2026-01-12
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Query,
|
||||
HttpStatus,
|
||||
HttpException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { DynamicConfigManagerService } from '../../core/zulip_core/services/dynamic_config_manager.service';
|
||||
|
||||
@ApiTags('unified-config')
|
||||
@Controller('api/zulip/config')
|
||||
export class DynamicConfigController {
|
||||
private readonly logger = new Logger(DynamicConfigController.name);
|
||||
|
||||
constructor(
|
||||
private readonly configManager: DynamicConfigManagerService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
*/
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: '获取当前配置',
|
||||
description: '获取当前的统一配置(自动从本地加载,如需最新数据请先调用同步接口)'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '配置获取成功',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
data: { type: 'object' },
|
||||
source: { type: 'string', enum: ['remote', 'local', 'default'] },
|
||||
timestamp: { type: 'string' }
|
||||
}
|
||||
}
|
||||
})
|
||||
async getCurrentConfig() {
|
||||
try {
|
||||
this.logger.log('获取当前配置');
|
||||
|
||||
const config = await this.configManager.getConfig();
|
||||
const status = this.configManager.getConfigStatus();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: config,
|
||||
source: config.source || 'unknown',
|
||||
lastSyncTime: status.lastSyncTime,
|
||||
mapCount: status.mapCount,
|
||||
objectCount: status.objectCount,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取配置失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置状态
|
||||
*/
|
||||
@Get('status')
|
||||
@ApiOperation({
|
||||
summary: '获取配置状态',
|
||||
description: '获取统一配置管理器的状态信息'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '状态获取成功'
|
||||
})
|
||||
async getConfigStatus() {
|
||||
try {
|
||||
const status = this.configManager.getConfigStatus();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...status,
|
||||
lastSyncTimeAgo: status.lastSyncTime ?
|
||||
Math.round((Date.now() - status.lastSyncTime.getTime()) / 60000) : null,
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取配置状态失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试Zulip连接
|
||||
*/
|
||||
@Get('test-connection')
|
||||
@ApiOperation({
|
||||
summary: '测试Zulip连接',
|
||||
description: '测试与Zulip服务器的连接状态'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '连接测试完成'
|
||||
})
|
||||
async testConnection() {
|
||||
try {
|
||||
this.logger.log('测试Zulip连接');
|
||||
|
||||
const connected = await this.configManager.testZulipConnection();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
connected,
|
||||
message: connected ? 'Zulip连接正常' : 'Zulip连接失败'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('连接测试失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
data: {
|
||||
connected: false,
|
||||
message: '连接测试异常'
|
||||
},
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步远程配置
|
||||
*/
|
||||
@Post('sync')
|
||||
@ApiOperation({
|
||||
summary: '同步远程配置',
|
||||
description: '手动触发从Zulip服务器同步配置到本地文件'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '配置同步完成'
|
||||
})
|
||||
async syncConfig() {
|
||||
try {
|
||||
this.logger.log('手动同步配置');
|
||||
|
||||
const result = await this.configManager.syncConfig();
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
data: {
|
||||
source: result.source,
|
||||
mapCount: result.mapCount,
|
||||
objectCount: result.objectCount,
|
||||
lastUpdated: result.lastUpdated,
|
||||
backupCreated: result.backupCreated,
|
||||
message: result.success ? '配置同步成功' : '配置同步失败'
|
||||
},
|
||||
error: result.error,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('同步配置失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Stream列表
|
||||
*/
|
||||
@Get('streams')
|
||||
@ApiOperation({
|
||||
summary: '获取Zulip Stream列表',
|
||||
description: '直接从Zulip服务器获取Stream列表'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Stream列表获取成功'
|
||||
})
|
||||
async getStreams() {
|
||||
try {
|
||||
this.logger.log('获取Zulip Stream列表');
|
||||
|
||||
const streams = await this.configManager.getZulipStreams();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
streams: streams.map(stream => ({
|
||||
id: stream.stream_id,
|
||||
name: stream.name,
|
||||
description: stream.description,
|
||||
isPublic: !stream.invite_only,
|
||||
isWebPublic: stream.is_web_public
|
||||
})),
|
||||
count: streams.length
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取Stream列表失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定Stream的Topic列表
|
||||
*/
|
||||
@Get('topics')
|
||||
@ApiOperation({
|
||||
summary: '获取Stream的Topic列表',
|
||||
description: '获取指定Stream的所有Topic'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'streamId',
|
||||
description: 'Stream ID',
|
||||
required: true,
|
||||
type: 'number'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Topic列表获取成功'
|
||||
})
|
||||
async getTopics(@Query('streamId') streamId: string) {
|
||||
try {
|
||||
const streamIdNum = parseInt(streamId, 10);
|
||||
if (isNaN(streamIdNum)) {
|
||||
throw new Error('无效的Stream ID');
|
||||
}
|
||||
|
||||
this.logger.log('获取Stream Topic列表', { streamId: streamIdNum });
|
||||
|
||||
const topics = await this.configManager.getZulipTopics(streamIdNum);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
streamId: streamIdNum,
|
||||
topics: topics.map(topic => ({
|
||||
name: topic.name,
|
||||
lastMessageId: topic.max_id
|
||||
})),
|
||||
count: topics.length
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取Topic列表失败', {
|
||||
streamId,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询地图配置
|
||||
*/
|
||||
@Get('maps')
|
||||
@ApiOperation({
|
||||
summary: '获取地图配置列表',
|
||||
description: '获取所有地图的配置信息'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '地图配置获取成功'
|
||||
})
|
||||
async getMaps() {
|
||||
try {
|
||||
this.logger.log('获取地图配置列表');
|
||||
|
||||
const maps = await this.configManager.getAllMapConfigs();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
maps: maps.map(map => ({
|
||||
mapId: map.mapId,
|
||||
mapName: map.mapName,
|
||||
zulipStream: map.zulipStream,
|
||||
zulipStreamId: map.zulipStreamId,
|
||||
description: map.description,
|
||||
isPublic: map.isPublic,
|
||||
objectCount: map.interactionObjects?.length || 0
|
||||
})),
|
||||
count: maps.length
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取地图配置失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据地图ID获取Stream
|
||||
*/
|
||||
@Get('map-to-stream')
|
||||
@ApiOperation({
|
||||
summary: '地图ID转Stream名称',
|
||||
description: '根据地图ID获取对应的Zulip Stream名称'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'mapId',
|
||||
description: '地图ID',
|
||||
required: true,
|
||||
type: 'string'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '转换成功'
|
||||
})
|
||||
async mapToStream(@Query('mapId') mapId: string) {
|
||||
try {
|
||||
this.logger.log('地图ID转Stream', { mapId });
|
||||
|
||||
const streamName = await this.configManager.getStreamByMap(mapId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
mapId,
|
||||
streamName,
|
||||
found: !!streamName
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('地图ID转Stream失败', {
|
||||
mapId,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Stream名称获取地图ID
|
||||
*/
|
||||
@Get('stream-to-map')
|
||||
@ApiOperation({
|
||||
summary: 'Stream名称转地图ID',
|
||||
description: '根据Zulip Stream名称获取对应的地图ID'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'streamName',
|
||||
description: 'Stream名称',
|
||||
required: true,
|
||||
type: 'string'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '转换成功'
|
||||
})
|
||||
async streamToMap(@Query('streamName') streamName: string) {
|
||||
try {
|
||||
this.logger.log('Stream转地图ID', { streamName });
|
||||
|
||||
const mapId = await this.configManager.getMapIdByStream(streamName);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
streamName,
|
||||
mapId,
|
||||
found: !!mapId
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Stream转地图ID失败', {
|
||||
streamName,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份文件列表
|
||||
*/
|
||||
@Get('backups')
|
||||
@ApiOperation({
|
||||
summary: '获取备份文件列表',
|
||||
description: '获取所有配置备份文件的列表'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '备份列表获取成功'
|
||||
})
|
||||
async getBackups() {
|
||||
try {
|
||||
this.logger.log('获取备份文件列表');
|
||||
|
||||
const backups = this.configManager.getBackupFiles();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
backups: backups.map(backup => ({
|
||||
name: backup.name,
|
||||
size: backup.size,
|
||||
created: backup.created,
|
||||
sizeKB: Math.round(backup.size / 1024)
|
||||
})),
|
||||
count: backups.length
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取备份列表失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从备份恢复配置
|
||||
*/
|
||||
@Post('restore')
|
||||
@ApiOperation({
|
||||
summary: '从备份恢复配置',
|
||||
description: '从指定的备份文件恢复配置'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'backupFile',
|
||||
description: '备份文件名',
|
||||
required: true,
|
||||
type: 'string'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '配置恢复完成'
|
||||
})
|
||||
async restoreFromBackup(@Query('backupFile') backupFile: string) {
|
||||
try {
|
||||
this.logger.log('从备份恢复配置', { backupFile });
|
||||
|
||||
const success = await this.configManager.restoreFromBackup(backupFile);
|
||||
|
||||
return {
|
||||
success,
|
||||
data: {
|
||||
backupFile,
|
||||
message: success ? '配置恢复成功' : '配置恢复失败'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('配置恢复失败', {
|
||||
backupFile,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/gateway/zulip/zulip.gateway.module.ts
Normal file
48
src/gateway/zulip/zulip.gateway.module.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Zulip网关模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip相关的HTTP API接口
|
||||
* - 提供动态配置管理接口
|
||||
* - 提供Zulip账号管理接口
|
||||
*
|
||||
* 架构说明:
|
||||
* - Gateway层:负责HTTP协议处理和API接口暴露
|
||||
* - 依赖Business层:调用ZulipModule提供的业务服务
|
||||
* - 职责分离:只做协议转换,不包含业务逻辑
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-14: 架构优化 - 从Business层分离Controller到Gateway层,符合四层架构规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
// Gateway层控制器
|
||||
import { DynamicConfigController } from './dynamic_config.controller';
|
||||
import { ZulipAccountsController } from './zulip_accounts.controller';
|
||||
// 依赖Business层模块
|
||||
import { ZulipModule } from '../../business/zulip/zulip.module';
|
||||
import { AuthModule } from '../../business/auth/auth.module';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// 导入Business层的Zulip模块
|
||||
ZulipModule,
|
||||
// 导入认证模块(用于JwtAuthGuard)
|
||||
AuthModule,
|
||||
// 导入登录核心模块(JwtAuthGuard依赖)
|
||||
LoginCoreModule,
|
||||
],
|
||||
controllers: [
|
||||
// 动态配置管理控制器
|
||||
DynamicConfigController,
|
||||
// Zulip账号关联管理控制器
|
||||
ZulipAccountsController,
|
||||
],
|
||||
})
|
||||
export class ZulipGatewayModule {}
|
||||
683
src/gateway/zulip/zulip_accounts.controller.ts
Normal file
683
src/gateway/zulip/zulip_accounts.controller.ts
Normal file
@@ -0,0 +1,683 @@
|
||||
/**
|
||||
* Zulip账号关联管理控制器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip账号关联管理的REST API接口
|
||||
* - 支持CRUD操作和批量管理
|
||||
* - 提供账号验证和统计功能
|
||||
* - 集成性能监控和结构化日志记录
|
||||
* - 实现统一的错误处理和响应格式
|
||||
*
|
||||
* 架构定位:
|
||||
* - 层级:Gateway层(网关层)
|
||||
* - 职责:HTTP协议处理、API接口暴露
|
||||
* - 依赖:调用Business层的ZulipAccountsBusinessService
|
||||
*
|
||||
* 职责分离:
|
||||
* - API接口:提供RESTful风格的HTTP接口
|
||||
* - 参数验证:使用DTO进行请求参数验证
|
||||
* - 业务调用:调用Service层处理业务逻辑
|
||||
* - 响应格式:统一API响应格式和错误处理
|
||||
* - 性能监控:记录接口调用耗时和性能指标
|
||||
* - 日志记录:使用AppLoggerService记录结构化日志
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-14: 架构优化 - 从Business层迁移到Gateway层,符合四层架构规范 (修改者: moyin)
|
||||
* - 2026-01-14: 代码质量优化 - 移除未使用的requestLogger属性 (修改者: moyin)
|
||||
* - 2026-01-14: 代码质量优化 - 移除未使用的导入 (修改者: moyin)
|
||||
* - 2026-01-12: 性能优化 - 集成AppLoggerService和性能监控,优化错误处理
|
||||
* - 2025-01-07: 初始创建 - 实现基础的CRUD和管理接口
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 2.0.0
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
HttpStatus,
|
||||
HttpCode,
|
||||
Inject,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { JwtAuthGuard } from '../auth/jwt_auth.guard';
|
||||
import { AppLoggerService } from '../../core/utils/logger/logger.service';
|
||||
import {
|
||||
CreateZulipAccountDto,
|
||||
UpdateZulipAccountDto,
|
||||
QueryZulipAccountDto,
|
||||
ZulipAccountResponseDto,
|
||||
ZulipAccountListResponseDto,
|
||||
ZulipAccountStatsResponseDto,
|
||||
BatchUpdateStatusDto,
|
||||
BatchUpdateResponseDto,
|
||||
VerifyAccountDto,
|
||||
VerifyAccountResponseDto,
|
||||
} from '../../core/db/zulip_accounts/zulip_accounts.dto';
|
||||
|
||||
@ApiTags('zulip-accounts')
|
||||
@Controller('zulip-accounts')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
export class ZulipAccountsController {
|
||||
constructor(
|
||||
@Inject('ZulipAccountsService') private readonly zulipAccountsService: any,
|
||||
@Inject(AppLoggerService) private readonly logger: AppLoggerService,
|
||||
) {
|
||||
this.logger.info('ZulipAccountsController初始化完成', {
|
||||
module: 'ZulipAccountsController',
|
||||
operation: 'constructor'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建性能监控器
|
||||
*
|
||||
* @param req HTTP请求对象
|
||||
* @param operation 操作名称
|
||||
* @param context 上下文信息
|
||||
* @returns 性能监控器
|
||||
* @private
|
||||
*/
|
||||
private createPerformanceMonitor(req: Request, operation: string, context?: Record<string, any>) {
|
||||
const startTime = Date.now();
|
||||
const requestLogger = this.logger.bindRequest(req, 'ZulipAccountsController');
|
||||
|
||||
requestLogger.info(`开始${operation}`, context);
|
||||
|
||||
return {
|
||||
success: (additionalContext?: Record<string, any>) => {
|
||||
const duration = Date.now() - startTime;
|
||||
requestLogger.info(`${operation}成功`, {
|
||||
...context,
|
||||
...additionalContext,
|
||||
duration
|
||||
});
|
||||
},
|
||||
error: (error: unknown, additionalContext?: Record<string, any>) => {
|
||||
const duration = Date.now() - startTime;
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
requestLogger.error(
|
||||
`${operation}失败`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
{
|
||||
...context,
|
||||
...additionalContext,
|
||||
error: errorMessage,
|
||||
duration
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Zulip账号关联
|
||||
*/
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
summary: '创建Zulip账号关联',
|
||||
description: '为游戏用户创建与Zulip账号的关联关系'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: '创建成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: '请求参数错误',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 409,
|
||||
description: '关联已存在',
|
||||
})
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async create(
|
||||
@Req() req: Request,
|
||||
@Body() createDto: CreateZulipAccountDto
|
||||
): Promise<ZulipAccountResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor(req, '创建Zulip账号关联', {
|
||||
gameUserId: createDto.gameUserId,
|
||||
zulipUserId: createDto.zulipUserId,
|
||||
zulipEmail: createDto.zulipEmail
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await this.zulipAccountsService.create(createDto);
|
||||
monitor.success({
|
||||
accountId: result.id,
|
||||
status: result.status
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有Zulip账号关联
|
||||
*/
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: '查询Zulip账号关联列表',
|
||||
description: '根据条件查询Zulip账号关联列表'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'gameUserId',
|
||||
required: false,
|
||||
description: '游戏用户ID',
|
||||
example: '12345'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'zulipUserId',
|
||||
required: false,
|
||||
description: 'Zulip用户ID',
|
||||
example: 67890
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'zulipEmail',
|
||||
required: false,
|
||||
description: 'Zulip邮箱地址',
|
||||
example: 'user@example.com'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'status',
|
||||
required: false,
|
||||
description: '账号状态',
|
||||
enum: ['active', 'inactive', 'suspended', 'error']
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'includeGameUser',
|
||||
required: false,
|
||||
description: '是否包含游戏用户信息',
|
||||
type: Boolean,
|
||||
example: false
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '查询成功',
|
||||
type: ZulipAccountListResponseDto,
|
||||
})
|
||||
async findMany(@Query() queryDto: QueryZulipAccountDto): Promise<ZulipAccountListResponseDto> {
|
||||
return this.zulipAccountsService.findMany(queryDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取Zulip账号关联
|
||||
*/
|
||||
@Get(':id')
|
||||
@ApiOperation({
|
||||
summary: '根据ID获取Zulip账号关联',
|
||||
description: '根据关联记录ID获取详细信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: '关联记录ID',
|
||||
example: '1'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'includeGameUser',
|
||||
required: false,
|
||||
description: '是否包含游戏用户信息',
|
||||
type: Boolean,
|
||||
example: false
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '记录不存在',
|
||||
})
|
||||
async findById(
|
||||
@Param('id') id: string,
|
||||
@Query('includeGameUser') includeGameUser?: boolean,
|
||||
): Promise<ZulipAccountResponseDto> {
|
||||
return this.zulipAccountsService.findById(id, includeGameUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID获取Zulip账号关联
|
||||
*/
|
||||
@Get('game-user/:gameUserId')
|
||||
@ApiOperation({
|
||||
summary: '根据游戏用户ID获取Zulip账号关联',
|
||||
description: '根据游戏用户ID获取关联的Zulip账号信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'gameUserId',
|
||||
description: '游戏用户ID',
|
||||
example: '12345'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'includeGameUser',
|
||||
required: false,
|
||||
description: '是否包含游戏用户信息',
|
||||
type: Boolean,
|
||||
example: false
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '关联不存在',
|
||||
})
|
||||
async findByGameUserId(
|
||||
@Param('gameUserId') gameUserId: string,
|
||||
@Query('includeGameUser') includeGameUser?: boolean,
|
||||
): Promise<ZulipAccountResponseDto | null> {
|
||||
return this.zulipAccountsService.findByGameUserId(gameUserId, includeGameUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip用户ID获取账号关联
|
||||
*/
|
||||
@Get('zulip-user/:zulipUserId')
|
||||
@ApiOperation({
|
||||
summary: '根据Zulip用户ID获取账号关联',
|
||||
description: '根据Zulip用户ID获取关联的游戏账号信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'zulipUserId',
|
||||
description: 'Zulip用户ID',
|
||||
example: '67890'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'includeGameUser',
|
||||
required: false,
|
||||
description: '是否包含游戏用户信息',
|
||||
type: Boolean,
|
||||
example: false
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '关联不存在',
|
||||
})
|
||||
async findByZulipUserId(
|
||||
@Param('zulipUserId') zulipUserId: string,
|
||||
@Query('includeGameUser') includeGameUser?: boolean,
|
||||
): Promise<ZulipAccountResponseDto | null> {
|
||||
return this.zulipAccountsService.findByZulipUserId(parseInt(zulipUserId), includeGameUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip邮箱获取账号关联
|
||||
*/
|
||||
@Get('zulip-email/:zulipEmail')
|
||||
@ApiOperation({
|
||||
summary: '根据Zulip邮箱获取账号关联',
|
||||
description: '根据Zulip邮箱地址获取关联的游戏账号信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'zulipEmail',
|
||||
description: 'Zulip邮箱地址',
|
||||
example: 'user@example.com'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'includeGameUser',
|
||||
required: false,
|
||||
description: '是否包含游戏用户信息',
|
||||
type: Boolean,
|
||||
example: false
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '关联不存在',
|
||||
})
|
||||
async findByZulipEmail(
|
||||
@Param('zulipEmail') zulipEmail: string,
|
||||
@Query('includeGameUser') includeGameUser?: boolean,
|
||||
): Promise<ZulipAccountResponseDto | null> {
|
||||
return this.zulipAccountsService.findByZulipEmail(zulipEmail, includeGameUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Zulip账号关联
|
||||
*/
|
||||
@Put(':id')
|
||||
@ApiOperation({
|
||||
summary: '更新Zulip账号关联',
|
||||
description: '根据ID更新Zulip账号关联信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: '关联记录ID',
|
||||
example: '1'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '更新成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '记录不存在',
|
||||
})
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateDto: UpdateZulipAccountDto,
|
||||
): Promise<ZulipAccountResponseDto> {
|
||||
return this.zulipAccountsService.update(id, updateDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID更新关联
|
||||
*/
|
||||
@Put('game-user/:gameUserId')
|
||||
@ApiOperation({
|
||||
summary: '根据游戏用户ID更新关联',
|
||||
description: '根据游戏用户ID更新Zulip账号关联信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'gameUserId',
|
||||
description: '游戏用户ID',
|
||||
example: '12345'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '更新成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '关联不存在',
|
||||
})
|
||||
async updateByGameUserId(
|
||||
@Param('gameUserId') gameUserId: string,
|
||||
@Body() updateDto: UpdateZulipAccountDto,
|
||||
): Promise<ZulipAccountResponseDto> {
|
||||
return this.zulipAccountsService.updateByGameUserId(gameUserId, updateDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Zulip账号关联
|
||||
*/
|
||||
@Delete(':id')
|
||||
@ApiOperation({
|
||||
summary: '删除Zulip账号关联',
|
||||
description: '根据ID删除Zulip账号关联记录'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: '关联记录ID',
|
||||
example: '1'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '删除成功',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', example: true },
|
||||
message: { type: 'string', example: '删除成功' }
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '记录不存在',
|
||||
})
|
||||
async delete(@Param('id') id: string): Promise<{ success: boolean; message: string }> {
|
||||
await this.zulipAccountsService.delete(id);
|
||||
return { success: true, message: '删除成功' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID删除关联
|
||||
*/
|
||||
@Delete('game-user/:gameUserId')
|
||||
@ApiOperation({
|
||||
summary: '根据游戏用户ID删除关联',
|
||||
description: '根据游戏用户ID删除Zulip账号关联记录'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'gameUserId',
|
||||
description: '游戏用户ID',
|
||||
example: '12345'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '删除成功',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', example: true },
|
||||
message: { type: 'string', example: '删除成功' }
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '关联不存在',
|
||||
})
|
||||
async deleteByGameUserId(@Param('gameUserId') gameUserId: string): Promise<{ success: boolean; message: string }> {
|
||||
await this.zulipAccountsService.deleteByGameUserId(gameUserId);
|
||||
return { success: true, message: '删除成功' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要验证的账号列表
|
||||
*/
|
||||
@Get('management/verification-needed')
|
||||
@ApiOperation({
|
||||
summary: '获取需要验证的账号列表',
|
||||
description: '获取超过指定时间未验证的账号列表'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'maxAge',
|
||||
required: false,
|
||||
description: '最大验证间隔(毫秒),默认24小时',
|
||||
example: 86400000
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountListResponseDto,
|
||||
})
|
||||
async findAccountsNeedingVerification(
|
||||
@Query('maxAge') maxAge?: number,
|
||||
): Promise<ZulipAccountListResponseDto> {
|
||||
return this.zulipAccountsService.findAccountsNeedingVerification(maxAge);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误状态的账号列表
|
||||
*/
|
||||
@Get('management/error-accounts')
|
||||
@ApiOperation({
|
||||
summary: '获取错误状态的账号列表',
|
||||
description: '获取处于错误状态的账号列表'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'maxRetryCount',
|
||||
required: false,
|
||||
description: '最大重试次数,默认3次',
|
||||
example: 3
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountListResponseDto,
|
||||
})
|
||||
async findErrorAccounts(
|
||||
@Query('maxRetryCount') maxRetryCount?: number,
|
||||
): Promise<ZulipAccountListResponseDto> {
|
||||
return this.zulipAccountsService.findErrorAccounts(maxRetryCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账号状态
|
||||
*/
|
||||
@Put('management/batch-status')
|
||||
@ApiOperation({
|
||||
summary: '批量更新账号状态',
|
||||
description: '批量更新多个账号的状态'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '更新成功',
|
||||
type: BatchUpdateResponseDto,
|
||||
})
|
||||
async batchUpdateStatus(@Body() batchDto: BatchUpdateStatusDto): Promise<BatchUpdateResponseDto> {
|
||||
return this.zulipAccountsService.batchUpdateStatus(batchDto.ids, batchDto.status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号状态统计
|
||||
*/
|
||||
@Get('management/statistics')
|
||||
@ApiOperation({
|
||||
summary: '获取账号状态统计',
|
||||
description: '获取各种状态的账号数量统计'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountStatsResponseDto,
|
||||
})
|
||||
async getStatusStatistics(@Req() req: Request): Promise<ZulipAccountStatsResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor(req, '获取账号状态统计');
|
||||
|
||||
try {
|
||||
const result = await this.zulipAccountsService.getStatusStatistics();
|
||||
monitor.success({
|
||||
total: result.total,
|
||||
active: result.active,
|
||||
error: result.error
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证账号有效性
|
||||
*/
|
||||
@Post('management/verify')
|
||||
@ApiOperation({
|
||||
summary: '验证账号有效性',
|
||||
description: '验证指定游戏用户的Zulip账号关联是否有效'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '验证完成',
|
||||
type: VerifyAccountResponseDto,
|
||||
})
|
||||
async verifyAccount(@Body() verifyDto: VerifyAccountDto): Promise<VerifyAccountResponseDto> {
|
||||
return this.zulipAccountsService.verifyAccount(verifyDto.gameUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*/
|
||||
@Get('validation/email-exists/:email')
|
||||
@ApiOperation({
|
||||
summary: '检查邮箱是否已存在',
|
||||
description: '检查指定的Zulip邮箱是否已被其他账号使用'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'email',
|
||||
description: 'Zulip邮箱地址',
|
||||
example: 'user@example.com'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'excludeId',
|
||||
required: false,
|
||||
description: '排除的记录ID(用于更新时检查)',
|
||||
example: '1'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '检查完成',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
exists: { type: 'boolean', example: false },
|
||||
email: { type: 'string', example: 'user@example.com' }
|
||||
}
|
||||
}
|
||||
})
|
||||
async checkEmailExists(
|
||||
@Param('email') email: string,
|
||||
@Query('excludeId') excludeId?: string,
|
||||
): Promise<{ exists: boolean; email: string }> {
|
||||
const exists = await this.zulipAccountsService.existsByEmail(email, excludeId);
|
||||
return { exists, email };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Zulip用户ID是否已存在
|
||||
*/
|
||||
@Get('validation/zulip-user-exists/:zulipUserId')
|
||||
@ApiOperation({
|
||||
summary: '检查Zulip用户ID是否已存在',
|
||||
description: '检查指定的Zulip用户ID是否已被其他账号使用'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'zulipUserId',
|
||||
description: 'Zulip用户ID',
|
||||
example: '67890'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'excludeId',
|
||||
required: false,
|
||||
description: '排除的记录ID(用于更新时检查)',
|
||||
example: '1'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '检查完成',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
exists: { type: 'boolean', example: false },
|
||||
zulipUserId: { type: 'number', example: 67890 }
|
||||
}
|
||||
}
|
||||
})
|
||||
async checkZulipUserIdExists(
|
||||
@Param('zulipUserId') zulipUserId: string,
|
||||
@Query('excludeId') excludeId?: string,
|
||||
): Promise<{ exists: boolean; zulipUserId: number }> {
|
||||
const zulipUserIdNum = parseInt(zulipUserId);
|
||||
const exists = await this.zulipAccountsService.existsByZulipUserId(zulipUserIdNum, excludeId);
|
||||
return { exists, zulipUserId: zulipUserIdNum };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user