# 项目详情参数配置页面设计文档
**创建时间:** 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
**下一步:** 创建详细实施计划