Files
secondary-school-grade-archive/backend/app/main.py
T
dekun f1ad4273f4 零 Node 部署、超级管理员,并完善本地构建发布文档。
- FastAPI 单进程托管 frontend/dist,systemd 替代 PM2

- 超级管理员 admin、注册开关与用户管理

- README/DEPLOY/USAGE 说明:改代码须本地构建 dist 后 push,服务器 update.sh

- 提交 frontend/dist 与 build-frontend 脚本
2026-06-28 13:19:41 +08:00

82 lines
2.7 KiB
Python

from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from app.core.config import settings
from app.core.database import Base, SessionLocal, engine
from app.routers import admin, auth, exams, export, settings as settings_router, students, subjects, wrong_questions
from app.services.migrate import run_migrations
from app.services.seed import seed_admin_and_settings, seed_subjects
def resolve_frontend_dist() -> Path | None:
backend_dir = Path(__file__).resolve().parent.parent
dist = Path(settings.FRONTEND_DIST)
if not dist.is_absolute():
dist = (backend_dir / dist).resolve()
if dist.is_dir() and (dist / "index.html").is_file():
return dist
return None
@asynccontextmanager
async def lifespan(app: FastAPI):
Path(settings.UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
Base.metadata.create_all(bind=engine)
run_migrations()
db = SessionLocal()
try:
seed_subjects(db)
seed_admin_and_settings(db)
finally:
db.close()
yield
app = FastAPI(title="中学成绩档案", version="1.0.0", lifespan=lifespan)
origins = [o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router, prefix="/api")
app.include_router(settings_router.router, prefix="/api")
app.include_router(admin.router, prefix="/api")
app.include_router(students.router, prefix="/api")
app.include_router(subjects.router, prefix="/api")
app.include_router(exams.router, prefix="/api")
app.include_router(wrong_questions.router, prefix="/api")
app.include_router(export.router, prefix="/api")
@app.get("/api/health")
def health():
return {"status": "ok"}
dist_dir = resolve_frontend_dist()
if dist_dir is not None:
assets_dir = dist_dir / "assets"
if assets_dir.is_dir():
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
@app.get("/{full_path:path}", include_in_schema=False)
async def serve_spa(full_path: str):
if full_path.startswith("api") or full_path.startswith("api/"):
raise HTTPException(status_code=404, detail="Not Found")
if full_path in ("", "index.html"):
return FileResponse(dist_dir / "index.html")
candidate = dist_dir / full_path
if candidate.is_file():
return FileResponse(candidate)
return FileResponse(dist_dir / "index.html")