--- req_id: REQ-MOD-004 date: 2026-05-06 module: module_mod --- # Spec: REQ-MOD-004 — 模块查询 ## 目标 实现后端 `GET /api/modules` 接口:以**树形结构**返回所有未软删除的模块,可选按 `sModuleNameZh` 模糊匹配过滤;过滤命中时同时保留命中节点的所有**祖先路径**以便定位上下文。 ## 输入 / 触发 **接口**:`GET /api/modules`,无请求体。 **Query parameters**: | 字段 | 类型 | 必填 | 校验 / 取值 | |---|---|---|---| | `keyword` | String | 否 | 长度 ≤ 50;非空时对 `sModuleNameZh` 做 `LIKE '%keyword%'`(不区分大小写——MySQL `utf8mb4_unicode_ci` 默认行为) | **鉴权**:契约要求 `Authorization: Bearer ` + 权限码 `MOD:READ`。沿用 SecurityConfig permitAll;Controller Javadoc:`REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:READ')")`。 ## 输出 / 结果 **HTTP 200,响应体**: ```json { "code": 200, "message": "操作成功", "data": [ { "iIncrement": 1, "sModuleNameZh": "系统配置", "sDisplayType": "系统配置", "sManageDeptEn": "IT", "iParentId": null, "iSortOrder": 0, "children": [ { "iIncrement": 2, "sModuleNameZh": "用户管理", "sDisplayType": "前端业务", "sManageDeptEn": "IT", "iParentId": 1, "iSortOrder": 1, "children": [] } ] } ], "timestamp": 1746528600000 } ``` `data` 是根节点数组(`iParentId == null` 的节点);每个节点带 `children` 数组(同结构递归)。 新增 VO `ModuleTreeNodeVO`:字段 `iIncrement` / `sModuleNameZh` / `sDisplayType` / `sManageDeptEn` / `iParentId` / `iSortOrder` / `children: List`。 > **不返回的字段**(避免泄露内部):`sProcedureName` / `sModuleType` / `bShowPermission` / `sId` / `sBrandsId` / `sSubsidiaryId` / `tCreateDate` / `sCreatedBy` / `bDeleted` / `tDeletedDate` / `sDeletedBy`。如未来 REQ 需要其中字段,再扩 VO。 ## 业务规则 1. **范围过滤**:`bDeleted = 0`(默认仅返回未软删除)。 2. **空 keyword**:返回所有未软删除模块构成的完整树。 3. **非空 keyword**: - 步骤 a:在所有未软删除模块中找出 `sModuleNameZh LIKE '%keyword%'` 的命中集合 `hits`。 - 步骤 b:对每个 `hit`,沿 `iParentId` 链向上收集所有未软删除祖先(深度上限 5 与 docs/03 § tModule 一致),并入结果集合。 - 步骤 c:用结果集合在内存中按 `iParentId` 组装树。 - 命中节点本身的子孙不会被强制纳入(除非也命中);这样避免一次过滤拉出整棵子树。 4. **排序**:同级节点按 `iSortOrder ASC` 升序,`iSortOrder` 相同则按 `iIncrement ASC`(确定性排序)。 5. **`children` 字段**:叶子节点为 `[]`,不为 `null`(确保前端可直接 `.map`)。 6. **空结果**:keyword 无匹配 → `data = []`,**HTTP 200 + code=200**,不返回 404 类错误。 7. **只读**:本接口不写库,无事务要求;标 `@Transactional(readOnly = true)`。 ## 边界与约束 ### 鉴权策略 沿用 REQ-MOD-001/002/003 SecurityConfig permitAll。 ### 错误码 | 场景 | 错误码 | ErrorCode 枚举 | |---|---|---| | `keyword` 长度 > 50 | 40010 | `PARAM_INVALID`(已存在) | | 服务端兜底 | 50000 | `INTERNAL_ERROR` | ### 性能 - 单次 `selectList(LambdaQueryWrapper.eq(bDeleted, false))` 拉取所有未删除模块;spec § 性能上限 docs/03 注明"单次返回不超过 500 项 / 树深度上限 5 层",本期数据量低不分页。 - 在内存里 O(N) 建索引(id → entity)+ O(N) 建子节点列表 + O(N) 排序 + O(K * D) 沿祖先链向上(K=hits 数,D=深度上限 5)。 - 不引入 SQL 递归 CTE / 自连接;保持 mapper 层轻量。 ### 大小写敏感 `utf8mb4_unicode_ci` collation 默认不区分大小写。MyBatis-Plus `LambdaQueryWrapper.like(...)` 走 SQL `LIKE`,行为与 collation 一致。无需额外处理。 ### keyword 中的特殊字符 `%` / `_` 在 SQL `LIKE` 模式里是通配符。简化处理:本期不做转义(业务模块名通常不含这些字符);如未来需要,service 层先把 keyword 中的 `%` / `_` / `\` 替换为转义形式(`MyBatis-Plus 5.x` 的 `like` 已自带 escape,本期 3.5.7 需手动)。**本 REQ 暂不处理转义**,作为已知边界记录。 ## 依赖的 schema 表 / 字段 **读表**:`tModule` | 字段 | 用途 | |---|---| | `iIncrement` | 节点 id;输出 | | `sModuleNameZh` | 模糊匹配列;输出 | | `sDisplayType` | 输出 | | `sManageDeptEn` | 输出 | | `iParentId` | 父子关系;输出 | | `iSortOrder` | 排序;输出 | | `bDeleted` | 过滤未删除(=0) | **索引利用**: - `idx_module_name_zh`:`LIKE '%keyword%'` 实际不走索引(左模糊),但本期数据量低可接受。 - `idx_parent`:祖先链查询时按 iIncrement PK 走 selectById,不用 idx_parent。 - `idx_module_deleted`(未单独建,但 bDeleted 在多个 idx 中已有):bDeleted 过滤时 MySQL 优化器自行选择。 **外键**:本接口只读,不触发 FK 检查。 ## 依赖的接口 无(独立查询接口)。 ## 验收标准 ### 功能正确性 1. **正向 — 空 keyword 返回完整树**:DB 中 5 棵根 + 多层子模块;GET 返回 5 个根节点,子孙完整嵌套;同级按 iSortOrder 升序。 2. **正向 — keyword 模糊匹配 + 祖先**:DB 有 root("系统配置") → 子("用户管理") → 孙("登录");GET `?keyword=登录` 返回 root → 子 → 孙 三层(即命中节点 + 全部祖先)。 3. **正向 — keyword 部分匹配**:keyword="管理",匹配多个节点;返回它们各自完整祖先链合并的树。 4. **正向 — 软删除模块过滤**:DELETE 一个模块后再查,结果不含该模块。 5. **正向 — 空结果**:keyword="不存在的关键词",返回 `data=[]` + code=200。 6. **正向 — keyword 含中英混合**:keyword="user 用户" 也走 LIKE '%user 用户%'(与 spec § 业务规则 3 一致;不拆词)。 7. **同级排序**:两根 iSortOrder=2/1,返回顺序 1 在前。 8. **children 字段**:叶子节点 children 是 `[]` 而非 null(jsonPath 断言)。 9. **keyword 长度超限**:keyword=51 字符,返回 `code=40010`。 10. **无登录也可访问**(permitAll 阶段):直接 GET 不带 Authorization 返回 200。 11. **响应字段精简**:响应不含 sProcedureName / sModuleType / bShowPermission / 标准列等内部字段(jsonPath 断言)。 ### 接口契约一致性 - 响应格式 `{code, message, data, timestamp}`。 - 错误码 200 / 40010 / 50000。 - 不回显堆栈。 ### 测试覆盖 - **单元测试** `ModuleServiceImplTest` 追加(mock ModuleMapper): - tree_emptyDb_returnsEmptyList - tree_singleRoot_returnsOneNodeWithEmptyChildren - tree_multiLevel_buildsNestedStructureSortedByISortOrder - tree_keywordHit_includesAncestorChain - tree_keywordNoMatch_returnsEmptyList - tree_softDeletedExcluded(mapper 已过滤;service 不重复过滤) - **集成测试** `ModuleControllerIT` 追加: - get_emptyKeyword_returnsAllUndeletedAsTree - get_keyword_filtersByModuleNameZhWithAncestors - get_keywordNoMatch_returnsEmptyArray - get_keywordTooLong_returns40010 - get_softDeletedNotInResult - get_responseExcludesInternalFields - get_leafNodeChildrenIsEmptyArrayNotNull ### 代码与文档 - `// REQ-MOD-004` 注释贴在 Controller / Service / VO。 - 提交按 `feat(mod): REQ-MOD-004` 规范。