nex_math/backend/main.py

142 lines
4.3 KiB
Python

"""数学学习系统后端入口。"""
from __future__ import annotations
import json
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from sqlalchemy import text
from database import REPO_ROOT, SessionLocal, ensure_schema
from routers import (
analytics,
archive,
auth,
catalog,
courses,
data,
ebooks,
llm,
knowledge_graph,
overview,
practice,
question_bank,
users,
)
from seed.catalog import ensure_catalog
from services import ebooks as ebook_store
from seed.ebooks import ensure_ebooks
from seed.knowledge_graph import ensure_knowledge_graph
from seed.rbac import ensure_rbac
from services.learning_profile import (
ensure_default_profiles,
remove_unused_admin_profile,
user_has_admin_role,
)
from services.llm_config import ensure_llm_channels
from models import AttemptItem, ErrorEntry, Question, User
from services.knowledge_service import set_question_knowledge_names
ensure_schema()
db = SessionLocal()
try:
ensure_rbac(db)
ensure_knowledge_graph(db)
ensure_default_profiles(db)
for user in db.query(User).all():
if user_has_admin_role(db, user.id):
remove_unused_admin_profile(db, user.id)
ensure_catalog(db)
ensure_ebooks(db)
ebook_store.backfill_pages(db)
ensure_llm_channels(db)
db.execute(
text(
"UPDATE questions SET used_count = ("
"SELECT COUNT(*) FROM attempt_items "
"WHERE attempt_items.question_id = questions.id"
") WHERE id IN (SELECT DISTINCT question_id FROM attempt_items "
"WHERE question_id IS NOT NULL)"
)
)
for question in db.query(Question).all():
has_links = db.execute(
text(
"SELECT 1 FROM question_knowledge "
"WHERE question_id = :qid LIMIT 1"
),
{"qid": question.id},
).first()
if has_links is None:
set_question_knowledge_names(
db,
question,
[question.knowledge_name],
attach_to_chapter=True,
)
for row in db.query(ErrorEntry).all():
if not row.knowledge_names or row.knowledge_names == "[]":
row.knowledge_names = json.dumps(
[row.knowledge_name], ensure_ascii=False
)
for row in db.query(AttemptItem).all():
if not row.knowledge_names or row.knowledge_names == "[]":
row.knowledge_names = json.dumps(
[row.knowledge_name], ensure_ascii=False
)
db.commit()
finally:
db.close()
def _cors_origins() -> list[str]:
raw = os.getenv("CORS_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173")
return [origin.strip() for origin in raw.split(",") if origin.strip()]
app = FastAPI(
title="数学学习系统 API",
description="个人数学学习系统后端:章节组卷判分、掌握度引擎、错题与学习档案。",
version="1.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins(),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
# 阅读器依赖 Range 分段加载,需要能读到这几个响应头
expose_headers=["Accept-Ranges", "Content-Range", "Content-Length"],
)
app.include_router(overview.router, prefix="/api")
app.include_router(data.router, prefix="/api")
app.include_router(analytics.router, prefix="/api")
app.include_router(practice.router, prefix="/api")
app.include_router(archive.router, prefix="/api")
app.include_router(catalog.router, prefix="/api")
app.include_router(courses.router, prefix="/api")
app.include_router(ebooks.router, prefix="/api")
app.include_router(ebooks.progress_router, prefix="/api")
app.include_router(question_bank.router, prefix="/api")
app.include_router(llm.router, prefix="/api")
app.include_router(auth.router, prefix="/api")
app.include_router(users.router, prefix="/api")
app.include_router(knowledge_graph.router, prefix="/api")
image_dir = REPO_ROOT / "data" / "question_images"
image_dir.mkdir(parents=True, exist_ok=True)
app.mount(
"/api/question-images",
StaticFiles(directory=image_dir),
name="question-images",
)
@app.get("/api/health")
def health():
return {"status": "ok", "service": "math-learning-backend"}