main.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. """FastAPI 应用入口"""
  2. import os
  3. import sys
  4. from contextlib import asynccontextmanager
  5. from pathlib import Path
  6. from fastapi import FastAPI, Request
  7. from fastapi.middleware.cors import CORSMiddleware
  8. from fastapi.responses import JSONResponse
  9. from fastapi.staticfiles import StaticFiles
  10. from app.config import get_config, Config
  11. from app.database import init_db, get_connection
  12. @asynccontextmanager
  13. async def lifespan(app: FastAPI):
  14. """应用生命周期:启动时初始化,关闭时清理"""
  15. # 启动
  16. config = app.state.config
  17. config.data_dir.mkdir(parents=True, exist_ok=True)
  18. config.problems_dir.mkdir(parents=True, exist_ok=True)
  19. config.papers_dir.mkdir(parents=True, exist_ok=True)
  20. config.backups_dir.mkdir(parents=True, exist_ok=True)
  21. init_db(config.db_path)
  22. print(f"✓ 数据库已初始化: {config.db_path}")
  23. print(f"✓ API Token: {config.token}")
  24. print(f"✓ 服务启动在 http://{config.host}:{config.port}")
  25. yield
  26. def create_app(config: Config) -> FastAPI:
  27. app = FastAPI(
  28. title="题库PLUS",
  29. description="C/S 架构数学题库管理系统",
  30. version="1.0.0",
  31. lifespan=lifespan,
  32. )
  33. app.state.config = config
  34. # CORS
  35. app.add_middleware(
  36. CORSMiddleware,
  37. allow_origins=["*"],
  38. allow_credentials=True,
  39. allow_methods=["*"],
  40. allow_headers=["*"],
  41. )
  42. # 全局异常处理
  43. @app.exception_handler(Exception)
  44. async def global_exception_handler(request: Request, exc: Exception):
  45. import traceback
  46. traceback.print_exc()
  47. return JSONResponse(
  48. status_code=500,
  49. content={"error": {"code": "internal_error", "message": str(exc)}},
  50. )
  51. # 注册路由
  52. from app.routers import auth, problems, import_, export_, knowledge, carts, admin
  53. app.include_router(auth.router, prefix="/api", tags=["auth"])
  54. app.include_router(problems.router, prefix="/api", tags=["problems"])
  55. app.include_router(import_.router, prefix="/api", tags=["import"])
  56. app.include_router(export_.router, prefix="/api", tags=["export"])
  57. app.include_router(knowledge.router, prefix="/api", tags=["knowledge"])
  58. app.include_router(carts.router, prefix="/api", tags=["carts"])
  59. app.include_router(admin.router, prefix="/api", tags=["admin"])
  60. # Flutter Web 静态文件
  61. web_dir = Path("web")
  62. if web_dir.exists() and any(web_dir.iterdir()):
  63. app.mount("/", StaticFiles(directory=str(web_dir), html=True), name="web")
  64. return app
  65. def main():
  66. """CLI 入口"""
  67. if len(sys.argv) < 2:
  68. print("用法: tiku-server <命令> [参数]")
  69. print()
  70. print("命令:")
  71. print(" serve 启动服务")
  72. print(" serve --restart 启动服务(崩溃/更新后自动重启)")
  73. print(" knowledge build 重建知识点编码")
  74. print(" rebuild-index 手动增量索引")
  75. print(" stats 终端统计")
  76. print(" backup 手动备份")
  77. print(" backup --remote 推送到 WebDAV")
  78. print(" token 显示/重新生成 API token")
  79. print(" token regenerate 重新生成 API token")
  80. sys.exit(1)
  81. cmd = sys.argv[1]
  82. config_path = os.environ.get("TIKU_CONFIG", "config.yaml")
  83. cfg = get_config(config_path)
  84. if cmd == "serve":
  85. import uvicorn
  86. auto_restart = len(sys.argv) > 2 and sys.argv[2] == "--restart"
  87. def run():
  88. uvicorn.run(
  89. create_app(cfg),
  90. host=cfg.host,
  91. port=cfg.port,
  92. log_level="info",
  93. )
  94. if auto_restart:
  95. import time as _time
  96. while True:
  97. print(f"{_time.strftime('%Y-%m-%d %H:%M:%S')} 启动服务端...")
  98. run()
  99. print(f"{_time.strftime('%Y-%m-%d %H:%M:%S')} 服务端退出,2 秒后重启...")
  100. _time.sleep(2)
  101. else:
  102. run()
  103. elif cmd == "knowledge":
  104. if len(sys.argv) > 2 and sys.argv[2] == "build":
  105. from app.services.knowledge import build_knowledge
  106. build_knowledge(cfg)
  107. elif cmd == "rebuild-index":
  108. from app.services.indexer import incremental_update
  109. incremental_update(cfg)
  110. elif cmd == "stats":
  111. from app.services.indexer import show_stats
  112. show_stats(cfg)
  113. elif cmd == "backup":
  114. remote = len(sys.argv) > 2 and sys.argv[2] == "--remote"
  115. from app.services.backup import manual_backup
  116. manual_backup(cfg, remote=remote)
  117. elif cmd == "token":
  118. action = sys.argv[2] if len(sys.argv) > 2 else "show"
  119. if action == "regenerate":
  120. new_token = cfg.regenerate_token()
  121. print(f"新 Token: {new_token}")
  122. else:
  123. print(f"Token: {cfg.token}")
  124. else:
  125. print(f"未知命令: {cmd}")
  126. sys.exit(1)
  127. if __name__ == "__main__":
  128. main()