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

req_id: REQ-MOD-004 date: 2026-04-29

module: module_mod

Spec: REQ-MOD-004 — 模块查询

目标

按关键字对 tModule.sModuleNameZh 模糊匹配,过滤 bDeleted=0 行,按 iParentId 拼装为树形(森林)输出。空关键字返回完整模块树。

输入 / 触发

HTTP 接口(docs/05 § REQ-MOD-004)

  • Method / Path: GET /api/mod/modules
  • Auth: 必需(沿用 MOD-001 stub:路径已在 SecurityConfig /api/mod/** permitAll;USR-004 后改 authenticated() 即可——不需要 hasAuthority,本接口面向所有登录用户)
  • Query: keyword(可选;缺省 / 空串 / 仅空白 → 视作空匹配返回完整树)

校验

输入 校验 失败码
keyword 长度 ≤ 100 字符(与 sModuleNameZh 列长一致) 40001

输出 / 结果

成功响应

{
  "code": 0,
  "msg": "ok",
  "data": [
    {
      "iIncrement": 1,
      "sModuleNameZh": "系统管理",
      "sDisplayType": "手机端",
      "sManageDeptEn": "IT",
      "iParentId": null,
      "iSortOrder": 0,
      "children": [
        { "iIncrement": 2, "sModuleNameZh": "用户管理", "sDisplayType": "手机端",
          "sManageDeptEn": "IT", "iParentId": 1, "iSortOrder": 0, "children": [] }
      ]
    }
  ]
}

data 是数组(森林),无命中时为 []

VO ModuleTreeVO

字段 类型 来源
iIncrement Integer tModule.iIncrement
sModuleNameZh String tModule.sModuleNameZh
sDisplayType String tModule.sDisplayType
sManageDeptEn String tModule.sManageDeptEn
iParentId Integer tModule.iParentId(null 表根)
iSortOrder Integer tModule.iSortOrder
children List<ModuleTreeVO> 拼装得到,叶节点为 []

不返回 sProcedureName / sCreatedBy / tCreateDate / sBrandsId / sSubsidiaryId / bShowPermission / sModuleType / 软删除审计字段——这些不在 docs/05 输出 schema 中。

业务规则

  1. 关键字归一化:controller 先 trim;null 或空串均当作空匹配。
  2. 长度校验:trim 后长度 > 100 → BizException(40001, "keyword 长度超过 100 字符")
  3. DB 查询SELECT iIncrement, sModuleNameZh, sDisplayType, sManageDeptEn, iParentId, iSortOrder FROM tModule WHERE bDeleted=0 AND sModuleNameZh LIKE CONCAT('%', #{keyword}, '%') ORDER BY iSortOrder ASC, iIncrement ASC;空 keyword → LIKE '%%' 命中所有。
  4. 拼树(service 内存算法):
    • 把命中行映射为 ModuleTreeVO;建 Map<Integer, ModuleTreeVO> idIndex
    • 遍历:若 iParentId != null && idIndex.containsKey(iParentId) → 挂到 parent.children;否则视作 root(含真 root 与"父被过滤掉"的孤立节点),加入返回 list
    • 由于 SQL 已 ORDER BY,拼装顺序天然有序;同 parent 下 children 顺序 = SQL 顺序
  5. 只读:service 上 @Transactional(readOnly = true),明示无写副作用。
  6. 空结果:返回 [],HTTP 200 / code=0

边界与约束

  • keyword 缺失 / 空 / 仅空白 → 等价空匹配,返回完整有效树
  • keyword > 100 字符40001
  • JWT 伪造20001(filter 短路)
  • JWT 缺失 → permitAll stub,正常 200(USR-004 后改为要求登录)
  • SQL LIKE 通配符注入keyword% / _ 时 MyBatis 直接拼到 LIKE 中——理论上影响匹配范围(%abc% 用户输入 % 等于 LIKE '%%abc%%' 仍匹配所有)。本期不做转义(业务上不敏感且 docs/05 未要求);后续若需要严格匹配可在 service 层做 keyword.replace("%","\\%").replace("_","\\_") 处理。spec 记录该选择。

实现范围与边界抉择

  1. 拼树策略:选择"过滤命中后拼树(孤立子节点视为 root,即森林)",与 docs/03 § tModule 业务注记一致。不实现"扩展祖先链以保留树形"——会让查询语义复杂化(要么二次查祖先,要么 SQL 用 CTE);REQ 卡仅要求"以树形结构展示匹配结果",森林是合法的树形结果。
  2. Mapper 直接 SQL LIKE:相对"先全查再内存过滤"更高效;模块数据量大时也撑得住。
  3. Service 排序在 SQL:拼树时不再排序,避免重复劳动;测试断言依赖 SQL ORDER BY iSortOrder ASC, iIncrement ASC

依赖的 schema 表 / 字段

读取表:tModule

字段 用途
iIncrement / sModuleNameZh / sDisplayType / sManageDeptEn / iParentId / iSortOrder 输出 VO 字段
bDeleted 过滤条件(=0)

依赖索引:idx_module_name_zh(sModuleNameZh) 命中 LIKE 前缀匹配;idx_parent(iParentId) 不直接命中(拼树用内存 Map),但保留供未来 join 用。

依赖的接口

无。

验收标准

单元测试(追加到 ModuleServiceImplTest

  • listTree_emptyKeyword_invokesMapperWithEmptyString_returnsAssembledTree — Mock selectActiveByKeyword("") 返回 5 行(root1, child1, child2, deepChild1, root2),ArgumentCaptor 验 mapper 入参为 "";返回结构符合树(root1.children 含 child1/child2;child1.children 含 deepChild1;root2.children 空)
  • listTree_nullKeyword_treatedAsEmpty — DTO keyword=null,效果同空串
  • listTree_blankKeyword_treatedAsEmpty — keyword " " trim 后空
  • listTree_keywordTooLong_throws40001 — keyword 101 字符 → BizException(40001)
  • listTree_returnsEmptyListWhenNoMatch — Mock 返回空 list;service 返回 List.of()
  • listTree_orphansBecomeRootsInForest — Mock 返回 child;child 出现在顶层 list
  • listTree_keywordIsTrimmedBeforeQuery — keyword " 系统 " → mapper 入参 "系统"

Mapper IT(追加到 ModuleMapperIT

  • selectActiveByKeyword_filtersAndOrders — 准备 5 行(含 1 个 bDeleted=1);查 keyword="" → 4 行(按 iSortOrder, iIncrement 升序);查 keyword="系统" → 仅命中 sModuleNameZh 含"系统"的活跃行;查 keyword="不存在" → 空

集成测试(追加到 ModuleControllerIT

  • getEmptyKeyword_returnsCompleteTreeAsForest — 直插 root + child;GET /api/mod/modules 带 JWT;code=0data 是数组;至少含 root 节点且其 children 含 child
  • getKeywordMatch_returnsForest — 直插含"系统"的 alive 模块 + 不含"系统"的;GET ?keyword=系统;只返回含"系统"的
  • getKeywordTooLong_returns40001keyword 101 字符 → code=40001
  • getNoMatch_returnsEmptyArraykeyword=不存在的关键字XYZdata[]
  • getWithoutJwt_permitAllStub_returns200 — 无 JWT GET;code=0
  • getTamperedJwt_returns20001 — Authorization 伪造 → code=20001

工程验收

  • cd backend && mvn -B test 全绿(53 + MOD-004 新增 7(svc) + 1(mapperIT) + 6(controllerIT) = 67 用例)
  • 输出 VO 字段集严格匹配 docs/05 列表,不暴露敏感字段
  • // REQ-MOD-001 stub: see USR-004 follow-up 锚点保持(路径已 permitAll,无需新增)