import { Controller, Get, Post, Put, Delete, Param, Body, Query, UseGuards, Request, Res, UseInterceptors, UploadedFile, BadRequestException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Student } from '../entities/student.entity'; import { FileInterceptor } from '@nestjs/platform-express'; import type { Response } from 'express'; import { OccupanciesService } from './occupancies.service'; import { NotificationsService } from '../notifications/notifications.service'; import { NotificationType } from '../entities/notification.entity'; import { CheckInDto, CheckOutDto, TransferRoomDto, BatchCheckOutDto } from './dto/occupancy.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'; import * as ExcelJS from 'exceljs'; import { createOccupancyImportTemplateWorkbook, parseOccupancyImportWorksheet, } from './occupancy-import-template'; @UseGuards(JwtAuthGuard) @Controller('occupancies') export class OccupanciesController { constructor( private service: OccupanciesService, private logService: OperationLogsService, private readonly notificationsService: NotificationsService, @InjectRepository(Student) private studentRepo: Repository, ) {} @Get() @RequirePermission('occupancy:view') findAll( @Query('roomId') roomId?: string, @Query('studentId') studentId?: string, @Query('active') active?: string, ) { return this.service.findAll({ roomId: roomId ? +roomId : undefined, studentId: studentId ? +studentId : undefined, active: active === 'true', }); } @Post('batch-check-out') @RequirePermission('occupancy:checkout') async batchCheckOut(@Body() dto: BatchCheckOutDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.batchCheckOut(dto); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '入住管理', action: '批量退宿', detail: `退宿 ${dto.ids.length} 人,日期 ${dto.checkOutDate}`, ipAddress, userAgent, }); return result; } @Post('check-in') @RequirePermission('occupancy:checkin') async checkIn(@Body() dto: CheckInDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.checkIn(dto, req.user?.id); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '入住管理', action: '办理入住', targetId: result.id, targetType: 'occupancy', detail: `学生${dto.studentId} 入住房间${dto.roomId}`, ipAddress, userAgent, }); // Send check_in notification try { const student = await this.studentRepo.findOne({ where: { id: dto.studentId } }); if (student?.userId) { void this.notificationsService.create({ recipientIds: [student.userId], type: NotificationType.CHECK_IN, title: '入住通知', content: `您已入住房间 #${dto.roomId}`, }); } } catch (_) { /* don't block response */ } return result; } @Put(':id/check-out') @RequirePermission('occupancy:checkout') async checkOut(@Param('id') id: string, @Body() dto: CheckOutDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.checkOut(+id, dto); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '入住管理', action: '办理退宿', targetId: +id, targetType: 'occupancy', ipAddress, userAgent, }); // Send check_out notification try { const student = await this.studentRepo.findOne({ where: { id: result.studentId } }); if (student?.userId) { void this.notificationsService.create({ recipientIds: [student.userId], type: NotificationType.CHECK_OUT, title: '退宿通知', content: `您已退宿房间 #${result.roomId}`, }); } } catch (_) { /* don't block response */ } return result; } @Put(':id/transfer') @RequirePermission('occupancy:transfer') async transferRoom(@Param('id') id: string, @Body() dto: TransferRoomDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.transferRoom(+id, dto); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '入住管理', action: '调换宿舍', targetId: +id, targetType: 'occupancy', detail: `换到房间${dto.newRoomId}`, ipAddress, userAgent, }); return result; } @Delete(':id') @RequirePermission('occupancy: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: 'occupancy', ipAddress, userAgent, }); return result; } @Post('batch-delete') @RequirePermission('occupancy:delete') async batchRemove(@Body() body: { ids: number[] }, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.batchRemove(body.ids || []); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '入住管理', action: '批量归档入住记录', detail: `IDs: ${(body.ids || []).join(',')}`, ipAddress, userAgent, }); return result; } @Get('export') @RequirePermission('occupancy:view') async exportExcel(@Query('active') active?: string, @Res() res?: Response) { const records = await this.service.findAll({ active: active === 'true' }); const workbook = new ExcelJS.Workbook(); const ws = workbook.addWorksheet('入住记录'); ws.columns = [ { header: '宿舍号', key: 'roomNumber', width: 12 }, { header: '楼栋', key: 'building', width: 12 }, { header: '床位号', key: 'bedNumber', width: 10 }, { header: '柜子号', key: 'lockerNumber', width: 10 }, { header: '学生姓名', key: 'studentName', width: 12 }, { header: '性别', key: 'gender', width: 8 }, { header: '电话', key: 'phone', width: 18 }, { header: '学号', key: 'studentNo', width: 15 }, { header: '身份证号', key: 'idNumber', width: 22 }, { header: '负责人/班主任', key: 'supervisor', width: 15 }, { header: '入住日期', key: 'checkInDate', width: 14 }, { header: '退宿日期', key: 'checkOutDate', width: 14 }, { header: '计费起始', key: 'billingStartDate', width: 14 }, { header: '计费截止', key: 'billingEndDate', width: 14 }, { header: '入住类型', key: 'stayType', width: 10 }, { header: '退宿原因', key: 'checkOutReason', width: 16 }, { header: '备注', key: 'notes', width: 24 }, ]; ws.getRow(1).font = { bold: true }; ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } }; for (const r of records) { ws.addRow({ roomNumber: r.room?.roomNumber || '', building: r.room?.building || '', bedNumber: r.bed?.bedNumber || '', lockerNumber: r.locker?.lockerNumber || '', studentName: r.student?.name || '', gender: r.student?.gender || '', phone: r.student?.phone || '', studentNo: r.student?.studentNo || '', idNumber: r.student?.idNumber || '', supervisor: r.student?.supervisor || '', checkInDate: r.checkInDate || '', checkOutDate: r.checkOutDate || '', billingStartDate: r.billingStartDate || '', billingEndDate: r.billingEndDate || '', stayType: r.stayType === 'long' ? '长租' : '短租', checkOutReason: r.checkOutReason || '', notes: r.notes || '', }); } res!.setHeader( 'Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ); res!.setHeader('Content-Disposition', 'attachment; filename=occupancies.xlsx'); await workbook.xlsx.write(res!); res!.end(); } @Get('template') @RequirePermission('occupancy:view') async downloadTemplate(@Res() res: Response) { const workbook = createOccupancyImportTemplateWorkbook(); res.setHeader( 'Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ); res.setHeader('Content-Disposition', 'attachment; filename=checkin_template.xlsx'); await workbook.xlsx.write(res); res.end(); } @Post('import') @RequirePermission('occupancy:checkin') @UseInterceptors(FileInterceptor('file')) async importCheckIn( @UploadedFile() file: Express.Multer.File, @Request() req: any, @Query('autoDeposit') autoDeposit?: string, @Query('depositAmount') depositAmount?: string, ) { const { ipAddress, userAgent } = extractRequestInfo(req); if (!file?.buffer) throw new BadRequestException('请上传入住名单 Excel 文件'); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(file.buffer as any); const ws = workbook.worksheets[0]; const rows = parseOccupancyImportWorksheet(ws); const result = await this.service.batchImportCheckIn(rows, { autoDeposit: autoDeposit === 'true', depositAmount: depositAmount?.trim() ? Number(depositAmount) : undefined, }); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '入住管理', action: '批量导入入住', detail: result.message, ipAddress, userAgent, }); return result; } }