跨 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 记录
358 lines
10 KiB
Rust
358 lines
10 KiB
Rust
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());
|
||
}
|
||
}
|