main.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. """
  2. 智创空间后端服务入口
  3. """
  4. import os
  5. import time
  6. import json
  7. import logging
  8. from logging.handlers import RotatingFileHandler
  9. from datetime import datetime
  10. from contextlib import asynccontextmanager
  11. from fastapi import FastAPI, Request
  12. from fastapi.middleware.cors import CORSMiddleware
  13. from starlette.middleware.base import BaseHTTPMiddleware
  14. from starlette.responses import Response
  15. from app.routers import (
  16. model_router, llm_router, auth_router, user_router, conversation_router,
  17. image_router, oss_router, audio_router, billing_router, invoice_router,
  18. video_router, audio_v2_router, toolbox_router, image_translation_router,
  19. ocr_router, tingwu_router, zhiwen_router, translation_router, research_router,
  20. local_model_router, platform_api_key_router, openai_compat_router, platform_stats_router,
  21. user_local_model_permission_router, password_strength_router
  22. )
  23. from app.core.async_logger import async_log_queue
  24. from app.core.redis import redis_manager
  25. from app.middleware.rate_limit_middleware import RateLimitMiddleware
  26. from app.routers.photo_answer_router import router as photo_answer_router
  27. from app.routers.multimodal_translation_router import router as multimodal_translation_router
  28. from app.routers.admin_auth_router import router as admin_auth_router
  29. from app.routers.admin_user_router import router as admin_user_router
  30. from app.routers.admin_model_router import router as admin_model_router
  31. from app.routers.admin_log_router import router as admin_log_router
  32. from app.routers.admin_stats_router import router as admin_stats_router
  33. from app.routers.admin_order_router import router as admin_order_router
  34. from app.routers.admin_bill_router import router as admin_bill_router
  35. from app.routers.admin_config_router import router as admin_config_router
  36. from app.routers.admin_review_router import router as admin_review_router
  37. from app.routers.admin_consumption_router import router as admin_consumption_router
  38. from app.routers.admin_invoice_router import router as admin_invoice_router
  39. from app.routers.admin_local_model_router import router as admin_local_model_router
  40. from app.routers.sso_router import router as sso_router
  41. from app.routers.alipay_router import router as alipay_router
  42. from app.routers.admin_local_config_router import router as admin_local_config_router
  43. from app.routers.enterprise_auth_router import router as enterprise_auth_router
  44. from app.routers.enterprise_router import router as enterprise_router
  45. from app.routers.super_admin_router import router as super_admin_router
  46. from app.routers.enterprise_local_config_router import router as enterprise_local_config_router
  47. from app.middleware import register_exception_handlers
  48. from app.middleware.tenant_middleware import TenantMiddleware
  49. from app.database import engine, SessionLocal
  50. from app.services.user_service import UserService
  51. # ==================== 日志配置 ====================
  52. # 创建 logs 目录
  53. os.makedirs('logs', exist_ok=True)
  54. # 配置日志格式
  55. log_formatter = logging.Formatter(
  56. '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  57. )
  58. # 配置根日志记录器
  59. root_logger = logging.getLogger()
  60. root_logger.setLevel(logging.INFO)
  61. # 清除已有的 handlers,避免重复(reload 模式下会重复加载)
  62. if root_logger.handlers:
  63. root_logger.handlers.clear()
  64. # 控制台处理器
  65. console_handler = logging.StreamHandler()
  66. console_handler.setFormatter(log_formatter)
  67. root_logger.addHandler(console_handler)
  68. # 文件处理器(所有日志)
  69. file_handler = RotatingFileHandler(
  70. 'logs/app.log',
  71. maxBytes=10*1024*1024, # 10MB
  72. backupCount=5,
  73. encoding='utf-8'
  74. )
  75. file_handler.setFormatter(log_formatter)
  76. root_logger.addHandler(file_handler)
  77. # 错误日志文件处理器
  78. error_handler = RotatingFileHandler(
  79. 'logs/error.log',
  80. maxBytes=10*1024*1024, # 10MB
  81. backupCount=5,
  82. encoding='utf-8'
  83. )
  84. error_handler.setLevel(logging.ERROR)
  85. error_handler.setFormatter(log_formatter)
  86. root_logger.addHandler(error_handler)
  87. logger = logging.getLogger(__name__)
  88. logger.info("Logging configured: logs/app.log, logs/error.log")
  89. class RequestLogMiddleware(BaseHTTPMiddleware):
  90. """全局请求日志中间件"""
  91. async def dispatch(self, request: Request, call_next):
  92. start_time = time.time()
  93. # 尝试获取用户ID和用户名
  94. user_id = None
  95. username = None
  96. auth_header = request.headers.get("Authorization")
  97. if auth_header and auth_header.startswith("Bearer "):
  98. try:
  99. from app.services.auth_service import AuthService
  100. from app.services.admin_auth_service import AdminAuthService
  101. token = auth_header[7:]
  102. # 先尝试普通用户token
  103. try:
  104. payload = AuthService.verify_token(token)
  105. user_id = payload.get("user_id")
  106. except:
  107. # 再尝试管理员token
  108. try:
  109. payload = AdminAuthService.verify_token(token)
  110. user_id = f"admin_{payload.get('admin_id')}"
  111. except:
  112. pass
  113. except:
  114. pass
  115. response = await call_next(request)
  116. duration_ms = int((time.time() - start_time) * 1000)
  117. # 记录到控制台
  118. log_data = {
  119. "timestamp": datetime.now().isoformat(),
  120. "method": request.method,
  121. "path": str(request.url.path),
  122. "query_params": str(request.query_params) if request.query_params else None,
  123. "user_id": user_id or "anonymous",
  124. "status_code": response.status_code,
  125. "duration_ms": duration_ms
  126. }
  127. if response.status_code >= 400:
  128. logger.warning(f"Request: {json.dumps(log_data, ensure_ascii=False)}")
  129. else:
  130. logger.info(f"Request: {json.dumps(log_data, ensure_ascii=False)}")
  131. # 记录到异步队列(非阻塞,需求 6.1, 6.4)
  132. if user_id and not request.url.path.startswith(("/health", "/static", "/exports")):
  133. async_log_queue.enqueue({
  134. "user_id": user_id,
  135. "api_path": str(request.url.path),
  136. "method": request.method,
  137. "status_code": response.status_code,
  138. "duration_ms": duration_ms,
  139. "request_params": dict(request.query_params) if request.query_params else None,
  140. "request_ip": request.client.host if request.client else None
  141. })
  142. return response
  143. def init_admin_user():
  144. """初始化管理员用户"""
  145. db = SessionLocal()
  146. try:
  147. user_service = UserService(db)
  148. user_service.init_admin_user()
  149. logger.info("管理员用户初始化完成")
  150. except Exception as e:
  151. logger.error(f"管理员用户初始化失败: {e}")
  152. finally:
  153. db.close()
  154. @asynccontextmanager
  155. async def lifespan(app: FastAPI):
  156. """应用生命周期管理"""
  157. logger.info("=" * 50)
  158. logger.info("智创空间后端服务启动中...")
  159. logger.info(f"数据库连接: {engine.url}")
  160. try:
  161. with engine.connect() as conn:
  162. logger.info("数据库连接成功")
  163. init_admin_user()
  164. # 初始化 Redis 连接(需求 4.1)
  165. redis_connected = await redis_manager.connect()
  166. if redis_connected:
  167. logger.info("Redis 连接已建立")
  168. else:
  169. logger.warning("Redis 连接失败,系统将以降级模式运行(无缓存、无分布式限流)")
  170. # 启动异步日志队列(需求 6.1)
  171. from app.core.async_database import AsyncSessionLocal
  172. await async_log_queue.start(AsyncSessionLocal)
  173. logger.info("异步日志队列已启动")
  174. # 启动定时任务
  175. from apscheduler.schedulers.background import BackgroundScheduler
  176. # TODO: hourly_deduction_task 模块已移除,需要重新实现或确认是否需要
  177. # from app.services.hourly_deduction_task import run_hourly_deduction
  178. scheduler = BackgroundScheduler()
  179. # 每小时整点执行定时扣减任务(已禁用)
  180. # scheduler.add_job(
  181. # run_hourly_deduction,
  182. # 'cron',
  183. # hour='*',
  184. # minute=0,
  185. # id='hourly_deduction',
  186. # name='每小时余额扣减任务'
  187. # )
  188. # 异步消费同步任务:定期扫描 balance_log 并回填到 user_consumption
  189. try:
  190. from app.services.consumption_sync_service import run_sync_job
  191. scheduler.add_job(
  192. run_sync_job,
  193. 'interval',
  194. seconds=60,
  195. args=[SessionLocal],
  196. id='consumption_sync',
  197. name='消费同步任务'
  198. )
  199. logger.info("定时任务已启动:消费同步任务每60秒执行一次")
  200. except Exception as _:
  201. logger.exception("注册消费同步任务失败")
  202. # 爬虫数据同步任务:每天凌晨3点执行
  203. try:
  204. import asyncio
  205. from app.services.crawler_sync_service import sync_from_crawler
  206. def run_crawler_sync():
  207. db = SessionLocal()
  208. try:
  209. asyncio.run(sync_from_crawler(db))
  210. except Exception as e:
  211. logger.error(f"爬虫同步任务异常: {e}")
  212. finally:
  213. db.close()
  214. scheduler.add_job(
  215. run_crawler_sync,
  216. 'cron',
  217. hour=3,
  218. minute=0,
  219. id='crawler_sync',
  220. name='爬虫数据同步任务'
  221. )
  222. logger.info("定时任务已启动:爬虫数据同步任务每天凌晨3点执行")
  223. # 启动时立即触发一次同步
  224. import threading
  225. threading.Thread(target=run_crawler_sync, daemon=True, name="crawler_sync_startup").start()
  226. logger.info("已触发启动时爬虫数据同步")
  227. except Exception as _:
  228. logger.exception("注册爬虫同步任务失败")
  229. # 企业余额预警短信定时检查(每小时整点)
  230. try:
  231. import asyncio as _asyncio
  232. import concurrent.futures as _futures
  233. def run_balance_warn():
  234. db = SessionLocal()
  235. try:
  236. from app.services.sms_service import tenant_warn_sms
  237. with _futures.ThreadPoolExecutor() as pool:
  238. pool.submit(_asyncio.run, tenant_warn_sms.check_and_warn(db)).result()
  239. except Exception as e:
  240. logger.error(f"余额预警短信任务异常: {e}")
  241. finally:
  242. db.close()
  243. scheduler.add_job(
  244. run_balance_warn,
  245. 'cron',
  246. hour='*',
  247. minute=30,
  248. id='balance_warn_sms',
  249. name='企业余额预警短信任务'
  250. )
  251. logger.info("定时任务已启动:企业余额预警短信任务每小时30分执行")
  252. except Exception as _:
  253. logger.exception("注册余额预警短信任务失败")
  254. scheduler.start()
  255. except Exception as e:
  256. logger.error(f"数据库连接失败: {e}")
  257. logger.info("=" * 50)
  258. yield
  259. # 停止异步日志队列(需求 6.1)
  260. logger.info("正在停止异步日志队列...")
  261. await async_log_queue.stop()
  262. # 关闭 Redis 连接(需求 4.1)
  263. logger.info("正在关闭 Redis 连接...")
  264. await redis_manager.close()
  265. logger.info("服务关闭")
  266. app = FastAPI(
  267. title="智创空间API",
  268. description="智创空间后端服务,包含模型广场等模块",
  269. version="1.0.0",
  270. lifespan=lifespan,
  271. docs_url="/docs",
  272. redoc_url="/redoc"
  273. )
  274. # 添加安全中间件
  275. from app.middleware.security_middleware import SecurityMiddleware
  276. # 添加全局请求日志中间件
  277. app.add_middleware(RequestLogMiddleware)
  278. # 添加租户上下文中间件
  279. app.add_middleware(TenantMiddleware)
  280. # 添加安全中间件(在CORS之前添加)
  281. app.add_middleware(SecurityMiddleware)
  282. # 添加限流中间件(需求 5.1)
  283. app.add_middleware(RateLimitMiddleware)
  284. app.add_middleware(
  285. CORSMiddleware,
  286. allow_origins=["*"],
  287. allow_credentials=True,
  288. allow_methods=["*"],
  289. allow_headers=["*"],
  290. )
  291. register_exception_handlers(app)
  292. # 注意:local_model_router 必须在 model_router 之前注册,
  293. # 因为 model_router 有 /{model_id} 路由会捕获 /local
  294. app.include_router(local_model_router)
  295. app.include_router(model_router)
  296. app.include_router(llm_router)
  297. app.include_router(auth_router)
  298. app.include_router(user_router)
  299. app.include_router(conversation_router)
  300. app.include_router(image_router)
  301. app.include_router(oss_router)
  302. app.include_router(audio_router)
  303. app.include_router(billing_router)
  304. app.include_router(invoice_router)
  305. app.include_router(video_router)
  306. app.include_router(audio_v2_router)
  307. app.include_router(toolbox_router)
  308. app.include_router(image_translation_router)
  309. app.include_router(ocr_router)
  310. app.include_router(tingwu_router)
  311. app.include_router(zhiwen_router)
  312. app.include_router(translation_router)
  313. app.include_router(research_router)
  314. app.include_router(photo_answer_router)
  315. app.include_router(multimodal_translation_router)
  316. app.include_router(platform_api_key_router)
  317. app.include_router(openai_compat_router)
  318. app.include_router(platform_stats_router)
  319. app.include_router(user_local_model_permission_router)
  320. app.include_router(password_strength_router)
  321. # 管理后台路由
  322. app.include_router(admin_auth_router)
  323. app.include_router(admin_user_router)
  324. app.include_router(admin_model_router)
  325. app.include_router(admin_log_router)
  326. app.include_router(admin_stats_router)
  327. app.include_router(admin_order_router)
  328. app.include_router(admin_bill_router)
  329. app.include_router(admin_config_router)
  330. app.include_router(admin_review_router)
  331. app.include_router(admin_consumption_router)
  332. app.include_router(admin_invoice_router)
  333. app.include_router(admin_local_model_router)
  334. app.include_router(sso_router)
  335. app.include_router(alipay_router)
  336. app.include_router(admin_local_config_router)
  337. # 企业管理员路由
  338. app.include_router(enterprise_auth_router)
  339. app.include_router(enterprise_router)
  340. from app.routers.enterprise_admin_user_router import router as enterprise_admin_user_router
  341. from app.routers.enterprise_admin_stats_router import router as enterprise_admin_stats_router
  342. from app.routers.enterprise_admin_proxy_router import router as enterprise_admin_proxy_router
  343. from app.routers.sms_router import router as sms_router
  344. app.include_router(enterprise_admin_user_router)
  345. app.include_router(enterprise_admin_stats_router)
  346. app.include_router(enterprise_admin_proxy_router)
  347. app.include_router(sms_router)
  348. # 超级管理后台路由
  349. app.include_router(super_admin_router)
  350. app.include_router(enterprise_local_config_router)
  351. # 租户申请公开路由
  352. from app.routers.tenant_apply_router import router as tenant_apply_router
  353. app.include_router(tenant_apply_router)
  354. # 公开品牌配置接口(无需登录,前端用于显示 logo/名称)
  355. @app.get("/api/public/branding")
  356. async def get_public_branding(request: Request):
  357. """返回当前租户的品牌配置(system_name, system_logo)"""
  358. from app.models.config import SystemConfig
  359. from app.database import SessionLocal
  360. import json as _json
  361. tenant_id = getattr(request.state, 'tenant_id', None)
  362. db = SessionLocal()
  363. try:
  364. def _get(key: str, default: str) -> str:
  365. row = db.query(SystemConfig).filter(
  366. SystemConfig.tenant_id == tenant_id,
  367. SystemConfig.config_key == key
  368. ).first()
  369. if row:
  370. try:
  371. return _json.loads(row.config_value)
  372. except Exception:
  373. return row.config_value
  374. # fallback 到全局配置
  375. row = db.query(SystemConfig).filter(
  376. SystemConfig.tenant_id == None,
  377. SystemConfig.config_key == key
  378. ).first()
  379. if row:
  380. try:
  381. return _json.loads(row.config_value)
  382. except Exception:
  383. return row.config_value
  384. return default
  385. return {
  386. "system_name": _get("system_name", "智创空间"),
  387. "system_logo": _get("system_logo", ""),
  388. "icp_number": _get("icp_number", ""),
  389. }
  390. finally:
  391. db.close()
  392. @app.get("/health")
  393. async def health_check():
  394. """基本健康检查
  395. 返回系统整体健康状态(healthy/degraded/unhealthy)。
  396. 需求引用: 8.1, 8.2, 8.3, 8.5
  397. """
  398. from app.services.health_service import health_service
  399. overall = await health_service.get_overall_health()
  400. return {"status": overall["status"]}
  401. @app.get("/health/detailed")
  402. async def health_check_detailed():
  403. """详细健康检查
  404. 返回所有组件的详细状态信息,包括:
  405. - 数据库连接状态和连接池使用情况
  406. - Redis 连接状态和内存使用情况
  407. - 异步日志队列状态
  408. 需求引用: 8.1, 8.2, 8.3, 8.4, 8.5
  409. """
  410. from app.services.health_service import health_service
  411. return await health_service.get_overall_health()
  412. if __name__ == "__main__":
  413. import uvicorn
  414. host = os.getenv("APP_HOST", "0.0.0.0")
  415. port = int(os.getenv("APP_PORT", "8010"))
  416. debug = os.getenv("DEBUG", "False").lower() == "true"
  417. logger.info(f"启动开发服务器: http://{host}:{port}")
  418. # 配置 reload 参数
  419. reload_config = {}
  420. if debug:
  421. # 只监控 app 目录,避免监控 logs
  422. reload_config = {
  423. "reload": True,
  424. "reload_dirs": ["app"], # 只监控 app 目录
  425. "reload_includes": ["*.py"], # 只监控 Python 文件
  426. }
  427. uvicorn.run(
  428. "main:app",
  429. host=host,
  430. port=port,
  431. log_level="info",
  432. **reload_config
  433. )