fix(用户管理): 修复用户列表页面加载失败问题
修复用户列表页面加载失败导致测试超时的问题,确保页面元素正确渲染
This commit is contained in:
1026
docs/superpowers/specs/2026-04-16-crm-plugin-design.md
Normal file
1026
docs/superpowers/specs/2026-04-16-crm-plugin-design.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,604 @@
|
||||
# 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<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 规格 |
|
||||
@@ -0,0 +1,337 @@
|
||||
# ERP 插件平台演进路线图 — 设计规格
|
||||
|
||||
> 日期: 2026-04-18
|
||||
> 来源: 无主题发散式互动探讨
|
||||
> 状态: Draft
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
ERP 平台已完成 Phase 1-6 核心开发和 Q2-Q4 成熟度路线图。当前有两个行业插件(CRM + 进销存)运行在 WASM 插件系统上。通过分析发现四大系统性缺口:
|
||||
|
||||
1. **跨插件数据引用完全不支持** — 进销存的 `customer_id` 只能存裸 UUID
|
||||
2. **插件无通用业务能力** — 导入导出/打印/配置/视图每个插件都要自己实现
|
||||
3. **无质量保障机制** — 第三方插件的安全性和性能无法保证
|
||||
4. **无发现和分发渠道** — 用户无法自助发现和安装插件
|
||||
|
||||
**目标:** 通过搭建财务/应收插件来验证和推动这些平台能力的实现。
|
||||
|
||||
**核心设计原则:**
|
||||
- 插件间**完全独立**,任何插件可自由安装/卸载,不受其他插件影响
|
||||
- 跨插件引用**声明式**,通过 plugin.toml 零代码实现
|
||||
- 通用业务能力**平台层提供**,插件声明式接入
|
||||
- 外部引用问题永远是**软警告**,永不硬阻塞用户操作
|
||||
|
||||
---
|
||||
|
||||
## 2. 跨插件数据引用系统
|
||||
|
||||
### 2.1 Entity Registry (平台实体注册表)
|
||||
|
||||
插件安装时将其所有实体注册到平台级 Entity Registry,其他插件通过 registry 动态发现和引用。
|
||||
|
||||
**数据结构:**
|
||||
|
||||
```
|
||||
entity_registry:
|
||||
- entity_name: string # 实体名 (如 "customer")
|
||||
- plugin_id: string # 注册该实体的插件 ID
|
||||
- display_fields: string[] # 用于下拉显示的字段列表
|
||||
- search_fields: string[] # 用于搜索的字段列表
|
||||
- status: active | inactive # 插件卸载时标记 inactive
|
||||
- registered_at: timestamp
|
||||
- tenant_id: uuid # 多租户隔离
|
||||
```
|
||||
|
||||
**生命周期:**
|
||||
- 插件安装 → 注册所有 entities 到 registry
|
||||
- 插件启用 → status = active
|
||||
- 插件禁用 → status = inactive(数据保留)
|
||||
- 插件卸载 → status = inactive + 标记为 orphaned
|
||||
|
||||
### 2.2 plugin.toml 扩展
|
||||
|
||||
```toml
|
||||
# 可选依赖声明
|
||||
[dependencies.crm]
|
||||
optional = true
|
||||
description = "客户管理 — 自动关联客户数据,未安装时客户字段为手动输入"
|
||||
|
||||
[dependencies.inventory]
|
||||
optional = true
|
||||
description = "进销存 — 自动关联商品数据"
|
||||
|
||||
# 跨插件引用字段
|
||||
[[schema.entities.fields]]
|
||||
name = "customer_id"
|
||||
field_type = "uuid"
|
||||
display_name = "客户"
|
||||
ref_entity = "customer" # 目标实体名
|
||||
ref_scope = "external" # "internal" (默认) | "external"
|
||||
ref_display_field = "name" # 下拉框显示字段
|
||||
ref_search_fields = ["name", "phone"] # 搜索字段
|
||||
ref_fallback_label = "外部客户" # 降级时显示文本
|
||||
```
|
||||
|
||||
### 2.3 运行时行为
|
||||
|
||||
**写入时校验:**
|
||||
|
||||
| 源插件状态 | 写入行为 | 读取行为 | 前端展示 |
|
||||
|-----------|---------|---------|---------|
|
||||
| 已安装 (active) | 强校验 UUID 存在性 | JOIN 富化 display_field | ✅ 绿色链接 "张三" |
|
||||
| 未安装 (inactive) | 无校验,接受任意 UUID | 返回原始 UUID | ⬜ 灰色 "外部客户" |
|
||||
| 刚重新启用 | 新写入强校验,不回溯已有 | 后台对账扫描 | ⚠️ 黄色警告 (悬空) |
|
||||
|
||||
**悬空引用处理 (插件重新启用时):**
|
||||
1. 后台扫描所有 `ref_scope=external` 且指向本插件实体的字段
|
||||
2. 验证每个 UUID 是否存在于本插件表中
|
||||
3. 生成对账报告: `{ valid: N, dangling: M, details: [...] }`
|
||||
4. 前端展示对账结果,用户逐条处理(映射/清空/忽略)
|
||||
5. 永不硬阻塞用户操作
|
||||
|
||||
### 2.4 需要改造的文件
|
||||
|
||||
| 文件 | 改动 | 复杂度 |
|
||||
|------|------|--------|
|
||||
| `crates/erp-plugin/src/manifest.rs` | 新增 `ref_scope`, `ref_display_field`, `ref_search_fields`, `ref_fallback_label`; 新增 `DependenciesSection` | 低 |
|
||||
| `crates/erp-plugin/src/entity_registry.rs` (新) | 实体注册/发现/inactive 标记/对账 | 中 |
|
||||
| `crates/erp-plugin/src/data_service.rs` | `validate_ref_entities` 支持运行时发现外部引用 | 中 |
|
||||
| `crates/erp-plugin/src/host.rs` | 新增 `resolve_ref_entity` Host API | 中 |
|
||||
| `crates/erp-plugin/wit/plugin.wit` | 新增 `resolve-ref-entity` 接口 | 低 |
|
||||
| `crates/erp-plugin/src/service.rs` | 插件安装/卸载时维护 Entity Registry | 中 |
|
||||
| `apps/web/src/` 前端 | entity_select 组件支持跨插件数据源 + 降级显示 + 对账 UI | 高 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 插件平台通用服务层 (P1)
|
||||
|
||||
### 3.1 数据导入导出服务
|
||||
|
||||
插件在 plugin.toml 中声明哪些实体支持导入导出,平台提供统一的导入导出 UI 和引擎。
|
||||
|
||||
```toml
|
||||
[[schema.entities]]
|
||||
name = "invoice"
|
||||
display_name = "发票"
|
||||
importable = true
|
||||
exportable = true
|
||||
import_template = "invoice_import_template.xlsx"
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- 自动生成导入模板(基于 schema entities fields)
|
||||
- Excel/CSV 解析 + schema 字段校验
|
||||
- 批量写入(支持事务 + 错误行级报告)
|
||||
- 导出为 Excel/CSV(支持筛选条件)
|
||||
- 导入历史记录 + 回滚
|
||||
|
||||
**实现位置:** `crates/erp-plugin/src/import_export.rs` + 前端 `ImportExportModal` 通用组件
|
||||
|
||||
### 3.2 打印模板引擎
|
||||
|
||||
平台提供 HTML → PDF 的模板渲染能力,插件定义模板和字段映射。
|
||||
|
||||
```toml
|
||||
[[templates]]
|
||||
name = "invoice_pdf"
|
||||
display_name = "发票"
|
||||
entity = "invoice"
|
||||
format = "pdf"
|
||||
template_file = "templates/invoice.html"
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- HTML 模板渲染 → PDF 下载
|
||||
- 模板变量替换(基于实体字段)
|
||||
- 租户级模板自定义(覆盖默认模板)
|
||||
- 打印预览
|
||||
|
||||
### 3.3 插件配置 UI
|
||||
|
||||
插件在 plugin.toml 中声明配置项,平台自动生成配置页面。
|
||||
|
||||
```toml
|
||||
[settings]
|
||||
[[settings.fields]]
|
||||
name = "default_tax_rate"
|
||||
display_name = "默认税率"
|
||||
field_type = "number"
|
||||
default_value = 0.13
|
||||
|
||||
[[settings.fields]]
|
||||
name = "invoice_prefix"
|
||||
display_name = "发票前缀"
|
||||
field_type = "text"
|
||||
default_value = "INV"
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- 根据 settings 声明自动生成配置表单
|
||||
- 配置数据存储在 `plugin_settings` 表(tenant_id + plugin_id + key/value)
|
||||
- 配置变更时通知插件(通过事件)
|
||||
- 支持配置权限控制(仅管理员可改)
|
||||
|
||||
### 3.4 自定义视图
|
||||
|
||||
用户可以保存列表页的列配置和筛选条件。
|
||||
|
||||
```
|
||||
user_views:
|
||||
- id: uuid
|
||||
- user_id: uuid
|
||||
- plugin_id: string
|
||||
- entity_name: string
|
||||
- view_name: string
|
||||
- columns: string[]
|
||||
- filters: json
|
||||
- sort: json
|
||||
- is_default: boolean
|
||||
```
|
||||
|
||||
### 3.5 通知规则
|
||||
|
||||
插件在 plugin.toml 中声明可触发的事件,平台提供通知规则配置 UI。
|
||||
|
||||
```toml
|
||||
[[trigger_events]]
|
||||
name = "invoice.overdue"
|
||||
display_name = "发票逾期"
|
||||
description = "发票超过付款期限未收款"
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- 规则引擎: WHEN event THEN notify [user/role/department]
|
||||
- 复用 erp-message 的通知渠道
|
||||
- 租户级规则配置
|
||||
|
||||
### 3.6 编号规则 (已有基础扩展)
|
||||
|
||||
复用 erp-config 的编号规则服务,扩展为插件可接入。
|
||||
|
||||
```toml
|
||||
[[numbering]]
|
||||
entity = "invoice"
|
||||
prefix = "INV"
|
||||
format = "{PREFIX}-{YEAR}-{SEQ:4}"
|
||||
reset_rule = "yearly"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 插件质量保障
|
||||
|
||||
### 4.1 上传时校验
|
||||
|
||||
```
|
||||
插件上传 → Schema 校验 → WASM 二进制验证 → 安全扫描 → 性能基准 → 发布/拒绝
|
||||
```
|
||||
|
||||
| 阶段 | 校验内容 | 现状 |
|
||||
|------|---------|------|
|
||||
| Schema 校验 | plugin.toml 格式、字段类型、权限码一致性 | 部分已有 |
|
||||
| WASM 验证 | 二进制格式、WIT 兼容性、导出函数检查 | 已有 |
|
||||
| 安全扫描 | 动态表 SQL 注入风险、Fuel 耗尽、内存泄漏 | 缺失 |
|
||||
| 性能基准 | 标准 CRUD 操作在 N 条数据下的响应时间 | 缺失 |
|
||||
| 兼容性 | 平台版本匹配、依赖插件版本兼容 | 缺失 |
|
||||
|
||||
### 4.2 运行时监控
|
||||
|
||||
```
|
||||
plugin_runtime_metrics:
|
||||
- plugin_id: string
|
||||
- error_rate: float
|
||||
- avg_response_ms: float
|
||||
- fuel_consumption: float
|
||||
- memory_peak_mb: float
|
||||
- active_instances: int
|
||||
```
|
||||
|
||||
**告警规则:** 错误率 > 5% / 平均响应 > 2s / Fuel 消耗异常 / 内存持续增长
|
||||
|
||||
---
|
||||
|
||||
## 5. 插件市场/商店
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 插件目录 | 按行业/功能分类浏览 |
|
||||
| 搜索 | 按名称/标签/行业搜索 |
|
||||
| 详情页 | 截图、演示、功能描述、权限说明 |
|
||||
| 一键安装 | 上传 → 自动安装 → 配置 → 启用 |
|
||||
| 评分/评论 | 用户评分和使用反馈 |
|
||||
| 版本管理 | 版本列表、更新日志、回滚 |
|
||||
| 依赖提示 | 安装时提示可选依赖 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证计划 — 财务/应收插件
|
||||
|
||||
### 6.1 实体设计
|
||||
|
||||
| 实体 | 字段概要 | 跨插件引用 |
|
||||
|------|---------|-----------|
|
||||
| invoice (发票) | 编号/客户/金额/税额/状态/到期日 | customer_id → CRM.customer |
|
||||
| invoice_line (发票行) | 发票/商品/数量/单价/税额 | product_id → Inventory.product |
|
||||
| payment (收款) | 发票/金额/方式/日期/状态 | invoice_id → 本插件内部 |
|
||||
| quote (报价单) | 编号/客户/有效期/状态 | customer_id → CRM.customer |
|
||||
| quote_line (报价行) | 报价单/商品/数量/单价 | product_id → Inventory.product |
|
||||
|
||||
### 6.2 验证矩阵
|
||||
|
||||
| 能力 | 验证方式 | 预期结果 |
|
||||
|------|---------|---------|
|
||||
| 跨插件引用 (CRM 安装) | 创建发票时选择客户 | entity_select 下拉显示 CRM 客户列表 |
|
||||
| 跨插件引用 (CRM 卸载) | 创建发票时输入客户 | 降级为文本输入,不阻塞 |
|
||||
| 悬空引用对账 | CRM 卸载→创建发票→重新安装 CRM | 对账报告显示悬空引用,用户可修复 |
|
||||
| 数据导入 | 导入 Excel 客户清单 | 解析+校验+批量写入 |
|
||||
| 数据导出 | 导出发票列表为 Excel | 筛选+下载 |
|
||||
| 打印模板 | 打印发票 PDF | HTML→PDF 渲染 |
|
||||
| 插件配置 | 设置税率/发票前缀 | 自动生成的配置页面 |
|
||||
| 编号规则 | 创建发票自动编号 | INV-2026-0001 |
|
||||
| 通知规则 | 发票逾期通知 | 规则引擎触发通知 |
|
||||
| 独立安装 | 不安装 CRM 单独安装财务 | 所有功能正常,客户字段降级 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 实施优先级
|
||||
|
||||
```
|
||||
P0 (已完成/进行中): P0 平台能力升级 + 插件系统增强
|
||||
|
||||
P1 (跨插件引用): Entity Registry + ref_scope 扩展 + 前端 entity_select 改造
|
||||
这是所有后续能力的基础
|
||||
|
||||
P2 (平台通用服务): 数据导入导出 → 插件配置 UI → 编号规则扩展 → 通知规则
|
||||
|
||||
P3 (质量保障): 上传时安全扫描 → 性能基准 → 运行时监控
|
||||
|
||||
P4 (插件市场): 插件目录 → 一键安装 → 版本管理 → 评分评论
|
||||
|
||||
验证: 财务/应收插件贯穿 P1-P2,每完成一个 P 就用财务插件验证
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险与缓解
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| Entity Registry 查询性能 | 每次数据操作都要查注册表 | 内存缓存 + DashMap,注册表数据量极小 |
|
||||
| 悬空引用数据量过大 | 对账扫描耗时长 | 异步后台任务 + 分批处理 + 进度条 |
|
||||
| Excel 导入内存占用 | 大文件解析 OOM | 流式解析 + 批量提交 + 文件大小限制 |
|
||||
| 打印模板安全 | 模板注入攻击 | 沙箱渲染 + 变量白名单 |
|
||||
| 插件市场审核成本 | 人工审核效率低 | 自动化扫描 + 人工抽查 + 社区举报 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 讨论溯源
|
||||
|
||||
本文档基于 2026-04-18 的无主题发散式互动探讨产出,完整讨论过程记录在 `plans/skill-cosmic-pancake.md`。
|
||||
|
||||
关键决策历程:
|
||||
- **Round 1:** 发现跨插件数据引用完全不支持(进销存的 customer_id 是裸 UUID)
|
||||
- **Round 2:** 确定声明式引用 + 完全独立(无硬依赖)+ 软警告对账方案
|
||||
- **Round 3:** 确定导入导出/打印/配置/视图/通知应为平台通用服务
|
||||
- **Round 4:** 收敛为统一设计规格,以财务插件为验证载体
|
||||
@@ -0,0 +1,183 @@
|
||||
# 插件系统增强设计规格
|
||||
|
||||
## Context
|
||||
|
||||
插件系统是 ERP 平台的核心差异化能力,当前声明式层面(manifest schema、动态表、前端页面)已达 90% 成熟度。但 WASM 逻辑层存在根本性限制:
|
||||
|
||||
1. **插件无法自主查询数据** — `db_query` 的 filter/pagination 参数被忽略,只能使用预填充结果
|
||||
2. **无读后写一致性** — 延迟刷新模型导致插件在一次调用中无法读取自己刚写入的数据
|
||||
3. **聚合只有 COUNT** — 缺少 SUM/AVG/MAX/MIN,无法支撑财务、统计类场景
|
||||
4. **热更新无原子回滚** — 旧版本先卸载再加载新版本,中间失败无保障
|
||||
5. **Schema 变更只支持新增实体** — 不支持已有实体的字段演进
|
||||
|
||||
这些限制使插件系统只能支撑"数据管理+展示"型轻量场景(CRM、简单进销存),无法支撑需要复杂业务逻辑的行业(财务、制造、电商)。
|
||||
|
||||
本次增强的目标:**让插件逻辑层从 40% 提升到 80%+,使系统能真正承载不同行业的定制化需求。**
|
||||
|
||||
---
|
||||
|
||||
## 改动 1:混合执行模型(解决查询和读后写一致性)
|
||||
|
||||
### 问题
|
||||
|
||||
`host.rs:99-109` — `db_query` 忽略 `_filter` 和 `_pagination` 参数,只从 `query_results` 预填充缓存取数据。插件无法自主构造查询。
|
||||
|
||||
### 方案:读操作走实时 SQL + 写操作保持延迟批量 + 读前自动 flush
|
||||
|
||||
核心流程变更:
|
||||
|
||||
```
|
||||
当前:
|
||||
WASM 调用 db_insert() → 入队 pending_ops
|
||||
WASM 调用 db_query() → 从预填充缓存读(忽略 filter/pagination)
|
||||
WASM 结束 → flush 全部 pending_ops
|
||||
|
||||
改为:
|
||||
WASM 调用 db_insert() → 入队 pending_ops
|
||||
WASM 调用 db_query() → 先 flush pending_ops → 执行真实 SQL 查询 → 返回结果
|
||||
WASM 结束 → flush 剩余 pending_ops
|
||||
```
|
||||
|
||||
### 改动文件
|
||||
|
||||
#### 1. `crates/erp-plugin/src/host.rs`
|
||||
|
||||
HostState 新增字段:
|
||||
|
||||
```rust
|
||||
pub struct HostState {
|
||||
// ... 现有字段保留 ...
|
||||
pub(crate) db: Option<DatabaseConnection>,
|
||||
pub(crate) event_bus: Option<EventBus>,
|
||||
}
|
||||
```
|
||||
|
||||
db_query 实现变更 — 使用 `tokio::runtime::Handle::current()` 在 `spawn_blocking` 内执行异步 DB 操作:
|
||||
|
||||
1. 先 `block_on(flush_ops(...))` 清空 pending writes
|
||||
2. 解析 filter/pagination 参数
|
||||
3. 调用 `DynamicTableManager::build_query_sql()` 构建查询
|
||||
4. `block_on` 执行查询并返回结果
|
||||
|
||||
向后兼容:`db = None` 时走旧的预填充路径。
|
||||
|
||||
#### 2. `crates/erp-plugin/src/dynamic_table.rs`
|
||||
|
||||
新增 `build_query_sql` 方法,复用 `data_service.rs` 中的查询构建逻辑。
|
||||
|
||||
### 向后兼容
|
||||
|
||||
- `HostState::new()` 不传 db → 走旧的预填充路径
|
||||
- `execute_wasm()` 传 db → 走新的实时查询路径
|
||||
- 现有 WASM 插件无需修改
|
||||
|
||||
---
|
||||
|
||||
## 改动 2:扩展聚合查询
|
||||
|
||||
### 问题
|
||||
|
||||
`data_service.rs:655` 的 `aggregate` 方法只支持 `GROUP BY + COUNT(*)`。
|
||||
|
||||
### 方案
|
||||
|
||||
新增 `aggregate_multi` 方法支持 SUM/AVG/MAX/MIN。
|
||||
|
||||
改动文件:
|
||||
|
||||
1. `data_service.rs` — 新增 `AggregateDef`、`AggregateFunc`、`AggregateResult` 类型和 `aggregate_multi` 方法
|
||||
2. `dynamic_table.rs` — 新增 `build_aggregate_multi_sql` 方法
|
||||
3. `data_handler.rs` — 扩展聚合 API 端点
|
||||
4. 前端 Dashboard Widget 适配多聚合返回格式
|
||||
|
||||
SQL 示例:
|
||||
```sql
|
||||
SELECT _f_status as key,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(_f_amount), 0) as sum_amount,
|
||||
COALESCE(AVG(_f_price), 0) as avg_price
|
||||
FROM plugin_erp_crm__order
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
GROUP BY _f_status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 改动 3:热更新原子回滚
|
||||
|
||||
### 问题
|
||||
|
||||
`service.rs:578-585` — 先 `unload(old)` 再 `load(new)`,中间失败无回滚。
|
||||
|
||||
### 方案:先加载新版本到临时 key,成功后原子替换
|
||||
|
||||
改动文件:
|
||||
|
||||
1. `service.rs` — upgrade 方法改用临时 key 加载新版本
|
||||
2. `engine.rs` — 新增 `rename_plugin` 方法
|
||||
|
||||
安全保证:新版本加载失败 → 旧版本仍在运行,零停机。
|
||||
|
||||
---
|
||||
|
||||
## 改动 4:Schema 演进(ALTER TABLE 支持)
|
||||
|
||||
### 问题
|
||||
|
||||
升级时只处理新增实体(CREATE TABLE),不处理已有实体的字段变更。
|
||||
|
||||
### 方案:利用 JSONB 特性实现轻量级 Schema 演进
|
||||
|
||||
大部分字段变更不需要 DDL(JSONB 天然支持),仅新增 filterable/sortable 字段需 ALTER TABLE ADD Generated Column + 索引。
|
||||
|
||||
改动文件:
|
||||
|
||||
1. `service.rs` — upgrade 方法增加 schema diff 逻辑
|
||||
2. `dynamic_table.rs` — 新增 `FieldDiff`、`diff_entity_fields`、`alter_add_generated_columns`
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序
|
||||
|
||||
| 阶段 | 改动 | 复杂度 | 影响范围 |
|
||||
|------|------|--------|---------|
|
||||
| 1 | 热更新原子回滚 | 低 | engine.rs + service.rs |
|
||||
| 2 | Schema 演进(ALTER TABLE) | 中低 | service.rs + dynamic_table.rs |
|
||||
| 3 | 扩展聚合查询 | 中 | data_service.rs + data_handler.rs + dynamic_table.rs |
|
||||
| 4 | 混合执行模型(查询能力) | 高 | host.rs + engine.rs + dynamic_table.rs |
|
||||
|
||||
---
|
||||
|
||||
## 验证方案
|
||||
|
||||
### 阶段 1:热更新回滚
|
||||
1. 上传损坏的 WASM 二进制 → 验证旧版本仍在运行
|
||||
2. 上传正确的新版本 → 验证成功切换
|
||||
|
||||
### 阶段 2:Schema 演进
|
||||
1. 升级插件增加 filterable 字段 → 验证 ALTER TABLE 正确执行
|
||||
2. 旧数据上新 Generated Column 值正确填充
|
||||
|
||||
### 阶段 3:聚合查询
|
||||
1. 创建测试数据,调用聚合 API → 验证 SUM/AVG 结果正确
|
||||
2. 前端 Dashboard 展示正确
|
||||
|
||||
### 阶段 4:混合执行模型
|
||||
1. 插件 WASM 中 db_insert 后立即 db_query → 读后写一致性
|
||||
2. 带 filter 的 db_query → 过滤结果正确
|
||||
3. 旧插件(预填充模式)仍能正常工作
|
||||
4. 多次连续 db_query 不超过 Fuel 限制
|
||||
|
||||
---
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
| 文件 | 改动类型 |
|
||||
|------|---------|
|
||||
| `crates/erp-plugin/src/host.rs` | 重构 db_query + 新增 db/事件总线字段 |
|
||||
| `crates/erp-plugin/src/engine.rs` | 调整 execute_wasm + 新增 rename_plugin |
|
||||
| `crates/erp-plugin/src/service.rs` | 升级流程回滚安全 + schema diff |
|
||||
| `crates/erp-plugin/src/dynamic_table.rs` | 新增 build_query_sql + alter_add_generated_columns + diff_entity_fields |
|
||||
| `crates/erp-plugin/src/data_service.rs` | 新增 aggregate_multi + AggregateDef |
|
||||
| `crates/erp-plugin/src/data_handler.rs` | 扩展聚合 API |
|
||||
| `apps/web/src/pages/PluginDashboardPage.tsx` | 适配多聚合返回格式 |
|
||||
Reference in New Issue
Block a user