668 lines
27 KiB
Markdown
668 lines
27 KiB
Markdown
# CRM WASM 插件架构审查报告
|
||
|
||
> 审查人:后端架构师
|
||
> 日期:2026-04-15
|
||
> 审查对象:CRM 客户管理 WASM 插件设计方案
|
||
|
||
---
|
||
|
||
## 0. 审查前提:当前系统实际状态
|
||
|
||
经过对代码库的详细审查,我确认以下事实作为审查基础:
|
||
|
||
- **WIT 接口** (`crates/erp-plugin/wit/plugin.wit`):9 个 Host API 函数(db-insert/db-query/db-update/db-delete/event-publish/config-get/log-write/current-user/check-permission)
|
||
- **动态表结构** (`crates/erp-plugin/src/dynamic_table.rs`):每张表只有 `id`, `tenant_id`, `data`(JSONB), `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted_at`, `version` 这 9 列。所有业务字段存储在 `data` JSONB 列中。
|
||
- **索引策略**:只为 `required` 和 `unique` 字段创建 GIN 索引(`data->>'field_name'`),没有复合索引能力。
|
||
- **查询能力** (`data_service.rs`):`list()` 方法只支持按 tenant_id 过滤 + 分页,**不支持任何业务字段过滤**。`PluginDataListParams` 只有 `page`, `page_size`, `search` 三个参数。
|
||
- **前端 CRUD** (`PluginCRUDPage.tsx`):纯粹的表单驱动,无过滤、无关联实体展示、无自定义操作。
|
||
- **设计规格文档** (`docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md`) 附录 D 中规划了 `db-aggregate` 接口,但**实际 WIT 文件中并未实现**。
|
||
- **设计规格文档**规划了 `render-page` / `handle-action` 插件导出函数,但**实际 WIT 中也没有**。
|
||
|
||
**结论:当前系统是一个 MVP 级别的插件原型,距离支撑 CRM 这种多实体、多关系的业务插件还有显著差距。**
|
||
|
||
---
|
||
|
||
## 1. JSONB 动态表能否支撑 CRM 数据模型?
|
||
|
||
### 1.1 结论:基本可行,但需要大幅增强查询能力
|
||
|
||
JSONB 存储本身不是问题。PostgreSQL 的 JSONB 在索引支持下,单字段等值查询和范围查询性能完全可以接受(毫秒级)。关键问题在于当前系统的**查询能力几乎为零**。
|
||
|
||
### 1.2 关系查询的实现路径
|
||
|
||
对于 CRM 中"A 的所有子客户"这类关系查询:
|
||
|
||
**方案 A:利用 JSONB 索引的等值过滤(推荐)**
|
||
|
||
`parent_id` 存储在 `data` JSONB 中。当前系统已有为 `required` 字段创建索引的逻辑:
|
||
|
||
```sql
|
||
-- 动态表已有的索引模式
|
||
CREATE INDEX idx_xxx_parent_id ON plugin_crm_customer (data->>'parent_id') WHERE deleted_at IS NULL;
|
||
```
|
||
|
||
查询"A 的直接子节点"只需:
|
||
|
||
```sql
|
||
SELECT * FROM plugin_crm_customer
|
||
WHERE tenant_id = $1 AND data->>'parent_id' = $2 AND deleted_at IS NULL;
|
||
```
|
||
|
||
这在有索引的情况下性能完全没问题,10 万条记录也能在 5ms 内返回。
|
||
|
||
**方案 B:递归查询(查询所有子孙节点)需要 db-query 增强**
|
||
|
||
当前的 `db_query` Host API 是预填充模式——Host 在执行 WASM 前就把查询结果塞进 `query_results` HashMap。WASM 内部只是读缓存。这意味着**插件无法控制 SQL 的构建过程**。
|
||
|
||
递归查询需要 PostgreSQL 的 `WITH RECURSIVE` CTE:
|
||
|
||
```sql
|
||
WITH RECURSIVE descendants AS (
|
||
SELECT id, data, 0 as depth FROM plugin_crm_customer
|
||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||
UNION ALL
|
||
SELECT c.id, c.data, d.depth + 1 FROM plugin_crm_customer c
|
||
JOIN descendants d ON c.data->>'parent_id' = d.id::text
|
||
WHERE c.tenant_id = $2 AND c.deleted_at IS NULL AND d.depth < 10
|
||
)
|
||
SELECT * FROM descendants ORDER BY depth;
|
||
```
|
||
|
||
**当前系统无法执行这种查询。** 需要 Host API 层面支持。
|
||
|
||
### 1.3 具体建议
|
||
|
||
1. **为外键字段显式声明索引**:在 `PluginField` 中新增 `indexed: bool` 属性,或者引入 `references` 属性自动创建索引。CRM 中 `contact.customer_id`、`communication.customer_id`、`customer_tag.customer_id`、`customer_relationship.from_customer_id`、`customer_relationship.to_customer_id` 这些外键字段必须有索引。
|
||
|
||
2. **对 unique 约束的处理需要修正**:当前 `customer.code` 声明为 unique,但索引只过滤了 `deleted_at IS NULL`。如果软删除后重建同名 code,唯一索引会冲突。需要在索引中加入排除已删除记录的逻辑,或使用 partial unique index:
|
||
|
||
```sql
|
||
CREATE UNIQUE INDEX idx_xxx_code_uniq ON plugin_crm_customer (data->>'code') WHERE deleted_at IS NULL;
|
||
```
|
||
|
||
当前代码已经使用了 `WHERE deleted_at IS NULL`,这一点做得正确。但要确认 `sanitize_identifier` 生成的索引名不会因为表名过长而截断。
|
||
|
||
---
|
||
|
||
## 2. Host API 限制:跨实体查询
|
||
|
||
### 2.1 问题诊断
|
||
|
||
这是 CRM 插件面临的最严重的架构缺陷。CRM 的核心场景全部涉及跨实体:
|
||
|
||
- **客户列表 + 联系人数量**:需要 JOIN 或子查询
|
||
- **客户详情页展示联系人列表**:需要按 `customer_id` 过滤 contact
|
||
- **沟通记录按客户筛选**:需要按 `customer_id` 过滤 communication
|
||
- **标签筛选客户**:需要按 `tag_name` 在 customer_tag 中查,再反查 customer
|
||
|
||
当前 `db_query` 的实现(`host.rs` 第 99-109 行)只是从预填充的 HashMap 中取缓存:
|
||
|
||
```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))
|
||
}
|
||
```
|
||
|
||
`_filter` 和 `_pagination` 参数被完全忽略了。
|
||
|
||
### 2.2 解决方案:引入结构化查询 API
|
||
|
||
不是加 JOIN(那会让 Host API 变成 SQL 构建器),而是引入**两级查询增强**:
|
||
|
||
**第一级(必须实现):单实体过滤查询**
|
||
|
||
在 Host API 中新增 `db-query-filtered`,或在现有 `db_query` 中实际处理 filter 参数:
|
||
|
||
```wit
|
||
/// 结构化过滤查询
|
||
db-query: func(entity: string, filter: list<u8>, pagination: list<u8>) -> result<list<u8>, string>;
|
||
```
|
||
|
||
filter 的 JSON 结构:
|
||
|
||
```json
|
||
{
|
||
"conditions": [
|
||
{"field": "customer_id", "op": "eq", "value": "uuid-here"},
|
||
{"field": "status", "op": "in", "value": ["active", "vip"]},
|
||
{"field": "occurred_at", "op": "gte", "value": "2026-01-01"}
|
||
],
|
||
"order_by": [{"field": "created_at", "dir": "desc"}],
|
||
"search": "关键词"
|
||
}
|
||
```
|
||
|
||
Host 层将其安全转换为参数化 SQL:
|
||
|
||
```sql
|
||
SELECT id, data, created_at, updated_at, version
|
||
FROM plugin_crm_contact
|
||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||
AND data->>'customer_id' = $2
|
||
ORDER BY created_at DESC
|
||
LIMIT $3 OFFSET $4
|
||
```
|
||
|
||
这样 CRM 插件就能实现"查某个客户的所有联系人"、"查某个客户的所有沟通记录"。
|
||
|
||
**第二级(必须实现):聚合查询**
|
||
|
||
设计规格中规划了 `db-aggregate` 但未实现。CRM 需要它来显示统计信息:
|
||
|
||
```wit
|
||
db-aggregate: func(entity: string, query: list<u8>) -> result<list<u8>, string>;
|
||
```
|
||
|
||
query 结构:
|
||
|
||
```json
|
||
{
|
||
"group_by": ["data->>'status'"],
|
||
"aggregates": [
|
||
{"alias": "count", "func": "count", "field": "id"},
|
||
{"alias": "total_value", "func": "sum", "field": "data->>'amount'"}
|
||
],
|
||
"filter": {"field": "data->>'level'", "op": "eq", "value": "vip"}
|
||
}
|
||
```
|
||
|
||
**第三级(推荐实现):跨实体关联查询**
|
||
|
||
不要实现通用 JOIN(太复杂且安全风险高),而是实现一种受限的"关联加载"模式:
|
||
|
||
```wit
|
||
/// 按外键值批量查询(一次加载多个关联记录)
|
||
db-query-batch: func(entity: string, field: string, ids: list<string>) -> result<list<u8>, string>;
|
||
```
|
||
|
||
这解决了 CRM 中最常见的 N+1 查询问题。例如加载 20 个客户后,一次性查出所有关联的联系人:
|
||
|
||
```json
|
||
// 请求:查 contact 表中 customer_id 在 [id1, id2, ...] 中的记录
|
||
{
|
||
"entity": "contact",
|
||
"field": "customer_id",
|
||
"ids": ["uuid-1", "uuid-2", "..."]
|
||
}
|
||
```
|
||
|
||
Host 生成:
|
||
|
||
```sql
|
||
SELECT id, data, created_at, updated_at, version
|
||
FROM plugin_crm_contact
|
||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||
AND data->>'customer_id' = ANY($2::text[])
|
||
```
|
||
|
||
### 2.3 关于"多次 API 调用"的问题
|
||
|
||
在 WASM 插件模型下,**每次 API 调用都是在同一个 Store 内完成的**,不涉及网络开销。多次 `db_query` 调用的成本是:
|
||
|
||
- WASM 到 Host 的函数调用(纳秒级)
|
||
- Host 从预填充缓存中读取(纳秒级)
|
||
|
||
所以即使需要多次调用,性能也不是问题。**真正的问题是当前预填充机制不支持按业务字段过滤**。只要实现了第一级的结构化过滤,CRM 插件的跨实体查询就可以通过以下模式实现:
|
||
|
||
```
|
||
// 插件内伪代码
|
||
let contacts = db_query("contact", {"field": "customer_id", "op": "eq", "value": customer_id}, pagination);
|
||
let communications = db_query("communication", {"field": "customer_id", "op": "eq", "value": customer_id}, pagination);
|
||
```
|
||
|
||
前端也可以直接通过 REST API 调用:
|
||
|
||
```
|
||
GET /api/v1/plugins/crm/contact?filter={"field":"customer_id","op":"eq","value":"uuid"}&page=1&page_size=20
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 关系图谱的可行性
|
||
|
||
### 3.1 结论:可行但有数据量上限
|
||
|
||
CRM 关系图谱的核心查询是:给定一个客户,找出它的所有直接关系(parent_child/partner/supplier 等)。
|
||
|
||
**查询模式**:
|
||
|
||
```sql
|
||
-- 查询 A 的所有关系
|
||
SELECT * FROM plugin_crm_customer_relationship
|
||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||
AND (data->>'from_customer_id' = $2 OR data->>'to_customer_id' = $2);
|
||
```
|
||
|
||
这个查询在索引支持下完全可行。需要为 `from_customer_id` 和 `to_customer_id` 都创建索引。
|
||
|
||
**前端加载策略**:
|
||
|
||
"加载全量关系+客户来渲染图谱"这个方案在以下条件下可行:
|
||
- 单租户客户量 < 5000
|
||
- 关系记录 < 10000
|
||
|
||
超出这个量级,需要改为"按中心节点展开"的模式:
|
||
1. 先加载中心客户的直接关系(1 跳)
|
||
2. 用户点击某个关联客户时,加载该客户的直接关系(2 跳)
|
||
3. 前端用力导向图逐步渲染
|
||
|
||
### 3.2 建议
|
||
|
||
1. **不要设计成全量加载**。manifest 页面类型 `graph` 应支持配置 `depth` 参数(默认 1 跳,最大 3 跳),以及 `center_entity_id` 参数。
|
||
|
||
2. **后端提供"关系网络"专用查询**。不是新增 Host API,而是在 data_handler 层增加一个通用的关系查询端点:
|
||
|
||
```
|
||
GET /api/v1/plugins/{plugin_id}/{entity}/graph?center_id=uuid&depth=2&relationship_types=parent_child,partner
|
||
```
|
||
|
||
这可以由基座的前端通用组件调用,不需要插件 WASM 参与。
|
||
|
||
3. **前端 graph 组件选型**:推荐 Ant Design 的 `@ant-design/graph` 或 G6。图谱渲染是纯前端能力,不需要 WASM 介入。
|
||
|
||
---
|
||
|
||
## 4. parent_id 层级(递归查询)
|
||
|
||
### 4.1 问题分析
|
||
|
||
"查询某客户的所有子孙节点"在当前架构下确实是个难题。三种场景需要递归:
|
||
|
||
1. **组织架构式展示**:树形组件显示客户层级
|
||
2. **汇总统计**:某集团下所有子公司的总交易额
|
||
3. **权限继承**:父客户的负责人可以看到子客户
|
||
|
||
### 4.2 解决方案
|
||
|
||
**方案 A:Host 端提供递归查询 API(推荐)**
|
||
|
||
新增 Host API:
|
||
|
||
```wit
|
||
/// 递归查询树形结构的所有子孙节点
|
||
db-query-tree: func(entity: string, parent-field: string, root-id: string, max-depth: s32) -> result<list<u8>, string>;
|
||
```
|
||
|
||
Host 层使用 `WITH RECURSIVE` 实现:
|
||
|
||
```sql
|
||
WITH RECURSIVE tree AS (
|
||
-- 锚点:根节点
|
||
SELECT id, data, 0 as depth
|
||
FROM plugin_crm_customer
|
||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||
UNION ALL
|
||
-- 递归:子节点
|
||
SELECT c.id, c.data, t.depth + 1
|
||
FROM plugin_crm_customer c
|
||
JOIN tree t ON c.data->>'parent_id' = t.id::text
|
||
WHERE c.tenant_id = $2 AND c.deleted_at IS NULL AND t.depth < $3
|
||
)
|
||
SELECT id, data, depth FROM tree ORDER BY depth;
|
||
```
|
||
|
||
**方案 B:物化路径(denormalization)**
|
||
|
||
在 customer 的 data 中存储物化路径 `path: "root_id/parent_id/self_id"`,然后可以用前缀匹配:
|
||
|
||
```sql
|
||
SELECT * FROM plugin_crm_customer
|
||
WHERE data->>'path' LIKE 'root_id/%' AND tenant_id = $1 AND deleted_at IS NULL;
|
||
```
|
||
|
||
这需要 GIN 索引支持 `LIKE` 前缀查询(`pg_trgm` 扩展或 `text_pattern_ops`)。
|
||
|
||
**推荐方案 A**。物化路径有数据一致性问题(移动节点时需要更新所有子节点的 path),而递归 CTE 是 PostgreSQL 原生支持的,一次查询搞定。`max_depth` 参数防止无限递归。
|
||
|
||
### 4.3 对 tree 页面类型的建议
|
||
|
||
manifest 中 `type: tree` 的页面需要额外的配置:
|
||
|
||
```toml
|
||
[[ui.pages]]
|
||
route = "/crm/customer-tree"
|
||
entity = "customer"
|
||
type = "tree"
|
||
tree_config = { parent_field = "parent_id", label_field = "name", icon_field = "customer_type", max_depth = 5 }
|
||
```
|
||
|
||
基座前端根据 `tree_config` 渲染 Ant Design Tree 组件,使用 `db-query-tree` API 加载数据。
|
||
|
||
---
|
||
|
||
## 5. 缺失的关键能力
|
||
|
||
### 5.1 必须新增的 Host API
|
||
|
||
按优先级排序:
|
||
|
||
| 优先级 | API | 原因 |
|
||
|--------|-----|------|
|
||
| P0 | **db-query-filtered**(结构化过滤) | 没有它 CRM 插件完全无法工作。当前 db_query 的 filter 参数被忽略。 |
|
||
| P0 | **db-aggregate**(聚合查询) | CRM 仪表盘需要"按状态统计客户数"、"按级别统计客户数"等。设计规格中已规划但未实现。 |
|
||
| P0 | **db-query-by-id**(按 ID 查单条) | 当前 `db_query` 不支持按 ID 查询。虽然 REST API 有 GET by ID,但 WASM 内部没有。 |
|
||
| P1 | **db-query-batch**(批量外键查询) | 解决 N+1 问题,关联实体加载。 |
|
||
| P1 | **db-query-tree**(递归树查询) | 支持客户层级树、组织架构等树形结构。 |
|
||
| P1 | **db-batch-insert**(批量插入) | 导入客户数据、批量创建联系人。 |
|
||
| P2 | **db-exists**(存在性检查) | 检查 code 是否已存在,比 count 更高效。 |
|
||
| P2 | **db-count**(计数) | 单独的计数查询,不返回数据。 |
|
||
|
||
### 5.2 必须增强的基座能力
|
||
|
||
| 能力 | 现状 | 需要 |
|
||
|------|------|------|
|
||
| **REST API 过滤** | `PluginDataListParams` 只有 `page/page_size/search` | 需要支持 `filter` 查询参数,支持 JSONB 字段的等值/范围/IN 查询 |
|
||
| **REST API 排序** | 固定 `ORDER BY created_at DESC` | 需要支持 `order_by` 参数 |
|
||
| **REST API 关联加载** | 无 | 需要支持 `?include=contact,communication` 参数,一次返回客户+联系人+沟通记录 |
|
||
| **Schema 唯一约束校验** | 只创建了索引,没有插入前校验 | 需要在 `PluginDataService::create` 中检查 unique 字段 |
|
||
| **实体间引用完整性** | 无 | 删除客户时应提示有关联的联系人/沟通记录。至少提供 `GET /plugins/{id}/{entity}/{record_id}/references` 端点 |
|
||
|
||
### 5.3 WASM 插件端需要的能力
|
||
|
||
设计规格中规划了 `render-page` 和 `handle-action`,但 WIT 中未实现。对于 CRM 插件,以下场景需要这两个函数:
|
||
|
||
- **客户 360 度视图**:在同一个页面展示客户基本信息 + 联系人列表 + 最近沟通记录 + 标签 + 关系图谱。这不是一个简单的 CRUD 页面,需要 WASM 返回复合 UI 指令。
|
||
- **自定义操作**:"转为正式客户"、"合并客户"、"批量分配"等操作需要 WASM 处理业务逻辑。
|
||
|
||
建议在 WIT 中新增:
|
||
|
||
```wit
|
||
interface plugin-api {
|
||
init: func() -> result<_, string>;
|
||
on-tenant-created: func(tenant-id: string) -> result<_, string>;
|
||
handle-event: func(event-type: string, payload: list<u8>) -> result<_, string>;
|
||
// 新增
|
||
render-page: func(page-path: string, params: list<u8>) -> result<list<u8>, string>;
|
||
handle-action: func(page-path: string, action: string, data: list<u8>) -> result<list<u8>, string>;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Manifest ui.pages 设计审查
|
||
|
||
### 6.1 当前类型覆盖度评估
|
||
|
||
| 类型 | CRM 场景 | 是否覆盖 | 说明 |
|
||
|------|---------|---------|------|
|
||
| `crud` | 客户列表、联系人列表、沟通记录列表 | 覆盖 | 基本够用 |
|
||
| `tree` | 客户层级树 | 需增强 | 需要增加 `tree_config` 配置 |
|
||
| `graph` | 关系图谱 | 需增强 | 需要增加 `graph_config`(relationship_entity, center_id, depth) |
|
||
| `timeline` | 沟通记录时间线 | 新增 | Ant Design Timeline 组件天然支持 |
|
||
| `tabs` | 客户 360 度视图 | 需增强 | 需要支持嵌套子页面 |
|
||
| `dashboard` | CRM 首页统计 | 缺失 | 需要 `stat_cards` + `charts` 配置 |
|
||
| `kanban` | 客户跟进阶段看板 | 缺失 | 按客户等级/跟进状态分列 |
|
||
| `detail` | 客户详情页 | 缺失 | 不是 CRUD,是复合视图 |
|
||
|
||
### 6.2 建议的页面类型扩展
|
||
|
||
```typescript
|
||
interface PluginPage {
|
||
route: string;
|
||
entity: string;
|
||
display_name: string;
|
||
icon?: string;
|
||
menu_group?: string;
|
||
|
||
// 页面类型扩展
|
||
type: "crud" | "tree" | "graph" | "timeline" | "tabs" | "dashboard" | "detail" | "custom";
|
||
|
||
// 类型专属配置
|
||
crud_config?: CrudConfig; // 列/过滤/排序/表单/操作
|
||
tree_config?: TreeConfig; // 父节点字段/标签字段/最大深度
|
||
graph_config?: GraphConfig; // 关系实体/关系类型/默认深度
|
||
dashboard_config?: DashboardConfig; // 统计卡片/图表
|
||
detail_config?: DetailConfig; // 关联实体/布局
|
||
tabs_config?: TabsConfig; // 子标签页列表
|
||
}
|
||
```
|
||
|
||
### 6.3 对 CRM 最关键的缺失:detail 页面类型
|
||
|
||
CRM 最核心的交互不是列表 CRUD,而是**客户 360 度详情页**:
|
||
|
||
```
|
||
┌────────────────────────────────────────────────┐
|
||
│ 客户名称:某某集团 [编辑] [更多 ▾] │
|
||
│ 类型:企业 | 行业:制造业 | 级别:VIP │
|
||
├────────────────────────────────────────────────┤
|
||
│ [基本信息] [联系人] [沟通记录] [关系图谱] [标签] │
|
||
├────────────────────────────────────────────────┤
|
||
│ 联系人列表(联系人的 CRUD,自动过滤当前客户) │
|
||
│ + 新增联系人 │
|
||
│ ┌─────┬──────┬──────┬───────┬────┐ │
|
||
│ │姓名 │职位 │电话 │邮箱 │操作│ │
|
||
│ └─────┴──────┴──────┴───────┴────┘ │
|
||
└────────────────────────────────────────────────┘
|
||
```
|
||
|
||
这种页面需要:
|
||
1. 顶部展示主实体详情
|
||
2. 底部 tabs 展示关联实体列表
|
||
3. 关联实体自动按主实体 ID 过滤
|
||
|
||
建议 `detail` 类型配置:
|
||
|
||
```toml
|
||
[[ui.pages]]
|
||
route = "/crm/customer/:id"
|
||
entity = "customer"
|
||
type = "detail"
|
||
display_name = "客户详情"
|
||
|
||
[ui.pages.detail_config.header]
|
||
title_field = "name"
|
||
subtitles = [
|
||
{ field = "customer_type", label = "类型" },
|
||
{ field = "industry", label = "行业" },
|
||
{ field = "level", label = "级别", tag_colors = {vip = "gold", svip = "red"} }
|
||
]
|
||
actions = ["edit", "delete", { label = "转为正式客户", action = "activate", permission = "crm.customer.update" }]
|
||
|
||
[[ui.pages.detail_config.tabs]]
|
||
label = "联系人"
|
||
entity = "contact"
|
||
filter_field = "customer_id"
|
||
type = "crud"
|
||
|
||
[[ui.pages.detail_config.tabs]]
|
||
label = "沟通记录"
|
||
entity = "communication"
|
||
filter_field = "customer_id"
|
||
type = "timeline"
|
||
|
||
[[ui.pages.detail_config.tabs]]
|
||
label = "关系图谱"
|
||
entity = "customer_relationship"
|
||
type = "graph"
|
||
graph_config = { center_from = "from_customer_id", center_to = "to_customer_id" }
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 权限模型审查
|
||
|
||
### 7.1 当前方案的 8 个权限码
|
||
|
||
根据 CRM 插件常见需求,推测 8 个权限码为:
|
||
- `crm.customer.list` / `crm.customer.create` / `crm.customer.update` / `crm.customer.delete`
|
||
- `crm.contact.list` / `crm.contact.create` / `crm.contact.update` / `crm.contact.delete`
|
||
|
||
### 7.2 不足之处
|
||
|
||
**问题 1:缺少沟通记录和关系图谱的权限**
|
||
|
||
沟通记录(communication)和关系图谱(customer_relationship)是独立实体,应该有独立的权限码。
|
||
|
||
**问题 2:缺少数据范围权限(data scope)**
|
||
|
||
CRM 中最常见的权限需求不是"能不能看联系人",而是"能看到哪些客户的联系人"。典型的数据范围:
|
||
|
||
- **本人**:只看自己创建的客户
|
||
- **本部门**:看本部门所有人创建的客户
|
||
- **本部门及下级**:看本部门及子部门创建的客户
|
||
- **全部**:看所有客户
|
||
|
||
当前的 `check_permission` 只支持布尔型权限检查(有没有这个权限),不支持数据范围过滤。
|
||
|
||
**问题 3:级联权限问题**
|
||
|
||
"联系人/沟通记录的访问是否应该依赖客户级别的权限?"——**是的,应该依赖**。如果用户看不到某个客户,就不应该看到该客户下的联系人和沟通记录。
|
||
|
||
### 7.3 建议的权限模型
|
||
|
||
**权限码(16 个)**:
|
||
|
||
```
|
||
crm.customer.list # 查看客户列表
|
||
crm.customer.create # 创建客户
|
||
crm.customer.update # 编辑客户
|
||
crm.customer.delete # 删除客户
|
||
crm.customer.export # 导出客户数据
|
||
|
||
crm.contact.list # 查看联系人
|
||
crm.contact.create # 创建联系人
|
||
crm.contact.update # 编辑联系人
|
||
crm.contact.delete # 删除联系人
|
||
|
||
crm.communication.list # 查看沟通记录
|
||
crm.communication.create # 创建沟通记录
|
||
crm.communication.update # 编辑沟通记录
|
||
crm.communication.delete # 删除沟通记录
|
||
|
||
crm.relationship.view # 查看关系图谱
|
||
crm.tag.manage # 管理标签分类
|
||
|
||
crm.customer.all # 数据范围:查看所有客户(默认只看自己的)
|
||
```
|
||
|
||
**数据范围过滤的实现**:
|
||
|
||
这不是插件自身能解决的,需要基座支持。建议在 `current_user` API 返回的数据中包含数据范围信息,或者在 Host API 层增加数据范围过滤:
|
||
|
||
```wit
|
||
/// 扩展 current_user 返回值
|
||
current-user: func() -> result<list<u8>, string>;
|
||
// 返回结构增加 data_scope 字段
|
||
// { "id": "...", "tenant_id": "...", "data_scope": "all" | "department" | "department_tree" | "self" }
|
||
```
|
||
|
||
然后在 `db_query` 的过滤逻辑中自动注入 `created_by` 过滤。
|
||
|
||
**关于级联权限**:建议在 REST API 层处理。当查询联系人时,如果用户没有 `crm.customer.all`,则联系人查询自动 JOIN customer 表检查 `created_by`:
|
||
|
||
```sql
|
||
SELECT c.* FROM plugin_crm_contact c
|
||
JOIN plugin_crm_customer cu ON c.data->>'customer_id' = cu.id::text
|
||
WHERE c.tenant_id = $1 AND c.deleted_at IS NULL
|
||
AND (cu.data->>'created_by' = $2 OR cu.data->>'level' IN ('vip', 'svip'))
|
||
-- VIP 客户所有人可见
|
||
```
|
||
|
||
这种过滤逻辑复杂且与业务强相关,建议在 WASM 插件的 `handle_action` 中实现,或者作为基座的通用数据范围机制。
|
||
|
||
---
|
||
|
||
## 8. 综合评估与实施建议
|
||
|
||
### 8.1 可行性评估
|
||
|
||
| 维度 | 评分 | 说明 |
|
||
|------|------|------|
|
||
| JSONB 数据存储 | 8/10 | PostgreSQL JSONB 足够支撑,索引机制需增强 |
|
||
| 单实体 CRUD | 6/10 | 基本可用,但查询/排序/过滤能力严重不足 |
|
||
| 跨实体关联 | 3/10 | 当前完全不支持,是最需要补强的部分 |
|
||
| 树形层级 | 3/10 | 需要递归查询支持 |
|
||
| 关系图谱 | 5/10 | 存储可行,查询/渲染需要增强 |
|
||
| 权限模型 | 5/10 | 权限码够用,数据范围权限缺失 |
|
||
| UI 配置驱动 | 4/10 | 缺少 detail/dashboard/kanban 类型 |
|
||
|
||
### 8.2 实施路径建议
|
||
|
||
**Phase 1:基座查询能力增强(前置条件,约 3-5 天)**
|
||
|
||
1. 实现 `db_query` 的 filter 参数解析和 SQL 构建
|
||
2. 实现 `db-aggregate` Host API
|
||
3. REST API 支持 filter / order_by 参数
|
||
4. 为外键字段自动创建索引
|
||
5. 实现 unique 约束的插入前校验
|
||
|
||
**Phase 2:CRM 核心功能(约 5-7 天)**
|
||
|
||
1. 创建 CRM 插件 crate(5 个实体)
|
||
2. 实现客户 + 联系人 + 沟通记录的 CRUD
|
||
3. 实现 `type: detail` 前端通用组件
|
||
4. 实现客户详情页(tabs 嵌套关联实体)
|
||
|
||
**Phase 3:高级功能(约 3-5 天)**
|
||
|
||
1. 实现 `db-query-tree` 支持客户层级
|
||
2. 实现关系图谱页面
|
||
3. 实现数据范围权限过滤
|
||
4. 实现 `render-page` / `handle-action` 支持自定义操作
|
||
|
||
### 8.3 风险提示
|
||
|
||
1. **最大的风险不是技术,是 Host API 的演进方向**。每增加一个 Host API 函数,就意味着所有已安装的插件都依赖这个接口。建议在 CRM 插件开发前先冻结 Host API v2(包含上述增强),然后所有插件基于 v2 开发。
|
||
|
||
2. **JSONB 动态表的性能天花板**。单表数据量超过 100 万条时,JSONB 的查询性能会显著下降(相比原生列)。如果 CRM 模块被高频使用,长期应考虑将高频实体(customer)升级为原生列存储。
|
||
|
||
3. **前端通用组件的复杂度**。detail + graph + tree + timeline 这四种页面类型,每种都需要基座提供通用实现。这不是 CRM 独有的需求,而是所有行业插件的共同需求。建议将这些组件沉淀为基座的 `PluginUI` 组件库,而不是 CRM 插件的一部分。
|
||
|
||
---
|
||
|
||
## 附录:Host API v2 建议的 WIT 定义
|
||
|
||
```wit
|
||
package erp:plugin;
|
||
|
||
interface host-api {
|
||
// === 基础 CRUD ===
|
||
db-insert: func(entity: string, data: list<u8>) -> result<list<u8>, string>;
|
||
db-get-by-id: func(entity: string, id: string) -> result<list<u8>, string>;
|
||
db-query: func(entity: string, filter: list<u8>, pagination: list<u8>) -> result<list<u8>, string>;
|
||
db-update: func(entity: string, id: string, data: list<u8>, version: s64) -> result<list<u8>, string>;
|
||
db-delete: func(entity: string, id: string) -> result<_, string>;
|
||
db-exists: func(entity: string, filter: list<u8>) -> result<bool, string>;
|
||
db-count: func(entity: string, filter: list<u8>) -> result<s64, string>;
|
||
|
||
// === 批量操作 ===
|
||
db-query-batch: func(entity: string, field: string, ids: list<string>) -> result<list<u8>, string>;
|
||
db-batch-insert: func(entity: string, items: list<list<u8>>) -> result<list<list<u8>>, string>;
|
||
|
||
// === 高级查询 ===
|
||
db-aggregate: func(entity: string, query: list<u8>) -> result<list<u8>, string>;
|
||
db-query-tree: func(entity: string, parent-field: string, root-id: string, max-depth: s32) -> result<list<u8>, string>;
|
||
|
||
// === 事件总线 ===
|
||
event-publish: func(event-type: string, payload: list<u8>) -> result<_, string>;
|
||
|
||
// === 配置 ===
|
||
config-get: func(key: string) -> result<list<u8>, string>;
|
||
|
||
// === 日志 ===
|
||
log-write: func(level: string, message: string);
|
||
|
||
// === 用户/权限 ===
|
||
current-user: func() -> result<list<u8>, string>;
|
||
check-permission: func(permission: string) -> result<bool, string>;
|
||
}
|
||
|
||
interface plugin-api {
|
||
init: func() -> result<_, string>;
|
||
on-tenant-created: func(tenant-id: string) -> result<_, string>;
|
||
on-tenant-deleted: func(tenant-id: string) -> result<_, string>;
|
||
handle-event: func(event-type: string, payload: list<u8>) -> result<_, string>;
|
||
render-page: func(page-path: string, params: list<u8>) -> result<list<u8>, string>;
|
||
handle-action: func(page-path: string, action: string, data: list<u8>) -> result<list<u8>, string>;
|
||
}
|
||
|
||
world plugin-world {
|
||
import host-api;
|
||
export plugin-api;
|
||
}
|
||
```
|
||
|
||
共计 15 个 Host API 函数(vs 当前 9 个),6 个 Plugin API 函数(vs 当前 3 个)。
|