import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request, Res, UseInterceptors, UploadedFile, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import type { Response } from 'express'; import { RoomsService } from './rooms.service'; import { CreateRoomDto, UpdateRoomDto } from './dto/room.dto'; import { CreateBedDto, UpdateBedDto, BatchCreateBedDto } from './dto/bed.dto'; import { CreateLockerDto, UpdateLockerDto, BatchCreateLockerDto } from './dto/locker.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('rooms') export class RoomsController { constructor( private service: RoomsService, private logService: OperationLogsService, ) {} @Get() @RequirePermission('room:view') findAll( @Query('building') building?: string, @Query('includeArchived') includeArchived?: string, ) { return this.service.findAll({ building, includeArchived: includeArchived === 'true' }); } @Get('overview') @RequirePermission('room:view') getOverview(@Query('includeArchived') includeArchived?: string) { return this.service.getRoomOverview({ includeArchived: includeArchived === 'true' }); } @Get('visual') @RequirePermission('room:view') getVisual(@Query('asOf') asOf?: string) { return this.service.getRoomVisual(asOf); } @Get('template') @RequirePermission('room:view') async downloadTemplate(@Res() res: Response) { const workbook = new ExcelJS.Workbook(); const ws = workbook.addWorksheet('宿舍导入模板'); ws.columns = [ { header: '房间号', key: 'roomNumber', width: 12 }, { header: '楼栋', key: 'building', width: 12 }, { header: '楼层', key: 'floor', width: 8 }, { header: '额定人数', key: 'capacity', width: 10 }, { header: '宿舍类型', key: 'roomType', width: 12 }, { header: '租赁类型(long/short)', key: 'rentalCategory', width: 18 }, { header: '月租金', key: 'monthlyRate', width: 10 }, ]; ws.addRow({ roomNumber: '4-102', building: '4号楼', floor: 1, capacity: 4, roomType: '四人间', rentalCategory: 'long', monthlyRate: 800, }); ws.addRow({ roomNumber: '2-201', building: '2号楼', floor: 2, capacity: 1, roomType: '单人间', rentalCategory: 'short', monthlyRate: 0, }); res.setHeader( 'Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ); res.setHeader('Content-Disposition', 'attachment; filename=room_template.xlsx'); await workbook.xlsx.write(res); res.end(); } @Get('export') @RequirePermission('room:view') async exportExcel(@Query('includeArchived') includeArchived?: string, @Res() res?: Response) { const rooms = await this.service.getRoomOverview({ includeArchived: includeArchived === 'true', }); const workbook = new ExcelJS.Workbook(); const ws = workbook.addWorksheet('宿舍列表'); ws.columns = [ { header: '房间号', key: 'roomNumber', width: 12 }, { header: '楼栋', key: 'building', width: 12 }, { header: '楼层', key: 'floor', width: 8 }, { header: '宿舍类型', key: 'roomType', width: 12 }, { header: '额定人数', key: 'capacity', width: 10 }, { header: '当前入住', key: 'currentCount', width: 10 }, { header: '状态', key: 'status', width: 10 }, { header: '租赁类型', key: 'rentalCategory', width: 12 }, { header: '月租金', key: 'monthlyRate', width: 10 }, ]; ws.getRow(1).font = { bold: true }; ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } }; const statusMap: Record = { available: '可入住', full: '已满', maintenance: '维修中', archived: '已归档', }; for (const r of rooms) { ws.addRow({ roomNumber: r.roomNumber, building: r.building || '', floor: r.floor || '', roomType: r.roomType || '', capacity: r.capacity, currentCount: r.currentCount, status: statusMap[r.status] || r.status, rentalCategory: r.rentalCategory === 'long' ? '长租' : '短租', monthlyRate: r.monthlyRate ?? '', }); } res!.setHeader( 'Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ); res!.setHeader('Content-Disposition', 'attachment; filename=rooms.xlsx'); await workbook.xlsx.write(res!); res!.end(); } // ── 床位管理 ── @Get(':roomId/beds') @RequirePermission('room:view') getBeds(@Param('roomId') roomId: string) { return this.service.getRoomBeds(+roomId); } @Get(':roomId/beds/available') @RequirePermission('room:view') getAvailableBeds(@Param('roomId') roomId: string) { return this.service.getRoomAvailableBeds(+roomId); } @Post(':roomId/beds') @RequirePermission('room:edit') createBed(@Param('roomId') roomId: string, @Body() dto: CreateBedDto) { return this.service.createBed(+roomId, dto); } @Put(':roomId/beds/:id') @RequirePermission('room:edit') updateBed(@Param('roomId') roomId: string, @Param('id') id: string, @Body() dto: UpdateBedDto) { return this.service.updateBed(+roomId, +id, dto); } @Delete(':roomId/beds/:id') @RequirePermission('room:edit') deleteBed(@Param('roomId') roomId: string, @Param('id') id: string) { return this.service.deleteBed(+roomId, +id); } @Post(':roomId/beds/batch') @RequirePermission('room:edit') batchCreateBeds(@Param('roomId') roomId: string, @Body() dto: BatchCreateBedDto) { return this.service.batchCreateBeds(+roomId, dto); } // ── 柜子管理 ── @Get(':roomId/lockers') @RequirePermission('room:view') getLockers(@Param('roomId') roomId: string) { return this.service.getRoomLockers(+roomId); } @Get(':roomId/lockers/available') @RequirePermission('room:view') getAvailableLockers(@Param('roomId') roomId: string) { return this.service.getRoomAvailableLockers(+roomId); } @Post(':roomId/lockers') @RequirePermission('room:edit') createLocker(@Param('roomId') roomId: string, @Body() dto: CreateLockerDto) { return this.service.createLocker(+roomId, dto); } @Put(':roomId/lockers/:id') @RequirePermission('room:edit') updateLocker( @Param('roomId') roomId: string, @Param('id') id: string, @Body() dto: UpdateLockerDto, ) { return this.service.updateLocker(+roomId, +id, dto); } @Delete(':roomId/lockers/:id') @RequirePermission('room:edit') deleteLocker(@Param('roomId') roomId: string, @Param('id') id: string) { return this.service.deleteLocker(+roomId, +id); } @Post(':roomId/lockers/batch') @RequirePermission('room:edit') batchCreateLockers(@Param('roomId') roomId: string, @Body() dto: BatchCreateLockerDto) { return this.service.batchCreateLockers(+roomId, dto); } @Get(':id') @RequirePermission('room:view') findOne(@Param('id') id: string) { return this.service.findOneWithOccupants(+id); } @Post() @RequirePermission('room:create') async create(@Body() dto: CreateRoomDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.create(dto); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '宿舍', action: '添加宿舍', detail: `房间号: ${dto.roomNumber}, 楼栋: ${dto.building || '无'}, 额定: ${dto.capacity}人`, ipAddress, userAgent, }); return result; } @Put(':id') @RequirePermission('room:edit') async update(@Param('id') id: string, @Body() dto: UpdateRoomDto, @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: 'room', detail: JSON.stringify(dto), ipAddress, userAgent, }); return result; } @Delete(':id') @RequirePermission('room: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: 'room', ipAddress, userAgent, }); return result; } @Post('batch-delete') @RequirePermission('room: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; } @Put(':id/restore') @RequirePermission('room:edit') async restore(@Param('id') id: string, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.restore(+id); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '宿舍', action: '恢复宿舍', targetId: +id, targetType: 'room', ipAddress, userAgent, }); return result; } @Post('import') @RequirePermission('room:create') @UseInterceptors(FileInterceptor('file')) async importExcel(@UploadedFile() file: Express.Multer.File, @Request() req: any) { 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: { roomNumber: string; building?: string; floor?: number; capacity?: number; roomType?: string; rentalCategory?: string; monthlyRate?: number; }[] = []; ws.eachRow((row, idx) => { if (idx === 1) return; const rentalCategoryRaw = String(row.getCell(6).value || '') .trim() .toLowerCase(); const rentalCategory = rentalCategoryRaw === 'long' || rentalCategoryRaw === 'short' ? rentalCategoryRaw : undefined; const monthlyRateRaw = Number(row.getCell(7).value); const monthlyRate = isNaN(monthlyRateRaw) ? undefined : monthlyRateRaw; rows.push({ roomNumber: String(row.getCell(1).value || ''), building: String(row.getCell(2).value || '') || undefined, floor: (n => Number.isNaN(n) ? undefined : n)(Number(row.getCell(3).value)), capacity: Number(row.getCell(4).value) || 4, roomType: String(row.getCell(5).value || '').trim() || undefined, rentalCategory, monthlyRate, }); }); const result = await this.service.batchImport(rows); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '宿舍', action: '批量导入', detail: result.message, ipAddress, userAgent, }); return result; } }