feat: 空状态引导全面应用与考勤批量标记

admin:
- 17 个列表页空态统一为 QueryEmpty 引导:学生/账单/入住/费用/押金/
  教室/房间/班级/考试/排课/机构/考勤机/钱包/租赁/通知/角色等,
  有创建权限的页面附带主操作按钮,无权限时纯展示
- 教师端课堂点名新增「全部已打卡/全部未打卡」批量按钮:
  仅作用于状态不一致的记录,确认后调用批量接口,展示成功/失败数量

server:
- 新增 PUT /attendance-records/batch-status 批量改状态接口
  (ids ≤200,逐条权限校验与会话锁,部分失败返回 failedIds,
  审计日志记录批量结果;路由声明在 :id 之前避免被捕获)

aislop scan: 5 引擎 0 issues
This commit is contained in:
2026-08-07 18:00:29 +08:00
parent c10476d203
commit 1b4ba893fd
23 changed files with 350 additions and 77 deletions

View File

@@ -17,6 +17,7 @@ import {
AttendanceReportQueryDto,
AttendanceAlertsQueryDto,
UpdateAttendanceRecordDto,
BatchUpdateAttendanceStatusDto,
GenerateFromSchedulesDto,
RefreshDingTalkAttendanceDto,
} from './dto/attendance.dto';
@@ -232,6 +233,36 @@ export class AttendanceRecordsController extends AttendanceControllerBase {
return this.service.findAll(query, await this.getAccessibleClassIds(req));
}
// ── Batch update attendance record statuses (纠错/点名) ──
// 注意:必须在 :id 路由之前声明,否则 "batch-status" 会被 :id(ParseIntPipe) 捕获
@Put('attendance-records/batch-status')
@RequirePermission('attendance:edit', 'attendance:self-edit')
async batchUpdateStatus(
@Body() dto: BatchUpdateAttendanceStatusDto,
@Request() req: any,
) {
const failedIds: number[] = [];
let updated = 0;
for (const id of dto.ids) {
try {
const existing = await this.service.findAttendanceRecord(id);
if (existing.classId == null && !this.canManageAllAttendance(req)) {
throw new ForbiddenException('无权修改未关联班级的考勤记录');
}
if (existing.classId != null) await this.assertClassAccess(req, existing.classId);
await this.service.update(id, { status: dto.status, remark: dto.remark });
updated += 1;
} catch {
failedIds.push(id);
}
}
await logAudit(this.logService, req, {
module: '考勤管理', action: '批量修改考勤状态', targetId: 0, targetType: 'attendanceRecord',
detail: `批量 ${dto.ids.length} 条 → ${dto.status},成功 ${updated},失败 ${failedIds.length}`,
});
return { updated, failed: failedIds.length, failedIds };
}
// ── Update a single attendance record ──
@Put('attendance-records/:id')
@RequirePermission('attendance:edit', 'attendance:self-edit')

View File

@@ -9,8 +9,11 @@ import {
ValidateNested,
IsNotEmpty,
ArrayNotEmpty,
ArrayMinSize,
ArrayMaxSize,
Matches,
Max,
MaxLength,
Min,
} from 'class-validator';
import { Type } from 'class-transformer';
@@ -248,6 +251,25 @@ export class UpdateAttendanceRecordDto {
remark?: string;
}
export class BatchUpdateAttendanceStatusDto {
/** 考勤记录 ID 列表(最多 200 条) */
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(200)
@IsInt({ each: true })
@Min(1, { each: true })
ids: number[];
@IsString()
@IsIn(['present', 'late', 'absent', 'leave'])
status: string;
@IsOptional()
@IsString()
@MaxLength(200)
remark?: string;
}
export class AttendanceAlertsQueryDto {
@IsOptional()