refactor: remove departmentId from Class entity
This commit is contained in:
@@ -236,13 +236,7 @@ const IntegrationConfigPage: React.FC = () => {
|
|||||||
const userIds = checkedKeys
|
const userIds = checkedKeys
|
||||||
.filter((k) => String(k).startsWith('user-'))
|
.filter((k) => String(k).startsWith('user-'))
|
||||||
.map((k) => String(k).replace('user-', ''));
|
.map((k) => String(k).replace('user-', ''));
|
||||||
const deptKeys = checkedKeys.filter((k) => String(k).startsWith('dept-'));
|
await api.post('/classes', { ...values, dingUserIds: userIds });
|
||||||
const deptId = deptKeys.length > 0
|
|
||||||
? Number(String(deptKeys[0]).replace('dept-', ''))
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
setImporting(true);
|
|
||||||
await api.post('/classes', { ...values, dingUserIds: userIds, departmentId: deptId });
|
|
||||||
message.success('班级创建成功');
|
message.success('班级创建成功');
|
||||||
setClassModalOpen(false);
|
setClassModalOpen(false);
|
||||||
classForm.resetFields();
|
classForm.resetFields();
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export class AttendanceService {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
source: 'schedule',
|
source: 'schedule',
|
||||||
});
|
});
|
||||||
entity.departmentId = cs.student?.departmentId ?? cls.departmentId ?? undefined;
|
entity.departmentId = cs.student?.departmentId ?? undefined;
|
||||||
entities.push(entity);
|
entities.push(entity);
|
||||||
existingKeys.add(key);
|
existingKeys.add(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,27 +86,6 @@ export class ClassesController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从钉钉同步部门创建班级 */
|
|
||||||
@Post('from-department')
|
|
||||||
@RequirePermission('class:create')
|
|
||||||
async createFromDepartment(
|
|
||||||
@Body() dto: { departmentId: number; name?: string; classType?: string },
|
|
||||||
@Request() req: any,
|
|
||||||
) {
|
|
||||||
const result = await this.service.createFromDepartment(dto);
|
|
||||||
await this.logService.log({
|
|
||||||
userId: req.user?.id,
|
|
||||||
username: req.user?.username,
|
|
||||||
module: '班级管理',
|
|
||||||
action: '从部门创建班级',
|
|
||||||
targetId: result.id,
|
|
||||||
targetType: 'class',
|
|
||||||
detail: `班级${result.code} ${result.name}`,
|
|
||||||
ipAddress: extractRequestInfo(req).ipAddress,
|
|
||||||
userAgent: extractRequestInfo(req).userAgent,
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 批量导入学生到班级(通过钉钉用户ID) */
|
/** 批量导入学生到班级(通过钉钉用户ID) */
|
||||||
@Post(':id/students/import')
|
@Post(':id/students/import')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, In, Like } from 'typeorm';
|
import { Repository, In, Like } from 'typeorm';
|
||||||
import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Department, Classroom, Student, StudentDingMapping } from '../entities';
|
import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Classroom, Student, StudentDingMapping } from '../entities';
|
||||||
import { CreateClassDto, UpdateClassDto, QueryClassDto, AddTeacherDto, QueryClassScheduleDto, QueryClassAttendanceSummaryDto, BatchImportStudentsDto } from './dto/class.dto';
|
import { CreateClassDto, UpdateClassDto, QueryClassDto, AddTeacherDto, QueryClassScheduleDto, QueryClassAttendanceSummaryDto, BatchImportStudentsDto } from './dto/class.dto';
|
||||||
|
|
||||||
interface RawStudentCount {
|
interface RawStudentCount {
|
||||||
@@ -22,8 +22,6 @@ export class ClassesService {
|
|||||||
private scheduleRepo: Repository<ClassSchedule>,
|
private scheduleRepo: Repository<ClassSchedule>,
|
||||||
@InjectRepository(AttendanceRecord)
|
@InjectRepository(AttendanceRecord)
|
||||||
private attendanceRepo: Repository<AttendanceRecord>,
|
private attendanceRepo: Repository<AttendanceRecord>,
|
||||||
@InjectRepository(Department)
|
|
||||||
private deptRepo: Repository<Department>,
|
|
||||||
@InjectRepository(Student)
|
@InjectRepository(Student)
|
||||||
private studentRepo: Repository<Student>,
|
private studentRepo: Repository<Student>,
|
||||||
@InjectRepository(StudentDingMapping)
|
@InjectRepository(StudentDingMapping)
|
||||||
@@ -32,7 +30,6 @@ export class ClassesService {
|
|||||||
|
|
||||||
async findAll(query: QueryClassDto) {
|
async findAll(query: QueryClassDto) {
|
||||||
let where: Record<string, unknown> = {};
|
let where: Record<string, unknown> = {};
|
||||||
if (query.departmentId) where.departmentId = query.departmentId;
|
|
||||||
if (query.status) where.status = query.status;
|
if (query.status) where.status = query.status;
|
||||||
if (query.classType) where.classType = query.classType;
|
if (query.classType) where.classType = query.classType;
|
||||||
if (query.keyword) where.name = Like(`%${query.keyword}%`);
|
if (query.keyword) where.name = Like(`%${query.keyword}%`);
|
||||||
@@ -99,21 +96,6 @@ export class ClassesService {
|
|||||||
async create(dto: CreateClassDto) {
|
async create(dto: CreateClassDto) {
|
||||||
const { studentIds, teachers, dingUserIds, ...classData } = dto;
|
const { studentIds, teachers, dingUserIds, ...classData } = dto;
|
||||||
|
|
||||||
// Resolve departmentId: frontend may pass DingTalk dept ID, map to local
|
|
||||||
if (classData.departmentId) {
|
|
||||||
const localDept = await this.deptRepo.findOne({
|
|
||||||
where: { source: 'dingtalk', sourceId: String(classData.departmentId) },
|
|
||||||
});
|
|
||||||
if (localDept) {
|
|
||||||
classData.departmentId = localDept.id;
|
|
||||||
} else {
|
|
||||||
// Verify it's a valid local department ID
|
|
||||||
const exists = await this.deptRepo.findOne({ where: { id: classData.departmentId } });
|
|
||||||
if (!exists) {
|
|
||||||
throw new BadRequestException(`部门 ID ${classData.departmentId} 不存在`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cls = this.classRepo.create(classData);
|
const cls = this.classRepo.create(classData);
|
||||||
const saved = await this.classRepo.save(cls);
|
const saved = await this.classRepo.save(cls);
|
||||||
@@ -165,7 +147,7 @@ export class ClassesService {
|
|||||||
this.studentRepo.create({
|
this.studentRepo.create({
|
||||||
name: `dd_${dingUserId}`,
|
name: `dd_${dingUserId}`,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
departmentId: classEntity.departmentId ?? undefined,
|
departmentId: undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const savedStudents = await this.studentRepo.save(newStudents);
|
const savedStudents = await this.studentRepo.save(newStudents);
|
||||||
@@ -240,32 +222,6 @@ export class ClassesService {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFromDepartment(dto: { departmentId: number; name?: string; classType?: string }) {
|
|
||||||
const dept = await this.deptRepo.findOne({
|
|
||||||
where: { id: dto.departmentId, source: 'dingtalk' },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!dept) {
|
|
||||||
throw new BadRequestException('所选部门不存在或非钉钉同步部门');
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = dto.name || dept.name;
|
|
||||||
const code = `DT_${dto.departmentId}`;
|
|
||||||
|
|
||||||
const existing = await this.classRepo.findOne({ where: { code } });
|
|
||||||
if (existing) {
|
|
||||||
throw new BadRequestException(`班级"${className}"已存在(编码: ${code})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cls = this.classRepo.create({
|
|
||||||
name: className,
|
|
||||||
code,
|
|
||||||
departmentId: dto.departmentId,
|
|
||||||
classType: dto.classType || 'culture',
|
|
||||||
status: 'enrolling',
|
|
||||||
});
|
|
||||||
return this.classRepo.save(cls);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStudents(classId: number) {
|
async getStudents(classId: number) {
|
||||||
return this.classStudentRepo.find({
|
return this.classStudentRepo.find({
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ export class CreateClassDto {
|
|||||||
@IsString() @IsNotEmpty()
|
@IsString() @IsNotEmpty()
|
||||||
code: string;
|
code: string;
|
||||||
|
|
||||||
@IsOptional() @IsInt()
|
|
||||||
departmentId?: number;
|
|
||||||
|
|
||||||
@IsEnum(ClassType) @IsString() @IsNotEmpty()
|
@IsEnum(ClassType) @IsString() @IsNotEmpty()
|
||||||
classType: string;
|
classType: string;
|
||||||
@@ -58,8 +56,6 @@ export class UpdateClassDto {
|
|||||||
@IsOptional() @IsString()
|
@IsOptional() @IsString()
|
||||||
code?: string;
|
code?: string;
|
||||||
|
|
||||||
@IsOptional() @IsInt()
|
|
||||||
departmentId?: number;
|
|
||||||
|
|
||||||
@IsEnum(ClassType) @IsOptional() @IsString()
|
@IsEnum(ClassType) @IsOptional() @IsString()
|
||||||
classType?: string;
|
classType?: string;
|
||||||
@@ -90,10 +86,6 @@ export class UpdateClassDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class QueryClassDto {
|
export class QueryClassDto {
|
||||||
@IsOptional()
|
|
||||||
@Type(() => Number)
|
|
||||||
@IsInt()
|
|
||||||
departmentId?: number;
|
|
||||||
|
|
||||||
@IsOptional() @IsString()
|
@IsOptional() @IsString()
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
export enum ClassType {
|
export enum ClassType {
|
||||||
@@ -33,8 +31,6 @@ export class Class {
|
|||||||
@Column({ name: 'code', length: 50, unique: true })
|
@Column({ name: 'code', length: 50, unique: true })
|
||||||
code: string;
|
code: string;
|
||||||
|
|
||||||
@Column({ name: 'department_id', type: 'integer', nullable: true })
|
|
||||||
departmentId: number;
|
|
||||||
|
|
||||||
|
|
||||||
@Column({ name: 'class_type', length: 20 })
|
@Column({ name: 'class_type', length: 20 })
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { Department } from '../entities/department.entity';
|
import { Department } from '../entities/department.entity';
|
||||||
import { Student } from '../entities/student.entity';
|
import { Student } from '../entities/student.entity';
|
||||||
import { Class } from '../entities/class.entity';
|
|
||||||
import { StudentDingMapping } from '../entities/student-ding-mapping.entity';
|
import { StudentDingMapping } from '../entities/student-ding-mapping.entity';
|
||||||
|
|
||||||
// ── Types ──
|
// ── Types ──
|
||||||
@@ -189,8 +188,6 @@ export class DingTalkService {
|
|||||||
private readonly studentRepo: Repository<Student>,
|
private readonly studentRepo: Repository<Student>,
|
||||||
@InjectRepository(StudentDingMapping)
|
@InjectRepository(StudentDingMapping)
|
||||||
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
|
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
|
||||||
@InjectRepository(Class)
|
|
||||||
private readonly classRepo: Repository<Class>,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private get configured(): boolean {
|
private get configured(): boolean {
|
||||||
@@ -381,26 +378,6 @@ export class DingTalkService {
|
|||||||
}
|
}
|
||||||
await this.deptRepo.save(syncedDepts);
|
await this.deptRepo.save(syncedDepts);
|
||||||
|
|
||||||
// ── Step 2.5: Auto-create Class for leaf departments ──
|
|
||||||
const parentIds = new Set(syncedDepts.map((d) => d.parentSourceId));
|
|
||||||
const leafDepts = syncedDepts.filter((d) => !parentIds.has(d.sourceId));
|
|
||||||
let classCreated = 0;
|
|
||||||
for (const leaf of leafDepts) {
|
|
||||||
const code = `DT_${leaf.sourceId}`;
|
|
||||||
const exists = await this.classRepo.findOne({ where: { code } });
|
|
||||||
if (!exists) {
|
|
||||||
const cls = this.classRepo.create({
|
|
||||||
name: leaf.name,
|
|
||||||
code,
|
|
||||||
departmentId: leaf.id,
|
|
||||||
classType: 'culture',
|
|
||||||
status: 'enrolling',
|
|
||||||
});
|
|
||||||
await this.classRepo.save(cls);
|
|
||||||
classCreated++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (classCreated > 0) this.logger.log(`从钉钉叶子部门自动创建 ${classCreated} 个班级`);
|
|
||||||
|
|
||||||
// ── Step 3: Sync users per department ──
|
// ── Step 3: Sync users per department ──
|
||||||
let userCount = 0;
|
let userCount = 0;
|
||||||
|
|||||||
@@ -52,9 +52,6 @@ export class SchedulesService {
|
|||||||
const schedule = this.scheduleRepo.create(dto);
|
const schedule = this.scheduleRepo.create(dto);
|
||||||
if (dto.departmentId) {
|
if (dto.departmentId) {
|
||||||
schedule.departmentId = dto.departmentId;
|
schedule.departmentId = dto.departmentId;
|
||||||
} else if (dto.classId) {
|
|
||||||
const cls = await this.classRepo.findOne({ where: { id: dto.classId } });
|
|
||||||
if (cls) schedule.departmentId = cls.departmentId;
|
|
||||||
}
|
}
|
||||||
const saved = await this.scheduleRepo.save(schedule);
|
const saved = await this.scheduleRepo.save(schedule);
|
||||||
return this.findOne(saved.id);
|
return this.findOne(saved.id);
|
||||||
|
|||||||
@@ -42,9 +42,6 @@ export class StudentsService {
|
|||||||
const entity = this.repo.create(dto);
|
const entity = this.repo.create(dto);
|
||||||
if (dto.departmentId) {
|
if (dto.departmentId) {
|
||||||
entity.departmentId = dto.departmentId;
|
entity.departmentId = dto.departmentId;
|
||||||
} else if (dto.classId) {
|
|
||||||
const cls = await this.classRepo.findOne({ where: { id: dto.classId } });
|
|
||||||
if (cls) entity.departmentId = cls.departmentId;
|
|
||||||
}
|
}
|
||||||
return this.repo.save(entity);
|
return this.repo.save(entity);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user