Files
erp/plans/merry-knitting-newt.md
iven 841766b168
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
fix(用户管理): 修复用户列表页面加载失败问题
修复用户列表页面加载失败导致测试超时的问题,确保页面元素正确渲染
2026-04-19 08:46:28 +08:00

7.0 KiB
Raw Blame History

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_idlet 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

| { 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

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 行)

搜索操作改为直接传参模式:

onSearch={(value) => {
  setSearchText(value);
  setPage(1);
  fetchData(1, { search: value });  // 直接传参
}}

修改 fetchData 签名允许覆盖搜索参数。

Fix 9: Canvas 高 DPI 支持

文件: apps/web/src/pages/PluginGraphPage.tsx (第 114-118 行)

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 变量读取主题色:

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_nameresolve_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 页面显示统计