forked from wangziqi/gongxue-base
365 lines
11 KiB
TypeScript
365 lines
11 KiB
TypeScript
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;
|
||
}
|
||
}
|