feat: 集成 AI 对话与只读查询工具

This commit is contained in:
2026-07-23 14:24:40 +08:00
parent 302dbe0621
commit f3b59935d6
52 changed files with 4942 additions and 4 deletions

View File

@@ -29,6 +29,14 @@ import {
SaveAttendancePeriodConfigsDto,
} from './dto/attendance.dto';
interface AgentAttendanceSummaryRow {
date: string;
classId: string | number;
className: string;
status: string;
count: string | number;
}
/** Keyed mutex serializing operations on the same attendance session. */
class SessionMutex {
private queueTails = new Map<number, Promise<void>>();
@@ -156,6 +164,40 @@ export class AttendanceService {
if (!assignment) throw new BadRequestException('只能访问自己任教班级的考勤');
}
async agentGetAttendanceSummary(
userId: number,
canManageAll: boolean,
query: { classId?: number; dateFrom?: string; dateTo?: string; limit?: number },
) {
const accessibleClassIds = await this.getAccessibleClassIds(userId, canManageAll);
if (accessibleClassIds?.length === 0) return [];
if (query.classId && accessibleClassIds && !accessibleClassIds.includes(query.classId)) return [];
const qb = this.attendanceRepo
.createQueryBuilder('attendance')
.leftJoin('attendance.class', 'class')
.select('attendance.attendanceDate', 'date')
.addSelect('attendance.classId', 'classId')
.addSelect('class.name', 'className')
.addSelect('attendance.status', 'status')
.addSelect('COUNT(attendance.id)', 'count')
.where('attendance.classId IS NOT NULL');
if (query.classId) qb.andWhere('attendance.classId = :classId', { classId: query.classId });
else if (accessibleClassIds) qb.andWhere('attendance.classId IN (:...accessibleClassIds)', { accessibleClassIds });
if (query.dateFrom) qb.andWhere('attendance.attendanceDate >= :dateFrom', { dateFrom: query.dateFrom });
if (query.dateTo) qb.andWhere('attendance.attendanceDate <= :dateTo', { dateTo: query.dateTo });
const rows = await qb
.groupBy('attendance.attendanceDate')
.addGroupBy('attendance.classId')
.addGroupBy('class.name')
.addGroupBy('attendance.status')
.orderBy('attendance.attendanceDate', 'DESC')
.addOrderBy('class.name', 'ASC')
.limit(query.limit ?? 30)
.getRawMany<AgentAttendanceSummaryRow>();
return rows.map((row) => ({ ...row, classId: Number(row.classId), count: Number(row.count || 0) }));
}
private isClassStudentActiveOnDate(classStudent: Pick<ClassStudent, 'joinDate' | 'leaveDate' | 'status'>, lessonDate: string): boolean {
const status = classStudent.status ?? 'active';
if (!['active', 'left'].includes(status)) return false;