You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

18 KiB

数据字典模块(crm-dict)

Status: ready-for-agent

Problem Statement

CRM 业务模块里散落着大量枚举取值——商机阶段/状态、关闭原因、暂缓原因、客户来源、行业分类等。目前这些取值要么硬编码在代码里,要么散落在各业务表的枚举字段中,改一个文案要发版、加一个选项要改代码。平台超级管理员无法在运行时统一维护这些"字典",业务规则模块和外部接口也没有一个稳定的编码体系可以取值。用户需要一套可视化的、两级结构(分组 → 字典项)的数据字典管理,让业务取值与展示文案解耦、稳定编码供各方引用、运行时可维护而无需发版。

Solution

新建独立模块 crm-dict,实现"系统设置 > 数据字典"功能。提供两个页签:字典分组(第一级容器)和字典列表(第二级键值项)。管理员可对分组和字典项做增删改查、启用/停用、设默认、排序。业务模块通过稳定的字典编码引用字典项(不存名称、不存主键),页面展示时按编码取最新名称。所有写操作服务端强制校验唯一性、引用关系、状态合法性;访问控制走权限点(dict:*)。字典读路径走 Caffeine 本地缓存(10 秒 TTL)扛高频读,引用计数走实时查询保证删除判定准确。

架构决策全部记录在 ADR-0015docs/adr/0015-data-dictionary-crm-dict-two-level.md);领域术语见 crm-dict/CONTEXT.md。本 spec 是 ADR 的下位实现说明,不重复论证已定架构,直接引用。

User Stories

字典分组管理

  1. 作为管理员,我希望分页查看字典分组列表,以便浏览系统中所有分组
  2. 作为管理员,我希望看到每个分组的字典数量(该分组下未删除的字典项数,含启用+停用),以便了解分组规模
  3. 作为管理员,我希望按分组名称/编码搜索分组,以便快速定位
  4. 作为管理员,我希望按状态(启用/停用)筛选分组,以便聚焦有效分组
  5. 作为管理员,我希望新建字典分组并填写名称(必填)、编码(必填)、排序号(选填)、描述(选填),以便定义一类字典
  6. 作为管理员,我希望分组编码未填时系统自动生成,以便省去手工命名作废,Q1 决策:编码不自动生成,管理员必填
  7. 作为管理员,我希望分组编码创建后不可修改,以便作为稳定标识不被破坏
  8. 作为管理员,我希望分组编码全局唯一被强制校验,以便两个分组不会共享编码
  9. 作为管理员,我希望编辑分组的名称、排序号、描述,以便维护分组信息
  10. 作为管理员,我希望启用/停用分组(需二次确认),以便控制分组是否对业务生效
  11. 作为管理员,我希望分组停用后,其下所有字典项都不再进入业务选择器(即使字典项自身是启用状态),以便临时下线整组取值
  12. 作为管理员,我希望删除分组仅在该分组下无任何未删除字典项时才允许,否则被阻断并提示,以便不会误删仍有内容的分组
  13. 作为管理员,我希望内置分组(builtin=true)不可删除,但可改名/改描述/调排序/启停,以便平台字典不被误删而仍可本地化

字典项管理

  1. 作为管理员,我希望切到"字典列表"页签查看字典项,以便管理具体键值条目
  2. 作为管理员,我希望按分组筛选字典项列表,以便查看某分组下的所有项
  3. 作为管理员,我希望按字典名称/编码/值搜索字典项,以便快速定位
  4. 作为管理员,我希望按状态筛选字典项,以便聚焦有效项
  5. 作为管理员,我希望新建字典项并选择所属分组(必填,只能选启用状态的分组)、填写名称(必填)、编码(选填自动生成)、值(选填)、排序号(选填)、描述(选填),以便定义一条取值
  6. 作为管理员,我希望字典编码未填时按 dict_ + 32 位小写十六进制 UUID 自动生成,以便省去命名
  7. 作为管理员,我希望字典编码在其所属分组内唯一被强制校验,以便同组内编码不重复
  8. 作为管理员,我希望字典编码创建后不可修改,以便作为业务引用的稳定标识
  9. 作为管理员,我希望字典项创建后所属分组也不可修改,以便身份稳定、历史引用不错乱(比 spec 更严,见 ADR-0015)
  10. 作为管理员,我希望字典值在同一分组内唯一被校验(空值不参与校验),以便同组取值不冲突
  11. 作为管理员,我希望编辑字典项名称,且改名后所有历史业务数据展示最新名称,以便文案调整无需改历史数据
  12. 作为管理员,我希望未被引用的字典项其字典值可以修改,被引用后字典值只读并提示,以便被业务依赖的取值不被随意改动
  13. 作为管理员,我希望把某字典项设为分组默认项,系统自动取消同组原默认项(一次事务完成),以便每组至多一个默认
  14. 作为管理员,我希望字典项停用时若它是默认项则自动取消默认状态,以便停用项不再作为新建数据的初始值
  15. 作为管理员,我希望分组停用时其默认项关系被保留(休眠),分组重新启用后原默认项继续生效(若其自身仍启用),以便临时下线不丢默认配置
  16. 作为管理员,我希望删除未被引用的字典项可直接删除(不要求先停用),以便清理无用项
  17. 作为管理员,我希望删除被引用的字典项时被阻断并提示"该字典已被引用,无法删除",以便不破坏历史数据
  18. 作为管理员,我希望内置字典项(builtin=true)不可删除,但可改名/改描述/调排序/启停,以便平台硬依赖的字典不被误删
  19. 作为管理员,我希望字典项列表中对"自身启用但所属分组已停用"的失活项有视觉标记(角标/灰化),以便一眼看出它当前不可用

业务侧取值

  1. 作为业务模块开发者,我希望通过 (分组编码, 字典编码) 反查唯一字典项,以便业务字段按编码取值
  2. 作为业务模块开发者,我希望查询某分组下的启用字典项列表(供业务选择器渲染),只返回分组和项都启用的项,以便下拉只出现有效选项
  3. 作为业务模块开发者,我希望编辑历史业务数据时原字典值回显,若该项已停用则回显并标注"已停用"、清除后不可再选,以便历史数据可见但不误导
  4. 作为业务模块开发者,我希望在保存业务数据(引用字典编码)的同一事务里登记引用计数(+1),以便字典侧能准确判定"被引用"
  5. 作为业务模块开发者,我希望在清除/改动业务字段的字典编码时同事务扣减引用计数(-1),以便计数反映真实引用
  6. 作为业务模块开发者,我希望批量导入业务数据时走"应用层聚合 delta + 单条 UPDATE"更新引用计数,以便不产生行锁热点导致吞吐塌方

权限与初始化

  1. 作为平台,我希望数据字典所有接口走权限点(dict:*)控制,配了权限的角色才能访问,以便权限统一由 RBAC 管理
  2. 作为部署者,我希望首批内置字典由产品提供的 Excel 通过幂等初始化脚本导入,任何时候可重跑,以便各环境字典一致
  3. 作为部署者,我希望初始化脚本只 upsert builtin=true 的字典、绝不触碰 builtin=false 的用户字典,以便管理员手建的数据不被覆盖
  4. 作为部署者,我希望初始化脚本只做加/改不做删,Excel 里消失的字典项不被自动删除,以便删除决策始终走人工界面
  5. 作为部署者,我希望导入前校验编码唯一、同组字典值不重、引用分组存在、每组至多一个启用默认项、字段格式合法,以便脏数据不进库

运维兜底

  1. 作为管理员,我希望在引用计数因故飘高、导致实际无引用的字典项无法删除时,有一个"强制删除"入口绕过 ref_count 校验,以便手工判定后仍能清理
  2. 作为开发者,我希望字典读路径走本地缓存(10 秒 TTL)扛高频读,以便不给数据库造成压力
  3. 作为开发者,我希望引用计数永不进缓存,删除判定读实时值,以便不因缓存导致误删/误阻断

Implementation Decisions

全部架构决策见 ADR-0015。以下为落地实现要点,不重复论证。

模块与依赖

  • 新建 Maven 模块 crm-dictpackaging=jar,仅依赖 crm-base(不 import crm-auth 任何类)。在 crm-app/pom.xml 注册依赖,在根 pom.xml 注册模块,在 CONTEXT-MAP.md 已登记。
  • 包结构对齐现有模块:controller/domain/{dto,entity,enums,param}/mapper/service/impl/config/
  • 实体继承 BaseEntity(自带雪花 id、审计字段、@TableLogic deleted)。

数据模型(Schema)

  • dict_group(字典分组):namecode(全局唯一)、sort_no(可空)、statusdescriptionbuiltindelete_key。唯一索引 unique(code, delete_key)
  • dict_item(字典项):group_idnamecodevalue(可空)、sort_no(可空)、statusdescriptionbuiltindelete_key。唯一索引 unique(group_id, code, delete_key)unique(group_id, value, delete_key)(空值不参与)。不含 is_default、不含 parent_id
  • dict_group_default(分组默认项):group_id(主键)、item_id。以主键约束保证每组最多一个默认项——"每组一默认"从业务规则升级为 DB 结构性约束。
  • dict_ref_count(引用计数):(group_code, item_code) + ref_count。判定被引用 = ref_count > 0。只记"有没有",不记"被谁"。
  • 软删除可复用delete_key 存活时固定 0,逻辑删除时写为该行主键 id;删除操作同事务写 deleted=1 + delete_key=id,不能只靠 @TableLogic 自动填充。

唯一性与引用契约(ADR-0015)

  • 分组编码全局唯一;字典编码分组内唯一;字典值同组唯一(空值不校验)。
  • 业务数据只存字典编码(不存 id、不存名称);定位需 (分组编码, 字典编码),分组由字段-分组绑定在设计期隐式提供,运行时只存编码一列。
  • 一个业务字段只绑定一个字典分组,禁止跨分组并集下拉;需合并选项时另建合并分组。

默认项与状态语义(ADR-0015)

  • 设默认 = dict_group_defaultINSERT ... ON DUPLICATE KEY UPDATE item_id=?(同库事务),无需应用层"再校验+行锁"。
  • 查"当前默认项"的服务方法内部联查 group.status,分组停用返回 null(分组状态是权威真相源,dict_group_default 只是记录)。
  • 有效可选 = group.status==enabled AND item.status==enabled;服务层按需联查两状态。
  • 字典项停用 → 同事务删 dict_group_default 行;分组停用 → 保留该行(休眠默认)。

引用计数(ADR-0015)

  • 同步 + 同库事务:业务写入与 ref_count 增减在同一 JDBC 事务内,非异步、不走 MQ。
  • 批量/导入场景强制"应用层聚合 delta + 单条 UPDATE",不允许循环里逐条 upsert(防行锁热点)。
  • 计数飘高兜底:提供管理员强制删除入口绕过 ref_count 校验;不引入离线对账任务(对账需知"哪些表哪些字段引用哪分组",会请回反向依赖)。

缓存(ADR-0015)

  • Caffeine 本地缓存 + expire-after-write=10s,可缓存分组/字典项元数据、启用项列表、当前默认项;空结果缓存 2 秒防穿透。
  • dict_ref_count 绝不缓存——靠 API 分层守护:DictQueryService(带缓存、业务侧读元数据)与 DictReferenceService(不带缓存、删除校验读实时计数)方法签名级分开,业务侧根本读不到 ref_count。
  • 变更写入不主动 evict,接受多副本 10 秒最终一致。

权限(ADR-0015)

  • 所有接口走权限点:@PreAuthorize("hasAuthority('dict:...')"),权限码字符串是与 auth 体系的唯一契约。不引入"超级管理员"概念。
  • 权限码建议:dict:group:listdict:group:savedict:group:deletedict:group:statusdict:item:listdict:item:savedict:item:deletedict:item:statusdict:item:defaultdict:item:force-delete。权限点通过幂等初始化器种子化到权限资源树并授予管理员角色(参照 role-management 的做法)。

初始化(ADR-0015)

  • 幂等初始化脚本按 (group_code, item_code) upsert,只管 builtin=true,绝不触碰 builtin=false
  • Excel 只加/改不删;builtin 创建后不可改(不能升降级)。
  • 导入前校验:编码唯一、同组值不重、引用分组存在、每组≤1个启用默认项、字段格式合法。

API 契约

遵循全局接口契约(非严格 RESTful,POST 用 application/x-www-form-urlencoded,GET 参数走 query,id 按字符串收发)。详细请求/响应见前端集成文档 docs/frontend-integration-data-dictionary.md。端点清单:

端点 方法 权限码 说明
/api/dict/group/page POST dict:group:list 分组分页,current/size/keyword/status
/api/dict/group/saveOrUpdate POST dict:group:save 分组保存(新建编码必填且全局唯一;编辑 code 只读)
/api/dict/group/delete POST dict:group:delete 删分组(有项则阻断;builtin 阻断)
/api/dict/group/status POST dict:group:status 分组启停(二次确认在前端)
/api/dict/group/enabled-list GET dict:group:list 启用分组列表(新建字典项选分组用)
/api/dict/item/page POST dict:item:list 字典项分页,groupId/keyword/status/分页
/api/dict/item/saveOrUpdate POST dict:item:save 字典项保存(分组/编码创建后只读;值被引用后只读)
/api/dict/item/delete POST dict:item:delete 删字典项(被引用则阻断;builtin 阻断)
/api/dict/item/force-delete POST dict:item:force-delete 强制删除(绕过 ref_count 校验)
/api/dict/item/status POST dict:item:status 字典项启停(停用连带取消默认)
/api/dict/item/set-default POST dict:item:default 设为分组默认项(同事务清原默认)
/api/dict/item/enabled-list GET dict:item:list 某分组启用项列表(业务选择器渲染用)

对业务模块暴露的内部 API(非 HTTP)

  • DictQueryService(带缓存):按 (groupCode, itemCode) 查项、查分组启用项列表、查分组当前默认项。业务侧渲染/校验用。
  • DictReferenceService(不带缓存):increment(groupCode, itemCode) / decrement(...) / batchApply(Map delta) / isReferenced(groupCode, itemCode)。业务侧写路径 + 字典删除校验用。这是业务模块与 crm-dict 的写路径契约

Testing Decisions

好的测试只验证外部行为,不绑定实现细节。断言"跳过 Controller 也拦不住的规则确实被服务端强制",不断言"某方法被调了几次"。

Seam 1(主):HTTP 层集成测试(MockMvc)

  • @SpringBootTest + MockMvc,请求从 HTTP 进 → service → MyBatis → 测试数据源 → 出参。
  • 覆盖:分组/字典项 CRUD、编码全局/组内唯一、字典值同组唯一、编码/分组创建后只读、被引用阻断删除、被引用后字典值只读、默认项互斥(设一个自动取消原默认)、分组停用后其项不进 enabled-list、字典项停用连带取消默认、分组停用保留休眠默认+重启恢复、builtin 不可删可改、软删后同编码可复用、权限点缺失返回无权限。
  • Prior artcrm-auth/service/impl/UserListIntegrationTest.javacrm-auth/security/scope/DataScopeIntegrationTest.java

Seam 2(辅):DictReferenceService 单元/集成测试

  • 独立测引用计数写路径契约:increment/decrement 事务原子性、被引用判定、batchApply 聚合、强制删除绕过校验。
  • 用同 seam 1 的测试基础设施,只测该 service。
  • 为何独立:这条路径业务模块也要调,是对外写路径 API,需独立契约测试;并发/事务语义在 Controller 层不好构造。

明确不做

  • 不单测 Mapper(SQL 正确性由 seam 1 顺带覆盖)。
  • 不单测 Caffeine 本身(黑盒);缓存"改后重查看到新值(可能 10s 延迟)"由 seam 1 借时钟抽象验证。

Out of Scope

  • 多级/树形字典:明确不做(ADR-0015)。字典项不加 parent_id。行业等"联动"需求用两个平铺分组 + value 承载父引用,或将来另建关联表,属另一 spec。
  • 字典项换分组:主动收窄为不支持(ADR-0015),换分组=删旧建新。
  • 审计日志(谁改了什么字典):属 crm-audit 模块职责,不在本 spec。
  • 字典名称国际化/多语言:spec 未要求,不实现。
  • 拖拽排序:后端只接受手填排序号;拖拽是前端功能,前端可自行在保存前重算所有排序号批量提交。
  • 导出功能:若原型有"导出全部字典",作为后续增量,不在本期。
  • Redis 分布式缓存失效广播:本期只用 Caffeine 本地缓存 + 短 TTL;多副本秒级一致性升级为后续(ADR-0015 已预留升级路径)。
  • 前端 UI 实现:见独立的前端集成文档 docs/frontend-integration-data-dictionary.md

Further Notes

  • 需求来源:蓝湖 Axure 原型「itc信息化业务中台 web端 V1.0-202607」页面 A7-4-1 数据字典(pageId 22f643af80fb452fa396b9ee166587e4)。
  • 领域语言以 crm-dict/CONTEXT.md 为准;本 spec 与前端文档中的术语(字典分组/字典项/字典编码/字典值/默认项/被引用/业务选择器/字段-分组绑定/内置字典/引用计数/强制删除)均引用该 glossary。
  • 架构取舍全部记录在 ADR-0015,实现时若发现 ADR 与 spec 冲突,以 ADR 为准并回来修订本 spec。
  • 前后端两份文档分工:本 spec 面向后端接口实现;docs/frontend-integration-data-dictionary.md 面向前端做原型与联调,含完整请求/响应示例、数据模型、交互态。