feat(rbac): seed usage stats permissions and schema

This commit is contained in:
wren
2026-05-09 20:08:22 +08:00
parent e8a93f25a6
commit be41863099
3 changed files with 360 additions and 0 deletions
@@ -179,6 +179,18 @@ class RbacAdminServiceImpl(IRbacAdminService):
"is_cache": True,
"meta": {"group": "settings"},
},
{
"route_path": "/usage-stats",
"route_name": "usage-stats",
"component": "usage-stats",
"route_title": "系统使用统计",
"icon": "ri-bar-chart-box-line",
"sort_order": 4,
"parent_path": "/settings",
"is_hidden": False,
"is_cache": True,
"meta": {"group": "settings"},
},
]
_MANAGEABLE_PERMISSION_BLUEPRINTS: list[dict[str, Any]] = [
@@ -193,6 +205,12 @@ class RbacAdminServiceImpl(IRbacAdminService):
{"permission_key": "doc_type:create:write", "display_name": "创建文档类型", "module": "doc_type", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/document-types", "route_path": "/document-types"},
{"permission_key": "doc_type:update:write", "display_name": "更新文档类型", "module": "doc_type", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/document-types/{id}", "route_path": "/document-types"},
{"permission_key": "doc_type:delete:delete", "display_name": "删除文档类型", "module": "doc_type", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/document-types/{id}", "route_path": "/document-types"},
{"permission_key": "usage_stats:overview:read", "display_name": "查看统计总览", "module": "usage_stats", "resource": "overview", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/overview", "route_path": "/usage-stats"},
{"permission_key": "usage_stats:trends:read", "display_name": "查看统计趋势", "module": "usage_stats", "resource": "trends", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/trends", "route_path": "/usage-stats"},
{"permission_key": "usage_stats:users:read", "display_name": "查看用户统计", "module": "usage_stats", "resource": "users", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/by-users", "route_path": "/usage-stats"},
{"permission_key": "usage_stats:departments:read", "display_name": "查看部门统计", "module": "usage_stats", "resource": "departments", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/by-departments", "route_path": "/usage-stats"},
{"permission_key": "usage_stats:areas:read", "display_name": "查看地区统计", "module": "usage_stats", "resource": "areas", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/by-areas", "route_path": "/usage-stats"},
{"permission_key": "usage_stats:details:read", "display_name": "查看统计明细", "module": "usage_stats", "resource": "details", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/details", "route_path": "/usage-stats"},
{"permission_key": "evaluation_group:list:read", "display_name": "评查点分组列表", "module": "evaluation_group", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/evaluation-point-groups", "route_path": "/rule-groups"},
{"permission_key": "evaluation_group:create:write", "display_name": "创建评查点分组", "module": "evaluation_group", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/evaluation-point-groups", "route_path": "/rule-groups"},
{"permission_key": "evaluation_group:update:write", "display_name": "更新评查点分组与绑定", "module": "evaluation_group", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/evaluation-point-groups/{id}", "route_path": "/rule-groups"},
+116
View File
@@ -0,0 +1,116 @@
BEGIN;
-- ============================================================================
-- LeAudit Platform Usage Stats / Login Audit Schema Patch
-- 目标:
-- 1. 补齐系统使用统计所需的登录事件明细表
-- 2. 为 sso_users 增加最近登录时间字段
-- 3. 为评查触发人统计补齐必要索引
-- 说明:
-- - 脚本设计为幂等,可重复执行
-- - 不依赖前端,可由 DBA 或运维先行执行
-- ============================================================================
-- --------------------------------------------------------------------------
-- 1. 用户最近登录时间
-- --------------------------------------------------------------------------
ALTER TABLE sso_users
ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ NULL;
COMMENT ON COLUMN sso_users.last_login_at IS '最近一次登录成功时间,用于用户维度统计与登录态审计';
CREATE INDEX IF NOT EXISTS idx_sso_users_last_login_at
ON sso_users(last_login_at DESC)
WHERE deleted_at IS NULL;
-- --------------------------------------------------------------------------
-- 2. 登录事件审计表
-- --------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS usage_login_events (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NULL,
sub VARCHAR(128) NULL,
username_snapshot VARCHAR(128) NULL,
nick_name_snapshot VARCHAR(128) NULL,
department_name_snapshot VARCHAR(255) NULL,
ou_id_snapshot VARCHAR(128) NULL,
ou_name_snapshot VARCHAR(255) NULL,
area_snapshot VARCHAR(64) NULL,
login_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
login_result VARCHAR(16) NOT NULL,
login_type VARCHAR(32) NOT NULL,
ip_address VARCHAR(64) NULL,
user_agent VARCHAR(1024) NULL,
client_type VARCHAR(32) NULL,
token_jti VARCHAR(128) NULL,
failure_reason VARCHAR(255) NULL,
extra JSONB NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT fk_usage_login_events_user
FOREIGN KEY (user_id) REFERENCES sso_users(id) ON DELETE SET NULL,
CONSTRAINT chk_usage_login_events_result
CHECK (login_result IN ('success', 'failed')),
CONSTRAINT chk_usage_login_events_type
CHECK (login_type IN ('password', 'oauth', 'unknown'))
);
COMMENT ON TABLE usage_login_events IS '系统登录事件审计表:记录成功/失败登录,用于系统使用统计';
COMMENT ON COLUMN usage_login_events.user_id IS '登录成功时关联用户 ID;失败时允许为空';
COMMENT ON COLUMN usage_login_events.sub IS '登录标识快照,通常对应 sso_users.sub';
COMMENT ON COLUMN usage_login_events.department_name_snapshot IS '登录时的部门名称快照';
COMMENT ON COLUMN usage_login_events.area_snapshot IS '登录时的地区快照';
COMMENT ON COLUMN usage_login_events.login_result IS '登录结果:success / failed';
COMMENT ON COLUMN usage_login_events.login_type IS '登录方式:password / oauth / unknown';
COMMENT ON COLUMN usage_login_events.token_jti IS 'JWT 唯一标识,当前版本可为空';
COMMENT ON COLUMN usage_login_events.extra IS '扩展信息,预留后续登录来源、设备信息等';
CREATE INDEX IF NOT EXISTS idx_usage_login_events_login_time
ON usage_login_events(login_time DESC);
CREATE INDEX IF NOT EXISTS idx_usage_login_events_user_id
ON usage_login_events(user_id, login_time DESC);
CREATE INDEX IF NOT EXISTS idx_usage_login_events_department
ON usage_login_events(department_name_snapshot, login_time DESC);
CREATE INDEX IF NOT EXISTS idx_usage_login_events_area
ON usage_login_events(area_snapshot, login_time DESC);
CREATE INDEX IF NOT EXISTS idx_usage_login_events_result
ON usage_login_events(login_result, login_time DESC);
CREATE INDEX IF NOT EXISTS idx_usage_login_events_type
ON usage_login_events(login_type, login_time DESC);
CREATE INDEX IF NOT EXISTS idx_usage_login_events_sub
ON usage_login_events(sub, login_time DESC);
-- --------------------------------------------------------------------------
-- 3. 评查运行触发人索引
-- --------------------------------------------------------------------------
CREATE INDEX IF NOT EXISTS idx_leaudit_audit_runs_trigger_user_id
ON leaudit_audit_runs(trigger_user_id, created_at DESC);
COMMENT ON COLUMN leaudit_audit_runs.trigger_user_id IS '发起本次评查的用户 ID,用于用户/部门/地区维度统计';
-- --------------------------------------------------------------------------
-- 4. 历史回填(仅在存在成功登录记录时回填)
-- --------------------------------------------------------------------------
WITH latest_success_login AS (
SELECT
user_id,
MAX(login_time) AS last_login_at
FROM usage_login_events
WHERE user_id IS NOT NULL
AND login_result = 'success'
GROUP BY user_id
)
UPDATE sso_users u
SET last_login_at = l.last_login_at
FROM latest_success_login l
WHERE u.id = l.user_id
AND (u.last_login_at IS NULL OR u.last_login_at < l.last_login_at);
COMMIT;
+226
View File
@@ -0,0 +1,226 @@
BEGIN;
-- ============================================================================
-- LeAudit Platform Usage Stats RBAC Seed
-- 目标:
-- 1. 为“系统使用统计”补齐菜单路由
-- 2. 补齐 usage-stats 相关 API 权限点
-- 3. 为 super_admin / provincial_admin / admin 分配菜单和权限
-- 说明:
-- - super_admin / provincial_admin 使用 ALL 数据范围
-- - admin 使用 DEPT 数据范围,对应地区管理员只看本地区
-- - 幂等脚本,可重复执行
-- ============================================================================
WITH settings_root AS (
SELECT id
FROM sys_routes
WHERE route_path = '/settings'
AND deleted_at IS NULL
LIMIT 1
)
INSERT INTO sys_routes (
route_path,
route_name,
component,
parent_id,
route_title,
icon,
sort_order,
is_hidden,
is_cache,
meta,
status,
created_at,
updated_at,
deleted_at
)
SELECT
'/usage-stats',
'usage-stats',
'usage-stats',
settings_root.id,
'系统使用统计',
'ri-bar-chart-box-line',
4,
FALSE,
TRUE,
'{"group":"settings"}'::jsonb,
0,
NOW(),
NOW(),
NULL
FROM settings_root
ON CONFLICT (route_path) WHERE deleted_at IS NULL
DO UPDATE SET
route_name = EXCLUDED.route_name,
component = EXCLUDED.component,
parent_id = EXCLUDED.parent_id,
route_title = EXCLUDED.route_title,
icon = EXCLUDED.icon,
sort_order = EXCLUDED.sort_order,
is_hidden = EXCLUDED.is_hidden,
is_cache = EXCLUDED.is_cache,
meta = EXCLUDED.meta,
status = 0,
updated_at = NOW(),
deleted_at = NULL;
WITH usage_route AS (
SELECT id FROM sys_routes WHERE route_path = '/usage-stats' AND deleted_at IS NULL LIMIT 1
)
INSERT INTO permissions (
permission_key,
module,
resource,
action,
description,
display_name,
permission_type,
is_system,
metadata,
created_at,
updated_at,
created_by,
updated_by,
parent_id,
sort_order,
route_id,
api_path,
api_method,
related_routes
)
SELECT
seed.permission_key,
seed.module,
seed.resource,
seed.action,
seed.description,
seed.display_name,
seed.permission_type,
seed.is_system,
seed.metadata,
seed.created_at,
seed.updated_at,
seed.created_by,
seed.updated_by,
seed.parent_id,
seed.sort_order,
usage_route.id AS route_id,
seed.api_path,
seed.api_method,
seed.related_routes
FROM (
VALUES
('usage_stats:overview:read', 'usage_stats', 'overview', 'read', '查看系统使用统计总览', '查看统计总览', 'API', TRUE, NULL::jsonb, NOW(), NOW(), NULL::bigint, NULL::bigint, NULL::bigint, 210, NULL::bigint, '/api/v3/usage-stats/overview', 'GET', NULL::bigint[]),
('usage_stats:trends:read', 'usage_stats', 'trends', 'read', '查看系统使用趋势', '查看统计趋势', 'API', TRUE, NULL::jsonb, NOW(), NOW(), NULL::bigint, NULL::bigint, NULL::bigint, 211, NULL::bigint, '/api/v3/usage-stats/trends', 'GET', NULL::bigint[]),
('usage_stats:users:read', 'usage_stats', 'users', 'read', '查看用户维度统计', '查看用户统计', 'API', TRUE, NULL::jsonb, NOW(), NOW(), NULL::bigint, NULL::bigint, NULL::bigint, 212, NULL::bigint, '/api/v3/usage-stats/by-users', 'GET', NULL::bigint[]),
('usage_stats:departments:read', 'usage_stats', 'departments', 'read', '查看部门维度统计', '查看部门统计', 'API', TRUE, NULL::jsonb, NOW(), NOW(), NULL::bigint, NULL::bigint, NULL::bigint, 213, NULL::bigint, '/api/v3/usage-stats/by-departments', 'GET', NULL::bigint[]),
('usage_stats:areas:read', 'usage_stats', 'areas', 'read', '查看地区维度统计', '查看地区统计', 'API', TRUE, NULL::jsonb, NOW(), NOW(), NULL::bigint, NULL::bigint, NULL::bigint, 214, NULL::bigint, '/api/v3/usage-stats/by-areas', 'GET', NULL::bigint[]),
('usage_stats:details:read', 'usage_stats', 'details', 'read', '查看统计明细', '查看统计明细', 'API', TRUE, NULL::jsonb, NOW(), NOW(), NULL::bigint, NULL::bigint, NULL::bigint, 215, NULL::bigint, '/api/v3/usage-stats/details', 'GET', NULL::bigint[])
) AS seed(
permission_key,
module,
resource,
action,
description,
display_name,
permission_type,
is_system,
metadata,
created_at,
updated_at,
created_by,
updated_by,
parent_id,
sort_order,
route_id,
api_path,
api_method,
related_routes
)
CROSS JOIN usage_route
ON CONFLICT (permission_key) DO UPDATE SET
module = EXCLUDED.module,
resource = EXCLUDED.resource,
action = EXCLUDED.action,
description = EXCLUDED.description,
display_name = EXCLUDED.display_name,
permission_type = EXCLUDED.permission_type,
is_system = EXCLUDED.is_system,
updated_at = NOW(),
api_path = EXCLUDED.api_path,
api_method = EXCLUDED.api_method,
sort_order = EXCLUDED.sort_order;
WITH role_map AS (
SELECT id, role_key
FROM roles
WHERE role_key IN ('super_admin', 'provincial_admin', 'admin')
),
route_map AS (
SELECT id, route_path
FROM sys_routes
WHERE deleted_at IS NULL
AND route_path = '/usage-stats'
),
seed(role_key, route_path, permission, status) AS (
VALUES
('super_admin', '/usage-stats', 'R', 1),
('provincial_admin', '/usage-stats', 'R', 1),
('admin', '/usage-stats', 'R', 1)
)
INSERT INTO role_route (role_id, route_id, permission, status, created_at, updated_at)
SELECT rm.id, tm.id, s.permission, s.status, NOW(), NOW()
FROM seed s
JOIN role_map rm ON rm.role_key = s.role_key
JOIN route_map tm ON tm.route_path = s.route_path
ON CONFLICT (role_id, route_id) DO UPDATE SET
permission = EXCLUDED.permission,
status = EXCLUDED.status,
updated_at = NOW();
WITH role_map AS (
SELECT id, role_key
FROM roles
WHERE role_key IN ('super_admin', 'provincial_admin', 'admin')
),
perm_map AS (
SELECT id, permission_key
FROM permissions
WHERE permission_key LIKE 'usage_stats:%'
),
seed(role_key, permission_key, grant_type, data_scope) AS (
VALUES
('super_admin', 'usage_stats:overview:read', 'GRANT', 'ALL'),
('super_admin', 'usage_stats:trends:read', 'GRANT', 'ALL'),
('super_admin', 'usage_stats:users:read', 'GRANT', 'ALL'),
('super_admin', 'usage_stats:departments:read', 'GRANT', 'ALL'),
('super_admin', 'usage_stats:areas:read', 'GRANT', 'ALL'),
('super_admin', 'usage_stats:details:read', 'GRANT', 'ALL'),
('provincial_admin', 'usage_stats:overview:read', 'GRANT', 'ALL'),
('provincial_admin', 'usage_stats:trends:read', 'GRANT', 'ALL'),
('provincial_admin', 'usage_stats:users:read', 'GRANT', 'ALL'),
('provincial_admin', 'usage_stats:departments:read', 'GRANT', 'ALL'),
('provincial_admin', 'usage_stats:areas:read', 'GRANT', 'ALL'),
('provincial_admin', 'usage_stats:details:read', 'GRANT', 'ALL'),
('admin', 'usage_stats:overview:read', 'GRANT', 'DEPT'),
('admin', 'usage_stats:trends:read', 'GRANT', 'DEPT'),
('admin', 'usage_stats:users:read', 'GRANT', 'DEPT'),
('admin', 'usage_stats:departments:read', 'GRANT', 'DEPT'),
('admin', 'usage_stats:areas:read', 'GRANT', 'DEPT'),
('admin', 'usage_stats:details:read', 'GRANT', 'DEPT')
)
INSERT INTO role_permissions (role_id, permission_id, grant_type, data_scope, created_at, updated_at)
SELECT rm.id, pm.id, seed.grant_type, seed.data_scope, NOW(), NOW()
FROM seed
JOIN role_map rm ON rm.role_key = seed.role_key
JOIN perm_map pm ON pm.permission_key = seed.permission_key
ON CONFLICT (role_id, permission_id) DO UPDATE SET
grant_type = EXCLUDED.grant_type,
data_scope = EXCLUDED.data_scope,
updated_at = NOW();
COMMIT;