refactor(server): remove Department/UserDepartment entities, CampusScope, and departmentId from all entities

- Delete department.entity.ts, user-department.entity.ts
- Remove Department/UserDepartment from entities/index.ts
- Remove departmentId column from 18 entities (AttendanceRecord, ArchiveAttachment, Bill, ClassSchedule, Classroom, ClassroomRental, Deposit, DepositInstallment, ExamScore, LearningRecord, Occupancy, PersonalExpense, ResultArchive, Room, RoomExpense, Student, StudentEnrollment, StudentProfile, StudentReport)
- Remove departments/ module entirely
- Delete campus-scope.ts, campus-scope.middleware.ts (request-utils.ts kept — it's just IP extraction)
- Simplify common.module.ts to empty module
- Remove CampusScopeMiddleware from app.module.ts
- Remove all CampusScope injections and filter calls across all services
- Remove departmentId from all DTOs and controllers
- Simplify dingtalk/wecom sync to only sync users (no dept table)
- Update seed module to remove department seeding
- Clean frontend compilation
This commit is contained in:
2026-07-09 17:51:32 +08:00
parent b0f7883f33
commit 6029d8e2fd
68 changed files with 220 additions and 1516 deletions

View File

@@ -3,10 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Not, IsNull } from 'typeorm';
import {
ClassSchedule,
ClassStudent,
ClassTeacher,
Department,
UserDepartment,
} from '../entities';
import { DingTalkService, DingTalkScheduleItem } from '../integration/dingtalk.service';
@@ -48,10 +45,6 @@ export class ScheduleSyncService {
private readonly scheduleRepo: Repository<ClassSchedule>,
@InjectRepository(ClassTeacher)
private readonly classTeacherRepo: Repository<ClassTeacher>,
@InjectRepository(Department)
private readonly deptRepo: Repository<Department>,
@InjectRepository(UserDepartment)
private readonly userDeptRepo: Repository<UserDepartment>,
private readonly dingTalkService: DingTalkService,
) {}
@@ -75,7 +68,6 @@ export class ScheduleSyncService {
status: 'active',
teacherId: Not(IsNull()),
},
relations: ['department'],
});
if (schedules.length === 0) {
@@ -110,102 +102,84 @@ export class ScheduleSyncService {
}
}
// ── Step 4: 按部门分组 ──
const deptGroups = new Map<
number,
{ deptName: string; schedules: ClassSchedule[]; teacherIds: Set<number> }
>();
// ── Step 4: Collect teacher IDs (no department entity) ──
const teacherIds = new Set<number>();
for (const s of schedules) {
const deptId = s.departmentId || 0;
if (!deptGroups.has(deptId)) {
deptGroups.set(deptId, {
deptName: (s.department as Department)?.name || `部门${deptId}`,
schedules: [],
teacherIds: new Set(),
});
}
const group = deptGroups.get(deptId)!;
group.schedules.push(s);
if (s.teacherId) group.teacherIds.add(s.teacherId);
if (s.teacherId) teacherIds.add(s.teacherId);
}
// ── Step 5: 每个部门 → 考勤组 → 排班 ──
// ── Step 5: Single group → attendance group → scheduling ──
let syncedItems = 0;
let skippedNoMapping = 0;
let groupCount = 0;
const groupDetails: ScheduleSyncResult['groups'] = [];
for (const [deptId, group] of deptGroups) {
// 获取该部门教师的钉钉 userIds
const dingUserIds: string[] = [];
const teacherDingMap = new Map<number, string>(); // local teacherId → dingUserId
for (const tid of group.teacherIds) {
const dingId = userIdToDingId.get(tid);
if (dingId) {
dingUserIds.push(dingId);
teacherDingMap.set(tid, dingId);
}
// Collect teacher→ding mapping
const dingUserIds: string[] = [];
const teacherDingMap = new Map<number, string>();
for (const tid of teacherIds) {
const dingId = userIdToDingId.get(tid);
if (dingId) {
dingUserIds.push(dingId);
teacherDingMap.set(tid, dingId);
}
if (dingUserIds.length === 0) {
skippedNoMapping += group.schedules.length;
this.logger.warn(`部门 ${group.deptName}: 无钉钉用户映射,跳过`);
continue;
}
// 该部门使用的班次 IDs
const deptShiftIds = new Set<number>();
for (const s of group.schedules) {
const key = shiftKey(s.startTime, s.endTime);
const sid = timeToShiftId.get(key);
if (sid) deptShiftIds.add(sid);
}
// 创建/匹配考勤组
const groupName = `排课_${group.deptName}`;
let attendanceGroupId: number;
try {
attendanceGroupId = await this.dingTalkService.findOrCreateAttendanceGroup(
groupName,
opUserId,
dingUserIds,
[...deptShiftIds],
);
groupCount++;
} catch (e) {
this.logger.error(`创建考勤组 ${groupName} 失败: ${(e as Error).message}`);
continue;
}
// 展开排课为每日排班
const items = this.expandSchedules(
group.schedules,
teacherDingMap,
timeToShiftId,
startDate,
endDate,
);
skippedNoMapping += group.schedules.length - new Set(items.map((i) => i.userid)).size;
// 分批写入每次最多200条
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;
} catch (e) {
this.logger.error(`排班写入失败 (groupId=${attendanceGroupId}, offset=${i}): ${(e as Error).message}`);
}
}
groupDetails.push({
deptName: group.deptName,
groupId: attendanceGroupId,
itemCount: items.length,
});
}
if (dingUserIds.length === 0) {
this.logger.warn('无钉钉用户映射,跳过全部排班');
return { scheduleCount: schedules.length, shiftCount, groupCount: 0, syncedItems: 0, skippedNoMapping: schedules.length, groups: [] };
}
// All used shift IDs
const allShiftIds = new Set<number>();
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);
try {
await this.dingTalkService.scheduleUsers(attendanceGroupId, batch, opUserId);
syncedItems += batch.length;
} catch (e) {
this.logger.error(`排班写入失败 (groupId=${attendanceGroupId}, offset=${i}): ${(e as Error).message}`);
}
}
groupDetails.push({
deptName: groupName,
groupId: attendanceGroupId,
itemCount: items.length,
});
this.logger.log(
`排班同步完成: ${schedules.length} 条排课 → ${syncedItems} 条钉钉排班, ` +
`${shiftCount} 班次, ${groupCount} 考勤组, 跳过 ${skippedNoMapping} 条无映射`,