Browse Source

feat(dict): 票09 步1 DictItem 加 parent_id 两级树字段

- DictItem + DictItemDTO 加 parentId (nullable, 顶层=null, 二级指向同组一级)
- H2 测试 schema dict_item 加 parent_id 列
- grill 拍板结论写入票 Answer 并 resolved (Q1两级封顶/Q2父子约束/Q3停用连带+删除拦截/Q4 getItem不变+listTree/Q5无存量直接覆盖/Q6客户到二级商机到一级/Q7商机存一级)
- 45 全绿 (纯加字段, 无行为变更)
master
luoweijian 2 weeks ago
parent
commit
d0856e18a0
  1. 21
      .scratch/data-dictionary/issues/09-hierarchical-item.md
  2. 3
      crm-dict/src/main/java/com/crm/dict/domain/dto/DictItemDTO.java
  3. 4
      crm-dict/src/main/java/com/crm/dict/domain/entity/DictItem.java
  4. 2
      crm-dict/src/test/java/com/crm/dict/AbstractDictH2Test.java

21
.scratch/data-dictionary/issues/09-hierarchical-item.md

@ -2,7 +2,7 @@
**Type:** grilling → task(本票先 grill 拍板不变式,再落地) **Type:** grilling → task(本票先 grill 拍板不变式,再落地)
**Status:** needs-triage **Status:** resolved
**Blocked by:** 04 **Blocked by:** 04
@ -64,3 +64,22 @@
- 能源与制造:石化-能源-矿业 / 发电站-核电站 / 电力实训基地 / 工厂 - 能源与制造:石化-能源-矿业 / 发电站-核电站 / 电力实训基地 / 工厂
- 文化旅游:文旅 / 酒店 / 展馆-博物馆 - 文化旅游:文旅 / 酒店 / 展馆-博物馆
- 园区与基建:智慧园区 / 集团大楼 / 商业中心 / 智算中心 / 低空飞行基地 - 园区与基建:智慧园区 / 集团大楼 / 商业中心 / 智算中心 / 低空飞行基地
## Answer(20260825 grill 拍板)
**Status → resolved(进入实现)**
- **Q1 层级上限**:**两级封顶**。一级 `parent_id=null`,二级挂一级;不做任意深度树(YAGNI,将来需三级再开票)。
- **Q2 父子约束**:采纳。二级 `parent_id` 必须指向**同分组内**的一级项;禁跨组、禁自引用、禁循环;一级 `parent_id=null`;**父项自身必须是一级**(parent_id=null),不允许挂到二级下(Q1 两级的强约束)。
- **Q3 停用/删除级联**:采纳建议。(a) 停用一级项 → 其二级子项**连带不可选**(对齐"停用分组→整组不可选"语义,`effectiveSelectable` 需回溯父项状态);(b) 删除有存活子项的一级项 → **拦截**并提示先删子项(对齐"删分组需组内无项"的保守语义),不级联软删。
- **Q4 树形查询 API + 缓存**:采纳。`getItem`(扁平按 code)**保持不变**,现存消费方零改动;新增 `listTree(groupCode)`(一级带 children 嵌套)+ `listChildren(groupCode, parentCode)`;缓存**复用现有 Caffeine 按 groupCode 的扁平 list**,内存组树,不新建缓存 key。
- **Q5 存量数据**:⭐ **无存量数据**。现有 `industry` 8 项无客户业务引用 → 种子直接**覆盖重种**为新树,零迁移、无悬空风险。
- **Q6 客户侧选到几级**:**客户选到二级、商机选一级**。二级项是客户模块可直接落库的真实值(非纯分类维度)。
- **Q7 商机落库形态**:商机主表 `industry_code` **只存一级项 code**;二级仅供客户选择与字典层级维护,不进商机主表。
### 实现顺序(每步独立编译测试提交)
1. `DictItem``parentId` + `DictItemDTO``parentId`(DDL 自动加列,现有行 null=顶层)。
2. `DictItemServiceImpl.saveItem` 父子校验(Q2)+ `doDeleteItem` 删除拦截(Q3-b)+ `effectiveSelectable` 回溯父项停用(Q3-a)。
3. `DictQueryService``listTree`/`listChildren`(Q4),扁平内存组树。
4. 种子:`industry` 分组重种新树(9 一级 + 全二级,覆盖旧 8 项);测试断言同步。
5. 回归现有 45 扁平测试全绿 + 新增树形测试。

3
crm-dict/src/main/java/com/crm/dict/domain/dto/DictItemDTO.java

@ -19,6 +19,9 @@ public class DictItemDTO extends BaseDTO {
private Long groupId; private Long groupId;
@Schema(description = "父项ID(两级树形:顶层null,二级指向同组一级项id)")
private Long parentId;
private String name; private String name;
private String code; private String code;

4
crm-dict/src/main/java/com/crm/dict/domain/entity/DictItem.java

@ -26,6 +26,10 @@ public class DictItem extends BaseEntity {
@Column(columnDefinition = "bigint not null comment '所属分组ID'") @Column(columnDefinition = "bigint not null comment '所属分组ID'")
private Long groupId; private Long groupId;
@Comment("父项ID:两级树形(顶层=null,二级指向同组一级项id);仅部分字典分组用(如行业)")
@Column(columnDefinition = "bigint comment '父项ID(两级树形,顶层null)'")
private Long parentId;
@Comment("字典名称(展示文案,改名后历史数据展示最新名)") @Comment("字典名称(展示文案,改名后历史数据展示最新名)")
@Column(columnDefinition = "varchar(50) not null comment '字典名称'") @Column(columnDefinition = "varchar(50) not null comment '字典名称'")
private String name; private String name;

2
crm-dict/src/test/java/com/crm/dict/AbstractDictH2Test.java

@ -185,7 +185,7 @@ public abstract class AbstractDictH2Test {
"deleted TINYINT NOT NULL DEFAULT 0, " + "deleted TINYINT NOT NULL DEFAULT 0, " +
"PRIMARY KEY (id), CONSTRAINT uk_dict_group_code UNIQUE (code, delete_key))", "PRIMARY KEY (id), CONSTRAINT uk_dict_group_code UNIQUE (code, delete_key))",
"CREATE TABLE IF NOT EXISTS dict_item (" + "CREATE TABLE IF NOT EXISTS dict_item (" +
"id BIGINT NOT NULL, group_id BIGINT NOT NULL, name VARCHAR(50) NOT NULL, " + "id BIGINT NOT NULL, group_id BIGINT NOT NULL, parent_id BIGINT, name VARCHAR(50) NOT NULL, " +
"code VARCHAR(64) NOT NULL, value VARCHAR(100), sort_no INT, " + "code VARCHAR(64) NOT NULL, value VARCHAR(100), sort_no INT, " +
"status TINYINT NOT NULL DEFAULT 1, description VARCHAR(200), " + "status TINYINT NOT NULL DEFAULT 1, description VARCHAR(200), " +
"builtin BOOLEAN NOT NULL DEFAULT FALSE, delete_key BIGINT NOT NULL DEFAULT 0, " + "builtin BOOLEAN NOT NULL DEFAULT FALSE, delete_key BIGINT NOT NULL DEFAULT 0, " +

Loading…
Cancel
Save