涵盖(每条对应已存的审计记录): - AI 模块拆分:apps/backend/app/ai/apps -> prompts/(8 个 APP + app2a 派生) audit: 2026-04-20__ai-module-complete.md - admin-web AI 管理套件:AIDashboard / AIOperations / AIRunLogs / AITriggers / TriggerManager audit: 2026-04-21__admin-web-ai-management-suite.md - App2 财务洞察 prompt v3 -> v5.1 + 小程序 AI 接入(chat / board-finance) audit: 2026-04-22__app2_prompt_v5_1_and_miniprogram_ai_insight.md - App2 prewarm 全过滤器 + AI 触发器 cron reschedule audit: 2026-04-21__app2-finance-prewarm-all-filters.md migration: 20260420_ai_trigger_jobs_and_app2_prewarm.sql / 20260421_app2_prewarm_cron_reschedule.sql - AppType 联合类型对齐 + adminAiAppTypes.test.ts audit: 2026-04-30__admin_web_ai_app_type_alignment.md - DashScope tokens_used 提取修复 audit: 2026-04-30__backend_dashscope_tokens_used_extraction.md - App3 线索完整详情 prompt audit: 2026-05-01__backend_app3_full_detail_prompt.md - Runtime Context 沙箱(5-1~5-2 主线): - 后端 schema/service + admin_runtime_context / xcx_runtime_clock 两个 router - admin-web RuntimeContext.tsx + miniprogram runtime-clock.ts - migration: 20260501__runtime_context_sandbox.sql - tools/db/verify_admin_web_sandbox.py + verify_sandbox_end_to_end.py - database/changes: 7 份 sandbox_* 验证报告 - 飞球 DWS 修复:finance_area_daily 区域汇总 + task_engine 调整 + RLS 视图业务日上界(migration 20260502 + scripts/ops/gen_rls_business_date_migration.py) 合规: - .gitignore 启用 tmp/ 排除 - 不入仓:apps/etl/connectors/feiqiu/.env(API_TOKEN secret,本地修改保留) 待验证清单: - docs/audit/changes/2026-05-04__cumulative_baseline_pending_verification.md 每个主题的功能完整性 / 上线验证几乎都未收口,按优先级 P0~P3 逐一处理
284 lines
14 KiB
Python
284 lines
14 KiB
Python
"""
|
||
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, xcx_runtime_clock, 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, admin_runtime_context
|
||
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
|
||
from app.ws.ai_events import ws_router as ai_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-04-22 | 启动时清理上次进程遗留的孤儿 run_logs(worker 被 kill/reload 导致 status 卡在 running)
|
||
try:
|
||
from app.database import get_connection as _get_conn_cleanup
|
||
_c = _get_conn_cleanup()
|
||
try:
|
||
with _c.cursor() as _cur:
|
||
_cur.execute(
|
||
"""
|
||
UPDATE biz.ai_run_logs
|
||
SET status = 'failed',
|
||
error_message = COALESCE(error_message, '') || ' [orphaned_by_restart]',
|
||
finished_at = COALESCE(finished_at, NOW())
|
||
WHERE status = 'running'
|
||
AND created_at < NOW() - INTERVAL '5 minutes'
|
||
"""
|
||
)
|
||
_cleaned = _cur.rowcount
|
||
_c.commit()
|
||
if _cleaned:
|
||
import logging as _log
|
||
_log.getLogger(__name__).info("启动清理 %d 条孤儿 run_logs(status=running > 5min)", _cleaned)
|
||
finally:
|
||
_c.close()
|
||
except Exception:
|
||
import logging as _log
|
||
_log.getLogger(__name__).warning("孤儿 run_logs 清理失败(忽略)", 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)
|
||
from app.routers import internal_ai as _internal_ai_router
|
||
_internal_ai_router.set_dispatcher(_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(ai_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(xcx_runtime_clock.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.include_router(admin_runtime_context.router)
|
||
app.include_router(admin_runtime_context.config_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(),
|
||
}
|