nex_math/backend/routers/ebooks.py

142 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""电子书:管理员上传/替换 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,
)