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) // ======== 修复回归:文本字段部分关键词模糊匹配 + 非文本字段等值筛选不受影响 ======== // 背景 bug(code/makecode.go Search):文本类字段(如 name)曾同时生成等值条件 // name=值 与模糊条件 name[~]=值,二者在 where 中以 AND 叠加;前端筛选框传入 // 部分关键词时等值条件必不命中,AND 之后查询恒为空。 // 修复后:文本字段只走模糊匹配;非文本字段(如 state,规则覆盖为 select 类型)维持等值匹配现状。 textId := a.DB().Insert("department", Map{ "name": "综合行政部", "state": 5, "parent_id": nodeX, "create_time[#]": "NOW()", "modify_time[#]": "NOW()", }) a.DB().Update("department", Map{"parent_ids": "," + ObjToStr(rootId) + "," + ObjToStr(nodeX) + "," + ObjToStr(textId) + ","}, Map{"id": textId}) a.WithSession(session). Note("修复前:name 等值(综合行政部≠行政)与模糊[~]同时AND叠加,传部分关键词恒查不到;修复后应命中模糊匹配"). Query(Map{"parent_id": rootId, "showself": "1", "showall": "1", "pageSize": "50", "name": "行政"}). Verify(func(a *Api) error { ids := resultIds(a) if !ids[textId] { return fmt.Errorf("文本字段部分关键词模糊查询应命中 id=%d, 实际: %v", textId, ids) } return nil }). Get("文本字段部分关键词命中模糊匹配", 0, rowSample) a.WithSession(session). Note("state 规则覆盖为 select 类型(非text),应维持等值匹配现状,只精确命中 state=5 的记录"). Query(Map{"parent_id": rootId, "showself": "1", "showall": "1", "pageSize": "50", "state": "5"}). Verify(func(a *Api) error { ids := resultIds(a) if !ids[textId] { return fmt.Errorf("非文本字段等值筛选应命中 id=%d, 实际: %v", textId, ids) } for id := range ids { if id != textId { return fmt.Errorf("非文本字段等值筛选不应包含其它 state 的记录,实际额外命中: id=%d", id) } } return nil }). Get("非文本字段等值筛选行为不变", 0, rowSample) }}, }, }