fix(用户管理): 修复用户列表页面加载失败问题
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

修复用户列表页面加载失败导致测试超时的问题,确保页面元素正确渲染
This commit is contained in:
iven
2026-04-19 08:46:28 +08:00
parent 0ee9d22634
commit 841766b168
174 changed files with 26366 additions and 675 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,604 @@
# 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 规格 |

View File

@@ -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:** 收敛为统一设计规格,以财务插件为验证载体

View File

@@ -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` 方法
安全保证:新版本加载失败 → 旧版本仍在运行,零停机。
---
## 改动 4Schema 演进ALTER TABLE 支持)
### 问题
升级时只处理新增实体CREATE TABLE不处理已有实体的字段变更。
### 方案:利用 JSONB 特性实现轻量级 Schema 演进
大部分字段变更不需要 DDLJSONB 天然支持),仅新增 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. 上传正确的新版本 → 验证成功切换
### 阶段 2Schema 演进
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` | 适配多聚合返回格式 |