docs: 添加项目详情参数配置页面设计文档
This commit is contained in:
854
docs/plans/2026-03-06-project-param-config-design.md
Normal file
854
docs/plans/2026-03-06-project-param-config-design.md
Normal file
@@ -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
|
||||
<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 方法
|
||||
|
||||
```java
|
||||
@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 方法
|
||||
|
||||
```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<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 新增:**
|
||||
|
||||
```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 前端错误处理
|
||||
|
||||
**网络错误:**
|
||||
|
||||
```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
|
||||
**下一步:** 创建详细实施计划
|
||||
Reference in New Issue
Block a user