forked from wangziqi/gongxue-base
163 lines
5.6 KiB
TypeScript
163 lines
5.6 KiB
TypeScript
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/typeorm';
|
||
import { Repository } from 'typeorm';
|
||
import { Department } from '../entities/department.entity';
|
||
import { DepartmentType } 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[]> {
|
||
// Load ALL departments flat (no relations — avoids N+1 and only loads 1 level),
|
||
// then build the full tree in memory.
|
||
const all = await this.deptRepo.find({
|
||
where: { status: 'active' },
|
||
order: { sortOrder: 'ASC', name: 'ASC' },
|
||
});
|
||
|
||
const byParent = new Map<number | null, Department[]>();
|
||
for (const dept of all) {
|
||
const key = dept.parentId ?? null;
|
||
const list = byParent.get(key);
|
||
if (list) {
|
||
list.push(dept);
|
||
} else {
|
||
byParent.set(key, [dept]);
|
||
}
|
||
}
|
||
|
||
const attachChildren = (dept: Department): void => {
|
||
const children = byParent.get(dept.id) ?? [];
|
||
dept.children = children;
|
||
for (const child of children) attachChildren(child);
|
||
};
|
||
|
||
const roots = byParent.get(null) ?? [];
|
||
for (const root of roots) attachChildren(root);
|
||
return roots;
|
||
}
|
||
|
||
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> {
|
||
if (dto.type === DepartmentType.CAMPUS && dto.parentId) {
|
||
throw new ConflictException('校区类型的部门必须是根部门,不能设置上级');
|
||
}
|
||
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);
|
||
|
||
const effectiveType = dto.type ?? dept.type;
|
||
const effectiveParentId = dto.parentId !== undefined ? dto.parentId : dept.parentId;
|
||
if (effectiveType === DepartmentType.CAMPUS && effectiveParentId) {
|
||
throw new ConflictException('校区类型的部门必须是根部门,不能设置上级');
|
||
}
|
||
|
||
// Prevent parent cycles: parentId must not be the dept itself or one of its descendants
|
||
if (dto.parentId !== undefined && dto.parentId !== null) {
|
||
const newParentId = dto.parentId;
|
||
if (newParentId === id) {
|
||
throw new ConflictException('不能将部门的上级设为自身');
|
||
}
|
||
const descendantIds = await this.getDescendantIds(id);
|
||
if (descendantIds.includes(newParentId)) {
|
||
throw new ConflictException('不能将部门的上级设为其子部门,会形成循环');
|
||
}
|
||
}
|
||
|
||
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 all = await this.deptRepo.find({ where: { status: 'active' }, select: ['id', 'parentId'] });
|
||
const byParent = new Map<number | null, number[]>();
|
||
for (const d of all) {
|
||
const key = d.parentId ?? null;
|
||
byParent.set(key, [...(byParent.get(key) ?? []), d.id]);
|
||
}
|
||
const ids: number[] = [departmentId];
|
||
const collect = (parentId: number) => {
|
||
const children = byParent.get(parentId) ?? [];
|
||
for (const childId of children) {
|
||
ids.push(childId);
|
||
collect(childId);
|
||
}
|
||
};
|
||
collect(departmentId);
|
||
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 });
|
||
}
|
||
}
|