Files
erp/.claude/skills/plugin-development/SKILL.md
iven 841766b168
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
fix(用户管理): 修复用户列表页面加载失败问题
修复用户列表页面加载失败导致测试超时的问题,确保页面元素正确渲染
2026-04-19 08:46:28 +08:00

12 KiB
Raw Blame History

插件开发 Skill

基于 CRM / 进销存插件的开发经验,提炼可复用的插件开发流程和模式。

触发场景

  • 用户说"开发一个新插件"、"新建行业模块"、"创建插件"
  • 用户提到需要在 ERP 平台上扩展新的业务模块

插件开发流程

第一步:需求分析 → 数据模型

  1. 确定插件 IDerp-crmerp-inventory

  2. 列出实体及其字段,为每个字段标注:

    • 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/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

[metadata]
id = "erp-xxx"
name = "模块名称"
version = "0.1.0"
description = "描述"
author = "ERP Team"
min_platform_version = "0.1.0"

# 权限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 必须与上面 permissions 的 code 前缀一致
display_name = "实体名"
data_scope = true         # 启用行级数据权限(可选)

[[schema.entities.fields]]
name = "field_name"
field_type = "String"
required = true
display_name = "字段名"
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]]
type = "crud"
entity = "entity"
label = "页面标题"
icon = "icon-name"
enable_search = true

铁律: permissions[].code 的前缀(.之前的部分)必须与 schema.entities[].name 完全一致。每个实体必须声明 .list.manage 两个权限。不一致会导致页面返回 403。

第三步:创建 Rust crate

mkdir -p crates/erp-plugin-xxx/src

Cargo.toml

[package]
name = "erp-plugin-xxx"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.55"
serde = { workspace = true }
serde_json = { workspace = true }

src/lib.rs

wit_bindgen::generate!({
    path: "../erp-plugin-prototype/wit/plugin.wit",
    world: "plugin-world",
});

use crate::exports::erp::plugin::plugin_api::Guest;

struct XxxPlugin;

impl Guest for XxxPlugin {
    fn init() -> Result<(), String> { Ok(()) }
    fn on_tenant_created(_tenant_id: String) -> Result<(), String> { Ok(()) }
    fn handle_event(_event_type: String, _payload: Vec<u8>) -> Result<(), String> { Ok(()) }
}

export!(XxxPlugin);

WASM 内部可用的 Host API

插件在 initon_tenant_createdhandle_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

{"status": "active", "type": "enterprise"}

pagination 格式JSON

{"limit": 50, "offset": 0}

读后写一致性db_query 会先自动 flush 所有 pending writes确保能读取到自己刚插入的数据。

限制Fuel 默认 1000 万条 WASM 指令,执行超时 30 秒。避免重计算循环。

第四步:注册到 workspace

Cargo.toml[workspace] members 添加 "crates/erp-plugin-xxx"

第五步:编译和转换

cargo build -p erp-plugin-xxx --target wasm32-unknown-unknown --release
wasm-tools component new target/wasm32-unknown-unknown/release/erp_plugin_xxx.wasm -o target/erp_plugin_xxx.component.wasm

第六步:上传和测试

PluginAdmin 页面上传 .component.wasm + plugin.toml:上传 → 安装 → 启用。

升级流程

上传新版本时,系统自动对比 schema 差异:

  • 新增实体 → CREATE TABLE
  • 已有实体新增 filterable/sortable/searchable 字段 → ALTER TABLE ADD Generated Column + 索引
  • JSONB 字段新增 → 无需 DDL更新 manifest 即可
  • 热更新采用原子替换:先加载新版本到临时 slot成功后替换旧版本失败则旧版本继续运行

可用页面类型

类型 说明 必填配置
crud 增删改查表格 entity, label
tree 树形展示 entity, label, id_field, parent_field, label_field
detail 详情 Drawer entity, label, sections
tabs 标签页容器 label, tabs(子页面列表)
graph 关系图谱 entity, label, edges(节点关系定义)
dashboard 统计概览 label, widgetsstat_card/pie_chart/funnel_chart 等)
kanban 看板拖拽 entity, label, status_field

detail section 类型

类型 说明 配置
fields 字段描述列表 label, fields(字段名数组)
crud 嵌套 CRUD 表格 label, entity, filter_field

字段属性速查

属性 说明
searchable 可搜索,自动创建 pg_trgm GIN 索引
filterable 可筛选Generated Column + B-tree 索引,前端渲染 Select
sortable 可排序Generated Column + B-tree 索引,表格列头排序
visible_when 条件显示,格式 field == 'value'
unique 唯一约束CREATE UNIQUE INDEX
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 正则校验

行级数据权限

在实体上设置 data_scope = true,并在权限中声明 data_scope_levels

[[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}

正确示例

# 实体名是 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

方法 路径 说明
GET /api/v1/plugins/{id}/{entity} 列表filter/search/sort
POST /api/v1/plugins/{id}/{entity} 创建required 校验)
GET /api/v1/plugins/{id}/{entity}/{rid} 详情
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 聚合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 请求体

{
  "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

测试检查清单

  • cargo check --workspace 通过
  • cargo test --workspace 通过
  • WASM 编译 + Component 转换成功
  • 上传 → 安装 → 启用流程正常
  • CRUD 完整可用
  • 唯一字段重复插入返回冲突
  • filter/search/sort 查询正常
  • visible_when 条件字段动态显示
  • 侧边栏菜单正确生成
  • 行级数据权限正常self/department/all
  • entity_select 外键引用和级联过滤正常
  • dashboard 聚合数据正确

常见陷阱

  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