删除不彻底的根因是库里 file_path 存了旧 Windows 相对路径(uploads\xxx.epub),Linux 容器里 os.path.exists 永远为假:
- 新增 _upload_file() 归一化路径(取文件名挂到当前 uploads 目录),读/删都走它,老库迁移的文件也能定位;文件删除挪到后台任务,失败不影响接口
- 删除后对 SQLite 执行 VACUUM,让 db 文件真正缩小
- 删除时一并清理 reading_progress
阅读位置原来只存 localStorage,换设备丢失:
- 新增 reading_progress 表 + GET/PUT /books/{id}/progress 接口
- 前端打开时优先取服务端位置(本地兜底并双端拉齐),翻页防抖同步到服务端,关闭时把防抖窗口内的最后一页立即刷上去
86 lines
4.2 KiB
Python
86 lines
4.2 KiB
Python
import uuid
|
|
from datetime import datetime
|
|
from sqlalchemy import String, Integer, Float, Text, DateTime, ForeignKey, JSON, func
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
from app.core.database import Base
|
|
|
|
|
|
def gen_uuid():
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
class Book(Base):
|
|
__tablename__ = "books"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=gen_uuid)
|
|
filename: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
title: Mapped[str | None] = mapped_column(String(500))
|
|
author: Mapped[str | None] = mapped_column(String(500))
|
|
language: Mapped[str] = mapped_column(String(20), default="zh")
|
|
file_path: Mapped[str] = mapped_column(String(1000), nullable=False)
|
|
file_hash: Mapped[str | None] = mapped_column(String(64), unique=True)
|
|
char_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
status: Mapped[str] = mapped_column(String(20), default="uploaded")
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
|
|
chapters: Mapped[list["Chapter"]] = relationship(back_populates="book", cascade="all, delete-orphan")
|
|
|
|
|
|
class Chapter(Base):
|
|
__tablename__ = "chapters"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=gen_uuid)
|
|
book_id: Mapped[str] = mapped_column(String(36), ForeignKey("books.id", ondelete="CASCADE"))
|
|
order_idx: Mapped[int] = mapped_column(Integer, default=0)
|
|
title: Mapped[str | None] = mapped_column(String(500))
|
|
level: Mapped[int] = mapped_column(Integer, default=1)
|
|
heading_path: Mapped[str | None] = mapped_column(Text)
|
|
cfi_start: Mapped[str | None] = mapped_column(String(500))
|
|
cfi_end: Mapped[str | None] = mapped_column(String(500))
|
|
char_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
|
|
book: Mapped["Book"] = relationship(back_populates="chapters")
|
|
segments: Mapped[list["Segment"]] = relationship(back_populates="chapter", cascade="all, delete-orphan")
|
|
|
|
|
|
class Segment(Base):
|
|
__tablename__ = "segments"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=gen_uuid)
|
|
chapter_id: Mapped[str] = mapped_column(String(36), ForeignKey("chapters.id", ondelete="CASCADE"))
|
|
book_id: Mapped[str] = mapped_column(String(36), ForeignKey("books.id", ondelete="CASCADE"))
|
|
order_idx: Mapped[int] = mapped_column(Integer, default=0)
|
|
text: Mapped[str] = mapped_column(Text, nullable=False)
|
|
char_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
cfi: Mapped[str | None] = mapped_column(String(500))
|
|
page_estimate: Mapped[float | None] = mapped_column(Float)
|
|
metadata_: Mapped[str | None] = mapped_column("metadata", Text)
|
|
embedding: Mapped[str | None] = mapped_column(Text) # JSON 浮点数组(向量检索用);NULL = 尚未生成向量
|
|
|
|
chapter: Mapped["Chapter"] = relationship(back_populates="segments")
|
|
|
|
|
|
class ReadingNote(Base):
|
|
"""A user-owned passage, highlight, or note anchored to an EPUB CFI."""
|
|
__tablename__ = "reading_notes"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=gen_uuid)
|
|
book_id: Mapped[str] = mapped_column(String(36), ForeignKey("books.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
kind: Mapped[str] = mapped_column(String(20), nullable=False, default="excerpt")
|
|
excerpt: Mapped[str] = mapped_column(Text, nullable=False)
|
|
note: Mapped[str | None] = mapped_column(Text)
|
|
cfi: Mapped[str] = mapped_column(String(2000), nullable=False)
|
|
chapter_title: Mapped[str | None] = mapped_column(String(500))
|
|
color: Mapped[str] = mapped_column(String(20), nullable=False, default="yellow")
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
|
|
|
|
class ReadingProgress(Base):
|
|
"""每本书的最近阅读位置(跨设备同步;书删除时一并清理)。"""
|
|
__tablename__ = "reading_progress"
|
|
|
|
book_id: Mapped[str] = mapped_column(String(36), ForeignKey("books.id", ondelete="CASCADE"), primary_key=True)
|
|
cfi: Mapped[str] = mapped_column(String(2000), nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
|