"""FastAPI 应用入口""" import os import sys from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from app.config import get_config, Config from app.database import init_db, get_connection @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期:启动时初始化,关闭时清理""" # 启动 config = app.state.config config.data_dir.mkdir(parents=True, exist_ok=True) config.problems_dir.mkdir(parents=True, exist_ok=True) config.papers_dir.mkdir(parents=True, exist_ok=True) config.backups_dir.mkdir(parents=True, exist_ok=True) init_db(config.db_path) print(f"✓ 数据库已初始化: {config.db_path}") print(f"✓ API Token: {config.token}") print(f"✓ 服务启动在 http://{config.host}:{config.port}") yield def create_app(config: Config) -> FastAPI: app = FastAPI( title="题库PLUS", description="C/S 架构数学题库管理系统", version="1.0.0", lifespan=lifespan, ) app.state.config = config # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 全局异常处理 @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): import traceback traceback.print_exc() return JSONResponse( status_code=500, content={"error": {"code": "internal_error", "message": str(exc)}}, ) # 注册路由 from app.routers import auth, problems, import_, export_, knowledge, carts, admin app.include_router(auth.router, prefix="/api", tags=["auth"]) app.include_router(problems.router, prefix="/api", tags=["problems"]) app.include_router(import_.router, prefix="/api", tags=["import"]) app.include_router(export_.router, prefix="/api", tags=["export"]) app.include_router(knowledge.router, prefix="/api", tags=["knowledge"]) app.include_router(carts.router, prefix="/api", tags=["carts"]) app.include_router(admin.router, prefix="/api", tags=["admin"]) # Flutter Web 静态文件 web_dir = Path("web") if web_dir.exists() and any(web_dir.iterdir()): app.mount("/", StaticFiles(directory=str(web_dir), html=True), name="web") return app def main(): """CLI 入口""" if len(sys.argv) < 2: print("用法: tiku-server <命令> [参数]") print() print("命令:") print(" serve 启动服务") print(" serve --restart 启动服务(崩溃/更新后自动重启)") print(" knowledge build 重建知识点编码") print(" rebuild-index 手动增量索引") print(" stats 终端统计") print(" backup 手动备份") print(" backup --remote 推送到 WebDAV") print(" token 显示/重新生成 API token") print(" token regenerate 重新生成 API token") sys.exit(1) cmd = sys.argv[1] config_path = os.environ.get("TIKU_CONFIG", "config.yaml") cfg = get_config(config_path) if cmd == "serve": import uvicorn auto_restart = len(sys.argv) > 2 and sys.argv[2] == "--restart" def run(): uvicorn.run( create_app(cfg), host=cfg.host, port=cfg.port, log_level="info", ) if auto_restart: import time as _time while True: print(f"{_time.strftime('%Y-%m-%d %H:%M:%S')} 启动服务端...") run() print(f"{_time.strftime('%Y-%m-%d %H:%M:%S')} 服务端退出,2 秒后重启...") _time.sleep(2) else: run() elif cmd == "knowledge": if len(sys.argv) > 2 and sys.argv[2] == "build": from app.services.knowledge import build_knowledge build_knowledge(cfg) elif cmd == "rebuild-index": from app.services.indexer import incremental_update incremental_update(cfg) elif cmd == "stats": from app.services.indexer import show_stats show_stats(cfg) elif cmd == "backup": remote = len(sys.argv) > 2 and sys.argv[2] == "--remote" from app.services.backup import manual_backup manual_backup(cfg, remote=remote) elif cmd == "token": action = sys.argv[2] if len(sys.argv) > 2 else "show" if action == "regenerate": new_token = cfg.regenerate_token() print(f"新 Token: {new_token}") else: print(f"Token: {cfg.token}") else: print(f"未知命令: {cmd}") sys.exit(1) if __name__ == "__main__": main()