# 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` 从 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 页面显示统计