refactor: remove User-based import and RBAC UserDingMapping endpoints

This commit is contained in:
2026-07-09 17:10:33 +08:00
parent 570ef7b7e9
commit 7b8691866e
7 changed files with 8 additions and 844 deletions

View File

@@ -1,26 +1,12 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { SyncLog, SyncState, StudentDingMapping, ClassStudent, ClassTeacher, Department } from '../entities';
import { Class as ClassEntity } from '../entities/class.entity';
import { Repository } from 'typeorm';
import { SyncLog, SyncState, StudentDingMapping } from '../entities';
import type { SyncPlatform, SyncType, SyncStatus } from '../entities/sync-log.entity';
import { DingTalkService } from '../integration/dingtalk.service';
import { WeComService } from '../integration/wecom.service';
import { AttendanceImportService } from '../attendance/attendance-import.service';
import { ScheduleSyncService } from './schedule-sync.service';
import { User } from '../entities/user.entity';
import { Student } from '../entities/student.entity';
import { Role } from '../entities/role.entity';
import * as bcrypt from 'bcryptjs';
import type { ImportClassItemDto } from './dto/import-users.dto';
export interface ImportUserDto {
dingUserId: string;
name: string;
mobile: string;
roleId: number | null;
dingDeptIds: number[];
}
@Injectable()
export class SyncService {
@@ -32,19 +18,11 @@ export class SyncService {
private readonly syncStateRepo: Repository<SyncState>,
@InjectRepository(StudentDingMapping)
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
@InjectRepository(User)
private readonly userRepo: Repository<User>,
@InjectRepository(Student)
private readonly studentRepo: Repository<Student>,
@InjectRepository(Role)
private readonly roleRepo: Repository<Role>,
@InjectRepository(ClassEntity)
private readonly classRepo: Repository<ClassEntity>,
private readonly dingTalkService: DingTalkService,
private readonly weComService: WeComService,
private readonly attendanceImportService: AttendanceImportService,
private readonly scheduleSyncService: ScheduleSyncService,
private readonly dataSource: DataSource,
) {}
// ── Scheduled sync disabled — use manual trigger via UI ──
@@ -120,208 +98,6 @@ export class SyncService {
return this.dingTalkService.fetchOrgTreeWithUsers(rootDeptId);
}
/**
* 从钉钉导入用户roleId 非 null → 老师User + 指定角色roleId null → 学生User + Student
* 支持同时创建班级并建立师生关联。
* 已存在 StudentDingMapping 的记录跳过。
*/
async importDingTalkUsers(
users: ImportUserDto[],
classes?: ImportClassItemDto[],
): Promise<{
teacherCount: number;
studentCount: number;
classCount: number;
skipped: number;
warnings: string[];
}> {
const classItems = classes ?? [];
const warnings: string[] = [];
// 预检查班级编码重复
if (classItems.length > 0) {
const codes = classItems.map((c) => c.code);
const existing = await this.classRepo.find({ where: codes.map((code) => ({ code })) });
if (existing.length > 0) {
const dup = existing.map((c) => c.code).join(', ');
throw new BadRequestException(`班级编码已存在: ${dup}`);
}
}
let teacherCount = 0;
let studentCount = 0;
let skipped = 0;
await this.dataSource.transaction(async (manager) => {
// 0. 同步部门:为每个班级标记的钉钉部门创建/查找本地 Department
const deptLocalIdMap = new Map<number, number>(); // dingDeptId -> local deptId
for (const c of classItems) {
let dept = await manager.findOne(Department, {
where: { source: 'dingtalk', sourceId: String(c.deptId) },
});
if (!dept) {
dept = manager.create(Department, {
name: c.name,
type: 'department',
source: 'dingtalk',
sourceId: String(c.deptId),
});
dept = await manager.save(dept);
}
deptLocalIdMap.set(c.deptId, dept.id);
}
// 1. 创建班级
const deptClassMap = new Map<number, number>(); // deptId -> classId
for (const c of classItems) {
const deptId = deptLocalIdMap.get(c.deptId) ?? null;
const result = await manager.save(ClassEntity, {
name: c.name,
code: c.code,
departmentId: deptId,
classType: c.classType,
startDate: c.startDate ?? null,
endDate: c.endDate ?? null,
maxStudents: c.maxStudents ?? 0,
notes: c.notes ?? null,
} as unknown as Record<string, unknown>);
deptClassMap.set(c.deptId, (result as ClassEntity).id);
}
// 2. 导入用户(逐用户)
for (const u of users) {
const existingMapping = await manager.findOne(StudentDingMapping, {
where: { dingUserId: u.dingUserId },
});
let userId: number | null = null;
let isTeacher = false;
if (existingMapping) {
skipped++;
// ponytail: studentDingMapping.studentId is Student FK, not User; full rewrite in Task 4
userId = existingMapping.studentId;
isTeacher = false;
} else {
// 检查是否已存在syncAll 或历史导入),避免撞 username 唯一约束
const username = `dd_${u.dingUserId}`;
const whereConditions: Record<string, unknown>[] = [{ username }];
if (u.mobile) whereConditions.push({ username: u.mobile });
let user = await manager.findOne(User, { where: whereConditions });
if (!user) {
const passwordHash = await bcrypt.hash('123456', 10);
user = manager.create(User, {
username,
name: u.name,
passwordHash,
isActive: true,
});
await manager.save(user);
} else {
// 已存在:更新姓名
user.name = u.name;
await manager.save(user);
}
userId = user.id;
// 解析学生所属部门
const studentDeptId = u.roleId == null && classItems.length > 0 && u.dingDeptIds?.length > 0
? deptLocalIdMap.get(u.dingDeptIds.find((d) => deptLocalIdMap.has(d)) ?? -1) ?? undefined
: undefined;
if (u.roleId != null) {
const role = await manager.findOne(Role, { where: { id: u.roleId } });
if (!role) throw new BadRequestException(`角色 id=${u.roleId} 不存在`);
user.roles = [role];
await manager.save(user);
isTeacher = true;
teacherCount++;
} else {
// 学生:检查 Student 是否已存在,不存在则创建
let student = await manager.findOne(Student, { where: { userId: user.id } });
if (!student) {
student = manager.create(Student, {
name: u.name,
phone: u.mobile || undefined,
userId: user.id,
departmentId: studentDeptId,
status: 'active',
});
await manager.save(student);
}
studentCount++;
}
// 钉钉映射(幂等:可能已被 syncAll 创建)
const existingMappingForUser = await manager.findOne(StudentDingMapping, {
where: { dingUserId: u.dingUserId },
});
if (!existingMappingForUser) {
const mapping = manager.create(StudentDingMapping, {
dingUserId: u.dingUserId,
studentId: (user as any).id,
});
await manager.save(mapping);
}
}
// 3. 建立班级关联(新建/已存在用户都处理)
if (classItems.length > 0 && u.dingDeptIds?.length > 0) {
for (const deptId of u.dingDeptIds) {
const classId = deptClassMap.get(deptId);
if (!classId) continue;
if (isTeacher) {
const existingCT = await manager.findOne(ClassTeacher, {
where: { classId, userId },
});
if (!existingCT) {
const ct = manager.create(ClassTeacher, {
classId,
userId,
roleType: 'teacher',
});
await manager.save(ct);
}
} else {
const studentRecord = await manager.findOne(Student, {
where: { userId },
});
if (!studentRecord) continue;
const existingCS = await manager.findOne(ClassStudent, {
where: { classId, studentId: studentRecord.id },
});
if (!existingCS) {
const cs = manager.create(ClassStudent, {
classId,
studentId: studentRecord.id,
status: 'active',
});
await manager.save(cs);
}
}
}
}
}
// 4. 检查空班级
for (const [deptId, classId] of deptClassMap) {
const tc = await manager.count(ClassTeacher, { where: { classId } });
const sc = await manager.count(ClassStudent, { where: { classId } });
if (tc === 0 && sc === 0) {
const cls = await manager.findOne(ClassEntity, { where: { id: classId } });
warnings.push(`班级 "${cls?.name}" (deptId=${deptId}) 无任何师生`);
}
}
});
this.logger.log(
`钉钉用户导入完成: ${teacherCount} 位老师, ${studentCount} 位学生, ${classItems.length} 个班级, ${skipped} 跳过`,
);
return { teacherCount, studentCount, classCount: classItems.length, skipped, warnings };
}
// ── 排班同步 ──
/** 将本地排课同步到钉钉考勤排班 */