forked from datawhale/whale-town-end
feat: 重构业务模块架构
- 新增auth模块处理认证逻辑 - 新增security模块处理安全相关功能 - 新增user-mgmt模块管理用户相关操作 - 新增shared模块存放共享组件 - 重构admin模块,添加DTO和Guards - 为admin模块添加测试文件结构
This commit is contained in:
@@ -15,10 +15,17 @@
|
||||
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards, ValidationPipe, UsePipes, Res, Logger } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiProduces, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AdminGuard } from '../../core/guards/admin.guard';
|
||||
import { AdminGuard } from './guards/admin.guard';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminLoginDto, AdminResetPasswordDto } from '../../dto/admin.dto';
|
||||
import { AdminLoginResponseDto, AdminUsersResponseDto, AdminCommonResponseDto, AdminUserResponseDto, AdminRuntimeLogsResponseDto } from '../../dto/admin_response.dto';
|
||||
import { AdminLoginDto, AdminResetPasswordDto } from './dto/admin-login.dto';
|
||||
import {
|
||||
AdminLoginResponseDto,
|
||||
AdminUsersResponseDto,
|
||||
AdminCommonResponseDto,
|
||||
AdminUserResponseDto,
|
||||
AdminRuntimeLogsResponseDto
|
||||
} from './dto/admin-response.dto';
|
||||
import { Throttle, ThrottlePresets } from '../security/decorators/throttle.decorator';
|
||||
import type { Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
@@ -35,6 +42,10 @@ export class AdminController {
|
||||
@ApiOperation({ summary: '管理员登录', description: '仅允许 role=9 的账户登录后台' })
|
||||
@ApiBody({ type: AdminLoginDto })
|
||||
@ApiResponse({ status: 200, description: '登录成功', type: AdminLoginResponseDto })
|
||||
@ApiResponse({ status: 401, description: '登录失败' })
|
||||
@ApiResponse({ status: 403, description: '权限不足或账户被禁用' })
|
||||
@ApiResponse({ status: 429, description: '登录尝试过于频繁' })
|
||||
@Throttle(ThrottlePresets.LOGIN)
|
||||
@Post('auth/login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@@ -73,7 +84,9 @@ export class AdminController {
|
||||
@ApiParam({ name: 'id', description: '用户ID' })
|
||||
@ApiBody({ type: AdminResetPasswordDto })
|
||||
@ApiResponse({ status: 200, description: '重置成功', type: AdminCommonResponseDto })
|
||||
@ApiResponse({ status: 429, description: '操作过于频繁' })
|
||||
@UseGuards(AdminGuard)
|
||||
@Throttle(ThrottlePresets.ADMIN_OPERATION)
|
||||
@Post('users/:id/reset-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
|
||||
@@ -21,5 +21,6 @@ import { AdminService } from './admin.service';
|
||||
imports: [AdminCoreModule, LoggerModule],
|
||||
controllers: [AdminController],
|
||||
providers: [AdminService],
|
||||
exports: [AdminService], // 导出AdminService供其他模块使用
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
@@ -11,12 +11,21 @@
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { Inject, Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { AdminCoreService } from '../../core/admin_core/admin_core.service';
|
||||
import { Users } from '../../core/db/users/users.entity';
|
||||
import { UsersService } from '../../core/db/users/users.service';
|
||||
import { UsersMemoryService } from '../../core/db/users/users_memory.service';
|
||||
import { LogManagementService } from '../../core/utils/logger/log_management.service';
|
||||
import { UserStatus, getUserStatusDescription } from '../user-mgmt/enums/user-status.enum';
|
||||
import { UserStatusDto, BatchUserStatusDto } from '../user-mgmt/dto/user-status.dto';
|
||||
import {
|
||||
UserStatusResponseDto,
|
||||
BatchUserStatusResponseDto,
|
||||
UserStatusStatsResponseDto,
|
||||
UserStatusInfoDto,
|
||||
BatchOperationResultDto
|
||||
} from '../user-mgmt/dto/user-status-response.dto';
|
||||
|
||||
export interface AdminApiResponse<T = any> {
|
||||
success: boolean;
|
||||
@@ -108,8 +117,318 @@ export class AdminService {
|
||||
phone: user.phone,
|
||||
avatar_url: user.avatar_url,
|
||||
role: user.role,
|
||||
status: user.status || UserStatus.ACTIVE, // 兼容旧数据
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化用户状态信息
|
||||
*
|
||||
* @param user 用户实体
|
||||
* @returns 格式化的用户状态信息
|
||||
*/
|
||||
private formatUserStatus(user: Users): UserStatusInfoDto {
|
||||
return {
|
||||
id: user.id.toString(),
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
status: user.status || UserStatus.ACTIVE,
|
||||
status_description: getUserStatusDescription(user.status || UserStatus.ACTIVE),
|
||||
updated_at: user.updated_at
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户状态
|
||||
*
|
||||
* 功能描述:
|
||||
* 管理员修改指定用户的账户状态,支持激活、锁定、禁用等操作
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户是否存在
|
||||
* 2. 检查状态变更的合法性
|
||||
* 3. 更新用户状态
|
||||
* 4. 记录状态变更日志
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param userStatusDto 状态修改数据
|
||||
* @returns 修改结果
|
||||
*
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
* @throws BadRequestException 当状态变更不合法时
|
||||
*/
|
||||
async updateUserStatus(userId: bigint, userStatusDto: UserStatusDto): Promise<UserStatusResponseDto> {
|
||||
try {
|
||||
this.logger.log('开始修改用户状态', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
newStatus: userStatusDto.status,
|
||||
reason: userStatusDto.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 1. 验证用户是否存在
|
||||
const user = await this.usersService.findOne(userId);
|
||||
if (!user) {
|
||||
this.logger.warn('修改用户状态失败:用户不存在', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString()
|
||||
});
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
// 2. 检查状态变更的合法性
|
||||
if (user.status === userStatusDto.status) {
|
||||
this.logger.warn('修改用户状态失败:状态未发生变化', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
currentStatus: user.status,
|
||||
newStatus: userStatusDto.status
|
||||
});
|
||||
throw new BadRequestException('用户状态未发生变化');
|
||||
}
|
||||
|
||||
// 3. 更新用户状态
|
||||
const updatedUser = await this.usersService.update(userId, {
|
||||
status: userStatusDto.status
|
||||
});
|
||||
|
||||
// 4. 记录状态变更日志
|
||||
this.logger.log('用户状态修改成功', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
oldStatus: user.status,
|
||||
newStatus: userStatusDto.status,
|
||||
reason: userStatusDto.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
user: this.formatUserStatus(updatedUser),
|
||||
reason: userStatusDto.reason
|
||||
},
|
||||
message: '用户状态修改成功'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('修改用户状态失败', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException || error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '用户状态修改失败',
|
||||
error_code: 'USER_STATUS_UPDATE_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量修改用户状态
|
||||
*
|
||||
* 功能描述:
|
||||
* 管理员批量修改多个用户的账户状态
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户ID列表
|
||||
* 2. 逐个处理用户状态修改
|
||||
* 3. 收集成功和失败的结果
|
||||
* 4. 返回批量操作结果
|
||||
*
|
||||
* @param batchUserStatusDto 批量状态修改数据
|
||||
* @returns 批量修改结果
|
||||
*/
|
||||
async batchUpdateUserStatus(batchUserStatusDto: BatchUserStatusDto): Promise<BatchUserStatusResponseDto> {
|
||||
try {
|
||||
this.logger.log('开始批量修改用户状态', {
|
||||
operation: 'batch_update_user_status',
|
||||
userCount: batchUserStatusDto.user_ids.length,
|
||||
newStatus: batchUserStatusDto.status,
|
||||
reason: batchUserStatusDto.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const successUsers: UserStatusInfoDto[] = [];
|
||||
const failedUsers: Array<{ user_id: string; error: string }> = [];
|
||||
|
||||
// 1. 逐个处理用户状态修改
|
||||
for (const userIdStr of batchUserStatusDto.user_ids) {
|
||||
try {
|
||||
const userId = BigInt(userIdStr);
|
||||
|
||||
// 2. 验证用户是否存在
|
||||
const user = await this.usersService.findOne(userId);
|
||||
if (!user) {
|
||||
failedUsers.push({
|
||||
user_id: userIdStr,
|
||||
error: '用户不存在'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 检查状态是否需要变更
|
||||
if (user.status === batchUserStatusDto.status) {
|
||||
failedUsers.push({
|
||||
user_id: userIdStr,
|
||||
error: '用户状态未发生变化'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. 更新用户状态
|
||||
const updatedUser = await this.usersService.update(userId, {
|
||||
status: batchUserStatusDto.status
|
||||
});
|
||||
|
||||
successUsers.push(this.formatUserStatus(updatedUser));
|
||||
|
||||
} catch (error) {
|
||||
failedUsers.push({
|
||||
user_id: userIdStr,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 构建批量操作结果
|
||||
const result: BatchOperationResultDto = {
|
||||
success_users: successUsers,
|
||||
failed_users: failedUsers,
|
||||
success_count: successUsers.length,
|
||||
failed_count: failedUsers.length,
|
||||
total_count: batchUserStatusDto.user_ids.length
|
||||
};
|
||||
|
||||
this.logger.log('批量修改用户状态完成', {
|
||||
operation: 'batch_update_user_status',
|
||||
successCount: result.success_count,
|
||||
failedCount: result.failed_count,
|
||||
totalCount: result.total_count,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
result,
|
||||
reason: batchUserStatusDto.reason
|
||||
},
|
||||
message: `批量用户状态修改完成,成功:${result.success_count},失败:${result.failed_count}`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('批量修改用户状态失败', {
|
||||
operation: 'batch_update_user_status',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '批量用户状态修改失败',
|
||||
error_code: 'BATCH_USER_STATUS_UPDATE_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户状态统计
|
||||
*
|
||||
* 功能描述:
|
||||
* 获取各种用户状态的数量统计信息
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 查询所有用户
|
||||
* 2. 按状态分组统计
|
||||
* 3. 计算各状态数量
|
||||
* 4. 返回统计结果
|
||||
*
|
||||
* @returns 状态统计信息
|
||||
*/
|
||||
async getUserStatusStats(): Promise<UserStatusStatsResponseDto> {
|
||||
try {
|
||||
this.logger.log('开始获取用户状态统计', {
|
||||
operation: 'get_user_status_stats',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 1. 查询所有用户(这里可以优化为直接查询统计信息)
|
||||
const allUsers = await this.usersService.findAll(10000, 0); // 假设最多1万用户
|
||||
|
||||
// 2. 按状态分组统计
|
||||
const stats = {
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
locked: 0,
|
||||
banned: 0,
|
||||
deleted: 0,
|
||||
pending: 0,
|
||||
total: allUsers.length
|
||||
};
|
||||
|
||||
// 3. 计算各状态数量
|
||||
allUsers.forEach((user: Users) => {
|
||||
const status = user.status || UserStatus.ACTIVE;
|
||||
switch (status) {
|
||||
case UserStatus.ACTIVE:
|
||||
stats.active++;
|
||||
break;
|
||||
case UserStatus.INACTIVE:
|
||||
stats.inactive++;
|
||||
break;
|
||||
case UserStatus.LOCKED:
|
||||
stats.locked++;
|
||||
break;
|
||||
case UserStatus.BANNED:
|
||||
stats.banned++;
|
||||
break;
|
||||
case UserStatus.DELETED:
|
||||
stats.deleted++;
|
||||
break;
|
||||
case UserStatus.PENDING:
|
||||
stats.pending++;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.log('用户状态统计获取成功', {
|
||||
operation: 'get_user_status_stats',
|
||||
stats,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
stats,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
message: '用户状态统计获取成功'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取用户状态统计失败', {
|
||||
operation: 'get_user_status_stats',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '用户状态统计获取失败',
|
||||
error_code: 'USER_STATUS_STATS_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
src/business/admin/dto/admin-login.dto.ts
Normal file
34
src/business/admin/dto/admin-login.dto.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 管理员相关 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义管理员登录与用户密码重置的请求结构
|
||||
* - 使用 class-validator 进行参数校验
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class AdminLoginDto {
|
||||
@ApiProperty({ description: '登录标识符(用户名/邮箱/手机号)', example: 'admin' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
identifier: string;
|
||||
|
||||
@ApiProperty({ description: '密码', example: 'Admin123456' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class AdminResetPasswordDto {
|
||||
@ApiProperty({ description: '新密码(至少8位,包含字母和数字)', example: 'NewPass1234' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(8)
|
||||
new_password: string;
|
||||
}
|
||||
104
src/business/admin/dto/admin-response.dto.ts
Normal file
104
src/business/admin/dto/admin-response.dto.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 管理员响应 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义管理员相关接口的响应格式
|
||||
* - 提供 Swagger 文档生成支持
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AdminLoginResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '登录成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: 'JWT Token', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' })
|
||||
token?: string;
|
||||
|
||||
@ApiProperty({ description: '管理员信息', required: false })
|
||||
admin?: {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
role: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class AdminUsersResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '获取用户列表成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: '用户列表', type: 'array' })
|
||||
users?: Array<{
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>;
|
||||
|
||||
@ApiProperty({ description: '总数', example: 100 })
|
||||
total?: number;
|
||||
|
||||
@ApiProperty({ description: '偏移量', example: 0 })
|
||||
offset?: number;
|
||||
|
||||
@ApiProperty({ description: '限制数量', example: 100 })
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export class AdminUserResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '获取用户详情成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: '用户信息', required: false })
|
||||
user?: {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_login_at?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class AdminCommonResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '操作成功' })
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class AdminRuntimeLogsResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '获取日志成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: '日志内容', type: 'array', items: { type: 'string' } })
|
||||
logs?: string[];
|
||||
|
||||
@ApiProperty({ description: '返回行数', example: 200 })
|
||||
lines?: number;
|
||||
}
|
||||
43
src/business/admin/guards/admin.guard.ts
Normal file
43
src/business/admin/guards/admin.guard.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 管理员鉴权守卫
|
||||
*
|
||||
* 功能描述:
|
||||
* - 保护后台管理接口
|
||||
* - 校验 Authorization: Bearer <admin_token>
|
||||
* - 仅允许 role=9 的管理员访问
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { AdminCoreService, AdminAuthPayload } from '../../../core/admin_core/admin_core.service';
|
||||
|
||||
export interface AdminRequest extends Request {
|
||||
admin?: AdminAuthPayload;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminGuard implements CanActivate {
|
||||
constructor(private readonly adminCoreService: AdminCoreService) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const req = context.switchToHttp().getRequest<AdminRequest>();
|
||||
const auth = req.headers['authorization'];
|
||||
|
||||
if (!auth || Array.isArray(auth)) {
|
||||
throw new UnauthorizedException('缺少Authorization头');
|
||||
}
|
||||
|
||||
const [scheme, token] = auth.split(' ');
|
||||
if (scheme !== 'Bearer' || !token) {
|
||||
throw new UnauthorizedException('Authorization格式错误');
|
||||
}
|
||||
|
||||
const payload = this.adminCoreService.verifyToken(token);
|
||||
req.admin = payload;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
24
src/business/admin/index.ts
Normal file
24
src/business/admin/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 管理员模块统一导出
|
||||
*
|
||||
* 功能描述:
|
||||
* - 导出管理员相关的所有组件
|
||||
* - 提供统一的导入入口
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
// 控制器
|
||||
export * from './admin.controller';
|
||||
|
||||
// 服务
|
||||
export * from './admin.service';
|
||||
|
||||
// DTO
|
||||
export * from './dto/admin-login.dto';
|
||||
export * from './dto/admin-response.dto';
|
||||
|
||||
// 模块
|
||||
export * from './admin.module';
|
||||
81
src/business/admin/tests
Normal file
81
src/business/admin/tests
Normal file
@@ -0,0 +1,81 @@
|
||||
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { AdminCoreService, AdminAuthPayload } from '../admin_core/admin_core.service';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
|
||||
describe('AdminGuard', () => {
|
||||
const payload: AdminAuthPayload = {
|
||||
adminId: '1',
|
||||
username: 'admin',
|
||||
role: 9,
|
||||
iat: 1,
|
||||
exp: 2,
|
||||
};
|
||||
|
||||
const adminCoreServiceMock: Pick<AdminCoreService, 'verifyToken'> = {
|
||||
verifyToken: jest.fn(),
|
||||
};
|
||||
|
||||
const makeContext = (authorization?: any) => {
|
||||
const req: any = { headers: {} };
|
||||
if (authorization !== undefined) {
|
||||
req.headers['authorization'] = authorization;
|
||||
}
|
||||
|
||||
const ctx: Partial<ExecutionContext> = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => req,
|
||||
getResponse: () => ({} as any),
|
||||
getNext: () => ({} as any),
|
||||
}),
|
||||
};
|
||||
|
||||
return { ctx: ctx as ExecutionContext, req };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should allow access with valid admin token', () => {
|
||||
(adminCoreServiceMock.verifyToken as jest.Mock).mockReturnValue(payload);
|
||||
|
||||
const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService);
|
||||
const { ctx, req } = makeContext('Bearer valid');
|
||||
|
||||
expect(guard.canActivate(ctx)).toBe(true);
|
||||
expect(adminCoreServiceMock.verifyToken).toHaveBeenCalledWith('valid');
|
||||
expect(req.admin).toEqual(payload);
|
||||
});
|
||||
|
||||
it('should deny access without token', () => {
|
||||
const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService);
|
||||
const { ctx } = makeContext(undefined);
|
||||
|
||||
expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should deny access with invalid Authorization format', () => {
|
||||
const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService);
|
||||
const { ctx } = makeContext('InvalidFormat');
|
||||
|
||||
expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should deny access when verifyToken throws (invalid/expired)', () => {
|
||||
(adminCoreServiceMock.verifyToken as jest.Mock).mockImplementation(() => {
|
||||
throw new UnauthorizedException('Token已过期');
|
||||
});
|
||||
|
||||
const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService);
|
||||
const { ctx } = makeContext('Bearer bad');
|
||||
|
||||
expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should deny access when Authorization header is an array', () => {
|
||||
const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService);
|
||||
const { ctx } = makeContext(['Bearer token']);
|
||||
|
||||
expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException);
|
||||
});
|
||||
});
|
||||
26
src/business/auth/auth.module.ts
Normal file
26
src/business/auth/auth.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 用户认证业务模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合所有用户认证相关功能
|
||||
* - 用户登录、注册、密码管理
|
||||
* - GitHub OAuth集成
|
||||
* - 邮箱验证功能
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LoginController } from './controllers/login.controller';
|
||||
import { LoginService } from './services/login.service';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
|
||||
@Module({
|
||||
imports: [LoginCoreModule],
|
||||
controllers: [LoginController],
|
||||
providers: [LoginService],
|
||||
exports: [LoginService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -22,8 +22,8 @@
|
||||
import { Controller, Post, Put, Body, HttpCode, HttpStatus, ValidationPipe, UsePipes, Logger, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse as SwaggerApiResponse, ApiBody } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { LoginService, ApiResponse, LoginResponse } from './login.service';
|
||||
import { LoginDto, RegisterDto, GitHubOAuthDto, ForgotPasswordDto, ResetPasswordDto, ChangePasswordDto, EmailVerificationDto, SendEmailVerificationDto } from '../../dto/login.dto';
|
||||
import { LoginService, ApiResponse, LoginResponse } from '../services/login.service';
|
||||
import { LoginDto, RegisterDto, GitHubOAuthDto, ForgotPasswordDto, ResetPasswordDto, ChangePasswordDto, EmailVerificationDto, SendEmailVerificationDto } from '../dto/login.dto';
|
||||
import {
|
||||
LoginResponseDto,
|
||||
RegisterResponseDto,
|
||||
@@ -32,7 +32,9 @@ import {
|
||||
CommonResponseDto,
|
||||
TestModeEmailVerificationResponseDto,
|
||||
SuccessEmailVerificationResponseDto
|
||||
} from '../../dto/login_response.dto';
|
||||
} from '../dto/login_response.dto';
|
||||
import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator';
|
||||
import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
@@ -65,6 +67,16 @@ export class LoginController {
|
||||
status: 401,
|
||||
description: '用户名或密码错误'
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 403,
|
||||
description: '账户被禁用或锁定'
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 429,
|
||||
description: '登录尝试过于频繁'
|
||||
})
|
||||
@Throttle(ThrottlePresets.LOGIN)
|
||||
@Timeout(TimeoutPresets.NORMAL)
|
||||
@Post('login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@@ -99,6 +111,12 @@ export class LoginController {
|
||||
status: 409,
|
||||
description: '用户名或邮箱已存在'
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 429,
|
||||
description: '注册请求过于频繁'
|
||||
})
|
||||
@Throttle(ThrottlePresets.REGISTER)
|
||||
@Timeout(TimeoutPresets.NORMAL)
|
||||
@Post('register')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@@ -180,6 +198,11 @@ export class LoginController {
|
||||
status: 404,
|
||||
description: '用户不存在'
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 429,
|
||||
description: '发送频率过高'
|
||||
})
|
||||
@Throttle(ThrottlePresets.SEND_CODE)
|
||||
@Post('forgot-password')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async forgotPassword(
|
||||
@@ -222,6 +245,11 @@ export class LoginController {
|
||||
status: 404,
|
||||
description: '用户不存在'
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 429,
|
||||
description: '重置请求过于频繁'
|
||||
})
|
||||
@Throttle(ThrottlePresets.RESET_PASSWORD)
|
||||
@Post('reset-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@@ -302,6 +330,8 @@ export class LoginController {
|
||||
status: 429,
|
||||
description: '发送频率过高'
|
||||
})
|
||||
@Throttle(ThrottlePresets.SEND_CODE)
|
||||
@Timeout(TimeoutPresets.EMAIL_SEND)
|
||||
@Post('send-email-verification')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async sendEmailVerification(
|
||||
@@ -380,6 +410,7 @@ export class LoginController {
|
||||
status: 429,
|
||||
description: '发送频率过高'
|
||||
})
|
||||
@Throttle(ThrottlePresets.SEND_CODE)
|
||||
@Post('resend-email-verification')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async resendEmailVerification(
|
||||
374
src/business/auth/dto/login.dto.ts
Normal file
374
src/business/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
/**
|
||||
* 登录业务数据传输对象
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义登录相关API的请求数据结构
|
||||
* - 提供数据验证规则和错误提示
|
||||
* - 确保API接口的数据格式一致性
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
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: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* 手机号(可选)
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '手机号码(可选)',
|
||||
example: '+8613800138000',
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsPhoneNumber(null, { message: '手机号格式不正确' })
|
||||
phone?: string;
|
||||
|
||||
/**
|
||||
* 邮箱验证码(当提供邮箱时必填)
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱验证码,当提供邮箱时必填',
|
||||
example: '123456',
|
||||
pattern: '^\\d{6}$',
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@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;
|
||||
}
|
||||
395
src/business/auth/dto/login_response.dto.ts
Normal file
395
src/business/auth/dto/login_response.dto.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
/**
|
||||
* 登录业务响应数据传输对象
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义登录相关API的响应数据结构
|
||||
* - 提供Swagger文档生成支持
|
||||
* - 确保API响应的数据格式一致性
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
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: '访问令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
})
|
||||
access_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '刷新令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
required: false
|
||||
})
|
||||
refresh_token?: 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 by angjustinl 2025-12-17
|
||||
*/
|
||||
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;
|
||||
}
|
||||
23
src/business/auth/index.ts
Normal file
23
src/business/auth/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 用户认证业务模块导出
|
||||
*
|
||||
* 功能概述:
|
||||
* - 用户登录和注册
|
||||
* - GitHub OAuth集成
|
||||
* - 密码管理(忘记密码、重置密码、修改密码)
|
||||
* - 邮箱验证功能
|
||||
* - JWT Token管理
|
||||
*/
|
||||
|
||||
// 模块
|
||||
export * from './auth.module';
|
||||
|
||||
// 控制器
|
||||
export * from './controllers/login.controller';
|
||||
|
||||
// 服务
|
||||
export * from './services/login.service';
|
||||
|
||||
// DTO
|
||||
export * from './dto/login.dto';
|
||||
export * from './dto/login_response.dto';
|
||||
@@ -17,8 +17,8 @@
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, AuthResult } from '../../core/login_core/login_core.service';
|
||||
import { Users } from '../../core/db/users/users.entity';
|
||||
import { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, AuthResult } from '../../../core/login_core/login_core.service';
|
||||
import { Users } from '../../../core/db/users/users.entity';
|
||||
|
||||
/**
|
||||
* 登录响应数据接口
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* 登录业务模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合登录相关的控制器、服务和依赖
|
||||
* - 提供完整的登录业务功能模块
|
||||
* - 可被其他模块导入使用
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LoginController } from './login.controller';
|
||||
import { LoginService } from './login.service';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
|
||||
@Module({
|
||||
imports: [LoginCoreModule],
|
||||
controllers: [LoginController],
|
||||
providers: [LoginService],
|
||||
exports: [LoginService],
|
||||
})
|
||||
export class LoginModule {}
|
||||
@@ -1,193 +0,0 @@
|
||||
/**
|
||||
* 登录业务服务测试
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LoginService } from './login.service';
|
||||
import { LoginCoreService } from '../../core/login_core/login_core.service';
|
||||
|
||||
describe('LoginService', () => {
|
||||
let service: LoginService;
|
||||
let loginCoreService: jest.Mocked<LoginCoreService>;
|
||||
|
||||
const mockUser = {
|
||||
id: BigInt(1),
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
phone: '+8613800138000',
|
||||
password_hash: '$2b$12$hashedpassword',
|
||||
nickname: '测试用户',
|
||||
github_id: null as string | null,
|
||||
avatar_url: null as string | null,
|
||||
role: 1,
|
||||
email_verified: false,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockLoginCoreService = {
|
||||
login: jest.fn(),
|
||||
register: jest.fn(),
|
||||
githubOAuth: jest.fn(),
|
||||
sendPasswordResetCode: jest.fn(),
|
||||
resetPassword: jest.fn(),
|
||||
changePassword: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
LoginService,
|
||||
{
|
||||
provide: LoginCoreService,
|
||||
useValue: mockLoginCoreService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LoginService>(LoginService);
|
||||
loginCoreService = module.get(LoginCoreService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should return success response for valid login', async () => {
|
||||
loginCoreService.login.mockResolvedValue({
|
||||
user: mockUser,
|
||||
isNewUser: false
|
||||
});
|
||||
|
||||
const result = await service.login({
|
||||
identifier: 'testuser',
|
||||
password: 'password123'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.user.username).toBe('testuser');
|
||||
expect(result.data?.access_token).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return error response for failed login', async () => {
|
||||
loginCoreService.login.mockRejectedValue(new Error('登录失败'));
|
||||
|
||||
const result = await service.login({
|
||||
identifier: 'testuser',
|
||||
password: 'wrongpassword'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('登录失败');
|
||||
expect(result.error_code).toBe('LOGIN_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should return success response for valid registration', async () => {
|
||||
loginCoreService.register.mockResolvedValue({
|
||||
user: mockUser,
|
||||
isNewUser: true
|
||||
});
|
||||
|
||||
const result = await service.register({
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.user.username).toBe('testuser');
|
||||
expect(result.data?.is_new_user).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error response for failed registration', async () => {
|
||||
loginCoreService.register.mockRejectedValue(new Error('用户名已存在'));
|
||||
|
||||
const result = await service.register({
|
||||
username: 'existinguser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('用户名已存在');
|
||||
expect(result.error_code).toBe('REGISTER_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('githubOAuth', () => {
|
||||
it('should return success response for GitHub OAuth', async () => {
|
||||
loginCoreService.githubOAuth.mockResolvedValue({
|
||||
user: mockUser,
|
||||
isNewUser: true
|
||||
});
|
||||
|
||||
const result = await service.githubOAuth({
|
||||
github_id: 'github123',
|
||||
username: 'githubuser',
|
||||
nickname: 'GitHub用户'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.user.username).toBe('testuser');
|
||||
expect(result.data?.is_new_user).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPasswordResetCode', () => {
|
||||
it('should return test mode response with verification code', async () => {
|
||||
loginCoreService.sendPasswordResetCode.mockResolvedValue({
|
||||
code: '123456',
|
||||
isTestMode: true
|
||||
});
|
||||
|
||||
const result = await service.sendPasswordResetCode('test@example.com');
|
||||
|
||||
expect(result.success).toBe(false); // 测试模式下不算成功
|
||||
expect(result.error_code).toBe('TEST_MODE_ONLY');
|
||||
expect(result.data?.verification_code).toBe('123456');
|
||||
expect(result.data?.is_test_mode).toBe(true);
|
||||
});
|
||||
|
||||
it('should return success response for real email sending', async () => {
|
||||
loginCoreService.sendPasswordResetCode.mockResolvedValue({
|
||||
code: '123456',
|
||||
isTestMode: false
|
||||
});
|
||||
|
||||
const result = await service.sendPasswordResetCode('test@example.com');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.is_test_mode).toBe(false);
|
||||
expect(result.data?.verification_code).toBeUndefined(); // 真实模式下不返回验证码
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('should return success response for password reset', async () => {
|
||||
loginCoreService.resetPassword.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.resetPassword({
|
||||
identifier: 'test@example.com',
|
||||
verificationCode: '123456',
|
||||
newPassword: 'newpassword123'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('密码重置成功');
|
||||
});
|
||||
});
|
||||
|
||||
describe('changePassword', () => {
|
||||
it('should return success response for password change', async () => {
|
||||
loginCoreService.changePassword.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.changePassword(BigInt(1), 'oldpassword', 'newpassword123');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('密码修改成功');
|
||||
});
|
||||
});
|
||||
});
|
||||
89
src/business/security/decorators/throttle.decorator.ts
Normal file
89
src/business/security/decorators/throttle.decorator.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 频率限制装饰器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供API接口的频率限制功能
|
||||
* - 防止恶意请求和系统滥用
|
||||
* - 支持基于IP和用户的限制策略
|
||||
*
|
||||
* 使用场景:
|
||||
* - 登录接口防暴力破解
|
||||
* - 注册接口防批量注册
|
||||
* - 验证码接口防频繁发送
|
||||
* - 敏感操作接口保护
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { SetMetadata, applyDecorators, UseGuards } from '@nestjs/common';
|
||||
import { ThrottleGuard } from '../guards/throttle.guard';
|
||||
|
||||
/**
|
||||
* 频率限制元数据键
|
||||
*/
|
||||
export const THROTTLE_KEY = 'throttle';
|
||||
|
||||
/**
|
||||
* 频率限制配置接口
|
||||
*/
|
||||
export interface ThrottleConfig {
|
||||
/** 时间窗口内允许的最大请求次数 */
|
||||
limit: number;
|
||||
/** 时间窗口长度(秒) */
|
||||
ttl: number;
|
||||
/** 限制类型:ip(基于IP)或 user(基于用户) */
|
||||
type?: 'ip' | 'user';
|
||||
/** 自定义错误消息 */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 频率限制装饰器
|
||||
*
|
||||
* @param config 频率限制配置
|
||||
* @returns 装饰器函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 每分钟最多5次登录尝试
|
||||
* @Throttle({ limit: 5, ttl: 60, message: '登录尝试过于频繁,请稍后再试' })
|
||||
* @Post('login')
|
||||
* async login() { ... }
|
||||
*
|
||||
* // 每5分钟最多3次注册
|
||||
* @Throttle({ limit: 3, ttl: 300, type: 'ip' })
|
||||
* @Post('register')
|
||||
* async register() { ... }
|
||||
* ```
|
||||
*/
|
||||
export function Throttle(config: ThrottleConfig) {
|
||||
return applyDecorators(
|
||||
SetMetadata(THROTTLE_KEY, config),
|
||||
UseGuards(ThrottleGuard)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义的频率限制配置
|
||||
*/
|
||||
export const ThrottlePresets = {
|
||||
/** 登录接口:每分钟5次 */
|
||||
LOGIN: { limit: 5, ttl: 60, message: '登录尝试过于频繁,请1分钟后再试' },
|
||||
|
||||
/** 注册接口:每5分钟3次 */
|
||||
REGISTER: { limit: 3, ttl: 300, message: '注册请求过于频繁,请5分钟后再试' },
|
||||
|
||||
/** 发送验证码:每分钟1次 */
|
||||
SEND_CODE: { limit: 1, ttl: 60, message: '验证码发送过于频繁,请1分钟后再试' },
|
||||
|
||||
/** 密码重置:每小时3次 */
|
||||
RESET_PASSWORD: { limit: 3, ttl: 3600, message: '密码重置请求过于频繁,请1小时后再试' },
|
||||
|
||||
/** 管理员操作:每分钟10次 */
|
||||
ADMIN_OPERATION: { limit: 10, ttl: 60, message: '管理员操作过于频繁,请稍后再试' },
|
||||
|
||||
/** 一般API:每分钟30次 */
|
||||
GENERAL_API: { limit: 30, ttl: 60, message: 'API调用过于频繁,请稍后再试' }
|
||||
} as const;
|
||||
119
src/business/security/decorators/timeout.decorator.ts
Normal file
119
src/business/security/decorators/timeout.decorator.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 超时处理装饰器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 为API接口添加超时控制
|
||||
* - 防止长时间运行的请求阻塞系统
|
||||
* - 提供友好的超时错误提示
|
||||
*
|
||||
* 使用场景:
|
||||
* - 数据库查询超时控制
|
||||
* - 外部API调用超时
|
||||
* - 文件上传下载超时
|
||||
* - 复杂计算任务超时
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||
import { ApiResponse } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 超时配置元数据键
|
||||
*/
|
||||
export const TIMEOUT_KEY = 'timeout';
|
||||
|
||||
/**
|
||||
* 超时配置接口
|
||||
*/
|
||||
export interface TimeoutConfig {
|
||||
/** 超时时间(毫秒) */
|
||||
timeout: number;
|
||||
/** 自定义超时错误消息 */
|
||||
message?: string;
|
||||
/** 是否记录超时日志 */
|
||||
logTimeout?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 超时装饰器
|
||||
*
|
||||
* @param config 超时配置或超时时间(毫秒)
|
||||
* @returns 装饰器函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 设置30秒超时
|
||||
* @Timeout(30000)
|
||||
* @Get('slow-operation')
|
||||
* async slowOperation() { ... }
|
||||
*
|
||||
* // 自定义超时配置
|
||||
* @Timeout({
|
||||
* timeout: 60000,
|
||||
* message: '数据查询超时,请稍后重试',
|
||||
* logTimeout: true
|
||||
* })
|
||||
* @Post('complex-query')
|
||||
* async complexQuery() { ... }
|
||||
* ```
|
||||
*/
|
||||
export function Timeout(config: number | TimeoutConfig) {
|
||||
const timeoutConfig: TimeoutConfig = typeof config === 'number'
|
||||
? { timeout: config }
|
||||
: config;
|
||||
|
||||
return applyDecorators(
|
||||
SetMetadata(TIMEOUT_KEY, timeoutConfig),
|
||||
ApiResponse({
|
||||
status: 408,
|
||||
description: timeoutConfig.message || '请求超时',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', example: false },
|
||||
message: { type: 'string', example: timeoutConfig.message || '请求超时,请稍后重试' },
|
||||
error_code: { type: 'string', example: 'REQUEST_TIMEOUT' },
|
||||
timeout_info: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timeout_ms: { type: 'number', example: timeoutConfig.timeout },
|
||||
timestamp: { type: 'string', example: '2025-12-24T10:00:00.000Z' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义的超时配置
|
||||
*/
|
||||
export const TimeoutPresets = {
|
||||
/** 快速操作:5秒 */
|
||||
FAST: { timeout: 5000, message: '操作超时,请检查网络连接' },
|
||||
|
||||
/** 一般操作:30秒 */
|
||||
NORMAL: { timeout: 30000, message: '请求超时,请稍后重试' },
|
||||
|
||||
/** 慢操作:60秒 */
|
||||
SLOW: { timeout: 60000, message: '操作超时,请稍后重试' },
|
||||
|
||||
/** 文件操作:2分钟 */
|
||||
FILE_OPERATION: { timeout: 120000, message: '文件操作超时,请检查文件大小和网络状况' },
|
||||
|
||||
/** 数据库查询:45秒 */
|
||||
DATABASE_QUERY: { timeout: 45000, message: '数据查询超时,请简化查询条件或稍后重试' },
|
||||
|
||||
/** 外部API调用:15秒 */
|
||||
EXTERNAL_API: { timeout: 15000, message: '外部服务调用超时,请稍后重试' },
|
||||
|
||||
/** 邮件发送:30秒 */
|
||||
EMAIL_SEND: { timeout: 30000, message: '邮件发送超时,请检查邮件服务配置' },
|
||||
|
||||
/** 长时间任务:5分钟 */
|
||||
LONG_TASK: { timeout: 300000, message: '任务执行超时,请稍后重试' }
|
||||
} as const;
|
||||
317
src/business/security/guards/throttle.guard.ts
Normal file
317
src/business/security/guards/throttle.guard.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 频率限制守卫
|
||||
*
|
||||
* 功能描述:
|
||||
* - 实现API接口的频率限制功能
|
||||
* - 基于IP地址进行限制
|
||||
* - 支持自定义限制规则
|
||||
*
|
||||
* 使用场景:
|
||||
* - 防止API滥用
|
||||
* - 登录暴力破解防护
|
||||
* - 验证码发送频率控制
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Request } from 'express';
|
||||
import { THROTTLE_KEY, ThrottleConfig } from '../decorators/throttle.decorator';
|
||||
|
||||
/**
|
||||
* 频率限制记录接口
|
||||
*/
|
||||
interface ThrottleRecord {
|
||||
/** 请求次数 */
|
||||
count: number;
|
||||
/** 窗口开始时间 */
|
||||
windowStart: number;
|
||||
/** 最后请求时间 */
|
||||
lastRequest: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 频率限制响应接口
|
||||
*/
|
||||
interface ThrottleResponse {
|
||||
/** 请求是否成功 */
|
||||
success: boolean;
|
||||
/** 响应消息 */
|
||||
message: string;
|
||||
/** 错误代码 */
|
||||
error_code: string;
|
||||
/** 限制信息 */
|
||||
throttle_info: {
|
||||
/** 限制次数 */
|
||||
limit: number;
|
||||
/** 时间窗口(秒) */
|
||||
window_seconds: number;
|
||||
/** 当前请求次数 */
|
||||
current_requests: number;
|
||||
/** 重置时间 */
|
||||
reset_time: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ThrottleGuard implements CanActivate {
|
||||
private readonly logger = new Logger(ThrottleGuard.name);
|
||||
|
||||
/**
|
||||
* 存储频率限制记录
|
||||
* Key: IP地址 + 路径
|
||||
* Value: 限制记录
|
||||
*/
|
||||
private readonly records = new Map<string, ThrottleRecord>();
|
||||
|
||||
/**
|
||||
* 清理过期记录的间隔(毫秒)
|
||||
*/
|
||||
private readonly cleanupInterval = 60000; // 1分钟
|
||||
|
||||
constructor(private readonly reflector: Reflector) {
|
||||
// 启动定期清理任务
|
||||
this.startCleanupTask();
|
||||
}
|
||||
|
||||
/**
|
||||
* 守卫检查函数
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 是否允许通过
|
||||
*/
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// 1. 获取频率限制配置
|
||||
const throttleConfig = this.getThrottleConfig(context);
|
||||
|
||||
if (!throttleConfig) {
|
||||
// 没有配置频率限制,直接通过
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. 获取请求信息
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const key = this.generateKey(request, throttleConfig);
|
||||
|
||||
// 3. 检查频率限制
|
||||
const isAllowed = this.checkThrottle(key, throttleConfig);
|
||||
|
||||
if (!isAllowed) {
|
||||
// 4. 记录被限制的请求
|
||||
this.logger.warn('请求被频率限制', {
|
||||
operation: 'throttle_limit',
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
ip: request.ip,
|
||||
userAgent: request.get('User-Agent'),
|
||||
limit: throttleConfig.limit,
|
||||
ttl: throttleConfig.ttl,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 5. 抛出频率限制异常
|
||||
const record = this.records.get(key);
|
||||
const resetTime = new Date(record!.windowStart + throttleConfig.ttl * 1000);
|
||||
|
||||
const response: ThrottleResponse = {
|
||||
success: false,
|
||||
message: throttleConfig.message || '请求过于频繁,请稍后再试',
|
||||
error_code: 'TOO_MANY_REQUESTS',
|
||||
throttle_info: {
|
||||
limit: throttleConfig.limit,
|
||||
window_seconds: throttleConfig.ttl,
|
||||
current_requests: record!.count,
|
||||
reset_time: resetTime.toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
throw new HttpException(response, HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取频率限制配置
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 频率限制配置或null
|
||||
*/
|
||||
private getThrottleConfig(context: ExecutionContext): ThrottleConfig | null {
|
||||
// 从方法装饰器获取配置
|
||||
const methodConfig = this.reflector.get<ThrottleConfig>(
|
||||
THROTTLE_KEY,
|
||||
context.getHandler()
|
||||
);
|
||||
|
||||
if (methodConfig) {
|
||||
return methodConfig;
|
||||
}
|
||||
|
||||
// 从类装饰器获取配置
|
||||
const classConfig = this.reflector.get<ThrottleConfig>(
|
||||
THROTTLE_KEY,
|
||||
context.getClass()
|
||||
);
|
||||
|
||||
return classConfig || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成限制键
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @param config 频率限制配置
|
||||
* @returns 限制键
|
||||
*/
|
||||
private generateKey(request: Request, config: ThrottleConfig): string {
|
||||
const ip = request.ip || 'unknown';
|
||||
const path = request.route?.path || request.url;
|
||||
const method = request.method;
|
||||
|
||||
// 根据限制类型生成不同的键
|
||||
if (config.type === 'user') {
|
||||
// 基于用户的限制(需要从JWT中获取用户ID)
|
||||
const userId = this.extractUserId(request);
|
||||
return `user:${userId}:${method}:${path}`;
|
||||
} else {
|
||||
// 基于IP的限制(默认)
|
||||
return `ip:${ip}:${method}:${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查频率限制
|
||||
*
|
||||
* @param key 限制键
|
||||
* @param config 频率限制配置
|
||||
* @returns 是否允许通过
|
||||
*/
|
||||
private checkThrottle(key: string, config: ThrottleConfig): boolean {
|
||||
const now = Date.now();
|
||||
const windowMs = config.ttl * 1000;
|
||||
|
||||
let record = this.records.get(key);
|
||||
|
||||
if (!record) {
|
||||
// 第一次请求
|
||||
this.records.set(key, {
|
||||
count: 1,
|
||||
windowStart: now,
|
||||
lastRequest: now
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否需要重置窗口
|
||||
if (now - record.windowStart >= windowMs) {
|
||||
// 重置窗口
|
||||
record.count = 1;
|
||||
record.windowStart = now;
|
||||
record.lastRequest = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 在当前窗口内
|
||||
if (record.count >= config.limit) {
|
||||
// 超过限制
|
||||
return false;
|
||||
}
|
||||
|
||||
// 增加计数
|
||||
record.count++;
|
||||
record.lastRequest = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中提取用户ID
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @returns 用户ID
|
||||
*/
|
||||
private extractUserId(request: Request): string {
|
||||
// 这里应该从JWT token中提取用户ID
|
||||
// 简化实现,使用IP作为fallback
|
||||
const authHeader = request.get('Authorization');
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
try {
|
||||
// 这里应该解析JWT token获取用户ID
|
||||
// 简化实现,返回token的hash
|
||||
const token = authHeader.substring(7);
|
||||
return Buffer.from(token).toString('base64').substring(0, 10);
|
||||
} catch (error) {
|
||||
// JWT解析失败,使用IP
|
||||
return request.ip || 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
return request.ip || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动清理任务
|
||||
*/
|
||||
private startCleanupTask(): void {
|
||||
setInterval(() => {
|
||||
this.cleanupExpiredRecords();
|
||||
}, this.cleanupInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期记录
|
||||
*/
|
||||
private cleanupExpiredRecords(): void {
|
||||
const now = Date.now();
|
||||
const maxAge = 3600000; // 1小时
|
||||
|
||||
for (const [key, record] of this.records.entries()) {
|
||||
if (now - record.lastRequest > maxAge) {
|
||||
this.records.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前记录统计
|
||||
*
|
||||
* @returns 记录统计信息
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
totalRecords: this.records.size,
|
||||
records: Array.from(this.records.entries()).map(([key, record]) => ({
|
||||
key,
|
||||
count: record.count,
|
||||
windowStart: new Date(record.windowStart).toISOString(),
|
||||
lastRequest: new Date(record.lastRequest).toISOString()
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有记录
|
||||
*/
|
||||
clearAllRecords(): void {
|
||||
this.records.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定键的记录
|
||||
*
|
||||
* @param key 限制键
|
||||
*/
|
||||
clearRecord(key: string): void {
|
||||
this.records.delete(key);
|
||||
}
|
||||
}
|
||||
27
src/business/security/index.ts
Normal file
27
src/business/security/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 安全功能模块导出
|
||||
*
|
||||
* 功能概述:
|
||||
* - 频率限制和防护机制
|
||||
* - 请求超时控制
|
||||
* - 维护模式管理
|
||||
* - 内容类型验证
|
||||
* - 系统安全中间件
|
||||
*/
|
||||
|
||||
// 模块
|
||||
export * from './security.module';
|
||||
|
||||
// 守卫
|
||||
export * from './guards/throttle.guard';
|
||||
|
||||
// 中间件
|
||||
export * from './middleware/maintenance.middleware';
|
||||
export * from './middleware/content-type.middleware';
|
||||
|
||||
// 拦截器
|
||||
export * from './interceptors/timeout.interceptor';
|
||||
|
||||
// 装饰器
|
||||
export * from './decorators/throttle.decorator';
|
||||
export * from './decorators/timeout.decorator';
|
||||
179
src/business/security/interceptors/timeout.interceptor.ts
Normal file
179
src/business/security/interceptors/timeout.interceptor.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 超时拦截器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 实现API接口的超时控制逻辑
|
||||
* - 在超时时自动取消请求并返回错误
|
||||
* - 记录超时事件的详细日志
|
||||
*
|
||||
* 使用场景:
|
||||
* - 全局超时控制
|
||||
* - 防止资源泄漏
|
||||
* - 提升系统稳定性
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
RequestTimeoutException,
|
||||
Logger
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable, throwError, TimeoutError } from 'rxjs';
|
||||
import { catchError, timeout } from 'rxjs/operators';
|
||||
import { TIMEOUT_KEY, TimeoutConfig } from '../decorators/timeout.decorator';
|
||||
|
||||
/**
|
||||
* 超时响应接口
|
||||
*/
|
||||
interface TimeoutResponse {
|
||||
/** 请求是否成功 */
|
||||
success: boolean;
|
||||
/** 响应消息 */
|
||||
message: string;
|
||||
/** 错误代码 */
|
||||
error_code: string;
|
||||
/** 超时信息 */
|
||||
timeout_info: {
|
||||
/** 超时时间(毫秒) */
|
||||
timeout_ms: number;
|
||||
/** 超时发生时间 */
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TimeoutInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(TimeoutInterceptor.name);
|
||||
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
/**
|
||||
* 拦截器处理函数
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 获取超时配置
|
||||
* 2. 应用超时控制
|
||||
* 3. 处理超时异常
|
||||
* 4. 记录超时日志
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @param next 调用处理器
|
||||
* @returns 可观察对象
|
||||
*/
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
// 1. 获取超时配置
|
||||
const timeoutConfig = this.getTimeoutConfig(context);
|
||||
|
||||
if (!timeoutConfig) {
|
||||
// 没有配置超时,直接执行
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
// 2. 获取请求信息用于日志记录
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const startTime = Date.now();
|
||||
|
||||
// 3. 应用超时控制
|
||||
return next.handle().pipe(
|
||||
timeout(timeoutConfig.timeout),
|
||||
catchError((error) => {
|
||||
if (error instanceof TimeoutError) {
|
||||
// 4. 处理超时异常
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// 5. 记录超时日志
|
||||
if (timeoutConfig.logTimeout !== false) {
|
||||
this.logger.warn('请求超时', {
|
||||
operation: 'request_timeout',
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
timeout_ms: timeoutConfig.timeout,
|
||||
actual_duration_ms: duration,
|
||||
userAgent: request.get('User-Agent'),
|
||||
ip: request.ip,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// 6. 构建超时响应
|
||||
const timeoutResponse: TimeoutResponse = {
|
||||
success: false,
|
||||
message: timeoutConfig.message || '请求超时,请稍后重试',
|
||||
error_code: 'REQUEST_TIMEOUT',
|
||||
timeout_info: {
|
||||
timeout_ms: timeoutConfig.timeout,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
// 7. 抛出超时异常
|
||||
return throwError(() => new RequestTimeoutException(timeoutResponse));
|
||||
}
|
||||
|
||||
// 其他异常直接抛出
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取超时配置
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 超时配置或null
|
||||
*/
|
||||
private getTimeoutConfig(context: ExecutionContext): TimeoutConfig | null {
|
||||
// 从方法装饰器获取配置
|
||||
const methodConfig = this.reflector.get<TimeoutConfig>(
|
||||
TIMEOUT_KEY,
|
||||
context.getHandler()
|
||||
);
|
||||
|
||||
if (methodConfig) {
|
||||
return methodConfig;
|
||||
}
|
||||
|
||||
// 从类装饰器获取配置
|
||||
const classConfig = this.reflector.get<TimeoutConfig>(
|
||||
TIMEOUT_KEY,
|
||||
context.getClass()
|
||||
);
|
||||
|
||||
return classConfig || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认超时配置
|
||||
*
|
||||
* @returns 默认超时配置
|
||||
*/
|
||||
private getDefaultTimeoutConfig(): TimeoutConfig {
|
||||
return {
|
||||
timeout: 30000, // 默认30秒
|
||||
message: '请求超时,请稍后重试',
|
||||
logTimeout: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证超时配置
|
||||
*
|
||||
* @param config 超时配置
|
||||
* @returns 是否有效
|
||||
*/
|
||||
private isValidTimeoutConfig(config: TimeoutConfig): boolean {
|
||||
return (
|
||||
config &&
|
||||
typeof config.timeout === 'number' &&
|
||||
config.timeout > 0 &&
|
||||
config.timeout <= 600000 // 最大10分钟
|
||||
);
|
||||
}
|
||||
}
|
||||
224
src/business/security/middleware/content-type.middleware.ts
Normal file
224
src/business/security/middleware/content-type.middleware.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 内容类型检查中间件
|
||||
*
|
||||
* 功能描述:
|
||||
* - 检查POST/PUT请求的Content-Type头
|
||||
* - 确保API接口接收正确的数据格式
|
||||
* - 提供友好的错误提示信息
|
||||
*
|
||||
* 使用场景:
|
||||
* - API接口数据格式验证
|
||||
* - 防止错误的请求格式
|
||||
* - 提升API接口的健壮性
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 不支持的媒体类型响应接口
|
||||
*/
|
||||
interface UnsupportedMediaTypeResponse {
|
||||
/** 请求是否成功 */
|
||||
success: boolean;
|
||||
/** 响应消息 */
|
||||
message: string;
|
||||
/** 错误代码 */
|
||||
error_code: string;
|
||||
/** 支持的媒体类型 */
|
||||
supported_types: string[];
|
||||
/** 接收到的媒体类型 */
|
||||
received_type?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ContentTypeMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger(ContentTypeMiddleware.name);
|
||||
|
||||
/**
|
||||
* 需要检查Content-Type的HTTP方法
|
||||
*/
|
||||
private readonly methodsToCheck = ['POST', 'PUT', 'PATCH'];
|
||||
|
||||
/**
|
||||
* 支持的Content-Type列表
|
||||
*/
|
||||
private readonly supportedTypes = [
|
||||
'application/json',
|
||||
'application/json; charset=utf-8'
|
||||
];
|
||||
|
||||
/**
|
||||
* 不需要检查Content-Type的路径(正则表达式)
|
||||
*/
|
||||
private readonly excludePaths = [
|
||||
/^\/api-docs/, // Swagger文档
|
||||
/^\/health/, // 健康检查
|
||||
/^\/admin\/logs\/archive/, // 文件下载
|
||||
/\/upload/, // 文件上传
|
||||
];
|
||||
|
||||
/**
|
||||
* 中间件处理函数
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查是否需要验证Content-Type
|
||||
* 2. 获取请求的Content-Type头
|
||||
* 3. 验证Content-Type是否支持
|
||||
* 4. 记录不支持的请求类型
|
||||
*
|
||||
* @param req HTTP请求对象
|
||||
* @param res HTTP响应对象
|
||||
* @param next 下一个中间件函数
|
||||
*/
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
// 1. 检查是否需要验证Content-Type
|
||||
if (!this.shouldCheckContentType(req)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取请求的Content-Type
|
||||
const contentType = req.get('Content-Type');
|
||||
|
||||
// 3. 检查Content-Type是否存在
|
||||
if (!contentType) {
|
||||
this.logger.warn('请求缺少Content-Type头', {
|
||||
operation: 'content_type_check',
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const response: UnsupportedMediaTypeResponse = {
|
||||
success: false,
|
||||
message: '请求缺少Content-Type头,请设置为application/json',
|
||||
error_code: 'MISSING_CONTENT_TYPE',
|
||||
supported_types: this.supportedTypes,
|
||||
received_type: undefined
|
||||
};
|
||||
|
||||
res.status(415).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 验证Content-Type是否支持
|
||||
const normalizedContentType = this.normalizeContentType(contentType);
|
||||
|
||||
if (!this.isSupportedContentType(normalizedContentType)) {
|
||||
this.logger.warn('不支持的Content-Type', {
|
||||
operation: 'content_type_check',
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
contentType: contentType,
|
||||
normalizedContentType: normalizedContentType,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const response: UnsupportedMediaTypeResponse = {
|
||||
success: false,
|
||||
message: `不支持的Content-Type: ${contentType},请使用application/json`,
|
||||
error_code: 'UNSUPPORTED_MEDIA_TYPE',
|
||||
supported_types: this.supportedTypes,
|
||||
received_type: contentType
|
||||
};
|
||||
|
||||
res.status(415).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Content-Type验证通过,继续处理
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要验证Content-Type
|
||||
*
|
||||
* @param req HTTP请求对象
|
||||
* @returns 是否需要验证
|
||||
*/
|
||||
private shouldCheckContentType(req: Request): boolean {
|
||||
// 1. 检查HTTP方法
|
||||
if (!this.methodsToCheck.includes(req.method)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 检查是否在排除路径中
|
||||
const url = req.url;
|
||||
for (const excludePattern of this.excludePaths) {
|
||||
if (excludePattern.test(url)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查Content-Length,如果为0则不需要验证
|
||||
const contentLength = req.get('Content-Length');
|
||||
if (contentLength === '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化Content-Type
|
||||
*
|
||||
* @param contentType 原始Content-Type
|
||||
* @returns 标准化后的Content-Type
|
||||
*/
|
||||
private normalizeContentType(contentType: string): string {
|
||||
// 移除空格并转换为小写
|
||||
return contentType.toLowerCase().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Content-Type是否支持
|
||||
*
|
||||
* @param contentType 标准化的Content-Type
|
||||
* @returns 是否支持
|
||||
*/
|
||||
private isSupportedContentType(contentType: string): boolean {
|
||||
// 检查是否以支持的类型开头
|
||||
return this.supportedTypes.some(supportedType =>
|
||||
contentType.startsWith(supportedType.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的Content-Type列表
|
||||
*
|
||||
* @returns 支持的类型列表
|
||||
*/
|
||||
getSupportedTypes(): string[] {
|
||||
return [...this.supportedTypes];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加支持的Content-Type
|
||||
*
|
||||
* @param contentType 要添加的Content-Type
|
||||
*/
|
||||
addSupportedType(contentType: string): void {
|
||||
if (!this.supportedTypes.includes(contentType)) {
|
||||
this.supportedTypes.push(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加排除路径
|
||||
*
|
||||
* @param pattern 路径正则表达式
|
||||
*/
|
||||
addExcludePath(pattern: RegExp): void {
|
||||
this.excludePaths.push(pattern);
|
||||
}
|
||||
}
|
||||
137
src/business/security/middleware/maintenance.middleware.ts
Normal file
137
src/business/security/middleware/maintenance.middleware.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 维护模式中间件
|
||||
*
|
||||
* 功能描述:
|
||||
* - 检查系统是否处于维护模式
|
||||
* - 在维护期间阻止用户访问API
|
||||
* - 提供维护状态和预计恢复时间信息
|
||||
*
|
||||
* 使用场景:
|
||||
* - 系统升级维护
|
||||
* - 数据库迁移
|
||||
* - 紧急故障修复
|
||||
* - 定期维护窗口
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 维护模式响应接口
|
||||
*/
|
||||
interface MaintenanceResponse {
|
||||
/** 请求是否成功 */
|
||||
success: boolean;
|
||||
/** 响应消息 */
|
||||
message: string;
|
||||
/** 错误代码 */
|
||||
error_code: string;
|
||||
/** 维护信息 */
|
||||
maintenance_info?: {
|
||||
/** 维护开始时间 */
|
||||
start_time: string;
|
||||
/** 预计结束时间 */
|
||||
estimated_end_time?: string;
|
||||
/** 重试间隔(秒) */
|
||||
retry_after: number;
|
||||
/** 维护原因 */
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MaintenanceMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger(MaintenanceMiddleware.name);
|
||||
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 中间件处理函数
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查维护模式环境变量
|
||||
* 2. 如果处于维护模式,返回503状态码
|
||||
* 3. 提供维护信息和重试建议
|
||||
* 4. 记录维护期间的访问尝试
|
||||
*
|
||||
* @param req HTTP请求对象
|
||||
* @param res HTTP响应对象
|
||||
* @param next 下一个中间件函数
|
||||
*/
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
// 1. 检查维护模式状态
|
||||
const isMaintenanceMode = this.configService.get<string>('MAINTENANCE_MODE') === 'true';
|
||||
|
||||
if (!isMaintenanceMode) {
|
||||
// 非维护模式,继续处理请求
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 记录维护期间的访问尝试
|
||||
this.logger.warn('维护模式:拒绝访问请求', {
|
||||
operation: 'maintenance_check',
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 3. 获取维护配置信息
|
||||
const maintenanceStartTime = this.configService.get<string>('MAINTENANCE_START_TIME') || new Date().toISOString();
|
||||
const maintenanceEndTime = this.configService.get<string>('MAINTENANCE_END_TIME');
|
||||
const maintenanceReason = this.configService.get<string>('MAINTENANCE_REASON') || '系统维护升级';
|
||||
const retryAfter = this.configService.get<number>('MAINTENANCE_RETRY_AFTER') || 1800; // 默认30分钟
|
||||
|
||||
// 4. 构建维护模式响应
|
||||
const maintenanceResponse: MaintenanceResponse = {
|
||||
success: false,
|
||||
message: '系统正在维护中,请稍后再试',
|
||||
error_code: 'SERVICE_UNAVAILABLE',
|
||||
maintenance_info: {
|
||||
start_time: maintenanceStartTime,
|
||||
estimated_end_time: maintenanceEndTime,
|
||||
retry_after: retryAfter,
|
||||
reason: maintenanceReason
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 设置HTTP响应头
|
||||
res.setHeader('Retry-After', retryAfter.toString());
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
// 6. 返回503服务不可用状态
|
||||
res.status(503).json(maintenanceResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查维护模式是否启用
|
||||
*
|
||||
* @returns 是否处于维护模式
|
||||
*/
|
||||
isMaintenanceEnabled(): boolean {
|
||||
return this.configService.get<string>('MAINTENANCE_MODE') === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取维护信息
|
||||
*
|
||||
* @returns 维护配置信息
|
||||
*/
|
||||
getMaintenanceInfo() {
|
||||
return {
|
||||
enabled: this.isMaintenanceEnabled(),
|
||||
startTime: this.configService.get<string>('MAINTENANCE_START_TIME'),
|
||||
endTime: this.configService.get<string>('MAINTENANCE_END_TIME'),
|
||||
reason: this.configService.get<string>('MAINTENANCE_REASON'),
|
||||
retryAfter: this.configService.get<number>('MAINTENANCE_RETRY_AFTER')
|
||||
};
|
||||
}
|
||||
}
|
||||
37
src/business/security/security.module.ts
Normal file
37
src/business/security/security.module.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 安全功能模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合所有安全相关功能
|
||||
* - 频率限制和请求超时控制
|
||||
* - 维护模式和内容类型验证
|
||||
* - 系统安全防护机制
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ThrottleGuard } from './guards/throttle.guard';
|
||||
import { TimeoutInterceptor } from './interceptors/timeout.interceptor';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
ThrottleGuard,
|
||||
TimeoutInterceptor,
|
||||
// 全局频率限制守卫
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottleGuard,
|
||||
},
|
||||
// 全局超时拦截器
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: TimeoutInterceptor,
|
||||
},
|
||||
],
|
||||
exports: [ThrottleGuard, TimeoutInterceptor],
|
||||
})
|
||||
export class SecurityModule {}
|
||||
72
src/business/shared/dto/app-status.dto.ts
Normal file
72
src/business/shared/dto/app-status.dto.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 应用状态响应 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义应用状态接口的响应格式
|
||||
* - 提供 Swagger 文档生成支持
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 应用状态响应 DTO
|
||||
*/
|
||||
export class AppStatusResponseDto {
|
||||
@ApiProperty({
|
||||
description: '服务名称',
|
||||
example: 'Pixel Game Server',
|
||||
type: String
|
||||
})
|
||||
service: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '服务版本',
|
||||
example: '1.0.0',
|
||||
type: String
|
||||
})
|
||||
version: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '运行状态',
|
||||
example: 'running',
|
||||
enum: ['running', 'starting', 'stopping', 'error'],
|
||||
type: String
|
||||
})
|
||||
status: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '当前时间戳',
|
||||
example: '2025-12-17T15:00:00.000Z',
|
||||
type: String,
|
||||
format: 'date-time'
|
||||
})
|
||||
timestamp: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '运行时间(秒)',
|
||||
example: 3600,
|
||||
type: Number,
|
||||
minimum: 0
|
||||
})
|
||||
uptime: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '运行环境',
|
||||
example: 'development',
|
||||
enum: ['development', 'production', 'test'],
|
||||
type: String
|
||||
})
|
||||
environment: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '存储模式',
|
||||
example: 'memory',
|
||||
enum: ['database', 'memory'],
|
||||
type: String
|
||||
})
|
||||
storage_mode: 'database' | 'memory';
|
||||
}
|
||||
56
src/business/shared/dto/error-response.dto.ts
Normal file
56
src/business/shared/dto/error-response.dto.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 通用错误响应 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义统一的错误响应格式
|
||||
* - 提供 Swagger 文档生成支持
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 通用错误响应 DTO
|
||||
*/
|
||||
export class ErrorResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'HTTP 状态码',
|
||||
example: 500,
|
||||
type: Number
|
||||
})
|
||||
statusCode: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误消息',
|
||||
example: 'Internal server error',
|
||||
type: String
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误发生时间',
|
||||
example: '2025-12-17T15:00:00.000Z',
|
||||
type: String,
|
||||
format: 'date-time'
|
||||
})
|
||||
timestamp: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '请求路径',
|
||||
example: '/api/status',
|
||||
type: String,
|
||||
required: false
|
||||
})
|
||||
path?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'INTERNAL_ERROR',
|
||||
type: String,
|
||||
required: false
|
||||
})
|
||||
error?: string;
|
||||
}
|
||||
17
src/business/shared/dto/index.ts
Normal file
17
src/business/shared/dto/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 共享 DTO 统一导出
|
||||
*
|
||||
* 功能描述:
|
||||
* - 导出所有共享的 DTO 类
|
||||
* - 提供统一的导入入口
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
// 应用状态相关
|
||||
export * from './app-status.dto';
|
||||
|
||||
// 错误响应相关
|
||||
export * from './error-response.dto';
|
||||
14
src/business/shared/index.ts
Normal file
14
src/business/shared/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 共享模块统一导出
|
||||
*
|
||||
* 功能描述:
|
||||
* - 导出所有共享的组件和类型
|
||||
* - 提供统一的导入入口
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
// DTO
|
||||
export * from './dto';
|
||||
162
src/business/user-mgmt/controllers/user-status.controller.ts
Normal file
162
src/business/user-mgmt/controllers/user-status.controller.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 用户状态管理控制器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 管理员管理用户账户状态
|
||||
* - 支持批量状态操作
|
||||
* - 提供状态变更审计日志
|
||||
*
|
||||
* API端点:
|
||||
* - PUT /admin/users/:id/status - 修改用户状态
|
||||
* - POST /admin/users/batch-status - 批量修改用户状态
|
||||
* - GET /admin/users/status-stats - 获取用户状态统计
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Put, Post, UseGuards, ValidationPipe, UsePipes, Logger } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AdminGuard } from '../../admin/guards/admin.guard';
|
||||
import { UserManagementService } from '../services/user-management.service';
|
||||
import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator';
|
||||
import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator';
|
||||
import { UserStatusDto, BatchUserStatusDto } from '../dto/user-status.dto';
|
||||
import { UserStatusResponseDto, BatchUserStatusResponseDto, UserStatusStatsResponseDto } from '../dto/user-status-response.dto';
|
||||
|
||||
@ApiTags('user-management')
|
||||
@Controller('admin/users')
|
||||
export class UserStatusController {
|
||||
private readonly logger = new Logger(UserStatusController.name);
|
||||
|
||||
constructor(private readonly userManagementService: UserManagementService) {}
|
||||
|
||||
/**
|
||||
* 修改用户状态
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @param userStatusDto 状态修改数据
|
||||
* @returns 修改结果
|
||||
*/
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: '修改用户状态',
|
||||
description: '管理员修改指定用户的账户状态,支持激活、锁定、禁用等操作'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '用户ID' })
|
||||
@ApiBody({ type: UserStatusDto })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '状态修改成功',
|
||||
type: UserStatusResponseDto
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 403,
|
||||
description: '权限不足'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '用户不存在'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 429,
|
||||
description: '操作过于频繁'
|
||||
})
|
||||
@UseGuards(AdminGuard)
|
||||
@Throttle(ThrottlePresets.ADMIN_OPERATION)
|
||||
@Timeout(TimeoutPresets.NORMAL)
|
||||
@Put(':id/status')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async updateUserStatus(
|
||||
@Param('id') id: string,
|
||||
@Body() userStatusDto: UserStatusDto
|
||||
): Promise<UserStatusResponseDto> {
|
||||
this.logger.log('管理员修改用户状态', {
|
||||
operation: 'update_user_status',
|
||||
userId: id,
|
||||
newStatus: userStatusDto.status,
|
||||
reason: userStatusDto.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return await this.userManagementService.updateUserStatus(BigInt(id), userStatusDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量修改用户状态
|
||||
*
|
||||
* @param batchUserStatusDto 批量状态修改数据
|
||||
* @returns 批量修改结果
|
||||
*/
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: '批量修改用户状态',
|
||||
description: '管理员批量修改多个用户的账户状态'
|
||||
})
|
||||
@ApiBody({ type: BatchUserStatusDto })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '批量修改成功',
|
||||
type: BatchUserStatusResponseDto
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 403,
|
||||
description: '权限不足'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 429,
|
||||
description: '操作过于频繁'
|
||||
})
|
||||
@UseGuards(AdminGuard)
|
||||
@Throttle(ThrottlePresets.ADMIN_OPERATION)
|
||||
@Timeout(TimeoutPresets.SLOW)
|
||||
@Post('batch-status')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async batchUpdateUserStatus(
|
||||
@Body() batchUserStatusDto: BatchUserStatusDto
|
||||
): Promise<BatchUserStatusResponseDto> {
|
||||
this.logger.log('管理员批量修改用户状态', {
|
||||
operation: 'batch_update_user_status',
|
||||
userCount: batchUserStatusDto.user_ids.length,
|
||||
newStatus: batchUserStatusDto.status,
|
||||
reason: batchUserStatusDto.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return await this.userManagementService.batchUpdateUserStatus(batchUserStatusDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户状态统计
|
||||
*
|
||||
* @returns 状态统计信息
|
||||
*/
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: '获取用户状态统计',
|
||||
description: '获取各种用户状态的数量统计信息'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: UserStatusStatsResponseDto
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 403,
|
||||
description: '权限不足'
|
||||
})
|
||||
@UseGuards(AdminGuard)
|
||||
@Timeout(TimeoutPresets.DATABASE_QUERY)
|
||||
@Get('status-stats')
|
||||
async getUserStatusStats(): Promise<UserStatusStatsResponseDto> {
|
||||
this.logger.log('管理员获取用户状态统计', {
|
||||
operation: 'get_user_status_stats',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return await this.userManagementService.getUserStatusStats();
|
||||
}
|
||||
}
|
||||
294
src/business/user-mgmt/dto/user-status-response.dto.ts
Normal file
294
src/business/user-mgmt/dto/user-status-response.dto.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* 用户状态管理响应 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户状态管理相关的响应数据结构
|
||||
* - 提供Swagger文档生成支持
|
||||
* - 确保状态管理API响应的数据格式一致性
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { UserStatus } from '../enums/user-status.enum';
|
||||
|
||||
/**
|
||||
* 用户状态信息DTO
|
||||
*/
|
||||
export class UserStatusInfoDto {
|
||||
@ApiProperty({
|
||||
description: '用户ID',
|
||||
example: '1'
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户名',
|
||||
example: 'testuser'
|
||||
})
|
||||
username: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户昵称',
|
||||
example: '测试用户'
|
||||
})
|
||||
nickname: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户状态',
|
||||
enum: UserStatus,
|
||||
example: UserStatus.ACTIVE
|
||||
})
|
||||
status: UserStatus;
|
||||
|
||||
@ApiProperty({
|
||||
description: '状态描述',
|
||||
example: '正常'
|
||||
})
|
||||
status_description: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '状态修改时间',
|
||||
example: '2025-12-24T10:00:00.000Z'
|
||||
})
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户状态修改响应数据DTO
|
||||
*/
|
||||
export class UserStatusDataDto {
|
||||
@ApiProperty({
|
||||
description: '用户信息',
|
||||
type: UserStatusInfoDto
|
||||
})
|
||||
user: UserStatusInfoDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '修改原因',
|
||||
example: '用户违反社区规定',
|
||||
required: false
|
||||
})
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户状态修改响应DTO
|
||||
*/
|
||||
export class UserStatusResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: UserStatusDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: UserStatusDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '用户状态修改成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'USER_STATUS_UPDATE_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作结果DTO
|
||||
*/
|
||||
export class BatchOperationResultDto {
|
||||
@ApiProperty({
|
||||
description: '成功处理的用户列表',
|
||||
type: [UserStatusInfoDto]
|
||||
})
|
||||
success_users: UserStatusInfoDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: '处理失败的用户列表',
|
||||
type: [Object],
|
||||
example: [
|
||||
{
|
||||
user_id: '999',
|
||||
error: '用户不存在'
|
||||
}
|
||||
]
|
||||
})
|
||||
failed_users: Array<{
|
||||
user_id: string;
|
||||
error: string;
|
||||
}>;
|
||||
|
||||
@ApiProperty({
|
||||
description: '成功处理数量',
|
||||
example: 5
|
||||
})
|
||||
success_count: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '失败处理数量',
|
||||
example: 1
|
||||
})
|
||||
failed_count: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '总处理数量',
|
||||
example: 6
|
||||
})
|
||||
total_count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量用户状态修改响应数据DTO
|
||||
*/
|
||||
export class BatchUserStatusDataDto {
|
||||
@ApiProperty({
|
||||
description: '批量操作结果',
|
||||
type: BatchOperationResultDto
|
||||
})
|
||||
result: BatchOperationResultDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '修改原因',
|
||||
example: '批量处理违规用户',
|
||||
required: false
|
||||
})
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量用户状态修改响应DTO
|
||||
*/
|
||||
export class BatchUserStatusResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: BatchUserStatusDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: BatchUserStatusDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '批量用户状态修改完成'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'BATCH_USER_STATUS_UPDATE_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户状态统计DTO
|
||||
*/
|
||||
export class UserStatusStatsDto {
|
||||
@ApiProperty({
|
||||
description: '正常用户数量',
|
||||
example: 1250
|
||||
})
|
||||
active: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '未激活用户数量',
|
||||
example: 45
|
||||
})
|
||||
inactive: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '锁定用户数量',
|
||||
example: 12
|
||||
})
|
||||
locked: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '禁用用户数量',
|
||||
example: 8
|
||||
})
|
||||
banned: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '已删除用户数量',
|
||||
example: 3
|
||||
})
|
||||
deleted: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '待审核用户数量',
|
||||
example: 15
|
||||
})
|
||||
pending: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '总用户数量',
|
||||
example: 1333
|
||||
})
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户状态统计响应数据DTO
|
||||
*/
|
||||
export class UserStatusStatsDataDto {
|
||||
@ApiProperty({
|
||||
description: '用户状态统计',
|
||||
type: UserStatusStatsDto
|
||||
})
|
||||
stats: UserStatusStatsDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '统计时间',
|
||||
example: '2025-12-24T10:00:00.000Z'
|
||||
})
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户状态统计响应DTO
|
||||
*/
|
||||
export class UserStatusStatsResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: UserStatusStatsDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: UserStatusStatsDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '用户状态统计获取成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'USER_STATUS_STATS_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
95
src/business/user-mgmt/dto/user-status.dto.ts
Normal file
95
src/business/user-mgmt/dto/user-status.dto.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 用户状态管理 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户状态管理相关的请求数据结构
|
||||
* - 提供数据验证规则和错误提示
|
||||
* - 确保状态管理操作的数据格式一致性
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { IsString, IsNotEmpty, IsEnum, IsOptional, IsArray, ArrayMinSize, ArrayMaxSize } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { UserStatus } from '../enums/user-status.enum';
|
||||
|
||||
/**
|
||||
* 用户状态修改请求DTO
|
||||
*/
|
||||
export class UserStatusDto {
|
||||
/**
|
||||
* 新的用户状态
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户状态',
|
||||
enum: UserStatus,
|
||||
example: UserStatus.ACTIVE,
|
||||
enumName: 'UserStatus'
|
||||
})
|
||||
@IsEnum(UserStatus, { message: '用户状态必须是有效的枚举值' })
|
||||
@IsNotEmpty({ message: '用户状态不能为空' })
|
||||
status: UserStatus;
|
||||
|
||||
/**
|
||||
* 状态修改原因
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '状态修改原因(可选)',
|
||||
example: '用户违反社区规定',
|
||||
required: false,
|
||||
maxLength: 200
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '修改原因必须是字符串' })
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量用户状态修改请求DTO
|
||||
*/
|
||||
export class BatchUserStatusDto {
|
||||
/**
|
||||
* 用户ID列表
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户ID列表',
|
||||
example: ['1', '2', '3'],
|
||||
type: [String],
|
||||
minItems: 1,
|
||||
maxItems: 100
|
||||
})
|
||||
@IsArray({ message: '用户ID列表必须是数组' })
|
||||
@ArrayMinSize(1, { message: '至少需要选择一个用户' })
|
||||
@ArrayMaxSize(100, { message: '一次最多只能操作100个用户' })
|
||||
@IsString({ each: true, message: '用户ID必须是字符串' })
|
||||
@IsNotEmpty({ each: true, message: '用户ID不能为空' })
|
||||
user_ids: string[];
|
||||
|
||||
/**
|
||||
* 新的用户状态
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户状态',
|
||||
enum: UserStatus,
|
||||
example: UserStatus.LOCKED,
|
||||
enumName: 'UserStatus'
|
||||
})
|
||||
@IsEnum(UserStatus, { message: '用户状态必须是有效的枚举值' })
|
||||
@IsNotEmpty({ message: '用户状态不能为空' })
|
||||
status: UserStatus;
|
||||
|
||||
/**
|
||||
* 状态修改原因
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '批量修改原因(可选)',
|
||||
example: '批量处理违规用户',
|
||||
required: false,
|
||||
maxLength: 200
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '修改原因必须是字符串' })
|
||||
reason?: string;
|
||||
}
|
||||
100
src/business/user-mgmt/enums/user-status.enum.ts
Normal file
100
src/business/user-mgmt/enums/user-status.enum.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 用户状态枚举
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户账户的各种状态
|
||||
* - 提供状态检查和描述功能
|
||||
* - 支持用户生命周期管理
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户状态枚举
|
||||
*
|
||||
* 状态说明:
|
||||
* - active: 正常状态,可以正常使用所有功能
|
||||
* - inactive: 未激活状态,通常是新注册用户需要邮箱验证
|
||||
* - locked: 临时锁定状态,可以解锁恢复
|
||||
* - banned: 永久禁用状态,需要管理员处理
|
||||
* - deleted: 软删除状态,数据保留但不可使用
|
||||
* - pending: 待审核状态,需要管理员审核后激活
|
||||
*/
|
||||
export enum UserStatus {
|
||||
ACTIVE = 'active', // 正常状态
|
||||
INACTIVE = 'inactive', // 未激活状态
|
||||
LOCKED = 'locked', // 锁定状态
|
||||
BANNED = 'banned', // 禁用状态
|
||||
DELETED = 'deleted', // 删除状态
|
||||
PENDING = 'pending' // 待审核状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户状态的中文描述
|
||||
*
|
||||
* @param status 用户状态
|
||||
* @returns 状态描述
|
||||
*/
|
||||
export function getUserStatusDescription(status: UserStatus): string {
|
||||
const descriptions = {
|
||||
[UserStatus.ACTIVE]: '正常',
|
||||
[UserStatus.INACTIVE]: '未激活',
|
||||
[UserStatus.LOCKED]: '已锁定',
|
||||
[UserStatus.BANNED]: '已禁用',
|
||||
[UserStatus.DELETED]: '已删除',
|
||||
[UserStatus.PENDING]: '待审核'
|
||||
};
|
||||
|
||||
return descriptions[status] || '未知状态';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否可以登录
|
||||
*
|
||||
* @param status 用户状态
|
||||
* @returns 是否可以登录
|
||||
*/
|
||||
export function canUserLogin(status: UserStatus): boolean {
|
||||
// 只有正常状态的用户可以登录
|
||||
return status === UserStatus.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户状态对应的错误消息
|
||||
*
|
||||
* @param status 用户状态
|
||||
* @returns 错误消息
|
||||
*/
|
||||
export function getUserStatusErrorMessage(status: UserStatus): string {
|
||||
const errorMessages = {
|
||||
[UserStatus.ACTIVE]: '', // 正常状态无错误
|
||||
[UserStatus.INACTIVE]: '账户未激活,请先验证邮箱',
|
||||
[UserStatus.LOCKED]: '账户已被锁定,请联系管理员',
|
||||
[UserStatus.BANNED]: '账户已被禁用,请联系管理员',
|
||||
[UserStatus.DELETED]: '账户不存在',
|
||||
[UserStatus.PENDING]: '账户待审核,请等待管理员审核'
|
||||
};
|
||||
|
||||
return errorMessages[status] || '账户状态异常';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的用户状态
|
||||
*
|
||||
* @returns 用户状态数组
|
||||
*/
|
||||
export function getAllUserStatuses(): UserStatus[] {
|
||||
return Object.values(UserStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查状态值是否有效
|
||||
*
|
||||
* @param status 状态值
|
||||
* @returns 是否为有效状态
|
||||
*/
|
||||
export function isValidUserStatus(status: string): status is UserStatus {
|
||||
return Object.values(UserStatus).includes(status as UserStatus);
|
||||
}
|
||||
22
src/business/user-mgmt/index.ts
Normal file
22
src/business/user-mgmt/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 用户管理业务模块导出
|
||||
*
|
||||
* 功能概述:
|
||||
* - 用户状态管理(激活、锁定、禁用等)
|
||||
* - 批量用户操作
|
||||
* - 用户状态统计和分析
|
||||
* - 状态变更审计和历史记录
|
||||
*/
|
||||
|
||||
// 模块
|
||||
export * from './user-mgmt.module';
|
||||
|
||||
// 控制器
|
||||
export * from './controllers/user-status.controller';
|
||||
|
||||
// 服务
|
||||
export * from './services/user-management.service';
|
||||
|
||||
// DTO
|
||||
export * from './dto/user-status.dto';
|
||||
export * from './dto/user-status-response.dto';
|
||||
199
src/business/user-mgmt/services/user-management.service.ts
Normal file
199
src/business/user-mgmt/services/user-management.service.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 用户管理业务服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 用户状态管理业务逻辑
|
||||
* - 批量用户操作
|
||||
* - 用户状态统计
|
||||
* - 状态变更审计
|
||||
*
|
||||
* 职责分工:
|
||||
* - 专注于用户管理相关的业务逻辑
|
||||
* - 调用 AdminService 的底层方法
|
||||
* - 提供用户管理特定的业务规则
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { AdminService } from '../../admin/admin.service';
|
||||
import { UserStatusDto, BatchUserStatusDto } from '../dto/user-status.dto';
|
||||
import {
|
||||
UserStatusResponseDto,
|
||||
BatchUserStatusResponseDto,
|
||||
UserStatusStatsResponseDto
|
||||
} from '../dto/user-status-response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UserManagementService {
|
||||
private readonly logger = new Logger(UserManagementService.name);
|
||||
|
||||
constructor(private readonly adminService: AdminService) {}
|
||||
|
||||
/**
|
||||
* 修改用户状态
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证状态变更的业务规则
|
||||
* 2. 记录状态变更原因
|
||||
* 3. 调用底层服务执行变更
|
||||
* 4. 记录业务审计日志
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param userStatusDto 状态修改数据
|
||||
* @returns 修改结果
|
||||
*/
|
||||
async updateUserStatus(userId: bigint, userStatusDto: UserStatusDto): Promise<UserStatusResponseDto> {
|
||||
this.logger.log('用户管理:开始修改用户状态', {
|
||||
operation: 'user_mgmt_update_status',
|
||||
userId: userId.toString(),
|
||||
newStatus: userStatusDto.status,
|
||||
reason: userStatusDto.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 调用底层管理员服务
|
||||
const result = await this.adminService.updateUserStatus(userId, userStatusDto);
|
||||
|
||||
// 记录业务层日志
|
||||
if (result.success) {
|
||||
this.logger.log('用户管理:用户状态修改成功', {
|
||||
operation: 'user_mgmt_update_status_success',
|
||||
userId: userId.toString(),
|
||||
newStatus: userStatusDto.status,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量修改用户状态
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证批量操作的业务规则
|
||||
* 2. 分批处理大量用户
|
||||
* 3. 提供批量操作的进度反馈
|
||||
* 4. 记录批量操作审计
|
||||
*
|
||||
* @param batchUserStatusDto 批量状态修改数据
|
||||
* @returns 批量修改结果
|
||||
*/
|
||||
async batchUpdateUserStatus(batchUserStatusDto: BatchUserStatusDto): Promise<BatchUserStatusResponseDto> {
|
||||
this.logger.log('用户管理:开始批量修改用户状态', {
|
||||
operation: 'user_mgmt_batch_update_status',
|
||||
userCount: batchUserStatusDto.user_ids.length,
|
||||
newStatus: batchUserStatusDto.status,
|
||||
reason: batchUserStatusDto.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 业务规则:限制批量操作的数量
|
||||
if (batchUserStatusDto.user_ids.length > 100) {
|
||||
this.logger.warn('用户管理:批量操作数量超限', {
|
||||
operation: 'user_mgmt_batch_update_limit_exceeded',
|
||||
requestCount: batchUserStatusDto.user_ids.length,
|
||||
maxAllowed: 100
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '批量操作数量不能超过100个用户',
|
||||
error_code: 'BATCH_OPERATION_LIMIT_EXCEEDED'
|
||||
};
|
||||
}
|
||||
|
||||
// 调用底层管理员服务
|
||||
const result = await this.adminService.batchUpdateUserStatus(batchUserStatusDto);
|
||||
|
||||
// 记录业务层日志
|
||||
if (result.success) {
|
||||
this.logger.log('用户管理:批量用户状态修改完成', {
|
||||
operation: 'user_mgmt_batch_update_status_success',
|
||||
successCount: result.data?.result.success_count || 0,
|
||||
failedCount: result.data?.result.failed_count || 0,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户状态统计
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 获取基础统计数据
|
||||
* 2. 计算业务相关的指标
|
||||
* 3. 提供状态分布分析
|
||||
* 4. 缓存统计结果
|
||||
*
|
||||
* @returns 状态统计信息
|
||||
*/
|
||||
async getUserStatusStats(): Promise<UserStatusStatsResponseDto> {
|
||||
this.logger.log('用户管理:获取用户状态统计', {
|
||||
operation: 'user_mgmt_get_status_stats',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 调用底层管理员服务
|
||||
const result = await this.adminService.getUserStatusStats();
|
||||
|
||||
// 业务层可以在这里添加额外的统计分析
|
||||
if (result.success && result.data) {
|
||||
const stats = result.data.stats;
|
||||
|
||||
// 计算业务指标
|
||||
const activeRate = stats.total > 0 ? (stats.active / stats.total * 100).toFixed(2) : '0';
|
||||
const problemUserCount = stats.locked + stats.banned + stats.deleted;
|
||||
|
||||
this.logger.log('用户管理:用户状态统计分析', {
|
||||
operation: 'user_mgmt_status_analysis',
|
||||
totalUsers: stats.total,
|
||||
activeUsers: stats.active,
|
||||
activeRate: `${activeRate}%`,
|
||||
problemUsers: problemUserCount,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户状态变更历史
|
||||
*
|
||||
* 业务功能:
|
||||
* - 查询指定用户的状态变更记录
|
||||
* - 提供状态变更的审计追踪
|
||||
* - 支持时间范围查询
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param limit 返回数量限制
|
||||
* @returns 状态变更历史
|
||||
*/
|
||||
async getUserStatusHistory(userId: bigint, limit: number = 10) {
|
||||
this.logger.log('用户管理:获取用户状态变更历史', {
|
||||
operation: 'user_mgmt_get_status_history',
|
||||
userId: userId.toString(),
|
||||
limit,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// TODO: 实现状态变更历史查询
|
||||
// 这里可以调用专门的审计日志服务
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
user_id: userId.toString(),
|
||||
history: [] as any[],
|
||||
total_count: 0
|
||||
},
|
||||
message: '状态变更历史获取成功(功能待实现)'
|
||||
};
|
||||
}
|
||||
}
|
||||
30
src/business/user-mgmt/user-mgmt.module.ts
Normal file
30
src/business/user-mgmt/user-mgmt.module.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 用户管理业务模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合用户状态管理相关的所有组件
|
||||
* - 提供用户生命周期管理功能
|
||||
* - 支持批量操作和状态统计
|
||||
*
|
||||
* 依赖关系:
|
||||
* - 依赖 AdminModule 提供底层管理功能
|
||||
* - 依赖 Core 模块提供基础设施
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UserStatusController } from './controllers/user-status.controller';
|
||||
import { UserManagementService } from './services/user-management.service';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { AdminCoreModule } from '../../core/admin_core/admin_core.module';
|
||||
|
||||
@Module({
|
||||
imports: [AdminModule, AdminCoreModule],
|
||||
controllers: [UserStatusController],
|
||||
providers: [UserManagementService],
|
||||
exports: [UserManagementService],
|
||||
})
|
||||
export class UserMgmtModule {}
|
||||
Reference in New Issue
Block a user