26 KiB
ERP 插件管理系统 — 完整实施计划
Context
ERP 平台已完成 Phase 1-6(基础设施、身份权限、系统配置、工作流、消息中心、整合打磨),WASM 插件原型 V1-V6 已验证通过。现在需要将原型集成到生产系统,形成完整的插件管理链路:开发 → 打包 → 上传 → 安装 → 启用 → 运行 → 停用 → 卸载。
当前差距:原型使用 mock HostState,无真实 DB 操作;无插件管理 API;无数据库表;无前端管理界面;无动态表/路由。
阶段依赖图
7A (基础设施升级) → 7B (插件运行时) → 7C (数据库表) → 7D (管理 API) → 7E (数据 CRUD API)
8A (前端 API + Store) → 8B (管理页面) → 8C (动态路由 + CRUD 页面) → 8D (E2E 验证)
7D 完成 → 8B 可开始
7E 完成 → 8C 可开始
Phase 7A: 基础设施升级
目标: 扩展 ErpModule trait、ModuleRegistry、EventBus,全部向后兼容,现有 4 个模块无需修改。
7A.1 EventBus 过滤订阅
修改: events.rs
// 新增方法
pub fn subscribe_filtered(
&self,
event_type_prefix: String,
) -> (FilteredEventReceiver, SubscriptionHandle)
// 新增类型
pub struct FilteredEventReceiver { /* mpsc::Receiver */ }
impl FilteredEventReceiver {
pub async fn recv(&mut self) -> Option<DomainEvent> { ... }
}
pub struct SubscriptionHandle { /* JoinHandle + sender for cancel */ }
impl SubscriptionHandle {
pub fn cancel(self) { ... }
}
实现:为每次 subscribe_filtered 调用 spawn 一个 Tokio task,从 broadcast channel 读取,过滤匹配 event_type_prefix 的事件转发到 mpsc channel(capacity 256)。
7A.2 ErpModule Trait v2
修改: module.rs
// 新增枚举
pub enum ModuleType { Builtin, Plugin }
// 新增上下文结构
pub struct ModuleContext {
pub db: sea_orm::DatabaseConnection,
pub event_bus: EventBus,
}
// trait 新增方法(全部有默认实现)
fn id(&self) -> &str { self.name() }
fn module_type(&self) -> ModuleType { ModuleType::Builtin }
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"}))
}
7A.3 ModuleRegistry v2
修改: module.rs
impl ModuleRegistry {
// 新增方法
pub fn sorted_modules(&self) -> Vec<Arc<dyn ErpModule>> // 拓扑排序
pub async fn startup_all(&self, ctx: &ModuleContext) -> AppResult<()>
pub async fn shutdown_all(&self) -> AppResult<()>
pub async fn health_check_all(&self) -> Vec<(String, AppResult<serde_json::Value>)>
pub fn get_module(&self, name: &str) -> Option<Arc<dyn ErpModule>>
}
拓扑排序: Kahn 算法,环检测返回 AppError::Validation。
7A.4 服务启动集成
修改: main.rs — 在 registry.register_handlers 之后添加:
let module_ctx = ModuleContext { db: db.clone(), event_bus: event_bus.clone() };
registry.startup_all(&module_ctx).await?;
修改: lib.rs — 导出新类型 ModuleType, ModuleContext
7A 文件清单:
| 操作 | 文件 |
|---|---|
| 修改 | crates/erp-core/src/events.rs |
| 修改 | crates/erp-core/src/module.rs |
| 修改 | crates/erp-core/src/lib.rs |
| 修改 | crates/erp-server/src/main.rs |
Phase 7B: 插件运行时 Crate
目标: 创建 erp-plugin crate,实现生产级 Host API(真实 DB/EventBus 操作)。
7B.1 Crate 骨架
新建: crates/erp-plugin/Cargo.toml
[dependencies]
wasmtime = "43"
wasmtime-wasi = "43"
erp-core.workspace = true
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
sea-orm.workspace = true
uuid.workspace = true
chrono.workspace = true
tracing.workspace = true
thiserror.workspace = true
dashmap = "6"
toml = "0.8"
新建: crates/erp-plugin/wit/plugin.wit — 从 prototype 复制
新建: crates/erp-plugin/src/lib.rs — 声明模块:engine, host, manifest, state, error, dynamic_table, entity, dto, service, handler, data_service, data_dto, module
修改: Cargo.toml — workspace members 添加 "crates/erp-plugin",dependencies 添加 erp-plugin = { path = "crates/erp-plugin" }
7B.2 错误类型
新建: crates/erp-plugin/src/error.rs
pub enum PluginError {
NotFound(String),
AlreadyExists(String),
InvalidManifest(String),
InvalidState { expected: String, actual: String },
ExecutionError(String),
InstantiationError(String),
FuelExhausted(String),
DependencyNotSatisfied(String),
DatabaseError(String),
PermissionDenied(String),
}
// From<PluginError> for AppError
pub type PluginResult<T> = Result<T, PluginError>;
7B.3 插件清单解析
新建: crates/erp-plugin/src/manifest.rs
pub struct PluginManifest {
pub metadata: PluginMetadata,
pub schema: Option<PluginSchema>,
pub events: Option<PluginEvents>,
pub ui: Option<PluginUi>,
pub permissions: Option<Vec<PluginPermission>>,
}
pub struct PluginMetadata { id, name, version, description, author, min_platform_version, dependencies }
pub struct PluginSchema { pub entities: Vec<PluginEntity> }
pub struct PluginEntity { name, display_name, fields: Vec<PluginField>, indexes }
pub struct PluginField { name, field_type: PluginFieldType, required, unique, default, display_name, ui_widget, options }
pub enum PluginFieldType { String, Integer, Float, Boolean, Date, DateTime, Json, Uuid, Decimal }
pub struct PluginEvents { pub subscribe: Vec<String> }
pub struct PluginUi { pub pages: Vec<PluginPage> }
pub struct PluginPage { route, entity, display_name, icon, menu_group }
pub struct PluginPermission { code, name, description }
pub fn parse_manifest(toml_str: &str) -> PluginResult<PluginManifest>
7B.4 生产 Host 实现(延迟执行模式)
新建: crates/erp-plugin/src/host.rs
关键设计: WASM 调用是同步的,SeaORM 是异步的。采用延迟执行模式:
- 读操作(db_query, config_get, current_user)→ 调用前预填充 HostState,Host 方法直接返回缓存数据
- 写操作(db_insert, db_update, db_delete, event_publish)→ Host 方法将操作入队到
self.pending_ops,返回合成成功响应 - WASM 调用结束后,engine 刷新
pending_ops执行真实 DB 操作
pub struct HostState {
pub(crate) limits: StoreLimits,
pub(crate) tenant_id: Uuid,
pub(crate) user_id: Uuid,
pub(crate) permissions: Vec<String>,
pub(crate) plugin_id: String,
// 预填充的读取缓存
pub(crate) query_results: HashMap<String, Vec<u8>>,
pub(crate) config_cache: HashMap<String, Vec<u8>>,
pub(crate) current_user_json: Vec<u8>,
// 待刷新的写操作
pub(crate) pending_ops: Vec<PendingOp>,
pub(crate) logs: Vec<(String, String)>,
}
pub enum PendingOp {
Insert { entity: String, data: Vec<u8> },
Update { entity: String, id: String, data: Vec<u8>, version: i64 },
Delete { entity: String, id: String },
PublishEvent { event_type: String, payload: Vec<u8> },
}
7B.5 插件引擎
新建: crates/erp-plugin/src/engine.rs
pub struct PluginEngine {
engine: wasmtime::Engine,
db: DatabaseConnection,
event_bus: EventBus,
plugins: Arc<DashMap<String, LoadedPlugin>>,
config: PluginEngineConfig,
}
pub struct PluginEngineConfig {
pub default_fuel: u64, // 10_000_000
pub execution_timeout_secs: u64, // 30
}
pub struct LoadedPlugin {
pub id: String,
pub manifest: PluginManifest,
pub component: Component,
pub linker: Linker<HostState>,
pub status: PluginStatus,
pub event_handle: Option<SubscriptionHandle>,
}
pub enum PluginStatus { Loaded, Initialized, Running, Error(String), Disabled }
// 核心方法
impl PluginEngine {
pub fn new(db, event_bus, config) -> Result<Self>
pub async fn load(&self, plugin_id, wasm_bytes, manifest) -> Result<()> // 加载到内存
pub async fn initialize(&self, plugin_id) -> Result<()> // 调用 init()
pub async fn start_event_listener(&self, plugin_id) -> Result<()> // 订阅事件
pub async fn handle_event(&self, plugin_id, event_type, payload, tenant_id, user_id) -> Result<()>
pub async fn on_tenant_created(&self, plugin_id, tenant_id) -> Result<()>
pub async fn disable(&self, plugin_id) -> Result<()> // 停止+卸载
pub async fn unload(&self, plugin_id) -> Result<()>
pub async fn health_check(&self, plugin_id) -> Result<serde_json::Value>
pub fn list_plugins(&self) -> Vec<PluginInfo>
pub fn get_manifest(&self, plugin_id) -> Option<PluginManifest>
// 内部: spawn_blocking + catch_unwind + fuel 限制 + timeout
async fn execute_wasm<F, R>(&self, plugin_id, operation: F) -> Result<R>
// 内部: 刷新 pending_ops 到真实 DB
async fn flush_ops(&self, state: &HostState) -> Result<()>
}
execute_wasm 流程:
- 从 DashMap 获取 LoadedPlugin
- 创建新 Store + HostState(预填充读数据)
tokio::task::spawn_blocking包装 WASM 调用- 内部
std::panic::catch_unwind(AssertUnwindSafe(...)) - 返回后
flush_ops执行真实 DB 操作 - 外层
tokio::time::timeout限制执行时间
7B.6 动态表管理器
新建: crates/erp-plugin/src/dynamic_table.rs
pub struct DynamicTableManager;
impl DynamicTableManager {
pub async fn create_table(db, plugin_id, entity: &PluginEntity) -> Result<()>
pub async fn drop_table(db, plugin_id, entity_name) -> Result<()>
pub async fn table_exists(db, table_name) -> Result<bool>
pub fn table_name(plugin_id, entity_name) -> String // "plugin_{sanitized_id}_{entity}"
pub fn build_insert_sql(table_name, data) -> (String, Vec<Value>)
pub fn build_query_sql(table_name, filter, pagination) -> (String, Vec<Value>)
pub fn build_update_sql(table_name, id, data, version) -> (String, Vec<Value>)
pub fn build_delete_sql(table_name, id) -> (String, Vec<Value>)
}
动态表结构: plugin_{id}_{entity} 列包括 id(UUID PK), tenant_id, data(JSONB), created_at, updated_at, created_by, updated_by, deleted_at, version
7B.7 插件状态
新建: crates/erp-plugin/src/state.rs
#[derive(Clone)]
pub struct PluginState {
pub db: DatabaseConnection,
pub event_bus: EventBus,
pub engine: PluginEngine,
}
7B 文件清单:
| 操作 | 文件 |
|---|---|
| 新建 | crates/erp-plugin/Cargo.toml |
| 新建 | crates/erp-plugin/wit/plugin.wit |
| 新建 | crates/erp-plugin/src/lib.rs |
| 新建 | crates/erp-plugin/src/error.rs |
| 新建 | crates/erp-plugin/src/manifest.rs |
| 新建 | crates/erp-plugin/src/host.rs |
| 新建 | crates/erp-plugin/src/engine.rs |
| 新建 | crates/erp-plugin/src/state.rs |
| 新建 | crates/erp-plugin/src/dynamic_table.rs |
| 修改 | Cargo.toml (workspace) |
Phase 7C: 数据库表
目标: 创建插件元数据表 + SeaORM Entity。
7C.1 迁移文件
新建: crates/erp-server/migration/src/m20260417_000033_create_plugins.rs
三张表:
plugins — 插件注册与生命周期
| 列 | 类型 | 说明 |
|---|---|---|
| id | UUID PK | manifest 中的 ID |
| tenant_id | UUID NOT NULL | 所属租户 |
| name | VARCHAR(200) | 插件名称 |
| plugin_version | VARCHAR(50) | 语义版本 |
| description | TEXT | |
| author | VARCHAR(200) | |
| status | VARCHAR(20) | uploaded/installed/enabled/running/disabled/uninstalled |
| manifest_json | JSONB | 完整清单 |
| wasm_binary | BYTEA | WASM 二进制 |
| wasm_hash | VARCHAR(64) | SHA-256 |
| config_json | JSONB DEFAULT '{}' | 插件配置 |
| error_message | TEXT | 最近错误 |
| installed_at | TIMESTAMPTZ | |
| enabled_at | TIMESTAMPTZ | |
| + 标准字段 | created_at, updated_at, created_by, updated_by, deleted_at, version |
索引: idx_plugins_tenant_status, idx_plugins_name (均 WHERE deleted_at IS NULL)
plugin_entities — 插件动态表注册
| 列 | 类型 | 说明 |
|---|---|---|
| id | UUID PK | |
| tenant_id | UUID NOT NULL | |
| plugin_id | UUID → plugins(id) | |
| entity_name | VARCHAR(100) | |
| table_name | VARCHAR(200) | 实际表名 |
| schema_json | JSONB | 字段定义 |
| + 标准字段 |
plugin_event_subscriptions — 事件订阅
| 列 | 类型 | 说明 |
|---|---|---|
| id | UUID PK | |
| plugin_id | UUID → plugins(id) | |
| event_pattern | VARCHAR(200) | 如 "workflow.task.*" |
| created_at | TIMESTAMPTZ |
修改: migration/lib.rs — 注册新迁移
7C.2 SeaORM Entity
新建: crates/erp-plugin/src/entity/mod.rs
新建: crates/erp-plugin/src/entity/plugin.rs — plugins 表 Entity
新建: crates/erp-plugin/src/entity/plugin_entity.rs — plugin_entities 表 Entity
新建: crates/erp-plugin/src/entity/plugin_event_subscription.rs — 事件订阅 Entity
每个 Entity 遵循标准模式: DeriveEntityModel, Relation, ActiveModelBehavior
7C 文件清单:
| 操作 | 文件 |
|---|---|
| 新建 | crates/erp-server/migration/src/m20260417_000033_create_plugins.rs |
| 新建 | crates/erp-plugin/src/entity/mod.rs |
| 新建 | crates/erp-plugin/src/entity/plugin.rs |
| 新建 | crates/erp-plugin/src/entity/plugin_entity.rs |
| 新建 | crates/erp-plugin/src/entity/plugin_event_subscription.rs |
| 修改 | crates/erp-server/migration/src/lib.rs |
Phase 7D: 插件管理 API
目标: 构建 admin REST API 实现完整插件生命周期管理。
7D.1 DTO
新建: crates/erp-plugin/src/dto.rs
// Response
pub struct PluginResp { id, name, version, description, author, status, config, installed_at, enabled_at, entities, permissions, version }
pub struct PluginEntityResp { name, display_name, table_name }
pub struct PluginHealthResp { plugin_id, status, details }
// Request
pub struct UpdatePluginConfigReq { config: serde_json::Value, version: i32 }
// Query
pub struct PluginListParams { page, page_size, status, search }
7D.2 Service
新建: crates/erp-plugin/src/service.rs
pub struct PluginService;
impl PluginService {
pub async fn upload(tenant_id, operator_id, wasm_binary, manifest_toml, db) -> AppResult<PluginResp>
pub async fn install(plugin_id, tenant_id, operator_id, db, engine) -> AppResult<PluginResp>
pub async fn enable(plugin_id, tenant_id, operator_id, db, engine) -> AppResult<PluginResp>
pub async fn disable(plugin_id, tenant_id, operator_id, db, engine) -> AppResult<PluginResp>
pub async fn uninstall(plugin_id, tenant_id, operator_id, db, engine) -> AppResult<PluginResp>
pub async fn list(tenant_id, pagination, status, search, db) -> AppResult<(Vec<PluginResp>, u64)>
pub async fn get_by_id(plugin_id, tenant_id, db) -> AppResult<PluginResp>
pub async fn update_config(plugin_id, tenant_id, operator_id, req, db) -> AppResult<PluginResp>
pub async fn health_check(plugin_id, tenant_id, db, engine) -> AppResult<PluginHealthResp>
pub async fn get_schema(plugin_id, tenant_id, db) -> AppResult<serde_json::Value>
}
生命周期状态机: uploaded → installed → enabled/running → disabled → uninstalled
- upload: 解析 manifest + 存储 wasm_binary + status=uploaded
- install: 创建动态表 + 注册 entity 记录 + 注册事件订阅 + status=installed
- enable: engine.load + engine.initialize + engine.start_event_listener + status=running
- disable: engine.disable + cancel 事件订阅 + status=disabled
- uninstall: disable(如运行中) + drop 动态表 + status=uninstalled
7D.3 Handlers
新建: crates/erp-plugin/src/handler/mod.rs
新建: crates/erp-plugin/src/handler/plugin_handler.rs
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /admin/plugins/upload |
上传 (multipart: wasm + manifest) |
| GET | /admin/plugins |
列表 (分页+过滤) |
| GET | /admin/plugins/{id} |
详情 |
| GET | /admin/plugins/{id}/schema |
实体 schema |
| POST | /admin/plugins/{id}/install |
安装 |
| POST | /admin/plugins/{id}/enable |
启用 |
| POST | /admin/plugins/{id}/disable |
停用 |
| POST | /admin/plugins/{id}/uninstall |
卸载 |
| DELETE | /admin/plugins/{id} |
清除 |
| GET | /admin/plugins/{id}/health |
健康检查 |
| PUT | /admin/plugins/{id}/config |
更新配置 |
所有 handler 遵循现有模式: State<PluginState>, Extension<TenantContext>, require_permission("plugin.admin"), utoipa 注解
7D.4 Module 注册
新建: crates/erp-plugin/src/module.rs
pub struct PluginModule;
impl ErpModule for PluginModule { name="plugin", dependencies=["auth","config"], module_type=Builtin }
impl PluginModule {
pub fn protected_routes<S>() -> Router<S> // 上述所有路由
}
7D.5 服务端集成
修改: main.rs
- 创建
PluginEngine::new(db.clone(), event_bus.clone(), config) - 注册
PluginModule到 registry - 合并
PluginModule::protected_routes()到 protected_routes - 启动时恢复已 enabled 的插件: 查询 plugins 表 → engine.load + initialize + start_event_listener
修改: state.rs
- AppState 新增
pub plugin_engine: erp_plugin::engine::PluginEngine - 添加
FromRef<AppState> for erp_plugin::PluginState
修改: seed.rs — 添加 plugin.admin, plugin.list 权限种子
修改: Cargo.toml — 添加 erp-plugin.workspace = true
7D 文件清单:
| 操作 | 文件 |
|---|---|
| 新建 | crates/erp-plugin/src/dto.rs |
| 新建 | crates/erp-plugin/src/service.rs |
| 新建 | crates/erp-plugin/src/handler/mod.rs |
| 新建 | crates/erp-plugin/src/handler/plugin_handler.rs |
| 新建 | crates/erp-plugin/src/module.rs |
| 修改 | crates/erp-server/src/main.rs |
| 修改 | crates/erp-server/src/state.rs |
| 修改 | crates/erp-server/Cargo.toml |
| 修改 | crates/erp-auth/src/service/seed.rs |
Phase 7E: 插件数据 CRUD API
目标: 通用数据 CRUD 端点 /api/v1/plugins/{plugin_id}/{entity}/*。
7E.1 数据 DTO
新建: crates/erp-plugin/src/data_dto.rs
pub struct PluginDataResp { id, data: serde_json::Value, created_at, updated_at, version }
pub struct CreatePluginDataReq { data: serde_json::Value }
pub struct UpdatePluginDataReq { data: serde_json::Value, version: i32 }
pub struct PluginDataListParams { page, page_size, search }
7E.2 数据 Service
新建: crates/erp-plugin/src/data_service.rs
pub struct PluginDataService;
impl PluginDataService {
pub async fn create(plugin_id, entity_name, tenant_id, operator_id, req, db, event_bus) -> AppResult<PluginDataResp>
pub async fn list(plugin_id, entity_name, tenant_id, pagination, search, db) -> AppResult<(Vec<PluginDataResp>, u64)>
pub async fn get_by_id(plugin_id, entity_name, id, tenant_id, db) -> AppResult<PluginDataResp>
pub async fn update(plugin_id, entity_name, id, tenant_id, operator_id, req, db, event_bus) -> AppResult<PluginDataResp>
pub async fn delete(plugin_id, entity_name, id, tenant_id, operator_id, db, event_bus) -> AppResult<()>
}
每个方法: 解析 table_name → 验证插件 running → 执行原始参数化 SQL → 发布 domain event → 审计日志
7E.3 数据 Handler
新建: crates/erp-plugin/src/handler/data_handler.rs
| 方法 | 路径 |
|---|---|
| GET | /plugins/{plugin_id}/{entity} |
| POST | /plugins/{plugin_id}/{entity} |
| GET | /plugins/{plugin_id}/{entity}/{id} |
| PUT | /plugins/{plugin_id}/{entity}/{id} |
| DELETE | /plugins/{plugin_id}/{entity}/{id} |
权限: plugin.{plugin_id}.{entity}.{action}
修改: module.rs — 添加数据路由
7E 文件清单:
| 操作 | 文件 |
|---|---|
| 新建 | crates/erp-plugin/src/data_dto.rs |
| 新建 | crates/erp-plugin/src/data_service.rs |
| 新建 | crates/erp-plugin/src/handler/data_handler.rs |
| 修改 | crates/erp-plugin/src/module.rs |
Phase 8A: 前端 API + Store
8A.1 插件 API 模块
新建: apps/web/src/api/plugins.ts
export interface PluginInfo { id, name, version, description, author, status, config, installed_at, enabled_at, entities, permissions, version }
export type PluginStatus = 'uploaded' | 'installed' | 'enabled' | 'running' | 'disabled' | 'uninstalled'
export interface PluginEntityInfo { name, display_name, table_name }
export async function listPlugins(page, pageSize, status?)
export async function getPlugin(id)
export async function uploadPlugin(file: File, manifest: string)
export async function installPlugin(id)
export async function enablePlugin(id)
export async function disablePlugin(id)
export async function uninstallPlugin(id)
export async function purgePlugin(id)
export async function getPluginHealth(id)
export async function updatePluginConfig(id, config, version)
export async function getPluginSchema(id)
8A.2 插件数据 API
新建: apps/web/src/api/pluginData.ts
export interface PluginDataRecord { id, data: Record<string,unknown>, created_at, updated_at, version }
export async function listPluginData(pluginId, entity, page?, pageSize?)
export async function getPluginData(pluginId, entity, id)
export async function createPluginData(pluginId, entity, data)
export async function updatePluginData(pluginId, entity, id, data, version)
export async function deletePluginData(pluginId, entity, id)
8A.3 Plugin Store
新建: apps/web/src/stores/plugin.ts
interface PluginStore {
plugins: PluginInfo[]
loading: boolean
pluginMenuItems: PluginMenuItem[]
fetchPlugins: (page?, status?) => Promise<void>
refreshMenuItems: () => void
}
interface PluginMenuItem { key: string, icon: string, label: string, pluginId: string, entity: string, menuGroup?: string }
8A 文件清单:
| 操作 | 文件 |
|---|---|
| 新建 | apps/web/src/api/plugins.ts |
| 新建 | apps/web/src/api/pluginData.ts |
| 新建 | apps/web/src/stores/plugin.ts |
Phase 8B: 插件管理页面
8B.1 PluginAdmin 页面
新建: apps/web/src/pages/PluginAdmin.tsx
遵循 Users.tsx 模式:
- Table 列: name, version, status(Tag 颜色), author, actions
- Upload Modal: Upload 组件(拖拽 .wasm) + TextArea(manifest TOML)
- Detail Drawer: manifest JSON, entities, config, health
- Actions 按钮根据 status 动态显示: Install/Enable/Disable/Uninstall
const STATUS_CONFIG = {
uploaded: { color: '#64748B', label: '已上传' },
installed: { color: '#2563EB', label: '已安装' },
enabled: { color: '#059669', label: '已启用' },
running: { color: '#059669', label: '运行中' },
disabled: { color: '#DC2626', label: '已禁用' },
uninstalled: { color: '#9333EA', label: '已卸载' },
}
8B.2 路由 + 侧边栏
修改: App.tsx — 添加 lazy(() => import('./pages/PluginAdmin')) + <Route path="/plugins/admin" ...>
修改: MainLayout.tsx
- sysMenuItems 添加
{ key: '/plugins/admin', icon: <AppstoreOutlined />, label: '插件管理' } - routeTitleMap 添加
'/plugins/admin': '插件管理' - 添加动态插件菜单组(从 pluginStore.pluginMenuItems 生成)
8B 文件清单:
| 操作 | 文件 |
|---|---|
| 新建 | apps/web/src/pages/PluginAdmin.tsx |
| 修改 | apps/web/src/App.tsx |
| 修改 | apps/web/src/layouts/MainLayout.tsx |
Phase 8C: 动态路由 + PluginCRUDPage
8C.1 PluginCRUDPage
新建: apps/web/src/pages/PluginCRUDPage.tsx
通用配置驱动 CRUD 页面:
- 从 URL params 获取
pluginId+entityName - 调用
getPluginSchema(pluginId)获取字段定义 - 自动生成 Table columns(从 entity.fields)
- 自动生成 Form fields(根据 ui_widget: text/number/select/date/switch)
- CRUD 操作调用 pluginData API
export default function PluginCRUDPage() {
const { pluginId, entityName } = useParams();
// fetch schema → generate columns → render Table + Modal form
}
8C.2 动态路由
修改: App.tsx — 添加:
<Route path="/plugins/:pluginId/:entityName" element={<PluginCRUDPage />} />
8C 文件清单:
| 操作 | 文件 |
|---|---|
| 新建 | apps/web/src/pages/PluginCRUDPage.tsx |
| 修改 | apps/web/src/App.tsx |
Phase 8D: E2E 验证
8D.1 测试插件 manifest
新建: crates/erp-plugin-test-sample/plugin.toml — 包含完整 schema/events/ui/permissions 定义
修改: crates/erp-plugin-test-sample/src/lib.rs — 适配最终 WIT 接口
8D.2 启动时恢复插件
修改: main.rs — 启动时查询 plugins(status=running) → 逐个 engine.load + initialize + start_event_listener
8D.3 验证清单
手动 E2E 测试流程:
- 编译测试插件:
cargo build -p erp-plugin-test-sample --target wasm32-unknown-unknown --release - 转换:
wasm-tools component new ... -o target/test-sample.component.wasm - 打包 manifest.toml + .component.wasm
- 通过 PluginAdmin 上传
- 安装 → 验证动态表创建
- 启用 → 验证 init() 调用成功
- 通过 PluginCRUDPage 创建/读取/更新/删除数据
- 触发 workflow.task.completed 事件 → 验证插件 handle_event 被调用
- 停用 → 验证事件订阅取消
- 卸载 → 验证动态表清理
文件统计
| Phase | 新建 | 修改 | 合计 |
|---|---|---|---|
| 7A | 0 | 4 | 4 |
| 7B | 9 | 1 | 10 |
| 7C | 5 | 1 | 6 |
| 7D | 5 | 4 | 9 |
| 7E | 3 | 1 | 4 |
| 8A | 3 | 0 | 3 |
| 8B | 1 | 2 | 3 |
| 8C | 1 | 1 | 2 |
| 8D | 1 | 2 | 3 |
| 合计 | 28 | 16 | 44 |
验证方式
每个 Phase 完成后:
cargo check全 workspace 编译通过cargo test --workspace测试通过- Phase 7 完成后:
cargo run -p erp-server启动成功,API 端点可用 - Phase 8 完成后:
pnpm dev前端启动,PluginAdmin 页面可访问,完整 CRUD 链路可用