diff --git a/backend/pom.xml b/backend/pom.xml
index bdeb6a1..ffbee2a 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -110,6 +110,17 @@
org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ **/*Test.java
+ **/*Tests.java
+ **/*IT.java
+
+
+
+
+ org.apache.maven.plugins
maven-compiler-plugin
${java.version}
diff --git a/backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java b/backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java
index 0b177b3..12365c4 100644
--- a/backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java
+++ b/backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java
@@ -2,13 +2,16 @@ package com.xly.erp.module.mod.controller;
import com.xly.erp.common.response.ApiResponse;
import com.xly.erp.module.mod.dto.ModuleCreateDTO;
+import com.xly.erp.module.mod.dto.ModuleQueryDTO;
import com.xly.erp.module.mod.dto.ModuleUpdateDTO;
import com.xly.erp.module.mod.service.ModuleService;
import com.xly.erp.module.mod.vo.ModuleDeleteResultVO;
+import com.xly.erp.module.mod.vo.ModuleTreeNodeVO;
import com.xly.erp.module.mod.vo.ModuleVO;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
@@ -16,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import java.util.List;
+
@RestController
@RequestMapping("/api/modules")
@RequiredArgsConstructor
@@ -40,4 +45,10 @@ public class ModuleController {
public ApiResponse delete(@PathVariable Integer id) {
return ApiResponse.ok(moduleService.delete(id));
}
+
+ /** REQ-MOD-004 模块树查询 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:READ')") */
+ @GetMapping
+ public ApiResponse> tree(@Valid ModuleQueryDTO query) {
+ return ApiResponse.ok(moduleService.tree(query));
+ }
}
diff --git a/backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java b/backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java
index 159146d..de75253 100644
--- a/backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java
+++ b/backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java
@@ -19,6 +19,7 @@ import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -418,4 +419,113 @@ class ModuleControllerIT {
assertThat(moduleMapper.selectById(id).getSProcedureName()).isEqualTo(origProc);
}
+
+ // ============================================================
+ // REQ-MOD-004 GET 系列
+ // ============================================================
+
+ @Test
+ void get_emptyKeyword_returnsAllUndeletedAsTree() throws Exception {
+ // 插入一个 root + 一个 child;不带 keyword 的 GET 应能看到二者
+ Integer rootId = insertExisting("sp_get_root_" + System.nanoTime(), null);
+ Integer childId = insertExisting("sp_get_child_" + System.nanoTime(), rootId);
+
+ mockMvc.perform(get("/api/modules"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data[?(@.iIncrement==" + rootId + ")]").exists())
+ .andExpect(jsonPath("$.data[?(@.iIncrement==" + rootId + ")].children[?(@.iIncrement==" + childId + ")]").exists());
+ }
+
+ @Test
+ void get_keyword_filtersByModuleNameZhWithAncestors() throws Exception {
+ // grandparent("系统配置") -> parent("用户管理") -> child("登录认证")
+ ModuleEntity gp = new ModuleEntity();
+ gp.setSDisplayType("前端业务"); gp.setSProcedureName("sp_get_kw_gp_" + System.nanoTime());
+ gp.setSModuleType("MOD"); gp.setSManageDeptEn("IT"); gp.setBShowPermission(false);
+ gp.setSModuleNameZh("系统配置-keyword test"); gp.setIParentId(null); gp.setISortOrder(0);
+ gp.setBDeleted(false); gp.setTCreateDate(LocalDateTime.now());
+ moduleMapper.insert(gp);
+
+ ModuleEntity p = new ModuleEntity();
+ p.setSDisplayType("前端业务"); p.setSProcedureName("sp_get_kw_p_" + System.nanoTime());
+ p.setSModuleType("MOD"); p.setSManageDeptEn("IT"); p.setBShowPermission(false);
+ p.setSModuleNameZh("用户管理-keyword test"); p.setIParentId(gp.getIIncrement()); p.setISortOrder(0);
+ p.setBDeleted(false); p.setTCreateDate(LocalDateTime.now());
+ moduleMapper.insert(p);
+
+ ModuleEntity c = new ModuleEntity();
+ c.setSDisplayType("前端业务"); c.setSProcedureName("sp_get_kw_c_" + System.nanoTime());
+ c.setSModuleType("MOD"); c.setSManageDeptEn("IT"); c.setBShowPermission(false);
+ c.setSModuleNameZh("唯一登录认证关键词"); c.setIParentId(p.getIIncrement()); c.setISortOrder(0);
+ c.setBDeleted(false); c.setTCreateDate(LocalDateTime.now());
+ moduleMapper.insert(c);
+
+ mockMvc.perform(get("/api/modules").param("keyword", "唯一登录认证关键词"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ // 命中 child + 全部祖先:grandparent 在 root 数组中
+ .andExpect(jsonPath("$.data[?(@.iIncrement==" + gp.getIIncrement() + ")]").exists())
+ // grandparent.children 含 parent
+ .andExpect(jsonPath("$.data[?(@.iIncrement==" + gp.getIIncrement() + ")].children[?(@.iIncrement==" + p.getIIncrement() + ")]").exists())
+ // parent.children 含 child
+ .andExpect(jsonPath("$.data[?(@.iIncrement==" + gp.getIIncrement() + ")].children[?(@.iIncrement==" + p.getIIncrement() + ")].children[?(@.iIncrement==" + c.getIIncrement() + ")]").exists());
+ }
+
+ @Test
+ void get_keywordNoMatch_returnsEmptyArray() throws Exception {
+ insertExisting("sp_get_nm_" + System.nanoTime(), null);
+
+ mockMvc.perform(get("/api/modules").param("keyword", "绝对不存在的关键词xyz"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data").isArray())
+ .andExpect(jsonPath("$.data.length()").value(0));
+ }
+
+ @Test
+ void get_keywordTooLong_returns40010() throws Exception {
+ String longKw = "a".repeat(51);
+ mockMvc.perform(get("/api/modules").param("keyword", longKw))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(40010));
+ }
+
+ @Test
+ void get_softDeletedNotInResult() throws Exception {
+ Integer id = insertExisting("sp_get_sd_" + System.nanoTime(), null);
+ // 软删除该模块
+ ModuleEntity patch = new ModuleEntity();
+ patch.setIIncrement(id);
+ patch.setBDeleted(true);
+ moduleMapper.updateById(patch);
+
+ mockMvc.perform(get("/api/modules"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data[?(@.iIncrement==" + id + ")]").doesNotExist());
+ }
+
+ @Test
+ void get_responseExcludesInternalFields() throws Exception {
+ insertExisting("sp_get_priv_" + System.nanoTime(), null);
+
+ mockMvc.perform(get("/api/modules"))
+ .andExpect(status().isOk())
+ // 树节点不应包含内部字段
+ .andExpect(jsonPath("$.data[0].sProcedureName").doesNotExist())
+ .andExpect(jsonPath("$.data[0].sModuleType").doesNotExist())
+ .andExpect(jsonPath("$.data[0].bShowPermission").doesNotExist())
+ .andExpect(jsonPath("$.data[0].tCreateDate").doesNotExist())
+ .andExpect(jsonPath("$.data[0].bDeleted").doesNotExist());
+ }
+
+ @Test
+ void get_leafNodeChildrenIsEmptyArrayNotNull() throws Exception {
+ Integer id = insertExisting("sp_get_leaf_" + System.nanoTime(), null);
+
+ mockMvc.perform(get("/api/modules"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data[?(@.iIncrement==" + id + ")].children").isArray())
+ .andExpect(jsonPath("$.data[?(@.iIncrement==" + id + ")].children.length()").value(0));
+ }
}