ソースを参照

statustic: 准确性分析

Yi 1 年間 前
コミット
b2c6208610

+ 2 - 2
README.md

@@ -37,5 +37,5 @@ TODO 列表
 - 用户登出没有用redis做缓存,所以后端没有提供登出接口,有以下弊端
   * 用户的token没有过期,如被人劫持,会被使用到直到token过期
   
-- 饲料列表
-  * 小料车下拉框问题
+- 饲料列表--小料车下拉框问题
+- 权限列表排序的问题

+ 2 - 0
backend/operation/mobile.proto

@@ -4,6 +4,7 @@ package backend.operation;
 option go_package = ".;operationPb";
 
 import "backend/operation/pagination.proto";
+import "backend/operation/system.proto";
 
 message SearchMobileRequest {
   string name = 1;       // 名称
@@ -28,6 +29,7 @@ message MobileData {
   string name = 2;
   uint32 created_at = 3;
   string created_at_format = 4;
+  repeated AddMenuRequest children = 15;   // 子分类
 }
 
 

+ 50 - 0
backend/operation/statistic.proto

@@ -5,6 +5,7 @@ option go_package = ".;operationPb";
 
 
 import "backend/operation/pagination.proto";
+import "backend/operation/enum.proto";
 
 // 添加配方评估 具体字段含义参照formula_estimate表对应的字段
 message AddFormulaEstimateRequest {
@@ -83,4 +84,53 @@ message SearchFormulaEstimate {
   int32 total = 2;
   int32 page_size = 3;
   repeated AddFormulaEstimateRequest list = 4;
+}
+
+// 首页 dashboard 准确性分析
+message SearchAnalysisAccuracyRequest {
+  CattleCategoryParent.Kind cattle_parent_category_id = 1;   // 牧畜分类id 泌乳牛
+  int32 feed_formula_id = 2;    // 配方id
+  string start_date = 3;       // 开始时间
+  string end_date = 4;         // 结束时间
+  repeated int32 pasture_ids = 5;   //牧场ids
+}
+
+message SearchAnalysisAccuracyResponse {
+  int32 code = 1;
+  string msg = 2;
+  AnalysisAccuracy data = 3;
+}
+
+message AnalysisAccuracy {
+  Chart chart = 1;
+  Table table = 2;
+}
+
+message Chart {
+  CommonValueRatio mixed_fodder_accurate_ratio = 4;              // 混料准确率
+  CommonValueRatio mixed_fodder_correct_ratio = 5;               // 混料正确率
+  CommonValueRatio sprinkle_fodder_accurate_ratio = 6;           // 撒料准确率
+  CommonValueRatio sprinkle_fodder_correct_ratio = 7;            // 撒料正确率
+}
+
+message Table {
+  message TableList {
+    int32 id = 1;
+    string name = 2;
+  }
+
+  repeated TableList table_list = 1;
+}
+
+message CommonValueRatio {
+  string max_value = 1;                  // 最高值
+  string middle_value = 2;               // 中位值
+  string min_value = 3;                  // 最低值
+  repeated ValueRatio data_list = 4;     // 数据集合
+  repeated string pasture_name = 5;      // 牧场名称集合
+  repeated string date_day = 6;          // 日期集合
+}
+
+message ValueRatio {
+  repeated string value_ratio = 1;
 }

+ 6 - 0
backend/operation/system.proto

@@ -202,6 +202,12 @@ message SystemUserMenuData {
   repeated AddPastureRequest pasture_list = 1;    // 牧场列表
   repeated AddMenuRequest menu_list = 2;          // 菜单列表
   repeated AddMobileRequest mobile_list = 3;      // 移动端权限
+  repeated MenuButtonsPath menu_buttons_path = 4;  // 按钮级别权限path,前端需要特别处理
+}
+
+message MenuButtonsPath {
+  string path = 1;
+  int32 menu_id = 2;
 }
 
 // 移动端

+ 1 - 2
http/handler/pasture/forage_list.go

@@ -59,10 +59,9 @@ func EditForage(c *gin.Context) {
 		valid.Field(&req.Id, valid.Required, valid.Min(1)),
 		valid.Field(&req.Name, valid.Required),
 		valid.Field(&req.CategoryId, valid.Required),
+		valid.Field(&req.CategoryName, 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

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

@@ -4,6 +4,7 @@ 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"
 
@@ -31,3 +32,28 @@ func SearchFormulaEstimateList(c *gin.Context) {
 	}
 	ginutil.JSONResp(c, res)
 }
+
+// AnalysisAccuracy 准备性分析
+func AnalysisAccuracy(c *gin.Context) {
+	var req operationPb.SearchAnalysisAccuracyRequest
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.PastureIds, valid.Required),
+		valid.Field(&req.CattleParentCategoryId, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	res, err := middleware.BackendOperation(c).OpsService.SearchAnalysisAccuracy(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, res)
+	//ginutil.JSONResp(c, res)
+}

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

@@ -262,3 +262,12 @@ func ResetPasswordSystemUser(c *gin.Context) {
 		Data: &operationPb.Success{Success: true},
 	})
 }
+
+// LogoutSystemUser 用户登出,
+func LogoutSystemUser(c *gin.Context) {
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}

+ 1 - 0
http/middleware/cors.go

@@ -36,6 +36,7 @@ func CORS(configs ...cors.Config) gin.HandlerFunc {
 		//允许类型校验
 		if method == "OPTIONS" {
 			c.JSON(http.StatusOK, "ok!")
+			return
 		}
 
 		defer func() {

+ 3 - 0
http/route/app_api.go

@@ -37,6 +37,7 @@ func AppAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		systemRoute.DELETE("/user/:user_id", system.DeleteUser)
 		systemRoute.POST("/user/permissions", system.GetSystemUserPermissions)
 		systemRoute.POST("/user/rest_password/:user_id", system.ResetPasswordSystemUser)
+		systemRoute.POST("/user/logout", system.LogoutSystemUser)
 
 		// 系统角色
 		systemRoute.POST("/role/add", system.AddSystemRole)
@@ -104,6 +105,8 @@ func AppAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		//统计分析 statistic analysis
 		opsRoute.POST("/feed_estimate/list", statistic.SearchFormulaEstimateList)
 
+		opsRoute.GET("/analysis/accuracy", statistic.AnalysisAccuracy)
+
 		// 其他
 	}
 }

+ 78 - 0
model/analysis_accuracy.go

@@ -0,0 +1,78 @@
+package model
+
+import (
+	operationPb "kpt-tmr-group/proto/go/backend/operation"
+)
+
+type AnalysisAccuracy struct {
+	Id                       int64                                 `json:"id"`
+	PastureId                int64                                 `json:"pasture_id"`
+	PastureName              string                                `json:"pasture_name"`
+	FeedFormulaId            int64                                 `json:"feed_formula_id"`
+	FeedFormulaName          string                                `json:"feed_formula_name"`
+	CattleParentCategoryId   operationPb.CattleCategoryParent_Kind `json:"cattle_parent_category_id"`
+	CattleParentCategoryName string                                `json:"cattle_parent_category_name"`
+	IWeight                  int64                                 `json:"iweight"`
+	LWeight                  int64                                 `json:"lweight"`
+	OWeight                  int64                                 `json:"oweight"`
+	ActualWeightMinus        int64                                 `json:"actual_weight_minus"`
+	AllowRatio               int64                                 `json:"allow_ratio"`
+	Alweight                 int64                                 `json:"alweight"`
+	DateDay                  string                                `json:"date_day"`
+	CreatedAt                int64                                 `json:"created_at"`
+	UpdateAt                 int64                                 `json:"update_at"`
+}
+
+func (c *AnalysisAccuracy) TableName() string {
+	return "analysis_accuracy"
+}
+
+type OptionsAnalysisAccuracy struct {
+	PastureId            int64  `json:"pasture_id"`
+	PastureName          string `json:"pasture_name"`
+	AllIweight           int64  `json:"all_iweight"`
+	AllLweight           int64  `json:"all_lweight"`
+	AllOweight           int64  `json:"all_oweight"`
+	AllActualWeightMinus int64  `json:"all_actual_weight_minus"`
+	AllAllowRatio        int64  `json:"all_allow_ratio"`
+	AllAlweight          int64  `json:"all_alweight"`
+	DateDay              string `json:"date_day"`
+}
+
+type SearchAnalysisAccuracyResponse struct {
+	Code int32                 `json:"code"`
+	Msg  string                `json:"msg"`
+	Data *AnalysisAccuracyData `json:"data"`
+}
+
+type AnalysisAccuracyData struct {
+	Chart *Chart `json:"chart"`
+	Table *Table `json:"table"`
+}
+
+type Table struct {
+	TitleList []*TableList   `json:"title_list"`
+	DataList  []*interface{} `json:"data_list"`
+}
+
+type TableList struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
+}
+
+type Chart struct {
+	MixedFodderAccurateRatio    *CommonValueRatio `json:"mixed_fodder_accurate_ratio"`
+	MixedFodderCorrectRatio     *CommonValueRatio `json:"mixed_fodder_correct_ratio"`
+	SprinkleFodderAccurateRatio *CommonValueRatio `json:"sprinkle_fodder_accurate_ratio"`
+	SprinkleFodderCorrectRatio  *CommonValueRatio `json:"sprinkle_fodder_correct_ratio"`
+}
+
+type CommonValueRatio struct {
+	MaxValue    string     `json:"max_value"`    // 最高值
+	MiddleValue string     `json:"middle_value"` // 中位值
+	MinValue    string     `json:"min_value"`    // 最低值
+	DataList    [][]string `json:"data_list"`    // 数据集合
+	PastureName []string   `json:"pasture_name"` // 牧场名称集合
+	PastureIds  []int32    `json:"pasture_ids"`
+	DateDay     []string   `json:"date_day"` // 日期集合
+}

+ 9 - 0
model/forage.go

@@ -11,6 +11,7 @@ type Forage struct {
 	PastureName        string                          `json:"pasture_name"`
 	Name               string                          `json:"name"`
 	CategoryId         int64                           `json:"category_id"`
+	CategoryName       string                          `json:"category_name"`
 	MaterialType       int64                           `json:"material_type"`
 	UniqueEncode       string                          `json:"unique_encode"`
 	ForageSourceId     operationPb.ForageSource_Kind   `json:"forage_source_id"`
@@ -44,6 +45,7 @@ func NewForage(req *operationPb.AddForageRequest) *Forage {
 	return &Forage{
 		Name:               req.Name,
 		CategoryId:         int64(req.CategoryId),
+		CategoryName:       req.CategoryName,
 		UniqueEncode:       req.UniqueEncode,
 		ForageSourceId:     req.ForageSourceId,
 		ForageSourceName:   req.ForageSourceName,
@@ -76,10 +78,13 @@ func (f ForageSlice) ToPB() []*operationPb.AddForageRequest {
 			Id:                 int32(v.Id),
 			Name:               v.Name,
 			CategoryId:         int32(v.CategoryId),
+			CategoryName:       v.CategoryName,
 			MaterialType:       int32(v.MaterialType),
 			UniqueEncode:       v.UniqueEncode,
 			ForageSourceId:     v.ForageSourceId,
+			ForageSourceName:   v.ForageSourceName,
 			PlanTypeId:         v.PlanTypeId,
+			PlanTypeName:       v.PlanTypeName,
 			SmallMaterialScale: v.SmallMaterialScale,
 			AllowError:         int32(v.AllowError),
 			PackageWeight:      int32(v.PackageWeight),
@@ -88,7 +93,11 @@ func (f ForageSlice) ToPB() []*operationPb.AddForageRequest {
 			JumpDelay:          v.JumpDelay,
 			ConfirmStart:       v.ConfirmStart,
 			RelayLocations:     int32(v.RelayLocations),
+			Jmp:                v.Jmp,
 			IsShow:             v.IsShow,
+			Backup1:            v.Backup1,
+			Backup2:            v.Backup2,
+			Backup3:            v.Backup3,
 			CreatedAt:          int32(v.CreatedAt),
 			CreatedAtFormat:    time.Unix(v.CreatedAt, 0).Format(LayoutTime),
 		}

+ 2 - 0
module/backend/interface.go

@@ -5,6 +5,7 @@ import (
 	"context"
 	"io"
 	"kpt-tmr-group/config"
+	"kpt-tmr-group/model"
 	"kpt-tmr-group/pkg/di"
 	operationPb "kpt-tmr-group/proto/go/backend/operation"
 	"kpt-tmr-group/service/wechat"
@@ -131,6 +132,7 @@ type SystemService interface {
 
 type StatisticService interface {
 	SearchFormulaEstimateList(ctx context.Context, req *operationPb.SearchFormulaEstimateRequest) (*operationPb.SearchFormulaEstimateResponse, error)
+	SearchAnalysisAccuracy(ctx context.Context, req *operationPb.SearchAnalysisAccuracyRequest) (*model.SearchAnalysisAccuracyResponse, error)
 }
 
 type WxAppletService interface {

+ 2 - 2
module/backend/pasture_service.go

@@ -403,7 +403,7 @@ func (s *StoreEntry) CreateForage(ctx context.Context, req *operationPb.AddForag
 // 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 err := s.DB.Where("is_delete = ?", operationPb.IsShow_OK).First(&forage).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return xerr.Custom("该数据不存在")
 		}
@@ -627,7 +627,7 @@ 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 {
+	if err := s.DB.Model(new(model.Forage)).Where("id IN ?", ids).Update("is_delete", operationPb.IsShow_NO).Error; err != nil {
 		return xerr.WithStack(err)
 	}
 	return nil

+ 118 - 0
module/backend/statistic_service.go

@@ -7,6 +7,8 @@ import (
 	"kpt-tmr-group/pkg/xerr"
 	operationPb "kpt-tmr-group/proto/go/backend/operation"
 	"net/http"
+	"strconv"
+	"strings"
 	"time"
 )
 
@@ -57,3 +59,119 @@ func (s *StoreEntry) SearchFormulaEstimateList(ctx context.Context, req *operati
 		},
 	}, nil
 }
+
+func (s *StoreEntry) SearchAnalysisAccuracy(ctx context.Context, req *operationPb.SearchAnalysisAccuracyRequest) (*model.SearchAnalysisAccuracyResponse, error) {
+
+	dataList := &model.CommonValueRatio{
+		MaxValue:    "",
+		MiddleValue: "",
+		MinValue:    "",
+		PastureName: make([]string, 0),
+		DateDay:     make([]string, 0),
+		DataList:    make([][]string, 0),
+		PastureIds:  make([]int32, 0),
+	}
+	res := &model.SearchAnalysisAccuracyResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &model.AnalysisAccuracyData{
+			Chart: &model.Chart{
+				MixedFodderAccurateRatio:    dataList,
+				MixedFodderCorrectRatio:     dataList,
+				SprinkleFodderAccurateRatio: dataList,
+				SprinkleFodderCorrectRatio:  dataList,
+			},
+			Table: &model.Table{
+				TitleList: make([]*model.TableList, 0),
+				DataList:  make([]*interface{}, 0),
+			},
+		},
+	}
+	res.Data.Table.TitleList = append(res.Data.Table.TitleList, &model.TableList{
+		Name:  "title",
+		Value: "牧场",
+	})
+
+	analysisAccuracy := make([]*model.OptionsAnalysisAccuracy, 0)
+	pref := s.DB.Model(new(model.AnalysisAccuracy))
+	if req.EndDate != "" && req.StartDate != "" {
+		pref.Where("date_day BETWEEN ? AND ?", req.StartDate, req.EndDate)
+	}
+
+	if req.CattleParentCategoryId > 0 {
+		pref.Where("cattle_parent_category_id = ?", req.CattleParentCategoryId)
+	}
+	if req.FeedFormulaId > 0 {
+		pref.Where("feed_formula_id = ?", req.FeedFormulaId)
+	}
+
+	if len(req.PastureIds) > 0 {
+		pref.Where("pasture_id IN ?", req.PastureIds)
+	}
+
+	if err := pref.Select("pasture_id,pasture_name,date_day,sum(iweight) as all_iweight,sum(lweight) as all_lweight,sum(oweight) as all_oweight,sum(actual_weight_minus) as all_actual_weight_minus,sum(allow_ratio) as all_allow_ratio,sum(alweight) as all_alweight").
+		Group("pasture_id,pasture_name,date_day").Order("pasture_name,date_day").Find(&analysisAccuracy).Debug().Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	mixedFodderAccurateRatio := make([]float64, 0)
+	mapPastureName := make(map[string]bool, 0)
+	mapDateDay := make(map[string]bool, 0)
+	mapRatio := make(map[string]bool, 0)
+	tableList := make([]*model.TableList, 0)
+	for k, v := range analysisAccuracy {
+		if _, ok := mapPastureName[v.PastureName]; !ok {
+			res.Data.Chart.MixedFodderAccurateRatio.PastureName = append(res.Data.Chart.MixedFodderAccurateRatio.PastureName, v.PastureName)
+			res.Data.Chart.MixedFodderAccurateRatio.PastureIds = append(res.Data.Chart.MixedFodderAccurateRatio.PastureIds, int32(v.PastureId))
+			mapPastureName[v.PastureName] = true
+		}
+
+		dataDayFormat := strings.TrimRight(v.DateDay, "T00:00:00+08:00")
+		if _, ok := mapDateDay[v.DateDay]; !ok {
+			res.Data.Chart.MixedFodderAccurateRatio.DateDay = append(res.Data.Chart.MixedFodderAccurateRatio.DateDay, dataDayFormat)
+			mapDateDay[v.DateDay] = true
+		}
+
+		valueRatio1 := float64(v.AllIweight/v.AllLweight) / 100.0
+		if _, ok := mapRatio[v.DateDay]; !ok {
+			valueRatio := make([]string, 0)
+			for _, a := range analysisAccuracy {
+				if a.DateDay == v.DateDay {
+					valueRatio = append(valueRatio, strconv.FormatFloat(float64(a.AllIweight/a.AllLweight)/100.0, 'f', 2, 64))
+				}
+			}
+			res.Data.Chart.MixedFodderAccurateRatio.DataList = append(res.Data.Chart.MixedFodderAccurateRatio.DataList, valueRatio)
+			mapRatio[v.DateDay] = true
+			tableList = append(tableList, &model.TableList{
+				Name:  fmt.Sprintf("date%d", k),
+				Value: dataDayFormat,
+			})
+		}
+		mixedFodderAccurateRatio = append(mixedFodderAccurateRatio, valueRatio1)
+	}
+
+	mixedFodderAccurateRatioMaxValue, mixedFodderAccurateRatioMiddleValue, mixedFodderAccurateRatioMinValue := calculateRatio(mixedFodderAccurateRatio)
+	res.Data.Chart.MixedFodderAccurateRatio.MaxValue = strconv.FormatFloat(mixedFodderAccurateRatioMaxValue, 'f', 2, 64)
+	res.Data.Chart.MixedFodderAccurateRatio.MiddleValue = strconv.FormatFloat(mixedFodderAccurateRatioMiddleValue, 'f', 2, 64)
+	res.Data.Chart.MixedFodderAccurateRatio.MinValue = strconv.FormatFloat(mixedFodderAccurateRatioMinValue, 'f', 2, 64)
+
+	res.Data.Table.TitleList = append(res.Data.Table.TitleList, tableList...)
+
+	return res, nil
+}
+
+func calculateRatio(res []float64) (float64, float64, float64) {
+	var maxValue, middleValue, minValue, allValue float64 = 0, 0, 0, 0
+	for _, v := range res {
+		if v > maxValue {
+			maxValue = v
+		}
+		if v <= maxValue {
+			minValue = v
+		}
+
+		allValue += v
+	}
+	middleValue = allValue / float64(len(res))
+	return maxValue, middleValue, minValue
+}

+ 35 - 5
module/backend/system_permissions.go

@@ -7,6 +7,8 @@ import (
 	"kpt-tmr-group/pkg/xerr"
 	operationPb "kpt-tmr-group/proto/go/backend/operation"
 	"net/http"
+	"sort"
+	"strings"
 	"sync"
 	"time"
 
@@ -73,9 +75,10 @@ func (s *StoreEntry) SystemPermissionsFormatPb(pastureList []*model.GroupPasture
 		Code: http.StatusOK,
 		Msg:  "ok",
 		Data: &operationPb.SystemUserMenuData{
-			PastureList: make([]*operationPb.AddPastureRequest, 0),
-			MenuList:    make([]*operationPb.AddMenuRequest, 0),
-			MobileList:  make([]*operationPb.AddMobileRequest, 0),
+			PastureList:     make([]*operationPb.AddPastureRequest, 0),
+			MenuList:        make([]*operationPb.AddMenuRequest, 0),
+			MobileList:      make([]*operationPb.AddMobileRequest, 0),
+			MenuButtonsPath: make([]*operationPb.MenuButtonsPath, 0),
 		},
 	}
 
@@ -120,10 +123,21 @@ func (s *StoreEntry) SystemPermissionsFormatPb(pastureList []*model.GroupPasture
 				KeepAlive:       true,
 				Children:        make([]*operationPb.AddMenuRequest, 0),
 			})
+			if menu.Level == model.Level3 {
+				systemUserMenuPermissions.Data.MenuButtonsPath = append(systemUserMenuPermissions.Data.MenuButtonsPath, &operationPb.MenuButtonsPath{
+					Path:   menu.Path,
+					MenuId: int32(menu.Id),
+				})
+			}
 		}
 
-		for _, leve3Data := range level[model.Level3] {
-			for _, leve2Data := range level[model.Level2] {
+		level2IsShow := make([]int32, 0)
+		for _, leve2Data := range level[model.Level2] {
+			for _, leve3Data := range level[model.Level3] {
+				if leve3Data.ParentId == leve2Data.Id && strings.Contains(leve3Data.Path, ":page") {
+					level2IsShow = append(level2IsShow, leve2Data.Id)
+				}
+
 				if leve3Data.ParentId == leve2Data.Id {
 					if leve2Data.Children == nil {
 						leve2Data.Children = make([]*operationPb.AddMenuRequest, 0)
@@ -133,7 +147,19 @@ func (s *StoreEntry) SystemPermissionsFormatPb(pastureList []*model.GroupPasture
 			}
 		}
 
+		sort.SliceStable(level[model.Level2], func(i, j int) bool {
+			return level[model.Level2][i].Sort > level[model.Level2][j].Sort
+		})
 		for _, leve2Data := range level[model.Level2] {
+			var info bool
+			for _, v := range level2IsShow {
+				if v == leve2Data.Id {
+					info = true
+				}
+			}
+			if !info {
+				continue
+			}
 			for _, leve1Data := range level[model.Level1] {
 				if leve2Data.ParentId == leve1Data.Id {
 					if leve1Data.Children == nil {
@@ -144,6 +170,10 @@ func (s *StoreEntry) SystemPermissionsFormatPb(pastureList []*model.GroupPasture
 			}
 		}
 
+		sort.SliceStable(level[model.Level1], func(i, j int) bool {
+			return level[model.Level1][i].Sort > level[model.Level1][j].Sort
+		})
+
 		systemUserMenuPermissions.Data.MenuList = level[model.Level1]
 		wg.Done()
 	}()

+ 39 - 5
module/backend/system_service.go

@@ -540,30 +540,64 @@ func (s *StoreEntry) IsShowSystemMenu(ctx context.Context, req *operationPb.IsSh
 
 // SearchSystemMenuList 菜单列表查询
 func (s *StoreEntry) SearchSystemMenuList(ctx context.Context, req *operationPb.SearchMenuRequest) (*operationPb.SearchMenuResponse, error) {
-	systemMenu := make([]*model.SystemMenu, 0)
+	systemMenuLevel1 := make([]*model.SystemMenu, 0)
 	var count int64 = 0
 
-	pref := s.DB.Model(new(model.SystemMenu)).Where("is_delete = ?", operationPb.IsShow_OK)
+	pref := s.DB.Model(new(model.SystemMenu)).Where("is_delete = ? and level = ?", operationPb.IsShow_OK, model.Level1)
 	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 {
+	if err := pref.Order("sort desc").Count(&count).Limit(int(req.Pagination.PageSize)).Offset(int(req.Pagination.PageOffset)).
+		Find(&systemMenuLevel1).Debug().Error; err != nil {
 		return nil, xerr.WithStack(err)
 	}
 
+	systemMenuLevel1 = append(systemMenuLevel1, s.searchMenuLevel23ByLevel1(ctx, systemMenuLevel1)...)
 	return &operationPb.SearchMenuResponse{
 		Code: http.StatusOK,
 		Msg:  "ok",
 		Data: &operationPb.SearchMenuData{
 			Page:  req.Pagination.Page,
 			Total: int32(count),
-			List:  model.SystemMenuSlice(systemMenu).ToPB(),
+			List:  model.SystemMenuSlice(systemMenuLevel1).ToPB(),
 		},
 	}, nil
 }
 
+// searchMenuLevel23ByLevel1 根据一级菜单返回对应二三级菜单
+func (s *StoreEntry) searchMenuLevel23ByLevel1(ctx context.Context, res []*model.SystemMenu) []*model.SystemMenu {
+	systemMenuLevel23 := make([]*model.SystemMenu, 0)
+	if len(res) <= 0 {
+		return systemMenuLevel23
+	}
+
+	ids1 := make([]int64, 0)
+	for _, v := range res {
+		ids1 = append(ids1, v.Id)
+	}
+	if err := s.DB.Model(new(model.SystemMenu)).Where("is_delete = ? and level = ?", operationPb.IsShow_OK, model.Level2).
+		Where("parent_id IN ?", ids1).Order("sort desc").
+		Find(&systemMenuLevel23).Error; err != nil {
+		return systemMenuLevel23
+	}
+	ids2 := make([]int64, 0)
+	for _, v := range systemMenuLevel23 {
+		ids2 = append(ids2, v.Id)
+	}
+	systemMenuLevel3 := make([]*model.SystemMenu, 0)
+	if err := s.DB.Model(new(model.SystemMenu)).Where("is_delete = ? and level = ?", operationPb.IsShow_OK, model.Level3).
+		Where("parent_id IN ?", ids2).Order("sort desc").
+		Find(&systemMenuLevel3).Error; err != nil {
+		return systemMenuLevel23
+	}
+	if len(systemMenuLevel3) > 0 {
+		systemMenuLevel23 = append(systemMenuLevel23, systemMenuLevel3...)
+	}
+
+	return systemMenuLevel23
+}
+
 // DeleteSystemMenu 删除系统菜单
 func (s *StoreEntry) DeleteSystemMenu(ctx context.Context, menuId int64) error {
 	systemMenu := &model.SystemMenu{Id: menuId}

+ 36 - 19
proto/go/backend/operation/mobile.pb.go

@@ -214,10 +214,11 @@ type MobileData struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Id              uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
-	Name            string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
-	CreatedAt       uint32 `protobuf:"varint,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
-	CreatedAtFormat string `protobuf:"bytes,4,opt,name=created_at_format,json=createdAtFormat,proto3" json:"created_at_format,omitempty"`
+	Id              uint32            `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+	Name            string            `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+	CreatedAt       uint32            `protobuf:"varint,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
+	CreatedAtFormat string            `protobuf:"bytes,4,opt,name=created_at_format,json=createdAtFormat,proto3" json:"created_at_format,omitempty"`
+	Children        []*AddMenuRequest `protobuf:"bytes,15,rep,name=children,proto3" json:"children,omitempty"` // 子分类
 }
 
 func (x *MobileData) Reset() {
@@ -280,6 +281,13 @@ func (x *MobileData) GetCreatedAtFormat() string {
 	return ""
 }
 
+func (x *MobileData) GetChildren() []*AddMenuRequest {
+	if x != nil {
+		return x.Children
+	}
+	return nil
+}
+
 var File_backend_operation_mobile_proto protoreflect.FileDescriptor
 
 var file_backend_operation_mobile_proto_rawDesc = []byte{
@@ -288,7 +296,9 @@ var file_backend_operation_mobile_proto_rawDesc = []byte{
 	0x12, 0x11, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74,
 	0x69, 0x6f, 0x6e, 0x1a, 0x22, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x6f, 0x70, 0x65,
 	0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f,
-	0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6d, 0x0a, 0x13, 0x53, 0x65, 0x61, 0x72, 0x63,
+	0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64,
+	0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x73, 0x79, 0x73, 0x74, 0x65,
+	0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6d, 0x0a, 0x13, 0x53, 0x65, 0x61, 0x72, 0x63,
 	0x68, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12,
 	0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
 	0x6d, 0x65, 0x12, 0x42, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e,
@@ -311,15 +321,19 @@ var file_backend_operation_mobile_proto_rawDesc = []byte{
 	0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x31, 0x0a, 0x04, 0x6c, 0x69, 0x73,
 	0x74, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e,
 	0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x6f, 0x62, 0x69,
-	0x6c, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x22, 0x7b, 0x0a, 0x0a,
-	0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
-	0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d,
-	0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01,
-	0x28, 0x0d, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x2a, 0x0a,
-	0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x6d,
-	0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
-	0x64, 0x41, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x3b, 0x6f,
+	0x6c, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x22, 0xba, 0x01, 0x0a,
+	0x0a, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69,
+	0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e,
+	0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
+	0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20,
+	0x01, 0x28, 0x0d, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x2a,
+	0x0a, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x66, 0x6f, 0x72,
+	0x6d, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x61, 0x74,
+	0x65, 0x64, 0x41, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x68,
+	0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x62,
+	0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+	0x2e, 0x41, 0x64, 0x64, 0x4d, 0x65, 0x6e, 0x75, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52,
+	0x08, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x3b, 0x6f,
 	0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
 	0x6f, 0x33,
 }
@@ -343,16 +357,18 @@ var file_backend_operation_mobile_proto_goTypes = []interface{}{
 	(*SearchMobileData)(nil),     // 2: backend.operation.SearchMobileData
 	(*MobileData)(nil),           // 3: backend.operation.MobileData
 	(*PaginationModel)(nil),      // 4: backend.operation.PaginationModel
+	(*AddMenuRequest)(nil),       // 5: backend.operation.AddMenuRequest
 }
 var file_backend_operation_mobile_proto_depIdxs = []int32{
 	4, // 0: backend.operation.SearchMobileRequest.pagination:type_name -> backend.operation.PaginationModel
 	2, // 1: backend.operation.SearchMobileResponse.data:type_name -> backend.operation.SearchMobileData
 	3, // 2: backend.operation.SearchMobileData.list:type_name -> backend.operation.MobileData
-	3, // [3:3] is the sub-list for method output_type
-	3, // [3:3] is the sub-list for method input_type
-	3, // [3:3] is the sub-list for extension type_name
-	3, // [3:3] is the sub-list for extension extendee
-	0, // [0:3] is the sub-list for field type_name
+	5, // 3: backend.operation.MobileData.children:type_name -> backend.operation.AddMenuRequest
+	4, // [4:4] is the sub-list for method output_type
+	4, // [4:4] is the sub-list for method input_type
+	4, // [4:4] is the sub-list for extension type_name
+	4, // [4:4] is the sub-list for extension extendee
+	0, // [0:4] is the sub-list for field type_name
 }
 
 func init() { file_backend_operation_mobile_proto_init() }
@@ -361,6 +377,7 @@ func file_backend_operation_mobile_proto_init() {
 		return
 	}
 	file_backend_operation_pagination_proto_init()
+	file_backend_operation_system_proto_init()
 	if !protoimpl.UnsafeEnabled {
 		file_backend_operation_mobile_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*SearchMobileRequest); i {

ファイルの差分が大きいため隠しています
+ 793 - 183
proto/go/backend/operation/statistic.pb.go


+ 118 - 32
proto/go/backend/operation/system.pb.go

@@ -1880,9 +1880,10 @@ type SystemUserMenuData struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	PastureList []*AddPastureRequest `protobuf:"bytes,1,rep,name=pasture_list,json=pastureList,proto3" json:"pasture_list,omitempty"` // 牧场列表
-	MenuList    []*AddMenuRequest    `protobuf:"bytes,2,rep,name=menu_list,json=menuList,proto3" json:"menu_list,omitempty"`          // 菜单列表
-	MobileList  []*AddMobileRequest  `protobuf:"bytes,3,rep,name=mobile_list,json=mobileList,proto3" json:"mobile_list,omitempty"`    // 移动端权限
+	PastureList     []*AddPastureRequest `protobuf:"bytes,1,rep,name=pasture_list,json=pastureList,proto3" json:"pasture_list,omitempty"`               // 牧场列表
+	MenuList        []*AddMenuRequest    `protobuf:"bytes,2,rep,name=menu_list,json=menuList,proto3" json:"menu_list,omitempty"`                        // 菜单列表
+	MobileList      []*AddMobileRequest  `protobuf:"bytes,3,rep,name=mobile_list,json=mobileList,proto3" json:"mobile_list,omitempty"`                  // 移动端权限
+	MenuButtonsPath []*MenuButtonsPath   `protobuf:"bytes,4,rep,name=menu_buttons_path,json=menuButtonsPath,proto3" json:"menu_buttons_path,omitempty"` // 按钮级别权限path,前端需要特别处理
 }
 
 func (x *SystemUserMenuData) Reset() {
@@ -1938,6 +1939,68 @@ func (x *SystemUserMenuData) GetMobileList() []*AddMobileRequest {
 	return nil
 }
 
+func (x *SystemUserMenuData) GetMenuButtonsPath() []*MenuButtonsPath {
+	if x != nil {
+		return x.MenuButtonsPath
+	}
+	return nil
+}
+
+type MenuButtonsPath struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Path   string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
+	MenuId int32  `protobuf:"varint,2,opt,name=menu_id,json=menuId,proto3" json:"menu_id,omitempty"`
+}
+
+func (x *MenuButtonsPath) Reset() {
+	*x = MenuButtonsPath{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_backend_operation_system_proto_msgTypes[27]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MenuButtonsPath) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MenuButtonsPath) ProtoMessage() {}
+
+func (x *MenuButtonsPath) ProtoReflect() protoreflect.Message {
+	mi := &file_backend_operation_system_proto_msgTypes[27]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MenuButtonsPath.ProtoReflect.Descriptor instead.
+func (*MenuButtonsPath) Descriptor() ([]byte, []int) {
+	return file_backend_operation_system_proto_rawDescGZIP(), []int{27}
+}
+
+func (x *MenuButtonsPath) GetPath() string {
+	if x != nil {
+		return x.Path
+	}
+	return ""
+}
+
+func (x *MenuButtonsPath) GetMenuId() int32 {
+	if x != nil {
+		return x.MenuId
+	}
+	return 0
+}
+
 // 移动端
 type AddMobileRequest struct {
 	state         protoimpl.MessageState
@@ -1951,7 +2014,7 @@ type AddMobileRequest struct {
 func (x *AddMobileRequest) Reset() {
 	*x = AddMobileRequest{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_backend_operation_system_proto_msgTypes[27]
+		mi := &file_backend_operation_system_proto_msgTypes[28]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1964,7 +2027,7 @@ func (x *AddMobileRequest) String() string {
 func (*AddMobileRequest) ProtoMessage() {}
 
 func (x *AddMobileRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_backend_operation_system_proto_msgTypes[27]
+	mi := &file_backend_operation_system_proto_msgTypes[28]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1977,7 +2040,7 @@ func (x *AddMobileRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use AddMobileRequest.ProtoReflect.Descriptor instead.
 func (*AddMobileRequest) Descriptor() ([]byte, []int) {
-	return file_backend_operation_system_proto_rawDescGZIP(), []int{27}
+	return file_backend_operation_system_proto_rawDescGZIP(), []int{28}
 }
 
 func (x *AddMobileRequest) GetId() uint32 {
@@ -2249,7 +2312,7 @@ var file_backend_operation_system_proto_rawDesc = []byte{
 	0x12, 0x39, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25,
 	0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69,
 	0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x6e,
-	0x75, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xe3, 0x01, 0x0a, 0x12,
+	0x75, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x02, 0x0a, 0x12,
 	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x6e, 0x75, 0x44, 0x61,
 	0x74, 0x61, 0x12, 0x47, 0x0a, 0x0c, 0x70, 0x61, 0x73, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6c, 0x69,
 	0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65,
@@ -2264,7 +2327,16 @@ var file_backend_operation_system_proto_rawDesc = []byte{
 	0x32, 0x23, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61,
 	0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x52, 0x65,
 	0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73,
-	0x74, 0x22, 0x36, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x52, 0x65,
+	0x74, 0x12, 0x4e, 0x0a, 0x11, 0x6d, 0x65, 0x6e, 0x75, 0x5f, 0x62, 0x75, 0x74, 0x74, 0x6f, 0x6e,
+	0x73, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x62,
+	0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+	0x2e, 0x4d, 0x65, 0x6e, 0x75, 0x42, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x73, 0x50, 0x61, 0x74, 0x68,
+	0x52, 0x0f, 0x6d, 0x65, 0x6e, 0x75, 0x42, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x73, 0x50, 0x61, 0x74,
+	0x68, 0x22, 0x3e, 0x0a, 0x0f, 0x4d, 0x65, 0x6e, 0x75, 0x42, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x73,
+	0x50, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x17, 0x0a, 0x07, 0x6d, 0x65, 0x6e, 0x75,
+	0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6d, 0x65, 0x6e, 0x75, 0x49,
+	0x64, 0x22, 0x36, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x52, 0x65,
 	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
 	0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
 	0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x3b, 0x6f,
@@ -2284,7 +2356,7 @@ func file_backend_operation_system_proto_rawDescGZIP() []byte {
 	return file_backend_operation_system_proto_rawDescData
 }
 
-var file_backend_operation_system_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
+var file_backend_operation_system_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
 var file_backend_operation_system_proto_goTypes = []interface{}{
 	(*CommonOK)(nil),                  // 0: backend.operation.CommonOK
 	(*Success)(nil),                   // 1: backend.operation.Success
@@ -2313,46 +2385,48 @@ var file_backend_operation_system_proto_goTypes = []interface{}{
 	(*SearchMenuData)(nil),            // 24: backend.operation.SearchMenuData
 	(*SystemUserMenuPermissions)(nil), // 25: backend.operation.SystemUserMenuPermissions
 	(*SystemUserMenuData)(nil),        // 26: backend.operation.SystemUserMenuData
-	(*AddMobileRequest)(nil),          // 27: backend.operation.AddMobileRequest
-	(IsShow_Kind)(0),                  // 28: backend.operation.IsShow.Kind
-	(*PaginationModel)(nil),           // 29: backend.operation.PaginationModel
-	(*UserPasture)(nil),               // 30: backend.operation.UserPasture
-	(*AddPastureRequest)(nil),         // 31: backend.operation.AddPastureRequest
+	(*MenuButtonsPath)(nil),           // 27: backend.operation.MenuButtonsPath
+	(*AddMobileRequest)(nil),          // 28: backend.operation.AddMobileRequest
+	(IsShow_Kind)(0),                  // 29: backend.operation.IsShow.Kind
+	(*PaginationModel)(nil),           // 30: backend.operation.PaginationModel
+	(*UserPasture)(nil),               // 31: backend.operation.UserPasture
+	(*AddPastureRequest)(nil),         // 32: backend.operation.AddPastureRequest
 }
 var file_backend_operation_system_proto_depIdxs = []int32{
 	1,  // 0: backend.operation.CommonOK.data:type_name -> backend.operation.Success
-	28, // 1: backend.operation.AddRoleRequest.is_show:type_name -> backend.operation.IsShow.Kind
-	29, // 2: backend.operation.SearchRoleRequest.pagination:type_name -> backend.operation.PaginationModel
+	29, // 1: backend.operation.AddRoleRequest.is_show:type_name -> backend.operation.IsShow.Kind
+	30, // 2: backend.operation.SearchRoleRequest.pagination:type_name -> backend.operation.PaginationModel
 	6,  // 3: backend.operation.SearchRoleResponse.data:type_name -> backend.operation.SearchRoleData
 	3,  // 4: backend.operation.SearchRoleData.list:type_name -> backend.operation.AddRoleRequest
 	8,  // 5: backend.operation.RolePermissionsList.data:type_name -> backend.operation.RolePermissionsData
 	10, // 6: backend.operation.SystemToken.data:type_name -> backend.operation.TokenData
 	12, // 7: backend.operation.UserAuth.data:type_name -> backend.operation.UserAuthData
 	13, // 8: backend.operation.UserAuthData.roles:type_name -> backend.operation.UserRole
-	30, // 9: backend.operation.UserAuthData.pastures:type_name -> backend.operation.UserPasture
+	31, // 9: backend.operation.UserAuthData.pastures:type_name -> backend.operation.UserPasture
 	15, // 10: backend.operation.UserDetails.data:type_name -> backend.operation.AddSystemUser
 	13, // 11: backend.operation.AddSystemUser.roles:type_name -> backend.operation.UserRole
-	28, // 12: backend.operation.AddSystemUser.is_show:type_name -> backend.operation.IsShow.Kind
-	28, // 13: backend.operation.SearchUserRequest.is_show:type_name -> backend.operation.IsShow.Kind
-	29, // 14: backend.operation.SearchUserRequest.pagination:type_name -> backend.operation.PaginationModel
+	29, // 12: backend.operation.AddSystemUser.is_show:type_name -> backend.operation.IsShow.Kind
+	29, // 13: backend.operation.SearchUserRequest.is_show:type_name -> backend.operation.IsShow.Kind
+	30, // 14: backend.operation.SearchUserRequest.pagination:type_name -> backend.operation.PaginationModel
 	18, // 15: backend.operation.SearchUserResponse.data:type_name -> backend.operation.SearchUserData
 	15, // 16: backend.operation.SearchUserData.list:type_name -> backend.operation.AddSystemUser
-	28, // 17: backend.operation.IsShowSystemUserRequest.is_show:type_name -> backend.operation.IsShow.Kind
-	28, // 18: backend.operation.AddMenuRequest.is_show:type_name -> backend.operation.IsShow.Kind
+	29, // 17: backend.operation.IsShowSystemUserRequest.is_show:type_name -> backend.operation.IsShow.Kind
+	29, // 18: backend.operation.AddMenuRequest.is_show:type_name -> backend.operation.IsShow.Kind
 	20, // 19: backend.operation.AddMenuRequest.children:type_name -> backend.operation.AddMenuRequest
-	28, // 20: backend.operation.IsShowSystemMenuRequest.is_show:type_name -> backend.operation.IsShow.Kind
-	29, // 21: backend.operation.SearchMenuRequest.pagination:type_name -> backend.operation.PaginationModel
+	29, // 20: backend.operation.IsShowSystemMenuRequest.is_show:type_name -> backend.operation.IsShow.Kind
+	30, // 21: backend.operation.SearchMenuRequest.pagination:type_name -> backend.operation.PaginationModel
 	24, // 22: backend.operation.SearchMenuResponse.data:type_name -> backend.operation.SearchMenuData
 	20, // 23: backend.operation.SearchMenuData.list:type_name -> backend.operation.AddMenuRequest
 	26, // 24: backend.operation.SystemUserMenuPermissions.data:type_name -> backend.operation.SystemUserMenuData
-	31, // 25: backend.operation.SystemUserMenuData.pasture_list:type_name -> backend.operation.AddPastureRequest
+	32, // 25: backend.operation.SystemUserMenuData.pasture_list:type_name -> backend.operation.AddPastureRequest
 	20, // 26: backend.operation.SystemUserMenuData.menu_list:type_name -> backend.operation.AddMenuRequest
-	27, // 27: backend.operation.SystemUserMenuData.mobile_list:type_name -> backend.operation.AddMobileRequest
-	28, // [28:28] is the sub-list for method output_type
-	28, // [28:28] is the sub-list for method input_type
-	28, // [28:28] is the sub-list for extension type_name
-	28, // [28:28] is the sub-list for extension extendee
-	0,  // [0:28] is the sub-list for field type_name
+	28, // 27: backend.operation.SystemUserMenuData.mobile_list:type_name -> backend.operation.AddMobileRequest
+	27, // 28: backend.operation.SystemUserMenuData.menu_buttons_path:type_name -> backend.operation.MenuButtonsPath
+	29, // [29:29] is the sub-list for method output_type
+	29, // [29:29] is the sub-list for method input_type
+	29, // [29:29] is the sub-list for extension type_name
+	29, // [29:29] is the sub-list for extension extendee
+	0,  // [0:29] is the sub-list for field type_name
 }
 
 func init() { file_backend_operation_system_proto_init() }
@@ -2689,6 +2763,18 @@ func file_backend_operation_system_proto_init() {
 			}
 		}
 		file_backend_operation_system_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MenuButtonsPath); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_backend_operation_system_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*AddMobileRequest); i {
 			case 0:
 				return &v.state
@@ -2707,7 +2793,7 @@ func file_backend_operation_system_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_backend_operation_system_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   28,
+			NumMessages:   29,
 			NumExtensions: 0,
 			NumServices:   0,
 		},

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません