From db11a57e98a45d99b809b38021e822972ddf9981 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sun, 9 Aug 2026 21:30:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(security):=20classrooms=20=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E9=99=90=E5=88=B6/=E8=B7=AF=E7=94=B1=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F/LIKE=20=E8=BD=AC=E4=B9=89=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 由 OCR(open-codereview.ai,deepseek-v4-flash)审查驱动修复: - classrooms.controller:report 路由移到 :id 之前、上传 10MB + 判空 - classrooms.service:搜索关键词 LIKE 通配符转义 Reviewed-by: OCR (open-codereview.ai) --- .../src/classrooms/classrooms.controller.ts | 68 ++++++++++--------- .../src/classrooms/classrooms.service.ts | 3 +- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/apps/server/src/classrooms/classrooms.controller.ts b/apps/server/src/classrooms/classrooms.controller.ts index 3dbfa05..0c4a932 100644 --- a/apps/server/src/classrooms/classrooms.controller.ts +++ b/apps/server/src/classrooms/classrooms.controller.ts @@ -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(); - } } diff --git a/apps/server/src/classrooms/classrooms.service.ts b/apps/server/src/classrooms/classrooms.service.ts index 215062a..dd39e56 100644 --- a/apps/server/src/classrooms/classrooms.service.ts +++ b/apps/server/src/classrooms/classrooms.service.ts @@ -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 = { 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' },