Commit bbceb21d4a237ee575ff957700875657f300330a
1 parent
0cc2b2eb
feat(mod): module list tree service + vo REQ-MOD-004
Showing
4 changed files
with
172 additions
and
0 deletions
backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java
| ... | ... | @@ -2,6 +2,9 @@ package com.xly.erp.module.mod.service; |
| 2 | 2 | |
| 3 | 3 | import com.xly.erp.module.mod.dto.CreateModuleDTO; |
| 4 | 4 | import com.xly.erp.module.mod.dto.UpdateModuleDTO; |
| 5 | +import com.xly.erp.module.mod.vo.ModuleTreeVO; | |
| 6 | + | |
| 7 | +import java.util.List; | |
| 5 | 8 | |
| 6 | 9 | public interface ModuleService { |
| 7 | 10 | Integer create(CreateModuleDTO dto); |
| ... | ... | @@ -9,4 +12,6 @@ public interface ModuleService { |
| 9 | 12 | Integer update(Integer id, UpdateModuleDTO dto); |
| 10 | 13 | |
| 11 | 14 | void delete(Integer id); |
| 15 | + | |
| 16 | + List<ModuleTreeVO> listTree(String keyword); | |
| 12 | 17 | } | ... | ... |
backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java
| ... | ... | @@ -7,6 +7,7 @@ import com.xly.erp.common.security.SecurityContextHelper; |
| 7 | 7 | import com.xly.erp.module.mod.dto.CreateModuleDTO; |
| 8 | 8 | import com.xly.erp.module.mod.dto.UpdateModuleDTO; |
| 9 | 9 | import com.xly.erp.module.mod.entity.Module; |
| 10 | +import com.xly.erp.module.mod.vo.ModuleTreeVO; | |
| 10 | 11 | import com.xly.erp.module.mod.mapper.ModuleMapper; |
| 11 | 12 | import com.xly.erp.module.mod.service.ModuleService; |
| 12 | 13 | import org.springframework.dao.DuplicateKeyException; |
| ... | ... | @@ -14,6 +15,10 @@ import org.springframework.stereotype.Service; |
| 14 | 15 | import org.springframework.transaction.annotation.Transactional; |
| 15 | 16 | |
| 16 | 17 | import java.time.LocalDateTime; |
| 18 | +import java.util.ArrayList; | |
| 19 | +import java.util.HashMap; | |
| 20 | +import java.util.List; | |
| 21 | +import java.util.Map; | |
| 17 | 22 | import java.util.Set; |
| 18 | 23 | |
| 19 | 24 | @Service |
| ... | ... | @@ -112,6 +117,42 @@ public class ModuleServiceImpl implements ModuleService { |
| 112 | 117 | moduleMapper.updateById(entity); |
| 113 | 118 | } |
| 114 | 119 | |
| 120 | + @Override | |
| 121 | + @Transactional(readOnly = true) | |
| 122 | + public List<ModuleTreeVO> listTree(String keyword) { | |
| 123 | + String normalized = keyword == null ? "" : keyword.trim(); | |
| 124 | + if (normalized.length() > 100) { | |
| 125 | + throw new BizException(40001, "keyword 长度超过 100 字符"); | |
| 126 | + } | |
| 127 | + List<Module> rows = moduleMapper.selectActiveByKeyword(normalized); | |
| 128 | + Map<Integer, ModuleTreeVO> idIndex = new HashMap<>(); | |
| 129 | + for (Module m : rows) { | |
| 130 | + idIndex.put(m.getIIncrement(), toTreeVO(m)); | |
| 131 | + } | |
| 132 | + List<ModuleTreeVO> roots = new ArrayList<>(); | |
| 133 | + for (Module m : rows) { | |
| 134 | + ModuleTreeVO vo = idIndex.get(m.getIIncrement()); | |
| 135 | + Integer parentId = m.getIParentId(); | |
| 136 | + if (parentId != null && idIndex.containsKey(parentId)) { | |
| 137 | + idIndex.get(parentId).getChildren().add(vo); | |
| 138 | + } else { | |
| 139 | + roots.add(vo); | |
| 140 | + } | |
| 141 | + } | |
| 142 | + return roots; | |
| 143 | + } | |
| 144 | + | |
| 145 | + private ModuleTreeVO toTreeVO(Module m) { | |
| 146 | + ModuleTreeVO vo = new ModuleTreeVO(); | |
| 147 | + vo.setIIncrement(m.getIIncrement()); | |
| 148 | + vo.setSModuleNameZh(m.getSModuleNameZh()); | |
| 149 | + vo.setSDisplayType(m.getSDisplayType()); | |
| 150 | + vo.setSManageDeptEn(m.getSManageDeptEn()); | |
| 151 | + vo.setIParentId(m.getIParentId()); | |
| 152 | + vo.setISortOrder(m.getISortOrder()); | |
| 153 | + return vo; | |
| 154 | + } | |
| 155 | + | |
| 115 | 156 | private void validateParent(Integer id, Integer parentId) { |
| 116 | 157 | if (parentId == null) { |
| 117 | 158 | return; | ... | ... |
backend/src/main/java/com/xly/erp/module/mod/vo/ModuleTreeVO.java
0 → 100644
| 1 | +package com.xly.erp.module.mod.vo; | |
| 2 | + | |
| 3 | +import com.fasterxml.jackson.annotation.JsonProperty; | |
| 4 | + | |
| 5 | +import java.util.ArrayList; | |
| 6 | +import java.util.List; | |
| 7 | + | |
| 8 | +public class ModuleTreeVO { | |
| 9 | + | |
| 10 | + @JsonProperty("iIncrement") | |
| 11 | + private Integer iIncrement; | |
| 12 | + | |
| 13 | + @JsonProperty("sModuleNameZh") | |
| 14 | + private String sModuleNameZh; | |
| 15 | + | |
| 16 | + @JsonProperty("sDisplayType") | |
| 17 | + private String sDisplayType; | |
| 18 | + | |
| 19 | + @JsonProperty("sManageDeptEn") | |
| 20 | + private String sManageDeptEn; | |
| 21 | + | |
| 22 | + @JsonProperty("iParentId") | |
| 23 | + private Integer iParentId; | |
| 24 | + | |
| 25 | + @JsonProperty("iSortOrder") | |
| 26 | + private Integer iSortOrder; | |
| 27 | + | |
| 28 | + @JsonProperty("children") | |
| 29 | + private List<ModuleTreeVO> children = new ArrayList<>(); | |
| 30 | + | |
| 31 | + public Integer getIIncrement() { return iIncrement; } | |
| 32 | + public void setIIncrement(Integer iIncrement) { this.iIncrement = iIncrement; } | |
| 33 | + public String getSModuleNameZh() { return sModuleNameZh; } | |
| 34 | + public void setSModuleNameZh(String sModuleNameZh) { this.sModuleNameZh = sModuleNameZh; } | |
| 35 | + public String getSDisplayType() { return sDisplayType; } | |
| 36 | + public void setSDisplayType(String sDisplayType) { this.sDisplayType = sDisplayType; } | |
| 37 | + public String getSManageDeptEn() { return sManageDeptEn; } | |
| 38 | + public void setSManageDeptEn(String sManageDeptEn) { this.sManageDeptEn = sManageDeptEn; } | |
| 39 | + public Integer getIParentId() { return iParentId; } | |
| 40 | + public void setIParentId(Integer iParentId) { this.iParentId = iParentId; } | |
| 41 | + public Integer getISortOrder() { return iSortOrder; } | |
| 42 | + public void setISortOrder(Integer iSortOrder) { this.iSortOrder = iSortOrder; } | |
| 43 | + public List<ModuleTreeVO> getChildren() { return children; } | |
| 44 | + public void setChildren(List<ModuleTreeVO> children) { this.children = children; } | |
| 45 | +} | ... | ... |
backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java
| ... | ... | @@ -8,6 +8,7 @@ import com.xly.erp.module.mod.dto.UpdateModuleDTO; |
| 8 | 8 | import com.xly.erp.module.mod.entity.Module; |
| 9 | 9 | import com.xly.erp.module.mod.mapper.ModuleMapper; |
| 10 | 10 | import com.xly.erp.module.mod.service.impl.ModuleServiceImpl; |
| 11 | +import com.xly.erp.module.mod.vo.ModuleTreeVO; | |
| 11 | 12 | import org.junit.jupiter.api.AfterEach; |
| 12 | 13 | import org.junit.jupiter.api.BeforeEach; |
| 13 | 14 | import org.junit.jupiter.api.Test; |
| ... | ... | @@ -17,6 +18,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio |
| 17 | 18 | import org.springframework.security.core.context.SecurityContextHolder; |
| 18 | 19 | |
| 19 | 20 | import java.util.Collections; |
| 21 | +import java.util.List; | |
| 20 | 22 | |
| 21 | 23 | import static org.assertj.core.api.Assertions.assertThat; |
| 22 | 24 | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
| ... | ... | @@ -306,6 +308,85 @@ class ModuleServiceImplTest { |
| 306 | 308 | assertThat(captor.getValue().getSDeletedBy()).isEqualTo("BOB"); |
| 307 | 309 | } |
| 308 | 310 | |
| 311 | + @Test | |
| 312 | + void listTree_emptyKeyword_invokesMapperWithEmptyString_returnsAssembledTree() { | |
| 313 | + Module root1 = treeRow(1, "根1", null, 0); | |
| 314 | + Module root2 = treeRow(2, "根2", null, 0); | |
| 315 | + Module child1 = treeRow(3, "子1", 1, 0); | |
| 316 | + Module child2 = treeRow(4, "子2", 1, 1); | |
| 317 | + Module grand1 = treeRow(5, "孙1", 3, 0); | |
| 318 | + when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of(root1, root2, child1, child2, grand1)); | |
| 319 | + | |
| 320 | + List<ModuleTreeVO> result = service.listTree(""); | |
| 321 | + | |
| 322 | + assertThat(result).extracting(ModuleTreeVO::getIIncrement).containsExactly(1, 2); | |
| 323 | + assertThat(result.get(0).getChildren()).extracting(ModuleTreeVO::getIIncrement).containsExactly(3, 4); | |
| 324 | + assertThat(result.get(0).getChildren().get(0).getChildren()).extracting(ModuleTreeVO::getIIncrement).containsExactly(5); | |
| 325 | + assertThat(result.get(1).getChildren()).isEmpty(); | |
| 326 | + } | |
| 327 | + | |
| 328 | + @Test | |
| 329 | + void listTree_nullKeyword_treatedAsEmpty() { | |
| 330 | + when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of()); | |
| 331 | + service.listTree(null); | |
| 332 | + ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); | |
| 333 | + verify(moduleMapper).selectActiveByKeyword(captor.capture()); | |
| 334 | + assertThat(captor.getValue()).isEqualTo(""); | |
| 335 | + } | |
| 336 | + | |
| 337 | + @Test | |
| 338 | + void listTree_blankKeyword_treatedAsEmpty() { | |
| 339 | + when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of()); | |
| 340 | + service.listTree(" "); | |
| 341 | + verify(moduleMapper).selectActiveByKeyword(""); | |
| 342 | + } | |
| 343 | + | |
| 344 | + @Test | |
| 345 | + void listTree_keywordTooLong_throws40001() { | |
| 346 | + String longKw = "x".repeat(101); | |
| 347 | + assertThatThrownBy(() -> service.listTree(longKw)) | |
| 348 | + .isInstanceOf(BizException.class) | |
| 349 | + .hasFieldOrPropertyWithValue("code", 40001); | |
| 350 | + verify(moduleMapper, never()).selectActiveByKeyword(any()); | |
| 351 | + } | |
| 352 | + | |
| 353 | + @Test | |
| 354 | + void listTree_returnsEmptyListWhenNoMatch() { | |
| 355 | + when(moduleMapper.selectActiveByKeyword("xyz")).thenReturn(List.of()); | |
| 356 | + List<ModuleTreeVO> result = service.listTree("xyz"); | |
| 357 | + assertThat(result).isEmpty(); | |
| 358 | + } | |
| 359 | + | |
| 360 | + @Test | |
| 361 | + void listTree_orphansBecomeRootsInForest() { | |
| 362 | + Module orphan = treeRow(3, "孤儿", 99, 0); | |
| 363 | + when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of(orphan)); | |
| 364 | + | |
| 365 | + List<ModuleTreeVO> result = service.listTree(""); | |
| 366 | + | |
| 367 | + assertThat(result).hasSize(1); | |
| 368 | + assertThat(result.get(0).getIIncrement()).isEqualTo(3); | |
| 369 | + assertThat(result.get(0).getChildren()).isEmpty(); | |
| 370 | + } | |
| 371 | + | |
| 372 | + @Test | |
| 373 | + void listTree_keywordIsTrimmedBeforeQuery() { | |
| 374 | + when(moduleMapper.selectActiveByKeyword("系统")).thenReturn(List.of()); | |
| 375 | + service.listTree(" 系统 "); | |
| 376 | + verify(moduleMapper).selectActiveByKeyword("系统"); | |
| 377 | + } | |
| 378 | + | |
| 379 | + private Module treeRow(int id, String name, Integer parentId, int sortOrder) { | |
| 380 | + Module m = new Module(); | |
| 381 | + m.setIIncrement(id); | |
| 382 | + m.setSModuleNameZh(name); | |
| 383 | + m.setSDisplayType("手机端"); | |
| 384 | + m.setSManageDeptEn("IT"); | |
| 385 | + m.setIParentId(parentId); | |
| 386 | + m.setISortOrder(sortOrder); | |
| 387 | + return m; | |
| 388 | + } | |
| 389 | + | |
| 309 | 390 | private UpdateModuleDTO baseUpdateDto() { |
| 310 | 391 | UpdateModuleDTO dto = new UpdateModuleDTO(); |
| 311 | 392 | dto.setSDisplayType("手机端"); | ... | ... |