import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request, Res, } from '@nestjs/common'; import type { Response } from 'express'; import { AttendanceService } from './attendance.service'; import { BatchCreateAttendanceDto, AttendanceSummaryQueryDto, AttendanceCalendarQueryDto, QueryAttendanceRecordsDto, QueryDingRawDto, MatchDingRecordDto, AttendanceReportQueryDto, UpdateAttendanceRecordDto, } from './dto/attendance.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() export class AttendanceController { constructor( private readonly service: AttendanceService, private readonly logService: OperationLogsService, ) {} // ── Batch create attendance records ── @Post('attendance-records/batch') @RequirePermission('attendance:create') async batchCreate( @Body() dto: BatchCreateAttendanceDto, @Request() req: any, ) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.batchCreate(dto); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '考勤管理', action: '批量录入考勤', detail: `共 ${result.count} 条`, ipAddress, userAgent, }); return result; } // ── Export attendance records ── @Get('attendance-records/export') @RequirePermission('attendance:export') async exportRecords( @Query() query: QueryAttendanceRecordsDto, @Res() res: Response, ) { const records = await this.service.findAllForExport(query); const workbook = new ExcelJS.Workbook(); const ws = workbook.addWorksheet('考勤统计报表'); ws.columns = [ { header: '姓名', key: 'studentName', width: 15 }, { header: '班级', key: 'className', width: 20 }, { header: '日期', key: 'attendanceDate', width: 15 }, { header: '时段', key: 'session', width: 15 }, { header: '状态', key: 'status', width: 10 }, { header: '来源', key: 'source', width: 10 }, { header: '备注', key: 'remark', width: 30 }, { header: '打卡时间', key: 'createdAt', width: 20 }, ]; ws.getRow(1).font = { bold: true }; ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } }; for (const record of records) { ws.addRow({ studentName: record.student?.name || '', className: record.class?.name || '', attendanceDate: record.attendanceDate || '', session: record.session || '', status: record.status || '', source: record.source || '', remark: record.remark || '', createdAt: record.createdAt ? record.createdAt.toISOString().replace('T', ' ').substring(0, 19) : '', }); } const dateRange = [query.dateFrom, query.dateTo] .filter(Boolean) .join('-') || '全部'; res.setHeader( 'Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ); res.setHeader( 'Content-Disposition', `attachment; filename=${encodeURIComponent(`考勤统计报表-${dateRange}`)}.xlsx`, ); await workbook.xlsx.write(res); res.end(); } // ── List attendance records with filters ── @Get('attendance-records') @RequirePermission('attendance:view') findAll(@Query() query: QueryAttendanceRecordsDto) { return this.service.findAll(query); } // ── Update a single attendance record ── @Put('attendance-records/:id') @RequirePermission('attendance:edit') async update( @Param('id') id: string, @Body() dto: UpdateAttendanceRecordDto, @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: 'attendanceRecord', detail: `状态=${result.status}, 备注=${result.remark || ''}`, ipAddress, userAgent, }); return result; } // ── Delete a single attendance record ── @Delete('attendance-records/:id') @RequirePermission('attendance:edit') 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: 'attendanceRecord', detail: `删除考勤记录 ${id}`, ipAddress, userAgent, }); return result; } // ── Get distinct classes with attendance records ── @Get('attendance-records/classes') @RequirePermission('attendance:view') getClasses() { return this.service.getClasses(); } // ── Attendance summary ── @Get('attendance-records/summary') @RequirePermission('attendance:view') getSummary(@Query() query: AttendanceSummaryQueryDto) { return this.service.getSummary(query); } // ── Attendance calendar ── @Get('attendance-records/calendar') @RequirePermission('attendance:view') getCalendar(@Query() query: AttendanceCalendarQueryDto) { return this.service.getCalendar(query); } // ── DingAttendance raw records ── @Get('ding-attendance-raw') @RequirePermission('attendance:view') getDingRaw(@Query() query: QueryDingRawDto) { return this.service.getDingRaw(query); } // ── Match a dingtalk record to a student ── @Post('ding-attendance-raw/:id/match') @RequirePermission('attendance:edit') async matchDingRecord( @Param('id') id: string, @Body() dto: MatchDingRecordDto, @Request() req: any, ) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.matchDingRecord(+id, dto); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '考勤管理', action: '匹配考勤记录', targetId: +id, targetType: 'dingAttendanceRaw', detail: `匹配到学生 ${dto.studentId}`, ipAddress, userAgent, }); return result; } // ── Attendance class-based report export ── @Get('attendance-records/report') @RequirePermission('attendance:export') async exportReport( @Query() query: AttendanceReportQueryDto, @Res() res: Response, @Request() req: any, ) { const reportData = await this.service.getReport(query); const workbook = new ExcelJS.Workbook(); const ws = workbook.addWorksheet('考勤统计报表'); ws.columns = [ { header: '班级名称', key: 'className', width: 30 }, { header: '总记录数', key: 'total', width: 12 }, { header: '出勤', key: 'present', width: 10 }, { header: '出勤率', key: 'presentRate', width: 10 }, { header: '缺勤', key: 'absent', width: 10 }, { header: '缺勤率', key: 'absentRate', width: 10 }, { header: '迟到', key: 'late', width: 10 }, { header: '迟到率', key: 'lateRate', width: 10 }, { header: '请假', key: 'leave', width: 10 }, { header: '请假率', key: 'leaveRate', width: 10 }, ]; ws.getRow(1).font = { bold: true }; ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } }; for (const row of reportData) { ws.addRow({ className: row.className, total: row.total, present: row.present, presentRate: `${row.presentRate}%`, absent: row.absent, absentRate: `${row.absentRate}%`, late: row.late, lateRate: `${row.lateRate}%`, leave: row.leave, leaveRate: `${row.leaveRate}%`, }); } // Audit log const { ipAddress, userAgent } = extractRequestInfo(req); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '考勤管理', action: '导出考勤报表', detail: `classId=${query.classId || '全部'} ${query.dateFrom || ''}~${query.dateTo || ''}`, ipAddress, userAgent, }); res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', 'attachment; filename=attendance-report.xlsx'); await workbook.xlsx.write(res); res.end(); } // ── Abnormal attendance alerts ── @Get('attendance-records/alerts') @RequirePermission('attendance:view') getAlerts( @Query('days') days?: string, @Query('threshold') threshold?: string, ) { return this.service.getAlerts( days ? +days : 14, threshold ? +threshold : 3, ); } @Post('ding-attendance-raw/auto-match') @RequirePermission('attendance:edit') async autoMatch() { return this.service.autoMatchDingRecords(); } }