import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request, Res, } from '@nestjs/common'; import type { Response } from 'express'; import { ClassesService } from './classes.service'; import { CreateClassDto, UpdateClassDto, QueryClassDto, AddStudentsDto, AddTeacherDto, QueryClassScheduleDto, QueryClassAttendanceSummaryDto, BatchImportStudentsDto, } from './dto/class.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; import { extractRequestInfo } from '../common/request-utils'; import { RequirePermission } from '../auth/decorators/permission.decorator'; import { NotificationsService } from '../notifications/notifications.service'; import { NotificationType } from '../entities/notification.entity'; import { TeacherRoleType } from '../entities'; import * as ExcelJS from 'exceljs'; import { AuthorizationService, CaslAction, SubjectName, AuthenticatedUser } from '../authorization'; interface AuthenticatedRequest { user: AuthenticatedUser; } const teacherRoleLabels: Record = { [TeacherRoleType.SUBJECT_TEACHER]: '任课老师', [TeacherRoleType.HEAD_TEACHER]: '班主任', [TeacherRoleType.LIFE_TEACHER]: '生活老师', [TeacherRoleType.ACADEMIC_TEACHER]: '学服老师', }; @UseGuards(JwtAuthGuard) @Controller('classes') export class ClassesController { constructor( private readonly service: ClassesService, private readonly logService: OperationLogsService, private readonly notificationsService: NotificationsService, private readonly authz: AuthorizationService, ) {} private assertReadAccess(req: AuthenticatedRequest, classId: number) { // Legacy: Manage (super_admin) or Update (class:edit) grants broad class access const canManageAll = this.authz.can(req, CaslAction.Manage, SubjectName.Class) || this.authz.can(req, CaslAction.Update, SubjectName.Class); return this.service.assertClassAccess(req.user.id, classId, canManageAll); } @Get() @RequirePermission('class:view') async findAll(@Query() query: QueryClassDto, @Request() req: AuthenticatedRequest) { const classIds = await this.service.getAccessibleClassIds( req.user.id, this.authz.can(req, CaslAction.Manage, SubjectName.Class) || this.authz.can(req, CaslAction.Update, SubjectName.Class), ); return this.service.findAll(query, classIds); } @Get(':id') @RequirePermission('class:view') async findOne(@Param('id') id: string, @Request() req: AuthenticatedRequest) { await this.assertReadAccess(req, +id); return this.service.findOne(+id); } @Get(':id/schedule') @RequirePermission('class:view') async getSchedule( @Param('id') id: string, @Query() query: QueryClassScheduleDto, @Request() req: AuthenticatedRequest, ) { await this.assertReadAccess(req, +id); return this.service.getSchedule(+id, query); } @Get(':id/attendance-summary') @RequirePermission('class:view') async getAttendanceSummary( @Param('id') id: string, @Query() query: QueryClassAttendanceSummaryDto, @Request() req: AuthenticatedRequest, ) { await this.assertReadAccess(req, +id); return this.service.getAttendanceSummary(+id, query); } @Post() @RequirePermission('class:create') async create(@Body() dto: CreateClassDto, @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: 'class', detail: `班级${result.code} ${result.name}`, ipAddress, userAgent, }); return result; } /** 批量导入学生到班级(通过钉钉用户ID) */ @Post(':id/students/import') @RequirePermission('class:edit') async batchImportStudents(@Param('id') id: string, @Body() dto: BatchImportStudentsDto) { return this.service.batchImportStudents(+id, dto.users); } /** 归档班级 */ @Put(':id/archive') @RequirePermission('class:edit') async archive(@Param('id') id: string) { return this.service.archive(+id); } /** 取消归档 */ @Put(':id/restore') @RequirePermission('class:edit') async restore(@Param('id') id: string) { return this.service.restore(+id); } @Put(':id') @RequirePermission('class:edit') async update(@Param('id') id: string, @Body() dto: UpdateClassDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); 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: 'class', detail: JSON.stringify(dto), ipAddress, userAgent, }); return result; } @Delete(':id') @RequirePermission('class:delete') async remove(@Param('id') id: string, @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: 'class', ipAddress, userAgent, }); return result; } @Get(':id/roster/export') @RequirePermission('class:view') async exportRoster( @Param('id') id: string, @Res() res: Response, @Request() req: AuthenticatedRequest, ) { await this.assertReadAccess(req, +id); const classEntity = await this.service.findOne(+id); const classStudents = await this.service.getStudents(+id); const workbook = new ExcelJS.Workbook(); const ws = workbook.addWorksheet('班级花名册'); ws.columns = [ { header: '姓名', key: 'name', width: 15 }, { header: '学号', key: 'studentNo', width: 15 }, { header: '加入日期', key: 'joinDate', width: 15 }, { header: '状态', key: 'status', width: 10 }, ]; ws.getRow(1).font = { bold: true }; ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } }; for (const cs of classStudents) { ws.addRow({ name: cs.student?.name || '', studentNo: cs.student?.idNumber || '', joinDate: cs.joinDate || '', status: cs.status || '', }); } res.setHeader( 'Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ); res.setHeader( 'Content-Disposition', `attachment; filename=${encodeURIComponent(`班级花名册-${classEntity.name}`)}.xlsx`, ); await workbook.xlsx.write(res); res.end(); } @Get(':id/students') @RequirePermission('class:view') async getStudents(@Param('id') id: string, @Request() req: AuthenticatedRequest) { await this.assertReadAccess(req, +id); return this.service.getStudents(+id); } @Post(':id/students') @RequirePermission('class:edit') async addStudents(@Param('id') id: string, @Body() dto: AddStudentsDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.addStudents(+id, dto.studentIds); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '班级管理', action: '添加学生', targetId: +id, targetType: 'class', detail: `新增${result.added}名学生`, ipAddress, userAgent, }); try { const cls = await this.service.findOne(+id); if (cls.headTeacherId) { void this.notificationsService.create({ recipientIds: [cls.headTeacherId], type: NotificationType.CLASS_CHANGE, title: '学员变动', content: `班级新增${result.added}名学生`, }); } } catch {} return result; } @Delete(':id/students/:studentId') @RequirePermission('class:edit') async removeStudent( @Param('id') id: string, @Param('studentId') studentId: string, @Request() req: any, ) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.removeStudent(+id, +studentId); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '班级管理', action: '移除学生', targetId: +id, targetType: 'class', detail: `移除学生${studentId}`, ipAddress, userAgent, }); return result; } @Get(':id/teachers') @RequirePermission('class:view') async getTeachers(@Param('id') id: string, @Request() req: AuthenticatedRequest) { await this.assertReadAccess(req, +id); return this.service.getTeachers(+id); } @Post(':id/teachers') @RequirePermission('class:edit') async addTeacher(@Param('id') id: string, @Body() dto: AddTeacherDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.addTeacher(+id, dto); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '班级管理', action: '添加教师', targetId: +id, targetType: 'class', detail: `教师${dto.userId} 角色${dto.roleType}`, ipAddress, userAgent, }); try { void this.notificationsService.create({ recipientIds: [dto.userId], type: NotificationType.CLASS_CHANGE, title: '班级分配', content: `您已被分配到班级担任${teacherRoleLabels[dto.roleType] ?? dto.roleType}角色`, }); } catch {} return result; } @Delete(':id/teacher-assignments/:assignmentId') @RequirePermission('class:edit') async removeTeacherAssignment( @Param('id') id: string, @Param('assignmentId') assignmentId: string, @Request() req: any, ) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.removeTeacherAssignment(+id, +assignmentId); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '班级管理', action: '移除教师角色', targetId: +id, targetType: 'class', detail: `移除教师分配${assignmentId}`, ipAddress, userAgent, }); return result; } @Delete(':id/teachers/:userId') @RequirePermission('class:edit') async removeTeacher( @Param('id') id: string, @Param('userId') userId: string, @Request() req: any, ) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.removeTeacher(+id, +userId); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '班级管理', action: '移除教师', targetId: +id, targetType: 'class', detail: `移除教师${userId}`, ipAddress, userAgent, }); return result; } }