Files
NovalRedot/backend/app/routers/outlines.py

249 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session, select, func
from ..database import get_session
from ..models import Novel, Outline
from ..schemas import (
OutlineCreate,
OutlineUpdate,
OutlineRead,
OutlineList,
OutlineReorder,
)
router = APIRouter(prefix="/api/novels/{novel_id}/outlines", tags=["outlines"])
def _get_novel_or_404(novel_id: int, session: Session) -> Novel:
novel = session.get(Novel, novel_id)
if not novel:
raise HTTPException(status_code=404, detail="小说不存在")
return novel
def _get_outline_or_404(
outline_id: int, novel_id: int, session: Session
) -> Outline:
outline = session.get(Outline, outline_id)
if not outline or outline.novel_id != novel_id:
raise HTTPException(status_code=404, detail="大纲不存在")
return outline
def _build_outline_read(outline: Outline, children: list[Outline]) -> OutlineRead:
"""将 Outline ORM 对象转为 OutlineRead附带子节点列表"""
return OutlineRead(
id=outline.id,
novel_id=outline.novel_id,
parent_id=outline.parent_id,
sort_order=outline.sort_order,
summary=outline.summary,
detail=outline.detail,
children=[
OutlineRead(
id=c.id,
novel_id=c.novel_id,
parent_id=c.parent_id,
sort_order=c.sort_order,
summary=c.summary,
detail=c.detail,
children=[],
created_at=c.created_at,
updated_at=c.updated_at,
)
for c in children
],
created_at=outline.created_at,
updated_at=outline.updated_at,
)
@router.get("", response_model=OutlineList)
def list_outlines(
novel_id: int,
session: Session = Depends(get_session),
):
_get_novel_or_404(novel_id, session)
# 只查顶级节点parent_id IS NULL
top_items = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id.is_(None)) # type: ignore
.order_by(Outline.sort_order)
).all()
# 查询所有子节点,按 parent_id 分组
all_children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id.isnot(None)) # type: ignore
.order_by(Outline.sort_order)
).all()
children_map: dict[int, list[Outline]] = {}
for child in all_children:
children_map.setdefault(child.parent_id, []).append(child)
items = [
_build_outline_read(o, children_map.get(o.id, []))
for o in top_items
]
return OutlineList(items=items, total=len(items))
@router.post("", response_model=OutlineRead, status_code=201)
def create_outline(
novel_id: int,
data: OutlineCreate,
session: Session = Depends(get_session),
):
_get_novel_or_404(novel_id, session)
# 如果指定了 parent_id校验父节点存在且是顶级节点
if data.parent_id is not None:
parent = session.get(Outline, data.parent_id)
if not parent or parent.novel_id != novel_id:
raise HTTPException(status_code=404, detail="父节点不存在")
if parent.parent_id is not None:
raise HTTPException(status_code=400, detail="仅支持两级结构,不能在子节点下创建子节点")
# 同级内自增 sort_order同 parent_id 下的 max + 1
if data.parent_id is not None:
max_order = session.exec(
select(func.max(Outline.sort_order))
.where(Outline.novel_id == novel_id, Outline.parent_id == data.parent_id)
).one()
else:
max_order = session.exec(
select(func.max(Outline.sort_order))
.where(Outline.novel_id == novel_id, Outline.parent_id.is_(None)) # type: ignore
).one()
next_order = (max_order or 0) + 1
outline = Outline(
novel_id=novel_id,
parent_id=data.parent_id,
sort_order=next_order,
summary=data.summary,
detail=data.detail,
)
session.add(outline)
session.commit()
session.refresh(outline)
# 返回时附带空 children
return _build_outline_read(outline, [])
@router.put("/reorder", response_model=OutlineList)
def reorder_outlines(
novel_id: int,
data: OutlineReorder,
session: Session = Depends(get_session),
):
"""仅处理顶级节点排序"""
_get_novel_or_404(novel_id, session)
now = datetime.now(timezone.utc)
for idx, oid in enumerate(data.ordered_ids):
outline = _get_outline_or_404(oid, novel_id, session)
if outline.parent_id is not None:
raise HTTPException(status_code=400, detail=f"节点 {oid} 不是顶级节点,不能在此排序")
outline.sort_order = idx + 1
outline.updated_at = now
session.add(outline)
session.commit()
# 重新查询并返回嵌套结构
return list_outlines(novel_id, session)
@router.put("/{outline_id}/children/reorder", response_model=OutlineRead)
def reorder_children(
novel_id: int,
outline_id: int,
data: OutlineReorder,
session: Session = Depends(get_session),
):
"""子节点排序"""
_get_novel_or_404(novel_id, session)
parent = _get_outline_or_404(outline_id, novel_id, session)
if parent.parent_id is not None:
raise HTTPException(status_code=400, detail="只有顶级节点可以排序子节点")
now = datetime.now(timezone.utc)
for idx, cid in enumerate(data.ordered_ids):
child = _get_outline_or_404(cid, novel_id, session)
if child.parent_id != outline_id:
raise HTTPException(status_code=400, detail=f"节点 {cid} 不是该父节点的子节点")
child.sort_order = idx + 1
child.updated_at = now
session.add(child)
session.commit()
# 查询更新后的子节点
children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id == outline_id)
.order_by(Outline.sort_order)
).all()
session.refresh(parent)
return _build_outline_read(parent, list(children))
@router.get("/{outline_id}", response_model=OutlineRead)
def get_outline(
novel_id: int,
outline_id: int,
session: Session = Depends(get_session),
):
outline = _get_outline_or_404(outline_id, novel_id, session)
# 查询子节点
children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id == outline_id)
.order_by(Outline.sort_order)
).all()
return _build_outline_read(outline, list(children))
@router.patch("/{outline_id}", response_model=OutlineRead)
def update_outline(
novel_id: int,
outline_id: int,
data: OutlineUpdate,
session: Session = Depends(get_session),
):
outline = _get_outline_or_404(outline_id, novel_id, session)
update_data = data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(outline, key, value)
outline.updated_at = datetime.now(timezone.utc)
session.add(outline)
session.commit()
session.refresh(outline)
# 查询子节点
children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id == outline_id)
.order_by(Outline.sort_order)
).all()
return _build_outline_read(outline, list(children))
@router.delete("/{outline_id}", status_code=204)
def delete_outline(
novel_id: int,
outline_id: int,
session: Session = Depends(get_session),
):
outline = _get_outline_or_404(outline_id, novel_id, session)
# 删除父节点时先删除所有子节点
children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id == outline_id)
).all()
for child in children:
session.delete(child)
session.delete(outline)
session.commit()