feat: H0-08 KnowledgeBase 增加业务字段 + 订阅 + 发现 + 置顶 + 公开
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 11s
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 11s
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>
This commit is contained in:
parent
4f59569b7c
commit
b9f8334245
@ -166,6 +166,13 @@ model KnowledgeBase {
|
|||||||
title String @db.VarChar(255)
|
title String @db.VarChar(255)
|
||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
coverKey String? @db.VarChar(100)
|
coverKey String? @db.VarChar(100)
|
||||||
|
coverType String @default("custom") @db.VarChar(32)
|
||||||
|
coverIcon String? @db.VarChar(50)
|
||||||
|
coverColor String? @db.VarChar(20)
|
||||||
|
visibility String @default("private") @db.VarChar(16)
|
||||||
|
isPinned Boolean @default(false)
|
||||||
|
ownerType String @default("user") @db.VarChar(16)
|
||||||
|
isVerified Boolean @default(false)
|
||||||
status String @default("active") @db.VarChar(32)
|
status String @default("active") @db.VarChar(32)
|
||||||
itemCount Int @default(0)
|
itemCount Int @default(0)
|
||||||
lastStudiedAt DateTime?
|
lastStudiedAt DateTime?
|
||||||
@ -180,9 +187,26 @@ model KnowledgeBase {
|
|||||||
chunks KnowledgeChunk[]
|
chunks KnowledgeChunk[]
|
||||||
focusItems FocusItem[]
|
focusItems FocusItem[]
|
||||||
folders KnowledgeFolder[]
|
folders KnowledgeFolder[]
|
||||||
|
subscriptions KnowledgeBaseSubscription[]
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([status])
|
@@index([status])
|
||||||
|
@@index([visibility])
|
||||||
|
@@index([ownerType])
|
||||||
|
}
|
||||||
|
|
||||||
|
model KnowledgeBaseSubscription {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
knowledgeBaseId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
knowledgeBase KnowledgeBase @relation(fields: [knowledgeBaseId], references: [id])
|
||||||
|
|
||||||
|
@@unique([userId, knowledgeBaseId])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([knowledgeBaseId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Artifact {
|
model Artifact {
|
||||||
|
|||||||
@ -65,4 +65,56 @@ export class KnowledgeBaseController {
|
|||||||
async deleteFolder(@Param('folderId') folderId: string) {
|
async deleteFolder(@Param('folderId') folderId: string) {
|
||||||
return this.service.deleteFolder(folderId);
|
return this.service.deleteFolder(folderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Pin ──
|
||||||
|
|
||||||
|
@Post(':id/pin')
|
||||||
|
@ApiOperation({ summary: '切换置顶状态' })
|
||||||
|
async togglePin(@CurrentUser() user: UserPayload, @Param('id') id: string) {
|
||||||
|
return this.service.togglePin(String(user?.id || 'anonymous'), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Visibility ──
|
||||||
|
|
||||||
|
@Patch(':id/visibility')
|
||||||
|
@ApiOperation({ summary: '切换公开/私有' })
|
||||||
|
async setVisibility(
|
||||||
|
@CurrentUser() user: UserPayload,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: { visibility: string },
|
||||||
|
) {
|
||||||
|
return this.service.setVisibility(String(user?.id || 'anonymous'), id, dto.visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Subscribe ──
|
||||||
|
|
||||||
|
@Post(':id/subscribe')
|
||||||
|
@ApiOperation({ summary: '订阅知识库' })
|
||||||
|
async subscribe(@CurrentUser() user: UserPayload, @Param('id') id: string) {
|
||||||
|
return this.service.subscribe(String(user?.id || 'anonymous'), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id/subscribe')
|
||||||
|
@ApiOperation({ summary: '取消订阅' })
|
||||||
|
async unsubscribe(@CurrentUser() user: UserPayload, @Param('id') id: string) {
|
||||||
|
return this.service.unsubscribe(String(user?.id || 'anonymous'), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('subscribed')
|
||||||
|
@ApiOperation({ summary: '已订阅知识库列表' })
|
||||||
|
async listSubscribed(@CurrentUser() user: UserPayload, @Query() pagination: PaginationDto) {
|
||||||
|
return this.service.listSubscribed(String(user?.id || 'anonymous'), pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Discover ──
|
||||||
|
|
||||||
|
@Get('discover')
|
||||||
|
@ApiOperation({ summary: '发现公开知识库' })
|
||||||
|
async discover(@Query('search') search?: string, @Query('page') page?: string, @Query('limit') limit?: string) {
|
||||||
|
return this.service.discoverPublic({
|
||||||
|
page: Number(page) || 1,
|
||||||
|
limit: Math.min(Number(limit) || 20, 100),
|
||||||
|
search,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,25 @@ import { PrismaService } from '../../infrastructure/database/prisma.service';
|
|||||||
export class KnowledgeBaseRepository {
|
export class KnowledgeBaseRepository {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async create(userId: string, dto: { title: string; description?: string; coverKey?: string }) {
|
async create(userId: string, dto: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
coverKey?: string;
|
||||||
|
coverType?: string;
|
||||||
|
coverIcon?: string;
|
||||||
|
coverColor?: string;
|
||||||
|
visibility?: string;
|
||||||
|
}) {
|
||||||
return this.prisma.knowledgeBase.create({
|
return this.prisma.knowledgeBase.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
userId,
|
||||||
title: dto.title,
|
title: dto.title,
|
||||||
description: dto.description ?? '',
|
description: dto.description ?? '',
|
||||||
coverKey: dto.coverKey ?? null,
|
coverKey: dto.coverKey ?? null,
|
||||||
|
coverType: dto.coverType ?? 'custom',
|
||||||
|
coverIcon: dto.coverIcon ?? null,
|
||||||
|
coverColor: dto.coverColor ?? null,
|
||||||
|
visibility: dto.visibility ?? 'private',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
},
|
},
|
||||||
@ -22,12 +34,41 @@ export class KnowledgeBaseRepository {
|
|||||||
return this.prisma.knowledgeBase.findUnique({ where: { id } });
|
return this.prisma.knowledgeBase.findUnique({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllByUserId(userId: string, pagination?: { page?: number; limit?: number }) {
|
async findAllByUserId(
|
||||||
const page = pagination?.page ?? 1;
|
userId: string,
|
||||||
const limit = pagination?.limit ?? 20;
|
opts?: {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
visibility?: string;
|
||||||
|
ownerType?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const page = opts?.page ?? 1;
|
||||||
|
const limit = opts?.limit ?? 20;
|
||||||
|
const where: any = { userId, deletedAt: null };
|
||||||
|
if (opts?.visibility) where.visibility = opts.visibility;
|
||||||
|
if (opts?.ownerType) where.ownerType = opts.ownerType;
|
||||||
return this.prisma.knowledgeBase.findMany({
|
return this.prisma.knowledgeBase.findMany({
|
||||||
where: { userId, deletedAt: null },
|
where,
|
||||||
orderBy: { updatedAt: 'desc' },
|
orderBy: [{ isPinned: 'desc' }, { updatedAt: 'desc' }],
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllPublic(opts?: { page?: number; limit?: number; search?: string }) {
|
||||||
|
const page = opts?.page ?? 1;
|
||||||
|
const limit = opts?.limit ?? 20;
|
||||||
|
const where: any = { visibility: 'public', deletedAt: null };
|
||||||
|
if (opts?.search) {
|
||||||
|
where.OR = [
|
||||||
|
{ title: { contains: opts.search } },
|
||||||
|
{ description: { contains: opts.search } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return this.prisma.knowledgeBase.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: [{ isPinned: 'desc' }, { updatedAt: 'desc' }],
|
||||||
skip: (page - 1) * limit,
|
skip: (page - 1) * limit,
|
||||||
take: limit,
|
take: limit,
|
||||||
});
|
});
|
||||||
@ -39,13 +80,36 @@ export class KnowledgeBaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, dto: { title?: string; description?: string }) {
|
async update(id: string, dto: {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
coverKey?: string;
|
||||||
|
coverType?: string;
|
||||||
|
coverIcon?: string;
|
||||||
|
coverColor?: string;
|
||||||
|
visibility?: string;
|
||||||
|
isPinned?: boolean;
|
||||||
|
}) {
|
||||||
return this.prisma.knowledgeBase.update({
|
return this.prisma.knowledgeBase.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: dto,
|
data: dto,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async togglePin(id: string, isPinned: boolean) {
|
||||||
|
return this.prisma.knowledgeBase.update({
|
||||||
|
where: { id },
|
||||||
|
data: { isPinned },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setVisibility(id: string, visibility: string) {
|
||||||
|
return this.prisma.knowledgeBase.update({
|
||||||
|
where: { id },
|
||||||
|
data: { visibility },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async softDelete(id: string) {
|
async softDelete(id: string) {
|
||||||
await this.prisma.knowledgeBase.update({
|
await this.prisma.knowledgeBase.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
@ -53,4 +117,40 @@ export class KnowledgeBaseRepository {
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Subscription ──
|
||||||
|
|
||||||
|
async subscribe(userId: string, knowledgeBaseId: string) {
|
||||||
|
return this.prisma.knowledgeBaseSubscription.create({
|
||||||
|
data: { userId, knowledgeBaseId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribe(userId: string, knowledgeBaseId: string) {
|
||||||
|
await this.prisma.knowledgeBaseSubscription.deleteMany({
|
||||||
|
where: { userId, knowledgeBaseId },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findSubscribed(userId: string, opts?: { page?: number; limit?: number }) {
|
||||||
|
const page = opts?.page ?? 1;
|
||||||
|
const limit = opts?.limit ?? 20;
|
||||||
|
return this.prisma.knowledgeBase.findMany({
|
||||||
|
where: {
|
||||||
|
subscriptions: { some: { userId } },
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
orderBy: { updatedAt: 'desc' },
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async isSubscribed(userId: string, knowledgeBaseId: string): Promise<boolean> {
|
||||||
|
const sub = await this.prisma.knowledgeBaseSubscription.findUnique({
|
||||||
|
where: { userId_knowledgeBaseId: { userId, knowledgeBaseId } },
|
||||||
|
});
|
||||||
|
return !!sub;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,8 +26,8 @@ export class KnowledgeBaseService {
|
|||||||
return this.enrichWithCoverUrl(await this.repository.create(userId, dto));
|
return this.enrichWithCoverUrl(await this.repository.create(userId, dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(userId: string, pagination: { page?: number; limit?: number }) {
|
async findAll(userId: string, opts?: { page?: number; limit?: number; visibility?: string; ownerType?: string }) {
|
||||||
const kbs = await this.repository.findAllByUserId(userId, pagination);
|
const kbs = await this.repository.findAllByUserId(userId, opts);
|
||||||
return Promise.all(kbs.map(kb => this.enrichWithCoverUrl(kb)));
|
return Promise.all(kbs.map(kb => this.enrichWithCoverUrl(kb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,9 +46,11 @@ export class KnowledgeBaseService {
|
|||||||
|
|
||||||
async findOne(userId: string, id: string) {
|
async findOne(userId: string, id: string) {
|
||||||
const kb = await this.repository.findById(id);
|
const kb = await this.repository.findById(id);
|
||||||
if (!kb || String(kb.userId) !== userId) {
|
if (!kb || kb.deletedAt) throw new NotFoundException('知识库不存在');
|
||||||
throw new NotFoundException('知识库不存在');
|
// 公开库允许任何人查看
|
||||||
}
|
if (kb.visibility === 'public') return kb;
|
||||||
|
// 私有库只能 owner 查看
|
||||||
|
if (String(kb.userId) !== userId) throw new NotFoundException('知识库不存在');
|
||||||
return kb;
|
return kb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +74,49 @@ export class KnowledgeBaseService {
|
|||||||
return this.repository.softDelete(id);
|
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 ──
|
// ── Folder CRUD ──
|
||||||
|
|
||||||
async createFolder(kbId: string, dto: { name: string; parentId?: string }) {
|
async createFolder(kbId: string, dto: { name: string; parentId?: string }) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user