forked from wangziqi/gongxue-base
- Delete department.entity.ts, user-department.entity.ts - Remove Department/UserDepartment from entities/index.ts - Remove departmentId column from 18 entities (AttendanceRecord, ArchiveAttachment, Bill, ClassSchedule, Classroom, ClassroomRental, Deposit, DepositInstallment, ExamScore, LearningRecord, Occupancy, PersonalExpense, ResultArchive, Room, RoomExpense, Student, StudentEnrollment, StudentProfile, StudentReport) - Remove departments/ module entirely - Delete campus-scope.ts, campus-scope.middleware.ts (request-utils.ts kept — it's just IP extraction) - Simplify common.module.ts to empty module - Remove CampusScopeMiddleware from app.module.ts - Remove all CampusScope injections and filter calls across all services - Remove departmentId from all DTOs and controllers - Simplify dingtalk/wecom sync to only sync users (no dept table) - Update seed module to remove department seeding - Clean frontend compilation
213 lines
8.3 KiB
TypeScript
213 lines
8.3 KiB
TypeScript
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository, Not } from 'typeorm';
|
|
import { Classroom } from '../entities/classroom.entity';
|
|
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
|
import { ClassSchedule } from '../entities/class-schedule.entity';
|
|
import { CreateClassroomDto, UpdateClassroomDto } from './dto/classroom.dto';
|
|
|
|
@Injectable()
|
|
export class ClassroomsService {
|
|
|
|
constructor(
|
|
@InjectRepository(Classroom) private repo: Repository<Classroom>,
|
|
@InjectRepository(ClassroomRental) private rentalRepo: Repository<ClassroomRental>,
|
|
@InjectRepository(ClassSchedule) private scheduleRepo: Repository<ClassSchedule>,
|
|
) {}
|
|
|
|
async findAll(query?: { building?: string; roomType?: string; includeArchived?: boolean }) {
|
|
const where: Record<string, unknown> = {};
|
|
if (query?.building) where.building = query.building;
|
|
if (query?.roomType) where.roomType = query.roomType;
|
|
if (!query?.includeArchived) where.status = Not('archived');
|
|
const list = await this.repo.find({ 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('教室不存在');
|
|
const usageMap = await this.getCurrentUsageForClassrooms([id]);
|
|
return { ...cls, currentUsage: usageMap.get(id) ?? null };
|
|
}
|
|
|
|
async create(dto: CreateClassroomDto) {
|
|
const exists = await this.repo.findOne({ where: { name: dto.name } });
|
|
if (exists) throw new BadRequestException(`教室 ${dto.name} 已存在`);
|
|
return this.repo.save(this.repo.create(dto));
|
|
}
|
|
|
|
async update(id: number, dto: UpdateClassroomDto) {
|
|
await this.findOne(id);
|
|
await this.repo.update(id, dto);
|
|
return this.repo.findOne({ where: { id } });
|
|
}
|
|
|
|
async remove(id: number) {
|
|
await this.findOne(id);
|
|
await this.repo.update(id, { status: 'archived' });
|
|
return { message: '已归档' };
|
|
}
|
|
|
|
async restore(id: number) {
|
|
await this.findOne(id);
|
|
await this.repo.update(id, { status: 'reserved' });
|
|
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;
|
|
building?: string;
|
|
floor?: number;
|
|
capacity?: number;
|
|
roomType?: string;
|
|
courseType?: string;
|
|
}[],
|
|
) {
|
|
let imported = 0;
|
|
let skipped = 0;
|
|
const errors: string[] = [];
|
|
for (const row of rows) {
|
|
if (!row.name?.trim()) { skipped++; continue; }
|
|
const exists = await this.repo.findOne({ where: { name: row.name.trim() } });
|
|
if (exists) { errors.push(`教室 ${row.name} 已存在`); skipped++; continue; }
|
|
await this.repo.save(this.repo.create({ ...row, capacity: row.capacity || 30 }));
|
|
imported++;
|
|
}
|
|
return { message: `成功导入 ${imported} 间教室,跳过 ${skipped} 间`, imported, skipped, errors: errors.length > 0 ? errors : undefined };
|
|
}
|
|
|
|
async getUsageReport(dateFrom: string, dateTo: string) {
|
|
const classrooms = await this.repo.find({
|
|
where: { status: Not('archived') },
|
|
order: { building: 'ASC', name: 'ASC' },
|
|
});
|
|
|
|
const rentals = await this.rentalRepo
|
|
.createQueryBuilder('r')
|
|
.where('r.status != :cancelled', { cancelled: 'cancelled' })
|
|
.andWhere('r.startDate <= :dateTo AND r.endDate >= :dateFrom', { dateFrom, dateTo })
|
|
.getMany();
|
|
|
|
const schedules = await this.scheduleRepo
|
|
.createQueryBuilder('s')
|
|
.where('s.status = :active', { active: 'active' })
|
|
.andWhere('s.scheduleType = :type', { type: 'INTERNAL' })
|
|
.andWhere('s.startDate <= :dateTo AND s.endDate >= :dateFrom', { dateFrom, dateTo })
|
|
.getMany();
|
|
|
|
const start = new Date(dateFrom);
|
|
const end = new Date(dateTo);
|
|
const totalDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
|
|
|
const rentalDaysByRoom: Record<number, Set<string>> = {};
|
|
const scheduleDaysByRoom: Record<number, Set<string>> = {};
|
|
|
|
for (const r of rentals) {
|
|
if (!rentalDaysByRoom[r.classroomId]) rentalDaysByRoom[r.classroomId] = new Set();
|
|
const effStart = new Date(Math.max(new Date(r.startDate).getTime(), start.getTime()));
|
|
const effEnd = new Date(Math.min(new Date(r.endDate).getTime(), end.getTime()));
|
|
for (let d = new Date(effStart); d <= effEnd; d.setDate(d.getDate() + 1)) {
|
|
rentalDaysByRoom[r.classroomId].add(d.toISOString().slice(0, 10));
|
|
}
|
|
}
|
|
|
|
for (const s of schedules) {
|
|
if (!scheduleDaysByRoom[s.classroomId]) scheduleDaysByRoom[s.classroomId] = new Set();
|
|
const effStart = new Date(Math.max(new Date(s.startDate).getTime(), start.getTime()));
|
|
const effEnd = new Date(Math.min(new Date(s.endDate).getTime(), end.getTime()));
|
|
for (let d = new Date(effStart); d <= effEnd; d.setDate(d.getDate() + 1)) {
|
|
scheduleDaysByRoom[s.classroomId].add(d.toISOString().slice(0, 10));
|
|
}
|
|
}
|
|
|
|
return classrooms.map((c) => {
|
|
const rentalDays = rentalDaysByRoom[c.id]?.size || 0;
|
|
const scheduleDays = scheduleDaysByRoom[c.id]?.size || 0;
|
|
const usedDays = rentalDays + scheduleDays;
|
|
return {
|
|
id: c.id,
|
|
name: c.name,
|
|
building: c.building || '',
|
|
roomType: c.roomType || '',
|
|
capacity: c.capacity,
|
|
totalDays,
|
|
rentalDays,
|
|
scheduleDays,
|
|
usedDays,
|
|
idleDays: totalDays - usedDays,
|
|
occupancyRate: totalDays > 0 ? ((usedDays / totalDays) * 100).toFixed(1) : '0.0',
|
|
};
|
|
});
|
|
}
|
|
}
|