# 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](crates/erp-core/src/events.rs) ```rust // 新增方法 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 { ... } } 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](crates/erp-core/src/module.rs) ```rust // 新增枚举 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 { Ok(serde_json::json!({"status": "healthy"})) } ``` ### 7A.3 ModuleRegistry v2 **修改**: [module.rs](crates/erp-core/src/module.rs) ```rust impl ModuleRegistry { // 新增方法 pub fn sorted_modules(&self) -> Vec> // 拓扑排序 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)> pub fn get_module(&self, name: &str) -> Option> } ``` 拓扑排序: Kahn 算法,环检测返回 `AppError::Validation`。 ### 7A.4 服务启动集成 **修改**: [main.rs](crates/erp-server/src/main.rs) — 在 `registry.register_handlers` 之后添加: ```rust let module_ctx = ModuleContext { db: db.clone(), event_bus: event_bus.clone() }; registry.startup_all(&module_ctx).await?; ``` **修改**: [lib.rs](crates/erp-core/src/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` ```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](Cargo.toml) — workspace members 添加 `"crates/erp-plugin"`,dependencies 添加 `erp-plugin = { path = "crates/erp-plugin" }` ### 7B.2 错误类型 **新建**: `crates/erp-plugin/src/error.rs` ```rust 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 for AppError pub type PluginResult = Result; ``` ### 7B.3 插件清单解析 **新建**: `crates/erp-plugin/src/manifest.rs` ```rust pub struct PluginManifest { pub metadata: PluginMetadata, pub schema: Option, pub events: Option, pub ui: Option, pub permissions: Option>, } pub struct PluginMetadata { id, name, version, description, author, min_platform_version, dependencies } pub struct PluginSchema { pub entities: Vec } pub struct PluginEntity { name, display_name, fields: Vec, 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 } pub struct PluginUi { pub pages: Vec } pub struct PluginPage { route, entity, display_name, icon, menu_group } pub struct PluginPermission { code, name, description } pub fn parse_manifest(toml_str: &str) -> PluginResult ``` ### 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 操作 ```rust pub struct HostState { pub(crate) limits: StoreLimits, pub(crate) tenant_id: Uuid, pub(crate) user_id: Uuid, pub(crate) permissions: Vec, pub(crate) plugin_id: String, // 预填充的读取缓存 pub(crate) query_results: HashMap>, pub(crate) config_cache: HashMap>, pub(crate) current_user_json: Vec, // 待刷新的写操作 pub(crate) pending_ops: Vec, pub(crate) logs: Vec<(String, String)>, } pub enum PendingOp { Insert { entity: String, data: Vec }, Update { entity: String, id: String, data: Vec, version: i64 }, Delete { entity: String, id: String }, PublishEvent { event_type: String, payload: Vec }, } ``` ### 7B.5 插件引擎 **新建**: `crates/erp-plugin/src/engine.rs` ```rust pub struct PluginEngine { engine: wasmtime::Engine, db: DatabaseConnection, event_bus: EventBus, plugins: Arc>, 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, pub status: PluginStatus, pub event_handle: Option, } pub enum PluginStatus { Loaded, Initialized, Running, Error(String), Disabled } // 核心方法 impl PluginEngine { pub fn new(db, event_bus, config) -> Result 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 pub fn list_plugins(&self) -> Vec pub fn get_manifest(&self, plugin_id) -> Option // 内部: spawn_blocking + catch_unwind + fuel 限制 + timeout async fn execute_wasm(&self, plugin_id, operation: F) -> Result // 内部: 刷新 pending_ops 到真实 DB async fn flush_ops(&self, state: &HostState) -> Result<()> } ``` `execute_wasm` 流程: 1. 从 DashMap 获取 LoadedPlugin 2. 创建新 Store + HostState(预填充读数据) 3. `tokio::task::spawn_blocking` 包装 WASM 调用 4. 内部 `std::panic::catch_unwind(AssertUnwindSafe(...))` 5. 返回后 `flush_ops` 执行真实 DB 操作 6. 外层 `tokio::time::timeout` 限制执行时间 ### 7B.6 动态表管理器 **新建**: `crates/erp-plugin/src/dynamic_table.rs` ```rust 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 pub fn table_name(plugin_id, entity_name) -> String // "plugin_{sanitized_id}_{entity}" pub fn build_insert_sql(table_name, data) -> (String, Vec) pub fn build_query_sql(table_name, filter, pagination) -> (String, Vec) pub fn build_update_sql(table_name, id, data, version) -> (String, Vec) pub fn build_delete_sql(table_name, id) -> (String, Vec) } ``` 动态表结构: `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` ```rust #[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](crates/erp-server/migration/src/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` ```rust // 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` ```rust pub struct PluginService; impl PluginService { pub async fn upload(tenant_id, operator_id, wasm_binary, manifest_toml, db) -> AppResult pub async fn install(plugin_id, tenant_id, operator_id, db, engine) -> AppResult pub async fn enable(plugin_id, tenant_id, operator_id, db, engine) -> AppResult pub async fn disable(plugin_id, tenant_id, operator_id, db, engine) -> AppResult pub async fn uninstall(plugin_id, tenant_id, operator_id, db, engine) -> AppResult pub async fn list(tenant_id, pagination, status, search, db) -> AppResult<(Vec, u64)> pub async fn get_by_id(plugin_id, tenant_id, db) -> AppResult pub async fn update_config(plugin_id, tenant_id, operator_id, req, db) -> AppResult pub async fn health_check(plugin_id, tenant_id, db, engine) -> AppResult pub async fn get_schema(plugin_id, tenant_id, db) -> AppResult } ``` 生命周期状态机: `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`, `Extension`, `require_permission("plugin.admin")`, utoipa 注解 ### 7D.4 Module 注册 **新建**: `crates/erp-plugin/src/module.rs` ```rust pub struct PluginModule; impl ErpModule for PluginModule { name="plugin", dependencies=["auth","config"], module_type=Builtin } impl PluginModule { pub fn protected_routes() -> Router // 上述所有路由 } ``` ### 7D.5 服务端集成 **修改**: [main.rs](crates/erp-server/src/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](crates/erp-server/src/state.rs) - AppState 新增 `pub plugin_engine: erp_plugin::engine::PluginEngine` - 添加 `FromRef for erp_plugin::PluginState` **修改**: [seed.rs](crates/erp-auth/src/service/seed.rs) — 添加 `plugin.admin`, `plugin.list` 权限种子 **修改**: [Cargo.toml](crates/erp-server/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` ```rust 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` ```rust pub struct PluginDataService; impl PluginDataService { pub async fn create(plugin_id, entity_name, tenant_id, operator_id, req, db, event_bus) -> AppResult pub async fn list(plugin_id, entity_name, tenant_id, pagination, search, db) -> AppResult<(Vec, u64)> pub async fn get_by_id(plugin_id, entity_name, id, tenant_id, db) -> AppResult pub async fn update(plugin_id, entity_name, id, tenant_id, operator_id, req, db, event_bus) -> AppResult 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](crates/erp-plugin/src/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` ```typescript 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` ```typescript export interface PluginDataRecord { id, data: Record, 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` ```typescript interface PluginStore { plugins: PluginInfo[] loading: boolean pluginMenuItems: PluginMenuItem[] fetchPlugins: (page?, status?) => Promise 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 ```typescript 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](apps/web/src/App.tsx) — 添加 `lazy(() => import('./pages/PluginAdmin'))` + `` **修改**: [MainLayout.tsx](apps/web/src/layouts/MainLayout.tsx) - sysMenuItems 添加 `{ key: '/plugins/admin', icon: , 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 ```typescript export default function PluginCRUDPage() { const { pluginId, entityName } = useParams(); // fetch schema → generate columns → render Table + Modal form } ``` ### 8C.2 动态路由 **修改**: [App.tsx](apps/web/src/App.tsx) — 添加: ```typescript } /> ``` **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](crates/erp-server/src/main.rs) — 启动时查询 plugins(status=running) → 逐个 engine.load + initialize + start_event_listener ### 8D.3 验证清单 手动 E2E 测试流程: 1. 编译测试插件: `cargo build -p erp-plugin-test-sample --target wasm32-unknown-unknown --release` 2. 转换: `wasm-tools component new ... -o target/test-sample.component.wasm` 3. 打包 manifest.toml + .component.wasm 4. 通过 PluginAdmin 上传 5. 安装 → 验证动态表创建 6. 启用 → 验证 init() 调用成功 7. 通过 PluginCRUDPage 创建/读取/更新/删除数据 8. 触发 workflow.task.completed 事件 → 验证插件 handle_event 被调用 9. 停用 → 验证事件订阅取消 10. 卸载 → 验证动态表清理 --- ## 文件统计 | 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 链路可用