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 <accessToken> + 权限码 MOD:READ。沿用 SecurityConfig permitAll;Controller Javadoc:REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:READ')")。
输出 / 结果
HTTP 200,响应体:
{
"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<ModuleTreeNodeVO>。
不返回的字段(避免泄露内部):
sProcedureName/sModuleType/bShowPermission/sId/sBrandsId/sSubsidiaryId/tCreateDate/sCreatedBy/bDeleted/tDeletedDate/sDeletedBy。如未来 REQ 需要其中字段,再扩 VO。
业务规则
-
范围过滤:
bDeleted = 0(默认仅返回未软删除)。 - 空 keyword:返回所有未软删除模块构成的完整树。
-
非空 keyword:
- 步骤 a:在所有未软删除模块中找出
sModuleNameZh LIKE '%keyword%'的命中集合hits。 - 步骤 b:对每个
hit,沿iParentId链向上收集所有未软删除祖先(深度上限 5 与 docs/03 § tModule 一致),并入结果集合。 - 步骤 c:用结果集合在内存中按
iParentId组装树。 - 命中节点本身的子孙不会被强制纳入(除非也命中);这样避免一次过滤拉出整棵子树。
- 步骤 a:在所有未软删除模块中找出
-
排序:同级节点按
iSortOrder ASC升序,iSortOrder相同则按iIncrement ASC(确定性排序)。 -
children字段:叶子节点为[],不为null(确保前端可直接.map)。 -
空结果:keyword 无匹配 →
data = [],HTTP 200 + code=200,不返回 404 类错误。 -
只读:本接口不写库,无事务要求;标
@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 检查。
依赖的接口
无(独立查询接口)。
验收标准
功能正确性
- 正向 — 空 keyword 返回完整树:DB 中 5 棵根 + 多层子模块;GET 返回 5 个根节点,子孙完整嵌套;同级按 iSortOrder 升序。
-
正向 — keyword 模糊匹配 + 祖先:DB 有 root("系统配置") → 子("用户管理") → 孙("登录");GET
?keyword=登录返回 root → 子 → 孙 三层(即命中节点 + 全部祖先)。 - 正向 — keyword 部分匹配:keyword="管理",匹配多个节点;返回它们各自完整祖先链合并的树。
- 正向 — 软删除模块过滤:DELETE 一个模块后再查,结果不含该模块。
-
正向 — 空结果:keyword="不存在的关键词",返回
data=[]+ code=200。 - 正向 — keyword 含中英混合:keyword="user 用户" 也走 LIKE '%user 用户%'(与 spec § 业务规则 3 一致;不拆词)。
- 同级排序:两根 iSortOrder=2/1,返回顺序 1 在前。
-
children 字段:叶子节点 children 是
[]而非 null(jsonPath 断言)。 -
keyword 长度超限:keyword=51 字符,返回
code=40010。 - 无登录也可访问(permitAll 阶段):直接 GET 不带 Authorization 返回 200。
- 响应字段精简:响应不含 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): <subject> REQ-MOD-004规范。