Files
ccdi/docs/plans/2026-03-06-project-param-config-design.md

28 KiB
Raw Blame History

项目详情参数配置页面设计文档

创建时间: 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 项目ID0表示默认参数
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

测试用例:

  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 中的 updateParamValueinsertBatch 方法
  2. 前端开发

    • 实现 ParamConfig.vue 组件
    • 复用 ccdi/modelParam.js API 接口
    • 确保组件正确接收 projectIdprojectInfo 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 下一步: 创建详细实施计划