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,6 +2,9 @@ package com.xly.erp.module.mod.service; | ||
| 2 | 2 | ||
| 3 | import com.xly.erp.module.mod.dto.CreateModuleDTO; | 3 | import com.xly.erp.module.mod.dto.CreateModuleDTO; |
| 4 | import com.xly.erp.module.mod.dto.UpdateModuleDTO; | 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 | public interface ModuleService { | 9 | public interface ModuleService { |
| 7 | Integer create(CreateModuleDTO dto); | 10 | Integer create(CreateModuleDTO dto); |
| @@ -9,4 +12,6 @@ public interface ModuleService { | @@ -9,4 +12,6 @@ public interface ModuleService { | ||
| 9 | Integer update(Integer id, UpdateModuleDTO dto); | 12 | Integer update(Integer id, UpdateModuleDTO dto); |
| 10 | 13 | ||
| 11 | void delete(Integer id); | 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,6 +7,7 @@ import com.xly.erp.common.security.SecurityContextHelper; | ||
| 7 | import com.xly.erp.module.mod.dto.CreateModuleDTO; | 7 | import com.xly.erp.module.mod.dto.CreateModuleDTO; |
| 8 | import com.xly.erp.module.mod.dto.UpdateModuleDTO; | 8 | import com.xly.erp.module.mod.dto.UpdateModuleDTO; |
| 9 | import com.xly.erp.module.mod.entity.Module; | 9 | import com.xly.erp.module.mod.entity.Module; |
| 10 | +import com.xly.erp.module.mod.vo.ModuleTreeVO; | ||
| 10 | import com.xly.erp.module.mod.mapper.ModuleMapper; | 11 | import com.xly.erp.module.mod.mapper.ModuleMapper; |
| 11 | import com.xly.erp.module.mod.service.ModuleService; | 12 | import com.xly.erp.module.mod.service.ModuleService; |
| 12 | import org.springframework.dao.DuplicateKeyException; | 13 | import org.springframework.dao.DuplicateKeyException; |
| @@ -14,6 +15,10 @@ import org.springframework.stereotype.Service; | @@ -14,6 +15,10 @@ import org.springframework.stereotype.Service; | ||
| 14 | import org.springframework.transaction.annotation.Transactional; | 15 | import org.springframework.transaction.annotation.Transactional; |
| 15 | 16 | ||
| 16 | import java.time.LocalDateTime; | 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 | import java.util.Set; | 22 | import java.util.Set; |
| 18 | 23 | ||
| 19 | @Service | 24 | @Service |
| @@ -112,6 +117,42 @@ public class ModuleServiceImpl implements ModuleService { | @@ -112,6 +117,42 @@ public class ModuleServiceImpl implements ModuleService { | ||
| 112 | moduleMapper.updateById(entity); | 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 | private void validateParent(Integer id, Integer parentId) { | 156 | private void validateParent(Integer id, Integer parentId) { |
| 116 | if (parentId == null) { | 157 | if (parentId == null) { |
| 117 | return; | 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,6 +8,7 @@ import com.xly.erp.module.mod.dto.UpdateModuleDTO; | ||
| 8 | import com.xly.erp.module.mod.entity.Module; | 8 | import com.xly.erp.module.mod.entity.Module; |
| 9 | import com.xly.erp.module.mod.mapper.ModuleMapper; | 9 | import com.xly.erp.module.mod.mapper.ModuleMapper; |
| 10 | import com.xly.erp.module.mod.service.impl.ModuleServiceImpl; | 10 | import com.xly.erp.module.mod.service.impl.ModuleServiceImpl; |
| 11 | +import com.xly.erp.module.mod.vo.ModuleTreeVO; | ||
| 11 | import org.junit.jupiter.api.AfterEach; | 12 | import org.junit.jupiter.api.AfterEach; |
| 12 | import org.junit.jupiter.api.BeforeEach; | 13 | import org.junit.jupiter.api.BeforeEach; |
| 13 | import org.junit.jupiter.api.Test; | 14 | import org.junit.jupiter.api.Test; |
| @@ -17,6 +18,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio | @@ -17,6 +18,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio | ||
| 17 | import org.springframework.security.core.context.SecurityContextHolder; | 18 | import org.springframework.security.core.context.SecurityContextHolder; |
| 18 | 19 | ||
| 19 | import java.util.Collections; | 20 | import java.util.Collections; |
| 21 | +import java.util.List; | ||
| 20 | 22 | ||
| 21 | import static org.assertj.core.api.Assertions.assertThat; | 23 | import static org.assertj.core.api.Assertions.assertThat; |
| 22 | import static org.assertj.core.api.Assertions.assertThatThrownBy; | 24 | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
| @@ -306,6 +308,85 @@ class ModuleServiceImplTest { | @@ -306,6 +308,85 @@ class ModuleServiceImplTest { | ||
| 306 | assertThat(captor.getValue().getSDeletedBy()).isEqualTo("BOB"); | 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 | private UpdateModuleDTO baseUpdateDto() { | 390 | private UpdateModuleDTO baseUpdateDto() { |
| 310 | UpdateModuleDTO dto = new UpdateModuleDTO(); | 391 | UpdateModuleDTO dto = new UpdateModuleDTO(); |
| 311 | dto.setSDisplayType("手机端"); | 392 | dto.setSDisplayType("手机端"); |