docs: 全局文档梳理归档 — 删除过期文件 + 归档 V1/早期设计 + wiki 数据校正 + CLAUDE.md 规则优化
**根目录清理:** - 删除 CLAUDE-1.md(ZCLAW 旧项目配置,HMS 已完全脱离) - 移动 DESIGN.md → docs/archive/(ERP 旧设计系统) - 删除 plans/ 98 个临时会话计划文件 **归档重组:** - V1 审计(12 文件)→ docs/archive/audits-v1/ - 早期 CRM/插件迭代设计(13 文件)→ docs/archive/superpowers-early/ - 已完成/已取代设计(28 文件)→ docs/archive/superpowers-completed/ - 早期讨论/测试报告 → docs/archive/discussions-early/ + test-reports-early/ - QA 重复文件清理(3 个旧版 result 文件) **wiki 数据校正:** - 迁移数 137→145,源文件 599→649,提交数 720→800+ - 小程序文件 124→163,Web 前端 297→332 - 后端测试 999→943(实际统计),权限码 75+→128 - 文档索引新增归档目录说明 **CLAUDE.md 规则优化:** - §2.5 闭环工作法:提交+文档+推送三合一 + wiki 更新触发条件 - §2.6 Feature DoD:新增文档一致性检查项 - §6 反模式:新增 wiki 更新滞后/推送不及时警告
This commit is contained in:
583
CLAUDE-1.md
583
CLAUDE-1.md
@@ -1,583 +0,0 @@
|
||||
# ZCLAW 协作与实现规则
|
||||
|
||||
> **ZCLAW 是一个独立成熟的 AI Agent 桌面客户端**,专注于提供真实可用的 AI 能力,而不是演示 UI。
|
||||
|
||||
> **当前阶段: 发布前管家模式实施。** 稳定化基线已达成,管家模式6交付物已完成。
|
||||
|
||||
## 1. 项目定位
|
||||
|
||||
### 1.1 ZCLAW 是什么
|
||||
|
||||
ZCLAW 是面向中文用户的 AI Agent 桌面端,核心能力包括:
|
||||
|
||||
- **智能对话** - 多模型支持(8 Provider)、流式响应、上下文管理
|
||||
- **自主能力** - 9 个启用的 Hands(另有 Predictor/Lead 已禁用)
|
||||
- **技能系统** - 75 个 SKILL.md 技能定义
|
||||
- **工作流编排** - Pipeline DSL + 10 行业模板
|
||||
- **安全审计** - 完整的操作日志和权限控制
|
||||
|
||||
### 1.2 决策原则
|
||||
|
||||
**任何改动都要问:这对 ZCLAW 用户今天能产生价值吗?**
|
||||
|
||||
- ✅ 修复已知的 P0/P1 缺陷 → 最高优先
|
||||
- ✅ 接通"写了没接"的断链 → 高优先
|
||||
- ✅ 清理死代码和孤立文件 → 应该做
|
||||
- ❌ 新增功能/页面/端点 → 稳定化完成前禁止
|
||||
- ❌ 增加复杂度但无实际价值 → 永远不做
|
||||
- ❌ 折中方案掩盖根因 → 永远不做
|
||||
|
||||
### 1.3 稳定化铁律
|
||||
|
||||
**稳定化基线达成后仍需遵守以下约束:**
|
||||
|
||||
| 禁止行为 | 原因 |
|
||||
|----------|------|
|
||||
| 新增 SaaS API 端点 | 已有 140 个(含 2 个 dev-only),前端未全部接通 |
|
||||
| 新增 SKILL.md 文件 | 已有 75 个,大部分未执行验证 |
|
||||
| 新增 Tauri 命令 | 已有 189 个,70 个无前端调用且无 @reserved |
|
||||
| 新增中间件/Store | 已有 13 层中间件 + 18 个 Store |
|
||||
| 新增 admin 页面 | 已有 15 页 |
|
||||
|
||||
### 1.4 系统真实状态
|
||||
|
||||
参见 [docs/TRUTH.md](docs/TRUTH.md) — 这是唯一的真相源,所有其他文档中的数字如果与此冲突,以 TRUTH.md 为准。
|
||||
***
|
||||
|
||||
## 2. 项目结构
|
||||
|
||||
```text
|
||||
ZCLAW/
|
||||
├── crates/ # Rust Workspace (10 crates)
|
||||
│ ├── zclaw-types/ # L1: 基础类型 (AgentId, Message, Error)
|
||||
│ ├── zclaw-memory/ # L2: 存储层 (SQLite, KV, 会话管理)
|
||||
│ ├── zclaw-runtime/ # L3: 运行时 (4 Driver, 7 工具, 12 层中间件)
|
||||
│ ├── zclaw-kernel/ # L4: 核心协调 (182 Tauri 命令)
|
||||
│ ├── zclaw-skills/ # 技能系统 (75 SKILL.md 解析, 语义路由)
|
||||
│ ├── zclaw-hands/ # 自主能力 (9 启用, 106 Rust 测试)
|
||||
│ ├── zclaw-protocols/ # 协议支持 (MCP 完整, A2A feature-gated)
|
||||
│ ├── zclaw-pipeline/ # Pipeline DSL (v1/v2, 10 行业模板)
|
||||
│ ├── zclaw-growth/ # 记忆增长 (FTS5 + TF-IDF)
|
||||
│ └── zclaw-saas/ # SaaS 后端 (130 API, Axum + PostgreSQL)
|
||||
├── admin-v2/ # 管理后台 (Vite + Ant Design Pro, 13 页)
|
||||
├── desktop/ # Tauri 桌面应用
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React UI 组件 (含 SaaS 集成)
|
||||
│ │ ├── store/ # Zustand 状态管理 (含 saasStore)
|
||||
│ │ └── lib/ # 客户端通信 / 工具函数 (含 saas-client)
|
||||
│ └── src-tauri/ # Tauri Rust 后端 (集成 Kernel)
|
||||
├── skills/ # SKILL.md 技能定义
|
||||
├── hands/ # HAND.toml 自主能力配置
|
||||
├── config/ # TOML 配置文件
|
||||
├── saas-config.toml # SaaS 后端配置 (PostgreSQL 连接等)
|
||||
├── docker-compose.yml # PostgreSQL 容器配置
|
||||
├── docs/ # 架构文档和知识库
|
||||
└── tests/ # Vitest 回归测试
|
||||
```
|
||||
|
||||
### 2.1 核心数据流
|
||||
|
||||
```text
|
||||
用户操作 → React UI → Zustand Store → Tauri Commands → zclaw-kernel → LLM/Tools/Skills/Hands
|
||||
```
|
||||
|
||||
### 2.2 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
| ---- | --------------------- |
|
||||
| 前端框架 | React 19 + TypeScript |
|
||||
| 状态管理 | Zustand 5 |
|
||||
| 桌面框架 | Tauri 2.x |
|
||||
| 样式方案 | Tailwind 4 |
|
||||
| 配置格式 | TOML |
|
||||
| 后端核心 | Rust Workspace (10 crates, ~66K 行) |
|
||||
| SaaS 后端 | Axum + PostgreSQL (zclaw-saas) |
|
||||
| 管理后台 | Vite + Ant Design Pro (admin-v2/) |
|
||||
|
||||
### 2.3 Crate 依赖关系
|
||||
|
||||
```text
|
||||
zclaw-types (无依赖)
|
||||
↑
|
||||
zclaw-memory (→ types)
|
||||
↑
|
||||
zclaw-runtime (→ types, memory)
|
||||
↑
|
||||
zclaw-kernel (→ types, memory, runtime)
|
||||
↑
|
||||
zclaw-saas (→ types, 独立运行于 8080 端口)
|
||||
↑
|
||||
desktop/src-tauri (→ kernel, skills, hands, protocols)
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 3. 工作风格
|
||||
|
||||
### 3.1 交付导向
|
||||
|
||||
- **先做最高杠杆问题** - 解决用户最痛的点
|
||||
- **真实能力优先** - 不做假数据占位
|
||||
- **完整闭环** - 每个功能都要能真正使用
|
||||
|
||||
### 3.2 根因优先
|
||||
|
||||
遇到问题时,先确认属于哪一类:
|
||||
|
||||
1. **协议问题** - API 端点、请求格式、响应解析
|
||||
2. **状态问题** - Store 更新、组件同步
|
||||
3. **UI 问题** - 交互逻辑、样式显示
|
||||
4. **配置问题** - TOML 解析、环境变量
|
||||
5. **运行时问题** - 服务启动、端口占用
|
||||
|
||||
不在根因未明时盲目堆补丁。
|
||||
|
||||
### 3.3 闭环工作法(强制)
|
||||
|
||||
每次改动**必须**按顺序完成以下步骤,不允许跳过:
|
||||
|
||||
1. **定位问题** — 理解根因,不盲目堆补丁
|
||||
2. **最小修复** — 只改必要的代码
|
||||
3. **自动验证** — `tsc --noEmit` / `cargo check` / `vitest run` 必须通过
|
||||
4. **提交推送** — 按 §11 规范提交,**立即 `git push`**,不积压
|
||||
5. **文档同步** — 按 §8.3 检查并更新相关文档,提交并推送
|
||||
|
||||
**铁律:步骤 4 和 5 是任务完成的硬性条件。不允许"等一下再提交"或"最后一起推送"。**
|
||||
|
||||
***
|
||||
|
||||
## 4. 实现规则
|
||||
|
||||
### 4.1 通信层
|
||||
|
||||
所有与后端的通信必须通过统一的客户端层:
|
||||
|
||||
- `desktop/src/lib/gateway-client.ts` - 主要通信客户端
|
||||
- `desktop/src/lib/tauri-gateway.ts` - Tauri 原生命令
|
||||
|
||||
**禁止**在组件内直接创建 WebSocket 或拼装 HTTP 请求。
|
||||
|
||||
### 4.2 分层职责
|
||||
|
||||
```
|
||||
UI 组件 → 只负责展示和交互
|
||||
Store → 负责状态组织和流程编排
|
||||
Client → 负责网络通信和协议转换
|
||||
```
|
||||
|
||||
### 4.3 代码自检规则
|
||||
|
||||
**每次修改代码前必须检查:**
|
||||
|
||||
1. **是否已有相同能力的代码?** — 先搜索再写,避免重复
|
||||
2. **前端是否有人调用?** — 没有 Rust 调用者的 Tauri 命令,先标注 `@reserved`
|
||||
3. **错误是否静默吞掉?** — `let _ =` 必须替换为 `log::warn!` 或更高级别处理
|
||||
4. **文档数字是否需要更新?** — 改了数量就要改文档```
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
### 4.4 代码规范
|
||||
|
||||
**TypeScript:**
|
||||
- 避免 `any`,优先 `unknown + 类型守卫`
|
||||
- 外部数据必须做容错解析
|
||||
- 不假设 API 响应永远只有一种格式
|
||||
|
||||
**React:**
|
||||
- 使用函数组件 + hooks
|
||||
- 复杂副作用收敛到 store
|
||||
- 组件保持"展示层"职责
|
||||
|
||||
**配置处理:**
|
||||
- 使用 TOML 解析器
|
||||
- 支持环境变量插值 `${VAR_NAME}`
|
||||
- 写回时保持格式一致
|
||||
|
||||
---
|
||||
|
||||
## 5. UI 完成度标准
|
||||
|
||||
### 5.1 允许存在的 UI
|
||||
|
||||
- 已接入真实后端能力的 UI
|
||||
- 明确标注"开发中 / 只读"的 UI
|
||||
- 有降级方案的 UI
|
||||
|
||||
### 5.2 不允许存在的 UI
|
||||
|
||||
- 看似可编辑但不会生效的设置
|
||||
- 展示假状态的面板
|
||||
- 用 mock 数据掩盖未完成能力
|
||||
|
||||
### 5.3 核心功能 UI
|
||||
|
||||
| 功能 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 聊天界面 | ✅ 完成 | 流式响应、多模型切换 |
|
||||
| 分身管理 | ✅ 完成 | 创建、配置、切换 Agent |
|
||||
| 自动化面板 | ✅ 完成 | Hands 触发、参数配置 |
|
||||
| 技能市场 | 🚧 进行中 | 技能浏览和安装 |
|
||||
| 工作流编辑 | 🚧 进行中 | Pipeline 工作流编辑器 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 自主能力系统 (Hands)
|
||||
|
||||
ZCLAW 提供 11 个自主能力包(9 启用 + 2 禁用):
|
||||
|
||||
| Hand | 功能 | 状态 |
|
||||
|------|------|------|
|
||||
| Browser | 浏览器自动化 | ✅ 可用 |
|
||||
| Collector | 数据收集聚合 | ✅ 可用 |
|
||||
| Researcher | 深度研究 | ✅ 可用 |
|
||||
| Predictor | 预测分析 | ❌ 已禁用 (enabled=false),无 Rust 实现 |
|
||||
| Lead | 销售线索发现 | ❌ 已禁用 (enabled=false),无 Rust 实现 |
|
||||
| Clip | 视频处理 | ⚠️ 需 FFmpeg |
|
||||
| Twitter | Twitter 自动化 | ✅ 可用(12 个 API v2 真实调用,写操作需 OAuth 1.0a) |
|
||||
| Whiteboard | 白板演示 | ✅ 可用(导出功能开发中,标注 demo) |
|
||||
| Slideshow | 幻灯片生成 | ✅ 可用 |
|
||||
| Speech | 语音合成 | ✅ 可用(Browser TTS 前端集成完成) |
|
||||
| Quiz | 测验生成 | ✅ 可用 |
|
||||
|
||||
**触发 Hand 时:**
|
||||
1. 检查依赖是否满足
|
||||
2. 收集必要参数
|
||||
3. 处理 `needs_approval` 状态
|
||||
4. 记录执行日志
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试与验证
|
||||
|
||||
### 7.1 必测场景
|
||||
|
||||
修改以下内容后必须验证:
|
||||
|
||||
- 聊天 / 流式响应
|
||||
- Store 状态更新
|
||||
- 配置读写
|
||||
- Hand 触发
|
||||
|
||||
### 7.2 前端调试优先使用 WebMCP
|
||||
|
||||
ZCLAW 注册了 WebMCP 结构化调试工具(`desktop/src/lib/webmcp-tools.ts`),AI 代理可直接查询应用状态而无需 DOM 截图。
|
||||
|
||||
**原则:能用 WebMCP 工具完成的调试,优先使用 WebMCP 而非 DevTools MCP(`take_snapshot`/`evaluate_script`),以减少约 67% 的 token 消耗。**
|
||||
|
||||
已注册的 WebMCP 工具:
|
||||
|
||||
| 工具名 | 用途 |
|
||||
|--------|------|
|
||||
| `get_zclaw_state` | 综合状态概览(连接、登录、流式、模型) |
|
||||
| `check_connection` | 连接状态检查 |
|
||||
| `send_message` | 发送聊天消息 |
|
||||
| `cancel_stream` | 取消当前流式响应 |
|
||||
| `get_streaming_state` | 流式响应详细状态 |
|
||||
| `list_conversations` | 列出最近对话 |
|
||||
| `get_current_conversation` | 获取当前对话完整消息 |
|
||||
| `switch_conversation` | 切换到指定对话 |
|
||||
| `get_token_usage` | Token 用量统计 |
|
||||
| `get_offline_queue` | 离线消息队列 |
|
||||
| `get_saas_account` | SaaS 账户和订阅信息 |
|
||||
| `get_available_models` | 可用 LLM 模型列表 |
|
||||
| `get_current_agent` | 当前 Agent 详情 |
|
||||
| `list_agents` | 所有 Agent 列表 |
|
||||
| `get_console_errors` | 应用日志中的错误 |
|
||||
|
||||
**使用前提**:Chrome 146+ 并启用 `chrome://flags/#enable-webmcp-testing`。仅在开发模式注册。
|
||||
|
||||
**何时仍需 DevTools MCP**:UI 布局/样式问题、点击交互、截图对比、网络请求检查。
|
||||
|
||||
### 7.3 验证命令
|
||||
|
||||
```bash
|
||||
# TypeScript 类型检查
|
||||
pnpm tsc --noEmit
|
||||
|
||||
# 前端单元测试
|
||||
cd desktop && pnpm vitest run
|
||||
|
||||
# Rust 全量测试(排除 SaaS)
|
||||
cargo test --workspace --exclude zclaw-saas
|
||||
|
||||
# SaaS 集成测试(需要 PostgreSQL)
|
||||
export TEST_DATABASE_URL="postgresql://postgres:123123@localhost:5432/zclaw"
|
||||
cargo test -p zclaw-saas -- --test-threads=1
|
||||
|
||||
# 启动开发环境
|
||||
pnpm start:dev
|
||||
````
|
||||
|
||||
### 7.4 人工验证清单
|
||||
|
||||
- [ ] 能否正常连接后端服务
|
||||
- [ ] 能否发送消息并获得流式响应
|
||||
- [ ] 模型切换是否生效
|
||||
- [ ] Hand 触发是否正常执行
|
||||
- [ ] 配置保存是否持久化
|
||||
|
||||
***
|
||||
|
||||
## 8. 文档管理
|
||||
|
||||
### 8.1 文档结构
|
||||
|
||||
```text
|
||||
docs/
|
||||
├── features/ # 功能文档
|
||||
│ ├── README.md # 功能索引
|
||||
│ └── */ # 各功能详细文档
|
||||
├── knowledge-base/ # 技术知识库
|
||||
│ ├── troubleshooting.md
|
||||
│ └── *.md
|
||||
└── archive/ # 归档文档
|
||||
```
|
||||
|
||||
### 8.2 文档更新原则
|
||||
|
||||
- **修完就记** - 解决问题后立即更新文档
|
||||
- **面向未来** - 文档要帮助未来的开发者快速理解
|
||||
- **中文优先** - 所有面向用户的文档使用中文
|
||||
|
||||
### 8.3 完成工作后的收尾流程(强制,不可跳过)
|
||||
|
||||
每次完成功能实现、架构变更、问题修复后,**必须立即执行以下收尾**:
|
||||
|
||||
#### 步骤 A:文档同步(代码提交前)
|
||||
|
||||
检查以下文档是否需要更新,有变更则立即修改:
|
||||
|
||||
1. **CLAUDE.md** — 项目结构、技术栈、工作流程、命令变化时
|
||||
2. **CLAUDE.md §13 架构快照** — 涉及子系统变更时,更新 `<!-- ARCH-SNAPSHOT-START/END -->` 标记区域(可执行 `/sync-arch` 技能自动分析)
|
||||
3. **docs/ARCHITECTURE_BRIEF.md** — 架构决策或关键组件变更时
|
||||
4. **docs/features/** — 功能状态变化时
|
||||
5. **docs/knowledge-base/** — 新的排查经验或配置说明
|
||||
6. **docs/TRUTH.md** — 数字(命令数、Store 数、crates 数等)变化时
|
||||
|
||||
#### 步骤 B:提交(按逻辑分组)
|
||||
|
||||
```
|
||||
代码变更 → 一个或多个逻辑提交
|
||||
文档变更 → 独立提交(如果和代码分开更清晰)
|
||||
```
|
||||
|
||||
#### 步骤 C:推送(立即)
|
||||
|
||||
```
|
||||
git push
|
||||
```
|
||||
|
||||
**不允许积压。** 每次完成一个独立工作单元后立即推送。不要留到"最后一起推"。
|
||||
|
||||
**判断标准:** 如果工作目录有未提交文件,说明收尾流程没完成。
|
||||
|
||||
***
|
||||
|
||||
## 9. 常见问题排查
|
||||
|
||||
### 9.1 连接问题
|
||||
|
||||
1. 检查后端服务是否启动(端口 50051)
|
||||
2. 检查 Vite 代理配置
|
||||
3. 检查防火墙设置
|
||||
|
||||
### 9.2 状态问题
|
||||
|
||||
1. 检查 Store 是否正确订阅
|
||||
2. 检查组件是否在正确的 Store 获取数据
|
||||
3. 检查是否有多个 Store 实例
|
||||
|
||||
### 9.3 配置问题
|
||||
|
||||
1. 检查 TOML 语法
|
||||
2. 检查环境变量是否设置
|
||||
3. 检查配置文件路径
|
||||
|
||||
***
|
||||
|
||||
## 10. 常用命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 开发模式
|
||||
pnpm start:dev
|
||||
|
||||
# 仅启动桌面端
|
||||
pnpm desktop
|
||||
|
||||
# 构建生产版本
|
||||
pnpm build
|
||||
|
||||
# 类型检查
|
||||
pnpm tsc --noEmit
|
||||
|
||||
# 运行测试
|
||||
pnpm vitest run
|
||||
|
||||
# 停止所有服务
|
||||
pnpm start:stop
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 11. 提交规范
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
**类型:**
|
||||
|
||||
- `feat` - 新功能
|
||||
- `fix` - 修复问题
|
||||
- `refactor` - 重构
|
||||
- `docs` - 文档更新
|
||||
- `test` - 测试相关
|
||||
- `chore` - 杂项
|
||||
|
||||
**示例:**
|
||||
|
||||
```
|
||||
feat(hands): 添加参数预设保存功能
|
||||
fix(chat): 修复流式响应中断问题
|
||||
refactor(store): 统一 Store 数据获取方式
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
|
||||
## 12. 安全注意事项
|
||||
|
||||
- 不在代码中硬编码密钥
|
||||
- 用户输入必须验证
|
||||
- 敏感操作需要确认
|
||||
- 保留操作审计日志
|
||||
- 环境变量 `ZCLAW_SAAS_DEV` 模式放宽安全限制(开发环境设 `ZCLAW_SAAS_DEV=true`)
|
||||
|
||||
### 认证安全
|
||||
|
||||
- **JWT password_version**: 密码修改后自动使所有已签发的 JWT 失效(Claims 含 `pwv`,中间件比对 DB)
|
||||
- **账户锁定**: 5 次登录失败后锁定 15 分钟
|
||||
- **邮箱验证**: RFC 5322 正则 + 254 字符长度限制
|
||||
- **JWT 密钥**: `#[cfg(debug_assertions)]` 保护 fallback,release 模式 `bail` 拒绝启动
|
||||
- **TOTP 加密密钥**: 生产环境强制独立 `ZCLAW_TOTP_ENCRYPTION_KEY`(64 字符 hex),不从 JWT 密钥派生
|
||||
- **TOTP/API Key 加密**: AES-256-GCM + 随机 Nonce
|
||||
- **密码存储**: Argon2id + OsRng 随机盐
|
||||
- **Refresh Token 轮换**: 单次使用,Logout 时撤销到 DB,rotation 校验已撤销的旧 token
|
||||
|
||||
### 网络安全
|
||||
|
||||
- **Cookie**: HttpOnly + Secure + SameSite=Strict + 路径作用域
|
||||
- **Cookie Secure**: 开发环境 false,生产 true
|
||||
- **CORS**: 生产强制白名单,缺失拒绝启动
|
||||
- **TLS**: 反向代理(nginx/caddy)提供 HTTPS 终止,Axum 不负责 TLS
|
||||
- **Docker**: SaaS 端口绑定 `127.0.0.1`,仅通过 nginx 反代访问
|
||||
- **XFF**: 仅信任配置的代理 IP
|
||||
|
||||
### 限流
|
||||
|
||||
- `/api/auth/login` — 5次/分钟/IP(防暴力破解)+ 持久化到 PostgreSQL
|
||||
- `/api/auth/register` — 3次/小时/IP(防刷注册)
|
||||
- 公共端点默认 20次/分钟/IP(防滥用)
|
||||
|
||||
### 前端安全
|
||||
|
||||
- **Admin Token**: HttpOnly Cookie 传递,JS 不存储/读取 token
|
||||
- **Tauri CSP**: 移除 `unsafe-inline` script,`connect-src` 限制为 `http://localhost:*` + `https://*`
|
||||
- **Pipeline 日志**: Debug 日志截断 + 仅记录 keys 不记录 values
|
||||
|
||||
### 环境变量
|
||||
|
||||
| 变量 | 用途 |
|
||||
|------|------|
|
||||
| `DB_PASSWORD` | 数据库密码 |
|
||||
| `ZCLAW_DATABASE_URL` | 完整数据库连接 URL(优先级最高) |
|
||||
| `ZCLAW_SAAS_JWT_SECRET` | JWT 签名密钥 (>= 32 字符) |
|
||||
| `ZCLAW_TOTP_ENCRYPTION_KEY` | TOTP/API Key 加密密钥 (64 hex) |
|
||||
| `ZCLAW_ADMIN_USERNAME` | 初始管理员用户名 |
|
||||
| `ZCLAW_ADMIN_PASSWORD` | 初始管理员密码 |
|
||||
| `ZCLAW_SAAS_DEV` | 开发模式标志 (true=开发, false=生产) |
|
||||
|
||||
`saas-config.toml` 支持 `${ENV_VAR}` 模式环境变量插值。
|
||||
|
||||
### 生产环境清单
|
||||
|
||||
- [ ] nginx/caddy 配置反向代理 + HTTPS
|
||||
- [ ] 确保设置 `ZCLAW_SAAS_DEV=false`(或不设置)
|
||||
- [ ] 启用 CORS 白名单(`cors_origins` 配置实际域名)
|
||||
- [ ] Cookie Secure=true + HttpOnly=true + SameSite=Strict
|
||||
- [ ] JWT 签名密钥 >= 32 字符随机字符串
|
||||
- [ ] `ZCLAW_TOTP_ENCRYPTION_KEY` 独立设置
|
||||
- [ ] 数据库密码通过 `${DB_PASSWORD}` 引用
|
||||
|
||||
### 完整审计报告
|
||||
|
||||
参见 `docs/features/SECURITY_PENETRATION_TEST_V1.md`
|
||||
|
||||
***
|
||||
|
||||
<!-- ARCH-SNAPSHOT-START -->
|
||||
<!-- 此区域由 auto-sync 自动更新,请勿手动编辑。更新时间: 2026-04-09 -->
|
||||
|
||||
## 13. 当前架构快照
|
||||
|
||||
### 活跃子系统
|
||||
|
||||
| 子系统 | 状态 | 最新变更 |
|
||||
|--------|------|----------|
|
||||
| 管家模式 (Butler) | ✅ 活跃 | 04-09 ButlerRouter + 双模式UI + 痛点持久化 + 冷启动 |
|
||||
| Hermes 管线 | ✅ 活跃 | 04-09 4 Chunk: 自我改进+用户建模+NL Cron+轨迹压缩 (684 tests) |
|
||||
| 聊天流 (ChatStream) | ✅ 稳定 | 04-02 ChatStore 拆分为 4 Store (stream/conversation/message/chat) |
|
||||
| 记忆管道 (Memory) | ✅ 稳定 | 04-02 闭环修复: 对话→提取→FTS5+TF-IDF→检索→注入 |
|
||||
| SaaS 认证 (Auth) | ✅ 稳定 | Token池 RPM/TPM 轮换 + JWT password_version 失效机制 |
|
||||
| Pipeline DSL | ✅ 稳定 | 04-01 17 个 YAML 模板 + DAG 执行器 |
|
||||
| Hands 系统 | ✅ 稳定 | 9 启用 (Browser/Collector/Researcher/Twitter/Whiteboard/Slideshow/Speech/Quiz/Clip) |
|
||||
| 技能系统 (Skills) | ✅ 稳定 | 75 个 SKILL.md + 语义路由 |
|
||||
| 中间件链 | ✅ 稳定 | 14 层 (含 DataMasking@90, ButlerRouter, TrajectoryRecorder@650) |
|
||||
|
||||
### 关键架构模式
|
||||
|
||||
- **Hermes 管线**: 4模块闭环 — ExperienceStore(FTS5经验存取) + UserProfiler(结构化用户画像) + NlScheduleParser(中文时间→cron) + TrajectoryRecorder+Compressor(轨迹记录压缩)。通过中间件链+intelligence hooks调用
|
||||
- **管家模式**: 双模式UI (默认简洁/解锁专业) + ButlerRouter 4域关键词分类 (healthcare/data_report/policy/meeting) + 冷启动4阶段hook (idle→greeting→waiting→completed) + 痛点双写 (内存Vec+SQLite)
|
||||
- **聊天流**: 3种实现 → GatewayClient(WebSocket) / KernelClient(Tauri Event) / SaaSRelay(SSE) + 5min超时守护。详见 [ARCHITECTURE_BRIEF.md](docs/ARCHITECTURE_BRIEF.md)
|
||||
- **客户端路由**: `getClient()` 4分支决策树 → Admin路由 / SaaS Relay(可降级到本地) / Local Kernel / External Gateway
|
||||
- **SaaS 认证**: JWT→OS keyring 存储 + HttpOnly cookie + Token池 RPM/TPM 限流轮换 + SaaS unreachable 自动降级
|
||||
- **记忆闭环**: 对话→extraction_adapter→FTS5全文+TF-IDF权重→检索→注入系统提示
|
||||
- **LLM 驱动**: 4 Rust Driver (Anthropic/OpenAI/Gemini/Local) + 国内兼容 (DeepSeek/Qwen/Moonshot 通过 base_url)
|
||||
|
||||
### 最近变更
|
||||
|
||||
1. [04-09] Hermes Intelligence Pipeline 4 Chunk: ExperienceStore+Extractor, UserProfileStore+Profiler, NlScheduleParser, TrajectoryRecorder+Compressor (684 tests, 0 failed)
|
||||
2. [04-09] 管家模式6交付物完成: ButlerRouter + 冷启动 + 简洁模式UI + 桥测试 + 发布文档
|
||||
3. [04-08] 侧边栏 AnimatePresence bug + TopBar 重复 Z 修复 + 发布评估报告
|
||||
3. [04-07] @reserved 标注 5 个 butler Tauri 命令 + 痛点持久化 SQLite
|
||||
4. [04-06] 4 个发布前 bug 修复 (身份覆盖/模型配置/agent同步/自动身份)
|
||||
|
||||
<!-- ARCH-SNAPSHOT-END -->
|
||||
|
||||
<!-- ANTI-PATTERN-START -->
|
||||
<!-- 此区域由 auto-sync 自动更新,请勿手动编辑。更新时间: 2026-04-09 -->
|
||||
|
||||
## 14. AI 协作注意事项
|
||||
|
||||
### 反模式警告
|
||||
|
||||
- ❌ **不要**建议新增 SaaS API 端点 — 已有 140 个,稳定化约束禁止新增
|
||||
- ❌ **不要**忽略管家模式 — 已上线且为默认模式,所有聊天经过 ButlerRouter
|
||||
- ❌ **不要**假设 Tauri 直连 LLM — 实际通过 SaaS Token 池中转,SaaS unreachable 时降级到本地 Kernel
|
||||
- ❌ **不要**建议从零实现已有能力 — 先查 Hand(9个)/Skill(75个)/Pipeline(17模板) 现有库
|
||||
- ❌ **不要**在 CLAUDE.md 以外创建项目级配置或规则文件 — 单一入口原则
|
||||
|
||||
### 场景化指令
|
||||
|
||||
- 当遇到**聊天相关** → 记住有 3 种 ChatStream 实现,先用 `getClient()` 判断当前路由模式
|
||||
- 当遇到**认证相关** → 记住 Tauri 模式用 OS keyring 存 JWT,SaaS 模式用 HttpOnly cookie
|
||||
- 当遇到**新功能建议** → 先查 [TRUTH.md](docs/TRUTH.md) 确认可用能力清单,避免重复建设
|
||||
- 当遇到**记忆/上下文相关** → 记住闭环已接通: FTS5+TF-IDF+embedding,不是空壳
|
||||
- 当遇到**管家/Butler** → 管家模式是默认模式,ButlerRouter 在中间件链中做关键词分类+system prompt 增强
|
||||
|
||||
<!-- ANTI-PATTERN-END -->
|
||||
35
CLAUDE.md
35
CLAUDE.md
@@ -116,15 +116,29 @@
|
||||
- `cargo test --workspace` — 所有测试通过(有相关测试时)
|
||||
- 功能验证 — 启动后端 + 前端服务,在浏览器中实际操作验证改动生效(涉及 API 或 UI 时)
|
||||
- `pnpm build` — 前端生产构建通过(涉及前端时)
|
||||
5. **提交** — 验证通过后按 §5 规范提交
|
||||
6. **文档同步** — 更新相关文档(如果涉及架构、接口、模块变化)
|
||||
7. **推送到仓库** — 提交后立即 `git push`,确保远程仓库同步
|
||||
5. **提交 + 文档 + 推送(三合一,强制)** — 验证通过后按顺序执行:
|
||||
- a. 按 §5 规范提交代码
|
||||
- b. 检查本次变更是否触发 wiki 更新(见下方 wiki 更新触发条件),触发则更新后单独 `docs(wiki)` 提交
|
||||
- c. `git push` 立即推送,不允许"等一下再推"
|
||||
- **禁止连续 5 个非 docs 提交而不更新 wiki 关键数字**
|
||||
|
||||
#### wiki 更新触发条件(步骤 5b 的判定标准)
|
||||
|
||||
以下任一条件满足时,**必须**更新 wiki 后才能继续下一任务:
|
||||
|
||||
- **fix 提交** → `wiki/index.md` 症状导航新增条目或标记"已修复"
|
||||
- **feat 提交(新功能)** → `wiki/index.md` 关键数字更新 + 对应模块 wiki 页更新(实体数/路由数/端点数等)
|
||||
- **数据库迁移变化** → 关键数字中的迁移数/表数更新
|
||||
- **API 路由变化** → 路由数更新
|
||||
- **测试数量变化** → 测试数/断言数更新
|
||||
- **连续 5 个代码提交** → 强制做一次 wiki/index.md 关键数字全文校正(对比代码实际数量)
|
||||
|
||||
**铁律:**
|
||||
- **步骤 0 阅读 Wiki 是绝对起点** — 不读 wiki 就开干 = 连环境配置都不知道,所有验证步骤都是空谈。
|
||||
- **步骤 1 现状确认是强制起点** — 不检查就开干 = 脱离实际,所有产出不可信。
|
||||
- **步骤 4 功能验证必须实际操作** — 只看编译通过不算验证,必须启动服务、在浏览器中确认功能正常。
|
||||
- **步骤 7 推送是强制环节**,不推送就等于没完成。不允许"等一下再推"。
|
||||
- **步骤 5 三合一是强制流程** — 提交后必须检查 wiki、必须推送,缺一不可。
|
||||
- **每次新会话开始时,先检查是否有未推送的提交并立即推送**。
|
||||
|
||||
### 2.6 Feature DoD — 功能完成定义(强制)
|
||||
|
||||
@@ -166,6 +180,13 @@
|
||||
- [ ] 用户输入已验证和消毒(防 SQL 注入、XSS)
|
||||
- [ ] 无 CORS 通配符、无硬编码密钥
|
||||
|
||||
#### 文档一致性
|
||||
|
||||
- [ ] `wiki/index.md` 关键数字与代码实际状态一致(迁移数、路由数、实体数、测试数等)
|
||||
- [ ] 新增/修复的 bug 已记录在症状导航中(含根因+解决方案)
|
||||
- [ ] 新增功能已记录在对应模块 wiki 页面中(实体、端点、事件等)
|
||||
- [ ] wiki 页面的"最后更新"日期已刷新为当天
|
||||
|
||||
#### 端到端验证
|
||||
|
||||
- [ ] `cargo check` 全 workspace 通过
|
||||
@@ -173,6 +194,7 @@
|
||||
- [ ] 浏览器中手动验证功能正常(列表/创建/编辑/删除/权限拦截)
|
||||
- [ ] 小程序中验证(涉及小程序页面时)
|
||||
- [ ] 相关路由权限按角色测试通过(至少 admin + 只读角色)
|
||||
- [ ] 本地提交已推送到远程仓库
|
||||
|
||||
---
|
||||
|
||||
@@ -370,8 +392,11 @@ chore(docker): 添加 PostgreSQL 健康检查
|
||||
- ❌ **不要**在 plugin.toml 中使用与实体名不一致的权限码 — `permissions[].code` 前缀必须与 `schema.entities[].name` 完全一致(如实体 `customer_tag` → 权限码 `customer_tag.list`/`customer_tag.manage`,不能写成 `tag.manage`),否则页面 403
|
||||
- ❌ **不要**漏掉实体的 `.list` 权限 — 每个实体必须同时声明 `.list` 和 `.manage`,缺少 `.list` 导致列表页 403
|
||||
- ❌ **不要**跳过验证直接提交 — 编译/测试/功能验证必须全部通过
|
||||
- ❌ **不要**提交后忘记推送 — 不推送等于没完成,远程仓库必须同步
|
||||
- ❌ **不要**提交后忘记推送 — 不推送等于没完成,远程仓库必须同步。每次新会话开始先检查未推送提交
|
||||
- ❌ **不要**忘记更新文档 — 涉及架构、接口、模块变化时必须同步更新相关文档
|
||||
- ❌ **不要**连续 5 个代码提交不更新 wiki — wiki 是团队唯一真相源,过期数据比没有数据更有害(5 月实测:89 fix 仅 11 有 wiki 更新,关键数字迁移数差 8 个)
|
||||
- ❌ **不要**修复 bug 后跳过症状导航更新 — 每个修复都应该帮助未来遇到同类问题的人快速定位根因
|
||||
- ❌ **不要**新增功能后不更新 wiki 关键数字 — 迁移数/路由数/实体数/测试数必须与代码同步,否则 wiki 指标表就是废数据
|
||||
- ❌ **不要**一次性输出长文档 — 超过 200 行的文档必须分步编写(先大纲 → 逐章 → 整合),否则会因上下文过长卡死
|
||||
- ❌ **不要**忽略讨论记录 — 每次发散式讨论结束后必须建立文档到 `docs/discussions/`,不要口头确认后就忘
|
||||
- ❌ **不要**跳过 Feature DoD — 每个功能标记"完成"前必须通过 §2.6 检查清单,不全面验证就提交 = 后续 5 次 fix(媒体库教训)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 884 KiB |
@@ -1,134 +0,0 @@
|
||||
# R01 — Admin 测试结果
|
||||
|
||||
> 测试日期: 2026-05-06 | 测试人: Claude | 环境: 本地 dev
|
||||
|
||||
## 1. 登录 & 仪表盘
|
||||
|
||||
| # | 测试项 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|--------|------|----------|----------|------|
|
||||
| 1.1 | 登录 | 输入 admin / Admin@2026 | 成功登录,左侧菜单 45 项 | 成功登录,菜单完整显示 | PASS |
|
||||
| 1.2 | 工作台仪表盘 | 查看首页 | 显示注册用户数、业务模块数、今日操作、本周活跃 | 注册用户17、业务模块8/8、今日操作5、本周活跃7;8模块均"运行中" | PASS |
|
||||
| 1.3 | 最近操作记录 | 查看操作日志 | 按时间倒序显示登录/操作记录 | 6条登录记录,按时间倒序 | PASS |
|
||||
|
||||
## 2. 场景 A — 患者建档全链路
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| A.1 | 创建患者 | 新增 → 填写姓名/身份证/手机/出生日期 → 保存 | 患者出现在列表,状态 active | 创建"测试患者R01"成功,列表首位显示 | PASS |
|
||||
| A.2 | 患者详情 | 点击新患者卡片 | 显示基本信息、体征数据 Tab、操作记录 | 详情页显示基本信息+体征Tab+操作记录 | PASS |
|
||||
| A.3 | 打标签 | 标签管理 → 新增"高血压高危"→ 回患者详情分配 | 标签显示在患者卡片和详情页 | 标签 CRUD 正常,患者卡片显示标签 | PASS |
|
||||
| A.4 | 绑定设备 | 查看设备列表 → 记录绑定状态 | 设备列表显示绑定关系 | 设备列表正常显示 | PASS |
|
||||
| A.5 | 知情同意 | 查看知情同意记录 | 知情同意书列表可查看 | 列表正常 | PASS |
|
||||
| A.6 | 验证完整性 | 搜索新患者 | 患者信息完整 | 搜索结果正确 | PASS |
|
||||
|
||||
## 3. 场景 B — 随访闭环(管理视角)
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| B.1 | 创建随访 | 新增 → 选患者+随访类型+计划日期 → 保存 | 随访任务创建成功,状态 pending | 创建电话随访(2026-05-15)成功 | PASS |
|
||||
| B.2 | 随访列表 | 按状态筛选:待办/进行中/已完成 | 筛选正确,数据一致 | **筛选不生效**:选"待处理"后列表仍显示全部22条混合状态 | FAIL |
|
||||
| B.3 | 查看模板 | 查看随访模板 | 模板列表显示结构和字段 | 模板列表正常 | PASS |
|
||||
| B.4 | 行动收件箱 | 筛选类型 | 显示行动项 | 新建随访出现在行动收件箱 | PASS |
|
||||
|
||||
## 4. 场景 C — 咨询流转(管理视角)
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| C.1 | 咨询列表 | 按状态筛选 | 显示 waiting/active/closed 状态 | 列表显示多条咨询,按状态分组 | PASS |
|
||||
| C.2 | 对话详情 | 点击某条咨询 → 查看对话 | 显示完整消息历史 | 对话详情正常,发送"测试回复消息"成功;患者名显示"未知"(minor) | PASS |
|
||||
|
||||
## 5. 场景 D — 告警处理链
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| D.1 | 危急值阈值 | 查看配置 | 显示各体征指标的阈值范围 | 显示收缩压/舒张压/心率/血氧/体温阈值 | PASS |
|
||||
| D.2 | 告警仪表盘 | 查看统计 | 按严重程度分类显示告警 | 显示 pending 告警统计 | PASS |
|
||||
| D.3 | 告警处理 | 点击告警 → 标记已确认/已处理 | 告警状态变更 | **无操作按钮**:详情面板只有 ID/score/severity 信息,无确认/处理按钮 | ISSUE |
|
||||
| D.4 | 实时监控 | 查看面板 | 显示实时体征数据流 | 实时监控面板正常显示 | PASS |
|
||||
| D.5 | BLE 网关 | 查看网关列表 | 显示连接状态 | 网关列表正常 | PASS |
|
||||
|
||||
## 6. 场景 E — AI 分析链
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| E.1 | Prompt 管理 | 查看 Prompt 模板列表 | 显示 Prompt 模板,可编辑 | 显示 4 个 Prompt 模板(趋势分析/化验报告/健康报告/通用) | PASS |
|
||||
| E.2 | 触发分析 | 查看 AI 分析历史 | 显示分析记录和结果 | 历史记录正常显示 | PASS |
|
||||
| E.3 | AI 用量 | 查看统计 | 显示调用次数、token 消耗 | 显示总量/成功/失败统计 | PASS |
|
||||
|
||||
## 7. 场景 F — 内容发布链
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| F.1 | 创建文章 | 新增 → 填写标题/内容 → 保存草稿 | 文章状态为 draft | 创建"R01测试文章-健康饮食"成功,状态 draft | PASS |
|
||||
| F.2 | 编辑文章 | 点击草稿 → 修改内容 → 保存 | 内容更新成功 | 编辑保存成功 | PASS |
|
||||
| F.3 | 发布文章 | 点击发布 | 状态 draft → published | 发布成功,状态变为 published | PASS |
|
||||
| F.4 | 下架文章 | 点击已发布文章 → 下架 | 状态变回 draft | 撤回按钮可见,操作正常 | PASS |
|
||||
|
||||
## 8. 场景 G — 积分商城链
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| G.1 | 积分规则 | 查看规则列表 | 显示积分获取/消费规则 | 9 条规则,有编辑/删除/启用禁用控制 | PASS |
|
||||
| G.2 | 商品管理 | 新增商品 → 保存 | 商品出现在列表 | 创建"R01测试商品-健康礼包"(实物/200积分)成功,列表 12→13 | PASS |
|
||||
| G.3 | 订单管理 | 查看订单 | 显示兑换订单列表 | 2 条订单(TestPatient/5积分/待核销),有核销按钮 | PASS |
|
||||
|
||||
## 9. 场景 H — 线下活动链
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| H.1 | 创建活动 | 新增 → 填写信息 → 保存 | 活动创建成功 | 创建"R01测试-血压管理讲座"(2026-05-20/15积分/30人)成功,列表 8→9 | PASS |
|
||||
| H.2 | 查看活动 | 列表中查看活动详情 | 显示报名人数、活动状态 | 列表显示名称/日期/地点/积分/人数/状态,编辑/签到/删除按钮齐全 | PASS |
|
||||
|
||||
## 10. 场景 I — 系统管理全链路
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| I.1 | 用户管理 | 搜索用户 → 查看详情 | 用户列表可搜索/分页/查看角色分配 | 17 条用户记录,角色列显示正确(管理员/医生/护士/运营人员/健康管理师) | PASS |
|
||||
| I.2 | 角色管理 | 查看角色详情 | 显示角色及权限码 | 9 个角色(admin/doctor/nurse/health_manager/operator/viewer+3测试角色) | PASS |
|
||||
| I.3 | 组织架构 | 展开树形结构 | 显示组织/部门/岗位层级 | 三优总公司含5个分公司,部门/岗位联动正常 | PASS |
|
||||
| I.4 | 统计报表 | 查看 | 显示患者数/随访数等图表 | 患者38/预约6/随访31%/体征21%/医护10,透析/化验/预约/体征4个Tab | PASS |
|
||||
| I.5 | 工作流 | 查看流程定义 | 显示已定义流程 | 3个流程定义,4个Tab(定义/待办/已办/监控) | PASS |
|
||||
| I.6 | 消息中心 | 查看 | 消息列表,支持已读/未读标记 | 41 条消息,全部/未读/模板/设置 4 Tab,标记已读/查看/删除操作正常 | PASS |
|
||||
| I.7 | 系统设置 | 编辑 → 保存 | 配置项可编辑保存 | 8 个 Tab(字典/语言/菜单/编号/参数/主题/审计/密码),字典 7 项可编辑 | PASS |
|
||||
| I.8 | 插件管理 | 查看插件列表 | 显示已安装插件 | 4 个插件(自由职业者/CRM/进销存/IT运维),上传/启用/卸载/详情按钮正常 | PASS |
|
||||
| I.9 | OAuth | 查看 | 显示 OAuth 客户端列表 | FHIR API 合作方管理页面正常,有创建按钮 | PASS |
|
||||
|
||||
## 11. 跨角色协作验证
|
||||
|
||||
| # | 协作场景 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|----------|------|----------|----------|------|
|
||||
| X.1 | 医护管理 | 查看医护列表 | 显示科室、职称信息 | 10 条医护记录,姓名/科室/职称/专长/执业编号/在线状态完整 | PASS |
|
||||
| X.2 | 角色分配 | 编辑某用户 → 分配角色 | 角色变更后菜单立即更新 | 用户列表角色列正确显示,编辑对话框字段可编辑 | PASS |
|
||||
| X.3 | 标签管理 | 新增/编辑/删除 | 标签变更同步到患者筛选器 | 标签 CRUD 正常,已在前序测试验证 | PASS |
|
||||
|
||||
## 12. 权限验证
|
||||
|
||||
| # | 测试项 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|--------|------|----------|----------|------|
|
||||
| 12.1 | 全页面可访问 | 逐一点击左侧菜单 | 每个路径正常打开,无 403 | 所有 ~45 个页面正常打开,无 403 错误 | PASS |
|
||||
| 12.2 | 全按钮可见 | 进入各页面 | 新增/编辑/删除按钮均可见 | 各页面 CRUD 按钮完整可见 | PASS |
|
||||
|
||||
## 测试摘要
|
||||
|
||||
- **通过数: 47 / 总数: 48**
|
||||
- **通过率: 97.9%**
|
||||
- **FAIL: 1** — B.2 随访状态筛选不生效
|
||||
- **ISSUE: 1** — D.3 告警详情无操作按钮(无确认/处理按钮)
|
||||
- **MINOR: 1** — C.2 咨询详情患者名显示"未知"而非实际姓名
|
||||
|
||||
### 问题清单
|
||||
|
||||
| # | 严重度 | 测试项 | 问题描述 | 复现步骤 |
|
||||
|---|--------|--------|----------|----------|
|
||||
| 1 | MEDIUM | B.2 随访筛选 | 按状态筛选"待处理"后列表仍显示全部22条混合状态记录 | 随访管理页 → 状态筛选选"待处理" → 列表未过滤 |
|
||||
| 2 | MEDIUM | D.3 告警处理 | 告警详情面板无确认/处理按钮,admin 应有完整操作权限 | 告警仪表盘 → 点击 pending 告警 → 详情无操作按钮 |
|
||||
| 3 | LOW | C.2 咨询详情 | WangWei 咨询详情中患者名显示"未知" | 咨询管理 → 点击 WangWei 咨询 → 详情患者名"未知" |
|
||||
|
||||
### 测试创建的数据
|
||||
|
||||
- 患者: 测试患者R01 (019dfdc6-2d4c-7db0-ae8f-ea0b244bb8bd)
|
||||
- 文章: R01测试文章-健康饮食 (已发布)
|
||||
- 随访: 电话随访 2026-05-15
|
||||
- 商品: R01测试商品-健康礼包 (实物/200积分)
|
||||
- 活动: R01测试-血压管理讲座 (2026-05-20)
|
||||
- 咨询回复: 测试回复消息
|
||||
@@ -1,90 +0,0 @@
|
||||
# R02 — Doctor 测试结果
|
||||
|
||||
> 测试日期: 2026-05-06 | 测试人: Claude | 环境: 本地 dev
|
||||
|
||||
## 1. 登录 & 仪表盘
|
||||
|
||||
| # | 测试项 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|--------|------|----------|----------|------|
|
||||
| 1.1 | 登录 | 输入 doctor_test / Admin@2026 | 成功登录,左侧菜单 24 项 | 成功登录,显示 doctor_test 用户 | PASS |
|
||||
| 1.2 | 医生仪表盘 | 查看首页 | 显示问候语、AI建议待审、重点关注、今日日程、未回复咨询 | "晚上好,d医生";2项AI建议待审、2条告警、3本月咨询、0今日预约 | PASS |
|
||||
| 1.3 | AI 建议卡片 | 查看建议列表 | 按风险排序,可"采纳"或"拒绝" | 2条AI建议(高风险BP trending/中风险HRV),有采纳/拒绝按钮;**但采纳按钮跳转AI分析页而非行内操作** | PASS(ISSUE) |
|
||||
| 1.4 | 快捷操作 | 查看底部 | 显示操作入口 | AI分析中心/告警中心/患者查询 | PASS |
|
||||
|
||||
## 2. 场景 A — 患者建档与诊疗
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| A.1 | 患者列表 | 搜索/标签筛选 | 显示患者列表 | 38条记录,搜索"测试患者R01"正确返回1条 | PASS |
|
||||
| A.2 | 患者详情 | 点击患者卡片 | 显示基本信息、标签、体征、操作记录 | 详情页显示完整信息+6个Tab+快捷跳转 | PASS |
|
||||
| A.3 | 新增患者 | 新建患者 | 患者创建成功 | 有"新建患者"按钮(未重复创建) | PASS |
|
||||
| A.4 | 医护管理 | 查看医护列表 | 显示科室、职称 | 11条医护记录,科室/职称显示正确 | PASS |
|
||||
| A.5 | 诊断记录 | 查看列表 | 显示诊断记录 | 诊断记录页面正常,需输入患者ID查询 | PASS |
|
||||
| A.6 | 知情同意 | 查看列表 | 显示知情同意书 | 知情同意管理页面正常 | PASS |
|
||||
|
||||
## 3. 场景 B — 随访闭环(医生端)
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| B.1 | 随访列表 | 查看随访任务 | 显示待办/进行中/已完成随访 | 22条记录,有新建/填写记录/分配/删除按钮 | PASS |
|
||||
| B.2 | 状态筛选 | 切换状态筛选 | 正确显示各状态 | 同R01 B.2筛选不生效问题 | FAIL |
|
||||
| B.3 | 随访详情 | 点击随访 → 查看录入内容 | 显示随访记录详情 | 有"填写记录"按钮可查看 | PASS |
|
||||
| B.4 | 随访模板 | 查看模板 | 显示模板列表 | 菜单中有随访模板管理入口 | PASS |
|
||||
| B.5 | 行动收件箱 | 筛选类型 | 显示AI建议/告警/随访 | 32项待办,含告警/AI建议/随访类型 | PASS |
|
||||
|
||||
## 4. 场景 C — 咨询接诊闭环
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| C.1 | 咨询列表 | 按状态筛选 | 显示 waiting/active/closed 咨询 | 10条记录,含进行中/已关闭状态,有新建/导出/关闭按钮 | PASS |
|
||||
|
||||
## 5. 场景 D — 告警处理
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| D.1 | 告警仪表盘 | 查看统计 | 按严重程度分类显示告警 | 5条告警,紧急(BP Critical/HR Abnormal)+严重(Blood Sugar),显示患者关联 | PASS |
|
||||
|
||||
## 6. 场景 E — AI 分析链
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| E.1 | AI 分析历史 | 查看列表 | 显示分析记录 | 10条记录,含 report_summary/checkup_plan/trend/lab_report 类型 | PASS |
|
||||
| E.2 | 查看分析详情 | 点击某条分析 | 显示分析结果 | 记录可展开查看详情 | PASS |
|
||||
| E.3 | 处理建议 | 采纳/拒绝AI建议 | 建议状态变更 | 行动收件箱显示AI建议;仪表盘采纳按钮**跳转页面而非行内操作** | PASS(ISSUE) |
|
||||
| E.4 | AI 用量 | 查看 | 显示AI调用量 | 总分析8次/4类型/本月8,类型分布清晰 | PASS |
|
||||
|
||||
## 7. 消息
|
||||
|
||||
| # | 测试项 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|--------|------|----------|----------|------|
|
||||
| 7.1 | 消息列表 | 查看 | 只读消息列表 | 菜单有消息中心入口 | PASS |
|
||||
|
||||
## 8. 权限边界验证
|
||||
|
||||
> doctor 不应访问的模块
|
||||
|
||||
| # | 测试项 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|--------|------|----------|----------|------|
|
||||
| 8.1 | 无用户管理 | 地址栏输入 /users | 403 | 显示空数据页面(无403) | FAIL |
|
||||
| 8.2 | 无权限管理 | 地址栏输入 /roles | 403 | 显示空数据页面(无403) | FAIL |
|
||||
| 8.3 | 无积分管理 | 地址栏输入 /health/points-rules | 403 | **可完整访问**,显示积分规则列表 | FAIL |
|
||||
| 8.4 | 无内容管理 | 地址栏输入 /health/articles | 403 | **可完整访问**,显示文章列表 | FAIL |
|
||||
| 8.5 | 无系统设置 | 地址栏输入 /settings | 403 | **可完整访问**,显示8个设置Tab | FAIL |
|
||||
| 8.6 | 无 BLE 网关 | 地址栏输入 /health/ble-gateways | 403 | 显示"权限不足" | PASS |
|
||||
| 8.7 | 无标签管理 | 地址栏输入 /health/tags | 403 | **可完整访问**,显示标签管理页面 | FAIL |
|
||||
|
||||
## 测试摘要
|
||||
|
||||
- **通过数: 28 / 总数: 35**
|
||||
- **通过率: 80.0%**
|
||||
- **FAIL: 7** — B.2 随访筛选 + 8.1-8.7 权限边界(6/7个受限页面可访问)
|
||||
- **ISSUE: 1** — 1.3 AI建议采纳按钮跳转而非行内操作
|
||||
|
||||
### 问题清单
|
||||
|
||||
| # | 严重度 | 测试项 | 问题描述 | 复现步骤 |
|
||||
|---|--------|--------|----------|----------|
|
||||
| 1 | **HIGH** | 8.3-8.7 权限边界 | doctor 可访问积分管理/内容管理/系统设置/标签管理页面(预期403) | 以 doctor_test 登录 → 地址栏输入对应路径 → 页面正常加载 |
|
||||
| 2 | **HIGH** | 8.1-8.2 权限边界 | doctor 可访问用户管理/角色管理页面(返回空数据而非403) | 同上,显示空表格 |
|
||||
| 3 | MEDIUM | B.2 随访筛选 | 状态筛选不生效(同R01问题) | 随访管理 → 选"待处理" → 列表未过滤 |
|
||||
| 4 | LOW | 1.3 AI建议采纳 | 仪表盘"采纳"按钮跳转到AI分析历史页而非行内操作 | 工作台 → 点击"采纳" → 页面跳转 |
|
||||
@@ -1,107 +0,0 @@
|
||||
# R04 — Health Manager 测试结果
|
||||
|
||||
> 测试日期: 2026-05-06 | 测试人: Claude | 环境: 本地 dev
|
||||
|
||||
## 1. 登录 & 仪表盘
|
||||
|
||||
| # | 测试项 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|--------|------|----------|----------|------|
|
||||
| 1.1 | 登录 | 使用 health_manager_test / Admin@2026 | 成功登录,左侧菜单 29 项 | 成功登录,显示 "Health Manager Test" 用户 | PASS |
|
||||
| 1.2 | 仪表盘 | 查看首页 | 显示综合仪表盘 | "今日任务流":待处理 9 / 已完成 0,含告警/AI建议/随访任务列表 | PASS |
|
||||
|
||||
## 2. 场景 A — 患者建档与标签管理
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| A.1 | 标签管理 | /health/tags → CRUD | 标签 CRUD 正常 | 标签管理页面正常,显示39条患者记录,有"管理标签"操作 | PASS |
|
||||
| A.2 | 患者管理 | /health/patients → 查看 | 患者列表正常 | 39条记录,有"新建患者"按钮,搜索/筛选完整 | PASS |
|
||||
| A.3 | 患者搜索 | 搜索框输入 → 标签筛选 | 搜索和筛选正常 | 页面搜索/筛选控件完整 | PASS |
|
||||
| A.4 | 医护列表 | /health/doctors → 查看(只读) | 无编辑/新增按钮 | 医护管理列表正常显示(11条),需确认按钮权限 | PASS |
|
||||
|
||||
## 3. 场景 B — 随访管理闭环
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| B.1 | 随访列表 | /health/follow-up-tasks | 显示随访任务 | 23条记录,有新建/填写记录/分配/删除按钮 | PASS |
|
||||
| B.2 | 状态筛选 | 切换状态 | 正确显示各状态 | **筛选不生效**(同 R01-R03) | FAIL |
|
||||
| B.3 | 随访录入 | 填写记录 | 可录入随访记录 | "填写记录"按钮可见 | PASS |
|
||||
| B.4 | 随访模板 | /health/follow-up-templates | 可管理随访模板 | 模板列表显示(含S5-BP-Followup-Template),有新建模板按钮 | PASS |
|
||||
| B.5 | 团队视图 | 行动收件箱 → 切换团队视图 | 支持团队视图 | 行动收件箱33项待办,筛选功能正常 | PASS |
|
||||
|
||||
## 4. 场景 C — 咨询管理
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| C.1 | 咨询列表 | 查看咨询 | 显示咨询列表 | 10条记录,含进行中/已关闭状态 | PASS |
|
||||
| C.2 | 回复咨询 | 进入对话 → 发送 | 可回复(有 consultation.manage) | 有"新建会话"和"导出"按钮 | PASS |
|
||||
| C.3 | 关闭咨询 | 点击结束 | 咨询状态变为 closed | 未实际操作(同 R01 C.2 已验证) | SKIP |
|
||||
|
||||
## 5. 场景 D — 告警与监测
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| D.1 | 告警仪表盘 | /health/alert-dashboard | 显示告警统计 | 5条告警,紧急(BP Critical/HR Abnormal)+严重(Blood Sugar),显示患者关联 | PASS |
|
||||
| D.2 | 告警规则 | 告警仪表盘 → 规则 | 可管理告警规则 | 危急值阈值页面有编辑/删除按钮(有 alert-rules.manage) | PASS |
|
||||
| D.3 | 处理告警 | 点击告警 → 确认/处理 | 告警状态变更 | **同 R01 D.3**:告警无确认/处理操作按钮 | ISSUE |
|
||||
| D.4 | 危急值阈值 | /health/critical-value-thresholds | 可查看阈值配置 | 阈值列表完整(血糖/收缩压/舒张压/心率/血氧/体温),有添加/编辑/删除按钮 | PASS |
|
||||
| D.5 | 设备管理 | /health/devices → 查看 | 设备列表可查看 | 设备管理页面正常 | PASS |
|
||||
| D.6 | 日常监测 | /health/daily-monitoring | 可查看日常监测数据 | 页面加载但内容为空(可能需关联数据) | ISSUE |
|
||||
| D.7 | 实时监控 | /health/realtime-monitor | 显示实时监控面板 | 实时体征监控面板正常,显示危急/高危/中等/低危分类 | PASS |
|
||||
|
||||
## 6. 场景 E — AI 分析与建议
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| E.1 | AI 分析 | /health/ai-analysis → 查看结果 | 可管理 AI 分析 | AI 分析历史显示记录(含 report_summary/checkup_plan/trend/lab_report),有筛选 | PASS |
|
||||
| E.2 | AI Prompt | /health/ai-prompts → 查看模板 | 只读查看 Prompt | Prompt 管理页面显示4个模板(health_trend_analysis 等),有编辑按钮 | PASS |
|
||||
| E.3 | AI 建议 | 行动收件箱 → 采纳/拒绝 | 可管理 AI 建议 | 行动收件箱含 AI 建议项(BP trending/HRV decreasing) | PASS |
|
||||
| E.4 | AI 用量 | /health/ai-usage → 查看 | 显示 AI 调用量 | 总分析 8次/4类型/本月8,类型分布(checkup_plan/lab_report/report_summary/trend) | PASS |
|
||||
|
||||
## 7. 场景 F — 诊断与知情同意
|
||||
|
||||
| # | 步骤 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|------|------|----------|----------|------|
|
||||
| F.1 | 诊断记录 | /health/diagnoses | 可查看诊断记录 | 诊断记录页面正常,需输入患者ID查询 | PASS |
|
||||
| F.2 | 知情同意 | /health/consents | 可管理知情同意 | 知情同意管理页面正常 | PASS |
|
||||
|
||||
## 8. 消息
|
||||
|
||||
| # | 测试项 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|--------|------|----------|----------|------|
|
||||
| 8.1 | 消息列表 | /messages → 查看 | 只读消息列表 | 消息中心4个Tab完整,当前0条消息 | PASS |
|
||||
|
||||
## 9. 工作流
|
||||
|
||||
| # | 测试项 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|--------|------|----------|----------|------|
|
||||
| 9.1 | 工作流 | 查看/启动流程 | 可查看和启动 | 未直接测试(侧栏有"系统管理"入口) | SKIP |
|
||||
|
||||
## 10. 权限边界验证
|
||||
|
||||
> health_manager 不应访问的模块
|
||||
|
||||
| # | 测试项 | 操作 | 预期结果 | 实际结果 | 通过 |
|
||||
|---|--------|------|----------|----------|------|
|
||||
| 10.1 | 无用户管理 | 地址栏输入 /users | 403 | **可访问**,显示用户管理页面(空数据) | FAIL |
|
||||
| 10.2 | 无积分管理 | 地址栏输入 /health/points-rules | 403 | **可完整访问**,显示积分规则页面 | FAIL |
|
||||
| 10.3 | 无内容管理 | 地址栏输入 /health/articles | 403 | **可完整访问**,显示内容管理页面 | FAIL |
|
||||
| 10.4 | 无系统设置 | 地址栏输入 /settings | 403 | **可完整访问**,显示8个设置Tab(字典/语言/菜单/编号/参数/主题/审计/密码) | FAIL |
|
||||
| 10.5 | 无插件管理 | 地址栏输入 /plugins/admin | 403 | **可访问**,显示插件上传页面 | FAIL |
|
||||
| 10.6 | 无 BLE 网关 | 地址栏输入 /health/ble-gateways | 403 | 显示"权限不足" | PASS |
|
||||
|
||||
## 测试摘要
|
||||
|
||||
- **通过数: 26 / 总数: 33**(不含 SKIP 3 项)
|
||||
- **通过率: 78.8%**
|
||||
- **FAIL: 6** — B.2 随访筛选 + 10.1-10.5 权限边界(5/6 个受限页面可访问)
|
||||
- **ISSUE: 2** — D.3 告警无操作按钮 + D.6 日常监测页面空白
|
||||
- **SKIP: 3** — C.3 关闭咨询 + 9.1 工作流 + X 跨角色验证
|
||||
|
||||
### 问题清单
|
||||
|
||||
| # | 严重度 | 测试项 | 问题描述 | 复现步骤 |
|
||||
|---|--------|--------|----------|----------|
|
||||
| 1 | **HIGH** | 10.1-10.5 权限边界 | health_manager 可访问用户管理/积分管理/内容管理/系统设置/插件管理(预期 403) | 以 health_manager_test 登录 → 地址栏输入对应路径 → 页面正常加载 |
|
||||
| 2 | MEDIUM | B.2 随访筛选 | 状态筛选不生效(同 R01-R03) | 随访管理 → 选"待处理" → 列表未过滤 |
|
||||
| 3 | MEDIUM | D.3 告警处理 | 告警详情无确认/处理按钮(同 R01-R03) | 告警仪表盘 → 查看 pending 告警 → 无操作按钮 |
|
||||
| 4 | LOW | D.6 日常监测 | 页面加载但内容为空 | /health/daily-monitoring → 空白页面 |
|
||||
@@ -1,382 +0,0 @@
|
||||
# HMS 全系统前端审计报告
|
||||
|
||||
> 日期: 2026-04-26 | 审计范围: 全系统(基础模块 + 健康模块 + 插件 + 小程序)
|
||||
> 方法: 代码扫描 + 浏览器操作测试 + 后端日志分析
|
||||
> 审计人: Claude Code 自动化审计
|
||||
|
||||
---
|
||||
|
||||
## 执行摘要
|
||||
|
||||
| 指标 | 值 |
|
||||
|------|-----|
|
||||
| 审计覆盖模块 | 12 个(auth/config/workflow/message/plugin/health/ai/settings/小程序) |
|
||||
| 浏览器验证页面 | 18 个 Web 页面 + 通知面板 |
|
||||
| 代码级审计模块 | 10 个(4 个后台 agent + 6 个手动/浏览器) |
|
||||
| **总问题数** | **72 个** |
|
||||
| P0 阻断 | **9 个** |
|
||||
| P1 严重 | **20 个** |
|
||||
| P2 中等 | **22 个** |
|
||||
| P3 轻微 | **15 个** |
|
||||
| P4 优化 | **6 个** |
|
||||
|
||||
### 风险矩阵
|
||||
|
||||
| 风险域 | 状态 | 关键发现 |
|
||||
|--------|------|---------|
|
||||
| 安全 | 🔴 高风险 | 2 个 P0(文件无认证访问 + SQL 注入)|
|
||||
| 数据完整性 | 🔴 高风险 | 级联删除缺失 × 3,并行网关死锁 |
|
||||
| 功能完整性 | 🟡 中等 | 多个模块 CRUD 不完整(模板/偏好/组织) |
|
||||
| 实时性 | 🟡 中等 | 消息 60 秒轮询,无 WebSocket |
|
||||
| 代码质量 | 🟢 良好 | 动态表 SQL 注入防护好,租户隔离一致 |
|
||||
|
||||
---
|
||||
|
||||
## P0 阻断问题(8 个)
|
||||
|
||||
### P0-1: 上传文件无认证访问
|
||||
- **模块**: erp-server 静态文件服务
|
||||
- **类型**: 安全漏洞
|
||||
- **复现步骤**: 直接访问 `http://localhost:3000/uploads/{任意文件名}`
|
||||
- **预期行为**: 需 JWT 认证才能访问医疗文档
|
||||
- **实际行为**: 所有上传文件公开可访问,包括医疗文档
|
||||
- **影响**: 医疗隐私数据泄露,违反 HIPAA 合规
|
||||
- **相关文件**: `crates/erp-server/src/main.rs:545-546`
|
||||
- **建议修复**: 在 ServeDir 前添加 JWT 认证中间件,或使用签名 URL
|
||||
|
||||
### P0-2: analytics/batch 端点公开
|
||||
- **模块**: erp-server API
|
||||
- **类型**: 安全漏洞
|
||||
- **复现步骤**: 无需认证 POST `/api/v1/analytics/batch`
|
||||
- **影响**: 任意 JSON 事件注入,可伪造分析数据
|
||||
- **相关文件**: `crates/erp-server/src/main.rs` 路由注册
|
||||
- **建议修复**: 添加 JWT 认证守卫
|
||||
|
||||
### P0-3: load_plugin_config SQL 注入
|
||||
- **模块**: erp-plugin 引擎
|
||||
- **类型**: 安全漏洞
|
||||
- **复现步骤**: 恶意 WASM 插件 manifest 中 craft `plugin_id` 包含 SQL 注入 payload
|
||||
- **影响**: 通过 `format!()` 拼接 SQL,`plugin_id` 仅用 `replace('\'', "''")` 转义,不安全
|
||||
- **相关文件**: `crates/erp-plugin/src/engine.rs:630-637`
|
||||
- **建议修复**: 改用参数化查询 `$N` 占位符
|
||||
|
||||
### P0-4: 组织删除无级联检查
|
||||
- **模块**: erp-auth 组织管理
|
||||
- **类型**: 数据完整性
|
||||
- **复现步骤**: 删除包含子部门/岗位/用户关联的组织
|
||||
- **影响**: 软删除组织后,子部门、岗位、用户关联成为孤儿数据
|
||||
- **建议修复**: 删除前检查并阻止存在子记录的删除操作
|
||||
|
||||
### P0-5: 部门删除无级联检查
|
||||
- **模块**: erp-auth 部门管理
|
||||
- **类型**: 数据完整性
|
||||
- **复现步骤**: 删除包含子部门或关联用户的部门
|
||||
- **影响**: 同 P0-4,部门树结构断裂
|
||||
- **建议修复**: 递归检查子部门和用户关联
|
||||
|
||||
### P0-6: 岗位删除无级联检查
|
||||
- **模块**: erp-auth 岗位管理
|
||||
- **类型**: 数据完整性
|
||||
- **影响**: 删除岗位后用户-岗位关联悬空
|
||||
- **建议修复**: 检查用户关联后阻止或级联更新
|
||||
|
||||
### P0-7: 并行网关 Join 逻辑可能导致死锁
|
||||
- **模块**: erp-workflow 执行引擎
|
||||
- **类型**: 功能缺陷
|
||||
- **相关文件**: `crates/erp-workflow/src/engine/executor.rs:369-425`
|
||||
- **影响**: 并行分支汇聚时,token 查询可能匹配到历史迭代的 stale token,导致提前/错误完成或永久等待
|
||||
- **建议修复**: 添加 token lineage/correlation 机制,区分不同 fork 产生的 token
|
||||
|
||||
### P0-8: workflow on_tenant_deleted 是空操作
|
||||
- **模块**: erp-workflow 租户清理
|
||||
- **类型**: 数据完整性
|
||||
- **相关文件**: `crates/erp-workflow/src/module.rs:149-154`
|
||||
- **影响**: 删除租户后,流程定义/实例/任务/Token 残留,跨租户数据泄漏风险
|
||||
- **建议修复**: 实现级联软删除所有租户相关数据
|
||||
|
||||
---
|
||||
|
||||
## P1 严重问题(18 个)
|
||||
|
||||
### P1-1: health-data 统计端点 500 错误
|
||||
- **模块**: erp-health 统计
|
||||
- **类型**: 功能缺陷
|
||||
- **根因**: PostgreSQL AVG() 返回 NUMERIC 类型,Rust 代码期望 f64(FLOAT8)
|
||||
- **相关文件**: `crates/erp-health/src/service/stats_service.rs`
|
||||
- **浏览器验证**: 统计报表页面显示"加载统计数据失败 500"
|
||||
- **建议修复**: 使用 `Decimal` 类型或 SQL 中显式 `CAST(AVG(...) AS FLOAT8)`
|
||||
|
||||
### P1-2: 行级数据权限未生效
|
||||
- **模块**: erp-auth 权限
|
||||
- **类型**: 功能缺陷
|
||||
- **影响**: `department_ids` 已填充但未用于数据过滤查询
|
||||
- **建议修复**: 在查询构建器中加入 department_ids 过滤条件
|
||||
|
||||
### P1-3: 消息无实时推送
|
||||
- **模块**: erp-message
|
||||
- **类型**: 功能缺失
|
||||
- **影响**: 医疗告警最多延迟 60 秒才能到达医护人员,临床不可接受
|
||||
- **建议修复**: 实现 WebSocket 或 SSE 推送
|
||||
|
||||
### P1-4: 消息模板 CRUD 不完整
|
||||
- **模块**: erp-message
|
||||
- **类型**: 功能缺失
|
||||
- **影响**: 模板只能创建和列表,无法编辑、删除,且 `render()` 方法未接入发送管道
|
||||
- **相关文件**: `crates/erp-message/src/template_service.rs`
|
||||
|
||||
### P1-5: 通知偏好设置无法加载已有配置
|
||||
- **模块**: erp-message + 前端
|
||||
- **类型**: 功能缺陷
|
||||
- **影响**: 后端无 GET `/message-subscriptions` 端点,前端无法加载用户已保存的偏好;第二次保存因缺少 version 字段必然失败
|
||||
- **相关文件**: `crates/erp-message/src/module.rs`, `NotificationPreferences.tsx`
|
||||
|
||||
### P1-6: 工作流 ServiceTask 是空操作
|
||||
- **模块**: erp-workflow
|
||||
- **类型**: 功能缺失
|
||||
- **相关文件**: `crates/erp-workflow/src/engine/executor.rs:277`
|
||||
- **影响**: 所有 ServiceTask 被自动跳过,`service_type` 字段无效
|
||||
|
||||
### P1-7: 工作流未注册任何事件处理器
|
||||
- **模块**: erp-workflow
|
||||
- **类型**: 功能缺失
|
||||
- **影响**: `register_event_handlers` 为空函数,工作流模块不响应任何外部事件
|
||||
- **相关文件**: `crates/erp-workflow/src/module.rs:137`
|
||||
|
||||
### P1-8: candidate_groups(角色/部门分配)存储但未使用
|
||||
- **模块**: erp-workflow
|
||||
- **类型**: 功能缺失
|
||||
- **影响**: 配置了 candidate_groups 但无 assignee 的 UserTask 对所有用户不可见,任务成为孤儿
|
||||
- **相关文件**: `crates/erp-workflow/src/service/task_service.rs:25-36`
|
||||
|
||||
### P1-9: 工作流超时检查仅记录日志
|
||||
- **模块**: erp-workflow
|
||||
- **类型**: 功能缺失
|
||||
- **影响**: 超时任务无升级/自动完成/通知,永久停留在 pending 状态
|
||||
- **相关文件**: `crates/erp-workflow/src/engine/timeout.rs`
|
||||
|
||||
### P1-10: 工作流 deprecated 状态不可达
|
||||
- **模块**: erp-workflow + 前端
|
||||
- **类型**: 功能缺陷
|
||||
- **影响**: 前端定义了 `deprecated` 状态样式,但后端无转换路径
|
||||
- **相关文件**: `ProcessDefinitions.tsx:19`
|
||||
|
||||
### P1-11: 工作流定义更新跳过校验
|
||||
- **模块**: erp-workflow
|
||||
- **类型**: 功能缺陷
|
||||
- **影响**: 只更新 nodes 而不提供 edges 时,不做图结构验证
|
||||
- **相关文件**: `crates/erp-workflow/src/service/definition_service.rs:174-181`
|
||||
|
||||
### P1-12: 编号序列表名未充分消毒
|
||||
- **模块**: erp-plugin
|
||||
- **类型**: 安全隐患
|
||||
- **影响**: `plugin_id` 格式化到 DDL/DML 语句时仅 `replace('-', "_")`,不处理引号/分号
|
||||
- **相关文件**: `crates/erp-plugin/src/host.rs:339`
|
||||
|
||||
### P1-13 ~ P1-18: 组织模块 P1 问题
|
||||
- **P1-13**: 组织/部门/岗位缺少列表 API 中的树形结构返回
|
||||
- **P1-14**: 部门树重组操作(拖拽移动父节点)未实现
|
||||
- **P1-15**: 组织/部门名称唯一性校验缺失
|
||||
- **P1-16**: 部门详情 GET 端点缺失
|
||||
- **P1-17**: 岗位分配/取消分配 API 缺失
|
||||
- **P1-18**: 消息群发(角色/部门/全员)fan-out 未实现
|
||||
|
||||
---
|
||||
|
||||
## P2 中等问题(20 个)
|
||||
|
||||
| ID | 模块 | 问题 | 文件 |
|
||||
|----|------|------|------|
|
||||
| P2-1 | health | 随访任务患者名显示为 UUID 片段 | `FollowUpTaskList.tsx` |
|
||||
| P2-2 | health | 前端测试覆盖率极低(3 个文件) | `apps/web/src/` |
|
||||
| P2-3 | health | 深色模式样式重复 ~19 页内联 isDark | 各健康页面 |
|
||||
| P2-4 | health | useDarkMode 和 useThemeMode 重叠 | hooks/ |
|
||||
| P2-5 | health | AppointmentList 冗余 404 请求 | `AppointmentList.tsx` |
|
||||
| P2-6 | message | 未读计数不即时刷新(等 60s 轮询) | `stores/message.ts` |
|
||||
| P2-7 | message | markAsRead 失败不回滚乐观更新 | `stores/message.ts:57-69` |
|
||||
| P2-8 | message | mark_all_read 不更新 version 字段 | `message_service.rs:298-326` |
|
||||
| P2-9 | message | 通知面板点击不导航到详情 | `NotificationPanel.tsx:81-85` |
|
||||
| P2-10 | message | 轮询间隔 60s 对医疗告警不可接受 | `NotificationPanel.tsx:28-31` |
|
||||
| P2-11 | workflow | N+1 查询问题(实例/任务列表) | `instance_service.rs`, `task_service.rs` |
|
||||
| P2-12 | workflow | 排他网关表达式错误被静默吞掉 | `executor.rs:176` |
|
||||
| P2-13 | workflow | ProcessDesigner 不支持边条件配置 | `ProcessDesigner.tsx` |
|
||||
| P2-14 | workflow | 委派 API 要求手动输入 UUID | `PendingTasks.tsx:207-210` |
|
||||
| P2-15 | workflow | 前端 UpdateDefinitionRequest 缺少 version | `workflowDefinitions.ts:45-51` |
|
||||
| P2-16 | plugin | reconcile_references 表名注入模式 | `data_service.rs:1204-1206` |
|
||||
| P2-17 | plugin | PluginMarket installed 判断字段不匹配 | `PluginMarket.tsx:68` |
|
||||
| P2-18 | plugin | 模板渲染未接入发送管道 | `template_service.rs:92-99` |
|
||||
| P2-19 | plugin | 偏好设置 version 字段未发送导致更新失败 | `NotificationPreferences.tsx:26-37` |
|
||||
| P2-20 | plugin | 偏好设置仅暴露 DND,channel_preferences 隐藏 | `NotificationPreferences.tsx:60` |
|
||||
|
||||
---
|
||||
|
||||
## P3 轻微问题(14 个)
|
||||
|
||||
| ID | 模块 | 问题 |
|
||||
|----|------|------|
|
||||
| P3-1 | health | 已完成任务仍显示操作按钮 |
|
||||
| P3-2 | health | ArticleEditor 图片上传未实现 (TODO) |
|
||||
| P3-3 | health | PatientDetail 头部标题显示"页面" |
|
||||
| P3-4 | health | 4 处 any 类型使用 |
|
||||
| P3-5 | health | 登录硬编码默认 tenant_id |
|
||||
| P3-6 | settings | 审计日志操作用户列显示原始 UUID |
|
||||
| P3-7 | settings | 审计日志资源类型过滤列表硬编码 |
|
||||
| P3-8 | settings | 系统参数无列表 API,需手动输入 key |
|
||||
| P3-9 | plugin | Kanban 拖拽 version 硬编码 0 导致锁冲突 |
|
||||
| P3-10 | plugin | CRUD 排序使用 JSONB 文本提取导致字典序 |
|
||||
| P3-11 | workflow | ProcessDesigner 用 destroyOnHidden 而非 destroyOnClose |
|
||||
| P3-12 | workflow | InstanceMonitor 显示 raw node_id 而非名称 |
|
||||
| P3-13 | workflow | 待办任务状态 Tag 不适配深色模式 |
|
||||
| P3-14 | message | 偏好设置 DND 启用但未填时间范围时无效 |
|
||||
|
||||
---
|
||||
|
||||
## P4 优化建议(6 个)
|
||||
|
||||
| ID | 模块 | 建议 |
|
||||
|----|------|------|
|
||||
| P4-1 | plugin | PluginAdmin purge 按钮状态与后端不一致 |
|
||||
| P4-2 | plugin | WASM init() 使用 nil UUID |
|
||||
| P4-3 | plugin | recover_plugins 不按 tenant_id 过滤 |
|
||||
| P4-4 | settings | LanguageManager 编辑弹窗无可编辑字段 |
|
||||
| P4-5 | settings | ChangePassword 最小长度仅前端校验 |
|
||||
| P4-6 | settings | Settings API delete/update URL 编码不一致 |
|
||||
|
||||
---
|
||||
|
||||
## 模块审计摘要
|
||||
|
||||
### 基础模块
|
||||
|
||||
| 模块 | 页面数 | 浏览器验证 | 代码审计 | 关键发现 |
|
||||
|------|--------|-----------|---------|---------|
|
||||
| 用户/权限 (B1) | 2 | ✅ | ✅ | email 验证宽松 |
|
||||
| 组织架构 (B2) | 1 | ❌ | ✅ | 3×P0 级联删除缺失 |
|
||||
| 工作流 (B3) | 6 | ✅ 3 页 | ✅ | 3×P0 + 6×P1 功能大量缺失 |
|
||||
| 消息 (B4) | 3+面板 | ✅ 面板 | ✅ | 5×P1 无实时推送 |
|
||||
|
||||
### 健康模块
|
||||
|
||||
| 模块 | 页面数 | 浏览器验证 | 关键发现 |
|
||||
|------|--------|-----------|---------|
|
||||
| 患者管理 (B5) | 2 | ✅ | 标题"页面"bug |
|
||||
| 医生/排班/预约 (B6) | 3 | ✅ | 冗余 404 请求 |
|
||||
| 随访/咨询 (B7) | 4 | ✅ 部分 | 患者 UUID 片段 |
|
||||
| 积分/文章/AI (B8) | 9 | ✅ 部分 | 统计报表 500 |
|
||||
|
||||
### 系统/插件
|
||||
|
||||
| 模块 | 页面数 | 浏览器验证 | 代码审计 | 关键发现 |
|
||||
|------|--------|-----------|---------|---------|
|
||||
| 插件 (B9) | 8 | ❌ | ✅ | P1 SQL 注入 |
|
||||
| 统计/仪表盘 (B9) | 2 | ✅ | ✅ | 500 错误 |
|
||||
| 系统设置 (B10) | 8 标签 | ✅ 3 标签 | ✅ | 审计日志 UUID |
|
||||
|
||||
### 小程序(B11-B14)
|
||||
|
||||
| 模块 | 审计方式 | 关键发现 |
|
||||
|------|---------|---------|
|
||||
| 登录/首页 (B11) | 代码审计 | P0 .env 泄露风险 |
|
||||
| 健康管理 (B12) | 代码审计 | P2 错误处理缺失 |
|
||||
| 医患交互 (B13) | 代码审计 | P1 咨询消息轮询无错误恢复 |
|
||||
| 内容/商城 (B14) | 代码审计 | P2 空状态处理缺失 |
|
||||
|
||||
#### 小程序专项发现
|
||||
|
||||
### P0-9: .env 未加入 .gitignore
|
||||
- **模块**: miniprogram 配置
|
||||
- **类型**: 安全漏洞
|
||||
- **相关文件**: `apps/miniprogram/.gitignore`(仅含 node_modules/ 和 dist/)
|
||||
- **影响**: `.env` 文件含 `TARO_APP_ENCRYPTION_KEY=hms_miniprogram_encryption_key_2026`,可能意外提交到 git
|
||||
- **当前状态**: 未被 git 追踪(git ls-files 为空),但风险存在
|
||||
- **建议修复**: 在 `.gitignore` 中添加 `.env` 和 `.env.*`
|
||||
|
||||
### P1-19: 小程序加密密钥为弱密钥
|
||||
- **模块**: miniprogram secure-storage
|
||||
- **类型**: 安全隐患
|
||||
- **相关文件**: `apps/miniprogram/.env:2`, `apps/miniprogram/src/utils/secure-storage.ts`
|
||||
- **影响**: `hms_miniprogram_encryption_key_2026` 为可预测字符串,AES 加密形同虚设
|
||||
- **建议修复**: 使用 `python -c "import secrets; print(secrets.token_hex(32))"` 生成强密钥
|
||||
|
||||
### P1-20: project.config.json urlCheck: false
|
||||
- **模块**: miniprogram 配置
|
||||
- **类型**: 安全隐患
|
||||
- **相关文件**: `apps/miniprogram/project.config.json:6`
|
||||
- **影响**: 生产环境允许请求任意域名,应限制为后端 API 域名
|
||||
- **建议修复**: 生产版本设为 `true` 并配置合法域名白名单
|
||||
|
||||
### P2-21: secure-storage 未设密钥时明文存储
|
||||
- **模块**: miniprogram secure-storage
|
||||
- **类型**: 安全隐患
|
||||
- **相关文件**: `apps/miniprogram/src/utils/secure-storage.ts:12`
|
||||
- **影响**: `ENCRYPTION_KEY` 为空时 `encrypt()` 直接返回明文,token 和敏感数据不加密存储
|
||||
- **建议修复**: 强制要求密钥,未设置时阻止存储敏感数据
|
||||
|
||||
### P2-22: 小程序各页面缺少统一错误边界
|
||||
- **模块**: miniprogram 全局
|
||||
- **类型**: 功能缺陷
|
||||
- **影响**: API 请求失败时页面无友好错误提示,可能白屏
|
||||
- **建议修复**: 添加全局错误边界组件和网络错误拦截器
|
||||
|
||||
### P3-15: 小程序部分页面缺少空状态处理
|
||||
- **模块**: miniprogram 健康管理
|
||||
- **类型**: UX 问题
|
||||
- **影响**: 健康数据为空时无引导提示
|
||||
|
||||
---
|
||||
|
||||
## 浏览器验证发现汇总
|
||||
|
||||
以下问题通过实际浏览器操作验证:
|
||||
|
||||
| 页面 | 操作 | 结果 | 问题 |
|
||||
|------|------|------|------|
|
||||
| Users | 创建用户(无效邮箱) | 成功 | email 验证宽松 |
|
||||
| Users | 删除用户 | 成功 | — |
|
||||
| Users | 分配角色 | 成功 | — |
|
||||
| Patient List | 搜索 | 成功 | — |
|
||||
| Patient Detail | 切换标签 | 成功 | 标题显示"页面" |
|
||||
| Statistics | 加载 | 失败 500 | SQL 类型不匹配 |
|
||||
| Settings-字典 | 新建/添加项/删除 | 全部成功 | — |
|
||||
| Settings-密码 | 错误旧密码 | 正确提示 | — |
|
||||
| Settings-审计日志 | 查看 | 成功 | UUID 而非用户名 |
|
||||
| Settings-菜单 | 查看 | 成功 | — |
|
||||
| Workflow-定义 | 列表 | 成功 | — |
|
||||
| Workflow-设计器 | 编辑草稿 | 成功 | 缺节点属性编辑器 |
|
||||
| Workflow-待办 | 查看 | 成功 | — |
|
||||
| 通知面板 | 打开 | 成功 | 60s 轮询延迟 |
|
||||
|
||||
---
|
||||
|
||||
## 优先修复建议
|
||||
|
||||
### 第一优先级(P0,必须立即修复)
|
||||
|
||||
1. **上传文件认证**(P0-1)— 医疗隐私合规要求
|
||||
2. **SQL 注入修复**(P0-3)— 安全风险,改用参数化查询
|
||||
3. **analytics/batch 认证**(P0-2)— 防止数据伪造
|
||||
4. **级联删除检查**(P0-4/5/6)— 防止数据完整性破坏
|
||||
5. **并行网关修复**(P0-7)— 核心工作流引擎可靠性
|
||||
6. **租户清理实现**(P0-8)— 多租户数据隔离
|
||||
|
||||
### 第二优先级(P1,2 周内修复)
|
||||
|
||||
1. 统计报表 SQL 类型修复(P1-1)— 面向用户的功能
|
||||
2. 行级数据权限实现(P1-2)— 安全要求
|
||||
3. 消息实时推送(P1-3)— 医疗告警时效性
|
||||
4. 工作流功能补全(P1-6~11)— 模块可用性
|
||||
5. 消息模板/偏好完善(P1-4/5)— 功能闭环
|
||||
|
||||
### 第三优先级(P2,1 月内修复)
|
||||
|
||||
1. 前端测试覆盖率提升(P2-2)
|
||||
2. 工作流 N+1 查询优化(P2-11)
|
||||
3. 各模块 UX 修复(UUID 显示、标题、按钮状态等)
|
||||
|
||||
---
|
||||
|
||||
## 附录:审计方法
|
||||
|
||||
- **代码扫描**: Explore agent 深度遍历源码,逐模块检查功能完整性
|
||||
- **浏览器测试**: Chrome DevTools MCP 实际操作 18 个页面,验证 CRUD 链路
|
||||
- **后端日志分析**: 解析 Axum tracing 日志定位 500 错误根因
|
||||
- **网络面板**: 监控 API 请求/响应,发现冗余调用和错误响应
|
||||
@@ -1,52 +0,0 @@
|
||||
# HMS 审计摘要 — 一页纸
|
||||
|
||||
> 2026-04-26 | 全系统审计(Web + 小程序)
|
||||
|
||||
## 总览
|
||||
|
||||
| | P0 | P1 | P2 | P3 | P4 | 合计 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 安全 | 4 | 2 | 2 | | | 8 |
|
||||
| 数据完整性 | 5 | 2 | 1 | | | 8 |
|
||||
| 功能缺失/缺陷 | | 12 | 8 | 5 | | 25 |
|
||||
| UX/代码质量 | | | 9 | 10 | 6 | 25 |
|
||||
| 配置/运维 | | 2 | 2 | | | 4 |
|
||||
| **合计** | **9** | **18** | **22** | **15** | **6** | **72** |
|
||||
|
||||
## 必须立即修复(Top 5)
|
||||
|
||||
1. **上传文件无认证** — 医疗文档公开可访问,合规风险极高
|
||||
2. **SQL 注入**(plugin engine `load_plugin_config`)— 改用参数化查询
|
||||
3. **小程序 .env 未加入 .gitignore** — 含弱加密密钥,可能意外泄露
|
||||
4. **组织/部门/岗位级联删除缺失** — 软删除后数据完整性破坏
|
||||
5. **统计报表 500** — PostgreSQL NUMERIC vs Rust f64 类型不匹配
|
||||
|
||||
## 模块健康度
|
||||
|
||||
| 模块 | 评级 | 核心问题 |
|
||||
|------|------|---------|
|
||||
| 用户/权限 | 🟢 | 基本可用,email 验证偏松 |
|
||||
| 组织架构 | 🔴 | 级联删除全部缺失 |
|
||||
| 工作流 | 🔴 | 大量功能未实现(ServiceTask/claim/deprecate/timeout) |
|
||||
| 消息 | 🟡 | 无实时推送,模板/偏好功能半成品 |
|
||||
| 患者管理 | 🟢 | 基本可用,细节问题 |
|
||||
| 预约排班 | 🟢 | 正常 |
|
||||
| 随访咨询 | 🟡 | 患者 UUID 显示 |
|
||||
| 积分文章 | 🟢 | 正常 |
|
||||
| 统计报表 | 🔴 | 500 错误 |
|
||||
| 插件系统 | 🟡 | SQL 注入需修,其他防护好 |
|
||||
| 系统设置 | 🟢 | 基本可用 |
|
||||
| **小程序** | 🟡 | .env 安全风险,加密密钥弱 |
|
||||
|
||||
## 下一步
|
||||
|
||||
1. **修复 9 个 P0**(预计 3-5 天)— 安全和数据完整性
|
||||
2. **修复高优 P1**(预计 1 周)— 统计报表/实时推送/加密密钥
|
||||
3. **补全工作流和消息模块**(预计 2-3 周)— 功能闭环
|
||||
4. **小程序安全加固**(预计 3 天)— .gitignore/密钥/urlCheck
|
||||
5. **提升前端测试覆盖率**(持续)
|
||||
|
||||
## 报告文件
|
||||
|
||||
- 详细报告: `plans/audit-report-2026-04-26.md`
|
||||
- 本摘要: `plans/audit-summary-2026-04-26.md`
|
||||
@@ -1,261 +0,0 @@
|
||||
# HMS 审计问题全量修复计划
|
||||
|
||||
> 日期: 2026-04-26 | 基于 audit-report-2026-04-26.md 的 72 个问题
|
||||
> 分 7 个 Phase 按优先级执行,每个 Phase 完成后提交验证
|
||||
|
||||
## Context
|
||||
|
||||
全系统审计发现 72 个问题(9 P0 / 18 P1 / 22 P2 / 15 P3 / 6 P4 + 2 额外发现)。本计划将所有问题按修复复杂度和依赖关系分 7 个阶段执行。
|
||||
|
||||
## 修正后的审计计数
|
||||
|
||||
经代码验证,部分 P0 发现需要修正:
|
||||
- **P0-4/5 部分存在**:组织删除已检查子组织(`org_service.rs:241-252`),部门删除已检查子部门(`dept_service.rs:264-275`),但均未检查关联的岗位/用户
|
||||
- **新增 P0**:`stats_service.rs:423-444` 的 `compute_avg_field` 函数也有 SQL 注入(`format!` 拼接 `field` 参数)
|
||||
- **P3-5 误报**:登录默认 tenant_id 是开发环境行为,非 bug
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 安全热修复(P0 安全,4 个问题)
|
||||
|
||||
### 1.1 P0-1: 上传文件认证
|
||||
- **文件**: `crates/erp-server/src/main.rs:543-546`
|
||||
- **现状**: `ServeDir` 在 auth middleware 之外
|
||||
- **修复**: 将 `/uploads` 移到 protected_routes,或添加 `axum_middleware::from_fn` JWT 检查
|
||||
- **方案**: 创建 `serve_file_with_auth` 中间件,从 query param 或 header 取 token 验证
|
||||
|
||||
### 1.2 P0-2: analytics/batch 认证
|
||||
- **文件**: `crates/erp-server/src/main.rs:497-500`
|
||||
- **现状**: 在 `public_routes` 中
|
||||
- **修复**: 移到 `protected_routes`(加 `.merge(...)` 到 line 514 的 protected_routes)
|
||||
|
||||
### 1.3 P0-3: plugin engine SQL 注入
|
||||
- **文件**: `crates/erp-plugin/src/engine.rs:630-637`
|
||||
- **现状**: `format!()` 拼接 `pid`
|
||||
- **修复**: 改用 `Statement::from_sql_and_values` + `$1`, `$2` 参数化
|
||||
|
||||
### 1.4 P0-new: stats_service compute_avg_field SQL 注入
|
||||
- **文件**: `crates/erp-health/src/service/stats_service.rs:423-444`
|
||||
- **现状**: `format!("SELECT AVG({field}) AS avg_val...")` 直接拼接字段名
|
||||
- **修复**: 添加字段名白名单验证 + 使用 `CAST(AVG(...) AS FLOAT8)` 同时解决类型问题
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 数据完整性修复(P0 数据,4 个问题)
|
||||
|
||||
### 2.1 P0-4: 组织删除级联(补充检查)
|
||||
- **文件**: `crates/erp-auth/src/service/org_service.rs:240-252`
|
||||
- **现状**: 已检查子组织,未检查部门
|
||||
- **修复**: 在 line 252 后添加部门存在性检查
|
||||
```rust
|
||||
// Check for departments under this org
|
||||
let depts = department::Entity::find()
|
||||
.filter(department::Column::OrganizationId.eq(id))
|
||||
.filter(department::Column::DeletedAt.is_null())
|
||||
.one(db).await?;
|
||||
if depts.is_some() {
|
||||
return Err(AuthError::Validation("该组织下存在部门,无法删除".into()));
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 P0-5: 部门删除级联(补充检查)
|
||||
- **文件**: `crates/erp-auth/src/service/dept_service.rs:264-275`
|
||||
- **现状**: 已检查子部门,未检查岗位
|
||||
- **修复**: 添加岗位存在性检查
|
||||
|
||||
### 2.3 P0-6: 岗位删除级联
|
||||
- **文件**: `crates/erp-auth/src/service/position_service.rs:214-249`
|
||||
- **现状**: 无关联检查
|
||||
- **修复**: 检查 user_position 关联表中是否有用户分配到此岗位
|
||||
|
||||
### 2.4 P0-8: workflow on_tenant_deleted
|
||||
- **文件**: `crates/erp-workflow/src/module.rs:148-154`
|
||||
- **现状**: 空操作 `Ok(())`
|
||||
- **修复**: 实现 5 个实体的批量软删除(process_definition → instance → task/token/variable)
|
||||
- **实体**: `process_definitions`, `process_instances`, `tasks`, `tokens`, `process_variables`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 并行网关 + P1 后端 Bug(7 个问题)
|
||||
|
||||
### 3.1 P0-7: 并行网关 token 关联
|
||||
- **文件**: `crates/erp-workflow/src/engine/executor.rs:369-425`
|
||||
- **修复**: 在 token 表添加 `fork_id` 字段(或使用 token 创建时间窗口),区分不同 fork 产生的 token
|
||||
- **轻量方案**: 使用 `SELECT ... FOR UPDATE` 加行锁 + 检查 token 的 `consumed_at` 时间窗口
|
||||
|
||||
### 3.2 P1-1: 统计报表 SQL 类型修复
|
||||
- **文件**: `crates/erp-health/src/service/stats_service.rs`
|
||||
- **修复**: SQL 中使用 `CAST(AVG(...) AS FLOAT8)` 或 `AVG(...)::FLOAT8`
|
||||
- **同时修复**: `compute_avg_field` 的字段名白名单
|
||||
|
||||
### 3.3 P1-12: plugin host 表名消毒
|
||||
- **文件**: `crates/erp-plugin/src/host.rs:339`
|
||||
- **修复**: 使用已有的 `sanitize_identifier()` 函数
|
||||
|
||||
### 3.4 P1-10: workflow deprecated 状态
|
||||
- **文件**: `crates/erp-workflow/src/service/definition_service.rs`
|
||||
- **修复**: 添加 `deprecate` 方法,实现 `published → deprecated` 转换
|
||||
|
||||
### 3.5 P1-11: workflow 更新验证
|
||||
- **文件**: `crates/erp-workflow/src/service/definition_service.rs:174-181`
|
||||
- **修复**: nodes 或 edges 任一存在即执行验证
|
||||
|
||||
### 3.6 P0-9: 小程序 .gitignore
|
||||
- **文件**: `apps/miniprogram/.gitignore`
|
||||
- **修复**: 添加 `.env`, `.env.*`, `*.log`
|
||||
|
||||
### 3.7 P1-19: 小程序加密密钥
|
||||
- **文件**: `apps/miniprogram/.env`
|
||||
- **修复**: 生成 64 字符 hex 强密钥替换
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 消息模块修复(P1,5 个问题)
|
||||
|
||||
### 4.1 P1-5: 通知偏好 GET + version
|
||||
- **后端**: `crates/erp-message/src/module.rs` 添加 `GET /message-subscriptions` 路由
|
||||
- **后端**: `crates/erp-message/src/handler/subscription_handler.rs` 添加 `get_subscription` handler
|
||||
- **前端**: `apps/web/src/pages/messages/NotificationPreferences.tsx`
|
||||
- useEffect 中调用 GET API 加载已有配置
|
||||
- 保存时发送 version 字段
|
||||
|
||||
### 4.2 P1-4: 消息模板 CRUD
|
||||
- **后端**: `template_service.rs` 添加 `update`, `delete` 方法
|
||||
- **后端**: `module.rs` 添加 `PUT /message-templates/{id}`, `DELETE /message-templates/{id}` 路由
|
||||
- **前端**: `MessageTemplates.tsx` 添加编辑/删除按钮
|
||||
|
||||
### 4.3 P2-6/7/8: 消息 store + mark_all_read 修复
|
||||
- `stores/message.ts`: markAsRead 改为乐观更新 + 失败回滚
|
||||
- `stores/message.ts`: 添加 markAllRead action,重置 unreadCount 为 0
|
||||
- `message_service.rs:298-326`: mark_all_read SQL 中添加 `version = version + 1`
|
||||
|
||||
### 4.4 P2-9: 通知面板点击导航
|
||||
- `NotificationPanel.tsx:81-85`: 添加 `navigate('/messages')` 跳转
|
||||
|
||||
### 4.5 P1-20: urlCheck 配置
|
||||
- **文件**: `apps/miniprogram/project.config.json:6`
|
||||
- **修复**: 添加注释说明仅开发环境使用 false,或改用条件配置
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 前端 P2-P3 Bug 修复(15 个问题)
|
||||
|
||||
### 5.1 P2-1: 随访任务患者名 UUID
|
||||
- **文件**: `apps/web/src/pages/health/FollowUpTaskList.tsx`
|
||||
- **修复**: 在 `fetchTasks` 后添加 `AppointmentList` 风格的批量 ID 解析循环
|
||||
|
||||
### 5.2 P2-5: AppointmentList 冗余请求
|
||||
- **文件**: `apps/web/src/pages/health/AppointmentList.tsx:103-121`
|
||||
- **修复**: 分离 patient_id 和 doctor_id,分别调用对应 API
|
||||
|
||||
### 5.3 P3-3: PatientDetail 标题"页面"
|
||||
- **文件**: `apps/web/src/layouts/MainLayout.tsx:84-95, 387`
|
||||
- **修复**: 将 `routeTitleFallback` 查找改为路径模式匹配(用 `startsWith` + 动态段替换)
|
||||
|
||||
### 5.4 P3-1: 已完成任务显示操作按钮
|
||||
- **文件**: 健康模块各列表页
|
||||
- **修复**: 根据状态条件渲染按钮
|
||||
|
||||
### 5.5 P2-17: PluginMarket installed 字段
|
||||
- **文件**: `apps/web/src/pages/PluginMarket.tsx:68`
|
||||
- **修复**: `Set` 改为 `result.data.map(p => p.id)` 而非 `p.name`
|
||||
|
||||
### 5.6 P3-6: 审计日志 UUID
|
||||
- **文件**: `apps/web/src/pages/settings/AuditLogViewer.tsx:123-135`
|
||||
- **修复**: 添加用户 ID → 用户名解析(批量查询或缓存)
|
||||
|
||||
### 5.7 P3-7: 审计日志资源类型过滤
|
||||
- **文件**: `AuditLogViewer.tsx:7-18`
|
||||
- **修复**: 添加 plugin 相关类型到 RESOURCE_TYPE_OPTIONS
|
||||
|
||||
### 5.8 P3-9: Kanban version 硬编码
|
||||
- **文件**: `apps/web/src/pages/PluginKanbanPage.tsx:113`
|
||||
- **修复**: 使用 record 的实际 version 字段
|
||||
|
||||
### 5.9 P3-11: destroyOnHidden → destroyOnClose
|
||||
- **文件**: `ProcessDefinitions.tsx:192`
|
||||
- **修复**: 替换 prop 名
|
||||
|
||||
### 5.10 P3-13: 深色模式 Tag
|
||||
- **文件**: `PendingTasks.tsx:95-101`
|
||||
- **修复**: 使用 `isDark` 条件判断颜色
|
||||
|
||||
### 5.11 P2-21: secure-storage 明文回退
|
||||
- **文件**: `apps/miniprogram/src/utils/secure-storage.ts:12`
|
||||
- **修复**: 生产环境下密钥为空时阻止存储,抛出错误
|
||||
|
||||
### 5.12 P3-12: InstanceMonitor node_id
|
||||
- **文件**: `InstanceMonitor.tsx:149`
|
||||
- **修复**: 从 definition 的 nodes 中查找 node_name
|
||||
|
||||
### 5.13 P2-14: 委派 UUID 输入
|
||||
- **文件**: `PendingTasks.tsx:207-213`
|
||||
- **修复**: 替换为用户搜索选择组件
|
||||
|
||||
### 5.14 P2-15: UpdateDefinition version
|
||||
- **文件**: `apps/web/src/api/workflowDefinitions.ts:45-51`
|
||||
- **修复**: 添加 version 字段
|
||||
|
||||
### 5.15 P3-4: any 类型替换
|
||||
- 搜索 `apps/web/src/` 中 4 处 any,替换为具体类型
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: 功能补全(P1 功能缺失,8 个问题)
|
||||
|
||||
### 6.1 P1-3: 消息 SSE 推送(最小可行方案)
|
||||
- **后端**: 添加 `GET /api/v1/messages/stream` SSE 端点
|
||||
- **后端**: EventBus 订阅 `message.sent` 事件推送给对应用户
|
||||
- **前端**: NotificationPanel 中连接 SSE,收到事件立即更新
|
||||
- **注意**: 先实现 SSE(比 WebSocket 简单),后续可升级
|
||||
|
||||
### 6.2 P1-6/7/8/9: 工作流引擎功能补全
|
||||
- **P1-6 ServiceTask**: 添加 HTTP 调用能力(基础版:支持 GET/POST URL)
|
||||
- **P1-7 事件注册**: 实现 `register_event_handlers`,监听 `user.deleted` 等事件
|
||||
- **P1-8 任务认领**: 添加 `claim` 方法,支持 candidate_groups 过滤列表
|
||||
- **P1-9 超时升级**: timeout checker 中添加自动通知发布
|
||||
|
||||
### 6.3 P1-15: 名称唯一性
|
||||
- **文件**: `org_service.rs`, `dept_service.rs`
|
||||
- **修复**: create/update 时检查同 tenant 下名称唯一性
|
||||
|
||||
### 6.4 P1-18: 消息群发 fan-out
|
||||
- **文件**: `message_service.rs`
|
||||
- **修复**: 当 recipient_type 为 role/dept/all 时,查询对应用户列表,批量创建消息
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: P3-P4 收尾 + 优化(6 个问题)
|
||||
|
||||
- P4-1: PluginAdmin purge 按钮状态
|
||||
- P4-3: recover_plugins tenant 过滤
|
||||
- P4-4: LanguageManager 编辑弹窗
|
||||
- P4-5: ChangePassword 后端验证
|
||||
- P4-6: Settings API URL 编码
|
||||
- P3-2: ArticleEditor 图片上传(标记为未来任务)
|
||||
- P3-14: 偏好设置 DND 时间范围验证
|
||||
- P3-15: 小程序空状态处理
|
||||
|
||||
---
|
||||
|
||||
## 验证计划
|
||||
|
||||
每个 Phase 完成后执行:
|
||||
|
||||
1. `cargo check` — 编译通过
|
||||
2. `cargo test --workspace` — 所有测试通过
|
||||
3. 浏览器验证 — 启动服务,操作对应页面确认修复生效
|
||||
4. `git commit` — 提交修复
|
||||
5. `git push` — 推送到远程
|
||||
|
||||
### 关键验证点
|
||||
|
||||
| Phase | 验证方式 |
|
||||
|-------|---------|
|
||||
| Phase 1 | curl 无 token 访问 /uploads 应 401 |
|
||||
| Phase 2 | 尝试删除含部门的组织应返回错误 |
|
||||
| Phase 3 | 统计报表页面应正常加载数据 |
|
||||
| Phase 4 | 偏好设置保存后重载应显示已有配置 |
|
||||
| Phase 5 | FollowUpTaskList 患者名应显示而非 UUID |
|
||||
| Phase 6 | 发送角色消息,该角色用户应收到 |
|
||||
| Phase 7 | 全量回归测试 |
|
||||
@@ -1,285 +0,0 @@
|
||||
# Phase 7: 审计日志 + 乐观锁 + Redis 限流 + 事件 Outbox
|
||||
|
||||
## Context
|
||||
|
||||
Phase 1-6 已完成。对比设计规格发现 4 项核心基础设施缺失:
|
||||
1. **审计日志** — AuditLog 类型存在但从未使用,audit_logs 表存在但无 Entity/Service
|
||||
2. **乐观锁** — 所有实体有 version 字段但更新时不检查/递增,DTO 不暴露 version
|
||||
3. **Redis 限流** — 客户端创建后立即丢弃(`_redis_client`),未存入 AppState
|
||||
4. **事件 Outbox** — EventBus 纯内存 broadcast,重启即丢失,无持久化
|
||||
|
||||
## 实施顺序与依赖
|
||||
|
||||
```
|
||||
Task 7.1 乐观锁 (erp-core error helper)
|
||||
→ Task 7.2 乐观锁 (全部 service 方法 + DTO)
|
||||
→ Task 7.3 审计日志 (Entity + Service + 集成)
|
||||
→ Task 7.4 Redis 限流 (AppState + 中间件)
|
||||
→ Task 7.5 事件 Outbox (迁移 + Entity + EventBus 改造)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7.1: 乐观锁 — erp-core 基础设施
|
||||
|
||||
**修改文件:**
|
||||
- `crates/erp-core/src/error.rs` — 添加 `VersionMismatch` 变体 + `check_version()` helper
|
||||
|
||||
```rust
|
||||
// 新增变体
|
||||
#[error("版本冲突: 数据已被其他操作修改,请刷新后重试")]
|
||||
VersionMismatch,
|
||||
|
||||
// 新增 helper 函数
|
||||
pub fn check_version(expected: i32, actual: i32) -> AppResult<i32> {
|
||||
if expected == actual { Ok(actual + 1) }
|
||||
else { Err(AppError::VersionMismatch) }
|
||||
}
|
||||
```
|
||||
|
||||
IntoResponse 中 `VersionMismatch` 映射到 `StatusCode::CONFLICT` (409)。
|
||||
|
||||
---
|
||||
|
||||
## Task 7.2: 乐观锁 — 全部 Service 方法 + DTO
|
||||
|
||||
**原则:** 所有用户可调用的 update/delete 方法必须检查并递增 version。
|
||||
|
||||
### DTO 变更
|
||||
|
||||
**所有 Update*Req** 添加 `pub version: i32` 字段(必填)。涉及:
|
||||
|
||||
| Crate | DTO 文件 | DTOs |
|
||||
|-------|---------|------|
|
||||
| erp-auth | `dto.rs` | UpdateUserReq, UpdateRoleReq, UpdateOrganizationReq, UpdateDepartmentReq, UpdatePositionReq |
|
||||
| erp-config | `dto.rs` | UpdateDictionaryReq, UpdateDictionaryItemReq, UpdateMenuReq, UpdateNumberingRuleReq |
|
||||
| erp-workflow | `dto.rs` | UpdateProcessDefinitionReq |
|
||||
| erp-message | `dto.rs` | UpdateSubscriptionReq (如果存在) |
|
||||
|
||||
**所有 *Resp** 添加 `pub version: i32` 字段。涉及:
|
||||
|
||||
| Crate | Resp DTOs |
|
||||
|-------|-----------|
|
||||
| erp-auth | UserResp, RoleResp, OrganizationResp, DepartmentResp, PositionResp |
|
||||
| erp-config | DictionaryResp, DictionaryItemResp, MenuResp, SettingResp, NumberingRuleResp |
|
||||
| erp-workflow | ProcessDefinitionResp, ProcessInstanceResp, TaskResp |
|
||||
| erp-message | MessageResp, MessageSubscriptionResp |
|
||||
|
||||
每个 `model_to_resp` 函数添加 `version: m.version`。
|
||||
|
||||
### Service 方法变更
|
||||
|
||||
**Update 模式(有 DTO):**
|
||||
```rust
|
||||
// 在 update 方法中,读取 model 后:
|
||||
let next_ver = erp_core::error::check_version(req.version, model.version)?;
|
||||
// ... 设置字段 ...
|
||||
active.version = Set(next_ver);
|
||||
active.update(db).await?;
|
||||
```
|
||||
|
||||
**Delete 模式(无 DTO version):**
|
||||
```rust
|
||||
// delete 方法中,读取 model 后:
|
||||
active.version = Set(model.version + 1);
|
||||
```
|
||||
|
||||
**涉及文件(13 个 service 的 update/delete 方法):**
|
||||
|
||||
| Crate | 文件 | 方法 |
|
||||
|-------|------|------|
|
||||
| erp-auth | `user_service.rs` | update, delete |
|
||||
| erp-auth | `role_service.rs` | update, delete |
|
||||
| erp-auth | `org_service.rs` | update, delete |
|
||||
| erp-auth | `dept_service.rs` | update, delete |
|
||||
| erp-auth | `position_service.rs` | update, delete |
|
||||
| erp-config | `dictionary_service.rs` | update, delete, update_item, delete_item |
|
||||
| erp-config | `menu_service.rs` | update, delete |
|
||||
| erp-config | `setting_service.rs` | set (update 分支), delete |
|
||||
| erp-config | `numbering_service.rs` | update, delete |
|
||||
| erp-workflow | `definition_service.rs` | update, publish, delete |
|
||||
| erp-workflow | `instance_service.rs` | 状态变更方法 (suspend/resume/terminate) |
|
||||
| erp-workflow | `task_service.rs` | complete, delegate |
|
||||
| erp-message | `message_service.rs` | mark_read, delete |
|
||||
| erp-message | `subscription_service.rs` | upsert (update 分支) |
|
||||
|
||||
**注意:** `numbering_service::generate_number` 使用 advisory lock,不需要 version 检查。
|
||||
|
||||
### 前端适配
|
||||
|
||||
前端所有编辑表单需要在请求时传递 version 字段。涉及:
|
||||
- `apps/web/src/pages/` 下所有调用 PUT API 的页面
|
||||
|
||||
---
|
||||
|
||||
## Task 7.3: 审计日志
|
||||
|
||||
### 7.3a: SeaORM Entity
|
||||
|
||||
**新建文件:**
|
||||
- `crates/erp-core/src/entity/mod.rs`
|
||||
- `crates/erp-core/src/entity/audit_log.rs`
|
||||
|
||||
**修改文件:**
|
||||
- `crates/erp-core/src/lib.rs` — 添加 `pub mod entity;`
|
||||
- `crates/erp-core/Cargo.toml` — 添加 sea-orm 依赖(如果尚未有)
|
||||
|
||||
audit_log.rs Entity 映射已有的 `audit_logs` 表(迁移 #26 已存在)。
|
||||
|
||||
### 7.3b: 审计记录服务
|
||||
|
||||
**新建文件:** `crates/erp-core/src/audit_service.rs`
|
||||
|
||||
```rust
|
||||
/// 持久化审计日志到 audit_logs 表。
|
||||
/// 使用 fire-and-forget 模式:失败仅记录日志,不影响业务操作。
|
||||
pub async fn record(log: AuditLog, db: &DatabaseConnection) {
|
||||
// AuditLog → audit_log::ActiveModel → insert
|
||||
// 失败时 tracing::warn!
|
||||
}
|
||||
```
|
||||
|
||||
**修改文件:** `crates/erp-core/src/lib.rs` — 添加 `pub mod audit_service;`
|
||||
|
||||
### 7.3c: 集成到所有 mutation service
|
||||
|
||||
在每个 service 的 create/update/delete 方法中,操作成功后调用 `audit_service::record()`。
|
||||
|
||||
**请求信息获取:** handler 层从 `HeaderMap` 提取 IP 和 User-Agent,传给 service。
|
||||
|
||||
```rust
|
||||
// handler 中
|
||||
fn extract_request_info(headers: &HeaderMap) -> (Option<String>, Option<String>) {
|
||||
let ip = headers.get("x-forwarded-for").or_else(|| headers.get("x-real-ip"))
|
||||
.and_then(|v| v.to_str().ok()).map(|s| s.to_string());
|
||||
let ua = headers.get("user-agent").and_then(|v| v.to_str().ok()).map(|s| s.to_string());
|
||||
(ip, ua)
|
||||
}
|
||||
```
|
||||
|
||||
Handler 签名增加 `headers: HeaderMap` 参数,service 方法签名增加 `ip: Option<String>, user_agent: Option<String>`。
|
||||
|
||||
**涉及文件(与乐观锁相同 + handler 层):**
|
||||
|
||||
| Crate | Handler 文件 |
|
||||
|-------|-------------|
|
||||
| erp-auth | `user_handler.rs`, `role_handler.rs`, `org_handler.rs` |
|
||||
| erp-config | `dictionary_handler.rs`, `menu_handler.rs`, `setting_handler.rs`, `numbering_handler.rs` |
|
||||
| erp-workflow | `definition_handler.rs`, `instance_handler.rs`, `task_handler.rs` |
|
||||
| erp-message | `message_handler.rs`, `subscription_handler.rs` |
|
||||
|
||||
---
|
||||
|
||||
## Task 7.4: Redis 限流
|
||||
|
||||
### 7.4a: Redis 存入 AppState
|
||||
|
||||
**修改文件:**
|
||||
- `crates/erp-server/src/state.rs` — `AppState` 添加 `pub redis: redis::Client`
|
||||
- `crates/erp-server/src/main.rs` — `_redis_client` → `redis_client`,传入 AppState
|
||||
|
||||
### 7.4b: 限流中间件
|
||||
|
||||
**新建文件:**
|
||||
- `crates/erp-server/src/middleware/mod.rs`
|
||||
- `crates/erp-server/src/middleware/rate_limit.rs`
|
||||
|
||||
使用 Redis INCR + EXPIRE 实现滑动窗口:
|
||||
- Key: `rate_limit:{prefix}:{identifier}`
|
||||
- 登录: 5 次/分钟/IP
|
||||
- 写操作: 100 次/分钟/user_id
|
||||
|
||||
### 7.4c: 应用限流层
|
||||
|
||||
**修改文件:** `crates/erp-server/src/main.rs`
|
||||
- 登录路由添加 IP 限流层
|
||||
- protected routes 添加 user_id 限流层
|
||||
- 超限返回 HTTP 429 Too Many Requests
|
||||
|
||||
**修改文件:** `crates/erp-core/src/error.rs`
|
||||
- 添加 `TooManyRequests` 变体(可选,中间件可直接返回 429)
|
||||
|
||||
---
|
||||
|
||||
## Task 7.5: 事件 Outbox 持久化
|
||||
|
||||
### 7.5a: 数据库迁移
|
||||
|
||||
**新建文件:** `crates/erp-server/migration/src/m20260416_000031_create_domain_events.rs`
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS domain_events (
|
||||
id UUID PRIMARY KEY,
|
||||
tenant_id UUID NOT NULL,
|
||||
event_type VARCHAR(200) NOT NULL,
|
||||
payload JSONB,
|
||||
correlation_id UUID,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
published_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_domain_events_status ON domain_events (status, created_at);
|
||||
CREATE INDEX idx_domain_events_tenant ON domain_events (tenant_id);
|
||||
```
|
||||
|
||||
**修改文件:** `crates/erp-server/migration/src/lib.rs` — 注册新迁移
|
||||
|
||||
### 7.5b: SeaORM Entity
|
||||
|
||||
**新建文件:** `crates/erp-core/src/entity/domain_event.rs`
|
||||
|
||||
**修改文件:** `crates/erp-core/src/entity/mod.rs` — 添加 `pub mod domain_event;`
|
||||
|
||||
### 7.5c: EventBus 改造
|
||||
|
||||
**修改文件:** `crates/erp-core/src/events.rs`
|
||||
|
||||
- 现有 `publish()` 重命名为 `broadcast()`(内部使用)
|
||||
- 新增 `publish_with_persist(event, db)` — 先 INSERT domain_events,再 broadcast
|
||||
- INSERT 失败时仅 log warning,仍然 broadcast(best-effort)
|
||||
|
||||
### 7.5d: 更新所有 publish 调用点
|
||||
|
||||
全部 25 个 `event_bus.publish(...)` 调用改为 `event_bus.publish_with_persist(event, db).await`。
|
||||
|
||||
**涉及文件:**
|
||||
- `erp-auth/src/service/` — 5 个文件 (user, role, org, dept, position)
|
||||
- `erp-config/src/service/` — 4 个文件 (dictionary, menu, setting, numbering)
|
||||
- `erp-workflow/src/service/` — 3 个文件 (definition, instance, task)
|
||||
- `erp-message/src/service/` — 1 个文件 (message_service)
|
||||
|
||||
### 7.5e: Outbox Relay 后台任务
|
||||
|
||||
**新建文件:** `crates/erp-server/src/outbox.rs`
|
||||
|
||||
后台 tokio task 每 5 秒扫描 `domain_events WHERE status = 'pending'`,重新 broadcast 并标记为 published。
|
||||
|
||||
**修改文件:** `crates/erp-server/src/main.rs` — 启动 outbox relay
|
||||
|
||||
---
|
||||
|
||||
## 关键文件索引
|
||||
|
||||
| 用途 | 文件路径 |
|
||||
|------|---------|
|
||||
| 错误类型 | `crates/erp-core/src/error.rs` |
|
||||
| 事件总线 | `crates/erp-core/src/events.rs` |
|
||||
| 审计日志类型 | `crates/erp-core/src/audit.rs` |
|
||||
| AppState | `crates/erp-server/src/state.rs` |
|
||||
| 服务器入口 | `crates/erp-server/src/main.rs` |
|
||||
| 迁移注册 | `crates/erp-server/migration/src/lib.rs` |
|
||||
| Auth DTO | `crates/erp-auth/src/dto.rs` |
|
||||
| Auth Service 参考 | `crates/erp-auth/src/service/user_service.rs` |
|
||||
| Auth Handler 参考 | `crates/erp-auth/src/handler/user_handler.rs` |
|
||||
|
||||
## 验证方式
|
||||
|
||||
1. `cargo check` — 全 workspace 编译通过
|
||||
2. `cargo test --workspace` — 所有测试通过
|
||||
3. 手动测试:更新用户两次(第二次用旧 version)→ 409 Conflict
|
||||
4. 手动测试:登录限流 → 第 6 次返回 429
|
||||
5. 查询 `SELECT * FROM audit_logs` → 验证审计记录
|
||||
6. 查询 `SELECT * FROM domain_events` → 验证事件持久化
|
||||
7. 重启服务后验证 pending 事件被 relay 处理
|
||||
@@ -1,172 +0,0 @@
|
||||
# P1-P4 审计修复实施计划
|
||||
|
||||
## Context
|
||||
|
||||
对 P1-P4 审计发现 8 项高/中优先级缺失:Excel/CSV 导入导出、市场后端 API、对账扫描、运行时监控、通知规则、编号 reset_rule。本计划按优先级分 3 批推进,每批独立可提交。
|
||||
|
||||
---
|
||||
|
||||
## 第一批:高优先级(Excel/CSV + 市场后端 + 对账扫描)
|
||||
|
||||
### 1.1 Excel/CSV 导入导出
|
||||
|
||||
**思路**: 后端新增 `csv` + `rust_xlsxwriter` 依赖,export handler 支持 format 参数输出 CSV/XLSX;前端同时支持。
|
||||
|
||||
**后端改动**:
|
||||
|
||||
1. `Cargo.toml` (workspace): 新增 `csv = "1"` 和 `rust_xlsxwriter = "0.82"`
|
||||
2. `crates/erp-plugin/Cargo.toml`: 添加 `csv` 和 `rust_xlsxwriter` 依赖
|
||||
3. `crates/erp-plugin/src/data_service.rs`:
|
||||
- `export()` 签名增加 `format: Option<String>` 参数
|
||||
- 内部新增 `export_csv()` 和 `export_xlsx()` 私有方法,返回 `Vec<u8>` bytes
|
||||
- format 为空/json 时返回原 JSON;csv/xlsx 时返回二进制
|
||||
- 返回类型改为 enum `ExportPayload { Json(Vec<Value>), Csv(Vec<u8>), Xlsx(Vec<u8>) }`
|
||||
4. `crates/erp-plugin/src/data_dto.rs`: ExportParams 的 format 字段已有
|
||||
5. `crates/erp-plugin/src/handler/data_handler.rs`:
|
||||
- `export_plugin_data` 根据 format 参数返回不同 Content-Type:
|
||||
- JSON: `application/json`
|
||||
- CSV: `text/csv` + `Content-Disposition: attachment`
|
||||
- XLSX: `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
|
||||
- 返回类型改为 `axum::response::Response`(不是 Json<>)
|
||||
6. 前端 `pluginData.ts`: `exportPluginData` 支持 format 参数,CSV/XLSX 时用 `responseType: 'blob'`
|
||||
7. 前端 `PluginCRUDPage.tsx`: 导出按钮增加下拉菜单选择格式(JSON/CSV/Excel)
|
||||
|
||||
**注意**: 导入仍保持 JSON(复杂度低),模板生成和导入历史不在本批范围。
|
||||
|
||||
### 1.2 P4 市场后端 API
|
||||
|
||||
**思路**: 新建 `market_service.rs` + `market_handler.rs`,复用 DB 迁移已建好的 `plugin_market_entries` 和 `plugin_market_reviews` 表。
|
||||
|
||||
**新增文件**:
|
||||
- `crates/erp-plugin/src/service/market_service.rs`: 市场业务逻辑
|
||||
- `crates/erp-plugin/src/handler/market_handler.rs`: 市场 API handler
|
||||
- `crates/erp-plugin/src/entity/market_entry.rs`: SeaORM Entity
|
||||
- `crates/erp-plugin/src/entity/market_review.rs`: SeaORM Entity
|
||||
|
||||
**后端 API**:
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/v1/market/entries` | 浏览市场目录(分类/搜索/分页) |
|
||||
| GET | `/api/v1/market/entries/{id}` | 市场条目详情 |
|
||||
| POST | `/api/v1/market/entries/{id}/install` | 从市场一键安装 |
|
||||
| GET | `/api/v1/market/entries/{id}/reviews` | 查看评论 |
|
||||
| POST | `/api/v1/market/entries/{id}/reviews` | 提交评分/评论 |
|
||||
|
||||
**一键安装逻辑**: 从 `plugin_market_entries` 取 `wasm_binary` + `manifest_toml`,调用已有的 `PluginService::upload` + `PluginService::install` + `PluginService::enable`。
|
||||
|
||||
**依赖提示**: install 时检查 manifest.dependencies,若目标插件未安装则返回警告(软提示,不阻塞)。
|
||||
|
||||
**前端改动**:
|
||||
- `apps/web/src/api/plugins.ts`: 新增市场 API 函数
|
||||
- `apps/web/src/pages/PluginMarket.tsx`: 对接真实 API,替换 mock 数据;增加评分提交 UI;安装按钮对接真实 API
|
||||
|
||||
### 1.3 P1 对账扫描
|
||||
|
||||
**思路**: 新增 `reconcile` service 方法和 handler,在插件重新启用时扫描悬空引用。
|
||||
|
||||
**后端改动**:
|
||||
- `crates/erp-plugin/src/data_service.rs`: 新增 `reconcile_references()` 方法
|
||||
- 查找所有指向目标插件的 `ref_entity` 字段(从 plugin_entities schema_json 解析)
|
||||
- 扫描这些字段的 UUID 值,验证目标表中是否存在
|
||||
- 返回 `ReconciliationReport { valid: N, dangling: M, details: Vec<DanglingRef> }`
|
||||
- `crates/erp-plugin/src/data_dto.rs`: 新增 DTO
|
||||
- `crates/erp-plugin/src/handler/data_handler.rs`: 新增 `reconcile_refs` handler
|
||||
- `crates/erp-plugin/src/module.rs`: 注册路由 `POST /plugins/{plugin_id}/reconcile`
|
||||
|
||||
**前端**: 暂不实现完整对账 UI(低优先级),仅提供 API 供后续使用。
|
||||
|
||||
---
|
||||
|
||||
## 第二批:中优先级(运行时监控 + 通知规则 + 编号 reset)
|
||||
|
||||
### 2.1 P3 运行时监控
|
||||
|
||||
**后端改动**:
|
||||
1. 新建迁移 `m20260420_000041_plugin_runtime_metrics.rs`:
|
||||
- `plugin_runtime_metrics` 表: plugin_id, tenant_id, error_count, total_invocations, avg_response_ms, fuel_consumption_avg, memory_peak_bytes, last_error, updated_at
|
||||
2. `crates/erp-plugin/src/engine.rs`:
|
||||
- `LoadedPlugin` 新增 `metrics: Arc<RwLock<RuntimeMetrics>>` 字段
|
||||
- `execute_wasm` 中采集指标: 记录开始时间、成功/失败计数、fuel 消耗
|
||||
- 定期持久化到 DB(每 10 次调用或 60 秒)
|
||||
3. `crates/erp-plugin/src/handler/plugin_handler.rs`:
|
||||
- 扩展 `health_check` 返回 RuntimeMetrics
|
||||
- 新增 `GET /admin/plugins/{id}/metrics` 端点
|
||||
|
||||
### 2.2 P2 通知规则引擎
|
||||
|
||||
**思路**: 复用 EventBus 的 `subscribe_filtered` + erp-message 的 `send_system`,在 plugin 模块启动时监听 `plugin.trigger.*` 前缀事件。
|
||||
|
||||
**后端改动**:
|
||||
- `crates/erp-plugin/src/module.rs`: 启动事件监听(参考 erp-message 的 `start_event_listener` 模式)
|
||||
- 新建 `crates/erp-plugin/src/notification.rs`:
|
||||
- 订阅 `plugin.trigger.*` 事件
|
||||
- 查询 trigger_events 声明,匹配事件名
|
||||
- 调用 erp-message 的系统消息发送(通过 EventBus 发布 `message.send` 事件,或直接调用 message service 的 REST API)
|
||||
- 通知对象: 通过 manifest 声明扩展(当前简化为通知所有管理员)
|
||||
|
||||
### 2.3 P2 编号 reset_rule
|
||||
|
||||
**思路**: 参考 erp-config 的 `numbering_service.rs` 的 `maybe_reset_sequence` 模式,替换 PostgreSQL 序列为表行 + advisory lock。
|
||||
|
||||
**后端改动**:
|
||||
- `crates/erp-plugin/src/host.rs`: 重写 `numbering_generate`
|
||||
- 改用 `pg_advisory_xact_lock` + 表行序列(而非 PostgreSQL SEQUENCE)
|
||||
- 在事务内: 读序列行 → 检查 reset_rule 是否需要重置 → 递增/重置 → 写回
|
||||
- 序列表: 使用已有的动态表模式,或新建 `plugin_numbering_sequences` 表
|
||||
- `crates/erp-plugin/src/engine.rs`: `NumberingRule` 中 reset_rule 字段已被传递但未使用,直接在 host.rs 中消费
|
||||
|
||||
---
|
||||
|
||||
## 第三批:低优先级(配置变更通知 + 自定义视图)
|
||||
|
||||
### 3.1 P2 配置变更通知
|
||||
|
||||
**后端改动**:
|
||||
- `crates/erp-plugin/src/service.rs`: `update_config` 增加 `event_bus: &EventBus` 参数,更新成功后发布 `plugin.config.updated` 事件
|
||||
- `crates/erp-plugin/src/handler/plugin_handler.rs`: `update_plugin_config` handler 从 state 获取 event_bus 传入
|
||||
- `crates/erp-plugin/src/engine.rs`: 订阅 `plugin.config.updated` 事件,刷新内存中的 `plugin_config`
|
||||
|
||||
### 3.2 P2 自定义视图
|
||||
|
||||
**后端改动**:
|
||||
1. 新建迁移 `plugin_user_views` 表
|
||||
2. 新建 `crates/erp-plugin/src/service/view_service.rs`: CRUD user views
|
||||
3. 新建 handler: `GET/POST/PUT/DELETE /plugins/{plugin_id}/{entity}/views`
|
||||
4. **前端**: PluginCRUDPage 增加视图保存/加载 UI
|
||||
|
||||
---
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
| 文件 | 改动类型 |
|
||||
|------|---------|
|
||||
| `Cargo.toml` (workspace) | 新增 csv, rust_xlsxwriter 依赖 |
|
||||
| `crates/erp-plugin/Cargo.toml` | 新增依赖 |
|
||||
| `crates/erp-plugin/src/data_service.rs` | export format 支持, reconcile 方法 |
|
||||
| `crates/erp-plugin/src/data_dto.rs` | ExportPayload enum, ReconciliationReport |
|
||||
| `crates/erp-plugin/src/handler/data_handler.rs` | export 返回 Response, reconcile handler |
|
||||
| `crates/erp-plugin/src/handler/market_handler.rs` | **新建** 市场 API |
|
||||
| `crates/erp-plugin/src/service/market_service.rs` | **新建** 市场业务逻辑 |
|
||||
| `crates/erp-plugin/src/entity/market_entry.rs` | **新建** SeaORM Entity |
|
||||
| `crates/erp-plugin/src/entity/market_review.rs` | **新建** SeaORM Entity |
|
||||
| `crates/erp-plugin/src/notification.rs` | **新建** 通知规则引擎 |
|
||||
| `crates/erp-plugin/src/engine.rs` | LoadedPlugin 增加 metrics, 配置热更新 |
|
||||
| `crates/erp-plugin/src/host.rs` | numbering_generate 重写 |
|
||||
| `crates/erp-plugin/src/service.rs` | update_config 增加 event_bus |
|
||||
| `crates/erp-plugin/src/module.rs` | 注册新路由, 启动通知监听 |
|
||||
| `crates/erp-plugin/src/lib.rs` | 导出新模块 |
|
||||
| `crates/erp-server/migration/src/m20260420_*.rs` | **新建** metrics 表迁移 |
|
||||
| `apps/web/src/api/plugins.ts` | 市场前端 API |
|
||||
| `apps/web/src/api/pluginData.ts` | export format 支持 |
|
||||
| `apps/web/src/pages/PluginMarket.tsx` | 对接真实 API |
|
||||
| `apps/web/src/pages/PluginCRUDPage.tsx` | 导出格式选择 |
|
||||
|
||||
## 验证计划
|
||||
|
||||
1. `cargo check` — 全 workspace 编译通过
|
||||
2. `pnpm build` — 前端构建通过
|
||||
3. 启动后端 + 前端,浏览器中验证:
|
||||
- CRM customer 导出 CSV/Excel 下载
|
||||
- 市场 API 返回数据(curl 测试)
|
||||
- 插件 health 接口返回 metrics
|
||||
4. 每批完成后独立提交推送
|
||||
@@ -1,61 +0,0 @@
|
||||
# 插件侧边栏三级菜单重构
|
||||
|
||||
## Context
|
||||
|
||||
当前插件菜单是扁平结构,所有插件的页面(客户管理、联系人、沟通记录等)都平铺在"插件"分组下。随着更多插件接入,菜单会变得杂乱无章。需要改为三级结构:
|
||||
|
||||
```
|
||||
插件(分组标题)
|
||||
└ CRM(插件名称,可折叠)
|
||||
├ 客户管理
|
||||
├ 联系人
|
||||
├ 沟通记录
|
||||
├ 标签管理
|
||||
└ 客户关系
|
||||
└ 未来插件名称(可折叠)
|
||||
├ 页面1
|
||||
└ 页面2
|
||||
```
|
||||
|
||||
## 改动范围
|
||||
|
||||
仅涉及前端两个文件,无需后端改动(`PluginMenuItem.pluginId` 字段已存在)。
|
||||
|
||||
### 1. `apps/web/src/stores/plugin.ts`
|
||||
|
||||
**改动:** 新增 `PluginMenuGroup` 类型和 `pluginMenuGroups` getter。
|
||||
|
||||
```ts
|
||||
export interface PluginMenuGroup {
|
||||
pluginId: string;
|
||||
pluginName: string; // 从 cachedPlugins 取 metadata.name
|
||||
items: PluginMenuItem[];
|
||||
}
|
||||
```
|
||||
|
||||
在 store 中添加一个 `pluginMenuGroups` 计算属性(Zustand getter),按 `pluginId` 分组,取插件名称从 `plugins` 数组中查找。
|
||||
|
||||
### 2. `apps/web/src/layouts/MainLayout.tsx`
|
||||
|
||||
**改动:**
|
||||
- 新增 `SidebarSubMenu` 组件:显示插件名称作为可折叠子标题,渲染其下级 `SidebarMenuItem`
|
||||
- 子标题使用 `expand-icon` (右箭头) 指示可折叠,点击切换展开/收起
|
||||
- 子标题样式:略深背景 + 左侧有缩进指示线
|
||||
- 子菜单项有额外左缩进(12px)表示层级
|
||||
- 折叠侧边栏时,子菜单标题只显示插件图标,悬浮弹出子项
|
||||
|
||||
**渲染逻辑:**
|
||||
```
|
||||
"插件" 标签
|
||||
for each group in pluginMenuGroups:
|
||||
<SidebarSubMenu title=group.pluginName items=group.items />
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
1. 启动前端 `pnpm dev`,访问侧边栏
|
||||
2. 确认 CRM 插件下所有 5 个菜单项嵌套在 "CRM" 子标题下
|
||||
3. 点击 "CRM" 子标题可展开/收起
|
||||
4. 折叠侧边栏后,CRM 子项通过 tooltip 或悬浮展示
|
||||
5. 点击各子项,路由正确跳转
|
||||
6. 页面标题(面包屑)正确显示
|
||||
@@ -1,430 +0,0 @@
|
||||
# P1 跨插件数据引用系统 — 实施计划
|
||||
|
||||
## Context
|
||||
|
||||
插件平台 P0 增强(混合执行模型/扩展聚合/原子回滚/Schema 演进)已全部完成。当前有两个行业插件(CRM + 进销存)运行在 WASM 插件系统上,但**跨插件数据引用完全不支持** — 进销存的 `customer_id` 只能存裸 UUID,无校验、无显示、无关联。
|
||||
|
||||
本计划实现 P1 跨插件数据引用系统,使插件能声明式引用其他插件的实体,并以财务插件作为验证载体。
|
||||
|
||||
**核心原则:** 外部引用永远是**软警告**,永不硬阻塞用户操作。
|
||||
|
||||
## 设计决策
|
||||
|
||||
| 决策点 | 方案 | 理由 |
|
||||
|--------|------|------|
|
||||
| Entity Registry | 复用 `plugin_entities` 表 + 新增 `manifest_id` 列 | 表已有 entity_name/table_name/schema_json,加列即可,无需新表 |
|
||||
| 跨插件引用标识 | 新增 `ref_plugin: Option<String>` 字段 | 比设计文档的 `ref_scope="external"` 更明确,直接指定目标插件 ID |
|
||||
| WIT 接口变更 | **不修改** WIT | 避免 recompile 所有插件,Host 层用点分记号 `"erp-crm.customer"` 解析 |
|
||||
| 表格列标签解析 | 新增批量 resolve-labels 端点 | O(1) 网络请求,`WHERE id = ANY($1)` 索引查找 |
|
||||
| 悬空引用对账 | 插件 re-enable 时异步触发 + 手动触发端点 | 不阻塞主流程,后台扫描 |
|
||||
|
||||
## 实施阶段总览
|
||||
|
||||
| Phase | 内容 | 依赖 | 预估 |
|
||||
|-------|------|------|------|
|
||||
| 1 | Manifest 扩展 + Entity Registry 数据层 | 无 | 1天 |
|
||||
| 2 | 后端跨插件引用解析 + 校验 | Phase 1 | 1天 |
|
||||
| 3 | API 端点(resolve-labels / registry / scan) | Phase 2 | 1天 |
|
||||
| 4 | 前端改造(EntitySelect + 列标签 + 降级) | Phase 3 | 1.5天 |
|
||||
| 5 | 悬空引用对账 | Phase 2 | 1天 |
|
||||
| 6 | 验证(进销存插件改造 + 端到端测试) | Phase 1-5 | 0.5天 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Manifest 扩展 + Entity Registry 数据层
|
||||
|
||||
> 纯数据结构和迁移,零运行时影响。现有插件完全兼容。
|
||||
|
||||
### 1.1 manifest.rs — 扩展 PluginField
|
||||
|
||||
文件:`crates/erp-plugin/src/manifest.rs`
|
||||
|
||||
在 `PluginField` struct(~line 82)新增:
|
||||
|
||||
```rust
|
||||
pub ref_plugin: Option<String>, // 目标插件 manifest ID(如 "erp-crm")
|
||||
pub ref_fallback_label: Option<String>, // 目标插件未安装时的降级显示文本
|
||||
```
|
||||
|
||||
两个新字段加 `#[serde(default)]`,向后兼容。
|
||||
|
||||
### 1.2 manifest.rs — 扩展 PluginRelation
|
||||
|
||||
CRM 的 plugin.toml 已在使用 `name`/`type`/`display_field`,但当前 struct 只解析 `entity`/`foreign_key`/`on_delete`,其余被 serde 静默丢弃。补齐:
|
||||
|
||||
```rust
|
||||
pub struct PluginRelation {
|
||||
pub entity: String,
|
||||
pub foreign_key: String,
|
||||
pub on_delete: OnDeleteStrategy,
|
||||
pub name: Option<String>, // serde(default)
|
||||
pub relation_type: Option<String>, // serde(default), "one_to_many" 等
|
||||
pub display_field: Option<String>, // serde(default)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 manifest.rs — 扩展 PluginEntity
|
||||
|
||||
新增 `is_public` 标记实体是否可被其他插件引用:
|
||||
|
||||
```rust
|
||||
pub is_public: Option<bool>, // serde(default), false by default
|
||||
```
|
||||
|
||||
### 1.4 数据库迁移 — plugin_entities 新增列
|
||||
|
||||
新迁移文件:`crates/erp-server/migration/src/m{timestamp}_entity_registry_columns.rs`
|
||||
|
||||
```sql
|
||||
-- 新增 manifest_id 列,避免每次 JOIN plugins 表
|
||||
ALTER TABLE plugin_entities
|
||||
ADD COLUMN IF NOT EXISTS manifest_id TEXT NOT NULL DEFAULT '';
|
||||
|
||||
-- 新增 is_public 列
|
||||
ALTER TABLE plugin_entities
|
||||
ADD COLUMN IF NOT EXISTS is_public BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- 回填 manifest_id(从 plugins.manifest_json 提取)
|
||||
UPDATE plugin_entities pe
|
||||
SET manifest_id = p.manifest_json->'metadata'->>'id'
|
||||
FROM plugins p
|
||||
WHERE pe.plugin_id = p.id AND pe.deleted_at IS NULL;
|
||||
|
||||
-- 跨插件查找索引
|
||||
CREATE INDEX IF NOT EXISTS idx_plugin_entities_cross_ref
|
||||
ON plugin_entities (manifest_id, entity_name, tenant_id)
|
||||
WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
### 1.5 SeaORM Entity 更新
|
||||
|
||||
文件:`crates/erp-plugin/src/entity/plugin_entity.rs`
|
||||
|
||||
新增字段映射:
|
||||
```rust
|
||||
pub manifest_id: String, // Column("manifest_id")
|
||||
pub is_public: bool, // Column("is_public")
|
||||
```
|
||||
|
||||
### 1.6 service.rs — install 时填充新列
|
||||
|
||||
文件:`crates/erp-plugin/src/service.rs` (~line 112)
|
||||
|
||||
在 `install` 方法创建 `plugin_entity` 记录时,设置:
|
||||
```rust
|
||||
manifest_id: Set(manifest.metadata.id.clone()),
|
||||
is_public: Set(entity_def.is_public.unwrap_or(false)),
|
||||
```
|
||||
|
||||
### 1.7 单元测试
|
||||
|
||||
- 解析含 `ref_plugin` + `ref_fallback_label` 的字段
|
||||
- 解析含 `name`/`type`/`display_field` 的 relation
|
||||
- 解析含 `is_public` 的 entity
|
||||
- 旧格式 TOML(无新字段)仍正常解析
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 后端跨插件引用解析 + 校验
|
||||
|
||||
> 让 `validate_ref_entities` 和 `db_query` 能解析其他插件的实体表。
|
||||
|
||||
### 2.1 data_service.rs — 跨插件实体解析
|
||||
|
||||
文件:`crates/erp-plugin/src/data_service.rs`
|
||||
|
||||
新增函数:
|
||||
|
||||
```rust
|
||||
/// 按 manifest_id + entity_name 跨插件解析实体信息
|
||||
pub async fn resolve_cross_plugin_entity(
|
||||
target_manifest_id: &str,
|
||||
entity_name: &str,
|
||||
tenant_id: Uuid,
|
||||
db: &DatabaseConnection,
|
||||
) -> AppResult<EntityInfo>
|
||||
```
|
||||
|
||||
查询 `plugin_entities` 表(`manifest_id = target AND entity_name = name AND tenant_id AND deleted_at IS NULL`),构建 `EntityInfo`。
|
||||
|
||||
### 2.2 data_service.rs — 修改 validate_ref_entities
|
||||
|
||||
文件:`crates/erp-plugin/src/data_service.rs` (~line 971)
|
||||
|
||||
当前逻辑:`resolve_manifest_id(plugin_id)` → `table_name(manifest_id, ref_entity)` — 始终用本插件的 manifest_id。
|
||||
|
||||
改为:
|
||||
1. 若 `field.ref_plugin` 存在 → 用 `ref_plugin` 作为 target_manifest_id
|
||||
2. 检查目标插件是否安装且活跃(查 `plugins` 表 status in `["running","installed"]`)
|
||||
3. **目标插件活跃** → 解析目标表名 → 执行 UUID 存在性校验(与现有逻辑相同)
|
||||
4. **目标插件未安装/禁用** → **跳过校验**(软警告,不阻塞)
|
||||
5. 若 `field.ref_plugin` 不存在 → 走原有同插件逻辑(完全兼容)
|
||||
|
||||
### 2.3 host.rs — HostState 跨插件实体映射
|
||||
|
||||
文件:`crates/erp-plugin/src/host.rs`
|
||||
|
||||
新增字段到 `HostState`:
|
||||
```rust
|
||||
pub(crate) cross_plugin_entities: HashMap<String, String>,
|
||||
// key: "erp-crm.customer" → value: "plugin_erp_crm__customer"
|
||||
```
|
||||
|
||||
修改 `db_query`(~line 168):
|
||||
```rust
|
||||
let table_name = if entity.contains('.') {
|
||||
// 点分记号 "erp-crm.customer" → 跨插件查询
|
||||
self.cross_plugin_entities.get(&entity)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("跨插件实体 '{}' 未注册", entity))?
|
||||
} else {
|
||||
DynamicTableManager::table_name(&self.plugin_id, &entity)
|
||||
};
|
||||
```
|
||||
|
||||
### 2.4 engine.rs — 构建跨插件映射
|
||||
|
||||
文件:`crates/erp-plugin/src/engine.rs` (~line 473)
|
||||
|
||||
`execute_wasm` 创建 `HostState` 后,从 manifest 的所有 `ref_plugin` 字段解析跨插件实体映射:
|
||||
|
||||
```rust
|
||||
// 从 manifest 提取所有 ref_plugin + ref_entity 组合
|
||||
// 查 plugin_entities 表获取实际 table_name
|
||||
// 填入 HostState.cross_plugin_entities
|
||||
```
|
||||
|
||||
### 2.5 集成测试
|
||||
|
||||
- 同插件 ref_entity → 行为不变(回归)
|
||||
- 跨插件 ref_plugin + 目标插件活跃 → 校验通过/拒绝
|
||||
- 跨插件 ref_plugin + 目标插件未安装 → 跳过校验,不报错
|
||||
- host.rs db_query 点分记号 → 正确路由到目标插件表
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: API 端点
|
||||
|
||||
> 新增 3 个端点支撑前端跨插件功能。
|
||||
|
||||
### 3.1 批量标签解析(核心)
|
||||
|
||||
文件:`crates/erp-plugin/src/handler/data_handler.rs`
|
||||
|
||||
```
|
||||
POST /api/v1/plugins/{plugin_id}/{entity}/resolve-labels
|
||||
```
|
||||
|
||||
请求:
|
||||
```json
|
||||
{ "fields": { "customer_id": ["uuid1", "uuid2"] } }
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"customer_id": { "uuid1": "张三", "uuid2": "李四" },
|
||||
"_meta": {
|
||||
"customer_id": {
|
||||
"target_plugin": "erp-crm",
|
||||
"target_entity": "customer",
|
||||
"label_field": "name",
|
||||
"plugin_installed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
逻辑:
|
||||
1. 从 entity schema 读取每个 field 的 `ref_plugin` / `ref_entity` / `ref_label_field`
|
||||
2. 对每个 field,解析目标表名(同 Phase 2 逻辑)
|
||||
3. `SELECT id, data->>'label_field' as label FROM target_table WHERE id = ANY($1) AND tenant_id = $2`
|
||||
4. 目标插件未安装 → 返回 `{ uuid: null }` + `plugin_installed: false`
|
||||
|
||||
### 3.2 实体注册表查询
|
||||
|
||||
文件:`crates/erp-plugin/src/handler/data_handler.rs`
|
||||
|
||||
```
|
||||
GET /api/v1/plugin-registry/entities?is_public=true
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "manifest_id": "erp-crm", "entity_name": "customer", "display_name": "客户", "label_fields": ["name"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
从 `plugin_entities` 查询 `is_public = true AND deleted_at IS NULL`,关联 plugin 状态。
|
||||
|
||||
### 3.3 悬空引用扫描
|
||||
|
||||
文件:`crates/erp-plugin/src/handler/data_handler.rs`
|
||||
|
||||
```
|
||||
POST /api/v1/plugins/{plugin_id}/scan-dangling-refs
|
||||
```
|
||||
|
||||
异步触发扫描,返回扫描结果。详见 Phase 5。
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 前端改造
|
||||
|
||||
### 4.1 扩展 TypeScript 类型
|
||||
|
||||
文件:`apps/web/src/api/plugins.ts`
|
||||
|
||||
```typescript
|
||||
// PluginFieldSchema 新增
|
||||
ref_plugin?: string;
|
||||
ref_fallback_label?: string;
|
||||
|
||||
// PluginEntitySchema 新增
|
||||
is_public?: boolean;
|
||||
```
|
||||
|
||||
文件:`apps/web/src/api/pluginData.ts` 新增:
|
||||
|
||||
```typescript
|
||||
resolveRefLabels(pluginId, entity, fields): Promise<ResolveLabelsResult>
|
||||
getPluginEntityRegistry(params?): Promise<RegistryEntity[]>
|
||||
scanDanglingRefs(pluginId): Promise<ScanResult>
|
||||
```
|
||||
|
||||
### 4.2 EntitySelect 跨插件支持
|
||||
|
||||
文件:`apps/web/src/components/EntitySelect.tsx`
|
||||
|
||||
新增 props:`refPlugin?: string`, `fallbackLabel?: string`
|
||||
|
||||
核心改动:
|
||||
- `refPlugin` 存在时 → 调用 `listPluginData(refPlugin, entity, ...)` 而非 `listPluginData(pluginId, entity, ...)`
|
||||
- 目标插件不可达(404) → 显示灰色禁用 Input + 警告图标 + fallbackLabel
|
||||
- 正常情况 → 保持现有 Select 行为
|
||||
|
||||
### 4.3 PluginCRUDPage 表格列标签解析
|
||||
|
||||
文件:`apps/web/src/pages/PluginCRUDPage.tsx`
|
||||
|
||||
**新增 hook:useResolveRefLabels**
|
||||
|
||||
数据加载后,收集所有 ref 字段的 UUID 值,调用 `resolveRefLabels` 批量获取标签。
|
||||
|
||||
**修改列渲染**(~line 263):
|
||||
|
||||
```typescript
|
||||
// ref 字段渲染逻辑
|
||||
if (f.ref_entity) {
|
||||
const label = resolvedLabels[f.name]?.[uuid];
|
||||
const installed = labelMeta[f.name]?.plugin_installed !== false;
|
||||
if (!installed) return <Tag color="default">{f.ref_fallback_label || '外部引用'}</Tag>;
|
||||
if (label === null) return <Tag color="warning">无效引用</Tag>;
|
||||
return <Tag color="blue">{label}</Tag>;
|
||||
}
|
||||
```
|
||||
|
||||
**修改 entity_select 表单渲染**(~line 341):
|
||||
|
||||
传 `refPlugin` 和 `fallbackLabel` 给 EntitySelect。
|
||||
|
||||
### 4.4 Detail Drawer 引用标签
|
||||
|
||||
在详情 Descriptions 中,对 ref 字段同样展示解析后的标签而非裸 UUID。
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 悬空引用对账
|
||||
|
||||
### 5.1 新增 reconciliation.rs
|
||||
|
||||
文件:`crates/erp-plugin/src/reconciliation.rs`
|
||||
|
||||
```rust
|
||||
pub struct DanglingRef {
|
||||
pub entity: String,
|
||||
pub field: String,
|
||||
pub record_id: Uuid,
|
||||
pub ref_value: String,
|
||||
pub reason: String, // "target_not_found" | "target_plugin_disabled"
|
||||
}
|
||||
|
||||
pub async fn scan_dangling_refs(
|
||||
manifest_id: &str, tenant_id: Uuid, db: &DatabaseConnection
|
||||
) -> Vec<DanglingRef>
|
||||
```
|
||||
|
||||
逻辑:遍历插件所有实体的 `ref_plugin` 字段,批量校验每个引用 UUID 是否存在于目标表。
|
||||
|
||||
### 5.2 数据库表
|
||||
|
||||
新迁移创建 `plugin_ref_scan_results` 表:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS plugin_ref_scan_results (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
plugin_id UUID NOT NULL REFERENCES plugins(id),
|
||||
status TEXT NOT NULL DEFAULT 'running',
|
||||
total_scanned INTEGER NOT NULL DEFAULT 0,
|
||||
dangling_count INTEGER NOT NULL DEFAULT 0,
|
||||
result_json JSONB NOT NULL DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
```
|
||||
|
||||
### 5.3 触发时机
|
||||
|
||||
- `service.rs::enable` 中:插件重新启用时,异步扫描依赖此插件的其他插件
|
||||
- 手动触发:管理员在 UI 点击 "扫描悬空引用"
|
||||
|
||||
### 5.4 前端
|
||||
|
||||
在 PluginAdmin 的插件详情 Drawer 中新增 "扫描引用" 按钮 + 扫描结果列表。
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: 验证
|
||||
|
||||
### 6.1 改造进销存插件
|
||||
|
||||
文件:`crates/erp-plugin-inventory/plugin.toml`
|
||||
|
||||
- `sales_order.customer_id` 增加 `ref_plugin = "erp-crm"`, `ref_entity = "customer"`, `ref_label_field = "name"`, `ref_fallback_label = "CRM 客户"`
|
||||
- `metadata.dependencies` 添加 `"erp-crm"`
|
||||
|
||||
### 6.2 改造 CRM 插件
|
||||
|
||||
文件:`crates/erp-plugin-crm/plugin.toml`
|
||||
|
||||
- `customer` 实体增加 `is_public = true`
|
||||
|
||||
### 6.3 端到端验证矩阵
|
||||
|
||||
| 场景 | 预期 |
|
||||
|------|------|
|
||||
| CRM 已安装 → 进销存创建订单选择客户 | EntitySelect 下拉显示 CRM 客户列表 |
|
||||
| CRM 未安装 → 进销存创建订单 | customer_id 字段降级为灰色文本输入 |
|
||||
| CRM 已安装 → 订单列表显示客户名 | 表格列显示蓝色 Tag "张三" |
|
||||
| CRM 卸载 → 重新安装 → 扫描悬空引用 | 对账报告显示悬空记录 |
|
||||
| 财务插件独立安装(无 CRM) | 所有功能正常,客户字段降级 |
|
||||
|
||||
---
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] `cargo check` 全 workspace 通过
|
||||
- [ ] `cargo test --workspace` 全部通过
|
||||
- [ ] 数据库迁移正/反向执行
|
||||
- [ ] 现有插件(CRM/进销存)功能不受影响
|
||||
- [ ] 新增端点通过 API 测试
|
||||
- [ ] 前端 `pnpm build` 通过
|
||||
- [ ] 浏览器端到端操作验证
|
||||
@@ -1,667 +0,0 @@
|
||||
# CRM WASM 插件架构审查报告
|
||||
|
||||
> 审查人:后端架构师
|
||||
> 日期:2026-04-15
|
||||
> 审查对象:CRM 客户管理 WASM 插件设计方案
|
||||
|
||||
---
|
||||
|
||||
## 0. 审查前提:当前系统实际状态
|
||||
|
||||
经过对代码库的详细审查,我确认以下事实作为审查基础:
|
||||
|
||||
- **WIT 接口** (`crates/erp-plugin/wit/plugin.wit`):9 个 Host API 函数(db-insert/db-query/db-update/db-delete/event-publish/config-get/log-write/current-user/check-permission)
|
||||
- **动态表结构** (`crates/erp-plugin/src/dynamic_table.rs`):每张表只有 `id`, `tenant_id`, `data`(JSONB), `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted_at`, `version` 这 9 列。所有业务字段存储在 `data` JSONB 列中。
|
||||
- **索引策略**:只为 `required` 和 `unique` 字段创建 GIN 索引(`data->>'field_name'`),没有复合索引能力。
|
||||
- **查询能力** (`data_service.rs`):`list()` 方法只支持按 tenant_id 过滤 + 分页,**不支持任何业务字段过滤**。`PluginDataListParams` 只有 `page`, `page_size`, `search` 三个参数。
|
||||
- **前端 CRUD** (`PluginCRUDPage.tsx`):纯粹的表单驱动,无过滤、无关联实体展示、无自定义操作。
|
||||
- **设计规格文档** (`docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md`) 附录 D 中规划了 `db-aggregate` 接口,但**实际 WIT 文件中并未实现**。
|
||||
- **设计规格文档**规划了 `render-page` / `handle-action` 插件导出函数,但**实际 WIT 中也没有**。
|
||||
|
||||
**结论:当前系统是一个 MVP 级别的插件原型,距离支撑 CRM 这种多实体、多关系的业务插件还有显著差距。**
|
||||
|
||||
---
|
||||
|
||||
## 1. JSONB 动态表能否支撑 CRM 数据模型?
|
||||
|
||||
### 1.1 结论:基本可行,但需要大幅增强查询能力
|
||||
|
||||
JSONB 存储本身不是问题。PostgreSQL 的 JSONB 在索引支持下,单字段等值查询和范围查询性能完全可以接受(毫秒级)。关键问题在于当前系统的**查询能力几乎为零**。
|
||||
|
||||
### 1.2 关系查询的实现路径
|
||||
|
||||
对于 CRM 中"A 的所有子客户"这类关系查询:
|
||||
|
||||
**方案 A:利用 JSONB 索引的等值过滤(推荐)**
|
||||
|
||||
`parent_id` 存储在 `data` JSONB 中。当前系统已有为 `required` 字段创建索引的逻辑:
|
||||
|
||||
```sql
|
||||
-- 动态表已有的索引模式
|
||||
CREATE INDEX idx_xxx_parent_id ON plugin_crm_customer (data->>'parent_id') WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
查询"A 的直接子节点"只需:
|
||||
|
||||
```sql
|
||||
SELECT * FROM plugin_crm_customer
|
||||
WHERE tenant_id = $1 AND data->>'parent_id' = $2 AND deleted_at IS NULL;
|
||||
```
|
||||
|
||||
这在有索引的情况下性能完全没问题,10 万条记录也能在 5ms 内返回。
|
||||
|
||||
**方案 B:递归查询(查询所有子孙节点)需要 db-query 增强**
|
||||
|
||||
当前的 `db_query` Host API 是预填充模式——Host 在执行 WASM 前就把查询结果塞进 `query_results` HashMap。WASM 内部只是读缓存。这意味着**插件无法控制 SQL 的构建过程**。
|
||||
|
||||
递归查询需要 PostgreSQL 的 `WITH RECURSIVE` CTE:
|
||||
|
||||
```sql
|
||||
WITH RECURSIVE descendants AS (
|
||||
SELECT id, data, 0 as depth FROM plugin_crm_customer
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
UNION ALL
|
||||
SELECT c.id, c.data, d.depth + 1 FROM plugin_crm_customer c
|
||||
JOIN descendants d ON c.data->>'parent_id' = d.id::text
|
||||
WHERE c.tenant_id = $2 AND c.deleted_at IS NULL AND d.depth < 10
|
||||
)
|
||||
SELECT * FROM descendants ORDER BY depth;
|
||||
```
|
||||
|
||||
**当前系统无法执行这种查询。** 需要 Host API 层面支持。
|
||||
|
||||
### 1.3 具体建议
|
||||
|
||||
1. **为外键字段显式声明索引**:在 `PluginField` 中新增 `indexed: bool` 属性,或者引入 `references` 属性自动创建索引。CRM 中 `contact.customer_id`、`communication.customer_id`、`customer_tag.customer_id`、`customer_relationship.from_customer_id`、`customer_relationship.to_customer_id` 这些外键字段必须有索引。
|
||||
|
||||
2. **对 unique 约束的处理需要修正**:当前 `customer.code` 声明为 unique,但索引只过滤了 `deleted_at IS NULL`。如果软删除后重建同名 code,唯一索引会冲突。需要在索引中加入排除已删除记录的逻辑,或使用 partial unique index:
|
||||
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_xxx_code_uniq ON plugin_crm_customer (data->>'code') WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
当前代码已经使用了 `WHERE deleted_at IS NULL`,这一点做得正确。但要确认 `sanitize_identifier` 生成的索引名不会因为表名过长而截断。
|
||||
|
||||
---
|
||||
|
||||
## 2. Host API 限制:跨实体查询
|
||||
|
||||
### 2.1 问题诊断
|
||||
|
||||
这是 CRM 插件面临的最严重的架构缺陷。CRM 的核心场景全部涉及跨实体:
|
||||
|
||||
- **客户列表 + 联系人数量**:需要 JOIN 或子查询
|
||||
- **客户详情页展示联系人列表**:需要按 `customer_id` 过滤 contact
|
||||
- **沟通记录按客户筛选**:需要按 `customer_id` 过滤 communication
|
||||
- **标签筛选客户**:需要按 `tag_name` 在 customer_tag 中查,再反查 customer
|
||||
|
||||
当前 `db_query` 的实现(`host.rs` 第 99-109 行)只是从预填充的 HashMap 中取缓存:
|
||||
|
||||
```rust
|
||||
fn db_query(&mut self, entity: String, _filter: Vec<u8>, _pagination: Vec<u8>) -> Result<Vec<u8>, String> {
|
||||
self.query_results.get(&entity).cloned()
|
||||
.ok_or_else(|| format!("实体 '{}' 的查询结果未预填充", entity))
|
||||
}
|
||||
```
|
||||
|
||||
`_filter` 和 `_pagination` 参数被完全忽略了。
|
||||
|
||||
### 2.2 解决方案:引入结构化查询 API
|
||||
|
||||
不是加 JOIN(那会让 Host API 变成 SQL 构建器),而是引入**两级查询增强**:
|
||||
|
||||
**第一级(必须实现):单实体过滤查询**
|
||||
|
||||
在 Host API 中新增 `db-query-filtered`,或在现有 `db_query` 中实际处理 filter 参数:
|
||||
|
||||
```wit
|
||||
/// 结构化过滤查询
|
||||
db-query: func(entity: string, filter: list<u8>, pagination: list<u8>) -> result<list<u8>, string>;
|
||||
```
|
||||
|
||||
filter 的 JSON 结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"conditions": [
|
||||
{"field": "customer_id", "op": "eq", "value": "uuid-here"},
|
||||
{"field": "status", "op": "in", "value": ["active", "vip"]},
|
||||
{"field": "occurred_at", "op": "gte", "value": "2026-01-01"}
|
||||
],
|
||||
"order_by": [{"field": "created_at", "dir": "desc"}],
|
||||
"search": "关键词"
|
||||
}
|
||||
```
|
||||
|
||||
Host 层将其安全转换为参数化 SQL:
|
||||
|
||||
```sql
|
||||
SELECT id, data, created_at, updated_at, version
|
||||
FROM plugin_crm_contact
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
AND data->>'customer_id' = $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
```
|
||||
|
||||
这样 CRM 插件就能实现"查某个客户的所有联系人"、"查某个客户的所有沟通记录"。
|
||||
|
||||
**第二级(必须实现):聚合查询**
|
||||
|
||||
设计规格中规划了 `db-aggregate` 但未实现。CRM 需要它来显示统计信息:
|
||||
|
||||
```wit
|
||||
db-aggregate: func(entity: string, query: list<u8>) -> result<list<u8>, string>;
|
||||
```
|
||||
|
||||
query 结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"group_by": ["data->>'status'"],
|
||||
"aggregates": [
|
||||
{"alias": "count", "func": "count", "field": "id"},
|
||||
{"alias": "total_value", "func": "sum", "field": "data->>'amount'"}
|
||||
],
|
||||
"filter": {"field": "data->>'level'", "op": "eq", "value": "vip"}
|
||||
}
|
||||
```
|
||||
|
||||
**第三级(推荐实现):跨实体关联查询**
|
||||
|
||||
不要实现通用 JOIN(太复杂且安全风险高),而是实现一种受限的"关联加载"模式:
|
||||
|
||||
```wit
|
||||
/// 按外键值批量查询(一次加载多个关联记录)
|
||||
db-query-batch: func(entity: string, field: string, ids: list<string>) -> result<list<u8>, string>;
|
||||
```
|
||||
|
||||
这解决了 CRM 中最常见的 N+1 查询问题。例如加载 20 个客户后,一次性查出所有关联的联系人:
|
||||
|
||||
```json
|
||||
// 请求:查 contact 表中 customer_id 在 [id1, id2, ...] 中的记录
|
||||
{
|
||||
"entity": "contact",
|
||||
"field": "customer_id",
|
||||
"ids": ["uuid-1", "uuid-2", "..."]
|
||||
}
|
||||
```
|
||||
|
||||
Host 生成:
|
||||
|
||||
```sql
|
||||
SELECT id, data, created_at, updated_at, version
|
||||
FROM plugin_crm_contact
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
AND data->>'customer_id' = ANY($2::text[])
|
||||
```
|
||||
|
||||
### 2.3 关于"多次 API 调用"的问题
|
||||
|
||||
在 WASM 插件模型下,**每次 API 调用都是在同一个 Store 内完成的**,不涉及网络开销。多次 `db_query` 调用的成本是:
|
||||
|
||||
- WASM 到 Host 的函数调用(纳秒级)
|
||||
- Host 从预填充缓存中读取(纳秒级)
|
||||
|
||||
所以即使需要多次调用,性能也不是问题。**真正的问题是当前预填充机制不支持按业务字段过滤**。只要实现了第一级的结构化过滤,CRM 插件的跨实体查询就可以通过以下模式实现:
|
||||
|
||||
```
|
||||
// 插件内伪代码
|
||||
let contacts = db_query("contact", {"field": "customer_id", "op": "eq", "value": customer_id}, pagination);
|
||||
let communications = db_query("communication", {"field": "customer_id", "op": "eq", "value": customer_id}, pagination);
|
||||
```
|
||||
|
||||
前端也可以直接通过 REST API 调用:
|
||||
|
||||
```
|
||||
GET /api/v1/plugins/crm/contact?filter={"field":"customer_id","op":"eq","value":"uuid"}&page=1&page_size=20
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 关系图谱的可行性
|
||||
|
||||
### 3.1 结论:可行但有数据量上限
|
||||
|
||||
CRM 关系图谱的核心查询是:给定一个客户,找出它的所有直接关系(parent_child/partner/supplier 等)。
|
||||
|
||||
**查询模式**:
|
||||
|
||||
```sql
|
||||
-- 查询 A 的所有关系
|
||||
SELECT * FROM plugin_crm_customer_relationship
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
AND (data->>'from_customer_id' = $2 OR data->>'to_customer_id' = $2);
|
||||
```
|
||||
|
||||
这个查询在索引支持下完全可行。需要为 `from_customer_id` 和 `to_customer_id` 都创建索引。
|
||||
|
||||
**前端加载策略**:
|
||||
|
||||
"加载全量关系+客户来渲染图谱"这个方案在以下条件下可行:
|
||||
- 单租户客户量 < 5000
|
||||
- 关系记录 < 10000
|
||||
|
||||
超出这个量级,需要改为"按中心节点展开"的模式:
|
||||
1. 先加载中心客户的直接关系(1 跳)
|
||||
2. 用户点击某个关联客户时,加载该客户的直接关系(2 跳)
|
||||
3. 前端用力导向图逐步渲染
|
||||
|
||||
### 3.2 建议
|
||||
|
||||
1. **不要设计成全量加载**。manifest 页面类型 `graph` 应支持配置 `depth` 参数(默认 1 跳,最大 3 跳),以及 `center_entity_id` 参数。
|
||||
|
||||
2. **后端提供"关系网络"专用查询**。不是新增 Host API,而是在 data_handler 层增加一个通用的关系查询端点:
|
||||
|
||||
```
|
||||
GET /api/v1/plugins/{plugin_id}/{entity}/graph?center_id=uuid&depth=2&relationship_types=parent_child,partner
|
||||
```
|
||||
|
||||
这可以由基座的前端通用组件调用,不需要插件 WASM 参与。
|
||||
|
||||
3. **前端 graph 组件选型**:推荐 Ant Design 的 `@ant-design/graph` 或 G6。图谱渲染是纯前端能力,不需要 WASM 介入。
|
||||
|
||||
---
|
||||
|
||||
## 4. parent_id 层级(递归查询)
|
||||
|
||||
### 4.1 问题分析
|
||||
|
||||
"查询某客户的所有子孙节点"在当前架构下确实是个难题。三种场景需要递归:
|
||||
|
||||
1. **组织架构式展示**:树形组件显示客户层级
|
||||
2. **汇总统计**:某集团下所有子公司的总交易额
|
||||
3. **权限继承**:父客户的负责人可以看到子客户
|
||||
|
||||
### 4.2 解决方案
|
||||
|
||||
**方案 A:Host 端提供递归查询 API(推荐)**
|
||||
|
||||
新增 Host API:
|
||||
|
||||
```wit
|
||||
/// 递归查询树形结构的所有子孙节点
|
||||
db-query-tree: func(entity: string, parent-field: string, root-id: string, max-depth: s32) -> result<list<u8>, string>;
|
||||
```
|
||||
|
||||
Host 层使用 `WITH RECURSIVE` 实现:
|
||||
|
||||
```sql
|
||||
WITH RECURSIVE tree AS (
|
||||
-- 锚点:根节点
|
||||
SELECT id, data, 0 as depth
|
||||
FROM plugin_crm_customer
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
UNION ALL
|
||||
-- 递归:子节点
|
||||
SELECT c.id, c.data, t.depth + 1
|
||||
FROM plugin_crm_customer c
|
||||
JOIN tree t ON c.data->>'parent_id' = t.id::text
|
||||
WHERE c.tenant_id = $2 AND c.deleted_at IS NULL AND t.depth < $3
|
||||
)
|
||||
SELECT id, data, depth FROM tree ORDER BY depth;
|
||||
```
|
||||
|
||||
**方案 B:物化路径(denormalization)**
|
||||
|
||||
在 customer 的 data 中存储物化路径 `path: "root_id/parent_id/self_id"`,然后可以用前缀匹配:
|
||||
|
||||
```sql
|
||||
SELECT * FROM plugin_crm_customer
|
||||
WHERE data->>'path' LIKE 'root_id/%' AND tenant_id = $1 AND deleted_at IS NULL;
|
||||
```
|
||||
|
||||
这需要 GIN 索引支持 `LIKE` 前缀查询(`pg_trgm` 扩展或 `text_pattern_ops`)。
|
||||
|
||||
**推荐方案 A**。物化路径有数据一致性问题(移动节点时需要更新所有子节点的 path),而递归 CTE 是 PostgreSQL 原生支持的,一次查询搞定。`max_depth` 参数防止无限递归。
|
||||
|
||||
### 4.3 对 tree 页面类型的建议
|
||||
|
||||
manifest 中 `type: tree` 的页面需要额外的配置:
|
||||
|
||||
```toml
|
||||
[[ui.pages]]
|
||||
route = "/crm/customer-tree"
|
||||
entity = "customer"
|
||||
type = "tree"
|
||||
tree_config = { parent_field = "parent_id", label_field = "name", icon_field = "customer_type", max_depth = 5 }
|
||||
```
|
||||
|
||||
基座前端根据 `tree_config` 渲染 Ant Design Tree 组件,使用 `db-query-tree` API 加载数据。
|
||||
|
||||
---
|
||||
|
||||
## 5. 缺失的关键能力
|
||||
|
||||
### 5.1 必须新增的 Host API
|
||||
|
||||
按优先级排序:
|
||||
|
||||
| 优先级 | API | 原因 |
|
||||
|--------|-----|------|
|
||||
| P0 | **db-query-filtered**(结构化过滤) | 没有它 CRM 插件完全无法工作。当前 db_query 的 filter 参数被忽略。 |
|
||||
| P0 | **db-aggregate**(聚合查询) | CRM 仪表盘需要"按状态统计客户数"、"按级别统计客户数"等。设计规格中已规划但未实现。 |
|
||||
| P0 | **db-query-by-id**(按 ID 查单条) | 当前 `db_query` 不支持按 ID 查询。虽然 REST API 有 GET by ID,但 WASM 内部没有。 |
|
||||
| P1 | **db-query-batch**(批量外键查询) | 解决 N+1 问题,关联实体加载。 |
|
||||
| P1 | **db-query-tree**(递归树查询) | 支持客户层级树、组织架构等树形结构。 |
|
||||
| P1 | **db-batch-insert**(批量插入) | 导入客户数据、批量创建联系人。 |
|
||||
| P2 | **db-exists**(存在性检查) | 检查 code 是否已存在,比 count 更高效。 |
|
||||
| P2 | **db-count**(计数) | 单独的计数查询,不返回数据。 |
|
||||
|
||||
### 5.2 必须增强的基座能力
|
||||
|
||||
| 能力 | 现状 | 需要 |
|
||||
|------|------|------|
|
||||
| **REST API 过滤** | `PluginDataListParams` 只有 `page/page_size/search` | 需要支持 `filter` 查询参数,支持 JSONB 字段的等值/范围/IN 查询 |
|
||||
| **REST API 排序** | 固定 `ORDER BY created_at DESC` | 需要支持 `order_by` 参数 |
|
||||
| **REST API 关联加载** | 无 | 需要支持 `?include=contact,communication` 参数,一次返回客户+联系人+沟通记录 |
|
||||
| **Schema 唯一约束校验** | 只创建了索引,没有插入前校验 | 需要在 `PluginDataService::create` 中检查 unique 字段 |
|
||||
| **实体间引用完整性** | 无 | 删除客户时应提示有关联的联系人/沟通记录。至少提供 `GET /plugins/{id}/{entity}/{record_id}/references` 端点 |
|
||||
|
||||
### 5.3 WASM 插件端需要的能力
|
||||
|
||||
设计规格中规划了 `render-page` 和 `handle-action`,但 WIT 中未实现。对于 CRM 插件,以下场景需要这两个函数:
|
||||
|
||||
- **客户 360 度视图**:在同一个页面展示客户基本信息 + 联系人列表 + 最近沟通记录 + 标签 + 关系图谱。这不是一个简单的 CRUD 页面,需要 WASM 返回复合 UI 指令。
|
||||
- **自定义操作**:"转为正式客户"、"合并客户"、"批量分配"等操作需要 WASM 处理业务逻辑。
|
||||
|
||||
建议在 WIT 中新增:
|
||||
|
||||
```wit
|
||||
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>;
|
||||
// 新增
|
||||
render-page: func(page-path: string, params: list<u8>) -> result<list<u8>, string>;
|
||||
handle-action: func(page-path: string, action: string, data: list<u8>) -> result<list<u8>, string>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Manifest ui.pages 设计审查
|
||||
|
||||
### 6.1 当前类型覆盖度评估
|
||||
|
||||
| 类型 | CRM 场景 | 是否覆盖 | 说明 |
|
||||
|------|---------|---------|------|
|
||||
| `crud` | 客户列表、联系人列表、沟通记录列表 | 覆盖 | 基本够用 |
|
||||
| `tree` | 客户层级树 | 需增强 | 需要增加 `tree_config` 配置 |
|
||||
| `graph` | 关系图谱 | 需增强 | 需要增加 `graph_config`(relationship_entity, center_id, depth) |
|
||||
| `timeline` | 沟通记录时间线 | 新增 | Ant Design Timeline 组件天然支持 |
|
||||
| `tabs` | 客户 360 度视图 | 需增强 | 需要支持嵌套子页面 |
|
||||
| `dashboard` | CRM 首页统计 | 缺失 | 需要 `stat_cards` + `charts` 配置 |
|
||||
| `kanban` | 客户跟进阶段看板 | 缺失 | 按客户等级/跟进状态分列 |
|
||||
| `detail` | 客户详情页 | 缺失 | 不是 CRUD,是复合视图 |
|
||||
|
||||
### 6.2 建议的页面类型扩展
|
||||
|
||||
```typescript
|
||||
interface PluginPage {
|
||||
route: string;
|
||||
entity: string;
|
||||
display_name: string;
|
||||
icon?: string;
|
||||
menu_group?: string;
|
||||
|
||||
// 页面类型扩展
|
||||
type: "crud" | "tree" | "graph" | "timeline" | "tabs" | "dashboard" | "detail" | "custom";
|
||||
|
||||
// 类型专属配置
|
||||
crud_config?: CrudConfig; // 列/过滤/排序/表单/操作
|
||||
tree_config?: TreeConfig; // 父节点字段/标签字段/最大深度
|
||||
graph_config?: GraphConfig; // 关系实体/关系类型/默认深度
|
||||
dashboard_config?: DashboardConfig; // 统计卡片/图表
|
||||
detail_config?: DetailConfig; // 关联实体/布局
|
||||
tabs_config?: TabsConfig; // 子标签页列表
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 对 CRM 最关键的缺失:detail 页面类型
|
||||
|
||||
CRM 最核心的交互不是列表 CRUD,而是**客户 360 度详情页**:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ 客户名称:某某集团 [编辑] [更多 ▾] │
|
||||
│ 类型:企业 | 行业:制造业 | 级别:VIP │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ [基本信息] [联系人] [沟通记录] [关系图谱] [标签] │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ 联系人列表(联系人的 CRUD,自动过滤当前客户) │
|
||||
│ + 新增联系人 │
|
||||
│ ┌─────┬──────┬──────┬───────┬────┐ │
|
||||
│ │姓名 │职位 │电话 │邮箱 │操作│ │
|
||||
│ └─────┴──────┴──────┴───────┴────┘ │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
这种页面需要:
|
||||
1. 顶部展示主实体详情
|
||||
2. 底部 tabs 展示关联实体列表
|
||||
3. 关联实体自动按主实体 ID 过滤
|
||||
|
||||
建议 `detail` 类型配置:
|
||||
|
||||
```toml
|
||||
[[ui.pages]]
|
||||
route = "/crm/customer/:id"
|
||||
entity = "customer"
|
||||
type = "detail"
|
||||
display_name = "客户详情"
|
||||
|
||||
[ui.pages.detail_config.header]
|
||||
title_field = "name"
|
||||
subtitles = [
|
||||
{ field = "customer_type", label = "类型" },
|
||||
{ field = "industry", label = "行业" },
|
||||
{ field = "level", label = "级别", tag_colors = {vip = "gold", svip = "red"} }
|
||||
]
|
||||
actions = ["edit", "delete", { label = "转为正式客户", action = "activate", permission = "crm.customer.update" }]
|
||||
|
||||
[[ui.pages.detail_config.tabs]]
|
||||
label = "联系人"
|
||||
entity = "contact"
|
||||
filter_field = "customer_id"
|
||||
type = "crud"
|
||||
|
||||
[[ui.pages.detail_config.tabs]]
|
||||
label = "沟通记录"
|
||||
entity = "communication"
|
||||
filter_field = "customer_id"
|
||||
type = "timeline"
|
||||
|
||||
[[ui.pages.detail_config.tabs]]
|
||||
label = "关系图谱"
|
||||
entity = "customer_relationship"
|
||||
type = "graph"
|
||||
graph_config = { center_from = "from_customer_id", center_to = "to_customer_id" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 权限模型审查
|
||||
|
||||
### 7.1 当前方案的 8 个权限码
|
||||
|
||||
根据 CRM 插件常见需求,推测 8 个权限码为:
|
||||
- `crm.customer.list` / `crm.customer.create` / `crm.customer.update` / `crm.customer.delete`
|
||||
- `crm.contact.list` / `crm.contact.create` / `crm.contact.update` / `crm.contact.delete`
|
||||
|
||||
### 7.2 不足之处
|
||||
|
||||
**问题 1:缺少沟通记录和关系图谱的权限**
|
||||
|
||||
沟通记录(communication)和关系图谱(customer_relationship)是独立实体,应该有独立的权限码。
|
||||
|
||||
**问题 2:缺少数据范围权限(data scope)**
|
||||
|
||||
CRM 中最常见的权限需求不是"能不能看联系人",而是"能看到哪些客户的联系人"。典型的数据范围:
|
||||
|
||||
- **本人**:只看自己创建的客户
|
||||
- **本部门**:看本部门所有人创建的客户
|
||||
- **本部门及下级**:看本部门及子部门创建的客户
|
||||
- **全部**:看所有客户
|
||||
|
||||
当前的 `check_permission` 只支持布尔型权限检查(有没有这个权限),不支持数据范围过滤。
|
||||
|
||||
**问题 3:级联权限问题**
|
||||
|
||||
"联系人/沟通记录的访问是否应该依赖客户级别的权限?"——**是的,应该依赖**。如果用户看不到某个客户,就不应该看到该客户下的联系人和沟通记录。
|
||||
|
||||
### 7.3 建议的权限模型
|
||||
|
||||
**权限码(16 个)**:
|
||||
|
||||
```
|
||||
crm.customer.list # 查看客户列表
|
||||
crm.customer.create # 创建客户
|
||||
crm.customer.update # 编辑客户
|
||||
crm.customer.delete # 删除客户
|
||||
crm.customer.export # 导出客户数据
|
||||
|
||||
crm.contact.list # 查看联系人
|
||||
crm.contact.create # 创建联系人
|
||||
crm.contact.update # 编辑联系人
|
||||
crm.contact.delete # 删除联系人
|
||||
|
||||
crm.communication.list # 查看沟通记录
|
||||
crm.communication.create # 创建沟通记录
|
||||
crm.communication.update # 编辑沟通记录
|
||||
crm.communication.delete # 删除沟通记录
|
||||
|
||||
crm.relationship.view # 查看关系图谱
|
||||
crm.tag.manage # 管理标签分类
|
||||
|
||||
crm.customer.all # 数据范围:查看所有客户(默认只看自己的)
|
||||
```
|
||||
|
||||
**数据范围过滤的实现**:
|
||||
|
||||
这不是插件自身能解决的,需要基座支持。建议在 `current_user` API 返回的数据中包含数据范围信息,或者在 Host API 层增加数据范围过滤:
|
||||
|
||||
```wit
|
||||
/// 扩展 current_user 返回值
|
||||
current-user: func() -> result<list<u8>, string>;
|
||||
// 返回结构增加 data_scope 字段
|
||||
// { "id": "...", "tenant_id": "...", "data_scope": "all" | "department" | "department_tree" | "self" }
|
||||
```
|
||||
|
||||
然后在 `db_query` 的过滤逻辑中自动注入 `created_by` 过滤。
|
||||
|
||||
**关于级联权限**:建议在 REST API 层处理。当查询联系人时,如果用户没有 `crm.customer.all`,则联系人查询自动 JOIN customer 表检查 `created_by`:
|
||||
|
||||
```sql
|
||||
SELECT c.* FROM plugin_crm_contact c
|
||||
JOIN plugin_crm_customer cu ON c.data->>'customer_id' = cu.id::text
|
||||
WHERE c.tenant_id = $1 AND c.deleted_at IS NULL
|
||||
AND (cu.data->>'created_by' = $2 OR cu.data->>'level' IN ('vip', 'svip'))
|
||||
-- VIP 客户所有人可见
|
||||
```
|
||||
|
||||
这种过滤逻辑复杂且与业务强相关,建议在 WASM 插件的 `handle_action` 中实现,或者作为基座的通用数据范围机制。
|
||||
|
||||
---
|
||||
|
||||
## 8. 综合评估与实施建议
|
||||
|
||||
### 8.1 可行性评估
|
||||
|
||||
| 维度 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| JSONB 数据存储 | 8/10 | PostgreSQL JSONB 足够支撑,索引机制需增强 |
|
||||
| 单实体 CRUD | 6/10 | 基本可用,但查询/排序/过滤能力严重不足 |
|
||||
| 跨实体关联 | 3/10 | 当前完全不支持,是最需要补强的部分 |
|
||||
| 树形层级 | 3/10 | 需要递归查询支持 |
|
||||
| 关系图谱 | 5/10 | 存储可行,查询/渲染需要增强 |
|
||||
| 权限模型 | 5/10 | 权限码够用,数据范围权限缺失 |
|
||||
| UI 配置驱动 | 4/10 | 缺少 detail/dashboard/kanban 类型 |
|
||||
|
||||
### 8.2 实施路径建议
|
||||
|
||||
**Phase 1:基座查询能力增强(前置条件,约 3-5 天)**
|
||||
|
||||
1. 实现 `db_query` 的 filter 参数解析和 SQL 构建
|
||||
2. 实现 `db-aggregate` Host API
|
||||
3. REST API 支持 filter / order_by 参数
|
||||
4. 为外键字段自动创建索引
|
||||
5. 实现 unique 约束的插入前校验
|
||||
|
||||
**Phase 2:CRM 核心功能(约 5-7 天)**
|
||||
|
||||
1. 创建 CRM 插件 crate(5 个实体)
|
||||
2. 实现客户 + 联系人 + 沟通记录的 CRUD
|
||||
3. 实现 `type: detail` 前端通用组件
|
||||
4. 实现客户详情页(tabs 嵌套关联实体)
|
||||
|
||||
**Phase 3:高级功能(约 3-5 天)**
|
||||
|
||||
1. 实现 `db-query-tree` 支持客户层级
|
||||
2. 实现关系图谱页面
|
||||
3. 实现数据范围权限过滤
|
||||
4. 实现 `render-page` / `handle-action` 支持自定义操作
|
||||
|
||||
### 8.3 风险提示
|
||||
|
||||
1. **最大的风险不是技术,是 Host API 的演进方向**。每增加一个 Host API 函数,就意味着所有已安装的插件都依赖这个接口。建议在 CRM 插件开发前先冻结 Host API v2(包含上述增强),然后所有插件基于 v2 开发。
|
||||
|
||||
2. **JSONB 动态表的性能天花板**。单表数据量超过 100 万条时,JSONB 的查询性能会显著下降(相比原生列)。如果 CRM 模块被高频使用,长期应考虑将高频实体(customer)升级为原生列存储。
|
||||
|
||||
3. **前端通用组件的复杂度**。detail + graph + tree + timeline 这四种页面类型,每种都需要基座提供通用实现。这不是 CRM 独有的需求,而是所有行业插件的共同需求。建议将这些组件沉淀为基座的 `PluginUI` 组件库,而不是 CRM 插件的一部分。
|
||||
|
||||
---
|
||||
|
||||
## 附录:Host API v2 建议的 WIT 定义
|
||||
|
||||
```wit
|
||||
package erp:plugin;
|
||||
|
||||
interface host-api {
|
||||
// === 基础 CRUD ===
|
||||
db-insert: func(entity: string, data: list<u8>) -> result<list<u8>, string>;
|
||||
db-get-by-id: func(entity: string, id: string) -> result<list<u8>, string>;
|
||||
db-query: func(entity: string, filter: list<u8>, pagination: list<u8>) -> result<list<u8>, string>;
|
||||
db-update: func(entity: string, id: string, data: list<u8>, version: s64) -> result<list<u8>, string>;
|
||||
db-delete: func(entity: string, id: string) -> result<_, string>;
|
||||
db-exists: func(entity: string, filter: list<u8>) -> result<bool, string>;
|
||||
db-count: func(entity: string, filter: list<u8>) -> result<s64, string>;
|
||||
|
||||
// === 批量操作 ===
|
||||
db-query-batch: func(entity: string, field: string, ids: list<string>) -> result<list<u8>, string>;
|
||||
db-batch-insert: func(entity: string, items: list<list<u8>>) -> result<list<list<u8>>, string>;
|
||||
|
||||
// === 高级查询 ===
|
||||
db-aggregate: func(entity: string, query: list<u8>) -> result<list<u8>, string>;
|
||||
db-query-tree: func(entity: string, parent-field: string, root-id: string, max-depth: s32) -> result<list<u8>, 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>;
|
||||
}
|
||||
|
||||
interface plugin-api {
|
||||
init: func() -> result<_, string>;
|
||||
on-tenant-created: func(tenant-id: string) -> result<_, string>;
|
||||
on-tenant-deleted: func(tenant-id: string) -> result<_, string>;
|
||||
handle-event: func(event-type: string, payload: list<u8>) -> result<_, string>;
|
||||
render-page: func(page-path: string, params: list<u8>) -> result<list<u8>, string>;
|
||||
handle-action: func(page-path: string, action: string, data: list<u8>) -> result<list<u8>, string>;
|
||||
}
|
||||
|
||||
world plugin-world {
|
||||
import host-api;
|
||||
export plugin-api;
|
||||
}
|
||||
```
|
||||
|
||||
共计 15 个 Host API 函数(vs 当前 9 个),6 个 Plugin API 函数(vs 当前 3 个)。
|
||||
@@ -1,561 +0,0 @@
|
||||
# CRM WASM 插件实现可行性审查报告
|
||||
|
||||
## 审查范围
|
||||
|
||||
基于对以下核心代码的完整审查:
|
||||
- `crates/erp-plugin/wit/plugin.wit` — WIT 接口定义
|
||||
- `crates/erp-plugin/src/host.rs` — Host API 实现
|
||||
- `crates/erp-plugin/src/engine.rs` — WASM 引擎(Fuel 限制、执行流程)
|
||||
- `crates/erp-plugin/src/manifest.rs` — 插件清单结构
|
||||
- `crates/erp-plugin/src/dynamic_table.rs` — 动态表管理(建表、CRUD SQL)
|
||||
- `crates/erp-plugin/src/data_service.rs` — 数据服务层
|
||||
- `crates/erp-plugin/src/data_handler.rs` — API handler(权限检查)
|
||||
- `crates/erp-plugin/src/service.rs` — 插件生命周期管理
|
||||
- `crates/erp-plugin/src/module.rs` — 路由注册
|
||||
- `apps/web/src/pages/PluginCRUDPage.tsx` — 前端通用 CRUD 页面
|
||||
- `apps/web/src/stores/plugin.ts` — 插件菜单 store
|
||||
- `crates/erp-server/migration/src/m20260417_000034_seed_plugin_permissions.rs` — 权限种子数据
|
||||
|
||||
---
|
||||
|
||||
## 1. WASM 插件逻辑复杂度
|
||||
|
||||
### init() — 需要做什么
|
||||
|
||||
CRM 插件的 `init()` 应当做以下事情:
|
||||
- 日志记录初始化信息
|
||||
- 可选:通过 `config_get` 读取 CRM 相关配置
|
||||
- 可选:通过 `db_insert` 创建默认字典数据(如客户类型、级别、来源等默认选项)
|
||||
|
||||
**复杂度:低。** 主要是 `log_write` + 若干 `db_insert` 插入初始配置数据。涉及的逻辑只是 JSON 构造和 Host API 调用。1000 万 Fuel 绰绰有余。
|
||||
|
||||
### on_tenant_created() — 需要做什么
|
||||
|
||||
为新建租户创建 CRM 默认数据:
|
||||
- 插入默认客户类型字典(潜在客户、意向客户、成交客户、流失客户)
|
||||
- 插入默认行业分类
|
||||
- 插入默认客户来源选项
|
||||
- 可选:创建默认客户级别(A/B/C/D)
|
||||
|
||||
**复杂度:低-中。** 纯数据初始化,大概需要 10-20 次 `db_insert` 调用。每次 `db_insert` 只是构造 JSON + 调用 Host API 入队 pending_ops,不涉及复杂计算。Fuel 消耗估算:每次 Host API 调用约 5000-10000 fuel,20 次调用约 20 万 fuel,远低于 1000 万限制。
|
||||
|
||||
### handle_event() — 需要做什么
|
||||
|
||||
取决于 CRM 订阅了哪些事件。典型场景:
|
||||
- 订阅 `user.created`:自动为客户经理创建关联记录
|
||||
- 订阅 `workflow.task.completed`:处理客户审批流程完成事件
|
||||
- 订阅自定义 CRM 事件:如 `crm.customer.created` 触发欢迎邮件模板创建
|
||||
|
||||
**复杂度:中。** 需要模式匹配 `event_type`,解析 JSON payload,可能需要 `db_query` 查询关联数据,然后 `db_insert`/`db_update` 写入结果。关键限制是 `db_query` 只能访问预填充的数据。
|
||||
|
||||
### Fuel 限制评估
|
||||
|
||||
当前默认 Fuel 为 1000 万(`engine.rs` 第 32 行)。对于 CRM 插件的三种场景:
|
||||
|
||||
| 场景 | 预估 Fuel | 是否足够 |
|
||||
|------|-----------|----------|
|
||||
| init() | 5-20 万 | 充裕 |
|
||||
| on_tenant_created() | 10-30 万 | 充裕 |
|
||||
| handle_event() | 5-50 万 | 充裕 |
|
||||
|
||||
**结论:Fuel 限制不是瓶颈。** 1000 万 fuel 对 CRM 的业务逻辑(JSON 解析 + 若干 Host API 调用)绰绰有余。真正的复杂业务逻辑(报表聚合、批量操作)应放在 Host 端而不是 WASM 内部。
|
||||
|
||||
---
|
||||
|
||||
## 2. 动态表 Schema 声明 — Select 类型
|
||||
|
||||
### 当前支持
|
||||
|
||||
`manifest.rs` 第 66-76 行定义了 `PluginFieldType` 枚举:
|
||||
```rust
|
||||
pub enum PluginFieldType {
|
||||
String, Integer, Float, Boolean, Date, DateTime, Json, Uuid, Decimal,
|
||||
}
|
||||
```
|
||||
|
||||
没有 `Select` 或 `Enum` 类型。
|
||||
|
||||
### 解决方案:不需要扩展 manifest schema
|
||||
|
||||
当前 `PluginField` 已经有两个关键字段(`manifest.rs` 第 50-61 行):
|
||||
|
||||
```rust
|
||||
pub struct PluginField {
|
||||
pub name: String,
|
||||
pub field_type: PluginFieldType,
|
||||
pub ui_widget: Option<String>, // <-- 关键:可指定前端渲染组件
|
||||
pub options: Option<Vec<serde_json::Value>>, // <-- 关键:下拉选项列表
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`ui_widget` 和 `options` 已经提供了 Select 的完整支持。前端 `PluginCRUDPage.tsx` 第 178-185 行已经处理了 `select` widget:
|
||||
|
||||
```typescript
|
||||
case 'select':
|
||||
return (
|
||||
<Select>
|
||||
{(field.options || []).map((opt) => (
|
||||
<Select.Option key={String(opt.value)} value={opt.value}>
|
||||
{opt.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
```
|
||||
|
||||
**结论:CRM 的 select 字段不需要扩展 manifest。** 在 plugin.toml 中声明方式:
|
||||
|
||||
```toml
|
||||
[[schema.entities.fields]]
|
||||
name = "customer_type"
|
||||
field_type = "string"
|
||||
ui_widget = "select"
|
||||
options = [
|
||||
{ label = "潜在客户", value = "potential" },
|
||||
{ label = "意向客户", value = "intention" },
|
||||
{ label = "成交客户", value = "closed" },
|
||||
{ label = "流失客户", value = "churned" },
|
||||
]
|
||||
```
|
||||
|
||||
底层存储仍为 `string`(JSONB 的 `data->>'customer_type'`),数据库层不需要 enum 约束。
|
||||
|
||||
---
|
||||
|
||||
## 3. 关联实体查询
|
||||
|
||||
### 当前 db-query 的限制
|
||||
|
||||
查看 `host.rs` 第 99-109 行,`db_query` 的实现:
|
||||
|
||||
```rust
|
||||
fn db_query(&mut self, entity: String, _filter: Vec<u8>, _pagination: Vec<u8>) -> Result<Vec<u8>, String> {
|
||||
self.query_results
|
||||
.get(&entity)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("实体 '{}' 的查询结果未预填充", entity))
|
||||
}
|
||||
```
|
||||
|
||||
**关键发现:当前 db-query 完全依赖预填充。** `filter` 和 `pagination` 参数被忽略(`_filter`, `_pagination`)。查询结果在 WASM 执行前由 Host 预填充到 `HostState.query_results` 中。
|
||||
|
||||
但 `engine.rs` 的 `execute_wasm` 方法(第 444-522 行)中并没有看到预填充查询结果的逻辑。`HostState::new` 只初始化了空的 `query_results: HashMap::new()`。
|
||||
|
||||
### CRM 关联查询场景
|
||||
|
||||
"联系人列表按 customer_id 过滤" 的需求有两种实现路径:
|
||||
|
||||
**路径 A:扩展 Host API 的 db-query(推荐)**
|
||||
|
||||
需要修改 `engine.rs` 的 `execute_wasm` 方法,在 WASM 执行前解析 filter 参数,执行真实 SQL 查询,将结果预填充。或者更直接的方案——让 `db_query` 在调用时实时执行查询,而不是依赖预填充。
|
||||
|
||||
这意味着需要在 `HostState` 中持有 `DatabaseConnection` 的引用,或者将 `db_query` 改为延迟执行模式。但当前架构是 WASM 在 `spawn_blocking` 中同步执行,无法直接持有异步的 DB 连接。
|
||||
|
||||
可行的改造方案:
|
||||
1. `db_query` 仍然走预填充模式,但在 `execute_wasm` 前根据 WASM 函数类型智能预填充(不现实,因为不知道插件会查什么)
|
||||
2. 改为同步查询模式:在 `HostState` 中持有同步的 DB 连接(需要 `blocking_spawn` 内部再 spawn async task)
|
||||
3. **最佳方案**:前端直接调用 REST API 查询关联数据,不走 WASM 的 db-query
|
||||
|
||||
**路径 B:前端直接按 customer_id 过滤(当前最可行)**
|
||||
|
||||
CRM 的关联查询(联系人与客户、沟通记录与客户)不需要走 WASM 的 `db-query`。前端 `PluginCRUDPage` 已经可以直接调用 `GET /api/v1/plugins/{plugin_id}/{entity}?page=1` 来获取联系人列表。
|
||||
|
||||
但当前 REST API 的 `list_plugin_data`(`data_handler.rs` 第 25-57 行)也不支持过滤参数。`PluginDataListParams` 只有 `page`, `page_size`, `search`。
|
||||
|
||||
**需要做的改造:**
|
||||
|
||||
1. **后端**:在 `PluginDataListParams` 中增加 `filter` 参数(JSON 格式,如 `{"customer_id": "xxx"}`)
|
||||
2. **后端**:在 `PluginDataService::list` 中解析 filter,构建带 WHERE 条件的 SQL
|
||||
3. **前端**:`PluginCRUDPage` 接收 URL 参数如 `?filter[customer_id]=xxx`,传给 API
|
||||
|
||||
`dynamic_table.rs` 需要新增一个 `build_filtered_query_sql` 方法:
|
||||
|
||||
```rust
|
||||
pub fn build_filtered_query_sql(
|
||||
table_name: &str,
|
||||
tenant_id: Uuid,
|
||||
filters: &serde_json::Value, // {"customer_id": "xxx", "status": "active"}
|
||||
limit: u64,
|
||||
offset: u64,
|
||||
) -> (String, Vec<Value>)
|
||||
```
|
||||
|
||||
使用 PostgreSQL 的 JSONB 查询操作符:`data->>'customer_id' = $N`。
|
||||
|
||||
**结论:关联查询需要扩展现有 API,但改造量不大。** 主要是在 REST API 层增加 filter 支持,不需要改 WASM 运行时。这是中等工作量的改造。
|
||||
|
||||
---
|
||||
|
||||
## 4. 唯一性约束
|
||||
|
||||
### customer.code 唯一性
|
||||
|
||||
`dynamic_table.rs` 第 67-87 行,建表时会为 `unique: true` 的字段创建索引:
|
||||
|
||||
```rust
|
||||
if field.unique || field.required {
|
||||
let idx_sql = format!(
|
||||
"CREATE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{table_name}\" \
|
||||
(\"data\"->>'{sanitized_field}') WHERE \"deleted_at\" IS NULL"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
但这里只创建了普通索引(`CREATE INDEX`),不是唯一索引。`field.unique` 判断只影响了索引名称后缀(`"uniq"` vs `"idx"`),索引类型始终是 `CREATE INDEX`,不是 `CREATE UNIQUE INDEX`。
|
||||
|
||||
**Bug:unique 字段没有创建唯一索引。**
|
||||
|
||||
此外,`engine.rs` 的 `flush_ops` 中 `PendingOp::Insert`(第 543-560 行)执行的是普通 INSERT,没有做唯一性检查:
|
||||
|
||||
```rust
|
||||
let (sql, values) = DynamicTableManager::build_insert_sql_with_id(
|
||||
&table_name, id_uuid, tenant_id, user_id, &parsed_data
|
||||
);
|
||||
txn.execute(Statement::from_sql_and_values(...)).await?;
|
||||
```
|
||||
|
||||
`build_insert_sql_with_id`(`dynamic_table.rs` 第 141-162 行)也是普通 INSERT,没有 ON CONFLICT 处理。
|
||||
|
||||
### 需要修复
|
||||
|
||||
1. **建表时**:`field.unique` 应创建 `CREATE UNIQUE INDEX`
|
||||
2. **INSERT 时**:需要检查 unique 字段的值是否已存在(或使用 `ON CONFLICT`)
|
||||
3. **REST API 层**:`PluginDataService::create` 也需要同样的检查
|
||||
|
||||
修复方案(`dynamic_table.rs` 建表部分):
|
||||
|
||||
```rust
|
||||
let idx_sql = if field.unique {
|
||||
format!(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{table_name}\" \
|
||||
(\"data\"->>'{sanitized_field}') WHERE \"deleted_at\" IS NULL"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"CREATE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{table_name}\" \
|
||||
(\"data\"->>'{sanitized_field}') WHERE \"deleted_at\" IS NULL"
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
INSERT 前需要做存在性检查,或使用事务内 SELECT + INSERT 模式。
|
||||
|
||||
**结论:唯一性约束是必须修复的 bug,否则 CRM 的 customer.code 无法保证唯一。** 改动量小但重要性高。
|
||||
|
||||
---
|
||||
|
||||
## 5. 前端通用组件实现量
|
||||
|
||||
### 5.1 tree(树形页面)
|
||||
|
||||
**用途**:组织架构树、客户层级树(parent_id 关系)
|
||||
|
||||
**Ant Design 基础组件**:`Tree` / `DirectoryTree` / `TreeSelect`
|
||||
|
||||
**开发量**:2-3 天
|
||||
- Schema 解析:从 manifest 中识别 `parent_id` 字段作为树形关系
|
||||
- 数据加载:一次性加载全量数据,前端组装树结构
|
||||
- CRUD 操作:内联新增/编辑/删除节点
|
||||
- 拖拽排序(可选)
|
||||
|
||||
**关键依赖**:需要 `PluginCRUDPage` 支持全量加载模式(不分页),当前 `data_service.rs` 的 `list` 只支持分页。
|
||||
|
||||
### 5.2 graph(关系图)
|
||||
|
||||
**用途**:客户关系图(customer_relationship 实体)
|
||||
|
||||
**Ant Design 基础组件**:没有内置 graph 组件
|
||||
|
||||
**需要引入的库**:`@ant-design/charts`(G6 封装)或 `reactflow`
|
||||
|
||||
**开发量**:5-8 天
|
||||
- 图数据转换:将 customer + customer_relationship 数据转换为 nodes/edges
|
||||
- 图布局算法:力导向布局 / 层次布局
|
||||
- 交互:节点点击查看详情、拖拽、缩放
|
||||
- 工具栏:放大/缩小/导出
|
||||
|
||||
**技术风险**:高。图可视化是这 5 种页面类型中最复杂的。需要考虑性能(100+ 节点时的渲染)。
|
||||
|
||||
### 5.3 timeline(时间线)
|
||||
|
||||
**用途**:沟通记录时间线、客户跟进历史
|
||||
|
||||
**Ant Design 基础组件**:`Timeline` / `Steps`
|
||||
|
||||
**开发量**:1-2 天
|
||||
- 数据排序:按 `occurred_at` 排序
|
||||
- 渲染:不同 `type`(电话/邮件/会议)显示不同图标和颜色
|
||||
- 交互:点击展开详情
|
||||
|
||||
**最简单的页面类型**,Ant Design Timeline 直接可用。
|
||||
|
||||
### 5.4 tabs(标签页嵌套)
|
||||
|
||||
**用途**:客户详情页包含多个标签(基本信息/联系人/沟通记录/标签/关系)
|
||||
|
||||
**Ant Design 基础组件**:`Tabs`
|
||||
|
||||
**开发量**:3-5 天
|
||||
- Manifest 扩展:定义 tabs 嵌套结构
|
||||
- 组件组合:每个 tab 内部是一个子页面(可能是 crud/timeline/graph)
|
||||
- 数据关联:子页面需要接收父实体的 ID 作为过滤条件
|
||||
- 路由:需要处理嵌套路由或状态切换
|
||||
|
||||
**核心挑战**:tabs 嵌套需要 manifest 结构支持 `children` 或 `tabs` 字段,并且在运行时动态组合不同类型的页面组件。
|
||||
|
||||
### 5.5 dashboard(仪表盘)
|
||||
|
||||
**用途**:CRM 概览(客户总数、本月新增、跟进中、转化率等)
|
||||
|
||||
**Ant Design 基础组件**:`Statistic` / `Card` / `@ant-design/charts`
|
||||
|
||||
**开发量**:5-7 天
|
||||
- 统计卡片:数值展示 + 趋势箭头
|
||||
- 图表:柱状图/折线图/饼图(需要聚合查询 API)
|
||||
- 筛选器:时间范围、客户类型等
|
||||
- 实时更新(可选)
|
||||
|
||||
**关键依赖**:需要后端提供聚合查询 API(COUNT/GROUP BY),当前 `db_query` 不支持聚合。`@ant-design/charts` 需要作为新依赖安装。
|
||||
|
||||
### 汇总
|
||||
|
||||
| 页面类型 | 开发量 | 技术风险 | Ant Design 支持 |
|
||||
|----------|--------|----------|-----------------|
|
||||
| tree | 2-3 天 | 低 | Timeline/Tree 组件直接可用 |
|
||||
| graph | 5-8 天 | 高 | 需要引入第三方库 |
|
||||
| timeline | 1-2 天 | 低 | Timeline 组件直接可用 |
|
||||
| tabs | 3-5 天 | 中 | Tabs 可用,但需要组合其他页面类型 |
|
||||
| dashboard | 5-7 天 | 中 | Statistic/Card 可用,图表需第三方库 |
|
||||
|
||||
**总计**:16-25 天(约 3-5 周)的前端开发量。建议分两期:
|
||||
- 第一期(1-2 周):timeline + tree + tabs(CRM 核心需要)
|
||||
- 第二期(2-3 周):dashboard + graph(锦上添花)
|
||||
|
||||
---
|
||||
|
||||
## 6. manifest.ui.pages 解析
|
||||
|
||||
### 当前结构
|
||||
|
||||
`manifest.rs` 第 94-109 行:
|
||||
|
||||
```rust
|
||||
pub struct PluginUi {
|
||||
pub pages: Vec<PluginPage>,
|
||||
}
|
||||
|
||||
pub struct PluginPage {
|
||||
pub route: String,
|
||||
pub entity: String,
|
||||
pub display_name: String,
|
||||
pub icon: String,
|
||||
pub menu_group: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### 需要扩展为
|
||||
|
||||
```rust
|
||||
pub struct PluginPage {
|
||||
pub route: String,
|
||||
pub entity: String,
|
||||
pub display_name: String,
|
||||
pub icon: String,
|
||||
pub menu_group: Option<String>,
|
||||
// 新增
|
||||
pub page_type: Option<String>, // "crud" | "tree" | "graph" | "timeline" | "tabs" | "dashboard"
|
||||
pub tabs: Option<Vec<PluginTab>>, // tabs 嵌套
|
||||
pub field_mappings: Option<HashMap<String, String>>, // 字段映射配置
|
||||
pub filters: Option<Vec<PluginFilter>>, // 过滤器配置
|
||||
}
|
||||
|
||||
pub struct PluginTab {
|
||||
pub label: String,
|
||||
pub entity: String,
|
||||
pub page_type: Option<String>,
|
||||
pub filters: Option<Vec<PluginFilter>>,
|
||||
}
|
||||
|
||||
pub struct PluginFilter {
|
||||
pub field: String,
|
||||
pub source: String, // "url_param" | "parent_entity" | "fixed"
|
||||
pub value: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### 影响范围
|
||||
|
||||
1. **manifest.rs**:扩展 `PluginPage` 结构体 — 改动量小,只新增字段
|
||||
2. **service.rs**:`install` 方法中需要将新的 manifest 数据存入 `plugin_entity.schema_json` — 无需改动(已序列化完整 entity 定义)
|
||||
3. **前端 plugin store**:`plugin.ts` 的 `PluginMenuItem` 需要增加 `pageType` 字段 — 改动量小
|
||||
4. **前端路由**:`App.tsx` 需要根据 `page_type` 渲染不同组件 — 中等改动
|
||||
5. **前端 PluginCRUDPage**:需要重构为通用页面路由器,根据 page_type 分发到不同组件
|
||||
|
||||
**结论:manifest 结构扩展的改动量不大**(Rust 端约 30 行),但前端需要重构路由分发逻辑。对现有代码的影响是可控的,因为都是新增字段(`Option<>`),不破坏现有 manifest 解析。
|
||||
|
||||
---
|
||||
|
||||
## 7. 权限与数据隔离
|
||||
|
||||
### 当前权限模型
|
||||
|
||||
`data_handler.rs` 中的权限检查(第 35、80、116、143、181 行):
|
||||
- 列表/详情:`require_permission(&ctx, "plugin.list")`
|
||||
- 创建/更新/删除:`require_permission(&ctx, "plugin.admin")`
|
||||
|
||||
**所有插件数据共用 `plugin.list` 和 `plugin.admin` 两个权限码。**
|
||||
|
||||
`m20260417_000034_seed_plugin_permissions.rs` 也只种子了这两个权限。
|
||||
|
||||
### CRM 需要 8 个权限码
|
||||
|
||||
CRM 设计的权限码如 `crm.customer.list`、`crm.customer.create` 等。这些与当前的 `plugin.list`/`plugin.admin` 是两套完全不同的权限体系。
|
||||
|
||||
### check-permission Host API
|
||||
|
||||
`host.rs` 第 167-169 行:
|
||||
|
||||
```rust
|
||||
fn check_permission(&mut self, permission: String) -> Result<bool, String> {
|
||||
Ok(self.permissions.contains(&permission))
|
||||
}
|
||||
```
|
||||
|
||||
这个 API 检查的是当前用户的权限列表。`permissions` 来自 `ExecutionContext.permissions`(`engine.rs` 第 68 行),是外部传入的用户权限列表。
|
||||
|
||||
**关键问题:CRM 的 8 个权限码在数据库中不存在。**
|
||||
|
||||
当前权限系统的流程:
|
||||
1. 权限定义在 `permissions` 表中(通过 migration seed 或 admin API 创建)
|
||||
2. 通过 `role_permissions` 表分配给角色
|
||||
3. 用户登录时,JWT 中间件从角色关联加载权限列表
|
||||
|
||||
CRM 插件的 `crm.customer.list` 等权限需要:
|
||||
1. 在插件安装时,动态 INSERT 到 `permissions` 表
|
||||
2. 分配给适当角色
|
||||
3. 这样用户请求时 JWT 中间件才能加载这些权限
|
||||
|
||||
### 需要的改造
|
||||
|
||||
1. **安装时自动注册权限**:`service.rs` 的 `install` 方法需要遍历 manifest 中的 `permissions` 列表,INSERT 到 `permissions` 表
|
||||
2. **REST API 权限映射**:`data_handler.rs` 的权限检查需要从固定 `plugin.list`/`plugin.admin` 改为动态检查——基于 manifest 中声明的权限码
|
||||
3. **卸载时清理权限**:`uninstall` 方法需要清理 `permissions` 表和 `role_permissions` 表中的插件权限
|
||||
|
||||
**当前 manifest 已支持 permissions 声明**(`manifest.rs` 第 112-118 行的 `PluginPermission`),但 `service.rs` 的 `install` 方法没有将其写入 `permissions` 表。
|
||||
|
||||
**结论:权限系统需要中等改造。** 核心改动点在 `service.rs` 的 install/uninstall 方法中增加权限 CRUD,以及 `data_handler.rs` 中从固定权限改为动态权限检查。
|
||||
|
||||
---
|
||||
|
||||
## 8. 整体实施风险评估
|
||||
|
||||
### 高风险
|
||||
|
||||
#### R1:db-query Host API 不可用(风险等级:高)
|
||||
|
||||
**问题**:当前 `db_query` 依赖预填充,但 `execute_wasm` 中没有预填充逻辑。CRM 插件在 WASM 内部无法执行关联查询。
|
||||
|
||||
**影响**:WASM 内的 `handle_event` 无法查询关联数据。例如,收到 `workflow.task.completed` 事件后,无法查询关联的客户记录。
|
||||
|
||||
**缓解方案**:
|
||||
- 短期:CRM 的关联查询全部走前端 REST API,WASM 内只做简单的 `db_insert`/`db_update`
|
||||
- 长期:改造 `db_query` 为实时查询模式(需要在 `spawn_blocking` 中支持异步 DB 调用)
|
||||
|
||||
**建议**:CRM 的复杂查询(关联、聚合)不应在 WASM 内完成。WASM 插件的 `handle_event` 应限于简单的状态变更,复杂查询由前端直接调用 REST API。
|
||||
|
||||
#### R2:唯一索引 Bug(风险等级:高)
|
||||
|
||||
**问题**:`dynamic_table.rs` 的 `unique` 字段只创建了普通索引,不是唯一索引。INSERT 时也没有冲突检查。
|
||||
|
||||
**影响**:`customer.code` 无法保证唯一性,可能插入重复数据。
|
||||
|
||||
**修复量**:约 20 行代码改动。
|
||||
|
||||
#### R3:插件权限未注册到数据库(风险等级:高)
|
||||
|
||||
**问题**:manifest 声明了 permissions,但 install 时没有写入 permissions 表。
|
||||
|
||||
**影响**:CRM 的 8 个权限码在数据库中不存在,check-permission 永远返回 false,JWT 中间件也无法加载这些权限。
|
||||
|
||||
**修复量**:约 50 行代码改动(install/uninstall 方法增加权限 CRUD)。
|
||||
|
||||
### 中风险
|
||||
|
||||
#### R4:REST API 不支持过滤查询(风险等级:中)
|
||||
|
||||
**问题**:`PluginDataListParams` 只有分页参数,不支持字段过滤。CRM 需要按 `customer_id` 过滤联系人等场景。
|
||||
|
||||
**修复量**:约 80 行代码改动(后端 filter 解析 + SQL 构建 + 前端传参)。
|
||||
|
||||
#### R5:前端 tabs 嵌套实现复杂度(风险等级:中)
|
||||
|
||||
**问题**:tabs 页面类型需要支持动态组合不同子页面类型,且子页面需要从父页面获取过滤参数。这涉及组件设计模式的选择。
|
||||
|
||||
**缓解方案**:先实现基本的 tabs + crud 组合,graph/timeline 作为子页面类型后续迭代。
|
||||
|
||||
#### R6:前端页面类型路由分发(风险等级:中)
|
||||
|
||||
**问题**:当前 `App.tsx` 只有一个 `PluginCRUDPage` 路由。需要根据 manifest 中的 `page_type` 动态选择渲染组件。
|
||||
|
||||
**修复量**:约 100 行代码改动(新增路由分发组件 + 各页面类型组件)。
|
||||
|
||||
### 低风险
|
||||
|
||||
#### R7:Fuel 限制(风险等级:低)
|
||||
|
||||
1000 万 fuel 对 CRM 插件绰绰有余。无需改动。
|
||||
|
||||
#### R8:manifest 扩展(风险等级:低)
|
||||
|
||||
新增的 `page_type`、`tabs`、`filters` 字段都是 `Option<>`,不破坏现有解析。改动量约 30 行。
|
||||
|
||||
---
|
||||
|
||||
## 实施建议
|
||||
|
||||
### 必须先修复的 3 个问题(1-2 天)
|
||||
|
||||
1. **修复唯一索引**:`dynamic_table.rs` 中 `field.unique` 创建 `CREATE UNIQUE INDEX`
|
||||
2. **权限注册**:`service.rs` 的 `install` 方法将 manifest.permissions 写入 permissions 表
|
||||
3. **REST API 过滤**:`PluginDataListParams` 增加 filter 参数,`build_query_sql` 支持条件过滤
|
||||
|
||||
### 分期实施计划
|
||||
|
||||
#### 第一期:最小可用 CRM(2-3 周)
|
||||
|
||||
- 5 个 JSONB 实体(customer/contact/communication/customer_tag/customer_relationship)
|
||||
- plugin.toml manifest 定义
|
||||
- WASM 插件 Rust crate(init + on_tenant_created + handle_event 基础逻辑)
|
||||
- 前端 CRUD 页面(复用已有 PluginCRUDPage)
|
||||
- 前端 tabs 页面类型(客户详情页的基本信息+联系人+沟通记录)
|
||||
- 前端 timeline 页面类型(沟通记录时间线)
|
||||
- REST API filter 支持
|
||||
|
||||
#### 第二期:增强功能(2-3 周)
|
||||
|
||||
- 前端 tree 页面类型(客户层级树)
|
||||
- 前端 dashboard 页面类型(CRM 概览统计)
|
||||
- 后端聚合查询 API(COUNT/GROUP BY)
|
||||
- graph 页面类型(客户关系图)
|
||||
- 高级搜索/筛选
|
||||
|
||||
#### 第三期:优化打磨(1-2 周)
|
||||
|
||||
- WASM 内部 db-query 实时查询改造
|
||||
- 批量操作支持
|
||||
- 数据导入导出
|
||||
- 性能优化(大量客户数据场景)
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
CRM 插件在当前 WASM 插件系统上的实现**整体可行**,但有几个前提条件需要先满足:
|
||||
|
||||
1. **必须修复**:唯一索引 bug、权限注册缺失、REST API 过滤能力缺失
|
||||
2. **核心路径**:CRM 的数据操作通过 REST API 而非 WASM 内的 db-query 完成(这是正确的架构选择)
|
||||
3. **WASM 角色定位**:WASM 负责生命周期钩子(init/on_tenant_created/handle_event),不负责复杂查询和聚合
|
||||
4. **前端是主要工作量**:5 种新页面类型中,graph 和 dashboard 开发量最大,建议分两期交付
|
||||
|
||||
最大的技术风险不在 WASM 运行时本身(它已被验证),而在于周边配套设施(权限、过滤、前端组件)的完善程度。
|
||||
@@ -1,512 +0,0 @@
|
||||
# CRM 插件 UI 架构审查报告
|
||||
|
||||
> 审查范围:CRM 插件 manifest 配置驱动 UI 方案
|
||||
> 审查人:ArchitectUX
|
||||
> 日期:2026-04-15
|
||||
|
||||
---
|
||||
|
||||
## 一、现状总结
|
||||
|
||||
读完代码后,当前的架构清晰度很高:
|
||||
|
||||
- **后端**:`PluginManifest`(manifest.rs)定义了 `PluginPage`,但只有 `route/entity/display_name/icon/menu_group` 五个字段,没有 `type` 概念。所有插件页面都走同一条路径渲染。
|
||||
- **前端路由**:`App.tsx` 只注册了一条通配路由 `/plugins/:pluginId/:entityName`,全部指向 `PluginCRUDPage`。
|
||||
- **CRUD 页面**:`PluginCRUDPage` 有基础的表格+分页+Modal表单+删除,但没有搜索、筛选、排序能力。表格硬编码只展示前 5 个字段。
|
||||
- **数据层**:`DynamicTableManager` 把所有字段塞进一个 JSONB `data` 列,查询能力有限(不支持按 JSONB 内字段筛选/排序/搜索)。`PluginDataListParams` 只有 `page/page_size/search`,没有 filter 参数。
|
||||
- **插件菜单**:`PluginStore.refreshMenuItems` 按 entity 平铺生成菜单项,不支持嵌套/分组/多页面类型。
|
||||
|
||||
这意味着 CRM 方案中设想的 `type="tabs"/tree/graph/timeline` 在 manifest 结构、后端 API、前端路由、前端渲染器四个层面都**没有基础设施**。下面逐项给出具体建议。
|
||||
|
||||
---
|
||||
|
||||
## 二、逐项审查与建议
|
||||
|
||||
### 2.1 CRUD 页面增强:筛选、搜索、标签过滤
|
||||
|
||||
**问题**:CRM 客户列表需要按类型/等级/地区/行业/状态筛选,按名称/编码模糊搜索,按标签过滤。现有 `PluginField` 没有"是否可搜索/可筛选"的概念,后端也没有 filter 参数。
|
||||
|
||||
**建议**:分两层扩展。
|
||||
|
||||
**Manifest 层** -- 在 `PluginField` 上新增配置:
|
||||
|
||||
```toml
|
||||
[[schema.entities.fields]]
|
||||
name = "customer_type"
|
||||
field_type = "string"
|
||||
display_name = "客户类型"
|
||||
ui_widget = "select"
|
||||
filterable = true # 新增:出现在筛选栏
|
||||
options = [
|
||||
{ label = "企业", value = "enterprise" },
|
||||
{ label = "个人", value = "individual" },
|
||||
]
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "name"
|
||||
field_type = "string"
|
||||
display_name = "客户名称"
|
||||
searchable = true # 新增:出现在搜索框
|
||||
```
|
||||
|
||||
新增字段语义:
|
||||
- `searchable: bool` -- 该字段参与模糊搜索(后端用 `data->>'name' ILIKE '%keyword%'`)
|
||||
- `filterable: bool` -- 该字段出现在筛选栏,根据 `ui_widget` 自动选择筛选控件(select 用 Select,date 用 DatePicker 等)
|
||||
- `sortable: bool` -- 该字段可排序(后端用 `ORDER BY data->>'field'`)
|
||||
|
||||
**后端层** -- 扩展 `PluginDataListParams`:
|
||||
|
||||
```rust
|
||||
pub struct PluginDataListParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub search: Option<String>, // 已有,保持
|
||||
pub filters: Option<String>, // 新增:JSON 格式 {"customer_type":"enterprise","level":"A"}
|
||||
pub sort_by: Option<String>, // 新增:字段名
|
||||
pub sort_order: Option<String>, // 新增:asc/desc
|
||||
}
|
||||
```
|
||||
|
||||
在 `DynamicTableManager` 中根据 searchable/filterable 字段动态拼接 WHERE 子句和 ORDER BY。JSONB 字段的索引需要用 GIN 索引或 expression index 支持。
|
||||
|
||||
**前端层** -- `PluginCRUDPage` 增加筛选栏区域:
|
||||
|
||||
从 schema 中提取 `filterable=true` 的字段,在表格上方渲染筛选控件。从 `searchable=true` 的字段提取字段名,传给搜索框的 placeholder 提示(如"搜索客户名称/编码")。
|
||||
|
||||
**评估**:这个方案是渐进式的,不需要引入新的页面类型,只需扩展现有 crud 的能力。实现优先级最高。
|
||||
|
||||
---
|
||||
|
||||
### 2.2 客户详情页
|
||||
|
||||
**问题**:CRM 的客户详情需要展示基本信息+关联联系人+沟通记录时间线+关系图谱。当前没有"详情页"概念。
|
||||
|
||||
**建议**:新增 `detail` 页面类型,作为 CRUD 的行级扩展,不需要独立路由。
|
||||
|
||||
**Manifest 设计**:
|
||||
|
||||
```toml
|
||||
[[ui.pages]]
|
||||
type = "crud"
|
||||
label = "客户列表"
|
||||
entity = "customer"
|
||||
detail = { # 新增:点击行进入详情
|
||||
layout = "tabs", # 详情页内部布局用 tabs 组织
|
||||
sections = [
|
||||
{ type = "fields", label = "基本信息", fields = ["name","code","customer_type","level","industry","region","status"] },
|
||||
{ type = "crud", label = "联系人", entity = "contact", parent_field = "customer_id" },
|
||||
{ type = "timeline", label = "沟通记录", entity = "communication", date_field = "created_at", content_field = "content" },
|
||||
{ type = "graph", label = "关系图谱", entity = "customer_relationship", source_field = "from_customer_id", target_field = "to_customer_id" },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**实现方式**:
|
||||
|
||||
1. 表格行增加"查看"按钮(或行点击事件),打开一个 Drawer/新页面
|
||||
2. 路由:`/plugins/:pluginId/:entityName/:id` -- 复用现有路由增加可选 `:id` 段
|
||||
3. 详情页是一个容器组件,根据 `detail.sections` 配置渲染 tabs,每个 tab 内部递归使用已有的渲染器(crud/tree/timeline/graph)
|
||||
4. "基本信息" tab 用 Ant Design `Descriptions` 组件渲染,`fields` 指定显示哪些字段及顺序
|
||||
|
||||
**为什么不用单独的 `tabs` 页面类型包裹**:tabs 作为顶级页面类型适合管理视角(如"客户管理"入口),但详情页是某个具体记录的上下文视图,两者是不同层级。顶级 tabs 是菜单入口,详情页 tabs 是数据上下文。
|
||||
|
||||
**优先级**:高。CRM 的核心体验就是从列表进入详情。建议先实现 `fields` + `crud` 两种 section 类型,`timeline` 和 `graph` 作为后续迭代。
|
||||
|
||||
---
|
||||
|
||||
### 2.3 关系图谱的交互设计
|
||||
|
||||
**问题**:graph 类型需要节点点击、拖拽、缩放、关系筛选等交互。如何配置控制?
|
||||
|
||||
**建议**:graph 页面是所有新类型中复杂度最高的,建议分两个阶段。
|
||||
|
||||
**第一阶段 -- 基础配置**:
|
||||
|
||||
```toml
|
||||
type = "graph"
|
||||
entity = "customer_relationship"
|
||||
source_entity = "customer" # 节点数据来源
|
||||
source_field = "from_customer_id" # 关系起点
|
||||
target_field = "to_customer_id" # 关系终点
|
||||
edge_label_field = "relationship_type" # 边上的标签
|
||||
node_label_field = "name" # 节点上的标签
|
||||
node_color_field = "customer_type" # 按字段值区分节点颜色
|
||||
interactions = ["click", "zoom", "drag"] # 启用的交互,默认全部
|
||||
on_click = "drawer" # 点击节点打开 drawer 查看详情
|
||||
```
|
||||
|
||||
**第二阶段 -- 高级配置**(CRM 专属,不急于实现):
|
||||
|
||||
```toml
|
||||
[[ui.pages.graph.filters]]
|
||||
field = "relationship_type"
|
||||
label = "关系类型"
|
||||
multiple = true
|
||||
|
||||
[[ui.pages.graph.node_styles]]
|
||||
value = "enterprise"
|
||||
color = "#2563EB"
|
||||
size = 40
|
||||
|
||||
[[ui.pages.graph.node_styles]]
|
||||
value = "individual"
|
||||
color = "#059669"
|
||||
size = 30
|
||||
```
|
||||
|
||||
**技术选型**:建议使用 Ant Design 内置能力 + 一个轻量图谱库(如 antv/G6 或 react-force-graph)。G6 是 AntV 生态的,和 Ant Design 风格一致,社区活跃,配置驱动友好。
|
||||
|
||||
**优先级**:中。图谱是 CRM 的差异化功能,但实现成本高。第一阶段可以先做一个静态展示(节点+边+基础交互),第二阶段再做筛选和样式。
|
||||
|
||||
---
|
||||
|
||||
### 2.4 个人/企业客户的差异化表单
|
||||
|
||||
**问题**:同一个 customer 实体,企业客户显示 credit_code,个人客户显示 id_number。
|
||||
|
||||
**建议**:在 `PluginField` 上新增 `visible_when` 条件表达式。
|
||||
|
||||
```toml
|
||||
[[schema.entities.fields]]
|
||||
name = "customer_type"
|
||||
field_type = "string"
|
||||
display_name = "客户类型"
|
||||
ui_widget = "select"
|
||||
required = true
|
||||
options = [
|
||||
{ label = "企业", value = "enterprise" },
|
||||
{ label = "个人", value = "individual" },
|
||||
]
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "credit_code"
|
||||
field_type = "string"
|
||||
display_name = "统一社会信用代码"
|
||||
visible_when = "customer_type == 'enterprise'" # 条件显示
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "id_number"
|
||||
field_type = "string"
|
||||
display_name = "身份证号"
|
||||
visible_when = "customer_type == 'individual'" # 条件显示
|
||||
```
|
||||
|
||||
**`visible_when` 语法**:采用简单表达式,前端解析执行。
|
||||
|
||||
支持的表达式类型:
|
||||
- `field_name == 'value'` -- 字段等于某值
|
||||
- `field_name != 'value'` -- 字段不等于某值
|
||||
- `field_name in ['a','b']` -- 字段在列表中
|
||||
|
||||
前端实现:`Form.useWatch` 监听 `customer_type` 值变化,动态显示/隐藏字段。Ant Design Form 的 `shouldUpdate` 或 `noStyle` + 条件渲染即可实现。
|
||||
|
||||
**后端无需改动** -- `visible_when` 是纯前端概念,后端只管存 data JSONB,不做校验。
|
||||
|
||||
**评估**:实现成本低,收益高。`visible_when` 是通用能力,不只服务于 CRM,任何需要条件表单的插件都能用到。优先级高。
|
||||
|
||||
---
|
||||
|
||||
### 2.5 树形页面
|
||||
|
||||
**问题**:客户层级树需要展开/折叠、拖拽排序、点击节点查看详情/子节点。交互够通用吗?
|
||||
|
||||
**建议**:树的交互拆分为"必选"和"可选"两层。
|
||||
|
||||
**Manifest 配置**:
|
||||
|
||||
```toml
|
||||
type = "tree"
|
||||
entity = "customer"
|
||||
id_field = "id"
|
||||
parent_field = "parent_id"
|
||||
label_field = "name"
|
||||
icon_field = "customer_type" # 可选:根据字段值显示不同图标
|
||||
default_expand_level = 2 # 默认展开层级
|
||||
draggable = false # CRM 客户层级通常不允许随意拖拽
|
||||
on_click = "drawer" # 点击节点:drawer 查看 | crud 子表格 | 路由跳转
|
||||
actions = ["add_child", "edit", "delete"] # 节点右键/操作按钮
|
||||
```
|
||||
|
||||
**交互拆解**:
|
||||
|
||||
| 交互 | 必选 | 说明 |
|
||||
|------|------|------|
|
||||
| 展开/折叠 | 是 | Ant Design Tree 原生支持 |
|
||||
| 点击查看详情 | 是 | 打开 Drawer 或跳转 |
|
||||
| 异步加载子节点 | 是 | 按需加载,避免一次性拉取大树 |
|
||||
| 搜索节点 | 是 | 过滤树节点,高亮匹配 |
|
||||
| 拖拽排序 | 否 | `draggable = true` 时启用,默认关闭 |
|
||||
| 右键菜单 | 否 | `actions` 配置后启用 |
|
||||
|
||||
**后端要求**:树形数据需要后端支持两种查询模式:
|
||||
1. 一次性返回所有节点(小数据量)
|
||||
2. 按父节点 ID 分批加载子节点(大数据量)
|
||||
|
||||
需要在 `PluginDataListParams` 增加 `parent_id` 和 `root_only` 参数。
|
||||
|
||||
**通用性评估**:足够通用。组织架构、部门、产品分类、地区层级都能用同一套配置。优先级中高。
|
||||
|
||||
---
|
||||
|
||||
### 2.6 时间线视图与 CRUD 列表的切换
|
||||
|
||||
**问题**:时间线和 CRUD 是同一数据的两种视图,用户如何切换?
|
||||
|
||||
**建议**:在 `crud` 页面类型上增加 `views` 配置,而不是用 tabs 包裹。
|
||||
|
||||
```toml
|
||||
type = "crud"
|
||||
entity = "communication"
|
||||
label = "沟通记录"
|
||||
|
||||
[crud.views]
|
||||
default = "table" # 默认视图
|
||||
alternatives = [
|
||||
{ type = "timeline", date_field = "created_at", content_field = "content", title_field = "subject" },
|
||||
]
|
||||
```
|
||||
|
||||
**前端实现**:表格右上角增加视图切换按钮组(Ant Design `Segmented` 或 `Radio.Group`),类似文件管理器的列表/网格切换。切换后保持筛选条件和分页状态。
|
||||
|
||||
**为什么不用 tabs**:
|
||||
- tabs 是语义上不同的内容(客户列表 vs 客户层级 vs 关系图谱),各自有独立的数据加载逻辑
|
||||
- 视图切换是**同一数据的不同呈现**,共享筛选、分页、搜索状态
|
||||
- 放在 CRUD 内部可以避免用户在 tab 间切换时丢失筛选上下文
|
||||
|
||||
**优先级**:中。时间线是沟通记录的最佳展示方式,但实现简单(Ant Design Timeline 组件即可),可以在 CRUD 增强后顺手实现。
|
||||
|
||||
---
|
||||
|
||||
### 2.7 配色和视觉一致性
|
||||
|
||||
**问题**:新增页面类型需与现有内置模块风格一致。
|
||||
|
||||
**现状分析**:
|
||||
- 主题系统已有亮/暗模式(`App.tsx` 的 `themeConfig` / `darkThemeConfig`),使用 Ant Design 的 `ConfigProvider` 全局注入
|
||||
- 主色 `#4F46E5`(Indigo),圆角 `8px`,控件高度 `36px`
|
||||
- 内置模块(如 Users)使用 `theme.useToken()` 获取设计令牌
|
||||
|
||||
**建议**:
|
||||
|
||||
1. **所有新增页面类型必须使用 Ant Design 组件** -- 树用 `Tree`/`DirectoryTree`,时间线用 `Timeline`,标签页用 `Tabs`,详情用 `Descriptions`,抽屉用 `Drawer`。不要引入自定义组件。
|
||||
2. **图谱组件是唯一例外** -- Ant Design 没有图谱组件,需要引入第三方库。选型标准:
|
||||
- 支持主题定制(节点/边颜色走 Ant Design token)
|
||||
- 节点样式默认使用 `token.colorPrimary` / `token.colorSuccess` 等
|
||||
3. **插件页面类型的容器样式统一** -- 所有页面类型共享同一个外层容器组件:
|
||||
|
||||
```tsx
|
||||
// 插件页面通用容器
|
||||
function PluginPageContainer({ title, toolbar, children }) {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h2 style={{ margin: 0 }}>{title}</h2>
|
||||
<Space>{toolbar}</Space>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这与现有 `PluginCRUDPage` 的布局结构一致(24px padding、flex header、标题+工具栏)。
|
||||
|
||||
4. **不要在插件 manifest 中暴露颜色配置** -- 颜色由基座主题系统统一管控。插件如果需要区分视觉(如节点类型着色),使用语义化的 token 名称(`success/error/warning`)而非具体色值。
|
||||
|
||||
---
|
||||
|
||||
## 三、Manifest 结构重构建议
|
||||
|
||||
当前 `PluginPage` 结构过于简单,无法支撑多种页面类型。建议重构为:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum PluginPage {
|
||||
/// CRUD 表格页面
|
||||
Crud(CrudPageConfig),
|
||||
/// 树形页面
|
||||
Tree(TreePageConfig),
|
||||
/// 关系图谱页面
|
||||
Graph(GraphPageConfig),
|
||||
/// 标签页容器
|
||||
Tabs(TabsPageConfig),
|
||||
/// 仪表盘
|
||||
Dashboard(DashboardPageConfig),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CrudPageConfig {
|
||||
pub route: String,
|
||||
pub entity: String,
|
||||
pub display_name: String,
|
||||
pub icon: Option<String>,
|
||||
pub menu_group: Option<String>,
|
||||
pub detail: Option<DetailConfig>,
|
||||
pub views: Option<ViewsConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TreePageConfig {
|
||||
pub route: String,
|
||||
pub entity: String,
|
||||
pub display_name: String,
|
||||
pub icon: Option<String>,
|
||||
pub id_field: String,
|
||||
pub parent_field: String,
|
||||
pub label_field: String,
|
||||
pub draggable: Option<bool>,
|
||||
pub on_click: Option<String>,
|
||||
pub default_expand_level: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GraphPageConfig {
|
||||
pub route: String,
|
||||
pub entity: String,
|
||||
pub display_name: String,
|
||||
pub icon: Option<String>,
|
||||
pub source_entity: String,
|
||||
pub source_field: String,
|
||||
pub target_field: String,
|
||||
pub edge_label_field: Option<String>,
|
||||
pub node_label_field: Option<String>,
|
||||
pub on_click: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TabsPageConfig {
|
||||
pub route: String,
|
||||
pub display_name: String,
|
||||
pub icon: Option<String>,
|
||||
pub menu_group: Option<String>,
|
||||
pub tabs: Vec<TabItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TabItem {
|
||||
pub label: String,
|
||||
#[serde(flatten)]
|
||||
pub page: Box<PluginPage>,
|
||||
}
|
||||
```
|
||||
|
||||
`PluginField` 扩展:
|
||||
|
||||
```rust
|
||||
pub struct PluginField {
|
||||
// ... 现有字段 ...
|
||||
pub searchable: Option<bool>,
|
||||
pub filterable: Option<bool>,
|
||||
pub sortable: Option<bool>,
|
||||
pub visible_when: Option<String>,
|
||||
pub column_width: Option<u32>,
|
||||
pub hidden_in_table: Option<bool>,
|
||||
pub hidden_in_form: Option<bool>,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、前端架构建议
|
||||
|
||||
### 4.1 路由改造
|
||||
|
||||
当前路由 `/plugins/:pluginId/:entityName` 需要改为基于 manifest 的动态路由:
|
||||
|
||||
```
|
||||
/plugins/:pluginId/*pagePath # pagePath 对应 manifest 中的 route
|
||||
```
|
||||
|
||||
`PluginCRUDPage` 重构为 `PluginPageRenderer`,根据 manifest 的 `type` 字段分发到不同的渲染器:
|
||||
|
||||
```
|
||||
PluginPageRenderer
|
||||
-> PluginCrudPage (type="crud")
|
||||
-> PluginTreePage (type="tree")
|
||||
-> PluginGraphPage (type="graph")
|
||||
-> PluginTabsContainer (type="tabs")
|
||||
-> PluginDashboardPage (type="dashboard")
|
||||
```
|
||||
|
||||
### 4.2 组件文件结构
|
||||
|
||||
```
|
||||
apps/web/src/
|
||||
plugins/ # 插件渲染器
|
||||
PluginPageRenderer.tsx # 入口分发器
|
||||
PluginPageContainer.tsx # 通用容器(padding、标题、工具栏)
|
||||
crud/
|
||||
PluginCrudPage.tsx # CRUD 表格+表单(重构自现有)
|
||||
CrudFilterBar.tsx # 筛选栏
|
||||
CrudSearchBar.tsx # 搜索栏
|
||||
CrudDetailDrawer.tsx # 详情抽屉
|
||||
CrudFormFields.tsx # 动态表单字段(含 visible_when)
|
||||
tree/
|
||||
PluginTreePage.tsx # 树形页面
|
||||
graph/
|
||||
PluginGraphPage.tsx # 图谱页面
|
||||
timeline/
|
||||
PluginTimelineView.tsx # 时间线视图(作为 CRUD 的备选视图)
|
||||
tabs/
|
||||
PluginTabsContainer.tsx # 标签页容器
|
||||
shared/
|
||||
manifestParser.ts # manifest 解析工具
|
||||
filterUtils.ts # 筛选参数构建
|
||||
```
|
||||
|
||||
### 4.3 PluginStore 改造
|
||||
|
||||
`refreshMenuItems` 需要从 manifest 的 `ui.pages` 解析菜单结构,支持嵌套:
|
||||
|
||||
```typescript
|
||||
interface PluginMenuItem {
|
||||
key: string; // 路由路径
|
||||
icon: string;
|
||||
label: string;
|
||||
pluginId: string;
|
||||
pageType: string; // crud/tree/graph/tabs/dashboard
|
||||
menuGroup?: string;
|
||||
children?: PluginMenuItem[]; // tabs 类型的子页面
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、实施优先级
|
||||
|
||||
| 优先级 | 内容 | 理由 |
|
||||
|--------|------|------|
|
||||
| P0 | Manifest 结构重构(PluginPage enum + PluginField 扩展) | 所有后续工作的基础 |
|
||||
| P0 | CRUD 增强:searchable/filterable/sortable + 后端 filter 支持 | CRM 最高频操作 |
|
||||
| P0 | visible_when 条件表单 | 个人/企业客户差异化核心需求 |
|
||||
| P1 | 详情页(detail 配置 + Drawer 渲染 + fields/crud section) | CRM 核心体验 |
|
||||
| P1 | 标签页容器(tabs 类型) | CRM 入口页面需要 |
|
||||
| P1 | 前端路由重构(PluginPageRenderer 分发) | 支持多页面类型 |
|
||||
| P2 | 树形页面 | 客户层级,通用性高 |
|
||||
| P2 | 时间线视图(作为 CRUD 备选视图) | 沟通记录展示 |
|
||||
| P3 | 关系图谱(基础版) | 差异化功能,实现成本高 |
|
||||
| P4 | 关系图谱(高级版:筛选、样式) | 锦上添花 |
|
||||
|
||||
---
|
||||
|
||||
## 六、风险提示
|
||||
|
||||
1. **JSONB 查询性能**:当数据量大时,`data->>'field' ILIKE '%keyword%'` 性能堪忧。建议在安装插件时,根据 `searchable=true` 的字段自动创建 GIN 索引(`gin((data->>'name'))` 或表达式索引)。
|
||||
|
||||
2. **图谱库体积**:G6 完整包约 1MB+。如果图谱不是核心功能,考虑用 antv/L7 或更轻量的 react-force-graph(~200KB),或者按需加载(dynamic import)。
|
||||
|
||||
3. **visible_when 表达式安全**:前端解析用户配置的表达式需要沙箱化,防止恶意 manifest 注入。用简单的字符串匹配(正则解析 `field == 'value'`),不要用 `eval` 或 `new Function`。
|
||||
|
||||
4. **向后兼容**:现有的 `PluginPage`(flat struct)需要保持兼容。可以在反序列化时同时支持新旧两种格式,或者在 manifest 版本号上做区分。
|
||||
|
||||
---
|
||||
|
||||
## 七、总结
|
||||
|
||||
CRM 插件的"完全配置驱动"方向是正确的。核心设计原则:
|
||||
|
||||
1. **Manifest 扩展而非前端代码** -- 所有 UI 差异通过 manifest 字段声明,前端渲染器统一处理
|
||||
2. **渐进增强** -- 从 CRUD 增强开始,逐步增加新的页面类型,每一步都可交付
|
||||
3. **通用性优先** -- visible_when、filterable、searchable、tree、detail 这些能力不是 CRM 专属的,任何插件都能受益
|
||||
4. **后端能力先行** -- 前端能做多炫取决于后端能查多深。JSONB 的查询能力是整个方案的瓶颈
|
||||
@@ -1,76 +0,0 @@
|
||||
# CRM 客户管理插件 — 实施计划
|
||||
|
||||
## Context
|
||||
|
||||
ERP 平台底座已完成 Phase 1-6 及 WASM 插件系统原型验证。本计划基于已批准的设计规格文档 `docs/superpowers/specs/2026-04-16-crm-plugin-design.md` v1.1,实施第一个行业插件——客户管理(CRM)。
|
||||
|
||||
设计规格经历了三组专家(架构师、UX 架构师、高级开发者)审查 + Spec 文档审查,所有 CRITICAL/HIGH 问题已修复。
|
||||
|
||||
## 关键决策回顾
|
||||
|
||||
- **插件形式**:WASM 插件(非内置 crate)
|
||||
- **数据层**:5 个 JSONB 动态表,通过 Host API 操作
|
||||
- **UI 层**:完全配置驱动,插件不写前端代码,新 UI 能力沉淀到基座组件库
|
||||
- **策略**:先修基座再做插件
|
||||
- **Skill 沉淀**:CRM 开发经验提炼为可复用 skill
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### Step 1: 调用 writing-plans skill 创建详细实施计划
|
||||
|
||||
**操作**:调用 `writing-plans` skill,基于设计规格文档 `docs/superpowers/specs/2026-04-16-crm-plugin-design.md` 创建分 Phase 的详细实施计划。
|
||||
|
||||
**输入给 writing-plans 的上下文**:
|
||||
- 设计规格文档路径:`docs/superpowers/specs/2026-04-16-crm-plugin-design.md`
|
||||
- 三期划分:Phase 1(基座增强)→ Phase 2(CRM 插件核心)→ Phase 3(高级功能)
|
||||
- 专家审查报告路径:
|
||||
- `plans/flickering-tinkering-pebble-agent-a409e20167941384b.md`(架构师)
|
||||
- `plans/flickering-tinkering-pebble-agent-a785dda8d2f4eeebc.md`(高级开发者)
|
||||
- `plans/flickering-tinkering-pebble-agent-ae1f1bf7d07977a2d.md`(UX 架构师)
|
||||
- Skill 沉淀要求:CRM 完成后将开发经验提炼为可复用 skill
|
||||
|
||||
### Step 2: 按计划实施 Phase 1 — 基座增强
|
||||
|
||||
**前置条件**:Phase 1 是 CRM 插件的前置条件,必须先完成。
|
||||
|
||||
Phase 1 的关键交付物:
|
||||
1. Bug 修复:唯一索引(`dynamic_table.rs`)、权限注册(`service.rs`)、数据 Handler 动态权限(`data_handler.rs`)
|
||||
2. REST API 过滤/搜索/排序(`dynamic_table.rs` + `data_service.rs` + `data_handler.rs` + `dto.rs`)
|
||||
3. Manifest schema 扩展(`manifest.rs`)
|
||||
4. 前端 CRUD 增强 + detail 页面 + visible_when(`PluginCRUDPage.tsx` + `PluginDetailPage.tsx` + `pluginData.ts`)
|
||||
5. 数据校验层
|
||||
|
||||
### Step 3: 按计划实施 Phase 2 — CRM 插件核心
|
||||
|
||||
**前置条件**:Phase 1 全部完成 + 验收通过。
|
||||
|
||||
Phase 2 的关键交付物:
|
||||
1. CRM WASM 插件 Rust 代码(`crates/erp-plugin-crm/`)
|
||||
2. CRM manifest(`plugin.toml`)
|
||||
3. 前端 tree/timeline/tabs 通用页面组件
|
||||
4. 侧边栏动态菜单集成
|
||||
|
||||
### Step 4: 按计划实施 Phase 3 — 高级功能
|
||||
|
||||
**前置条件**:Phase 2 全部完成 + 验收通过。
|
||||
|
||||
Phase 3 的关键交付物:
|
||||
1. 前端 graph 页面类型(AntV G6)
|
||||
2. 前端 dashboard 页面类型
|
||||
3. Host API 扩展(db-count/db-aggregate)
|
||||
4. 关系图谱 + 统计概览
|
||||
|
||||
### Step 5: 提炼插件开发 Skill
|
||||
|
||||
**前置条件**:Phase 2 完成后即可开始(Phase 3 可并行)。
|
||||
|
||||
将 CRM 开发经验提炼为可复用 skill,包含:
|
||||
- 插件开发流程指南
|
||||
- Manifest 配置模板
|
||||
- Rust 插件脚手架
|
||||
- 可用页面类型清单
|
||||
- 测试检查清单
|
||||
|
||||
## 验证方式
|
||||
|
||||
每个 Phase 完成后执行设计规格中的验收标准检查,具体验收项见 `docs/superpowers/specs/2026-04-16-crm-plugin-design.md` 第 8 节。
|
||||
@@ -1,242 +0,0 @@
|
||||
# CRM 插件平台标杆 — P0 基础能力设计
|
||||
|
||||
## Context
|
||||
|
||||
CRM 插件作为 ERP 平台的第一个行业插件,目前暴露了插件平台的多项基础能力缺口。本次设计的定位不是"CRM 功能最全",而是"插件平台能力最扎实"——通过 CRM 验证的每个能力都应被所有未来插件(inventory、生产、财务等)零改动复用。
|
||||
|
||||
对标一流 CRM(Salesforce/HubSpot/Pipedrive)的差距分析表明,当前 CRM 更接近"客户通讯录+标签+图谱",距离可用 CRM 有显著差距。但这些差距中,最优先补的是**平台基础设施**,而非 CRM 业务功能。
|
||||
|
||||
## 设计决策记录
|
||||
|
||||
| 决策点 | 选择 | 理由 |
|
||||
|--------|------|------|
|
||||
| 目标定位 | 插件平台标杆 | 打磨通用能力,所有插件受益 |
|
||||
| 推进节奏 | P0 基础先行 | 基础不扎实,上层的业务功能无法可靠运行 |
|
||||
| 实体关系复杂度 | 全覆盖 (1:N/N:1/N:N/自引用) | CRM 和 inventory 都需要,一次到位 |
|
||||
| 字段校验范围 | 完整套件 (6种) | 数据质量是所有插件的生命线 |
|
||||
| 前端硬编码 | 全部通用化 | 第二个插件必须零改动可用 |
|
||||
|
||||
## P0-1: 实体关系声明 + ref_entity + 级联策略
|
||||
|
||||
### Manifest Schema 扩展
|
||||
|
||||
在 entity 下新增 `[[relations]]` 段:
|
||||
|
||||
```toml
|
||||
[[schema.entities.relations]]
|
||||
name = "contacts" # 关系名
|
||||
target_entity = "contact" # 目标实体
|
||||
type = "one_to_many" # one_to_many | many_to_one | many_to_many
|
||||
foreign_key = "customer_id" # FK 字段
|
||||
on_delete = "cascade_soft_delete" # cascade_soft_delete | set_null | restrict
|
||||
display_field = "name" # 下拉框显示字段
|
||||
|
||||
# N:N 需要中间表
|
||||
[[schema.entities.relations]]
|
||||
name = "related_customers"
|
||||
target_entity = "customer"
|
||||
type = "many_to_many"
|
||||
through_entity = "customer_relationship"
|
||||
through_source_field = "from_customer_id"
|
||||
through_target_field = "to_customer_id"
|
||||
```
|
||||
|
||||
### 后端实现 (crates/erp-plugin/)
|
||||
|
||||
**关键文件:**
|
||||
- `src/manifest.rs` — ManifestParser 新增 relations 解析
|
||||
- `src/dynamic_table.rs` — 安装时存储关系到 entity metadata
|
||||
- `src/data_service.rs` — 删除时执行级联策略,创建/更新时验证 FK
|
||||
- `src/handler/data_handler.rs` — 错误响应格式
|
||||
|
||||
**级联策略执行流程:**
|
||||
```
|
||||
DELETE /plugins/{id}/{entity}/{rid}
|
||||
→ 查询 entity 的所有 incoming relations (被引用的关系)
|
||||
→ for each relation:
|
||||
cascade_soft_delete → UPDATE child SET deleted_at=now() WHERE fk=rid
|
||||
set_null → UPDATE child SET fk=NULL WHERE fk=rid
|
||||
restrict → SELECT COUNT children, if >0 return 409 Conflict
|
||||
→ 软删除目标记录
|
||||
```
|
||||
|
||||
**FK 存在性校验:**
|
||||
```
|
||||
POST/PUT /plugins/{id}/{entity}
|
||||
→ for each field with ref_entity:
|
||||
SELECT EXISTS(SELECT 1 FROM plugin_xxx_{ref_entity} WHERE id=field_value AND deleted_at IS NULL)
|
||||
→ 不存在则返回 400 + 具体字段错误
|
||||
```
|
||||
|
||||
### 前端实现 (apps/web/)
|
||||
|
||||
**关键文件:**
|
||||
- `src/pages/PluginCRUDPage.tsx` — 自动为 ref_entity 字段渲染 EntitySelect
|
||||
- `src/pages/PluginDetailPage` — 自动渲染关联子实体内嵌列表
|
||||
- `src/components/EntitySelect.tsx` — 增强支持 display_field 配置
|
||||
- `src/api/plugins.ts` — schema 类型新增 relations
|
||||
|
||||
**详情页自动关联渲染:**
|
||||
- 读取 entity 的 outgoing relations (one_to_many)
|
||||
- 为每个 relation 渲染内嵌 CRUD 表格(compact 模式,带 filter=fk:parent_id)
|
||||
- 级联删除前弹出确认("将同时删除 3 条联系人")
|
||||
|
||||
### CRM plugin.toml 改造
|
||||
|
||||
为 5 个实体补充 relations 声明:
|
||||
- customer → contacts (1:N, cascade_soft_delete)
|
||||
- customer → communications (1:N, cascade_soft_delete)
|
||||
- customer → tags (1:N, cascade_soft_delete)
|
||||
- customer → parent (N:1, set_null, 自引用)
|
||||
- contact → communications (1:N, cascade_soft_delete)
|
||||
|
||||
---
|
||||
|
||||
## P0-2: 字段校验层
|
||||
|
||||
### Manifest Schema 扩展
|
||||
|
||||
在 field 下新增 `[validation]` 子结构:
|
||||
|
||||
```toml
|
||||
[[schema.entities.fields]]
|
||||
name = "phone"
|
||||
field_type = "string"
|
||||
|
||||
[schema.entities.fields.validation]
|
||||
pattern = "^1[3-9]\\d{9}$"
|
||||
message = "请输入有效的手机号码"
|
||||
min_length = 11
|
||||
max_length = 11
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "email"
|
||||
field_type = "string"
|
||||
|
||||
[schema.entities.fields.validation]
|
||||
pattern = "^[\\w.-]+@[\\w.-]+\\.\\w+$"
|
||||
message = "请输入有效的邮箱地址"
|
||||
max_length = 254
|
||||
```
|
||||
|
||||
### 后端校验器 (crates/erp-plugin/src/validation.rs — 新文件)
|
||||
|
||||
6 种校验器统一执行:
|
||||
|
||||
| 校验器 | 触发条件 | 错误格式 |
|
||||
|--------|---------|---------|
|
||||
| required | `field.required = true` | `{field}: 不能为空` |
|
||||
| unique | `field.unique = true` | `{field}: 该值已存在` |
|
||||
| pattern | `validation.pattern` regex match | `{field}: {validation.message}` |
|
||||
| ref_exists | `field.ref_entity` FK 查询 | `{field}: 引用的{entity}不存在` |
|
||||
| min_length / max_length | `validation.min_length / max_length` | `{field}: 长度必须在 {min}-{max} 之间` |
|
||||
| min_value / max_value | `validation.min_value / max_value` | `{field}: 值必须在 {min}-{max} 之间` |
|
||||
|
||||
**执行位置:** `data_service.rs` 的 create/update 方法中,数据写入前统一调用。
|
||||
|
||||
**错误响应:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "数据验证失败",
|
||||
"details": [
|
||||
{ "field": "phone", "message": "请输入有效的手机号码" },
|
||||
{ "field": "customer_id", "message": "引用的客户不存在" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 前端校验生成
|
||||
|
||||
从 schema 自动生成 Ant Design Form rules:
|
||||
- `required` → `{ required: true, message: "..." }`
|
||||
- `pattern` → `{ pattern: /regex/, message: "..." }`
|
||||
- `min_length / max_length` → `{ min: n, max: n, message: "..." }`
|
||||
|
||||
### CRM plugin.toml 补充校验
|
||||
|
||||
- phone: pattern 手机号
|
||||
- email: pattern 邮箱
|
||||
- credit_code: pattern 统一社会信用代码 (18位)
|
||||
- website: pattern URL
|
||||
- customer_id: ref_entity = "customer" (FK 校验)
|
||||
|
||||
---
|
||||
|
||||
## P0-3: 前端去硬编码
|
||||
|
||||
### Dashboard 通用化
|
||||
|
||||
**文件:** `apps/web/src/pages/dashboard/dashboardConstants.tsx`, `PluginDashboardPage.tsx`
|
||||
|
||||
改造方案:
|
||||
- 移除 `ENTITY_COLORS` 和 `ENTITY_ICONS` 硬编码映射
|
||||
- 颜色自动分配: 8 色调色板按 entity 顺序循环
|
||||
- 图标从 page schema 的 icon 字段读取
|
||||
- 标题: `{manifest.name} 统计概览`,副标题: `{manifest.description}`
|
||||
- Widget 定义从 page schema 的 widgets 数组读取
|
||||
|
||||
### Graph 通用化
|
||||
|
||||
**文件:** `apps/web/src/pages/plugins/graph/graphConstants.ts`
|
||||
|
||||
改造方案:
|
||||
- 移除 `RELATIONSHIP_COLORS` 硬编码
|
||||
- 关系类型标签从 field.options 读取 (已有 label 映射)
|
||||
- 颜色用调色板按 option 顺序循环分配
|
||||
- 未知类型 fallback 到灰色 + 原始 label
|
||||
|
||||
### CRUD 表格列可配置
|
||||
|
||||
**文件:** `PluginCRUDPage.tsx`
|
||||
|
||||
改造方案:
|
||||
- manifest page 新增 `table_columns: ["name", "customer_type", "level", "status", "owner_id"]`
|
||||
- 不声明则默认取前 8 个非 hidden 非 FK 字段
|
||||
- 移除 `fields.slice(0, 5)` 硬编码
|
||||
|
||||
### 验证标准
|
||||
|
||||
> 换成 inventory 插件,Dashboard/Graph/CRUD 应该零改动正确渲染。
|
||||
|
||||
---
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
| 文件 | 改动类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| `crates/erp-plugin/src/manifest.rs` | 修改 | 新增 relations + validation 解析 |
|
||||
| `crates/erp-plugin/src/validation.rs` | 新建 | 校验引擎 |
|
||||
| `crates/erp-plugin/src/data_service.rs` | 修改 | 集成级联策略 + 校验 |
|
||||
| `crates/erp-plugin/src/dynamic_table.rs` | 修改 | 安装时存储关系元数据 |
|
||||
| `crates/erp-plugin/src/handler/data_handler.rs` | 修改 | FK 校验错误格式 |
|
||||
| `crates/erp-plugin-crm/plugin.toml` | 修改 | 补充 relations + validation |
|
||||
| `apps/web/src/pages/dashboard/dashboardConstants.tsx` | 修改 | 去硬编码,通用调色板 |
|
||||
| `apps/web/src/pages/dashboard/DashboardWidgets.tsx` | 修改 | schema 驱动 |
|
||||
| `apps/web/src/pages/PluginDashboardPage.tsx` | 修改 | 通用标题/副标题 |
|
||||
| `apps/web/src/pages/plugins/graph/graphConstants.ts` | 修改 | 关系类型从 options 读取 |
|
||||
| `apps/web/src/pages/PluginCRUDPage.tsx` | 修改 | 可配置列数 |
|
||||
| `apps/web/src/api/plugins.ts` | 修改 | 类型定义更新 |
|
||||
|
||||
---
|
||||
|
||||
## 验证方案
|
||||
|
||||
1. **编译检查**: `cargo check` 全 workspace 通过
|
||||
2. **单元测试**: validation.rs 每种校验器独立测试
|
||||
3. **集成测试**: Testcontainers 验证级联删除/FK 校验/unique 冲突
|
||||
4. **功能验证**:
|
||||
- 重新安装 CRM 插件,确认 5 个 relation 正确注册
|
||||
- 删除客户 → 联系人/沟通记录/标签级联软删除
|
||||
- 创建联系人 → customer_id 不存在时返回 400
|
||||
- 手机号/邮箱格式不正确时返回校验错误
|
||||
- Dashboard 切换 inventory 插件时正确渲染
|
||||
5. **前端验证**: `pnpm dev` 启动后手动测试所有页面
|
||||
|
||||
---
|
||||
|
||||
## 输出产物
|
||||
|
||||
1. 设计规格文档: `docs/superpowers/specs/2026-04-18-crm-plugin-platform-p0-design.md`
|
||||
2. 实施计划: 通过 writing-plans skill 生成
|
||||
3. 知识库文档: 记录讨论过程和决策理由
|
||||
@@ -1,269 +0,0 @@
|
||||
# Phase 2: 身份与权限模块实施计划
|
||||
|
||||
## Context
|
||||
|
||||
Phase 1 基础设施已完成(workspace、core types、EventBus、ErpModule trait、AppState、health check、graceful shutdown)。现在需要构建 Phase 2 身份与权限模块,这是所有后续模块的基础——工作流、消息、配置都依赖认证和权限中间件。
|
||||
|
||||
**目标**:实现完整的用户认证(JWT)、RBAC 权限模型、多租户中间件、用户/角色/组织管理 CRUD,以及对应的前端页面。
|
||||
|
||||
**范围界定**:Phase 2 仅实现用户名/密码认证 + RBAC。OAuth/SSO/TOTP/ABAC 延后到后续 Phase。
|
||||
|
||||
---
|
||||
|
||||
## 关键决策
|
||||
|
||||
1. **ErpModule trait 路由问题**:当前 trait `register_routes` 使用 `Router`(无状态泛型),但实际路由需要 `Router<AppState>`。Phase 2 采用**务实方案**:模块暴露独立 `routes() -> Router<AppState>` 函数,server 手动 merge。不改动 trait,避免核心层依赖 server 类型。
|
||||
2. **Token 存储**:JWT 的 SHA-256 哈希存入 `user_tokens` 表,支持吊销。前端用 localStorage 存储 access/refresh token(httpOnly cookie 为 Phase 6 优化项)。
|
||||
3. **中间件方案**:使用 `axum::middleware::from_fn_with_state` 实现 JWT 认证中间件,将 `TenantContext` 注入 `req.extensions()`。
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 数据库迁移(10 张表)
|
||||
|
||||
**目标**:创建所有 Auth 相关的数据库表。
|
||||
|
||||
**创建文件**(`crates/erp-server/migration/src/`):
|
||||
- `m20260411_000002_create_users.rs`
|
||||
- `m20260411_000003_create_user_credentials.rs`
|
||||
- `m20260411_000004_create_user_tokens.rs`
|
||||
- `m20260411_000005_create_roles.rs`
|
||||
- `m20260411_000006_create_permissions.rs`
|
||||
- `m20260411_000007_create_role_permissions.rs`
|
||||
- `m20260411_000008_create_user_roles.rs`
|
||||
- `m20260411_000009_create_organizations.rs`
|
||||
- `m20260411_000010_create_departments.rs`
|
||||
- `m20260411_000011_create_positions.rs`
|
||||
|
||||
**修改文件**:`migration/src/lib.rs` 注册所有新迁移
|
||||
|
||||
**表结构要点**:
|
||||
- 所有表含标准字段:id(UUID PK), tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version
|
||||
- **users**: username(VARCHAR, 复合唯一 tenant_id+username), email, phone, display_name, avatar_url, status(CHECK active/disabled/locked), last_login_at
|
||||
- **user_credentials**: user_id FK, credential_type(CHECK password/oauth/sso), credential_data(JSONB), verified(bool)
|
||||
- **user_tokens**: user_id FK, token_hash(VARCHAR UNIQUE), token_type(CHECK access/refresh), expires_at, revoked_at, device_info
|
||||
- **roles**: name, code(复合唯一 tenant_id+code), description, is_system(bool)
|
||||
- **permissions**: code(复合唯一 tenant_id+code), name, resource, action, description
|
||||
- **role_permissions**: role_id + permission_id 复合 PK
|
||||
- **user_roles**: user_id + role_id 复合 PK
|
||||
- **organizations**: name, code, parent_id(自引用 FK), path(TEXT), level, sort_order
|
||||
- **departments**: org_id FK, name, code, parent_id(自引用), manager_id FK users, path, sort_order
|
||||
- **positions**: dept_id FK, name, code, level, sort_order
|
||||
|
||||
**验证**:`cargo run -p erp-server` → `\dt` 显示全部 11 张表
|
||||
|
||||
---
|
||||
|
||||
## Task 2: SeaORM Entity 定义
|
||||
|
||||
**目标**:为所有 Auth 表创建类型安全的 Entity/Model/Relation。
|
||||
|
||||
**创建文件**(`crates/erp-auth/src/entity/`):
|
||||
- `mod.rs`, `user.rs`, `user_credential.rs`, `user_token.rs`, `role.rs`, `permission.rs`, `role_permission.rs`, `user_role.rs`, `organization.rs`, `department.rs`, `position.rs`
|
||||
|
||||
**修改文件**:
|
||||
- `crates/erp-auth/Cargo.toml` — 添加 workspace 依赖:jsonwebtoken, argon2, validator, thiserror, utoipa, erp-common
|
||||
|
||||
**同时创建 DTO 文件**:`crates/erp-auth/src/dto.rs`
|
||||
- `LoginReq` { username, password } — #[derive(Validate)]
|
||||
- `LoginResp` { access_token, refresh_token, expires_in, user }
|
||||
- `RefreshReq` { refresh_token }
|
||||
- `CreateUserReq` { username, password, email?, phone?, display_name? }
|
||||
- `UpdateUserReq` { email?, phone?, display_name?, status? }
|
||||
- `UserResp`, `RoleResp`, `PermissionResp` 等
|
||||
- `CreateRoleReq`, `UpdateRoleReq`, `AssignPermissionsReq`, `AssignRolesReq`
|
||||
- 所有 DTO 添加 `#[derive(utoipa::ToSchema)]`
|
||||
|
||||
**验证**:`cargo check -p erp-auth` 通过
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 核心服务层(密码 + JWT + 认证)
|
||||
|
||||
**目标**:实现密码哈希、JWT 签发/验证、登录/刷新/登出逻辑。
|
||||
|
||||
**创建文件**(`crates/erp-auth/src/`):
|
||||
- `error.rs` — AuthError 枚举 + From<AuthError> for AppError
|
||||
- `service/mod.rs`
|
||||
- `service/password.rs` — Argon2 hash/verify
|
||||
- `service/token_service.rs` — JWT sign/validate, token DB CRUD
|
||||
- `service/auth_service.rs` — login/refresh/logout
|
||||
- `service/user_service.rs` — user CRUD
|
||||
|
||||
**修改文件**:`crates/erp-auth/src/lib.rs` — 声明模块
|
||||
|
||||
**关键实现**:
|
||||
- `password.rs`: `hash_password(plain) -> Result<String>`, `verify_password(plain, hash) -> Result<bool>`
|
||||
- `token_service.rs`:
|
||||
- JWT Claims: { sub: Uuid, tid: Uuid, roles: Vec<String>, permissions: Vec<String>, exp, iat, token_type: String }
|
||||
- `sign_access_token(user_id, tenant_id, roles, permissions, jwt_config) -> Result<String>`
|
||||
- `sign_refresh_token(user_id, tenant_id, db, jwt_config) -> Result<(String, Uuid)>` — 存 SHA-256 哈希到 DB
|
||||
- `validate_refresh_token(token, db, jwt_config) -> Result<(Uuid, Claims)>` — 检查吊销状态
|
||||
- `revoke_all_user_tokens(user_id, tenant_id, db)`
|
||||
- `auth_service.rs`:
|
||||
- `login(tenant_id, username, password, device_info, db, token_svc, event_bus)` → 查用户 → 验证密码 → 签发双 token → 更新 last_login_at → 发 user.login 事件
|
||||
- `refresh(refresh_token, db, token_svc)` → 验证 → 吊销旧 refresh → 签发新双 token
|
||||
- `logout(user_id, tenant_id, db, token_svc)` → 吊销全部 token
|
||||
- `user_service.rs`: CRUD,查询始终带 tenant_id 过滤 + deleted_at IS NULL
|
||||
|
||||
**验证**:`cargo check -p erp-auth` 通过,password hash/verify 单元测试通过
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Handler 路由 + AuthModule 注册
|
||||
|
||||
**目标**:创建 Axum handler,注册到 server。
|
||||
|
||||
**创建文件**(`crates/erp-auth/src/`):
|
||||
- `handler/mod.rs`
|
||||
- `handler/auth_handler.rs` — login/refresh/logout
|
||||
- `handler/user_handler.rs` — user CRUD + 角色分配
|
||||
- `module.rs` — AuthModule struct + ErpModule impl
|
||||
|
||||
**修改文件**:
|
||||
- `crates/erp-server/Cargo.toml` — 添加 `erp-auth.workspace = true`
|
||||
- `crates/erp-server/src/main.rs` — 注册 auth 模块路由
|
||||
- `crates/erp-server/src/config.rs` — 添加 `AuthConfig { super_admin_password }`
|
||||
- `crates/erp-server/config/default.toml` — 添加 `[auth]` 段
|
||||
|
||||
**路由设计**:
|
||||
- 公开(无需 JWT):`POST /api/v1/auth/login`, `POST /api/v1/auth/refresh`
|
||||
- 受保护(需 JWT):`POST /api/v1/auth/logout`, 全部 users/roles/permissions/orgs 路由
|
||||
|
||||
**main.rs 路由结构**:
|
||||
```
|
||||
Router::new()
|
||||
.merge(public_routes) // health + login + refresh
|
||||
.merge(protected_routes) // logout + user/role CRUD, 后续加 JWT 中间件
|
||||
.with_state(state)
|
||||
```
|
||||
|
||||
**验证**:server 启动,`POST /api/v1/auth/login` 返回 400(无 body)而非 404
|
||||
|
||||
---
|
||||
|
||||
## Task 5: JWT 认证中间件 + RBAC 权限检查
|
||||
|
||||
**目标**:JWT 验证中间件注入 TenantContext,RBAC 权限检查辅助函数。
|
||||
|
||||
**创建文件**(`crates/erp-auth/src/`):
|
||||
- `middleware/mod.rs`
|
||||
- `middleware/jwt_auth.rs` — `jwt_auth_middleware(State, Request, Next)` → 从 Bearer token 解码 Claims → 注入 TenantContext 到 extensions
|
||||
- `middleware/rbac.rs` — `require_permission(perm: &str, ctx: &TenantContext) -> AppResult<()>`
|
||||
|
||||
**修改文件**:
|
||||
- `crates/erp-server/src/main.rs` — public/protected 路由分离,protected 路由层加 JWT 中间件
|
||||
|
||||
**关键实现**:
|
||||
- 中间件用 `axum::middleware::from_fn_with_state(state, jwt_auth_middleware)`
|
||||
- 从 `Authorization: Bearer xxx` 提取 token → decode → 检查过期 → 注入 `TenantContext { tenant_id, user_id, roles, permissions }`
|
||||
- RBAC: 简单检查 `ctx.permissions.contains(&required_permission)`
|
||||
- login/refresh/health 路由跳过 JWT 中间件
|
||||
|
||||
**验证**:无 token 访问 `/api/v1/users` 返回 401;login/refresh 不受影响
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 租户初始化钩子 + 种子数据
|
||||
|
||||
**目标**:实现 `on_tenant_created` 创建默认角色/权限/管理员。
|
||||
|
||||
**创建文件**:`crates/erp-auth/src/service/seed.rs`
|
||||
|
||||
**修改文件**:
|
||||
- `crates/erp-auth/src/module.rs` — 实现 `on_tenant_created`
|
||||
- `crates/erp-server/src/main.rs` — server 启动时自动创建默认租户(开发用)
|
||||
|
||||
**种子数据**:
|
||||
- 20 个默认权限(user:crud, role:crud, permission:read, organization:crud, department:crud, position:crud)
|
||||
- "admin" 角色(is_system=true)绑定所有权限
|
||||
- "viewer" 角色(is_system=true)绑定只读权限
|
||||
- super admin 用户(username="admin",密码从配置读取)
|
||||
|
||||
**验证**:启动后查询 DB 有 admin 用户、admin/viewer 角色、20 个权限
|
||||
|
||||
---
|
||||
|
||||
## Task 7: 前端登录页 + Auth Store + API 层
|
||||
|
||||
**目标**:登录 UI、token 管理、路由守卫。
|
||||
|
||||
**创建文件**(`apps/web/src/`):
|
||||
- `api/client.ts` — axios 实例 + 请求/响应拦截器(附加 token、401 自动 refresh)
|
||||
- `api/auth.ts` — login/refresh/logout API 调用
|
||||
- `api/users.ts` — 用户 CRUD API
|
||||
- `stores/auth.ts` — auth Zustand store(user, permissions, tokens, login/logout)
|
||||
- `pages/Login.tsx` — 登录表单页
|
||||
- `pages/Home.tsx` — 首页(从 App.tsx 抽取)
|
||||
|
||||
**修改文件**:
|
||||
- `apps/web/src/App.tsx` — 添加 PrivateRoute 守卫、/login 路由
|
||||
- `apps/web/src/stores/app.ts` — 移除 isLoggedIn stub(由 auth store 接管)
|
||||
|
||||
**验证**:打开 `/#/` 自动跳转 `/login`,输入 admin/Admin@2026 登录后跳转到首页,刷新保持登录状态
|
||||
|
||||
---
|
||||
|
||||
## Task 8: 角色/权限管理
|
||||
|
||||
**目标**:角色 CRUD + 权限分配后端 + 前端页面。
|
||||
|
||||
**创建文件**:
|
||||
- Backend: `crates/erp-auth/src/service/role_service.rs`, `service/permission_service.rs`, `handler/role_handler.rs`, `handler/permission_handler.rs`
|
||||
- Frontend: `apps/web/src/pages/Roles.tsx`, `apps/web/src/api/roles.ts`
|
||||
|
||||
**修改文件**:`module.rs` 注册新路由, `App.tsx` 添加路由
|
||||
|
||||
**验证**:admin 登录 → 角色管理 → 创建角色 → 分配权限 → DB 验证
|
||||
|
||||
---
|
||||
|
||||
## Task 9: 组织/部门/岗位管理
|
||||
|
||||
**目标**:树形组织架构管理。
|
||||
|
||||
**创建文件**:
|
||||
- Backend: `service/org_service.rs`, `service/dept_service.rs`, `service/position_service.rs`, `handler/org_handler.rs`, `handler/dept_handler.rs`, `handler/position_handler.rs`
|
||||
- Frontend: `apps/web/src/pages/Organizations.tsx`, `apps/web/src/api/orgs.ts`
|
||||
|
||||
**关键**:树形结构用 parent_id + path 列实现祖先查询
|
||||
|
||||
**验证**:创建根组织 → 子组织 → 部门 → 岗位,验证 path 正确
|
||||
|
||||
---
|
||||
|
||||
## Task 10: 用户管理页面 + 整合
|
||||
|
||||
**目标**:完整的用户 CRUD 界面 + 角色分配 + 主布局用户信息。
|
||||
|
||||
**创建文件**:
|
||||
- `apps/web/src/pages/Users.tsx` — Ant Design Table + 创建/编辑/角色分配 Modal
|
||||
- `apps/web/src/layouts/MainLayout.tsx` — 更新:显示当前用户名、登出菜单
|
||||
|
||||
**验证**:admin 登录 → 用户管理 → 创建用户 → 分配角色 → 禁用/启用 → 所有操作即时反映
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
Task 1 (迁移) → Task 2 (Entity) → Task 3 (服务层) → Task 4 (Handler+注册)
|
||||
↓
|
||||
Task 5 (JWT 中间件)
|
||||
Task 6 (种子数据)
|
||||
↓
|
||||
Task 7 (前端登录)
|
||||
↓
|
||||
Task 8 (角色权限) Task 9 (组织部门)
|
||||
↓
|
||||
Task 10 (用户管理 UI)
|
||||
```
|
||||
|
||||
## 验证标准
|
||||
|
||||
- [ ] `cargo check --workspace` 零错误零警告
|
||||
- [ ] `cargo test --workspace` 全部通过
|
||||
- [ ] server 启动后可 login → 获得 JWT → 带 JWT 访问 /api/v1/users
|
||||
- [ ] 无 JWT 访问受保护端点返回 401
|
||||
- [ ] 前端登录 → 跳转首页 → 刷新保持 → 登出清除
|
||||
- [ ] 用户/角色/组织 CRUD 页面功能完整
|
||||
- [ ] 多租户隔离:每个查询自动带 tenant_id 过滤
|
||||
@@ -1,169 +0,0 @@
|
||||
# CRM 插件审计修复计划
|
||||
|
||||
## Context
|
||||
|
||||
CRM 插件开发审计发现 4 个 CRITICAL、6 个 HIGH、7 个 MEDIUM 问题。本计划按优先级分 3 批修复,覆盖后端 Rust 和前端 React 两端。
|
||||
|
||||
## Batch 1 — CRITICAL(阻断性,不修功能不可用)
|
||||
|
||||
### Fix 1: 后端权限 SQL 参数化
|
||||
**文件**: `crates/erp-plugin/src/service.rs` (第 578-714 行)
|
||||
|
||||
- `register_plugin_permissions()`: 将 `format!()` + `Statement::from_string()` 改为 `Statement::from_sql_and_values()` + 参数化占位符 `$1, $2, ...`
|
||||
- `unregister_plugin_permissions()`: 同上,两段 UPDATE 都参数化
|
||||
- 需要新增一个辅助函数 `resolve_manifest_id(plugin_id: Uuid, db) -> AppResult<String>` 从 plugins 表查 manifest_json 获取 metadata.id(供 Fix 2 使用)
|
||||
|
||||
### Fix 2: 后端权限码使用 manifest_id 而非 UUID
|
||||
**文件**: `crates/erp-plugin/src/data_handler.rs` (第 19-25 行, 第 49/107/145/177/211/256/301 行)
|
||||
|
||||
- 每个 handler 先从 DB 查 manifest_id:`let manifest_id = resolve_manifest_id(plugin_id, &state.db).await?;`
|
||||
- `compute_permission_code` 改为接受 manifest_id
|
||||
- 添加 `resolve_manifest_id` 辅助函数:查 plugins 表 → 解析 manifest_json → 提取 metadata.id
|
||||
- 考虑在 PluginState 中缓存 manifest_id 映射(或直接在 data_service 层缓存)
|
||||
|
||||
**实现**: 将 `resolve_manifest_id` 放在 `data_service.rs` 中(与 `resolve_table_name` 同级),handler 调用它。
|
||||
|
||||
### Fix 3: 4 个路由页面组件自行加载 schema
|
||||
**文件**: 4 个页面组件 + `api/plugins.ts` + `api/pluginData.ts`
|
||||
|
||||
统一模式:每个组件通过 `useParams()` 获取路由参数,内部调用 `getPluginSchema()` 加载 schema,从 schema 中提取所需数据。
|
||||
|
||||
**3a. PluginTabsPage.tsx**:
|
||||
- 移除所有 props(pluginId/label/tabs/entities),改用 `useParams<{ pluginId: string; pageLabel: string }>()`
|
||||
- 内部调用 `getPluginSchema(pluginId)` 获取 schema
|
||||
- 从 `schema.ui.pages` 中找到 type='tabs' 且 label 匹配 pageLabel 的页面
|
||||
- 替换 `require()` 为顶层 ES `import`
|
||||
- 移除不存在的 `enableSearch` prop 传递
|
||||
|
||||
**3b. PluginTreePage.tsx**:
|
||||
- 移除 props(pluginId/entity/idField/parentField/labelField/fields),改用 `useParams<{ pluginId: string; entityName: string }>()`
|
||||
- 从 schema 加载 entity 字段和页面配置(tree 页面的 id_field/parent_field/label_field)
|
||||
|
||||
**3c. PluginGraphPage.tsx**:
|
||||
- 移除所有 props,改用 `useParams<{ pluginId: string; entityName: string }>()`
|
||||
- 从 schema 中找到 type='graph' 的页面配置
|
||||
|
||||
**3d. PluginDashboardPage.tsx**:
|
||||
- 移除 props(pluginId/entities),改用 `useParams<{ pluginId: string }>()`
|
||||
- 内部调用 `getPluginSchema(pluginId)` 获取所有 entities
|
||||
|
||||
### Fix 4: PluginPageSchema 补充 graph/dashboard 类型
|
||||
**文件**: `apps/web/src/api/plugins.ts` (第 154-158 行)
|
||||
|
||||
扩展 union type:
|
||||
```typescript
|
||||
| { type: 'graph'; entity: string; label: string; relationship_entity: string; source_field: string; target_field: string; edge_label_field: string; node_label_field: string }
|
||||
| { type: 'dashboard'; label: string }
|
||||
```
|
||||
|
||||
### Fix 5: api/pluginData.ts 补充 count/aggregate API
|
||||
**文件**: `apps/web/src/api/pluginData.ts`
|
||||
|
||||
新增两个函数:
|
||||
- `countPluginData(pluginId, entity, options?)` → `GET /plugins/{id}/{entity}/count`
|
||||
- `aggregatePluginData(pluginId, entity, groupBy, filter?)` → `GET /plugins/{id}/{entity}/aggregate`
|
||||
|
||||
## Batch 2 — HIGH(稳定性和正确性)
|
||||
|
||||
### Fix 6: AbortController 防竞态
|
||||
**文件**: 所有 5 个页面组件的 useEffect
|
||||
|
||||
为数据加载 useEffect 添加 AbortController:
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
// ... async loadData 中检查 abortController.signal.aborted
|
||||
return () => abortController.abort();
|
||||
}, [deps]);
|
||||
```
|
||||
|
||||
注意:当前 api client (axios) 不支持 AbortSignal 透传,简单方案是在 setState 前检查 `abortController.signal.aborted` 或使用一个 `mounted` flag。
|
||||
|
||||
### Fix 7: Dashboard 改用后端 aggregate API
|
||||
**文件**: `apps/web/src/pages/PluginDashboardPage.tsx`
|
||||
|
||||
- 使用 `countPluginData()` 获取总数
|
||||
- 使用 `aggregatePluginData()` 获取分组统计
|
||||
- 移除全量循环加载逻辑
|
||||
- 保留 fallback:如果 aggregate API 失败,显示总数 + 提示
|
||||
|
||||
### Fix 8: fetchData 双重请求修复
|
||||
**文件**: `apps/web/src/pages/PluginCRUDPage.tsx` (第 501-511 行)
|
||||
|
||||
搜索操作改为直接传参模式:
|
||||
```typescript
|
||||
onSearch={(value) => {
|
||||
setSearchText(value);
|
||||
setPage(1);
|
||||
fetchData(1, { search: value }); // 直接传参
|
||||
}}
|
||||
```
|
||||
修改 `fetchData` 签名允许覆盖搜索参数。
|
||||
|
||||
### Fix 9: Canvas 高 DPI 支持
|
||||
**文件**: `apps/web/src/pages/PluginGraphPage.tsx` (第 114-118 行)
|
||||
|
||||
```typescript
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
ctx.scale(dpr, dpr);
|
||||
```
|
||||
|
||||
## Batch 3 — MEDIUM(建议修复)
|
||||
|
||||
### Fix 10: 服务端排序替代前端排序
|
||||
**文件**: `apps/web/src/pages/PluginCRUDPage.tsx`
|
||||
|
||||
- Table 的 `onChange` 回调捕获 sortField/sortOrder
|
||||
- 传给 `fetchData` 作为 `sort_by`/`sort_order` 参数
|
||||
- 移除列定义中的 `sorter: true`
|
||||
|
||||
### Fix 11: Canvas 暗色主题支持
|
||||
**文件**: `apps/web/src/pages/PluginGraphPage.tsx`
|
||||
|
||||
从 CSS 变量读取主题色:
|
||||
```typescript
|
||||
const style = getComputedStyle(canvas);
|
||||
const textColor = style.getPropertyValue('--antd-color-text') || '#333';
|
||||
const lineColor = style.getPropertyValue('--antd-color-border') || '#999';
|
||||
```
|
||||
|
||||
### Fix 12: schema 加载失败提示用户
|
||||
**文件**: 所有页面组件的 `.catch(() => {})`
|
||||
|
||||
替换为 `message.warning('Schema 加载失败,部分功能不可用')`
|
||||
|
||||
### Fix 13: 后端 data_service 缓存优化
|
||||
**文件**: `crates/erp-plugin/src/data_service.rs`
|
||||
|
||||
合并 `resolve_table_name` 和 `resolve_entity_fields` 为一个函数 `resolve_entity_info()`,减少数据库查询次数。
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
| 文件 | 修改类型 |
|
||||
|------|---------|
|
||||
| `crates/erp-plugin/src/service.rs` | SQL 参数化 |
|
||||
| `crates/erp-plugin/src/data_handler.rs` | manifest_id 查找 |
|
||||
| `crates/erp-plugin/src/data_service.rs` | resolve_manifest_id + resolve_entity_info 缓存 |
|
||||
| `apps/web/src/api/plugins.ts` | PluginPageSchema 扩展 |
|
||||
| `apps/web/src/api/pluginData.ts` | count/aggregate API |
|
||||
| `apps/web/src/pages/PluginTabsPage.tsx` | 自加载 schema |
|
||||
| `apps/web/src/pages/PluginTreePage.tsx` | 自加载 schema |
|
||||
| `apps/web/src/pages/PluginGraphPage.tsx` | 自加载 schema + DPI + 暗色 |
|
||||
| `apps/web/src/pages/PluginDashboardPage.tsx` | 自加载 schema + 后端 aggregate |
|
||||
| `apps/web/src/pages/PluginCRUDPage.tsx` | 搜索/排序修复 |
|
||||
|
||||
## 验证计划
|
||||
|
||||
1. `cargo check --workspace` 通过
|
||||
2. `cargo test --workspace` 通过
|
||||
3. `cd apps/web && pnpm build` 通过(验证 require → import 修复)
|
||||
4. 手动验证:
|
||||
- 侧边栏点击 CRM tabs 菜单 → 页面正常渲染
|
||||
- CRUD 页面搜索/筛选/排序正常
|
||||
- Tree 页面展示树形结构
|
||||
- Graph 页面渲染图谱(高 DPI 清晰)
|
||||
- Dashboard 页面显示统计
|
||||
@@ -1,90 +0,0 @@
|
||||
# 审计问题修复计划(按优先级)
|
||||
|
||||
## Context
|
||||
|
||||
2026-04-18 系统全面审计发现多个问题(详见 `docs/audit-2026-04-18.md`)。当前系统因 Redis 未安装且限流策略为 fail-closed,**所有 API 请求返回 429**,系统完全不可用。本计划按优先级逐步修复,恢复系统可用性。
|
||||
|
||||
---
|
||||
|
||||
## Fix 1: 限流中间件改为 fail-open(P0-3)
|
||||
|
||||
**问题**: Redis 未安装时,fail-closed 策略拒绝所有请求,系统完全不可用。
|
||||
|
||||
**文件**: `crates/erp-server/src/middleware/rate_limit.rs`
|
||||
|
||||
**改动**:
|
||||
- `apply_rate_limit()` 中 3 处 Redis 不可达时的处理,从返回 429 改为放行(调用 `next.run(req).await`)
|
||||
- 仅保留 tracing::warn 日志,不阻断业务
|
||||
- 超限计数本身仍按原逻辑:Redis 可达时正常限流,不可达时放行
|
||||
|
||||
**涉及行**: 约第 121-129 行、第 138-147 行、第 151-158 行
|
||||
|
||||
---
|
||||
|
||||
## Fix 2: 插件权限自动分配给 admin 角色(P0-1)
|
||||
|
||||
**问题**: 插件安装时注册权限到 `permissions` 表,但不分配给 admin 角色,导致 JWT 中缺少插件权限码,所有插件数据页面返回 403。
|
||||
|
||||
**文件**: `crates/erp-plugin/src/service.rs`
|
||||
|
||||
**改动**:
|
||||
|
||||
1. 新增 `grant_permissions_to_admin()` 函数(约第 766 行 `register_plugin_permissions` 之后):
|
||||
- 查询 admin 角色 ID
|
||||
- 查询当前插件的所有权限 ID(按 manifest_id 前缀匹配)
|
||||
- INSERT INTO role_permissions,跳过已存在的记录(ON CONFLICT DO NOTHING)
|
||||
- 参考 `crates/erp-auth/src/service/seed.rs` 中的 SQL 模式
|
||||
|
||||
2. 在 `install()` 函数(约第 81 行)中,`register_plugin_permissions()` 调用之后,调用 `grant_permissions_to_admin()`
|
||||
|
||||
3. 在 `enable()` 函数(约第 191 行)中也调用 `grant_permissions_to_admin()`,确保启用时权限也已分配
|
||||
|
||||
**参考文件**:
|
||||
- `crates/erp-auth/src/service/seed.rs` — 已有的 admin 权限授予模式
|
||||
- `crates/erp-server/migration/src/m20260417_000034_seed_plugin_permissions.rs` — 迁移中的 SQL 模式
|
||||
- `crates/erp-plugin/src/handler/data_handler.rs:75` — `compute_permission_code()` 权限码格式
|
||||
|
||||
---
|
||||
|
||||
## Fix 3: CRM 插件 WASM 数据修复(P0-2)
|
||||
|
||||
**问题**: 数据库中存储的 CRM 插件 WASM 是错误的测试插件二进制(110KB),而非真正的 CRM 插件(~22KB)。导致服务器重启后插件恢复失败。
|
||||
|
||||
**步骤**:
|
||||
1. 重新编译 CRM 插件 WASM Component
|
||||
2. 通过插件升级 API 上传正确的二进制
|
||||
3. 验证插件恢复成功
|
||||
|
||||
**命令**:
|
||||
```bash
|
||||
cargo build -p erp-plugin-crm --target wasm32-unknown-unknown --release
|
||||
wasm-tools component new target/wasm32-unknown-unknown/release/erp_plugin_crm.wasm -o target/erp_plugin_crm.component.wasm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fix 4: `/roles/permissions` 路由冲突(P1)
|
||||
|
||||
**问题**: `GET /api/v1/roles/permissions` 被 `GET /api/v1/roles/{id}` 路由拦截,`permissions` 被当作 UUID 解析失败。
|
||||
|
||||
**文件**: `crates/erp-auth/src/module.rs`
|
||||
|
||||
**改动**:
|
||||
- 在 `register_routes()` 中(约第 64-82 行),将 `/roles/permissions` 精确匹配路由放在 `:id` 参数路由**之前**
|
||||
- Axum 路由匹配按注册顺序,精确路径优先于参数路径
|
||||
|
||||
---
|
||||
|
||||
## 验证计划
|
||||
|
||||
每个 Fix 完成后独立验证:
|
||||
|
||||
1. **Fix 1 验证**: 启动后端,`curl http://localhost:3000/api/v1/health` 返回 200(不再 429)
|
||||
2. **Fix 2 验证**: 重新安装/启用 CRM 插件,用 admin 登录后访问 CRM 数据页面不再 403
|
||||
3. **Fix 3 验证**: 重启后端,日志显示 `Plugins recovered: 1`(而非 0)
|
||||
4. **Fix 4 验证**: `curl -H "Authorization: Bearer <token>" http://localhost:3000/api/v1/roles/permissions` 返回权限列表(非 UUID 错误)
|
||||
|
||||
**最终验证**:
|
||||
- `cargo check` 编译通过
|
||||
- `cargo test --workspace` 全部通过
|
||||
- 前端页面正常访问,CRM 数据可操作
|
||||
@@ -1,277 +0,0 @@
|
||||
# ERP 平台系统性功能审计计划
|
||||
|
||||
## Context
|
||||
|
||||
ERP 平台底座 Phase 1-6 全部标记完成,包含 7 个 Rust crate、31 个数据库迁移、1 个 React SPA 前端。在进入下一阶段(行业模块插接)之前,需要系统性审计验证所有已实现功能的完整性、一致性和可用性,确保底座稳固可靠。
|
||||
|
||||
---
|
||||
|
||||
## 审计发现总览
|
||||
|
||||
| 类别 | 严重程度 | 数量 |
|
||||
|------|---------|------|
|
||||
| 死代码/未使用模块 | P2 | 4 项 |
|
||||
| 事件总线断线 | P1 | 5 项 |
|
||||
| 前后端不一致 | P2 | 4 项 |
|
||||
| 规格未实现功能 | P2-P3 | 8 项 |
|
||||
| 安全隐患 | P1 | 3 项 |
|
||||
| 架构缺陷 | P2 | 2 项 |
|
||||
|
||||
---
|
||||
|
||||
## 阶段 A: 生产阻塞问题 (P1)
|
||||
|
||||
### A1. 修复登录端点无限流保护
|
||||
|
||||
**问题**: 公共路由 (`/auth/login`, `/auth/refresh`) 未应用 `rate_limit_by_ip` 中间件,只有受保护路由有 `rate_limit_by_user`。登录接口可被暴力破解。
|
||||
|
||||
**修改文件**:
|
||||
- [main.rs](crates/erp-server/src/main.rs) — 将 `rate_limit_by_ip` 中间件应用到 `public_routes`
|
||||
|
||||
**验证**: 使用 `curl` 快速发送 20 次登录请求,第 11 次起应返回 429。
|
||||
|
||||
### A2. 补全工作流实例状态变更事件
|
||||
|
||||
**问题**: 实例完成 (`completed`)、挂起 (`suspended`)、恢复 (`resumed`)、终止 (`terminated`) 时未发布领域事件。只有 `process_instance.started` 被发布。
|
||||
|
||||
**修改文件**:
|
||||
- [instance_service.rs](crates/erp-workflow/src/service/instance_service.rs) — 在 `change_status()` 方法中补全事件发布
|
||||
- [flow_executor.rs](crates/erp-workflow/src/engine/flow_executor.rs) — 在 `check_instance_completion()` 中发布 `process_instance.completed`
|
||||
|
||||
**验证**: 启动流程实例 → 挂起 → 恢复 → 完成,检查 `domain_events` 表应有 4 条对应事件。
|
||||
|
||||
### A3. 消除硬编码默认租户 ID
|
||||
|
||||
**问题**: [state.rs:49](crates/erp-server/src/state.rs#L49) 使用 nil UUID 作为 `default_tenant_id`,[auth_handler.rs:30](crates/erp-auth/src/handler/auth_handler.rs#L30) 在登录时直接使用。实际租户是 UUID v7,nil UUID 不对应任何真实租户。
|
||||
|
||||
**修改文件**:
|
||||
- [state.rs](crates/erp-server/src/state.rs) — 从数据库或配置读取真实的默认租户 ID
|
||||
- [auth_handler.rs](crates/erp-auth/src/handler/auth_handler.rs) — 支持动态租户解析
|
||||
|
||||
**验证**: 启动服务后检查日志,确认 `default_tenant_id` 为种子数据中的实际租户 ID。
|
||||
|
||||
### A4. 实现审计日志查询 API
|
||||
|
||||
**问题**: 43 处审计日志写入覆盖所有 CRUD 操作,但无任何读取接口。`audit_logs` 表数据不可访问。
|
||||
|
||||
**新增文件**:
|
||||
- `crates/erp-core/src/handler/audit_handler.rs` — 分页查询处理器
|
||||
- 在 [main.rs](crates/erp-server/src/main.rs) 注册 `GET /api/v1/audit-logs` 路由
|
||||
|
||||
**查询参数**: `resource_type`, `user_id`, `from`, `to`, `page`, `page_size`
|
||||
|
||||
**验证**: 通过 API 查询审计日志,返回分页结果。
|
||||
|
||||
### A5. 修复 CORS 生产环境配置
|
||||
|
||||
**问题**: 默认配置允许 `"*"` 来源,生产环境不安全。
|
||||
|
||||
**修改文件**:
|
||||
- [main.rs](crates/erp-server/src/main.rs) — 在 CORS 为 `"*"` 且非开发模式时发出警告或拒绝启动
|
||||
|
||||
---
|
||||
|
||||
## 阶段 B: 功能完整性修复 (P2)
|
||||
|
||||
### B1. 清理 erp-common 死代码 crate
|
||||
|
||||
**问题**: `erp-common` crate 导出了 4 个工具函数,但全代码库中零引用 (`use erp_common` 无匹配)。`erp-server` 和 `erp-auth` 的 `Cargo.toml` 声明了依赖但从未使用。
|
||||
|
||||
**操作**:
|
||||
1. 从根 `Cargo.toml` 移除 workspace member
|
||||
2. 从 `erp-server/Cargo.toml` 和 `erp-auth/Cargo.toml` 移除依赖
|
||||
3. 删除 `crates/erp-common/` 目录
|
||||
4. `cargo build` 验证
|
||||
|
||||
### B2. 修复前端 API 层绕行问题
|
||||
|
||||
**问题**: 5 个设置子页面 (DictionaryManager, MenuConfig, NumberingRules, SystemSettings, ThemeSettings) 和 NotificationPreferences 直接调用 `client.get/put`,绕过了已存在的类型化 API 模块。导致 `api/` 目录下多个导出成为死代码。
|
||||
|
||||
**死代码清单**:
|
||||
- `api/errors.ts` — `extractErrorMessage()` 从未被导入
|
||||
- `api/dictionaries.ts` — `listItemsByCode()` 从未被导入
|
||||
- `api/menus.ts` — `getMenus()`, `batchSaveMenus()` 从未被导入
|
||||
- `api/settings.ts` — `getSetting()`, `updateSetting()` 从未被导入
|
||||
- `api/numberingRules.ts` — 所有导出函数从未被导入
|
||||
|
||||
**操作**: 重构所有页面使用对应的 `api/` 模块函数,删除未使用的直接调用。
|
||||
|
||||
### B3. 添加流程实例恢复/挂起按钮
|
||||
|
||||
**问题**: 后端有 `POST /instances/{id}/suspend` 和 `POST /instances/{id}/resume`,但前端 InstanceMonitor 只有"终止"按钮。`workflowInstances.ts` 导出了 `suspendInstance` 但没有 `resumeInstance`。
|
||||
|
||||
**修改文件**:
|
||||
- [workflowInstances.ts](apps/web/src/api/workflowInstances.ts) — 添加 `resumeInstance()`
|
||||
- [InstanceMonitor.tsx](apps/web/src/pages/workflow/InstanceMonitor.tsx) — 根据状态显示挂起/恢复按钮
|
||||
|
||||
### B4. 消除 EventHandler 死 trait
|
||||
|
||||
**问题**: `EventHandler` trait 定义在 [events.rs](crates/erp-core/src/events.rs) 但全代码库零实现 (`impl EventHandler` 无匹配)。所有模块的 `register_event_handlers()` 方法体为空。消息模块通过独立的 `start_event_listener()` 静态方法处理事件。
|
||||
|
||||
**操作**: 两种方案二选一:
|
||||
- **方案 A (推荐)**: 删除 `EventHandler` trait,让 `register_event_handlers()` 接收 `&EventBus` 引用,各模块自行订阅
|
||||
- **方案 B**: 实际在消息模块实现该 trait,作为示范
|
||||
|
||||
### B5. 补全任务完成通知
|
||||
|
||||
**问题**: 消息模块收到 `task.completed` 事件后跳过处理(仅输出 debug 日志)。工作流任务完成后无通知。
|
||||
|
||||
**修改文件**:
|
||||
- [module.rs](crates/erp-message/src/module.rs) — 在 `handle_workflow_event()` 中处理 `task.completed`
|
||||
|
||||
### B6. 接线 TimeoutChecker 后台任务
|
||||
|
||||
**问题**: [timeout.rs](crates/erp-workflow/src/engine/timeout.rs) 实现了 `TimeoutChecker::find_overdue_tasks()` 但从未被调用。无后台定时任务检查超时。
|
||||
|
||||
**修改文件**:
|
||||
- [main.rs](crates/erp-server/src/main.rs) — 添加定时调用 TimeoutChecker 的后台任务(参考 outbox relay 模式)
|
||||
|
||||
### B7. 处理 ServiceTask 节点
|
||||
|
||||
**问题**: [flow_executor.rs](crates/erp-workflow/src/engine/flow_executor.rs) 遇到 ServiceTask 节点时直接返回错误 "ServiceTask not yet implemented",导致包含 ServiceTask 的流程无法运行。
|
||||
|
||||
**操作**: 两种方案二选一:
|
||||
- **方案 A**: 实现 HTTP 调用类型的 ServiceTask
|
||||
- **方案 B**: 在设计器中禁止放置 ServiceTask 节点,并在引擎中给出更友好的错误提示
|
||||
|
||||
### B8. 修复 ErpModule trait 的 register_routes() 空实现
|
||||
|
||||
**问题**: 所有 4 个模块的 `register_routes()` 都原样返回传入的 Router。实际路由通过 `public_routes()` / `protected_routes()` 静态方法注册。`ModuleRegistry::build_router()` 调用 trait 方法但无效。
|
||||
|
||||
**修改文件**:
|
||||
- [module.rs](crates/erp-core/src/module.rs) — 重新设计 trait 接口,使 `register_routes()` 有实际作用,或删除并改用静态方法
|
||||
|
||||
---
|
||||
|
||||
## 阶段 C: 规格合规补全 (P2-P3)
|
||||
|
||||
### C1. 实现审计日志前端页面
|
||||
|
||||
**依赖**: A4 (审计日志查询 API)
|
||||
|
||||
**新增文件**:
|
||||
- `apps/web/src/api/auditLogs.ts`
|
||||
- `apps/web/src/pages/settings/AuditLogViewer.tsx`
|
||||
- 在 Settings.tsx 添加"审计日志"标签页
|
||||
|
||||
### C2. 实现语言管理前端页面
|
||||
|
||||
**问题**: 后端有 `GET /config/languages` 和 `PUT /config/languages/{code}`,无前端。
|
||||
|
||||
**新增文件**:
|
||||
- `apps/web/src/api/languages.ts`
|
||||
- `apps/web/src/pages/settings/LanguageManager.tsx`
|
||||
- 在 Settings.tsx 添加"语言管理"标签页
|
||||
|
||||
### C3. 创建 Theme API 模块
|
||||
|
||||
**问题**: ThemeSettings.tsx 直接调用 `client`,无 `api/themes.ts` 模块。
|
||||
|
||||
**新增文件**:
|
||||
- `apps/web/src/api/themes.ts`
|
||||
- 重构 ThemeSettings.tsx 使用该模块
|
||||
|
||||
### C4. 评估 JWT 存储安全
|
||||
|
||||
**问题**: 规格要求 "httpOnly cookie (web)",实际使用 localStorage。XSS 可窃取 token。
|
||||
|
||||
**操作**: 评估迁移到 httpOnly cookie 的可行性,或文档化安全权衡。
|
||||
|
||||
### C5. WebSocket 实时推送 (P3)
|
||||
|
||||
**问题**: 规格要求 `WS /ws/v1/messages` 实时推送,实际使用 HTTP 轮询 (60s 间隔)。无后端 WebSocket 端点,无前端 WebSocket 客户端。
|
||||
|
||||
**操作**: 实现基础 WebSocket 升级 + JWT 认证 + 前端连接。
|
||||
|
||||
### C6. 全局搜索 (P3)
|
||||
|
||||
**问题**: 规格要求顶部导航栏搜索框,实际未实现。
|
||||
|
||||
### C7. 多标签页切换 (P3)
|
||||
|
||||
**问题**: 规格要求浏览器式多标签页,实际使用单页路由。
|
||||
|
||||
### C8. 浏览器通知 (P3)
|
||||
|
||||
**问题**: 规格要求 Web Notification API 集成,未实现。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 D: 端到端验证测试
|
||||
|
||||
### D1. 用户生命周期 E2E
|
||||
|
||||
注册 → 分配角色 → 登录 → 执行操作 → 验证审计日志 → 删除用户
|
||||
|
||||
**检查点**: Argon2 哈希、access token TTL、refresh 轮换、软删除
|
||||
|
||||
### D2. 审批流程 E2E
|
||||
|
||||
创建定义 → 发布 → 启动实例 → 完成首任务 → 条件分支 → 第二任务 → 完成
|
||||
|
||||
**检查点**: 表达式求值、并行网关 fork/join、任务委派、流程变量
|
||||
|
||||
### D3. 多租户隔离验证
|
||||
|
||||
创建两个租户 → 各创建用户 → 验证数据隔离
|
||||
|
||||
**检查点**: 所有查询含 `tenant_id`、中间件注入正确、跨租户访问被拒
|
||||
|
||||
### D4. 通知流程 E2E
|
||||
|
||||
启动流程实例 → 验证消息创建 → 标记已读 → 验证未读计数
|
||||
|
||||
**检查点**: 模板渲染、订阅偏好、未读计数、全部标记已读
|
||||
|
||||
---
|
||||
|
||||
## 5 种差距模式检测结果
|
||||
|
||||
| 模式 | 发现 | 示例 |
|
||||
|------|------|------|
|
||||
| 写了没接 | 6 处 | TimeoutChecker 实现但未接线、EventHandler trait 定义但未使用 |
|
||||
| 接了没传 | 3 处 | `task.completed` 事件有订阅但处理器跳过、`register_routes()` 被调用但所有模块返回空 |
|
||||
| 传了没存 | 0 处 | 未发现 |
|
||||
| 存了没用 | 2 处 | audit_logs 写入 43 处但无查询 API、`erp-common` crate 完整但从未引用 |
|
||||
| 双系统不同步 | 4 处 | 前端缺 resume 按钮后端有、前端缺语言管理后端有、前端 settings 页面绕过 api 模块、JWT 存储方式与规格不符 |
|
||||
|
||||
---
|
||||
|
||||
## 10 项审计清单结果
|
||||
|
||||
| # | 审计项 | 状态 | 说明 |
|
||||
|---|--------|------|------|
|
||||
| 1 | 代码存在性 | ⚠️ 部分缺失 | ServiceTask/TimeoutChecker 存在但未接线 |
|
||||
| 2 | 调用链连通 | ⚠️ 部分断裂 | EventHandler→register_event_handlers 断线 |
|
||||
| 3 | 配置传递 | ✅ 正常 | config-rs + env 覆盖工作正常 |
|
||||
| 4 | 降级策略 | ❌ 缺失 | 无断路器、无数据库连接重试 |
|
||||
| 5 | 多租户隔离 | ⚠️ 有风险 | 默认 tenant ID 硬编码 nil UUID |
|
||||
| 6 | 审计追溯 | ⚠️ 部分缺失 | 写入完整但无查询接口 |
|
||||
| 7 | 事件传播 | ⚠️ 大量断裂 | 24 种事件仅 2 种被消费 |
|
||||
| 8 | 前后端一致 | ⚠️ 部分缺失 | 5 个后端端点无前端消费者 |
|
||||
| 9 | 死代码检测 | ❌ 存在 | erp-common 整个 crate 未使用 |
|
||||
| 10 | 安全合规 | ⚠️ 有风险 | 登录无限流、JWT 存 localStorage、CORS 默认 * |
|
||||
|
||||
---
|
||||
|
||||
## 关键文件索引
|
||||
|
||||
| 文件 | 审计关联 |
|
||||
|------|---------|
|
||||
| [main.rs](crates/erp-server/src/main.rs) | 模块注册、路由组装、事件总线、后台任务 |
|
||||
| [state.rs](crates/erp-server/src/state.rs) | 硬编码 tenant_id、AppState 定义 |
|
||||
| [module.rs](crates/erp-core/src/module.rs) | ErpModule trait、ModuleRegistry |
|
||||
| [events.rs](crates/erp-core/src/events.rs) | EventBus、EventHandler trait |
|
||||
| [instance_service.rs](crates/erp-workflow/src/service/instance_service.rs) | 缺失事件发布 |
|
||||
| [module.rs (message)](crates/erp-message/src/module.rs) | 唯一的事件订阅者 |
|
||||
| [timeout.rs](crates/erp-workflow/src/engine/timeout.rs) | 未接线的超时检查 |
|
||||
| [flow_executor.rs](crates/erp-workflow/src/engine/flow_executor.rs) | ServiceTask 未实现 |
|
||||
| [InstanceMonitor.tsx](apps/web/src/pages/workflow/InstanceMonitor.tsx) | 缺 resume/suspend 按钮 |
|
||||
|
||||
---
|
||||
|
||||
## 执行优先级
|
||||
|
||||
**Phase A (生产阻塞)** → **Phase B (功能完整)** → **Phase C (规格合规)** → **Phase D (E2E 验证)**
|
||||
|
||||
每个 Phase 完成后运行 `cargo check && cargo test --workspace && pnpm build` 确认无回归。
|
||||
@@ -1,189 +0,0 @@
|
||||
# CRM 插件深度分析 & 多专家组头脑风暴
|
||||
|
||||
## Context
|
||||
|
||||
CRM 插件 (`erp-plugin-crm`) 是 ERP 平台的第一个行业业务插件,也是 WASM 插件系统的标杆验证案例。它已完成了设计规格中的全部 3 个阶段(共 24 个任务),包含 5 个实体、9 个权限、7 种页面类型。现在需要从深度和广度两个维度进行全方位审视,发现改进空间、架构风险和演进方向。
|
||||
|
||||
---
|
||||
|
||||
## 一、CRM 插件全景画像
|
||||
|
||||
### 1.1 架构定位
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 前端 SPA │
|
||||
│ PluginCRUDPage / Tree / Graph / Dashboard / │
|
||||
│ Tabs / Detail / Admin — 全部 schema 驱动渲染 │
|
||||
└────────────────────┬────────────────────────────┘
|
||||
│ REST API (pluginData + plugins)
|
||||
┌────────────────────▼────────────────────────────┐
|
||||
│ Plugin Host (erp-plugin) │
|
||||
│ service.rs → engine.rs → host.rs → dynamic_table│
|
||||
│ data_service.rs → handler → module │
|
||||
└────────────────────┬────────────────────────────┘
|
||||
│ WIT 接口 (Host API 9 个函数)
|
||||
┌────────────────────▼────────────────────────────┐
|
||||
│ CRM WASM Guest (erp-plugin-crm) │
|
||||
│ lib.rs (30行) — init/on_tenant_created/ │
|
||||
│ handle_event 全部 no-op │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 数据模型 (5 实体)
|
||||
|
||||
| 实体 | 表名 | 字段数 | 用途 |
|
||||
|------|------|--------|------|
|
||||
| customer | plugin_erp_crm_customer | 15 | 客户主数据 (企业/个人) |
|
||||
| contact | plugin_erp_crm_contact | 8 | 联系人 |
|
||||
| communication | plugin_erp_crm_communication | 7 | 沟通记录 |
|
||||
| customer_tag | plugin_erp_crm_customer_tag | 3 | 客户标签 |
|
||||
| customer_relationship | plugin_erp_crm_customer_relationship | 4 | 客户关系 |
|
||||
|
||||
### 1.3 页面矩阵 (7 页面类型)
|
||||
|
||||
| 页面类型 | CRM 用途 | 复杂度 |
|
||||
|----------|---------|--------|
|
||||
| CRUD | 客户/联系人/沟通/标签/关系列表 | 中 (搜索/筛选/排序/视图切换) |
|
||||
| Detail (Drawer) | 客户360度视图 | 高 (字段+嵌套CRUD+时间线) |
|
||||
| Tree | 客户层级树 | 低 |
|
||||
| Tabs | 客户管理入口 | 中 |
|
||||
| Graph | 关系图谱 (Canvas手绘) | 高 |
|
||||
| Dashboard | 统计概览 | 中 |
|
||||
| Admin | 插件生命周期管理 | 中 |
|
||||
|
||||
### 1.4 权限模型 (9 权限)
|
||||
|
||||
两级权限体系:平台级 (`plugin.admin/list`) + 插件级 (`erp-crm.customer.list/manage` 等)
|
||||
|
||||
---
|
||||
|
||||
## 二、深度分析 — 六维度体检
|
||||
|
||||
### 2.1 架构健壮性 ⚠️
|
||||
|
||||
**优势:**
|
||||
- Host-Guest 沙箱隔离,Fuel 资源限制,WASM panic 不影响主服务
|
||||
- 延迟写入模式 (PendingOp) 保证事务原子性
|
||||
- 乐观锁 + 软删除 + 多租户隔离
|
||||
|
||||
**风险:**
|
||||
- **JSONB 外键完整性为零** — `contact.customer_id` 指向已删除客户不会被拦截
|
||||
- **WASM Guest 价值极低** — CRM 插件 lib.rs 仅 30 行,3 个生命周期钩子全是 no-op,所有业务逻辑实际在 Host 侧的 data_service.rs
|
||||
- **动态表 SQL 拼接风险** — 虽有 `sanitize_identifier()`,但 JSONB 查询构建器复杂度高
|
||||
- **插件热升级策略缺失** — 版本升级时如何处理已有数据的 schema 变更?
|
||||
|
||||
### 2.2 数据完整性 ⚠️
|
||||
|
||||
**当前状态:**
|
||||
- 无 DB 级外键约束(JSONB 字段无法使用 PostgreSQL FK)
|
||||
- 无应用层 FK 校验(data_service.rs 的 create/update 不检查关联实体是否存在)
|
||||
- 删除客户时不级联处理关联的联系人/沟通记录/标签
|
||||
- `customer.parent_id` 的循环引用无检测
|
||||
|
||||
### 2.3 查询性能 🟡
|
||||
|
||||
**已做优化:**
|
||||
- GIN 索引覆盖 searchable 字段
|
||||
- tenant_id 索引
|
||||
- Redis 连接缓存 (响应 2.26s → 2ms)
|
||||
|
||||
**潜在瓶颈:**
|
||||
- JSONB 内字段排序 (`ORDER BY data->>'field'`) 无法使用 B-tree 索引
|
||||
- 聚合查询 (`aggregate`) 对大数据量可能慢
|
||||
- 关系图谱需要全量加载 customer + customer_relationship,前端 Canvas 渲染
|
||||
- 无分页的树结构查询(前端全量加载后客户端构建)
|
||||
|
||||
### 2.4 安全合规 🔴
|
||||
|
||||
- **数据权限缺失** — 只有操作权限 (能否操作),无数据权限 (能看到谁的数据)
|
||||
- **搜索注入** — search 参数直接拼入 SQL ILIKE,需确认转义逻辑
|
||||
- **权限 fallback 过宽** — `plugin.admin` 权限自动获得所有插件的所有操作权限
|
||||
|
||||
### 2.5 前端体验 🟡
|
||||
|
||||
**亮点:**
|
||||
- 全 schema 驱动,零 CRM 专用前端代码
|
||||
- visible_when 条件表单字段
|
||||
- 时间线/表格视图切换
|
||||
- Canvas 手绘关系图谱(无第三方库依赖)
|
||||
- 嵌套 CRUD (Detail Drawer)
|
||||
|
||||
**不足:**
|
||||
- 无看板视图(销售漏斗/客户跟进阶段)
|
||||
- 无数据导入/导出功能
|
||||
- 无批量操作
|
||||
- 标签系统过于简单(无标签分组管理、无标签云)
|
||||
- Dashboard 统计维度有限(只有计数+分组聚合)
|
||||
- 树页面不支持拖拽排序
|
||||
|
||||
### 2.6 可扩展性 🟡
|
||||
|
||||
**平台能力沉淀:**
|
||||
- 7 种通用页面类型可供未来插件复用
|
||||
- 插件 manifest (TOML) schema 定义了完整的元数据规范
|
||||
- 插件生命周期管理 (上传→安装→启用→禁用→卸载→清除)
|
||||
|
||||
**限制:**
|
||||
- 页面类型有限 — 无日历、看板、甘特图、地图等业务常见页面
|
||||
- WASM WIT 接口固定 — Host API 9 个函数,扩展需要修改 WIT + 双端重编译
|
||||
- 插件间通信未实现 — 当前只能通过 EventBus 订阅系统事件,插件间无法直接交互
|
||||
|
||||
---
|
||||
|
||||
## 三、专家组头脑风暴议题
|
||||
|
||||
基于以上分析,建议从以下 6 个专家组视角展开头脑风暴:
|
||||
|
||||
### 专家组 1: 后端架构师
|
||||
**焦点:** WASM 插件架构的真正价值在哪里?如何让 Guest 代码从 no-op 变成有意义的业务逻辑?
|
||||
- Host API 的 db_query 为何不可用?如何修复?
|
||||
- JSONB 动态表 vs 独立 schema 表的取舍
|
||||
- 插件版本升级的数据迁移策略
|
||||
- 延迟写入模式的局限性和改进
|
||||
|
||||
### 专家组 2: CRM 产品专家
|
||||
**焦点:** 这个 CRM 能用吗?缺什么核心能力?
|
||||
- 销售漏斗/商机管理缺失的影响
|
||||
- 客户跟进提醒机制
|
||||
- 数据导入/导出 (Excel)
|
||||
- 客户画像/360度视图的完整性
|
||||
- 与 ERP 其他模块 (进销存/财务) 的联动
|
||||
|
||||
### 专家组 3: 安全工程师
|
||||
**焦点:** 数据权限、隔离性、合规风险
|
||||
- 行级数据权限 (谁能看到哪些客户)
|
||||
- JSONB 查询注入风险
|
||||
- 插件 WASM 沙箱逃逸可能性
|
||||
- 审计日志的完整性
|
||||
- GDPR/数据隐私合规
|
||||
|
||||
### 专家组 4: 前端架构师
|
||||
**焦点:** Schema 驱动 UI 的天花板和突破路径
|
||||
- 复杂交互场景 (拖拽、批量操作、右键菜单) 的 schema 描述能力
|
||||
- 看板/日历/甘特图等新页面类型的设计
|
||||
- 大数据量下的性能优化 (虚拟滚动、懒加载)
|
||||
- 插件自定义 UI 组件的可行性
|
||||
|
||||
### 专家组 5: 平台架构师
|
||||
**焦点:** 插件平台的通用性和未来演进
|
||||
- 插件间通信和协作机制
|
||||
- 插件市场/分发的技术架构
|
||||
- 插件依赖管理和版本兼容
|
||||
- 多租户插件隔离的成本和安全性
|
||||
|
||||
### 专家组 6: 性能工程师
|
||||
**焦点:** JSONB 动态表的性能天花板
|
||||
- 百万级数据的查询优化策略
|
||||
- 聚合查询的预计算/缓存
|
||||
- 关系图谱大数据量下的前端渲染
|
||||
- PostgreSQL JSONB 索引策略优化
|
||||
|
||||
---
|
||||
|
||||
## 四、实施计划
|
||||
|
||||
1. 退出计划模式后,立即调用 `/brainstorm` 技能,以本分析报告为基础材料
|
||||
2. 对 6 个专家组视角逐一展开头脑风暴
|
||||
3. 汇总所有专家建议,形成优先级排序的改进路线图
|
||||
4. 输出最终的 CRM 插件改进计划
|
||||
@@ -1,615 +0,0 @@
|
||||
# ERP 平台发散式探讨记录
|
||||
|
||||
> 日期: 2026-04-18 | 形式: 无主题发散式互动讨论
|
||||
|
||||
---
|
||||
|
||||
## 项目当前状态快照
|
||||
|
||||
**已完成:**
|
||||
- Phase 1-6 核心平台 (core/auth/config/workflow/message/plugin)
|
||||
- WASM 插件系统 (Wasmtime + WIT + 动态表 + 热更新)
|
||||
- 2 个行业插件 (CRM 5实体 + 进销存 6实体)
|
||||
- Q2-Q4 成熟度路线图 (安全/架构/测试/插件生态)
|
||||
- 13 个 Rust crate, 37 个迁移, 15+ 前端页面
|
||||
|
||||
**进行中 (29 个未提交文件):**
|
||||
- P0 平台能力升级 (实体关系增强/字段校验/前端去硬编码)
|
||||
- 插件系统增强 (混合执行模型/聚合查询扩展/热更新原子回滚/Schema演进)
|
||||
|
||||
**代码中的 TODO:**
|
||||
- Workflow 超时自动完成/升级逻辑
|
||||
- Redis 缓存层 (data_service)
|
||||
|
||||
---
|
||||
|
||||
## 发散探讨方向
|
||||
|
||||
### 方向 A: 技术纵深 — 平台能力的下一个突破点
|
||||
|
||||
**插件系统能力边界在哪里?**
|
||||
- 混合执行模型 (WASM + Host Query) 的安全边界如何界定?
|
||||
- 插件能否拥有自己的定时任务?事件订阅后的异步处理链?
|
||||
- WASM 组件之间的通信机制 — 插件 A 能否调用插件 B 的能力?
|
||||
- 插件市场/分发机制 — 如何做到"一键安装"?
|
||||
|
||||
**性能与规模化的隐藏挑战:**
|
||||
- 动态表在海量数据下的查询性能 — 索引策略?
|
||||
- 多租户隔离在大规模场景下的瓶颈 — schema-per-tenant 何时比 row-level 更优?
|
||||
- WASM 执行的 Fuel 限制如何平衡安全与灵活性?
|
||||
- 热更新期间的请求如何处理 — 连接排空?
|
||||
|
||||
### 方向 B: 业务纵深 — ERP 领域的深度探索
|
||||
|
||||
**CRM 插件的完整度缺口:**
|
||||
- 商机/销售漏斗 — 从线索到成单的全链路
|
||||
- 合同管理 — 模板、电子签章、履约跟踪
|
||||
- 报价单 — 产品目录、价格策略、审批流
|
||||
- 客户画像 — 标签体系、行为追踪、智能推荐
|
||||
|
||||
**下一个行业插件应该是什么?**
|
||||
- 财务 (总账/应收/应付/固定资产)
|
||||
- 采购 (供应商/询价/采购订单/入库)
|
||||
- 制造 (BOM/工单/排产/质检)
|
||||
- 人力 (员工/考勤/薪资/绩效)
|
||||
- 电商 (商品/订单/物流/售后)
|
||||
|
||||
**跨模块业务流程:**
|
||||
- 从销售订单 → 采购 → 入库 → 付款 的端到端流程
|
||||
- 插件间的数据如何流转?订单确认触发采购申请?
|
||||
- 工作流引擎如何编排跨插件流程?
|
||||
|
||||
### 方向 C: 体验纵深 — 前端与用户交互
|
||||
|
||||
**低代码/零代码的可能性:**
|
||||
- 插件的前端页面能否完全由 schema 驱动生成?
|
||||
- 可视化表单设计器 — 拖拽生成插件页面
|
||||
- 自定义 Dashboard — 用户拼装自己的工作台
|
||||
- 报表引擎 — 从数据到图表的可视化配置
|
||||
|
||||
**移动端/多端体验:**
|
||||
- PWA 方案 — 离线能力 + 推送通知
|
||||
- Tauri 桌面端何时启动?哪些场景需要桌面端?
|
||||
- 小程序/企业微信集成 — 中国市场的刚需?
|
||||
|
||||
**AI 增强交互:**
|
||||
- 自然语言查询 — "帮我查上个月销售额最高的 10 个客户"
|
||||
- 智能推荐 — 基于操作习惯的快捷入口
|
||||
- 数据洞察 — 自动发现异常趋势并提醒
|
||||
- AI 辅助填单 — 自动补全/智能校验
|
||||
|
||||
### 方向 D: 商业纵深 — SaaS 化与商业化
|
||||
|
||||
**多租户高级能力:**
|
||||
- 租户级别的功能开关 — 不同套餐解锁不同插件
|
||||
- 计量计费 — 按用户数/存储/API调用量计费
|
||||
- 租户数据导出/迁移 — 保障数据主权
|
||||
- 白标/品牌定制 — 租户自定义 Logo/主题
|
||||
|
||||
**开放平台战略:**
|
||||
- API Gateway + 开发者门户
|
||||
- Webhook 系统 — 外部系统集成
|
||||
- 第三方插件审核/上架流程
|
||||
- 合作伙伴生态 — ISV 开发行业插件
|
||||
|
||||
### 方向 E: 团队与工程效率
|
||||
|
||||
**开发体验提升:**
|
||||
- 插件开发脚手架 CLI — `erp-plugin create crm`
|
||||
- 本地开发热重载 — 改 WASM 代码即时生效
|
||||
- 插件调试工具 — 断点/日志/性能分析
|
||||
- 一键生成插件 CRUD — 从 schema 到完整页面
|
||||
|
||||
**DevOps 与运维:**
|
||||
- 蓝绿部署 / 金丝雀发布策略
|
||||
- 数据库迁移的零停机方案
|
||||
- 多环境管理 (dev/staging/prod)
|
||||
- 监控告警体系 (APM + 日志聚合)
|
||||
|
||||
---
|
||||
|
||||
## 讨论记录
|
||||
|
||||
> 以下是互动讨论的要点,按时间顺序记录
|
||||
|
||||
### Round 1: "造一个财务插件来验证平台" — 立刻暴露了跨插件数据引用的缺失
|
||||
|
||||
**用户意图:** 希望通过搭建第二个行业插件(财务/应收),验证基座和插件系统,特别是与 CRM 插件的数据交互。
|
||||
|
||||
**已发现的系统缺陷 — 跨插件数据引用完全不支持:**
|
||||
|
||||
| 能力 | 现状 | 影响 |
|
||||
|------|------|------|
|
||||
| `ref_entity` 跨插件引用 | 仅限当前插件表空间 | 财务插件的 `customer_id` 无法声明指向 CRM 的 customer |
|
||||
| Host API 跨插件查询 | `db-query` 无 plugin_id 参数 | WASM 插件无法查询其他插件数据 |
|
||||
| PluginRelation 跨插件 | `entity` 字段无插件限定 | 无法声明跨插件的关联关系 |
|
||||
| 前端 entity_select | 仅加载当前插件数据源 | 下拉框无法显示其他插件的实体列表 |
|
||||
| 引用完整性校验 | 仅校验当前插件表空间 | 跨插件的外键约束无法生效 |
|
||||
|
||||
**进销存插件已有的"绕路":** `customer_id` 作为裸 UUID 存在,没有 `ref_entity` 声明 — 证明这是一个已知的痛点。
|
||||
|
||||
**唯一现有机制:** EventBus 事件广播(松耦合通知),但无法支持同步查询或声明式引用。
|
||||
|
||||
**财务插件与 CRM 的理想交互场景:**
|
||||
```
|
||||
CRM.customer ──引用──→ Finance.invoice.customer_id (外键 + 下拉选择)
|
||||
CRM.opportunity ──引用──→ Finance.sales_order.opportunity_id
|
||||
CRM.contact ──引用──→ Finance.quote.contact_id
|
||||
```
|
||||
|
||||
**要实现这些,需要改造:**
|
||||
1. `manifest.rs` — PluginField/PluginRelation 增加 `ref_plugin` 字段
|
||||
2. `data_service.rs` — validate_ref_entities 支持跨插件表名解析
|
||||
3. `plugin.wit` + `host.rs` — 新增跨插件查询 API
|
||||
4. `dynamic_table.rs` — 表名解析支持目标 plugin_id
|
||||
5. 前端 entity_select — 支持加载其他插件数据源
|
||||
6. 权限模型 — 跨插件数据访问控制
|
||||
|
||||
### Round 2: 方案收敛 — 软引用 + 实体注册表 + 优雅降级
|
||||
|
||||
**决策记录:**
|
||||
|
||||
| 问题 | 决策 | 理由 |
|
||||
|------|------|------|
|
||||
| 引用模式 | **声明式** (plugin.toml) | 与现有 schema-driven 模式一致,插件作者零代码 |
|
||||
| 依赖严格度 | **完全独立,无硬依赖** | SaaS 用户必须能自由组合/卸载插件 |
|
||||
| 实体归属 | **插件自拥有,平台注册表发现** | 不改变现有模型,通过注册表实现运行时发现 |
|
||||
| 悬空引用 | **软警告 + 后台对账** | 永不阻塞用户操作,对账工具引导修复 |
|
||||
|
||||
**架构设计:**
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ Layer 3: Plugin (财务/采购/制造...) │
|
||||
│ - optional_dependencies 声明 │
|
||||
│ - ref_scope = "external" 跨插件引用字段 │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ Layer 2: Entity Registry (平台实体注册表) │
|
||||
│ - 插件安装时注册实体、卸载时标记 inactive │
|
||||
│ - 查询时动态发现源插件 │
|
||||
│ - 悬空引用检测 + 对账报告 │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ Layer 1: Plugin System (现有基础设施) │
|
||||
│ - 动态表、Host API、EventBus 不变 │
|
||||
│ - 新增 Entity Registry 接入点 │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**plugin.toml 声明示例:**
|
||||
```toml
|
||||
[dependencies.crm]
|
||||
optional = true
|
||||
description = "客户管理 — 自动关联客户数据"
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "customer_id"
|
||||
field_type = "uuid"
|
||||
ref_entity = "customer"
|
||||
ref_scope = "external"
|
||||
ref_display_field = "name"
|
||||
ref_fallback_label = "外部客户"
|
||||
```
|
||||
|
||||
**运行时行为:**
|
||||
|
||||
| 源插件状态 | 写入 | 读取 | 展示 |
|
||||
|-----------|------|------|------|
|
||||
| 已安装 | 强校验 | JOIN 富化 | ✅ 绿色链接 "张三" |
|
||||
| 未安装 | 无校验 | 原始 UUID | ⬜ 灰色 "外部客户" |
|
||||
| 刚重新启用 | 新写入强校验 | 后台对账 | ⚠️ 黄色警告 (悬空) |
|
||||
|
||||
**悬空引用处理 (CRM 重新启用时):**
|
||||
1. 后台扫描所有 `ref_scope=external` 的字段
|
||||
2. 生成引用对账报告(有效/悬空分类)
|
||||
3. 前端提示用户逐条处理(映射/清空/忽略)
|
||||
4. 永不硬阻塞用户操作
|
||||
|
||||
**需改造的 6 个点:**
|
||||
1. `manifest.rs` — 新增 `ref_scope`, `ref_display_field`, `ref_fallback_label`, `dependencies` 段
|
||||
2. `entity_registry` (新模块) — 实体注册/发现/inactive 标记
|
||||
3. `data_service.rs` — validate_ref_entities 支持运行时发现
|
||||
4. `host.rs` + `plugin.wit` — 新增 resolve-ref-entity API
|
||||
5. 前端 `entity_select` — 检测注册表,有源插件加载下拉,无则降级
|
||||
6. 对账工具 — 后台扫描 + 前端对账 UI
|
||||
|
||||
### Round 3: 插件生态与商业化 — 技术优先路径
|
||||
|
||||
**用户选择:** 技术优先 → 市场,先做好平台能力再考虑商业模式。
|
||||
|
||||
**发现的三大技术缺口:**
|
||||
|
||||
1. **插件质量保障** — 安全扫描、性能基准、兼容性检测、运行时监控全部缺失
|
||||
2. **插件配置与数据管理** — 导入导出、打印模板、配置 UI、自定义视图全部缺失
|
||||
3. **插件市场/商店** — 浏览、发现、一键安装、评分全部缺失
|
||||
|
||||
**决策: 这些能力应该是平台级通用服务,不是插件各自实现。**
|
||||
|
||||
新增架构层:
|
||||
```
|
||||
插件 → Plugin Platform Services → Plugin System → ERP Core
|
||||
↑
|
||||
导入导出 / 打印 / 配置 / 视图 / 通知 / 编号
|
||||
```
|
||||
|
||||
**平台 P1 通用服务清单:**
|
||||
|
||||
| 服务 | 接入方式 | 财务插件示例 |
|
||||
|------|---------|-------------|
|
||||
| 数据导入导出 | plugin.toml 声明 importable/exportable | 导入客户清单、导出发票明细 |
|
||||
| 打印模板 | 模板文件 + schema 映射 | 发票 PDF、收款凭证 |
|
||||
| 插件配置 UI | plugin.toml 声明 settings | 税率表、付款条件、发票前缀 |
|
||||
| 自定义视图 | 用户保存列/筛选配置 | 财务看不同列、销售看不同列 |
|
||||
| 通知规则 | 插件定义触发事件 | 发票逾期 → 通知负责人 |
|
||||
| 编号规则 | 复用 erp-config 的编号服务 | INV-2026-0001 |
|
||||
|
||||
### Round 4: 收敛 — 全部整合为一份设计规格
|
||||
|
||||
用户确认将所有讨论成果写入一份"插件平台演进设计规格"文档。
|
||||
|
||||
---
|
||||
|
||||
## 设计规格: ERP 插件平台演进路线图
|
||||
|
||||
> 基于 2026-04-18 发散式探讨的成果,涵盖跨插件引用、平台通用服务、质量保障、插件市场四个维度。
|
||||
|
||||
### 1. 背景与动机
|
||||
|
||||
ERP 平台已完成 Phase 1-6 核心开发和 Q2-Q4 成熟度路线图。当前有两个行业插件(CRM + 进销存)运行在 WASM 插件系统上。但通过分析发现:
|
||||
|
||||
- **跨插件数据引用完全不支持** — 进销存的 `customer_id` 只能存裸 UUID
|
||||
- **插件无通用业务能力** — 导入导出/打印/配置/视图每个插件都要自己实现
|
||||
- **无质量保障机制** — 第三方插件的安全性和性能无法保证
|
||||
- **无发现和分发渠道** — 用户无法自助发现和安装插件
|
||||
|
||||
目标:通过搭建财务/应收插件来验证和推动这些平台能力的实现。
|
||||
|
||||
### 2. 跨插件数据引用系统
|
||||
|
||||
#### 2.1 设计原则
|
||||
|
||||
- **插件完全独立** — 任何插件可独立安装/卸载,不受其他插件影响
|
||||
- **声明式配置** — 跨插件引用通过 plugin.toml 声明,插件作者零代码
|
||||
- **优雅降级** — 源插件不存在时功能降级,不阻塞用户操作
|
||||
- **软警告** — 外部引用问题永远是警告,不是错误
|
||||
|
||||
#### 2.2 实体注册表 (Entity Registry)
|
||||
|
||||
**数据结构:**
|
||||
```
|
||||
entity_registry:
|
||||
- entity_name: string # 实体名 (如 "customer")
|
||||
- plugin_id: string # 注册该实体的插件 ID
|
||||
- display_fields: string[] # 用于下拉显示的字段列表
|
||||
- search_fields: string[] # 用于搜索的字段列表
|
||||
- status: active | inactive # 插件卸载时标记 inactive
|
||||
- registered_at: timestamp
|
||||
- tenant_id: uuid # 多租户隔离
|
||||
```
|
||||
|
||||
**生命周期:**
|
||||
- 插件安装 → 注册所有 entities 到 registry
|
||||
- 插件启用 → status = active
|
||||
- 插件禁用 → status = inactive(数据保留)
|
||||
- 插件卸载 → status = inactive + 标记为 orphaned
|
||||
|
||||
#### 2.3 plugin.toml 扩展
|
||||
|
||||
```toml
|
||||
# 可选依赖声明
|
||||
[dependencies.crm]
|
||||
optional = true
|
||||
description = "客户管理 — 自动关联客户数据,未安装时客户字段为手动输入"
|
||||
|
||||
[dependencies.inventory]
|
||||
optional = true
|
||||
description = "进销存 — 自动关联商品数据"
|
||||
|
||||
# 跨插件引用字段
|
||||
[[schema.entities.fields]]
|
||||
name = "customer_id"
|
||||
field_type = "uuid"
|
||||
display_name = "客户"
|
||||
ref_entity = "customer" # 目标实体名
|
||||
ref_scope = "external" # "internal" (默认) | "external"
|
||||
ref_display_field = "name" # 下拉框显示字段
|
||||
ref_search_fields = ["name", "phone"] # 搜索字段
|
||||
ref_fallback_label = "外部客户" # 降级时显示文本
|
||||
```
|
||||
|
||||
#### 2.4 运行时行为
|
||||
|
||||
**写入时校验:**
|
||||
```
|
||||
IF ref_scope == "external":
|
||||
registry = EntityRegistry.find("customer")
|
||||
IF registry.status == "active":
|
||||
强校验: customer_id 必须存在于 registry.plugin_id 的对应表中
|
||||
ELSE:
|
||||
无校验: 接受任意 UUID
|
||||
```
|
||||
|
||||
**读取时富化:**
|
||||
```
|
||||
IF ref_scope == "external" AND registry.status == "active":
|
||||
JOIN plugin_{registry.plugin_id}_{ref_entity} 获取 display_field
|
||||
前端显示: "张三 (CRM)" (绿色可点击链接)
|
||||
ELIF ref_scope == "external" AND registry.status == "inactive":
|
||||
前端显示: "外部客户 ({uuid})" (灰色)
|
||||
```
|
||||
|
||||
**悬空引用处理:**
|
||||
```
|
||||
ON plugin.activate:
|
||||
1. 后台扫描所有 ref_scope="external" 且指向本插件实体的字段
|
||||
2. 验证每个 UUID 是否存在于本插件表中
|
||||
3. 生成对账报告: { valid: N, dangling: M, details: [...] }
|
||||
4. 前端展示对账结果,用户逐条处理
|
||||
```
|
||||
|
||||
#### 2.5 需要改造的文件
|
||||
|
||||
| 文件 | 改动 | 复杂度 |
|
||||
|------|------|--------|
|
||||
| `crates/erp-plugin/src/manifest.rs` | 新增 `ref_scope`, `ref_display_field`, `ref_search_fields`, `ref_fallback_label`; 新增 `DependenciesSection` | 低 |
|
||||
| `crates/erp-plugin/src/entity_registry.rs` (新) | 实体注册/发现/inactive 标记/对账 | 中 |
|
||||
| `crates/erp-plugin/src/data_service.rs` | `validate_ref_entities` 支持运行时发现外部引用 | 中 |
|
||||
| `crates/erp-plugin/src/host.rs` | 新增 `resolve_ref_entity` Host API | 中 |
|
||||
| `crates/erp-plugin/wit/plugin.wit` | 新增 `resolve-ref-entity` 接口 | 低 |
|
||||
| `crates/erp-plugin/src/service.rs` | 插件安装/卸载时维护 Entity Registry | 中 |
|
||||
| `apps/web/src/` 前端 | entity_select 组件支持跨插件数据源 + 降级显示 + 对账 UI | 高 |
|
||||
|
||||
### 3. 插件平台通用服务层 (P1)
|
||||
|
||||
#### 3.1 数据导入导出服务
|
||||
|
||||
**设计思路:** 插件在 plugin.toml 中声明哪些实体支持导入导出,平台提供统一的导入导出 UI 和引擎。
|
||||
|
||||
```toml
|
||||
# plugin.toml 中的声明
|
||||
[[schema.entities]]
|
||||
name = "invoice"
|
||||
display_name = "发票"
|
||||
importable = true
|
||||
exportable = true
|
||||
import_template = "invoice_import_template.xlsx" # 可选: 自定义导入模板
|
||||
|
||||
[[schema.entities]]
|
||||
name = "payment"
|
||||
display_name = "收款"
|
||||
importable = true
|
||||
exportable = true
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- 自动生成导入模板(基于 schema entities fields)
|
||||
- Excel/CSV 解析 + schema 字段校验
|
||||
- 批量写入(支持事务 + 错误行级报告)
|
||||
- 导出为 Excel/CSV(支持筛选条件)
|
||||
- 导入历史记录 + 回滚
|
||||
|
||||
**实现位置:** 新增 `crates/erp-plugin/src/import_export.rs`,前端新增 `ImportExportModal` 通用组件。
|
||||
|
||||
#### 3.2 打印模板引擎
|
||||
|
||||
**设计思路:** 平台提供 HTML → PDF 的模板渲染能力,插件定义模板和字段映射。
|
||||
|
||||
```toml
|
||||
# plugin.toml 中的声明
|
||||
[[templates]]
|
||||
name = "invoice_pdf"
|
||||
display_name = "发票"
|
||||
entity = "invoice"
|
||||
format = "pdf"
|
||||
template_file = "templates/invoice.html" # HTML 模板
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- HTML 模板渲染 → PDF 下载
|
||||
- 模板变量替换(基于实体字段)
|
||||
- 租户级模板自定义(覆盖默认模板)
|
||||
- 打印预览
|
||||
|
||||
**实现位置:** 后端使用 `wkhtmltopdf` 或 `headless-chrome` 渲染,前端新增 `PrintPreviewModal` 组件。
|
||||
|
||||
#### 3.3 插件配置 UI
|
||||
|
||||
**设计思路:** 插件在 plugin.toml 中声明配置项,平台自动生成配置页面。
|
||||
|
||||
```toml
|
||||
# plugin.toml 中的声明
|
||||
[settings]
|
||||
[[settings.fields]]
|
||||
name = "default_tax_rate"
|
||||
display_name = "默认税率"
|
||||
field_type = "number"
|
||||
default_value = 0.13
|
||||
|
||||
[[settings.fields]]
|
||||
name = "invoice_prefix"
|
||||
display_name = "发票前缀"
|
||||
field_type = "text"
|
||||
default_value = "INV"
|
||||
|
||||
[[settings.fields]]
|
||||
name = "payment_terms"
|
||||
display_name = "默认付款条件"
|
||||
field_type = "select"
|
||||
options = ["net_15", "net_30", "net_60", "cod"]
|
||||
default_value = "net_30"
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- 根据 settings 声明自动生成配置表单
|
||||
- 配置数据存储在 `plugin_settings` 表(tenant_id + plugin_id + key/value)
|
||||
- 配置变更时通知插件(通过事件)
|
||||
- 支持配置权限控制(仅管理员可改)
|
||||
|
||||
#### 3.4 自定义视图
|
||||
|
||||
**设计思路:** 用户可以保存列表页的列配置和筛选条件。
|
||||
|
||||
```
|
||||
user_views:
|
||||
- id: uuid
|
||||
- user_id: uuid
|
||||
- plugin_id: string
|
||||
- entity_name: string
|
||||
- view_name: string
|
||||
- columns: string[] # 显示的列
|
||||
- filters: json # 筛选条件
|
||||
- sort: json # 排序条件
|
||||
- is_default: boolean
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- 列表页支持列拖拽排序、显示/隐藏
|
||||
- 筛选条件保存/加载
|
||||
- 每个用户可以有多个视图
|
||||
- 支持共享视图给同角色用户
|
||||
|
||||
#### 3.5 通知规则
|
||||
|
||||
**设计思路:** 插件在 plugin.toml 中声明可触发的事件,平台提供通知规则配置 UI。
|
||||
|
||||
```toml
|
||||
# plugin.toml 中的声明
|
||||
[[trigger_events]]
|
||||
name = "invoice.overdue"
|
||||
display_name = "发票逾期"
|
||||
description = "发票超过付款期限未收款"
|
||||
|
||||
[[trigger_events]]
|
||||
name = "payment.received"
|
||||
display_name = "收款确认"
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- 规则引擎: WHEN event THEN notify [user/role/department]
|
||||
- 复用 erp-message 的通知渠道
|
||||
- 租户级规则配置
|
||||
- 通知模板自定义
|
||||
|
||||
#### 3.6 编号规则 (已有基础扩展)
|
||||
|
||||
**设计思路:** 复用 erp-config 的编号规则服务,扩展为插件可接入。
|
||||
|
||||
```toml
|
||||
# plugin.toml 中的声明
|
||||
[[numbering]]
|
||||
entity = "invoice"
|
||||
prefix = "INV"
|
||||
format = "{PREFIX}-{YEAR}-{SEQ:4}"
|
||||
reset_rule = "yearly" # daily/monthly/yearly/never
|
||||
```
|
||||
|
||||
### 4. 插件质量保障
|
||||
|
||||
#### 4.1 上传时校验
|
||||
|
||||
```
|
||||
插件上传 → Schema 校验 → WASM 二进制验证 → 安全扫描 → 性能基准 → 发布/拒绝
|
||||
```
|
||||
|
||||
| 阶段 | 校验内容 | 现状 |
|
||||
|------|---------|------|
|
||||
| Schema 校验 | plugin.toml 格式、字段类型、权限码一致性 | ✅ 已有部分 |
|
||||
| WASM 验证 | 二进制格式、WIT 兼容性、导出函数检查 | ✅ 已有 |
|
||||
| 安全扫描 | 动态表 SQL 注入风险、Fuel 耗尽、内存泄漏 | ❌ 缺失 |
|
||||
| 性能基准 | 标准 CRUD 操作在 N 条数据下的响应时间 | ❌ 缺失 |
|
||||
| 兼容性 | 平台版本匹配、依赖插件版本兼容 | ❌ 缺失 |
|
||||
|
||||
#### 4.2 运行时监控
|
||||
|
||||
```
|
||||
plugin_runtime_metrics:
|
||||
- plugin_id: string
|
||||
- error_rate: float # 24h 错误率
|
||||
- avg_response_ms: float # 平均响应时间
|
||||
- fuel_consumption: float # 平均 Fuel 消耗
|
||||
- memory_peak_mb: float # 内存峰值
|
||||
- active_instances: int # 活跃实例数
|
||||
```
|
||||
|
||||
**告警规则:**
|
||||
- 错误率 > 5% → 警告
|
||||
- 平均响应 > 2s → 警告
|
||||
- Fuel 消耗异常 → 警告
|
||||
- 内存持续增长 → 疑似泄漏
|
||||
|
||||
### 5. 插件市场/商店
|
||||
|
||||
#### 5.1 功能范围
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 插件目录 | 按行业/功能分类浏览 |
|
||||
| 搜索 | 按名称/标签/行业搜索 |
|
||||
| 详情页 | 截图、演示、功能描述、权限说明 |
|
||||
| 一键安装 | 上传 → 自动安装 → 配置 → 启用 |
|
||||
| 评分/评论 | 用户评分和使用反馈 |
|
||||
| 版本管理 | 版本列表、更新日志、回滚 |
|
||||
| 依赖提示 | 安装时提示可选依赖("推荐配合 CRM 使用") |
|
||||
|
||||
#### 5.2 技术实现
|
||||
|
||||
- 后端: 新增 `plugin_store` 表 + API
|
||||
- 前端: 新增 `PluginStore` 页面
|
||||
- 管理端: 管理员审核/上架/下架
|
||||
|
||||
### 6. 验证计划 — 财务/应收插件
|
||||
|
||||
#### 6.1 实体设计
|
||||
|
||||
| 实体 | 字段概要 | 跨插件引用 |
|
||||
|------|---------|-----------|
|
||||
| invoice (发票) | 编号/客户/金额/税额/状态/到期日 | customer_id → CRM.customer |
|
||||
| invoice_line (发票行) | 发票/商品/数量/单价/税额 | product_id → Inventory.product |
|
||||
| payment (收款) | 发票/金额/方式/日期/状态 | invoice_id → 本插件内部 |
|
||||
| quote (报价单) | 编号/客户/有效期/状态 | customer_id → CRM.customer |
|
||||
| quote_line (报价行) | 报价单/商品/数量/单价 | product_id → Inventory.product |
|
||||
|
||||
#### 6.2 验证矩阵
|
||||
|
||||
| 能力 | 验证方式 | 预期结果 |
|
||||
|------|---------|---------|
|
||||
| 跨插件引用 (CRM 安装) | 创建发票时选择客户 | entity_select 下拉显示 CRM 客户列表 |
|
||||
| 跨插件引用 (CRM 卸载) | 创建发票时输入客户 | 降级为文本输入,不阻塞 |
|
||||
| 悬空引用对账 | CRM 卸载→创建发票→重新安装 CRM | 对账报告显示悬空引用,用户可修复 |
|
||||
| 数据导入 | 导入 Excel 客户清单 | 解析+校验+批量写入 |
|
||||
| 数据导出 | 导出发票列表为 Excel | 筛选+下载 |
|
||||
| 打印模板 | 打印发票 PDF | HTML→PDF 渲染 |
|
||||
| 插件配置 | 设置税率/发票前缀 | 自动生成的配置页面 |
|
||||
| 编号规则 | 创建发票自动编号 | INV-2026-0001 |
|
||||
| 通知规则 | 发票逾期通知 | 规则引擎触发通知 |
|
||||
| 独立安装 | 不安装 CRM 单独安装财务 | 所有功能正常,客户字段降级 |
|
||||
|
||||
### 7. 实施优先级
|
||||
|
||||
```
|
||||
P0 (已完成/进行中): P0 平台能力升级 (实体关系增强/字段校验/前端去硬编码)
|
||||
插件系统增强 (混合执行模型/聚合查询/热更新回滚/Schema演进)
|
||||
|
||||
P1 (跨插件引用): Entity Registry + ref_scope 扩展 + 前端 entity_select 改造
|
||||
这是所有后续能力的基础
|
||||
|
||||
P2 (平台通用服务): 数据导入导出 → 插件配置 UI → 编号规则扩展 → 通知规则
|
||||
按业务迫切程度排序
|
||||
|
||||
P3 (质量保障): 上传时安全扫描 → 性能基准 → 运行时监控
|
||||
逐步建立信任体系
|
||||
|
||||
P4 (插件市场): 插件目录 → 一键安装 → 版本管理 → 评分评论
|
||||
商业化的最后一块拼图
|
||||
|
||||
验证: 财务/应收插件贯穿 P1-P2,每完成一个 P 就用财务插件验证
|
||||
```
|
||||
|
||||
### 8. 风险与缓解
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| Entity Registry 查询性能 | 每次数据操作都要查注册表 | 内存缓存 + DashMap,注册表数据量极小 |
|
||||
| 悬空引用数据量过大 | 对账扫描耗时长 | 异步后台任务 + 分批处理 + 进度条 |
|
||||
| Excel 导入内存占用 | 大文件解析 OOM | 流式解析 + 批量提交 + 文件大小限制 |
|
||||
| 打印模板安全 | 模板注入攻击 | 沙箱渲染 + 变量白名单 |
|
||||
| 插件市场审核成本 | 人工审核效率低 | 自动化扫描 + 人工抽查 + 社区举报 |
|
||||
@@ -1,138 +0,0 @@
|
||||
# UX 分析报告:一人 IT 公司 ERP 插件方案
|
||||
|
||||
> 基于智界科技(一人 IT 服务公司)的业务场景,对 freelance + itops 两个插件的 UX 审查。
|
||||
|
||||
---
|
||||
|
||||
## 1. 一人公司的 UX 痛点
|
||||
|
||||
**WHY**: 一个人没有分工,老板就是销售、项目经理、财务、运维工程师。每次切换页面等于中断心流,表单越复杂越容易填一半放弃。
|
||||
|
||||
**核心摩擦**:
|
||||
|
||||
- **上下文切换成本高** -- 一个人同时处理客户咨询、写代码、记账、回工单。在"客户详情"和"工时记录"之间来回跳转,每次跳转丢失工作记忆。
|
||||
- **重复录入** -- 同一个客户信息在 client、opportunity、quote、invoice、ticket 中反复手填。一人公司没有人帮忙补数据。
|
||||
- **决策疲劳** -- 每天面对 10 个入口,要思考"这个操作该去哪个页面"。对于一人公司,ERP 应该像手机首页一样直觉。
|
||||
- **过度结构化** -- 一人公司的商机通常是微信聊几句就定了,不需要复杂的销售漏斗流程。
|
||||
|
||||
**HOW -- 减少操作的具体措施**:
|
||||
|
||||
1. **全局搜索 + 命令面板**(Ctrl+K):输入"张三"直接跳到客户详情,输入"新工时"直接弹出计时器,输入"#102"跳到工单。一人公司的 ERP 应该像一个大的搜索框 + 几个快捷按钮。
|
||||
2. **自动填充上下文**:在项目工作台记工时时,自动关联当前活跃项目;从客户详情页创建报价单时,自动带入客户信息。减少手动关联操作。
|
||||
3. **合并创建流程**:新建项目时一步内同时创建第一个任务,不用先建项目再跳到任务页。
|
||||
|
||||
---
|
||||
|
||||
## 2. 页面布局合理性 -- 10 个页面是否太多
|
||||
|
||||
**结论:可以压缩到 7 个页面,但不应低于 5 个。**
|
||||
|
||||
**WHY**: 一人公司的操作场景有明确的节奏切换(见客户 vs 做项目 vs 记账),完全合并会导致单页信息过载。但两个插件共 10 个页面确实有冗余。
|
||||
|
||||
**建议合并方案**:
|
||||
|
||||
| 原方案 (10 页) | 优化方案 (7 页) | 理由 |
|
||||
|---|---|---|
|
||||
| freelance 仪表盘 | **全局工作台**(合并两个仪表盘) | 一人只需一个首页 |
|
||||
| 客户管理 (360度) | 客户管理 (保留) | 核心入口,高频使用 |
|
||||
| 商机跟进 (看板) | **并入客户管理**,作为客户详情的一个 tab | 一人公司的商机极少同时超过 5 个,看板过重 |
|
||||
| 项目工作台 | 项目工作台 (保留) | 核心工作场景,需要独立空间 |
|
||||
| 财务中心 | 财务中心 (保留) | 收支是独立节奏,需要集中视图 |
|
||||
| 报价管理 | **并入财务中心**,作为 tab | 报价是财务流程的前置步骤,不放独立页面 |
|
||||
| itops 运维仪表盘 | (已合并到全局工作台) | -- |
|
||||
| 合同管理 | 合同管理 (保留) | 维保合同是独立业务实体 |
|
||||
| 工单中心 | 工单中心 (保留) | 最高频运维操作 |
|
||||
| 巡检管理 | **并入工单中心**,作为 tab 或筛选 | 巡检本质是周期性工单,不需要独立页面 |
|
||||
|
||||
**HOW -- 实现层面**:
|
||||
- freelance 插件减少为 4 个页面:全局工作台(dashboard)、客户管理(tabs 类型,含商机看板 tab)、项目工作台、财务中心(tabs 类型,含报价 tab)
|
||||
- itops 插件减少为 3 个页面:工单中心(tabs 类型,含巡检 tab)、合同管理、(全局工作台跨插件共享)
|
||||
- 跨插件共享的 dashboard 通过 ui.pages 的 `shared: true` 或放在 freelance 插件中声明,itops 通过 `dependencies = ["erp-freelance"]` 引用
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键缺失场景
|
||||
|
||||
**WHY**: 一人 IT 公司有 3 个高频场景在当前方案中完全缺失,不做这些等于 ERP 只覆盖了 60% 的日常工作。
|
||||
|
||||
| 缺失场景 | 严重性 | 说明 |
|
||||
|---|---|---|
|
||||
| **合同/报价到期提醒** | 高 | 维保合同到期前 30 天没有提醒 = 流失续费收入。一人公司靠记忆管理,ERP 必须补上 |
|
||||
| **工时 -> 开票 自动联动** | 高 | 项目完成后手动从工时记录汇总金额再创建发票,这个手工过程在一人公司中最容易被跳过,导致漏收 |
|
||||
| **知识库/文档管理** | 中 | IT 运维的核心资产是文档(网络拓扑、服务器配置、密码记录)。当前方案只有结构化数据,缺非结构化知识 |
|
||||
| **续约提醒 + 自动创建续约商机** | 中 | 维保合同到期时自动生成一个续约 opportunity,串联 freelance 和 itops |
|
||||
|
||||
**HOW -- 实现建议**:
|
||||
|
||||
1. **到期提醒**:在 itops 插件的 service_contract 实体上加 `end_date` 字段(已有),在后端增加定时事件检查 `contract.expiring`,通过消息中心的订阅机制推送到通知面板。
|
||||
2. **工时 -> 开票联动**:在 invoice 实体增加 `source_type = "time_entry"` 和 `source_ids` 字段,前端提供"从工时记录生成发票"的一键操作,按项目汇总自动填充。
|
||||
3. **知识库**:Phase 2 考虑。可以在 client 或 project 实体上加 `attachments` (json) 字段存储文件引用,先做轻量版。
|
||||
|
||||
---
|
||||
|
||||
## 4. 仪表盘设计建议 -- 合并为一个全局工作台
|
||||
|
||||
**WHY**: 一人只有一个视角(老板视角),不存在"销售看销售数据、运维看运维数据"的角色分离。两个仪表盘让用户每次登录还要选择看哪个,增加了无意义的决策。
|
||||
|
||||
**HOW -- 全局工作台设计**:
|
||||
|
||||
```
|
||||
+------------------------------------------------------------------+
|
||||
| 全局工作台 |
|
||||
+------------------------------------------------------------------+
|
||||
| 今日待办 (3) 本周收入: ¥12,500 |
|
||||
| [ ] 回复张三报价 (2h前) 待开票: ¥8,200 |
|
||||
| [ ] 完成服务器巡检 (今天) 本月支出: ¥3,400 |
|
||||
| [ ] 提交项目A发票 (明天截止) 到期合同: 2个 (30天内) |
|
||||
+------------------------------------------------------------------+
|
||||
| 活跃项目 (2) 最新工单 (3) |
|
||||
| 项目A - 进行中 ██████░░ 75% #102 网络... 进行中 |
|
||||
| 项目B - 待启动 ░░░░░░░░ 0% #101 备份... 已完成 |
|
||||
| #100 升级... 待处理 |
|
||||
+------------------------------------------------------------------+
|
||||
| [快速操作] +新建客户 +新建工单 +开始计时 +新建报价 |
|
||||
+------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
**布局规则**:
|
||||
- 上方:紧急事项 + 财务概览(左右分栏)
|
||||
- 中间:核心业务对象快照(活跃项目 + 最新工单)
|
||||
- 下方:一键操作按钮条
|
||||
|
||||
**实现**:使用现有的 `PluginDashboardPage` 组件,通过 plugin.toml 的 `ui.pages` 中 type = "dashboard" 声明,dashboard widgets 跨插件聚合数据。freelance 插件声明这个 dashboard,itops 插件通过 `dependencies` 引用后注册自己的 widgets。
|
||||
|
||||
---
|
||||
|
||||
## 5. 快速操作 -- 一键完成的快捷入口
|
||||
|
||||
**WHY**: 一人公司最高频的操作是"快速记一笔"和"快速创建"。如果每次都要打开表单、填完所有字段、点击保存,摩擦太大导致用户放弃使用 ERP,回到微信记事本。
|
||||
|
||||
| 快速操作 | 频率 | HOW |
|
||||
|---|---|---|
|
||||
| **开始/停止计时** | 每天 3-5 次 | 全局悬浮按钮,点击选择项目 -> 开始计时,再点停止自动生成 time_entry。不需要打开任何页面 |
|
||||
| **快速记工单** | 每天 2-3 次 | 工单中心的 "+新建" 按钮,弹出一个精简表单(只填标题+客户+紧急度),详情后续补充 |
|
||||
| **快速记支出** | 每周 2-3 次 | 财务中心的"+记一笔"按钮,3 个字段:金额、分类、备注。日期默认今天 |
|
||||
| **快速创建报价** | 每周 1-2 次 | 从客户详情页一键"生成报价",自动带入客户信息 + 最近的项目工时数据 |
|
||||
| **快速创建工单 from 合同** | 每月 1-2 次 | 合同详情页"创建工单"按钮,自动关联合同+客户 |
|
||||
|
||||
**实现要点**:
|
||||
- 全局悬浮计时器通过前端组件实现,不依赖特定插件页面,放在 MainLayout 层
|
||||
- 快速操作按钮放在各页面的 PageHeader 区域,使用 Ant Design 的 `FloatButton` 或 `Button` 组件
|
||||
- 精简表单 = 只标记 `required = true` 的字段,其他字段全部可选,后续可补充
|
||||
|
||||
---
|
||||
|
||||
## 总结 -- 核心建议优先级
|
||||
|
||||
| 优先级 | 建议 | 预期收益 |
|
||||
|---|---|---|
|
||||
| P0 | 合并两个仪表盘为全局工作台 | 消除首次登录的困惑 |
|
||||
| P0 | 全局悬浮计时器(开始/停止) | 工时记录从"每周补"变成"实时记" |
|
||||
| P1 | 商机看板并入客户管理 tab | 减少 1 个页面,降低认知负担 |
|
||||
| P1 | 工时 -> 发票一键生成 | 消除最大手工流程,防漏收 |
|
||||
| P1 | 合同到期提醒 | 防止续费流失 |
|
||||
| P2 | 报价并入财务中心 tab | 减少 1 个页面 |
|
||||
| P2 | 巡检并入工单中心 tab | 减少 1 个页面 |
|
||||
| P2 | 全局搜索命令面板 (Ctrl+K) | 极大提升操作效率 |
|
||||
|
||||
**核心原则**:一人公司的 ERP 应该像瑞士军刀,不是像工具箱。不需要 10 个抽屉分门别类,需要一把刀随时打开就能用。
|
||||
@@ -1,50 +0,0 @@
|
||||
# Freelance + IT-OPS 插件技术评审
|
||||
|
||||
## 1. 实体数量合理性
|
||||
|
||||
**freelance 8 实体不过重。** 现有插件代码证实:WASM Guest 实现极其轻量(CRM 仅 30 行 Rust,只实现 Guest trait 的 3 个空方法),所有业务逻辑由 Host 侧的 `PluginDataService` + `DynamicTableManager` 通用处理。WASM 二进制不含 ORM/业务逻辑,因此 8 实体与 5 实体的 Guest 代码几乎无差别。manifest 解析(`manifest.rs`)和建表 DDL 均按 entity 循环处理,无硬上限。
|
||||
|
||||
真正的复杂度在前端页面数量和表间关联(quote/quote_line 父子关系),需确保 plugin.toml 的 `relations` 声明完整。
|
||||
|
||||
## 2. 跨插件引用性能
|
||||
|
||||
itops 4 个实体都引用 `freelance.client` 是合理的。代码显示跨插件引用走的是**同一数据库内 SQL 查询**(`resolve_cross_plugin_entity` 解析出 `plugin_erp-freelance_client` 表名后直接 JOIN/EXISTS),**没有 RPC 调用或跨服务开销**。列表查询时 `resolve_labels` 会批量解析 UUID→label,也是单次 IN 查询。
|
||||
|
||||
风险点:4 个实体同时查询时各做一次跨插件表 JOIN,并发高时需关注连接池。但每个查询都是标准 SQL,PostgreSQL 处理无压力。**建议 itops 的 client_id 字段设 `filterable = true` 使其走 generated column 索引。**
|
||||
|
||||
## 3. select 枚举字段声明
|
||||
|
||||
现有代码已完全支持。CRM plugin.toml 中有大量实例:
|
||||
|
||||
```toml
|
||||
[[schema.entities.fields]]
|
||||
name = "status"
|
||||
field_type = "string"
|
||||
ui_widget = "select"
|
||||
filterable = true
|
||||
options = [
|
||||
{ label = "草稿", value = "draft" },
|
||||
{ label = "已审核", value = "approved" },
|
||||
]
|
||||
```
|
||||
|
||||
字段类型声明为 `string`,枚举值通过 `options` 数组提供(`label` + `value`)。解析侧 `PluginField.options: Option<Vec<serde_json::Value>>` 兼容此格式。`filterable = true` 会自动创建 generated column + 索引以加速过滤查询。
|
||||
|
||||
## 4. WASM 体积预估
|
||||
|
||||
实测数据:
|
||||
- CRM (5 实体): **22 KB** (raw), **22.8 KB** (component)
|
||||
- Inventory (6 实体): **22 KB** (raw)
|
||||
- test-sample (含 Host API 回调测试): **109 KB**
|
||||
|
||||
freelance (8 实体) Guest 代码同样只有 Guest trait 空实现,预估 **22-23 KB**。体积与实体数量无关,取决于引入的 Host API 回调复杂度。itops 更小(4 实体),预估 22 KB。两者合计约 45 KB,对运行时内存和加载速度无影响。
|
||||
|
||||
## 5. 技术风险
|
||||
|
||||
1. **quote/quote_line 父子关系**:quote_line 引用 quote 是同插件内引用,需在 plugin.toml 中声明 `ref_entity = "quote"` + `relations` 的 `on_delete = "cascade"`。父实体删除时需级联软删除子记录 -- 当前 `validate_ref_entities` 只做引用存在性校验,级联软删除需确认 `DynamicTableManager` 是否支持(需检查 `on_delete: cascade` 在 list/create 流程中的实现)。
|
||||
|
||||
2. **itops 依赖声明**:`metadata.dependencies = ["erp-plugin-freelance"]`,但 `ref_plugin` 字段应填 manifest ID(即 `"erp-plugin-freelance"`)。需确认 manifest ID 与 Cargo crate name 的命名映射一致。
|
||||
|
||||
3. **freelance.client 需标记 `is_public = true`**:否则 itops 的跨插件 `ref_plugin` 查询会找不到目标实体。CRM 的 customer 已正确标记。
|
||||
|
||||
4. **权限码数量**:freelance 16 个权限码、itops 8 个,均在合理范围。注意每个实体必须声明 `.list` + `.manage`,缺 `.list` 会导致列表页 403。
|
||||
@@ -1,74 +0,0 @@
|
||||
# 智界科技 ERP 插件方案 -- 业务顾问分析
|
||||
|
||||
## 分析结论
|
||||
|
||||
### 1. 经营范围覆盖度
|
||||
|
||||
| 经营范围 | 覆盖插件 | 覆盖情况 |
|
||||
|----------|---------|---------|
|
||||
| 软件开发 | freelance(project/task) | 部分 -- 缺合同签约流程 |
|
||||
| AI 开发 | 无 | 未覆盖 |
|
||||
| 系统集成 | freelance(project) | 部分 |
|
||||
| 软件销售(批发+零售) | 无 | 未覆盖 |
|
||||
| IT 运维服务 | itops(service_contract/ticket/check_plan/check_record) | 覆盖良好 |
|
||||
| 软件外包 | freelance(project/task/time_entry) | 部分 |
|
||||
| IT 咨询 | freelance(opportunity/quote) | 部分 -- 缺知识产品化 |
|
||||
| 数字内容制作 | 无 | 未覆盖 |
|
||||
| 市场营销策划 | 无 | 未覆盖 |
|
||||
|
||||
**覆盖 5/9,遗漏 4 条。**
|
||||
|
||||
### 2. 最赚钱业务优先级
|
||||
|
||||
汕头市场实际排序:
|
||||
1. **软件开发 + AI 开发**(利润率 70-90%,一人公司最佳赛道)
|
||||
2. **IT 运维服务**(稳定年费收入,itops 已覆盖)
|
||||
3. **系统集成**(客单价高,freelance 的 project 可部分支撑)
|
||||
4. **软件销售批发零售**(需配合 inventory 插件)
|
||||
5. **IT 咨询**(高毛利但低频)
|
||||
|
||||
插件设计基本正确地优先了 1-3,但 freelance 插件缺少对"产品化销售"的支持。
|
||||
|
||||
### 3. 市场营销策划 -- 需要补充吗?
|
||||
|
||||
**不需要独立插件。** 原因:一人公司做营销策划,本质是卖自己的专业能力,核心需求是:
|
||||
- 客户管理(freelance.client 已覆盖)
|
||||
- 报价(freelance.quote 已覆盖)
|
||||
- 项目交付(freelance.project 已覆盖)
|
||||
|
||||
在 freelance 的 project 实体中增加 `type` 字段(枚举:software/ai/integration/consulting/marketing/content),即可区分不同业务线,无需新增插件。
|
||||
|
||||
### 4. 软硬件批发零售 -- inventory 需要配合吗?
|
||||
|
||||
**需要,但方式不同。** 软硬件批发零售有两种场景:
|
||||
- **代理分销**(从供应商进货再卖)-- 需要 inventory 插件管库存 + freelance 的 invoice 开票
|
||||
- **纯中介/推荐**(帮客户选型,供应商直发)-- 只需 freelance 的 quote + invoice,库存量写 0 或标记"虚拟商品"
|
||||
|
||||
建议:inventory 插件中增加 `product.type`(enum: physical/virtual/service),virtual 类型走零库存逻辑,physical 走完整进销存。freelance 的 invoice 关联 inventory 的 product 即可。
|
||||
|
||||
### 5. 数字内容制作 -- 需要什么?
|
||||
|
||||
**不需要独立插件。** 数字内容制作(网站、小程序、视频、设计稿等)本质是项目制交付,与软件开发共用同一套 project/task/time_entry 流程。在 freelance 的 project 增加 `deliverable_type`(enum: software/website/miniprogram/video/design/document)即可。
|
||||
|
||||
---
|
||||
|
||||
## 调整建议(300 字版)
|
||||
|
||||
**freelance 插件调整:**
|
||||
|
||||
1. **project 实体增加字段:**
|
||||
- `business_type`(enum: software_development/ai_development/system_integration/software_sales/it_outsourcing/it_consulting/marketing_planning/digital_content)-- 对齐 9 条经营范围
|
||||
- `deliverable_type`(enum: software/website/miniprogram/video/design/document/consulting_report)
|
||||
|
||||
2. **client 实体增加字段:**
|
||||
- `source`(enum: referral/marketing/tender/platform/repeat)-- 追踪客户来源,为营销策划提供数据
|
||||
|
||||
3. **新增 contract 实体:** 独立于 quote,合同签约、履约跟踪是法律实体,目前只有报价没有合同,这是 B2B 业务的核心缺失。字段:title/client_id/quote_id/amount/start_date/end_date/status/terms
|
||||
|
||||
4. **invoice 关联 product:** 增加 `line_items`(JSON 数组),每行关联 inventory 的 product_id + quantity + unit_price,打通软硬件销售闭环。
|
||||
|
||||
**itops 插件:保持不变,设计合理。**
|
||||
|
||||
**inventory 插件:** 增加 `product.type`(physical/virtual/service),virtual/service 走零库存逻辑。
|
||||
|
||||
**不新增独立插件。** 9 条经营范围通过 freelance 的分类字段 + inventory 配合即可全覆盖。一人公司最忌讳系统复杂度过高,三个插件(freelance + itops + inventory)足够。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,175 +0,0 @@
|
||||
# 三级可折叠侧边栏菜单 — 论证与实施计划
|
||||
|
||||
## Context
|
||||
|
||||
HMS 健康管理平台的侧边栏菜单在"健康管理"分组下已有 18 个平铺菜单项。随着功能继续增长(AI 模块、专科模块等),这个问题会持续恶化。用户提出将菜单改为可折叠的 3 级目录结构。
|
||||
|
||||
## 1. 论证分析
|
||||
|
||||
### 1.1 当前问题
|
||||
|
||||
| 指标 | 现状 | 趋势 |
|
||||
|------|------|------|
|
||||
| 健康管理下菜单项 | 18 个 | 还会增加(AI、血透、OCR) |
|
||||
| 侧边栏可视区域 | ~600px 高度 | 固定 |
|
||||
| 单项高度 | ~40px | 固定 |
|
||||
| 满屏可显示项数 | ~15 个 | — |
|
||||
| 溢出 | 是,需滚动 | 严重恶化 |
|
||||
|
||||
**结论:18 个平铺项已超出可视区域,必须滚动才能看到底部菜单。** 这违反了信息架构的基本原则——用户应在首屏看到完整的导航结构。
|
||||
|
||||
### 1.2 方案对比
|
||||
|
||||
| 方案 | 描述 | 优点 | 缺点 |
|
||||
|------|------|------|------|
|
||||
| A. 维持现状 | 平铺 2 级菜单 | 简单、无改动 | 菜单项越多越难用,不可接受 |
|
||||
| B. 3 级可折叠 | 在 directory 下再嵌套 directory | 归类清晰、按需展开、**数据库已支持** | 前端需改造渲染逻辑 |
|
||||
| C. Tab 切换 | 顶部 Tab 分域(患者/预约/管理) | 分区明确 | 破坏侧边栏导航范式,改动大 |
|
||||
| D. 搜索导航 | 搜索框替代层级导航 | 适合超多菜单 | 学习成本高,不适合当前规模 |
|
||||
|
||||
**推荐方案 B:3 级可折叠目录。**
|
||||
|
||||
核心理由:
|
||||
1. **数据库零改动** — `menus` 表的 `parent_id` 自引用和 `build_tree()` 递归构建已支持 N 级嵌套
|
||||
2. **前端改动小** — `DynamicMenuSection` 只需加递归渲染,`SidebarSubMenu` 的展开/折叠逻辑可直接复用
|
||||
3. **向后兼容** — 没有子分组的 directory 仍按原有方式渲染(2 级),有子分组的自动变为 3 级
|
||||
4. **可扩展** — 未来第 4 级也自然支持
|
||||
|
||||
### 1.3 提出的分组结构
|
||||
|
||||
将"健康管理"下的 18 个菜单项按业务域归入 5 个子分组:
|
||||
|
||||
```
|
||||
健康管理
|
||||
├── 患者管理 (icon: TeamOutlined)
|
||||
│ ├── 患者列表
|
||||
│ ├── 医护档案
|
||||
│ └── 健康档案
|
||||
├── 预约排班 (icon: CalendarOutlined)
|
||||
│ ├── 预约管理
|
||||
│ └── 排班管理
|
||||
├── 随访咨询 (icon: PhoneOutlined)
|
||||
│ ├── 随访管理
|
||||
│ └── 咨询管理
|
||||
├── 健康数据 (icon: HeartOutlined)
|
||||
│ ├── 体征监测
|
||||
│ ├── 化验报告
|
||||
│ ├── 健康趋势
|
||||
│ ├── 诊断记录
|
||||
│ ├── 过敏管理
|
||||
│ └── 血透记录
|
||||
├── 内容运营 (icon: FileTextOutlined)
|
||||
│ ├── 文章管理
|
||||
│ ├── 分类管理
|
||||
│ └── 标签管理
|
||||
└── 综合管理 (icon: TrophyOutlined)
|
||||
├── 积分管理
|
||||
├── 线下活动
|
||||
└── 统计分析
|
||||
```
|
||||
|
||||
其他顶级分组(仪表盘、系统管理、工作流、消息中心、AI 智能分析)保持不变。
|
||||
|
||||
### 1.4 反对意见与回应
|
||||
|
||||
**反对:增加一级嵌套增加点击次数。**
|
||||
回应:折叠状态下子分组只占一行(~40px),18 项从 ~720px 压缩到 ~200px。用户只需展开自己关注的子域,实际点击次数不会增加,反而因为分类清晰减少了寻找时间。
|
||||
|
||||
**反对:3 级菜单对用户认知负担更重。**
|
||||
回应:当前 18 个平铺项的认知负担远大于 5 个分类名称。分组名称("患者管理"、"预约排班")本身是业务语义,不需要额外学习。
|
||||
|
||||
## 2. 技术分析
|
||||
|
||||
### 2.1 后端:零改动
|
||||
|
||||
- `menus` 表 `parent_id` 自引用已支持任意层级
|
||||
- `build_tree()` 递归构建已生成完整嵌套 JSON
|
||||
- `MenuInfo.children` 前端类型已是递归结构
|
||||
- **不需要修改任何 Rust 代码**
|
||||
|
||||
### 2.2 数据层:插入子分组记录
|
||||
|
||||
在 `menus` 表中为"健康管理"目录插入 6 个子 directory,然后将现有菜单项的 `parent_id` 指向对应子 directory。
|
||||
|
||||
```
|
||||
INSERT 新记录: 6 个 sub-directory (parent_id = 健康管理目录的 id)
|
||||
UPDATE 现有记录: 18 个菜单项的 parent_id → 对应子 directory 的 id
|
||||
```
|
||||
|
||||
可通过 SQL 迁移或直接 UPDATE 实现。
|
||||
|
||||
### 2.3 前端改造:DynamicMenuSection 递归化
|
||||
|
||||
**当前代码**([MainLayout.tsx:199-256](apps/web/src/layouts/MainLayout.tsx#L199-L256)):
|
||||
- 遍历 `menus`,directory → 渲染标题 + 遍历 children
|
||||
- children 只渲染为 `SidebarMenuItem`(叶子节点),不再检查嵌套
|
||||
|
||||
**改造目标**:
|
||||
- directory 有 children → 检查 children 中是否有 sub-directory
|
||||
- 如果有 sub-directory → 用 `SidebarSubMenu` 的展开/折叠模式渲染
|
||||
- 递归调用自身,支持任意深度
|
||||
|
||||
**复用的模式**:`SidebarSubMenu`([MainLayout.tsx:136-196](apps/web/src/layouts/MainLayout.tsx#L136-L196))的展开/折叠逻辑:
|
||||
- `useState(true)` 管理展开状态
|
||||
- `RightOutlined` 箭头旋转动画
|
||||
- 折叠状态下 Tooltip 显示子菜单列表
|
||||
- `hasActive` 检测高亮
|
||||
|
||||
### 2.4 关键文件
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `apps/web/src/layouts/MainLayout.tsx` | `DynamicMenuSection` 改为递归渲染 |
|
||||
| 数据库(SQL 迁移或直接 UPDATE) | 插入 6 个子 directory + 更新 18 项 parent_id |
|
||||
|
||||
## 3. 实施步骤
|
||||
|
||||
### Step 1: 数据层 — 菜单重组
|
||||
|
||||
直接用 SQL 更新现有菜单数据(不需要新建迁移文件,因为这是数据变更不是 schema 变更):
|
||||
|
||||
1. 查出"健康管理"目录的 `id`
|
||||
2. INSERT 6 个 sub-directory 记录(`menu_type = 'directory'`,`parent_id` 指向健康管理)
|
||||
3. UPDATE 18 个菜单项的 `parent_id` 指向对应 sub-directory
|
||||
|
||||
### Step 2: 前端 — DynamicMenuSection 递归化
|
||||
|
||||
将 `DynamicMenuSection` 改为支持递归渲染:
|
||||
|
||||
1. 提取一个通用 `MenuNode` 组件,接收 `MenuInfo` 递归渲染
|
||||
2. `menu_type === 'directory'` → 渲染分组标题 + 递归渲染 children
|
||||
3. children 中如果是 directory → 用展开/折叠样式(复用 SidebarSubMenu 的箭头模式)
|
||||
4. children 中如果是 menu → 渲染 `SidebarMenuItem`
|
||||
5. 折叠侧边栏时:sub-directory 显示 Tooltip,首项可点击
|
||||
|
||||
### Step 3: 样式微调
|
||||
|
||||
- sub-directory 标题增加左侧缩进(`padding-left: 24px`)
|
||||
- 展开动画过渡
|
||||
- 活跃路径自动展开父级目录
|
||||
|
||||
### Step 4: 验证
|
||||
|
||||
1. `pnpm build` 前端编译通过
|
||||
2. 浏览器验证:侧边栏显示 3 级结构
|
||||
3. 子分组可展开/折叠
|
||||
4. 折叠侧边栏时 Tooltip 正确显示
|
||||
5. 当前页面所在分组自动展开
|
||||
6. 其他顶级分组(系统管理、工作流等)不受影响
|
||||
|
||||
## 4. 工作量估计
|
||||
|
||||
| 步骤 | 预计时间 |
|
||||
|------|---------|
|
||||
| Step 1 数据重组 | 30 分钟 |
|
||||
| Step 2 前端递归化 | 1-2 小时 |
|
||||
| Step 3 样式微调 | 30 分钟 |
|
||||
| Step 4 验证 | 30 分钟 |
|
||||
| **总计** | **3-4 小时** |
|
||||
|
||||
## 5. 验证方式
|
||||
|
||||
- `pnpm build` 编译无错误
|
||||
- 浏览器实际操作:展开/折叠/导航/折叠侧边栏
|
||||
- 确认所有原有路由可正常访问
|
||||
- 确认插件菜单不受影响
|
||||
@@ -1,488 +0,0 @@
|
||||
# 插件系统增强设计规格
|
||||
|
||||
## Context
|
||||
|
||||
插件系统是 ERP 平台的核心差异化能力,当前声明式层面(manifest schema、动态表、前端页面)已达 90% 成熟度。但 WASM 逻辑层存在根本性限制:
|
||||
|
||||
1. **插件无法自主查询数据** — `db_query` 的 filter/pagination 参数被忽略,只能使用预填充结果
|
||||
2. **无读后写一致性** — 延迟刷新模型导致插件在一次调用中无法读取自己刚写入的数据
|
||||
3. **聚合只有 COUNT** — 缺少 SUM/AVG/MAX/MIN,无法支撑财务、统计类场景
|
||||
4. **热更新无原子回滚** — 旧版本先卸载再加载新版本,中间失败无保障
|
||||
5. **Schema 变更只支持新增实体** — 不支持已有实体的字段演进
|
||||
|
||||
这些限制使插件系统只能支撑"数据管理+展示"型轻量场景(CRM、简单进销存),无法支撑需要复杂业务逻辑的行业(财务、制造、电商)。
|
||||
|
||||
本次增强的目标:**让插件逻辑层从 40% 提升到 80%+,使系统能真正承载不同行业的定制化需求。**
|
||||
|
||||
---
|
||||
|
||||
## 改动 1:混合执行模型(解决查询和读后写一致性)
|
||||
|
||||
### 问题
|
||||
|
||||
`host.rs:99-109` — `db_query` 忽略 `_filter` 和 `_pagination` 参数,只从 `query_results` 预填充缓存取数据。插件无法自主构造查询。
|
||||
|
||||
### 方案:读操作走实时 SQL + 写操作保持延迟批量 + 读前自动 flush
|
||||
|
||||
**核心流程变更:**
|
||||
|
||||
```
|
||||
当前:
|
||||
WASM 调用 db_insert() → 入队 pending_ops
|
||||
WASM 调用 db_query() → 从预填充缓存读(忽略 filter/pagination)
|
||||
WASM 结束 → flush 全部 pending_ops
|
||||
|
||||
改为:
|
||||
WASM 调用 db_insert() → 入队 pending_ops
|
||||
WASM 调用 db_query() → 先 flush pending_ops → 执行真实 SQL 查询 → 返回结果
|
||||
WASM 结束 → flush 剩余 pending_ops
|
||||
```
|
||||
|
||||
### 改动文件
|
||||
|
||||
#### 1. `crates/erp-plugin/src/host.rs`
|
||||
|
||||
**HostState 新增字段:**
|
||||
|
||||
```rust
|
||||
pub struct HostState {
|
||||
// ... 现有字段保留 ...
|
||||
// 新增:用于实时查询的数据服务引用和数据库连接
|
||||
pub(crate) db: Option<DatabaseConnection>,
|
||||
pub(crate) data_service_ready: bool,
|
||||
}
|
||||
```
|
||||
|
||||
**db_query 实现变更:**
|
||||
|
||||
```rust
|
||||
fn db_query(&mut self, entity: String, filter: Vec<u8>, pagination: Vec<u8>)
|
||||
-> Result<Vec<u8>, String>
|
||||
{
|
||||
// 如果没有数据库连接(向后兼容预填充模式),走旧路径
|
||||
if self.db.is_none() {
|
||||
return self.query_results
|
||||
.get(&entity)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("实体 '{}' 的查询结果未预填充", entity));
|
||||
}
|
||||
|
||||
// 解析 filter 和 pagination 参数
|
||||
let filter_val: Option<serde_json::Value> = if filter.is_empty() {
|
||||
None
|
||||
} else {
|
||||
serde_json::from_slice(&filter).ok()
|
||||
};
|
||||
|
||||
let pagination_val: Option<serde_json::Value> = if pagination.is_empty() {
|
||||
None
|
||||
} else {
|
||||
serde_json::from_slice(&pagination).ok()
|
||||
};
|
||||
|
||||
// 先同步 flush pending writes(确保读后写一致性)
|
||||
// 注意:在 WASM 的 spawn_blocking 上下文中,需要同步执行
|
||||
// 方案:将 pending_ops 暂存到临时变量,由调用方在 execute_wasm 中处理
|
||||
|
||||
// 使用 pre_query_ops 标记,让 engine 在 execute_wasm 中间阶段 flush
|
||||
self.pre_query_ops = std::mem::take(&mut self.pending_ops);
|
||||
self.pending_query = Some(PendingQuery { entity, filter_val, pagination_val });
|
||||
|
||||
// 返回占位符 — 真正的查询在 execute_wasm 的两阶段执行中完成
|
||||
Ok(serde_json::to_vec(&serde_json::json!({"status": "query_pending"})).unwrap_or_default())
|
||||
}
|
||||
```
|
||||
|
||||
**实际实现策略 — 采用回调模式:**
|
||||
|
||||
由于 `db_query` 在 `spawn_blocking` 内执行,不能直接 await 异步数据库操作。采用两阶段执行:
|
||||
|
||||
1. WASM 执行期间:`db_query` 收集查询参数,设置 `needs_flush_and_query = true`
|
||||
2. `execute_wasm` 的 `spawn_blocking` 结束后:检查标志,如果需要查询则:
|
||||
- flush pending_ops
|
||||
- 执行查询
|
||||
- 用查询结果重新调用 WASM(继续执行后续逻辑)
|
||||
|
||||
**更好的方案 — 分段执行:**
|
||||
|
||||
将 `execute_wasm` 改为分段执行模型:
|
||||
|
||||
```rust
|
||||
async fn execute_wasm(&self, ...) -> PluginResult<R> {
|
||||
// 阶段 1:执行 WASM,遇到 db_query 时暂停
|
||||
let (result, pending_ops, pending_queries) = tokio::task::spawn_blocking(move || {
|
||||
// WASM 执行中遇到 db_query 时,收集查询参数并设置标志
|
||||
// 标志在 HostState 中:self.needs_query = true, self.query_params = ...
|
||||
// WASM 继续执行(db_query 返回空结果集作为占位)
|
||||
// ...
|
||||
}).await?;
|
||||
|
||||
// 中间阶段:flush writes + execute queries
|
||||
Self::flush_ops(&self.db, plugin_id, pending_ops, ...).await?;
|
||||
let query_results = Self::execute_queries(&self.db, plugin_id, pending_queries, ...).await?;
|
||||
|
||||
// 阶段 2:如果有待处理的查询,重新执行 WASM(或继续后续逻辑)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**最终推荐方案 — 简化版:**
|
||||
|
||||
实际上最简单的做法是:**让 db_query 同步执行真实查询**。在 `spawn_blocking` 中使用 `tokio::runtime::Handle` 来在阻塞线程中执行异步代码。
|
||||
|
||||
```rust
|
||||
fn db_query(&mut self, entity: String, filter: Vec<u8>, pagination: Vec<u8>)
|
||||
-> Result<Vec<u8>, String>
|
||||
{
|
||||
let db = self.db.as_ref().ok_or("数据库连接不可用")?;
|
||||
|
||||
// 先 flush pending writes(通过 tokio handle 在阻塞上下文中执行异步)
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
let ops = std::mem::take(&mut self.pending_ops);
|
||||
if !ops.is_empty() {
|
||||
rt.block_on(Self::flush_ops_static(db, &self.plugin_id, ops,
|
||||
self.tenant_id, self.user_id, &self.event_bus))
|
||||
.map_err(|e| format!("flush 失败: {}", e))?;
|
||||
}
|
||||
|
||||
// 解析 filter
|
||||
let filter_val: Option<serde_json::Value> = if filter.is_empty() {
|
||||
None
|
||||
} else {
|
||||
serde_json::from_slice(&filter).ok()
|
||||
};
|
||||
|
||||
// 构建并执行查询
|
||||
let table_name = DynamicTableManager::table_name(&self.plugin_id, &entity);
|
||||
let (sql, values) = DynamicTableManager::build_query_sql(
|
||||
&table_name, self.tenant_id, filter_val, pagination_val
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
let rows: Vec<serde_json::Value> = rt.block_on(async {
|
||||
// 执行查询
|
||||
}).map_err(|e| e.to_string())?;
|
||||
|
||||
serde_json::to_vec(&rows).map_err(|e| e.to_string())
|
||||
}
|
||||
```
|
||||
|
||||
**改动影响:**
|
||||
- `HostState` 增加 `db: Option<DatabaseConnection>` 和 `event_bus: Option<EventBus>` 字段
|
||||
- `execute_wasm` 创建 HostState 时传入 db 和 event_bus
|
||||
- `db_query` 从忽略参数改为实时查询
|
||||
- `PluginEngine::new` 已持有 db 和 event_bus,无需新增依赖
|
||||
|
||||
#### 2. `crates/erp-plugin/src/dynamic_table.rs`
|
||||
|
||||
新增 `build_query_sql` 方法,复用现有 `data_service.rs` 的查询构建逻辑:
|
||||
|
||||
```rust
|
||||
pub fn build_query_sql(
|
||||
table_name: &str,
|
||||
tenant_id: Uuid,
|
||||
filter: Option<serde_json::Value>,
|
||||
pagination: Option<serde_json::Value>,
|
||||
) -> Result<(String, Vec<sea_orm::Value>)>
|
||||
```
|
||||
|
||||
### 向后兼容
|
||||
|
||||
- `HostState::new()` 不传 db → `db = None` → 走旧的预填充路径
|
||||
- `execute_wasm()` 传 db → 走新的实时查询路径
|
||||
- 现有 WASM 插件无需修改(旧路径仍然可用)
|
||||
|
||||
---
|
||||
|
||||
## 改动 2:扩展聚合查询
|
||||
|
||||
### 问题
|
||||
|
||||
`data_service.rs:655` 的 `aggregate` 方法只支持 `GROUP BY + COUNT(*)`,返回 `Vec<(String, i64)>`。
|
||||
|
||||
### 方案
|
||||
|
||||
扩展聚合函数支持 SUM/AVG/MAX/MIN。
|
||||
|
||||
#### 改动文件
|
||||
|
||||
**1. `crates/erp-plugin/src/data_service.rs`**
|
||||
|
||||
新增多聚合函数方法:
|
||||
|
||||
```rust
|
||||
pub struct AggregateResult {
|
||||
pub key: String,
|
||||
pub metrics: HashMap<String, f64>, // "count" -> 10, "total_amount" -> 5000.0
|
||||
}
|
||||
|
||||
pub async fn aggregate_multi(
|
||||
plugin_id: Uuid,
|
||||
entity_name: &str,
|
||||
tenant_id: Uuid,
|
||||
db: &DatabaseConnection,
|
||||
group_by_field: &str,
|
||||
aggregations: &[AggregateDef], // [{field: "amount", func: "sum"}, ...]
|
||||
filter: Option<serde_json::Value>,
|
||||
scope: Option<DataScopeParams>,
|
||||
) -> AppResult<Vec<AggregateResult>>
|
||||
|
||||
pub struct AggregateDef {
|
||||
pub field: String,
|
||||
pub func: AggregateFunc,
|
||||
}
|
||||
|
||||
pub enum AggregateFunc {
|
||||
Count,
|
||||
Sum,
|
||||
Avg,
|
||||
Min,
|
||||
Max,
|
||||
}
|
||||
```
|
||||
|
||||
SQL 构建示例:
|
||||
|
||||
```sql
|
||||
SELECT _f_status as key,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(_f_amount), 0) as sum_amount,
|
||||
COALESCE(AVG(_f_price), 0) as avg_price
|
||||
FROM plugin_erp_crm__order
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
GROUP BY _f_status
|
||||
```
|
||||
|
||||
**2. `crates/erp-plugin/src/dynamic_table.rs`**
|
||||
|
||||
新增 `build_aggregate_multi_sql` 方法,构建多聚合 SQL。
|
||||
|
||||
**3. `crates/erp-plugin/src/data_handler.rs`**
|
||||
|
||||
扩展聚合 API 端点,接受 `aggregations` 参数:
|
||||
|
||||
```json
|
||||
POST /api/v1/plugins/{pluginId}/data/{entityName}/aggregate
|
||||
{
|
||||
"group_by": "status",
|
||||
"aggregations": [
|
||||
{"field": "amount", "func": "sum"},
|
||||
{"field": "price", "func": "avg"}
|
||||
],
|
||||
"filter": {"status": "active"}
|
||||
}
|
||||
```
|
||||
|
||||
**4. 前端 Dashboard Widget 适配**
|
||||
|
||||
`PluginDashboardPage.tsx` 中的 `stat_card` 和图表 widget 需要适配新的多聚合返回格式。
|
||||
|
||||
---
|
||||
|
||||
## 改动 3:热更新原子回滚
|
||||
|
||||
### 问题
|
||||
|
||||
`service.rs:578-585` — 升级时先 `unload(old)` 再 `load(new)`,如果 `load` 失败,旧版本已不在内存中。
|
||||
|
||||
### 方案:先加载新版本,成功后原子替换
|
||||
|
||||
#### 改动文件
|
||||
|
||||
**`crates/erp-plugin/src/service.rs`** — `upgrade` 方法:
|
||||
|
||||
```rust
|
||||
// 当前(有风险):
|
||||
engine.unload(plugin_manifest_id).await.ok(); // 旧版本已卸载
|
||||
engine.load(plugin_manifest_id, &new_wasm, manifest) // 如果这里失败 → 无回滚
|
||||
.await?;
|
||||
|
||||
// 改为(安全):
|
||||
// 1. 先加载新版本(用临时 key)
|
||||
let temp_id = format!("{}__upgrade_{}", plugin_manifest_id, Uuid::now_v7());
|
||||
engine.load(&temp_id, &new_wasm, new_manifest.clone()).await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "新版本 WASM 加载失败,旧版本仍在运行");
|
||||
e
|
||||
})?;
|
||||
|
||||
// 2. 卸载旧版本
|
||||
engine.unload(plugin_manifest_id).await.ok();
|
||||
|
||||
// 3. 将新版本从临时 key 改为正式 key
|
||||
engine.rename_plugin(&temp_id, plugin_manifest_id).await?;
|
||||
|
||||
// 4. 更新数据库记录
|
||||
```
|
||||
|
||||
**`crates/erp-plugin/src/engine.rs`** — 新增 `rename_plugin` 方法:
|
||||
|
||||
```rust
|
||||
pub async fn rename_plugin(&self, old_id: &str, new_id: &str) -> PluginResult<()> {
|
||||
let loaded = self.plugins.remove(old_id)
|
||||
.ok_or_else(|| PluginError::NotFound(old_id.to_string()))?;
|
||||
let mut loaded = Arc::try_unwrap(loaded.1)
|
||||
.map_err(|_| PluginError::ExecutionError("插件仍被引用".to_string()))?;
|
||||
loaded.id = new_id.to_string();
|
||||
self.plugins.insert(new_id.to_string(), Arc::new(loaded));
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**改进后的安全保证:**
|
||||
- 新版本加载失败 → 旧版本仍在运行,零停机
|
||||
- 数据库记录只在 WASM 替换成功后才更新
|
||||
- 事务性:要么完全切换到新版本,要么保持旧版本
|
||||
|
||||
---
|
||||
|
||||
## 改动 4:Schema 演进(ALTER TABLE 支持)
|
||||
|
||||
### 问题
|
||||
|
||||
`service.rs:562-575` — 升级时只处理新增实体(CREATE TABLE),不处理已有实体的字段变更。
|
||||
|
||||
### 方案:利用 JSONB 特性实现轻量级 Schema 演进
|
||||
|
||||
由于核心数据在 JSONB 的 `data` 列中,大部分字段变更不需要 DDL:
|
||||
|
||||
- **新增字段**:JSONB 天然支持,只需更新 manifest
|
||||
- **新增 filterable/sortable 字段**:需要 ALTER TABLE ADD Generated Column + 索引
|
||||
- **删除字段**:JSONB 中多余字段不影响,Generated Column 可保留(无害)
|
||||
- **重命名字段**:添加新 Generated Column,旧的保留
|
||||
- **修改字段类型**:Generated Column 需要 DROP + ADD(JSONB 数据不需要改)
|
||||
|
||||
#### 改动文件
|
||||
|
||||
**`crates/erp-plugin/src/service.rs`** — `upgrade` 方法增加 schema diff 逻辑:
|
||||
|
||||
```rust
|
||||
// 对比 schema 变更
|
||||
if let Some(new_schema) = &new_manifest.schema {
|
||||
let old_schema = old_manifest.schema.as_ref();
|
||||
|
||||
for new_entity in &new_schema.entities {
|
||||
let old_entity = old_schema
|
||||
.and_then(|s| s.entities.iter().find(|e| e.name == new_entity.name));
|
||||
|
||||
match old_entity {
|
||||
None => {
|
||||
// 全新实体 — CREATE TABLE
|
||||
DynamicTableManager::create_table(db, plugin_manifest_id, new_entity).await?;
|
||||
}
|
||||
Some(old) => {
|
||||
// 已有实体 — diff 字段
|
||||
let diff = diff_entity_fields(old, new_entity);
|
||||
if !diff.new_filterable.is_empty() || !diff.new_sortable.is_empty() {
|
||||
DynamicTableManager::alter_add_generated_columns(
|
||||
db, plugin_manifest_id, new_entity, &diff
|
||||
).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`crates/erp-plugin/src/dynamic_table.rs`** — 新增:
|
||||
|
||||
```rust
|
||||
pub struct FieldDiff {
|
||||
pub new_filterable: Vec<PluginField>, // 新增的需要 Generated Column 的字段
|
||||
pub new_sortable: Vec<PluginField>,
|
||||
pub new_searchable: Vec<PluginField>, // 新增的需要 pg_trgm 索引的字段
|
||||
}
|
||||
|
||||
pub fn diff_entity_fields(old: &PluginEntity, new: &PluginEntity) -> FieldDiff
|
||||
|
||||
pub async fn alter_add_generated_columns(
|
||||
db: &DatabaseConnection,
|
||||
plugin_id: &str,
|
||||
entity: &PluginEntity,
|
||||
diff: &FieldDiff,
|
||||
) -> PluginResult<()>
|
||||
```
|
||||
|
||||
ALTER TABLE 示例:
|
||||
|
||||
```sql
|
||||
-- 新增 filterable 字段
|
||||
ALTER TABLE plugin_erp_crm__customer
|
||||
ADD COLUMN IF NOT EXISTS _f_source TEXT GENERATED ALWAYS AS (data->>'source') STORED;
|
||||
|
||||
-- 新增索引
|
||||
CREATE INDEX IF NOT EXISTS idx_plugin_erp_crm__customer__f_source
|
||||
ON plugin_erp_crm__customer (_f_source) WHERE deleted_at IS NULL;
|
||||
|
||||
-- 新增 searchable 字段的 pg_trgm 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_plugin_erp_crm__customer__f_source_trgm
|
||||
ON plugin_erp_crm__customer USING gin (_f_source gin_trgm_ops)
|
||||
WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序
|
||||
|
||||
| 阶段 | 改动 | 复杂度 | 影响范围 |
|
||||
|------|------|--------|---------|
|
||||
| 1 | 热更新原子回滚 | 低 | engine.rs + service.rs |
|
||||
| 2 | Schema 演进(ALTER TABLE) | 中低 | service.rs + dynamic_table.rs |
|
||||
| 3 | 扩展聚合查询 | 中 | data_service.rs + data_handler.rs + dynamic_table.rs |
|
||||
| 4 | 混合执行模型(查询能力) | 高 | host.rs + engine.rs + dynamic_table.rs |
|
||||
|
||||
建议按复杂度从低到高实施,每个阶段独立可验证。
|
||||
|
||||
---
|
||||
|
||||
## 验证方案
|
||||
|
||||
### 阶段 1:热更新回滚
|
||||
|
||||
1. 准备两个版本的 CRM 插件 WASM(v1.0.0 和 v2.0.0)
|
||||
2. 上传 v2.0.0 但故意让 WASM 二进制损坏
|
||||
3. 验证:旧版本 v1.0.0 仍在正常运行
|
||||
4. 上传正确的 v2.0.0
|
||||
5. 验证:成功切换到 v2.0.0
|
||||
|
||||
### 阶段 2:Schema 演进
|
||||
|
||||
1. 创建 CRM 插件 v1.0.0(含 customer 实体,3 个字段)
|
||||
2. 升级到 v1.1.0(customer 增加 2 个 filterable 字段 + 1 个新实体 contact)
|
||||
3. 验证:新字段可以过滤/排序,旧数据不受影响
|
||||
4. 在已有数据上验证新 Generated Column 的值正确填充
|
||||
|
||||
### 阶段 3:聚合查询
|
||||
|
||||
1. 创建测试数据(不同状态的订单,含 amount 字段)
|
||||
2. 调用聚合 API:group_by=status, aggregations=[sum(amount), avg(amount)]
|
||||
3. 验证返回结果正确
|
||||
4. 前端 Dashboard stat_card 展示正确的聚合数据
|
||||
|
||||
### 阶段 4:混合执行模型
|
||||
|
||||
1. 在插件 WASM 中调用 db_insert 后立即 db_query
|
||||
2. 验证能读取到刚插入的数据(读后写一致性)
|
||||
3. 验证带 filter 参数的 db_query 返回正确过滤结果
|
||||
4. 验证旧插件(使用预填充模式)仍能正常工作
|
||||
5. 压力测试:多次连续 db_query 不超过 Fuel 限制
|
||||
|
||||
---
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
| 文件 | 改动类型 |
|
||||
|------|---------|
|
||||
| `crates/erp-plugin/src/host.rs` | 重构 db_query + 新增 db/事件总线字段 |
|
||||
| `crates/erp-plugin/src/engine.rs` | 调整 execute_wasm + 新增 rename_plugin |
|
||||
| `crates/erp-plugin/src/service.rs` | 升级流程回滚安全 + schema diff |
|
||||
| `crates/erp-plugin/src/dynamic_table.rs` | 新增 build_query_sql + alter_add_generated_columns + diff_entity_fields |
|
||||
| `crates/erp-plugin/src/data_service.rs` | 新增 aggregate_multi + AggregateDef |
|
||||
| `crates/erp-plugin/src/data_handler.rs` | 扩展聚合 API |
|
||||
| `apps/web/src/pages/PluginDashboardPage.tsx` | 适配多聚合返回格式 |
|
||||
|
||||
### 可复用的现有函数
|
||||
|
||||
- `DynamicTableManager::build_query_sql` — 可复用 `data_service.rs` 中的查询构建逻辑
|
||||
- `DynamicTableManager::build_insert_sql` — flush 时已有,无需改动
|
||||
- `sanitize_identifier` — 已有,用于新字段名的安全检查
|
||||
- `flush_ops` — 已有事务性 flush 逻辑,混合模型中复用
|
||||
@@ -1,275 +0,0 @@
|
||||
# Code Review: ERP Platform Base Design Specification
|
||||
|
||||
**Document reviewed:** `G:\erp\docs\superpowers\specs\2026-04-10-erp-platform-base-design.md`
|
||||
**Verdict:** ISSUES FOUND
|
||||
|
||||
---
|
||||
|
||||
## What Is Done Well
|
||||
|
||||
The spec demonstrates several strengths before I get into the issues:
|
||||
|
||||
1. **Clear architectural vision.** The modular monolith with progressive extraction is a sound strategy for a product targeting small-to-large enterprises. It avoids premature microservice complexity while preserving the extraction path.
|
||||
|
||||
2. **Crate structure is well-considered.** Separating `erp-core` (traits, types, events) from `erp-common` (utilities, macros) and keeping each business module as its own crate is the right granularity for Rust workspace management and future extraction.
|
||||
|
||||
3. **RBAC + ABAC hybrid permission model.** This is the correct approach for a multi-tenant ERP. Pure RBAC breaks down at scale; ABAC alone is hard to administer. The hybrid is pragmatic.
|
||||
|
||||
4. **UUID v7 as primary keys.** Time-sortable UUIDs are the right choice for distributed systems and work well with PostgreSQL's B-tree indexes.
|
||||
|
||||
5. **Soft delete via `deleted_at`.** Appropriate for an ERP where audit trails are non-negotiable.
|
||||
|
||||
6. **Database design principles.** The universal columns (`tenant_id`, `created_at`, `updated_at`, `created_by`, `updated_by`) are a solid foundation.
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL Issues (Must Fix Before Implementation)
|
||||
|
||||
### C1. No Error Handling Strategy Defined
|
||||
|
||||
The spec lists `erp-core` as containing "error handling" but provides zero detail on the error model. For a Rust codebase, this is an immediate blocker because error types pervade every crate boundary.
|
||||
|
||||
**What is missing:**
|
||||
- A unified error type hierarchy (domain errors vs infrastructure errors vs API errors)
|
||||
- How module-level errors map to HTTP responses (status codes, error envelopes)
|
||||
- Whether a single `erp-core::Error` enum or per-module error types with `From` conversions
|
||||
- Error chain propagation strategy (anyhow vs thiserror vs custom)
|
||||
- How validation errors are represented and returned to the client
|
||||
|
||||
**Recommendation:** Add a section defining the error architecture. The standard Rust approach for this kind of project is `thiserror` for library crates with per-module error enums, converting to a unified API error type in `erp-server`. Define the HTTP error response envelope (status, code, message, details).
|
||||
|
||||
### C2. Event Bus Specification Is Absent
|
||||
|
||||
The spec states "Modules communicate via event bus, no direct coupling" as a core design principle but provides zero detail on:
|
||||
- The event bus implementation (in-process? tokio channels? trait-object dispatch?)
|
||||
- Event schema and versioning strategy
|
||||
- Event delivery guarantees (at-least-once? exactly-once?)
|
||||
- Event persistence (are events stored for audit/replay?)
|
||||
- Dead letter handling
|
||||
- How cross-module event subscriptions are registered
|
||||
|
||||
This is not an implementation detail -- it is a load-bearing architectural decision. The wrong choice here is extremely expensive to change later.
|
||||
|
||||
**Recommendation:** Add an "Event System" subsection under Architecture that specifies the event bus mechanism, event schema, delivery semantics, and at minimum the key events exchanged between the four core modules.
|
||||
|
||||
### C3. API Versioning and Contract Strategy Is Undefined
|
||||
|
||||
The APIs all use `/api/v1/` prefixes, but there is no statement on:
|
||||
- How breaking changes are managed across versions
|
||||
- Whether API versioning is URL-path-based (current) or header-based
|
||||
- How the Tauri client and backend version compatibility is maintained
|
||||
- Whether API contracts are formally defined (OpenAPI generation from code vs. design-first)
|
||||
|
||||
**Recommendation:** Add an API governance section. At minimum: the versioning strategy, the OpenAPI generation approach (utoipa is listed, but is it code-first or spec-first?), and the client-server compatibility contract.
|
||||
|
||||
### C4. No Data Migration Strategy for Multi-Tenant Schema Changes
|
||||
|
||||
The spec mentions SeaORM Migrations but does not address the multi-tenant reality:
|
||||
- How are schema migrations applied across tenants?
|
||||
- For the "independent schema per tenant" option, how is per-tenant migration managed?
|
||||
- How are tenant-specific data migrations (dictionary changes, menu changes) handled separately from schema migrations?
|
||||
- What happens when a migration fails for one tenant but succeeds for others?
|
||||
|
||||
**Recommendation:** Add a migration strategy section that covers schema migration in the shared-database model, per-tenant schema model, and data seeding for new tenants.
|
||||
|
||||
### C5. Workflow Engine BPMN 2.0 Compatibility Is Underspecified for Implementation
|
||||
|
||||
The workflow engine is listed as Phase 4 (3-4 weeks) and claims "BPMN 2.0 compatible," but implementing a BPMN engine from scratch in 3-4 weeks is not realistic. BPMN 2.0 is a massive specification (~500 pages). The spec does not specify:
|
||||
- Which BPMN elements are in scope for Phase 4 vs. later phases
|
||||
- How BPMN XML is parsed, stored, and rendered
|
||||
- The expression language for conditions (mentions "EL expressions" but does not specify the engine)
|
||||
- How subprocesses and call activities interact with multi-tenancy
|
||||
|
||||
**Recommendation:** Define a BPMN subset for Phase 4. A realistic Phase 4 scope would be: Start/End nodes, UserTask, ServiceTask, ExclusiveGateway, ParallelGateway, sequence flows with simple conditions. SubProcesses, InclusiveGateways, timers, and signals should be deferred.
|
||||
|
||||
---
|
||||
|
||||
## IMPORTANT Issues (Should Fix Before Implementation)
|
||||
|
||||
### I1. No Deployment Architecture or Operations Section
|
||||
|
||||
For a commercial SaaS product, the spec lacks:
|
||||
- Production deployment topology (containers, orchestration, load balancing)
|
||||
- How Tauri desktop clients connect to the backend (direct? reverse proxy? CDN?)
|
||||
- Database connection pooling strategy
|
||||
- Redis clustering strategy for production
|
||||
- Backup and disaster recovery
|
||||
- Monitoring, alerting, and observability stack
|
||||
- Rate limiting strategy (mentioned in security checklist but not in the spec)
|
||||
|
||||
**Recommendation:** Add an "Operations" or "Infrastructure" section. Even if production deployment is future work, the development Docker Compose topology should be defined, and the backend should be designed to support horizontal scaling from day one (stateless server, sticky-session considerations for WebSocket, etc.).
|
||||
|
||||
### I2. Authentication Token Lifecycle Is Incomplete
|
||||
|
||||
The spec mentions JWT and refresh tokens but does not address:
|
||||
- Token format and claims structure
|
||||
- Access token TTL vs. refresh token TTL
|
||||
- Refresh token rotation strategy
|
||||
- Token revocation mechanism (critical for logout and security incidents)
|
||||
- How tokens are transmitted (authorization header? cookies? both?)
|
||||
- Tauri-specific secure token storage on the client side
|
||||
- Concurrent session management (can a user be logged in on multiple devices?)
|
||||
- Token handling across tenant boundaries (if a user belongs to multiple tenants)
|
||||
|
||||
**Recommendation:** Add a token lifecycle section covering issuance, rotation, revocation, storage, and multi-tenant user scenarios.
|
||||
|
||||
### I3. No Concurrency and Transaction Strategy
|
||||
|
||||
An ERP handles concurrent edits to shared data. The spec does not address:
|
||||
- Optimistic vs. pessimistic locking strategy for business entities
|
||||
- How SeaORM transactions are managed across module boundaries
|
||||
- Idempotency for API operations (especially workflow actions)
|
||||
- Concurrent workflow task handling (two people approving the same task simultaneously)
|
||||
|
||||
**Recommendation:** Add a concurrency section. For ERP, optimistic locking with version columns is the typical baseline. Define how cross-module transactions are handled (saga pattern? eventual consistency?).
|
||||
|
||||
### I4. Audit Logging Is Mentioned But Not Specified
|
||||
|
||||
The core shared layer includes "audit" but there is no specification for:
|
||||
- What events are audited (all mutations? login/logout? data access?)
|
||||
- Audit log storage (same database? separate? append-only?)
|
||||
- Audit log retention policy
|
||||
- Audit log query API
|
||||
- How audit logs relate to multi-tenancy
|
||||
|
||||
For a commercial ERP, audit logging is not optional. It is a compliance requirement.
|
||||
|
||||
**Recommendation:** Add an audit specification section. Define the audit event schema, what triggers audit events, storage, and retention.
|
||||
|
||||
### I5. No Module Registration and Plugin Interface Defined
|
||||
|
||||
One of the four design principles is "Plugin extensibility: Industry modules register through standard interfaces, support dynamic enable/disable." But the spec provides:
|
||||
- No trait definitions for module registration
|
||||
- No lifecycle hooks (init, start, stop, health-check)
|
||||
- No mechanism for modules to register their routes, menu items, or event handlers
|
||||
- No tenant-level module enable/disable data model
|
||||
|
||||
This is the mechanism that makes the "platform base + industry plugins" architecture work. Without it, the industry modules cannot be built.
|
||||
|
||||
**Recommendation:** Add a "Module System" section that defines the module trait interface, the registration mechanism, and the per-tenant module configuration model.
|
||||
|
||||
### I6. Numbering Rules Are Too Simplistic
|
||||
|
||||
The spec lists "Numbering rules: Document number generation (e.g., PO-2024-001)" as a Config capability but does not address:
|
||||
- Concurrency-safe sequence generation (multiple users creating POs simultaneously)
|
||||
- Sequence reset rules (yearly? monthly? daily? never?)
|
||||
- Sequence gaps (are gaps allowed? this has legal implications in some jurisdictions)
|
||||
- Multi-tenant sequence isolation
|
||||
- How different document types get different sequences
|
||||
|
||||
**Recommendation:** Expand the numbering rules section with the sequence generation strategy. PostgreSQL sequences or a dedicated sequence table with row-level locking are the typical approaches.
|
||||
|
||||
### I7. Desktop-Backend Communication Protocol Undefined
|
||||
|
||||
The spec shows REST APIs and a WebSocket endpoint but does not specify:
|
||||
- How the Tauri app authenticates WebSocket connections
|
||||
- WebSocket message format and protocol
|
||||
- Reconnection and offline queueing strategy
|
||||
- How Tauri IPC interacts with HTTP calls (does the frontend call the backend directly, or go through Tauri commands?)
|
||||
- File upload/download handling (common in ERP: attachments, exports)
|
||||
- How the desktop app handles backend version mismatches
|
||||
|
||||
**Recommendation:** Add a "Client-Server Communication" section covering HTTP API patterns, WebSocket protocol, Tauri IPC strategy, and offline behavior.
|
||||
|
||||
---
|
||||
|
||||
## SUGGESTIONS (Nice to Have)
|
||||
|
||||
### S1. Consider Adding a `erp-infra` Crate
|
||||
|
||||
The current structure has `erp-core` for shared types/traits and `erp-common` for utilities. Consider a third crate, `erp-infra`, for infrastructure adapters (database connection pool, Redis client, event bus implementation). This keeps `erp-core` purely abstract and makes testing easier since modules depend on `erp-core` traits, not `erp-infra` implementations.
|
||||
|
||||
### S2. Define the Configuration File Format
|
||||
|
||||
`config-rs` is listed but no application configuration schema is defined. What does `config.toml` or `config.yaml` look like? Database URL, Redis URL, JWT secret, server port, log level, etc. should be enumerated.
|
||||
|
||||
### S3. Consider API Response Envelope Standardization
|
||||
|
||||
Define a standard API response format (as mentioned in the project's own patterns rules). Example:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... },
|
||||
"error": null,
|
||||
"meta": { "total": 100, "page": 1, "limit": 20 }
|
||||
}
|
||||
```
|
||||
|
||||
### S4. Add Database Index Strategy
|
||||
|
||||
While the spec mentions "Indexes on `tenant_id` + business keys," a more detailed index strategy would help. Consider composite indexes for common query patterns, partial indexes for soft-deleted rows, and index-only scans for high-frequency queries.
|
||||
|
||||
### S5. Consider Health Check and Readiness Endpoints
|
||||
|
||||
For containerized deployment and monitoring, define `/health` and `/ready` endpoints. These are essential for Docker orchestration and load balancer configuration.
|
||||
|
||||
### S6. The `apps/admin` Directory Implies a Web Admin Panel
|
||||
|
||||
The spec mentions it as "Optional web admin panel" but does not define its relationship to the Tauri desktop client. If both exist, is it a separate SPA build? Does it share the `packages/ui-components` library? Clarify whether this is in scope or should be removed to avoid confusion.
|
||||
|
||||
### S7. Timeline Estimates May Be Tight
|
||||
|
||||
The total roadmap is 10-15 weeks. For a solo developer or small team building a BPMN workflow engine from scratch in Rust, Phase 4 (3-4 weeks) is extremely aggressive. Consider:
|
||||
- Phase 4 should be 5-8 weeks for a usable workflow engine
|
||||
- Phase 1 should include CI/CD pipeline as a deliverable, which can take 3-5 days alone
|
||||
- Total realistic estimate: 14-22 weeks for a solid base
|
||||
|
||||
---
|
||||
|
||||
## Consistency Check
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| Module API prefixes consistent | PASS - all use `/api/v1/{module}/` |
|
||||
| Data model matches API surface | PARTIAL - no API for Organization CRUD, Policy CRUD, or Position CRUD in Auth |
|
||||
| Crate structure matches module list | PASS |
|
||||
| Tech stack choices are compatible | PASS - Axum + Tokio + SeaORM + redis-rs are a well-tested stack |
|
||||
| Multi-tenant strategy is applied consistently | PARTIAL - some APIs use `/:tenant_id` in path (Config menus) while Auth relies on middleware injection. Should be consistent. |
|
||||
| UI features match backend capabilities | PASS |
|
||||
| Roadmap phases align with module dependencies | PASS - Auth first, then Config, then Workflow (which depends on both) |
|
||||
|
||||
---
|
||||
|
||||
## Security Gap Analysis
|
||||
|
||||
| Area | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| Password hashing (Argon2) | Covered | Good |
|
||||
| JWT authentication | Partially covered | Missing token revocation, rotation |
|
||||
| SQL injection | Implicitly covered | SeaORM parameterized queries |
|
||||
| XSS | Not addressed | Should specify CSP policy for Tauri |
|
||||
| CSRF | Not applicable | Tauri desktop, not browser-based |
|
||||
| Rate limiting | Not specified | Mentioned in verification plan but not in design |
|
||||
| Input validation | Not specified | No validation strategy defined |
|
||||
| CORS | Not specified | Needed if web admin panel is built |
|
||||
| Secret management | Not specified | Where do JWT secrets, DB credentials, Redis passwords live? |
|
||||
| Audit trail | Mentioned but not specified | See I4 above |
|
||||
| Data encryption at rest | Not addressed | Relevant for private deployment customers |
|
||||
| Backup/restore security | Not addressed | |
|
||||
| Tenant data isolation verification | Covered | In verification plan |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The specification provides a strong architectural vision and reasonable technology choices. However, it has five critical gaps that would block or severely impair implementation:
|
||||
|
||||
1. **C1** - No error handling strategy
|
||||
2. **C2** - No event bus specification (despite being a core design principle)
|
||||
3. **C3** - No API versioning and contract governance
|
||||
4. **C4** - No multi-tenant migration strategy
|
||||
5. **C5** - Workflow engine scope is unrealistic for the stated timeline
|
||||
|
||||
And seven important issues that should be addressed before coding begins:
|
||||
|
||||
1. **I1** - No deployment/operations architecture
|
||||
2. **I2** - Incomplete authentication token lifecycle
|
||||
3. **I3** - No concurrency and transaction strategy
|
||||
4. **I4** - Audit logging mentioned but not specified
|
||||
5. **I5** - No module registration/plugin interface defined
|
||||
6. **I6** - Numbering rules too simplistic for ERP use
|
||||
7. **I7** - Desktop-backend communication protocol undefined
|
||||
|
||||
**Recommendation:** Address all five CRITICAL issues and the IMPORTANT issues before beginning Phase 1. The CRITICAL issues represent architectural foundations that are extremely expensive to retrofit. The IMPORTANT issues represent functionality that will be needed within the first three phases of development.
|
||||
|
||||
The spec should move from `Status: Draft` to `Status: Under Review` until these issues are resolved.
|
||||
@@ -1,369 +0,0 @@
|
||||
# Phase 4: 工作流引擎模块 — 实施计划
|
||||
|
||||
## Context
|
||||
|
||||
Phase 1(基础设施)、Phase 2(身份与权限)和 Phase 3(系统配置)已完成。Phase 4 需要实现工作流引擎模块(erp-workflow),提供流程定义、流程实例管理、任务审批、Token 驱动的执行引擎和可视化流程设计器。当前 `erp-workflow` 仅为 placeholder。
|
||||
|
||||
用户选择"完整实施"方案,包括 BPMN 子集解析器、Token 驱动执行引擎、完整 CRUD 端点和 React Flow 可视化设计器。
|
||||
|
||||
---
|
||||
|
||||
## Task 1: erp-workflow 骨架 + WorkflowState + Error
|
||||
|
||||
**目标:** 创建可编译的最小 crate,注册到 erp-server。
|
||||
|
||||
**创建/修改文件:**
|
||||
- 修改: `crates/erp-workflow/Cargo.toml` — 添加完整依赖
|
||||
- 修改: `crates/erp-workflow/src/lib.rs` — 模块声明 + re-export
|
||||
- 创建: `crates/erp-workflow/src/workflow_state.rs` — `WorkflowState { db, event_bus }`
|
||||
- 创建: `crates/erp-workflow/src/error.rs` — WorkflowError 枚举
|
||||
- 创建: `crates/erp-workflow/src/module.rs` — WorkflowModule + ErpModule trait
|
||||
- 创建: `crates/erp-workflow/src/dto.rs` — 占位
|
||||
- 创建: `crates/erp-workflow/src/entity/mod.rs` — 占位
|
||||
- 创建: `crates/erp-workflow/src/service/mod.rs` — 占位
|
||||
- 创建: `crates/erp-workflow/src/handler/mod.rs` — 占位
|
||||
- 创建: `crates/erp-workflow/src/engine/mod.rs` — 占位
|
||||
- 修改: `crates/erp-server/Cargo.toml` — 确认 erp-workflow 依赖
|
||||
- 修改: `crates/erp-server/src/state.rs` — 添加 `FromRef<AppState>` for WorkflowState
|
||||
- 修改: `crates/erp-server/src/main.rs` — 注册 WorkflowModule
|
||||
|
||||
**依赖:**
|
||||
```toml
|
||||
erp-core.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v7", "serde"] }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
axum = { workspace = true }
|
||||
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls", "with-uuid", "with-chrono", "with-json"] }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
utoipa = { workspace = true, features = ["uuid", "chrono"] }
|
||||
async-trait = { workspace = true }
|
||||
```
|
||||
|
||||
**验证:** `cargo check` 通过
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 数据库迁移(5 张表)
|
||||
|
||||
**创建文件(`crates/erp-server/migration/src/`):**
|
||||
- `m20260412_000018_create_process_definitions.rs`
|
||||
- `m20260412_000019_create_process_instances.rs`
|
||||
- `m20260412_000020_create_tokens.rs`
|
||||
- `m20260412_000021_create_tasks.rs`
|
||||
- `m20260412_000022_create_process_variables.rs`
|
||||
- 修改: `lib.rs` — 注册新迁移
|
||||
|
||||
### process_definitions
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | uuid PK | UUIDv7 |
|
||||
| tenant_id | uuid NOT NULL | 租户 ID |
|
||||
| name | string NOT NULL | 流程名称 |
|
||||
| key | string NOT NULL | 流程唯一编码 |
|
||||
| version | int NOT NULL DEFAULT 1 | 版本号 |
|
||||
| category | string NULL | 分类(如 leave, expense) |
|
||||
| description | text NULL | 描述 |
|
||||
| nodes | jsonb NOT NULL DEFAULT '[]' | 节点定义(BPMN 子集) |
|
||||
| edges | jsonb NOT NULL DEFAULT '[]' | 连线定义 |
|
||||
| status | string NOT NULL DEFAULT 'draft' | draft/published/deprecated |
|
||||
| + 标准审计字段 | | |
|
||||
| 唯一索引: | `(tenant_id, key, version) WHERE deleted_at IS NULL` | |
|
||||
|
||||
### process_instances
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | uuid PK | UUIDv7 |
|
||||
| tenant_id | uuid NOT NULL | |
|
||||
| definition_id | uuid NOT NULL FK → process_definitions | |
|
||||
| business_key | string NULL | 业务关联键(如请假单 ID) |
|
||||
| status | string NOT NULL DEFAULT 'running' | running/suspended/completed/terminated |
|
||||
| started_by | uuid NOT NULL | 发起人 user_id |
|
||||
| started_at | timestamptz NOT NULL DEFAULT NOW() | |
|
||||
| completed_at | timestamptz NULL | |
|
||||
| + 标准审计字段 | | |
|
||||
| 索引: | `idx_instances_status (tenant_id, status)` | |
|
||||
|
||||
### tokens
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | uuid PK | UUIDv7 |
|
||||
| tenant_id | uuid NOT NULL | |
|
||||
| instance_id | uuid NOT NULL FK → process_instances | |
|
||||
| node_id | string NOT NULL | 当前所在节点 ID |
|
||||
| status | string NOT NULL DEFAULT 'active' | active/consumed/terminated |
|
||||
| created_at | timestamptz NOT NULL DEFAULT NOW() | |
|
||||
| consumed_at | timestamptz NULL | |
|
||||
| 索引: | `idx_tokens_instance (instance_id)` | |
|
||||
|
||||
### tasks
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | uuid PK | UUIDv7 |
|
||||
| tenant_id | uuid NOT NULL | |
|
||||
| instance_id | uuid NOT NULL FK → process_instances | |
|
||||
| token_id | uuid NOT NULL FK → tokens | |
|
||||
| node_id | string NOT NULL | 对应的流程节点 |
|
||||
| node_name | string NULL | 节点名称(冗余,便于查询) |
|
||||
| assignee_id | uuid NULL | 指定处理人 |
|
||||
| candidate_groups | jsonb NULL | 候选角色组 |
|
||||
| status | string NOT NULL DEFAULT 'pending' | pending/approved/rejected/delegated |
|
||||
| outcome | string NULL | 审批结果 |
|
||||
| form_data | jsonb NULL | 表单数据 |
|
||||
| due_date | timestamptz NULL | 到期时间 |
|
||||
| completed_at | timestamptz NULL | |
|
||||
| + 标准审计字段 | | |
|
||||
| 索引: | `idx_tasks_assignee (tenant_id, assignee_id, status)` | |
|
||||
| 索引: | `idx_tasks_instance (instance_id)` | |
|
||||
|
||||
### process_variables
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | uuid PK | UUIDv7 |
|
||||
| tenant_id | uuid NOT NULL | |
|
||||
| instance_id | uuid NOT NULL FK → process_instances | |
|
||||
| name | string NOT NULL | 变量名 |
|
||||
| var_type | string NOT NULL DEFAULT 'string' | string/number/boolean/date/json |
|
||||
| value_string | text NULL | |
|
||||
| value_number | double precision NULL | |
|
||||
| value_boolean | boolean NULL | |
|
||||
| value_date | timestamptz NULL | |
|
||||
| 唯一索引: | `(instance_id, name)` | |
|
||||
|
||||
**验证:** `cargo run -p erp-server` 启动后 `\dt` 可见新表
|
||||
|
||||
---
|
||||
|
||||
## Task 3: SeaORM Entity
|
||||
|
||||
**创建文件(`crates/erp-workflow/src/entity/`):**
|
||||
- `mod.rs` — 导出所有实体
|
||||
- `process_definition.rs`
|
||||
- `process_instance.rs`
|
||||
- `token.rs`
|
||||
- `task.rs`
|
||||
- `process_variable.rs`
|
||||
|
||||
**模式:** 参考 `erp-config/src/entity/numbering_rule.rs`,包含 Relation 和 Related。
|
||||
|
||||
**验证:** `cargo check` 通过
|
||||
|
||||
---
|
||||
|
||||
## Task 4: DTO 定义
|
||||
|
||||
**修改文件:** `crates/erp-workflow/src/dto.rs`
|
||||
|
||||
**包含:**
|
||||
- 流程定义:`ProcessDefinitionResp`, `CreateProcessDefinitionReq`, `UpdateProcessDefinitionReq`, `PublishDefinitionReq`
|
||||
- 流程实例:`ProcessInstanceResp`, `StartInstanceReq`
|
||||
- 任务:`TaskResp`, `CompleteTaskReq`(含 outcome + form_data)
|
||||
- 流程变量:`ProcessVariableResp`, `SetVariableReq`
|
||||
- 流程图:`NodeDef`(BPMN 节点), `EdgeDef`(连线), `FlowDiagram`(完整图)
|
||||
|
||||
**节点类型:** StartEvent, EndEvent, UserTask, ServiceTask, ExclusiveGateway, ParallelGateway
|
||||
**连线条件:** `condition` 字段为可选表达式字符串(如 `amount > 1000`)
|
||||
|
||||
**验证:** `cargo check` 通过
|
||||
|
||||
---
|
||||
|
||||
## Task 5: BPMN 解析器 + 表达式引擎
|
||||
|
||||
**创建文件(`crates/erp-workflow/src/engine/`):**
|
||||
- `model.rs` — 流程图内存模型(FlowDiagram, FlowNode, FlowEdge, NodeType 枚举)
|
||||
- `parser.rs` — 解析 JSON nodes/edges 为内存模型,验证流程图合法性
|
||||
- `expression.rs` — 简单表达式求值器(支持比较运算和流程变量引用)
|
||||
|
||||
**关键逻辑:**
|
||||
- `FlowDiagram::validate()` — 检查:恰好 1 个 StartEvent,至少 1 个 EndEvent,无悬空连线,网关分支/汇合配对
|
||||
- `ExpressionEvaluator::eval(expr, variables) -> bool` — 支持 `var > 1000`, `status == "approved"`, `amount <= budget` 格式
|
||||
- 解析器将 `nodes` 和 `edges` jsonb 反序列化为 `Vec<FlowNode>` 和 `Vec<FlowEdge>`
|
||||
|
||||
**验证:** 单元测试覆盖:合法流程验证、缺少 StartEvent 报错、表达式求值
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Token 驱动执行引擎
|
||||
|
||||
**创建文件:** `crates/erp-workflow/src/engine/executor.rs`
|
||||
|
||||
**核心逻辑:**
|
||||
```
|
||||
start(instance_id, definition) → 在 StartEvent 创建 token
|
||||
advance(token_id, instance_id, definition, variables) → 消费当前 token,在下一节点创建新 token
|
||||
- 到达 EndEvent → 消费 token,检查实例是否所有 token 都完成 → 完成实例
|
||||
- 到达 UserTask → 创建 token + 创建 task 记录
|
||||
- 到达 ServiceTask → 创建 token + 执行动作(占位,发布事件)
|
||||
- 到达 ExclusiveGateway → 求值条件,选择一条分支
|
||||
- 到达 ParallelGateway(分支)→ 为每条出边创建 token
|
||||
- 到达 ParallelGateway(汇合)→ 消费当前 token,等待所有入边 token 到达后创建新 token
|
||||
```
|
||||
|
||||
**并发安全:** 使用 `pg_advisory_xact_lock` 保护 token 操作(参考 NumberingService 模式)
|
||||
|
||||
**验证:** 单元测试覆盖:直线流程、排他网关分支、并行网关分支与汇合
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Service 层
|
||||
|
||||
**创建文件(`crates/erp-workflow/src/service/`):**
|
||||
- `definition_service.rs` — 流程定义 CRUD + 发布 + 版本管理
|
||||
- `instance_service.rs` — 启动实例 + 查询 + 挂起/恢复/终止
|
||||
- `task_service.rs` — 查询待办 + 完成任务 + 委派 + 查询已办
|
||||
|
||||
**关键逻辑:**
|
||||
- `DefinitionService::publish` — draft → published,验证流程图合法性
|
||||
- `InstanceService::start` — 创建实例 + 初始化变量 + 调用 executor.start
|
||||
- `TaskService::complete` — 更新 task 状态 + 调用 executor.advance + 处理下一节点
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Handler 层
|
||||
|
||||
**创建文件(`crates/erp-workflow/src/handler/`):**
|
||||
- `definition_handler.rs` — 5 个端点
|
||||
- `instance_handler.rs` — 4 个端点
|
||||
- `task_handler.rs` — 4 个端点
|
||||
|
||||
**端点映射:**
|
||||
```
|
||||
GET/POST /workflow/definitions
|
||||
GET /workflow/definitions/{id}
|
||||
PUT /workflow/definitions/{id}
|
||||
POST /workflow/definitions/{id}/publish
|
||||
POST /workflow/instances
|
||||
GET /workflow/instances
|
||||
GET /workflow/instances/{id}
|
||||
POST /workflow/instances/{id}/suspend
|
||||
POST /workflow/instances/{id}/terminate
|
||||
GET /workflow/tasks/pending — 我的待办
|
||||
GET /workflow/tasks/completed — 我的已办
|
||||
POST /workflow/tasks/{id}/complete — 完成任务
|
||||
POST /workflow/tasks/{id}/delegate — 委派任务
|
||||
```
|
||||
|
||||
**RBAC:** 所有端点使用 `require_permission(&ctx, "workflow:xxx")`
|
||||
|
||||
---
|
||||
|
||||
## Task 9: 模块注册 + 种子数据
|
||||
|
||||
**修改文件:**
|
||||
- `crates/erp-workflow/src/module.rs` — 填充真实路由(12 个端点)
|
||||
- `crates/erp-auth/src/service/seed.rs` — 添加工作流权限
|
||||
- `crates/erp-server/src/main.rs` — 注册 WorkflowModule,合并路由
|
||||
|
||||
**新增种子权限(8 个):**
|
||||
- workflow:create, workflow:list, workflow:read, workflow:update
|
||||
- workflow:publish, workflow:start, workflow:approve, workflow:delegate
|
||||
|
||||
**验证:** `cargo check` + `cargo test` 通过
|
||||
|
||||
---
|
||||
|
||||
## Task 10: 前端 API 层 + 工作流页面
|
||||
|
||||
**创建文件(`apps/web/src/`):**
|
||||
- `api/workflowDefinitions.ts` — 流程定义 API
|
||||
- `api/workflowInstances.ts` — 流程实例 API
|
||||
- `api/workflowTasks.ts` — 任务 API
|
||||
- `pages/Workflow.tsx` — Tab 壳页面(流程定义 | 我的待办 | 我的已办 | 流程监控)
|
||||
|
||||
**修改文件:**
|
||||
- `App.tsx` — 添加 workflow 路由
|
||||
- `MainLayout.tsx` — 侧边栏添加工作流菜单
|
||||
|
||||
---
|
||||
|
||||
## Task 11: React Flow 可视化设计器
|
||||
|
||||
**创建文件(`apps/web/src/pages/workflow/`):**
|
||||
- `ProcessDesigner.tsx` — React Flow 画布 + 节点面板 + 属性面板
|
||||
- `nodes/` — 自定义节点组件(StartEvent, EndEvent, UserTask, ServiceTask, Gateway)
|
||||
- `edges/` — 条件标签连线组件
|
||||
- `hooks/useFlowValidation.ts` — 流程图前端验证
|
||||
|
||||
**依赖:** `@xyflow/react` npm 包
|
||||
|
||||
**功能:**
|
||||
- 拖拽添加节点到画布
|
||||
- 连线编辑(含条件表达式)
|
||||
- 节点属性编辑面板
|
||||
- 导出为 JSON nodes/edges 格式(匹配后端 DTO)
|
||||
- 流程图合法性前端验证
|
||||
|
||||
---
|
||||
|
||||
## Task 12: 流程图查看器 + 超时框架
|
||||
|
||||
**创建文件(`apps/web/src/pages/workflow/`):**
|
||||
- `ProcessViewer.tsx` — 只读 React Flow 渲染,高亮当前活跃节点
|
||||
- `InstanceDetail.tsx` — 实例详情页(流程图 + 变量 + 任务历史)
|
||||
|
||||
**超时框架(后端占位):**
|
||||
- `crates/erp-workflow/src/engine/timeout.rs` — 超时检查接口
|
||||
- Task 表 `due_date` 字段已支持
|
||||
|
||||
**验证:** `pnpm dev` 启动,工作流设计器可拖拽节点、连线、保存
|
||||
|
||||
---
|
||||
|
||||
## 依赖图
|
||||
|
||||
```
|
||||
Task 1(骨架)
|
||||
|
|
||||
Task 2(迁移)→ Task 3(Entity)→ Task 4(DTO)
|
||||
|
|
||||
+---------------+---------------+
|
||||
| |
|
||||
Task 5(BPMN 解析器) Task 6(执行引擎)
|
||||
| |
|
||||
+---------------+---------------+
|
||||
|
|
||||
Task 7(Service)
|
||||
|
|
||||
Task 8(Handler)
|
||||
|
|
||||
Task 9(集成+种子)
|
||||
|
|
||||
+---------------+---------------+
|
||||
| |
|
||||
Task 10(前端页面) Task 11(可视化设计器)
|
||||
| |
|
||||
+---------------+---------------+
|
||||
|
|
||||
Task 12(查看器+超时)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] `cargo check` 全 workspace 通过
|
||||
- [ ] `cargo test --workspace` 全部通过
|
||||
- [ ] Docker 环境正常启动
|
||||
- [ ] 所有迁移可正/反向执行
|
||||
- [ ] 12 个工作流 API 端点可测试
|
||||
- [ ] 前端工作流设计器可拖拽节点和连线
|
||||
- [ ] 流程图保存和加载正常
|
||||
- [ ] 所有代码已提交
|
||||
|
||||
## 关键参考文件
|
||||
|
||||
| 用途 | 文件路径 |
|
||||
|------|----------|
|
||||
| Service 模式 | `crates/erp-config/src/service/numbering_service.rs` |
|
||||
| Handler 模式 | `crates/erp-config/src/handler/numbering_handler.rs` |
|
||||
| State 桥接 | `crates/erp-server/src/state.rs` |
|
||||
| 模块注册 | `crates/erp-config/src/module.rs` |
|
||||
| 迁移模式 | `crates/erp-server/migration/src/m20260412_000016_create_settings.rs` |
|
||||
| Advisory Lock | `crates/erp-config/src/service/numbering_service.rs` (generate_number) |
|
||||
| 前端 Table CRUD | `apps/web/src/pages/Roles.tsx` |
|
||||
| 前端树形展示 | `apps/web/src/pages/Organizations.tsx` |
|
||||
| RBAC | `crates/erp-core/src/rbac.rs` |
|
||||
@@ -1,357 +0,0 @@
|
||||
# WASM 插件系统设计 — 可行性分析与原型验证计划
|
||||
|
||||
> 基于 `docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md` 的全面审查
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体评估
|
||||
|
||||
**结论:技术可行,但需要对设计做重要调整。**
|
||||
|
||||
设计方案的核心思路(WASM 沙箱 + 宿主代理 API + 配置驱动 UI)是正确的架构方向。Wasmtime v43.x + Component Model + WASI 0.2/0.3 已达到生产就绪状态。但设计中有 **7 个关键问题** 和 **5 个改进点** 需要在实施前解决。
|
||||
|
||||
**可行性评分:**
|
||||
|
||||
| 维度 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| 技术可行性 | 8/10 | Wasmtime 成熟,核心 API 可用;动态表和事务支持需额外封装 |
|
||||
| 架构兼容性 | 6/10 | 与现有 FromRef 状态模式、静态路由模式存在结构性冲突 |
|
||||
| 安全性 | 9/10 | WASM 沙箱 + 权限模型 + 租户隔离设计扎实 |
|
||||
| 前端可行性 | 7/10 | PluginCRUDPage 概念正确但实现细节不足 |
|
||||
| 实施复杂度 | 高 | 估计 3 个 Phase、6-8 周工作量 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 与现有代码的兼容性分析
|
||||
|
||||
### 2.1 现有架构快照
|
||||
|
||||
| 组件 | 现状 | 设计目标 | 差距 |
|
||||
|------|------|----------|------|
|
||||
| ErpModule trait | 7 个方法(name/version/dependencies/register_event_handlers/on_tenant_created/on_tenant_deleted/as_any) | 15+ 方法(新增 id/module_type/on_startup/on_shutdown/health_check/public_routes/protected_routes/migrations/config_schema) | **重大差距** |
|
||||
| ModuleRegistry | `Arc<Vec<Arc<dyn ErpModule>>>`,Builder 模式注册,无路由收集 | 需要 build_routes()、topological_sort()、load_wasm_plugins()、health_check_all() | **重大差距** |
|
||||
| EventBus | `tokio::broadcast`,仅有 subscribe()(全量订阅) | 需要 subscribe_filtered() + unsubscribe() | **中等差距** |
|
||||
| 路由 | main.rs 手动 merge 静态方法 | registry.build_routes() 自动收集 | **中等差距** |
|
||||
| 状态注入 | FromRef 模式(编译时桥接 AppState → 各模块 State) | WASM 插件需要运行时状态注入 | **结构性冲突** |
|
||||
| 前端菜单 | MainLayout.tsx 硬编码 3 组菜单 | 从 PluginStore 动态生成 | **中等差距** |
|
||||
| 前端路由 | App.tsx 静态定义,React.lazy 懒加载 | DynamicRouter 根据插件配置动态生成 | **中等差距** |
|
||||
|
||||
### 2.2 关键差距详解
|
||||
|
||||
**差距 1:ErpModule trait 路由方法缺失**
|
||||
|
||||
当前路由不是 trait 的一部分,而是每个模块的关联函数:
|
||||
|
||||
```rust
|
||||
// 当前(静态关联函数,非 trait 方法)
|
||||
impl AuthModule {
|
||||
pub fn public_routes<S: Clone + Send + Sync + 'static>() -> Router<S> { ... }
|
||||
pub fn protected_routes<S: Clone + Send + Sync + 'static>() -> Router<S> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
设计将路由提升为 trait 方法,但这引入了泛型参数问题:`Router<S>` 中的 `S` 是 `AppState` 类型,而 trait 不能有泛型方法(会导致 trait 不是 object-safe)。需要设计新的路由注入机制。
|
||||
|
||||
**差距 2:FromRef 状态模式与 WASM 插件的冲突**
|
||||
|
||||
当前每个模块有自己的 State 类型(`AuthState`、`ConfigState` 等),通过 `FromRef` 从 `AppState` 桥接。WASM 插件无法定义编译时的 `FromRef` 实现,需要运行时状态传递机制。
|
||||
|
||||
**差距 3:EventBus 缺少类型化订阅**
|
||||
|
||||
`subscribe()` 返回 `broadcast::Receiver<DomainEvent>`,订阅者需要自行过滤。这会导致每个插件都收到所有事件,增加不必要的开销。设计中的 `subscribe_filtered()` 是必要的扩展。
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键问题(Critical Issues)
|
||||
|
||||
### C1: 路由注入机制设计不完整
|
||||
|
||||
**问题:** 设计中的 `fn public_routes(&self) -> Option<Router>` 和 `fn protected_routes(&self) -> Option<Router>` 缺少泛型参数 `S`(AppState)。Axum 的 Router 依赖状态类型,而 trait object 不能携带泛型。
|
||||
|
||||
**影响:** 这是路由自动收集的基础。如果无法解决,整个 `registry.build_routes()` 的设计就不能实现。
|
||||
|
||||
**建议方案:**
|
||||
|
||||
```rust
|
||||
// 方案:使用 Router<()>, 由 ModuleRegistry 在 build_routes() 时添加 .with_state()
|
||||
|
||||
pub trait ErpModule: Send + Sync {
|
||||
fn protected_routes(&self) -> Option<Router<()>> { None }
|
||||
fn public_routes(&self) -> Option<Router<()>> { None }
|
||||
}
|
||||
|
||||
impl ModuleRegistry {
|
||||
pub fn build_routes(&self, state: AppState) -> (Router, Router) {
|
||||
let public = self.modules.iter()
|
||||
.filter_map(|m| m.public_routes())
|
||||
.fold(Router::new(), |acc, r| acc.merge(r))
|
||||
.with_state(state.clone());
|
||||
|
||||
let protected = self.modules.iter()
|
||||
.filter_map(|m| m.protected_routes())
|
||||
.fold(Router::new(), |acc, r| acc.merge(r))
|
||||
.with_state(state);
|
||||
|
||||
(public, protected)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### C2: 事务支持缺失
|
||||
|
||||
**问题:** Host API 每次 db 调用都是独立事务。但 ERP 业务经常需要多步骤原子操作。
|
||||
|
||||
**建议:** 增加声明式事务,插件提交一组操作,宿主在单事务中执行。
|
||||
|
||||
### C3: 插件间依赖未解决
|
||||
|
||||
**建议:** Phase 7 暂不实现,在 plugin.toml 中预留 `plugins = [...]` 字段。
|
||||
|
||||
### C4: 版本号严重过时
|
||||
|
||||
**建议:** 锁定 `wasmtime = "43"` + `wit-bindgen = "0.55"`。
|
||||
|
||||
### C5: 动态表 Schema 迁移策略缺失
|
||||
|
||||
**建议:** 添加 schema_version + 迁移 SQL 机制。
|
||||
|
||||
### C6: PluginCRUDPage 实现细节不足
|
||||
|
||||
**建议:** 扩展关联数据、主从表、文件上传支持。
|
||||
|
||||
### C7: 错误传播机制不完整
|
||||
|
||||
**建议:** 定义结构化插件错误协议。
|
||||
|
||||
---
|
||||
|
||||
## 4. 替代方案比较
|
||||
|
||||
**结论:WASM 方案优于 Lua 脚本和进程外 gRPC。** dylib 因安全性排除。
|
||||
|
||||
---
|
||||
|
||||
## 5. Wasmtime 原型验证计划
|
||||
|
||||
### 5.1 验证目标
|
||||
|
||||
验证 Wasmtime Component Model 与 ERP 插件系统核心需求的集成可行性:
|
||||
|
||||
| # | 验证项 | 关键问题 |
|
||||
|---|--------|---------|
|
||||
| V1 | WIT 接口定义 + bindgen! 宏 | C4: 版本兼容性 |
|
||||
| V2 | Host 调用插件导出函数 | init / handle_event 能否正常工作 |
|
||||
| V3 | 插件调用 Host 导入函数 | db_insert / log_write 能否正常回调 |
|
||||
| V4 | async 支持 | Host async 函数(数据库操作)能否正确桥接 |
|
||||
| V5 | Fuel + Epoch 资源限制 | 是否能限制插件 CPU 时间和内存 |
|
||||
| V6 | 从二进制动态加载 | 从数据库/文件加载 WASM 并实例化 |
|
||||
|
||||
### 5.2 原型项目结构
|
||||
|
||||
在 workspace 中创建独立的原型 crate(不影响现有代码):
|
||||
|
||||
```
|
||||
crates/
|
||||
erp-plugin-prototype/ ← 新增原型 crate
|
||||
Cargo.toml
|
||||
wit/
|
||||
plugin.wit ← WIT 接口定义
|
||||
src/
|
||||
lib.rs ← Host 端:运行时 + Host API 实现
|
||||
main.rs ← 测试入口:加载插件并调用
|
||||
tests/
|
||||
test_plugin_integration.rs ← 集成测试
|
||||
|
||||
erp-plugin-test-sample/ ← 新增测试插件 crate
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs ← 插件端:实现 Guest trait
|
||||
```
|
||||
|
||||
### 5.3 WIT 接口(验证用最小子集)
|
||||
|
||||
```wit
|
||||
package erp:plugin;
|
||||
|
||||
interface host {
|
||||
db-insert: func(entity: string, data: list<u8>) -> result<list<u8>, string>;
|
||||
log-write: func(level: string, message: string);
|
||||
}
|
||||
|
||||
interface plugin {
|
||||
init: func() -> result<_, string>;
|
||||
handle-event: func(event-type: string, payload: list<u8>) -> result<_, string>;
|
||||
}
|
||||
|
||||
world plugin-world {
|
||||
import host;
|
||||
export plugin;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Host 端实现(验证要点)
|
||||
|
||||
```rust
|
||||
// crates/erp-plugin-prototype/src/lib.rs
|
||||
|
||||
use wasmtime::component::*;
|
||||
use wasmtime::{Config, Engine, Store};
|
||||
use wasmtime::StoreLimitsBuilder;
|
||||
|
||||
// bindgen! 生成类型化绑定
|
||||
bindgen!({
|
||||
path: "./wit/plugin.wit",
|
||||
world: "plugin-world",
|
||||
async: true, // ← 验证 V4: async 支持
|
||||
imports: { default: async | trappable },
|
||||
exports: { default: async },
|
||||
});
|
||||
|
||||
struct HostState {
|
||||
fuel_consumed: u64,
|
||||
logs: Vec<(String, String)>,
|
||||
db_ops: Vec<(String, Vec<u8>)>,
|
||||
}
|
||||
|
||||
// 实现 bindgen 生成的 Host trait
|
||||
impl Host for HostState {
|
||||
async fn db_insert(&mut self, entity: String, data: Vec<u8>) -> Result<Vec<u8>, String> {
|
||||
// 模拟数据库操作
|
||||
self.db_ops.push((entity, data.clone()));
|
||||
Ok(br#"{"id":"test-uuid","tenant_id":"tenant-1"}"#.to_vec())
|
||||
}
|
||||
|
||||
async fn log_write(&mut self, level: String, message: String) {
|
||||
self.logs.push((level, message));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 测试插件
|
||||
|
||||
```rust
|
||||
// crates/erp-plugin-test-sample/src/lib.rs
|
||||
|
||||
wit_bindgen::generate!({
|
||||
path: "../../erp-plugin-prototype/wit/plugin.wit",
|
||||
world: "plugin-world",
|
||||
});
|
||||
|
||||
struct TestPlugin;
|
||||
|
||||
impl Guest for TestPlugin {
|
||||
fn init() -> Result<(), String> {
|
||||
host::log_write("info", "测试插件初始化成功");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(event_type: String, payload: Vec<u8>) -> Result<(), String> {
|
||||
// 调用 Host API 验证双向通信
|
||||
let result = host::db_insert("test_entity", br#"{"name":"test"}"#.to_vec())
|
||||
.map_err(|e| format!("db_insert 失败: {}", e))?;
|
||||
host::log_write("info", &format!("处理事件 {} 成功", event_type));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
export!(TestPlugin);
|
||||
```
|
||||
|
||||
### 5.6 验证测试用例
|
||||
|
||||
```rust
|
||||
// crates/erp-plugin-prototype/tests/test_plugin_integration.rs
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_plugin_lifecycle() {
|
||||
// V1: WIT 接口 + bindgen 编译通过(隐式验证)
|
||||
|
||||
// V6: 从文件加载 WASM 二进制
|
||||
let wasm_bytes = std::fs::read("../erp-plugin-test-sample/target/.../test_plugin.wasm")
|
||||
.expect("请先编译测试插件");
|
||||
let engine = setup_engine(); // V5: 启用 fuel + epoch
|
||||
let module = Module::from_binary(&engine, &wasm_bytes).unwrap();
|
||||
|
||||
let mut store = setup_store(&engine); // V5: 设置资源限制
|
||||
let instance = instantiate(&mut store, &module).await.unwrap();
|
||||
|
||||
// V2: Host 调用插件 init()
|
||||
instance.plugin().call_init(&mut store).await.unwrap();
|
||||
|
||||
// V3: Host 调用插件 handle_event(),插件回调 Host API
|
||||
instance.plugin().call_handle_event(
|
||||
&mut store,
|
||||
"test.event".to_string(),
|
||||
vec![],
|
||||
).await.unwrap();
|
||||
|
||||
// 验证 Host 端收到了插件的操作
|
||||
let state = store.data();
|
||||
assert!(state.logs.iter().any(|(l, m)| m.contains("测试插件初始化成功")));
|
||||
assert_eq!(state.db_ops.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fuel_limit() {
|
||||
// V5: 验证 fuel 耗尽时正确 trap
|
||||
let mut store = setup_store_with_fuel(100); // 极低 fuel
|
||||
let result = instance.plugin().call_init(&mut store).await;
|
||||
assert!(result.is_err()); // 应该因 fuel 耗尽而失败
|
||||
}
|
||||
```
|
||||
|
||||
### 5.7 验证步骤
|
||||
|
||||
```
|
||||
步骤 1: 添加 crate 和 Cargo.toml 依赖
|
||||
- crates/erp-plugin-prototype/Cargo.toml
|
||||
wasmtime = "43", wasmtime-wasi = "43", tokio, anyhow
|
||||
- crates/erp-plugin-test-sample/Cargo.toml
|
||||
wit-bindgen = "0.55", serde, serde_json, crate-type = ["cdylib"]
|
||||
|
||||
步骤 2: 编写 WIT 接口文件
|
||||
- crates/erp-plugin-prototype/wit/plugin.wit
|
||||
|
||||
步骤 3: 实现 Host 端(bindgen + Host trait)
|
||||
- crates/erp-plugin-prototype/src/lib.rs
|
||||
|
||||
步骤 4: 实现测试插件
|
||||
- crates/erp-plugin-test-sample/src/lib.rs
|
||||
|
||||
步骤 5: 编译测试插件为 WASM
|
||||
- cargo build -p erp-plugin-test-sample --target wasm32-unknown-unknown --release
|
||||
|
||||
步骤 6: 运行集成测试
|
||||
- cargo test -p erp-plugin-prototype
|
||||
|
||||
步骤 7: 验证资源限制
|
||||
- fuel 耗尽 trap
|
||||
- 内存限制
|
||||
- epoch 中断
|
||||
```
|
||||
|
||||
### 5.8 验证成功标准
|
||||
|
||||
| 标准 | 衡量方式 |
|
||||
|------|---------|
|
||||
| V1 编译通过 | Host 和插件 crate 均能 `cargo check` 通过 |
|
||||
| V2 Host→插件调用 | `init()` 返回 Ok,Host 端日志记录初始化成功 |
|
||||
| V3 插件→Host回调 | `handle_event()` 中调用 `host::db_insert()` 成功返回数据 |
|
||||
| V4 async 正确 | Host 的 async db_insert 在 tokio runtime 中正确执行 |
|
||||
| V5 资源限制 | 低 fuel 时 init() 返回错误而非无限循环 |
|
||||
| V6 动态加载 | 从 .wasm 文件加载并实例化成功 |
|
||||
| 编译大小 | 测试插件 WASM < 2MB |
|
||||
| 启动耗时 | 单个插件实例化 < 100ms |
|
||||
|
||||
### 5.9 关键文件清单
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `crates/erp-plugin-prototype/Cargo.toml` | 新建 - Host 端 crate 配置 |
|
||||
| `crates/erp-plugin-prototype/wit/plugin.wit` | 新建 - WIT 接口定义 |
|
||||
| `crates/erp-plugin-prototype/src/lib.rs` | 新建 - Host 运行时 + API 实现 |
|
||||
| `crates/erp-plugin-prototype/src/main.rs` | 新建 - 手动测试入口 |
|
||||
| `crates/erp-plugin-prototype/tests/test_plugin_integration.rs` | 新建 - 集成测试 |
|
||||
| `crates/erp-plugin-test-sample/Cargo.toml` | 新建 - 插件 crate 配置 |
|
||||
| `crates/erp-plugin-test-sample/src/lib.rs` | 新建 - 测试插件实现 |
|
||||
| `Cargo.toml` | 修改 - 添加两个新 workspace member |
|
||||
@@ -1,763 +0,0 @@
|
||||
# ERP 插件管理系统 — 完整实施计划
|
||||
|
||||
## Context
|
||||
|
||||
ERP 平台已完成 Phase 1-6(基础设施、身份权限、系统配置、工作流、消息中心、整合打磨),WASM 插件原型 V1-V6 已验证通过。现在需要将原型集成到生产系统,形成**完整的插件管理链路**:开发 → 打包 → 上传 → 安装 → 启用 → 运行 → 停用 → 卸载。
|
||||
|
||||
**当前差距**:原型使用 mock HostState,无真实 DB 操作;无插件管理 API;无数据库表;无前端管理界面;无动态表/路由。
|
||||
|
||||
---
|
||||
|
||||
## 阶段依赖图
|
||||
|
||||
```
|
||||
7A (基础设施升级) → 7B (插件运行时) → 7C (数据库表) → 7D (管理 API) → 7E (数据 CRUD API)
|
||||
8A (前端 API + Store) → 8B (管理页面) → 8C (动态路由 + CRUD 页面) → 8D (E2E 验证)
|
||||
|
||||
7D 完成 → 8B 可开始
|
||||
7E 完成 → 8C 可开始
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 7A: 基础设施升级
|
||||
|
||||
**目标**: 扩展 ErpModule trait、ModuleRegistry、EventBus,全部向后兼容,现有 4 个模块无需修改。
|
||||
|
||||
### 7A.1 EventBus 过滤订阅
|
||||
|
||||
**修改**: [events.rs](crates/erp-core/src/events.rs)
|
||||
|
||||
```rust
|
||||
// 新增方法
|
||||
pub fn subscribe_filtered(
|
||||
&self,
|
||||
event_type_prefix: String,
|
||||
) -> (FilteredEventReceiver, SubscriptionHandle)
|
||||
|
||||
// 新增类型
|
||||
pub struct FilteredEventReceiver { /* mpsc::Receiver */ }
|
||||
impl FilteredEventReceiver {
|
||||
pub async fn recv(&mut self) -> Option<DomainEvent> { ... }
|
||||
}
|
||||
|
||||
pub struct SubscriptionHandle { /* JoinHandle + sender for cancel */ }
|
||||
impl SubscriptionHandle {
|
||||
pub fn cancel(self) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
实现:为每次 `subscribe_filtered` 调用 spawn 一个 Tokio task,从 broadcast channel 读取,过滤匹配 `event_type_prefix` 的事件转发到 mpsc channel(capacity 256)。
|
||||
|
||||
### 7A.2 ErpModule Trait v2
|
||||
|
||||
**修改**: [module.rs](crates/erp-core/src/module.rs)
|
||||
|
||||
```rust
|
||||
// 新增枚举
|
||||
pub enum ModuleType { Builtin, Plugin }
|
||||
|
||||
// 新增上下文结构
|
||||
pub struct ModuleContext {
|
||||
pub db: sea_orm::DatabaseConnection,
|
||||
pub event_bus: EventBus,
|
||||
}
|
||||
|
||||
// trait 新增方法(全部有默认实现)
|
||||
fn id(&self) -> &str { self.name() }
|
||||
fn module_type(&self) -> ModuleType { ModuleType::Builtin }
|
||||
async fn on_startup(&self, _ctx: &ModuleContext) -> AppResult<()> { Ok(()) }
|
||||
async fn on_shutdown(&self) -> AppResult<()> { Ok(()) }
|
||||
async fn health_check(&self) -> AppResult<serde_json::Value> {
|
||||
Ok(serde_json::json!({"status": "healthy"}))
|
||||
}
|
||||
```
|
||||
|
||||
### 7A.3 ModuleRegistry v2
|
||||
|
||||
**修改**: [module.rs](crates/erp-core/src/module.rs)
|
||||
|
||||
```rust
|
||||
impl ModuleRegistry {
|
||||
// 新增方法
|
||||
pub fn sorted_modules(&self) -> Vec<Arc<dyn ErpModule>> // 拓扑排序
|
||||
pub async fn startup_all(&self, ctx: &ModuleContext) -> AppResult<()>
|
||||
pub async fn shutdown_all(&self) -> AppResult<()>
|
||||
pub async fn health_check_all(&self) -> Vec<(String, AppResult<serde_json::Value>)>
|
||||
pub fn get_module(&self, name: &str) -> Option<Arc<dyn ErpModule>>
|
||||
}
|
||||
```
|
||||
|
||||
拓扑排序: Kahn 算法,环检测返回 `AppError::Validation`。
|
||||
|
||||
### 7A.4 服务启动集成
|
||||
|
||||
**修改**: [main.rs](crates/erp-server/src/main.rs) — 在 `registry.register_handlers` 之后添加:
|
||||
|
||||
```rust
|
||||
let module_ctx = ModuleContext { db: db.clone(), event_bus: event_bus.clone() };
|
||||
registry.startup_all(&module_ctx).await?;
|
||||
```
|
||||
|
||||
**修改**: [lib.rs](crates/erp-core/src/lib.rs) — 导出新类型 `ModuleType`, `ModuleContext`
|
||||
|
||||
**7A 文件清单**:
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 修改 | `crates/erp-core/src/events.rs` |
|
||||
| 修改 | `crates/erp-core/src/module.rs` |
|
||||
| 修改 | `crates/erp-core/src/lib.rs` |
|
||||
| 修改 | `crates/erp-server/src/main.rs` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 7B: 插件运行时 Crate
|
||||
|
||||
**目标**: 创建 `erp-plugin` crate,实现生产级 Host API(真实 DB/EventBus 操作)。
|
||||
|
||||
### 7B.1 Crate 骨架
|
||||
|
||||
**新建**: `crates/erp-plugin/Cargo.toml`
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
wasmtime = "43"
|
||||
wasmtime-wasi = "43"
|
||||
erp-core.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sea-orm.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
dashmap = "6"
|
||||
toml = "0.8"
|
||||
```
|
||||
|
||||
**新建**: `crates/erp-plugin/wit/plugin.wit` — 从 prototype 复制
|
||||
|
||||
**新建**: `crates/erp-plugin/src/lib.rs` — 声明模块:`engine`, `host`, `manifest`, `state`, `error`, `dynamic_table`, `entity`, `dto`, `service`, `handler`, `data_service`, `data_dto`, `module`
|
||||
|
||||
**修改**: [Cargo.toml](Cargo.toml) — workspace members 添加 `"crates/erp-plugin"`,dependencies 添加 `erp-plugin = { path = "crates/erp-plugin" }`
|
||||
|
||||
### 7B.2 错误类型
|
||||
|
||||
**新建**: `crates/erp-plugin/src/error.rs`
|
||||
|
||||
```rust
|
||||
pub enum PluginError {
|
||||
NotFound(String),
|
||||
AlreadyExists(String),
|
||||
InvalidManifest(String),
|
||||
InvalidState { expected: String, actual: String },
|
||||
ExecutionError(String),
|
||||
InstantiationError(String),
|
||||
FuelExhausted(String),
|
||||
DependencyNotSatisfied(String),
|
||||
DatabaseError(String),
|
||||
PermissionDenied(String),
|
||||
}
|
||||
// From<PluginError> for AppError
|
||||
pub type PluginResult<T> = Result<T, PluginError>;
|
||||
```
|
||||
|
||||
### 7B.3 插件清单解析
|
||||
|
||||
**新建**: `crates/erp-plugin/src/manifest.rs`
|
||||
|
||||
```rust
|
||||
pub struct PluginManifest {
|
||||
pub metadata: PluginMetadata,
|
||||
pub schema: Option<PluginSchema>,
|
||||
pub events: Option<PluginEvents>,
|
||||
pub ui: Option<PluginUi>,
|
||||
pub permissions: Option<Vec<PluginPermission>>,
|
||||
}
|
||||
pub struct PluginMetadata { id, name, version, description, author, min_platform_version, dependencies }
|
||||
pub struct PluginSchema { pub entities: Vec<PluginEntity> }
|
||||
pub struct PluginEntity { name, display_name, fields: Vec<PluginField>, indexes }
|
||||
pub struct PluginField { name, field_type: PluginFieldType, required, unique, default, display_name, ui_widget, options }
|
||||
pub enum PluginFieldType { String, Integer, Float, Boolean, Date, DateTime, Json, Uuid, Decimal }
|
||||
pub struct PluginEvents { pub subscribe: Vec<String> }
|
||||
pub struct PluginUi { pub pages: Vec<PluginPage> }
|
||||
pub struct PluginPage { route, entity, display_name, icon, menu_group }
|
||||
pub struct PluginPermission { code, name, description }
|
||||
|
||||
pub fn parse_manifest(toml_str: &str) -> PluginResult<PluginManifest>
|
||||
```
|
||||
|
||||
### 7B.4 生产 Host 实现(延迟执行模式)
|
||||
|
||||
**新建**: `crates/erp-plugin/src/host.rs`
|
||||
|
||||
**关键设计**: WASM 调用是同步的,SeaORM 是异步的。采用**延迟执行模式**:
|
||||
- 读操作(db_query, config_get, current_user)→ 调用前预填充 HostState,Host 方法直接返回缓存数据
|
||||
- 写操作(db_insert, db_update, db_delete, event_publish)→ Host 方法将操作入队到 `self.pending_ops`,返回合成成功响应
|
||||
- WASM 调用结束后,engine 刷新 `pending_ops` 执行真实 DB 操作
|
||||
|
||||
```rust
|
||||
pub struct HostState {
|
||||
pub(crate) limits: StoreLimits,
|
||||
pub(crate) tenant_id: Uuid,
|
||||
pub(crate) user_id: Uuid,
|
||||
pub(crate) permissions: Vec<String>,
|
||||
pub(crate) plugin_id: String,
|
||||
// 预填充的读取缓存
|
||||
pub(crate) query_results: HashMap<String, Vec<u8>>,
|
||||
pub(crate) config_cache: HashMap<String, Vec<u8>>,
|
||||
pub(crate) current_user_json: Vec<u8>,
|
||||
// 待刷新的写操作
|
||||
pub(crate) pending_ops: Vec<PendingOp>,
|
||||
pub(crate) logs: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
pub enum PendingOp {
|
||||
Insert { entity: String, data: Vec<u8> },
|
||||
Update { entity: String, id: String, data: Vec<u8>, version: i64 },
|
||||
Delete { entity: String, id: String },
|
||||
PublishEvent { event_type: String, payload: Vec<u8> },
|
||||
}
|
||||
```
|
||||
|
||||
### 7B.5 插件引擎
|
||||
|
||||
**新建**: `crates/erp-plugin/src/engine.rs`
|
||||
|
||||
```rust
|
||||
pub struct PluginEngine {
|
||||
engine: wasmtime::Engine,
|
||||
db: DatabaseConnection,
|
||||
event_bus: EventBus,
|
||||
plugins: Arc<DashMap<String, LoadedPlugin>>,
|
||||
config: PluginEngineConfig,
|
||||
}
|
||||
|
||||
pub struct PluginEngineConfig {
|
||||
pub default_fuel: u64, // 10_000_000
|
||||
pub execution_timeout_secs: u64, // 30
|
||||
}
|
||||
|
||||
pub struct LoadedPlugin {
|
||||
pub id: String,
|
||||
pub manifest: PluginManifest,
|
||||
pub component: Component,
|
||||
pub linker: Linker<HostState>,
|
||||
pub status: PluginStatus,
|
||||
pub event_handle: Option<SubscriptionHandle>,
|
||||
}
|
||||
|
||||
pub enum PluginStatus { Loaded, Initialized, Running, Error(String), Disabled }
|
||||
|
||||
// 核心方法
|
||||
impl PluginEngine {
|
||||
pub fn new(db, event_bus, config) -> Result<Self>
|
||||
pub async fn load(&self, plugin_id, wasm_bytes, manifest) -> Result<()> // 加载到内存
|
||||
pub async fn initialize(&self, plugin_id) -> Result<()> // 调用 init()
|
||||
pub async fn start_event_listener(&self, plugin_id) -> Result<()> // 订阅事件
|
||||
pub async fn handle_event(&self, plugin_id, event_type, payload, tenant_id, user_id) -> Result<()>
|
||||
pub async fn on_tenant_created(&self, plugin_id, tenant_id) -> Result<()>
|
||||
pub async fn disable(&self, plugin_id) -> Result<()> // 停止+卸载
|
||||
pub async fn unload(&self, plugin_id) -> Result<()>
|
||||
pub async fn health_check(&self, plugin_id) -> Result<serde_json::Value>
|
||||
pub fn list_plugins(&self) -> Vec<PluginInfo>
|
||||
pub fn get_manifest(&self, plugin_id) -> Option<PluginManifest>
|
||||
|
||||
// 内部: spawn_blocking + catch_unwind + fuel 限制 + timeout
|
||||
async fn execute_wasm<F, R>(&self, plugin_id, operation: F) -> Result<R>
|
||||
// 内部: 刷新 pending_ops 到真实 DB
|
||||
async fn flush_ops(&self, state: &HostState) -> Result<()>
|
||||
}
|
||||
```
|
||||
|
||||
`execute_wasm` 流程:
|
||||
1. 从 DashMap 获取 LoadedPlugin
|
||||
2. 创建新 Store + HostState(预填充读数据)
|
||||
3. `tokio::task::spawn_blocking` 包装 WASM 调用
|
||||
4. 内部 `std::panic::catch_unwind(AssertUnwindSafe(...))`
|
||||
5. 返回后 `flush_ops` 执行真实 DB 操作
|
||||
6. 外层 `tokio::time::timeout` 限制执行时间
|
||||
|
||||
### 7B.6 动态表管理器
|
||||
|
||||
**新建**: `crates/erp-plugin/src/dynamic_table.rs`
|
||||
|
||||
```rust
|
||||
pub struct DynamicTableManager;
|
||||
|
||||
impl DynamicTableManager {
|
||||
pub async fn create_table(db, plugin_id, entity: &PluginEntity) -> Result<()>
|
||||
pub async fn drop_table(db, plugin_id, entity_name) -> Result<()>
|
||||
pub async fn table_exists(db, table_name) -> Result<bool>
|
||||
pub fn table_name(plugin_id, entity_name) -> String // "plugin_{sanitized_id}_{entity}"
|
||||
pub fn build_insert_sql(table_name, data) -> (String, Vec<Value>)
|
||||
pub fn build_query_sql(table_name, filter, pagination) -> (String, Vec<Value>)
|
||||
pub fn build_update_sql(table_name, id, data, version) -> (String, Vec<Value>)
|
||||
pub fn build_delete_sql(table_name, id) -> (String, Vec<Value>)
|
||||
}
|
||||
```
|
||||
|
||||
动态表结构: `plugin_{id}_{entity}` 列包括 id(UUID PK), tenant_id, data(JSONB), created_at, updated_at, created_by, updated_by, deleted_at, version
|
||||
|
||||
### 7B.7 插件状态
|
||||
|
||||
**新建**: `crates/erp-plugin/src/state.rs`
|
||||
|
||||
```rust
|
||||
#[derive(Clone)]
|
||||
pub struct PluginState {
|
||||
pub db: DatabaseConnection,
|
||||
pub event_bus: EventBus,
|
||||
pub engine: PluginEngine,
|
||||
}
|
||||
```
|
||||
|
||||
**7B 文件清单**:
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 新建 | `crates/erp-plugin/Cargo.toml` |
|
||||
| 新建 | `crates/erp-plugin/wit/plugin.wit` |
|
||||
| 新建 | `crates/erp-plugin/src/lib.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/error.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/manifest.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/host.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/engine.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/state.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/dynamic_table.rs` |
|
||||
| 修改 | `Cargo.toml` (workspace) |
|
||||
|
||||
---
|
||||
|
||||
## Phase 7C: 数据库表
|
||||
|
||||
**目标**: 创建插件元数据表 + SeaORM Entity。
|
||||
|
||||
### 7C.1 迁移文件
|
||||
|
||||
**新建**: `crates/erp-server/migration/src/m20260417_000033_create_plugins.rs`
|
||||
|
||||
三张表:
|
||||
|
||||
**plugins** — 插件注册与生命周期
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | UUID PK | manifest 中的 ID |
|
||||
| tenant_id | UUID NOT NULL | 所属租户 |
|
||||
| name | VARCHAR(200) | 插件名称 |
|
||||
| plugin_version | VARCHAR(50) | 语义版本 |
|
||||
| description | TEXT | |
|
||||
| author | VARCHAR(200) | |
|
||||
| status | VARCHAR(20) | uploaded/installed/enabled/running/disabled/uninstalled |
|
||||
| manifest_json | JSONB | 完整清单 |
|
||||
| wasm_binary | BYTEA | WASM 二进制 |
|
||||
| wasm_hash | VARCHAR(64) | SHA-256 |
|
||||
| config_json | JSONB DEFAULT '{}' | 插件配置 |
|
||||
| error_message | TEXT | 最近错误 |
|
||||
| installed_at | TIMESTAMPTZ | |
|
||||
| enabled_at | TIMESTAMPTZ | |
|
||||
| + 标准字段 | | created_at, updated_at, created_by, updated_by, deleted_at, version |
|
||||
|
||||
索引: `idx_plugins_tenant_status`, `idx_plugins_name` (均 WHERE deleted_at IS NULL)
|
||||
|
||||
**plugin_entities** — 插件动态表注册
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| plugin_id | UUID → plugins(id) | |
|
||||
| entity_name | VARCHAR(100) | |
|
||||
| table_name | VARCHAR(200) | 实际表名 |
|
||||
| schema_json | JSONB | 字段定义 |
|
||||
| + 标准字段 | | |
|
||||
|
||||
**plugin_event_subscriptions** — 事件订阅
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | UUID PK | |
|
||||
| plugin_id | UUID → plugins(id) | |
|
||||
| event_pattern | VARCHAR(200) | 如 "workflow.task.*" |
|
||||
| created_at | TIMESTAMPTZ | |
|
||||
|
||||
**修改**: [migration/lib.rs](crates/erp-server/migration/src/lib.rs) — 注册新迁移
|
||||
|
||||
### 7C.2 SeaORM Entity
|
||||
|
||||
**新建**: `crates/erp-plugin/src/entity/mod.rs`
|
||||
**新建**: `crates/erp-plugin/src/entity/plugin.rs` — plugins 表 Entity
|
||||
**新建**: `crates/erp-plugin/src/entity/plugin_entity.rs` — plugin_entities 表 Entity
|
||||
**新建**: `crates/erp-plugin/src/entity/plugin_event_subscription.rs` — 事件订阅 Entity
|
||||
|
||||
每个 Entity 遵循标准模式: DeriveEntityModel, Relation, ActiveModelBehavior
|
||||
|
||||
**7C 文件清单**:
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 新建 | `crates/erp-server/migration/src/m20260417_000033_create_plugins.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/entity/mod.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/entity/plugin.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/entity/plugin_entity.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/entity/plugin_event_subscription.rs` |
|
||||
| 修改 | `crates/erp-server/migration/src/lib.rs` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 7D: 插件管理 API
|
||||
|
||||
**目标**: 构建 admin REST API 实现完整插件生命周期管理。
|
||||
|
||||
### 7D.1 DTO
|
||||
|
||||
**新建**: `crates/erp-plugin/src/dto.rs`
|
||||
|
||||
```rust
|
||||
// Response
|
||||
pub struct PluginResp { id, name, version, description, author, status, config, installed_at, enabled_at, entities, permissions, version }
|
||||
pub struct PluginEntityResp { name, display_name, table_name }
|
||||
pub struct PluginHealthResp { plugin_id, status, details }
|
||||
|
||||
// Request
|
||||
pub struct UpdatePluginConfigReq { config: serde_json::Value, version: i32 }
|
||||
|
||||
// Query
|
||||
pub struct PluginListParams { page, page_size, status, search }
|
||||
```
|
||||
|
||||
### 7D.2 Service
|
||||
|
||||
**新建**: `crates/erp-plugin/src/service.rs`
|
||||
|
||||
```rust
|
||||
pub struct PluginService;
|
||||
|
||||
impl PluginService {
|
||||
pub async fn upload(tenant_id, operator_id, wasm_binary, manifest_toml, db) -> AppResult<PluginResp>
|
||||
pub async fn install(plugin_id, tenant_id, operator_id, db, engine) -> AppResult<PluginResp>
|
||||
pub async fn enable(plugin_id, tenant_id, operator_id, db, engine) -> AppResult<PluginResp>
|
||||
pub async fn disable(plugin_id, tenant_id, operator_id, db, engine) -> AppResult<PluginResp>
|
||||
pub async fn uninstall(plugin_id, tenant_id, operator_id, db, engine) -> AppResult<PluginResp>
|
||||
pub async fn list(tenant_id, pagination, status, search, db) -> AppResult<(Vec<PluginResp>, u64)>
|
||||
pub async fn get_by_id(plugin_id, tenant_id, db) -> AppResult<PluginResp>
|
||||
pub async fn update_config(plugin_id, tenant_id, operator_id, req, db) -> AppResult<PluginResp>
|
||||
pub async fn health_check(plugin_id, tenant_id, db, engine) -> AppResult<PluginHealthResp>
|
||||
pub async fn get_schema(plugin_id, tenant_id, db) -> AppResult<serde_json::Value>
|
||||
}
|
||||
```
|
||||
|
||||
生命周期状态机: `uploaded → installed → enabled/running → disabled → uninstalled`
|
||||
- upload: 解析 manifest + 存储 wasm_binary + status=uploaded
|
||||
- install: 创建动态表 + 注册 entity 记录 + 注册事件订阅 + status=installed
|
||||
- enable: engine.load + engine.initialize + engine.start_event_listener + status=running
|
||||
- disable: engine.disable + cancel 事件订阅 + status=disabled
|
||||
- uninstall: disable(如运行中) + drop 动态表 + status=uninstalled
|
||||
|
||||
### 7D.3 Handlers
|
||||
|
||||
**新建**: `crates/erp-plugin/src/handler/mod.rs`
|
||||
**新建**: `crates/erp-plugin/src/handler/plugin_handler.rs`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/admin/plugins/upload` | 上传 (multipart: wasm + manifest) |
|
||||
| GET | `/admin/plugins` | 列表 (分页+过滤) |
|
||||
| GET | `/admin/plugins/{id}` | 详情 |
|
||||
| GET | `/admin/plugins/{id}/schema` | 实体 schema |
|
||||
| POST | `/admin/plugins/{id}/install` | 安装 |
|
||||
| POST | `/admin/plugins/{id}/enable` | 启用 |
|
||||
| POST | `/admin/plugins/{id}/disable` | 停用 |
|
||||
| POST | `/admin/plugins/{id}/uninstall` | 卸载 |
|
||||
| DELETE | `/admin/plugins/{id}` | 清除 |
|
||||
| GET | `/admin/plugins/{id}/health` | 健康检查 |
|
||||
| PUT | `/admin/plugins/{id}/config` | 更新配置 |
|
||||
|
||||
所有 handler 遵循现有模式: `State<PluginState>`, `Extension<TenantContext>`, `require_permission("plugin.admin")`, utoipa 注解
|
||||
|
||||
### 7D.4 Module 注册
|
||||
|
||||
**新建**: `crates/erp-plugin/src/module.rs`
|
||||
|
||||
```rust
|
||||
pub struct PluginModule;
|
||||
impl ErpModule for PluginModule { name="plugin", dependencies=["auth","config"], module_type=Builtin }
|
||||
|
||||
impl PluginModule {
|
||||
pub fn protected_routes<S>() -> Router<S> // 上述所有路由
|
||||
}
|
||||
```
|
||||
|
||||
### 7D.5 服务端集成
|
||||
|
||||
**修改**: [main.rs](crates/erp-server/src/main.rs)
|
||||
- 创建 `PluginEngine::new(db.clone(), event_bus.clone(), config)`
|
||||
- 注册 `PluginModule` 到 registry
|
||||
- 合并 `PluginModule::protected_routes()` 到 protected_routes
|
||||
- 启动时恢复已 enabled 的插件: 查询 plugins 表 → engine.load + initialize + start_event_listener
|
||||
|
||||
**修改**: [state.rs](crates/erp-server/src/state.rs)
|
||||
- AppState 新增 `pub plugin_engine: erp_plugin::engine::PluginEngine`
|
||||
- 添加 `FromRef<AppState> for erp_plugin::PluginState`
|
||||
|
||||
**修改**: [seed.rs](crates/erp-auth/src/service/seed.rs) — 添加 `plugin.admin`, `plugin.list` 权限种子
|
||||
|
||||
**修改**: [Cargo.toml](crates/erp-server/Cargo.toml) — 添加 `erp-plugin.workspace = true`
|
||||
|
||||
**7D 文件清单**:
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 新建 | `crates/erp-plugin/src/dto.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/service.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/handler/mod.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/handler/plugin_handler.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/module.rs` |
|
||||
| 修改 | `crates/erp-server/src/main.rs` |
|
||||
| 修改 | `crates/erp-server/src/state.rs` |
|
||||
| 修改 | `crates/erp-server/Cargo.toml` |
|
||||
| 修改 | `crates/erp-auth/src/service/seed.rs` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 7E: 插件数据 CRUD API
|
||||
|
||||
**目标**: 通用数据 CRUD 端点 `/api/v1/plugins/{plugin_id}/{entity}/*`。
|
||||
|
||||
### 7E.1 数据 DTO
|
||||
|
||||
**新建**: `crates/erp-plugin/src/data_dto.rs`
|
||||
|
||||
```rust
|
||||
pub struct PluginDataResp { id, data: serde_json::Value, created_at, updated_at, version }
|
||||
pub struct CreatePluginDataReq { data: serde_json::Value }
|
||||
pub struct UpdatePluginDataReq { data: serde_json::Value, version: i32 }
|
||||
pub struct PluginDataListParams { page, page_size, search }
|
||||
```
|
||||
|
||||
### 7E.2 数据 Service
|
||||
|
||||
**新建**: `crates/erp-plugin/src/data_service.rs`
|
||||
|
||||
```rust
|
||||
pub struct PluginDataService;
|
||||
impl PluginDataService {
|
||||
pub async fn create(plugin_id, entity_name, tenant_id, operator_id, req, db, event_bus) -> AppResult<PluginDataResp>
|
||||
pub async fn list(plugin_id, entity_name, tenant_id, pagination, search, db) -> AppResult<(Vec<PluginDataResp>, u64)>
|
||||
pub async fn get_by_id(plugin_id, entity_name, id, tenant_id, db) -> AppResult<PluginDataResp>
|
||||
pub async fn update(plugin_id, entity_name, id, tenant_id, operator_id, req, db, event_bus) -> AppResult<PluginDataResp>
|
||||
pub async fn delete(plugin_id, entity_name, id, tenant_id, operator_id, db, event_bus) -> AppResult<()>
|
||||
}
|
||||
```
|
||||
|
||||
每个方法: 解析 table_name → 验证插件 running → 执行原始参数化 SQL → 发布 domain event → 审计日志
|
||||
|
||||
### 7E.3 数据 Handler
|
||||
|
||||
**新建**: `crates/erp-plugin/src/handler/data_handler.rs`
|
||||
|
||||
| 方法 | 路径 |
|
||||
|------|------|
|
||||
| GET | `/plugins/{plugin_id}/{entity}` |
|
||||
| POST | `/plugins/{plugin_id}/{entity}` |
|
||||
| GET | `/plugins/{plugin_id}/{entity}/{id}` |
|
||||
| PUT | `/plugins/{plugin_id}/{entity}/{id}` |
|
||||
| DELETE | `/plugins/{plugin_id}/{entity}/{id}` |
|
||||
|
||||
权限: `plugin.{plugin_id}.{entity}.{action}`
|
||||
|
||||
**修改**: [module.rs](crates/erp-plugin/src/module.rs) — 添加数据路由
|
||||
|
||||
**7E 文件清单**:
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 新建 | `crates/erp-plugin/src/data_dto.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/data_service.rs` |
|
||||
| 新建 | `crates/erp-plugin/src/handler/data_handler.rs` |
|
||||
| 修改 | `crates/erp-plugin/src/module.rs` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 8A: 前端 API + Store
|
||||
|
||||
### 8A.1 插件 API 模块
|
||||
|
||||
**新建**: `apps/web/src/api/plugins.ts`
|
||||
|
||||
```typescript
|
||||
export interface PluginInfo { id, name, version, description, author, status, config, installed_at, enabled_at, entities, permissions, version }
|
||||
export type PluginStatus = 'uploaded' | 'installed' | 'enabled' | 'running' | 'disabled' | 'uninstalled'
|
||||
export interface PluginEntityInfo { name, display_name, table_name }
|
||||
|
||||
export async function listPlugins(page, pageSize, status?)
|
||||
export async function getPlugin(id)
|
||||
export async function uploadPlugin(file: File, manifest: string)
|
||||
export async function installPlugin(id)
|
||||
export async function enablePlugin(id)
|
||||
export async function disablePlugin(id)
|
||||
export async function uninstallPlugin(id)
|
||||
export async function purgePlugin(id)
|
||||
export async function getPluginHealth(id)
|
||||
export async function updatePluginConfig(id, config, version)
|
||||
export async function getPluginSchema(id)
|
||||
```
|
||||
|
||||
### 8A.2 插件数据 API
|
||||
|
||||
**新建**: `apps/web/src/api/pluginData.ts`
|
||||
|
||||
```typescript
|
||||
export interface PluginDataRecord { id, data: Record<string,unknown>, created_at, updated_at, version }
|
||||
export async function listPluginData(pluginId, entity, page?, pageSize?)
|
||||
export async function getPluginData(pluginId, entity, id)
|
||||
export async function createPluginData(pluginId, entity, data)
|
||||
export async function updatePluginData(pluginId, entity, id, data, version)
|
||||
export async function deletePluginData(pluginId, entity, id)
|
||||
```
|
||||
|
||||
### 8A.3 Plugin Store
|
||||
|
||||
**新建**: `apps/web/src/stores/plugin.ts`
|
||||
|
||||
```typescript
|
||||
interface PluginStore {
|
||||
plugins: PluginInfo[]
|
||||
loading: boolean
|
||||
pluginMenuItems: PluginMenuItem[]
|
||||
fetchPlugins: (page?, status?) => Promise<void>
|
||||
refreshMenuItems: () => void
|
||||
}
|
||||
interface PluginMenuItem { key: string, icon: string, label: string, pluginId: string, entity: string, menuGroup?: string }
|
||||
```
|
||||
|
||||
**8A 文件清单**:
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 新建 | `apps/web/src/api/plugins.ts` |
|
||||
| 新建 | `apps/web/src/api/pluginData.ts` |
|
||||
| 新建 | `apps/web/src/stores/plugin.ts` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 8B: 插件管理页面
|
||||
|
||||
### 8B.1 PluginAdmin 页面
|
||||
|
||||
**新建**: `apps/web/src/pages/PluginAdmin.tsx`
|
||||
|
||||
遵循 Users.tsx 模式:
|
||||
- Table 列: name, version, status(Tag 颜色), author, actions
|
||||
- Upload Modal: Upload 组件(拖拽 .wasm) + TextArea(manifest TOML)
|
||||
- Detail Drawer: manifest JSON, entities, config, health
|
||||
- Actions 按钮根据 status 动态显示: Install/Enable/Disable/Uninstall
|
||||
|
||||
```typescript
|
||||
const STATUS_CONFIG = {
|
||||
uploaded: { color: '#64748B', label: '已上传' },
|
||||
installed: { color: '#2563EB', label: '已安装' },
|
||||
enabled: { color: '#059669', label: '已启用' },
|
||||
running: { color: '#059669', label: '运行中' },
|
||||
disabled: { color: '#DC2626', label: '已禁用' },
|
||||
uninstalled: { color: '#9333EA', label: '已卸载' },
|
||||
}
|
||||
```
|
||||
|
||||
### 8B.2 路由 + 侧边栏
|
||||
|
||||
**修改**: [App.tsx](apps/web/src/App.tsx) — 添加 `lazy(() => import('./pages/PluginAdmin'))` + `<Route path="/plugins/admin" ...>`
|
||||
|
||||
**修改**: [MainLayout.tsx](apps/web/src/layouts/MainLayout.tsx)
|
||||
- sysMenuItems 添加 `{ key: '/plugins/admin', icon: <AppstoreOutlined />, label: '插件管理' }`
|
||||
- routeTitleMap 添加 `'/plugins/admin': '插件管理'`
|
||||
- 添加动态插件菜单组(从 pluginStore.pluginMenuItems 生成)
|
||||
|
||||
**8B 文件清单**:
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 新建 | `apps/web/src/pages/PluginAdmin.tsx` |
|
||||
| 修改 | `apps/web/src/App.tsx` |
|
||||
| 修改 | `apps/web/src/layouts/MainLayout.tsx` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 8C: 动态路由 + PluginCRUDPage
|
||||
|
||||
### 8C.1 PluginCRUDPage
|
||||
|
||||
**新建**: `apps/web/src/pages/PluginCRUDPage.tsx`
|
||||
|
||||
通用配置驱动 CRUD 页面:
|
||||
- 从 URL params 获取 `pluginId` + `entityName`
|
||||
- 调用 `getPluginSchema(pluginId)` 获取字段定义
|
||||
- 自动生成 Table columns(从 entity.fields)
|
||||
- 自动生成 Form fields(根据 ui_widget: text/number/select/date/switch)
|
||||
- CRUD 操作调用 pluginData API
|
||||
|
||||
```typescript
|
||||
export default function PluginCRUDPage() {
|
||||
const { pluginId, entityName } = useParams();
|
||||
// fetch schema → generate columns → render Table + Modal form
|
||||
}
|
||||
```
|
||||
|
||||
### 8C.2 动态路由
|
||||
|
||||
**修改**: [App.tsx](apps/web/src/App.tsx) — 添加:
|
||||
```typescript
|
||||
<Route path="/plugins/:pluginId/:entityName" element={<PluginCRUDPage />} />
|
||||
```
|
||||
|
||||
**8C 文件清单**:
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 新建 | `apps/web/src/pages/PluginCRUDPage.tsx` |
|
||||
| 修改 | `apps/web/src/App.tsx` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 8D: E2E 验证
|
||||
|
||||
### 8D.1 测试插件 manifest
|
||||
|
||||
**新建**: `crates/erp-plugin-test-sample/plugin.toml` — 包含完整 schema/events/ui/permissions 定义
|
||||
|
||||
**修改**: `crates/erp-plugin-test-sample/src/lib.rs` — 适配最终 WIT 接口
|
||||
|
||||
### 8D.2 启动时恢复插件
|
||||
|
||||
**修改**: [main.rs](crates/erp-server/src/main.rs) — 启动时查询 plugins(status=running) → 逐个 engine.load + initialize + start_event_listener
|
||||
|
||||
### 8D.3 验证清单
|
||||
|
||||
手动 E2E 测试流程:
|
||||
1. 编译测试插件: `cargo build -p erp-plugin-test-sample --target wasm32-unknown-unknown --release`
|
||||
2. 转换: `wasm-tools component new ... -o target/test-sample.component.wasm`
|
||||
3. 打包 manifest.toml + .component.wasm
|
||||
4. 通过 PluginAdmin 上传
|
||||
5. 安装 → 验证动态表创建
|
||||
6. 启用 → 验证 init() 调用成功
|
||||
7. 通过 PluginCRUDPage 创建/读取/更新/删除数据
|
||||
8. 触发 workflow.task.completed 事件 → 验证插件 handle_event 被调用
|
||||
9. 停用 → 验证事件订阅取消
|
||||
10. 卸载 → 验证动态表清理
|
||||
|
||||
---
|
||||
|
||||
## 文件统计
|
||||
|
||||
| Phase | 新建 | 修改 | 合计 |
|
||||
|-------|------|------|------|
|
||||
| 7A | 0 | 4 | 4 |
|
||||
| 7B | 9 | 1 | 10 |
|
||||
| 7C | 5 | 1 | 6 |
|
||||
| 7D | 5 | 4 | 9 |
|
||||
| 7E | 3 | 1 | 4 |
|
||||
| 8A | 3 | 0 | 3 |
|
||||
| 8B | 1 | 2 | 3 |
|
||||
| 8C | 1 | 1 | 2 |
|
||||
| 8D | 1 | 2 | 3 |
|
||||
| **合计** | **28** | **16** | **44** |
|
||||
|
||||
## 验证方式
|
||||
|
||||
每个 Phase 完成后:
|
||||
- `cargo check` 全 workspace 编译通过
|
||||
- `cargo test --workspace` 测试通过
|
||||
- Phase 7 完成后: `cargo run -p erp-server` 启动成功,API 端点可用
|
||||
- Phase 8 完成后: `pnpm dev` 前端启动,PluginAdmin 页面可访问,完整 CRUD 链路可用
|
||||
@@ -24,7 +24,7 @@ tags: [database, seaorm, migration, multi-tenant]
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `crates/erp-server/migration/src/lib.rs` | Migrator 注册所有迁移 |
|
||||
| `crates/erp-server/migration/src/m*.rs` | 103 个迁移文件 |
|
||||
| `crates/erp-server/migration/src/m*.rs` | 145 个迁移文件 |
|
||||
| `crates/erp-core/src/types.rs` | BaseFields 标准字段定义 |
|
||||
|
||||
### 迁移命名规则
|
||||
|
||||
@@ -4,30 +4,30 @@
|
||||
|
||||
## 关键数字
|
||||
|
||||
> 最后更新: 2026-05-13 | 数据截止: feat/media-library-banner 分支
|
||||
> 最后更新: 2026-05-15 | 数据截止: feat/media-library-banner 分支
|
||||
|
||||
| 指标 | 值 |
|
||||
|------|-----|
|
||||
| Rust crate | 17 个(erp-core + 5 基础业务 + erp-health + erp-ai + erp-dialysis + erp-plugin + 7 插件/原型) |
|
||||
| Rust 源文件 | 599 个 |
|
||||
| Rust 源文件 | **649 个** |
|
||||
| 数据库表 | 30 基础表 + 49 健康业务表 + 9 AI 表 + 3 媒体库/轮播图表 |
|
||||
| 数据库迁移 | 137 个(最新 m20260510_000137) |
|
||||
| 数据库迁移 | **145 个**(最新 m20260513_000145) |
|
||||
| 后端路由 | 260+ 个(11 公开 + 14 FHIR + 2 网关 + ~240 受保护) |
|
||||
| 核心模块 | 5 基础 (auth/config/workflow/message/plugin) + 3 业务 (health + ai + dialysis) |
|
||||
| erp-health 实体 | **57 个** Entity(31 handler / 36 service / 21 DTO,189 文件) |
|
||||
| erp-ai 实体 | 9 个 Entity(45 文件,4 AI Provider) |
|
||||
| Web 前端 | 297 个 TS/TSX 文件(29 活跃路由 + 6 冻结路由,52 API 模块) |
|
||||
| 微信小程序 | Taro 4.2 + React 18,124 个 TS/TSX 文件 / 66 页面 / 4 TabBar + 医生端分包 |
|
||||
| 前端单元测试 | 62 个测试文件(472 Web 断言 + 39 MP 断言)+ 13 E2E spec(124 断言) |
|
||||
| 后端测试 | **999 个函数**(815 同步 + 184 异步),78 个文件含内联测试 |
|
||||
| Web 前端 | 332 个 TS/TSX 文件(29 活跃路由 + 6 冻结路由,52 API 模块) |
|
||||
| 微信小程序 | Taro 4.2 + React 18,163 个 TS/TSX 文件 / 66 页面 / 4 TabBar + 医生端分包 |
|
||||
| 前端单元测试 | 88 个测试文件(472 Web 断言 + 39 MP 断言)+ 13 E2E spec(124 断言) |
|
||||
| 后端测试 | **943 个函数**(762 同步 + 181 异步),79 个文件含内联测试 |
|
||||
| 事件系统 | 31 事件类型(health 模块内)/ 23 幂等消费者 / Outbox + LISTEN/NOTIFY |
|
||||
| 权限码 | **75+ 个**(health 28 + auth 25 + ai 7 + workflow 8 + dialysis 5 + plugin 2) |
|
||||
| 权限码 | **128 个**(health 28 + auth 25 + ai 7 + workflow 8 + dialysis 5 + plugin 2 + Copilot + 媒体库) |
|
||||
| 生产 unwrap | **24 处**(从 514 降至 24),全为安全解包 |
|
||||
| utoipa 注解 | 322 个 / 21/64 handler 文件 = 33% 覆盖 |
|
||||
| utoipa 注解 | 88 个文件含注解 |
|
||||
| Clippy | **全 workspace 0 警告**(2026-05-07 清零) |
|
||||
| 依赖版本 | 全部最新主版本线(Rust edition 2024) |
|
||||
| API 文档 | `http://localhost:3000/api/docs/openapi.json` |
|
||||
| Git 提交 | 720+ 次 |
|
||||
| Git 提交 | **800+ 次** |
|
||||
| 系统分析评分 | **6.9/10 (B)**(六维度全面均衡分析,2026-05-11) |
|
||||
| 审计状态 | V1: 83% → V2: 85%,P0 安全修复已完成,V2 CRITICAL 全清零 |
|
||||
| 角色测试 | R01-R05 全角色验证完成,86.5% 通过率,5 个 BUG 已修复;小程序 MP 多角色 96.2% 通过率 |
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
### 基础设施
|
||||
- [[infrastructure]] — 连接信息 · 环境变量 · 一键启动 (**单一真相源**)
|
||||
- [[database]] — SeaORM 迁移 · 多租户表结构(137 迁移)
|
||||
- [[database]] — SeaORM 迁移 · 多租户表结构(145 迁移)
|
||||
- [[frontend]] — React 19 SPA · 健康管理页面(29 活跃路由 + 6 冻结 + 工作台组件)
|
||||
- [[testing]] — 验证清单 · 测试分布 · 性能基准
|
||||
|
||||
@@ -138,38 +138,27 @@
|
||||
|
||||
| 类型 | 位置 |
|
||||
|------|------|
|
||||
| 健康模块设计规格 | `docs/superpowers/specs/2026-04-23-health-management-module-design.md` |
|
||||
| 健康模块设计规格 | `docs/superpowers/specs/2026-04-23-health-management-module-design.md`(已归档到 `archive/superpowers-completed/`) |
|
||||
| AI 模块设计规格 | `docs/superpowers/specs/2026-04-25-erp-ai-module-design.md` |
|
||||
| 内容管理设计规格 | `docs/superpowers/specs/2026-04-26-content-management-design.md` |
|
||||
| 媒体库+轮播图设计规格 | `docs/superpowers/specs/2026-05-10-media-library-banner-design.md` |
|
||||
| Copilot 基因化设计 | `docs/superpowers/specs/2026-05-11-copilot-gene-design.md` |
|
||||
| 六维度全面均衡分析 | `docs/superpowers/specs/2026-05-11-system-comprehensive-analysis-design.md`(6.9/10 B,六维度评估) |
|
||||
| PII 加密扩展规格 | `docs/superpowers/specs/2026-04-26-pii-encryption-expansion-design.md` |
|
||||
| 实时体征管线探讨 | `docs/superpowers/specs/2026-04-26-realtime-vital-signs-pipeline-design.md` |
|
||||
| 平台复盘与演进 | `docs/superpowers/specs/2026-04-26-platform-retrospective-and-evolution-design.md` |
|
||||
| 设计规格(全量) | `docs/superpowers/specs/` (50 份) |
|
||||
| 设计规格(活跃) | `docs/superpowers/specs/` (32 份) |
|
||||
| 实施计划(活跃) | `docs/superpowers/plans/` (30 份) |
|
||||
| UI/UX 重构设计规格 | `docs/superpowers/specs/2026-04-28-ui-ux-overhaul-design.md` |
|
||||
| UI/UX 重构实施计划 | `docs/superpowers/plans/2026-04-28-ui-ux-overhaul-plan.md` |
|
||||
| 实施计划(全量) | `docs/superpowers/plans/` (51 份) |
|
||||
| 全系统审计报告(V1) | `docs/audits/08-audit-report-2026-04-30.md`(83% 总体完成度,2 CRITICAL + 3 HIGH) |
|
||||
| 全系统审计报告(V1) | `docs/archive/audits-v1/08-audit-report-2026-04-30.md`(已归档) |
|
||||
| 全系统审计报告(V2) | `docs/audits/v2/13-final-report.md`(85% 总体完成度,P0 安全修复已完成) |
|
||||
| 审计基线快照 | `docs/audits/00-baseline-snapshot.md` |
|
||||
| 审计功能清单 | `docs/audits/01-feature-inventory.md`(328 路由三端映射矩阵) |
|
||||
| 审计后端完整性 | `docs/audits/02-backend-integrity.md`(100% 调用链连通) |
|
||||
| 审计事件系统 | `docs/audits/03-event-system.md`(25 事件 / 14 消费者 / 100% payload 一致) |
|
||||
| 审计参数配置 | `docs/audits/04-parameter-config.md`(105 DTO / 50 权限码 / 数据映射缺口) |
|
||||
| 审计差距模式 | `docs/audits/05-gap-patterns.md`(5 种模式,透析/知情同意 MP 缺失) |
|
||||
| 审计错误处理 | `docs/audits/06-error-handling.md`(SSE 不挂起 / 日志 30% 覆盖) |
|
||||
| 审计测试覆盖 | `docs/audits/07-test-coverage.md`(772 测试 / 前端极低 / AI 无集成测试) |
|
||||
| 讨论记录 | `docs/discussions/` (29 份) |
|
||||
| 讨论记录 | `docs/discussions/` (41 份) |
|
||||
| 事件注册表 | `docs/event-registry.md` |
|
||||
| Wiki 方法论 | `docs/wiki-methodology.md` |
|
||||
| 项目深度分析 | `docs/superpowers/specs/2026-05-03-project-analysis-brainstorm-design.md`(5 专家组分析,B+ 评分) |
|
||||
| 三维度系统分析 | `docs/discussions/2026-05-07-three-dimension-analysis.md`(后端/前端/质量三维深度分析,2026-05-07) |
|
||||
| 多专家组头脑风暴 | `docs/discussions/2026-05-07-expert-brainstorm-session.md`(5 专家组评审,综合 6.4/10 B-,行动清单) |
|
||||
| 六维度系统分析+头脑风暴 | `docs/superpowers/specs/2026-05-11-system-comprehensive-analysis-design.md`(6 专家组,综合 6.9/10 B,三个月路线图) |
|
||||
| 角色测试计划(全量) | `docs/qa/role-test-plans/` (R01-R05) |
|
||||
| 角色测试结果 | `docs/qa/role-test-results/` (R01 100% / R02 100% / R03 90.9% / R04 90.0% / R05 72.7% → 修复后待复测) |
|
||||
| 系统集成测试结果 | `docs/qa/role-test-results/T00-system-integration-results.md` (20/28 通过) |
|
||||
| 小程序 E2E 测试结果 | `docs/qa/role-test-results/T10-miniprogram-e2e-results.md` (需手动执行) |
|
||||
| 协作规则 | `CLAUDE.md` |
|
||||
| 插件制作指南 | `.claude/skills/plugin-development/SKILL.md` |
|
||||
| **归档** | |
|
||||
| 早期 CRM/插件设计 | `docs/archive/superpowers-early/` (13 份,4月13-20日早期迭代) |
|
||||
| 已完成设计/计划 | `docs/archive/superpowers-completed/` (28 份,已实施完成) |
|
||||
| V1 审计报告 | `docs/archive/audits-v1/` (13 份,已被 V2 取代) |
|
||||
| 早期讨论/测试报告 | `docs/archive/discussions-early/` + `docs/archive/test-reports-early/` |
|
||||
|
||||
Reference in New Issue
Block a user