"""电子书:管理员上传/替换 PDF·EPUB,登录用户在线阅读并记录进度。""" from __future__ import annotations from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse from sqlalchemy.orm import Session from database import get_db from dependencies import get_current_user, require_permission from models import EbookProgress, Textbook, User from schemas import EbookProgressIn, EbookProgressOut from services import ebooks as store # 上传 / 删除 / 取文件挂在教材下,进度单独成组 router = APIRouter(prefix="/textbooks", tags=["ebooks"]) progress_router = APIRouter(prefix="/ebooks", tags=["ebooks"]) def _load(db: Session, textbook_id: int) -> Textbook: textbook = db.get(Textbook, textbook_id) if textbook is None: raise HTTPException(status_code=404, detail="教材不存在") return textbook def _require_file(textbook: Textbook) -> None: if not textbook.ebook_file: raise HTTPException(status_code=404, detail="该书还没有上传电子书") @router.post("/{textbook_id}/ebook") async def upload_ebook( textbook_id: int, file: UploadFile = File(...), db: Session = Depends(get_db), _: User = Depends(require_permission("catalog:manage")), ): textbook = _load(db, textbook_id) await store.store_upload(db, textbook, file) return { "id": textbook.id, "ebook_format": textbook.ebook_format, "ebook_name": textbook.ebook_name, "ebook_size": textbook.ebook_size, "ebook_pages": textbook.ebook_pages, } @router.delete("/{textbook_id}/ebook") def delete_ebook( textbook_id: int, db: Session = Depends(get_db), _: User = Depends(require_permission("catalog:manage")), ): textbook = _load(db, textbook_id) _require_file(textbook) store.clear_ebook(db, textbook) return {"deleted": textbook_id} @router.get("/{textbook_id}/ebook/file/{filename}") def ebook_file( textbook_id: int, filename: str, db: Session = Depends(get_db), _: User = Depends(get_current_user), ): """阅读器取原始文件。FileResponse 自带 Range/206 支持,PDF 可分段加载。""" textbook = _load(db, textbook_id) _require_file(textbook) if filename != textbook.ebook_file: raise HTTPException(status_code=404, detail="电子书文件不存在") path = store.resolve_path(textbook.id, filename) return FileResponse( path, media_type=store.media_type(textbook.ebook_format), filename=textbook.ebook_name or path.name, content_disposition_type="inline", ) @progress_router.get("/progress", response_model=list[EbookProgressOut]) def list_progress( db: Session = Depends(get_db), user: User = Depends(get_current_user), ): rows = ( db.query(EbookProgress) .filter(EbookProgress.user_id == user.id) .all() ) return [ EbookProgressOut( textbook_id=row.textbook_id, locator=row.locator, page=row.page, total=row.total, position=row.position, updated_at=row.updated_at, ) for row in rows ] @progress_router.put("/{textbook_id}/progress", response_model=EbookProgressOut) def save_progress( textbook_id: int, payload: EbookProgressIn, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): textbook = _load(db, textbook_id) _require_file(textbook) row = ( db.query(EbookProgress) .filter( EbookProgress.user_id == user.id, EbookProgress.textbook_id == textbook_id, ) .first() ) if row is None: row = EbookProgress(user_id=user.id, textbook_id=textbook_id) db.add(row) row.locator = payload.locator[:512] row.page = max(0, payload.page) row.total = max(0, payload.total) row.position = min(1.0, max(0.0, payload.position)) # 阅读器拿到的才是真实篇幅(PDF 页数 / EPUB 章节数),回填给管理端展示 if row.total > 0 and textbook.ebook_pages != row.total: textbook.ebook_pages = row.total db.commit() return EbookProgressOut( textbook_id=textbook_id, locator=row.locator, page=row.page, total=row.total, position=row.position, updated_at=row.updated_at, )