2026-05-06-REQ-MOD-004.md 7.54 KB

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-tdd executes 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)
  • 实现步骤:
    1. all = moduleMapper.selectList(LambdaQueryWrapper.eq(BDeleted, false))
    2. query.keyword == null || keyword.isEmpty() → 跳到步骤 5(用全量构树)
    3. 否则在 all 内存里筛选 hits = all.filter(e -> e.sModuleNameZh.contains(keyword))
    4. 对每个 hit 沿 iParentId 链向上收集祖先(用 byId = all.stream().toMap(IIncrement) 索引);合并到 survivors = hits ∪ ancestors
    5. 按 iIncrement 索引 survivors,按 iParentId 分组子节点列表,同级按 (iSortOrder, iIncrement) ASC 排序
    6. iParentId == null || iParentId not in survivors 的节点作为根(这样过滤后一些祖先链断裂的节点也会被提到根,避免孤立);递归挂 children
    7. 返回根节点列表
  • @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 会把 keyword query 参数绑定到 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].sProcedureName doesNotExist;同时验证 sModuleType / bShowPermission / tCreateDate / bDeleted 不出现。
    • get_leafNodeChildrenIsEmptyArrayNotNull:断言叶子 $.data[*].children is 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)