fix(security): classrooms 上传限制/路由顺序/LIKE 转义补充
Some checks failed
CI / check (pull_request) Failing after 3m26s

由 OCR(open-codereview.ai,deepseek-v4-flash)审查驱动修复:
- classrooms.controller:report 路由移到 :id 之前、上传 10MB + 判空
- classrooms.service:搜索关键词 LIKE 通配符转义

Reviewed-by: OCR (open-codereview.ai)
This commit is contained in:
2026-08-09 21:30:42 +08:00
parent 1289aea7ce
commit db11a57e98
2 changed files with 37 additions and 34 deletions

View File

@@ -12,6 +12,7 @@ import {
Res,
UseInterceptors,
UploadedFile,
BadRequestException,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import type { Response } from 'express';
@@ -112,6 +113,38 @@ export class ClassroomsController {
res.end();
}
@Get('report')
@RequirePermission('classroom:view')
async exportReport(
@Query('dateFrom') dateFrom: string,
@Query('dateTo') dateTo: string,
@Res() res: Response,
) {
const data = await this.service.getUsageReport(dateFrom, dateTo);
const workbook = new ExcelJS.Workbook();
const ws = workbook.addWorksheet('教室使用统计');
ws.columns = [
{ header: '教室名称', key: 'name', width: 20 },
{ header: '楼栋', key: 'building', width: 12 },
{ header: '类型', key: 'roomType', width: 12 },
{ header: '容量', key: 'capacity', width: 8 },
{ header: '统计天数', key: 'totalDays', width: 10 },
{ header: '租赁占用天数', key: 'rentalDays', width: 14 },
{ header: '排课占用天数', key: 'scheduleDays', width: 14 },
{ header: '空闲天数', key: 'idleDays', width: 10 },
{ header: '占用率', key: 'occupancyRate', width: 10 },
];
ws.getRow(1).font = { bold: true };
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
for (const row of data) {
ws.addRow({ ...row, occupancyRate: `${row.occupancyRate}%` });
}
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename=classroom-report.xlsx');
await workbook.xlsx.write(res);
res.end();
}
@Get(':id')
@RequirePermission('classroom:view')
findOne(@Param('id') id: string) {
@@ -170,8 +203,9 @@ export class ClassroomsController {
@Post('import')
@RequirePermission('classroom:create')
@UseInterceptors(FileInterceptor('file'))
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 10 * 1024 * 1024 } }))
async importExcel(@UploadedFile() file: Express.Multer.File, @Request() req: AuthenticatedRequest) {
if (!file) throw new BadRequestException('缺少上传文件');
const { ipAddress, userAgent } = extractRequestInfo(req);
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(bufferToArrayBuffer(file.buffer));
@@ -205,36 +239,4 @@ export class ClassroomsController {
});
return result;
}
@Get('report')
@RequirePermission('classroom:view')
async exportReport(
@Query('dateFrom') dateFrom: string,
@Query('dateTo') dateTo: string,
@Res() res: Response,
) {
const data = await this.service.getUsageReport(dateFrom, dateTo);
const workbook = new ExcelJS.Workbook();
const ws = workbook.addWorksheet('教室使用统计');
ws.columns = [
{ header: '教室名称', key: 'name', width: 20 },
{ header: '楼栋', key: 'building', width: 12 },
{ header: '类型', key: 'roomType', width: 12 },
{ header: '容量', key: 'capacity', width: 8 },
{ header: '统计天数', key: 'totalDays', width: 10 },
{ header: '租赁占用天数', key: 'rentalDays', width: 14 },
{ header: '排课占用天数', key: 'scheduleDays', width: 14 },
{ header: '空闲天数', key: 'idleDays', width: 10 },
{ header: '占用率', key: 'occupancyRate', width: 10 },
];
ws.getRow(1).font = { bold: true };
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
for (const row of data) {
ws.addRow({ ...row, occupancyRate: `${row.occupancyRate}%` });
}
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename=classroom-report.xlsx');
await workbook.xlsx.write(res);
res.end();
}
}

View File

@@ -7,6 +7,7 @@ import { ClassSchedule } from '../entities/class-schedule.entity';
import { AttendanceDevice } from '../entities/attendance-device.entity';
import { CreateClassroomDto, UpdateClassroomDto } from './dto/classroom.dto';
import dayjs from '../common/dayjs';
import { escapeLike } from '../common/like-escape';
/** getRawMany 原始行:驱动可能返回 string/number,date 列可能是 string 或 Date */
interface ScheduleUsageRawRow {
@@ -72,7 +73,7 @@ export class ClassroomsService {
> {
const where: Record<string, unknown> = { status: Not('archived') };
if (query?.building) where.building = query.building;
if (query?.keyword) where.name = Like(`%${query.keyword}%`);
if (query?.keyword) where.name = Like(`%${escapeLike(query.keyword)}%`);
const list = await this.repo.find({
where,
order: { building: 'ASC', name: 'ASC' },