From 9568dd787519f62a235cdda7c481582cd562b41b Mon Sep 17 00:00:00 2001 From: iven Date: Wed, 15 Apr 2026 00:49:20 +0800 Subject: [PATCH] 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) --- CLAUDE.md | 18 + Cargo.lock | 1594 ++++++++++++++++- Cargo.toml | 2 + apps/web/src/api/auth.ts | 1 + apps/web/src/api/orgs.ts | 6 + apps/web/src/api/roles.ts | 2 + apps/web/src/api/users.ts | 1 + apps/web/src/components/NotificationPanel.tsx | 4 +- apps/web/src/layouts/MainLayout.tsx | 2 +- apps/web/src/pages/Login.tsx | 1 - apps/web/src/pages/Organizations.tsx | 5 +- apps/web/src/pages/Roles.tsx | 2 +- apps/web/src/pages/Users.tsx | 5 +- apps/web/src/pages/Workflow.tsx | 4 +- .../web/src/pages/settings/AuditLogViewer.tsx | 10 +- .../src/pages/settings/DictionaryManager.tsx | 2 +- apps/web/src/pages/settings/MenuConfig.tsx | 25 +- .../web/src/pages/settings/NumberingRules.tsx | 2 +- apps/web/src/pages/settings/ThemeSettings.tsx | 3 +- .../src/pages/workflow/InstanceMonitor.tsx | 2 +- .../src/pages/workflow/ProcessDesigner.tsx | 4 +- apps/web/vite.config.ts | 26 +- crates/erp-auth/src/entity/mod.rs | 14 +- crates/erp-auth/src/handler/auth_handler.rs | 7 +- crates/erp-auth/src/handler/org_handler.rs | 20 +- crates/erp-auth/src/handler/role_handler.rs | 2 +- crates/erp-auth/src/handler/user_handler.rs | 18 +- crates/erp-auth/src/middleware/jwt_auth.rs | 4 +- crates/erp-auth/src/middleware/mod.rs | 2 +- crates/erp-auth/src/module.rs | 3 +- crates/erp-auth/src/service/auth_service.rs | 12 +- crates/erp-auth/src/service/dept_service.rs | 61 +- crates/erp-auth/src/service/org_service.rs | 57 +- crates/erp-auth/src/service/password.rs | 2 +- .../erp-auth/src/service/position_service.rs | 30 +- crates/erp-auth/src/service/role_service.rs | 43 +- crates/erp-auth/src/service/seed.rs | 348 +++- crates/erp-auth/src/service/user_service.rs | 56 +- crates/erp-config/src/entity/mod.rs | 2 +- crates/erp-config/src/error.rs | 4 +- .../src/handler/dictionary_handler.rs | 34 +- .../src/handler/language_handler.rs | 26 +- crates/erp-config/src/handler/menu_handler.rs | 3 +- .../src/handler/numbering_handler.rs | 3 +- .../erp-config/src/handler/theme_handler.rs | 3 +- crates/erp-config/src/module.rs | 11 +- .../src/service/dictionary_service.rs | 88 +- crates/erp-config/src/service/menu_service.rs | 58 +- .../src/service/numbering_service.rs | 76 +- .../erp-config/src/service/setting_service.rs | 120 +- crates/erp-core/src/error.rs | 2 +- crates/erp-core/src/module.rs | 6 +- crates/erp-core/src/rbac.rs | 5 +- crates/erp-message/src/dto.rs | 2 +- .../src/handler/message_handler.rs | 4 +- .../src/handler/subscription_handler.rs | 12 +- .../src/handler/template_handler.rs | 4 +- crates/erp-message/src/module.rs | 42 +- .../src/service/message_service.rs | 61 +- .../src/service/template_service.rs | 4 +- crates/erp-plugin-prototype/Cargo.toml | 18 + crates/erp-plugin-prototype/src/lib.rs | 208 +++ crates/erp-plugin-prototype/src/main.rs | 10 + .../tests/test_plugin_integration.rs | 220 +++ crates/erp-plugin-prototype/wit/plugin.wit | 48 + crates/erp-plugin-test-sample/Cargo.toml | 13 + crates/erp-plugin-test-sample/src/lib.rs | 68 + crates/erp-server/migration/src/lib.rs | 2 + .../src/m20260410_000001_create_tenant.rs | 7 +- .../src/m20260411_000002_create_users.rs | 7 +- ...20260411_000003_create_user_credentials.rs | 6 +- .../src/m20260411_000005_create_roles.rs | 7 +- ...20260411_000007_create_role_permissions.rs | 6 +- ...20260412_000013_create_dictionary_items.rs | 6 +- .../src/m20260412_000014_create_menus.rs | 7 +- .../src/m20260412_000016_create_settings.rs | 7 +- ...60412_000018_create_process_definitions.rs | 24 +- ...0260412_000019_create_process_instances.rs | 30 +- .../src/m20260412_000020_create_tokens.rs | 7 +- .../src/m20260412_000021_create_tasks.rs | 7 +- ...0260412_000022_create_process_variables.rs | 18 +- ...0260413_000023_create_message_templates.rs | 42 +- .../src/m20260413_000024_create_messages.rs | 43 +- ...413_000025_create_message_subscriptions.rs | 60 +- .../src/m20260413_000026_create_audit_logs.rs | 31 +- ...4_000027_fix_unique_indexes_soft_delete.rs | 4 +- ...4_000032_fix_settings_unique_index_null.rs | 65 + .../m20260416_000031_create_domain_events.rs | 6 +- crates/erp-server/src/handlers/audit_log.rs | 5 +- crates/erp-server/src/handlers/health.rs | 2 +- crates/erp-server/src/handlers/openapi.rs | 9 +- crates/erp-server/src/main.rs | 54 +- .../erp-server/src/middleware/rate_limit.rs | 9 +- crates/erp-workflow/src/engine/executor.rs | 249 +-- crates/erp-workflow/src/engine/expression.rs | 10 +- crates/erp-workflow/src/engine/mod.rs | 2 +- crates/erp-workflow/src/engine/model.rs | 17 +- crates/erp-workflow/src/engine/parser.rs | 16 +- crates/erp-workflow/src/entity/mod.rs | 4 +- .../src/handler/definition_handler.rs | 14 +- .../src/handler/instance_handler.rs | 6 +- .../erp-workflow/src/handler/task_handler.rs | 9 +- crates/erp-workflow/src/module.rs | 10 +- .../src/service/definition_service.rs | 74 +- .../src/service/instance_service.rs | 39 +- .../erp-workflow/src/service/task_service.rs | 56 +- wiki/architecture.md | 41 + wiki/erp-core.md | 1 + wiki/frontend.md | 4 +- wiki/index.md | 18 +- wiki/infrastructure.md | 58 +- wiki/testing.md | 193 ++ wiki/wasm-plugin.md | 445 +++++ 113 files changed, 4355 insertions(+), 937 deletions(-) create mode 100644 crates/erp-plugin-prototype/Cargo.toml create mode 100644 crates/erp-plugin-prototype/src/lib.rs create mode 100644 crates/erp-plugin-prototype/src/main.rs create mode 100644 crates/erp-plugin-prototype/tests/test_plugin_integration.rs create mode 100644 crates/erp-plugin-prototype/wit/plugin.wit create mode 100644 crates/erp-plugin-test-sample/Cargo.toml create mode 100644 crates/erp-plugin-test-sample/src/lib.rs create mode 100644 crates/erp-server/migration/src/m20260414_000032_fix_settings_unique_index_null.rs create mode 100644 wiki/testing.md create mode 100644 wiki/wasm-plugin.md diff --git a/CLAUDE.md b/CLAUDE.md index ca6ee77..23fe4a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -372,6 +372,17 @@ cd apps/web && pnpm build # 构建生产版本 # === 数据库 === docker exec -it erp-postgres psql -U erp # 连接数据库 + +# === WASM 插件 === +cargo build -p erp-plugin-test-sample --target wasm32-unknown-unknown --release # 编译测试插件 +wasm-tools component new target/wasm32-unknown-unknown/release/erp_plugin_test_sample.wasm -o target/erp_plugin_test_sample.component.wasm # 转为 Component +cargo test -p erp-plugin-prototype # 运行插件集成测试 + +# === 一键启动 (PowerShell) === +.\dev.ps1 # 启动前后端(自动清理端口占用) +.\dev.ps1 -Stop # 停止前后端 +.\dev.ps1 -Restart # 重启前后端 +.\dev.ps1 -Status # 查看端口状态 ``` --- @@ -402,6 +413,7 @@ docker exec -it erp-postgres psql -U erp # 连接数据库 | `message` | erp-message | | `config` | erp-config | | `server` | erp-server | +| `plugin` | erp-plugin-prototype / erp-plugin-test-sample | | `web` | Web 前端 | | `ui` | React 组件 | | `db` | 数据库迁移 | @@ -425,6 +437,8 @@ chore(docker): 添加 PostgreSQL 健康检查 |------|------| | `docs/superpowers/specs/2026-04-10-erp-platform-base-design.md` | 平台底座设计规格 | | `docs/superpowers/plans/2026-04-10-erp-platform-base-plan.md` | 平台底座实施计划 | +| `docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md` | WASM 插件系统设计规格 | +| `docs/superpowers/plans/2026-04-13-wasm-plugin-system-plan.md` | WASM 插件原型验证计划 | 所有设计决策以设计规格文档为准。实施计划按阶段拆分,每阶段开始前细化。 @@ -445,6 +459,7 @@ chore(docker): 添加 PostgreSQL 健康检查 | Phase 4 | 工作流引擎 (Workflow) | ✅ 完成 | | Phase 5 | 消息中心 (Message) | ✅ 完成 | | Phase 6 | 整合与打磨 | ✅ 完成 | +| - | WASM 插件原型 (V1-V6) | ✅ 验证通过 | ### 已实现模块 @@ -457,6 +472,8 @@ chore(docker): 添加 PostgreSQL 健康检查 | erp-workflow | 工作流引擎 (BPMN 解析/Token 驱动/任务分配) | ✅ 完成 | | erp-message | 消息中心 (CRUD/模板/订阅/通知面板) | ✅ 完成 | | erp-config | 系统配置 (字典/菜单/设置/编号规则/主题) | ✅ 完成 | +| erp-plugin-prototype | WASM 插件 Host 运行时 (Wasmtime + bindgen + Host API) | ✅ 原型验证 | +| erp-plugin-test-sample | WASM 测试插件 (Guest trait + Host API 回调) | ✅ 原型验证 | @@ -484,5 +501,6 @@ chore(docker): 添加 PostgreSQL 健康检查 - 当遇到**新增 API** → 添加 utoipa 注解,确保 OpenAPI 文档同步 - 当遇到**新增表** → 创建 SeaORM migration + Entity,包含所有标准字段 - 当遇到**新增页面** → 使用 Ant Design 组件,i18n key 引用文案 +- 当遇到**新增业务模块插件** → 参考 `wiki/wasm-plugin.md` 的插件制作完整流程,创建 cdylib crate + 实现 Guest trait + 编译为 WASM Component diff --git a/Cargo.lock b/Cargo.lock index a565879..31f2f87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -52,6 +61,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -117,6 +132,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arc-swap" version = "1.9.1" @@ -318,6 +339,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -377,6 +407,9 @@ name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytecheck" @@ -412,6 +445,84 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.52.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 1.1.4", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.52.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" +dependencies = [ + "ambient-authority", + "rand", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.1.4", + "winx", +] + [[package]] name = "cc" version = "1.2.60" @@ -419,6 +530,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -488,6 +601,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -549,7 +671,7 @@ dependencies = [ "rust-ini", "serde", "serde_json", - "toml", + "toml 0.8.23", "yaml-rust2", ] @@ -594,6 +716,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -603,6 +734,148 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046d4b584c3bb9b5eb500c8f29549bec36be11000f1ba2a927cef3d1a9875691" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b194a7870becb1490366fc0ae392ccd188065ff35f8391e77ac659db6fb977" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6a4ab44c6b371e661846b97dab687387a60ac4e2f864e2d4257284aad9e889" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b7a44150c2f471a94023482bda1902710746e4bed9f9973d60c5a94319b06d" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b06598133b1dd76758b8b95f8d6747c124124aade50cea96a3d88b962da9fa" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.16.1", + "libm", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6190e2e7bcf0a678da2f715363d34ed530fedf7a2f0ab75edaefef72a70465ff" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck 0.5.0", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f583cf203d1aa8b79560e3b01f929bdacf9070b015eec4ea9c46e22a3f83e4a0" + +[[package]] +name = "cranelift-control" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803159df35cc398ae54473c150b16d6c77e92ab2948be638488de126a3328fbc" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e417257082d88087f5bcce677525bdaa8322b88dd7f175ed1a1fd41d546c" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14db6b0e0e4994c581092df78d837be2072578f7cb2528f96a6cf895e56dee63" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec66ea5025c7317383699778282ac98741d68444f956e3b1d7b62f12b7216e67" + +[[package]] +name = "cranelift-native" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373ade56438e6232619d85678477d0a88a31b3581936e0503e61e96b546b0800" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53619d3cd5c78fd998c6d9420547af26b72e6456f94c2a8a2334cb76b42baa" + [[package]] name = "crc" version = "3.4.0" @@ -627,6 +900,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -693,6 +985,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + [[package]] name = "der" version = "0.7.10" @@ -748,6 +1049,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -783,6 +1105,18 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -813,7 +1147,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -833,7 +1167,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -852,7 +1186,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "uuid", @@ -870,7 +1204,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -878,6 +1212,28 @@ dependencies = [ "validator", ] +[[package]] +name = "erp-plugin-prototype" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "tokio", + "tracing", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "erp-plugin-test-sample" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen 0.55.0", +] + [[package]] name = "erp-server" version = "0.1.0" @@ -925,7 +1281,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -971,12 +1327,29 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.4", + "windows-sys 0.52.0", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.1.9" @@ -1010,6 +1383,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1019,6 +1398,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix 1.1.4", + "windows-sys 0.52.0", +] + [[package]] name = "funty" version = "2.0.0" @@ -1124,6 +1514,20 @@ dependencies = [ "slab", ] +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1147,6 +1551,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -1155,11 +1571,23 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "glob" version = "0.3.3" @@ -1193,7 +1621,18 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -1484,6 +1923,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1507,6 +1960,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.52.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1522,12 +1997,51 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.94" @@ -1573,6 +2087,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1613,6 +2133,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1645,6 +2177,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "macro-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1660,6 +2212,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "md-5" version = "0.10.6" @@ -1676,6 +2234,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -1812,6 +2379,18 @@ dependencies = [ "libm", ] +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +dependencies = [ + "crc32fast", + "hashbrown 0.16.1", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1981,6 +2560,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pgvector" version = "0.4.1" @@ -2029,6 +2618,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2136,6 +2737,29 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pulley-interpreter" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010dec3755eb61b2f1051ecb3611b718460b7a74c131e474de2af20a845938af" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad360c32e85ca4b083ac0e2b6856e8f11c3d5060dafa7d5dc57b370857fa3018" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "quote" version = "1.0.45" @@ -2145,6 +2769,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -2187,6 +2817,35 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redis" version = "0.27.6" @@ -2200,7 +2859,7 @@ dependencies = [ "combine", "futures", "futures-util", - "itertools", + "itertools 0.13.0", "itoa", "num-bigint", "percent-encoding", @@ -2231,6 +2890,31 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952ddbfc6f9f64d006c3efd8c9851a6ba2f2b944ba94730db255d55006e0ffda" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + [[package]] name = "regex" version = "1.12.3" @@ -2371,6 +3055,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2380,6 +3076,42 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + [[package]] name = "rustls" version = "0.23.37" @@ -2469,7 +3201,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "url", @@ -2569,7 +3301,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -2608,6 +3340,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -2672,6 +3408,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2684,6 +3429,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2767,10 +3525,20 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 2.0.18", "time", ] +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.12" @@ -2869,7 +3637,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", @@ -2957,7 +3725,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -3000,7 +3768,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -3027,7 +3795,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "url", @@ -3114,19 +3882,83 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.52.0", + "winx", +] + [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -3273,11 +4105,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_edit 0.22.27", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -3287,6 +4134,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -3304,7 +4160,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", "winnow 0.7.15", @@ -3337,6 +4193,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tower" version = "0.5.3" @@ -3505,12 +4367,24 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -3638,7 +4512,7 @@ version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -3647,7 +4521,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -3702,6 +4576,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-compose" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" +dependencies = [ + "anyhow", + "heck 0.5.0", + "im-rc", + "indexmap", + "log", + "petgraph", + "serde", + "serde_derive", + "serde_yaml", + "smallvec", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wat", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -3709,7 +4604,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" +dependencies = [ + "leb128fmt", + "wasmparser 0.246.2", ] [[package]] @@ -3720,8 +4635,20 @@ checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e4c2aa916c425dcca61a6887d3e135acdee2c6d0ed51fd61c08d41ddaf62b1" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.246.2", + "wasmparser 0.246.2", ] [[package]] @@ -3736,6 +4663,373 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmparser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags", + "hashbrown 0.16.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" +dependencies = [ + "bitflags", + "hashbrown 0.16.1", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasmtime" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce205cd643d661b5ba5ba4717e13730262e8cdbc8f2eacbc7b906d45c1a74026" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix 1.1.4", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8b78abf3677d4a0a5db82e5015b4d085ff3a1b8b472cbb8c70d4b769f019ce" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.16.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e4fd4103ba413c0da2e636f73490c6c8e446d708cbde7573703941bc3d6a448" +dependencies = [ + "base64 0.22.1", + "directories-next", + "log", + "postcard", + "rustix 1.1.4", + "serde", + "serde_derive", + "sha2", + "toml 0.9.12+spec-1.1.0", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d3d6914f34be2f9d78d8ee9f422e834dfc204e71ccce697205fae95fed87892" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.245.1", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3751b0616b914fdd87fe1bf804694a078f321b000338e6476bc48a4d6e454f21" + +[[package]] +name = "wasmtime-internal-core" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22632b187e1b0716f1b9ac57ad29013bed33175fcb19e10bb6896126f82fac67" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3ca07b3e0bb3429674b173b5800577719d600774dd81bff58f775c0aaa64ee" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools 0.14.0", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c8b2c9704eb1f33ead025ec16038277ccb63d0a14c31e99d5b765d7c36da55" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d950310d07391d34369f62c48336ebb14eacbd4d6f772bb5f349c24e838e0664" +dependencies = [ + "cc", + "object", + "rustix 1.1.4", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3606662c156962d096be3127b8b8ae8ee2f8be3f896dad29259ff01ddb64abfd" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75eef0747e52dc545b075f64fd0e0cc237ae738e641266b1970e07e2d744bc32" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b0a5dab02a8fb527f547855ecc0e05f9fdc3d5bd57b8b080349408f9a6cece" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8007342bd12ff400293a817973f7ecd6f1d9a8549a53369a9c1af357166f1f1e" +dependencies = [ + "cranelift-codegen", + "gimli", + "log", + "object", + "target-lexicon", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7900c3e3c1d6e475bc225d73b02d6d5484815f260022e6964dca9558e50dd01a" +dependencies = [ + "anyhow", + "bitflags", + "heck 0.5.0", + "indexmap", + "wit-parser 0.245.1", +] + +[[package]] +name = "wasmtime-wasi" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3e3ddcfad69e9eb025bd19bff70dad45bafe1d6eacd134c0ffdfc4c161d045" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", + "system-interface", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5dd3b9f04a851c422d05f333366722742da46bff9369ae0191f32cf83565a" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "246.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3fe8e3bf88ad96d031b4181ddbd64634b17cb0d06dfc3de589ef43591a9a62" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.246.2", +] + +[[package]] +name = "wat" +version = "1.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd7fda1199b94fff395c2d19a153f05dbe7807630316fa9673367666fd2ad8c" +dependencies = [ + "wast 246.0.2", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -3764,6 +5058,46 @@ dependencies = [ "wasite", ] +[[package]] +name = "wiggle" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1b1135efc8e5a008971897bea8d41ca56d8d501d4efb807842ae0a1c78f639" +dependencies = [ + "bitflags", + "thiserror 2.0.18", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7bc2b0d50ec8773b44fbfe1da6cb5cc44a92deaf8483233dcf0831e6db33172" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasmtime-environ", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6c7d44ea552e1fbfdcd7a2cd83f5c2d1e803d5b1a11e3462c06888b77f455f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "wiggle-generate", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3780,12 +5114,40 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f45f7172a2628c8317766e427babc0a400f9d10b1c0f0b0617c5ed5b79de6" +dependencies = [ + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -4011,13 +5373,33 @@ dependencies = [ "memchr", ] +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.52.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "wit-bindgen-rust-macro", + "wit-bindgen-rust-macro 0.51.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6870386de1813a61406d88749d5897484e2f6fe90a39408a6a94e160d8c72378" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro 0.55.0", ] [[package]] @@ -4028,7 +5410,18 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck 0.5.0", - "wit-parser", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4779c97d3b9dda56600c3404355d404f8c6567fae0c4d8dfeb92f6e9b2c4c8c3" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser 0.246.2", ] [[package]] @@ -4042,9 +5435,25 @@ dependencies = [ "indexmap", "prettyplease", "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "wasm-metadata 0.244.0", + "wit-bindgen-core 0.51.0", + "wit-component 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a89a98e0efe034f47f5cf86fa8aeb5d6d7175bade32bbba476aeba29541fed9" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata 0.246.2", + "wit-bindgen-core 0.55.0", + "wit-component 0.246.2", ] [[package]] @@ -4058,8 +5467,24 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", + "wit-bindgen-core 0.51.0", + "wit-bindgen-rust 0.51.0", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b81978b3d68d12116ae8e5ef3d2125c4cb619ea30002ed20cb7549383f6fca9" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core 0.55.0", + "wit-bindgen-rust 0.55.0", ] [[package]] @@ -4075,10 +5500,29 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "wasm-encoder 0.244.0", + "wasm-metadata 0.244.0", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-component" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1936c26cb24b93dc36bf78fb5dc35c55cd37f66ecdc2d2663a717d9fb3ee951e" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.246.2", + "wasm-metadata 0.246.2", + "wasmparser 0.246.2", + "wit-parser 0.246.2", ] [[package]] @@ -4096,7 +5540,57 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.245.1", +] + +[[package]] +name = "wit-parser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd979042b5ff288607ccf3b314145435453f20fc67173195f91062d2289b204d" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.246.2", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", ] [[package]] @@ -4239,3 +5733,31 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 0adce0c..e632cb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ members = [ "crates/erp-message", "crates/erp-config", "crates/erp-server/migration", + "crates/erp-plugin-prototype", + "crates/erp-plugin-test-sample", ] [workspace.package] diff --git a/apps/web/src/api/auth.ts b/apps/web/src/api/auth.ts index ba813f3..9d4e2bd 100644 --- a/apps/web/src/api/auth.ts +++ b/apps/web/src/api/auth.ts @@ -14,6 +14,7 @@ export interface UserInfo { avatar_url?: string; status: string; roles: RoleInfo[]; + version: number; } export interface RoleInfo { diff --git a/apps/web/src/api/orgs.ts b/apps/web/src/api/orgs.ts index 10e40c8..d70afc3 100644 --- a/apps/web/src/api/orgs.ts +++ b/apps/web/src/api/orgs.ts @@ -11,6 +11,7 @@ export interface OrganizationInfo { level: number; sort_order: number; children: OrganizationInfo[]; + version: number; } export interface CreateOrganizationRequest { @@ -24,6 +25,7 @@ export interface UpdateOrganizationRequest { name?: string; code?: string; sort_order?: number; + version: number; } // --- Department types --- @@ -38,6 +40,7 @@ export interface DepartmentInfo { path?: string; sort_order: number; children: DepartmentInfo[]; + version: number; } export interface CreateDepartmentRequest { @@ -53,6 +56,7 @@ export interface UpdateDepartmentRequest { code?: string; manager_id?: string; sort_order?: number; + version: number; } // --- Position types --- @@ -64,6 +68,7 @@ export interface PositionInfo { code?: string; level: number; sort_order: number; + version: number; } export interface CreatePositionRequest { @@ -78,6 +83,7 @@ export interface UpdatePositionRequest { code?: string; level?: number; sort_order?: number; + version: number; } // --- Organization API --- diff --git a/apps/web/src/api/roles.ts b/apps/web/src/api/roles.ts index 02a50f3..1942824 100644 --- a/apps/web/src/api/roles.ts +++ b/apps/web/src/api/roles.ts @@ -7,6 +7,7 @@ export interface RoleInfo { code: string; description?: string; is_system: boolean; + version: number; } export interface PermissionInfo { @@ -27,6 +28,7 @@ export interface CreateRoleRequest { export interface UpdateRoleRequest { name?: string; description?: string; + version: number; } export async function listRoles(page = 1, pageSize = 20) { diff --git a/apps/web/src/api/users.ts b/apps/web/src/api/users.ts index 8f1afa3..cc473c3 100644 --- a/apps/web/src/api/users.ts +++ b/apps/web/src/api/users.ts @@ -22,6 +22,7 @@ export interface UpdateUserRequest { phone?: string; display_name?: string; status?: string; + version: number; } export async function listUsers(page = 1, pageSize = 20, search = '') { diff --git a/apps/web/src/components/NotificationPanel.tsx b/apps/web/src/components/NotificationPanel.tsx index 913542c..9077e1d 100644 --- a/apps/web/src/components/NotificationPanel.tsx +++ b/apps/web/src/components/NotificationPanel.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; -import { Badge, List, Popover, Button, Empty, Typography, Space, theme } from 'antd'; -import { BellOutlined, CheckOutlined } from '@ant-design/icons'; +import { Badge, List, Popover, Button, Empty, Typography, theme } from 'antd'; +import { BellOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { useMessageStore } from '../stores/message'; diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx index c84a1fb..9744e30 100644 --- a/apps/web/src/layouts/MainLayout.tsx +++ b/apps/web/src/layouts/MainLayout.tsx @@ -82,7 +82,7 @@ const SidebarMenuItem = memo(function SidebarMenuItem({ export default function MainLayout({ children }: { children: React.ReactNode }) { const { sidebarCollapsed, toggleSidebar, theme: themeMode, setTheme } = useAppStore(); const { user, logout } = useAuthStore(); - const { token } = theme.useToken(); + theme.useToken(); const navigate = useNavigate(); const location = useLocation(); const currentPath = location.pathname || '/'; diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx index 941a456..b356b17 100644 --- a/apps/web/src/pages/Login.tsx +++ b/apps/web/src/pages/Login.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Form, Input, Button, message, Divider } from 'antd'; import { UserOutlined, LockOutlined, SafetyCertificateOutlined } from '@ant-design/icons'; diff --git a/apps/web/src/pages/Organizations.tsx b/apps/web/src/pages/Organizations.tsx index c2e25a4..0f1d8b4 100644 --- a/apps/web/src/pages/Organizations.tsx +++ b/apps/web/src/pages/Organizations.tsx @@ -10,8 +10,6 @@ import { Table, Popconfirm, message, - Typography, - Card, Empty, Tag, theme, @@ -52,7 +50,7 @@ export default function Organizations() { // --- Org tree state --- const [orgTree, setOrgTree] = useState([]); const [selectedOrg, setSelectedOrg] = useState(null); - const [loading, setLoading] = useState(false); + const [, setLoading] = useState(false); // --- Department tree state --- const [deptTree, setDeptTree] = useState([]); @@ -144,6 +142,7 @@ export default function Organizations() { name: values.name, code: values.code, sort_order: values.sort_order, + version: editOrg.version, }); message.success('组织更新成功'); } else { diff --git a/apps/web/src/pages/Roles.tsx b/apps/web/src/pages/Roles.tsx index e480f39..9de5e24 100644 --- a/apps/web/src/pages/Roles.tsx +++ b/apps/web/src/pages/Roles.tsx @@ -69,7 +69,7 @@ export default function Roles() { }) => { try { if (editRole) { - await updateRole(editRole.id, values); + await updateRole(editRole.id, { ...values, version: editRole.version }); message.success('角色更新成功'); } else { await createRole(values); diff --git a/apps/web/src/pages/Users.tsx b/apps/web/src/pages/Users.tsx index f635f0f..e4829ff 100644 --- a/apps/web/src/pages/Users.tsx +++ b/apps/web/src/pages/Users.tsx @@ -107,6 +107,7 @@ export default function Users() { display_name: values.display_name, email: values.email, phone: values.phone, + version: editUser.version, }; await updateUser(editUser.id, req); message.success('用户更新成功'); @@ -144,7 +145,9 @@ export default function Users() { const handleToggleStatus = async (id: string, status: string) => { try { - await updateUser(id, { status }); + const user = users.find(u => u.id === id); + if (!user) return; + await updateUser(id, { status, version: user.version }); message.success(status === 'disabled' ? '用户已禁用' : '用户已启用'); fetchUsers(); } catch { diff --git a/apps/web/src/pages/Workflow.tsx b/apps/web/src/pages/Workflow.tsx index 75eedb1..fe5366b 100644 --- a/apps/web/src/pages/Workflow.tsx +++ b/apps/web/src/pages/Workflow.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Tabs, theme } from 'antd'; +import { Tabs } from 'antd'; import { PartitionOutlined, FileSearchOutlined, CheckSquareOutlined, MonitorOutlined } from '@ant-design/icons'; import ProcessDefinitions from './workflow/ProcessDefinitions'; import PendingTasks from './workflow/PendingTasks'; @@ -8,8 +8,6 @@ import InstanceMonitor from './workflow/InstanceMonitor'; export default function Workflow() { const [activeKey, setActiveKey] = useState('definitions'); - const { token } = theme.useToken(); - const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)'; return (
diff --git a/apps/web/src/pages/settings/AuditLogViewer.tsx b/apps/web/src/pages/settings/AuditLogViewer.tsx index 1cf2d35..00a116b 100644 --- a/apps/web/src/pages/settings/AuditLogViewer.tsx +++ b/apps/web/src/pages/settings/AuditLogViewer.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import { Table, Select, Input, Space, Tag, message, theme } from 'antd'; +import { useState, useEffect, useCallback } from 'react'; +import { Table, Select, Input, Tag, message, theme } from 'antd'; import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; import { listAuditLogs, type AuditLogItem, type AuditLogQuery } from '../../api/auditLogs'; @@ -53,12 +53,8 @@ export default function AuditLogViewer() { setLoading(false); }, []); - const isFirstRender = useRef(true); useEffect(() => { - if (isFirstRender.current) { - isFirstRender.current = false; - fetchLogs(query); - } + fetchLogs(query); }, [query, fetchLogs]); const handleFilterChange = (field: keyof AuditLogQuery, value: string | undefined) => { diff --git a/apps/web/src/pages/settings/DictionaryManager.tsx b/apps/web/src/pages/settings/DictionaryManager.tsx index 55b7fa2..fefe149 100644 --- a/apps/web/src/pages/settings/DictionaryManager.tsx +++ b/apps/web/src/pages/settings/DictionaryManager.tsx @@ -50,7 +50,7 @@ export default function DictionaryManager() { setLoading(true); try { const result = await listDictionaries(); - setDictionaries(Array.isArray(result) ? result : result.items ?? []); + setDictionaries(Array.isArray(result) ? result : result.data ?? []); } catch { message.error('加载字典列表失败'); } diff --git a/apps/web/src/pages/settings/MenuConfig.tsx b/apps/web/src/pages/settings/MenuConfig.tsx index 42fceb5..8d4a603 100644 --- a/apps/web/src/pages/settings/MenuConfig.tsx +++ b/apps/web/src/pages/settings/MenuConfig.tsx @@ -45,29 +45,6 @@ function flattenMenuTree(tree: MenuItem[]): MenuItem[] { return result; } -/** Convert flat menu list to tree structure for Table children prop */ -function buildMenuTree(items: MenuItem[]): MenuItem[] { - const map = new Map(); - const roots: MenuItem[] = []; - - const withChildren = items.map((item) => ({ - ...item, - children: [] as MenuItem[], - })); - - withChildren.forEach((item) => map.set(item.id, item)); - - withChildren.forEach((item) => { - if (item.parent_id && map.has(item.parent_id)) { - map.get(item.parent_id)!.children!.push(item); - } else { - roots.push(item); - } - }); - - return roots; -} - /** Convert menu tree to TreeSelect data nodes */ function toTreeSelectData( items: MenuItem[], @@ -91,7 +68,7 @@ const menuTypeLabels: Record = { // --- Component --- export default function MenuConfig() { - const [menus, setMenus] = useState([]); + const [_menus, setMenus] = useState([]); const [menuTree, setMenuTree] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); diff --git a/apps/web/src/pages/settings/NumberingRules.tsx b/apps/web/src/pages/settings/NumberingRules.tsx index 2886483..7b6f4a0 100644 --- a/apps/web/src/pages/settings/NumberingRules.tsx +++ b/apps/web/src/pages/settings/NumberingRules.tsx @@ -57,7 +57,7 @@ export default function NumberingRules() { setLoading(true); try { const result = await listNumberingRules(); - setRules(Array.isArray(result) ? result : result.items ?? []); + setRules(Array.isArray(result) ? result : result.data ?? []); } catch { message.error('加载编号规则失败'); } diff --git a/apps/web/src/pages/settings/ThemeSettings.tsx b/apps/web/src/pages/settings/ThemeSettings.tsx index ebd88fb..8219f1e 100644 --- a/apps/web/src/pages/settings/ThemeSettings.tsx +++ b/apps/web/src/pages/settings/ThemeSettings.tsx @@ -3,14 +3,13 @@ import { Form, Input, Select, Button, ColorPicker, message, Typography } from 'a import { getTheme, updateTheme, - type ThemeConfig, } from '../../api/themes'; // --- Component --- export default function ThemeSettings() { const [form] = Form.useForm(); - const [loading, setLoading] = useState(false); + const [, setLoading] = useState(false); const [saving, setSaving] = useState(false); const fetchTheme = useCallback(async () => { diff --git a/apps/web/src/pages/workflow/InstanceMonitor.tsx b/apps/web/src/pages/workflow/InstanceMonitor.tsx index f03f505..4c3e7c3 100644 --- a/apps/web/src/pages/workflow/InstanceMonitor.tsx +++ b/apps/web/src/pages/workflow/InstanceMonitor.tsx @@ -86,7 +86,7 @@ export default function InstanceMonitor() { title: '确认挂起', content: '确定要挂起该流程实例吗?挂起后可通过"恢复"按钮继续执行。', okText: '确定挂起', - okType: 'warning', + okType: 'default', cancelText: '取消', onOk: async () => { try { diff --git a/apps/web/src/pages/workflow/ProcessDesigner.tsx b/apps/web/src/pages/workflow/ProcessDesigner.tsx index fc4eae6..3a7228c 100644 --- a/apps/web/src/pages/workflow/ProcessDesigner.tsx +++ b/apps/web/src/pages/workflow/ProcessDesigner.tsx @@ -162,7 +162,7 @@ export default function ProcessDesigner({ definitionId, onSave }: ProcessDesigne const flowNodes: NodeDef[] = nodes.map((n) => ({ id: n.id, type: (n.data.nodeType as NodeDef['type']) || 'UserTask', - name: n.data.name || String(n.data.label), + name: String(n.data.name || n.data.label || ''), position: { x: Math.round(n.position.x), y: Math.round(n.position.y) }, })); const flowEdges: EdgeDef[] = edges.map((e) => ({ @@ -220,7 +220,7 @@ export default function ProcessDesigner({ definitionId, onSave }: ProcessDesigne

节点属性

handleUpdateNodeName(e.target.value)} placeholder="节点名称" style={{ marginBottom: 8 }} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 7bb6587..4ffe889 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -3,9 +3,9 @@ import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react(), ...tailwindcss()], server: { - port: 5173, + port: 5174, proxy: { "/api": { target: "http://localhost:3000", @@ -22,21 +22,19 @@ export default defineConfig({ cssTarget: "chrome120", rollupOptions: { output: { - manualChunks: { - "vendor-react": ["react", "react-dom", "react-router-dom"], - "vendor-antd": ["antd", "@ant-design/icons"], - "vendor-utils": ["axios", "zustand"], + manualChunks(id) { + if (id.includes("node_modules/react-dom") || id.includes("node_modules/react/") || id.includes("node_modules/react-router-dom")) { + return "vendor-react"; + } + if (id.includes("node_modules/antd") || id.includes("node_modules/@ant-design")) { + return "vendor-antd"; + } + if (id.includes("node_modules/axios") || id.includes("node_modules/zustand")) { + return "vendor-utils"; + } }, }, }, - minify: "terser", - terserOptions: { - compress: { - drop_console: true, - drop_debugger: true, - pure_funcs: ["console.log", "console.info", "console.debug"], - }, - }, sourcemap: false, reportCompressedSize: false, chunkSizeWarningLimit: 600, diff --git a/crates/erp-auth/src/entity/mod.rs b/crates/erp-auth/src/entity/mod.rs index 0d84550..74466d5 100644 --- a/crates/erp-auth/src/entity/mod.rs +++ b/crates/erp-auth/src/entity/mod.rs @@ -1,10 +1,10 @@ +pub mod department; +pub mod organization; +pub mod permission; +pub mod position; +pub mod role; +pub mod role_permission; pub mod user; pub mod user_credential; -pub mod user_token; -pub mod role; -pub mod permission; -pub mod role_permission; pub mod user_role; -pub mod organization; -pub mod department; -pub mod position; +pub mod user_token; diff --git a/crates/erp-auth/src/handler/auth_handler.rs b/crates/erp-auth/src/handler/auth_handler.rs index e6cd76d..582357c 100644 --- a/crates/erp-auth/src/handler/auth_handler.rs +++ b/crates/erp-auth/src/handler/auth_handler.rs @@ -66,12 +66,7 @@ where refresh_ttl_secs: state.refresh_ttl_secs, }; - let resp = AuthService::refresh( - &req.refresh_token, - &state.db, - &jwt_config, - ) - .await?; + let resp = AuthService::refresh(&req.refresh_token, &state.db, &jwt_config).await?; Ok(Json(ApiResponse::ok(resp))) } diff --git a/crates/erp-auth/src/handler/org_handler.rs b/crates/erp-auth/src/handler/org_handler.rs index 65fd456..c39ecd9 100644 --- a/crates/erp-auth/src/handler/org_handler.rs +++ b/crates/erp-auth/src/handler/org_handler.rs @@ -12,10 +12,10 @@ use crate::dto::{ CreateDepartmentReq, CreateOrganizationReq, CreatePositionReq, DepartmentResp, OrganizationResp, PositionResp, UpdateDepartmentReq, UpdateOrganizationReq, UpdatePositionReq, }; -use erp_core::rbac::require_permission; use crate::service::dept_service::DeptService; use crate::service::org_service::OrgService; use crate::service::position_service::PositionService; +use erp_core::rbac::require_permission; // --- Organization handlers --- @@ -180,14 +180,7 @@ where { require_permission(&ctx, "department.update")?; - let dept = DeptService::update( - id, - ctx.tenant_id, - ctx.user_id, - &req, - &state.db, - ) - .await?; + let dept = DeptService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; Ok(Json(ApiResponse::ok(dept))) } @@ -284,14 +277,7 @@ where { require_permission(&ctx, "position.update")?; - let pos = PositionService::update( - id, - ctx.tenant_id, - ctx.user_id, - &req, - &state.db, - ) - .await?; + let pos = PositionService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; Ok(Json(ApiResponse::ok(pos))) } diff --git a/crates/erp-auth/src/handler/role_handler.rs b/crates/erp-auth/src/handler/role_handler.rs index ca7dda0..d2d8a46 100644 --- a/crates/erp-auth/src/handler/role_handler.rs +++ b/crates/erp-auth/src/handler/role_handler.rs @@ -9,9 +9,9 @@ use uuid::Uuid; use crate::auth_state::AuthState; use crate::dto::{AssignPermissionsReq, CreateRoleReq, PermissionResp, RoleResp, UpdateRoleReq}; -use erp_core::rbac::require_permission; use crate::service::permission_service::PermissionService; use crate::service::role_service::RoleService; +use erp_core::rbac::require_permission; /// GET /api/v1/roles /// diff --git a/crates/erp-auth/src/handler/user_handler.rs b/crates/erp-auth/src/handler/user_handler.rs index 6c40af3..be0b5cd 100644 --- a/crates/erp-auth/src/handler/user_handler.rs +++ b/crates/erp-auth/src/handler/user_handler.rs @@ -10,8 +10,8 @@ use uuid::Uuid; use crate::auth_state::AuthState; use crate::dto::{CreateUserReq, RoleResp, UpdateUserReq, UserResp}; -use erp_core::rbac::require_permission; use crate::service::user_service::UserService; +use erp_core::rbac::require_permission; /// Query parameters for user list endpoint. #[derive(Debug, Deserialize)] @@ -41,9 +41,13 @@ where page: params.page, page_size: params.page_size, }; - let (users, total) = - UserService::list(ctx.tenant_id, &pagination, params.search.as_deref(), &state.db) - .await?; + let (users, total) = UserService::list( + ctx.tenant_id, + &pagination, + params.search.as_deref(), + &state.db, + ) + .await?; let page = pagination.page.unwrap_or(1); let page_size = pagination.limit(); @@ -123,8 +127,7 @@ where { require_permission(&ctx, "user.update")?; - let user = - UserService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; + let user = UserService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; Ok(Json(ApiResponse::ok(user))) } @@ -181,8 +184,7 @@ where require_permission(&ctx, "user.update")?; let roles = - UserService::assign_roles(id, ctx.tenant_id, ctx.user_id, &req.role_ids, &state.db) - .await?; + UserService::assign_roles(id, ctx.tenant_id, ctx.user_id, &req.role_ids, &state.db).await?; Ok(Json(ApiResponse::ok(AssignRolesResp { roles }))) } diff --git a/crates/erp-auth/src/middleware/jwt_auth.rs b/crates/erp-auth/src/middleware/jwt_auth.rs index 13a48a1..20f2b3b 100644 --- a/crates/erp-auth/src/middleware/jwt_auth.rs +++ b/crates/erp-auth/src/middleware/jwt_auth.rs @@ -39,8 +39,8 @@ pub async fn jwt_auth_middleware_fn( .strip_prefix("Bearer ") .ok_or(AppError::Unauthorized)?; - let claims = TokenService::decode_token(token, &jwt_secret) - .map_err(|_| AppError::Unauthorized)?; + let claims = + TokenService::decode_token(token, &jwt_secret).map_err(|_| AppError::Unauthorized)?; // Verify this is an access token, not a refresh token if claims.token_type != "access" { diff --git a/crates/erp-auth/src/middleware/mod.rs b/crates/erp-auth/src/middleware/mod.rs index 2d7bd74..8dd2661 100644 --- a/crates/erp-auth/src/middleware/mod.rs +++ b/crates/erp-auth/src/middleware/mod.rs @@ -1,4 +1,4 @@ pub mod jwt_auth; -pub use jwt_auth::jwt_auth_middleware_fn; pub use erp_core::rbac::{require_any_permission, require_permission, require_role}; +pub use jwt_auth::jwt_auth_middleware_fn; diff --git a/crates/erp-auth/src/module.rs b/crates/erp-auth/src/module.rs index 6a62a16..99052b7 100644 --- a/crates/erp-auth/src/module.rs +++ b/crates/erp-auth/src/module.rs @@ -101,8 +101,7 @@ impl AuthModule { // Position routes (nested under department) .route( "/departments/{dept_id}/positions", - axum::routing::get(org_handler::list_positions) - .post(org_handler::create_position), + axum::routing::get(org_handler::list_positions).post(org_handler::create_position), ) .route( "/positions/{id}", diff --git a/crates/erp-auth/src/service/auth_service.rs b/crates/erp-auth/src/service/auth_service.rs index f2a51c5..0ddc266 100644 --- a/crates/erp-auth/src/service/auth_service.rs +++ b/crates/erp-auth/src/service/auth_service.rs @@ -79,10 +79,8 @@ impl AuthService { } // 5. Get roles and permissions - let roles: Vec = - TokenService::get_user_roles(user_model.id, tenant_id, db).await?; - let permissions = - TokenService::get_user_permissions(user_model.id, tenant_id, db).await?; + let roles: Vec = TokenService::get_user_roles(user_model.id, tenant_id, db).await?; + let permissions = TokenService::get_user_permissions(user_model.id, tenant_id, db).await?; // 6. Sign tokens let access_token = TokenService::sign_access_token( @@ -154,10 +152,8 @@ impl AuthService { TokenService::revoke_token(old_token_id, db).await?; // Fetch fresh roles and permissions - let roles: Vec = - TokenService::get_user_roles(claims.sub, claims.tid, db).await?; - let permissions = - TokenService::get_user_permissions(claims.sub, claims.tid, db).await?; + let roles: Vec = TokenService::get_user_roles(claims.sub, claims.tid, db).await?; + let permissions = TokenService::get_user_permissions(claims.sub, claims.tid, db).await?; // Sign new token pair let access_token = TokenService::sign_access_token( diff --git a/crates/erp-auth/src/service/dept_service.rs b/crates/erp-auth/src/service/dept_service.rs index b5aefbc..cb32cc3 100644 --- a/crates/erp-auth/src/service/dept_service.rs +++ b/crates/erp-auth/src/service/dept_service.rs @@ -85,7 +85,9 @@ impl DeptService { .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? - .filter(|d| d.tenant_id == tenant_id && d.org_id == org_id && d.deleted_at.is_none()) + .filter(|d| { + d.tenant_id == tenant_id && d.org_id == org_id && d.deleted_at.is_none() + }) .ok_or_else(|| AuthError::Validation("父级部门不存在".to_string()))?; let parent_path = parent.path.clone().unwrap_or_default(); @@ -120,15 +122,25 @@ impl DeptService { .await .map_err(|e| AuthError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "department.created", - tenant_id, - serde_json::json!({ "dept_id": id, "org_id": org_id, "name": req.name }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "department.created", + tenant_id, + serde_json::json!({ "dept_id": id, "org_id": org_id, "name": req.name }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "department.create", "department") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "department.create", + "department", + ) + .with_resource_id(id), db, ) .await; @@ -205,8 +217,13 @@ impl DeptService { .map_err(|e| AuthError::Validation(e.to_string()))?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "department.update", "department") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "department.update", + "department", + ) + .with_resource_id(id), db, ) .await; @@ -267,15 +284,25 @@ impl DeptService { .await .map_err(|e| AuthError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "department.deleted", - tenant_id, - serde_json::json!({ "dept_id": id }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "department.deleted", + tenant_id, + serde_json::json!({ "dept_id": id }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "department.delete", "department") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "department.delete", + "department", + ) + .with_resource_id(id), db, ) .await; diff --git a/crates/erp-auth/src/service/org_service.rs b/crates/erp-auth/src/service/org_service.rs index 8ef0276..c95733d 100644 --- a/crates/erp-auth/src/service/org_service.rs +++ b/crates/erp-auth/src/service/org_service.rs @@ -106,15 +106,25 @@ impl OrgService { .await .map_err(|e| AuthError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "organization.created", - tenant_id, - serde_json::json!({ "org_id": id, "name": req.name }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "organization.created", + tenant_id, + serde_json::json!({ "org_id": id, "name": req.name }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "organization.create", "organization") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "organization.create", + "organization", + ) + .with_resource_id(id), db, ) .await; @@ -187,8 +197,13 @@ impl OrgService { .map_err(|e| AuthError::Validation(e.to_string()))?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "organization.update", "organization") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "organization.update", + "organization", + ) + .with_resource_id(id), db, ) .await; @@ -246,15 +261,25 @@ impl OrgService { .await .map_err(|e| AuthError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "organization.deleted", - tenant_id, - serde_json::json!({ "org_id": id }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "organization.deleted", + tenant_id, + serde_json::json!({ "org_id": id }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "organization.delete", "organization") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "organization.delete", + "organization", + ) + .with_resource_id(id), db, ) .await; diff --git a/crates/erp-auth/src/service/password.rs b/crates/erp-auth/src/service/password.rs index 41fc1e5..76c0a74 100644 --- a/crates/erp-auth/src/service/password.rs +++ b/crates/erp-auth/src/service/password.rs @@ -1,6 +1,6 @@ use argon2::{ - password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, + password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng}, }; use crate::error::{AuthError, AuthResult}; diff --git a/crates/erp-auth/src/service/position_service.rs b/crates/erp-auth/src/service/position_service.rs index fa6a8bd..5b565ca 100644 --- a/crates/erp-auth/src/service/position_service.rs +++ b/crates/erp-auth/src/service/position_service.rs @@ -105,11 +105,16 @@ impl PositionService { .await .map_err(|e| AuthError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "position.created", - tenant_id, - serde_json::json!({ "position_id": id, "dept_id": dept_id, "name": req.name }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "position.created", + tenant_id, + serde_json::json!({ "position_id": id, "dept_id": dept_id, "name": req.name }), + ), + db, + ) + .await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "position.create", "position") @@ -230,11 +235,16 @@ impl PositionService { .await .map_err(|e| AuthError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "position.deleted", - tenant_id, - serde_json::json!({ "position_id": id }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "position.deleted", + tenant_id, + serde_json::json!({ "position_id": id }), + ), + db, + ) + .await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "position.delete", "position") diff --git a/crates/erp-auth/src/service/role_service.rs b/crates/erp-auth/src/service/role_service.rs index e4e0250..998a7b0 100644 --- a/crates/erp-auth/src/service/role_service.rs +++ b/crates/erp-auth/src/service/role_service.rs @@ -1,7 +1,5 @@ use chrono::Utc; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, -}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::{PermissionResp, RoleResp}; @@ -127,15 +125,19 @@ impl RoleService { .await .map_err(|e| AuthError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "role.created", - tenant_id, - serde_json::json!({ "role_id": id, "code": code }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "role.created", + tenant_id, + serde_json::json!({ "role_id": id, "code": code }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "role.create", "role") - .with_resource_id(id), + AuditLog::new(tenant_id, Some(operator_id), "role.create", "role").with_resource_id(id), db, ) .await; @@ -190,8 +192,7 @@ impl RoleService { .map_err(|e| AuthError::Validation(e.to_string()))?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "role.update", "role") - .with_resource_id(id), + AuditLog::new(tenant_id, Some(operator_id), "role.update", "role").with_resource_id(id), db, ) .await; @@ -238,15 +239,19 @@ impl RoleService { .await .map_err(|e| AuthError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "role.deleted", - tenant_id, - serde_json::json!({ "role_id": id }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "role.deleted", + tenant_id, + serde_json::json!({ "role_id": id }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "role.delete", "role") - .with_resource_id(id), + AuditLog::new(tenant_id, Some(operator_id), "role.delete", "role").with_resource_id(id), db, ) .await; diff --git a/crates/erp-auth/src/service/seed.rs b/crates/erp-auth/src/service/seed.rs index bcd03ea..489d247 100644 --- a/crates/erp-auth/src/service/seed.rs +++ b/crates/erp-auth/src/service/seed.rs @@ -22,52 +22,286 @@ const DEFAULT_PERMISSIONS: &[(&str, &str, &str, &str, &str)] = &[ ("role.read", "查看角色详情", "role", "read", "查看角色信息"), ("role.update", "编辑角色", "role", "update", "编辑角色"), ("role.delete", "删除角色", "role", "delete", "删除角色"), - ("permission.list", "查看权限", "permission", "list", "查看权限列表"), - ("organization.list", "查看组织列表", "organization", "list", "查看组织列表"), - ("organization.create", "创建组织", "organization", "create", "创建组织"), - ("organization.update", "编辑组织", "organization", "update", "编辑组织"), - ("organization.delete", "删除组织", "organization", "delete", "删除组织"), - ("department.list", "查看部门列表", "department", "list", "查看部门列表"), - ("department.create", "创建部门", "department", "create", "创建部门"), - ("department.update", "编辑部门", "department", "update", "编辑部门"), - ("department.delete", "删除部门", "department", "delete", "删除部门"), - ("position.list", "查看岗位列表", "position", "list", "查看岗位列表"), - ("position.create", "创建岗位", "position", "create", "创建岗位"), - ("position.update", "编辑岗位", "position", "update", "编辑岗位"), - ("position.delete", "删除岗位", "position", "delete", "删除岗位"), + ( + "permission.list", + "查看权限", + "permission", + "list", + "查看权限列表", + ), + ( + "organization.list", + "查看组织列表", + "organization", + "list", + "查看组织列表", + ), + ( + "organization.create", + "创建组织", + "organization", + "create", + "创建组织", + ), + ( + "organization.update", + "编辑组织", + "organization", + "update", + "编辑组织", + ), + ( + "organization.delete", + "删除组织", + "organization", + "delete", + "删除组织", + ), + ( + "department.list", + "查看部门列表", + "department", + "list", + "查看部门列表", + ), + ( + "department.create", + "创建部门", + "department", + "create", + "创建部门", + ), + ( + "department.update", + "编辑部门", + "department", + "update", + "编辑部门", + ), + ( + "department.delete", + "删除部门", + "department", + "delete", + "删除部门", + ), + ( + "position.list", + "查看岗位列表", + "position", + "list", + "查看岗位列表", + ), + ( + "position.create", + "创建岗位", + "position", + "create", + "创建岗位", + ), + ( + "position.update", + "编辑岗位", + "position", + "update", + "编辑岗位", + ), + ( + "position.delete", + "删除岗位", + "position", + "delete", + "删除岗位", + ), // === Config module === - ("dictionary.list", "查看字典", "dictionary", "list", "查看数据字典"), - ("dictionary.create", "创建字典", "dictionary", "create", "创建数据字典"), - ("dictionary.update", "编辑字典", "dictionary", "update", "编辑数据字典"), - ("dictionary.delete", "删除字典", "dictionary", "delete", "删除数据字典"), + ( + "dictionary.list", + "查看字典", + "dictionary", + "list", + "查看数据字典", + ), + ( + "dictionary.create", + "创建字典", + "dictionary", + "create", + "创建数据字典", + ), + ( + "dictionary.update", + "编辑字典", + "dictionary", + "update", + "编辑数据字典", + ), + ( + "dictionary.delete", + "删除字典", + "dictionary", + "delete", + "删除数据字典", + ), ("menu.list", "查看菜单", "menu", "list", "查看菜单配置"), ("menu.update", "编辑菜单", "menu", "update", "编辑菜单配置"), - ("setting.read", "查看配置", "setting", "read", "查看系统参数"), - ("setting.update", "编辑配置", "setting", "update", "编辑系统参数"), - ("setting.delete", "删除配置", "setting", "delete", "删除系统参数"), - ("numbering.list", "查看编号规则", "numbering", "list", "查看编号规则"), - ("numbering.create", "创建编号规则", "numbering", "create", "创建编号规则"), - ("numbering.update", "编辑编号规则", "numbering", "update", "编辑编号规则"), - ("numbering.delete", "删除编号规则", "numbering", "delete", "删除编号规则"), - ("numbering.generate", "生成编号", "numbering", "generate", "生成文档编号"), + ( + "setting.read", + "查看配置", + "setting", + "read", + "查看系统参数", + ), + ( + "setting.update", + "编辑配置", + "setting", + "update", + "编辑系统参数", + ), + ( + "setting.delete", + "删除配置", + "setting", + "delete", + "删除系统参数", + ), + ( + "numbering.list", + "查看编号规则", + "numbering", + "list", + "查看编号规则", + ), + ( + "numbering.create", + "创建编号规则", + "numbering", + "create", + "创建编号规则", + ), + ( + "numbering.update", + "编辑编号规则", + "numbering", + "update", + "编辑编号规则", + ), + ( + "numbering.delete", + "删除编号规则", + "numbering", + "delete", + "删除编号规则", + ), + ( + "numbering.generate", + "生成编号", + "numbering", + "generate", + "生成文档编号", + ), ("theme.read", "查看主题", "theme", "read", "查看主题设置"), - ("theme.update", "编辑主题", "theme", "update", "编辑主题设置"), - ("language.list", "查看语言", "language", "list", "查看语言配置"), - ("language.update", "编辑语言", "language", "update", "编辑语言设置"), + ( + "theme.update", + "编辑主题", + "theme", + "update", + "编辑主题设置", + ), + ( + "language.list", + "查看语言", + "language", + "list", + "查看语言配置", + ), + ( + "language.update", + "编辑语言", + "language", + "update", + "编辑语言设置", + ), // === Workflow module === - ("workflow.create", "创建流程", "workflow", "create", "创建流程定义"), - ("workflow.list", "查看流程", "workflow", "list", "查看流程列表"), - ("workflow.read", "查看流程详情", "workflow", "read", "查看流程定义详情"), - ("workflow.update", "编辑流程", "workflow", "update", "编辑流程定义"), - ("workflow.publish", "发布流程", "workflow", "publish", "发布流程定义"), - ("workflow.start", "发起流程", "workflow", "start", "发起流程实例"), - ("workflow.approve", "审批任务", "workflow", "approve", "审批流程任务"), - ("workflow.delegate", "委派任务", "workflow", "delegate", "委派流程任务"), + ( + "workflow.create", + "创建流程", + "workflow", + "create", + "创建流程定义", + ), + ( + "workflow.list", + "查看流程", + "workflow", + "list", + "查看流程列表", + ), + ( + "workflow.read", + "查看流程详情", + "workflow", + "read", + "查看流程定义详情", + ), + ( + "workflow.update", + "编辑流程", + "workflow", + "update", + "编辑流程定义", + ), + ( + "workflow.publish", + "发布流程", + "workflow", + "publish", + "发布流程定义", + ), + ( + "workflow.start", + "发起流程", + "workflow", + "start", + "发起流程实例", + ), + ( + "workflow.approve", + "审批任务", + "workflow", + "approve", + "审批流程任务", + ), + ( + "workflow.delegate", + "委派任务", + "workflow", + "delegate", + "委派流程任务", + ), // === Message module === - ("message.list", "查看消息", "message", "list", "查看消息列表"), + ( + "message.list", + "查看消息", + "message", + "list", + "查看消息列表", + ), ("message.send", "发送消息", "message", "send", "发送新消息"), - ("message.template.list", "查看消息模板", "message.template", "list", "查看消息模板列表"), - ("message.template.create", "创建消息模板", "message.template", "create", "创建消息模板"), + ( + "message.template.list", + "查看消息模板", + "message.template", + "list", + "查看消息模板列表", + ), + ( + "message.template.create", + "创建消息模板", + "message.template", + "create", + "创建消息模板", + ), ]; /// Indices of read-only (list/read) permissions within DEFAULT_PERMISSIONS. @@ -128,7 +362,9 @@ pub async fn seed_tenant_auth( deleted_at: Set(None), version: Set(1), }; - perm.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + perm.insert(db) + .await + .map_err(|e| AuthError::Validation(e.to_string()))?; } // 2. Create "admin" role with all permissions @@ -147,7 +383,10 @@ pub async fn seed_tenant_auth( deleted_at: Set(None), version: Set(1), }; - admin_role.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + admin_role + .insert(db) + .await + .map_err(|e| AuthError::Validation(e.to_string()))?; // Assign all permissions to admin role for perm_id in &perm_ids { @@ -162,7 +401,9 @@ pub async fn seed_tenant_auth( deleted_at: Set(None), version: Set(1), }; - rp.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + rp.insert(db) + .await + .map_err(|e| AuthError::Validation(e.to_string()))?; } // 3. Create "viewer" role with read-only permissions @@ -181,7 +422,10 @@ pub async fn seed_tenant_auth( deleted_at: Set(None), version: Set(1), }; - viewer_role.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + viewer_role + .insert(db) + .await + .map_err(|e| AuthError::Validation(e.to_string()))?; // Assign read permissions to viewer role for idx in READ_PERM_INDICES { @@ -197,7 +441,9 @@ pub async fn seed_tenant_auth( deleted_at: Set(None), version: Set(1), }; - rp.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + rp.insert(db) + .await + .map_err(|e| AuthError::Validation(e.to_string()))?; } } @@ -222,7 +468,10 @@ pub async fn seed_tenant_auth( deleted_at: Set(None), version: Set(1), }; - admin_user.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + admin_user + .insert(db) + .await + .map_err(|e| AuthError::Validation(e.to_string()))?; // Create password credential for admin user let cred = user_credential::ActiveModel { @@ -239,7 +488,9 @@ pub async fn seed_tenant_auth( deleted_at: Set(None), version: Set(1), }; - cred.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + cred.insert(db) + .await + .map_err(|e| AuthError::Validation(e.to_string()))?; // 5. Assign admin role to admin user let user_role_assignment = user_role::ActiveModel { @@ -253,7 +504,10 @@ pub async fn seed_tenant_auth( deleted_at: Set(None), version: Set(1), }; - user_role_assignment.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + user_role_assignment + .insert(db) + .await + .map_err(|e| AuthError::Validation(e.to_string()))?; tracing::info!( tenant_id = %tenant_id, diff --git a/crates/erp-auth/src/service/user_service.rs b/crates/erp-auth/src/service/user_service.rs index 3eaead6..184386c 100644 --- a/crates/erp-auth/src/service/user_service.rs +++ b/crates/erp-auth/src/service/user_service.rs @@ -84,17 +84,21 @@ impl UserService { deleted_at: Set(None), version: Set(1), }; - cred - .insert(db) + cred.insert(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; // Publish domain event - event_bus.publish(erp_core::events::DomainEvent::new( - "user.created", - tenant_id, - serde_json::json!({ "user_id": user_id, "username": req.username }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "user.created", + tenant_id, + serde_json::json!({ "user_id": user_id, "username": req.username }), + ), + db, + ) + .await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "user.create", "user") @@ -147,11 +151,11 @@ impl UserService { .filter(user::Column::TenantId.eq(tenant_id)) .filter(user::Column::DeletedAt.is_null()); - if let Some(term) = search && !term.is_empty() { + if let Some(term) = search + && !term.is_empty() + { use sea_orm::sea_query::Expr; - query = query.filter( - Expr::col(user::Column::Username).like(format!("%{}%", term)), - ); + query = query.filter(Expr::col(user::Column::Username).like(format!("%{}%", term))); } let paginator = query.paginate(db, pagination.limit()); @@ -225,8 +229,7 @@ impl UserService { .map_err(|e| AuthError::Validation(e.to_string()))?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "user.update", "user") - .with_resource_id(id), + AuditLog::new(tenant_id, Some(operator_id), "user.update", "user").with_resource_id(id), db, ) .await; @@ -261,15 +264,19 @@ impl UserService { .await .map_err(|e| AuthError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "user.deleted", - tenant_id, - serde_json::json!({ "user_id": id }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "user.deleted", + tenant_id, + serde_json::json!({ "user_id": id }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "user.delete", "user") - .with_resource_id(id), + AuditLog::new(tenant_id, Some(operator_id), "user.delete", "user").with_resource_id(id), db, ) .await; @@ -302,7 +309,9 @@ impl UserService { .await .map_err(|e| AuthError::Validation(e.to_string()))?; if found.len() != role_ids.len() { - return Err(AuthError::Validation("部分角色不存在或不属于当前租户".to_string())); + return Err(AuthError::Validation( + "部分角色不存在或不属于当前租户".to_string(), + )); } } @@ -328,7 +337,10 @@ impl UserService { deleted_at: Set(None), version: Set(1), }; - assignment.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + assignment + .insert(db) + .await + .map_err(|e| AuthError::Validation(e.to_string()))?; } audit_service::record( diff --git a/crates/erp-config/src/entity/mod.rs b/crates/erp-config/src/entity/mod.rs index 11fa4fd..909af4a 100644 --- a/crates/erp-config/src/entity/mod.rs +++ b/crates/erp-config/src/entity/mod.rs @@ -2,5 +2,5 @@ pub mod dictionary; pub mod dictionary_item; pub mod menu; pub mod menu_role; -pub mod setting; pub mod numbering_rule; +pub mod setting; diff --git a/crates/erp-config/src/error.rs b/crates/erp-config/src/error.rs index 9176e73..3e05fa7 100644 --- a/crates/erp-config/src/error.rs +++ b/crates/erp-config/src/error.rs @@ -22,9 +22,7 @@ pub enum ConfigError { impl From> for ConfigError { fn from(err: sea_orm::TransactionError) -> Self { match err { - sea_orm::TransactionError::Connection(err) => { - ConfigError::Validation(err.to_string()) - } + sea_orm::TransactionError::Connection(err) => ConfigError::Validation(err.to_string()), sea_orm::TransactionError::Transaction(inner) => inner, } } diff --git a/crates/erp-config/src/handler/dictionary_handler.rs b/crates/erp-config/src/handler/dictionary_handler.rs index 1ace124..9749613 100644 --- a/crates/erp-config/src/handler/dictionary_handler.rs +++ b/crates/erp-config/src/handler/dictionary_handler.rs @@ -97,14 +97,8 @@ where { require_permission(&ctx, "dictionary.update")?; - let dictionary = DictionaryService::update( - id, - ctx.tenant_id, - ctx.user_id, - &req, - &state.db, - ) - .await?; + let dictionary = + DictionaryService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; Ok(Json(ApiResponse::ok(dictionary))) } @@ -185,14 +179,8 @@ where req.validate() .map_err(|e| AppError::Validation(e.to_string()))?; - let item = DictionaryService::add_item( - dict_id, - ctx.tenant_id, - ctx.user_id, - &req, - &state.db, - ) - .await?; + let item = + DictionaryService::add_item(dict_id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; Ok(Json(ApiResponse::ok(item))) } @@ -214,20 +202,12 @@ where require_permission(&ctx, "dictionary.update")?; // 验证 item_id 属于 dict_id - let item = DictionaryService::update_item( - item_id, - ctx.tenant_id, - ctx.user_id, - &req, - &state.db, - ) - .await?; + let item = DictionaryService::update_item(item_id, ctx.tenant_id, ctx.user_id, &req, &state.db) + .await?; // 确保 item 属于指定的 dictionary if item.dictionary_id != dict_id { - return Err(AppError::Validation( - "字典项不属于指定的字典".to_string(), - )); + return Err(AppError::Validation("字典项不属于指定的字典".to_string())); } Ok(Json(ApiResponse::ok(item))) diff --git a/crates/erp-config/src/handler/language_handler.rs b/crates/erp-config/src/handler/language_handler.rs index 8af8ed4..0dad963 100644 --- a/crates/erp-config/src/handler/language_handler.rs +++ b/crates/erp-config/src/handler/language_handler.rs @@ -30,14 +30,9 @@ where page_size: Some(100), }; - let (settings, _total) = SettingService::list_by_scope( - "platform", - &None, - ctx.tenant_id, - &pagination, - &state.db, - ) - .await?; + let (settings, _total) = + SettingService::list_by_scope("platform", &None, ctx.tenant_id, &pagination, &state.db) + .await?; let languages: Vec = settings .into_iter() @@ -83,7 +78,7 @@ where SettingService::set( SetSettingParams { - key, + key: key.clone(), scope: "platform".to_string(), scope_id: None, value, @@ -96,9 +91,20 @@ where ) .await?; + // 从返回的 SettingResp 中读取实际值 + let updated = SettingService::get(&key, "platform", &None, ctx.tenant_id, &state.db).await?; + + // 尝试从 value 中提取 name,否则用 code 作为默认名称 + let name = updated + .setting_value + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or(&code) + .to_string(); + Ok(JsonResponse(ApiResponse::ok(LanguageResp { code, - name: String::new(), + name, is_active: req.is_active, }))) } diff --git a/crates/erp-config/src/handler/menu_handler.rs b/crates/erp-config/src/handler/menu_handler.rs index 3c58322..d7c07da 100644 --- a/crates/erp-config/src/handler/menu_handler.rs +++ b/crates/erp-config/src/handler/menu_handler.rs @@ -142,8 +142,7 @@ where role_ids: item.role_ids.clone(), version, }; - MenuService::update(id, ctx.tenant_id, ctx.user_id, &update_req, &state.db) - .await?; + MenuService::update(id, ctx.tenant_id, ctx.user_id, &update_req, &state.db).await?; } None => { let create_req = CreateMenuReq { diff --git a/crates/erp-config/src/handler/numbering_handler.rs b/crates/erp-config/src/handler/numbering_handler.rs index 684083e..3b6ec79 100644 --- a/crates/erp-config/src/handler/numbering_handler.rs +++ b/crates/erp-config/src/handler/numbering_handler.rs @@ -91,8 +91,7 @@ where { require_permission(&ctx, "numbering.update")?; - let rule = - NumberingService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; + let rule = NumberingService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; Ok(Json(ApiResponse::ok(rule))) } diff --git a/crates/erp-config/src/handler/theme_handler.rs b/crates/erp-config/src/handler/theme_handler.rs index 06bb2e4..6f8732e 100644 --- a/crates/erp-config/src/handler/theme_handler.rs +++ b/crates/erp-config/src/handler/theme_handler.rs @@ -25,8 +25,7 @@ where { require_permission(&ctx, "theme.read")?; - let setting = - SettingService::get("theme", "tenant", &None, ctx.tenant_id, &state.db).await?; + let setting = SettingService::get("theme", "tenant", &None, ctx.tenant_id, &state.db).await?; let theme: ThemeResp = serde_json::from_value(setting.setting_value) .map_err(|e| AppError::Validation(format!("主题配置解析失败: {e}")))?; diff --git a/crates/erp-config/src/module.rs b/crates/erp-config/src/module.rs index ece3e57..eec58a6 100644 --- a/crates/erp-config/src/module.rs +++ b/crates/erp-config/src/module.rs @@ -50,8 +50,7 @@ impl ConfigModule { ) .route( "/config/dictionaries/{dict_id}/items/{item_id}", - put(dictionary_handler::update_item) - .delete(dictionary_handler::delete_item), + put(dictionary_handler::update_item).delete(dictionary_handler::delete_item), ) // Menu routes .route( @@ -62,8 +61,7 @@ impl ConfigModule { ) .route( "/config/menus/{id}", - put(menu_handler::update_menu) - .delete(menu_handler::delete_menu), + put(menu_handler::update_menu).delete(menu_handler::delete_menu), ) // Setting routes .route( @@ -93,10 +91,7 @@ impl ConfigModule { get(theme_handler::get_theme).put(theme_handler::update_theme), ) // Language routes - .route( - "/config/languages", - get(language_handler::list_languages), - ) + .route("/config/languages", get(language_handler::list_languages)) .route( "/config/languages/{code}", put(language_handler::update_language), diff --git a/crates/erp-config/src/service/dictionary_service.rs b/crates/erp-config/src/service/dictionary_service.rs index 1e299f8..9f7dbff 100644 --- a/crates/erp-config/src/service/dictionary_service.rs +++ b/crates/erp-config/src/service/dictionary_service.rs @@ -1,7 +1,5 @@ use chrono::Utc; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, -}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::{DictionaryItemResp, DictionaryResp}; @@ -133,15 +131,25 @@ impl DictionaryService { .await .map_err(|e| ConfigError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "dictionary.created", - tenant_id, - serde_json::json!({ "dictionary_id": id, "code": code }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "dictionary.created", + tenant_id, + serde_json::json!({ "dictionary_id": id, "code": code }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "dictionary.create", "dictionary") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "dictionary.create", + "dictionary", + ) + .with_resource_id(id), db, ) .await; @@ -198,8 +206,13 @@ impl DictionaryService { let items = Self::fetch_items(updated.id, tenant_id, db).await?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "dictionary.update", "dictionary") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "dictionary.update", + "dictionary", + ) + .with_resource_id(id), db, ) .await; @@ -244,15 +257,25 @@ impl DictionaryService { .await .map_err(|e| ConfigError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "dictionary.deleted", - tenant_id, - serde_json::json!({ "dictionary_id": id }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "dictionary.deleted", + tenant_id, + serde_json::json!({ "dictionary_id": id }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "dictionary.delete", "dictionary") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "dictionary.delete", + "dictionary", + ) + .with_resource_id(id), db, ) .await; @@ -315,8 +338,13 @@ impl DictionaryService { .map_err(|e| ConfigError::Validation(e.to_string()))?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "dictionary_item.create", "dictionary_item") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "dictionary_item.create", + "dictionary_item", + ) + .with_resource_id(id), db, ) .await; @@ -376,8 +404,13 @@ impl DictionaryService { .map_err(|e| ConfigError::Validation(e.to_string()))?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "dictionary_item.update", "dictionary_item") - .with_resource_id(item_id), + AuditLog::new( + tenant_id, + Some(operator_id), + "dictionary_item.update", + "dictionary_item", + ) + .with_resource_id(item_id), db, ) .await; @@ -423,8 +456,13 @@ impl DictionaryService { .map_err(|e| ConfigError::Validation(e.to_string()))?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "dictionary_item.delete", "dictionary_item") - .with_resource_id(item_id), + AuditLog::new( + tenant_id, + Some(operator_id), + "dictionary_item.delete", + "dictionary_item", + ) + .with_resource_id(item_id), db, ) .await; diff --git a/crates/erp-config/src/service/menu_service.rs b/crates/erp-config/src/service/menu_service.rs index 3399e12..c1e2b5a 100644 --- a/crates/erp-config/src/service/menu_service.rs +++ b/crates/erp-config/src/service/menu_service.rs @@ -1,9 +1,7 @@ use std::collections::HashMap; use chrono::Utc; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set, -}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set}; use uuid::Uuid; use crate::dto::{CreateMenuReq, MenuResp}; @@ -58,19 +56,13 @@ impl MenuService { // 3. 按 parent_id 分组构建 HashMap let filtered: Vec<&menu::Model> = match &visible_menu_ids { - Some(ids) => all_menus - .iter() - .filter(|m| ids.contains(&m.id)) - .collect(), + Some(ids) => all_menus.iter().filter(|m| ids.contains(&m.id)).collect(), None => all_menus.iter().collect(), }; let mut children_map: HashMap, Vec<&menu::Model>> = HashMap::new(); for m in &filtered { - children_map - .entry(m.parent_id) - .or_default() - .push(*m); + children_map.entry(m.parent_id).or_default().push(*m); } // 4. 递归构建树形结构(从 parent_id == None 的根节点开始) @@ -152,15 +144,19 @@ impl MenuService { Self::assign_roles(id, role_ids, tenant_id, operator_id, db).await?; } - event_bus.publish(erp_core::events::DomainEvent::new( - "menu.created", - tenant_id, - serde_json::json!({ "menu_id": id, "title": req.title }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "menu.created", + tenant_id, + serde_json::json!({ "menu_id": id, "title": req.title }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "menu.create", "menu") - .with_resource_id(id), + AuditLog::new(tenant_id, Some(operator_id), "menu.create", "menu").with_resource_id(id), db, ) .await; @@ -235,8 +231,7 @@ impl MenuService { } audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "menu.update", "menu") - .with_resource_id(id), + AuditLog::new(tenant_id, Some(operator_id), "menu.update", "menu").with_resource_id(id), db, ) .await; @@ -285,15 +280,19 @@ impl MenuService { .await .map_err(|e| ConfigError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "menu.deleted", - tenant_id, - serde_json::json!({ "menu_id": id }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "menu.deleted", + tenant_id, + serde_json::json!({ "menu_id": id }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "menu.delete", "menu") - .with_resource_id(id), + AuditLog::new(tenant_id, Some(operator_id), "menu.delete", "menu").with_resource_id(id), db, ) .await; @@ -370,10 +369,7 @@ impl MenuService { nodes .iter() .map(|m| { - let children = children_map - .get(&Some(m.id)) - .cloned() - .unwrap_or_default(); + let children = children_map.get(&Some(m.id)).cloned().unwrap_or_default(); MenuResp { id: m.id, parent_id: m.parent_id, diff --git a/crates/erp-config/src/service/numbering_service.rs b/crates/erp-config/src/service/numbering_service.rs index 31d67ee..1a61475 100644 --- a/crates/erp-config/src/service/numbering_service.rs +++ b/crates/erp-config/src/service/numbering_service.rs @@ -1,7 +1,7 @@ use chrono::{Datelike, NaiveDate, Utc}; use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, - Statement, ConnectionTrait, DatabaseBackend, TransactionTrait, + ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseBackend, EntityTrait, PaginatorTrait, + QueryFilter, Set, Statement, TransactionTrait, }; use uuid::Uuid; @@ -41,10 +41,7 @@ impl NumberingService { .await .map_err(|e| ConfigError::Validation(e.to_string()))?; - let resps: Vec = models - .iter() - .map(Self::model_to_resp) - .collect(); + let resps: Vec = models.iter().map(Self::model_to_resp).collect(); Ok((resps, total)) } @@ -89,7 +86,10 @@ impl NumberingService { seq_start: Set(seq_start), seq_current: Set(seq_start as i64), separator: Set(req.separator.clone().unwrap_or_else(|| "-".to_string())), - reset_cycle: Set(req.reset_cycle.clone().unwrap_or_else(|| "never".to_string())), + reset_cycle: Set(req + .reset_cycle + .clone() + .unwrap_or_else(|| "never".to_string())), last_reset_date: Set(Some(Utc::now().date_naive())), created_at: Set(now), updated_at: Set(now), @@ -103,15 +103,25 @@ impl NumberingService { .await .map_err(|e| ConfigError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "numbering_rule.created", - tenant_id, - serde_json::json!({ "rule_id": id, "code": req.code }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "numbering_rule.created", + tenant_id, + serde_json::json!({ "rule_id": id, "code": req.code }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "numbering_rule.create", "numbering_rule") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "numbering_rule.create", + "numbering_rule", + ) + .with_resource_id(id), db, ) .await; @@ -126,7 +136,10 @@ impl NumberingService { seq_start, seq_current: seq_start as i64, separator: req.separator.clone().unwrap_or_else(|| "-".to_string()), - reset_cycle: req.reset_cycle.clone().unwrap_or_else(|| "never".to_string()), + reset_cycle: req + .reset_cycle + .clone() + .unwrap_or_else(|| "never".to_string()), last_reset_date: Some(Utc::now().date_naive().to_string()), version: 1, }) @@ -181,8 +194,13 @@ impl NumberingService { .map_err(|e| ConfigError::Validation(e.to_string()))?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "numbering_rule.update", "numbering_rule") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "numbering_rule.update", + "numbering_rule", + ) + .with_resource_id(id), db, ) .await; @@ -219,15 +237,25 @@ impl NumberingService { .await .map_err(|e| ConfigError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "numbering_rule.deleted", - tenant_id, - serde_json::json!({ "rule_id": id }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "numbering_rule.deleted", + tenant_id, + serde_json::json!({ "rule_id": id }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "numbering_rule.delete", "numbering_rule") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "numbering_rule.delete", + "numbering_rule", + ) + .with_resource_id(id), db, ) .await; diff --git a/crates/erp-config/src/service/setting_service.rs b/crates/erp-config/src/service/setting_service.rs index f57135e..e997e81 100644 --- a/crates/erp-config/src/service/setting_service.rs +++ b/crates/erp-config/src/service/setting_service.rs @@ -1,7 +1,5 @@ use chrono::Utc; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, -}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::SettingResp; @@ -46,9 +44,7 @@ impl SettingService { db: &sea_orm::DatabaseConnection, ) -> ConfigResult { // 1. Try exact match - if let Some(resp) = - Self::find_exact(key, scope, scope_id, tenant_id, db).await? - { + if let Some(resp) = Self::find_exact(key, scope, scope_id, tenant_id, db).await? { return Ok(resp); } @@ -81,12 +77,18 @@ impl SettingService { event_bus: &EventBus, ) -> ConfigResult { // Look for an existing non-deleted record - let existing = setting::Entity::find() + let mut query = setting::Entity::find() .filter(setting::Column::TenantId.eq(tenant_id)) .filter(setting::Column::Scope.eq(¶ms.scope)) - .filter(setting::Column::ScopeId.eq(params.scope_id)) .filter(setting::Column::SettingKey.eq(¶ms.key)) - .filter(setting::Column::DeletedAt.is_null()) + .filter(setting::Column::DeletedAt.is_null()); + + query = match params.scope_id { + Some(id) => query.filter(setting::Column::ScopeId.eq(id)), + None => query.filter(setting::Column::ScopeId.is_null()), + }; + + let existing = query .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; @@ -94,7 +96,9 @@ impl SettingService { if let Some(model) = existing { // Update existing record — 乐观锁校验 let next_version = match params.version { - Some(v) => check_version(v, model.version).map_err(|_| ConfigError::VersionMismatch)?, + Some(v) => { + check_version(v, model.version).map_err(|_| ConfigError::VersionMismatch)? + } None => model.version + 1, }; @@ -109,15 +113,20 @@ impl SettingService { .await .map_err(|e| ConfigError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "setting.updated", - tenant_id, - serde_json::json!({ - "setting_id": updated.id, - "key": params.key, - "scope": params.scope, - }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "setting.updated", + tenant_id, + serde_json::json!({ + "setting_id": updated.id, + "key": params.key, + "scope": params.scope, + }), + ), + db, + ) + .await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "setting.upsert", "setting") @@ -150,15 +159,20 @@ impl SettingService { .await .map_err(|e| ConfigError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "setting.created", - tenant_id, - serde_json::json!({ - "setting_id": id, - "key": params.key, - "scope": params.scope, - }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "setting.created", + tenant_id, + serde_json::json!({ + "setting_id": id, + "key": params.key, + "scope": params.scope, + }), + ), + db, + ) + .await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "setting.upsert", "setting") @@ -179,12 +193,17 @@ impl SettingService { pagination: &Pagination, db: &sea_orm::DatabaseConnection, ) -> ConfigResult<(Vec, u64)> { - let paginator = setting::Entity::find() + let mut query = setting::Entity::find() .filter(setting::Column::TenantId.eq(tenant_id)) .filter(setting::Column::Scope.eq(scope)) - .filter(setting::Column::ScopeId.eq(*scope_id)) - .filter(setting::Column::DeletedAt.is_null()) - .paginate(db, pagination.limit()); + .filter(setting::Column::DeletedAt.is_null()); + + query = match scope_id { + Some(id) => query.filter(setting::Column::ScopeId.eq(*id)), + None => query.filter(setting::Column::ScopeId.is_null()), + }; + + let paginator = query.paginate(db, pagination.limit()); let total = paginator .num_items() @@ -197,8 +216,7 @@ impl SettingService { .await .map_err(|e| ConfigError::Validation(e.to_string()))?; - let resps: Vec = - models.iter().map(Self::model_to_resp).collect(); + let resps: Vec = models.iter().map(Self::model_to_resp).collect(); Ok((resps, total)) } @@ -214,20 +232,23 @@ impl SettingService { version: i32, db: &sea_orm::DatabaseConnection, ) -> ConfigResult<()> { - let model = setting::Entity::find() + let mut query = setting::Entity::find() .filter(setting::Column::TenantId.eq(tenant_id)) .filter(setting::Column::Scope.eq(scope)) - .filter(setting::Column::ScopeId.eq(*scope_id)) .filter(setting::Column::SettingKey.eq(key)) - .filter(setting::Column::DeletedAt.is_null()) + .filter(setting::Column::DeletedAt.is_null()); + + query = match scope_id { + Some(id) => query.filter(setting::Column::ScopeId.eq(*id)), + None => query.filter(setting::Column::ScopeId.is_null()), + }; + + let model = query .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .ok_or_else(|| { - ConfigError::NotFound(format!( - "设置 '{}' 在 '{}' 作用域下不存在", - key, scope - )) + ConfigError::NotFound(format!("设置 '{}' 在 '{}' 作用域下不存在", key, scope)) })?; let next_version = @@ -264,12 +285,19 @@ impl SettingService { tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> ConfigResult> { - let model = setting::Entity::find() + let mut query = setting::Entity::find() .filter(setting::Column::TenantId.eq(tenant_id)) .filter(setting::Column::Scope.eq(scope)) - .filter(setting::Column::ScopeId.eq(*scope_id)) .filter(setting::Column::SettingKey.eq(key)) - .filter(setting::Column::DeletedAt.is_null()) + .filter(setting::Column::DeletedAt.is_null()); + + // SQL 中 `= NULL` 永远返回 false,必须用 IS NULL 匹配 NULL 值 + query = match scope_id { + Some(id) => query.filter(setting::Column::ScopeId.eq(*id)), + None => query.filter(setting::Column::ScopeId.is_null()), + }; + + let model = query .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; @@ -301,9 +329,7 @@ impl SettingService { (SCOPE_TENANT.to_string(), Some(tenant_id)), (SCOPE_PLATFORM.to_string(), None), ]), - SCOPE_TENANT => { - Ok(vec![(SCOPE_PLATFORM.to_string(), None)]) - } + SCOPE_TENANT => Ok(vec![(SCOPE_PLATFORM.to_string(), None)]), SCOPE_PLATFORM => Ok(vec![]), _ => Err(ConfigError::Validation(format!( "不支持的作用域类型: '{}'", diff --git a/crates/erp-core/src/error.rs b/crates/erp-core/src/error.rs index c4a71ba..e652b63 100644 --- a/crates/erp-core/src/error.rs +++ b/crates/erp-core/src/error.rs @@ -1,6 +1,6 @@ +use axum::Json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; -use axum::Json; use serde::Serialize; /// 统一错误响应格式 diff --git a/crates/erp-core/src/module.rs b/crates/erp-core/src/module.rs index f3b7ce9..e4b268c 100644 --- a/crates/erp-core/src/module.rs +++ b/crates/erp-core/src/module.rs @@ -57,7 +57,11 @@ impl ModuleRegistry { } pub fn register(mut self, module: impl ErpModule + 'static) -> Self { - tracing::info!(module = module.name(), version = module.version(), "Module registered"); + tracing::info!( + module = module.name(), + version = module.version(), + "Module registered" + ); let mut modules = (*self.modules).clone(); modules.push(Arc::new(module)); self.modules = Arc::new(modules); diff --git a/crates/erp-core/src/rbac.rs b/crates/erp-core/src/rbac.rs index a147812..9b25068 100644 --- a/crates/erp-core/src/rbac.rs +++ b/crates/erp-core/src/rbac.rs @@ -15,10 +15,7 @@ pub fn require_permission(ctx: &TenantContext, permission: &str) -> Result<(), A /// Check whether the `TenantContext` includes at least one of the specified permission codes. /// /// Useful when multiple permissions can grant access to the same resource. -pub fn require_any_permission( - ctx: &TenantContext, - permissions: &[&str], -) -> Result<(), AppError> { +pub fn require_any_permission(ctx: &TenantContext, permissions: &[&str]) -> Result<(), AppError> { let has_any = permissions .iter() .any(|p| ctx.permissions.iter().any(|up| up == *p)); diff --git a/crates/erp-message/src/dto.rs b/crates/erp-message/src/dto.rs index 9aebcc0..b02022e 100644 --- a/crates/erp-message/src/dto.rs +++ b/crates/erp-message/src/dto.rs @@ -1,7 +1,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use uuid::Uuid; use utoipa::ToSchema; +use uuid::Uuid; use validator::Validate; // ============ 消息 DTO ============ diff --git a/crates/erp-message/src/handler/message_handler.rs b/crates/erp-message/src/handler/message_handler.rs index 485e92b..fe11db9 100644 --- a/crates/erp-message/src/handler/message_handler.rs +++ b/crates/erp-message/src/handler/message_handler.rs @@ -1,6 +1,6 @@ -use axum::extract::{Extension, Path, Query, State}; -use axum::extract::FromRef; use axum::Json; +use axum::extract::FromRef; +use axum::extract::{Extension, Path, Query, State}; use uuid::Uuid; use erp_core::error::AppError; diff --git a/crates/erp-message/src/handler/subscription_handler.rs b/crates/erp-message/src/handler/subscription_handler.rs index b2706f9..8099fce 100644 --- a/crates/erp-message/src/handler/subscription_handler.rs +++ b/crates/erp-message/src/handler/subscription_handler.rs @@ -1,6 +1,6 @@ -use axum::extract::{Extension, State}; -use axum::extract::FromRef; use axum::Json; +use axum::extract::FromRef; +use axum::extract::{Extension, State}; use erp_core::error::AppError; use erp_core::types::{ApiResponse, TenantContext}; @@ -19,13 +19,7 @@ where MessageState: FromRef, S: Clone + Send + Sync + 'static, { - let resp = SubscriptionService::upsert( - ctx.tenant_id, - ctx.user_id, - &req, - &_state.db, - ) - .await?; + let resp = SubscriptionService::upsert(ctx.tenant_id, ctx.user_id, &req, &_state.db).await?; Ok(Json(ApiResponse::ok(resp))) } diff --git a/crates/erp-message/src/handler/template_handler.rs b/crates/erp-message/src/handler/template_handler.rs index 3a7bbdf..090bf38 100644 --- a/crates/erp-message/src/handler/template_handler.rs +++ b/crates/erp-message/src/handler/template_handler.rs @@ -1,6 +1,6 @@ -use axum::extract::{Extension, Query, State}; -use axum::extract::FromRef; use axum::Json; +use axum::extract::FromRef; +use axum::extract::{Extension, Query, State}; use serde::Deserialize; use erp_core::error::AppError; diff --git a/crates/erp-message/src/module.rs b/crates/erp-message/src/module.rs index b931515..20616ea 100644 --- a/crates/erp-message/src/module.rs +++ b/crates/erp-message/src/module.rs @@ -8,9 +8,7 @@ use erp_core::error::AppResult; use erp_core::events::EventBus; use erp_core::module::ErpModule; -use crate::handler::{ - message_handler, subscription_handler, template_handler, -}; +use crate::handler::{message_handler, subscription_handler, template_handler}; /// 消息中心模块,实现 ErpModule trait。 pub struct MessageModule; @@ -32,22 +30,10 @@ impl MessageModule { "/messages", get(message_handler::list_messages).post(message_handler::send_message), ) - .route( - "/messages/unread-count", - get(message_handler::unread_count), - ) - .route( - "/messages/{id}/read", - put(message_handler::mark_read), - ) - .route( - "/messages/read-all", - put(message_handler::mark_all_read), - ) - .route( - "/messages/{id}", - delete(message_handler::delete_message), - ) + .route("/messages/unread-count", get(message_handler::unread_count)) + .route("/messages/{id}/read", put(message_handler::mark_read)) + .route("/messages/read-all", put(message_handler::mark_all_read)) + .route("/messages/{id}", delete(message_handler::delete_message)) // 模板路由 .route( "/message-templates", @@ -79,9 +65,7 @@ impl MessageModule { // 先获取许可,再 spawn 任务 tokio::spawn(async move { let _permit = permit.acquire().await.unwrap(); - if let Err(e) = - handle_workflow_event(&event, &db, &event_bus).await - { + if let Err(e) = handle_workflow_event(&event, &db, &event_bus).await { tracing::warn!( event_type = %event.event_type, error = %e, @@ -146,11 +130,12 @@ async fn handle_workflow_event( ) -> Result<(), String> { match event.event_type.as_str() { "process_instance.started" => { - let instance_id = event.payload.get("instance_id") + let instance_id = event + .payload + .get("instance_id") .and_then(|v| v.as_str()) .unwrap_or("unknown"); - let starter_id = event.payload.get("started_by") - .and_then(|v| v.as_str()); + let starter_id = event.payload.get("started_by").and_then(|v| v.as_str()); if let Some(starter) = starter_id { let recipient = match uuid::Uuid::parse_str(starter) { @@ -174,11 +159,12 @@ async fn handle_workflow_event( } "task.completed" => { // 任务完成时通知流程发起人 - let task_id = event.payload.get("task_id") + let task_id = event + .payload + .get("task_id") .and_then(|v| v.as_str()) .unwrap_or("unknown"); - let starter_id = event.payload.get("started_by") - .and_then(|v| v.as_str()); + let starter_id = event.payload.get("started_by").and_then(|v| v.as_str()); if let Some(starter) = starter_id { let recipient = match uuid::Uuid::parse_str(starter) { diff --git a/crates/erp-message/src/service/message_service.rs b/crates/erp-message/src/service/message_service.rs index af1d917..88f890f 100644 --- a/crates/erp-message/src/service/message_service.rs +++ b/crates/erp-message/src/service/message_service.rs @@ -1,7 +1,7 @@ use chrono::Utc; use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, - Statement, ConnectionTrait, DatabaseBackend, + ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseBackend, EntityTrait, PaginatorTrait, + QueryFilter, Set, Statement, }; use uuid::Uuid; @@ -122,15 +122,20 @@ impl MessageService { .await .map_err(|e| MessageError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "message.sent", - tenant_id, - serde_json::json!({ - "message_id": id, - "recipient_id": req.recipient_id, - "title": req.title, - }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "message.sent", + tenant_id, + serde_json::json!({ + "message_id": id, + "recipient_id": req.recipient_id, + "title": req.title, + }), + ), + db, + ) + .await; audit_service::record( AuditLog::new(tenant_id, Some(sender_id), "message.send", "message") @@ -191,18 +196,28 @@ impl MessageService { .await .map_err(|e| MessageError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "message.sent", - tenant_id, - serde_json::json!({ - "message_id": id, - "recipient_id": recipient_id, - }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "message.sent", + tenant_id, + serde_json::json!({ + "message_id": id, + "recipient_id": recipient_id, + }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(system_user), "message.send_system", "message") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(system_user), + "message.send_system", + "message", + ) + .with_resource_id(id), db, ) .await; @@ -301,9 +316,7 @@ impl MessageService { .ok_or_else(|| MessageError::NotFound(format!("消息不存在: {id}")))?; if model.recipient_id != user_id { - return Err(MessageError::Validation( - "只能删除自己的消息".to_string(), - )); + return Err(MessageError::Validation("只能删除自己的消息".to_string())); } let current_version = model.version; diff --git a/crates/erp-message/src/service/template_service.rs b/crates/erp-message/src/service/template_service.rs index 705a8c5..d0a44f2 100644 --- a/crates/erp-message/src/service/template_service.rs +++ b/crates/erp-message/src/service/template_service.rs @@ -1,7 +1,5 @@ use chrono::Utc; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, -}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::{CreateTemplateReq, MessageTemplateResp}; diff --git a/crates/erp-plugin-prototype/Cargo.toml b/crates/erp-plugin-prototype/Cargo.toml new file mode 100644 index 0000000..d3ab06e --- /dev/null +++ b/crates/erp-plugin-prototype/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "erp-plugin-prototype" +version = "0.1.0" +edition = "2024" +description = "WASM 插件系统原型验证 — Host 端运行时" + +[dependencies] +wasmtime = "43" +wasmtime-wasi = "43" +tokio = { workspace = true } +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } + +[[test]] +name = "test_plugin_integration" +path = "tests/test_plugin_integration.rs" diff --git a/crates/erp-plugin-prototype/src/lib.rs b/crates/erp-plugin-prototype/src/lib.rs new file mode 100644 index 0000000..f876f3c --- /dev/null +++ b/crates/erp-plugin-prototype/src/lib.rs @@ -0,0 +1,208 @@ +//! WASM 插件原型验证 — Host 端运行时 +//! +//! 验证目标: +//! - V1: WIT 接口定义 + bindgen! 宏编译通过 +//! - V2: Host 调用插件导出函数(init / handle_event) +//! - V3: 插件调用 Host 导入函数(db_insert / log_write) +//! - V4: async 支持(Host async 函数正确桥接) +//! - V5: Fuel + Epoch 资源限制 +//! - V6: 从二进制动态加载 + +use anyhow::Result; +use wasmtime::component::{Component, HasSelf, Linker, bindgen}; +use wasmtime::{Config, Engine, Store, StoreLimits, StoreLimitsBuilder}; + +/// Host 端状态,绑定到每个 Store 实例 +pub struct HostState { + /// Store 级资源限制 + pub(crate) limits: StoreLimits, + /// 模拟数据库操作记录 + pub db_ops: Vec, + /// 日志记录 + pub logs: Vec<(String, String)>, + /// 发布的事件 + pub events: Vec<(String, Vec)>, + /// 配置存储(模拟) + pub config_map: std::collections::HashMap>, +} + +/// 数据库操作记录 +#[derive(Debug, Clone)] +pub struct DbOperation { + pub op_type: String, + pub entity: String, + pub data: Option>, + pub id: Option, + pub version: Option, +} + +impl Default for HostState { + fn default() -> Self { + Self::new() + } +} + +impl HostState { + pub fn new() -> Self { + Self { + limits: StoreLimitsBuilder::new().build(), + db_ops: Vec::new(), + logs: Vec::new(), + events: Vec::new(), + config_map: std::collections::HashMap::new(), + } + } +} + +// bindgen! 生成类型化绑定(包含 Host trait 和 add_to_linker) +bindgen!({ + path: "./wit/plugin.wit", + world: "plugin-world", +}); + +// 实现 bindgen 生成的 Host trait — 插件调用 Host API 的入口 +impl erp::plugin::host_api::Host for HostState { + fn db_insert(&mut self, entity: String, data: Vec) -> Result, String> { + let id = format!("id-{}", self.db_ops.len() + 1); + let record = serde_json::json!({ + "id": id, + "tenant_id": "tenant-default", + "entity": entity, + "data": serde_json::from_slice::(&data).unwrap_or(serde_json::Value::Null), + }); + let result = serde_json::to_vec(&record).map_err(|e| e.to_string())?; + + self.db_ops.push(DbOperation { + op_type: "insert".into(), + entity: entity.clone(), + data: Some(data), + id: Some(id), + version: None, + }); + + Ok(result) + } + + fn db_query( + &mut self, + entity: String, + _filter: Vec, + _pagination: Vec, + ) -> Result, String> { + let results: Vec = self + .db_ops + .iter() + .filter(|op| op.entity == entity && op.op_type == "insert") + .map(|op| { + serde_json::json!({ + "id": op.id, + "entity": op.entity, + }) + }) + .collect(); + serde_json::to_vec(&results).map_err(|e| e.to_string()) + } + + fn db_update( + &mut self, + entity: String, + id: String, + data: Vec, + version: i64, + ) -> Result, String> { + let record = serde_json::json!({ + "id": id, + "tenant_id": "tenant-default", + "entity": entity, + "version": version + 1, + "data": serde_json::from_slice::(&data).unwrap_or(serde_json::Value::Null), + }); + let result = serde_json::to_vec(&record).map_err(|e| e.to_string())?; + + self.db_ops.push(DbOperation { + op_type: "update".into(), + entity, + data: Some(data), + id: Some(id), + version: Some(version), + }); + + Ok(result) + } + + fn db_delete(&mut self, entity: String, id: String) -> Result<(), String> { + self.db_ops.push(DbOperation { + op_type: "delete".into(), + entity, + data: None, + id: Some(id), + version: None, + }); + Ok(()) + } + + fn event_publish(&mut self, event_type: String, payload: Vec) -> Result<(), String> { + self.events.push((event_type, payload)); + Ok(()) + } + + fn config_get(&mut self, key: String) -> Result, String> { + self.config_map + .get(&key) + .cloned() + .ok_or_else(|| format!("配置项 '{}' 不存在", key)) + } + + fn log_write(&mut self, level: String, message: String) { + self.logs.push((level, message)); + } + + fn current_user(&mut self) -> Result, String> { + let user = serde_json::json!({ + "id": "user-default", + "username": "admin", + "tenant_id": "tenant-default", + }); + serde_json::to_vec(&user).map_err(|e| e.to_string()) + } + + fn check_permission(&mut self, _permission: String) -> Result { + Ok(true) + } +} + +/// 创建 Wasmtime Engine(启用 Fuel 限制) +pub fn create_engine() -> Result { + let mut config = Config::new(); + config.wasm_component_model(true); + config.consume_fuel(true); + Ok(Engine::new(&config)?) +} + +/// 创建带 Fuel 限制的 Store +pub fn create_store(engine: &Engine, fuel: u64) -> Result> { + let state = HostState::new(); + let mut store = Store::new(engine, state); + store.set_fuel(fuel)?; + store.limiter(|state| &mut state.limits); + Ok(store) +} + +/// 从 WASM 二进制加载并实例化插件 +pub async fn load_plugin( + engine: &Engine, + wasm_bytes: &[u8], + fuel: u64, +) -> Result<(Store, PluginWorld)> { + let mut store = create_store(engine, fuel)?; + let component = Component::from_binary(engine, wasm_bytes)?; + + let mut linker = Linker::new(engine); + // 注册 Host API 到 Linker,使插件可以调用 host 函数 + // HasSelf 表示 Data<'a> = &'a mut HostState + PluginWorld::add_to_linker::<_, HasSelf>(&mut linker, |state| state)?; + + let instance = PluginWorld::instantiate_async(&mut store, &component, &linker).await?; + + Ok((store, instance)) +} diff --git a/crates/erp-plugin-prototype/src/main.rs b/crates/erp-plugin-prototype/src/main.rs new file mode 100644 index 0000000..f1d5093 --- /dev/null +++ b/crates/erp-plugin-prototype/src/main.rs @@ -0,0 +1,10 @@ +use erp_plugin_prototype::*; + +fn _discover_api(instance: PluginWorld, mut store: wasmtime::Store) { + let api = instance.erp_plugin_plugin_api(); + let _ = api.call_init(&mut store); + let _ = api.call_on_tenant_created(&mut store, "test-tenant"); + let _ = api.call_handle_event(&mut store, "test.event", &[1u8]); +} + +fn main() {} diff --git a/crates/erp-plugin-prototype/tests/test_plugin_integration.rs b/crates/erp-plugin-prototype/tests/test_plugin_integration.rs new file mode 100644 index 0000000..3fa9d00 --- /dev/null +++ b/crates/erp-plugin-prototype/tests/test_plugin_integration.rs @@ -0,0 +1,220 @@ +//! WASM 插件集成测试 +//! +//! 验证目标: +//! V1 — WIT 接口 + bindgen! 编译通过 +//! V2 — Host 调用插件 init() / handle_event() +//! V3 — 插件回调 Host db_insert / log_write +//! V4 — async 实例化桥接 +//! V5 — Fuel 资源限制 +//! V6 — 从二进制动态加载 + +use anyhow::Result; +use erp_plugin_prototype::{create_engine, load_plugin}; + +/// 获取测试插件 WASM Component 文件路径 +fn wasm_path() -> String { + let candidates = [ + // 预构建的 WASM Component(通过 wasm-tools component new 生成) + "../../target/erp_plugin_test_sample.component.wasm".into(), + // 备选:绝对路径 + format!( + "{}/../../target/erp_plugin_test_sample.component.wasm", + std::env::current_dir().unwrap().display() + ), + ]; + for path in &candidates { + if std::path::Path::new(path).exists() { + return path.clone(); + } + } + candidates[0].clone() +} + +#[tokio::test] +async fn test_v6_load_plugin_from_binary() -> Result<()> { + let wasm_path = wasm_path(); + let wasm_bytes = std::fs::read(&wasm_path).map_err(|e| { + anyhow::anyhow!( + "读取 WASM 失败: {}。请先编译: cargo build -p erp-plugin-test-sample --target wasm32-unknown-unknown --release\n路径: {}", + e, wasm_path + ) + })?; + + assert!(!wasm_bytes.is_empty(), "WASM 文件不应为空"); + println!("WASM 文件大小: {} bytes", wasm_bytes.len()); + + let engine = create_engine()?; + let (_store, _instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_v2_host_calls_plugin_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?; + + // V2: 调用插件 init() + instance + .erp_plugin_plugin_api() + .call_init(&mut store)? + .map_err(|e| anyhow::anyhow!(e))?; + + // 验证 Host 端收到了日志 + let state = store.data(); + assert!( + state + .logs + .iter() + .any(|(_, m)| m.contains("测试插件初始化成功")), + "应收到初始化日志" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_v3_plugin_calls_host_api() -> 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(插件会调用 db_insert 和 log_write) + instance + .erp_plugin_plugin_api() + .call_init(&mut store)? + .map_err(|e| anyhow::anyhow!(e))?; + + let state = store.data(); + + // 验证 db_insert 被调用 + assert!( + state + .db_ops + .iter() + .any(|op| op.op_type == "insert" && op.entity == "inventory_item"), + "应有 inventory_item 的 insert 操作" + ); + + // 验证 log_write 被调用 + assert!( + state.logs.iter().any(|(_, m)| m.contains("插入成功")), + "应有插入成功的日志" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_v3_plugin_handle_event_with_db_callback() -> 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))?; + + // 发送事件 + let payload = serde_json::json!({"order_id": "PO-001", "action": "approve"}).to_string(); + instance + .erp_plugin_plugin_api() + .call_handle_event(&mut store, "workflow.task.completed", payload.as_bytes())? + .map_err(|e| anyhow::anyhow!(e))?; + + let state = store.data(); + + // 验证 db_update 被调用 + assert!( + state + .db_ops + .iter() + .any(|op| op.op_type == "update" && op.entity == "purchase_order"), + "应有 purchase_order 的 update 操作" + ); + + // 验证事件被发布 + assert!( + state + .events + .iter() + .any(|(t, _)| t == "purchase_order.approved"), + "应发布 purchase_order.approved 事件" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_v5_fuel_limit_traps() -> Result<()> { + let wasm_bytes = std::fs::read(wasm_path())?; + let engine = create_engine()?; + + // 给极少量的 fuel,插件 init() 应该无法完成 + let result = load_plugin(&engine, &wasm_bytes, 10).await; + match result { + Ok((mut store, instance)) => { + let init_result = instance.erp_plugin_plugin_api().call_init(&mut store); + assert!( + init_result.is_err() || init_result.unwrap().is_err(), + "低 fuel 应导致失败" + ); + } + Err(_) => { + // 实例化就失败了,也是预期行为 + } + } + + Ok(()) +} + +#[tokio::test] +async fn test_full_lifecycle() -> 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?; + + // 1. init + instance + .erp_plugin_plugin_api() + .call_init(&mut store)? + .map_err(|e| anyhow::anyhow!(e))?; + + // 2. on_tenant_created + instance + .erp_plugin_plugin_api() + .call_on_tenant_created(&mut store, "tenant-new-001")? + .map_err(|e| anyhow::anyhow!(e))?; + + // 3. handle_event + let payload = serde_json::json!({"order_id": "PO-002"}).to_string(); + instance + .erp_plugin_plugin_api() + .call_handle_event(&mut store, "workflow.task.completed", payload.as_bytes())? + .map_err(|e| anyhow::anyhow!(e))?; + + let state = store.data(); + + // 完整生命周期验证 + assert!( + state.logs.len() >= 3, + "应有多条日志记录,实际: {}", + state.logs.len() + ); + assert!( + state.db_ops.len() >= 3, + "应有多次数据库操作,实际: {}", + state.db_ops.len() + ); + assert!(state.events.len() >= 1, "应有事件发布"); + + println!("\n=== 完整生命周期验证 ==="); + println!("日志: {} 条", state.logs.len()); + println!("DB 操作: {} 次", state.db_ops.len()); + println!("事件: {} 条", state.events.len()); + + Ok(()) +} diff --git a/crates/erp-plugin-prototype/wit/plugin.wit b/crates/erp-plugin-prototype/wit/plugin.wit new file mode 100644 index 0000000..a61ca68 --- /dev/null +++ b/crates/erp-plugin-prototype/wit/plugin.wit @@ -0,0 +1,48 @@ +package erp:plugin; + +/// 宿主暴露给插件的 API(插件 import 这些函数) +interface host-api { + /// 插入记录(宿主自动注入 tenant_id, id, created_at 等标准字段) + db-insert: func(entity: string, data: list) -> result, string>; + + /// 查询记录(自动注入 tenant_id 过滤 + 软删除过滤) + db-query: func(entity: string, filter: list, pagination: list) -> result, string>; + + /// 更新记录(自动检查 version 乐观锁) + db-update: func(entity: string, id: string, data: list, version: s64) -> result, string>; + + /// 软删除记录 + db-delete: func(entity: string, id: string) -> result<_, string>; + + /// 发布领域事件 + event-publish: func(event-type: string, payload: list) -> result<_, string>; + + /// 读取系统配置 + config-get: func(key: string) -> result, string>; + + /// 写日志(自动关联 tenant_id + plugin_id) + log-write: func(level: string, message: string); + + /// 获取当前用户信息 + current-user: func() -> result, string>; + + /// 检查当前用户权限 + check-permission: func(permission: string) -> result; +} + +/// 插件导出的 API(宿主调用这些函数) +interface plugin-api { + /// 插件初始化(加载时调用一次) + init: func() -> result<_, string>; + + /// 租户创建时调用 + on-tenant-created: func(tenant-id: string) -> result<_, string>; + + /// 处理订阅的事件 + handle-event: func(event-type: string, payload: list) -> result<_, string>; +} + +world plugin-world { + import host-api; + export plugin-api; +} diff --git a/crates/erp-plugin-test-sample/Cargo.toml b/crates/erp-plugin-test-sample/Cargo.toml new file mode 100644 index 0000000..8cf547a --- /dev/null +++ b/crates/erp-plugin-test-sample/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "erp-plugin-test-sample" +version = "0.1.0" +edition = "2024" +description = "WASM 插件系统原型验证 — 测试插件" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = "0.55" +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/erp-plugin-test-sample/src/lib.rs b/crates/erp-plugin-test-sample/src/lib.rs new file mode 100644 index 0000000..308b1a9 --- /dev/null +++ b/crates/erp-plugin-test-sample/src/lib.rs @@ -0,0 +1,68 @@ +//! WASM 测试插件 — 验证插件端 API + +use serde_json::json; + +wit_bindgen::generate!({ + path: "../erp-plugin-prototype/wit/plugin.wit", + world: "plugin-world", +}); + +use crate::erp::plugin::host_api; +use crate::exports::erp::plugin::plugin_api::Guest; + +struct TestPlugin; + +impl Guest for TestPlugin { + fn init() -> Result<(), String> { + host_api::log_write("info", "测试插件初始化成功"); + + let data = json!({"sku": "TEST-001", "name": "测试商品", "quantity": 100}).to_string(); + let result = host_api::db_insert("inventory_item", data.as_bytes()) + .map_err(|e| format!("db_insert 失败: {}", e))?; + + let record: serde_json::Value = + serde_json::from_slice(&result).map_err(|e| format!("解析结果失败: {}", e))?; + + host_api::log_write( + "info", + &format!( + "插入成功: id={}, tenant_id={}", + record["id"].as_str().unwrap_or("?"), + record["tenant_id"].as_str().unwrap_or("?") + ), + ); + + Ok(()) + } + + fn on_tenant_created(tenant_id: String) -> Result<(), String> { + host_api::log_write("info", &format!("租户创建: {}", tenant_id)); + let data = json!({"name": "默认分类", "tenant_id": tenant_id}).to_string(); + host_api::db_insert("inventory_category", data.as_bytes()) + .map_err(|e| format!("创建默认分类失败: {}", e))?; + Ok(()) + } + + fn handle_event(event_type: String, payload: Vec) -> Result<(), String> { + host_api::log_write("info", &format!("处理事件: {}", event_type)); + let data: serde_json::Value = + serde_json::from_slice(&payload).map_err(|e| format!("解析失败: {}", e))?; + + if event_type == "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, "status": "approved"}).to_string(); + host_api::event_publish("purchase_order.approved", evt.as_bytes()) + .map_err(|e| format!("发布事件失败: {}", e))?; + + host_api::log_write("info", &format!("采购单 {} 审批完成", order_id)); + } + + Ok(()) + } +} + +export!(TestPlugin); diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index 1af0fd9..d889cda 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -29,6 +29,7 @@ mod m20260413_000026_create_audit_logs; mod m20260414_000027_fix_unique_indexes_soft_delete; mod m20260414_000028_add_standard_fields_to_tokens; mod m20260414_000029_add_standard_fields_to_process_variables; +mod m20260414_000032_fix_settings_unique_index_null; mod m20260415_000030_add_version_to_message_tables; mod m20260416_000031_create_domain_events; @@ -69,6 +70,7 @@ impl MigratorTrait for Migrator { Box::new(m20260414_000029_add_standard_fields_to_process_variables::Migration), Box::new(m20260415_000030_add_version_to_message_tables::Migration), Box::new(m20260416_000031_create_domain_events::Migration), + Box::new(m20260414_000032_fix_settings_unique_index_null::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260410_000001_create_tenant.rs b/crates/erp-server/migration/src/m20260410_000001_create_tenant.rs index c695330..282996d 100644 --- a/crates/erp-server/migration/src/m20260410_000001_create_tenant.rs +++ b/crates/erp-server/migration/src/m20260410_000001_create_tenant.rs @@ -11,12 +11,7 @@ impl MigrationTrait for Migration { Table::create() .table(Tenant::Table) .if_not_exists() - .col( - ColumnDef::new(Tenant::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Tenant::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Tenant::Name).string().not_null()) .col( ColumnDef::new(Tenant::Code) diff --git a/crates/erp-server/migration/src/m20260411_000002_create_users.rs b/crates/erp-server/migration/src/m20260411_000002_create_users.rs index ef4a8c2..8d6bc9d 100644 --- a/crates/erp-server/migration/src/m20260411_000002_create_users.rs +++ b/crates/erp-server/migration/src/m20260411_000002_create_users.rs @@ -11,12 +11,7 @@ impl MigrationTrait for Migration { Table::create() .table(Users::Table) .if_not_exists() - .col( - ColumnDef::new(Users::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Users::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Users::TenantId).uuid().not_null()) .col(ColumnDef::new(Users::Username).string().not_null()) .col(ColumnDef::new(Users::Email).string().null()) diff --git a/crates/erp-server/migration/src/m20260411_000003_create_user_credentials.rs b/crates/erp-server/migration/src/m20260411_000003_create_user_credentials.rs index e062333..69c05c9 100644 --- a/crates/erp-server/migration/src/m20260411_000003_create_user_credentials.rs +++ b/crates/erp-server/migration/src/m20260411_000003_create_user_credentials.rs @@ -25,7 +25,11 @@ impl MigrationTrait for Migration { .not_null() .default("password"), ) - .col(ColumnDef::new(UserCredentials::CredentialData).json().null()) + .col( + ColumnDef::new(UserCredentials::CredentialData) + .json() + .null(), + ) .col( ColumnDef::new(UserCredentials::Verified) .boolean() diff --git a/crates/erp-server/migration/src/m20260411_000005_create_roles.rs b/crates/erp-server/migration/src/m20260411_000005_create_roles.rs index 98d5498..e424fce 100644 --- a/crates/erp-server/migration/src/m20260411_000005_create_roles.rs +++ b/crates/erp-server/migration/src/m20260411_000005_create_roles.rs @@ -11,12 +11,7 @@ impl MigrationTrait for Migration { Table::create() .table(Roles::Table) .if_not_exists() - .col( - ColumnDef::new(Roles::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Roles::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Roles::TenantId).uuid().not_null()) .col(ColumnDef::new(Roles::Name).string().not_null()) .col(ColumnDef::new(Roles::Code).string().not_null()) diff --git a/crates/erp-server/migration/src/m20260411_000007_create_role_permissions.rs b/crates/erp-server/migration/src/m20260411_000007_create_role_permissions.rs index 9c7ac4c..0535761 100644 --- a/crates/erp-server/migration/src/m20260411_000007_create_role_permissions.rs +++ b/crates/erp-server/migration/src/m20260411_000007_create_role_permissions.rs @@ -12,7 +12,11 @@ impl MigrationTrait for Migration { .table(RolePermissions::Table) .if_not_exists() .col(ColumnDef::new(RolePermissions::RoleId).uuid().not_null()) - .col(ColumnDef::new(RolePermissions::PermissionId).uuid().not_null()) + .col( + ColumnDef::new(RolePermissions::PermissionId) + .uuid() + .not_null(), + ) .col(ColumnDef::new(RolePermissions::TenantId).uuid().not_null()) .col( ColumnDef::new(RolePermissions::CreatedAt) diff --git a/crates/erp-server/migration/src/m20260412_000013_create_dictionary_items.rs b/crates/erp-server/migration/src/m20260412_000013_create_dictionary_items.rs index 816cdfe..ffb5793 100644 --- a/crates/erp-server/migration/src/m20260412_000013_create_dictionary_items.rs +++ b/crates/erp-server/migration/src/m20260412_000013_create_dictionary_items.rs @@ -18,7 +18,11 @@ impl MigrationTrait for Migration { .primary_key(), ) .col(ColumnDef::new(DictionaryItems::TenantId).uuid().not_null()) - .col(ColumnDef::new(DictionaryItems::DictionaryId).uuid().not_null()) + .col( + ColumnDef::new(DictionaryItems::DictionaryId) + .uuid() + .not_null(), + ) .col(ColumnDef::new(DictionaryItems::Label).string().not_null()) .col(ColumnDef::new(DictionaryItems::Value).string().not_null()) .col( diff --git a/crates/erp-server/migration/src/m20260412_000014_create_menus.rs b/crates/erp-server/migration/src/m20260412_000014_create_menus.rs index 31f222f..c491458 100644 --- a/crates/erp-server/migration/src/m20260412_000014_create_menus.rs +++ b/crates/erp-server/migration/src/m20260412_000014_create_menus.rs @@ -11,12 +11,7 @@ impl MigrationTrait for Migration { Table::create() .table(Menus::Table) .if_not_exists() - .col( - ColumnDef::new(Menus::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Menus::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Menus::TenantId).uuid().not_null()) .col(ColumnDef::new(Menus::ParentId).uuid().null()) .col(ColumnDef::new(Menus::Title).string().not_null()) diff --git a/crates/erp-server/migration/src/m20260412_000016_create_settings.rs b/crates/erp-server/migration/src/m20260412_000016_create_settings.rs index 4517ca2..2df5833 100644 --- a/crates/erp-server/migration/src/m20260412_000016_create_settings.rs +++ b/crates/erp-server/migration/src/m20260412_000016_create_settings.rs @@ -11,12 +11,7 @@ impl MigrationTrait for Migration { Table::create() .table(Settings::Table) .if_not_exists() - .col( - ColumnDef::new(Settings::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Settings::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Settings::TenantId).uuid().not_null()) .col(ColumnDef::new(Settings::Scope).string().not_null()) .col(ColumnDef::new(Settings::ScopeId).uuid().null()) diff --git a/crates/erp-server/migration/src/m20260412_000018_create_process_definitions.rs b/crates/erp-server/migration/src/m20260412_000018_create_process_definitions.rs index bdc5622..d909e23 100644 --- a/crates/erp-server/migration/src/m20260412_000018_create_process_definitions.rs +++ b/crates/erp-server/migration/src/m20260412_000018_create_process_definitions.rs @@ -17,7 +17,11 @@ impl MigrationTrait for Migration { .not_null() .primary_key(), ) - .col(ColumnDef::new(ProcessDefinitions::TenantId).uuid().not_null()) + .col( + ColumnDef::new(ProcessDefinitions::TenantId) + .uuid() + .not_null(), + ) .col(ColumnDef::new(ProcessDefinitions::Name).string().not_null()) .col(ColumnDef::new(ProcessDefinitions::Key).string().not_null()) .col( @@ -27,7 +31,11 @@ impl MigrationTrait for Migration { .default(1), ) .col(ColumnDef::new(ProcessDefinitions::Category).string().null()) - .col(ColumnDef::new(ProcessDefinitions::Description).text().null()) + .col( + ColumnDef::new(ProcessDefinitions::Description) + .text() + .null(), + ) .col( ColumnDef::new(ProcessDefinitions::Nodes) .json_binary() @@ -58,8 +66,16 @@ impl MigrationTrait for Migration { .not_null() .default(Expr::current_timestamp()), ) - .col(ColumnDef::new(ProcessDefinitions::CreatedBy).uuid().not_null()) - .col(ColumnDef::new(ProcessDefinitions::UpdatedBy).uuid().not_null()) + .col( + ColumnDef::new(ProcessDefinitions::CreatedBy) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(ProcessDefinitions::UpdatedBy) + .uuid() + .not_null(), + ) .col( ColumnDef::new(ProcessDefinitions::DeletedAt) .timestamp_with_time_zone() diff --git a/crates/erp-server/migration/src/m20260412_000019_create_process_instances.rs b/crates/erp-server/migration/src/m20260412_000019_create_process_instances.rs index c4b7ef4..8de4985 100644 --- a/crates/erp-server/migration/src/m20260412_000019_create_process_instances.rs +++ b/crates/erp-server/migration/src/m20260412_000019_create_process_instances.rs @@ -18,15 +18,27 @@ impl MigrationTrait for Migration { .primary_key(), ) .col(ColumnDef::new(ProcessInstances::TenantId).uuid().not_null()) - .col(ColumnDef::new(ProcessInstances::DefinitionId).uuid().not_null()) - .col(ColumnDef::new(ProcessInstances::BusinessKey).string().null()) + .col( + ColumnDef::new(ProcessInstances::DefinitionId) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(ProcessInstances::BusinessKey) + .string() + .null(), + ) .col( ColumnDef::new(ProcessInstances::Status) .string() .not_null() .default("running"), ) - .col(ColumnDef::new(ProcessInstances::StartedBy).uuid().not_null()) + .col( + ColumnDef::new(ProcessInstances::StartedBy) + .uuid() + .not_null(), + ) .col( ColumnDef::new(ProcessInstances::StartedAt) .timestamp_with_time_zone() @@ -50,8 +62,16 @@ impl MigrationTrait for Migration { .not_null() .default(Expr::current_timestamp()), ) - .col(ColumnDef::new(ProcessInstances::CreatedBy).uuid().not_null()) - .col(ColumnDef::new(ProcessInstances::UpdatedBy).uuid().not_null()) + .col( + ColumnDef::new(ProcessInstances::CreatedBy) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(ProcessInstances::UpdatedBy) + .uuid() + .not_null(), + ) .col( ColumnDef::new(ProcessInstances::DeletedAt) .timestamp_with_time_zone() diff --git a/crates/erp-server/migration/src/m20260412_000020_create_tokens.rs b/crates/erp-server/migration/src/m20260412_000020_create_tokens.rs index 3b06992..5af0191 100644 --- a/crates/erp-server/migration/src/m20260412_000020_create_tokens.rs +++ b/crates/erp-server/migration/src/m20260412_000020_create_tokens.rs @@ -11,12 +11,7 @@ impl MigrationTrait for Migration { Table::create() .table(Tokens::Table) .if_not_exists() - .col( - ColumnDef::new(Tokens::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Tokens::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Tokens::TenantId).uuid().not_null()) .col(ColumnDef::new(Tokens::InstanceId).uuid().not_null()) .col(ColumnDef::new(Tokens::NodeId).string().not_null()) diff --git a/crates/erp-server/migration/src/m20260412_000021_create_tasks.rs b/crates/erp-server/migration/src/m20260412_000021_create_tasks.rs index e919fee..18aa89c 100644 --- a/crates/erp-server/migration/src/m20260412_000021_create_tasks.rs +++ b/crates/erp-server/migration/src/m20260412_000021_create_tasks.rs @@ -11,12 +11,7 @@ impl MigrationTrait for Migration { Table::create() .table(Tasks::Table) .if_not_exists() - .col( - ColumnDef::new(Tasks::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Tasks::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Tasks::TenantId).uuid().not_null()) .col(ColumnDef::new(Tasks::InstanceId).uuid().not_null()) .col(ColumnDef::new(Tasks::TokenId).uuid().not_null()) diff --git a/crates/erp-server/migration/src/m20260412_000022_create_process_variables.rs b/crates/erp-server/migration/src/m20260412_000022_create_process_variables.rs index 4dad07a..4b0c1e4 100644 --- a/crates/erp-server/migration/src/m20260412_000022_create_process_variables.rs +++ b/crates/erp-server/migration/src/m20260412_000022_create_process_variables.rs @@ -18,7 +18,11 @@ impl MigrationTrait for Migration { .primary_key(), ) .col(ColumnDef::new(ProcessVariables::TenantId).uuid().not_null()) - .col(ColumnDef::new(ProcessVariables::InstanceId).uuid().not_null()) + .col( + ColumnDef::new(ProcessVariables::InstanceId) + .uuid() + .not_null(), + ) .col(ColumnDef::new(ProcessVariables::Name).string().not_null()) .col( ColumnDef::new(ProcessVariables::VarType) @@ -27,8 +31,16 @@ impl MigrationTrait for Migration { .default("string"), ) .col(ColumnDef::new(ProcessVariables::ValueString).text().null()) - .col(ColumnDef::new(ProcessVariables::ValueNumber).double().null()) - .col(ColumnDef::new(ProcessVariables::ValueBoolean).boolean().null()) + .col( + ColumnDef::new(ProcessVariables::ValueNumber) + .double() + .null(), + ) + .col( + ColumnDef::new(ProcessVariables::ValueBoolean) + .boolean() + .null(), + ) .col( ColumnDef::new(ProcessVariables::ValueDate) .timestamp_with_time_zone() diff --git a/crates/erp-server/migration/src/m20260413_000023_create_message_templates.rs b/crates/erp-server/migration/src/m20260413_000023_create_message_templates.rs index 5d195ff..77b9f13 100644 --- a/crates/erp-server/migration/src/m20260413_000023_create_message_templates.rs +++ b/crates/erp-server/migration/src/m20260413_000023_create_message_templates.rs @@ -18,16 +18,8 @@ impl MigrationTrait for Migration { .primary_key(), ) .col(ColumnDef::new(MessageTemplates::TenantId).uuid().not_null()) - .col( - ColumnDef::new(MessageTemplates::Name) - .string() - .not_null(), - ) - .col( - ColumnDef::new(MessageTemplates::Code) - .string() - .not_null(), - ) + .col(ColumnDef::new(MessageTemplates::Name).string().not_null()) + .col(ColumnDef::new(MessageTemplates::Code).string().not_null()) .col( ColumnDef::new(MessageTemplates::Channel) .string() @@ -50,11 +42,31 @@ impl MigrationTrait for Migration { .not_null() .default("zh-CN"), ) - .col(ColumnDef::new(MessageTemplates::CreatedAt).timestamp_with_time_zone().not_null()) - .col(ColumnDef::new(MessageTemplates::UpdatedAt).timestamp_with_time_zone().not_null()) - .col(ColumnDef::new(MessageTemplates::CreatedBy).uuid().not_null()) - .col(ColumnDef::new(MessageTemplates::UpdatedBy).uuid().not_null()) - .col(ColumnDef::new(MessageTemplates::DeletedAt).timestamp_with_time_zone().null()) + .col( + ColumnDef::new(MessageTemplates::CreatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .col( + ColumnDef::new(MessageTemplates::UpdatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .col( + ColumnDef::new(MessageTemplates::CreatedBy) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(MessageTemplates::UpdatedBy) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(MessageTemplates::DeletedAt) + .timestamp_with_time_zone() + .null(), + ) .to_owned(), ) .await?; diff --git a/crates/erp-server/migration/src/m20260413_000024_create_messages.rs b/crates/erp-server/migration/src/m20260413_000024_create_messages.rs index ba7b1ef..38a3628 100644 --- a/crates/erp-server/migration/src/m20260413_000024_create_messages.rs +++ b/crates/erp-server/migration/src/m20260413_000024_create_messages.rs @@ -11,12 +11,7 @@ impl MigrationTrait for Migration { Table::create() .table(Messages::Table) .if_not_exists() - .col( - ColumnDef::new(Messages::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Messages::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Messages::TenantId).uuid().not_null()) .col(ColumnDef::new(Messages::TemplateId).uuid().null()) .col(ColumnDef::new(Messages::SenderId).uuid().null()) @@ -49,26 +44,50 @@ impl MigrationTrait for Migration { .not_null() .default(false), ) - .col(ColumnDef::new(Messages::ReadAt).timestamp_with_time_zone().null()) + .col( + ColumnDef::new(Messages::ReadAt) + .timestamp_with_time_zone() + .null(), + ) .col( ColumnDef::new(Messages::IsArchived) .boolean() .not_null() .default(false), ) - .col(ColumnDef::new(Messages::ArchivedAt).timestamp_with_time_zone().null()) - .col(ColumnDef::new(Messages::SentAt).timestamp_with_time_zone().null()) + .col( + ColumnDef::new(Messages::ArchivedAt) + .timestamp_with_time_zone() + .null(), + ) + .col( + ColumnDef::new(Messages::SentAt) + .timestamp_with_time_zone() + .null(), + ) .col( ColumnDef::new(Messages::Status) .string() .not_null() .default("sent"), ) - .col(ColumnDef::new(Messages::CreatedAt).timestamp_with_time_zone().not_null()) - .col(ColumnDef::new(Messages::UpdatedAt).timestamp_with_time_zone().not_null()) + .col( + ColumnDef::new(Messages::CreatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .col( + ColumnDef::new(Messages::UpdatedAt) + .timestamp_with_time_zone() + .not_null(), + ) .col(ColumnDef::new(Messages::CreatedBy).uuid().not_null()) .col(ColumnDef::new(Messages::UpdatedBy).uuid().not_null()) - .col(ColumnDef::new(Messages::DeletedAt).timestamp_with_time_zone().null()) + .col( + ColumnDef::new(Messages::DeletedAt) + .timestamp_with_time_zone() + .null(), + ) .to_owned(), ) .await?; diff --git a/crates/erp-server/migration/src/m20260413_000025_create_message_subscriptions.rs b/crates/erp-server/migration/src/m20260413_000025_create_message_subscriptions.rs index e7921f6..8a355d3 100644 --- a/crates/erp-server/migration/src/m20260413_000025_create_message_subscriptions.rs +++ b/crates/erp-server/migration/src/m20260413_000025_create_message_subscriptions.rs @@ -17,23 +17,63 @@ impl MigrationTrait for Migration { .not_null() .primary_key(), ) - .col(ColumnDef::new(MessageSubscriptions::TenantId).uuid().not_null()) - .col(ColumnDef::new(MessageSubscriptions::UserId).uuid().not_null()) - .col(ColumnDef::new(MessageSubscriptions::NotificationTypes).json().null()) - .col(ColumnDef::new(MessageSubscriptions::ChannelPreferences).json().null()) + .col( + ColumnDef::new(MessageSubscriptions::TenantId) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(MessageSubscriptions::UserId) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(MessageSubscriptions::NotificationTypes) + .json() + .null(), + ) + .col( + ColumnDef::new(MessageSubscriptions::ChannelPreferences) + .json() + .null(), + ) .col( ColumnDef::new(MessageSubscriptions::DndEnabled) .boolean() .not_null() .default(false), ) - .col(ColumnDef::new(MessageSubscriptions::DndStart).string().null()) + .col( + ColumnDef::new(MessageSubscriptions::DndStart) + .string() + .null(), + ) .col(ColumnDef::new(MessageSubscriptions::DndEnd).string().null()) - .col(ColumnDef::new(MessageSubscriptions::CreatedAt).timestamp_with_time_zone().not_null()) - .col(ColumnDef::new(MessageSubscriptions::UpdatedAt).timestamp_with_time_zone().not_null()) - .col(ColumnDef::new(MessageSubscriptions::CreatedBy).uuid().not_null()) - .col(ColumnDef::new(MessageSubscriptions::UpdatedBy).uuid().not_null()) - .col(ColumnDef::new(MessageSubscriptions::DeletedAt).timestamp_with_time_zone().null()) + .col( + ColumnDef::new(MessageSubscriptions::CreatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .col( + ColumnDef::new(MessageSubscriptions::UpdatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .col( + ColumnDef::new(MessageSubscriptions::CreatedBy) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(MessageSubscriptions::UpdatedBy) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(MessageSubscriptions::DeletedAt) + .timestamp_with_time_zone() + .null(), + ) .to_owned(), ) .await?; diff --git a/crates/erp-server/migration/src/m20260413_000026_create_audit_logs.rs b/crates/erp-server/migration/src/m20260413_000026_create_audit_logs.rs index b604ca8..4aed077 100644 --- a/crates/erp-server/migration/src/m20260413_000026_create_audit_logs.rs +++ b/crates/erp-server/migration/src/m20260413_000026_create_audit_logs.rs @@ -26,20 +26,33 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(AuditLogs::NewValue).json().null()) .col(ColumnDef::new(AuditLogs::IpAddress).string().null()) .col(ColumnDef::new(AuditLogs::UserAgent).text().null()) - .col(ColumnDef::new(AuditLogs::CreatedAt).timestamp_with_time_zone().not_null()) + .col( + ColumnDef::new(AuditLogs::CreatedAt) + .timestamp_with_time_zone() + .not_null(), + ) .to_owned(), ) .await?; - manager.get_connection().execute(sea_orm::Statement::from_string( - sea_orm::DatabaseBackend::Postgres, - "CREATE INDEX idx_audit_logs_tenant ON audit_logs (tenant_id)".to_string(), - )).await.map_err(|e| DbErr::Custom(e.to_string()))?; + manager + .get_connection() + .execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + "CREATE INDEX idx_audit_logs_tenant ON audit_logs (tenant_id)".to_string(), + )) + .await + .map_err(|e| DbErr::Custom(e.to_string()))?; - manager.get_connection().execute(sea_orm::Statement::from_string( - sea_orm::DatabaseBackend::Postgres, - "CREATE INDEX idx_audit_logs_resource ON audit_logs (resource_type, resource_id)".to_string(), - )).await.map_err(|e| DbErr::Custom(e.to_string()))?; + manager + .get_connection() + .execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + "CREATE INDEX idx_audit_logs_resource ON audit_logs (resource_type, resource_id)" + .to_string(), + )) + .await + .map_err(|e| DbErr::Custom(e.to_string()))?; Ok(()) } diff --git a/crates/erp-server/migration/src/m20260414_000027_fix_unique_indexes_soft_delete.rs b/crates/erp-server/migration/src/m20260414_000027_fix_unique_indexes_soft_delete.rs index fd052b3..f343424 100644 --- a/crates/erp-server/migration/src/m20260414_000027_fix_unique_indexes_soft_delete.rs +++ b/crates/erp-server/migration/src/m20260414_000027_fix_unique_indexes_soft_delete.rs @@ -1,6 +1,6 @@ -use sea_orm_migration::prelude::*; -use sea_orm::Statement; use sea_orm::DatabaseBackend; +use sea_orm::Statement; +use sea_orm_migration::prelude::*; /// Recreate unique indexes on roles and permissions to include soft-delete awareness. #[derive(DeriveMigrationName)] diff --git a/crates/erp-server/migration/src/m20260414_000032_fix_settings_unique_index_null.rs b/crates/erp-server/migration/src/m20260414_000032_fix_settings_unique_index_null.rs new file mode 100644 index 0000000..b86d845 --- /dev/null +++ b/crates/erp-server/migration/src/m20260414_000032_fix_settings_unique_index_null.rs @@ -0,0 +1,65 @@ +use sea_orm_migration::prelude::*; + +/// 修复 settings 表唯一索引:原索引使用 scope_id 列,当 scope_id 为 NULL 时 +/// PostgreSQL B-tree 不认为两行重复(NULL != NULL),导致可插入重复数据。 +/// 修复方案:使用 COALESCE(scope_id, '00000000-0000-0000-0000-000000000000') +/// 将 NULL 转为固定 UUID,使索引能正确约束 NULL scope_id 的行。 +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // 删除旧索引 + manager + .get_connection() + .execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + "DROP INDEX IF EXISTS idx_settings_scope_key".to_string(), + )) + .await + .map_err(|e| DbErr::Custom(e.to_string()))?; + + // 创建新索引,使用 COALESCE 处理 NULL scope_id + manager.get_connection().execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + "CREATE UNIQUE INDEX idx_settings_scope_key ON settings (tenant_id, scope, COALESCE(scope_id, '00000000-0000-0000-0000-000000000000'), setting_key) WHERE deleted_at IS NULL".to_string(), + )).await.map_err(|e| DbErr::Custom(e.to_string()))?; + + // 清理可能已存在的重复数据(保留每组最新的一条) + manager.get_connection().execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + r#" + DELETE FROM settings a USING settings b + WHERE a.id < b.id + AND a.tenant_id = b.tenant_id + AND a.scope = b.scope + AND a.setting_key = b.setting_key + AND a.deleted_at IS NULL + AND b.deleted_at IS NULL + AND COALESCE(a.scope_id, '00000000-0000-0000-0000-000000000000') = COALESCE(b.scope_id, '00000000-0000-0000-0000-000000000000') + "#.to_string(), + )).await.map_err(|e| DbErr::Custom(e.to_string()))?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // 回滚:删除新索引,恢复旧索引 + manager + .get_connection() + .execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + "DROP INDEX IF EXISTS idx_settings_scope_key".to_string(), + )) + .await + .map_err(|e| DbErr::Custom(e.to_string()))?; + + manager.get_connection().execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + "CREATE UNIQUE INDEX idx_settings_scope_key ON settings (tenant_id, scope, scope_id, setting_key) WHERE deleted_at IS NULL".to_string(), + )).await.map_err(|e| DbErr::Custom(e.to_string()))?; + + Ok(()) + } +} diff --git a/crates/erp-server/migration/src/m20260416_000031_create_domain_events.rs b/crates/erp-server/migration/src/m20260416_000031_create_domain_events.rs index 6c35a60..3a0c6ba 100644 --- a/crates/erp-server/migration/src/m20260416_000031_create_domain_events.rs +++ b/crates/erp-server/migration/src/m20260416_000031_create_domain_events.rs @@ -18,7 +18,11 @@ impl MigrationTrait for Migration { .primary_key(), ) .col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null()) - .col(ColumnDef::new(Alias::new("event_type")).string_len(200).not_null()) + .col( + ColumnDef::new(Alias::new("event_type")) + .string_len(200) + .not_null(), + ) .col(ColumnDef::new(Alias::new("payload")).json().null()) .col(ColumnDef::new(Alias::new("correlation_id")).uuid().null()) .col( diff --git a/crates/erp-server/src/handlers/audit_log.rs b/crates/erp-server/src/handlers/audit_log.rs index 2fc34e5..f507c2c 100644 --- a/crates/erp-server/src/handlers/audit_log.rs +++ b/crates/erp-server/src/handlers/audit_log.rs @@ -1,7 +1,7 @@ +use axum::Router; use axum::extract::{Extension, FromRef, Query, State}; use axum::response::Json; use axum::routing::get; -use axum::Router; use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder}; use serde::Deserialize; @@ -35,8 +35,7 @@ where let page_size = params.page_size.unwrap_or(20).min(100); let tenant_id = ctx.tenant_id; - let mut q = audit_log::Entity::find() - .filter(audit_log::Column::TenantId.eq(tenant_id)); + let mut q = audit_log::Entity::find().filter(audit_log::Column::TenantId.eq(tenant_id)); if let Some(rt) = ¶ms.resource_type { q = q.filter(audit_log::Column::ResourceType.eq(rt.clone())); diff --git a/crates/erp-server/src/handlers/health.rs b/crates/erp-server/src/handlers/health.rs index 4af07ea..bac8079 100644 --- a/crates/erp-server/src/handlers/health.rs +++ b/crates/erp-server/src/handlers/health.rs @@ -1,7 +1,7 @@ +use axum::Router; use axum::extract::State; use axum::response::Json; use axum::routing::get; -use axum::Router; use serde::Serialize; use crate::state::AppState; diff --git a/crates/erp-server/src/handlers/openapi.rs b/crates/erp-server/src/handlers/openapi.rs index 66ea668..1eba8c4 100644 --- a/crates/erp-server/src/handlers/openapi.rs +++ b/crates/erp-server/src/handlers/openapi.rs @@ -6,14 +6,9 @@ use utoipa::openapi::OpenApiBuilder; /// /// 返回 OpenAPI 3.0 规范 JSON 文档 pub async fn openapi_spec() -> Json { - let mut info = utoipa::openapi::Info::new( - "ERP Platform API", - env!("CARGO_PKG_VERSION"), - ); + let mut info = utoipa::openapi::Info::new("ERP Platform API", env!("CARGO_PKG_VERSION")); info.description = Some("ERP 平台底座 REST API 文档".to_string()); - let spec = OpenApiBuilder::new() - .info(info) - .build(); + let spec = OpenApiBuilder::new().info(info).build(); Json(serde_json::to_value(spec).unwrap_or_default()) } diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index 1bfce17..4c1256e 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -7,13 +7,11 @@ mod state; /// OpenAPI 规范定义(预留,未来可通过 utoipa derive 合并各模块 schema)。 #[derive(OpenApi)] -#[openapi( - info( - title = "ERP Platform API", - version = "0.1.0", - description = "ERP 平台底座 REST API 文档" - ) -)] +#[openapi(info( + title = "ERP Platform API", + version = "0.1.0", + description = "ERP 平台底座 REST API 文档" +))] #[allow(dead_code)] struct ApiDoc; @@ -38,13 +36,15 @@ async fn main() -> anyhow::Result<()> { // Initialize tracing tracing_subscriber::fmt() .with_env_filter( - EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new(&config.log.level)), + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log.level)), ) .json() .init(); - tracing::info!(version = env!("CARGO_PKG_VERSION"), "ERP Server starting..."); + tracing::info!( + version = env!("CARGO_PKG_VERSION"), + "ERP Server starting..." + ); // Connect to database let db = db::connect(&config.database).await?; @@ -116,19 +116,35 @@ async fn main() -> anyhow::Result<()> { // Initialize auth module let auth_module = erp_auth::AuthModule::new(); - tracing::info!(module = auth_module.name(), version = auth_module.version(), "Auth module initialized"); + tracing::info!( + module = auth_module.name(), + version = auth_module.version(), + "Auth module initialized" + ); // Initialize config module let config_module = erp_config::ConfigModule::new(); - tracing::info!(module = config_module.name(), version = config_module.version(), "Config module initialized"); + tracing::info!( + module = config_module.name(), + version = config_module.version(), + "Config module initialized" + ); // Initialize workflow module let workflow_module = erp_workflow::WorkflowModule::new(); - tracing::info!(module = workflow_module.name(), version = workflow_module.version(), "Workflow module initialized"); + tracing::info!( + module = workflow_module.name(), + version = workflow_module.version(), + "Workflow module initialized" + ); // Initialize message module let message_module = erp_message::MessageModule::new(); - tracing::info!(module = message_module.name(), version = message_module.version(), "Message module initialized"); + tracing::info!( + module = message_module.name(), + version = message_module.version(), + "Message module initialized" + ); // Initialize module registry and register modules let registry = ModuleRegistry::new() @@ -136,7 +152,10 @@ async fn main() -> anyhow::Result<()> { .register(config_module) .register(workflow_module) .register(message_module); - tracing::info!(module_count = registry.modules().len(), "Modules registered"); + tracing::info!( + module_count = registry.modules().len(), + "Modules registered" + ); // Register event handlers registry.register_handlers(&event_bus); @@ -182,7 +201,10 @@ async fn main() -> anyhow::Result<()> { let public_routes = Router::new() .merge(handlers::health::health_check_router()) .merge(erp_auth::AuthModule::public_routes()) - .route("/docs/openapi.json", axum::routing::get(handlers::openapi::openapi_spec)) + .route( + "/docs/openapi.json", + axum::routing::get(handlers::openapi::openapi_spec), + ) .layer(axum::middleware::from_fn_with_state( state.clone(), middleware::rate_limit::rate_limit_by_ip, diff --git a/crates/erp-server/src/middleware/rate_limit.rs b/crates/erp-server/src/middleware/rate_limit.rs index 118e81e..2715177 100644 --- a/crates/erp-server/src/middleware/rate_limit.rs +++ b/crates/erp-server/src/middleware/rate_limit.rs @@ -15,7 +15,8 @@ struct RateLimitResponse { message: String, } -/// 限流参数。 +/// 限流参数(预留配置化扩展)。 +#[allow(dead_code)] pub struct RateLimitConfig { /// 窗口内最大请求数。 pub max_requests: u64, @@ -74,11 +75,7 @@ async fn apply_rate_limit( } }; - let count: i64 = match redis::cmd("INCR") - .arg(&key) - .query_async(&mut conn) - .await - { + let count: i64 = match redis::cmd("INCR").arg(&key).query_async(&mut conn).await { Ok(n) => n, Err(e) => { tracing::warn!(error = %e, "Redis INCR 失败,跳过限流"); diff --git a/crates/erp-workflow/src/engine/executor.rs b/crates/erp-workflow/src/engine/executor.rs index 0567abc..c85231b 100644 --- a/crates/erp-workflow/src/engine/executor.rs +++ b/crates/erp-workflow/src/engine/executor.rs @@ -2,15 +2,14 @@ use std::collections::HashMap; use chrono::Utc; use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, ConnectionTrait, - PaginatorTrait, + ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, }; use uuid::Uuid; use crate::dto::NodeType; use crate::engine::expression::ExpressionEvaluator; use crate::engine::model::FlowGraph; -use crate::entity::{token, process_instance, task}; +use crate::entity::{process_instance, task, token}; use crate::error::{WorkflowError, WorkflowResult}; /// Token 驱动的流程执行引擎。 @@ -92,11 +91,16 @@ impl FlowExecutor { let mut active: token::ActiveModel = current_token.into(); active.status = Set("consumed".to_string()); active.consumed_at = Set(Some(Utc::now())); - active.update(txn).await.map_err(|e| WorkflowError::Validation(e.to_string()))?; + active + .update(txn) + .await + .map_err(|e| WorkflowError::Validation(e.to_string()))?; // 获取当前节点的出边 let outgoing = graph.get_outgoing_edges(&node_id); - let current_node = graph.nodes.get(&node_id) + let current_node = graph + .nodes + .get(&node_id) .ok_or_else(|| WorkflowError::InvalidDiagram(format!("节点不存在: {node_id}")))?; match current_node.node_type { @@ -177,11 +181,9 @@ impl FlowExecutor { } } - let target = matched_target - .or(default_target) - .ok_or_else(|| WorkflowError::ExpressionError( - "排他网关没有匹配的条件分支".to_string(), - ))?; + let target = matched_target.or(default_target).ok_or_else(|| { + WorkflowError::ExpressionError("排他网关没有匹配的条件分支".to_string()) + })?; Self::create_token_at_node(instance_id, tenant_id, target, graph, variables, txn).await } @@ -219,136 +221,139 @@ impl FlowExecutor { graph: &'a FlowGraph, variables: &'a HashMap, txn: &'a impl ConnectionTrait, - ) -> std::pin::Pin>> + Send + 'a>> { + ) -> std::pin::Pin>> + Send + 'a>> + { Box::pin(async move { - let node = graph.nodes.get(node_id) - .ok_or_else(|| WorkflowError::InvalidDiagram(format!("节点不存在: {node_id}")))?; + let node = graph + .nodes + .get(node_id) + .ok_or_else(|| WorkflowError::InvalidDiagram(format!("节点不存在: {node_id}")))?; - match node.node_type { - NodeType::EndEvent => { - // 到达 EndEvent,不创建新 token - // 检查实例是否所有 token 都完成 - Self::check_instance_completion(instance_id, tenant_id, txn).await?; - Ok(vec![]) - } - NodeType::ParallelGateway - if Self::is_join_gateway(node_id, graph) => - { - // 并行网关汇合:等待所有入边 token 到达 - Self::handle_join_gateway( - instance_id, - tenant_id, - node_id, - graph, - variables, - txn, - ) - .await - } - NodeType::ServiceTask => { - // ServiceTask 自动执行:当前阶段自动跳过(直接推进到后继节点) - // 创建一个立即消费的 token 记录(用于审计追踪) - let now = Utc::now(); - let system_user = uuid::Uuid::nil(); - let auto_token_id = Uuid::now_v7(); - - let token_model = token::ActiveModel { - id: Set(auto_token_id), - tenant_id: Set(tenant_id), - instance_id: Set(instance_id), - node_id: Set(node_id.to_string()), - status: Set("consumed".to_string()), - created_at: Set(now), - updated_at: Set(now), - created_by: Set(system_user), - updated_by: Set(system_user), - deleted_at: Set(None), - version: Set(1), - consumed_at: Set(Some(now)), - }; - token_model - .insert(txn) - .await - .map_err(|e| WorkflowError::Validation(e.to_string()))?; - - tracing::info!(node_id = node_id, node_name = %node.name, "ServiceTask 自动跳过(尚未实现 HTTP 调用)"); - - // 沿出边继续推进 - let outgoing = graph.get_outgoing_edges(node_id); - let mut new_tokens = Vec::new(); - for edge in &outgoing { - let tokens = Self::create_token_at_node( + match node.node_type { + NodeType::EndEvent => { + // 到达 EndEvent,不创建新 token + // 检查实例是否所有 token 都完成 + Self::check_instance_completion(instance_id, tenant_id, txn).await?; + Ok(vec![]) + } + NodeType::ParallelGateway if Self::is_join_gateway(node_id, graph) => { + // 并行网关汇合:等待所有入边 token 到达 + Self::handle_join_gateway( instance_id, tenant_id, - &edge.target, + node_id, graph, variables, txn, ) - .await?; - new_tokens.extend(tokens); - } - Ok(new_tokens) - } - _ => { - // UserTask / 网关(分支)等:创建活跃 token - let new_token_id = Uuid::now_v7(); - let now = Utc::now(); - - let system_user = uuid::Uuid::nil(); - - let token_model = token::ActiveModel { - id: Set(new_token_id), - tenant_id: Set(tenant_id), - instance_id: Set(instance_id), - node_id: Set(node_id.to_string()), - status: Set("active".to_string()), - created_at: Set(now), - updated_at: Set(now), - created_by: Set(system_user), - updated_by: Set(system_user), - deleted_at: Set(None), - version: Set(1), - consumed_at: Set(None), - }; - token_model - .insert(txn) .await - .map_err(|e| WorkflowError::Validation(e.to_string()))?; + } + NodeType::ServiceTask => { + // ServiceTask 自动执行:当前阶段自动跳过(直接推进到后继节点) + // 创建一个立即消费的 token 记录(用于审计追踪) + let now = Utc::now(); + let system_user = uuid::Uuid::nil(); + let auto_token_id = Uuid::now_v7(); - // UserTask: 同时创建 task 记录 - if node.node_type == NodeType::UserTask { - let task_model = task::ActiveModel { - id: Set(Uuid::now_v7()), + let token_model = token::ActiveModel { + id: Set(auto_token_id), tenant_id: Set(tenant_id), instance_id: Set(instance_id), - token_id: Set(new_token_id), node_id: Set(node_id.to_string()), - node_name: Set(Some(node.name.clone())), - assignee_id: Set(node.assignee_id), - candidate_groups: Set(node.candidate_groups.as_ref() - .map(|g| serde_json::to_value(g).unwrap_or_default())), - status: Set("pending".to_string()), - outcome: Set(None), - form_data: Set(None), - due_date: Set(None), - completed_at: Set(None), + status: Set("consumed".to_string()), created_at: Set(now), updated_at: Set(now), - created_by: Set(Uuid::nil()), - updated_by: Set(Uuid::nil()), + created_by: Set(system_user), + updated_by: Set(system_user), deleted_at: Set(None), version: Set(1), + consumed_at: Set(Some(now)), }; - task_model + token_model .insert(txn) .await .map_err(|e| WorkflowError::Validation(e.to_string()))?; - } - Ok(vec![new_token_id]) + tracing::info!(node_id = node_id, node_name = %node.name, "ServiceTask 自动跳过(尚未实现 HTTP 调用)"); + + // 沿出边继续推进 + let outgoing = graph.get_outgoing_edges(node_id); + let mut new_tokens = Vec::new(); + for edge in &outgoing { + let tokens = Self::create_token_at_node( + instance_id, + tenant_id, + &edge.target, + graph, + variables, + txn, + ) + .await?; + new_tokens.extend(tokens); + } + Ok(new_tokens) + } + _ => { + // UserTask / 网关(分支)等:创建活跃 token + let new_token_id = Uuid::now_v7(); + let now = Utc::now(); + + let system_user = uuid::Uuid::nil(); + + let token_model = token::ActiveModel { + id: Set(new_token_id), + tenant_id: Set(tenant_id), + instance_id: Set(instance_id), + node_id: Set(node_id.to_string()), + status: Set("active".to_string()), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(system_user), + updated_by: Set(system_user), + deleted_at: Set(None), + version: Set(1), + consumed_at: Set(None), + }; + token_model + .insert(txn) + .await + .map_err(|e| WorkflowError::Validation(e.to_string()))?; + + // UserTask: 同时创建 task 记录 + if node.node_type == NodeType::UserTask { + let task_model = task::ActiveModel { + id: Set(Uuid::now_v7()), + tenant_id: Set(tenant_id), + instance_id: Set(instance_id), + token_id: Set(new_token_id), + node_id: Set(node_id.to_string()), + node_name: Set(Some(node.name.clone())), + assignee_id: Set(node.assignee_id), + candidate_groups: Set(node + .candidate_groups + .as_ref() + .map(|g| serde_json::to_value(g).unwrap_or_default())), + status: Set("pending".to_string()), + outcome: Set(None), + form_data: Set(None), + due_date: Set(None), + completed_at: Set(None), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(Uuid::nil()), + updated_by: Set(Uuid::nil()), + deleted_at: Set(None), + version: Set(1), + }; + task_model + .insert(txn) + .await + .map_err(|e| WorkflowError::Validation(e.to_string()))?; + } + + Ok(vec![new_token_id]) + } } - } }) } @@ -445,7 +450,10 @@ impl FlowExecutor { active.status = Set("completed".to_string()); active.completed_at = Set(Some(Utc::now())); active.updated_at = Set(Utc::now()); - active.update(txn).await.map_err(|e| WorkflowError::Validation(e.to_string()))?; + active + .update(txn) + .await + .map_err(|e| WorkflowError::Validation(e.to_string()))?; // 写入完成事件到 outbox,由 relay 广播 let now = Utc::now(); @@ -461,7 +469,10 @@ impl FlowExecutor { created_at: Set(now), published_at: Set(None), }; - outbox_event.insert(txn).await.map_err(|e| WorkflowError::Validation(e.to_string()))?; + outbox_event + .insert(txn) + .await + .map_err(|e| WorkflowError::Validation(e.to_string()))?; } Ok(()) diff --git a/crates/erp-workflow/src/engine/expression.rs b/crates/erp-workflow/src/engine/expression.rs index 0b6ad25..79616ff 100644 --- a/crates/erp-workflow/src/engine/expression.rs +++ b/crates/erp-workflow/src/engine/expression.rs @@ -18,7 +18,10 @@ impl ExpressionEvaluator { /// 求值单个条件表达式。 /// /// 表达式格式: `{left} {op} {right}` 或复合表达式 `{expr1} && {expr2}` - pub fn eval(expr: &str, variables: &HashMap) -> WorkflowResult { + pub fn eval( + expr: &str, + variables: &HashMap, + ) -> WorkflowResult { let expr = expr.trim(); // 处理逻辑 OR @@ -72,7 +75,10 @@ impl ExpressionEvaluator { } /// 求值单个比较表达式。 - fn eval_comparison(expr: &str, variables: &HashMap) -> WorkflowResult { + fn eval_comparison( + expr: &str, + variables: &HashMap, + ) -> WorkflowResult { let operators = [">=", "<=", "!=", "==", ">", "<"]; for op in &operators { diff --git a/crates/erp-workflow/src/engine/mod.rs b/crates/erp-workflow/src/engine/mod.rs index 6bbe114..6d9b996 100644 --- a/crates/erp-workflow/src/engine/mod.rs +++ b/crates/erp-workflow/src/engine/mod.rs @@ -1,5 +1,5 @@ -pub mod expression; pub mod executor; +pub mod expression; pub mod model; pub mod parser; pub mod timeout; diff --git a/crates/erp-workflow/src/engine/model.rs b/crates/erp-workflow/src/engine/model.rs index 98dfd80..69bd2bf 100644 --- a/crates/erp-workflow/src/engine/model.rs +++ b/crates/erp-workflow/src/engine/model.rs @@ -75,13 +75,16 @@ impl FlowGraph { } for e in edges { - graph.edges.insert(e.id.clone(), FlowEdge { - id: e.id.clone(), - source: e.source.clone(), - target: e.target.clone(), - condition: e.condition.clone(), - label: e.label.clone(), - }); + graph.edges.insert( + e.id.clone(), + FlowEdge { + id: e.id.clone(), + source: e.source.clone(), + target: e.target.clone(), + condition: e.condition.clone(), + label: e.label.clone(), + }, + ); if let Some(out) = graph.outgoing.get_mut(&e.source) { out.push(e.id.clone()); diff --git a/crates/erp-workflow/src/engine/parser.rs b/crates/erp-workflow/src/engine/parser.rs index d28ea0d..2304606 100644 --- a/crates/erp-workflow/src/engine/parser.rs +++ b/crates/erp-workflow/src/engine/parser.rs @@ -10,7 +10,10 @@ pub fn parse_and_validate(nodes: &[NodeDef], edges: &[EdgeDef]) -> WorkflowResul } // 检查恰好 1 个 StartEvent - let start_count = nodes.iter().filter(|n| n.node_type == NodeType::StartEvent).count(); + let start_count = nodes + .iter() + .filter(|n| n.node_type == NodeType::StartEvent) + .count(); if start_count == 0 { return Err(WorkflowError::InvalidDiagram( "流程图必须包含一个开始事件".to_string(), @@ -23,7 +26,10 @@ pub fn parse_and_validate(nodes: &[NodeDef], edges: &[EdgeDef]) -> WorkflowResul } // 检查至少 1 个 EndEvent - let end_count = nodes.iter().filter(|n| n.node_type == NodeType::EndEvent).count(); + let end_count = nodes + .iter() + .filter(|n| n.node_type == NodeType::EndEvent) + .count(); if end_count == 0 { return Err(WorkflowError::InvalidDiagram( "流程图必须包含至少一个结束事件".to_string(), @@ -31,8 +37,7 @@ pub fn parse_and_validate(nodes: &[NodeDef], edges: &[EdgeDef]) -> WorkflowResul } // 检查节点 ID 唯一性 - let node_ids: std::collections::HashSet<&str> = - nodes.iter().map(|n| n.id.as_str()).collect(); + let node_ids: std::collections::HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect(); if node_ids.len() != nodes.len() { return Err(WorkflowError::InvalidDiagram( "节点 ID 不能重复".to_string(), @@ -101,7 +106,8 @@ pub fn parse_and_validate(nodes: &[NodeDef], edges: &[EdgeDef]) -> WorkflowResul } // 排他网关的出边应该有条件(第一条可以无条件作为默认分支) if node.node_type == NodeType::ExclusiveGateway && out.len() > 1 { - let with_condition: Vec<_> = out.iter().filter(|e| e.condition.is_some()).collect(); + let with_condition: Vec<_> = + out.iter().filter(|e| e.condition.is_some()).collect(); if with_condition.is_empty() { return Err(WorkflowError::InvalidDiagram(format!( "排他网关 '{}' 有多条出边但没有条件表达式", diff --git a/crates/erp-workflow/src/entity/mod.rs b/crates/erp-workflow/src/entity/mod.rs index c268c74..410e4fd 100644 --- a/crates/erp-workflow/src/entity/mod.rs +++ b/crates/erp-workflow/src/entity/mod.rs @@ -1,5 +1,5 @@ pub mod process_definition; pub mod process_instance; -pub mod token; -pub mod task; pub mod process_variable; +pub mod task; +pub mod token; diff --git a/crates/erp-workflow/src/handler/definition_handler.rs b/crates/erp-workflow/src/handler/definition_handler.rs index 258b484..a195a05 100644 --- a/crates/erp-workflow/src/handler/definition_handler.rs +++ b/crates/erp-workflow/src/handler/definition_handler.rs @@ -94,8 +94,7 @@ where { require_permission(&ctx, "workflow.update")?; - let resp = - DefinitionService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; + let resp = DefinitionService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; Ok(Json(ApiResponse::ok(resp))) } @@ -111,14 +110,9 @@ where { require_permission(&ctx, "workflow.publish")?; - let resp = DefinitionService::publish( - id, - ctx.tenant_id, - ctx.user_id, - &state.db, - &state.event_bus, - ) - .await?; + let resp = + DefinitionService::publish(id, ctx.tenant_id, ctx.user_id, &state.db, &state.event_bus) + .await?; Ok(Json(ApiResponse::ok(resp))) } diff --git a/crates/erp-workflow/src/handler/instance_handler.rs b/crates/erp-workflow/src/handler/instance_handler.rs index 78902ab..dc79f4c 100644 --- a/crates/erp-workflow/src/handler/instance_handler.rs +++ b/crates/erp-workflow/src/handler/instance_handler.rs @@ -23,7 +23,8 @@ where S: Clone + Send + Sync + 'static, { require_permission(&ctx, "workflow.start")?; - req.validate().map_err(|e| AppError::Validation(e.to_string()))?; + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; let resp = InstanceService::start( ctx.tenant_id, @@ -49,8 +50,7 @@ where { require_permission(&ctx, "workflow.list")?; - let (instances, total) = - InstanceService::list(ctx.tenant_id, &pagination, &state.db).await?; + let (instances, total) = InstanceService::list(ctx.tenant_id, &pagination, &state.db).await?; let page = pagination.page.unwrap_or(1); let page_size = pagination.limit(); diff --git a/crates/erp-workflow/src/handler/task_handler.rs b/crates/erp-workflow/src/handler/task_handler.rs index 2eb0670..277520c 100644 --- a/crates/erp-workflow/src/handler/task_handler.rs +++ b/crates/erp-workflow/src/handler/task_handler.rs @@ -80,7 +80,8 @@ where S: Clone + Send + Sync + 'static, { require_permission(&ctx, "workflow.approve")?; - req.validate().map_err(|e| AppError::Validation(e.to_string()))?; + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; let resp = TaskService::complete( id, @@ -107,10 +108,10 @@ where S: Clone + Send + Sync + 'static, { require_permission(&ctx, "workflow.delegate")?; - req.validate().map_err(|e| AppError::Validation(e.to_string()))?; + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; - let resp = - TaskService::delegate(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; + let resp = TaskService::delegate(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; Ok(Json(ApiResponse::ok(resp))) } diff --git a/crates/erp-workflow/src/module.rs b/crates/erp-workflow/src/module.rs index f9c603e..029b34c 100644 --- a/crates/erp-workflow/src/module.rs +++ b/crates/erp-workflow/src/module.rs @@ -7,9 +7,7 @@ use erp_core::error::AppResult; use erp_core::events::EventBus; use erp_core::module::ErpModule; -use crate::handler::{ - definition_handler, instance_handler, task_handler, -}; +use crate::handler::{definition_handler, instance_handler, task_handler}; /// Workflow module implementing the `ErpModule` trait. /// @@ -37,8 +35,7 @@ impl WorkflowModule { ) .route( "/workflow/definitions/{id}", - get(definition_handler::get_definition) - .put(definition_handler::update_definition), + get(definition_handler::get_definition).put(definition_handler::update_definition), ) .route( "/workflow/definitions/{id}/publish", @@ -47,8 +44,7 @@ impl WorkflowModule { // Instance routes .route( "/workflow/instances", - post(instance_handler::start_instance) - .get(instance_handler::list_instances), + post(instance_handler::start_instance).get(instance_handler::list_instances), ) .route( "/workflow/instances/{id}", diff --git a/crates/erp-workflow/src/service/definition_service.rs b/crates/erp-workflow/src/service/definition_service.rs index b254cf4..a63fd95 100644 --- a/crates/erp-workflow/src/service/definition_service.rs +++ b/crates/erp-workflow/src/service/definition_service.rs @@ -1,12 +1,8 @@ use chrono::Utc; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, -}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; use uuid::Uuid; -use crate::dto::{ - CreateProcessDefinitionReq, ProcessDefinitionResp, UpdateProcessDefinitionReq, -}; +use crate::dto::{CreateProcessDefinitionReq, ProcessDefinitionResp, UpdateProcessDefinitionReq}; use crate::engine::parser; use crate::entity::process_definition; use crate::error::{WorkflowError, WorkflowResult}; @@ -103,15 +99,25 @@ impl DefinitionService { .await .map_err(|e| WorkflowError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "process_definition.created", - tenant_id, - serde_json::json!({ "definition_id": id, "key": req.key }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "process_definition.created", + tenant_id, + serde_json::json!({ "definition_id": id, "key": req.key }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "process_definition.create", "process_definition") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "process_definition.create", + "process_definition", + ) + .with_resource_id(id), db, ) .await; @@ -192,8 +198,13 @@ impl DefinitionService { .map_err(|e| WorkflowError::Validation(e.to_string()))?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "process_definition.update", "process_definition") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "process_definition.update", + "process_definition", + ) + .with_resource_id(id), db, ) .await; @@ -241,15 +252,25 @@ impl DefinitionService { .await .map_err(|e| WorkflowError::Validation(e.to_string()))?; - event_bus.publish(erp_core::events::DomainEvent::new( - "process_definition.published", - tenant_id, - serde_json::json!({ "definition_id": id }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "process_definition.published", + tenant_id, + serde_json::json!({ "definition_id": id }), + ), + db, + ) + .await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "process_definition.publish", "process_definition") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "process_definition.publish", + "process_definition", + ) + .with_resource_id(id), db, ) .await; @@ -283,8 +304,13 @@ impl DefinitionService { .map_err(|e| WorkflowError::Validation(e.to_string()))?; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "process_definition.delete", "process_definition") - .with_resource_id(id), + AuditLog::new( + tenant_id, + Some(operator_id), + "process_definition.delete", + "process_definition", + ) + .with_resource_id(id), db, ) .await; diff --git a/crates/erp-workflow/src/service/instance_service.rs b/crates/erp-workflow/src/service/instance_service.rs index 1cb5e0b..dc4dc09 100644 --- a/crates/erp-workflow/src/service/instance_service.rs +++ b/crates/erp-workflow/src/service/instance_service.rs @@ -2,8 +2,8 @@ use std::collections::HashMap; use chrono::Utc; use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, - TransactionTrait, ConnectionTrait, + ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, + TransactionTrait, }; use uuid::Uuid; @@ -94,7 +94,10 @@ impl InstanceService { deleted_at: Set(None), version: Set(1), }; - instance.insert(txn).await.map_err(|e| WorkflowError::Validation(e.to_string()))?; + instance + .insert(txn) + .await + .map_err(|e| WorkflowError::Validation(e.to_string()))?; // 保存初始变量 if let Some(vars) = vars_to_save { @@ -112,14 +115,8 @@ impl InstanceService { } // 启动执行引擎 - FlowExecutor::start( - instance_id_clone, - tenant_id_clone, - &graph, - &variables, - txn, - ) - .await?; + FlowExecutor::start(instance_id_clone, tenant_id_clone, &graph, &variables, txn) + .await?; Ok(()) }) @@ -133,8 +130,13 @@ impl InstanceService { ), db).await; audit_service::record( - AuditLog::new(tenant_id, Some(operator_id), "process_instance.start", "process_instance") - .with_resource_id(instance_id), + AuditLog::new( + tenant_id, + Some(operator_id), + "process_instance.start", + "process_instance", + ) + .with_resource_id(instance_id), db, ) .await; @@ -320,7 +322,9 @@ impl InstanceService { id: Set(event_id), tenant_id: Set(tenant_id), event_type: Set(event_type), - payload: Set(Some(serde_json::json!({ "instance_id": id, "changed_by": operator_id }))), + payload: Set(Some( + serde_json::json!({ "instance_id": id, "changed_by": operator_id }), + )), correlation_id: Set(Some(Uuid::now_v7())), status: Set("pending".to_string()), attempts: Set(0), @@ -378,7 +382,12 @@ impl InstanceService { ) -> WorkflowResult<()> { let id = Uuid::now_v7(); - let (value_string, value_number, value_boolean, _value_date): (Option, Option, Option, Option>) = match var_type { + let (value_string, value_number, value_boolean, _value_date): ( + Option, + Option, + Option, + Option>, + ) = match var_type { "string" => (value.as_str().map(|s| s.to_string()), None, None, None), "number" => (None, value.as_f64(), None, None), "boolean" => (None, None, value.as_bool(), None), diff --git a/crates/erp-workflow/src/service/task_service.rs b/crates/erp-workflow/src/service/task_service.rs index e8615f8..cb4cc66 100644 --- a/crates/erp-workflow/src/service/task_service.rs +++ b/crates/erp-workflow/src/service/task_service.rs @@ -2,8 +2,8 @@ use std::collections::HashMap; use chrono::Utc; use sea_orm::{ - ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseBackend, EntityTrait, - PaginatorTrait, QueryFilter, Set, Statement, TransactionTrait, + ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseBackend, EntityTrait, PaginatorTrait, + QueryFilter, Set, Statement, TransactionTrait, }; use uuid::Uuid; @@ -178,14 +178,10 @@ impl TaskService { ))); } - let nodes: Vec = - serde_json::from_value(definition.nodes.clone()).map_err(|e| { - WorkflowError::InvalidDiagram(format!("节点数据无效: {e}")) - })?; - let edges: Vec = - serde_json::from_value(definition.edges.clone()).map_err(|e| { - WorkflowError::InvalidDiagram(format!("连线数据无效: {e}")) - })?; + let nodes: Vec = serde_json::from_value(definition.nodes.clone()) + .map_err(|e| WorkflowError::InvalidDiagram(format!("节点数据无效: {e}")))?; + let edges: Vec = serde_json::from_value(definition.edges.clone()) + .map_err(|e| WorkflowError::InvalidDiagram(format!("连线数据无效: {e}")))?; let graph = parser::parse_and_validate(&nodes, &edges)?; // 准备变量(从 req.form_data 中提取) @@ -223,31 +219,29 @@ impl TaskService { .map_err(|e| WorkflowError::Validation(e.to_string()))?; // 推进 token - FlowExecutor::advance( - token_id, - instance_id, - tenant_id, - &graph, - &variables, - txn, - ) - .await?; + FlowExecutor::advance(token_id, instance_id, tenant_id, &graph, &variables, txn) + .await?; Ok(()) }) }) .await?; - event_bus.publish(erp_core::events::DomainEvent::new( - "task.completed", - tenant_id, - serde_json::json!({ - "task_id": id, - "instance_id": instance_id, - "started_by": instance.started_by, - "outcome": req.outcome, - }), - ), db).await; + event_bus + .publish( + erp_core::events::DomainEvent::new( + "task.completed", + tenant_id, + serde_json::json!({ + "task_id": id, + "instance_id": instance_id, + "started_by": instance.started_by, + "outcome": req.outcome, + }), + ), + db, + ) + .await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "task.complete", "task") @@ -359,7 +353,9 @@ impl TaskService { node_id: Set(node_id.to_string()), node_name: Set(node_name.map(|s| s.to_string())), assignee_id: Set(assignee_id), - candidate_groups: Set(candidate_groups.map(|g| serde_json::to_value(g).unwrap_or_default())), + candidate_groups: Set( + candidate_groups.map(|g| serde_json::to_value(g).unwrap_or_default()) + ), status: Set("pending".to_string()), outcome: Set(None), form_data: Set(None), diff --git a/wiki/architecture.md b/wiki/architecture.md index 415a54a..b7d4791 100644 --- a/wiki/architecture.md +++ b/wiki/architecture.md @@ -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]]** — 插件扩展架构的实现 diff --git a/wiki/erp-core.md b/wiki/erp-core.md index 4559297..f8b0e06 100644 --- a/wiki/erp-core.md +++ b/wiki/erp-core.md @@ -49,6 +49,7 @@ - **[[erp-message]]** — 实现 `ErpModule` trait,订阅通知事件 - **[[erp-config]]** — 实现 `ErpModule` trait - **[[database]]** — 迁移表结构必须与 `BaseFields` 对齐 +- **[[wasm-plugin]]** — WASM 插件通过 Host Bridge 桥接 EventBus ## 关键文件 diff --git a/wiki/frontend.md b/wiki/frontend.md index ef0f348..dccc03d 100644 --- a/wiki/frontend.md +++ b/wiki/frontend.md @@ -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) ``` ### 当前状态 diff --git a/wiki/index.md b/wiki/index.md index ffe3f06..e35a940 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -5,12 +5,13 @@ **模块化 SaaS ERP 底座**,Rust + React 技术栈,提供身份权限/工作流/消息/配置四大基础模块,支持行业业务模块快速插接。 关键数字: -- 8 个 Rust crate(全部已实现),1 个前端 SPA -- 29 个数据库迁移 +- 10 个 Rust crate(8 个已实现 + 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` | diff --git a/wiki/infrastructure.md b/wiki/infrastructure.md index 9909896..d1c8c69 100644 --- a/wiki/infrastructure.md +++ b/wiki/infrastructure.md @@ -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 ``` diff --git a/wiki/testing.md b/wiki/testing.md new file mode 100644 index 0000000..de07fa1 --- /dev/null +++ b/wiki/testing.md @@ -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 -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]] — 后端服务配置 diff --git a/wiki/wasm-plugin.md b/wiki/wasm-plugin.md new file mode 100644 index 0000000..2db8a4a --- /dev/null +++ b/wiki/wasm-plugin.md @@ -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) -> result, string>; + db-query: func(entity: string, filter: list, pagination: list) -> result, string>; + db-update: func(entity, id: string, data: list, version: s64) -> result, string>; + db-delete: func(entity: string, id: string) -> result<_, string>; + event-publish: func(event-type: string, payload: list) -> result<_, string>; + config-get: func(key: string) -> result, string>; + log-write: func(level: string, message: string); + current-user: func() -> result, string>; + check-permission: func(permission: string) -> result; +} + +// 插件导出的 API(Host 调用) +interface plugin-api { + init: func() -> result<_, string>; + on-tenant-created: func(tenant-id: string) -> result<_, string>; + handle-event: func(event-type: string, payload: list) -> result<_, string>; +} + +world plugin-world { + import host-api; + export plugin-api; +} +``` + +## 关键技术要点 + +### HasSelf — Linker 注册模式 + +当 Store 数据类型(`HostState`)直接实现 `Host` trait 时,使用 `HasSelf` 作为 `add_to_linker` 的类型参数: + +```rust +use wasmtime::component::{HasSelf, Linker}; + +let mut linker = Linker::new(engine); +PluginWorld::add_to_linker::<_, HasSelf>(&mut linker, |state| state)?; +``` + +`HasSelf` 表示 `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 --target wasm32-unknown-unknown --release + +# 2. 转换为 component +wasm-tools component new target/wasm32-unknown-unknown/release/.wasm \ + -o target/.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) -> result; +http-proxy: func(url: string, method: string, body: option>) -> result, 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 API(bindgen 生成) +use crate::erp::plugin::host_api; +// 导入 Guest trait(bindgen 生成) +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) -> 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` | 插入记录,Host 自动注入 id/tenant_id/timestamp | +| `db_query` | `(entity, filter, pagination) → result` | 查询记录,自动过滤 tenant_id + 排除软删除 | +| `db_update` | `(entity, id, data, version) → result` | 更新记录,检查乐观锁 version | +| `db_delete` | `(entity, id) → result<_, string>` | 软删除记录 | +| `event_publish` | `(event_type, payload) → result<_, string>` | 发布领域事件 | +| `config_get` | `(key) → result` | 读取系统配置 | +| `log_write` | `(level, message) → ()` | 写日志,自动关联 tenant_id + plugin_id | +| `current_user` | `() → result` | 获取当前用户信息 | +| `check_permission` | `(permission) → result` | 检查当前用户权限 | + +### 数据传递约定 + +所有 Host API 的数据参数使用 `list`(即 `Vec`),约定用 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 -o + ↓ +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`:`add_to_linker::<_, HasSelf>(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 页面 +- **插件市场**: 插件元数据、版本管理、签名验证