Files
zTools2/app/models/pdf_job.py
zikai aa08a1cf60 feat: 新增 PDF 转换服务(epub->pdf,cookie 用户隔离,软删/硬删两级删除)
纯 Python 转换(ebooklib 解析 epub + weasyprint 渲染 HTML/CSS 为 PDF),
无需 calibre/xvfb 系统依赖。复用 UploadedFile 存储(原始文件与产物 PDF)。

- model/dao/schema:PdfJob 记录转换任务(owner_cookie/source/output/进度/状态)
- pdf_converter:epub->PDF 转换器(按 spine 顺序拼 HTML,OPF 目录解析相对资源)
- pdf_service:上传落盘 + asyncio 后台转换(独立 Session)+ 进度追踪 + 两级删除
  · 用户软删(user_deleted,管理页仍可见)· 管理员硬删(真正删磁盘+记录)
- pdf_controller:/api/pdf/*(用户,cookie)+ /api/admin/pdf/*(Basic Auth)
- /pdf、/pdf-admin 页面(原生 JS,复用 common.css/js)
- 配置 pdf 段(max_size 250MB、转换超时、cookie)
- pytest 7 项(SQLite→MySQL 隔离测试库):上传/转换/下载/软删/硬删/超限/越权
2026-07-27 11:33:43 +08:00

57 lines
2.8 KiB
Python
Raw Permalink 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.

"""PDF 转换任务实体。
一个任务记录一次「上传文件 -> 转为 PDF」的转换。原始上传文件与转换产物 PDF
均复用 UploadedFile 存储(落盘 + 元数据入库),本表只记录两者关系与转换状态,
不重复实现存储逻辑。
删除语义(两级):
用户软删user_deleted=true-- 用户页不再展示,但磁盘与 DB 行保留;
管理页仍可见且标注「已删除」。
管理员硬删 -- 删原始/产物磁盘文件 + UploadedFile 行 + 本表行,真正删除。
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import BigInteger, Boolean, DateTime, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from ..database import Base
class PdfJob(Base):
__tablename__ = "pdf_job"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
# 用户标识httpOnly cookie 值uuid4.hex用户凭此查看自己的任务
owner_cookie: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
# 原始上传文件(复用 UploadedFile 存储)
source_file_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
source_filename: Mapped[str] = mapped_column(String(512), nullable=False)
source_size: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0)
# 转换产物 PDF复用 UploadedFile 存储);转换完成前为 NULL
output_file_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, default=None)
# pending(排队) / converting(转换中) / done(完成) / failed(失败)
status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending", index=True)
# 转换进度 0-100供前端轮询显示
progress: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
# 失败原因status=failed 时填写)
error_message: Mapped[str] = mapped_column(String(512), nullable=False, default="")
# 用户软删标记true=用户已从其页面删除,不再对用户展示
user_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
# 用户软删时间(管理页展示「是否已删除」时用)
deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None)
created_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
)
def __repr__(self) -> str: # pragma: no cover
return (
f"<PdfJob id={self.id} status={self.status} "
f"src={self.source_filename!r} progress={self.progress}>"
)