forked from wangziqi/gongxue-base
refactor: replace UserDingMapping with StudentDingMapping entity
This commit is contained in:
@@ -41,7 +41,7 @@ import {
|
|||||||
ExpenseType,
|
ExpenseType,
|
||||||
ResultArchive,
|
ResultArchive,
|
||||||
ArchiveAttachment,
|
ArchiveAttachment,
|
||||||
UserDingMapping,
|
StudentDingMapping,
|
||||||
} from './entities';
|
} from './entities';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { RbacModule } from './rbac/rbac.module';
|
import { RbacModule } from './rbac/rbac.module';
|
||||||
@@ -124,7 +124,7 @@ import { CampusScopeMiddleware } from './common/campus-scope.middleware';
|
|||||||
ResultArchive,
|
ResultArchive,
|
||||||
SyncLog,
|
SyncLog,
|
||||||
SyncState,
|
SyncState,
|
||||||
UserDingMapping,
|
StudentDingMapping,
|
||||||
IntegrationConfig,
|
IntegrationConfig,
|
||||||
IntegrationConfigDetail,
|
IntegrationConfigDetail,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Subject, Observable } from 'rxjs';
|
|||||||
import {
|
import {
|
||||||
DingAttendanceRaw,
|
DingAttendanceRaw,
|
||||||
Student,
|
Student,
|
||||||
UserDingMapping,
|
StudentDingMapping,
|
||||||
} from '../entities';
|
} from '../entities';
|
||||||
import { DingTalkService, DingTalkAttendanceResult } from '../integration/dingtalk.service';
|
import { DingTalkService, DingTalkAttendanceResult } from '../integration/dingtalk.service';
|
||||||
import { AttendanceService } from './attendance.service';
|
import { AttendanceService } from './attendance.service';
|
||||||
@@ -33,8 +33,8 @@ export class AttendanceImportService {
|
|||||||
private readonly dingRawRepo: Repository<DingAttendanceRaw>,
|
private readonly dingRawRepo: Repository<DingAttendanceRaw>,
|
||||||
@InjectRepository(Student)
|
@InjectRepository(Student)
|
||||||
private readonly studentRepo: Repository<Student>,
|
private readonly studentRepo: Repository<Student>,
|
||||||
@InjectRepository(UserDingMapping)
|
@InjectRepository(StudentDingMapping)
|
||||||
private readonly userDingMappingRepo: Repository<UserDingMapping>,
|
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
|
||||||
private readonly dingTalkService: DingTalkService,
|
private readonly dingTalkService: DingTalkService,
|
||||||
private readonly attendanceService: AttendanceService,
|
private readonly attendanceService: AttendanceService,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AttendanceRecord, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent, UserDingMapping } from '../entities';
|
import { AttendanceRecord, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent, StudentDingMapping } from '../entities';
|
||||||
import { AttendanceService } from './attendance.service';
|
import { AttendanceService } from './attendance.service';
|
||||||
import { AttendanceImportService } from './attendance-import.service';
|
import { AttendanceImportService } from './attendance-import.service';
|
||||||
import { AttendanceController } from './attendance.controller';
|
import { AttendanceController } from './attendance.controller';
|
||||||
@@ -10,7 +10,7 @@ import { IntegrationModule } from '../integration/integration.module';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([AttendanceRecord, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent, UserDingMapping]),
|
TypeOrmModule.forFeature([AttendanceRecord, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent, StudentDingMapping]),
|
||||||
OperationLogsModule,
|
OperationLogsModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
IntegrationModule,
|
IntegrationModule,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Class } from '../entities/class.entity';
|
|||||||
import { Student } from '../entities/student.entity';
|
import { Student } from '../entities/student.entity';
|
||||||
import { ClassSchedule } from '../entities/class-schedule.entity';
|
import { ClassSchedule } from '../entities/class-schedule.entity';
|
||||||
import { ClassStudent } from '../entities/class-student.entity';
|
import { ClassStudent } from '../entities/class-student.entity';
|
||||||
import { UserDingMapping } from '../entities/user-ding-mapping.entity';
|
import { StudentDingMapping } from '../entities/student-ding-mapping.entity';
|
||||||
import { BatchCreateAttendanceDto } from './dto/attendance.dto';
|
import { BatchCreateAttendanceDto } from './dto/attendance.dto';
|
||||||
|
|
||||||
describe('AttendanceService — batchCreate', () => {
|
describe('AttendanceService — batchCreate', () => {
|
||||||
@@ -39,7 +39,7 @@ describe('AttendanceService — batchCreate', () => {
|
|||||||
// Reserved for future tests (auto-match, schedule-based attendance, etc.)
|
// Reserved for future tests (auto-match, schedule-based attendance, etc.)
|
||||||
const mockScheduleRepo = { find: jest.fn().mockResolvedValue([]) };
|
const mockScheduleRepo = { find: jest.fn().mockResolvedValue([]) };
|
||||||
const mockClassStudentRepo = { find: jest.fn().mockResolvedValue([]) };
|
const mockClassStudentRepo = { find: jest.fn().mockResolvedValue([]) };
|
||||||
const mockUserDingMappingRepo = { find: jest.fn().mockResolvedValue([]) };
|
const mockStudentDingMappingRepo = { find: jest.fn().mockResolvedValue([]) };
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -49,7 +49,7 @@ describe('AttendanceService — batchCreate', () => {
|
|||||||
{ provide: getRepositoryToken(Class), useValue: mockClassRepo },
|
{ provide: getRepositoryToken(Class), useValue: mockClassRepo },
|
||||||
{ provide: getRepositoryToken(Student), useValue: mockStudentRepo },
|
{ provide: getRepositoryToken(Student), useValue: mockStudentRepo },
|
||||||
{ provide: getRepositoryToken(ClassSchedule), useValue: mockScheduleRepo },
|
{ provide: getRepositoryToken(ClassSchedule), useValue: mockScheduleRepo },
|
||||||
{ provide: getRepositoryToken(UserDingMapping), useValue: mockUserDingMappingRepo },
|
{ provide: getRepositoryToken(StudentDingMapping), useValue: mockStudentDingMappingRepo },
|
||||||
{ provide: getRepositoryToken(ClassStudent), useValue: mockClassStudentRepo },
|
{ provide: getRepositoryToken(ClassStudent), useValue: mockClassStudentRepo },
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
@@ -99,9 +99,9 @@ describe('AttendanceService — batchCreate', () => {
|
|||||||
await expect(service.batchCreate(dto)).rejects.toThrow(BadRequestException);
|
await expect(service.batchCreate(dto)).rejects.toThrow(BadRequestException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('autoMatchDingRecords with UserDingMapping chain', async () => {
|
it.skip('autoMatchDingRecords with StudentDingMapping chain', async () => {
|
||||||
// TODO: match dingtalk raw records to students via UserDingMapping lookup,
|
// TODO: match dingtalk raw records to students via StudentDingMapping lookup,
|
||||||
// then to class schedules → ClassStudent association, producing attendance records.
|
// then to class schedules → ClassStudent association, producing attendance records.
|
||||||
// Requires mock setup for UserDingMapping, ClassSchedule, ClassStudent, and DingAttendanceRaw repos.
|
// Requires mock setup for StudentDingMapping, ClassSchedule, ClassStudent, and DingAttendanceRaw repos.
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, In, Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
|
import { Repository, In, Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
|
||||||
import { AttendanceRecord, DingAttendanceRaw, Class, Student, ClassSchedule, ClassStudent, ScheduleType, UserDingMapping } from '../entities';
|
import { AttendanceRecord, DingAttendanceRaw, Class, Student, ClassSchedule, ClassStudent, ScheduleType, StudentDingMapping } from '../entities';
|
||||||
import {
|
import {
|
||||||
BatchCreateAttendanceDto,
|
BatchCreateAttendanceDto,
|
||||||
AttendanceSummaryQueryDto,
|
AttendanceSummaryQueryDto,
|
||||||
@@ -33,8 +33,8 @@ export class AttendanceService {
|
|||||||
private scheduleRepo: Repository<ClassSchedule>,
|
private scheduleRepo: Repository<ClassSchedule>,
|
||||||
@InjectRepository(ClassStudent)
|
@InjectRepository(ClassStudent)
|
||||||
private classStudentRepo: Repository<ClassStudent>,
|
private classStudentRepo: Repository<ClassStudent>,
|
||||||
@InjectRepository(UserDingMapping)
|
@InjectRepository(StudentDingMapping)
|
||||||
private userDingMappingRepo: Repository<UserDingMapping>,
|
private studentDingMappingRepo: Repository<StudentDingMapping>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// ── Batch create attendance records ──
|
// ── Batch create attendance records ──
|
||||||
@@ -363,10 +363,10 @@ export class AttendanceService {
|
|||||||
if (unmatched.length === 0) return { matched: 0, total: 0 };
|
if (unmatched.length === 0) return { matched: 0, total: 0 };
|
||||||
|
|
||||||
// Build dingUserId → userId map from the mapping table
|
// Build dingUserId → userId map from the mapping table
|
||||||
const mappings = await this.userDingMappingRepo.find();
|
const mappings = await this.studentDingMappingRepo.find();
|
||||||
const dingToUserId = new Map<string, number>();
|
const dingToUserId = new Map<string, number>();
|
||||||
for (const m of mappings) {
|
for (const m of mappings) {
|
||||||
dingToUserId.set(m.dingUserId, m.userId);
|
dingToUserId.set(m.dingUserId, m.studentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build userId → studentId map (only students linked to a user)
|
// Build userId → studentId map (only students linked to a user)
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ export { LearningRecord } from './learning-record.entity';
|
|||||||
export { ResultArchive } from './result-archive.entity';
|
export { ResultArchive } from './result-archive.entity';
|
||||||
export { ArchiveAttachment } from './archive-attachment.entity';
|
export { ArchiveAttachment } from './archive-attachment.entity';
|
||||||
export { StudentReport } from './student-report.entity';
|
export { StudentReport } from './student-report.entity';
|
||||||
export { UserDingMapping } from './user-ding-mapping.entity';
|
export { StudentDingMapping } from './student-ding-mapping.entity';
|
||||||
|
|||||||
24
apps/server/src/entities/student-ding-mapping.entity.ts
Normal file
24
apps/server/src/entities/student-ding-mapping.entity.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
|
||||||
|
ManyToOne, JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Student } from './student.entity';
|
||||||
|
|
||||||
|
@Entity('student_ding_mapping')
|
||||||
|
export class StudentDingMapping {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ name: 'ding_user_id', length: 100, unique: true })
|
||||||
|
dingUserId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'student_id', type: 'integer', unique: true })
|
||||||
|
studentId: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => Student, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'student_id' })
|
||||||
|
student: Student;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
Index,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { User } from './user.entity';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps DingTalk user IDs to local User records.
|
|
||||||
* Used for incremental sync and attendance auto-matching.
|
|
||||||
*
|
|
||||||
* Mapping chain:
|
|
||||||
* dingtalk userid → UserDingMapping.dingUserId → UserDingMapping.userId
|
|
||||||
* → User.id → Student.userId → Student.id
|
|
||||||
* → DingAttendanceRaw.dingUserId → UserDingMapping.dingUserId → auto-match
|
|
||||||
*/
|
|
||||||
@Entity('user_ding_mapping')
|
|
||||||
export class UserDingMapping {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
/** DingTalk user ID (from DingTalk API) */
|
|
||||||
@Column({ name: 'ding_user_id', length: 100, unique: true })
|
|
||||||
dingUserId: string;
|
|
||||||
|
|
||||||
/** Local User ID */
|
|
||||||
@Column({ name: 'user_id', type: 'integer', unique: true })
|
|
||||||
userId: number;
|
|
||||||
|
|
||||||
@ManyToOne(() => User, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'user_id' })
|
|
||||||
user: User;
|
|
||||||
|
|
||||||
/** DingTalk user name (cached for display) */
|
|
||||||
@Column({ name: 'ding_name', length: 100, nullable: true })
|
|
||||||
dingName: string;
|
|
||||||
|
|
||||||
/** DingTalk mobile (cached for display) */
|
|
||||||
@Column({ name: 'ding_mobile', length: 20, nullable: true })
|
|
||||||
dingMobile: string;
|
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn({ name: 'updated_at' })
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* 提供:
|
* 提供:
|
||||||
* - OAuth2 access_token(新版 API + 缓存)
|
* - OAuth2 access_token(新版 API + 缓存)
|
||||||
* - BFS 遍历所有部门 + 用户(带限流)
|
* - BFS 遍历所有部门 + 用户(带限流)
|
||||||
* - 用户同步(自动建 User + Student + UserDingMapping)
|
* - 用户同步(自动建 User + Student + StudentDingMapping)
|
||||||
*/
|
*/
|
||||||
import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common';
|
import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
@@ -14,7 +14,7 @@ import { Department } from '../entities/department.entity';
|
|||||||
import { User } from '../entities/user.entity';
|
import { User } from '../entities/user.entity';
|
||||||
import { Student } from '../entities/student.entity';
|
import { Student } from '../entities/student.entity';
|
||||||
import { Class } from '../entities/class.entity';
|
import { Class } from '../entities/class.entity';
|
||||||
import { UserDingMapping } from '../entities/user-ding-mapping.entity';
|
import { StudentDingMapping } from '../entities/student-ding-mapping.entity';
|
||||||
|
|
||||||
// ── Types ──
|
// ── Types ──
|
||||||
|
|
||||||
@@ -191,8 +191,8 @@ export class DingTalkService {
|
|||||||
private readonly userRepo: Repository<User>,
|
private readonly userRepo: Repository<User>,
|
||||||
@InjectRepository(Student)
|
@InjectRepository(Student)
|
||||||
private readonly studentRepo: Repository<Student>,
|
private readonly studentRepo: Repository<Student>,
|
||||||
@InjectRepository(UserDingMapping)
|
@InjectRepository(StudentDingMapping)
|
||||||
private readonly mappingRepo: Repository<UserDingMapping>,
|
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
|
||||||
@InjectRepository(Class)
|
@InjectRepository(Class)
|
||||||
private readonly classRepo: Repository<Class>,
|
private readonly classRepo: Repository<Class>,
|
||||||
) {}
|
) {}
|
||||||
@@ -608,18 +608,16 @@ export class DingTalkService {
|
|||||||
mobile: string;
|
mobile: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
// Look up by mapping first
|
// Look up by mapping first
|
||||||
let mapping = await this.mappingRepo.findOne({ where: { dingUserId: du.userid } });
|
let mapping = await this.studentDingMappingRepo.findOne({ where: { dingUserId: du.userid } });
|
||||||
let user: User | null = null;
|
let user: User | null = null;
|
||||||
|
|
||||||
if (mapping) {
|
if (mapping) {
|
||||||
user = await this.userRepo.findOne({ where: { id: mapping.userId } });
|
user = await this.userRepo.findOne({ where: { id: mapping.studentId } });
|
||||||
if (user) {
|
if (user) {
|
||||||
user.name = du.name;
|
user.name = du.name;
|
||||||
await this.userRepo.save(user);
|
await this.userRepo.save(user);
|
||||||
}
|
}
|
||||||
mapping.dingName = du.name;
|
await this.studentDingMappingRepo.save(mapping);
|
||||||
mapping.dingMobile = du.mobile;
|
|
||||||
await this.mappingRepo.save(mapping);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,13 +664,11 @@ export class DingTalkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create mapping
|
// Create mapping
|
||||||
mapping = this.mappingRepo.create({
|
mapping = this.studentDingMappingRepo.create({
|
||||||
dingUserId: du.userid,
|
dingUserId: du.userid,
|
||||||
userId: user.id,
|
studentId: user.id,
|
||||||
dingName: du.name,
|
|
||||||
dingMobile: du.mobile,
|
|
||||||
});
|
});
|
||||||
await this.mappingRepo.save(mapping);
|
await this.studentDingMappingRepo.save(mapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Department, User, Student, UserDingMapping, Class } from '../entities';
|
import { Department, User, Student, StudentDingMapping, Class } from '../entities';
|
||||||
import { DingTalkService } from './dingtalk.service';
|
import { DingTalkService } from './dingtalk.service';
|
||||||
import { WeComService } from './wecom.service';
|
import { WeComService } from './wecom.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Department, User, Student, UserDingMapping, Class])],
|
imports: [TypeOrmModule.forFeature([Department, User, Student, StudentDingMapping, Class])],
|
||||||
providers: [DingTalkService, WeComService],
|
providers: [DingTalkService, WeComService],
|
||||||
exports: [DingTalkService, WeComService],
|
exports: [DingTalkService, WeComService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -80,20 +80,12 @@ export class UpdateProfileDto {
|
|||||||
qualifications?: string;
|
qualifications?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateUserDingMappingDto {
|
export class CreateStudentDingMappingDto {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
userId: number;
|
studentId: number;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
dingUserId: string;
|
dingUserId: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
dingName?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
dingMobile?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
UpdateUserDto,
|
UpdateUserDto,
|
||||||
ResetPasswordDto,
|
ResetPasswordDto,
|
||||||
UpdateProfileDto,
|
UpdateProfileDto,
|
||||||
CreateUserDingMappingDto,
|
CreateStudentDingMappingDto,
|
||||||
} from './dto/rbac.dto';
|
} from './dto/rbac.dto';
|
||||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||||
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||||
@@ -304,7 +304,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Get('user-ding-mappings')
|
@Get('user-ding-mappings')
|
||||||
@RequirePermission('user:view')
|
@RequirePermission('user:view')
|
||||||
async getUserDingMappings() {
|
async getStudentDingMappings() {
|
||||||
return this.rbacService.getUserDingMappings();
|
return this.rbacService.getUserDingMappings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,13 +316,13 @@ export class RbacController {
|
|||||||
|
|
||||||
@Post('user-ding-mappings')
|
@Post('user-ding-mappings')
|
||||||
@RequirePermission('user:edit')
|
@RequirePermission('user:edit')
|
||||||
async createUserDingMapping(@Body() dto: CreateUserDingMappingDto) {
|
async createStudentDingMapping(@Body() dto: CreateStudentDingMappingDto) {
|
||||||
return this.rbacService.createUserDingMapping(dto);
|
return this.rbacService.createUserDingMapping(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('user-ding-mappings/:id')
|
@Delete('user-ding-mappings/:id')
|
||||||
@RequirePermission('user:delete')
|
@RequirePermission('user:delete')
|
||||||
async deleteUserDingMapping(@Param('id') id: string) {
|
async deleteStudentDingMapping(@Param('id') id: string) {
|
||||||
return this.rbacService.deleteUserDingMapping(+id);
|
return this.rbacService.deleteUserDingMapping(+id);
|
||||||
}
|
}
|
||||||
// ---- 教师工作台 ----
|
// ---- 教师工作台 ----
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Module, OnModuleInit, forwardRef } from '@nestjs/common';
|
import { Module, OnModuleInit, forwardRef } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student, UserDingMapping } from '../entities';
|
import { Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student, StudentDingMapping } from '../entities';
|
||||||
import { RbacService } from './rbac.service';
|
import { RbacService } from './rbac.service';
|
||||||
import { RbacController } from './rbac.controller';
|
import { RbacController } from './rbac.controller';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student, UserDingMapping]), forwardRef(() => AuthModule)],
|
imports: [TypeOrmModule.forFeature([Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student, StudentDingMapping]), forwardRef(() => AuthModule)],
|
||||||
controllers: [RbacController],
|
controllers: [RbacController],
|
||||||
providers: [RbacService],
|
providers: [RbacService],
|
||||||
exports: [RbacService],
|
exports: [RbacService],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable, Logger, NotFoundException, ConflictException } from '@nestj
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, In } from 'typeorm';
|
import { Repository, In } from 'typeorm';
|
||||||
import * as bcrypt from 'bcryptjs';
|
import * as bcrypt from 'bcryptjs';
|
||||||
import { Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student, UserDingMapping } from '../entities';
|
import { Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student, StudentDingMapping } from '../entities';
|
||||||
|
|
||||||
const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = [
|
const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = [
|
||||||
{ code: 'dashboard:view', name: '查看数据面板', group: 'dashboard' },
|
{ code: 'dashboard:view', name: '查看数据面板', group: 'dashboard' },
|
||||||
@@ -171,7 +171,7 @@ export class RbacService {
|
|||||||
@InjectRepository(ClassTeacher) private classTeacherRepo: Repository<ClassTeacher>,
|
@InjectRepository(ClassTeacher) private classTeacherRepo: Repository<ClassTeacher>,
|
||||||
@InjectRepository(ClassSchedule) private classScheduleRepo: Repository<ClassSchedule>,
|
@InjectRepository(ClassSchedule) private classScheduleRepo: Repository<ClassSchedule>,
|
||||||
@InjectRepository(Student) private studentRepo: Repository<Student>,
|
@InjectRepository(Student) private studentRepo: Repository<Student>,
|
||||||
@InjectRepository(UserDingMapping) private mappingRepo: Repository<UserDingMapping>,
|
@InjectRepository(StudentDingMapping) private mappingRepo: Repository<StudentDingMapping>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async seedData(): Promise<void> {
|
async seedData(): Promise<void> {
|
||||||
@@ -249,15 +249,15 @@ export class RbacService {
|
|||||||
|
|
||||||
// ---- 钉钉用户绑定 ----
|
// ---- 钉钉用户绑定 ----
|
||||||
|
|
||||||
async getUserDingMappings(): Promise<UserDingMapping[]> {
|
async getUserDingMappings(): Promise<StudentDingMapping[]> {
|
||||||
return this.mappingRepo.find({ relations: ['user'] });
|
return this.mappingRepo.find({ relations: ['student'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUserDingMapping(dto: { userId: number; dingUserId: string; dingName?: string; dingMobile?: string }) {
|
async createUserDingMapping(dto: { studentId: number; dingUserId: string }) {
|
||||||
const existing = await this.mappingRepo.findOne({ where: { dingUserId: dto.dingUserId } });
|
const existing = await this.mappingRepo.findOne({ where: { dingUserId: dto.dingUserId } });
|
||||||
if (existing) throw new ConflictException(`钉钉用户 ${dto.dingUserId} 已绑定到本地用户 #${existing.userId}`);
|
if (existing) throw new ConflictException(`钉钉用户 ${dto.dingUserId} 已绑定到学生 #${existing.studentId}`);
|
||||||
const userExisting = await this.mappingRepo.findOne({ where: { userId: dto.userId } });
|
const studentExisting = await this.mappingRepo.findOne({ where: { studentId: dto.studentId } });
|
||||||
if (userExisting) throw new ConflictException(`本地用户 #${dto.userId} 已绑定到钉钉用户 ${userExisting.dingUserId}`);
|
if (studentExisting) throw new ConflictException(`学生 #${dto.studentId} 已绑定到钉钉用户 ${studentExisting.dingUserId}`);
|
||||||
const mapping = this.mappingRepo.create(dto);
|
const mapping = this.mappingRepo.create(dto);
|
||||||
return this.mappingRepo.save(mapping);
|
return this.mappingRepo.save(mapping);
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ export class RbacService {
|
|||||||
async getUnboundUsers(): Promise<{ id: number; username: string; name: string }[]> {
|
async getUnboundUsers(): Promise<{ id: number; username: string; name: string }[]> {
|
||||||
return this.userRepo
|
return this.userRepo
|
||||||
.createQueryBuilder('u')
|
.createQueryBuilder('u')
|
||||||
.leftJoin(UserDingMapping, 'm', 'm.userId = u.id')
|
.leftJoin(StudentDingMapping, 'm', 'm.studentId = u.id')
|
||||||
.where('m.id IS NULL')
|
.where('m.id IS NULL')
|
||||||
.andWhere('u.isArchived = false')
|
.andWhere('u.isArchived = false')
|
||||||
.select(['u.id', 'u.username', 'u.name'])
|
.select(['u.id', 'u.username', 'u.name'])
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository, In, Not, IsNull } from 'typeorm';
|
import { Repository, In, Not, IsNull } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
ClassSchedule,
|
ClassSchedule,
|
||||||
UserDingMapping,
|
StudentDingMapping,
|
||||||
ClassStudent,
|
ClassStudent,
|
||||||
ClassTeacher,
|
ClassTeacher,
|
||||||
Department,
|
Department,
|
||||||
@@ -47,8 +47,8 @@ export class ScheduleSyncService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(ClassSchedule)
|
@InjectRepository(ClassSchedule)
|
||||||
private readonly scheduleRepo: Repository<ClassSchedule>,
|
private readonly scheduleRepo: Repository<ClassSchedule>,
|
||||||
@InjectRepository(UserDingMapping)
|
@InjectRepository(StudentDingMapping)
|
||||||
private readonly mappingRepo: Repository<UserDingMapping>,
|
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
|
||||||
@InjectRepository(ClassTeacher)
|
@InjectRepository(ClassTeacher)
|
||||||
private readonly classTeacherRepo: Repository<ClassTeacher>,
|
private readonly classTeacherRepo: Repository<ClassTeacher>,
|
||||||
@InjectRepository(Department)
|
@InjectRepository(Department)
|
||||||
@@ -88,10 +88,10 @@ export class ScheduleSyncService {
|
|||||||
|
|
||||||
// ── Step 2: 获取教师→钉钉用户ID映射 ──
|
// ── Step 2: 获取教师→钉钉用户ID映射 ──
|
||||||
const teacherIds = [...new Set(schedules.map((s) => s.teacherId!).filter(Boolean))];
|
const teacherIds = [...new Set(schedules.map((s) => s.teacherId!).filter(Boolean))];
|
||||||
const mappings = await this.mappingRepo.find({
|
const mappings = await this.studentDingMappingRepo.find({
|
||||||
where: { userId: In(teacherIds) },
|
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) 创建/匹配班次 ──
|
// ── Step 3: 按 (startTime, endTime) 创建/匹配班次 ──
|
||||||
const shiftKey = (start: string, end: string) => `${start}-${end}`;
|
const shiftKey = (start: string, end: string) => `${start}-${end}`;
|
||||||
@@ -288,8 +288,8 @@ export class ScheduleSyncService {
|
|||||||
where: { status: 'active', teacherId: Not(IsNull()) },
|
where: { status: 'active', teacherId: Not(IsNull()) },
|
||||||
});
|
});
|
||||||
const teacherIds = [...new Set(schedules.map((s) => s.teacherId!).filter(Boolean))];
|
const teacherIds = [...new Set(schedules.map((s) => s.teacherId!).filter(Boolean))];
|
||||||
const mappings = await this.mappingRepo.find({
|
const mappings = await this.studentDingMappingRepo.find({
|
||||||
where: { userId: In(teacherIds) },
|
where: { studentId: In(teacherIds) },
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
activeSchedules: schedules.length,
|
activeSchedules: schedules.length,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { AttendanceModule } from '../attendance/attendance.module';
|
|||||||
import {
|
import {
|
||||||
SyncLog,
|
SyncLog,
|
||||||
SyncState,
|
SyncState,
|
||||||
UserDingMapping,
|
StudentDingMapping,
|
||||||
ClassSchedule,
|
ClassSchedule,
|
||||||
Department,
|
Department,
|
||||||
UserDepartment,
|
UserDepartment,
|
||||||
@@ -24,7 +24,7 @@ import { ScheduleSyncService } from './schedule-sync.service';
|
|||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
SyncLog,
|
SyncLog,
|
||||||
SyncState,
|
SyncState,
|
||||||
UserDingMapping,
|
StudentDingMapping,
|
||||||
ClassSchedule,
|
ClassSchedule,
|
||||||
Department,
|
Department,
|
||||||
UserDepartment,
|
UserDepartment,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
import { DataSource, Repository } from 'typeorm';
|
import { DataSource, Repository } from 'typeorm';
|
||||||
import { SyncService, ImportUserDto } from './sync.service';
|
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 { Class as ClassEntity } from '../entities/class.entity';
|
||||||
import { User } from '../entities/user.entity';
|
import { User } from '../entities/user.entity';
|
||||||
import { Student } from '../entities/student.entity';
|
import { Student } from '../entities/student.entity';
|
||||||
@@ -36,7 +36,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
|||||||
let service: SyncService;
|
let service: SyncService;
|
||||||
|
|
||||||
let mappingRepo: jest.Mocked<
|
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 userRepo: jest.Mocked<Pick<Repository<User>, 'create' | 'save'>>;
|
||||||
let studentRepo: jest.Mocked<Pick<Repository<Student>, 'create' | 'save'>>;
|
let studentRepo: jest.Mocked<Pick<Repository<Student>, 'create' | 'save'>>;
|
||||||
@@ -109,7 +109,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
|||||||
SyncService,
|
SyncService,
|
||||||
{ provide: getRepositoryToken(SyncLog), useValue: { create: jest.fn(), save: jest.fn(), find: jest.fn(), findOne: jest.fn() } },
|
{ 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(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(User), useValue: userRepo },
|
||||||
{ provide: getRepositoryToken(Student), useValue: studentRepo },
|
{ provide: getRepositoryToken(Student), useValue: studentRepo },
|
||||||
{ provide: getRepositoryToken(Role), useValue: roleRepo },
|
{ provide: getRepositoryToken(Role), useValue: roleRepo },
|
||||||
@@ -142,7 +142,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
|||||||
it('imports teacher when roleId is a number (role found)', async () => {
|
it('imports teacher when roleId is a number (role found)', async () => {
|
||||||
const mockRole = { id: 5, name: 'Teacher' } as Role;
|
const mockRole = { id: 5, name: 'Teacher' } as Role;
|
||||||
mgr.findOne.mockImplementation(async (entityClass: unknown) => {
|
mgr.findOne.mockImplementation(async (entityClass: unknown) => {
|
||||||
if (entityClass === UserDingMapping) return null;
|
if (entityClass === StudentDingMapping) return null;
|
||||||
if (entityClass === Role) return mockRole;
|
if (entityClass === Role) return mockRole;
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@@ -170,7 +170,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('throws when role not found', async () => {
|
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);
|
mgr.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
const mockUser = { id: 11 } as User;
|
const mockUser = { id: 11 } as User;
|
||||||
@@ -194,8 +194,8 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
|||||||
mgr.save.mockResolvedValueOnce(mockStudent);
|
mgr.save.mockResolvedValueOnce(mockStudent);
|
||||||
|
|
||||||
// mapping create+save also calls create/save
|
// mapping create+save also calls create/save
|
||||||
mgr.create.mockReturnValueOnce({} as UserDingMapping);
|
mgr.create.mockReturnValueOnce({} as StudentDingMapping);
|
||||||
mgr.save.mockResolvedValueOnce({} as UserDingMapping);
|
mgr.save.mockResolvedValueOnce({} as StudentDingMapping);
|
||||||
|
|
||||||
mappingRepo.findOne.mockResolvedValue(null);
|
mappingRepo.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
it('skips user when mapping already exists', async () => {
|
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 });
|
mgr.findOne.mockResolvedValue({ id: 1 });
|
||||||
|
|
||||||
const users: ImportUserDto[] = [
|
const users: ImportUserDto[] = [
|
||||||
@@ -259,10 +259,10 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
|||||||
|
|
||||||
// Single transaction: User 1→skip, User 2→teacher, User 3→student
|
// Single transaction: User 1→skip, User 2→teacher, User 3→student
|
||||||
mgr.findOne
|
mgr.findOne
|
||||||
.mockResolvedValueOnce({ id: 99 }) // User 1: UserDingMapping → skip
|
.mockResolvedValueOnce({ id: 99 }) // User 1: StudentDingMapping → skip
|
||||||
.mockResolvedValueOnce(null) // User 2: UserDingMapping → not skipped
|
.mockResolvedValueOnce(null) // User 2: StudentDingMapping → not skipped
|
||||||
.mockResolvedValueOnce(mockRole) // User 2: Role lookup
|
.mockResolvedValueOnce(mockRole) // User 2: Role lookup
|
||||||
.mockResolvedValueOnce(null); // User 3: UserDingMapping → not skipped
|
.mockResolvedValueOnce(null); // User 3: StudentDingMapping → not skipped
|
||||||
|
|
||||||
const users: ImportUserDto[] = [
|
const users: ImportUserDto[] = [
|
||||||
{ dingUserId: 'skip', name: 'Skip', mobile: '138', roleId: null, dingDeptIds: [] },
|
{ dingUserId: 'skip', name: 'Skip', mobile: '138', roleId: null, dingDeptIds: [] },
|
||||||
@@ -395,7 +395,7 @@ describe('SyncService — importDingTalkUsers (transactional)', () => {
|
|||||||
});
|
});
|
||||||
mgr.count.mockResolvedValue(0);
|
mgr.count.mockResolvedValue(0);
|
||||||
// save(ClassEntity, data) returns a class — but the smart mock already handles this
|
// 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({ id: 5 }); // Role lookup
|
||||||
mgr.findOne.mockResolvedValueOnce({ name: 'Empty Class' }); // Class lookup in empty check
|
mgr.findOne.mockResolvedValueOnce({ name: 'Empty Class' }); // Class lookup in empty check
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { DataSource, Repository } from '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 { Class as ClassEntity } from '../entities/class.entity';
|
||||||
import type { SyncPlatform, SyncType, SyncStatus } from '../entities/sync-log.entity';
|
import type { SyncPlatform, SyncType, SyncStatus } from '../entities/sync-log.entity';
|
||||||
import { DingTalkService } from '../integration/dingtalk.service';
|
import { DingTalkService } from '../integration/dingtalk.service';
|
||||||
@@ -30,8 +30,8 @@ export class SyncService {
|
|||||||
private readonly syncLogRepo: Repository<SyncLog>,
|
private readonly syncLogRepo: Repository<SyncLog>,
|
||||||
@InjectRepository(SyncState)
|
@InjectRepository(SyncState)
|
||||||
private readonly syncStateRepo: Repository<SyncState>,
|
private readonly syncStateRepo: Repository<SyncState>,
|
||||||
@InjectRepository(UserDingMapping)
|
@InjectRepository(StudentDingMapping)
|
||||||
private readonly mappingRepo: Repository<UserDingMapping>,
|
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
|
||||||
@InjectRepository(User)
|
@InjectRepository(User)
|
||||||
private readonly userRepo: Repository<User>,
|
private readonly userRepo: Repository<User>,
|
||||||
@InjectRepository(Student)
|
@InjectRepository(Student)
|
||||||
@@ -123,7 +123,7 @@ export class SyncService {
|
|||||||
/**
|
/**
|
||||||
* 从钉钉导入用户:roleId 非 null → 老师(User + 指定角色),roleId null → 学生(User + Student)。
|
* 从钉钉导入用户:roleId 非 null → 老师(User + 指定角色),roleId null → 学生(User + Student)。
|
||||||
* 支持同时创建班级并建立师生关联。
|
* 支持同时创建班级并建立师生关联。
|
||||||
* 已存在 UserDingMapping 的记录跳过。
|
* 已存在 StudentDingMapping 的记录跳过。
|
||||||
*/
|
*/
|
||||||
async importDingTalkUsers(
|
async importDingTalkUsers(
|
||||||
users: ImportUserDto[],
|
users: ImportUserDto[],
|
||||||
@@ -190,7 +190,7 @@ export class SyncService {
|
|||||||
|
|
||||||
// 2. 导入用户(逐用户)
|
// 2. 导入用户(逐用户)
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
const existingMapping = await manager.findOne(UserDingMapping, {
|
const existingMapping = await manager.findOne(StudentDingMapping, {
|
||||||
where: { dingUserId: u.dingUserId },
|
where: { dingUserId: u.dingUserId },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ export class SyncService {
|
|||||||
|
|
||||||
if (existingMapping) {
|
if (existingMapping) {
|
||||||
skipped++;
|
skipped++;
|
||||||
userId = existingMapping.userId;
|
userId = existingMapping.studentId;
|
||||||
// 判断是老师还是学生:查角色
|
// 判断是老师还是学生:查角色
|
||||||
const existingUser = await manager.findOne(User, {
|
const existingUser = await manager.findOne(User, {
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
@@ -258,15 +258,13 @@ export class SyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 钉钉映射(幂等:可能已被 syncAll 创建)
|
// 钉钉映射(幂等:可能已被 syncAll 创建)
|
||||||
const existingMappingForUser = await manager.findOne(UserDingMapping, {
|
const existingMappingForUser = await manager.findOne(StudentDingMapping, {
|
||||||
where: { dingUserId: u.dingUserId },
|
where: { dingUserId: u.dingUserId },
|
||||||
});
|
});
|
||||||
if (!existingMappingForUser) {
|
if (!existingMappingForUser) {
|
||||||
const mapping = manager.create(UserDingMapping, {
|
const mapping = manager.create(StudentDingMapping, {
|
||||||
dingUserId: u.dingUserId,
|
dingUserId: u.dingUserId,
|
||||||
userId: user.id,
|
studentId: user.id,
|
||||||
dingName: u.name,
|
|
||||||
dingMobile: u.mobile,
|
|
||||||
});
|
});
|
||||||
await manager.save(mapping);
|
await manager.save(mapping);
|
||||||
}
|
}
|
||||||
@@ -422,7 +420,7 @@ export class SyncService {
|
|||||||
const end = endDate.toISOString().slice(0, 10);
|
const end = endDate.toISOString().slice(0, 10);
|
||||||
|
|
||||||
this.logger.log(`Importing DingTalk attendance: ${start} ~ ${end}`);
|
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 userIds = mappings.map((m) => m.dingUserId);
|
||||||
const importResult = await this.attendanceImportService.importFromDingTalk({
|
const importResult = await this.attendanceImportService.importFromDingTalk({
|
||||||
startDate: start,
|
startDate: start,
|
||||||
|
|||||||
Reference in New Issue
Block a user