权限监控与管理界面全面美化
重新设计权限系统所有 UI 组件的视觉风格,统一配色、圆角、阴影和交互动效: - 实时概览:统计卡片加图标和渐变色条,健康评分改为环形进度,告警改为卡片式 - 趋势分析:图表卡片加彩色图标标识,ECharts 配色升级为渐变面积填充 - 访问日志:指标卡片带图标,日志审计改为卡片式入口,IP排行前三高亮 - IP属地:工具栏重设计,排行列表前三渐变高亮,指标卡片统一新风格 - 系统级权限:从 el-table 改为自定义卡片列表,模块块独立圆角卡片 - 项目权限配置:空状态引导优化,成员表格加头像,工具栏加背景容器 - 角色概览卡片:加进度条可视化,hover 微动效 - 接口权限矩阵:工具栏分离布局,表格圆角包裹 - 角色管理抽屉:侧边栏选中态渐变,操作行 hover 高亮 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,38 @@
|
||||
"""权限系统监控API
|
||||
|
||||
提供权限系统的监控数据和告警信息。
|
||||
提供权限系统的监控数据、访问日志、趋势分析和告警信息。
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from app.core.deps import get_current_user
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
from sqlalchemy import func, select, desc
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.core.permission_monitor import evaluate_permission_system_health, get_permission_monitor
|
||||
from app.models.permission_access_log import PermissionAccessLog
|
||||
from app.models.permission_metric_snapshot import PermissionMetricSnapshot
|
||||
from app.models.security_access_log import SecurityAccessLog
|
||||
from app.models.user import User
|
||||
from app.services.ip_location import resolve_ip_location
|
||||
|
||||
router = APIRouter(prefix="/permission-monitoring", tags=["permission-monitoring"])
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 原有端点(保持兼容)
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@router.get("/metrics", status_code=status.HTTP_200_OK)
|
||||
async def get_permission_metrics(
|
||||
_=Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""获取权限系统指标
|
||||
|
||||
返回权限检查、缓存等运行指标。
|
||||
"""
|
||||
monitor = get_permission_monitor()
|
||||
return monitor.get_metrics()
|
||||
|
||||
@@ -29,10 +41,6 @@ async def get_permission_metrics(
|
||||
async def get_cache_statistics(
|
||||
_=Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""获取缓存统计信息
|
||||
|
||||
返回缓存命中率、缓存项目数等信息。
|
||||
"""
|
||||
monitor = get_permission_monitor()
|
||||
return monitor.get_cache_stats()
|
||||
|
||||
@@ -43,15 +51,6 @@ async def get_alerts(
|
||||
limit: int = 100,
|
||||
_=Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""获取告警列表
|
||||
|
||||
Args:
|
||||
level: 告警级别过滤(info, warning, error)
|
||||
limit: 返回的最大告警数
|
||||
|
||||
Returns:
|
||||
告警列表
|
||||
"""
|
||||
monitor = get_permission_monitor()
|
||||
alerts = monitor.get_alerts(level=level, limit=limit)
|
||||
return {
|
||||
@@ -59,15 +58,10 @@ async def get_alerts(
|
||||
"alerts": alerts,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/reset-metrics", status_code=status.HTTP_200_OK)
|
||||
async def reset_metrics(
|
||||
_=Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""重置监控指标
|
||||
|
||||
清除所有累积的指标数据,重新开始统计。
|
||||
"""
|
||||
monitor = get_permission_monitor()
|
||||
monitor.reset_metrics()
|
||||
return {"message": "指标已重置"}
|
||||
@@ -77,10 +71,6 @@ async def reset_metrics(
|
||||
async def clear_alerts(
|
||||
_=Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""清除所有告警
|
||||
|
||||
删除所有累积的告警记录。
|
||||
"""
|
||||
monitor = get_permission_monitor()
|
||||
monitor.clear_alerts()
|
||||
return {"message": "告警已清除"}
|
||||
@@ -90,12 +80,527 @@ async def clear_alerts(
|
||||
async def permission_system_health(
|
||||
_=Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""权限系统健康检查
|
||||
|
||||
返回权限系统的健康状态。
|
||||
"""
|
||||
monitor = get_permission_monitor()
|
||||
metrics = monitor.get_metrics()
|
||||
cache_stats = monitor.get_cache_stats()
|
||||
|
||||
return evaluate_permission_system_health(metrics, cache_stats)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 新增端点:访问日志
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@router.get("/access-logs", status_code=status.HTTP_200_OK)
|
||||
async def get_access_logs(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
_=Depends(get_current_user),
|
||||
study_id: Optional[uuid.UUID] = Query(None),
|
||||
user_id: Optional[uuid.UUID] = Query(None),
|
||||
endpoint_key: Optional[str] = Query(None),
|
||||
role: Optional[str] = Query(None),
|
||||
allowed: Optional[bool] = Query(None),
|
||||
start_time: Optional[datetime] = Query(None),
|
||||
end_time: Optional[datetime] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
) -> dict:
|
||||
"""分页查询权限访问日志"""
|
||||
conditions = []
|
||||
if study_id:
|
||||
conditions.append(PermissionAccessLog.study_id == study_id)
|
||||
if user_id:
|
||||
conditions.append(PermissionAccessLog.user_id == user_id)
|
||||
if endpoint_key:
|
||||
conditions.append(PermissionAccessLog.endpoint_key == endpoint_key)
|
||||
if role:
|
||||
conditions.append(PermissionAccessLog.role == role)
|
||||
if allowed is not None:
|
||||
conditions.append(PermissionAccessLog.allowed == allowed)
|
||||
if start_time:
|
||||
conditions.append(PermissionAccessLog.created_at >= start_time)
|
||||
if end_time:
|
||||
conditions.append(PermissionAccessLog.created_at <= end_time)
|
||||
|
||||
query = (
|
||||
select(PermissionAccessLog, User.full_name)
|
||||
.outerjoin(User, PermissionAccessLog.user_id == User.id)
|
||||
)
|
||||
count_query = select(func.count()).select_from(PermissionAccessLog)
|
||||
summary_query = select(
|
||||
func.count().label("total_count"),
|
||||
func.count(func.distinct(PermissionAccessLog.user_id)).label("unique_user_count"),
|
||||
func.count(func.distinct(PermissionAccessLog.ip_address)).label("unique_ip_count"),
|
||||
func.count().filter(PermissionAccessLog.allowed.is_(False)).label("denied_count"),
|
||||
func.coalesce(func.avg(PermissionAccessLog.elapsed_ms), 0).label("avg_elapsed_ms"),
|
||||
).select_from(PermissionAccessLog)
|
||||
user_stats_query = (
|
||||
select(
|
||||
PermissionAccessLog.user_id,
|
||||
User.full_name,
|
||||
PermissionAccessLog.role,
|
||||
PermissionAccessLog.ip_address.label("sample_ip_address"),
|
||||
func.count().label("total_count"),
|
||||
func.count().filter(PermissionAccessLog.allowed.is_(False)).label("denied_count"),
|
||||
func.count(func.distinct(PermissionAccessLog.ip_address)).label("unique_ip_count"),
|
||||
func.max(PermissionAccessLog.created_at).label("last_seen_at"),
|
||||
)
|
||||
.outerjoin(User, PermissionAccessLog.user_id == User.id)
|
||||
.group_by(PermissionAccessLog.ip_address, PermissionAccessLog.user_id, User.full_name, PermissionAccessLog.role)
|
||||
.order_by(desc("total_count"), desc("last_seen_at"))
|
||||
.limit(10)
|
||||
)
|
||||
for condition in conditions:
|
||||
query = query.where(condition)
|
||||
count_query = count_query.where(condition)
|
||||
summary_query = summary_query.where(condition)
|
||||
user_stats_query = user_stats_query.where(condition)
|
||||
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar() or 0
|
||||
summary_result = await db.execute(summary_query)
|
||||
summary_row = summary_result.one()
|
||||
user_stats_result = await db.execute(user_stats_query)
|
||||
user_stats_rows = user_stats_result.all()
|
||||
|
||||
query = query.order_by(desc(PermissionAccessLog.created_at))
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
rows = result.all()
|
||||
|
||||
items = []
|
||||
for log, full_name in rows:
|
||||
ip_location = resolve_ip_location(log.ip_address)
|
||||
items.append(
|
||||
{
|
||||
"id": str(log.id),
|
||||
"study_id": str(log.study_id),
|
||||
"user_id": str(log.user_id),
|
||||
"user_name": full_name or "未知用户",
|
||||
"endpoint_key": log.endpoint_key,
|
||||
"role": log.role,
|
||||
"allowed": log.allowed,
|
||||
"elapsed_ms": round(log.elapsed_ms, 2),
|
||||
"ip_address": log.ip_address,
|
||||
"ip_location": ip_location.location,
|
||||
"ip_country": ip_location.country,
|
||||
"ip_province": ip_location.province,
|
||||
"ip_city": ip_location.city,
|
||||
"ip_isp": ip_location.isp,
|
||||
"created_at": log.created_at.isoformat(),
|
||||
}
|
||||
)
|
||||
user_stats = []
|
||||
for stat in user_stats_rows:
|
||||
sample_location = resolve_ip_location(stat.sample_ip_address)
|
||||
user_stats.append(
|
||||
{
|
||||
"user_id": str(stat.user_id),
|
||||
"user_name": stat.full_name or "未知用户",
|
||||
"role": stat.role,
|
||||
"total_count": stat.total_count,
|
||||
"denied_count": stat.denied_count,
|
||||
"unique_ip_count": stat.unique_ip_count,
|
||||
"sample_ip_address": stat.sample_ip_address,
|
||||
"last_seen_at": stat.last_seen_at.isoformat() if stat.last_seen_at else None,
|
||||
"primary_location": sample_location.location,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"summary": {
|
||||
"total_count": summary_row.total_count,
|
||||
"unique_user_count": summary_row.unique_user_count,
|
||||
"unique_ip_count": summary_row.unique_ip_count,
|
||||
"denied_count": summary_row.denied_count,
|
||||
"avg_elapsed_ms": round(float(summary_row.avg_elapsed_ms), 2),
|
||||
},
|
||||
"user_stats": user_stats,
|
||||
"items": items,
|
||||
}
|
||||
|
||||
|
||||
def _security_account_label(auth_status: str, user_identifier: str | None, user_names: dict[str, str] | None = None) -> str:
|
||||
if auth_status == "AUTHENTICATED" and user_identifier:
|
||||
return (user_names or {}).get(user_identifier) or user_identifier
|
||||
if auth_status == "INVALID_TOKEN":
|
||||
return "无效令牌"
|
||||
return "未知账号"
|
||||
|
||||
|
||||
@router.get("/security-logs", status_code=status.HTTP_200_OK)
|
||||
async def get_security_access_logs(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
_=Depends(get_current_user),
|
||||
status_min: Optional[int] = Query(None, ge=100, le=599),
|
||||
auth_status: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
) -> dict:
|
||||
"""查询底层安全访问日志,覆盖匿名、无效令牌和异常状态请求。"""
|
||||
conditions = []
|
||||
if status_min is not None:
|
||||
conditions.append(SecurityAccessLog.status_code >= status_min)
|
||||
if auth_status:
|
||||
conditions.append(SecurityAccessLog.auth_status == auth_status)
|
||||
|
||||
query = select(SecurityAccessLog)
|
||||
count_query = select(func.count()).select_from(SecurityAccessLog)
|
||||
summary_query = select(
|
||||
func.count().label("total_count"),
|
||||
func.count().filter(SecurityAccessLog.auth_status == "ANONYMOUS").label("anonymous_count"),
|
||||
func.count().filter(SecurityAccessLog.auth_status == "INVALID_TOKEN").label("invalid_token_count"),
|
||||
func.count().filter(SecurityAccessLog.status_code >= 400).label("error_count"),
|
||||
).select_from(SecurityAccessLog)
|
||||
for condition in conditions:
|
||||
query = query.where(condition)
|
||||
count_query = count_query.where(condition)
|
||||
summary_query = summary_query.where(condition)
|
||||
|
||||
total_result = await db.execute(count_query)
|
||||
summary_result = await db.execute(summary_query)
|
||||
result = await db.execute(
|
||||
query.order_by(desc(SecurityAccessLog.created_at))
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
summary_row = summary_result.one()
|
||||
|
||||
logs = result.scalars().all()
|
||||
user_ids: list[uuid.UUID] = []
|
||||
for log in logs:
|
||||
if log.auth_status != "AUTHENTICATED" or not log.user_identifier:
|
||||
continue
|
||||
try:
|
||||
user_ids.append(uuid.UUID(log.user_identifier))
|
||||
except ValueError:
|
||||
continue
|
||||
user_names: dict[str, str] = {}
|
||||
if user_ids:
|
||||
users_result = await db.execute(select(User.id, User.full_name).where(User.id.in_(user_ids)))
|
||||
user_names = {str(row.id): row.full_name for row in users_result.all()}
|
||||
|
||||
items = []
|
||||
for log in logs:
|
||||
items.append(
|
||||
{
|
||||
"id": str(log.id),
|
||||
"method": log.method,
|
||||
"path": log.path,
|
||||
"status_code": log.status_code,
|
||||
"elapsed_ms": round(log.elapsed_ms, 2),
|
||||
"client_ip": log.client_ip,
|
||||
"user_agent": log.user_agent,
|
||||
"auth_status": log.auth_status,
|
||||
"user_identifier": log.user_identifier,
|
||||
"account_label": _security_account_label(log.auth_status, log.user_identifier, user_names),
|
||||
"created_at": log.created_at.isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"total": total_result.scalar() or 0,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"summary": {
|
||||
"total_count": summary_row.total_count,
|
||||
"anonymous_count": summary_row.anonymous_count,
|
||||
"invalid_token_count": summary_row.invalid_token_count,
|
||||
"error_count": summary_row.error_count,
|
||||
},
|
||||
"items": items,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 新增端点:趋势数据
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@router.get("/trends", status_code=status.HTTP_200_OK)
|
||||
async def get_trends(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
_=Depends(get_current_user),
|
||||
period: str = Query("24h", pattern="^(24h|7d|30d)$"),
|
||||
) -> dict:
|
||||
"""获取趋势数据(从快照表或实时聚合)"""
|
||||
now = datetime.now(timezone.utc)
|
||||
period_map = {"24h": timedelta(hours=24), "7d": timedelta(days=7), "30d": timedelta(days=30)}
|
||||
start_time = now - period_map[period]
|
||||
|
||||
# 先尝试从快照表获取
|
||||
snapshot_query = (
|
||||
select(PermissionMetricSnapshot)
|
||||
.where(PermissionMetricSnapshot.bucket_time >= start_time)
|
||||
.order_by(PermissionMetricSnapshot.bucket_time)
|
||||
)
|
||||
result = await db.execute(snapshot_query)
|
||||
snapshots = result.scalars().all()
|
||||
|
||||
if snapshots:
|
||||
return {
|
||||
"period": period,
|
||||
"data_points": [
|
||||
{
|
||||
"bucket_time": s.bucket_time.isoformat(),
|
||||
"total_checks": s.total_checks,
|
||||
"allowed_checks": s.allowed_checks,
|
||||
"denied_checks": s.denied_checks,
|
||||
"avg_elapsed_ms": round(s.avg_elapsed_ms, 2),
|
||||
"max_elapsed_ms": round(s.max_elapsed_ms, 2),
|
||||
"cache_hits": s.cache_hits,
|
||||
"cache_misses": s.cache_misses,
|
||||
"cache_hit_rate": round(
|
||||
s.cache_hits / (s.cache_hits + s.cache_misses) * 100, 1
|
||||
) if (s.cache_hits + s.cache_misses) > 0 else 0,
|
||||
"error_count": s.error_count,
|
||||
}
|
||||
for s in snapshots
|
||||
],
|
||||
}
|
||||
|
||||
# 如果没有快照数据,从原始日志实时聚合(适用于刚部署时)。
|
||||
# 这里使用 Python 分桶,避免 SQLite 测试库不支持 PostgreSQL date_trunc。
|
||||
result = await db.execute(
|
||||
select(
|
||||
PermissionAccessLog.created_at,
|
||||
PermissionAccessLog.allowed,
|
||||
PermissionAccessLog.elapsed_ms,
|
||||
).where(PermissionAccessLog.created_at >= start_time)
|
||||
)
|
||||
buckets: dict[datetime, dict[str, float | int]] = defaultdict(
|
||||
lambda: {"total": 0, "allowed": 0, "denied": 0, "elapsed_sum": 0.0, "max_ms": 0.0}
|
||||
)
|
||||
for created_at, allowed, elapsed_ms in result.all():
|
||||
bucket = _trend_bucket_time(created_at, period)
|
||||
buckets[bucket]["total"] += 1
|
||||
buckets[bucket]["allowed" if allowed else "denied"] += 1
|
||||
buckets[bucket]["elapsed_sum"] += float(elapsed_ms or 0)
|
||||
buckets[bucket]["max_ms"] = max(float(buckets[bucket]["max_ms"]), float(elapsed_ms or 0))
|
||||
|
||||
return {
|
||||
"period": period,
|
||||
"data_points": [
|
||||
{
|
||||
"bucket_time": bucket.isoformat(),
|
||||
"total_checks": values["total"],
|
||||
"allowed_checks": values["allowed"],
|
||||
"denied_checks": values["denied"],
|
||||
"avg_elapsed_ms": round(float(values["elapsed_sum"]) / int(values["total"]), 2),
|
||||
"max_elapsed_ms": round(float(values["max_ms"]), 2),
|
||||
"cache_hits": 0,
|
||||
"cache_misses": 0,
|
||||
"cache_hit_rate": 0,
|
||||
"error_count": 0,
|
||||
}
|
||||
for bucket, values in sorted(buckets.items())
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _trend_bucket_time(value: datetime, period: str) -> datetime:
|
||||
if value.tzinfo is None:
|
||||
value = value.replace(tzinfo=timezone.utc)
|
||||
if period == "24h":
|
||||
return value.replace(minute=0, second=0, microsecond=0)
|
||||
if period == "7d":
|
||||
return value.replace(hour=(value.hour // 6) * 6, minute=0, second=0, microsecond=0)
|
||||
return value.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 新增端点:被拒绝最多的权限
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@router.get("/top-denied", status_code=status.HTTP_200_OK)
|
||||
async def get_top_denied(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
_=Depends(get_current_user),
|
||||
days: int = Query(7, ge=1, le=90),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
) -> dict:
|
||||
"""获取被拒绝最多的权限"""
|
||||
start_time = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
|
||||
query = (
|
||||
select(
|
||||
PermissionAccessLog.endpoint_key,
|
||||
PermissionAccessLog.role,
|
||||
func.count().label("denied_count"),
|
||||
)
|
||||
.where(
|
||||
PermissionAccessLog.created_at >= start_time,
|
||||
PermissionAccessLog.allowed.is_(False),
|
||||
)
|
||||
.group_by(PermissionAccessLog.endpoint_key, PermissionAccessLog.role)
|
||||
.order_by(desc("denied_count"))
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
rows = result.all()
|
||||
|
||||
return {
|
||||
"days": days,
|
||||
"items": [
|
||||
{
|
||||
"endpoint_key": row.endpoint_key,
|
||||
"role": row.role,
|
||||
"denied_count": row.denied_count,
|
||||
}
|
||||
for row in rows
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 新增端点:IP 属地统计
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@router.get("/ip-locations", status_code=status.HTTP_200_OK)
|
||||
async def get_ip_locations(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
_=Depends(get_current_user),
|
||||
days: int = Query(7, ge=1, le=90),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
) -> dict:
|
||||
"""获取 IP 省市属地统计。"""
|
||||
start_time = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
query = (
|
||||
select(
|
||||
PermissionAccessLog.ip_address,
|
||||
PermissionAccessLog.user_id,
|
||||
PermissionAccessLog.allowed,
|
||||
)
|
||||
.where(
|
||||
PermissionAccessLog.created_at >= start_time,
|
||||
PermissionAccessLog.ip_address.is_not(None),
|
||||
)
|
||||
)
|
||||
result = await db.execute(query)
|
||||
buckets: dict[tuple[str, str, str, str], dict] = {}
|
||||
all_ip_addresses: set[str] = set()
|
||||
all_user_ids: set[uuid.UUID] = set()
|
||||
total_count = 0
|
||||
allowed_count = 0
|
||||
denied_count = 0
|
||||
|
||||
for ip_address, user_id, allowed in result.all():
|
||||
ip_info = resolve_ip_location(ip_address)
|
||||
key = (ip_info.country, ip_info.province, ip_info.city, ip_info.isp)
|
||||
bucket = buckets.setdefault(
|
||||
key,
|
||||
{
|
||||
"country": ip_info.country,
|
||||
"province": ip_info.province,
|
||||
"city": ip_info.city,
|
||||
"isp": ip_info.isp,
|
||||
"location": ip_info.location,
|
||||
"total_count": 0,
|
||||
"allowed_count": 0,
|
||||
"denied_count": 0,
|
||||
"ip_addresses": set(),
|
||||
"user_ids": set(),
|
||||
},
|
||||
)
|
||||
bucket["total_count"] += 1
|
||||
bucket["allowed_count" if allowed else "denied_count"] += 1
|
||||
bucket["ip_addresses"].add(ip_address)
|
||||
bucket["user_ids"].add(user_id)
|
||||
total_count += 1
|
||||
if allowed:
|
||||
allowed_count += 1
|
||||
else:
|
||||
denied_count += 1
|
||||
all_ip_addresses.add(ip_address)
|
||||
all_user_ids.add(user_id)
|
||||
|
||||
items = sorted(buckets.values(), key=lambda item: item["total_count"], reverse=True)[:limit]
|
||||
return {
|
||||
"days": days,
|
||||
"summary": {
|
||||
"total_count": total_count,
|
||||
"allowed_count": allowed_count,
|
||||
"denied_count": denied_count,
|
||||
"unique_ip_count": len(all_ip_addresses),
|
||||
"unique_user_count": len(all_user_ids),
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"country": item["country"],
|
||||
"province": item["province"],
|
||||
"city": item["city"],
|
||||
"isp": item["isp"],
|
||||
"location": item["location"],
|
||||
"total_count": item["total_count"],
|
||||
"allowed_count": item["allowed_count"],
|
||||
"denied_count": item["denied_count"],
|
||||
"unique_ip_count": len(item["ip_addresses"]),
|
||||
"unique_user_count": len(item["user_ids"]),
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 新增端点:实时统计摘要(从DB获取)
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@router.get("/stats-summary", status_code=status.HTTP_200_OK)
|
||||
async def get_stats_summary(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
_=Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""获取基于数据库的统计摘要"""
|
||||
now = datetime.now(timezone.utc)
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
hour_ago = now - timedelta(hours=1)
|
||||
|
||||
# 今日统计
|
||||
today_result = await db.execute(
|
||||
select(
|
||||
func.count().label("total"),
|
||||
func.count().filter(PermissionAccessLog.allowed.is_(True)).label("allowed"),
|
||||
func.count().filter(PermissionAccessLog.allowed.is_(False)).label("denied"),
|
||||
func.coalesce(func.avg(PermissionAccessLog.elapsed_ms), 0).label("avg_ms"),
|
||||
func.coalesce(func.max(PermissionAccessLog.elapsed_ms), 0).label("max_ms"),
|
||||
).where(PermissionAccessLog.created_at >= today_start)
|
||||
)
|
||||
today = today_result.one()
|
||||
|
||||
# 最近一小时
|
||||
hour_result = await db.execute(
|
||||
select(
|
||||
func.count().label("total"),
|
||||
func.count().filter(PermissionAccessLog.allowed.is_(True)).label("allowed"),
|
||||
func.count().filter(PermissionAccessLog.allowed.is_(False)).label("denied"),
|
||||
).where(PermissionAccessLog.created_at >= hour_ago)
|
||||
)
|
||||
hour = hour_result.one()
|
||||
|
||||
# 总记录数
|
||||
total_result = await db.execute(
|
||||
select(func.count()).select_from(PermissionAccessLog)
|
||||
)
|
||||
total_logs = total_result.scalar() or 0
|
||||
|
||||
return {
|
||||
"total_logs": total_logs,
|
||||
"today": {
|
||||
"total_checks": today.total,
|
||||
"allowed_checks": today.allowed,
|
||||
"denied_checks": today.denied,
|
||||
"avg_elapsed_ms": round(float(today.avg_ms), 2),
|
||||
"max_elapsed_ms": round(float(today.max_ms), 2),
|
||||
"allow_rate": round(today.allowed / today.total * 100, 1) if today.total > 0 else 0,
|
||||
"deny_rate": round(today.denied / today.total * 100, 1) if today.total > 0 else 0,
|
||||
},
|
||||
"last_hour": {
|
||||
"total_checks": hour.total,
|
||||
"allowed_checks": hour.allowed,
|
||||
"denied_checks": hour.denied,
|
||||
"requests_per_minute": round(hour.total / 60, 1),
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user