Files
gongxue-base/apps/server/src/departments/departments.service.ts

163 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 });
}
}