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

24 KiB
Raw Blame History

模型参数配置页面优化设计文档

文档版本: v1.0 创建日期: 2026-03-06 设计人员: Claude Code


一、概述

1.1 背景

当前模型参数配置页面采用模型下拉框切换的方式,用户需要逐个切换模型才能查看和配置不同模型的参数,操作不够便捷。本次优化旨在取消模型切换,改为在同一页面中以垂直堆叠方式展示所有模型的参数表格,提升用户体验。

1.2 目标

  • 取消模型名称查询切换
  • 在同一页面中分多个表格展示所有模型的参数
  • 全局模型参数配置页面和项目内模型参数配置页面同步修改
  • 统一保存机制,一次性保存所有修改

1.3 影响范围

前端页面:

  • ruoyi-ui/src/views/ccdi/modelParam/index.vue - 全局模型参数配置页面
  • ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue - 项目内参数配置页面

后端接口:

  • CcdiModelParamController.java - 新增批量查询和批量保存接口
  • ICcdiModelParamService.java - 新增Service方法
  • CcdiModelParamServiceImpl.java - 实现批量操作逻辑
  • CcdiModelParamMapper.java - 新增Mapper方法
  • CcdiModelParamMapper.xml - 新增SQL查询

二、详细设计

2.1 后端接口设计

2.1.1 批量查询所有模型参数

接口路径: GET /ccdi/modelParam/listAll

请求参数:

public class ModelParamAllQueryDTO {
    /** 项目ID0表示全局配置>0表示项目配置 */
    private Long projectId;
}

响应结构:

public class ModelParamAllVO {
    /** 模型列表(包含每个模型及其参数) */
    private List<ModelGroupVO> models;
}

public class ModelGroupVO {
    /** 模型编码 */
    private String modelCode;

    /** 模型名称 */
    private String modelName;

    /** 参数列表 */
    private List<ModelParamVO> params;
}

返回数据示例:

{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "models": [
      {
        "modelCode": "LARGE_TRANSACTION",
        "modelName": "大额交易模型",
        "params": [
          {
            "paramCode": "THRESHOLD_AMOUNT",
            "paramName": "单笔交易金额阈值",
            "paramDesc": "单笔交易金额超过此值触发预警",
            "paramValue": "50000",
            "paramUnit": "元",
            "sortOrder": 1
          }
        ]
      },
      {
        "modelCode": "SUSPICIOUS_FOREIGN_EXCHANGE",
        "modelName": "可疑外汇交易模型",
        "params": [
          {
            "paramCode": "FREQUENCY_THRESHOLD",
            "paramName": "交易频次阈值",
            "paramDesc": "交易频次超过此值触发预警",
            "paramValue": "10",
            "paramUnit": "次/天",
            "sortOrder": 1
          }
        ]
      },
      {
        "modelCode": "SUSPICIOUS_PART_TIME",
        "modelName": "可疑兼职模型",
        "params": [...]
      }
    ]
  }
}

2.1.2 批量保存所有模型参数

接口路径: POST /ccdi/modelParam/saveAll

请求结构:

public class ModelParamSaveAllDTO {
    /** 项目ID */
    private Long projectId;

    /** 所有模型的参数修改(只包含修改过的参数) */
    private List<ModelParamGroupDTO> models;
}

public class ModelParamGroupDTO {
    /** 模型编码 */
    private String modelCode;

    /** 该模型下修改过的参数 */
    private List<ParamValueItem> params;
}

public class ParamValueItem {
    private String paramCode;
    private String paramValue;
}

请求示例:

{
  "projectId": 1,
  "models": [
    {
      "modelCode": "LARGE_TRANSACTION",
      "params": [
        {
          "paramCode": "THRESHOLD_AMOUNT",
          "paramValue": "60000"
        }
      ]
    },
    {
      "modelCode": "SUSPICIOUS_FOREIGN_EXCHANGE",
      "params": [
        {
          "paramCode": "FREQUENCY_THRESHOLD",
          "paramValue": "5"
        }
      ]
    }
  ]
}

响应示例:

{
  "code": 200,
  "msg": "保存成功"
}

错误码说明:

错误码 说明
400 参数验证失败项目ID为空、参数列表为空等
500 服务器内部错误(数据库操作失败等)

2.2 后端Service层设计

2.2.1 Service接口新增方法

public interface ICcdiModelParamService {

    /**
     * 查询所有模型及其参数(按模型分组)
     *
     * @param projectId 项目ID0表示全局配置
     * @return 所有模型的参数配置
     */
    ModelParamAllVO selectAllParams(Long projectId);

    /**
     * 批量保存所有模型的参数修改
     *
     * @param saveAllDTO 所有模型的参数修改数据
     */
    void saveAllParams(ModelParamSaveAllDTO saveAllDTO);

    // ... 保留原有的其他方法
}

2.2.2 Service实现类核心逻辑

查询所有模型参数:

@Override
public ModelParamAllVO selectAllParams(Long projectId) {
    // 1. 参数验证
    if (projectId == null) {
        projectId = 0L;
    }

    // 2. 如果是项目查询,根据 configType 决定查询哪组参数
    Long effectiveProjectId = projectId;
    if (projectId > 0) {
        CcdiProject project = projectMapper.selectById(projectId);
        if (project == null) {
            throw new ServiceException("项目不存在");
        }
        if ("default".equals(project.getConfigType())) {
            effectiveProjectId = 0L;
        }
    }

    // 3. 查询所有模型的参数
    List<CcdiModelParam> allParams = modelParamMapper.selectByProjectId(effectiveProjectId);

    // 4. 按模型分组
    Map<String, List<CcdiModelParam>> groupedParams = allParams.stream()
        .collect(Collectors.groupingBy(CcdiModelParam::getModelCode));

    // 5. 转换为VO
    ModelParamAllVO result = new ModelParamAllVO();
    List<ModelGroupVO> models = new ArrayList<>();

    groupedParams.forEach((modelCode, params) -> {
        ModelGroupVO groupVO = new ModelGroupVO();
        groupVO.setModelCode(modelCode);
        groupVO.setModelName(params.get(0).getModelName());

        List<ModelParamVO> paramVOs = params.stream()
            .map(param -> {
                ModelParamVO vo = new ModelParamVO();
                BeanUtils.copyProperties(param, vo);
                return vo;
            })
            .collect(Collectors.toList());

        groupVO.setParams(paramVOs);
        models.add(groupVO);
    });

    // 6. 按模型编码排序(保证固定顺序)
    models.sort(Comparator.comparing(ModelGroupVO::getModelCode));

    result.setModels(models);
    return result;
}

批量保存参数:

@Override
@Transactional(rollbackFor = Exception.class)
public void saveAllParams(ModelParamSaveAllDTO saveAllDTO) {
    try {
        // 1. 参数验证
        if (saveAllDTO.getProjectId() == null) {
            throw new ServiceException("项目ID不能为空");
        }
        if (saveAllDTO.getModels() == null || saveAllDTO.getModels().isEmpty()) {
            throw new ServiceException("参数列表不能为空");
        }

        Long projectId = saveAllDTO.getProjectId();

        // 2. 如果是项目保存,检查是否需要复制默认参数
        if (projectId > 0) {
            CcdiProject project = projectMapper.selectById(projectId);
            if (project == null) {
                throw new ServiceException("项目不存在");
            }

            // 如果是首次保存configType=default需要复制所有模型的系统默认参数
            if ("default".equals(project.getConfigType())) {
                for (ModelParamGroupDTO modelGroup : saveAllDTO.getModels()) {
                    copyDefaultParamsToProject(projectId, modelGroup.getModelCode());
                }

                // 更新项目配置类型为 custom
                project.setConfigType("custom");
                projectMapper.updateById(project);
            }
        }

        // 3. 批量更新所有模型的参数值
        String username = SecurityUtils.getUsername();
        for (ModelParamGroupDTO modelGroup : saveAllDTO.getModels()) {
            for (ParamValueItem item : modelGroup.getParams()) {
                int updated = modelParamMapper.updateParamValue(
                    projectId,
                    modelGroup.getModelCode(),
                    item.getParamCode(),
                    item.getParamValue()
                );
                if (updated == 0) {
                    log.warn("参数不存在或未更新modelCode={}, paramCode={}",
                        modelGroup.getModelCode(), item.getParamCode());
                }
            }
        }

    } catch (ServiceException e) {
        throw e;
    } catch (Exception e) {
        log.error("批量保存模型参数失败", e);
        throw new ServiceException("批量保存模型参数失败:" + e.getMessage());
    }
}

2.3 后端Mapper层设计

2.3.1 Mapper接口新增方法

public interface CcdiModelParamMapper extends BaseMapper<CcdiModelParam> {

    /**
     * 根据项目ID查询所有模型参数
     */
    List<CcdiModelParam> selectByProjectId(@Param("projectId") Long projectId);

    // ... 保留原有的其他方法
}

2.3.2 Mapper XML

<select id="selectByProjectId" resultType="CcdiModelParam">
    SELECT * FROM ccdi_model_param
    WHERE project_id = #{projectId}
    ORDER BY model_code, sort_order
</select>

2.4 前端组件设计

2.4.1 页面结构(两个页面相同布局)

<template>
  <div class="param-config-container">
    <!-- 页面标题 -->
    <div class="page-header">
      <h2>{{ pageTitle }}</h2>
    </div>

    <!-- 模型参数卡片组垂直堆叠 -->
    <div class="model-cards-container">
      <div
        v-for="model in modelGroups"
        :key="model.modelCode"
        class="model-card"
      >
        <!-- 模型标题 -->
        <div class="model-header">
          <h3>{{ model.modelName }}</h3>
        </div>

        <!-- 参数表格 -->
        <el-table :data="model.params" 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(model.modelCode, row)"
              />
            </template>
          </el-table-column>
          <el-table-column label="单位" prop="paramUnit" width="120" />
        </el-table>
      </div>
    </div>

    <!-- 统一保存按钮 -->
    <div class="button-section">
      <el-button type="primary" @click="handleSaveAll" :loading="saving">
        保存所有修改
      </el-button>
      <span v-if="modifiedCount > 0" class="modified-tip">
        已修改 {{ modifiedCount }} 个参数
      </span>
    </div>
  </div>
</template>

2.4.2 核心数据结构

data() {
  return {
    // 页面标题(全局配置 vs 项目配置)
    pageTitle: this.projectId ? '项目参数配置' : '全局模型参数管理',

    // 模型参数数据(按模型分组)
    modelGroups: [],  // ModelGroupVO[]

    // 修改记录(记录哪些参数被修改过)
    modifiedParams: new Map(), // Map<modelCode, Set<paramCode>>

    // 保存状态
    saving: false
  }
}

2.4.3 核心方法

methods: {
  /** 加载所有模型参数 */
  async loadAllParams() {
    try {
      const res = await listAllParams({ projectId: this.projectId })
      this.modelGroups = res.data.models
      // 清空修改记录
      this.modifiedParams.clear()
    } catch (error) {
      this.$message.error('加载参数失败:' + error.message)
    }
  },

  /** 标记参数为已修改 */
  markAsModified(modelCode, row) {
    if (!this.modifiedParams.has(modelCode)) {
      this.modifiedParams.set(modelCode, new Set())
    }
    this.modifiedParams.get(modelCode).add(row.paramCode)
  },

  /** 保存所有修改 */
  async handleSaveAll() {
    // 验证是否有修改
    if (this.modifiedCount === 0) {
      this.$message.info('没有需要保存的修改')
      return
    }

    // 构造保存数据(只包含修改过的参数)
    const saveDTO = {
      projectId: this.projectId,
      models: []
    }

    this.modifiedParams.forEach((paramCodes, modelCode) => {
      const modelGroup = this.modelGroups.find(m => m.modelCode === modelCode)
      const modifiedParamList = modelGroup.params
        .filter(p => paramCodes.has(p.paramCode))
        .map(p => ({
          paramCode: p.paramCode,
          paramValue: p.paramValue
        }))

      if (modifiedParamList.length > 0) {
        saveDTO.models.push({
          modelCode: modelCode,
          params: modifiedParamList
        })
      }
    })

    // 保存
    this.saving = true
    try {
      await saveAllParams(saveDTO)
      this.$message.success('保存成功')
      // 清空修改记录并重新加载
      this.modifiedParams.clear()
      await this.loadAllParams()
    } catch (error) {
      this.$message.error('保存失败:' + error.message)
    } finally {
      this.saving = false
    }
  }
},

computed: {
  /** 计算已修改参数数量 */
  modifiedCount() {
    let count = 0
    this.modifiedParams.forEach(params => {
      count += params.size
    })
    return count
  }
}

2.4.4 样式设计

.param-config-container {
  padding: 20px;
  background-color: #fff;
  min-height: 400px;
}

.page-header {
  margin-bottom: 20px;
  padding: 15px;
  background: #fff;
  border-radius: 4px;

  h2 {
    font-size: 18px;
    font-weight: bold;
    color: #333;
    margin: 0;
  }
}

.model-cards-container {
  margin-bottom: 20px;
}

.model-card {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  margin-bottom: 20px;
  border: 1px solid #e4e7ed;

  .model-header {
    margin-bottom: 15px;

    h3 {
      font-size: 16px;
      font-weight: bold;
      color: #333;
      margin: 0;
    }
  }
}

.button-section {
  padding: 15px;
  background: #fff;
  border-radius: 4px;
  text-align: left;

  .modified-tip {
    margin-left: 15px;
    color: #909399;
    font-size: 14px;
  }
}

2.5 前端API层设计

ruoyi-ui/src/api/ccdi/modelParam.js 中添加:

import request from '@/utils/request'

/**
 * 查询所有模型及其参数(按模型分组)
 */
export function listAllParams(query) {
  return request({
    url: '/ccdi/modelParam/listAll',
    method: 'get',
    params: query
  })
}

/**
 * 批量保存所有模型的参数修改
 */
export function saveAllParams(data) {
  return request({
    url: '/ccdi/modelParam/saveAll',
    method: 'post',
    data: data
  })
}

// 保留原有的其他API方法...

三、数据库设计

无需修改数据库表结构,现有的 ccdi_model_param 表结构已满足需求。

现有表结构说明:

字段名 类型 说明
id BIGINT 主键ID
project_id BIGINT 项目ID0表示默认参数
model_code VARCHAR 模型编码
model_name VARCHAR 模型名称
param_code VARCHAR 参数编码
param_name VARCHAR 监测项名称
param_desc VARCHAR 参数描述
param_value VARCHAR 参数值
param_unit VARCHAR 参数单位
sort_order INT 排序号
create_by VARCHAR 创建者
create_time DATETIME 创建时间
update_by VARCHAR 更新者
update_time DATETIME 更新时间
remark VARCHAR 备注

索引说明:

  • 主键:id
  • 常用查询索引:idx_project_model (project_id, model_code)

四、实现步骤

4.1 后端开发任务

第一阶段DTO/VO类创建

  • 创建 ModelParamAllQueryDTO.java - 批量查询请求DTO
  • 创建 ModelParamAllVO.java - 批量查询响应VO
  • 创建 ModelGroupVO.java - 模型分组VO
  • 创建 ModelParamSaveAllDTO.java - 批量保存请求DTO
  • 创建 ModelParamGroupDTO.java - 模型参数分组DTO

第二阶段Mapper层修改

  • CcdiModelParamMapper.java 中添加 selectByProjectId 方法
  • CcdiModelParamMapper.xml 中添加对应的SQL查询

第三阶段Service层修改

  • ICcdiModelParamService.java 接口中添加 selectAllParams 方法
  • ICcdiModelParamService.java 接口中添加 saveAllParams 方法
  • CcdiModelParamServiceImpl.java 中实现 selectAllParams 方法
  • CcdiModelParamServiceImpl.java 中实现 saveAllParams 方法

第四阶段Controller层修改

  • CcdiModelParamController.java 中添加 listAll 接口GET
  • CcdiModelParamController.java 中添加 saveAll 接口POST

第五阶段:后端测试

  • 使用 Swagger 测试 listAll 接口
    • 测试全局配置查询projectId=0
    • 测试项目配置查询projectId>0
    • 测试使用默认配置的项目configType=default
  • 使用 Swagger 测试 saveAll 接口
    • 测试全局配置保存
    • 测试项目首次保存(验证参数复制逻辑)
    • 测试项目二次保存
    • 测试多模型同时保存
  • 验证错误处理
    • 参数验证失败
    • 项目不存在
    • 数据库异常

4.2 前端开发任务

第一阶段API层修改

  • ruoyi-ui/src/api/ccdi/modelParam.js 中添加 listAllParams 方法
  • ruoyi-ui/src/api/ccdi/modelParam.js 中添加 saveAllParams 方法

第二阶段:全局配置页面重构

  • 重构 ruoyi-ui/src/views/ccdi/modelParam/index.vue
    • 去掉模型下拉框
    • 添加页面标题
    • 实现垂直堆叠布局展示所有模型
    • 实现参数修改跟踪
    • 实现统一保存按钮
    • 添加修改提示(显示已修改参数数量)
    • 优化样式

第三阶段:项目配置页面重构

  • 重构 ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue
    • 采用与全局配置页面相同的布局和逻辑
    • 适配 projectId 传递
    • 适配项目信息显示

第四阶段:前端测试

  • 测试全局配置页面
    • 页面加载是否正确显示所有模型
    • 参数修改和标记是否正常
    • 统一保存功能是否正常
    • 修改提示是否准确
  • 测试项目配置页面
    • 页面加载是否正确显示所有模型
    • 参数修改和保存功能是否正常
    • 使用默认配置的项目是否正确显示系统参数
    • 首次保存是否成功
  • 测试用户体验
    • 页面加载速度
    • 操作流畅性
    • 错误提示友好性

五、兼容性与迁移说明

5.1 向后兼容

保留原有接口:

  • 原有的 GET /list 接口保留,不影响其他可能的调用方
  • 原有的 POST /save 接口保留,继续可用

数据库无变更:

  • 数据库表结构无修改
  • 现有数据无需迁移

5.2 废弃说明

功能废弃:

  • 前端的模型下拉框切换方式不再使用
  • 但后端接口仍保留,以确保向后兼容

建议:

  • 逐步迁移所有调用方到新接口
  • 未来版本可以废弃旧接口

六、性能考虑

6.1 查询性能

优化措施:

  • 使用 selectByProjectId 一次性查询所有参数,减少数据库往返
  • 在内存中按模型分组,避免多次查询
  • 利用现有的 idx_project_model 索引

预期性能:

  • 当前模型数量3个
  • 预计参数总数约30个
  • 单次查询时间:<50ms
  • 完全满足性能要求

6.2 保存性能

优化措施:

  • 只保存修改过的参数,减少数据库更新操作
  • 使用事务保证数据一致性
  • 批量更新,避免多次提交

预期性能:

  • 典型修改场景1-5个参数
  • 保存时间:<100ms
  • 完全满足性能要求

6.3 前端性能

优化措施:

  • 使用 v-for 高效渲染列表
  • 使用计算属性缓存已修改参数数量
  • 避免不必要的重渲染

预期性能:

  • 页面渲染时间:<200ms
  • 操作响应时间:<50ms
  • 完全满足用户体验要求

七、安全考虑

7.1 权限控制

现有权限机制:

  • 使用 Spring Security + JWT 进行认证
  • 基于角色的访问控制RBAC
  • 新接口继承现有权限控制机制

权限标识:

  • 查询:ccdi:modelParam:list
  • 保存:ccdi:modelParam:edit

7.2 数据验证

后端验证:

  • 使用 @Validated 注解进行参数验证
  • 验证项目ID、模型编码、参数编码的合法性
  • 验证参数值的格式和范围

前端验证:

  • 参数值非空验证
  • 参数值格式验证

7.3 数据一致性

事务管理:

  • 使用 @Transactional 保证批量保存的原子性
  • 保存失败时自动回滚

并发控制:

  • 使用乐观锁或悲观锁(根据实际并发情况决定)
  • 当前场景并发量低,无需特殊处理

八、测试策略

8.1 单元测试

Service层测试

  • 测试 selectAllParams 方法
    • 测试全局配置查询
    • 测试项目配置查询
    • 测试使用默认配置的项目
    • 测试空数据情况
  • 测试 saveAllParams 方法
    • 测试参数验证
    • 测试首次保存(参数复制)
    • 测试二次保存
    • 测试事务回滚

8.2 集成测试

API接口测试

  • 使用 Swagger UI 进行接口测试
  • 测试各种参数组合
  • 测试错误场景

8.3 前端测试

功能测试:

  • 测试页面加载和渲染
  • 测试参数修改和标记
  • 测试保存功能
  • 测试错误处理

用户体验测试:

  • 测试页面响应速度
  • 测试操作流畅性
  • 测试错误提示友好性

九、风险评估

9.1 技术风险

风险 概率 影响 应对措施
后端接口设计不合理 充分设计评审,参考现有接口
前端组件复杂度高 采用简单清晰的组件结构
数据库查询性能差 极低 已有索引支持,数据量小
批量保存失败 使用事务保证原子性

9.2 业务风险

风险 概率 影响 应对措施
用户不习惯新界面 提供用户培训,界面简洁直观
误操作导致参数错误 添加确认提示,记录操作日志
保存时数据丢失 极低 使用事务,添加错误处理

9.3 兼容性风险

风险 概率 影响 应对措施
旧接口调用方受影响 保留旧接口,逐步迁移
数据库不兼容 极低 无数据库结构变更

十、上线计划

10.1 上线前准备

  • 完成所有开发任务
  • 完成所有测试任务
  • 准备上线文档
  • 准备回滚方案

10.2 上线步骤

  1. 后端部署

    • 停止应用服务
    • 部署新版本代码
    • 启动应用服务
    • 验证接口可用性
  2. 前端部署

    • 构建前端代码
    • 部署到服务器
    • 清理浏览器缓存
    • 验证页面可用性
  3. 功能验证

    • 测试全局配置页面
    • 测试项目配置页面
    • 验证保存功能
    • 验证数据一致性

10.3 上线后监控

  • 监控接口响应时间
  • 监控错误日志
  • 收集用户反馈
  • 准备问题修复

10.4 回滚方案

如果出现严重问题:

  1. 前端回滚到旧版本
  2. 后端回滚到旧版本(接口保留不影响)
  3. 数据无需回滚(无数据库变更)

十一、总结

本次设计采用了优化接口的方案,通过新增批量查询和批量保存接口,实现了在同一页面中展示和编辑所有模型参数的需求。设计充分考虑了性能、安全性、兼容性和可维护性,是一个可行且高效的解决方案。

设计亮点:

  • 接口设计合理,易于理解和扩展
  • 前后端分离,逻辑清晰
  • 保留向后兼容,降低风险
  • 性能优化,用户体验好
  • 代码复用性高,可维护性好

预期收益:

  • 🎯 提升用户体验,减少操作步骤
  • 🎯 提高工作效率,一次查看所有模型
  • 🎯 降低误操作风险,统一保存机制
  • 🎯 代码结构更清晰,便于后续维护

附录

A. 相关文档

B. 变更记录

版本 日期 修改人 修改内容
v1.0 2026-03-06 Claude Code 初始版本

文档结束