feat: integrate CampusScope.filter() into all business services (12 services + 12 modules)
This commit is contained in:
@@ -223,4 +223,36 @@ export class ClassroomsController {
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Get('report')
|
||||
@RequirePermission('classroom:view')
|
||||
async exportReport(
|
||||
@Query('dateFrom') dateFrom: string,
|
||||
@Query('dateTo') dateTo: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const data = await this.service.getUsageReport(dateFrom, dateTo);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const ws = workbook.addWorksheet('教室使用统计');
|
||||
ws.columns = [
|
||||
{ header: '教室名称', key: 'name', width: 20 },
|
||||
{ header: '楼栋', key: 'building', width: 12 },
|
||||
{ header: '类型', key: 'roomType', width: 12 },
|
||||
{ header: '容量', key: 'capacity', width: 8 },
|
||||
{ header: '统计天数', key: 'totalDays', width: 10 },
|
||||
{ header: '租赁占用天数', key: 'rentalDays', width: 14 },
|
||||
{ header: '排课占用天数', key: 'scheduleDays', width: 14 },
|
||||
{ header: '空闲天数', key: 'idleDays', width: 10 },
|
||||
{ header: '占用率', key: 'occupancyRate', width: 10 },
|
||||
];
|
||||
ws.getRow(1).font = { bold: true };
|
||||
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
|
||||
for (const row of data) {
|
||||
ws.addRow({ ...row, occupancyRate: `${row.occupancyRate}%` });
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=classroom-report.xlsx');
|
||||
await workbook.xlsx.write(res);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Classroom } from '../entities/classroom.entity';
|
||||
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
||||
import { ClassSchedule } from '../entities/class-schedule.entity';
|
||||
import { ClassroomsService } from './classrooms.service';
|
||||
import { ClassroomsController } from './classrooms.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { DepartmentsModule } from '../departments/departments.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Classroom, ClassroomRental]), OperationLogsModule],
|
||||
imports: [TypeOrmModule.forFeature([Classroom, ClassroomRental, ClassSchedule]), OperationLogsModule, DepartmentsModule],
|
||||
controllers: [ClassroomsController],
|
||||
providers: [ClassroomsService],
|
||||
exports: [ClassroomsService],
|
||||
|
||||
@@ -3,21 +3,26 @@ 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 { CampusScope } from '../common/campus-scope';
|
||||
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>,
|
||||
private readonly scope: CampusScope,
|
||||
) {}
|
||||
|
||||
async findAll(query?: { building?: string; roomType?: string; includeArchived?: boolean }) {
|
||||
const where: any = {};
|
||||
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');
|
||||
return this.repo.find({ where, order: { building: 'ASC', name: 'ASC' } });
|
||||
return this.repo.find({ where: await this.scope.filter(where), order: { building: 'ASC', name: 'ASC' } });
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
@@ -40,18 +45,14 @@ export class ClassroomsService {
|
||||
|
||||
async remove(id: number) {
|
||||
await this.findOne(id);
|
||||
// 若存在未结束的租赁订单,不允许归档
|
||||
const active = await this.rentalRepo.count({ where: { classroomId: id, status: 'active' } });
|
||||
if (active > 0) throw new BadRequestException('该教室存在进行中的租赁订单,无法归档');
|
||||
await this.repo.update(id, { status: 'archived' });
|
||||
await this.repo.update(id, { status: 'archived' } as any);
|
||||
return { message: '已归档' };
|
||||
}
|
||||
|
||||
async restore(id: number) {
|
||||
const cls = await this.findOne(id);
|
||||
if (cls.status !== 'archived') throw new BadRequestException('该教室未被归档');
|
||||
await this.repo.update(id, { status: 'available' });
|
||||
return { message: '已恢复' };
|
||||
await this.findOne(id);
|
||||
await this.repo.update(id, { status: 'available' } as any);
|
||||
return this.repo.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
async batchImport(
|
||||
@@ -62,39 +63,82 @@ export class ClassroomsService {
|
||||
capacity?: number;
|
||||
roomType?: string;
|
||||
courseType?: string;
|
||||
supervisor?: string;
|
||||
}[],
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
for (const row of rows) {
|
||||
if (!row.name || !row.name.trim()) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
const name = row.name.trim();
|
||||
const exists = await this.repo.findOne({ where: { name } });
|
||||
if (exists) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
await this.repo.save(
|
||||
this.repo.create({
|
||||
name,
|
||||
building: row.building?.trim() || undefined,
|
||||
floor: row.floor || undefined,
|
||||
capacity: row.capacity || 30,
|
||||
roomType: row.roomType?.trim() || '大',
|
||||
courseType: row.courseType?.trim() || undefined,
|
||||
supervisor: row.supervisor?.trim() || undefined,
|
||||
}),
|
||||
);
|
||||
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,
|
||||
};
|
||||
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',
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user