# 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, _pagination: Vec) -> Result, 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, pagination: list) -> result, 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) -> result, 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) -> result, 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, 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) -> result<_, string>; // 新增 render-page: func(page-path: string, params: list) -> result, string>; handle-action: func(page-path: string, action: string, data: list) -> result, 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, 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) -> result, string>; db-get-by-id: func(entity: string, id: string) -> result, string>; db-query: func(entity: string, filter: list, pagination: list) -> result, string>; db-update: func(entity: string, id: string, data: list, version: s64) -> result, string>; db-delete: func(entity: string, id: string) -> result<_, string>; db-exists: func(entity: string, filter: list) -> result; db-count: func(entity: string, filter: list) -> result; // === 批量操作 === db-query-batch: func(entity: string, field: string, ids: list) -> result, string>; db-batch-insert: func(entity: string, items: list>) -> result>, string>; // === 高级查询 === db-aggregate: func(entity: string, query: list) -> result, string>; db-query-tree: func(entity: string, parent-field: string, root-id: string, max-depth: s32) -> result, string>; // === 事件总线 === event-publish: func(event-type: string, payload: list) -> result<_, string>; // === 配置 === config-get: func(key: string) -> result, string>; // === 日志 === log-write: func(level: string, message: string); // === 用户/权限 === current-user: func() -> result, string>; check-permission: func(permission: string) -> result; } 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) -> result<_, string>; render-page: func(page-path: string, params: list) -> result, string>; handle-action: func(page-path: string, action: string, data: list) -> result, string>; } world plugin-world { import host-api; export plugin-api; } ``` 共计 15 个 Host API 函数(vs 当前 9 个),6 个 Plugin API 函数(vs 当前 3 个)。