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

39 KiB
Raw Blame History

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 清单文件:

[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.typecrud 时由通用渲染引擎自动生成页面,custom 时由插件处理渲染逻辑
  • 插件事件命名使用 {plugin_id}.{entity}.{action} 三段式,避免与基础模块的 {module}.{action} 二段式冲突
  • 动态创建的表使用 plugin_{entity_name} 格式,所有租户共享同一张表,通过 tenant_id 列实现行级隔离(与现有表模式一致)

5. 宿主 API (Host Functions)

WASM 插件通过宿主暴露的函数访问系统资源,这是插件与外部世界的唯一通道:

5.1 API 定义

/// 宿主暴露给 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 启动加载流程

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 新增表

-- 插件注册表
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 页面配置类型

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

#[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

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

#[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

[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 插件代码示例

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 构建与发布

# 编译为 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 从手动路由合并变为自动收集:

// 迁移前(手动)
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 扩展

现有的 EventBuserp-core/src/events.rs)只有 subscribe() 方法返回全部事件的 Receiver。需要添加类型化过滤订阅:

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 插件事件处理器包装

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)

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 生成绑定代码:

# 生成 Rust 插件绑定
wit-bindgen rust ./plugin.wit --out-dir ./src/generated

宿主使用 wasmtimebindgen! 宏生成调用端代码:

// 在 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. 加载失败的插件自动标记为 errorerror_message 记录原因
  3. 管理后台显示 error 状态的插件,提供"重试"按钮
  4. 重试成功后恢复为 enabled,重试失败保持 error

E.3 插件健康检查

/// 定期健康检查(每 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 依赖 wasmtimeWASM 运行时)
  • erp-plugin-runtime 不依赖任何业务 crateauth/config/workflow/message
  • erp-server 在组装时引入 erp-plugin-runtime