diff --git a/backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java b/backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java index 798d0ab..e17f15f 100644 --- a/backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java +++ b/backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java @@ -2,6 +2,9 @@ package com.xly.erp.module.mod.service; import com.xly.erp.module.mod.dto.CreateModuleDTO; import com.xly.erp.module.mod.dto.UpdateModuleDTO; +import com.xly.erp.module.mod.vo.ModuleTreeVO; + +import java.util.List; public interface ModuleService { Integer create(CreateModuleDTO dto); @@ -9,4 +12,6 @@ public interface ModuleService { Integer update(Integer id, UpdateModuleDTO dto); void delete(Integer id); + + List listTree(String keyword); } diff --git a/backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java b/backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java index 2187eff..b272bb2 100644 --- a/backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java +++ b/backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java @@ -7,6 +7,7 @@ import com.xly.erp.common.security.SecurityContextHelper; import com.xly.erp.module.mod.dto.CreateModuleDTO; import com.xly.erp.module.mod.dto.UpdateModuleDTO; import com.xly.erp.module.mod.entity.Module; +import com.xly.erp.module.mod.vo.ModuleTreeVO; import com.xly.erp.module.mod.mapper.ModuleMapper; import com.xly.erp.module.mod.service.ModuleService; import org.springframework.dao.DuplicateKeyException; @@ -14,6 +15,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Set; @Service @@ -112,6 +117,42 @@ public class ModuleServiceImpl implements ModuleService { moduleMapper.updateById(entity); } + @Override + @Transactional(readOnly = true) + public List listTree(String keyword) { + String normalized = keyword == null ? "" : keyword.trim(); + if (normalized.length() > 100) { + throw new BizException(40001, "keyword 长度超过 100 字符"); + } + List rows = moduleMapper.selectActiveByKeyword(normalized); + Map idIndex = new HashMap<>(); + for (Module m : rows) { + idIndex.put(m.getIIncrement(), toTreeVO(m)); + } + List roots = new ArrayList<>(); + for (Module m : rows) { + ModuleTreeVO vo = idIndex.get(m.getIIncrement()); + Integer parentId = m.getIParentId(); + if (parentId != null && idIndex.containsKey(parentId)) { + idIndex.get(parentId).getChildren().add(vo); + } else { + roots.add(vo); + } + } + return roots; + } + + private ModuleTreeVO toTreeVO(Module m) { + ModuleTreeVO vo = new ModuleTreeVO(); + vo.setIIncrement(m.getIIncrement()); + vo.setSModuleNameZh(m.getSModuleNameZh()); + vo.setSDisplayType(m.getSDisplayType()); + vo.setSManageDeptEn(m.getSManageDeptEn()); + vo.setIParentId(m.getIParentId()); + vo.setISortOrder(m.getISortOrder()); + return vo; + } + private void validateParent(Integer id, Integer parentId) { if (parentId == null) { return; diff --git a/backend/src/main/java/com/xly/erp/module/mod/vo/ModuleTreeVO.java b/backend/src/main/java/com/xly/erp/module/mod/vo/ModuleTreeVO.java new file mode 100644 index 0000000..461a131 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/mod/vo/ModuleTreeVO.java @@ -0,0 +1,45 @@ +package com.xly.erp.module.mod.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +public class ModuleTreeVO { + + @JsonProperty("iIncrement") + private Integer iIncrement; + + @JsonProperty("sModuleNameZh") + private String sModuleNameZh; + + @JsonProperty("sDisplayType") + private String sDisplayType; + + @JsonProperty("sManageDeptEn") + private String sManageDeptEn; + + @JsonProperty("iParentId") + private Integer iParentId; + + @JsonProperty("iSortOrder") + private Integer iSortOrder; + + @JsonProperty("children") + private List children = new ArrayList<>(); + + public Integer getIIncrement() { return iIncrement; } + public void setIIncrement(Integer iIncrement) { this.iIncrement = iIncrement; } + public String getSModuleNameZh() { return sModuleNameZh; } + public void setSModuleNameZh(String sModuleNameZh) { this.sModuleNameZh = sModuleNameZh; } + public String getSDisplayType() { return sDisplayType; } + public void setSDisplayType(String sDisplayType) { this.sDisplayType = sDisplayType; } + public String getSManageDeptEn() { return sManageDeptEn; } + public void setSManageDeptEn(String sManageDeptEn) { this.sManageDeptEn = sManageDeptEn; } + public Integer getIParentId() { return iParentId; } + public void setIParentId(Integer iParentId) { this.iParentId = iParentId; } + public Integer getISortOrder() { return iSortOrder; } + public void setISortOrder(Integer iSortOrder) { this.iSortOrder = iSortOrder; } + public List getChildren() { return children; } + public void setChildren(List children) { this.children = children; } +} diff --git a/backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java b/backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java index ca40fb1..4e49e77 100644 --- a/backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java +++ b/backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java @@ -8,6 +8,7 @@ import com.xly.erp.module.mod.dto.UpdateModuleDTO; import com.xly.erp.module.mod.entity.Module; import com.xly.erp.module.mod.mapper.ModuleMapper; import com.xly.erp.module.mod.service.impl.ModuleServiceImpl; +import com.xly.erp.module.mod.vo.ModuleTreeVO; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -17,6 +18,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.core.context.SecurityContextHolder; import java.util.Collections; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -306,6 +308,85 @@ class ModuleServiceImplTest { assertThat(captor.getValue().getSDeletedBy()).isEqualTo("BOB"); } + @Test + void listTree_emptyKeyword_invokesMapperWithEmptyString_returnsAssembledTree() { + Module root1 = treeRow(1, "根1", null, 0); + Module root2 = treeRow(2, "根2", null, 0); + Module child1 = treeRow(3, "子1", 1, 0); + Module child2 = treeRow(4, "子2", 1, 1); + Module grand1 = treeRow(5, "孙1", 3, 0); + when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of(root1, root2, child1, child2, grand1)); + + List result = service.listTree(""); + + assertThat(result).extracting(ModuleTreeVO::getIIncrement).containsExactly(1, 2); + assertThat(result.get(0).getChildren()).extracting(ModuleTreeVO::getIIncrement).containsExactly(3, 4); + assertThat(result.get(0).getChildren().get(0).getChildren()).extracting(ModuleTreeVO::getIIncrement).containsExactly(5); + assertThat(result.get(1).getChildren()).isEmpty(); + } + + @Test + void listTree_nullKeyword_treatedAsEmpty() { + when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of()); + service.listTree(null); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(moduleMapper).selectActiveByKeyword(captor.capture()); + assertThat(captor.getValue()).isEqualTo(""); + } + + @Test + void listTree_blankKeyword_treatedAsEmpty() { + when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of()); + service.listTree(" "); + verify(moduleMapper).selectActiveByKeyword(""); + } + + @Test + void listTree_keywordTooLong_throws40001() { + String longKw = "x".repeat(101); + assertThatThrownBy(() -> service.listTree(longKw)) + .isInstanceOf(BizException.class) + .hasFieldOrPropertyWithValue("code", 40001); + verify(moduleMapper, never()).selectActiveByKeyword(any()); + } + + @Test + void listTree_returnsEmptyListWhenNoMatch() { + when(moduleMapper.selectActiveByKeyword("xyz")).thenReturn(List.of()); + List result = service.listTree("xyz"); + assertThat(result).isEmpty(); + } + + @Test + void listTree_orphansBecomeRootsInForest() { + Module orphan = treeRow(3, "孤儿", 99, 0); + when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of(orphan)); + + List result = service.listTree(""); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getIIncrement()).isEqualTo(3); + assertThat(result.get(0).getChildren()).isEmpty(); + } + + @Test + void listTree_keywordIsTrimmedBeforeQuery() { + when(moduleMapper.selectActiveByKeyword("系统")).thenReturn(List.of()); + service.listTree(" 系统 "); + verify(moduleMapper).selectActiveByKeyword("系统"); + } + + private Module treeRow(int id, String name, Integer parentId, int sortOrder) { + Module m = new Module(); + m.setIIncrement(id); + m.setSModuleNameZh(name); + m.setSDisplayType("手机端"); + m.setSManageDeptEn("IT"); + m.setIParentId(parentId); + m.setISortOrder(sortOrder); + return m; + } + private UpdateModuleDTO baseUpdateDto() { UpdateModuleDTO dto = new UpdateModuleDTO(); dto.setSDisplayType("手机端");