fix(用户管理): 修复用户列表页面加载失败问题
修复用户列表页面加载失败导致测试超时的问题,确保页面元素正确渲染
This commit is contained in:
763
plans/zany-wobbling-shannon.md
Normal file
763
plans/zany-wobbling-shannon.md
Normal file
@@ -0,0 +1,763 @@
|
||||
# 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 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<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)→ 调用前预填充 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<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 链路可用
|
||||
Reference in New Issue
Block a user