fix(db,ci): 补全 26 个缺失权限码 seed 注册 + 检查脚本增强
- 新增迁移 000144 全实体乐观锁 version 字段强制化 - 新增迁移 000145 注册 26 个后端已声明但 seed 缺失的权限码 (ai.analysis/prompt/suggestion/usage/provider, copilot.insights/risk/rules, health.ble-gateways/critical-alerts/devices/family-proxy/shifts 等) - check-permissions.sh: 增加 module.rs PermissionDescriptor 提取, 支持两段式权限码 (plugin.admin/tenant.manage) - CI 检查结果: Check 1 PASS, Check 2 PASS, 0 个不一致
This commit is contained in:
@@ -145,6 +145,8 @@ mod m20260512_000140_create_copilot_risk_snapshots;
|
||||
mod m20260512_000141_create_copilot_chat_logs;
|
||||
mod m20260512_000142_seed_copilot_rules;
|
||||
mod m20260512_000143_seed_copilot_alert_rules;
|
||||
mod m20260513_000144_enforce_version_optimistic_lock;
|
||||
mod m20260513_000145_seed_missing_permissions;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -297,6 +299,8 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260512_000141_create_copilot_chat_logs::Migration),
|
||||
Box::new(m20260512_000142_seed_copilot_rules::Migration),
|
||||
Box::new(m20260512_000143_seed_copilot_alert_rules::Migration),
|
||||
Box::new(m20260513_000144_enforce_version_optimistic_lock::Migration),
|
||||
Box::new(m20260513_000145_seed_missing_permissions::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
// 1. 创建触发器函数:适用于 `version` 列(erp-health / erp-auth / erp-config 等所有模块)
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
CREATE OR REPLACE FUNCTION enforce_version() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF NEW.version IS DISTINCT FROM OLD.version + 1 THEN
|
||||
RAISE EXCEPTION 'Optimistic lock conflict on %: expected version %, got %',
|
||||
TG_TABLE_NAME, OLD.version + 1, NEW.version;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 2. 创建触发器函数:适用于 `version_lock` 列(erp-ai 模块)
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
CREATE OR REPLACE FUNCTION enforce_version_lock() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF NEW.version_lock IS DISTINCT FROM OLD.version_lock + 1 THEN
|
||||
RAISE EXCEPTION 'Optimistic lock conflict on %: expected version_lock %, got %',
|
||||
TG_TABLE_NAME, OLD.version_lock + 1, NEW.version_lock;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 3. 自动发现所有含 version / version_lock 的表并绑定触发器
|
||||
// 排除 market_entry(version 是 String,非乐观锁)
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
DO $$
|
||||
DECLARE
|
||||
rec RECORD;
|
||||
trig_name text;
|
||||
BEGIN
|
||||
FOR rec IN
|
||||
SELECT table_name, column_name
|
||||
FROM information_schema.columns
|
||||
WHERE column_name IN ('version', 'version_lock')
|
||||
AND table_schema = 'public'
|
||||
AND table_name NOT IN ('market_entry', 'process_definitions', 'ai_prompt')
|
||||
LOOP
|
||||
IF rec.column_name = 'version' THEN
|
||||
trig_name := 'trg_enforce_version';
|
||||
EXECUTE format(
|
||||
'CREATE TRIGGER %I BEFORE UPDATE ON %I
|
||||
FOR EACH ROW EXECUTE FUNCTION enforce_version()',
|
||||
trig_name, rec.table_name
|
||||
);
|
||||
ELSE
|
||||
trig_name := 'trg_enforce_version_lock';
|
||||
EXECUTE format(
|
||||
'CREATE TRIGGER %I BEFORE UPDATE ON %I
|
||||
FOR EACH ROW EXECUTE FUNCTION enforce_version_lock()',
|
||||
trig_name, rec.table_name
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
// 1. 删除所有乐观锁触发器
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
DO $$
|
||||
DECLARE
|
||||
rec RECORD;
|
||||
BEGIN
|
||||
FOR rec IN
|
||||
SELECT event_object_table AS table_name, trigger_name
|
||||
FROM information_schema.triggers
|
||||
WHERE trigger_name IN ('trg_enforce_version', 'trg_enforce_version_lock')
|
||||
LOOP
|
||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I', rec.trigger_name, rec.table_name);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 2. 删除触发器函数
|
||||
db.execute_unprepared("DROP FUNCTION IF EXISTS enforce_version() CASCADE")
|
||||
.await?;
|
||||
db.execute_unprepared("DROP FUNCTION IF EXISTS enforce_version_lock() CASCADE")
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
//! 补全缺失权限码注册
|
||||
//!
|
||||
//! CI check-permissions.sh 发现 23 个后端 handler 已使用但 seed 迁移未注册的权限码。
|
||||
//! 本迁移统一补注册到 permissions 表,并绑定 admin 角色。
|
||||
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
let sys = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
// (code, name, resource, action)
|
||||
let permissions: &[(&str, &str, &str, &str)] = &[
|
||||
// AI 模块
|
||||
("ai.analysis.list", "查看 AI 分析", "ai", "analysis.list"),
|
||||
(
|
||||
"ai.analysis.manage",
|
||||
"管理 AI 分析",
|
||||
"ai",
|
||||
"analysis.manage",
|
||||
),
|
||||
("ai.prompt.list", "查看 AI 提示词", "ai", "prompt.list"),
|
||||
("ai.prompt.manage", "管理 AI 提示词", "ai", "prompt.manage"),
|
||||
(
|
||||
"ai.provider.manage",
|
||||
"管理 AI Provider",
|
||||
"ai",
|
||||
"provider.manage",
|
||||
),
|
||||
(
|
||||
"ai.suggestion.list",
|
||||
"查看 AI 建议",
|
||||
"ai",
|
||||
"suggestion.list",
|
||||
),
|
||||
(
|
||||
"ai.suggestion.manage",
|
||||
"管理 AI 建议",
|
||||
"ai",
|
||||
"suggestion.manage",
|
||||
),
|
||||
("ai.usage.list", "查看 AI 用量", "ai", "usage.list"),
|
||||
// Copilot
|
||||
(
|
||||
"copilot.insights.list",
|
||||
"查看 Copilot 洞察",
|
||||
"copilot",
|
||||
"insights.list",
|
||||
),
|
||||
(
|
||||
"copilot.insights.manage",
|
||||
"管理 Copilot 洞察",
|
||||
"copilot",
|
||||
"insights.manage",
|
||||
),
|
||||
("copilot.risk.view", "查看风险快照", "copilot", "risk.view"),
|
||||
(
|
||||
"copilot.rules.list",
|
||||
"查看 Copilot 规则",
|
||||
"copilot",
|
||||
"rules.list",
|
||||
),
|
||||
(
|
||||
"copilot.rules.manage",
|
||||
"管理 Copilot 规则",
|
||||
"copilot",
|
||||
"rules.manage",
|
||||
),
|
||||
// Health — IoT/设备
|
||||
(
|
||||
"health.ble-gateways.manage",
|
||||
"管理 BLE 网关",
|
||||
"health",
|
||||
"ble-gateways.manage",
|
||||
),
|
||||
(
|
||||
"health.critical-alerts.manage",
|
||||
"管理危急值告警",
|
||||
"health",
|
||||
"critical-alerts.manage",
|
||||
),
|
||||
(
|
||||
"health.critical-value-thresholds.manage",
|
||||
"管理危急值阈值",
|
||||
"health",
|
||||
"critical-value-thresholds.manage",
|
||||
),
|
||||
(
|
||||
"health.device-readings.manage",
|
||||
"管理设备读数",
|
||||
"health",
|
||||
"device-readings.manage",
|
||||
),
|
||||
(
|
||||
"health.devices.manage",
|
||||
"管理患者设备",
|
||||
"health",
|
||||
"devices.manage",
|
||||
),
|
||||
(
|
||||
"health.medication-reminders.manage",
|
||||
"管理药物提醒",
|
||||
"health",
|
||||
"medication-reminders.manage",
|
||||
),
|
||||
// Health — 其他
|
||||
(
|
||||
"health.family-proxy.list",
|
||||
"查看家庭健康代理",
|
||||
"health",
|
||||
"family-proxy.list",
|
||||
),
|
||||
(
|
||||
"health.family-proxy.manage",
|
||||
"管理家庭健康代理",
|
||||
"health",
|
||||
"family-proxy.manage",
|
||||
),
|
||||
(
|
||||
"health.oauth.manage",
|
||||
"管理 OAuth 合作方",
|
||||
"health",
|
||||
"oauth.manage",
|
||||
),
|
||||
(
|
||||
"health.shifts.manage",
|
||||
"管理班次",
|
||||
"health",
|
||||
"shifts.manage",
|
||||
),
|
||||
// Plugin / Tenant
|
||||
("plugin.admin", "插件管理", "plugin", "admin"),
|
||||
("plugin.list", "查看插件列表", "plugin", "list"),
|
||||
("tenant.manage", "租户管理", "tenant", "manage"),
|
||||
];
|
||||
|
||||
for &(code, name, resource, action) in permissions {
|
||||
db.execute_unprepared(&format!(
|
||||
r#"
|
||||
INSERT INTO permissions (id, tenant_id, code, name, resource, action, description,
|
||||
created_at, updated_at, created_by, updated_by, deleted_at, version)
|
||||
SELECT gen_random_uuid(), t.id, '{code}', '{name}', '{resource}', '{action}', '{name}',
|
||||
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
|
||||
FROM tenant t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM permissions p
|
||||
WHERE p.code = '{code}' AND p.tenant_id = t.id AND p.deleted_at IS NULL
|
||||
)
|
||||
"#
|
||||
)).await?;
|
||||
}
|
||||
|
||||
// 绑定所有新权限到 admin 角色
|
||||
let codes: Vec<&str> = permissions.iter().map(|p| p.0).collect();
|
||||
let codes_sql = codes
|
||||
.iter()
|
||||
.map(|c| format!("'{c}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
db.execute_unprepared(&format!(
|
||||
r#"
|
||||
INSERT INTO role_permissions (role_id, permission_id, tenant_id, created_by, updated_by, version)
|
||||
SELECT r.id, p.id, t.id, r.id, r.id, 1
|
||||
FROM tenant t
|
||||
JOIN roles r ON r.tenant_id = t.id AND r.code = 'admin' AND r.deleted_at IS NULL
|
||||
JOIN permissions p ON p.tenant_id = t.id
|
||||
AND p.code IN ({codes_sql})
|
||||
AND p.deleted_at IS NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM role_permissions rp
|
||||
WHERE rp.permission_id = p.id AND rp.role_id = r.id
|
||||
)
|
||||
"#
|
||||
)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
let codes = [
|
||||
"ai.analysis.list",
|
||||
"ai.analysis.manage",
|
||||
"ai.prompt.list",
|
||||
"ai.prompt.manage",
|
||||
"ai.provider.manage",
|
||||
"ai.suggestion.list",
|
||||
"ai.suggestion.manage",
|
||||
"ai.usage.list",
|
||||
"copilot.insights.list",
|
||||
"copilot.insights.manage",
|
||||
"copilot.risk.view",
|
||||
"copilot.rules.list",
|
||||
"copilot.rules.manage",
|
||||
"health.ble-gateways.manage",
|
||||
"health.critical-alerts.manage",
|
||||
"health.critical-value-thresholds.manage",
|
||||
"health.device-readings.manage",
|
||||
"health.devices.manage",
|
||||
"health.family-proxy.list",
|
||||
"health.family-proxy.manage",
|
||||
"health.medication-reminders.manage",
|
||||
"health.oauth.manage",
|
||||
"health.shifts.manage",
|
||||
"plugin.admin",
|
||||
"plugin.list",
|
||||
"tenant.manage",
|
||||
];
|
||||
let codes_sql = codes
|
||||
.iter()
|
||||
.map(|c| format!("'{c}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
db.execute_unprepared(&format!(
|
||||
"DELETE FROM role_permissions WHERE permission_id IN \
|
||||
(SELECT id FROM permissions WHERE code IN ({codes_sql}))"
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
db.execute_unprepared(&format!(
|
||||
"DELETE FROM permissions WHERE code IN ({codes_sql})"
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -28,22 +28,27 @@ echo " 权限注册完整性检查"
|
||||
echo "=========================================="
|
||||
|
||||
# --- 提取后端 handler 权限码 ---
|
||||
# 1) require_permission 调用
|
||||
grep -roh 'require_permission.*"[^"]*"' crates/ --include="*.rs" \
|
||||
| grep -oE '"[^"]*"' | tr -d '"' | sort -u > "$BACKEND_PERMS"
|
||||
# 2) module.rs 中 PermissionDescriptor 声明的 code 字段
|
||||
grep -roh 'code: *"[^"]*"' crates/ --include="*.rs" \
|
||||
| grep -oE '"[^"]*\.[^"]*\.[^"]*"' | tr -d '"' | sort -u >> "$BACKEND_PERMS"
|
||||
# 去重
|
||||
cat "$BACKEND_PERMS" | sort -u > "${BACKEND_PERMS}.tmp" && mv "${BACKEND_PERMS}.tmp" "$BACKEND_PERMS"
|
||||
|
||||
# --- 提取前端 routeConfig 权限码 ---
|
||||
grep -oE '"[a-z][-a-z0-9]*\.[a-z][-a-z0-9]*\.[a-z][-a-z0-9]*"' \
|
||||
apps/web/src/routeConfig.ts | tr -d '"' | sort -u > "$FRONTEND_PERMS"
|
||||
|
||||
# --- 提取 seed 迁移权限码 ---
|
||||
grep -roh 'health\.[a-z_-]*\.[a-z_-]*' crates/erp-server/migration/src/ --include="*.rs" \
|
||||
| grep -vE 'fn |mod |use |struct |impl |async |let |pub |self|super|crate' \
|
||||
| sort -u > "$SEED_PERMS"
|
||||
# 也提取非 health 前缀的
|
||||
grep -roh '(user|role|workflow|message|setting|plugin|department|organization|position|dictionary|menu|numbering|theme|language|tenant|ai)\.[a-z_-]*\.[a-z_-]*' \
|
||||
# 匹配三段式(health.patient.list)和两段式(plugin.admin)权限码
|
||||
grep -rohE '[a-z][-a-z0-9]*\.[a-z][-a-z0-9]*(\.[a-z][-a-z0-9]*)?' \
|
||||
crates/erp-server/migration/src/ --include="*.rs" \
|
||||
| grep -vE 'fn |mod |use |struct |impl |async |let |pub |self|super|crate' \
|
||||
| sort -u >> "$SEED_PERMS"
|
||||
| grep -E '^(user|role|workflow|message|setting|plugin|department|organization|position|dictionary|menu|numbering|theme|language|tenant|ai|copilot|health)' \
|
||||
| grep -v '\.(rs|sql|md|toml)$' \
|
||||
| sort -u > "$SEED_PERMS"
|
||||
# 提取 handler 中的非 health 权限码也加入 seed 对比
|
||||
grep -roh 'require_permission.*"[^"]*"' crates/erp-auth/ crates/erp-config/ crates/erp-workflow/ crates/erp-message/ --include="*.rs" \
|
||||
| grep -oE '"[^"]*"' | tr -d '"' | sort -u >> "$SEED_PERMS"
|
||||
|
||||
Reference in New Issue
Block a user