chore: commit oxfmt formatting changes and verify artifacts

This commit is contained in:
2026-07-02 15:55:09 +08:00
parent 34726cc46d
commit 4f4cee157a
77 changed files with 5576 additions and 1400 deletions

View File

@@ -1,4 +1,18 @@
import { Controller, Get, Post, Put, Delete, Param, Body, Query, UseGuards, Request, Res, UseInterceptors, UploadedFile } from '@nestjs/common';
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';
@@ -12,7 +26,10 @@ import * as ExcelJS from 'exceljs';
@UseGuards(JwtAuthGuard)
@Controller('occupancies')
export class OccupanciesController {
constructor(private service: OccupanciesService, private logService: OperationLogsService) {}
constructor(
private service: OccupanciesService,
private logService: OperationLogsService,
) {}
@Get()
@RequirePermission('occupancy:view')
@@ -33,7 +50,15 @@ export class OccupanciesController {
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 });
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '入住',
action: '批量退宿',
detail: `退宿 ${dto.ids.length} 人,日期 ${dto.checkOutDate}`,
ipAddress,
userAgent,
});
return result;
}
@@ -42,7 +67,17 @@ export class OccupanciesController {
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 });
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;
}
@@ -51,7 +86,16 @@ export class OccupanciesController {
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 });
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '入住',
action: '退宿',
targetId: +id,
targetType: 'occupancy',
ipAddress,
userAgent,
});
return result;
}
@@ -60,7 +104,17 @@ export class OccupanciesController {
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 });
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;
}
@@ -69,7 +123,16 @@ export class OccupanciesController {
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 });
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '入住',
action: '删除入住记录',
targetId: +id,
targetType: 'occupancy',
ipAddress,
userAgent,
});
return result;
}
@@ -78,7 +141,15 @@ export class OccupanciesController {
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;
}
@@ -122,7 +193,10 @@ export class OccupanciesController {
checkOutReason: r.checkOutReason || '',
});
}
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=occupancies.xlsx');
await workbook.xlsx.write(res!);
res!.end();
@@ -151,8 +225,36 @@ export class OccupanciesController {
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: '王老师' });
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;
@@ -166,7 +268,10 @@ export class OccupanciesController {
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-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
res.setHeader('Content-Disposition', 'attachment; filename=checkin_template.xlsx');
await workbook.xlsx.write(res);
res.end();
@@ -175,7 +280,12 @@ export class OccupanciesController {
@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) {
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);
@@ -225,7 +335,15 @@ export class OccupanciesController {
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 });
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '入住',
action: '批量导入入住',
detail: result.message,
ipAddress,
userAgent,
});
return result;
}
}

View File

@@ -1,6 +1,14 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, IsNull, Between, LessThanOrEqual, MoreThanOrEqual, In } from 'typeorm';
import {
Repository,
DataSource,
IsNull,
Between,
LessThanOrEqual,
MoreThanOrEqual,
In,
} from 'typeorm';
import { Occupancy } from '../entities/occupancy.entity';
import { Room } from '../entities/room.entity';
import { Student } from '../entities/student.entity';
@@ -19,7 +27,8 @@ export class OccupanciesService {
) {}
async findAll(query?: { roomId?: number; studentId?: number; active?: boolean }) {
const qb = this.repo.createQueryBuilder('o')
const qb = this.repo
.createQueryBuilder('o')
.leftJoinAndSelect('o.student', 'student')
.leftJoinAndSelect('o.room', 'room')
.orderBy('o.checkInDate', 'DESC');
@@ -31,7 +40,9 @@ export class OccupanciesService {
async checkIn(dto: CheckInDto) {
// 检查学生是否已有活跃入住
const existing = await this.repo.findOne({ where: { studentId: dto.studentId, checkOutDate: IsNull() } });
const existing = await this.repo.findOne({
where: { studentId: dto.studentId, checkOutDate: IsNull() },
});
if (existing) throw new BadRequestException('该学生已有在住记录,请先办理退宿');
// 检查宿舍容量
@@ -44,7 +55,9 @@ export class OccupanciesService {
const student = await this.studentRepo.findOne({ where: { id: dto.studentId } });
if (!student) throw new NotFoundException('学生不存在');
if (student.gender && room.gender && student.gender !== room.gender) {
throw new BadRequestException(`该宿舍当前为${room.gender}生寝室,${student.gender}生无法入住`);
throw new BadRequestException(
`该宿舍当前为${room.gender}生寝室,${student.gender}生无法入住`,
);
}
const occ = this.repo.create({
@@ -82,7 +95,9 @@ export class OccupanciesService {
await this.roomRepo.update(occ.roomId, { status: 'available' });
// 如果房间已无在住人员,重置房间性别
const remaining = await this.repo.count({ where: { roomId: occ.roomId, checkOutDate: IsNull() } });
const remaining = await this.repo.count({
where: { roomId: occ.roomId, checkOutDate: IsNull() },
});
if (remaining === 0) {
await this.roomRepo.update(occ.roomId, { gender: null as any });
}
@@ -106,7 +121,9 @@ export class OccupanciesService {
await runner.manager.save(oldOcc);
await runner.manager.update(Room, oldOcc.roomId, { status: 'available' });
// 旧房如果已无在住人员,重置性别
const oldRemaining = await runner.manager.count(Occupancy, { where: { roomId: oldOcc.roomId, checkOutDate: IsNull() } });
const oldRemaining = await runner.manager.count(Occupancy, {
where: { roomId: oldOcc.roomId, checkOutDate: IsNull() },
});
if (oldRemaining === 0) {
await runner.manager.update(Room, oldOcc.roomId, { gender: null as any });
}
@@ -114,7 +131,9 @@ export class OccupanciesService {
// 检查新房容量
const newRoom = await runner.manager.findOne(Room, { where: { id: dto.newRoomId } });
if (!newRoom) throw new NotFoundException('目标宿舍不存在');
const count = await runner.manager.count(Occupancy, { where: { roomId: dto.newRoomId, checkOutDate: IsNull() } });
const count = await runner.manager.count(Occupancy, {
where: { roomId: dto.newRoomId, checkOutDate: IsNull() },
});
if (count >= newRoom.capacity) throw new BadRequestException('目标宿舍已满');
// 换房性别约束检查
@@ -160,7 +179,8 @@ export class OccupanciesService {
// 获取某宿舍在指定时间段内的入住记录(用于计费)
async getRoomOccupanciesInPeriod(roomId: number, periodStart: string, periodEnd: string) {
return this.repo.createQueryBuilder('o')
return this.repo
.createQueryBuilder('o')
.leftJoinAndSelect('o.student', 'student')
.where('o.roomId = :roomId', { roomId })
.andWhere('o.billingStartDate <= :periodEnd', { periodEnd })
@@ -190,19 +210,26 @@ export class OccupanciesService {
}
let deleted = 0;
if (deletableIds.length > 0) {
const result = await this.repo.createQueryBuilder()
const result = await this.repo
.createQueryBuilder()
.delete()
.where('id IN (:...ids)', { ids: deletableIds })
.execute();
deleted = result.affected || 0;
}
const message = skipped.length > 0
? `成功删除 ${deleted} 条;${skipped.length} 条在住记录被跳过(${skipped.slice(0, 5).join('、')}${skipped.length > 5 ? '…' : ''}),请先办理退宿`
: `批量删除成功,共 ${deleted}`;
const message =
skipped.length > 0
? `成功删除 ${deleted}${skipped.length} 条在住记录被跳过(${skipped.slice(0, 5).join('、')}${skipped.length > 5 ? '…' : ''}),请先办理退宿`
: `批量删除成功,共 ${deleted}`;
return { message, deleted, skipped: skipped.length };
}
async batchCheckOut(dto: { ids: number[]; checkOutDate: string; billingEndDate?: string; checkOutReason?: string }) {
async batchCheckOut(dto: {
ids: number[];
checkOutDate: string;
billingEndDate?: string;
checkOutReason?: string;
}) {
if (!dto.ids || dto.ids.length === 0) {
throw new BadRequestException('请选择要退宿的记录');
}
@@ -213,9 +240,18 @@ export class OccupanciesService {
const errors: string[] = [];
try {
for (const id of dto.ids) {
const occ = await runner.manager.findOne(Occupancy, { where: { id }, relations: ['student'] });
if (!occ) { errors.push(`记录${id}不存在`); continue; }
if (occ.checkOutDate) { errors.push(`${occ.student?.name || id}已退宿`); continue; }
const occ = await runner.manager.findOne(Occupancy, {
where: { id },
relations: ['student'],
});
if (!occ) {
errors.push(`记录${id}不存在`);
continue;
}
if (occ.checkOutDate) {
errors.push(`${occ.student?.name || id}已退宿`);
continue;
}
occ.checkOutDate = dto.checkOutDate;
occ.billingEndDate = dto.billingEndDate || dto.checkOutDate;
occ.checkOutReason = dto.checkOutReason || '';
@@ -223,7 +259,9 @@ export class OccupanciesService {
// 更新房间状态
await runner.manager.update(Room, occ.roomId, { status: 'available' });
// 如果房间已无在住人员,重置性别
const remaining = await runner.manager.count(Occupancy, { where: { roomId: occ.roomId, checkOutDate: IsNull() } });
const remaining = await runner.manager.count(Occupancy, {
where: { roomId: occ.roomId, checkOutDate: IsNull() },
});
if (remaining === 0) {
await runner.manager.update(Room, occ.roomId, { gender: null as any });
}
@@ -236,7 +274,12 @@ export class OccupanciesService {
} finally {
await runner.release();
}
return { success, failed: errors.length, message: `已成功退宿 ${success}${errors.length > 0 ? `${errors.length} 条失败` : ''}`, errors: errors.length > 0 ? errors : undefined };
return {
success,
failed: errors.length,
message: `已成功退宿 ${success}${errors.length > 0 ? `${errors.length} 条失败` : ''}`,
errors: errors.length > 0 ? errors : undefined,
};
}
/**
@@ -244,13 +287,24 @@ export class OccupanciesService {
* 每行数据:姓名、电话、学号、房间号、楼栋、入住日期
* 自动创建不存在的学生和宿舍,并登记入住
*/
async batchImportCheckIn(rows: {
name: string; phone?: string; idNumber?: string;
gender?: string; ethnicity?: string; emergencyContact?: string; emergencyPhone?: string;
organization?: string; supervisor?: string;
roomNumber: string; building?: string;
checkInDate: string; checkOutDate?: string;
}[], options?: { autoDeposit?: boolean; depositAmount?: number }) {
async batchImportCheckIn(
rows: {
name: string;
phone?: string;
idNumber?: string;
gender?: string;
ethnicity?: string;
emergencyContact?: string;
emergencyPhone?: string;
organization?: string;
supervisor?: string;
roomNumber: string;
building?: string;
checkInDate: string;
checkOutDate?: string;
}[],
options?: { autoDeposit?: boolean; depositAmount?: number },
) {
let imported = 0;
let skipped = 0;
let depositsCreated = 0;
@@ -269,17 +323,19 @@ export class OccupanciesService {
// 1. 查找或创建学生
let student = await this.studentRepo.findOne({ where: { name: row.name.trim() } });
if (!student) {
student = await this.studentRepo.save(this.studentRepo.create({
name: row.name.trim(),
phone: row.phone?.trim() || undefined,
idNumber: row.idNumber?.trim() || undefined,
gender: row.gender?.trim() || undefined,
ethnicity: row.ethnicity?.trim() || undefined,
emergencyContact: row.emergencyContact?.trim() || undefined,
emergencyPhone: row.emergencyPhone?.trim() || undefined,
organization: row.organization?.trim() || undefined,
supervisor: row.supervisor?.trim() || undefined,
}));
student = await this.studentRepo.save(
this.studentRepo.create({
name: row.name.trim(),
phone: row.phone?.trim() || undefined,
idNumber: row.idNumber?.trim() || undefined,
gender: row.gender?.trim() || undefined,
ethnicity: row.ethnicity?.trim() || undefined,
emergencyContact: row.emergencyContact?.trim() || undefined,
emergencyPhone: row.emergencyPhone?.trim() || undefined,
organization: row.organization?.trim() || undefined,
supervisor: row.supervisor?.trim() || undefined,
}),
);
} else {
// 更新已有学生的缺失信息
const updates: any = {};
@@ -287,10 +343,14 @@ export class OccupanciesService {
if (!student.idNumber && row.idNumber?.trim()) updates.idNumber = row.idNumber.trim();
if (!student.gender && row.gender?.trim()) updates.gender = row.gender.trim();
if (!student.ethnicity && row.ethnicity?.trim()) updates.ethnicity = row.ethnicity.trim();
if (!student.emergencyContact && row.emergencyContact?.trim()) updates.emergencyContact = row.emergencyContact.trim();
if (!student.emergencyPhone && row.emergencyPhone?.trim()) updates.emergencyPhone = row.emergencyPhone.trim();
if (!student.organization && row.organization?.trim()) updates.organization = row.organization.trim();
if (!student.supervisor && row.supervisor?.trim()) updates.supervisor = row.supervisor.trim();
if (!student.emergencyContact && row.emergencyContact?.trim())
updates.emergencyContact = row.emergencyContact.trim();
if (!student.emergencyPhone && row.emergencyPhone?.trim())
updates.emergencyPhone = row.emergencyPhone.trim();
if (!student.organization && row.organization?.trim())
updates.organization = row.organization.trim();
if (!student.supervisor && row.supervisor?.trim())
updates.supervisor = row.supervisor.trim();
if (Object.keys(updates).length > 0) {
await this.studentRepo.update(student.id, updates);
Object.assign(student, updates);
@@ -301,19 +361,26 @@ export class OccupanciesService {
let room = await this.roomRepo.findOne({ where: { roomNumber: row.roomNumber.trim() } });
if (!room) {
const parsed = RoomsService.parseRoomNumber(row.roomNumber.trim());
room = await this.roomRepo.save(this.roomRepo.create({
roomNumber: row.roomNumber.trim(),
building: row.building?.trim() || parsed.building || undefined,
floor: parsed.floor || undefined,
capacity: parsed.capacity || 4,
roomType: parsed.roomType || undefined,
}));
room = await this.roomRepo.save(
this.roomRepo.create({
roomNumber: row.roomNumber.trim(),
building: row.building?.trim() || parsed.building || undefined,
floor: parsed.floor || undefined,
capacity: parsed.capacity || 4,
roomType: parsed.roomType || undefined,
}),
);
}
// 3. 检查是否已有活跃入住
const existing = await this.repo.findOne({ where: { studentId: student.id, checkOutDate: IsNull() }, relations: ['room'] });
const existing = await this.repo.findOne({
where: { studentId: student.id, checkOutDate: IsNull() },
relations: ['room'],
});
if (existing) {
errors.push(`${rowNum}行: ${row.name} 已在住(${existing.room?.roomNumber || '房间' + existing.roomId}),跳过`);
errors.push(
`${rowNum}行: ${row.name} 已在住(${existing.room?.roomNumber || '房间' + existing.roomId}),跳过`,
);
skipped++;
continue;
}
@@ -321,14 +388,18 @@ export class OccupanciesService {
// 4. 检查宿舍容量
const count = await this.repo.count({ where: { roomId: room.id, checkOutDate: IsNull() } });
if (count >= room.capacity) {
errors.push(`${rowNum}行: 宿舍 ${row.roomNumber} 已满(${count}/${room.capacity}),跳过 ${row.name}`);
errors.push(
`${rowNum}行: 宿舍 ${row.roomNumber} 已满(${count}/${room.capacity}),跳过 ${row.name}`,
);
skipped++;
continue;
}
// 5. 房间级别性别约束
if (student.gender && room.gender && student.gender !== room.gender) {
errors.push(`${rowNum}行: 宿舍 ${row.roomNumber}${room.gender}生寝室,${row.name}(${student.gender})无法入住,跳过`);
errors.push(
`${rowNum}行: 宿舍 ${row.roomNumber}${room.gender}生寝室,${row.name}(${student.gender})无法入住,跳过`,
);
skipped++;
continue;
}
@@ -361,15 +432,19 @@ export class OccupanciesService {
// 9. 自动收取押金(仅对新入住且非历史记录的学生)
if (options?.autoDeposit && !row.checkOutDate?.trim()) {
const existingDeposit = await this.depositRepo.findOne({ where: { studentId: student.id, status: 'paid' } });
const existingDeposit = await this.depositRepo.findOne({
where: { studentId: student.id, status: 'paid' },
});
if (!existingDeposit) {
await this.depositRepo.save(this.depositRepo.create({
studentId: student.id,
amount: options.depositAmount || 500,
paidDate: checkInDate,
status: 'paid',
notes: '入住导入自动收取',
}));
await this.depositRepo.save(
this.depositRepo.create({
studentId: student.id,
amount: options.depositAmount || 500,
paidDate: checkInDate,
status: 'paid',
notes: '入住导入自动收取',
}),
);
depositsCreated++;
}
}