146 lines
5.6 KiB
Python
146 lines
5.6 KiB
Python
from collections.abc import AsyncGenerator
|
|
from contextlib import asynccontextmanager
|
|
import logging
|
|
|
|
from fastapi import Depends, FastAPI, HTTPException, Request, status
|
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from sqlalchemy.exc import OperationalError, SQLAlchemyError
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.db import SessionLocal, backup_sqlite_files, get_db_session, init_db
|
|
from app.models import Comment, ContentItem, Hotspot, Report
|
|
from app.schemas import CreateTaskRequest, CreateTaskResponse, TaskResponse
|
|
from app.services.task_service import (
|
|
RUNNING_TASK_MESSAGE,
|
|
create_task,
|
|
get_task,
|
|
has_running_task,
|
|
list_tasks,
|
|
recover_running_tasks,
|
|
)
|
|
from app.templating import templates
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
DATABASE_UNAVAILABLE_MESSAGE = "数据库暂时不可用,请稍后重试或联系维护者恢复数据。"
|
|
|
|
|
|
def handle_database_error(request: Request, exc: SQLAlchemyError):
|
|
try:
|
|
backup_sqlite_files()
|
|
except Exception:
|
|
logger.exception("Failed to back up SQLite files after database error")
|
|
if request.url.path.startswith("/api/"):
|
|
return JSONResponse(status_code=503, content={"detail": DATABASE_UNAVAILABLE_MESSAGE})
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"errors/database_unavailable.html",
|
|
{"message": DATABASE_UNAVAILABLE_MESSAGE, "detail": str(exc)},
|
|
status_code=503,
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_app: FastAPI) -> AsyncGenerator[None]:
|
|
init_db()
|
|
with SessionLocal() as session:
|
|
recover_running_tasks(session)
|
|
yield
|
|
|
|
|
|
app = FastAPI(title="热榜评论分析工具", lifespan=lifespan)
|
|
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
|
|
|
|
|
@app.exception_handler(OperationalError)
|
|
def operational_error_handler(request: Request, exc: OperationalError):
|
|
return handle_database_error(request, exc)
|
|
|
|
|
|
@app.exception_handler(SQLAlchemyError)
|
|
def sqlalchemy_error_handler(request: Request, exc: SQLAlchemyError):
|
|
return handle_database_error(request, exc)
|
|
|
|
|
|
@app.get("/health")
|
|
def health() -> dict[str, str]:
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
def index_page(request: Request, session: Session = Depends(get_db_session)) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"index.html",
|
|
{"tasks": list_tasks(session)},
|
|
)
|
|
|
|
|
|
@app.post("/api/tasks", response_model=CreateTaskResponse, status_code=status.HTTP_201_CREATED)
|
|
def create_task_api(
|
|
request: CreateTaskRequest,
|
|
session: Session = Depends(get_db_session),
|
|
) -> CreateTaskResponse:
|
|
if has_running_task(session):
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=RUNNING_TASK_MESSAGE)
|
|
|
|
task = create_task(session, request)
|
|
return CreateTaskResponse(task_id=task.id, status=task.status)
|
|
|
|
|
|
@app.get("/api/tasks", response_model=list[TaskResponse])
|
|
def list_tasks_api(session: Session = Depends(get_db_session)) -> list[TaskResponse]:
|
|
return [TaskResponse.model_validate(task) for task in list_tasks(session)]
|
|
|
|
|
|
@app.get("/api/tasks/{task_id}", response_model=TaskResponse)
|
|
def get_task_api(task_id: str, session: Session = Depends(get_db_session)) -> TaskResponse:
|
|
task = get_task(session, task_id)
|
|
if task is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
|
|
return TaskResponse.model_validate(task)
|
|
|
|
|
|
@app.get("/tasks/{task_id}", response_class=HTMLResponse)
|
|
def task_detail_page(task_id: str, request: Request, session: Session = Depends(get_db_session)) -> HTMLResponse:
|
|
task = get_task(session, task_id)
|
|
if task is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
|
|
hotspots = list(session.scalars(select(Hotspot).where(Hotspot.task_id == task.id).order_by(Hotspot.rank)))
|
|
return templates.TemplateResponse(request, "tasks/detail.html", {"task": task, "hotspots": hotspots})
|
|
|
|
|
|
@app.get("/hotspots/{hotspot_id}/report", response_class=HTMLResponse)
|
|
def hotspot_report_page(hotspot_id: str, request: Request, session: Session = Depends(get_db_session)) -> HTMLResponse:
|
|
hotspot = session.get(Hotspot, hotspot_id)
|
|
if hotspot is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="热点不存在")
|
|
task = get_task(session, hotspot.task_id)
|
|
report = session.scalar(select(Report).where(Report.hotspot_id == hotspot.id, Report.report_type == "hotspot"))
|
|
return templates.TemplateResponse(request, "hotspots/report.html", {"task": task, "hotspot": hotspot, "report": report})
|
|
|
|
|
|
@app.get("/items/{item_id}", response_class=HTMLResponse)
|
|
def item_detail_page(item_id: str, request: Request, session: Session = Depends(get_db_session)) -> HTMLResponse:
|
|
item = session.get(ContentItem, item_id)
|
|
if item is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="内容条目不存在")
|
|
hotspot = session.get(Hotspot, item.hotspot_id)
|
|
task = get_task(session, item.task_id)
|
|
report = session.scalar(select(Report).where(Report.content_item_id == item.id, Report.report_type == "item"))
|
|
comments = list(
|
|
session.scalars(
|
|
select(Comment)
|
|
.where(Comment.content_item_id == item.id)
|
|
.order_by(Comment.like_count.desc().nullslast(), Comment.comment_time.desc().nullslast())
|
|
.limit(100)
|
|
)
|
|
)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"items/detail.html",
|
|
{"task": task, "hotspot": hotspot, "item": item, "report": report, "comments": comments},
|
|
)
|