main.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. from fastapi import FastAPI, Request
  2. from fastapi.staticfiles import StaticFiles
  3. from fastapi.middleware.cors import CORSMiddleware
  4. from fastapi.responses import HTMLResponse, FileResponse
  5. from utils.config import settings
  6. from utils.auth_middleware import auth_middleware
  7. from utils.logger import logger
  8. from routers import api_router
  9. import uvicorn
  10. import time
  11. from pathlib import Path
  12. # 创建FastAPI应用
  13. app = FastAPI(
  14. title=settings.app.name,
  15. debug=settings.app.debug
  16. )
  17. # 配置CORS(必须先配置)
  18. app.add_middleware(
  19. CORSMiddleware,
  20. allow_origins=["*"],
  21. allow_credentials=True,
  22. allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
  23. allow_headers=["Origin", "Authorization", "Access-Control-Allow-Origin",
  24. "Access-Control-Allow-Headers", "Content-Type", "token"],
  25. expose_headers=["Content-Length", "Access-Control-Allow-Origin",
  26. "Access-Control-Allow-Headers", "Content-Type"]
  27. )
  28. # 添加请求日志和认证中间件
  29. @app.middleware("http")
  30. async def combined_middleware(request: Request, call_next):
  31. """组合中间件:日志 + 认证"""
  32. from fastapi.responses import JSONResponse
  33. from utils.token import verify_token
  34. start_time = time.time()
  35. path = request.url.path
  36. # 先打印,确认中间件被执行
  37. print(f"[DEBUG] 中间件执行 - 路径: {path}")
  38. logger.info(f"[中间件] 开始处理请求: {path}")
  39. # 白名单路径(不需要认证)
  40. whitelist_paths = ["/health", "/docs", "/redoc", "/openapi.json", "/static/", "/assets/", "/apiv1/auth/local_login", "/apiv1/auth/register"]
  41. # 检查是否在白名单中(精确匹配或以/结尾的前缀匹配)
  42. is_whitelist = path == "/" or any(path.startswith(wp) for wp in whitelist_paths)
  43. print(f"[DEBUG] 是否白名单: {is_whitelist}")
  44. if is_whitelist:
  45. print(f"[DEBUG] 白名单路径,跳过认证")
  46. request.state.user = None
  47. response = await call_next(request)
  48. else:
  49. # 获取Token
  50. auth_header = (request.headers.get("Authorization") or "").strip()
  51. token = request.headers.get("token") or request.headers.get("Token") or auth_header
  52. if auth_header.lower().startswith("bearer "):
  53. token = auth_header[7:].strip()
  54. print(f"[DEBUG] Token: {token[:20] if token else 'None'}...")
  55. logger.info(f"认证中间件 - 路径: {path}")
  56. logger.info(f"认证中间件 - Token (前20字符): {token[:20] if token else 'None'}...")
  57. if not token:
  58. print(f"[DEBUG] 未提供Token")
  59. logger.warning("认证中间件 - 未提供Token")
  60. response = JSONResponse(
  61. status_code=401,
  62. content={"statusCode": 401, "msg": "未提供认证Token"}
  63. )
  64. else:
  65. # 验证Token
  66. print(f"[DEBUG] 开始验证Token")
  67. logger.info("认证中间件 - 开始验证Token")
  68. try:
  69. user_info = await verify_token(token)
  70. except Exception as e:
  71. logger.error(f"Token验证异常: {e}")
  72. user_info = None
  73. print(f"[DEBUG] 验证结果: {user_info}")
  74. if not user_info:
  75. print(f"[DEBUG] Token验证失败")
  76. logger.error("认证中间件 - Token验证失败,返回401")
  77. response = JSONResponse(
  78. status_code=401,
  79. content={"statusCode": 401, "msg": "Token验证失败"}
  80. )
  81. else:
  82. print(f"[DEBUG] Token验证成功: {user_info.username}")
  83. logger.info(f"认证中间件 - Token验证成功,用户: {user_info.username} ({user_info.account})")
  84. request.state.user = user_info
  85. response = await call_next(request)
  86. # 记录日志
  87. process_time = time.time() - start_time
  88. print(f"[DEBUG] 请求完成 - 状态码: {response.status_code}")
  89. logger.info(f"请求完成: {request.method} {path} - 状态码: {response.status_code} - 耗时: {process_time:.3f}s")
  90. return response
  91. # 注册路由
  92. app.include_router(api_router)
  93. # 单独注册报告兼容路由(避免双重前缀)
  94. from routers.report_compat import router as report_compat_router
  95. app.include_router(report_compat_router)
  96. # 创建静态文件目录
  97. Path("static").mkdir(exist_ok=True)
  98. Path("assets").mkdir(exist_ok=True)
  99. # 挂载静态文件
  100. app.mount("/static", StaticFiles(directory="static"), name="static")
  101. app.mount("/assets", StaticFiles(directory="assets"), name="assets")
  102. @app.get("/", response_class=HTMLResponse)
  103. async def root():
  104. """根路径 - 欢迎页面"""
  105. frontend_index = Path("views") / "index.html"
  106. if frontend_index.is_file():
  107. return FileResponse(frontend_index, media_type="text/html")
  108. html_content = """
  109. <!DOCTYPE html>
  110. <html lang="zh-CN">
  111. <head>
  112. <meta charset="UTF-8">
  113. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  114. <title>Shudao Chat API</title>
  115. <style>
  116. * { margin: 0; padding: 0; box-sizing: border-box; }
  117. body {
  118. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  119. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  120. min-height: 100vh;
  121. display: flex;
  122. align-items: center;
  123. justify-content: center;
  124. padding: 20px;
  125. }
  126. .container {
  127. background: white;
  128. border-radius: 20px;
  129. box-shadow: 0 20px 60px rgba(0,0,0,0.3);
  130. padding: 60px 40px;
  131. max-width: 800px;
  132. width: 100%;
  133. }
  134. h1 {
  135. color: #667eea;
  136. font-size: 3em;
  137. margin-bottom: 20px;
  138. text-align: center;
  139. }
  140. .subtitle {
  141. color: #666;
  142. font-size: 1.2em;
  143. text-align: center;
  144. margin-bottom: 40px;
  145. }
  146. .info-grid {
  147. display: grid;
  148. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  149. gap: 20px;
  150. margin-bottom: 40px;
  151. }
  152. .info-card {
  153. background: #f8f9fa;
  154. padding: 20px;
  155. border-radius: 10px;
  156. text-align: center;
  157. }
  158. .info-card h3 {
  159. color: #667eea;
  160. font-size: 1.1em;
  161. margin-bottom: 10px;
  162. }
  163. .info-card p {
  164. color: #666;
  165. font-size: 0.95em;
  166. }
  167. .links {
  168. display: flex;
  169. gap: 15px;
  170. justify-content: center;
  171. flex-wrap: wrap;
  172. }
  173. .btn {
  174. display: inline-block;
  175. padding: 12px 30px;
  176. background: #667eea;
  177. color: white;
  178. text-decoration: none;
  179. border-radius: 8px;
  180. font-weight: 500;
  181. transition: all 0.3s;
  182. }
  183. .btn:hover {
  184. background: #764ba2;
  185. transform: translateY(-2px);
  186. box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
  187. }
  188. .btn-secondary {
  189. background: #48bb78;
  190. }
  191. .btn-secondary:hover {
  192. background: #38a169;
  193. }
  194. .status {
  195. display: inline-block;
  196. padding: 5px 15px;
  197. background: #48bb78;
  198. color: white;
  199. border-radius: 20px;
  200. font-size: 0.9em;
  201. margin-bottom: 20px;
  202. }
  203. .features {
  204. margin-top: 40px;
  205. padding-top: 40px;
  206. border-top: 2px solid #f0f0f0;
  207. }
  208. .features h2 {
  209. color: #333;
  210. margin-bottom: 20px;
  211. text-align: center;
  212. }
  213. .feature-list {
  214. display: grid;
  215. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  216. gap: 15px;
  217. }
  218. .feature-item {
  219. display: flex;
  220. align-items: center;
  221. padding: 15px;
  222. background: #f8f9fa;
  223. border-radius: 8px;
  224. }
  225. .feature-icon {
  226. font-size: 1.5em;
  227. margin-right: 15px;
  228. }
  229. .feature-text {
  230. color: #666;
  231. font-size: 0.95em;
  232. }
  233. </style>
  234. </head>
  235. <body>
  236. <div class="container">
  237. <div style="text-align: center;">
  238. <span class="status">🟢 服务运行中</span>
  239. </div>
  240. <h1>🚀 Shudao Chat API</h1>
  241. <p class="subtitle">基于 FastAPI 的现代化 AI 聊天服务</p>
  242. <div class="info-grid">
  243. <div class="info-card">
  244. <h3>📦 版本</h3>
  245. <p>v1.0.0</p>
  246. </div>
  247. <div class="info-card">
  248. <h3>⚡ 框架</h3>
  249. <p>FastAPI</p>
  250. </div>
  251. <div class="info-card">
  252. <h3>🗄️ 数据库</h3>
  253. <p>MySQL + SQLAlchemy</p>
  254. </div>
  255. <div class="info-card">
  256. <h3>🔐 认证</h3>
  257. <p>Token Based</p>
  258. </div>
  259. </div>
  260. <div class="links">
  261. <a href="/docs" class="btn">📚 API 文档 (Swagger)</a>
  262. <a href="/redoc" class="btn btn-secondary">📖 API 文档 (ReDoc)</a>
  263. <a href="/health" class="btn">💚 健康检查</a>
  264. </div>
  265. <div class="features">
  266. <h2>✨ 核心功能</h2>
  267. <div class="feature-list">
  268. <div class="feature-item">
  269. <span class="feature-icon">💬</span>
  270. <span class="feature-text">AI 智能对话</span>
  271. </div>
  272. <div class="feature-item">
  273. <span class="feature-icon">📝</span>
  274. <span class="feature-text">历史记录管理</span>
  275. </div>
  276. <div class="feature-item">
  277. <span class="feature-icon">🎯</span>
  278. <span class="feature-text">场景识别</span>
  279. </div>
  280. <div class="feature-item">
  281. <span class="feature-icon">📊</span>
  282. <span class="feature-text">埋点统计</span>
  283. </div>
  284. <div class="feature-item">
  285. <span class="feature-icon">🔒</span>
  286. <span class="feature-text">安全认证</span>
  287. </div>
  288. <div class="feature-item">
  289. <span class="feature-icon">🌐</span>
  290. <span class="feature-text">CORS 支持</span>
  291. </div>
  292. </div>
  293. </div>
  294. </div>
  295. </body>
  296. </html>
  297. """
  298. return HTMLResponse(content=html_content)
  299. @app.get("/health")
  300. async def health_check():
  301. """健康检查"""
  302. return {"status": "ok"}
  303. if __name__ == "__main__":
  304. logger.info("=" * 60)
  305. logger.info("🚀 Shudao Chat API 启动中...")
  306. logger.info(f"📍 服务地址: http://{settings.app.host}:{settings.app.port}")
  307. logger.info(f"📚 API 文档: http://{settings.app.host}:{settings.app.port}/docs")
  308. logger.info(f"🗄️ 数据库: {settings.database.host}:{settings.database.port}/{settings.database.database}")
  309. logger.info(f"🔧 调试模式: {'开启' if settings.app.debug else '关闭'}")
  310. logger.info("=" * 60)
  311. uvicorn.run(
  312. "main:app",
  313. host=settings.app.host,
  314. port=settings.app.port,
  315. reload=settings.app.debug,
  316. log_level="info"
  317. )