Ver código fonte

analysis: CowBehaviorDistribution 牛只行为曲线分布

Yi 1 mês atrás
pai
commit
46733c5c23

+ 18 - 18
config/app.develop.yaml

@@ -36,25 +36,25 @@ side_work_setting:
       default: 5
 cron:
   crontab_start_run: false
-  update_cow_info: "0 01 1 * * ?"       # 每天凌晨1点01分执行
-  indicators: "0 30 11 * * ?"            # 每天凌晨1点03分执行
-  generate_work_order: "0 05 1 * * ?"   # 每天凌晨1点05分执行
-  immunization_plan: "0 10 1 * * ?"     # 每天凌晨1点10分执行
-  same_time_plan: "0 15 1 * * ?"        # 每天凌晨1点15分执行
-  update_same_time: "0 20 1 * * ?"      # 每天凌晨1点20分执行
-  system_basic_crontab: "0 25 1 * * ?"  # 每天凌晨1点25分执行
+  update_cow_info: "0 01 1 * * ?"
+  indicators: "0 0 23 * * ?"            # 每天凌晨1点03分执行
+  generate_work_order: "0 05 1 * * ?"
+  immunization_plan: "0 10 1 * * ?"
+  same_Time_plan: "0 15 1 * * ?"
+  update_same_time: "0 20 1 * * ?"
   delete_old_original: "0 30 1 * * ?"   # 每天凌晨1点30分执行
-  update_disease_to_calendar: "0 05 1 * * ?" # 每天凌晨1点05分执行
-  cow_pregnant: "0 00 15 * * ?"         # 每天15点执行
-  neck_ring_estrus: "0 45 * * * ?"      # 更新脖环发情数据
-  neck_ring_merge: "*/5 * * * * ?"      # 合并脖环原始2小时数据(5分钟)
-  neck_ring_calculate: "*/10 * * * * ?" # 计算脖环数据
-  neck_ring_estrus_warning: "*/50 * * * * ?"   # 脖环预警(每50分钟执行一次
-  neck_ring_health_warning: "*/50 * * * * ?"   # 脖环预警(每50分钟执行一次
-  update_pen_behavior: "0 45 * * * ?"  # 更新栏舍行行为数据
-  update_pen_behavior_daily: "0 05 2 * * ?"  # 更新栏舍饲养监测数据
-  update_milk_original: "0 */30 * * * ?"     # 更新奶厅原始数据(每30分钟执行一次
-  insert_milk_daily: "0 05 2 * * ?"          # 更新每日奶量和活动量数据
+  system_basic_crontab: "0 35 1 * * ?"
+  update_disease_to_calendar: "0 50 1 * * ?"
+  cow_pregnant: "0 01 15 * * ?"
+  neck_ring_estrus: "0 45 * * * *"     # 每小时的45分钟执行一次
+  neck_ring_merge: "*/60 * * * * ?"      # 合并脖环原始2小时数据(5分钟)
+  neck_ring_calculate: "*/300 * * * * ?"  # 计算脖环数据
+  neck_ring_estrus_warning: "* */30 * * * ?"   # 脖环发情预警(每50分钟执行一次
+  neck_ring_health_warning: "* */30 * * * ?"   # 脖环健康预警
+  update_pen_behavior: "0 */20 * * * ?"        # 更新栏舍行为数据(20分钟执行一次)
+  update_pen_behavior_daily: "0 50 15 * * ?"  # 更新栏舍饲养监测数据
+  update_milk_original: "0 */30 * * * ?"     # 更新奶厅原始数据(每30分钟执行一次)
+  insert_milk_daily: "0 05 02 * * ?"          # 更新每日奶量和活动量数据
 
 mqtt:
   broker: "kptyun.com:1983"

+ 15 - 11
config/app.test.yaml

@@ -26,20 +26,24 @@ cache_key_suffix: "gmym"
 cron:
   crontab_start_run: false
   update_cow_info: "0 01 1 * * ?"
-  indicators: "0 24 10 * * ?"
+  indicators: "0 0 23 * * ?"            # 每天凌晨1点03分执行
   generate_work_order: "0 05 1 * * ?"
   immunization_plan: "0 10 1 * * ?"
-  same_time_plan: "0 15 1 * * ?"
+  same_Time_plan: "0 15 1 * * ?"
   update_same_time: "0 20 1 * * ?"
-  system_basic_crontab: "0 25 1 * * ?"
-  cow_pregnant: "0 00 15 * * ?"
-  neck_ring_estrus: "0 45 * * * ?"      # 更新脖环发情数据
-  neck_ring_merge: "*/5 * * * * ?"      # 合并脖环原始2小时数据(5分钟)
-  neck_ring_calculate: "*/10 * * * * ?"  # 计算脖环数据
-  neck_ring_estrus_warning: "*/50 * * * * ?"   # 脖环预警(每50分钟执行一次
-  neck_ring_health_warning: "*/50 * * * * ?"   # 脖环预警(每50分钟执行一次
-  update_pen_behavior: "0 45 * * * ?"  # 更新栏舍行行为数据
-  update_pen_behavior_daily: "0 05 2 * * ?"  # 更新栏舍饲养监测数据
+  delete_old_original: "0 30 1 * * ?"   # 每天凌晨1点30分执行
+  system_basic_crontab: "0 35 1 * * ?"
+  update_disease_to_calendar: "0 50 1 * * ?"
+  cow_pregnant: "0 01 15 * * ?"
+  neck_ring_estrus: "0 45 * * * *"     # 每小时的45分钟执行一次
+  neck_ring_merge: "*/60 * * * * ?"      # 合并脖环原始2小时数据(5分钟)
+  neck_ring_calculate: "*/300 * * * * ?"  # 计算脖环数据
+  neck_ring_estrus_warning: "* */30 * * * ?"   # 脖环发情预警(每50分钟执行一次
+  neck_ring_health_warning: "* */30 * * * ?"   # 脖环健康预警
+  update_pen_behavior: "0 */20 * * * ?"        # 更新栏舍行为数据(20分钟执行一次)
+  update_pen_behavior_daily: "0 50 15 * * ?"  # 更新栏舍饲养监测数据
+  update_milk_original: "0 */30 * * * ?"     # 更新奶厅原始数据(每30分钟执行一次)
+  insert_milk_daily: "0 05 02 * * ?"          # 更新每日奶量和活动量数据
 
 mqtt:
   broker: "kptyun.com:1983"

+ 1 - 1
go.mod

@@ -3,7 +3,7 @@ module kpt-pasture
 go 1.17
 
 require (
-	gitee.com/xuyiping_admin/go_proto v0.0.0-20250425013204-c2e3c2cb22b5
+	gitee.com/xuyiping_admin/go_proto v0.0.0-20250425035842-15ee838b15e8
 	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

@@ -64,6 +64,12 @@ gitee.com/xuyiping_admin/go_proto v0.0.0-20250425011443-a83a84c81b7d h1:/2tQo6Ln
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250425011443-a83a84c81b7d/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250425013204-c2e3c2cb22b5 h1:9HdkH0TLT69nYrSpVNpK+AqRsEj6zsSAI0wyn3txEJo=
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250425013204-c2e3c2cb22b5/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250425022013-aea428d7468b h1:HsSfifHMvaNok/W1Q22axMJuYDSkpFnpFAIoO4Qo0NA=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250425022013-aea428d7468b/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250425025152-491564f7b245 h1:WSBs/sMmKJBm+GBlFrvlAy9hYDgnxOSo9sFFsqdkvpo=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250425025152-491564f7b245/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250425035842-15ee838b15e8 h1:8EsgPwBQn44Ntxxk8Ysat0IAAlFaEGwl/fszj38I+wc=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250425035842-15ee838b15e8/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=

+ 50 - 1
http/handler/analysis/analysis.go

@@ -336,6 +336,31 @@ func PenBehaviorAnalysis(c *gin.Context) {
 		return
 	}
 
+	ginutil.JSONResp(c, res)
+}
+
+func PenBehaviorAnalysis2(c *gin.Context) {
+	var req pasturePb.BarnBehaviorCurveRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.StartAt, valid.Required),
+		valid.Field(&req.EndAt, valid.Required),
+		valid.Field(&req.PenId, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.PenBehavior2(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
 	c.JSON(http.StatusOK, res)
 }
 
@@ -360,5 +385,29 @@ func PenBehaviorDaily(c *gin.Context) {
 		return
 	}
 
-	c.JSON(http.StatusOK, res)
+	ginutil.JSONResp(c, res)
+}
+
+func CowBehaviorDistribution(c *gin.Context) {
+	var req pasturePb.CowBehaviorDistributionRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.DateTime, valid.Required),
+		valid.Field(&req.BehaviorKind, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.CowBehaviorDistribution(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, res)
 }

+ 4 - 2
http/route/analysis_api.go

@@ -27,7 +27,9 @@ func AnalysisAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		analysisRoute.POST("/single/factor/pregnant/report", analysis.SingleFactorInfantSurvivalRate) // 单因素受胎率
 		analysisRoute.POST("/multi/factor/pregnant/report", analysis.MultiFactorInfantSurvivalRate)   // 多因素受胎率
 
-		analysisRoute.POST("/pen/behavior", analysis.PenBehaviorAnalysis)      // 栏舍行为数据
-		analysisRoute.POST("/pen/behavior/monitor", analysis.PenBehaviorDaily) // 栏舍饲喂监测
+		analysisRoute.POST("/pen/behavior", analysis.PenBehaviorAnalysis)                  // 栏舍行为数据
+		analysisRoute.POST("/pen/behavior2", analysis.PenBehaviorAnalysis2)                // 栏舍行为数据
+		analysisRoute.POST("/pen/behavior/monitor", analysis.PenBehaviorDaily)             // 栏舍饲喂监测
+		analysisRoute.POST("/cow/behavior/distribution", analysis.CowBehaviorDistribution) // 牛只行为分布
 	}
 }

+ 7 - 1
model/cow.go

@@ -703,7 +703,13 @@ func (c *Cow) GetAbortionAge() int32 {
 
 // GetLactationAge 泌乳天数
 func (c *Cow) GetLactationAge() int32 {
-	if c.MilkKind == pasturePb.CowMilk_Lactation {
+	if c.LastCalvingAt <= 0 {
+		return 0
+	}
+
+	if c.MilkKind == pasturePb.CowMilk_Lactation ||
+		c.BreedStatus == pasturePb.BreedStatus_Calving ||
+		c.BreedStatus == pasturePb.BreedStatus_Abort {
 		return int32(math.Floor(float64(time.Now().Local().Unix()-c.LastCalvingAt) / 86400))
 	}
 	return c.LactationAge

+ 61 - 0
model/pen_behavior.go

@@ -1,6 +1,7 @@
 package model
 
 import (
+	"fmt"
 	"kpt-pasture/util"
 
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
@@ -63,6 +64,12 @@ func NewPenBehavior(data *PenBehaviorData, activeTime string) *PenBehavior {
 
 type PenBehaviorSlice []*PenBehavior
 
+type BarnBehaviorCurveResponse struct {
+	Code int32                  `json:"code"`
+	Msg  string                 `json:"msg"`
+	Data *BarnBehaviorCurveItem `json:"data"`
+}
+
 func (p PenBehaviorSlice) ToPB() *pasturePb.BarnBehaviorCurveItem {
 	res := &pasturePb.BarnBehaviorCurveItem{
 		EventTime: &pasturePb.EventTime{
@@ -113,3 +120,57 @@ type PenBehaviorData struct {
 	RestRate   int32  `json:"restRate"`
 	GaspRate   int32  `json:"gaspRate"`
 }
+
+type BarnBehaviorCurveItem struct {
+	EventTime     *EventTime `json:"eventTime"`
+	DateTime      []string   `json:"dateTime"`
+	Headers       []string   `json:"headers"`
+	Rumina        []int32    `json:"rumina"`
+	Intake        []int32    `json:"intake"`
+	Rest          []int32    `json:"rest"`
+	WeekAvgRumina [][]string `json:"weekAvgRumina"`
+	WeekAvgIntake [][]string `json:"weekAvgIntake"`
+	WeekAvgReset  [][]string `json:"weekAvgReset"`
+}
+
+type EventTime struct {
+	FeedTime []string `json:"feedTime"`
+	MilkTime []string `json:"milkTime"`
+}
+
+func (p PenBehaviorSlice) ToPB2() *BarnBehaviorCurveItem {
+	res := &BarnBehaviorCurveItem{
+		EventTime: &EventTime{
+			FeedTime: make([]string, 0),
+			MilkTime: make([]string, 0),
+		},
+		DateTime:      make([]string, 0),
+		Rumina:        make([]int32, 0),
+		Intake:        make([]int32, 0),
+		Rest:          make([]int32, 0),
+		WeekAvgRumina: make([][]string, 0),
+		WeekAvgIntake: make([][]string, 0),
+		WeekAvgReset:  make([][]string, 0),
+	}
+
+	for i, v := range p {
+		dateTime := ""
+		if v.ActiveTime != "" {
+			dt, _ := util.TimeParseLocal(LayoutTime, v.ActiveTime)
+			dateTime = dt.Format(LayoutMinute)
+		}
+		res.DateTime = append(res.DateTime, dateTime)
+		res.Rumina = append(res.Rumina, v.RuminaRate)
+		res.Intake = append(res.Intake, v.IntakeRate)
+		res.Rest = append(res.Rest, v.RestRate)
+		weekRuminaRate, weekIntakeRate, weekRestRate := make([]string, 0), make([]string, 0), make([]string, 0)
+		weekRuminaRate = append(weekRuminaRate, v.ActiveTime, fmt.Sprintf("%d", v.WeekRuminaRate), fmt.Sprintf("%d", v.PenId), v.HeatDate, fmt.Sprintf("%d", v.Frameid), fmt.Sprintf("%d", v.RuminaStd))
+		weekRuminaRate = append(weekRuminaRate, v.ActiveTime, fmt.Sprintf("%d", v.WeekIntakeRate), fmt.Sprintf("%d", v.PenId), v.HeatDate, fmt.Sprintf("%d", v.Frameid), fmt.Sprintf("%d", v.IntakeStd))
+		weekRestRate = append(weekRestRate, v.ActiveTime, fmt.Sprintf("%d", v.WeekRestRate), fmt.Sprintf("%d", v.PenId), v.HeatDate, fmt.Sprintf("%d", v.Frameid), fmt.Sprintf("%d", v.RestStd))
+		res.WeekAvgRumina[i] = append(res.WeekAvgRumina[i], weekRuminaRate...)
+		res.WeekAvgIntake[i] = append(res.WeekAvgIntake[i], weekIntakeRate...)
+		res.WeekAvgReset[i] = append(res.WeekAvgReset[i], weekRestRate...)
+	}
+
+	return res
+}

+ 8 - 8
model/pen_behavior_day.go

@@ -9,15 +9,15 @@ type PenBehaviorDay struct {
 	PenId       int32   `json:"penId"`
 	PenName     string  `json:"penName"`
 	CowCount    int32   `json:"cowCount"`
-	DayAvgMilk  float32 `json:"dayAvgMilk"`
+	DayMilk     float32 `json:"dayMilk"`
 	DayHigh     int32   `json:"dayHigh"`
 	DayRumina   int32   `json:"dayRumina"`
 	DayIntake   int32   `json:"dayIntake"`
 	DayInactive int32   `json:"dayInactive"`
 	DayGasp     int32   `json:"dayGasp"`
 	DayActive   int32   `json:"dayActive"`
-	WeekAvgMilk float32 `json:"weekAvgMilk"`
-	RuminaStd   int32   `json:"ruminaStd"`
+	WeekMilk    float32 `json:"weekMilk"`
+	RuminaStd   float64 `json:"ruminaStd"`
 	CreatedAt   int64   `json:"createdAt"`
 	UpdatedAt   int64   `json:"updatedAt"`
 }
@@ -28,21 +28,21 @@ func (p *PenBehaviorDay) TableName() string {
 
 func NewPenBehaviorDay(pastureId int64, heatDate string, penId int32, penName string, cowCount int32, dayAvgMilk float32,
 	dayHigh int32, dayRumina int32, dayIntake int32, dayInactive int32, dayGasp int32, dayActive int32, weekAvgMilk float32,
-	ruminaStd int32) *PenBehaviorDay {
+	ruminaStd float64) *PenBehaviorDay {
 	return &PenBehaviorDay{
 		PastureId:   pastureId,
 		HeatDate:    heatDate,
 		PenId:       penId,
 		PenName:     penName,
 		CowCount:    cowCount,
-		DayAvgMilk:  dayAvgMilk,
+		DayMilk:     dayAvgMilk,
 		DayHigh:     dayHigh,
 		DayRumina:   dayRumina,
 		DayIntake:   dayIntake,
 		DayInactive: dayInactive,
 		DayGasp:     dayGasp,
 		DayActive:   dayActive,
-		WeekAvgMilk: weekAvgMilk,
+		WeekMilk:    weekAvgMilk,
 		RuminaStd:   ruminaStd,
 	}
 }
@@ -62,7 +62,7 @@ type PenBehaviorDayModel struct {
 	DayChew       int32   `json:"dayChew"`
 	DayActive     int32   `json:"dayActive"`
 	DayImmobility int32   `json:"dayImmobility"`
-	RuminaStd     int32   `json:"ruminaStd"`
+	RuminaStd     float64 `json:"ruminaStd"`
 }
 
 type PenBehaviorDayModelSlice []*PenBehaviorDayModel
@@ -88,7 +88,7 @@ func (p PenBehaviorDayModelSlice) ToPB(dataTimeRange []string) *pasturePb.BarnMo
 				res.DayIntake = append(res.DayIntake, v.DayIntake)
 				res.DayImmobility = append(res.DayImmobility, v.DayImmobility)
 				res.DayChew = append(res.DayChew, v.DayChew)
-				res.DayStd = append(res.DayStd, v.RuminaStd)
+				res.DayStd = append(res.DayStd, int32(v.RuminaStd))
 			}
 		}
 	}

+ 203 - 0
module/backend/analysis_more.go

@@ -2,9 +2,11 @@ package backend
 
 import (
 	"context"
+	"fmt"
 	"kpt-pasture/model"
 	"kpt-pasture/util"
 	"net/http"
+	"sort"
 	"time"
 
 	"gitee.com/xuyiping_admin/pkg/xerr"
@@ -39,6 +41,33 @@ func (s *StoreEntry) PenBehavior(ctx context.Context, req *pasturePb.BarnBehavio
 	}, nil
 }
 
+func (s *StoreEntry) PenBehavior2(ctx context.Context, req *pasturePb.BarnBehaviorCurveRequest) (*model.BarnBehaviorCurveResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	if req.StartAt == 0 || req.EndAt == 0 || req.EndAt < req.StartAt {
+		return nil, xerr.Customf("时间范围错误")
+	}
+	startTime := time.Unix(int64(req.StartAt), 0).Local().Format(model.LayoutDate2)
+	endTime := time.Unix(int64(req.EndAt), 0).Local().Format(model.LayoutDate2)
+	penBehaviorList := make([]*model.PenBehavior, 0)
+	if err = s.DB.Model(new(model.PenBehavior)).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("pen_id = ?", req.PenId).
+		Where("heat_date BETWEEN ? AND ?", startTime, endTime).
+		Find(&penBehaviorList).Error; err != nil {
+		return nil, err
+	}
+
+	return &model.BarnBehaviorCurveResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: model.PenBehaviorSlice(penBehaviorList).ToPB2(),
+	}, nil
+}
+
 func (s *StoreEntry) PenBehaviorDaily(ctx context.Context, req *pasturePb.BarnMonitorRequest) (*pasturePb.BarnMonitorResponse, error) {
 	userModel, err := s.GetUserModel(ctx)
 	if err != nil {
@@ -82,3 +111,177 @@ func (s *StoreEntry) PenBehaviorDaily(ctx context.Context, req *pasturePb.BarnMo
 		Data: model.PenBehaviorDayModelSlice(penBehaviorDayModelList).ToPB(dataTimeRange),
 	}, err
 }
+
+func (s *StoreEntry) CowBehaviorDistribution(ctx context.Context, req *pasturePb.CowBehaviorDistributionRequest) (*pasturePb.CowBehaviorDistributionResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	// 校验时间必须比当天时间小一天
+	if time.Now().Local().Format(model.LayoutDate2) == req.DateTime {
+		return nil, xerr.Customf("时间范围错误")
+	}
+
+	milkDailList := make([]*model.MilkDaily, 0)
+	pref := s.DB.Table(fmt.Sprintf("%s as a", new(model.Cow).TableName())).
+		Joins(fmt.Sprintf("JOIN %s AS b on a.id = b.cow_id", new(model.MilkDaily).TableName())).
+		Select("b.*").
+		Where("a.pasture_id = ?", userModel.AppPasture.Id).
+		Where("a.neck_ring_number != ?", "").
+		Where("a.sex = ?", pasturePb.Genders_Female).
+		Where("b.heat_date = ? ", req.DateTime).
+		Where("b.day_high > ?", 0)
+
+	if len(req.PenIds) > 0 {
+		pref.Where("a.pen_id IN (?)", req.PenIds)
+	}
+
+	if err = pref.Order("b.breed_status,b.lactation_age").
+		Find(&milkDailList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	// 未配 空怀 怀孕 配种
+	data := &pasturePb.CowBehaviorDistributionItem{
+		Headers:    make([]string, 0),
+		Color:      make([]string, 0),
+		MedianLine: make(map[string]float32),
+		CalvingAge: make([]int32, 0),
+		UnBreed:    make([]*pasturePb.CowBehaviorData, 0),
+		Breed:      make([]*pasturePb.CowBehaviorData, 0),
+		Pregnant:   make([]*pasturePb.CowBehaviorData, 0),
+		Empty:      make([]*pasturePb.CowBehaviorData, 0),
+	}
+
+	breedStatus := s.BreedStatusEnumList()
+	for _, v := range breedStatus {
+		if v.Value == int32(pasturePb.BreedStatus_Abort) ||
+			v.Value == int32(pasturePb.BreedStatus_Calving) ||
+			v.Value == int32(pasturePb.BreedStatus_No_Mating) ||
+			v.Value == int32(pasturePb.BreedStatus_Invalid) {
+			continue
+		}
+		data.Headers = append(data.Headers, v.Label)
+		switch v.Label {
+		case "未配":
+			data.Color = append(data.Color, "#b53827")
+		case "空怀":
+			data.Color = append(data.Color, "#2784b5")
+		case "怀孕":
+			data.Color = append(data.Color, "#2757b5")
+		case "配种":
+			data.Color = append(data.Color, "#27b560")
+		}
+	}
+
+	if len(milkDailList) <= 0 {
+		return &pasturePb.CowBehaviorDistributionResponse{
+			Code: http.StatusOK,
+			Msg:  "ok",
+			Data: data,
+		}, nil
+	}
+
+	for _, v := range milkDailList {
+		dayData := int32(0)
+		switch req.BehaviorKind {
+		case pasturePb.Behavior_Rumina:
+			dayData = v.DayRumina
+		case pasturePb.Behavior_Intake:
+			dayData = v.DayIntake
+		case pasturePb.Behavior_Reset:
+			dayData = v.DayInactive
+		case pasturePb.Behavior_Immobility:
+			dayData = 24*60 - v.DayActive
+		case pasturePb.Behavior_Chew:
+			dayData = v.DayRumina + v.DayIntake
+		}
+		switch v.BreedStatus {
+		case pasturePb.BreedStatus_Calving:
+			data.UnBreed = append(data.UnBreed, &pasturePb.CowBehaviorData{
+				EarNumber:  v.EarNumber,
+				CalvingAge: v.LactationAge,
+				DayData:    dayData,
+			})
+		case pasturePb.BreedStatus_Empty:
+			data.Empty = append(data.Empty, &pasturePb.CowBehaviorData{
+				EarNumber:  v.EarNumber,
+				CalvingAge: v.LactationAge,
+				DayData:    dayData,
+			})
+		case pasturePb.BreedStatus_UnBreed:
+			data.UnBreed = append(data.UnBreed, &pasturePb.CowBehaviorData{
+				EarNumber:  v.EarNumber,
+				CalvingAge: v.LactationAge,
+				DayData:    dayData,
+			})
+		case pasturePb.BreedStatus_Breeding:
+			data.Breed = append(data.Breed, &pasturePb.CowBehaviorData{
+				EarNumber:  v.EarNumber,
+				CalvingAge: v.LactationAge,
+				DayData:    dayData,
+			})
+		case pasturePb.BreedStatus_Pregnant:
+			data.Pregnant = append(data.Pregnant, &pasturePb.CowBehaviorData{
+				EarNumber:  v.EarNumber,
+				CalvingAge: v.LactationAge,
+				DayData:    dayData,
+			})
+		}
+	}
+
+	// 获取Breed的中位数
+	if len(data.Breed) > 0 {
+		data.MedianLine["breed"] = float32(getMedian(data.Breed, func(p *pasturePb.CowBehaviorData) int {
+			return int(p.DayData)
+		}))
+	}
+
+	if len(data.Pregnant) > 0 {
+		data.MedianLine["pregnant"] = float32(getMedian(data.Pregnant, func(p *pasturePb.CowBehaviorData) int {
+			return int(p.DayData)
+		}))
+	}
+
+	if len(data.Empty) > 0 {
+		data.MedianLine["empty"] = float32(getMedian(data.Empty, func(p *pasturePb.CowBehaviorData) int {
+			return int(p.DayData)
+		}))
+	}
+
+	if len(data.UnBreed) > 0 {
+		data.MedianLine["unBreed"] = float32(getMedian(data.UnBreed, func(p *pasturePb.CowBehaviorData) int {
+			return int(p.DayData)
+		}))
+	}
+
+	return &pasturePb.CowBehaviorDistributionResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: data,
+	}, err
+}
+
+// 获取结构体切片中某个int字段的中位值
+func getMedian(dataList []*pasturePb.CowBehaviorData, getField func(*pasturePb.CowBehaviorData) int) float64 {
+	// 1. 提取字段值
+	values := make([]int, len(dataList))
+	for i, p := range dataList {
+		values[i] = getField(p)
+	}
+
+	// 2. 排序
+	sort.Ints(values)
+
+	// 3. 计算中位数
+	n := len(values)
+	if n == 0 {
+		return 0
+	}
+
+	if n%2 == 1 {
+		return float64(values[n/2])
+	}
+	return float64(values[n/2-1]+values[n/2]) / 2.0
+}

+ 3 - 0
module/backend/calendar.go

@@ -411,6 +411,9 @@ func (s *StoreEntry) PregnancyCheckCowList(ctx context.Context, req *pasturePb.I
 		Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
 		Where("a.pasture_id = ?", userModel.AppPasture.Id).
 		Where("a.status = ?", pasturePb.IsShow_No)
+	if req.EarNumber != "" {
+		pref.Where("a.ear_number = ?", req.EarNumber)
+	}
 
 	if req.EndDay != "" {
 		dateTime := util.TimeParseLocalEndUnix(req.EndDay)

+ 3 - 3
module/backend/config_data_base.go

@@ -433,12 +433,12 @@ func (s *StoreEntry) Behavior(isAll string) []*pasturePb.ConfigOptionsList {
 		Label:    "休息",
 		Disabled: true,
 	}, &pasturePb.ConfigOptionsList{
-		Value:    int32(pasturePb.Behavior_Stationary),
+		Value:    int32(pasturePb.Behavior_Immobility),
 		Label:    "静止",
 		Disabled: true,
 	}, &pasturePb.ConfigOptionsList{
-		Value:    int32(pasturePb.Behavior_Milk),
-		Label:    "奶量",
+		Value:    int32(pasturePb.Behavior_Chew),
+		Label:    "咀嚼",
 		Disabled: true,
 	})
 	return configOptions

+ 2 - 0
module/backend/interface.go

@@ -296,7 +296,9 @@ type AnalyseService interface {
 	MultipleFactorAnalysis(ctx context.Context, req *pasturePb.MultiFactorPregnancyRateRequest) (*model.MultiFactorPregnancyRateResponse, error)
 
 	PenBehavior(ctx context.Context, req *pasturePb.BarnBehaviorCurveRequest) (*pasturePb.BarnBehaviorCurveResponse, error)
+	PenBehavior2(ctx context.Context, req *pasturePb.BarnBehaviorCurveRequest) (*model.BarnBehaviorCurveResponse, error)
 	PenBehaviorDaily(ctx context.Context, req *pasturePb.BarnMonitorRequest) (*pasturePb.BarnMonitorResponse, error)
+	CowBehaviorDistribution(ctx context.Context, req *pasturePb.CowBehaviorDistributionRequest) (*pasturePb.CowBehaviorDistributionResponse, error)
 }
 
 //go:generate mockgen -destination mock/DashboardService.go -package kptservicemock kpt-pasture/module/backend DashboardService

+ 68 - 15
module/crontab/milk_daily.go

@@ -5,6 +5,8 @@ import (
 	"kpt-pasture/util"
 	"time"
 
+	"gitee.com/xuyiping_admin/pkg/xerr"
+
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
 
 	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
@@ -43,30 +45,79 @@ func (e *Entry) InsertMilkDaily() error {
 
 func (e *Entry) ProcessMilkDaily(pastureId int64, maxDate time.Time) {
 	nowTime := time.Now().Local()
+
 	// 处理每一天的数据
 	for maxDate.Before(nowTime) {
 		// 处理有胎次的奶牛
-		if err := e.processCowsWithLact(pastureId, maxDate); err != nil {
+		processCowIds := make([]int64, 0)
+		cowIds, err := e.processCowsWithLact(pastureId, maxDate)
+		if err != nil {
 			zaplog.Error("ProcessMilkDaily", zap.Any("processCowsWithFetal", err))
 		}
-
+		if len(cowIds) > 0 {
+			processCowIds = append(processCowIds, cowIds...)
+		}
 		// 处理无胎次的奶牛
-		if err := e.processCowsWithNoLact(pastureId, maxDate); err != nil {
+		cowIds, err = e.processCowsWithNoLact(pastureId, maxDate)
+		if err != nil {
 			zaplog.Error("ProcessMilkDaily", zap.Any("processCowsWithoutFetal", err))
 		}
+
+		if len(cowIds) > 0 {
+			processCowIds = append(processCowIds, cowIds...)
+		}
+
+		if len(processCowIds) > 0 {
+			e.UpdateMilkDaily(pastureId, processCowIds, maxDate.Format(model.LayoutDate2))
+		}
 		// 日期加1天
 		maxDate = maxDate.AddDate(0, 0, 1)
 	}
 }
 
-func (e *Entry) UpdateMilkDaily(pastureId int64, maxDateTime string) error {
-	//yesterday := time.Now().Local().AddDate(0, 0, -1).Format(model.LayoutDate2)
+// UpdateMilkDaily
+// SELECT h.intCowId, ROUND(AVG(h.filterhigh), 0) high, ROUND(AVG( h.rumina)*12, 0) rumina, ROUND(AVG( h.intake)*12, 0) intake,
+// //				ROUND(AVG( h.inactive)*12, 0) inactive, ROUND(AVG( h.act)*12, 0) act, COUNT(1) nb
+// //			FROM h_activehabit h WHERE h.intPastureId=PastuId AND h.heatdate=xDate GROUP BY h.intCowId HAVING nb>=8
+func (e *Entry) UpdateMilkDaily(pastureId int64, processCowIds []int64, heatDate string) error {
 
+	neckActiveHabitList := make([]*model.NeckActiveHabit, 0)
+	if err := e.DB.Model(new(model.NeckActiveHabit)).
+		Select(`h.cow_id, ROUND(AVG(h.filter_high), 0) AS high, ROUND(AVG( h.rumina)*12, 0) AS rumina, ROUND(AVG( h.intake)*12, 0) AS intake,
+			ROUND(AVG( h.inactive)*12, 0) AS inactive, ROUND(AVG( h.active)*12, 0) AS active, COUNT(1) AS record_count`).
+		Where("pasture_id = ?", pastureId).
+		Where("cow_id IN ?", processCowIds).
+		Where("heat_date = ?", heatDate).
+		Group("cow_id").
+		Find(&neckActiveHabitList).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+
+	for _, v := range neckActiveHabitList {
+		// todo 先不过滤
+		/*if v.RecordCount < 8 {
+			continue
+		}*/
+		if err := e.DB.Model(new(model.MilkDaily)).
+			Where("pasture_id = ?", pastureId).
+			Where("cow_id = ?", v.CowId).
+			Where("heat_date = ?", heatDate).
+			Updates(map[string]interface{}{
+				"day_high":     v.High,
+				"day_rumina":   v.Rumina,
+				"day_intake":   v.Intake,
+				"day_inactive": v.Inactive,
+				"day_active":   v.Active,
+			}).Error; err != nil {
+			zaplog.Error("UpdateMilkDaily", zap.Any("pastureId", pastureId), zap.Any("cowId", v.CowId), zap.Any("heatDate", heatDate), zap.Any("err", err))
+			continue
+		}
+	}
 	return nil
 }
 
 // 处理有胎次的奶牛
-func (e *Entry) processCowsWithLact(pastureId int64, recordDate time.Time) error {
+func (e *Entry) processCowsWithLact(pastureId int64, recordDate time.Time) ([]int64, error) {
 	// 查询有胎次且在记录日期前有记录的奶牛
 	cowList := make([]*model.Cow, 0)
 	if err := e.DB.Model(new(model.Cow)).
@@ -75,11 +126,12 @@ func (e *Entry) processCowsWithLact(pastureId int64, recordDate time.Time) error
 		Where("lact > ?", 0).
 		Where("last_calving_at <= ?", recordDate.Local().Unix()).
 		Find(&cowList).Error; err != nil {
-		return err
+		return nil, err
 	}
 
 	// 批量插入数据
 	milkDailyList := make([]*model.MilkDaily, 0)
+	cowIds := make([]int64, 0)
 	for _, cow := range cowList {
 		calvingDate := ""
 		if cow.LastCalvingAt > 0 {
@@ -98,6 +150,7 @@ func (e *Entry) processCowsWithLact(pastureId int64, recordDate time.Time) error
 			BreedStatus:  cow.BreedStatus,
 		}
 		milkDailyList = append(milkDailyList, milkDaily)
+		cowIds = append(cowIds, cow.Id)
 	}
 	if len(milkDailyList) > 0 {
 		// 分批次插入数据
@@ -108,16 +161,16 @@ func (e *Entry) processCowsWithLact(pastureId int64, recordDate time.Time) error
 			}
 			if err := e.DB.Model(new(model.MilkDaily)).
 				Create(milkDailyList[i:end]).Error; err != nil {
-				return err
+				return nil, err
 			}
 		}
 	}
 
-	return nil
+	return cowIds, nil
 }
 
 // 处理无胎次的奶牛
-func (e *Entry) processCowsWithNoLact(pastureId int64, recordDate time.Time) error {
+func (e *Entry) processCowsWithNoLact(pastureId int64, recordDate time.Time) ([]int64, error) {
 	// 查询无胎次且EID1>0的奶牛
 	cowList := make([]*model.Cow, 0)
 	err := e.DB.Model(new(model.Cow)).
@@ -129,13 +182,13 @@ func (e *Entry) processCowsWithNoLact(pastureId int64, recordDate time.Time) err
 		Where("neck_ring_number != ?", "").
 		Find(&cowList).Error
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// 批量插入数据
 	milkDailyList := make([]*model.MilkDaily, 0)
+	cowIds := make([]int64, 0)
 	for _, cow := range cowList {
-
 		birthDate, calvingDate := "", ""
 		if cow.BirthAt > 0 {
 			birthDate = time.Unix(cow.BirthAt, 0).Local().Format(model.LayoutDate2)
@@ -158,14 +211,14 @@ func (e *Entry) processCowsWithNoLact(pastureId int64, recordDate time.Time) err
 			BreedStatus:  cow.BreedStatus,
 		}
 		milkDailyList = append(milkDailyList, milkDaily)
+		cowIds = append(cowIds, cow.Id)
 	}
 
 	if len(milkDailyList) > 0 {
 		if err = e.DB.Model(new(model.MilkDaily)).
 			Create(&milkDailyList).Error; err != nil {
-			return err
+			return nil, err
 		}
 	}
-
-	return nil
+	return cowIds, nil
 }

+ 5 - 14
module/crontab/neck_ring_calculate.go

@@ -19,15 +19,6 @@ func (e *Entry) NeckRingCalculate() error {
 	if pastureList == nil || len(pastureList) == 0 {
 		return nil
 	}
-
-	if calculateIsRunning {
-		return nil
-	}
-	defer func() {
-		calculateIsRunning = false
-	}()
-
-	calculateIsRunning = true
 	for _, pasture := range pastureList {
 		if err := e.EntryUpdateActiveHabit(pasture.Id); err != nil {
 			zaplog.Error("NeckRingCalculate", zap.Any("err", err), zap.Any("pasture", pasture))
@@ -56,7 +47,7 @@ func (e *Entry) EntryUpdateActiveHabit(pastureId int64) (err error) {
 		zaplog.Error("NeckRingCalculate", zap.Any("pastureId", pastureId), zap.Any("FirstFilterUpdate", err), zap.Any("xToday", xToday))
 	}
 
-	zaplog.Info("NeckRingCalculate", zap.Any("xToday", xToday), zap.Any("processIds", processIds))
+	zaplog.Info("NeckRingCalculate", zap.Any("pastureId", pastureId), zap.Any("xToday", xToday), zap.Any("processIds", processIds))
 	if len(processIds) <= 0 {
 		return nil
 	}
@@ -100,7 +91,7 @@ func (e *Entry) FirstFilterUpdate(pastureId int64, xToDay *XToday) (processIds [
 		Where("heat_date >= ?", time.Now().Local().AddDate(0, 0, -30).Format(model.LayoutDate2)).
 		Where("pasture_id = ?", pastureId).
 		Where("is_show = ?", pasturePb.IsShow_No).
-		//Where(e.DB.Where("high >= ?", xToDay.High).Or("rumina >= ?", xToDay.Rumina)).
+		Where(e.DB.Where("high >= ?", xToDay.High).Or("rumina >= ?", xToDay.Rumina)).
 		Order("heat_date,neck_ring_number,frameid").
 		Limit(int(limit)).
 		Find(&newNeckActiveHabitList).Error; err != nil {
@@ -109,9 +100,9 @@ func (e *Entry) FirstFilterUpdate(pastureId int64, xToDay *XToday) (processIds [
 
 	// 活动量滤波
 	for _, v := range newNeckActiveHabitList {
-		if !(v.High >= xToDay.High || v.Rumina >= xToDay.Rumina) {
+		/*if !(v.High >= xToDay.High || v.Rumina >= xToDay.Rumina) {
 			continue
-		}
+		}*/
 		// 4小时数据不全的不参与滤波
 		activeTime, _ := util.TimeParseLocal(model.LayoutTime, v.ActiveTime)
 		if v.RecordCount != model.DefaultRecordCount && time.Now().Local().Sub(activeTime).Hours() <= 4 {
@@ -405,7 +396,7 @@ func (e *Entry) UpdateChangeAdJust(pastureId int64, xToday *XToday) {
 			Where("pasture_id = ?", pastureId).
 			Where("neck_ring_number = ?", v.NeckRingNumber).
 			Where("heat_date = ?", v.HeatDate).
-			Where("pen_id = ?", v.PenId).
+			//Where("pen_id = ?", v.PenId).
 			Where("frameid = ?", v.FrameId).
 			Update("change_adjust", v.ChangeFilter).Error; err != nil {
 			zaplog.Error("UpdateChangeAdJust-1", zap.Any("error", err), zap.Any("xToday", xToday))

+ 46 - 0
module/crontab/pen_behavior.go

@@ -23,6 +23,7 @@ func (e *Entry) UpdatePenBehavior() error {
 			continue
 		}
 		e.PenBehavior(pasture.Id, conf.Value)
+		e.UpdatePenBehaviorWeekData(pasture.Id)
 	}
 	return nil
 }
@@ -189,6 +190,51 @@ func (e *Entry) savePenBehaviorData(penData map[string]*model.PenBehaviorData) e
 	return nil
 }
 
+func (e *Entry) UpdatePenBehaviorWeekData(pastureId int64) {
+	nowTime := time.Now().Local()
+	currTime := nowTime.Format(model.LayoutDate2)
+	startTime := nowTime.AddDate(0, 0, -7).Format(model.LayoutDate2)
+	endTime := nowTime.AddDate(0, 0, -1).Format(model.LayoutDate2)
+	penBehaviorList := make([]*model.PenBehavior, 0)
+	if err := e.DB.Table(fmt.Sprintf("%s as b1", new(model.PenBehavior).TableName())).
+		Joins(fmt.Sprintf("JOIN %s as b0 ON b1.pen_id = b0.pen_id", new(model.PenBehavior).TableName())).
+		Select(`b1.id, ROUND(AVG(b0.rumina_rate)) AS week_rumina_rate, 
+		ROUND(AVG(b0.intake_rate)) AS week_intake_rate,
+		ROUND(AVG(b0.rest_rate)) AS week_rest_rate, 
+		ROUND(AVG(b0.gasp_rate)) AS week_gasp_rate, 
+		ROUND(STD(b0.rumina_rate)) AS rumina_std, 		
+		ROUND(STD(b0.intake_rate)) AS  intake_std, 		
+		ROUND(STD(b0.rest_rate)) AS rest_std, 	
+		ROUND(STD(b0.gasp_rate)) AS gasp_std`).
+		Where("b1.pasture_id = ?", pastureId).
+		Where("b1.heat_date = ?", currTime).
+		Where("b1.week_rumina_rate = ?", 0).
+		Where("b1.frameid = b0.frameid").
+		Where("b0.heat_date BETWEEN ? AND ?", startTime, endTime).
+		Find(&penBehaviorList).Error; err != nil {
+		zaplog.Error("PenBehavior", zap.Any("penBehaviorList", penBehaviorList), zap.Any("err", err))
+		return
+	}
+
+	for _, v := range penBehaviorList {
+		if err := e.DB.Model(new(model.PenBehavior)).
+			Where("id = ?", v.Id).
+			Updates(map[string]interface{}{
+				"week_rumina_rate": v.WeekRuminaRate,
+				"week_intake_rate": v.WeekIntakeRate,
+				"week_rest_rate":   v.WeekRestRate,
+				"week_gasp_rate":   v.WeekGaspRate,
+				"rumina_std":       v.RuminaStd,
+				"intake_std":       v.IntakeStd,
+				"rest_std":         v.RestStd,
+				"gasp_std":         v.GaspStd,
+			}).Error; err != nil {
+			zaplog.Error("PenBehavior", zap.Any("penBehaviorWeekData", v), zap.Any("err", err))
+			continue
+		}
+	}
+}
+
 // calculateActiveTime 计算活动时间
 func (e *Entry) calculateActiveTime(heatDate string, frameid int32) string {
 	// 计算小时和分钟

+ 0 - 1
module/crontab/pen_behavior_day.go

@@ -110,6 +110,5 @@ func (e *Entry) insertBarBehaviorDay(pastureId int64, targetDate string) error {
 			return xerr.WithStack(err)
 		}
 	}
-
 	return nil
 }