import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request, Res, UseInterceptors, UploadedFile, BadRequestException, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import type { Response } from 'express'; import * as fs from 'fs'; import { ClassroomRentalsService } from './classroom-rentals.service'; import { CreateRentalDto, UpdateRentalDto } from './dto/rental.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; import { extractRequestInfo } from '../common/request-utils'; import { RequirePermission } from '../auth/decorators/permission.decorator'; @UseGuards(JwtAuthGuard) @Controller('classroom-rentals') export class ClassroomRentalsController { constructor( private service: ClassroomRentalsService, private logService: OperationLogsService, ) {} @Get() @RequirePermission('rental:view') findAll( @Query('classroomId') classroomId?: string, @Query('lesseeOrganizationId') lesseeOrganizationId?: string, @Query('month') month?: string, @Query('includeEnded') includeEnded?: string, ) { return this.service.findAll({ classroomId: classroomId ? +classroomId : undefined, lesseeOrganizationId: lesseeOrganizationId ? +lesseeOrganizationId : undefined, month, includeEnded: includeEnded === 'true', }); } @Get('schedule') @RequirePermission('rental:view') getSchedule(@Query('year') year?: string, @Query('month') month?: string) { const now = new Date(); const y = year ? +year : now.getFullYear(); const m = month ? +month : now.getMonth() + 1; if (m < 1 || m > 12) throw new BadRequestException('月份必须在 1-12 之间'); return this.service.getSchedule(y, m); } @Get('unavailable-dates') @RequirePermission('rental:view') getUnavailableDates( @Query('classroomId') classroomId?: string, @Query('year') year?: string, @Query('month') month?: string, @Query('excludeId') excludeId?: string, ) { const parsedClassroomId = Number(classroomId); const parsedYear = Number(year); const parsedMonth = Number(month); if (!Number.isInteger(parsedClassroomId) || parsedClassroomId <= 0) { throw new BadRequestException('请选择有效教室'); } if (!Number.isInteger(parsedYear) || parsedYear < 2000 || parsedYear > 2100) { throw new BadRequestException('年份不合法'); } if (!Number.isInteger(parsedMonth) || parsedMonth < 1 || parsedMonth > 12) { throw new BadRequestException('月份必须在 1-12 之间'); } const parsedExcludeId = excludeId === undefined ? undefined : Number(excludeId); if ( parsedExcludeId !== undefined && (!Number.isInteger(parsedExcludeId) || parsedExcludeId <= 0) ) { throw new BadRequestException('排除的租赁订单不合法'); } return this.service.getUnavailableDates( parsedClassroomId, parsedYear, parsedMonth, parsedExcludeId, ); } @Get(':id') @RequirePermission('rental:view') findOne(@Param('id') id: string) { return this.service.findOne(+id); } @Post() @RequirePermission('rental:create') async create(@Body() dto: CreateRentalDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.create(dto, req.user?.id); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '教室租赁', action: '新增租赁', targetId: result.id, targetType: 'classroom-rental', detail: `教室${dto.classroomId} 承租机构${dto.lesseeOrganizationId} ${dto.startDate}~${dto.endDate}`, ipAddress, userAgent, }); return result; } @Put(':id') @RequirePermission('rental:edit') async update(@Param('id') id: string, @Body() dto: UpdateRentalDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.update(+id, dto); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '教室租赁', action: '编辑租赁', targetId: +id, targetType: 'classroom-rental', detail: JSON.stringify(dto), ipAddress, userAgent, }); return result; } @Put(':id/cancel') @RequirePermission('rental:edit') async cancel(@Param('id') id: string, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.cancel(+id); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '教室租赁', action: '取消租赁', targetId: +id, targetType: 'classroom-rental', ipAddress, userAgent, }); return result; } @Put(':id/end') @RequirePermission('rental:edit') async end(@Param('id') id: string, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.end(+id); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '教室租赁', action: '结束租赁', targetId: +id, targetType: 'classroom-rental', ipAddress, userAgent, }); return result; } @Delete(':id') @RequirePermission('rental:delete') async remove(@Param('id') id: string, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.remove(+id); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '教室租赁', action: '删除租赁', targetId: +id, targetType: 'classroom-rental', ipAddress, userAgent, }); return result; } // 合同上传:multer 限制 10MB + 仅 PDF @Post(':id/contract') @RequirePermission('rental:edit') @UseInterceptors( FileInterceptor('file', { limits: { fileSize: 10 * 1024 * 1024 }, fileFilter: (_req, file, cb) => { if (file.mimetype !== 'application/pdf') { return cb(new BadRequestException('仅支持 PDF 文件'), false); } cb(null, true); }, }), ) async uploadContract( @Param('id') id: string, @UploadedFile() file: Express.Multer.File, @Request() req: any, ) { if (!file) throw new BadRequestException('请上传合同文件'); const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.attachContract(+id, file); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '教室租赁', action: '上传合同', targetId: +id, targetType: 'classroom-rental', detail: file.originalname, ipAddress, userAgent, }); return result; } @Get(':id/contract') @RequirePermission('rental:view') async downloadContract(@Param('id') id: string, @Res() res: Response) { const { fullPath, originalName } = await this.service.getContractPath(+id); res.setHeader('Content-Type', 'application/pdf'); res.setHeader( 'Content-Disposition', `attachment; filename="${encodeURIComponent(originalName)}"`, ); const stream = fs.createReadStream(fullPath); stream.pipe(res); } @Delete(':id/contract') @RequirePermission('rental:edit') async deleteContract(@Param('id') id: string, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.removeContract(+id); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '教室租赁', action: '删除合同', targetId: +id, targetType: 'classroom-rental', ipAddress, userAgent, }); return result; } }