import { Controller, Get, Post, Put, Delete, Param, Body, Query, UseGuards, Request, Res, UseInterceptors, UploadedFile, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import type { Response } from 'express'; import { OccupanciesService } from './occupancies.service'; 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'; @UseGuards(JwtAuthGuard) @Controller('occupancies') export class OccupanciesController { constructor( private service: OccupanciesService, private logService: OperationLogsService, ) {} @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); 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, }); 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, }); 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:view') 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:view') 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: 'studentName', width: 12 }, { header: '性别', key: 'gender', width: 8 }, { header: '电话', key: 'phone', width: 18 }, { header: '学号/身份证', key: 'idNumber', width: 22 }, { header: '所属机构', key: 'organization', width: 18 }, { 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: 'checkOutReason', width: 12 }, ]; 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 || '', studentName: r.student?.name || '', gender: r.student?.gender || '', phone: r.student?.phone || '', idNumber: r.student?.idNumber || '', organization: r.student?.organization || '', supervisor: r.student?.supervisor || '', checkInDate: r.checkInDate || '', checkOutDate: r.checkOutDate || '', billingStartDate: r.billingStartDate || '', billingEndDate: r.billingEndDate || '', checkOutReason: r.checkOutReason || '', }); } 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 = new ExcelJS.Workbook(); const ws = workbook.addWorksheet('入住名单导入模板'); ws.columns = [ { header: '宿舍号', key: 'roomNumber', width: 12 }, { header: '床位号', key: 'bedNumber', width: 8 }, { header: '姓名', key: 'name', width: 12 }, { header: '性别', key: 'gender', width: 8 }, { header: '民族', key: 'ethnicity', width: 10 }, { header: '电话', key: 'phone', width: 15 }, { header: '学号/身份证', key: 'idNumber', width: 22 }, { header: '入住时间', key: 'checkInDate', width: 14 }, { header: '离宿时间', key: 'checkOutDate', width: 14 }, { header: '紧急联系人', key: 'emergencyContact', width: 15 }, { header: '紧急联系人电话', key: 'emergencyPhone', width: 18 }, { header: '所属机构', key: 'organization', width: 18 }, { header: '负责人/班主任', key: 'supervisor', width: 15 }, ]; ws.getRow(1).font = { bold: true }; ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } }; // 添加说明行 ws.addRow({ roomNumber: '4-102', bedNumber: 1, name: '张三', gender: '男', ethnicity: '汉族', phone: '13800138000', idNumber: '2024001', checkInDate: '2026-04-21', checkOutDate: '', emergencyContact: '张父', emergencyPhone: '13900000000', organization: '', supervisor: '', }); ws.addRow({ roomNumber: '4-102', bedNumber: 2, name: '李四', gender: '男', ethnicity: '汉族', phone: '13800138001', idNumber: '2024002', checkInDate: '2026-04-21', checkOutDate: '', emergencyContact: '', emergencyPhone: '', organization: 'XXX教育科技', supervisor: '王老师', }); // 添加使用说明sheet const helpWs = workbook.addWorksheet('使用说明'); helpWs.getColumn(1).width = 60; helpWs.addRow(['【入住名单导入说明】']); helpWs.addRow(['1. 导入入住名单会自动创建不存在的学生和宿舍,无需单独导入学生或宿舍']); helpWs.addRow(['2. 宿舍号会智能解析楼栋、楼层和房间类型(如4-102自动识别为4号楼1层四人间)']); helpWs.addRow(['3. 同一宿舍号的多个学生可合并宿舍号单元格,系统会自动继承上一行的宿舍号']); helpWs.addRow(['4. 已存在的学生(按姓名匹配)会自动补充缺失信息(性别、民族等)']); helpWs.addRow(['5. 已有在住记录的学生会自动跳过,不会重复入住']); helpWs.addRow(['6. 填了离宿时间的记录会直接标记为已退宿(用于导入历史数据)']); helpWs.addRow(['7. 性别约束:同一宿舍只能住同性别学生,首位入住者确定宿舍性别']); helpWs.addRow(['8. 床位号仅做标识参考,不影响入住逻辑']); helpWs.getRow(1).font = { bold: true, size: 14 }; 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); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(file.buffer as any); const ws = workbook.worksheets[0]; const rows: any[] = []; let lastRoomNumber = ''; ws.eachRow((row, idx) => { if (idx === 1) return; // 跳过表头 // 宿舍号可能是合并单元格,需要继承上一行 const roomNumberVal = row.getCell(1).value; const roomNumber = roomNumberVal ? String(roomNumberVal).trim() : ''; if (roomNumber) lastRoomNumber = roomNumber; const name = String(row.getCell(3).value || '').trim(); if (!name) return; // 无姓名则跳过空行 // 解析日期 const parseDate = (cell: any): string => { const val = cell.value; if (!val) return ''; if (val instanceof Date) return val.toISOString().split('T')[0]; const s = String(val).trim(); // 处理 "YYYY/MM/DD" 或 "YYYY-MM-DD" 或 "YYYY.MM.DD" const m = s.match(/(\d{4})[\/\-\.](\d{1,2})[\/\-\.](\d{1,2})/); if (m) return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}`; return s; }; rows.push({ name, roomNumber: lastRoomNumber, gender: String(row.getCell(4).value || '').trim() || undefined, ethnicity: String(row.getCell(5).value || '').trim() || undefined, phone: String(row.getCell(6).value || '').trim() || undefined, idNumber: String(row.getCell(7).value || '').trim() || undefined, checkInDate: parseDate(row.getCell(8)), checkOutDate: parseDate(row.getCell(9)) || undefined, emergencyContact: String(row.getCell(10).value || '').trim() || undefined, emergencyPhone: String(row.getCell(11).value || '').trim() || undefined, organization: String(row.getCell(12).value || '').trim() || undefined, supervisor: String(row.getCell(13).value || '').trim() || undefined, }); }); const result = await this.service.batchImportCheckIn(rows, { autoDeposit: autoDeposit === 'true', depositAmount: depositAmount ? +depositAmount : undefined, }); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '入住管理', action: '批量导入入住', detail: result.message, ipAddress, userAgent, }); return result; } }