feat: add batch-import students to class endpoint

This commit is contained in:
2026-07-09 17:12:23 +08:00
parent 7b8691866e
commit 91035e9f2c
4 changed files with 80 additions and 6 deletions

View File

@@ -21,6 +21,7 @@ import {
AddTeacherDto,
QueryClassScheduleDto,
QueryClassAttendanceSummaryDto,
BatchImportStudentsDto,
} from './dto/class.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
@@ -107,6 +108,16 @@ export class ClassesController {
return result;
}
/** 批量导入学生到班级通过钉钉用户ID */
@Post(':id/students/import')
@RequirePermission('class:edit')
async batchImportStudents(
@Param('id') id: string,
@Body() dto: BatchImportStudentsDto,
) {
return this.service.batchImportStudents(+id, dto.dingUserIds);
}
/** 归档班级 */
@Put(':id/archive')
@RequirePermission('class:edit')

View File

@@ -1,14 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommonModule } from '../common/common.module';
import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Department } from '../entities';
import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Department, Student, StudentDingMapping } from '../entities';
import { ClassesService } from './classes.service';
import { ClassesController } from './classes.controller';
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [TypeOrmModule.forFeature([Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Department]), OperationLogsModule, NotificationsModule, CommonModule],
imports: [TypeOrmModule.forFeature([Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Department, Student, StudentDingMapping]), OperationLogsModule, NotificationsModule, CommonModule],
controllers: [ClassesController],
providers: [ClassesService],
exports: [ClassesService],

View File

@@ -1,8 +1,8 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In, Like } from 'typeorm';
import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Department, Classroom } from '../entities';
import { CreateClassDto, UpdateClassDto, QueryClassDto, AddTeacherDto, QueryClassScheduleDto, QueryClassAttendanceSummaryDto } from './dto/class.dto';
import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Department, Classroom, Student, StudentDingMapping } from '../entities';
import { CreateClassDto, UpdateClassDto, QueryClassDto, AddTeacherDto, QueryClassScheduleDto, QueryClassAttendanceSummaryDto, BatchImportStudentsDto } from './dto/class.dto';
interface RawStudentCount {
classId: string;
@@ -24,6 +24,10 @@ export class ClassesService {
private attendanceRepo: Repository<AttendanceRecord>,
@InjectRepository(Department)
private deptRepo: Repository<Department>,
@InjectRepository(Student)
private studentRepo: Repository<Student>,
@InjectRepository(StudentDingMapping)
private studentDingMappingRepo: Repository<StudentDingMapping>,
) {}
async findAll(query: QueryClassDto) {
@@ -93,7 +97,7 @@ export class ClassesService {
}
async create(dto: CreateClassDto) {
const { studentIds, teachers, ...classData } = dto;
const { studentIds, teachers, dingUserIds, ...classData } = dto;
const cls = this.classRepo.create(classData);
const saved = await this.classRepo.save(cls);
@@ -117,9 +121,56 @@ export class ClassesService {
await this.syncClassTeacherIds(saved.id);
}
// batch import students by dingUserIds
if (dingUserIds?.length) {
await this.batchImportStudents(saved.id, dingUserIds);
}
return this.findOne(saved.id);
}
async batchImportStudents(classId: number, dingUserIds: string[]): Promise<{ imported: number; skipped: number }> {
const classEntity = await this.classRepo.findOne({ where: { id: classId } });
if (!classEntity) throw new NotFoundException('班级不存在');
let imported = 0;
let skipped = 0;
for (const dingUserId of dingUserIds) {
let mapping = await this.studentDingMappingRepo.findOne({ where: { dingUserId } });
let studentId: number;
if (mapping) {
studentId = mapping.studentId;
} else {
const student = this.studentRepo.create({
name: `dd_${dingUserId}`,
status: 'active',
departmentId: classEntity.departmentId ?? undefined,
});
const saved = await this.studentRepo.save(student);
studentId = saved.id;
mapping = this.studentDingMappingRepo.create({ dingUserId, studentId });
await this.studentDingMappingRepo.save(mapping);
}
const existing = await this.classStudentRepo.findOne({
where: { classId, studentId },
});
if (existing) { skipped++; continue; }
await this.classStudentRepo.save(
this.classStudentRepo.create({
classId, studentId, status: 'active',
joinDate: new Date().toISOString().slice(0, 10),
}),
);
imported++;
}
return { imported, skipped };
}
async update(id: number, dto: UpdateClassDto) {
const cls = await this.classRepo.findOne({ where: { id } });
if (!cls) throw new NotFoundException('班级不存在');

View File

@@ -1,4 +1,4 @@
import { IsOptional, IsString, IsNotEmpty, IsInt, IsArray, IsDateString, IsEnum } from 'class-validator';
import { IsOptional, IsString, IsNotEmpty, IsInt, IsArray, IsDateString, IsEnum, ArrayNotEmpty } from 'class-validator';
import { Type } from 'class-transformer';
import { ClassType, ClassStatus, TeacherRoleType } from '../../entities';
@@ -43,6 +43,11 @@ export class CreateClassDto {
studentIds?: number[];
@IsOptional() @IsArray()
@IsArray()
@IsString({ each: true })
@IsOptional()
dingUserIds?: string[];
teachers?: Array<{ userId: number; roleType: string; subject?: string }>;
}
@@ -135,3 +140,10 @@ export class QueryClassAttendanceSummaryDto {
@IsOptional() @IsDateString()
endDate?: string;
}
export class BatchImportStudentsDto {
@IsArray()
@IsString({ each: true })
@ArrayNotEmpty()
dingUserIds: string[];
}