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

View File

@@ -0,0 +1,169 @@
# 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**:
- 移除所有 propspluginId/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**:
- 移除 propspluginId/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**:
- 移除 propspluginId/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 页面显示统计