2026-04-29-REQ-MOD-004.md 7.84 KB

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;新增 ModuleTreeVOmapper.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:

  • ModuleTreeVO POJO:字段 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)
    1. String normalized = keyword == null ? "" : keyword.trim()
    2. if (normalized.length() > 100) throw new BizException(40001, "keyword 长度超过 100 字符")
    3. List<Module> rows = moduleMapper.selectActiveByKeyword(normalized)
    4. 拼树:建 Map<Integer, ModuleTreeVO> 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<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/modulescode=0data 是数组;找出 iIncrement=root 的节点,children 含 iIncrement=child
    • getKeywordMatch_returnsForest:直插 "系统模块A"+"用户模块B";GET ?keyword=系统;返回数组只含 sModuleNameZh 含"系统"的节点
    • getKeywordTooLong_returns40001keyword 101 字符 → code=40001
    • getNoMatch_returnsEmptyArraykeyword=不存在的关键字XYZdata 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