diff --git a/apps/admin/src/pages/IntegrationConfig/index.tsx b/apps/admin/src/pages/IntegrationConfig/index.tsx index 0a9b1fb..5c45180 100644 --- a/apps/admin/src/pages/IntegrationConfig/index.tsx +++ b/apps/admin/src/pages/IntegrationConfig/index.tsx @@ -6,9 +6,10 @@ import { } from 'antd'; import { SaveOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined, - SyncOutlined, PlusOutlined, BankOutlined, UserOutlined, + SyncOutlined, BankOutlined, UserOutlined, } from '@ant-design/icons'; import type { DataNode } from 'antd/es/tree'; +import type { TreeSelectProps } from 'antd/es/tree-select'; import api from '../../api'; interface DingTalkConfig { @@ -42,6 +43,8 @@ interface OrgTreeWithUsersResponse { data: DingOrgTreeNodeExt[]; } +type DeptPickerTreeNode = NonNullable['treeData']>[number]; + interface ClassItem { id: number; name: string; @@ -72,7 +75,7 @@ const IntegrationConfigPage: React.FC = () => { const [drawerOpen, setDrawerOpen] = useState(false); const [fetchingTree, setFetchingTree] = useState(false); const [importing, setImporting] = useState(false); - const [deptPickerTree, setDeptPickerTree] = useState }>>([]); + const [deptPickerTree, setDeptPickerTree] = useState([]); const [checkedKeys, setCheckedKeys] = useState([]); const [selectedClassId, setSelectedClassId] = useState(null); const [classes, setClasses] = useState([]); @@ -140,7 +143,7 @@ const IntegrationConfigPage: React.FC = () => { try { const res = await api.get('/sync/dingtalk/org-tree'); if (res.success && res.data) { - const toTreeNode = (nodes: OrgTreeNodeRaw[]): Array<{ title: string; value: number; children?: Array<{ title: string; value: number; children?: unknown[] }> }> => + const toTreeNode = (nodes: OrgTreeNodeRaw[]): DeptPickerTreeNode[] => nodes.map((n) => ({ title: n.name, value: n.id, @@ -188,23 +191,36 @@ const IntegrationConfigPage: React.FC = () => { }; const buildTreeData = useCallback((nodes: DingOrgTreeNodeExt[]): DataNode[] => { - return nodes.map((node) => ({ - title: ( - - - {node.name} - {node.users.length}人 - - ), - key: `dept-${node.id}`, - children: [ - ...buildTreeData(node.children), - ...node.users.map((u) => ({ - title: {u.name}{u.mobile}, + return nodes.map((node) => { + const users = node.users ?? []; + const children: DataNode[] = [ + ...buildTreeData(node.children ?? []), + ...users.map((u) => ({ + title: ( + + + {u.name} + {u.mobile ? {u.mobile} : null} + + ), key: `user-${u.userid}`, + isLeaf: true, })), - ], - })); + ]; + return { + title: ( + + + {node.name} + {users.length}人 + + ), + key: `dept-${node.id}`, + // Only attach children when there are any, so empty/leaf departments + // don't render a phantom expand arrow that opens to nothing. + ...(children.length > 0 ? { children } : {}), + }; + }); }, []); const treeData = useMemo(() => buildTreeData(orgTree), [orgTree, buildTreeData]); @@ -213,12 +229,12 @@ const IntegrationConfigPage: React.FC = () => { const result: Array<{ dingUserId: string; name: string; mobile?: string }> = []; const walk = (nodes: DingOrgTreeNodeExt[]) => { for (const node of nodes) { - for (const u of node.users) { + for (const u of node.users ?? []) { if (checkedKeys.includes(`user-${u.userid}`)) { result.push({ dingUserId: u.userid, name: u.name, mobile: u.mobile || undefined }); } } - walk(node.children); + walk(node.children ?? []); } }; walk(orgTree); diff --git a/apps/admin/src/pages/Schedules/index.tsx b/apps/admin/src/pages/Schedules/index.tsx index f20b225..53046ea 100644 --- a/apps/admin/src/pages/Schedules/index.tsx +++ b/apps/admin/src/pages/Schedules/index.tsx @@ -71,7 +71,7 @@ interface ScheduleSyncResult { groupCount: number; syncedItems: number; skippedNoMapping: number; - groups: Array<{ deptName: string; groupId: number; itemCount: number }>; + groups: Array<{ className: string; groupId: number; itemCount: number }>; } const WEEKDAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; @@ -112,12 +112,12 @@ const SchedulesPage: React.FC = () => { const [syncModalOpen, setSyncModalOpen] = useState(false); const [syncing, setSyncing] = useState(false); const [syncStatus, setSyncStatus] = useState<{ - activeSchedules: number; mappedTeachers: number; totalTeachers: number; + activeSchedules: number; mappedClasses: number; totalClasses: number; } | null>(null); const [syncResult, setSyncResult] = useState<{ scheduleCount: number; shiftCount: number; groupCount: number; syncedItems: number; skippedNoMapping: number; - groups: Array<{ deptName: string; groupId: number; itemCount: number }>; + groups: Array<{ className: string; groupId: number; itemCount: number }>; } | null>(null); const [syncDateFrom, setSyncDateFrom] = useState(dayjs); const [syncDays, setSyncDays] = useState(30); @@ -128,7 +128,7 @@ const SchedulesPage: React.FC = () => { setSyncResult(null); try { const res = await api.get<{ - success: boolean; data: { activeSchedules: number; mappedTeachers: number; totalTeachers: number }; + success: boolean; data: { activeSchedules: number; mappedClasses: number; totalClasses: number }; }>('/sync/schedule/status'); setSyncStatus(res.data); } catch { @@ -986,17 +986,17 @@ const SchedulesPage: React.FC = () => { {syncResult.skippedNoMapping > 0 && ( )} {syncResult.groups.length > 0 && (
-
按部门分组:
+
按班级分组:
{syncResult.groups.map((g) => ( - {g.deptName}:{g.itemCount} 条排班 + {g.className}:{g.itemCount} 条排班 ))}
@@ -1011,20 +1011,20 @@ const SchedulesPage: React.FC = () => { - + - {syncStatus.mappedTeachers < syncStatus.totalTeachers && ( + {syncStatus.mappedClasses < syncStatus.totalClasses && ( diff --git a/apps/server/src/integration/dingtalk.service.ts b/apps/server/src/integration/dingtalk.service.ts index 5f8e4a5..b76badb 100644 --- a/apps/server/src/integration/dingtalk.service.ts +++ b/apps/server/src/integration/dingtalk.service.ts @@ -562,24 +562,6 @@ export class DingTalkService { return (data.result ?? []).map((s) => ({ id: s.id, name: s.name })); } - /** 按名称查找班次,不存在则创建 */ - async findOrCreateShift(name: string, startTime: string, endTime: string, opUserId = 'manager'): Promise { - const existing = await this.queryShifts(opUserId); - const found = existing.find((s) => s.name === name); - if (found) return found.id; - - return this.upsertShift({ - name, - owner: opUserId, - sections: [{ - times: [ - { check_type: 'OnDuty', across: 0, check_time: `1970-01-01 ${startTime}:00`, free_check: false }, - { check_type: 'OffDuty', across: 0, check_time: `1970-01-01 ${endTime}:00`, free_check: false }, - ], - }], - setting: { is_flexible: false, serious_late_minutes: -1, absenteeism_late_minutes: -1 }, - }); - } // ═══════════════════════════════════════════ // 考勤排班 — 考勤组管理 @@ -672,29 +654,6 @@ export class DingTalkService { return all; } - /** 按名称查找考勤组,不存在则创建 */ - async findOrCreateAttendanceGroup( - name: string, ownerUserId: string, memberUserIds: string[], shiftIds: number[], - ): Promise { - const existing = await this.queryAttendanceGroups(ownerUserId); - const found = existing.find((g) => g.group_name === name); - if (found) return found.group_id; - - return this.createAttendanceGroup({ - name, - type: 'TURN', - owner: ownerUserId, - members: memberUserIds.map((uid) => ({ - role: 'Attendance', - type: 'StaffMember', - user_id: uid, - })), - shift_ids: shiftIds, - enable_emp_select_class: true, - disable_check_without_schedule: false, - disable_check_when_rest: true, - }); - } // ═══════════════════════════════════════════ // 考勤排班 — 排班分配 diff --git a/apps/server/src/sync/schedule-sync.service.ts b/apps/server/src/sync/schedule-sync.service.ts index cb339d3..7b23a40 100644 --- a/apps/server/src/sync/schedule-sync.service.ts +++ b/apps/server/src/sync/schedule-sync.service.ts @@ -1,27 +1,29 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, Not, IsNull } from 'typeorm'; +import { Repository, In } from 'typeorm'; import { ClassSchedule, - ClassTeacher, + ClassStudent, + StudentDingMapping, + Class, } from '../entities'; import { DingTalkService, DingTalkScheduleItem } from '../integration/dingtalk.service'; /** 单次排班同步的结果 */ export interface ScheduleSyncResult { - /** 同步的排课记录数 */ + /** 参与同步的排课记录数 */ scheduleCount: number; - /** 创建的班次数 */ + /** 创建/复用的班次数 */ shiftCount: number; - /** 创建/使用的考勤组数 */ + /** 创建/复用的考勤组数 */ groupCount: number; - /** 实际发送的排班条数 */ + /** 实际写入钉钉的排班条数 */ syncedItems: number; - /** 跳过的记录数(无钉钉映射的用户) */ + /** 因无学生或无钉钉映射而跳过的排课数 */ skippedNoMapping: number; - /** 按部门分组的详情 */ + /** 按班级分组的详情 */ groups: Array<{ - deptName: string; + className: string; groupId: number; itemCount: number; }>; @@ -30,11 +32,16 @@ export interface ScheduleSyncResult { /** * 排班同步服务 — 将本地 ClassSchedule 同步到钉钉考勤排班。 * - * ## 同步流程 - * 1. 查询活跃排课 + 关联教师 - * 2. 按 (startTime, endTime) 创建/匹配钉钉班次 - * 3. 按部门创建/匹配钉钉排班制考勤组 - * 4. 将排课展开为每日排班,批量写入钉钉 + * ## 同步流程(按班级学生) + * 1. 查询活跃排课,按 classId 分组 + * 2. 通过 ClassStudent + StudentDingMapping 拿到每个班级学生的钉钉 userId + * 3. 按 (startTime, endTime) 创建/匹配钉钉班次(班次列表只拉一次) + * 4. 每个班级创建/匹配一个排班制考勤组(考勤组列表只拉一次) + * 5. 将排课展开为每个学生的每日排班,批量写入钉钉 + * + * ## API 调用优化 + * - 班次列表、考勤组列表各只查询一次,在内存中按名称匹配,避免每次 findOrCreate 都发一次查询。 + * - 排班写入按考勤组分批(钉钉单次最多 200 条)。 */ @Injectable() export class ScheduleSyncService { @@ -43,13 +50,17 @@ export class ScheduleSyncService { constructor( @InjectRepository(ClassSchedule) private readonly scheduleRepo: Repository, - @InjectRepository(ClassTeacher) - private readonly classTeacherRepo: Repository, + @InjectRepository(ClassStudent) + private readonly classStudentRepo: Repository, + @InjectRepository(StudentDingMapping) + private readonly mappingRepo: Repository, + @InjectRepository(Class) + private readonly classRepo: Repository, private readonly dingTalkService: DingTalkService, ) {} /** - * 全量同步:将所有活跃排课同步到钉钉排班 + * 全量同步:将所有活跃排课同步到钉钉排班。 * @param dateFrom 起始日期(YYYY-MM-DD),默认今天 * @param days 同步天数,默认 30 * @param opUserId 钉钉操作人 userId @@ -62,24 +73,26 @@ export class ScheduleSyncService { const startDate = dateFrom || new Date().toISOString().slice(0, 10); const endDate = this.addDays(startDate, days); - // ── Step 1: 查询活跃排课 + 关联教师 ── - const schedules = await this.scheduleRepo.find({ - where: { - status: 'active', - teacherId: Not(IsNull()), - }, - }); + const empty: ScheduleSyncResult = { + scheduleCount: 0, shiftCount: 0, groupCount: 0, + syncedItems: 0, skippedNoMapping: 0, groups: [], + }; + // ── Step 1: 查询活跃排课(必须关联到班级才能取学生) ── + const allSchedules = await this.scheduleRepo.find({ + where: { status: 'active' }, + }); + const schedules = allSchedules.filter((s) => s.classId != null); if (schedules.length === 0) { - this.logger.log('没有需要同步的活跃排课'); - return { scheduleCount: 0, shiftCount: 0, groupCount: 0, syncedItems: 0, skippedNoMapping: 0, groups: [] }; + this.logger.log('没有需要同步的活跃排课(无关联班级)'); + return empty; } - // ── Step 2: 获取教师→钉钉用户ID映射 ── - // ponytail: teacher scheduling deprecated; StudentDingMapping.studentId is Student FK, not User - const userIdToDingId = new Map(); + // ── Step 2: 班级 → 学生钉钉ID 映射 ── + const classIds = [...new Set(schedules.map((s) => s.classId as number))]; + const classDingUsers = await this.buildClassDingUserMap(classIds); - // ── Step 3: 按 (startTime, endTime) 创建/匹配班次 ── + // ── Step 3: 班次(按时间段去重,班次列表只查一次) ── const shiftKey = (start: string, end: string) => `${start}-${end}`; const uniqueShifts = new Map(); for (const s of schedules) { @@ -89,12 +102,28 @@ export class ScheduleSyncService { } } + const existingShifts = await this.dingTalkService.queryShifts(opUserId); + const shiftByName = new Map(existingShifts.map((s) => [s.name, s.id])); const timeToShiftId = new Map(); let shiftCount = 0; for (const [key, { startTime, endTime }] of uniqueShifts) { const shiftName = `排课_${startTime}-${endTime}`; try { - const shiftId = await this.dingTalkService.findOrCreateShift(shiftName, startTime, endTime, opUserId); + let shiftId = shiftByName.get(shiftName); + if (shiftId === undefined) { + shiftId = await this.dingTalkService.upsertShift({ + name: shiftName, + owner: opUserId, + sections: [{ + times: [ + { check_type: 'OnDuty', across: 0, check_time: `1970-01-01 ${startTime}:00`, free_check: false }, + { check_type: 'OffDuty', across: 0, check_time: `1970-01-01 ${endTime}:00`, free_check: false }, + ], + }], + setting: { is_flexible: false, serious_late_minutes: -1, absenteeism_late_minutes: -1 }, + }); + shiftByName.set(shiftName, shiftId); + } timeToShiftId.set(key, shiftId); shiftCount++; } catch (e) { @@ -102,84 +131,94 @@ export class ScheduleSyncService { } } - // ── Step 4: Collect teacher IDs (no department entity) ── - const teacherIds = new Set(); + // ── Step 4: 考勤组列表只查一次,供每个班级匹配 ── + const existingGroups = await this.dingTalkService.queryAttendanceGroups(opUserId); + const groupByName = new Map(existingGroups.map((g) => [g.group_name, g.group_id])); + + // ── Step 5: 按班级同步 ── + const classNameMap = await this.loadClassNames(classIds); + const schedulesByClass = new Map(); for (const s of schedules) { - if (s.teacherId) teacherIds.add(s.teacherId); + const cid = s.classId as number; + if (!schedulesByClass.has(cid)) schedulesByClass.set(cid, []); + schedulesByClass.get(cid)!.push(s); } - // ── Step 5: Single group → attendance group → scheduling ── let syncedItems = 0; let skippedNoMapping = 0; let groupCount = 0; const groupDetails: ScheduleSyncResult['groups'] = []; - // Collect teacher→ding mapping - const dingUserIds: string[] = []; - const teacherDingMap = new Map(); - for (const tid of teacherIds) { - const dingId = userIdToDingId.get(tid); - if (dingId) { - dingUserIds.push(dingId); - teacherDingMap.set(tid, dingId); + for (const [classId, classSchedules] of schedulesByClass) { + const className = classNameMap.get(classId) || `班级${classId}`; + const dingUserIds = classDingUsers.get(classId) ?? []; + + if (dingUserIds.length === 0) { + this.logger.warn(`班级 ${className} 无钉钉映射学生,跳过 ${classSchedules.length} 条排课`); + skippedNoMapping += classSchedules.length; + continue; } - } - if (dingUserIds.length === 0) { - this.logger.warn('无钉钉用户映射,跳过全部排班'); - return { scheduleCount: schedules.length, shiftCount, groupCount: 0, syncedItems: 0, skippedNoMapping: schedules.length, groups: [] }; - } + // 该班级用到的班次 + const classShiftIds = new Set(); + for (const s of classSchedules) { + const sid = timeToShiftId.get(shiftKey(s.startTime, s.endTime)); + if (sid) classShiftIds.add(sid); + } + if (classShiftIds.size === 0) { + this.logger.warn(`班级 ${className} 无可用班次,跳过`); + skippedNoMapping += classSchedules.length; + continue; + } - // All used shift IDs - const allShiftIds = new Set(); - for (const s of schedules) { - const key = shiftKey(s.startTime, s.endTime); - const sid = timeToShiftId.get(key); - if (sid) allShiftIds.add(sid); - } - - // Create/find attendance group - const groupName = '排课_全部'; - let attendanceGroupId: number; - try { - attendanceGroupId = await this.dingTalkService.findOrCreateAttendanceGroup( - groupName, - opUserId, - dingUserIds, - [...allShiftIds], - ); - groupCount++; - } catch (e) { - this.logger.error(`创建考勤组 ${groupName} 失败: ${(e as Error).message}`); - return { scheduleCount: schedules.length, shiftCount, groupCount: 0, syncedItems: 0, skippedNoMapping: schedules.length, groups: [] }; - } - - // Expand schedules to daily items - const items = this.expandSchedules( - schedules, - teacherDingMap, - timeToShiftId, - startDate, - endDate, - ); - skippedNoMapping = schedules.length - new Set(items.map((i) => i.userid)).size; - - // Batch write (max 200 per batch) - for (let i = 0; i < items.length; i += 200) { - const batch = items.slice(i, i + 200); + // 创建/匹配该班级的考勤组 + const groupName = `排课_${className}`; + let attendanceGroupId: number; try { - await this.dingTalkService.scheduleUsers(attendanceGroupId, batch, opUserId); - syncedItems += batch.length; + const cached = groupByName.get(groupName); + if (cached !== undefined) { + attendanceGroupId = cached; + } else { + attendanceGroupId = await this.dingTalkService.createAttendanceGroup({ + name: groupName, + type: 'TURN', + owner: opUserId, + members: dingUserIds.map((uid) => ({ role: 'Attendance', type: 'StaffMember', user_id: uid })), + shift_ids: [...classShiftIds], + enable_emp_select_class: true, + disable_check_without_schedule: false, + disable_check_when_rest: true, + }); + groupByName.set(groupName, attendanceGroupId); + } + groupCount++; } catch (e) { - this.logger.error(`排班写入失败 (groupId=${attendanceGroupId}, offset=${i}): ${(e as Error).message}`); + this.logger.error(`创建考勤组 ${groupName} 失败: ${(e as Error).message}`); + skippedNoMapping += classSchedules.length; + continue; } + + // 展开为每个学生的每日排班 + const items = this.expandSchedules( + classSchedules, dingUserIds, timeToShiftId, startDate, endDate, + ); + + // 批量写入(单次≤200) + let classItems = 0; + for (let i = 0; i < items.length; i += 200) { + const batch = items.slice(i, i + 200); + try { + await this.dingTalkService.scheduleUsers(attendanceGroupId, batch, opUserId); + syncedItems += batch.length; + classItems += batch.length; + } catch (e) { + this.logger.error(`排班写入失败 (groupId=${attendanceGroupId}, offset=${i}): ${(e as Error).message}`); + } + } + + groupDetails.push({ className, groupId: attendanceGroupId, itemCount: classItems }); } - groupDetails.push({ - deptName: groupName, - groupId: attendanceGroupId, - itemCount: items.length, - }); this.logger.log( `排班同步完成: ${schedules.length} 条排课 → ${syncedItems} 条钉钉排班, ` + `${shiftCount} 班次, ${groupCount} 考勤组, 跳过 ${skippedNoMapping} 条无映射`, @@ -196,12 +235,52 @@ export class ScheduleSyncService { } /** - * 将排课记录展开为每日排班数组。 - * 每条 ClassSchedule(weekDay, startDate-endDate) → 该日期范围内所有 weekDay 对应日期的排班 + * 构建 classId → 学生钉钉 userId 列表。 + * 一次性查询所有班级的活跃学生与钉钉映射,避免 N+1。 + */ + private async buildClassDingUserMap(classIds: number[]): Promise> { + const result = new Map(); + if (classIds.length === 0) return result; + + // 班级 → 活跃学生 + const links = await this.classStudentRepo.find({ + where: { classId: In(classIds), status: 'active' }, + }); + if (links.length === 0) return result; + + // 学生 → 钉钉 userId + const studentIds = [...new Set(links.map((l) => l.studentId))]; + const mappings = await this.mappingRepo.find({ + where: { studentId: In(studentIds) }, + }); + const studentToDing = new Map(mappings.map((m) => [m.studentId, m.dingUserId])); + + for (const link of links) { + const dingId = studentToDing.get(link.studentId); + if (!dingId) continue; + if (!result.has(link.classId)) result.set(link.classId, []); + const arr = result.get(link.classId)!; + if (!arr.includes(dingId)) arr.push(dingId); + } + return result; + } + + private async loadClassNames(classIds: number[]): Promise> { + const map = new Map(); + if (classIds.length === 0) return map; + const classes = await this.classRepo.find({ where: { id: In(classIds) } }); + for (const c of classes) map.set(c.id, c.name); + return map; + } + + /** + * 将排课记录展开为每个学生的每日排班数组。 + * 每条 ClassSchedule(weekDay, startDate-endDate) × 班级每个学生 → + * 该日期范围内所有 weekDay 对应日期的排班。 */ private expandSchedules( schedules: ClassSchedule[], - teacherDingMap: Map, + dingUserIds: string[], timeToShiftId: Map, syncFrom: string, syncTo: string, @@ -210,17 +289,14 @@ export class ScheduleSyncService { const fromDate = new Date(syncFrom); const toDate = new Date(syncTo); - // 预计算日期范围内每一天是星期几 + // 预计算日期范围内每一天是星期几(周日=7) const dateWeekDays = new Map(); for (let d = new Date(fromDate); d <= toDate; d.setDate(d.getDate() + 1)) { const dateStr = d.toISOString().slice(0, 10); - dateWeekDays.set(dateStr, d.getDay() === 0 ? 7 : d.getDay()); // 周日=7 + dateWeekDays.set(dateStr, d.getDay() === 0 ? 7 : d.getDay()); } for (const s of schedules) { - const dingUserId = s.teacherId ? teacherDingMap.get(s.teacherId) : undefined; - if (!dingUserId) continue; - const shiftId = timeToShiftId.get(`${s.startTime}-${s.endTime}`); if (!shiftId) continue; @@ -232,12 +308,9 @@ export class ScheduleSyncService { if (weekDay !== s.weekDay) continue; const workDate = new Date(dateStr + 'T00:00:00+08:00').getTime(); - items.push({ - userid: dingUserId, - work_date: workDate, - shift_id: shiftId, - is_rest: false, - }); + for (const userid of dingUserIds) { + items.push({ userid, work_date: workDate, shift_id: shiftId, is_rest: false }); + } } } @@ -250,16 +323,22 @@ export class ScheduleSyncService { return d.toISOString().slice(0, 10); } - /** 获取排班同步状态:活跃排课数量 */ - async getStatus(_targetDate: string): Promise<{ activeSchedules: number; mappedTeachers: number; totalTeachers: number }> { - const schedules = await this.scheduleRepo.find({ - where: { status: 'active', teacherId: Not(IsNull()) }, - }); - // ponytail: teacher scheduling is deprecated; mappedTeachers always 0 + /** 获取排班同步状态:活跃排课数、有钉钉映射学生的班级数 */ + async getStatus(_targetDate: string): Promise<{ + activeSchedules: number; + mappedClasses: number; + totalClasses: number; + }> { + const allSchedules = await this.scheduleRepo.find({ where: { status: 'active' } }); + const schedules = allSchedules.filter((s) => s.classId != null); + const classIds = [...new Set(schedules.map((s) => s.classId as number))]; + const classDingUsers = await this.buildClassDingUserMap(classIds); + const mappedClasses = [...classDingUsers.values()].filter((u) => u.length > 0).length; + return { activeSchedules: schedules.length, - mappedTeachers: 0, - totalTeachers: 0, + mappedClasses, + totalClasses: classIds.length, }; } } diff --git a/apps/server/src/sync/sync.module.ts b/apps/server/src/sync/sync.module.ts index c7a811d..18ff38c 100644 --- a/apps/server/src/sync/sync.module.ts +++ b/apps/server/src/sync/sync.module.ts @@ -7,7 +7,7 @@ import { SyncState, StudentDingMapping, ClassSchedule, - ClassTeacher, + ClassStudent, User, Student, Role, @@ -24,7 +24,7 @@ import { ScheduleSyncService } from './schedule-sync.service'; SyncState, StudentDingMapping, ClassSchedule, - ClassTeacher, + ClassStudent, User, Student, Role,