feat(plugin): 集成 WASM 插件系统到主服务并修复链路问题

- 新增 erp-plugin crate:插件管理、WASM 运行时、动态表、数据 CRUD
- 新增前端插件管理页面(PluginAdmin/PluginCRUDPage)和 API 层
- 新增插件数据迁移(plugins/plugin_entities/plugin_event_subscriptions)
- 新增权限补充迁移(为已有租户补充 plugin.admin/plugin.list 权限)
- 修复 PluginAdmin 页面 InstallOutlined 图标不存在的崩溃问题
- 修复 settings 唯一索引迁移顺序错误(先去重再建索引)
- 更新 wiki 和 CLAUDE.md 反映插件系统集成状态
- 新增 dev.ps1 一键启动脚本
This commit is contained in:
iven
2026-04-15 23:32:02 +08:00
parent 7e8fabb095
commit ff352a4c24
46 changed files with 6723 additions and 19 deletions

View File

@@ -1,7 +1,7 @@
use chrono::Utc;
use sea_orm::{ActiveModelTrait, Set};
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
use tokio::sync::{broadcast, mpsc};
use tracing::{error, info};
use uuid::Uuid;
@@ -31,6 +31,32 @@ impl DomainEvent {
}
}
/// 过滤事件接收器 — 只接收匹配 `event_type_prefix` 的事件
pub struct FilteredEventReceiver {
receiver: mpsc::Receiver<DomainEvent>,
}
impl FilteredEventReceiver {
/// 接收下一个匹配的事件
pub async fn recv(&mut self) -> Option<DomainEvent> {
self.receiver.recv().await
}
}
/// 订阅句柄 — 用于取消过滤订阅
pub struct SubscriptionHandle {
cancel_tx: mpsc::Sender<()>,
join_handle: tokio::task::JoinHandle<()>,
}
impl SubscriptionHandle {
/// 取消订阅并等待后台任务结束
pub async fn cancel(self) {
let _ = self.cancel_tx.send(()).await;
let _ = self.join_handle.await;
}
}
/// 进程内事件总线
#[derive(Clone)]
pub struct EventBus {
@@ -84,4 +110,57 @@ impl EventBus {
pub fn subscribe(&self) -> broadcast::Receiver<DomainEvent> {
self.sender.subscribe()
}
/// 按事件类型前缀过滤订阅。
///
/// 为每次调用 spawn 一个 Tokio task 从 broadcast channel 读取,
/// 只转发匹配 `event_type_prefix` 的事件到 mpsc channelcapacity 256
pub fn subscribe_filtered(
&self,
event_type_prefix: String,
) -> (FilteredEventReceiver, SubscriptionHandle) {
let mut broadcast_rx = self.sender.subscribe();
let (mpsc_tx, mpsc_rx) = mpsc::channel(256);
let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1);
let prefix = event_type_prefix.clone();
let join_handle = tokio::spawn(async move {
loop {
tokio::select! {
biased;
_ = cancel_rx.recv() => {
tracing::info!(prefix = %prefix, "Filtered subscription cancelled");
break;
}
event = broadcast_rx.recv() => {
match event {
Ok(event) => {
if event.event_type.starts_with(&prefix) {
if mpsc_tx.send(event).await.is_err() {
break;
}
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!(prefix = %prefix, lagged = n, "Filtered subscriber lagged");
}
Err(broadcast::error::RecvError::Closed) => {
break;
}
}
}
}
}
});
tracing::info!(prefix = %event_type_prefix, "Filtered subscription created");
(
FilteredEventReceiver { receiver: mpsc_rx },
SubscriptionHandle {
cancel_tx,
join_handle,
},
)
}
}

View File

@@ -6,3 +6,6 @@ pub mod events;
pub mod module;
pub mod rbac;
pub mod types;
// 便捷导出
pub use module::{ModuleContext, ModuleType};

View File

@@ -1,11 +1,27 @@
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
use crate::error::AppResult;
use crate::error::{AppError, AppResult};
use crate::events::EventBus;
/// 模块类型
#[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]
@@ -13,11 +29,21 @@ 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![]
@@ -26,6 +52,21 @@ pub trait ErpModule: Send + Sync {
/// 注册事件处理器
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"}))
}
/// 租户创建时的初始化钩子。
///
/// 用于为新建租户创建默认角色、管理员用户等初始数据。
@@ -72,7 +113,9 @@ impl ModuleRegistry {
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();
@@ -90,4 +133,202 @@ impl ModuleRegistry {
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());
}
}