项目是研发管理的执行单元,但「项目长什么样」------用瀑布还是敏捷?侧栏有哪些模块?能建哪些工作项?表单上要填哪些字段?------这些问题不应该硬编码在代码里,也不应该每建一个项目都从零配一遍。配置中心解决的就是这件事:把项目范式、模块开关、工作项类型、字段表单抽象成配置数据,让模板一次性定义、项目直接消费。本篇记录配置中心六张表的数据模型、四层配置链路、工作项类型自定义、动态表单渲染,以及前端三个独立页面与模板详情双 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 绑定类型与字段的可见/必填/排序。
覆盖优先级机制(项目级 > 模板级 > 默认)让配置既统一又灵活------系统模板提供基线,自定义模板可克隆调整,项目级可按需覆盖。
工作项类型自定义功能补全了配置链路的最后一环------此前类型只能从参考模板复制,现在支持在模板详情页直接新增/编辑/删除自定义类型,配合字段绑定抽屉完成表单配置的闭环。系统类型保护机制确保预置类型不被误删。