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

306 lines
10 KiB
TypeScript

import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
UseGuards,
Query,
Request,
Res,
UseInterceptors,
UploadedFile,
Inject,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Tenant } from '../entities/tenant.entity';
import { FileInterceptor } from '@nestjs/platform-express';
import type { Response } from 'express';
import { StudentsService } from './students.service';
import { CreateStudentDto, 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 { RequirePermission } from '../auth/decorators/permission.decorator';
import * as ExcelJS from 'exceljs';
@UseGuards(JwtAuthGuard)
@Controller('students')
export class StudentsController {
constructor(
private service: StudentsService,
private logService: OperationLogsService,
@InjectRepository(Tenant) private tenantRepo: Repository<Tenant>,
) {}
@Get()
@RequirePermission('student:view')
findAll(
@Query('name') name?: string,
@Query('status') status?: string,
@Query('includeArchived') includeArchived?: string,
) {
return this.service.findAll({ name, status, includeArchived: includeArchived === 'true' });
}
@Get('export')
@RequirePermission('student:export')
async exportExcel(@Query('includeArchived') includeArchived?: string, @Res() res?: Response, @Request() req?: any) {
const students = await this.service.findAll({ includeArchived: includeArchived === 'true' });
const workbook = new ExcelJS.Workbook();
const ws = workbook.addWorksheet('学生名单');
ws.columns = [
{ header: '姓名', key: 'name', width: 12 },
{ header: '性别', key: 'gender', width: 8 },
{ header: '电话', key: 'phone', width: 18 },
{ header: '学号/身份证', key: 'idNumber', width: 22 },
{ header: '民族', key: 'ethnicity', width: 10 },
{ header: '紧急联系人', key: 'emergencyContact', width: 15 },
{ header: '紧急联系人电话', key: 'emergencyPhone', width: 18 },
{ header: '所属机构', key: 'tenant', width: 18 },
{ header: '负责人/班主任', key: 'supervisor', width: 15 },
{ header: '状态', key: 'status', width: 10 },
];
ws.getRow(1).font = { bold: true };
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
const statusMap: Record<string, string> = {
active: '在读',
graduated: '已毕业',
withdrawn: '已退训',
archived: '已归档',
};
for (const s of students) {
ws.addRow({
name: s.name,
gender: s.gender || '',
phone: s.phone || '',
idNumber: s.idNumber || '',
ethnicity: s.ethnicity || '',
emergencyContact: s.emergencyContact || '',
emergencyPhone: s.emergencyPhone || '',
tenant: s.tenant?.name || '',
supervisor: s.supervisor || '',
status: statusMap[s.status] || s.status,
});
}
const { ipAddress, userAgent } = extractRequestInfo(req);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '导出学生',
detail: `导出 ${students.length} 名学生`,
ipAddress,
userAgent,
});
res!.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
res!.setHeader('Content-Disposition', 'attachment; filename=students.xlsx');
await workbook.xlsx.write(res!);
res!.end();
}
@Get('template')
@RequirePermission('student:view')
async downloadTemplate(@Res() res: Response) {
const workbook = new ExcelJS.Workbook();
const ws = workbook.addWorksheet('学生导入模板');
ws.columns = [
{ header: '姓名', key: 'name', width: 15 },
{ header: '电话', key: 'phone', width: 18 },
{ header: '学号/身份证', key: 'idNumber', width: 22 },
{ header: '性别', key: 'gender', width: 8 },
{ header: '民族', key: 'ethnicity', width: 10 },
{ header: '紧急联系人', key: 'emergencyContact', width: 15 },
{ header: '紧急联系人电话', key: 'emergencyPhone', width: 18 },
{ header: '所属机构(租赁方名称)', key: 'tenant', width: 18 },
{ header: '负责人/班主任', key: 'supervisor', width: 15 },
];
ws.getRow(1).font = { bold: true };
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
ws.addRow({
name: '张三',
phone: '13800138000',
idNumber: '2024001',
gender: '男',
ethnicity: '汉族',
emergencyContact: '张父',
emergencyPhone: '13900000000',
tenant: 'XX教育公司',
supervisor: '',
});
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
res.setHeader('Content-Disposition', 'attachment; filename=student_template.xlsx');
await workbook.xlsx.write(res);
res.end();
}
@Get(':id')
@RequirePermission('student:view')
findOne(@Param('id') id: string) {
return this.service.findOne(+id);
}
@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,
});
return result;
}
@Put(':id')
@RequirePermission('student:edit')
async update(@Param('id') id: string, @Body() dto: UpdateStudentDto, @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: 'student',
detail: JSON.stringify(dto),
ipAddress,
userAgent,
});
return result;
}
@Delete(':id')
@RequirePermission('student: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: 'student',
ipAddress,
userAgent,
});
return result;
}
@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,
});
return result;
}
@Put(':id/restore')
@RequirePermission('student:edit')
async restore(@Param('id') id: string, @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,
});
return result;
}
@Post('import')
@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);
const ws = workbook.worksheets[0];
const rows: {
name: string;
phone?: string;
idNumber?: string;
gender?: string;
ethnicity?: string;
emergencyContact?: string;
emergencyPhone?: string;
tenant?: string;
supervisor?: string;
tenantId?: number;
}[] = [];
ws.eachRow((row, idx) => {
if (idx === 1) return;
rows.push({
name: String(row.getCell(1).value || ''),
phone: String(row.getCell(2).value || ''),
idNumber: String(row.getCell(3).value || ''),
gender: String(row.getCell(4).value || '').trim() || undefined,
ethnicity: String(row.getCell(5).value || '').trim() || undefined,
emergencyContact: String(row.getCell(6).value || '').trim() || undefined,
emergencyPhone: String(row.getCell(7).value || '').trim() || undefined,
tenant: String(row.getCell(8).value || '').trim() || undefined,
supervisor: String(row.getCell(9).value || '').trim() || undefined,
});
});
// Resolve tenant names to IDs
for (const row of rows) {
if (row.tenant) {
const tenant = await this.tenantRepo.findOne({ where: { name: row.tenant } });
if (tenant) {
row.tenantId = tenant.id;
}
}
}
const result = await this.service.batchImport(rows);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '学生管理',
action: '导入学生',
detail: result.message,
ipAddress,
userAgent,
});
return result;
}
@Get(':id/compare-classes')
@RequirePermission('student:view')
compareClasses(@Param('id') id: string) {
return this.service.compareClasses(+id);
}
}