refactor: replace UserDingMapping with StudentDingMapping entity
This commit is contained in:
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, In, Not, IsNull } from 'typeorm';
|
||||
import {
|
||||
ClassSchedule,
|
||||
UserDingMapping,
|
||||
StudentDingMapping,
|
||||
ClassStudent,
|
||||
ClassTeacher,
|
||||
Department,
|
||||
@@ -47,8 +47,8 @@ export class ScheduleSyncService {
|
||||
constructor(
|
||||
@InjectRepository(ClassSchedule)
|
||||
private readonly scheduleRepo: Repository<ClassSchedule>,
|
||||
@InjectRepository(UserDingMapping)
|
||||
private readonly mappingRepo: Repository<UserDingMapping>,
|
||||
@InjectRepository(StudentDingMapping)
|
||||
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
|
||||
@InjectRepository(ClassTeacher)
|
||||
private readonly classTeacherRepo: Repository<ClassTeacher>,
|
||||
@InjectRepository(Department)
|
||||
@@ -88,10 +88,10 @@ export class ScheduleSyncService {
|
||||
|
||||
// ── Step 2: 获取教师→钉钉用户ID映射 ──
|
||||
const teacherIds = [...new Set(schedules.map((s) => s.teacherId!).filter(Boolean))];
|
||||
const mappings = await this.mappingRepo.find({
|
||||
where: { userId: In(teacherIds) },
|
||||
const mappings = await this.studentDingMappingRepo.find({
|
||||
where: { studentId: In(teacherIds) },
|
||||
});
|
||||
const userIdToDingId = new Map(mappings.map((m) => [m.userId, m.dingUserId]));
|
||||
const userIdToDingId = new Map(mappings.map((m) => [m.studentId, m.dingUserId]));
|
||||
|
||||
// ── Step 3: 按 (startTime, endTime) 创建/匹配班次 ──
|
||||
const shiftKey = (start: string, end: string) => `${start}-${end}`;
|
||||
@@ -288,8 +288,8 @@ export class ScheduleSyncService {
|
||||
where: { status: 'active', teacherId: Not(IsNull()) },
|
||||
});
|
||||
const teacherIds = [...new Set(schedules.map((s) => s.teacherId!).filter(Boolean))];
|
||||
const mappings = await this.mappingRepo.find({
|
||||
where: { userId: In(teacherIds) },
|
||||
const mappings = await this.studentDingMappingRepo.find({
|
||||
where: { studentId: In(teacherIds) },
|
||||
});
|
||||
return {
|
||||
activeSchedules: schedules.length,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AttendanceModule } from '../attendance/attendance.module';
|
||||
import {
|
||||
SyncLog,
|
||||
SyncState,
|
||||
UserDingMapping,
|
||||
StudentDingMapping,
|
||||
ClassSchedule,
|
||||
Department,
|
||||
UserDepartment,
|
||||
@@ -24,7 +24,7 @@ import { ScheduleSyncService } from './schedule-sync.service';
|
||||
TypeOrmModule.forFeature([
|
||||
SyncLog,
|
||||
SyncState,
|
||||
UserDingMapping,
|
||||
StudentDingMapping,
|
||||
ClassSchedule,
|
||||
Department,
|
||||
UserDepartment,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { SyncService, ImportUserDto } from './sync.service';
|
||||
import { SyncLog, SyncState, UserDingMapping, ClassStudent, ClassTeacher } from '../entities';
|
||||
import { SyncLog, SyncState, StudentDingMapping, ClassStudent, ClassTeacher } from '../entities';
|
||||
import { Class as ClassEntity } from '../entities/class.entity';
|
||||
import { User } from '../entities/user.entity';
|
||||
import { Student } from '../entities/student.entity';
|
||||
@@ -36,7 +36,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
||||
let service: SyncService;
|
||||
|
||||
let mappingRepo: jest.Mocked<
|
||||
Pick<Repository<UserDingMapping>, 'findOne' | 'create' | 'save' | 'find'>
|
||||
Pick<Repository<StudentDingMapping>, 'findOne' | 'create' | 'save' | 'find'>
|
||||
>;
|
||||
let userRepo: jest.Mocked<Pick<Repository<User>, 'create' | 'save'>>;
|
||||
let studentRepo: jest.Mocked<Pick<Repository<Student>, 'create' | 'save'>>;
|
||||
@@ -109,7 +109,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
||||
SyncService,
|
||||
{ provide: getRepositoryToken(SyncLog), useValue: { create: jest.fn(), save: jest.fn(), find: jest.fn(), findOne: jest.fn() } },
|
||||
{ provide: getRepositoryToken(SyncState), useValue: { findOne: jest.fn(), upsert: jest.fn() } },
|
||||
{ provide: getRepositoryToken(UserDingMapping), useValue: mappingRepo },
|
||||
{ provide: getRepositoryToken(StudentDingMapping), useValue: mappingRepo },
|
||||
{ provide: getRepositoryToken(User), useValue: userRepo },
|
||||
{ provide: getRepositoryToken(Student), useValue: studentRepo },
|
||||
{ provide: getRepositoryToken(Role), useValue: roleRepo },
|
||||
@@ -142,7 +142,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
||||
it('imports teacher when roleId is a number (role found)', async () => {
|
||||
const mockRole = { id: 5, name: 'Teacher' } as Role;
|
||||
mgr.findOne.mockImplementation(async (entityClass: unknown) => {
|
||||
if (entityClass === UserDingMapping) return null;
|
||||
if (entityClass === StudentDingMapping) return null;
|
||||
if (entityClass === Role) return mockRole;
|
||||
return null;
|
||||
});
|
||||
@@ -170,7 +170,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
||||
});
|
||||
|
||||
it('throws when role not found', async () => {
|
||||
// UserDingMapping lookup → null (not skipped), Role lookup → null → throws
|
||||
// StudentDingMapping lookup → null (not skipped), Role lookup → null → throws
|
||||
mgr.findOne.mockResolvedValue(null);
|
||||
|
||||
const mockUser = { id: 11 } as User;
|
||||
@@ -194,8 +194,8 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
||||
mgr.save.mockResolvedValueOnce(mockStudent);
|
||||
|
||||
// mapping create+save also calls create/save
|
||||
mgr.create.mockReturnValueOnce({} as UserDingMapping);
|
||||
mgr.save.mockResolvedValueOnce({} as UserDingMapping);
|
||||
mgr.create.mockReturnValueOnce({} as StudentDingMapping);
|
||||
mgr.save.mockResolvedValueOnce({} as StudentDingMapping);
|
||||
|
||||
mappingRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
||||
|
||||
});
|
||||
it('skips user when mapping already exists', async () => {
|
||||
// mgr.findOne(UserDingMapping, ...) returns truthy → skip inside transaction
|
||||
// mgr.findOne(StudentDingMapping, ...) returns truthy → skip inside transaction
|
||||
mgr.findOne.mockResolvedValue({ id: 1 });
|
||||
|
||||
const users: ImportUserDto[] = [
|
||||
@@ -259,10 +259,10 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
||||
|
||||
// Single transaction: User 1→skip, User 2→teacher, User 3→student
|
||||
mgr.findOne
|
||||
.mockResolvedValueOnce({ id: 99 }) // User 1: UserDingMapping → skip
|
||||
.mockResolvedValueOnce(null) // User 2: UserDingMapping → not skipped
|
||||
.mockResolvedValueOnce({ id: 99 }) // User 1: StudentDingMapping → skip
|
||||
.mockResolvedValueOnce(null) // User 2: StudentDingMapping → not skipped
|
||||
.mockResolvedValueOnce(mockRole) // User 2: Role lookup
|
||||
.mockResolvedValueOnce(null); // User 3: UserDingMapping → not skipped
|
||||
.mockResolvedValueOnce(null); // User 3: StudentDingMapping → not skipped
|
||||
|
||||
const users: ImportUserDto[] = [
|
||||
{ dingUserId: 'skip', name: 'Skip', mobile: '138', roleId: null, dingDeptIds: [] },
|
||||
@@ -395,7 +395,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
||||
});
|
||||
mgr.count.mockResolvedValue(0);
|
||||
// save(ClassEntity, data) returns a class — but the smart mock already handles this
|
||||
mgr.findOne.mockResolvedValueOnce(null); // first findOne: UserDingMapping for teacher
|
||||
mgr.findOne.mockResolvedValueOnce(null); // first findOne: StudentDingMapping for teacher
|
||||
mgr.findOne.mockResolvedValueOnce({ id: 5 }); // Role lookup
|
||||
mgr.findOne.mockResolvedValueOnce({ name: 'Empty Class' }); // Class lookup in empty check
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { SyncLog, SyncState, UserDingMapping, ClassStudent, ClassTeacher, Department } from '../entities';
|
||||
import { SyncLog, SyncState, StudentDingMapping, ClassStudent, ClassTeacher, Department } from '../entities';
|
||||
import { Class as ClassEntity } from '../entities/class.entity';
|
||||
import type { SyncPlatform, SyncType, SyncStatus } from '../entities/sync-log.entity';
|
||||
import { DingTalkService } from '../integration/dingtalk.service';
|
||||
@@ -30,8 +30,8 @@ export class SyncService {
|
||||
private readonly syncLogRepo: Repository<SyncLog>,
|
||||
@InjectRepository(SyncState)
|
||||
private readonly syncStateRepo: Repository<SyncState>,
|
||||
@InjectRepository(UserDingMapping)
|
||||
private readonly mappingRepo: Repository<UserDingMapping>,
|
||||
@InjectRepository(StudentDingMapping)
|
||||
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepo: Repository<User>,
|
||||
@InjectRepository(Student)
|
||||
@@ -123,7 +123,7 @@ export class SyncService {
|
||||
/**
|
||||
* 从钉钉导入用户:roleId 非 null → 老师(User + 指定角色),roleId null → 学生(User + Student)。
|
||||
* 支持同时创建班级并建立师生关联。
|
||||
* 已存在 UserDingMapping 的记录跳过。
|
||||
* 已存在 StudentDingMapping 的记录跳过。
|
||||
*/
|
||||
async importDingTalkUsers(
|
||||
users: ImportUserDto[],
|
||||
@@ -190,7 +190,7 @@ export class SyncService {
|
||||
|
||||
// 2. 导入用户(逐用户)
|
||||
for (const u of users) {
|
||||
const existingMapping = await manager.findOne(UserDingMapping, {
|
||||
const existingMapping = await manager.findOne(StudentDingMapping, {
|
||||
where: { dingUserId: u.dingUserId },
|
||||
});
|
||||
|
||||
@@ -199,7 +199,7 @@ export class SyncService {
|
||||
|
||||
if (existingMapping) {
|
||||
skipped++;
|
||||
userId = existingMapping.userId;
|
||||
userId = existingMapping.studentId;
|
||||
// 判断是老师还是学生:查角色
|
||||
const existingUser = await manager.findOne(User, {
|
||||
where: { id: userId },
|
||||
@@ -258,15 +258,13 @@ export class SyncService {
|
||||
}
|
||||
|
||||
// 钉钉映射(幂等:可能已被 syncAll 创建)
|
||||
const existingMappingForUser = await manager.findOne(UserDingMapping, {
|
||||
const existingMappingForUser = await manager.findOne(StudentDingMapping, {
|
||||
where: { dingUserId: u.dingUserId },
|
||||
});
|
||||
if (!existingMappingForUser) {
|
||||
const mapping = manager.create(UserDingMapping, {
|
||||
const mapping = manager.create(StudentDingMapping, {
|
||||
dingUserId: u.dingUserId,
|
||||
userId: user.id,
|
||||
dingName: u.name,
|
||||
dingMobile: u.mobile,
|
||||
studentId: user.id,
|
||||
});
|
||||
await manager.save(mapping);
|
||||
}
|
||||
@@ -422,7 +420,7 @@ export class SyncService {
|
||||
const end = endDate.toISOString().slice(0, 10);
|
||||
|
||||
this.logger.log(`Importing DingTalk attendance: ${start} ~ ${end}`);
|
||||
const mappings = await this.mappingRepo.find();
|
||||
const mappings = await this.studentDingMappingRepo.find();
|
||||
const userIds = mappings.map((m) => m.dingUserId);
|
||||
const importResult = await this.attendanceImportService.importFromDingTalk({
|
||||
startDate: start,
|
||||
|
||||
Reference in New Issue
Block a user