249 lines
7.8 KiB
Python
249 lines
7.8 KiB
Python
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()
|