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 中。
业务规则
- 关键字归一化:controller 先 trim;null 或空串均当作空匹配。
-
长度校验:trim 后长度 > 100 →
BizException(40001, "keyword 长度超过 100 字符")。 -
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 '%%'命中所有。 -
拼树(service 内存算法):
- 把命中行映射为
ModuleTreeVO;建Map<Integer, ModuleTreeVO> idIndex - 遍历:若
iParentId != null && idIndex.containsKey(iParentId)→ 挂到 parent.children;否则视作 root(含真 root 与"父被过滤掉"的孤立节点),加入返回 list - 由于 SQL 已 ORDER BY,拼装顺序天然有序;同 parent 下 children 顺序 = SQL 顺序
- 把命中行映射为
-
只读:service 上
@Transactional(readOnly = true),明示无写副作用。 -
空结果:返回
[],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 记录该选择。
实现范围与边界抉择
- 拼树策略:选择"过滤命中后拼树(孤立子节点视为 root,即森林)",与 docs/03 § tModule 业务注记一致。不实现"扩展祖先链以保留树形"——会让查询语义复杂化(要么二次查祖先,要么 SQL 用 CTE);REQ 卡仅要求"以树形结构展示匹配结果",森林是合法的树形结果。
- Mapper 直接 SQL LIKE:相对"先全查再内存过滤"更高效;模块数据量大时也撑得住。
-
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— MockselectActiveByKeyword("")返回 5 行(root1, child1, child2, deepChild1, root2),ArgumentCaptor 验 mapper 入参为"";返回结构符合树(root1.children 含 child1/child2;child1.children 含 deepChild1;root2.children 空) -
listTree_nullKeyword_treatedAsEmpty— DTOkeyword=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=0;data是数组;至少含 root 节点且其 children 含 child -
getKeywordMatch_returnsForest— 直插含"系统"的 alive 模块 + 不含"系统"的;GET?keyword=系统;只返回含"系统"的 -
getKeywordTooLong_returns40001—keyword101 字符 →code=40001 -
getNoMatch_returnsEmptyArray—keyword=不存在的关键字XYZ;data是[] -
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,无需新增)