8.8 KiB
8.8 KiB
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]] 段:
[[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 metadatasrc/data_service.rs— 删除时执行级联策略,创建/更新时验证 FKsrc/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 字段渲染 EntitySelectsrc/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] 子结构:
[[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 方法中,数据写入前统一调用。
错误响应:
{
"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 |
修改 | 类型定义更新 |
验证方案
- 编译检查:
cargo check全 workspace 通过 - 单元测试: validation.rs 每种校验器独立测试
- 集成测试: Testcontainers 验证级联删除/FK 校验/unique 冲突
- 功能验证:
- 重新安装 CRM 插件,确认 5 个 relation 正确注册
- 删除客户 → 联系人/沟通记录/标签级联软删除
- 创建联系人 → customer_id 不存在时返回 400
- 手机号/邮箱格式不正确时返回校验错误
- Dashboard 切换 inventory 插件时正确渲染
- 前端验证:
pnpm dev启动后手动测试所有页面
输出产物
- 设计规格文档:
docs/superpowers/specs/2026-04-18-crm-plugin-platform-p0-design.md - 实施计划: 通过 writing-plans skill 生成
- 知识库文档: 记录讨论过程和决策理由