feat: improve attendance scheduling and API validation

This commit is contained in:
2026-07-14 23:12:14 +08:00
parent c75a08affe
commit e45da7f998
33 changed files with 869 additions and 297 deletions

View File

@@ -13,6 +13,7 @@ import {
Res,
BadRequestException,
ForbiddenException,
ParseIntPipe,
} from '@nestjs/common';
import { Observable, filter } from 'rxjs';
import type { Request as ExpressRequest, Response } from 'express';
@@ -27,6 +28,7 @@ import {
QueryDingRawDto,
MatchDingRecordDto,
AttendanceReportQueryDto,
AttendanceAlertsQueryDto,
UpdateAttendanceRecordDto,
GenerateFromSchedulesDto,
LessonAttendanceQueryDto,
@@ -93,11 +95,11 @@ export class AttendanceController {
@Get('attendance-lessons/schedules/:scheduleId')
@RequirePermission('attendance:view')
async getLessonAttendance(
@Param('scheduleId') scheduleId: string,
@Param('scheduleId', ParseIntPipe) scheduleId: number,
@Query() query: LessonAttendanceQueryDto,
@Request() req: { user: RequestUser },
) {
const result = await this.service.getLessonAttendance(+scheduleId, query.date);
const result = await this.service.getLessonAttendance(scheduleId, query.date);
await this.assertClassAccess(req, result.schedule.classId!);
return result;
}
@@ -105,26 +107,29 @@ export class AttendanceController {
@Post('attendance-lessons/schedules/:scheduleId/pull')
@RequirePermission('attendance:create')
async pullLessonAttendance(
@Param('scheduleId') scheduleId: string,
@Param('scheduleId', ParseIntPipe) scheduleId: number,
@Body() dto: StartLessonAttendanceDto,
@Request() req: { user: RequestUser },
) {
const schedule = await this.service.getLessonAttendance(+scheduleId, dto.date);
const schedule = await this.service.getLessonAttendance(scheduleId, dto.date);
await this.assertClassAccess(req, schedule.schedule.classId!);
const importClassIds = await this.service.getTeacherClassDingUserIds(
req.user.id,
schedule.schedule.classId!,
this.canManageAllAttendance(req),
);
const importRange = this.service.getLessonAttendanceImportDateRange(
schedule.schedule,
dto.date,
);
const importResult = await this.importService.importFromDingTalk({
startDate: dto.date,
endDate: dto.date,
...importRange,
userIds: importClassIds,
autoMatch: true,
userId: req.user.id,
});
const result = await this.service.createLessonAttendanceFromDingTalk(
+scheduleId,
scheduleId,
dto.date,
req.user.id,
);
@@ -143,18 +148,18 @@ export class AttendanceController {
@Post('attendance-lessons/:sessionId/complete')
@RequirePermission('attendance:create')
async completeLessonAttendance(
@Param('sessionId') sessionId: string,
@Param('sessionId', ParseIntPipe) sessionId: number,
@Request() req: { user: RequestUser },
) {
const session = await this.service.findAttendanceSession(+sessionId);
const session = await this.service.findAttendanceSession(sessionId);
await this.assertClassAccess(req, session.classId);
const result = await this.service.completeLessonAttendance(+sessionId, req.user.id);
const result = await this.service.completeLessonAttendance(sessionId, req.user.id);
await this.logService.log({
userId: req.user.id,
username: req.user.username,
module: '考勤管理',
action: '完成课程点名',
targetId: +sessionId,
targetId: sessionId,
targetType: 'attendanceSession',
detail: `班级${session.classId} 日期${session.lessonDate}`,
});
@@ -278,23 +283,23 @@ export class AttendanceController {
@Put('attendance-records/:id')
@RequirePermission('attendance:edit', 'attendance:self-edit')
async update(
@Param('id') id: string,
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateAttendanceRecordDto,
@Request() req: any,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const existing = await this.service.findAttendanceRecord(+id);
const existing = await this.service.findAttendanceRecord(id);
if (existing.classId == null && !this.canManageAllAttendance(req)) {
throw new ForbiddenException('无权修改未关联班级的考勤记录');
}
if (existing.classId != null) await this.assertClassAccess(req, existing.classId);
const result = await this.service.update(+id, dto);
const result = await this.service.update(id, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '考勤管理',
action: '编辑考勤记录',
targetId: +id,
targetId: id,
targetType: 'attendanceRecord',
detail: `状态=${result.status}, 备注=${result.remark || ''}`,
ipAddress,
@@ -306,20 +311,20 @@ export class AttendanceController {
// ── Delete a single attendance record ──
@Delete('attendance-records/:id')
@RequirePermission('attendance:edit', 'attendance:self-edit')
async remove(@Param('id') id: string, @Request() req: any) {
async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const existing = await this.service.findAttendanceRecord(+id);
const existing = await this.service.findAttendanceRecord(id);
if (existing.classId == null && !this.canManageAllAttendance(req)) {
throw new ForbiddenException('无权删除未关联班级的考勤记录');
}
if (existing.classId != null) await this.assertClassAccess(req, existing.classId);
const result = await this.service.remove(+id);
const result = await this.service.remove(id);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '考勤管理',
action: '删除考勤记录',
targetId: +id,
targetId: id,
targetType: 'attendanceRecord',
detail: `删除考勤记录 ${id}`,
ipAddress,
@@ -369,18 +374,18 @@ export class AttendanceController {
@Post('ding-attendance-raw/:id/match')
@RequirePermission('attendance:edit')
async matchDingRecord(
@Param('id') id: string,
@Param('id', ParseIntPipe) id: number,
@Body() dto: MatchDingRecordDto,
@Request() req: any,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.matchDingRecord(+id, dto);
const result = await this.service.matchDingRecord(id, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '考勤管理',
action: '匹配考勤记录',
targetId: +id,
targetId: id,
targetType: 'dingAttendanceRaw',
detail: `匹配到学生 ${dto.studentId}`,
ipAddress,
@@ -458,12 +463,11 @@ export class AttendanceController {
@RequirePermission('attendance:view')
async getAlerts(
@Request() req: { user: RequestUser },
@Query('days') days?: string,
@Query('threshold') threshold?: string,
@Query() query: AttendanceAlertsQueryDto,
) {
return this.service.getAlerts(
days ? +days : 14,
threshold ? +threshold : 3,
query.days ?? 14,
query.threshold ?? 3,
await this.getAccessibleClassIds(req),
);
}