Files
erp/plans/zany-wobbling-shannon.md
iven 841766b168
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix(用户管理): 修复用户列表页面加载失败问题
修复用户列表页面加载失败导致测试超时的问题,确保页面元素正确渲染
2026-04-19 08:46:28 +08:00

764 lines
26 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.

# 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<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 channelcapacity 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<serde_json::Value> {
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<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](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<PluginError> for AppError
pub type PluginResult<T> = Result<T, PluginError>;
```
### 7B.3 插件清单解析
**新建**: `crates/erp-plugin/src/manifest.rs`
```rust
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→ 调用前预填充 HostStateHost 方法直接返回缓存数据
- 写操作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<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`
```rust
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` 流程:
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<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`
```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<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`
```rust
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](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<AppState> 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<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](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<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`
```typescript
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
```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'))` + `<Route path="/plugins/admin" ...>`
**修改**: [MainLayout.tsx](apps/web/src/layouts/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
```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
<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](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 链路可用