"""桥侧计划 API(07-30 用户需求):对外提供每日选股计划。 容器常驻命令改为 uvicorn 后随容器启动(docker-compose 已配端口,默认 8300); cron 的 docker exec 构建/出计划照旧,互不影响。局域网内部服务,v1 无鉴权。 GET /health 存活 + 最新计划日 GET /plan 最新一天的计划(JSON) GET /plan?date=2026-07-30 指定日期 GET /plan?format=md Markdown 原文(浏览器直接可读) GET /plan/dates 可用日期列表 POST /plan/refresh?date=... 重新生成该日计划文件(data/plan/*.md) 统一任务调度平台(XXL-JOB)触发入口挂在 /api/v1/xxl/*(见 xxl.py,2026-08-03): 盘前链(build → plan → push-pool)可由平台拉起并回调结案,.env 配 XXL_TRIGGER_KEY 才启用。 """ import pandas as pd from fastapi import FastAPI, HTTPException from fastapi.responses import PlainTextResponse import db import plan from xxl import router as xxl_router app = FastAPI(title="akg-factor-bridge · 每日选股计划", version="0.1") app.include_router(xxl_router) @app.get("/health") def health(): try: d = plan._latest_date("t_factor_akg_score") # noqa: SLF001 —— 桥内自用 except Exception as e: # noqa: BLE001 —— 库连不上也要能回答"我还活着" return {"ok": False, "error": repr(e)} return {"ok": True, "latest_plan_date": d} @app.get("/plan/dates") def plan_dates(limit: int = 30): df = db.read_mysql( "factor", "SELECT DISTINCT trade_date FROM t_factor_akg_score " "ORDER BY trade_date DESC LIMIT %s", (int(limit),)) if df.empty: return {"dates": []} return {"dates": [pd.Timestamp(x).date().isoformat() for x in df["trade_date"]]} @app.get("/plan") def get_plan(date: str | None = None, format: str = "json", top: int = 20, obs_top: int = 10, theme_cap: int = 5): try: data = plan.collect(date, top, obs_top, theme_cap) except RuntimeError as e: raise HTTPException(status_code=404, detail=str(e)) if format == "md": return PlainTextResponse(plan.render_md(data), media_type="text/markdown; charset=utf-8") return data @app.post("/plan/refresh") def refresh(date: str | None = None): try: out = plan.generate(date) except SystemExit as e: raise HTTPException(status_code=404, detail=str(e)) return {"ok": True, "file": out}