spec.md 5.5 KB

字典数据键值唯一性校验

status: done created: 2026-08-04 complexity: 🟢简单

1. 背景与目标

字典数据表已经通过数据库唯一索引限制同一租户、同一字典类型下的有效 dict_value 不重复,但新增或修改重复值时只能依赖数据库异常,前端无法获得稳定、清晰的业务提示。

本变更在字典数据新增和修改入口增加业务唯一性校验:同一租户、同一 dict_type 下,未删除字典项的 dict_value 不能重复;修改时排除当前字典项自身。数据库唯一索引继续作为并发写入的最终防线。

2. 代码现状(Research Findings)

2.1 相关入口与链路

  • forge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/controller/SysDictDataController.javaaddedit 分别调用 insertDictDataupdateDictData
  • forge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/service/impl/SysDictDataServiceImpl.java:当前新增、修改直接调用 MyBatis-Plus insert / updateById,没有业务唯一性预检。
  • forge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/mapper/SysDictDataMapper.java:当前没有字典键值重复查询方法。

2.2 现有数据库约束

  • forge-server/db/migration/V1.0.51__replace_logic_delete_generated_columns.sql 已建立 uk_tenant_dict_data_active (tenant_id, dict_type, dict_value, del_flag)
  • SysDictData.delFlag 使用 @TableLogic(value = "0", delval = "dict_code"),有效数据为 del_flag = 0,删除后允许同值重建。

2.3 发现与风险

  • 只做 Service 预查询不能消除并发竞态,因此不能替代现有数据库唯一索引。
  • 查询 SQL 必须写入 Mapper XML,并显式过滤 del_flag = 0;租户条件由租户拦截器追加。
  • 修改校验必须通过 dict_code <> 当前值 排除自身,否则仅修改标签等其它属性也会被误判重复。

3. 功能点

  • 新增字典项时,若同一字典类型已存在相同键值,返回业务异常“同一字典下的字典键值不能重复”。
  • 修改字典项时,排除当前 dictCode 后执行相同校验。
  • 非重复新增、修改保持原保存和字典缓存刷新行为。
  • 保留数据库唯一索引作为并发最终防线。

4. 业务规则

  1. “同一个字典”以当前租户内的 dictType 标识。
  2. 唯一键为当前租户下 dictType + dictValue,只统计 del_flag = 0 的有效记录。
  3. 修改时排除当前 dictCode
  4. 值比较遵循数据库列现有字符集与排序规则,不在本变更中改变大小写、空格或存储值协议。
  5. 已逻辑删除记录不阻止相同键值重新创建。

5. 数据变更

操作 表名 字段/索引 说明
sys_dict_data 复用 uk_tenant_dict_data_active 不新增或修改 Flyway 脚本

6. 接口变更

操作 接口 方法 变更内容
行为优化 /system/dict/data/add POST 重复键值返回明确业务异常
行为优化 /system/dict/data/edit POST 排除自身后校验重复键值

请求和成功响应协议不变。

7. 影响范围

  • forge-plugin-system 字典数据 Mapper、Mapper XML、Service。
  • 字典数据 Service 单元测试。
  • 不修改前端表单、接口路径和数据库结构。

8. 风险与关注点

  • Service 预检与写入之间仍存在并发窗口,数据库唯一索引负责最终拒绝重复数据。
  • 本变更不涉及资金、状态流转或权限放开。

8.5 测试策略

  • 测试范围:新增重复、修改重复、修改排除自身、非重复写入。
  • 覆盖率目标:覆盖 insertDictDataupdateDictData 新增分支及 Mapper 参数传递。
  • 独立 Test Spec:是,见 test-spec.md

9. 待澄清

无。按用户原始要求将“同一个字典”解释为同一租户下相同 dictType

10. 技术决策

  1. 使用 Mapper XML countByDictTypeAndValue(dictType, dictValue, excludeDictCode) 完成业务预检,遵循数据权限可审查约定。
  2. Service 统一通过私有方法抛出 BusinessException,新增和修改复用相同提示。
  3. 不新增数据库脚本,复用已经存在的有效记录唯一索引处理并发竞态。

11. 执行日志

Task 状态 实际改动文件 备注
Proposal 完成 spec.mdtasks.mdtest-spec.mdexecution-log.md 已完成现状和唯一索引核对
Task 1 完成 SysDictDataServiceImplTest.javaSysDictDataMapper.java/xmlSysDictDataServiceImpl.java Red 4/4 失败;Green 4/4 通过
Task 2 完成 本变更四份 SDD 文档 系统插件全量单测 56/56 通过,静态检查通过

12. 审查结论

  • Spec 覆盖自检:新增、修改、排除自身、非重复保存和数据库并发兜底均已覆盖。
  • 代码质量自检:查询位于 Mapper XML,显式过滤 del_flag = 0,无前端/接口/数据库结构变更。
  • 验证结论:定向测试和 forge-plugin-system 全量测试通过;真实 API/数据库 E2E 未执行并已记录为跳过项。

13. 确认记录(HARD-GATE)

  • 确认时间:2026-08-04
  • 确认人:用户
  • 确认内容:用户明确提出“同一个字典下面字典的值不能有重复”,授权实现该校验。