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

@@ -11,6 +11,7 @@ import {
UseInterceptors,
UploadedFile,
Res,
ParseIntPipe,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import type { Request as ExpressRequest, Response } from 'express';
@@ -47,15 +48,15 @@ export class ArchiveController {
@Get(':studentId')
@RequirePermission('student:view')
async getProfile(@Param('studentId') studentId: string, @Request() req: AuthenticatedRequest) {
async getProfile(@Param('studentId', ParseIntPipe) studentId: number, @Request() req: AuthenticatedRequest) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.getProfile(+studentId);
const result = await this.archiveService.getProfile(studentId);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生档案',
action: '查看档案',
targetId: +studentId,
targetId: studentId,
targetType: 'archive',
ipAddress,
userAgent,
@@ -66,18 +67,18 @@ export class ArchiveController {
@Put(':studentId/profile')
@RequirePermission('student:edit')
async upsertProfile(
@Param('studentId') studentId: string,
@Param('studentId', ParseIntPipe) studentId: number,
@Body() dto: UpsertProfileDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.upsertProfile(+studentId, dto);
const result = await this.archiveService.upsertProfile(studentId, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生档案',
action: '更新档案信息',
targetId: +studentId,
targetId: studentId,
targetType: 'student_profile',
detail: JSON.stringify(dto),
ipAddress,
@@ -89,12 +90,12 @@ export class ArchiveController {
@Post(':studentId/enrollments')
@RequirePermission('student:edit')
async addEnrollment(
@Param('studentId') studentId: string,
@Param('studentId', ParseIntPipe) studentId: number,
@Body() dto: CreateEnrollmentDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.addEnrollment(+studentId, dto);
const result = await this.archiveService.addEnrollment(studentId, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
@@ -112,18 +113,18 @@ export class ArchiveController {
@Put('enrollments/:id')
@RequirePermission('student:edit')
async updateEnrollment(
@Param('id') id: string,
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateEnrollmentDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.updateEnrollment(+id, dto);
const result = await this.archiveService.updateEnrollment(id, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生档案',
action: '编辑报名记录',
targetId: +id,
targetId: id,
targetType: 'student_enrollment',
detail: JSON.stringify(dto),
ipAddress,
@@ -134,15 +135,15 @@ export class ArchiveController {
@Delete('enrollments/:id')
@RequirePermission('student:edit')
async deleteEnrollment(@Param('id') id: string, @Request() req: AuthenticatedRequest) {
async deleteEnrollment(@Param('id', ParseIntPipe) id: number, @Request() req: AuthenticatedRequest) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.deleteEnrollment(+id);
const result = await this.archiveService.deleteEnrollment(id);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生档案',
action: '删除报名记录',
targetId: +id,
targetId: id,
targetType: 'student_enrollment',
ipAddress,
userAgent,
@@ -153,12 +154,12 @@ export class ArchiveController {
@Post(':studentId/exam-scores')
@RequirePermission('student:edit')
async addExamScore(
@Param('studentId') studentId: string,
@Param('studentId', ParseIntPipe) studentId: number,
@Body() dto: CreateExamScoreDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.addExamScore(+studentId, dto);
const result = await this.archiveService.addExamScore(studentId, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
@@ -176,18 +177,18 @@ export class ArchiveController {
@Put('exam-scores/:id')
@RequirePermission('student:edit')
async updateExamScore(
@Param('id') id: string,
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateExamScoreDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.updateExamScore(+id, dto);
const result = await this.archiveService.updateExamScore(id, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生档案',
action: '编辑考试成绩',
targetId: +id,
targetId: id,
targetType: 'exam_score',
detail: JSON.stringify(dto),
ipAddress,
@@ -198,15 +199,15 @@ export class ArchiveController {
@Delete('exam-scores/:id')
@RequirePermission('student:edit')
async deleteExamScore(@Param('id') id: string, @Request() req: AuthenticatedRequest) {
async deleteExamScore(@Param('id', ParseIntPipe) id: number, @Request() req: AuthenticatedRequest) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.deleteExamScore(+id);
const result = await this.archiveService.deleteExamScore(id);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生档案',
action: '删除考试成绩',
targetId: +id,
targetId: id,
targetType: 'exam_score',
ipAddress,
userAgent,
@@ -217,12 +218,12 @@ export class ArchiveController {
@Post(':studentId/learning-records')
@RequirePermission('student:edit')
async addLearningRecord(
@Param('studentId') studentId: string,
@Param('studentId', ParseIntPipe) studentId: number,
@Body() dto: CreateLearningRecordDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.addLearningRecord(+studentId, dto);
const result = await this.archiveService.addLearningRecord(studentId, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
@@ -240,18 +241,18 @@ export class ArchiveController {
@Put('learning-records/:id')
@RequirePermission('student:edit')
async updateLearningRecord(
@Param('id') id: string,
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateLearningRecordDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.updateLearningRecord(+id, dto);
const result = await this.archiveService.updateLearningRecord(id, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生档案',
action: '编辑学习记录',
targetId: +id,
targetId: id,
targetType: 'learning_record',
detail: JSON.stringify(dto),
ipAddress,
@@ -262,15 +263,15 @@ export class ArchiveController {
@Delete('learning-records/:id')
@RequirePermission('student:edit')
async deleteLearningRecord(@Param('id') id: string, @Request() req: AuthenticatedRequest) {
async deleteLearningRecord(@Param('id', ParseIntPipe) id: number, @Request() req: AuthenticatedRequest) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.deleteLearningRecord(+id);
const result = await this.archiveService.deleteLearningRecord(id);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生档案',
action: '删除学习记录',
targetId: +id,
targetId: id,
targetType: 'learning_record',
ipAddress,
userAgent,
@@ -281,18 +282,18 @@ export class ArchiveController {
@Put(':studentId/result')
@RequirePermission('student:edit')
async upsertResult(
@Param('studentId') studentId: string,
@Param('studentId', ParseIntPipe) studentId: number,
@Body() dto: UpsertResultDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.upsertResult(+studentId, dto);
const result = await this.archiveService.upsertResult(studentId, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生档案',
action: '更新录取结果',
targetId: +studentId,
targetId: studentId,
targetType: 'result_archive',
detail: JSON.stringify(dto),
ipAddress,
@@ -305,13 +306,13 @@ export class ArchiveController {
@RequirePermission('student:edit')
@UseInterceptors(FileInterceptor('file'))
async uploadAttachment(
@Param('studentId') studentId: string,
@Param('studentId', ParseIntPipe) studentId: number,
@UploadedFile() file: Express.Multer.File,
@Body('category') category: string,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.addAttachment(+studentId, file, category || 'other');
const result = await this.archiveService.addAttachment(studentId, file, category || 'other');
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
@@ -329,13 +330,13 @@ export class ArchiveController {
@Get(':studentId/attachments/:id')
@RequirePermission('student:view')
async downloadAttachment(
@Param('studentId') studentId: string,
@Param('id') id: string,
@Param('studentId', ParseIntPipe) studentId: number,
@Param('id', ParseIntPipe) id: number,
@Res() res: Response,
) {
const { fullPath, fileName, mimeType } = await this.archiveService.getAttachmentFile(
+studentId,
+id,
studentId,
id,
);
res.setHeader('Content-Type', mimeType);
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(fileName)}"`);
@@ -345,15 +346,15 @@ export class ArchiveController {
@Delete('attachments/:id')
@RequirePermission('student:edit')
async deleteAttachment(@Param('id') id: string, @Request() req: AuthenticatedRequest) {
async deleteAttachment(@Param('id', ParseIntPipe) id: number, @Request() req: AuthenticatedRequest) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.archiveService.deleteAttachment(+id);
const result = await this.archiveService.deleteAttachment(id);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生档案',
action: '删除附件',
targetId: +id,
targetId: id,
targetType: 'archive_attachment',
ipAddress,
userAgent,
@@ -364,7 +365,7 @@ export class ArchiveController {
@Get(':studentId/report-html')
@RequirePermission('student:view')
async generateReportHtml(
@Param('studentId') studentId: string,
@Param('studentId', ParseIntPipe) studentId: number,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
@@ -373,12 +374,12 @@ export class ArchiveController {
username: req.user?.username,
module: 'archive',
action: 'generate_report_html',
targetId: +studentId,
targetId: studentId,
targetType: 'student',
ipAddress,
userAgent,
});
const html = await this.reportService.generateReportHtml(+studentId);
const html = await this.reportService.generateReportHtml(studentId);
return { html };
}
}

View File

@@ -17,6 +17,7 @@ describe('ArchiveService.getProfile', () => {
const learningRecordRepo = { find: jest.fn().mockResolvedValue([]) };
const resultRepo = { findOne: jest.fn().mockResolvedValue(result) };
const attachmentRepo = { find: jest.fn().mockResolvedValue([]) };
const attendanceRepo = { find: jest.fn().mockResolvedValue([]) };
const service = new ArchiveService(
studentRepo as never,
@@ -26,12 +27,13 @@ describe('ArchiveService.getProfile', () => {
learningRecordRepo as never,
resultRepo as never,
attachmentRepo as never,
attendanceRepo as never,
{} as never,
);
const response = await service.getProfile(7);
expect(response).toMatchObject({ student, result });
expect(response).toMatchObject({ student, result, attendances: [] });
expect(response).not.toHaveProperty('resultArchive');
});
});

View File

@@ -12,6 +12,7 @@ import { ExamScore } from '../entities/exam-score.entity';
import { LearningRecord } from '../entities/learning-record.entity';
import { ResultArchive } from '../entities/result-archive.entity';
import { ArchiveAttachment } from '../entities/archive-attachment.entity';
import { AttendanceRecord } from '../entities/attendance-record.entity';
import {
UpsertProfileDto,
CreateEnrollmentDto,
@@ -33,6 +34,7 @@ export class ArchiveService {
@InjectRepository(LearningRecord) private learningRecordRepo: Repository<LearningRecord>,
@InjectRepository(ResultArchive) private resultRepo: Repository<ResultArchive>,
@InjectRepository(ArchiveAttachment) private attachmentRepo: Repository<ArchiveAttachment>,
@InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
private readonly notificationsService: NotificationsService,
) {}
@@ -59,7 +61,7 @@ export class ArchiveService {
const student = await this.studentRepo.findOne({ where: { id: studentId } });
if (!student) throw new NotFoundException('学生不存在');
const [profileRaw, enrollments, examScores, learningRecords, resultArchive, attachments] =
const [profileRaw, enrollments, examScores, learningRecords, resultArchive, attachments, attendances] =
await Promise.all([
this.profileRepo.findOne({ where: { studentId } }),
this.enrollmentRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } }),
@@ -67,6 +69,11 @@ export class ArchiveService {
this.learningRecordRepo.find({ where: { studentId }, order: { recordDate: 'DESC' } }),
this.resultRepo.findOne({ where: { studentId } }),
this.attachmentRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } }),
this.attendanceRepo.find({
where: { studentId },
relations: ['schedule', 'class'],
order: { attendanceDate: 'DESC', punchTime: 'DESC' },
}),
]);
return {
@@ -77,6 +84,7 @@ export class ArchiveService {
learningRecords,
result: resultArchive,
attachments,
attendances,
};
}