From c1da2bdaab9534c0e32d77513c586e0416dd8e82 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Fri, 6 Mar 2026 15:10:23 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E5=8F=82=E6=95=B0=E9=85=8D=E7=BD=AE=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-03-06-project-param-config-design.md | 854 ++++++++++++++++++ 1 file changed, 854 insertions(+) create mode 100644 docs/plans/2026-03-06-project-param-config-design.md diff --git a/docs/plans/2026-03-06-project-param-config-design.md b/docs/plans/2026-03-06-project-param-config-design.md new file mode 100644 index 0000000..e7ba890 --- /dev/null +++ b/docs/plans/2026-03-06-project-param-config-design.md @@ -0,0 +1,854 @@ +# 项目详情参数配置页面设计文档 + +**创建时间:** 2026-03-06 +**作者:** Claude Code +**状态:** 已批准 + +--- + +## 1. 概述 + +### 1.1 需求背景 + +纪检初核系统需要在项目详情页面中添加参数配置功能,允许用户为每个项目自定义模型参数配置。当前系统已有独立的模型参数配置页面(管理系统默认参数),需要将其功能复用到项目详情页面中。 + +### 1.2 核心需求 + +1. **配置模式:** 自动切换模式(修改即切换为 custom) +2. **界面布局:** 完全复用独立页面的布局(模型下拉框 + 参数表格 + 保存按钮) +3. **重置功能:** 不提供切换回默认配置的功能 +4. **初始化策略:** 查询时复制(按需创建自定义参数) + +### 1.3 设计原则 + +1. **最小改动原则:** 前端组件直接复用代码,后端只修改必要的方法 +2. **自动切换原则:** 用户保存参数时自动从 default 切换到 custom +3. **按需创建原则:** 只在首次保存时创建项目自定义参数,不预复制 +4. **数据隔离原则:** 项目自定义参数与系统默认参数完全独立 + +--- + +## 2. 架构设计 + +### 2.1 整体架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 项目详情页面 │ +│ detail.vue │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ 菜单栏: 上传数据 | 参数配置 | 结果总览 | ... │ │ +│ └───────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ ParamConfig 组件 │ │ +│ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ 模型选择下拉框 │ │ │ +│ │ └─────────────────────────────────────────────┘ │ │ +│ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ 参数表格(可编辑) │ │ │ +│ │ └─────────────────────────────────────────────┘ │ │ +│ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ 保存按钮 │ │ │ +│ │ └─────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ + │ API 调用 + ▼ +┌─────────────────────────────────────────────────────────┐ +│ 后端 CcdiModelParamController │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ GET /ccdi/modelParam/modelList?projectId={id} │ │ +│ │ - 查询模型列表 │ │ +│ └───────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ GET /ccdi/modelParam/list?projectId={id} │ │ +│ │ - 查询模型参数列表 │ │ +│ │ - 如果 configType=default,返回系统默认参数 │ │ +│ │ - 如果 configType=custom,返回项目自定义参数 │ │ +│ └───────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ POST /ccdi/modelParam/save │ │ +│ │ - 保存参数 │ │ +│ │ - 如果是首次保存,自动复制系统默认参数 │ │ +│ │ - 更新 configType=custom │ │ +│ └───────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ 数据库 ccdi_model_param │ +│ - projectId=0:系统默认参数 │ +│ - projectId>0:项目自定义参数 │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 数据模型 + +**表:ccdi_model_param** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | BIGINT | 主键ID | +| project_id | BIGINT | 项目ID(0表示默认参数) | +| model_code | VARCHAR(50) | 模型编码 | +| model_name | VARCHAR(100) | 模型名称 | +| param_code | VARCHAR(50) | 参数编码 | +| param_name | VARCHAR(100) | 监测项名称 | +| param_desc | VARCHAR(500) | 参数描述 | +| param_value | VARCHAR(200) | 参数值 | +| param_unit | VARCHAR(50) | 参数单位 | +| sort_order | INT | 排序号 | +| create_by | VARCHAR(64) | 创建者 | +| create_time | DATETIME | 创建时间 | +| update_by | VARCHAR(64) | 更新者 | +| update_time | DATETIME | 更新时间 | +| remark | VARCHAR(500) | 备注 | + +**表:ccdi_project(相关字段)** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| project_id | BIGINT | 项目ID | +| config_type | VARCHAR(20) | 配置方式:default-全局默认,custom-自定义 | + +--- + +## 3. 组件设计 + +### 3.1 前端组件 + +**组件路径:** `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue` + +**组件结构:** + +```vue + + + + + + + + + + + + + + + + 阈值参数配置 + + + + + + + + + + + + + + + + 保存配置 + + + + + + + + +``` + +### 3.2 后端接口 + +**文件:** `CcdiModelParamServiceImpl.java` + +**修改的方法:** + +#### 3.2.1 selectParamList 方法 + +```java +@Override +public List selectParamList(ModelParamQueryDTO queryDTO) { + // 1. 查询项目信息 + CcdiProject project = projectMapper.selectById(queryDTO.getProjectId()); + + if (project == null) { + throw new ServiceException("项目不存在"); + } + + // 2. 根据 configType 决定查询哪组参数 + Long effectiveProjectId; + if ("default".equals(project.getConfigType())) { + // 使用系统默认参数 + effectiveProjectId = 0L; + } else { + // 使用项目自定义参数 + effectiveProjectId = queryDTO.getProjectId(); + } + + // 3. 查询参数列表 + return modelParamMapper.selectParamList(effectiveProjectId, queryDTO.getModelCode()); +} +``` + +#### 3.2.2 saveParams 方法 + +```java +@Override +@Transactional(rollbackFor = Exception.class) +public void saveParams(ModelParamSaveDTO saveDTO) { + try { + // 1. 参数验证 + if (saveDTO.getProjectId() == null) { + throw new ServiceException("项目ID不能为空"); + } + if (StringUtils.isBlank(saveDTO.getModelCode())) { + throw new ServiceException("模型编码不能为空"); + } + if (saveDTO.getParams() == null || saveDTO.getParams().isEmpty()) { + throw new ServiceException("参数列表不能为空"); + } + + // 2. 查询项目信息 + CcdiProject project = projectMapper.selectById(saveDTO.getProjectId()); + if (project == null) { + throw new ServiceException("项目不存在"); + } + + // 3. 如果是首次保存(configType=default),需要复制系统默认参数 + if ("default".equals(project.getConfigType())) { + int copiedCount = copyDefaultParamsToProject( + saveDTO.getProjectId(), + saveDTO.getModelCode() + ); + if (copiedCount == 0) { + log.warn("系统默认参数为空,projectId={}, modelCode={}", + saveDTO.getProjectId(), saveDTO.getModelCode()); + } + + // 更新项目配置类型为 custom + project.setConfigType("custom"); + projectMapper.updateById(project); + } + + // 4. 更新参数值 + for (ModelParamSaveDTO.ParamValueItem item : saveDTO.getParams()) { + int updated = modelParamMapper.updateParamValue( + saveDTO.getProjectId(), + saveDTO.getModelCode(), + item.getParamCode(), + item.getParamValue() + ); + if (updated == 0) { + log.warn("参数不存在或未更新,paramCode={}", item.getParamCode()); + } + } + + } catch (ServiceException e) { + // 业务异常,直接抛出 + throw e; + } catch (Exception e) { + // 系统异常,记录日志并抛出 + log.error("保存模型参数失败", e); + throw new ServiceException("保存模型参数失败:" + e.getMessage()); + } +} + +/** + * 复制系统默认参数到项目 + * + * @param projectId 项目ID + * @param modelCode 模型编码 + * @return 复制的参数数量 + */ +private int copyDefaultParamsToProject(Long projectId, String modelCode) { + // 查询系统默认参数 + List defaultParams = modelParamMapper.selectList( + new LambdaQueryWrapper() + .eq(CcdiModelParam::getProjectId, 0L) + .eq(CcdiModelParam::getModelCode, modelCode) + ); + + if (defaultParams.isEmpty()) { + return 0; + } + + // 复制到项目 + List projectParams = defaultParams.stream() + .map(param -> { + CcdiModelParam newParam = new CcdiModelParam(); + BeanUtils.copyProperties(param, newParam); + newParam.setId(null); // 清空ID,让数据库自动生成 + newParam.setProjectId(projectId); + newParam.setCreateBy(null); // 清空审计字段,让 MyBatis Plus 自动填充 + newParam.setCreateTime(null); + newParam.setUpdateBy(null); + newParam.setUpdateTime(null); + return newParam; + }) + .collect(Collectors.toList()); + + // 批量插入 + modelParamMapper.insertBatch(projectParams); + + return projectParams.size(); +} +``` + +#### 3.2.3 Mapper 方法 + +**CcdiModelParamMapper.xml 新增:** + +```xml + + + UPDATE ccdi_model_param + SET param_value = #{paramValue}, + update_by = NULL, + update_time = NOW() + WHERE project_id = #{projectId} + AND model_code = #{modelCode} + AND param_code = #{paramCode} + + + + + INSERT INTO ccdi_model_param ( + project_id, model_code, model_name, param_code, param_name, + param_desc, param_value, param_unit, sort_order, + create_by, create_time, remark + ) VALUES + + ( + #{item.projectId}, #{item.modelCode}, #{item.modelName}, + #{item.paramCode}, #{item.paramName}, #{item.paramDesc}, + #{item.paramValue}, #{item.paramUnit}, #{item.sortOrder}, + NULL, NOW(), #{item.remark} + ) + + +``` + +--- + +## 4. 数据流设计 + +### 4.1 查看参数配置(configType=default) + +``` +用户点击"参数配置"菜单 + ↓ +前端调用 GET /ccdi/modelParam/modelList?projectId=123 + ↓ +后端返回模型列表 + ↓ +前端选择第一个模型,调用 GET /ccdi/modelParam/list?projectId=123&modelCode=MODEL_001 + ↓ +后端查询项目,发现 configType=default + ↓ +后端返回系统默认参数(projectId=0) + ↓ +前端显示参数表格 +``` + +### 4.2 查看参数配置(configType=custom) + +``` +用户点击"参数配置"菜单 + ↓ +前端调用 GET /ccdi/modelParam/modelList?projectId=123 + ↓ +后端返回模型列表 + ↓ +前端选择第一个模型,调用 GET /ccdi/modelParam/list?projectId=123&modelCode=MODEL_001 + ↓ +后端查询项目,发现 configType=custom + ↓ +后端返回项目自定义参数(projectId=123) + ↓ +前端显示参数表格 +``` + +### 4.3 首次保存参数(default → custom) + +``` +用户修改参数值,点击"保存配置" + ↓ +前端调用 POST /ccdi/modelParam/save +{ + "projectId": 123, + "modelCode": "MODEL_001", + "params": [ + {"paramCode": "THRESHOLD_1", "paramValue": "100"}, + {"paramCode": "THRESHOLD_2", "paramValue": "50"} + ] +} + ↓ +后端检查项目 configType=default + ↓ +后端执行复制操作: + 1. 查询系统默认参数(projectId=0, modelCode=MODEL_001) + 2. 复制所有参数,设置 projectId=123 + 3. 批量插入到数据库 + ↓ +后端更新项目的 configType=custom + ↓ +后端更新参数值: + UPDATE ccdi_model_param + SET param_value='100' + WHERE project_id=123 AND model_code='MODEL_001' AND param_code='THRESHOLD_1' + ↓ +后端返回成功 + ↓ +前端重新加载参数列表(此时查询的是项目自定义参数) + ↓ +前端显示成功消息 +``` + +### 4.4 再次保存参数(configType=custom) + +``` +用户修改参数值,点击"保存配置" + ↓ +前端调用 POST /ccdi/modelParam/save + ↓ +后端检查项目 configType=custom + ↓ +后端跳过复制步骤,直接更新参数值 + ↓ +后端返回成功 +``` + +--- + +## 5. 错误处理 + +### 5.1 前端错误处理 + +**网络错误:** + +```javascript +async loadParamList() { + try { + const res = await listParams(this.queryParams) + this.paramList = res.data + } catch (error) { + this.$message.error('加载参数列表失败:' + error.message) + console.error('加载参数列表失败', error) + } +} +``` + +**保存验证:** + +```javascript +async handleSave() { + // 验证是否有修改 + const modifiedParams = this.paramList.filter(item => item.modified) + if (modifiedParams.length === 0) { + this.$message.info('没有需要保存的修改') + return + } + + // 验证参数值 + const invalidParams = modifiedParams.filter( + item => !item.paramValue || item.paramValue.trim() === '' + ) + if (invalidParams.length > 0) { + this.$message.error('请填写所有参数值') + return + } + + // 保存 + this.saving = true + try { + await saveParams(saveDTO) + this.$message.success('保存成功') + // 清除修改标记并重新加载 + this.paramList.forEach(item => { item.modified = false }) + await this.loadParamList() + } catch (error) { + if (error.response && error.response.data && error.response.data.msg) { + this.$message.error('保存失败:' + error.response.data.msg) + } else { + this.$message.error('保存失败:' + error.message) + } + } finally { + this.saving = false + } +} +``` + +### 5.2 后端错误处理 + +**异常处理:** + +```java +@Override +@Transactional(rollbackFor = Exception.class) +public void saveParams(ModelParamSaveDTO saveDTO) { + try { + // 1. 参数验证 + if (saveDTO.getProjectId() == null) { + throw new ServiceException("项目ID不能为空"); + } + if (StringUtils.isBlank(saveDTO.getModelCode())) { + throw new ServiceException("模型编码不能为空"); + } + if (saveDTO.getParams() == null || saveDTO.getParams().isEmpty()) { + throw new ServiceException("参数列表不能为空"); + } + + // 2. 查询项目信息 + CcdiProject project = projectMapper.selectById(saveDTO.getProjectId()); + if (project == null) { + throw new ServiceException("项目不存在"); + } + + // 3. 复制默认参数(如果需要) + if ("default".equals(project.getConfigType())) { + int copiedCount = copyDefaultParamsToProject( + saveDTO.getProjectId(), + saveDTO.getModelCode() + ); + if (copiedCount == 0) { + log.warn("系统默认参数为空,projectId={}, modelCode={}", + saveDTO.getProjectId(), saveDTO.getModelCode()); + } + + // 更新项目配置类型 + project.setConfigType("custom"); + projectMapper.updateById(project); + } + + // 4. 更新参数值 + for (ModelParamSaveDTO.ParamValueItem item : saveDTO.getParams()) { + int updated = modelParamMapper.updateParamValue( + saveDTO.getProjectId(), + saveDTO.getModelCode(), + item.getParamCode(), + item.getParamValue() + ); + if (updated == 0) { + log.warn("参数不存在或未更新,paramCode={}", item.getParamCode()); + } + } + + } catch (ServiceException e) { + // 业务异常,直接抛出 + throw e; + } catch (Exception e) { + // 系统异常,记录日志并抛出 + log.error("保存模型参数失败", e); + throw new ServiceException("保存模型参数失败:" + e.getMessage()); + } +} +``` + +### 5.3 错误场景处理表 + +| 错误场景 | 处理方式 | +|---------|---------| +| 项目不存在 | 返回 404 错误,提示"项目不存在" | +| 系统默认参数为空 | 记录警告日志,继续执行(允许项目自定义参数) | +| 参数值验证失败 | 前端拦截,不提交到后端 | +| 数据库连接失败 | 返回 500 错误,提示"系统异常,请稍后重试" | +| 事务回滚 | 自动回滚所有操作,保证数据一致性 | + +--- + +## 6. 测试策略 + +### 6.1 后端单元测试 + +**测试类:** `CcdiModelParamServiceImplTest.java` + +**测试用例:** + +1. `testSelectParamList_DefaultConfig()` - 测试查询默认配置项目的参数列表 +2. `testSelectParamList_CustomConfig()` - 测试查询自定义配置项目的参数列表 +3. `testSaveParams_FirstTimeSave()` - 测试首次保存参数(触发 default → custom 切换) +4. `testSaveParams_SecondTimeSave()` - 测试再次保存参数(已为 custom 模式) + +### 6.2 前端集成测试 + +**测试脚本:** `test-param-config.sh` + +**测试流程:** + +1. 登录获取 Token +2. 创建测试项目 +3. 查询模型列表 +4. 查询参数列表(default 模式) +5. 首次保存参数(触发切换) +6. 查询参数列表(custom 模式) +7. 查询项目信息(验证 configType) +8. 清理测试数据 + +### 6.3 手动测试清单 + +| 编号 | 测试场景 | 预期结果 | 通过标准 | +|------|---------|---------|---------| +| 1 | 新项目查看参数配置 | 显示系统默认参数 | 参数值与系统默认一致 | +| 2 | 新项目修改并保存参数 | 自动切换为自定义配置 | configType 变为 custom | +| 3 | 再次查看参数 | 显示项目自定义参数 | 参数值为修改后的值 | +| 4 | 再次修改参数 | 直接更新参数值 | 参数值更新成功 | +| 5 | 切换模型 | 正确加载不同模型的参数 | 参数列表正确切换 | +| 6 | 不修改任何参数点击保存 | 提示"没有需要保存的修改" | 不发起保存请求 | +| 7 | 清空参数值后保存 | 前端验证拦截 | 显示错误提示 | +| 8 | 并发保存同一参数 | 后保存的值生效 | 数据一致性 | +| 9 | 网络异常时保存 | 显示错误提示 | 不更新页面数据 | +| 10 | 项目状态为"已归档"时保存 | 根据业务规则处理 | 符合业务逻辑 | + +### 6.4 性能测试 + +| 测试项 | 测试方法 | 性能目标 | +|--------|---------|---------| +| 查询参数列表 | 模拟 100 个项目同时查询 | 响应时间 < 500ms | +| 首次保存参数 | 模拟 50 个项目同时首次保存 | 响应时间 < 2s | +| 数据库查询性能 | EXPLAIN 分析 SQL | 使用索引,无全表扫描 | +| 并发保存 | 10 个并发请求保存同一项目 | 无死锁,数据一致 | + +--- + +## 7. 实施计划 + +### 7.1 实施步骤 + +1. **后端开发** + - 修改 `CcdiModelParamServiceImpl.selectParamList()` 方法 + - 修改 `CcdiModelParamServiceImpl.saveParams()` 方法 + - 新增 `copyDefaultParamsToProject()` 私有方法 + - 新增 Mapper XML 中的 `updateParamValue` 和 `insertBatch` 方法 + +2. **前端开发** + - 实现 `ParamConfig.vue` 组件 + - 复用 `ccdi/modelParam.js` API 接口 + - 确保组件正确接收 `projectId` 和 `projectInfo` props + +3. **测试** + - 编写后端单元测试 + - 编写集成测试脚本 + - 执行手动测试清单 + +4. **文档** + - 更新 API 文档 + - 更新用户手册 + +### 7.2 风险评估 + +| 风险项 | 影响 | 概率 | 应对措施 | +|--------|------|------|---------| +| 并发保存导致数据不一致 | 高 | 低 | 使用事务隔离,数据库行锁 | +| 系统默认参数缺失 | 中 | 低 | 记录日志,允许项目自定义 | +| 前端缓存导致参数不更新 | 低 | 中 | 保存后重新加载参数列表 | +| 大批量参数复制性能问题 | 中 | 低 | 使用批量插入,控制事务大小 | + +--- + +## 8. 附录 + +### 8.1 相关文件清单 + +**前端文件:** +- `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue` - 参数配置组件 +- `ruoyi-ui/src/api/ccdi/modelParam.js` - API 接口(已存在,无需修改) + +**后端文件:** +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java` - Service 实现 +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiModelParamMapper.java` - Mapper 接口 +- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiModelParamMapper.xml` - Mapper XML + +**测试文件:** +- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiModelParamServiceImplTest.java` - 单元测试 +- `docs/test-scripts/test-param-config.sh` - 集成测试脚本 + +### 8.2 参考文档 + +- 若依框架官方文档 +- MyBatis Plus 官方文档 +- Element UI 官方文档 +- 项目 CLAUDE.md 开发规范 + +--- + +**设计完成时间:** 2026-03-06 +**下一步:** 创建详细实施计划