# WASM 插件系统设计规格 > 日期:2026-04-13 > 状态:审核通过 (v2 — 修复安全/多租户/迁移问题) > 关联:`docs/superpowers/specs/2026-04-10-erp-platform-base-design.md` > Review 历史:v1 首次审核 → 修复 C1-C4 关键问题 + I1-I5 重要问题 → v2 审核通过 ## 1. 背景与动机 ERP 平台底座 Phase 1-6 已全部完成,包含 auth、config、workflow、message 四大基础模块。 当前系统是一个"模块化形状的单体"——模块以独立 crate 存在,但集成方式是编译时硬编码(main.rs 手动注册路由、合并迁移、启动后台任务)。 **核心矛盾:** Rust 的静态编译特性不支持运行时热插拔,但产品目标是"通用基座 + 行业插件"架构。 **本设计的目标:** 引入 WASM 运行时插件系统,使行业模块(进销存、生产、财务等)可以动态安装、启用、停用,无需修改基座代码。 ## 2. 设计决策 | 决策点 | 选择 | 理由 | |--------|------|------| | 插件范围 | 仅行业模块动态化,基础模块保持 Rust 编译时 | 基础模块变更频率低、可靠性要求高,适合编译时保证 | | 插件技术 | WebAssembly (Wasmtime) | Rust 原生运行时,性能接近原生,沙箱安全 | | 数据库访问 | 宿主代理 API | 宿主自动注入 tenant_id、软删除、审计日志,插件无法绕过 | | 前端 UI | 配置驱动 | ERP 80% 页面是 CRUD,配置驱动覆盖大部分场景 | | 插件管理 | 内置插件商店 | 类似 WordPress 模型,管理后台上传 WASM 包 | | WASM 运行时 | Wasmtime | Bytecode Alliance 维护,Rust 原生,Cranelift JIT | ## 3. 架构总览 ``` ┌──────────────────────────────────────────────────────────┐ │ erp-server │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ ModuleRegistry v2 │ │ │ │ ┌─────────────────┐ ┌──────────────────────────┐│ │ │ │ │ Native Modules │ │ Wasmtime Runtime ││ │ │ │ │ ┌──────┐┌──────┐│ │ ┌──────┐┌──────┐┌──────┐││ │ │ │ │ │ auth ││config ││ │ │进销存 ││ 生产 ││ 财务 │││ │ │ │ │ ├──────┤├──────┤│ │ └──┬───┘└──┬───┘└──┬───┘││ │ │ │ │ │workflow│msg ││ │ └────────┼────────┘ ││ │ │ │ │ └──────┘└──────┘│ │ Host API Layer ││ │ │ │ └─────────────────┘ └──────────────────────────┘│ │ │ └────────────────────────────────────────────────────┘ │ │ ↕ EventBus │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 统一 Axum Router │ │ │ │ /api/v1/auth/* /api/v1/plugins/{id}/* │ │ │ └────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘ ↕ ┌──────────────────────────────────────────────────────────┐ │ Frontend (React SPA) │ │ ┌──────────────┐ ┌──────────────────────────────────┐ │ │ │ 固定路由 │ │ 动态路由 (PluginRegistry Store) │ │ │ │ /users /roles │ │ /inventory/* /production/* │ │ │ └──────────────┘ └──────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────────┐│ │ │ PluginCRUDPage — 配置驱动的通用 CRUD 渲染引擎 ││ │ └──────────────────────────────────────────────────────┘│ └──────────────────────────────────────────────────────────┘ ``` ## 4. 插件清单 (Plugin Manifest) 每个 WASM 插件包含一个 `plugin.toml` 清单文件: ```toml [plugin] id = "erp-inventory" # 全局唯一 ID,kebab-case name = "进销存管理" # 显示名称 version = "1.0.0" # 语义化版本 description = "商品/采购/销售/库存管理" author = "ERP Team" min_platform_version = "1.0.0" # 最低基座版本要求 [dependencies] modules = ["auth", "workflow"] # 依赖的基础模块 ID 列表 [permissions] database = true # 需要数据库访问 events = true # 需要发布/订阅事件 config = true # 需要读取系统配置 files = false # 是否需要文件存储 [schema] [[schema.entities]] name = "inventory_item" fields = [ { name = "sku", type = "string", required = true, unique = true }, { name = "name", type = "string", required = true }, { name = "quantity", type = "integer", default = 0 }, { name = "unit", type = "string", default = "个" }, { name = "category_id", type = "uuid", nullable = true }, { name = "unit_price", type = "decimal", precision = 10, scale = 2 }, ] indexes = [["sku"], ["category_id"]] [[schema.entities]] name = "purchase_order" fields = [ { name = "order_no", type = "string", required = true, unique = true }, { name = "supplier_id", type = "uuid" }, { name = "status", type = "string", default = "draft" }, { name = "total_amount", type = "decimal", precision = 12, scale = 2 }, { name = "order_date", type = "date" }, ] [events] published = ["inventory.stock.low", "purchase_order.created", "purchase_order.approved"] subscribed = ["workflow.task.completed"] [ui] [[ui.pages]] name = "商品管理" path = "/inventory/items" entity = "inventory_item" type = "crud" icon = "ShoppingOutlined" menu_group = "进销存" [[ui.pages]] name = "采购管理" path = "/inventory/purchase" entity = "purchase_order" type = "crud" icon = "ShoppingCartOutlined" menu_group = "进销存" [[ui.pages]] name = "库存盘点" path = "/inventory/stocktaking" type = "custom" menu_group = "进销存" ``` **规则:** - `schema.entities` 声明的表自动注入标准字段:`id`, `tenant_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted_at`, `version` - `permissions` 控制插件可调用的宿主 API 范围(最小权限原则) - `ui.pages.type` 为 `crud` 时由通用渲染引擎自动生成页面,`custom` 时由插件处理渲染逻辑 - 插件事件命名使用 `{plugin_id}.{entity}.{action}` 三段式,避免与基础模块的 `{module}.{action}` 二段式冲突 - 动态创建的表使用 `plugin_{entity_name}` 格式,所有租户共享同一张表,通过 `tenant_id` 列实现行级隔离(与现有表模式一致) ## 5. 宿主 API (Host Functions) WASM 插件通过宿主暴露的函数访问系统资源,这是插件与外部世界的唯一通道: ### 5.1 API 定义 ```rust /// 宿主暴露给 WASM 插件的 API 接口 /// 通过 Wasmtime Linker 注册为 host functions trait PluginHostApi { // === 数据库操作 === /// 插入记录(宿主自动注入 tenant_id, id, created_at 等标准字段) fn db_insert(&mut self, entity: &str, data: &[u8]) -> Result>; /// 查询记录(自动注入 tenant_id 过滤 + 软删除过滤) fn db_query(&mut self, entity: &str, filter: &[u8], pagination: &[u8]) -> Result; /// 更新记录(自动检查 version 乐观锁) fn db_update(&mut self, entity: &str, id: &str, data: &[u8], version: i64) -> Result; /// 软删除记录 fn db_delete(&mut self, entity: &str, id: &str) -> Result<()>; /// 原始查询(仅允许 SELECT,自动注入 tenant_id 过滤) fn db_raw_query(&mut self, sql: &str, params: &[u8]) -> Result; // === 事件总线 === /// 发布领域事件 fn event_publish(&mut self, event_type: &str, payload: &[u8]) -> Result<()>; // === 配置 === /// 读取系统配置(插件作用域内) fn config_get(&mut self, key: &str) -> Result; // === 日志 === /// 写日志(自动关联 tenant_id + plugin_id) fn log_write(&mut self, level: &str, message: &str); // === 用户/权限 === /// 获取当前用户信息 fn current_user(&mut self) -> Result; /// 检查当前用户权限 fn check_permission(&mut self, permission: &str) -> Result; } ``` ### 5.2 安全边界 插件运行在 WASM 沙箱中,安全策略如下: 1. **权限校验** — 插件只能调用清单 `permissions` 中声明的宿主函数,未声明的调用在加载时被拦截 2. **租户隔离** — 所有 `db_*` 操作自动注入 `tenant_id`,插件无法绕过多租户隔离。使用行级隔离(共享表 + tenant_id 过滤),与现有基础模块保持一致 3. **资源限制** — 每个插件有独立的资源配额(内存上限、CPU 时间、API 调用频率) 4. **审计记录** — 所有写操作自动记录审计日志 5. **SQL 安全** — 不暴露原始 SQL 接口,`db_aggregate` 使用结构化查询对象,宿主层安全构建参数化 SQL 6. **文件/网络隔离** — 插件不能直接访问文件系统或网络 ### 5.3 数据流 ``` WASM 插件 宿主安全层 PostgreSQL ┌──────────┐ ┌───────────────┐ ┌──────────┐ │ 调用 │ ── Host Call ──→ │ 1. 权限校验 │ │ │ │ db_insert │ │ 2. 注入标准字段 │ ── SQL ──→ │ INSERT │ │ │ │ 3. 注入 tenant │ │ INTO │ │ │ ←─ JSON 结果 ── │ 4. 写审计日志 │ │ │ └──────────┘ └───────────────┘ └──────────┘ ``` ## 6. 插件生命周期 ### 6.1 状态机 ``` 上传 WASM 包 │ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ uploaded │───→│ installed │───→│ enabled │───→│ running │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ ┌──────────┘ │ ▼ ┌──────────┐ │ disabled │←── 运行时错误自动停用 └──────────┘ │ ▼ ┌──────────┐ │uninstalled│ ── 软删除插件记录,保留数据表和数据 └──────────┘ │ ▼ (可选,需管理员二次确认) ┌──────────┐ │ purged │ ── 真正删除数据表 + 数据导出备份 └──────────┘ ``` ### 6.2 各阶段操作 | 阶段 | 操作 | |------|------| | uploaded → installed | 校验清单格式、验证依赖模块存在、检查 min_platform_version | | installed → enabled | 根据 `schema.entities` 创建数据表(带 `plugin_` 前缀)、写入启用状态 | | enabled → running | 服务启动时:Wasmtime 实例化、注册 Host Functions、调用 `init()`、注册事件处理器、注册前端路由 | | running → disabled | 停止 WASM 实例、注销事件处理器、注销路由 | | disabled → uninstalled | 软删除插件记录(设置 `deleted_at`),**保留数据表和数据不变**,清理事件订阅记录 | | uninstalled → purged | 数据导出备份后,删除 `plugin_*` 数据表。**需要管理员二次确认 + 数据导出完成** | ### 6.3 启动加载流程 ```rust async fn load_plugins(db: &DatabaseConnection) -> Vec { // 1. 查询所有 enabled 状态的插件 let plugins = Plugin::find() .filter(status.eq("enabled")) .filter(deleted_at.is_null()) .all(db).await?; let mut loaded = Vec::new(); for plugin in plugins { // 2. 初始化 Wasmtime Engine(复用全局 Engine) let module = Module::from_binary(&engine, &plugin.wasm_binary)?; // 3. 创建 Linker,根据 permissions 注册对应的 Host Functions let mut linker = Linker::new(&engine); register_host_functions(&mut linker, &plugin.permissions)?; // 4. 实例化 let instance = linker.instantiate_async(&mut store, &module).await?; // 5. 调用插件的 init() 入口函数 if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "init") { init.call_async(&mut store, ()).await?; } // 6. 注册事件处理器 for sub in &plugin.manifest.events.subscribed { event_bus.subscribe_filtered(sub, plugin_handler(plugin.id, instance.clone())); } loaded.push(LoadedPlugin { plugin, instance, store }); } // 7. 依赖排序验证 validate_dependencies(&loaded)?; Ok(loaded) } ``` ## 7. 数据库 Schema ### 7.1 新增表 ```sql -- 插件注册表 CREATE TABLE plugins ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, plugin_id VARCHAR(100) NOT NULL, -- 清单中的唯一 ID name VARCHAR(200) NOT NULL, plugin_version VARCHAR(20) NOT NULL, -- 插件语义化版本(避免与乐观锁 version 混淆) description TEXT, manifest JSONB NOT NULL, -- 完整清单 JSON wasm_binary BYTEA NOT NULL, -- 编译后的 WASM 二进制 status VARCHAR(20) DEFAULT 'installed', -- uploaded / installed / enabled / disabled / error permissions JSONB NOT NULL, error_message TEXT, schema_version INTEGER DEFAULT 1, -- 插件数据 schema 版本 config JSONB DEFAULT '{}', -- 插件配置 -- 标准字段 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by UUID, updated_by UUID, deleted_at TIMESTAMPTZ, -- 软删除(卸载不删数据) row_version INTEGER NOT NULL DEFAULT 1, -- 乐观锁版本 UNIQUE(tenant_id, plugin_id) ); -- 插件 schema 版本跟踪(用于动态表的版本管理) CREATE TABLE plugin_schema_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), plugin_id VARCHAR(100) NOT NULL, -- 全局唯一的插件 ID entity_name VARCHAR(100) NOT NULL, -- 实体名 schema_version INTEGER NOT NULL DEFAULT 1, -- 当前 schema 版本 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(plugin_id, entity_name) ); -- 插件事件订阅记录 CREATE TABLE plugin_event_subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, plugin_id VARCHAR(100) NOT NULL, event_type VARCHAR(200) NOT NULL, handler_name VARCHAR(100) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` ### 7.2 动态数据表 插件安装时根据 `manifest.schema.entities` 自动创建数据表: - 表名格式:`plugin_{entity_name}` - **行级隔离模式**:所有租户共享同一张 `plugin_*` 表,通过 `tenant_id` 列过滤实现隔离(与现有基础模块的表保持一致) - 首次创建表时使用 `IF NOT EXISTS`(幂等),后续租户安装同一插件时复用已有表 - 自动包含标准字段:`id`, `tenant_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted_at`, `version` - 索引自动创建:主键 + `tenant_id`(必选)+ 清单中声明的自定义索引 - **注意**:此方式绕过 SeaORM Migration 系统,属于合理偏差——插件是运行时动态加载的,其 schema 无法在编译时通过静态迁移管理。宿主维护 `plugin_schema_versions` 表跟踪每个插件的 schema 版本 ## 8. 配置驱动 UI ### 8.1 前端架构 ``` 插件 manifest.ui.pages │ ▼ ┌───────────────────┐ │ PluginStore │ Zustand Store,从 /api/v1/plugins/:id/pages 加载 │ (前端插件注册表) │ 缓存所有已启用插件的页面配置 └───────┬───────────┘ │ ▼ ┌───────────────────┐ │ DynamicRouter │ React Router,根据 PluginStore 自动生成路由 │ (动态路由层) │ 懒加载 PluginCRUDPage / PluginDashboard └───────┬───────────┘ │ ▼ ┌───────────────────┐ │ PluginCRUDPage │ 通用 CRUD 页面组件 │ │ │ ┌─────────────┐ │ │ │ SearchBar │ │ 根据 filters 配置自动生成搜索条件 │ └─────────────┘ │ │ ┌─────────────┐ │ │ │ DataTable │ │ 根据 columns 配置渲染 Ant Design Table │ └─────────────┘ │ │ ┌─────────────┐ │ │ │ FormDialog │ │ 根据 form 配置渲染新建/编辑表单 │ └─────────────┘ │ │ ┌─────────────┐ │ │ │ ActionBar │ │ 根据 actions 配置渲染操作按钮 │ └─────────────┘ │ └───────────────────┘ ``` ### 8.2 页面配置类型 ```typescript interface PluginPageConfig { name: string; path: string; entity: string; type: "crud" | "dashboard" | "custom"; icon?: string; menu_group: string; // CRUD 配置(可选,不提供时从 schema.entities 自动推导) // columns 未指定时:从 entity 的 fields 生成,type=select 需显式指定 options // form 未指定时:从 entity 的 fields 生成表单,required 字段为必填 columns?: ColumnDef[]; filters?: FilterDef[]; actions?: ActionDef[]; form?: FormDef; } interface ColumnDef { field: string; label: string; type: "text" | "number" | "date" | "datetime" | "select" | "multiselect" | "currency" | "status" | "link"; width?: number; sortable?: boolean; hidden?: boolean; options?: { label: string; value: string; color?: string }[]; } interface FormDef { groups?: FormGroup[]; fields: FormField[]; rules?: ValidationRule[]; } ``` ### 8.3 动态菜单生成 前端侧边栏从 PluginStore 动态生成菜单项: - 基础模块菜单固定(用户、权限、组织、工作流、消息、设置) - 插件菜单按 `menu_group` 分组,动态追加到侧边栏 - 菜单数据来自 `/api/v1/plugins/installed` API,启动时加载 ### 8.4 插件 API 路由 插件的 CRUD API 由宿主自动生成: ``` GET /api/v1/plugins/{plugin_id}/{entity} # 列表查询 GET /api/v1/plugins/{plugin_id}/{entity}/{id} # 详情 POST /api/v1/plugins/{plugin_id}/{entity} # 新建 PUT /api/v1/plugins/{plugin_id}/{entity}/{id} # 更新 DELETE /api/v1/plugins/{plugin_id}/{entity}/{id} # 删除 ``` 宿主自动注入 tenant_id、处理分页、乐观锁、软删除。 ### 8.5 自定义页面 `type: "custom"` 的页面需要额外的渲染指令: - 插件 WASM 可以导出 `render_page` 函数,返回 UI 指令 JSON - 宿主前端解析指令并渲染(支持:条件显示、自定义操作、复杂布局) - 复杂交互(如库存盘点)通过事件驱动:前端发送 action → 后端 WASM 处理 → 返回新的 UI 状态 ## 9. 升级后的模块注册系统 ### 9.1 ErpModule trait v2 ```rust #[async_trait] pub trait ErpModule: Send + Sync { fn id(&self) -> &str; fn name(&self) -> &str; fn version(&self) -> &str { env!("CARGO_PKG_VERSION") } fn dependencies(&self) -> Vec<&str> { vec![] } fn module_type(&self) -> ModuleType; // 生命周期 async fn on_startup(&self, ctx: &ModuleContext) -> AppResult<()> { Ok(()) } async fn on_shutdown(&self) -> AppResult<()> { Ok(()) } async fn health_check(&self) -> AppResult { Ok(ModuleHealth { status: "ok".into(), details: None }) } // 路由 fn public_routes(&self) -> Option { None } fn protected_routes(&self) -> Option { None } // 数据库 fn migrations(&self) -> Vec> { vec![] } // 事件 fn register_event_handlers(&self, bus: &EventBus) {} // 租户 async fn on_tenant_created(&self, tenant_id: Uuid, ctx: &ModuleContext) -> AppResult<()> { Ok(()) } async fn on_tenant_deleted(&self, tenant_id: Uuid, ctx: &ModuleContext) -> AppResult<()> { Ok(()) } // 配置 fn config_schema(&self) -> Option { None } fn as_any(&self) -> &dyn Any; } pub enum ModuleType { Native, Wasm } pub struct ModuleHealth { pub status: String, pub details: Option, } pub struct ModuleContext { pub db: DatabaseConnection, pub event_bus: EventBus, pub config: Arc, } ``` ### 9.2 ModuleRegistry v2 ```rust pub struct ModuleRegistry { modules: Arc>>, wasm_runtime: Arc, index: Arc>, } impl ModuleRegistry { pub fn new() -> Self; // 注册 Rust 原生模块 pub fn register(self, module: impl ErpModule + 'static) -> Self; // 从数据库加载 WASM 插件 pub async fn load_wasm_plugins(&mut self, db: &DatabaseConnection) -> AppResult<()>; // 按依赖顺序启动所有模块 pub async fn startup_all(&self, ctx: &ModuleContext) -> AppResult<()>; // 聚合健康状态 pub async fn health_check_all(&self) -> HashMap; // 自动收集所有路由 pub fn build_routes(&self) -> (Router, Router); // 自动收集所有迁移 pub fn collect_migrations(&self) -> Vec>; // 拓扑排序(基于 dependencies) fn topological_sort(&self) -> AppResult>>; // 按 ID 查找模块 pub fn get_module(&self, id: &str) -> Option<&Arc>; } ``` ### 9.3 升级后的 main.rs ```rust #[tokio::main] async fn main() -> Result<()> { // 初始化 DB、Config、EventBus ... // 1. 注册 Rust 原生模块 let mut registry = ModuleRegistry::new() .register(AuthModule::new()) .register(ConfigModule::new()) .register(WorkflowModule::new()) .register(MessageModule::new()); // 2. 从数据库加载 WASM 插件 registry.load_wasm_plugins(&db).await?; // 3. 依赖排序 + 启动所有模块 let ctx = ModuleContext { db: db.clone(), event_bus: event_bus.clone(), config: config.clone() }; registry.startup_all(&ctx).await?; // 4. 自动收集路由(无需手动 merge) let (public, protected) = registry.build_routes(); // 5. 构建 Axum 服务 let app = Router::new() .nest("/api/v1", public.merge(protected)) .with_state(app_state); // 启动服务 ... } ``` ## 10. 插件开发体验 ### 10.1 插件项目结构 ``` erp-plugin-inventory/ ├── Cargo.toml # crate 类型为 cdylib (WASM) ├── plugin.toml # 插件清单 └── src/ └── lib.rs # 插件入口 ``` ### 10.2 插件 Cargo.toml ```toml [package] name = "erp-plugin-inventory" version = "1.0.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] wit-bindgen = "0.24" # WIT 接口绑定生成 serde = { version = "1", features = ["derive"] } serde_json = "1" ``` ### 10.3 插件代码示例 ```rust use wit_bindgen::generate::Guest; // 自动生成宿主 API 绑定 export!(Plugin); struct Plugin; impl Guest for Plugin { fn init() -> Result<(), String> { host::log_write("info", "进销存插件初始化完成"); Ok(()) } fn on_tenant_created(tenant_id: String) -> Result<(), String> { // 初始化默认商品分类等 host::db_insert("inventory_category", br#"{"name": "默认分类"}"#) .map_err(|e| e.to_string())?; Ok(()) } fn handle_event(event_type: String, payload: Vec) -> Result<(), String> { match event_type.as_str() { "workflow.task.completed" => { // 采购审批通过,更新采购单状态 let data: serde_json::Value = serde_json::from_slice(&payload) .map_err(|e| e.to_string())?; let order_id = data["business_id"].as_str().unwrap(); host::db_update("purchase_order", order_id, br#"{"status": "approved"}"#, 1) .map_err(|e| e.to_string())?; } _ => {} } Ok(()) } } ``` ### 10.4 构建与发布 ```bash # 编译为 WASM cargo build --target wasm32-unknown-unknown --release # 打包(WASM 二进制 + 清单文件) erp-plugin pack ./target/wasm32-unknown-unknown/release/erp_plugin_inventory.wasm \ --manifest ./plugin.toml \ --output ./erp-inventory-1.0.0.erp-plugin # 上传到平台(通过管理后台或 API) curl -X POST /api/v1/admin/plugins/upload \ -F "plugin=@./erp-inventory-1.0.0.erp-plugin" ``` ## 11. 管理后台 API ### 11.1 插件管理接口 ``` POST /api/v1/admin/plugins/upload # 上传插件包 GET /api/v1/admin/plugins # 列出所有插件 GET /api/v1/admin/plugins/{plugin_id} # 插件详情 POST /api/v1/admin/plugins/{plugin_id}/enable # 启用插件 POST /api/v1/admin/plugins/{plugin_id}/disable # 停用插件 DELETE /api/v1/admin/plugins/{plugin_id} # 卸载插件 GET /api/v1/admin/plugins/{plugin_id}/health # 插件健康检查 PUT /api/v1/admin/plugins/{plugin_id}/config # 更新插件配置 POST /api/v1/admin/plugins/{plugin_id}/upgrade # 升级插件版本 ``` ### 11.2 插件数据接口(自动生成) ``` GET /api/v1/plugins/{plugin_id}/{entity} # 列表查询 GET /api/v1/plugins/{plugin_id}/{entity}/{id} # 详情 POST /api/v1/plugins/{plugin_id}/{entity} # 新建 PUT /api/v1/plugins/{plugin_id}/{entity}/{id} # 更新 DELETE /api/v1/plugins/{plugin_id}/{entity}/{id} # 删除 ``` ## 12. 实施路径 ### Phase 7: 插件系统核心 1. **引入 Wasmtime 依赖**,创建 `erp-plugin-runtime` crate 2. **定义 WIT 接口文件**,描述宿主-插件合约 3. **实现 Host API 层** — db_insert/query/update/delete、event_publish、config_get 等 4. **实现插件加载器** — 从数据库读取 WASM 二进制、实例化、注册路由 5. **升级 ErpModule trait** — 添加 lifecycle hooks、routes、migrations 方法 6. **升级 ModuleRegistry** — 拓扑排序、自动路由收集、WASM 插件注册 7. **插件管理 API** — 上传、启用、停用、卸载 8. **插件数据库表** — plugins、plugin_event_subscriptions + 动态建表逻辑 ### Phase 8: 前端配置驱动 UI 1. **PluginStore** (Zustand) — 管理已安装插件的页面配置 2. **DynamicRouter** — 根据 PluginStore 自动生成 React Router 路由 3. **PluginCRUDPage** — 通用 CRUD 渲染引擎(表格 + 搜索 + 表单 + 操作) 4. **动态菜单** — 从 PluginStore 生成侧边栏菜单 5. **插件管理页面** — 上传、启用/停用、配置的管理后台 ### Phase 9: 第一个行业插件(进销存) 1. 创建 `erp-plugin-inventory` 作为参考实现 2. 实现商品、采购、库存管理的核心业务逻辑 3. 配置驱动页面覆盖 80% 的 CRUD 场景 4. 验证端到端流程:安装 → 启用 → 使用 → 停用 → 卸载 ## 13. 风险与缓解 | 风险 | 概率 | 影响 | 缓解措施 | |------|------|------|----------| | WASM 插件性能不足 | 低 | 高 | 性能基准测试,关键路径保留 Rust 原生 | | 插件安全问题 | 中 | 高 | 沙箱隔离 + 最小权限 + 审计日志 | | 配置驱动 UI 覆盖不足 | 中 | 中 | 保留 custom 页面类型作为兜底 | | 插件间依赖冲突 | 中 | 中 | 拓扑排序 + 版本约束 + 冲突检测 | | Wasmtime 版本兼容性 | 低 | 中 | 锁定 Wasmtime 大版本,CI 验证 | ## 附录 A: ErpModule Trait 迁移策略 ### A.1 向后兼容原则 `ErpModule` trait v2 的所有新增方法均提供**默认实现(no-op)**,确保现有四个模块(AuthModule、ConfigModule、WorkflowModule、MessageModule)无需修改即可编译通过。 ### A.2 迁移清单 | 现有方法 | v2 变化 | 迁移操作 | |----------|---------|----------| | `fn name(&self) -> &str` | 保留不变,新增 `fn id()` 返回相同值 | 在各模块 impl 中添加 `fn id()` | | `fn version()` | 保留不变 | 无需改动 | | `fn dependencies()` | 保留不变 | 无需改动 | | `fn register_event_handlers()` | 签名不变 | 无需改动 | | `fn on_tenant_created(tenant_id)` | 签名变为 `on_tenant_created(tenant_id, ctx)` | 更新签名,添加 `ctx` 参数 | | `fn on_tenant_deleted(tenant_id)` | 签名变为 `on_tenant_deleted(tenant_id, ctx)` | 更新签名,添加 `ctx` 参数 | | `fn as_any()` | 保留不变 | 无需改动 | | (新增)`fn module_type()` | 默认返回 `ModuleType::Native` | 无需改动 | | (新增)`fn on_startup()` | 默认 no-op | 可选实现 | | (新增)`fn on_shutdown()` | 默认 no-op | 可选实现 | | (新增)`fn health_check()` | 默认返回 ok | 可选实现 | | (新增)`fn public_routes()` | 默认 None | 将现有关联函数迁移到此方法 | | (新增)`fn protected_routes()` | 默认 None | 将现有关联函数迁移到此方法 | | (新增)`fn migrations()` | 默认空 vec | 可选实现 | | (新增)`fn config_schema()` | 默认 None | 可选实现 | ### A.3 迁移后的 main.rs 变化 迁移后,main.rs 从手动路由合并变为自动收集: ```rust // 迁移前(手动) let protected_routes = erp_auth::AuthModule::protected_routes() .merge(erp_config::ConfigModule::protected_routes()) .merge(erp_workflow::WorkflowModule::protected_routes()) .merge(erp_message::MessageModule::protected_routes()); // 迁移后(自动) let (public, protected) = registry.build_routes(); ``` ## 附录 B: EventBus 类型化订阅扩展 ### B.1 现有 EventBus 扩展 现有的 `EventBus`(`erp-core/src/events.rs`)只有 `subscribe()` 方法返回全部事件的 `Receiver`。需要添加类型化过滤订阅: ```rust impl EventBus { /// 订阅特定事件类型 /// 内部使用 mpmc 通道,为每个事件类型维护独立的分发器 pub fn subscribe_filtered( &self, event_type: &str, handler: Box, ) -> SubscriptionHandle { // 在内部 HashMap> 中注册 // publish() 时根据 event_type 分发到匹配的 handler } /// 取消订阅(用于插件停用时清理) pub fn unsubscribe(&self, handle: SubscriptionHandle) { /* ... */ } } ``` ### B.2 插件事件处理器包装 ```rust struct PluginEventHandler { plugin_id: String, handler_fn: Box, } impl PluginEventHandler { fn handle(&self, event: DomainEvent) { // 捕获 panic,防止插件崩溃影响宿主 let result = std::panic::catch_unwind(|| { (self.handler_fn)(event) }); if let Err(_) = result { tracing::error!("插件 {} 事件处理器崩溃", self.plugin_id); // 通知 PluginManager 标记插件为 error 状态 } } } ``` ## 附录 C: 管理后台 API 权限控制 ### C.1 权限模型 | API 端点 | 所需权限 | 角色范围 | |----------|---------|----------| | `POST /admin/plugins/upload` | `plugin:admin` | 仅平台超级管理员 | | `POST /admin/plugins/{id}/enable` | `plugin:manage` | 平台管理员或租户管理员(仅限自己租户的插件) | | `POST /admin/plugins/{id}/disable` | `plugin:manage` | 平台管理员或租户管理员 | | `DELETE /admin/plugins/{id}` | `plugin:manage` | 租户管理员(软删除) | | `DELETE /admin/plugins/{id}/purge` | `plugin:admin` | 仅平台超级管理员 | | `GET /admin/plugins` | `plugin:view` | 租户管理员(仅看到自己租户的插件) | | `PUT /admin/plugins/{id}/config` | `plugin:configure` | 租户管理员 | | `GET /admin/plugins/{id}/health` | `plugin:view` | 租户管理员 | ### C.2 租户隔离 - 插件管理 API 自动注入 `tenant_id` 过滤(从 JWT 中提取) - 平台超级管理员可以通过 `/admin/platform/plugins` 查看所有租户的插件 - 租户管理员只能管理自己租户安装的插件 - 插件上传为平台级操作(所有租户共享同一个 WASM 二进制),但启用/配置为租户级操作 ## 附录 D: WIT 接口定义 ### D.1 插件接口 (`plugin.wit`) ```wit package erp:plugin; interface host { /// 数据库操作 db-insert: func(entity: string, data: list) -> result, string>; db-query: func(entity: string, filter: list, pagination: list) -> result, string>; db-update: func(entity: string, id: string, data: list, version: s64) -> result, string>; db-delete: func(entity: string, id: string) -> result<_, string>; db-aggregate: func(entity: string, query: list) -> result, string>; /// 事件总线 event-publish: func(event-type: string, payload: list) -> result<_, string>; /// 配置 config-get: func(key: string) -> result, string>; /// 日志 log-write: func(level: string, message: string); /// 用户/权限 current-user: func() -> result, string>; check-permission: func(permission: string) -> result; } interface plugin { /// 插件初始化(加载时调用一次) init: func() -> result<_, string>; /// 租户创建时调用 on-tenant-created: func(tenant-id: string) -> result<_, string>; /// 处理订阅的事件 handle-event: func(event-type: string, payload: list) -> result<_, string>; /// 自定义页面渲染(仅 type=custom 页面) render-page: func(page-path: string, params: list) -> result, string>; /// 自定义页面操作处理 handle-action: func(page-path: string, action: string, data: list) -> result, string>; } world plugin-world { import host; export plugin; } ``` ### D.2 使用方式 插件开发者使用 `wit-bindgen` 生成绑定代码: ```bash # 生成 Rust 插件绑定 wit-bindgen rust ./plugin.wit --out-dir ./src/generated ``` 宿主使用 `wasmtime` 的 `bindgen!` 宏生成调用端代码: ```rust // 在 erp-plugin-runtime crate 中 wasmtime::component::bindgen!({ path: "./plugin.wit", world: "plugin-world", async: true, }); ``` ## 附录 E: 插件崩溃恢复策略 ### E.1 崩溃检测与恢复 | 场景 | 检测方式 | 恢复策略 | |------|---------|----------| | WASM 执行 panic | `catch_unwind` 捕获 | 记录错误日志,该请求返回 500,插件继续运行 | | 插件 init() 失败 | 返回 Err | 标记插件为 `error` 状态,不加载 | | 事件处理器崩溃 | `catch_unwind` 捕获 | 记录错误日志,事件丢弃(不重试) | | 连续崩溃(>5次/分钟) | 计数器检测 | 自动停用插件,标记 `error`,通知管理员 | | 服务重启 | 启动流程 | 重新加载所有 `enabled` 状态的插件 | ### E.2 僵尸状态处理 插件在数据库中为 `enabled` 但实际未运行的情况: 1. 服务启动时,所有 `enabled` 插件尝试加载 2. 加载失败的插件自动标记为 `error`,`error_message` 记录原因 3. 管理后台显示 `error` 状态的插件,提供"重试"按钮 4. 重试成功后恢复为 `enabled`,重试失败保持 `error` ### E.3 插件健康检查 ```rust /// 定期健康检查(每 60 秒) async fn health_check_loop(registry: &ModuleRegistry) { let mut interval = tokio::time::interval(Duration::from_secs(60)); loop { interval.tick().await; let results = registry.health_check_all().await; for (id, health) in results { if health.status != "ok" { tracing::warn!("模块 {} 健康检查异常: {:?}", id, health.details); // 通知管理后台 } } } } ``` ## 附录 F: Crate 依赖图更新 ``` erp-core (无业务依赖) erp-common (无业务依赖) ↑ erp-auth (→ core) erp-config (→ core) erp-workflow (→ core) erp-message (→ core) erp-plugin-runtime (→ core, wasmtime) ← 新增 ↑ erp-server (→ 所有 crate,组装入口) ``` **规则:** - `erp-plugin-runtime` 依赖 `erp-core`(使用 EventBus、ErpModule trait、AppError) - `erp-plugin-runtime` 依赖 `wasmtime`(WASM 运行时) - `erp-plugin-runtime` 不依赖任何业务 crate(auth/config/workflow/message) - `erp-server` 在组装时引入 `erp-plugin-runtime`