Files
aurak/server/src/knowledge-group/knowledge-group.service.ts
T
Developer 8686d101cd Initial commit: AuraK人才测评系统基础框架
## 已实现功能
- 题库管理后端API完整实现
- 模板管理页面(Settings-测评模板)
- 评估统计页面
- 人才测评页面(AssessmentView)
- QuestionBank前端服务层

## 技术栈
- 后端: Node.js + NestJS + TypeORM
- 前端: React + TypeScript
- 容器化: Docker Compose

## 已知待完善
- 题库列表页缺少删除按钮
- 题库详情页未实现(题目管理/AI生成/审核)
2026-05-13 21:32:41 +08:00

413 lines
11 KiB
TypeScript

import {
Injectable,
NotFoundException,
ForbiddenException,
Inject,
forwardRef,
} from '@nestjs/common';
import { I18nService } from '../i18n/i18n.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { KnowledgeGroup } from './knowledge-group.entity';
import { KnowledgeBase } from '../knowledge-base/knowledge-base.entity';
import { KnowledgeBaseService } from '../knowledge-base/knowledge-base.service';
import { TenantService } from '../tenant/tenant.service';
export interface CreateGroupDto {
name: string;
description?: string;
color?: string;
parentId?: string | null;
}
export interface UpdateGroupDto {
name?: string;
description?: string;
color?: string;
parentId?: string | null;
}
export interface GroupWithFileCount {
id: string;
name: string;
description?: string;
color: string;
fileCount: number;
parentId?: string | null;
children?: GroupWithFileCount[];
createdAt: Date;
}
export interface PaginatedGroups {
items: GroupWithFileCount[];
total: number;
page: number;
limit: number;
}
@Injectable()
export class KnowledgeGroupService {
constructor(
@InjectRepository(KnowledgeGroup)
private groupRepository: Repository<KnowledgeGroup>,
@InjectRepository(KnowledgeBase)
private knowledgeBaseRepository: Repository<KnowledgeBase>,
@Inject(forwardRef(() => KnowledgeBaseService))
private knowledgeBaseService: KnowledgeBaseService,
private tenantService: TenantService,
private i18nService: I18nService,
) {}
async findAll(
userId: string,
tenantId: string,
): Promise<GroupWithFileCount[]> {
console.log('[KnowledgeGroup findAll] userId:', userId, 'tenantId:', tenantId);
// Return all groups for the tenant with file counts
const queryBuilder = this.groupRepository
.createQueryBuilder('group')
.leftJoin('group.knowledgeBases', 'kb');
if (tenantId === null) {
queryBuilder.where('group.tenantId IS NULL');
} else {
queryBuilder.where('group.tenantId = :tenantId', { tenantId });
}
const groups = await queryBuilder
.addSelect('COUNT(kb.id)', 'fileCount')
.groupBy('group.id')
.orderBy('group.createdAt', 'ASC')
.getRawAndEntities();
const flatList: GroupWithFileCount[] = groups.entities.map(
(group, index) => ({
id: group.id,
name: group.name,
description: group.description,
color: group.color,
parentId: group.parentId ?? null,
fileCount: parseInt(groups.raw[index].fileCount) || 0,
createdAt: group.createdAt,
children: [],
}),
);
// Build tree structure
return this.buildTree(flatList);
}
/** Build a nested tree from a flat list */
private buildTree(items: GroupWithFileCount[]): GroupWithFileCount[] {
const map = new Map<string, GroupWithFileCount>();
items.forEach((item) => map.set(item.id, { ...item, children: [] }));
const roots: GroupWithFileCount[] = [];
map.forEach((item) => {
if (item.parentId && map.has(item.parentId)) {
map.get(item.parentId)!.children!.push(item);
} else {
roots.push(item);
}
});
return roots;
}
async findOne(
id: string,
userId: string,
tenantId: string,
): Promise<KnowledgeGroup> {
const group = await this.groupRepository.findOne({
where: { id },
relations: ['knowledgeBases'],
});
if (!group) {
throw new NotFoundException(this.i18nService.getMessage('groupNotFound'));
}
// Check permission using TenantService
const hasAccess = await this.tenantService.canAccessTenant(
userId,
group.tenantId,
tenantId,
);
if (!hasAccess) {
throw new ForbiddenException(
`You do not have permission to access this knowledge group`,
);
}
return group;
}
async create(
userId: string,
tenantId: string,
createGroupDto: CreateGroupDto,
): Promise<KnowledgeGroup> {
console.log('[KnowledgeGroup create] userId:', userId, 'tenantId:', tenantId);
const group = this.groupRepository.create({
...createGroupDto,
parentId: createGroupDto.parentId ?? null,
tenantId,
});
const saved = await this.groupRepository.save(group);
console.log('[KnowledgeGroup create] saved group tenantId:', saved.tenantId);
return saved;
}
async update(
id: string,
userId: string,
tenantId: string,
updateGroupDto: UpdateGroupDto,
): Promise<KnowledgeGroup> {
// Update group within the tenant
const group = await this.groupRepository.findOne({
where: { id, tenantId },
});
if (!group) {
throw new NotFoundException(this.i18nService.getMessage('groupNotFound'));
}
Object.assign(group, updateGroupDto);
return await this.groupRepository.save(group);
}
async remove(id: string, userId: string, tenantId: string): Promise<void> {
const group = await this.groupRepository.findOne({
where: { id, tenantId },
});
if (!group) {
throw new NotFoundException(this.i18nService.getMessage('groupNotFound'));
}
// Recursively delete this group and all its descendants
await this.removeGroupRecursive(id, userId, tenantId);
}
/** Recursively delete a group, all its children, and all associated files */
private async removeGroupRecursive(
id: string,
userId: string,
tenantId: string,
): Promise<void> {
// 1. Find all direct children of this group
const children = await this.groupRepository.find({
where: { parentId: id, tenantId },
});
// 2. Recurse into each child first (depth-first)
for (const child of children) {
await this.removeGroupRecursive(child.id, userId, tenantId);
}
// 3. Delete all files belonging to this group
const files = await this.knowledgeBaseRepository
.createQueryBuilder('kb')
.innerJoin('kb.groups', 'group')
.where('group.id = :groupId', { groupId: id })
.select('kb.id')
.getMany();
for (const file of files) {
try {
const fullFile = await this.knowledgeBaseRepository.findOne({
where: { id: file.id },
select: ['id', 'userId', 'tenantId'],
});
if (fullFile) {
await this.knowledgeBaseService.deleteFile(
fullFile.id,
fullFile.userId,
fullFile.tenantId,
);
}
} catch (error) {
console.error(
`Failed to delete file ${file.id} when deleting group ${id}`,
error,
);
}
}
// 4. Delete the group itself
const group = await this.groupRepository.findOne({ where: { id } });
if (group) {
await this.groupRepository.remove(group);
}
}
async getGroupFiles(
groupId: string,
userId: string,
tenantId: string,
): Promise<KnowledgeBase[]> {
const group = await this.groupRepository.findOne({
where: { id: groupId },
relations: ['knowledgeBases'],
});
if (!group) {
throw new NotFoundException(this.i18nService.getMessage('groupNotFound'));
}
// Check permission using TenantService
const hasAccess = await this.tenantService.canAccessTenant(
userId,
group.tenantId,
tenantId,
);
if (!hasAccess) {
throw new ForbiddenException(
`You do not have permission to access this knowledge group`,
);
}
return group.knowledgeBases;
}
async addFilesToGroup(
fileId: string,
groupIds: string[],
userId: string,
tenantId: string,
): Promise<void> {
const file = await this.knowledgeBaseRepository.findOne({
where: { id: fileId },
relations: ['groups'],
});
if (!file) {
throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
}
// Check permission for file
const fileAccess = await this.tenantService.canAccessTenant(
userId,
file.tenantId,
tenantId,
);
if (!fileAccess) {
throw new ForbiddenException(
`You do not have permission to modify this knowledge base`,
);
}
// Load all groups by ID without user restriction
const groups = await this.groupRepository.findBy({ id: In(groupIds) });
// Verify each group access
for (const g of groups) {
const gAccess = await this.tenantService.canAccessTenant(
userId,
g.tenantId,
tenantId,
);
if (!gAccess) {
throw new ForbiddenException(
`You do not have permission to access group ${g.name}`,
);
}
}
if (groupIds.length > 0 && groups.length !== groupIds.length) {
throw new NotFoundException(
this.i18nService.getMessage('someGroupsNotFound'),
);
}
file.groups = groups;
await this.knowledgeBaseRepository.save(file);
}
async removeFileFromGroup(
fileId: string,
groupId: string,
userId: string,
tenantId: string,
): Promise<void> {
const file = await this.knowledgeBaseRepository.findOne({
where: { id: fileId },
relations: ['groups'],
});
if (!file) {
throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
}
// Check permission for file
const fileAccess = await this.tenantService.canAccessTenant(
userId,
file.tenantId,
tenantId,
);
if (!fileAccess) {
throw new ForbiddenException(
`You do not have permission to modify this knowledge base`,
);
}
file.groups = file.groups.filter((group) => group.id !== groupId);
await this.knowledgeBaseRepository.save(file);
}
async getFileIdsByGroups(
groupIds: string[],
userId: string,
tenantId: string,
): Promise<string[]> {
if (!groupIds || groupIds.length === 0) {
return [];
}
// Security check: Verify user has access to these groups
const groups = await this.groupRepository.findBy({ id: In(groupIds) });
for (const g of groups) {
const hasAccess = await this.tenantService.canAccessTenant(
userId,
g.tenantId,
tenantId,
);
if (!hasAccess) {
throw new ForbiddenException(
`You do not have permission to access group ${g.name}`,
);
}
}
const result = await this.knowledgeBaseRepository
.createQueryBuilder('kb')
.innerJoin('kb.groups', 'group')
.where('group.id IN (:...groupIds)', { groupIds })
// No extra tenantId check here because we verified the groups above
.select('DISTINCT kb.id', 'id')
.getRawMany();
return result.map((row) => row.id);
}
/**
* Find or create a group by name and parentId within a tenant.
* Used by import tasks to build folder hierarchy.
*/
async findOrCreate(
userId: string,
tenantId: string,
name: string,
parentId: string | null,
description?: string,
): Promise<KnowledgeGroup> {
const existing = await this.groupRepository.findOne({
where: { name, tenantId, parentId: parentId ?? undefined },
});
if (existing) return existing;
return this.create(userId, tenantId, { name, description, parentId });
}
}