DevOps平台 — 第九篇:配置中心的设计与实现

项目是研发管理的执行单元,但「项目长什么样」------用瀑布还是敏捷?侧栏有哪些模块?能建哪些工作项?表单上要填哪些字段?------这些问题不应该硬编码在代码里,也不应该每建一个项目都从零配一遍。配置中心解决的就是这件事:把项目范式、模块开关、工作项类型、字段表单抽象成配置数据,让模板一次性定义、项目直接消费。本篇记录配置中心六张表的数据模型、四层配置链路、工作项类型自定义、动态表单渲染,以及前端三个独立页面与模板详情双 Tab 的交互设计。

一、配置层要解决什么问题

1.1 硬编码的困境

如果不做配置层,项目范式的差异只能写在代码里:

  • 瀑布项目侧栏有「规划」模块,敏捷没有------写死在前端路由表里
  • Scrum 项目能建史诗和故事,瀑布能建需求规格------写死在后端枚举里
  • 故事需要故事点字段,缺陷需要严重程度字段------写死在表单组件里
  • 某企业想要瀑布项目但去掉测试模块------改代码重新部署

每改一次策略就要动代码,每加一种项目范式就要发版。更关键的是:不同企业对同一种范式有不同的定制需求,硬编码无法响应。

1.2 配置层的设计目标

配置层回答四个问题:

复制代码
配置层四问

  ┌──────────────────────────────────────────────────────────┐
  │  1. 项目是什么范式?                                        │
  │     → 项目模板表:waterfall / scrum / kanban              │
  ├──────────────────────────────────────────────────────────┤
  │  2. 侧栏有什么?                                            │
  │     → 模块注册表 + 模板-模块关系表:某模板启用哪些模块      │
  ├──────────────────────────────────────────────────────────┤
  │  3. 能建什么工作项?                                        │
  │     → 工作项类型表:某模板下有哪些类型(故事/缺陷/任务...)  │
  ├──────────────────────────────────────────────────────────┤
  │  4. 表单与字段怎么走?                                      │
  │     → 类型-字段绑定表 + 字段定义表:某类型展示哪些字段      │
  └──────────────────────────────────────────────────────────┘

不存 具体需求/任务内容;业务在 biz_work_item 等表。配置层只决策略,具体单据在工作项表。

1.3 覆盖优先级

配置数据有一个重要的设计原则------覆盖优先级

复制代码
覆盖优先级(高 → 低)

  项目级 config_overrides  >  模板级 cfg_*  >  系统默认

这意味着:系统模板(waterfall/scrum/kanban)提供基线配置,企业可基于系统模板克隆出自定义模板,项目创建时绑定模板,还可以通过 config_overrides 在项目级别覆盖模板配置(如临时开启看板的迭代模块)。

二、数据模型

2.1 六张表的关系

配置中心当前已实现六张表,构成一条完整的配置链路:

markdown 复制代码
配置层数据模型

  cfg_project_template(项目模板表)
      │
      ├── cfg_template_module ── cfg_module(模块注册表)
      │       │
      │       └── 模板 × 模块 的启用矩阵
      │
      ├── cfg_work_item_type(工作项类型表)
      │       │
      │       └── cfg_type_field ── cfg_field(字段定义表)
      │                               │
      │                               └── 字典表(priority / severity ...)
      │
      └── 被 biz_project.template_id 引用
              │
              └── biz_project.config_overrides(项目级覆盖)

六张表各司其职:

表名 中文名 职责
cfg_project_template 项目模板表 定义瀑布/Scrum/看板等项目范式,系统内置 + 企业自定义
cfg_module 模块注册表 注册平台有哪些功能模块(全局清单)
cfg_template_module 模板-模块关系表 某模板启用哪些模块,及模块级私有配置
cfg_work_item_type 工作项类型表 某模板下有哪些类型(故事/缺陷/任务等)
cfg_field 字段定义表 全局可复用的字段元数据
cfg_type_field 类型-字段绑定表 某类型展示哪些字段、是否必填、是否可见

2.2 平台级数据的隔离特征

这六张表都是平台级数据 ------无 company_code 字段,不参与企业隔离。这意味着:

  • 系统模板(waterfall/scrum/kanban)全平台共享
  • 模块注册表全平台统一
  • 字段库全平台复用

这与第八篇介绍的三级查询分层一致:配置表走白名单豁免,TenantLineInnerInterceptor 不拦截。所有配置接口统一挂在平台级路径前缀下。

当前企业自定义模板的 company_code 隔离尚未实现------自定义模板虽然能创建,但全部企业可见。这是有意的简化:当前阶段配置模板的管理权在平台管理员,企业级模板隔离是后续需求。

三、项目模板管理

3.1 系统模板预置

平台预置三个系统模板:

模板编码 模板名称 项目模式 特点
waterfall 瀑布项目 waterfall 阶段化交付,基线驱动,含规划模块
scrum Scrum 敏捷项目 agile 迭代交付,价值驱动,含迭代模块
kanban 看板项目 agile 持续流动,WIP 驱动,迭代可后开

系统模板 systemFlag = true不可删除、不可停用 。自定义模板 systemFlag = false,可停用但已有项目绑定不受影响。

功能截图:

3.2 自定义模板的克隆机制

新建自定义模板时,不是从空白开始,而是从参考模板克隆

sql 复制代码
克隆流程

  用户提交(templateCode, templateName, projectMode)
    │
    ├─ 1. 校验编码格式 + 唯一性
    │
    ├─ 2. 按 projectMode 选择参考模板
    │      waterfall → 参考系统模板 waterfall
    │      agile    → 参考系统模板 scrum
    │      kanban   → 参考系统模板 kanban
    │
    ├─ 3. 创建模板实体(systemFlag = false)
    │
    ├─ 4. 克隆模块启用矩阵
    │      遍历参考模板的 cfg_template_module
    │      → 逐条 INSERT 到新模板下
    │
    └─ 5. 克隆工作项类型 + 字段绑定
           遍历参考模板的 cfg_work_item_type
           → 逐条 INSERT 类型
           → 遍历该类型的 cfg_type_field
           → 逐条 INSERT 字段绑定

克隆是事务性的------要么模块矩阵和类型绑定全部克隆成功,要么回滚。这保证了自定义模板的完整性:创建后立刻可用,不需要额外的配置步骤。

功能截图:

对应的实现代码:

csharp 复制代码
createTemplate(dto):
    validateCreate(dto)                          // 校验编码格式 + 名称
    if templateCode 已存在: throw "编码已存在"

    refCode = switch(dto.projectMode):            // 选择参考模板
        waterfall → "waterfall"
        kanban   → "kanban"
        default  → "scrum"
    ref = requireByCode(refCode)

    entity = new Template(systemFlag=false, ...)  // 创建自定义模板
    save(entity)
    cloneTemplateModules(ref.id, entity.id)       // 克隆模块启用矩阵
    cloneWorkItemTypes(ref.id, entity.id)          // 克隆类型 + 字段绑定
    return entity

3.3 模板启用/停用

自定义模板可以停用------停用后新建项目时不可选择此模板,但已绑定该模板的项目不受影响。系统模板不可停用:

ini 复制代码
updateTemplateStatus(templateCode, disabled):
    entity = requireByCode(templateCode)
    if disabled == true AND entity.systemFlag == true:
        throw "系统内置模板不可停用"
    entity.disabled = disabled
    updateById(entity)

四、模块注册与模板-模块矩阵

4.1 模块注册表

cfg_module 是全局模块清单------注册「平台有哪些模块」,与项目侧栏对应。不管某项目开不开某模块,开关在 cfg_template_module

模块字段说明:

字段 说明 示例
moduleCode 模块编码(唯一) requirement
moduleName 模块名称 需求
sidebarKey 前端侧栏 key req
moduleGroup 功能分组 execution
icon 图标 key list
sortOrder 排序 3
customFlag 是否自定义模块 false

模块分五个组:

分组 含义 典型模块
planning 规划 规划(甘特)
execution 执行 需求、任务、缺陷、迭代
release 发布 版本
quality 质量 测试
collaboration 协作 概览、代码评审、知识库、度量、成员、动态

4.2 模板-模块启用矩阵

cfg_template_module 回答「瀑布有没有迭代?」「敏捷有没有规划?」------是一张模板 × 模块的启用矩阵:

模块 瀑布 Scrum 看板
概览/需求/任务/缺陷/版本/评审/测试/知识库/度量/成员/动态
规划(甘特)
迭代 ❌(可后开)

关系表中的 config_json 字段用于存储模块私有配置(如看板列定义),当前预留未用。

功能截图:

4.3 模块开关的交互设计

模块注册页面以表格形式展示全部模块,每行末尾有三个开关分别对应瀑布/Scrum/看板三个系统模板。切换开关时弹出二次确认,提示影响范围:

功能截图:

arduino 复制代码
切换模块开关流程

  用户点击开关
    │
    ├─ 弹出确认框
    │   "确认在「Scrum」中启用「规划」模块?"
    │   "Scrum 项目侧栏将显示该模块"
    │
    ├─ 确认 → 调用切换模块开关接口
    │         后端 toggleModule 方法:
    │           1. 查找模板 + 模块
    │           2. 查找 template_module 关系记录
    │           3. 存在 → 更新 enabled 字段
    │           4. 不存在 → 新建关系记录
    │
    └─ 前端更新本地状态(无需重新加载整页)

五、字段库管理

5.1 字段定义表

cfg_field 是全局字段目录------可复用的字段元数据。枚举选项 在此维护,通过 dictTypeCode 引用字典(如 priority → P0-P3)。

字段类型支持六种:

字段类型 说明 典型用法
text 文本 变更影响分析
number 数字 故事点
date 日期 计划完成日期
enum 枚举 优先级(引用字典)
user 成员 测试负责人
rich_text 富文本 验收条件

字段分两类:

  • 系统字段systemFlag = true):不可删除,仅可在类型绑定中停用
  • 扩展字段systemFlag = false):可删除,删除前校验是否仍被类型绑定

功能截图:

5.2 字段与固定列的关系

标题、状态、负责人等主表固定列 一般不进 cfg_field 表------它们是 biz_work_item 的物理列,直接读写。cfg_field 偏扩展/可配置字段。

但有一个设计约定:baseline_version(基线版本)建议落在 biz_work_item 固定列,如果希望表单配置化,可以在 cfg_field 登记为 text,绑定到类型后表单上出现,但写入仍落固定列。fixedColumn 字段标识该字段是否对应业务表固定列。

5.3 删除字段的前置校验

删除字段前需检查是否仍被工作项类型绑定:

sql 复制代码
// 前端调用查询绑定数接口
countBound(fieldId):
    return SELECT COUNT(*) FROM cfg_type_field WHERE field_id = {fieldId}

如果绑定数 > 0,前端提示「字段仍被 N 个工作项类型绑定,请先在字段绑定中解绑」,不允许删除。

功能截图:

六、工作项类型自定义

工作项类型挂在模板下------同是「需求」入口,瀑布建「需求规格」,敏捷建「史诗/故事」。此前工作项类型只能在克隆模板时从参考模板复制,现在支持在模板详情页直接新增/编辑/删除自定义类型。

功能截图:

6.1 类型实体

cfg_work_item_type 的核心字段:

字段 说明 约束
templateId 所属模板 ID 必填
typeCode 类型编码 模板内唯一,小写字母+数字+下划线
typeName 类型名称 必填
category 分类 requirement / task / defect
icon 图标 key 可选,缺省按分类推导
color 标识色 可选,缺省按分类推导
tint 图标底色 可选,缺省 = color + "1A"
systemFlag 是否系统内置 系统类型不可修改/删除
allowSubItem 是否允许子工作项 布尔
estimateUnit 估算单位 hours / story_points
sortOrder 排序 新建时自动追加到末尾
description 描述 选填

6.2 新建类型

新建自定义类型的完整流程:

ini 复制代码
新建工作项类型流程

  前端 WorkItemTypeDialog(抽屉表单)
    │
    ├─ 用户填写:类型名称、编码、分类、估算单位、图标、标识色、描述
    │
    ├─ 前端校验:
    │   ├─ typeName 不能为空
    │   ├─ typeCode 格式 ^[a-z][a-z0-9_]{1,29}$
    │   ├─ typeCode 模板内不重复(前端缓存校验)
    │   └─ category 必选
    │
    ├─ 调用新建工作项类型接口
    │
    └─ 后端 createWorkItemType:
        ├─ 加载模板
        ├─ validateTypePayload(编码格式 + 分类枚举 + 估算单位)
        ├─ 编码唯一性校验(数据库)
        ├─ 计算排序值(当前最大 sortOrder + 1)
        ├─ 推导默认图标/颜色(用户未填时)
        ├─ systemFlag = false(自定义)
        ├─ INSERT 类型记录
        └─ 返回 VO(fields 为空数组)

功能截图:

后端校验的核心逻辑:

csharp 复制代码
validateTypePayload(dto, creating):
    if dto == null: throw "请求体不能为空"
    if dto.typeName 为空: throw "类型名称不能为空"

    if creating:                              // 仅新建时校验编码
        if dto.typeCode 为空: throw "类型编码不能为空"
        if dto.typeCode 不匹配 ^[a-z][a-z0-9_]{1,29}$:
            throw "类型编码格式不正确"

    if dto.category 不在 [requirement, task, defect] 中:
        throw "分类仅支持 requirement / task / defect"

    if dto.estimateUnit 为空: dto.estimateUnit = "hours"  // 缺省
    if dto.estimateUnit 不在 [hours, story_points] 中:
        throw "估算单位仅支持 hours / story_points"

6.3 默认图标与颜色的推导

用户不填图标和颜色时,按分类自动推导:

rust 复制代码
defaultIcon(category):
    defect  -> "bug"
    task    -> "check"
    default -> "list"

defaultColor(category):
    defect  -> "#F53F3F"   // 红色
    task    -> "#14C9C9"   // 青色
    default -> "#1677FF"   // 蓝色

底色 tint 的默认值为 color + "1A"------即在 HEX 色值后追加 1A 透明度(10%),实现图标背景半透明效果。

6.4 编辑与删除:系统类型保护

系统内置类型(systemFlag = true不可修改、不可删除

ini 复制代码
// 编辑
updateWorkItemType(templateCode, typeCode, dto):
    entity = requireType(templateCode, typeCode)
    if entity.systemFlag == true: throw "系统内置类型不可修改"
    validateTypePayload(dto, creating=false)
    applyTypeMeta(entity, dto)               // 写入可编辑元数据
    updateById(entity)

// 删除(事务)
deleteWorkItemType(templateCode, typeCode):
    entity = requireType(templateCode, typeCode)
    if entity.systemFlag == true: throw "系统内置类型不可删除"
    typeFieldMapper.delete(typeId=entity.id)  // 先删字段绑定
    deleteById(entity.id)                     // 再删类型

删除类型时先删字段绑定再删类型记录,保证引用完整性。删除操作是事务性的。

功能截图:

6.5 编辑模式下编码不可修改

编辑类型时,typeCode 输入框禁用------类型编码创建后不可修改。这是因为 typeCode 是前端路由参数和后端路径变量的一部分,修改编码会导致所有引用链路断裂。

前端表单通过 isEdit 状态控制:

ini 复制代码
<el-input
  v-model="form.typeCode"
  :disabled="isEdit"              // 编辑模式下禁用
  placeholder="小写字母 / 数字 / 下划线"
/>

七、类型-字段绑定与动态表单

7.1 绑定关系

cfg_type_field 回答「故事要不要故事点?缺陷要不要严重程度?」------控制某类型展示哪些字段、是否必填、是否可见、排序。

绑定示例:

工作项类型 字段 必填 可见
story story_point
story acceptance_criteria
bug severity
bug environment
多数类型 priority

7.2 整表覆盖式保存

类型-字段绑定采用整表覆盖策略------保存时先删除该类型的所有绑定,再按新列表逐条插入:

ini 复制代码
saveTypeFields(templateCode, typeCode, bindings):  // 事务
    type = requireType(templateCode, typeCode)
    DELETE FROM cfg_type_field WHERE work_item_type_id = type.id   // 先删
    if bindings 为空: return

    fieldMap = 查询全部字段,按 fieldCode 建索引
    sort = 1
    for binding in bindings:
        field = fieldMap[binding.fieldCode]
        if field == null: throw "字段不存在"
        INSERT cfg_type_field (
            work_item_type_id = type.id,
            field_id          = field.id,
            required          = binding.required,
            visible           = binding.visible ?? true,
            sort_order        = sort++
        )

为什么选整表覆盖而不是逐条 diff?因为字段绑定的排序、必填、可见三个维度经常同时变化,diff 逻辑复杂且容易出错。整表覆盖简单可靠------绑定数据量小(单类型通常 5-15 个字段),性能不是瓶颈。

7.3 字段绑定抽屉交互

前端使用 FieldBindDrawer 组件------从类型行点击打开,展示已绑定字段列表(每行有必填/可见开关和解绑按钮),底部列出字段库中未绑定的字段(点击即绑定)。保存时一次性提交全部绑定。

功能截图:

css 复制代码
字段绑定抽屉结构

  ┌────────────────────────────────────────────────┐
  │  [图标] 故事 · 字段绑定                          │
  │  story · 控制动态表单的可见 / 必填 / 默认值       │
  ├────────────────────────────────────────────────┤
  │  已绑定字段 · 3                                  │
  │  ┌────────────────────────────────────────┐    │
  │  │ 优先级          [dict:priority]         │    │
  │  │ 必填 [开关]   可见 [开关]   [解绑]       │    │
  │  ├────────────────────────────────────────┤    │
  │  │ 故事点          number                  │    │
  │  │ 必填 [开关]   可见 [开关]   [解绑]       │    │
  │  └────────────────────────────────────────┘    │
  │                                                  │
  │  从字段库添加 · 共 5 个                          │
  │  [+ 验收条件] [+ 严重程度] [+ 发现环境] ...     │
  ├────────────────────────────────────────────────┤
  │  已绑定 3 / 8 个字段            [完成]           │
  └────────────────────────────────────────────────┘

八、运行时配置读取链路

8.1 从项目到配置的解析

项目运行时需要读取配置------侧栏显示哪些模块、能建哪些工作项类型、某类型的表单字段有哪些。ProjectConfigServiceImpl 负责这个解析链路:

markdown 复制代码
运行时配置读取链路

  biz_project
    │
    ├─ template_id 不为空?
    │   ├─ 是 → 直接加载该模板
    │   └─ 否 → 按 project_mode 回落系统模板
    │            agile → scrum
    │            kanban → kanban
    │            其他 → waterfall
    │
    ├─ 读取模板配置(cfg_template_module)
    │   └─ 得到「模板启用了哪些模块」
    │
    ├─ 叠加项目级覆盖(config_overrides.modules)
    │   └─ 覆盖优先级:项目级 > 模板级
    │
    └─ 返回最终生效的模块列表

模板解析的回落逻辑:

arduino 复制代码
resolveTemplate(project):
    if project.templateId != null:
        template = findById(project.templateId)
        if template != null: return template

    // 按 project_mode 回落系统模板
    code = switch(project.projectMode):
        agile  -> "scrum"
        kanban -> "kanban"
        default -> "waterfall"
    fallback = findByCode(code)
    if fallback == null: throw "未找到可用项目模板"
    return fallback

8.2 模块叠加覆盖

项目级覆盖存储在 biz_project.config_overrides 字段(JSON),当前只支持模块开关覆盖:

json 复制代码
{
  "modules": {
    "iteration": true,     // 看板项目临时开启迭代
    "planning": false      // 关闭规划模块
  }
}

解析逻辑:

kotlin 复制代码
readModuleOverrides(configOverrides):
    if configOverrides 为空或 "{}": return {}
    try:
        root = JSON.parse(configOverrides)
        modules = root["modules"]
        if modules 是 Map:
            return { key: value for key, value in modules if value is Boolean }
    catch:
        log.warn("解析 config_overrides 失败")
    return {}

叠加时的优先级处理:

csharp 复制代码
for m in modules:
    enabled = enabledById.get(m.id)           // 模板级开关
    if m.moduleCode in overrides:            // 项目级覆盖优先
        enabled = overrides[m.moduleCode]
    // 构造 VO 返回

8.3 动态表单读取

读取某工作项类型的表单字段------这是运行时最关键的链路,决定了用户创建工作项时表单上显示什么:

ini 复制代码
getWorkItemTypeForm(projectId, typeCode):
    project  = requireProject(projectId)
    template = resolveTemplate(project)
    type     = findType(template.id, typeCode)
    if type == null: throw "工作项类型不存在"

    binds = SELECT * FROM cfg_type_field
            WHERE work_item_type_id = type.id
            ORDER BY sort_order
    if binds 为空: return []

    fieldMap = SELECT * FROM cfg_field WHERE id IN (binds.fieldIds)
               -> 按 id 建索引

    result = []
    for bind in binds:
        field = fieldMap[bind.fieldId]
        if field == null: continue
        result.add({
            fieldCode:  field.fieldCode,
            fieldName:  field.fieldName,
            fieldType:  field.fieldType,
            dictTypeCode: field.dictTypeCode,
            defaultValue: bind.defaultValue ?? field.defaultValue,  // 绑定级 > 字段级
            fixedColumn:  field.fixedColumn,
            required:  bind.required,
            visible:   bind.visible,
            sortOrder: bind.sortOrder,
        })
    return result

这里有一个默认值优先级 的设计:绑定级 cfg_type_field.defaultValue > 字段级 cfg_field.defaultValue。如果类型绑定时指定了默认值,覆盖字段定义的默认值。

九、前端交互设计

9.1 三个独立页面

配置中心前端由三个独立页面组成:

yaml 复制代码
配置中心页面结构

  设置中心 → 配置中心
    │
    ├─ 项目模板(TemplateList)
    │   ├─ 模板列表(卡片网格 + 搜索)
    │   ├─ 运行时链路卡片(RuntimeChainCard)
    │   ├─ 新建模板弹窗(NewTemplateDialog)
    │   └─ 模板详情页(TemplateDetail)
    │       ├─ Tab 1: 启用模块(分组展示 + 开关矩阵)
    │       └─ Tab 2: 工作项类型(类型列表 + 操作菜单)
    │           ├─ 字段绑定抽屉(FieldBindDrawer)
    │           └─ 类型编辑抽屉(WorkItemTypeDialog)
    │
    ├─ 模块注册(ModuleRegistry)
    │   ├─ 模块表格(分组标签 + 三模板开关矩阵)
    │   └─ 模块编辑弹窗(ModuleDialog)
    │
    └─ 字段库(FieldLibrary)
        ├─ 字段表格(类型标签 + 字典引用 + 固定列标记)
        └─ 字段编辑弹窗(FieldDialog)

9.2 模板列表页

模板列表以卡片网格展示,每张卡片包含:

  • 模板图标 + 名称 + 系统内置/自定义标签
  • 模板编码(monospace 字体)
  • 项目模式标签(瀑布/敏捷/看板)
  • 模板介绍
  • 统计信息(启用模块数 / 工作项类型数)
  • 两个快捷入口:模块配置、类型与字段

页面顶部展示 RuntimeChainCard------运行时链路卡片,用四个步骤直观展示配置链路:

复制代码
运行时链路

  1 项目模板  →  2 启用模块 · 决定侧栏  →  3 工作项类型  →  4 类型绑定字段 · 动态表单

  覆盖优先级 项目级 > 模板级 > 默认

9.3 模板详情页:双 Tab 设计

模板详情页使用 Tab 切换两个视图:

Tab 1 --- 启用模块:

  • 顶部 Banner:模板图标、名称、模式标签、系统/自定义标签、停用按钮
  • 模块按分组展示(规划/执行/发布/质量/协作),每组内模块以行排列
  • 每行显示模块图标、名称、编码、侧栏 key、排序、启用开关
  • 看板的「迭代」模块未启用时显示「可后开」提示

功能截图:

Tab 2 --- 工作项类型:

  • 顶部显示类型总数和「新增类型」按钮
  • 每个类型以行排列:图标、名称、分类标签、系统/自定义标签、字段数
  • 点击行打开字段绑定抽屉
  • 右侧下拉菜单:字段绑定、编辑信息(仅自定义)、删除(仅自定义)

功能截图:

9.4 状态管理与数据流

前端使用 useConfigCenter 作为统一的配置中心状态管理------维护 templates、modules、fields 三个 reactive 列表,所有操作(增删改查)都通过该 composable 完成:

php 复制代码
// 状态结构
state = reactive({
  templates: [],     // 项目模板列表(含模块开关 + 类型摘要)
  modules: [],       // 模块注册列表
  fields: [],        // 字段库列表
  loading: false,
})

// 操作列表
operations = {
  // 模板操作
  loadTemplates, refreshTemplate, createTemplate, updateTemplate, setTemplateDisabled,
  // 模块操作
  loadModules, createModule, updateModule, setTemplateModuleEnabled, toggleTemplateModule,
  // 字段操作
  loadFields, createField, updateField, deleteField, fieldBoundCount,
  // 类型操作
  createWorkItemType, updateWorkItemType, deleteWorkItemType,
  // 绑定操作
  saveTypeFields,
}

每次写操作完成后,调用 refreshTemplate 刷新单个模板的详情缓存------不需要重新加载全部模板列表,减少网络请求。

9.5 表单防脏机制

编辑类弹窗(新建模板、类型编辑)使用 useDrawerDirty 进行脏检测------表单内容变化后标记为 dirty,关闭抽屉时弹出确认提示。保存成功后清除 dirty 标记,直接关闭。

scss 复制代码
const { markClean, isDirty } = useDrawerDirty(() => form)

// 保存成功后
markClean()          // 清除脏标记
visible.value = false  // 关闭抽屉

十、API 设计

配置中心的后端 API 按功能分三组,统一挂在平台级路径前缀下。

10.1 项目模板 API

功能 说明
查询全部模板 GET 请求,返回所有项目模板列表
查询模板详情 GET 请求,按模板编码查询单个模板完整信息
新建模板 POST 请求,提交模板信息 + 参考模板编码,后端执行克隆逻辑
更新模板信息 PUT 请求,更新模板名称、描述等基础字段
启用/停用模板 PUT 请求,切换模板的 disabled 状态(系统模板不可停用)
切换模块开关 PUT 请求,在指定模板下启用或停用某模块
保存类型字段绑定 PUT 请求,整表覆盖某工作项类型的字段绑定
新建工作项类型 POST 请求,在指定模板下创建自定义类型
更新工作项类型 PUT 请求,更新类型的名称、图标、颜色等字段
删除工作项类型 DELETE 请求,删除自定义类型(系统类型不可删)

10.2 模块注册 API

功能 说明
查询全部模块 GET 请求,返回全局模块注册清单
新建模块 POST 请求,注册新的自定义模块
更新模块 PUT 请求,更新模块名称、图标、排序等

10.3 字段库 API

功能 说明
查询全部字段 GET 请求,返回全局字段元数据列表
新建字段 POST 请求,创建扩展字段定义
更新字段 PUT 请求,更新字段名称、类型、字典引用等
删除字段 DELETE 请求,删除扩展字段(删除前校验绑定数)
查询字段绑定数 GET 请求,查询某字段被多少工作项类型绑定

10.4 项目运行时 API

项目运行时配置读取通过企业视角的项目管理接口暴露:

功能 说明
查询项目有效模块列表 GET 请求,按企业 + 项目查询启用的模块
查询项目工作项类型 GET 请求,按企业 + 项目查询可用的工作项类型
查询动态表单字段 GET 请求,按企业 + 项目 + 类型编码查询表单字段定义

10.5 操作日志

配置中心的所有写操作都通过 @LogRecord 注解记录操作日志,日志类型为 TYPE_CONFIG_TEMPLATE / TYPE_CONFIG_MODULE / TYPE_CONFIG_FIELD,子类型为 SUB_CREATE / SUB_UPDATE / SUB_DELETE / SUB_STATUS

ini 复制代码
@LogRecord(
    success = "模板「{code}」新建工作项类型「{dto.typeName}」({dto.typeCode})",
    type    = CONFIG_TEMPLATE,
    subType = CREATE,
    bizNo   = "{code}"
)
// 新建工作项类型入口
createType(code, dto) -> templateService.createWorkItemType(code, dto)

模板更新和字段更新还支持字段级差异日志 ------通过 OperLogContext.DIFF_OLD_KEY / DIFF_NEW_KEY 记录变更前后的字段差异。

十一、权衡与反思

11.1 已实现的能力

能力 状态 说明
项目模板管理 系统模板预置 + 自定义模板克隆
模块注册与启用矩阵 全局模块清单 + 模板×模块开关
字段库管理 全局字段元数据 + 字典引用
工作项类型自定义 新增/编辑/删除 + 系统类型保护
类型-字段绑定 整表覆盖式保存
运行时动态表单 模板→类型→字段→表单
项目级配置覆盖 config_overrides.modules
操作日志 全部写操作记录 + 字段级差异

11.2 未实现与后续方向

规划项 状态 分析
模块级私有配置 config_json 字段已预留,未使用
企业级模板隔离 自定义模板全平台可见,无 company_code 隔离
项目级类型覆盖 当前只支持模块覆盖,不支持类型级覆盖
类型级默认值 cfg_type_field.defaultValue 字段存在但前端未暴露编辑入口

11.3 设计决策回顾

1. 为什么选整表覆盖而不是逐条 diff 保存字段绑定?

字段绑定的三个维度(必填、可见、排序)经常同时变化。如果做逐条 diff,需要比较三列差异,逻辑复杂且容易遗漏。整表覆盖简单------先删后插,一致性有保证。绑定数据量小(单类型通常 5-15 个字段),性能不是瓶颈。

2. 为什么工作项类型挂在模板下而不是全局?

同是「需求」入口,瀑布建「需求规格」,敏捷建「史诗/故事」。类型的分类、估算单位、是否允许子项都与项目范式相关。挂在模板下可以做到模板间的类型差异隔离,克隆模板时类型自动带过去。

3. 为什么自定义模板用克隆而不是从空白开始?

配置链路有四层(模板→模块→类型→字段),如果从空白开始,用户要逐层配置,体验差且容易遗漏。克隆参考模板可以一次性把模块矩阵和类型字段绑定全部带过来,创建后立刻可用,用户只需在差异点做调整。

4. 为什么 config_overrides 只支持模块覆盖?

当前业务场景中,项目级覆盖需求最高的是模块开关------比如看板项目临时开迭代。类型和字段绑定的项目级覆盖需求尚未出现,且实现复杂度高(需要项目维度的类型副本)。保持简单,按需扩展。

5. 为什么配置表不做企业隔离?

当前阶段配置模板的管理权在平台管理员,企业级模板隔离增加了数据模型复杂度(每张表都加 company_code + 隔离逻辑),但当前没有明确的业务需求驱动。等有多租户企业自定义模板的强需求时再实现。

十二、总结

配置中心的核心设计可以浓缩为一条链路:

复制代码
选模板 → 启用模块(侧栏)→ 可用工作项类型 → 类型绑定字段(动态表单)

六张表各司其职:cfg_project_template 定义项目范式,cfg_module 注册全局模块清单,cfg_template_module 控制模板×模块启用矩阵,cfg_work_item_type 定义模板下的工作项类型,cfg_field 管理全局字段元数据,cfg_type_field 绑定类型与字段的可见/必填/排序。

覆盖优先级机制(项目级 > 模板级 > 默认)让配置既统一又灵活------系统模板提供基线,自定义模板可克隆调整,项目级可按需覆盖。

工作项类型自定义功能补全了配置链路的最后一环------此前类型只能从参考模板复制,现在支持在模板详情页直接新增/编辑/删除自定义类型,配合字段绑定抽屉完成表单配置的闭环。系统类型保护机制确保预置类型不被误删。

相关推荐
吃饱了得干活1 小时前
MySQL 进阶:锁与事务、执行计划、内存管理、高可用架构及 8.0 新特性
后端·mysql
Json____1 小时前
基于 FastAPI + Vue3 的高校选课管理系统技术解析
后端·fastapi·wwwoop.com
创新技术阁1 小时前
FastapiAdmin 定时任务实现原理与新建任务实操指南
前端·后端·fastapi
嘻哈baby1 小时前
高并发下怎么做余额扣减?
后端
SimonKing1 小时前
Java 图片处理还在用 ImageIO?这个库让你代码从 30 行变 3 行
java·后端·程序员
嘻哈baby1 小时前
Rust 是不是就相当于新时代的 C 语言?
后端
IT_陈寒2 小时前
Redis持久化配置漏了这一步,线上数据丢了5小时
前端·人工智能·后端
Moment2 小时前
太好了!NestJS 12 大版本转向 ESM,新项目默认构建换 Rspack
前端·javascript·后端
名字还没想好☜2 小时前
Go 用 -race 抓数据竞争:一个偶发崩溃的排查、原理与修复
开发语言·后端·golang·go