Files
gongxue-base/apps/server/src/occupancies/occupancies.controller.ts

355 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { 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';
@UseGuards(JwtAuthGuard)
@Controller('occupancies')
export class OccupanciesController {
constructor(
private service: OccupanciesService,
private logService: OperationLogsService,
private readonly notificationsService: NotificationsService,
) {}
@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,
});
// TODO: Send notification for check_in — studentId→userId mapping unavailable
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,
});
// TODO: Send notification for check_out — studentId→userId mapping unavailable
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;
}
}