feat: integrate classroom real-time occupancy with schedule and rental data

This commit is contained in:
2026-07-06 01:14:58 +08:00
parent 3bcb7d41c0
commit 8453e62088
2 changed files with 92 additions and 5 deletions

View File

@@ -22,13 +22,16 @@ export class ClassroomsService {
if (query?.building) where.building = query.building;
if (query?.roomType) where.roomType = query.roomType;
if (!query?.includeArchived) where.status = Not('archived');
return this.repo.find({ where: await this.scope.filter(where), order: { building: 'ASC', name: 'ASC' } });
const list = await this.repo.find({ where: await this.scope.filter(where), order: { building: 'ASC', name: 'ASC' } });
const usageMap = await this.getCurrentUsageForClassrooms(list.map((c) => c.id));
return list.map((c) => ({ ...c, currentUsage: usageMap.get(c.id) ?? null }));
}
async findOne(id: number) {
const cls = await this.repo.findOne({ where: { id } });
if (!cls) throw new NotFoundException('教室不存在');
return cls;
const usageMap = await this.getCurrentUsageForClassrooms([id]);
return { ...cls, currentUsage: usageMap.get(id) ?? null };
}
async create(dto: CreateClassroomDto) {
@@ -47,16 +50,83 @@ export class ClassroomsService {
async remove(id: number) {
await this.findOne(id);
await this.repo.update(id, { status: 'archived' } as any);
await this.repo.update(id, { status: 'archived' });
return { message: '已归档' };
}
async restore(id: number) {
await this.findOne(id);
await this.repo.update(id, { status: 'available' } as any);
await this.repo.update(id, { status: 'available' });
return this.repo.findOne({ where: { id } });
}
private async getCurrentUsageForClassrooms(classroomIds: number[]): Promise<Map<number, { type: 'schedule' | 'rental'; title: string; startTime: string; endTime: string }>> {
const result = new Map<number, { type: 'schedule' | 'rental'; title: string; startTime: string; endTime: string }>();
if (classroomIds.length === 0) return result;
const now = new Date();
const todayStr = now.toISOString().slice(0, 10);
const currentTime = now.toTimeString().slice(0, 5);
const weekDay = now.getDay() || 7;
const schedules = await this.scheduleRepo
.createQueryBuilder('s')
.leftJoin('Class', 'c', 'c.id = s.classId')
.select('s.classroomId', 'classroomId')
.addSelect('s.startTime', 'startTime')
.addSelect('s.endTime', 'endTime')
.addSelect('s.subject', 'subject')
.addSelect('c.name', 'className')
.where('s.classroomId IN (:...ids)', { ids: classroomIds })
.andWhere('s.status = :active', { active: 'active' })
.andWhere('s.scheduleType = :type', { type: 'INTERNAL' })
.andWhere('s.startDate <= :today', { today: todayStr })
.andWhere('s.endDate >= :today', { today: todayStr })
.andWhere('s.weekDay = :weekDay', { weekDay })
.andWhere('s.startTime <= :currentTime', { currentTime })
.andWhere('s.endTime >= :currentTime', { currentTime })
.getRawMany();
for (const s of schedules) {
const classroomId = Number(s.classroomId);
if (!result.has(classroomId)) {
result.set(classroomId, {
type: 'schedule',
title: `${s.className || ''} ${s.subject || ''}`.trim() || '内部课程',
startTime: String(s.startTime),
endTime: String(s.endTime),
});
}
}
const rentals = await this.rentalRepo
.createQueryBuilder('r')
.leftJoin('Tenant', 't', 't.id = r.tenantId')
.select('r.classroomId', 'classroomId')
.addSelect('r.startDate', 'startDate')
.addSelect('r.endDate', 'endDate')
.addSelect('t.name', 'tenantName')
.where('r.classroomId IN (:...ids)', { ids: classroomIds })
.andWhere('r.status != :cancelled', { cancelled: 'cancelled' })
.andWhere('r.startDate <= :today', { today: todayStr })
.andWhere('r.endDate >= :today', { today: todayStr })
.getRawMany();
for (const r of rentals) {
const classroomId = Number(r.classroomId);
if (!result.has(classroomId)) {
result.set(classroomId, {
type: 'rental',
title: r.tenantName ? `${r.tenantName} 租赁` : '外部租赁',
startTime: '00:00',
endTime: '23:59',
});
}
}
return result;
}
async batchImport(
rows: {
name: string;