"""数学学习系统后端入口。""" 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"}