修复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
|
/node_modules
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
/logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
pnpm-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