""" NeoZQYY 后端 API 入口 基于 FastAPI 构建,为管理后台和微信小程序提供 RESTful API。 OpenAPI 文档自动生成于 /docs(Swagger UI)和 /redoc(ReDoc)。 """ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from starlette.exceptions import HTTPException as StarletteHTTPException from app.middleware.response_wrapper import ( ResponseWrapperMiddleware, http_exception_handler, unhandled_exception_handler, ) from app.trace.middleware import TraceMiddleware from app import config # CHANGE 2026-02-19 | 新增 xcx_test 路由(MVP 验证)+ wx_callback 路由(微信消息推送) # CHANGE 2026-02-23 | 新增 ops_panel 路由(运维控制面板) # CHANGE 2026-02-25 | 新增 xcx_auth 路由(小程序微信登录 + 申请 + 状态查询 + 店铺切换) # CHANGE 2026-02-26 | member_birthday 路由替换为 member_retention_clue(维客线索重构) # CHANGE 2026-02-26 | 新增 admin_applications 路由(管理端申请审核) # CHANGE 2026-02-27 | 新增 xcx_tasks / xcx_notes 路由(小程序核心业务) # CHANGE 2026-03-09 | 新增 xcx_ai_chat 路由(AI SSE 对话 + 历史对话)→ 2026-03-20 迁移为 xcx_chat(/api/xcx/chat/*) # CHANGE 2026-03-09 | 新增 xcx_ai_cache 路由(AI 缓存查询) # CHANGE 2026-03-18 | 新增 xcx_customers 路由(CUST-1 客户详情、CUST-2 客户服务记录) # CHANGE 2026-03-19 | 新增 xcx_coaches 路由(COACH-1 助教详情) # CHANGE 2026-03-19 | 新增 xcx_board / xcx_config 路由(RNS1.3 三看板 + 技能类型配置) # CHANGE 2026-03-22 | 新增 admin_registry 路由(NS4.1 注册体系:租户/店铺/简写ID 管理) # CHANGE 2026-03-23 | 新增 admin_ai 路由(P15 AI 监控后台:Dashboard/调度/调用/缓存/预算/批量/告警) # CHANGE 2026-03-24 | 新增 admin_dev_trace 路由(dev-trace-log: 开发调试日志管理 API) # CHANGE 2026-03-23 | 新增 trigger_jobs 路由(定时任务管理页面 API) # CHANGE 2026-03-24 | P18 任务引擎运营看板:新增 admin_task_engine 路由 # CHANGE 2026-03-29 | DWS_TASK_ENGINE:新增 internal_events 路由(按 job_name 执行任务) from app.routers import auth, execution, schedules, tasks, env_config, db_viewer, etl_status, xcx_test, wx_callback, member_retention_clue, ops_panel, xcx_auth, xcx_avatar, admin_applications, business_day, xcx_tasks, xcx_notes, xcx_chat, xcx_ai_cache, xcx_performance, xcx_customers, xcx_coaches, xcx_board, xcx_config, tenant_auth, tenant_users, tenant_excel, tenant_clues, tenant_site_admins, admin_tenant_admins, admin_registry, internal_ai, admin_ai, admin_dev_trace, trigger_jobs, admin_task_engine, admin_db_health, admin_triggers, internal_events from app.services.scheduler import scheduler from app.services.task_queue import task_queue from app.services.task_executor import task_executor from app.ws.logs import ws_router @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期:启动时拉起后台服务,关闭时优雅停止。""" # CHANGE 2026-03-07 | 启动横幅:打印关键路径,便于诊断连到了哪个实例 import sys _banner = ( "\n" "╔══════════════════════════════════════════════════════╗\n" "║ NeoZQYY Backend — 启动诊断 ║\n" "╠══════════════════════════════════════════════════════╣\n" f"║ Python: {sys.executable}\n" f"║ ROOT: {config._project_root}\n" f"║ ETL_PATH: {config.ETL_PROJECT_PATH}\n" f"║ ETL_PY: {config.ETL_PYTHON_EXECUTABLE}\n" f"║ OPS_BASE: {config.OPS_SERVER_BASE}\n" f"║ APP_DB: {config.APP_DB_NAME}\n" f"║ .env: {config._root_env}\n" "╚══════════════════════════════════════════════════════╝\n" ) print(_banner, flush=True) # CHANGE 2026-03-22 | 启动时清理本机僵尸任务(上次非正常关闭遗留的 running 记录) task_executor.recover_stale() # 启动 task_queue.start() scheduler.start() # CHANGE 2026-02-27 | 注册触发器 job handler(核心业务模块) # CHANGE 2026-03-24 | dev-trace-log: 用 trace_job 包装 job handler,追踪后台任务执行 from app.services.trigger_scheduler import register_job from app.services import task_generator, task_expiry, recall_detector, note_reclassifier from app.trace.job_wrapper import trace_job register_job("task_generator", trace_job("task_generator")(lambda **_kw: task_generator.run())) register_job("task_expiry_check", trace_job("task_expiry_check")(lambda **_kw: task_expiry.run())) register_job("recall_completion_check", trace_job("recall_completion_check")(recall_detector.run)) register_job("note_reclassify_backfill", trace_job("note_reclassify_backfill")(note_reclassifier.run)) # CHANGE 2026-03-23 | 启动时检查定时任务是否今天执行过,打印提示 from app.services.trigger_scheduler import check_startup_jobs try: pending_jobs = check_startup_jobs() if pending_jobs: _lines = ["╔══ 定时任务提醒 ══════════════════════════════════════╗"] for j in pending_jobs: _lines.append(f"║ ⚠ {j['description']}({j['job_name']})— {j['last_run_at']}") _lines.append("║ → 请在管理后台「定时任务」页面手动执行") _lines.append("╚══════════════════════════════════════════════════════╝") print("\n".join(_lines), flush=True) else: print("✓ 所有定时任务今天已执行过", flush=True) except Exception: import logging as _log _log.getLogger(__name__).warning("启动检查定时任务失败", exc_info=True) # CHANGE 2026-03-10 | 注册 AI 事件处理器(消费/备注/任务分配 → AI 调用链) # CHANGE 2026-03-22 | P14 迁移:BailianClient → DashScopeClient + AIConfig + 防护层 try: from app.ai.config import AIConfig from app.ai.dashscope_client import DashScopeClient from app.ai.cache_service import AICacheService from app.ai.conversation_service import ConversationService from app.ai.circuit_breaker import CircuitBreaker from app.ai.rate_limiter import RateLimiter from app.ai.budget_tracker import BudgetTracker from app.ai.run_log_service import AIRunLogService from app.ai.dispatcher import AIDispatcher, register_ai_handlers from app.database import get_connection _ai_config = AIConfig.from_env() _client = DashScopeClient(api_key=_ai_config.api_key, workspace_id=_ai_config.workspace_id) _run_log_svc = AIRunLogService(get_conn=get_connection) _dispatcher = AIDispatcher( client=_client, cache_svc=AICacheService(), conv_svc=ConversationService(), circuit_breaker=CircuitBreaker(), rate_limiter=RateLimiter(), budget_tracker=BudgetTracker(usage_provider=_run_log_svc), run_log_svc=_run_log_svc, config=_ai_config, ) register_ai_handlers(_dispatcher) except Exception: import logging as _log _log.getLogger(__name__).warning("AI 事件处理器注册失败,AI 功能不可用", exc_info=True) yield # CHANGE 2026-03-22 | 优雅关闭:先终止所有运行中的子进程(3s 超时),再停调度和队列 await task_executor.shutdown(timeout=3.0) await scheduler.stop() await task_queue.stop() app = FastAPI( title="NeoZQYY API", description="台球门店运营助手 — 后端 API(管理后台 + 微信小程序)", version="0.1.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, ) # ---- CORS 中间件 ---- # 允许来源从环境变量 CORS_ORIGINS 读取,缺省允许 Vite 开发服务器 (localhost:5173) app.add_middleware( CORSMiddleware, allow_origins=config.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ---- 全局响应包装中间件(在 CORS 之后添加,执行顺序在 CORS 内层) ---- # CHANGE 2026-03-16 | RNS1.0 T0-1: 全局响应包装 + 异常处理器 app.add_middleware(ResponseWrapperMiddleware) # ---- 全链路追踪中间件(最后添加 = 最先执行 = 最外层) ---- # CHANGE 2026-03-24 | dev-trace-log: TraceMiddleware 包裹所有中间件,仅拦截 /api/xcx/ 路由 app.add_middleware(TraceMiddleware) # ---- 全局异常处理器 ---- app.add_exception_handler(StarletteHTTPException, http_exception_handler) app.add_exception_handler(Exception, unhandled_exception_handler) # ---- 路由注册 ---- app.include_router(auth.router) app.include_router(tasks.router) app.include_router(execution.router) app.include_router(schedules.router) app.include_router(env_config.router) app.include_router(db_viewer.router) app.include_router(etl_status.router) app.include_router(ws_router) app.include_router(xcx_test.router) app.include_router(wx_callback.router) app.include_router(member_retention_clue.router) app.include_router(ops_panel.router) app.include_router(xcx_auth.router) app.include_router(xcx_avatar.router) app.include_router(admin_applications.router) app.include_router(business_day.router) app.include_router(xcx_tasks.router) app.include_router(xcx_notes.router) app.include_router(xcx_chat.router) app.include_router(xcx_ai_cache.router) app.include_router(xcx_performance.router) app.include_router(xcx_customers.router) app.include_router(xcx_coaches.router) app.include_router(xcx_board.router) app.include_router(xcx_config.router) app.include_router(tenant_auth.router) app.include_router(tenant_users.router) app.include_router(tenant_excel.router) app.include_router(tenant_clues.router) app.include_router(tenant_site_admins.router) app.include_router(admin_tenant_admins.router) app.include_router(admin_registry.router) app.include_router(internal_ai.router) app.include_router(internal_events.router) app.include_router(admin_ai.router) app.include_router(admin_dev_trace.router) app.include_router(trigger_jobs.router) app.include_router(admin_task_engine.router) app.include_router(admin_db_health.router) app.include_router(admin_triggers.router) @app.get("/health", tags=["系统"]) async def health_check(): """健康检查端点,用于探活和监控。""" return {"status": "ok"} # CHANGE 2026-03-07 | 诊断端点:返回关键路径配置,用于确认连到的是哪个实例 @app.get("/debug/config-paths", tags=["系统"]) async def debug_config_paths(): """返回当前后端实例的关键路径配置(仅开发环境使用)。""" import sys import os import platform from app.services.cli_builder import cli_builder as _cb from app.schemas.tasks import TaskConfigSchema as _TCS _test_cfg = _TCS(flow="api_ods_dwd", processing_mode="increment_only", tasks=["DWD_LOAD_FROM_ODS"], store_id=123) _test_cmd = _cb.build_command( _test_cfg, config.ETL_PROJECT_PATH, python_executable=config.ETL_PYTHON_EXECUTABLE, ) _test_cmd_str = " ".join(_test_cmd) return { "hostname": platform.node(), "python_executable": sys.executable, "project_root": str(config._project_root), "env_file": str(config._root_env), "etl_python_executable": config.ETL_PYTHON_EXECUTABLE, "etl_project_path": config.ETL_PROJECT_PATH, "simulated_command": _test_cmd_str, "NEOZQYY_ROOT_env": os.environ.get("NEOZQYY_ROOT", "<未设置>"), "cwd": os.getcwd(), }