feat: 空状态引导全面应用与考勤批量标记
admin: - 17 个列表页空态统一为 QueryEmpty 引导:学生/账单/入住/费用/押金/ 教室/房间/班级/考试/排课/机构/考勤机/钱包/租赁/通知/角色等, 有创建权限的页面附带主操作按钮,无权限时纯展示 - 教师端课堂点名新增「全部已打卡/全部未打卡」批量按钮: 仅作用于状态不一致的记录,确认后调用批量接口,展示成功/失败数量 server: - 新增 PUT /attendance-records/batch-status 批量改状态接口 (ids ≤200,逐条权限校验与会话锁,部分失败返回 failedIds, 审计日志记录批量结果;路由声明在 :id 之前避免被捕获) aislop scan: 5 引擎 0 issues
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user