From 0e08770cf4a4b87f6eaaf8131e8eec784562c496 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sun, 5 Jul 2026 23:47:41 +0800 Subject: [PATCH] feat: add DepartmentsModule with DTO, Service, and stub controller --- apps/server/src/app.module.ts | 8 +- .../src/departments/departments.controller.ts | 7 ++ .../src/departments/departments.module.ts | 14 +++ .../src/departments/departments.service.ts | 111 ++++++++++++++++++ .../src/departments/dto/department.dto.ts | 37 ++++++ 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 apps/server/src/departments/departments.controller.ts create mode 100644 apps/server/src/departments/departments.module.ts create mode 100644 apps/server/src/departments/departments.service.ts create mode 100644 apps/server/src/departments/dto/department.dto.ts diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 09aa57d..0a3d121 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -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 }, diff --git a/apps/server/src/departments/departments.controller.ts b/apps/server/src/departments/departments.controller.ts new file mode 100644 index 0000000..e1b4e18 --- /dev/null +++ b/apps/server/src/departments/departments.controller.ts @@ -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) {} +} diff --git a/apps/server/src/departments/departments.module.ts b/apps/server/src/departments/departments.module.ts new file mode 100644 index 0000000..d253ecd --- /dev/null +++ b/apps/server/src/departments/departments.module.ts @@ -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 {} diff --git a/apps/server/src/departments/departments.service.ts b/apps/server/src/departments/departments.service.ts new file mode 100644 index 0000000..2342425 --- /dev/null +++ b/apps/server/src/departments/departments.service.ts @@ -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, + @InjectRepository(UserDepartment) + private userDeptRepo: Repository, + ) {} + + async findAll(): Promise { + return this.deptRepo.find({ + where: { status: 'active' }, + order: { sortOrder: 'ASC', name: 'ASC' }, + }); + } + + async findTree(): Promise { + 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 { + const dept = await this.deptRepo.findOne({ where: { id } }); + if (!dept) throw new NotFoundException('部门不存在'); + return dept; + } + + async create(dto: CreateDepartmentDto): Promise { + const dept = this.deptRepo.create(dto); + return this.deptRepo.save(dept); + } + + async update(id: number, dto: UpdateDepartmentDto): Promise { + const dept = await this.findOne(id); + Object.assign(dept, dto); + return this.deptRepo.save(dept); + } + + async remove(id: number): Promise { + 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 { + 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 { + const records = await this.userDeptRepo.find({ + where: { userId }, + }); + return records.map((r) => r.departmentId); + } + + /** 获取用户默认校区 ID */ + async getUserDefaultDepartmentId(userId: number): Promise { + const record = await this.userDeptRepo.findOne({ + where: { userId, isDefault: true }, + }); + return record?.departmentId ?? null; + } + + /** 获取部门下的用户 */ + async getUsers(departmentId: number): Promise { + return this.userDeptRepo.find({ + where: { departmentId }, + relations: ['user'], + }); + } + + /** 为用户分配部门 */ + async assignUser(departmentId: number, dto: AssignUserDto): Promise { + 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 { + await this.userDeptRepo.delete({ departmentId, userId }); + } +} diff --git a/apps/server/src/departments/dto/department.dto.ts b/apps/server/src/departments/dto/department.dto.ts new file mode 100644 index 0000000..de75d33 --- /dev/null +++ b/apps/server/src/departments/dto/department.dto.ts @@ -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; +}