feat: add DepartmentsModule with DTO, Service, and stub controller
This commit is contained in:
@@ -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 },
|
||||
|
||||
7
apps/server/src/departments/departments.controller.ts
Normal file
7
apps/server/src/departments/departments.controller.ts
Normal 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) {}
|
||||
}
|
||||
14
apps/server/src/departments/departments.module.ts
Normal file
14
apps/server/src/departments/departments.module.ts
Normal 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 {}
|
||||
111
apps/server/src/departments/departments.service.ts
Normal file
111
apps/server/src/departments/departments.service.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
37
apps/server/src/departments/dto/department.dto.ts
Normal file
37
apps/server/src/departments/dto/department.dto.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user