feat: 重构各业务模块管理页面与服务

This commit is contained in:
2026-08-05 17:12:00 +08:00
parent 80e6fccf05
commit fd39e1686a
163 changed files with 18409 additions and 13449 deletions

View File

@@ -12,7 +12,6 @@ import {
Res,
UseInterceptors,
UploadedFile,
Inject,
ParseIntPipe,
UsePipes,
ValidationPipe,
@@ -20,17 +19,20 @@ import {
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Organization } from '../entities/organization.entity';
import { ClassTeacher } from '../entities/class-teacher.entity';
import { FileInterceptor } from '@nestjs/platform-express';
import type { Response } from 'express';
import { StudentsService } from './students.service';
import { CreateStudentDto, QueryStudentDto, UpdateStudentDto } from './dto/student.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
import { extractRequestInfo } from '../common/request-utils';
import { logAudit } from '../common/with-audit-log';
import { RequirePermission } from '../auth/decorators/permission.decorator';
import { AuthorizationService, CaslAction, SubjectName } from '../authorization';
import type { AuthenticatedUser } from '../authorization';
import {
AuthorizationService,
CaslAction,
SubjectName,
type AuthenticatedUser,
} from '../authorization';
import * as ExcelJS from 'exceljs';
import {
createStudentImportTemplateWorkbook,
@@ -79,27 +81,17 @@ export class StudentsController {
@Get()
@RequirePermission('student:view')
async findAll(
@Query() query: QueryStudentDto,
@Request() req: AuthenticatedRequest,
) {
async findAll(@Query() query: QueryStudentDto, @Request() req: AuthenticatedRequest) {
const classIds = await this.service.getAccessibleClassIds(
req.user.id,
this.canManageAllStudents(req),
);
return this.service.findAll(
query,
classIds,
);
return this.service.findAll(query, classIds);
}
@Get('export')
@RequirePermission('student:export')
async exportExcel(
@Query() query: QueryStudentDto,
@Res() res?: Response,
@Request() req?: any,
) {
async exportExcel(@Query() query: QueryStudentDto, @Res() res?: Response, @Request() req?: any) {
const classIds = await this.service.getAccessibleClassIds(
req.user.id,
this.canManageAllStudents(req),
@@ -142,15 +134,8 @@ export class StudentsController {
admittedMajor: result?.admittedMajor || '',
});
}
const { ipAddress, userAgent } = extractRequestInfo(req);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '导出学生',
detail: `导出 ${students.length} 名学生`,
ipAddress,
userAgent,
await logAudit(this.logService, req, {
module: '学生管理', action: '导出学生', detail: `导出 ${students.length} 名学生`,
});
res!.setHeader(
'Content-Type',
@@ -183,18 +168,9 @@ export class StudentsController {
@Post()
@RequirePermission('student:create')
async create(@Body() dto: CreateStudentDto, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.create(dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '新增学生',
targetId: result.id,
targetType: 'student',
detail: `姓名: ${dto.name}, 电话: ${dto.phone || '无'}, 学号: ${dto.idNumber || '无'}`,
ipAddress,
userAgent,
await logAudit(this.logService, req, {
module: '学生管理', action: '新增学生', targetId: result.id, targetType: 'student', detail: `姓名: ${dto.name}, 电话: ${dto.phone || '无'}, 学号: ${dto.idNumber || '无'}`,
});
return result;
}
@@ -203,35 +179,23 @@ export class StudentsController {
@RequirePermission('student:edit')
@UsePipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }))
async batchRestore(@Body() dto: BatchIdsDto, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.batchRestore(dto.ids);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '批量恢复学生',
detail: `IDs: ${dto.ids.join(',')}`,
ipAddress,
userAgent,
await logAudit(this.logService, req, {
module: '学生管理', action: '批量恢复学生', detail: `IDs: ${dto.ids.join(',')}`,
});
return result;
}
@Put(':id')
@RequirePermission('student:edit')
async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateStudentDto, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
async update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateStudentDto,
@Request() req: any,
) {
const result = await this.service.update(id, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '编辑学生',
targetId: id,
targetType: 'student',
detail: JSON.stringify(dto),
ipAddress,
userAgent,
await logAudit(this.logService, req, {
module: '学生管理', action: '编辑学生', targetId: id, targetType: 'student', detail: JSON.stringify(dto),
});
return result;
}
@@ -239,17 +203,9 @@ export class StudentsController {
@Delete(':id')
@RequirePermission('student:delete')
async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.remove(id);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '归档学生',
targetId: id,
targetType: 'student',
ipAddress,
userAgent,
await logAudit(this.logService, req, {
module: '学生管理', action: '归档学生', targetId: id, targetType: 'student',
});
return result;
}
@@ -257,16 +213,29 @@ export class StudentsController {
@Post('batch-delete')
@RequirePermission('student:delete')
async batchRemove(@Body() body: { ids: number[] }, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.batchRemove(body.ids || []);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '批量归档学生',
detail: `IDs: ${(body.ids || []).join(',')}`,
ipAddress,
userAgent,
await logAudit(this.logService, req, {
module: '学生管理', action: '批量归档学生', detail: `IDs: ${(body.ids || []).join(',')}`,
});
return result;
}
@Delete(':id/permanent')
@RequirePermission('student:purge')
async purge(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
const result = await this.service.purge(id);
await logAudit(this.logService, req, {
module: '学生管理', action: '永久删除学生', targetId: id, targetType: 'student', detail: '物理删除,不可恢复',
});
return result;
}
@Post('batch-permanent-delete')
@RequirePermission('student:purge')
async batchPurge(@Body() body: { ids: number[] }, @Request() req: any) {
const result = await this.service.batchPurge(body.ids || []);
await logAudit(this.logService, req, {
module: '学生管理', action: '批量永久删除学生', detail: `IDs: ${(body.ids || []).join(',')}`,
});
return result;
}
@@ -274,17 +243,9 @@ export class StudentsController {
@Put(':id/restore')
@RequirePermission('student:edit')
async restore(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.restore(id);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '恢复学生',
targetId: id,
targetType: 'student',
ipAddress,
userAgent,
await logAudit(this.logService, req, {
module: '学生管理', action: '恢复学生', targetId: id, targetType: 'student',
});
return result;
}
@@ -293,9 +254,8 @@ export class StudentsController {
@RequirePermission('student:import')
@UseInterceptors(FileInterceptor('file'))
async importExcel(@UploadedFile() file: Express.Multer.File, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(file.buffer as any);
await workbook.xlsx.load(file.buffer.buffer as ArrayBuffer);
const importData = parseStudentImportWorkbook(workbook);
// Resolve organization names to IDs
for (const row of importData.students) {
@@ -309,14 +269,8 @@ export class StudentsController {
}
}
const result = await this.service.batchImport(importData);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '导入学生',
detail: result.message,
ipAddress,
userAgent,
await logAudit(this.logService, req, {
module: '学生管理', action: '导入学生', detail: result.message,
});
return result;
}
@@ -325,9 +279,8 @@ export class StudentsController {
@RequirePermission('student:import')
@UseInterceptors(FileInterceptor('file'))
async matchImport(@UploadedFile() file: Express.Multer.File, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(file.buffer as unknown as ArrayBuffer);
await workbook.xlsx.load(file.buffer.buffer as ArrayBuffer);
const importData = parseStudentImportWorkbook(workbook);
// Resolve organization names to IDs
for (const row of importData.students) {
@@ -339,14 +292,8 @@ export class StudentsController {
}
}
const result = await this.service.matchImport(importData);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '更新已有学生资料',
detail: result.message,
ipAddress,
userAgent,
await logAudit(this.logService, req, {
module: '学生管理', action: '更新已有学生资料', detail: result.message,
});
return result;
}