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.
 
 
 
 
 
 

17 KiB

crm-dict 代码功能与接口现状盘点(票 02 资产)

日期:2026-09-07。所有行号基于当前工作区代码。路径缩写:dict/ = crm-dict/src/main/java/com/crm/dict/

1. 端点表

1.1 DictGroupController(dict/controller/DictGroupController.java/api/dict/group

@Tag(name = "A7 后台管理/数据字典-分组")(DictGroupController.java:24)

端点 方法 权限码 入参 出参
/page POST dict:group:list GroupPageParam(表单绑定,见 1.3) PageResult<DictGroupDTO>(见 1.4)
/saveOrUpdate POST dict:group:save DictGroupDTO(表单绑定,双向 DTO) Void
/delete POST dict:group:delete id: Long(@RequestParam,必填) Void
/status POST dict:group:status id: Longstatus: Integer(1/0) Void
/enabled-list GET 无 @PreAuthorize(登录即可读) 无参 List<DictGroupDTO>

1.2 DictItemController(dict/controller/DictItemController.java/api/dict/item

@Tag(name = "A7 后台管理/数据字典-字典项")(DictItemController.java:25)

端点 方法 权限码 入参 出参
/page POST dict:item:list ItemPageParam(表单绑定,见 1.3) PageResult<DictItemDTO>(见 1.5)
/saveOrUpdate POST dict:item:save DictItemDTO(表单绑定,双向 DTO) Void
/delete POST dict:item:delete id: Long Void
/force-delete POST dict:item:force-delete id: Long Void
/status POST dict:item:status id: Longstatus: Integer(1/0) Void
/set-default POST dict:item:default groupId: LongitemId: Long Void
/enabled-list GET @PreAuthorize groupCode: String(@RequestParam,必填) List<DictItemDTO>

合计 12 个端点(分组 5 + 字典项 7),全部扁平动作动词风格、无 @PathVariable/@PutMap/@DeleteMap/@RequestBody,符合 ADR-0017。

1.3 分页 Param 字段

通用父类 BaseParam(crm-base domain/param/BaseParam.java:14-27):current: Integer=1size: Integer=10(上限 500)、keyword: StringorderBy: Stringasc: Boolean=false注意:分组/字典项两个分页实现均把 orderBy/asc 强制置 null(见 §2.8),即这两个参数收了但不用。

Param 自有字段 说明
GroupPageParam(dict/domain/param/GroupPageParam.java:16) status: Integer 1=启用 0=停用,空=全部
ItemPageParam(dict/domain/param/ItemPageParam.java:15-19) groupId: Longstatus: Integer groupId 空=全部

现状过滤参数缺口:无按 builtinisDefaultparentId(层级)过滤的参数。

1.4 DictGroupDTO 字段(dict/domain/dto/DictGroupDTO.java:16-31)

继承 BaseDTO(id/createTime/updateTime,NON_NULL 序列化)。自有:namecodesortNostatusdescriptionbuiltin: BooleanitemCount: Long(未删除字典项数,含启用+停用,Service 聚合填充)。

1.5 DictItemDTO 字段(dict/domain/dto/DictItemDTO.java:18-52)

继承 BaseDTO。自有:groupIdparentId(两级树:顶层 null)、namecodevaluesortNostatusdescriptionbuiltingroupNameisDefaultreferenced(实时引用计数>0)、effectiveSelectable(分组+项+父项均启用)、children(仅 listTree 填充)。

2. 服务规则

2.1 分组 saveOrUpdate(dict/service/impl/DictGroupServiceImpl.java:67-108

校验 位置 规则
dto 非空 :69 null 抛 63001
name 必填 :75 空抛 63001
sortNo 非负 :78 <0 抛 63001
新建 code 必填 :84 空抛 63001
code 格式 :87 ^[A-Za-z0-9_]+$(DictConstants.java:22)
code 全局唯一 :90 count 重复抛 63002;deleted 带 @TableLogic(BaseEntity.java:65)自动过滤软删行,且唯一索引挂 (code, delete_key)(DictGroup.java:20),软删后编码可复用
新建默认值 :95-97 status=1、builtin 由 InitBinder strip→DB false、deleteKey=0
编辑只读 :102-105 code/status 强制保留原值;builtin/createTime/creatorId null 跳过自动保留

2.2 字典项 saveOrUpdate(dict/service/impl/DictItemServiceImpl.java:76-173

校验 位置 规则
groupId 必选(新建) :85-88 新建空抛 63001;编辑可省略(身份只读)
换组拦截 :91-94, :157-160 创建后改组抛 63008
name 必填 :96 空抛 63001
sortNo 非负 :99 <0 抛 63001
value 空白归一 null :103-105 空值不参与同组唯一
parentId 自引用 :111-113 抛 63001
父项存在 :114-117 不存在抛 63010
父项同组 :118-120 跨组抛 63001
父项必须一级 :121-123 parent.parentId 非空抛 63001(仅两级)
新建分组可用 :127-131 分组不存在抛 63011;停用抛 63005
code 自动生成 :133-135 空 → dict_+32 位 hex UUID
code 格式+组内唯一 :137-142 抛 63001 / 63006(跨组可重复)
value 组内唯一 :143, :170 抛 63007
新建默认值 :144-146 status=1、deleteKey=0
同事务预建计数行 :148-153 insert dict_ref_count(ref_count=0),保证 increment 有行可加
编辑 code 只读 :161-164 改 code 抛 63008
value 被引用只读 :169, :262-271 值变化且 isReferenced 抛 63009(US-25)

2.3 默认项处理

场景 行为 位置
设默认 setDefault groupId/itemId 必填(63012);分组启用(63005)、项属同组(63012)、项启用(63012);INSERT ... ON DUPLICATE KEY UPDATE item_id(dict_group_default 主键=group_id),同事务覆盖旧默认 DictItemServiceImpl.java:202-226;DictGroupDefaultMapper.java:15-17
停用字典项时对默认项 自动取消默认(同事务删除 dict_group_default 行),不阻断停用 DictItemServiceImpl.java:196-199 → removeDefaultIfPresent(:299-304);US-27
停用分组时对默认项 保留默认记录(休眠),不取消;重新启用后自动恢复;getDefaultItem 读时以分组状态为权威:停用分组返回 null DictGroupServiceImpl.java:142(休眠注释);DictQueryServiceImpl.java:156-182(:166-172 权威真相源);US-28
删除字典项 同事务清默认记录(若是默认) DictItemServiceImpl.java:255
删除分组 同事务清默认记录 DictGroupServiceImpl.java:129-130

2.4 删除与 force-delete

操作 阻断条件 位置
删分组 builtin(63003);组下有未删除字典项(63004) DictGroupServiceImpl.java:110-122
删分组附带 软删(deleted=1+delete_key=id,编码可复用)+ 清默认 + 清 dict_ref_count(按 group_code) DictGroupServiceImpl.java:124-132
删字典项 builtin(63003);被引用(63009,走 dictReferenceService.isReferencedselectRefCount 实时 SQL,绝不进缓存);一级项下有存活子项(63009「还有子项」分支,Q3-b 保守不级联) DictItemServiceImpl.java:230-248;DictRefCountMapper.java:32-35
删字典项附带 软删 + 清默认 + 清计数行(同 group_code+item_code) DictItemServiceImpl.java:250-258
force-delete doDeleteItem(id, true)仅跳过引用计数校验,builtin/子项阻断仍生效,清计数行照做 DictItemServiceImpl.java:181-185, :236

引用判定读取点:isReferenced(DictReferenceServiceImpl.java:54-57)→ DictRefCountMapper.selectRefCount(实时 SELECT dict_ref_count WHERE group_code/item_code,ref_count>0 即被引用)。列表 referenced 标记走 queryReferencedKeys 一次 OR 配对查询(DictItemServiceImpl.java:348-371)。

2.5 enabled-list 过滤语义

  • 分组 /enabled-list:仅 status=1 分组,sort_no 空排最后再按创建时间(DictGroupServiceImpl.java:146-154)。
  • 字典项 /enabled-listDictQueryService.listEnabledItems):分组启用 + 项启用 双条件;分组不存在/停用返回空列表(US-11);排序同上(DictQueryServiceImpl.java:86-115)。
  • effectiveSelectable(列表 enrich):分组启用 ∧ 项启用 ∧ (二级项时)父项启用(Q3-a 回溯父状态)(DictItemServiceImpl.java:311-345,:323-328 父状态批量查,:338-43 计算)。
  • listTree / listChildren:复用启用扁平列表内存组树/过滤(DictQueryServiceImpl.java:118-153,Q4 不新建缓存键)。注意:listTree/listChildren/getDefaultItem 目前无 Controller 端点暴露,仅 Java 契约供模块内调用。

2.6 引用计数写路径(DictReferenceServiceImpl.java)

  • increment/decrement/batchApply@Transactional(rollbackFor)(:26, :32, :38)。
  • addDelta(:62-79):原子 UPDATE dict_ref_count SET ref_count = GREATEST(ref_count + delta, 0)(DictRefCountMapper.java:23-27);行不存在且 delta>0 → 补插(DuplicateKeyException 时重试 UPDATE);delta<0 无行则忽略;下限 0 不为负。
  • 接口契约要求业务写入与计数增减同一 JDBC 事务(DictReferenceService.java:9-10)。

2.7 Caffeine 缓存(DictQueryServiceImpl.java:39-49)

缓存 TTL 容量
valueCache(正结果) item:{groupCode}:{itemCode} / enabledItems:{groupCode} / defaultItem:{groupCode} 10s(CACHE_TTL_SECONDS) 10_000
emptyCache(空结果防穿透) 同上键,EMPTY 哨兵 2s(CACHE_EMPTY_TTL_SECONDS) 10_000

变更写入不主动 evict,接受多副本 10s 最终一致(类注释 :25-27)。DictReferenceService 一律实时查询不进缓存(DictReferenceService.java:7-8)。

2.8 分页排序实现

分组/字典项分页均:先把 param.setOrderBy(null); param.setAsc(null)(DictGroupServiceImpl.java:49-50;DictItemServiceImpl.java:56-57),再 Wrapper .last("ORDER BY sort_no IS NULL, sort_no ASC, create_time ASC")(DictGroupServiceImpl.java:58;DictItemServiceImpl.java:68)——排序固定,前端传排序参数被静默忽略。keyword 匹配:分组 name/code(DictGroupServiceImpl.java:53-56);字典项 name/code/value(DictItemServiceImpl.java:62-67,注意 bruno 文档只写了名称/编码,见 §5)。

3. 消费方接线

依赖方向:crm-lead / crm-opportunity / crm-customer 的 pom 均直接依赖 crm-dict(如 crm-lead/pom.xml 注释「依赖方向单向」),无 port/适配层,直接注入 com.crm.dict.service.DictQueryService

模块 调用点 业务动作 方法 分组码
crm-lead query/impl/LeadViewQueryImpl.java:62(字段)、:281-296 fillDictNames,由 :227 fillDisplayFields 串联 列表/详情读路径回显(无 @Transactional) getNames ×4 lead_source / brand / product / scene
crm-opportunity query/impl/OpportunityViewQueryImpl.java:75、:223-234 fillDictNames 列表/详情读路径回显 getNames ×2 opp_source / industry
crm-customer service/impl/CustomerServiceImpl.java:64、:185 / :196(detail 内) 客户详情读路径回显 getNames ×2 customer_type / industry

关键发现:ref_count 增减在业务侧 0 调用。全仓 grep increment(/decrement(/batchApply(DictReferenceService,除 crm-dict 自身(DictItemServiceImpl 的 isReferenced 删除校验、测试)外无任何业务模块调用。即:

  • dict_ref_count 行只在字典项新建时预建为 0(DictItemServiceImpl.java:148-153);
  • 业务保存/清除字典编码时加减计数 → referenced 标记恒 false、被引用阻断(63009)与 value 只读(US-25)实际从不触发;
  • 「同事务」接线点目前不存在,是消费方缺口而非 dict 侧缺口。

4. SEED_GROUPS 分组编码清单(dict/config/DictDataInitializer.java:49-335

24 个内置分组(test 断言:24 组 + 229 项,DictDataInitializerTest.java:34):

# group_code 名称 sortNo # group_code 名称 sortNo
1 lead_source 渠道 1 13 pause_reason 暂缓原因 13
2 customer_status 客户状态 2 14 pool_reason 进入公海原因 14
3 industry 所属行业(两级树) 3 15 project_role 商机项目角色 15
4 brand 品牌 4 16 close_reason 关闭原因 16
5 product 需求产品 5 17 locality_type 项目属地 17
6 scene 需求场景 6 18 follow_way 跟进方式 18
7 opp_stage 商机阶段 7 19 survey_seq 勘察次数 19
8 opp_source 商机来源 8 20 apply_way 申请方式 20
9 opp_type 商机类型 9 21 customer_role 客户角色 21
10 bid_form 招标形式 10 22 attachment_type 商机资料类型 22
11 result_tag 跟进结果标签 11 23 customer_type 客户类型 23
12 design_config 设计配置 12 24 transfer_reason 交接原因 24

种子幂等:builtin 分组按种子刷新、种子外旧内置项剪除、builtin=false 用户数据/同 code 用户分组绝不触碰(DictDataInitializer.java:409-476)。权限侧另有 DictPermissionInitializer.java:35-59:10 个权限码种子化到「系统管理/数据字典」菜单(/system/dict)。

5. 接口文档现状

现状 位置
bruno-sync sourceRoots 已含 crm-dict/src/main/java bruno-sync.config.json:4
menuBindings 无「A7 后台管理/数据字典」条目(A7 只有「客户管理设置」) bruno-sync.config.json:8-19
Controller @Tag 分组/字典项各一个 tag(A7 后台管理/数据字典-分组 / -字典项),非「数据字典」单一菜单 DictGroupController.java:24;DictItemController.java:25
文档仓 外部仓 D:/code/crm-api-docs(bruno-sync.local.json:1)
生成物 A7 后台管理/数据字典-分组/(5 个 .bru:分组分页/新建编辑分组/删除分组/分组启停/启用分组列表)+ 数据字典-字典项/(7 个 .bru:字典项分页/新建编辑字典项/删除字典项/强制删除字典项/字典项启停/设为默认项/启用项列表)——12/12 端点全覆盖,2026-08-31 提交(456cd21) D:/code/crm-api-docs/A7 后台管理/
生成方式 .scratch/bruno-coldstart/gen_bruno.py + .scratch/bru_lib.py 幂等脚本写外部仓;曾有「crm-dict 暂缓纳入」待办(2026-08-14 用户决定),后已补 .scratch/bruno-coldstart/PENDING-review-and-crm-dict.md 待办2

文档缺口/漂移:

  1. menuBindings 缺 dict 条目 → 示例值无 viewType/scopeKey 可依(ADR-0024 菜单粒度);folder.bru 仅 name/seq 无绑定。
  2. 字典项分页.bru 文档表:keyword 写「匹配名称/编码」,代码实际匹配 name/code/value(DictItemServiceImpl.java:62-67)。
  3. 字典项分页.bru 响应表缺 parentIdchildren 字段(DictItemDTO.java:22, 52)。
  4. 文档示例仍列 orderBy/asc 参数且说明可排序,但服务端强制置 null(§2.8)——语义为「收了不用」。
  5. enabled-list(业务选择器)与 listTree/listChildren 语义只存在于 Java 契约,端点文档仅覆盖 /item/enabled-list;树形/级联契约无文档承载。

6. 测试清单(crm-dict/src/test/java/com/crm/dict/)

文件 覆盖面(一句话)
AbstractDictH2Test.java H2 MySQL 模式手工装配 MyBatis-Plus 集成测试基类:每方法新建服务实例(缓存隔离)+ TRUNCATE 清表。
config/DictDataInitializerTest.java(T08,9 测试) 种子幂等初始化:首装 24 组 229 项、重跑不重复、builtin 刷新不碰用户数据、默认项不覆盖、种子外剪除、同 code 用户组跳过、坏种子 fail-fast。
config/DictPermissionInitializerTest.java(T07,2 测试) 权限种子描述符正确性 + Controller 端点 @PreAuthorize 契约(enabled-list 放行除外)。
service/impl/DictGroupServiceTest.java(T02,12 测试) 分组分页(itemCount/keyword/status)、新建校验(必填/格式/唯一/默认值)、编辑保 code、删除阻断(有项/内置)、软删复用、启停与启用列表排序。
service/impl/DictItemServiceTest.java(T04+T05,16 测试) 字典项新建(自动编码/预建计数行/组内唯一/分组可用)、身份只读、值引用只读、删除规则、强删、停用取消默认、设默认互斥、分页 enrich、两级树校验、effectiveSelectable 父回溯。
service/impl/DictQueryServiceTest.java(T06,9 测试) 读路径与缓存:getItem 命中/未命中、10s 快照、启用列表过滤排序、分组停用返空、默认项基本/休眠恢复、2s 空负缓存、listTree、listChildren。
service/impl/DictReferenceServiceTest.java(T03,6 测试) 引用计数写路径:增减、累计、下限 0、batchApply 跨组聚合、无行 increment 补行、无行 decrement 忽略。