Files
hms/crates/erp-core/src/module.rs
iven a6d3a0efcc feat(plugin): 实现插件权限注册,install 时写入 permissions 表、uninstall 时软删除
跨 crate 方案:erp-plugin 使用 raw SQL 操作 permissions 表,
避免直接依赖 erp-auth entity,保持模块间松耦合。

- erp-core: 新增 PermissionDescriptor 类型和 ErpModule::permissions() 方法
- erp-plugin service.rs install(): 解析 manifest.permissions,INSERT ON CONFLICT DO NOTHING
- erp-plugin service.rs uninstall(): 软删除 role_permissions 关联 + permissions 记录
2026-04-16 12:42:13 +08:00

358 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
use crate::error::{AppError, AppResult};
use crate::events::EventBus;
/// 权限描述符,用于模块声明自己需要的权限。
///
/// 各业务模块通过 `ErpModule::permissions()` 返回此列表,
/// 由 erp-server 在启动时统一注册到权限表。
#[derive(Clone, Debug)]
pub struct PermissionDescriptor {
/// 权限编码,全局唯一,格式建议 `{模块}.{动作}` 如 `plugin.admin`
pub code: String,
/// 权限显示名称
pub name: String,
/// 权限描述
pub description: String,
/// 所属模块名称
pub module: String,
}
/// 模块类型
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleType {
/// 内置模块(编译时链接)
Builtin,
/// 插件模块(运行时加载)
Plugin,
}
/// 模块启动上下文 — 在 on_startup 时提供给模块
pub struct ModuleContext {
pub db: sea_orm::DatabaseConnection,
pub event_bus: EventBus,
}
/// 模块注册接口
/// 所有业务模块Auth, Workflow, Message, Config, 行业模块)都实现此 trait
#[async_trait::async_trait]
pub trait ErpModule: Send + Sync {
/// 模块名称(唯一标识)
fn name(&self) -> &str;
/// 模块唯一 ID默认等于 name
fn id(&self) -> &str {
self.name()
}
/// 模块版本
fn version(&self) -> &str {
env!("CARGO_PKG_VERSION")
}
/// 模块类型
fn module_type(&self) -> ModuleType {
ModuleType::Builtin
}
/// 依赖的其他模块名称
fn dependencies(&self) -> Vec<&str> {
vec![]
}
/// 注册事件处理器
fn register_event_handlers(&self, _bus: &EventBus) {}
/// 模块启动钩子 — 服务启动时调用
async fn on_startup(&self, _ctx: &ModuleContext) -> AppResult<()> {
Ok(())
}
/// 模块关闭钩子 — 服务关闭时调用
async fn on_shutdown(&self) -> AppResult<()> {
Ok(())
}
/// 健康检查
async fn health_check(&self) -> AppResult<serde_json::Value> {
Ok(serde_json::json!({"status": "healthy"}))
}
/// 租户创建时的初始化钩子。
///
/// 用于为新建租户创建默认角色、管理员用户等初始数据。
async fn on_tenant_created(
&self,
_tenant_id: Uuid,
_db: &sea_orm::DatabaseConnection,
_event_bus: &EventBus,
) -> AppResult<()> {
Ok(())
}
/// 租户删除时的清理钩子。
///
/// 用于软删除该租户下的所有关联数据。
async fn on_tenant_deleted(
&self,
_tenant_id: Uuid,
_db: &sea_orm::DatabaseConnection,
) -> AppResult<()> {
Ok(())
}
/// 返回此模块需要注册的权限列表。
///
/// 默认返回空列表,有权限需求的模块(如 plugin可覆写此方法。
fn permissions(&self) -> Vec<PermissionDescriptor> {
vec![]
}
/// Downcast support: return `self` as `&dyn Any` for concrete type access.
///
/// This allows the server crate to retrieve module-specific methods
/// (e.g. `AuthModule::public_routes()`) that are not part of the trait.
fn as_any(&self) -> &dyn Any;
}
/// 模块注册器 — 用 Arc 包装使其可 Clone用于 Axum State
#[derive(Clone, Default)]
pub struct ModuleRegistry {
modules: Arc<Vec<Arc<dyn ErpModule>>>,
}
impl ModuleRegistry {
pub fn new() -> Self {
Self {
modules: Arc::new(vec![]),
}
}
pub fn register(mut self, module: impl ErpModule + 'static) -> Self {
tracing::info!(
module = module.name(),
id = module.id(),
version = module.version(),
module_type = ?module.module_type(),
"Module registered"
);
let mut modules = (*self.modules).clone();
modules.push(Arc::new(module));
self.modules = Arc::new(modules);
self
}
pub fn register_handlers(&self, bus: &EventBus) {
for module in self.modules.iter() {
module.register_event_handlers(bus);
}
}
pub fn modules(&self) -> &[Arc<dyn ErpModule>] {
&self.modules
}
/// 按名称获取模块
pub fn get_module(&self, name: &str) -> Option<Arc<dyn ErpModule>> {
self.modules.iter().find(|m| m.name() == name).cloned()
}
/// 按拓扑排序返回模块(依赖在前,被依赖在后)
///
/// 使用 Kahn 算法,环检测返回 Validation 错误。
pub fn sorted_modules(&self) -> AppResult<Vec<Arc<dyn ErpModule>>> {
let modules = &*self.modules;
let n = modules.len();
if n == 0 {
return Ok(vec![]);
}
// 构建名称到索引的映射
let name_to_idx: HashMap<&str, usize> = modules
.iter()
.enumerate()
.map(|(i, m)| (m.name(), i))
.collect();
// 构建邻接表和入度
let mut adjacency: Vec<Vec<usize>> = vec![vec![]; n];
let mut in_degree: Vec<usize> = vec![0; n];
for (idx, module) in modules.iter().enumerate() {
for dep in module.dependencies() {
if let Some(&dep_idx) = name_to_idx.get(dep) {
adjacency[dep_idx].push(idx);
in_degree[idx] += 1;
}
// 依赖未注册的模块不阻断(可能是可选依赖)
}
}
// Kahn 算法
let mut queue: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
let mut sorted_indices = Vec::with_capacity(n);
while let Some(idx) = queue.pop() {
sorted_indices.push(idx);
for &next in &adjacency[idx] {
in_degree[next] -= 1;
if in_degree[next] == 0 {
queue.push(next);
}
}
}
if sorted_indices.len() != n {
let cycle_modules: Vec<&str> = (0..n)
.filter(|i| !sorted_indices.contains(i))
.filter_map(|i| modules.get(i).map(|m| m.name()))
.collect();
return Err(AppError::Validation(format!(
"模块依赖存在循环: {}",
cycle_modules.join(", ")
)));
}
Ok(sorted_indices
.into_iter()
.map(|i| modules[i].clone())
.collect())
}
/// 按拓扑顺序启动所有模块
pub async fn startup_all(&self, ctx: &ModuleContext) -> AppResult<()> {
let sorted = self.sorted_modules()?;
for module in sorted {
tracing::info!(module = module.name(), "Starting module");
module.on_startup(ctx).await?;
tracing::info!(module = module.name(), "Module started");
}
Ok(())
}
/// 按拓扑逆序关闭所有模块
pub async fn shutdown_all(&self) -> AppResult<()> {
let sorted = self.sorted_modules()?;
for module in sorted.into_iter().rev() {
tracing::info!(module = module.name(), "Shutting down module");
if let Err(e) = module.on_shutdown().await {
tracing::error!(module = module.name(), error = %e, "Module shutdown failed");
}
}
Ok(())
}
/// 对所有模块执行健康检查
pub async fn health_check_all(&self) -> Vec<(String, AppResult<serde_json::Value>)> {
let mut results = Vec::with_capacity(self.modules.len());
for module in self.modules.iter() {
let result = module.health_check().await;
results.push((module.name().to_string(), result));
}
results
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestModule {
name: &'static str,
deps: Vec<&'static str>,
}
#[async_trait::async_trait]
impl ErpModule for TestModule {
fn name(&self) -> &str {
self.name
}
fn dependencies(&self) -> Vec<&str> {
self.deps.clone()
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[test]
fn sorted_modules_empty() {
let registry = ModuleRegistry::new();
let sorted = registry.sorted_modules().unwrap();
assert!(sorted.is_empty());
}
#[test]
fn sorted_modules_no_deps() {
let registry = ModuleRegistry::new()
.register(TestModule {
name: "a",
deps: vec![],
})
.register(TestModule {
name: "b",
deps: vec![],
});
let sorted = registry.sorted_modules().unwrap();
assert_eq!(sorted.len(), 2);
}
#[test]
fn sorted_modules_with_deps() {
let registry = ModuleRegistry::new()
.register(TestModule {
name: "auth",
deps: vec![],
})
.register(TestModule {
name: "plugin",
deps: vec!["auth", "config"],
})
.register(TestModule {
name: "config",
deps: vec!["auth"],
});
let sorted = registry.sorted_modules().unwrap();
let names: Vec<&str> = sorted.iter().map(|m| m.name()).collect();
let auth_pos = names.iter().position(|&n| n == "auth").unwrap();
let config_pos = names.iter().position(|&n| n == "config").unwrap();
let plugin_pos = names.iter().position(|&n| n == "plugin").unwrap();
assert!(auth_pos < config_pos);
assert!(config_pos < plugin_pos);
}
#[test]
fn sorted_modules_circular_dep() {
let registry = ModuleRegistry::new()
.register(TestModule {
name: "a",
deps: vec!["b"],
})
.register(TestModule {
name: "b",
deps: vec!["a"],
});
let result = registry.sorted_modules();
assert!(result.is_err());
match result.err().unwrap() {
AppError::Validation(msg) => assert!(msg.contains("循环")),
other => panic!("Expected Validation, got {:?}", other),
}
}
#[test]
fn get_module_found() {
let registry = ModuleRegistry::new().register(TestModule {
name: "auth",
deps: vec![],
});
assert!(registry.get_module("auth").is_some());
assert!(registry.get_module("unknown").is_none());
}
}