You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/app/main.py

224 lines
6.5 KiB

"""
数据缓冲平台 - FastAPI 主入口
"""
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, RedirectResponse
from app.database import engine, Base
from app.user_models import Base as UserBase
from app.config import HOST, PORT, LOG_LEVEL
from app.api import data, tasks, config, futures_analysis, ai_config, auth, review_plan, trade_review
from app.services.scheduler import start_scheduler, stop_scheduler
# 配置日志
logging.basicConfig(
level=getattr(logging, LOG_LEVEL.upper(), logging.INFO),
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
# 启动时:建表 + 启动调度器
logger.info("创建数据库表...")
Base.metadata.create_all(bind=engine)
UserBase.metadata.create_all(bind=engine)
from app.analysis_db import init_analysis_db
init_analysis_db()
logger.info("期货智析数据库初始化完成")
# 初始化 Redis 和 MySQL
from app.redis_client import init_redis
from app.mysql_database import init_mysql
from app.storage_manager import get_storage_manager
redis_client = None
mysql_engine = None
try:
redis_client = init_redis()
except Exception:
redis_client = None
try:
mysql_engine = init_mysql()
except Exception:
mysql_engine = None
redis_ok = redis_client is not None
mysql_ok = mysql_engine is not None
if redis_ok and mysql_ok:
logger.info("存储模式: Redis + MySQL")
elif mysql_ok:
logger.warning("存储模式: MySQL (Redis 不可用)")
else:
logger.error("存储模式: SQLite (Redis 和 MySQL 均不可用)")
# 将初始化后的客户端注入 StorageManager 全局单例
storage_manager = get_storage_manager()
storage_manager.initialize(redis_client, mysql_engine)
if mysql_ok and mysql_engine is not None:
from app.models import Base as MarketBase, MarketData, SymbolTimestamp
# 在 MySQL 中创建行情相关表
MarketBase.metadata.create_all(bind=mysql_engine, tables=[MarketData.__table__, SymbolTimestamp.__table__])
logger.info("行情数据 MySQL 表结构初始化完成")
from app.migration import migrate_sqlite_to_mysql
migrate_sqlite_to_mysql()
# 在 MySQL 中创建 analysis 相关表
from app.analysis_db import init_analysis_mysql
init_analysis_mysql(mysql_engine)
# 迁移 SQLite analysis 数据到 MySQL
from app.analysis_migration import migrate_analysis_sqlite_to_mysql
migrate_analysis_sqlite_to_mysql()
# 在 MySQL 中创建配置表
from app.models import AppConfig
MarketBase.metadata.create_all(bind=mysql_engine, tables=[AppConfig.__table__])
logger.info("配置表 MySQL 表结构初始化完成")
# 迁移 JSON 配置到 MySQL
from app.config_migration import migrate_configs_to_mysql
migrate_configs_to_mysql()
# 创建默认管理员账户
from app.database import SessionLocal
from app import auth_service
db = SessionLocal()
try:
auth_service.create_default_admin(db)
finally:
db.close()
logger.info("启动定时调度器...")
start_scheduler()
# 恢复已启用的任务
from app.services.cache import list_tasks
from app.services.scheduler import add_job
db = SessionLocal()
try:
enabled_tasks = [t for t in list_tasks(db) if t.enabled]
for t in enabled_tasks:
add_job(t.id, t.interval_seconds)
logger.info(f"恢复定时任务: {t.symbol} (每 {t.interval_seconds}s)")
finally:
db.close()
logger.info(f"数据缓冲平台已启动 http://{HOST}:{PORT}")
yield
# 关闭时
logger.info("停止调度器...")
stop_scheduler()
app = FastAPI(
title="数据缓冲平台",
description="期货/股票行情数据缓存与定时采集平台",
version="1.0.0",
lifespan=lifespan,
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 静态文件服务(禁用缓存以确保前端更新立即生效)
STATIC_DIR = Path(__file__).resolve().parent / "static"
STATIC_DIR.mkdir(parents=True, exist_ok=True)
app.mount("/static", StaticFiles(directory=str(STATIC_DIR), html=True), name="static")
# 登录页面(无需认证)
@app.get("/login")
def login_page():
"""登录页面"""
return FileResponse(str(STATIC_DIR / "login.html"))
# 角色选择页面(需要管理员权限)
@app.get("/role-select")
def role_select_page():
"""角色选择页面"""
return FileResponse(str(STATIC_DIR / "role_select.html"))
# 品种配置管理页面(需要管理员权限)
@app.get("/ui")
def ui_page():
"""品种配置管理页面"""
return FileResponse(str(STATIC_DIR / "index.html"))
# 注册路由
app.include_router(auth.router, prefix="/api/v1")
app.include_router(data.router, prefix="/api/v1")
app.include_router(tasks.router, prefix="/api/v1")
app.include_router(config.router, prefix="/api/v1")
app.include_router(futures_analysis.router, prefix="/api/v1")
app.include_router(ai_config.router, prefix="/api/v1")
app.include_router(review_plan.router, prefix="/api/v1")
app.include_router(trade_review.router, prefix="/api/v1")
@app.get("/futures-analysis")
def futures_analysis_page():
"""期货智析页面"""
return FileResponse(str(STATIC_DIR / "futures_analysis.html"))
@app.get("/ai-config")
def ai_config_page():
"""AI模型配置页面"""
return FileResponse(str(STATIC_DIR / "ai_config.html"))
@app.get("/review-plan")
def review_plan_page():
"""复盘计划页面"""
return FileResponse(str(STATIC_DIR / "review_plan.html"))
@app.get("/trade-reflection-demo")
def trade_reflection_demo_page():
"""交易反思系统 Demo 页面"""
return FileResponse(str(STATIC_DIR / "trade_reflection_demo.html"))
@app.get("/api/v1/health")
def health():
return {"status": "ok", "service": "market-data-buffer"}
# 根路径重定向到登录页
@app.get("/")
def root():
"""根路径重定向到登录页"""
return RedirectResponse(url="/login")
if __name__ == "__main__":
import uvicorn
uvicorn.run("app.main:app", host=HOST, port=PORT, reload=True)