426 lines
13 KiB
TypeScript
Executable File
426 lines
13 KiB
TypeScript
Executable File
import { Injectable } from '@nestjs/common';
|
||
import {
|
||
TermMethodSchema,
|
||
db,
|
||
Staff,
|
||
Term,
|
||
Prisma,
|
||
TermDto,
|
||
TreeDataNode,
|
||
UserProfile,
|
||
getUniqueItems,
|
||
RolePerms,
|
||
TaxonomySlug,
|
||
ObjectType,
|
||
TermAncestry,
|
||
} from '@nice/common';
|
||
import { z } from 'zod';
|
||
import { BaseTreeService } from '../base/base.tree.service';
|
||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||
import { formatToTermTreeData, mapToTermSimpleTree } from './utils';
|
||
|
||
@Injectable()
|
||
export class TermService extends BaseTreeService<Prisma.TermDelegate> {
|
||
constructor() {
|
||
super(db, ObjectType.TERM, 'termAncestry', true);
|
||
}
|
||
|
||
async create(args: Prisma.TermCreateArgs, params?: { staff?: UserProfile }) {
|
||
args.data.createdBy = params?.staff?.id;
|
||
const result = await super.create(args);
|
||
EventBus.emit('dataChanged', {
|
||
type: this.objectType,
|
||
operation: CrudOperation.CREATED,
|
||
data: result,
|
||
});
|
||
return result;
|
||
}
|
||
async update(args: Prisma.TermUpdateArgs) {
|
||
const result = await super.update(args);
|
||
EventBus.emit('dataChanged', {
|
||
type: this.objectType,
|
||
operation: CrudOperation.UPDATED,
|
||
data: result,
|
||
});
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 删除现有单位并清理DeptAncestry关系。
|
||
* @param data - 用于删除现有单位的数据。
|
||
* @returns 删除的单位对象。
|
||
*/
|
||
async softDeleteByIds(ids: string[]) {
|
||
const descendantIds = await this.getDescendantIds(ids, true);
|
||
const result = await super.softDeleteByIds(descendantIds);
|
||
EventBus.emit('dataChanged', {
|
||
type: this.objectType,
|
||
operation: CrudOperation.DELETED,
|
||
data: result,
|
||
});
|
||
return result;
|
||
}
|
||
|
||
// async query(data: z.infer<typeof TermMethodSchema.findManyWithCursor>) {
|
||
// const { limit = 10, initialIds, taxonomyId, taxonomySlug } = data;
|
||
// // Fetch additional objects excluding initialIds
|
||
// const ids =
|
||
// typeof initialIds === 'string' ? [initialIds] : initialIds || [];
|
||
// const initialTerms = await db.term.findMany({
|
||
// where: {
|
||
// id: {
|
||
// in: ids,
|
||
// },
|
||
// },
|
||
// include: {
|
||
// domain: true,
|
||
// children: true,
|
||
// },
|
||
// });
|
||
// const terms = await db.term.findMany({
|
||
// where: {
|
||
// taxonomyId,
|
||
// taxonomy: taxonomySlug && { slug: taxonomySlug },
|
||
// deletedAt: null,
|
||
// },
|
||
// take: limit !== -1 ? limit! : undefined,
|
||
// include: {
|
||
// domain: true,
|
||
// taxonomy: true,
|
||
// },
|
||
// orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
||
// });
|
||
|
||
// const results = getUniqueItems(
|
||
// [...initialTerms, ...terms].filter(Boolean),
|
||
// 'id',
|
||
// );
|
||
// return results;
|
||
// }
|
||
// /**
|
||
|
||
async upsertTags(staff: UserProfile, tags: string[]) {
|
||
const tagTax = await db.taxonomy.findFirst({
|
||
where: {
|
||
slug: TaxonomySlug.TAG,
|
||
},
|
||
});
|
||
// 批量查找所有存在的标签
|
||
const existingTerms = await db.term.findMany({
|
||
where: {
|
||
name: {
|
||
in: tags,
|
||
},
|
||
taxonomyId: tagTax.id,
|
||
},
|
||
});
|
||
|
||
// 找出不存在的标签
|
||
const existingTagNames = new Set(existingTerms.map((term) => term.name));
|
||
const newTags = tags.filter((tag) => !existingTagNames.has(tag));
|
||
// 批量创建不存在的标签
|
||
const newTerms = await Promise.all(
|
||
newTags.map((tag) =>
|
||
this.create({
|
||
data: {
|
||
name: tag,
|
||
taxonomyId: tagTax.id,
|
||
domainId: staff.domainId,
|
||
},
|
||
}),
|
||
),
|
||
);
|
||
|
||
// 合并现有标签和新创建的标签
|
||
return [...existingTerms, ...newTerms];
|
||
}
|
||
|
||
// /**
|
||
// * 查找多个术语并生成TermDto对象。
|
||
// *
|
||
// * @param staff 当前操作的工作人员
|
||
// * @param ids 术语ID
|
||
// * @returns 包含详细信息的术语对象
|
||
// */
|
||
// async findByIds(ids: string[], staff?: UserProfile) {
|
||
// const terms = await db.term.findMany({
|
||
// where: {
|
||
// id: {
|
||
// in: ids,
|
||
// },
|
||
// },
|
||
// include: {
|
||
// domain: true,
|
||
// children: true,
|
||
// },
|
||
// });
|
||
// return await Promise.all(
|
||
// terms.map(async (term) => {
|
||
// return await this.transformDto(term, staff);
|
||
// }),
|
||
// );
|
||
// }
|
||
// /**
|
||
// * 获取指定条件下的术语子节点。
|
||
// *
|
||
// * @param staff 当前操作的工作人员
|
||
// * @param data 查询条件
|
||
// * @returns 子节点术语列表
|
||
// */
|
||
// async getChildren(
|
||
// staff: UserProfile,
|
||
// data: z.infer<typeof TermMethodSchema.getChildren>,
|
||
// ) {
|
||
// const { parentId, domainId, taxonomyId, cursor, limit = 10 } = data;
|
||
// let queryCondition: Prisma.TermWhereInput = {
|
||
// taxonomyId,
|
||
// parentId: parentId,
|
||
// OR: [{ domainId: null }],
|
||
// deletedAt: null,
|
||
// };
|
||
// if (
|
||
// staff?.permissions?.includes(RolePerms.MANAGE_ANY_TERM) ||
|
||
// staff?.permissions?.includes(RolePerms.READ_ANY_TERM)
|
||
// ) {
|
||
// queryCondition.OR = undefined;
|
||
// } else {
|
||
// queryCondition.OR = queryCondition.OR.concat([
|
||
// { domainId: staff?.domainId },
|
||
// { domainId: null },
|
||
// ]);
|
||
// }
|
||
// const terms = await db.term.findMany({
|
||
// where: queryCondition,
|
||
// include: {
|
||
// children: {
|
||
// where: {
|
||
// deletedAt: null,
|
||
// },
|
||
// },
|
||
// },
|
||
// take: limit + 1,
|
||
// cursor: cursor
|
||
// ? { createdAt: cursor.split('_')[0], id: cursor.split('_')[1] }
|
||
// : undefined,
|
||
// });
|
||
// let nextCursor: typeof cursor | undefined = undefined;
|
||
// if (terms.length > limit) {
|
||
// const nextItem = terms.pop();
|
||
// nextCursor = `${nextItem.createdAt.toISOString()}_${nextItem!.id}`;
|
||
// }
|
||
// const termDtos = await Promise.all(
|
||
// terms.map((item) => this.transformDto(item, staff)),
|
||
// );
|
||
// return {
|
||
// items: termDtos,
|
||
// nextCursor,
|
||
// };
|
||
// }
|
||
|
||
async getTreeData(data: z.infer<typeof TermMethodSchema.getTreeData>) {
|
||
const { taxonomyId, taxonomySlug, domainId } = data;
|
||
let terms = [];
|
||
if (taxonomyId) {
|
||
terms = await db.term.findMany({
|
||
where: { taxonomyId, domainId, deletedAt: null },
|
||
include: { children: true },
|
||
orderBy: [{ order: 'asc' }],
|
||
});
|
||
} else if (taxonomySlug) {
|
||
terms = await db.term.findMany({
|
||
where: {
|
||
taxonomy: {
|
||
slug: taxonomySlug,
|
||
},
|
||
deletedAt: null,
|
||
domainId,
|
||
},
|
||
include: { children: true },
|
||
orderBy: [{ order: 'asc' }],
|
||
});
|
||
}
|
||
// Map to store terms by id for quick lookup
|
||
const termMap = new Map<string, any>();
|
||
terms.forEach((term) =>
|
||
termMap.set(term.id, {
|
||
...term,
|
||
children: [],
|
||
key: term.id,
|
||
value: term.id,
|
||
title: term.name,
|
||
isLeaf: true, // Initialize as true, will update later if it has children
|
||
}),
|
||
);
|
||
// Root nodes collection
|
||
const roots = [];
|
||
// Build the tree structure iteratively
|
||
terms.forEach((term) => {
|
||
if (term.parentId) {
|
||
const parent = termMap.get(term.parentId);
|
||
if (parent) {
|
||
parent.children.push(termMap.get(term.id));
|
||
parent.isLeaf = false; // Update parent's isLeaf field
|
||
}
|
||
} else {
|
||
roots.push(termMap.get(term.id));
|
||
}
|
||
});
|
||
return roots as TreeDataNode[];
|
||
}
|
||
|
||
async getChildSimpleTree(
|
||
staff: UserProfile,
|
||
data: z.infer<typeof TermMethodSchema.getSimpleTree>,
|
||
) {
|
||
const { domainId = null, permissions } = staff;
|
||
const hasAnyPerms =
|
||
staff?.permissions?.includes(RolePerms.MANAGE_ANY_TERM) ||
|
||
staff?.permissions?.includes(RolePerms.READ_ANY_TERM);
|
||
const { termIds, parentId, taxonomyId } = data;
|
||
// 提取非空 deptIds
|
||
const validTermIds = termIds?.filter((id) => id !== null) ?? [];
|
||
const hasNullTermId = termIds?.includes(null) ?? false;
|
||
|
||
const [childrenData, selfData] = await Promise.all([
|
||
db.termAncestry.findMany({
|
||
where: {
|
||
...(termIds && {
|
||
OR: [
|
||
...(validTermIds.length
|
||
? [{ ancestorId: { in: validTermIds } }]
|
||
: []),
|
||
...(hasNullTermId ? [{ ancestorId: null }] : []),
|
||
],
|
||
}),
|
||
descendant: {
|
||
taxonomyId: taxonomyId,
|
||
// 动态权限控制条件
|
||
...(hasAnyPerms
|
||
? {} // 当有全局权限时,不添加任何额外条件
|
||
: {
|
||
// 当无全局权限时,添加域ID过滤
|
||
OR: [
|
||
{ domainId: null }, // 通用记录
|
||
{ domainId: domainId }, // 特定域记录
|
||
],
|
||
}),
|
||
},
|
||
ancestorId: parentId,
|
||
relDepth: 1,
|
||
},
|
||
include: {
|
||
descendant: { include: { children: true } },
|
||
},
|
||
orderBy: { descendant: { order: 'asc' } },
|
||
}),
|
||
termIds
|
||
? db.term.findMany({
|
||
where: {
|
||
...(termIds && {
|
||
OR: [
|
||
...(validTermIds.length
|
||
? [{ id: { in: validTermIds } }]
|
||
: []),
|
||
],
|
||
}),
|
||
taxonomyId: taxonomyId,
|
||
// 动态权限控制条件
|
||
...(hasAnyPerms
|
||
? {} // 当有全局权限时,不添加任何额外条件
|
||
: {
|
||
// 当无全局权限时,添加域ID过滤
|
||
OR: [
|
||
{ domainId: null }, // 通用记录
|
||
{ domainId: domainId }, // 特定域记录
|
||
],
|
||
}),
|
||
},
|
||
include: { children: true },
|
||
orderBy: { order: 'asc' },
|
||
})
|
||
: [],
|
||
]);
|
||
const children = childrenData
|
||
.map(({ descendant }) => descendant)
|
||
.filter(Boolean)
|
||
.map(formatToTermTreeData);
|
||
const selfItems = selfData.map(formatToTermTreeData);
|
||
return getUniqueItems([...children, ...selfItems], 'id');
|
||
}
|
||
|
||
async getParentSimpleTree(
|
||
staff: UserProfile,
|
||
data: z.infer<typeof TermMethodSchema.getSimpleTree>,
|
||
) {
|
||
const { domainId = null, permissions } = staff;
|
||
const hasAnyPerms =
|
||
permissions.includes(RolePerms.READ_ANY_TERM) ||
|
||
permissions.includes(RolePerms.MANAGE_ANY_TERM);
|
||
// 解构输入参数
|
||
const { termIds, taxonomyId } = data;
|
||
|
||
// 并行查询父级部门ancestry和自身部门数据
|
||
// 使用Promise.all提高查询效率,减少等待时间
|
||
const [parentData, selfData] = await Promise.all([
|
||
// 查询指定部门的所有祖先节点,包含子节点和父节点信息
|
||
db.termAncestry.findMany({
|
||
where: {
|
||
descendantId: { in: termIds }, // 查询条件:descendant在给定的部门ID列表中
|
||
ancestor: {
|
||
taxonomyId: taxonomyId,
|
||
...(hasAnyPerms
|
||
? {} // 当有全局权限时,不添加任何额外条件
|
||
: {
|
||
// 当无全局权限时,添加域ID过滤
|
||
OR: [
|
||
{ domainId: null }, // 通用记录
|
||
{ domainId: domainId }, // 特定域记录
|
||
],
|
||
}),
|
||
},
|
||
},
|
||
include: {
|
||
ancestor: {
|
||
include: {
|
||
children: true, // 包含子节点信息
|
||
parent: true, // 包含父节点信息
|
||
},
|
||
},
|
||
},
|
||
orderBy: { ancestor: { order: 'asc' } }, // 按祖先节点顺序升序排序
|
||
}),
|
||
|
||
// 查询自身部门数据
|
||
db.term.findMany({
|
||
where: {
|
||
id: { in: termIds },
|
||
taxonomyId: taxonomyId,
|
||
...(hasAnyPerms
|
||
? {} // 当有全局权限时,不添加任何额外条件
|
||
: {
|
||
// 当无全局权限时,添加域ID过滤
|
||
OR: [
|
||
{ domainId: null }, // 通用记录
|
||
{ domainId: domainId }, // 特定域记录
|
||
],
|
||
}),
|
||
},
|
||
include: { children: true }, // 包含子节点信息
|
||
orderBy: { order: 'asc' }, // 按顺序升序排序
|
||
}),
|
||
]);
|
||
|
||
// 处理父级节点:过滤并映射为简单树结构
|
||
const parents = parentData
|
||
.map(({ ancestor }) => ancestor) // 提取祖先节点
|
||
.filter((ancestor) => ancestor) // 过滤有效且超出根节点层级的节点
|
||
.map(mapToTermSimpleTree); // 映射为简单树结构
|
||
|
||
// 处理自身节点:映射为简单树结构
|
||
const selfItems = selfData.map(mapToTermSimpleTree);
|
||
|
||
// 合并并去重父级和自身节点,返回唯一项
|
||
return getUniqueItems([...parents, ...selfItems], 'id');
|
||
}
|
||
}
|