import logging from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from .aggregator import aggregate_period, enrich_snapshot_meta from .config import ROOT_DIR, settings from .db import get_latest_snapshot, init_db, log_push from .periods import get_today_period, get_yesterday_period from .scheduler import job_finalize_yesterday, job_push_wecom, job_refresh_today, start_scheduler, startup_tasks, stop_scheduler from .state import get_today_cache from .wecom import build_markdown, send_wecom_markdown logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) logger = logging.getLogger(__name__) WEB_DIR = ROOT_DIR / "web" @asynccontextmanager async def lifespan(app: FastAPI): init_db() if settings.proxy_enabled: logger.info( "Proxy enabled: %s (scope=%s)", settings.proxy_url, settings.proxy_for, ) else: logger.info("Proxy disabled (direct connection)") await startup_tasks() start_scheduler() yield stop_scheduler() app = FastAPI(title="币安成交量排名监控", lifespan=lifespan) if WEB_DIR.exists(): app.mount("/static", StaticFiles(directory=str(WEB_DIR)), name="static") @app.get("/") async def index(): index_path = WEB_DIR / "index.html" if index_path.exists(): return FileResponse(index_path) return {"message": "Web UI not found. Place files in /web"} @app.get("/api/yesterday/top30") async def api_yesterday_top30(): snap = get_latest_snapshot("yesterday") if snap: return { "period_start": snap["period_start"], "period_end": snap["period_end"], "updated_at": snap["created_at"], "top_n": settings.top_n, "volume_threshold": settings.volume_threshold, "change_threshold": settings.change_threshold, "items": snap["items"], } start, end = get_yesterday_period() try: items = await aggregate_period(start, end) return enrich_snapshot_meta(items, start, end) except Exception as e: logger.error("api yesterday failed: %s", e) meta = enrich_snapshot_meta([], start, end) meta["error"] = "数据暂不可用,请检查网络或稍后重试" return meta @app.get("/api/today/top30") async def api_today_top30(): cached = get_today_cache() if cached: return cached start, end = get_today_period() try: items = await aggregate_period(start, end, use_live_prices=True) return enrich_snapshot_meta(items, start, end) except Exception as e: logger.error("api today failed: %s", e) meta = enrich_snapshot_meta([], start, end) meta["error"] = "数据暂不可用,请检查网络或稍后重试" return meta @app.post("/api/push/test") async def api_push_test(): snap = get_latest_snapshot("yesterday") if not snap: start, end = get_yesterday_period() items = await aggregate_period(start, end) from .db import save_snapshot save_snapshot("yesterday", start, end, items) snap = get_latest_snapshot("yesterday") if not snap: raise HTTPException(500, "无法生成昨日数据") content = build_markdown(snap) ok, msg = await send_wecom_markdown(content) log_push(snap["period_start"], snap["period_end"], ok, msg) if not ok: raise HTTPException(500, f"推送失败: {msg}") return {"success": True, "message": "推送成功"} @app.post("/api/refresh/yesterday") async def api_refresh_yesterday(): await job_finalize_yesterday() snap = get_latest_snapshot("yesterday") return snap or {"message": "done"} @app.post("/api/refresh/today") async def api_refresh_today(): await job_refresh_today() return get_today_cache() or {"message": "done"}