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

26 KiB
Raw Blame History

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 channelcapacity 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→ 调用前预填充 HostStateHost 方法直接返回缓存数据
  • 写操作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 流程:

  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

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 测试流程:

  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 链路可用