28 KiB
28 KiB
项目详情参数配置页面设计文档
创建时间: 2026-03-06 作者: Claude Code 状态: 已批准
1. 概述
1.1 需求背景
纪检初核系统需要在项目详情页面中添加参数配置功能,允许用户为每个项目自定义模型参数配置。当前系统已有独立的模型参数配置页面(管理系统默认参数),需要将其功能复用到项目详情页面中。
1.2 核心需求
- 配置模式: 自动切换模式(修改即切换为 custom)
- 界面布局: 完全复用独立页面的布局(模型下拉框 + 参数表格 + 保存按钮)
- 重置功能: 不提供切换回默认配置的功能
- 初始化策略: 查询时复制(按需创建自定义参数)
1.3 设计原则
- 最小改动原则: 前端组件直接复用代码,后端只修改必要的方法
- 自动切换原则: 用户保存参数时自动从 default 切换到 custom
- 按需创建原则: 只在首次保存时创建项目自定义参数,不预复制
- 数据隔离原则: 项目自定义参数与系统默认参数完全独立
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
组件结构:
<template>
<div class="param-config-container">
<!-- 模型选择区域 -->
<div class="filter-section">
<el-form :inline="true" :model="queryParams">
<el-form-item label="模型名称">
<el-select
v-model="queryParams.modelCode"
placeholder="请选择模型"
@change="handleModelChange"
>
<el-option
v-for="model in modelList"
:key="model.modelCode"
:label="model.modelName"
:value="model.modelCode"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<!-- 参数配置表格 -->
<div class="table-section">
<h3>阈值参数配置</h3>
<el-table :data="paramList" border>
<el-table-column label="监测项" prop="paramName" width="200" />
<el-table-column label="描述" prop="paramDesc" />
<el-table-column label="阈值设置" width="200">
<template #default="{ row }">
<el-input
v-model="row.paramValue"
placeholder="请输入阈值"
@input="markAsModified(row)"
/>
</template>
</el-table-column>
<el-table-column label="单位" prop="paramUnit" width="120" />
</el-table>
</div>
<!-- 操作按钮 -->
<div class="button-section">
<el-button
type="primary"
@click="handleSave"
:loading="saving"
>
保存配置
</el-button>
</div>
</div>
</template>
<script>
import { listModels, listParams, saveParams } from "@/api/ccdi/modelParam";
export default {
name: 'ParamConfig',
props: {
projectId: {
type: [String, Number],
required: true
},
projectInfo: {
type: Object,
default: () => ({})
}
},
data() {
return {
modelList: [],
queryParams: {
modelCode: undefined,
projectId: this.projectId
},
paramList: [],
saving: false
}
},
watch: {
projectId(newVal) {
this.queryParams.projectId = newVal
this.loadModelList()
}
},
created() {
this.loadModelList()
},
methods: {
/** 加载模型列表 */
async loadModelList() {
try {
const res = await listModels({ projectId: this.projectId })
this.modelList = res.data
if (this.modelList.length > 0) {
this.queryParams.modelCode = this.modelList[0].modelCode
this.loadParamList()
}
} catch (error) {
this.$message.error('加载模型列表失败:' + error.message)
console.error('加载模型列表失败', error)
}
},
/** 加载参数列表 */
async loadParamList() {
try {
const res = await listParams(this.queryParams)
this.paramList = res.data
} catch (error) {
this.$message.error('加载参数列表失败:' + error.message)
console.error('加载参数列表失败', error)
}
},
/** 模型切换 */
handleModelChange() {
this.loadParamList()
},
/** 标记为已修改 */
markAsModified(row) {
row.modified = true
},
/** 保存配置 */
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
}
// 构造保存数据
const saveDTO = {
projectId: this.projectId,
modelCode: this.queryParams.modelCode,
params: modifiedParams.map(item => ({
paramCode: item.paramCode,
paramValue: item.paramValue
}))
}
// 保存
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
}
}
}
}
</script>
<style scoped lang="scss">
.param-config-container {
padding: 20px;
background-color: #fff;
min-height: 400px;
}
.filter-section {
padding: 15px;
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
}
.table-section {
padding: 20px;
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
h3 {
font-size: 16px;
font-weight: bold;
color: #333;
margin: 0 0 15px 0;
}
}
.button-section {
padding: 15px;
background: #fff;
border-radius: 4px;
text-align: left;
}
</style>
3.2 后端接口
文件: CcdiModelParamServiceImpl.java
修改的方法:
3.2.1 selectParamList 方法
@Override
public List<ModelParamVO> 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 方法
@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<CcdiModelParam> defaultParams = modelParamMapper.selectList(
new LambdaQueryWrapper<CcdiModelParam>()
.eq(CcdiModelParam::getProjectId, 0L)
.eq(CcdiModelParam::getModelCode, modelCode)
);
if (defaultParams.isEmpty()) {
return 0;
}
// 复制到项目
List<CcdiModelParam> 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 新增:
<!-- 更新参数值 -->
<update id="updateParamValue">
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}
</update>
<!-- 批量插入 -->
<insert id="insertBatch" parameterType="java.util.List">
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
<foreach collection="list" item="item" separator=",">
(
#{item.projectId}, #{item.modelCode}, #{item.modelName},
#{item.paramCode}, #{item.paramName}, #{item.paramDesc},
#{item.paramValue}, #{item.paramUnit}, #{item.sortOrder},
NULL, NOW(), #{item.remark}
)
</foreach>
</insert>
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 前端错误处理
网络错误:
async loadParamList() {
try {
const res = await listParams(this.queryParams)
this.paramList = res.data
} catch (error) {
this.$message.error('加载参数列表失败:' + error.message)
console.error('加载参数列表失败', error)
}
}
保存验证:
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 后端错误处理
异常处理:
@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
测试用例:
testSelectParamList_DefaultConfig()- 测试查询默认配置项目的参数列表testSelectParamList_CustomConfig()- 测试查询自定义配置项目的参数列表testSaveParams_FirstTimeSave()- 测试首次保存参数(触发 default → custom 切换)testSaveParams_SecondTimeSave()- 测试再次保存参数(已为 custom 模式)
6.2 前端集成测试
测试脚本: test-param-config.sh
测试流程:
- 登录获取 Token
- 创建测试项目
- 查询模型列表
- 查询参数列表(default 模式)
- 首次保存参数(触发切换)
- 查询参数列表(custom 模式)
- 查询项目信息(验证 configType)
- 清理测试数据
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 实施步骤
-
后端开发
- 修改
CcdiModelParamServiceImpl.selectParamList()方法 - 修改
CcdiModelParamServiceImpl.saveParams()方法 - 新增
copyDefaultParamsToProject()私有方法 - 新增 Mapper XML 中的
updateParamValue和insertBatch方法
- 修改
-
前端开发
- 实现
ParamConfig.vue组件 - 复用
ccdi/modelParam.jsAPI 接口 - 确保组件正确接收
projectId和projectInfoprops
- 实现
-
测试
- 编写后端单元测试
- 编写集成测试脚本
- 执行手动测试清单
-
文档
- 更新 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 下一步: 创建详细实施计划