Commit 9e107120a81688178e3086d6c50c4d86c9c597c4
1 parent
a46be5d2
docs(mod): review approval REQ-MOD-004
Showing
4 changed files
with
348 additions
and
1 deletions
docs/08-模块任务管理.md
docs/superpowers/plans/2026-05-06-REQ-MOD-004.md
0 → 100644
| 1 | +--- | |
| 2 | +req_id: REQ-MOD-004 | |
| 3 | +date: 2026-05-06 | |
| 4 | +spec_ref: docs/superpowers/specs/2026-05-06-REQ-MOD-004.md | |
| 5 | +--- | |
| 6 | + | |
| 7 | +# REQ-MOD-004 模块查询 Implementation Plan | |
| 8 | + | |
| 9 | +> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. | |
| 10 | + | |
| 11 | +**Goal:** 实现 `GET /api/modules?keyword=...`:返回未软删除模块的树形结构,可选按 `sModuleNameZh` 模糊匹配并保留命中节点的祖先链。 | |
| 12 | + | |
| 13 | +**Architecture:** 一次性 selectList 拉所有未删除模块到内存,按 keyword 过滤命中集合,沿父链向上收集祖先合并到结果集,最后在内存按 iParentId 组装树。同级按 iSortOrder ASC + iIncrement ASC 排序。叶子 `children=[]`。 | |
| 14 | + | |
| 15 | +**Tech Stack:** 沿用 REQ-MOD-001~003。 | |
| 16 | + | |
| 17 | +--- | |
| 18 | + | |
| 19 | +## Schema 改动 | |
| 20 | + | |
| 21 | +无。 | |
| 22 | + | |
| 23 | +## 文件变更清单 | |
| 24 | + | |
| 25 | +- 创建: `backend/src/main/java/com/xly/erp/module/mod/vo/ModuleTreeNodeVO.java` — 树形节点 VO(7 字段 + children 列表) | |
| 26 | +- 创建: `backend/src/main/java/com/xly/erp/module/mod/dto/ModuleQueryDTO.java` — 查询参数 DTO(keyword) | |
| 27 | +- 修改: `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java` — 追加 `tree(ModuleQueryDTO query): List<ModuleTreeNodeVO>` | |
| 28 | +- 修改: `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` — 实现 tree | |
| 29 | +- 修改: `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` — 追加 `@GetMapping` | |
| 30 | +- 修改: `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java` — 追加 6 个 tree 单测 | |
| 31 | +- 修改: `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java` — 追加 7 个 GET 集成测试 | |
| 32 | + | |
| 33 | +## 任务步骤 | |
| 34 | + | |
| 35 | +### Task 1: VO + DTO | |
| 36 | + | |
| 37 | +**Files:** | |
| 38 | +- Create: `backend/src/main/java/com/xly/erp/module/mod/vo/ModuleTreeNodeVO.java` | |
| 39 | +- Create: `backend/src/main/java/com/xly/erp/module/mod/dto/ModuleQueryDTO.java` | |
| 40 | + | |
| 41 | +**API shape:** | |
| 42 | +- `ModuleTreeNodeVO`:7 字段 + `List<ModuleTreeNodeVO> children`,Lombok `@Data`,含静态工厂 `from(ModuleEntity e)`(创建带空 children 列表的节点)。 | |
| 43 | +- `ModuleQueryDTO`:单字段 `@Size(max=50) String keyword`(可空)。Bean Validation 由 controller 触发。 | |
| 44 | + | |
| 45 | +- [ ] **Step 1.1 创建 VO + DTO(无独立单测;Bean Validation 在 IT 层覆盖)** | |
| 46 | + | |
| 47 | +- [ ] **Step 1.2 提交** | |
| 48 | + - `git commit -m "feat(mod): module tree VO + query DTO REQ-MOD-004"` | |
| 49 | + | |
| 50 | +--- | |
| 51 | + | |
| 52 | +### Task 2: ModuleService.tree — 树构建逻辑(mock 单元测试) | |
| 53 | + | |
| 54 | +**Files:** | |
| 55 | +- Modify: `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java` | |
| 56 | +- Modify: `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` | |
| 57 | +- Modify: `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java` | |
| 58 | + | |
| 59 | +**API shape:** | |
| 60 | +- `interface ModuleService` 追加:`List<ModuleTreeNodeVO> tree(ModuleQueryDTO query)` | |
| 61 | +- 实现步骤: | |
| 62 | + 1. `all = moduleMapper.selectList(LambdaQueryWrapper.eq(BDeleted, false))` | |
| 63 | + 2. 若 `query.keyword == null || keyword.isEmpty()` → 跳到步骤 5(用全量构树) | |
| 64 | + 3. 否则在 `all` 内存里筛选 `hits = all.filter(e -> e.sModuleNameZh.contains(keyword))` | |
| 65 | + 4. 对每个 hit 沿 `iParentId` 链向上收集祖先(用 `byId = all.stream().toMap(IIncrement)` 索引);合并到 `survivors = hits ∪ ancestors` | |
| 66 | + 5. 按 iIncrement 索引 `survivors`,按 iParentId 分组子节点列表,同级按 (iSortOrder, iIncrement) ASC 排序 | |
| 67 | + 6. 取 `iParentId == null || iParentId not in survivors` 的节点作为根(这样过滤后一些祖先链断裂的节点也会被提到根,避免孤立);递归挂 children | |
| 68 | + 7. 返回根节点列表 | |
| 69 | +- 标 `@Transactional(readOnly = true)` | |
| 70 | + | |
| 71 | +**关键不变量**(写入 plan 锁定): | |
| 72 | +- 叶子节点 `children = new ArrayList<>()`,**不为 null** | |
| 73 | +- 排序键稳定:`Comparator.comparingInt(ModuleTreeNodeVO::getISortOrder).thenComparingInt(ModuleTreeNodeVO::getIIncrement)` | |
| 74 | +- 祖先链深度上限 5(与 docs/03 § tModule 注记一致);超 5 仍向上视为业务异常并记一行 `log.warn`,不抛错(不影响 200 返回) | |
| 75 | + | |
| 76 | +- [ ] **Step 2.1 写失败测试(6 个)** | |
| 77 | + - `tree_emptyDb_returnsEmptyList`:mapper.selectList 返回空 → service 返回 `List.of()` | |
| 78 | + - `tree_singleRoot_returnsOneNodeWithEmptyChildren`:1 个 root,断言返回 list 长度 1,children 是 `[]` | |
| 79 | + - `tree_multiLevel_buildsNestedStructureSortedByISortOrder`:root(sort=2) + root(sort=1) + child of root1,断言返回顺序 [sort=1, sort=2],root1.children 包含 child | |
| 80 | + - `tree_keywordHit_includesAncestorChain`:grandparent → parent → child(sModuleNameZh 各不同),keyword 匹配 child;断言返回 grandparent → parent → child 三层 | |
| 81 | + - `tree_keywordNoMatch_returnsEmptyList` | |
| 82 | + - `tree_softDeletedExcluded`(验证 mapper 调用时 wrapper 含 `eq(bDeleted, false)`;ArgumentCaptor<Wrapper>) | |
| 83 | + - 测试方式:`@ExtendWith(MockitoExtension.class)`,构造 `List<ModuleEntity>` 喂 mock 返回值 | |
| 84 | + - 子会话: FAIL(方法不存在) | |
| 85 | + | |
| 86 | +- [ ] **Step 2.2 实现 service.tree** | |
| 87 | + - 子会话: PASS | |
| 88 | + | |
| 89 | +- [ ] **Step 2.3 提交** | |
| 90 | + - `git commit -m "feat(mod): query module tree service REQ-MOD-004"` | |
| 91 | + | |
| 92 | +--- | |
| 93 | + | |
| 94 | +### Task 3: ModuleController GET 端点 + 端到端 IT | |
| 95 | + | |
| 96 | +**Files:** | |
| 97 | +- Modify: `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` | |
| 98 | +- Modify: `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java` | |
| 99 | + | |
| 100 | +**API shape:** | |
| 101 | +- 新方法: | |
| 102 | + ``` | |
| 103 | + @GetMapping | |
| 104 | + public ApiResponse<List<ModuleTreeNodeVO>> tree(@Valid ModuleQueryDTO query) | |
| 105 | + ``` | |
| 106 | +- Javadoc:`REQ-MOD-004 模块查询 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:READ')")` | |
| 107 | +- `@Valid` 触发 Bean Validation;keyword 长度超限 → MethodArgumentNotValidException → GlobalExceptionHandler → 40010 | |
| 108 | + | |
| 109 | +> **注意**:query 参数对象用 `@Valid ModuleQueryDTO` 时,Spring MVC 会把 `keyword` query 参数绑定到 DTO 字段(无需 @ModelAttribute,bean 类型默认走 query string)。 | |
| 110 | + | |
| 111 | +- [ ] **Step 3.1 写失败测试(7 个)** | |
| 112 | + - `get_emptyKeyword_returnsAllUndeletedAsTree`:先 mapper.insert 几条(含 root + child + soft-deleted),GET 不带 keyword;断言 200 + data 长度等于未删 root 数;递归断言子树。 | |
| 113 | + - `get_keyword_filtersByModuleNameZhWithAncestors`:插入 grandparent("系统配置") → parent("用户管理") → child("登录");GET `?keyword=登录`;断言返回三层链。 | |
| 114 | + - `get_keywordNoMatch_returnsEmptyArray`:GET `?keyword=不存在`,断言 `data=[]`,code=200。 | |
| 115 | + - `get_keywordTooLong_returns40010`:keyword 51 字符。 | |
| 116 | + - `get_softDeletedNotInResult`:插入一条并立即 update bDeleted=1,GET 全量,断言不在结果。 | |
| 117 | + - `get_responseExcludesInternalFields`:断言 `$.data[0].sProcedureName` doesNotExist;同时验证 `sModuleType` / `bShowPermission` / `tCreateDate` / `bDeleted` 不出现。 | |
| 118 | + - `get_leafNodeChildrenIsEmptyArrayNotNull`:断言叶子 `$.data[*].children` is array 且 length=0(非 null)。 | |
| 119 | + - 测试方式:`@SpringBootTest @AutoConfigureMockMvc @Transactional @Rollback` + `@Autowired ModuleMapper` | |
| 120 | + - 子会话: FAIL(端点不存在) | |
| 121 | + | |
| 122 | +- [ ] **Step 3.2 实现 GET 端点** | |
| 123 | + - 子会话: PASS | |
| 124 | + | |
| 125 | +- [ ] **Step 3.3 跑全量 backend 测试** | |
| 126 | + - 期望累计 47 + 6(service tree) + 7(controller GET) = 60 个,全绿(mvn 报数会因共享 context 而合并) | |
| 127 | + | |
| 128 | +- [ ] **Step 3.4 提交** | |
| 129 | + - `git commit -m "feat(mod): GET /api/modules controller REQ-MOD-004"` | |
| 130 | + | |
| 131 | +--- | |
| 132 | + | |
| 133 | +## 提交计划 | |
| 134 | + | |
| 135 | +- `feat(mod): module tree VO + query DTO REQ-MOD-004`(Task 1) | |
| 136 | +- `feat(mod): query module tree service REQ-MOD-004`(Task 2) | |
| 137 | +- `feat(mod): GET /api/modules controller REQ-MOD-004`(Task 3) | ... | ... |
docs/superpowers/reviews/2026-05-06-REQ-MOD-004.md
0 → 100644
| 1 | +--- | |
| 2 | +req_id: REQ-MOD-004 | |
| 3 | +date: 2026-05-06 | |
| 4 | +round: 1 | |
| 5 | +reviewer: superpower-code-reviewer | |
| 6 | +--- | |
| 7 | + | |
| 8 | +# Review: REQ-MOD-004 — round 1 | |
| 9 | + | |
| 10 | +## 结论 | |
| 11 | +approve | |
| 12 | + | |
| 13 | +## Must-fix | |
| 14 | +(无) | |
| 15 | + | |
| 16 | +## Nice-to-have | |
| 17 | + | |
| 18 | +- backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java:530 — 补 IT 覆盖 spec § 验收 #7『同级排序』:插入 root(iSortOrder=2) + root(iSortOrder=1),断言 GET 返回数组首个 iIncrement 是 sort=1 那条。当前同级排序仅在单测覆盖,缺端到端断言。 | |
| 19 | +- backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java:530 — 补 IT 覆盖 spec § 验收 #6『keyword 含中英混合』:插入 sModuleNameZh='user 用户',GET ?keyword=user 用户 命中。 | |
| 20 | +- backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java:497 — `tree_softDeletedExcluded` 仅 verify(...).selectList(any()),未捕 wrapper 形状(plan 要求 ArgumentCaptor)。建议改名或真用 ArgumentCaptor 抽 `wrapper.getTargetSql()` 断言含 `bDeleted = 0`。 | |
| 21 | +- backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java:227 — Comparator 对 iSortOrder 做了 null→0 防御,但 docs/03 已注明 NOT NULL;可简化为 `Comparator.comparingInt(ModuleTreeNodeVO::getISortOrder).thenComparingInt(::getIIncrement)`。 | |
| 22 | +- backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java:198 — `survivorIds = byId.keySet()` 是 live view,与 keyword 分支返回的独立 HashSet 类型不一致;建议改成 `new HashSet<>(byId.keySet())` 一致化。 | |
| 23 | +- backend/pom.xml:121 — surefire includes 修改是项目级配置;建议在 docs/04 测试规范节追加一行说明『*IT.java 走 surefire (mvn test) 而非 failsafe (mvn verify)』,避免后续贡献者破坏 includes。 | |
| 24 | +- docs/03-数据库设计文档.md § tModule 业务注记 — 建议追加一行『模块树最大深度 5 层(iParentId 自引用),由 service 层校验,REQ-MOD-002 / REQ-MOD-004 引用本约束』,把深度上限提升为 SSoT。 | |
| 25 | +- backend/src/main/java/com/xly/erp/module/mod/dto/ModuleQueryDTO.java:11 — 在 @Size 上方 javadoc 写明『empty 视为不过滤』。 | |
| 26 | + | |
| 27 | +## 反例 / 测试覆盖缺口 | |
| 28 | + | |
| 29 | +1. spec § 验收 #7 同级排序的端到端 IT 缺失(仅单测)。 | |
| 30 | +2. spec § 验收 #6 中英混合 keyword 没有专属 IT。 | |
| 31 | +3. `tree_softDeletedExcluded` 单测断言强度低于 plan 期望。 | |
| 32 | +4. 反例缺:keyword 含 SQL 通配符 `%` / `_`(spec 声明本期不转义,作为已知边界)。 | |
| 33 | +5. 反例缺:祖先链深度 ≥ 5 的截断边界用例;docs/03 已限深度 5,正常打不到边界,可后续补 6 层强构数据单测固化截断语义。 | |
| 34 | +6. Controller javadoc 标了『REQ-USR-004 完成后追加 @PreAuthorize』但缺集中跟踪——建议在 docs/08 § 二 USR-004 子项挂 follow-up 备注。 | |
| 35 | + | |
| 36 | +**核心结论**:实现忠实于 spec — keyword 过滤 + 祖先链 walk-up + 内存树构造 + 同级 (iSortOrder, iIncrement) 排序 + 叶子 children=[] 全部正确。API 契约字段精简到位(VO 7 个公开字段 + children)。surefire includes 修复是揭示性发现——本仓库之前 *IT.java 没在 mvn test 跑过,REQ-MOD-001/002/003 的 IT 都是"死代码";本 REQ commit body 已披露,REQ-MOD-004 的 IT 是首次在 mvn test 跑通。 | ... | ... |
docs/superpowers/specs/2026-05-06-REQ-MOD-004.md
0 → 100644
| 1 | +--- | |
| 2 | +req_id: REQ-MOD-004 | |
| 3 | +date: 2026-05-06 | |
| 4 | +module: module_mod | |
| 5 | +--- | |
| 6 | + | |
| 7 | +# Spec: REQ-MOD-004 — 模块查询 | |
| 8 | + | |
| 9 | +## 目标 | |
| 10 | + | |
| 11 | +实现后端 `GET /api/modules` 接口:以**树形结构**返回所有未软删除的模块,可选按 `sModuleNameZh` 模糊匹配过滤;过滤命中时同时保留命中节点的所有**祖先路径**以便定位上下文。 | |
| 12 | + | |
| 13 | +## 输入 / 触发 | |
| 14 | + | |
| 15 | +**接口**:`GET /api/modules`,无请求体。 | |
| 16 | + | |
| 17 | +**Query parameters**: | |
| 18 | + | |
| 19 | +| 字段 | 类型 | 必填 | 校验 / 取值 | | |
| 20 | +|---|---|---|---| | |
| 21 | +| `keyword` | String | 否 | 长度 ≤ 50;非空时对 `sModuleNameZh` 做 `LIKE '%keyword%'`(不区分大小写——MySQL `utf8mb4_unicode_ci` 默认行为) | | |
| 22 | + | |
| 23 | +**鉴权**:契约要求 `Authorization: Bearer <accessToken>` + 权限码 `MOD:READ`。沿用 SecurityConfig permitAll;Controller Javadoc:`REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:READ')")`。 | |
| 24 | + | |
| 25 | +## 输出 / 结果 | |
| 26 | + | |
| 27 | +**HTTP 200,响应体**: | |
| 28 | + | |
| 29 | +```json | |
| 30 | +{ | |
| 31 | + "code": 200, | |
| 32 | + "message": "操作成功", | |
| 33 | + "data": [ | |
| 34 | + { | |
| 35 | + "iIncrement": 1, | |
| 36 | + "sModuleNameZh": "系统配置", | |
| 37 | + "sDisplayType": "系统配置", | |
| 38 | + "sManageDeptEn": "IT", | |
| 39 | + "iParentId": null, | |
| 40 | + "iSortOrder": 0, | |
| 41 | + "children": [ | |
| 42 | + { | |
| 43 | + "iIncrement": 2, | |
| 44 | + "sModuleNameZh": "用户管理", | |
| 45 | + "sDisplayType": "前端业务", | |
| 46 | + "sManageDeptEn": "IT", | |
| 47 | + "iParentId": 1, | |
| 48 | + "iSortOrder": 1, | |
| 49 | + "children": [] | |
| 50 | + } | |
| 51 | + ] | |
| 52 | + } | |
| 53 | + ], | |
| 54 | + "timestamp": 1746528600000 | |
| 55 | +} | |
| 56 | +``` | |
| 57 | + | |
| 58 | +`data` 是根节点数组(`iParentId == null` 的节点);每个节点带 `children` 数组(同结构递归)。 | |
| 59 | + | |
| 60 | +新增 VO `ModuleTreeNodeVO`:字段 `iIncrement` / `sModuleNameZh` / `sDisplayType` / `sManageDeptEn` / `iParentId` / `iSortOrder` / `children: List<ModuleTreeNodeVO>`。 | |
| 61 | + | |
| 62 | +> **不返回的字段**(避免泄露内部):`sProcedureName` / `sModuleType` / `bShowPermission` / `sId` / `sBrandsId` / `sSubsidiaryId` / `tCreateDate` / `sCreatedBy` / `bDeleted` / `tDeletedDate` / `sDeletedBy`。如未来 REQ 需要其中字段,再扩 VO。 | |
| 63 | + | |
| 64 | +## 业务规则 | |
| 65 | + | |
| 66 | +1. **范围过滤**:`bDeleted = 0`(默认仅返回未软删除)。 | |
| 67 | +2. **空 keyword**:返回所有未软删除模块构成的完整树。 | |
| 68 | +3. **非空 keyword**: | |
| 69 | + - 步骤 a:在所有未软删除模块中找出 `sModuleNameZh LIKE '%keyword%'` 的命中集合 `hits`。 | |
| 70 | + - 步骤 b:对每个 `hit`,沿 `iParentId` 链向上收集所有未软删除祖先(深度上限 5 与 docs/03 § tModule 一致),并入结果集合。 | |
| 71 | + - 步骤 c:用结果集合在内存中按 `iParentId` 组装树。 | |
| 72 | + - 命中节点本身的子孙不会被强制纳入(除非也命中);这样避免一次过滤拉出整棵子树。 | |
| 73 | +4. **排序**:同级节点按 `iSortOrder ASC` 升序,`iSortOrder` 相同则按 `iIncrement ASC`(确定性排序)。 | |
| 74 | +5. **`children` 字段**:叶子节点为 `[]`,不为 `null`(确保前端可直接 `.map`)。 | |
| 75 | +6. **空结果**:keyword 无匹配 → `data = []`,**HTTP 200 + code=200**,不返回 404 类错误。 | |
| 76 | +7. **只读**:本接口不写库,无事务要求;标 `@Transactional(readOnly = true)`。 | |
| 77 | + | |
| 78 | +## 边界与约束 | |
| 79 | + | |
| 80 | +### 鉴权策略 | |
| 81 | + | |
| 82 | +沿用 REQ-MOD-001/002/003 SecurityConfig permitAll。 | |
| 83 | + | |
| 84 | +### 错误码 | |
| 85 | + | |
| 86 | +| 场景 | 错误码 | ErrorCode 枚举 | | |
| 87 | +|---|---|---| | |
| 88 | +| `keyword` 长度 > 50 | 40010 | `PARAM_INVALID`(已存在) | | |
| 89 | +| 服务端兜底 | 50000 | `INTERNAL_ERROR` | | |
| 90 | + | |
| 91 | +### 性能 | |
| 92 | + | |
| 93 | +- 单次 `selectList(LambdaQueryWrapper.eq(bDeleted, false))` 拉取所有未删除模块;spec § 性能上限 docs/03 注明"单次返回不超过 500 项 / 树深度上限 5 层",本期数据量低不分页。 | |
| 94 | +- 在内存里 O(N) 建索引(id → entity)+ O(N) 建子节点列表 + O(N) 排序 + O(K * D) 沿祖先链向上(K=hits 数,D=深度上限 5)。 | |
| 95 | +- 不引入 SQL 递归 CTE / 自连接;保持 mapper 层轻量。 | |
| 96 | + | |
| 97 | +### 大小写敏感 | |
| 98 | + | |
| 99 | +`utf8mb4_unicode_ci` collation 默认不区分大小写。MyBatis-Plus `LambdaQueryWrapper.like(...)` 走 SQL `LIKE`,行为与 collation 一致。无需额外处理。 | |
| 100 | + | |
| 101 | +### keyword 中的特殊字符 | |
| 102 | + | |
| 103 | +`%` / `_` 在 SQL `LIKE` 模式里是通配符。简化处理:本期不做转义(业务模块名通常不含这些字符);如未来需要,service 层先把 keyword 中的 `%` / `_` / `\` 替换为转义形式(`MyBatis-Plus 5.x` 的 `like` 已自带 escape,本期 3.5.7 需手动)。**本 REQ 暂不处理转义**,作为已知边界记录。 | |
| 104 | + | |
| 105 | +## 依赖的 schema 表 / 字段 | |
| 106 | + | |
| 107 | +**读表**:`tModule` | |
| 108 | + | |
| 109 | +| 字段 | 用途 | | |
| 110 | +|---|---| | |
| 111 | +| `iIncrement` | 节点 id;输出 | | |
| 112 | +| `sModuleNameZh` | 模糊匹配列;输出 | | |
| 113 | +| `sDisplayType` | 输出 | | |
| 114 | +| `sManageDeptEn` | 输出 | | |
| 115 | +| `iParentId` | 父子关系;输出 | | |
| 116 | +| `iSortOrder` | 排序;输出 | | |
| 117 | +| `bDeleted` | 过滤未删除(=0) | | |
| 118 | + | |
| 119 | +**索引利用**: | |
| 120 | +- `idx_module_name_zh`:`LIKE '%keyword%'` 实际不走索引(左模糊),但本期数据量低可接受。 | |
| 121 | +- `idx_parent`:祖先链查询时按 iIncrement PK 走 selectById,不用 idx_parent。 | |
| 122 | +- `idx_module_deleted`(未单独建,但 bDeleted 在多个 idx 中已有):bDeleted 过滤时 MySQL 优化器自行选择。 | |
| 123 | + | |
| 124 | +**外键**:本接口只读,不触发 FK 检查。 | |
| 125 | + | |
| 126 | +## 依赖的接口 | |
| 127 | + | |
| 128 | +无(独立查询接口)。 | |
| 129 | + | |
| 130 | +## 验收标准 | |
| 131 | + | |
| 132 | +### 功能正确性 | |
| 133 | + | |
| 134 | +1. **正向 — 空 keyword 返回完整树**:DB 中 5 棵根 + 多层子模块;GET 返回 5 个根节点,子孙完整嵌套;同级按 iSortOrder 升序。 | |
| 135 | +2. **正向 — keyword 模糊匹配 + 祖先**:DB 有 root("系统配置") → 子("用户管理") → 孙("登录");GET `?keyword=登录` 返回 root → 子 → 孙 三层(即命中节点 + 全部祖先)。 | |
| 136 | +3. **正向 — keyword 部分匹配**:keyword="管理",匹配多个节点;返回它们各自完整祖先链合并的树。 | |
| 137 | +4. **正向 — 软删除模块过滤**:DELETE 一个模块后再查,结果不含该模块。 | |
| 138 | +5. **正向 — 空结果**:keyword="不存在的关键词",返回 `data=[]` + code=200。 | |
| 139 | +6. **正向 — keyword 含中英混合**:keyword="user 用户" 也走 LIKE '%user 用户%'(与 spec § 业务规则 3 一致;不拆词)。 | |
| 140 | +7. **同级排序**:两根 iSortOrder=2/1,返回顺序 1 在前。 | |
| 141 | +8. **children 字段**:叶子节点 children 是 `[]` 而非 null(jsonPath 断言)。 | |
| 142 | +9. **keyword 长度超限**:keyword=51 字符,返回 `code=40010`。 | |
| 143 | +10. **无登录也可访问**(permitAll 阶段):直接 GET 不带 Authorization 返回 200。 | |
| 144 | +11. **响应字段精简**:响应不含 sProcedureName / sModuleType / bShowPermission / 标准列等内部字段(jsonPath 断言)。 | |
| 145 | + | |
| 146 | +### 接口契约一致性 | |
| 147 | + | |
| 148 | +- 响应格式 `{code, message, data, timestamp}`。 | |
| 149 | +- 错误码 200 / 40010 / 50000。 | |
| 150 | +- 不回显堆栈。 | |
| 151 | + | |
| 152 | +### 测试覆盖 | |
| 153 | + | |
| 154 | +- **单元测试** `ModuleServiceImplTest` 追加(mock ModuleMapper): | |
| 155 | + - tree_emptyDb_returnsEmptyList | |
| 156 | + - tree_singleRoot_returnsOneNodeWithEmptyChildren | |
| 157 | + - tree_multiLevel_buildsNestedStructureSortedByISortOrder | |
| 158 | + - tree_keywordHit_includesAncestorChain | |
| 159 | + - tree_keywordNoMatch_returnsEmptyList | |
| 160 | + - tree_softDeletedExcluded(mapper 已过滤;service 不重复过滤) | |
| 161 | + | |
| 162 | +- **集成测试** `ModuleControllerIT` 追加: | |
| 163 | + - get_emptyKeyword_returnsAllUndeletedAsTree | |
| 164 | + - get_keyword_filtersByModuleNameZhWithAncestors | |
| 165 | + - get_keywordNoMatch_returnsEmptyArray | |
| 166 | + - get_keywordTooLong_returns40010 | |
| 167 | + - get_softDeletedNotInResult | |
| 168 | + - get_responseExcludesInternalFields | |
| 169 | + - get_leafNodeChildrenIsEmptyArrayNotNull | |
| 170 | + | |
| 171 | +### 代码与文档 | |
| 172 | + | |
| 173 | +- `// REQ-MOD-004` 注释贴在 Controller / Service / VO。 | |
| 174 | +- 提交按 `feat(mod): <subject> REQ-MOD-004` 规范。 | ... | ... |