fix: 删书即删对话——清理已删除书籍遗留的孤儿对话

根因:Conversation 与 Book 无 ORM 级联关系,且 SQLite 外键未强制开启,
delete_book 只删书/章节/段落,书的对话变成孤儿残留(book_id 指向不存在的书),
前端确认文案「书、目录和对话都会被删除」与事实不符,AI 助手会把这些幽灵
对话当作阅读记录。

- books.py delete_book: 显式删除该书的全部对话(消息经 ORM cascade 一并删)
- database.py _migrate: 启动时清理孤儿对话及其消息(book_id 不在 books 表内;
  assistant 全局对话 book_id 为空不受影响),幂等,本地/服务器下次启动即生效

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-10-25 17:46:24 +08:00
co-authored by Claude Code
parent f63c0c55c9
commit 64df54ad55
2 changed files with 19 additions and 1 deletions
+10 -1
View File
@@ -10,6 +10,7 @@ from sqlalchemy import select, func
from app.core.database import get_db, async_session
from app.core.config import settings
from app.models.book import Book, Chapter, Segment
from app.models.chat import Conversation
from app.schemas.book import BookOut, BookDetailOut, ChapterOut, SegmentOut
from app.epub.parser import parse_epub
from app.rag.vector import ensure_embeddings
@@ -139,8 +140,16 @@ async def delete_book(book_id: str, db: AsyncSession = Depends(get_db)):
if not book:
raise HTTPException(404, "书不存在")
file_path = book.file_path
# 删书即删对话(与前端确认文案「书、目录和对话都会被删除」一致)。
# Conversation 与 Book 没有 ORM 级联关系、SQLite 外键又未强制开启,
# 必须显式删除该书的对话;其消息经 Conversation.messages 的 cascade 一并删除。
convs = (await db.execute(
select(Conversation).where(Conversation.book_id == book_id)
)).scalars().all()
for c in convs:
await db.delete(c)
await db.delete(book)
await db.commit()
if file_path and os.path.exists(file_path):
os.remove(file_path)
return {"deleted": book_id}
return {"deleted": book_id, "conversations": len(convs)}
+9
View File
@@ -53,6 +53,15 @@ async def _migrate():
)
await conn.exec_driver_sql("DROP TABLE conversations_old")
# 数据完整性:删书即删对话。清理由已删除书籍遗留的孤儿对话及其消息
#(book_id 指向不存在的书;assistant 全局对话 book_id 为空,不受影响)。
await conn.exec_driver_sql(
"DELETE FROM conversations WHERE book_id IS NOT NULL AND book_id NOT IN (SELECT id FROM books)"
)
await conn.exec_driver_sql(
"DELETE FROM messages WHERE conversation_id NOT IN (SELECT id FROM conversations)"
)
async def get_db():
async with async_session() as session: