Files
gongxue-base/apps/server/src/integration/dingtalk-student-sync.ts

125 lines
4.3 KiB
TypeScript

import { EntityManager, In } from 'typeorm';
import { Organization, Student, StudentDingMapping } from '../entities';
export interface DingTalkStudentInput {
dingUserId: string;
name: string;
mobile?: string;
}
export interface DingTalkStudentConflict {
dingUserId: string;
name: string;
reason: string;
}
export interface DingTalkStudentSyncResult {
created: number;
updated: number;
studentIds: Map<string, number>;
conflicts: DingTalkStudentConflict[];
}
export async function syncDingTalkStudents(
manager: EntityManager,
inputs: DingTalkStudentInput[],
): Promise<DingTalkStudentSyncResult> {
const users = new Map<string, DingTalkStudentInput>();
const conflicts: DingTalkStudentConflict[] = [];
for (const input of inputs) {
const dingUserId = input.dingUserId?.trim();
const name = input.name?.trim();
const mobile = input.mobile?.trim() || undefined;
if (!dingUserId || dingUserId.length > 100 || !name || name.length > 50) {
conflicts.push({ dingUserId: dingUserId || '', name: name || '', reason: '钉钉用户ID或姓名无效' });
continue;
}
if (mobile && mobile.length > 20) {
conflicts.push({ dingUserId, name, reason: '手机号超过20个字符' });
continue;
}
if (!users.has(dingUserId)) users.set(dingUserId, { dingUserId, name, mobile });
}
if (users.size === 0) {
return { created: 0, updated: 0, studentIds: new Map(), conflicts };
}
const dingUserIds = [...users.keys()];
const mappings = await manager.find(StudentDingMapping, {
where: { dingUserId: In(dingUserIds) },
});
const mappingByDingId = new Map(mappings.map((mapping) => [mapping.dingUserId, mapping]));
const mappedStudentIds = mappings.map((mapping) => mapping.studentId);
const mappedStudents = mappedStudentIds.length
? await manager.find(Student, { where: { id: In(mappedStudentIds) } })
: [];
const studentById = new Map(mappedStudents.map((student) => [student.id, student]));
const studentIds = new Map<string, number>();
const updates: Student[] = [];
for (const mapping of mappings) {
const input = users.get(mapping.dingUserId);
const student = studentById.get(mapping.studentId);
if (!input || !student) {
conflicts.push({
dingUserId: mapping.dingUserId,
name: input?.name || '',
reason: '钉钉映射对应的学生不存在',
});
continue;
}
studentIds.set(mapping.dingUserId, student.id);
student.name = input.name;
if (input.mobile) student.phone = input.mobile;
updates.push(student);
}
const newUsers = [...users.values()].filter((user) => !mappingByDingId.has(user.dingUserId));
const mobiles = [...new Set(newUsers.map((user) => user.mobile).filter((mobile): mobile is string => !!mobile))];
const occupiedPhones = mobiles.length
? await manager.find(Student, { where: { phone: In(mobiles) } })
: [];
const studentByPhone = new Map(occupiedPhones.map((student) => [student.phone, student]));
const creatable = newUsers.filter((user) => {
if (!user.mobile || !studentByPhone.has(user.mobile)) return true;
conflicts.push({ dingUserId: user.dingUserId, name: user.name, reason: '手机号已属于其他学生,请人工绑定' });
return false;
});
const host = creatable.length
? await manager.findOne(Organization, { where: { isHost: true, status: 'active' } })
: null;
if (creatable.length && !host) throw new Error('尚未配置本机构');
if (updates.length) await manager.save(Student, updates);
const createdStudents = creatable.length
? await manager.save(
Student,
creatable.map((user) =>
manager.create(Student, {
name: user.name,
phone: user.mobile,
status: 'active',
organizationId: host!.id,
}),
),
)
: [];
if (createdStudents.length) {
await manager.save(
StudentDingMapping,
createdStudents.map((student, index) =>
manager.create(StudentDingMapping, {
dingUserId: creatable[index].dingUserId,
studentId: student.id,
}),
),
);
createdStudents.forEach((student, index) => studentIds.set(creatable[index].dingUserId, student.id));
}
return { created: createdStudents.length, updated: updates.length, studentIds, conflicts };
}