forked from wangziqi/gongxue-base
- 全模块类型化:controller 的 req: any → AuthenticatedRequest/RequestUser, 聚合查询 getRawMany 泛型标注、导入行/响应体定义具体 interface、 catch (e: any) → unknown + 收窄、no-base-to-string 用 String() 显式转换 - 第三方无类型库边界(pdfkit/exceljs)文件级或单行 disable 并注明理由 - 顺带修复:get-business-context.tool 两个 require-await error、 bills.controller 参数顺序隐患、main.ts compression 调用 - 运行时逻辑零改动;测试 142 套件 / 1065 用例全部通过
189 lines
6.2 KiB
TypeScript
189 lines
6.2 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { ClassSchedule } from '../entities';
|
|
import type { WeeklyViewQueryDto } from './dto/schedule.dto';
|
|
|
|
const ACTIVE_SCHEDULE_STATUS = 'active';
|
|
|
|
/** String() 包装:避免 raw 行值(unknown 收窄为对象类型)触发 no-base-to-string。 */
|
|
function stringify(value: unknown): string {
|
|
return String(value);
|
|
}
|
|
|
|
@Injectable()
|
|
export class ScheduleQueriesService {
|
|
constructor(
|
|
@InjectRepository(ClassSchedule)
|
|
private readonly scheduleRepo: Repository<ClassSchedule>,
|
|
) {}
|
|
|
|
maskScheduleOccupancy(schedule: ClassSchedule) {
|
|
return {
|
|
id: null,
|
|
classId: null,
|
|
classroomId: schedule.classroomId,
|
|
weekDay: schedule.weekDay,
|
|
startTime: schedule.startTime,
|
|
endTime: schedule.endTime,
|
|
attendanceAdvanceMinutes: schedule.attendanceAdvanceMinutes,
|
|
startDate: schedule.startDate,
|
|
endDate: schedule.endDate,
|
|
subject: '已占用',
|
|
teacherId: null,
|
|
scheduleType: schedule.scheduleType,
|
|
status: schedule.status,
|
|
notes: null,
|
|
canViewDetails: false,
|
|
};
|
|
}
|
|
|
|
|
|
async agentSearchSchedules(
|
|
accessibleClassIds: number[] | undefined,
|
|
query?: { classId?: number; classroomId?: number; weekDay?: number; limit?: number },
|
|
): Promise<
|
|
{
|
|
id: number;
|
|
classId: number | null;
|
|
className: string | null;
|
|
classroomId: number;
|
|
classroomName: string | null;
|
|
weekDay: number;
|
|
startTime: string;
|
|
endTime: string;
|
|
subject: string;
|
|
teacherName: string | null;
|
|
startDate: string;
|
|
endDate: string;
|
|
scheduleType: string;
|
|
status: string;
|
|
}[]
|
|
> {
|
|
if (query?.classId && accessibleClassIds && !accessibleClassIds.includes(query.classId)) {
|
|
return [];
|
|
}
|
|
if (accessibleClassIds && accessibleClassIds.length === 0) {
|
|
return [];
|
|
}
|
|
const qb = this.scheduleRepo
|
|
.createQueryBuilder('cs')
|
|
.leftJoin('cs.class', 'class')
|
|
.leftJoin('cs.classroom', 'classroom')
|
|
.leftJoin('cs.teacher', 'teacher')
|
|
.select([
|
|
'cs.id',
|
|
'cs.classId',
|
|
'cs.classroomId',
|
|
'cs.weekDay',
|
|
'cs.startTime',
|
|
'cs.endTime',
|
|
'cs.subject',
|
|
'cs.teacherId',
|
|
'cs.startDate',
|
|
'cs.endDate',
|
|
'cs.scheduleType',
|
|
'cs.status',
|
|
'class.name',
|
|
'classroom.name',
|
|
'teacher.name',
|
|
])
|
|
.where('cs.status = :active', { active: 'active' });
|
|
|
|
if (query?.classroomId) {
|
|
qb.andWhere('cs.classroomId = :classroomId', { classroomId: query.classroomId });
|
|
}
|
|
if (query?.classId) {
|
|
qb.andWhere('cs.classId = :classId', { classId: query.classId });
|
|
}
|
|
if (accessibleClassIds) {
|
|
qb.andWhere('cs.classId IN (:...accessibleClassIds)', { accessibleClassIds });
|
|
}
|
|
if (query?.weekDay) {
|
|
qb.andWhere('cs.weekDay = :weekDay', { weekDay: query.weekDay });
|
|
}
|
|
|
|
const rows = await qb
|
|
.orderBy('cs.weekDay', 'ASC')
|
|
.addOrderBy('cs.startTime', 'ASC')
|
|
.limit(Math.max(1, Math.min(query?.limit ?? 20, 50)))
|
|
.getRawMany<Record<string, unknown>>();
|
|
return rows.map((row) => ({
|
|
id: Number(row.cs_id),
|
|
classId: row.cs_class_id == null ? null : Number(row.cs_class_id),
|
|
className: row.class_name == null ? null : stringify(row.class_name),
|
|
classroomId: Number(row.cs_classroom_id),
|
|
classroomName: row.classroom_name == null ? null : stringify(row.classroom_name),
|
|
weekDay: Number(row.cs_week_day),
|
|
startTime: stringify(row.cs_start_time),
|
|
endTime: stringify(row.cs_end_time),
|
|
subject: stringify(row.cs_subject),
|
|
teacherName: row.teacher_name == null ? null : stringify(row.teacher_name),
|
|
startDate: stringify(row.cs_start_date),
|
|
endDate: stringify(row.cs_end_date),
|
|
scheduleType: stringify(row.cs_schedule_type),
|
|
status: stringify(row.cs_status),
|
|
}));
|
|
}
|
|
|
|
|
|
async getWeeklyView(query: WeeklyViewQueryDto, accessibleClassIds?: number[]) {
|
|
const qb = this.scheduleRepo.createQueryBuilder('cs');
|
|
if (query.classroomId) {
|
|
qb.andWhere('cs.classroomId = :classroomId', { classroomId: query.classroomId });
|
|
}
|
|
if (query.startDate) {
|
|
qb.andWhere('cs.endDate >= :startDate', { startDate: query.startDate });
|
|
}
|
|
if (query.endDate) {
|
|
qb.andWhere('cs.startDate <= :endDate', { endDate: query.endDate });
|
|
}
|
|
|
|
const schedules = await qb
|
|
.andWhere('cs.status = :status', { status: ACTIVE_SCHEDULE_STATUS })
|
|
.orderBy('cs.weekDay', 'ASC')
|
|
.addOrderBy('cs.startTime', 'ASC')
|
|
.getMany();
|
|
|
|
const allowedClassIds = accessibleClassIds ? new Set(accessibleClassIds) : null;
|
|
const visibleSchedules = schedules.map((schedule) => {
|
|
const canViewDetails =
|
|
allowedClassIds === null ||
|
|
(schedule.classId !== null && allowedClassIds.has(schedule.classId));
|
|
if (canViewDetails) return { ...schedule, canViewDetails: true };
|
|
|
|
// Other classes remain visible only as a room/time occupancy block.
|
|
// Do not expose class, subject, teacher, notes, or internal record IDs.
|
|
return this.maskScheduleOccupancy(schedule);
|
|
});
|
|
|
|
// Group by classroomId → weekDay
|
|
const matrix: Record<number, Record<number, typeof visibleSchedules>> = {};
|
|
for (const schedule of visibleSchedules) {
|
|
if (!matrix[schedule.classroomId]) matrix[schedule.classroomId] = {};
|
|
if (!matrix[schedule.classroomId][schedule.weekDay])
|
|
matrix[schedule.classroomId][schedule.weekDay] = [];
|
|
matrix[schedule.classroomId][schedule.weekDay].push(schedule);
|
|
}
|
|
|
|
return matrix;
|
|
}
|
|
|
|
|
|
async getClassroomOccupancy(classroomId: number, date?: string) {
|
|
const qb = this.scheduleRepo
|
|
.createQueryBuilder('cs')
|
|
.where('cs.classroomId = :classroomId', { classroomId })
|
|
.andWhere('cs.status = :status', { status: ACTIVE_SCHEDULE_STATUS })
|
|
.andWhere('cs.scheduleType IN (:...scheduleTypes)', {
|
|
scheduleTypes: ['INTERNAL', 'RENTAL'],
|
|
});
|
|
|
|
if (date) {
|
|
qb.andWhere('cs.startDate <= :date', { date }).andWhere('cs.endDate >= :date', { date });
|
|
}
|
|
|
|
return qb.orderBy('cs.weekDay', 'ASC').addOrderBy('cs.startTime', 'ASC').getMany();
|
|
}
|
|
}
|