chore: apply cargo fmt across workspace and update docs

- Run cargo fmt on all Rust crates for consistent formatting
- Update CLAUDE.md with WASM plugin commands and dev.ps1 instructions
- Update wiki: add WASM plugin architecture, rewrite dev environment docs
- Minor frontend cleanup (unused imports)
This commit is contained in:
iven
2026-04-15 00:49:20 +08:00
parent e16c1a85d7
commit 9568dd7875
113 changed files with 4355 additions and 937 deletions

View File

@@ -76,9 +76,50 @@ ERP 平台采用 **模块化单体 + 渐进式拆分** 架构。核心原则:
| React 19 + Ant Design 6 | 成熟的组件库,企业后台 UI 标配 |
| Zustand | 极简状态管理,无 boilerplate |
| utoipa | Rust 代码生成 OpenAPI 文档,零额外维护 |
| Wasmtime 43 | WASM 沙箱运行时Component Model 支持Fuel 资源限制 |
## 插件扩展架构
### Q: 为什么用 WASM 而不是 Lua / gRPC / dylib
**A:**
| 方案 | 安全性 | 隔离性 | 性能 | 复杂度 |
|------|--------|--------|------|--------|
| **WASM** | 高(沙箱) | 进程内隔离 | 接近原生JIT | 中 |
| Lua 脚本 | 中 | 无隔离 | 快 | 低 |
| 进程外 gRPC | 高 | 进程级隔离 | 网络开销 | 高 |
| dylib | 低(直接内存) | 无隔离 | 原生 | 低 |
WASM 在安全性和性能之间取得最佳平衡。Wasmtime v43 的 Component Model 提供了类型安全的 Host-Plugin 接口Fuel 机制防止恶意/有缺陷的插件消耗过多资源。
### 插件架构拓扑
```
┌─────────────────────────────────────────────────┐
│ erp-server │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ EventBus │ │ PluginRuntime (Wasmtime) │ │
│ │ (broadcast) │ │ ┌──────┐ ┌──────┐ │ │
│ └──────┬───────┘ │ │插件 A│ │插件 B│ │ │
│ │ │ └──┬───┘ └──┬───┘ │ │
│ │ │ │ Host API │ │ │
│ │ │ ┌──┴────────┴──┐ │ │
│ │ │ │ Host Bridge │ │ │
│ │ │ └──┬───────────┘ │ │
│ │ └─────┼────────────────────┘ │
│ │ │ │
│ ┌──────┴───────┐ ┌────┴─────┐ │
│ │ DB (SeaORM) │ │ EventBus │ │
│ └──────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
```
插件通过 Host Bridge 调用系统功能db_insert、event_publish 等Host Bridge 自动注入多租户隔离tenant_id和权限检查。详见 [[wasm-plugin]]。
## 关联模块
- **[[erp-core]]** — 架构契约的定义者
- **[[erp-server]]** — 架构的组装执行者
- **[[database]]** — 多租户隔离的物理实现
- **[[wasm-plugin]]** — 插件扩展架构的实现

View File

@@ -49,6 +49,7 @@
- **[[erp-message]]** — 实现 `ErpModule` trait订阅通知事件
- **[[erp-config]]** — 实现 `ErpModule` trait
- **[[database]]** — 迁移表结构必须与 `BaseFields` 对齐
- **[[wasm-plugin]]** — WASM 插件通过 Host Bridge 桥接 EventBus
## 关键文件

View File

@@ -43,8 +43,8 @@ appStore {
### 开发服务器代理
```
http://localhost:5173/api/* → http://localhost:3000/* (API)
ws://localhost:5173/ws/* → ws://localhost:3000/* (WebSocket)
http://localhost:5174/api/* → http://localhost:3000/* (API)
ws://localhost:5174/ws/* → ws://localhost:3000/* (WebSocket)
```
### 当前状态

View File

@@ -5,12 +5,13 @@
**模块化 SaaS ERP 底座**Rust + React 技术栈,提供身份权限/工作流/消息/配置四大基础模块,支持行业业务模块快速插接。
关键数字:
- 8 个 Rust crate全部已实现1 个前端 SPA
- 29 个数据库迁移
- 10 个 Rust crate8 个已实现 + 2 个插件原型1 个前端 SPA
- 32 个数据库迁移
- 5 个业务模块 (auth, config, workflow, message, server)
- 2 个插件 crate (plugin-prototype Host 运行时, plugin-test-sample 测试插件)
- Health Check API (`/api/v1/health`)
- OpenAPI JSON (`/api/docs/openapi.json`)
- Phase 1-6 全部完成
- Phase 1-6 全部完成WASM 插件原型 V1-V6 验证通过
## 模块导航树
@@ -27,10 +28,14 @@
### L3 组装层
- [[erp-server]] — Axum 服务入口 · AppState · ModuleRegistry 集成 · 配置加载 · 数据库连接 · 优雅关闭
### 插件系统
- [[wasm-plugin]] — Wasmtime 运行时 · WIT 接口契约 · Host API · Fuel 资源限制 · 插件制作完整流程
### 基础设施
- [[database]] — SeaORM 迁移 · 多租户表结构 · 软删除模式
- [[infrastructure]] — Docker Compose · PostgreSQL 16 · Redis 7
- [[infrastructure]] — Windows 开发环境 · PostgreSQL 16 · Redis 7 · 一键启动脚本
- [[frontend]] — React SPA · Ant Design 布局 · Zustand 状态
- [[testing]] — 测试环境指南 · 验证清单 · 常见问题
### 横切关注点
- [[architecture]] — 架构决策记录 · 设计原则 · 技术选型理由
@@ -47,6 +52,8 @@
**ModuleRegistry 怎么工作?** 每个 Phase 2+ 的业务模块实现 ErpModule trait在 main.rs 中链式注册。registry 自动构建路由和事件处理器。
**插件系统怎么扩展业务?** 通过 [[wasm-plugin]] 的 WASM 沙箱运行第三方插件,插件通过 WIT 定义的 Host API 与系统交互。详细流程见插件制作指南。
**版本差异怎么办?** package.json 使用 React 19 + Ant Design 6比规格文档更新以实际代码为准。
## 开发进度
@@ -59,6 +66,7 @@
| 4 | 工作流引擎 | 完成 |
| 5 | 消息中心 | 完成 |
| 6 | 整合与打磨 | 完成 |
| - | WASM 插件原型 | V1-V6 验证通过 |
## 关键文档索引
@@ -66,5 +74,7 @@
|------|------|
| 设计规格 | `docs/superpowers/specs/2026-04-10-erp-platform-base-design.md` |
| 实施计划 | `docs/superpowers/plans/2026-04-10-erp-platform-base-plan.md` |
| WASM 插件设计 | `docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md` |
| WASM 插件计划 | `docs/superpowers/plans/2026-04-13-wasm-plugin-system-plan.md` |
| 协作规则 | `CLAUDE.md` |
| 设计评审 | `plans/squishy-pondering-aho-agent-a23c7497aadc6da41.md` |

View File

@@ -1,20 +1,25 @@
# infrastructure (Docker 与开发环境)
# infrastructure (开发环境)
## 设计思想
开发环境使用 Docker Compose 提供基础设施服务,应用服务在宿主机运行。这种设计允许
- 后端 Rust 服务快速重启(无需容器化构建)
开发环境**Windows** 宿主机直接运行所有服务
- PostgreSQL 16+ 和 Redis 7+ 通过 Windows 原生安装运行
- 后端 Rust 服务通过 `cargo run` 快速重启
- 前端 Vite 热更新直接在宿主机
- 数据库和缓存服务标准化,团队成员环境一致
- PowerShell 脚本 (`dev.ps1`) 提供一键启动/停止
> Docker Compose 配置保留在 `docker/` 目录下,可供需要容器化环境的场景使用,但日常开发不依赖 Docker。
## 代码逻辑
### 服务配置
| 服务 | 镜像 | 端口 | 用途 |
|------|------|------|------|
| erp-postgres | postgres:16-alpine | 5432 | 主数据库 |
| erp-redis | redis:7-alpine | 6379 | 缓存 + 会话 |
| 服务 | 端口 | 用途 |
|------|------|------|
| PostgreSQL 16+ | 5432 | 主数据库 |
| Redis 7+ | 6379 | 缓存 + 会话 |
| erp-server (Axum) | 3000 | 后端 API |
| Vite dev server | 5174 | 前端 SPA |
### 连接信息
```
@@ -22,36 +27,45 @@ PostgreSQL: postgres://erp:erp_dev_2024@localhost:5432/erp
Redis: redis://localhost:6379
```
### 健康检查
- PostgreSQL: `pg_isready` 每 5 秒5 次重试
- Redis: `redis-cli ping` 每 5 秒5 次重试
### 一键启动
### 数据持久化
- `postgres_data` — 命名卷PostgreSQL 数据
- `redis_data` — 命名卷Redis 数据
```powershell
.\dev.ps1 # 启动后端 + 前端
.\dev.ps1 -Status # 查看端口状态
.\dev.ps1 -Stop # 停止所有服务
.\dev.ps1 -Restart # 重启所有服务
```
### 环境变量
通过 `docker/.env.example` 文档化,使用默认值即可启动开发环境
通过 `crates/erp-server/config/default.toml` 配置,无需额外环境变量文件即可启动
## 关联模块
- **[[erp-server]]** — 连接 PostgreSQL 和 Redis
- **[[database]]** — 迁移在 PostgreSQL 中执行
- **[[frontend]]** — Vite 代理 API 到后端
- **[[testing]]** — 测试环境详细指南
## 关键文件
| 文件 | 职责 |
|------|------|
| `docker/docker-compose.yml` | 服务定义 |
| `docker/.env.example` | 环境变量模板 |
| `dev.ps1` | 一键启动/停止脚本 |
| `docker/docker-compose.yml` | 可选的 Docker Compose 配置 |
| `crates/erp-server/config/default.toml` | 默认连接配置 |
## 常用命令
```bash
cd docker && docker compose up -d # 启动服务
docker compose -f docker/docker-compose.yml ps # 查看状态
docker compose -f docker/docker-compose.yml down # 停止
docker exec -it erp-postgres psql -U erp # 连接数据库
```powershell
# 一键启动(推荐)
.\dev.ps1
# 手动启动后端
cargo run -p erp-server
# 手动启动前端
cd apps/web && pnpm dev
# 连接数据库
psql -U erp -d erp -h localhost
```

193
wiki/testing.md Normal file
View File

@@ -0,0 +1,193 @@
# 测试环境指南
> 本项目在 **Windows** 环境下开发,使用 PowerShell 脚本一键启动。不使用 Docker数据库和缓存直接通过原生安装运行。
## 环境要求
| 工具 | 最低版本 | 用途 |
|------|---------|------|
| Rust | stable (1.93+) | 后端编译 |
| Node.js | 20+ | 前端工具链 |
| pnpm | 9+ | 前端包管理 |
| PostgreSQL | 16+ | 主数据库 |
| Redis | 7+ | 缓存 + 会话 |
## 服务连接信息
| 服务 | 地址 | 用途 |
|------|------|------|
| PostgreSQL | `postgres://erp:erp_dev_2024@localhost:5432/erp` | 主数据库 |
| Redis | `redis://localhost:6379` | 缓存 + 会话 |
| 后端 API | `http://localhost:3000/api/v1` | Axum 服务 |
| 前端 SPA | `http://localhost:5174` | Vite 开发服务器 |
## 一键启动(推荐)
使用 PowerShell 脚本管理前后端服务:
```powershell
.\dev.ps1 # 启动后端 + 前端
.\dev.ps1 -Status # 查看端口状态
.\dev.ps1 -Restart # 重启所有服务
.\dev.ps1 -Stop # 停止所有服务
```
脚本会自动:
1. 清理端口占用
2. 编译并启动 Rust 后端 (`cargo run -p erp-server`)
3. 安装前端依赖并启动 Vite 开发服务器 (`pnpm dev`)
## 手动启动
### 1. 启动基础设施
确保 PostgreSQL 和 Redis 服务已在 Windows 上运行:
```powershell
# 检查 PostgreSQL 服务状态
Get-Service -Name "postgresql*"
# 检查 Redis 是否运行(如果作为 Windows 服务安装)
Get-Service -Name "Redis" -ErrorAction SilentlyContinue
# 或通过命令行启动 Redis
redis-server
```
### 2. 启动后端
```powershell
cargo run -p erp-server
```
首次运行会自动执行数据库迁移。
### 3. 启动前端
```powershell
cd apps/web
pnpm install # 首次需要安装依赖
pnpm dev # 启动开发服务器
```
## 验证清单
### 后端验证
```bash
# 编译检查(无错误)
cargo check
# 全量测试(应全部通过)
cargo test --workspace
# Lint 检查(无警告)
cargo clippy -- -D warnings
# 格式检查
cargo fmt --check
```
### 前端验证
```bash
cd apps/web
# 安装依赖
pnpm install
# TypeScript 编译 + 生产构建
pnpm build
# 类型检查
pnpm tsc -b
```
### 功能验证
| 端点 | 方法 | 说明 |
|------|------|------|
| `http://localhost:3000/api/v1/health` | GET | 健康检查 |
| `http://localhost:3000/api/docs/openapi.json` | GET | OpenAPI 文档 |
| `http://localhost:5174` | GET | 前端页面 |
### 登录信息
- 用户名: `admin`
- 密码: `Admin@2026`
## 数据库管理
### 连接数据库
```bash
psql -U erp -d erp -h localhost
```
### 查看表结构
```sql
\dt -- 列出所有表
\d table_name -- 查看表结构
```
### 迁移
迁移在 `crates/erp-server/migration/src/` 目录下。后端启动时自动执行。
## 测试详情
### 测试分布
| Crate | 测试数 | 说明 |
|-------|--------|------|
| erp-auth | 8 | 密码哈希、TTL 解析 |
| erp-core | 6 | RBAC 权限检查 |
| erp-workflow | 16 | BPMN 解析、表达式求值 |
| erp-plugin-prototype | 6 | WASM 插件集成测试 |
| **总计** | **36** | |
### 运行特定测试
```bash
# 运行单个 crate 的测试
cargo test -p erp-auth
# 运行匹配名称的测试
cargo test -p erp-core -- require_permission
# 运行插件集成测试
cargo test -p erp-plugin-prototype
```
## 常见问题
### Q: 端口被占用
```powershell
# 查看占用端口的进程
Get-NetTCPConnection -LocalPort 3000 -State Listen
# 终止进程
Stop-Process -Id <PID> -Force
# 或使用 dev.ps1 自动清理
.\dev.ps1 -Stop
```
### Q: 数据库连接失败
1. 确认 PostgreSQL 服务正在运行
2. 检查 `crates/erp-server/config/default.toml` 中的连接字符串
3. 确认 `erp` 数据库已创建
### Q: 首次启动很慢
首次 `cargo run` 需要编译整个 workspace后续增量编译会很快。
## 关联模块
- [[infrastructure]] — 基础设施配置详情
- [[database]] — 数据库迁移和表结构
- [[frontend]] — 前端技术栈和配置
- [[erp-server]] — 后端服务配置

445
wiki/wasm-plugin.md Normal file
View File

@@ -0,0 +1,445 @@
# wasm-plugin (WASM 插件系统)
## 设计思想
ERP 平台通过 WASM 沙箱实现**安全、隔离、热插拔**的业务扩展。插件在 Wasmtime 运行时中执行,只能通过 WIT 定义的 Host API 与系统交互,无法直接访问数据库、文件系统或网络。
核心决策:
- **WASM 沙箱** — 插件代码在隔离环境中运行Host 控制所有资源访问
- **WIT 接口契约** — 通过 `.wit` 文件定义 Host ↔ 插件的双向接口bindgen 自动生成类型化绑定
- **Fuel 资源限制** — 通过燃料机制限制插件 CPU 使用,防止无限循环
- **声明式 Host API** — 插件通过 `db_insert` / `event_publish` 等函数操作数据Host 自动注入 tenant_id、校验权限
## 原型验证结果 (V1-V6)
| 验证项 | 状态 | 说明 |
|--------|------|------|
| V1: WIT 接口 + bindgen! 编译 | 通过 | `bindgen!({ path, world })` 生成 Host trait + Guest 绑定 |
| V2: Host 调用插件导出函数 | 通过 | `call_init()` / `call_handle_event()` / `call_on_tenant_created()` |
| V3: 插件回调 Host API | 通过 | 插件中 `host_api::db_insert()` 等正确回调到 HostState |
| V4: async 实例化桥接 | 通过 | `instantiate_async` 正常工作(调用方法本身是同步的) |
| V5: Fuel 资源限制 | 通过 | 低 fuel 时正确 trap不会无限循环 |
| V6: 从二进制动态加载 | 通过 | `.component.wasm` 文件加载,测试插件 110KB |
## 项目结构
```
crates/
erp-plugin-prototype/ ← Host 端运行时
wit/
plugin.wit ← WIT 接口定义
src/
lib.rs ← Engine/Store/Linker 创建、HostState + Host trait 实现
main.rs ← 手动测试入口(空)
tests/
test_plugin_integration.rs ← 6 个集成测试
erp-plugin-test-sample/ ← 测试插件
src/
lib.rs ← 实现 Guest trait调用 Host API
```
## WIT 接口定义
文件:`crates/erp-plugin-prototype/wit/plugin.wit`
```
package erp:plugin;
// Host 暴露给插件的 API插件 import
interface host-api {
db-insert: func(entity: string, data: list<u8>) -> result<list<u8>, string>;
db-query: func(entity: string, filter: list<u8>, pagination: list<u8>) -> result<list<u8>, string>;
db-update: func(entity, id: string, data: list<u8>, version: s64) -> result<list<u8>, string>;
db-delete: func(entity: string, id: string) -> result<_, string>;
event-publish: func(event-type: string, payload: list<u8>) -> result<_, string>;
config-get: func(key: string) -> result<list<u8>, string>;
log-write: func(level: string, message: string);
current-user: func() -> result<list<u8>, string>;
check-permission: func(permission: string) -> result<bool, string>;
}
// 插件导出的 APIHost 调用)
interface plugin-api {
init: func() -> result<_, string>;
on-tenant-created: func(tenant-id: string) -> result<_, string>;
handle-event: func(event-type: string, payload: list<u8>) -> result<_, string>;
}
world plugin-world {
import host-api;
export plugin-api;
}
```
## 关键技术要点
### HasSelf<T> — Linker 注册模式
当 Store 数据类型(`HostState`)直接实现 `Host` trait 时,使用 `HasSelf<T>` 作为 `add_to_linker` 的类型参数:
```rust
use wasmtime::component::{HasSelf, Linker};
let mut linker = Linker::new(engine);
PluginWorld::add_to_linker::<_, HasSelf<HostState>>(&mut linker, |state| state)?;
```
`HasSelf<HostState>` 表示 `Data<'a> = &'a mut HostState`bindgen 生成的 `Host for &mut T` blanket impl 确保调用链正确。
### WASM Component vs Core Module
`wit_bindgen::generate!` 生成的是 core WASM 模块(`.wasm`),但 `Component::from_binary()` 需要 WASM Component 格式。转换步骤:
```bash
# 1. 编译为 core wasm
cargo build -p <plugin-crate> --target wasm32-unknown-unknown --release
# 2. 转换为 component
wasm-tools component new target/wasm32-unknown-unknown/release/<name>.wasm \
-o target/<name>.component.wasm
```
### Fuel 资源限制
```rust
let mut store = Store::new(engine, HostState::new());
store.set_fuel(1_000_000)?; // 分配 100 万 fuel
store.limiter(|state| &mut state.limits); // 内存限制
```
Fuel 不足时WASM 执行会 trap`wasm trap: interrupt`Host 可以捕获并处理。
### 调用方法 — 同步,非 async
bindgen 生成的调用方法(`call_init``call_handle_event`)是同步的:
```rust
// 正确
instance.erp_plugin_plugin_api().call_init(&mut store)?;
// 错误(不存在 async 版本的调用方法)
instance.erp_plugin_plugin_api().call_init(&mut store).await?;
```
但实例化可以异步:`PluginWorld::instantiate_async(&mut store, &component, &linker).await?`
## 关键文件
| 文件 | 职责 |
|------|------|
| `crates/erp-plugin-prototype/wit/plugin.wit` | WIT 接口定义Host API + Plugin API |
| `crates/erp-plugin-prototype/src/lib.rs` | Host 运行时Engine/Store 创建、HostState、Host trait 实现 |
| `crates/erp-plugin-prototype/tests/test_plugin_integration.rs` | V1-V6 集成测试 |
| `crates/erp-plugin-test-sample/src/lib.rs` | 测试插件Guest trait 实现 |
## 关联模块
- **[[architecture]]** — 插件架构是模块化单体的重要扩展机制
- **[[erp-core]]** — EventBus 事件将被桥接到插件的 `handle_event`
- **[[erp-server]]** — 未来集成插件运行时的组装点
---
# 插件制作完整流程
以下是从零创建一个新业务模块插件的完整步骤。
## 第一步:准备 WIT 接口
WIT 文件定义 Host 和插件之间的契约。现有接口位于 `crates/erp-plugin-prototype/wit/plugin.wit`
如果新插件需要扩展 Host API如新增文件上传、HTTP 代理等),在 `host-api` interface 中添加函数:
```wit
// 在 host-api 中新增
file-upload: func(filename: string, data: list<u8>) -> result<string, string>;
http-proxy: func(url: string, method: string, body: option<list<u8>>) -> result<list<u8>, string>;
```
如果插件需要新的生命周期钩子,在 `plugin-api` interface 中添加:
```wit
// 在 plugin-api 中新增
on-order-approved: func(order-id: string) -> result<_, string>;
```
修改 WIT 后,需要重新编译 Host crate 和所有插件。
## 第二步:创建插件 crate
`crates/` 下创建新的插件 crate
```bash
mkdir -p crates/erp-plugin-<业务名>
```
`Cargo.toml` 模板:
```toml
[package]
name = "erp-plugin-<业务名>"
version = "0.1.0"
edition = "2024"
description = "<业务描述> WASM 插件"
[lib]
crate-type = ["cdylib"] # 必须是 cdylib 才能编译为 WASM
[dependencies]
wit-bindgen = "0.55" # 生成 Guest 端绑定
serde = { workspace = true }
serde_json = { workspace = true }
```
将新 crate 加入 workspace编辑根 `Cargo.toml`
```toml
members = [
# ... 已有成员 ...
"crates/erp-plugin-<业务名>",
]
```
## 第三步:实现插件逻辑
创建 `src/lib.rs`,实现 `Guest` trait
```rust
//! <业务名> WASM 插件
use serde_json::json;
// 生成 Guest 端绑定(路径指向 Host crate 的 WIT 文件)
wit_bindgen::generate!({
path: "../erp-plugin-prototype/wit/plugin.wit",
world: "plugin-world",
});
// 导入 Host APIbindgen 生成)
use crate::erp::plugin::host_api;
// 导入 Guest traitbindgen 生成)
use crate::exports::erp::plugin::plugin_api::Guest;
// 插件结构体(名称任意,但必须是模块级可见的)
struct MyPlugin;
impl Guest for MyPlugin {
/// 初始化 — 注册默认数据、订阅事件等
fn init() -> Result<(), String> {
host_api::log_write("info", "<业务名>插件初始化");
// 示例:创建默认配置
let config = json!({"default_category": "通用"}).to_string();
host_api::db_insert("<业务>_config", config.as_bytes())
.map_err(|e| format!("初始化失败: {}", e))?;
Ok(())
}
/// 租户创建时 — 初始化租户的默认数据
fn on_tenant_created(tenant_id: String) -> Result<(), String> {
host_api::log_write("info", &format!("新租户: {}", tenant_id));
let data = json!({"tenant_id": tenant_id, "name": "默认仓库"}).to_string();
host_api::db_insert("warehouse", data.as_bytes())
.map_err(|e| format!("创建默认仓库失败: {}", e))?;
Ok(())
}
/// 处理订阅的事件
fn handle_event(event_type: String, payload: Vec<u8>) -> Result<(), String> {
host_api::log_write("debug", &format!("收到事件: {}", event_type));
let data: serde_json::Value = serde_json::from_slice(&payload)
.map_err(|e| format!("解析事件失败: {}", e))?;
match event_type.as_str() {
"order.created" => {
// 处理订单创建事件
let order_id = data["id"].as_str().unwrap_or("");
host_api::log_write("info", &format!("新订单: {}", order_id));
}
"workflow.task.completed" => {
// 处理审批完成事件
let order_id = data["order_id"].as_str().unwrap_or("unknown");
let update = json!({"status": "approved"}).to_string();
host_api::db_update("purchase_order", order_id, update.as_bytes(), 1)
.map_err(|e| format!("更新失败: {}", e))?;
// 发布下游事件
let evt = json!({"order_id": order_id}).to_string();
host_api::event_publish("<业务>.order.approved", evt.as_bytes())
.map_err(|e| format!("发布事件失败: {}", e))?;
}
_ => {
host_api::log_write("debug", &format!("忽略事件: {}", event_type));
}
}
Ok(())
}
}
// 导出插件实例(宏会注册 Guest trait 实现)
export!(MyPlugin);
```
### Host API 速查
| 函数 | 签名 | 用途 |
|------|------|------|
| `db_insert` | `(entity, data) → result<record, string>` | 插入记录Host 自动注入 id/tenant_id/timestamp |
| `db_query` | `(entity, filter, pagination) → result<list, string>` | 查询记录,自动过滤 tenant_id + 排除软删除 |
| `db_update` | `(entity, id, data, version) → result<record, string>` | 更新记录,检查乐观锁 version |
| `db_delete` | `(entity, id) → result<_, string>` | 软删除记录 |
| `event_publish` | `(event_type, payload) → result<_, string>` | 发布领域事件 |
| `config_get` | `(key) → result<value, string>` | 读取系统配置 |
| `log_write` | `(level, message) → ()` | 写日志,自动关联 tenant_id + plugin_id |
| `current_user` | `() → result<user_info, string>` | 获取当前用户信息 |
| `check_permission` | `(permission) → result<bool, string>` | 检查当前用户权限 |
### 数据传递约定
所有 Host API 的数据参数使用 `list<u8>`(即 `Vec<u8>`),约定用 JSON 序列化:
```rust
// 构造数据
let data = json!({"sku": "ITEM-001", "quantity": 100}).to_string();
// 插入
let result_bytes = host_api::db_insert("inventory_item", data.as_bytes())
.map_err(|e| e.to_string())?;
// 解析返回
let record: serde_json::Value = serde_json::from_slice(&result_bytes)
.map_err(|e| e.to_string())?;
let new_id = record["id"].as_str().unwrap();
```
## 第四步:编译为 WASM
```bash
# 编译为 core WASM 模块
cargo build -p erp-plugin-<业务名> --target wasm32-unknown-unknown --release
# 转换为 WASM Component必须Host 只接受 Component 格式)
wasm-tools component new \
target/wasm32-unknown-unknown/release/erp_plugin_<业务名>.wasm \
-o target/erp_plugin_<业务名>.component.wasm
```
检查产物大小(目标 < 2MB
```bash
ls -la target/erp_plugin_<业务名>.component.wasm
```
## 第五步:编写集成测试
`crates/erp-plugin-prototype/tests/` 下创建测试文件或扩展现有测试
```rust
use anyhow::Result;
use erp_plugin_prototype::{create_engine, load_plugin};
fn wasm_path() -> String {
"../../target/erp_plugin_<业务名>.component.wasm".into()
}
#[tokio::test]
async fn test_<业务名>_init() -> Result<()> {
let wasm_bytes = std::fs::read(wasm_path())?;
let engine = create_engine()?;
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
// 调用 init
instance.erp_plugin_plugin_api().call_init(&mut store)?
.map_err(|e| anyhow::anyhow!(e))?;
// 验证 Host 端效果
let state = store.data();
assert!(state.db_ops.iter().any(|op| op.entity == "<业务>_config"));
Ok(())
}
#[tokio::test]
async fn test_<业务名>_handle_event() -> Result<()> {
let wasm_bytes = std::fs::read(wasm_path())?;
let engine = create_engine()?;
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
// 先初始化
instance.erp_plugin_plugin_api().call_init(&mut store)?
.map_err(|e| anyhow::anyhow!(e))?;
// 模拟事件
let payload = json!({"id": "ORD-001"}).to_string();
instance.erp_plugin_plugin_api()
.call_handle_event(&mut store, "order.created", payload.as_bytes())?
.map_err(|e| anyhow::anyhow!(e))?;
Ok(())
}
```
## 第六步:运行测试
```bash
# 先确保编译了 component
cargo build -p erp-plugin-<业务名> --target wasm32-unknown-unknown --release
wasm-tools component new target/wasm32-unknown-unknown/release/erp_plugin_<业务名>.wasm \
-o target/erp_plugin_<业务名>.component.wasm
# 运行集成测试
cargo test -p erp-plugin-prototype
```
## 流程速查图
```
1. 修改 WIT如需新接口 crates/erp-plugin-prototype/wit/plugin.wit
2. 创建插件 crate crates/erp-plugin-<名>/
- Cargo.toml (cdylib + wit-bindgen)
- src/lib.rs (impl Guest)
3. 编译 core wasm cargo build --target wasm32-unknown-unknown --release
4. 转为 component wasm-tools component new <in.wasm> -o <out.component.wasm>
5. 编写测试 crates/erp-plugin-prototype/tests/
6. 运行测试 cargo test -p erp-plugin-prototype
```
## 常见问题
### Q: "attempted to parse a wasm module with a component parser"
**A:** 使用了 core WASM 而非 Component运行 `wasm-tools component new` 转换
### Q: "cannot infer type of the type parameter D"
**A:** `add_to_linker` 需要显式指定 `HasSelf<T>``add_to_linker::<_, HasSelf<HostState>>(linker, |s| s)`
### Q: "wasm trap: interrupt"(非 fuel 耗尽)
**A:** 检查是否启用了 epoch_interruption 但未定期 bump epoch原型阶段建议只使用 fuel 限制
### Q: 插件中如何调试?
**A:** 使用 `host_api::log_write("debug", "message")` 输出日志Host `store.data().logs` 可查看所有日志
### Q: 如何限制插件内存?
**A:** 通过 `StoreLimitsBuilder` 配置
```rust
let limits = StoreLimitsBuilder::new()
.memory_size(10 * 1024 * 1024) // 10MB
.build();
```
## 后续规划
- **Phase 7**: 将原型集成到 erp-server替换模拟 Host API 为真实数据库操作
- **动态表**: 支持 `db_insert("dynamic_table", ...)` 自动创建/迁移表
- **前端集成**: PluginCRUDPage 组件根据 WIT 定义自动生成 CRUD 页面
- **插件市场**: 插件元数据版本管理签名验证