243 lines
8.8 KiB
Markdown
243 lines
8.8 KiB
Markdown
# CRM 插件平台标杆 — P0 基础能力设计
|
||
|
||
## Context
|
||
|
||
CRM 插件作为 ERP 平台的第一个行业插件,目前暴露了插件平台的多项基础能力缺口。本次设计的定位不是"CRM 功能最全",而是"插件平台能力最扎实"——通过 CRM 验证的每个能力都应被所有未来插件(inventory、生产、财务等)零改动复用。
|
||
|
||
对标一流 CRM(Salesforce/HubSpot/Pipedrive)的差距分析表明,当前 CRM 更接近"客户通讯录+标签+图谱",距离可用 CRM 有显著差距。但这些差距中,最优先补的是**平台基础设施**,而非 CRM 业务功能。
|
||
|
||
## 设计决策记录
|
||
|
||
| 决策点 | 选择 | 理由 |
|
||
|--------|------|------|
|
||
| 目标定位 | 插件平台标杆 | 打磨通用能力,所有插件受益 |
|
||
| 推进节奏 | P0 基础先行 | 基础不扎实,上层的业务功能无法可靠运行 |
|
||
| 实体关系复杂度 | 全覆盖 (1:N/N:1/N:N/自引用) | CRM 和 inventory 都需要,一次到位 |
|
||
| 字段校验范围 | 完整套件 (6种) | 数据质量是所有插件的生命线 |
|
||
| 前端硬编码 | 全部通用化 | 第二个插件必须零改动可用 |
|
||
|
||
## P0-1: 实体关系声明 + ref_entity + 级联策略
|
||
|
||
### Manifest Schema 扩展
|
||
|
||
在 entity 下新增 `[[relations]]` 段:
|
||
|
||
```toml
|
||
[[schema.entities.relations]]
|
||
name = "contacts" # 关系名
|
||
target_entity = "contact" # 目标实体
|
||
type = "one_to_many" # one_to_many | many_to_one | many_to_many
|
||
foreign_key = "customer_id" # FK 字段
|
||
on_delete = "cascade_soft_delete" # cascade_soft_delete | set_null | restrict
|
||
display_field = "name" # 下拉框显示字段
|
||
|
||
# N:N 需要中间表
|
||
[[schema.entities.relations]]
|
||
name = "related_customers"
|
||
target_entity = "customer"
|
||
type = "many_to_many"
|
||
through_entity = "customer_relationship"
|
||
through_source_field = "from_customer_id"
|
||
through_target_field = "to_customer_id"
|
||
```
|
||
|
||
### 后端实现 (crates/erp-plugin/)
|
||
|
||
**关键文件:**
|
||
- `src/manifest.rs` — ManifestParser 新增 relations 解析
|
||
- `src/dynamic_table.rs` — 安装时存储关系到 entity metadata
|
||
- `src/data_service.rs` — 删除时执行级联策略,创建/更新时验证 FK
|
||
- `src/handler/data_handler.rs` — 错误响应格式
|
||
|
||
**级联策略执行流程:**
|
||
```
|
||
DELETE /plugins/{id}/{entity}/{rid}
|
||
→ 查询 entity 的所有 incoming relations (被引用的关系)
|
||
→ for each relation:
|
||
cascade_soft_delete → UPDATE child SET deleted_at=now() WHERE fk=rid
|
||
set_null → UPDATE child SET fk=NULL WHERE fk=rid
|
||
restrict → SELECT COUNT children, if >0 return 409 Conflict
|
||
→ 软删除目标记录
|
||
```
|
||
|
||
**FK 存在性校验:**
|
||
```
|
||
POST/PUT /plugins/{id}/{entity}
|
||
→ for each field with ref_entity:
|
||
SELECT EXISTS(SELECT 1 FROM plugin_xxx_{ref_entity} WHERE id=field_value AND deleted_at IS NULL)
|
||
→ 不存在则返回 400 + 具体字段错误
|
||
```
|
||
|
||
### 前端实现 (apps/web/)
|
||
|
||
**关键文件:**
|
||
- `src/pages/PluginCRUDPage.tsx` — 自动为 ref_entity 字段渲染 EntitySelect
|
||
- `src/pages/PluginDetailPage` — 自动渲染关联子实体内嵌列表
|
||
- `src/components/EntitySelect.tsx` — 增强支持 display_field 配置
|
||
- `src/api/plugins.ts` — schema 类型新增 relations
|
||
|
||
**详情页自动关联渲染:**
|
||
- 读取 entity 的 outgoing relations (one_to_many)
|
||
- 为每个 relation 渲染内嵌 CRUD 表格(compact 模式,带 filter=fk:parent_id)
|
||
- 级联删除前弹出确认("将同时删除 3 条联系人")
|
||
|
||
### CRM plugin.toml 改造
|
||
|
||
为 5 个实体补充 relations 声明:
|
||
- customer → contacts (1:N, cascade_soft_delete)
|
||
- customer → communications (1:N, cascade_soft_delete)
|
||
- customer → tags (1:N, cascade_soft_delete)
|
||
- customer → parent (N:1, set_null, 自引用)
|
||
- contact → communications (1:N, cascade_soft_delete)
|
||
|
||
---
|
||
|
||
## P0-2: 字段校验层
|
||
|
||
### Manifest Schema 扩展
|
||
|
||
在 field 下新增 `[validation]` 子结构:
|
||
|
||
```toml
|
||
[[schema.entities.fields]]
|
||
name = "phone"
|
||
field_type = "string"
|
||
|
||
[schema.entities.fields.validation]
|
||
pattern = "^1[3-9]\\d{9}$"
|
||
message = "请输入有效的手机号码"
|
||
min_length = 11
|
||
max_length = 11
|
||
|
||
[[schema.entities.fields]]
|
||
name = "email"
|
||
field_type = "string"
|
||
|
||
[schema.entities.fields.validation]
|
||
pattern = "^[\\w.-]+@[\\w.-]+\\.\\w+$"
|
||
message = "请输入有效的邮箱地址"
|
||
max_length = 254
|
||
```
|
||
|
||
### 后端校验器 (crates/erp-plugin/src/validation.rs — 新文件)
|
||
|
||
6 种校验器统一执行:
|
||
|
||
| 校验器 | 触发条件 | 错误格式 |
|
||
|--------|---------|---------|
|
||
| required | `field.required = true` | `{field}: 不能为空` |
|
||
| unique | `field.unique = true` | `{field}: 该值已存在` |
|
||
| pattern | `validation.pattern` regex match | `{field}: {validation.message}` |
|
||
| ref_exists | `field.ref_entity` FK 查询 | `{field}: 引用的{entity}不存在` |
|
||
| min_length / max_length | `validation.min_length / max_length` | `{field}: 长度必须在 {min}-{max} 之间` |
|
||
| min_value / max_value | `validation.min_value / max_value` | `{field}: 值必须在 {min}-{max} 之间` |
|
||
|
||
**执行位置:** `data_service.rs` 的 create/update 方法中,数据写入前统一调用。
|
||
|
||
**错误响应:**
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": "数据验证失败",
|
||
"details": [
|
||
{ "field": "phone", "message": "请输入有效的手机号码" },
|
||
{ "field": "customer_id", "message": "引用的客户不存在" }
|
||
]
|
||
}
|
||
```
|
||
|
||
### 前端校验生成
|
||
|
||
从 schema 自动生成 Ant Design Form rules:
|
||
- `required` → `{ required: true, message: "..." }`
|
||
- `pattern` → `{ pattern: /regex/, message: "..." }`
|
||
- `min_length / max_length` → `{ min: n, max: n, message: "..." }`
|
||
|
||
### CRM plugin.toml 补充校验
|
||
|
||
- phone: pattern 手机号
|
||
- email: pattern 邮箱
|
||
- credit_code: pattern 统一社会信用代码 (18位)
|
||
- website: pattern URL
|
||
- customer_id: ref_entity = "customer" (FK 校验)
|
||
|
||
---
|
||
|
||
## P0-3: 前端去硬编码
|
||
|
||
### Dashboard 通用化
|
||
|
||
**文件:** `apps/web/src/pages/dashboard/dashboardConstants.tsx`, `PluginDashboardPage.tsx`
|
||
|
||
改造方案:
|
||
- 移除 `ENTITY_COLORS` 和 `ENTITY_ICONS` 硬编码映射
|
||
- 颜色自动分配: 8 色调色板按 entity 顺序循环
|
||
- 图标从 page schema 的 icon 字段读取
|
||
- 标题: `{manifest.name} 统计概览`,副标题: `{manifest.description}`
|
||
- Widget 定义从 page schema 的 widgets 数组读取
|
||
|
||
### Graph 通用化
|
||
|
||
**文件:** `apps/web/src/pages/plugins/graph/graphConstants.ts`
|
||
|
||
改造方案:
|
||
- 移除 `RELATIONSHIP_COLORS` 硬编码
|
||
- 关系类型标签从 field.options 读取 (已有 label 映射)
|
||
- 颜色用调色板按 option 顺序循环分配
|
||
- 未知类型 fallback 到灰色 + 原始 label
|
||
|
||
### CRUD 表格列可配置
|
||
|
||
**文件:** `PluginCRUDPage.tsx`
|
||
|
||
改造方案:
|
||
- manifest page 新增 `table_columns: ["name", "customer_type", "level", "status", "owner_id"]`
|
||
- 不声明则默认取前 8 个非 hidden 非 FK 字段
|
||
- 移除 `fields.slice(0, 5)` 硬编码
|
||
|
||
### 验证标准
|
||
|
||
> 换成 inventory 插件,Dashboard/Graph/CRUD 应该零改动正确渲染。
|
||
|
||
---
|
||
|
||
## 关键文件清单
|
||
|
||
| 文件 | 改动类型 | 说明 |
|
||
|------|---------|------|
|
||
| `crates/erp-plugin/src/manifest.rs` | 修改 | 新增 relations + validation 解析 |
|
||
| `crates/erp-plugin/src/validation.rs` | 新建 | 校验引擎 |
|
||
| `crates/erp-plugin/src/data_service.rs` | 修改 | 集成级联策略 + 校验 |
|
||
| `crates/erp-plugin/src/dynamic_table.rs` | 修改 | 安装时存储关系元数据 |
|
||
| `crates/erp-plugin/src/handler/data_handler.rs` | 修改 | FK 校验错误格式 |
|
||
| `crates/erp-plugin-crm/plugin.toml` | 修改 | 补充 relations + 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` | 修改 | 可配置列数 |
|
||
| `apps/web/src/api/plugins.ts` | 修改 | 类型定义更新 |
|
||
|
||
---
|
||
|
||
## 验证方案
|
||
|
||
1. **编译检查**: `cargo check` 全 workspace 通过
|
||
2. **单元测试**: validation.rs 每种校验器独立测试
|
||
3. **集成测试**: Testcontainers 验证级联删除/FK 校验/unique 冲突
|
||
4. **功能验证**:
|
||
- 重新安装 CRM 插件,确认 5 个 relation 正确注册
|
||
- 删除客户 → 联系人/沟通记录/标签级联软删除
|
||
- 创建联系人 → customer_id 不存在时返回 400
|
||
- 手机号/邮箱格式不正确时返回校验错误
|
||
- Dashboard 切换 inventory 插件时正确渲染
|
||
5. **前端验证**: `pnpm dev` 启动后手动测试所有页面
|
||
|
||
---
|
||
|
||
## 输出产物
|
||
|
||
1. 设计规格文档: `docs/superpowers/specs/2026-04-18-crm-plugin-platform-p0-design.md`
|
||
2. 实施计划: 通过 writing-plans skill 生成
|
||
3. 知识库文档: 记录讨论过程和决策理由
|