Files
erp/docs/superpowers/specs/2026-04-18-crm-plugin-platform-p0-design.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

605 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CRM 插件平台标杆 — P0 基础能力设计规格
> **版本**: v1.1 (修正版 — 基于代码审查发现,对齐现有实现)
> **日期**: 2026-04-18
> **状态**: Draft
> **定位**: 插件平台标杆 — CRM 是试金石,打磨通用能力
---
## 1. 背景与动机
### 1.1 为什么要做这个
CRM 插件是 ERP 平台的第一个行业插件,当前状态是"客户通讯录 + 标签 + 关系图谱",距离一流 CRMSalesforce/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<RelationType>`,默认 `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<String>,
#[serde(default, rename = "type")]
pub relation_type: Option<RelationType>,
#[serde(default)]
pub display_field: Option<String>,
// many_to_many 专属
#[serde(default)]
pub through_entity: Option<String>,
#[serde(default)]
pub through_source_field: Option<String>,
#[serde(default)]
pub through_target_field: Option<String>,
}
#[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<String>, // 已有
pub message: Option<String>, // 已有
// ↓ 新增
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub min_value: Option<f64>,
pub max_value: Option<f64>,
}
```
#### 扩展 `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 规格 |