Files
life-echo/api/routers/books.py
Sully 2eb066dbec 把“章节正文 + 图片”从 chapters 单表/JSON 结构,重构为“章节 chapter + 段落 section + 图片 memoir_images 独立表”的新数据模型,同时联动修改接口、PDF 导出、异步任务、迁移脚本、测试,以及修复 Android 端聊天列表显示问题。 (#9)
* refactor: 表结构重构,新增段落section和图片image新表

* fix: fix android app import error

* refactor: 重构文件名

* fix: 优化提示词

* fix: 消息气泡显示位置异常问题

---------

Co-authored-by: yangshilin <2157598560@qq.com>
2026-03-13 11:12:10 +08:00

138 lines
4.3 KiB
Python
Raw 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.
"""
回忆录相关 API 路由
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Body
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_async_db
from database.models import Book as BookModel
from database.models import User as UserModel
from services.pdf_service import pdf_service
from middleware.auth import get_current_user
router = APIRouter(prefix="/api/books", tags=["books"])
@router.get("/current")
async def get_current_book(
current_user: UserModel = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""获取当前回忆录(需要认证)"""
stmt = select(BookModel).where(BookModel.user_id == current_user.id).order_by(BookModel.updated_at.desc()).limit(1)
result = await db.execute(stmt)
book = result.scalar_one_or_none()
if not book:
return {"message": "No book found"}
return {
"id": book.id,
"title": book.title,
"total_pages": book.total_pages,
"total_words": book.total_words,
"cover_image_url": book.cover_image_url,
"has_update": book.has_update,
"last_update_chapter_id": book.last_update_chapter_id,
}
@router.post("/clear-update")
async def clear_book_update(
current_user: UserModel = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db),
):
"""清除回忆录更新标记"""
stmt = select(BookModel).where(BookModel.user_id == current_user.id).order_by(BookModel.updated_at.desc()).limit(1)
result = await db.execute(stmt)
book = result.scalar_one_or_none()
if not book:
return {"status": "ok", "message": "No book found"}
book.has_update = False
await db.commit()
return {"status": "ok"}
class UpdateBookRequest(BaseModel):
title: str
subtitle: str | None = None # 目前数据库不支持subtitle但保留字段以便将来扩展
@router.put("/{book_id}")
async def update_book(
book_id: str,
request: UpdateBookRequest = Body(...),
current_user: UserModel = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""更新书籍标题(需要认证,只能更新自己的回忆录)"""
book = await db.get(BookModel, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
# 验证用户权限
if book.user_id != current_user.id:
raise HTTPException(status_code=403, detail="无权更新此回忆录")
# 更新标题
book.title = request.title
# subtitle字段目前数据库不支持暂时忽略
await db.commit()
await db.refresh(book)
return {
"id": book.id,
"title": book.title,
"total_pages": book.total_pages,
"total_words": book.total_words,
"cover_image_url": book.cover_image_url,
"has_update": book.has_update,
"last_update_chapter_id": book.last_update_chapter_id,
}
class ExportPdfRequest(BaseModel):
book_id: str
@router.post("/export-pdf")
async def export_pdf(
request: ExportPdfRequest = Body(...),
current_user: UserModel = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""导出 PDF需要认证只能导出自己的回忆录"""
book = await db.get(BookModel, request.book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
# 验证用户权限
if book.user_id != current_user.id:
raise HTTPException(status_code=403, detail="无权导出此回忆录")
# 获取所有 active 章节并预加载 sections供 PDF 按段渲染)
from database.models import Chapter
from sqlalchemy.orm import joinedload
stmt = (
select(Chapter)
.where(
Chapter.user_id == current_user.id,
Chapter.is_active == True,
)
.options(joinedload(Chapter.sections))
.order_by(Chapter.order_index)
)
result = await db.execute(stmt)
chapters = result.unique().scalars().all()
# 生成 PDF
pdf_bytes = await pdf_service.generate_pdf(book, chapters)
return {
"pdf_base64": pdf_bytes.decode('latin1'), # 简化处理,实际应该用 base64
"filename": f"{book.title}.pdf"
}