chore: commit oxfmt formatting changes and verify artifacts
This commit is contained in:
@@ -1,4 +1,18 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request, Res, UseInterceptors, UploadedFile } from '@nestjs/common';
|
||||
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';
|
||||
@@ -12,11 +26,17 @@ import * as ExcelJS from 'exceljs';
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('rooms')
|
||||
export class RoomsController {
|
||||
constructor(private service: RoomsService, private logService: OperationLogsService) {}
|
||||
constructor(
|
||||
private service: RoomsService,
|
||||
private logService: OperationLogsService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermission('room:view')
|
||||
findAll(@Query('building') building?: string, @Query('includeArchived') includeArchived?: string) {
|
||||
findAll(
|
||||
@Query('building') building?: string,
|
||||
@Query('includeArchived') includeArchived?: string,
|
||||
) {
|
||||
return this.service.findAll({ building, includeArchived: includeArchived === 'true' });
|
||||
}
|
||||
|
||||
@@ -46,9 +66,24 @@ export class RoomsController {
|
||||
];
|
||||
ws.getRow(1).font = { bold: true };
|
||||
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
|
||||
ws.addRow({ roomNumber: '4-102', building: '4号楼', floor: 1, capacity: 4, roomType: '四人间' });
|
||||
ws.addRow({ roomNumber: '2-201', building: '2号楼', floor: 2, capacity: 1, roomType: '单人间' });
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
ws.addRow({
|
||||
roomNumber: '4-102',
|
||||
building: '4号楼',
|
||||
floor: 1,
|
||||
capacity: 4,
|
||||
roomType: '四人间',
|
||||
});
|
||||
ws.addRow({
|
||||
roomNumber: '2-201',
|
||||
building: '2号楼',
|
||||
floor: 2,
|
||||
capacity: 1,
|
||||
roomType: '单人间',
|
||||
});
|
||||
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();
|
||||
@@ -57,7 +92,9 @@ export class RoomsController {
|
||||
@Get('export')
|
||||
@RequirePermission('room:view')
|
||||
async exportExcel(@Query('includeArchived') includeArchived?: string, @Res() res?: Response) {
|
||||
const rooms = await this.service.getRoomOverview({ includeArchived: includeArchived === 'true' });
|
||||
const rooms = await this.service.getRoomOverview({
|
||||
includeArchived: includeArchived === 'true',
|
||||
});
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const ws = workbook.addWorksheet('宿舍列表');
|
||||
ws.columns = [
|
||||
@@ -72,11 +109,28 @@ export class RoomsController {
|
||||
];
|
||||
ws.getRow(1).font = { bold: true };
|
||||
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
|
||||
const statusMap: Record<string, string> = { available: '可入住', full: '已满', maintenance: '维修中', archived: '已归档' };
|
||||
const statusMap: Record<string, string> = {
|
||||
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, gender: r.gender || '', status: statusMap[r.status] || r.status });
|
||||
ws.addRow({
|
||||
roomNumber: r.roomNumber,
|
||||
building: r.building || '',
|
||||
floor: r.floor || '',
|
||||
roomType: r.roomType || '',
|
||||
capacity: r.capacity,
|
||||
currentCount: r.currentCount,
|
||||
gender: r.gender || '',
|
||||
status: statusMap[r.status] || r.status,
|
||||
});
|
||||
}
|
||||
res!.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
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();
|
||||
@@ -93,7 +147,15 @@ export class RoomsController {
|
||||
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 });
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -102,7 +164,17 @@ export class RoomsController {
|
||||
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 });
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -111,7 +183,16 @@ export class RoomsController {
|
||||
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 });
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '宿舍',
|
||||
action: '归档宿舍',
|
||||
targetId: +id,
|
||||
targetType: 'room',
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -120,7 +201,15 @@ export class RoomsController {
|
||||
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 });
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '宿舍',
|
||||
action: '批量归档宿舍',
|
||||
detail: `IDs: ${(body.ids || []).join(',')}`,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -129,7 +218,16 @@ export class RoomsController {
|
||||
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 });
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '宿舍',
|
||||
action: '恢复宿舍',
|
||||
targetId: +id,
|
||||
targetType: 'room',
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -141,7 +239,13 @@ export class RoomsController {
|
||||
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 }[] = [];
|
||||
const rows: {
|
||||
roomNumber: string;
|
||||
building?: string;
|
||||
floor?: number;
|
||||
capacity?: number;
|
||||
roomType?: string;
|
||||
}[] = [];
|
||||
ws.eachRow((row, idx) => {
|
||||
if (idx === 1) return;
|
||||
rows.push({
|
||||
@@ -153,7 +257,15 @@ export class RoomsController {
|
||||
});
|
||||
});
|
||||
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 });
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '宿舍',
|
||||
action: '批量导入',
|
||||
detail: result.message,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@ export class RoomsService {
|
||||
* "3-301" → building:"3号楼", floor:3, roomType:"四人间"
|
||||
* "8-102" → building:"8号楼", floor:1, roomType:"爆改房"
|
||||
*/
|
||||
static parseRoomNumber(roomNumber: string): { building?: string; floor?: number; roomType?: string; capacity?: number } {
|
||||
static parseRoomNumber(roomNumber: string): {
|
||||
building?: string;
|
||||
floor?: number;
|
||||
roomType?: string;
|
||||
capacity?: number;
|
||||
} {
|
||||
const cleaned = roomNumber.replace(/[((].*?[))]/g, '').trim();
|
||||
// 家庭房: X-Y-ZZZ 格式
|
||||
const familyMatch = cleaned.match(/^(\d+)-(\d+)-(\d+)$/);
|
||||
@@ -36,12 +41,18 @@ export class RoomsService {
|
||||
if (stdMatch) {
|
||||
const bldgNum = stdMatch[1];
|
||||
const roomPart = stdMatch[2];
|
||||
const floor = roomPart.length >= 3 ? parseInt(roomPart.charAt(0), 10) : parseInt(roomPart.charAt(0), 10);
|
||||
const floor =
|
||||
roomPart.length >= 3 ? parseInt(roomPart.charAt(0), 10) : parseInt(roomPart.charAt(0), 10);
|
||||
const building = `${bldgNum}号楼`;
|
||||
let roomType = '四人间';
|
||||
let capacity = 4;
|
||||
if (bldgNum === '2') { roomType = '单人间'; capacity = 1; }
|
||||
else if (bldgNum === '8') { roomType = '爆改房'; capacity = 2; }
|
||||
if (bldgNum === '2') {
|
||||
roomType = '单人间';
|
||||
capacity = 1;
|
||||
} else if (bldgNum === '8') {
|
||||
roomType = '爆改房';
|
||||
capacity = 2;
|
||||
}
|
||||
return { building, floor, roomType, capacity };
|
||||
}
|
||||
return {};
|
||||
@@ -76,7 +87,9 @@ export class RoomsService {
|
||||
const rooms = await this.repo.find({ where, order: { building: 'ASC', roomNumber: 'ASC' } });
|
||||
const result: any[] = [];
|
||||
for (const room of rooms) {
|
||||
const count = await this.occRepo.count({ where: { roomId: room.id, checkOutDate: IsNull() } });
|
||||
const count = await this.occRepo.count({
|
||||
where: { roomId: room.id, checkOutDate: IsNull() },
|
||||
});
|
||||
result.push({ ...room, currentCount: count });
|
||||
}
|
||||
return result;
|
||||
@@ -113,7 +126,9 @@ export class RoomsService {
|
||||
skipped.push(`${r.roomNumber}(已归档)`);
|
||||
continue;
|
||||
}
|
||||
const activeCount = await this.occRepo.count({ where: { roomId: r.id, checkOutDate: IsNull() } });
|
||||
const activeCount = await this.occRepo.count({
|
||||
where: { roomId: r.id, checkOutDate: IsNull() },
|
||||
});
|
||||
if (activeCount > 0) {
|
||||
skipped.push(`${r.roomNumber}(有在住人员)`);
|
||||
continue;
|
||||
@@ -122,16 +137,18 @@ export class RoomsService {
|
||||
}
|
||||
let affected = 0;
|
||||
if (targetIds.length > 0) {
|
||||
const result = await this.repo.createQueryBuilder()
|
||||
const result = await this.repo
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({ status: 'archived' })
|
||||
.where('id IN (:...ids)', { ids: targetIds })
|
||||
.execute();
|
||||
affected = result.affected || 0;
|
||||
}
|
||||
const message = skipped.length > 0
|
||||
? `成功归档 ${affected} 间;${skipped.length} 间被跳过(${skipped.slice(0, 5).join('、')}${skipped.length > 5 ? '…' : ''})`
|
||||
: `已批量归档 ${affected} 间宿舍(数据已保留,可随时恢复)`;
|
||||
const message =
|
||||
skipped.length > 0
|
||||
? `成功归档 ${affected} 间;${skipped.length} 间被跳过(${skipped.slice(0, 5).join('、')}${skipped.length > 5 ? '…' : ''})`
|
||||
: `已批量归档 ${affected} 间宿舍(数据已保留,可随时恢复)`;
|
||||
return { message, archived: affected, skipped: skipped.length };
|
||||
}
|
||||
|
||||
@@ -143,7 +160,10 @@ export class RoomsService {
|
||||
}
|
||||
|
||||
async getRoomVisual() {
|
||||
const rooms = await this.repo.find({ where: { status: Not('archived') }, order: { building: 'ASC', roomNumber: 'ASC' } });
|
||||
const rooms = await this.repo.find({
|
||||
where: { status: Not('archived') },
|
||||
order: { building: 'ASC', roomNumber: 'ASC' },
|
||||
});
|
||||
const occupancies = await this.occRepo.find({
|
||||
where: { checkOutDate: IsNull() },
|
||||
relations: ['student'],
|
||||
@@ -156,7 +176,10 @@ export class RoomsService {
|
||||
if (!occMap.has(occ.roomId)) occMap.set(occ.roomId, []);
|
||||
const now = new Date();
|
||||
const checkIn = new Date(occ.checkInDate);
|
||||
const days = Math.max(1, Math.ceil((now.getTime() - checkIn.getTime()) / (1000 * 60 * 60 * 24)));
|
||||
const days = Math.max(
|
||||
1,
|
||||
Math.ceil((now.getTime() - checkIn.getTime()) / (1000 * 60 * 60 * 24)),
|
||||
);
|
||||
occMap.get(occ.roomId)!.push({
|
||||
studentId: occ.studentId,
|
||||
studentName: occ.student?.name || '未知',
|
||||
@@ -201,24 +224,44 @@ export class RoomsService {
|
||||
};
|
||||
}
|
||||
|
||||
async batchImport(rows: { roomNumber: string; building?: string; floor?: number; capacity?: number; roomType?: string }[]) {
|
||||
async batchImport(
|
||||
rows: {
|
||||
roomNumber: string;
|
||||
building?: string;
|
||||
floor?: number;
|
||||
capacity?: number;
|
||||
roomType?: string;
|
||||
}[],
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
for (const row of rows) {
|
||||
if (!row.roomNumber || !row.roomNumber.trim()) { skipped++; continue; }
|
||||
if (!row.roomNumber || !row.roomNumber.trim()) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
const exists = await this.repo.findOne({ where: { roomNumber: row.roomNumber.trim() } });
|
||||
if (exists) { skipped++; continue; }
|
||||
if (exists) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
// 智能解析房间号
|
||||
const parsed = RoomsService.parseRoomNumber(row.roomNumber.trim());
|
||||
await this.repo.save(this.repo.create({
|
||||
roomNumber: row.roomNumber.trim(),
|
||||
building: row.building?.trim() || parsed.building || undefined,
|
||||
floor: row.floor || parsed.floor || undefined,
|
||||
capacity: row.capacity || parsed.capacity || 4,
|
||||
roomType: row.roomType || parsed.roomType || undefined,
|
||||
}));
|
||||
await this.repo.save(
|
||||
this.repo.create({
|
||||
roomNumber: row.roomNumber.trim(),
|
||||
building: row.building?.trim() || parsed.building || undefined,
|
||||
floor: row.floor || parsed.floor || undefined,
|
||||
capacity: row.capacity || parsed.capacity || 4,
|
||||
roomType: row.roomType || parsed.roomType || undefined,
|
||||
}),
|
||||
);
|
||||
imported++;
|
||||
}
|
||||
return { message: `成功导入 ${imported} 间宿舍,跳过 ${skipped} 条(重复或空行)`, imported, skipped };
|
||||
return {
|
||||
message: `成功导入 ${imported} 间宿舍,跳过 ${skipped} 条(重复或空行)`,
|
||||
imported,
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user