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, ) {} @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 = { 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); } }