Files
Neo-ZQYY/apps/backend/app/routers/etl_status.py
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

137 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""ETL 状态监控 API
提供 2 个端点:
- GET /api/etl-status/cursors — 返回各任务的数据游标(最后抓取时间、记录数)
- GET /api/etl-status/recent-runs — 返回最近 50 条任务执行记录
所有端点需要 JWT 认证。
游标端点查询 ETL 数据库meta.etl_cursor
执行记录端点查询 zqyy_app 数据库task_execution_log
"""
from __future__ import annotations
import logging
from fastapi import APIRouter, Depends, HTTPException, status
from psycopg2 import OperationalError
from app.auth.dependencies import CurrentUser, get_current_user
from app.database import get_connection, get_etl_global_readonly_connection
from app.schemas.etl_status import CursorInfo, RecentRun
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/etl-status", tags=["ETL 状态"])
# 最近执行记录条数上限
_RECENT_RUNS_LIMIT = 50
# ── GET /api/etl-status/cursors ──────────────────────────────
@router.get("/cursors", response_model=list[CursorInfo])
async def list_cursors(
user: CurrentUser = Depends(get_current_user),
) -> list[CursorInfo]:
"""返回各 ODS 表的最新数据游标。
查询 ETL 数据库中的 meta.etl_cursor 表。
如果该表不存在,返回空列表而非报错。
"""
# CHANGE 2026-03-23 | 系统管理后台全局视角,不按门店过滤
conn = get_etl_global_readonly_connection()
try:
with conn.cursor() as cur:
# CHANGE 2026-02-15 | 对齐新库 etl_feiqiu 六层架构etl_admin → meta
cur.execute(
"""
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'meta'
AND table_name = 'etl_cursor'
)
"""
)
exists = cur.fetchone()[0]
if not exists:
return []
cur.execute(
"""
SELECT t.task_code, c.last_start, c.last_end
FROM meta.etl_cursor c
JOIN meta.etl_task t ON c.task_id = t.task_id
ORDER BY t.task_code
"""
)
rows = cur.fetchall()
return [
CursorInfo(
task_code=row[0],
last_start=str(row[1]) if row[1] is not None else None,
last_end=str(row[2]) if row[2] is not None else None,
)
for row in rows
]
except OperationalError as exc:
logger.error("ETL 游标查询连接错误: %s", exc)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="ETL 数据库连接错误",
)
finally:
conn.close()
# ── GET /api/etl-status/recent-runs ──────────────────────────
@router.get("/recent-runs", response_model=list[RecentRun])
async def list_recent_runs(
user: CurrentUser = Depends(get_current_user),
) -> list[RecentRun]:
"""返回最近 50 条任务执行记录。
查询 zqyy_app 数据库中的 task_execution_log 表,
按 site_id 过滤,按 started_at DESC 排序。
"""
conn = get_connection()
try:
with conn.cursor() as cur:
# CHANGE 2026-03-23 | 系统管理后台全局视角,不按门店过滤
cur.execute(
"""
SELECT id, task_codes, status, started_at,
finished_at, duration_ms, exit_code
FROM task_execution_log
ORDER BY started_at DESC
LIMIT %s
""",
(_RECENT_RUNS_LIMIT,),
)
rows = cur.fetchall()
return [
RecentRun(
id=str(row[0]),
task_codes=list(row[1]) if row[1] else [],
status=row[2],
started_at=str(row[3]),
finished_at=str(row[4]) if row[4] is not None else None,
duration_ms=row[5],
exit_code=row[6],
)
for row in rows
]
except OperationalError as exc:
logger.error("执行记录查询连接错误: %s", exc)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="数据库连接错误",
)
finally:
conn.close()