修复backend的gitignore规则并添加日志模块
- 修复.gitignore中logs规则匹配范围过大的问题 - 添加后端日志管理模块源代码 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fe91f88350
commit
00491c7ac4
2
backend/.gitignore
vendored
2
backend/.gitignore
vendored
@ -3,7 +3,7 @@
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
22
backend/src/logs/dto/create-log.dto.ts
Normal file
22
backend/src/logs/dto/create-log.dto.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { IsNumber, IsString, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateLogDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
userId?: number;
|
||||
|
||||
@IsString()
|
||||
action: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
content?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
ip?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userAgent?: string;
|
||||
}
|
||||
39
backend/src/logs/dto/query-log.dto.ts
Normal file
39
backend/src/logs/dto/query-log.dto.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { IsNumber, IsString, IsOptional, IsDateString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class QueryLogDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
page?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
pageSize?: number = 20;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
userId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
action?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
ip?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startTime?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endTime?: string;
|
||||
}
|
||||
64
backend/src/logs/logs.controller.ts
Normal file
64
backend/src/logs/logs.controller.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { LogsService } from './logs.service';
|
||||
import { QueryLogDto } from './dto/query-log.dto';
|
||||
import { RequirePermission } from '../auth/decorators/require-permission.decorator';
|
||||
|
||||
@Controller('logs')
|
||||
export class LogsController {
|
||||
constructor(private readonly logsService: LogsService) {}
|
||||
|
||||
/**
|
||||
* 查询日志列表(分页)
|
||||
*/
|
||||
@Get()
|
||||
@RequirePermission('log:read')
|
||||
async findAll(@Query() queryLogDto: QueryLogDto) {
|
||||
return this.logsService.findAll(queryLogDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志统计信息
|
||||
*/
|
||||
@Get('statistics')
|
||||
@RequirePermission('log:read')
|
||||
async getStatistics(@Query('days') days?: string) {
|
||||
const daysNum = days ? parseInt(days, 10) : 7;
|
||||
return this.logsService.getStatistics(daysNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单条日志详情
|
||||
*/
|
||||
@Get(':id')
|
||||
@RequirePermission('log:read')
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.logsService.findOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除日志
|
||||
*/
|
||||
@Delete()
|
||||
@RequirePermission('log:delete')
|
||||
async remove(@Body('ids') ids: number[]) {
|
||||
return this.logsService.remove(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期日志
|
||||
*/
|
||||
@Post('clean')
|
||||
@RequirePermission('log:delete')
|
||||
async cleanOldLogs(@Body('daysToKeep') daysToKeep?: number) {
|
||||
return this.logsService.cleanOldLogs(daysToKeep || 90);
|
||||
}
|
||||
}
|
||||
13
backend/src/logs/logs.module.ts
Normal file
13
backend/src/logs/logs.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { LogsService } from './logs.service';
|
||||
import { LogsController } from './logs.controller';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
|
||||
@Global() // 设为全局模块,让 LoggingInterceptor 可以注入 LogsService
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [LogsController],
|
||||
providers: [LogsService],
|
||||
exports: [LogsService],
|
||||
})
|
||||
export class LogsModule {}
|
||||
227
backend/src/logs/logs.service.ts
Normal file
227
backend/src/logs/logs.service.ts
Normal file
@ -0,0 +1,227 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateLogDto } from './dto/create-log.dto';
|
||||
import { QueryLogDto } from './dto/query-log.dto';
|
||||
|
||||
@Injectable()
|
||||
export class LogsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* 创建日志记录
|
||||
*/
|
||||
async create(createLogDto: CreateLogDto) {
|
||||
return this.prisma.log.create({
|
||||
data: {
|
||||
userId: createLogDto.userId,
|
||||
action: createLogDto.action,
|
||||
content: createLogDto.content,
|
||||
ip: createLogDto.ip,
|
||||
userAgent: createLogDto.userAgent,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询日志列表(分页)
|
||||
*/
|
||||
async findAll(queryLogDto: QueryLogDto) {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
userId,
|
||||
action,
|
||||
keyword,
|
||||
ip,
|
||||
startTime,
|
||||
endTime,
|
||||
} = queryLogDto;
|
||||
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
// 构建查询条件
|
||||
const where: any = {};
|
||||
|
||||
if (userId) {
|
||||
where.userId = userId;
|
||||
}
|
||||
|
||||
if (action) {
|
||||
where.action = {
|
||||
contains: action,
|
||||
};
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
where.OR = [
|
||||
{ action: { contains: keyword } },
|
||||
{ content: { contains: keyword } },
|
||||
];
|
||||
}
|
||||
|
||||
if (ip) {
|
||||
where.ip = {
|
||||
contains: ip,
|
||||
};
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (startTime || endTime) {
|
||||
where.createTime = {};
|
||||
if (startTime) {
|
||||
where.createTime.gte = new Date(startTime);
|
||||
}
|
||||
if (endTime) {
|
||||
where.createTime.lte = new Date(endTime);
|
||||
}
|
||||
}
|
||||
|
||||
const [list, total] = await Promise.all([
|
||||
this.prisma.log.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: pageSize,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nickname: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createTime: 'desc' },
|
||||
}),
|
||||
this.prisma.log.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单条日志详情
|
||||
*/
|
||||
async findOne(id: number) {
|
||||
return this.prisma.log.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nickname: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除日志(支持批量删除)
|
||||
*/
|
||||
async remove(ids: number[]) {
|
||||
return this.prisma.log.deleteMany({
|
||||
where: {
|
||||
id: { in: ids },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期日志(默认保留90天)
|
||||
*/
|
||||
async cleanOldLogs(daysToKeep: number = 90) {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
||||
|
||||
const result = await this.prisma.log.deleteMany({
|
||||
where: {
|
||||
createTime: {
|
||||
lt: cutoffDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleted: result.count,
|
||||
cutoffDate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志统计信息
|
||||
*/
|
||||
async getStatistics(days: number = 7) {
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - days);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
|
||||
// 总日志数
|
||||
const totalCount = await this.prisma.log.count();
|
||||
|
||||
// 近N天日志数
|
||||
const recentCount = await this.prisma.log.count({
|
||||
where: {
|
||||
createTime: {
|
||||
gte: startDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 按操作类型统计(取前10)
|
||||
const actionStats = await this.prisma.log.groupBy({
|
||||
by: ['action'],
|
||||
_count: {
|
||||
action: true,
|
||||
},
|
||||
where: {
|
||||
createTime: {
|
||||
gte: startDate,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
_count: {
|
||||
action: 'desc',
|
||||
},
|
||||
},
|
||||
take: 10,
|
||||
});
|
||||
|
||||
// 按天统计近N天的日志数量
|
||||
const dailyStats = await this.prisma.$queryRaw`
|
||||
SELECT
|
||||
DATE(create_time) as date,
|
||||
COUNT(*) as count
|
||||
FROM logs
|
||||
WHERE create_time >= ${startDate}
|
||||
GROUP BY DATE(create_time)
|
||||
ORDER BY date DESC
|
||||
` as Array<{ date: Date; count: bigint }>;
|
||||
|
||||
return {
|
||||
totalCount,
|
||||
recentCount,
|
||||
days,
|
||||
actionStats: actionStats.map((item) => ({
|
||||
action: item.action,
|
||||
count: item._count.action,
|
||||
})),
|
||||
dailyStats: dailyStats.map((item) => ({
|
||||
date: item.date,
|
||||
count: Number(item.count),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户操作日志
|
||||
*/
|
||||
async getUserLogs(userId: number, page: number = 1, pageSize: number = 20) {
|
||||
return this.findAll({ userId, page, pageSize });
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user