refactor: resolve remaining field audit issues

This commit is contained in:
2026-07-13 15:12:36 +08:00
parent 0533c30ece
commit aa1ed7db56
34 changed files with 953 additions and 263 deletions

View File

@@ -20,8 +20,11 @@ import { ArchiveService } from './archive.service';
import {
UpsertProfileDto,
CreateEnrollmentDto,
UpdateEnrollmentDto,
CreateExamScoreDto,
UpdateExamScoreDto,
CreateLearningRecordDto,
UpdateLearningRecordDto,
UpsertResultDto,
} from './dto/archive.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@@ -110,7 +113,7 @@ export class ArchiveController {
@RequirePermission('student:edit')
async updateEnrollment(
@Param('id') id: string,
@Body() dto: Partial<CreateEnrollmentDto>,
@Body() dto: UpdateEnrollmentDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
@@ -174,7 +177,7 @@ export class ArchiveController {
@RequirePermission('student:edit')
async updateExamScore(
@Param('id') id: string,
@Body() dto: Partial<CreateExamScoreDto>,
@Body() dto: UpdateExamScoreDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
@@ -238,7 +241,7 @@ export class ArchiveController {
@RequirePermission('student:edit')
async updateLearningRecord(
@Param('id') id: string,
@Body() dto: Partial<CreateLearningRecordDto>,
@Body() dto: UpdateLearningRecordDto,
@Request() req: AuthenticatedRequest,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
@@ -330,12 +333,12 @@ export class ArchiveController {
@Param('id') id: string,
@Res() res: Response,
) {
const { fullPath, fileName, mimeType } = await this.archiveService.getAttachmentFile(+studentId, +id);
res.setHeader('Content-Type', mimeType);
res.setHeader(
'Content-Disposition',
`inline; filename="${encodeURIComponent(fileName)}"`,
const { fullPath, fileName, mimeType } = await this.archiveService.getAttachmentFile(
+studentId,
+id,
);
res.setHeader('Content-Type', mimeType);
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(fileName)}"`);
const stream = fs.createReadStream(fullPath);
stream.pipe(res);
}
@@ -358,7 +361,6 @@ export class ArchiveController {
return result;
}
@Get(':studentId/report-html')
@RequirePermission('student:view')
async generateReportHtml(

View File

@@ -15,8 +15,11 @@ import { ArchiveAttachment } from '../entities/archive-attachment.entity';
import {
UpsertProfileDto,
CreateEnrollmentDto,
UpdateEnrollmentDto,
CreateExamScoreDto,
UpdateExamScoreDto,
CreateLearningRecordDto,
UpdateLearningRecordDto,
UpsertResultDto,
} from './dto/archive.dto';
@@ -44,7 +47,9 @@ export class ArchiveService {
? path.resolve(process.cwd(), normalizedPath)
: path.resolve(this.uploadDir, normalizedPath);
const allowedRoots = [this.uploadDir, path.resolve(process.cwd(), 'uploads', 'archive')];
if (!allowedRoots.some((root) => fullPath === root || fullPath.startsWith(`${root}${path.sep}`))) {
if (
!allowedRoots.some((root) => fullPath === root || fullPath.startsWith(`${root}${path.sep}`))
) {
throw new BadRequestException('路径非法');
}
return fullPath;
@@ -54,21 +59,15 @@ export class ArchiveService {
const student = await this.studentRepo.findOne({ where: { id: studentId } });
if (!student) throw new NotFoundException('学生不存在');
const [
profileRaw,
enrollments,
examScores,
learningRecords,
resultArchive,
attachments,
] = await Promise.all([
this.profileRepo.findOne({ where: { studentId } }),
this.enrollmentRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } }),
this.examScoreRepo.find({ where: { studentId }, order: { examDate: 'DESC' } }),
this.learningRecordRepo.find({ where: { studentId }, order: { recordDate: 'DESC' } }),
this.resultRepo.findOne({ where: { studentId } }),
this.attachmentRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } }),
]);
const [profileRaw, enrollments, examScores, learningRecords, resultArchive, attachments] =
await Promise.all([
this.profileRepo.findOne({ where: { studentId } }),
this.enrollmentRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } }),
this.examScoreRepo.find({ where: { studentId }, order: { examDate: 'DESC' } }),
this.learningRecordRepo.find({ where: { studentId }, order: { recordDate: 'DESC' } }),
this.resultRepo.findOne({ where: { studentId } }),
this.attachmentRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } }),
]);
return {
student,
@@ -102,7 +101,7 @@ export class ArchiveService {
return this.enrollmentRepo.save(entity);
}
async updateEnrollment(id: number, dto: Partial<CreateEnrollmentDto>) {
async updateEnrollment(id: number, dto: UpdateEnrollmentDto) {
const entity = await this.enrollmentRepo.findOne({ where: { id } });
if (!entity) throw new NotFoundException('报名记录不存在');
Object.assign(entity, dto);
@@ -124,7 +123,7 @@ export class ArchiveService {
return this.examScoreRepo.save(entity);
}
async updateExamScore(id: number, dto: Partial<CreateExamScoreDto>) {
async updateExamScore(id: number, dto: UpdateExamScoreDto) {
const entity = await this.examScoreRepo.findOne({ where: { id } });
if (!entity) throw new NotFoundException('考试成绩不存在');
Object.assign(entity, dto);
@@ -146,7 +145,7 @@ export class ArchiveService {
return this.learningRecordRepo.save(entity);
}
async updateLearningRecord(id: number, dto: Partial<CreateLearningRecordDto>) {
async updateLearningRecord(id: number, dto: UpdateLearningRecordDto) {
const entity = await this.learningRecordRepo.findOne({ where: { id } });
if (!entity) throw new NotFoundException('学习记录不存在');
Object.assign(entity, dto);
@@ -225,4 +224,3 @@ export class ArchiveService {
return { message: '已删除' };
}
}

View File

@@ -1,7 +1,19 @@
import 'reflect-metadata';
import { ValidationPipe } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { UpsertProfileDto } from './archive.dto';
import {
UpdateEnrollmentDto,
UpdateExamScoreDto,
UpdateLearningRecordDto,
UpsertProfileDto,
} from './archive.dto';
const pipe = new ValidationPipe({ transform: true, whitelist: true });
async function transform<T extends object>(metatype: new () => T, value: unknown) {
return pipe.transform(value, { type: 'body', metatype });
}
describe('UpsertProfileDto retired fields', () => {
it('removes the retired campusLocation field under whitelist validation', async () => {
@@ -16,3 +28,30 @@ describe('UpsertProfileDto retired fields', () => {
expect(dto).not.toHaveProperty('campusLocation');
});
});
describe('archive update DTOs', () => {
it('allows partial enrollment updates and strips unknown fields', async () => {
await expect(
transform(UpdateEnrollmentDto, { className: '新班级', ignored: 'value' }),
).resolves.toEqual(expect.objectContaining({ className: '新班级' }));
const result = await transform(UpdateEnrollmentDto, {
className: '新班级',
ignored: 'value',
});
expect(result).not.toHaveProperty('ignored');
});
it('retains create DTO validation rules for exam scores', async () => {
await expect(transform(UpdateExamScoreDto, { score: '90' })).rejects.toThrow();
await expect(transform(UpdateExamScoreDto, { score: 90 })).resolves.toMatchObject({
score: 90,
});
});
it('retains create DTO date validation for learning records', async () => {
await expect(
transform(UpdateLearningRecordDto, { recordDate: 'not-a-date' }),
).rejects.toThrow();
await expect(transform(UpdateLearningRecordDto, {})).resolves.toEqual({});
});
});

View File

@@ -1,3 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { IsOptional, IsString, IsNumber, IsDateString } from 'class-validator';
export class UpsertProfileDto {
@@ -20,6 +21,8 @@ export class CreateEnrollmentDto {
@IsOptional() @IsString() status?: string;
}
export class UpdateEnrollmentDto extends PartialType(CreateEnrollmentDto) {}
export class CreateExamScoreDto {
@IsString() examType: string;
@IsOptional() @IsString() examName?: string;
@@ -31,6 +34,8 @@ export class CreateExamScoreDto {
@IsOptional() @IsNumber() enrollmentId?: number;
}
export class UpdateExamScoreDto extends PartialType(CreateExamScoreDto) {}
export class CreateLearningRecordDto {
@IsDateString() recordDate: string;
@IsString() recordType: string;
@@ -39,6 +44,8 @@ export class CreateLearningRecordDto {
@IsOptional() @IsString() nextStep?: string;
}
export class UpdateLearningRecordDto extends PartialType(CreateLearningRecordDto) {}
export class UpsertResultDto {
@IsOptional() @IsNumber() cultureFinalScore?: number;
@IsOptional() @IsNumber() professionalFinalScore?: number;