feat: 拆分学号/身份证字段 + 考勤教师展示 + 代码优化

- 入住导入模板:学号和身份证号拆为独立字段,前后端对齐
- 排课查询关联教师,考勤归档页展示教师姓名
- 抽查时段增加 IsIn 校验
- 抽取 withPessimisticWriteLock 去重悲观锁查询
- import 增加文件空 buffer 校验
- 测试 mock 补全,适配事务 manager
- MySQL init.sql VALUES() 语法兼容修复
This commit is contained in:
2026-07-20 11:54:01 +08:00
parent 375c7ec60b
commit c7ba2799b9
12 changed files with 772 additions and 365 deletions

View File

@@ -971,7 +971,9 @@
.student-teacher-item {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
text-align: center;
min-width: 0;
padding: 10px;
border: 1px solid rgb(21 122 101 / 10%);
@@ -990,7 +992,9 @@
}
.student-teacher-item > div {
flex: 1;
min-width: 0;
text-align: center;
}
.student-teacher-item strong,

View File

@@ -74,9 +74,18 @@ const ADMIN_CORRECTION_OPTIONS = [
{ value: 'absent', label: '缺勤' },
];
interface ClassTeacherOption {
userId: number;
username: string | null;
name: string | null;
roleType: string;
subject: string | null;
}
interface ClassOption {
classId: number;
className: string;
teachers?: ClassTeacherOption[];
}
interface AttendanceRecordItem {
@@ -126,6 +135,8 @@ interface HistoryScheduleOption {
endDate: string;
subject: string;
teacherId: number | null;
teacherName?: string | null;
teacherUsername?: string | null;
status: string;
}
@@ -197,6 +208,28 @@ function displayAttendanceStatus(status?: string | null): string {
return status === 'pending' || !status ? 'absent' : status;
}
function getTeacherDisplayName(teacher?: {
name?: string | null;
username?: string | null;
}): string {
const name = teacher?.name?.trim();
if (name) return name;
return teacher?.username?.trim() || '未设置';
}
function formatTeacherNames(
teachers: readonly { name?: string | null; username?: string | null }[],
): string {
const names = [
...new Set(
teachers
.map((teacher) => getTeacherDisplayName(teacher))
.filter((name) => name && name !== '未设置'),
),
];
return names.length > 0 ? names.join('、') : '未设置';
}
function AttendanceStatusTag({ status }: { status: string }) {
const displayStatus = displayAttendanceStatus(status);
const meta = STATUS_META[displayStatus] ?? {
@@ -916,9 +949,30 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) =>
});
}, [metricFilter, studentPanels, studentSearch]);
const selectedClass = classId
? classOptions.find((item) => item.classId === classId)?.className || `班级 ${classId}`
: '全部班级';
const selectedClassOption = classId
? classOptions.find((item) => item.classId === classId)
: undefined;
const selectedClass = selectedClassOption?.className || (classId ? `班级 ${classId}` : '全部班级');
const overviewTeachers = selectedClassOption?.teachers ?? [];
const headTeacherNames = classId
? formatTeacherNames(overviewTeachers.filter((teacher) => teacher.roleType === 'head_teacher'))
: '请选择班级';
const lifeTeacherNames = classId
? formatTeacherNames(overviewTeachers.filter((teacher) => teacher.roleType === 'life_teacher'))
: '请选择班级';
const currentSchedule = scheduleId
? scheduleOptions.find((item) => item.id === scheduleId)
: undefined;
const subjectTeacherNames = currentSchedule
? getTeacherDisplayName({
name: currentSchedule.teacherName,
username: currentSchedule.teacherUsername,
})
: classId
? formatTeacherNames(
overviewTeachers.filter((teacher) => teacher.roleType === 'subject_teacher'),
)
: '请选择班级';
const attendanceRate =
summary.total > 0 ? Math.round((summary.present / summary.total) * 100) : 0;
const dateLabel = attendanceDate?.format('YYYY-MM-DD') || '未选择日期';
@@ -1121,21 +1175,21 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) =>
<Avatar></Avatar>
<div>
<span></span>
<strong></strong>
<strong>{headTeacherNames}</strong>
</div>
</div>
<div className="student-teacher-item is-muted">
<div className="student-teacher-item">
<Avatar></Avatar>
<div>
<span></span>
<strong></strong>
<strong>{lifeTeacherNames}</strong>
</div>
</div>
<div className="student-teacher-item">
<Avatar></Avatar>
<div>
<span></span>
<strong>{session ? sessionMap[session] : '全部时段'}</strong>
<span></span>
<strong>{subjectTeacherNames}</strong>
</div>
</div>
</div>