feat: 拆分学号/身份证字段 + 考勤教师展示 + 代码优化
- 入住导入模板:学号和身份证号拆为独立字段,前后端对齐 - 排课查询关联教师,考勤归档页展示教师姓名 - 抽查时段增加 IsIn 校验 - 抽取 withPessimisticWriteLock 去重悲观锁查询 - import 增加文件空 buffer 校验 - 测试 mock 补全,适配事务 manager - MySQL init.sql VALUES() 语法兼容修复
This commit is contained in:
@@ -45,6 +45,7 @@ const createService = () => {
|
||||
{} as never,
|
||||
sessionRepo as never,
|
||||
attendanceDeviceRepo as never,
|
||||
{} as never,
|
||||
dataSource as unknown as DataSource,
|
||||
);
|
||||
return { service, attendanceRepo, dingRawRepo, scheduleRepo, classStudentRepo, sessionRepo, attendanceDeviceRepo, dataSource };
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AttendanceService } from './attendance.service';
|
||||
import { AttendanceRecord } from '../entities/attendance-record.entity';
|
||||
import { AttendanceSession } from '../entities/attendance-session.entity';
|
||||
import { AttendanceDevice } from '../entities/attendance-device.entity';
|
||||
import { AttendancePeriodConfig } from '../entities/attendance-period-config.entity';
|
||||
import { DingAttendanceRaw } from '../entities/ding-attendance-raw.entity';
|
||||
import { Class } from '../entities/class.entity';
|
||||
import { Student } from '../entities/student.entity';
|
||||
@@ -59,6 +60,7 @@ describe('AttendanceService — batchCreate', () => {
|
||||
{ provide: getRepositoryToken(ClassTeacher), useValue: { findOne: jest.fn() } },
|
||||
{ provide: getRepositoryToken(AttendanceSession), useValue: {} },
|
||||
{ provide: getRepositoryToken(AttendanceDevice), useValue: mockAttendanceDeviceRepo },
|
||||
{ provide: getRepositoryToken(AttendancePeriodConfig), useValue: {} },
|
||||
{ provide: getDataSourceToken(), useValue: { transaction: jest.fn() } },
|
||||
],
|
||||
}).compile();
|
||||
@@ -140,6 +142,7 @@ describe('AttendanceService — teacher DingTalk class scope', () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -216,6 +219,7 @@ describe('AttendanceService — DingTalk raw query', () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -280,6 +284,7 @@ describe('AttendanceService — attendance device display mappings', () => {
|
||||
{} as never,
|
||||
attendanceDeviceRepo as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
);
|
||||
return { service, qb, attendanceDeviceRepo };
|
||||
}
|
||||
@@ -389,6 +394,7 @@ describe('AttendanceService — session serialization', () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{ find: jest.fn().mockResolvedValue([]) } as never,
|
||||
{} as never,
|
||||
dataSourceMock as never,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ClassSchedule,
|
||||
ClassStudent,
|
||||
ClassTeacher,
|
||||
TeacherRoleType,
|
||||
ScheduleType,
|
||||
StudentDingMapping,
|
||||
} from '../entities';
|
||||
@@ -970,8 +971,12 @@ export class AttendanceService {
|
||||
|
||||
async getScheduleOptionsForAttendance(classId: number, date: string) {
|
||||
const weekDay = this.getWeekDayForDate(date);
|
||||
return this.scheduleRepo
|
||||
const { entities, raw } = await this.scheduleRepo
|
||||
.createQueryBuilder('cs')
|
||||
.leftJoin('cs.teacher', 'teacher')
|
||||
.addSelect('cs.id', 'scheduleIdForTeacherMap')
|
||||
.addSelect('teacher.username', 'teacherUsername')
|
||||
.addSelect('teacher.name', 'teacherName')
|
||||
.where('cs.classId = :classId', { classId })
|
||||
.andWhere('cs.weekDay = :weekDay', { weekDay })
|
||||
.andWhere('cs.startDate <= :date', { date })
|
||||
@@ -979,7 +984,25 @@ export class AttendanceService {
|
||||
.andWhere('cs.status = :status', { status: 'active' })
|
||||
.orderBy('cs.startTime', 'ASC')
|
||||
.addOrderBy('cs.subject', 'ASC')
|
||||
.getMany();
|
||||
.getRawAndEntities();
|
||||
|
||||
const teacherByScheduleId = new Map(
|
||||
raw.map((row) => [
|
||||
Number(row.scheduleIdForTeacherMap),
|
||||
{
|
||||
teacherName: row.teacherName || null,
|
||||
teacherUsername: row.teacherUsername || null,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
return entities.map((schedule, index) => {
|
||||
const teacher = teacherByScheduleId.get(schedule.id) ?? {
|
||||
teacherName: raw[index]?.teacherName || null,
|
||||
teacherUsername: raw[index]?.teacherUsername || null,
|
||||
};
|
||||
return { ...schedule, ...teacher };
|
||||
});
|
||||
}
|
||||
|
||||
private async buildCalendar(classId: number, weekStart: string) {
|
||||
@@ -1095,9 +1118,51 @@ export class AttendanceService {
|
||||
if (classIds.length === 0) return [];
|
||||
|
||||
const where = { id: In(classIds) };
|
||||
const classes = await this.classRepo.find({ where });
|
||||
const [classes, teachers] = await Promise.all([
|
||||
this.classRepo.find({ where }),
|
||||
this.classTeacherRepo.find({
|
||||
where: {
|
||||
classId: In(classIds),
|
||||
roleType: In([
|
||||
TeacherRoleType.HEAD_TEACHER,
|
||||
TeacherRoleType.LIFE_TEACHER,
|
||||
TeacherRoleType.SUBJECT_TEACHER,
|
||||
]),
|
||||
},
|
||||
relations: ['user'],
|
||||
order: { roleType: 'ASC', id: 'ASC' },
|
||||
}),
|
||||
]);
|
||||
const nameMap = new Map(classes.map((c) => [c.id, c.name]));
|
||||
return classIds.map((id) => ({ classId: id, className: nameMap.get(id) || `班级${id}` }));
|
||||
const teacherMap = new Map<
|
||||
number,
|
||||
Array<{
|
||||
userId: number;
|
||||
username: string | null;
|
||||
name: string | null;
|
||||
roleType: string;
|
||||
subject: string | null;
|
||||
}>
|
||||
>();
|
||||
|
||||
for (const teacher of teachers) {
|
||||
const user = teacher.user as { username?: string | null; name?: string | null } | undefined;
|
||||
const items = teacherMap.get(teacher.classId) ?? [];
|
||||
items.push({
|
||||
userId: teacher.userId,
|
||||
username: user?.username || null,
|
||||
name: user?.name || null,
|
||||
roleType: teacher.roleType,
|
||||
subject: teacher.subject || null,
|
||||
});
|
||||
teacherMap.set(teacher.classId, items);
|
||||
}
|
||||
|
||||
return classIds.map((id) => ({
|
||||
classId: id,
|
||||
className: nameMap.get(id) || `班级${id}`,
|
||||
teachers: teacherMap.get(id) ?? [],
|
||||
}));
|
||||
}
|
||||
|
||||
// ── DingAttendance raw records ──
|
||||
|
||||
@@ -64,6 +64,7 @@ export class AttendanceRecordItem {
|
||||
attendanceDate: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['morning_reading', 'morning', 'afternoon', 'evening_study', 'night_check'])
|
||||
@IsNotEmpty()
|
||||
session: string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user