Explorar el Código

cow: neckRing 牛只脖环数据app端

Yi hace 3 semanas
padre
commit
9377abb5e7
Se han modificado 12 ficheros con 985 adiciones y 685 borrados
  1. 1 1
      go.mod
  2. 12 0
      go.sum
  3. 24 0
      http/handler/cow/cow.go
  4. 1 0
      http/route/cow_api.go
  5. 1 0
      locales/en.json
  6. 1 0
      locales/zh.json
  7. 1 408
      model/cow.go
  8. 415 0
      model/cow_more.go
  9. 111 0
      model/neck_active_habit.go
  10. 2 276
      module/backend/cow.go
  11. 415 0
      module/backend/cow_more.go
  12. 1 0
      module/backend/interface.go

+ 1 - 1
go.mod

@@ -3,7 +3,7 @@ module kpt-pasture
 go 1.17
 
 require (
-	gitee.com/xuyiping_admin/go_proto v0.0.0-20250828014705-705e31707eaf
+	gitee.com/xuyiping_admin/go_proto v0.0.0-20250903094829-eb25a6aec6c4
 	gitee.com/xuyiping_admin/pkg v0.0.0-20250613101634-36c36a2d27d0
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/eclipse/paho.mqtt.golang v1.4.3

+ 12 - 0
go.sum

@@ -1339,6 +1339,18 @@ gitee.com/xuyiping_admin/go_proto v0.0.0-20250822092947-b3acec2e3359 h1:HcFY+HPt
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250822092947-b3acec2e3359/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250828014705-705e31707eaf h1:ZDcAav9r7va8fKGGd4mLprqbWwVS8TrQ+ScdwGuhbGI=
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250828014705-705e31707eaf/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903070416-403c999b1f63 h1:uj9/4zOKfrjk8CWNv50qto93fxfS/JcadN5tRwhBlpM=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903070416-403c999b1f63/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903071045-49078fb63665 h1:piCMbecQI3/DxDs510Zu6R0K/zSHY2kWDnze3QixBho=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903071045-49078fb63665/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903071322-6d5bbf06b445 h1:uLq0mFqhKlix/r9Fb8HHIYn3HLtVirdBAnH1oPlolg4=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903071322-6d5bbf06b445/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903073133-7d5ef92c5ff6 h1:O76VxKdx0S1eiR1WoAEJFnqNWIB7IpJdjDe+DGwTEIw=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903073133-7d5ef92c5ff6/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903092259-88c4bd8e87c2 h1:Hxgj54WgHZT6e9HJXvWgkC8g+HeCRyD7WeAc2jW0Si4=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903092259-88c4bd8e87c2/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903094829-eb25a6aec6c4 h1:2381qSPRvM4cExQSZX8H8B9NwxuKiosN6PGhrTznS34=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250903094829-eb25a6aec6c4/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
 gitee.com/xuyiping_admin/pkg v0.0.0-20250613101634-36c36a2d27d0 h1:ZCOqEAnGm6+DTAhACigzWKbwMKtleb8/7OzP2xfHG7g=
 gitee.com/xuyiping_admin/pkg v0.0.0-20250613101634-36c36a2d27d0/go.mod h1:8tF25X6pE9WkFCczlNAC0K2mrjwKvhhp02I7o0HtDxY=
 github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

+ 24 - 0
http/handler/cow/cow.go

@@ -98,6 +98,30 @@ func BehaviorCurve(c *gin.Context) {
 	c.JSON(http.StatusOK, res)
 }
 
+func BehaviorCurveApp(c *gin.Context) {
+	var req pasturePb.CowBehaviorCurveRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.EarNumber, valid.Required),
+		valid.Field(&req.Days, valid.Required),
+		valid.Field(&req.CurveNameList, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.BehaviorCurveApp(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}
+
 func GrowthCurve(c *gin.Context) {
 	var req pasturePb.CowGrowthCurveRequest
 	if err := ginutil.BindProto(c, &req); err != nil {

+ 1 - 0
http/route/cow_api.go

@@ -17,6 +17,7 @@ func CowAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		cowRoute.POST("/list", cow.List)
 		cowRoute.POST("/event/list", cow.EventList)
 		cowRoute.POST("/behavior/curve", cow.BehaviorCurve)
+		cowRoute.POST("/behavior/curve/app", cow.BehaviorCurveApp)
 		cowRoute.POST("/growth/curve", cow.GrowthCurve)
 		cowRoute.POST("/lact/curve", cow.LactCurve)
 		cowRoute.POST("/behavior/rate", cow.BehaviorRate)

+ 1 - 0
locales/en.json

@@ -57,6 +57,7 @@
   "validate.matingDataItemAlready": "The breeding list already has the data for this cow: {{.earNumber}}, and the breeding data has been submitted today!",
   "validate.CowNotPregnant": "Cow: {{.earNumber}}, not pregnant!",
   "validate.AbortionDateBirthDate": "Cattle: {{.earNumber}}, abortion time cannot be earlier than the cow's birth time!",
+  "validate.neckRingAppDays": "The number of days cannot exceed 60 days",
   "analysis.wrongMethod": "Incorrect statistical method!",
   "analysis.wrongLact": "Incorrect lactation interval symbol!",
   "analysis.wrongDateRange": "Start time cannot be later than end time!",

+ 1 - 0
locales/zh.json

@@ -57,6 +57,7 @@
   "validate.matingDataItemAlready": "配种清单已经有该牛只数据: {{.earNumber}},今天已经提交过配种数据!",
   "validate.CowNotPregnant": "牛只: {{.earNumber}},不是怀孕状态!",
   "validate.AbortionDateBirthDate": "牛只: {{.earNumber}},流产时间不能早于牛只出生时间!",
+  "validate.neckRingAppDays": "天数不能大于60天",
   "analysis.wrongMethod": "错误的统计方式!",
   "analysis.wrongLact": "错误的胎次区间符号!",
   "analysis.wrongDateRange": "开始时间不能大于结束时间!",

+ 1 - 408
model/cow.go

@@ -1,13 +1,13 @@
 package model
 
 import (
-	"fmt"
 	"math"
 	"time"
 
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
 )
 
+const NeckRingDataDays = 50 // 默认获取50天的脖环数据
 type Cow struct {
 	Id                    int64                          `json:"id"`
 	PastureId             int64                          `json:"pastureId"`             // 牧场id
@@ -576,410 +576,3 @@ func (c CowSlice) ToPB(
 	}
 	return res
 }
-
-func (c CowSlice) ToPB2(penWeightSlice PenWeightSlice) []*pasturePb.CowList {
-	res := make([]*pasturePb.CowList, len(c))
-	for i, v := range c {
-		penWeight := penWeightSlice.GetPenWeight(v.PenId)
-		penAvgWeight := float32(0)
-		cowPenAvgWeightDiffValue := float32(0)
-
-		if penWeight != nil {
-			penAvgWeight = float32(penWeight.AvgWeight) / 1000
-			cowPenAvgWeightDiffValue = float32(v.CurrentWeight-int64(penWeight.AvgWeight)) / 1000
-		}
-
-		res[i] = &pasturePb.CowList{
-			CowId:                    int32(v.Id),
-			DayAge:                   v.DayAge,
-			AverageDailyWeightGain:   float32(v.GetAverageDailyWeight()),
-			EarNumber:                v.EarNumber,
-			PenName:                  v.PenName,
-			BirthAt:                  int32(v.BirthAt),
-			BirthWeight:              float32(v.BirthWeight) / 1000,
-			CurrentWeight:            float32(v.CurrentWeight) / 1000,
-			LastWeightAt:             int32(v.LastWeightAt),
-			AdmissionAge:             v.AdmissionAge,
-			AdmissionWeight:          float32(v.AbortionAge) / 1000,
-			PreviousStageDailyWeight: float32(v.GetPreviousStageDailyWeight()),
-			PenAvgWeight:             penAvgWeight,
-			CowPenAvgWeightDiffValue: cowPenAvgWeightDiffValue,
-		}
-	}
-	return res
-}
-
-// NewEnterCow 入场新增牛只
-func NewEnterCow(pastureId int64, req *pasturePb.EventEnterRequest, penMap map[int32]*Pen) *Cow {
-	var isPregnant = pasturePb.IsShow_No
-	if req.BreedStatus == pasturePb.BreedStatus_Pregnant {
-		isPregnant = pasturePb.IsShow_Ok
-	}
-
-	admissionAt := int64(0)
-	switch req.CowSource {
-	case pasturePb.CowSource_Calving:
-		admissionAt = int64(req.BirthAt)
-	case pasturePb.CowSource_Transfer_In:
-		admissionAt = int64(req.EnterAt)
-	case pasturePb.CowSource_Buy:
-		admissionAt = int64(req.EnterAt)
-	}
-
-	breedStatus := pasturePb.BreedStatus_Invalid
-	isForbiddenMating := pasturePb.IsShow_No
-	cowType := pasturePb.CowType_Invalid
-	if req.Sex == pasturePb.Genders_Female {
-
-		if req.Lact == 0 && req.MatingAt <= 0 {
-			breedStatus = pasturePb.BreedStatus_UnBreed
-			cowType = pasturePb.CowType_Reserve_Calf
-		}
-
-		if req.MatingAt > 0 && (req.PregnantCheckResult != pasturePb.PregnantCheckResult_Pregnant &&
-			req.PregnantCheckResult != pasturePb.PregnantCheckResult_UnPregnant) &&
-			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
-			breedStatus = pasturePb.BreedStatus_Breeding
-			if req.Lact == 0 {
-				cowType = pasturePb.CowType_Reserve_Calf
-			} else {
-				cowType = pasturePb.CowType_Breeding_Calf
-			}
-		}
-
-		if req.MatingAt > 0 && req.PregnantCheckResult == pasturePb.PregnantCheckResult_Pregnant &&
-			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
-			breedStatus = pasturePb.BreedStatus_Pregnant
-			if req.Lact == 0 {
-				cowType = pasturePb.CowType_Reserve_Calf
-			} else {
-				cowType = pasturePb.CowType_Breeding_Calf
-			}
-		}
-
-		if req.MatingAt > 0 && req.PregnantCheckResult == pasturePb.PregnantCheckResult_UnPregnant &&
-			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
-			breedStatus = pasturePb.BreedStatus_Empty
-			if req.Lact == 0 {
-				cowType = pasturePb.CowType_Reserve_Calf
-			} else {
-				cowType = pasturePb.CowType_Breeding_Calf
-			}
-		}
-
-		if req.CalvingAt > 0 && req.CalvingAt >= req.MatingAt && req.CalvingAt >= req.AbortionAt {
-			breedStatus = pasturePb.BreedStatus_Calving
-			cowType = pasturePb.CowType_Breeding_Calf
-		}
-
-		if req.AbortionAt > 0 && req.AbortionAt >= req.CalvingAt && req.AbortionAt >= req.MatingAt {
-			breedStatus = pasturePb.BreedStatus_Abort
-			cowType = pasturePb.CowType_Breeding_Calf
-		}
-	}
-
-	if breedStatus == pasturePb.BreedStatus_No_Mating {
-		isForbiddenMating = pasturePb.IsShow_Ok
-	}
-
-	cow := &Cow{
-		PastureId:           pastureId,
-		Sex:                 req.Sex,
-		EarNumber:           req.EarNumber,
-		PenId:               req.PenId,
-		PenName:             penMap[req.PenId].Name,
-		Lact:                req.Lact,
-		CowType:             cowType,
-		BreedStatus:         breedStatus,
-		CowKind:             req.CowKind,
-		SourceKind:          req.CowSource,
-		FatherNumber:        req.FatherNumber,
-		MotherNumber:        req.MotherNumber,
-		AdmissionStatus:     pasturePb.AdmissionStatus_Admission,
-		HealthStatus:        pasturePb.HealthStatus_Health,
-		PurposeKind:         req.PurposeKind,
-		EleEarNumber:        req.EleEarNumber,
-		IsPregnant:          isPregnant,
-		IsForbiddenMating:   isForbiddenMating,
-		WeaningAt:           int64(req.WeaningAt),
-		BirthAt:             int64(req.BirthAt),
-		AdmissionWeight:     int64(req.Weight * 1000),
-		FirstMatingAt:       int64(req.MatingAt),
-		LastMatingAt:        int64(req.MatingAt),
-		LastPregnantCheckAt: int64(req.PregnancyCheckAt),
-		AdmissionAt:         admissionAt,
-		BirthWeight:         int64(req.Weight * 1000),
-		LastWeightAt:        int64(req.EstrusAt),
-		CurrentWeight:       int64(req.Weight * 1000),
-		LastDryMilkAt:       int64(req.DryMilkAt),
-		MatingTimes:         req.MatingTimes,
-		LastCalvingAt:       int64(req.CalvingAt),
-		LastBullNumber:      req.BullNumber,
-		LastAbortionAt:      int64(req.AbortionAt),
-		AdmissionPrice:      req.Price,
-		BatchNumber:         req.BatchNumber,
-		NeckRingNumber:      req.NeckRingNumber,
-	}
-	cow.AdmissionAge = cow.GetAdmissionAge()
-	cow.DayAge = cow.GetDayAge()
-	return cow
-}
-
-// NewCalfCow 产犊新增
-func NewCalfCow(matherInfo *Cow, calf *CalvingCalf) *Cow {
-	return &Cow{
-		PastureId:       calf.PastureId,
-		Sex:             calf.Sex,
-		EarNumber:       calf.EarNumber,
-		PenId:           calf.PenId,
-		PenName:         calf.PenName,
-		CowType:         pasturePb.CowType_Lactating_Calf, // 哺乳犊牛
-		BreedStatus:     pasturePb.BreedStatus_UnBreed,    // 未配
-		CowKind:         matherInfo.CowKind,               // 牛只品种
-		BirthWeight:     calf.BirthWeight,
-		BirthAt:         calf.BirthAt,
-		SourceKind:      pasturePb.CowSource_Calving, // 产犊方式
-		FatherNumber:    matherInfo.LastBullNumber,
-		MotherNumber:    matherInfo.EarNumber,
-		AdmissionStatus: pasturePb.AdmissionStatus_Admission,
-		IsPregnant:      pasturePb.IsShow_No,
-		AdmissionAt:     calf.BirthAt,
-	}
-}
-
-// ExcelEnterCow excel导入入场新增牛只
-func ExcelEnterCow(pastureId int64, req *pasturePb.EventEnterRequest) *Cow {
-	var isPregnant = pasturePb.IsShow_No
-	if req.BreedStatus == pasturePb.BreedStatus_Pregnant {
-		isPregnant = pasturePb.IsShow_Ok
-	}
-
-	admissionAt := int64(0)
-	switch req.CowSource {
-	case pasturePb.CowSource_Calving:
-		admissionAt = int64(req.BirthAt)
-	case pasturePb.CowSource_Transfer_In:
-		admissionAt = int64(req.EnterAt)
-	case pasturePb.CowSource_Buy:
-		admissionAt = int64(req.EnterAt)
-	}
-
-	breedStatus := pasturePb.BreedStatus_Invalid
-	if req.Sex == pasturePb.Genders_Female {
-		if req.Lact == 0 && req.MatingAt <= 0 {
-			breedStatus = pasturePb.BreedStatus_UnBreed
-		}
-
-		if req.MatingAt > 0 && (req.PregnantCheckResult != pasturePb.PregnantCheckResult_Pregnant &&
-			req.PregnantCheckResult != pasturePb.PregnantCheckResult_UnPregnant) &&
-			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
-			breedStatus = pasturePb.BreedStatus_Breeding
-
-		}
-
-		if req.MatingAt > 0 && req.PregnantCheckResult == pasturePb.PregnantCheckResult_Pregnant &&
-			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
-			breedStatus = pasturePb.BreedStatus_Pregnant
-
-		}
-
-		if req.MatingAt > 0 && req.PregnantCheckResult == pasturePb.PregnantCheckResult_UnPregnant &&
-			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
-			breedStatus = pasturePb.BreedStatus_Empty
-		}
-
-		if req.CalvingAt > 0 && req.CalvingAt >= req.MatingAt && req.CalvingAt >= req.AbortionAt {
-			breedStatus = pasturePb.BreedStatus_Calving
-		}
-
-		if req.AbortionAt > 0 && req.AbortionAt >= req.CalvingAt && req.AbortionAt >= req.MatingAt {
-			breedStatus = pasturePb.BreedStatus_Abort
-		}
-	}
-
-	cow := &Cow{
-		PastureId:           pastureId,
-		Sex:                 req.Sex,
-		EarNumber:           req.EarNumber,
-		PenId:               req.PenId,
-		PenName:             req.PenName,
-		Lact:                req.Lact,
-		CowType:             req.CowType,
-		BreedStatus:         breedStatus,
-		CowKind:             req.CowKind,
-		SourceKind:          req.CowSource,
-		FatherNumber:        req.FatherNumber,
-		MotherNumber:        req.MotherNumber,
-		AdmissionStatus:     pasturePb.AdmissionStatus_Admission,
-		HealthStatus:        pasturePb.HealthStatus_Health,
-		PurposeKind:         req.PurposeKind,
-		EleEarNumber:        req.EleEarNumber,
-		IsPregnant:          isPregnant,
-		IsForbiddenMating:   req.IsForbiddenMatingKind,
-		WeaningAt:           int64(req.WeaningAt),
-		BirthAt:             int64(req.BirthAt),
-		AdmissionWeight:     int64(req.Weight * 1000),
-		FirstMatingAt:       int64(req.MatingAt),
-		LastMatingAt:        int64(req.MatingAt),
-		LastPregnantCheckAt: int64(req.PregnancyCheckAt),
-		AdmissionAt:         admissionAt,
-		BirthWeight:         int64(req.Weight * 1000),
-		LastWeightAt:        int64(req.EstrusAt),
-		CurrentWeight:       int64(req.Weight * 1000),
-		LastDryMilkAt:       int64(req.DryMilkAt),
-		MatingTimes:         req.MatingTimes,
-		LastCalvingAt:       int64(req.CalvingAt),
-		LastBullNumber:      req.BullNumber,
-		LastAbortionAt:      int64(req.AbortionAt),
-		AdmissionPrice:      req.Price,
-		BatchNumber:         req.BatchNumber,
-		NeckRingNumber:      req.NeckRingNumber,
-	}
-	cow.AdmissionAge = cow.GetAdmissionAge()
-	cow.DayAge = cow.GetDayAge()
-	return cow
-}
-
-type BarCowStruct struct {
-	Number int32                  `json:"number"`
-	TypeId pasturePb.CowType_Kind `json:"type_id"`
-}
-
-// BarCowStructSlice 首页牛群结构
-type BarCowStructSlice []*BarCowStruct
-
-func (b BarCowStructSlice) ToPB(cowTypeMap map[pasturePb.CowType_Kind]string, count int32) []*pasturePb.BarCowStruct {
-	var pb []*pasturePb.BarCowStruct
-	for _, v := range b {
-		name := fmt.Sprintf("%s", cowTypeMap[v.TypeId])
-		pb = append(pb, &pasturePb.BarCowStruct{Name: name, Value: v.Number})
-	}
-	return pb
-}
-
-type CowWeightRange struct {
-	WeightRange string `json:"weight_range"`
-	Count       int32  `json:"count"`
-}
-
-func (c CowSlice) WeightRangeToPB(penMap map[int32]*Pen) []*pasturePb.CowList {
-	res := make([]*pasturePb.CowList, len(c))
-	for i, v := range c {
-		penName := ""
-		if pen, ok := penMap[v.PenId]; ok {
-			penName = pen.Name
-		}
-		res[i] = &pasturePb.CowList{
-			CowId:                    int32(v.Id),
-			DayAge:                   v.DayAge,
-			AverageDailyWeightGain:   float32(v.GetAverageDailyWeight()),
-			PreviousStageDailyWeight: float32(v.GetPreviousStageDailyWeight()),
-			EarNumber:                v.EarNumber,
-			PenName:                  penName,
-			BirthAt:                  int32(v.BirthAt),
-			BirthWeight:              float32(v.BirthWeight) / 1000,
-			CurrentWeight:            float32(v.CurrentWeight) / 1000,
-			LastWeightAt:             int32(v.LastWeightAt),
-			AdmissionAge:             v.AdmissionAge,
-		}
-	}
-	return res
-}
-
-func (c CowSlice) LongTermInfertilityToPB(breedStatusMap map[pasturePb.BreedStatus_Kind]string) []*pasturePb.LongTermInfertility {
-	res := make([]*pasturePb.LongTermInfertility, len(c))
-	for i, v := range c {
-		breedStatusName := ""
-		if breedStatus, ok := breedStatusMap[v.BreedStatus]; ok {
-			breedStatusName = breedStatus
-		}
-		lastCalvingAtFormat := ""
-		if v.LastCalvingAt > 0 {
-			lastCalvingAtFormat = time.Unix(v.LastCalvingAt, 0).Local().Format(LayoutDate2)
-		}
-		lastAbortionAtFormat := ""
-		if v.LastAbortionAt > 0 {
-			lastAbortionAtFormat = time.Unix(v.LastAbortionAt, 0).Local().Format(LayoutDate2)
-		}
-		lastMatingAtFormat := ""
-		if v.LastMatingAt > 0 {
-			lastMatingAtFormat = time.Unix(v.LastMatingAt, 0).Local().Format(LayoutDate2)
-		}
-		res[i] = &pasturePb.LongTermInfertility{
-			CowId:                int32(v.Id),
-			EarNumber:            v.EarNumber,
-			Lact:                 v.Lact,
-			PenId:                v.PenId,
-			CalvingAge:           v.CalvingAge,
-			PenName:              v.PenName,
-			BreedStatusName:      breedStatusName,
-			BreedStatus:          v.BreedStatus,
-			LastCalvingAtFormat:  lastCalvingAtFormat,
-			LastAbortionAtFormat: lastAbortionAtFormat,
-			LastMatingAtFormat:   lastMatingAtFormat,
-			MatingTimes:          v.MatingTimes,
-			LastBullNumber:       v.LastBullNumber,
-			AbortionTimes:        v.AbortionTimes,
-		}
-	}
-	return res
-}
-
-func (c CowSlice) CanSaleToPB(cowKindMap map[pasturePb.CowKind_Kind]string) []*pasturePb.CanSalesReport {
-	res := make([]*pasturePb.CanSalesReport, len(c))
-	for i, v := range c {
-		cowKindName, lastWeightAtFormat, admissionAtFormat := "", "", ""
-		if name, ok := cowKindMap[v.CowKind]; ok {
-			cowKindName = name
-		}
-		if v.LastWeightAt > 0 {
-			lastWeightAtFormat = time.Unix(v.LastWeightAt, 0).Local().Format(LayoutDate2)
-		}
-		if v.AdmissionAt > 0 {
-			admissionAtFormat = time.Unix(v.AdmissionAt, 0).Local().Format(LayoutDate2)
-		}
-		res[i] = &pasturePb.CanSalesReport{
-			CowId:              int32(v.Id),
-			EarNumber:          v.EarNumber,
-			BatchNumber:        v.BatchNumber,
-			CowKindName:        cowKindName,
-			PenName:            v.PenName,
-			Weight:             float32(v.CurrentWeight) / 1000,
-			AdmissionAge:       v.AdmissionAge,
-			EnterWeight:        0,
-			EnterPrice:         0,
-			LastWeightAtFormat: lastWeightAtFormat,
-			DayAvgFeedCost:     0,
-			AllFeedCost:        0,
-			AllMedicalCharge:   0,
-			OtherCost:          0,
-			ProfitAndLoss:      0,
-			AdmissionAtFormat:  admissionAtFormat,
-			DayAvgWeight:       0,
-		}
-	}
-	return res
-}
-
-// CowBehaviorCurveResponse 脖环行为数据
-type CowBehaviorCurveResponse struct {
-	Code int32                 `json:"code"`
-	Msg  string                `json:"msg"`
-	Data *CowBehaviorCurveData `json:"data"`
-}
-
-type CowBehaviorCurveData struct {
-	OriginalDateList []int32                                 `json:"originalDateList"` // 原始行为数据
-	ChangeDateList   []int32                                 `json:"changeDateList"`   // 变化数据
-	SumDateList      []int32                                 `json:"sumDateList"`      // 累计24小时数据
-	SumChewList      []int32                                 `json:"sumChewList"`      // 累计24小时咀嚼
-	DateTimeList     []string                                `json:"dateTimeList"`     // 时间数据
-	EstrusList       map[pasturePb.EstrusLevel_Kind][]string `json:"estrusList"`       // 发情预警
-	EventList        []*pasturePb.CowEvent                   `json:"eventList"`        // 事件数据
-	EventMap         map[pasturePb.EventType_Kind]string     `json:"eventMap"`         // 所有事件
-	RuminaChange     []int32                                 `json:"ruminaChange"`     // 反刍变化
-	LowActivity      int32                                   `json:"lowActivity"`      // 低活动量参数
-	MiddleActivity   int32                                   `json:"middleActivity"`   // 中活动量行数
-	IQR1             []int32                                 `json:"IQR1"`             // 1IQR
-	IQR3             []int32                                 `json:"IQR3"`             // 3IQR
-}

+ 415 - 0
model/cow_more.go

@@ -0,0 +1,415 @@
+package model
+
+import (
+	"fmt"
+	"time"
+
+	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+)
+
+func (c CowSlice) ToPB2(penWeightSlice PenWeightSlice) []*pasturePb.CowList {
+	res := make([]*pasturePb.CowList, len(c))
+	for i, v := range c {
+		penWeight := penWeightSlice.GetPenWeight(v.PenId)
+		penAvgWeight := float32(0)
+		cowPenAvgWeightDiffValue := float32(0)
+
+		if penWeight != nil {
+			penAvgWeight = float32(penWeight.AvgWeight) / 1000
+			cowPenAvgWeightDiffValue = float32(v.CurrentWeight-int64(penWeight.AvgWeight)) / 1000
+		}
+
+		res[i] = &pasturePb.CowList{
+			CowId:                    int32(v.Id),
+			DayAge:                   v.DayAge,
+			AverageDailyWeightGain:   float32(v.GetAverageDailyWeight()),
+			EarNumber:                v.EarNumber,
+			PenName:                  v.PenName,
+			BirthAt:                  int32(v.BirthAt),
+			BirthWeight:              float32(v.BirthWeight) / 1000,
+			CurrentWeight:            float32(v.CurrentWeight) / 1000,
+			LastWeightAt:             int32(v.LastWeightAt),
+			AdmissionAge:             v.AdmissionAge,
+			AdmissionWeight:          float32(v.AbortionAge) / 1000,
+			PreviousStageDailyWeight: float32(v.GetPreviousStageDailyWeight()),
+			PenAvgWeight:             penAvgWeight,
+			CowPenAvgWeightDiffValue: cowPenAvgWeightDiffValue,
+		}
+	}
+	return res
+}
+
+// NewEnterCow 入场新增牛只
+func NewEnterCow(pastureId int64, req *pasturePb.EventEnterRequest, penMap map[int32]*Pen) *Cow {
+	var isPregnant = pasturePb.IsShow_No
+	if req.BreedStatus == pasturePb.BreedStatus_Pregnant {
+		isPregnant = pasturePb.IsShow_Ok
+	}
+
+	admissionAt := int64(0)
+	switch req.CowSource {
+	case pasturePb.CowSource_Calving:
+		admissionAt = int64(req.BirthAt)
+	case pasturePb.CowSource_Transfer_In:
+		admissionAt = int64(req.EnterAt)
+	case pasturePb.CowSource_Buy:
+		admissionAt = int64(req.EnterAt)
+	}
+
+	breedStatus := pasturePb.BreedStatus_Invalid
+	isForbiddenMating := pasturePb.IsShow_No
+	cowType := pasturePb.CowType_Invalid
+	if req.Sex == pasturePb.Genders_Female {
+
+		if req.Lact == 0 && req.MatingAt <= 0 {
+			breedStatus = pasturePb.BreedStatus_UnBreed
+			cowType = pasturePb.CowType_Reserve_Calf
+		}
+
+		if req.MatingAt > 0 && (req.PregnantCheckResult != pasturePb.PregnantCheckResult_Pregnant &&
+			req.PregnantCheckResult != pasturePb.PregnantCheckResult_UnPregnant) &&
+			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
+			breedStatus = pasturePb.BreedStatus_Breeding
+			if req.Lact == 0 {
+				cowType = pasturePb.CowType_Reserve_Calf
+			} else {
+				cowType = pasturePb.CowType_Breeding_Calf
+			}
+		}
+
+		if req.MatingAt > 0 && req.PregnantCheckResult == pasturePb.PregnantCheckResult_Pregnant &&
+			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
+			breedStatus = pasturePb.BreedStatus_Pregnant
+			if req.Lact == 0 {
+				cowType = pasturePb.CowType_Reserve_Calf
+			} else {
+				cowType = pasturePb.CowType_Breeding_Calf
+			}
+		}
+
+		if req.MatingAt > 0 && req.PregnantCheckResult == pasturePb.PregnantCheckResult_UnPregnant &&
+			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
+			breedStatus = pasturePb.BreedStatus_Empty
+			if req.Lact == 0 {
+				cowType = pasturePb.CowType_Reserve_Calf
+			} else {
+				cowType = pasturePb.CowType_Breeding_Calf
+			}
+		}
+
+		if req.CalvingAt > 0 && req.CalvingAt >= req.MatingAt && req.CalvingAt >= req.AbortionAt {
+			breedStatus = pasturePb.BreedStatus_Calving
+			cowType = pasturePb.CowType_Breeding_Calf
+		}
+
+		if req.AbortionAt > 0 && req.AbortionAt >= req.CalvingAt && req.AbortionAt >= req.MatingAt {
+			breedStatus = pasturePb.BreedStatus_Abort
+			cowType = pasturePb.CowType_Breeding_Calf
+		}
+	}
+
+	if breedStatus == pasturePb.BreedStatus_No_Mating {
+		isForbiddenMating = pasturePb.IsShow_Ok
+	}
+
+	cow := &Cow{
+		PastureId:           pastureId,
+		Sex:                 req.Sex,
+		EarNumber:           req.EarNumber,
+		PenId:               req.PenId,
+		PenName:             penMap[req.PenId].Name,
+		Lact:                req.Lact,
+		CowType:             cowType,
+		BreedStatus:         breedStatus,
+		CowKind:             req.CowKind,
+		SourceKind:          req.CowSource,
+		FatherNumber:        req.FatherNumber,
+		MotherNumber:        req.MotherNumber,
+		AdmissionStatus:     pasturePb.AdmissionStatus_Admission,
+		HealthStatus:        pasturePb.HealthStatus_Health,
+		PurposeKind:         req.PurposeKind,
+		EleEarNumber:        req.EleEarNumber,
+		IsPregnant:          isPregnant,
+		IsForbiddenMating:   isForbiddenMating,
+		WeaningAt:           int64(req.WeaningAt),
+		BirthAt:             int64(req.BirthAt),
+		AdmissionWeight:     int64(req.Weight * 1000),
+		FirstMatingAt:       int64(req.MatingAt),
+		LastMatingAt:        int64(req.MatingAt),
+		LastPregnantCheckAt: int64(req.PregnancyCheckAt),
+		AdmissionAt:         admissionAt,
+		BirthWeight:         int64(req.Weight * 1000),
+		LastWeightAt:        int64(req.EstrusAt),
+		CurrentWeight:       int64(req.Weight * 1000),
+		LastDryMilkAt:       int64(req.DryMilkAt),
+		MatingTimes:         req.MatingTimes,
+		LastCalvingAt:       int64(req.CalvingAt),
+		LastBullNumber:      req.BullNumber,
+		LastAbortionAt:      int64(req.AbortionAt),
+		AdmissionPrice:      req.Price,
+		BatchNumber:         req.BatchNumber,
+		NeckRingNumber:      req.NeckRingNumber,
+	}
+	cow.AdmissionAge = cow.GetAdmissionAge()
+	cow.DayAge = cow.GetDayAge()
+	return cow
+}
+
+// NewCalfCow 产犊新增
+func NewCalfCow(matherInfo *Cow, calf *CalvingCalf) *Cow {
+	return &Cow{
+		PastureId:       calf.PastureId,
+		Sex:             calf.Sex,
+		EarNumber:       calf.EarNumber,
+		PenId:           calf.PenId,
+		PenName:         calf.PenName,
+		CowType:         pasturePb.CowType_Lactating_Calf, // 哺乳犊牛
+		BreedStatus:     pasturePb.BreedStatus_UnBreed,    // 未配
+		CowKind:         matherInfo.CowKind,               // 牛只品种
+		BirthWeight:     calf.BirthWeight,
+		BirthAt:         calf.BirthAt,
+		SourceKind:      pasturePb.CowSource_Calving, // 产犊方式
+		FatherNumber:    matherInfo.LastBullNumber,
+		MotherNumber:    matherInfo.EarNumber,
+		AdmissionStatus: pasturePb.AdmissionStatus_Admission,
+		IsPregnant:      pasturePb.IsShow_No,
+		AdmissionAt:     calf.BirthAt,
+	}
+}
+
+// ExcelEnterCow excel导入入场新增牛只
+func ExcelEnterCow(pastureId int64, req *pasturePb.EventEnterRequest) *Cow {
+	var isPregnant = pasturePb.IsShow_No
+	if req.BreedStatus == pasturePb.BreedStatus_Pregnant {
+		isPregnant = pasturePb.IsShow_Ok
+	}
+
+	admissionAt := int64(0)
+	switch req.CowSource {
+	case pasturePb.CowSource_Calving:
+		admissionAt = int64(req.BirthAt)
+	case pasturePb.CowSource_Transfer_In:
+		admissionAt = int64(req.EnterAt)
+	case pasturePb.CowSource_Buy:
+		admissionAt = int64(req.EnterAt)
+	}
+
+	breedStatus := pasturePb.BreedStatus_Invalid
+	if req.Sex == pasturePb.Genders_Female {
+		if req.Lact == 0 && req.MatingAt <= 0 {
+			breedStatus = pasturePb.BreedStatus_UnBreed
+		}
+
+		if req.MatingAt > 0 && (req.PregnantCheckResult != pasturePb.PregnantCheckResult_Pregnant &&
+			req.PregnantCheckResult != pasturePb.PregnantCheckResult_UnPregnant) &&
+			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
+			breedStatus = pasturePb.BreedStatus_Breeding
+
+		}
+
+		if req.MatingAt > 0 && req.PregnantCheckResult == pasturePb.PregnantCheckResult_Pregnant &&
+			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
+			breedStatus = pasturePb.BreedStatus_Pregnant
+
+		}
+
+		if req.MatingAt > 0 && req.PregnantCheckResult == pasturePb.PregnantCheckResult_UnPregnant &&
+			req.MatingAt >= req.CalvingAt && req.MatingAt >= req.AbortionAt {
+			breedStatus = pasturePb.BreedStatus_Empty
+		}
+
+		if req.CalvingAt > 0 && req.CalvingAt >= req.MatingAt && req.CalvingAt >= req.AbortionAt {
+			breedStatus = pasturePb.BreedStatus_Calving
+		}
+
+		if req.AbortionAt > 0 && req.AbortionAt >= req.CalvingAt && req.AbortionAt >= req.MatingAt {
+			breedStatus = pasturePb.BreedStatus_Abort
+		}
+	}
+
+	cow := &Cow{
+		PastureId:           pastureId,
+		Sex:                 req.Sex,
+		EarNumber:           req.EarNumber,
+		PenId:               req.PenId,
+		PenName:             req.PenName,
+		Lact:                req.Lact,
+		CowType:             req.CowType,
+		BreedStatus:         breedStatus,
+		CowKind:             req.CowKind,
+		SourceKind:          req.CowSource,
+		FatherNumber:        req.FatherNumber,
+		MotherNumber:        req.MotherNumber,
+		AdmissionStatus:     pasturePb.AdmissionStatus_Admission,
+		HealthStatus:        pasturePb.HealthStatus_Health,
+		PurposeKind:         req.PurposeKind,
+		EleEarNumber:        req.EleEarNumber,
+		IsPregnant:          isPregnant,
+		IsForbiddenMating:   req.IsForbiddenMatingKind,
+		WeaningAt:           int64(req.WeaningAt),
+		BirthAt:             int64(req.BirthAt),
+		AdmissionWeight:     int64(req.Weight * 1000),
+		FirstMatingAt:       int64(req.MatingAt),
+		LastMatingAt:        int64(req.MatingAt),
+		LastPregnantCheckAt: int64(req.PregnancyCheckAt),
+		AdmissionAt:         admissionAt,
+		BirthWeight:         int64(req.Weight * 1000),
+		LastWeightAt:        int64(req.EstrusAt),
+		CurrentWeight:       int64(req.Weight * 1000),
+		LastDryMilkAt:       int64(req.DryMilkAt),
+		MatingTimes:         req.MatingTimes,
+		LastCalvingAt:       int64(req.CalvingAt),
+		LastBullNumber:      req.BullNumber,
+		LastAbortionAt:      int64(req.AbortionAt),
+		AdmissionPrice:      req.Price,
+		BatchNumber:         req.BatchNumber,
+		NeckRingNumber:      req.NeckRingNumber,
+	}
+	cow.AdmissionAge = cow.GetAdmissionAge()
+	cow.DayAge = cow.GetDayAge()
+	return cow
+}
+
+type BarCowStruct struct {
+	Number int32                  `json:"number"`
+	TypeId pasturePb.CowType_Kind `json:"type_id"`
+}
+
+// BarCowStructSlice 首页牛群结构
+type BarCowStructSlice []*BarCowStruct
+
+func (b BarCowStructSlice) ToPB(cowTypeMap map[pasturePb.CowType_Kind]string, count int32) []*pasturePb.BarCowStruct {
+	var pb []*pasturePb.BarCowStruct
+	for _, v := range b {
+		name := fmt.Sprintf("%s", cowTypeMap[v.TypeId])
+		pb = append(pb, &pasturePb.BarCowStruct{Name: name, Value: v.Number})
+	}
+	return pb
+}
+
+type CowWeightRange struct {
+	WeightRange string `json:"weight_range"`
+	Count       int32  `json:"count"`
+}
+
+func (c CowSlice) WeightRangeToPB(penMap map[int32]*Pen) []*pasturePb.CowList {
+	res := make([]*pasturePb.CowList, len(c))
+	for i, v := range c {
+		penName := ""
+		if pen, ok := penMap[v.PenId]; ok {
+			penName = pen.Name
+		}
+		res[i] = &pasturePb.CowList{
+			CowId:                    int32(v.Id),
+			DayAge:                   v.DayAge,
+			AverageDailyWeightGain:   float32(v.GetAverageDailyWeight()),
+			PreviousStageDailyWeight: float32(v.GetPreviousStageDailyWeight()),
+			EarNumber:                v.EarNumber,
+			PenName:                  penName,
+			BirthAt:                  int32(v.BirthAt),
+			BirthWeight:              float32(v.BirthWeight) / 1000,
+			CurrentWeight:            float32(v.CurrentWeight) / 1000,
+			LastWeightAt:             int32(v.LastWeightAt),
+			AdmissionAge:             v.AdmissionAge,
+		}
+	}
+	return res
+}
+
+func (c CowSlice) LongTermInfertilityToPB(breedStatusMap map[pasturePb.BreedStatus_Kind]string) []*pasturePb.LongTermInfertility {
+	res := make([]*pasturePb.LongTermInfertility, len(c))
+	for i, v := range c {
+		breedStatusName := ""
+		if breedStatus, ok := breedStatusMap[v.BreedStatus]; ok {
+			breedStatusName = breedStatus
+		}
+		lastCalvingAtFormat := ""
+		if v.LastCalvingAt > 0 {
+			lastCalvingAtFormat = time.Unix(v.LastCalvingAt, 0).Local().Format(LayoutDate2)
+		}
+		lastAbortionAtFormat := ""
+		if v.LastAbortionAt > 0 {
+			lastAbortionAtFormat = time.Unix(v.LastAbortionAt, 0).Local().Format(LayoutDate2)
+		}
+		lastMatingAtFormat := ""
+		if v.LastMatingAt > 0 {
+			lastMatingAtFormat = time.Unix(v.LastMatingAt, 0).Local().Format(LayoutDate2)
+		}
+		res[i] = &pasturePb.LongTermInfertility{
+			CowId:                int32(v.Id),
+			EarNumber:            v.EarNumber,
+			Lact:                 v.Lact,
+			PenId:                v.PenId,
+			CalvingAge:           v.CalvingAge,
+			PenName:              v.PenName,
+			BreedStatusName:      breedStatusName,
+			BreedStatus:          v.BreedStatus,
+			LastCalvingAtFormat:  lastCalvingAtFormat,
+			LastAbortionAtFormat: lastAbortionAtFormat,
+			LastMatingAtFormat:   lastMatingAtFormat,
+			MatingTimes:          v.MatingTimes,
+			LastBullNumber:       v.LastBullNumber,
+			AbortionTimes:        v.AbortionTimes,
+		}
+	}
+	return res
+}
+
+func (c CowSlice) CanSaleToPB(cowKindMap map[pasturePb.CowKind_Kind]string) []*pasturePb.CanSalesReport {
+	res := make([]*pasturePb.CanSalesReport, len(c))
+	for i, v := range c {
+		cowKindName, lastWeightAtFormat, admissionAtFormat := "", "", ""
+		if name, ok := cowKindMap[v.CowKind]; ok {
+			cowKindName = name
+		}
+		if v.LastWeightAt > 0 {
+			lastWeightAtFormat = time.Unix(v.LastWeightAt, 0).Local().Format(LayoutDate2)
+		}
+		if v.AdmissionAt > 0 {
+			admissionAtFormat = time.Unix(v.AdmissionAt, 0).Local().Format(LayoutDate2)
+		}
+		res[i] = &pasturePb.CanSalesReport{
+			CowId:              int32(v.Id),
+			EarNumber:          v.EarNumber,
+			BatchNumber:        v.BatchNumber,
+			CowKindName:        cowKindName,
+			PenName:            v.PenName,
+			Weight:             float32(v.CurrentWeight) / 1000,
+			AdmissionAge:       v.AdmissionAge,
+			EnterWeight:        0,
+			EnterPrice:         0,
+			LastWeightAtFormat: lastWeightAtFormat,
+			DayAvgFeedCost:     0,
+			AllFeedCost:        0,
+			AllMedicalCharge:   0,
+			OtherCost:          0,
+			ProfitAndLoss:      0,
+			AdmissionAtFormat:  admissionAtFormat,
+			DayAvgWeight:       0,
+		}
+	}
+	return res
+}
+
+// CowBehaviorCurveResponse 脖环行为数据PC端
+type CowBehaviorCurveResponse struct {
+	Code int32                 `json:"code"`
+	Msg  string                `json:"msg"`
+	Data *CowBehaviorCurveData `json:"data"`
+}
+
+type CowBehaviorCurveData struct {
+	OriginalDateList []int32                                 `json:"originalDateList"` // 原始行为数据
+	ChangeDateList   []int32                                 `json:"changeDateList"`   // 变化数据
+	SumDateList      []int32                                 `json:"sumDateList"`      // 累计24小时数据
+	SumChewList      []int32                                 `json:"sumChewList"`      // 累计24小时咀嚼
+	DateTimeList     []string                                `json:"dateTimeList"`     // 时间数据
+	EstrusList       map[pasturePb.EstrusLevel_Kind][]string `json:"estrusList"`       // 发情预警
+	EventList        []*pasturePb.CowEvent                   `json:"eventList"`        // 事件数据
+	EventMap         map[pasturePb.EventType_Kind]string     `json:"eventMap"`         // 所有事件
+	RuminaChange     []int32                                 `json:"ruminaChange"`     // 反刍变化
+	LowActivity      int32                                   `json:"lowActivity"`      // 低活动量参数
+	MiddleActivity   int32                                   `json:"middleActivity"`   // 中活动量行数
+	IQR1             []int32                                 `json:"IQR1"`             // 1IQR
+	IQR3             []int32                                 `json:"IQR3"`             // 3IQR
+}

+ 111 - 0
model/neck_active_habit.go

@@ -303,6 +303,117 @@ func (n NeckActiveHabitSlice) ToPB2(dataBetween []string, groupNeckActiveHabitLi
 	return data
 }
 
+func (n NeckActiveHabitSlice) ToPBApp(curveNameList []string) *pasturePb.CowNeckRingAppData {
+	res := &pasturePb.CowNeckRingAppData{
+		DateTime: make([]string, 0),
+		Activity: &pasturePb.ActivityData{
+			ChangeActivity: make([]int32, 0),
+			ChangeRumina:   make([]int32, 0),
+		},
+		Rumina: &pasturePb.RuminaData{
+			ChangeRumina: make([]int32, 0),
+			SumRumina:    make([]int32, 0),
+		},
+		Chew: &pasturePb.ChewData{
+			ChangeChew: make([]int32, 0),
+			SumChew:    make([]int32, 0),
+		},
+		Intake: &pasturePb.SumData{
+			SumData: make([]int32, 0),
+		},
+		Inactive: &pasturePb.SumData{
+			SumData: make([]int32, 0),
+		},
+		Immobility: &pasturePb.SumData{
+			SumData: make([]int32, 0),
+		},
+		Gasp: &pasturePb.SumData{
+			SumData: make([]int32, 0),
+		},
+		EstrusList: make([]string, 0),
+		EventList:  make([]*pasturePb.EventDetail, 0),
+	}
+	initFrameId := int32(0)
+	dateFrameMap := make(map[string][]int32)
+
+	for _, v := range n {
+		if dateFrameMap[v.HeatDate] == nil {
+			initFrameId = 0
+			dateFrameMap[v.HeatDate] = make([]int32, 0)
+		}
+		// 补全结尾不够的数据
+		if initFrameId == 0 && len(res.DateTime) > 0 {
+			lastDateTime := res.DateTime[len(res.DateTime)-1]
+			lastDatePare := strings.Split(lastDateTime, " ")
+			if len(lastDatePare) == 2 {
+				lastDay := lastDatePare[0]
+				lastHourStr := lastDatePare[1]
+				lastHour, _ := strconv.ParseInt(lastHourStr, 10, 64)
+				maxHour := util.ExpectedFrameIDs[len(util.ExpectedFrameIDs)-1]
+				xframeId := int32(lastHour-1)/2 + 1
+				if xframeId != maxHour {
+					for ; xframeId <= maxHour; xframeId++ {
+						res.DateTime = append(res.DateTime, fmt.Sprintf("%s %02d", lastDay, util.ExpectedFrameIDs[xframeId]*2+1))
+						res.Activity.ChangeActivity = append(res.Activity.ChangeActivity, 0)
+						res.Activity.ChangeRumina = append(res.Activity.ChangeRumina, 0)
+						res.Rumina.ChangeRumina = append(res.Rumina.ChangeRumina, 0)
+						res.Rumina.SumRumina = append(res.Rumina.SumRumina, 0)
+						res.Intake.SumData = append(res.Intake.SumData, 0)
+						res.Inactive.SumData = append(res.Inactive.SumData, 0)
+						res.Chew.ChangeChew = append(res.Chew.ChangeChew, 0)
+						res.Chew.SumChew = append(res.Chew.SumChew, 0)
+						res.Immobility.SumData = append(res.Immobility.SumData, 0)
+					}
+				}
+			}
+		}
+		expectedFrameId := util.ExpectedFrameIDs[initFrameId]
+		if expectedFrameId != v.Frameid {
+			maxFrameId := int32(math.Abs(float64(expectedFrameId - v.Frameid)))
+			for ; expectedFrameId < maxFrameId; expectedFrameId++ {
+				res.DateTime = append(res.DateTime, fmt.Sprintf("%s %02d", v.HeatDate, util.ExpectedFrameIDs[expectedFrameId]*2+1))
+				res.Activity.ChangeActivity = append(res.Activity.ChangeActivity, 0)
+				res.Activity.ChangeRumina = append(res.Activity.ChangeRumina, 0)
+				res.Rumina.ChangeRumina = append(res.Rumina.ChangeRumina, 0)
+				res.Rumina.SumRumina = append(res.Rumina.SumRumina, 0)
+				res.Intake.SumData = append(res.Intake.SumData, 0)
+				res.Inactive.SumData = append(res.Inactive.SumData, 0)
+				res.Chew.ChangeChew = append(res.Chew.ChangeChew, 0)
+				res.Chew.SumChew = append(res.Chew.SumChew, 0)
+				res.Immobility.SumData = append(res.Immobility.SumData, 0)
+			}
+			initFrameId = expectedFrameId
+		}
+
+		// 格式化为到小时的字符串
+		parsedTime, _ := util.TimeParseLocal(LayoutTime, v.ActiveTime)
+		hourStr := parsedTime.Format(LayoutHour)
+		res.DateTime = append(res.DateTime, hourStr)
+
+		// 活动量
+		changeDateList := v.ChangeFilter * v.FilterCorrect / 100
+		if changeDateList == DefaultChangeFilter {
+			changeDateList = 0
+		}
+		res.Activity.ChangeActivity = append(res.Activity.ChangeActivity, changeDateList)
+		res.Activity.ChangeRumina = append(res.Activity.ChangeRumina, v.RuminaFilter)
+		// 反刍
+		res.Rumina.ChangeRumina = append(res.Rumina.ChangeRumina, v.RuminaFilter)
+		res.Rumina.SumRumina = append(res.Rumina.SumRumina, v.SumRumina)
+		// 咀嚼
+		res.Chew.ChangeChew = append(res.Chew.ChangeChew, v.ChewFilter)
+		res.Chew.SumChew = append(res.Chew.SumChew, v.SumRumina+v.SumIntake)
+		// 采食
+		res.Intake.SumData = append(res.Intake.SumData, v.SumRumina+v.SumIntake)
+		// 休息
+		res.Inactive.SumData = append(res.Inactive.SumData, v.SumInactive)
+		// 静止
+		res.Immobility.SumData = append(res.Immobility.SumData, 60*24-v.SumActive)
+		initFrameId++
+	}
+	return res
+}
+
 type MaxHabitIdModel struct {
 	Id int64 `json:"id"`
 }

+ 2 - 276
module/backend/cow.go

@@ -9,7 +9,6 @@ import (
 	"net/http"
 	"sort"
 	"strings"
-	"sync"
 	"time"
 
 	"github.com/nicksnyder/go-i18n/v2/i18n"
@@ -246,7 +245,7 @@ func (s *StoreEntry) BehaviorCurve(ctx context.Context, req *pasturePb.CowBehavi
 	nowTime := time.Now().Local()
 	nowDayZero := util.TimeParseLocalUnix(nowTime.Format(model.LayoutDate2))
 	endDataTime := nowTime.Format(model.LayoutDate2)
-	startDataTime := nowTime.AddDate(0, 0, -50).Format(model.LayoutDate2)
+	startDataTime := nowTime.AddDate(0, 0, -model.NeckRingDataDays).Format(model.LayoutDate2)
 
 	dayRange, err := util.GetDaysBetween(startDataTime, endDataTime)
 	if err != nil {
@@ -294,7 +293,7 @@ func (s *StoreEntry) BehaviorCurve(ctx context.Context, req *pasturePb.CowBehavi
 	if err = s.DB.Table(eventLog.TableName()).
 		Where("cow_id = ?", cowInfo.Id).
 		Where("pasture_id = ?", userModel.AppPasture.Id).
-		Where("event_at BETWEEN ? AND ?", nowDayZero-(30*86400), nowDayZero+86400).
+		Where("event_at BETWEEN ? AND ?", nowDayZero-(model.NeckRingDataDays*86400), nowDayZero+86400).
 		Order("event_at").
 		Find(&eventLogList).Error; err != nil {
 		return nil, xerr.WithStack(err)
@@ -438,276 +437,3 @@ func (s *StoreEntry) CowIQR(cowInfo *model.Cow, curveName string, dayRange []str
 	}
 	return q1, q3
 }
-
-func (s *StoreEntry) GetNeckActiveHabitsConcurrent(cowInfo *model.Cow, cowIds []int64, dayRange []string) ([]*model.NeckActiveHabit, error) {
-	const (
-		batchSize   = 100
-		workerCount = 5
-	)
-
-	resultsChan := make(chan []*model.NeckActiveHabit)
-	errChan := make(chan error, 1) // 缓冲 1,避免 goroutine 阻塞
-	doneChan := make(chan struct{})
-
-	var (
-		neckActiveHabitList []*model.NeckActiveHabit
-		mu                  sync.Mutex
-	)
-
-	// 结果收集 goroutine
-	go func() {
-		defer close(doneChan)
-		for batchResults := range resultsChan {
-			mu.Lock()
-			neckActiveHabitList = append(neckActiveHabitList, batchResults...)
-			mu.Unlock()
-		}
-	}()
-
-	sem := make(chan struct{}, workerCount)
-	var wg sync.WaitGroup
-
-	// 分发任务
-	go func() {
-		defer func() {
-			wg.Wait()          // 等待所有 goroutine 完成
-			close(resultsChan) // 安全关闭
-		}()
-
-		for i := 0; i < len(cowIds); i += batchSize {
-			end := i + batchSize
-			if end > len(cowIds) {
-				end = len(cowIds)
-			}
-			batch := cowIds[i:end]
-
-			wg.Add(1)
-			sem <- struct{}{} // 获取令牌(可能阻塞,但不会死锁)
-
-			go func(batch []int64) {
-				defer func() {
-					<-sem // 释放令牌
-					wg.Done()
-				}()
-
-				var batchResults []*model.NeckActiveHabit
-				if err := s.DB.Table(new(model.NeckActiveHabit).TableName()).
-					Select("rumina,intake,inactive,gasp,high,active,active_time").
-					Where("pasture_id = ?", cowInfo.PastureId).
-					Where("is_show = ?", pasturePb.IsShow_Ok).
-					Where("cow_id IN (?)", batch).
-					Where("heat_date BETWEEN ? AND ?", dayRange[0], dayRange[len(dayRange)-1]).
-					Find(&batchResults).Error; err != nil {
-					select {
-					case errChan <- fmt.Errorf("batch query error: %v", err):
-					default:
-					}
-					return
-				}
-
-				if len(batchResults) > 0 {
-					resultsChan <- batchResults
-				}
-			}(batch)
-		}
-	}()
-
-	// 等待结果
-	select {
-	case err := <-errChan:
-		return nil, err
-	case <-doneChan:
-		return neckActiveHabitList, nil
-	}
-}
-
-func (s *StoreEntry) CowGrowthCurve(ctx context.Context, req *pasturePb.CowGrowthCurveRequest) (*pasturePb.CowGrowthCurveResponse, error) {
-	userModel, err := s.GetUserModel(ctx)
-	if err != nil {
-		return nil, xerr.WithStack(err)
-	}
-	cowInfo, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, req.EarNumber)
-	if err != nil {
-		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
-			MessageID:    "auth.errorCow",
-			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
-		})
-		return nil, xerr.Customf(messageId)
-	}
-
-	weightList := make([]*model.EventWeight, 0)
-	if err = s.DB.Table(new(model.EventWeight).TableName()).
-		Where("cow_id = ?", cowInfo.Id).
-		Where("pasture_id = ?", userModel.AppPasture.Id).
-		Order("weight_at").
-		Find(&weightList).Error; err != nil {
-		return nil, xerr.WithStack(err)
-	}
-
-	eventCowLogList := make([]*model.EventCowLog, 0)
-	eventCowLog := &model.EventCowLog{CowId: cowInfo.Id}
-	if err = s.DB.Table(eventCowLog.TableName()).
-		Where("pasture_id = ?", userModel.AppPasture.Id).
-		Where("cow_id = ?", cowInfo.Id).
-		Order("id desc").
-		Find(&eventCowLogList).Error; err != nil {
-		return nil, xerr.WithStack(err)
-	}
-
-	return &pasturePb.CowGrowthCurveResponse{
-		Code: http.StatusOK,
-		Msg:  "ok",
-		Data: model.EventWeightSlice(weightList).ToPB(eventCowLogList),
-	}, nil
-}
-
-func (s *StoreEntry) CowLactCurve(ctx context.Context, req *pasturePb.CowLactCurveRequest) (*pasturePb.CowLactCurveResponse, error) {
-	userModel, err := s.GetUserModel(ctx)
-	if err != nil {
-		return nil, xerr.WithStack(err)
-	}
-	cowInfo, err := s.GetCowInfoByCowId(ctx, userModel.AppPasture.Id, int64(req.CowId))
-	if err != nil {
-		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
-			MessageID:    "auth.errorCow",
-			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
-		})
-		return nil, xerr.Customf(messageId)
-	}
-
-	cowLactList := make([]*model.CowLact, 0)
-	if err = s.DB.Table(new(model.CowLact).TableName()).
-		Where("cow_id = ?", req.CowId).
-		Where("pasture_id = ?", userModel.AppPasture.Id).
-		Order("lact").
-		Find(&cowLactList).Error; err != nil {
-		return nil, xerr.WithStack(err)
-	}
-
-	data := &pasturePb.CowLactCurveData{
-		DateTime:            make(map[int32]string),
-		WeekAvgMilk:         make([]float32, 0),
-		DayMilk:             make([]float32, 0),
-		DHI:                 make([]float32, 0),
-		MilkProductionTrend: make([]float32, 0),
-		DayHigh:             make([]int32, 0),
-		DayRumina:           make([]int32, 0),
-		DayIntake:           make([]int32, 0),
-		DayInactive:         make([]int32, 0),
-		DayChew:             make([]int32, 0),
-		DayImmobility:       make([]int32, 0),
-		EstrusWarning:       make(map[int32]int32),
-	}
-
-	if len(cowLactList) <= 0 {
-		return &pasturePb.CowLactCurveResponse{
-			Code: http.StatusOK,
-			Msg:  "ok",
-			Data: data,
-		}, nil
-	}
-	starTime := ""
-	endTime := ""
-	cowLactMap := make(map[int32]*model.CowLact)
-	for _, v := range cowLactList {
-		cowLactMap[v.Lact] = v
-		if v.Lact == req.Lact {
-			starTime = v.StartTime
-		}
-	}
-	if st, ok := cowLactMap[req.Lact+1]; ok {
-		et, _ := util.TimeParseLocal(model.LayoutDate2, st.StartTime)
-		endTime = et.AddDate(0, 0, -1).Format(model.LayoutDate2)
-	} else {
-		endTime = time.Now().Local().Format(model.LayoutDate2)
-	}
-
-	neckRingList := make([]*model.NeckActiveHabit, 0)
-	if err = s.DB.Model(new(model.NeckActiveHabit)).
-		Where("neck_ring_number = ?", cowInfo.NeckRingNumber).
-		Where("pasture_id = ?", userModel.AppPasture.Id).
-		Where("cow_id > ?", 0).
-		Where("heat_date BETWEEN ? AND ?", starTime, endTime).
-		Order("heat_date, frameid").Find(&neckRingList).Error; err != nil {
-		return nil, xerr.WithStack(err)
-	}
-
-	return &pasturePb.CowLactCurveResponse{
-		Code: http.StatusOK,
-		Msg:  "ok",
-		Data: data,
-	}, nil
-}
-
-func (s *StoreEntry) BehaviorRate(ctx context.Context, req *pasturePb.CowBehaviorRateRequest) (*pasturePb.CowBehaviorRateResponse, error) {
-	userModel, err := s.GetUserModel(ctx)
-	if err != nil {
-		return nil, xerr.WithStack(err)
-	}
-	cowInfo, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, req.EarNumber)
-	if err != nil {
-		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
-			MessageID:    "auth.errorCow",
-			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
-		})
-		return nil, xerr.Customf(messageId)
-	}
-
-	if req.EndAt <= 0 || req.StartAt <= 0 || req.EndAt < req.StartAt {
-		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
-			MessageID: "auth.errorDateRange",
-		})
-		return nil, xerr.Customf(messageId)
-	}
-
-	t1 := time.Unix(int64(req.StartAt), 0).Local().Format(model.LayoutDate2)
-	t2 := time.Unix(int64(req.EndAt), 0).Local().Format(model.LayoutDate2)
-
-	dataBetween, err := util.GetDaysBetween(t1, t2)
-	if err != nil {
-		return nil, xerr.WithStack(err)
-	}
-
-	neckActiveHabitList := make([]*model.NeckActiveHabit, 0)
-	if err = s.DB.Model(new(model.NeckActiveHabit)).
-		Select("heat_date,SUM(rumina) as rumina,SUM(intake) as intake,SUM(inactive) as inactive,SUM(gasp) AS gasp").
-		Where("neck_ring_number = ?", cowInfo.NeckRingNumber).
-		Where("pasture_id = ?", userModel.AppPasture.Id).
-		Where("cow_id > ?", 0).
-		Where("heat_date BETWEEN ? AND ?", t1, t2).
-		Order("heat_date").
-		Group("heat_date").
-		Find(&neckActiveHabitList).Error; err != nil {
-	}
-
-	cowList := make([]*model.Cow, 0)
-	if err = s.DB.Model(new(model.Cow)).
-		Where("neck_ring_number != ?", "").
-		Where("pasture_id = ?", userModel.AppPasture.Id).
-		Where("pen_id = ?", cowInfo.PenId).
-		Find(&cowList).Error; err != nil {
-	}
-
-	neckRingNumbers := make([]string, 0)
-	for _, v := range cowList {
-		neckRingNumbers = append(neckRingNumbers, v.NeckRingNumber)
-	}
-
-	groupNeckActiveHabitList := make([]*model.NeckActiveHabit, 0)
-	if err = s.DB.Model(new(model.NeckActiveHabit)).
-		Select("heat_date,SUM(rumina) as rumina,SUM(intake) as intake,SUM(inactive) as inactive,SUM(gasp) AS gasp").
-		Where("neck_ring_number IN ?", neckRingNumbers).
-		Where("pasture_id = ?", userModel.AppPasture.Id).
-		Where("cow_id > ?", 0).
-		Where("heat_date BETWEEN ? AND ?", t1, t2).
-		Order("heat_date").
-		Group("heat_date").
-		Find(&groupNeckActiveHabitList).Error; err != nil {
-	}
-
-	return &pasturePb.CowBehaviorRateResponse{
-		Code: http.StatusOK,
-		Msg:  "ok",
-		Data: model.NeckActiveHabitSlice(neckActiveHabitList).ToPB2(dataBetween, groupNeckActiveHabitList),
-	}, nil
-}

+ 415 - 0
module/backend/cow_more.go

@@ -0,0 +1,415 @@
+package backend
+
+import (
+	"context"
+	"fmt"
+	"kpt-pasture/model"
+	"kpt-pasture/util"
+	"net/http"
+	"strings"
+	"sync"
+	"time"
+
+	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+	"gitee.com/xuyiping_admin/pkg/xerr"
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+)
+
+func (s *StoreEntry) GetNeckActiveHabitsConcurrent(cowInfo *model.Cow, cowIds []int64, dayRange []string) ([]*model.NeckActiveHabit, error) {
+	const (
+		batchSize   = 100
+		workerCount = 5
+	)
+
+	resultsChan := make(chan []*model.NeckActiveHabit)
+	errChan := make(chan error, 1) // 缓冲 1,避免 goroutine 阻塞
+	doneChan := make(chan struct{})
+
+	var (
+		neckActiveHabitList []*model.NeckActiveHabit
+		mu                  sync.Mutex
+	)
+
+	// 结果收集 goroutine
+	go func() {
+		defer close(doneChan)
+		for batchResults := range resultsChan {
+			mu.Lock()
+			neckActiveHabitList = append(neckActiveHabitList, batchResults...)
+			mu.Unlock()
+		}
+	}()
+
+	sem := make(chan struct{}, workerCount)
+	var wg sync.WaitGroup
+
+	// 分发任务
+	go func() {
+		defer func() {
+			wg.Wait()          // 等待所有 goroutine 完成
+			close(resultsChan) // 安全关闭
+		}()
+
+		for i := 0; i < len(cowIds); i += batchSize {
+			end := i + batchSize
+			if end > len(cowIds) {
+				end = len(cowIds)
+			}
+			batch := cowIds[i:end]
+
+			wg.Add(1)
+			sem <- struct{}{} // 获取令牌(可能阻塞,但不会死锁)
+
+			go func(batch []int64) {
+				defer func() {
+					<-sem // 释放令牌
+					wg.Done()
+				}()
+
+				var batchResults []*model.NeckActiveHabit
+				if err := s.DB.Table(new(model.NeckActiveHabit).TableName()).
+					Select("rumina,intake,inactive,gasp,high,active,active_time").
+					Where("pasture_id = ?", cowInfo.PastureId).
+					Where("is_show = ?", pasturePb.IsShow_Ok).
+					Where("cow_id IN (?)", batch).
+					Where("heat_date BETWEEN ? AND ?", dayRange[0], dayRange[len(dayRange)-1]).
+					Find(&batchResults).Error; err != nil {
+					select {
+					case errChan <- fmt.Errorf("batch query error: %v", err):
+					default:
+					}
+					return
+				}
+
+				if len(batchResults) > 0 {
+					resultsChan <- batchResults
+				}
+			}(batch)
+		}
+	}()
+
+	// 等待结果
+	select {
+	case err := <-errChan:
+		return nil, err
+	case <-doneChan:
+		return neckActiveHabitList, nil
+	}
+}
+
+func (s *StoreEntry) CowGrowthCurve(ctx context.Context, req *pasturePb.CowGrowthCurveRequest) (*pasturePb.CowGrowthCurveResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	cowInfo, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, req.EarNumber)
+	if err != nil {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "auth.errorCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return nil, xerr.Customf(messageId)
+	}
+
+	weightList := make([]*model.EventWeight, 0)
+	if err = s.DB.Table(new(model.EventWeight).TableName()).
+		Where("cow_id = ?", cowInfo.Id).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Order("weight_at").
+		Find(&weightList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	eventCowLogList := make([]*model.EventCowLog, 0)
+	eventCowLog := &model.EventCowLog{CowId: cowInfo.Id}
+	if err = s.DB.Table(eventCowLog.TableName()).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("cow_id = ?", cowInfo.Id).
+		Order("id desc").
+		Find(&eventCowLogList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &pasturePb.CowGrowthCurveResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: model.EventWeightSlice(weightList).ToPB(eventCowLogList),
+	}, nil
+}
+
+func (s *StoreEntry) CowLactCurve(ctx context.Context, req *pasturePb.CowLactCurveRequest) (*pasturePb.CowLactCurveResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	cowInfo, err := s.GetCowInfoByCowId(ctx, userModel.AppPasture.Id, int64(req.CowId))
+	if err != nil {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "auth.errorCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return nil, xerr.Customf(messageId)
+	}
+
+	cowLactList := make([]*model.CowLact, 0)
+	if err = s.DB.Table(new(model.CowLact).TableName()).
+		Where("cow_id = ?", req.CowId).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Order("lact").
+		Find(&cowLactList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	data := &pasturePb.CowLactCurveData{
+		DateTime:            make(map[int32]string),
+		WeekAvgMilk:         make([]float32, 0),
+		DayMilk:             make([]float32, 0),
+		DHI:                 make([]float32, 0),
+		MilkProductionTrend: make([]float32, 0),
+		DayHigh:             make([]int32, 0),
+		DayRumina:           make([]int32, 0),
+		DayIntake:           make([]int32, 0),
+		DayInactive:         make([]int32, 0),
+		DayChew:             make([]int32, 0),
+		DayImmobility:       make([]int32, 0),
+		EstrusWarning:       make(map[int32]int32),
+	}
+
+	if len(cowLactList) <= 0 {
+		return &pasturePb.CowLactCurveResponse{
+			Code: http.StatusOK,
+			Msg:  "ok",
+			Data: data,
+		}, nil
+	}
+	starTime := ""
+	endTime := ""
+	cowLactMap := make(map[int32]*model.CowLact)
+	for _, v := range cowLactList {
+		cowLactMap[v.Lact] = v
+		if v.Lact == req.Lact {
+			starTime = v.StartTime
+		}
+	}
+	if st, ok := cowLactMap[req.Lact+1]; ok {
+		et, _ := util.TimeParseLocal(model.LayoutDate2, st.StartTime)
+		endTime = et.AddDate(0, 0, -1).Format(model.LayoutDate2)
+	} else {
+		endTime = time.Now().Local().Format(model.LayoutDate2)
+	}
+
+	neckRingList := make([]*model.NeckActiveHabit, 0)
+	if err = s.DB.Model(new(model.NeckActiveHabit)).
+		Where("neck_ring_number = ?", cowInfo.NeckRingNumber).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("cow_id > ?", 0).
+		Where("heat_date BETWEEN ? AND ?", starTime, endTime).
+		Order("heat_date, frameid").Find(&neckRingList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &pasturePb.CowLactCurveResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: data,
+	}, nil
+}
+
+func (s *StoreEntry) BehaviorRate(ctx context.Context, req *pasturePb.CowBehaviorRateRequest) (*pasturePb.CowBehaviorRateResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	cowInfo, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, req.EarNumber)
+	if err != nil {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "auth.errorCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return nil, xerr.Customf(messageId)
+	}
+
+	if req.EndAt <= 0 || req.StartAt <= 0 || req.EndAt < req.StartAt {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.errorDateRange",
+		})
+		return nil, xerr.Customf(messageId)
+	}
+
+	t1 := time.Unix(int64(req.StartAt), 0).Local().Format(model.LayoutDate2)
+	t2 := time.Unix(int64(req.EndAt), 0).Local().Format(model.LayoutDate2)
+
+	dataBetween, err := util.GetDaysBetween(t1, t2)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	neckActiveHabitList := make([]*model.NeckActiveHabit, 0)
+	if err = s.DB.Model(new(model.NeckActiveHabit)).
+		Select("heat_date,SUM(rumina) as rumina,SUM(intake) as intake,SUM(inactive) as inactive,SUM(gasp) AS gasp").
+		Where("neck_ring_number = ?", cowInfo.NeckRingNumber).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("cow_id > ?", 0).
+		Where("heat_date BETWEEN ? AND ?", t1, t2).
+		Order("heat_date").
+		Group("heat_date").
+		Find(&neckActiveHabitList).Error; err != nil {
+	}
+
+	cowList := make([]*model.Cow, 0)
+	if err = s.DB.Model(new(model.Cow)).
+		Where("neck_ring_number != ?", "").
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("pen_id = ?", cowInfo.PenId).
+		Find(&cowList).Error; err != nil {
+	}
+
+	neckRingNumbers := make([]string, 0)
+	for _, v := range cowList {
+		neckRingNumbers = append(neckRingNumbers, v.NeckRingNumber)
+	}
+
+	groupNeckActiveHabitList := make([]*model.NeckActiveHabit, 0)
+	if err = s.DB.Model(new(model.NeckActiveHabit)).
+		Select("heat_date,SUM(rumina) as rumina,SUM(intake) as intake,SUM(inactive) as inactive,SUM(gasp) AS gasp").
+		Where("neck_ring_number IN ?", neckRingNumbers).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("cow_id > ?", 0).
+		Where("heat_date BETWEEN ? AND ?", t1, t2).
+		Order("heat_date").
+		Group("heat_date").
+		Find(&groupNeckActiveHabitList).Error; err != nil {
+	}
+
+	return &pasturePb.CowBehaviorRateResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: model.NeckActiveHabitSlice(neckActiveHabitList).ToPB2(dataBetween, groupNeckActiveHabitList),
+	}, nil
+}
+
+func (s *StoreEntry) BehaviorCurveApp(ctx context.Context, req *pasturePb.CowBehaviorCurveRequest) (*pasturePb.CowNeckRingAppResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	if req.Days > model.NeckRingDataDays+10 {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.neckRingAppDays",
+		})
+		return nil, xerr.Customf(messageId)
+	}
+
+	cowInfo, err := s.GetCowEventByEarNumber(ctx, userModel.AppPasture.Id, req.EarNumber)
+	if err != nil {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "auth.errorCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return nil, xerr.Customf(messageId)
+	}
+
+	nowTime := time.Now().Local()
+	nowDayZero := util.TimeParseLocalUnix(nowTime.Format(model.LayoutDate2))
+	endDataTime := nowTime.Format(model.LayoutDate2)
+	startDataTime := nowTime.AddDate(0, 0, int(-req.Days)+1).Format(model.LayoutDate2)
+
+	dayRange, err := util.GetDaysBetween(startDataTime, endDataTime)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	if len(dayRange) <= 0 {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.errorDateRange",
+		})
+		return nil, xerr.Customf(messageId)
+	}
+
+	// 行为曲线数据
+	neckActiveHabitList := make([]*model.NeckActiveHabit, 0)
+	if err = s.DB.Table(new(model.NeckActiveHabit).TableName()).
+		Where("neck_ring_number = ?", cowInfo.NeckRingNumber).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("is_show = ?", pasturePb.IsShow_Ok).
+		Where("cow_id > ?", 0).
+		Where("heat_date IN (?)", dayRange).
+		Order("heat_date, frameid").
+		Find(&neckActiveHabitList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	data := model.NeckActiveHabitSlice(neckActiveHabitList).ToPBApp(req.CurveNameList)
+	eventMapList := s.EventTypeMap(userModel)
+	for k, v := range eventMapList {
+		if k == pasturePb.EventType_Enter || k == pasturePb.EventType_Body_Score || k == pasturePb.EventType_Birth ||
+			k == pasturePb.EventType_Weaning || k == pasturePb.EventType_Sale || k == pasturePb.EventType_Weight ||
+			k == pasturePb.EventType_Castrated || k == pasturePb.EventType_Perinatal_Transition || k == pasturePb.EventType_Insect_Repellent {
+			continue
+		}
+		data.EventMap = append(data.EventMap, &pasturePb.EventMap{
+			EventTypeKind: k,
+			EventTypeName: v,
+		})
+	}
+	data.LowActivity = model.LowActivity
+	data.MiddleActivity = model.MiddleActivity
+	// 牛只事件列表
+	eventLogList := make([]*model.EventCowLog, 0)
+	eventLog := &model.EventCowLog{CowId: cowInfo.Id}
+	if err = s.DB.Table(eventLog.TableName()).
+		Where("cow_id = ?", cowInfo.Id).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("event_at BETWEEN ? AND ?", nowDayZero-(int64(req.Days+1)*86400), nowDayZero).
+		Order("event_at").
+		Find(&eventLogList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	for _, v := range eventLogList {
+		if v.EventType == pasturePb.EventType_Enter || v.EventType == pasturePb.EventType_Body_Score ||
+			v.EventType == pasturePb.EventType_Birth || v.EventType == pasturePb.EventType_Weaning ||
+			v.EventType == pasturePb.EventType_Sale || v.EventType == pasturePb.EventType_Weight ||
+			v.EventType == pasturePb.EventType_Castrated {
+			continue
+		}
+
+		if v.EventAt <= 0 {
+			continue
+		}
+
+		eventAt := time.Unix(v.EventAt, 0).Local()
+		data.EventList = append(data.EventList, &pasturePb.EventDetail{
+			EventTypeKind:    v.EventType,
+			EventTypeName:    v.EventTypeName,
+			EventDescription: v.EventDescription,
+			EventAtFormat:    fmt.Sprintf("%s 09", eventAt.Format(model.LayoutDate2)),
+		})
+	}
+
+	// 发情数据
+	estrusList := make([]*model.NeckRingEstrus, 0)
+	if err = s.DB.Model(new(model.NeckRingEstrus)).
+		Select("id,active_level,MAX(active_time) AS active_time,is_peak").
+		Where("cow_id = ?", cowInfo.Id).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("active_time BETWEEN ? AND ?", fmt.Sprintf("%s 00:00:00", startDataTime), fmt.Sprintf("%s 23:59:59", endDataTime)).
+		Where("is_peak = ?", pasturePb.IsShow_Ok).
+		Group("first_time").
+		Find(&estrusList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	for _, v := range estrusList {
+		if data.EstrusList == nil {
+			data.EstrusList = make([]string, 0)
+		}
+		data.EstrusList = append(data.EstrusList, strings.TrimSuffix(v.ActiveTime, ":00:00")+fmt.Sprintf("/%d", v.ActiveLevel))
+	}
+
+	return &pasturePb.CowNeckRingAppResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: data,
+	}, nil
+}

+ 1 - 0
module/backend/interface.go

@@ -257,6 +257,7 @@ type CowService interface {
 	List(ctx context.Context, req *pasturePb.SearchEventRequest, pagination *pasturePb.PaginationModel) (*pasturePb.SearchCowListResponse, error)
 	EventList(ctx context.Context, req *pasturePb.SearchCowEventListRequest, pagination *pasturePb.PaginationModel) (*pasturePb.CowEventListResponse, error)
 	BehaviorCurve(ctx context.Context, req *pasturePb.CowBehaviorCurveRequest) (*model.CowBehaviorCurveResponse, error)
+	BehaviorCurveApp(ctx context.Context, req *pasturePb.CowBehaviorCurveRequest) (*pasturePb.CowNeckRingAppResponse, error)
 	CowGrowthCurve(ctx context.Context, req *pasturePb.CowGrowthCurveRequest) (*pasturePb.CowGrowthCurveResponse, error)
 	CowLactCurve(ctx context.Context, req *pasturePb.CowLactCurveRequest) (*pasturePb.CowLactCurveResponse, error)
 	BehaviorRate(ctx context.Context, req *pasturePb.CowBehaviorRateRequest) (*pasturePb.CowBehaviorRateResponse, error)