req_id: REQ-MOD-004 date: 2026-05-06
spec_ref: docs/superpowers/specs/2026-05-06-REQ-MOD-004.md
REQ-MOD-004 模块查询 Implementation Plan
Execution: Parent skill
feature-tddexecutes this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: 实现 GET /api/modules?keyword=...:返回未软删除模块的树形结构,可选按 sModuleNameZh 模糊匹配并保留命中节点的祖先链。
Architecture: 一次性 selectList 拉所有未删除模块到内存,按 keyword 过滤命中集合,沿父链向上收集祖先合并到结果集,最后在内存按 iParentId 组装树。同级按 iSortOrder ASC + iIncrement ASC 排序。叶子 children=[]。
Tech Stack: 沿用 REQ-MOD-001~003。
Schema 改动
无。
文件变更清单
- 创建:
backend/src/main/java/com/xly/erp/module/mod/vo/ModuleTreeNodeVO.java— 树形节点 VO(7 字段 + children 列表) - 创建:
backend/src/main/java/com/xly/erp/module/mod/dto/ModuleQueryDTO.java— 查询参数 DTO(keyword) - 修改:
backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java— 追加tree(ModuleQueryDTO query): List<ModuleTreeNodeVO> - 修改:
backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java— 实现 tree - 修改:
backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java— 追加@GetMapping - 修改:
backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java— 追加 6 个 tree 单测 - 修改:
backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java— 追加 7 个 GET 集成测试
任务步骤
Task 1: VO + DTO
Files:
- Create:
backend/src/main/java/com/xly/erp/module/mod/vo/ModuleTreeNodeVO.java - Create:
backend/src/main/java/com/xly/erp/module/mod/dto/ModuleQueryDTO.java
API shape:
-
ModuleTreeNodeVO:7 字段 +List<ModuleTreeNodeVO> children,Lombok@Data,含静态工厂from(ModuleEntity e)(创建带空 children 列表的节点)。 ModuleQueryDTO:单字段@Size(max=50) String keyword(可空)。Bean Validation 由 controller 触发。Step 1.1 创建 VO + DTO(无独立单测;Bean Validation 在 IT 层覆盖)
-
Step 1.2 提交
git commit -m "feat(mod): module tree VO + query DTO REQ-MOD-004"
Task 2: ModuleService.tree — 树构建逻辑(mock 单元测试)
Files:
- 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:
-
interface ModuleService追加:List<ModuleTreeNodeVO> tree(ModuleQueryDTO query) - 实现步骤:
all = moduleMapper.selectList(LambdaQueryWrapper.eq(BDeleted, false))- 若
query.keyword == null || keyword.isEmpty()→ 跳到步骤 5(用全量构树) - 否则在
all内存里筛选hits = all.filter(e -> e.sModuleNameZh.contains(keyword)) - 对每个 hit 沿
iParentId链向上收集祖先(用byId = all.stream().toMap(IIncrement)索引);合并到survivors = hits ∪ ancestors - 按 iIncrement 索引
survivors,按 iParentId 分组子节点列表,同级按 (iSortOrder, iIncrement) ASC 排序 - 取
iParentId == null || iParentId not in survivors的节点作为根(这样过滤后一些祖先链断裂的节点也会被提到根,避免孤立);递归挂 children - 返回根节点列表
- 标
@Transactional(readOnly = true)
关键不变量(写入 plan 锁定):
- 叶子节点
children = new ArrayList<>(),不为 null - 排序键稳定:
Comparator.comparingInt(ModuleTreeNodeVO::getISortOrder).thenComparingInt(ModuleTreeNodeVO::getIIncrement) 祖先链深度上限 5(与 docs/03 § tModule 注记一致);超 5 仍向上视为业务异常并记一行
log.warn,不抛错(不影响 200 返回)-
Step 2.1 写失败测试(6 个)
-
tree_emptyDb_returnsEmptyList:mapper.selectList 返回空 → service 返回List.of() -
tree_singleRoot_returnsOneNodeWithEmptyChildren:1 个 root,断言返回 list 长度 1,children 是[] -
tree_multiLevel_buildsNestedStructureSortedByISortOrder:root(sort=2) + root(sort=1) + child of root1,断言返回顺序 [sort=1, sort=2],root1.children 包含 child -
tree_keywordHit_includesAncestorChain:grandparent → parent → child(sModuleNameZh 各不同),keyword 匹配 child;断言返回 grandparent → parent → child 三层 tree_keywordNoMatch_returnsEmptyList-
tree_softDeletedExcluded(验证 mapper 调用时 wrapper 含eq(bDeleted, false);ArgumentCaptor) - 测试方式:
@ExtendWith(MockitoExtension.class),构造List<ModuleEntity>喂 mock 返回值 - 子会话: FAIL(方法不存在)
-
-
Step 2.2 实现 service.tree
- 子会话: PASS
-
Step 2.3 提交
git commit -m "feat(mod): query module tree service REQ-MOD-004"
Task 3: ModuleController GET 端点 + 端到端 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 public ApiResponse<List<ModuleTreeNodeVO>> tree(@Valid ModuleQueryDTO query) - Javadoc:
REQ-MOD-004 模块查询 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:READ')") -
@Valid触发 Bean Validation;keyword 长度超限 → MethodArgumentNotValidException → GlobalExceptionHandler → 40010
注意:query 参数对象用
@Valid ModuleQueryDTO时,Spring MVC 会把keywordquery 参数绑定到 DTO 字段(无需 @ModelAttribute,bean 类型默认走 query string)。
-
Step 3.1 写失败测试(7 个)
-
get_emptyKeyword_returnsAllUndeletedAsTree:先 mapper.insert 几条(含 root + child + soft-deleted),GET 不带 keyword;断言 200 + data 长度等于未删 root 数;递归断言子树。 -
get_keyword_filtersByModuleNameZhWithAncestors:插入 grandparent("系统配置") → parent("用户管理") → child("登录");GET?keyword=登录;断言返回三层链。 -
get_keywordNoMatch_returnsEmptyArray:GET?keyword=不存在,断言data=[],code=200。 -
get_keywordTooLong_returns40010:keyword 51 字符。 -
get_softDeletedNotInResult:插入一条并立即 update bDeleted=1,GET 全量,断言不在结果。 -
get_responseExcludesInternalFields:断言$.data[0].sProcedureNamedoesNotExist;同时验证sModuleType/bShowPermission/tCreateDate/bDeleted不出现。 -
get_leafNodeChildrenIsEmptyArrayNotNull:断言叶子$.data[*].childrenis array 且 length=0(非 null)。 - 测试方式:
@SpringBootTest @AutoConfigureMockMvc @Transactional @Rollback+@Autowired ModuleMapper - 子会话: FAIL(端点不存在)
-
-
Step 3.2 实现 GET 端点
- 子会话: PASS
-
Step 3.3 跑全量 backend 测试
- 期望累计 47 + 6(service tree) + 7(controller GET) = 60 个,全绿(mvn 报数会因共享 context 而合并)
-
Step 3.4 提交
git commit -m "feat(mod): GET /api/modules controller REQ-MOD-004"
提交计划
-
feat(mod): module tree VO + query DTO REQ-MOD-004(Task 1) -
feat(mod): query module tree service REQ-MOD-004(Task 2) -
feat(mod): GET /api/modules controller REQ-MOD-004(Task 3)