feat: improve occupancy import template

This commit is contained in:
2026-07-14 11:20:27 +08:00
parent 811e7ce826
commit 5cf6aede1e
8 changed files with 415 additions and 110 deletions

View File

@@ -27,6 +27,10 @@ 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')
@@ -240,68 +244,7 @@ export class OccupanciesController {
@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.getRow(1).font = { bold: true, size: 14 };
const workbook = createOccupancyImportTemplateWorkbook();
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
@@ -324,47 +267,7 @@ export class OccupanciesController {
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 rows = parseOccupancyImportWorksheet(ws);
const result = await this.service.batchImportCheckIn(rows, {
autoDeposit: autoDeposit === 'true',
depositAmount: depositAmount ? +depositAmount : undefined,