api-server/src/modules/knowledge-base/knowledge-base.service.ts
wangdl b9f8334245
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 11s
feat: H0-08 KnowledgeBase 增加业务字段 + 订阅 + 发现 + 置顶 + 公开
Schema 新增:
- coverType/coverIcon/coverColor(封面类型/系统图标/颜色)
- visibility(private/public)
- isPinned(置顶)
- ownerType(user/official)
- isVerified(认证标识)
- KnowledgeBaseSubscription 表

API 新增:
- POST /knowledge-bases/:id/pin(切换置顶)
- PATCH /knowledge-bases/:id/visibility(切换公开/私有)
- POST /knowledge-bases/:id/subscribe(订阅)
- DELETE /knowledge-bases/:id/subscribe(取消订阅)
- GET /knowledge-bases/subscribed(已订阅列表)
- GET /knowledge-bases/discover(发现公开库)

增强:
- findAll 支持 visibility/ownerType 筛选 + 置顶优先排序
- findOne 公开库允许任何人查看
- update 支持所有新字段

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:24:21 +08:00

148 lines
5.6 KiB
TypeScript

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