Commit a46be5d2ef437ad43a6bb96db456ef39c04da92d
1 parent
f9a9070e
feat(mod): GET /api/modules controller REQ-MOD-004
- 新增 @GetMapping 树查询端点 + 7 个集成测试。 - 修复 maven-surefire-plugin 默认 includes 不含 *IT.java 的配置缺陷, 让 ModuleMapperIT / ModuleControllerIT 也参与 mvn test 全量。
Showing
3 changed files
with
132 additions
and
0 deletions
backend/pom.xml
| @@ -110,6 +110,17 @@ | @@ -110,6 +110,17 @@ | ||
| 110 | </plugin> | 110 | </plugin> |
| 111 | <plugin> | 111 | <plugin> |
| 112 | <groupId>org.apache.maven.plugins</groupId> | 112 | <groupId>org.apache.maven.plugins</groupId> |
| 113 | + <artifactId>maven-surefire-plugin</artifactId> | ||
| 114 | + <configuration> | ||
| 115 | + <includes> | ||
| 116 | + <include>**/*Test.java</include> | ||
| 117 | + <include>**/*Tests.java</include> | ||
| 118 | + <include>**/*IT.java</include> | ||
| 119 | + </includes> | ||
| 120 | + </configuration> | ||
| 121 | + </plugin> | ||
| 122 | + <plugin> | ||
| 123 | + <groupId>org.apache.maven.plugins</groupId> | ||
| 113 | <artifactId>maven-compiler-plugin</artifactId> | 124 | <artifactId>maven-compiler-plugin</artifactId> |
| 114 | <configuration> | 125 | <configuration> |
| 115 | <source>${java.version}</source> | 126 | <source>${java.version}</source> |
backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java
| @@ -2,13 +2,16 @@ package com.xly.erp.module.mod.controller; | @@ -2,13 +2,16 @@ package com.xly.erp.module.mod.controller; | ||
| 2 | 2 | ||
| 3 | import com.xly.erp.common.response.ApiResponse; | 3 | import com.xly.erp.common.response.ApiResponse; |
| 4 | import com.xly.erp.module.mod.dto.ModuleCreateDTO; | 4 | import com.xly.erp.module.mod.dto.ModuleCreateDTO; |
| 5 | +import com.xly.erp.module.mod.dto.ModuleQueryDTO; | ||
| 5 | import com.xly.erp.module.mod.dto.ModuleUpdateDTO; | 6 | import com.xly.erp.module.mod.dto.ModuleUpdateDTO; |
| 6 | import com.xly.erp.module.mod.service.ModuleService; | 7 | import com.xly.erp.module.mod.service.ModuleService; |
| 7 | import com.xly.erp.module.mod.vo.ModuleDeleteResultVO; | 8 | import com.xly.erp.module.mod.vo.ModuleDeleteResultVO; |
| 9 | +import com.xly.erp.module.mod.vo.ModuleTreeNodeVO; | ||
| 8 | import com.xly.erp.module.mod.vo.ModuleVO; | 10 | import com.xly.erp.module.mod.vo.ModuleVO; |
| 9 | import jakarta.validation.Valid; | 11 | import jakarta.validation.Valid; |
| 10 | import lombok.RequiredArgsConstructor; | 12 | import lombok.RequiredArgsConstructor; |
| 11 | import org.springframework.web.bind.annotation.DeleteMapping; | 13 | import org.springframework.web.bind.annotation.DeleteMapping; |
| 14 | +import org.springframework.web.bind.annotation.GetMapping; | ||
| 12 | import org.springframework.web.bind.annotation.PathVariable; | 15 | import org.springframework.web.bind.annotation.PathVariable; |
| 13 | import org.springframework.web.bind.annotation.PostMapping; | 16 | import org.springframework.web.bind.annotation.PostMapping; |
| 14 | import org.springframework.web.bind.annotation.PutMapping; | 17 | import org.springframework.web.bind.annotation.PutMapping; |
| @@ -16,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestBody; | @@ -16,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestBody; | ||
| 16 | import org.springframework.web.bind.annotation.RequestMapping; | 19 | import org.springframework.web.bind.annotation.RequestMapping; |
| 17 | import org.springframework.web.bind.annotation.RestController; | 20 | import org.springframework.web.bind.annotation.RestController; |
| 18 | 21 | ||
| 22 | +import java.util.List; | ||
| 23 | + | ||
| 19 | @RestController | 24 | @RestController |
| 20 | @RequestMapping("/api/modules") | 25 | @RequestMapping("/api/modules") |
| 21 | @RequiredArgsConstructor | 26 | @RequiredArgsConstructor |
| @@ -40,4 +45,10 @@ public class ModuleController { | @@ -40,4 +45,10 @@ public class ModuleController { | ||
| 40 | public ApiResponse<ModuleDeleteResultVO> delete(@PathVariable Integer id) { | 45 | public ApiResponse<ModuleDeleteResultVO> delete(@PathVariable Integer id) { |
| 41 | return ApiResponse.ok(moduleService.delete(id)); | 46 | return ApiResponse.ok(moduleService.delete(id)); |
| 42 | } | 47 | } |
| 48 | + | ||
| 49 | + /** REQ-MOD-004 模块树查询 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:READ')") */ | ||
| 50 | + @GetMapping | ||
| 51 | + public ApiResponse<List<ModuleTreeNodeVO>> tree(@Valid ModuleQueryDTO query) { | ||
| 52 | + return ApiResponse.ok(moduleService.tree(query)); | ||
| 53 | + } | ||
| 43 | } | 54 | } |
backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java
| @@ -19,6 +19,7 @@ import java.time.LocalDateTime; | @@ -19,6 +19,7 @@ import java.time.LocalDateTime; | ||
| 19 | 19 | ||
| 20 | import static org.assertj.core.api.Assertions.assertThat; | 20 | import static org.assertj.core.api.Assertions.assertThat; |
| 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; | 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; |
| 22 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||
| 22 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | 23 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; |
| 23 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; | 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; |
| 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
| @@ -418,4 +419,113 @@ class ModuleControllerIT { | @@ -418,4 +419,113 @@ class ModuleControllerIT { | ||
| 418 | 419 | ||
| 419 | assertThat(moduleMapper.selectById(id).getSProcedureName()).isEqualTo(origProc); | 420 | assertThat(moduleMapper.selectById(id).getSProcedureName()).isEqualTo(origProc); |
| 420 | } | 421 | } |
| 422 | + | ||
| 423 | + // ============================================================ | ||
| 424 | + // REQ-MOD-004 GET 系列 | ||
| 425 | + // ============================================================ | ||
| 426 | + | ||
| 427 | + @Test | ||
| 428 | + void get_emptyKeyword_returnsAllUndeletedAsTree() throws Exception { | ||
| 429 | + // 插入一个 root + 一个 child;不带 keyword 的 GET 应能看到二者 | ||
| 430 | + Integer rootId = insertExisting("sp_get_root_" + System.nanoTime(), null); | ||
| 431 | + Integer childId = insertExisting("sp_get_child_" + System.nanoTime(), rootId); | ||
| 432 | + | ||
| 433 | + mockMvc.perform(get("/api/modules")) | ||
| 434 | + .andExpect(status().isOk()) | ||
| 435 | + .andExpect(jsonPath("$.code").value(200)) | ||
| 436 | + .andExpect(jsonPath("$.data[?(@.iIncrement==" + rootId + ")]").exists()) | ||
| 437 | + .andExpect(jsonPath("$.data[?(@.iIncrement==" + rootId + ")].children[?(@.iIncrement==" + childId + ")]").exists()); | ||
| 438 | + } | ||
| 439 | + | ||
| 440 | + @Test | ||
| 441 | + void get_keyword_filtersByModuleNameZhWithAncestors() throws Exception { | ||
| 442 | + // grandparent("系统配置") -> parent("用户管理") -> child("登录认证") | ||
| 443 | + ModuleEntity gp = new ModuleEntity(); | ||
| 444 | + gp.setSDisplayType("前端业务"); gp.setSProcedureName("sp_get_kw_gp_" + System.nanoTime()); | ||
| 445 | + gp.setSModuleType("MOD"); gp.setSManageDeptEn("IT"); gp.setBShowPermission(false); | ||
| 446 | + gp.setSModuleNameZh("系统配置-keyword test"); gp.setIParentId(null); gp.setISortOrder(0); | ||
| 447 | + gp.setBDeleted(false); gp.setTCreateDate(LocalDateTime.now()); | ||
| 448 | + moduleMapper.insert(gp); | ||
| 449 | + | ||
| 450 | + ModuleEntity p = new ModuleEntity(); | ||
| 451 | + p.setSDisplayType("前端业务"); p.setSProcedureName("sp_get_kw_p_" + System.nanoTime()); | ||
| 452 | + p.setSModuleType("MOD"); p.setSManageDeptEn("IT"); p.setBShowPermission(false); | ||
| 453 | + p.setSModuleNameZh("用户管理-keyword test"); p.setIParentId(gp.getIIncrement()); p.setISortOrder(0); | ||
| 454 | + p.setBDeleted(false); p.setTCreateDate(LocalDateTime.now()); | ||
| 455 | + moduleMapper.insert(p); | ||
| 456 | + | ||
| 457 | + ModuleEntity c = new ModuleEntity(); | ||
| 458 | + c.setSDisplayType("前端业务"); c.setSProcedureName("sp_get_kw_c_" + System.nanoTime()); | ||
| 459 | + c.setSModuleType("MOD"); c.setSManageDeptEn("IT"); c.setBShowPermission(false); | ||
| 460 | + c.setSModuleNameZh("唯一登录认证关键词"); c.setIParentId(p.getIIncrement()); c.setISortOrder(0); | ||
| 461 | + c.setBDeleted(false); c.setTCreateDate(LocalDateTime.now()); | ||
| 462 | + moduleMapper.insert(c); | ||
| 463 | + | ||
| 464 | + mockMvc.perform(get("/api/modules").param("keyword", "唯一登录认证关键词")) | ||
| 465 | + .andExpect(status().isOk()) | ||
| 466 | + .andExpect(jsonPath("$.code").value(200)) | ||
| 467 | + // 命中 child + 全部祖先:grandparent 在 root 数组中 | ||
| 468 | + .andExpect(jsonPath("$.data[?(@.iIncrement==" + gp.getIIncrement() + ")]").exists()) | ||
| 469 | + // grandparent.children 含 parent | ||
| 470 | + .andExpect(jsonPath("$.data[?(@.iIncrement==" + gp.getIIncrement() + ")].children[?(@.iIncrement==" + p.getIIncrement() + ")]").exists()) | ||
| 471 | + // parent.children 含 child | ||
| 472 | + .andExpect(jsonPath("$.data[?(@.iIncrement==" + gp.getIIncrement() + ")].children[?(@.iIncrement==" + p.getIIncrement() + ")].children[?(@.iIncrement==" + c.getIIncrement() + ")]").exists()); | ||
| 473 | + } | ||
| 474 | + | ||
| 475 | + @Test | ||
| 476 | + void get_keywordNoMatch_returnsEmptyArray() throws Exception { | ||
| 477 | + insertExisting("sp_get_nm_" + System.nanoTime(), null); | ||
| 478 | + | ||
| 479 | + mockMvc.perform(get("/api/modules").param("keyword", "绝对不存在的关键词xyz")) | ||
| 480 | + .andExpect(status().isOk()) | ||
| 481 | + .andExpect(jsonPath("$.code").value(200)) | ||
| 482 | + .andExpect(jsonPath("$.data").isArray()) | ||
| 483 | + .andExpect(jsonPath("$.data.length()").value(0)); | ||
| 484 | + } | ||
| 485 | + | ||
| 486 | + @Test | ||
| 487 | + void get_keywordTooLong_returns40010() throws Exception { | ||
| 488 | + String longKw = "a".repeat(51); | ||
| 489 | + mockMvc.perform(get("/api/modules").param("keyword", longKw)) | ||
| 490 | + .andExpect(status().isOk()) | ||
| 491 | + .andExpect(jsonPath("$.code").value(40010)); | ||
| 492 | + } | ||
| 493 | + | ||
| 494 | + @Test | ||
| 495 | + void get_softDeletedNotInResult() throws Exception { | ||
| 496 | + Integer id = insertExisting("sp_get_sd_" + System.nanoTime(), null); | ||
| 497 | + // 软删除该模块 | ||
| 498 | + ModuleEntity patch = new ModuleEntity(); | ||
| 499 | + patch.setIIncrement(id); | ||
| 500 | + patch.setBDeleted(true); | ||
| 501 | + moduleMapper.updateById(patch); | ||
| 502 | + | ||
| 503 | + mockMvc.perform(get("/api/modules")) | ||
| 504 | + .andExpect(status().isOk()) | ||
| 505 | + .andExpect(jsonPath("$.data[?(@.iIncrement==" + id + ")]").doesNotExist()); | ||
| 506 | + } | ||
| 507 | + | ||
| 508 | + @Test | ||
| 509 | + void get_responseExcludesInternalFields() throws Exception { | ||
| 510 | + insertExisting("sp_get_priv_" + System.nanoTime(), null); | ||
| 511 | + | ||
| 512 | + mockMvc.perform(get("/api/modules")) | ||
| 513 | + .andExpect(status().isOk()) | ||
| 514 | + // 树节点不应包含内部字段 | ||
| 515 | + .andExpect(jsonPath("$.data[0].sProcedureName").doesNotExist()) | ||
| 516 | + .andExpect(jsonPath("$.data[0].sModuleType").doesNotExist()) | ||
| 517 | + .andExpect(jsonPath("$.data[0].bShowPermission").doesNotExist()) | ||
| 518 | + .andExpect(jsonPath("$.data[0].tCreateDate").doesNotExist()) | ||
| 519 | + .andExpect(jsonPath("$.data[0].bDeleted").doesNotExist()); | ||
| 520 | + } | ||
| 521 | + | ||
| 522 | + @Test | ||
| 523 | + void get_leafNodeChildrenIsEmptyArrayNotNull() throws Exception { | ||
| 524 | + Integer id = insertExisting("sp_get_leaf_" + System.nanoTime(), null); | ||
| 525 | + | ||
| 526 | + mockMvc.perform(get("/api/modules")) | ||
| 527 | + .andExpect(status().isOk()) | ||
| 528 | + .andExpect(jsonPath("$.data[?(@.iIncrement==" + id + ")].children").isArray()) | ||
| 529 | + .andExpect(jsonPath("$.data[?(@.iIncrement==" + id + ")].children.length()").value(0)); | ||
| 530 | + } | ||
| 421 | } | 531 | } |