Files
my_bookmate/backend/app/models/book.py
T
gavindiaz 956323007d fix: 删书彻底清理文件与DB + 阅读位置跨设备同步
删除不彻底的根因是库里 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 接口

- 前端打开时优先取服务端位置(本地兜底并双端拉齐),翻页防抖同步到服务端,关闭时把防抖窗口内的最后一页立即刷上去
2026-10-27 21:42:04 +08:00

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())