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

View File

@@ -1,6 +1,6 @@
# 插件开发 Skill
基于 CRM 客户管理插件的开发经验,提炼可复用的插件开发流程和模式。
基于 CRM / 进销存插件的开发经验,提炼可复用的插件开发流程和模式。
## 触发场景
@@ -16,8 +16,14 @@
- `field_type`: String/Integer/Float/Boolean/Date/DateTime/Uuid/Decimal/Json
- `required` / `unique` / `searchable` / `filterable` / `sortable`
- `visible_when`: 条件显示表达式(如 `type == 'enterprise'`
- `ui_widget`: 表单控件input/select/textarea/datepicker
- `ui_widget`: 表单控件input/select/textarea/datepicker/entity_select/number
- `options`: select 类型的选项列表
- `ref_entity` / `ref_label_field` / `ref_search_fields`: 外键引用entity_select 下拉)
- `cascade_from` / `cascade_filter`: 级联过滤(如选了客户后过滤联系人)
- `scope_role`: 标记数据权限的"所有者"字段(用于行级权限 self/department 模式)
3. 确定实体间关系(`relations`)和级联策略(`cascade`/`nullify`/`restrict`
4. 确定是否需要行级数据权限(`data_scope = true`
### 第二步:编写 plugin.toml manifest
@@ -30,26 +36,51 @@ description = "描述"
author = "ERP Team"
min_platform_version = "0.1.0"
# 权限:{entity}.{list|manage}
# 权限:code 必须是 {entity_name}.{list|manage},与下面的 entities[].name 完全一致
[[permissions]]
code = "entity.list"
name = "查看 XX"
description = "查看 XX 列表和详情"
data_scope_levels = ["self", "department", "department_tree", "all"]
[[permissions]]
code = "entity.manage"
name = "管理 XX"
description = "创建、编辑、删除 XX"
# 事件订阅(可选)
[events]
subscribe = ["order.*", "user.created"]
# 实体定义
[[schema.entities]]
name = "entity"
name = "entity" # ← 这个 name 必须与上面 permissions 的 code 前缀一致
display_name = "实体名"
data_scope = true # 启用行级数据权限(可选)
[[schema.entities.fields]]
name = "field_name"
field_type = "String"
required = true
display_name = "字段名"
searchable = true
searchable = true # pg_trgm 全文搜索索引
filterable = true # Generated Column + B-tree 索引,前端渲染 Select 筛选
sortable = true # Generated Column + B-tree 索引,表格列头排序
# 外键引用示例
[[schema.entities.fields]]
name = "customer_id"
field_type = "Uuid"
ref_entity = "customer"
ref_label_field = "name"
ref_search_fields = ["name", "phone"]
ui_widget = "entity_select"
# 实体关系
[[schema.entities.relations]]
entity = "contact"
foreign_key = "customer_id"
on_delete = "cascade" # cascade | nullify | restrict
# 页面声明
[[ui.pages]]
@@ -60,6 +91,8 @@ icon = "icon-name"
enable_search = true
```
> **铁律:** `permissions[].code` 的前缀(`.`之前的部分)必须与 `schema.entities[].name` **完全一致**。每个实体必须声明 `.list` 和 `.manage` 两个权限。不一致会导致页面返回 403。
### 第三步:创建 Rust crate
```bash
@@ -102,6 +135,36 @@ impl Guest for XxxPlugin {
export!(XxxPlugin);
```
### WASM 内部可用的 Host API
插件在 `init``on_tenant_created``handle_event` 中可以调用以下 Host API
| API | 说明 | 使用模式 |
|-----|------|---------|
| `db_insert(entity, data)` | 插入记录,自动注入 tenant_id/id/created_at | 延迟刷新 |
| `db_query(entity, filter, pagination)` | 查询记录,支持 filter 和分页 | **实时查询**(自动先 flush pending writes |
| `db_update(entity, id, data, version)` | 更新记录,乐观锁 version 检查 | 延迟刷新 |
| `db_delete(entity, id)` | 软删除 | 延迟刷新 |
| `event_publish(event_type, payload)` | 发布领域事件 | 事务提交后投递 |
| `config_get(key)` | 读取系统配置 | 预填充 |
| `log_write(level, message)` | 写日志 | 立即 |
| `current_user()` | 获取当前用户 id + tenant_id | 立即 |
| `check_permission(permission)` | 检查当前用户权限 | 立即 |
**filter 格式**JSON
```json
{"status": "active", "type": "enterprise"}
```
**pagination 格式**JSON
```json
{"limit": 50, "offset": 0}
```
**读后写一致性**`db_query` 会先自动 flush 所有 pending writes确保能读取到自己刚插入的数据。
**限制**Fuel 默认 1000 万条 WASM 指令,执行超时 30 秒。避免重计算循环。
### 第四步:注册到 workspace
`Cargo.toml``[workspace] members` 添加 `"crates/erp-plugin-xxx"`
@@ -117,6 +180,14 @@ wasm-tools component new target/wasm32-unknown-unknown/release/erp_plugin_xxx.wa
PluginAdmin 页面上传 `.component.wasm` + `plugin.toml`:上传 → 安装 → 启用。
### 升级流程
上传新版本时,系统自动对比 schema 差异:
- **新增实体** → CREATE TABLE
- **已有实体新增 filterable/sortable/searchable 字段** → ALTER TABLE ADD Generated Column + 索引
- **JSONB 字段新增** → 无需 DDL更新 manifest 即可
- 热更新采用原子替换:先加载新版本到临时 slot成功后替换旧版本失败则旧版本继续运行
## 可用页面类型
| 类型 | 说明 | 必填配置 |
@@ -125,6 +196,9 @@ PluginAdmin 页面上传 `.component.wasm` + `plugin.toml`:上传 → 安装
| `tree` | 树形展示 | `entity`, `label`, `id_field`, `parent_field`, `label_field` |
| `detail` | 详情 Drawer | `entity`, `label`, `sections` |
| `tabs` | 标签页容器 | `label`, `tabs`(子页面列表) |
| `graph` | 关系图谱 | `entity`, `label`, `edges`(节点关系定义) |
| `dashboard` | 统计概览 | `label`, `widgets`stat_card/pie_chart/funnel_chart 等) |
| `kanban` | 看板拖拽 | `entity`, `label`, `status_field` |
## detail section 类型
@@ -137,19 +211,76 @@ PluginAdmin 页面上传 `.component.wasm` + `plugin.toml`:上传 → 安装
| 属性 | 说明 |
|------|------|
| `searchable` | 可搜索,自动创建 B-tree 索引 |
| `filterable` | 可筛选,前端渲染 Select |
| `sortable` | 可排序,表格列头排序图标 |
| `searchable` | 可搜索,自动创建 pg_trgm GIN 索引 |
| `filterable` | 可筛选,Generated Column + B-tree 索引,前端渲染 Select |
| `sortable` | 可排序,Generated Column + B-tree 索引,表格列头排序 |
| `visible_when` | 条件显示,格式 `field == 'value'` |
| `unique` | 唯一约束CREATE UNIQUE INDEX |
| `ui_widget` | 控件select / textarea |
| `ref_entity` | 外键引用,配合 `ui_widget = "entity_select"` |
| `cascade_from` | 级联过滤,如选了客户后过滤联系人 |
| `scope_role` | 数据权限所有者字段,行级权限 self/department 模式依赖此字段 |
| `ui_widget` | 控件select / textarea / entity_select / number / datepicker |
| `options` | select 选项 `[{label, value}]` |
| `validation.pattern` | 正则校验 |
## 权限规则
## 行级数据权限
- 格式:`{entity}.{list|manage}`
- 安装时自动加 manifest_id 前缀
- REST API 动态检查,无精细权限时回退 `plugin.list` / `plugin.admin`
在实体上设置 `data_scope = true`,并在权限中声明 `data_scope_levels`
```toml
[[schema.entities]]
name = "order"
data_scope = true
[[schema.entities.fields]]
name = "owner_id"
field_type = "Uuid"
scope_role = true # 标记此字段为"所有者"
[[permissions]]
code = "order.list"
data_scope_levels = ["self", "department", "department_tree", "all"]
```
四种 scope 含义:
- `self` — 只能看自己创建的scope_role 字段匹配当前用户)
- `department` — 本部门成员创建的
- `department_tree` — 本部门及下级部门
- `all` — 不限
## 权限规则(铁律)
**权限码必须与实体名完全一致。** 这是系统自动校验的基础,不一致会导致 403。
### 命名约定
- manifest 中声明:`{entity_name}.{list|manage}`
- 数据库存储:`{manifest_id}.{entity_name}.{list|manage}`
- 运行时校验:`data_handler` 按 URL 中的 entity name 自动拼接为 `{manifest_id}.{entity_name}.{list|manage}`
### 正确示例
```toml
# 实体名是 customer_tag → 权限码必须用 customer_tag
[[schema.entities]]
name = "customer_tag" # ← 实体名
display_name = "客户标签"
[[permissions]]
code = "customer_tag.list" # ← 必须与实体名一致!
name = "查看客户标签"
[[permissions]]
code = "customer_tag.manage" # ← 必须与实体名一致!
name = "管理客户标签"
```
### 规则总结
1. **每个实体必须声明 `.list` 和 `.manage` 两个权限**(缺一不可)
2. **权限码前缀必须与 `schema.entities[].name` 完全一致**
3. 安装时自动加 manifest_id 前缀(如 `erp-crm.customer_tag.list`
4. REST API 动态检查,无精细权限时回退 `plugin.list` / `plugin.admin`
## REST API
@@ -161,7 +292,30 @@ PluginAdmin 页面上传 `.component.wasm` + `plugin.toml`:上传 → 安装
| PUT | `/api/v1/plugins/{id}/{entity}/{rid}` | 更新(乐观锁) |
| DELETE | `/api/v1/plugins/{id}/{entity}/{rid}` | 软删除 |
| GET | `/api/v1/plugins/{id}/{entity}/count` | 统计 |
| GET | `/api/v1/plugins/{id}/{entity}/aggregate` | 聚合 |
| GET | `/api/v1/plugins/{id}/{entity}/aggregate` | 聚合GROUP BY + COUNT |
| POST | `/api/v1/plugins/{id}/{entity}/aggregate-multi` | 多聚合COUNT + SUM/AVG/MIN/MAX |
| GET | `/api/v1/plugins/{id}/{entity}/timeseries` | 时间序列聚合 |
| POST | `/api/v1/plugins/{id}/{entity}/batch` | 批量操作delete/update上限 100 条) |
### aggregate-multi 请求体
```json
{
"group_by": "status",
"aggregations": [
{"func": "sum", "field": "amount"},
{"func": "avg", "field": "price"}
],
"filter": {"status": "active"}
}
```
## 存储模型
所有业务数据存入 JSONB `data` 列,高频查询字段通过 **Generated Column** 提取为独立列(`_f_{field_name}`
- `filterable` / `sortable` / `unique` 字段 → `_f_{name}` Generated Column + B-tree 索引
- `searchable` String 字段 → `_f_{name}` Generated Column + pg_trgm GIN 索引
- 其余字段仅存在于 JSONB 中,无需 DDL
## 测试检查清单
@@ -174,12 +328,19 @@ PluginAdmin 页面上传 `.component.wasm` + `plugin.toml`:上传 → 安装
- [ ] filter/search/sort 查询正常
- [ ] visible_when 条件字段动态显示
- [ ] 侧边栏菜单正确生成
- [ ] 行级数据权限正常self/department/all
- [ ] entity_select 外键引用和级联过滤正常
- [ ] dashboard 聚合数据正确
## 常见陷阱
1. 表名格式:`plugin_{sanitized_id}_{sanitized_entity}`,连字符变下划线
2. edition 必须是 "2024"
3. WIT 路径:`../erp-plugin-prototype/wit/plugin.wit`,不是 `erp-plugin`
4. JSONB 无外键约束Uuid 字段不自动校验引用完整性
5. Fuel 限制 1000 万,简单逻辑足够,避免重计算循环
6. manifest 中只写 `entity.action`,安装时自动加 manifest_id 前缀
1. **权限码必须与实体名一致P0 级)**`permissions[].code` 的前缀必须与 `schema.entities[].name` 完全匹配。不一致会导致 403
2. **每个实体必须同时声明 `.list` 和 `.manage`**:漏掉 `.list` 会导致列表/详情查询 403漏掉 `.manage` 会导致增删改 403
3. 表名格式:`plugin_{sanitized_id}_{sanitized_entity}`,连字符变下划线
4. edition 必须是 "2024"
5. WIT 路径:`../erp-plugin-prototype/wit/plugin.wit`,不是 `erp-plugin`
6. JSONB 无外键约束Uuid 字段不自动校验引用完整性(需在 WASM 中自行校验)
7. Fuel 限制 1000 万,执行超时 30 秒,避免重计算循环
8. manifest 中只写 `entity.action`,安装时自动加 manifest_id 前缀
9. `db_query` 使用 Generated Column 路径(`_f_` 前缀)过滤,确保字段标记了 `filterable`
10. 热更新时已有实体的普通字段新增不需要 DDLJSONB 天然支持),但 `filterable`/`sortable` 新字段会自动 ALTER TABLE