Files
gongxue-base/apps/server/src/schedules/schedule-queries.service.ts
T
wangziqi a644a8de42 refactor(server): 清理全量 any 类型安全警告 (692 → 0)
- 全模块类型化: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 用例全部通过
2026-08-08 09:28:23 +08:00

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();
}
}