import { Injectable, BadRequestException, NotFoundException, ForbiddenException, Optional } from '@nestjs/common'; import { KnowledgeBaseRepository } from './knowledge-base.repository'; import { PrismaService } from '../../infrastructure/database/prisma.service'; import { ContentSafetyService } from '../content-safety/content-safety.service'; import { StorageService } from '../../infrastructure/storage/storage.service'; import { MAX_KNOWLEDGE_BASE_COUNT } from './constants/knowledge-base.constants'; @Injectable() export class KnowledgeBaseService { constructor( private readonly repository: KnowledgeBaseRepository, private readonly prisma: PrismaService, private readonly storage: StorageService, @Optional() private readonly safety?: ContentSafetyService, ) {} async create(userId: string, dto: any) { const count = await this.repository.countByUserId(userId); if (count >= MAX_KNOWLEDGE_BASE_COUNT) { throw new BadRequestException('知识库数量已达到上限'); } if (dto.title) { const check = await this.safety?.check(dto.title, { userId, contentType: 'kb-title' }); if (check && !check.safe) throw new ForbiddenException('知识库名称包含违规内容'); } return this.enrichWithCoverUrl(await this.repository.create(userId, dto)); } async findAll(userId: string, opts?: { page?: number; limit?: number; visibility?: string; ownerType?: string }) { const kbs = await this.repository.findAllByUserId(userId, opts); return Promise.all(kbs.map(kb => this.enrichWithCoverUrl(kb))); } private async enrichWithCoverUrl(kb: any) { if (kb.coverKey) { try { kb.coverUrl = await this.storage.getDownloadUrl(kb.coverKey, 86400); } catch { kb.coverUrl = null; } } else { kb.coverUrl = null; } return kb; } async findOne(userId: string, id: string) { const kb = await this.repository.findById(id); if (!kb || kb.deletedAt) throw new NotFoundException('知识库不存在'); // 公开库允许任何人查看 if (kb.visibility === 'public') return kb; // 私有库只能 owner 查看 if (String(kb.userId) !== userId) throw new NotFoundException('知识库不存在'); return kb; } async update(userId: string, id: string, dto: any) { const kb = await this.repository.findById(id); if (!kb || String(kb.userId) !== userId) { throw new NotFoundException('知识库不存在'); } if (dto.title) { const check = await this.safety?.check(dto.title, { userId, contentType: 'kb-title' }); if (check && !check.safe) throw new ForbiddenException('知识库名称包含违规内容'); } return this.repository.update(id, dto); } async remove(userId: string, id: string) { const kb = await this.repository.findById(id); if (!kb || String(kb.userId) !== userId) { throw new NotFoundException('知识库不存在'); } return this.repository.softDelete(id); } // ── Pin ── async togglePin(userId: string, id: string) { const kb = await this.repository.findById(id); if (!kb || String(kb.userId) !== userId) throw new NotFoundException('知识库不存在'); return this.repository.togglePin(id, !kb.isPinned); } // ── Visibility ── async setVisibility(userId: string, id: string, visibility: string) { const kb = await this.repository.findById(id); if (!kb || String(kb.userId) !== userId) throw new NotFoundException('知识库不存在'); if (!['private', 'public'].includes(visibility)) throw new BadRequestException('visibility 必须为 private 或 public'); return this.repository.setVisibility(id, visibility); } // ── Subscribe ── async subscribe(userId: string, knowledgeBaseId: string) { const kb = await this.repository.findById(knowledgeBaseId); if (!kb || kb.deletedAt) throw new NotFoundException('知识库不存在'); if (kb.visibility !== 'public') throw new BadRequestException('只能订阅公开知识库'); if (kb.userId === userId) throw new BadRequestException('不能订阅自己的知识库'); return this.repository.subscribe(userId, knowledgeBaseId); } async unsubscribe(userId: string, knowledgeBaseId: string) { return this.repository.unsubscribe(userId, knowledgeBaseId); } async listSubscribed(userId: string, opts?: { page?: number; limit?: number }) { const kbs = await this.repository.findSubscribed(userId, opts); return Promise.all(kbs.map(kb => this.enrichWithCoverUrl(kb))); } // ── Discover ── async discoverPublic(opts?: { page?: number; limit?: number; search?: string }) { const kbs = await this.repository.findAllPublic(opts); return Promise.all(kbs.map(kb => this.enrichWithCoverUrl(kb))); } // ── Folder CRUD ── async createFolder(kbId: string, dto: { name: string; parentId?: string }) { return this.prisma.knowledgeFolder.create({ data: { knowledgeBaseId: kbId, name: dto.name, parentId: dto.parentId || null }, }); } async getFolders(kbId: string) { return this.prisma.knowledgeFolder.findMany({ where: { knowledgeBaseId: kbId, deletedAt: null }, orderBy: { sortOrder: 'asc' }, }); } async updateFolder(folderId: string, dto: { name?: string; parentId?: string | null }) { return this.prisma.knowledgeFolder.update({ where: { id: folderId }, data: dto }); } async deleteFolder(folderId: string) { // Soft-delete folder and children await this.prisma.knowledgeFolder.updateMany({ where: { OR: [{ id: folderId }, { parentId: folderId }] }, data: { deletedAt: new Date() }, }); return { success: true }; } }