From 3f68510076d14af19f8f48d130de87f6154848dd Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 21 Nov 2021 21:15:33 +0800 Subject: [PATCH] =?UTF-8?q?+=20JWT,=20Captcha,=20Password=20=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.go | 14 ++-- helper/array.process.go | 47 +++++++++++ helper/array_test.go | 48 ++++++++++++ helper/captcha.go | 170 ++++++++++++++++++++++++++++++++++++++++ helper/captcha_test.go | 65 +++++++++++++++ helper/jwt.go | 97 +++++++++++++++++++++++ helper/jwt_test.go | 32 ++++++++ helper/map.go | 16 ++++ helper/password.go | 21 +++++ helper/password_test.go | 27 +++++++ helper/process.go | 82 ++----------------- helper/process_test.go | 56 ------------- helper/range.go | 19 +++++ 13 files changed, 554 insertions(+), 140 deletions(-) create mode 100644 helper/array.process.go create mode 100644 helper/captcha.go create mode 100644 helper/captcha_test.go create mode 100644 helper/jwt.go create mode 100644 helper/jwt_test.go create mode 100644 helper/password.go create mode 100644 helper/password_test.go delete mode 100644 helper/process_test.go diff --git a/config/config.go b/config/config.go index 41bbcbeb..c055ae7f 100644 --- a/config/config.go +++ b/config/config.go @@ -89,17 +89,17 @@ func NewConfig(envfile ...string) Config { if len(envfile) > 0 { file, err := filepath.Abs(envfile[0]) - if err != nil { - log.Printf("加载环境配置文件%s出错 %s\n", envfile[0], err.Error()) - } else { + if err == nil { + // log.Printf("加载环境配置文件%s出错 %s\n", envfile[0], err.Error()) + // } else { filename = file } } - err := godotenv.Overload(filename) - if err != nil { - log.Printf("加载环境配置文件%s出错 %s\n", filename, err.Error()) - } + godotenv.Overload(filename) + // if err != nil { + // log.Printf("加载环境配置文件%s出错 %s\n", filename, err.Error()) + // } cfg := Config{} if err := env.Parse(&cfg); err != nil { diff --git a/helper/array.process.go b/helper/array.process.go new file mode 100644 index 00000000..e03918da --- /dev/null +++ b/helper/array.process.go @@ -0,0 +1,47 @@ +package helper + +import "github.com/yaoapp/gou" + +// ProcessArrayPluck xiang.helper.ArrayPluck 将多个数据记录集合,合并为一个数据记录集合 +func ProcessArrayPluck(process *gou.Process) interface{} { + process.ValidateArgNums(2) + columns := process.ArgsStrings(0) + pluck := process.ArgsMap(1) + return ArrayPluck(columns, pluck) +} + +// ProcessArraySplit xiang.helper.ArraySplit 将多条数记录集合,分解为一个 columns:[]string 和 values: [][]interface{} +func ProcessArraySplit(process *gou.Process) interface{} { + process.ValidateArgNums(1) + records := process.ArgsRecords(0) + columns, values := ArraySplit(records) + return map[string]interface{}{ + "columns": columns, + "values": values, + } +} + +// ProcessArrayColumn xiang.helper.ArrayColumn 返回多条数据记录,指定字段数值。 +func ProcessArrayColumn(process *gou.Process) interface{} { + process.ValidateArgNums(2) + records := process.ArgsRecords(0) + name := process.ArgsString(1) + values := ArrayColumn(records, name) + return values +} + +// ProcessArrayKeep xiang.helper.ArrayKeep 仅保留指定键名的数据 +func ProcessArrayKeep(process *gou.Process) interface{} { + process.ValidateArgNums(2) + 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) +} diff --git a/helper/array_test.go b/helper/array_test.go index ab5d1522..42cec626 100644 --- a/helper/array_test.go +++ b/helper/array_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou" "github.com/yaoapp/kun/maps" ) @@ -106,3 +107,50 @@ func TestArrayTree(t *testing.T) { res := ArrayTree(records, map[string]interface{}{"parent": "parent_id"}) assert.Equal(t, 9, len(res)) } + +func TestProcessArrayPluck(t *testing.T) { + args := []interface{}{ + []interface{}{"城市", "行业", "计费"}, + map[string]interface{}{ + "行业": map[string]interface{}{"key": "city", "value": "数量", "items": []map[string]interface{}{{"city": "北京", "数量": 32}, {"city": "上海", "数量": 20}}}, + "计费": map[string]interface{}{"key": "city", "value": "计费种类", "items": []map[string]interface{}{{"city": "北京", "计费种类": 6}, {"city": "西安", "计费种类": 3}}}, + }, + } + process := gou.NewProcess("xiang.helper.ArrayPluck", args...) + response := ProcessArrayPluck(process) + assert.NotNil(t, response) + items, ok := response.([]map[string]interface{}) + assert.True(t, ok) + + assert.Equal(t, 3, len(items)) + for _, item := range items { + maps.Of(item).Has("城市") + maps.Of(item).Has("行业") + maps.Of(item).Has("计费") + } +} + +func TestProcessArraySplit(t *testing.T) { + args := []interface{}{ + []map[string]interface{}{ + {"name": "阿里云计算有限公司", "short_name": "阿里云"}, + {"name": "世纪互联蓝云", "short_name": "上海蓝云"}, + }, + } + process := gou.NewProcess("xiang.helper.ArraySplit", args...) + response := process.Run() + assert.NotNil(t, response) + res, ok := response.(map[string]interface{}) + assert.True(t, ok) + + columns, ok := res["columns"].([]string) + assert.True(t, ok) + + values, ok := res["values"].([][]interface{}) + assert.True(t, ok) + assert.Equal(t, 2, len(columns)) + assert.Equal(t, 2, len(values)) + for _, value := range values { + assert.Equal(t, 2, len(value)) + } +} diff --git a/helper/captcha.go b/helper/captcha.go new file mode 100644 index 00000000..8c52e43e --- /dev/null +++ b/helper/captcha.go @@ -0,0 +1,170 @@ +package helper + +import ( + "errors" + "image/color" + + "github.com/mojocn/base64Captcha" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/kun/maps" + "github.com/yaoapp/xiang/config" + "github.com/yaoapp/xiang/xlog" +) + +var captchaStore = base64Captcha.DefaultMemStore + +// CaptchaOption 验证码配置 +type CaptchaOption struct { + Type string + Height int + Width int + Length int + Lang string + Background string +} + +// NewCaptchaOption 创建验证码配置 +func NewCaptchaOption() CaptchaOption { + return CaptchaOption{ + Width: 240, + Height: 80, + Length: 4, + Lang: "zh", + Background: "#FFFFFF", + } +} + +// CaptchaMake 制作验证码 +func CaptchaMake(option CaptchaOption) (string, string) { + + if option.Width == 0 { + option.Width = 240 + } + + if option.Height == 0 { + option.Width = 80 + } + + if option.Length == 0 { + option.Length = 4 + } + + if option.Lang == "" { + option.Lang = "zh" + } + + var driver base64Captcha.Driver + switch option.Type { + case "audio": + driver = base64Captcha.NewDriverAudio(option.Length, option.Lang) + break + case "math": + background := captchaBackground(option.Background) + driver = base64Captcha.NewDriverMath( + option.Height, option.Width, 3, + base64Captcha.OptionShowHollowLine, background, + base64Captcha.DefaultEmbeddedFonts, []string{}, + ) + break + default: + driver = base64Captcha.NewDriverDigit( + option.Height, option.Width, 5, + 0.7, 80, + ) + break + } + + c := base64Captcha.NewCaptcha(driver, captchaStore) + id, content, err := c.Generate() + if err != nil { + exception.New("生成验证码出错 %s", 500, err).Throw() + } + + // 打印日志 + if config.IsDebug() { + xlog.Println("图形/音频 ID:", id, "验证码:", captchaStore.Get(id, false)) + } + + return id, content +} + +// CaptchaValidate 校验验证码 +func CaptchaValidate(id string, value string) bool { + return captchaStore.Verify(id, value, true) +} + +func captchaBackground(s string) *color.RGBA { + if s == "" { + s = "#555555" + } + bg, err := captchaParseHexColorFast(s) + if err != nil { + exception.New("背景色格式错误 %s", 400, s).Throw() + } + return &bg +} + +func captchaParseHexColorFast(s string) (c color.RGBA, err error) { + c.A = 0xff + + if s[0] != '#' { + return c, errors.New("invalid format") + } + + hexToByte := func(b byte) byte { + switch { + case b >= '0' && b <= '9': + return b - '0' + case b >= 'a' && b <= 'f': + return b - 'a' + 10 + case b >= 'A' && b <= 'F': + return b - 'A' + 10 + } + err = errors.New("invalid format") + return 0 + } + + switch len(s) { + case 7: + c.R = hexToByte(s[1])<<4 + hexToByte(s[2]) + c.G = hexToByte(s[3])<<4 + hexToByte(s[4]) + c.B = hexToByte(s[5])<<4 + hexToByte(s[6]) + case 4: + c.R = hexToByte(s[1]) * 17 + c.G = hexToByte(s[2]) * 17 + c.B = hexToByte(s[3]) * 17 + default: + err = errors.New("invalid format") + } + return +} + +// ProcessCaptchaValidate xiang.helper.CaptchaValidate 校验图形/音频验证码 +func ProcessCaptchaValidate(process *gou.Process) interface{} { + process.ValidateArgNums(2) + if !CaptchaValidate(process.ArgsString(0), process.ArgsString(1)) { + exception.New("验证码不正确", 400).Throw() + return false + } + return true +} + +// ProcessCaptcha xiang.helper.Captcha 校验图形/音频验证码 +func ProcessCaptcha(process *gou.Process) interface{} { + process.ValidateArgNums(1) + option := CaptchaOption{ + Width: any.Of(process.ArgsURLValue(0, "width", "240")).CInt(), + Height: any.Of(process.ArgsURLValue(0, "height", "80")).CInt(), + Length: any.Of(process.ArgsURLValue(0, "height", "4")).CInt(), + Type: process.ArgsURLValue(0, "type", "math"), + Background: process.ArgsURLValue(0, "background", "#FFFFFF"), + Lang: process.ArgsURLValue(0, "lang", "zh"), + } + id, content := CaptchaMake(option) + return maps.Map{ + "id": id, + "content": content, + } +} diff --git a/helper/captcha_test.go b/helper/captcha_test.go new file mode 100644 index 00000000..587f419a --- /dev/null +++ b/helper/captcha_test.go @@ -0,0 +1,65 @@ +package helper + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/maps" +) + +func TestCaptcha(t *testing.T) { + id, content := CaptchaMake(CaptchaOption{ + Type: "audio", + Width: 240, + Height: 80, + Length: 4, + Lang: "zh", + }) + assert.IsType(t, "string", id) + assert.IsType(t, "string", content) + captchaStore.Get(id, false) + assert.True(t, CaptchaValidate(id, captchaStore.Get(id, false))) + + id, content = CaptchaMake(CaptchaOption{ + Type: "math", + Width: 240, + Height: 80, + Length: 4, + Lang: "zh", + }) + assert.IsType(t, "string", id) + assert.IsType(t, "string", content) + captchaStore.Get(id, false) + assert.True(t, CaptchaValidate(id, captchaStore.Get(id, false))) + + id, content = CaptchaMake(CaptchaOption{ + Type: "digit", + Width: 240, + Height: 80, + Length: 4, + Lang: "zh", + }) + assert.IsType(t, "string", id) + assert.IsType(t, "string", content) + captchaStore.Get(id, false) + assert.True(t, CaptchaValidate(id, captchaStore.Get(id, false))) +} + +func TestProcessCaptcha(t *testing.T) { + args := url.Values{} + args.Add("type", "math") + args.Add("lang", "zh") + process := gou.NewProcess("xiang.helper.Captcha", args) + res := process.Run().(maps.Map) + assert.IsType(t, "string", res.Get("id")) + assert.IsType(t, "string", res.Get("content")) + + value := captchaStore.Get(res.Get("id").(string), false) + process = gou.NewProcess("xiang.helper.CaptchaValidate", res.Get("id"), value) + assert.True(t, process.Run().(bool)) + assert.Panics(t, func() { + gou.NewProcess("xiang.helper.CaptchaValidate", res.Get("id"), "xxx").Run() + }) +} diff --git a/helper/jwt.go b/helper/jwt.go new file mode 100644 index 00000000..d9bcc8a2 --- /dev/null +++ b/helper/jwt.go @@ -0,0 +1,97 @@ +package helper + +import ( + "fmt" + "time" + + "github.com/golang-jwt/jwt" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/xiang/config" +) + +// JwtClaims 用户Token +type JwtClaims struct { + ID int + Data map[string]interface{} + jwt.StandardClaims +} + +// JwtValidate JWT 校验 +func JwtValidate(tokenString string) map[string]interface{} { + token, err := jwt.ParseWithClaims(tokenString, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(config.Conf.JWT.Secret), nil + }) + + if err != nil { + exception.New("令牌无效", 403).Ctx(err.Error()).Throw() + return nil + } + + if claims, ok := token.Claims.(*JwtClaims); ok && token.Valid { + return claims.Data + } + + exception.New("令牌无效", 403).Ctx(token.Claims).Throw() + return nil +} + +// JwtMake 生成 JWT +// subject options[0], audience options[1], issuer options[1] +func JwtMake(id int, data map[string]interface{}, timeout int64, options ...string) string { + now := time.Now().Unix() + expiresAt := now + timeout + uid := fmt.Sprintf("%d", id) + subject := "User Token" + audience := "Xiang Metadata Admin Panel" + issuer := fmt.Sprintf("xiang:%d", id) + length := len(options) + if length > 0 { + subject = options[0] + } + if length > 1 { + audience = options[1] + } + if length > 2 { + issuer = options[2] + } + claims := &JwtClaims{ + ID: id, + Data: data, + StandardClaims: jwt.StandardClaims{ + Id: uid, // 唯一ID + Subject: subject, // 主题 + Audience: audience, // 接收人 + ExpiresAt: expiresAt, // 过期时间 + NotBefore: now, // 生效时间 + IssuedAt: now, // 签发时间 + Issuer: issuer, // 签发人 + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(config.Conf.JWT.Secret)) + if err != nil { + exception.New("生成令牌失败", 500).Ctx(err).Throw() + } + return tokenString +} + +// ProcessJwtMake xiang.helper.JwtMake 生成JWT +func ProcessJwtMake(process *gou.Process) interface{} { + process.ValidateArgNums(3) + id := process.ArgsInt(0) + data := process.ArgsMap(1) + timeout := int64(process.ArgsInt(2)) + args := []string{} + for i := 3; i < len(process.Args); i++ { + args = append(args, fmt.Sprintf("%v", process.Args[i])) + } + return JwtMake(id, data, timeout, args...) +} + +// ProcessJwtValidate xiang.helper.JwtValidate 校验JWT +func ProcessJwtValidate(process *gou.Process) interface{} { + process.ValidateArgNums(1) + tokenString := process.ArgsString(0) + return JwtValidate(tokenString) +} diff --git a/helper/jwt_test.go b/helper/jwt_test.go new file mode 100644 index 00000000..17cd6e30 --- /dev/null +++ b/helper/jwt_test.go @@ -0,0 +1,32 @@ +package helper + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou" +) + +func TestJwt(t *testing.T) { + data := map[string]interface{}{"hello": "world", "id": 1} + tokenString := JwtMake(1, data, 1, "Unit Test", "Test", "UnitTest") + res := JwtValidate(tokenString) + assert.Equal(t, float64(1), res["id"]) + assert.Equal(t, "world", res["hello"]) + time.Sleep(2 * time.Second) + assert.Panics(t, func() { JwtValidate(tokenString) }) +} + +func TestProcessJwt(t *testing.T) { + data := map[string]interface{}{"hello": "world", "id": 1} + args := []interface{}{1, data, 1, "Unit Test", "Test", "UnitTest"} + process := gou.NewProcess("xiang.helper.JwtMake", args...) + token := process.Run() + tokenString := token.(string) + res := gou.NewProcess("xiang.helper.JwtValidate", tokenString).Run().(map[string]interface{}) + assert.Equal(t, float64(1), res["id"]) + assert.Equal(t, "world", res["hello"]) + time.Sleep(2 * time.Second) + assert.Panics(t, func() { gou.NewProcess("xiang.helper.JwtValidate", tokenString).Run() }) +} diff --git a/helper/map.go b/helper/map.go index 08a09abf..bf196ef9 100644 --- a/helper/map.go +++ b/helper/map.go @@ -1,5 +1,7 @@ package helper +import "github.com/yaoapp/gou" + // MapValues 返回映射的数值 func MapValues(record map[string]interface{}) []interface{} { values := []interface{}{} @@ -17,3 +19,17 @@ func MapKeys(record map[string]interface{}) []string { } return keys } + +// ProcessMapValues xiang.helper.MapValues 返回映射的数值 +func ProcessMapValues(process *gou.Process) interface{} { + process.ValidateArgNums(1) + record := process.ArgsMap(0) + return MapValues(record) +} + +// ProcessMapKeys xiang.helper.MapKeys 返回映射的键 +func ProcessMapKeys(process *gou.Process) interface{} { + process.ValidateArgNums(1) + record := process.ArgsMap(0) + return MapKeys(record) +} diff --git a/helper/password.go b/helper/password.go new file mode 100644 index 00000000..08a0ac85 --- /dev/null +++ b/helper/password.go @@ -0,0 +1,21 @@ +package helper + +import ( + "github.com/yaoapp/gou" + "golang.org/x/crypto/bcrypt" +) + +// PasswordValidate 校验密码 +func PasswordValidate(password string, passwordHash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)) + if err != nil { + return false + } + return true +} + +// ProcessPasswordValidate xiang.helper.PasswordValidate 校验密码 +func ProcessPasswordValidate(process *gou.Process) interface{} { + process.ValidateArgNums(2) + return PasswordValidate(process.ArgsString(0), process.ArgsString(1)) +} diff --git a/helper/password_test.go b/helper/password_test.go new file mode 100644 index 00000000..efe6a2f7 --- /dev/null +++ b/helper/password_test.go @@ -0,0 +1,27 @@ +package helper + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou" +) + +func TestPassword(t *testing.T) { + assert.True(t, PasswordValidate("U123456p+", "$2a$04$TS/rWBs66jADjQl8fa.w..ivkNAjH8d4sI1OPGvEB9Leed6EpzIF2")) + assert.False(t, PasswordValidate("U123456p+", "123456")) +} + +func TestProcessPassword(t *testing.T) { + pwd := "U123456p+" + hash := "$2a$04$TS/rWBs66jADjQl8fa.w..ivkNAjH8d4sI1OPGvEB9Leed6EpzIF2" + args := []interface{}{pwd, hash} + process := gou.NewProcess("xiang.helper.PasswordValidate", args...) + res := process.Run() + assert.True(t, res.(bool)) + + args = []interface{}{pwd, "123456"} + process = gou.NewProcess("xiang.helper.PasswordValidate", args...) + res = process.Run() + assert.False(t, res.(bool)) +} diff --git a/helper/process.go b/helper/process.go index 104a133c..c877a123 100644 --- a/helper/process.go +++ b/helper/process.go @@ -14,88 +14,16 @@ func init() { gou.RegisterProcessHandler("xiang.helper.ArrayTree", ProcessArrayTree) gou.RegisterProcessHandler("xiang.helper.MapKeys", ProcessMapKeys) gou.RegisterProcessHandler("xiang.helper.MapValues", ProcessMapValues) + gou.RegisterProcessHandler("xiang.helper.Captcha", ProcessCaptcha) + gou.RegisterProcessHandler("xiang.helper.CaptchaValidate", ProcessCaptchaValidate) + gou.RegisterProcessHandler("xiang.helper.PasswordValidate", ProcessPasswordValidate) + gou.RegisterProcessHandler("xiang.helper.JwtMake", ProcessJwtMake) + gou.RegisterProcessHandler("xiang.helper.JwtValidate", ProcessJwtValidate) gou.RegisterProcessHandler("xiang.helper.For", ProcessFor) gou.RegisterProcessHandler("xiang.helper.Each", ProcessEach) gou.RegisterProcessHandler("xiang.helper.Print", ProcessPrint) } -// ProcessArrayPluck xiang.helper.ArrayPluck 将多个数据记录集合,合并为一个数据记录集合 -func ProcessArrayPluck(process *gou.Process) interface{} { - process.ValidateArgNums(2) - columns := process.ArgsStrings(0) - pluck := process.ArgsMap(1) - return ArrayPluck(columns, pluck) -} - -// ProcessArraySplit xiang.helper.ArraySplit 将多条数记录集合,分解为一个 columns:[]string 和 values: [][]interface{} -func ProcessArraySplit(process *gou.Process) interface{} { - process.ValidateArgNums(1) - records := process.ArgsRecords(0) - columns, values := ArraySplit(records) - return map[string]interface{}{ - "columns": columns, - "values": values, - } -} - -// ProcessArrayColumn xiang.helper.ArrayColumn 返回多条数据记录,指定字段数值。 -func ProcessArrayColumn(process *gou.Process) interface{} { - process.ValidateArgNums(2) - records := process.ArgsRecords(0) - name := process.ArgsString(1) - values := ArrayColumn(records, name) - return values -} - -// ProcessArrayKeep xiang.helper.ArrayKeep 仅保留指定键名的数据 -func ProcessArrayKeep(process *gou.Process) interface{} { - process.ValidateArgNums(2) - 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) - record := process.ArgsMap(0) - return MapValues(record) -} - -// ProcessMapKeys xiang.helper.MapKeys 返回映射的键 -func ProcessMapKeys(process *gou.Process) interface{} { - process.ValidateArgNums(1) - record := process.ArgsMap(0) - return MapKeys(record) -} - -// ProcessEach xiang.helper.Each 循环过程控制 -func ProcessEach(process *gou.Process) interface{} { - process.ValidateArgNums(2) - v := process.Args[0] - p := ProcessOf(process.ArgsMap(1)) - Range(v, p) - return nil -} - -// ProcessFor xiang.helper.For 循环过程控制 -func ProcessFor(process *gou.Process) interface{} { - process.ValidateArgNums(3) - from := process.ArgsInt(0) - to := process.ArgsInt(1) - p := ProcessOf(process.ArgsMap(2)) - For(from, to, p) - return nil -} - // ProcessPrint xiang.helper.Print 打印语句 func ProcessPrint(process *gou.Process) interface{} { process.ValidateArgNums(1) diff --git a/helper/process_test.go b/helper/process_test.go deleted file mode 100644 index a49f16e2..00000000 --- a/helper/process_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package helper - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/yaoapp/gou" - "github.com/yaoapp/kun/maps" -) - -func TestProcessArrayPluck(t *testing.T) { - args := []interface{}{ - []interface{}{"城市", "行业", "计费"}, - map[string]interface{}{ - "行业": map[string]interface{}{"key": "city", "value": "数量", "items": []map[string]interface{}{{"city": "北京", "数量": 32}, {"city": "上海", "数量": 20}}}, - "计费": map[string]interface{}{"key": "city", "value": "计费种类", "items": []map[string]interface{}{{"city": "北京", "计费种类": 6}, {"city": "西安", "计费种类": 3}}}, - }, - } - process := gou.NewProcess("xiang.helper.ArrayPluck", args...) - response := ProcessArrayPluck(process) - assert.NotNil(t, response) - items, ok := response.([]map[string]interface{}) - assert.True(t, ok) - - assert.Equal(t, 3, len(items)) - for _, item := range items { - maps.Of(item).Has("城市") - maps.Of(item).Has("行业") - maps.Of(item).Has("计费") - } -} - -func TestProcessArraySplit(t *testing.T) { - args := []interface{}{ - []map[string]interface{}{ - {"name": "阿里云计算有限公司", "short_name": "阿里云"}, - {"name": "世纪互联蓝云", "short_name": "上海蓝云"}, - }, - } - process := gou.NewProcess("xiang.helper.ArraySplit", args...) - response := process.Run() - assert.NotNil(t, response) - res, ok := response.(map[string]interface{}) - assert.True(t, ok) - - columns, ok := res["columns"].([]string) - assert.True(t, ok) - - values, ok := res["values"].([][]interface{}) - assert.True(t, ok) - assert.Equal(t, 2, len(columns)) - assert.Equal(t, 2, len(values)) - for _, value := range values { - assert.Equal(t, 2, len(value)) - } -} diff --git a/helper/range.go b/helper/range.go index 1f3264bf..d9beda06 100644 --- a/helper/range.go +++ b/helper/range.go @@ -139,3 +139,22 @@ func ProcessOf(v map[string]interface{}) Process { Args: []interface{}{}, } } + +// ProcessEach xiang.helper.Each 循环过程控制 +func ProcessEach(process *gou.Process) interface{} { + process.ValidateArgNums(2) + v := process.Args[0] + p := ProcessOf(process.ArgsMap(1)) + Range(v, p) + return nil +} + +// ProcessFor xiang.helper.For 循环过程控制 +func ProcessFor(process *gou.Process) interface{} { + process.ValidateArgNums(3) + from := process.ArgsInt(0) + to := process.ArgsInt(1) + p := ProcessOf(process.ArgsMap(2)) + For(from, to, p) + return nil +}