562 lines
22 KiB
Markdown
562 lines
22 KiB
Markdown
# CRM WASM 插件实现可行性审查报告
|
||
|
||
## 审查范围
|
||
|
||
基于对以下核心代码的完整审查:
|
||
- `crates/erp-plugin/wit/plugin.wit` — WIT 接口定义
|
||
- `crates/erp-plugin/src/host.rs` — Host API 实现
|
||
- `crates/erp-plugin/src/engine.rs` — WASM 引擎(Fuel 限制、执行流程)
|
||
- `crates/erp-plugin/src/manifest.rs` — 插件清单结构
|
||
- `crates/erp-plugin/src/dynamic_table.rs` — 动态表管理(建表、CRUD SQL)
|
||
- `crates/erp-plugin/src/data_service.rs` — 数据服务层
|
||
- `crates/erp-plugin/src/data_handler.rs` — API handler(权限检查)
|
||
- `crates/erp-plugin/src/service.rs` — 插件生命周期管理
|
||
- `crates/erp-plugin/src/module.rs` — 路由注册
|
||
- `apps/web/src/pages/PluginCRUDPage.tsx` — 前端通用 CRUD 页面
|
||
- `apps/web/src/stores/plugin.ts` — 插件菜单 store
|
||
- `crates/erp-server/migration/src/m20260417_000034_seed_plugin_permissions.rs` — 权限种子数据
|
||
|
||
---
|
||
|
||
## 1. WASM 插件逻辑复杂度
|
||
|
||
### init() — 需要做什么
|
||
|
||
CRM 插件的 `init()` 应当做以下事情:
|
||
- 日志记录初始化信息
|
||
- 可选:通过 `config_get` 读取 CRM 相关配置
|
||
- 可选:通过 `db_insert` 创建默认字典数据(如客户类型、级别、来源等默认选项)
|
||
|
||
**复杂度:低。** 主要是 `log_write` + 若干 `db_insert` 插入初始配置数据。涉及的逻辑只是 JSON 构造和 Host API 调用。1000 万 Fuel 绰绰有余。
|
||
|
||
### on_tenant_created() — 需要做什么
|
||
|
||
为新建租户创建 CRM 默认数据:
|
||
- 插入默认客户类型字典(潜在客户、意向客户、成交客户、流失客户)
|
||
- 插入默认行业分类
|
||
- 插入默认客户来源选项
|
||
- 可选:创建默认客户级别(A/B/C/D)
|
||
|
||
**复杂度:低-中。** 纯数据初始化,大概需要 10-20 次 `db_insert` 调用。每次 `db_insert` 只是构造 JSON + 调用 Host API 入队 pending_ops,不涉及复杂计算。Fuel 消耗估算:每次 Host API 调用约 5000-10000 fuel,20 次调用约 20 万 fuel,远低于 1000 万限制。
|
||
|
||
### handle_event() — 需要做什么
|
||
|
||
取决于 CRM 订阅了哪些事件。典型场景:
|
||
- 订阅 `user.created`:自动为客户经理创建关联记录
|
||
- 订阅 `workflow.task.completed`:处理客户审批流程完成事件
|
||
- 订阅自定义 CRM 事件:如 `crm.customer.created` 触发欢迎邮件模板创建
|
||
|
||
**复杂度:中。** 需要模式匹配 `event_type`,解析 JSON payload,可能需要 `db_query` 查询关联数据,然后 `db_insert`/`db_update` 写入结果。关键限制是 `db_query` 只能访问预填充的数据。
|
||
|
||
### Fuel 限制评估
|
||
|
||
当前默认 Fuel 为 1000 万(`engine.rs` 第 32 行)。对于 CRM 插件的三种场景:
|
||
|
||
| 场景 | 预估 Fuel | 是否足够 |
|
||
|------|-----------|----------|
|
||
| init() | 5-20 万 | 充裕 |
|
||
| on_tenant_created() | 10-30 万 | 充裕 |
|
||
| handle_event() | 5-50 万 | 充裕 |
|
||
|
||
**结论:Fuel 限制不是瓶颈。** 1000 万 fuel 对 CRM 的业务逻辑(JSON 解析 + 若干 Host API 调用)绰绰有余。真正的复杂业务逻辑(报表聚合、批量操作)应放在 Host 端而不是 WASM 内部。
|
||
|
||
---
|
||
|
||
## 2. 动态表 Schema 声明 — Select 类型
|
||
|
||
### 当前支持
|
||
|
||
`manifest.rs` 第 66-76 行定义了 `PluginFieldType` 枚举:
|
||
```rust
|
||
pub enum PluginFieldType {
|
||
String, Integer, Float, Boolean, Date, DateTime, Json, Uuid, Decimal,
|
||
}
|
||
```
|
||
|
||
没有 `Select` 或 `Enum` 类型。
|
||
|
||
### 解决方案:不需要扩展 manifest schema
|
||
|
||
当前 `PluginField` 已经有两个关键字段(`manifest.rs` 第 50-61 行):
|
||
|
||
```rust
|
||
pub struct PluginField {
|
||
pub name: String,
|
||
pub field_type: PluginFieldType,
|
||
pub ui_widget: Option<String>, // <-- 关键:可指定前端渲染组件
|
||
pub options: Option<Vec<serde_json::Value>>, // <-- 关键:下拉选项列表
|
||
...
|
||
}
|
||
```
|
||
|
||
`ui_widget` 和 `options` 已经提供了 Select 的完整支持。前端 `PluginCRUDPage.tsx` 第 178-185 行已经处理了 `select` widget:
|
||
|
||
```typescript
|
||
case 'select':
|
||
return (
|
||
<Select>
|
||
{(field.options || []).map((opt) => (
|
||
<Select.Option key={String(opt.value)} value={opt.value}>
|
||
{opt.label}
|
||
</Select.Option>
|
||
))}
|
||
</Select>
|
||
);
|
||
```
|
||
|
||
**结论:CRM 的 select 字段不需要扩展 manifest。** 在 plugin.toml 中声明方式:
|
||
|
||
```toml
|
||
[[schema.entities.fields]]
|
||
name = "customer_type"
|
||
field_type = "string"
|
||
ui_widget = "select"
|
||
options = [
|
||
{ label = "潜在客户", value = "potential" },
|
||
{ label = "意向客户", value = "intention" },
|
||
{ label = "成交客户", value = "closed" },
|
||
{ label = "流失客户", value = "churned" },
|
||
]
|
||
```
|
||
|
||
底层存储仍为 `string`(JSONB 的 `data->>'customer_type'`),数据库层不需要 enum 约束。
|
||
|
||
---
|
||
|
||
## 3. 关联实体查询
|
||
|
||
### 当前 db-query 的限制
|
||
|
||
查看 `host.rs` 第 99-109 行,`db_query` 的实现:
|
||
|
||
```rust
|
||
fn db_query(&mut self, entity: String, _filter: Vec<u8>, _pagination: Vec<u8>) -> Result<Vec<u8>, String> {
|
||
self.query_results
|
||
.get(&entity)
|
||
.cloned()
|
||
.ok_or_else(|| format!("实体 '{}' 的查询结果未预填充", entity))
|
||
}
|
||
```
|
||
|
||
**关键发现:当前 db-query 完全依赖预填充。** `filter` 和 `pagination` 参数被忽略(`_filter`, `_pagination`)。查询结果在 WASM 执行前由 Host 预填充到 `HostState.query_results` 中。
|
||
|
||
但 `engine.rs` 的 `execute_wasm` 方法(第 444-522 行)中并没有看到预填充查询结果的逻辑。`HostState::new` 只初始化了空的 `query_results: HashMap::new()`。
|
||
|
||
### CRM 关联查询场景
|
||
|
||
"联系人列表按 customer_id 过滤" 的需求有两种实现路径:
|
||
|
||
**路径 A:扩展 Host API 的 db-query(推荐)**
|
||
|
||
需要修改 `engine.rs` 的 `execute_wasm` 方法,在 WASM 执行前解析 filter 参数,执行真实 SQL 查询,将结果预填充。或者更直接的方案——让 `db_query` 在调用时实时执行查询,而不是依赖预填充。
|
||
|
||
这意味着需要在 `HostState` 中持有 `DatabaseConnection` 的引用,或者将 `db_query` 改为延迟执行模式。但当前架构是 WASM 在 `spawn_blocking` 中同步执行,无法直接持有异步的 DB 连接。
|
||
|
||
可行的改造方案:
|
||
1. `db_query` 仍然走预填充模式,但在 `execute_wasm` 前根据 WASM 函数类型智能预填充(不现实,因为不知道插件会查什么)
|
||
2. 改为同步查询模式:在 `HostState` 中持有同步的 DB 连接(需要 `blocking_spawn` 内部再 spawn async task)
|
||
3. **最佳方案**:前端直接调用 REST API 查询关联数据,不走 WASM 的 db-query
|
||
|
||
**路径 B:前端直接按 customer_id 过滤(当前最可行)**
|
||
|
||
CRM 的关联查询(联系人与客户、沟通记录与客户)不需要走 WASM 的 `db-query`。前端 `PluginCRUDPage` 已经可以直接调用 `GET /api/v1/plugins/{plugin_id}/{entity}?page=1` 来获取联系人列表。
|
||
|
||
但当前 REST API 的 `list_plugin_data`(`data_handler.rs` 第 25-57 行)也不支持过滤参数。`PluginDataListParams` 只有 `page`, `page_size`, `search`。
|
||
|
||
**需要做的改造:**
|
||
|
||
1. **后端**:在 `PluginDataListParams` 中增加 `filter` 参数(JSON 格式,如 `{"customer_id": "xxx"}`)
|
||
2. **后端**:在 `PluginDataService::list` 中解析 filter,构建带 WHERE 条件的 SQL
|
||
3. **前端**:`PluginCRUDPage` 接收 URL 参数如 `?filter[customer_id]=xxx`,传给 API
|
||
|
||
`dynamic_table.rs` 需要新增一个 `build_filtered_query_sql` 方法:
|
||
|
||
```rust
|
||
pub fn build_filtered_query_sql(
|
||
table_name: &str,
|
||
tenant_id: Uuid,
|
||
filters: &serde_json::Value, // {"customer_id": "xxx", "status": "active"}
|
||
limit: u64,
|
||
offset: u64,
|
||
) -> (String, Vec<Value>)
|
||
```
|
||
|
||
使用 PostgreSQL 的 JSONB 查询操作符:`data->>'customer_id' = $N`。
|
||
|
||
**结论:关联查询需要扩展现有 API,但改造量不大。** 主要是在 REST API 层增加 filter 支持,不需要改 WASM 运行时。这是中等工作量的改造。
|
||
|
||
---
|
||
|
||
## 4. 唯一性约束
|
||
|
||
### customer.code 唯一性
|
||
|
||
`dynamic_table.rs` 第 67-87 行,建表时会为 `unique: true` 的字段创建索引:
|
||
|
||
```rust
|
||
if field.unique || field.required {
|
||
let idx_sql = format!(
|
||
"CREATE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{table_name}\" \
|
||
(\"data\"->>'{sanitized_field}') WHERE \"deleted_at\" IS NULL"
|
||
);
|
||
}
|
||
```
|
||
|
||
但这里只创建了普通索引(`CREATE INDEX`),不是唯一索引。`field.unique` 判断只影响了索引名称后缀(`"uniq"` vs `"idx"`),索引类型始终是 `CREATE INDEX`,不是 `CREATE UNIQUE INDEX`。
|
||
|
||
**Bug:unique 字段没有创建唯一索引。**
|
||
|
||
此外,`engine.rs` 的 `flush_ops` 中 `PendingOp::Insert`(第 543-560 行)执行的是普通 INSERT,没有做唯一性检查:
|
||
|
||
```rust
|
||
let (sql, values) = DynamicTableManager::build_insert_sql_with_id(
|
||
&table_name, id_uuid, tenant_id, user_id, &parsed_data
|
||
);
|
||
txn.execute(Statement::from_sql_and_values(...)).await?;
|
||
```
|
||
|
||
`build_insert_sql_with_id`(`dynamic_table.rs` 第 141-162 行)也是普通 INSERT,没有 ON CONFLICT 处理。
|
||
|
||
### 需要修复
|
||
|
||
1. **建表时**:`field.unique` 应创建 `CREATE UNIQUE INDEX`
|
||
2. **INSERT 时**:需要检查 unique 字段的值是否已存在(或使用 `ON CONFLICT`)
|
||
3. **REST API 层**:`PluginDataService::create` 也需要同样的检查
|
||
|
||
修复方案(`dynamic_table.rs` 建表部分):
|
||
|
||
```rust
|
||
let idx_sql = if field.unique {
|
||
format!(
|
||
"CREATE UNIQUE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{table_name}\" \
|
||
(\"data\"->>'{sanitized_field}') WHERE \"deleted_at\" IS NULL"
|
||
)
|
||
} else {
|
||
format!(
|
||
"CREATE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{table_name}\" \
|
||
(\"data\"->>'{sanitized_field}') WHERE \"deleted_at\" IS NULL"
|
||
)
|
||
};
|
||
```
|
||
|
||
INSERT 前需要做存在性检查,或使用事务内 SELECT + INSERT 模式。
|
||
|
||
**结论:唯一性约束是必须修复的 bug,否则 CRM 的 customer.code 无法保证唯一。** 改动量小但重要性高。
|
||
|
||
---
|
||
|
||
## 5. 前端通用组件实现量
|
||
|
||
### 5.1 tree(树形页面)
|
||
|
||
**用途**:组织架构树、客户层级树(parent_id 关系)
|
||
|
||
**Ant Design 基础组件**:`Tree` / `DirectoryTree` / `TreeSelect`
|
||
|
||
**开发量**:2-3 天
|
||
- Schema 解析:从 manifest 中识别 `parent_id` 字段作为树形关系
|
||
- 数据加载:一次性加载全量数据,前端组装树结构
|
||
- CRUD 操作:内联新增/编辑/删除节点
|
||
- 拖拽排序(可选)
|
||
|
||
**关键依赖**:需要 `PluginCRUDPage` 支持全量加载模式(不分页),当前 `data_service.rs` 的 `list` 只支持分页。
|
||
|
||
### 5.2 graph(关系图)
|
||
|
||
**用途**:客户关系图(customer_relationship 实体)
|
||
|
||
**Ant Design 基础组件**:没有内置 graph 组件
|
||
|
||
**需要引入的库**:`@ant-design/charts`(G6 封装)或 `reactflow`
|
||
|
||
**开发量**:5-8 天
|
||
- 图数据转换:将 customer + customer_relationship 数据转换为 nodes/edges
|
||
- 图布局算法:力导向布局 / 层次布局
|
||
- 交互:节点点击查看详情、拖拽、缩放
|
||
- 工具栏:放大/缩小/导出
|
||
|
||
**技术风险**:高。图可视化是这 5 种页面类型中最复杂的。需要考虑性能(100+ 节点时的渲染)。
|
||
|
||
### 5.3 timeline(时间线)
|
||
|
||
**用途**:沟通记录时间线、客户跟进历史
|
||
|
||
**Ant Design 基础组件**:`Timeline` / `Steps`
|
||
|
||
**开发量**:1-2 天
|
||
- 数据排序:按 `occurred_at` 排序
|
||
- 渲染:不同 `type`(电话/邮件/会议)显示不同图标和颜色
|
||
- 交互:点击展开详情
|
||
|
||
**最简单的页面类型**,Ant Design Timeline 直接可用。
|
||
|
||
### 5.4 tabs(标签页嵌套)
|
||
|
||
**用途**:客户详情页包含多个标签(基本信息/联系人/沟通记录/标签/关系)
|
||
|
||
**Ant Design 基础组件**:`Tabs`
|
||
|
||
**开发量**:3-5 天
|
||
- Manifest 扩展:定义 tabs 嵌套结构
|
||
- 组件组合:每个 tab 内部是一个子页面(可能是 crud/timeline/graph)
|
||
- 数据关联:子页面需要接收父实体的 ID 作为过滤条件
|
||
- 路由:需要处理嵌套路由或状态切换
|
||
|
||
**核心挑战**:tabs 嵌套需要 manifest 结构支持 `children` 或 `tabs` 字段,并且在运行时动态组合不同类型的页面组件。
|
||
|
||
### 5.5 dashboard(仪表盘)
|
||
|
||
**用途**:CRM 概览(客户总数、本月新增、跟进中、转化率等)
|
||
|
||
**Ant Design 基础组件**:`Statistic` / `Card` / `@ant-design/charts`
|
||
|
||
**开发量**:5-7 天
|
||
- 统计卡片:数值展示 + 趋势箭头
|
||
- 图表:柱状图/折线图/饼图(需要聚合查询 API)
|
||
- 筛选器:时间范围、客户类型等
|
||
- 实时更新(可选)
|
||
|
||
**关键依赖**:需要后端提供聚合查询 API(COUNT/GROUP BY),当前 `db_query` 不支持聚合。`@ant-design/charts` 需要作为新依赖安装。
|
||
|
||
### 汇总
|
||
|
||
| 页面类型 | 开发量 | 技术风险 | Ant Design 支持 |
|
||
|----------|--------|----------|-----------------|
|
||
| tree | 2-3 天 | 低 | Timeline/Tree 组件直接可用 |
|
||
| graph | 5-8 天 | 高 | 需要引入第三方库 |
|
||
| timeline | 1-2 天 | 低 | Timeline 组件直接可用 |
|
||
| tabs | 3-5 天 | 中 | Tabs 可用,但需要组合其他页面类型 |
|
||
| dashboard | 5-7 天 | 中 | Statistic/Card 可用,图表需第三方库 |
|
||
|
||
**总计**:16-25 天(约 3-5 周)的前端开发量。建议分两期:
|
||
- 第一期(1-2 周):timeline + tree + tabs(CRM 核心需要)
|
||
- 第二期(2-3 周):dashboard + graph(锦上添花)
|
||
|
||
---
|
||
|
||
## 6. manifest.ui.pages 解析
|
||
|
||
### 当前结构
|
||
|
||
`manifest.rs` 第 94-109 行:
|
||
|
||
```rust
|
||
pub struct PluginUi {
|
||
pub pages: Vec<PluginPage>,
|
||
}
|
||
|
||
pub struct PluginPage {
|
||
pub route: String,
|
||
pub entity: String,
|
||
pub display_name: String,
|
||
pub icon: String,
|
||
pub menu_group: Option<String>,
|
||
}
|
||
```
|
||
|
||
### 需要扩展为
|
||
|
||
```rust
|
||
pub struct PluginPage {
|
||
pub route: String,
|
||
pub entity: String,
|
||
pub display_name: String,
|
||
pub icon: String,
|
||
pub menu_group: Option<String>,
|
||
// 新增
|
||
pub page_type: Option<String>, // "crud" | "tree" | "graph" | "timeline" | "tabs" | "dashboard"
|
||
pub tabs: Option<Vec<PluginTab>>, // tabs 嵌套
|
||
pub field_mappings: Option<HashMap<String, String>>, // 字段映射配置
|
||
pub filters: Option<Vec<PluginFilter>>, // 过滤器配置
|
||
}
|
||
|
||
pub struct PluginTab {
|
||
pub label: String,
|
||
pub entity: String,
|
||
pub page_type: Option<String>,
|
||
pub filters: Option<Vec<PluginFilter>>,
|
||
}
|
||
|
||
pub struct PluginFilter {
|
||
pub field: String,
|
||
pub source: String, // "url_param" | "parent_entity" | "fixed"
|
||
pub value: Option<String>,
|
||
}
|
||
```
|
||
|
||
### 影响范围
|
||
|
||
1. **manifest.rs**:扩展 `PluginPage` 结构体 — 改动量小,只新增字段
|
||
2. **service.rs**:`install` 方法中需要将新的 manifest 数据存入 `plugin_entity.schema_json` — 无需改动(已序列化完整 entity 定义)
|
||
3. **前端 plugin store**:`plugin.ts` 的 `PluginMenuItem` 需要增加 `pageType` 字段 — 改动量小
|
||
4. **前端路由**:`App.tsx` 需要根据 `page_type` 渲染不同组件 — 中等改动
|
||
5. **前端 PluginCRUDPage**:需要重构为通用页面路由器,根据 page_type 分发到不同组件
|
||
|
||
**结论:manifest 结构扩展的改动量不大**(Rust 端约 30 行),但前端需要重构路由分发逻辑。对现有代码的影响是可控的,因为都是新增字段(`Option<>`),不破坏现有 manifest 解析。
|
||
|
||
---
|
||
|
||
## 7. 权限与数据隔离
|
||
|
||
### 当前权限模型
|
||
|
||
`data_handler.rs` 中的权限检查(第 35、80、116、143、181 行):
|
||
- 列表/详情:`require_permission(&ctx, "plugin.list")`
|
||
- 创建/更新/删除:`require_permission(&ctx, "plugin.admin")`
|
||
|
||
**所有插件数据共用 `plugin.list` 和 `plugin.admin` 两个权限码。**
|
||
|
||
`m20260417_000034_seed_plugin_permissions.rs` 也只种子了这两个权限。
|
||
|
||
### CRM 需要 8 个权限码
|
||
|
||
CRM 设计的权限码如 `crm.customer.list`、`crm.customer.create` 等。这些与当前的 `plugin.list`/`plugin.admin` 是两套完全不同的权限体系。
|
||
|
||
### check-permission Host API
|
||
|
||
`host.rs` 第 167-169 行:
|
||
|
||
```rust
|
||
fn check_permission(&mut self, permission: String) -> Result<bool, String> {
|
||
Ok(self.permissions.contains(&permission))
|
||
}
|
||
```
|
||
|
||
这个 API 检查的是当前用户的权限列表。`permissions` 来自 `ExecutionContext.permissions`(`engine.rs` 第 68 行),是外部传入的用户权限列表。
|
||
|
||
**关键问题:CRM 的 8 个权限码在数据库中不存在。**
|
||
|
||
当前权限系统的流程:
|
||
1. 权限定义在 `permissions` 表中(通过 migration seed 或 admin API 创建)
|
||
2. 通过 `role_permissions` 表分配给角色
|
||
3. 用户登录时,JWT 中间件从角色关联加载权限列表
|
||
|
||
CRM 插件的 `crm.customer.list` 等权限需要:
|
||
1. 在插件安装时,动态 INSERT 到 `permissions` 表
|
||
2. 分配给适当角色
|
||
3. 这样用户请求时 JWT 中间件才能加载这些权限
|
||
|
||
### 需要的改造
|
||
|
||
1. **安装时自动注册权限**:`service.rs` 的 `install` 方法需要遍历 manifest 中的 `permissions` 列表,INSERT 到 `permissions` 表
|
||
2. **REST API 权限映射**:`data_handler.rs` 的权限检查需要从固定 `plugin.list`/`plugin.admin` 改为动态检查——基于 manifest 中声明的权限码
|
||
3. **卸载时清理权限**:`uninstall` 方法需要清理 `permissions` 表和 `role_permissions` 表中的插件权限
|
||
|
||
**当前 manifest 已支持 permissions 声明**(`manifest.rs` 第 112-118 行的 `PluginPermission`),但 `service.rs` 的 `install` 方法没有将其写入 `permissions` 表。
|
||
|
||
**结论:权限系统需要中等改造。** 核心改动点在 `service.rs` 的 install/uninstall 方法中增加权限 CRUD,以及 `data_handler.rs` 中从固定权限改为动态权限检查。
|
||
|
||
---
|
||
|
||
## 8. 整体实施风险评估
|
||
|
||
### 高风险
|
||
|
||
#### R1:db-query Host API 不可用(风险等级:高)
|
||
|
||
**问题**:当前 `db_query` 依赖预填充,但 `execute_wasm` 中没有预填充逻辑。CRM 插件在 WASM 内部无法执行关联查询。
|
||
|
||
**影响**:WASM 内的 `handle_event` 无法查询关联数据。例如,收到 `workflow.task.completed` 事件后,无法查询关联的客户记录。
|
||
|
||
**缓解方案**:
|
||
- 短期:CRM 的关联查询全部走前端 REST API,WASM 内只做简单的 `db_insert`/`db_update`
|
||
- 长期:改造 `db_query` 为实时查询模式(需要在 `spawn_blocking` 中支持异步 DB 调用)
|
||
|
||
**建议**:CRM 的复杂查询(关联、聚合)不应在 WASM 内完成。WASM 插件的 `handle_event` 应限于简单的状态变更,复杂查询由前端直接调用 REST API。
|
||
|
||
#### R2:唯一索引 Bug(风险等级:高)
|
||
|
||
**问题**:`dynamic_table.rs` 的 `unique` 字段只创建了普通索引,不是唯一索引。INSERT 时也没有冲突检查。
|
||
|
||
**影响**:`customer.code` 无法保证唯一性,可能插入重复数据。
|
||
|
||
**修复量**:约 20 行代码改动。
|
||
|
||
#### R3:插件权限未注册到数据库(风险等级:高)
|
||
|
||
**问题**:manifest 声明了 permissions,但 install 时没有写入 permissions 表。
|
||
|
||
**影响**:CRM 的 8 个权限码在数据库中不存在,check-permission 永远返回 false,JWT 中间件也无法加载这些权限。
|
||
|
||
**修复量**:约 50 行代码改动(install/uninstall 方法增加权限 CRUD)。
|
||
|
||
### 中风险
|
||
|
||
#### R4:REST API 不支持过滤查询(风险等级:中)
|
||
|
||
**问题**:`PluginDataListParams` 只有分页参数,不支持字段过滤。CRM 需要按 `customer_id` 过滤联系人等场景。
|
||
|
||
**修复量**:约 80 行代码改动(后端 filter 解析 + SQL 构建 + 前端传参)。
|
||
|
||
#### R5:前端 tabs 嵌套实现复杂度(风险等级:中)
|
||
|
||
**问题**:tabs 页面类型需要支持动态组合不同子页面类型,且子页面需要从父页面获取过滤参数。这涉及组件设计模式的选择。
|
||
|
||
**缓解方案**:先实现基本的 tabs + crud 组合,graph/timeline 作为子页面类型后续迭代。
|
||
|
||
#### R6:前端页面类型路由分发(风险等级:中)
|
||
|
||
**问题**:当前 `App.tsx` 只有一个 `PluginCRUDPage` 路由。需要根据 manifest 中的 `page_type` 动态选择渲染组件。
|
||
|
||
**修复量**:约 100 行代码改动(新增路由分发组件 + 各页面类型组件)。
|
||
|
||
### 低风险
|
||
|
||
#### R7:Fuel 限制(风险等级:低)
|
||
|
||
1000 万 fuel 对 CRM 插件绰绰有余。无需改动。
|
||
|
||
#### R8:manifest 扩展(风险等级:低)
|
||
|
||
新增的 `page_type`、`tabs`、`filters` 字段都是 `Option<>`,不破坏现有解析。改动量约 30 行。
|
||
|
||
---
|
||
|
||
## 实施建议
|
||
|
||
### 必须先修复的 3 个问题(1-2 天)
|
||
|
||
1. **修复唯一索引**:`dynamic_table.rs` 中 `field.unique` 创建 `CREATE UNIQUE INDEX`
|
||
2. **权限注册**:`service.rs` 的 `install` 方法将 manifest.permissions 写入 permissions 表
|
||
3. **REST API 过滤**:`PluginDataListParams` 增加 filter 参数,`build_query_sql` 支持条件过滤
|
||
|
||
### 分期实施计划
|
||
|
||
#### 第一期:最小可用 CRM(2-3 周)
|
||
|
||
- 5 个 JSONB 实体(customer/contact/communication/customer_tag/customer_relationship)
|
||
- plugin.toml manifest 定义
|
||
- WASM 插件 Rust crate(init + on_tenant_created + handle_event 基础逻辑)
|
||
- 前端 CRUD 页面(复用已有 PluginCRUDPage)
|
||
- 前端 tabs 页面类型(客户详情页的基本信息+联系人+沟通记录)
|
||
- 前端 timeline 页面类型(沟通记录时间线)
|
||
- REST API filter 支持
|
||
|
||
#### 第二期:增强功能(2-3 周)
|
||
|
||
- 前端 tree 页面类型(客户层级树)
|
||
- 前端 dashboard 页面类型(CRM 概览统计)
|
||
- 后端聚合查询 API(COUNT/GROUP BY)
|
||
- graph 页面类型(客户关系图)
|
||
- 高级搜索/筛选
|
||
|
||
#### 第三期:优化打磨(1-2 周)
|
||
|
||
- WASM 内部 db-query 实时查询改造
|
||
- 批量操作支持
|
||
- 数据导入导出
|
||
- 性能优化(大量客户数据场景)
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
CRM 插件在当前 WASM 插件系统上的实现**整体可行**,但有几个前提条件需要先满足:
|
||
|
||
1. **必须修复**:唯一索引 bug、权限注册缺失、REST API 过滤能力缺失
|
||
2. **核心路径**:CRM 的数据操作通过 REST API 而非 WASM 内的 db-query 完成(这是正确的架构选择)
|
||
3. **WASM 角色定位**:WASM 负责生命周期钩子(init/on_tenant_created/handle_event),不负责复杂查询和聚合
|
||
4. **前端是主要工作量**:5 种新页面类型中,graph 和 dashboard 开发量最大,建议分两期交付
|
||
|
||
最大的技术风险不在 WASM 运行时本身(它已被验证),而在于周边配套设施(权限、过滤、前端组件)的完善程度。
|