From 94effd1ce5b21cbfcc4e777897cf8c99c3eb5689 Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Tue, 25 Aug 2026 15:46:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(dict):=20=E7=A5=A809=20=E6=AD=A52=20?= =?UTF-8?q?=E5=AD=97=E5=85=B8=E9=A1=B9=E4=B8=A4=E7=BA=A7=E6=A0=91=E7=88=B6?= =?UTF-8?q?=E5=AD=90=E6=A0=A1=E9=AA=8C+=E5=88=A0=E9=99=A4=E6=8B=A6?= =?UTF-8?q?=E6=88=AA+=E5=AD=90=E9=A1=B9=E9=9A=8F=E7=88=B6=E5=81=9C?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - saveItem 父项校验 (Q2): parentId 非空须同组+父为一级(parentId=null)+禁自引用, 父不存在63012/跨组或超两级63001 - doDeleteItem 拦截 (Q3-b): 删一级项若有存活子项则拒 (对齐删分组需组内无项) - enrich effectiveSelectable 回溯父状态 (Q3-a): 二级项父停用则不可选, 批量取父状态无N+1 - 新增3测试: 父子校验/删除拦截/子随父停用; 48 全绿 (+3) --- .../service/impl/DictItemServiceImpl.java | 40 +++++++- .../service/impl/DictItemServiceTest.java | 91 +++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/crm-dict/src/main/java/com/crm/dict/service/impl/DictItemServiceImpl.java b/crm-dict/src/main/java/com/crm/dict/service/impl/DictItemServiceImpl.java index 1f518c0..2057c4d 100644 --- a/crm-dict/src/main/java/com/crm/dict/service/impl/DictItemServiceImpl.java +++ b/crm-dict/src/main/java/com/crm/dict/service/impl/DictItemServiceImpl.java @@ -104,6 +104,25 @@ public class DictItemServiceImpl extends BaseServiceImpl() + .eq(DictItem::getParentId, id)); + if (childCount > 0) { + throw new BusinessErrorException(DictConstants.CODE_ITEM_REFERENCED, + "该字典项下还有子项,请先删除子项"); + } + } // 软删除 + 复用键(deleted=1 + delete_key=id) this.update(null, new LambdaUpdateWrapper() .eq(DictItem::getId, id) @@ -292,17 +320,27 @@ public class DictItemServiceImpl extends BaseServiceImpl referencedKeys = queryReferencedKeys(vos, groups); + // 两级树形 Q3-a:二级项有效可选需回溯父项状态(父停用则子项不可选) + Set parentIds = vos.stream().map(DictItemDTO::getParentId) + .filter(java.util.Objects::nonNull).collect(Collectors.toSet()); + Map parentStatus = parentIds.isEmpty() ? Map.of() + : dictItemMapper.selectBatchIds(parentIds).stream() + .collect(Collectors.toMap(DictItem::getId, DictItem::getStatus)); vos.forEach(v -> { DictGroup g = groups.get(v.getGroupId()); v.setGroupName(g == null ? null : g.getName()); v.setIsDefault(defaultMap.containsKey(v.getGroupId()) && defaultMap.get(v.getGroupId()).equals(v.getId())); v.setReferenced(referencedKeys.contains(pairKey(g == null ? "" : g.getCode(), v.getCode()))); + boolean parentEnabled = v.getParentId() == null + || (parentStatus.get(v.getParentId()) != null + && DictConstants.STATUS_ENABLED == parentStatus.get(v.getParentId())); v.setEffectiveSelectable(g != null && g.getStatus() != null && DictConstants.STATUS_ENABLED == g.getStatus() && v.getStatus() != null - && DictConstants.STATUS_ENABLED == v.getStatus()); + && DictConstants.STATUS_ENABLED == v.getStatus() + && parentEnabled); }); } diff --git a/crm-dict/src/test/java/com/crm/dict/service/impl/DictItemServiceTest.java b/crm-dict/src/test/java/com/crm/dict/service/impl/DictItemServiceTest.java index 6c38962..0ddc5b9 100644 --- a/crm-dict/src/test/java/com/crm/dict/service/impl/DictItemServiceTest.java +++ b/crm-dict/src/test/java/com/crm/dict/service/impl/DictItemServiceTest.java @@ -288,4 +288,95 @@ class DictItemServiceTest extends AbstractDictH2Test { PageResult after = dictItemService.pageItems(param); assertThat(after.getContent()).allMatch(v -> !v.getEffectiveSelectable()); } + + @Test + @DisplayName("树形:二级项 parent_id 必须指向同组一级项;跨组/非一级父/自引用被拒") + void saveItem_treeParentValidation() { + DictGroup g = newGroup("industry", "所属行业"); + DictGroup other = newGroup("other_g", "其他组"); + DictItem lv1 = newItem(g.getId(), "gov", "gov"); // 一级(parentId=null) + DictItem otherLv1 = newItem(other.getId(), "x", "x"); // 异组一级 + + // 正常:二级挂同组一级 + DictItemDTO lv2 = new DictItemDTO(); + lv2.setGroupId(g.getId()); + lv2.setCode("gov_ncpol"); + lv2.setName("人大政协"); + lv2.setParentId(lv1.getId()); + dictItemService.saveItem(lv2); + DictItem savedLv2 = dictItemMapper.selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(DictItem::getCode, "gov_ncpol")).get(0); + assertThat(savedLv2.getParentId()).isEqualTo(lv1.getId()); + + // 父不存在 → 63012 + DictItemDTO badParent = new DictItemDTO(); + badParent.setGroupId(g.getId()); + badParent.setCode("gov_x"); + badParent.setName("坏父"); + badParent.setParentId(888888L); + assertDictError(() -> dictItemService.saveItem(badParent), DictConstants.CODE_ITEM_NOT_EXIST); + + // 跨组父 → 63001 + DictItemDTO crossParent = new DictItemDTO(); + crossParent.setGroupId(g.getId()); + crossParent.setCode("gov_cross"); + crossParent.setName("跨组父"); + crossParent.setParentId(otherLv1.getId()); + assertDictError(() -> dictItemService.saveItem(crossParent), DictConstants.CODE_DICT_INVALID); + + // 父自身是二级(超两级)→ 63001 + DictItemDTO lv3 = new DictItemDTO(); + lv3.setGroupId(g.getId()); + lv3.setCode("gov_ncpol_sub"); + lv3.setName("三级"); + lv3.setParentId(savedLv2.getId()); + assertDictError(() -> dictItemService.saveItem(lv3), DictConstants.CODE_DICT_INVALID); + } + + @Test + @DisplayName("树形:删一级项若有存活子项被拦;先删子项后可删") + void deleteItem_blockedWhenHasChildren() { + DictGroup g = newGroup("industry", "所属行业"); + DictItem lv1 = newItem(g.getId(), "gov", "gov"); + DictItemDTO lv2 = new DictItemDTO(); + lv2.setGroupId(g.getId()); + lv2.setCode("gov_ncpol"); + lv2.setName("人大政协"); + lv2.setParentId(lv1.getId()); + dictItemService.saveItem(lv2); + DictItem child = dictItemMapper.selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(DictItem::getCode, "gov_ncpol")).get(0); + + // 有子项 → 删一级被拦 63010 + assertDictError(() -> dictItemService.deleteItem(lv1.getId()), DictConstants.CODE_ITEM_REFERENCED); + // 先删子项,再删一级→ 通过 + dictItemService.deleteItem(child.getId()); + dictItemService.deleteItem(lv1.getId()); + assertThat(dictItemMapper.selectCount(null)).isZero(); + } + + @Test + @DisplayName("树形:停用一级项 → 其二级子项 effectiveSelectable=false(Q3-a 回溯父状态)") + void effectiveSelectable_childFollowsParentDisabled() { + DictGroup g = newGroup("industry", "所属行业"); + DictItem lv1 = newItem(g.getId(), "gov", "gov"); + DictItemDTO lv2 = new DictItemDTO(); + lv2.setGroupId(g.getId()); + lv2.setCode("gov_ncpol"); + lv2.setName("人大政协"); + lv2.setParentId(lv1.getId()); + dictItemService.saveItem(lv2); + + ItemPageParam param = new ItemPageParam(); + param.setGroupId(g.getId()); + // 父启用时子可选 + assertThat(dictItemService.pageItems(param).getContent()) + .filteredOn(v -> "gov_ncpol".equals(v.getCode())) + .allMatch(DictItemDTO::getEffectiveSelectable); + // 停用一级父 → 子项不可选 + dictItemService.updateItemStatus(lv1.getId(), DictConstants.STATUS_DISABLED); + assertThat(dictItemService.pageItems(param).getContent()) + .filteredOn(v -> "gov_ncpol".equals(v.getCode())) + .allMatch(v -> !v.getEffectiveSelectable()); + } }