Files
gongxue-base/apps/server/src/classes/classes.controller.ts

365 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, string> = {
[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;
}
}