483fdb982b
- 创建后端数据库模型:DocumentChapter 和 Document,支持按章节组织的树形文档结构 - 创建数据库迁移文件:002_add_document_management.sql - 创建 Pydantic schemas:定义文档和章节的请求/响应模型 - 创建后端服务层:document_service.py 实现 CRUD 和树形结构构建 - 创建 API 路由:/api/v1/documents 和 /api/v1/documents/chapters,支持完整的 RESTful 操作 - 创建前端类型定义:document.ts - 创建文档管理页面:/admin/documents,包含章节树形目录和文档表格,支持增删改查 - 创建文档展示页面:/admin/docs-view,左侧目录树右侧内容展示,支持 Markdown 渲染 - 安装 react-markdown 依赖用于文档内容展示 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
252 lines
7.4 KiB
Python
252 lines
7.4 KiB
Python
from __future__ import annotations
|
|
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ..models.document import Document, DocumentChapter
|
|
from ..schemas.document import (
|
|
DocumentChapterCreateRequest,
|
|
DocumentChapterListResponse,
|
|
DocumentChapterPublic,
|
|
DocumentChapterTreeItem,
|
|
DocumentChapterUpdateRequest,
|
|
DocumentCreateRequest,
|
|
DocumentListResponse,
|
|
DocumentPublic,
|
|
DocumentUpdateRequest,
|
|
)
|
|
|
|
|
|
def serialize_document_chapter(chapter: DocumentChapter) -> DocumentChapterPublic:
|
|
return DocumentChapterPublic(
|
|
id=chapter.id,
|
|
name=chapter.name,
|
|
description=chapter.description,
|
|
parent_id=chapter.parent_id,
|
|
sort_order=chapter.sort_order,
|
|
created_at=chapter.created_at,
|
|
updated_at=chapter.updated_at,
|
|
)
|
|
|
|
|
|
def serialize_document(document: Document) -> DocumentPublic:
|
|
return DocumentPublic(
|
|
id=document.id,
|
|
title=document.title,
|
|
content=document.content,
|
|
chapter_id=document.chapter_id,
|
|
sort_order=document.sort_order,
|
|
status=document.status,
|
|
created_at=document.created_at,
|
|
updated_at=document.updated_at,
|
|
)
|
|
|
|
|
|
def build_chapter_tree(chapters: list[DocumentChapter]) -> list[DocumentChapterTreeItem]:
|
|
chapter_map = {c.id: c for c in chapters}
|
|
tree = []
|
|
|
|
for chapter in chapters:
|
|
if chapter.parent_id is None:
|
|
tree_item = DocumentChapterTreeItem(
|
|
id=chapter.id,
|
|
name=chapter.name,
|
|
description=chapter.description,
|
|
parent_id=chapter.parent_id,
|
|
sort_order=chapter.sort_order,
|
|
created_at=chapter.created_at,
|
|
updated_at=chapter.updated_at,
|
|
children=[],
|
|
documents=[serialize_document(d) for d in chapter.documents],
|
|
)
|
|
tree.append(tree_item)
|
|
|
|
def add_children(node: DocumentChapterTreeItem):
|
|
for chapter in chapters:
|
|
if chapter.parent_id == node.id:
|
|
child_item = DocumentChapterTreeItem(
|
|
id=chapter.id,
|
|
name=chapter.name,
|
|
description=chapter.description,
|
|
parent_id=chapter.parent_id,
|
|
sort_order=chapter.sort_order,
|
|
created_at=chapter.created_at,
|
|
updated_at=chapter.updated_at,
|
|
children=[],
|
|
documents=[serialize_document(d) for d in chapter.documents],
|
|
)
|
|
node.children.append(child_item)
|
|
add_children(child_item)
|
|
|
|
for item in tree:
|
|
add_children(item)
|
|
|
|
return tree
|
|
|
|
|
|
def list_document_chapters(
|
|
db: Session,
|
|
*,
|
|
limit: int,
|
|
offset: int,
|
|
keyword: str | None = None,
|
|
) -> DocumentChapterListResponse:
|
|
stmt = select(DocumentChapter)
|
|
if keyword:
|
|
stmt = stmt.where(DocumentChapter.name.ilike(f"%{keyword}%"))
|
|
stmt = stmt.order_by(DocumentChapter.sort_order, DocumentChapter.id)
|
|
stmt = stmt.limit(limit).offset(offset)
|
|
|
|
total_stmt = select(func.count()).select_from(DocumentChapter)
|
|
if keyword:
|
|
total_stmt = total_stmt.where(DocumentChapter.name.ilike(f"%{keyword}%"))
|
|
|
|
total = db.scalar(total_stmt) or 0
|
|
chapters = list(db.scalars(stmt).all())
|
|
|
|
return DocumentChapterListResponse(
|
|
items=[serialize_document_chapter(c) for c in chapters],
|
|
total=total,
|
|
)
|
|
|
|
|
|
def get_document_chapter_tree(db: Session) -> list[DocumentChapterTreeItem]:
|
|
stmt = select(DocumentChapter).order_by(DocumentChapter.sort_order, DocumentChapter.id)
|
|
chapters = list(db.scalars(stmt).all())
|
|
return build_chapter_tree(chapters)
|
|
|
|
|
|
def get_document_chapter_by_id(db: Session, chapter_id: int) -> DocumentChapter | None:
|
|
return db.get(DocumentChapter, chapter_id)
|
|
|
|
|
|
def create_document_chapter(
|
|
db: Session, payload: DocumentChapterCreateRequest
|
|
) -> DocumentChapter:
|
|
chapter = DocumentChapter(
|
|
name=payload.name,
|
|
description=payload.description,
|
|
parent_id=payload.parent_id,
|
|
sort_order=payload.sort_order,
|
|
)
|
|
db.add(chapter)
|
|
db.commit()
|
|
db.refresh(chapter)
|
|
return chapter
|
|
|
|
|
|
def update_document_chapter(
|
|
db: Session, chapter_id: int, payload: DocumentChapterUpdateRequest
|
|
) -> DocumentChapter | None:
|
|
chapter = db.get(DocumentChapter, chapter_id)
|
|
if not chapter:
|
|
return None
|
|
|
|
if payload.name is not None:
|
|
chapter.name = payload.name
|
|
if payload.description is not None:
|
|
chapter.description = payload.description
|
|
if payload.parent_id is not None:
|
|
chapter.parent_id = payload.parent_id
|
|
if payload.sort_order is not None:
|
|
chapter.sort_order = payload.sort_order
|
|
|
|
db.commit()
|
|
db.refresh(chapter)
|
|
return chapter
|
|
|
|
|
|
def delete_document_chapter(db: Session, chapter_id: int) -> bool:
|
|
chapter = db.get(DocumentChapter, chapter_id)
|
|
if not chapter:
|
|
return False
|
|
db.delete(chapter)
|
|
db.commit()
|
|
return True
|
|
|
|
|
|
def list_documents(
|
|
db: Session,
|
|
*,
|
|
limit: int,
|
|
offset: int,
|
|
keyword: str | None = None,
|
|
chapter_id: int | None = None,
|
|
status: str | None = None,
|
|
) -> DocumentListResponse:
|
|
stmt = select(Document)
|
|
if keyword:
|
|
stmt = stmt.where(Document.title.ilike(f"%{keyword}%"))
|
|
if chapter_id is not None:
|
|
stmt = stmt.where(Document.chapter_id == chapter_id)
|
|
if status:
|
|
stmt = stmt.where(Document.status == status)
|
|
stmt = stmt.order_by(Document.sort_order, Document.id)
|
|
stmt = stmt.limit(limit).offset(offset)
|
|
|
|
total_stmt = select(func.count()).select_from(Document)
|
|
if keyword:
|
|
total_stmt = total_stmt.where(Document.title.ilike(f"%{keyword}%"))
|
|
if chapter_id is not None:
|
|
total_stmt = total_stmt.where(Document.chapter_id == chapter_id)
|
|
if status:
|
|
total_stmt = total_stmt.where(Document.status == status)
|
|
|
|
total = db.scalar(total_stmt) or 0
|
|
documents = list(db.scalars(stmt).all())
|
|
|
|
return DocumentListResponse(
|
|
items=[serialize_document(d) for d in documents],
|
|
total=total,
|
|
)
|
|
|
|
|
|
def get_document_by_id(db: Session, document_id: int) -> Document | None:
|
|
return db.get(Document, document_id)
|
|
|
|
|
|
def create_document(db: Session, payload: DocumentCreateRequest) -> Document:
|
|
document = Document(
|
|
title=payload.title,
|
|
content=payload.content,
|
|
chapter_id=payload.chapter_id,
|
|
sort_order=payload.sort_order,
|
|
status=payload.status,
|
|
)
|
|
db.add(document)
|
|
db.commit()
|
|
db.refresh(document)
|
|
return document
|
|
|
|
|
|
def update_document(
|
|
db: Session, document_id: int, payload: DocumentUpdateRequest
|
|
) -> Document | None:
|
|
document = db.get(Document, document_id)
|
|
if not document:
|
|
return None
|
|
|
|
if payload.title is not None:
|
|
document.title = payload.title
|
|
if payload.content is not None:
|
|
document.content = payload.content
|
|
if payload.chapter_id is not None:
|
|
document.chapter_id = payload.chapter_id
|
|
if payload.sort_order is not None:
|
|
document.sort_order = payload.sort_order
|
|
if payload.status is not None:
|
|
document.status = payload.status
|
|
|
|
db.commit()
|
|
db.refresh(document)
|
|
return document
|
|
|
|
|
|
def delete_document(db: Session, document_id: int) -> bool:
|
|
document = db.get(Document, document_id)
|
|
if not document:
|
|
return False
|
|
db.delete(document)
|
|
db.commit()
|
|
return True
|