feat(health): 添加 erp-health 健康管理模块骨架
新建 erp-health 原生 Rust crate,覆盖设计规格中定义的 5 大业务域: - 16 个 SeaORM Entity(患者/家属/标签/医生/健康档案/体征/化验单/预约/排班/随访/咨询等) - 16 表数据库迁移(含索引、外键、默认值、可回滚) - 40+ API 路由骨架(患者管理/健康数据/预约排班/随访/咨询/医生管理) - 12 个权限声明(health.patient/health-data/appointment/follow-up/consultation/doctor 各 .list/.manage) - DTO / Service / Handler / Event 四层架构,Service 使用 todo!() 占位 - erp-server 集成:模块注册 + AppState FromRef 桥接 + 路由挂载 同步更新 CLAUDE.md 项目进度、wiki 知识库、设计规格文档。
This commit is contained in:
@@ -1,125 +1,102 @@
|
||||
# architecture (架构决策记录)
|
||||
---
|
||||
title: 架构决策记录
|
||||
updated: 2026-04-23
|
||||
status: stable
|
||||
tags: [architecture, decisions, design-principles]
|
||||
---
|
||||
|
||||
## 设计思想
|
||||
# 架构决策记录
|
||||
|
||||
ERP 平台采用 **模块化单体 + 渐进式拆分** 架构。核心原则:模块间零直接依赖,所有跨模块通信通过事件总线和 trait 接口。
|
||||
> 从 [[index]] 导航。关联: [[erp-core]] [[erp-server]] [[database]] [[wasm-plugin]] [[erp-health]]
|
||||
|
||||
## 关键架构决策
|
||||
## 1. 设计决策
|
||||
|
||||
### Q: 为什么用模块化单体而非微服务?
|
||||
### 模块化单体 + 渐进式拆分
|
||||
|
||||
**A:** ERP 系统的模块间数据一致性要求高,分布式事务成本大。单体起步,模块边界清晰,未来需要时可按模块拆分为微服务。`ErpModule` trait 天然支持这种渐进式迁移。
|
||||
模块间零直接依赖,跨模块通信通过事件总线和 trait 接口。`ErpModule` trait 天然支持未来按模块拆分为微服务。
|
||||
|
||||
### Q: 为什么用 UUIDv7 而不是自增 ID?
|
||||
### HMS 架构:原生模块 + 插件并存
|
||||
|
||||
**A:** UUIDv7 是时间排序的,既有 UUID 的分布式唯一性,又有接近自增 ID 的索引性能。对多租户 SaaS 尤其重要——不同租户的数据不会因为 ID 冲突而互相影响。
|
||||
HMS 继承 ERP 底座的所有基础模块,`erp-health` 作为原生 Rust 模块承载医疗业务。WASM 插件系统保留但非 HMS 主要扩展方式。
|
||||
|
||||
### Q: 为什么用 broadcast channel 做事件总线?
|
||||
```
|
||||
HMS 平台
|
||||
├── 基础模块(继承 ERP): auth, config, workflow, message, plugin
|
||||
├── 核心业务模块: erp-health(原生 Rust)★
|
||||
└── 可选插件: crm, inventory, freelance, itops(WASM)
|
||||
```
|
||||
|
||||
**A:** `tokio::sync::broadcast` 提供多消费者发布/订阅,语义匹配模块间事件通知。设计规格要求 outbox 模式(持久化到 domain_events 表),但当前 Phase 1 先用内存 broadcast,后续再补持久化。
|
||||
### 为什么 erp-health 用原生模块?
|
||||
|
||||
### Q: 为什么错误类型跨 crate 边界必须用 thiserror?
|
||||
医疗业务需要 16+ 强类型实体、自定义 API(趋势分析/统计报表)、文件上传、未来 AI 集成。WASM 插件的 JSONB 动态存储和 20 实体上限无法满足。详见 [[erp-health]]。
|
||||
|
||||
**A:** `anyhow` 的错误没有类型信息,无法在 API 层做精确的 HTTP 状态码映射。`thiserror` 定义明确的错误变体,`AppError` 可以精确映射到 400/401/403/404/409/500。crate 内部可以用 `anyhow` 简化,但对外必须转 `AppError`。
|
||||
### 为什么用 UUIDv7?
|
||||
|
||||
### Q: 为什么 tenant_id 不在 API 路径中?
|
||||
时间排序 + UUID 唯一性 + 接近自增 ID 的索引性能。多租户 SaaS 下不同租户数据不会因 ID 冲突互相影响。
|
||||
|
||||
**A:** 从 JWT token 中提取 tenant_id,通过中间件注入 `TenantContext`。这防止了:
|
||||
- 用户手动修改 URL 访问其他租户数据
|
||||
- API 路径暴露租户信息
|
||||
- 开发者忘记检查租户权限
|
||||
### 为什么 tenant_id 不在 API 路径中?
|
||||
|
||||
管理员接口例外,可以通过路径指定 tenant_id。
|
||||
从 JWT 提取,中间件注入 `TenantContext`。防止:手动改 URL 越权 / API 暴露租户信息 / 忘记检查权限。管理员接口例外。
|
||||
|
||||
### Q: 为什么前端用 HashRouter 而非 BrowserRouter?
|
||||
### 为什么错误类型跨 crate 用 thiserror?
|
||||
|
||||
**A:** 部署时可能不在根路径下,HashRouter 不需要服务端配置 fallback 路由。对 SPA 来说更稳健。
|
||||
`anyhow` 无类型信息,无法精确映射 HTTP 状态码。`thiserror` → `AppError` → 400/401/403/404/409/500。
|
||||
|
||||
## 模块依赖铁律
|
||||
## 2. 关键文件 + 数据流
|
||||
|
||||
### 模块依赖图
|
||||
|
||||
```
|
||||
erp-core (L1)
|
||||
erp-common (L1)
|
||||
|
|
||||
+--------------+--------------+--------------+
|
||||
| | | |
|
||||
erp-auth erp-config erp-workflow erp-message (L2)
|
||||
| | | |
|
||||
+--------------+--------------+--------------+
|
||||
+--------------+--------------+--------------+-----------+
|
||||
| | | | |
|
||||
erp-auth erp-config erp-workflow erp-message erp-health (L2)
|
||||
| | | | |
|
||||
+--------------+--------------+--------------+-----------+
|
||||
|
|
||||
erp-server (L3: 唯一组装点)
|
||||
|
|
||||
erp-plugin (WASM 插件运行时)
|
||||
```
|
||||
|
||||
**禁止:**
|
||||
- L2 模块之间直接依赖
|
||||
- L1 模块依赖任何业务模块
|
||||
- 绕过事件总线直接调用其他模块
|
||||
**禁止**: L2 间直接依赖 / L1 依赖业务模块 / 绕过事件总线
|
||||
|
||||
## 多租户隔离策略
|
||||
|
||||
**当前策略:共享数据库 + tenant_id 列过滤**
|
||||
|
||||
所有业务表包含 `tenant_id` 列,查询时通过中间件自动注入过滤条件。这是最简单的 SaaS 多租户方案,未来可扩展为:
|
||||
- Schema 隔离 — 每个租户独立 schema
|
||||
- 数据库隔离 — 每个租户独立数据库(私有化部署)
|
||||
|
||||
`ErpModule::on_tenant_created()` 和 `on_tenant_deleted()` 钩子确保模块能在租户创建/删除时初始化/清理数据。
|
||||
|
||||
## 技术选型理由
|
||||
### 技术选型
|
||||
|
||||
| 选择 | 理由 |
|
||||
|------|------|
|
||||
| Axum 0.8 | Tokio 团队维护,与 tower 生态无缝集成,类型安全路由 |
|
||||
| SeaORM 1.1 | 异步、类型安全、Rust 原生 ORM,迁移工具完善 |
|
||||
| PostgreSQL 16 | 企业级关系型数据库,JSON 支持好,扩展丰富 |
|
||||
| Redis 7 | 高性能缓存,会话存储,限流 token bucket |
|
||||
| React 19 + Ant Design 6 | 成熟的组件库,企业后台 UI 标配 |
|
||||
| Zustand | 极简状态管理,无 boilerplate |
|
||||
| utoipa | Rust 代码生成 OpenAPI 文档,零额外维护 |
|
||||
| Wasmtime 43 | WASM 沙箱运行时,Component Model 支持,Fuel 资源限制 |
|
||||
| Axum 0.8 | Tokio 团队维护,tower 生态,类型安全路由 |
|
||||
| SeaORM 1.1 | 异步、类型安全、迁移工具完善 |
|
||||
| PostgreSQL 18 | 企业级,JSON 支持,扩展丰富 |
|
||||
| Redis 7 | 缓存 + 限流 token bucket |
|
||||
| React 19 + Ant Design 6 | 企业后台 UI 标配 |
|
||||
| Zustand 5 | 极简状态管理 |
|
||||
| Wasmtime 43 | WASM 沙箱,Component Model,Fuel 限制 |
|
||||
|
||||
## 插件扩展架构
|
||||
### 集成契约
|
||||
|
||||
### Q: 为什么用 WASM 而不是 Lua / gRPC / dylib?
|
||||
| 方向 | 模块 | 触发时机 |
|
||||
|------|------|---------|
|
||||
| 定义 → | [[erp-core]] | 所有模块的 trait 和类型 |
|
||||
| 组装 ← | [[erp-server]] | 模块注册和启动 |
|
||||
| 扩展 ← | [[wasm-plugin]] | 插件通过 Host Bridge 桥接 |
|
||||
|
||||
**A:**
|
||||
## 3. 代码逻辑
|
||||
|
||||
| 方案 | 安全性 | 隔离性 | 性能 | 复杂度 |
|
||||
|------|--------|--------|------|--------|
|
||||
| **WASM** | 高(沙箱) | 进程内隔离 | 接近原生(JIT) | 中 |
|
||||
| Lua 脚本 | 中 | 无隔离 | 快 | 低 |
|
||||
| 进程外 gRPC | 高 | 进程级隔离 | 网络开销 | 高 |
|
||||
| dylib | 低(直接内存) | 无隔离 | 原生 | 低 |
|
||||
⚡ **不变量**: 模块间只通过 EventBus 和 trait 通信,无直接依赖
|
||||
⚡ **不变量**: 所有数据表必须含 `tenant_id`,查询自动过滤
|
||||
⚡ **不变量**: UUID v7 作为主键
|
||||
⚡ **不变量**: 软删除,不硬删除
|
||||
⚡ **不变量**: 所有 API 使用 `/api/v1/` 前缀
|
||||
|
||||
WASM 在安全性和性能之间取得最佳平衡。Wasmtime v43 的 Component Model 提供了类型安全的 Host-Plugin 接口,Fuel 机制防止恶意/有缺陷的插件消耗过多资源。
|
||||
## 4. 活跃问题 + 陷阱
|
||||
|
||||
### 插件架构拓扑
|
||||
⚠️ 当前共享数据库 + tenant_id 过滤,未来可扩展为 Schema 隔离或数据库隔离
|
||||
⚠️ EventBus 内存 broadcast 需 outbox 持久化保障(已通过后台任务实现)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ erp-server │
|
||||
│ ┌──────────────┐ ┌──────────────────────────┐ │
|
||||
│ │ EventBus │ │ PluginRuntime (Wasmtime) │ │
|
||||
│ │ (broadcast) │ │ ┌──────┐ ┌──────┐ │ │
|
||||
│ └──────┬───────┘ │ │插件 A│ │插件 B│ │ │
|
||||
│ │ │ └──┬───┘ └──┬───┘ │ │
|
||||
│ │ │ │ Host API │ │ │
|
||||
│ │ │ ┌──┴────────┴──┐ │ │
|
||||
│ │ │ │ Host Bridge │ │ │
|
||||
│ │ │ └──┬───────────┘ │ │
|
||||
│ │ └─────┼────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ ┌──────┴───────┐ ┌────┴─────┐ │
|
||||
│ │ DB (SeaORM) │ │ EventBus │ │
|
||||
│ └──────────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
## 5. 变更记录
|
||||
|
||||
插件通过 Host Bridge 调用系统功能(db_insert、event_publish 等),Host Bridge 自动注入多租户隔离(tenant_id)和权限检查。详见 [[wasm-plugin]]。
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-core]]** — 架构契约的定义者
|
||||
- **[[erp-server]]** — 架构的组装执行者
|
||||
- **[[database]]** — 多租户隔离的物理实现
|
||||
- **[[wasm-plugin]]** — 插件扩展架构的实现
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-04-23 | 重构为 5 节结构,删除 erp-common 引用,精简技术选型表 |
|
||||
|
||||
123
wiki/database.md
123
wiki/database.md
@@ -1,73 +1,92 @@
|
||||
# database (数据库迁移与模式)
|
||||
---
|
||||
title: 数据库迁移与模式
|
||||
updated: 2026-04-23
|
||||
status: stable
|
||||
tags: [database, seaorm, migration, multi-tenant]
|
||||
---
|
||||
|
||||
## 设计思想
|
||||
# 数据库迁移与模式
|
||||
|
||||
数据库迁移使用 SeaORM Migration 框架,遵循以下原则:
|
||||
> 从 [[index]] 导航。关联: [[erp-core]] [[erp-server]] [[infrastructure]]
|
||||
|
||||
- **所有表必须包含标准字段** — `id`(UUIDv7), `tenant_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted_at`, `version`
|
||||
- **软删除** — 不执行硬删除,设置 `deleted_at` 时间戳
|
||||
## 1. 设计决策
|
||||
|
||||
- **SeaORM Migration** — 异步、类型安全、幂等(`if_not_exists`),每个迁移必须实现 `down()` 可回滚
|
||||
- **所有表必须含标准字段** — `id`(UUIDv7), `tenant_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted_at`, `version`
|
||||
- **软删除** — 不硬删除,设置 `deleted_at` 时间戳
|
||||
- **乐观锁** — 更新时检查 `version` 字段
|
||||
- **多租户隔离** — 所有业务表必须含 `tenant_id`,查询时自动过滤
|
||||
- **幂等迁移** — 使用 `if_not_exists` 确保可重复执行
|
||||
- **可回滚** — 每个迁移必须实现 `down()` 方法
|
||||
- **多租户** — 所有业务表含 `tenant_id`,中间件自动过滤
|
||||
|
||||
## 代码逻辑
|
||||
## 2. 关键文件 + 数据流
|
||||
|
||||
### 核心文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `crates/erp-server/migration/src/lib.rs` | Migrator 注册所有迁移 |
|
||||
| `crates/erp-server/migration/src/m*.rs` | 41 个迁移文件 |
|
||||
| `crates/erp-core/src/types.rs` | BaseFields 标准字段定义 |
|
||||
|
||||
### 迁移命名规则
|
||||
|
||||
### 迁移文件命名规则
|
||||
```
|
||||
m{YYYYMMDD}_{6位序号}_{描述}.rs
|
||||
例: m20260410_000001_create_tenant.rs
|
||||
```
|
||||
|
||||
### 当前表结构
|
||||
### 当前表概览(30 张)
|
||||
|
||||
**tenant 表** (唯一已实现的表):
|
||||
| 列名 | 类型 | 约束 |
|
||||
|------|------|------|
|
||||
| id | UUID | PK, NOT NULL |
|
||||
| name | STRING | NOT NULL |
|
||||
| code | STRING | NOT NULL, UNIQUE |
|
||||
| status | STRING | NOT NULL, DEFAULT 'active' |
|
||||
| settings | JSON | NULLABLE |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
|
||||
| deleted_at | TIMESTAMPTZ | NULLABLE |
|
||||
| 模块 | 表 |
|
||||
|------|-----|
|
||||
| 基础 | tenant |
|
||||
| 认证 (auth) | users, user_credentials, user_tokens, roles, permissions, role_permissions, user_roles, organizations, departments, positions, user_departments |
|
||||
| 配置 (config) | dictionaries, dictionary_items, menus, menu_roles, settings, numbering_rules |
|
||||
| 工作流 (workflow) | process_definitions, process_instances, tokens, tasks, process_variables |
|
||||
| 消息 (message) | message_templates, messages, message_subscriptions |
|
||||
| 审计 | audit_logs, domain_events |
|
||||
| 插件 (plugin) | plugins, entity_registry, plugin_market, plugin_user_views |
|
||||
|
||||
### 已知缺失字段
|
||||
tenant 表缺少 `BaseFields` 要求的:
|
||||
- `created_by` — 创建人
|
||||
- `updated_by` — 最后修改人
|
||||
- `version` — 乐观锁版本号
|
||||
### 集成契约
|
||||
|
||||
### 迁移执行
|
||||
```
|
||||
erp-server 启动 → Migrator::up(&db_conn) → 自动运行所有 pending 迁移
|
||||
```
|
||||
| 方向 | 模块 | 触发时机 |
|
||||
|------|------|---------|
|
||||
| 消费 ← | [[erp-server]] | 启动时自动运行 `Migrator::up()` |
|
||||
| 依赖 ← | [[erp-core]] | BaseFields 定义标准字段规范 |
|
||||
| 提供 → | 所有业务模块 | 表结构供 SeaORM Entity 使用 |
|
||||
|
||||
## 关联模块
|
||||
## 3. 代码逻辑
|
||||
|
||||
- **[[erp-core]]** — `BaseFields` 定义了标准字段规范,迁移表结构必须对齐
|
||||
- **[[erp-server]]** — 启动时自动运行迁移
|
||||
- **[[erp-auth]]** — Phase 2 将创建 users, roles, permissions 表
|
||||
- **[[erp-config]]** — Phase 3 将创建 system_configs 表
|
||||
- **[[erp-workflow]]** — Phase 4 将创建 workflow_definitions, workflow_instances 表
|
||||
- **[[erp-message]]** — Phase 5 将创建 messages, notification_settings 表
|
||||
⚡ **不变量**: 所有业务表必须含 `tenant_id` 列 — 多租户是核心能力,不可事后补
|
||||
|
||||
## 关键文件
|
||||
⚡ **不变量**: 迁移必须幂等 — 使用 `if_not_exists`,可重复执行
|
||||
|
||||
| 文件 | 职责 |
|
||||
⚡ **不变量**: 迁移执行由 erp-server 启动自动触发,不手动执行 SQL
|
||||
|
||||
### 关键结构变更迁移
|
||||
|
||||
| 迁移 | 变更 |
|
||||
|------|------|
|
||||
| `crates/erp-server/migration/src/lib.rs` | Migrator 注册所有迁移 |
|
||||
| `crates/erp-server/migration/src/m20260410_000001_create_tenant.rs` | tenant 表迁移 |
|
||||
| `crates/erp-core/src/types.rs` | BaseFields 标准字段定义 |
|
||||
| `docker/docker-compose.yml` | PostgreSQL 16 服务定义 |
|
||||
| m000027 | 修复唯一索引 + 软删除冲突 |
|
||||
| m000034 | 种子插件权限 |
|
||||
| m000035 | pg_trgm 扩展 + entity 列 |
|
||||
| m000036 | role_permissions 添加 data_scope(行级数据权限) |
|
||||
| m000038 | 修复 CRM 权限码 |
|
||||
| m000039 | entity_registry 列 |
|
||||
| m000041 | plugin_user_views |
|
||||
|
||||
## 未来迁移计划 (按 Phase)
|
||||
## 4. 活跃问题 + 陷阱
|
||||
|
||||
| Phase | 表 | 说明 |
|
||||
|-------|-----|------|
|
||||
| Phase 2 | users, roles, permissions, user_roles, role_permissions | RBAC + ABAC |
|
||||
| Phase 3 | system_configs, config_histories | 层级配置 |
|
||||
| Phase 4 | workflow_definitions, workflow_instances, workflow_tasks | BPMN 工作流 |
|
||||
| Phase 5 | messages, notification_settings, message_templates | 多渠道消息 |
|
||||
| 持续 | domain_events | 事件 outbox 表 |
|
||||
### 历史教训
|
||||
|
||||
- 唯一索引 + 软删除冲突 — 已删除记录的 unique key 阻止新建(m000027 修复)
|
||||
- tenant 表缺少 `created_by`/`updated_by`/`version` 字段 — 首个迁移早于 BaseFields 规范
|
||||
|
||||
⚠️ settings 表的唯一索引曾需修复(m000032)
|
||||
⚠️ 新增表时务必对齐 `crates/erp-core/src/types.rs` 中的 BaseFields
|
||||
|
||||
## 5. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-04-23 | 重构为 5 节结构,更新表清单至 41 个迁移 |
|
||||
| 2026-04-19 | CRM 权限码修复迁移 (m000038) |
|
||||
|
||||
128
wiki/erp-core.md
128
wiki/erp-core.md
@@ -1,57 +1,27 @@
|
||||
---
|
||||
title: erp-core
|
||||
updated: 2026-04-23
|
||||
status: stable
|
||||
tags: [core, error, event-bus, module-trait, shared-types]
|
||||
---
|
||||
|
||||
# erp-core
|
||||
|
||||
## 设计思想
|
||||
> 从 [[index]] 导航。关联: [[erp-server]] [[database]] [[wasm-plugin]] [[architecture]]
|
||||
|
||||
`erp-core` 是整个 ERP 平台的 L1 基础层,所有业务模块的唯一共同依赖。它的职责是定义**跨模块共享的契约**,而非实现业务逻辑。
|
||||
## 1. 设计决策
|
||||
|
||||
核心设计决策:
|
||||
- **AppError 统一错误体系** — 6 种错误变体映射到 HTTP 状态码,业务 crate 只需 `?` 传播错误,由 Axum `IntoResponse` 自动转换
|
||||
- **EventBus 进程内广播** — 用 `tokio::sync::broadcast` 实现发布/订阅,模块间零耦合通信
|
||||
- **ErpModule 插件 trait** — 每个业务模块实现此 trait,由 `ModuleRegistry` 统一注册路由和事件处理器
|
||||
- **BaseFields 强制多租户** — 所有实体的基础字段模板,确保 `tenant_id` 从第一天就存在
|
||||
`erp-core` 是 L1 基础层,所有业务模块的唯一共同依赖。定义**跨模块共享的契约**,不含业务逻辑。
|
||||
|
||||
## 代码逻辑
|
||||
核心决策:
|
||||
- **AppError 统一错误体系** — 6 种变体映射 HTTP 状态码,`?` 传播 + Axum `IntoResponse` 自动转换
|
||||
- **EventBus 进程内广播** — `tokio::sync::broadcast` 实现零耦合通信
|
||||
- **ErpModule 插件 trait** — 统一注册路由和事件处理器
|
||||
- **BaseFields 强制多租户** — 所有实体基础字段模板
|
||||
|
||||
### 错误处理链
|
||||
```
|
||||
业务 crate (thiserror) → AppError → IntoResponse → HTTP JSON 响应
|
||||
数据库 (sea_orm::DbErr) → From 转换 → AppError (自动识别 duplicate key → Conflict)
|
||||
```
|
||||
## 2. 关键文件 + 数据流
|
||||
|
||||
错误响应统一格式:`{ "error": "not_found", "message": "资源不存在", "details": null }`
|
||||
|
||||
### 事件总线
|
||||
```
|
||||
发布者: EventBus::publish(DomainEvent) → broadcast channel
|
||||
订阅者: EventBus::subscribe() → Receiver<DomainEvent>
|
||||
事件字段: id(UUIDv7), event_type("user.created"), tenant_id, payload(JSON), timestamp, correlation_id
|
||||
```
|
||||
|
||||
事件类型命名规则:`{模块}.{动作}` 如 `user.created`, `workflow.task.completed`
|
||||
|
||||
### 模块注册
|
||||
```
|
||||
业务模块实现 ErpModule trait → ModuleRegistry::register() →
|
||||
build_router(): 折叠所有模块的 register_routes() → Axum Router
|
||||
register_handlers(): 注册所有模块的事件处理器 → EventBus
|
||||
```
|
||||
|
||||
### 共享类型
|
||||
- `TenantContext` — 中间件注入的租户上下文(tenant_id, user_id, roles, permissions)
|
||||
- `Pagination` / `PaginatedResponse<T>` — 分页查询标准化(每页上限 100)
|
||||
- `ApiResponse<T>` — API 统一信封 `{ success, data, message }`
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-server]]** — 消费所有 erp-core 类型和 trait,是唯一组装点
|
||||
- **[[erp-auth]]** — 实现 `ErpModule` trait,发布认证事件
|
||||
- **[[erp-workflow]]** — 实现 `ErpModule` trait,订阅业务事件
|
||||
- **[[erp-message]]** — 实现 `ErpModule` trait,订阅通知事件
|
||||
- **[[erp-config]]** — 实现 `ErpModule` trait
|
||||
- **[[database]]** — 迁移表结构必须与 `BaseFields` 对齐
|
||||
- **[[wasm-plugin]]** — WASM 插件通过 Host Bridge 桥接 EventBus
|
||||
|
||||
## 关键文件
|
||||
### 核心文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
@@ -61,9 +31,63 @@
|
||||
| `crates/erp-core/src/types.rs` | BaseFields、Pagination、ApiResponse、TenantContext |
|
||||
| `crates/erp-core/src/lib.rs` | 模块导出入口 |
|
||||
|
||||
## 当前状态
|
||||
### 集成契约
|
||||
|
||||
**已实现,Phase 1 可用。** 但以下部分尚未被 erp-server 集成:
|
||||
- `ModuleRegistry` 未在 `main.rs` 中使用
|
||||
- `EventBus` 未创建实例
|
||||
- `TenantContext` 未通过中间件注入
|
||||
| 方向 | 模块 | 接口 | 触发时机 |
|
||||
|------|------|------|---------|
|
||||
| 提供 → | erp-auth | ErpModule trait, AppError, EventBus | 模块实现 |
|
||||
| 提供 → | erp-config | ErpModule trait, AppError | 模块实现 |
|
||||
| 提供 → | erp-workflow | ErpModule trait, AppError, EventBus | 模块实现 |
|
||||
| 提供 → | erp-message | ErpModule trait, AppError, EventBus | 模块实现 |
|
||||
| 提供 → | erp-plugin | ErpModule trait, AppError, EventBus | 模块实现 |
|
||||
| 消费 ← | [[erp-server]] | ModuleRegistry 组装 | 启动时 |
|
||||
| 桥接 ← | [[wasm-plugin]] | EventBus → 插件 handle_event | 运行时 |
|
||||
|
||||
## 3. 代码逻辑
|
||||
|
||||
### 错误处理链
|
||||
|
||||
```
|
||||
业务 crate (thiserror) → AppError → IntoResponse → HTTP JSON
|
||||
数据库 (sea_orm::DbErr) → From 转换 → AppError (自动识别 duplicate key → Conflict)
|
||||
```
|
||||
|
||||
响应格式:`{ "error": "not_found", "message": "资源不存在", "details": null }`
|
||||
|
||||
### 事件总线
|
||||
|
||||
```
|
||||
EventBus::publish(DomainEvent) → broadcast channel → Receiver<DomainEvent>
|
||||
事件字段: id(UUIDv7), event_type("user.created"), tenant_id, payload(JSON), timestamp
|
||||
```
|
||||
|
||||
命名规则:`{模块}.{动作}` 如 `user.created`, `workflow.task.completed`
|
||||
|
||||
### 模块注册
|
||||
|
||||
```
|
||||
ErpModule trait → ModuleRegistry::register() →
|
||||
build_router(): 折叠所有模块路由 → Axum Router
|
||||
register_handlers(): 注册事件处理器 → EventBus
|
||||
```
|
||||
|
||||
### 共享类型
|
||||
|
||||
- `TenantContext` — 租户上下文(tenant_id, user_id, roles, permissions, department_ids)
|
||||
- `Pagination` / `PaginatedResponse<T>` — 分页标准化(每页上限 100)
|
||||
- `ApiResponse<T>` — 统一信封 `{ success, data, message }`
|
||||
|
||||
⚡ **不变量**: erp-core 不依赖任何业务 crate,只被依赖
|
||||
⚡ **不变量**: 所有 API 使用 `/api/v1/` 前缀
|
||||
⚡ **不变量**: tenant_id 从 JWT 中间件注入,应用层不可伪造
|
||||
|
||||
## 4. 活跃问题 + 陷阱
|
||||
|
||||
⚠️ crate 内部可用 `anyhow`,但跨 crate 边界必须转 `AppError`
|
||||
⚠️ EventBus 当前为内存 broadcast,outbox 持久化通过后台任务实现
|
||||
|
||||
## 5. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-04-23 | 重构为 5 节结构,更新为已完全集成状态 |
|
||||
|
||||
122
wiki/erp-health.md
Normal file
122
wiki/erp-health.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: erp-health 健康管理模块
|
||||
updated: 2026-04-23
|
||||
status: developing
|
||||
tags: [health, patient, appointment, follow-up, consultation]
|
||||
---
|
||||
|
||||
# erp-health 健康管理模块
|
||||
|
||||
> 从 [[index]] 导航。关联: [[erp-core]] [[erp-server]] [[database]] [[frontend]]
|
||||
>
|
||||
> 设计规格: `docs/superpowers/specs/2026-04-23-health-management-module-design.md`
|
||||
|
||||
## 1. 设计决策
|
||||
|
||||
### 为什么用原生模块而非 WASM 插件?
|
||||
|
||||
| WASM 插件限制 | 健康模块需求 |
|
||||
|---------------|-------------|
|
||||
| 实体上限 20 个 | 16+ 强类型医疗实体 |
|
||||
| JSONB 动态存储 | 医疗数据需要强类型、索引、关联 |
|
||||
| 无自定义 API | 趋势分析、统计报表需专用端点 |
|
||||
| 无文件上传 | 化验单、体检报告需存储 |
|
||||
| 沙箱限制 | 无法引入加密、AI、外部 API |
|
||||
|
||||
### 为什么患者/医护账号走 erp-auth?
|
||||
|
||||
复用现有用户体系(认证、JWT、权限),erp-health 只存医疗扩展字段。患者可先建档(体检中心导入),后续再绑定账号。
|
||||
|
||||
### 核心架构选择
|
||||
|
||||
- **原生 Rust crate** — 与 erp-auth、erp-workflow 同等地位,直接访问数据库
|
||||
- **固有方法暴露路由** — `public_routes()` / `protected_routes()`,在 erp-server 中 `.nest("/api/v1/health", ...)`
|
||||
- **EventBus 通信** — 发布 `patient.created`、`appointment.confirmed` 等,订阅 `workflow.task.completed`
|
||||
|
||||
## 2. 关键文件 + 数据流
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
crates/erp-health/
|
||||
├── src/
|
||||
│ ├── lib.rs ← ErpModule trait + routes()
|
||||
│ ├── error.rs ← HealthError → AppError
|
||||
│ ├── state.rs ← HealthState
|
||||
│ ├── entity/ ← 16 个 SeaORM Entity
|
||||
│ ├── service/ ← 5 个业务 service
|
||||
│ ├── handler/ ← 5 个路由 handler
|
||||
│ ├── dto/ ← 请求/响应结构体
|
||||
│ └── event.rs ← 事件定义和处理器
|
||||
```
|
||||
|
||||
### 实体模型(16 张表)
|
||||
|
||||
| 域 | 实体 |
|
||||
|----|------|
|
||||
| 患者管理 | patient, patient_family_member, patient_tag, patient_tag_relation, patient_doctor_relation |
|
||||
| 医护管理 | doctor_profile |
|
||||
| 健康数据 | health_record, vital_signs, lab_report, health_trend |
|
||||
| 预约排班 | appointment, doctor_schedule |
|
||||
| 随访管理 | follow_up_task, follow_up_record |
|
||||
| 咨询管理 | consultation_session, consultation_message |
|
||||
|
||||
### 集成契约
|
||||
|
||||
| 方向 | 模块 | 接口 | 触发时机 |
|
||||
|------|------|------|---------|
|
||||
| 提供 → | [[erp-server]] | `protected_routes()` | 启动时注册 `/api/v1/health/*` |
|
||||
| 调用 → | [[erp-core]] | EventBus | 发布/订阅领域事件 |
|
||||
| 关联 → | erp-auth | `users` 表 (user_id FK) | 患者/医护关联账号 |
|
||||
| 订阅 ← | erp-workflow | `workflow.task.completed` | 随访任务状态更新 |
|
||||
| 订阅 ← | erp-message | `message.sent` | 咨询会话 last_message_at |
|
||||
|
||||
## 3. 代码逻辑
|
||||
|
||||
### API 前缀: `/api/v1/health/`
|
||||
|
||||
关键端点分组:
|
||||
- `/patients` — 患者列表/详情/标签管理/健康摘要
|
||||
- `/patients/:id/vital-signs` — 日常监测数据(血压/心率/体重/血糖)
|
||||
- `/patients/:id/lab-reports` — 化验报告(JSONB 指标数据)
|
||||
- `/patients/:id/trends` — 健康趋势报告(自动/手动生成)
|
||||
- `/appointments` — 预约管理(状态机: pending→confirmed→completed)
|
||||
- `/doctor-schedules` — 排班管理(日历视图)
|
||||
- `/follow-up-tasks` — 随访任务(逾期自动标记)
|
||||
- `/consultation-sessions` — 咨询会话管理
|
||||
|
||||
### 预约并发控制
|
||||
|
||||
创建预约时使用原子 CAS:`UPDATE doctor_schedule SET current_appointments = current_appointments + 1 WHERE id = $1 AND current_appointments < max_appointments RETURNING *`
|
||||
|
||||
### 随访自动链接
|
||||
|
||||
`follow_up_record.next_follow_up_date` 不为空时,自动创建新的 `follow_up_task`。
|
||||
|
||||
### 权限码
|
||||
|
||||
`health.patient.list/manage` · `health.health-data.list/manage` · `health.appointment.list/manage` · `health.follow-up.list/manage` · `health.consultation.list/manage` · `health.doctor.list/manage`
|
||||
|
||||
⚡ **不变量**: 预约创建必须走原子 CAS,不能用 read-then-write
|
||||
⚡ **不变量**: `patient.user_id` 允许 NULL(先建档后绑定)
|
||||
⚡ **不变量**: `consultation_message` 对 `created_at` 按月分区,超 1 年归档
|
||||
|
||||
## 4. 活跃问题 + 陷阱
|
||||
|
||||
### 当前状态: 🔧 开发中
|
||||
|
||||
设计规格已确认,尚未开始编码。
|
||||
|
||||
### 待解决
|
||||
|
||||
| 问题 | 级别 | 说明 |
|
||||
|------|------|------|
|
||||
| 文件上传基础能力 | P1 | 化验单/体检报告需要文件存储服务 |
|
||||
| ECharts 趋势图 | P1 | 前端健康趋势可视化 |
|
||||
| 导出功能 | P2 | 随访台账/咨询记录导出 Excel |
|
||||
|
||||
## 5. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-04-23 | 创建模块 wiki 页,设计规格确认 |
|
||||
@@ -1,66 +1,110 @@
|
||||
---
|
||||
title: erp-server
|
||||
updated: 2026-04-23
|
||||
status: stable
|
||||
tags: [server, axum, assembly, entry-point]
|
||||
---
|
||||
|
||||
# erp-server
|
||||
|
||||
## 设计思想
|
||||
> 从 [[index]] 导航。关联: [[erp-core]] [[infrastructure]] [[database]] [[frontend]]
|
||||
|
||||
`erp-server` 是 L3 层——**唯一的组装点**。它不包含业务逻辑,只负责把所有业务模块组装成可运行的服务。
|
||||
## 1. 设计决策
|
||||
|
||||
核心决策:
|
||||
- **配置优先** — 使用 `config` crate 从 TOML 文件 + 环境变量加载,`ERP__` 前缀覆盖(如 `ERP__DATABASE__URL`)
|
||||
- **启动序列严格有序** — 配置 → 日志 → 数据库 → 迁移 → Redis → 路由 → 监听,每步失败即终止
|
||||
- **单一入口** — 所有模块通过 `ModuleRegistry` 注册,server 本身不直接 import 业务模块的类型
|
||||
- **唯一组装点** — 不含业务逻辑,只负责把所有模块组装成可运行服务
|
||||
- **配置优先** — `config` crate 从 TOML + 环境变量加载,`ERP__` 前缀覆盖
|
||||
- **严格启动序列** — 每步失败即终止,不做部分启动
|
||||
- **安全检查** — 拒绝默认 JWT 密钥 / 数据库 URL
|
||||
|
||||
## 代码逻辑
|
||||
## 2. 关键文件 + 数据流
|
||||
|
||||
### 启动流程 (`main.rs`)
|
||||
```
|
||||
1. AppConfig::load() ← config/default.toml + 环境变量
|
||||
2. init_tracing(level) ← JSON 格式日志
|
||||
3. db::connect(&db_config) ← SeaORM 连接池 (max=20, min=5)
|
||||
4. Migrator::up(&db_conn) ← 运行所有待执行迁移
|
||||
5. redis::Client::open(url) ← Redis 客户端(当前未使用)
|
||||
6. Router::new() ← 当前仅有 404 fallback
|
||||
7. bind(host, port).serve() ← 启动 HTTP 服务
|
||||
```
|
||||
|
||||
### 配置结构
|
||||
```
|
||||
AppConfig
|
||||
├── server: ServerConfig { host: "0.0.0.0", port: 3000 }
|
||||
├── database: DatabaseConfig { url, max_connections: 20, min_connections: 5 }
|
||||
├── redis: RedisConfig { url: "redis://localhost:6379" }
|
||||
├── jwt: JwtConfig { secret, access_token_ttl, refresh_token_ttl }
|
||||
└── log: LogConfig { level: "info" }
|
||||
```
|
||||
|
||||
### 当前状态
|
||||
- 数据库连接池正常工作
|
||||
- 迁移自动执行
|
||||
- **没有注册任何路由** — 仅返回 404
|
||||
- **没有使用 ModuleRegistry** — 未集成业务模块
|
||||
- Redis 客户端已创建但未执行任何命令
|
||||
- 缺少 CORS、压缩、请求追踪中间件
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-core]]** — 提供 AppError、ErpModule trait、ModuleRegistry
|
||||
- **[[database]]** — 迁移文件通过 `erp-server-migration` crate 引用
|
||||
- **[[infrastructure]]** — Docker 提供 PostgreSQL 和 Redis 服务
|
||||
- **[[frontend]]** — Vite 代理 `/api` 请求到 server 的 3000 端口
|
||||
|
||||
## 关键文件
|
||||
### 核心文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `crates/erp-server/src/main.rs` | 服务启动入口 |
|
||||
| `crates/erp-server/src/state.rs` | AppState 定义 |
|
||||
| `crates/erp-server/src/config.rs` | 5 个配置 struct + 加载逻辑 |
|
||||
| `crates/erp-server/src/db.rs` | SeaORM 连接池配置 |
|
||||
| `crates/erp-server/config/default.toml` | 默认配置值 |
|
||||
| `crates/erp-server/Cargo.toml` | 依赖声明 |
|
||||
| `crates/erp-server/config/default.toml` | 默认配置(密钥为占位符) |
|
||||
|
||||
## 待完成 (Phase 1 剩余)
|
||||
### 启动流程
|
||||
|
||||
1. 实例化 `ModuleRegistry` 并注册模块
|
||||
2. 添加 CORS 中间件(tower-http)
|
||||
3. 添加请求追踪中间件
|
||||
4. 将 Redis 连接注入 AppState
|
||||
5. 健康检查端点 (`/api/v1/health`)
|
||||
```
|
||||
AppConfig::load() → 安全检查 → init_tracing → db::connect → Migrator::up
|
||||
→ 种子数据(默认租户+管理员) → Redis客户端 → EventBus(容量1024)
|
||||
→ 注册5个模块 → 初始化插件引擎+恢复插件 → 4个后台任务
|
||||
→ 构建Router → bind + serve → 优雅关闭(CTRL+C/SIGTERM)
|
||||
```
|
||||
|
||||
### 注册的 5 个模块
|
||||
|
||||
AuthModule → ConfigModule → WorkflowModule → MessageModule → PluginModule
|
||||
|
||||
### AppState
|
||||
|
||||
```
|
||||
AppState {
|
||||
db: DatabaseConnection,
|
||||
config: AppConfig,
|
||||
event_bus: EventBus,
|
||||
module_registry: ModuleRegistry,
|
||||
redis: redis::Client,
|
||||
default_tenant_id: Uuid,
|
||||
plugin_engine: PluginEngine,
|
||||
plugin_entity_cache: moka::Cache (1000容量, 5分钟TTL),
|
||||
}
|
||||
```
|
||||
|
||||
### 集成契约
|
||||
|
||||
| 方向 | 模块 | 接口 | 触发时机 |
|
||||
|------|------|------|---------|
|
||||
| 组装 → | erp-auth | `AuthModule` | 启动时注册 |
|
||||
| 组装 → | erp-config | `ConfigModule` | 启动时注册 |
|
||||
| 组装 → | erp-workflow | `WorkflowModule` | 启动时注册 |
|
||||
| 组装 → | erp-message | `MessageModule` | 启动时注册 |
|
||||
| 组装 → | erp-plugin | `PluginModule` | 启动时注册 |
|
||||
| 依赖 ← | [[erp-core]] | ErpModule trait, EventBus | 所有模块 |
|
||||
| 依赖 ← | [[infrastructure]] | PostgreSQL, Redis | 连接 |
|
||||
|
||||
### 后台任务
|
||||
|
||||
1. 消息监听器 — EventBus → MessageModule
|
||||
2. 插件通知 — EventBus → PluginModule
|
||||
3. Outbox relay — domain_events → 外部
|
||||
4. 超时检查器 — 工作流任务超时处理
|
||||
|
||||
## 3. 代码逻辑
|
||||
|
||||
### 中间件栈
|
||||
|
||||
```
|
||||
CORS(可配置 origins) → IP限流(公开路由) → 用户限流(受保护路由) → JWT认证
|
||||
```
|
||||
|
||||
### 配置结构
|
||||
|
||||
```
|
||||
AppConfig
|
||||
├── server: { host: "0.0.0.0", port: 3000 }
|
||||
├── database: { url, max_connections: 20, min_connections: 5 }
|
||||
├── redis: { url }
|
||||
├── jwt: { secret, access_token_ttl: 15min, refresh_token_ttl: 7d }
|
||||
└── log: { level: "info" }
|
||||
```
|
||||
|
||||
⚡ **不变量**: 4 个环境变量在 default.toml 中都是 `__MUST_SET_VIA_ENV__` 占位符,必须通过环境变量设置
|
||||
|
||||
⚡ **不变量**: 启动顺序不可变更 — 数据库必须先于迁移,迁移必须先于模块注册
|
||||
|
||||
## 4. 活跃问题 + 陷阱
|
||||
|
||||
⚠️ 后端必须从 `crates/erp-server/` 目录启动(或通过环境变量覆盖所有配置)
|
||||
⚠️ 种子数据自动创建默认租户和管理员,重复启动幂等
|
||||
|
||||
## 5. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-04-23 | 重构为 5 节结构,更新为当前集成状态 |
|
||||
|
||||
152
wiki/frontend.md
152
wiki/frontend.md
@@ -1,81 +1,103 @@
|
||||
# frontend (Web 前端)
|
||||
---
|
||||
title: Web 前端
|
||||
updated: 2026-04-23
|
||||
status: stable
|
||||
tags: [frontend, react, antd, vite, spa]
|
||||
---
|
||||
|
||||
## 设计思想
|
||||
# Web 前端
|
||||
|
||||
前端是一个 Vite + React SPA,遵循 **UI 层只做展示** 的原则:
|
||||
> 从 [[index]] 导航。关联: [[erp-server]] [[infrastructure]]
|
||||
|
||||
- **组件库优先** — 使用 Ant Design,不自造轮子
|
||||
- **状态集中** — Zustand 管理全局状态(主题、侧边栏、认证)
|
||||
- **API 层分离** — HTTP 调用封装到 service 层,组件不直接 fetch
|
||||
- **代理开发** — Vite 开发服务器代理 `/api` 到后端 3000 端口
|
||||
## 1. 设计决策
|
||||
|
||||
版本实际使用情况(与设计规格有差异):
|
||||
| 技术 | 规格 | 实际 |
|
||||
|------|------|------|
|
||||
| React | 18 | 19.2.4 |
|
||||
| Ant Design | 5 | 6.3.5 |
|
||||
| React Router | 7 | 7.14.0 |
|
||||
- **组件库优先** — Ant Design 6,不自造轮子
|
||||
- **状态集中** — Zustand 管理全局状态(4 个 store)
|
||||
- **API 层分离** — HTTP 调用封装到 `src/api/`(21 个文件),组件不直接 fetch
|
||||
- **代理开发** — Vite 代理 `/api` 到后端 3000 端口
|
||||
- **HashRouter** — 不需要服务端 fallback 配置,部署更稳健
|
||||
- **懒加载** — 除 Login 外所有页面使用 `lazy()` 按需加载
|
||||
|
||||
## 代码逻辑
|
||||
### 版本(以实际 package.json 为准)
|
||||
|
||||
### 应用结构
|
||||
```
|
||||
main.tsx → App.tsx (ConfigProvider + HashRouter) → MainLayout → 各页面组件
|
||||
```
|
||||
React 19.2.4 / Ant Design 6.3.5 / React Router 7.14.0 / Zustand 5.0.12 / Vite 8.0.4 / TypeScript 6.0.2
|
||||
|
||||
### MainLayout 布局
|
||||
经典 SaaS 后台管理布局:
|
||||
- **左侧 Sidebar** — 可折叠暗色菜单,分组:首页/用户/权限/设置
|
||||
- **顶部 Header** — 侧边栏切换 + 通知徽标(硬编码5) + 头像("Admin")
|
||||
- **中间 Content** — React Router Outlet,多标签页切换
|
||||
- **底部 Footer** — 租户名 + 版本号
|
||||
## 2. 关键文件 + 数据流
|
||||
|
||||
### 状态管理 (Zustand)
|
||||
```typescript
|
||||
appStore {
|
||||
isLoggedIn: boolean // 未使用,无登录页
|
||||
tenantName: string // 默认 "ERP Platform"
|
||||
theme: 'light' | 'dark' // 切换 Ant Design 主题
|
||||
sidebarCollapsed: boolean
|
||||
toggleSidebar(), setTheme(), login(), logout()
|
||||
}
|
||||
```
|
||||
|
||||
### 开发服务器代理
|
||||
```
|
||||
http://localhost:5174/api/* → http://localhost:3000/* (API)
|
||||
ws://localhost:5174/ws/* → ws://localhost:3000/* (WebSocket)
|
||||
```
|
||||
|
||||
### 当前状态
|
||||
- 布局壳体完整,暗色/亮色主题切换可用
|
||||
- 只有一个路由 `/` → 占位 HomePage ("Welcome to ERP Platform")
|
||||
- 无 API 调用、无认证流程、无真实数据
|
||||
- 通知计数硬编码为 5,用户名硬编码为 "Admin"
|
||||
- 未实现 i18n(代码中有 zh_CN locale 但文案硬编码)
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-server]]** — API 后端,通过 Vite proxy 连接
|
||||
- **[[infrastructure]]** — Docker 提供 PostgreSQL + Redis
|
||||
|
||||
## 关键文件
|
||||
### 核心文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `apps/web/src/main.tsx` | React 入口 |
|
||||
| `apps/web/src/App.tsx` | 根组件:ConfigProvider + 路由 |
|
||||
| `apps/web/src/layouts/MainLayout.tsx` | 完整后台管理布局 |
|
||||
| `apps/web/src/stores/app.ts` | Zustand 全局状态 |
|
||||
| `apps/web/src/index.css` | TailwindCSS 导入 |
|
||||
| `apps/web/src/App.tsx` | 路由定义(公开 + 受保护) |
|
||||
| `apps/web/src/layouts/MainLayout.tsx` | SaaS 后台管理布局 |
|
||||
| `apps/web/src/stores/` | 4 个 Zustand store |
|
||||
| `apps/web/src/api/` | 21 个 API 服务文件 |
|
||||
| `apps/web/vite.config.ts` | Vite 配置 + API 代理 |
|
||||
| `apps/web/package.json` | 依赖声明 |
|
||||
|
||||
## 待实现 (按 Phase)
|
||||
### 路由结构
|
||||
|
||||
| Phase | 内容 |
|
||||
**公开**: `/login`
|
||||
|
||||
**受保护(MainLayout 包裹)**:
|
||||
|
||||
| 路径 | 页面 |
|
||||
|------|------|
|
||||
| `/` | 首页 |
|
||||
| `/users`, `/roles`, `/organizations` | 用户/角色/组织管理 |
|
||||
| `/workflow` | 工作流 |
|
||||
| `/messages` | 消息中心 |
|
||||
| `/settings` | 系统设置 |
|
||||
| `/plugins/admin`, `/plugins/market` | 插件管理/市场 |
|
||||
| `/plugins/:pluginId/:entityName` | 插件 CRUD(动态生成) |
|
||||
| `/plugins/:pluginId/tabs|tree|graph|dashboard|kanban/:name` | 插件多视图页面 |
|
||||
|
||||
### 集成契约
|
||||
|
||||
| 方向 | 模块 | 接口 | 触发时机 |
|
||||
|------|------|------|---------|
|
||||
| 调用 → | [[erp-server]] | `/api/v1/*` REST | 所有数据操作 |
|
||||
| 调用 → | [[erp-server]] | `ws://localhost:3000/ws/*` | WebSocket |
|
||||
| 消费 ← | 插件系统 | `plugin.toml` schema | 动态生成插件页面 |
|
||||
|
||||
## 3. 代码逻辑
|
||||
|
||||
### 状态管理(4 个 Zustand Store)
|
||||
|
||||
| Store | 状态 |
|
||||
|-------|------|
|
||||
| Phase 2 | 登录页、用户管理页、角色权限页 |
|
||||
| Phase 3 | 系统配置管理页 |
|
||||
| Phase 4 | 工作流设计器、审批列表 |
|
||||
| Phase 5 | 消息中心、通知设置 |
|
||||
| `app.ts` | theme(light/dark), sidebarCollapsed |
|
||||
| `auth.ts` | user, isAuthenticated, localStorage 持久化 |
|
||||
| `message.ts` | unreadCount, recentMessages, 请求去重 |
|
||||
| `plugin.ts` | plugins 列表, 动态菜单, schema 缓存, 请求去重 |
|
||||
|
||||
### 插件页面系统
|
||||
|
||||
插件通过 `plugin.toml` schema 声明页面,前端根据 schema 动态生成:
|
||||
- `PluginCRUDPage` — 标准列表+表单
|
||||
- `PluginTabsPage` — 标签页切换
|
||||
- `PluginTreePage` — 树形展示
|
||||
- `PluginGraphPage` — 关系图谱
|
||||
- `PluginKanbanPage` — 看板视图
|
||||
- `PluginDashboardPage` — 仪表盘
|
||||
|
||||
⚡ **不变量**: 插件菜单由 `plugin.ts` store 从 API 动态获取,不硬编码
|
||||
⚡ **不变量**: API client 在请求前 30s 检查 token 过期,提前刷新避免 401
|
||||
|
||||
### 代理配置
|
||||
|
||||
```
|
||||
http://localhost:5174/api/* → http://localhost:3000/* (API)
|
||||
ws://localhost:5174/ws/* → ws://localhost:3000/* (WebSocket)
|
||||
```
|
||||
|
||||
## 4. 活跃问题 + 陷阱
|
||||
|
||||
⚠️ Ant Design 6 废弃 API 警告(`valueStyle`/`Spin tip`/`trailColor`)已在历史版本中修复
|
||||
⚠️ `antd.setScaleParam` 强制回流 64ms — antd 内部问题,无法直接修复
|
||||
|
||||
## 5. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-04-23 | 重构为 5 节结构,更新为当前完整前端状态 |
|
||||
|
||||
124
wiki/index.md
124
wiki/index.md
@@ -1,95 +1,71 @@
|
||||
# ERP 平台底座 — 知识库
|
||||
# HMS 健康管理平台 — 知识库
|
||||
|
||||
## 项目画像
|
||||
> **Health Management System (HMS)** — 面向体检中心/医疗机构的综合健康管理平台。从 ERP 底座分叉,继承身份权限/工作流/消息/配置等基础能力,`erp-health` 原生模块承载医疗业务。
|
||||
|
||||
**模块化 SaaS ERP 底座**,Rust + React 技术栈,提供身份权限/工作流/消息/配置/插件五大基础模块,支持行业业务模块快速插接。
|
||||
## 关键数字
|
||||
|
||||
关键数字:
|
||||
- 13 个 Rust crate(9 个已实现 + 2 个插件原型 + 2 个业务插件),1 个前端 SPA
|
||||
- 37 个数据库迁移
|
||||
- 6 个业务模块 (auth, config, workflow, message, plugin, server)
|
||||
- 4 个插件 crate (plugin-prototype, plugin-test-sample, plugin-crm, plugin-inventory)
|
||||
- Health Check API (`/api/v1/health`)
|
||||
- OpenAPI JSON (`/api/docs/openapi.json`)
|
||||
- Phase 1-6 全部完成,WASM 插件系统已集成到主服务
|
||||
- Q2-Q4 成熟度路线图已完成(安全地基/架构强化/测试覆盖/插件生态)
|
||||
| 指标 | 值 |
|
||||
|------|-----|
|
||||
| Rust crate | 14 个(7 核心 + erp-health + 6 插件) |
|
||||
| 数据库表 | 30+ 基础表 + 16 健康业务表(规划中) |
|
||||
| 核心模块 | 5 基础 (auth/config/workflow/message/plugin) + 1 业务 (health) |
|
||||
| 健康模块页面 | 13 个(规划中) |
|
||||
| API 文档 | `http://localhost:3000/api/docs/openapi.json` |
|
||||
|
||||
## 模块导航树
|
||||
## 症状导航
|
||||
|
||||
### L1 基础层
|
||||
| 症状 | 先查 | 再查 | 常见根因 |
|
||||
|------|------|------|----------|
|
||||
| API 返回 403 | 权限码检查 | [[wasm-plugin]] 权限系统 | 权限码不匹配 / 缺少 .list 权限 |
|
||||
| API 返回 500 无日志 | [[erp-core]] 错误链 | 后端 tracing 输出 | AppError::Internal 静默 |
|
||||
| 数据库连接失败 | [[infrastructure]] | PostgreSQL 服务状态 | 服务未启动 / 环境变量未设置 |
|
||||
| 前端 401 刷新时 | [[frontend]] auth store | API client token 刷新 | token 过期未主动刷新 |
|
||||
| 迁移执行失败 | [[database]] | PostgreSQL 日志 | 表冲突 / 唯一索引 + 软删除 |
|
||||
| 端口被占用 | [[infrastructure]] dev.ps1 | 端口 5174-5189 进程 | 残留 Vite 进程 |
|
||||
| 预约超额 | [[erp-health]] 排班并发 | appointment CAS 操作 | 并发控制未走原子 CAS |
|
||||
| 跨租户数据泄漏 | [[architecture]] 多租户策略 | [[database]] tenant_id | 查询缺少 tenant_id 过滤 |
|
||||
|
||||
## 模块导航
|
||||
|
||||
### 基础层(继承自 ERP 底座)
|
||||
- [[erp-core]] — 错误体系 · 事件总线 · 模块 trait · 共享类型
|
||||
- [[erp-common]] — ID 生成 · 时间戳 · 编号生成工具
|
||||
- [[architecture]] — 架构决策 · 设计原则 · 技术选型
|
||||
|
||||
### L2 业务层
|
||||
- erp-auth — 用户/角色/权限/组织/部门/岗位管理 · JWT 认证 · RBAC · 行级数据权限
|
||||
### 业务层(继承自 ERP 底座)
|
||||
- erp-auth — 用户/角色/权限/组织/部门/岗位 · JWT · RBAC · 行级数据权限
|
||||
- erp-config — 字典/菜单/设置/编号规则/主题/语言
|
||||
- erp-workflow — BPMN 解析 · Token 驱动执行 · 任务分配 · 流程设计器
|
||||
- erp-message — 消息 CRUD · 模板管理 · 订阅偏好 · 通知面板 · 事件集成
|
||||
- erp-plugin — 插件管理 · WASM 运行时 · 动态表 · 数据 CRUD · 生命周期管理 · 热更新 · 行级数据权限
|
||||
- erp-workflow — BPMN 解析 · Token 驱动 · 任务分配
|
||||
- erp-message — 消息 CRUD · 模板 · 订阅 · 通知面板
|
||||
- erp-plugin — WASM 运行时 · 动态表 · 热更新(HMS 保留但非主要扩展方式)
|
||||
|
||||
### L3 组装层
|
||||
- [[erp-server]] — Axum 服务入口 · AppState · ModuleRegistry 集成 · 配置加载 · 数据库连接 · 优雅关闭
|
||||
### 核心业务层(HMS 专属)
|
||||
- [[erp-health]] — **患者管理 · 健康数据 · 预约排班 · 随访管理 · 咨询管理**(原生 Rust 模块)
|
||||
|
||||
### 插件系统
|
||||
- [[wasm-plugin]] — Wasmtime 运行时 · WIT 接口契约 · Host API · Fuel 资源限制 · 插件制作完整流程
|
||||
- erp-plugin-crm — CRM 客户管理插件 (5 实体/9 权限/6 页面)
|
||||
- erp-plugin-inventory — 进销存管理插件 (6 实体/12 权限/6 页面)
|
||||
### 组装层
|
||||
- [[erp-server]] — Axum 入口 · AppState · 模块注册 · 后台任务 · 优雅关闭
|
||||
|
||||
### 基础设施
|
||||
- [[database]] — SeaORM 迁移 · 多租户表结构 · 软删除模式
|
||||
- [[infrastructure]] — Windows 开发环境 · PostgreSQL 16 · Redis 7 · 一键启动脚本
|
||||
- [[frontend]] — React SPA · Ant Design 布局 · Zustand 状态
|
||||
- [[testing]] — 测试环境指南 · 验证清单 · 常见问题
|
||||
- [[infrastructure]] — 连接信息 · 环境变量 · 一键启动 (**单一真相源**)
|
||||
- [[database]] — SeaORM 迁移 · 多租户表结构
|
||||
- [[frontend]] — React 19 SPA · 健康管理页面
|
||||
- [[testing]] — 验证清单 · 测试分布 · 性能基准
|
||||
|
||||
### 横切关注点
|
||||
- [[architecture]] — 架构决策记录 · 设计原则 · 技术选型理由
|
||||
## 核心架构问答
|
||||
|
||||
## 核心架构决策
|
||||
**为什么 erp-health 用原生模块而非 WASM 插件?** 医疗业务需要 16+ 强类型实体、自定义 API(趋势分析/统计报表)、文件上传(化验单/体检报告)、未来 AI 集成,超出 WASM 插件能力范围。详见 [[erp-health]]。
|
||||
|
||||
**模块间如何通信?** 通过 [[erp-core]] 的 EventBus 发布/订阅 DomainEvent,不直接依赖。
|
||||
**模块间如何通信?** [[erp-core]] EventBus 发布/订阅 DomainEvent。erp-health 发布 `patient.created`、`appointment.confirmed` 等事件,订阅 `workflow.task.completed` 等。详见 [[architecture]]。
|
||||
|
||||
**多租户怎么隔离?** 共享数据库 + tenant_id 列过滤,中间件从 JWT 注入 TenantContext。详见 [[database]] 和 [[architecture]]。
|
||||
**多租户怎么隔离?** 共享数据库 + `tenant_id` 列过滤,中间件从 JWT 注入。详见 [[database]] [[architecture]]。
|
||||
|
||||
**错误怎么传播?** 业务 crate 用 thiserror → AppError → Axum IntoResponse 自动转 HTTP。详见 [[erp-core]] 错误处理链。
|
||||
**患者/医护与 erp-auth 的关系?** 账号走 `users` 表,erp-health 通过 `user_id` 外键关联扩展字段(科室、职称、档案等)。患者可先建档后绑定账号。
|
||||
|
||||
**状态如何共享?** AppState 包含 DB、Config、EventBus、ModuleRegistry,通过 Axum State 提取器注入所有 handler。
|
||||
## 文档索引
|
||||
|
||||
**ModuleRegistry 怎么工作?** 每个 Phase 2+ 的业务模块实现 ErpModule trait,在 main.rs 中链式注册。registry 自动构建路由和事件处理器。
|
||||
|
||||
**插件系统怎么扩展业务?** 通过 [[wasm-plugin]] 的 WASM 沙箱运行第三方插件,插件通过 WIT 定义的 Host API 与系统交互。详细流程见插件制作指南。
|
||||
|
||||
**版本差异怎么办?** package.json 使用 React 19 + Ant Design 6(比规格文档更新),以实际代码为准。
|
||||
|
||||
**行级数据权限怎么控制?** role_permissions 表增加 data_scope 字段(all/self/department/department_tree),JWT 中间件注入 department_ids,插件数据查询自动拼接 scope 条件。
|
||||
|
||||
**插件怎么热更新?** 通过 `/api/v1/admin/plugins/{id}/upgrade` 上传新版本 WASM + manifest,系统对比 schema 变更执行增量 DDL,卸载旧 WASM 加载新 WASM,失败时保持旧版本继续运行。
|
||||
|
||||
## 开发进度
|
||||
|
||||
| Phase | 内容 | 状态 |
|
||||
|-------|------|------|
|
||||
| 1 | 基础设施 | 完成 |
|
||||
| 2 | 身份与权限 | 完成 |
|
||||
| 3 | 系统配置 | 完成 |
|
||||
| 4 | 工作流引擎 | 完成 |
|
||||
| 5 | 消息中心 | 完成 |
|
||||
| 6 | 整合与打磨 | 完成 |
|
||||
| - | WASM 插件原型 | V1-V6 验证通过 |
|
||||
| - | 插件系统集成 | 已集成到主服务 |
|
||||
| - | CRM 插件 | 完成 |
|
||||
| - | Q2 安全地基 + CI/CD | 完成 |
|
||||
| - | Q3 架构强化 + 前端体验 | 完成 |
|
||||
| - | Q4 测试覆盖 + 插件生态 | 完成 |
|
||||
|
||||
## 关键文档索引
|
||||
|
||||
| 文档 | 位置 |
|
||||
| 类型 | 位置 |
|
||||
|------|------|
|
||||
| 设计规格 | `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` |
|
||||
| CRM 插件设计 | `docs/superpowers/specs/2026-04-16-crm-plugin-design.md` |
|
||||
| CRM 插件计划 | `docs/superpowers/plans/2026-04-16-crm-plugin-plan.md` |
|
||||
| 健康模块设计规格 | `docs/superpowers/specs/2026-04-23-health-management-module-design.md` |
|
||||
| 设计规格 | `docs/superpowers/specs/` |
|
||||
| 实施计划 | `docs/superpowers/plans/` |
|
||||
| 协作规则 | `CLAUDE.md` |
|
||||
| 设计评审 | `plans/squishy-pondering-aho-agent-a23c7497aadc6da41.md` |
|
||||
| 插件制作指南 | `.claude/skills/plugin-development/SKILL.md` |
|
||||
|
||||
@@ -1,138 +1,104 @@
|
||||
# infrastructure (开发环境)
|
||||
---
|
||||
title: 开发环境
|
||||
updated: 2026-04-23
|
||||
status: stable
|
||||
tags: [infrastructure, dev-environment, windows, postgresql]
|
||||
---
|
||||
|
||||
## 设计思想
|
||||
# 开发环境
|
||||
|
||||
开发环境在 **Windows** 宿主机直接运行所有服务:
|
||||
- PostgreSQL 通过 Windows 原生安装运行
|
||||
- Redis 7+ 通过 Windows 原生安装运行(可选,缺省时限流降级为 fail-open)
|
||||
- 后端 Rust 服务通过 `cargo run` 快速重启
|
||||
- 前端 Vite 热更新直接在宿主机
|
||||
- PowerShell 脚本 (`dev.ps1`) 提供一键启动/停止
|
||||
> 从 [[index]] 导航。关联: [[erp-server]] [[database]] [[frontend]] [[testing]]
|
||||
>
|
||||
> **本页是连接信息、启动命令、登录凭据的单一真相源。** 其他页面引用此处。
|
||||
|
||||
> Docker Compose 配置保留在 `docker/` 目录下,可供需要容器化环境的场景使用,但日常开发不依赖 Docker。
|
||||
## 1. 设计决策
|
||||
|
||||
## 本机环境实际配置
|
||||
- **Windows 原生运行** — PostgreSQL/Redis/Rust/Node 直接在宿主机,不用 Docker
|
||||
- **一键启动** — `dev.ps1` 管理前后端生命周期
|
||||
- **环境变量优先** — 敏感配置通过 `ERP__` 前缀环境变量覆盖 TOML
|
||||
|
||||
> **重要:以下为当前开发机的实际配置,以本文件为准。**
|
||||
## 2. 关键文件 + 连接信息
|
||||
|
||||
| 组件 | 安装路径 | 配置 |
|
||||
|------|---------|------|
|
||||
| PostgreSQL 18 | `D:\postgreSQL\` | 服务名 `postgresql-x64-18`, 端口 5432 |
|
||||
| Redis | 云端实例 | `redis://:redis_KBCYJk@129.204.154.246:6379`, 限流中间件已正常工作 |
|
||||
| Rust | stable (cargo in PATH) | workspace 根目录编译 |
|
||||
| Node.js + pnpm | in PATH | apps/web/ |
|
||||
### 核心文件
|
||||
|
||||
### 数据库连接
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `dev.ps1` | 一键启动/停止(自动清理端口 5174-5189) |
|
||||
| `crates/erp-server/config/default.toml` | 默认配置模板 |
|
||||
| `docker/docker-compose.yml` | 可选 Docker 配置 |
|
||||
|
||||
### 服务连接
|
||||
|
||||
| 服务 | 地址 | 用途 |
|
||||
|------|------|------|
|
||||
| PostgreSQL 18 | `postgres://postgres:123123@localhost:5432/erp` | 主数据库 |
|
||||
| Redis 7 | `redis://:redis_KBCYJk@129.204.154.246:6379` (云端) | 缓存 + 限流 |
|
||||
| 后端 API | `http://localhost:3000/api/v1` | Axum 服务 |
|
||||
| 前端 SPA | `http://localhost:5174` | Vite 开发服务器 |
|
||||
|
||||
### 登录凭据
|
||||
|
||||
```
|
||||
用户: postgres
|
||||
密码: 123123
|
||||
数据库: erp
|
||||
连接串: postgres://postgres:123123@localhost:5432/erp
|
||||
用户名: admin 密码: Admin@2026
|
||||
```
|
||||
|
||||
psql 路径: `D:\postgreSQL\bin\psql.exe -U postgres -h localhost -d erp`
|
||||
psql: `D:\postgreSQL\bin\psql.exe -U postgres -h localhost -d erp`
|
||||
|
||||
### 后端启动命令
|
||||
### 必须设置的环境变量
|
||||
|
||||
后端**必须**从 `crates/erp-server/` 目录启动(需要读取 `config/default.toml`),或通过环境变量覆盖:
|
||||
| 变量 | 开发值 |
|
||||
|------|--------|
|
||||
| `ERP__DATABASE__URL` | `postgres://postgres:123123@localhost:5432/erp` |
|
||||
| `ERP__JWT__SECRET` | `dev-secret-key-change-in-prod` |
|
||||
| `ERP__AUTH__SUPER_ADMIN_PASSWORD` | `Admin@2026` |
|
||||
| `ERP__REDIS__URL` | `redis://:redis_KBCYJk@129.204.154.246:6379` |
|
||||
|
||||
> 所有四个在 `default.toml` 中为 `__MUST_SET_VIA_ENV__` 占位符
|
||||
|
||||
### 集成契约
|
||||
|
||||
| 方向 | 模块 | 触发时机 |
|
||||
|------|------|---------|
|
||||
| 提供 → | [[erp-server]] | 数据库/Redis 连接 |
|
||||
| 提供 → | [[frontend]] | Vite 代理目标 |
|
||||
| 提供 → | [[testing]] | 测试环境配置 |
|
||||
|
||||
## 3. 代码逻辑
|
||||
|
||||
### 一键启动(推荐)
|
||||
|
||||
```powershell
|
||||
# 方式一:从 crates/erp-server 目录启动(使用 default.toml + 环境变量覆盖)
|
||||
.\dev.ps1 # 启动后端 + 前端
|
||||
.\dev.ps1 -Status # 查看端口状态
|
||||
.\dev.ps1 -Stop # 停止所有服务
|
||||
.\dev.ps1 -Restart # 重启所有服务
|
||||
```
|
||||
|
||||
### 手动启动
|
||||
|
||||
```powershell
|
||||
# 后端(必须从 crates/erp-server 目录)
|
||||
cd crates/erp-server
|
||||
$env:ERP__DATABASE__URL = "postgres://postgres:123123@localhost:5432/erp"
|
||||
$env:ERP__JWT__SECRET = "dev-secret-key-change-in-prod"
|
||||
$env:ERP__AUTH__SUPER_ADMIN_PASSWORD = "Admin@2026"
|
||||
cargo run -p erp-server
|
||||
|
||||
# 方式二:一键启动脚本(推荐)
|
||||
.\dev.ps1
|
||||
# 前端
|
||||
cd apps/web && pnpm install && pnpm dev
|
||||
```
|
||||
|
||||
### 登录凭据
|
||||
⚡ **不变量**: 后端必须从 `crates/erp-server/` 目录启动或通过环境变量覆盖所有配置
|
||||
⚡ **不变量**: Vite 固定端口 5174(`--strictPort`),前端代理 `/api` → 后端 3000
|
||||
|
||||
```
|
||||
用户名: admin
|
||||
密码: Admin@2026
|
||||
```
|
||||
## 4. 活跃问题 + 陷阱
|
||||
|
||||
## 服务端口
|
||||
⚠️ Redis 不可达时限流自动降级为 fail-open(放行所有请求)
|
||||
⚠️ Docker Compose 配置保留在 `docker/` 下但日常开发不依赖
|
||||
⚠️ 首次 `cargo run` 编译整个 workspace 较慢(含 wasmtime),后续增量快
|
||||
|
||||
| 服务 | 端口 | 用途 |
|
||||
|------|------|------|
|
||||
| PostgreSQL 18 | 5432 | 主数据库 |
|
||||
| Redis 7+ | 6379 (云端) | 缓存 + 限流 |
|
||||
| erp-server (Axum) | 3000 | 后端 API |
|
||||
| Vite dev server | 5174 | 前端 SPA(固定端口,--strictPort) |
|
||||
## 5. 变更记录
|
||||
|
||||
### 连接信息(配置文件版本)
|
||||
```
|
||||
PostgreSQL: postgres://postgres:123123@localhost:5432/erp
|
||||
Redis: redis://:redis_KBCYJk@129.204.154.246:6379 (云端实例)
|
||||
```
|
||||
|
||||
## 一键启动
|
||||
|
||||
```powershell
|
||||
.\dev.ps1 # 启动后端 + 前端(自动清理旧进程 5174-5189)
|
||||
.\dev.ps1 -Status # 查看端口状态
|
||||
.\dev.ps1 -Stop # 停止所有服务
|
||||
.\dev.ps1 -Restart # 重启所有服务
|
||||
```
|
||||
|
||||
> `dev.ps1` 会在启动前清理端口 5174-5189 范围内所有残留进程,并使用 `--strictPort` 确保 Vite 固定在 5174 端口。
|
||||
|
||||
### 环境变量
|
||||
|
||||
必须通过环境变量设置的值(`default.toml` 中为占位符):
|
||||
|
||||
| 变量 | 说明 | 开发值 |
|
||||
|------|------|--------|
|
||||
| `ERP__DATABASE__URL` | 数据库连接串 | `postgres://postgres:123123@localhost:5432/erp` |
|
||||
| `ERP__JWT__SECRET` | JWT 签名密钥 | 自定义字符串 |
|
||||
| `ERP__AUTH__SUPER_ADMIN_PASSWORD` | admin 初始密码 | `Admin@2026` |
|
||||
| `ERP__REDIS__URL` | Redis 连接串 | `redis://:redis_KBCYJk@129.204.154.246:6379` |
|
||||
|
||||
> 所有四个变量在 `default.toml` 中都是 `__MUST_SET_VIA_ENV__` 占位符,**必须**通过环境变量设置,否则服务拒绝启动。
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-server]]** — 连接 PostgreSQL 和 Redis
|
||||
- **[[database]]** — 迁移在 PostgreSQL 中执行
|
||||
- **[[frontend]]** — Vite 代理 API 到后端
|
||||
- **[[testing]]** — 测试环境详细指南
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| `dev.ps1` | 一键启动/停止脚本(自动清理端口 5174-5189) |
|
||||
| `docker/docker-compose.yml` | 可选的 Docker Compose 配置 |
|
||||
| `crates/erp-server/config/default.toml` | 默认配置模板(密钥为占位符) |
|
||||
| `D:\postgreSQL\bin\psql.exe` | PostgreSQL 客户端 |
|
||||
|
||||
## 常用命令
|
||||
|
||||
```powershell
|
||||
# 一键启动(推荐)
|
||||
.\dev.ps1
|
||||
|
||||
# 手动启动后端(从 crates/erp-server 目录)
|
||||
cd crates/erp-server
|
||||
$env:ERP__DATABASE__URL="postgres://postgres:123123@localhost:5432/erp"
|
||||
$env:ERP__JWT__SECRET="dev-secret"
|
||||
$env:ERP__AUTH__SUPER_ADMIN_PASSWORD="Admin@2026"
|
||||
cargo run -p erp-server
|
||||
|
||||
# 手动启动前端(固定端口)
|
||||
cd apps/web && pnpm dev -- --strictPort
|
||||
|
||||
# 连接数据库
|
||||
D:\postgreSQL\bin\psql.exe -U postgres -h localhost -d erp
|
||||
|
||||
# 健康检查
|
||||
curl http://localhost:3000/api/v1/health
|
||||
|
||||
# 登录测试
|
||||
curl -s http://localhost:3000/api/v1/auth/login -H "Content-Type: application/json" -d '{"username":"admin","password":"Admin@2026"}'
|
||||
```
|
||||
| 2026-04-23 | 重构为 5 节结构,确立为连接信息的单一真相源 |
|
||||
|
||||
294
wiki/testing.md
294
wiki/testing.md
@@ -1,119 +1,49 @@
|
||||
# 测试环境指南
|
||||
---
|
||||
title: 测试与验证
|
||||
updated: 2026-04-23
|
||||
status: stable
|
||||
tags: [testing, verification]
|
||||
---
|
||||
|
||||
> 本项目在 **Windows** 环境下开发,使用 PowerShell 脚本一键启动。不使用 Docker,数据库直接通过原生安装运行。
|
||||
# 测试与验证
|
||||
|
||||
## 环境要求
|
||||
> 从 [[index]] 导航。关联: [[infrastructure]] [[database]] [[frontend]] [[erp-server]]
|
||||
|
||||
| 工具 | 最低版本 | 用途 |
|
||||
|------|---------|------|
|
||||
| Rust | stable (1.93+) | 后端编译 |
|
||||
| Node.js | 20+ | 前端工具链 |
|
||||
| pnpm | 9+ | 前端包管理 |
|
||||
| PostgreSQL | 16+ (当前 18) | 主数据库 |
|
||||
| Redis | 7+ (云端实例) | 缓存 + 限流 |
|
||||
## 1. 设计决策
|
||||
|
||||
## 服务连接信息(实际配置)
|
||||
- **真实数据库优先** — 集成测试用真实 PostgreSQL,不用内存模拟
|
||||
- **分层验证** — 编译检查 → 单元测试 → 功能验证 → 生产构建
|
||||
- **环境配置统一由 [[infrastructure]] 管理** — 连接信息、启动命令、登录凭据见该页
|
||||
|
||||
| 服务 | 地址 | 用途 |
|
||||
|------|------|------|
|
||||
| PostgreSQL | `postgres://postgres:123123@localhost:5432/erp` | 主数据库 |
|
||||
| Redis | `redis://:redis_KBCYJk@129.204.154.246:6379` (云端) | 缓存 + 限流 |
|
||||
| 后端 API | `http://localhost:3000/api/v1` | Axum 服务 |
|
||||
| 前端 SPA | `http://localhost:5174` | Vite 开发服务器 |
|
||||
## 2. 关键文件 + 验证清单
|
||||
|
||||
### 登录信息
|
||||
### 测试分布
|
||||
|
||||
- 用户名: `admin`
|
||||
- 密码: `Admin@2026`
|
||||
| Crate | 测试数 | 覆盖 |
|
||||
|-------|--------|------|
|
||||
| erp-auth | 8 | 密码哈希、TTL 解析 |
|
||||
| erp-core | 6 | RBAC 权限检查 |
|
||||
| erp-workflow | 16 | BPMN 解析、表达式求值 |
|
||||
| erp-plugin-prototype | 6 | WASM 插件集成 |
|
||||
| **总计** | **36** | |
|
||||
|
||||
## 一键启动(推荐)
|
||||
|
||||
使用 PowerShell 脚本管理前后端服务:
|
||||
|
||||
```powershell
|
||||
.\dev.ps1 # 启动后端 + 前端(自动清理旧进程)
|
||||
.\dev.ps1 -Status # 查看端口状态
|
||||
.\dev.ps1 -Restart # 重启所有服务
|
||||
.\dev.ps1 -Stop # 停止所有服务
|
||||
```
|
||||
|
||||
脚本会自动:
|
||||
1. 清理端口 5174-5189 范围内所有残留进程
|
||||
2. 编译并启动 Rust 后端 (`cargo run -p erp-server`)
|
||||
3. 安装前端依赖并启动 Vite 开发服务器 (`pnpm dev -- --strictPort`)
|
||||
|
||||
## 手动启动
|
||||
|
||||
### 1. 确保基础设施运行
|
||||
|
||||
```powershell
|
||||
# 检查 PostgreSQL 服务状态
|
||||
Get-Service -Name "postgresql*"
|
||||
|
||||
# 如需启动
|
||||
# PostgreSQL 通常自动启动,服务名 postgresql-x64-18
|
||||
```
|
||||
|
||||
### 2. 启动后端(必须从 crates/erp-server 目录)
|
||||
|
||||
```powershell
|
||||
cd crates/erp-server
|
||||
$env:ERP__DATABASE__URL = "postgres://postgres:123123@localhost:5432/erp"
|
||||
$env:ERP__JWT__SECRET = "dev-secret-key-change-in-prod"
|
||||
$env:ERP__AUTH__SUPER_ADMIN_PASSWORD = "Admin@2026"
|
||||
cargo run -p erp-server
|
||||
```
|
||||
|
||||
首次运行会自动执行数据库迁移。
|
||||
|
||||
### 3. 启动前端
|
||||
|
||||
```powershell
|
||||
cd apps/web
|
||||
pnpm install # 首次需要安装依赖
|
||||
pnpm dev # 启动开发服务器(端口 5174,固定)
|
||||
```
|
||||
|
||||
## 验证清单
|
||||
|
||||
### 后端验证
|
||||
### 编译 + 测试
|
||||
|
||||
```bash
|
||||
# 编译检查(无错误)
|
||||
cargo check
|
||||
|
||||
# 全量测试(应全部通过)
|
||||
cargo test --workspace
|
||||
|
||||
# Lint 检查(无警告)
|
||||
cargo clippy -- -D warnings
|
||||
|
||||
# 格式检查
|
||||
cargo fmt --check
|
||||
cargo check # 编译无错误
|
||||
cargo test --workspace # 全量测试
|
||||
cargo clippy -- -D warnings # Lint 无警告
|
||||
cargo fmt --check # 格式检查
|
||||
cd apps/web && pnpm build # 前端生产构建
|
||||
```
|
||||
|
||||
### 前端验证
|
||||
### 功能验证端点
|
||||
|
||||
```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 | 前端页面 |
|
||||
| 端点 | 说明 |
|
||||
|------|------|
|
||||
| `http://localhost:3000/api/v1/health` | 健康检查 |
|
||||
| `http://localhost:3000/api/docs/openapi.json` | OpenAPI 文档 |
|
||||
| `http://localhost:5174` | 前端页面 |
|
||||
|
||||
### API 快速测试
|
||||
|
||||
@@ -122,143 +52,57 @@ pnpm tsc -b
|
||||
curl -s http://localhost:3000/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"Admin@2026"}'
|
||||
|
||||
# 列出用户(需要 Token)
|
||||
curl -s http://localhost:3000/api/v1/users \
|
||||
-H "Authorization: Bearer <token>"
|
||||
|
||||
# 列出插件
|
||||
curl -s http://localhost:3000/api/v1/admin/plugins \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
## 数据库管理
|
||||
### 前端性能基准(2026-04-18 Lighthouse)
|
||||
|
||||
### 连接数据库
|
||||
Accessibility / SEO / Best Practices 均 100,LCP 840ms,CLS 0.02
|
||||
|
||||
## 3. 代码逻辑
|
||||
|
||||
### 集成契约
|
||||
|
||||
| 方向 | 模块 | 触发时机 |
|
||||
|------|------|---------|
|
||||
| 依赖 ← | [[infrastructure]] | 环境准备、连接信息 |
|
||||
| 验证 → | [[erp-server]] | 健康检查、API 测试 |
|
||||
| 验证 → | [[frontend]] | 生产构建 |
|
||||
|
||||
⚡ **不变量**: 功能验证需要后端服务运行中,编译检查必须先于测试通过
|
||||
|
||||
### 数据库常用查询
|
||||
|
||||
```bash
|
||||
# 连接数据库
|
||||
D:\postgreSQL\bin\psql.exe -U postgres -h localhost -d erp
|
||||
```
|
||||
|
||||
### 常用查询
|
||||
|
||||
```sql
|
||||
-- 列出所有表
|
||||
\dt
|
||||
|
||||
-- 查看迁移记录
|
||||
SELECT version FROM seaql_migrations ORDER BY version;
|
||||
|
||||
-- 查看插件权限
|
||||
SELECT code, name FROM permissions WHERE deleted_at IS NULL ORDER BY code;
|
||||
|
||||
-- 查看 admin 角色的权限
|
||||
SELECT p.code FROM role_permissions rp
|
||||
JOIN permissions p ON rp.permission_id = p.id
|
||||
JOIN roles r ON rp.role_id = r.id
|
||||
WHERE r.code = 'admin' AND rp.deleted_at IS NULL AND p.deleted_at IS NULL;
|
||||
SELECT version FROM seaql_migrations ORDER BY version; -- 迁移记录
|
||||
SELECT code, name FROM permissions WHERE deleted_at IS NULL ORDER BY code; -- 插件权限
|
||||
```
|
||||
|
||||
### 迁移
|
||||
## 4. 活跃问题 + 陷阱
|
||||
|
||||
迁移在 `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
|
||||
|
||||
# 集成测试(需要 Docker/PostgreSQL)
|
||||
cargo test -p erp-server --test integration
|
||||
```
|
||||
|
||||
## 已知问题(2026-04-18 审计)
|
||||
|
||||
| 问题 | 严重度 | 状态 |
|
||||
|------|--------|------|
|
||||
| CRM 插件权限未分配给 admin 角色 → 数据页面 403 | P0 | ✅ 已修复 |
|
||||
| CRM 插件权限码与实体名不匹配(`tag.manage` vs `customer_tag`)→ 标签/关系/图谱 403 | P0 | ✅ 已修复(迁移 m20260419_000038) |
|
||||
| CRM 插件 WASM 二进制错误(存储了测试插件而非 CRM 插件) | P0 | ✅ 已修复 |
|
||||
| 首页统计卡片永久 loading | P0 | ✅ 已修复 |
|
||||
| `roles/permissions` 路由被 UUID 解析拦截 | P1 | ✅ 已修复 |
|
||||
| 统计概览 `tagColor` undefined crash(`getEntityPalette` 负数索引) | P1 | ✅ 已修复 |
|
||||
| 销售漏斗/看板 filter 请求 500(CRM customer 表缺少 generated columns) | P1 | ✅ 已修复(手动 ALTER TABLE 补齐 `_f_level`/`_f_status`/`_f_customer_type`/`_f_industry`/`_f_region` + 索引) |
|
||||
| `build_scope_sql` 参数索引硬编码 `$100` 导致 SQL 参数错位 | P1 | ✅ 已修复(动态 `values.len()+1`) |
|
||||
| `AppError::Internal` 无日志输出,500 错误静默 | P1 | ✅ 已修复(添加 `tracing::error` 日志) |
|
||||
| antd 6 废弃 API 警告(`valueStyle`/`Spin tip`/`trailColor`) | P2 | ✅ 已修复 |
|
||||
| 问题 | 级别 | 状态 |
|
||||
|------|------|------|
|
||||
| display_name 存储 XSS HTML | P1 | 待修复 |
|
||||
| 页面刷新时 4 个 401 错误(过期 token 未主动刷新) | P2 | ✅ 已修复(proactive token refresh) |
|
||||
| 插件列表重复请求(无并发去重) | P2 | ✅ 已修复(fetchPlugins promise 去重) |
|
||||
| TS 编译错误:未使用变量 | P3 | ✅ 已修复 |
|
||||
| antd vendor chunk 2.9MB(gzip 后约 400KB) | P3 | 待优化 |
|
||||
| antd `setScaleParam` 强制回流 64ms | P3 | antd 内部问题,无法直接修复 |
|
||||
| antd vendor chunk 2.9MB (gzip ~400KB) | P3 | 待优化 |
|
||||
|
||||
详见 `docs/audit-2026-04-18.md`。
|
||||
### 历史教训
|
||||
|
||||
### 前端审计摘要(2026-04-18 Lighthouse + 性能)
|
||||
- CRM 权限码与实体名不一致 → 403(详见 [[wasm-plugin]] 权限命名铁律)
|
||||
- `AppError::Internal` 无日志 → 500 静默(已加 `tracing::error`)
|
||||
- `build_scope_sql` 参数索引硬编码 → SQL 参数错位(已动态化)
|
||||
|
||||
| 指标 | 得分 |
|
||||
⚠️ 首次 `cargo run` 需编译整个 workspace(含 wasmtime),后续增量快
|
||||
⚠️ Redis 不可达时限流自动降级为 fail-open
|
||||
|
||||
## 5. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| Accessibility | 100 |
|
||||
| SEO | 100 |
|
||||
| Best Practices | 100 |
|
||||
| LCP | 840ms |
|
||||
| CLS | 0.02 |
|
||||
| TTFB | 4ms |
|
||||
|
||||
**已实施的优化:**
|
||||
- API client proactive token refresh(请求前 30s 检查过期,提前刷新避免 401)
|
||||
- plugin store 请求去重(promise 复用,防止并发重复调用)
|
||||
- 生产构建中 StrictMode 双重渲染导致的重复请求不会出现
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 端口被占用 / 多个 Vite 进程残留
|
||||
|
||||
```powershell
|
||||
# 使用 dev.ps1 自动清理
|
||||
.\dev.ps1 -Stop
|
||||
|
||||
# 手动清理 Vite 残留进程(端口 5174-5189)
|
||||
Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -ge 5174 -and $_.LocalPort -le 5189 } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }
|
||||
```
|
||||
|
||||
### Q: 数据库连接失败
|
||||
|
||||
1. 确认 PostgreSQL 服务正在运行: `Get-Service -Name "postgresql*"`
|
||||
2. 使用正确连接串: `postgres://postgres:123123@localhost:5432/erp`
|
||||
3. psql 路径: `D:\postgreSQL\bin\psql.exe`
|
||||
|
||||
### Q: 首次启动很慢
|
||||
|
||||
首次 `cargo run` 需要编译整个 workspace(特别是 wasmtime),后续增量编译会很快。
|
||||
|
||||
### Q: Redis 未安装
|
||||
|
||||
Redis 已配置为云端实例(`129.204.154.246:6379`)。限流中间件使用固定窗口计数器,登录接口限制 60 秒内 5 次请求。如 Redis 不可达,自动降级为 fail-open(放行所有请求)。
|
||||
|
||||
## 关联模块
|
||||
|
||||
- [[infrastructure]] — 基础设施配置详情
|
||||
- [[database]] — 数据库迁移和表结构
|
||||
- [[frontend]] — 前端技术栈和配置
|
||||
- [[erp-server]] — 后端服务配置
|
||||
| 2026-04-23 | 重构为 5 节结构,去除与 infrastructure.md 重复 |
|
||||
| 2026-04-18 | Lighthouse 审计 + 性能优化 |
|
||||
|
||||
@@ -1,514 +1,125 @@
|
||||
# wasm-plugin (WASM 插件系统)
|
||||
---
|
||||
title: WASM 插件系统
|
||||
updated: 2026-04-23
|
||||
status: stable
|
||||
tags: [wasm, plugin, wasmtime, wit]
|
||||
---
|
||||
|
||||
## 设计思想
|
||||
# WASM 插件系统
|
||||
|
||||
ERP 平台通过 WASM 沙箱实现**安全、隔离、热插拔**的业务扩展。插件在 Wasmtime 运行时中执行,只能通过 WIT 定义的 Host API 与系统交互,无法直接访问数据库、文件系统或网络。
|
||||
> 从 [[index]] 导航。关联: [[erp-core]] [[architecture]] [[erp-server]]
|
||||
|
||||
核心决策:
|
||||
- **WASM 沙箱** — 插件代码在隔离环境中运行,Host 控制所有资源访问
|
||||
- **WIT 接口契约** — 通过 `.wit` 文件定义 Host ↔ 插件的双向接口,bindgen 自动生成类型化绑定
|
||||
- **Fuel 资源限制** — 通过燃料机制限制插件 CPU 使用,防止无限循环
|
||||
- **声明式 Host API** — 插件通过 `db_insert` / `event_publish` 等函数操作数据,Host 自动注入 tenant_id、校验权限
|
||||
## 1. 设计决策
|
||||
|
||||
## 原型验证结果 (V1-V6)
|
||||
### 为什么选 WASM 而非 Lua / gRPC / dylib?
|
||||
|
||||
| 验证项 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| 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 |
|
||||
| 方案 | 安全性 | 隔离性 | 性能 | 复杂度 |
|
||||
|------|--------|--------|------|--------|
|
||||
| **WASM** | 高(沙箱) | 进程内隔离 | 接近原生 | 中 |
|
||||
| Lua 脚本 | 中 | 无隔离 | 快 | 低 |
|
||||
| 进程外 gRPC | 高 | 进程级隔离 | 网络开销 | 高 |
|
||||
| dylib | 低 | 无隔离 | 原生 | 低 |
|
||||
|
||||
## 项目结构
|
||||
核心权衡:WASM 在安全和性能间取得最佳平衡。Wasmtime v43 Component Model 提供类型安全的 Host-Plugin 接口,Fuel 防止无限循环。
|
||||
|
||||
### 架构拓扑
|
||||
|
||||
```
|
||||
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
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ erp-server │
|
||||
│ ┌───────────┐ ┌────────────────────────┐ │
|
||||
│ │ EventBus │ │ PluginRuntime(Wasmtime) │ │
|
||||
│ │(broadcast)│ │ ┌─────┐ ┌─────┐ │ │
|
||||
│ └─────┬─────┘ │ │CRM │ │库存 │ │ │
|
||||
│ │ │ └──┬──┘ └──┬──┘ │ │
|
||||
│ │ │ Host Bridge(自动注入 │ │
|
||||
│ │ │ tenant_id+权限检查) │ │
|
||||
│ │ └─────┼──────────────────┘ │
|
||||
│ ┌─────┴──────┐ │
|
||||
│ │ DB(SeaORM) │ │
|
||||
│ └────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## WIT 接口定义
|
||||
### 原型验证 (V1-V6)
|
||||
|
||||
文件:`crates/erp-plugin-prototype/wit/plugin.wit`
|
||||
全部通过:WIT+bindgen 编译、Host 调用插件、插件回调 Host API、async 实例化、Fuel 限制、动态加载。
|
||||
|
||||
```
|
||||
package erp:plugin;
|
||||
## 2. 关键文件 + 数据流
|
||||
|
||||
// Host 暴露给插件的 API(插件 import)
|
||||
interface host-api {
|
||||
db-insert: func(entity: string, data: list<u8>) -> result<list<u8>, string>;
|
||||
db-query: func(entity: string, filter: list<u8>, pagination: list<u8>) -> result<list<u8>, string>;
|
||||
db-update: func(entity, id: string, data: list<u8>, version: s64) -> result<list<u8>, string>;
|
||||
db-delete: func(entity: string, id: string) -> result<_, string>;
|
||||
event-publish: func(event-type: string, payload: list<u8>) -> result<_, string>;
|
||||
config-get: func(key: string) -> result<list<u8>, string>;
|
||||
log-write: func(level: string, message: string);
|
||||
current-user: func() -> result<list<u8>, string>;
|
||||
check-permission: func(permission: string) -> result<bool, string>;
|
||||
}
|
||||
|
||||
// 插件导出的 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<u8>) -> result<_, string>;
|
||||
}
|
||||
|
||||
world plugin-world {
|
||||
import host-api;
|
||||
export plugin-api;
|
||||
}
|
||||
```
|
||||
|
||||
## 关键技术要点
|
||||
|
||||
### HasSelf<T> — Linker 注册模式
|
||||
|
||||
当 Store 数据类型(`HostState`)直接实现 `Host` trait 时,使用 `HasSelf<T>` 作为 `add_to_linker` 的类型参数:
|
||||
|
||||
```rust
|
||||
use wasmtime::component::{HasSelf, Linker};
|
||||
|
||||
let mut linker = Linker::new(engine);
|
||||
PluginWorld::add_to_linker::<_, HasSelf<HostState>>(&mut linker, |state| state)?;
|
||||
```
|
||||
|
||||
`HasSelf<HostState>` 表示 `Data<'a> = &'a mut HostState`,bindgen 生成的 `Host for &mut T` blanket impl 确保调用链正确。
|
||||
|
||||
### WASM Component vs Core Module
|
||||
|
||||
`wit_bindgen::generate!` 生成的是 core WASM 模块(`.wasm`),但 `Component::from_binary()` 需要 WASM Component 格式。转换步骤:
|
||||
|
||||
```bash
|
||||
# 1. 编译为 core wasm
|
||||
cargo build -p <plugin-crate> --target wasm32-unknown-unknown --release
|
||||
|
||||
# 2. 转换为 component
|
||||
wasm-tools component new target/wasm32-unknown-unknown/release/<name>.wasm \
|
||||
-o target/<name>.component.wasm
|
||||
```
|
||||
|
||||
### Fuel 资源限制
|
||||
|
||||
```rust
|
||||
let mut store = Store::new(engine, HostState::new());
|
||||
store.set_fuel(1_000_000)?; // 分配 100 万 fuel
|
||||
store.limiter(|state| &mut state.limits); // 内存限制
|
||||
```
|
||||
|
||||
Fuel 不足时,WASM 执行会 trap(`wasm trap: interrupt`),Host 可以捕获并处理。
|
||||
|
||||
### 调用方法 — 同步,非 async
|
||||
|
||||
bindgen 生成的调用方法(`call_init`、`call_handle_event`)是同步的:
|
||||
|
||||
```rust
|
||||
// 正确
|
||||
instance.erp_plugin_plugin_api().call_init(&mut store)?;
|
||||
|
||||
// 错误(不存在 async 版本的调用方法)
|
||||
instance.erp_plugin_plugin_api().call_init(&mut store).await?;
|
||||
```
|
||||
|
||||
但实例化可以异步:`PluginWorld::instantiate_async(&mut store, &component, &linker).await?`
|
||||
|
||||
## 关键文件
|
||||
### 核心文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `crates/erp-plugin-prototype/wit/plugin.wit` | WIT 接口定义(Host API + Plugin API) |
|
||||
| `crates/erp-plugin-prototype/src/lib.rs` | Host 运行时:Engine/Store 创建、HostState、Host trait 实现 |
|
||||
| `crates/erp-plugin-prototype/src/lib.rs` | Host 运行时:Engine/Store/Linker/HostState |
|
||||
| `crates/erp-plugin-prototype/tests/test_plugin_integration.rs` | V1-V6 集成测试 |
|
||||
| `crates/erp-plugin-test-sample/src/lib.rs` | 测试插件:Guest trait 实现 |
|
||||
|
||||
## 关联模块
|
||||
### WIT 接口概要
|
||||
|
||||
- **[[architecture]]** — 插件架构是模块化单体的重要扩展机制
|
||||
- **[[erp-core]]** — EventBus 事件将被桥接到插件的 `handle_event`
|
||||
- **[[erp-server]]** — 未来集成插件运行时的组装点
|
||||
Host 暴露给插件(`host-api`):`db_insert` `db_query` `db_update` `db_delete` `event_publish` `config_get` `log_write` `current_user` `check_permission`
|
||||
|
||||
---
|
||||
插件导出给 Host(`plugin-api`):`init` `on_tenant_created` `handle_event`
|
||||
|
||||
# 插件制作完整流程
|
||||
### 集成契约
|
||||
|
||||
以下是从零创建一个新业务模块插件的完整步骤。
|
||||
| 方向 | 模块 | 接口 | 触发时机 |
|
||||
|------|------|------|---------|
|
||||
| 被调用 ← | [[erp-server]] | `PluginEngine` | 服务启动时恢复插件 |
|
||||
| 调用 → | [[erp-core]] | `EventBus` | 桥接领域事件到插件 |
|
||||
| 提供 → | 所有插件 | Host API (9 函数) | 插件运行时 |
|
||||
|
||||
## 第一步:准备 WIT 接口
|
||||
## 3. 代码逻辑
|
||||
|
||||
WIT 文件定义 Host 和插件之间的契约。现有接口位于 `crates/erp-plugin-prototype/wit/plugin.wit`。
|
||||
|
||||
如果新插件需要扩展 Host API(如新增文件上传、HTTP 代理等),在 `host-api` interface 中添加函数:
|
||||
|
||||
```wit
|
||||
// 在 host-api 中新增
|
||||
file-upload: func(filename: string, data: list<u8>) -> result<string, string>;
|
||||
http-proxy: func(url: string, method: string, body: option<list<u8>>) -> result<list<u8>, string>;
|
||||
```
|
||||
|
||||
如果插件需要新的生命周期钩子,在 `plugin-api` interface 中添加:
|
||||
|
||||
```wit
|
||||
// 在 plugin-api 中新增
|
||||
on-order-approved: func(order-id: string) -> result<_, string>;
|
||||
```
|
||||
|
||||
修改 WIT 后,需要重新编译 Host crate 和所有插件。
|
||||
|
||||
## 第二步:创建插件 crate
|
||||
|
||||
在 `crates/` 下创建新的插件 crate:
|
||||
|
||||
```bash
|
||||
mkdir -p crates/erp-plugin-<业务名>
|
||||
```
|
||||
|
||||
`Cargo.toml` 模板:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "erp-plugin-<业务名>"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "<业务描述> WASM 插件"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"] # 必须是 cdylib 才能编译为 WASM
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = "0.55" # 生成 Guest 端绑定
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
```
|
||||
|
||||
将新 crate 加入 workspace(编辑根 `Cargo.toml`):
|
||||
|
||||
```toml
|
||||
members = [
|
||||
# ... 已有成员 ...
|
||||
"crates/erp-plugin-<业务名>",
|
||||
]
|
||||
```
|
||||
|
||||
## 第三步:实现插件逻辑
|
||||
|
||||
创建 `src/lib.rs`,实现 `Guest` trait:
|
||||
|
||||
```rust
|
||||
//! <业务名> WASM 插件
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
// 生成 Guest 端绑定(路径指向 Host crate 的 WIT 文件)
|
||||
wit_bindgen::generate!({
|
||||
path: "../erp-plugin-prototype/wit/plugin.wit",
|
||||
world: "plugin-world",
|
||||
});
|
||||
|
||||
// 导入 Host 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<u8>) -> Result<(), String> {
|
||||
host_api::log_write("debug", &format!("收到事件: {}", event_type));
|
||||
|
||||
let data: serde_json::Value = serde_json::from_slice(&payload)
|
||||
.map_err(|e| format!("解析事件失败: {}", e))?;
|
||||
|
||||
match event_type.as_str() {
|
||||
"order.created" => {
|
||||
// 处理订单创建事件
|
||||
let order_id = data["id"].as_str().unwrap_or("");
|
||||
host_api::log_write("info", &format!("新订单: {}", order_id));
|
||||
}
|
||||
"workflow.task.completed" => {
|
||||
// 处理审批完成事件
|
||||
let order_id = data["order_id"].as_str().unwrap_or("unknown");
|
||||
let update = json!({"status": "approved"}).to_string();
|
||||
host_api::db_update("purchase_order", order_id, update.as_bytes(), 1)
|
||||
.map_err(|e| format!("更新失败: {}", e))?;
|
||||
|
||||
// 发布下游事件
|
||||
let evt = json!({"order_id": order_id}).to_string();
|
||||
host_api::event_publish("<业务>.order.approved", evt.as_bytes())
|
||||
.map_err(|e| format!("发布事件失败: {}", e))?;
|
||||
}
|
||||
_ => {
|
||||
host_api::log_write("debug", &format!("忽略事件: {}", event_type));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// 导出插件实例(宏会注册 Guest trait 实现)
|
||||
export!(MyPlugin);
|
||||
```
|
||||
|
||||
### Host API 速查
|
||||
|
||||
| 函数 | 签名 | 用途 |
|
||||
|------|------|------|
|
||||
| `db_insert` | `(entity, data) → result<record, string>` | 插入记录,Host 自动注入 id/tenant_id/timestamp |
|
||||
| `db_query` | `(entity, filter, pagination) → result<list, string>` | 查询记录,自动过滤 tenant_id + 排除软删除 |
|
||||
| `db_update` | `(entity, id, data, version) → result<record, string>` | 更新记录,检查乐观锁 version |
|
||||
| `db_delete` | `(entity, id) → result<_, string>` | 软删除记录 |
|
||||
| `event_publish` | `(event_type, payload) → result<_, string>` | 发布领域事件 |
|
||||
| `config_get` | `(key) → result<value, string>` | 读取系统配置 |
|
||||
| `log_write` | `(level, message) → ()` | 写日志,自动关联 tenant_id + plugin_id |
|
||||
| `current_user` | `() → result<user_info, string>` | 获取当前用户信息 |
|
||||
| `check_permission` | `(permission) → result<bool, string>` | 检查当前用户权限 |
|
||||
|
||||
### 数据传递约定
|
||||
|
||||
所有 Host API 的数据参数使用 `list<u8>`(即 `Vec<u8>`),约定用 JSON 序列化:
|
||||
|
||||
```rust
|
||||
// 构造数据
|
||||
let data = json!({"sku": "ITEM-001", "quantity": 100}).to_string();
|
||||
|
||||
// 插入
|
||||
let result_bytes = host_api::db_insert("inventory_item", data.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 解析返回
|
||||
let record: serde_json::Value = serde_json::from_slice(&result_bytes)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let new_id = record["id"].as_str().unwrap();
|
||||
```
|
||||
|
||||
## 第四步:编译为 WASM
|
||||
|
||||
```bash
|
||||
# 编译为 core WASM 模块
|
||||
cargo build -p erp-plugin-<业务名> --target wasm32-unknown-unknown --release
|
||||
|
||||
# 转换为 WASM Component(必须,Host 只接受 Component 格式)
|
||||
wasm-tools component new \
|
||||
target/wasm32-unknown-unknown/release/erp_plugin_<业务名>.wasm \
|
||||
-o target/erp_plugin_<业务名>.component.wasm
|
||||
```
|
||||
|
||||
检查产物大小(目标 < 2MB):
|
||||
|
||||
```bash
|
||||
ls -la target/erp_plugin_<业务名>.component.wasm
|
||||
```
|
||||
|
||||
## 第五步:编写集成测试
|
||||
|
||||
在 `crates/erp-plugin-prototype/tests/` 下创建测试文件,或扩展现有测试:
|
||||
|
||||
```rust
|
||||
use anyhow::Result;
|
||||
use erp_plugin_prototype::{create_engine, load_plugin};
|
||||
|
||||
fn wasm_path() -> String {
|
||||
"../../target/erp_plugin_<业务名>.component.wasm".into()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_<业务名>_init() -> Result<()> {
|
||||
let wasm_bytes = std::fs::read(wasm_path())?;
|
||||
let engine = create_engine()?;
|
||||
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
|
||||
|
||||
// 调用 init
|
||||
instance.erp_plugin_plugin_api().call_init(&mut store)?
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
// 验证 Host 端效果
|
||||
let state = store.data();
|
||||
assert!(state.db_ops.iter().any(|op| op.entity == "<业务>_config"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_<业务名>_handle_event() -> Result<()> {
|
||||
let wasm_bytes = std::fs::read(wasm_path())?;
|
||||
let engine = create_engine()?;
|
||||
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
|
||||
|
||||
// 先初始化
|
||||
instance.erp_plugin_plugin_api().call_init(&mut store)?
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
// 模拟事件
|
||||
let payload = json!({"id": "ORD-001"}).to_string();
|
||||
instance.erp_plugin_plugin_api()
|
||||
.call_handle_event(&mut store, "order.created", payload.as_bytes())?
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 第六步:运行测试
|
||||
|
||||
```bash
|
||||
# 先确保编译了 component
|
||||
cargo build -p erp-plugin-<业务名> --target wasm32-unknown-unknown --release
|
||||
wasm-tools component new target/wasm32-unknown-unknown/release/erp_plugin_<业务名>.wasm \
|
||||
-o target/erp_plugin_<业务名>.component.wasm
|
||||
|
||||
# 运行集成测试
|
||||
cargo test -p erp-plugin-prototype
|
||||
```
|
||||
|
||||
## 流程速查图
|
||||
### 插件生命周期
|
||||
|
||||
```
|
||||
1. 修改 WIT(如需新接口) crates/erp-plugin-prototype/wit/plugin.wit
|
||||
↓
|
||||
2. 创建插件 crate crates/erp-plugin-<名>/
|
||||
- Cargo.toml (cdylib + wit-bindgen)
|
||||
- src/lib.rs (impl Guest)
|
||||
↓
|
||||
3. 编译 core wasm cargo build --target wasm32-unknown-unknown --release
|
||||
↓
|
||||
4. 转为 component wasm-tools component new <in.wasm> -o <out.component.wasm>
|
||||
↓
|
||||
5. 编写测试 crates/erp-plugin-prototype/tests/
|
||||
↓
|
||||
6. 运行测试 cargo test -p erp-plugin-prototype
|
||||
安装(manifest+WASM) → 注册权限 → 实例化(Wasmtime) → init()
|
||||
→ 日常: db_query/db_insert 通过 data_handler 自动注入 tenant_id + 权限校验
|
||||
→ 事件: EventBus → handle_event()
|
||||
→ 升级: upload 新 WASM → 增量 DDL → 卸载旧实例 → 加载新实例
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
### 权限系统
|
||||
|
||||
### Q: "attempted to parse a wasm module with a component parser"
|
||||
**A:** 使用了 core WASM 而非 Component。运行 `wasm-tools component new` 转换。
|
||||
|
||||
### Q: "cannot infer type of the type parameter D"
|
||||
**A:** `add_to_linker` 需要显式指定 `HasSelf<T>`:`add_to_linker::<_, HasSelf<HostState>>(linker, |s| s)`。
|
||||
|
||||
### Q: "wasm trap: interrupt"(非 fuel 耗尽)
|
||||
**A:** 检查是否启用了 epoch_interruption 但未定期 bump epoch。原型阶段建议只使用 fuel 限制。
|
||||
|
||||
### Q: 插件中如何调试?
|
||||
**A:** 使用 `host_api::log_write("debug", "message")` 输出日志,Host 端 `store.data().logs` 可查看所有日志。
|
||||
|
||||
### Q: 如何限制插件内存?
|
||||
**A:** 通过 `StoreLimitsBuilder` 配置:
|
||||
```rust
|
||||
let limits = StoreLimitsBuilder::new()
|
||||
.memory_size(10 * 1024 * 1024) // 10MB
|
||||
.build();
|
||||
```
|
||||
|
||||
## 后续规划
|
||||
|
||||
- **Phase 7**: 将原型集成到 erp-server,替换模拟 Host API 为真实数据库操作
|
||||
- **动态表**: 支持 `db_insert("dynamic_table", ...)` 自动创建/迁移表
|
||||
- **前端集成**: PluginCRUDPage 组件根据 WIT 定义自动生成 CRUD 页面
|
||||
- **插件市场**: 插件元数据、版本管理、签名验证
|
||||
|
||||
## 插件权限系统(关键)
|
||||
|
||||
### 权限码格式
|
||||
|
||||
插件数据操作的权限码由 `data_handler.rs` 中的 `compute_permission_code()` 按以下规则自动生成:
|
||||
权限码由 `data_handler.rs` 的 `compute_permission_code()` 自动生成:
|
||||
|
||||
```
|
||||
{manifest_id}.{url_entity_name}.{action_suffix}
|
||||
例: erp-crm.customer.list / erp-crm.customer.manage
|
||||
```
|
||||
|
||||
- `manifest_id`:plugin.toml 中 `[metadata].id`(如 `erp-crm`)
|
||||
- `url_entity_name`:REST API 路径中的实体名(如 `customer_tag`)
|
||||
- `action_suffix`:`list`(读操作)或 `manage`(写操作)
|
||||
⚡ **权限命名铁律**: `plugin.toml` 中 `permissions[].code` 前缀必须与 `schema.entities[].name` 完全一致,每个实体必须声明 `.list` + `.manage`
|
||||
|
||||
| 操作 | 权限码示例 |
|
||||
|------|-----------|
|
||||
| 列表/详情 | `erp-crm.customer.list` |
|
||||
| 创建/更新/删除 | `erp-crm.customer.manage` |
|
||||
⚡ **Host API 数据约定**: 所有数据参数用 `list<u8>` + JSON 序列化,Host 自动注入 id/tenant_id/timestamp
|
||||
|
||||
### 权限码命名铁律(P0 级)
|
||||
⚡ **同步调用**: bindgen 生成的 `call_init`/`call_handle_event` 是同步的,只有实例化可以 async
|
||||
|
||||
**`plugin.toml` 中 `permissions[].code` 的前缀必须与 `schema.entities[].name` 完全一致。**
|
||||
### 不变量
|
||||
|
||||
```
|
||||
data_handler 生成:{manifest_id}.{url_entity_name}.{action}
|
||||
↑ 来自 URL 路径中的 entity 参数
|
||||
manifest 声明: {entity_name}.{action}
|
||||
↑ 必须与 URL 中的 entity name 匹配
|
||||
```
|
||||
⚡ Fuel 默认 100 万,耗尽时 WASM trap(`wasm trap: interrupt`)
|
||||
⚡ `HasSelf<HostState>` 是 Linker 注册的必要类型参数(`Data<'a> = &'a mut HostState`)
|
||||
⚡ Core WASM 必须通过 `wasm-tools component new` 转为 Component 格式才能被 Host 加载
|
||||
|
||||
每个实体必须同时声明 `.list` 和 `.manage` 两个权限:
|
||||
## 4. 活跃问题 + 陷阱
|
||||
|
||||
```toml
|
||||
# ✅ 正确:权限码前缀与实体名一致
|
||||
[[schema.entities]]
|
||||
name = "customer_tag"
|
||||
### 历史教训
|
||||
|
||||
[[permissions]]
|
||||
code = "customer_tag.list" # 匹配!
|
||||
- CRM 权限码 `tag.manage` vs 实体 `customer_tag` → 三个页面 403(迁移 m000038 修复)
|
||||
- CRM WASM 二进制错误存储了测试插件而非 CRM 插件(重新编译修复)
|
||||
- 权限未自动分配给 admin 角色 → 403(添加 `grant_permissions_to_admin()`)
|
||||
|
||||
[[permissions]]
|
||||
code = "customer_tag.manage" # 匹配!
|
||||
### 注意事项
|
||||
|
||||
# ❌ 错误:权限码用了简写,与实体名不一致 → 403
|
||||
[[permissions]]
|
||||
code = "tag.manage" # data_handler 生成 erp-crm.customer_tag.manage
|
||||
# 但 DB 中只有 erp-crm.tag.manage → 403
|
||||
```
|
||||
⚠️ 插件 API 路由用 `Path<(Uuid, String)>` 解析 plugin_id,必须用数据库 UUID 而非 manifest_id
|
||||
⚠️ 修改 WIT 后需重编译 Host crate 和所有插件
|
||||
|
||||
**历史教训:** CRM 插件首个版本中,`customer_tag` 实体的权限码写成了 `tag.manage`,`customer_relationship` 实体的权限码写成了 `relationship.list/manage`。结果标签管理、客户关系、关系图谱三个页面全部 403。修复迁移:`m20260419_000038_fix_crm_permission_codes.rs`。
|
||||
## 5. 变更记录
|
||||
|
||||
### 权限注册流程
|
||||
|
||||
1. **插件安装时** → `register_plugin_permissions()` 将 manifest 中声明的权限批量 INSERT 到 `permissions` 表(`ON CONFLICT DO NOTHING` 保证幂等)
|
||||
2. **权限分配** → `grant_permissions_to_admin()` 自动将权限分配给 admin 角色
|
||||
3. **运行时校验** → `data_handler.rs` 的 `compute_permission_code()` 按 URL entity name 生成权限码,通过 `require_permission()` 检查 JWT 中的权限列表
|
||||
|
||||
### 已修复问题
|
||||
|
||||
| 问题 | 修复 |
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 权限未自动分配给 admin 角色 → 403 | `grant_permissions_to_admin()` 在 install/enable 时自动调用 |
|
||||
| 权限码与实体名不匹配 → 403 | 迁移 m20260419_000038 + plugin.toml 修正 |
|
||||
### 插件 API 路由注意事项
|
||||
| 2026-04-23 | 重构为 5 节结构,插件制作流程移至 `.claude/skills/plugin-development/` |
|
||||
| 2026-04-19 | CRM 权限码修复 (m000038) |
|
||||
| 2026-04-18 | 插件权限系统审计 |
|
||||
|
||||
- 后端路由使用 `Path<(Uuid, String)>` 解析 `plugin_id`,必须是 UUID 格式
|
||||
- 前端使用 `plugin.id`(数据库 UUID)而非 `manifest_id`(如 `erp-crm`)构建请求 URL
|
||||
- 直接用 manifest_id 调用 API 会返回 `UUID parsing failed` 错误
|
||||
> **插件制作完整流程**: 详见 `.claude/skills/plugin-development/SKILL.md`(WIT 接口 → 创建 crate → 编译 WASM → 集成测试)
|
||||
|
||||
Reference in New Issue
Block a user