Files
erp/docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md
iven ff352a4c24 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 一键启动脚本
2026-04-15 23:32:02 +08:00

986 lines
39 KiB
Markdown
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.

# 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" # 全局唯一 IDkebab-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<Vec<u8>>;
/// 查询记录(自动注入 tenant_id 过滤 + 软删除过滤)
fn db_query(&mut self, entity: &str, filter: &[u8], pagination: &[u8]) -> Result<Vec<u8]>;
/// 更新记录(自动检查 version 乐观锁)
fn db_update(&mut self, entity: &str, id: &str, data: &[u8], version: i64) -> Result<Vec<u8]>;
/// 软删除记录
fn db_delete(&mut self, entity: &str, id: &str) -> Result<()>;
/// 原始查询(仅允许 SELECT自动注入 tenant_id 过滤)
fn db_raw_query(&mut self, sql: &str, params: &[u8]) -> Result<Vec<u8]>;
// === 事件总线 ===
/// 发布领域事件
fn event_publish(&mut self, event_type: &str, payload: &[u8]) -> Result<()>;
// === 配置 ===
/// 读取系统配置(插件作用域内)
fn config_get(&mut self, key: &str) -> Result<Vec<u8]>;
// === 日志 ===
/// 写日志(自动关联 tenant_id + plugin_id
fn log_write(&mut self, level: &str, message: &str);
// === 用户/权限 ===
/// 获取当前用户信息
fn current_user(&mut self) -> Result<Vec<u8]>;
/// 检查当前用户权限
fn check_permission(&mut self, permission: &str) -> Result<bool>;
}
```
### 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<LoadedPlugin> {
// 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<ModuleHealth> {
Ok(ModuleHealth { status: "ok".into(), details: None })
}
// 路由
fn public_routes(&self) -> Option<Router> { None }
fn protected_routes(&self) -> Option<Router> { None }
// 数据库
fn migrations(&self) -> Vec<Box<dyn MigrationTrait>> { 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<serde_json::Value> { None }
fn as_any(&self) -> &dyn Any;
}
pub enum ModuleType { Native, Wasm }
pub struct ModuleHealth {
pub status: String,
pub details: Option<String>,
}
pub struct ModuleContext {
pub db: DatabaseConnection,
pub event_bus: EventBus,
pub config: Arc<AppConfig>,
}
```
### 9.2 ModuleRegistry v2
```rust
pub struct ModuleRegistry {
modules: Arc<Vec<Arc<dyn ErpModule>>>,
wasm_runtime: Arc<WasmPluginRuntime>,
index: Arc<HashMap<String, usize>>,
}
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<String, ModuleHealth>;
// 自动收集所有路由
pub fn build_routes(&self) -> (Router, Router);
// 自动收集所有迁移
pub fn collect_migrations(&self) -> Vec<Box<dyn MigrationTrait>>;
// 拓扑排序(基于 dependencies
fn topological_sort(&self) -> AppResult<Vec<Arc<dyn ErpModule>>>;
// 按 ID 查找模块
pub fn get_module(&self, id: &str) -> Option<&Arc<dyn ErpModule>>;
}
```
### 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<u8>) -> 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<dyn Fn(DomainEvent) + Send + Sync>,
) -> SubscriptionHandle {
// 在内部 HashMap<String, Vec<Handler>> 中注册
// publish() 时根据 event_type 分发到匹配的 handler
}
/// 取消订阅(用于插件停用时清理)
pub fn unsubscribe(&self, handle: SubscriptionHandle) { /* ... */ }
}
```
### B.2 插件事件处理器包装
```rust
struct PluginEventHandler {
plugin_id: String,
handler_fn: Box<dyn Fn(DomainEvent) + Send + Sync>,
}
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<u8>) -> result<list<u8>, string>;
db-query: func(entity: string, filter: list<u8>, pagination: list<u8>) -> result<list<u8>, string>;
db-update: func(entity: string, id: string, data: list<u8>, version: s64) -> result<list<u8>, string>;
db-delete: func(entity: string, id: string) -> result<_, string>;
db-aggregate: func(entity: string, query: list<u8>) -> result<list<u8>, string>;
/// 事件总线
event-publish: func(event-type: string, payload: list<u8>) -> result<_, string>;
/// 配置
config-get: func(key: string) -> result<list<u8>, string>;
/// 日志
log-write: func(level: string, message: string);
/// 用户/权限
current-user: func() -> result<list<u8>, string>;
check-permission: func(permission: string) -> result<bool, string>;
}
interface plugin {
/// 插件初始化(加载时调用一次)
init: func() -> result<_, string>;
/// 租户创建时调用
on-tenant-created: func(tenant-id: string) -> result<_, string>;
/// 处理订阅的事件
handle-event: func(event-type: string, payload: list<u8>) -> result<_, string>;
/// 自定义页面渲染(仅 type=custom 页面)
render-page: func(page-path: string, params: list<u8>) -> result<list<u8>, string>;
/// 自定义页面操作处理
handle-action: func(page-path: string, action: string, data: list<u8>) -> result<list<u8>, 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` 不依赖任何业务 crateauth/config/workflow/message
- `erp-server` 在组装时引入 `erp-plugin-runtime`