1. Dashboard: stats新增queryStats查询统计, trend API真实工单趋势数据 2. Layout铃铛: 显示真实待审批工单数量 3. 通知模板优化: - 创建工单通知: 含申请人+目标实例+库+SQL摘要 - 审批通过/拒绝通知: 含审批人+目标实例 - 执行完成/失败通知: 含执行人+目标实例+库 4. 飞书卡片颜色: 创建=turquoise, 审批=blue, 成功=green, 失败=red 5. 钉钉消息: 添加状态emoji标签(🆕✅❌⚠️) 6. TicketCreatePage: 底部操作栏(SQL检测+提交审批) 7. ticket_executor: 移除aiomysql不支持的read_timeout/write_timeout Tests: 1057 passed, 17 pre-existing failures (notification 35/35 passed)
134 lines
4.7 KiB
Python
134 lines
4.7 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy import select, func, case, extract
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_user
|
|
from app.models.user import User
|
|
from app.models.ticket import Ticket
|
|
from app.models.instance import Instance
|
|
from app.models.audit_log import AuditLog
|
|
from app.models.query_history import QueryHistory
|
|
from app.schemas.dashboard import DashboardStats, TrendData, TrendPoint
|
|
|
|
router = APIRouter(prefix="/dashboard", tags=["仪表盘"])
|
|
|
|
|
|
@router.get("/stats", response_model=DashboardStats)
|
|
async def get_stats(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
now = datetime.now(timezone.utc)
|
|
month_ago = now - timedelta(days=30)
|
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
ticket_total = (await db.execute(select(func.count(Ticket.id)))).scalar() or 0
|
|
ticket_pending = (await db.execute(
|
|
select(func.count(Ticket.id)).where(Ticket.status.in_(["pending", "reviewing"]))
|
|
)).scalar() or 0
|
|
ticket_month = (await db.execute(
|
|
select(func.count(Ticket.id)).where(Ticket.create_time >= month_ago)
|
|
)).scalar() or 0
|
|
ticket_executing = (await db.execute(
|
|
select(func.count(Ticket.id)).where(Ticket.status == "executing")
|
|
)).scalar() or 0
|
|
|
|
instance_total = (await db.execute(select(func.count(Instance.id)))).scalar() or 0
|
|
instance_online = (await db.execute(
|
|
select(func.count(Instance.id)).where(Instance.status == "connected")
|
|
)).scalar() or 0
|
|
|
|
user_total = (await db.execute(
|
|
select(func.count(User.id)).where(User.is_active == True)
|
|
)).scalar() or 0
|
|
|
|
queries_today = (await db.execute(
|
|
select(func.count(QueryHistory.id)).where(
|
|
QueryHistory.created_at >= today_start,
|
|
)
|
|
)).scalar() or 0
|
|
|
|
# --- 查询统计 ---
|
|
qh_today = select(QueryHistory).where(QueryHistory.created_at >= today_start)
|
|
total_q = (await db.execute(
|
|
select(func.count(QueryHistory.id))
|
|
)).scalar() or 0
|
|
avg_time = (await db.execute(
|
|
select(func.avg(QueryHistory.execution_time_ms))
|
|
)).scalar() or 0
|
|
slow_count = (await db.execute(
|
|
select(func.count(QueryHistory.id)).where(QueryHistory.execution_time_ms > 1000)
|
|
)).scalar() or 0
|
|
|
|
# 按数据库类型分布
|
|
type_rows = (await db.execute(
|
|
select(QueryHistory.db_type, func.count(QueryHistory.id).label('cnt'))
|
|
.group_by(QueryHistory.db_type)
|
|
)).all()
|
|
type_distribution = [
|
|
{"label": row[0] or "unknown", "count": row[1]}
|
|
for row in type_rows
|
|
]
|
|
|
|
from app.schemas.dashboard import QueryStatsData
|
|
query_stats = QueryStatsData(
|
|
total=total_q,
|
|
avg_time_ms=int(avg_time),
|
|
slow_count=slow_count,
|
|
type_distribution=type_distribution,
|
|
)
|
|
|
|
return DashboardStats(
|
|
ticket_total=ticket_total,
|
|
ticket_pending=ticket_pending,
|
|
ticket_month=ticket_month,
|
|
ticket_executing=ticket_executing,
|
|
instance_total=instance_total,
|
|
instance_online=instance_online,
|
|
user_total=user_total,
|
|
queries_today=queries_today,
|
|
query_stats=query_stats,
|
|
)
|
|
|
|
|
|
@router.get("/trend", response_model=TrendData)
|
|
async def get_trend(
|
|
days: int = Query(7, ge=1, le=90),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""工单趋势数据 — 按天聚合最近 N 天的提交/成功/失败数"""
|
|
now = datetime.now(timezone.utc)
|
|
start = now - timedelta(days=days)
|
|
|
|
# 按天聚合工单数
|
|
rows = (await db.execute(
|
|
select(
|
|
func.date(Ticket.create_time).label('d'),
|
|
func.count(Ticket.id).label('total'),
|
|
func.sum(case((Ticket.status.in_(['completed', 'success']), 1), else_=0)).label('success'),
|
|
func.sum(case((Ticket.status.in_(['failed', 'rejected']), 1), else_=0)).label('failed'),
|
|
)
|
|
.where(Ticket.create_time >= start)
|
|
.group_by(func.date(Ticket.create_time))
|
|
.order_by(func.date(Ticket.create_time))
|
|
)).all()
|
|
|
|
# 构建日期映射
|
|
data_map = {}
|
|
for row in rows:
|
|
d = str(row[0])
|
|
data_map[d] = {"submitted": row[1], "success": int(row[2] or 0), "failed": int(row[3] or 0)}
|
|
|
|
# 填充缺失日期
|
|
items = []
|
|
for i in range(days):
|
|
dt = (start + timedelta(days=i)).strftime('%Y-%m-%d')
|
|
d = data_map.get(dt, {"submitted": 0, "success": 0, "failed": 0})
|
|
label = dt[5:] # MM-DD
|
|
items.append(TrendPoint(date=label, **d))
|
|
|
|
return TrendData(items=items)
|