--- req_id: REQ-MOD-004 date: 2026-04-29 spec_ref: docs/superpowers/specs/2026-04-29-REQ-MOD-004.md --- # REQ-MOD-004 模块查询 Implementation Plan > **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. **Goal:** 在 MOD-001~003 已建工程基础上增量实现 `GET /api/mod/modules` 模块树查询:DB 模糊匹配 + 内存拼装森林。 **Architecture:** 复用 `ModuleService` / `ModuleServiceImpl` / `ModuleController` / `ModuleMapper`;新增 `ModuleTreeVO`、`mapper.selectActiveByKeyword(String)`、`service.listTree(String)`、controller `@GetMapping`。无新外部依赖。空 keyword 由 controller 归一化为 `""`,超长校验在 service。SecurityConfig 已对 `/api/mod/**` permitAll 覆盖该接口。 **Tech Stack:** 沿用(Spring Boot 3.3.5 / MyBatis-Plus / JUnit 5 + Mockito + TestRestTemplate)。 --- ## Schema 改动 无(仅 SELECT)。 ## 文件变更清单 ### 新增 - `backend/src/main/java/com/xly/erp/module/mod/vo/ModuleTreeVO.java` — 树节点出参 VO ### 修改 - `backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java` — 追加 `List selectActiveByKeyword(@Param("keyword") String keyword)` 注解 SELECT - `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java` — 追加 `List listTree(String keyword)` - `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` — 实现 listTree(trim + 长度校验 + 拼树) - `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` — 追加 `@GetMapping("/modules")` - `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java` — 追加 7 用例 - `backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java` — 追加 1 用例 - `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java` — 追加 6 用例 ## 任务步骤 > 全局:每 commit `(mod): REQ-MOD-004`;测试派发子会话;现有 53 用例全程绿。 ### Task 1: Mapper#selectActiveByKeyword + IT **Files:** - Modify: `backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java` - Modify: `backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java` **API shape:** - `@Select("SELECT iIncrement, sModuleNameZh, sDisplayType, sManageDeptEn, iParentId, iSortOrder FROM tModule WHERE bDeleted = 0 AND sModuleNameZh LIKE CONCAT('%', #{keyword}, '%') ORDER BY iSortOrder ASC, iIncrement ASC")` - `List selectActiveByKeyword(@Param("keyword") String keyword)` - 返回的 Module 实例只填查询的 6 列;其他字段为 null(MyBatis 默认行为) - [ ] **Step 1: 写失败测试 `ModuleMapperIT#selectActiveByKeyword_filtersAndOrders`** - 准备 5 行:A "系统-A" iSortOrder=1; B "系统-B" iSortOrder=0; C "用户" iSortOrder=2; D "系统-D" bDeleted=1; E "测试" iSortOrder=3 - 断言:`selectActiveByKeyword("")` → 4 行,顺序 [B(0), A(1), C(2), E(3)](D 被 bDeleted 过滤) - 断言:`selectActiveByKeyword("系统")` → [B, A](D 被 bDeleted 过滤) - 断言:`selectActiveByKeyword("不存在XYZ")` → 空 list - [ ] **Step 2: 实现 mapper 方法** - [ ] **Step 3: 子会话验证 PASS** - 命令:`cd backend && mvn -B test -Dtest=ModuleMapperIT` - [ ] **Step 4: Commit** - `git commit -m "feat(mod): mapper#selectActiveByKeyword REQ-MOD-004"` ### Task 2: ModuleTreeVO + Service.listTree + 单测 **Files:** - Create: `backend/src/main/java/com/xly/erp/module/mod/vo/ModuleTreeVO.java` - Modify: `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java` - Modify: `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` - Modify: `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java` **API shape:** - `ModuleTreeVO` POJO:字段 `iIncrement` / `sModuleNameZh` / `sDisplayType` / `sManageDeptEn` / `iParentId` / `iSortOrder` / `List children`(默认 new ArrayList<>());getter/setter;含 `@JsonProperty` 锁定 JSON 名(与 DTO 风格一致)。 - `ModuleService#listTree(String keyword) : List` - `ModuleServiceImpl#listTree(String keyword)`: 1. `String normalized = keyword == null ? "" : keyword.trim()` 2. `if (normalized.length() > 100) throw new BizException(40001, "keyword 长度超过 100 字符")` 3. `List rows = moduleMapper.selectActiveByKeyword(normalized)` 4. 拼树:建 `Map idIndex`,遍历 rows 转 VO 入 map;二次遍历:parent 在 map → 挂入 parent.children;否则视为 root 加入返回 list 5. 返回 list(保持 SQL ORDER BY 顺序,孤立子节点出现在 root 列表中按其行序) - 类级 `@Transactional` 不影响只读;可在方法上加 `@Transactional(readOnly = true)` 显式覆盖(建议) - [ ] **Step 1: 写失败测试(7 用例)** - `listTree_emptyKeyword_invokesMapperWithEmptyString_returnsAssembledTree`:mock 返回 [root1(id=1,parent=null), root2(id=2,parent=null), child1(id=3,parent=1), child2(id=4,parent=1), grand1(id=5,parent=3)];断言返回 list size==2;root1.children size==2 含 child1+child2;child1.children 含 grand1;root2.children 空 - `listTree_nullKeyword_treatedAsEmpty`:参数 null;ArgumentCaptor 抓 mapper 入参 == "" - `listTree_blankKeyword_treatedAsEmpty`:参数 " ";mapper 入参 == "" - `listTree_keywordTooLong_throws40001`:参数 = "x".repeat(101);BizException(40001);mapper 永不调用 - `listTree_returnsEmptyListWhenNoMatch`:mock 返回 emptyList;返回 List.of() - `listTree_orphansBecomeRootsInForest`:mock 返回 [child(id=3,parent=99)];返回 list size==1,第 0 项 iIncrement=3,children 空 - `listTree_keywordIsTrimmedBeforeQuery`:参数 " 系统 ";mapper 入参 == "系统" - 子会话先跑 → 7 用例 FAIL - [ ] **Step 2: 实现 VO + service** - [ ] **Step 3: 子会话验证 PASS** - 命令:`cd backend && mvn -B test -Dtest=ModuleServiceImplTest` - 期望:18 (前) + 7 = 25 用例全绿 - [ ] **Step 4: Commit** - `git commit -m "feat(mod): module list tree service + vo REQ-MOD-004"` ### Task 3: Controller GET + 6 IT + 全量回归 **Files:** - Modify: `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` - Modify: `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java` **API shape:** - `@GetMapping("/modules") public Result> list(@RequestParam(required = false) String keyword)` - 返回 `Result.ok(moduleService.listTree(keyword))` - [ ] **Step 1: 写失败测试(6 用例)** - `getEmptyKeyword_returnsCompleteTreeAsForest`:直插 root + child(parent=root);GET 带 JWT `/api/mod/modules`;`code=0`,`data` 是数组;找出 iIncrement=root 的节点,children 含 iIncrement=child - `getKeywordMatch_returnsForest`:直插 "系统模块A"+"用户模块B";GET `?keyword=系统`;返回数组只含 sModuleNameZh 含"系统"的节点 - `getKeywordTooLong_returns40001`:`keyword` 101 字符 → `code=40001` - `getNoMatch_returnsEmptyArray`:`keyword=不存在的关键字XYZ`;`data` JsonNode isArray && size==0 - `getWithoutJwt_permitAllStub_returns200`:无 token GET;`code=0` - `getTamperedJwt_returns20001`:Authorization "Bearer not.a.real.jwt" → `code=20001` - 子会话先跑 → FAIL - [ ] **Step 2: 实现 controller** - [ ] **Step 3: 子会话跑全量回归** - 命令:`cd backend && mvn -B test` - 期望:MOD-001 26 + MOD-002 15 + MOD-003 12 + MOD-004 新增 1(mapperIT) + 7(svc) + 6(IT) = 67 用例全绿 - [ ] **Step 4: Commit** - `git commit -m "test(mod): module list integration coverage REQ-MOD-004"` ## 提交计划 | commit | 覆盖 | |---|---| | `feat(mod): mapper#selectActiveByKeyword REQ-MOD-004` | Task 1 | | `feat(mod): module list tree service + vo REQ-MOD-004` | Task 2 | | `test(mod): module list integration coverage REQ-MOD-004` | Task 3 |