fix(makecode): 树查询 showself 仅在 showall 时生效
避免 parent_id 懒加载把节点自身混入子级导致无限嵌套,并补充 department 回归用例与文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4
-2
@@ -1244,8 +1244,10 @@ func (that *MakeCode) Search(table string, userData Map, data Map, req *http.Req
|
||||
} else {
|
||||
where["parent_id"] = reqValue
|
||||
}
|
||||
//是否展示自己
|
||||
if req.FormValue("showself") == "1" {
|
||||
//是否展示自己:仅在 showall=1(选中节点及全部子孙)场景生效;
|
||||
//普通子级懒加载(仅 parent_id=X)不得把 id=X 的行本身混入结果,
|
||||
//否则前端树把节点当成自己的子级,造成无限嵌套
|
||||
if req.FormValue("showself") == "1" && req.FormValue("showall") == "1" {
|
||||
if len(where) == 0 {
|
||||
data["OR"] = Map{"id": reqValue}
|
||||
} else {
|
||||
|
||||
@@ -752,6 +752,19 @@ CREATE TABLE user (
|
||||
|
||||
表命名、外键命名与 COMMENT 约定见 [数据库设计规范](DatabaseDesign_数据库设计规范.md)。
|
||||
|
||||
#### 树形表查询参数(parent_id / showself / showall)
|
||||
|
||||
带 `parent_id` 列的表,通用 CRUD 的 `search` 接口支持树查询参数,语义如下(`code/makecode.go` 树节点分支):
|
||||
|
||||
| 请求参数组合 | WHERE 语义 | 典型场景 |
|
||||
| --- | --- | --- |
|
||||
| 不传 `parent_id` 或 `parent_id=0` | `id=当前用户锚点 OR parent_id IS NULL`(返回根层) | 树侧栏根节点加载 |
|
||||
| `parent_id=X` | `parent_id=X`,仅直接子级 | 树节点懒加载展开 |
|
||||
| `parent_id=X&showall=1` | `parent_ids LIKE '%,X,%'`,X 及其全部子孙(X 自身的 parent_ids 含 `,X,`) | 列表按树节点过滤 |
|
||||
| `parent_id=X&showall=1&showself=1` | 在 showall 基础上再 `OR id=X`,显式保证含 X 自身 | 管理端列表树筛选(Table.vue) |
|
||||
|
||||
**规则**:`showself=1` 仅在 `showall=1` 时生效。普通子级查询(仅 `parent_id=X`)**不会**把 id=X 的行本身混入结果——否则前端树会把节点当作自己的子级,造成无限嵌套(历史缺陷,已修复;回归用例见 `example/app/makecode_tree_test.go`,`go test ./app/ -count=1 -run 'TestApi/admin/department/search'`)。
|
||||
|
||||
### 4.5 生成的代码结构
|
||||
|
||||
#### 内嵌模式 (mode=0)
|
||||
|
||||
@@ -32,6 +32,11 @@ func TestMain(m *testing.M) {
|
||||
Proj: Project,
|
||||
Tests: ProjectTest,
|
||||
},
|
||||
// 通用 CRUD(codeConfig=admin)路由由 Init 时按库表生成,这里仅挂测试
|
||||
"admin": {
|
||||
Proj: Proj{},
|
||||
Tests: AdminDepartmentTest,
|
||||
},
|
||||
}).
|
||||
Flows(DemoFlows)
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package app
|
||||
|
||||
// 通用 CRUD 树表查询(MakeCode.Search 的 parent_id / showself / showall 语义)行为测试。
|
||||
// 回归背景:parent_id=X 且 showself=1 时旧逻辑拼出 OR(parent_id=X, id=X),
|
||||
// 把节点 X 自己当作 X 的子级返回,前端树无限嵌套(无限套娃)。
|
||||
// 依赖 department 树表(表名避开 admin.json 遗留 flow 对 org 的 admin_id 注入;
|
||||
// 见 setup_mysql.go;表需在 Init 前已存在于测试库)。
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
. "code.hoteas.com/golang/hotime"
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
)
|
||||
|
||||
// resultIds 提取 result.data 的 id 集合
|
||||
func resultIds(a *Api) map[int64]bool {
|
||||
ids := map[int64]bool{}
|
||||
data := a.Resp().GetBody().GetMap("result").GetSlice("data")
|
||||
for k := range data {
|
||||
ids[data.GetMap(k).GetCeilInt64("id")] = true
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
var AdminDepartmentTest = ProjTest{
|
||||
"department": CtrTest{
|
||||
"search": {Desc: "树表通用查询:parent_id 子级懒加载不得返回自身", Func: func(a *Api) {
|
||||
// ======== 错误用例 ========
|
||||
a.Query(Map{"parent_id": "1", "showself": "1"}).
|
||||
Get("未登录访问", 2, "你还没有登录")
|
||||
|
||||
// ======== 准备树数据:root → nodeX → child ========
|
||||
adminId := a.DB().Insert("admin", Map{
|
||||
"name": "树测管理员", "phone": "139" + ObjToStr(RandX(10000000, 99999999)),
|
||||
"state": 1, "password": Md5("tree123"), "role_id": 1,
|
||||
"create_time[#]": "NOW()", "modify_time[#]": "NOW()",
|
||||
})
|
||||
session := Map{"admin_id": adminId}
|
||||
|
||||
rootId := a.DB().Insert("department", Map{
|
||||
"name": "树测根部门", "state": 0,
|
||||
"create_time[#]": "NOW()", "modify_time[#]": "NOW()",
|
||||
})
|
||||
a.DB().Update("department", Map{"parent_ids": "," + ObjToStr(rootId) + ","}, Map{"id": rootId})
|
||||
|
||||
nodeX := a.DB().Insert("department", Map{
|
||||
"name": "树测节点X", "parent_id": rootId, "state": 0,
|
||||
"create_time[#]": "NOW()", "modify_time[#]": "NOW()",
|
||||
})
|
||||
a.DB().Update("department", Map{"parent_ids": "," + ObjToStr(rootId) + "," + ObjToStr(nodeX) + ","}, Map{"id": nodeX})
|
||||
|
||||
childId := a.DB().Insert("department", Map{
|
||||
"name": "树测子节点", "parent_id": nodeX, "state": 0,
|
||||
"create_time[#]": "NOW()", "modify_time[#]": "NOW()",
|
||||
})
|
||||
a.DB().Update("department", Map{"parent_ids": "," + ObjToStr(rootId) + "," + ObjToStr(nodeX) + "," + ObjToStr(childId) + ","}, Map{"id": childId})
|
||||
|
||||
rowSample := Map{"count": int64(1), "data": Slice{Map{"id": int64(1), "name": "sample"}}}
|
||||
|
||||
// ======== 核心回归:子级懒加载带 showself 不得包含节点自身 ========
|
||||
a.WithSession(session).
|
||||
Note("旧缺陷:OR(parent_id=X, id=X) 把 X 自己当作 X 的子级返回,树无限嵌套").
|
||||
Query(Map{"parent_id": nodeX, "showself": "1", "pageSize": "50"}).
|
||||
Verify(func(a *Api) error {
|
||||
ids := resultIds(a)
|
||||
if ids[nodeX] {
|
||||
return fmt.Errorf("子级查询结果不应包含节点自身 id=%d", nodeX)
|
||||
}
|
||||
if !ids[childId] {
|
||||
return fmt.Errorf("子级查询结果应包含直接子级 id=%d", childId)
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Get("子级懒加载showself不返回自身", 0, rowSample)
|
||||
|
||||
// ======== 不传 showself:同样只返回直接子级 ========
|
||||
a.WithSession(session).
|
||||
Query(Map{"parent_id": nodeX, "pageSize": "50"}).
|
||||
Verify(func(a *Api) error {
|
||||
ids := resultIds(a)
|
||||
if ids[nodeX] || !ids[childId] {
|
||||
return fmt.Errorf("普通子级查询应只含直接子级,实际: %v", ids)
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Get("普通子级查询只返回子级", 0, rowSample)
|
||||
|
||||
// ======== showall+showself(管理端列表按树筛选场景):含自身与全部子孙 ========
|
||||
a.WithSession(session).
|
||||
Note("Table.vue 树筛选场景:showall=1 展示选中节点及全部子孙,showself=1 保留自身").
|
||||
Query(Map{"parent_id": nodeX, "showself": "1", "showall": "1", "pageSize": "50"}).
|
||||
Verify(func(a *Api) error {
|
||||
ids := resultIds(a)
|
||||
if !ids[nodeX] {
|
||||
return fmt.Errorf("showall+showself 应包含节点自身 id=%d", nodeX)
|
||||
}
|
||||
if !ids[childId] {
|
||||
return fmt.Errorf("showall+showself 应包含子孙 id=%d", childId)
|
||||
}
|
||||
if ids[rootId] {
|
||||
return fmt.Errorf("showall+showself 不应包含父级 id=%d", rootId)
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Get("showall加showself含自身与子孙", 0, rowSample)
|
||||
}},
|
||||
},
|
||||
}
|
||||
@@ -83,6 +83,26 @@ func createMySQLTables(db *HoTimeDB, prefix string) {
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4")
|
||||
}
|
||||
|
||||
// department 部门表(树形结构,供通用 CRUD 树查询测试使用;
|
||||
// 不叫 org 是因为 config/admin.json 遗留 flow 会给 org 注入 admin_id 过滤)
|
||||
// 注意:MakeCode 路由在 Init 时按库中已有表生成,
|
||||
// 全新空库首次运行建表晚于 Init,/admin/department/search 需第二次运行才可用
|
||||
tbl = prefix + "department"
|
||||
if !mysqlTableExists(db, tbl) {
|
||||
fmt.Println("[MySQL Setup] 创建表:", tbl)
|
||||
db.Exec("CREATE TABLE `" + tbl + "` (" +
|
||||
"`id` int(11) unsigned NOT NULL AUTO_INCREMENT," +
|
||||
"`name` varchar(100) DEFAULT NULL COMMENT '部门名称'," +
|
||||
"`parent_id` int(11) DEFAULT NULL COMMENT '父级ID'," +
|
||||
"`parent_ids` varchar(255) DEFAULT NULL COMMENT '层级路径'," +
|
||||
"`state` int(2) DEFAULT '0'," +
|
||||
"`create_time` datetime DEFAULT NULL," +
|
||||
"`modify_time` datetime DEFAULT NULL," +
|
||||
"PRIMARY KEY (`id`)," +
|
||||
"KEY `idx_parent_id` (`parent_id`)" +
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表'")
|
||||
}
|
||||
|
||||
// test_batch 测试批量表
|
||||
tbl = prefix + "test_batch"
|
||||
if !mysqlTableExists(db, tbl) {
|
||||
|
||||
Reference in New Issue
Block a user