feat: add DepartmentsModule with DTO, Service, and stub controller

This commit is contained in:
2026-07-05 23:47:41 +08:00
parent ad24c9ef23
commit 0e08770cf4
5 changed files with 175 additions and 2 deletions

View File

@@ -30,6 +30,8 @@ import {
SyncLog,
SyncState,
Notification,
Department,
UserDepartment,
} from './entities';
import { AuthModule } from './auth/auth.module';
import { RbacModule } from './rbac/rbac.module';
@@ -51,6 +53,7 @@ import { SchedulesModule } from './schedules/schedules.module';
import { ClassroomRentalsModule } from './classroom-rentals/classroom-rentals.module';
import { SyncModule } from './sync/sync.module';
import { NotificationsModule } from './notifications/notifications.module';
import { DepartmentsModule } from './departments/departments.module';
@Module({
imports: [
@@ -90,9 +93,9 @@ import { NotificationsModule } from './notifications/notifications.module';
ClassSchedule,
AttendanceRecord,
DingAttendanceRaw,
SyncLog,
SyncState,
Notification,
Department,
UserDepartment,
];
if (dbType === 'mysql') {
return {
@@ -133,6 +136,7 @@ import { NotificationsModule } from './notifications/notifications.module';
ClassroomRentalsModule,
SyncModule,
NotificationsModule,
DepartmentsModule,
],
providers: [
{ provide: APP_GUARD, useClass: ThrottlerGuard },

View File

@@ -0,0 +1,7 @@
import { Controller } from '@nestjs/common';
import { DepartmentsService } from './departments.service';
@Controller('api/departments')
export class DepartmentsController {
constructor(private readonly departmentsService: DepartmentsService) {}
}

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Department } from '../entities/department.entity';
import { UserDepartment } from '../entities/user-department.entity';
import { DepartmentsService } from './departments.service';
import { DepartmentsController } from './departments.controller';
@Module({
imports: [TypeOrmModule.forFeature([Department, UserDepartment])],
controllers: [DepartmentsController],
providers: [DepartmentsService],
exports: [DepartmentsService],
})
export class DepartmentsModule {}

View File

@@ -0,0 +1,111 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Department } from '../entities/department.entity';
import { UserDepartment } from '../entities/user-department.entity';
import { CreateDepartmentDto, UpdateDepartmentDto, AssignUserDto } from './dto/department.dto';
@Injectable()
export class DepartmentsService {
constructor(
@InjectRepository(Department)
private deptRepo: Repository<Department>,
@InjectRepository(UserDepartment)
private userDeptRepo: Repository<UserDepartment>,
) {}
async findAll(): Promise<Department[]> {
return this.deptRepo.find({
where: { status: 'active' },
order: { sortOrder: 'ASC', name: 'ASC' },
});
}
async findTree(): Promise<Department[]> {
const all = await this.deptRepo.find({
where: { status: 'active' },
order: { sortOrder: 'ASC', name: 'ASC' },
relations: ['children'],
});
return all.filter((d) => d.parentId === null);
}
async findOne(id: number): Promise<Department> {
const dept = await this.deptRepo.findOne({ where: { id } });
if (!dept) throw new NotFoundException('部门不存在');
return dept;
}
async create(dto: CreateDepartmentDto): Promise<Department> {
const dept = this.deptRepo.create(dto);
return this.deptRepo.save(dept);
}
async update(id: number, dto: UpdateDepartmentDto): Promise<Department> {
const dept = await this.findOne(id);
Object.assign(dept, dto);
return this.deptRepo.save(dept);
}
async remove(id: number): Promise<void> {
const children = await this.deptRepo.count({ where: { parentId: id } });
if (children > 0) throw new ConflictException('该部门下存在子部门,无法删除');
const users = await this.userDeptRepo.count({ where: { departmentId: id } });
if (users > 0) throw new ConflictException('该部门下有用户关联,无法删除');
await this.deptRepo.update(id, { status: 'archived' });
}
/** 获取部门的所有子部门 ID递归含自身 */
async getDescendantIds(departmentId: number): Promise<number[]> {
const ids = [departmentId];
const children = await this.deptRepo.find({
where: { parentId: departmentId, status: 'active' },
});
for (const child of children) {
const childIds = await this.getDescendantIds(child.id);
ids.push(...childIds);
}
return ids;
}
/** 获取用户可访问的部门 ID 列表 */
async getUserDepartments(userId: number): Promise<number[]> {
const records = await this.userDeptRepo.find({
where: { userId },
});
return records.map((r) => r.departmentId);
}
/** 获取用户默认校区 ID */
async getUserDefaultDepartmentId(userId: number): Promise<number | null> {
const record = await this.userDeptRepo.findOne({
where: { userId, isDefault: true },
});
return record?.departmentId ?? null;
}
/** 获取部门下的用户 */
async getUsers(departmentId: number): Promise<UserDepartment[]> {
return this.userDeptRepo.find({
where: { departmentId },
relations: ['user'],
});
}
/** 为用户分配部门 */
async assignUser(departmentId: number, dto: AssignUserDto): Promise<UserDepartment> {
const record = this.userDeptRepo.create({
userId: dto.userId,
departmentId,
isDefault: dto.isDefault ?? false,
});
return this.userDeptRepo.save(record);
}
/** 移除用户-部门关联 */
async removeUser(departmentId: number, userId: number): Promise<void> {
await this.userDeptRepo.delete({ departmentId, userId });
}
}

View File

@@ -0,0 +1,37 @@
import { IsString, IsNotEmpty, IsOptional, IsInt, IsBoolean } from 'class-validator';
export class CreateDepartmentDto {
@IsString() @IsNotEmpty()
name: string;
@IsOptional() @IsInt()
parentId?: number;
@IsOptional() @IsString()
type?: string;
@IsOptional() @IsInt()
sortOrder?: number;
}
export class UpdateDepartmentDto {
@IsOptional() @IsString()
name?: string;
@IsOptional() @IsInt()
parentId?: number;
@IsOptional() @IsString()
type?: string;
@IsOptional() @IsInt()
sortOrder?: number;
}
export class AssignUserDto {
@IsInt()
userId: number;
@IsOptional() @IsBoolean()
isDefault?: boolean;
}