Browse Source

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

xuyiping 1 year ago
parent
commit
ee5182199f
100 changed files with 8942 additions and 867 deletions
  1. 1 0
      .gitignore
  2. 21 0
      Makefile
  3. 6 0
      README.md
  4. 116 0
      backend/common/errors.proto
  5. 74 0
      backend/operation/enum.proto
  6. 51 0
      backend/operation/feed_formula.proto
  7. 33 0
      backend/operation/mobile.proto
  8. 12 0
      backend/operation/pagination.proto
  9. 212 0
      backend/operation/pasture.proto
  10. 86 0
      backend/operation/statistic.proto
  11. 210 0
      backend/operation/system.proto
  12. 2 2
      cmd/http.go
  13. 34 6
      config/app.go
  14. 23 3
      config/app.test.yaml
  15. 9 1
      dep/dep.go
  16. 1 1
      dep/di.go
  17. 4 1
      dep/di_http.go
  18. 44 8
      go.mod
  19. 75 705
      go.sum
  20. 2 6
      http/handler/default.go
  21. 250 0
      http/handler/feed/feed_formula.go
  22. 33 0
      http/handler/mobile/mobile_list.go
  23. 278 0
      http/handler/pasture/cattle_forage_category.go
  24. 249 0
      http/handler/pasture/forage_list.go
  25. 161 0
      http/handler/pasture/pasture_list.go
  26. 33 0
      http/handler/statistic/analysis.go
  27. 158 0
      http/handler/system/menu.go
  28. 134 0
      http/handler/system/role.go
  29. 264 0
      http/handler/system/user.go
  30. 34 28
      http/middleware/cors.go
  31. 11 0
      http/middleware/hub.go
  32. 10 4
      http/middleware/pagination.go
  33. 2 2
      http/middleware/sentry.go
  34. 57 0
      http/middleware/sso.go
  35. 2 2
      http/route/api_debug_route.go
  36. 92 9
      http/route/app_api.go
  37. 3 3
      main.go
  38. 67 0
      model/cattle_category.go
  39. 0 17
      model/event_fileds.go
  40. 79 0
      model/feed_formula.go
  41. 91 0
      model/forage.go
  42. 67 0
      model/forage_category.go
  43. 132 0
      model/formula_estimate.go
  44. 73 0
      model/group_pasture.go
  45. 0 19
      model/kpe_event.go
  46. 0 19
      model/kpt_fileds.go
  47. 28 0
      model/system_group_pasture_permissions.go
  48. 124 0
      model/system_menu.go
  49. 28 0
      model/system_menu_permissions.go
  50. 33 0
      model/system_mobile.go
  51. 28 0
      model/system_mobile_permissions.go
  52. 61 0
      model/system_role.go
  53. 128 0
      model/system_user.go
  54. 9 0
      model/wechat.go
  55. 288 0
      module/backend/feed_service.go
  56. 129 0
      module/backend/interface.go
  57. 709 0
      module/backend/pasture_service.go
  58. 59 0
      module/backend/statistic_service.go
  59. 255 0
      module/backend/system_permissions.go
  60. 603 0
      module/backend/system_service.go
  61. 34 0
      module/backend/wx_applet_service.go
  62. 207 0
      pkg/apierr/apierr.go
  63. 761 0
      pkg/apierr/apierr_gen.go
  64. 47 0
      pkg/apierr/config.go
  65. 23 0
      pkg/apiok/common.go
  66. 5 5
      pkg/cleanup/entry.go
  67. 0 0
      pkg/cleanup/entry_test.go
  68. 0 0
      pkg/cputil/cp.go
  69. 0 0
      pkg/cputil/cp_test.go
  70. 0 0
      pkg/di/annotation.go
  71. 3 3
      pkg/di/hub.go
  72. 0 0
      pkg/di/hub_test.go
  73. 1 1
      pkg/di/option.go
  74. 4 4
      pkg/di/xreflect/reflect.go
  75. 2 2
      pkg/di/xreflect/reflect_test.go
  76. 0 0
      pkg/di/xreflect/stack.go
  77. 11 11
      pkg/di/xreflect/stack_test.go
  78. 68 0
      pkg/ginutil/bind.go
  79. 34 0
      pkg/ginutil/bind_proto.go
  80. 19 0
      pkg/ginutil/json_proto_response.go
  81. 322 0
      pkg/ginutil/setter.go
  82. 57 0
      pkg/grpcutil/client_conn.go
  83. 61 0
      pkg/grpcutil/error.go
  84. 22 0
      pkg/grpcutil/grpc_error.go
  85. 30 0
      pkg/jsonpb/decode.go
  86. 33 0
      pkg/jsonpb/decode_test.go
  87. 52 0
      pkg/jsonpb/encode.go
  88. 37 0
      pkg/jsonpb/encode_test.go
  89. 108 0
      pkg/jsonpb/query_decode.go
  90. 9 0
      pkg/jsonpb/query_decode_test.go
  91. 63 0
      pkg/jwt/jwt.go
  92. 16 4
      pkg/logger/logrus/log.go
  93. 84 0
      pkg/logger/zaplog/log.go
  94. 0 0
      pkg/runtimeutil/caller.go
  95. 0 0
      pkg/runtimeutil/caller_test.go
  96. 1 1
      pkg/sentry/sentry.go
  97. 69 0
      pkg/tool/tool.go
  98. 124 0
      pkg/tool/tool_test.go
  99. 658 0
      pkg/valid/README.md
  100. 199 0
      pkg/valid/error.go

+ 1 - 0
.gitignore

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

+ 21 - 0
Makefile

@@ -0,0 +1,21 @@
+GO_FILES=`go list ./... | grep -v -E "mock|store|test|fake|cmd|bin|backend|google|logger|proto"`
+
+proto-build:
+	protoc -I=. --go_out=:./proto/go/ --go_opt=paths=source_relative \
+    --go-grpc_out=:./proto/go/ --go-grpc_opt=paths=source_relative ./backend/common/*.proto
+
+	protoc -I=. --go_out=:./proto/go/ --go_opt=paths=source_relative \
+    --go-grpc_out=:./proto/go/ --go-grpc_opt=paths=source_relative ./backend/operation/*.proto
+
+ci-test:
+	go test $(GO_FILES) -coverprofile .cover.txt
+	go tool cover -func .cover.txt
+	rm .cover.txt
+
+lint:
+	golangci-lint run ./...
+
+build:
+	rm -rf bin
+	mkdir -p bin
+	GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -o bin/kptEvent -ldflags "-X kpt.kptyun.cn:3000/kpt-event/pod.appVersion=${version}" main.go

+ 6 - 0
README.md

@@ -30,3 +30,9 @@ lint:
 需要更新 gomock:
 - 生成 mock 前,请确保你能够编译 & 编译完成
 - make generate
+
+TODO 列表
+- proto3 int64 jsonpb处理后自动转成string
+  * 现在处理的方式是把int64类型改成int32类型
+- 用户登出没有用redis做缓存,所以后端没有提供登出接口,有以下弊端
+  * 用户的token没有过期,如被人劫持,会被使用到直到token过期

+ 116 - 0
backend/common/errors.proto

@@ -0,0 +1,116 @@
+syntax = "proto3";
+
+package backend.common;
+
+option go_package = ".;commonPb";
+
+message Error {
+  enum Code {
+    OK = 0;
+
+    reserved 1 to 9999;
+
+    // ========= Common =========
+
+    // 鉴权
+    UNAUTHORIZED = 10000;
+    reserved 10001 to 10999;
+
+    // 通用请求错误
+    BAD_REQUEST = 11000;
+    INVALID_CONTENT_TYPE = 11001;
+    INVALID_CONTENT_ENCODING = 11002;
+    TOO_MANY_REQUESTS = 11003;
+    reserved 11004 to 11099;
+
+    // ========= Biz =========
+
+    // Config
+    INVALID_STORAGE_TYPE = 11100;
+    reserved 11101 to 11199;
+
+    // DataEvent
+    INVALID_DE_DATA = 11200;
+    reserved 11201 to 19999;
+
+    // Checkin
+    CHECKIN_REPEATED = 20000;
+    reserved 20001 to 20999;
+
+    // Course
+    COURSE_NOT_FOUND = 21000;
+    // 课程没有权益
+    COURSE_NOT_INTEREST = 21001;
+    reserved 21002 to 21999;
+
+    // Recommend
+    MODULE_NOT_FOUND = 22000;
+    reserved 22001 to 22999;
+
+    // User Course
+    USER_COURSE_ALREADY_ADDED = 23000;
+    USER_COURSE_NOT_FOUND = 23001;
+    reserved 23002 to 23999;
+
+    // PT
+    PT_LIMITED = 24000;
+    reserved 24001 to 24099;
+
+    // Payment 24100 - 24299
+
+    // 无效的价格
+    INVALID_PRICE = 24100;
+    // 无效的 product ID
+    INVALID_PRODUCT_ID = 24101;
+    // 无效的订单号
+    INVALID_ORDER_NUMBER = 24102;
+    // 无效的用户 ID
+    INVALID_USER_ID = 24103;
+    // 无效的收据
+    INVALID_RECEIPT = 24104;
+    // 异常的 iOS 收据,需要客户端 check 和重试
+    EMPTY_IOS_RECEIPT = 24105;
+
+    reserved 24106 to 24299;
+
+    // UserPlan 相关
+    // 免费用户限制 plan 课程数量
+    USER_PLAN_LIMITED_COURSE_COUNT = 24300;
+
+
+    // 保留业务段 24400 to 89999
+    reserved 24400 to 89999;
+
+    // ========= SYSTEM =========
+    // 服务自身错误
+    INTERNAL_ERROR = 90000;
+    reserved 90001 to 90099;
+
+    // encoding/decoding error
+
+    // JSONPB encoding/decoding with error
+    JSONPB_ERROR = 90100;
+
+    // JSON encoding/decoding with error
+    JSON_ERROR = 90101;
+
+    // PB encoding/decoding with error
+    PB_ERROR = 90102;
+    reserved 90103 to 90999;
+
+    // 依赖服务错误
+    EXTERNAL_ERROR = 91000;
+
+    reserved 91001 to max;
+  }
+
+  // 业务错误码
+  Code code = 1;
+
+  // 错误信息
+  string msg = 2;
+
+  // 补充错误信息
+  // @optional
+  repeated string errors = 3;
+}

+ 74 - 0
backend/operation/enum.proto

@@ -0,0 +1,74 @@
+syntax = "proto3";
+
+package backend.operation;
+
+option go_package = ".;operationPb";
+
+// 字段类型
+message IsShow {
+  enum Kind {
+    INVALID = 0;  // 无效
+    OK = 1;       // 是
+    NO = 2;       // 否
+  }
+}
+
+message CattleCategoryParent {
+  enum Kind {
+    INVALID = 0;           // 无效
+    LACTATION_CAW = 1;     // 泌乳牛
+    FATTEN_CAW = 2;        // 育肥牛
+    RESERVE_CAW  = 3;      // 后备牛
+    DRY_CAW = 4;           // 干奶牛
+    PERINATAL_CAW  = 5;    // 泌乳牛
+    OTHER_CAW = 6;         // 其他
+  }
+}
+
+// 饲料类别
+message ForageCategoryParent {
+  enum Kind {
+    INVALID = 0;           // 无效
+    ROUGHAGE = 1;          // 粗料
+    CONCENTRATE  = 2;      // 精料(浓缩料)
+    HALF_ROUGHAGE_HALF_CONCENTRATE = 3;   // 粗料精料各半
+    OTHER = 4;        // 其他
+  }
+}
+
+// 饲料来源
+message ForageSource {
+  enum Kind {
+    INVALID = 0;           // 无效
+    SYSTEM_BUILT_IN = 1;   // 系统内置
+    USER_DEFINED = 2;      // 用户自定义
+  }
+}
+
+// 跳转延迟
+message JumpDelaType {
+  enum Kind {
+    INVALID = 0;           // 禁用
+    THREE = 1;             // 3秒
+    SIX = 2;               // 6秒
+    NINE = 3;              // 9秒
+  }
+}
+
+// 计划类型
+message ForagePlanType {
+  enum Kind {
+    INVALID = 0;           // 无
+    FORKLIFT = 1;          // 铲车
+    CONCENTRATE = 2;       // 精料
+  }
+}
+
+message DataSource {
+  enum Kind {
+    INVALID = 0;           // 无
+    BACKGROUND_ADD = 1;    // 后台手动添加
+    EXCEL_IMPORT = 2;     // excel报表导入
+    FROM_PASTURE = 3;     // 来自牧场
+  }
+}

+ 51 - 0
backend/operation/feed_formula.proto

@@ -0,0 +1,51 @@
+syntax = "proto3";
+package backend.operation;
+
+option go_package = ".;operationPb";
+
+import "backend/operation/pagination.proto";
+import "backend/operation/enum.proto";
+
+message AddFeedFormulaRequest {
+  int32 id = 1;
+  string name = 2;                   // 名称
+  string  encode_number = 3;         // 编码
+  string colour = 4;                 // 颜色
+  CattleCategoryParent.Kind cattle_category_id = 5;      // 畜牧类别id
+  string cattle_category_name = 6;   // 畜牧类型名称
+  int32 formula_type_id = 7;         // 配方类型id
+  string formula_type_name = 8;      // 配方类型名称
+  DataSource.Kind data_source_id = 9;   // 数据来源
+  string data_source_name = 10;   // 数据来源
+  string remarks = 11;               // 备注
+  int32 version = 12;                // 版本号
+  IsShow.Kind is_show = 13;          // 是否启用
+  IsShow.Kind is_modify = 14;        // 是否可修改
+  int32 created_at = 15;             // 创建时间
+  string created_at_format = 16;     // 创建时间格式化
+  string Pasture_name = 17;          // 牧场名称
+}
+
+message SearchFeedFormulaRequest {
+  int32 cattle_category_id = 1;    // 分类id
+  int32 formula_type_id = 2;       // 配方类型id
+  int32 data_source = 3;           // 饲料来源
+  IsShow.Kind is_show = 4;         // 是否启用
+  string name = 5;                 // 配方名称
+  string remarks = 6;              // 备注
+  PaginationModel pagination = 7;  // 分页
+}
+
+message SearchFeedFormulaListResponse {
+  int32 page = 1;
+  int32 page_size = 2;
+  int32 total = 3;
+  repeated AddFeedFormulaRequest list = 4;
+}
+
+// 是否启用
+message IsShowModifyFeedFormula {
+  int32 feed_formula_id = 1;
+  IsShow.Kind is_show = 2;
+  int32 edit_type = 3;                  // 1 更新是否启用 2 更新 modify
+}

+ 33 - 0
backend/operation/mobile.proto

@@ -0,0 +1,33 @@
+syntax = "proto3";
+package backend.operation;
+
+option go_package = ".;operationPb";
+
+import "backend/operation/pagination.proto";
+
+message SearchMobileRequest {
+  string name = 1;       // 名称
+  PaginationModel pagination = 2; // 分页
+}
+
+message SearchMobileResponse {
+  int32 code = 1;
+  string msg = 2;
+  SearchMobileData data = 3;
+}
+
+message SearchMobileData {
+  int32 page = 1;
+  int32 total = 2;
+  int32 page_size = 3;
+  repeated  MobileData list = 4;
+}
+
+message MobileData {
+  uint32 id = 1;
+  string name = 2;
+  uint32 created_at = 3;
+  string created_at_format = 4;
+}
+
+

+ 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;
+}

+ 212 - 0
backend/operation/pasture.proto

@@ -0,0 +1,212 @@
+syntax = "proto3";
+package backend.operation;
+
+option go_package = ".;operationPb";
+
+import "backend/operation/enum.proto";
+import "backend/operation/pagination.proto";
+
+message AddPastureRequest {
+  int32 id = 1;
+  string name = 2; // 牧场名称
+  string account = 3;   // 登录账号名称
+  string manager_user = 4; // 牧场负责人名称
+  string manager_phone = 5;   // 牧场负责人手机号
+  string address = 6;   // 牧场地址
+  IsShow.Kind is_show = 7;    // 是否启用
+  int32 created_at = 8;    // 创建时间
+  string created_at_format = 9;    // 创建时间格式化
+}
+
+message SearchPastureRequest {
+  string name = 1;       // 牧场名称
+  string manager_user = 2; // 牧场负责人名称
+  string manager_phone = 3;   // 牧场负责人手机号
+  uint32 start_time = 4;       // 开始时间
+  uint32 end_time = 5;    // 结束时间
+  PaginationModel pagination = 6; // 分页
+}
+
+message SearchPastureResponse {
+  int32 code = 1;
+  string msg = 2;
+  SearchPastureData data = 3;
+}
+
+message SearchPastureData {
+  int32 page = 1;
+  int32 total = 2;
+  int32 page_size = 3;
+  repeated AddPastureRequest list = 4;
+}
+
+message IsShowGroupPasture {
+  int32 pasture_id = 1;    // 牧场id
+  IsShow.Kind is_show = 2;     // 是否开启
+}
+
+// 添加牧畜分类
+message AddCattleCategoryRequest {
+  uint32 id = 1;
+  CattleCategoryParent.Kind parent_id = 2;  // 父类id
+  string parent_name = 3;        // 父类名称
+  string name = 4;               // 牧畜分类名称
+  string number = 5;            // 畜牧类别编号
+  IsShow.Kind is_show = 6;      // 是否启用
+  uint32 created_at = 7;         // 创建时间
+  string created_at_format = 8;   // 创建时间格式
+}
+
+// 是否启用
+message IsShowCattleCategory {
+  int32 cattle_category_id = 1;
+  IsShow.Kind is_show = 2;
+}
+
+// 畜牧分类查询列表
+message SearchCattleCategoryRequest {
+  string parent_name = 1;
+  IsShow.Kind is_show = 2;
+  string name = 3;
+  PaginationModel pagination = 4; // 分页
+}
+
+message SearchCattleCategoryResponse {
+  int32 code = 1;
+  string msg = 2;
+  SearchCattleCategoryData data = 3;
+}
+
+message SearchCattleCategoryData {
+  int32 page = 1;
+  int32 total = 2;
+  repeated AddCattleCategoryRequest list = 3;
+}
+
+// 添加饲料分类
+message AddForageCategoryRequest {
+  uint32 id = 1;
+  ForageCategoryParent.Kind parent_id = 2;  // 父类id
+  string parent_name = 3;        // 父类名称
+  string name = 4;               // 牧畜分类名称
+  string number = 5;            // 畜牧类别编号
+  IsShow.Kind is_show = 6;      // 是否启用
+  uint32 created_at = 7;         // 创建时间
+  string created_at_format = 8;   // 创建时间格式
+}
+
+// 是否启用
+message IsShowForageCategory {
+  int32 forage_category_id = 1;
+  IsShow.Kind is_show = 2;
+}
+
+// 饲料分类查询列表
+message SearchForageCategoryRequest {
+  string parent_name = 1;
+  IsShow.Kind is_show = 2;
+  string name = 3;
+  PaginationModel pagination = 4; // 分页
+}
+
+message SearchForageCategoryResponse {
+  int32 code = 1;
+  string msg = 2;
+    SearchForageCategoryData data = 3;
+}
+
+message SearchForageCategoryData {
+  int32 page = 1;
+  int32 total = 2;
+  repeated AddForageCategoryRequest list = 3;
+}
+
+// 饲料列表
+message AddForageRequest {
+  uint32 id = 1;
+  string name = 2;                // 饲料名称
+  uint32 category_id = 3;          // 饲料分类id
+  string category_name = 4;       // 饲料分类名称
+  uint32 material_type = 5;        // 物料类型
+  string unique_encode = 7;       // 唯一编码
+  ForageSource.Kind forage_source_id = 8;       // 饲料来源
+  ForagePlanType.Kind plan_type_id = 9;       // 计划类型
+  string  small_material_scale = 10;           // 小料称
+  uint32 allow_error = 11;                     // 允许误差 (单位kg)
+  uint32 package_weight = 12;                  // 包装重量 (单位kg)
+  uint32 price = 13;                          // 单价(单位分)
+  uint32 jump_weight = 14;                    // 跳转重量域(单位kg)
+  JumpDelaType.Kind jump_delay = 15;         // 跳转延迟
+  IsShow.Kind confirm_start = 16;            // 确认开始
+  uint32  relay_locations = 17;               // 继电器位置
+  IsShow.Kind jmp = 18;                     // 无上域
+  string backup1 = 19;                      // 备用字段1
+  string backup2 = 20;                      // 备用字段2
+  string backup3 = 21;                      // 备用字段3
+  uint32 created_at = 22;                    // 创建时间
+  string created_at_format = 23;            // 创建时间格式化
+  IsShow.Kind is_show = 24;                  // 是否启用
+}
+
+message SearchForageListRequest {
+  string name = 1;   // 饲料名称
+  string category_id = 2;   // 饲料分类id
+  uint32 forage_source_id = 3;   // 饲料来源
+  IsShow.Kind is_show = 4;    // 是否启用
+  uint32 allow_error = 5;      // 允许误差
+  uint32 jump_weight = 6;      // 跳转重量域
+  JumpDelaType.Kind  jump_delay = 7;   // 跳转延迟
+  PaginationModel pagination = 8; // 分页
+}
+
+message SearchForageListResponse {
+  int32 page = 1;
+  int32 page_size = 2;
+  int32 total = 3;
+  repeated AddForageRequest list = 4;
+}
+
+// 是否启用
+message IsShowForage {
+  int32 forage_id = 1;
+  IsShow.Kind is_show = 2;
+}
+
+message ForageEnumList {
+  repeated ForageSourceEnum forage_source = 1;
+  repeated ForagePlanTypeEnum forage_plan_type = 2;
+  repeated JumpDelaTypeEnum jump_dela_type = 3;
+  repeated CattleParentCategoryEnum cattle_parent_category = 4;
+  repeated ForageParentCategoryEnum forage_parent_category = 5;
+  repeated IsShowEnum is_show = 6;
+}
+
+message ForageSourceEnum {
+  ForageSource.Kind value = 1;
+  string label = 2;
+}
+
+message ForagePlanTypeEnum {
+  ForagePlanType.Kind value = 1;
+  string label = 2;
+}
+
+message JumpDelaTypeEnum {
+  JumpDelaType.Kind value = 1;
+  string label = 2;
+}
+
+message CattleParentCategoryEnum {
+  CattleCategoryParent.Kind value = 1;
+  string label = 2;
+}
+
+message ForageParentCategoryEnum {
+  ForageCategoryParent.Kind value = 1;
+  string label = 2;
+}
+
+message IsShowEnum {
+  IsShow.Kind value = 1;
+  string label = 2;
+}

+ 86 - 0
backend/operation/statistic.proto

@@ -0,0 +1,86 @@
+syntax = "proto3";
+package backend.operation;
+
+option go_package = ".;operationPb";
+
+
+import "backend/operation/pagination.proto";
+
+// 添加配方评估 具体字段含义参照formula_estimate表对应的字段
+message AddFormulaEstimateRequest {
+  int32 id = 1;
+  int32 pasture_id = 2;
+  string pasture_name = 3;
+  int32 barn_id = 4;
+  int32 feed_formula_id = 5;
+  string feed_formula_name = 6;
+  int32 cow_number = 7;
+  int32 dry_formula_number = 8;
+  int32 dry_tmr_feed = 9;
+  int32 dry_food_intake = 10;
+  int32 mj_formula_number = 11;
+  int32 mj_tmr_feed = 12;
+  int32 mj_food_intake = 13;
+  int32 nnd_formula_number = 14;
+  int32 nnd_tmr_feed = 15;
+  int32 nnd_food_intake = 16;
+  int32 cpg_formula_number = 17;
+  int32 cpg_tmr_feed = 18;
+  int32 cpg_food_intake = 19;
+  int32 pg_formula_number = 20;
+  int32 pg_tmr_feed = 21;
+  int32 pg_food_intake = 22;
+  int32 dm_formula_number = 23;
+  int32 dm_tmr_feed = 24;
+  int32 dm_food_intake = 25;
+  int32 cpdm_formula_number = 26;
+  int32 cpdm_tmr_feed = 27;
+  int32 cpdm_food_intake = 28;
+  int32 fat_formula_number = 29;
+  int32 fat_tmr_feed = 30;
+  int32 fat_food_intake = 31;
+  int32 starch_formula_number = 32;
+  int32 starch_tmr_feed = 33;
+  int32 starch_food_intake = 34;
+  int32 ndf_formula_number = 35;
+  int32 ndf_tmr_feed = 36;
+  int32 ndf_food_intake = 37;
+  int32 cp_ndf_formula_number = 38;
+  int32 cp_ndf_tmr_feed = 39;
+  int32 cp_ndf_food_intake = 40;
+  int32 adf_formula_number = 41;
+  int32 adf_tmr_feed = 42;
+  int32 adf_food_intake = 43;
+  int32 calcium_formula_number = 44;
+  int32 calcium_tmr_feed = 45;
+  int32 calcium_food_intake = 46;
+  int32 pdm_formula_number = 47;
+  int32 pdm_tmr_feed = 48;
+  int32 pdm_food_intake = 49;
+  int32 cf_ratio_formula_number = 50;
+  int32 cf_ratio_tmr_feed = 51;
+  int32 cf_ratio_food_intake = 52;
+  int32 created_at = 53;
+  string created_at_format = 54;
+}
+
+message SearchFormulaEstimateRequest {
+  string start_time  = 1;     // 开始时间
+  string end_time    = 2;     // 结束时间
+  int32 search_type = 3;      // 查询方式  1 安照配方 2 按照栏舍
+  string name = 4;            // 名称
+  PaginationModel pagination = 5; // 分页
+}
+
+message SearchFormulaEstimateResponse {
+  int32 code = 1;
+  string msg = 2;
+  SearchFormulaEstimate data = 3;
+}
+
+message SearchFormulaEstimate {
+  int32 page = 1;
+  int32 total = 2;
+  int32 page_size = 3;
+  repeated AddFormulaEstimateRequest list = 4;
+}

+ 210 - 0
backend/operation/system.proto

@@ -0,0 +1,210 @@
+syntax = "proto3";
+package backend.operation;
+
+option go_package = ".;operationPb";
+
+import "backend/operation/enum.proto";
+import "backend/operation/pagination.proto";
+import "backend/operation/pasture.proto";
+
+message CommonOK {
+  int32 code = 1;
+  string msg = 2;
+  Success data = 3;
+}
+
+message Success {
+  bool success = 1;
+}
+
+message WxOpenId {
+  string openid = 1;
+}
+
+// 用户角色
+message AddRoleRequest {
+  uint32 id = 1;
+  string name = 2;        // 角色名称
+  string remarks = 3;     // 角色备注
+  IsShow.Kind is_show = 4;     // 是否启用
+  repeated uint32 pasture_id = 5;    // 牧场id
+  repeated uint32 menu_id = 6;       // 菜单id
+  repeated uint32 mobile_id = 7;     // 移动端id
+  string create_user = 8;          // 创建用户
+  uint32 created_at = 9;             // 创建时间
+  string created_at_format = 10;     // 创建时间格式化
+}
+
+message SearchRoleRequest {
+  string name = 3;       // 角色名称
+  PaginationModel pagination = 2;  // 分页
+}
+
+message SearchRoleResponse {
+  uint32 code = 1;
+  string msg = 2;
+  SearchRoleData data = 3;
+}
+
+message SearchRoleData {
+  int32 page = 1;
+  int32 total = 2;
+  int32 page_size = 3;
+  repeated AddRoleRequest list = 4;
+}
+
+// 角色对应权限列表
+message RolePermissionsList {
+  int32 code = 1;
+  string msg = 2;
+  RolePermissionsData data = 3;
+}
+
+message RolePermissionsData {
+  repeated uint32 mobile_list = 1;    // 移动端权限id
+  repeated uint32 pasture_list = 2;    // 牧场端列表
+  repeated uint32 menu_list = 3;      // 后端权限列表
+}
+
+// 用户token
+message SystemToken {
+  int32 code =  1;
+  string msg = 2;
+  TokenData data = 3;
+}
+
+message TokenData {
+  string token = 1;
+}
+
+// 用户登录
+message UserAuth {
+  int32 code = 1;
+  string msg = 2;
+  UserAuthData data = 3;
+}
+
+message UserAuthData {
+  string user_name = 1;   // 用户名称
+  string password = 2;    // 用户密码
+  string phone = 3;       // 用户手机号
+  repeated UserRole roles  = 4;    // 用户角色
+  string employee_name = 5;    // 员工名称
+}
+
+message UserRole {
+  int32 id = 1;      // 角色id
+  string name = 2;   // 角色名称
+}
+
+// 用户详情
+message UserDetails {
+  int32 code = 1;
+  string msg = 2;
+  AddSystemUser data = 3;
+}
+
+
+message AddSystemUser {
+  int32 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;          // 创建人
+  int32 created_at = 8;            // 创建时间
+  string created_at_format = 9;      // 创建时间格式化
+  string role_name = 10;            // 角色名称
+  repeated int32 role_ids = 11;     // 角色ids
+}
+
+// 查询用户
+message SearchUserRequest {
+  string name = 1;               // 用户名称
+  string employee_name = 2;       // 员工姓名
+  IsShow.Kind is_show = 3;        // 是否启用
+  uint32 created_start_time = 4;   // 开始时间
+  uint32 created_end_time = 5;     // 结束时间
+  PaginationModel pagination = 6; // 分页
+}
+
+message SearchUserResponse {
+    int32 code = 1;
+    string msg = 2;
+    SearchUserData data = 3;
+}
+
+message SearchUserData  {
+  int32 page = 1;
+  int32 total = 2;
+  int32 page_size = 3;
+  repeated AddSystemUser list = 4;
+}
+
+message IsShowSystemUserRequest {
+  int32 user_id = 1;  // 用户id
+  IsShow.Kind is_show = 2;   // is_show
+}
+// 系统菜单权限
+message AddMenuRequest {
+  int32 id = 1;
+  string name = 2;           // 名称
+  int32 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;      // 重定向
+  int32 created_at = 12;     // 创建时间
+  string created_at_format = 13;      // 创建时间格式化
+  int32 level = 14;                   // 菜单等级
+  repeated AddMenuRequest children = 15;   // 子分类
+  bool affix = 16;
+  bool keepAlive = 17;
+}
+
+message IsShowSystemMenuRequest {
+  int32 menu_id = 1;  // 角色id
+  IsShow.Kind is_show = 2;   // is_show
+}
+
+// 查询菜单权限
+message SearchMenuRequest {
+  string name = 1;               // 菜单名称
+  PaginationModel pagination = 2; // 分页
+}
+
+message SearchMenuResponse {
+  int32 code = 1;
+  string msg = 2;
+  SearchMenuData data = 3;
+}
+
+message SearchMenuData {
+  int32 page = 1;
+  int32 total = 2;
+  repeated AddMenuRequest list = 3;
+}
+
+// 系统用户权限相关
+message SystemUserMenuPermissions {
+  int32 code = 1;
+  string msg = 2;
+  SystemUserMenuData data = 3;
+}
+
+message SystemUserMenuData {
+  repeated AddPastureRequest pasture_list = 1;    // 牧场列表
+  repeated AddMenuRequest menu_list = 2;          // 菜单列表
+  repeated AddMobileRequest mobile_list = 3;      // 移动端权限
+}
+
+// 移动端
+message AddMobileRequest {
+  uint32 id = 1;     // id
+  string name = 2;  // 名称
+}

+ 2 - 2
cmd/http.go

@@ -5,7 +5,7 @@ import (
 	"kpt-tmr-group/config"
 	"kpt-tmr-group/dep"
 	"kpt-tmr-group/http"
-	log "kpt-tmr-group/util/logger"
+	"kpt-tmr-group/pkg/logger/logrus"
 
 	"github.com/spf13/cobra"
 )
@@ -21,7 +21,7 @@ var httpCmd = &cobra.Command{
 
 func bootHTTPServer(cfg *config.AppConfig) {
 	dependency := dep.DIHttpDependency()
-	log.Info("kpt-tmr-group: boot HTTP server")
+	logrus.Info("kpt-tmr-group: boot HTTP server")
 	server := http.NewServer(
 		http.ExportLogOption(),
 		http.WithDependency(dependency),

+ 34 - 6
config/app.go

@@ -1,8 +1,7 @@
 package config
 
 import (
-	"kpt-tmr-group/util/di"
-	log "kpt-tmr-group/util/logger"
+	"kpt-tmr-group/pkg/di"
 	"os"
 	"strings"
 	"sync"
@@ -26,14 +25,44 @@ 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"`
+
+	ExcelSetting ExcelSetting `json:"excelSetting" yaml:"excel_setting"`
+
+	WechatSetting WechatSetting `json:"wechatSetting" yaml:"wechat_setting"`
+}
+
+type WechatSetting struct {
+	Appid  string `yaml:"appid"`
+	Secret string `yaml:"secret"`
+}
+
+type ExcelSetting struct {
+	SheetName string  `yaml:"sheet_name"` // = "Sheet1" //默认Sheet名称
+	Height    float64 `yaml:"height"`     // = 25.0     //默认行高度
 }
 
 // StoreSetting 数据库配置
 type StoreSetting struct {
 	// 开启 SyDb SQL 记录
+	DriverName      string `yaml:"driver_name" json:"driver_name"`
 	ShowSQL         bool   `yaml:"show_sql" env:"STORE_SHOW_SQL"`
-	KptEventDSNRW   string `yaml:"kpt_event_rw" env:"LINGO_COURSE_DSN_RW"`
-	KptEventDSNMigr string `yaml:"kpt_event_migr" env:"LINGO_COURSE_DSN_MIGR"`
+	KptEventDSNRW   string `yaml:"kpt_tmr_group_rw" env:"LINGO_COURSE_DSN_RW"`
+	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 {
@@ -49,14 +78,13 @@ func init() {
 		switch appEnv {
 		default:
 			err = Initialize("app.test.yaml", cfg)
-			log.SetLevel(log.ErrorLevel)
 		case "development":
 			err = Initialize("app.develop.yaml", cfg)
 		case "production":
 			err = Initialize("app.production.yaml", cfg)
 		}
 		if err != nil {
-			log.Fatalf("%+v", err)
+			panic(err)
 		}
 		options = cfg
 	})

+ 23 - 3
config/app.test.yaml

@@ -1,8 +1,28 @@
-app_name: kpt-tmr-group
+app_name: kpt-event
 app_environment: test
 debug: true
 http_server_addr: ':8000'
+http_metrics_addr: ':23332'
 
 store:
-  kpt_event_rw: "root@tcp(127.0.0.1:3306)/kpt_tmr_group?charset=utf8mb4&parseTime=true&loc=Local&allowNativePasswords=true&timeout=300s&readTimeout=300s&writeTimeout=300s"
-  kpt_event_migr: "root@tcp(127.0.0.1:3306)/kpt_tmr_group?charset=utf8mb4&parseTime=true&loc=Local&allowNativePasswords=true&timeout=300s&readTimeout=300s&writeTimeout=300s"
+  show_sql: true
+  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"
+
+excel_setting:
+  sheet_name: "Sheet1"
+  height: 25.0
+
+wechat_setting:
+  appid: wxd6e17d5709ce9c80
+  secret: 13b93e6c1cda8b6e46adb358a04f9f8f

+ 9 - 1
dep/dep.go

@@ -2,7 +2,11 @@ package dep
 
 import (
 	"kpt-tmr-group/config"
-	"kpt-tmr-group/util/di"
+	"kpt-tmr-group/module/backend"
+	"kpt-tmr-group/pkg/di"
+	"kpt-tmr-group/service/sso"
+	"kpt-tmr-group/service/wechat"
+	"kpt-tmr-group/store/kptstore"
 )
 
 // Global 全局所有的依赖
@@ -19,5 +23,9 @@ func Options() []di.HubOption {
 		// 基础依赖
 		config.Module,
 		// store
+		kptstore.Module,
+		backend.Module,
+		sso.Module,
+		wechat.Module,
 	}
 }

+ 1 - 1
dep/di.go

@@ -1,6 +1,6 @@
 package dep
 
-import "kpt-tmr-group/util/di"
+import "kpt-tmr-group/pkg/di"
 
 func DI(opts ...di.HubOption) *di.Hub {
 	var hubOpts []di.HubOption

+ 4 - 1
dep/di_http.go

@@ -1,6 +1,8 @@
 package dep
 
 import (
+	"kpt-tmr-group/module/backend"
+
 	"go.uber.org/dig"
 )
 
@@ -23,5 +25,6 @@ type HttpDependency struct {
 	// module
 	//Content    llscontent.Content       // Content 内容获取服务
 	//DataCenter llsdatacenter.DataCenter // DMP 数据中心
-	//Event      *event.Event
+
+	StoreEventHub backend.Hub
 }

+ 44 - 8
go.mod

@@ -3,24 +3,44 @@ module kpt-tmr-group
 go 1.17
 
 require (
+	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
+	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/golang/protobuf v1.5.3
 	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/huandu/xstrings v1.4.0
 	github.com/jinzhu/copier v0.3.5
-	github.com/lunny/log v0.0.0-20160921050905-7887c61bf0de
+	github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
+	github.com/magiconair/properties v1.8.7
 	github.com/mitchellh/mapstructure v1.5.0
+	github.com/natefinch/lumberjack v2.0.0+incompatible
+	github.com/nyaruka/phonenumbers v1.1.7
 	github.com/sirupsen/logrus v1.9.0
 	github.com/spf13/cobra v1.7.0
 	github.com/spf13/viper v1.15.0
 	github.com/stretchr/testify v1.8.2
+	github.com/xuri/excelize/v2 v2.7.1
 	go.uber.org/dig v1.15.0
+	go.uber.org/zap v1.21.0
+	google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef
+	google.golang.org/grpc v1.52.0
+	google.golang.org/protobuf v1.30.0
+	gorm.io/driver/mysql v1.5.0
+	gorm.io/gorm v1.25.0
 )
 
 require (
+	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bytedance/sonic v1.8.0 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fsnotify/fsnotify v1.6.0 // indirect
@@ -28,20 +48,32 @@ require (
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/validator/v10 v10.11.2 // indirect
+	github.com/go-sql-driver/mysql v1.7.0 // indirect
 	github.com/goccy/go-json v0.10.0 // indirect
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/jonboulle/clockwork v0.4.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/cpuid/v2 v2.0.9 // indirect
 	github.com/leodido/go-urn v1.2.1 // indirect
-	github.com/magiconair/properties v1.8.7 // indirect
+	github.com/lestrrat-go/strftime v1.0.6 // 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
+	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/prometheus/client_golang v1.14.0 // indirect
+	github.com/prometheus/client_model v0.3.0 // indirect
+	github.com/prometheus/common v0.42.0 // indirect
+	github.com/prometheus/procfs v0.9.0 // indirect
+	github.com/richardlehane/mscfb v1.0.4 // indirect
+	github.com/richardlehane/msoleps v1.0.3 // indirect
 	github.com/spf13/afero v1.9.3 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
@@ -49,12 +81,16 @@ require (
 	github.com/subosito/gotenv v1.4.2 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.9 // indirect
+	github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
+	github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect
+	go.uber.org/atomic v1.9.0 // indirect
+	go.uber.org/multierr v1.8.0 // indirect
 	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
-	golang.org/x/crypto v0.7.0 // indirect
-	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/protobuf v1.29.1 // indirect
+	golang.org/x/crypto v0.8.0 // indirect
+	golang.org/x/net v0.9.0 // indirect
+	golang.org/x/sys v0.7.0 // indirect
+	golang.org/x/text v0.9.0 // 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
 )

File diff suppressed because it is too large
+ 75 - 705
go.sum


+ 2 - 6
http/api/default.go → http/handler/default.go

@@ -1,10 +1,10 @@
-package api
+package handler
 
 import (
 	"compress/gzip"
 	"fmt"
 	"io/ioutil"
-	"kpt-tmr-group/util/xerr"
+	"kpt-tmr-group/pkg/xerr"
 	"net/http"
 
 	"github.com/gin-gonic/gin"
@@ -37,7 +37,3 @@ func dumpPerfEventBody(c *gin.Context) ([]byte, error) {
 
 	return ioutil.ReadAll(reader)
 }
-
-func Hello(c *gin.Context) {
-	c.String(http.StatusOK, "hello world!")
-}

+ 250 - 0
http/handler/feed/feed_formula.go

@@ -0,0 +1,250 @@
+package feed
+
+import (
+	"fmt"
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/ginutil"
+	"kpt-tmr-group/pkg/valid"
+	"kpt-tmr-group/pkg/xerr"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"mime/multipart"
+	"net/http"
+	"path"
+	"strconv"
+	"time"
+
+	"github.com/gin-gonic/gin"
+)
+
+func AddFeedFormula(c *gin.Context) {
+	var req operationPb.AddFeedFormulaRequest
+	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.EncodeNumber, valid.Required, valid.Length(1, 30)),
+		valid.Field(&req.Colour, valid.Required),
+		valid.Field(&req.CattleCategoryId, valid.Required, valid.Min(1)),
+		valid.Field(&req.CattleCategoryName, valid.Required),
+		valid.Field(&req.FormulaTypeId, valid.Required, valid.Min(1)),
+		valid.Field(&req.FormulaTypeName, valid.Required),
+		valid.Field(&req.DataSourceId, valid.Required, valid.Min(1)),
+		valid.Field(&req.DataSourceName, valid.Required),
+		valid.Field(&req.Remarks, valid.Required),
+		valid.Field(&req.IsShow, valid.Required, valid.Min(1), valid.Max(2)),
+		valid.Field(&req.IsModify, valid.Required, valid.Min(1), valid.Max(2)),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.CreateFeedFormula(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func EditFeedFormula(c *gin.Context) {
+	var req operationPb.AddFeedFormulaRequest
+	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.EncodeNumber, valid.Required, valid.Length(1, 30)),
+		valid.Field(&req.Colour, valid.Required),
+		valid.Field(&req.CattleCategoryId, valid.Required, valid.Min(1)),
+		valid.Field(&req.CattleCategoryName, valid.Required),
+		valid.Field(&req.FormulaTypeId, valid.Required, valid.Min(1)),
+		valid.Field(&req.FormulaTypeName, valid.Required),
+		valid.Field(&req.DataSourceId, valid.Required, valid.Min(1)),
+		valid.Field(&req.DataSourceName, valid.Required),
+		valid.Field(&req.Remarks, valid.Required),
+		valid.Field(&req.IsShow, valid.Required, valid.Min(1), valid.Max(2)),
+		valid.Field(&req.IsModify, valid.Required, valid.Min(1), valid.Max(2)),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.EditFeedFormula(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func SearchFeedFormulaList(c *gin.Context) {
+	req := &operationPb.SearchFeedFormulaRequest{}
+	if err := ginutil.BindProto(c, 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.BackendOperation(c).OpsService.SearchFeedFormulaList(c, req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}
+
+func DeleteFeedFormula(c *gin.Context) {
+	feedFormulaIdStr := c.Param("feed_formula_id")
+	feedFormulaId, _ := strconv.Atoi(feedFormulaIdStr)
+
+	if err := valid.Validate(feedFormulaId, valid.Required, valid.Min(1)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.DeleteFeedFormula(c, int64(feedFormulaId)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func IsShowModifyFeedFormula(c *gin.Context) {
+	var req operationPb.IsShowModifyFeedFormula
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.FeedFormulaId, 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.BackendOperation(c).OpsService.IsShowFeedFormula(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func ExcelImportFeedFormula(c *gin.Context) {
+	file, err := func(c *gin.Context) (multipart.File, error) {
+		if c.ContentType() != "multipart/form-data" {
+			return nil, xerr.Custom("invalid Content-Type")
+		}
+		if c.Request.Body == nil || int(c.Request.ContentLength) <= 0 {
+			return nil, xerr.Custom("invalid body")
+		}
+
+		file, fileHeader, err := c.Request.FormFile("file")
+		if err != nil {
+			return nil, xerr.WithStack(err)
+		}
+
+		fileSuffix := path.Ext(path.Base(fileHeader.Filename))
+		if fileSuffix != ".xlsx" {
+			return nil, xerr.Custom("invalid file suffix")
+		}
+
+		return file, nil
+	}(c)
+
+	if err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err = middleware.BackendOperation(c).OpsService.ExcelImportFeedFormula(c, file); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func ExcelExportFeedFormula(c *gin.Context) {
+	req := &operationPb.SearchFeedFormulaRequest{}
+	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)),
+	}
+
+	buffer, err := middleware.BackendOperation(c).OpsService.ExcelExportFeedFormula(c, req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	c.Header("Content-Type", "application/octet-stream")
+	c.Header("Content-Disposition", "attachment; filename="+(fmt.Sprintf("饲料表 %s.xlsx", time.Now().Format("200601021504"))))
+	if _, err = c.Writer.Write(buffer.Bytes()); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func ExcelTemplateFeedFormula(c *gin.Context) {
+	buffer, err := middleware.BackendOperation(c).OpsService.ExcelTemplateFeedFormula(c)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	c.Header("Content-Type", "application/octet-stream")
+	c.Header("Content-Disposition", "attachment; filename="+(fmt.Sprintf("饲料表 %s.xlsx", time.Now().Format("200601021504"))))
+	if _, err = c.Writer.Write(buffer.Bytes()); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}

+ 33 - 0
http/handler/mobile/mobile_list.go

@@ -0,0 +1,33 @@
+package mobile
+
+import (
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/ginutil"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+)
+
+func SearchMobileList(c *gin.Context) {
+	var req operationPb.SearchMobileRequest
+	if err := ginutil.BindProto(c, &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.BackendOperation(c).OpsService.SearchMobileList(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, res)
+}

+ 278 - 0
http/handler/pasture/cattle_forage_category.go

@@ -0,0 +1,278 @@
+package pasture
+
+import (
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/ginutil"
+	"kpt-tmr-group/pkg/valid"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"net/http"
+	"strconv"
+
+	"github.com/gin-gonic/gin"
+)
+
+/*// ParentCattleCategoryList 牲畜父类列表
+func ParentCattleCategoryList(c *gin.Context) {
+	res := middleware.BackendOperation(c).OpsService.ParentCattleCategoryList(c)
+	c.JSON(http.StatusOK, apiok.CommonResponse(res))
+}
+*/
+
+func AddCattleCategory(c *gin.Context) {
+	var req operationPb.AddCattleCategoryRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.ParentId, valid.Required, valid.Min(1), valid.Max(6)),
+		valid.Field(&req.ParentName, valid.Required),
+		valid.Field(&req.Number, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.AddCattleCategory(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func EditCattleCategory(c *gin.Context) {
+	var req operationPb.AddCattleCategoryRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Id, valid.Required),
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.ParentId, valid.Required, valid.Min(1), valid.Max(6)),
+		valid.Field(&req.ParentName, valid.Required),
+		valid.Field(&req.Number, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.EditCattleCategory(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func IsShowCattleCategory(c *gin.Context) {
+	var req operationPb.IsShowCattleCategory
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.CattleCategoryId, valid.Required, valid.Min(1)),
+		valid.Field(&req.IsShow, valid.Required, valid.Min(1), valid.Max(2)),
+	); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.IsShowCattleCategory(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func DeleteCattleCategory(c *gin.Context) {
+	cattleCategoryIdStr := c.Param("cattle_category_id")
+	cattleCategoryId, _ := strconv.Atoi(cattleCategoryIdStr)
+
+	if err := valid.Validate(cattleCategoryId, valid.Required, valid.Min(1)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.DeleteCattleCategory(c, int64(cattleCategoryId)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func SearchCattleCategory(c *gin.Context) {
+	req := &operationPb.SearchCattleCategoryRequest{}
+	if err := ginutil.BindProto(c, 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.BackendOperation(c).OpsService.SearchCattleCategoryList(c, req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, res)
+}
+
+/*// ParentForageCategoryList 饲料父类列表
+func ParentForageCategoryList(c *gin.Context) {
+	res := middleware.BackendOperation(c).OpsService.ParentForageCategoryList(c)
+	c.JSON(http.StatusOK, apiok.CommonResponse(res))
+}*/
+
+func AddForageCategory(c *gin.Context) {
+	var req operationPb.AddForageCategoryRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.ParentId, valid.Required, valid.Min(1), valid.Max(6)),
+		valid.Field(&req.ParentName, valid.Required),
+		valid.Field(&req.Number, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.AddForageCategory(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func EditForageCategory(c *gin.Context) {
+	var req operationPb.AddForageCategoryRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Id, valid.Required),
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.ParentId, valid.Required, valid.Min(1), valid.Max(6)),
+		valid.Field(&req.ParentName, valid.Required),
+		valid.Field(&req.Number, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.EditForageCategory(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func IsShowForageCategory(c *gin.Context) {
+	var req operationPb.IsShowForageCategory
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.ForageCategoryId, valid.Required, valid.Min(1)),
+		valid.Field(&req.IsShow, valid.Required, valid.Min(1), valid.Max(2)),
+	); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.IsShowForageCategory(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func DeleteForageCategory(c *gin.Context) {
+	forageCategoryIdStr := c.Param("forage_category_id")
+	forageCategoryId, _ := strconv.Atoi(forageCategoryIdStr)
+
+	if err := valid.Validate(forageCategoryId, valid.Required, valid.Min(1)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.DeleteForageCategory(c, int64(forageCategoryId)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func SearchForageCategory(c *gin.Context) {
+	req := &operationPb.SearchForageCategoryRequest{}
+	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.BackendOperation(c).OpsService.SearchForageCategoryList(c, req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, res)
+}

+ 249 - 0
http/handler/pasture/forage_list.go

@@ -0,0 +1,249 @@
+package pasture
+
+import (
+	"fmt"
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/ginutil"
+	"kpt-tmr-group/pkg/valid"
+	"kpt-tmr-group/pkg/xerr"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"mime/multipart"
+	"net/http"
+	"path"
+	"time"
+
+	"github.com/gin-gonic/gin"
+)
+
+func AddForage(c *gin.Context) {
+	var req operationPb.AddForageRequest
+	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.CategoryId, valid.Required),
+		valid.Field(&req.UniqueEncode, valid.Required),
+		valid.Field(&req.ForageSourceId, valid.Required),
+		valid.Field(&req.PlanTypeId, valid.Required),
+		valid.Field(&req.JumpWeight, valid.Required, valid.Min(0), valid.Max(50)),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.CreateForage(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func EditForage(c *gin.Context) {
+	var req operationPb.AddForageRequest
+	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.CategoryId, valid.Required),
+		valid.Field(&req.UniqueEncode, valid.Required),
+		valid.Field(&req.ForageSourceId, valid.Required),
+		valid.Field(&req.PlanTypeId, valid.Required),
+		valid.Field(&req.JumpWeight, valid.Required, valid.Min(0), valid.Max(50)),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.EditForage(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func SearchForageList(c *gin.Context) {
+	req := &operationPb.SearchForageListRequest{}
+	if err := ginutil.BindProto(c, 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.BackendOperation(c).OpsService.SearchForageList(c, req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}
+
+// SearchForageEnumList 饲料列表公共枚举
+func SearchForageEnumList(c *gin.Context) {
+	res := middleware.BackendOperation(c).OpsService.ForageEnumList(c)
+	ginutil.JSONResp(c, res)
+}
+
+func DeleteForageList(c *gin.Context) {
+	ids := make([]int64, 0)
+	if err := c.BindJSON(&ids); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if len(ids) <= 0 {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Custom("参数错误"))
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.DeleteForageList(c, ids); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func IsShowForage(c *gin.Context) {
+	var req operationPb.IsShowForage
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.ForageId, 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.BackendOperation(c).OpsService.IsShowForage(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func ExcelImportForage(c *gin.Context) {
+
+	file, err := func(c *gin.Context) (multipart.File, error) {
+		if c.ContentType() != "multipart/form-data" {
+			return nil, xerr.Custom("invalid Content-Type")
+		}
+		if c.Request.Body == nil || int(c.Request.ContentLength) <= 0 {
+			return nil, xerr.Custom("invalid body")
+		}
+
+		file, fileHeader, err := c.Request.FormFile("file")
+		if err != nil {
+			return nil, xerr.WithStack(err)
+		}
+
+		fileSuffix := path.Ext(path.Base(fileHeader.Filename))
+		if fileSuffix != ".xlsx" {
+			return nil, xerr.Custom("invalid file suffix")
+		}
+
+		return file, nil
+	}(c)
+
+	if err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err = middleware.BackendOperation(c).OpsService.ExcelImportForage(c, file); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func ExcelExportForage(c *gin.Context) {
+
+	req := &operationPb.SearchForageListRequest{}
+	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)),
+	}
+
+	buffer, err := middleware.BackendOperation(c).OpsService.ExcelExportForage(c, req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	c.Header("Content-Type", "application/octet-stream")
+	c.Header("Content-Disposition", "attachment; filename="+(fmt.Sprintf("饲料表 %s.xlsx", time.Now().Format("200601021504"))))
+	if _, err = c.Writer.Write(buffer.Bytes()); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func ExcelTemplateForage(c *gin.Context) {
+
+	buffer, err := middleware.BackendOperation(c).OpsService.ExcelTemplateForage(c)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	c.Header("Content-Type", "application/octet-stream")
+	c.Header("Content-Disposition", "attachment; filename="+(fmt.Sprintf("饲料表 %s.xlsx", time.Now().Format("200601021504"))))
+	if _, err = c.Writer.Write(buffer.Bytes()); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}

+ 161 - 0
http/handler/pasture/pasture_list.go

@@ -0,0 +1,161 @@
+package pasture
+
+import (
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/ginutil"
+	"kpt-tmr-group/pkg/valid"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"net/http"
+	"strconv"
+
+	"github.com/gin-gonic/gin"
+)
+
+func AddGroupPasture(c *gin.Context) {
+	var req operationPb.AddPastureRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.Name, valid.Required),
+		valid.Field(&req.ManagerUser, valid.Required),
+		valid.Field(&req.ManagerPhone, valid.Required),
+		valid.Field(&req.Account, valid.Required),
+		valid.Field(&req.Address, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.CreateGroupPasture(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func EditGroupPasture(c *gin.Context) {
+	var req operationPb.AddPastureRequest
+	if err := ginutil.BindProto(c, &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.ManagerUser, valid.Required),
+		valid.Field(&req.ManagerPhone, valid.Required),
+		valid.Field(&req.Account, valid.Required),
+		valid.Field(&req.Address, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.EditGroupPasture(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func SearchGroupPastureList(c *gin.Context) {
+	req := &operationPb.SearchPastureRequest{}
+	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.BackendOperation(c).OpsService.SearchGroupPastureList(c, req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}
+
+func DeleteGroupPasture(c *gin.Context) {
+	pastureIdStr := c.Param("pasture_id")
+	pastureId, _ := strconv.Atoi(pastureIdStr)
+
+	if err := valid.Validate(pastureId, valid.Required, valid.Min(1)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.DeleteGroupPasture(c, int64(pastureId)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func ResetPasswordGroupPasture(c *gin.Context) {
+	pastureIdStr := c.Param("pasture_id")
+	pastureId, _ := strconv.Atoi(pastureIdStr)
+
+	if err := valid.Validate(pastureId, valid.Required, valid.Min(1)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.ResetPasswordGroupPasture(c, int64(pastureId)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+func IsShowGroupPasture(c *gin.Context) {
+	var req operationPb.IsShowGroupPasture
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.PastureId, valid.Required, valid.Min(1)),
+		valid.Field(&req.IsShow, valid.Required, valid.Min(1), valid.Max(2)),
+	); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	if err := middleware.BackendOperation(c).OpsService.IsShowGroupPasture(c, &req); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}

+ 33 - 0
http/handler/statistic/analysis.go

@@ -0,0 +1,33 @@
+package statistic
+
+import (
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/ginutil"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+)
+
+// SearchFormulaEstimateList 配方评估
+func SearchFormulaEstimateList(c *gin.Context) {
+	req := &operationPb.SearchFormulaEstimateRequest{}
+	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.BackendOperation(c).OpsService.SearchFormulaEstimateList(c, req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}

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

@@ -0,0 +1,158 @@
+package system
+
+import (
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/ginutil"
+	"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 := ginutil.BindProto(c, &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
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+// EditSystemMenu 编辑系统菜单权限
+func EditSystemMenu(c *gin.Context) {
+	var req operationPb.AddMenuRequest
+	if err := ginutil.BindProto(c, &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.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
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+// IsShowSystemMenu 是否启动
+func IsShowSystemMenu(c *gin.Context) {
+	var req operationPb.IsShowSystemMenuRequest
+	if err := ginutil.BindProto(c, &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
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+// SearchSystemMenuList 菜单列表查询
+func SearchSystemMenuList(c *gin.Context) {
+	var req operationPb.SearchMenuRequest
+	if err := ginutil.BindProto(c, &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
+	}
+	ginutil.JSONResp(c, 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.DeleteSystemMenu(c, int64(menuId)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}

+ 134 - 0
http/handler/system/role.go

@@ -0,0 +1,134 @@
+package system
+
+import (
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/ginutil"
+	"kpt-tmr-group/pkg/valid"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"net/http"
+	"strconv"
+
+	"github.com/gin-gonic/gin"
+)
+
+// AddSystemRole 添加角色
+func AddSystemRole(c *gin.Context) {
+	var req operationPb.AddRoleRequest
+	if err := ginutil.BindProto(c, &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
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+// EditSystemRole 编辑角色
+func EditSystemRole(c *gin.Context) {
+	var req operationPb.AddRoleRequest
+	if err := ginutil.BindProto(c, &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
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+// GetRolePermissions 获取角色对应的权限
+func GetRolePermissions(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
+	}
+
+	res, err := middleware.BackendOperation(c).OpsService.GetRolePermissions(c, int64(roleId))
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, res)
+}
+
+// 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
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+// SearchSystemRoleList 角色列表
+func SearchSystemRoleList(c *gin.Context) {
+	var req operationPb.SearchRoleRequest
+	if err := ginutil.BindProto(c, &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
+	}
+	ginutil.JSONResp(c, res)
+}

+ 264 - 0
http/handler/system/user.go

@@ -0,0 +1,264 @@
+package system
+
+import (
+	"kpt-tmr-group/http/middleware"
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/ginutil"
+	"kpt-tmr-group/pkg/valid"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"net/http"
+	"strconv"
+
+	"github.com/gin-gonic/gin"
+)
+
+// Auth 用户登录
+func Auth(c *gin.Context) {
+	var req operationPb.UserAuthData
+	if err := ginutil.BindProto(c, &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
+	}
+	ginutil.JSONResp(c, res)
+}
+
+func GetWxAppletOpenId(c *gin.Context) {
+	jsCode := c.Param("js_code")
+	if err := valid.Validate(jsCode, valid.Required, valid.Length(1, 100)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	res, err := middleware.BackendOperation(c).OpsService.GetOpenId(c, jsCode)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, res)
+}
+
+// AddSystemUser 创建系统用户
+func AddSystemUser(c *gin.Context) {
+	var req operationPb.AddSystemUser
+	if err := ginutil.BindProto(c, &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
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+// DetailsSystemUser 系统用户详情
+func DetailsSystemUser(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
+	}
+
+	res, err := middleware.BackendOperation(c).OpsService.DetailsSystemUser(c, int64(userId))
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, res)
+}
+
+// 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
+	}
+	ginutil.JSONResp(c, res)
+}
+
+// SearchSystemUserList 查询系统用户列表
+func SearchSystemUserList(c *gin.Context) {
+	var req operationPb.SearchUserRequest
+	if err := ginutil.BindProto(c, &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
+	}
+	ginutil.JSONResp(c, res)
+}
+
+// EditSystemUser 编辑系统用户
+func EditSystemUser(c *gin.Context) {
+	var req operationPb.AddSystemUser
+	if err := ginutil.BindProto(c, &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
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: 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
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}
+
+// IsShowSystemUser 系统用户启动开关
+func IsShowSystemUser(c *gin.Context) {
+	var req operationPb.IsShowSystemUserRequest
+	if err := ginutil.BindProto(c, &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
+	}
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: 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
+	}
+	ginutil.JSONResp(c, res)
+}
+
+// ResetPasswordSystemUser 用户密码重置
+func ResetPasswordSystemUser(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.ResetPasswordSystemUser(c, int64(userId)); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}

+ 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 - 0
http/middleware/hub.go

@@ -0,0 +1,11 @@
+package middleware
+
+import (
+	"kpt-tmr-group/module/backend"
+
+	"github.com/gin-gonic/gin"
+)
+
+func BackendOperation(c *gin.Context) *backend.Hub {
+	return &(Dependency(c).StoreEventHub)
+}

+ 10 - 4
http/middleware/pagination.go

@@ -7,12 +7,18 @@ import (
 	"github.com/gin-gonic/gin"
 )
 
+const (
+	Page       = "page"
+	PageSize   = "page_size"
+	PageOffset = "page_offset"
+)
+
 // 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()
 	}
 }
@@ -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
 }

+ 2 - 2
http/middleware/sentry.go

@@ -1,8 +1,8 @@
 package middleware
 
 import (
-	sentry2 "kpt-tmr-group/util/sentry"
-	"kpt-tmr-group/util/xerr"
+	sentry2 "kpt-tmr-group/pkg/sentry"
+	"kpt-tmr-group/pkg/xerr"
 	"sync"
 
 	"github.com/getsentry/sentry-go"

+ 57 - 0
http/middleware/sso.go

@@ -0,0 +1,57 @@
+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"
+	XRequestId    = "X-Request-Id"
+)
+
+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 GetXRequestId(c *gin.Context) string {
+	item := c.Request.Header.Get(XRequestId)
+	return item
+}
+
+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)
+			c.Abort()
+		}
+
+		claims, err := jwt.ParseToken(token)
+		if err != nil || claims == nil || claims.Username == "" {
+			unauthorized(c)
+			c.Abort()
+		}
+
+		c.Set(UserName, claims.Username)
+		c.Set(XRequestId, GetXRequestId(c))
+		c.Next()
+	}
+}

+ 2 - 2
http/route/api_debug_route.go

@@ -1,8 +1,8 @@
 package route
 
 import (
-	"kpt-tmr-group/http/api"
 	"kpt-tmr-group/http/debug"
+	"kpt-tmr-group/http/handler"
 
 	"github.com/gin-gonic/gin"
 )
@@ -14,7 +14,7 @@ func DebugAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		}
 
 		// Not Found
-		s.NoRoute(api.Handle404)
+		s.NoRoute(handler.Handle404)
 		debugRoute := authRouteGroup(s, "/api/v1/kpt/debug/")
 		// kpt debug api
 		debugRoute.GET("hello", debug.HelloOk)

+ 92 - 9
http/route/app_api.go

@@ -1,8 +1,13 @@
 package route
 
 import (
-	// m "git.llsapp.com/zhenghe/pkg/http/middleware"
-	"kpt-tmr-group/http/api"
+	"kpt-tmr-group/http/handler"
+	"kpt-tmr-group/http/handler/feed"
+	"kpt-tmr-group/http/handler/mobile"
+	"kpt-tmr-group/http/handler/pasture"
+	"kpt-tmr-group/http/handler/statistic"
+	"kpt-tmr-group/http/handler/system"
+	"kpt-tmr-group/http/middleware"
 
 	"github.com/gin-gonic/gin"
 )
@@ -13,19 +18,97 @@ func AppAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 			opt(s)
 		}
 		// Not Found
-		s.NoRoute(api.Handle404)
+		s.NoRoute(handler.Handle404)
 		// Health Check
-		s.GET("/check", api.Health)
+		s.GET("/check", handler.Health)
 
-		// lingo API 组
-		lingoRoute := authRouteGroup(s, "/api/v1/kpt-tmr-group/")
-		lingoRoute.GET("/hello", api.Hello)
+		s.POST("/auth", system.Auth)
+		s.GET("/wx_applet/openid/:js_code", system.GetWxAppletOpenId)
+
+		// system API 组
+		// 系统用户
+		systemRoute := authRouteGroup(s, "/api/v1/system/")
+		systemRoute.POST("/user_info", system.GetUserInfo)
+		systemRoute.POST("/user/add", system.AddSystemUser)
+		systemRoute.GET("/user/details/:user_id", system.DetailsSystemUser)
+		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("/user/rest_password/:user_id", system.ResetPasswordSystemUser)
+
+		// 系统角色
+		systemRoute.POST("/role/add", system.AddSystemRole)
+		systemRoute.GET("/role/permissions/:role_id", system.GetRolePermissions)
+		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)
+
+		// 移动端
+		systemRoute.POST("/mobile/list", mobile.SearchMobileList)
+
+		// 牧场管理
+		opsRoute := authRouteGroup(s, "/api/v1/ops/")
+		opsRoute.POST("/pasture/add", pasture.AddGroupPasture)
+		opsRoute.POST("/pasture/edit", pasture.EditGroupPasture)
+		opsRoute.POST("/pasture/list", pasture.SearchGroupPastureList)
+		opsRoute.DELETE("/pasture/:pasture_id", pasture.DeleteGroupPasture)
+		opsRoute.POST("/pasture/rest_password/:pasture_id", pasture.ResetPasswordGroupPasture)
+		opsRoute.POST("/pasture/is_show", pasture.IsShowGroupPasture)
+
+		// 牲牧类型
+		// opsRoute.GET("/cattle/category/parent_list", pasture.ParentCattleCategoryList)
+		opsRoute.POST("/cattle/category/add", pasture.AddCattleCategory)
+		opsRoute.POST("/cattle/category/edit", pasture.EditCattleCategory)
+		opsRoute.POST("/cattle/category/is_show", pasture.IsShowCattleCategory)
+		opsRoute.DELETE("/cattle/category/:cattle_category_id", pasture.DeleteCattleCategory)
+		opsRoute.POST("/cattle/category/list", pasture.SearchCattleCategory)
+
+		// 饲料类别
+		// opsRoute.GET("/forage/category/parent_list", pasture.ParentForageCategoryList)
+		opsRoute.POST("/forage/category/add", pasture.AddForageCategory)
+		opsRoute.POST("/forage/category/edit", pasture.EditForageCategory)
+		opsRoute.POST("/forage/category/is_show", pasture.IsShowForageCategory)
+		opsRoute.DELETE("/forage/category/:forage_category_id", pasture.DeleteForageCategory)
+		opsRoute.POST("/forage/category/list", pasture.SearchForageCategory)
+
+		// 饲料列表
+		opsRoute.POST("/forage/add", pasture.AddForage)
+		opsRoute.POST("/forage/edit", pasture.EditForage)
+		opsRoute.POST("/forage/list", pasture.SearchForageList)
+		opsRoute.POST("forage/delete", pasture.DeleteForageList)
+		opsRoute.POST("forage/is_show", pasture.IsShowForage)
+		opsRoute.POST("forage/excel_import", pasture.ExcelImportForage)
+		opsRoute.POST("forage/excel_export", pasture.ExcelExportForage)
+		opsRoute.POST("forage/excel_template", pasture.ExcelTemplateForage)
+		opsRoute.GET("/forage/enum/list", pasture.SearchForageEnumList)
+
+		// 饲料配方
+		opsRoute.POST("/feed_formula/add", feed.AddFeedFormula)
+		opsRoute.POST("/feed_formula/edit", feed.EditFeedFormula)
+		opsRoute.POST("/feed_formula/list", feed.SearchFeedFormulaList)
+		opsRoute.POST("/feed_formula/delete/:feed_formula_id", feed.DeleteFeedFormula)
+		opsRoute.POST("/feed_formula/is_modify_show", feed.IsShowModifyFeedFormula)
+		opsRoute.POST("/feed_formula/excel_export", feed.ExcelExportFeedFormula)
+		opsRoute.POST("/feed_formula/excel_import", feed.ExcelImportFeedFormula)
+		opsRoute.POST("/feed_formula/excel_template", feed.ExcelTemplateFeedFormula)
+
+		//统计分析 statistic analysis
+		opsRoute.POST("/feed_formula/add", statistic.SearchFormulaEstimateList)
 	}
 }
 
 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.CORS())
 	return group
 }

+ 3 - 3
main.go

@@ -6,11 +6,11 @@ package main
 
 import (
 	"kpt-tmr-group/cmd"
-	log "kpt-tmr-group/util/logger"
+	"kpt-tmr-group/pkg/logger/logrus"
 )
 
 func main() {
-	log.Info("kpe-event: is starting")
+	logrus.Info("kpe-event: is starting")
 	cmd.Execute()
-	log.Error("kpt-tmr-group: is shut down")
+	logrus.Error("kpt-tmr-group: is shut down")
 }

+ 67 - 0
model/cattle_category.go

@@ -0,0 +1,67 @@
+package model
+
+import (
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
+)
+
+type CattleCategory struct {
+	Id          int64                                 `json:"id"`
+	ParentId    operationPb.CattleCategoryParent_Kind `json:"parent_id"`
+	ParentName  string                                `json:"parent_name"`
+	PastureId   int64                                 `json:"pasture_id"`
+	PastureName string                                `json:"pasture_name"`
+	Name        string                                `json:"name"`
+	Number      string                                `json:"number"`
+	IsShow      operationPb.IsShow_Kind               `json:"is_show"`
+	IsDelete    operationPb.IsShow_Kind               `json:"is_delete"`
+	DataSource  operationPb.DataSource_Kind           `json:"data_source"`
+	CreatedAt   int64                                 `json:"created_at"`
+	UpdatedAt   int64                                 `json:"updated_at"`
+}
+
+func (c *CattleCategory) TableName() string {
+	return "cattle_category"
+}
+
+func NewCattleCategory(req *operationPb.AddCattleCategoryRequest) *CattleCategory {
+	return &CattleCategory{
+		ParentId:   req.ParentId,
+		ParentName: req.ParentName,
+		Name:       req.Name,
+		Number:     req.Number,
+		IsShow:     operationPb.IsShow_OK,
+		IsDelete:   operationPb.IsShow_OK,
+	}
+}
+
+type CattleCategorySlice []*CattleCategory
+
+func (c CattleCategorySlice) ToPB() []*operationPb.AddCattleCategoryRequest {
+	res := make([]*operationPb.AddCattleCategoryRequest, len(c))
+	for i, v := range c {
+		res[i] = &operationPb.AddCattleCategoryRequest{
+			Id:              uint32(v.Id),
+			Name:            v.Name,
+			ParentId:        v.ParentId,
+			ParentName:      v.ParentName,
+			Number:          v.Number,
+			IsShow:          v.IsShow,
+			CreatedAt:       uint32(v.CreatedAt),
+			CreatedAtFormat: time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+		}
+	}
+	return res
+}
+
+func (c *CattleCategory) ToPb() *operationPb.AddCattleCategoryRequest {
+	return &operationPb.AddCattleCategoryRequest{
+		Id:         uint32(c.Id),
+		Name:       c.Name,
+		Number:     c.Number,
+		ParentId:   c.ParentId,
+		ParentName: c.ParentName,
+		IsShow:     c.IsShow,
+		CreatedAt:  uint32(c.CreatedAt),
+	}
+}

+ 0 - 17
model/event_fileds.go

@@ -1,17 +0,0 @@
-package model
-
-import (
-	"time"
-)
-
-type EventFileds struct {
-	Id         int       `xorm:"not null pk default 0 comment('主键自增ID') INT(11)"`
-	EventId    int       `xorm:"not null default 0 comment('事件表id') index INT(11)"`
-	FiledId    int       `xorm:"not null default 0 comment('字段表id') INT(11)"`
-	IsRequired int       `xorm:"not null default 0 comment('改字段是否必填 1 是 0 否') TINYINT(1)"`
-	DataSource int       `xorm:"not null default 0 comment('数据来源 1 自动生成 0 手动输入') TINYINT(1)"`
-	IsList     int       `xorm:"not null default 0 comment('列表是否可见 1 是 0 否') TINYINT(1)"`
-	ShowLine   int       `xorm:"not null comment('显示分布 1 显示1行 2 显示2行 3 显示3行...') TINYINT(1)"`
-	CreateTime time.Time `xorm:"not null default 'CURRENT_TIMESTAMP' comment('创建时间') DATETIME"`
-	UpdateTime time.Time `xorm:"not null default 'CURRENT_TIMESTAMP' comment('更新时间') DATETIME"`
-}

+ 79 - 0
model/feed_formula.go

@@ -0,0 +1,79 @@
+package model
+
+import (
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
+)
+
+type FeedFormula struct {
+	Id                 int64                                 `json:"id"`
+	Name               string                                `json:"name"`
+	Colour             string                                `json:"colour"`
+	EncodeNumber       string                                `json:"encode_number"`
+	CattleCategoryId   operationPb.CattleCategoryParent_Kind `json:"cattle_category_id"`
+	CattleCategoryName string                                `json:"cattle_category_name"`
+	FormulaTypeId      int32                                 `json:"formula_type_id"`
+	FormulaTypeName    string                                `json:"formula_type_name"`
+	DataSourceId       operationPb.DataSource_Kind           `json:"data_source_id"`
+	DataSourceName     string                                `json:"data_source_name"`
+	Remarks            string                                `json:"remarks"`
+	Version            int64                                 `json:"version"`
+	PastureId          int64                                 `json:"pasture_id"`
+	PastureName        string                                `json:"pasture_name"`
+	IsShow             operationPb.IsShow_Kind               `json:"is_show"`
+	IsModify           operationPb.IsShow_Kind               `json:"is_modify"`
+	IsDelete           operationPb.IsShow_Kind               `json:"is_delete"`
+	CreatedAt          int64                                 `json:"created_at"`
+	UpdatedAt          int64                                 `json:"updated_at"`
+}
+
+func (f *FeedFormula) TableName() string {
+	return "feed_formula"
+}
+
+func NewFeedFormula(req *operationPb.AddFeedFormulaRequest) *FeedFormula {
+	return &FeedFormula{
+		Name:               req.Name,
+		Colour:             req.Colour,
+		EncodeNumber:       req.EncodeNumber,
+		CattleCategoryId:   req.CattleCategoryId,
+		CattleCategoryName: req.CattleCategoryName,
+		FormulaTypeId:      req.FormulaTypeId,
+		FormulaTypeName:    req.FormulaTypeName,
+		DataSourceId:       req.DataSourceId,
+		DataSourceName:     req.DataSourceName,
+		Remarks:            req.Remarks,
+		Version:            0,
+		PastureId:          0,
+		PastureName:        "",
+		IsShow:             req.IsShow,
+		IsDelete:           operationPb.IsShow_OK,
+		IsModify:           operationPb.IsShow_OK,
+	}
+}
+
+type FeedFormulaSlice []*FeedFormula
+
+func (f FeedFormulaSlice) ToPB() []*operationPb.AddFeedFormulaRequest {
+	res := make([]*operationPb.AddFeedFormulaRequest, len(f))
+	for i, v := range f {
+		res[i] = &operationPb.AddFeedFormulaRequest{
+			Id:                 int32(v.Id),
+			Name:               v.Name,
+			CattleCategoryId:   v.CattleCategoryId,
+			CattleCategoryName: v.CattleCategoryName,
+			FormulaTypeId:      v.FormulaTypeId,
+			FormulaTypeName:    v.FormulaTypeName,
+			DataSourceName:     v.DataSourceName,
+			DataSourceId:       v.DataSourceId,
+			Remarks:            v.Remarks,
+			Version:            int32(v.Version),
+			PastureName:        v.PastureName,
+			IsShow:             v.IsShow,
+			IsModify:           v.IsModify,
+			CreatedAt:          int32(v.CreatedAt),
+			CreatedAtFormat:    time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+		}
+	}
+	return res
+}

+ 91 - 0
model/forage.go

@@ -0,0 +1,91 @@
+package model
+
+import (
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
+)
+
+type Forage struct {
+	Id                 int64                           `json:"int_64"`
+	Name               string                          `json:"name"`
+	CategoryId         int64                           `json:"category_id"`
+	MaterialType       int64                           `json:"material_type"`
+	UniqueEncode       string                          `json:"unique_encode"`
+	ForageSourceId     operationPb.ForageSource_Kind   `json:"forage_source_id"`
+	PlanTypeId         operationPb.ForagePlanType_Kind `json:"plan_type_id"`
+	SmallMaterialScale string                          `json:"small_material_scale"`
+	AllowError         int64                           `json:"allow_error"`
+	PackageWeight      int64                           `json:"package_weight"`
+	Price              int64                           `json:"price"`
+	JumpWeight         int64                           `json:"jump_weight"`
+	JumpDelay          operationPb.JumpDelaType_Kind   `json:"jump_delay"`
+	ConfirmStart       operationPb.IsShow_Kind         `json:"confirm_start"`
+	RelayLocations     int64                           `json:"relay_locations"`
+	Jmp                operationPb.IsShow_Kind         `json:"jmp"`
+	DataSource         operationPb.DataSource_Kind     `json:"data_source"`
+	Backup1            string                          `json:"backup1"`
+	Backup2            string                          `json:"backup2"`
+	Backup3            string                          `json:"backup3"`
+	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 (c *Forage) TableName() string {
+	return "forage"
+}
+
+func NewForage(req *operationPb.AddForageRequest) *Forage {
+	return &Forage{
+		Name:               req.Name,
+		CategoryId:         int64(req.CategoryId),
+		UniqueEncode:       req.UniqueEncode,
+		ForageSourceId:     req.ForageSourceId,
+		PlanTypeId:         req.PlanTypeId,
+		SmallMaterialScale: req.SmallMaterialScale,
+		AllowError:         int64(req.AllowError),
+		PackageWeight:      int64(req.PackageWeight),
+		Price:              int64(req.Price),
+		JumpWeight:         int64(req.JumpWeight),
+		JumpDelay:          req.JumpDelay,
+		ConfirmStart:       req.ConfirmStart,
+		RelayLocations:     int64(req.RelayLocations),
+		IsShow:             operationPb.IsShow_OK,
+		IsDelete:           operationPb.IsShow_OK,
+		DataSource:         operationPb.DataSource_BACKGROUND_ADD,
+		Jmp:                req.Jmp,
+		Backup1:            req.Backup1,
+		Backup2:            req.Backup2,
+		Backup3:            req.Backup3,
+	}
+}
+
+type ForageSlice []*Forage
+
+func (f ForageSlice) ToPB() []*operationPb.AddForageRequest {
+	res := make([]*operationPb.AddForageRequest, len(f))
+	for i, v := range f {
+		res[i] = &operationPb.AddForageRequest{
+			Id:                 uint32(v.Id),
+			Name:               v.Name,
+			CategoryId:         uint32(v.CategoryId),
+			MaterialType:       uint32(v.MaterialType),
+			UniqueEncode:       v.UniqueEncode,
+			ForageSourceId:     v.ForageSourceId,
+			PlanTypeId:         v.PlanTypeId,
+			SmallMaterialScale: v.SmallMaterialScale,
+			AllowError:         uint32(v.AllowError),
+			PackageWeight:      uint32(v.PackageWeight),
+			Price:              uint32(v.Price),
+			JumpWeight:         uint32(v.JumpWeight),
+			JumpDelay:          v.JumpDelay,
+			ConfirmStart:       v.ConfirmStart,
+			RelayLocations:     uint32(v.RelayLocations),
+			IsShow:             v.IsShow,
+			CreatedAt:          uint32(v.CreatedAt),
+			CreatedAtFormat:    time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+		}
+	}
+	return res
+}

+ 67 - 0
model/forage_category.go

@@ -0,0 +1,67 @@
+package model
+
+import (
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
+)
+
+type ForageCategory struct {
+	Id          int64                                 `json:"id"`
+	ParentId    operationPb.ForageCategoryParent_Kind `json:"parent_id"`
+	ParentName  string                                `json:"parent_name"`
+	PastureId   int64                                 `json:"pasture_id"`
+	PastureName string                                `json:"pasture_name"`
+	Name        string                                `json:"name"`
+	Number      string                                `json:"number"`
+	IsShow      operationPb.IsShow_Kind               `json:"is_show"`
+	IsDelete    operationPb.IsShow_Kind               `json:"is_delete"`
+	DataSource  operationPb.DataSource_Kind           `json:"data_source"`
+	CreatedAt   int64                                 `json:"created_at"`
+	UpdatedAt   int64                                 `json:"updated_at"`
+}
+
+func (s *ForageCategory) TableName() string {
+	return "forage_category"
+}
+
+func NewForageCategory(req *operationPb.AddForageCategoryRequest) *ForageCategory {
+	return &ForageCategory{
+		ParentId:   req.ParentId,
+		ParentName: req.ParentName,
+		Name:       req.Name,
+		Number:     req.Number,
+		IsShow:     operationPb.IsShow_OK,
+		IsDelete:   operationPb.IsShow_OK,
+	}
+}
+
+type ForageCategorySlice []*ForageCategory
+
+func (f ForageCategorySlice) ToPB() []*operationPb.AddForageCategoryRequest {
+	res := make([]*operationPb.AddForageCategoryRequest, len(f))
+	for i, v := range f {
+		res[i] = &operationPb.AddForageCategoryRequest{
+			Id:              uint32(v.Id),
+			Name:            v.Name,
+			ParentId:        v.ParentId,
+			ParentName:      v.ParentName,
+			Number:          v.Number,
+			IsShow:          v.IsShow,
+			CreatedAt:       uint32(v.CreatedAt),
+			CreatedAtFormat: time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+		}
+	}
+	return res
+}
+
+func (c *ForageCategory) ToPb() *operationPb.AddForageCategoryRequest {
+	return &operationPb.AddForageCategoryRequest{
+		Id:         uint32(c.Id),
+		Name:       c.Name,
+		Number:     c.Number,
+		ParentId:   c.ParentId,
+		ParentName: c.ParentName,
+		IsShow:     c.IsShow,
+		CreatedAt:  uint32(c.CreatedAt),
+	}
+}

+ 132 - 0
model/formula_estimate.go

@@ -0,0 +1,132 @@
+package model
+
+import (
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
+)
+
+type FormulaEstimate struct {
+	Id                   int32  `json:"id"`
+	PastureId            int32  `json:"pasture_id"`
+	PastureName          string `json:"pasture_name"`
+	BarnId               int32  `json:"barn_id"`
+	FeedFormulaId        int32  `json:"feed_formula_id"`
+	FeedFormulaName      string `json:"feed_formula_name"`
+	CowNumber            int32  `json:"cow_number"`
+	DryFormulaNumber     int32  `json:"dry_formula_number"`
+	DryTmrFeed           int32  `json:"dry_tmr_feed"`
+	DryFoodIntake        int32  `json:"dry_food_intake"`
+	MjFormulaNumber      int32  `json:"mj_formula_number"`
+	MjTmrFeed            int32  `json:"mj_tmr_feed"`
+	MjFoodIntake         int32  `json:"mj_food_intake"`
+	NndFormulaNumber     int32  `json:"nnd_formula_number"`
+	NndTmrFeed           int32  `json:"nnd_tmr_feed"`
+	NndFoodIntake        int32  `json:"nnd_food_intake"`
+	CpgFormulaNumber     int32  `json:"cpg_formula_number"`
+	CpgTmrFeed           int32  `json:"cpg_tmr_feed"`
+	CpgFoodIntake        int32  `json:"cpg_food_intake"`
+	PgFormulaNumber      int32  `json:"pg_formula_number"`
+	PgTmrFeed            int32  `json:"pg_tmr_feed"`
+	PgFoodIntake         int32  `json:"pg_food_intake"`
+	DmFormulaNumber      int32  `json:"dm_formula_number"`
+	DmTmrFeed            int32  `json:"dm_tmr_feed"`
+	DmFoodIntake         int32  `json:"dm_food_intake"`
+	CpdmFormulaNumber    int32  `json:"cpdm_formula_number"`
+	CpdmTmrFeed          int32  `json:"cpdm_tmr_feed"`
+	CpdmFoodIntake       int32  `json:"cpdm_food_intake"`
+	FatFormulaNumber     int32  `json:"fat_formula_number"`
+	FatTmrFeed           int32  `json:"fat_tmr_feed"`
+	FatFoodIntake        int32  `json:"fat_food_intake"`
+	StarchFormulaNumber  int32  `json:"starch_formula_number"`
+	StarchTmrFeed        int32  `json:"starch_tmr_feed"`
+	StarchFoodIntake     int32  `json:"starch_food_intake"`
+	NdfFormulaNumber     int32  `json:"ndf_formula_number"`
+	NdfTmrFeed           int32  `json:"ndf_tmr_feed"`
+	NdfFoodIntake        int32  `json:"ndf_food_intake"`
+	CpNdfFormulaNumber   int32  `json:"cp_ndf_formula_number"`
+	CpNdfTmrFeed         int32  `json:"cp_ndf_tmr_feed"`
+	CpNdfFoodIntake      int32  `json:"cp_ndf_food_intake"`
+	AdfFormulaNumber     int32  `json:"adf_formula_number"`
+	AdfTmrFeed           int32  `json:"adf_tmr_feed"`
+	AdfFoodIntake        int32  `json:"adf_food_intake"`
+	CalciumFormulaNumber int32  `json:"calcium_formula_number"`
+	CalciumTmrFeed       int32  `json:"calcium_tmr_feed"`
+	CalciumFoodIntake    int32  `json:"calcium_food_intake"`
+	PdmFormulaNumber     int32  `json:"pdm_formula_number"`
+	PdmTmrFeed           int32  `json:"pdm_tmr_feed"`
+	PdmFoodIntake        int32  `json:"pdm_food_intake"`
+	CfRatioFormulaNumber int32  `json:"cf_ratio_formula_number"`
+	CfRatioTmrFeed       int32  `json:"cf_ratio_tmr_feed"`
+	CfRatioFoodIntake    int32  `json:"cf_ratio_food_intake"`
+	CreatedAt            int32  `json:"created_at"`
+	UpdatedAt            int32  `json:"updated_at"`
+}
+
+func (f *FormulaEstimate) TableName() string {
+	return "formula_estimate"
+}
+
+type FormulaEstimateSlice []*FormulaEstimate
+
+func (f FormulaEstimateSlice) ToPB() []*operationPb.AddFormulaEstimateRequest {
+	res := make([]*operationPb.AddFormulaEstimateRequest, len(f))
+	for i, v := range f {
+		res[i] = &operationPb.AddFormulaEstimateRequest{
+			Id:                   v.Id,
+			PastureId:            v.PastureId,
+			PastureName:          v.PastureName,
+			BarnId:               v.BarnId,
+			FeedFormulaId:        v.FeedFormulaId,
+			FeedFormulaName:      v.FeedFormulaName,
+			CowNumber:            v.CowNumber,
+			DryFoodIntake:        v.DryFoodIntake,
+			DryFormulaNumber:     v.DryFormulaNumber,
+			DryTmrFeed:           v.DryTmrFeed,
+			MjFoodIntake:         v.MjFoodIntake,
+			MjFormulaNumber:      v.MjFormulaNumber,
+			MjTmrFeed:            v.MjTmrFeed,
+			NndFoodIntake:        v.NndFoodIntake,
+			NndFormulaNumber:     v.NndFormulaNumber,
+			NndTmrFeed:           v.NndTmrFeed,
+			CpgFoodIntake:        v.CpgFoodIntake,
+			CpgFormulaNumber:     v.CpgFormulaNumber,
+			CpgTmrFeed:           v.CpgTmrFeed,
+			PgFoodIntake:         v.PgFoodIntake,
+			PgFormulaNumber:      v.PgFormulaNumber,
+			PgTmrFeed:            v.PgTmrFeed,
+			DmFoodIntake:         v.DmFoodIntake,
+			DmFormulaNumber:      v.DmFormulaNumber,
+			DmTmrFeed:            v.DmTmrFeed,
+			CpdmFoodIntake:       v.CpdmFoodIntake,
+			CpdmFormulaNumber:    v.CpdmFormulaNumber,
+			CpdmTmrFeed:          v.CpdmTmrFeed,
+			FatFoodIntake:        v.FatFoodIntake,
+			FatFormulaNumber:     v.FatFormulaNumber,
+			FatTmrFeed:           v.FatTmrFeed,
+			StarchFoodIntake:     v.StarchFoodIntake,
+			StarchFormulaNumber:  v.StarchFormulaNumber,
+			StarchTmrFeed:        v.StarchTmrFeed,
+			NdfFoodIntake:        v.NdfFoodIntake,
+			NdfFormulaNumber:     v.NdfFormulaNumber,
+			NdfTmrFeed:           v.NdfTmrFeed,
+			CpNdfFoodIntake:      v.CpNdfFoodIntake,
+			CpNdfFormulaNumber:   v.CpNdfFormulaNumber,
+			CpNdfTmrFeed:         v.CpNdfTmrFeed,
+			AdfFoodIntake:        v.AdfFoodIntake,
+			AdfFormulaNumber:     v.AdfFormulaNumber,
+			AdfTmrFeed:           v.AdfTmrFeed,
+			CalciumFoodIntake:    v.CalciumFoodIntake,
+			CalciumFormulaNumber: v.CalciumFormulaNumber,
+			CalciumTmrFeed:       v.CalciumTmrFeed,
+			PdmFoodIntake:        v.PdmFoodIntake,
+			PdmFormulaNumber:     v.PdmFormulaNumber,
+			PdmTmrFeed:           v.PdmTmrFeed,
+			CfRatioFoodIntake:    v.CfRatioFoodIntake,
+			CfRatioFormulaNumber: v.CfRatioFormulaNumber,
+			CfRatioTmrFeed:       v.CfRatioTmrFeed,
+			CreatedAt:            v.CreatedAt,
+			CreatedAtFormat:      time.Unix(int64(v.CreatedAt), 0).Format(LayoutTime),
+		}
+	}
+	return res
+}

+ 73 - 0
model/group_pasture.go

@@ -0,0 +1,73 @@
+package model
+
+import (
+	"kpt-tmr-group/pkg/tool"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
+)
+
+type GroupPasture struct {
+	Id           int64                   `json:"id,omitempty"`
+	Name         string                  `json:"name,omitempty"`
+	Account      string                  `json:"account,omitempty"`
+	Password     string                  `json:"password"`
+	ManagerUser  string                  `json:"manager_user"`
+	ManagerPhone string                  `json:"manager_phone"`
+	IsShow       operationPb.IsShow_Kind `json:"is_show,omitempty"`
+	IsDelete     operationPb.IsShow_Kind `json:"is_delete,omitempty"`
+	Address      string                  `json:"address"`
+	CreatedAt    int64                   `json:"created_at,omitempty"`
+	UpdatedAt    int64                   `json:"updated_at,omitempty"`
+}
+
+func (s *GroupPasture) TableName() string {
+	return "group_pasture"
+}
+
+const InitManagerPassword = "123456"
+
+func NewGroupPasture(req *operationPb.AddPastureRequest) *GroupPasture {
+	groupPasture := &GroupPasture{
+		Name:         req.Name,
+		Account:      req.Account,
+		Password:     tool.Md5String(InitManagerPassword),
+		ManagerUser:  req.ManagerUser,
+		ManagerPhone: req.ManagerPhone,
+		IsShow:       operationPb.IsShow_OK,
+		IsDelete:     operationPb.IsShow_OK,
+		Address:      req.Address,
+	}
+	return groupPasture
+}
+
+type GroupPastureSlice []*GroupPasture
+
+func (g GroupPastureSlice) ToPB() []*operationPb.AddPastureRequest {
+	res := make([]*operationPb.AddPastureRequest, len(g))
+	for i, v := range g {
+		res[i] = &operationPb.AddPastureRequest{
+			Id:              int32(v.Id),
+			Name:            v.Name,
+			Account:         v.Account,
+			ManagerUser:     v.ManagerUser,
+			ManagerPhone:    v.ManagerPhone,
+			Address:         v.Address,
+			IsShow:          v.IsShow,
+			CreatedAt:       int32(v.CreatedAt),
+			CreatedAtFormat: time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+		}
+	}
+	return res
+}
+
+func (g *GroupPasture) ToPb() *operationPb.AddPastureRequest {
+	return &operationPb.AddPastureRequest{
+		Id:           int32(g.Id),
+		Name:         g.Name,
+		ManagerUser:  g.ManagerUser,
+		ManagerPhone: g.ManagerPhone,
+		Address:      g.Address,
+		IsShow:       g.IsShow,
+		CreatedAt:    int32(g.CreatedAt),
+	}
+}

+ 0 - 19
model/kpe_event.go

@@ -1,19 +0,0 @@
-package model
-
-import (
-	"time"
-)
-
-type KpeEvent struct {
-	Id            int       `xorm:"not null pk default 0 comment('主键自增ID') INT(11)"`
-	Name          string    `xorm:"not null default '' comment('事件名称') VARCHAR(260)"`
-	Remarks       string    `xorm:"not null default '' comment('事件备注') VARCHAR(260)"`
-	Category      int       `xorm:"not null default 0 comment('事件分类') SMALLINT(2)"`
-	IndicatorsIds string    `xorm:"not null default '{}' comment('指标影响IDS json格式') VARCHAR(260)"`
-	BaseIds       string    `xorm:"not null default '{}' comment('字段影响IDS json格式') VARCHAR(260)"`
-	EventIds      string    `xorm:"not null default '{}' comment('事件IDS json格式') VARCHAR(260)"`
-	FiledIds      string    `xorm:"not null default '{}' comment('字段IDS json格式') VARCHAR(260)"`
-	IsShow        int       `xorm:"not null default 1 comment('是否显示菜单 1:显示 0: 隐藏') TINYINT(1)"`
-	CreateTime    time.Time `xorm:"not null default 'CURRENT_TIMESTAMP' comment('创建时间') DATETIME"`
-	UpdateTime    time.Time `xorm:"not null default 'CURRENT_TIMESTAMP' comment('更新时间') DATETIME"`
-}

+ 0 - 19
model/kpt_fileds.go

@@ -1,19 +0,0 @@
-package model
-
-import (
-	"time"
-)
-
-type KptFileds struct {
-	Id             int       `xorm:"not null pk autoincr comment('主键自增') INT(11)"`
-	FieldName      string    `xorm:"not null comment('字段名称') VARCHAR(260)"`
-	ComponentsType int       `xorm:"not null default 0 comment('组件类型 0 单行文本 1 多行文本 2 下拉框 3 单选框 4 多选框 5 日期选择器 6 时间选择器 7 开关') TINYINT(1)"`
-	FieldType      int       `xorm:"not null default 0 comment('字段类型 0 无效类型 1 日期类型 2 时间类型 3 字符串 4 小数类型 5 bool类型') TINYINT(1)"`
-	FiledLen       int       `xorm:"not null default 0 comment('字段长度') SMALLINT(5)"`
-	MinValue       int       `xorm:"not null default 0 comment('取值范围: 最小值') INT(11)"`
-	MaxValue       int       `xorm:"not null default 0 comment('取值范围: 最大值') INT(11)"`
-	IsShow         int       `xorm:"not null default 1 comment('是否启用 0 否 1 是') TINYINT(1)"`
-	Description    string    `xorm:"not null default '' comment('字段描述') VARCHAR(260)"`
-	CreateTime     time.Time `xorm:"not null default 'CURRENT_TIMESTAMP' comment('创建时间') DATETIME"`
-	UpdateTime     time.Time `xorm:"not null default 'CURRENT_TIMESTAMP' comment('更新时间') DATETIME"`
-}

+ 28 - 0
model/system_group_pasture_permissions.go

@@ -0,0 +1,28 @@
+package model
+
+import operationPb "kpt-tmr-group/proto/go/backend/operation"
+
+type SystemGroupPasturePermissions 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 *SystemGroupPasturePermissions) TableName() string {
+	return "system_group_pasture_permissions"
+}
+
+func NewSystemGroupPasturePermissions(roleID int64, pastureIds []uint32) []*SystemGroupPasturePermissions {
+	systemPasturePermissions := make([]*SystemGroupPasturePermissions, len(pastureIds))
+	for i, v := range pastureIds {
+		systemPasturePermissions[i] = &SystemGroupPasturePermissions{
+			RoleId:    roleID,
+			PastureId: int64(v),
+			IsShow:    operationPb.IsShow_OK,
+		}
+	}
+	return systemPasturePermissions
+}

+ 124 - 0
model/system_menu.go

@@ -0,0 +1,124 @@
+package model
+
+import (
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
+)
+
+const (
+	Level1 = iota + 1
+	Level2
+	Level3
+)
+
+type SystemMenu struct {
+	Id        int64                   `json:"id,omitempty"`
+	Name      string                  `json:"name,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      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_menu"
+}
+
+func NewSystemMenu(req *operationPb.AddMenuRequest) *SystemMenu {
+	return &SystemMenu{
+		Name:      req.Name,
+		MenuType:  req.MenuType,
+		Level:     req.Level,
+		Title:     req.Title,
+		Path:      req.Path,
+		Component: req.Component,
+		Icon:      req.Icon,
+		Sort:      req.Sort,
+		Redirect:  req.Redirect,
+		ParentId:  int64(req.ParentId),
+		IsShow:    operationPb.IsShow_OK,
+		IsDelete:  operationPb.IsShow_OK,
+	}
+}
+
+type SystemMenuSlice []*SystemMenu
+
+func (s SystemMenuSlice) ToPB() []*operationPb.AddMenuRequest {
+	level := make(map[int32][]*operationPb.AddMenuRequest, 0)
+	for _, menu := range s {
+		if _, ok := level[menu.Level]; !ok {
+			level[menu.Level] = make([]*operationPb.AddMenuRequest, 0)
+		}
+		level[menu.Level] = append(level[menu.Level], &operationPb.AddMenuRequest{
+			Id:              int32(menu.Id),
+			Name:            menu.Name,
+			ParentId:        int32(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:       int32(menu.CreatedAt),
+			CreatedAtFormat: time.Unix(menu.CreatedAt, 0).Format(LayoutTime),
+			Level:           menu.Level,
+			Affix:           true,
+			KeepAlive:       true,
+			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)
+			}
+		}
+	}
+
+	return level[Level1]
+}
+
+func (s *SystemMenu) ToPb() *operationPb.AddMenuRequest {
+	return &operationPb.AddMenuRequest{
+		Id:              int32(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:        int32(s.ParentId),
+		IsShow:          s.IsShow,
+		CreatedAt:       int32(s.CreatedAt),
+		CreatedAtFormat: time.Unix(s.CreatedAt, 0).Format(LayoutTime),
+	}
+}

+ 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 []uint32) []*SystemMenuPermissions {
+	systemMenuPermissions := make([]*SystemMenuPermissions, len(menuIds))
+	for i, v := range menuIds {
+		systemMenuPermissions[i] = &SystemMenuPermissions{
+			RoleId: roleID,
+			MenuId: int64(v),
+			IsShow: operationPb.IsShow_OK,
+		}
+	}
+	return systemMenuPermissions
+}

+ 33 - 0
model/system_mobile.go

@@ -0,0 +1,33 @@
+package model
+
+import (
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
+)
+
+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"
+}
+
+type SystemMobileSlice []*SystemMobile
+
+func (s SystemMobileSlice) ToPB() []*operationPb.MobileData {
+	res := make([]*operationPb.MobileData, len(s))
+	for i, v := range s {
+		res[i] = &operationPb.MobileData{
+			Id:              uint32(v.Id),
+			Name:            v.Name,
+			CreatedAt:       uint32(v.CreatedAt),
+			CreatedAtFormat: time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+		}
+	}
+	return res
+}

+ 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 []uint32) []*SystemMobilePermissions {
+	systemMobilePermissions := make([]*SystemMobilePermissions, len(mobileIds))
+	for i, v := range mobileIds {
+		systemMobilePermissions[i] = &SystemMobilePermissions{
+			RoleId:   roleID,
+			MobileId: int64(v),
+			IsShow:   operationPb.IsShow_OK,
+		}
+	}
+	return systemMobilePermissions
+}

+ 61 - 0
model/system_role.go

@@ -0,0 +1,61 @@
+package model
+
+import (
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"time"
+)
+
+type SystemRole struct {
+	Id         int64                   `json:"id,omitempty"`
+	Name       string                  `json:"name,omitempty"`
+	Remarks    string                  `json:"remarks,omitempty"`
+	IsShow     operationPb.IsShow_Kind `json:"is_show,omitempty"`
+	CreateUser string                  `json:"create_user,omitempty"`
+	CreatedAt  int64                   `json:"created_at,omitempty"`
+	UpdatedAt  int64                   `json:"updated_at,omitempty"`
+}
+
+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:              uint32(v.Id),
+			Name:            v.Name,
+			Remarks:         v.Remarks,
+			CreateUser:      v.CreateUser,
+			IsShow:          v.IsShow,
+			CreatedAt:       uint32(v.CreatedAt),
+			CreatedAtFormat: time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+		}
+	}
+	return res
+}
+
+func (s *SystemRole) ToPb() *operationPb.AddRoleRequest {
+	return &operationPb.AddRoleRequest{
+		Id:              uint32(s.Id),
+		Name:            s.Name,
+		CreateUser:      s.CreateUser,
+		IsShow:          s.IsShow,
+		CreatedAt:       uint32(s.CreatedAt),
+		CreatedAtFormat: time.Unix(s.CreatedAt, 0).Format(LayoutTime),
+	}
+}

+ 128 - 0
model/system_user.go

@@ -0,0 +1,128 @@
+package model
+
+import (
+	"fmt"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+)
+
+type SystemUser struct {
+	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:   int32(v.Id),
+			Name: v.Name,
+		}
+	}
+
+	return &operationPb.UserAuth{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.UserAuthData{
+			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(roleList []*SystemRole) []*operationPb.AddSystemUser {
+	res := make([]*operationPb.AddSystemUser, len(s))
+	for i, v := range s {
+		res[i] = &operationPb.AddSystemUser{
+			Id:              int32(v.Id),
+			Name:            v.Name,
+			Phone:           v.Phone,
+			EmployeeName:    v.EmployeeName,
+			CreateUser:      v.CreateUser,
+			IsShow:          v.IsShow,
+			CreatedAt:       int32(v.CreatedAt),
+			CreatedAtFormat: time.Unix(v.CreatedAt, 0).Format(LayoutTime),
+			RoleName:        strings.TrimRight(v.UserRoleFormat(roleList), ","),
+		}
+	}
+	return res
+}
+
+func (s *SystemUser) UserRoleFormat(roleList []*SystemRole) string {
+	ids := strings.Split(s.RoleIds, ",")
+	roleListName := ""
+	for _, id := range ids {
+		for _, r := range roleList {
+			if fmt.Sprintf("%d", r.Id) != id {
+				continue
+			}
+			roleListName += fmt.Sprintf("%s,", r.Name)
+		}
+	}
+	return roleListName
+}
+
+func (s *SystemUser) ToPb() *operationPb.AddSystemUser {
+
+	roles := make([]int32, 0)
+	roleIds := strings.Split(s.RoleIds, ",")
+	for _, roleId := range roleIds {
+		id, _ := strconv.Atoi(roleId)
+		roles = append(roles, int32(id))
+	}
+
+	return &operationPb.AddSystemUser{
+		Id:              int32(s.Id),
+		Name:            s.Name,
+		Phone:           s.Phone,
+		CreateUser:      s.CreateUser,
+		EmployeeName:    s.EmployeeName,
+		IsShow:          s.IsShow,
+		CreatedAt:       int32(s.CreatedAt),
+		CreatedAtFormat: time.Unix(s.CreatedAt, 0).Format(LayoutTime),
+		RoleIds:         roles,
+	}
+}

+ 9 - 0
model/wechat.go

@@ -0,0 +1,9 @@
+package model
+
+type JsCode struct {
+	SessionKey string `json:"session_key"`
+	UnionId    string `json:"unionid"`
+	ErrMsg     string `json:"errmsg"`
+	OpenId     string `json:"openid"`
+	ErrCode    int32  `json:"errcode"`
+}

+ 288 - 0
module/backend/feed_service.go

@@ -0,0 +1,288 @@
+package backend
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"kpt-tmr-group/model"
+	"kpt-tmr-group/pkg/logger/zaplog"
+	"kpt-tmr-group/pkg/xerr"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+
+	"github.com/xuri/excelize/v2"
+	"go.uber.org/zap"
+
+	"gorm.io/gorm"
+)
+
+// CreateFeedFormula 添加数据
+func (s *StoreEntry) CreateFeedFormula(ctx context.Context, req *operationPb.AddFeedFormulaRequest) error {
+	forage := model.NewFeedFormula(req)
+	if err := s.DB.Create(forage).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// EditFeedFormula 编辑数据
+func (s *StoreEntry) EditFeedFormula(ctx context.Context, req *operationPb.AddFeedFormulaRequest) error {
+
+	forage := model.FeedFormula{Id: int64(req.Id)}
+	if err := s.DB.Where("is_delete = ?", operationPb.IsShow_OK).First(forage).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	updateData := &model.FeedFormula{
+		Name:               req.Name,
+		Colour:             req.Colour,
+		CattleCategoryId:   req.CattleCategoryId,
+		CattleCategoryName: req.CattleCategoryName,
+		FormulaTypeId:      req.FormulaTypeId,
+		FormulaTypeName:    req.FormulaTypeName,
+		DataSourceId:       req.DataSourceId,
+		DataSourceName:     req.DataSourceName,
+		Remarks:            req.Remarks,
+		IsShow:             req.IsShow,
+	}
+
+	if err := s.DB.Model(new(model.Forage)).
+		Omit("is_show", "is_delete", "encode_number", "formula_type_id", "formula_type_name", "data_source", "version", "is_modify").
+		Where("id = ?", req.Id).
+		Updates(updateData).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+
+	return nil
+}
+
+// SearchFeedFormulaList 查询数据列表
+func (s *StoreEntry) SearchFeedFormulaList(ctx context.Context, req *operationPb.SearchFeedFormulaRequest) (*operationPb.SearchFeedFormulaListResponse, error) {
+	feedFormula := make([]*model.FeedFormula, 0)
+	var count int64 = 0
+
+	pref := s.DB.Model(new(model.Forage)).Where("is_delete = ?", operationPb.IsShow_OK)
+	if req.Name != "" {
+		pref.Where("name like ?", fmt.Sprintf("%s%s%s", "%", req.Name, "%"))
+	}
+
+	if req.CattleCategoryId > 0 {
+		pref.Where("cattle_category_id = ?", req.CattleCategoryId)
+	}
+
+	if req.FormulaTypeId > 0 {
+		pref.Where("formula_type_id = ?", req.FormulaTypeId)
+	}
+
+	if req.IsShow > 0 {
+		pref.Where("is_show = ?", req.IsShow)
+	}
+
+	if req.DataSource > 0 {
+		pref.Where("data_source = ?", req.DataSource)
+	}
+
+	if req.Remarks != "" {
+		pref.Where("remarks = ?", req.Remarks)
+	}
+
+	if err := pref.Order("id desc").Count(&count).Limit(int(req.Pagination.PageSize)).Offset(int(req.Pagination.PageOffset)).
+		Find(&feedFormula).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &operationPb.SearchFeedFormulaListResponse{
+		Page:     req.Pagination.Page,
+		PageSize: req.Pagination.PageSize,
+		Total:    int32(count),
+		List:     model.FeedFormulaSlice(feedFormula).ToPB(),
+	}, nil
+}
+
+// IsShowFeedFormula 是否启用和是否可修改
+func (s *StoreEntry) IsShowFeedFormula(ctx context.Context, req *operationPb.IsShowModifyFeedFormula) error {
+	feedFormula := &model.FeedFormula{Id: int64(req.FeedFormulaId)}
+	if err := s.DB.First(feedFormula).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if req.EditType == 1 {
+		if err := s.DB.Model(new(model.FeedFormula)).Where("id = ?", req.FeedFormulaId).Update("is_show", req.IsShow).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+	}
+
+	if req.EditType == 2 {
+		if err := s.DB.Model(new(model.FeedFormula)).Where("id = ?", req.FeedFormulaId).Update("is_modify", req.IsShow).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+	}
+	return nil
+}
+
+// DeleteFeedFormula 是否删除
+func (s *StoreEntry) DeleteFeedFormula(ctx context.Context, feedFormulaId int64) error {
+	feedFormula := &model.FeedFormula{Id: feedFormulaId}
+	if err := s.DB.First(feedFormula).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(new(model.FeedFormula)).Where("id = ?", feedFormula).Update("is_delete", operationPb.IsShow_NO).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// ExcelImportFeedFormula 导入excel
+func (s *StoreEntry) ExcelImportFeedFormula(ctx context.Context, req io.Reader) error {
+	xlsx, err := excelize.OpenReader(req)
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+	defer xlsx.Close()
+
+	rows, err := xlsx.GetRows(xlsx.GetSheetName(xlsx.GetActiveSheetIndex()))
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+
+	if len(rows) > 10000 {
+		rows = rows[:10000]
+	}
+	feedFormulaList := make([]*model.FeedFormula, 0)
+	for i, row := range rows {
+		if i == 0 {
+			continue
+		}
+		var (
+			name, encodeNumber, cattleCategoryName, formulaTypeName, dataSourceName, remarks, isShowStr string
+			isShow                                                                                      operationPb.IsShow_Kind
+		)
+
+		for k, v := range row {
+			if k == 0 {
+				name = v
+			}
+			if k == 1 {
+				encodeNumber = v
+			}
+			if k == 2 {
+				cattleCategoryName = v
+			}
+			if k == 3 {
+				formulaTypeName = v
+			}
+			if k == 4 {
+				dataSourceName = v
+			}
+			if k == 5 {
+				remarks = v
+			}
+			if k == 6 {
+				isShowStr = v
+			}
+		}
+
+		if isShowStr == "是" {
+			isShow = operationPb.IsShow_OK
+		} else {
+			isShow = operationPb.IsShow_NO
+		}
+
+		feedFormulaItem := &model.FeedFormula{
+			Name:               name,
+			EncodeNumber:       encodeNumber,
+			CattleCategoryName: cattleCategoryName,
+			FormulaTypeName:    formulaTypeName,
+			Remarks:            remarks,
+			IsShow:             isShow,
+			IsDelete:           operationPb.IsShow_OK,
+			DataSourceId:       operationPb.DataSource_EXCEL_IMPORT,
+			DataSourceName:     dataSourceName,
+		}
+		feedFormulaList = append(feedFormulaList, feedFormulaItem)
+	}
+
+	if len(feedFormulaList) > 0 {
+		if err = s.DB.Create(feedFormulaList).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+	}
+
+	return nil
+}
+
+// ExcelExportFeedFormula 流式导出excel
+func (s *StoreEntry) ExcelExportFeedFormula(ctx context.Context, req *operationPb.SearchFeedFormulaRequest) (*bytes.Buffer, error) {
+	res, err := s.SearchFeedFormulaList(ctx, req)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	if len(res.List) <= 0 {
+		return nil, xerr.Custom("数据为空")
+	}
+
+	file := excelize.NewFile()
+	defer file.Close()
+
+	streamWriter, err := file.NewStreamWriter("Sheet1")
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	titles := []interface{}{"配方名称", "配方编码", "畜牧类别", "配方类别", "来源", "备注", "是否启用",
+		"饲料组", "饲料名称", "重量(kg)", "搅拌延迟(min)", "是否锁定牛头数比例", "顺序"}
+	if err = streamWriter.SetRow("A1", titles); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	for i, item := range res.List {
+		cell, err := excelize.CoordinatesToCellName(1, i+2)
+		if err != nil {
+			zaplog.Error("excelize.CoordinatesToCellName", zap.Any("Err", err))
+			continue
+		}
+		row := make([]interface{}, 0)
+		row = append(row, item.Name, item.EncodeNumber, item.CattleCategoryName, item.FormulaTypeName, item.DataSourceName,
+			item.Remarks, item.IsShow)
+
+		if err = streamWriter.SetRow(cell, row); err != nil {
+			return nil, xerr.WithStack(err)
+		}
+	}
+
+	if err = streamWriter.Flush(); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return file.WriteToBuffer()
+}
+
+// ExcelTemplateFeedFormula 导出模板
+func (s *StoreEntry) ExcelTemplateFeedFormula(ctx context.Context) (*bytes.Buffer, error) {
+	file := excelize.NewFile()
+	defer file.Close()
+
+	streamWriter, err := file.NewStreamWriter("Sheet1")
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	titles := []interface{}{"配方名称", "配方编码", "畜牧类别", "配方类别", "来源", "备注", "是否启用",
+		"饲料组", "饲料名称", "重量(kg)", "搅拌延迟(min)", "是否锁定牛头数比例", "顺序"}
+	if err = streamWriter.SetRow("A1", titles); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	if err = streamWriter.Flush(); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return file.WriteToBuffer()
+}

+ 129 - 0
module/backend/interface.go

@@ -0,0 +1,129 @@
+package backend
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"kpt-tmr-group/config"
+	"kpt-tmr-group/pkg/di"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"kpt-tmr-group/service/wechat"
+	"kpt-tmr-group/store/kptstore"
+
+	"go.uber.org/dig"
+)
+
+var Module = di.Options(di.Provide(NewStore))
+
+type Hub struct {
+	dig.In
+	OpsService KptService
+}
+
+type StoreEntry struct {
+	dig.In
+
+	Cfg *config.AppConfig
+	DB  *kptstore.DB
+	//SSO *sso.Cache
+	// AsynqClient asynqsvc.Client
+	// Cache *redis.Client
+	WxClient *wechat.ClientService
+}
+
+func NewStore(store StoreEntry) KptService {
+	return &store
+}
+
+type KptService interface {
+	PastureService   // 牧场相关操作
+	SystemOperation  // 系统相关操作
+	WxAppletService  // 小程序相关
+	StatisticService // 统计分析
+}
+
+type PastureService interface {
+	// CreateGroupPasture 牧场管理相关
+	CreateGroupPasture(ctx context.Context, req *operationPb.AddPastureRequest) error
+	EditGroupPasture(ctx context.Context, req *operationPb.AddPastureRequest) error
+	SearchGroupPastureList(ctx context.Context, req *operationPb.SearchPastureRequest) (*operationPb.SearchPastureResponse, error)
+	DeleteGroupPasture(ctx context.Context, pastureId int64) error
+	ResetPasswordGroupPasture(ctx context.Context, pastureId int64) error
+	IsShowGroupPasture(ctx context.Context, req *operationPb.IsShowGroupPasture) error
+
+	// ParentCattleCategoryList 牧畜类别
+	ParentCattleCategoryList(ctx context.Context) map[operationPb.CattleCategoryParent_Kind]string
+	AddCattleCategory(ctx context.Context, req *operationPb.AddCattleCategoryRequest) error
+	EditCattleCategory(ctx context.Context, req *operationPb.AddCattleCategoryRequest) error
+	IsShowCattleCategory(ctx context.Context, req *operationPb.IsShowCattleCategory) error
+	DeleteCattleCategory(ctx context.Context, cattleCategoryId int64) error
+	SearchCattleCategoryList(ctx context.Context, req *operationPb.SearchCattleCategoryRequest) (*operationPb.SearchCattleCategoryResponse, error)
+
+	// ParentForageCategoryList 饲料类别相关
+	ParentForageCategoryList(ctx context.Context) map[operationPb.ForageCategoryParent_Kind]string
+	AddForageCategory(ctx context.Context, req *operationPb.AddForageCategoryRequest) error
+	EditForageCategory(ctx context.Context, req *operationPb.AddForageCategoryRequest) error
+	IsShowForageCategory(ctx context.Context, req *operationPb.IsShowForageCategory) error
+	DeleteForageCategory(ctx context.Context, cattleCategoryId int64) error
+	SearchForageCategoryList(ctx context.Context, req *operationPb.SearchForageCategoryRequest) (*operationPb.SearchForageCategoryResponse, error)
+
+	// CreateForage 饲料相关
+	CreateForage(ctx context.Context, req *operationPb.AddForageRequest) error
+	EditForage(ctx context.Context, req *operationPb.AddForageRequest) error
+	SearchForageList(ctx context.Context, req *operationPb.SearchForageListRequest) (*operationPb.SearchForageListResponse, error)
+	ForageEnumList(ctx context.Context) *operationPb.ForageEnumList
+	DeleteForageList(ctx context.Context, ids []int64) error
+	IsShowForage(ctx context.Context, req *operationPb.IsShowForage) error
+	ExcelImportForage(ctx context.Context, req io.Reader) error
+	ExcelExportForage(ctx context.Context, req *operationPb.SearchForageListRequest) (*bytes.Buffer, error)
+	ExcelTemplateForage(ctx context.Context) (*bytes.Buffer, error)
+
+	// CreateFeedFormula 饲料配方
+	CreateFeedFormula(ctx context.Context, req *operationPb.AddFeedFormulaRequest) error
+	EditFeedFormula(ctx context.Context, req *operationPb.AddFeedFormulaRequest) error
+	SearchFeedFormulaList(ctx context.Context, req *operationPb.SearchFeedFormulaRequest) (*operationPb.SearchFeedFormulaListResponse, error)
+	IsShowFeedFormula(ctx context.Context, req *operationPb.IsShowModifyFeedFormula) error
+	DeleteFeedFormula(ctx context.Context, feedFormulaId int64) error
+	ExcelImportFeedFormula(ctx context.Context, req io.Reader) error
+	ExcelExportFeedFormula(ctx context.Context, req *operationPb.SearchFeedFormulaRequest) (*bytes.Buffer, error)
+	ExcelTemplateFeedFormula(ctx context.Context) (*bytes.Buffer, error)
+}
+
+type SystemOperation interface {
+	// Auth 系统用户相关
+	Auth(ctx context.Context, auth *operationPb.UserAuthData) (*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) (*operationPb.SearchUserResponse, error)
+	EditSystemUser(ctx context.Context, req *operationPb.AddSystemUser) error
+	DeleteSystemUser(ctx context.Context, userId int64) error
+	ResetPasswordSystemUser(ctx context.Context, userId int64) error
+	DetailsSystemUser(ctx context.Context, userId int64) (*operationPb.UserDetails, 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
+	GetRolePermissions(ctx context.Context, roleId int64) (*operationPb.RolePermissionsList, error)
+	SearchSystemRoleList(ctx context.Context, req *operationPb.SearchRoleRequest) (*operationPb.SearchRoleResponse, 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) (*operationPb.SearchMenuResponse, error)
+	DeleteSystemMenu(ctx context.Context, menuId int64) error
+
+	// SearchMobileList 移动端
+	SearchMobileList(ctx context.Context, req *operationPb.SearchMobileRequest) (*operationPb.SearchMobileResponse, error)
+}
+
+type StatisticService interface {
+	SearchFormulaEstimateList(ctx context.Context, req *operationPb.SearchFormulaEstimateRequest) (*operationPb.SearchFormulaEstimateResponse, error)
+}
+
+type WxAppletService interface {
+	GetOpenId(ctx context.Context, jsCode string) (*operationPb.WxOpenId, error)
+}

+ 709 - 0
module/backend/pasture_service.go

@@ -0,0 +1,709 @@
+package backend
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"kpt-tmr-group/model"
+	"kpt-tmr-group/pkg/logger/zaplog"
+	"kpt-tmr-group/pkg/tool"
+	"kpt-tmr-group/pkg/xerr"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"net/http"
+	"strconv"
+
+	"go.uber.org/zap"
+
+	"github.com/xuri/excelize/v2"
+
+	"gorm.io/gorm"
+)
+
+var (
+	CattleParentCategoryMap = map[operationPb.CattleCategoryParent_Kind]string{
+		operationPb.CattleCategoryParent_LACTATION_CAW: "泌乳牛",
+		operationPb.CattleCategoryParent_FATTEN_CAW:    "育肥牛",
+		operationPb.CattleCategoryParent_RESERVE_CAW:   "后备牛",
+		operationPb.CattleCategoryParent_DRY_CAW:       "干奶牛",
+		operationPb.CattleCategoryParent_PERINATAL_CAW: "围产牛",
+		operationPb.CattleCategoryParent_OTHER_CAW:     "其他",
+	}
+	ForageParentCategoryMap = map[operationPb.ForageCategoryParent_Kind]string{
+		operationPb.ForageCategoryParent_ROUGHAGE:                       "粗料",
+		operationPb.ForageCategoryParent_CONCENTRATE:                    "精料",
+		operationPb.ForageCategoryParent_HALF_ROUGHAGE_HALF_CONCENTRATE: "粗料精料各半",
+		operationPb.ForageCategoryParent_OTHER:                          "其他",
+	}
+	ForageSourceMap = map[operationPb.ForageSource_Kind]string{
+		operationPb.ForageSource_SYSTEM_BUILT_IN: "系统内置",
+		operationPb.ForageSource_USER_DEFINED:    "用户自定义",
+	}
+	ForagePlanTypeMap = map[operationPb.ForagePlanType_Kind]string{
+		operationPb.ForagePlanType_INVALID:     "无",
+		operationPb.ForagePlanType_FORKLIFT:    "铲车",
+		operationPb.ForagePlanType_CONCENTRATE: "精料",
+	}
+	JumpDelaTypeMap = map[operationPb.JumpDelaType_Kind]string{
+		operationPb.JumpDelaType_INVALID: "禁用",
+		operationPb.JumpDelaType_THREE:   "3秒",
+		operationPb.JumpDelaType_SIX:     "6秒",
+		operationPb.JumpDelaType_NINE:    "9秒",
+	}
+	IsShowMap = map[operationPb.IsShow_Kind]string{
+		operationPb.IsShow_OK: "是",
+		operationPb.IsShow_NO: "否",
+	}
+)
+
+// CreateGroupPasture 创建集团牧场
+func (s *StoreEntry) CreateGroupPasture(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
+}
+
+// EditGroupPasture 创建集团牧场
+func (s *StoreEntry) EditGroupPasture(ctx context.Context, req *operationPb.AddPastureRequest) error {
+	groupPasture := &model.GroupPasture{Id: int64(req.Id)}
+	if err := s.DB.First(groupPasture).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在!")
+		}
+		return xerr.WithStack(err)
+	}
+
+	updateData := &model.GroupPasture{
+		Name:         req.Name,
+		Account:      req.Account,
+		ManagerUser:  req.ManagerUser,
+		ManagerPhone: req.ManagerPhone,
+		Address:      req.Address,
+	}
+
+	if err := s.DB.Model(new(model.GroupPasture)).Omit("is_show", "password").
+		Where("id = ?", req.Id).
+		Updates(updateData).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// SearchGroupPastureList 查询牧场列表
+func (s *StoreEntry) SearchGroupPastureList(ctx context.Context, req *operationPb.SearchPastureRequest) (*operationPb.SearchPastureResponse, error) {
+	groupPasture := make([]*model.GroupPasture, 0)
+	var count int64 = 0
+
+	pref := s.DB.Model(new(model.GroupPasture)).Where("is_delete = ? ", operationPb.IsShow_OK)
+	if req.Name != "" {
+		pref.Where("name like ?", fmt.Sprintf("%s%s%s", "%", req.Name, "%"))
+	}
+	if req.ManagerPhone != "" {
+		pref.Where("manager_phone like ?", fmt.Sprintf("%s%s%s", "%", req.ManagerPhone, "%"))
+	}
+
+	if req.ManagerUser != "" {
+		pref.Where("manager_user like ?", fmt.Sprintf("%s%s%s", "%", req.ManagerUser, "%"))
+	}
+
+	if req.StartTime > 0 && req.EndTime > 0 && req.EndTime >= req.StartTime {
+		pref.Where("created_at BETWEEN  ? AND ? ", req.StartTime, req.EndTime)
+	}
+
+	if err := pref.Order("id desc").Count(&count).Limit(int(req.Pagination.PageSize)).Offset(int(req.Pagination.PageOffset)).
+		Find(&groupPasture).Debug().Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &operationPb.SearchPastureResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.SearchPastureData{
+			Page:     req.Pagination.Page,
+			Total:    int32(count),
+			PageSize: req.Pagination.PageSize,
+			List:     model.GroupPastureSlice(groupPasture).ToPB(),
+		},
+	}, nil
+}
+
+func (s *StoreEntry) DeleteGroupPasture(ctx context.Context, pastureId int64) error {
+	groupPasture := &model.GroupPasture{
+		Id: pastureId,
+	}
+	if err := s.DB.First(groupPasture).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(groupPasture).Update("is_delete", operationPb.IsShow_NO).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+func (s *StoreEntry) ResetPasswordGroupPasture(ctx context.Context, pastureId int64) error {
+	groupPasture := &model.GroupPasture{
+		Id: pastureId,
+	}
+	if err := s.DB.Where("is_delete = ?", operationPb.IsShow_OK).First(groupPasture).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(groupPasture).Update("password", tool.Md5String(model.InitManagerPassword)).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+func (s *StoreEntry) IsShowGroupPasture(ctx context.Context, req *operationPb.IsShowGroupPasture) error {
+	groupPasture := &model.GroupPasture{
+		Id: int64(req.PastureId),
+	}
+	if err := s.DB.First(groupPasture).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(groupPasture).Where("is_delete = ?", operationPb.IsShow_OK).Update("is_show", req.IsShow).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// ParentCattleCategoryList 畜牧类别父类列表
+func (s *StoreEntry) ParentCattleCategoryList(ctx context.Context) map[operationPb.CattleCategoryParent_Kind]string {
+	return CattleParentCategoryMap
+}
+
+// AddCattleCategory 添加畜牧分类
+func (s *StoreEntry) AddCattleCategory(ctx context.Context, req *operationPb.AddCattleCategoryRequest) error {
+	cattleCategory := model.NewCattleCategory(req)
+	if err := s.DB.Create(cattleCategory).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// EditCattleCategory 编辑畜牧分类
+func (s *StoreEntry) EditCattleCategory(ctx context.Context, req *operationPb.AddCattleCategoryRequest) error {
+	cattleCategory := &model.CattleCategory{Id: int64(req.Id)}
+	if err := s.DB.First(cattleCategory).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+	updateData := &model.CattleCategory{
+		ParentName: req.ParentName,
+		Name:       req.Name,
+		Number:     req.Number,
+		ParentId:   req.ParentId,
+	}
+
+	if err := s.DB.Model(new(model.CattleCategory)).Omit("is_show", "is_delete").
+		Where("id = ?", req.Id).
+		Updates(updateData).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// IsShowCattleCategory 是否启用
+func (s *StoreEntry) IsShowCattleCategory(ctx context.Context, req *operationPb.IsShowCattleCategory) error {
+	cattleCategory := &model.CattleCategory{Id: int64(req.CattleCategoryId)}
+	if err := s.DB.First(cattleCategory).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(new(model.CattleCategory)).Where("id = ?", req.CattleCategoryId).Update("is_show", req.IsShow).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// DeleteCattleCategory 是否删除
+func (s *StoreEntry) DeleteCattleCategory(ctx context.Context, cattleCategoryId int64) error {
+	cattleCategory := &model.CattleCategory{Id: cattleCategoryId}
+	if err := s.DB.First(cattleCategory).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(new(model.CattleCategory)).Where("id = ?", cattleCategoryId).Update("is_delete", operationPb.IsShow_NO).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// SearchCattleCategoryList 牧畜分类类别列表
+func (s *StoreEntry) SearchCattleCategoryList(ctx context.Context, req *operationPb.SearchCattleCategoryRequest) (*operationPb.SearchCattleCategoryResponse, error) {
+	cattleCategory := make([]*model.CattleCategory, 0)
+	var count int64 = 0
+
+	pref := s.DB.Model(new(model.CattleCategory)).Where("is_delete = ?", operationPb.IsShow_OK)
+	if req.Name != "" {
+		pref.Where("name like ?", fmt.Sprintf("%s%s%s", "%", req.Name, "%"))
+	}
+
+	if req.ParentName != "" {
+		pref.Where("parent_name like ?", fmt.Sprintf("%s%s%s", "%", req.ParentName, "%"))
+	}
+
+	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(&cattleCategory).Debug().Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &operationPb.SearchCattleCategoryResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.SearchCattleCategoryData{
+			Page:  req.Pagination.Page,
+			Total: int32(count),
+			List:  model.CattleCategorySlice(cattleCategory).ToPB(),
+		},
+	}, nil
+}
+
+// ParentForageCategoryList 饲料类别父类列表
+func (s *StoreEntry) ParentForageCategoryList(ctx context.Context) map[operationPb.ForageCategoryParent_Kind]string {
+	return ForageParentCategoryMap
+}
+
+// AddForageCategory 添加饲料分类
+func (s *StoreEntry) AddForageCategory(ctx context.Context, req *operationPb.AddForageCategoryRequest) error {
+	forageCategory := model.NewForageCategory(req)
+	if err := s.DB.Create(forageCategory).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// EditForageCategory 编辑饲料分类
+func (s *StoreEntry) EditForageCategory(ctx context.Context, req *operationPb.AddForageCategoryRequest) error {
+	forageCategory := &model.ForageCategory{Id: int64(req.Id)}
+	if err := s.DB.First(forageCategory).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+	updateData := &model.ForageCategory{
+		ParentName: req.ParentName,
+		Name:       req.Name,
+		Number:     req.Number,
+		ParentId:   req.ParentId,
+	}
+
+	if err := s.DB.Model(new(model.ForageCategory)).Omit("is_show", "is_delete").
+		Where("id = ?", req.Id).
+		Updates(updateData).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// IsShowForageCategory 是否启用
+func (s *StoreEntry) IsShowForageCategory(ctx context.Context, req *operationPb.IsShowForageCategory) error {
+	forageCategory := &model.ForageCategory{Id: int64(req.ForageCategoryId)}
+	if err := s.DB.First(forageCategory).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(new(model.ForageCategory)).Where("id = ?", req.ForageCategoryId).Update("is_show", req.IsShow).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// DeleteForageCategory 是否删除
+func (s *StoreEntry) DeleteForageCategory(ctx context.Context, forageCategoryId int64) error {
+	forageCategory := &model.ForageCategory{Id: forageCategoryId}
+	if err := s.DB.First(forageCategory).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(new(model.ForageCategory)).Where("id = ?", forageCategoryId).Update("is_delete", operationPb.IsShow_NO).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// SearchForageCategoryList 饲料分类类别列表
+func (s *StoreEntry) SearchForageCategoryList(ctx context.Context, req *operationPb.SearchForageCategoryRequest) (*operationPb.SearchForageCategoryResponse, error) {
+	forageCategory := make([]*model.ForageCategory, 0)
+	var count int64 = 0
+
+	pref := s.DB.Model(new(model.ForageCategory)).Where("is_delete = ?", operationPb.IsShow_OK)
+	if req.Name != "" {
+		pref.Where("name like ?", fmt.Sprintf("%s%s%s", "%", req.Name, "%"))
+	}
+
+	if req.ParentName != "" {
+		pref.Where("parent_name like ?", fmt.Sprintf("%s%s%s", "%", req.ParentName, "%"))
+	}
+
+	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(&forageCategory).Debug().Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &operationPb.SearchForageCategoryResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.SearchForageCategoryData{
+			Page:  req.Pagination.Page,
+			Total: int32(count),
+			List:  model.ForageCategorySlice(forageCategory).ToPB(),
+		},
+	}, nil
+}
+
+// CreateForage 创建饲料
+func (s *StoreEntry) CreateForage(ctx context.Context, req *operationPb.AddForageRequest) error {
+	forage := model.NewForage(req)
+	if err := s.DB.Create(forage).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// EditForage 编辑饲料
+func (s *StoreEntry) EditForage(ctx context.Context, req *operationPb.AddForageRequest) error {
+	forage := model.Forage{Id: int64(req.Id)}
+	if err := s.DB.Where("is_delete = ?", operationPb.IsShow_OK).First(forage).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	updateData := &model.Forage{
+		Name:               req.Name,
+		CategoryId:         int64(req.CategoryId),
+		UniqueEncode:       req.UniqueEncode,
+		ForageSourceId:     req.ForageSourceId,
+		PlanTypeId:         req.PlanTypeId,
+		SmallMaterialScale: req.SmallMaterialScale,
+		AllowError:         int64(req.AllowError),
+		PackageWeight:      int64(req.PackageWeight),
+		Price:              int64(req.Price),
+		JumpWeight:         int64(req.JumpWeight),
+		JumpDelay:          req.JumpDelay,
+		ConfirmStart:       req.ConfirmStart,
+		RelayLocations:     int64(req.RelayLocations),
+		Jmp:                req.Jmp,
+		Backup1:            req.Backup1,
+		Backup2:            req.Backup2,
+		Backup3:            req.Backup3,
+	}
+
+	if err := s.DB.Model(new(model.Forage)).Omit("is_show", "is_delete").Where("id = ?", req.Id).
+		Updates(updateData).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+
+	return nil
+}
+
+func (s *StoreEntry) SearchForageList(ctx context.Context, req *operationPb.SearchForageListRequest) (*operationPb.SearchForageListResponse, error) {
+	forage := make([]*model.Forage, 0)
+	var count int64 = 0
+
+	pref := s.DB.Model(new(model.Forage)).Where("is_delete = ?", operationPb.IsShow_OK)
+	if req.Name != "" {
+		pref.Where("name like ?", fmt.Sprintf("%s%s%s", "%", req.Name, "%"))
+	}
+
+	if req.CategoryId != "" {
+		pref.Where("category_id = ?", req.CategoryId)
+	}
+
+	if req.ForageSourceId > 0 {
+		pref.Where("forage_source_id = ?", req.ForageSourceId)
+	}
+
+	if req.IsShow > 0 {
+		pref.Where("is_show = ?", req.IsShow)
+	}
+
+	if req.AllowError > 0 {
+		pref.Where("allow_error = ?", req.AllowError)
+	}
+
+	if req.AllowError > 0 {
+		pref.Where("allow_error = ?", req.AllowError)
+	}
+
+	if req.JumpWeight > 0 {
+		pref.Where("jump_weight = ?", req.JumpWeight)
+	}
+
+	if req.JumpDelay > 0 {
+		pref.Where("jump_delay = ?", req.JumpDelay)
+	}
+
+	if err := pref.Order("id desc").Count(&count).Limit(int(req.Pagination.PageSize)).Offset(int(req.Pagination.PageOffset)).
+		Find(&forage).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &operationPb.SearchForageListResponse{
+		Page:     req.Pagination.Page,
+		PageSize: req.Pagination.PageSize,
+		Total:    int32(count),
+		List:     model.ForageSlice(forage).ToPB(),
+	}, nil
+}
+
+func (s *StoreEntry) ForageEnumList(ctx context.Context) *operationPb.ForageEnumList {
+	res := &operationPb.ForageEnumList{
+		JumpDelaType:         make([]*operationPb.JumpDelaTypeEnum, 0),
+		ForageSource:         make([]*operationPb.ForageSourceEnum, 0),
+		ForagePlanType:       make([]*operationPb.ForagePlanTypeEnum, 0),
+		CattleParentCategory: make([]*operationPb.CattleParentCategoryEnum, 0),
+		ForageParentCategory: make([]*operationPb.ForageParentCategoryEnum, 0),
+		IsShow:               make([]*operationPb.IsShowEnum, 0),
+	}
+
+	res.JumpDelaType = append(res.JumpDelaType, &operationPb.JumpDelaTypeEnum{
+		Value: operationPb.JumpDelaType_INVALID,
+		Label: "禁用",
+	}, &operationPb.JumpDelaTypeEnum{
+		Value: operationPb.JumpDelaType_THREE,
+		Label: "3秒",
+	}, &operationPb.JumpDelaTypeEnum{
+		Value: operationPb.JumpDelaType_SIX,
+		Label: "6秒",
+	})
+
+	return res
+}
+
+func (s *StoreEntry) DeleteForageList(ctx context.Context, ids []int64) error {
+	if len(ids) == 0 {
+		return xerr.Custom("参数错误")
+	}
+	if err := s.DB.Where("id IN ?", ids).Update("is_delete", operationPb.IsShow_NO).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+func (s *StoreEntry) IsShowForage(ctx context.Context, req *operationPb.IsShowForage) error {
+	forage := &model.Forage{Id: int64(req.ForageId)}
+	if err := s.DB.First(forage).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Model(new(model.Forage)).Where("id = ?", req.ForageId).Update("is_show", req.IsShow).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// ExcelImportForage 导入excel
+func (s *StoreEntry) ExcelImportForage(ctx context.Context, req io.Reader) error {
+	xlsx, err := excelize.OpenReader(req)
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+	defer xlsx.Close()
+
+	rows, err := xlsx.GetRows(xlsx.GetSheetName(xlsx.GetActiveSheetIndex()))
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+
+	if len(rows) > 10000 {
+		rows = rows[:10000]
+	}
+	forageList := make([]*model.Forage, 0)
+	for i, row := range rows {
+		if i == 0 {
+			continue
+		}
+		var (
+			forageSourceId                                int32
+			categoryId, price, jumpWeight, relayLocations int
+			allowError, packageWeight                     int
+			name, uniqueEncode, backup1, backup2, backup3 string
+		)
+
+		for k, v := range row {
+			if k == 0 {
+				name = v
+			}
+			if k == 2 {
+				uniqueEncode = v
+			}
+			if k == 3 {
+				forageSourceId = operationPb.ForageSource_Kind_value[v]
+			}
+			if k == 5 {
+				allowError, _ = strconv.Atoi(v)
+			}
+
+			if k == 6 {
+				packageWeight, _ = strconv.Atoi(v)
+			}
+
+			if k == 7 {
+				price, _ = strconv.Atoi(v)
+			}
+
+			if k == 8 {
+				jumpWeight, _ = strconv.Atoi(v)
+			}
+
+			if k == 11 {
+				relayLocations, _ = strconv.Atoi(v)
+			}
+
+			if k == 13 {
+				backup1 = v
+			}
+
+			if k == 14 {
+				backup2 = v
+			}
+
+			if k == 15 {
+				backup3 = v
+			}
+		}
+
+		forageItem := &model.Forage{
+			Name:               name,
+			CategoryId:         int64(categoryId),
+			MaterialType:       0,
+			UniqueEncode:       uniqueEncode,
+			ForageSourceId:     operationPb.ForageSource_Kind(forageSourceId),
+			PlanTypeId:         0,
+			SmallMaterialScale: "",
+			AllowError:         int64(allowError),
+			PackageWeight:      int64(packageWeight),
+			Price:              int64(price),
+			JumpWeight:         int64(jumpWeight),
+			JumpDelay:          0,
+			ConfirmStart:       0,
+			RelayLocations:     int64(relayLocations),
+			Jmp:                0,
+			Backup1:            backup1,
+			Backup2:            backup2,
+			Backup3:            backup3,
+			IsShow:             operationPb.IsShow_OK,
+			IsDelete:           operationPb.IsShow_OK,
+			DataSource:         operationPb.DataSource_EXCEL_IMPORT,
+		}
+
+		forageList = append(forageList, forageItem)
+	}
+
+	if len(forageList) > 0 {
+		if err = s.DB.Create(forageList).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+	}
+
+	return nil
+}
+
+// ExcelExportForage 流式导出excel
+func (s *StoreEntry) ExcelExportForage(ctx context.Context, req *operationPb.SearchForageListRequest) (*bytes.Buffer, error) {
+	res, err := s.SearchForageList(ctx, req)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	if len(res.List) <= 0 {
+		return nil, xerr.Custom("数据为空")
+	}
+
+	file := excelize.NewFile()
+	defer file.Close()
+
+	streamWriter, err := file.NewStreamWriter("Sheet1")
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	titles := []interface{}{"饲料名称", "饲料分类", "唯一编码", "饲料来源", "计划类型", "允许误差(kg)", "包装单位重量(kg)",
+		"单价", "跳转重量域(kg)", "跳转延迟", "确认开始", "继电器位置", "无上域", "备用字段01", "备用字段02", "备用字段03"}
+	if err = streamWriter.SetRow("A1", titles); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	for i, item := range res.List {
+		cell, err := excelize.CoordinatesToCellName(1, i+2)
+		if err != nil {
+			zaplog.Error("excelize.CoordinatesToCellName", zap.Any("Err", err))
+			continue
+		}
+		row := make([]interface{}, 0)
+		row = append(row, item.Name, item.CategoryName, item.UniqueEncode, item.ForageSourceId, item.PlanTypeId,
+			item.AllowError, item.PackageWeight, float64(item.Price/100.00), item.JumpWeight, item.JumpDelay,
+			item.ConfirmStart, item.RelayLocations, item.Jmp, item.Backup1, item.Backup2, item.Backup3)
+
+		if err = streamWriter.SetRow(cell, row); err != nil {
+			return nil, xerr.WithStack(err)
+		}
+	}
+
+	if err = streamWriter.Flush(); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return file.WriteToBuffer()
+}
+
+// ExcelTemplateForage 导出模板
+func (s *StoreEntry) ExcelTemplateForage(ctx context.Context) (*bytes.Buffer, error) {
+	file := excelize.NewFile()
+	defer file.Close()
+
+	streamWriter, err := file.NewStreamWriter("Sheet1")
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	titles := []interface{}{"饲料名称", "饲料分类", "唯一编码", "饲料来源", "计划类型", "允许误差(kg)", "包装单位重量(kg)",
+		"单价", "跳转重量域(kg)", "跳转延迟", "确认开始", "继电器位置", "无上域", "备用字段01", "备用字段02", "备用字段03"}
+	if err = streamWriter.SetRow("A1", titles); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	if err = streamWriter.Flush(); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return file.WriteToBuffer()
+}

+ 59 - 0
module/backend/statistic_service.go

@@ -0,0 +1,59 @@
+package backend
+
+import (
+	"context"
+	"fmt"
+	"kpt-tmr-group/model"
+	"kpt-tmr-group/pkg/xerr"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+	"net/http"
+	"time"
+)
+
+func (s *StoreEntry) SearchFormulaEstimateList(ctx context.Context, req *operationPb.SearchFormulaEstimateRequest) (*operationPb.SearchFormulaEstimateResponse, error) {
+	startTime, err := time.Parse(model.LayoutTime, req.StartTime)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	endTime, err := time.Parse(model.LayoutTime, req.EndTime)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	startTimeUnix := startTime.Unix()
+	endTimeUnix := endTime.Unix()
+
+	formulaEstimate := make([]*model.FormulaEstimate, 0)
+	var count int64 = 0
+
+	pref := s.DB.Model(new(model.FormulaEstimate))
+	if req.Name != "" {
+		pref.Where("name like ?", fmt.Sprintf("%s%s%s", "%", req.Name, "%"))
+	}
+	if startTimeUnix > 0 && endTimeUnix > 0 && endTimeUnix >= startTimeUnix {
+		pref.Where("created_at BETWEEN ? AND ?", startTimeUnix, endTimeUnix)
+	}
+
+	if req.SearchType == 1 {
+		pref.Where("feed_formula_name = ?", req.Name)
+	} else {
+		pref.Where("barn_id = ?", req.Name)
+	}
+
+	if err = pref.Order("id desc").Count(&count).Limit(int(req.Pagination.PageSize)).Offset(int(req.Pagination.PageOffset)).
+		Find(&formulaEstimate).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &operationPb.SearchFormulaEstimateResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.SearchFormulaEstimate{
+			Page:     req.Pagination.Page,
+			Total:    int32(count),
+			PageSize: req.Pagination.PageSize,
+			List:     model.FormulaEstimateSlice(formulaEstimate).ToPB(),
+		},
+	}, nil
+}

+ 255 - 0
module/backend/system_permissions.go

@@ -0,0 +1,255 @@
+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"
+	"net/http"
+	"sync"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+type SystemAllPermissionsList struct {
+	PastureList []*model.SystemGroupPasturePermissions
+	MenuList    []*model.SystemMenuPermissions
+	MobileList  []*model.SystemMobilePermissions
+}
+
+func (s *StoreEntry) AllPermissionsListToRolePermissions(req *SystemAllPermissionsList) *operationPb.RolePermissionsList {
+	res := &operationPb.RolePermissionsList{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.RolePermissionsData{
+			MobileList:  make([]uint32, 0),
+			MenuList:    make([]uint32, 0),
+			PastureList: make([]uint32, 0),
+		},
+	}
+	wg := sync.WaitGroup{}
+	wg.Add(3)
+	go func() {
+		for _, v := range req.MobileList {
+			res.Data.MobileList = append(res.Data.MobileList, uint32(v.MobileId))
+		}
+		wg.Done()
+	}()
+
+	go func() {
+		for _, v := range req.MenuList {
+			res.Data.MenuList = append(res.Data.MenuList, uint32(v.MenuId))
+		}
+		wg.Done()
+	}()
+
+	go func() {
+		for _, v := range req.PastureList {
+			res.Data.PastureList = append(res.Data.PastureList, uint32(v.PastureId))
+		}
+		wg.Done()
+	}()
+
+	wg.Wait()
+	return res
+}
+
+// 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{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.SystemUserMenuData{
+			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.Data.PastureList = append(systemUserMenuPermissions.Data.PastureList,
+				&operationPb.AddPastureRequest{
+					Id:   int32(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:              int32(menu.Id),
+				Name:            menu.Name,
+				ParentId:        int32(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:       int32(menu.CreatedAt),
+				CreatedAtFormat: time.Unix(menu.CreatedAt, 0).Format(model.LayoutTime),
+				Level:           menu.Level,
+				Affix:           true,
+				KeepAlive:       true,
+				Children:        make([]*operationPb.AddMenuRequest, 0),
+			})
+		}
+
+		for _, leve3Data := range level[model.Level3] {
+			for _, leve2Data := range level[model.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[model.Level2] {
+			for _, leve1Data := range level[model.Level1] {
+				if leve2Data.ParentId == leve1Data.Id {
+					if leve1Data.Children == nil {
+						leve1Data.Children = make([]*operationPb.AddMenuRequest, 0)
+					}
+					leve1Data.Children = append(leve1Data.Children, leve2Data)
+				}
+			}
+		}
+
+		systemUserMenuPermissions.Data.MenuList = level[model.Level1]
+		wg.Done()
+	}()
+
+	go func() {
+		for _, v := range mobileList {
+			systemUserMenuPermissions.Data.MobileList = append(systemUserMenuPermissions.Data.MobileList,
+				&operationPb.AddMobileRequest{
+					Id:   uint32(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.SystemGroupPasturePermissions, 0),
+		MenuList:    make([]*model.SystemMenuPermissions, 0),
+		MobileList:  make([]*model.SystemMobilePermissions, 0),
+	}
+
+	wg := sync.WaitGroup{}
+	wg.Add(3)
+	go func() {
+
+		pastureList := make([]*model.SystemGroupPasturePermissions, 0)
+		if err := s.DB.Where("role_id = ? and is_show = ?", roleId, operationPb.IsShow_OK).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 = ? and is_show = ?", roleId, operationPb.IsShow_OK).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 = ? and is_show = ?", roleId, operationPb.IsShow_OK).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.SystemGroupPasturePermissions) ([]*model.GroupPasture, error) {
+	ids := make([]int64, 0)
+	for _, v := range req {
+		ids = append(ids, v.PastureId)
+	}
+	groupPastureList := make([]*model.GroupPasture, 0)
+	if len(ids) > 0 {
+		if err := s.DB.Where("is_show = ?", operationPb.IsShow_OK).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 len(ids) > 0 {
+		if err := s.DB.Where("is_show = ?", operationPb.IsShow_OK).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 len(ids) > 0 {
+		if err := s.DB.Where("is_show = ?", operationPb.IsShow_OK).Find(&mobileList, ids).Error; err != nil {
+			return nil, xerr.WithStack(err)
+		}
+	}
+
+	return mobileList, nil
+}

+ 603 - 0
module/backend/system_service.go

@@ -0,0 +1,603 @@
+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"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"gorm.io/gorm"
+)
+
+// Auth 用户登录
+func (s *StoreEntry) Auth(ctx context.Context, auth *operationPb.UserAuthData) (*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{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.TokenData{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:       operationPb.IsShow_OK,
+		IsDelete:     operationPb.IsShow_OK,
+	}
+	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) (*operationPb.SearchUserResponse, error) {
+	systemUserList := 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 req.CreatedStartTime > 0 && req.CreatedEndTime > 0 && req.CreatedStartTime <= req.CreatedEndTime {
+		pref.Where("created_at  BETWEEN  ? AND ?", req.CreatedStartTime, req.CreatedEndTime)
+	}
+
+	if err := pref.Order("id desc").Count(&count).Limit(int(req.Pagination.PageSize)).Offset(int(req.Pagination.PageOffset)).
+		Find(&systemUserList).Debug().Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	roleList, err := s.SearchSystemRoleListByIds(ctx, []int64{})
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &operationPb.SearchUserResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.SearchUserData{
+			Page:     req.Pagination.Page,
+			Total:    int32(count),
+			PageSize: req.Pagination.PageSize,
+			List:     model.SystemUserSlice(systemUserList).ToPB(roleList),
+		},
+	}, nil
+}
+
+// EditSystemUser 编辑用户
+func (s *StoreEntry) EditSystemUser(ctx context.Context, req *operationPb.AddSystemUser) error {
+	systemUser := &model.SystemUser{Id: int64(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
+}
+
+// ResetPasswordSystemUser 重置系统用户密码
+func (s *StoreEntry) ResetPasswordSystemUser(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("password", tool.Md5String(model.InitManagerPassword)).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// DetailsSystemUser 系统用户详情
+func (s *StoreEntry) DetailsSystemUser(ctx context.Context, userId int64) (*operationPb.UserDetails, error) {
+	systemUser := &model.SystemUser{
+		Id: userId,
+	}
+	if err := s.DB.First(systemUser).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, xerr.Custom("该用户不存在")
+		}
+		return nil, xerr.WithStack(err)
+	}
+
+	return &operationPb.UserDetails{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: systemUser.ToPb(),
+	}, nil
+}
+
+// IsShowSystemUser 用户是否启用
+func (s *StoreEntry) IsShowSystemUser(ctx context.Context, req *operationPb.IsShowSystemUserRequest) error {
+	systemUser := &model.SystemUser{
+		Id: int64(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.Where("is_show = ?", operationPb.IsShow_OK).Find(&systemRoles, roleIds).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	systemAllPermissionsList := &SystemAllPermissionsList{
+		PastureList: make([]*model.SystemGroupPasturePermissions, 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.NewSystemGroupPasturePermissions(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: int64(req.Id)}
+	if err := s.DB.First(role).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return xerr.Custom("该数据不存在")
+		}
+		return xerr.WithStack(err)
+	}
+
+	if err := s.DB.Transaction(func(tx *gorm.DB) error {
+		updateSystemRole := model.NewSystemRole(req)
+		if err := tx.Omit("is_show").
+			Where("id = ?", role.Id).
+			Updates(updateSystemRole).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+
+		// 更新牧场权限关系表
+		pastureIds := req.PastureId
+		if err := tx.Model(new(model.SystemGroupPasturePermissions)).
+			Where("role_id = ?", req.Id).Update("is_show", operationPb.IsShow_NO).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+
+		newSystemGroupPasturePermissions := model.NewSystemGroupPasturePermissions(int64(req.Id), pastureIds)
+		if err := tx.Create(newSystemGroupPasturePermissions).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+
+		// 更新菜单权限关系表
+		menuIds := req.MenuId
+		if err := tx.Model(new(model.SystemMenuPermissions)).
+			Where("role_id = ?", req.Id).Update("is_show", operationPb.IsShow_NO).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+
+		newSystemMenuPermissions := model.NewSystemMenuPermissions(int64(req.Id), menuIds)
+		if err := tx.Create(newSystemMenuPermissions).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+		// 更新移动端权限关系表
+		mobileIds := req.MobileId
+		if err := tx.Model(new(model.SystemMobilePermissions)).
+			Where("role_id = ?", req.Id).Update("is_show", operationPb.IsShow_NO).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+
+		newSystemMobilePermissions := model.NewSystemMobilePermissions(int64(req.Id), mobileIds)
+		if err := tx.Create(newSystemMobilePermissions).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+		return nil
+	}); 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) (*operationPb.SearchRoleResponse, 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 &operationPb.SearchRoleResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.SearchRoleData{
+			Page:     req.Pagination.Page,
+			Total:    int32(count),
+			PageSize: req.Pagination.PageSize,
+			List:     model.SystemRoleSlice(systemRole).ToPB(),
+		},
+	}, nil
+}
+
+// SearchSystemRoleListByIds 根据id查询角色列表
+func (s *StoreEntry) SearchSystemRoleListByIds(ctx context.Context, ids []int64) ([]*model.SystemRole, error) {
+	systemRoleList := make([]*model.SystemRole, 0)
+	if err := s.DB.Model(new(model.SystemRole)).Where("is_show = ?", operationPb.IsShow_OK).Find(&systemRoleList, ids).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return systemRoleList, nil
+}
+
+// GetRolePermissions 查询系统角色权限
+func (s *StoreEntry) GetRolePermissions(ctx context.Context, roleId int64) (*operationPb.RolePermissionsList, error) {
+	systemRole := &model.SystemRole{
+		Id: roleId,
+	}
+	if err := s.DB.First(systemRole).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, xerr.Custom("该数据不存在")
+		}
+		return nil, xerr.WithStack(err)
+	}
+
+	systemAllPermissionsList := &SystemAllPermissionsList{
+		PastureList: make([]*model.SystemGroupPasturePermissions, 0),
+		MenuList:    make([]*model.SystemMenuPermissions, 0),
+		MobileList:  make([]*model.SystemMobilePermissions, 0),
+	}
+
+	item := s.GetSystemAllPermissionsList(ctx, systemRole.Id)
+	systemAllPermissionsList.PastureList = append(systemAllPermissionsList.PastureList, item.PastureList...)
+	systemAllPermissionsList.MenuList = append(systemAllPermissionsList.MenuList, item.MenuList...)
+	systemAllPermissionsList.MobileList = append(systemAllPermissionsList.MobileList, item.MobileList...)
+
+	return s.AllPermissionsListToRolePermissions(systemAllPermissionsList), 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: int64(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:  int64(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: int64(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) (*operationPb.SearchMenuResponse, 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 &operationPb.SearchMenuResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.SearchMenuData{
+			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
+}
+
+// SearchMobileList 查询移动端角色
+func (s *StoreEntry) SearchMobileList(ctx context.Context, req *operationPb.SearchMobileRequest) (*operationPb.SearchMobileResponse, error) {
+	systemMobile := make([]*model.SystemMobile, 0)
+	var count int64 = 0
+
+	pref := s.DB.Model(new(model.SystemMobile)).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(&systemMobile).Debug().Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &operationPb.SearchMobileResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.SearchMobileData{
+			Page:     req.Pagination.Page,
+			Total:    int32(count),
+			PageSize: req.Pagination.PageSize,
+			List:     model.SystemMobileSlice(systemMobile).ToPB(),
+		},
+	}, nil
+}

+ 34 - 0
module/backend/wx_applet_service.go

@@ -0,0 +1,34 @@
+package backend
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"kpt-tmr-group/model"
+	"kpt-tmr-group/pkg/logger/zaplog"
+	"kpt-tmr-group/pkg/xerr"
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+
+	"go.uber.org/zap"
+)
+
+const (
+	GRANT_TYPE = "authorization_code"
+	OPENID_URL = "https://api.weixin.qq.com/sns/jscode2session"
+)
+
+func (s *StoreEntry) GetOpenId(ctx context.Context, jsCode string) (*operationPb.WxOpenId, error) {
+	url := fmt.Sprintf("%s?appid=%s&secret=%s&js_code=%s&grant_type=%s", OPENID_URL, s.WxClient.AppID, s.WxClient.Secret, jsCode, GRANT_TYPE)
+	res, err := s.WxClient.DoGet(url)
+	if err != nil {
+		zaplog.Error("GetOpenId", zap.Any("DoGet", err), zap.String("url", url))
+		return nil, xerr.WithStack(err)
+	}
+	data := &model.JsCode{}
+	if err = json.Unmarshal(res, data); err != nil {
+		zaplog.Error("GetOpenId", zap.Any("Unmarshal", err), zap.String("url", url))
+		return nil, xerr.WithStack(err)
+	}
+	zaplog.Info("GetOpenId", zap.Any("ok", data))
+	return &operationPb.WxOpenId{Openid: data.OpenId}, nil
+}

+ 207 - 0
pkg/apierr/apierr.go

@@ -0,0 +1,207 @@
+package apierr
+
+import (
+	"encoding/json"
+	"kpt-tmr-group/pkg/xerr"
+	common "kpt-tmr-group/proto/go/backend/common"
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+)
+
+func New(code common.Error_Code) *Error {
+	return &Error{err: &common.Error{
+		Code: code,
+		Msg:  errorMessage(code),
+	}}
+}
+
+func errorMessage(code common.Error_Code) string {
+	var errMessage string
+	if msg, ok := common.Error_Code_name[int32(code)]; ok {
+		errMessage = msg
+	} else {
+		errMessage = "INTERNAL_ERROR"
+	}
+
+	return errMessage
+}
+
+type Error struct {
+	err *common.Error
+}
+
+func (e *Error) GetCode() common.Error_Code {
+	return e.err.Code
+}
+
+func (e *Error) GetMsg() string {
+	return e.err.Msg
+}
+
+func (e *Error) GetErrors() []string {
+	return e.err.Errors
+}
+
+func (e *Error) Error() string {
+	bs, _ := json.Marshal(e.err)
+	return string(bs)
+}
+
+// Is 判断 err 和 e 是否相同
+func (e *Error) Is(err error) bool {
+	if gotErr, ok := err.(*Error); !ok {
+		return false
+	} else {
+		return e.err.Code == gotErr.err.Code
+	}
+}
+
+func (e *Error) MarshalJSON() ([]byte, error) {
+	return json.Marshal(e.err)
+}
+
+func (e *Error) UnmarshalJSON(data []byte) error {
+	var commonErr common.Error
+	if err := json.Unmarshal(data, &commonErr); err != nil {
+		return err
+	}
+
+	e.err = &commonErr
+	return nil
+}
+
+func (e *Error) WithLocaleMessage(c *gin.Context) *Error {
+	if msg := e.GetMsg(); msg != "" {
+		return e.SetMessage(msg)
+	}
+
+	return e
+}
+
+func (e *Error) SetMessage(m string) *Error {
+	e.err.Msg = m
+	return e
+}
+
+func (e *Error) SetErrors(errMessages []string) *Error {
+	e.err.Errors = errMessages
+	return e
+}
+
+// With more information
+func (e *Error) With(errs ...error) *Error {
+	for _, err := range errs {
+		e.err.Errors = append(e.err.Errors, err.Error())
+	}
+
+	return e
+}
+
+// WithContext return error with i18n message ?
+func WithContext(c *gin.Context, code common.Error_Code) *Error {
+	return New(code).WithLocaleMessage(c)
+}
+
+// AbortError 用来处理多种内部错误.
+// 在复杂业务场景中,存在一个接口返回多种业务错误码情况,通过这个函数来统一处理
+func AbortError(c *gin.Context, err error) {
+	if err == nil {
+		return
+	}
+
+	if e, ok := xerr.Cause(err).(*Error); ok {
+		// 取默认状态码
+		statusCode := DefaultErrorStatusCode(e.err.Code)
+		if !shouldIgnoreCode(statusCode) {
+			c.Error(e)
+		}
+		c.AbortWithStatusJSON(statusCode, e.WithLocaleMessage(c))
+		return
+	}
+
+	c.Error(err)
+	c.AbortWithStatusJSON(http.StatusInternalServerError, WithContext(c, common.Error_INTERNAL_ERROR).With(err))
+}
+
+// AbortStatusError 用来处理多种内部错误和定制返回的 http status code.
+// 在复杂业务场景中,存在一个接口返回多种业务错误码情况,通过这个函数来统一处理
+func AbortStatusError(c *gin.Context, httpCode int, err error) {
+	if err == nil {
+		return
+	}
+
+	if e, ok := xerr.Cause(err).(*Error); ok {
+		c.Error(e)
+		c.AbortWithStatusJSON(httpCode, WithContext(c, e.err.Code))
+		return
+	}
+
+	c.Error(err)
+	c.AbortWithStatusJSON(httpCode, WithContext(c, common.Error_INTERNAL_ERROR).With(err))
+}
+
+// DefaultErrorStatusCode 返回错误码对应的默认 http status code
+func DefaultErrorStatusCode(code common.Error_Code) int {
+	if statusCode, ok := errorStatusCode[int(code)]; ok {
+		return statusCode
+	}
+
+	return http.StatusInternalServerError
+}
+
+// 错误对应默认返回的 http status code
+var errorStatusCode = map[int]int{
+	0:     http.StatusOK,
+	10000: http.StatusUnauthorized,
+	11000: http.StatusBadRequest,
+	11001: http.StatusBadRequest,
+	11002: http.StatusBadRequest,
+	11003: http.StatusTooManyRequests,
+	11100: http.StatusBadRequest,
+	11200: http.StatusBadRequest,
+	20000: http.StatusBadRequest,
+	21000: http.StatusBadRequest,
+	22000: http.StatusBadRequest,
+	23000: http.StatusBadRequest,
+	23001: http.StatusBadRequest,
+	24000: http.StatusBadRequest,
+	24100: http.StatusBadRequest,
+	24101: http.StatusBadRequest,
+	24102: http.StatusBadRequest,
+	24103: http.StatusBadRequest,
+	24104: http.StatusBadRequest,
+	24400: http.StatusBadRequest,
+	24500: http.StatusBadRequest,
+	24501: http.StatusBadRequest,
+	24502: http.StatusBadRequest,
+	24520: http.StatusBadRequest,
+	24521: http.StatusBadRequest,
+	24522: http.StatusBadRequest,
+	24523: http.StatusBadRequest,
+	24524: http.StatusBadRequest,
+	24525: http.StatusBadRequest,
+	24526: http.StatusBadRequest,
+	24527: http.StatusBadRequest,
+	24528: http.StatusBadRequest,
+	24600: http.StatusBadRequest,
+	24601: http.StatusBadRequest,
+	24602: http.StatusBadRequest,
+	24603: http.StatusBadRequest,
+	24700: http.StatusBadRequest,
+	24701: http.StatusBadRequest,
+	24702: http.StatusBadRequest,
+	24704: http.StatusBadRequest,
+	24705: http.StatusBadRequest,
+	24706: http.StatusBadRequest,
+	24707: http.StatusBadRequest,
+	24800: http.StatusBadRequest,
+	24801: http.StatusBadRequest,
+	24802: http.StatusBadRequest,
+	24803: http.StatusBadRequest,
+	90000: http.StatusInternalServerError,
+	90100: http.StatusBadRequest,
+	90101: http.StatusBadRequest,
+	90102: http.StatusBadRequest,
+	91000: http.StatusInternalServerError,
+}

+ 761 - 0
pkg/apierr/apierr_gen.go

@@ -0,0 +1,761 @@
+package apierr
+
+import (
+	common "kpt-tmr-group/proto/go/backend/common"
+
+	"github.com/gin-gonic/gin"
+)
+
+// NewErrUnauthorized create err with default message
+func NewErrUnauthorized(err ...error) *Error {
+	return New(common.Error_UNAUTHORIZED).With(err...)
+}
+
+// ErrUnauthorized create err with locales
+func ErrUnauthorized(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_UNAUTHORIZED).With(err...)
+}
+
+// AbortUnauthorized abort with status code and log common.Error_UNAUTHORIZED to newrelic
+func AbortUnauthorized(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrUnauthorized(c, err...))
+}
+
+// AbortStatusUnauthorized abort with status code and log common.Error_UNAUTHORIZED to newrelic
+func AbortStatusUnauthorized(c *gin.Context, err ...error) {
+	AbortUnauthorized(c, 401, err...)
+}
+
+// NewErrBadRequest create err with default message
+func NewErrBadRequest(err ...error) *Error {
+	return New(common.Error_BAD_REQUEST).With(err...)
+}
+
+// ErrBadRequest create err with locales
+func ErrBadRequest(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_BAD_REQUEST).With(err...)
+}
+
+// AbortBadRequest abort with status code and log common.Error_BAD_REQUEST to newrelic
+func AbortBadRequest(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrBadRequest(c, err...))
+}
+
+// AbortStatusBadRequest abort with status code and log common.Error_BAD_REQUEST to newrelic
+func AbortStatusBadRequest(c *gin.Context, err ...error) {
+	AbortBadRequest(c, 400, err...)
+}
+
+// NewErrInvalidContentType create err with default message
+func NewErrInvalidContentType(err ...error) *Error {
+	return New(common.Error_INVALID_CONTENT_TYPE).With(err...)
+}
+
+// ErrInvalidContentType create err with locales
+func ErrInvalidContentType(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_INVALID_CONTENT_TYPE).With(err...)
+}
+
+// AbortInvalidContentType abort with status code and log common.Error_INVALID_CONTENT_TYPE to newrelic
+func AbortInvalidContentType(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrInvalidContentType(c, err...))
+}
+
+// AbortStatusInvalidContentType abort with status code and log common.Error_INVALID_CONTENT_TYPE to newrelic
+func AbortStatusInvalidContentType(c *gin.Context, err ...error) {
+	AbortInvalidContentType(c, 400, err...)
+}
+
+// NewErrInvalidContentEncoding create err with default message
+func NewErrInvalidContentEncoding(err ...error) *Error {
+	return New(common.Error_INVALID_CONTENT_ENCODING).With(err...)
+}
+
+// ErrInvalidContentEncoding create err with locales
+func ErrInvalidContentEncoding(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_INVALID_CONTENT_ENCODING).With(err...)
+}
+
+// AbortInvalidContentEncoding abort with status code and log common.Error_INVALID_CONTENT_ENCODING to newrelic
+func AbortInvalidContentEncoding(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrInvalidContentEncoding(c, err...))
+}
+
+// AbortStatusInvalidContentEncoding abort with status code and log common.Error_INVALID_CONTENT_ENCODING to newrelic
+func AbortStatusInvalidContentEncoding(c *gin.Context, err ...error) {
+	AbortInvalidContentEncoding(c, 400, err...)
+}
+
+// NewErrTooManyRequests create err with default message
+func NewErrTooManyRequests(err ...error) *Error {
+	return New(common.Error_TOO_MANY_REQUESTS).With(err...)
+}
+
+// ErrTooManyRequests create err with locales
+func ErrTooManyRequests(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_TOO_MANY_REQUESTS).With(err...)
+}
+
+// AbortTooManyRequests abort with status code and log common.Error_TOO_MANY_REQUESTS to newrelic
+func AbortTooManyRequests(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrTooManyRequests(c, err...))
+}
+
+// AbortStatusTooManyRequests abort with status code and log common.Error_TOO_MANY_REQUESTS to newrelic
+func AbortStatusTooManyRequests(c *gin.Context, err ...error) {
+	AbortTooManyRequests(c, 429, err...)
+}
+
+// NewErrInvalidStorageType create err with default message
+func NewErrInvalidStorageType(err ...error) *Error {
+	return New(common.Error_INVALID_STORAGE_TYPE).With(err...)
+}
+
+// ErrInvalidStorageType create err with locales
+func ErrInvalidStorageType(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_INVALID_STORAGE_TYPE).With(err...)
+}
+
+// AbortInvalidStorageType abort with status code and log common.Error_INVALID_STORAGE_TYPE to newrelic
+func AbortInvalidStorageType(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrInvalidStorageType(c, err...))
+}
+
+// AbortStatusInvalidStorageType abort with status code and log common.Error_INVALID_STORAGE_TYPE to newrelic
+func AbortStatusInvalidStorageType(c *gin.Context, err ...error) {
+	AbortInvalidStorageType(c, 400, err...)
+}
+
+// NewErrInvalidDeData create err with default message
+func NewErrInvalidDeData(err ...error) *Error {
+	return New(common.Error_INVALID_DE_DATA).With(err...)
+}
+
+// ErrInvalidDeData create err with locales
+func ErrInvalidDeData(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_INVALID_DE_DATA).With(err...)
+}
+
+// AbortInvalidDeData abort with status code and log common.Error_INVALID_DE_DATA to newrelic
+func AbortInvalidDeData(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrInvalidDeData(c, err...))
+}
+
+// AbortStatusInvalidDeData abort with status code and log common.Error_INVALID_DE_DATA to newrelic
+func AbortStatusInvalidDeData(c *gin.Context, err ...error) {
+	AbortInvalidDeData(c, 400, err...)
+}
+
+// NewErrCheckinRepeated create err with default message
+func NewErrCheckinRepeated(err ...error) *Error {
+	return New(common.Error_CHECKIN_REPEATED).With(err...)
+}
+
+// ErrCheckinRepeated create err with locales
+func ErrCheckinRepeated(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_CHECKIN_REPEATED).With(err...)
+}
+
+// AbortCheckinRepeated abort with status code and log common.Error_CHECKIN_REPEATED to newrelic
+func AbortCheckinRepeated(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrCheckinRepeated(c, err...))
+}
+
+// AbortStatusCheckinRepeated abort with status code and log common.Error_CHECKIN_REPEATED to newrelic
+func AbortStatusCheckinRepeated(c *gin.Context, err ...error) {
+	AbortCheckinRepeated(c, 400, err...)
+}
+
+// NewErrCourseNotFound create err with default message
+func NewErrCourseNotFound(err ...error) *Error {
+	return New(common.Error_COURSE_NOT_FOUND).With(err...)
+}
+
+// ErrCourseNotFound create err with locales
+func ErrCourseNotFound(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_COURSE_NOT_FOUND).With(err...)
+}
+
+// AbortCourseNotFound abort with status code and log common.Error_COURSE_NOT_FOUND to newrelic
+func AbortCourseNotFound(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrCourseNotFound(c, err...))
+}
+
+// AbortStatusCourseNotFound abort with status code and log common.Error_COURSE_NOT_FOUND to newrelic
+func AbortStatusCourseNotFound(c *gin.Context, err ...error) {
+	AbortCourseNotFound(c, 400, err...)
+}
+
+// NewErrCourseNotInterest create err with default message
+func NewErrCourseNotInterest(err ...error) *Error {
+	return New(common.Error_COURSE_NOT_INTEREST).With(err...)
+}
+
+// ErrCourseNotInterest create err with locales
+func ErrCourseNotInterest(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_COURSE_NOT_INTEREST).With(err...)
+}
+
+// AbortCourseNotInterest abort with status code and log common.Error_COURSE_NOT_INTEREST to newrelic
+func AbortCourseNotInterest(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrCourseNotInterest(c, err...))
+}
+
+// AbortStatusCourseNotInterest abort with status code and log common.Error_COURSE_NOT_INTEREST to newrelic
+func AbortStatusCourseNotInterest(c *gin.Context, err ...error) {
+	AbortCourseNotInterest(c, 500, err...)
+}
+
+// NewErrModuleNotFound create err with default message
+func NewErrModuleNotFound(err ...error) *Error {
+	return New(common.Error_MODULE_NOT_FOUND).With(err...)
+}
+
+// ErrModuleNotFound create err with locales
+func ErrModuleNotFound(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_MODULE_NOT_FOUND).With(err...)
+}
+
+// AbortModuleNotFound abort with status code and log common.Error_MODULE_NOT_FOUND to newrelic
+func AbortModuleNotFound(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrModuleNotFound(c, err...))
+}
+
+// AbortStatusModuleNotFound abort with status code and log common.Error_MODULE_NOT_FOUND to newrelic
+func AbortStatusModuleNotFound(c *gin.Context, err ...error) {
+	AbortModuleNotFound(c, 400, err...)
+}
+
+// NewErrUserCourseAlreadyAdded create err with default message
+func NewErrUserCourseAlreadyAdded(err ...error) *Error {
+	return New(common.Error_USER_COURSE_ALREADY_ADDED).With(err...)
+}
+
+// ErrUserCourseAlreadyAdded create err with locales
+func ErrUserCourseAlreadyAdded(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_USER_COURSE_ALREADY_ADDED).With(err...)
+}
+
+// AbortUserCourseAlreadyAdded abort with status code and log common.Error_USER_COURSE_ALREADY_ADDED to newrelic
+func AbortUserCourseAlreadyAdded(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrUserCourseAlreadyAdded(c, err...))
+}
+
+// AbortStatusUserCourseAlreadyAdded abort with status code and log common.Error_USER_COURSE_ALREADY_ADDED to newrelic
+func AbortStatusUserCourseAlreadyAdded(c *gin.Context, err ...error) {
+	AbortUserCourseAlreadyAdded(c, 400, err...)
+}
+
+// NewErrUserCourseNotFound create err with default message
+func NewErrUserCourseNotFound(err ...error) *Error {
+	return New(common.Error_USER_COURSE_NOT_FOUND).With(err...)
+}
+
+// ErrUserCourseNotFound create err with locales
+func ErrUserCourseNotFound(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_USER_COURSE_NOT_FOUND).With(err...)
+}
+
+// AbortUserCourseNotFound abort with status code and log common.Error_USER_COURSE_NOT_FOUND to newrelic
+func AbortUserCourseNotFound(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrUserCourseNotFound(c, err...))
+}
+
+// AbortStatusUserCourseNotFound abort with status code and log common.Error_USER_COURSE_NOT_FOUND to newrelic
+func AbortStatusUserCourseNotFound(c *gin.Context, err ...error) {
+	AbortUserCourseNotFound(c, 400, err...)
+}
+
+// NewErrPtLimited create err with default message
+func NewErrPtLimited(err ...error) *Error {
+	return New(common.Error_PT_LIMITED).With(err...)
+}
+
+// ErrPtLimited create err with locales
+func ErrPtLimited(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_PT_LIMITED).With(err...)
+}
+
+// AbortPtLimited abort with status code and log common.Error_PT_LIMITED to newrelic
+func AbortPtLimited(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrPtLimited(c, err...))
+}
+
+// AbortStatusPtLimited abort with status code and log common.Error_PT_LIMITED to newrelic
+func AbortStatusPtLimited(c *gin.Context, err ...error) {
+	AbortPtLimited(c, 400, err...)
+}
+
+// NewErrInvalidPrice create err with default message
+func NewErrInvalidPrice(err ...error) *Error {
+	return New(common.Error_INVALID_PRICE).With(err...)
+}
+
+// ErrInvalidPrice create err with locales
+func ErrInvalidPrice(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_INVALID_PRICE).With(err...)
+}
+
+// AbortInvalidPrice abort with status code and log common.Error_INVALID_PRICE to newrelic
+func AbortInvalidPrice(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrInvalidPrice(c, err...))
+}
+
+// AbortStatusInvalidPrice abort with status code and log common.Error_INVALID_PRICE to newrelic
+func AbortStatusInvalidPrice(c *gin.Context, err ...error) {
+	AbortInvalidPrice(c, 400, err...)
+}
+
+// NewErrInvalidProductId create err with default message
+func NewErrInvalidProductId(err ...error) *Error {
+	return New(common.Error_INVALID_PRODUCT_ID).With(err...)
+}
+
+// ErrInvalidProductId create err with locales
+func ErrInvalidProductId(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_INVALID_PRODUCT_ID).With(err...)
+}
+
+// AbortInvalidProductId abort with status code and log common.Error_INVALID_PRODUCT_ID to newrelic
+func AbortInvalidProductId(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrInvalidProductId(c, err...))
+}
+
+// AbortStatusInvalidProductId abort with status code and log common.Error_INVALID_PRODUCT_ID to newrelic
+func AbortStatusInvalidProductId(c *gin.Context, err ...error) {
+	AbortInvalidProductId(c, 400, err...)
+}
+
+// NewErrInvalidOrderNumber create err with default message
+func NewErrInvalidOrderNumber(err ...error) *Error {
+	return New(common.Error_INVALID_ORDER_NUMBER).With(err...)
+}
+
+// ErrInvalidOrderNumber create err with locales
+func ErrInvalidOrderNumber(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_INVALID_ORDER_NUMBER).With(err...)
+}
+
+// AbortInvalidOrderNumber abort with status code and log common.Error_INVALID_ORDER_NUMBER to newrelic
+func AbortInvalidOrderNumber(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrInvalidOrderNumber(c, err...))
+}
+
+// AbortStatusInvalidOrderNumber abort with status code and log common.Error_INVALID_ORDER_NUMBER to newrelic
+func AbortStatusInvalidOrderNumber(c *gin.Context, err ...error) {
+	AbortInvalidOrderNumber(c, 400, err...)
+}
+
+// NewErrInvalidUserId create err with default message
+func NewErrInvalidUserId(err ...error) *Error {
+	return New(common.Error_INVALID_USER_ID).With(err...)
+}
+
+// ErrInvalidUserId create err with locales
+func ErrInvalidUserId(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_INVALID_USER_ID).With(err...)
+}
+
+// AbortInvalidUserId abort with status code and log common.Error_INVALID_USER_ID to newrelic
+func AbortInvalidUserId(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrInvalidUserId(c, err...))
+}
+
+// AbortStatusInvalidUserId abort with status code and log common.Error_INVALID_USER_ID to newrelic
+func AbortStatusInvalidUserId(c *gin.Context, err ...error) {
+	AbortInvalidUserId(c, 400, err...)
+}
+
+// NewErrInvalidReceipt create err with default message
+func NewErrInvalidReceipt(err ...error) *Error {
+	return New(common.Error_INVALID_RECEIPT).With(err...)
+}
+
+// ErrInvalidReceipt create err with locales
+func ErrInvalidReceipt(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_INVALID_RECEIPT).With(err...)
+}
+
+// AbortInvalidReceipt abort with status code and log common.Error_INVALID_RECEIPT to newrelic
+func AbortInvalidReceipt(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrInvalidReceipt(c, err...))
+}
+
+// AbortStatusInvalidReceipt abort with status code and log common.Error_INVALID_RECEIPT to newrelic
+func AbortStatusInvalidReceipt(c *gin.Context, err ...error) {
+	AbortInvalidReceipt(c, 400, err...)
+}
+
+// NewErrEmptyIosReceipt create err with default message
+func NewErrEmptyIosReceipt(err ...error) *Error {
+	return New(common.Error_EMPTY_IOS_RECEIPT).With(err...)
+}
+
+// ErrEmptyIosReceipt create err with locales
+func ErrEmptyIosReceipt(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_EMPTY_IOS_RECEIPT).With(err...)
+}
+
+// AbortEmptyIosReceipt abort with status code and log common.Error_EMPTY_IOS_RECEIPT to newrelic
+func AbortEmptyIosReceipt(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrEmptyIosReceipt(c, err...))
+}
+
+// AbortStatusEmptyIosReceipt abort with status code and log common.Error_EMPTY_IOS_RECEIPT to newrelic
+func AbortStatusEmptyIosReceipt(c *gin.Context, err ...error) {
+	AbortEmptyIosReceipt(c, 500, err...)
+}
+
+// NewErrUserPlanLimitedCourseCount create err with default message
+func NewErrUserPlanLimitedCourseCount(err ...error) *Error {
+	return New(common.Error_USER_PLAN_LIMITED_COURSE_COUNT).With(err...)
+}
+
+// ErrUserPlanLimitedCourseCount create err with locales
+func ErrUserPlanLimitedCourseCount(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_USER_PLAN_LIMITED_COURSE_COUNT).With(err...)
+}
+
+// AbortUserPlanLimitedCourseCount abort with status code and log common.Error_USER_PLAN_LIMITED_COURSE_COUNT to newrelic
+func AbortUserPlanLimitedCourseCount(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrUserPlanLimitedCourseCount(c, err...))
+}
+
+// AbortStatusUserPlanLimitedCourseCount abort with status code and log common.Error_USER_PLAN_LIMITED_COURSE_COUNT to newrelic
+func AbortStatusUserPlanLimitedCourseCount(c *gin.Context, err ...error) {
+	AbortUserPlanLimitedCourseCount(c, 500, err...)
+}
+
+// NewErrInternalError create err with default message
+func NewErrInternalError(err ...error) *Error {
+	return New(common.Error_INTERNAL_ERROR).With(err...)
+}
+
+// ErrInternalError create err with locales
+func ErrInternalError(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_INTERNAL_ERROR).With(err...)
+}
+
+// AbortInternalError abort with status code and log common.Error_INTERNAL_ERROR to newrelic
+func AbortInternalError(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrInternalError(c, err...))
+}
+
+// AbortStatusInternalError abort with status code and log common.Error_INTERNAL_ERROR to newrelic
+func AbortStatusInternalError(c *gin.Context, err ...error) {
+	AbortInternalError(c, 500, err...)
+}
+
+// NewErrJsonpbError create err with default message
+func NewErrJsonpbError(err ...error) *Error {
+	return New(common.Error_JSONPB_ERROR).With(err...)
+}
+
+// ErrJsonpbError create err with locales
+func ErrJsonpbError(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_JSONPB_ERROR).With(err...)
+}
+
+// AbortJsonpbError abort with status code and log common.Error_JSONPB_ERROR to newrelic
+func AbortJsonpbError(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrJsonpbError(c, err...))
+}
+
+// AbortStatusJsonpbError abort with status code and log common.Error_JSONPB_ERROR to newrelic
+func AbortStatusJsonpbError(c *gin.Context, err ...error) {
+	AbortJsonpbError(c, 400, err...)
+}
+
+// NewErrJsonError create err with default message
+func NewErrJsonError(err ...error) *Error {
+	return New(common.Error_JSON_ERROR).With(err...)
+}
+
+// ErrJsonError create err with locales
+func ErrJsonError(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_JSON_ERROR).With(err...)
+}
+
+// AbortJsonError abort with status code and log common.Error_JSON_ERROR to newrelic
+func AbortJsonError(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrJsonError(c, err...))
+}
+
+// AbortStatusJsonError abort with status code and log common.Error_JSON_ERROR to newrelic
+func AbortStatusJsonError(c *gin.Context, err ...error) {
+	AbortJsonError(c, 400, err...)
+}
+
+// NewErrPbError create err with default message
+func NewErrPbError(err ...error) *Error {
+	return New(common.Error_PB_ERROR).With(err...)
+}
+
+// ErrPbError create err with locales
+func ErrPbError(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_PB_ERROR).With(err...)
+}
+
+// AbortPbError abort with status code and log common.Error_PB_ERROR to newrelic
+func AbortPbError(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrPbError(c, err...))
+}
+
+// AbortStatusPbError abort with status code and log common.Error_PB_ERROR to newrelic
+func AbortStatusPbError(c *gin.Context, err ...error) {
+	AbortPbError(c, 400, err...)
+}
+
+// NewErrExternalError create err with default message
+func NewErrExternalError(err ...error) *Error {
+	return New(common.Error_EXTERNAL_ERROR).With(err...)
+}
+
+// ErrExternalError create err with locales
+func ErrExternalError(c *gin.Context, err ...error) *Error {
+	return WithContext(c, common.Error_EXTERNAL_ERROR).With(err...)
+}
+
+// AbortExternalError abort with status code and log common.Error_EXTERNAL_ERROR to newrelic
+func AbortExternalError(c *gin.Context, code int, err ...error) {
+	for _, e := range err {
+		// if err is nil, gin will panic
+		if e != nil {
+			// so many 4xx error in new relic, only ignore 400/401/404 error, have fun!
+			if !shouldIgnoreCode(code) {
+				c.Error(e)
+			}
+		}
+	}
+	c.AbortWithStatusJSON(code, ErrExternalError(c, err...))
+}
+
+// AbortStatusExternalError abort with status code and log common.Error_EXTERNAL_ERROR to newrelic
+func AbortStatusExternalError(c *gin.Context, err ...error) {
+	AbortExternalError(c, 500, err...)
+}

+ 47 - 0
pkg/apierr/config.go

@@ -0,0 +1,47 @@
+package apierr
+
+import (
+	"net/http"
+	"sort"
+
+	"github.com/gin-gonic/gin"
+
+	"kpt-tmr-group/pkg/grpcutil"
+	"kpt-tmr-group/pkg/xerr"
+)
+
+var (
+	ignoreCodes = []int{400, 401, 404, 429}
+	codesSize   = int(3)
+)
+
+// IgnoreStatusCodes ignore error with status codes
+func IgnoreStatusCodes(codes ...int) {
+	sort.Ints(codes)
+
+	ignoreCodes = codes
+	codesSize = len(codes)
+}
+
+func shouldIgnoreCode(code int) bool {
+	// SearchInts using binary search, more fast
+	idx := sort.SearchInts(ignoreCodes, code)
+	return idx < codesSize && ignoreCodes[idx] == code
+}
+
+// ClassifiedAbort classify base on error type
+func ClassifiedAbort(c *gin.Context, err error) {
+	if err == nil {
+		return
+	}
+
+	if e, isc := xerr.IsCustomError(err); isc {
+		// 服务内部自定义错误
+		AbortBadRequest(c, http.StatusBadRequest, e)
+	} else if !grpcutil.FilterServerError(err) {
+		// gRPC 部分错误
+		AbortBadRequest(c, http.StatusBadRequest, err)
+	} else {
+		AbortError(c, err)
+	}
+}

+ 23 - 0
pkg/apiok/common.go

@@ -0,0 +1,23 @@
+package apiok
+
+/*type OkResponse struct {
+	Code int         `json:"code"`
+	Msg  string      `json:"msg"`
+	Data interface{} `json:"data"`
+}
+
+func CommonResponse(data interface{}) *OkResponse {
+	return &OkResponse{
+		Code: 200,
+		Msg:  "ok",
+		Data: data,
+	}
+}
+*/
+type ApiOk struct {
+	Success bool `json:"success"`
+}
+
+func NewApiOk(success bool) *ApiOk {
+	return &ApiOk{Success: success}
+}

+ 5 - 5
util/cleanup/entry.go → pkg/cleanup/entry.go

@@ -23,8 +23,8 @@ import (
 	"reflect"
 	"sync"
 
-	log "kpt-tmr-group/util/logger"
-	"kpt-tmr-group/util/xerr"
+	"kpt-tmr-group/pkg/logger/logrus"
+	"kpt-tmr-group/pkg/xerr"
 )
 
 // entry global cleanup entry
@@ -48,7 +48,7 @@ type Entry struct {
 
 // Run runs all the cleanup functions registered.
 func (entry *Entry) Run() {
-	log.Infof("cleanup: performing %d cleanups", len(entry.fns))
+	logrus.Infof("cleanup: performing %d cleanups", len(entry.fns))
 
 	entry.once.Do(func() {
 		for _, f := range entry.fns {
@@ -56,7 +56,7 @@ func (entry *Entry) Run() {
 		}
 	})
 
-	log.Infof("cleanup: all done")
+	logrus.Infof("cleanup: all done")
 }
 
 // Register adds a function to the cleanup queue.
@@ -92,7 +92,7 @@ func (entry *Entry) RegisterStruct(ctor interface{}) error {
 		}
 		if method.IsValid() {
 			if err := entry.RegisterFunc(method.Interface()); err != nil {
-				log.WithError(err).WithField("fieldName", field.Type().Name()).Error("register func failed")
+				logrus.WithError(err).WithField("fieldName", field.Type().Name()).Error("register func failed")
 			}
 		}
 	}

+ 0 - 0
util/cleanup/entry_test.go → pkg/cleanup/entry_test.go


+ 0 - 0
util/cputil/cp.go → pkg/cputil/cp.go


+ 0 - 0
util/cputil/cp_test.go → pkg/cputil/cp_test.go


+ 0 - 0
util/di/annotation.go → pkg/di/annotation.go


+ 3 - 3
util/di/hub.go → pkg/di/hub.go

@@ -3,9 +3,9 @@ package di
 import (
 	"reflect"
 
-	"kpt-tmr-group/util/cleanup"
-	"kpt-tmr-group/util/di/xreflect"
-	"kpt-tmr-group/util/xerr"
+	"kpt-tmr-group/pkg/cleanup"
+	"kpt-tmr-group/pkg/di/xreflect"
+	"kpt-tmr-group/pkg/xerr"
 
 	"go.uber.org/dig"
 )

+ 0 - 0
util/di/hub_test.go → pkg/di/hub_test.go


+ 1 - 1
util/di/option.go → pkg/di/option.go

@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"strings"
 
-	"kpt-tmr-group/util/di/xreflect"
+	"kpt-tmr-group/pkg/di/xreflect"
 
 	"go.uber.org/dig"
 )

+ 4 - 4
util/di/xreflect/reflect.go → pkg/di/xreflect/reflect.go

@@ -144,12 +144,12 @@ func shouldIgnoreFrame(f Frame) bool {
 
 	// The unique, fully-qualified name for all functions begins with
 	// "{{importPath}}.". We'll ignore di and its subpackages.
-	s := strings.TrimPrefix(f.Function, "git.llsapp.com/zhenghe/pkg/di")
+	s := strings.TrimPrefix(f.Function, "kpt-tmr-group/pkg/di")
 	if len(s) > 0 && s[0] == '.' || s[0] == '/' {
 		// We want to match,
-		//   git.llsapp.com/zhenghe/pkg/di.Foo
-		//   git.llsapp.com/zhenghe/pkg/di/something.Foo
-		// But not, git.llsapp.com/zhenghe/pkg/difoo
+		//   kpt-tmr-group/pkg/di.Foo
+		//   kpt-tmr-group/pkg/di/something.Foo
+		// But not, kpt-tmr-group/pkg/difoo
 		return true
 	}
 

+ 2 - 2
util/di/xreflect/reflect_test.go → pkg/di/xreflect/reflect_test.go

@@ -83,7 +83,7 @@ func TestReturnTypes(t *testing.T) {
 }
 
 func TestCaller(t *testing.T) {
-	assert.Equal(t, "git.llsapp.com/zhenghe/pkg/di/xreflect.TestCaller", Caller())
+	assert.Equal(t, "kpt-tmr-group/pkg/di/xreflect.TestCaller", Caller())
 }
 
 func someFunc() {}
@@ -97,7 +97,7 @@ func TestFuncName(t *testing.T) {
 		{
 			desc: "function",
 			give: someFunc,
-			want: "git.llsapp.com/zhenghe/pkg/di/xreflect.someFunc()",
+			want: "kpt-tmr-group/pkg/di/xreflect.someFunc()",
 		},
 		{
 			desc: "not a function",

+ 0 - 0
util/di/xreflect/stack.go → pkg/di/xreflect/stack.go


+ 11 - 11
util/di/xreflect/stack_test.go → pkg/di/xreflect/stack_test.go

@@ -38,7 +38,7 @@ func TestStack(t *testing.T) {
 		frames := CallerStack(0, 0)
 		require.NotEmpty(t, frames)
 		f := frames[0]
-		assert.Equal(t, "git.llsapp.com/zhenghe/pkg/di/xreflect.TestStack.func1", f.Function)
+		assert.Equal(t, "kpt-tmr-group/pkg/di/xreflect.TestStack.func1", f.Function)
 		assert.Contains(t, f.File, "xreflect/stack_test.go")
 		assert.NotZero(t, f.Line)
 	})
@@ -54,7 +54,7 @@ func TestStack(t *testing.T) {
 		require.True(t, len(frames) > 3, "expected at least three frames")
 		for i, name := range []string{"func2.1.1", "func2.1", "func2"} {
 			f := frames[i]
-			assert.Equal(t, "git.llsapp.com/zhenghe/pkg/di/xreflect.TestStack."+name, f.Function)
+			assert.Equal(t, "kpt-tmr-group/pkg/di/xreflect.TestStack."+name, f.Function)
 			assert.Contains(t, f.File, "xreflect/stack_test.go")
 			assert.NotZero(t, f.Line)
 		}
@@ -70,7 +70,7 @@ func TestStack(t *testing.T) {
 
 		require.NotEmpty(t, frames)
 		f := frames[0]
-		assert.Equal(t, "git.llsapp.com/zhenghe/pkg/di/xreflect.TestStack.func3", f.Function)
+		assert.Equal(t, "kpt-tmr-group/pkg/di/xreflect.TestStack.func3", f.Function)
 		assert.Contains(t, f.File, "xreflect/stack_test.go")
 		assert.NotZero(t, f.Line)
 	})
@@ -87,8 +87,8 @@ func TestStackCallerName(t *testing.T) {
 			desc: "skip di components",
 			give: Stack{
 				{
-					Function: "git.llsapp.com/zhenghe/pkg/di.Foo()",
-					File:     "git.llsapp.com/zhenghe/pkg/di/foo.go",
+					Function: "kpt-tmr-group/pkg/di.Foo()",
+					File:     "kpt-tmr-group/pkg/di/foo.go",
 				},
 				{
 					Function: "foo/bar.Baz()",
@@ -101,7 +101,7 @@ func TestStackCallerName(t *testing.T) {
 			desc: "skip di in wrong directory",
 			give: Stack{
 				{
-					Function: "git.llsapp.com/zhenghe/pkg/di/di.Foo()",
+					Function: "kpt-tmr-group/pkg/di/di.Foo()",
 					File:     "di/foo.go",
 				},
 				{
@@ -115,7 +115,7 @@ func TestStackCallerName(t *testing.T) {
 			desc: "skip di subpackage",
 			give: Stack{
 				{
-					Function: "git.llsapp.com/zhenghe/pkg/di/xreflect.Foo()",
+					Function: "kpt-tmr-group/pkg/di/xreflect.Foo()",
 					File:     "di/internal/xreflect/foo.go",
 				},
 				{
@@ -130,7 +130,7 @@ func TestStackCallerName(t *testing.T) {
 			give: Stack{
 				{
 					Function: "some/thing.Foo()",
-					File:     "git.llsapp.com/zhenghe/pkg/di/foo_test.go",
+					File:     "kpt-tmr-group/pkg/di/foo_test.go",
 				},
 			},
 			want: "some/thing.Foo()",
@@ -139,11 +139,11 @@ func TestStackCallerName(t *testing.T) {
 			desc: "don't skip di prefix",
 			give: Stack{
 				{
-					Function: "git.llsapp.com/zhenghe/pkg/difoo.Bar()",
-					File:     "git.llsapp.com/zhenghe/pkg/difoo/bar.go",
+					Function: "kpt-tmr-group/pkg/difoo.Bar()",
+					File:     "kpt-tmr-group/pkg/difoo/bar.go",
 				},
 			},
-			want: "git.llsapp.com/zhenghe/pkg/difoo.Bar()",
+			want: "kpt-tmr-group/pkg/difoo.Bar()",
 		},
 	}
 

+ 68 - 0
pkg/ginutil/bind.go

@@ -0,0 +1,68 @@
+package ginutil
+
+import (
+	"net/http"
+	"reflect"
+
+	"kpt-tmr-group/pkg/jsonpb"
+	"kpt-tmr-group/pkg/xerr"
+
+	"github.com/gin-gonic/gin"
+	"github.com/gin-gonic/gin/binding"
+	"github.com/golang/protobuf/proto"
+	"github.com/huandu/xstrings"
+)
+
+var camelQuery = &CamelQueryBinding{}
+
+// BindQuery with query params
+func BindQuery(c *gin.Context, obj interface{}) error {
+	return c.ShouldBindWith(obj, camelQuery)
+}
+
+type CamelQueryBinding struct{}
+
+func (*CamelQueryBinding) Name() string {
+	return "camel_query"
+}
+
+func (*CamelQueryBinding) Bind(req *http.Request, obj interface{}) error {
+	values := req.URL.Query()
+	if err := mapFormByTag(obj, values, "json"); err != nil {
+		return err
+	}
+	return binding.Validator.ValidateStruct(obj)
+}
+
+type camelFormSource map[string][]string
+
+// TrySet tries to set a value by request's form source (like map[string][]string)
+func (form camelFormSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
+	return setByForm(value, field, form, xstrings.FirstRuneToLower(xstrings.ToCamelCase(tagValue)), opt)
+}
+
+// BindProtoMessage with proto message from json body
+func BindProtoMessage(c *gin.Context, obj proto.Message) error {
+	return c.ShouldBindWith(obj, protoMessageBindingFromBody)
+}
+
+var protoMessageBindingFromBody = &ProtoMessageBinding{}
+
+type ProtoMessageBinding struct{}
+
+func (*ProtoMessageBinding) Name() string {
+	return "proto_message_binding"
+}
+
+func (*ProtoMessageBinding) Bind(req *http.Request, obj interface{}) error {
+	pbMessage, ok := obj.(proto.Message)
+	if !ok {
+		return xerr.New("bind obj should be proto.Message")
+	}
+
+	if err := jsonpb.Unmarshal(req.Body, pbMessage); err != nil {
+		return xerr.WithStack(err)
+	}
+
+	return binding.Validator.ValidateStruct(obj)
+}

+ 34 - 0
pkg/ginutil/bind_proto.go

@@ -0,0 +1,34 @@
+package ginutil
+
+import (
+	"net/http"
+
+	"kpt-tmr-group/pkg/jsonpb"
+	"kpt-tmr-group/pkg/xerr"
+
+	"github.com/gin-gonic/gin"
+	"github.com/golang/protobuf/proto"
+)
+
+type ProtoMessageQueryBinding struct{}
+
+func BindQueryProto(c *gin.Context, pb proto.Message) error {
+	values := c.Request.URL.Query()
+	if err := jsonpb.UnmarshalQuery(values, pb); err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+func BindProto(c *gin.Context, pb proto.Message) (err error) {
+	switch c.Request.Method {
+	case http.MethodGet, http.MethodDelete:
+		err = BindQueryProto(c, pb)
+	default:
+		err = BindProtoMessage(c, pb)
+	}
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+	return
+}

+ 19 - 0
pkg/ginutil/json_proto_response.go

@@ -0,0 +1,19 @@
+package ginutil
+
+import (
+	"kpt-tmr-group/pkg/apierr"
+	"kpt-tmr-group/pkg/jsonpb"
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+	"github.com/golang/protobuf/proto"
+)
+
+func JSONResp(c *gin.Context, pb proto.Message) {
+	bs, err := jsonpb.MarshalBytes(pb)
+	if err != nil {
+		apierr.AbortInternalError(c, http.StatusInternalServerError, err)
+		return
+	}
+	c.Data(http.StatusOK, "application/json", bs)
+}

+ 322 - 0
pkg/ginutil/setter.go

@@ -0,0 +1,322 @@
+package ginutil
+
+import (
+	"encoding/json"
+	"errors"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+
+	"kpt-tmr-group/pkg/xerr"
+)
+
+var _ setter = camelFormSource(nil)
+
+var emptyField = reflect.StructField{}
+
+// setter tries to set value on a walking by fields of a struct
+type setter interface {
+	TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error)
+}
+
+type setOptions struct {
+	isDefaultExists bool
+	defaultValue    string
+}
+
+func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
+	return mappingByPtr(ptr, camelFormSource(form), tag)
+}
+
+func mappingByPtr(ptr interface{}, setter setter, tag string) error {
+	_, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
+	return err
+}
+
+func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
+	var vKind = value.Kind()
+
+	if vKind == reflect.Ptr {
+		var isNew bool
+		vPtr := value
+		if value.IsNil() {
+			isNew = true
+			vPtr = reflect.New(value.Type().Elem())
+		}
+		isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
+		if err != nil {
+			return false, err
+		}
+		if isNew && isSetted {
+			value.Set(vPtr)
+		}
+		return isSetted, nil
+	}
+
+	ok, err := tryToSetValue(value, field, setter, tag)
+	if err != nil {
+		return false, err
+	}
+	if ok {
+		return true, nil
+	}
+
+	if vKind == reflect.Struct {
+		tValue := value.Type()
+
+		var isSetted bool
+		for i := 0; i < value.NumField(); i++ {
+			if !value.Field(i).CanSet() {
+				continue
+			}
+			ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
+			if err != nil {
+				return false, err
+			}
+			isSetted = isSetted || ok
+		}
+		return isSetted, nil
+	}
+	return false, nil
+}
+
+func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
+	var tagValue string
+	var setOpt setOptions
+
+	tagValue = field.Tag.Get(tag)
+	tagValue, opts := head(tagValue, ",")
+
+	if tagValue == "-" { // just ignoring this field
+		return false, nil
+	}
+	if tagValue == "" { // when field is "emptyField" variable
+		return false, nil
+	}
+
+	var opt string
+	for len(opts) > 0 {
+		opt, opts = head(opts, ",")
+
+		k, v := head(opt, "=")
+		switch k {
+		case "default":
+			setOpt.isDefaultExists = true
+			setOpt.defaultValue = v
+		}
+	}
+
+	return setter.TrySet(value, field, tagValue, setOpt)
+}
+
+func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) {
+	vs, ok := form[tagValue]
+	if !ok && !opt.isDefaultExists {
+		return false, nil
+	}
+
+	switch value.Kind() {
+	case reflect.Slice:
+		if !ok {
+			vs = []string{opt.defaultValue}
+		}
+		return true, setSlice(vs, value, field)
+	case reflect.Array:
+		if !ok {
+			vs = []string{opt.defaultValue}
+		}
+		if len(vs) != value.Len() {
+			return false, xerr.Errorf("%q is not valid value for %s", vs, value.Type().String())
+		}
+		return true, setArray(vs, value, field)
+	default:
+		var val string
+		if !ok {
+			val = opt.defaultValue
+		}
+
+		if len(vs) > 0 {
+			val = vs[0]
+		}
+		return true, setWithProperType(val, value, field)
+	}
+}
+
+var errUnknownType = errors.New("unknown type")
+
+func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
+	switch value.Kind() {
+	case reflect.Int:
+		return setIntField(val, 0, value)
+	case reflect.Int8:
+		return setIntField(val, 8, value)
+	case reflect.Int16:
+		return setIntField(val, 16, value)
+	case reflect.Int32:
+		return setIntField(val, 32, value)
+	case reflect.Int64:
+		switch value.Interface().(type) {
+		case time.Duration:
+			return setTimeDuration(val, value, field)
+		}
+		return setIntField(val, 64, value)
+	case reflect.Uint:
+		return setUintField(val, 0, value)
+	case reflect.Uint8:
+		return setUintField(val, 8, value)
+	case reflect.Uint16:
+		return setUintField(val, 16, value)
+	case reflect.Uint32:
+		return setUintField(val, 32, value)
+	case reflect.Uint64:
+		return setUintField(val, 64, value)
+	case reflect.Bool:
+		return setBoolField(val, value)
+	case reflect.Float32:
+		return setFloatField(val, 32, value)
+	case reflect.Float64:
+		return setFloatField(val, 64, value)
+	case reflect.String:
+		value.SetString(val)
+	case reflect.Struct:
+		switch value.Interface().(type) {
+		case time.Time:
+			return setTimeField(val, field, value)
+		}
+		return json.Unmarshal([]byte(val), value.Addr().Interface())
+	case reflect.Map:
+		return json.Unmarshal([]byte(val), value.Addr().Interface())
+	default:
+		return errUnknownType
+	}
+	return nil
+}
+
+func setIntField(val string, bitSize int, field reflect.Value) error {
+	if val == "" {
+		val = "0"
+	}
+	intVal, err := strconv.ParseInt(val, 10, bitSize)
+	if err == nil {
+		field.SetInt(intVal)
+	}
+	return xerr.WithStack(err)
+}
+
+func setUintField(val string, bitSize int, field reflect.Value) error {
+	if val == "" {
+		val = "0"
+	}
+	uintVal, err := strconv.ParseUint(val, 10, bitSize)
+	if err == nil {
+		field.SetUint(uintVal)
+	}
+	return xerr.WithStack(err)
+}
+
+func setBoolField(val string, field reflect.Value) error {
+	if val == "" {
+		val = "false"
+	}
+	boolVal, err := strconv.ParseBool(val)
+	if err == nil {
+		field.SetBool(boolVal)
+	}
+	return xerr.WithStack(err)
+}
+
+func setFloatField(val string, bitSize int, field reflect.Value) error {
+	if val == "" {
+		val = "0.0"
+	}
+	floatVal, err := strconv.ParseFloat(val, bitSize)
+	if err == nil {
+		field.SetFloat(floatVal)
+	}
+	return xerr.WithStack(err)
+}
+
+func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
+	timeFormat := structField.Tag.Get("time_format")
+	if timeFormat == "" {
+		timeFormat = time.RFC3339
+	}
+
+	if val == "" {
+		value.Set(reflect.ValueOf(time.Time{}))
+		return nil
+	}
+
+	l := time.Local
+	if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
+		l = time.UTC
+	}
+
+	if locTag := structField.Tag.Get("time_location"); locTag != "" {
+		loc, err := time.LoadLocation(locTag)
+		if err != nil {
+			return xerr.WithStack(err)
+		}
+		l = loc
+	}
+
+	var (
+		t   time.Time
+		err error
+	)
+
+	if isTS, _ := strconv.ParseBool(structField.Tag.Get("time_ts")); isTS {
+		sec, e := strconv.ParseInt(val, 10, 64)
+		if e != nil {
+			return xerr.WithStack(e)
+		}
+		t = time.Unix(sec, 0).In(l)
+	} else {
+		t, err = time.ParseInLocation(timeFormat, val, l)
+	}
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+
+	value.Set(reflect.ValueOf(t))
+	return nil
+}
+
+// TODO: should support array case. eg. myArray[]=a&myArray[]=b
+func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
+	for i, s := range vals {
+		err := setWithProperType(s, value.Index(i), field)
+		if err != nil {
+			return xerr.WithStack(err)
+		}
+	}
+	return nil
+}
+
+func setSlice(vals []string, value reflect.Value, field reflect.StructField) error {
+	slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
+	err := setArray(vals, slice, field)
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+	value.Set(slice)
+	return nil
+}
+
+func setTimeDuration(val string, value reflect.Value, _ reflect.StructField) error {
+	d, err := time.ParseDuration(val)
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+	value.Set(reflect.ValueOf(d))
+	return nil
+}
+
+func head(str, sep string) (head string, tail string) {
+	idx := strings.Index(str, sep)
+	if idx < 0 {
+		return str, ""
+	}
+	return str[:idx], str[idx+len(sep):]
+}

+ 57 - 0
pkg/grpcutil/client_conn.go

@@ -0,0 +1,57 @@
+package grpcutil
+
+import (
+	"context"
+	"time"
+
+	"kpt-tmr-group/pkg/xerr"
+
+	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
+	grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
+	grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+)
+
+func DialContext(ctx context.Context, target string, options ...grpc.DialOption) (*grpc.ClientConn, error) {
+	if len(options) == 0 {
+		options = DefaultDialOptions()
+	}
+
+	conn, err := grpc.DialContext(ctx, target, options...)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return conn, err
+}
+
+func DefaultDialOptions() []grpc.DialOption {
+	options := DefaultAsyncDialOptions()
+	return append(options, grpc.WithBlock())
+}
+
+func DefaultAsyncDialOptions() []grpc.DialOption {
+	unaryInterceptors := []grpc.UnaryClientInterceptor{
+		grpc_retry.UnaryClientInterceptor(
+			grpc_retry.WithMax(3),
+			grpc_retry.WithCodes(codes.Aborted, codes.DeadlineExceeded),
+			grpc_retry.WithPerRetryTimeout(time.Millisecond*500),
+		),
+		grpc_prometheus.UnaryClientInterceptor,
+	}
+	streamInterceptors := []grpc.StreamClientInterceptor{
+		grpc_retry.StreamClientInterceptor(
+			grpc_retry.WithMax(3),
+			grpc_retry.WithCodes(codes.Aborted, codes.DeadlineExceeded),
+			grpc_retry.WithPerRetryTimeout(time.Millisecond*500),
+		),
+		grpc_prometheus.StreamClientInterceptor,
+	}
+
+	return []grpc.DialOption{
+		grpc.WithInsecure(),
+		grpc.WithUnaryInterceptor(grpc_middleware.ChainUnaryClient(unaryInterceptors...)),
+		grpc.WithStreamInterceptor(grpc_middleware.ChainStreamClient(streamInterceptors...)),
+	}
+}

+ 61 - 0
pkg/grpcutil/error.go

@@ -0,0 +1,61 @@
+package grpcutil
+
+import (
+	"errors"
+	"reflect"
+
+	"kpt-tmr-group/pkg/xerr"
+
+	spb "google.golang.org/genproto/googleapis/rpc/status"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+)
+
+// StatusError cause of grpc status error
+func StatusError(err error) (*status.Status, bool) {
+	statusErr, ok := status.FromError(xerr.Cause(err))
+	if !ok {
+		return nil, false
+	}
+
+	return statusErr, true
+}
+
+// FilterServerError 优化服务端错误上报逻辑
+func FilterServerError(err error) bool {
+	statusErr, ok := StatusError(err)
+	if !ok {
+		return true
+	}
+
+	details := statusErr.Details()
+	for _, detail := range details {
+		if _, ok := detail.(*spb.Status); ok && statusErr.Code() != codes.Canceled {
+			return false
+		}
+	}
+
+	ignoreStatus := []codes.Code{codes.InvalidArgument, codes.Unauthenticated, codes.NotFound, codes.FailedPrecondition, codes.AlreadyExists}
+	if inArray, _ := In(statusErr.Code(), ignoreStatus); inArray {
+		return false
+	}
+	return true
+}
+
+func In(obj interface{}, target interface{}) (bool, error) {
+	targetValue := reflect.ValueOf(target)
+	switch reflect.TypeOf(target).Kind() {
+	case reflect.Slice, reflect.Array:
+		for i := 0; i < targetValue.Len(); i++ {
+			if targetValue.Index(i).Interface() == obj {
+				return true, nil
+			}
+		}
+	case reflect.Map:
+		if targetValue.MapIndex(reflect.ValueOf(obj)).IsValid() {
+			return true, nil
+		}
+	}
+
+	return false, errors.New("not in array")
+}

+ 22 - 0
pkg/grpcutil/grpc_error.go

@@ -0,0 +1,22 @@
+package grpcutil
+
+import (
+	"fmt"
+
+	"kpt-tmr-group/pkg/xerr"
+
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+)
+
+func ClassifyError(err error, request interface{}) error {
+	if err == nil {
+		return nil
+	}
+	_, isCus := xerr.IsCustomError(err)
+	if isCus {
+		return status.Error(codes.InvalidArgument, xerr.WrapWithLog(fmt.Errorf("err: %v, request: %+v", err, request)).Error())
+	} else {
+		return status.Error(codes.Internal, xerr.WrapWithLog(fmt.Errorf("err: %v, request: %+v", err, request)).Error())
+	}
+}

+ 30 - 0
pkg/jsonpb/decode.go

@@ -0,0 +1,30 @@
+package jsonpb
+
+import (
+	"io"
+
+	"kpt-tmr-group/pkg/xerr"
+
+	"github.com/golang/protobuf/jsonpb"
+	"github.com/golang/protobuf/proto"
+)
+
+var unmarshaler = &jsonpb.Unmarshaler{AllowUnknownFields: true}
+
+func Unmarshal(r io.Reader, pb proto.Message) (err error) {
+	defer func() {
+		if e := recover(); e != nil {
+			err = e.(error)
+		}
+	}()
+
+	if err := unmarshaler.Unmarshal(r, pb); err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+// Unmarshaler jsonpb unmarshaler
+type Unmarshaler interface {
+	Unmarshal(r io.Reader, pb proto.Message) error
+}

+ 33 - 0
pkg/jsonpb/decode_test.go

@@ -0,0 +1,33 @@
+package jsonpb
+
+import (
+	"testing"
+)
+
+var bs = `{
+  "status": {
+    "code": "SUCCESS"
+  },
+  "done": true,
+  "result": {
+    "ptTimestampUsec": "1558687721507006",
+    "level": 2,
+    "fluencyLevel": 0,
+    "pronunciationLevel": 0
+  },
+  "suggestedActivityId": [],
+  "preloadContent": {
+    "activityContent": []
+  },
+  "cbParam": {
+    "previousPart": 4,
+    "warmed": false,
+    "ptSubPart": 0
+  },
+  "IsWarmup": false
+}
+`
+
+func TestUnmarshal(t *testing.T) {
+
+}

+ 52 - 0
pkg/jsonpb/encode.go

@@ -0,0 +1,52 @@
+package jsonpb
+
+import (
+	"bytes"
+	"sync"
+
+	"github.com/golang/protobuf/jsonpb"
+	"github.com/golang/protobuf/proto"
+)
+
+var marshaller = &jsonpb.Marshaler{EmitDefaults: true, OrigName: true, EnumsAsInts: true}
+
+func Marshal(pb proto.Message) (string, error) {
+	e := newEncodeState()
+	if err := marshaller.Marshal(e, pb); err != nil {
+		return "", err
+	}
+
+	s := e.String()
+	e.Reset()
+	encodeStatePool.Put(e)
+
+	return s, nil
+}
+
+func MarshalBytes(pb proto.Message) ([]byte, error) {
+	e := newEncodeState()
+	if err := marshaller.Marshal(e, pb); err != nil {
+		return nil, err
+	}
+
+	buf := append([]byte(nil), e.Bytes()...)
+	e.Reset()
+	encodeStatePool.Put(e)
+	return buf, nil
+}
+
+// An encodeState encodes proto into a bytes.Buffer.
+type encodeState struct {
+	*bytes.Buffer
+}
+
+var encodeStatePool sync.Pool
+
+func newEncodeState() *encodeState {
+	if v := encodeStatePool.Get(); v != nil {
+		e := v.(*encodeState)
+		e.Reset()
+		return e
+	}
+	return &encodeState{Buffer: bytes.NewBuffer(make([]byte, 0, 2048))}
+}

+ 37 - 0
pkg/jsonpb/encode_test.go

@@ -0,0 +1,37 @@
+package jsonpb
+
+import (
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+)
+
+var message proto.Message
+
+func BenchmarkMarshalWithPool(b *testing.B) {
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		Marshal(message)
+	}
+}
+
+func BenchmarkMarshalBytesWithPool(b *testing.B) {
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		Marshal(message)
+	}
+}
+
+func BenchmarkMarshalToPBString(b *testing.B) {
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		marshalToPBString(message)
+	}
+}
+
+func marshalToPBString(pb proto.Message) (string, error) {
+	return marshaller.MarshalToString(pb)
+}

+ 108 - 0
pkg/jsonpb/query_decode.go

@@ -0,0 +1,108 @@
+package jsonpb
+
+import (
+	"net/url"
+	"reflect"
+	"strings"
+
+	"kpt-tmr-group/pkg/xerr"
+	"kpt-tmr-group/pkg/xreflect"
+
+	"github.com/golang/protobuf/proto"
+)
+
+var emptyField = reflect.StructField{}
+
+func UnmarshalQuery(values url.Values, pb proto.Message) error {
+	target := reflect.ValueOf(pb).Elem()
+	targetType := target.Type()
+	if targetType.Kind() != reflect.Struct {
+		return xerr.New("target should be struct")
+	}
+	sprops := proto.GetProperties(targetType)
+	for i := 0; i < target.NumField(); i++ {
+		ft := target.Type().Field(i)
+		if strings.HasPrefix(ft.Name, "XXX_") {
+			continue
+		}
+		getField := func(prop *proto.Properties) ([]string, bool) {
+			// Be liberal in what names we accept; both orig_name and camelName are okay.
+			camel, orig := prop.JSONName, prop.OrigName
+
+			keys := []string{camel, orig}
+			// handle condition xxx[]=1&xxx[]=2
+			if prop.Repeated {
+				keys = append(keys, camel+"[]", orig+"[]")
+			}
+			for _, key := range keys {
+				v, ok := values[key]
+				if ok {
+					return v, true
+				}
+			}
+			return nil, false
+		}
+
+		valueForField, ok := getField(sprops.Prop[i])
+		if !ok {
+			continue
+		}
+		err := setProtoByQueryValue(target.Field(i), valueForField, sprops.Prop[i])
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func setProtoByQueryValue(target reflect.Value, queryValue []string, prop *proto.Properties) (err error) {
+	if len(queryValue) == 0 {
+		if prop.HasDefault {
+			queryValue = []string{prop.Default}
+		} else {
+			return nil
+		}
+	}
+	if target.Kind() == reflect.Slice {
+		slice := reflect.MakeSlice(target.Type(), len(queryValue), len(queryValue))
+		for i, s := range queryValue {
+			err := setProtoByQueryValue(slice.Index(i), []string{s}, prop)
+			if err != nil {
+				return err
+			}
+		}
+		target.Set(slice)
+		return nil
+	}
+
+	// Handle enums, which have an underlying type of int32,
+	// and may appear as strings.
+	// The case of an enum appearing as a number is handled
+	// at the bottom of this function.
+	if prop != nil && prop.Enum != "" {
+		vmap := proto.EnumValueMap(prop.Enum)
+		if len(queryValue) == 1 {
+			// Don't need to do unquoting; valid enum names
+			// are from a limited character set.
+			s := queryValue[0]
+			n, ok := vmap[s]
+			if !ok {
+				return xerr.Errorf("unknown value %q for enum %s", s, prop.Enum)
+			}
+			return setProtoEnum(n, target, prop)
+		}
+	}
+	return xreflect.SetString(target, queryValue[0])
+}
+
+func setProtoEnum(val int32, target reflect.Value, prop *proto.Properties) error {
+	if target.Kind() == reflect.Ptr { // proto2
+		target.Set(reflect.New(target.Type().Elem()))
+		target = target.Elem()
+	}
+	if target.Kind() != reflect.Int32 {
+		return xerr.Errorf("invalid target %q for enum %s", target.Kind(), prop.Enum)
+	}
+	target.SetInt(int64(val))
+	return nil
+}

+ 9 - 0
pkg/jsonpb/query_decode_test.go

@@ -0,0 +1,9 @@
+package jsonpb
+
+import (
+	"testing"
+)
+
+func TestUnmarshalQuery(t *testing.T) {
+
+}

+ 63 - 0
pkg/jwt/jwt.go

@@ -0,0 +1,63 @@
+package jwt
+
+import (
+	"fmt"
+	"kpt-tmr-group/config"
+	"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(4 * time.Hour)
+
+	claims := Claims{
+		username,
+		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 ""
+}

+ 16 - 4
util/logger/log.go → pkg/logger/logrus/log.go

@@ -1,14 +1,21 @@
-package log
+package logrus
 
 import (
 	"fmt"
-	"os"
+	"kpt-tmr-group/pkg/tool"
 	"path"
 	"runtime"
+	"time"
 
+	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
 	"github.com/sirupsen/logrus"
 )
 
+var (
+	logPath     = "./logger"
+	logFileName = fmt.Sprintf("/logrus-%s.log", time.Now().Format(tool.DateTime))
+)
+
 func init() {
 	// Log as JSON instead of the default ASCII formatter.
 	logrus.SetFormatter(&logrus.JSONFormatter{
@@ -20,8 +27,13 @@ func init() {
 
 	// Output to stdout instead of the default stderr
 	// Can be any io.Writer, see below for File example
-	logrus.SetOutput(os.Stdout)
-
+	writer, _ := rotatelogs.New(
+		fmt.Sprintf("%s%s", logPath, logFileName),
+		// rotatelogs.WithLinkName(logPath),
+		rotatelogs.WithMaxAge(time.Duration(7*24)*time.Hour),     // 备份7天的日志
+		rotatelogs.WithRotationTime(time.Duration(24)*time.Hour), // 24小时切割一次日志
+	)
+	logrus.SetOutput(writer) // logrus 设置日志的输出方式
 	// Only log the warning severity or above.
 	logrus.SetLevel(DebugLevel)
 }

+ 84 - 0
pkg/logger/zaplog/log.go

@@ -0,0 +1,84 @@
+package zaplog
+
+import (
+	"fmt"
+	"kpt-tmr-group/pkg/tool"
+	"path"
+	"runtime"
+	"time"
+
+	"github.com/natefinch/lumberjack"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+var (
+	Logger      *zap.Logger
+	logFileName = fmt.Sprintf("./logger/zap-%s.log", time.Now().Format(tool.DateTime))
+)
+
+func init() {
+	encoderConfig := zap.NewDevelopmentEncoderConfig()
+	// 设置日志时间格式
+	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
+	// 日志encoder 还是json encode,把日志进行格式化json格式的
+	encoder := zapcore.NewJSONEncoder(encoderConfig)
+
+	// topicErrors := zapcore.AddSync(ioutil.Discard)  //kafka topic
+
+	fileWriteSyncer := getFileLogWriter(logFileName)
+	core := zapcore.NewTee(
+		// zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel),  // 打印到控制台
+		// zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), topicErrors, zapcore.ErrorLevel),   // 打印到kafka 待验证
+		zapcore.NewCore(encoder, fileWriteSyncer, zapcore.DebugLevel), // 打印到指定的日志文件
+	)
+	Logger = zap.New(core)
+}
+
+func getFileLogWriter(logFileName string) zapcore.WriteSyncer {
+	lumberJackLogger := &lumberjack.Logger{
+		Filename:   logFileName,
+		MaxSize:    100, // 单个文件最大100M
+		MaxBackups: 10,  // 大于60个日志文件后,清理比较旧的日志文件
+		MaxAge:     1,   // 一天切割1次
+		Compress:   false,
+	}
+	return zapcore.AddSync(lumberJackLogger)
+}
+
+func getCallerInfoForLog() (callerFields []zap.Field) {
+	pc, file, line, ok := runtime.Caller(2)
+	if !ok {
+		return
+	}
+	funcName := runtime.FuncForPC(pc).Name()
+	funcName = path.Base(funcName)
+
+	callerFields = append(callerFields, zap.String("func", funcName), zap.String("file", file), zap.Int("line", line))
+	return
+}
+
+func Info(message string, fields ...zap.Field) {
+	callerFields := getCallerInfoForLog()
+	fields = append(fields, callerFields...)
+	Logger.Info(message, fields...)
+}
+
+func Debug(message string, fields ...zap.Field) {
+	callerFields := getCallerInfoForLog()
+	fields = append(fields, callerFields...)
+	Logger.Debug(message, fields...)
+}
+
+func Error(message string, fields ...zap.Field) {
+	callerFields := getCallerInfoForLog()
+	fields = append(fields, callerFields...)
+	Logger.Error(message, fields...)
+}
+
+func Warn(message string, fields ...zap.Field) {
+	callerFields := getCallerInfoForLog()
+	fields = append(fields, callerFields...)
+	Logger.Warn(message, fields...)
+}

+ 0 - 0
util/runtimeutil/caller.go → pkg/runtimeutil/caller.go


+ 0 - 0
util/runtimeutil/caller_test.go → pkg/runtimeutil/caller_test.go


+ 1 - 1
util/sentry/sentry.go → pkg/sentry/sentry.go

@@ -2,7 +2,7 @@ package sentry
 
 import (
 	"context"
-	log "kpt-tmr-group/util/logger"
+	log "kpt-tmr-group/pkg/logger/logrus"
 
 	"github.com/getsentry/sentry-go"
 )

+ 69 - 0
pkg/tool/tool.go

@@ -0,0 +1,69 @@
+package tool
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+const (
+	Layout          = "2006-01-02 15:04:05"
+	DateTime        = "2006-01-02"
+	DefaultExecTime = "0_0_0"
+)
+
+// StringToTimeUnix 时间字符串转换成时间戳
+// a.g 6_2_3  ===> 转换成 6天2小时3分钟后的时间戳
+// TODO 需要优化代码写得太死,需要优化后兼容秒的场景
+func StringToTimeUnix(execTimeStr string) int64 {
+	var processAt = time.Now().Unix()
+	if execTimeStr == DefaultExecTime {
+		return processAt
+	}
+
+	execTime := strings.Split(execTimeStr, "_")
+	if len(execTime) < 3 {
+		return processAt
+	}
+
+	// 天数
+	days, _ := strconv.Atoi(execTime[0])
+	processAt += int64(days) * 24 * 60 * 60
+	// 小时
+	hours, _ := strconv.Atoi(execTime[1])
+	processAt += int64(hours) * 60 * 60
+
+	// 分钟
+	minutes, _ := strconv.Atoi(execTime[2])
+	processAt += int64(minutes) * 60
+
+	return processAt
+}
+
+func GetLocalTime(timeStr string) time.Time {
+	execTime, _ := time.ParseInLocation(Layout, timeStr, time.Local)
+	return execTime
+}
+
+// TimeParseLocalUnix 获取当天零点的时间戳
+// eg 2023-02-22 => 1676995200
+func TimeParseLocalUnix(DayTime string) int64 {
+	value := DayTime
+	if len(DayTime) <= 11 {
+		value = fmt.Sprintf("%s 00:00:00", DayTime)
+	}
+
+	loc, _ := time.LoadLocation("Local")
+	theTime, _ := time.ParseInLocation(Layout, value, loc)
+	return theTime.Unix()
+}
+
+func Md5String(input string) string {
+	s := md5.New()
+	digest := strings.ReplaceAll(input, "\n", "")
+	s.Write([]byte(digest))
+	return hex.EncodeToString(s.Sum(nil))
+}

+ 124 - 0
pkg/tool/tool_test.go

@@ -0,0 +1,124 @@
+package tool
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConditionsInterpreter(t *testing.T) {
+	conditions := `[
+    {
+        "condition_item": [
+            {
+                "condition_name": "aaa",
+                "conditions_kind": 2,
+                "conditions_value": "11111"
+            },
+            {
+                "condition_name": "bbb",
+                "conditions_kind": 1,
+                "conditions_value": "22222"
+            }
+        ]
+    },
+    {
+        "condition_item": [
+            {
+                "condition_name": "ccc",
+                "conditions_kind": 4,
+                "conditions_value": "3333"
+            },
+            {
+                "condition_name": "ddddd",
+                "conditions_kind": 1,
+                "conditions_value": "4444"
+            }
+        ]
+    }
+]`
+	t.Run("ok", func(t *testing.T) {
+		sql, err := ConditionsInterpreter(conditions)
+		if err != nil {
+			t.Error(err)
+		}
+		want := "( aaa != '11111'  AND bbb = '22222' ) OR ( ccc <= '3333'  AND ddddd = '4444' )"
+		assert.Equal(t, sql, want)
+	})
+
+}
+
+func TestTimeParseLocalUnix(t *testing.T) {
+	t.Run("ok", func(t *testing.T) {
+		testMap := map[string]int64{
+			"2023-02-22": 1676995200,
+			"2023-02-23": 1677081600,
+		}
+		for localTime, timestamp := range testMap {
+			assert.Equal(t, TimeParseLocalUnix(localTime), timestamp)
+		}
+	})
+}
+
+func TestGetTargetByValueForTag(t *testing.T) {
+
+}
+
+func TestStringToTimeUnix(t *testing.T) {
+	// 测试分钟维度
+	t.Run("minute", func(t *testing.T) {
+		for i := 0; i < 10; i++ {
+			execTime := fmt.Sprintf("0_0_%d", i)
+			want := time.Now().Add(time.Duration(i) * time.Minute).Unix()
+			got := StringToTimeUnix(execTime)
+			assert.Equal(t, want, got)
+		}
+	})
+
+	// 测试小时维度
+	t.Run("hour", func(t *testing.T) {
+		for i := 0; i < 10; i++ {
+			execTime := fmt.Sprintf("0_%d_0", i)
+			want := time.Now().Add(time.Duration(i) * time.Hour).Unix()
+			got := StringToTimeUnix(execTime)
+			assert.Equal(t, want, got)
+		}
+	})
+
+	// 测试天维度
+	t.Run("day", func(t *testing.T) {
+		for i := 0; i < 10; i++ {
+			execTime := fmt.Sprintf("%d_0_0", i)
+			want := time.Now().Add(time.Duration(i) * time.Hour * 24).Unix()
+			got := StringToTimeUnix(execTime)
+			assert.Equal(t, want, got)
+		}
+	})
+}
+
+func TestGetLocalTime(t *testing.T) {
+	t.Run("ok", func(t *testing.T) {
+		tests := []struct {
+			PushTimeStr string
+			Got         int64
+		}{
+			{
+				PushTimeStr: "2023-02-28 09:30:00",
+				Got:         1677547800,
+			},
+		}
+		for _, tt := range tests {
+			want := GetLocalTime(tt.PushTimeStr)
+			assert.Equal(t, want.Unix(), tt.Got)
+		}
+	})
+}
+
+func TestDemo(t *testing.T) {
+
+	t.Run("ok", func(t *testing.T) {
+
+	})
+}

+ 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))
+}

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