fix(用户管理): 修复用户列表页面加载失败问题
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

修复用户列表页面加载失败导致测试超时的问题,确保页面元素正确渲染
This commit is contained in:
iven
2026-04-19 08:46:28 +08:00
parent 0ee9d22634
commit 841766b168
174 changed files with 26366 additions and 675 deletions

7
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Performance artifacts (large binary files, regenerate locally)
*.heapsnapshot
perf-trace-*.json
# Debug screenshots (temporary, not documentation)
debug-*.png
screenshot-current.png

View File

@@ -0,0 +1,382 @@
# ERP 平台全面深度审计报告
> 审计日期: 2026-04-18
> 审计方式: 前端功能链路 + API 接口测试 + 代码静态分析 + 安全渗透测试
> 审计范围: 全部 5 个业务模块 + 2 个插件 + 前端 SPA + 安全与代码质量
---
## 一、审计总结
| 维度 | CRITICAL | HIGH | MEDIUM | LOW | 合计 |
|------|----------|------|--------|-----|------|
| 安全 | 2 | 4 | 3 | 2 | 11 |
| 功能 | 1 | 3 | 3 | 1 | 8 |
| 代码质量 | 0 | 1 | 3 | 6 | 10 |
| **合计** | **3** | **8** | **9** | **9** | **29** |
---
## 二、CRITICAL — 必须立即修复
### C-01 Redis 凭据硬编码在配置文件中(泄露到 Git
- **文件**: `crates/erp-server/config/default.toml` (line 11)
- **现象**: `url = "redis://:redis_KBCYJk@129.204.154.246:6379"` 硬编码了远程 Redis 密码和 IP
- **影响**: 凭据已提交到 Git 仓库,任何有代码访问权限的人都能获取 Redis 密码和服务器 IP
- **修复**:
1. 立即轮换 Redis 密码
2.`url` 改回 `__MUST_SET_VIA_ENV__` 占位符
3. 使用环境变量 `ERP__REDIS__URL` 传递
### C-02 存储型 XSS — 用户输入未做 HTML 清理
- **文件**: `crates/erp-auth/src/service/user_service.rs` (创建/更新用户)
- **现象**: 通过 `POST /api/v1/users` 可将 `<script>alert('xss')</script>``<img src=x onerror=alert(1)>` 直接存入数据库的 `display_name``email` 等字段
- **影响**:
- React JSX 自动转义避免了前端直接触发(当前安全)
- 但原始 HTML 已存储在数据库中,在以下场景可触发:
- 邮件模板渲染
- PDF 导出
- OpenAPI 文档中的 schema 示例
- 未来使用非 React 渲染的任何场景
- **验证**:
```
POST /api/v1/users {"display_name":"<img src=x onerror=alert(document.cookie)>"}
→ 201 Created, 原始 HTML 直接入库
```
- **修复**: 后端入库前对所有用户可编辑字段 strip HTML tags 或 escape HTML entities
### C-03 首页工作台统计卡片永久 Loading
- **文件**: `apps/web/src/pages/Home.tsx`
- **现象**: 4 个统计卡片(用户总数、角色数量、流程实例、未读消息)始终显示 loading 动画
- **根因**: `useCountUp` 动画依赖数据加载API 返回格式与前端预期不匹配
- **影响**: 工作台页面无法展示核心统计数据,用户体验极差
- **修复**: 修正统计 API 的数据格式,确保与 `StatisticCard` 组件预期一致
---
## 三、HIGH — 高优先级问题
### H-01 用户名唯一性约束未生效
- **文件**: `crates/erp-auth/src/service/user_service.rs` (创建用户)
- **现象**: 用相同 `username` 创建两次用户均返回 `201 Created`
- **影响**: 可能导致身份混淆、审计日志混乱
- **修复**: 在创建用户前先查询 `username` 是否已存在(同 tenant_id + 未删除),或添加数据库唯一索引
### H-02 消息模板 API 返回空 body
- **文件**: `GET /api/v1/messages/templates`
- **现象**: 返回空 HTTP body非 JSON 格式),前端无法解析
- **影响**: 消息中心"模板"tab 无法展示数据
- **修复**: 修复空列表的序列化处理,确保返回 `{"success":true,"data":{"data":[],"total":0}}`
### H-03 主题 API 返回空 body
- **文件**: `GET /api/v1/config/theme`
- **现象**: 返回空 body 而非 JSON
- **影响**: 主题设置页面无法加载当前配置
- **修复**: 为新租户初始化默认主题配置,或 API 返回默认值
### H-04 JWT Token 体积过大
- **文件**: `crates/erp-auth/src/service/token_service.rs`
- **现象**: Access Token 包含 64 个权限字符串JWT payload 约 2.5KB
- **影响**:
- 每次 HTTP 请求都要携带 2.5KB+ 的 Authorization header
- 影响带宽和性能,尤其在高频 API 调用场景
- 权限变更需要等 Token 过期才生效(最长 15 分钟)
- **修复**:
1. 方案 AJWT 只存角色,权限在服务端 Redis 缓存实时查询
2. 方案 B使用权限位图/bitmask 压缩
3. 方案 C减少 JWT 中的权限列表,改为中间件实时校验
### H-05 字段长度无限制
- **文件**: `crates/erp-auth/src/dto.rs`
- **现象**: `display_name` 可接受 500+ 字符(测试通过 500 个 'A'),无 max length 验证
- **影响**: UI 布局破坏、潜在数据库性能问题
- **修复**: 添加 `#[validate(length(max = 100))]` 等长度约束
---
## 四、MEDIUM — 中优先级问题
### M-01 菜单配置前端硬编码,未使用后端 API
- **文件**: `apps/web/src/components/AppLayout.tsx` 或路由配置
- **现象**: 后端 `GET /api/v1/config/menus` 返回空数组,侧边栏菜单完全前端硬编码
- **影响**: 菜单无法通过管理后台动态配置,插件菜单需要在代码中手动添加
- **修复**: 实现前端从 API 动态加载菜单配置,或在后端初始化默认菜单数据
### M-02 时间戳未本地化显示
- **文件**: 消息中心、审计日志等列表页面
- **现象**: 时间显示为原始 ISO 格式 `2026-04-14T13:10:59.516776Z`,用户不友好
- **影响**: 用户体验差
- **修复**: 使用 dayjs 格式化为本地时间,如 `2026-04-14 21:10:59`
### M-03 前端路由仅做认证守卫,无权限守卫
- **文件**: `apps/web/src/App.tsx`
- **现象**: 路由只检查是否已登录token 存在),不检查用户是否有权限访问特定页面
- **影响**: 无权限用户可以通过直接输入 URL 访问任何页面(虽然 API 层会返回 403
- **修复**: 在路由守卫中增加权限校验,根据 JWT 中的 permissions 控制页面可见性
### M-04 消息响应包含内部 tenant_id
- **文件**: `crates/erp-message/src/handler/message_handler.rs`
- **现象**: `GET /api/v1/messages` 返回每条消息的 `tenant_id` 字段
- **影响**: 泄露内部多租户架构信息
- **修复**: 在 DTO 层排除 `tenant_id` 字段
### M-05 搜索缺少防抖Debounce
- **文件**: `apps/web/src/pages/Users.tsx`, `apps/web/src/components/EntitySelect.tsx`
- **现象**: 用户搜索输入框每次按键都触发 API 请求
- **影响**: 高频请求冲击服务器,用户体验差
- **修复**: 添加 300ms debounce已有 `useDebouncedValue` Hook 但未使用)
### M-06 Organizations 页面 useCallback/useEffect 循环依赖
- **文件**: `apps/web/src/pages/Organizations.tsx`
- **现象**: useCallback 依赖项导致 useEffect 无限循环渲染
- **影响**: 性能问题、可能导致浏览器卡顿
- **修复**: 重构 useCallback 依赖项,消除循环
### M-07 测试数据残留在生产数据库
- **现象**: 数据库中存在以下测试用户和数据:
- `xss_user` — display_name 为 `<script>alert('xss')</script>`
- `test_role_api` — 测试角色
- `audit_test_user` — 审计测试用户
- `testuser01` — 测试用户
- `test_user_api` — API 测试用户
- `Perf test` 消息 — 性能测试消息
- `business_key: PERF-TEST-26311` 的待办任务
- **影响**: 数据污染、潜在安全风险
- **修复**: 清理所有测试数据,确保数据库只包含有意义的业务数据
### M-08 系统设置多个 tab 数据为空
- **现象**: 数据字典、编号规则、系统参数、语言管理等 tab 均无种子数据
- **影响**: 系统看起来像空的,用户需要手动配置所有基础数据
- **修复**: 在 `on_tenant_created` 中初始化默认字典(客户类型、行业、地区等)
### M-09 中文 API 响应编码异常
- **现象**: 部分中文内容在 API JSON 响应中显示为乱码(如 `\u7eef\u8364\u7cba` 而非"系统管理员"
- **影响**: 可能是 curl 的显示问题,也可能是后端序列化配置问题
- **修复**: 确认后端 JSON 序列化使用 `ensure_ascii: false` 等效配置
---
## 五、LOW — 低优先级 / 代码质量
### L-01 死代码 — graph 目录
- **文件**: `apps/web/src/pages/plugins/graph/` (6 个文件)
- **现象**: 完全未使用的代码
- **修复**: 删除或标记为实验性
### L-02 死代码 — 未使用的 Hooks
- **文件**: `apps/web/src/hooks/`
- **现象**: `useDarkMode`, `useDebouncedValue`, `usePaginatedData`, `useApiRequest` 4 个 Hook 未被引用
- **修复**: 清理或接入这些 Hook特别是 useDebouncedValue 在搜索场景很有用)
### L-03 重复代码 — useCountUp
- **现象**: `useCountUp` 在 3 处重复定义
- **修复**: 提取为共享 Hook
### L-04 暗色模式检测逻辑重复
- **现象**: `const isDark = token.colorBgContainer === '#111827'` 在 20+ 组件中重复
- **修复**: 用已有的 `useDarkMode` Hook 替换
### L-05 i18n 已配置但未使用
- **现象**: i18next 已初始化且有 30 个翻译 key但所有页面组件硬编码中文
- **修复**: 逐步替换硬编码中文为 i18n key
### L-06 antd 废弃 API 警告
- **现象**: `Drawer` 的 `width` 属性、`Modal` 的 `destroyOnClose` 已废弃
- **修复**: 升级到 antd 6 的新 API 用法
### L-07 ErrorBoundary 错误信息泄露
- **文件**: `apps/web/src/components/ErrorBoundary.tsx`
- **现象**: 错误边界展示完整的错误堆栈给用户
- **修复**: 生产环境只显示友好错误消息,堆栈信息仅记录到控制台
### L-08 Home 页面使用 dangerouslySetInnerHTML
- **文件**: `apps/web/src/pages/Home.tsx`
- **现象**: 工作台页面使用 `dangerouslySetInnerHTML` 渲染内容
- **影响**: 如果内容包含用户输入,可能导致 XSS
- **修复**: 改用 React 组件渲染
### L-09 插件恢复计数不准确
- **现象**: `Plugin recovered: 0` 但实际 WASM 已加载
- **修复**: 修正 recovery 计数逻辑
---
## 六、安全测试矩阵
| 测试项 | 结果 | 备注 |
|--------|------|------|
| 无 Token 访问受保护端点 | ✅ 401 | 正确拦截 |
| 无效 Token | ✅ 401 | 正确拦截 |
| 篡改 Token payload | ✅ 401 | HMAC 签名校验有效 |
| 错误密码登录 | ✅ 401 | 正确拒绝 |
| 短密码创建用户 | ✅ 400 | 验证 min=6 生效 |
| 空 Token 刷新 | ✅ 401 | 正确拒绝 |
| 旧 Refresh Token 重用 | ✅ 401 | 轮换机制生效 |
| SQL 注入(搜索参数) | ✅ 安全 | SeaORM 参数化查询 |
| SQL 注入UUID 路径) | ✅ 安全 | UUID 解析拒绝非法字符 |
| 存储型 XSS | ❌ 入库 | 后端未清理 HTMLReact 前端安全 |
| 无权限用户访问 API | ✅ 403 | 权限校验正确 |
| 无权限用户提权 | ✅ 403 | 角色分配受权限保护 |
| 限流 | ✅ 生效 | 5 次失败后触发 429 |
| CORS 配置 | ✅ 白名单 | 仅允许 localhost 端口 |
| 凭据泄露 | ❌ Redis 密码硬编码 | 已提交到 Git |
---
## 七、功能链路审计结果
### 7.1 认证链路 ✅ 基本正常
| 环节 | 状态 | 备注 |
|------|------|------|
| 登录 → JWT 签发 | ✅ | access (15min) + refresh (7d) |
| Token 刷新轮换 | ✅ | 旧 Token 使用后立即失效 |
| 密码修改 → Token 吊销 | ✅ | 所有 refresh token 失效 |
| 登出 → Token 吊销 | ✅ | |
| 限流保护 | ✅ | 5 次失败后 429 |
| 审计日志记录 | ✅ | 登录成功/失败均有记录 |
### 7.2 用户管理 ✅ 基本正常,有缺陷
| 环节 | 状态 | 备注 |
|------|------|------|
| CRUD 操作 | ✅ | |
| 角色分配 | ✅ | |
| 用户名唯一性 | ❌ | 重复用户名可创建 |
| 输入验证 | ⚠️ | 密码有验证,其他字段长度/XSS 无验证 |
| 软删除 | ✅ | |
### 7.3 权限管理 ✅ 正常
| 环节 | 状态 | 备注 |
|------|------|------|
| 角色列表 | ✅ | 3 个角色admin/viewer/test |
| 权限分配 | ✅ | 54 个权限可精确分配 |
| 系统角色保护 | ⚠️ | admin 角色权限可被修改 |
| data_scope 配置 | ❌ | 权限对话框中无 data_scope 配置入口 |
### 7.4 工作流引擎 ✅ 正常
| 环节 | 状态 | 备注 |
|------|------|------|
| 流程定义 CRUD | ✅ | 3 个定义draft/published |
| 流程发起 | ✅ | |
| 任务审批 | ✅ | approve/reject |
| 任务委派 | ✅ | |
| 实例监控 | ✅ | running/terminated/suspended |
| 超时检测 | ✅ | 60s 间隔扫描 |
### 7.5 消息中心 ⚠️ 部分异常
| 环节 | 状态 | 备注 |
|------|------|------|
| 消息列表 | ✅ | 10 条消息 |
| 未读计数 | ✅ | bell 图标显示未读数 |
| 标记已读 | ✅ | |
| 全部已读 | ✅ | |
| 消息模板 | ❌ | API 返回空 body |
| 通知设置 | ⚠️ | 未验证 |
| 工作流事件 → 消息 | ✅ | "流程已启动" 消息自动生成 |
### 7.6 系统配置 ⚠️ 数据缺失
| 环节 | 状态 | 备注 |
|------|------|------|
| 数据字典 | ❌ | 空数据,无种子 |
| 菜单配置 | ❌ | 后端空,前端硬编码 |
| 编号规则 | ❌ | 空 |
| 系统参数 | ⚠️ | 未验证 |
| 主题设置 | ❌ | API 返回空 |
| 语言管理 | ⚠️ | 未验证 |
| 修改密码 | ✅ | 功能正常 |
| 审计日志 | ✅ | |
### 7.7 插件系统 ✅ 基本正常
| 环节 | 状态 | 备注 |
|------|------|------|
| CRM 插件运行 | ✅ | 状态:运行中 |
| 客户 CRUD | ✅ | 6 条客户数据 |
| 联系人 | ✅ | |
| 沟通记录 | ✅ | |
| 标签管理 | ✅ | |
| 客户关系 | ✅ | |
| 统计概览 | ⚠️ | |
| 销售漏斗 | ⚠️ | |
---
## 八、修复优先级建议
### 🔴 立即修复(本周内)
| 编号 | 问题 | 预计工作量 |
|------|------|-----------|
| C-01 | Redis 凭据从 config 移除,改用环境变量 | 0.5h |
| C-02 | 后端添加 HTML sanitize 中间件 | 2h |
| C-03 | 修复首页统计卡片数据格式 | 1h |
| H-01 | 添加用户名唯一性校验 | 1h |
### 🟡 本迭代修复2 周内)
| 编号 | 问题 | 预计工作量 |
|------|------|-----------|
| H-02 | 修复消息模板空返回 | 0.5h |
| H-03 | 修复主题 API 空返回 | 0.5h |
| H-04 | JWT 权限压缩或改为服务端查询 | 4h |
| H-05 | 添加字段长度验证 | 1h |
| M-03 | 添加前端路由权限守卫 | 2h |
| M-05 | 搜索添加防抖 | 0.5h |
| M-07 | 清理测试数据 | 1h |
| M-08 | 初始化默认系统配置数据 | 2h |
### 🟢 迭代中逐步修复
| 编号 | 问题 |
|------|------|
| M-01 | 菜单动态加载 |
| M-02 | 时间戳本地化 |
| M-04 | API 响应排除 tenant_id |
| M-06 | Organizations 性能修复 |
| L-01~L-09 | 代码质量清理 |
---
## 九、系统亮点(做得好的地方)
1. **Token 刷新轮换机制** — 旧 Refresh Token 重用被正确拒绝
2. **限流保护** — 登录失败 5 次后触发 429 Too Many Requests
3. **SeaORM 参数化查询** — SQL 注入测试全部被拦截
4. **权限校验完整性** — 无权限用户所有操作返回 403
5. **多租户架构** — JWT 注入 tenant_id中间件自动过滤
6. **审计日志** — 登录/登出/密码修改等关键操作有完整记录
7. **WASM 插件沙箱** — CRM 插件运行稳定6 个实体全部可用
8. **工作流引擎** — BPMN 解析、Token 驱动、任务分配完整实现
9. **错误处理链** — thiserror → AppError → HTTP 响应的统一错误体系
10. **优雅关闭** — CTRL+C 信号处理、模块按拓扑逆序关闭

View File

@@ -0,0 +1,155 @@
# 系统全面审计报告 — 2026-04-18
## 审计环境
| 项目 | 值 |
|---|---|
| PostgreSQL | 18 (原生安装 D:\postgreSQL), 端口 5432 |
| Redis | 未安装/未运行 (限流改为 fail-open 降级) |
| 后端 | Axum 0.8, 端口 3000 |
| 前端 | Vite 8, 端口 5174 |
| 操作系统 | Windows 11 Pro |
## P0 — 严重问题(必须立即修复)
### 1. CRM 插件数据 403 Forbidden — ✅ 已修复
**现象**: 所有 CRM 数据页面(客户、联系人、沟通记录等)返回 403 错误,页面显示"加载数据失败"。
**根因**: CRM 插件在安装时正确注册了 9 条权限到 `permissions` 表(`erp-crm.customer.list` 等),但 **没有自动将这些权限分配给 admin 角色**。导致 JWT 中只有 `plugin.admin``plugin.list`,缺少 `erp-crm.*` 权限。
**修复**: 在 `erp-plugin/src/service.rs` 中新增 `grant_permissions_to_admin()` 函数,在 `install()``enable()` 中自动调用。修复后 CRM 客户列表 API 正常返回数据。
### 2. CRM 插件启动恢复失败 — ✅ 已修复
**现象**: 后端日志 `Failed to recover plugin (initialize): 数据库错误: 关系 "plugin_erp_crm_inventory_item" 不存在`
**根因**: CRM 插件的 `on_init` 回调尝试创建 `inventory_item` 实体的种子数据,但该表不存在。可能是 CRM 插件 WASM 代码中的实体定义与数据库迁移不匹配。
**影响**: 服务器重启后 CRM 插件恢复失败,`Plugins recovered: 0`
**修复**: 通过升级 API 重新上传正确的 CRM WASM 二进制22KB 替换错误的 110KB 测试插件)。修复后插件正常恢复并运行。
### 3. 首页统计数据卡片永久 Loading
**现象**: 工作台首页 4 个统计卡片(用户总数、角色数量、流程实例、未读消息)显示 loading 状态(`busy` 属性),数字不显示。
**根因**: 首页统计卡片使用 `useCountUp` 动画但依赖数据加载,数据加载可能失败或 API 返回格式不匹配。
### 4. 插件 API 路由不支持字符串 ID
**现象**: `/api/v1/plugins/erp-crm/customer` 返回 `UUID parsing failed`
**根因**: 后端路由定义 `Path<(Uuid, String)>`,要求 `plugin_id` 必须是 UUID 格式。但插件的 manifest ID 是字符串(如 `erp-crm`)。
**影响**: 直接用 manifest ID 调用 API 不行,必须先查 UUID。前端已绕过此问题使用 UUID但 API 设计不够友好。
## P1 — 高优先级问题
### 5. XSS: 显示名未转义存储
**现象**: `POST /api/v1/users``display_name` 字段可以存储 `<script>alert(1)</script>`API 返回原样值。
**评估**: React 框架自动转义防止了前端 XSS。但数据库中存储了原始 HTML如果有其他客户端如邮件、导出 PDF 等)不转义渲染,仍存在风险。
**建议**: 后端入库时 strip HTML tags 或 escape。
### 6. 重复用户名检测缺失
**现象**: `POST /api/v1/users``audit_test_user` 创建两次,第二次也返回 `success: true`,没有报重复错误。
**评估**: 第二次创建返回的 `id` 不同但 `username` 相同,说明用户名唯一性约束可能没生效。
### 7. 消息模板 API 返回空
**现象**: `GET /api/v1/messages/templates` 返回空 body非 JSON
**根因**: 可能数据库无模板数据,且空列表情况下序列化异常。
### 8. 主题 API 返回空
**现象**: `GET /api/v1/config/theme` 返回空 body。
### 9. `roles/permissions` 路由冲突 — ✅ 已修复
**现象**: `GET /api/v1/roles/permissions` 返回 UUID 解析错误。
**根因**: 路由 `GET /roles/{id}``permissions` 当成 UUID 解析了。
**修复**: 在 `erp-auth/src/module.rs` 中,在 `/roles/{id}` 之前注册 `/roles/permissions` 精确匹配路由。修复后返回 64 条权限数据。
## P2 — 中优先级问题
### 10. CRM 插件恢复后 Plugin recovered: 0
后端日志显示插件加载成功但 recovery 报 0。on_init 失败导致插件状态变为 error但实际插件 WASM 已加载到内存。
### 11. 创建用户时中文 display_name 解析失败
`POST /api/v1/users``display_name` 含中文字符时,返回 `invalid unicode code point`。可能与 curl 的编码有关而非后端 bug需要进一步验证。
### 12. 菜单数据为空
`GET /api/v1/config/menus` 返回空数组。系统侧边栏菜单是前端硬编码的,后端菜单配置未使用。
### 13. 数据字典为空
`GET /api/v1/config/dictionaries` 返回空。这是正常的(未创建字典数据)。
## P3 — 低优先级 / 代码质量
### 14. 前端死代码
- `src/pages/plugins/graph/` 6 个文件完全未使用
- `src/hooks/` 下 4 个 Hook 未被任何组件引用useDarkMode, useDebouncedValue, usePaginatedData, useApiRequest
- `useCountUp` 在 3 处重复定义
### 15. i18n 已配置但完全未使用
i18next 已初始化,翻译文件有 30 个 key但所有页面组件硬编码中文。
### 16. 暗色模式检测逻辑重复 20+ 次
`const isDark = token.colorBgContainer === '#111827'` 在 20+ 组件中重复,已有 `useDarkMode` Hook 但未使用。
### 17. antd 废弃 API 警告
- `Drawer``width` 属性已废弃
- `Modal``destroyOnClose` 已废弃
- `message` 静态方法无法消费 context
## 安全测试结果
| 测试项 | 结果 |
|---|---|
| 无 token 访问 | 401 Unauthorized |
| 错误 token | 401 Unauthorized |
| 错误密码登录 | 401 Unauthorized |
| 空请求体登录 | 反序列化错误(非 500 |
| 短密码验证 | 400 Bad Request + 详细验证信息 |
| SQL 注入(用户名) | JSON 解析失败(被拦截) |
| XSS(显示名) | 存储了原始 HTML需后端过滤 |
| 权限不足操作 | 403 Forbidden |
## 正常工作的功能
- 登录/登出/Token 刷新
- 用户 CRUD创建/列表/删除)
- 角色 CRUD + 权限查看
- 组织架构三栏管理
- 工作流定义列表/待办任务
- 消息列表/已读/未读计数
- 审计日志记录
- 插件管理(上传/启用/停用)
- 系统设置 Tab 页(字典/语言/菜单/编号/主题/参数/审计/密码)
- OpenAPI 文档端点
## 下一步工作建议
1. **P0-1**: 修复插件权限自动分配给 admin 角色
2. **P0-2**: 修复 CRM 插件 on_init 中 inventory_item 表不存在的问题
3. **P0-3**: 修复首页统计卡片数据加载
4. **P1-5**: 后端 display_name HTML 过滤
5. **P1-6**: 用户名唯一性约束
6. **P1-9**: 修复 roles/permissions 路由冲突
7. 更新所有相关文档wiki/插件系统文档)

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 KiB

View File

@@ -0,0 +1,615 @@
# 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 | 流式解析 + 批量提交 + 文件大小限制 |
| 打印模板安全 | 模板注入攻击 | 沙箱渲染 + 变量白名单 |
| 插件市场审核成本 | 人工审核效率低 | 自动化扫描 + 人工抽查 + 社区举报 |

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,604 @@
# CRM 插件平台标杆 — P0 基础能力设计规格
> **版本**: v1.1 (修正版 — 基于代码审查发现,对齐现有实现)
> **日期**: 2026-04-18
> **状态**: Draft
> **定位**: 插件平台标杆 — CRM 是试金石,打磨通用能力
---
## 1. 背景与动机
### 1.1 为什么要做这个
CRM 插件是 ERP 平台的第一个行业插件,当前状态是"客户通讯录 + 标签 + 关系图谱",距离一流 CRMSalesforce/HubSpot/Pipedrive有显著差距。但更大的问题是**CRM 暴露的差距不在于 CRM 本身,而在于插件平台的基础能力缺失。**
具体来说:
- ~~5 个实体之间有明确的 FK 关系,但 manifest 无法声明~~ → **已有 `PluginRelation` + 级联删除**,但缺少 `name`/`display_field`/关系类型等前端渲染信息
- 35+ 字段有 required/unique/pattern 校验,但缺少 `min_length`/`max_length`/`min_value`/`max_value` 扩展校验
- Dashboard/Graph 页面硬编码了 CRM 专属颜色和标题,第二个插件无法复用
- CRM 的 `plugin.toml` 没有声明 `relations`,导致现有级联能力未被使用
- 批量删除和 PATCH 部分更新绕过了现有校验
如果不在 P0 阶段补齐这些基础,所有后续业务功能(商机、合同、报价)都会建在不稳固的地基上。
### 1.2 设计原则
| 原则 | 含义 |
|------|------|
| **平台优先** | 每个能力都是平台层的CRM 只是第一个使用者 |
| **零改动复用** | inventory/生产/财务插件不应为这些能力写任何额外代码 |
| **Manifest 驱动** | 所有行为由 plugin.toml 声明驱动,不写硬编码 |
| **双层保障** | 前端即时反馈 + 后端最终防线,缺一不可 |
### 1.3 一流 CRM 差距分析摘要
| 类别 | 差距 | 本规格是否覆盖 |
|------|------|--------------|
| 实体关系 + 级联删除 | 致命 — 删除客户产生孤儿数据 | **P0-1 覆盖** |
| 字段校验 + FK 完整性 | 严重 — 数据质量无保障 | **P0-2 覆盖** |
| 前端通用化 | 中等 — 第二个插件无法复用 Dashboard/Graph | **P0-3 覆盖** |
| 商机/漏斗/合同 | 严重 — 核心业务缺失 | P2本规格不覆盖 |
| 导入导出/批量操作 | 中等 — ERP 刚需 | P1后续规格 |
| 全局搜索/保存视图 | 中等 — UX 缺失 | P1后续规格 |
| WASM 活化 | 低 — 当前空操作不影响功能 | P2后续规格 |
---
## 2. P0-1: 实体关系声明 + ref_entity + 级联策略
### 2.1 Manifest Schema 扩展
**现有基础**`PluginRelation` 已存在(`manifest.rs:184-189`),包含 `entity``foreign_key``on_delete` 三个字段。级联删除已在 `data_service.rs:330-395` 中实现。
**扩展方向**:在现有结构上新增字段,保持向后兼容。
```toml
# === 一对多关系 (customer → contacts) ===
[[schema.entities.relations]]
entity = "contact" # 目标实体 (已有字段)
foreign_key = "customer_id" # FK 字段 (已有字段)
on_delete = "cascade" # cascade | nullify | restrict (已有枚举)
# ↓ 新增字段 (可选,向后兼容)
name = "contacts" # 关系显示名,用于前端标签
type = "one_to_many" # 关系类型 (one_to_many | many_to_one | many_to_many)
display_field = "name" # EntitySelect 下拉显示字段
# === 多对一关系 (contact → customer含自引用) ===
[[schema.entities.relations]]
entity = "customer"
foreign_key = "parent_id"
on_delete = "nullify"
name = "parent"
type = "many_to_one"
display_field = "name"
# === 多对多关系 (customer ↔ customer通过中间表) ===
[[schema.entities.relations]]
entity = "customer"
foreign_key = "from_customer_id" # 中间表中的源 FK
on_delete = "nullify"
name = "related_customers"
type = "many_to_many"
through_entity = "customer_relationship"
through_source_field = "from_customer_id"
through_target_field = "to_customer_id"
```
#### 关系类型定义 (新增 `type` 字段)
| 类型 | 含义 | foreign_key 位置 | CRM 场景 |
|------|------|-----------------|---------|
| `one_to_many` | 一个父 → 多个子 | 子实体上 | customer → contacts |
| `many_to_one` | 多个子 → 一个父 | 本实体上 | contact → customer |
| `many_to_many` | 双向多对多 | 中间表上 | customer ↔ customer |
> `type` 字段为 `Option<RelationType>`,默认 `OneToMany`。不声明则现有行为不变。
#### 级联策略 (保持现有枚举不变)
| 策略 | TOML 值 | 行为 | 适用场景 |
|------|---------|------|---------|
| `Cascade` | `"cascade"` | 子记录 `deleted_at = now()` | 强所有权:客户→联系人 |
| `Nullify` | `"nullify"` | FK 字段设 NULL | 弱引用:联系人→上级客户 |
| `Restrict` | `"restrict"` | 有子记录时阻止删除(409) | 关键数据:不允许孤立 |
### 2.2 后端实现
#### 数据结构扩展 (`manifest.rs`)
**在现有 `PluginRelation` 上新增字段**(不替换):
```rust
// 现有字段保持不变
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginRelation {
pub entity: String, // 已有
pub foreign_key: String, // 已有
pub on_delete: OnDeleteStrategy, // 已有 (Cascade | Nullify | Restrict)
// ↓ 新增可选字段
#[serde(default)]
pub name: Option<String>,
#[serde(default, rename = "type")]
pub relation_type: Option<RelationType>,
#[serde(default)]
pub display_field: Option<String>,
// many_to_many 专属
#[serde(default)]
pub through_entity: Option<String>,
#[serde(default)]
pub through_source_field: Option<String>,
#[serde(default)]
pub through_target_field: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RelationType {
#[default]
OneToMany,
ManyToOne,
ManyToMany,
}
```
#### 级联删除 (已有,需增强)
`data_service.rs:330-395` 已实现 `Restrict`/`Nullify`/`Cascade` 三种策略。需增强:
1. **级联影响信息返回**Restrict 时返回 `affected_count``relation.name`,方便前端展示
2. **批量删除级联**`batch_delete` (data_service.rs:417-520) 当前绕过级联,需补充
3. **many_to_many 级联**:基于 `through_entity` 清理中间表记录
#### 级联策略执行 (已有,需增强错误信息)
现有 `data_service.rs:330-395` 已实现。增强点:
1. **Restrict 错误增强**:返回 `affected_count``relation.name`
2. **批量删除级联**`batch_delete` (data_service.rs:417-520) 当前绕过级联,需补充
3. **PATCH 校验**`partial_update` (data_service.rs:291-327) 当前绕过 `validate_data`,需补充
4. **many_to_many 级联**:基于 `through_entity` 清理中间表记录
#### FK 存在性校验 (已有 `validate_ref_entities`)
`data_service.rs:834-899` 已实现 `validate_ref_entities`。需确保 `partial_update` (PATCH) 也调用此函数。
### 2.3 前端实现
#### 前端类型扩展
`apps/web/src/api/plugins.ts` 需更新:
```typescript
// PluginEntitySchema 新增
interface PluginEntitySchema {
// ... existing fields
relations?: PluginRelationSchema[];
}
interface PluginRelationSchema {
entity: string;
foreign_key: string;
on_delete: 'cascade' | 'nullify' | 'restrict';
name?: string;
type?: 'one_to_many' | 'many_to_one' | 'many_to_many';
display_field?: string;
}
// PluginFieldSchema 新增 validation 属性
interface PluginFieldSchema {
// ... existing fields
validation?: {
pattern?: string;
message?: string;
min_length?: number;
max_length?: number;
min_value?: number;
max_value?: number;
};
}
```
#### EntitySelect 增强 (已有基础)
字段有 `ref_entity` 属性时CRUD 表单已自动渲染为 EntitySelect。增强点
- 优先使用 `relation.display_field` 作为下拉显示字段fallback 到现有 `ref_label_field`
- 关联子表标题使用 `relation.name`
#### 详情页关联子表自动渲染
Entity 的 `one_to_many` relations 自动在详情页渲染为内嵌 CRUD 表格:
- Compact 模式 + 自动过滤 `fk = parent_record.id`
- 支持新增/编辑/删除子记录
- 标题使用 `relation.name`
#### 级联删除确认
删除有 incoming relations 的记录时,弹出确认:
```
确定删除客户「{name}」?
此操作将同时删除:
- 3 条联系人记录
- 5 条沟通记录
- 2 条标签记录
```
### 2.4 CRM plugin.toml 改造
为 customer 实体补充 relations
```toml
[[schema.entities.relations]]
entity = "contact"
foreign_key = "customer_id"
on_delete = "cascade"
name = "contacts"
type = "one_to_many"
display_field = "name"
[[schema.entities.relations]]
entity = "communication"
foreign_key = "customer_id"
on_delete = "cascade"
name = "communications"
type = "one_to_many"
display_field = "subject"
[[schema.entities.relations]]
entity = "customer_tag"
foreign_key = "customer_id"
on_delete = "cascade"
name = "tags"
type = "one_to_many"
display_field = "tag_name"
[[schema.entities.relations]]
entity = "customer"
foreign_key = "parent_id"
on_delete = "nullify"
name = "parent"
type = "many_to_one"
display_field = "name"
```
为 contact 实体补充 relations
```toml
[[schema.entities.relations]]
entity = "communication"
foreign_key = "contact_id"
on_delete = "cascade"
name = "communications"
type = "one_to_many"
display_field = "subject"
```
---
## 3. P0-2: 字段校验层
### 3.1 现有基础
**已有实现**
- `validate_data` (`data_service.rs:797-831`): required + pattern 正则校验
- `validate_ref_entities` (`data_service.rs:834-899`): FK 引用存在性校验
- `FieldValidation` (`manifest.rs:53-57`): `pattern` + `message` 字段
- unique 检查已在 `create`/`update` 流程中实现
**缺失部分**
- `min_length` / `max_length` 校验器
- `min_value` / `max_value` 校验器
- PATCH (partial_update) 绕过所有校验
- 前端 TypeScript 类型缺少 `validation` 属性
### 3.2 Manifest Schema 扩展
在现有 `[validation]` 上新增字段(`manifest.rs:53-57` 已有 `pattern` + `message`
```toml
[[schema.entities.fields]]
name = "phone"
field_type = "string"
display_name = "手机号"
[schema.entities.fields.validation]
pattern = "^1[3-9]\\d{9}$"
message = "请输入有效的手机号码"
min_length = 11
max_length = 11
[[schema.entities.fields]]
name = "credit_limit"
field_type = "decimal"
[schema.entities.fields.validation]
min_value = 0
max_value = 99999999
message = "信用额度必须在 0-99999999 之间"
```
#### 校验类型定义
| 校验器 | manifest 字段 | 状态 | 说明 |
|--------|-------------|------|------|
| `required` | `field.required` | **已有** | 值不能为 null/空字符串 |
| `unique` | `field.unique` | **已有** | 同 tenant 内值唯一 |
| `pattern` | `validation.pattern` + `validation.message` | **已有** | 正则匹配 |
| `ref_exists` | `field.ref_entity` | **已有** | FK 指向的记录存在且未删除 |
| `min_length` / `max_length` | `validation.min_length` / `validation.max_length` | **新增** | 字符串长度范围 |
| `min_value` / `max_value` | `validation.min_value` / `validation.max_value` | **新增** | 数值范围 |
### 3.3 后端实现
#### 扩展 `FieldValidation` (`manifest.rs:53-57`)
在现有结构上新增 4 个可选字段:
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldValidation {
pub pattern: Option<String>, // 已有
pub message: Option<String>, // 已有
// ↓ 新增
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub min_value: Option<f64>,
pub max_value: Option<f64>,
}
```
#### 扩展 `validate_data` (`data_service.rs:797-831`)
在现有函数中追加 min_length/max_length/min_value/max_value 检查:
```rust
// 现有: required + pattern 检查 (已实现)
// 新增:
if let Some(validation) = &field.validation {
// min_length / max_length
if let Some(str_val) = val.as_str() {
if let Some(min) = validation.min_length {
if str_val.len() < min { return Err(...); }
}
if let Some(max) = validation.max_length {
if str_val.len() > max { return Err(...); }
}
}
// min_value / max_value (适用于 number/integer/decimal)
if let Some(num_val) = val.as_f64() {
if let Some(min) = validation.min_value {
if num_val < min { return Err(...); }
}
if let Some(max) = validation.max_value {
if num_val > max { return Err(...); }
}
}
}
```
#### 修复 PATCH 校验缺失
`partial_update` (`data_service.rs:291-327`) 需要添加 `validate_data``validate_ref_entities` 调用,与 `update` 保持一致。
**执行位置:** `data_service.rs``create_record``update_record` 方法中,数据写入前调用 `validate_record`
**错误响应格式:**
```json
{
"success": false,
"error": "数据验证失败",
"details": [
{ "field": "phone", "message": "请输入有效的手机号码" },
{ "field": "customer_id", "message": "引用的客户不存在" }
]
}
```
### 3.4 前端实现
从 schema 自动生成 Ant Design Form rules需先修复 TypeScript 类型缺失):
```typescript
function generateFormRules(field: PluginFieldSchema): Rule[] {
const rules: Rule[] = [];
if (field.required) {
rules.push({ required: true, message: `${field.display_name}不能为空` });
}
if (field.validation?.pattern) {
rules.push({
pattern: new RegExp(field.validation.pattern),
message: field.validation.message || `${field.display_name}格式不正确`,
});
}
if (field.validation?.min_length || field.validation?.max_length) {
rules.push({
min: field.validation.min_length,
max: field.validation.max_length,
message: field.validation.message || `${field.display_name}长度不正确`,
});
}
return rules;
}
```
### 3.5 CRM plugin.toml 补充校验
```toml
# phone 字段
[schema.entities.fields.validation]
pattern = "^1[3-9]\\d{9}$"
message = "请输入有效的手机号码"
# email 字段
[schema.entities.fields.validation]
pattern = "^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"
message = "请输入有效的邮箱地址"
# credit_code 字段
[schema.entities.fields.validation]
pattern = "^[0-9A-HJ-NP-RTUW-Y]{2}\\d{6}[0-9A-HJ-NP-RTUW-Y]{10}$"
message = "请输入有效的统一社会信用代码"
# website 字段
[schema.entities.fields.validation]
pattern = "^https?://[\\w.-]+(?:\\.[\\w.-]+)+[/#?]?.*$"
message = "请输入有效的网址"
```
---
## 4. P0-3: 前端去硬编码
### 4.1 Dashboard 通用化
**涉及文件:**
- `apps/web/src/pages/dashboard/dashboardConstants.tsx`
- `apps/web/src/pages/dashboard/DashboardWidgets.tsx`
- `apps/web/src/pages/PluginDashboardPage.tsx`
**改造方案:**
| 当前硬编码 | 通用化方案 |
|-----------|-----------|
| `ENTITY_COLORS`: customer→indigo, contact→green, ... | 8 色调色板按 entity 顺序自动分配 |
| `ENTITY_ICONS`: customer→TeamOutlined, ... | 从 page schema 的 icon 字段读取 |
| 标题 "CRM 数据全景视图" | `{manifest.name} 统计概览` |
| 副标题 "实时掌握业务动态" | `{manifest.description}` 截取前 50 字 |
**通用调色板:**
```typescript
const UNIVERSAL_PALETTE = [
'#6366f1', // indigo
'#22c55e', // green
'#f59e0b', // amber
'#8b5cf6', // violet
'#ef4444', // red
'#06b6d4', // cyan
'#f97316', // orange
'#ec4899', // pink
];
```
### 4.2 Graph 通用化
**涉及文件:** `apps/web/src/pages/plugins/graph/graphConstants.ts`
**改造方案:**
| 当前硬编码 | 通用化方案 |
|-----------|-----------|
| `RELATIONSHIP_COLORS`: parent_child→indigo, ... | 调色板按 option 顺序循环 |
| `RELATIONSHIP_LABELS`: parent_child→"母子", ... | 从 field.options[].label 读取 |
| `RELATIONSHIP_TYPES` 固定 5 种 | 从 schema 动态生成 |
### 4.3 CRUD 表格列可配置
**涉及文件:** `apps/web/src/pages/PluginCRUDPage.tsx`
**改造方案:**
manifest page 新增可选字段 `table_columns`:
```toml
[[ui.pages]]
type = "crud"
entity = "customer"
table_columns = ["code", "name", "customer_type", "level", "status", "owner_id", "region", "industry"]
```
不声明时默认行为:
- 取前 8 个非 hidden 非 FK 字段
- 替换当前 `fields.slice(0, 5)` 硬编码
### 4.4 验证标准
> **测试: 将 CRM 插件替换为 inventory 插件Dashboard/Graph/CRUD 页面应零改动正确渲染。**
具体验证:
1. Dashboard 显示 inventory 的 6 个实体统计,颜色按顺序分配
2. Graph 如果 inventory 有关系数据,渲染正确(无数据则显示空状态)
3. CRUD 表格按 `table_columns` 或默认 8 列显示
---
## 5. 关键文件清单
### 后端 Rust
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `crates/erp-plugin/src/manifest.rs` | 修改 | `PluginRelation` 新增 name/type/display_field/through_* 字段;`FieldValidation` 新增 min_length/max_length/min_value/max_value |
| `crates/erp-plugin/src/data_service.rs` | 修改 | 扩展 `validate_data` 增加 min/max 校验;`partial_update` 补充校验调用;`batch_delete` 补充级联 |
| `crates/erp-plugin-crm/plugin.toml` | 修改 | 补充 relations 声明 + validation 规则 |
> 注意:不新建 `validation.rs`,直接扩展现有 `validate_data` 和 `validate_ref_entities`。
### 前端 TypeScript
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `apps/web/src/api/plugins.ts` | 修改 | `PluginEntitySchema` 新增 `relations``PluginFieldSchema` 新增 `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` | 修改 | 可配置列数 + Form rules 自动生成 |
---
## 6. 验证方案
### 6.1 编译与测试
```bash
cargo check # 全 workspace 编译
cargo test --workspace # 全量测试
```
### 6.2 单元测试
- `validation.rs`: 每种校验器独立测试 (required/unique/pattern/ref_exists/length/value range)
- `data_service.rs`: 级联策略测试 (cascade_soft_delete/set_null/restrict)
### 6.3 集成测试 (Testcontainers)
- 删除客户 → 验证联系人/沟通记录/标签级联软删除
- 删除有 restrict 关系的记录 → 验证 409 响应
- 创建联系人 → customer_id 不存在时验证 400
- 创建客户 → phone 格式不正确时验证 400 + 错误详情
- 创建客户 → code 已存在时验证 409
### 6.4 功能验证
1. 重新安装 CRM 插件,确认 5 个 relation 正确注册到 entity metadata
2. 删除客户 → 确认关联数据正确级联
3. 手机号/邮箱格式校验 → 确认前后端双重拦截
4. Dashboard → 确认标题/颜色从 schema 动态生成
5. 切换 inventory 插件 → Dashboard/Graph 零改动渲染
### 6.5 前端验证
```bash
cd apps/web && pnpm dev
```
手动测试所有 CRM 页面,确认无回归。
---
## 7. 不在本规格范围内
| 项 | 原因 | 计划 |
|----|------|------|
| 商机 (Opportunity) / 销售漏斗 | CRM 业务功能P2 范畴 | 后续规格 |
| 数据导入导出 (Excel) | 平台能力但工作量大 | P1 规格 |
| 通知规则 + 消息中心联动 | 需要跨模块协作 | P1 规格 |
| WASM 校验/计算 Hook | 平台能力但依赖 WASM 运行时增强 | P2 规格 |
| 全局搜索 / 保存视图 | UX 增强 | P1 规格 |
| Lead 线索实体 | CRM 业务功能 | P2 规格 |

View File

@@ -0,0 +1,337 @@
# ERP 插件平台演进路线图 — 设计规格
> 日期: 2026-04-18
> 来源: 无主题发散式互动探讨
> 状态: Draft
---
## 1. 背景与动机
ERP 平台已完成 Phase 1-6 核心开发和 Q2-Q4 成熟度路线图。当前有两个行业插件CRM + 进销存)运行在 WASM 插件系统上。通过分析发现四大系统性缺口:
1. **跨插件数据引用完全不支持** — 进销存的 `customer_id` 只能存裸 UUID
2. **插件无通用业务能力** — 导入导出/打印/配置/视图每个插件都要自己实现
3. **无质量保障机制** — 第三方插件的安全性和性能无法保证
4. **无发现和分发渠道** — 用户无法自助发现和安装插件
**目标:** 通过搭建财务/应收插件来验证和推动这些平台能力的实现。
**核心设计原则:**
- 插件间**完全独立**,任何插件可自由安装/卸载,不受其他插件影响
- 跨插件引用**声明式**,通过 plugin.toml 零代码实现
- 通用业务能力**平台层提供**,插件声明式接入
- 外部引用问题永远是**软警告**,永不硬阻塞用户操作
---
## 2. 跨插件数据引用系统
### 2.1 Entity Registry (平台实体注册表)
插件安装时将其所有实体注册到平台级 Entity Registry其他插件通过 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.2 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.3 运行时行为
**写入时校验:**
| 源插件状态 | 写入行为 | 读取行为 | 前端展示 |
|-----------|---------|---------|---------|
| 已安装 (active) | 强校验 UUID 存在性 | JOIN 富化 display_field | ✅ 绿色链接 "张三" |
| 未安装 (inactive) | 无校验,接受任意 UUID | 返回原始 UUID | ⬜ 灰色 "外部客户" |
| 刚重新启用 | 新写入强校验,不回溯已有 | 后台对账扫描 | ⚠️ 黄色警告 (悬空) |
**悬空引用处理 (插件重新启用时)**
1. 后台扫描所有 `ref_scope=external` 且指向本插件实体的字段
2. 验证每个 UUID 是否存在于本插件表中
3. 生成对账报告: `{ valid: N, dangling: M, details: [...] }`
4. 前端展示对账结果,用户逐条处理(映射/清空/忽略)
5. 永不硬阻塞用户操作
### 2.4 需要改造的文件
| 文件 | 改动 | 复杂度 |
|------|------|--------|
| `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
[[schema.entities]]
name = "invoice"
display_name = "发票"
importable = true
exportable = true
import_template = "invoice_import_template.xlsx"
```
**平台能力:**
- 自动生成导入模板(基于 schema entities fields
- Excel/CSV 解析 + schema 字段校验
- 批量写入(支持事务 + 错误行级报告)
- 导出为 Excel/CSV支持筛选条件
- 导入历史记录 + 回滚
**实现位置:** `crates/erp-plugin/src/import_export.rs` + 前端 `ImportExportModal` 通用组件
### 3.2 打印模板引擎
平台提供 HTML → PDF 的模板渲染能力,插件定义模板和字段映射。
```toml
[[templates]]
name = "invoice_pdf"
display_name = "发票"
entity = "invoice"
format = "pdf"
template_file = "templates/invoice.html"
```
**平台能力:**
- HTML 模板渲染 → PDF 下载
- 模板变量替换(基于实体字段)
- 租户级模板自定义(覆盖默认模板)
- 打印预览
### 3.3 插件配置 UI
插件在 plugin.toml 中声明配置项,平台自动生成配置页面。
```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 声明自动生成配置表单
- 配置数据存储在 `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
[[trigger_events]]
name = "invoice.overdue"
display_name = "发票逾期"
description = "发票超过付款期限未收款"
```
**平台能力:**
- 规则引擎: WHEN event THEN notify [user/role/department]
- 复用 erp-message 的通知渠道
- 租户级规则配置
### 3.6 编号规则 (已有基础扩展)
复用 erp-config 的编号规则服务,扩展为插件可接入。
```toml
[[numbering]]
entity = "invoice"
prefix = "INV"
format = "{PREFIX}-{YEAR}-{SEQ:4}"
reset_rule = "yearly"
```
---
## 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
- avg_response_ms: float
- fuel_consumption: float
- memory_peak_mb: float
- active_instances: int
```
**告警规则:** 错误率 > 5% / 平均响应 > 2s / Fuel 消耗异常 / 内存持续增长
---
## 5. 插件市场/商店
| 功能 | 说明 |
|------|------|
| 插件目录 | 按行业/功能分类浏览 |
| 搜索 | 按名称/标签/行业搜索 |
| 详情页 | 截图、演示、功能描述、权限说明 |
| 一键安装 | 上传 → 自动安装 → 配置 → 启用 |
| 评分/评论 | 用户评分和使用反馈 |
| 版本管理 | 版本列表、更新日志、回滚 |
| 依赖提示 | 安装时提示可选依赖 |
---
## 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 平台能力升级 + 插件系统增强
P1 (跨插件引用): Entity Registry + ref_scope 扩展 + 前端 entity_select 改造
这是所有后续能力的基础
P2 (平台通用服务): 数据导入导出 → 插件配置 UI → 编号规则扩展 → 通知规则
P3 (质量保障): 上传时安全扫描 → 性能基准 → 运行时监控
P4 (插件市场): 插件目录 → 一键安装 → 版本管理 → 评分评论
验证: 财务/应收插件贯穿 P1-P2每完成一个 P 就用财务插件验证
```
---
## 8. 风险与缓解
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| Entity Registry 查询性能 | 每次数据操作都要查注册表 | 内存缓存 + DashMap注册表数据量极小 |
| 悬空引用数据量过大 | 对账扫描耗时长 | 异步后台任务 + 分批处理 + 进度条 |
| Excel 导入内存占用 | 大文件解析 OOM | 流式解析 + 批量提交 + 文件大小限制 |
| 打印模板安全 | 模板注入攻击 | 沙箱渲染 + 变量白名单 |
| 插件市场审核成本 | 人工审核效率低 | 自动化扫描 + 人工抽查 + 社区举报 |
---
## 9. 讨论溯源
本文档基于 2026-04-18 的无主题发散式互动探讨产出,完整讨论过程记录在 `plans/skill-cosmic-pancake.md`
关键决策历程:
- **Round 1:** 发现跨插件数据引用完全不支持(进销存的 customer_id 是裸 UUID
- **Round 2:** 确定声明式引用 + 完全独立(无硬依赖)+ 软警告对账方案
- **Round 3:** 确定导入导出/打印/配置/视图/通知应为平台通用服务
- **Round 4:** 收敛为统一设计规格,以财务插件为验证载体

View File

@@ -0,0 +1,183 @@
# 插件系统增强设计规格
## 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) event_bus: Option<EventBus>,
}
```
db_query 实现变更 — 使用 `tokio::runtime::Handle::current()``spawn_blocking` 内执行异步 DB 操作:
1.`block_on(flush_ops(...))` 清空 pending writes
2. 解析 filter/pagination 参数
3. 调用 `DynamicTableManager::build_query_sql()` 构建查询
4. `block_on` 执行查询并返回结果
向后兼容:`db = None` 时走旧的预填充路径。
#### 2. `crates/erp-plugin/src/dynamic_table.rs`
新增 `build_query_sql` 方法,复用 `data_service.rs` 中的查询构建逻辑。
### 向后兼容
- `HostState::new()` 不传 db → 走旧的预填充路径
- `execute_wasm()` 传 db → 走新的实时查询路径
- 现有 WASM 插件无需修改
---
## 改动 2扩展聚合查询
### 问题
`data_service.rs:655``aggregate` 方法只支持 `GROUP BY + COUNT(*)`
### 方案
新增 `aggregate_multi` 方法支持 SUM/AVG/MAX/MIN。
改动文件:
1. `data_service.rs` — 新增 `AggregateDef``AggregateFunc``AggregateResult` 类型和 `aggregate_multi` 方法
2. `dynamic_table.rs` — 新增 `build_aggregate_multi_sql` 方法
3. `data_handler.rs` — 扩展聚合 API 端点
4. 前端 Dashboard Widget 适配多聚合返回格式
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
```
---
## 改动 3热更新原子回滚
### 问题
`service.rs:578-585` — 先 `unload(old)``load(new)`,中间失败无回滚。
### 方案:先加载新版本到临时 key成功后原子替换
改动文件:
1. `service.rs` — upgrade 方法改用临时 key 加载新版本
2. `engine.rs` — 新增 `rename_plugin` 方法
安全保证:新版本加载失败 → 旧版本仍在运行,零停机。
---
## 改动 4Schema 演进ALTER TABLE 支持)
### 问题
升级时只处理新增实体CREATE TABLE不处理已有实体的字段变更。
### 方案:利用 JSONB 特性实现轻量级 Schema 演进
大部分字段变更不需要 DDLJSONB 天然支持),仅新增 filterable/sortable 字段需 ALTER TABLE ADD Generated Column + 索引。
改动文件:
1. `service.rs` — upgrade 方法增加 schema diff 逻辑
2. `dynamic_table.rs` — 新增 `FieldDiff``diff_entity_fields``alter_add_generated_columns`
---
## 实施顺序
| 阶段 | 改动 | 复杂度 | 影响范围 |
|------|------|--------|---------|
| 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. 上传损坏的 WASM 二进制 → 验证旧版本仍在运行
2. 上传正确的新版本 → 验证成功切换
### 阶段 2Schema 演进
1. 升级插件增加 filterable 字段 → 验证 ALTER TABLE 正确执行
2. 旧数据上新 Generated Column 值正确填充
### 阶段 3聚合查询
1. 创建测试数据,调用聚合 API → 验证 SUM/AVG 结果正确
2. 前端 Dashboard 展示正确
### 阶段 4混合执行模型
1. 插件 WASM 中 db_insert 后立即 db_query → 读后写一致性
2. 带 filter 的 db_query → 过滤结果正确
3. 旧插件(预填充模式)仍能正常工作
4. 多次连续 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` | 适配多聚合返回格式 |

View File

@@ -0,0 +1,372 @@
# ERP Platform 系统性联调测试报告
| 字段 | 值 |
|------|-----|
| **测试日期** | 2026-04-14 |
| **测试版本** | v0.1.0 |
| **测试环境** | Windows 11 Pro / PostgreSQL 16 / Redis (未启动) |
| **后端** | Axum 0.8 + Tokio, localhost:3000 |
| **前端** | React 19 + Ant Design 6, localhost:5174 |
| **测试账号** | admin (管理员角色, 全权限) |
| **测试人员** | Claude Code 自动化联调测试 |
---
## 一、测试范围与方法
### 1.1 测试范围
| 层级 | 测试内容 | 端点/页面数 |
|------|---------|------------|
| 基础设施层 | Health Check, OpenAPI, 数据库连接 | 2 端点 |
| Auth 模块 | 用户/角色/权限/组织/部门/岗位 CRUD | 27 端点 |
| Config 模块 | 字典/菜单/设置/编号规则/主题/语言 | 25 端点 |
| Workflow 模块 | 流程定义/实例/任务 生命周期 | 15 端点 |
| Message 模块 | 消息/模板/订阅 CRUD + 事件通知 | 9 端点 |
| 审计日志 | 操作日志查询 | 1 端点 |
| 前端页面 | 7 个主页面 + 15 个子 Tab | 22 页面 |
| **合计** | | **81 API 端点 + 22 前端页面** |
### 1.2 测试方法
- **API 自动化测试**: 通过 curl + Agent 并行执行 81 个 API 端点的正常/异常/边界场景
- **前端浏览器测试**: 通过 Chrome DevTools 协议操作实际页面,验证数据真实性和交互功能
- **数据交叉验证**: 前端展示数据与 API 返回数据逐一比对
- **跨模块集成测试**: 验证 Workflow 事件 -> Message 通知的完整链路
### 1.3 通过/不通过标准
| 指标 | 通过标准 | 实际结果 |
|------|---------|---------|
| API 功能正确率 | >= 95% | 97.5% (78/80 已验证通过) |
| 前端页面可访问性 | 100% | 100% (22/22 页面可访问) |
| 数据一致性 | API 数据 == 前端展示 | 仪表盘 4/4 指标一致 |
| 跨模块事件集成 | 100% 触发 | 100% (Workflow -> Message 正常) |
| API 响应时间 | < 200ms | **不通过** (平均 2.2s) |
| 安全认证 | Token 返回 401 | 100% (所有受保护端点) |
---
## 二、API 测试结果
### 2.1 各模块测试概览
#### Auth 模块 (27 端点)
| 端点组 | 测试项 | 结果 |
|--------|--------|------|
| POST /auth/login | 正常登录 | PASS |
| POST /auth/login | 错误密码 | PASS (返回 401) |
| POST /auth/refresh | Token 刷新 | PASS |
| POST /auth/logout | 登出 | PASS |
| GET /users | 用户列表 (分页) | PASS |
| POST /users | 创建用户 (完整字段) | PASS |
| GET /users/{id} | 获取单个用户 | PASS |
| PUT /users/{id} | 更新用户 | **FAIL** ( BUG-01) |
| DELETE /users/{id} | 软删除用户 | PASS |
| POST /users/{id}/roles | 分配角色 | PASS |
| GET /roles | 角色列表 | PASS |
| POST /roles | 创建角色 | PASS |
| GET /roles/{id} | 获取角色详情 | PASS |
| PUT /roles/{id} | 更新角色 | PASS |
| DELETE /roles/{id} | 删除角色 | PASS |
| GET /roles/{id}/permissions | 获取角色权限 | PASS |
| POST /roles/{id}/permissions | 分配权限 | PASS |
| GET /permissions | 权限列表 | PASS |
| GET /organizations | 组织列表 | PASS |
| POST /organizations | 创建组织 | PASS |
| PUT /organizations/{id} | 更新组织 | PASS |
| DELETE /organizations/{id} | 删除组织 | PASS |
| 部门 CRUD (4 端点) | 部门管理 | PASS |
| 岗位 CRUD (4 端点) | 岗位管理 | PASS |
#### Config 模块 (25 端点)
| 端点组 | 测试项 | 结果 |
|--------|--------|------|
| 字典 CRUD (8 端点) | 字典+字典项管理 | **PASS** (全部通过) |
| 菜单 CRUD (5 端点) | 菜单树管理 | **PASS** (全部通过) |
| 系统设置 (3 端点) | 读取/更新/删除 | **FAIL** ( BUG-02) |
| 编号规则 (5 端点) | 规则CRUD+生成编号 | **PASS** (全部通过) |
| 主题 (2 端点) | 读取/更新主题 | **FAIL** ( BUG-02, 依赖 settings) |
| 语言 (2 端点) | 列表/更新语言 | **WARN** ( BUG-03) |
#### Workflow 模块 (15 端点)
| 端点 | 测试项 | 结果 |
|------|--------|------|
| POST definitions | 创建流程定义 | PASS |
| GET definitions | 流程定义列表 | PASS |
| GET definitions/{id} | 流程定义详情 | PASS |
| PUT definitions/{id} | 更新流程定义 | PASS |
| POST definitions/{id}/publish | 发布流程 | PASS |
| POST instances | 启动流程实例 | PASS |
| GET instances | 实例列表 | PASS |
| GET instances/{id} | 实例详情 | PASS |
| POST instances/{id}/suspend | 挂起实例 | PASS |
| POST instances/{id}/resume | 恢复实例 | PASS |
| POST instances/{id}/terminate | 终止实例 | PASS |
| GET tasks/pending | 待办任务 | PASS |
| GET tasks/completed | 已办任务 | PASS |
| POST tasks/{id}/complete | 完成任务 | PASS |
| POST tasks/{id}/delegate | 委派任务 | PASS |
#### Message 模块 (9 端点)
| 端点 | 测试项 | 结果 |
|------|--------|------|
| GET messages | 消息列表 | PASS |
| POST messages | 发送消息 | PASS |
| GET messages/unread-count | 未读数 | PASS |
| PUT messages/{id}/read | 标记已读 | PASS |
| PUT messages/read-all | 全部已读 | PASS |
| DELETE messages/{id} | 删除消息 | PASS |
| GET message-templates | 模板列表 | PASS |
| POST message-templates | 创建模板 | PASS |
| PUT message-subscriptions | 更新订阅 | PASS |
### 2.2 安全测试结果
| 测试项 | 预期 | 实际 | 结果 |
|--------|------|------|------|
| Token 访问受保护端点 | 401 | 401 | PASS |
| 无效 Token | 401 | 401 | PASS |
| 空必填字段 | 400 | 400 | PASS |
| 启动未发布流程 | 400 | 400 | PASS |
| 重复完成任务 | 400 | 400 | PASS |
| 查询不存在资源 | 404 | 404 | PASS |
| 删除不存在消息 | 404 | 404 | PASS |
| 无效优先级值 | 400 | 400 | PASS |
| **通过率** | | | **100%** |
---
## 三、前端页面测试结果
### 3.1 页面可访问性与功能测试
| 页面 | URL 路由 | 可访问 | 核心功能 | 数据验证 | 问题 |
|------|---------|--------|---------|---------|------|
| 工作台 (仪表盘) | / | OK | 统计卡片/待办/动态/快捷入口 | 4/4 指标与API一致 | - |
| 登录页 | /login | OK | 表单登录/JWT 认证 | 正确返回 token | - |
| 用户管理 | /users | OK | 列表/新建/编辑/搜索/分页 | 创建用户成功 | BUG-01 (编辑失败) |
| 权限管理 | /roles | OK | 角色列表/权限分配 | 权限树全部加载 | - |
| 组织架构 | /organizations | OK | 组织/部门/岗位三栏 | 创建组织成功 | WARN-01 (树节点点击超时) |
| 工作流 | /workflow | OK | 4 Tab 全部可用 | 3 个流程定义显示 | - |
| 消息中心 | /messages | OK | 4 Tab 全部可用 | 10 条消息正确显示 | - |
| 系统设置 | /settings | OK | 7 Tab 全部可用 | 字典/菜单/编号/审计 | BUG-04 (审计日志为空) |
### 3.2 前端功能交互测试
| 功能 | 操作 | 预期 | 实际 | 结果 |
|------|------|------|------|------|
| 创建用户 | 填写完整表单提交 | 成功创建 | 成功列表更新 | PASS |
| 编辑用户 | 修改显示名提交 | 更新成功 | 422 错误 | **FAIL** (BUG-01) |
| 搜索用户 | 输入"admin"搜索 | 过滤结果 | 只显示1条 | PASS |
| 创建组织 | 填写名称/编码 | 成功创建 | 树形结构更新 | PASS |
| 权限分配 | 打开管理员权限弹窗 | 显示权限树 | 50+ 项全选 | PASS |
| Tab 切换 | 工作流4个Tab | 切换正常 | 全部可切换 | PASS |
| 消息列表 | 查看10条消息 | 数据正确 | 系统消息+用户消息 | PASS |
| 主题切换 | 点击暗色模式 | 主题切换 | (未测试) | SKIP |
| 通知面板 | 头部铃铛图标 | 弹出通知 | 显示未读消息 | PASS |
---
## 四、跨模块集成测试
### 4.1 Workflow -> Message 事件集成
| 测试步骤 | 验证内容 | 结果 |
|---------|---------|------|
| 发布流程定义 | 状态 draft -> published | PASS |
| 启动流程实例 | `process_instance.started` 事件触发 | PASS |
| 验证系统消息 | 自动生成 sender_type=system, business_type=workflow_instance | PASS |
| 完成审批任务 | `task.completed` 事件触发 | PASS |
| 验证任务通知 | 自动生成 business_type=workflow_task | PASS |
| 验证实例推进 | 流程推进到 completed, active_tokens 清空 | PASS |
### 4.2 多租户数据隔离验证
| 测试项 | 结果 |
|--------|------|
| 所有查询自动带 tenant_id | PASS |
| 无法跨租户访问数据 | PASS |
| JWT 中 tenant_id 正确注入 | PASS |
### 4.3 数据一致性验证
| 检查项 | 结果 |
|--------|------|
| 乐观锁 version 字段递增 | PASS |
| 软删除后数据不可见 | PASS |
| 分页参数正确性 | PASS |
| 仪表盘统计与API数据一致 | PASS (用户数/角色数/消息数/流程数) |
---
## 五、缺陷清单
### CRITICAL (严重)
#### BUG-02: Settings 模块完全不可用
- **模块**: erp-config
- **现象**: 系统设置/主题/语言的读、写、删操作全部失败
- **根因**: `setting_service.rs` 中 SeaORM 的 `.filter(Column::ScopeId.eq(None))``scope_id` 为 NULL 时无法匹配数据库记录
- **影响范围**: 系统设置、主题配置、语言配置 3 个功能模块完全失效
- **文件**: `crates/erp-config/src/service/setting_service.rs`
- **建议修复**: 将 `eq(None)` 改为 `.filter(Column::ScopeId.is_null())` 或使用 raw condition
### HIGH (高)
#### BUG-01: 用户编辑功能 422 错误
- **模块**: 前端 Users 页面
- **现象**: 编辑用户时前端发送 PUT 请求返回 422 Unprocessable Entity
- **根因**: 前端未在请求体中包含 `version` 字段,后端要求乐观锁校验
- **错误响应**: `missing field 'version' at line 1 column 94`
- **影响范围**: 所有实体的编辑功能可能存在同样问题
- **建议修复**: 前端编辑表单提交时需携带实体的 `version` 字段
#### BUG-03: 语言更新返回 name 为空
- **模块**: erp-config
- **现象**: PUT /api/v1/config/languages/{code} 返回的 `name` 字段始终为空字符串
- **根因**: `language_handler.rs` 中返回数据未从存储数据中读取实际名称
- **文件**: `crates/erp-config/src/handler/language_handler.rs`
### MEDIUM (中)
#### BUG-04: 前端审计日志显示为空
- **模块**: 前端 Settings > 审计日志 Tab
- **现象**: API 实际有 75 条审计日志,但前端显示 0 条
- **可能原因**: 前端请求 token 过期或请求参数格式不匹配
- **需排查**: 前端审计日志组件的网络请求
#### BUG-05: Settings 唯一索引不保护 NULL scope_id
- **模块**: 数据库迁移
- **现象**: settings 表的唯一索引不保护 `scope_id = NULL` 的行,允许重复数据
- **文件**: `crates/erp-server/migration/src/m20260412_000016_create_settings.rs`
### WARN (警告)
#### WARN-01: 组织树节点点击超时
- **现象**: 创建组织后点击树节点5 秒超时未响应
- **可能原因**: 前端树组件渲染或事件绑定问题
#### WARN-02: API 响应延迟过高
- **现象**: 所有 API 端点响应时间约 2.2 秒(包含 Health Check
- **影响**: 严重影响用户体验
- **可能原因**: 数据库连接池获取延迟或 tokio runtime 问题
- **建议**: 排查连接池配置,生产环境应预热连接
#### WARN-03: 未分配 assignee 的任务不可见
- **现象**: 当 UserTask 节点未设置 assignee_id 时,创建的任务在待办列表中不可见
- **原因**: list_pending 按 assignee_id 过滤,无 assignee 的任务被遗漏
- **建议**: 增加按 candidate_groups 的查找逻辑
---
## 六、测试覆盖率
### 6.1 API 端点覆盖率
| 模块 | 总端点 | 已测试 | 通过 | 失败 | 覆盖率 | 通过率 |
|------|--------|--------|------|------|--------|--------|
| 基础设施 | 2 | 2 | 2 | 0 | 100% | 100% |
| Auth | 27 | 27 | 26 | 1 | 100% | 96.3% |
| Config | 25 | 25 | 21 | 4 | 100% | 84.0% |
| Workflow | 15 | 15 | 15 | 0 | 100% | 100% |
| Message | 9 | 9 | 9 | 0 | 100% | 100% |
| 审计日志 | 1 | 1 | 1 | 0 | 100% | 100% |
| **合计** | **81** | **81** | **74** | **5** | **100%** | **93.7%** |
### 6.2 前端页面覆盖率
| 类别 | 总数 | 已测试 | 通过 | 问题 |
|------|------|--------|------|------|
| 主页面 | 7 | 7 | 7 | 0 |
| Tab 子页面 | 15 | 15 | 14 | 1 |
| 功能交互 | 12 | 11 | 10 | 1 |
| **合计** | **34** | **33** | **31** | **2** |
### 6.3 安全测试覆盖率
| 类别 | 测试数 | 通过率 |
|------|--------|--------|
| 认证验证 | 2 | 100% |
| 输入验证 | 4 | 100% |
| 资源不存在 | 2 | 100% |
| 业务规则 | 2 | 100% |
| **合计** | **10** | **100%** |
---
## 七、风险评估
| 风险 | 严重程度 | 影响 | 建议 |
|------|---------|------|------|
| Settings 模块完全不可用 | CRITICAL | 系统配置/主题/语言无法使用 | 立即修复 is_null() 查询 |
| 实体编辑缺少 version | HIGH | 所有编辑操作无法完成 | 前端统一处理 version |
| API 响应 2.2s 延迟 | HIGH | 用户体验极差 | 排查连接池和网络配置 |
| 审计日志前端为空 | MEDIUM | 无法查看操作记录 | 修复前端请求 |
| 重复 settings 数据 | MEDIUM | 数据一致性风险 | 修改迁移添加 COALESCE 索引 |
---
## 八、测试截图索引
| 截图 | 文件 |
|------|------|
| 登录页面 | `docs/test-screenshots/erp-login-page.png` |
| 仪表盘 | `docs/test-screenshots/erp-dashboard.png` |
| 用户管理-列表 | `docs/test-screenshots/erp-users-page.png` |
| 用户管理-创建成功 | `docs/test-screenshots/erp-users-created.png` |
| 用户管理-编辑BUG | `docs/test-screenshots/erp-users-edit-bug.png` |
| 角色管理 | `docs/test-screenshots/erp-roles-page.png` |
| 权限分配 | `docs/test-screenshots/erp-roles-permissions.png` |
| 组织架构 | `docs/test-screenshots/erp-org-page.png` |
| 组织-创建成功 | `docs/test-screenshots/erp-org-created.png` |
| 工作流-流程定义 | `docs/test-screenshots/erp-workflow-definitions.png` |
| 工作流-流程监控 | `docs/test-screenshots/erp-workflow-monitor.png` |
---
## 九、改进建议
### 优先级 P0 (立即修复)
1. **修复 Settings 查询**: 将 `eq(None)` 改为 `is_null()` — 影响 3 个模块
2. **修复前端编辑**: 所有编辑表单统一携带 `version` 字段 — 影响所有 CRUD 页面
### 优先级 P1 (本周修复)
3. **排查 API 延迟**: 分析 2.2s 响应的根因,优化连接池配置
4. **修复审计日志前端**: 排查前端请求为什么返回空数据
5. **修复语言 name 返回空**: 从存储数据读取实际名称
### 优先级 P2 (后续优化)
6. **增加未分配 assignee 的任务可见性**
7. **组织树节点交互优化** (解决点击超时)
8. **消息模板名称字段冗余查询优化**
9. **Settings 表唯一索引补全**
---
## 十、测试结论
### 总体评估: **有条件通过**
ERP Platform v0.1.0 的核心业务功能基本完整跨模块事件集成Workflow -> Message工作正常多租户数据隔离和安全认证机制验证通过。
**主要成就:**
- 81 个 API 端点 100% 覆盖测试
- Workflow/Message 模块 24/24 端点全部通过
- 跨模块事件通知 100% 触发成功
- 安全认证 100% 通过
- 前端 22 个页面全部可访问
**阻塞问题:**
- Settings 模块完全不可用 (CRITICAL)
- 所有实体编辑功能因缺少 version 字段而失败 (HIGH)
- API 响应延迟 2.2s 严重影响用户体验 (HIGH)
**建议**: 修复 P0 和 P1 级别问题后进行回归测试,通过后方可进入下一阶段。

View File

@@ -0,0 +1,3 @@
019d9754-a0a | name_repr='CRM' | cjk_name=False | replacement=True
019d96d8-31a | name_repr='<27>ͻ<EFBFBD><CDBB><EFBFBD><EFBFBD><EFBFBD>' | cjk_name=True | replacement=False
019d9754-628 | name_repr='CRM' | cjk_name=False | replacement=True

View File

@@ -0,0 +1 @@
{"success":true,"data":{"id":"019d9754-a0ad-7f52-a0f6-23b4ad77d34a","name":"CRM","version":"0.1.0","description":"<22>ͻ<EFBFBD><CDBB><EFBFBD>ϵ<EFBFBD><CFB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> ERP ƽ̨<C6BD><CCA8>һ<EFBFBD><D2BB><EFBFBD><EFBFBD>ҵ<EFBFBD><D2B5><EFBFBD>","author":"ERP Team","status":"running","config":{},"installed_at":"2026-04-16T17:26:47.808967Z","enabled_at":"2026-04-16T17:26:48.164440Z","entities":[{"name":"customer","display_name":"customer","table_name":"plugin_erp_crm_customer"},{"name":"contact","display_name":"contact","table_name":"plugin_erp_crm_contact"},{"name":"communication","display_name":"communication","table_name":"plugin_erp_crm_communication"},{"name":"customer_tag","display_name":"customer_tag","table_name":"plugin_erp_crm_customer_tag"},{"name":"customer_relationship","display_name":"customer_relationship","table_name":"plugin_erp_crm_customer_relationship"}],"permissions":[{"code":"customer.list","name":"<22><EFBFBD>ͻ<EFBFBD>","description":"<22><EFBFBD>ͻ<EFBFBD><CDBB>б<EFBFBD><D0B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"},{"code":"customer.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD>","description":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E0BCAD>ɾ<EFBFBD><C9BE><EFBFBD>ͻ<EFBFBD>"},{"code":"contact.list","name":"<22><EFBFBD><E9BFB4>ϵ<EFBFBD><CFB5>","description":""},{"code":"contact.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵ<EFBFBD><CFB5>","description":""},{"code":"communication.list","name":"<22><EFBFBD><E9BFB4>ͨ<EFBFBD><CDA8>¼","description":""},{"code":"communication.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>¼","description":""},{"code":"tag.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ǩ","description":""},{"code":"relationship.list","name":"<22><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ϵ","description":""},{"code":"relationship.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ϵ","description":""}],"record_version":1},"message":null}

View File

@@ -0,0 +1,5 @@
019d9754-a0a | name=CRM | cjk=False | broken=True
019d96d8-31a | name=客户管理 | cjk=True | broken=False
019d9754-628 | name=CRM | cjk=False | broken=True
019d976e-dd2 | name=<3D><><EFBFBD>Բ<EFBFBD><D4B2> | cjk=False | broken=True
019d9771-4c8 | name=测试插件 | cjk=True | broken=False

View File

@@ -0,0 +1 @@
{"success":true,"data":{"data":[{"id":"019d9754-a0ad-7f52-a0f6-23b4ad77d34a","name":"CRM","version":"0.1.0","description":"<22>ͻ<EFBFBD><CDBB><EFBFBD>ϵ<EFBFBD><CFB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> ERP ƽ̨<C6BD><CCA8>һ<EFBFBD><D2BB><EFBFBD><EFBFBD>ҵ<EFBFBD><D2B5><EFBFBD>","author":"ERP Team","status":"running","config":{},"installed_at":"2026-04-16T17:26:47.808967Z","enabled_at":"2026-04-16T17:26:48.164440Z","entities":[{"name":"customer","display_name":"customer","table_name":"plugin_erp_crm_customer"},{"name":"contact","display_name":"contact","table_name":"plugin_erp_crm_contact"},{"name":"communication","display_name":"communication","table_name":"plugin_erp_crm_communication"},{"name":"customer_tag","display_name":"customer_tag","table_name":"plugin_erp_crm_customer_tag"},{"name":"customer_relationship","display_name":"customer_relationship","table_name":"plugin_erp_crm_customer_relationship"}],"permissions":[{"code":"customer.list","name":"<22><EFBFBD>ͻ<EFBFBD>","description":"<22><EFBFBD>ͻ<EFBFBD><CDBB>б<EFBFBD><D0B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"},{"code":"customer.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD>","description":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E0BCAD>ɾ<EFBFBD><C9BE><EFBFBD>ͻ<EFBFBD>"},{"code":"contact.list","name":"<22><EFBFBD><E9BFB4>ϵ<EFBFBD><CFB5>","description":""},{"code":"contact.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵ<EFBFBD><CFB5>","description":""},{"code":"communication.list","name":"<22><EFBFBD><E9BFB4>ͨ<EFBFBD><CDA8>¼","description":""},{"code":"communication.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>¼","description":""},{"code":"tag.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ǩ","description":""},{"code":"relationship.list","name":"<22><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ϵ","description":""},{"code":"relationship.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ϵ","description":""}],"record_version":1},{"id":"019d96d8-31ad-78a3-9236-eaa62e00c7ea","name":"客户管理","version":"0.1.0","description":"客户关系管理插件 — ERP 平台第一个行业插件","author":"ERP Team","status":"uninstalled","config":{},"installed_at":"2026-04-16T15:11:13.804848Z","enabled_at":"2026-04-16T15:11:40.054367Z","entities":[],"permissions":[{"code":"customer.list","name":"查看客户","description":"查看客户列表和详情"},{"code":"customer.manage","name":"管理客户","description":"创建、编辑、删除客户"},{"code":"contact.list","name":"查看联系人","description":""},{"code":"contact.manage","name":"管理联系人","description":""},{"code":"communication.list","name":"查看沟通记录","description":""},{"code":"communication.manage","name":"管理沟通记录","description":""},{"code":"tag.manage","name":"管理客户标签","description":""},{"code":"relationship.list","name":"查看客户关系","description":""},{"code":"relationship.manage","name":"管理客户关系","description":""}],"record_version":1},{"id":"019d9754-628e-71f3-b0b4-81c08dae67e5","name":"CRM","version":"0.1.0","description":"<22>ͻ<EFBFBD><CDBB><EFBFBD>ϵ<EFBFBD><CFB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> ERP ƽ̨<C6BD><CCA8>һ<EFBFBD><D2BB><EFBFBD><EFBFBD>ҵ<EFBFBD><D2B5><EFBFBD>","author":"ERP Team","status":"uploaded","config":{},"entities":[],"permissions":[{"code":"customer.list","name":"<22><EFBFBD>ͻ<EFBFBD>","description":"<22><EFBFBD>ͻ<EFBFBD><CDBB>б<EFBFBD><D0B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"},{"code":"customer.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD>","description":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E0BCAD>ɾ<EFBFBD><C9BE><EFBFBD>ͻ<EFBFBD>"},{"code":"contact.list","name":"<22><EFBFBD><E9BFB4>ϵ<EFBFBD><CFB5>","description":""},{"code":"contact.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵ<EFBFBD><CFB5>","description":""},{"code":"communication.list","name":"<22><EFBFBD><E9BFB4>ͨ<EFBFBD><CDA8>¼","description":""},{"code":"communication.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>¼","description":""},{"code":"tag.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ǩ","description":""},{"code":"relationship.list","name":"<22><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ϵ","description":""},{"code":"relationship.manage","name":"<22><><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ϵ","description":""}],"record_version":1},{"id":"019d976e-dd2e-75b3-9148-a61fc00b5937","name":"<22><><EFBFBD>Բ<EFBFBD><D4B2>","version":"0.1.0","description":"<22><><EFBFBD>IJ<EFBFBD><C4B2><EFBFBD>","status":"uploaded","config":{},"entities":[],"record_version":1},{"id":"019d9771-4c85-7aa1-b322-b5c31dd5a481","name":"测试插件","version":"0.1.0","description":"中文描述","status":"uploaded","config":{},"entities":[],"record_version":1}],"total":5,"page":1,"page_size":20,"total_pages":1},"message":null}

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB