diff --git a/helper/array.go b/helper/array.go index b1cec79f..ac1a1006 100644 --- a/helper/array.go +++ b/helper/array.go @@ -14,6 +14,14 @@ type ArrayPluckValue struct { Items []map[string]interface{} `json:"items"` } +// ArrayTreeOption Array转树形结构参数表 +type ArrayTreeOption struct { + Key string `json:"id"` // 主键名称, 默认为 id + Empty interface{} `json:"empty"` // Top节点 parent 数值, 默认为 0 + Parent string `json:"parent"` // 父节点字段名称, 默认为 parent + Children string `json:"children"` // 子节点字段名称, 默认为 children +} + // ArrayColumn 返回多条数据记录,指定字段数值。 func ArrayColumn(records []map[string]interface{}, name string) []interface{} { values := []interface{}{} @@ -119,3 +127,69 @@ func OfArrayPluckValue(any interface{}) ArrayPluckValue { } return value } + +// NewArrayTreeOption 创建配置 +func NewArrayTreeOption(option map[string]interface{}) ArrayTreeOption { + new := ArrayTreeOption{ + Empty: 0, + Key: "id", + Parent: "parent", + Children: "children", + } + if v, ok := option["parent"].(string); ok { + new.Parent = v + } + if v, ok := option["children"].(string); ok { + new.Children = v + } + return new +} + +// ArrayTree []map[string]interface{} 转树形结构 +func ArrayTree(records []map[string]interface{}, setting map[string]interface{}) []map[string]interface{} { + opt := NewArrayTreeOption(setting) + return opt.Tree(records) +} + +// Tree Array 转换为 Tree +func (opt ArrayTreeOption) Tree(records []map[string]interface{}) []map[string]interface{} { + + mapping := map[string]map[string]interface{}{} + for i := range records { + if key, has := records[i][opt.Key]; has { + records[i][opt.Children] = []map[string]interface{}{} + mapping[fmt.Sprintf("%v", key)] = records[i] + } + } + + // 向上归集 + for key, record := range mapping { + parent := record[opt.Parent] + if parent == opt.Empty { // 第一级 + continue + } + pKey := fmt.Sprintf("%v", parent) + if _, has := mapping[pKey]; !has { + continue + } + children, ok := mapping[pKey][opt.Children].([]map[string]interface{}) + if !ok { + children = []map[string]interface{}{} + } + children = append(children, mapping[key]) + mapping[pKey][opt.Children] = children + } + + res := []map[string]interface{}{} + for i := range records { + if key, has := records[i][opt.Key]; has { + record := mapping[fmt.Sprintf("%v", key)] + parent := record[opt.Parent] + if parent == opt.Empty { // 只保留第一级 + res = append(res, record) + } + } + } + + return res +} diff --git a/helper/array_test.go b/helper/array_test.go index 0a425ddc..ab5d1522 100644 --- a/helper/array_test.go +++ b/helper/array_test.go @@ -7,6 +7,72 @@ import ( "github.com/yaoapp/kun/maps" ) +var testRecords = []map[string]interface{}{ + {"id": 1, "name": "云服务", "category_id": nil, "type_id": nil, "rank": 1, "parent_id": 0}, + {"id": 2, "name": "基础服务", "category_id": nil, "type_id": nil, "rank": 1, "parent_id": 1}, + {"id": 3, "name": "云主机", "category_id": nil, "type_id": 4, "rank": 1, "parent_id": 2}, + {"id": 4, "name": "对象存储", "category_id": nil, "type_id": 5, "rank": 2, "parent_id": 2}, + {"id": 5, "name": "云数据库", "category_id": nil, "type_id": 6, "rank": 3, "parent_id": 2}, + {"id": 6, "name": "块存储", "category_id": nil, "type_id": 7, "rank": 4, "parent_id": 2}, + {"id": 7, "name": "应用托管容器", "category_id": nil, "type_id": 8, "rank": 5, "parent_id": 2}, + {"id": 8, "name": "云缓存", "category_id": nil, "type_id": 9, "rank": 6, "parent_id": 2}, + {"id": 9, "name": "本地负载均衡", "category_id": nil, "type_id": 10, "rank": 7, "parent_id": 2}, + {"id": 10, "name": "全局负载均衡", "category_id": nil, "type_id": 13, "rank": 8, "parent_id": 2}, + {"id": 11, "name": "云分发", "category_id": nil, "type_id": 11, "rank": 9, "parent_id": 2}, + {"id": 12, "name": "企业级SaaS", "category_id": nil, "type_id": 12, "rank": 10, "parent_id": 2}, + {"id": 13, "name": "云桌面", "category_id": nil, "type_id": 14, "rank": 11, "parent_id": 2}, + {"id": 14, "name": "云备份", "category_id": nil, "type_id": 17, "rank": 12, "parent_id": 2}, + {"id": 15, "name": "GPU云主机", "category_id": nil, "type_id": 18, "rank": 13, "parent_id": 2}, + {"id": 16, "name": "物理云主机", "category_id": nil, "type_id": 20, "rank": 14, "parent_id": 2}, + {"id": 17, "name": "智能云", "category_id": 41, "type_id": nil, "rank": 2, "parent_id": 2}, + {"id": 18, "name": "软件和开发", "category_id": nil, "type_id": nil, "rank": 2, "parent_id": 0}, + {"id": 19, "name": "虚拟化及管理", "category_id": 59, "type_id": nil, "rank": 1, "parent_id": 18}, + {"id": 20, "name": "容器解决方案", "category_id": 65, "type_id": nil, "rank": 2, "parent_id": 18}, + {"id": 21, "name": "微服务解决方案", "category_id": 76, "type_id": nil, "rank": 3, "parent_id": 18}, + {"id": 22, "name": "serverless解决方案", "category_id": 82, "type_id": nil, "rank": 4, "parent_id": 18}, + {"id": 23, "name": "云管理和云运营", "category_id": nil, "type_id": nil, "rank": 3, "parent_id": 0}, + {"id": 24, "name": "混合云", "category_id": nil, "type_id": nil, "rank": 1, "parent_id": 23}, + {"id": 25, "name": "混合云解决方案", "category_id": 88, "type_id": nil, "rank": 1, "parent_id": 24}, + {"id": 26, "name": "混合云安全", "category_id": 671, "type_id": nil, "rank": 2, "parent_id": 24}, + {"id": 27, "name": "多云管理", "category_id": 94, "type_id": nil, "rank": 2, "parent_id": 23}, + {"id": 28, "name": "金牌运维", "category_id": 120, "type_id": nil, "rank": 3, "parent_id": 23}, + {"id": 29, "name": "研发运营一体化", "category_id": 443, "type_id": nil, "rank": 4, "parent_id": 23}, + {"id": 30, "name": "MSP", "category_id": 112, "type_id": nil, "rank": 5, "parent_id": 23}, + {"id": 31, "name": "安全与保险", "category_id": nil, "type_id": nil, "rank": 4, "parent_id": 0}, + {"id": 32, "name": "风险管理", "category_id": 141, "type_id": nil, "rank": 1, "parent_id": 31}, + {"id": 33, "name": "云服务用户数据保护", "category_id": 152, "type_id": nil, "rank": 2, "parent_id": 31}, + {"id": 34, "name": "业务风控", "category_id": nil, "type_id": nil, "rank": 3, "parent_id": 31}, + {"id": 35, "name": "内容安全", "category_id": 159, "type_id": nil, "rank": 1, "parent_id": 34}, + {"id": 36, "name": "反交易欺诈", "category_id": 164, "type_id": nil, "rank": 2, "parent_id": 34}, + {"id": 37, "name": "反信贷欺诈", "category_id": 165, "type_id": nil, "rank": 3, "parent_id": 34}, + {"id": 38, "name": "反营销欺诈", "category_id": 166, "type_id": nil, "rank": 4, "parent_id": 34}, + {"id": 39, "name": "反钓鱼欺诈", "category_id": 167, "type_id": nil, "rank": 5, "parent_id": 34}, + {"id": 40, "name": "云主机安全", "category_id": 184, "type_id": nil, "rank": 4, "parent_id": 31}, + {"id": 41, "name": "态势感知", "category_id": 190, "type_id": nil, "rank": 5, "parent_id": 31}, + {"id": 42, "name": "云保险", "category_id": 272, "type_id": nil, "rank": 6, "parent_id": 31}, + {"id": 43, "name": "云网&云边", "category_id": nil, "type_id": nil, "rank": 5, "parent_id": 0}, + {"id": 44, "name": "云平台网络能力", "category_id": 102, "type_id": nil, "rank": 1, "parent_id": 43}, + {"id": 45, "name": "SD-WAN", "category_id": 106, "type_id": nil, "rank": 2, "parent_id": 43}, + {"id": 46, "name": "物联网", "category_id": 234, "type_id": nil, "rank": 3, "parent_id": 43}, + {"id": 47, "name": "行业云", "category_id": nil, "type_id": nil, "rank": 6, "parent_id": 0}, + {"id": 48, "name": "政务", "category_id": nil, "type_id": nil, "rank": 1, "parent_id": 47}, + {"id": 49, "name": "政务云综合水平评估", "category_id": 215, "type_id": nil, "rank": 1, "parent_id": 48}, + {"id": 50, "name": "可信政务云评估", "category_id": 216, "type_id": nil, "rank": 2, "parent_id": 48}, + {"id": 51, "name": "金融", "category_id": 225, "type_id": nil, "rank": 2, "parent_id": 47}, + {"id": 52, "name": "开源治理", "category_id": nil, "type_id": nil, "rank": 7, "parent_id": 0}, + {"id": 53, "name": "面向开源用户企业", "category_id": 196, "type_id": nil, "rank": 1, "parent_id": 52}, + {"id": 54, "name": "面向自发开源企业", "category_id": 202, "type_id": nil, "rank": 2, "parent_id": 52}, + {"id": 55, "name": "开源项目评估", "category_id": 711, "type_id": nil, "rank": 3, "parent_id": 52}, + {"id": 56, "name": "开源工具评估", "category_id": 720, "type_id": nil, "rank": 4, "parent_id": 52}, + {"id": 57, "name": "检测平台", "category_id": nil, "type_id": nil, "rank": 8, "parent_id": 0}, + {"id": 58, "name": "云主机分级", "category_id": 371, "type_id": nil, "rank": 1, "parent_id": 57}, + {"id": 59, "name": "监管支撑", "category_id": nil, "type_id": nil, "rank": 9, "parent_id": 0}, + {"id": 60, "name": "企业上云效果成熟度", "category_id": 250, "type_id": nil, "rank": 1, "parent_id": 59}, + {"id": 61, "name": "综合信用评估", "category_id": nil, "type_id": nil, "rank": 2, "parent_id": 59}, + {"id": 62, "name": "云服务企业", "category_id": 261, "type_id": nil, "rank": 1, "parent_id": 61}, + {"id": 63, "name": "CDN服务企业", "category_id": 271, "type_id": nil, "rank": 2, "parent_id": 61}, +} + func TestArrayPluck(t *testing.T) { columns := []string{"城市", "行业", "计费"} pluck := map[string]interface{}{ @@ -34,3 +100,9 @@ func TestArraySplit(t *testing.T) { assert.Equal(t, 2, len(value)) } } + +func TestArrayTree(t *testing.T) { + records := testRecords + res := ArrayTree(records, map[string]interface{}{"parent": "parent_id"}) + assert.Equal(t, 9, len(res)) +} diff --git a/helper/process.go b/helper/process.go index 44f1881f..104a133c 100644 --- a/helper/process.go +++ b/helper/process.go @@ -1,12 +1,7 @@ package helper import ( - "fmt" - "reflect" - "github.com/yaoapp/gou" - "github.com/yaoapp/gou/query/share" - "github.com/yaoapp/kun/exception" "github.com/yaoapp/kun/utils" ) @@ -16,6 +11,7 @@ func init() { gou.RegisterProcessHandler("xiang.helper.ArraySplit", ProcessArraySplit) gou.RegisterProcessHandler("xiang.helper.ArrayColumn", ProcessArrayColumn) gou.RegisterProcessHandler("xiang.helper.ArrayKeep", ProcessArrayKeep) + gou.RegisterProcessHandler("xiang.helper.ArrayTree", ProcessArrayTree) gou.RegisterProcessHandler("xiang.helper.MapKeys", ProcessMapKeys) gou.RegisterProcessHandler("xiang.helper.MapValues", ProcessMapValues) gou.RegisterProcessHandler("xiang.helper.For", ProcessFor) @@ -26,23 +22,7 @@ func init() { // ProcessArrayPluck xiang.helper.ArrayPluck 将多个数据记录集合,合并为一个数据记录集合 func ProcessArrayPluck(process *gou.Process) interface{} { process.ValidateArgNums(2) - columnsAny := process.Args[0] - columns := []string{} - switch columnsAny.(type) { - case []interface{}: - for _, v := range columnsAny.([]interface{}) { - value, ok := v.(string) - if ok { - columns = append(columns, value) - continue - } - exception.New("参数错误: 第1个参数不是字符串数组", 400).Ctx(process.Args[0]).Throw() - } - case []string: - default: - exception.New("参数错误: 第1个参数不是字符串数组", 400).Ctx(process.Args[0]).Throw() - break - } + columns := process.ArgsStrings(0) pluck := process.ArgsMap(1) return ArrayPluck(columns, pluck) } @@ -50,33 +30,7 @@ func ProcessArrayPluck(process *gou.Process) interface{} { // ProcessArraySplit xiang.helper.ArraySplit 将多条数记录集合,分解为一个 columns:[]string 和 values: [][]interface{} func ProcessArraySplit(process *gou.Process) interface{} { process.ValidateArgNums(1) - args := process.Args[0] - records := []map[string]interface{}{} - - switch args.(type) { - case []interface{}: - for _, v := range args.([]interface{}) { - value, ok := v.(map[string]interface{}) - if ok { - records = append(records, value) - continue - } - exception.New("参数错误: 第1个参数不是数组", 400).Ctx(fmt.Sprintf("%#v", process.Args[0])).Throw() - } - break - case []share.Record: - for _, v := range args.([]share.Record) { - records = append(records, v) - } - break - case []map[string]interface{}: - records = args.([]map[string]interface{}) - break - default: - fmt.Printf("%#v %s\n", args, reflect.TypeOf(args).Kind()) - exception.New("参数错误: 第1个参数不是数组", 400).Ctx(fmt.Sprintf("%#v", process.Args[0])).Throw() - break - } + records := process.ArgsRecords(0) columns, values := ArraySplit(records) return map[string]interface{}{ "columns": columns, @@ -87,34 +41,8 @@ func ProcessArraySplit(process *gou.Process) interface{} { // ProcessArrayColumn xiang.helper.ArrayColumn 返回多条数据记录,指定字段数值。 func ProcessArrayColumn(process *gou.Process) interface{} { process.ValidateArgNums(2) - args := process.Args[0] - records := []map[string]interface{}{} + records := process.ArgsRecords(0) name := process.ArgsString(1) - - switch args.(type) { - case []interface{}: - for _, v := range args.([]interface{}) { - value, ok := v.(map[string]interface{}) - if ok { - records = append(records, value) - continue - } - exception.New("参数错误: 第1个参数不是数组", 400).Ctx(fmt.Sprintf("%#v", process.Args[0])).Throw() - } - break - case []share.Record: - for _, v := range args.([]share.Record) { - records = append(records, v) - } - break - case []map[string]interface{}: - records = args.([]map[string]interface{}) - break - default: - fmt.Printf("%#v %s\n", args, reflect.TypeOf(args).Kind()) - exception.New("参数错误: 第1个参数不是数组", 400).Ctx(fmt.Sprintf("%#v", process.Args[0])).Throw() - break - } values := ArrayColumn(records, name) return values } @@ -122,55 +50,19 @@ func ProcessArrayColumn(process *gou.Process) interface{} { // ProcessArrayKeep xiang.helper.ArrayKeep 仅保留指定键名的数据 func ProcessArrayKeep(process *gou.Process) interface{} { process.ValidateArgNums(2) - args := process.Args[0] - columnsAny := process.Args[1] - records := []map[string]interface{}{} - columns := []string{} - - switch args.(type) { - case []interface{}: - for _, v := range args.([]interface{}) { - value, ok := v.(map[string]interface{}) - if ok { - records = append(records, value) - continue - } - exception.New("参数错误: 第1个参数不是数组", 400).Ctx(fmt.Sprintf("%#v", process.Args[0])).Throw() - } - break - case []share.Record: - for _, v := range args.([]share.Record) { - records = append(records, v) - } - break - case []map[string]interface{}: - records = args.([]map[string]interface{}) - break - default: - fmt.Printf("%#v %s\n", args, reflect.TypeOf(args).Kind()) - exception.New("参数错误: 第1个参数不是数组", 400).Ctx(fmt.Sprintf("%#v", process.Args[0])).Throw() - break - } - - switch columnsAny.(type) { - case []interface{}: - for _, v := range columnsAny.([]interface{}) { - value, ok := v.(string) - if ok { - columns = append(columns, value) - continue - } - exception.New("参数错误: 第2个参数不是字符串数组", 400).Ctx(process.Args[0]).Throw() - } - case []string: - default: - exception.New("参数错误: 第2个参数不是字符串数组", 400).Ctx(process.Args[0]).Throw() - break - } - + records := process.ArgsRecords(0) + columns := process.ArgsStrings(1) return ArrayKeep(records, columns) } +// ProcessArrayTree xiang.helper.ArrayTree 转换为属性结构 +func ProcessArrayTree(process *gou.Process) interface{} { + process.ValidateArgNums(2) + records := process.ArgsRecords(0) + setting := process.ArgsMap(1) + return ArrayTree(records, setting) +} + // ProcessMapValues xiang.helper.MapValues 返回映射的数值 func ProcessMapValues(process *gou.Process) interface{} { process.ValidateArgNums(1)