Browse Source

dashboard: 数据预警

Yi 1 month ago
parent
commit
780ebc2125

+ 5 - 0
README.md

@@ -31,6 +31,11 @@ lint:
 - 生成 mock 前,请确保你能够编译 & 编译完成
 - make generate
 
+## 初始化系统表
+1. data_warning
+2. data_warning_items
+
+
 todo列表:
 - [x] module/crontab/crontab.go 中119行[Limit(100)] 待优化,case为产后日期类型待测试
 - [ ] 后台添加配种数据时候,不知道该牛只是同期还是自然发情还是人工揭发?

+ 1 - 1
go.mod

@@ -3,7 +3,7 @@ module kpt-pasture
 go 1.17
 
 require (
-	gitee.com/xuyiping_admin/go_proto v0.0.0-20250212025811-9b8f9f1277a3
+	gitee.com/xuyiping_admin/go_proto v0.0.0-20250212100748-0f1f0b733b1a
 	gitee.com/xuyiping_admin/pkg v0.0.0-20241108060137-caea58c59f5b
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/eclipse/paho.mqtt.golang v1.4.3

+ 6 - 0
go.sum

@@ -185,6 +185,12 @@ gitee.com/xuyiping_admin/go_proto v0.0.0-20250211132718-87536ed342b5 h1:Bt+bNBIY
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250211132718-87536ed342b5/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250212025811-9b8f9f1277a3 h1:J6mQy1zYbBeFDdCECev2DS1HEAnuroTfC4EP4FtzmFA=
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250212025811-9b8f9f1277a3/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250212081438-4cafb1f562a2 h1:Ctl4GEYsQoykkNvWM9RjV7qF5OntEzTU0smigA5iX2M=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250212081438-4cafb1f562a2/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250212083935-9847adaab5cc h1:MENa5LOICsSJ4cRl81+SZJyWq1xxUVZikKJlF3uwsKo=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250212083935-9847adaab5cc/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250212100748-0f1f0b733b1a h1:R8ys1+phiHtAWETRRb88/WWGheExHA6MnH89LXwGVz0=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250212100748-0f1f0b733b1a/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
 gitee.com/xuyiping_admin/pkg v0.0.0-20241108060137-caea58c59f5b h1:w05MxH7yqveRlaRbxHhbif5YjPrJFodRPfOjYhXn7Zk=
 gitee.com/xuyiping_admin/pkg v0.0.0-20241108060137-caea58c59f5b/go.mod h1:8tF25X6pE9WkFCczlNAC0K2mrjwKvhhp02I7o0HtDxY=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

+ 28 - 0
http/handler/dashboard/dashboard.go

@@ -65,3 +65,31 @@ func FocusIndicatorsSet(c *gin.Context) {
 		Data: &operationPb.Success{Success: true},
 	})
 }
+
+func DataWarningSet(c *gin.Context) {
+	var req pasturePb.IndexDataWarningSetRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := middleware.Dependency(c).StoreEventHub.OpsService.DataWarningSet(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 DataWarningList(c *gin.Context) {
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.DataWarningList(c)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}

+ 13 - 0
http/handler/test.go

@@ -69,3 +69,16 @@ func UpdateCowPen(c *gin.Context) {
 		Data: &operationPb.Success{Success: true},
 	})
 }
+
+func DataWarning(c *gin.Context) {
+	if err := middleware.BackendOperation(c).OpsService.TestDataWaring(c); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, &operationPb.CommonOK{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &operationPb.Success{Success: true},
+	})
+}

+ 2 - 0
http/route/dashboard_api.go

@@ -17,5 +17,7 @@ func DashboardApi(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		dashboardRoute.GET("/neck_ring/warning", dashboard.NeckRingWarning)
 		dashboardRoute.GET("/focus/indicators/:dimension", dashboard.FocusIndicators)
 		dashboardRoute.POST("/focus/indicators/set", dashboard.FocusIndicatorsSet)
+		dashboardRoute.GET("/data/warning/list", dashboard.DataWarningList)
+		dashboardRoute.POST("/data/warning/set", dashboard.DataWarningSet)
 	}
 }

+ 1 - 0
http/route/test_api.go

@@ -16,5 +16,6 @@ func TestAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		testRoute.POST("/cow/neck_ring/bound", handler.CowNeckRingBound)
 		testRoute.POST("/cow/neck_ring/bound2", handler.CowNeckRingBound2)
 		testRoute.POST("/cow/pen/update", handler.UpdateCowPen)
+		testRoute.GET("/data/warning", handler.DataWarning)
 	}
 }

+ 3 - 0
model/cow.go

@@ -42,6 +42,7 @@ type Cow struct {
 	DepartureAt         int64                          `json:"departureAt"`         // 离场时间
 	FirstMatingAt       int64                          `json:"firstMatingAt"`       // 首次配种时间
 	MatingTimes         int32                          `json:"matingTimes"`         // 配种次数
+	AbortionTimes       int32                          `json:"abortionTimes"`       // 流产次数
 	WeeklyActive        int32                          `json:"weeklyActive"`        // 每周活跃度
 	LastEstrusAt        int64                          `json:"lastEstrusAt"`        // 最后一次发情时间
 	LastCalvingAt       int64                          `json:"lastCalvingAt"`       // 最后一次产犊时间
@@ -77,6 +78,7 @@ func (c *Cow) EventCalvingUpdate(calvingAt int64) {
 	c.Lact += 1
 	c.MatingTimes = 0
 	c.PregnancyAge = 0
+	c.AbortionTimes = 0
 	c.BreedStatus = pasturePb.BreedStatus_Calving
 	c.IsPregnant = pasturePb.IsShow_No
 	c.LastCalvingAt = calvingAt
@@ -103,6 +105,7 @@ func (c *Cow) EventAbortionUpdate(abortionAt int64) {
 	c.IsPregnant = pasturePb.IsShow_No
 	c.LastAbortionAt = abortionAt
 	c.BreedStatus = pasturePb.BreedStatus_Abort
+	c.AbortionTimes += 1
 }
 
 // EventWeightUpdate 称重更新

+ 40 - 0
model/data_waring.go

@@ -0,0 +1,40 @@
+package model
+
+import pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+
+const DefaultUserId = 0
+
+type DataWarning struct {
+	Id                int64  `json:"id"`
+	UserId            int64  `json:"userId"`
+	Kind              string `json:"kind"`
+	Name              string `json:"name"`
+	Description       string `json:"description"`
+	DataValue         string `json:"dataValue"`
+	DataUpdateAt      int64  `json:"dataUpdateAt"`
+	ConditionUpdateAt int64  `json:"conditionUpdateAt"`
+	CreatedAt         int64  `json:"createdAt"`
+	UpdatedAt         int64  `json:"updatedAt"`
+}
+
+func (d *DataWarning) TableName() string {
+	return "data_warning"
+}
+
+func NewDataWarningList(userId int64, req []*pasturePb.WarningDataSet, warningMap map[string]*DataWarning) []*DataWarning {
+	res := make([]*DataWarning, 0)
+	for _, v := range req {
+		defaultDataWarning := warningMap[v.Kind]
+		res = append(res, NewDataWarning(userId, v.Kind, defaultDataWarning))
+	}
+	return res
+}
+
+func NewDataWarning(userId int64, Kind string, defaultDataWarning *DataWarning) *DataWarning {
+	return &DataWarning{
+		UserId:      userId,
+		Kind:        Kind,
+		Name:        defaultDataWarning.Name,
+		Description: defaultDataWarning.Description,
+	}
+}

+ 17 - 0
model/data_warning_items.go

@@ -0,0 +1,17 @@
+package model
+
+type DataWarningItems struct {
+	Id        int64  `json:"id"`
+	WarningId int64  `json:"warningId"`
+	GroupId   int32  `json:"groupId"`
+	FieldName string `json:"fieldName"`
+	FieldDesc string `json:"fieldDesc"`
+	Operator  string `json:"operator"`
+	Value     string `json:"value"`
+	CreatedAt int64  `json:"createdAt"`
+	UpdatedAt int64  `json:"updatedAt"`
+}
+
+func (d *DataWarningItems) TableName() string {
+	return "data_warning_items"
+}

+ 1 - 1
module/backend/analysis_other.go

@@ -380,7 +380,7 @@ func (s *StoreEntry) query1(pastureId int64, cowType pasturePb.CowType_Kind, sta
 			pasturePb.HealthStatus_Dead, pasturePb.HealthStatus_Curable, pasturePb.HealthStatus_Curable, pasturePb.HealthStatus_Curable,
 		).
 		Where("diagnosed_result = ?", pasturePb.IsShow_Ok).
-		Where("pastureI_id = ?", pastureId).
+		Where("pasture_id = ?", pastureId).
 		Where("diagnosed_at BETWEEN ? AND ?", startDayTimeUnix, endDayTimeUnix)
 	if cowType > 0 {
 		pref.Where("cow_type = ?", cowType)

+ 118 - 0
module/backend/dashboard.go

@@ -2,6 +2,7 @@ package backend
 
 import (
 	"context"
+	"errors"
 	"kpt-pasture/model"
 	"kpt-pasture/util"
 	"net/http"
@@ -9,6 +10,8 @@ import (
 	"strings"
 	"time"
 
+	"gorm.io/gorm"
+
 	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
 	"go.uber.org/zap"
 
@@ -184,3 +187,118 @@ func (s *StoreEntry) FocusIndicatorsSet(ctx context.Context, req *pasturePb.Inde
 	}
 	return nil
 }
+
+func (s *StoreEntry) DataWarningSet(ctx context.Context, req *pasturePb.IndexDataWarningSetRequest) error {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+	if len(req.WarningDataSet) <= 0 {
+		return nil
+	}
+	userDataWarningList := make([]*model.DataWarning, 0)
+	if err = s.DB.Model(new(model.DataWarning)).
+		Where("user_id = ?", userModel.SystemUser.Id).
+		Find(&userDataWarningList).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+
+		} else {
+			return xerr.WithStack(err)
+		}
+	}
+	/*for _, v := range userDataWarningList {
+
+	}*/
+	return nil
+}
+
+func (s *StoreEntry) DataWarningList(ctx context.Context) (*pasturePb.IndexDataWarningResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	defaultUserDataWarningList, err := s.DefaultDataWarning(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	currentUserDataWarningList := make([]*model.DataWarning, 0)
+	if err = s.DB.Model(new(model.DataWarning)).
+		Where("user_id = ?", userModel.SystemUser.Id).
+		Find(&currentUserDataWarningList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	// 如果用户没有配置自己的预警数据,则使用默认数据
+	if len(currentUserDataWarningList) <= 0 {
+		currentUserDataWarningList = defaultUserDataWarningList
+	}
+
+	if len(currentUserDataWarningList) <= 0 {
+		return nil, xerr.Custom("当前用户未配置预警数据,请联系管理员!")
+	}
+
+	warningIds := make([]int64, 0)
+	warningMap := make(map[int64]*model.DataWarning)
+	dataShow := make([]*pasturePb.WarningDataShow, 0)
+	for _, warningData := range currentUserDataWarningList {
+		dataUpdateTimeFormat := ""
+		if warningData.DataUpdateAt > 0 {
+			dataUpdateTimeFormat = time.Unix(warningData.DataUpdateAt, 0).Format(model.LayoutTime)
+		}
+		dataShow = append(dataShow, &pasturePb.WarningDataShow{
+			Name:                 warningData.Name,
+			Number:               warningData.DataValue,
+			Describe:             warningData.Description,
+			DataUpdateTimeFormat: dataUpdateTimeFormat,
+		})
+		warningMap[warningData.Id] = warningData
+		// 如果预警数据更新时间大于预警条件更新时间,则跳过
+		if warningData.DataUpdateAt > warningData.ConditionUpdateAt {
+			continue
+		}
+		warningIds = append(warningIds, warningData.Id)
+
+	}
+
+	dataWaningItems := make([]*model.DataWarningItems, 0)
+	if err = s.DB.Model(new(model.DataWarningItems)).
+		Where("warning_id in (?)", warningIds).
+		Find(&dataWaningItems).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	dataSet := make([]*pasturePb.WarningDataSet, 0)
+	for _, v := range dataWaningItems {
+		warningData := warningMap[v.WarningId]
+		dataSet = append(dataSet, &pasturePb.WarningDataSet{
+			WarningId: int32(v.WarningId),
+			GroupId:   v.GroupId,
+			Name:      warningData.Name,
+			FieldDesc: v.FieldDesc,
+			Operator:  v.Operator,
+			Value:     v.Value,
+			Kind:      warningData.Kind,
+			IsShow:    pasturePb.IsShow_Ok,
+		})
+	}
+
+	return &pasturePb.IndexDataWarningResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &pasturePb.DataWarning{
+			DataSet:  dataSet,
+			DataShow: dataShow,
+		},
+	}, nil
+}
+
+func (s *StoreEntry) DefaultDataWarning(ctx context.Context) ([]*model.DataWarning, error) {
+	defaultUserDataWarningList := make([]*model.DataWarning, 0)
+	if err := s.DB.Model(new(model.DataWarning)).
+		Where("user_id = ?", model.DefaultUserId).
+		Find(&defaultUserDataWarningList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	return defaultUserDataWarningList, nil
+}

+ 62 - 0
module/backend/data_warning.go

@@ -0,0 +1,62 @@
+package backend
+
+import (
+	"context"
+	"fmt"
+	"kpt-pasture/model"
+	"strings"
+
+	"gitee.com/xuyiping_admin/pkg/xerr"
+)
+
+func (s *StoreEntry) TestDataWaring(ctx context.Context) error {
+	a, params, err := s.buildQuery(1)
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+	var count int64
+	if err = s.DB.Model(new(model.Cow)).Where(a, params...).Count(&count).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}
+
+func (s *StoreEntry) buildQuery(ruleId int64) (string, []interface{}, error) {
+	conditionsMap := make(map[int32][]string)
+	params := make([]interface{}, 0)
+	res := make([]*model.DataWarningItems, 0)
+	if err := s.DB.Model(new(model.DataWarningItems)).
+		Where("rule_id = ?", ruleId).
+		Order("group_id").
+		Find(&res).Error; err != nil {
+		return "", nil, xerr.WithStack(err)
+	}
+
+	for _, v := range res {
+		conditionsMap[v.GroupId] = append(conditionsMap[v.GroupId], fmt.Sprintf(" %s %s ? ", v.FieldName, v.Operator))
+		params = append(params, v.Value)
+	}
+
+	if len(conditionsMap) == 0 {
+		return "", nil, xerr.Custom("条件组不能为空")
+	}
+
+	var sqlConditions []string
+	for _, groupConditions := range conditionsMap {
+		if len(groupConditions) == 0 {
+			continue // 跳过空组
+		}
+
+		// 同一个组内的条件用 OR 拼接
+		groupQuery := strings.Join(groupConditions, " OR ")
+
+		// 如果组内有多个条件,用括号包裹
+		if len(groupConditions) > 1 {
+			groupQuery = fmt.Sprintf("(%s)", groupQuery)
+		}
+
+		sqlConditions = append(sqlConditions, groupQuery)
+	}
+
+	return strings.Join(sqlConditions, " AND "), params, nil
+}

+ 2 - 2
module/backend/event_cow_log.go

@@ -83,9 +83,9 @@ func (s *StoreEntry) SubmitEventLog(
 	case pasturePb.EventType_Estrus:
 		data := req.(*model.EventEstrus)
 		eventAt = data.PlanDay
-		isMating := ""
+		isMating := ""
 		if data.IsMating == pasturePb.IsShow_Ok {
-			isMating = ""
+			isMating = ""
 		}
 		desc = fmt.Sprintf("发情揭发方式:%s;是否配种:%s;", s.ExposeEstrusTypeMap()[exposeEstrusType], isMating)
 		operationUser.Id = int64(data.OperationId)

+ 3 - 0
module/backend/interface.go

@@ -263,6 +263,8 @@ type DashboardService interface {
 	NeckRingWarning(ctx context.Context) (*pasturePb.IndexNeckRingResponse, error)
 	FocusIndicatorsList(ctx context.Context, dimension string) (*pasturePb.IndexFocusIndicatorsResponse, error)
 	FocusIndicatorsSet(ctx context.Context, req *pasturePb.IndexFocusIndicatorsSetRequest) error
+	DataWarningSet(ctx context.Context, req *pasturePb.IndexDataWarningSetRequest) error
+	DataWarningList(ctx context.Context) (*pasturePb.IndexDataWarningResponse, error)
 }
 
 //go:generate mockgen -destination mock/WorkService.go -package kptservicemock kpt-pasture/module/backend WorkService
@@ -292,4 +294,5 @@ type TestService interface {
 	CowNeckRingNumberBound(ctx context.Context, pagination *pasturePb.PaginationModel) error
 	CowNeckRingNumberBound2(ctx context.Context, pagination *pasturePb.PaginationModel) error
 	UpdateCowPen(ctx context.Context, pagination *pasturePb.PaginationModel) error
+	TestDataWaring(ctx context.Context) error
 }