# -*- coding: utf-8 -*- """管理端 — 触发器统一视图 API 提供 1 个端点: - GET /api/admin/triggers/unified — 聚合三张表的触发器数据 数据源: - biz.trigger_jobs(业务触发器)→ source="biz" - biz.ai_trigger_jobs(AI 事件链,最近 100 条)→ source="ai" - public.scheduled_tasks(ETL 调度)→ source="etl" 某数据源查询失败时记录日志,返回其他数据源数据。 需求: 4.1, 4.2, 4.3 """ from __future__ import annotations import logging from fastapi import APIRouter, Depends from app.auth.dependencies import CurrentUser, get_current_user from app.database import get_connection from app.schemas.admin_triggers import UnifiedTriggerItem logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/admin/triggers", tags=["系统管理"]) def _fetch_biz_triggers(conn) -> list[UnifiedTriggerItem]: """查询 biz.trigger_jobs,映射 source='biz'。""" with conn.cursor() as cur: cur.execute( """ SELECT id, job_name, trigger_condition, status, last_run_at, next_run_at, last_error FROM biz.trigger_jobs ORDER BY id """ ) rows = cur.fetchall() return [ UnifiedTriggerItem( id=row[0], name=row[1], source="biz", trigger_condition=row[2] or "", status=row[3] or "", last_run_at=str(row[4]) if row[4] is not None else None, next_run_at=str(row[5]) if row[5] is not None else None, last_error=row[6], ) for row in rows ] def _fetch_ai_triggers(conn) -> list[UnifiedTriggerItem]: """查询 biz.ai_trigger_jobs(最近 100 条),映射 source='ai'。 字段映射(DDL 实际列 → UnifiedTriggerItem): - id → id - event_type → name(ai_trigger_jobs 无 job_name 列) - 'event' → trigger_condition(AI 触发器均为事件驱动) - status → status - started_at → last_run_at(ai_trigger_jobs 无 last_run_at 列) - None → next_run_at(事件驱动无预定下次执行时间) - error_message → last_error(ai_trigger_jobs 列名为 error_message) """ with conn.cursor() as cur: cur.execute( """ SELECT id, event_type, status, started_at, error_message FROM biz.ai_trigger_jobs ORDER BY id DESC LIMIT 100 """ ) rows = cur.fetchall() return [ UnifiedTriggerItem( id=row[0], name=row[1] or "", source="ai", trigger_condition="event", status=row[2] or "", last_run_at=str(row[3]) if row[3] is not None else None, next_run_at=None, last_error=row[4], ) for row in rows ] def _fetch_etl_triggers(conn) -> list[UnifiedTriggerItem]: """查询 public.scheduled_tasks,映射 source='etl'。 字段映射(DDL 实际列 → UnifiedTriggerItem): - id → id(UUID,转为字符串后取 hashcode 作为 int 不合适,改用 row_number) - name → name - schedule_config->>'schedule_type' → trigger_condition - last_status / enabled → status(组合判断) - last_run_at → last_run_at - next_run_at → next_run_at - None → last_error(scheduled_tasks 无 last_error 列) 注意:scheduled_tasks.id 是 UUID 类型,UnifiedTriggerItem.id 是 int。 使用 ROW_NUMBER() 生成临时整数 ID,加 100000 偏移避免与其他数据源冲突。 """ with conn.cursor() as cur: cur.execute( """ SELECT ROW_NUMBER() OVER (ORDER BY created_at) + 100000 AS row_id, name, schedule_config->>'schedule_type' AS schedule_type, CASE WHEN enabled = FALSE THEN 'disabled' WHEN last_status IS NOT NULL THEN last_status ELSE 'idle' END AS status, last_run_at, next_run_at FROM scheduled_tasks ORDER BY created_at """ ) rows = cur.fetchall() return [ UnifiedTriggerItem( id=int(row[0]), name=row[1] or "", source="etl", trigger_condition=row[2] or "unknown", status=row[3] or "idle", last_run_at=str(row[4]) if row[4] is not None else None, next_run_at=str(row[5]) if row[5] is not None else None, last_error=None, ) for row in rows ] @router.get("/unified", response_model=list[UnifiedTriggerItem]) async def get_unified_triggers( user: CurrentUser = Depends(get_current_user), ) -> list[UnifiedTriggerItem]: """聚合三张表的触发器数据。 依次查询 biz.trigger_jobs、biz.ai_trigger_jobs、scheduled_tasks, 某数据源查询失败时记录日志并跳过,返回其他数据源的数据。 """ results: list[UnifiedTriggerItem] = [] conn = get_connection() try: # 数据源 1:biz.trigger_jobs try: results.extend(_fetch_biz_triggers(conn)) except Exception: logger.warning("查询 biz.trigger_jobs 失败", exc_info=True) # 数据源 2:biz.ai_trigger_jobs try: results.extend(_fetch_ai_triggers(conn)) except Exception: logger.warning("查询 biz.ai_trigger_jobs 失败", exc_info=True) # 数据源 3:public.scheduled_tasks try: results.extend(_fetch_etl_triggers(conn)) except Exception: logger.warning("查询 scheduled_tasks 失败", exc_info=True) return results finally: conn.close()