# CRM 插件平台标杆 — P0 基础能力设计规格 > **版本**: v1.1 (修正版 — 基于代码审查发现,对齐现有实现) > **日期**: 2026-04-18 > **状态**: Draft > **定位**: 插件平台标杆 — CRM 是试金石,打磨通用能力 --- ## 1. 背景与动机 ### 1.1 为什么要做这个 CRM 插件是 ERP 平台的第一个行业插件,当前状态是"客户通讯录 + 标签 + 关系图谱",距离一流 CRM(Salesforce/HubSpot/Pipedrive)有显著差距。但更大的问题是:**CRM 暴露的差距不在于 CRM 本身,而在于插件平台的基础能力缺失。** 具体来说: - ~~5 个实体之间有明确的 FK 关系,但 manifest 无法声明~~ → **已有 `PluginRelation` + 级联删除**,但缺少 `name`/`display_field`/关系类型等前端渲染信息 - 35+ 字段有 required/unique/pattern 校验,但缺少 `min_length`/`max_length`/`min_value`/`max_value` 扩展校验 - Dashboard/Graph 页面硬编码了 CRM 专属颜色和标题,第二个插件无法复用 - CRM 的 `plugin.toml` 没有声明 `relations`,导致现有级联能力未被使用 - 批量删除和 PATCH 部分更新绕过了现有校验 如果不在 P0 阶段补齐这些基础,所有后续业务功能(商机、合同、报价)都会建在不稳固的地基上。 ### 1.2 设计原则 | 原则 | 含义 | |------|------| | **平台优先** | 每个能力都是平台层的,CRM 只是第一个使用者 | | **零改动复用** | inventory/生产/财务插件不应为这些能力写任何额外代码 | | **Manifest 驱动** | 所有行为由 plugin.toml 声明驱动,不写硬编码 | | **双层保障** | 前端即时反馈 + 后端最终防线,缺一不可 | ### 1.3 一流 CRM 差距分析摘要 | 类别 | 差距 | 本规格是否覆盖 | |------|------|--------------| | 实体关系 + 级联删除 | 致命 — 删除客户产生孤儿数据 | **P0-1 覆盖** | | 字段校验 + FK 完整性 | 严重 — 数据质量无保障 | **P0-2 覆盖** | | 前端通用化 | 中等 — 第二个插件无法复用 Dashboard/Graph | **P0-3 覆盖** | | 商机/漏斗/合同 | 严重 — 核心业务缺失 | P2(本规格不覆盖) | | 导入导出/批量操作 | 中等 — ERP 刚需 | P1(后续规格) | | 全局搜索/保存视图 | 中等 — UX 缺失 | P1(后续规格) | | WASM 活化 | 低 — 当前空操作不影响功能 | P2(后续规格) | --- ## 2. P0-1: 实体关系声明 + ref_entity + 级联策略 ### 2.1 Manifest Schema 扩展 **现有基础**:`PluginRelation` 已存在(`manifest.rs:184-189`),包含 `entity`、`foreign_key`、`on_delete` 三个字段。级联删除已在 `data_service.rs:330-395` 中实现。 **扩展方向**:在现有结构上新增字段,保持向后兼容。 ```toml # === 一对多关系 (customer → contacts) === [[schema.entities.relations]] entity = "contact" # 目标实体 (已有字段) foreign_key = "customer_id" # FK 字段 (已有字段) on_delete = "cascade" # cascade | nullify | restrict (已有枚举) # ↓ 新增字段 (可选,向后兼容) name = "contacts" # 关系显示名,用于前端标签 type = "one_to_many" # 关系类型 (one_to_many | many_to_one | many_to_many) display_field = "name" # EntitySelect 下拉显示字段 # === 多对一关系 (contact → customer,含自引用) === [[schema.entities.relations]] entity = "customer" foreign_key = "parent_id" on_delete = "nullify" name = "parent" type = "many_to_one" display_field = "name" # === 多对多关系 (customer ↔ customer,通过中间表) === [[schema.entities.relations]] entity = "customer" foreign_key = "from_customer_id" # 中间表中的源 FK on_delete = "nullify" name = "related_customers" type = "many_to_many" through_entity = "customer_relationship" through_source_field = "from_customer_id" through_target_field = "to_customer_id" ``` #### 关系类型定义 (新增 `type` 字段) | 类型 | 含义 | foreign_key 位置 | CRM 场景 | |------|------|-----------------|---------| | `one_to_many` | 一个父 → 多个子 | 子实体上 | customer → contacts | | `many_to_one` | 多个子 → 一个父 | 本实体上 | contact → customer | | `many_to_many` | 双向多对多 | 中间表上 | customer ↔ customer | > `type` 字段为 `Option`,默认 `OneToMany`。不声明则现有行为不变。 #### 级联策略 (保持现有枚举不变) | 策略 | TOML 值 | 行为 | 适用场景 | |------|---------|------|---------| | `Cascade` | `"cascade"` | 子记录 `deleted_at = now()` | 强所有权:客户→联系人 | | `Nullify` | `"nullify"` | FK 字段设 NULL | 弱引用:联系人→上级客户 | | `Restrict` | `"restrict"` | 有子记录时阻止删除(409) | 关键数据:不允许孤立 | ### 2.2 后端实现 #### 数据结构扩展 (`manifest.rs`) **在现有 `PluginRelation` 上新增字段**(不替换): ```rust // 现有字段保持不变 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginRelation { pub entity: String, // 已有 pub foreign_key: String, // 已有 pub on_delete: OnDeleteStrategy, // 已有 (Cascade | Nullify | Restrict) // ↓ 新增可选字段 #[serde(default)] pub name: Option, #[serde(default, rename = "type")] pub relation_type: Option, #[serde(default)] pub display_field: Option, // many_to_many 专属 #[serde(default)] pub through_entity: Option, #[serde(default)] pub through_source_field: Option, #[serde(default)] pub through_target_field: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum RelationType { #[default] OneToMany, ManyToOne, ManyToMany, } ``` #### 级联删除 (已有,需增强) `data_service.rs:330-395` 已实现 `Restrict`/`Nullify`/`Cascade` 三种策略。需增强: 1. **级联影响信息返回**:Restrict 时返回 `affected_count` 和 `relation.name`,方便前端展示 2. **批量删除级联**:`batch_delete` (data_service.rs:417-520) 当前绕过级联,需补充 3. **many_to_many 级联**:基于 `through_entity` 清理中间表记录 #### 级联策略执行 (已有,需增强错误信息) 现有 `data_service.rs:330-395` 已实现。增强点: 1. **Restrict 错误增强**:返回 `affected_count` 和 `relation.name` 2. **批量删除级联**:`batch_delete` (data_service.rs:417-520) 当前绕过级联,需补充 3. **PATCH 校验**:`partial_update` (data_service.rs:291-327) 当前绕过 `validate_data`,需补充 4. **many_to_many 级联**:基于 `through_entity` 清理中间表记录 #### FK 存在性校验 (已有 `validate_ref_entities`) `data_service.rs:834-899` 已实现 `validate_ref_entities`。需确保 `partial_update` (PATCH) 也调用此函数。 ### 2.3 前端实现 #### 前端类型扩展 `apps/web/src/api/plugins.ts` 需更新: ```typescript // PluginEntitySchema 新增 interface PluginEntitySchema { // ... existing fields relations?: PluginRelationSchema[]; } interface PluginRelationSchema { entity: string; foreign_key: string; on_delete: 'cascade' | 'nullify' | 'restrict'; name?: string; type?: 'one_to_many' | 'many_to_one' | 'many_to_many'; display_field?: string; } // PluginFieldSchema 新增 validation 属性 interface PluginFieldSchema { // ... existing fields validation?: { pattern?: string; message?: string; min_length?: number; max_length?: number; min_value?: number; max_value?: number; }; } ``` #### EntitySelect 增强 (已有基础) 字段有 `ref_entity` 属性时,CRUD 表单已自动渲染为 EntitySelect。增强点: - 优先使用 `relation.display_field` 作为下拉显示字段(fallback 到现有 `ref_label_field`) - 关联子表标题使用 `relation.name` #### 详情页关联子表自动渲染 Entity 的 `one_to_many` relations 自动在详情页渲染为内嵌 CRUD 表格: - Compact 模式 + 自动过滤 `fk = parent_record.id` - 支持新增/编辑/删除子记录 - 标题使用 `relation.name` #### 级联删除确认 删除有 incoming relations 的记录时,弹出确认: ``` 确定删除客户「{name}」? 此操作将同时删除: - 3 条联系人记录 - 5 条沟通记录 - 2 条标签记录 ``` ### 2.4 CRM plugin.toml 改造 为 customer 实体补充 relations: ```toml [[schema.entities.relations]] entity = "contact" foreign_key = "customer_id" on_delete = "cascade" name = "contacts" type = "one_to_many" display_field = "name" [[schema.entities.relations]] entity = "communication" foreign_key = "customer_id" on_delete = "cascade" name = "communications" type = "one_to_many" display_field = "subject" [[schema.entities.relations]] entity = "customer_tag" foreign_key = "customer_id" on_delete = "cascade" name = "tags" type = "one_to_many" display_field = "tag_name" [[schema.entities.relations]] entity = "customer" foreign_key = "parent_id" on_delete = "nullify" name = "parent" type = "many_to_one" display_field = "name" ``` 为 contact 实体补充 relations: ```toml [[schema.entities.relations]] entity = "communication" foreign_key = "contact_id" on_delete = "cascade" name = "communications" type = "one_to_many" display_field = "subject" ``` --- ## 3. P0-2: 字段校验层 ### 3.1 现有基础 **已有实现**: - `validate_data` (`data_service.rs:797-831`): required + pattern 正则校验 - `validate_ref_entities` (`data_service.rs:834-899`): FK 引用存在性校验 - `FieldValidation` (`manifest.rs:53-57`): `pattern` + `message` 字段 - unique 检查已在 `create`/`update` 流程中实现 **缺失部分**: - `min_length` / `max_length` 校验器 - `min_value` / `max_value` 校验器 - PATCH (partial_update) 绕过所有校验 - 前端 TypeScript 类型缺少 `validation` 属性 ### 3.2 Manifest Schema 扩展 在现有 `[validation]` 上新增字段(`manifest.rs:53-57` 已有 `pattern` + `message`): ```toml [[schema.entities.fields]] name = "phone" field_type = "string" display_name = "手机号" [schema.entities.fields.validation] pattern = "^1[3-9]\\d{9}$" message = "请输入有效的手机号码" min_length = 11 max_length = 11 [[schema.entities.fields]] name = "credit_limit" field_type = "decimal" [schema.entities.fields.validation] min_value = 0 max_value = 99999999 message = "信用额度必须在 0-99999999 之间" ``` #### 校验类型定义 | 校验器 | manifest 字段 | 状态 | 说明 | |--------|-------------|------|------| | `required` | `field.required` | **已有** | 值不能为 null/空字符串 | | `unique` | `field.unique` | **已有** | 同 tenant 内值唯一 | | `pattern` | `validation.pattern` + `validation.message` | **已有** | 正则匹配 | | `ref_exists` | `field.ref_entity` | **已有** | FK 指向的记录存在且未删除 | | `min_length` / `max_length` | `validation.min_length` / `validation.max_length` | **新增** | 字符串长度范围 | | `min_value` / `max_value` | `validation.min_value` / `validation.max_value` | **新增** | 数值范围 | ### 3.3 后端实现 #### 扩展 `FieldValidation` (`manifest.rs:53-57`) 在现有结构上新增 4 个可选字段: ```rust #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FieldValidation { pub pattern: Option, // 已有 pub message: Option, // 已有 // ↓ 新增 pub min_length: Option, pub max_length: Option, pub min_value: Option, pub max_value: Option, } ``` #### 扩展 `validate_data` (`data_service.rs:797-831`) 在现有函数中追加 min_length/max_length/min_value/max_value 检查: ```rust // 现有: required + pattern 检查 (已实现) // 新增: if let Some(validation) = &field.validation { // min_length / max_length if let Some(str_val) = val.as_str() { if let Some(min) = validation.min_length { if str_val.len() < min { return Err(...); } } if let Some(max) = validation.max_length { if str_val.len() > max { return Err(...); } } } // min_value / max_value (适用于 number/integer/decimal) if let Some(num_val) = val.as_f64() { if let Some(min) = validation.min_value { if num_val < min { return Err(...); } } if let Some(max) = validation.max_value { if num_val > max { return Err(...); } } } } ``` #### 修复 PATCH 校验缺失 `partial_update` (`data_service.rs:291-327`) 需要添加 `validate_data` 和 `validate_ref_entities` 调用,与 `update` 保持一致。 **执行位置:** `data_service.rs` 的 `create_record` 和 `update_record` 方法中,数据写入前调用 `validate_record`。 **错误响应格式:** ```json { "success": false, "error": "数据验证失败", "details": [ { "field": "phone", "message": "请输入有效的手机号码" }, { "field": "customer_id", "message": "引用的客户不存在" } ] } ``` ### 3.4 前端实现 从 schema 自动生成 Ant Design Form rules(需先修复 TypeScript 类型缺失): ```typescript function generateFormRules(field: PluginFieldSchema): Rule[] { const rules: Rule[] = []; if (field.required) { rules.push({ required: true, message: `${field.display_name}不能为空` }); } if (field.validation?.pattern) { rules.push({ pattern: new RegExp(field.validation.pattern), message: field.validation.message || `${field.display_name}格式不正确`, }); } if (field.validation?.min_length || field.validation?.max_length) { rules.push({ min: field.validation.min_length, max: field.validation.max_length, message: field.validation.message || `${field.display_name}长度不正确`, }); } return rules; } ``` ### 3.5 CRM plugin.toml 补充校验 ```toml # phone 字段 [schema.entities.fields.validation] pattern = "^1[3-9]\\d{9}$" message = "请输入有效的手机号码" # email 字段 [schema.entities.fields.validation] pattern = "^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$" message = "请输入有效的邮箱地址" # credit_code 字段 [schema.entities.fields.validation] pattern = "^[0-9A-HJ-NP-RTUW-Y]{2}\\d{6}[0-9A-HJ-NP-RTUW-Y]{10}$" message = "请输入有效的统一社会信用代码" # website 字段 [schema.entities.fields.validation] pattern = "^https?://[\\w.-]+(?:\\.[\\w.-]+)+[/#?]?.*$" message = "请输入有效的网址" ``` --- ## 4. P0-3: 前端去硬编码 ### 4.1 Dashboard 通用化 **涉及文件:** - `apps/web/src/pages/dashboard/dashboardConstants.tsx` - `apps/web/src/pages/dashboard/DashboardWidgets.tsx` - `apps/web/src/pages/PluginDashboardPage.tsx` **改造方案:** | 当前硬编码 | 通用化方案 | |-----------|-----------| | `ENTITY_COLORS`: customer→indigo, contact→green, ... | 8 色调色板按 entity 顺序自动分配 | | `ENTITY_ICONS`: customer→TeamOutlined, ... | 从 page schema 的 icon 字段读取 | | 标题 "CRM 数据全景视图" | `{manifest.name} 统计概览` | | 副标题 "实时掌握业务动态" | `{manifest.description}` 截取前 50 字 | **通用调色板:** ```typescript const UNIVERSAL_PALETTE = [ '#6366f1', // indigo '#22c55e', // green '#f59e0b', // amber '#8b5cf6', // violet '#ef4444', // red '#06b6d4', // cyan '#f97316', // orange '#ec4899', // pink ]; ``` ### 4.2 Graph 通用化 **涉及文件:** `apps/web/src/pages/plugins/graph/graphConstants.ts` **改造方案:** | 当前硬编码 | 通用化方案 | |-----------|-----------| | `RELATIONSHIP_COLORS`: parent_child→indigo, ... | 调色板按 option 顺序循环 | | `RELATIONSHIP_LABELS`: parent_child→"母子", ... | 从 field.options[].label 读取 | | `RELATIONSHIP_TYPES` 固定 5 种 | 从 schema 动态生成 | ### 4.3 CRUD 表格列可配置 **涉及文件:** `apps/web/src/pages/PluginCRUDPage.tsx` **改造方案:** manifest page 新增可选字段 `table_columns`: ```toml [[ui.pages]] type = "crud" entity = "customer" table_columns = ["code", "name", "customer_type", "level", "status", "owner_id", "region", "industry"] ``` 不声明时默认行为: - 取前 8 个非 hidden 非 FK 字段 - 替换当前 `fields.slice(0, 5)` 硬编码 ### 4.4 验证标准 > **测试: 将 CRM 插件替换为 inventory 插件,Dashboard/Graph/CRUD 页面应零改动正确渲染。** 具体验证: 1. Dashboard 显示 inventory 的 6 个实体统计,颜色按顺序分配 2. Graph 如果 inventory 有关系数据,渲染正确(无数据则显示空状态) 3. CRUD 表格按 `table_columns` 或默认 8 列显示 --- ## 5. 关键文件清单 ### 后端 Rust | 文件 | 改动类型 | 说明 | |------|---------|------| | `crates/erp-plugin/src/manifest.rs` | 修改 | `PluginRelation` 新增 name/type/display_field/through_* 字段;`FieldValidation` 新增 min_length/max_length/min_value/max_value | | `crates/erp-plugin/src/data_service.rs` | 修改 | 扩展 `validate_data` 增加 min/max 校验;`partial_update` 补充校验调用;`batch_delete` 补充级联 | | `crates/erp-plugin-crm/plugin.toml` | 修改 | 补充 relations 声明 + validation 规则 | > 注意:不新建 `validation.rs`,直接扩展现有 `validate_data` 和 `validate_ref_entities`。 ### 前端 TypeScript | 文件 | 改动类型 | 说明 | |------|---------|------| | `apps/web/src/api/plugins.ts` | 修改 | `PluginEntitySchema` 新增 `relations`;`PluginFieldSchema` 新增 `validation` | | `apps/web/src/pages/dashboard/dashboardConstants.tsx` | 修改 | 去硬编码,通用调色板自动分配 | | `apps/web/src/pages/dashboard/DashboardWidgets.tsx` | 修改 | schema 驱动颜色/图标 | | `apps/web/src/pages/PluginDashboardPage.tsx` | 修改 | 通用标题/副标题 | | `apps/web/src/pages/plugins/graph/graphConstants.ts` | 修改 | 关系类型从 options 动态读取 | | `apps/web/src/pages/PluginCRUDPage.tsx` | 修改 | 可配置列数 + Form rules 自动生成 | --- ## 6. 验证方案 ### 6.1 编译与测试 ```bash cargo check # 全 workspace 编译 cargo test --workspace # 全量测试 ``` ### 6.2 单元测试 - `validation.rs`: 每种校验器独立测试 (required/unique/pattern/ref_exists/length/value range) - `data_service.rs`: 级联策略测试 (cascade_soft_delete/set_null/restrict) ### 6.3 集成测试 (Testcontainers) - 删除客户 → 验证联系人/沟通记录/标签级联软删除 - 删除有 restrict 关系的记录 → 验证 409 响应 - 创建联系人 → customer_id 不存在时验证 400 - 创建客户 → phone 格式不正确时验证 400 + 错误详情 - 创建客户 → code 已存在时验证 409 ### 6.4 功能验证 1. 重新安装 CRM 插件,确认 5 个 relation 正确注册到 entity metadata 2. 删除客户 → 确认关联数据正确级联 3. 手机号/邮箱格式校验 → 确认前后端双重拦截 4. Dashboard → 确认标题/颜色从 schema 动态生成 5. 切换 inventory 插件 → Dashboard/Graph 零改动渲染 ### 6.5 前端验证 ```bash cd apps/web && pnpm dev ``` 手动测试所有 CRM 页面,确认无回归。 --- ## 7. 不在本规格范围内 | 项 | 原因 | 计划 | |----|------|------| | 商机 (Opportunity) / 销售漏斗 | CRM 业务功能,P2 范畴 | 后续规格 | | 数据导入导出 (Excel) | 平台能力但工作量大 | P1 规格 | | 通知规则 + 消息中心联动 | 需要跨模块协作 | P1 规格 | | WASM 校验/计算 Hook | 平台能力但依赖 WASM 运行时增强 | P2 规格 | | 全局搜索 / 保存视图 | UX 增强 | P1 规格 | | Lead 线索实体 | CRM 业务功能 | P2 规格 |