后端: - 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 等类型定义
225 lines
7.9 KiB
Python
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
|