From 1886765db878be87a5de2f936350d7564701621b Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Thu, 11 Jun 2026 11:06:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E7=B3=BB=E7=BB=9F=E7=9B=91?= =?UTF-8?q?=E6=B5=8B=E6=A8=A1=E5=9D=97=E5=B9=B6=E7=BB=9F=E4=B8=80=E8=BF=90?= =?UTF-8?q?=E7=BB=B4=E5=8C=96=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/permission_monitoring.py | 7 +- backend/app/core/api_permissions.py | 24 +++---- .../tests/test_permission_monitoring_api.py | 31 ++++++++ frontend/src/api/projectPermissions.ts | 4 +- frontend/src/components/Layout.vue | 8 ++- .../LayoutPermissionMonitoring.test.ts | 22 ++++++ .../components/PermissionIpLocations.test.ts | 14 +++- .../src/components/PermissionIpLocations.vue | 8 +-- .../components/PermissionMonitoring.test.ts | 24 +++++++ .../src/components/PermissionMonitoring.vue | 18 ++--- .../components/PermissionTrendCharts.test.ts | 25 +++++++ .../src/components/PermissionTrendCharts.vue | 10 +-- frontend/src/locales/zh-CN.ts | 3 +- frontend/src/router.test.ts | 19 ++++- frontend/src/router/index.ts | 23 +++--- frontend/src/types/api.ts | 8 +-- .../src/views/admin/ApiPermissions.test.ts | 3 +- frontend/src/views/admin/ApiPermissions.vue | 72 +------------------ .../views/admin/PermissionManagement.test.ts | 4 +- .../src/views/admin/PermissionManagement.vue | 20 +----- .../src/views/admin/SystemMonitoringPage.vue | 18 +++++ 21 files changed, 222 insertions(+), 143 deletions(-) create mode 100644 frontend/src/components/LayoutPermissionMonitoring.test.ts create mode 100644 frontend/src/components/PermissionTrendCharts.test.ts create mode 100644 frontend/src/views/admin/SystemMonitoringPage.vue diff --git a/backend/app/api/v1/permission_monitoring.py b/backend/app/api/v1/permission_monitoring.py index ee09ce29..19c2585e 100644 --- a/backend/app/api/v1/permission_monitoring.py +++ b/backend/app/api/v1/permission_monitoring.py @@ -14,7 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException, 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, is_system_admin, list_active_pm_study_ids +from app.core.deps import get_current_user, get_db_session, is_system_admin from app.core.permission_monitor import get_permission_monitor from app.models.permission_access_log import PermissionAccessLog from app.models.permission_metric_snapshot import PermissionMetricSnapshot @@ -39,10 +39,7 @@ class MonitoringScope: async def resolve_monitoring_scope(db: AsyncSession, current_user) -> MonitoringScope: if is_system_admin(current_user): return MonitoringScope(is_admin=True, study_ids=set()) - return MonitoringScope( - is_admin=False, - study_ids=await list_active_pm_study_ids(db, current_user.id), - ) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") def _apply_monitoring_scope_to_log_query(query, scope: MonitoringScope): diff --git a/backend/app/core/api_permissions.py b/backend/app/core/api_permissions.py index 25859c0f..28fbc53e 100644 --- a/backend/app/core/api_permissions.py +++ b/backend/app/core/api_permissions.py @@ -969,54 +969,54 @@ SYSTEM_PERMISSIONS: dict[str, dict] = { "description": "管理权限模板", "roles": ["ADMIN"], }, - # 权限监控 + # 系统监测 "system:monitoring:metrics": { "module": "system_monitoring", "action": "read", - "description": "查看权限监控指标", - "roles": ["ADMIN", "PM"], + "description": "查看系统监测指标", + "roles": ["ADMIN"], }, "system:monitoring:cache_stats": { "module": "system_monitoring", "action": "read", "description": "查看缓存统计", - "roles": ["ADMIN", "PM"], + "roles": ["ADMIN"], }, "system:monitoring:alerts": { "module": "system_monitoring", "action": "read", "description": "查看权限告警", - "roles": ["ADMIN", "PM"], + "roles": ["ADMIN"], }, "system:monitoring:health": { "module": "system_monitoring", "action": "read", "description": "查看权限系统健康状态", - "roles": ["ADMIN", "PM"], + "roles": ["ADMIN"], }, "system:monitoring:access_logs": { "module": "system_monitoring", "action": "read", "description": "查看权限访问日志", - "roles": ["ADMIN", "PM"], + "roles": ["ADMIN"], }, "system:monitoring:trends": { "module": "system_monitoring", "action": "read", "description": "查看权限趋势数据", - "roles": ["ADMIN", "PM"], + "roles": ["ADMIN"], }, "system:monitoring:reset_metrics": { "module": "system_monitoring", "action": "update", - "description": "重置监控指标", - "roles": ["ADMIN", "PM"], + "description": "重置系统监测指标", + "roles": ["ADMIN"], }, "system:monitoring:clear_alerts": { "module": "system_monitoring", "action": "update", "description": "清除告警", - "roles": ["ADMIN", "PM"], + "roles": ["ADMIN"], }, "system:monitoring:security_logs": { "module": "system_monitoring", @@ -1044,6 +1044,6 @@ SYSTEM_MODULE_LABELS: dict[str, str] = { "system_users": "账号管理", "system_projects": "项目管理", "system_permissions": "权限管理", - "system_monitoring": "权限监控", + "system_monitoring": "系统监测", "system_audit": "审计日志", } diff --git a/backend/tests/test_permission_monitoring_api.py b/backend/tests/test_permission_monitoring_api.py index ec650f97..f961d1d2 100644 --- a/backend/tests/test_permission_monitoring_api.py +++ b/backend/tests/test_permission_monitoring_api.py @@ -3,10 +3,12 @@ import uuid import pytest +from fastapi import HTTPException from sqlalchemy import text from app.core.permission_monitor import set_permission_monitor, PermissionMonitor from app.api.v1 import permission_monitoring +from app.api.v1.system_permissions import list_system_permissions class FakeIpInfo: @@ -22,6 +24,35 @@ class AdminUserStub: is_admin = True +class ProjectPmUserStub: + id = uuid.uuid4() + is_admin = False + + +@pytest.mark.asyncio +async def test_resolve_monitoring_scope_rejects_project_pm(db_session): + """系统监测模块仅允许系统管理员访问,项目 PM 不再具备监测范围。""" + with pytest.raises(HTTPException) as exc_info: + await permission_monitoring.resolve_monitoring_scope(db_session, ProjectPmUserStub()) + + assert exc_info.value.status_code == 403 + + +@pytest.mark.asyncio +async def test_system_permission_monitoring_definitions_are_admin_only(): + """系统级权限定义中的系统监测模块不应再标注 PM 角色。""" + data = await list_system_permissions() + monitoring_items = [ + item for item in data["permissions"] + if item["module"] == "system_monitoring" + ] + + assert monitoring_items + assert data["module_labels"]["system_monitoring"] == "系统监测" + assert all(item["roles"] == ["ADMIN"] for item in monitoring_items) + assert all("PM" not in item["roles"] for item in monitoring_items) + + async def _seed_permission_log(db_session, study_id: uuid.UUID, user_id: uuid.UUID, *, allowed: bool, elapsed_ms: float) -> None: study_exists = ( await db_session.execute(text("SELECT id FROM studies WHERE id = :id"), {"id": str(study_id)}) diff --git a/frontend/src/api/projectPermissions.ts b/frontend/src/api/projectPermissions.ts index 21e6d195..1edd936b 100644 --- a/frontend/src/api/projectPermissions.ts +++ b/frontend/src/api/projectPermissions.ts @@ -34,7 +34,7 @@ export const updateApiEndpointPermissions = ( export const fetchApiOperations = () => apiGet<{ operations: ApiOperation[] }>(`/api/v1/api-permissions/operations`); -// 权限系统监控API +// 系统监测API export const fetchPermissionMetrics = () => apiGet(`/api/v1/permission-monitoring/metrics`); @@ -55,7 +55,7 @@ export const resetPermissionMetrics = () => export const clearPermissionAlerts = () => apiPost(`/api/v1/permission-monitoring/clear-alerts`); -// 权限监控 - 新增API +// 系统监测 - 新增API export const fetchAccessLogs = (params: { study_id?: string; user_id?: string; diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue index 31bcb742..6545a018 100644 --- a/frontend/src/components/Layout.vue +++ b/frontend/src/components/Layout.vue @@ -28,6 +28,10 @@ {{ TEXT.menu.auditLogs }} + + + {{ TEXT.menu.systemMonitoring }} + 系统级权限 项目权限配置 - 权限监控 @@ -235,7 +238,7 @@ import { fetchSites } from "../api/sites"; import { TEXT } from "../locales"; import { User, Suitcase, House, Calendar, Flag, - CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton, Files, Key + CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton, Files, Key, DataAnalysis } from "@element-plus/icons-vue"; import { ElMessage, ElMessageBox } from "element-plus"; import { getProjectRoutePermission, hasProjectPermission, projectRouteLandingPaths } from "../utils/projectRoutePermissions"; @@ -301,6 +304,7 @@ const activeMenu = computed(() => { if (path.startsWith("/knowledge/instruction-files")) return "/knowledge/instruction-files"; if (path.startsWith("/projects/")) return "/admin/projects"; if (path.startsWith("/admin/projects/")) return "/admin/projects"; + if (path.startsWith("/admin/system-monitoring") || path.startsWith("/admin/permission-monitoring")) return "/admin/system-monitoring"; if (path.startsWith("/admin/permissions/")) return path; return path; }); diff --git a/frontend/src/components/LayoutPermissionMonitoring.test.ts b/frontend/src/components/LayoutPermissionMonitoring.test.ts new file mode 100644 index 00000000..3bfb9fe1 --- /dev/null +++ b/frontend/src/components/LayoutPermissionMonitoring.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const readSource = () => readFileSync(resolve(__dirname, "./Layout.vue"), "utf8"); + +describe("layout system monitoring navigation", () => { + it("shows system monitoring as an admin-only peer module outside permission management", () => { + const source = readSource(); + const permissionMenuStart = source.indexOf(''); + const permissionMenuEnd = source.indexOf("", permissionMenuStart); + const permissionMenu = source.slice(permissionMenuStart, permissionMenuEnd); + + expect(source).toContain('v-if="isAdmin" index="/admin/system-monitoring"'); + expect(source).toContain("{{ TEXT.menu.systemMonitoring }}"); + expect(permissionMenu).not.toContain("/admin/system-monitoring"); + expect(permissionMenu).not.toContain("/admin/permission-monitoring"); + expect(permissionMenu).not.toContain("/admin/permissions/monitoring"); + expect(source).not.toContain('index="/admin/permissions/monitoring"'); + expect(source).toContain('return "/admin/system-monitoring";'); + }); +}); diff --git a/frontend/src/components/PermissionIpLocations.test.ts b/frontend/src/components/PermissionIpLocations.test.ts index f9f832ea..bd848d54 100644 --- a/frontend/src/components/PermissionIpLocations.test.ts +++ b/frontend/src/components/PermissionIpLocations.test.ts @@ -21,13 +21,25 @@ describe("PermissionIpLocations", () => { expect(source).toContain("VChart"); expect(source).toContain("chinaMapOption"); - expect(source).toContain("用户分布图"); + expect(source).toContain("访问来源分布"); expect(source).toContain("访问次数"); expect(source).toContain("访问用户数"); expect(source).toContain("来源 IP 数"); expect(source).toContain("summary"); }); + it("uses source-analysis copy instead of IP-location tab wording", () => { + const source = readSource(); + + expect(source).toContain("访问来源分析"); + expect(source).toContain("来源分布"); + expect(source).toContain("热点来源地"); + expect(source).not.toContain("IP 属地分析"); + expect(source).not.toContain("中国地图"); + expect(source).not.toContain("热点属地"); + expect(source).not.toContain("用户分布图"); + }); + it("uses the standard geojson.cn China map data instead of generated rectangles", () => { const source = readSource(); const mapSource = readMapSource(); diff --git a/frontend/src/components/PermissionIpLocations.vue b/frontend/src/components/PermissionIpLocations.vue index 62686944..493b8e72 100644 --- a/frontend/src/components/PermissionIpLocations.vue +++ b/frontend/src/components/PermissionIpLocations.vue @@ -3,7 +3,7 @@
- IP 属地分析 + 访问来源分析 查看访问来源的地理分布
@@ -37,7 +37,7 @@
-

中国地图

+

来源分布

访问来源地理分布热力图

{{ currentPeriodLabel }} @@ -48,7 +48,7 @@