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-tddexecutes 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<Module> selectActiveByKeyword(@Param("keyword") String keyword)注解 SELECT -
backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java— 追加List<ModuleTreeVO> 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
<type>(mod): <subject> 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<Module> 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:
-
ModuleTreeVOPOJO:字段iIncrement/sModuleNameZh/sDisplayType/sManageDeptEn/iParentId/iSortOrder/List<ModuleTreeVO> children(默认 new ArrayList<>());getter/setter;含@JsonProperty锁定 JSON 名(与 DTO 风格一致)。 ModuleService#listTree(String keyword) : List<ModuleTreeVO>-
ModuleServiceImpl#listTree(String keyword):String normalized = keyword == null ? "" : keyword.trim()if (normalized.length() > 100) throw new BizException(40001, "keyword 长度超过 100 字符")List<Module> rows = moduleMapper.selectActiveByKeyword(normalized)- 拼树:建
Map<Integer, ModuleTreeVO> idIndex,遍历 rows 转 VO 入 map;二次遍历:parent 在 map → 挂入 parent.children;否则视为 root 加入返回 list - 返回 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<ModuleTreeVO>> 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:keyword101 字符 →code=40001 -
getNoMatch_returnsEmptyArray:keyword=不存在的关键字XYZ;dataJsonNode 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 |