Files
opsxsql/backend/app/api/v1/endpoints/dashboard.py
T
logan 43d1f3a234 feat: 工单权限过滤+仪表盘优化+LDAP登录+审批流程+待处理工单
后端:
- ticket_statistics/list_tickets 按用户身份过滤(admin全部, dba/auditor审批相关, developer自己的)
- 新增 /dashboard/my-tickets 接口(按角色返回待处理工单)
- auth.py 本地优先+LDAP fallback 登录认证
- ldap_config.py LDAPS 证书验证修复
- instance_detail.py developer/auditor 权限修复

前端:
- Dashboard 仪表盘: 3核心大数字指标+公告+我的待处理工单
- 删除 ECharts 趋势图(无真实数据)
- TicketDetailPage 审批流程时间线展示
- TicketsPage 工单统计按用户过滤+关联列(我提交的/待我审批)
- types 新增 TicketApproval 等类型定义
2026-05-25 20:47:15 +08:00

225 lines
7.9 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)
@router.get("/my-tickets")
async def my_pending_tickets(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""返回当前用户关联的待处理工单:
- admin: 所有 pending/reviewing/executing
- dba: 自己需要审批的 + 自己提交的
- developer/auditor: 自己提交的
"""
from app.models.workflow import TicketApproval, WorkflowStep
from sqlalchemy import or_
import json as _json
user_id = current_user.id
role = current_user.role
if role == "admin":
# admin 看所有待处理
rows = (await db.execute(
select(Ticket).where(
Ticket.status.in_(["pending", "reviewing", "executing"])
).order_by(Ticket.create_time.desc()).limit(10)
)).scalars().all()
else:
# 非admin: 找"和我有关"的工单
# 1) 我提交的
my_submitted = set((await db.execute(
select(Ticket.id).where(
Ticket.status.in_(["pending", "reviewing", "executing"]),
Ticket.applicant_id == user_id,
)
)).scalars().all())
# 2) ticket_approvals 中 action=pending 且 approver_id = me
my_approvals = set((await db.execute(
select(TicketApproval.ticket_id).where(
TicketApproval.action == "pending",
TicketApproval.approver_id == user_id,
)
)).scalars().all())
# 3) 通过角色匹配: approver_role == my role
my_role_approvals = set((await db.execute(
select(TicketApproval.ticket_id).where(
TicketApproval.action == "pending",
TicketApproval.approver_role == role,
)
)).scalars().all())
all_ids = my_submitted | my_approvals | my_role_approvals
if not all_ids:
return []
rows = (await db.execute(
select(Ticket).where(Ticket.id.in_(all_ids)).order_by(Ticket.create_time.desc()).limit(10)
)).scalars().all()
# 用户名映射
all_users = (await db.execute(select(User))).scalars().all()
user_map = {u.id: u.username for u in all_users}
result = []
for t in rows:
pending_approvals = (await db.execute(
select(TicketApproval).where(
TicketApproval.ticket_id == t.id,
TicketApproval.action == "pending",
)
)).scalars().all()
pending_info = []
for pa in pending_approvals:
approver_name = user_map.get(pa.approver_id, "")
label = pa.step_name or ""
if approver_name:
pending_info.append(f"{label}: {approver_name}" if label else approver_name)
is_mine = t.applicant_id == user_id
result.append({
"id": t.id,
"title": t.title or "",
"status": t.status,
"db_type": t.db_type or "",
"applicant": user_map.get(t.applicant_id, ""),
"create_time": t.create_time.strftime("%m-%d %H:%M") if t.create_time else "",
"pending_info": ", ".join(filter(None, pending_info)),
"relation": "我提交的" if is_mine else "待我审批",
})
return result