Browse Source

Merge branch 'feature/system-user' of xuyiping/kpt-tmr-group into develop

xuyiping 2 years ago
parent
commit
d9c075bd42
80 changed files with 8530 additions and 597 deletions
  1. 1 0
      .gitignore
  2. 12 0
      backend/operation/pagination.proto
  3. 2 3
      backend/operation/pasture.proto
  4. 123 4
      backend/operation/system.proto
  5. 15 0
      config/app.go
  6. 9 0
      config/app.test.yaml
  7. 2 0
      dep/dep.go
  8. 5 2
      go.mod
  9. 28 4
      go.sum
  10. 3 3
      http/handler/pasture/pasture.go
  11. 144 0
      http/handler/system/menu.go
  12. 91 1
      http/handler/system/role.go
  13. 181 1
      http/handler/system/user.go
  14. 34 28
      http/middleware/cors.go
  15. 11 5
      http/middleware/pagination.go
  16. 50 0
      http/middleware/sso.go
  17. 30 6
      http/route/app_api.go
  18. 0 53
      logger/logrus-2023-05-06.log
  19. 37 31
      model/group_pasture.go
  20. 79 3
      model/system_menu.go
  21. 28 0
      model/system_menu_permissions.go
  22. 15 0
      model/system_mobile.go
  23. 28 0
      model/system_mobile_permissions.go
  24. 28 0
      model/system_pasture_permissions.go
  25. 55 0
      model/system_role.go
  26. 100 8
      model/system_user.go
  27. 32 5
      module/backend/interface.go
  28. 5 5
      module/backend/ops_service.go
  29. 211 0
      module/backend/system_permissions.go
  30. 436 0
      module/backend/system_service.go
  31. 6 6
      pkg/apiok/common.go
  32. 64 0
      pkg/jwt/jwt.go
  33. 1 176
      pkg/tool/tool.go
  34. 658 0
      pkg/valid/README.md
  35. 199 0
      pkg/valid/error.go
  36. 185 0
      pkg/valid/error_test.go
  37. 192 0
      pkg/valid/example_test.go
  38. 29 0
      pkg/valid/interface.go
  39. 293 0
      pkg/valid/is/rule.go
  40. 98 0
      pkg/valid/is/rule_test.go
  41. 144 0
      pkg/valid/map.go
  42. 119 0
      pkg/valid/map_test.go
  43. 25 0
      pkg/valid/rule.go
  44. 63 0
      pkg/valid/rule_absent.go
  45. 100 0
      pkg/valid/rule_absent_test.go
  46. 98 0
      pkg/valid/rule_date.go
  47. 87 0
      pkg/valid/rule_date_test.go
  48. 93 0
      pkg/valid/rule_each.go
  49. 74 0
      pkg/valid/rule_each_test.go
  50. 51 0
      pkg/valid/rule_in.go
  51. 53 0
      pkg/valid/rule_in_test.go
  52. 100 0
      pkg/valid/rule_length.go
  53. 103 0
      pkg/valid/rule_length_test.go
  54. 50 0
      pkg/valid/rule_match.go
  55. 51 0
      pkg/valid/rule_match_test.go
  56. 191 0
      pkg/valid/rule_minmax.go
  57. 144 0
      pkg/valid/rule_minmax_test.go
  58. 47 0
      pkg/valid/rule_not_in.go
  59. 51 0
      pkg/valid/rule_not_in_test.go
  60. 40 0
      pkg/valid/rule_not_nil.go
  61. 58 0
      pkg/valid/rule_not_nil_test.go
  62. 68 0
      pkg/valid/rule_required.go
  63. 96 0
      pkg/valid/rule_required_test.go
  64. 79 0
      pkg/valid/rule_strings.go
  65. 139 0
      pkg/valid/rule_strings_test.go
  66. 47 0
      pkg/valid/rule_when.go
  67. 90 0
      pkg/valid/rule_when_test.go
  68. 140 0
      pkg/valid/struct.go
  69. 204 0
      pkg/valid/struct_test.go
  70. 175 0
      pkg/valid/util.go
  71. 296 0
      pkg/valid/util_test.go
  72. 221 0
      pkg/valid/validation.go
  73. 266 0
      pkg/valid/validation_test.go
  74. 0 187
      proto/go/backend/common/enum.pb.go
  75. 164 0
      proto/go/backend/operation/pagination.pb.go
  76. 45 56
      proto/go/backend/operation/pasture.pb.go
  77. 1126 6
      proto/go/backend/operation/system.pb.go
  78. 67 0
      service/sso/cache.go
  79. 45 0
      service/sso/sso.go
  80. 0 4
      store/kptstore/rw_store.go

+ 1 - 0
.gitignore

@@ -46,3 +46,4 @@ _testmain.go
 .idea/
 bin/
 .vscode/
+logger

+ 12 - 0
backend/operation/pagination.proto

@@ -0,0 +1,12 @@
+syntax = "proto3";
+
+package backend.operation;
+
+option go_package = ".;operationPb";
+
+
+message PaginationModel {
+  int32 Page       = 1;
+  int32 PageSize   = 2;
+  int32 PageOffset = 3;
+}

+ 2 - 3
backend/operation/pasture.proto

@@ -9,11 +9,10 @@ message AddPastureRequest {
   int64 id = 1;
   string name = 2; // 牧场名称
   string manager_user = 3; // 牧场负责人名称
-  string manager_password = 4; // 牧场负责人账号
   string manager_phone = 5;   // 牧场负责人手机号
   string address = 6;   // 牧场地址
   IsShow.Kind is_show = 7;    // 是否启用
-  int64 Created_at = 8;    // 创建时间
+  int64 created_at = 8;    // 创建时间
 }
 
 message SearchPastureRequest {
@@ -29,5 +28,5 @@ message SearchPastureRequest {
 message SearchPastureResponse {
   int32 page = 1;
   int32 total = 2;
-  repeated AddPastureRequest data = 3;
+  repeated AddPastureRequest list = 3;
 }

+ 123 - 4
backend/operation/system.proto

@@ -4,11 +4,130 @@ package backend.operation;
 option go_package = ".;operationPb";
 
 import "backend/operation/enum.proto";
-// 用户权限
+import "backend/operation/pagination.proto";
+import "backend/operation/pasture.proto";
+// 用户角色
 message AddRoleRequest {
   int64 id = 1;
-  string name = 2;      // 角色名称
-  string remarks = 3;   // 角色备注
-  IsShow is_show = 4;   // 是否启用
+  string name = 2;        // 角色名称
+  string remarks = 3;     // 角色备注
+  IsShow.Kind is_show = 4;     // 是否启用
+  repeated int64 pasture_id = 5;    // 牧场id
+  repeated int64 menu_id = 6;       // 菜单id
+  repeated int64 mobile_id = 7;     // 移动端id
+  string create_user = 8;          // 创建用户
+  int64 created_at = 9;             // 创建时间
+  string crated_at_format = 10;     // 创建时间格式化
 }
 
+message SearchRoleRequest {
+  string name = 3;       // 角色名称
+  PaginationModel pagination = 2;  // 分页
+}
+
+message SearchRoleResponse {
+  int32 page = 1;
+  int32 total = 2;
+  repeated AddRoleRequest list = 3;
+}
+
+// 用户token
+message SystemToken {
+  string token = 1;
+}
+
+// 用户登录
+message UserAuth {
+  string user_name = 1;   // 用户名称
+  string password = 2;    // 用户密码
+  string phone = 3;       // 用户手机号
+  repeated UserRole roles  = 4;    // 用户角色
+  string employee_name = 5;    // 员工名称
+}
+
+message UserRole {
+  int64 id = 1;      // 角色id
+  string name = 2;   // 角色名称
+}
+
+message AddSystemUser {
+  int64 id = 1;                    // 用户id
+  string name = 2;                 // 用户名称
+  string phone = 3;                // 用户手机号
+  repeated UserRole roles = 4;      // 角色
+  IsShow.Kind is_show = 5;         // 是否开启
+  string employee_name = 6;        // 员工姓名
+  string create_user = 7;          // 创建人
+  int64 created_at = 8;            // 创建时间
+  string crated_at_format = 9;      // 创建时间格式化
+}
+
+// 查询用户
+message SearchUserRequest {
+  string name = 1;               // 用户名称
+  string employee_name = 2;       // 员工姓名
+  IsShow.Kind is_show = 3;        // 是否启用
+  int64 created_start_time = 4;   // 开始时间
+  int64 created_end_time = 5;     // 结束时间
+  PaginationModel pagination = 6; // 分页
+}
+
+message SearchUserResponse {
+  int32 page = 1;
+  int32 total = 2;
+  repeated AddSystemUser list = 3;
+}
+
+message IsShowSystemUserRequest {
+  int64 user_id = 1;  // 用户id
+  IsShow.Kind is_show = 2;   // is_show
+}
+
+// 系统菜单权限
+message AddMenuRequest {
+  int64 id = 1;
+  string name = 2;       // 名称
+  int64 parent_id = 3;   // 父id
+  int32 menu_type = 4;  // 菜单类型 1 菜单 2 按钮
+  string title = 5;      // 标题
+  string path = 6;       // 路径 path
+  IsShow.Kind is_show = 7;   // 是否显示
+  string component = 8;      // 组件
+  string icon = 9;           // 图标
+  int32 sort = 10;           // 排序
+  string redirect = 11;      // 重定向
+  int64 created_at = 12;     // 创建时间
+  string crated_at_format = 13;      // 创建时间格式化
+  int32 level = 14;      // 菜单等级
+  repeated AddMenuRequest children = 15;   // 子分类
+}
+
+message IsShowSystemMenuRequest {
+  int64 menu_id = 1;  // 角色id
+  IsShow.Kind is_show = 2;   // is_show
+}
+
+// 查询菜单权限
+message SearchMenuRequest {
+  string name = 1;               // 菜单名称
+  PaginationModel pagination = 2; // 分页
+}
+
+message SearchMenuResponse {
+  int32 page = 1;
+  int32 total = 2;
+  repeated AddMenuRequest list = 3;
+}
+
+// 系统用户权限相关
+message SystemUserMenuPermissions {
+  repeated AddPastureRequest pasture_list = 1;    // 牧场列表
+  repeated AddMenuRequest menu_list = 2;          // 菜单列表
+  repeated AddMobileRequest mobile_list = 3;      // 移动端权限
+}
+
+// 移动端
+message AddMobileRequest {
+  int64 id = 1;     // id
+  string name = 2;  // 名称
+}

+ 15 - 0
config/app.go

@@ -25,6 +25,9 @@ type AppConfig struct {
 
 	// 数据库配置 额外加载文件部分 database.yaml
 	StoreSetting StoreSetting `json:"storeSetting" yaml:"store"`
+	// redis 配置
+	RedisSetting RedisSetting `json:"RedisSetting" yaml:"redis_setting"`
+	JwtSecret    string       `json:"jwtSecret" yaml:"jwt_secret"`
 }
 
 // StoreSetting 数据库配置
@@ -36,6 +39,18 @@ type StoreSetting struct {
 	KptEventDSNMigr string `yaml:"kpt_tmr_group_migr" env:"LINGO_COURSE_DSN_MIGR"`
 }
 
+type RedisSetting struct {
+	// sso 配置
+	SSOCache SSOCache `json:"SSOCache" yaml:"sso_cache"`
+}
+
+type SSOCache struct {
+	Addr        string `json:"addr" yaml:"addr"`
+	Requirepass string `json:"requirepass" yaml:"requirepass"`
+	DB          int    `json:"DB" yaml:"DB"`
+	Expiry      int    `json:"expiry" yaml:"expiry"`
+}
+
 func Options() *AppConfig {
 	return options
 }

+ 9 - 0
config/app.test.yaml

@@ -9,3 +9,12 @@ store:
   driver_name: mysql
   kpt_tmr_group_rw: "root:123456@tcp(127.0.0.1:3306)/kpt_tmr_group?charset=utf8mb4&parseTime=true&loc=Local&allowNativePasswords=true&timeout=300s&readTimeout=300s&writeTimeout=300s"
   kpt_tmr_group_migr: "root:123456@tcp(127.0.0.1:3306)/kpt_tmr_group?charset=utf8mb4&parseTime=true&loc=Local&allowNativePasswords=true&timeout=300s&readTimeout=300s&writeTimeout=300s"
+
+redis_setting:
+  sso_cache:
+    addr: 192.168.1.70:6379
+    db: 12
+    requirepass: ""
+    expiry: 120
+
+jwt_secret: "sUd7j%UfJMt59ywh"

+ 2 - 0
dep/dep.go

@@ -4,6 +4,7 @@ import (
 	"kpt-tmr-group/config"
 	"kpt-tmr-group/module/backend"
 	"kpt-tmr-group/pkg/di"
+	"kpt-tmr-group/service/sso"
 	"kpt-tmr-group/store/kptstore"
 )
 
@@ -23,5 +24,6 @@ func Options() []di.HubOption {
 		// store
 		kptstore.Module,
 		backend.Module,
+		sso.Module,
 	}
 }

+ 5 - 2
go.mod

@@ -3,17 +3,19 @@ module kpt-tmr-group
 go 1.17
 
 require (
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/getsentry/sentry-go v0.20.0
 	github.com/gin-contrib/cors v1.4.0
 	github.com/gin-contrib/gzip v0.0.6
 	github.com/gin-contrib/requestid v0.0.6
 	github.com/gin-gonic/gin v1.9.0
+	github.com/go-redis/redis v6.15.9+incompatible
+	github.com/go-redis/redis/v7 v7.4.1
 	github.com/google/go-cmp v0.5.9
 	github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
 	github.com/jinzhu/copier v0.3.5
 	github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
-	github.com/lunny/log v0.0.0-20160921050905-7887c61bf0de
 	github.com/mitchellh/mapstructure v1.5.0
 	github.com/natefinch/lumberjack v2.0.0+incompatible
 	github.com/sirupsen/logrus v1.9.0
@@ -22,6 +24,7 @@ require (
 	github.com/stretchr/testify v1.8.2
 	go.uber.org/dig v1.15.0
 	go.uber.org/zap v1.21.0
+	golang.org/x/oauth2 v0.5.0
 	google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef
 	google.golang.org/grpc v1.52.0
 	google.golang.org/protobuf v1.30.0
@@ -55,7 +58,6 @@ require (
 	github.com/lestrrat-go/strftime v1.0.6 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mattn/go-isatty v0.0.17 // indirect
-	github.com/mattn/go-sqlite3 v1.14.16 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -80,6 +82,7 @@ require (
 	golang.org/x/net v0.8.0 // indirect
 	golang.org/x/sys v0.6.0 // indirect
 	golang.org/x/text v0.8.0 // indirect
+	google.golang.org/appengine v1.6.7 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect

+ 28 - 4
go.sum

@@ -64,6 +64,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -71,6 +73,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/getsentry/sentry-go v0.20.0 h1:bwXW98iMRIWxn+4FgPW7vMrjmbym6HblXALmhjHmQaQ=
@@ -103,6 +106,10 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
 github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
 github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
 github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
+github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
+github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
+github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
 github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -181,6 +188,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -219,15 +228,11 @@ github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkL
 github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
 github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
 github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
-github.com/lunny/log v0.0.0-20160921050905-7887c61bf0de h1:nyxwRdWHAVxpFcDThedEgQ07DbcRc5xgNObtbTp76fk=
-github.com/lunny/log v0.0.0-20160921050905-7887c61bf0de/go.mod h1:3q8WtuPQsoRbatJuy3nvq/hRSvuBJrHHr+ybPPiNvHQ=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
 github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
-github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -239,6 +244,11 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
 github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
 github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
@@ -376,6 +386,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -386,6 +397,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -417,6 +429,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
+golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -429,6 +443,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -439,6 +454,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -566,6 +582,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -641,14 +658,21 @@ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cn
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

+ 3 - 3
http/handler/pasture/pasture.go

@@ -10,13 +10,13 @@ import (
 	"github.com/gin-gonic/gin"
 )
 
-func AddPasture(c *gin.Context) {
-	req := make([]*operationPb.AddPastureRequest, 0)
+func AddSystemPasture(c *gin.Context) {
+	var req operationPb.AddPastureRequest
 	if err := c.BindJSON(&req); err != nil {
 		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
 		return
 	}
-	if err := middleware.BackendOperation(c).OpsService.CreatePastureList(c, req); err != nil {
+	if err := middleware.BackendOperation(c).OpsService.CreatePasture(c, &req); err != nil {
 		apierr.ClassifiedAbort(c, err)
 		return
 	}

+ 144 - 0
http/handler/system/menu.go

@@ -0,0 +1,144 @@
+package system
+
+import (
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/apiok"
+	"kpt-tmr-group/pkg/valid"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"net/http"
+	"strconv"
+
+	"github.com/gin-gonic/gin"
+)
+
+// AddSystemMenu 添加系统菜单权限
+func AddSystemMenu(c *gin.Context) {
+	var req operationPb.AddMenuRequest
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.Title, valid.Required),
+		valid.Field(&req.MenuType, valid.Required, valid.Min(0), valid.Max(2)),
+		valid.Field(&req.Level, valid.Required, valid.Min(1), valid.Max(3)),
+		valid.Field(&req.Path, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if req.MenuType == 1 {
+		if err := valid.Validate(&req.Component, valid.Required); err != nil {
+			apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+			return
+		}
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.CreateSystemMenu(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
+}
+
+// EditSystemMenu 编辑系统菜单权限
+func EditSystemMenu(c *gin.Context) {
+	var req operationPb.AddMenuRequest
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Id, valid.Required, valid.Min(1)),
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.ParentId, valid.Required, valid.Min(0)),
+		valid.Field(&req.MenuType, valid.Required, valid.Min(0), valid.Max(2)),
+		valid.Field(&req.Path, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if req.MenuType == 1 {
+		if err := valid.Validate(&req.Component, valid.Required); err != nil {
+			apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+			return
+		}
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.EditSystemMenu(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
+}
+
+// IsShowSystemMenu 是否启动
+func IsShowSystemMenu(c *gin.Context) {
+	var req operationPb.IsShowSystemMenuRequest
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.MenuId, valid.Required, valid.Min(1)),
+		valid.Field(&req.IsShow, valid.Required, valid.Min(1), valid.Max(2)),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.Dependency(c).StoreEventHub.OpsService.IsShowSystemMenu(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
+}
+
+// SearchSystemMenuList 菜单列表查询
+func SearchSystemMenuList(c *gin.Context) {
+	var req operationPb.SearchMenuRequest
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	req.Pagination = &operationPb.PaginationModel{
+		Page:       int32(c.GetInt(middleware.Page)),
+		PageSize:   int32(c.GetInt(middleware.PageSize)),
+		PageOffset: int32(c.GetInt(middleware.PageOffset)),
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.SearchSystemMenuList(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, apiok.CommonResponse(res))
+}
+
+// DeleteSystemMenu 删除菜单
+func DeleteSystemMenu(c *gin.Context) {
+	menuIdStr := c.Param("menu_id")
+	menuId, _ := strconv.Atoi(menuIdStr)
+
+	if err := valid.Validate(menuId, valid.Required, valid.Min(1)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.DeleteSystemRole(c, int64(menuId)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
+}

+ 91 - 1
http/handler/system/role.go

@@ -1,13 +1,103 @@
 package system
 
 import (
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
 	"kpt-tmr-group/pkg/apiok"
+	"kpt-tmr-group/pkg/valid"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
 	"net/http"
+	"strconv"
 
 	"github.com/gin-gonic/gin"
 )
 
-func AddRole(c *gin.Context) {
+// AddSystemRole 添加角色
+func AddSystemRole(c *gin.Context) {
+	var req operationPb.AddRoleRequest
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.PastureId, valid.Required),
+		valid.Field(&req.MenuId, valid.Required),
+		valid.Field(&req.CreateUser, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.Dependency(c).StoreEventHub.OpsService.CreateSystemRole(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
+}
+
+// EditSystemRole 编辑角色
+func EditSystemRole(c *gin.Context) {
+	var req operationPb.AddRoleRequest
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.PastureId, valid.Required),
+		valid.Field(&req.MenuId, valid.Required),
+		valid.Field(&req.CreateUser, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.Dependency(c).StoreEventHub.OpsService.EditSystemRole(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
+}
+
+// DeleteSystemRole 删除角色
+func DeleteSystemRole(c *gin.Context) {
+	roleIdStr := c.Param("role_id")
+	roleId, _ := strconv.Atoi(roleIdStr)
+
+	if err := valid.Validate(roleId, valid.Required, valid.Min(1)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.DeleteSystemRole(c, int64(roleId)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
 
 	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
 }
+
+// SearchSystemRoleList 角色列表
+func SearchSystemRoleList(c *gin.Context) {
+	var req operationPb.SearchRoleRequest
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	req.Pagination = &operationPb.PaginationModel{
+		Page:       int32(c.GetInt(middleware.Page)),
+		PageSize:   int32(c.GetInt(middleware.PageSize)),
+		PageOffset: int32(c.GetInt(middleware.PageOffset)),
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.SearchSystemRoleList(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, apiok.CommonResponse(res))
+}

+ 181 - 1
http/handler/system/user.go

@@ -1,13 +1,193 @@
 package system
 
 import (
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
 	"kpt-tmr-group/pkg/apiok"
+	"kpt-tmr-group/pkg/valid"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
 	"net/http"
+	"strconv"
 
 	"github.com/gin-gonic/gin"
 )
 
-func AddUser(c *gin.Context) {
+// Auth 用户登录
+func Auth(c *gin.Context) {
+	var req operationPb.UserAuth
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
 
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.UserName, valid.Required),
+		valid.Field(&req.Password, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.Auth(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, apiok.CommonResponse(res))
+}
+
+// AddSystemUser 创建系统用户
+func AddSystemUser(c *gin.Context) {
+	var req operationPb.AddSystemUser
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.Phone, valid.Required),
+		valid.Field(&req.EmployeeName, valid.Required),
+		valid.Field(&req.Roles, valid.NotNil),
+		valid.Field(&req.IsShow, valid.Max(2), valid.Min(1)),
+		valid.Field(&req.CreateUser, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.Dependency(c).StoreEventHub.OpsService.CreateSystemUser(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
+}
+
+// GetUserInfo 获取用户信息
+func GetUserInfo(c *gin.Context) {
+	token := middleware.GetToken(c)
+	if err := valid.Validate(token, valid.Required); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	res, err := middleware.BackendOperation(c).OpsService.GetUserInfo(c, token)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, apiok.CommonResponse(res))
+}
+
+// SearchSystemUserList 查询系统用户列表
+func SearchSystemUserList(c *gin.Context) {
+	var req operationPb.SearchUserRequest
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.IsShow, valid.Max(2), valid.Min(1)),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	req.Pagination = &operationPb.PaginationModel{
+		Page:       int32(c.GetInt(middleware.Page)),
+		PageSize:   int32(c.GetInt(middleware.PageSize)),
+		PageOffset: int32(c.GetInt(middleware.PageOffset)),
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.SearchSystemUserList(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, apiok.CommonResponse(res))
+}
+
+// EditSystemUser 编辑系统用户
+func EditSystemUser(c *gin.Context) {
+	var req operationPb.AddSystemUser
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Id, valid.Required, valid.Min(1)),
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.Phone, valid.Required),
+		valid.Field(&req.EmployeeName, valid.Required),
+		valid.Field(&req.Roles, valid.Required),
+		valid.Field(&req.CreateUser, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.Dependency(c).StoreEventHub.OpsService.EditSystemUser(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
+}
+
+// DeleteUser 删除系统用户
+func DeleteUser(c *gin.Context) {
+	userIdStr := c.Param("user_id")
+	userId, _ := strconv.Atoi(userIdStr)
+
+	if err := valid.Validate(userId, valid.Required, valid.Min(1)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.DeleteSystemUser(c, int64(userId)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
+}
+
+// IsShowSystemUser 系统用户启动开关
+func IsShowSystemUser(c *gin.Context) {
+	var req operationPb.IsShowSystemUserRequest
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.UserId, valid.Required, valid.Min(1)),
+		valid.Field(&req.IsShow, valid.Required, valid.Min(1), valid.Max(2)),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.Dependency(c).StoreEventHub.OpsService.IsShowSystemUser(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
 	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
 }
+
+// GetSystemUserPermissions 获取系统用户菜单权限
+func GetSystemUserPermissions(c *gin.Context) {
+	token := middleware.GetToken(c)
+	if err := valid.Validate(token, valid.Required); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	res, err := middleware.BackendOperation(c).OpsService.GetSystemUserPermissions(c, token)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, apiok.CommonResponse(res))
+}

+ 34 - 28
http/middleware/cors.go

@@ -1,43 +1,49 @@
 package middleware
 
 import (
-	"time"
+	"kpt-tmr-group/pkg/logger/zaplog"
+	"net/http"
+
+	"go.uber.org/zap"
 
 	"github.com/gin-contrib/cors"
 	"github.com/gin-gonic/gin"
 )
 
-var defaultCORSConfig = cors.Config{
-	AllowAllOrigins: true,
-	AllowMethods:    []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"},
-	AllowHeaders: []string{
-		"Origin",
-		"Accept",
-		"Accept-Language",
-		"Content-Language",
-		"Content-Type",
-		"User-Agent",
-		"Authorization",
-		"x-timezone-offset",
-		"x-user-id",
-		"X-User-Id",
-		"X-Rate-Limit-Token",
-		"x-rate-limit-token",
-		"X-Timezone-Name",
-		"x-timezone-name",
-		"X-Lingochamp-Id",
-		"x-lingochamp-id",
-		"x-user-language",
-	},
-	AllowCredentials: true,
-	MaxAge:           12 * time.Hour,
-}
-
 // CORS enable CORS support
 func CORS(configs ...cors.Config) gin.HandlerFunc {
 	if len(configs) != 0 {
 		return cors.New(configs[0])
 	}
+	return func(c *gin.Context) {
+		method := c.Request.Method
+		origin := c.Request.Header.Get("Origin") //请求头部
+		if origin != "" {
+			//接收客户端发送的origin (重要!)
+			c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
+			//服务器支持的所有跨域请求的方法
+			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
+			//允许跨域设置可以返回其他子段,可以自定义字段
+			c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session")
+			// 允许浏览器(客户端)可以解析的头部 (重要)
+			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
+			//设置缓存时间
+			c.Header("Access-Control-Max-Age", "172800")
+			//允许客户端传递校验信息比如 cookie (重要)
+			c.Header("Access-Control-Allow-Credentials", "true")
+		}
 
-	return cors.New(defaultCORSConfig)
+		//允许类型校验
+		if method == "OPTIONS" {
+			c.JSON(http.StatusOK, "ok!")
+		}
+
+		defer func() {
+			if err := recover(); err != nil {
+				zaplog.Error("cors", zap.Any("recover", err))
+			}
+		}()
+
+		c.Next()
+	}
 }

+ 11 - 5
http/middleware/pagination.go

@@ -7,19 +7,25 @@ import (
 	"github.com/gin-gonic/gin"
 )
 
+const (
+	Page       = "page"
+	PageSize   = "pageSize"
+	PageOffset = "pageOffset"
+)
+
 // Pagination sets page, pageSize and pageOffset to *gin.Context
 func Pagination() gin.HandlerFunc {
 	return func(c *gin.Context) {
-		page := getSetItem(c, "page", 1)
-		size := getSetItem(c, "pageSize", 20)
-		c.Set("pageOffset", (page-1)*size)
+		page := getSetItem(c, Page, 1)
+		size := getSetItem(c, PageSize, 20)
+		c.Set(PageOffset, (page-1)*size)
 		c.Next()
 	}
 }
 
 func getSetItem(c *gin.Context, k string, d int) int {
 	var n int
-	if v := c.Query(k); v != "" {
+	if v := c.Request.Header.Get(k); v != "" {
 		if i, err := strconv.Atoi(v); err == nil {
 			if i > 0 {
 				n = i
@@ -32,6 +38,6 @@ func getSetItem(c *gin.Context, k string, d int) int {
 	}
 
 	c.Set(k, n)
-	c.Request.URL.Query().Set(k, fmt.Sprintf("%d", n))
+	c.Request.Header.Set(k, fmt.Sprintf("%d", n))
 	return n
 }

+ 50 - 0
http/middleware/sso.go

@@ -0,0 +1,50 @@
+package middleware
+
+import (
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/jwt"
+	commonPb "kpt-tmr-group/proto/go/backend/common"
+
+	"net/http"
+	"strings"
+
+	"github.com/gin-gonic/gin"
+)
+
+const (
+	Authorization = "Authorization"
+	ToKenPrefix   = "Bearer "
+	UserName      = "userName"
+)
+
+func GetToken(c *gin.Context) string {
+	value := c.Request.Header.Get(Authorization)
+	if value != "" && strings.HasPrefix(value, ToKenPrefix) {
+		return strings.TrimPrefix(value, ToKenPrefix)
+	}
+	return ""
+}
+
+func unauthorized(c *gin.Context) {
+	c.AbortWithStatusJSON(http.StatusBadRequest, apierr.WithContext(c, commonPb.Error_UNAUTHORIZED))
+}
+
+// RequireAdmin ...
+func RequireAdmin() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		token := GetToken(c)
+		if token == "" {
+			unauthorized(c)
+			return
+		}
+
+		claims, err := jwt.ParseToken(token)
+		if err != nil || claims == nil || claims.Username == "" {
+			unauthorized(c)
+			return
+		}
+
+		c.Set(UserName, claims.Username)
+		c.Next()
+	}
+}

+ 30 - 6
http/route/app_api.go

@@ -4,6 +4,7 @@ import (
 	"kpt-tmr-group/http/handler"
 	"kpt-tmr-group/http/handler/pasture"
 	"kpt-tmr-group/http/handler/system"
+	"kpt-tmr-group/http/middleware"
 
 	"github.com/gin-gonic/gin"
 )
@@ -18,19 +19,42 @@ func AppAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		// Health Check
 		s.GET("/check", handler.Health)
 
+		s.POST("/auth", system.Auth)
+
 		// system API 组
-		lingoRoute := authRouteGroup(s, "/api/v1/system/")
-		lingoRoute.POST("/user/add", system.AddUser)
+		// 系统用户
+		systemRoute := authRouteGroup(s, "/api/v1/system/")
+		systemRoute.POST("/user_info", system.GetUserInfo)
+		systemRoute.POST("/user/add", system.AddSystemUser)
+		systemRoute.POST("/user/list", system.SearchSystemUserList)
+		systemRoute.POST("/user/edit", system.EditSystemUser)
+		systemRoute.POST("/user/is_show", system.IsShowSystemUser)
+		systemRoute.DELETE("/user/:user_id", system.DeleteUser)
+		systemRoute.POST("/user/permissions", system.GetSystemUserPermissions)
+
+		// 系统角色
+		systemRoute.POST("/role/add", system.AddSystemRole)
+		systemRoute.POST("/role/edit", system.EditSystemRole)
+		systemRoute.DELETE("/role/:role_id", system.DeleteSystemRole)
+		systemRoute.POST("/role/list", system.SearchSystemRoleList)
+
+		// 系统菜单权限
+		systemRoute.POST("/menu/add", system.AddSystemMenu)
+		systemRoute.POST("/menu/edit", system.EditSystemMenu)
+		systemRoute.POST("/menu/is_show", system.IsShowSystemMenu)
+		systemRoute.POST("/menu/list", system.SearchSystemMenuList)
+		systemRoute.DELETE("/menu/:menu_id", system.DeleteSystemMenu)
 
 		// 牧场管理
-		lingoRoute.POST("/pasture/add", pasture.AddPasture)
-		lingoRoute.POST("/pasture/list", pasture.SearchPastureList)
+		opsRoute := authRouteGroup(s, "/api/v1/ops/")
+		opsRoute.POST("/pasture/add", pasture.AddSystemPasture)
+		opsRoute.POST("/pasture/list", pasture.SearchPastureList)
 	}
 }
 
 func authRouteGroup(s *gin.Engine, relativePath string) *gin.RouterGroup {
 	group := s.Group(relativePath)
-	// TODO 中间件鉴权
-	// group.Use(middleware.Auth(), m.UserKitMiddleware())
+	// 中间件鉴权
+	group.Use(middleware.RequireAdmin(), middleware.Pagination(), middleware.CORS())
 	return group
 }

+ 0 - 53
logger/logrus-2023-05-06.log

@@ -1,53 +0,0 @@
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T16:51:38+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/store/kptstore/rw_store.go:47\n[error] failed to initialize database, got error Error 1045 (28000): Access denied for user 'root'@'172.19.0.1' (using password: NO)","time":"2023-05-06T16:51:38+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T16:54:30+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/store/kptstore/rw_store.go:47\n[error] failed to initialize database, got error Error 1049 (42000): Unknown database 'kpt_tmr-group'","time":"2023-05-06T16:54:30+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T16:54:56+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/store/kptstore/rw_store.go:47\n[error] failed to initialize database, got error Error 1049 (42000): Unknown database 'kpt_tmr-group'","time":"2023-05-06T16:54:56+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T16:55:18+08:00"}
-{"level":"info","msg":"kpt-tmr-group: boot HTTP server","time":"2023-05-06T16:55:18+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T17:59:10+08:00"}
-{"level":"info","msg":"kpt-tmr-group: boot HTTP server","time":"2023-05-06T17:59:10+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[66.660ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:06:55+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[5.984ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:06:55+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T18:09:56+08:00"}
-{"level":"info","msg":"kpt-tmr-group: boot HTTP server","time":"2023-05-06T18:09:56+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[15.212ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:10:01+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[6.141ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:10:01+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[4.496ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:11:22+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[3.826ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:11:22+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[2.234ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:11:22+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[3.514ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:11:22+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[5.620ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:11:23+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[2.685ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:11:23+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T18:12:14+08:00"}
-{"level":"info","msg":"kpt-tmr-group: boot HTTP server","time":"2023-05-06T18:12:14+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[9.825ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:12:24+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[2.170ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:12:24+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[2.756ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:12:26+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[1.601ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:12:26+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[2.269ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:12:27+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[1.699ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:12:27+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T18:12:54+08:00"}
-{"level":"info","msg":"kpt-tmr-group: boot HTTP server","time":"2023-05-06T18:12:54+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[8.080ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:13:13+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[3.225ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:13:13+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T18:14:27+08:00"}
-{"level":"info","msg":"kpt-tmr-group: boot HTTP server","time":"2023-05-06T18:14:27+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[10.386ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:14:32+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[2.703ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:14:32+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T18:15:01+08:00"}
-{"level":"info","msg":"kpt-tmr-group: boot HTTP server","time":"2023-05-06T18:15:01+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[15.669ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:15:06+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[2.203ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:15:06+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T18:16:02+08:00"}
-{"level":"info","msg":"kpt-tmr-group: boot HTTP server","time":"2023-05-06T18:16:02+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[15.813ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:16:06+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[2.120ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:16:06+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[4.555ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:16:08+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[3.303ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:16:08+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:38\n[2.195ms] [rows:1] SELECT count(*) FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%'","time":"2023-05-06T18:18:12+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:39\n[2.724ms] [rows:0] SELECT * FROM `group_pasture` WHERE is_show = 1  AND name like '%现代牧业%' ORDER BY id desc LIMIT 10","time":"2023-05-06T18:18:12+08:00"}
-{"level":"info","msg":"kpe-event: is starting","time":"2023-05-06T18:27:20+08:00"}
-{"level":"info","msg":"kpt-tmr-group: boot HTTP server","time":"2023-05-06T18:27:21+08:00"}
-{"level":"info","msg":"D:/project/golangNew/kpt-tmr-group/module/backend/service.go:14 Error 1265 (01000): Data truncated for column 'created_at' at row 1\n[31.262ms] [rows:0] INSERT INTO `group_pasture` (`name`,`manager_user`,`manager_password`,`manager_phone`,`is_show`,`address`,`created_at`,`updated_at`) VALUES ('选地低识要','officia cillum','e10adc3949ba59abbe56e057f20f883e','18642558992',1,'福建省汕尾市巴林左旗','2023-05-06 18:27:26.506','2023-05-06 18:27:26.506'),('种切命','minim Duis sint aliqua deserunt','e10adc3949ba59abbe56e057f20f883e','18135781774',1,'贵州省七台河市贵南县','2023-05-06 18:27:26.506','2023-05-06 18:27:26.506')","time":"2023-05-06T18:27:26+08:00"}

+ 37 - 31
model/group_pasture.go

@@ -21,21 +21,18 @@ func (s *GroupPasture) TableName() string {
 	return "group_pasture"
 }
 
-const InitPassword = "123456"
+const InitManagerPassword = "123456"
 
-func NewGroupPastureList(reqs []*operationPb.AddPastureRequest) []*GroupPasture {
-	groupPastureList := make([]*GroupPasture, len(reqs))
-	for k, pasture := range reqs {
-		groupPastureList[k] = &GroupPasture{
-			Name:            pasture.Name,
-			ManagerUser:     pasture.ManagerUser,
-			ManagerPassword: tool.Md5String(InitPassword),
-			ManagerPhone:    pasture.ManagerPhone,
-			IsShow:          operationPb.IsShow_OK,
-			Address:         pasture.Address,
-		}
+func NewGroupPasture(req *operationPb.AddPastureRequest) *GroupPasture {
+	groupPasture := &GroupPasture{
+		Name:            req.Name,
+		ManagerUser:     req.ManagerUser,
+		ManagerPassword: tool.Md5String(InitManagerPassword),
+		ManagerPhone:    req.ManagerPhone,
+		IsShow:          operationPb.IsShow_OK,
+		Address:         req.Address,
 	}
-	return groupPastureList
+	return groupPasture
 }
 
 type GroupPastureSlice []*GroupPasture
@@ -44,14 +41,24 @@ func (g GroupPastureSlice) ToPB() []*operationPb.AddPastureRequest {
 	res := make([]*operationPb.AddPastureRequest, len(g))
 	for i, v := range g {
 		res[i] = &operationPb.AddPastureRequest{
-			Id:              v.Id,
-			Name:            v.Name,
-			ManagerUser:     v.ManagerUser,
-			ManagerPassword: v.ManagerPassword,
-			ManagerPhone:    v.ManagerPhone,
-			Address:         v.ManagerPassword,
-			IsShow:          v.IsShow,
-			CreatedAt:       v.CreatedAt,
+			Id:           v.Id,
+			Name:         v.Name,
+			ManagerUser:  v.ManagerUser,
+			ManagerPhone: v.ManagerPhone,
+			Address:      v.Address,
+			IsShow:       v.IsShow,
+			CreatedAt:    v.CreatedAt,
+		}
+	}
+	return res
+}
+
+func (g GroupPastureSlice) ToPermissionsPB() []*operationPb.AddPastureRequest {
+	res := make([]*operationPb.AddPastureRequest, len(g))
+	for i, v := range g {
+		res[i] = &operationPb.AddPastureRequest{
+			Id:   v.Id,
+			Name: v.Name,
 		}
 	}
 	return res
@@ -59,27 +66,26 @@ func (g GroupPastureSlice) ToPB() []*operationPb.AddPastureRequest {
 
 func (g *GroupPasture) ToPb() *operationPb.AddPastureRequest {
 	return &operationPb.AddPastureRequest{
-		Id:              g.Id,
-		Name:            g.Name,
-		ManagerUser:     g.ManagerUser,
-		ManagerPassword: g.ManagerPassword,
-		ManagerPhone:    g.ManagerPhone,
-		Address:         g.Address,
-		IsShow:          g.IsShow,
-		CreatedAt:       g.CreatedAt,
+		Id:           g.Id,
+		Name:         g.Name,
+		ManagerUser:  g.ManagerUser,
+		ManagerPhone: g.ManagerPhone,
+		Address:      g.Address,
+		IsShow:       g.IsShow,
+		CreatedAt:    g.CreatedAt,
 	}
 }
 
 type GroupPastureResponse struct {
 	Page  int32                            `json:"page"`
 	Total int32                            `json:"total"`
-	Data  []*operationPb.AddPastureRequest `json:"data"`
+	List  []*operationPb.AddPastureRequest `json:"list"`
 }
 
 func (g *GroupPastureResponse) ToPB() *operationPb.SearchPastureResponse {
 	return &operationPb.SearchPastureResponse{
 		Page:  g.Page,
 		Total: g.Total,
-		Data:  g.Data,
+		List:  g.List,
 	}
 }

+ 79 - 3
model/system_menu.go

@@ -2,23 +2,99 @@ package model
 
 import (
 	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
 )
 
 type SystemMenu struct {
 	Id        int64                   `json:"id,omitempty"`
 	Name      string                  `json:"name,omitempty"`
-	MenuType  int                     `json:"menu_type,omitempty"`
+	MenuType  int32                   `json:"menu_type,omitempty"`
+	Level     int32                   `json:"level,omitempty"`
 	Title     string                  `json:"title,omitempty"`
 	Path      string                  `json:"path,omitempty"`
 	IsShow    operationPb.IsShow_Kind `json:"is_show,omitempty"`
+	IsDelete  operationPb.IsShow_Kind `json:"is_delete,omitempty"`
 	Component string                  `json:"component,omitempty"`
 	Icon      string                  `json:"icon,omitempty"`
-	Sort      int                     `json:"sort,omitempty"`
+	Sort      int32                   `json:"sort,omitempty"`
+	ParentId  int64                   `json:"parent_id"`
 	Redirect  string                  `json:"redirect,omitempty"`
 	CreatedAt int64                   `json:"created_at,omitempty"`
 	UpdatedAt int64                   `json:"updated_at,omitempty"`
 }
 
 func (s *SystemMenu) TableName() string {
-	return "system_role"
+	return "system_menu"
+}
+
+func NewSystemMenu(req *operationPb.AddMenuRequest) *SystemMenu {
+	return &SystemMenu{
+		Name:      req.Name,
+		MenuType:  req.MenuType,
+		Title:     req.Title,
+		Path:      req.Path,
+		Component: req.Component,
+		Icon:      req.Icon,
+		Sort:      req.Sort,
+		Redirect:  req.Redirect,
+		ParentId:  req.ParentId,
+		IsShow:    operationPb.IsShow_OK,
+		IsDelete:  operationPb.IsShow_OK,
+	}
+}
+
+type SystemMenuSlice []*SystemMenu
+
+func (s SystemMenuSlice) ToPB() []*operationPb.AddMenuRequest {
+	res := make([]*operationPb.AddMenuRequest, len(s))
+	for i, v := range s {
+		res[i] = &operationPb.AddMenuRequest{
+			Id:             v.Id,
+			Name:           v.Name,
+			MenuType:       v.MenuType,
+			Level:          v.Level,
+			Title:          v.Title,
+			IsShow:         v.IsShow,
+			Component:      v.Component,
+			Icon:           v.Icon,
+			Sort:           v.Sort,
+			Redirect:       v.Redirect,
+			ParentId:       v.ParentId,
+			CreatedAt:      v.CreatedAt,
+			CratedAtFormat: time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+		}
+	}
+	return res
+}
+
+func (s *SystemMenu) ToPb() *operationPb.AddMenuRequest {
+	return &operationPb.AddMenuRequest{
+		Id:        s.Id,
+		Name:      s.Name,
+		MenuType:  s.MenuType,
+		Level:     s.Level,
+		Title:     s.Title,
+		Path:      s.Path,
+		Component: s.Component,
+		Icon:      s.Icon,
+		Sort:      s.Sort,
+		Redirect:  s.Redirect,
+		ParentId:  s.ParentId,
+		IsShow:    s.IsShow,
+		CreatedAt: s.CreatedAt,
+	}
+}
+
+type SystemMenuResponse struct {
+	Page  int32                         `json:"page"`
+	Total int32                         `json:"total"`
+	List  []*operationPb.AddMenuRequest `json:"list"`
+}
+
+func (s *SystemMenuResponse) ToPB() *operationPb.SearchMenuResponse {
+	return &operationPb.SearchMenuResponse{
+		Page:  s.Page,
+		Total: s.Total,
+		List:  s.List,
+	}
 }

+ 28 - 0
model/system_menu_permissions.go

@@ -0,0 +1,28 @@
+package model
+
+import operationPb "kpt-tmr-group/proto/go/backend/operation"
+
+type SystemMenuPermissions struct {
+	Id        int64                   `json:"id,omitempty"`
+	RoleId    int64                   `json:"role_id"`
+	MenuId    int64                   `json:"menu_id"`
+	IsShow    operationPb.IsShow_Kind `json:"is_show"`
+	CreatedAt int64                   `json:"created_at"`
+	UpdatedAt int64                   `json:"updated_at"`
+}
+
+func (s *SystemMenuPermissions) TableName() string {
+	return "system_menu_permissions"
+}
+
+func NewSystemMenuPermissions(roleID int64, menuIds []int64) []*SystemMenuPermissions {
+	systemMenuPermissions := make([]*SystemMenuPermissions, len(menuIds))
+	for i, v := range menuIds {
+		systemMenuPermissions[i] = &SystemMenuPermissions{
+			RoleId: roleID,
+			MenuId: v,
+			IsShow: operationPb.IsShow_OK,
+		}
+	}
+	return systemMenuPermissions
+}

+ 15 - 0
model/system_mobile.go

@@ -0,0 +1,15 @@
+package model
+
+import operationPb "kpt-tmr-group/proto/go/backend/operation"
+
+type SystemMobile struct {
+	Id        int64                   `json:"id,omitempty"`
+	Name      string                  `json:"name"`
+	IsShow    operationPb.IsShow_Kind `json:"is_show"`
+	CreatedAt int64                   `json:"created_at"`
+	UpdatedAt int64                   `json:"updated_at"`
+}
+
+func (s *SystemMobile) TableName() string {
+	return "system_mobile"
+}

+ 28 - 0
model/system_mobile_permissions.go

@@ -0,0 +1,28 @@
+package model
+
+import operationPb "kpt-tmr-group/proto/go/backend/operation"
+
+type SystemMobilePermissions struct {
+	Id        int64                   `json:"id,omitempty"`
+	RoleId    int64                   `json:"role_id"`
+	MobileId  int64                   `json:"mobile_id"`
+	IsShow    operationPb.IsShow_Kind `json:"is_show"`
+	CreatedAt int64                   `json:"created_at"`
+	UpdatedAt int64                   `json:"updated_at"`
+}
+
+func (s *SystemMobilePermissions) TableName() string {
+	return "system_mobile_permissions"
+}
+
+func NewSystemMobilePermissions(roleID int64, mobileIds []int64) []*SystemMobilePermissions {
+	systemMobilePermissions := make([]*SystemMobilePermissions, len(mobileIds))
+	for i, v := range mobileIds {
+		systemMobilePermissions[i] = &SystemMobilePermissions{
+			RoleId:   roleID,
+			MobileId: v,
+			IsShow:   operationPb.IsShow_OK,
+		}
+	}
+	return systemMobilePermissions
+}

+ 28 - 0
model/system_pasture_permissions.go

@@ -0,0 +1,28 @@
+package model
+
+import operationPb "kpt-tmr-group/proto/go/backend/operation"
+
+type SystemPasturePermissions struct {
+	Id        int64                   `json:"id,omitempty"`
+	RoleId    int64                   `json:"role_id"`
+	PastureId int64                   `json:"pasture_id"`
+	IsShow    operationPb.IsShow_Kind `json:"is_show"`
+	CreatedAt int64                   `json:"created_at"`
+	UpdatedAt int64                   `json:"updated_at"`
+}
+
+func (s *SystemPasturePermissions) TableName() string {
+	return "system_pasture_permissions"
+}
+
+func NewSystemPasturePermissions(roleID int64, pastureIds []int64) []*SystemPasturePermissions {
+	systemPasturePermissions := make([]*SystemPasturePermissions, len(pastureIds))
+	for i, v := range pastureIds {
+		systemPasturePermissions[i] = &SystemPasturePermissions{
+			RoleId:    roleID,
+			PastureId: v,
+			IsShow:    operationPb.IsShow_OK,
+		}
+	}
+	return systemPasturePermissions
+}

+ 55 - 0
model/system_role.go

@@ -2,6 +2,7 @@ package model
 
 import (
 	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
 )
 
 type SystemRole struct {
@@ -17,3 +18,57 @@ type SystemRole struct {
 func (s *SystemRole) TableName() string {
 	return "system_role"
 }
+
+const LayoutTime = "2006-01-02 15:04:05"
+
+func NewSystemRole(req *operationPb.AddRoleRequest) *SystemRole {
+	systemRole := &SystemRole{
+		Name:       req.Name,
+		Remarks:    req.Remarks,
+		IsShow:     operationPb.IsShow_OK,
+		CreateUser: req.CreateUser,
+	}
+	return systemRole
+}
+
+type SystemRoleSlice []*SystemRole
+
+func (s SystemRoleSlice) ToPB() []*operationPb.AddRoleRequest {
+	res := make([]*operationPb.AddRoleRequest, len(s))
+	for i, v := range s {
+		res[i] = &operationPb.AddRoleRequest{
+			Id:             v.Id,
+			Name:           v.Name,
+			Remarks:        v.Remarks,
+			CreateUser:     v.CreateUser,
+			IsShow:         v.IsShow,
+			CreatedAt:      v.CreatedAt,
+			CratedAtFormat: time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+		}
+	}
+	return res
+}
+
+func (s *SystemRole) ToPb() *operationPb.AddRoleRequest {
+	return &operationPb.AddRoleRequest{
+		Id:         s.Id,
+		Name:       s.Name,
+		CreateUser: s.CreateUser,
+		IsShow:     s.IsShow,
+		CreatedAt:  s.CreatedAt,
+	}
+}
+
+type SystemRoleResponse struct {
+	Page  int32                         `json:"page"`
+	Total int32                         `json:"total"`
+	List  []*operationPb.AddRoleRequest `json:"list"`
+}
+
+func (s *SystemRoleResponse) ToPB() *operationPb.SearchRoleResponse {
+	return &operationPb.SearchRoleResponse{
+		Page:  s.Page,
+		Total: s.Total,
+		List:  s.List,
+	}
+}

+ 100 - 8
model/system_user.go

@@ -1,20 +1,112 @@
 package model
 
 import (
+	"fmt"
 	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"strconv"
+	"strings"
+	"time"
 )
 
 type SystemUser struct {
-	Id         int64                   `json:"id,omitempty"`
-	Name       string                  `json:"name,omitempty"`
-	RoleId     int                     `json:"role_id,omitempty"`
-	RoleName   string                  `json:"role_name,omitempty"`
-	CreateUser string                  `json:"create_user,omitempty"`
-	IsShow     operationPb.IsShow_Kind `json:"is_show,omitempty"`
-	CreatedAt  int64                   `json:"created_at,omitempty"`
-	UpdatedAt  int64                   `json:"updated_at,omitempty"`
+	Id           int64                   `json:"id"`
+	Name         string                  `json:"name"`
+	EmployeeName string                  `json:"employee_name"`
+	Phone        string                  `json:"phone"`
+	Password     string                  `json:"password"`
+	RoleIds      string                  `json:"role_ids"`
+	CreateUser   string                  `json:"create_user"`
+	IsShow       operationPb.IsShow_Kind `json:"is_show"`
+	IsDelete     operationPb.IsShow_Kind `json:"is_delete"`
+	CreatedAt    int64                   `json:"created_at"`
+	UpdatedAt    int64                   `json:"updated_at"`
 }
 
 func (s *SystemUser) TableName() string {
 	return "system_user"
 }
+
+func (s *SystemUser) SystemUserFormat(userRoles []*SystemRole) *operationPb.UserAuth {
+	roles := make([]*operationPb.UserRole, len(userRoles))
+	for k, v := range userRoles {
+		roles[k] = &operationPb.UserRole{
+			Id:   v.Id,
+			Name: v.Name,
+		}
+	}
+
+	return &operationPb.UserAuth{
+		UserName:     s.Name,
+		Phone:        s.Phone,
+		EmployeeName: s.EmployeeName,
+		Roles:        roles,
+	}
+}
+
+func (s *SystemUser) SystemUserRoleFormat(req *operationPb.AddSystemUser) {
+	roleIds := ""
+	for _, role := range req.Roles {
+		roleIds += fmt.Sprintf("%d,", role.Id)
+	}
+	if roleIds != "" {
+		s.RoleIds = strings.TrimRight(roleIds, ",")
+	}
+}
+
+func (s *SystemUser) SystemUserRoleToSlice() []int {
+	roleIds := make([]int, 0)
+	if s.RoleIds != "" {
+		roleIdsStr := strings.Split(s.RoleIds, ",")
+		for _, v := range roleIdsStr {
+			roleIdsInt, _ := strconv.Atoi(v)
+			roleIds = append(roleIds, roleIdsInt)
+		}
+	}
+
+	return roleIds
+}
+
+type SystemUserSlice []*SystemUser
+
+func (s SystemUserSlice) ToPB() []*operationPb.AddSystemUser {
+	res := make([]*operationPb.AddSystemUser, len(s))
+	for i, v := range s {
+		res[i] = &operationPb.AddSystemUser{
+			Id:             v.Id,
+			Name:           v.Name,
+			Phone:          v.Phone,
+			EmployeeName:   v.EmployeeName,
+			CreateUser:     v.CreateUser,
+			IsShow:         v.IsShow,
+			CreatedAt:      v.CreatedAt,
+			CratedAtFormat: time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+		}
+	}
+	return res
+}
+
+func (s *SystemUser) ToPb() *operationPb.AddSystemUser {
+	return &operationPb.AddSystemUser{
+		Id:           s.Id,
+		Name:         s.Name,
+		Phone:        s.Phone,
+		CreateUser:   s.CreateUser,
+		EmployeeName: s.EmployeeName,
+		IsShow:       s.IsShow,
+		CreatedAt:    s.CreatedAt,
+	}
+}
+
+type SystemUserResponse struct {
+	Page  int32                        `json:"page"`
+	Total int32                        `json:"total"`
+	List  []*operationPb.AddSystemUser `json:"list"`
+}
+
+func (s *SystemUserResponse) ToPB() *operationPb.SearchUserResponse {
+	return &operationPb.SearchUserResponse{
+		Page:  s.Page,
+		Total: s.Total,
+		List:  s.List,
+	}
+}

+ 32 - 5
module/backend/interface.go

@@ -11,9 +11,7 @@ import (
 	"go.uber.org/dig"
 )
 
-var Module = di.Options(
-	di.Provide(NewStore),
-)
+var Module = di.Options(di.Provide(NewStore))
 
 type Hub struct {
 	dig.In
@@ -25,6 +23,7 @@ type StoreEntry struct {
 
 	Cfg *config.AppConfig
 	DB  *kptstore.DB
+	//SSO *sso.Cache
 	// AsynqClient asynqsvc.Client
 	// Cache *redis.Client
 }
@@ -35,10 +34,38 @@ func NewStore(store StoreEntry) KptService {
 
 type KptService interface {
 	Operation
+	SystemOperation
 }
 
 type Operation interface {
-	// CreatePastureList 牧场管理相关
-	CreatePastureList(ctx context.Context, req []*operationPb.AddPastureRequest) error
+
+	// CreatePasture 牧场管理相关
+	CreatePasture(ctx context.Context, req *operationPb.AddPastureRequest) error
 	SearchPastureList(ctx context.Context, req *operationPb.SearchPastureRequest) (*model.GroupPastureResponse, error)
 }
+
+type SystemOperation interface {
+
+	// Auth 系统用户相关
+	Auth(ctx context.Context, auth *operationPb.UserAuth) (*operationPb.SystemToken, error)
+	GetUserInfo(ctx context.Context, token string) (*operationPb.UserAuth, error)
+	CreateSystemUser(ctx context.Context, req *operationPb.AddSystemUser) error
+	SearchSystemUserList(ctx context.Context, req *operationPb.SearchUserRequest) (*model.SystemUserResponse, error)
+	EditSystemUser(ctx context.Context, req *operationPb.AddSystemUser) error
+	DeleteSystemUser(ctx context.Context, userId int64) error
+	IsShowSystemUser(ctx context.Context, req *operationPb.IsShowSystemUserRequest) error
+	GetSystemUserPermissions(ctx context.Context, token string) (*operationPb.SystemUserMenuPermissions, error)
+
+	// CreateSystemRole 系统角色相关
+	CreateSystemRole(ctx context.Context, req *operationPb.AddRoleRequest) error
+	EditSystemRole(ctx context.Context, req *operationPb.AddRoleRequest) error
+	DeleteSystemRole(ctx context.Context, roleId int64) error
+	SearchSystemRoleList(ctx context.Context, req *operationPb.SearchRoleRequest) (*model.SystemRoleResponse, error)
+
+	// CreateSystemMenu 系统菜单权限
+	CreateSystemMenu(ctx context.Context, req *operationPb.AddMenuRequest) error
+	EditSystemMenu(ctx context.Context, req *operationPb.AddMenuRequest) error
+	IsShowSystemMenu(ctx context.Context, req *operationPb.IsShowSystemMenuRequest) error
+	SearchSystemMenuList(ctx context.Context, req *operationPb.SearchMenuRequest) (*model.SystemMenuResponse, error)
+	DeleteSystemMenu(ctx context.Context, menuId int64) error
+}

+ 5 - 5
module/backend/service.go → module/backend/ops_service.go

@@ -8,10 +8,10 @@ import (
 	operationPb "kpt-tmr-group/proto/go/backend/operation"
 )
 
-// CreatePastureList 创建牧场
-func (s *StoreEntry) CreatePastureList(ctx context.Context, req []*operationPb.AddPastureRequest) error {
-	pastureListModel := model.NewGroupPastureList(req)
-	if err := s.DB.Create(pastureListModel).Error; err != nil {
+// CreatePasture 创建牧场
+func (s *StoreEntry) CreatePasture(ctx context.Context, req *operationPb.AddPastureRequest) error {
+	pastureList := model.NewGroupPasture(req)
+	if err := s.DB.Create(pastureList).Error; err != nil {
 		return xerr.WithStack(err)
 	}
 	return nil
@@ -43,6 +43,6 @@ func (s *StoreEntry) SearchPastureList(ctx context.Context, req *operationPb.Sea
 	return &model.GroupPastureResponse{
 		Page:  req.Page,
 		Total: int32(count),
-		Data:  model.GroupPastureSlice(groupPasture).ToPB(),
+		List:  model.GroupPastureSlice(groupPasture).ToPB(),
 	}, nil
 }

+ 211 - 0
module/backend/system_permissions.go

@@ -0,0 +1,211 @@
+package backend
+
+import (
+	"context"
+	"kpt-tmr-group/model"
+	"kpt-tmr-group/pkg/logger/zaplog"
+	"kpt-tmr-group/pkg/xerr"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"sync"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+type SystemAllPermissionsList struct {
+	PastureList []*model.SystemPasturePermissions
+	MenuList    []*model.SystemMenuPermissions
+	MobileList  []*model.SystemMobilePermissions
+}
+
+const (
+	Level1 = iota + 1
+	Level2
+	Level3
+)
+
+// SystemUserMenuPermissionsUnDuplicate 角色权限去重
+func (s *SystemAllPermissionsList) SystemUserMenuPermissionsUnDuplicate() {
+	newMenuList := make([]*model.SystemMenuPermissions, 0)
+	seen := make(map[int64]bool)
+	for _, menu := range s.MenuList {
+		if _, ok := seen[menu.MenuId]; !ok {
+			newMenuList = append(newMenuList, menu)
+		}
+	}
+	s.MenuList = newMenuList
+}
+
+func (s *StoreEntry) SystemPermissionsFormatPb(pastureList []*model.GroupPasture, mobileList []*model.SystemMobile, menuList []*model.SystemMenu) *operationPb.SystemUserMenuPermissions {
+	systemUserMenuPermissions := &operationPb.SystemUserMenuPermissions{
+		PastureList: make([]*operationPb.AddPastureRequest, 0),
+		MenuList:    make([]*operationPb.AddMenuRequest, 0),
+		MobileList:  make([]*operationPb.AddMobileRequest, 0),
+	}
+
+	wg := sync.WaitGroup{}
+	wg.Add(3)
+
+	go func() {
+		for _, v := range pastureList {
+			systemUserMenuPermissions.PastureList = append(systemUserMenuPermissions.PastureList,
+				&operationPb.AddPastureRequest{
+					Id:   v.Id,
+					Name: v.Name,
+				},
+			)
+		}
+		wg.Done()
+	}()
+
+	// TODO 后面优化成递归算法
+	go func() {
+
+		level := make(map[int32][]*operationPb.AddMenuRequest, 0)
+		for _, menu := range menuList {
+			if _, ok := level[menu.Level]; !ok {
+				level[menu.Level] = make([]*operationPb.AddMenuRequest, 0)
+			}
+			level[menu.Level] = append(level[menu.Level], &operationPb.AddMenuRequest{
+				Id:             menu.Id,
+				Name:           menu.Name,
+				ParentId:       menu.ParentId,
+				MenuType:       menu.MenuType,
+				Title:          menu.Title,
+				Path:           menu.Path,
+				IsShow:         menu.IsShow,
+				Component:      menu.Component,
+				Icon:           menu.Icon,
+				Sort:           menu.Sort,
+				Redirect:       menu.Redirect,
+				CreatedAt:      menu.CreatedAt,
+				CratedAtFormat: time.Unix(menu.CreatedAt, 0).Format(model.LayoutTime),
+				Level:          menu.Level,
+				Children:       make([]*operationPb.AddMenuRequest, 0),
+			})
+		}
+
+		for _, leve3Data := range level[Level3] {
+			for _, leve2Data := range level[Level2] {
+				if leve3Data.ParentId == leve2Data.Id {
+					if leve2Data.Children == nil {
+						leve2Data.Children = make([]*operationPb.AddMenuRequest, 0)
+					}
+					leve2Data.Children = append(leve2Data.Children, leve3Data)
+				}
+			}
+		}
+
+		for _, leve2Data := range level[Level2] {
+			for _, leve1Data := range level[Level1] {
+				if leve2Data.ParentId == leve1Data.Id {
+					if leve1Data.Children == nil {
+						leve1Data.Children = make([]*operationPb.AddMenuRequest, 0)
+					}
+					leve1Data.Children = append(leve1Data.Children, leve2Data)
+				}
+			}
+		}
+
+		systemUserMenuPermissions.MenuList = level[Level1]
+		wg.Done()
+	}()
+
+	go func() {
+		for _, v := range mobileList {
+			systemUserMenuPermissions.MobileList = append(systemUserMenuPermissions.MobileList,
+				&operationPb.AddMobileRequest{
+					Id:   v.Id,
+					Name: v.Name,
+				},
+			)
+		}
+		wg.Done()
+	}()
+
+	wg.Wait()
+	return systemUserMenuPermissions
+}
+
+// GetSystemAllPermissionsList 获取用户相关权限
+func (s *StoreEntry) GetSystemAllPermissionsList(ctx context.Context, roleId int64) *SystemAllPermissionsList {
+	systemAllPermissionsList := &SystemAllPermissionsList{
+		PastureList: make([]*model.SystemPasturePermissions, 0),
+		MenuList:    make([]*model.SystemMenuPermissions, 0),
+		MobileList:  make([]*model.SystemMobilePermissions, 0),
+	}
+
+	wg := sync.WaitGroup{}
+	wg.Add(3)
+	go func() {
+		pastureList := make([]*model.SystemPasturePermissions, 0)
+		if err := s.DB.Where("role_id = ?", roleId).Find(&pastureList).Error; err != nil {
+			zaplog.Error("SystemPasturePermissions", zap.Any("Err", err))
+		}
+		systemAllPermissionsList.PastureList = pastureList
+		wg.Done()
+	}()
+
+	go func() {
+		menuList := make([]*model.SystemMenuPermissions, 0)
+		if err := s.DB.Where("role_id = ?", roleId).Find(&menuList).Error; err != nil {
+			zaplog.Error("SystemMenuPermissions", zap.Any("Err", err))
+		}
+		systemAllPermissionsList.MenuList = menuList
+		wg.Done()
+	}()
+
+	// 获取供应商数据
+	go func() {
+		mobileList := make([]*model.SystemMobilePermissions, 0)
+		if err := s.DB.Where("role_id = ?", roleId).Find(&mobileList).Error; err != nil {
+			zaplog.Error("SystemMobilePermissions", zap.Any("Err", err))
+		}
+		systemAllPermissionsList.MobileList = mobileList
+		wg.Done()
+	}()
+	wg.Wait()
+	return systemAllPermissionsList
+}
+
+// GetPastureList 获取******
+func (s *StoreEntry) GetPastureList(ctx context.Context, req []*model.SystemPasturePermissions) ([]*model.GroupPasture, error) {
+	ids := make([]int64, 0)
+	for _, v := range req {
+		ids = append(ids, v.PastureId)
+	}
+
+	groupPastureList := make([]*model.GroupPasture, 0)
+	if err := s.DB.Find(&groupPastureList, ids).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	return groupPastureList, nil
+}
+
+// GetMenuList 获取******
+func (s *StoreEntry) GetMenuList(ctx context.Context, req []*model.SystemMenuPermissions) ([]*model.SystemMenu, error) {
+	ids := make([]int64, 0)
+	for _, v := range req {
+		ids = append(ids, v.MenuId)
+	}
+
+	menuList := make([]*model.SystemMenu, 0)
+	if err := s.DB.Find(&menuList, ids).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	return menuList, nil
+}
+
+// GetMobileList 获取******
+func (s *StoreEntry) GetMobileList(ctx context.Context, req []*model.SystemMobilePermissions) ([]*model.SystemMobile, error) {
+	ids := make([]int64, 0)
+	for _, v := range req {
+		ids = append(ids, v.MobileId)
+	}
+
+	mobileList := make([]*model.SystemMobile, 0)
+	if err := s.DB.Find(&mobileList, ids).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	return mobileList, nil
+}

+ 436 - 0
module/backend/system_service.go

@@ -0,0 +1,436 @@
+package backend
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"kpt-tmr-group/model"
+	"kpt-tmr-group/pkg/jwt"
+	"kpt-tmr-group/pkg/tool"
+	"kpt-tmr-group/pkg/xerr"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"strconv"
+	"strings"
+
+	"gorm.io/gorm"
+)
+
+// Auth 用户登录
+func (s *StoreEntry) Auth(ctx context.Context, auth *operationPb.UserAuth) (*operationPb.SystemToken, error) {
+	systemUser := &model.SystemUser{}
+
+	if err := s.DB.Where("name = ?", auth.UserName).Find(systemUser).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	if systemUser.Password != auth.Password {
+		return nil, xerr.Customf("密码错误,来自用户:%s", auth.UserName)
+	}
+
+	token, err := jwt.GenerateToken(systemUser.Name, systemUser.Password)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	if token == "" {
+		return nil, xerr.Custom("获取token错误")
+	}
+
+	return &operationPb.SystemToken{
+		Token: token,
+	}, nil
+}
+
+// GetUserInfo 获取用户信息
+func (s *StoreEntry) GetUserInfo(ctx context.Context, token string) (*operationPb.UserAuth, error) {
+	systemUser := &model.SystemUser{}
+	claims, err := jwt.ParseToken(token)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	if claims.Username == "" {
+		return nil, xerr.Custom("token解析失败")
+	}
+
+	if err = s.DB.Where("name = ?", claims.Username).First(systemUser).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	systemRole := make([]*model.SystemRole, 0)
+
+	roleIdStr := strings.Split(systemUser.RoleIds, ",")
+	if len(roleIdStr) > 0 {
+		roleIds := make([]int, 0)
+		for _, v := range roleIdStr {
+			roleId, _ := strconv.Atoi(v)
+			roleIds = append(roleIds, roleId)
+		}
+
+		if err = s.DB.Find(&systemRole, roleIds).Error; err != nil {
+			return nil, xerr.WithStack(err)
+		}
+	}
+
+	return systemUser.SystemUserFormat(systemRole), nil
+}
+
+// CreateSystemUser 创建系统用户
+func (s *StoreEntry) CreateSystemUser(ctx context.Context, req *operationPb.AddSystemUser) error {
+
+	systemUsers := &model.SystemUser{
+		Name:         req.Name,
+		EmployeeName: req.EmployeeName,
+		Phone:        req.Phone,
+		Password:     tool.Md5String(model.InitManagerPassword),
+		CreateUser:   req.CreateUser,
+		IsShow:       req.IsShow,
+	}
+	systemUsers.SystemUserRoleFormat(req)
+
+	if err := s.DB.Create(systemUsers).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// SearchSystemUserList 查询系统用户
+func (s *StoreEntry) SearchSystemUserList(ctx context.Context, req *operationPb.SearchUserRequest) (*model.SystemUserResponse, error) {
+	systemUser := make([]*model.SystemUser, 0)
+	var count int64 = 0
+
+	pref := s.DB.Model(new(model.SystemUser)).Where("is_delete = ?", operationPb.IsShow_OK)
+	if req.Name != "" {
+		pref.Where("name like ?", fmt.Sprintf("%s%s%s", "%", req.Name, "%"))
+	}
+	if req.EmployeeName != "" {
+		pref.Where("employee_name like ?", fmt.Sprintf("%s%s%s", "%", req.EmployeeName, "%"))
+	}
+
+	if req.IsShow > 0 {
+		pref.Where("is_show = ?", req.IsShow)
+	}
+
+	if err := pref.Order("id desc").Count(&count).Limit(int(req.Pagination.PageSize)).Offset(int(req.Pagination.PageOffset)).
+		Find(&systemUser).Debug().Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &model.SystemUserResponse{
+		Page:  req.Pagination.Page,
+		Total: int32(count),
+		List:  model.SystemUserSlice(systemUser).ToPB(),
+	}, nil
+}
+
+// EditSystemUser 编辑用户
+func (s *StoreEntry) EditSystemUser(ctx context.Context, req *operationPb.AddSystemUser) error {
+	systemUser := &model.SystemUser{Id: req.Id}
+	if err := s.DB.First(systemUser).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在!")
+		}
+		return xerr.WithStack(err)
+	}
+
+	updateData := &model.SystemUser{
+		Name:         req.Name,
+		EmployeeName: req.EmployeeName,
+		Phone:        req.Phone,
+		CreateUser:   req.CreateUser,
+	}
+	updateData.SystemUserRoleFormat(req)
+
+	if err := s.DB.Model(new(model.SystemUser)).Omit("is_show", "password", "is_delete").
+		Where("id = ?", systemUser.Id).
+		Updates(updateData).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+
+	return nil
+}
+
+// DeleteSystemUser 删除系统用户
+func (s *StoreEntry) DeleteSystemUser(ctx context.Context, userId int64) error {
+	systemUser := &model.SystemUser{
+		Id: userId,
+	}
+	if err := s.DB.First(systemUser).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该用户不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(systemUser).Update("is_delete", operationPb.IsShow_NO).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// IsShowSystemUser 用户是否启用
+func (s *StoreEntry) IsShowSystemUser(ctx context.Context, req *operationPb.IsShowSystemUserRequest) error {
+	systemUser := &model.SystemUser{
+		Id: req.UserId,
+	}
+	if err := s.DB.First(systemUser).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该用户不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(systemUser).Update("is_show", req.IsShow).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// GetSystemUserPermissions 返回系统用户相关菜单权限
+func (s *StoreEntry) GetSystemUserPermissions(ctx context.Context, token string) (*operationPb.SystemUserMenuPermissions, error) {
+	// 解析token
+	claims, err := jwt.ParseToken(token)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	if err = claims.Valid(); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	// 根据用户token获取用户数据
+	systemUser := &model.SystemUser{Name: claims.Username}
+	if err = s.DB.Where("name = ?", claims.Username).First(systemUser).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, xerr.Custom("该用户数据不存在")
+		}
+		return nil, xerr.WithStack(err)
+	}
+	roleIds := systemUser.SystemUserRoleToSlice()
+
+	// 获取用户角色数据
+	systemRoles := make([]*model.SystemRole, 0)
+	if err = s.DB.Find(&systemRoles, roleIds).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	systemAllPermissionsList := &SystemAllPermissionsList{
+		PastureList: make([]*model.SystemPasturePermissions, 0),
+		MenuList:    make([]*model.SystemMenuPermissions, 0),
+		MobileList:  make([]*model.SystemMobilePermissions, 0),
+	}
+
+	for _, role := range systemRoles {
+		item := s.GetSystemAllPermissionsList(ctx, role.Id)
+		systemAllPermissionsList.PastureList = append(systemAllPermissionsList.PastureList, item.PastureList...)
+		systemAllPermissionsList.MenuList = append(systemAllPermissionsList.MenuList, item.MenuList...)
+		systemAllPermissionsList.MobileList = append(systemAllPermissionsList.MobileList, item.MobileList...)
+	}
+
+	systemAllPermissionsList.SystemUserMenuPermissionsUnDuplicate()
+
+	groupPastureList, err := s.GetPastureList(ctx, systemAllPermissionsList.PastureList)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	menuList, err := s.GetMenuList(ctx, systemAllPermissionsList.MenuList)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	mobileList, err := s.GetMobileList(ctx, systemAllPermissionsList.MobileList)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return s.SystemPermissionsFormatPb(groupPastureList, mobileList, menuList), nil
+}
+
+// CreateSystemRole 添加角色
+func (s *StoreEntry) CreateSystemRole(ctx context.Context, req *operationPb.AddRoleRequest) error {
+	if err := s.DB.Transaction(func(tx *gorm.DB) error {
+		// 创建角色数据
+		role := model.NewSystemRole(req)
+		if err := tx.Create(role).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+		// 创建角色菜单权限数据
+		if len(req.MenuId) > 0 {
+			menuPermissions := model.NewSystemMenuPermissions(role.Id, req.MenuId)
+			if err := tx.Create(menuPermissions).Error; err != nil {
+				return xerr.WithStack(err)
+			}
+		}
+		// 创建角色移动端权限数据
+		if len(req.MobileId) > 0 {
+			mobilePermissions := model.NewSystemMobilePermissions(role.Id, req.MobileId)
+			if err := tx.Create(mobilePermissions).Error; err != nil {
+				return xerr.WithStack(err)
+			}
+		}
+
+		// 创建角色牧场端权限数据
+		if len(req.PastureId) > 0 {
+			pasturePermissions := model.NewSystemPasturePermissions(role.Id, req.PastureId)
+			if err := tx.Create(pasturePermissions).Error; err != nil {
+				return xerr.WithStack(err)
+			}
+		}
+		return nil
+	}); err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// EditSystemRole 编辑角色
+func (s *StoreEntry) EditSystemRole(ctx context.Context, req *operationPb.AddRoleRequest) error {
+	role := &model.SystemRole{Id: req.Id}
+	if err := s.DB.First(role).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	updateSystemRole := model.NewSystemRole(req)
+	if err := s.DB.Omit("is_show").
+		Where("id = ?", role.Id).
+		Updates(updateSystemRole).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+
+	return nil
+}
+
+// DeleteSystemRole 删除系统角色
+func (s *StoreEntry) DeleteSystemRole(ctx context.Context, roleId int64) error {
+	systemRole := &model.SystemRole{
+		Id: roleId,
+	}
+	if err := s.DB.First(systemRole).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(systemRole).Update("is_show", operationPb.IsShow_NO).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// SearchSystemRoleList 查询系统角色
+func (s *StoreEntry) SearchSystemRoleList(ctx context.Context, req *operationPb.SearchRoleRequest) (*model.SystemRoleResponse, error) {
+	systemRole := make([]*model.SystemRole, 0)
+	var count int64 = 0
+
+	pref := s.DB.Model(new(model.SystemRole)).Where("is_show = ?", operationPb.IsShow_OK)
+	if req.Name != "" {
+		pref.Where("name like ?", fmt.Sprintf("%s%s%s", "%", req.Name, "%"))
+	}
+
+	if err := pref.Order("id desc").Count(&count).Limit(int(req.Pagination.PageSize)).Offset(int(req.Pagination.PageOffset)).
+		Find(&systemRole).Debug().Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &model.SystemRoleResponse{
+		Page:  req.Pagination.Page,
+		Total: int32(count),
+		List:  model.SystemRoleSlice(systemRole).ToPB(),
+	}, nil
+}
+
+// CreateSystemMenu 添加系统菜单权限
+func (s *StoreEntry) CreateSystemMenu(ctx context.Context, req *operationPb.AddMenuRequest) error {
+	systemMenu := model.NewSystemMenu(req)
+	if err := s.DB.Create(systemMenu).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// EditSystemMenu 编辑系统菜单权限
+func (s *StoreEntry) EditSystemMenu(ctx context.Context, req *operationPb.AddMenuRequest) error {
+	systemMenu := &model.SystemMenu{Id: req.Id}
+	if err := s.DB.First(systemMenu).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在!")
+		}
+		return xerr.WithStack(err)
+	}
+
+	updateData := &model.SystemMenu{
+		Name:      req.Name,
+		MenuType:  req.MenuType,
+		Title:     req.Title,
+		Path:      req.Path,
+		Component: req.Component,
+		Icon:      req.Icon,
+		Sort:      req.Sort,
+		Redirect:  req.Redirect,
+		ParentId:  req.ParentId,
+	}
+
+	if err := s.DB.Model(new(model.SystemMenu)).Omit("is_show").
+		Where("id = ?", systemMenu.Id).
+		Updates(updateData).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+
+	return nil
+}
+
+// IsShowSystemMenu 菜单是否启用
+func (s *StoreEntry) IsShowSystemMenu(ctx context.Context, req *operationPb.IsShowSystemMenuRequest) error {
+	systemMenu := &model.SystemMenu{Id: req.MenuId}
+	if err := s.DB.First(systemMenu).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(systemMenu).Update("is_show", req.IsShow).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// SearchSystemMenuList 菜单列表查询
+func (s *StoreEntry) SearchSystemMenuList(ctx context.Context, req *operationPb.SearchMenuRequest) (*model.SystemMenuResponse, error) {
+	systemMenu := make([]*model.SystemMenu, 0)
+	var count int64 = 0
+
+	pref := s.DB.Model(new(model.SystemMenu)).Where("is_delete = ?", operationPb.IsShow_OK)
+	if req.Name != "" {
+		pref.Where("name like ?", fmt.Sprintf("%s%s%s", "%", req.Name, "%"))
+	}
+
+	if err := pref.Order("id desc").Count(&count).Limit(int(req.Pagination.PageSize)).Offset(int(req.Pagination.PageOffset)).
+		Find(&systemMenu).Debug().Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &model.SystemMenuResponse{
+		Page:  req.Pagination.Page,
+		Total: int32(count),
+		List:  model.SystemMenuSlice(systemMenu).ToPB(),
+	}, nil
+}
+
+// DeleteSystemMenu 删除系统菜单
+func (s *StoreEntry) DeleteSystemMenu(ctx context.Context, menuId int64) error {
+	systemMenu := &model.SystemMenu{Id: menuId}
+	if err := s.DB.First(systemMenu).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(systemMenu).Update("is_delete", operationPb.IsShow_NO).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}

+ 6 - 6
pkg/apiok/common.go

@@ -1,16 +1,16 @@
 package apiok
 
 type OkResponse struct {
-	Code   int         `json:"code"`
-	Msg    string      `json:"msg"`
-	Result interface{} `json:"result"`
+	Code int         `json:"code"`
+	Msg  string      `json:"msg"`
+	Data interface{} `json:"data"`
 }
 
 func CommonResponse(data interface{}) *OkResponse {
 	return &OkResponse{
-		Code:   200,
-		Msg:    "ok",
-		Result: data,
+		Code: 200,
+		Msg:  "ok",
+		Data: data,
 	}
 }
 

+ 64 - 0
pkg/jwt/jwt.go

@@ -0,0 +1,64 @@
+package jwt
+
+import (
+	"fmt"
+	"kpt-tmr-group/config"
+	"kpt-tmr-group/pkg/tool"
+	"reflect"
+	"time"
+
+	"github.com/dgrijalva/jwt-go"
+)
+
+var jwtSecret = []byte(config.Options().JwtSecret)
+
+type Claims struct {
+	Username string `json:"username"`
+	Password string `json:"password"`
+	jwt.StandardClaims
+}
+
+func GenerateToken(username, password string) (string, error) {
+	nowTime := time.Now()
+	expireTime := nowTime.Add(2 * time.Hour)
+
+	claims := Claims{
+		username,
+		tool.Md5String(password),
+		jwt.StandardClaims{
+			ExpiresAt: expireTime.Unix(),
+			Issuer:    "https://github.com/kptyun/go-admin/",
+		},
+	}
+
+	tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+	return tokenClaims.SignedString(jwtSecret)
+}
+
+func ParseToken(token string) (*Claims, error) {
+	tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
+		return jwtSecret, nil
+	})
+
+	if tokenClaims != nil {
+		if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
+			return claims, nil
+		}
+	}
+
+	return nil, err
+}
+
+func GetIdFromClaims(key string, claims jwt.Claims) string {
+	v := reflect.ValueOf(claims)
+	if v.Kind() == reflect.Map {
+		for _, k := range v.MapKeys() {
+			value := v.MapIndex(k)
+
+			if fmt.Sprintf("%s", k.Interface()) == key {
+				return fmt.Sprintf("%v", value.Interface())
+			}
+		}
+	}
+	return ""
+}

+ 1 - 176
pkg/tool/tool.go

@@ -4,7 +4,6 @@ import (
 	"crypto/md5"
 	"encoding/hex"
 	"fmt"
-	"reflect"
 	"strconv"
 	"strings"
 	"time"
@@ -49,97 +48,6 @@ func GetLocalTime(timeStr string) time.Time {
 	return execTime
 }
 
-// GetTargetByValueForTag 利用反射根据cond获取对应字段数据,主要tag必须为gorm
-// e.g person{Name:"ping",Age:12}
-// e.g GetTargetByTag(person,age) ==> 12
-func GetTargetByValueForTag(target interface{}, cond string) interface{} {
-	tf := reflect.Value{}
-	// 判断是不是指针类型
-	if reflect.TypeOf(target).Kind() == reflect.Ptr {
-		if reflect.ValueOf(target).IsNil() { // 空指针
-			return ""
-		}
-		tf = reflect.Indirect(reflect.ValueOf(target))
-	} else {
-		tf = reflect.ValueOf(target)
-	}
-
-	typ := tf.Type()
-	var condValue interface{}
-	for k := 0; k < typ.NumField(); k++ {
-		key := typ.Field(k).Tag.Get("gorm")
-		if key != cond {
-			continue
-		}
-		field := typ.Field(k).Name
-		switch tf.FieldByName(field).Kind() {
-		case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-			condValue = tf.FieldByName(field).Int()
-		case reflect.String:
-			condValue = tf.FieldByName(field).String()
-		}
-	}
-	return condValue
-}
-
-// GetTargetByTypeForTag 利用反射根据cond获取对应的字段类型默认值
-// e.g person{Name:"ping",Age:12,EventStatus: fieldPb.EventStatus_ADMISSION}
-// e.g GetTargetByTypeForTag(person,age) ==> person{Name:"ping",Age:0,EventStatus: fieldPb.EventStatus_ADMISSION}
-// e.g GetTargetByTypeForTag(person,name) ==> person{Name:"",Age:12,EventStatus: fieldPb.EventStatus_ADMISSION}
-// e.g GetTargetByTypeForTag(person,event_status) ==> person{Name:"ping",Age:12,EventStatus: fieldPb.EventStatus_INVALID}
-func GetTargetByTypeForTag(target interface{}, cond string) {
-	tf := reflect.TypeOf(target)
-	// 判断是不是指针类型
-	if tf.Kind() == reflect.Ptr {
-		tf = tf.Elem()
-	}
-	tv := reflect.ValueOf(target)
-	if tv.Kind() == reflect.Ptr {
-		tv = tv.Elem()
-	}
-	for i := 0; i < tf.NumField(); i++ {
-		key := tf.Field(i).Tag.Get("gorm")
-		if key != cond {
-			continue
-		}
-		fieldName := tf.Field(i).Name
-		tfKind := tf.Field(i).Type
-
-		if tfKind.Kind() == reflect.String {
-			tv.FieldByName(fieldName).SetString("")
-		}
-
-		if tfKind.Kind() == reflect.Int || tfKind.Kind() == reflect.Int8 || tfKind.Kind() == reflect.Int16 ||
-			tfKind.Kind() == reflect.Int32 || tfKind.Kind() == reflect.Int64 {
-			tv.FieldByName(fieldName).SetInt(0)
-		}
-
-		if tfKind.Kind() == reflect.Uint || tfKind.Kind() == reflect.Uint8 || tfKind.Kind() == reflect.Uint16 ||
-			tfKind.Kind() == reflect.Uint32 || tfKind.Kind() == reflect.Uint64 {
-			tv.FieldByName(fieldName).SetUint(0)
-		}
-		if tfKind.Kind() == reflect.Float32 || tfKind.Kind() == reflect.Float64 {
-			tv.FieldByName(fieldName).SetFloat(0)
-		}
-	}
-}
-
-// MonthLastDay 获取当月最后一天
-//eg 2023-01 => 2023-01-31
-//eg 2023-02 => 2023-02-28
-func MonthLastDay(yearMonth string) string {
-	item := strings.Split(yearMonth, "-")
-	if len(item) < 2 {
-		return ""
-	}
-
-	year, _ := strconv.Atoi(item[0])
-	loc, _ := time.LoadLocation("Local")
-	theTime, _ := time.ParseInLocation(Layout, fmt.Sprintf("%s-01 00:00:00", yearMonth), loc)
-
-	return time.Date(year, theTime.Month()+1, 0, 0, 0, 0, 0, time.Local).Format("2006-01-02")
-}
-
 // TimeParseLocalUnix 获取当天零点的时间戳
 // eg 2023-02-22 => 1676995200
 func TimeParseLocalUnix(DayTime string) int64 {
@@ -153,92 +61,9 @@ func TimeParseLocalUnix(DayTime string) int64 {
 	return theTime.Unix()
 }
 
-// ColumNameValueJoinSqlString 获取columName对应的值拼接成sql语句
-// eg columName = "a,b,c"  ==> ('1','2','3')
-func ColumNameValueJoinSqlString(columName string, target interface{}) string {
-	columNames := strings.Split(columName, ",")
-	insertValue := "("
-	if target == nil {
-		return insertValue
-	}
-
-	for _, c := range columNames {
-		val := reflect.Value{}
-		if reflect.TypeOf(target).Kind() == reflect.Ptr {
-			val = reflect.Indirect(reflect.ValueOf(target))
-		} else {
-			val = reflect.ValueOf(target)
-		}
-
-		typ := val.Type()
-		strValue := ""
-		for k := 0; k < typ.NumField(); k++ {
-			key := typ.Field(k).Tag.Get("json")
-			key = strings.ReplaceAll(key, ",omitempty", "")
-			if key != c {
-				continue
-			}
-			field := typ.Field(k).Name
-			switch val.FieldByName(field).Kind() {
-			case reflect.String:
-				strValue = fmt.Sprintf("'%s'", val.FieldByName(field).String())
-			case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-				strValue = fmt.Sprintf("%d", val.FieldByName(field).Int())
-			case reflect.Float64, reflect.Float32:
-				strValue = fmt.Sprintf("%f", val.FieldByName(field).Float())
-			}
-		}
-
-		insertValue += fmt.Sprintf("%s,", strValue)
-	}
-
-	return fmt.Sprintf("%s)", strings.TrimRight(insertValue, ","))
-}
-
-// ColumNameValue 获取columName 对应的值
-// eg a => 1
-func ColumNameValue(columName string, target interface{}) string {
-	strValue := ""
-	if target == nil {
-		return strValue
-	}
-
-	val := reflect.Value{}
-	if reflect.TypeOf(target).Kind() == reflect.Ptr {
-		val = reflect.Indirect(reflect.ValueOf(target))
-	} else {
-		val = reflect.ValueOf(target)
-	}
-
-	typ := val.Type()
-	for k := 0; k < typ.NumField(); k++ {
-		key := typ.Field(k).Tag.Get("json")
-		key = strings.ReplaceAll(key, ",omitempty", "")
-		if key != columName {
-			continue
-		}
-		field := typ.Field(k).Name
-		switch val.FieldByName(field).Kind() {
-		case reflect.String:
-			strValue = fmt.Sprintf("'%s'", val.FieldByName(field).String())
-		case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-			strValue = fmt.Sprintf("%d", val.FieldByName(field).Int())
-		case reflect.Float64, reflect.Float32:
-			strValue = fmt.Sprintf("%f", val.FieldByName(field).Float())
-		}
-	}
-
-	return strValue
-}
-
-// UnixTimeString unix时间戳转化成string  eg 1673939363 => 2023-01-17
-func UnixTimeString(unixTime int64) string {
-	return time.Unix(unixTime, 0).Format(DateTime)
-}
-
 func Md5String(input string) string {
 	s := md5.New()
-	digest := strings.ReplaceAll(string(input), "\n", "")
+	digest := strings.ReplaceAll(input, "\n", "")
 	s.Write([]byte(digest))
 	return hex.EncodeToString(s.Sum(nil))
 }

+ 658 - 0
pkg/valid/README.md

@@ -0,0 +1,658 @@
+# valid 校验 library
+
+### Validating a Simple Value
+
+For a simple value, such as a string or an integer, you may use `valid.Validate()` to validate it. For example, 
+
+```go
+package main
+
+import (
+	"fmt"
+
+	"kpt-tmr-group/pkg/valid"
+	"kpt-tmr-group/pkg/valid/is"
+)
+
+func main() {
+	data := "example"
+	err := valid.Validate(data,
+		valid.Required,       // not empty
+		valid.Length(5, 100), // length between 5 and 100
+		is.URL,                    // is a valid URL
+	)
+	fmt.Println(err)
+	// Output:
+	// must be a valid URL
+}
+```
+
+The method `valid.Validate()` will run through the rules in the order that they are listed. If a rule fails
+the validation, the method will return the corresponding error and skip the rest of the rules. The method will
+return nil if the value passes all validation rules.
+
+
+### Validating a Struct
+
+For a struct value, you usually want to check if its fields are valid. For example, in a RESTful application, you
+may unmarshal the request payload into a struct and then validate the struct fields. If one or multiple fields
+are invalid, you may want to get an error describing which fields are invalid. You can use `valid.ValidateStruct()`
+to achieve this purpose. A single struct can have rules for multiple fields, and a field can be associated with multiple 
+rules. For example,
+
+```go
+type Address struct {
+	Street string
+	City   string
+	State  string
+	Zip    string
+}
+
+func (a Address) Validate() error {
+	return valid.ValidateStruct(&a,
+		// Street cannot be empty, and the length must between 5 and 50
+		valid.Field(&a.Street, valid.Required, valid.Length(5, 50)),
+		// City cannot be empty, and the length must between 5 and 50
+		valid.Field(&a.City, valid.Required, valid.Length(5, 50)),
+		// State cannot be empty, and must be a string consisting of two letters in upper case
+		valid.Field(&a.State, valid.Required, valid.Match(regexp.MustCompile("^[A-Z]{2}$"))),
+		// State cannot be empty, and must be a string consisting of five digits
+		valid.Field(&a.Zip, valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+	)
+}
+
+a := Address{
+    Street: "123",
+    City:   "Unknown",
+    State:  "Virginia",
+    Zip:    "12345",
+}
+
+err := a.Validate()
+fmt.Println(err)
+// Output:
+// Street: the length must be between 5 and 50; State: must be in a valid format.
+```
+
+Note that when calling `valid.ValidateStruct` to validate a struct, you should pass to the method a pointer 
+to the struct instead of the struct itself. Similarly, when calling `valid.Field` to specify the rules
+for a struct field, you should use a pointer to the struct field. 
+
+When the struct validation is performed, the fields are validated in the order they are specified in `ValidateStruct`. 
+And when each field is validated, its rules are also evaluated in the order they are associated with the field.
+If a rule fails, an error is recorded for that field, and the validation will continue with the next field.
+
+
+### Validating a Map
+
+Sometimes you might need to work with dynamic data stored in maps rather than a typed model. You can use `valid.Map()`
+in this situation. A single map can have rules for multiple keys, and a key can be associated with multiple 
+rules. For example,
+
+```go
+c := map[string]interface{}{
+	"Name":  "Qiang Xue",
+	"Email": "q",
+	"Address": map[string]interface{}{
+		"Street": "123",
+		"City":   "Unknown",
+		"State":  "Virginia",
+		"Zip":    "12345",
+	},
+}
+
+err := valid.Validate(c,
+	valid.Map(
+		// Name cannot be empty, and the length must be between 5 and 20.
+		valid.Key("Name", valid.Required, valid.Length(5, 20)),
+		// Email cannot be empty and should be in a valid email format.
+		valid.Key("Email", valid.Required, is.Email),
+		// Validate Address using its own validation rules
+		valid.Key("Address", valid.Map(
+			// Street cannot be empty, and the length must between 5 and 50
+			valid.Key("Street", valid.Required, valid.Length(5, 50)),
+			// City cannot be empty, and the length must between 5 and 50
+			valid.Key("City", valid.Required, valid.Length(5, 50)),
+			// State cannot be empty, and must be a string consisting of two letters in upper case
+			valid.Key("State", valid.Required, valid.Match(regexp.MustCompile("^[A-Z]{2}$"))),
+			// State cannot be empty, and must be a string consisting of five digits
+			valid.Key("Zip", valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+		)),
+	),
+)
+fmt.Println(err)
+// Output:
+// Address: (State: must be in a valid format; Street: the length must be between 5 and 50.); Email: must be a valid email address.
+```
+
+When the map validation is performed, the keys are validated in the order they are specified in `Map`. 
+And when each key is validated, its rules are also evaluated in the order they are associated with the key.
+If a rule fails, an error is recorded for that key, and the validation will continue with the next key.
+
+
+### Validation Errors
+
+The `valid.ValidateStruct` method returns validation errors found in struct fields in terms of `valid.Errors` 
+which is a map of fields and their corresponding errors. Nil is returned if validation passes.
+
+By default, `valid.Errors` uses the struct tags named `json` to determine what names should be used to 
+represent the invalid fields. The type also implements the `json.Marshaler` interface so that it can be marshaled 
+into a proper JSON object. For example,
+
+```go
+type Address struct {
+	Street string `json:"street"`
+	City   string `json:"city"`
+	State  string `json:"state"`
+	Zip    string `json:"zip"`
+}
+
+// ...perform validation here...
+
+err := a.Validate()
+b, _ := json.Marshal(err)
+fmt.Println(string(b))
+// Output:
+// {"street":"the length must be between 5 and 50","state":"must be in a valid format"}
+```
+
+You may modify `valid.ErrorTag` to use a different struct tag name.
+
+If you do not like the magic that `ValidateStruct` determines error keys based on struct field names or corresponding
+tag values, you may use the following alternative approach:
+
+```go
+c := Customer{
+	Name:  "Qiang Xue",
+	Email: "q",
+	Address: Address{
+		State:  "Virginia",
+	},
+}
+
+err := valid.Errors{
+	"name": valid.Validate(c.Name, valid.Required, valid.Length(5, 20)),
+	"email": valid.Validate(c.Name, valid.Required, is.Email),
+	"zip": valid.Validate(c.Address.Zip, valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+}.Filter()
+fmt.Println(err)
+// Output:
+// email: must be a valid email address; zip: cannot be blank.
+```
+
+In the above example, we build a `valid.Errors` by a list of names and the corresponding validation results. 
+At the end we call `Errors.Filter()` to remove from `Errors` all nils which correspond to those successful validation 
+results. The method will return nil if `Errors` is empty.
+
+The above approach is very flexible as it allows you to freely build up your validation error structure. You can use
+it to validate both struct and non-struct values. Compared to using `ValidateStruct` to validate a struct, 
+it has the drawback that you have to redundantly specify the error keys while `ValidateStruct` can automatically 
+find them out.
+
+
+### Internal Errors
+
+Internal errors are different from validation errors in that internal errors are caused by malfunctioning code (e.g.
+a validator making a remote call to validate some data when the remote service is down) rather
+than the data being validated. When an internal error happens during data validation, you may allow the user to resubmit
+the same data to perform validation again, hoping the program resumes functioning. On the other hand, if data validation
+fails due to data error, the user should generally not resubmit the same data again.
+
+To differentiate internal errors from validation errors, when an internal error occurs in a validator, wrap it
+into `valid.InternalError` by calling `valid.NewInternalError()`. The user of the validator can then check
+if a returned error is an internal error or not. For example,
+
+```go
+if err := a.Validate(); err != nil {
+	if e, ok := err.(valid.InternalError); ok {
+		// an internal error happened
+		fmt.Println(e.InternalError())
+	}
+}
+```
+
+
+## Validatable Types
+
+A type is validatable if it implements the `valid.Validatable` interface. 
+
+When `valid.Validate` is used to validate a validatable value, if it does not find any error with the 
+given validation rules, it will further call the value's `Validate()` method. 
+
+Similarly, when `valid.ValidateStruct` is validating a struct field whose type is validatable, it will call 
+the field's `Validate` method after it passes the listed rules.
+
+> Note: When implementing `valid.Validatable`, do not call `valid.Validate()` to validate the value in its
+> original type because this will cause infinite loops. For example, if you define a new type `MyString` as `string`
+> and implement `valid.Validatable` for `MyString`, within the `Validate()` function you should cast the value 
+> to `string` first before calling `valid.Validate()` to validate it.
+
+In the following example, the `Address` field of `Customer` is validatable because `Address` implements 
+`valid.Validatable`. Therefore, when validating a `Customer` struct with `valid.ValidateStruct`,
+validation will "dive" into the `Address` field.
+
+```go
+type Customer struct {
+	Name    string
+	Gender  string
+	Email   string
+	Address Address
+}
+
+func (c Customer) Validate() error {
+	return valid.ValidateStruct(&c,
+		// Name cannot be empty, and the length must be between 5 and 20.
+		valid.Field(&c.Name, valid.Required, valid.Length(5, 20)),
+		// Gender is optional, and should be either "Female" or "Male".
+		valid.Field(&c.Gender, valid.In("Female", "Male")),
+		// Email cannot be empty and should be in a valid email format.
+		valid.Field(&c.Email, valid.Required, is.Email),
+		// Validate Address using its own validation rules
+		valid.Field(&c.Address),
+	)
+}
+
+c := Customer{
+	Name:  "Qiang Xue",
+	Email: "q",
+	Address: Address{
+		Street: "123 Main Street",
+		City:   "Unknown",
+		State:  "Virginia",
+		Zip:    "12345",
+	},
+}
+
+err := c.Validate()
+fmt.Println(err)
+// Output:
+// Address: (State: must be in a valid format.); Email: must be a valid email address.
+```
+
+Sometimes, you may want to skip the invocation of a type's `Validate` method. To do so, simply associate
+a `valid.Skip` rule with the value being validated.
+
+### Maps/Slices/Arrays of Validatables
+
+When validating an iterable (map, slice, or array), whose element type implements the `valid.Validatable` interface,
+the `valid.Validate` method will call the `Validate` method of every non-nil element.
+The validation errors of the elements will be returned as `valid.Errors` which maps the keys of the
+invalid elements to their corresponding validation errors. For example,
+
+```go
+addresses := []Address{
+	Address{State: "MD", Zip: "12345"},
+	Address{Street: "123 Main St", City: "Vienna", State: "VA", Zip: "12345"},
+	Address{City: "Unknown", State: "NC", Zip: "123"},
+}
+err := valid.Validate(addresses)
+fmt.Println(err)
+// Output:
+// 0: (City: cannot be blank; Street: cannot be blank.); 2: (Street: cannot be blank; Zip: must be in a valid format.).
+```
+
+When using `valid.ValidateStruct` to validate a struct, the above validation procedure also applies to those struct 
+fields which are map/slices/arrays of validatables. 
+
+#### Each
+
+The `Each` validation rule allows you to apply a set of rules to each element of an array, slice, or map.
+
+```go
+type Customer struct {
+    Name      string
+    Emails    []string
+}
+
+func (c Customer) Validate() error {
+    return valid.ValidateStruct(&c,
+        // Name cannot be empty, and the length must be between 5 and 20.
+		valid.Field(&c.Name, valid.Required, valid.Length(5, 20)),
+		// Emails are optional, but if given must be valid.
+		valid.Field(&c.Emails, valid.Each(is.Email)),
+    )
+}
+
+c := Customer{
+    Name:   "Qiang Xue",
+    Emails: []Email{
+        "valid@example.com",
+        "invalid",
+    },
+}
+
+err := c.Validate()
+fmt.Println(err)
+// Output:
+// Emails: (1: must be a valid email address.).
+```
+
+### Pointers
+
+When a value being validated is a pointer, most validation rules will validate the actual value pointed to by the pointer.
+If the pointer is nil, these rules will skip the valid.
+
+An exception is the `valid.Required` and `valid.NotNil` rules. When a pointer is nil, they
+will report a validation error.
+
+
+### Types Implementing `sql.Valuer`
+
+If a data type implements the `sql.Valuer` interface (e.g. `sql.NullString`), the built-in validation rules will handle
+it properly. In particular, when a rule is validating such data, it will call the `Value()` method and validate
+the returned value instead.
+
+
+### Required vs. Not Nil
+
+When validating input values, there are two different scenarios about checking if input values are provided or not.
+
+In the first scenario, an input value is considered missing if it is not entered or it is entered as a zero value
+(e.g. an empty string, a zero integer). You can use the `valid.Required` rule in this case.
+
+In the second scenario, an input value is considered missing only if it is not entered. A pointer field is usually
+used in this case so that you can detect if a value is entered or not by checking if the pointer is nil or not.
+You can use the `valid.NotNil` rule to ensure a value is entered (even if it is a zero value).
+
+
+### Embedded Structs
+
+The `valid.ValidateStruct` method will properly validate a struct that contains embedded structs. In particular,
+the fields of an embedded struct are treated as if they belong directly to the containing struct. For example,
+
+```go
+type Employee struct {
+	Name string
+}
+
+type Manager struct {
+	Employee
+	Level int
+}
+
+m := Manager{}
+err := valid.ValidateStruct(&m,
+	valid.Field(&m.Name, valid.Required),
+	valid.Field(&m.Level, valid.Required),
+)
+fmt.Println(err)
+// Output:
+// Level: cannot be blank; Name: cannot be blank.
+```
+
+In the above code, we use `&m.Name` to specify the validation of the `Name` field of the embedded struct `Employee`.
+And the validation error uses `Name` as the key for the error associated with the `Name` field as if `Name` a field
+directly belonging to `Manager`.
+
+If `Employee` implements the `valid.Validatable` interface, we can also use the following code to validate
+`Manager`, which generates the same validation result:
+
+```go
+func (e Employee) Validate() error {
+	return valid.ValidateStruct(&e,
+		valid.Field(&e.Name, valid.Required),
+	)
+}
+
+err := valid.ValidateStruct(&m,
+	valid.Field(&m.Employee),
+	valid.Field(&m.Level, valid.Required),
+)
+fmt.Println(err)
+// Output:
+// Level: cannot be blank; Name: cannot be blank.
+```
+
+
+### Conditional Validation
+
+Sometimes, we may want to validate a value only when certain condition is met. For example, we want to ensure the 
+`unit` struct field is not empty only when the `quantity` field is not empty; or we may want to ensure either `email`
+or `phone` is provided. The so-called conditional validation can be achieved with the help of `valid.When`.
+The following code implements the aforementioned examples:
+
+```go
+result := valid.ValidateStruct(&a,
+    valid.Field(&a.Unit, valid.When(a.Quantity != "", valid.Required).Else(valid.Nil)),
+    valid.Field(&a.Phone, valid.When(a.Email == "", valid.Required.Error('Either phone or Email is required.')),
+    valid.Field(&a.Email, valid.When(a.Phone == "", valid.Required.Error('Either phone or Email is required.')),
+)
+```
+
+Note that `valid.When` and `valid.When.Else` can take a list of validation rules. These rules will be executed only when the condition is true (When) or false (Else).
+
+The above code can also be simplified using the shortcut `valid.Required.When`:
+
+```go
+result := valid.ValidateStruct(&a,
+    valid.Field(&a.Unit, valid.Required.When(a.Quantity != ""), valid.Nil.When(a.Quantity == "")),
+    valid.Field(&a.Phone, valid.Required.When(a.Email == "").Error('Either phone or Email is required.')),
+    valid.Field(&a.Email, valid.Required.When(a.Phone == "").Error('Either phone or Email is required.')),
+)
+```
+
+### Customizing Error Messages
+
+All built-in validation rules allow you to customize their error messages. To do so, simply call the `Error()` method
+of the rules. For example,
+
+```go
+data := "2123"
+err := valid.Validate(data,
+	valid.Required.Error("is required"),
+	valid.Match(regexp.MustCompile("^[0-9]{5}$")).Error("must be a string with five digits"),
+)
+fmt.Println(err)
+// Output:
+// must be a string with five digits
+```
+
+You can also customize the pre-defined error(s) of a built-in rule such that the customization applies to *every*
+instance of the rule. For example, the `Required` rule uses the pre-defined error `ErrRequired`. You can customize it
+during the application initialization:
+```go
+valid.ErrRequired = valid.ErrRequired.SetMessage("the value is required") 
+```
+
+### Error Code and Message Translation
+
+The errors returned by the validation rules implement the `Error` interface which contains the `Code()` method 
+to provide the error code information. While the message of a validation error is often customized, the code is immutable.
+You can use error code to programmatically check a validation error or look for the translation of the corresponding message.
+
+If you are developing your own validation rules, you can use `valid.NewError()` to create a validation error which
+implements the aforementioned `Error` interface.
+
+## Creating Custom Rules
+
+Creating a custom rule is as simple as implementing the `valid.Rule` interface. The interface contains a single
+method as shown below, which should validate the value and return the validation error, if any:
+
+```go
+// Validate validates a value and returns an error if validation fails.
+Validate(value interface{}) error
+```
+
+If you already have a function with the same signature as shown above, you can call `valid.By()` to turn
+it into a validation rule. For example,
+
+```go
+func checkAbc(value interface{}) error {
+	s, _ := value.(string)
+	if s != "abc" {
+		return errors.New("must be abc")
+	}
+	return nil
+}
+
+err := valid.Validate("xyz", valid.By(checkAbc))
+fmt.Println(err)
+// Output: must be abc
+```
+
+If your validation function takes additional parameters, you can use the following closure trick:
+
+```go
+func stringEquals(str string) valid.RuleFunc {
+	return func(value interface{}) error {
+		s, _ := value.(string)
+        if s != str {
+            return errors.New("unexpected string")
+        }
+        return nil
+    }
+}
+
+err := valid.Validate("xyz", valid.By(stringEquals("abc")))
+fmt.Println(err)
+// Output: unexpected string
+```
+
+
+### Rule Groups
+
+When a combination of several rules are used in multiple places, you may use the following trick to create a 
+rule group so that your code is more maintainable.
+
+```go
+var NameRule = []valid.Rule{
+	valid.Required,
+	valid.Length(5, 20),
+}
+
+type User struct {
+	FirstName string
+	LastName  string
+}
+
+func (u User) Validate() error {
+	return valid.ValidateStruct(&u,
+		valid.Field(&u.FirstName, NameRule...),
+		valid.Field(&u.LastName, NameRule...),
+	)
+}
+```
+
+In the above example, we create a rule group `NameRule` which consists of two validation rules. We then use this rule
+group to validate both `FirstName` and `LastName`.
+
+
+## Context-aware Validation
+
+While most validation rules are self-contained, some rules may depend dynamically on a context. A rule may implement the
+`valid.RuleWithContext` interface to support the so-called context-aware valid.
+ 
+To validate an arbitrary value with a context, call `valid.ValidateWithContext()`. The `context.Conext` parameter 
+will be passed along to those rules that implement `valid.RuleWithContext`.
+
+To validate the fields of a struct with a context, call `valid.ValidateStructWithContext()`. 
+
+You can define a context-aware rule from scratch by implementing both `valid.Rule` and `valid.RuleWithContext`. 
+You can also use `valid.WithContext()` to turn a function into a context-aware rule. For example,
+
+
+```go
+rule := valid.WithContext(func(ctx context.Context, value interface{}) error {
+	if ctx.Value("secret") == value.(string) {
+	    return nil
+	}
+	return errors.New("value incorrect")
+})
+value := "xyz"
+ctx := context.WithValue(context.Background(), "secret", "example")
+err := valid.ValidateWithContext(ctx, value, rule)
+fmt.Println(err)
+// Output: value incorrect
+```
+
+When performing context-aware validation, if a rule does not implement `valid.RuleWithContext`, its
+`valid.Rule` will be used instead.
+
+
+## Built-in Validation Rules
+
+The following rules are provided in the `validation` package:
+
+* `In(...interface{})`: checks if a value can be found in the given list of values.
+* `NotIn(...interface{})`: checks if a value is NOT among the given list of values.
+* `Length(min, max int)`: checks if the length of a value is within the specified range.
+  This rule should only be used for validating strings, slices, maps, and arrays.
+* `RuneLength(min, max int)`: checks if the length of a string is within the specified range.
+  This rule is similar as `Length` except that when the value being validated is a string, it checks
+  its rune length instead of byte length.
+* `Min(min interface{})` and `Max(max interface{})`: checks if a value is within the specified range.
+  These two rules should only be used for validating int, uint, float and time.Time types.
+* `Match(*regexp.Regexp)`: checks if a value matches the specified regular expression.
+  This rule should only be used for strings and byte slices.
+* `Date(layout string)`: checks if a string value is a date whose format is specified by the layout.
+  By calling `Min()` and/or `Max()`, you can check additionally if the date is within the specified range.
+* `Required`: checks if a value is not empty (neither nil nor zero).
+* `NotNil`: checks if a pointer value is not nil. Non-pointer values are considered valid.
+* `NilOrNotEmpty`: checks if a value is a nil pointer or a non-empty value. This differs from `Required` in that it treats a nil pointer as valid.
+* `Nil`: checks if a value is a nil pointer.
+* `Empty`: checks if a value is empty. nil pointers are considered valid.
+* `Skip`: this is a special rule used to indicate that all rules following it should be skipped (including the nested ones).
+* `MultipleOf`: checks if the value is a multiple of the specified range.
+* `Each(rules ...Rule)`: checks the elements within an iterable (map/slice/array) with other rules.
+* `When(condition, rules ...Rule)`: validates with the specified rules only when the condition is true.
+* `Else(rules ...Rule)`: must be used with `When(condition, rules ...Rule)`, validates with the specified rules only when the condition is false.
+
+The `is` sub-package provides a list of commonly used string validation rules that can be used to check if the format
+of a value satisfies certain requirements. Note that these rules only handle strings and byte slices and if a string
+ or byte slice is empty, it is considered valid. You may use a `Required` rule to ensure a value is not empty.
+Below is the whole list of the rules provided by the `is` package:
+
+* `Email`: validates if a string is an email or not. It also checks if the MX record exists for the email domain.
+* `EmailFormat`: validates if a string is an email or not. It does NOT check the existence of the MX record.
+* `URL`: validates if a string is a valid URL
+* `RequestURL`: validates if a string is a valid request URL
+* `RequestURI`: validates if a string is a valid request URI
+* `Alpha`: validates if a string contains English letters only (a-zA-Z)
+* `Digit`: validates if a string contains digits only (0-9)
+* `Alphanumeric`: validates if a string contains English letters and digits only (a-zA-Z0-9)
+* `UTFLetter`: validates if a string contains unicode letters only
+* `UTFDigit`: validates if a string contains unicode decimal digits only
+* `UTFLetterNumeric`: validates if a string contains unicode letters and numbers only
+* `UTFNumeric`: validates if a string contains unicode number characters (category N) only
+* `LowerCase`: validates if a string contains lower case unicode letters only
+* `UpperCase`: validates if a string contains upper case unicode letters only
+* `Hexadecimal`: validates if a string is a valid hexadecimal number
+* `HexColor`: validates if a string is a valid hexadecimal color code
+* `RGBColor`: validates if a string is a valid RGB color in the form of rgb(R, G, B)
+* `Int`: validates if a string is a valid integer number
+* `Float`: validates if a string is a floating point number
+* `UUIDv3`: validates if a string is a valid version 3 UUID
+* `UUIDv4`: validates if a string is a valid version 4 UUID
+* `UUIDv5`: validates if a string is a valid version 5 UUID
+* `UUID`: validates if a string is a valid UUID
+* `CreditCard`: validates if a string is a valid credit card number
+* `ISBN10`: validates if a string is an ISBN version 10
+* `ISBN13`: validates if a string is an ISBN version 13
+* `ISBN`: validates if a string is an ISBN (either version 10 or 13)
+* `JSON`: validates if a string is in valid JSON format
+* `ASCII`: validates if a string contains ASCII characters only
+* `PrintableASCII`: validates if a string contains printable ASCII characters only
+* `Multibyte`: validates if a string contains multibyte characters
+* `FullWidth`: validates if a string contains full-width characters
+* `HalfWidth`: validates if a string contains half-width characters
+* `VariableWidth`: validates if a string contains both full-width and half-width characters
+* `Base64`: validates if a string is encoded in Base64
+* `DataURI`: validates if a string is a valid base64-encoded data URI
+* `E164`: validates if a string is a valid E164 phone number (+19251232233)
+* `CountryCode2`: validates if a string is a valid ISO3166 Alpha 2 country code
+* `CountryCode3`: validates if a string is a valid ISO3166 Alpha 3 country code
+* `DialString`: validates if a string is a valid dial string that can be passed to Dial()
+* `MAC`: validates if a string is a MAC address
+* `IP`: validates if a string is a valid IP address (either version 4 or 6)
+* `IPv4`: validates if a string is a valid version 4 IP address
+* `IPv6`: validates if a string is a valid version 6 IP address
+* `Subdomain`: validates if a string is valid subdomain
+* `Domain`: validates if a string is valid domain
+* `DNSName`: validates if a string is valid DNS name
+* `Host`: validates if a string is a valid IP (both v4 and v6) or a valid DNS name
+* `Port`: validates if a string is a valid port number
+* `MongoID`: validates if a string is a valid Mongo ID
+* `Latitude`: validates if a string is a valid latitude
+* `Longitude`: validates if a string is a valid longitude
+* `SSN`: validates if a string is a social security number (SSN)
+* `Semver`: validates if a string is a valid semantic version

+ 199 - 0
pkg/valid/error.go

@@ -0,0 +1,199 @@
+package valid
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"sort"
+	"strings"
+	"text/template"
+)
+
+var (
+	// ErrStructPointer is the error that a struct being validated is not specified as a pointer.
+	ErrStructPointer = errors.New("only a pointer to a struct can be validated")
+)
+
+// ErrorTag is the struct tag name used to customize the error field name for a struct field.
+var ErrorTag = "json"
+
+// Error interface represents an validation error
+type Error interface {
+	Error() string
+	Code() string
+	Message() string
+	SetMessage(string) Error
+	Params() map[string]interface{}
+	SetParams(map[string]interface{}) Error
+}
+
+// InternalError represents an error that should NOT be treated as a validation error.
+type InternalError interface {
+	error
+	InternalError() error
+}
+
+type internalError struct {
+	error
+}
+
+// NewInternalError wraps a given error into an InternalError.
+func NewInternalError(err error) InternalError {
+	return internalError{error: err}
+}
+
+// InternalError returns the actual error that it wraps around.
+func (e internalError) InternalError() error {
+	return e.error
+}
+
+// Errors represents the validation errors that are indexed by struct field names, map or slice keys.
+// values are Error or Errors (for map, slice and array error value is Errors).
+type Errors map[string]error
+
+// Error returns the error string of Errors.
+func (es Errors) Error() string {
+	if len(es) == 0 {
+		return ""
+	}
+
+	keys := make([]string, len(es))
+	i := 0
+	for key := range es {
+		keys[i] = key
+		i++
+	}
+	sort.Strings(keys)
+
+	var s strings.Builder
+	for i, key := range keys {
+		if i > 0 {
+			s.WriteString("; ")
+		}
+		if errs, ok := es[key].(Errors); ok {
+			_, _ = fmt.Fprintf(&s, "%v: (%v)", key, errs)
+		} else {
+			_, _ = fmt.Fprintf(&s, "%v: %v", key, es[key].Error())
+		}
+	}
+	s.WriteString(".")
+	return s.String()
+}
+
+// MarshalJSON converts the Errors into a valid JSON.
+func (es Errors) MarshalJSON() ([]byte, error) {
+	errs := map[string]interface{}{}
+	for key, err := range es {
+		if ms, ok := err.(json.Marshaler); ok {
+			errs[key] = ms
+		} else {
+			errs[key] = err.Error()
+		}
+	}
+	return json.Marshal(errs)
+}
+
+// Filter removes all nils from Errors and returns back the updated Errors as an error.
+// If the length of Errors becomes 0, it will return nil.
+func (es Errors) Filter() error {
+	for key, value := range es {
+		if value == nil {
+			delete(es, key)
+		}
+	}
+	if len(es) == 0 {
+		return nil
+	}
+	return es
+}
+
+// NewError create new validation error.
+func NewError(code, message string) Error {
+	return ErrorObject{
+		code:    code,
+		message: message,
+	}
+}
+
+// Assert that our ErrorObject implements the Error interface.
+var _ Error = ErrorObject{}
+
+// ErrorObject is the default validation error
+// that implements the Error interface.
+type ErrorObject struct {
+	code    string
+	message string
+	params  map[string]interface{}
+}
+
+// SetCode set the error's translation code.
+func (e ErrorObject) SetCode(code string) Error {
+	e.code = code
+	return e
+}
+
+// Code get the error's translation code.
+func (e ErrorObject) Code() string {
+	return e.code
+}
+
+// SetParams set the error's params.
+func (e ErrorObject) SetParams(params map[string]interface{}) Error {
+	e.params = params
+	return e
+}
+
+// AddParam add parameter to the error's parameters.
+func (e ErrorObject) AddParam(name string, value interface{}) Error {
+	if e.params == nil {
+		e.params = make(map[string]interface{})
+	}
+
+	e.params[name] = value
+	return e
+}
+
+// Params returns the error's params.
+func (e ErrorObject) Params() map[string]interface{} {
+	return e.params
+}
+
+// SetMessage set the error's message.
+func (e ErrorObject) SetMessage(message string) Error {
+	e.message = message
+	return e
+}
+
+// Message return the error's message.
+func (e ErrorObject) Message() string {
+	return e.message
+}
+
+// Error returns the error message.
+func (e ErrorObject) Error() string {
+	if len(e.params) == 0 {
+		return e.message
+	}
+
+	res := bytes.Buffer{}
+	_ = template.Must(template.New("err").Parse(e.message)).Execute(&res, e.params)
+
+	return res.String()
+}
+
+// ErrFieldPointer is the error that a field is not specified as a pointer.
+type ErrFieldPointer int
+
+// Error returns the error string of ErrFieldPointer.
+func (e ErrFieldPointer) Error() string {
+	return fmt.Sprintf("field #%v must be specified as a pointer", int(e))
+}
+
+// ErrFieldNotFound is the error that a field cannot be found in the struct.
+type ErrFieldNotFound int
+
+// Error returns the error string of ErrFieldNotFound.
+func (e ErrFieldNotFound) Error() string {
+	return fmt.Sprintf("field #%v cannot be found in the struct", int(e))
+}

+ 185 - 0
pkg/valid/error_test.go

@@ -0,0 +1,185 @@
+package valid
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewInternalError(t *testing.T) {
+	err := NewInternalError(errors.New("abc"))
+	if assert.NotNil(t, err.InternalError()) {
+		assert.Equal(t, "abc", err.InternalError().Error())
+	}
+}
+
+func TestErrors_Error(t *testing.T) {
+	errs := Errors{
+		"B": errors.New("B1"),
+		"C": errors.New("C1"),
+		"A": errors.New("A1"),
+	}
+	assert.Equal(t, "A: A1; B: B1; C: C1.", errs.Error())
+
+	errs = Errors{
+		"B": errors.New("B1"),
+	}
+	assert.Equal(t, "B: B1.", errs.Error())
+
+	errs = Errors{}
+	assert.Equal(t, "", errs.Error())
+}
+
+func TestErrors_MarshalMessage(t *testing.T) {
+	errs := Errors{
+		"A": errors.New("A1"),
+		"B": Errors{
+			"2": errors.New("B1"),
+		},
+	}
+	errsJSON, err := errs.MarshalJSON()
+	assert.Nil(t, err)
+	assert.Equal(t, "{\"A\":\"A1\",\"B\":{\"2\":\"B1\"}}", string(errsJSON))
+}
+
+func TestErrors_Filter(t *testing.T) {
+	errs := Errors{
+		"B": errors.New("B1"),
+		"C": nil,
+		"A": errors.New("A1"),
+	}
+	err := errs.Filter()
+	assert.Equal(t, 2, len(errs))
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "A: A1; B: B1.", err.Error())
+	}
+
+	errs = Errors{}
+	assert.Nil(t, errs.Filter())
+
+	errs = Errors{
+		"B": nil,
+		"C": nil,
+	}
+
+	assert.Nil(t, errs.Filter())
+}
+
+func TestErrorObject_SetCode(t *testing.T) {
+	err := NewError("A", "msg").(ErrorObject)
+
+	assert.Equal(t, err.code, "A")
+	assert.Equal(t, err.Code(), "A")
+
+	err = err.SetCode("B").(ErrorObject)
+	assert.Equal(t, "B", err.code)
+}
+
+func TestErrorObject_Code(t *testing.T) {
+	err := NewError("A", "msg").(ErrorObject)
+
+	assert.Equal(t, err.Code(), "A")
+}
+
+func TestErrorObject_SetMessage(t *testing.T) {
+	err := NewError("code", "A").(ErrorObject)
+
+	assert.Equal(t, err.message, "A")
+	assert.Equal(t, err.Message(), "A")
+
+	err = err.SetMessage("abc").(ErrorObject)
+	assert.Equal(t, err.message, "abc")
+	assert.Equal(t, err.Message(), "abc")
+}
+
+func TestErrorObject_Message(t *testing.T) {
+	err := NewError("code", "A").(ErrorObject)
+
+	assert.Equal(t, err.message, "A")
+	assert.Equal(t, err.Message(), "A")
+}
+
+func TestErrorObject_Params(t *testing.T) {
+	p := map[string]interface{}{"A": "val1", "AA": "val2"}
+
+	err := NewError("code", "A").(ErrorObject)
+	err = err.SetParams(p).(ErrorObject)
+	err = err.SetMessage("B").(ErrorObject)
+
+	assert.Equal(t, err.params, p)
+	assert.Equal(t, err.Params(), p)
+}
+
+func TestErrorObject_AddParam(t *testing.T) {
+	t.Run("case1", func(t *testing.T) {
+		p := map[string]interface{}{"A": "val1", "B": "val2"}
+
+		err := NewError("code", "A").(ErrorObject)
+		err = err.SetParams(p).(ErrorObject)
+		err = err.AddParam("C", "val3").(ErrorObject)
+
+		p["C"] = "val3"
+
+		assert.Equal(t, err.params, p)
+		assert.Equal(t, err.Params(), p)
+	})
+
+	t.Run("case2", func(t *testing.T) {
+		p := map[string]interface{}{"key": "val"}
+		err := NewError("code", "A").(ErrorObject)
+		err = err.AddParam("key", "val").(ErrorObject)
+
+		assert.Equal(t, err.params, p)
+		assert.Equal(t, err.Params(), p)
+	})
+}
+
+func TestError_Code(t *testing.T) {
+	err := NewError("A", "msg")
+
+	assert.Equal(t, err.Code(), "A")
+}
+
+func TestError_SetMessage(t *testing.T) {
+	err := NewError("code", "A")
+
+	assert.Equal(t, err.Message(), "A")
+
+	err = err.SetMessage("abc")
+	assert.Equal(t, err.Message(), "abc")
+}
+
+func TestError_Message(t *testing.T) {
+	err := NewError("code", "A")
+
+	assert.Equal(t, err.Message(), "A")
+}
+
+func TestError_Params(t *testing.T) {
+	p := map[string]interface{}{"A": "val1", "AA": "val2"}
+
+	err := NewError("code", "A")
+	err = err.SetParams(p)
+	err = err.SetMessage("B")
+
+	assert.Equal(t, err.Params(), p)
+}
+
+func TestValidationError(t *testing.T) {
+	params := map[string]interface{}{
+		"A": "B",
+	}
+
+	err := NewError("code", "msg")
+	err = err.SetParams(params)
+
+	assert.Equal(t, err.Code(), "code")
+	assert.Equal(t, err.Message(), "msg")
+	assert.Equal(t, err.Params(), params)
+
+	params = map[string]interface{}{"min": 1}
+	err = err.SetParams(params)
+
+	assert.Equal(t, err.Params(), params)
+}

+ 192 - 0
pkg/valid/example_test.go

@@ -0,0 +1,192 @@
+package valid_test
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"regexp"
+
+	"kpt-tmr-group/pkg/valid"
+	"kpt-tmr-group/pkg/valid/is"
+)
+
+type Address struct {
+	Street string
+	City   string
+	State  string
+	Zip    string
+}
+
+type Customer struct {
+	Name    string
+	Gender  string
+	Email   string
+	Address Address
+}
+
+func (a Address) Validate() error {
+	return valid.ValidateStruct(&a,
+		// Street cannot be empty, and the length must between 5 and 50
+		valid.Field(&a.Street, valid.Required, valid.Length(5, 50)),
+		// City cannot be empty, and the length must between 5 and 50
+		valid.Field(&a.City, valid.Required, valid.Length(5, 50)),
+		// State cannot be empty, and must be a string consisting of two letters in upper case
+		valid.Field(&a.State, valid.Required, valid.Match(regexp.MustCompile("^[A-Z]{2}$"))),
+		// State cannot be empty, and must be a string consisting of five digits
+		valid.Field(&a.Zip, valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+	)
+}
+
+func (c Customer) Validate() error {
+	return valid.ValidateStruct(&c,
+		// Name cannot be empty, and the length must be between 5 and 20.
+		valid.Field(&c.Name, valid.Required, valid.Length(5, 20)),
+		// Gender is optional, and should be either "Female" or "Male".
+		valid.Field(&c.Gender, valid.In("Female", "Male")),
+		// Email cannot be empty and should be in a valid email format.
+		valid.Field(&c.Email, valid.Required, is.Email),
+		// Validate Address using its own validation rules
+		valid.Field(&c.Address),
+	)
+}
+
+func Example() {
+	c := Customer{
+		Name:  "Qiang Xue",
+		Email: "q",
+		Address: Address{
+			Street: "123 Main Street",
+			City:   "Unknown",
+			State:  "Virginia",
+			Zip:    "12345",
+		},
+	}
+
+	err := c.Validate()
+	fmt.Println(err)
+	// Output:
+	// Address: (State: must be in a valid format.); Email: must be a valid email address.
+}
+
+func Example_second() {
+	data := "example"
+	err := valid.Validate(data,
+		valid.Required,       // not empty
+		valid.Length(5, 100), // length between 5 and 100
+		is.URL,               // is a valid URL
+	)
+	fmt.Println(err)
+	// Output:
+	// must be a valid URL
+}
+
+func Example_third() {
+	addresses := []Address{
+		{State: "MD", Zip: "12345"},
+		{Street: "123 Main St", City: "Vienna", State: "VA", Zip: "12345"},
+		{City: "Unknown", State: "NC", Zip: "123"},
+	}
+	err := valid.Validate(addresses)
+	fmt.Println(err)
+	// Output:
+	// 0: (City: cannot be blank; Street: cannot be blank.); 2: (Street: cannot be blank; Zip: must be in a valid format.).
+}
+
+func Example_four() {
+	c := Customer{
+		Name:  "Qiang Xue",
+		Email: "q",
+		Address: Address{
+			State: "Virginia",
+		},
+	}
+
+	err := valid.Errors{
+		"name":  valid.Validate(c.Name, valid.Required, valid.Length(5, 20)),
+		"email": valid.Validate(c.Name, valid.Required, is.Email),
+		"zip":   valid.Validate(c.Address.Zip, valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+	}.Filter()
+	fmt.Println(err)
+	// Output:
+	// email: must be a valid email address; zip: cannot be blank.
+}
+
+func Example_five() {
+	type Employee struct {
+		Name string
+	}
+
+	type Manager struct {
+		Employee
+		Level int
+	}
+
+	m := Manager{}
+	err := valid.ValidateStruct(&m,
+		valid.Field(&m.Name, valid.Required),
+		valid.Field(&m.Level, valid.Required),
+	)
+	fmt.Println(err)
+	// Output:
+	// Level: cannot be blank; Name: cannot be blank.
+}
+
+type contextKey int
+
+func Example_six() {
+	key := contextKey(1)
+	rule := valid.WithContext(func(ctx context.Context, value interface{}) error {
+		s, _ := value.(string)
+		if ctx.Value(key) == s {
+			return nil
+		}
+		return errors.New("unexpected value")
+	})
+	ctx := context.WithValue(context.Background(), key, "good sample")
+
+	err1 := valid.ValidateWithContext(ctx, "bad sample", rule)
+	fmt.Println(err1)
+
+	err2 := valid.ValidateWithContext(ctx, "good sample", rule)
+	fmt.Println(err2)
+
+	// Output:
+	// unexpected value
+	// <nil>
+}
+
+func Example_seven() {
+	c := map[string]interface{}{
+		"Name":  "Qiang Xue",
+		"Email": "q",
+		"Address": map[string]interface{}{
+			"Street": "123",
+			"City":   "Unknown",
+			"State":  "Virginia",
+			"Zip":    "12345",
+		},
+	}
+
+	err := valid.Validate(c,
+		valid.Map(
+			// Name cannot be empty, and the length must be between 5 and 20.
+			valid.Key("Name", valid.Required, valid.Length(5, 20)),
+			// Email cannot be empty and should be in a valid email format.
+			valid.Key("Email", valid.Required, is.Email),
+			// Validate Address using its own validation rules
+			valid.Key("Address", valid.Map(
+				// Street cannot be empty, and the length must between 5 and 50
+				valid.Key("Street", valid.Required, valid.Length(5, 50)),
+				// City cannot be empty, and the length must between 5 and 50
+				valid.Key("City", valid.Required, valid.Length(5, 50)),
+				// State cannot be empty, and must be a string consisting of two letters in upper case
+				valid.Key("State", valid.Required, valid.Match(regexp.MustCompile("^[A-Z]{2}$"))),
+				// State cannot be empty, and must be a string consisting of five digits
+				valid.Key("Zip", valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+			)),
+		),
+	)
+	fmt.Println(err)
+	// Output:
+	// Address: (State: must be in a valid format; Street: the length must be between 5 and 50.); Email: must be a valid email address.
+}

+ 29 - 0
pkg/valid/interface.go

@@ -0,0 +1,29 @@
+package valid
+
+import (
+	"context"
+)
+
+// Validatable is the interface indicating the type implementing it supports data validation.
+type Validatable interface {
+	// Validate validates the data and returns an error if validation fails.
+	Validate() error
+}
+
+// ValidatableWithContext is the interface indicating the type implementing it supports context-aware data validation.
+type ValidatableWithContext interface {
+	// ValidateWithContext validates the data with the given context and returns an error if validation fails.
+	ValidateWithContext(ctx context.Context) error
+}
+
+// Rule represents a validation rule.
+type Rule interface {
+	// Validate validates a value and returns a value if validation fails.
+	Validate(value interface{}) error
+}
+
+// RuleWithContext represents a context-aware validation rule.
+type RuleWithContext interface {
+	// ValidateWithContext validates a value and returns a value if validation fails.
+	ValidateWithContext(ctx context.Context, value interface{}) error
+}

+ 293 - 0
pkg/valid/is/rule.go

@@ -0,0 +1,293 @@
+package is
+
+import (
+	"regexp"
+	"unicode"
+
+	"git.llsapp.com/zhenghe/pkg/valid"
+	"github.com/asaskevich/govalidator"
+	"github.com/nyaruka/phonenumbers"
+)
+
+var (
+	// ErrEmail is the error that returns in case of an invalid email.
+	ErrEmail = valid.NewError("validation_is_email", "must be a valid email address")
+	// ErrURL is the error that returns in case of an invalid URL.
+	ErrURL = valid.NewError("validation_is_url", "must be a valid URL")
+	// ErrRequestURL is the error that returns in case of an invalid request URL.
+	ErrRequestURL = valid.NewError("validation_is_request_url", "must be a valid request URL")
+	// ErrRequestURI is the error that returns in case of an invalid request URI.
+	ErrRequestURI = valid.NewError("validation_request_is_request_uri", "must be a valid request URI")
+	// ErrAlpha is the error that returns in case of an invalid alpha value.
+	ErrAlpha = valid.NewError("validation_is_alpha", "must contain English letters only")
+	// ErrDigit is the error that returns in case of an invalid digit value.
+	ErrDigit = valid.NewError("validation_is_digit", "must contain digits only")
+	// ErrAlphanumeric is the error that returns in case of an invalid alphanumeric value.
+	ErrAlphanumeric = valid.NewError("validation_is_alphanumeric", "must contain English letters and digits only")
+	// ErrUTFLetter is the error that returns in case of an invalid utf letter value.
+	ErrUTFLetter = valid.NewError("validation_is_utf_letter", "must contain unicode letter characters only")
+	// ErrUTFDigit is the error that returns in case of an invalid utf digit value.
+	ErrUTFDigit = valid.NewError("validation_is_utf_digit", "must contain unicode decimal digits only")
+	// ErrUTFLetterNumeric is the error that returns in case of an invalid utf numeric or letter value.
+	ErrUTFLetterNumeric = valid.NewError("validation_is utf_letter_numeric", "must contain unicode letters and numbers only")
+	// ErrUTFNumeric is the error that returns in case of an invalid utf numeric value.
+	ErrUTFNumeric = valid.NewError("validation_is_utf_numeric", "must contain unicode number characters only")
+	// ErrLowerCase is the error that returns in case of an invalid lower case value.
+	ErrLowerCase = valid.NewError("validation_is_lower_case", "must be in lower case")
+	// ErrUpperCase is the error that returns in case of an invalid upper case value.
+	ErrUpperCase = valid.NewError("validation_is_upper_case", "must be in upper case")
+	// ErrHexadecimal is the error that returns in case of an invalid hexadecimal number.
+	ErrHexadecimal = valid.NewError("validation_is_hexadecimal", "must be a valid hexadecimal number")
+	// ErrHexColor is the error that returns in case of an invalid hexadecimal color code.
+	ErrHexColor = valid.NewError("validation_is_hex_color", "must be a valid hexadecimal color code")
+	// ErrRGBColor is the error that returns in case of an invalid RGB color code.
+	ErrRGBColor = valid.NewError("validation_is_rgb_color", "must be a valid RGB color code")
+	// ErrInt is the error that returns in case of an invalid integer value.
+	ErrInt = valid.NewError("validation_is_int", "must be an integer number")
+	// ErrFloat is the error that returns in case of an invalid float value.
+	ErrFloat = valid.NewError("validation_is_float", "must be a floating point number")
+	// ErrUUIDv3 is the error that returns in case of an invalid UUIDv3 value.
+	ErrUUIDv3 = valid.NewError("validation_is_uuid_v3", "must be a valid UUID v3")
+	// ErrUUIDv4 is the error that returns in case of an invalid UUIDv4 value.
+	ErrUUIDv4 = valid.NewError("validation_is_uuid_v4", "must be a valid UUID v4")
+	// ErrUUIDv5 is the error that returns in case of an invalid UUIDv5 value.
+	ErrUUIDv5 = valid.NewError("validation_is_uuid_v5", "must be a valid UUID v5")
+	// ErrUUID is the error that returns in case of an invalid UUID value.
+	ErrUUID = valid.NewError("validation_is_uuid", "must be a valid UUID")
+	// ErrCreditCard is the error that returns in case of an invalid credit card number.
+	ErrCreditCard = valid.NewError("validation_is_credit_card", "must be a valid credit card number")
+	// ErrISBN10 is the error that returns in case of an invalid ISBN-10 value.
+	ErrISBN10 = valid.NewError("validation_is_isbn_10", "must be a valid ISBN-10")
+	// ErrISBN13 is the error that returns in case of an invalid ISBN-13 value.
+	ErrISBN13 = valid.NewError("validation_is_isbn_13", "must be a valid ISBN-13")
+	// ErrISBN is the error that returns in case of an invalid ISBN value.
+	ErrISBN = valid.NewError("validation_is_isbn", "must be a valid ISBN")
+	// ErrJSON is the error that returns in case of an invalid JSON.
+	ErrJSON = valid.NewError("validation_is_json", "must be in valid JSON format")
+	// ErrASCII is the error that returns in case of an invalid ASCII.
+	ErrASCII = valid.NewError("validation_is_ascii", "must contain ASCII characters only")
+	// ErrPrintableASCII is the error that returns in case of an invalid printable ASCII value.
+	ErrPrintableASCII = valid.NewError("validation_is_printable_ascii", "must contain printable ASCII characters only")
+	// ErrMultibyte is the error that returns in case of an invalid multibyte value.
+	ErrMultibyte = valid.NewError("validation_is_multibyte", "must contain multibyte characters")
+	// ErrFullWidth is the error that returns in case of an invalid full-width value.
+	ErrFullWidth = valid.NewError("validation_is_full_width", "must contain full-width characters")
+	// ErrHalfWidth is the error that returns in case of an invalid half-width value.
+	ErrHalfWidth = valid.NewError("validation_is_half_width", "must contain half-width characters")
+	// ErrVariableWidth is the error that returns in case of an invalid variable width value.
+	ErrVariableWidth = valid.NewError("validation_is_variable_width", "must contain both full-width and half-width characters")
+	// ErrBase64 is the error that returns in case of an invalid base54 value.
+	ErrBase64 = valid.NewError("validation_is_base64", "must be encoded in Base64")
+	// ErrDataURI is the error that returns in case of an invalid data URI.
+	ErrDataURI = valid.NewError("validation_is_data_uri", "must be a Base64-encoded data URI")
+	// ErrE164 is the error that returns in case of an invalid e165.
+	ErrE164 = valid.NewError("validation_is_e164_number", "must be a valid E164 number")
+	// ErrCountryCode2 is the error that returns in case of an invalid two-letter country code.
+	ErrCountryCode2 = valid.NewError("validation_is_country_code_2_letter", "must be a valid two-letter country code")
+	// ErrCountryCode3 is the error that returns in case of an invalid three-letter country code.
+	ErrCountryCode3 = valid.NewError("validation_is_country_code_3_letter", "must be a valid three-letter country code")
+	// ErrCurrencyCode is the error that returns in case of an invalid currency code.
+	ErrCurrencyCode = valid.NewError("validation_is_currency_code", "must be valid ISO 4217 currency code")
+	// ErrDialString is the error that returns in case of an invalid string.
+	ErrDialString = valid.NewError("validation_is_dial_string", "must be a valid dial string")
+	// ErrMac is the error that returns in case of an invalid mac address.
+	ErrMac = valid.NewError("validation_is_mac_address", "must be a valid MAC address")
+	// ErrIP is the error that returns in case of an invalid IP.
+	ErrIP = valid.NewError("validation_is_ip", "must be a valid IP address")
+	// ErrIPv4 is the error that returns in case of an invalid IPv4.
+	ErrIPv4 = valid.NewError("validation_is_ipv4", "must be a valid IPv4 address")
+	// ErrIPv6 is the error that returns in case of an invalid IPv6.
+	ErrIPv6 = valid.NewError("validation_is_ipv6", "must be a valid IPv6 address")
+	// ErrSubdomain is the error that returns in case of an invalid subdomain.
+	ErrSubdomain = valid.NewError("validation_is_sub_domain", "must be a valid subdomain")
+	// ErrDomain is the error that returns in case of an invalid domain.
+	ErrDomain = valid.NewError("validation_is_domain", "must be a valid domain")
+	// ErrDNSName is the error that returns in case of an invalid DNS name.
+	ErrDNSName = valid.NewError("validation_is_dns_name", "must be a valid DNS name")
+	// ErrHost is the error that returns in case of an invalid host.
+	ErrHost = valid.NewError("validation_is_host", "must be a valid IP address or DNS name")
+	// ErrPort is the error that returns in case of an invalid port.
+	ErrPort = valid.NewError("validation_is_port", "must be a valid port number")
+	// ErrMongoID is the error that returns in case of an invalid MongoID.
+	ErrMongoID = valid.NewError("validation_is_mongo_id", "must be a valid hex-encoded MongoDB ObjectId")
+	// ErrLatitude is the error that returns in case of an invalid latitude.
+	ErrLatitude = valid.NewError("validation_is_latitude", "must be a valid latitude")
+	// ErrLongitude is the error that returns in case of an invalid longitude.
+	ErrLongitude = valid.NewError("validation_is_longitude", "must be a valid longitude")
+	// ErrSSN is the error that returns in case of an invalid SSN.
+	ErrSSN = valid.NewError("validation_is_ssn", "must be a valid social security number")
+	// ErrSemver is the error that returns in case of an invalid semver.
+	ErrSemver = valid.NewError("validation_is_semver", "must be a valid semantic version")
+)
+
+var (
+	// Email validates if a string is an email or not. It also checks if the MX record exists for the email domain.
+	Email = valid.NewStringRuleWithError(govalidator.IsExistingEmail, ErrEmail)
+	// EmailFormat validates if a string is an email or not. Note that it does NOT check if the MX record exists or not.
+	EmailFormat = valid.NewStringRuleWithError(govalidator.IsEmail, ErrEmail)
+	// EmailUser validates if a string is an user email or not.
+	EmailUser = valid.NewStringRuleWithError(isUserEmail, ErrEmail)
+	// URL validates if a string is a valid URL
+	URL = valid.NewStringRuleWithError(govalidator.IsURL, ErrURL)
+	// RequestURL validates if a string is a valid request URL
+	RequestURL = valid.NewStringRuleWithError(govalidator.IsRequestURL, ErrRequestURL)
+	// RequestURI validates if a string is a valid request URI
+	RequestURI = valid.NewStringRuleWithError(govalidator.IsRequestURI, ErrRequestURI)
+	// Alpha validates if a string contains English letters only (a-zA-Z)
+	Alpha = valid.NewStringRuleWithError(govalidator.IsAlpha, ErrAlpha)
+	// Digit validates if a string contains digits only (0-9)
+	Digit = valid.NewStringRuleWithError(isDigit, ErrDigit)
+	// Alphanumeric validates if a string contains English letters and digits only (a-zA-Z0-9)
+	Alphanumeric = valid.NewStringRuleWithError(govalidator.IsAlphanumeric, ErrAlphanumeric)
+	// UTFLetter validates if a string contains unicode letters only
+	UTFLetter = valid.NewStringRuleWithError(govalidator.IsUTFLetter, ErrUTFLetter)
+	// UTFDigit validates if a string contains unicode decimal digits only
+	UTFDigit = valid.NewStringRuleWithError(govalidator.IsUTFDigit, ErrUTFDigit)
+	// UTFLetterNumeric validates if a string contains unicode letters and numbers only
+	UTFLetterNumeric = valid.NewStringRuleWithError(govalidator.IsUTFLetterNumeric, ErrUTFLetterNumeric)
+	// UTFNumeric validates if a string contains unicode number characters (category N) only
+	UTFNumeric = valid.NewStringRuleWithError(isUTFNumeric, ErrUTFNumeric)
+	// LowerCase validates if a string contains lower case unicode letters only
+	LowerCase = valid.NewStringRuleWithError(govalidator.IsLowerCase, ErrLowerCase)
+	// UpperCase validates if a string contains upper case unicode letters only
+	UpperCase = valid.NewStringRuleWithError(govalidator.IsUpperCase, ErrUpperCase)
+	// Hexadecimal validates if a string is a valid hexadecimal number
+	Hexadecimal = valid.NewStringRuleWithError(govalidator.IsHexadecimal, ErrHexadecimal)
+	// HexColor validates if a string is a valid hexadecimal color code
+	HexColor = valid.NewStringRuleWithError(govalidator.IsHexcolor, ErrHexColor)
+	// RGBColor validates if a string is a valid RGB color in the form of rgb(R, G, B)
+	RGBColor = valid.NewStringRuleWithError(govalidator.IsRGBcolor, ErrRGBColor)
+	// Int validates if a string is a valid integer number
+	Int = valid.NewStringRuleWithError(govalidator.IsInt, ErrInt)
+	// Float validates if a string is a floating point number
+	Float = valid.NewStringRuleWithError(govalidator.IsFloat, ErrFloat)
+	// UUIDv3 validates if a string is a valid version 3 UUID
+	UUIDv3 = valid.NewStringRuleWithError(govalidator.IsUUIDv3, ErrUUIDv3)
+	// UUIDv4 validates if a string is a valid version 4 UUID
+	UUIDv4 = valid.NewStringRuleWithError(govalidator.IsUUIDv4, ErrUUIDv4)
+	// UUIDv5 validates if a string is a valid version 5 UUID
+	UUIDv5 = valid.NewStringRuleWithError(govalidator.IsUUIDv5, ErrUUIDv5)
+	// UUID validates if a string is a valid UUID
+	UUID = valid.NewStringRuleWithError(govalidator.IsUUID, ErrUUID)
+	// CreditCard validates if a string is a valid credit card number
+	CreditCard = valid.NewStringRuleWithError(govalidator.IsCreditCard, ErrCreditCard)
+	// ISBN10 validates if a string is an ISBN version 10
+	ISBN10 = valid.NewStringRuleWithError(govalidator.IsISBN10, ErrISBN10)
+	// ISBN13 validates if a string is an ISBN version 13
+	ISBN13 = valid.NewStringRuleWithError(govalidator.IsISBN13, ErrISBN13)
+	// ISBN validates if a string is an ISBN (either version 10 or 13)
+	ISBN = valid.NewStringRuleWithError(isISBN, ErrISBN)
+	// JSON validates if a string is in valid JSON format
+	JSON = valid.NewStringRuleWithError(govalidator.IsJSON, ErrJSON)
+	// ASCII validates if a string contains ASCII characters only
+	ASCII = valid.NewStringRuleWithError(govalidator.IsASCII, ErrASCII)
+	// PrintableASCII validates if a string contains printable ASCII characters only
+	PrintableASCII = valid.NewStringRuleWithError(govalidator.IsPrintableASCII, ErrPrintableASCII)
+	// Multibyte validates if a string contains multibyte characters
+	Multibyte = valid.NewStringRuleWithError(govalidator.IsMultibyte, ErrMultibyte)
+	// FullWidth validates if a string contains full-width characters
+	FullWidth = valid.NewStringRuleWithError(govalidator.IsFullWidth, ErrFullWidth)
+	// HalfWidth validates if a string contains half-width characters
+	HalfWidth = valid.NewStringRuleWithError(govalidator.IsHalfWidth, ErrHalfWidth)
+	// VariableWidth validates if a string contains both full-width and half-width characters
+	VariableWidth = valid.NewStringRuleWithError(govalidator.IsVariableWidth, ErrVariableWidth)
+	// Base64 validates if a string is encoded in Base64
+	Base64 = valid.NewStringRuleWithError(govalidator.IsBase64, ErrBase64)
+	// DataURI validates if a string is a valid base64-encoded data URI
+	DataURI = valid.NewStringRuleWithError(govalidator.IsDataURI, ErrDataURI)
+	// E164 validates if a string is a valid ISO3166 Alpha 2 country code
+	E164 = valid.NewStringRuleWithError(isE164Number, ErrE164)
+	// CountryCode2 validates if a string is a valid ISO3166 Alpha 2 country code
+	CountryCode2 = valid.NewStringRuleWithError(govalidator.IsISO3166Alpha2, ErrCountryCode2)
+	// CountryCode3 validates if a string is a valid ISO3166 Alpha 3 country code
+	CountryCode3 = valid.NewStringRuleWithError(govalidator.IsISO3166Alpha3, ErrCountryCode3)
+	// CurrencyCode validates if a string is a valid IsISO4217 currency code.
+	CurrencyCode = valid.NewStringRuleWithError(govalidator.IsISO4217, ErrCurrencyCode)
+	// DialString validates if a string is a valid dial string that can be passed to Dial()
+	DialString = valid.NewStringRuleWithError(govalidator.IsDialString, ErrDialString)
+	// MAC validates if a string is a MAC address
+	MAC = valid.NewStringRuleWithError(govalidator.IsMAC, ErrMac)
+	// IP validates if a string is a valid IP address (either version 4 or 6)
+	IP = valid.NewStringRuleWithError(govalidator.IsIP, ErrIP)
+	// IPv4 validates if a string is a valid version 4 IP address
+	IPv4 = valid.NewStringRuleWithError(govalidator.IsIPv4, ErrIPv4)
+	// IPv6 validates if a string is a valid version 6 IP address
+	IPv6 = valid.NewStringRuleWithError(govalidator.IsIPv6, ErrIPv6)
+	// Subdomain validates if a string is valid subdomain
+	Subdomain = valid.NewStringRuleWithError(isSubdomain, ErrSubdomain)
+	// Domain validates if a string is valid domain
+	Domain = valid.NewStringRuleWithError(isDomain, ErrDomain)
+	// DNSName validates if a string is valid DNS name
+	DNSName = valid.NewStringRuleWithError(govalidator.IsDNSName, ErrDNSName)
+	// Host validates if a string is a valid IP (both v4 and v6) or a valid DNS name
+	Host = valid.NewStringRuleWithError(govalidator.IsHost, ErrHost)
+	// Port validates if a string is a valid port number
+	Port = valid.NewStringRuleWithError(govalidator.IsPort, ErrPort)
+	// MongoID validates if a string is a valid Mongo ID
+	MongoID = valid.NewStringRuleWithError(govalidator.IsMongoID, ErrMongoID)
+	// Latitude validates if a string is a valid latitude
+	Latitude = valid.NewStringRuleWithError(govalidator.IsLatitude, ErrLatitude)
+	// Longitude validates if a string is a valid longitude
+	Longitude = valid.NewStringRuleWithError(govalidator.IsLongitude, ErrLongitude)
+	// SSN validates if a string is a social security number (SSN)
+	SSN = valid.NewStringRuleWithError(govalidator.IsSSN, ErrSSN)
+	// Semver validates if a string is a valid semantic version
+	Semver = valid.NewStringRuleWithError(govalidator.IsSemver, ErrSemver)
+)
+
+var (
+	reDigit = regexp.MustCompile("^[0-9]+$")
+	// Subdomain regex source: https://stackoverflow.com/a/7933253
+	reSubdomain = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$`)
+	// Domain regex source: https://stackoverflow.com/a/7933253
+	// Slightly modified: Removed 255 max length validation since Go regex does not
+	// support lookarounds. More info: https://stackoverflow.com/a/38935027
+	reDomain = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-zA-Z]{1,63}| xn--[a-z0-9]{1,59})$`)
+	// UserEmail regex 来源自 Russell 的代码,这里作为用户邮箱的校验逻辑
+	reUserEmail = regexp.MustCompile(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`)
+)
+
+func isISBN(value string) bool {
+	return govalidator.IsISBN(value, 10) || govalidator.IsISBN(value, 13)
+}
+
+func isDigit(value string) bool {
+	return reDigit.MatchString(value)
+}
+
+func isE164Number(value string) bool {
+	phoneNumber, err := phonenumbers.Parse(value, "CN")
+	if err != nil {
+		return false
+	}
+	if !phonenumbers.IsValidNumber(phoneNumber) {
+		return false
+	}
+
+	return true
+}
+
+func isSubdomain(value string) bool {
+	return reSubdomain.MatchString(value)
+}
+
+func isDomain(value string) bool {
+	if len(value) > 255 {
+		return false
+	}
+
+	return reDomain.MatchString(value)
+}
+
+func isUTFNumeric(value string) bool {
+	for _, c := range value {
+		if unicode.IsNumber(c) == false {
+			return false
+		}
+	}
+	return true
+}
+
+func isUserEmail(value string) bool {
+	return reUserEmail.MatchString(value)
+}

+ 98 - 0
pkg/valid/is/rule_test.go

@@ -0,0 +1,98 @@
+package is
+
+import (
+	"strings"
+	"testing"
+
+	"git.llsapp.com/zhenghe/pkg/valid"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAll(t *testing.T) {
+	tests := []struct {
+		tag            string
+		rule           valid.Rule
+		valid, invalid string
+		err            string
+	}{
+		{"Email", Email, "test@example.com", "example.com", "must be a valid email address"},
+		{"EmailFormat", EmailFormat, "test@example.com", "example.com", "must be a valid email address"},
+		{"EmailUser", EmailUser, "test@example.com", "example.com", "must be a valid email address"},
+		{"URL", URL, "http://example.com", "examplecom", "must be a valid URL"},
+		{"RequestURL", RequestURL, "http://example.com", "examplecom", "must be a valid request URL"},
+		{"RequestURI", RequestURI, "http://example.com", "examplecom", "must be a valid request URI"},
+		{"Alpha", Alpha, "abcd", "ab12", "must contain English letters only"},
+		{"Digit", Digit, "123", "12ab", "must contain digits only"},
+		{"Alphanumeric", Alphanumeric, "abc123", "abc.123", "must contain English letters and digits only"},
+		{"UTFLetter", UTFLetter, "abc", "123", "must contain unicode letter characters only"},
+		{"UTFDigit", UTFDigit, "123", "abc", "must contain unicode decimal digits only"},
+		{"UTFNumeric", UTFNumeric, "123", "abc.123", "must contain unicode number characters only"},
+		{"UTFLetterNumeric", UTFLetterNumeric, "abc123", "abc.123", "must contain unicode letters and numbers only"},
+		{"LowerCase", LowerCase, "abc", "Abc", "must be in lower case"},
+		{"UpperCase", UpperCase, "ABC", "ABc", "must be in upper case"},
+		{"IP", IP, "74.125.19.99", "74.125.19.999", "must be a valid IP address"},
+		{"IPv4", IPv4, "74.125.19.99", "2001:4860:0:2001::68", "must be a valid IPv4 address"},
+		{"IPv6", IPv6, "2001:4860:0:2001::68", "74.125.19.99", "must be a valid IPv6 address"},
+		{"MAC", MAC, "0123.4567.89ab", "74.125.19.99", "must be a valid MAC address"},
+		{"Subdomain", Subdomain, "example-subdomain", "example.com", "must be a valid subdomain"},
+		{"Domain", Domain, "example-domain.com", "localhost", "must be a valid domain"},
+		{"Domain", Domain, "example-domain.com", strings.Repeat("a", 256), "must be a valid domain"},
+		{"DNSName", DNSName, "example.com", "abc%", "must be a valid DNS name"},
+		{"Host", Host, "example.com", "abc%", "must be a valid IP address or DNS name"},
+		{"Port", Port, "123", "99999", "must be a valid port number"},
+		{"Latitude", Latitude, "23.123", "100", "must be a valid latitude"},
+		{"Longitude", Longitude, "123.123", "abc", "must be a valid longitude"},
+		{"SSN", SSN, "100-00-1000", "100-0001000", "must be a valid social security number"},
+		{"Semver", Semver, "1.0.0", "1.0.0.0", "must be a valid semantic version"},
+		{"ISBN", ISBN, "1-61729-085-8", "1-61729-085-81", "must be a valid ISBN"},
+		{"ISBN10", ISBN10, "1-61729-085-8", "1-61729-085-81", "must be a valid ISBN-10"},
+		{"ISBN13", ISBN13, "978-4-87311-368-5", "978-4-87311-368-a", "must be a valid ISBN-13"},
+		{"UUID", UUID, "a987fbc9-4bed-3078-cf07-9141ba07c9f1", "a987fbc9-4bed-3078-cf07-9141ba07c9f3a", "must be a valid UUID"},
+		{"UUIDv3", UUIDv3, "b987fbc9-4bed-3078-cf07-9141ba07c9f3", "b987fbc9-4bed-4078-cf07-9141ba07c9f3", "must be a valid UUID v3"},
+		{"UUIDv4", UUIDv4, "57b73598-8764-4ad0-a76a-679bb6640eb1", "b987fbc9-4bed-3078-cf07-9141ba07c9f3", "must be a valid UUID v4"},
+		{"UUIDv5", UUIDv5, "987fbc97-4bed-5078-af07-9141ba07c9f3", "b987fbc9-4bed-3078-cf07-9141ba07c9f3", "must be a valid UUID v5"},
+		{"MongoID", MongoID, "507f1f77bcf86cd799439011", "507f1f77bcf86cd79943901", "must be a valid hex-encoded MongoDB ObjectId"},
+		{"CreditCard", CreditCard, "375556917985515", "375556917985516", "must be a valid credit card number"},
+		{"JSON", JSON, "[1, 2]", "[1, 2,]", "must be in valid JSON format"},
+		{"ASCII", ASCII, "abc", "aabc", "must contain ASCII characters only"},
+		{"PrintableASCII", PrintableASCII, "abc", "aabc", "must contain printable ASCII characters only"},
+		{"E164", E164, "+8618964826225", "+0001", "must be a valid E164 number"},
+		{"CountryCode2", CountryCode2, "US", "XY", "must be a valid two-letter country code"},
+		{"CountryCode3", CountryCode3, "USA", "XYZ", "must be a valid three-letter country code"},
+		{"CurrencyCode", CurrencyCode, "USD", "USS", "must be valid ISO 4217 currency code"},
+		{"DialString", DialString, "localhost.local:1", "localhost.loc:100000", "must be a valid dial string"},
+		{"DataURI", DataURI, "data:image/png;base64,TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=", "image/gif;base64,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", "must be a Base64-encoded data URI"},
+		{"Base64", Base64, "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=", "image", "must be encoded in Base64"},
+		{"Multibyte", Multibyte, "abc", "abc", "must contain multibyte characters"},
+		{"FullWidth", FullWidth, "3ー0", "abc", "must contain full-width characters"},
+		{"HalfWidth", HalfWidth, "abc123い", "0011", "must contain half-width characters"},
+		{"VariableWidth", VariableWidth, "3ー0123", "abc", "must contain both full-width and half-width characters"},
+		{"Hexadecimal", Hexadecimal, "FEF", "FTF", "must be a valid hexadecimal number"},
+		{"HexColor", HexColor, "F00", "FTF", "must be a valid hexadecimal color code"},
+		{"RGBColor", RGBColor, "rgb(100, 200, 1)", "abc", "must be a valid RGB color code"},
+		{"Int", Int, "100", "1.1", "must be an integer number"},
+		{"Float", Float, "1.1", "a.1", "must be a floating point number"},
+		{"VariableWidth", VariableWidth, "", "", ""},
+	}
+
+	for _, test := range tests {
+		err := test.rule.Validate("")
+		assert.Nil(t, err, test.tag)
+		err = test.rule.Validate(test.valid)
+		assert.Nil(t, err, test.tag)
+		err = test.rule.Validate(&test.valid)
+		assert.Nil(t, err, test.tag)
+		err = test.rule.Validate(test.invalid)
+		assertError(t, test.err, err, test.tag)
+		err = test.rule.Validate(&test.invalid)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func assertError(t *testing.T, expected string, err error, tag string) {
+	if expected == "" {
+		assert.Nil(t, err, tag)
+	} else if assert.NotNil(t, err, tag) {
+		assert.Equal(t, expected, err.Error(), tag)
+	}
+}

+ 144 - 0
pkg/valid/map.go

@@ -0,0 +1,144 @@
+package valid
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"reflect"
+)
+
+var (
+	// ErrNotMap is the error that the value being validated is not a map.
+	ErrNotMap = errors.New("only a map can be validated")
+
+	// ErrKeyWrongType is the error returned in case of an incorrect key type.
+	ErrKeyWrongType = NewError("validation_key_wrong_type", "key not the correct type")
+
+	// ErrKeyMissing is the error returned in case of a missing key.
+	ErrKeyMissing = NewError("validation_key_missing", "required key is missing")
+
+	// ErrKeyUnexpected is the error returned in case of an unexpected key.
+	ErrKeyUnexpected = NewError("validation_key_unexpected", "key not expected")
+)
+
+type (
+	// MapRule represents a rule set associated with a map.
+	MapRule struct {
+		keys           []*KeyRules
+		allowExtraKeys bool
+	}
+
+	// KeyRules represents a rule set associated with a map key.
+	KeyRules struct {
+		key      interface{}
+		optional bool
+		rules    []Rule
+	}
+)
+
+// Map returns a validation rule that checks the keys and values of a map.
+// This rule should only be used for validating maps, or a validation error will be reported.
+// Use Key() to specify map keys that need to be validated. Each Key() call specifies a single key which can
+// be associated with multiple rules.
+// For example,
+//    validation.Map(
+//        validation.Key("Name", validation.Required),
+//        validation.Key("Value", validation.Required, validation.Length(5, 10)),
+//    )
+//
+// A nil value is considered valid. Use the Required rule to make sure a map value is present.
+func Map(keys ...*KeyRules) MapRule {
+	return MapRule{keys: keys}
+}
+
+// AllowExtraKeys configures the rule to ignore extra keys.
+func (r MapRule) AllowExtraKeys() MapRule {
+	r.allowExtraKeys = true
+	return r
+}
+
+// Validate checks if the given value is valid or not.
+func (r MapRule) Validate(m interface{}) error {
+	return r.ValidateWithContext(nil, m)
+}
+
+// ValidateWithContext checks if the given value is valid or not.
+func (r MapRule) ValidateWithContext(ctx context.Context, m interface{}) error {
+	value := reflect.ValueOf(m)
+	if value.Kind() == reflect.Ptr {
+		value = value.Elem()
+	}
+	if value.Kind() != reflect.Map {
+		// must be a map
+		return NewInternalError(ErrNotMap)
+	}
+	if value.IsNil() {
+		// treat a nil map as valid
+		return nil
+	}
+
+	errs := Errors{}
+	kt := value.Type().Key()
+
+	var extraKeys map[interface{}]bool
+	if !r.allowExtraKeys {
+		extraKeys = make(map[interface{}]bool, value.Len())
+		for _, k := range value.MapKeys() {
+			extraKeys[k.Interface()] = true
+		}
+	}
+
+	for _, kr := range r.keys {
+		var err error
+		if kv := reflect.ValueOf(kr.key); !kt.AssignableTo(kv.Type()) {
+			err = ErrKeyWrongType
+		} else if vv := value.MapIndex(kv); !vv.IsValid() {
+			if !kr.optional {
+				err = ErrKeyMissing
+			}
+		} else if ctx == nil {
+			err = Validate(vv.Interface(), kr.rules...)
+		} else {
+			err = ValidateWithContext(ctx, vv.Interface(), kr.rules...)
+		}
+		if err != nil {
+			if ie, ok := err.(InternalError); ok && ie.InternalError() != nil {
+				return err
+			}
+			errs[getErrorKeyName(kr.key)] = err
+		}
+		if !r.allowExtraKeys {
+			delete(extraKeys, kr.key)
+		}
+	}
+
+	if !r.allowExtraKeys {
+		for key := range extraKeys {
+			errs[getErrorKeyName(key)] = ErrKeyUnexpected
+		}
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+// Key specifies a map key and the corresponding validation rules.
+func Key(key interface{}, rules ...Rule) *KeyRules {
+	return &KeyRules{
+		key:   key,
+		rules: rules,
+	}
+}
+
+// Optional configures the rule to ignore the key if missing.
+func (r *KeyRules) Optional() *KeyRules {
+	r.optional = true
+	return r
+}
+
+// getErrorKeyName returns the name that should be used to represent the validation error of a map key.
+func getErrorKeyName(key interface{}) string {
+	return fmt.Sprintf("%v", key)
+}

+ 119 - 0
pkg/valid/map_test.go

@@ -0,0 +1,119 @@
+package valid
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMap(t *testing.T) {
+	var m0 map[string]interface{}
+	m1 := map[string]interface{}{"A": "abc", "B": "xyz", "c": "abc", "D": (*string)(nil), "F": (*String123)(nil), "H": []string{"abc", "abc"}, "I": map[string]string{"foo": "abc"}}
+	m2 := map[string]interface{}{"E": String123("xyz"), "F": (*String123)(nil)}
+	m3 := map[string]interface{}{"M3": Model3{}}
+	m4 := map[string]interface{}{"M3": Model3{A: "abc"}}
+	m5 := map[string]interface{}{"A": "internal", "B": ""}
+	m6 := map[int]string{11: "abc", 22: "xyz"}
+	tests := []struct {
+		tag   string
+		model interface{}
+		rules []*KeyRules
+		err   string
+	}{
+		// empty rules
+		{"t1.1", m1, []*KeyRules{}, ""},
+		{"t1.2", m1, []*KeyRules{Key("A"), Key("B")}, ""},
+		// normal rules
+		{"t2.1", m1, []*KeyRules{Key("A", &validateAbc{}), Key("B", &validateXyz{})}, ""},
+		{"t2.2", m1, []*KeyRules{Key("A", &validateXyz{}), Key("B", &validateAbc{})}, "A: error xyz; B: error abc."},
+		{"t2.3", m1, []*KeyRules{Key("A", &validateXyz{}), Key("c", &validateXyz{})}, "A: error xyz; c: error xyz."},
+		{"t2.4", m1, []*KeyRules{Key("D", Length(0, 5))}, ""},
+		{"t2.5", m1, []*KeyRules{Key("F", Length(0, 5))}, ""},
+		{"t2.6", m1, []*KeyRules{Key("H", Each(&validateAbc{})), Key("I", Each(&validateAbc{}))}, ""},
+		{"t2.7", m1, []*KeyRules{Key("H", Each(&validateXyz{})), Key("I", Each(&validateXyz{}))}, "H: (0: error xyz; 1: error xyz.); I: (foo: error xyz.)."},
+		{"t2.8", m1, []*KeyRules{Key("I", Map(Key("foo", &validateAbc{})))}, ""},
+		{"t2.9", m1, []*KeyRules{Key("I", Map(Key("foo", &validateXyz{})))}, "I: (foo: error xyz.)."},
+		// non-map value
+		{"t3.1", &m1, []*KeyRules{}, ""},
+		{"t3.2", nil, []*KeyRules{}, ErrNotMap.Error()},
+		{"t3.3", m0, []*KeyRules{}, ""},
+		{"t3.4", &m0, []*KeyRules{}, ""},
+		{"t3.5", 123, []*KeyRules{}, ErrNotMap.Error()},
+		// invalid key spec
+		{"t4.1", m1, []*KeyRules{Key(123)}, "123: key not the correct type."},
+		{"t4.2", m1, []*KeyRules{Key("X")}, "X: required key is missing."},
+		{"t4.3", m1, []*KeyRules{Key("X").Optional()}, ""},
+		// non-string keys
+		{"t5.1", m6, []*KeyRules{Key(11, &validateAbc{}), Key(22, &validateXyz{})}, ""},
+		{"t5.2", m6, []*KeyRules{Key(11, &validateXyz{}), Key(22, &validateAbc{})}, "11: error xyz; 22: error abc."},
+		// validatable value
+		{"t6.1", m2, []*KeyRules{Key("E")}, "E: error 123."},
+		{"t6.2", m2, []*KeyRules{Key("E", Skip)}, ""},
+		{"t6.3", m2, []*KeyRules{Key("E", Skip.When(true))}, ""},
+		{"t6.4", m2, []*KeyRules{Key("E", Skip.When(false))}, "E: error 123."},
+		// Required, NotNil
+		{"t7.1", m2, []*KeyRules{Key("F", Required)}, "F: cannot be blank."},
+		{"t7.2", m2, []*KeyRules{Key("F", NotNil)}, "F: is required."},
+		{"t7.3", m2, []*KeyRules{Key("F", Skip, Required)}, ""},
+		{"t7.4", m2, []*KeyRules{Key("F", Skip, NotNil)}, ""},
+		{"t7.5", m2, []*KeyRules{Key("F", Skip.When(true), Required)}, ""},
+		{"t7.6", m2, []*KeyRules{Key("F", Skip.When(true), NotNil)}, ""},
+		{"t7.7", m2, []*KeyRules{Key("F", Skip.When(false), Required)}, "F: cannot be blank."},
+		{"t7.8", m2, []*KeyRules{Key("F", Skip.When(false), NotNil)}, "F: is required."},
+		// validatable structs
+		{"t8.1", m3, []*KeyRules{Key("M3", Skip)}, ""},
+		{"t8.2", m3, []*KeyRules{Key("M3")}, "M3: (A: error abc.)."},
+		{"t8.3", m4, []*KeyRules{Key("M3")}, ""},
+		// internal error
+		{"t9.1", m5, []*KeyRules{Key("A", &validateAbc{}), Key("B", Required), Key("A", &validateInternalError{})}, "error internal"},
+	}
+	for _, test := range tests {
+		err1 := Validate(test.model, Map(test.rules...).AllowExtraKeys())
+		err2 := ValidateWithContext(context.Background(), test.model, Map(test.rules...).AllowExtraKeys())
+		assertError(t, test.err, err1, test.tag)
+		assertError(t, test.err, err2, test.tag)
+	}
+
+	a := map[string]interface{}{"Name": "name", "Value": "demo", "Extra": true}
+	err := Validate(a, Map(
+		Key("Name", Required),
+		Key("Value", Required, Length(5, 10)),
+	))
+	assert.EqualError(t, err, "Extra: key not expected; Value: the length must be between 5 and 10.")
+}
+
+func TestMapWithContext(t *testing.T) {
+	m1 := map[string]interface{}{"A": "abc", "B": "xyz", "c": "abc", "g": "xyz"}
+	m2 := map[string]interface{}{"A": "internal", "B": ""}
+	tests := []struct {
+		tag   string
+		model interface{}
+		rules []*KeyRules
+		err   string
+	}{
+		// normal rules
+		{"t1.1", m1, []*KeyRules{Key("A", &validateContextAbc{}), Key("B", &validateContextXyz{})}, ""},
+		{"t1.2", m1, []*KeyRules{Key("A", &validateContextXyz{}), Key("B", &validateContextAbc{})}, "A: error xyz; B: error abc."},
+		{"t1.3", m1, []*KeyRules{Key("A", &validateContextXyz{}), Key("c", &validateContextXyz{})}, "A: error xyz; c: error xyz."},
+		{"t1.4", m1, []*KeyRules{Key("g", &validateContextAbc{})}, "g: error abc."},
+		// skip rule
+		{"t2.1", m1, []*KeyRules{Key("g", Skip, &validateContextAbc{})}, ""},
+		{"t2.2", m1, []*KeyRules{Key("g", &validateContextAbc{}, Skip)}, "g: error abc."},
+		// internal error
+		{"t3.1", m2, []*KeyRules{Key("A", &validateContextAbc{}), Key("B", Required), Key("A", &validateInternalError{})}, "error internal"},
+	}
+	for _, test := range tests {
+		err := ValidateWithContext(context.Background(), test.model, Map(test.rules...).AllowExtraKeys())
+		assertError(t, test.err, err, test.tag)
+	}
+
+	a := map[string]interface{}{"Name": "name", "Value": "demo", "Extra": true}
+	err := ValidateWithContext(context.Background(), a, Map(
+		Key("Name", Required),
+		Key("Value", Required, Length(5, 10)),
+	))
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "Extra: key not expected; Value: the length must be between 5 and 10.", err.Error())
+	}
+}

+ 25 - 0
pkg/valid/rule.go

@@ -0,0 +1,25 @@
+package valid
+
+var (
+	// ErrNilOrNotEmpty is the error that returns when a value is not nil and is empty.
+	ErrNilOrNotEmpty = NewError("validation_nil_or_not_empty_required", "cannot be blank")
+)
+
+var (
+	// Skip is a special validation rule that indicates all rules following it should be skipped.
+	Skip = skipRule{skip: true}
+)
+
+type skipRule struct {
+	skip bool
+}
+
+func (r skipRule) Validate(interface{}) error {
+	return nil
+}
+
+// When determines if all rules following it should be skipped.
+func (r skipRule) When(condition bool) skipRule {
+	r.skip = condition
+	return r
+}

+ 63 - 0
pkg/valid/rule_absent.go

@@ -0,0 +1,63 @@
+package valid
+
+var (
+	// ErrNil is the error that returns when a value is not nil.
+	ErrNil = NewError("validation_nil", "must be blank")
+	// ErrEmpty is the error that returns when a not nil value is not empty.
+	ErrEmpty = NewError("validation_empty", "must be blank")
+)
+
+// Nil is a validation rule that checks if a value is nil.
+// It is the opposite of NotNil rule
+var Nil = absentRule{condition: true, skipNil: false}
+
+// Empty checks if a not nil value is empty.
+var Empty = absentRule{condition: true, skipNil: true}
+
+type absentRule struct {
+	condition bool
+	err       Error
+	skipNil   bool
+}
+
+// Validate checks if the given value is valid or not.
+func (r absentRule) Validate(value interface{}) error {
+	if r.condition {
+		value, isNil := Indirect(value)
+		if !r.skipNil && !isNil || r.skipNil && !isNil && !IsEmpty(value) {
+			if r.err != nil {
+				return r.err
+			}
+			if r.skipNil {
+				return ErrEmpty
+			}
+			return ErrNil
+		}
+	}
+	return nil
+}
+
+// When sets the condition that determines if the validation should be performed.
+func (r absentRule) When(condition bool) absentRule {
+	r.condition = condition
+	return r
+}
+
+// Error sets the error message for the rule.
+func (r absentRule) Error(message string) absentRule {
+	if r.err == nil {
+		if r.skipNil {
+			r.err = ErrEmpty
+		} else {
+			r.err = ErrNil
+		}
+	}
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r absentRule) ErrorObject(err Error) absentRule {
+	r.err = err
+	return r
+}

+ 100 - 0
pkg/valid/rule_absent_test.go

@@ -0,0 +1,100 @@
+package valid
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNil(t *testing.T) {
+	s1 := "123"
+	s2 := ""
+	var time1 time.Time
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", 123, "must be blank"},
+		{"t2", "", "must be blank"},
+		{"t3", &s1, "must be blank"},
+		{"t4", &s2, "must be blank"},
+		{"t5", nil, ""},
+		{"t6", time1, "must be blank"},
+	}
+
+	for _, test := range tests {
+		r := Nil
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestEmpty(t *testing.T) {
+	s1 := "123"
+	s2 := ""
+	time1 := time.Now()
+	var time2 time.Time
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", 123, "must be blank"},
+		{"t2", "", ""},
+		{"t3", &s1, "must be blank"},
+		{"t4", &s2, ""},
+		{"t5", nil, ""},
+		{"t6", time1, "must be blank"},
+		{"t7", time2, ""},
+	}
+
+	for _, test := range tests {
+		r := Empty
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestAbsentRule_When(t *testing.T) {
+	r := Nil.When(false)
+	err := Validate(42, r)
+	assert.Nil(t, err)
+
+	r = Nil.When(true)
+	err = Validate(42, r)
+	assert.Equal(t, ErrNil, err)
+}
+
+func Test_absentRule_Error(t *testing.T) {
+	r := Nil
+	assert.Equal(t, "must be blank", r.Validate("42").Error())
+	assert.False(t, r.skipNil)
+	r2 := r.Error("123")
+	assert.Equal(t, "must be blank", r.Validate("42").Error())
+	assert.False(t, r.skipNil)
+	assert.Equal(t, "123", r2.err.Message())
+	assert.False(t, r2.skipNil)
+
+	r = Empty
+	assert.Equal(t, "must be blank", r.Validate("42").Error())
+	assert.True(t, r.skipNil)
+	r2 = r.Error("123")
+	assert.Equal(t, "must be blank", r.Validate("42").Error())
+	assert.True(t, r.skipNil)
+	assert.Equal(t, "123", r2.err.Message())
+	assert.True(t, r2.skipNil)
+}
+
+func TestAbsentRule_Error(t *testing.T) {
+	r := Nil
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+	assert.NotEqual(t, err, Nil.err)
+}

+ 98 - 0
pkg/valid/rule_date.go

@@ -0,0 +1,98 @@
+package valid
+
+import (
+	"time"
+)
+
+var (
+	// ErrDateInvalid is the error that returns in case of an invalid date.
+	ErrDateInvalid = NewError("validation_date_invalid", "must be a valid date")
+	// ErrDateOutOfRange is the error that returns in case of an invalid date.
+	ErrDateOutOfRange = NewError("validation_date_out_of_range", "the date is out of range")
+)
+
+// DateRule is a validation rule that validates date/time string values.
+type DateRule struct {
+	layout        string
+	min, max      time.Time
+	err, rangeErr Error
+}
+
+// Date returns a validation rule that checks if a string value is in a format that can be parsed into a date.
+// The format of the date should be specified as the layout parameter which accepts the same value as that for time.Parse.
+// For example,
+//    validation.Date(time.ANSIC)
+//    validation.Date("02 Jan 06 15:04 MST")
+//    validation.Date("2006-01-02")
+//
+// By calling Min() and/or Max(), you can let the Date rule to check if a parsed date value is within
+// the specified date range.
+//
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+func Date(layout string) DateRule {
+	return DateRule{
+		layout:   layout,
+		err:      ErrDateInvalid,
+		rangeErr: ErrDateOutOfRange,
+	}
+}
+
+// Error sets the error message that is used when the value being validated is not a valid date.
+func (r DateRule) Error(message string) DateRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct that is used when the value being validated is not a valid date..
+func (r DateRule) ErrorObject(err Error) DateRule {
+	r.err = err
+	return r
+}
+
+// RangeError sets the error message that is used when the value being validated is out of the specified Min/Max date range.
+func (r DateRule) RangeError(message string) DateRule {
+	r.rangeErr = r.rangeErr.SetMessage(message)
+	return r
+}
+
+// RangeErrorObject sets the error struct that is used when the value being validated is out of the specified Min/Max date range.
+func (r DateRule) RangeErrorObject(err Error) DateRule {
+	r.rangeErr = err
+	return r
+}
+
+// Min sets the minimum date range. A zero value means skipping the minimum range validation.
+func (r DateRule) Min(min time.Time) DateRule {
+	r.min = min
+	return r
+}
+
+// Max sets the maximum date range. A zero value means skipping the maximum range validation.
+func (r DateRule) Max(max time.Time) DateRule {
+	r.max = max
+	return r
+}
+
+// Validate checks if the given value is a valid date.
+func (r DateRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil || IsEmpty(value) {
+		return nil
+	}
+
+	str, err := EnsureString(value)
+	if err != nil {
+		return err
+	}
+
+	date, err := time.Parse(r.layout, str)
+	if err != nil {
+		return r.err
+	}
+
+	if !r.min.IsZero() && r.min.After(date) || !r.max.IsZero() && date.After(r.max) {
+		return r.rangeErr
+	}
+
+	return nil
+}

+ 87 - 0
pkg/valid/rule_date_test.go

@@ -0,0 +1,87 @@
+package valid
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDate(t *testing.T) {
+	tests := []struct {
+		tag    string
+		layout string
+		value  interface{}
+		err    string
+	}{
+		{"t1", time.ANSIC, "", ""},
+		{"t2", time.ANSIC, "Wed Feb  4 21:00:57 2009", ""},
+		{"t3", time.ANSIC, "Wed Feb  29 21:00:57 2009", "must be a valid date"},
+		{"t4", "2006-01-02", "2009-11-12", ""},
+		{"t5", "2006-01-02", "2009-11-12 21:00:57", "must be a valid date"},
+		{"t6", "2006-01-02", "2009-1-12", "must be a valid date"},
+		{"t7", "2006-01-02", "2009-01-12", ""},
+		{"t8", "2006-01-02", "2009-01-32", "must be a valid date"},
+		{"t9", "2006-01-02", 1, "must be either a string or byte slice"},
+	}
+
+	for _, test := range tests {
+		r := Date(test.layout)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestDateRule_Error(t *testing.T) {
+	r := Date(time.RFC3339)
+	assert.Equal(t, "must be a valid date", r.Validate("0001-01-02T15:04:05Z07:00").Error())
+	r2 := r.Min(time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC))
+	assert.Equal(t, "the date is out of range", r2.Validate("1999-01-02T15:04:05Z").Error())
+	r = r.Error("123")
+	r = r.RangeError("456")
+	assert.Equal(t, "123", r.err.Message())
+	assert.Equal(t, "456", r.rangeErr.Message())
+}
+
+func TestDateRule_ErrorObject(t *testing.T) {
+	r := Date(time.RFC3339)
+	assert.Equal(t, "must be a valid date", r.Validate("0001-01-02T15:04:05Z07:00").Error())
+
+	r = r.ErrorObject(NewError("code", "abc"))
+
+	assert.Equal(t, "code", r.err.Code())
+	assert.Equal(t, "abc", r.Validate("0001-01-02T15:04:05Z07:00").Error())
+
+	r2 := r.Min(time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC))
+	assert.Equal(t, "the date is out of range", r2.Validate("1999-01-02T15:04:05Z").Error())
+
+	r = r.ErrorObject(NewError("C", "def"))
+	r = r.RangeErrorObject(NewError("D", "123"))
+
+	assert.Equal(t, "C", r.err.Code())
+	assert.Equal(t, "def", r.err.Message())
+	assert.Equal(t, "D", r.rangeErr.Code())
+	assert.Equal(t, "123", r.rangeErr.Message())
+}
+
+func TestDateRule_MinMax(t *testing.T) {
+	r := Date(time.ANSIC)
+	assert.True(t, r.min.IsZero())
+	assert.True(t, r.max.IsZero())
+	r = r.Min(time.Now())
+	assert.False(t, r.min.IsZero())
+	assert.True(t, r.max.IsZero())
+	r = r.Max(time.Now())
+	assert.False(t, r.max.IsZero())
+
+	r2 := Date("2006-01-02").Min(time.Date(2000, 12, 1, 0, 0, 0, 0, time.UTC)).Max(time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC))
+	assert.Nil(t, r2.Validate("2010-01-02"))
+	err := r2.Validate("1999-01-02")
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "the date is out of range", err.Error())
+	}
+	err = r2.Validate("2021-01-02")
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "the date is out of range", err.Error())
+	}
+}

+ 93 - 0
pkg/valid/rule_each.go

@@ -0,0 +1,93 @@
+package valid
+
+import (
+	"context"
+	"errors"
+	"reflect"
+	"strconv"
+)
+
+// Each returns a validation rule that loops through an iterable (map, slice or array)
+// and validates each value inside with the provided rules.
+// An empty iterable is considered valid. Use the Required rule to make sure the iterable is not empty.
+func Each(rules ...Rule) EachRule {
+	return EachRule{
+		rules: rules,
+	}
+}
+
+// EachRule is a validation rule that validates elements in a map/slice/array using the specified list of rules.
+type EachRule struct {
+	rules []Rule
+}
+
+// Validate loops through the given iterable and calls the Ozzo Validate() method for each value.
+func (r EachRule) Validate(value interface{}) error {
+	return r.ValidateWithContext(nil, value)
+}
+
+// ValidateWithContext loops through the given iterable and calls the Ozzo ValidateWithContext() method for each value.
+func (r EachRule) ValidateWithContext(ctx context.Context, value interface{}) error {
+	errs := Errors{}
+
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.Map:
+		for _, k := range v.MapKeys() {
+			val := r.getInterface(v.MapIndex(k))
+			var err error
+			if ctx == nil {
+				err = Validate(val, r.rules...)
+			} else {
+				err = ValidateWithContext(ctx, val, r.rules...)
+			}
+			if err != nil {
+				errs[r.getString(k)] = err
+			}
+		}
+	case reflect.Slice, reflect.Array:
+		for i := 0; i < v.Len(); i++ {
+			val := r.getInterface(v.Index(i))
+			var err error
+			if ctx == nil {
+				err = Validate(val, r.rules...)
+			} else {
+				err = ValidateWithContext(ctx, val, r.rules...)
+			}
+			if err != nil {
+				errs[strconv.Itoa(i)] = err
+			}
+		}
+	default:
+		return errors.New("must be an iterable (map, slice or array)")
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+func (r EachRule) getInterface(value reflect.Value) interface{} {
+	switch value.Kind() {
+	case reflect.Ptr, reflect.Interface:
+		if value.IsNil() {
+			return nil
+		}
+		return value.Elem().Interface()
+	default:
+		return value.Interface()
+	}
+}
+
+func (r EachRule) getString(value reflect.Value) string {
+	switch value.Kind() {
+	case reflect.Ptr, reflect.Interface:
+		if value.IsNil() {
+			return ""
+		}
+		return value.Elem().String()
+	default:
+		return value.String()
+	}
+}

+ 74 - 0
pkg/valid/rule_each_test.go

@@ -0,0 +1,74 @@
+package valid
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"testing"
+)
+
+func TestEach(t *testing.T) {
+	var a *int
+	var f = func(v string) string { return v }
+	var c0 chan int
+	c1 := make(chan int)
+
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", nil, "must be an iterable (map, slice or array)"},
+		{"t2", map[string]string{}, ""},
+		{"t3", map[string]string{"key1": "value1", "key2": "value2"}, ""},
+		{"t4", map[string]string{"key1": "", "key2": "value2", "key3": ""}, "key1: cannot be blank; key3: cannot be blank."},
+		{"t5", map[string]map[string]string{"key1": {"key1.1": "value1"}, "key2": {"key2.1": "value1"}}, ""},
+		{"t6", map[string]map[string]string{"": nil}, ": cannot be blank."},
+		{"t7", map[interface{}]interface{}{}, ""},
+		{"t8", map[interface{}]interface{}{"key1": struct{ foo string }{"foo"}}, ""},
+		{"t9", map[interface{}]interface{}{nil: "", "": "", "key1": nil}, ": cannot be blank; key1: cannot be blank."},
+		{"t10", []string{"value1", "value2", "value3"}, ""},
+		{"t11", []string{"", "value2", ""}, "0: cannot be blank; 2: cannot be blank."},
+		{"t12", []interface{}{struct{ foo string }{"foo"}}, ""},
+		{"t13", []interface{}{nil, a}, "0: cannot be blank; 1: cannot be blank."},
+		{"t14", []interface{}{c0, c1, f}, "0: cannot be blank."},
+	}
+
+	for _, test := range tests {
+		r := Each(Required)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestEachWithContext(t *testing.T) {
+	rule := Each(WithContext(func(ctx context.Context, value interface{}) error {
+		if !strings.Contains(value.(string), ctx.Value("contains").(string)) {
+			return errors.New("unexpected value")
+		}
+		return nil
+	}))
+	ctx1 := context.WithValue(context.Background(), "contains", "abc")
+	ctx2 := context.WithValue(context.Background(), "contains", "xyz")
+
+	tests := []struct {
+		tag   string
+		value interface{}
+		ctx   context.Context
+		err   string
+	}{
+		{"t1.1", map[string]string{"key": "abc"}, ctx1, ""},
+		{"t1.2", map[string]string{"key": "abc"}, ctx2, "key: unexpected value."},
+		{"t1.3", map[string]string{"key": "xyz"}, ctx1, "key: unexpected value."},
+		{"t1.4", map[string]string{"key": "xyz"}, ctx2, ""},
+		{"t1.5", []string{"abc"}, ctx1, ""},
+		{"t1.6", []string{"abc"}, ctx2, "0: unexpected value."},
+		{"t1.7", []string{"xyz"}, ctx1, "0: unexpected value."},
+		{"t1.8", []string{"xyz"}, ctx2, ""},
+	}
+
+	for _, test := range tests {
+		err := ValidateWithContext(test.ctx, test.value, rule)
+		assertError(t, test.err, err, test.tag)
+	}
+}

+ 51 - 0
pkg/valid/rule_in.go

@@ -0,0 +1,51 @@
+package valid
+
+import "reflect"
+
+// ErrInInvalid is the error that returns in case of an invalid value for "in" rule.
+var ErrInInvalid = NewError("validation_in_invalid", "must be a valid value")
+
+// In returns a validation rule that checks if a value can be found in the given list of values.
+// reflect.DeepEqual() will be used to determine if two values are equal.
+// For more details please refer to https://golang.org/pkg/reflect/#DeepEqual
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+func In(values ...interface{}) InRule {
+	return InRule{
+		elements: values,
+		err:      ErrInInvalid,
+	}
+}
+
+// InRule is a validation rule that validates if a value can be found in the given list of values.
+type InRule struct {
+	elements []interface{}
+	err      Error
+}
+
+// Validate checks if the given value is valid or not.
+func (r InRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil || IsEmpty(value) {
+		return nil
+	}
+
+	for _, e := range r.elements {
+		if reflect.DeepEqual(e, value) {
+			return nil
+		}
+	}
+
+	return r.err
+}
+
+// Error sets the error message for the rule.
+func (r InRule) Error(message string) InRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r InRule) ErrorObject(err Error) InRule {
+	r.err = err
+	return r
+}

+ 53 - 0
pkg/valid/rule_in_test.go

@@ -0,0 +1,53 @@
+package valid
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIn(t *testing.T) {
+	var v = 1
+	var v2 *int
+	tests := []struct {
+		tag    string
+		values []interface{}
+		value  interface{}
+		err    string
+	}{
+		{"t0", []interface{}{1, 2}, 0, ""},
+		{"t1", []interface{}{1, 2}, 1, ""},
+		{"t2", []interface{}{1, 2}, 2, ""},
+		{"t3", []interface{}{1, 2}, 3, "must be a valid value"},
+		{"t4", []interface{}{}, 3, "must be a valid value"},
+		{"t5", []interface{}{1, 2}, "1", "must be a valid value"},
+		{"t6", []interface{}{1, 2}, &v, ""},
+		{"t7", []interface{}{1, 2}, v2, ""},
+		{"t8", []interface{}{[]byte{1}, 1, 2}, []byte{1}, ""},
+	}
+
+	for _, test := range tests {
+		r := In(test.values...)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_InRule_Error(t *testing.T) {
+	r := In(1, 2, 3)
+	val := 4
+	assert.Equal(t, "must be a valid value", r.Validate(&val).Error())
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestInRule_ErrorObject(t *testing.T) {
+	r := In(1, 2, 3)
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+}

+ 100 - 0
pkg/valid/rule_length.go

@@ -0,0 +1,100 @@
+package valid
+
+import (
+	"unicode/utf8"
+)
+
+var (
+	// ErrLengthTooLong is the error that returns in case of too long length.
+	ErrLengthTooLong = NewError("validation_length_too_long", "the length must be no more than {{.max}}")
+	// ErrLengthTooShort is the error that returns in case of too short length.
+	ErrLengthTooShort = NewError("validation_length_too_short", "the length must be no less than {{.min}}")
+	// ErrLengthInvalid is the error that returns in case of an invalid length.
+	ErrLengthInvalid = NewError("validation_length_invalid", "the length must be exactly {{.min}}")
+	// ErrLengthOutOfRange is the error that returns in case of out of range length.
+	ErrLengthOutOfRange = NewError("validation_length_out_of_range", "the length must be between {{.min}} and {{.max}}")
+	// ErrLengthEmptyRequired is the error that returns in case of non-empty value.
+	ErrLengthEmptyRequired = NewError("validation_length_empty_required", "the value must be empty")
+)
+
+// Length returns a validation rule that checks if a value's length is within the specified range.
+// If max is 0, it means there is no upper bound for the length.
+// This rule should only be used for validating strings, slices, maps, and arrays.
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+func Length(min, max int) LengthRule {
+	return LengthRule{min: min, max: max, err: buildLengthRuleError(min, max)}
+}
+
+// RuneLength returns a validation rule that checks if a string's rune length is within the specified range.
+// If max is 0, it means there is no upper bound for the length.
+// This rule should only be used for validating strings, slices, maps, and arrays.
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+// If the value being validated is not a string, the rule works the same as Length.
+func RuneLength(min, max int) LengthRule {
+	r := Length(min, max)
+	r.rune = true
+
+	return r
+}
+
+// LengthRule is a validation rule that checks if a value's length is within the specified range.
+type LengthRule struct {
+	err Error
+
+	min, max int
+	rune     bool
+}
+
+// Validate checks if the given value is valid or not.
+func (r LengthRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil || IsEmpty(value) {
+		return nil
+	}
+
+	var (
+		l   int
+		err error
+	)
+	if s, ok := value.(string); ok && r.rune {
+		l = utf8.RuneCountInString(s)
+	} else if l, err = LengthOfValue(value); err != nil {
+		return err
+	}
+
+	if r.min > 0 && l < r.min || r.max > 0 && l > r.max || r.min == 0 && r.max == 0 && l > 0 {
+		return r.err
+	}
+
+	return nil
+}
+
+// Error sets the error message for the rule.
+func (r LengthRule) Error(message string) LengthRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r LengthRule) ErrorObject(err Error) LengthRule {
+	r.err = err
+	return r
+}
+
+func buildLengthRuleError(min, max int) (err Error) {
+	if min == 0 && max > 0 {
+		err = ErrLengthTooLong
+	} else if min > 0 && max == 0 {
+		err = ErrLengthTooShort
+	} else if min > 0 && max > 0 {
+		if min == max {
+			err = ErrLengthInvalid
+		} else {
+			err = ErrLengthOutOfRange
+		}
+	} else {
+		err = ErrLengthEmptyRequired
+	}
+
+	return err.SetParams(map[string]interface{}{"min": min, "max": max})
+}

+ 103 - 0
pkg/valid/rule_length_test.go

@@ -0,0 +1,103 @@
+package valid
+
+import (
+	"database/sql"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLength(t *testing.T) {
+	var v *string
+	tests := []struct {
+		tag      string
+		min, max int
+		value    interface{}
+		err      string
+	}{
+		{"t1", 2, 4, "abc", ""},
+		{"t2", 2, 4, "", ""},
+		{"t3", 2, 4, "abcdf", "the length must be between 2 and 4"},
+		{"t4", 0, 4, "ab", ""},
+		{"t5", 0, 4, "abcde", "the length must be no more than 4"},
+		{"t6", 2, 0, "ab", ""},
+		{"t7", 2, 0, "a", "the length must be no less than 2"},
+		{"t8", 2, 0, v, ""},
+		{"t9", 2, 0, 123, "cannot get the length of int"},
+		{"t10", 2, 4, sql.NullString{String: "abc", Valid: true}, ""},
+		{"t11", 2, 4, sql.NullString{String: "", Valid: true}, ""},
+		{"t12", 2, 4, &sql.NullString{String: "abc", Valid: true}, ""},
+		{"t13", 2, 2, "abcdf", "the length must be exactly 2"},
+		{"t14", 2, 2, "ab", ""},
+		{"t15", 0, 0, "", ""},
+		{"t16", 0, 0, "ab", "the value must be empty"},
+	}
+
+	for _, test := range tests {
+		r := Length(test.min, test.max)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestRuneLength(t *testing.T) {
+	var v *string
+	tests := []struct {
+		tag      string
+		min, max int
+		value    interface{}
+		err      string
+	}{
+		{"t1", 2, 4, "abc", ""},
+		{"t1.1", 2, 3, "💥💥", ""},
+		{"t1.2", 2, 3, "💥💥💥", ""},
+		{"t1.3", 2, 3, "💥", "the length must be between 2 and 3"},
+		{"t1.4", 2, 3, "💥💥💥💥", "the length must be between 2 and 3"},
+		{"t2", 2, 4, "", ""},
+		{"t3", 2, 4, "abcdf", "the length must be between 2 and 4"},
+		{"t4", 0, 4, "ab", ""},
+		{"t5", 0, 4, "abcde", "the length must be no more than 4"},
+		{"t6", 2, 0, "ab", ""},
+		{"t7", 2, 0, "a", "the length must be no less than 2"},
+		{"t8", 2, 0, v, ""},
+		{"t9", 2, 0, 123, "cannot get the length of int"},
+		{"t10", 2, 4, sql.NullString{String: "abc", Valid: true}, ""},
+		{"t11", 2, 4, sql.NullString{String: "", Valid: true}, ""},
+		{"t12", 2, 4, &sql.NullString{String: "abc", Valid: true}, ""},
+		{"t13", 2, 3, &sql.NullString{String: "💥💥", Valid: true}, ""},
+		{"t14", 2, 3, &sql.NullString{String: "💥", Valid: true}, "the length must be between 2 and 3"},
+	}
+
+	for _, test := range tests {
+		r := RuneLength(test.min, test.max)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_LengthRule_Error(t *testing.T) {
+	r := Length(10, 20)
+	assert.Equal(t, "the length must be between 10 and 20", r.Validate("abc").Error())
+
+	r = Length(0, 20)
+	assert.Equal(t, "the length must be no more than 20", r.Validate(make([]string, 21)).Error())
+
+	r = Length(10, 0)
+	assert.Equal(t, "the length must be no less than 10", r.Validate([9]string{}).Error())
+
+	r = Length(0, 0)
+	assert.Equal(t, "validation_length_empty_required", r.err.Code())
+
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestLengthRule_ErrorObject(t *testing.T) {
+	r := Length(10, 20)
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+}

+ 50 - 0
pkg/valid/rule_match.go

@@ -0,0 +1,50 @@
+package valid
+
+import "regexp"
+
+// ErrMatchInvalid is the error that returns in case of invalid format.
+var ErrMatchInvalid = NewError("validation_match_invalid", "must be in a valid format")
+
+// Match returns a validation rule that checks if a value matches the specified regular expression.
+// This rule should only be used for validating strings and byte slices, or a validation error will be reported.
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+func Match(re *regexp.Regexp) MatchRule {
+	return MatchRule{
+		re:  re,
+		err: ErrMatchInvalid,
+	}
+}
+
+// MatchRule is a validation rule that checks if a value matches the specified regular expression.
+type MatchRule struct {
+	re  *regexp.Regexp
+	err Error
+}
+
+// Validate checks if the given value is valid or not.
+func (r MatchRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil {
+		return nil
+	}
+
+	isString, str, isBytes, bs := StringOrBytes(value)
+	if isString && (str == "" || r.re.MatchString(str)) {
+		return nil
+	} else if isBytes && (len(bs) == 0 || r.re.Match(bs)) {
+		return nil
+	}
+	return r.err
+}
+
+// Error sets the error message for the rule.
+func (r MatchRule) Error(message string) MatchRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r MatchRule) ErrorObject(err Error) MatchRule {
+	r.err = err
+	return r
+}

+ 51 - 0
pkg/valid/rule_match_test.go

@@ -0,0 +1,51 @@
+package valid
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMatch(t *testing.T) {
+	var v2 *string
+	tests := []struct {
+		tag   string
+		re    string
+		value interface{}
+		err   string
+	}{
+		{"t1", "[a-z]+", "abc", ""},
+		{"t2", "[a-z]+", "", ""},
+		{"t3", "[a-z]+", v2, ""},
+		{"t4", "[a-z]+", "123", "must be in a valid format"},
+		{"t5", "[a-z]+", []byte("abc"), ""},
+		{"t6", "[a-z]+", []byte("123"), "must be in a valid format"},
+		{"t7", "[a-z]+", []byte(""), ""},
+		{"t8", "[a-z]+", nil, ""},
+	}
+
+	for _, test := range tests {
+		r := Match(regexp.MustCompile(test.re))
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_MatchRule_Error(t *testing.T) {
+	r := Match(regexp.MustCompile("[a-z]+"))
+	assert.Equal(t, "must be in a valid format", r.Validate("13").Error())
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestMatchRule_ErrorObject(t *testing.T) {
+	r := Match(regexp.MustCompile("[a-z]+"))
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+}

+ 191 - 0
pkg/valid/rule_minmax.go

@@ -0,0 +1,191 @@
+package valid
+
+import (
+	"fmt"
+	"reflect"
+	"time"
+)
+
+var (
+	// ErrMinGreaterEqualThanRequired is the error that returns when a value is less than a specified threshold.
+	ErrMinGreaterEqualThanRequired = NewError("validation_min_greater_equal_than_required", "must be no less than {{.threshold}}")
+	// ErrMaxLessEqualThanRequired is the error that returns when a value is greater than a specified threshold.
+	ErrMaxLessEqualThanRequired = NewError("validation_max_less_equal_than_required", "must be no greater than {{.threshold}}")
+	// ErrMinGreaterThanRequired is the error that returns when a value is less than or equal to a specified threshold.
+	ErrMinGreaterThanRequired = NewError("validation_min_greater_than_required", "must be greater than {{.threshold}}")
+	// ErrMaxLessThanRequired is the error that returns when a value is greater than or equal to a specified threshold.
+	ErrMaxLessThanRequired = NewError("validation_max_less_than_required", "must be less than {{.threshold}}")
+)
+
+// ThresholdRule is a validation rule that checks if a value satisfies the specified threshold requirement.
+type ThresholdRule struct {
+	threshold interface{}
+	operator  int
+	err       Error
+}
+
+const (
+	greaterThan = iota
+	greaterEqualThan
+	lessThan
+	lessEqualThan
+)
+
+// Min returns a validation rule that checks if a value is greater or equal than the specified value.
+// By calling Exclusive, the rule will check if the value is strictly greater than the specified value.
+// Note that the value being checked and the threshold value must be of the same type.
+// Only int, uint, float and time.Time types are supported.
+// An empty value is considered valid. Please use the Required rule to make sure a value is not empty.
+func Min(min interface{}) ThresholdRule {
+	return ThresholdRule{
+		threshold: min,
+		operator:  greaterEqualThan,
+		err:       ErrMinGreaterEqualThanRequired,
+	}
+
+}
+
+// Max returns a validation rule that checks if a value is less or equal than the specified value.
+// By calling Exclusive, the rule will check if the value is strictly less than the specified value.
+// Note that the value being checked and the threshold value must be of the same type.
+// Only int, uint, float and time.Time types are supported.
+// An empty value is considered valid. Please use the Required rule to make sure a value is not empty.
+func Max(max interface{}) ThresholdRule {
+	return ThresholdRule{
+		threshold: max,
+		operator:  lessEqualThan,
+		err:       ErrMaxLessEqualThanRequired,
+	}
+}
+
+// Exclusive sets the comparison to exclude the boundary value.
+func (r ThresholdRule) Exclusive() ThresholdRule {
+	if r.operator == greaterEqualThan {
+		r.operator = greaterThan
+		r.err = ErrMinGreaterThanRequired
+	} else if r.operator == lessEqualThan {
+		r.operator = lessThan
+		r.err = ErrMaxLessThanRequired
+	}
+	return r
+}
+
+// Validate checks if the given value is valid or not.
+func (r ThresholdRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil || IsEmpty(value) {
+		return nil
+	}
+
+	rv := reflect.ValueOf(r.threshold)
+	switch rv.Kind() {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		v, err := ToInt(value)
+		if err != nil {
+			return err
+		}
+		if r.compareInt(rv.Int(), v) {
+			return nil
+		}
+
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		v, err := ToUint(value)
+		if err != nil {
+			return err
+		}
+		if r.compareUint(rv.Uint(), v) {
+			return nil
+		}
+
+	case reflect.Float32, reflect.Float64:
+		v, err := ToFloat(value)
+		if err != nil {
+			return err
+		}
+		if r.compareFloat(rv.Float(), v) {
+			return nil
+		}
+
+	case reflect.Struct:
+		t, ok := r.threshold.(time.Time)
+		if !ok {
+			return fmt.Errorf("type not supported: %v", rv.Type())
+		}
+		v, ok := value.(time.Time)
+		if !ok {
+			return fmt.Errorf("cannot convert %v to time.Time", reflect.TypeOf(value))
+		}
+		if v.IsZero() || r.compareTime(t, v) {
+			return nil
+		}
+
+	default:
+		return fmt.Errorf("type not supported: %v", rv.Type())
+	}
+
+	return r.err.SetParams(map[string]interface{}{"threshold": r.threshold})
+}
+
+// Error sets the error message for the rule.
+func (r ThresholdRule) Error(message string) ThresholdRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r ThresholdRule) ErrorObject(err Error) ThresholdRule {
+	r.err = err
+	return r
+}
+
+func (r ThresholdRule) compareInt(threshold, value int64) bool {
+	switch r.operator {
+	case greaterThan:
+		return value > threshold
+	case greaterEqualThan:
+		return value >= threshold
+	case lessThan:
+		return value < threshold
+	default:
+		return value <= threshold
+	}
+}
+
+func (r ThresholdRule) compareUint(threshold, value uint64) bool {
+	switch r.operator {
+	case greaterThan:
+		return value > threshold
+	case greaterEqualThan:
+		return value >= threshold
+	case lessThan:
+		return value < threshold
+	default:
+		return value <= threshold
+	}
+}
+
+func (r ThresholdRule) compareFloat(threshold, value float64) bool {
+	switch r.operator {
+	case greaterThan:
+		return value > threshold
+	case greaterEqualThan:
+		return value >= threshold
+	case lessThan:
+		return value < threshold
+	default:
+		return value <= threshold
+	}
+}
+
+func (r ThresholdRule) compareTime(threshold, value time.Time) bool {
+	switch r.operator {
+	case greaterThan:
+		return value.After(threshold)
+	case greaterEqualThan:
+		return value.After(threshold) || value.Equal(threshold)
+	case lessThan:
+		return value.Before(threshold)
+	default:
+		return value.Before(threshold) || value.Equal(threshold)
+	}
+}

+ 144 - 0
pkg/valid/rule_minmax_test.go

@@ -0,0 +1,144 @@
+package valid
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMin(t *testing.T) {
+	date0 := time.Time{}
+	date20000101 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+	date20001201 := time.Date(2000, 12, 1, 0, 0, 0, 0, time.UTC)
+	date20000601 := time.Date(2000, 6, 1, 0, 0, 0, 0, time.UTC)
+
+	tests := []struct {
+		tag       string
+		threshold interface{}
+		exclusive bool
+		value     interface{}
+		err       string
+	}{
+		// int cases
+		{"t1.1", 1, false, 1, ""},
+		{"t1.2", 1, false, 2, ""},
+		{"t1.3", 1, false, -1, "must be no less than 1"},
+		{"t1.4", 1, false, 0, ""},
+		{"t1.5", 1, true, 1, "must be greater than 1"},
+		{"t1.6", 1, false, "1", "cannot convert string to int64"},
+		{"t1.7", "1", false, 1, "type not supported: string"},
+		// uint cases
+		{"t2.1", uint(2), false, uint(2), ""},
+		{"t2.2", uint(2), false, uint(3), ""},
+		{"t2.3", uint(2), false, uint(1), "must be no less than 2"},
+		{"t2.4", uint(2), false, uint(0), ""},
+		{"t2.5", uint(2), true, uint(2), "must be greater than 2"},
+		{"t2.6", uint(2), false, "1", "cannot convert string to uint64"},
+		// float cases
+		{"t3.1", float64(2), false, float64(2), ""},
+		{"t3.2", float64(2), false, float64(3), ""},
+		{"t3.3", float64(2), false, float64(1), "must be no less than 2"},
+		{"t3.4", float64(2), false, float64(0), ""},
+		{"t3.5", float64(2), true, float64(2), "must be greater than 2"},
+		{"t3.6", float64(2), false, "1", "cannot convert string to float64"},
+		// Time cases
+		{"t4.1", date20000601, false, date20000601, ""},
+		{"t4.2", date20000601, false, date20001201, ""},
+		{"t4.3", date20000601, false, date20000101, "must be no less than 2000-06-01 00:00:00 +0000 UTC"},
+		{"t4.4", date20000601, false, date0, ""},
+		{"t4.5", date20000601, true, date20000601, "must be greater than 2000-06-01 00:00:00 +0000 UTC"},
+		{"t4.6", date20000601, true, 1, "cannot convert int to time.Time"},
+		{"t4.7", struct{}{}, false, 1, "type not supported: struct {}"},
+		{"t4.8", date0, false, date20000601, ""},
+	}
+
+	for _, test := range tests {
+		r := Min(test.threshold)
+		if test.exclusive {
+			r = r.Exclusive()
+		}
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestMinError(t *testing.T) {
+	r := Min(10)
+	assert.Equal(t, "must be no less than 10", r.Validate(9).Error())
+
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestMax(t *testing.T) {
+	date0 := time.Time{}
+	date20000101 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+	date20001201 := time.Date(2000, 12, 1, 0, 0, 0, 0, time.UTC)
+	date20000601 := time.Date(2000, 6, 1, 0, 0, 0, 0, time.UTC)
+
+	tests := []struct {
+		tag       string
+		threshold interface{}
+		exclusive bool
+		value     interface{}
+		err       string
+	}{
+		// int cases
+		{"t1.1", 2, false, 2, ""},
+		{"t1.2", 2, false, 1, ""},
+		{"t1.3", 2, false, 3, "must be no greater than 2"},
+		{"t1.4", 2, false, 0, ""},
+		{"t1.5", 2, true, 2, "must be less than 2"},
+		{"t1.6", 2, false, "1", "cannot convert string to int64"},
+		{"t1.7", "1", false, 1, "type not supported: string"},
+		// uint cases
+		{"t2.1", uint(2), false, uint(2), ""},
+		{"t2.2", uint(2), false, uint(1), ""},
+		{"t2.3", uint(2), false, uint(3), "must be no greater than 2"},
+		{"t2.4", uint(2), false, uint(0), ""},
+		{"t2.5", uint(2), true, uint(2), "must be less than 2"},
+		{"t2.6", uint(2), false, "1", "cannot convert string to uint64"},
+		// float cases
+		{"t3.1", float64(2), false, float64(2), ""},
+		{"t3.2", float64(2), false, float64(1), ""},
+		{"t3.3", float64(2), false, float64(3), "must be no greater than 2"},
+		{"t3.4", float64(2), false, float64(0), ""},
+		{"t3.5", float64(2), true, float64(2), "must be less than 2"},
+		{"t3.6", float64(2), false, "1", "cannot convert string to float64"},
+		// Time cases
+		{"t4.1", date20000601, false, date20000601, ""},
+		{"t4.2", date20000601, false, date20000101, ""},
+		{"t4.3", date20000601, false, date20001201, "must be no greater than 2000-06-01 00:00:00 +0000 UTC"},
+		{"t4.4", date20000601, false, date0, ""},
+		{"t4.5", date20000601, true, date20000601, "must be less than 2000-06-01 00:00:00 +0000 UTC"},
+		{"t4.6", date20000601, true, 1, "cannot convert int to time.Time"},
+	}
+
+	for _, test := range tests {
+		r := Max(test.threshold)
+		if test.exclusive {
+			r = r.Exclusive()
+		}
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestMaxError(t *testing.T) {
+	r := Max(10)
+	assert.Equal(t, "must be no greater than 10", r.Validate(11).Error())
+
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestThresholdRule_ErrorObject(t *testing.T) {
+	r := Max(10)
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+}

+ 47 - 0
pkg/valid/rule_not_in.go

@@ -0,0 +1,47 @@
+package valid
+
+// ErrNotInInvalid is the error that returns when a value is in a list.
+var ErrNotInInvalid = NewError("validation_not_in_invalid", "must not be in list")
+
+// NotIn returns a validation rule that checks if a value is absent from the given list of values.
+// Note that the value being checked and the possible range of values must be of the same type.
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+func NotIn(values ...interface{}) NotInRule {
+	return NotInRule{
+		elements: values,
+		err:      ErrNotInInvalid,
+	}
+}
+
+// NotInRule is a validation rule that checks if a value is absent from the given list of values.
+type NotInRule struct {
+	elements []interface{}
+	err      Error
+}
+
+// Validate checks if the given value is valid or not.
+func (r NotInRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil || IsEmpty(value) {
+		return nil
+	}
+
+	for _, e := range r.elements {
+		if e == value {
+			return r.err
+		}
+	}
+	return nil
+}
+
+// Error sets the error message for the rule.
+func (r NotInRule) Error(message string) NotInRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r NotInRule) ErrorObject(err Error) NotInRule {
+	r.err = err
+	return r
+}

+ 51 - 0
pkg/valid/rule_not_in_test.go

@@ -0,0 +1,51 @@
+package valid
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNotIn(t *testing.T) {
+	v := 1
+	var v2 *int
+	var tests = []struct {
+		tag    string
+		values []interface{}
+		value  interface{}
+		err    string
+	}{
+		{"t0", []interface{}{1, 2}, 0, ""},
+		{"t1", []interface{}{1, 2}, 1, "must not be in list"},
+		{"t2", []interface{}{1, 2}, 2, "must not be in list"},
+		{"t3", []interface{}{1, 2}, 3, ""},
+		{"t4", []interface{}{}, 3, ""},
+		{"t5", []interface{}{1, 2}, "1", ""},
+		{"t6", []interface{}{1, 2}, &v, "must not be in list"},
+		{"t7", []interface{}{1, 2}, v2, ""},
+	}
+
+	for _, test := range tests {
+		r := NotIn(test.values...)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_NotInRule_Error(t *testing.T) {
+	r := NotIn(1, 2, 3)
+	assert.Equal(t, "must not be in list", r.Validate(1).Error())
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestNotInRule_ErrorObject(t *testing.T) {
+	r := NotIn(1, 2, 3)
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+}

+ 40 - 0
pkg/valid/rule_not_nil.go

@@ -0,0 +1,40 @@
+package valid
+
+// ErrNotNilRequired is the error that returns when a value is Nil.
+var ErrNotNilRequired = NewError("validation_not_nil_required", "is required")
+
+// NotNil is a validation rule that checks if a value is not nil.
+// NotNil only handles types including interface, pointer, slice, and map.
+// All other types are considered valid.
+var NotNil = notNilRule{}
+
+type notNilRule struct {
+	err Error
+}
+
+// Validate checks if the given value is valid or not.
+func (r notNilRule) Validate(value interface{}) error {
+	_, isNil := Indirect(value)
+	if isNil {
+		if r.err != nil {
+			return r.err
+		}
+		return ErrNotNilRequired
+	}
+	return nil
+}
+
+// Error sets the error message for the rule.
+func (r notNilRule) Error(message string) notNilRule {
+	if r.err == nil {
+		r.err = ErrNotNilRequired
+	}
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r notNilRule) ErrorObject(err Error) notNilRule {
+	r.err = err
+	return r
+}

+ 58 - 0
pkg/valid/rule_not_nil_test.go

@@ -0,0 +1,58 @@
+package valid
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type MyInterface interface {
+	Hello()
+}
+
+func TestNotNil(t *testing.T) {
+	var v1 []int
+	var v2 map[string]int
+	var v3 *int
+	var v4 interface{}
+	var v5 MyInterface
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", v1, "is required"},
+		{"t2", v2, "is required"},
+		{"t3", v3, "is required"},
+		{"t4", v4, "is required"},
+		{"t5", v5, "is required"},
+		{"t6", "", ""},
+		{"t7", 0, ""},
+	}
+
+	for _, test := range tests {
+		r := NotNil
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_notNilRule_Error(t *testing.T) {
+	r := NotNil
+	assert.Equal(t, "is required", r.Validate(nil).Error())
+	r2 := r.Error("123")
+	assert.Equal(t, "is required", r.Validate(nil).Error())
+	assert.Equal(t, "123", r2.err.Message())
+}
+
+func TestNotNilRule_ErrorObject(t *testing.T) {
+	r := NotNil
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+	assert.NotEqual(t, err, NotNil.err)
+}

+ 68 - 0
pkg/valid/rule_required.go

@@ -0,0 +1,68 @@
+package valid
+
+var (
+	// Required is a validation rule that checks if a value is not empty.
+	// A value is considered not empty if
+	// - integer, float: not zero
+	// - bool: true
+	// - string, array, slice, map: len() > 0
+	// - interface, pointer: not nil and the referenced value is not empty
+	// - any other types
+	Required = RequiredRule{skipNil: false, condition: true}
+
+	// NilOrNotEmpty checks if a value is a nil pointer or a value that is not empty.
+	// NilOrNotEmpty differs from Required in that it treats a nil pointer as valid.
+	NilOrNotEmpty = RequiredRule{skipNil: true, condition: true}
+
+	// ErrRequired is the error that returns when a value is required.
+	ErrRequired = NewError("validation_required", "cannot be blank")
+)
+
+// RequiredRule is a rule that checks if a value is not empty.
+type RequiredRule struct {
+	condition bool
+	skipNil   bool
+	err       Error
+}
+
+// Validate checks if the given value is valid or not.
+func (r RequiredRule) Validate(value interface{}) error {
+	if r.condition {
+		value, isNil := Indirect(value)
+		if r.skipNil && !isNil && IsEmpty(value) || !r.skipNil && (isNil || IsEmpty(value)) {
+			if r.err != nil {
+				return r.err
+			}
+			if r.skipNil {
+				return ErrNilOrNotEmpty
+			}
+			return ErrRequired
+		}
+	}
+	return nil
+}
+
+// When sets the condition that determines if the validation should be performed.
+func (r RequiredRule) When(condition bool) RequiredRule {
+	r.condition = condition
+	return r
+}
+
+// Error sets the error message for the rule.
+func (r RequiredRule) Error(message string) RequiredRule {
+	if r.err == nil {
+		if r.skipNil {
+			r.err = ErrNilOrNotEmpty
+		} else {
+			r.err = ErrRequired
+		}
+	}
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r RequiredRule) ErrorObject(err Error) RequiredRule {
+	r.err = err
+	return r
+}

+ 96 - 0
pkg/valid/rule_required_test.go

@@ -0,0 +1,96 @@
+package valid
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRequired(t *testing.T) {
+	s1 := "123"
+	s2 := ""
+	var time1 time.Time
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", 123, ""},
+		{"t2", "", "cannot be blank"},
+		{"t3", &s1, ""},
+		{"t4", &s2, "cannot be blank"},
+		{"t5", nil, "cannot be blank"},
+		{"t6", time1, "cannot be blank"},
+	}
+
+	for _, test := range tests {
+		r := Required
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestRequiredRule_When(t *testing.T) {
+	r := Required.When(false)
+	err := Validate(nil, r)
+	assert.Nil(t, err)
+
+	r = Required.When(true)
+	err = Validate(nil, r)
+	assert.Equal(t, ErrRequired, err)
+}
+
+func TestNilOrNotEmpty(t *testing.T) {
+	s1 := "123"
+	s2 := ""
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", 123, ""},
+		{"t2", "", "cannot be blank"},
+		{"t3", &s1, ""},
+		{"t4", &s2, "cannot be blank"},
+		{"t5", nil, ""},
+	}
+
+	for _, test := range tests {
+		r := NilOrNotEmpty
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_requiredRule_Error(t *testing.T) {
+	r := Required
+	assert.Equal(t, "cannot be blank", r.Validate(nil).Error())
+	assert.False(t, r.skipNil)
+	r2 := r.Error("123")
+	assert.Equal(t, "cannot be blank", r.Validate(nil).Error())
+	assert.False(t, r.skipNil)
+	assert.Equal(t, "123", r2.err.Message())
+	assert.False(t, r2.skipNil)
+
+	r = NilOrNotEmpty
+	assert.Equal(t, "cannot be blank", r.Validate("").Error())
+	assert.True(t, r.skipNil)
+	r2 = r.Error("123")
+	assert.Equal(t, "cannot be blank", r.Validate("").Error())
+	assert.True(t, r.skipNil)
+	assert.Equal(t, "123", r2.err.Message())
+	assert.True(t, r2.skipNil)
+}
+
+func TestRequiredRule_Error(t *testing.T) {
+	r := Required
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+	assert.NotEqual(t, err, Required.err)
+}

+ 79 - 0
pkg/valid/rule_strings.go

@@ -0,0 +1,79 @@
+package valid
+
+var (
+	// ErrBlankString is the error that returns when a value is an empty string/only whitespaces.
+	ErrBlankString = NewError("validation_required", "cannot be blank")
+)
+
+type stringValidator func(string) bool
+
+// StringRule is a rule that checks a string variable using a specified stringValidator.
+type StringRule struct {
+	validate  stringValidator
+	err       Error
+	denyBlank bool
+}
+
+// NewStringRule creates a new validation rule using a function that takes a string value and returns a bool.
+// The rule returned will use the function to check if a given string or byte slice is valid or not.
+// An empty value is considered to be valid. Please use the Required rule to make sure a value is not empty.
+func NewStringRule(allowBlank bool, validator stringValidator, message string) StringRule {
+	return StringRule{
+		validate:  validator,
+		err:       NewError("", message),
+		denyBlank: !allowBlank,
+	}
+}
+
+// NewStringRuleWithError creates a new validation rule using a function that takes a string value and returns a bool.
+// The rule returned will use the function to check if a given string or byte slice is valid or not.
+// An empty value is considered to be valid. Please use the Required rule to make sure a value is not empty.
+func NewStringRuleWithError(validator stringValidator, err Error) StringRule {
+	return StringRule{
+		validate: validator,
+		err:      err,
+	}
+}
+
+// Error sets the error message for the rule.
+func (r StringRule) Error(message string) StringRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r StringRule) ErrorObject(err Error) StringRule {
+	r.err = err
+	return r
+}
+
+// Validate checks if the given value is valid or not.
+func (r StringRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil {
+		return nil
+	}
+
+	str, err := EnsureString(value)
+	if err != nil {
+		return err
+	}
+	if r.denyBlank && IsEmpty(value) {
+		return nil
+	}
+	if !r.denyBlank {
+		if IsEmpty(value) {
+			return nil
+		}
+	} else {
+		if IsBlankString(str) {
+			return ErrBlankString
+		}
+	}
+
+	if r.validate(str) {
+		return nil
+	}
+
+	return r.err
+}

+ 139 - 0
pkg/valid/rule_strings_test.go

@@ -0,0 +1,139 @@
+package valid
+
+import (
+	"database/sql"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func validateMe(s string) bool {
+	return s == "me"
+}
+
+func TestNewStringRule(t *testing.T) {
+	v := NewStringRule(true, validateMe, "abc")
+
+	assert.NotNil(t, v.validate)
+	assert.Equal(t, "", v.err.Code())
+	assert.Equal(t, "abc", v.err.Message())
+}
+
+func TestNewStringRuleWithError(t *testing.T) {
+	err := NewError("C", "abc")
+	v := NewStringRuleWithError(validateMe, err)
+
+	assert.NotNil(t, v.validate)
+	assert.Equal(t, err, v.err)
+	assert.Equal(t, "C", v.err.Code())
+	assert.Equal(t, "abc", v.err.Message())
+}
+
+func TestStringRule_Error(t *testing.T) {
+	err := NewError("code", "abc")
+	v := NewStringRuleWithError(validateMe, err).Error("abc")
+	assert.Equal(t, "code", v.err.Code())
+	assert.Equal(t, "abc", v.err.Message())
+
+	v2 := v.Error("correct")
+	assert.Equal(t, "code", v.err.Code())
+	assert.Equal(t, "correct", v2.err.Message())
+	assert.Equal(t, "abc", v.err.Message())
+}
+
+func TestStringValidator_Validate(t *testing.T) {
+	t.Run("allow blank", func(t *testing.T) {
+		v := NewStringRule(true, validateMe, "wrong_rule").Error("wrong")
+
+		value := "me"
+
+		err := v.Validate(value)
+		assert.Nil(t, err)
+
+		err = v.Validate(&value)
+		assert.Nil(t, err)
+
+		value = ""
+
+		err = v.Validate(value)
+		assert.Nil(t, err)
+
+		err = v.Validate(&value)
+		assert.Nil(t, err)
+
+		nullValue := sql.NullString{String: "me", Valid: true}
+		err = v.Validate(nullValue)
+		assert.Nil(t, err)
+
+		nullValue = sql.NullString{String: "", Valid: true}
+		err = v.Validate(nullValue)
+		assert.Nil(t, err)
+
+		var s *string
+		err = v.Validate(s)
+		assert.Nil(t, err)
+
+		err = v.Validate("not me")
+		if assert.NotNil(t, err) {
+			assert.Equal(t, "wrong", err.Error())
+		}
+
+		err = v.Validate(100)
+		if assert.NotNil(t, err) {
+			assert.NotEqual(t, "wrong", err.Error())
+		}
+
+		v2 := v.Error("Wrong!")
+		err = v2.Validate("not me")
+		if assert.NotNil(t, err) {
+			assert.Equal(t, "Wrong!", err.Error())
+		}
+	})
+
+	t.Run("deny blank", func(t *testing.T) {
+		v := NewStringRule(false, validateMe, "abc")
+
+		err := v.Validate(" ")
+		if assert.NotNil(t, err) {
+			assert.Equal(t, "cannot be blank", err.Error())
+		}
+	})
+}
+
+func TestGetErrorFieldName(t *testing.T) {
+	type A struct {
+		T0 string
+		T1 string `json:"t1"`
+		T2 string `json:"t2,omitempty"`
+		T3 string `json:",omitempty"`
+		T4 string `json:"t4,x1,omitempty"`
+	}
+	tests := []struct {
+		tag   string
+		field string
+		name  string
+	}{
+		{"t1", "T0", "T0"},
+		{"t2", "T1", "t1"},
+		{"t3", "T2", "t2"},
+		{"t4", "T3", "T3"},
+		{"t5", "T4", "t4"},
+	}
+	a := reflect.TypeOf(A{})
+	for _, test := range tests {
+		field, _ := a.FieldByName(test.field)
+		assert.Equal(t, test.name, getErrorFieldName(&field), test.tag)
+	}
+}
+
+func TestStringRule_ErrorObject(t *testing.T) {
+	r := NewStringRule(true, validateMe, "wrong_rule")
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, "code", r.err.Code())
+	assert.Equal(t, "abc", r.err.Message())
+}

+ 47 - 0
pkg/valid/rule_when.go

@@ -0,0 +1,47 @@
+package valid
+
+import "context"
+
+// When returns a validation rule that executes the given list of rules when the condition is true.
+func When(condition bool, rules ...Rule) WhenRule {
+	return WhenRule{
+		condition: condition,
+		rules:     rules,
+		elseRules: []Rule{},
+	}
+}
+
+// WhenRule is a validation rule that executes the given list of rules when the condition is true.
+type WhenRule struct {
+	condition bool
+	rules     []Rule
+	elseRules []Rule
+}
+
+// Validate checks if the condition is true and if so, it validates the value using the specified rules.
+func (r WhenRule) Validate(value interface{}) error {
+	return r.ValidateWithContext(nil, value)
+}
+
+// ValidateWithContext checks if the condition is true and if so, it validates the value using the specified rules.
+func (r WhenRule) ValidateWithContext(ctx context.Context, value interface{}) error {
+	if r.condition {
+		if ctx == nil {
+			return Validate(value, r.rules...)
+		} else {
+			return ValidateWithContext(ctx, value, r.rules...)
+		}
+	}
+
+	if ctx == nil {
+		return Validate(value, r.elseRules...)
+	} else {
+		return ValidateWithContext(ctx, value, r.elseRules...)
+	}
+}
+
+// Else returns a validation rule that executes the given list of rules when the condition is false.
+func (r WhenRule) Else(rules ...Rule) WhenRule {
+	r.elseRules = rules
+	return r
+}

+ 90 - 0
pkg/valid/rule_when_test.go

@@ -0,0 +1,90 @@
+package valid
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"testing"
+)
+
+func abcValidation(val string) bool {
+	return val == "abc"
+}
+
+func TestWhen(t *testing.T) {
+	abcRule := NewStringRule(true, abcValidation, "wrong_abc")
+	validateMeRule := NewStringRule(true, validateMe, "wrong_me")
+
+	tests := []struct {
+		tag       string
+		condition bool
+		value     interface{}
+		rules     []Rule
+		elseRules []Rule
+		err       string
+	}{
+		// True condition
+		{"t1.1", true, nil, []Rule{}, []Rule{}, ""},
+		{"t1.2", true, "", []Rule{}, []Rule{}, ""},
+		{"t1.3", true, "", []Rule{abcRule}, []Rule{}, ""},
+		{"t1.4", true, 12, []Rule{Required}, []Rule{}, ""},
+		{"t1.5", true, nil, []Rule{Required}, []Rule{}, "cannot be blank"},
+		{"t1.6", true, "123", []Rule{abcRule}, []Rule{}, "wrong_abc"},
+		{"t1.7", true, "abc", []Rule{abcRule}, []Rule{}, ""},
+		{"t1.8", true, "abc", []Rule{abcRule, abcRule}, []Rule{}, ""},
+		{"t1.9", true, "abc", []Rule{abcRule, validateMeRule}, []Rule{}, "wrong_me"},
+		{"t1.10", true, "me", []Rule{abcRule, validateMeRule}, []Rule{}, "wrong_abc"},
+		{"t1.11", true, "me", []Rule{}, []Rule{abcRule}, ""},
+
+		// False condition
+		{"t2.1", false, "", []Rule{}, []Rule{}, ""},
+		{"t2.2", false, "", []Rule{abcRule}, []Rule{}, ""},
+		{"t2.3", false, "abc", []Rule{abcRule}, []Rule{}, ""},
+		{"t2.4", false, "abc", []Rule{abcRule, abcRule}, []Rule{}, ""},
+		{"t2.5", false, "abc", []Rule{abcRule, validateMeRule}, []Rule{}, ""},
+		{"t2.6", false, "me", []Rule{abcRule, validateMeRule}, []Rule{}, ""},
+		{"t2.7", false, "", []Rule{abcRule, validateMeRule}, []Rule{}, ""},
+		{"t2.8", false, "me", []Rule{}, []Rule{abcRule, validateMeRule}, "wrong_abc"},
+	}
+
+	for _, test := range tests {
+		err := Validate(test.value, When(test.condition, test.rules...).Else(test.elseRules...))
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestWhenWithContext(t *testing.T) {
+	rule := WithContext(func(ctx context.Context, value interface{}) error {
+		if !strings.Contains(value.(string), ctx.Value("contains").(string)) {
+			return errors.New("unexpected value")
+		}
+		return nil
+	})
+	ctx1 := context.WithValue(context.Background(), "contains", "abc")
+	ctx2 := context.WithValue(context.Background(), "contains", "xyz")
+
+	tests := []struct {
+		tag       string
+		condition bool
+		value     interface{}
+		ctx       context.Context
+		err       string
+	}{
+		// True condition
+		{"t1.1", true, "abc", ctx1, ""},
+		{"t1.2", true, "abc", ctx2, "unexpected value"},
+		{"t1.3", true, "xyz", ctx1, "unexpected value"},
+		{"t1.4", true, "xyz", ctx2, ""},
+
+		// False condition
+		{"t2.1", false, "abc", ctx1, ""},
+		{"t2.2", false, "abc", ctx2, "unexpected value"},
+		{"t2.3", false, "xyz", ctx1, "unexpected value"},
+		{"t2.4", false, "xyz", ctx2, ""},
+	}
+
+	for _, test := range tests {
+		err := ValidateWithContext(test.ctx, test.value, When(test.condition, rule).Else(rule))
+		assertError(t, test.err, err, test.tag)
+	}
+}

+ 140 - 0
pkg/valid/struct.go

@@ -0,0 +1,140 @@
+package valid
+
+import (
+	"context"
+	"reflect"
+	"strings"
+)
+
+// ValidateStruct validates a struct by checking the specified struct fields against the corresponding validation rules.
+// Note that the struct being validated must be specified as a pointer to it. If the pointer is nil, it is considered valid.
+// Use Field() to specify struct fields that need to be validated. Each Field() call specifies a single field which
+// should be specified as a pointer to the field. A field can be associated with multiple rules.
+// For example,
+//
+//    value := struct {
+//        Name  string
+//        Value string
+//    }{"name", "demo"}
+//    err := validation.ValidateStruct(&value,
+//        validation.Field(&a.Name, validation.Required),
+//        validation.Field(&a.Value, validation.Required, validation.Length(5, 10)),
+//    )
+//    fmt.Println(err)
+//    // Value: the length must be between 5 and 10.
+//
+// An error will be returned if validation fails.
+func ValidateStruct(structPtr interface{}, fields ...*FieldRules) error {
+	return ValidateStructWithContext(nil, structPtr, fields...)
+}
+
+// ValidateStructWithContext validates a struct with the given context.
+// The only difference between ValidateStructWithContext and ValidateStruct is that the former will
+// validate struct fields with the provided context.
+// Please refer to ValidateStruct for the detailed instructions on how to use this function.
+func ValidateStructWithContext(ctx context.Context, structPtr interface{}, fields ...*FieldRules) error {
+	value := reflect.ValueOf(structPtr)
+	if value.Kind() != reflect.Ptr || !value.IsNil() && value.Elem().Kind() != reflect.Struct {
+		// must be a pointer to a struct
+		return NewInternalError(ErrStructPointer)
+	}
+	if value.IsNil() {
+		// treat a nil struct pointer as valid
+		return nil
+	}
+	value = value.Elem()
+
+	errs := Errors{}
+
+	for i, fr := range fields {
+		fv := reflect.ValueOf(fr.fieldPtr)
+		if fv.Kind() != reflect.Ptr {
+			return NewInternalError(ErrFieldPointer(i))
+		}
+		ft := findStructField(value, fv)
+		if ft == nil {
+			return NewInternalError(ErrFieldNotFound(i))
+		}
+		var err error
+		if ctx == nil {
+			err = Validate(fv.Elem().Interface(), fr.rules...)
+		} else {
+			err = ValidateWithContext(ctx, fv.Elem().Interface(), fr.rules...)
+		}
+		if err != nil {
+			if ie, ok := err.(InternalError); ok && ie.InternalError() != nil {
+				return err
+			}
+			if ft.Anonymous {
+				// merge errors from anonymous struct field
+				if es, ok := err.(Errors); ok {
+					for name, value := range es {
+						errs[name] = value
+					}
+					continue
+				}
+			}
+			errs[getErrorFieldName(ft)] = err
+		}
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+// FieldRules represents a rule set associated with a struct field.
+type FieldRules struct {
+	fieldPtr interface{}
+	rules    []Rule
+}
+
+// Field specifies a struct field and the corresponding validation rules.
+// The struct field must be specified as a pointer to it.
+func Field(fieldPtr interface{}, rules ...Rule) *FieldRules {
+	return &FieldRules{
+		fieldPtr: fieldPtr,
+		rules:    rules,
+	}
+}
+
+// findStructField looks for a field in the given struct.
+// The field being looked for should be a pointer to the actual struct field.
+// If found, the field info will be returned. Otherwise, nil will be returned.
+func findStructField(structValue reflect.Value, fieldValue reflect.Value) *reflect.StructField {
+	ptr := fieldValue.Pointer()
+	for i := structValue.NumField() - 1; i >= 0; i-- {
+		sf := structValue.Type().Field(i)
+		if ptr == structValue.Field(i).UnsafeAddr() {
+			// do additional type comparison because it's possible that the address of
+			// an embedded struct is the same as the first field of the embedded struct
+			if sf.Type == fieldValue.Elem().Type() {
+				return &sf
+			}
+		}
+		if sf.Anonymous {
+			// delve into anonymous struct to look for the field
+			fi := structValue.Field(i)
+			if sf.Type.Kind() == reflect.Ptr {
+				fi = fi.Elem()
+			}
+			if fi.Kind() == reflect.Struct {
+				if f := findStructField(fi, fieldValue); f != nil {
+					return f
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// getErrorFieldName returns the name that should be used to represent the validation error of a struct field.
+func getErrorFieldName(f *reflect.StructField) string {
+	if tag := f.Tag.Get(ErrorTag); tag != "" && tag != "-" {
+		if cps := strings.SplitN(tag, ",", 2); cps[0] != "" {
+			return cps[0]
+		}
+	}
+	return f.Name
+}

+ 204 - 0
pkg/valid/struct_test.go

@@ -0,0 +1,204 @@
+package valid
+
+import (
+	"context"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type Struct1 struct {
+	Field1 int
+	Field2 *int
+	Field3 []int
+	Field4 [4]int
+	field5 int
+	Struct2
+	S1               *Struct2
+	S2               Struct2
+	JSONField        int `json:"some_json_field"`
+	JSONIgnoredField int `json:"-"`
+}
+
+type Struct2 struct {
+	Field21 string
+	Field22 string
+}
+
+type Struct3 struct {
+	*Struct2
+	S1 string
+}
+
+func TestFindStructField(t *testing.T) {
+	var s1 Struct1
+	v1 := reflect.ValueOf(&s1).Elem()
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field1)))
+	assert.Nil(t, findStructField(v1, reflect.ValueOf(s1.Field2)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field2)))
+	assert.Nil(t, findStructField(v1, reflect.ValueOf(s1.Field3)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field3)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field4)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.field5)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Struct2)))
+	assert.Nil(t, findStructField(v1, reflect.ValueOf(s1.S1)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.S1)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field21)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field22)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Struct2.Field22)))
+	s2 := reflect.ValueOf(&s1.Struct2).Elem()
+	assert.NotNil(t, findStructField(s2, reflect.ValueOf(&s1.Field21)))
+	assert.NotNil(t, findStructField(s2, reflect.ValueOf(&s1.Struct2.Field21)))
+	assert.NotNil(t, findStructField(s2, reflect.ValueOf(&s1.Struct2.Field22)))
+	s3 := Struct3{
+		Struct2: &Struct2{},
+	}
+	v3 := reflect.ValueOf(&s3).Elem()
+	assert.NotNil(t, findStructField(v3, reflect.ValueOf(&s3.Struct2)))
+	assert.NotNil(t, findStructField(v3, reflect.ValueOf(&s3.Field21)))
+}
+
+func TestValidateStruct(t *testing.T) {
+	var m0 *Model1
+	m1 := Model1{A: "abc", B: "xyz", c: "abc", G: "xyz", H: []string{"abc", "abc"}, I: map[string]string{"foo": "abc"}}
+	m2 := Model1{E: String123("xyz")}
+	m3 := Model2{}
+	m4 := Model2{M3: Model3{A: "abc"}, Model3: Model3{A: "abc"}}
+	m5 := Model2{Model3: Model3{A: "internal"}}
+	tests := []struct {
+		tag   string
+		model interface{}
+		rules []*FieldRules
+		err   string
+	}{
+		// empty rules
+		{"t1.1", &m1, []*FieldRules{}, ""},
+		{"t1.2", &m1, []*FieldRules{Field(&m1.A), Field(&m1.B)}, ""},
+		// normal rules
+		{"t2.1", &m1, []*FieldRules{Field(&m1.A, &validateAbc{}), Field(&m1.B, &validateXyz{})}, ""},
+		{"t2.2", &m1, []*FieldRules{Field(&m1.A, &validateXyz{}), Field(&m1.B, &validateAbc{})}, "A: error xyz; B: error abc."},
+		{"t2.3", &m1, []*FieldRules{Field(&m1.A, &validateXyz{}), Field(&m1.c, &validateXyz{})}, "A: error xyz; c: error xyz."},
+		{"t2.4", &m1, []*FieldRules{Field(&m1.D, Length(0, 5))}, ""},
+		{"t2.5", &m1, []*FieldRules{Field(&m1.F, Length(0, 5))}, ""},
+		{"t2.6", &m1, []*FieldRules{Field(&m1.H, Each(&validateAbc{})), Field(&m1.I, Each(&validateAbc{}))}, ""},
+		{"t2.7", &m1, []*FieldRules{Field(&m1.H, Each(&validateXyz{})), Field(&m1.I, Each(&validateXyz{}))}, "H: (0: error xyz; 1: error xyz.); I: (foo: error xyz.)."},
+		// non-struct pointer
+		{"t3.1", m1, []*FieldRules{}, ErrStructPointer.Error()},
+		{"t3.2", nil, []*FieldRules{}, ErrStructPointer.Error()},
+		{"t3.3", m0, []*FieldRules{}, ""},
+		{"t3.4", &m0, []*FieldRules{}, ErrStructPointer.Error()},
+		// invalid field spec
+		{"t4.1", &m1, []*FieldRules{Field(m1)}, ErrFieldPointer(0).Error()},
+		{"t4.2", &m1, []*FieldRules{Field(&m1)}, ErrFieldNotFound(0).Error()},
+		// struct tag
+		{"t5.1", &m1, []*FieldRules{Field(&m1.G, &validateAbc{})}, "g: error abc."},
+		// validatable field
+		{"t6.1", &m2, []*FieldRules{Field(&m2.E)}, "E: error 123."},
+		{"t6.2", &m2, []*FieldRules{Field(&m2.E, Skip)}, ""},
+		{"t6.3", &m2, []*FieldRules{Field(&m2.E, Skip.When(true))}, ""},
+		{"t6.4", &m2, []*FieldRules{Field(&m2.E, Skip.When(false))}, "E: error 123."},
+		// Required, NotNil
+		{"t7.1", &m2, []*FieldRules{Field(&m2.F, Required)}, "F: cannot be blank."},
+		{"t7.2", &m2, []*FieldRules{Field(&m2.F, NotNil)}, "F: is required."},
+		{"t7.3", &m2, []*FieldRules{Field(&m2.F, Skip, Required)}, ""},
+		{"t7.4", &m2, []*FieldRules{Field(&m2.F, Skip, NotNil)}, ""},
+		{"t7.5", &m2, []*FieldRules{Field(&m2.F, Skip.When(true), Required)}, ""},
+		{"t7.6", &m2, []*FieldRules{Field(&m2.F, Skip.When(true), NotNil)}, ""},
+		{"t7.7", &m2, []*FieldRules{Field(&m2.F, Skip.When(false), Required)}, "F: cannot be blank."},
+		{"t7.8", &m2, []*FieldRules{Field(&m2.F, Skip.When(false), NotNil)}, "F: is required."},
+		// embedded structs
+		{"t8.1", &m3, []*FieldRules{Field(&m3.M3, Skip)}, ""},
+		{"t8.2", &m3, []*FieldRules{Field(&m3.M3)}, "M3: (A: error abc.)."},
+		{"t8.3", &m3, []*FieldRules{Field(&m3.Model3, Skip)}, ""},
+		{"t8.4", &m3, []*FieldRules{Field(&m3.Model3)}, "A: error abc."},
+		{"t8.5", &m4, []*FieldRules{Field(&m4.M3)}, ""},
+		{"t8.6", &m4, []*FieldRules{Field(&m4.Model3)}, ""},
+		{"t8.7", &m3, []*FieldRules{Field(&m3.A, Required), Field(&m3.B, Required)}, "A: cannot be blank; B: cannot be blank."},
+		{"t8.8", &m3, []*FieldRules{Field(&m4.A, Required)}, "field #0 cannot be found in the struct"},
+		// internal error
+		{"t9.1", &m5, []*FieldRules{Field(&m5.A, &validateAbc{}), Field(&m5.B, Required), Field(&m5.A, &validateInternalError{})}, "error internal"},
+	}
+	for _, test := range tests {
+		err1 := ValidateStruct(test.model, test.rules...)
+		err2 := ValidateStructWithContext(context.Background(), test.model, test.rules...)
+		assertError(t, test.err, err1, test.tag)
+		assertError(t, test.err, err2, test.tag)
+	}
+
+	// embedded struct
+	err := Validate(&m3)
+	assert.EqualError(t, err, "A: error abc.")
+
+	a := struct {
+		Name  string
+		Value string
+	}{"name", "demo"}
+	err = ValidateStruct(&a,
+		Field(&a.Name, Required),
+		Field(&a.Value, Required, Length(5, 10)),
+	)
+	assert.EqualError(t, err, "Value: the length must be between 5 and 10.")
+}
+
+func TestValidateStructWithContext(t *testing.T) {
+	m1 := Model1{A: "abc", B: "xyz", c: "abc", G: "xyz"}
+	m2 := Model2{Model3: Model3{A: "internal"}}
+	m3 := Model5{}
+	tests := []struct {
+		tag   string
+		model interface{}
+		rules []*FieldRules
+		err   string
+	}{
+		// normal rules
+		{"t1.1", &m1, []*FieldRules{Field(&m1.A, &validateContextAbc{}), Field(&m1.B, &validateContextXyz{})}, ""},
+		{"t1.2", &m1, []*FieldRules{Field(&m1.A, &validateContextXyz{}), Field(&m1.B, &validateContextAbc{})}, "A: error xyz; B: error abc."},
+		{"t1.3", &m1, []*FieldRules{Field(&m1.A, &validateContextXyz{}), Field(&m1.c, &validateContextXyz{})}, "A: error xyz; c: error xyz."},
+		{"t1.4", &m1, []*FieldRules{Field(&m1.G, &validateContextAbc{})}, "g: error abc."},
+		// skip rule
+		{"t2.1", &m1, []*FieldRules{Field(&m1.G, Skip, &validateContextAbc{})}, ""},
+		{"t2.2", &m1, []*FieldRules{Field(&m1.G, &validateContextAbc{}, Skip)}, "g: error abc."},
+		// internal error
+		{"t3.1", &m2, []*FieldRules{Field(&m2.A, &validateContextAbc{}), Field(&m2.B, Required), Field(&m2.A, &validateInternalError{})}, "error internal"},
+	}
+	for _, test := range tests {
+		err := ValidateStructWithContext(context.Background(), test.model, test.rules...)
+		assertError(t, test.err, err, test.tag)
+	}
+
+	//embedded struct
+	err := ValidateWithContext(context.Background(), &m3)
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "A: error abc.", err.Error())
+	}
+
+	a := struct {
+		Name  string
+		Value string
+	}{"name", "demo"}
+	err = ValidateStructWithContext(context.Background(), &a,
+		Field(&a.Name, Required),
+		Field(&a.Value, Required, Length(5, 10)),
+	)
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "Value: the length must be between 5 and 10.", err.Error())
+	}
+}
+
+func Test_getErrorFieldName(t *testing.T) {
+	var s1 Struct1
+	v1 := reflect.ValueOf(&s1).Elem()
+
+	sf1 := findStructField(v1, reflect.ValueOf(&s1.Field1))
+	assert.NotNil(t, sf1)
+	assert.Equal(t, "Field1", getErrorFieldName(sf1))
+
+	jsonField := findStructField(v1, reflect.ValueOf(&s1.JSONField))
+	assert.NotNil(t, jsonField)
+	assert.Equal(t, "some_json_field", getErrorFieldName(jsonField))
+
+	jsonIgnoredField := findStructField(v1, reflect.ValueOf(&s1.JSONIgnoredField))
+	assert.NotNil(t, jsonIgnoredField)
+	assert.Equal(t, "JSONIgnoredField", getErrorFieldName(jsonIgnoredField))
+}

+ 175 - 0
pkg/valid/util.go

@@ -0,0 +1,175 @@
+package valid
+
+import (
+	"database/sql/driver"
+	"errors"
+	"fmt"
+	"reflect"
+	"strings"
+	"time"
+)
+
+var (
+	bytesType  = reflect.TypeOf([]byte(nil))
+	valuerType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
+
+	// ErrEnsureString is the error that returns when a value is not a string or byte slice.
+	ErrEnsureString = errors.New("must be either a string or byte slice")
+)
+
+// EnsureString ensures the given value is a string.
+// If the value is a byte slice, it will be typecast into a string.
+// An error is returned otherwise.
+func EnsureString(value interface{}) (string, error) {
+	v := reflect.ValueOf(value)
+	if v.Kind() == reflect.String {
+		return v.String(), nil
+	}
+	if v.Type() == bytesType {
+		return string(v.Interface().([]byte)), nil
+	}
+	return "", ErrEnsureString
+}
+
+// IsBlankString check the given value is a empty values or whitespace only values
+func IsBlankString(value string) bool {
+	if value == "" {
+		return true
+	}
+	if strings.TrimSpace(value) == "" {
+		return true
+	}
+
+	return false
+}
+
+// StringOrBytes typecasts a value into a string or byte slice.
+// Boolean flags are returned to indicate if the typecasting succeeds or not.
+func StringOrBytes(value interface{}) (isString bool, str string, isBytes bool, bs []byte) {
+	v := reflect.ValueOf(value)
+	if v.Kind() == reflect.String {
+		str = v.String()
+		isString = true
+	} else if v.Kind() == reflect.Slice && v.Type() == bytesType {
+		bs = v.Interface().([]byte)
+		isBytes = true
+	}
+	return
+}
+
+// LengthOfValue returns the length of a value that is a string, slice, map, or array.
+// An error is returned for all other types.
+func LengthOfValue(value interface{}) (int, error) {
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.String, reflect.Slice, reflect.Map, reflect.Array:
+		return v.Len(), nil
+	}
+	return 0, fmt.Errorf("cannot get the length of %v", v.Kind())
+}
+
+// ToInt converts the given value to an int64.
+// An error is returned for all incompatible types.
+func ToInt(value interface{}) (int64, error) {
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int(), nil
+	}
+	return 0, fmt.Errorf("cannot convert %v to int64", v.Kind())
+}
+
+// ToUint converts the given value to an uint64.
+// An error is returned for all incompatible types.
+func ToUint(value interface{}) (uint64, error) {
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint(), nil
+	}
+	return 0, fmt.Errorf("cannot convert %v to uint64", v.Kind())
+}
+
+// ToFloat converts the given value to a float64.
+// An error is returned for all incompatible types.
+func ToFloat(value interface{}) (float64, error) {
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.Float32, reflect.Float64:
+		return v.Float(), nil
+	}
+	return 0, fmt.Errorf("cannot convert %v to float64", v.Kind())
+}
+
+// IsEmpty checks if a value is empty or not.
+// A value is considered empty if
+// - integer, float: zero
+// - bool: false
+// - string, array: len() == 0
+// - slice, map: nil or len() == 0
+// - interface, pointer: nil or the referenced value is empty
+func IsEmpty(value interface{}) bool {
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.String, reflect.Array, reflect.Map, reflect.Slice:
+		return v.Len() == 0
+	case reflect.Bool:
+		return !v.Bool()
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int() == 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint() == 0
+	case reflect.Float32, reflect.Float64:
+		return v.Float() == 0
+	case reflect.Invalid:
+		return true
+	case reflect.Interface, reflect.Ptr:
+		if v.IsNil() {
+			return true
+		}
+		return IsEmpty(v.Elem().Interface())
+	case reflect.Struct:
+		v, ok := value.(time.Time)
+		if ok && v.IsZero() {
+			return true
+		}
+	}
+
+	return false
+}
+
+// Indirect returns the value that the given interface or pointer references to.
+// If the value implements driver.Valuer, it will deal with the value returned by
+// the Value() method instead. A boolean value is also returned to indicate if
+// the value is nil or not (only applicable to interface, pointer, map, and slice).
+// If the value is neither an interface nor a pointer, it will be returned back.
+func Indirect(value interface{}) (interface{}, bool) {
+	rv := reflect.ValueOf(value)
+	kind := rv.Kind()
+	switch kind {
+	case reflect.Invalid:
+		return nil, true
+	case reflect.Ptr, reflect.Interface:
+		if rv.IsNil() {
+			return nil, true
+		}
+		return Indirect(rv.Elem().Interface())
+	case reflect.Slice, reflect.Map, reflect.Func, reflect.Chan:
+		if rv.IsNil() {
+			return nil, true
+		}
+	}
+
+	if rv.Type().Implements(valuerType) {
+		return indirectValuer(value.(driver.Valuer))
+	}
+
+	return value, false
+}
+
+func indirectValuer(valuer driver.Valuer) (interface{}, bool) {
+	if value, err := valuer.Value(); value != nil && err == nil {
+		return Indirect(value)
+	}
+	return nil, true
+}

+ 296 - 0
pkg/valid/util_test.go

@@ -0,0 +1,296 @@
+package valid
+
+import (
+	"database/sql"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEnsureString(t *testing.T) {
+	str := "abc"
+	bytes := []byte("abc")
+
+	tests := []struct {
+		tag      string
+		value    interface{}
+		expected string
+		hasError bool
+	}{
+		{"t1", "abc", "abc", false},
+		{"t2", &str, "", true},
+		{"t3", bytes, "abc", false},
+		{"t4", &bytes, "", true},
+		{"t5", 100, "", true},
+	}
+	for _, test := range tests {
+		s, err := EnsureString(test.value)
+		if test.hasError {
+			assert.NotNil(t, err, test.tag)
+		} else {
+			assert.Nil(t, err, test.tag)
+			assert.Equal(t, test.expected, s, test.tag)
+		}
+	}
+}
+
+type MyString string
+
+func TestStringOrBytes(t *testing.T) {
+	str := "abc"
+	bytes := []byte("abc")
+	var str2 string
+	var bytes2 []byte
+	var str3 MyString = "abc"
+	var str4 *string
+
+	tests := []struct {
+		tag      string
+		value    interface{}
+		str      string
+		bs       []byte
+		isString bool
+		isBytes  bool
+	}{
+		{"t1", str, "abc", nil, true, false},
+		{"t2", &str, "", nil, false, false},
+		{"t3", bytes, "", []byte("abc"), false, true},
+		{"t4", &bytes, "", nil, false, false},
+		{"t5", 100, "", nil, false, false},
+		{"t6", str2, "", nil, true, false},
+		{"t7", &str2, "", nil, false, false},
+		{"t8", bytes2, "", nil, false, true},
+		{"t9", &bytes2, "", nil, false, false},
+		{"t10", str3, "abc", nil, true, false},
+		{"t11", &str3, "", nil, false, false},
+		{"t12", str4, "", nil, false, false},
+	}
+	for _, test := range tests {
+		isString, str, isBytes, bs := StringOrBytes(test.value)
+		assert.Equal(t, test.str, str, test.tag)
+		assert.Equal(t, test.bs, bs, test.tag)
+		assert.Equal(t, test.isString, isString, test.tag)
+		assert.Equal(t, test.isBytes, isBytes, test.tag)
+	}
+}
+
+func TestLengthOfValue(t *testing.T) {
+	var a [3]int
+
+	tests := []struct {
+		tag    string
+		value  interface{}
+		length int
+		err    string
+	}{
+		{"t1", "abc", 3, ""},
+		{"t2", []int{1, 2}, 2, ""},
+		{"t3", map[string]int{"A": 1, "B": 2}, 2, ""},
+		{"t4", a, 3, ""},
+		{"t5", &a, 0, "cannot get the length of ptr"},
+		{"t6", 123, 0, "cannot get the length of int"},
+	}
+
+	for _, test := range tests {
+		l, err := LengthOfValue(test.value)
+		assert.Equal(t, test.length, l, test.tag)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestToInt(t *testing.T) {
+	var a int
+
+	tests := []struct {
+		tag    string
+		value  interface{}
+		result int64
+		err    string
+	}{
+		{"t1", 1, 1, ""},
+		{"t2", int8(1), 1, ""},
+		{"t3", int16(1), 1, ""},
+		{"t4", int32(1), 1, ""},
+		{"t5", int64(1), 1, ""},
+		{"t6", &a, 0, "cannot convert ptr to int64"},
+		{"t7", uint(1), 0, "cannot convert uint to int64"},
+		{"t8", float64(1), 0, "cannot convert float64 to int64"},
+		{"t9", "abc", 0, "cannot convert string to int64"},
+		{"t10", []int{1, 2}, 0, "cannot convert slice to int64"},
+		{"t11", map[string]int{"A": 1}, 0, "cannot convert map to int64"},
+	}
+
+	for _, test := range tests {
+		l, err := ToInt(test.value)
+		assert.Equal(t, test.result, l, test.tag)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestToUint(t *testing.T) {
+	var a int
+	var b uint
+
+	tests := []struct {
+		tag    string
+		value  interface{}
+		result uint64
+		err    string
+	}{
+		{"t1", uint(1), 1, ""},
+		{"t2", uint8(1), 1, ""},
+		{"t3", uint16(1), 1, ""},
+		{"t4", uint32(1), 1, ""},
+		{"t5", uint64(1), 1, ""},
+		{"t6", 1, 0, "cannot convert int to uint64"},
+		{"t7", &a, 0, "cannot convert ptr to uint64"},
+		{"t8", &b, 0, "cannot convert ptr to uint64"},
+		{"t9", float64(1), 0, "cannot convert float64 to uint64"},
+		{"t10", "abc", 0, "cannot convert string to uint64"},
+		{"t11", []int{1, 2}, 0, "cannot convert slice to uint64"},
+		{"t12", map[string]int{"A": 1}, 0, "cannot convert map to uint64"},
+	}
+
+	for _, test := range tests {
+		l, err := ToUint(test.value)
+		assert.Equal(t, test.result, l, test.tag)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestToFloat(t *testing.T) {
+	var a int
+	var b uint
+
+	tests := []struct {
+		tag    string
+		value  interface{}
+		result float64
+		err    string
+	}{
+		{"t1", float32(1), 1, ""},
+		{"t2", float64(1), 1, ""},
+		{"t3", 1, 0, "cannot convert int to float64"},
+		{"t4", uint(1), 0, "cannot convert uint to float64"},
+		{"t5", &a, 0, "cannot convert ptr to float64"},
+		{"t6", &b, 0, "cannot convert ptr to float64"},
+		{"t7", "abc", 0, "cannot convert string to float64"},
+		{"t8", []int{1, 2}, 0, "cannot convert slice to float64"},
+		{"t9", map[string]int{"A": 1}, 0, "cannot convert map to float64"},
+	}
+
+	for _, test := range tests {
+		l, err := ToFloat(test.value)
+		assert.Equal(t, test.result, l, test.tag)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestIsEmpty(t *testing.T) {
+	var s1 string
+	var s2 = "a"
+	var s3 *string
+	s4 := struct{}{}
+	time1 := time.Now()
+	var time2 time.Time
+	tests := []struct {
+		tag   string
+		value interface{}
+		empty bool
+	}{
+		// nil
+		{"t0", nil, true},
+		// string
+		{"t1.1", "", true},
+		{"t1.2", "1", false},
+		{"t1.3", MyString(""), true},
+		{"t1.4", MyString("1"), false},
+		// slice
+		{"t2.1", []byte(""), true},
+		{"t2.2", []byte("1"), false},
+		// map
+		{"t3.1", map[string]int{}, true},
+		{"t3.2", map[string]int{"a": 1}, false},
+		// bool
+		{"t4.1", false, true},
+		{"t4.2", true, false},
+		// int
+		{"t5.1", 0, true},
+		{"t5.2", int8(0), true},
+		{"t5.3", int16(0), true},
+		{"t5.4", int32(0), true},
+		{"t5.5", int64(0), true},
+		{"t5.6", 1, false},
+		{"t5.7", int8(1), false},
+		{"t5.8", int16(1), false},
+		{"t5.9", int32(1), false},
+		{"t5.10", int64(1), false},
+		// uint
+		{"t6.1", uint(0), true},
+		{"t6.2", uint8(0), true},
+		{"t6.3", uint16(0), true},
+		{"t6.4", uint32(0), true},
+		{"t6.5", uint64(0), true},
+		{"t6.6", uint(1), false},
+		{"t6.7", uint8(1), false},
+		{"t6.8", uint16(1), false},
+		{"t6.9", uint32(1), false},
+		{"t6.10", uint64(1), false},
+		// float
+		{"t7.1", float32(0), true},
+		{"t7.2", float64(0), true},
+		{"t7.3", float32(1), false},
+		{"t7.4", float64(1), false},
+		// interface, ptr
+		{"t8.1", &s1, true},
+		{"t8.2", &s2, false},
+		{"t8.3", s3, true},
+		// struct
+		{"t9.1", s4, false},
+		{"t9.2", &s4, false},
+		// time.Time
+		{"t10.1", time1, false},
+		{"t10.2", &time1, false},
+		{"t10.3", time2, true},
+		{"t10.4", &time2, true},
+	}
+
+	for _, test := range tests {
+		empty := IsEmpty(test.value)
+		assert.Equal(t, test.empty, empty, test.tag)
+	}
+}
+
+func TestIndirect(t *testing.T) {
+	var a = 100
+	var b *int
+	var c *sql.NullInt64
+
+	tests := []struct {
+		tag    string
+		value  interface{}
+		result interface{}
+		isNil  bool
+	}{
+		{"t1", 100, 100, false},
+		{"t2", &a, 100, false},
+		{"t3", b, nil, true},
+		{"t4", nil, nil, true},
+		{"t5", sql.NullInt64{Int64: 0, Valid: false}, nil, true},
+		{"t6", sql.NullInt64{Int64: 1, Valid: false}, nil, true},
+		{"t7", &sql.NullInt64{Int64: 0, Valid: false}, nil, true},
+		{"t8", &sql.NullInt64{Int64: 1, Valid: false}, nil, true},
+		{"t9", sql.NullInt64{Int64: 0, Valid: true}, int64(0), false},
+		{"t10", sql.NullInt64{Int64: 1, Valid: true}, int64(1), false},
+		{"t11", &sql.NullInt64{Int64: 0, Valid: true}, int64(0), false},
+		{"t12", &sql.NullInt64{Int64: 1, Valid: true}, int64(1), false},
+		{"t13", c, nil, true},
+	}
+
+	for _, test := range tests {
+		result, isNil := Indirect(test.value)
+		assert.Equal(t, test.result, result, test.tag)
+		assert.Equal(t, test.isNil, isNil, test.tag)
+	}
+}

+ 221 - 0
pkg/valid/validation.go

@@ -0,0 +1,221 @@
+package valid
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strconv"
+)
+
+var (
+	validatableType            = reflect.TypeOf((*Validatable)(nil)).Elem()
+	validatableWithContextType = reflect.TypeOf((*ValidatableWithContext)(nil)).Elem()
+)
+
+// Validate validates the given value and returns the validation error, if any.
+//
+// Validate performs validation using the following steps:
+// 1. For each rule, call its `Validate()` to validate the value. Return if any error is found.
+// 2. If the value being validated implements `Validatable`, call the value's `Validate()`.
+//    Return with the validation result.
+// 3. If the value being validated is a map/slice/array, and the element type implements `Validatable`,
+//    for each element call the element value's `Validate()`. Return with the validation result.
+func Validate(value interface{}, rules ...Rule) error {
+	for _, rule := range rules {
+		if s, ok := rule.(skipRule); ok && s.skip {
+			return nil
+		}
+		if err := rule.Validate(value); err != nil {
+			return err
+		}
+	}
+
+	rv := reflect.ValueOf(value)
+	if (rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface) && rv.IsNil() {
+		return nil
+	}
+
+	if v, ok := value.(Validatable); ok {
+		return v.Validate()
+	}
+
+	switch rv.Kind() {
+	case reflect.Map:
+		if rv.Type().Elem().Implements(validatableType) {
+			return validateMap(rv)
+		}
+	case reflect.Slice, reflect.Array:
+		if rv.Type().Elem().Implements(validatableType) {
+			return validateSlice(rv)
+		}
+	case reflect.Ptr, reflect.Interface:
+		return Validate(rv.Elem().Interface())
+	}
+
+	return nil
+}
+
+// ValidateWithContext validates the given value with the given context and returns the validation error, if any.
+//
+// ValidateWithContext performs validation using the following steps:
+// 1. For each rule, call its `ValidateWithContext()` to validate the value if the rule implements `RuleWithContext`.
+//    Otherwise call `Validate()` of the rule. Return if any error is found.
+// 2. If the value being validated implements `ValidatableWithContext`, call the value's `ValidateWithContext()`
+//    and return with the validation result.
+// 3. If the value being validated implements `Validatable`, call the value's `Validate()`
+//    and return with the validation result.
+// 4. If the value being validated is a map/slice/array, and the element type implements `ValidatableWithContext`,
+//    for each element call the element value's `ValidateWithContext()`. Return with the validation result.
+// 5. If the value being validated is a map/slice/array, and the element type implements `Validatable`,
+//    for each element call the element value's `Validate()`. Return with the validation result.
+func ValidateWithContext(ctx context.Context, value interface{}, rules ...Rule) error {
+	for _, rule := range rules {
+		if s, ok := rule.(skipRule); ok && s.skip {
+			return nil
+		}
+		if rc, ok := rule.(RuleWithContext); ok {
+			if err := rc.ValidateWithContext(ctx, value); err != nil {
+				return err
+			}
+		} else if err := rule.Validate(value); err != nil {
+			return err
+		}
+	}
+
+	rv := reflect.ValueOf(value)
+	if (rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface) && rv.IsNil() {
+		return nil
+	}
+
+	if v, ok := value.(ValidatableWithContext); ok {
+		return v.ValidateWithContext(ctx)
+	}
+
+	if v, ok := value.(Validatable); ok {
+		return v.Validate()
+	}
+
+	switch rv.Kind() {
+	case reflect.Map:
+		if rv.Type().Elem().Implements(validatableWithContextType) {
+			return validateMapWithContext(ctx, rv)
+		}
+		if rv.Type().Elem().Implements(validatableType) {
+			return validateMap(rv)
+		}
+	case reflect.Slice, reflect.Array:
+		if rv.Type().Elem().Implements(validatableWithContextType) {
+			return validateSliceWithContext(ctx, rv)
+		}
+		if rv.Type().Elem().Implements(validatableType) {
+			return validateSlice(rv)
+		}
+	case reflect.Ptr, reflect.Interface:
+		return ValidateWithContext(ctx, rv.Elem().Interface())
+	}
+
+	return nil
+}
+
+// By wraps a RuleFunc into a Rule.
+func By(f RuleFunc) Rule {
+	return &inlineRule{f: f}
+}
+
+// RuleFunc represents a validator function.
+// You may wrap it as a Rule by calling By().
+type RuleFunc func(value interface{}) error
+
+// WithContext wraps a RuleWithContextFunc into a context-aware Rule.
+func WithContext(f RuleWithContextFunc) Rule {
+	return &inlineRule{fc: f}
+}
+
+// RuleWithContextFunc represents a validator function that is context-aware.
+// You may wrap it as a Rule by calling WithContext().
+type RuleWithContextFunc func(ctx context.Context, value interface{}) error
+
+// validateMap validates a map of validatable elements
+func validateMap(rv reflect.Value) error {
+	errs := Errors{}
+	for _, key := range rv.MapKeys() {
+		if mv := rv.MapIndex(key).Interface(); mv != nil {
+			if err := mv.(Validatable).Validate(); err != nil {
+				errs[fmt.Sprintf("%v", key.Interface())] = err
+			}
+		}
+	}
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+// validateMapWithContext validates a map of validatable elements with the given context.
+func validateMapWithContext(ctx context.Context, rv reflect.Value) error {
+	errs := Errors{}
+	for _, key := range rv.MapKeys() {
+		if mv := rv.MapIndex(key).Interface(); mv != nil {
+			if err := mv.(ValidatableWithContext).ValidateWithContext(ctx); err != nil {
+				errs[fmt.Sprintf("%v", key.Interface())] = err
+			}
+		}
+	}
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+// validateSlice validates a slice/array of validatable elements
+func validateSlice(rv reflect.Value) error {
+	errs := Errors{}
+	l := rv.Len()
+	for i := 0; i < l; i++ {
+		if ev := rv.Index(i).Interface(); ev != nil {
+			if err := ev.(Validatable).Validate(); err != nil {
+				errs[strconv.Itoa(i)] = err
+			}
+		}
+	}
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+// validateSliceWithContext validates a slice/array of validatable elements with the given context.
+func validateSliceWithContext(ctx context.Context, rv reflect.Value) error {
+	errs := Errors{}
+	l := rv.Len()
+	for i := 0; i < l; i++ {
+		if ev := rv.Index(i).Interface(); ev != nil {
+			if err := ev.(ValidatableWithContext).ValidateWithContext(ctx); err != nil {
+				errs[strconv.Itoa(i)] = err
+			}
+		}
+	}
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+type inlineRule struct {
+	f  RuleFunc
+	fc RuleWithContextFunc
+}
+
+func (r *inlineRule) Validate(value interface{}) error {
+	if r.f == nil {
+		return r.fc(context.Background(), value)
+	}
+	return r.f(value)
+}
+
+func (r *inlineRule) ValidateWithContext(ctx context.Context, value interface{}) error {
+	if r.fc == nil {
+		return r.f(value)
+	}
+	return r.fc(ctx, value)
+}

+ 266 - 0
pkg/valid/validation_test.go

@@ -0,0 +1,266 @@
+package valid
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidate(t *testing.T) {
+	slice := []String123{String123("abc"), String123("123"), String123("xyz")}
+	ctxSlice := []Model4{{A: "abc"}, {A: "def"}}
+	mp := map[string]String123{"c": String123("abc"), "b": String123("123"), "a": String123("xyz")}
+	mpCtx := map[string]StringValidateContext{"c": StringValidateContext("abc"), "b": StringValidateContext("123"), "a": StringValidateContext("xyz")}
+	var (
+		ptr     *string
+		noCtx   StringValidate        = "abc"
+		withCtx StringValidateContext = "xyz"
+	)
+	tests := []struct {
+		tag            string
+		value          interface{}
+		err            string
+		errWithContext string
+	}{
+		{"t1", 123, "", ""},
+		{"t2", String123("123"), "", ""},
+		{"t3", String123("abc"), "error 123", "error 123"},
+		{"t4", []String123{}, "", ""},
+		{"t4.1", []StringValidateContext{}, "", ""},
+		{"t4.2", map[string]StringValidateContext{}, "", ""},
+		{"t5", slice, "0: error 123; 2: error 123.", "0: error 123; 2: error 123."},
+		{"t6", &slice, "0: error 123; 2: error 123.", "0: error 123; 2: error 123."},
+		{"t7", ctxSlice, "", "1: (A: error abc.)."},
+		{"t8", mp, "a: error 123; c: error 123.", "a: error 123; c: error 123."},
+		{"t8.1", mpCtx, "a: must be abc; b: must be abc.", "a: must be abc with context; b: must be abc with context."},
+		{"t9", &mp, "a: error 123; c: error 123.", "a: error 123; c: error 123."},
+		{"t10", map[string]String123{}, "", ""},
+		{"t11", ptr, "", ""},
+		{"t12", noCtx, "called validate", "called validate"},
+		{"t13", withCtx, "must be abc", "must be abc with context"},
+	}
+	for _, test := range tests {
+		err := Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+		// rules that are not context-aware should still be applied in context-aware validation
+		err = ValidateWithContext(context.Background(), test.value)
+		assertError(t, test.errWithContext, err, test.tag)
+	}
+
+	// with rules
+	err := Validate("123", &validateAbc{}, &validateXyz{})
+	assert.EqualError(t, err, "error abc")
+	err = Validate("abc", &validateAbc{}, &validateXyz{})
+	assert.EqualError(t, err, "error xyz")
+	err = Validate("abcxyz", &validateAbc{}, &validateXyz{})
+	assert.NoError(t, err)
+
+	err = Validate("123", &validateAbc{}, Skip, &validateXyz{})
+	assert.EqualError(t, err, "error abc")
+	err = Validate("abc", &validateAbc{}, Skip, &validateXyz{})
+	assert.NoError(t, err)
+
+	err = Validate("123", &validateAbc{}, Skip.When(true), &validateXyz{})
+	assert.EqualError(t, err, "error abc")
+	err = Validate("abc", &validateAbc{}, Skip.When(true), &validateXyz{})
+	assert.NoError(t, err)
+
+	err = Validate("123", &validateAbc{}, Skip.When(false), &validateXyz{})
+	assert.EqualError(t, err, "error abc")
+	err = Validate("abc", &validateAbc{}, Skip.When(false), &validateXyz{})
+	assert.EqualError(t, err, "error xyz")
+}
+
+func stringEqual(str string) RuleFunc {
+	return func(value interface{}) error {
+		s, _ := value.(string)
+		if s != str {
+			return errors.New("unexpected string")
+		}
+		return nil
+	}
+}
+
+func TestBy(t *testing.T) {
+	abcRule := By(func(value interface{}) error {
+		s, _ := value.(string)
+		if s != "abc" {
+			return errors.New("must be abc")
+		}
+		return nil
+	})
+	assert.Nil(t, Validate("abc", abcRule))
+	err := Validate("xyz", abcRule)
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "must be abc", err.Error())
+	}
+
+	xyzRule := By(stringEqual("xyz"))
+	assert.Nil(t, Validate("xyz", xyzRule))
+	assert.NotNil(t, Validate("abc", xyzRule))
+	assert.Nil(t, ValidateWithContext(context.Background(), "xyz", xyzRule))
+	assert.NotNil(t, ValidateWithContext(context.Background(), "abc", xyzRule))
+}
+
+type key int
+
+func TestByWithContext(t *testing.T) {
+	k := key(1)
+	abcRule := WithContext(func(ctx context.Context, value interface{}) error {
+		if ctx.Value(k) != value.(string) {
+			return errors.New("must be abc")
+		}
+		return nil
+	})
+	ctx := context.WithValue(context.Background(), k, "abc")
+	assert.Nil(t, ValidateWithContext(ctx, "abc", abcRule))
+	err := ValidateWithContext(ctx, "xyz", abcRule)
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "must be abc", err.Error())
+	}
+
+	assert.NotNil(t, Validate("abc", abcRule))
+}
+
+func Test_skipRule_Validate(t *testing.T) {
+	assert.Nil(t, Skip.Validate(100))
+}
+
+func assertError(t *testing.T, expected string, err error, tag string) {
+	if expected == "" {
+		assert.NoError(t, err, tag)
+	} else {
+		assert.EqualError(t, err, expected, tag)
+	}
+}
+
+type validateAbc struct{}
+
+func (v *validateAbc) Validate(obj interface{}) error {
+	if !strings.Contains(obj.(string), "abc") {
+		return errors.New("error abc")
+	}
+	return nil
+}
+
+type validateContextAbc struct{}
+
+func (v *validateContextAbc) Validate(obj interface{}) error {
+	return v.ValidateWithContext(context.Background(), obj)
+}
+
+func (v *validateContextAbc) ValidateWithContext(_ context.Context, obj interface{}) error {
+	if !strings.Contains(obj.(string), "abc") {
+		return errors.New("error abc")
+	}
+	return nil
+}
+
+type validateXyz struct{}
+
+func (v *validateXyz) Validate(obj interface{}) error {
+	if !strings.Contains(obj.(string), "xyz") {
+		return errors.New("error xyz")
+	}
+	return nil
+}
+
+type validateContextXyz struct{}
+
+func (v *validateContextXyz) Validate(obj interface{}) error {
+	return v.ValidateWithContext(context.Background(), obj)
+}
+
+func (v *validateContextXyz) ValidateWithContext(_ context.Context, obj interface{}) error {
+	if !strings.Contains(obj.(string), "xyz") {
+		return errors.New("error xyz")
+	}
+	return nil
+}
+
+type validateInternalError struct{}
+
+func (v *validateInternalError) Validate(obj interface{}) error {
+	if strings.Contains(obj.(string), "internal") {
+		return NewInternalError(errors.New("error internal"))
+	}
+	return nil
+}
+
+type Model1 struct {
+	A string
+	B string
+	c string
+	D *string
+	E String123
+	F *String123
+	G string `json:"g"`
+	H []string
+	I map[string]string
+}
+
+type String123 string
+
+func (s String123) Validate() error {
+	if !strings.Contains(string(s), "123") {
+		return errors.New("error 123")
+	}
+	return nil
+}
+
+type Model2 struct {
+	Model3
+	M3 Model3
+	B  string
+}
+
+type Model3 struct {
+	A string
+}
+
+func (m Model3) Validate() error {
+	return ValidateStruct(&m,
+		Field(&m.A, &validateAbc{}),
+	)
+}
+
+type Model4 struct {
+	A string
+}
+
+func (m Model4) ValidateWithContext(ctx context.Context) error {
+	return ValidateStructWithContext(ctx, &m,
+		Field(&m.A, &validateContextAbc{}),
+	)
+}
+
+type Model5 struct {
+	Model4
+	M4 Model4
+	B  string
+}
+
+type StringValidate string
+
+func (s StringValidate) Validate() error {
+	return errors.New("called validate")
+}
+
+type StringValidateContext string
+
+func (s StringValidateContext) Validate() error {
+	if string(s) != "abc" {
+		return errors.New("must be abc")
+	}
+	return nil
+}
+
+func (s StringValidateContext) ValidateWithContext(context.Context) error {
+	if string(s) != "abc" {
+		return errors.New("must be abc with context")
+	}
+	return nil
+}

+ 0 - 187
proto/go/backend/common/enum.pb.go

@@ -1,187 +0,0 @@
-// Code generated by protoc-gen-go. DO NOT EDIT.
-// versions:
-// 	protoc-gen-go v1.28.1
-// 	protoc        v3.21.9
-// source: backend/common/enum.proto
-
-package commonPb
-
-import (
-	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
-	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
-	reflect "reflect"
-	sync "sync"
-)
-
-const (
-	// Verify that this generated code is sufficiently up-to-date.
-	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
-	// Verify that runtime/protoimpl is sufficiently up-to-date.
-	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
-)
-
-type IsShow_Kind int32
-
-const (
-	IsShow_INVALID IsShow_Kind = 0 // 无效
-	IsShow_OK      IsShow_Kind = 1 // 是
-	IsShow_NO      IsShow_Kind = 2 // 否
-)
-
-// Enum value maps for IsShow_Kind.
-var (
-	IsShow_Kind_name = map[int32]string{
-		0: "INVALID",
-		1: "OK",
-		2: "NO",
-	}
-	IsShow_Kind_value = map[string]int32{
-		"INVALID": 0,
-		"OK":      1,
-		"NO":      2,
-	}
-)
-
-func (x IsShow_Kind) Enum() *IsShow_Kind {
-	p := new(IsShow_Kind)
-	*p = x
-	return p
-}
-
-func (x IsShow_Kind) String() string {
-	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
-}
-
-func (IsShow_Kind) Descriptor() protoreflect.EnumDescriptor {
-	return file_backend_common_enum_proto_enumTypes[0].Descriptor()
-}
-
-func (IsShow_Kind) Type() protoreflect.EnumType {
-	return &file_backend_common_enum_proto_enumTypes[0]
-}
-
-func (x IsShow_Kind) Number() protoreflect.EnumNumber {
-	return protoreflect.EnumNumber(x)
-}
-
-// Deprecated: Use IsShow_Kind.Descriptor instead.
-func (IsShow_Kind) EnumDescriptor() ([]byte, []int) {
-	return file_backend_common_enum_proto_rawDescGZIP(), []int{0, 0}
-}
-
-// 是否标识
-type IsShow struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-}
-
-func (x *IsShow) Reset() {
-	*x = IsShow{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_backend_common_enum_proto_msgTypes[0]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *IsShow) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*IsShow) ProtoMessage() {}
-
-func (x *IsShow) ProtoReflect() protoreflect.Message {
-	mi := &file_backend_common_enum_proto_msgTypes[0]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use IsShow.ProtoReflect.Descriptor instead.
-func (*IsShow) Descriptor() ([]byte, []int) {
-	return file_backend_common_enum_proto_rawDescGZIP(), []int{0}
-}
-
-var File_backend_common_enum_proto protoreflect.FileDescriptor
-
-var file_backend_common_enum_proto_rawDesc = []byte{
-	0x0a, 0x19, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
-	0x2f, 0x65, 0x6e, 0x75, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x62, 0x61, 0x63,
-	0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x73, 0x22, 0x2d, 0x0a, 0x06, 0x49, 0x73, 0x53, 0x68,
-	0x6f, 0x77, 0x22, 0x23, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e,
-	0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x01, 0x12,
-	0x06, 0x0a, 0x02, 0x4e, 0x4f, 0x10, 0x02, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x3b, 0x63, 0x6f, 0x6d,
-	0x6d, 0x6f, 0x6e, 0x50, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
-}
-
-var (
-	file_backend_common_enum_proto_rawDescOnce sync.Once
-	file_backend_common_enum_proto_rawDescData = file_backend_common_enum_proto_rawDesc
-)
-
-func file_backend_common_enum_proto_rawDescGZIP() []byte {
-	file_backend_common_enum_proto_rawDescOnce.Do(func() {
-		file_backend_common_enum_proto_rawDescData = protoimpl.X.CompressGZIP(file_backend_common_enum_proto_rawDescData)
-	})
-	return file_backend_common_enum_proto_rawDescData
-}
-
-var file_backend_common_enum_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_backend_common_enum_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
-var file_backend_common_enum_proto_goTypes = []interface{}{
-	(IsShow_Kind)(0), // 0: backend.ops.IsShow.Kind
-	(*IsShow)(nil),   // 1: backend.ops.IsShow
-}
-var file_backend_common_enum_proto_depIdxs = []int32{
-	0, // [0:0] is the sub-list for method output_type
-	0, // [0:0] is the sub-list for method input_type
-	0, // [0:0] is the sub-list for extension type_name
-	0, // [0:0] is the sub-list for extension extendee
-	0, // [0:0] is the sub-list for field type_name
-}
-
-func init() { file_backend_common_enum_proto_init() }
-func file_backend_common_enum_proto_init() {
-	if File_backend_common_enum_proto != nil {
-		return
-	}
-	if !protoimpl.UnsafeEnabled {
-		file_backend_common_enum_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*IsShow); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-	}
-	type x struct{}
-	out := protoimpl.TypeBuilder{
-		File: protoimpl.DescBuilder{
-			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: file_backend_common_enum_proto_rawDesc,
-			NumEnums:      1,
-			NumMessages:   1,
-			NumExtensions: 0,
-			NumServices:   0,
-		},
-		GoTypes:           file_backend_common_enum_proto_goTypes,
-		DependencyIndexes: file_backend_common_enum_proto_depIdxs,
-		EnumInfos:         file_backend_common_enum_proto_enumTypes,
-		MessageInfos:      file_backend_common_enum_proto_msgTypes,
-	}.Build()
-	File_backend_common_enum_proto = out.File
-	file_backend_common_enum_proto_rawDesc = nil
-	file_backend_common_enum_proto_goTypes = nil
-	file_backend_common_enum_proto_depIdxs = nil
-}

+ 164 - 0
proto/go/backend/operation/pagination.pb.go

@@ -0,0 +1,164 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.9
+// source: backend/operation/pagination.proto
+
+package operationPb
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type PaginationModel struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Page       int32 `protobuf:"varint,1,opt,name=Page,proto3" json:"Page,omitempty"`
+	PageSize   int32 `protobuf:"varint,2,opt,name=PageSize,proto3" json:"PageSize,omitempty"`
+	PageOffset int32 `protobuf:"varint,3,opt,name=PageOffset,proto3" json:"PageOffset,omitempty"`
+}
+
+func (x *PaginationModel) Reset() {
+	*x = PaginationModel{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_backend_operation_pagination_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PaginationModel) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PaginationModel) ProtoMessage() {}
+
+func (x *PaginationModel) ProtoReflect() protoreflect.Message {
+	mi := &file_backend_operation_pagination_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PaginationModel.ProtoReflect.Descriptor instead.
+func (*PaginationModel) Descriptor() ([]byte, []int) {
+	return file_backend_operation_pagination_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *PaginationModel) GetPage() int32 {
+	if x != nil {
+		return x.Page
+	}
+	return 0
+}
+
+func (x *PaginationModel) GetPageSize() int32 {
+	if x != nil {
+		return x.PageSize
+	}
+	return 0
+}
+
+func (x *PaginationModel) GetPageOffset() int32 {
+	if x != nil {
+		return x.PageOffset
+	}
+	return 0
+}
+
+var File_backend_operation_pagination_proto protoreflect.FileDescriptor
+
+var file_backend_operation_pagination_proto_rawDesc = []byte{
+	0x0a, 0x22, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74,
+	0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70,
+	0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x61, 0x0a, 0x0f, 0x50, 0x61, 0x67, 0x69, 0x6e,
+	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61,
+	0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x50, 0x61, 0x67, 0x65, 0x12, 0x1a,
+	0x0a, 0x08, 0x50, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05,
+	0x52, 0x08, 0x50, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x50, 0x61,
+	0x67, 0x65, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a,
+	0x50, 0x61, 0x67, 0x65, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x3b,
+	0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
+}
+
+var (
+	file_backend_operation_pagination_proto_rawDescOnce sync.Once
+	file_backend_operation_pagination_proto_rawDescData = file_backend_operation_pagination_proto_rawDesc
+)
+
+func file_backend_operation_pagination_proto_rawDescGZIP() []byte {
+	file_backend_operation_pagination_proto_rawDescOnce.Do(func() {
+		file_backend_operation_pagination_proto_rawDescData = protoimpl.X.CompressGZIP(file_backend_operation_pagination_proto_rawDescData)
+	})
+	return file_backend_operation_pagination_proto_rawDescData
+}
+
+var file_backend_operation_pagination_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_backend_operation_pagination_proto_goTypes = []interface{}{
+	(*PaginationModel)(nil), // 0: backend.operation.PaginationModel
+}
+var file_backend_operation_pagination_proto_depIdxs = []int32{
+	0, // [0:0] is the sub-list for method output_type
+	0, // [0:0] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_backend_operation_pagination_proto_init() }
+func file_backend_operation_pagination_proto_init() {
+	if File_backend_operation_pagination_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_backend_operation_pagination_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PaginationModel); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_backend_operation_pagination_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_backend_operation_pagination_proto_goTypes,
+		DependencyIndexes: file_backend_operation_pagination_proto_depIdxs,
+		MessageInfos:      file_backend_operation_pagination_proto_msgTypes,
+	}.Build()
+	File_backend_operation_pagination_proto = out.File
+	file_backend_operation_pagination_proto_rawDesc = nil
+	file_backend_operation_pagination_proto_goTypes = nil
+	file_backend_operation_pagination_proto_depIdxs = nil
+}

+ 45 - 56
proto/go/backend/operation/pasture.pb.go

@@ -25,14 +25,13 @@ type AddPastureRequest struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Id              int64       `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
-	Name            string      `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`                                                       // 牧场名称
-	ManagerUser     string      `protobuf:"bytes,3,opt,name=manager_user,json=managerUser,proto3" json:"manager_user,omitempty"`                      // 牧场负责人名称
-	ManagerPassword string      `protobuf:"bytes,4,opt,name=manager_password,json=managerPassword,proto3" json:"manager_password,omitempty"`          // 牧场负责人账号
-	ManagerPhone    string      `protobuf:"bytes,5,opt,name=manager_phone,json=managerPhone,proto3" json:"manager_phone,omitempty"`                   // 牧场负责人手机号
-	Address         string      `protobuf:"bytes,6,opt,name=address,proto3" json:"address,omitempty"`                                                 // 牧场地址
-	IsShow          IsShow_Kind `protobuf:"varint,7,opt,name=is_show,json=isShow,proto3,enum=backend.operation.IsShow_Kind" json:"is_show,omitempty"` // 是否启用
-	CreatedAt       int64       `protobuf:"varint,8,opt,name=Created_at,json=CreatedAt,proto3" json:"Created_at,omitempty"`                           // 创建时间
+	Id           int64       `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+	Name         string      `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`                                                       // 牧场名称
+	ManagerUser  string      `protobuf:"bytes,3,opt,name=manager_user,json=managerUser,proto3" json:"manager_user,omitempty"`                      // 牧场负责人名称
+	ManagerPhone string      `protobuf:"bytes,5,opt,name=manager_phone,json=managerPhone,proto3" json:"manager_phone,omitempty"`                   // 牧场负责人手机号
+	Address      string      `protobuf:"bytes,6,opt,name=address,proto3" json:"address,omitempty"`                                                 // 牧场地址
+	IsShow       IsShow_Kind `protobuf:"varint,7,opt,name=is_show,json=isShow,proto3,enum=backend.operation.IsShow_Kind" json:"is_show,omitempty"` // 是否启用
+	CreatedAt    int64       `protobuf:"varint,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`                           // 创建时间
 }
 
 func (x *AddPastureRequest) Reset() {
@@ -88,13 +87,6 @@ func (x *AddPastureRequest) GetManagerUser() string {
 	return ""
 }
 
-func (x *AddPastureRequest) GetManagerPassword() string {
-	if x != nil {
-		return x.ManagerPassword
-	}
-	return ""
-}
-
 func (x *AddPastureRequest) GetManagerPhone() string {
 	if x != nil {
 		return x.ManagerPhone
@@ -225,7 +217,7 @@ type SearchPastureResponse struct {
 
 	Page  int32                `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"`
 	Total int32                `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"`
-	Data  []*AddPastureRequest `protobuf:"bytes,3,rep,name=data,proto3" json:"data,omitempty"`
+	List  []*AddPastureRequest `protobuf:"bytes,3,rep,name=list,proto3" json:"list,omitempty"`
 }
 
 func (x *SearchPastureResponse) Reset() {
@@ -274,9 +266,9 @@ func (x *SearchPastureResponse) GetTotal() int32 {
 	return 0
 }
 
-func (x *SearchPastureResponse) GetData() []*AddPastureRequest {
+func (x *SearchPastureResponse) GetList() []*AddPastureRequest {
 	if x != nil {
-		return x.Data
+		return x.List
 	}
 	return nil
 }
@@ -289,48 +281,45 @@ var file_backend_operation_pasture_proto_rawDesc = []byte{
 	0x6f, 0x12, 0x11, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61,
 	0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x1c, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x6f, 0x70,
 	0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x65, 0x6e, 0x75, 0x6d, 0x2e, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x22, 0x9c, 0x02, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x50, 0x61, 0x73, 0x74, 0x75, 0x72,
+	0x74, 0x6f, 0x22, 0xf1, 0x01, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x50, 0x61, 0x73, 0x74, 0x75, 0x72,
 	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
 	0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
 	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c,
 	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01,
 	0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12,
-	0x29, 0x0a, 0x10, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77,
-	0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
-	0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61,
-	0x6e, 0x61, 0x67, 0x65, 0x72, 0x5f, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x12,
-	0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x37, 0x0a, 0x07, 0x69, 0x73, 0x5f,
-	0x73, 0x68, 0x6f, 0x77, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x62, 0x61, 0x63,
-	0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x49,
-	0x73, 0x53, 0x68, 0x6f, 0x77, 0x2e, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x06, 0x69, 0x73, 0x53, 0x68,
-	0x6f, 0x77, 0x12, 0x1d, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74,
-	0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41,
-	0x74, 0x22, 0xdd, 0x01, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x50, 0x61, 0x73, 0x74,
-	0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61,
-	0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1b,
-	0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e,
-	0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
-	0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x18,
-	0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x55, 0x73,
-	0x65, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x5f, 0x70, 0x68,
-	0x6f, 0x6e, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67,
-	0x65, 0x72, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74,
-	0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61,
-	0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69,
-	0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d,
-	0x65, 0x22, 0x7b, 0x0a, 0x15, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x50, 0x61, 0x73, 0x74, 0x75,
-	0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61,
-	0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x14,
-	0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x74,
-	0x6f, 0x74, 0x61, 0x6c, 0x12, 0x38, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x03,
-	0x28, 0x0b, 0x32, 0x24, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x65,
-	0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x64, 0x64, 0x50, 0x61, 0x73, 0x74, 0x75, 0x72,
-	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0f,
-	0x5a, 0x0d, 0x2e, 0x3b, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x62, 0x62,
-	0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x23, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x5f, 0x70, 0x68, 0x6f, 0x6e, 0x65,
+	0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50,
+	0x68, 0x6f, 0x6e, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18,
+	0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x37,
+	0x0a, 0x07, 0x69, 0x73, 0x5f, 0x73, 0x68, 0x6f, 0x77, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32,
+	0x1e, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74,
+	0x69, 0x6f, 0x6e, 0x2e, 0x49, 0x73, 0x53, 0x68, 0x6f, 0x77, 0x2e, 0x4b, 0x69, 0x6e, 0x64, 0x52,
+	0x06, 0x69, 0x73, 0x53, 0x68, 0x6f, 0x77, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74,
+	0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65,
+	0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xdd, 0x01, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63,
+	0x68, 0x50, 0x61, 0x73, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+	0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70,
+	0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
+	0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
+	0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x5f,
+	0x75, 0x73, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x6e, 0x61,
+	0x67, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+	0x65, 0x72, 0x5f, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
+	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x12, 0x1d, 0x0a, 0x0a,
+	0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03,
+	0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65,
+	0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x65,
+	0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x7b, 0x0a, 0x15, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68,
+	0x50, 0x61, 0x73, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+	0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70,
+	0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x05, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x38, 0x0a, 0x04, 0x6c, 0x69, 0x73,
+	0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e,
+	0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x64, 0x64, 0x50,
+	0x61, 0x73, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x04, 0x6c,
+	0x69, 0x73, 0x74, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x3b, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69,
+	0x6f, 0x6e, 0x50, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -354,7 +343,7 @@ var file_backend_operation_pasture_proto_goTypes = []interface{}{
 }
 var file_backend_operation_pasture_proto_depIdxs = []int32{
 	3, // 0: backend.operation.AddPastureRequest.is_show:type_name -> backend.operation.IsShow.Kind
-	0, // 1: backend.operation.SearchPastureResponse.data:type_name -> backend.operation.AddPastureRequest
+	0, // 1: backend.operation.SearchPastureResponse.list:type_name -> backend.operation.AddPastureRequest
 	2, // [2:2] is the sub-list for method output_type
 	2, // [2:2] is the sub-list for method input_type
 	2, // [2:2] is the sub-list for extension type_name

File diff suppressed because it is too large
+ 1126 - 6
proto/go/backend/operation/system.pb.go


+ 67 - 0
service/sso/cache.go

@@ -0,0 +1,67 @@
+package sso
+
+import (
+	"encoding/json"
+	"fmt"
+	"kpt-tmr-group/config"
+	"kpt-tmr-group/pkg/tool"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
+
+	"kpt-tmr-group/pkg/xerr"
+
+	"github.com/go-redis/redis"
+	redisv7 "github.com/go-redis/redis/v7"
+)
+
+type Cache struct {
+	Client *redisv7.Client
+	Expiry time.Duration
+}
+
+func NewCache(cfg *config.AppConfig) *Cache {
+	return &Cache{
+		Client: NewClientLatest(cfg),
+		Expiry: time.Duration(cfg.RedisSetting.SSOCache.Expiry) * time.Minute,
+	}
+}
+
+func (c *Cache) Auth(userAuth *operationPb.UserAuth) (string, error) {
+	return c.get(fmt.Sprintf("sso:auth:%s", tool.Md5String(fmt.Sprintf("%s-%s", userAuth.UserName, userAuth.Password))))
+}
+
+func (c *Cache) CacheAuth(token string, res interface{}) error {
+	return c.set(fmt.Sprintf("sso:auth:%s", token), res)
+}
+
+func (c *Cache) GetAccount(token string) (interface{}, error) {
+	return c.get(fmt.Sprintf("sso:get_account:%s", token))
+}
+
+func (c *Cache) CacheSetAccount(token string, res interface{}) error {
+	return c.set(fmt.Sprintf("sso:get_account:%s", token), res)
+}
+
+func (c *Cache) get(key string) (string, error) {
+	bs, err := c.Client.Get(key).Bytes()
+	if err != nil {
+		return "", xerr.WithMessage(err, key)
+	}
+
+	if len(bs) == 0 {
+		return "", xerr.WithStack(redis.Nil)
+	}
+
+	return string(bs), nil
+}
+
+func (c *Cache) set(key string, res interface{}) error {
+	if res == nil {
+		return nil
+	}
+	b, _ := json.Marshal(res)
+	if err := c.Client.Set(key, string(b), c.Expiry).Err(); err != nil {
+		return xerr.WithMessage(err, key, string(b))
+	}
+	return nil
+}

+ 45 - 0
service/sso/sso.go

@@ -0,0 +1,45 @@
+package sso
+
+import (
+	"kpt-tmr-group/config"
+	"kpt-tmr-group/pkg/di"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+
+	redisv7 "github.com/go-redis/redis/v7"
+)
+
+var Module = di.Provide(NewSSOClient)
+
+type ClientInterface interface {
+	Auth(userAuth *operationPb.UserAuth) (string, error)
+	CacheAuth(token string, res interface{}) error
+	CacheSetAccount(token string, res interface{}) error
+	GetAccount(token string) (interface{}, error)
+	// Permissions(token string) (*Response, error)
+	// CheckPermission(token, code string) (bool, error)
+}
+
+func NewClientLatest(cfg *config.AppConfig) *redisv7.Client {
+	return initClientLatest(cfg.RedisSetting.SSOCache.Addr, cfg.RedisSetting.SSOCache.Requirepass, cfg.RedisSetting.SSOCache.DB)
+}
+
+func initClientLatest(addr, password string, db int, opts ...func(*redisv7.Options)) *redisv7.Client {
+	option := &redisv7.Options{
+		Addr:       addr,
+		DB:         db,
+		Password:   password,
+		MaxRetries: 3,
+		OnConnect: func(cn *redisv7.Conn) error {
+			return cn.ClientSetName("on_connect").Err()
+		},
+	}
+	for _, opt := range opts {
+		opt(option)
+	}
+
+	return redisv7.NewClient(option)
+}
+
+func NewSSOClient(cfg *config.AppConfig) ClientInterface {
+	return NewCache(cfg)
+}

+ 0 - 4
store/kptstore/rw_store.go

@@ -3,12 +3,9 @@ package kptstore
 import (
 	"kpt-tmr-group/config"
 	"kpt-tmr-group/pkg/logger/logrus"
-	"kpt-tmr-group/pkg/logger/zaplog"
 	"kpt-tmr-group/pkg/xerr"
 	"time"
 
-	"go.uber.org/zap"
-
 	"gorm.io/driver/mysql"
 	"gorm.io/gorm"
 	"gorm.io/gorm/logger"
@@ -50,7 +47,6 @@ func MustNewStore(cfg *config.AppConfig) *DB {
 		&gorm.Config{Logger: newLogger},
 	)
 	if err != nil {
-		zaplog.Error("MustNewStore", zap.String("Err", err.Error()), zap.Any("KptEventDSNRW", cfg.StoreSetting.KptEventDSNRW))
 		panic(xerr.WithStack(err))
 	}
 

Some files were not shown because too many files changed in this diff