From 08d95df19a16efaf588da8f5eeeb90764236e3e5 Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Tue, 25 Aug 2026 15:52:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(dict):=20=E7=A5=A809=20=E6=AD=A53=20DictQu?= =?UTF-8?q?eryService=20=E5=8A=A0=20listTree/listChildren=20=E6=A0=91?= =?UTF-8?q?=E5=BD=A2=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - listTree(groupCode): 复用扁平启用列表内存组两级树, 一级为根带 children(仅启用子项), 不新建缓存key (Q4) - listChildren(groupCode, parentCode): 取一级项直接启用子项, 父不存在返空 - DictItemDTO 加 children (仅 listTree 填充); getItem/listEnabledItems 扁平接口不变, 现存消费方零改动 - 测试助手 newChildItem; 新增2测试 listTree_twoLevel/listChildren_byParentCode; 50 全绿 (+2) --- .../com/crm/dict/domain/dto/DictItemDTO.java | 3 ++ .../crm/dict/service/DictQueryService.java | 13 +++++++ .../service/impl/DictQueryServiceImpl.java | 38 +++++++++++++++++++ .../java/com/crm/dict/AbstractDictH2Test.java | 8 ++++ .../service/impl/DictQueryServiceTest.java | 32 ++++++++++++++++ 5 files changed, 94 insertions(+) diff --git a/crm-dict/src/main/java/com/crm/dict/domain/dto/DictItemDTO.java b/crm-dict/src/main/java/com/crm/dict/domain/dto/DictItemDTO.java index ea11bfd..72921f9 100644 --- a/crm-dict/src/main/java/com/crm/dict/domain/dto/DictItemDTO.java +++ b/crm-dict/src/main/java/com/crm/dict/domain/dto/DictItemDTO.java @@ -48,6 +48,9 @@ public class DictItemDTO extends BaseDTO { @Schema(description = "是否有效可选(分组与项均启用)") private Boolean effectiveSelectable; + @Schema(description = "二级子项(仅 listTree 返回时填充;二级项自身为空/null)") + private java.util.List children; + /** * Entity → DTO(出参转换,Service 层调用) */ diff --git a/crm-dict/src/main/java/com/crm/dict/service/DictQueryService.java b/crm-dict/src/main/java/com/crm/dict/service/DictQueryService.java index b69a807..c4d3bc2 100644 --- a/crm-dict/src/main/java/com/crm/dict/service/DictQueryService.java +++ b/crm-dict/src/main/java/com/crm/dict/service/DictQueryService.java @@ -25,6 +25,19 @@ public interface DictQueryService { */ List listEnabledItems(String groupCode); + /** + * 分组启用字典项两级树(业务树形选择器渲染用):仅返回分组与项均启用的项, + * 顶层(parentId=null)作为树根带 children(也只含启用子项);父启用但无启用子项时 children 为空列表。 + * 分组不存在/停用时返回空列表。复用扁平启用列表内存组树(Q4)。 + */ + List listTree(String groupCode); + + /** + * 某一级项下的启用二级子项列表(级联选择器选完一级再拉二级用); + * 分组/父项不存在或停用时返回空列表,按 sort_no 排序(空排最后)。 + */ + List listChildren(String groupCode, String parentCode); + /** * 分组当前默认项:联查 group.status,分组停用(休眠)或未设默认时返回 null */ diff --git a/crm-dict/src/main/java/com/crm/dict/service/impl/DictQueryServiceImpl.java b/crm-dict/src/main/java/com/crm/dict/service/impl/DictQueryServiceImpl.java index 4824aa4..b473f31 100644 --- a/crm-dict/src/main/java/com/crm/dict/service/impl/DictQueryServiceImpl.java +++ b/crm-dict/src/main/java/com/crm/dict/service/impl/DictQueryServiceImpl.java @@ -97,6 +97,44 @@ public class DictQueryServiceImpl implements DictQueryService { return items.stream().map(DictItemDTO::fromEntity).toList(); } + @Override + public List listTree(String groupCode) { + // 复用已缓存的扁平启用列表,内存组两级树(不新建缓存 key,Q4) + List flat = listEnabledItems(groupCode); + if (flat.isEmpty()) { + return List.of(); + } + List roots = new java.util.ArrayList<>(); + java.util.Map> childrenByParent = new java.util.HashMap<>(); + for (DictItemDTO dto : flat) { + if (dto.getParentId() == null) { + roots.add(dto); + } else { + childrenByParent.computeIfAbsent(dto.getParentId(), k -> new java.util.ArrayList<>()).add(dto); + } + } + for (DictItemDTO root : roots) { + root.setChildren(childrenByParent.getOrDefault(root.getId(), List.of())); + } + return roots; + } + + @Override + public List listChildren(String groupCode, String parentCode) { + // 启用且父启用才可选:复用启用列表,取 parentCode 对应一级项的直接子项 + List flat = listEnabledItems(groupCode); + if (flat.isEmpty()) { + return List.of(); + } + DictItemDTO parent = flat.stream() + .filter(d -> d.getParentId() == null && parentCode.equals(d.getCode())) + .findFirst().orElse(null); + if (parent == null) { + return List.of(); + } + return flat.stream().filter(d -> parent.getId().equals(d.getParentId())).toList(); + } + @Override public DictItem getDefaultItem(String groupCode) { String key = "defaultItem:" + groupCode; diff --git a/crm-dict/src/test/java/com/crm/dict/AbstractDictH2Test.java b/crm-dict/src/test/java/com/crm/dict/AbstractDictH2Test.java index 9a31793..4533d04 100644 --- a/crm-dict/src/test/java/com/crm/dict/AbstractDictH2Test.java +++ b/crm-dict/src/test/java/com/crm/dict/AbstractDictH2Test.java @@ -166,6 +166,14 @@ public abstract class AbstractDictH2Test { return i; } + /** 新建二级子项(parentId 指向同组一级项) */ + protected DictItem newChildItem(Long groupId, Long parentId, String code, String value) { + DictItem i = newItem(groupId, code, value); + i.setParentId(parentId); + dictItemMapper.updateById(i); + return i; + } + /** 断言抛业务异常且错误码匹配 */ protected void assertDictError(org.assertj.core.api.ThrowableAssert.ThrowingCallable callable, int expectedCode) { assertThatThrownBy(callable) diff --git a/crm-dict/src/test/java/com/crm/dict/service/impl/DictQueryServiceTest.java b/crm-dict/src/test/java/com/crm/dict/service/impl/DictQueryServiceTest.java index 78ee714..fa4578a 100644 --- a/crm-dict/src/test/java/com/crm/dict/service/impl/DictQueryServiceTest.java +++ b/crm-dict/src/test/java/com/crm/dict/service/impl/DictQueryServiceTest.java @@ -129,4 +129,36 @@ class DictQueryServiceTest extends AbstractDictH2Test { Thread.sleep(2200); assertThat(dictQueryService.getItem("g", "late_item")).isNotNull(); } + + @Test + @DisplayName("listTree:一级为根带 children,仅含启用项,分组停用返空") + void listTree_twoLevel() { + DictGroup g = newGroup("industry", "所属行业"); + DictItem gov = newItem(g.getId(), "gov", "gov"); + DictItem edu = newItem(g.getId(), "edu", "edu"); + newChildItem(g.getId(), gov.getId(), "gov_ncpol", "gov_ncpol"); + newChildItem(g.getId(), gov.getId(), "gov_emg", "gov_emg"); + // edu 无启用子项 + + List tree = dictQueryService.listTree("industry"); + assertThat(tree).extracting(DictItemDTO::getCode).containsExactlyInAnyOrder("gov", "edu"); + DictItemDTO govNode = tree.stream().filter(t -> "gov".equals(t.getCode())).findFirst().orElseThrow(); + assertThat(govNode.getChildren()).extracting(DictItemDTO::getCode) + .containsExactlyInAnyOrder("gov_ncpol", "gov_emg"); + DictItemDTO eduNode = tree.stream().filter(t -> "edu".equals(t.getCode())).findFirst().orElseThrow(); + assertThat(eduNode.getChildren()).isEmpty(); + } + + @Test + @DisplayName("listChildren:取一级项直接启用子项;父不存在返空") + void listChildren_byParentCode() { + DictGroup g = newGroup("industry", "所属行业"); + DictItem gov = newItem(g.getId(), "gov", "gov"); + newChildItem(g.getId(), gov.getId(), "gov_ncpol", "gov_ncpol"); + newChildItem(g.getId(), gov.getId(), "gov_emg", "gov_emg"); + + assertThat(dictQueryService.listChildren("industry", "gov")) + .extracting(DictItemDTO::getCode).containsExactlyInAnyOrder("gov_ncpol", "gov_emg"); + assertThat(dictQueryService.listChildren("industry", "not_exist")).isEmpty(); + } }