feat: Dashboard数据关联修复 + 通知模板优化 + 工单创建页布局

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)
This commit is contained in:
logan committed 2026-05-25 01:29:24 +08:00
1 parent 7109031f71
commit 880dbfefa4
9 files changed
+423 -209

No files matched your search

+77 -6
View File
@@ -1,6 +1,6 @@
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends
from sqlalchemy import select, func
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
@@ -9,7 +9,8 @@ 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.schemas.dashboard import DashboardStats
from app.models.query_history import QueryHistory
from app.schemas.dashboard import DashboardStats, TrendData, TrendPoint
router = APIRouter(prefix="/dashboard", tags=["仪表盘"])
@@ -44,12 +45,41 @@ async def get_stats(
)).scalar() or 0
queries_today = (await db.execute(
select(func.count(AuditLog.id)).where(
AuditLog.action == "QUERY",
AuditLog.created_at >= today_start,
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,
@@ -59,4 +89,45 @@ async def get_stats(
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)