Przeglądaj źródła

cow: behaviorRate 行为占比

Yi 1 tydzień temu
rodzic
commit
8e6680fcb5

+ 1 - 1
go.mod

@@ -3,7 +3,7 @@ module kpt-pasture
 go 1.17
 
 require (
-	gitee.com/xuyiping_admin/go_proto v0.0.0-20250408075038-dd76bfdd8a73
+	gitee.com/xuyiping_admin/go_proto v0.0.0-20250410061346-7010a8affda4
 	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

+ 14 - 0
go.sum

@@ -146,6 +146,20 @@ gitee.com/xuyiping_admin/go_proto v0.0.0-20250408072509-1117ad78b23f h1:VWi6G/NW
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250408072509-1117ad78b23f/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250408075038-dd76bfdd8a73 h1:QNo+OSvJtuCgxvYreVcqZJXHFafnaD3/NTzzQbWNigo=
 gitee.com/xuyiping_admin/go_proto v0.0.0-20250408075038-dd76bfdd8a73/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409012212-55a38ade5fbe h1:dUQ3PYq07bX0q8E3ZF/sq6TdS12IfE4ogLzCmK1Im5U=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409012212-55a38ade5fbe/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409054106-c044078978f9 h1:HjAO7MAZz/Vu3dRE6GI7kxx31RBHH0lQ5WXaWvtwz3k=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409054106-c044078978f9/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409060243-3b624c8d5ece h1:tHo+IVad4bQ9eQDSdVEGfQZ3hbGr3w6c/vi4ibQzt08=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409060243-3b624c8d5ece/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409093335-d8b013bd5bab h1:9nfap+BBJeP17KlVGViBUXDf1u84YKGN1Qliz/2VvBo=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409093335-d8b013bd5bab/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409093847-cb281b5a01b2 h1:KPl+ZJuQKgGQfgxfJn/RtX1+YwzFI5x0iEDeAvHV6Cg=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409093847-cb281b5a01b2/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409095220-1fc7ef026108 h1:Sz/paz5RJjGg2C50xE3xuFl2aVkQs31zRstVep+xejQ=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250409095220-1fc7ef026108/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250410061346-7010a8affda4 h1:CJbOeCcCLtUX2ht3wPqdgrSUd3RTvtSNPeiHX8rlUtQ=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20250410061346-7010a8affda4/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=

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

@@ -318,7 +318,7 @@ func PenBehaviorAnalysis(c *gin.Context) {
 	if err := valid.ValidateStruct(&req,
 		valid.Field(&req.StartAt, valid.Required),
 		valid.Field(&req.EndAt, valid.Required),
-		valid.Field(&req.BarnId, valid.Required),
+		valid.Field(&req.PenId, valid.Required),
 	); err != nil {
 		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
 		return

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

@@ -149,6 +149,30 @@ func LactCurve(c *gin.Context) {
 	ginutil.JSONResp(c, res)
 }
 
+func BehaviorRate(c *gin.Context) {
+	var req pasturePb.CowBehaviorRateRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.CowId, valid.Required),
+		valid.Field(&req.StartTime, valid.Required),
+		valid.Field(&req.EndTime, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.BehaviorRate(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}
+
 // IndicatorsComparison 指标对比
 func IndicatorsComparison(c *gin.Context) {
 	var req pasturePb.IndicatorsComparisonRequest

+ 7 - 32
http/handler/upload/upload.go

@@ -3,9 +3,9 @@ package upload
 import (
 	"fmt"
 	"kpt-pasture/config"
+	"kpt-pasture/http/middleware"
 	"net/http"
 	"os"
-	"time"
 
 	"gitee.com/xuyiping_admin/pkg/apierr"
 	"gitee.com/xuyiping_admin/pkg/xerr"
@@ -13,6 +13,7 @@ import (
 )
 
 func Photos(c *gin.Context) {
+
 	form, err := c.MultipartForm()
 	if err != nil {
 		apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Customf("No multipartForm: %s", err.Error()))
@@ -25,39 +26,13 @@ func Photos(c *gin.Context) {
 		return
 	}
 
-	workDir := fmt.Sprintf("%s", config.WorkDir)
-	pathDir := fmt.Sprintf("/files/photos/%s", time.Now().Format("20060102"))
-	saveDir := fmt.Sprintf("%s/%s", workDir, pathDir)
-	if _, err = os.Stat(saveDir); os.IsNotExist(err) {
-		if err = os.MkdirAll(saveDir, 0755); err != nil {
-			apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Customf("创建目录失败: %s", err.Error()))
-			return
-		}
+	res, err := middleware.BackendOperation(c).OpsService.Photos(c, files)
+	if err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
 	}
 
-	// 处理每个文件
-	filePaths := make([]string, len(files))
-	timestamp := time.Now().Unix()
-	for i, file := range files {
-		if file.Header.Get("Content-Type") != "image/jpeg" &&
-			file.Header.Get("Content-Type") != "image/png" &&
-			file.Header.Get("Content-Type") != "image/gif" {
-			apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Customf("图片格式错误: %s", file.Filename))
-			return
-		}
-		if file.Size > 1024*1024*5 {
-			apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Custom("单个图片文件不能超过5MB"))
-			return
-		}
-		fpath := fmt.Sprintf("%s/%d_%d_%s", saveDir, timestamp, i+1, file.Filename)
-		urlPath := fmt.Sprintf("%s/%d_%d_%s", pathDir, timestamp, i+1, file.Filename)
-		if err = c.SaveUploadedFile(file, fpath); err != nil {
-			apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Customf("保存文件失败: %s", err.Error()))
-			return
-		}
-		filePaths[i] = urlPath
-	}
-	c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "Msg": "ok", "data": filePaths})
+	c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "Msg": "ok", "data": res})
 }
 
 func Files(c *gin.Context) {

+ 1 - 0
http/route/cow_api.go

@@ -19,6 +19,7 @@ func CowAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		cowRoute.POST("/behavior/curve", cow.BehaviorCurve)
 		cowRoute.POST("/growth/curve", cow.GrowthCurve)
 		cowRoute.POST("/lact/curve", cow.LactCurve)
+		cowRoute.POST("/behavior/rate", cow.BehaviorRate)
 
 		searchRoute := authRouteGroup(s, "/api/v1/search/")
 		searchRoute.POST("/indicators/comparison", cow.IndicatorsComparison)

+ 2 - 7
model/cow.go

@@ -204,7 +204,6 @@ func (c *Cow) UnForbiddenMatingUpdate() {
 type CowSlice []*Cow
 
 func (c CowSlice) ToPB(
-	penMap map[int32]*Pen,
 	cowTypeMap map[pasturePb.CowType_Kind]string,
 	breedStatusMap map[pasturePb.BreedStatus_Kind]string,
 	cowKindMap map[pasturePb.CowKind_Kind]string,
@@ -214,11 +213,6 @@ func (c CowSlice) ToPB(
 ) []*pasturePb.CowDetails {
 	res := make([]*pasturePb.CowDetails, len(c))
 	for i, v := range c {
-		penName := ""
-		if pen, ok := penMap[v.PenId]; ok {
-			penName = pen.Name
-		}
-
 		sex := "公"
 		if v.Sex == pasturePb.Genders_Female {
 			sex = "母"
@@ -284,7 +278,7 @@ func (c CowSlice) ToPB(
 			CowId:                     int32(v.Id),
 			Sex:                       sex,
 			NeckRingNumber:            v.NeckRingNumber,
-			PenName:                   penName,
+			PenName:                   v.PenName,
 			Lact:                      v.Lact,
 			CowTypeName:               cowTypeMap[v.CowType],
 			CowType:                   v.CowType,
@@ -296,6 +290,7 @@ func (c CowSlice) ToPB(
 			CurrentWeight:             float32(v.CurrentWeight) / 1000,
 			CurrentHeight:             int32(v.CurrentHeight),
 			DayAge:                    v.DayAge,
+			AdmissionAge:              v.AdmissionAge,
 			SourceName:                cowSourceMap[v.SourceKind],
 			MotherNumber:              v.MotherNumber,
 			FatherNumber:              v.FatherNumber,

+ 1 - 0
model/event_sale.go

@@ -97,6 +97,7 @@ func (e EventSaleSlice) ToPB(eventSaleCarMap map[int64][]*EventSaleCar, eventSal
 			SaleTicket:       strings.Split(v.SaleTicker, ","),
 			QuarantineReport: strings.Split(v.QuarantineReport, ","),
 			SaleVehicleItems: saleVehicleItems,
+			SaleCount:        int32(len(eventSaleCowList)),
 		}
 	}
 	return res

+ 78 - 49
model/milk_original.go

@@ -1,51 +1,54 @@
 package model
 
+import pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+
 type MilkOriginal struct {
-	Id               int64   `json:"id"`
-	PastureId        int64   `json:"pastureId"`
-	CowId            int64   `json:"cowId"`
-	EarNumber        string  `json:"earNumber"`
-	EleEarNumber     string  `json:"eleEarNumber"`
-	PenId            int32   `json:"penId"`
-	PenName          string  `json:"penName"`
-	MilkHallBrand    string  `json:"milkHallBrand"`
-	MilkDate         string  `json:"milkDate"`
-	MilkWeight       float64 `json:"milkWeight"`
-	StartTime        string  `json:"startTime"`
-	InitialTime      string  `json:"initialTime"`
-	AttachTime       string  `json:"attachTime"`
-	AttachAdjustTime string  `json:"attachAdjustTime"`
-	DetacherTime     string  `json:"detacherTime"`
-	EndTime          string  `json:"endTime"`
-	DetacherAddress  int64   `json:"detacherAddress"`
-	Conductivity     int32   `json:"conductivity"`
-	CowActivity      int32   `json:"cowActivity"`
-	Source           int8    `json:"source"`
-	MilkHallNumber   string  `json:"milkHallNumber"`
-	Shifts           int32   `json:"shifts"`
-	Load             int32   `json:"load"`
-	Nattach          int32   `json:"nattach"`
-	RecognitionTime  string  `json:"recognitionTime"`
-	IsYieldLow       int8    `json:"isYieldLow"`
-	PeakFlow         float64 `json:"peakFlow"`
-	AvgFlow          float64 `json:"avgFlow"`
-	Duration         float64 `json:"duration"`
-	PeakFlowTime     int32   `json:"peakFlowTime"`
-	LowFlowTime      int32   `json:"lowFlowTime"`
-	YieldPercentage  int32   `json:"yieldPercentage"`
-	ActualMilkTime   string  `json:"actualMilkTime"`
-	KickOffs         bool    `json:"kickOffs"`
-	Blocks           int8    `json:"blocks"`
-	Slips            int8    `json:"slips"`
-	ManualDetach     int8    `json:"manualDetach"`
-	TakeOffFlow      float64 `json:"takeOffFlow"`
-	LowMilkFlowPc    int64   `json:"lowMilkFlowPc"`
-	Flow0to15        int64   `json:"flow0To15"`
-	Flow15to30       int64   `json:"flow15To30"`
-	Flow30to60       int64   `json:"flow30To60"`
-	Flow60to120      int64   `json:"flow60To120"`
-	CreatedAt        int64   `json:"createdAt"`
-	UpdatedAt        int64   `json:"updatedAt"`
+	Id               int64                 `json:"id"`
+	PastureId        int64                 `json:"pastureId"`
+	CowId            int64                 `json:"cowId"`
+	EarNumber        string                `json:"earNumber"`
+	EleEarNumber     string                `json:"eleEarNumber"`
+	PenId            int32                 `json:"penId"`
+	PenName          string                `json:"penName"`
+	MilkHallBrand    string                `json:"milkHallBrand"`
+	MilkDate         string                `json:"milkDate"`
+	MilkWeight       float64               `json:"milkWeight"`
+	StartTime        string                `json:"startTime"`
+	InitialTime      string                `json:"initialTime"`
+	AttachTime       string                `json:"attachTime"`
+	AttachAdjustTime string                `json:"attachAdjustTime"`
+	DetachedTime     string                `json:"detachedTime"`
+	EndTime          string                `json:"endTime"`
+	DetachedAddress  int64                 `json:"detachedAddress"`
+	Conductivity     int32                 `json:"conductivity"`
+	CowActivity      int32                 `json:"cowActivity"`
+	Source           int8                  `json:"source"`
+	MilkHallNumber   string                `json:"milkHallNumber"`
+	Shifts           int32                 `json:"shifts"`
+	Load             int32                 `json:"load"`
+	Nattach          int32                 `json:"nattach"`
+	RecognitionTime  string                `json:"recognitionTime"`
+	IsYieldLow       int8                  `json:"isYieldLow"`
+	PeakFlow         float64               `json:"peakFlow"`
+	AvgFlow          float64               `json:"avgFlow"`
+	Duration         float64               `json:"duration"`
+	PeakFlowTime     int32                 `json:"peakFlowTime"`
+	LowFlowTime      int32                 `json:"lowFlowTime"`
+	YieldPercentage  int32                 `json:"yieldPercentage"`
+	ActualMilkTime   string                `json:"actualMilkTime"`
+	KickOffs         bool                  `json:"kickOffs"`
+	Blocks           int8                  `json:"blocks"`
+	Slips            int8                  `json:"slips"`
+	ManualDetach     int8                  `json:"manualDetach"`
+	TakeOffFlow      float64               `json:"takeOffFlow"`
+	LowMilkFlowPc    int64                 `json:"lowMilkFlowPc"`
+	Flow0to15        int64                 `json:"flow0To15"`
+	Flow15to30       int64                 `json:"flow15To30"`
+	Flow30to60       int64                 `json:"flow30To60"`
+	Flow60to120      int64                 `json:"flow60To120"`
+	IsIdentify       pasturePb.IsShow_Kind `json:"isIdentify"`
+	CreatedAt        int64                 `json:"createdAt"`
+	UpdatedAt        int64                 `json:"updatedAt"`
 }
 
 func (m *MilkOriginal) TableName() string {
@@ -73,9 +76,9 @@ func NewAFIMilkOriginal(pastureId int64, milkHallNumber string, req *AFIMilkHall
 		InitialTime:      "",
 		AttachTime:       "",
 		AttachAdjustTime: "",
-		DetacherTime:     "",
+		DetachedTime:     "",
 		EndTime:          "",
-		DetacherAddress:  req.StallNumber,
+		DetachedAddress:  req.StallNumber,
 		Conductivity:     req.Amt1,
 		CowActivity:      0,
 		Source:           0,
@@ -123,9 +126,9 @@ func NewGEAMilkOriginal(
 		InitialTime:      "",
 		AttachTime:       attachTime,
 		AttachAdjustTime: "",
-		DetacherTime:     detachTime,
+		DetachedTime:     detachTime,
 		EndTime:          "",
-		DetacherAddress:  detacherAddress,
+		DetachedAddress:  detacherAddress,
 		Conductivity:     conductivity,
 		CowActivity:      0,
 		Source:           0,
@@ -154,3 +157,29 @@ func NewGEAMilkOriginal(
 		Flow60to120:      f60t120,
 	}
 }
+
+type MinMilkOriginalRecords struct {
+	MilkDate        string
+	Shifts          string
+	DetachedAddress string
+	CowId           int64
+	MinId           int64
+	Count           int64
+	MinAttachTime   string
+}
+
+type BaseRecords struct {
+	Id              int64
+	AttachAdjust    string
+	DetachedAddress int32
+	Shifts          int32
+	EarNumber       string
+	MilkDate        string
+	RecognitionTime string
+	CowId           int64
+}
+
+type UpdateLoadRecord struct {
+	Id   int64
+	Load int32
+}

+ 11 - 0
model/neck_active_habit.go

@@ -67,6 +67,7 @@ type NeckActiveHabit struct {
 	BeforeThreeSumIntake int32                 `json:"beforeThreeSumIntake"`
 	Score                int32                 `json:"score"`
 	IsShow               pasturePb.IsShow_Kind `json:"isShow"`
+	IsEstrus             pasturePb.IsShow_Kind `json:"isEstrus"`
 	RecordCount          int32                 `json:"recordCount"`
 	FirmwareVersion      int32                 `json:"firmwareVersion"`
 	CreatedAt            int64                 `json:"createdAt"`
@@ -219,6 +220,16 @@ func (n NeckActiveHabitSlice) ToPB(curveName string) *CowBehaviorCurveData {
 	return res
 }
 
+func (n NeckActiveHabitSlice) ToPB2(dataBetween []string) *pasturePb.CowBehaviorRateData {
+	return &pasturePb.CowBehaviorRateData{
+		DateTime:     dataBetween,
+		RuminaRate:   make([]float32, 0),
+		IntakeRate:   make([]float32, 0),
+		InactiveRate: make([]float32, 0),
+		OtherRate:    make([]float32, 0),
+	}
+}
+
 type MaxHabitIdModel struct {
 	Id int64 `json:"id"`
 }

+ 1 - 1
model/pen_behavior.go

@@ -82,7 +82,7 @@ func (p PenBehaviorSlice) ToPB() *pasturePb.BarnBehaviorCurveItem {
 		dateTime := ""
 		if v.ActiveTime != "" {
 			dt, _ := util.TimeParseLocal(LayoutTime, v.ActiveTime)
-			dateTime = dt.Format(LayoutHour)
+			dateTime = dt.Format(LayoutMinute)
 		}
 		res.DateTime = append(res.DateTime, dateTime)
 		res.Rumina = append(res.Rumina, v.RuminaStd)

+ 7 - 6
model/system_role.go

@@ -21,12 +21,13 @@ func (s *SystemRole) TableName() string {
 }
 
 const (
-	LayoutTime  = "2006-01-02 15:04:05"
-	LayoutDate  = "20060102"
-	LayoutDate2 = "2006-01-02"
-	LayoutMonth = "2006-01"
-	LayoutHour  = "2006-01-02 15"
-	LayoutYear  = "2006"
+	LayoutTime   = "2006-01-02 15:04:05"
+	LayoutDate   = "20060102"
+	LayoutDate2  = "2006-01-02"
+	LayoutMonth  = "2006-01"
+	LayoutHour   = "2006-01-02 15"
+	LayoutMinute = "2006-01-02 15:04"
+	LayoutYear   = "2006"
 )
 
 type SystemRoleSlice []*SystemRole

+ 7 - 4
module/backend/analysis.go

@@ -29,11 +29,14 @@ func (s *StoreEntry) WeightScatterPlot(ctx context.Context, req *pasturePb.Searc
 		pref.Where("ear_number = ?", req.EarNumber)
 	}
 
-	if len(req.BirthDate) == 2 && req.BirthDate[0] != "" && req.BirthDate[1] != "" {
-		t0, _ := util.TimeParseLocal(model.LayoutDate2, req.BirthDate[0])
-		t1, _ := util.TimeParseLocal(model.LayoutDate2, req.BirthDate[1])
+	if len(req.PenIds) > 0 {
+		pref.Where("pen_id IN (?)", req.PenIds)
+	}
 
-		pref.Where("birth_at BETWEEN ? AND ?", t0.Unix(), t1.Unix()+86399)
+	if len(req.AdmissionDate) == 2 {
+		t0, _ := util.TimeParseLocal(model.LayoutDate2, req.AdmissionDate[0])
+		t1, _ := util.TimeParseLocal(model.LayoutDate2, req.AdmissionDate[1])
+		pref.Where("admission_at BETWEEN ? AND ?", t0.Unix(), t1.Unix()+86399)
 	}
 
 	var count int64

+ 1 - 1
module/backend/analysis_more.go

@@ -26,7 +26,7 @@ func (s *StoreEntry) PenBehavior(ctx context.Context, req *pasturePb.BarnBehavio
 	penBehaviorList := make([]*model.PenBehavior, 0)
 	if err = s.DB.Model(new(model.PenBehavior)).
 		Where("pasture_id = ?", userModel.AppPasture.Id).
-		Where("pen_id = ?", req.BarnId).
+		Where("pen_id = ?", req.PenId).
 		Where("heat_date BETWEEN ? AND ?", startTime, endTime).
 		Find(&penBehaviorList).Error; err != nil {
 		return nil, err

+ 20 - 11
module/backend/calendar.go

@@ -250,9 +250,10 @@ func (s *StoreEntry) ImmunisationCowList(ctx context.Context, req *pasturePb.Ite
 		Code: http.StatusOK,
 		Msg:  "ok",
 		Data: &pasturePb.ImmunizationItemsData{
-			Total:    int32(count),
-			Page:     pagination.Page,
-			PageSize: pagination.PageSize,
+			Total:      int32(count),
+			Page:       pagination.Page,
+			PageSize:   pagination.PageSize,
+			HeaderSort: []string{"id", "cowId", "planDay", "planName", "penName", "dayAge", "earNumber", "planId"},
 			Header: map[string]string{
 				"id":        "编号",
 				"cowId":     "牛号",
@@ -317,6 +318,9 @@ func (s *StoreEntry) SameTimeCowList(ctx context.Context, req *pasturePb.ItemsRe
 			Total:    int32(count),
 			Page:     pagination.Page,
 			PageSize: pagination.PageSize,
+			HeaderSort: []string{"id", "cowId", "earNumber", "breedStatusName", "cowTypeName", "planDayAtFormat", "penName",
+				"lact", "calvingAge", "abortionAge", "dayAge", "status", "sameTimeTypeName", "matingTimes", "calvingAtFormat",
+				"abortionAtFormat", "sameTimeName"},
 			Header: map[string]string{
 				"id":               "编号",
 				"cowId":            "牛号",
@@ -393,6 +397,8 @@ func (s *StoreEntry) PregnancyCheckCowList(ctx context.Context, req *pasturePb.I
 			Total:    int32(count),
 			Page:     pagination.Page,
 			PageSize: pagination.PageSize,
+			HeaderSort: []string{"id", "cowId", "earNumber", "cowTypeName", "penName", "lact", "dayAge", "planDay",
+				"checkTypeName", "status", "matingTimes", "calvingAtFormat", "matingAtFormat", "matingAge", "bullId", "pregnancyAge"},
 			Header: map[string]string{
 				"id":              "编号",
 				"cowId":           "牛号",
@@ -425,8 +431,9 @@ func (s *StoreEntry) WeaningCowList(ctx context.Context, req *pasturePb.ItemsReq
 	weaningItems := make([]*pasturePb.WeaningItems, 0)
 	count := int64(0)
 	pref := s.DB.Table(fmt.Sprintf("%s as a", new(model.EventWeaning).TableName())).
-		Select(`a.id,a.cow_id,ROUND(b.current_weight / 1000,2) as current_weight,DATE_FORMAT(FROM_UNIXTIME(a.plan_day), '%Y-%m-%d') AS plan_day_format,
-			b.day_age,b.pen_name,b.ear_number,DATE_FORMAT(FROM_UNIXTIME(b.birth_at), '%Y-%m-%d') AS birth_at_format`).
+		Select(`a.id,a.cow_id,ROUND(b.current_weight / 1000,2) as current_weight,
+		DATE_FORMAT(FROM_UNIXTIME(a.plan_day), '%Y-%m-%d') AS plan_day_format,b.day_age,b.pen_name,
+		b.ear_number,DATE_FORMAT(FROM_UNIXTIME(b.birth_at), '%Y-%m-%d') AS birth_at_format`).
 		Joins("left join cow as b on a.cow_id = b.id").
 		Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
 		Where("a.status = ?", pasturePb.IsShow_No).
@@ -448,9 +455,10 @@ func (s *StoreEntry) WeaningCowList(ctx context.Context, req *pasturePb.ItemsReq
 		Code: http.StatusOK,
 		Msg:  "ok",
 		Data: &pasturePb.WeaningItemsData{
-			Total:    int32(count),
-			Page:     pagination.Page,
-			PageSize: pagination.PageSize,
+			Total:      int32(count),
+			Page:       pagination.Page,
+			PageSize:   pagination.PageSize,
+			HeaderSort: []string{"id", "cowId", "earNumber", "penName", "dayAge", "planDayFormat", "birthAtFormat", "currentWeight"},
 			Header: map[string]string{
 				"id":            "编号",
 				"cowId":         "牛号",
@@ -516,9 +524,10 @@ func (s *StoreEntry) MatingCowList(ctx context.Context, req *pasturePb.ItemsRequ
 		Code: http.StatusOK,
 		Msg:  "ok",
 		Data: &pasturePb.MatingItemsData{
-			Total:    int32(count),
-			Page:     pagination.Page,
-			PageSize: pagination.PageSize,
+			Total:      int32(count),
+			Page:       pagination.Page,
+			PageSize:   pagination.PageSize,
+			HeaderSort: []string{"id", "cowId", "earNumber", "dayAge", "lact", "penName", "breedStatusName", "cowTypeName", "calvingAge", "abortionAge", "exposeEstrusTypeName", "lastCalvingAtFormat"},
 			Header: map[string]string{
 				"id":                   "编号",
 				"cowId":                "牛号",

+ 18 - 8
module/backend/calendar_more.go

@@ -22,8 +22,9 @@ func (s *StoreEntry) CalvingCowList(ctx context.Context, req *pasturePb.ItemsReq
 	calvingItems := make([]*pasturePb.CalvingItems, 0)
 	count := int64(0)
 	pref := s.DB.Table(fmt.Sprintf("%s as a", new(model.EventCalving).TableName())).
-		Select(`a.id,a.cow_id,a.ear_number,a.status,b.breed_status,b.pen_id,ROUND(b.current_weight/100,2) as current_weight,DATE_FORMAT(FROM_UNIXTIME(last_mating_at), '%Y-%m-%d') AS mating_at_format,
-		b.day_age,b.last_bull_number as bull_id,b.pen_name,DATEDIFF(NOW(),FROM_UNIXTIME(last_mating_at)) AS mating_age,DATE_FORMAT(FROM_UNIXTIME(a.plan_day), '%Y-%m-%d') AS plan_day`).
+		Select(`a.id,a.cow_id,a.ear_number,a.status,b.breed_status,b.pen_id,ROUND(b.current_weight/1000,2) as current_weight,
+		DATE_FORMAT(FROM_UNIXTIME(last_mating_at), '%Y-%m-%d') AS mating_at_format,b.day_age,b.last_bull_number as bull_id,
+		b.pen_name,DATEDIFF(NOW(),FROM_UNIXTIME(last_mating_at)) AS mating_age,DATE_FORMAT(FROM_UNIXTIME(a.plan_day), '%Y-%m-%d') AS plan_day`).
 		Joins("left join cow as b on a.cow_id = b.id").
 		Where("a.status = ?", pasturePb.IsShow_No).
 		Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
@@ -62,6 +63,10 @@ func (s *StoreEntry) CalvingCowList(ctx context.Context, req *pasturePb.ItemsReq
 			Total:    int32(count),
 			Page:     pagination.Page,
 			PageSize: pagination.PageSize,
+			HeaderSort: []string{
+				"id", "cowId", "earNumber", "penName", "lact", "breedStatusName", "matingAge", "dayAge", "status",
+				"bullId", "planDay", "matingAtFormat", "currentWeight",
+			},
 			Header: map[string]string{
 				"id":              "编号",
 				"cowId":           "牛号",
@@ -90,8 +95,9 @@ func (s *StoreEntry) DryMilkCowList(ctx context.Context, req *pasturePb.ItemsReq
 	dryMilkItems := make([]*pasturePb.DruMilkItems, 0)
 	count := int64(0)
 	pref := s.DB.Table(fmt.Sprintf("%s as a", new(model.EventDryMilk).TableName())).
-		Select(`a.id,a.cow_id,a.ear_number,a.status,b.breed_status,b.pen_id,b.day_age,b.last_bull_number as bull_number,
-b.pen_name,DATEDIFF(NOW(),FROM_UNIXTIME(last_mating_at)) AS mating_age,DATE_FORMAT(FROM_UNIXTIME(a.plan_day), '%Y-%m-%d') AS plan_day`).
+		Select(`a.id,a.cow_id,a.ear_number,a.status,b.breed_status,b.pen_id,b.day_age,
+		b.last_bull_number as bull_number,b.pen_name,DATEDIFF(NOW(),FROM_UNIXTIME(last_mating_at)) AS mating_age,
+		DATE_FORMAT(FROM_UNIXTIME(a.plan_day), '%Y-%m-%d') AS plan_day`).
 		Joins("left join cow as b on a.cow_id = b.id").
 		Where("a.status = ?", pasturePb.IsShow_No).
 		Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
@@ -136,6 +142,9 @@ b.pen_name,DATEDIFF(NOW(),FROM_UNIXTIME(last_mating_at)) AS mating_age,DATE_FORM
 			Total:    int32(count),
 			Page:     pagination.Page,
 			PageSize: pagination.PageSize,
+			HeaderSort: []string{
+				"id", "cowId", "earNumber", "dayAge", "penName", "lact", "pregnancyAge", "status", "bullNumber", "planDay", "calvingAtFormat",
+			},
 			Header: map[string]string{
 				"id":              "编号",
 				"cowId":           "牛号",
@@ -186,10 +195,11 @@ func (s *StoreEntry) TreatmentCowList(ctx context.Context, req *pasturePb.ItemsR
 		Code: http.StatusOK,
 		Msg:  "ok",
 		Data: &pasturePb.EventCowDiseaseData{
-			List:     model.EventCowDiseaseSlice(diseaseItems).ToPB(s.HealthStatusMap()),
-			Total:    int32(count),
-			PageSize: pagination.PageSize,
-			Page:     pagination.Page,
+			List:       model.EventCowDiseaseSlice(diseaseItems).ToPB(s.HealthStatusMap()),
+			Total:      int32(count),
+			PageSize:   pagination.PageSize,
+			Page:       pagination.Page,
+			HeaderSort: []string{"id", "cowId", "earNumber", "penName", "diagnoseName", "healthStatus", "lastPrescriptionName", "treatmentDays", "onsetDays"},
 			Header: map[string]string{
 				"id":                   "编号",
 				"cowId":                "牛号",

+ 40 - 12
module/backend/cow.go

@@ -41,7 +41,6 @@ func (s *StoreEntry) Detail(ctx context.Context, req *pasturePb.SearchEventReque
 		}
 	}
 
-	penMap := s.PenMap(ctx, userModel.AppPasture.Id)
 	cowTypeMap := s.CowTypeMap()
 	breedStatusMap := s.CowBreedStatusMap()
 	cowKindMap := s.CowKindMap()
@@ -52,7 +51,7 @@ func (s *StoreEntry) Detail(ctx context.Context, req *pasturePb.SearchEventReque
 	return &pasturePb.CowInfoResponse{
 		Code: http.StatusOK,
 		Msg:  "ok",
-		Data: model.CowSlice([]*model.Cow{cowInfo}).ToPB(penMap, cowTypeMap, breedStatusMap, cowKindMap, cowSourceMap, admissionStatusMap, healthStatusMap)[0],
+		Data: model.CowSlice([]*model.Cow{cowInfo}).ToPB(cowTypeMap, breedStatusMap, cowKindMap, cowSourceMap, admissionStatusMap, healthStatusMap)[0],
 	}, nil
 }
 
@@ -73,7 +72,7 @@ func (s *StoreEntry) List(ctx context.Context, req *pasturePb.SearchEventRequest
 	}
 
 	if req.EarNumber != "" {
-		pref.Where("ear_number = ?", req.EarNumber)
+		pref.Where("ear_number like ?", fmt.Sprintf("%s%s%s", "%", req.EarNumber, "%"))
 	}
 
 	if req.Id > 0 {
@@ -108,10 +107,6 @@ func (s *StoreEntry) List(ctx context.Context, req *pasturePb.SearchEventRequest
 		pref.Where("source_id = ?", req.CowSource)
 	}
 
-	if req.EarNumber != "" {
-		pref.Where("ear_number = ?", req.EarNumber)
-	}
-
 	if err = pref.Order("id desc").
 		Count(&count).
 		Limit(int(pagination.PageSize)).
@@ -120,7 +115,6 @@ func (s *StoreEntry) List(ctx context.Context, req *pasturePb.SearchEventRequest
 		return nil, xerr.WithStack(err)
 	}
 
-	penMap := s.PenMap(ctx, userModel.AppPasture.Id)
 	cowTypeMap := s.CowTypeMap()
 	breedStatusMap := s.CowBreedStatusMap()
 	cowKindMap := s.CowKindMap()
@@ -131,10 +125,7 @@ func (s *StoreEntry) List(ctx context.Context, req *pasturePb.SearchEventRequest
 		Code: http.StatusOK,
 		Msg:  "ok",
 		Data: &pasturePb.SearchCowData{
-			List: model.CowSlice(cowList).ToPB(
-				penMap, cowTypeMap, breedStatusMap, cowKindMap,
-				cowSourceMap, admissionStatusMap, healthStatusMap,
-			),
+			List:     model.CowSlice(cowList).ToPB(cowTypeMap, breedStatusMap, cowKindMap, cowSourceMap, admissionStatusMap, healthStatusMap),
 			Total:    int32(count),
 			PageSize: pagination.PageSize,
 			Page:     pagination.Page,
@@ -366,3 +357,40 @@ func (s *StoreEntry) CowLactCurve(ctx context.Context, req *pasturePb.CowLactCur
 		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.GetCowInfoByCowId(ctx, userModel.AppPasture.Id, int64(req.CowId))
+	if err != nil {
+		return nil, xerr.Customf("错误的牛只信息: %d", req.CowId)
+	}
+
+	if cowInfo.NeckRingNumber == "" {
+		return nil, xerr.Customf("该牛只未佩戴脖环: %s", req.EarNumber)
+	}
+
+	neckActiveHabitList := 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 ?", req.StartTime, req.EndTime).
+		Order("heat_date, frameid").
+		Find(&neckActiveHabitList).Error; err != nil {
+	}
+
+	dataBetween, err := util.GetDaysBetween(req.StartTime, req.EndTime)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &pasturePb.CowBehaviorRateResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: model.NeckActiveHabitSlice(neckActiveHabitList).ToPB2(dataBetween),
+	}, nil
+}

+ 24 - 4
module/backend/dashboard.go

@@ -2,7 +2,6 @@ package backend
 
 import (
 	"context"
-	"fmt"
 	"kpt-pasture/model"
 	"kpt-pasture/util"
 	"net/http"
@@ -32,20 +31,41 @@ func (s *StoreEntry) NeckRingWarning(ctx context.Context) (*pasturePb.IndexNeckR
 		int32(pasturePb.EstrusLevel_High):   0,
 	}
 
-	if err = s.DB.Table(fmt.Sprintf("%s as a", new(model.NeckRingEstrusWarning).TableName())).
+	pref, err := s.EstrusWarningQuery(ctx, userModel.AppPasture.Id)
+	if err != nil {
+		return nil, xerr.Customf("系统错误!")
+	}
+	if err = pref.Order("a.level DESC").
+		Select("a.level, count(a.level) as count").
+		Group("a.level").
+		Find(&estrusWarningCowList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	/*if err = s.DB.Table(fmt.Sprintf("%s as a", new(model.NeckRingEstrusWarning).TableName())).
 		Select("a.level, count(a.level) as count").
 		Where("a.pasture_id = ?", userModel.AppPasture.Id).
 		Where("a.is_show = ?", pasturePb.IsShow_Ok).
 		Group("a.level").
 		Find(&estrusWarningCowList).Error; err != nil {
 		zaplog.Error("NeckRingWarning", zap.Any("estrusWarningNumber", err))
-	}
+	}*/
 	countEstrusWarning := 0
 	for _, v := range estrusWarningCowList {
 		estrusWarningLevelItems[int32(v.Level)] = estrusWarningLevelItems[v.Count]
 		countEstrusWarning += int(v.Count)
 	}
 
+	abortionCount := int64(0)
+	pref, err = s.AbortionWarningQuery(ctx, userModel.AppPasture.Id)
+	if err != nil {
+		return nil, xerr.Customf("系统错误!")
+	}
+
+	if err = pref.Group("cow_id").Count(&abortionCount).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
 	healthWarningNumber := int64(0)
 	if err = s.DB.Model(new(model.NeckRingHealth)).
 		Where("pasture_id = ?", userModel.AppPasture.Id).
@@ -60,7 +80,7 @@ func (s *StoreEntry) NeckRingWarning(ctx context.Context) (*pasturePb.IndexNeckR
 		Data: &pasturePb.NeckRingData{
 			EstrusWarningNumber:     int32(countEstrusWarning),
 			HealthWarningNumber:     int32(healthWarningNumber),
-			AbortionWarningNumber:   0,
+			AbortionWarningNumber:   int32(abortionCount),
 			StressWarningNumber:     0,
 			EstrusWarningLevelItems: estrusWarningLevelItems,
 		},

+ 3 - 0
module/backend/event_base.go

@@ -106,6 +106,9 @@ func (s *StoreEntry) CreateEnter(ctx context.Context, req *pasturePb.EventEnterR
 	}
 
 	newCow := model.NewEnterCow(userModel.AppPasture.Id, req, penMap)
+	if req.BirthAt > 0 {
+		newCow.DayAge = newCow.GetDayAge()
+	}
 	if err = s.DB.Transaction(func(tx *gorm.DB) error {
 		// 新增牛只信息
 		if err = tx.Model(new(model.Cow)).Create(newCow).Error; err != nil {

+ 2 - 2
module/backend/event_base_more.go

@@ -252,7 +252,7 @@ func (s *StoreEntry) CowSaleList(ctx context.Context, req *pasturePb.EventCowSal
 		pref.Where("dealer_id = ?", req.DealerId)
 	}
 
-	if err = pref.Order("a.id desc").
+	if err = pref.Order("id desc").
 		Count(&count).Limit(int(pagination.PageSize)).
 		Offset(int(pagination.PageOffset)).
 		Find(&eventSale).Error; err != nil {
@@ -297,7 +297,7 @@ func (s *StoreEntry) CowSaleList(ctx context.Context, req *pasturePb.EventCowSal
 			PageSize: pagination.PageSize,
 			Page:     pagination.Page,
 		},
-	}, err
+	}, nil
 }
 
 func (s *StoreEntry) ImmunizationList(ctx context.Context, req *pasturePb.SearchEventImmunizationRequest, pagination *pasturePb.PaginationModel) (*pasturePb.SearchEventImmunizationResponse, error) {

+ 1 - 1
module/backend/event_breed.go

@@ -250,7 +250,7 @@ func (s *StoreEntry) SameTimeBatch(ctx context.Context, req *pasturePb.EventSame
 			return xerr.WithStack(err)
 		}
 		if time.Unix(eventCowSameTime.PlanDay, 0).Format(model.LayoutDate2) != nowTime {
-			return xerr.Customf("该牛只不是今日计划: %d", eventCowSameTime.EarNumber)
+			return xerr.Customf("该牛只不是今日计划: %s", eventCowSameTime.EarNumber)
 		}
 		eventCowSameTimeList = append(eventCowSameTimeList, eventCowSameTime)
 	}

+ 7 - 0
module/backend/interface.go

@@ -7,6 +7,7 @@ import (
 	"kpt-pasture/service/asynqsvc"
 	"kpt-pasture/service/wechat"
 	"kpt-pasture/store/kptstore"
+	"mime/multipart"
 
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
 	"gitee.com/xuyiping_admin/pkg/di"
@@ -53,6 +54,7 @@ type KptService interface {
 	DashboardService     // 牧场统计相关
 	WorkService          // 日常工作相关
 	MilkHallService      // 奶厅数据相关
+	UploadService        // 上传文件相关
 	TestService          // 测试相关
 }
 
@@ -247,6 +249,7 @@ type CowService interface {
 	BehaviorCurve(ctx context.Context, req *pasturePb.CowBehaviorCurveRequest) (*model.CowBehaviorCurveResponse, 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)
 
 	IndicatorsComparison(ctx context.Context, req *pasturePb.IndicatorsComparisonRequest) (*model.IndicatorsComparisonResponse, error)
 	LongTermInfertility(ctx context.Context, req *pasturePb.LongTermInfertilityRequest, pagination *pasturePb.PaginationModel) (*pasturePb.LongTermInfertilityResponse, error)
@@ -330,6 +333,10 @@ type MilkHallService interface {
 	MilkHallOriginal(ctx context.Context, req []byte) error
 }
 
+type UploadService interface {
+	Photos(ctx context.Context, files []*multipart.FileHeader) ([]string, error)
+}
+
 type TestService interface {
 	CowNeckRingNumberBound(ctx context.Context, pagination *pasturePb.PaginationModel) error
 	CowNeckRingNumberBound2(ctx context.Context, pagination *pasturePb.PaginationModel) error

+ 40 - 26
module/backend/neck_ring_warning.go

@@ -26,36 +26,15 @@ func (s *StoreEntry) EstrusOrAbortionCowList(ctx context.Context, req *pasturePb
 	var pref *gorm.DB
 	switch req.Kind {
 	case "abortion":
-		pref = s.DB.Table(fmt.Sprintf("%s as a", new(model.NeckRingEstrusWarning).TableName())).
-			Joins(fmt.Sprintf("JOIN %s AS b on a.cow_id = b.id", new(model.Cow).TableName())).
-			Where("b.pregnancy_age BETWEEN ? AND ?", 1, 260).
-			Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
-			Where("b.is_pregnant = ?", pasturePb.IsShow_Ok).
-			Where("a.level >= ?", pasturePb.EstrusLevel_Middle).
-			Where("a.pasture_id = ?", userModel.AppPasture.Id).
-			Where("a.is_show = ?", pasturePb.IsShow_Ok)
+		pref, err = s.AbortionWarningQuery(ctx, userModel.AppPasture.Id)
+		if err != nil {
+			return nil, xerr.WithStack(err)
+		}
 	default:
-		nowTime := time.Now()
-		startTime := time.Unix(util.TimeParseLocalUnix(nowTime.Format(model.LayoutDate2)), 0).Format(model.LayoutTime)
-		entTime := time.Unix(util.TimeParseLocalEndUnix(nowTime.AddDate(0, 0, 1).Format(model.LayoutDate2)), 0).Format(model.LayoutTime)
-		systemBasic, err := s.FindSystemBasic(ctx, userModel.AppPasture.Id, model.EstrusWaringDays)
+		pref, err = s.EstrusWarningQuery(ctx, userModel.AppPasture.Id)
 		if err != nil {
 			return nil, xerr.WithStack(err)
 		}
-
-		pref = s.DB.Table(fmt.Sprintf("%s as a", new(model.NeckRingEstrusWarning).TableName())).
-			Joins(fmt.Sprintf("JOIN %s AS b on a.cow_id = b.id", new(model.Cow).TableName())).
-			Where("b.last_mating_at < UNIX_TIMESTAMP(a.date_time)").
-			Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
-			Where("b.is_forbidden_mating = ?", pasturePb.IsShow_No).
-			Where("a.level >= ?", pasturePb.EstrusLevel_Low).
-			Where("a.pasture_id = ?", userModel.AppPasture.Id).
-			Where("a.date_time BETWEEN ? AND ?", startTime, entTime).
-			Where(s.DB.Where("b.last_mating_at < UNIX_TIMESTAMP(a.first_time)").
-				Or(s.DB.Where("b.last_mating_at = ?", 0).
-					Where("b.calving_age > ?", systemBasic.MinValue).
-					Or("b.lact = ?", 0))).
-			Where("a.is_show = ?", pasturePb.IsShow_Ok)
 	}
 
 	if len(req.EarNumber) > 0 {
@@ -108,3 +87,38 @@ func (s *StoreEntry) EstrusOrAbortionCowList(ctx context.Context, req *pasturePb
 		},
 	}, nil
 }
+
+func (s *StoreEntry) EstrusWarningQuery(ctx context.Context, pastureId int64) (*gorm.DB, error) {
+	nowTime := time.Now()
+	startTime := time.Unix(util.TimeParseLocalUnix(nowTime.Format(model.LayoutDate2)), 0).Format(model.LayoutTime)
+	entTime := time.Unix(util.TimeParseLocalEndUnix(nowTime.AddDate(0, 0, 1).Format(model.LayoutDate2)), 0).Format(model.LayoutTime)
+	systemBasic, err := s.FindSystemBasic(ctx, pastureId, model.EstrusWaringDays)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return s.DB.Table(fmt.Sprintf("%s as a", new(model.NeckRingEstrusWarning).TableName())).
+		Joins(fmt.Sprintf("JOIN %s AS b on a.cow_id = b.id", new(model.Cow).TableName())).
+		Where("b.last_mating_at < UNIX_TIMESTAMP(a.date_time)").
+		Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
+		Where("b.is_forbidden_mating = ?", pasturePb.IsShow_No).
+		Where("a.level >= ?", pasturePb.EstrusLevel_Low).
+		Where("a.pasture_id = ?", pastureId).
+		Where("a.date_time BETWEEN ? AND ?", startTime, entTime).
+		Where(s.DB.Where("b.last_mating_at < UNIX_TIMESTAMP(a.first_time)").
+			Or(s.DB.Where("b.last_mating_at = ?", 0).
+				Where("b.calving_age > ?", systemBasic.MinValue).
+				Or("b.lact = ?", 0))).
+		Where("a.is_show = ?", pasturePb.IsShow_Ok), nil
+}
+
+func (s *StoreEntry) AbortionWarningQuery(ctx context.Context, pastureId int64) (*gorm.DB, error) {
+	return s.DB.Table(fmt.Sprintf("%s as a", new(model.NeckRingEstrusWarning).TableName())).
+		Joins(fmt.Sprintf("JOIN %s AS b on a.cow_id = b.id", new(model.Cow).TableName())).
+		Where("b.pregnancy_age BETWEEN ? AND ?", 1, 260).
+		Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
+		Where("b.is_pregnant = ?", pasturePb.IsShow_Ok).
+		Where("a.level >= ?", pasturePb.EstrusLevel_Middle).
+		Where("a.pasture_id = ?", pastureId).
+		Where("a.is_show = ?", pasturePb.IsShow_Ok), nil
+}

+ 67 - 0
module/backend/upload_file.go

@@ -0,0 +1,67 @@
+package backend
+
+import (
+	"context"
+	"fmt"
+	"kpt-pasture/config"
+	"kpt-pasture/util"
+	"mime/multipart"
+	"os"
+	"path/filepath"
+	"time"
+
+	"gitee.com/xuyiping_admin/pkg/xerr"
+)
+
+func (s *StoreEntry) Photos(ctx context.Context, files []*multipart.FileHeader) ([]string, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	workDir := fmt.Sprintf("%s", config.WorkDir)
+	pathDir := fmt.Sprintf("/files/photos/%d/%s", userModel.AppPasture.Id, time.Now().Format("20060102"))
+	saveDir := filepath.Join(workDir, pathDir)
+	if _, err = os.Stat(saveDir); os.IsNotExist(err) {
+		if err = os.MkdirAll(saveDir, 0755); err != nil {
+			return nil, xerr.Customf("创建目录失败: %s", err.Error())
+		}
+	}
+
+	// 处理每个文件
+	filePaths := make([]string, len(files))
+	for i, file := range files {
+		contentType := file.Header.Get("Content-Type")
+		if contentType != "image/jpeg" && contentType != "image/png" && contentType != "image/gif" {
+			return nil, xerr.Customf("图片格式错误: %s", file.Filename)
+
+		}
+		if file.Size > 1024*1024*5 {
+			return nil, xerr.Customf("单个图片文件不能超过5MB")
+
+		}
+		ext := filepath.Ext(file.Filename)
+		if ext == "" {
+			switch contentType {
+			case "image/jpeg":
+				ext = ".jpg"
+			case "image/png":
+				ext = ".png"
+			case "image/gif":
+				ext = ".gif"
+			default:
+				ext = ".jpg" // 默认
+			}
+		}
+		randomName := util.GenerateRandomNumberString(32)
+		finalFilename := randomName + ext
+		fPath := filepath.Join(saveDir, finalFilename)
+		urlPath := filepath.Join(pathDir, finalFilename)
+
+		if err = util.SaveUploadedFile(file, fPath); err != nil {
+			return nil, xerr.Customf("保存文件失败: %s", err.Error())
+		}
+		filePaths[i] = urlPath
+	}
+	return filePaths, nil
+}

+ 29 - 299
module/crontab/milk_original.go → module/crontab/milk_original_update.go

@@ -4,13 +4,10 @@ import (
 	"fmt"
 	"kpt-pasture/model"
 	"kpt-pasture/util"
-	"sort"
 	"strconv"
 	"strings"
 	"time"
 
-	"gorm.io/gorm"
-
 	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
 	"go.uber.org/zap"
 )
@@ -93,14 +90,21 @@ func (e *Entry) ProcessMilkOriginal(pastureId int64) {
 		zaplog.Error("DeleteRepeatMilkData", zap.Any("pastureId", pastureId), zap.Any("err", err))
 		return
 	}
-
-	milkHallList := e.FindMilkHallList(pastureId)
 	e.DeleteRepeatMilkData(pastureId, deleteModel, milkClassConfig, milkOriginalList)
-	e.UpdateRecognitionTime(pastureId, milkHallList)
-	e.UpdateRepeatCupSet1(milkOriginalList)
-	e.UpdateMilkOriginCowInfo(milkOriginalList, milkHallList)
-	e.UpdateRepeatCupSet2(milkOriginalList)
-	e.UpdateMilkOriginalInitialTimesAndAttachAdjustTime(shifts, milkOriginalList)
+	milkHallList := e.FindMilkHallList(pastureId)
+	for _, hall := range milkHallList {
+		e.UpdateRecognitionTime(pastureId, hall)
+		e.UpdateRepeatCupSet1(milkOriginalList)
+		e.UpdateMilkOriginCowInfo(milkOriginalList, hall)
+		e.UpdateRepeatCupSet2(milkOriginalList)
+		e.UpdateMilkOriginalInitialTimesAndAttachAdjustTime(shifts, milkOriginalList)
+		e.UpdateMilkNattach(pastureId, milkClassConfig, hall)
+		e.UpdateMilkNoCowId(pastureId, milkClassConfig, hall)
+		e.UpdateMilkLoad(pastureId, milkClassConfig, hall)
+		e.UpdateMilkLoad2(pastureId, milkClassConfig, hall)
+		e.UpdateMilkLoad3(pastureId, milkClassConfig, hall)
+		e.UpdateMilkCowIdResetZero(pastureId, milkClassConfig, hall)
+	}
 }
 
 // UpdateShifts 更新班次
@@ -114,8 +118,8 @@ func (e *Entry) UpdateShifts(pastureId int64, xBeg1, xBeg2, xBeg3, xBeg4 int) {
 	}
 
 	for _, v := range milkOriginalList {
-		subDetachTime1 := util.Substr(v.DetacherTime, 11, 2)
-		subDetachTime2 := util.Substr(v.DetacherTime, 14, 2)
+		subDetachTime1 := util.Substr(v.DetachedTime, 11, 2)
+		subDetachTime2 := util.Substr(v.DetachedTime, 14, 2)
 
 		subDetachTime1Int, _ := strconv.ParseInt(subDetachTime1, 10, 64)
 		subDetachTime2Int, _ := strconv.ParseInt(subDetachTime2, 10, 64)
@@ -189,9 +193,9 @@ func (e *Entry) UpdateMilkDate(pastureId int64, xDBeg int) {
 		}
 
 		// 比较挤奶时间和结束时间,取较晚的时间
-		detacherTime, _ := util.TimeParseLocal(model.LayoutTime, v.DetacherTime)
-		latestTime := detacherTime
-		if endTime.After(detacherTime) {
+		detachedTime, _ := util.TimeParseLocal(model.LayoutTime, v.DetachedTime)
+		latestTime := detachedTime
+		if endTime.After(detachedTime) {
 			latestTime = endTime
 		}
 
@@ -225,298 +229,24 @@ func (e *Entry) DeleteRepeatMilkData(pastureId int64, deleteModel *DeleteMilkOri
 		e.delete1(v, deleteModel.XMind, cfg)
 		e.delete2(v, deleteModel.XMind, cfg)
 		e.delete3(v, deleteModel.XMind, cfg)
-		e.delete4(v, deleteModel.XMind, cfg)
-	}
-}
-
-// UpdateRecognitionTime 识别时间超过40分钟未套杯牛只,识别改为未识别
-func (e *Entry) UpdateRecognitionTime(pastureId int64, milkHallList []*model.MilkHall) {
-	if len(milkHallList) == 0 {
-		return
-	}
-	for _, hall := range milkHallList {
-		milkOriginalList := make([]*model.MilkOriginal, 0)
-		if err := e.DB.Model(new(model.MilkOriginal)).
-			Where("pasture_id = ?", pastureId).
-			Where("milk_hall_number = ?", hall.Name).
-			Where("milk_hall_brand = ?", hall.Brand).
-			Where("load = ?", 0).
-			Find(&milkOriginalList).Error; err != nil {
-			zaplog.Error("MilkHallData", zap.Any("err", err))
-		}
-
-		for _, v := range milkOriginalList {
-			t1, _ := util.TimeParseLocal(model.LayoutTime, v.AttachTime)
-			t2, _ := util.TimeParseLocal(model.LayoutTime, v.RecognitionTime)
-			diff := t1.Sub(t2)
-			minute := int(diff.Minutes())
-
-			if util.Substr(v.RecognitionTime, -1, 8) != "00:00:00" && minute > 40 {
-				if err := e.DB.Model(new(model.MilkOriginal)).
-					Where("id = ?", v.Id).
-					Updates(map[string]interface{}{
-						"cow_id":           0,
-						"ele_ear_number":   "",
-						"recognition_time": fmt.Sprintf("%s 00:00:00", util.Substr(v.RecognitionTime, 0, 10)),
-					}).Error; err != nil {
-					zaplog.Error("MilkHallData", zap.Any("err", err))
-				}
-			}
-		}
-	}
-}
-
-// UpdateRepeatCupSet1 更新重复套杯1, 识别时间相同,且不为0为重复套杯
-func (e *Entry) UpdateRepeatCupSet1(milkOriginalList []*model.MilkOriginal) {
-	if len(milkOriginalList) == 0 {
-		return
-	}
-
-	milkOriginalMap := make(map[string][]*model.MilkOriginal)
-	for _, v := range milkOriginalList {
-		if strings.HasSuffix(v.RecognitionTime, "00:00:00") {
-			continue
-		}
-		key := fmt.Sprintf("%s_%d_%d_%s", v.MilkDate, v.Shifts, v.DetacherAddress, v.RecognitionTime)
-		milkOriginalMap[key] = append(milkOriginalMap[key], v)
-	}
-
-	for _, originalList := range milkOriginalMap {
-		if len(originalList) >= 2 {
-			// 按照Id升序排序(保留第一条)
-			sort.Slice(originalList, func(i, j int) bool {
-				return originalList[i].Id < originalList[j].Id
-			})
-
-			for i, v := range originalList {
-				if i == 0 {
-					continue
-				}
-				if err := e.DB.Model(new(model.MilkOriginal)).
-					Select("").Where("id = ?", v.Id).
-					Update("nattach", 2).Error; err != nil {
-					zaplog.Error("UpdateRepeatCupSet1", zap.Any("err", err))
-				}
-			}
-		}
-	}
-}
-
-// UpdateMilkOriginCowInfo 更新牛只信息
-func (e *Entry) UpdateMilkOriginCowInfo(milkOriginalList []*model.MilkOriginal, milkHallList []*model.MilkHall) {
-	milkHallMap := make(map[string][]*model.MilkOriginal)
-	for _, v := range milkOriginalList {
-		key := fmt.Sprintf("%s", v.MilkHallNumber)
-		milkHallMap[key] = append(milkHallMap[key], v)
-	}
-
-	for _, v := range milkHallList {
-		dataList, ok := milkHallMap[v.Name]
-		if !ok {
-			continue
-		}
-		switch v.IsExtraUpdate {
-		case model.IsExtra0:
-
-		case model.IsExtra1, model.IsExtra3:
-			for _, d := range dataList {
-				if d.EarNumber == "" {
-					continue
-				}
-
-				cowInfo, err := e.GetCowByEarNumber(d.PastureId, d.EarNumber)
-				if err != nil {
-					zaplog.Error("UpdateMilkOriginCowInfo", zap.Any("err", err), zap.Any("data", d))
-					continue
-				}
-				// 更新牛只信息
-				d.UpdateCowInfo(cowInfo)
-				if err = e.DB.Model(new(model.MilkOriginal)).
-					Select("cow_id", "pen_id", "pen_name").
-					Where("id = ?", d.Id).Updates(d).Error; err != nil {
-					zaplog.Error("UpdateMilkOriginCowInfo", zap.Any("err", err), zap.Any("data", d))
-				}
-			}
-		case model.IsExtra2:
-		default:
-		}
-	}
-}
-
-func (e *Entry) UpdateMilkOriginalInitialTimesAndAttachAdjustTime(shifts []int32, milkOriginalList []*model.MilkOriginal) {
-	for _, shift := range shifts {
-		shiftMinDetachTimes := ""
-		// 按脱杯地址分组处理
-		addressMap := make(map[int64][]*model.MilkOriginal)
-		for _, m := range milkOriginalList {
-			if m.Shifts != shift || m.DetacherTime == "" {
-				continue
-			}
-			if shiftMinDetachTimes == "" {
-				shiftMinDetachTimes = m.DetacherTime
-			} else {
-				t1, _ := util.TimeParseLocal(model.LayoutTime, m.DetacherTime)
-				t2, _ := util.TimeParseLocal(model.LayoutTime, shiftMinDetachTimes)
-				if t2.Before(t1) {
-					shiftMinDetachTimes = m.DetacherTime
-				}
-			}
-			addressMap[m.DetacherAddress] = append(addressMap[m.DetacherAddress], m)
-		}
-
-		if shiftMinDetachTimes == "" {
-			continue
-		}
-
-		bt, _ := util.TimeParseLocal(model.LayoutTime, shiftMinDetachTimes)
-		b5 := bt.Add(-5*time.Minute).Format(model.LayoutHour) + "00:00"
-
-		for _, list := range addressMap {
-			// 对当前地址的记录按时间排序
-			sort.Slice(list, func(i, j int) bool {
-				if list[i].MilkDate != list[j].MilkDate {
-					return list[i].MilkDate < list[j].MilkDate
-				}
-
-				if list[i].Shifts != list[j].Shifts {
-					return list[i].Shifts < list[j].Shifts
-				}
-				if list[i].DetacherAddress != list[j].DetacherAddress {
-					return list[i].DetacherAddress < list[j].DetacherAddress
-				}
-				return list[i].Id < list[j].Id
-			})
-
-			// 初始化变量,模拟SQL中的@address和@det
-			var lastAddress int64 = 0
-			var lastDetachTime string = "2001-01-01 06:00:00" // 默认初始值
-			// 批量更新参数
-			var updateParams []struct {
-				ID           int64
-				InitialTimes string
-				AttachAdjust string
-			}
-
-			for _, m := range list {
-				var initialTimeStr string
-				var attachAdjust string
-				// 如果当前记录的脱杯地址与上一条不同,则使用基准时间b5
-				if m.DetacherAddress != lastAddress {
-					initialTimeStr = b5
-				} else {
-					// 否则使用上一条记录的脱杯时间
-					initialTimeStr = lastDetachTime
-				}
-
-				// 更新最后记录的地址和时间
-				lastAddress = m.DetacherAddress
-				lastDetachTime = m.DetacherTime
-
-				// 只有当initialTime不为空且与原有值不同时才需要更新
-				if initialTimeStr != "" {
-					initialTime, _ := util.TimeParseLocal(model.LayoutTime, initialTimeStr)
-					attachTime, _ := util.TimeParseLocal(model.LayoutTime, m.AttachTime)
-					detachTime, _ := util.TimeParseLocal(model.LayoutTime, m.DetacherTime)
-
-					// 条件1:attachtimes以'00:00:00'结尾或attachtimes <= initialtimes
-					if strings.HasSuffix(m.AttachTime, "00:00:00") || attachTime.Before(initialTime) || attachTime.Equal(initialTime) {
-						// 计算 detachtimes - (1.5 + duration)*60 秒
-						adjustTime1 := detachTime.Add(-time.Duration((90 + m.Duration*60)) * time.Second)
-
-						// 取三者中的最大值
-						maxTime := util.FindMaxTime(attachTime, initialTime, adjustTime1)
-						attachAdjust = maxTime.Format(model.LayoutTime)
-					} else {
-						// 计算 detachtimes - duration*60 秒
-						adjustTime2 := detachTime.Add(-time.Duration(m.Duration*60) * time.Second)
-
-						// 取 attachtimes 和 adjustTime2 中的较小值
-						minTime := util.FindMinTime(attachTime, adjustTime2)
-
-						// 再与 initialtimes 取较大值
-						maxTime := util.FindMaxTime(minTime, initialTime)
-						attachAdjust = maxTime.Format(model.LayoutTime)
-					}
-
-					// 记录需要更新的字段
-					updateParams = append(updateParams, struct {
-						ID           int64
-						InitialTimes string
-						AttachAdjust string
-					}{
-						ID:           m.Id,
-						InitialTimes: initialTimeStr,
-						AttachAdjust: attachAdjust,
-					})
-
-					/*if err := e.DB.Model(new(model.MilkOriginal)).
-						Select("initial_time").
-						Where("id = ?", m.Id).
-						Update("initial_time", initialTime).Error; err != nil {
-						zaplog.Error("UpdateMilkOriginalInitialTimesAndAttachAdjustTime", zap.Any("err", err))
-					}*/
-				}
-			}
-
-			if len(updateParams) > 0 {
-				// 批量更新数据库
-				if err := e.DB.Transaction(func(tx *gorm.DB) error {
-					for _, param := range updateParams {
-						updates := map[string]interface{}{
-							"initial_times":      param.InitialTimes,
-							"attach_adjust_time": param.AttachAdjust,
-						}
-
-						if err := tx.Model(new(model.MilkOriginal)).
-							Select("initial_time", "attach_adjust_time").
-							Where("id = ? ", param.ID).
-							Updates(updates).Error; err != nil {
-							return err
-						}
-					}
-					return nil
-				}); err != nil {
-					zaplog.Error("UpdateMilkOriginalInitialTimesAndAttachAdjustTime", zap.Any("err", err))
-				}
-			}
-
-		}
-	}
-}
-
-// UpdateRepeatCupSet2  非标准重复套杯
-func (e *Entry) UpdateRepeatCupSet2(milkOriginalList []*model.MilkOriginal) {
-	for _, v := range milkOriginalList {
-		if v.AttachTime == "" || v.InitialTime == "" {
-			continue
-		}
-		nattchTime, _ := util.TimeParseLocal(model.LayoutTime, v.AttachTime)
-		initialTime, _ := util.TimeParseLocal(model.LayoutTime, v.InitialTime)
-		if util.Substr(v.InitialTime, -1, 5) != "00:00" && v.Nattach == 0 && nattchTime.Sub(initialTime).Minutes() <= 1 {
-			if err := e.DB.Model(new(model.MilkOriginal)).
-				Select("nattach").
-				Where("id = ?", v.Id).
-				Update("nattach", 2).Error; err != nil {
-				zaplog.Error("UpdateRepeatCupSet2", zap.Any("err", err))
-			}
-		}
+		e.delete4(v)
 	}
 }
 
 func (e *Entry) delete1(data *model.MilkOriginal, xMinD string, cfg *MilkClassConfig) {
 	// 1. 删除attach_time为00:00:00的记录
-	acctchStr := util.Substr(data.AttachTime, -1, 8)
-	if data.MilkDate < xMinD || acctchStr != "00:00:00" {
+	actchStr := util.Substr(data.AttachTime, -1, 8)
+	if data.MilkDate < xMinD || actchStr != "00:00:00" {
 		return
 	}
 
 	// 2. 检查是否存在符合条件的m2记录
 	var count int64
 	if err := e.DB.Model(new(model.MilkOriginal)).
-		Where("wid BETWEEN ? AND ?", cfg.OldUpdateMaxId+1, cfg.CurrentMaxId).
+		Where("id BETWEEN ? AND ?", cfg.OldUpdateMaxId+1, cfg.CurrentMaxId).
 		Where("milk_date = ?", data.MilkDate).
-		Where("detacher_address = ?", data.DetacherAddress).
-		Where("ABS(TIMESTAMPDIFF(SECOND, detach_time, ?)) < 10", data.DetacherTime).
+		Where("detached_address = ?", data.DetachedAddress).
+		Where("ABS(TIMESTAMPDIFF(SECOND, detach_time, ?)) < 10", data.DetachedTime).
 		Where("milk_weight = ?", data.MilkWeight).
 		Where("pasture_id = ?", data.PastureId).
 		Where("RIGHT(attach_time, 8) != '00:00:00'").
@@ -546,7 +276,7 @@ func (e *Entry) delete2(data *model.MilkOriginal, xMinD string, cfg *MilkClassCo
 		Where("id BETWEEN ? AND ?", cfg.OldUpdateMaxId+1, cfg.CurrentMaxId).
 		Where("milk_date = ?", data.MilkDate).
 		Where("shifts = ?", data.Shifts).
-		Where("detacher_address = ?", data.DetacherAddress).
+		Where("detached_address = ?", data.DetachedAddress).
 		Where("attach_time = ?", data.AttachTime).
 		Where("milk_weight = ?", data.MilkWeight).
 		Where("pasture_id = ?", data.PastureId).
@@ -603,7 +333,7 @@ func (e *Entry) delete3(data *model.MilkOriginal, xMinD string, cfg *MilkClassCo
 	}
 }
 
-func (e *Entry) delete4(data *model.MilkOriginal, xMinD string, cfg *MilkClassConfig) {
+func (e *Entry) delete4(data *model.MilkOriginal) {
 	// 1. 检查记录是否在时间范围内
 	if data.MilkDate < "2020-10-01" {
 		return
@@ -614,8 +344,8 @@ func (e *Entry) delete4(data *model.MilkOriginal, xMinD string, cfg *MilkClassCo
 	if err := e.DB.Model(new(model.MilkOriginal)).
 		Where("id = ?", data.Id).
 		Where("milk_date >= ?", "2020-10-01").
-		Where("recognition_time > detacher_time").
-		Where("attach_time > detacher_time").
+		Where("recognition_time > detached_time").
+		Where("attach_time > detached_time").
 		Where("SUBSTRING(attach_time, 12, 2) = ?", "23").
 		Where("pasture_id = ?", data.PastureId).
 		Select("1").

+ 493 - 0
module/crontab/milk_original_update_gea.go

@@ -0,0 +1,493 @@
+package crontab
+
+import (
+	"fmt"
+	"kpt-pasture/model"
+	"kpt-pasture/util"
+	"sort"
+	"strings"
+	"time"
+
+	"gorm.io/gorm"
+
+	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
+	"go.uber.org/zap"
+)
+
+// UpdateRecognitionTime 识别时间超过40分钟未套杯牛只,识别改为未识别
+func (e *Entry) UpdateRecognitionTime(pastureId int64, hall *model.MilkHall) {
+	milkOriginalList := make([]*model.MilkOriginal, 0)
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Where("pasture_id = ?", pastureId).
+		Where("milk_hall_number = ?", hall.Name).
+		Where("milk_hall_brand = ?", hall.Brand).
+		Where("load = ?", 0).
+		Find(&milkOriginalList).Error; err != nil {
+		zaplog.Error("MilkHallData", zap.Any("err", err))
+	}
+
+	for _, v := range milkOriginalList {
+		t1, _ := util.TimeParseLocal(model.LayoutTime, v.AttachTime)
+		t2, _ := util.TimeParseLocal(model.LayoutTime, v.RecognitionTime)
+		diff := t1.Sub(t2)
+		minute := int(diff.Minutes())
+
+		if util.Substr(v.RecognitionTime, -1, 8) != "00:00:00" && minute > 40 {
+			if err := e.DB.Model(new(model.MilkOriginal)).
+				Where("id = ?", v.Id).
+				Updates(map[string]interface{}{
+					"cow_id":           0,
+					"ele_ear_number":   "",
+					"recognition_time": fmt.Sprintf("%s 00:00:00", util.Substr(v.RecognitionTime, 0, 10)),
+				}).Error; err != nil {
+				zaplog.Error("MilkHallData", zap.Any("err", err))
+			}
+		}
+	}
+}
+
+// UpdateRepeatCupSet1 更新重复套杯1, 识别时间相同,且不为0为重复套杯
+func (e *Entry) UpdateRepeatCupSet1(milkOriginalList []*model.MilkOriginal) {
+	if len(milkOriginalList) == 0 {
+		return
+	}
+
+	milkOriginalMap := make(map[string][]*model.MilkOriginal)
+	for _, v := range milkOriginalList {
+		if strings.HasSuffix(v.RecognitionTime, "00:00:00") {
+			continue
+		}
+		key := fmt.Sprintf("%s_%d_%d_%s", v.MilkDate, v.Shifts, v.DetachedAddress, v.RecognitionTime)
+		milkOriginalMap[key] = append(milkOriginalMap[key], v)
+	}
+
+	for _, originalList := range milkOriginalMap {
+		if len(originalList) >= 2 {
+			// 按照Id升序排序(保留第一条)
+			sort.Slice(originalList, func(i, j int) bool {
+				return originalList[i].Id < originalList[j].Id
+			})
+
+			for i, v := range originalList {
+				if i == 0 {
+					continue
+				}
+				if err := e.DB.Model(new(model.MilkOriginal)).
+					Select("").Where("id = ?", v.Id).
+					Update("nattach", 2).Error; err != nil {
+					zaplog.Error("UpdateRepeatCupSet1", zap.Any("err", err))
+				}
+			}
+		}
+	}
+}
+
+// UpdateMilkOriginCowInfo 更新牛只信息
+func (e *Entry) UpdateMilkOriginCowInfo(milkOriginalList []*model.MilkOriginal, hall *model.MilkHall) {
+	milkHallMap := make(map[string][]*model.MilkOriginal)
+	for _, v := range milkOriginalList {
+		key := fmt.Sprintf("%s", v.MilkHallNumber)
+		milkHallMap[key] = append(milkHallMap[key], v)
+	}
+
+	dataList, ok := milkHallMap[hall.Name]
+	if !ok {
+		return
+	}
+	switch hall.IsExtraUpdate {
+	case model.IsExtra0:
+
+	case model.IsExtra1, model.IsExtra3:
+		for _, d := range dataList {
+			if d.EarNumber == "" {
+				continue
+			}
+
+			cowInfo, err := e.GetCowByEarNumber(d.PastureId, d.EarNumber)
+			if err != nil {
+				zaplog.Error("UpdateMilkOriginCowInfo", zap.Any("err", err), zap.Any("data", d))
+				continue
+			}
+			// 更新牛只信息
+			d.UpdateCowInfo(cowInfo)
+			if err = e.DB.Model(new(model.MilkOriginal)).
+				Select("cow_id", "pen_id", "pen_name").
+				Where("id = ?", d.Id).Updates(d).Error; err != nil {
+				zaplog.Error("UpdateMilkOriginCowInfo", zap.Any("err", err), zap.Any("data", d))
+			}
+		}
+	case model.IsExtra2:
+	default:
+	}
+
+}
+
+func (e *Entry) UpdateMilkOriginalInitialTimesAndAttachAdjustTime(shifts []int32, milkOriginalList []*model.MilkOriginal) {
+	for _, shift := range shifts {
+		shiftMinDetachTimes := ""
+		// 按脱杯地址分组处理
+		addressMap := make(map[int64][]*model.MilkOriginal)
+		for _, m := range milkOriginalList {
+			if m.Shifts != shift || m.DetachedTime == "" {
+				continue
+			}
+			if shiftMinDetachTimes == "" {
+				shiftMinDetachTimes = m.DetachedTime
+			} else {
+				t1, _ := util.TimeParseLocal(model.LayoutTime, m.DetachedTime)
+				t2, _ := util.TimeParseLocal(model.LayoutTime, shiftMinDetachTimes)
+				if t2.Before(t1) {
+					shiftMinDetachTimes = m.DetachedTime
+				}
+			}
+			addressMap[m.DetachedAddress] = append(addressMap[m.DetachedAddress], m)
+		}
+
+		if shiftMinDetachTimes == "" {
+			continue
+		}
+
+		bt, _ := util.TimeParseLocal(model.LayoutTime, shiftMinDetachTimes)
+		b5 := bt.Add(-5*time.Minute).Format(model.LayoutHour) + "00:00"
+
+		for _, list := range addressMap {
+			// 对当前地址的记录按时间排序
+			sort.Slice(list, func(i, j int) bool {
+				if list[i].MilkDate != list[j].MilkDate {
+					return list[i].MilkDate < list[j].MilkDate
+				}
+
+				if list[i].Shifts != list[j].Shifts {
+					return list[i].Shifts < list[j].Shifts
+				}
+				if list[i].DetachedAddress != list[j].DetachedAddress {
+					return list[i].DetachedAddress < list[j].DetachedAddress
+				}
+				return list[i].Id < list[j].Id
+			})
+
+			// 初始化变量,模拟SQL中的@address和@det
+			var lastAddress int64 = 0
+			var lastDetachTime string = "2001-01-01 06:00:00" // 默认初始值
+			// 批量更新参数
+			var updateParams []struct {
+				ID           int64
+				InitialTimes string
+				AttachAdjust string
+			}
+
+			for _, m := range list {
+				var initialTimeStr string
+				var attachAdjust string
+				// 如果当前记录的脱杯地址与上一条不同,则使用基准时间b5
+				if m.DetachedAddress != lastAddress {
+					initialTimeStr = b5
+				} else {
+					// 否则使用上一条记录的脱杯时间
+					initialTimeStr = lastDetachTime
+				}
+
+				// 更新最后记录的地址和时间
+				lastAddress = m.DetachedAddress
+				lastDetachTime = m.DetachedTime
+
+				// 只有当initialTime不为空且与原有值不同时才需要更新
+				if initialTimeStr != "" {
+					initialTime, _ := util.TimeParseLocal(model.LayoutTime, initialTimeStr)
+					attachTime, _ := util.TimeParseLocal(model.LayoutTime, m.AttachTime)
+					detachTime, _ := util.TimeParseLocal(model.LayoutTime, m.DetachedTime)
+
+					// 条件1:attachtimes以'00:00:00'结尾或attachtimes <= initialtimes
+					if strings.HasSuffix(m.AttachTime, "00:00:00") || attachTime.Before(initialTime) || attachTime.Equal(initialTime) {
+						// 计算 detachtimes - (1.5 + duration)*60 秒
+						adjustTime1 := detachTime.Add(-time.Duration((90 + m.Duration*60)) * time.Second)
+
+						// 取三者中的最大值
+						maxTime := util.FindMaxTime(attachTime, initialTime, adjustTime1)
+						attachAdjust = maxTime.Format(model.LayoutTime)
+					} else {
+						// 计算 detachtimes - duration*60 秒
+						adjustTime2 := detachTime.Add(-time.Duration(m.Duration*60) * time.Second)
+
+						// 取 attachtimes 和 adjustTime2 中的较小值
+						minTime := util.FindMinTime(attachTime, adjustTime2)
+
+						// 再与 initialtimes 取较大值
+						maxTime := util.FindMaxTime(minTime, initialTime)
+						attachAdjust = maxTime.Format(model.LayoutTime)
+					}
+
+					// 记录需要更新的字段
+					updateParams = append(updateParams, struct {
+						ID           int64
+						InitialTimes string
+						AttachAdjust string
+					}{
+						ID:           m.Id,
+						InitialTimes: initialTimeStr,
+						AttachAdjust: attachAdjust,
+					})
+
+					/*if err := e.DB.Model(new(model.MilkOriginal)).
+						Select("initial_time").
+						Where("id = ?", m.Id).
+						Update("initial_time", initialTime).Error; err != nil {
+						zaplog.Error("UpdateMilkOriginalInitialTimesAndAttachAdjustTime", zap.Any("err", err))
+					}*/
+				}
+			}
+
+			if len(updateParams) > 0 {
+				// 批量更新数据库
+				if err := e.DB.Transaction(func(tx *gorm.DB) error {
+					for _, param := range updateParams {
+						updates := map[string]interface{}{
+							"initial_times":      param.InitialTimes,
+							"attach_adjust_time": param.AttachAdjust,
+						}
+
+						if err := tx.Model(new(model.MilkOriginal)).
+							Select("initial_time", "attach_adjust_time").
+							Where("id = ? ", param.ID).
+							Updates(updates).Error; err != nil {
+							return err
+						}
+					}
+					return nil
+				}); err != nil {
+					zaplog.Error("UpdateMilkOriginalInitialTimesAndAttachAdjustTime", zap.Any("err", err))
+				}
+			}
+
+		}
+	}
+}
+
+// UpdateRepeatCupSet2  非标准重复套杯
+func (e *Entry) UpdateRepeatCupSet2(milkOriginalList []*model.MilkOriginal) {
+	for _, v := range milkOriginalList {
+		if v.AttachTime == "" || v.InitialTime == "" {
+			continue
+		}
+		nattchTime, _ := util.TimeParseLocal(model.LayoutTime, v.AttachTime)
+		initialTime, _ := util.TimeParseLocal(model.LayoutTime, v.InitialTime)
+		if util.Substr(v.InitialTime, -1, 5) != "00:00" && v.Nattach == 0 && nattchTime.Sub(initialTime).Minutes() <= 1 {
+			if err := e.DB.Model(new(model.MilkOriginal)).
+				Select("nattach").
+				Where("id = ?", v.Id).
+				Update("nattach", 2).Error; err != nil {
+				zaplog.Error("UpdateRepeatCupSet2", zap.Any("err", err))
+			}
+		}
+	}
+}
+
+// UpdateMilkNattach 非标准重复套杯
+func (e *Entry) UpdateMilkNattach(pastureId int64, milkClassConfig *MilkClassConfig, hall *model.MilkHall) {
+	milkOriginalList := make([]*model.MilkOriginal, 0)
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Where("pasture_id = ?", pastureId).
+		Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+		Find(&milkOriginalList).Error; err != nil {
+		zaplog.Error("DeleteRepeatMilkData", zap.Any("pastureId", pastureId), zap.Any("err", err))
+		return
+	}
+	for _, v := range milkOriginalList {
+		if v.InitialTime == "" || v.AttachTime == "" {
+			continue
+		}
+		attachTime, _ := util.TimeParseLocal(model.LayoutTime, v.AttachTime)
+		initialTime, _ := util.TimeParseLocal(model.LayoutTime, v.InitialTime)
+		initialTimePlus1Min := initialTime.Add(time.Minute)
+
+		if hall.Brand == v.MilkHallBrand && hall.Name == v.MilkHallNumber && v.Nattach == 0 &&
+			!strings.HasSuffix(v.InitialTime, "00:00") && (attachTime.Before(initialTimePlus1Min) || attachTime.Equal(initialTimePlus1Min)) {
+			if err := e.DB.Model(new(model.MilkOriginal)).
+				Select("nattach").
+				Where("id = ?", v.Id).
+				Update("nattach", 2).Error; err != nil {
+				zaplog.Error("UpdateMilkNattach", zap.Any("err", err))
+			}
+		}
+	}
+}
+
+// UpdateMilkNoCowId 清理无牛号牛只:重复超过2圈牛号,套杯时间间隔大于17分钟
+func (e *Entry) UpdateMilkNoCowId(pastureId int64, milkClassConfig *MilkClassConfig, hall *model.MilkHall) {
+	//  SELECT 	m2.wid
+	//	FROM (SELECT m0.milkdate, m0.shifts, m0.detacher_address, m0.cow_id, MIN(m0.wid) wid, COUNT(0) nb, MIN(m0.attachtimes) attachtimes
+	//	FROM milkweight m0 WHERE m0.wid BETWEEN xdminwid AND xdmaxwid AND m0.milkdate=xcurdate AND m0.cow_id>0 AND m0.station=xvarName
+	//	GROUP BY m0.shifts, m0.detacher_address, m0.cow_id HAVING nb>1) m1
+	//	JOIN milkweight m2 ON m1.milkdate=m2.milkdate AND m1.shifts=m2.shifts AND m2.station=xvarName AND m1.detacher_address=m2.detacher_address
+	//	AND m1.cow_id=m2.cow_id AND m1.wid<m2.wid WHERE m2.wid BETWEEN xdminwid AND xdmaxwid AND m2.milkdate=xcurdate AND m2.cow_id>0
+	//	AND (m1.attachtimes + INTERVAL 17 MINUTE) < m2.attachtimes ;
+
+	minMilkOriginalRecordsList := make([]*model.MinMilkOriginalRecords, 0)
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Select("MIN(id) AS min_id, COUNT(0) AS count, cow_id, detached_address, milk_date, MIN(attach_time) AS min_attach_time").
+		Where("pasture_id = ?", pastureId).
+		Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+		Where("cow_id > ?", 0).
+		Where("milk_hall_number = ?", hall.Name).
+		Group("shifts, detached_address, cow_id").
+		Having("count > ? AND min_attach_time != ?", 1, "").
+		Find(&minMilkOriginalRecordsList).Error; err != nil {
+		zaplog.Error("UpdateMilkNoCowId", zap.Any("err", err))
+		return
+	}
+
+	// 2. 收集需要更新的 id 列表
+	idsToUpdate := make([]int64, 0)
+	for _, v := range minMilkOriginalRecordsList {
+		var laterRecords []struct {
+			Id int64
+		}
+
+		attachTime, _ := util.TimeParseLocal(model.LayoutTime, v.MinAttachTime)
+		attachTimeAdd17 := attachTime.Add(time.Minute * 17).Format(model.LayoutTime)
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Select("id").
+			Where("pasture_id = ?", pastureId).
+			Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+			Where("cow_id = ?", v.CowId).
+			Where("detached_address = ?", v.DetachedAddress).
+			Where("milk_date = ?", v.MilkDate).
+			Where("attach_time > ?", attachTimeAdd17).Find(&laterRecords).Error; err != nil {
+			zaplog.Error("UpdateMilkNoCowId", zap.Any("err", err))
+			continue
+		}
+		if len(laterRecords) > 0 {
+			idsToUpdate = append(idsToUpdate, v.MinId)
+		}
+	}
+
+	if len(idsToUpdate) > 0 {
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Where("id IN ?", idsToUpdate).
+			Updates(map[string]interface{}{
+				"cow_id":     0,
+				"ear_number": "",
+				"pen_id":     0,
+				"pen_name":   "",
+			}).Error; err != nil {
+			zaplog.Error("UpdateMilkNoCowId", zap.Any("err", err))
+		}
+	}
+}
+
+// UpdateMilkLoad 设置批次 圈数
+func (e *Entry) UpdateMilkLoad(pastureId int64, milkClassConfig *MilkClassConfig, hall *model.MilkHall) {
+	var (
+		xAddress = int32(999)
+		//xShift          = 6
+		xLoad           = int32(0)
+		recognitionTime = time.Date(2001, 1, 1, 1, 1, 1, 0, time.Local)
+	)
+
+	baseRecords := make([]*model.BaseRecords, 0)
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Select("id,attach_adjust,detached_address,shifts,ear_number,milk_date,recognition_time").
+		Where("pasture_id = ?", pastureId).
+		Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+		Where("nattach = ?", 0).Where("milk_hall_number = ?", hall.Name).
+		Order("attach_adjust,detached_address").
+		Limit(99).Find(&baseRecords).Error; err != nil {
+		zaplog.Error("UpdateMilkLoad", zap.Any("err", err))
+		return
+	}
+
+	// 3. 处理每条记录并计算 load 值
+	recordsToUpdate := make([]*model.UpdateLoadRecord, 0)
+	for _, record := range baseRecords {
+		if record.RecognitionTime != "" && record.AttachAdjust != "" {
+			r, _ := util.TimeParseLocal(model.LayoutTime, record.RecognitionTime)
+			a, _ := util.TimeParseLocal(model.LayoutTime, record.AttachAdjust)
+			diff := int32(a.Sub(r).Minutes())
+			if !(diff >= 0 && diff <= 15 && !strings.HasSuffix(record.RecognitionTime, "00:00:00")) {
+				record.RecognitionTime = record.AttachAdjust
+			}
+		}
+		r, _ := util.TimeParseLocal(model.LayoutTime, record.RecognitionTime)
+		/*var maxLoad int32
+		if (record.DetachedAddress < xAddress-27 && r.After(recognitionTime.Add(-5*time.Minute))) || record.Shifts > xShift || r.After(recognitionTime.Add(6*time.Minute)) {
+			xLoad++
+			maxLoad = xLoad
+		} else {
+			maxLoad = xLoad
+		}*/
+
+		// 计算 currentLoad
+		var currentLoad int32
+		if (xAddress+60-record.DetachedAddress <= 27) &&
+			r.Before(recognitionTime.Add(3*time.Minute)) {
+			currentLoad = maxInt(xLoad-1, 1)
+		} else {
+			currentLoad = xLoad
+		}
+
+		// 更新地址
+		if (xAddress+60-record.DetachedAddress <= 27) &&
+			r.Before(recognitionTime.Add(3*time.Minute)) {
+			// 保持原地址
+		} else {
+			xAddress = record.DetachedAddress
+		}
+
+		// 更新shift和时间
+		//xShift = record.Shifts
+		if r.After(recognitionTime) {
+			recognitionTime = r
+		}
+
+		// 记录需要更新的数据
+		recordsToUpdate = append(recordsToUpdate, &model.UpdateLoadRecord{
+			Id:   record.Id,
+			Load: currentLoad,
+		})
+	}
+	if len(recordsToUpdate) > 0 {
+		batchSize := 100 // 每批更新100条
+		for i := 0; i < len(recordsToUpdate); i += batchSize {
+			end := i + batchSize
+			if end > len(recordsToUpdate) {
+				end = len(recordsToUpdate)
+			}
+			batch := recordsToUpdate[i:end]
+			if err := e.DB.Model(new(model.MilkOriginal)).
+				Where("id IN (?)", getWIDsFromUpdateRecords(batch)).
+				Updates(map[string]interface{}{
+					"load": gorm.Expr("CASE id " + buildCaseWhen(batch) + " END"),
+				}).Error; err != nil {
+				zaplog.Error("UpdateMilkLoad", zap.Any("err", err))
+				continue
+			}
+		}
+	}
+}
+
+// 辅助函数:从更新记录中提取WID列表
+func getWIDsFromUpdateRecords(records []*model.UpdateLoadRecord) []int64 {
+	ids := make([]int64, len(records))
+	for i, r := range records {
+		ids[i] = r.Id
+	}
+	return ids
+}
+
+// 辅助函数:构建CASE WHEN语句
+func buildCaseWhen(records []*model.UpdateLoadRecord) string {
+	var builder strings.Builder
+	for _, r := range records {
+		builder.WriteString(fmt.Sprintf("WHEN %d THEN %d ", r.Id, r.Load))
+	}
+	builder.WriteString("ELSE load")
+	return builder.String()
+}
+
+// 辅助函数:返回两个整数中的最大值
+func maxInt(a, b int32) int32 {
+	if a > b {
+		return a
+	}
+	return b
+}

+ 181 - 0
module/crontab/milk_original_update_gea_cycl_bar.go

@@ -0,0 +1,181 @@
+package crontab
+
+import (
+	"kpt-pasture/model"
+
+	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+
+	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
+	"go.uber.org/zap"
+)
+
+func (e *Entry) UpdateMilkCyclBar(pastureId int64, milkClassConfig *MilkClassConfig, hall *model.MilkHall) {
+	// 遍历4个班次
+	for shifts := int32(1); shifts <= 4; shifts++ {
+		// 1. 查询挤奶牛舍信息
+		// 查询条件:同一牧场、同一奶厅、同一班次、nattach=1、cow_id>0
+		milkOriginalList := make([]*model.MilkOriginal, 0)
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Where("pasture_id = ?", pastureId).
+			Where("milk_hall_number = ?", hall.Name).
+			Where("shifts = ?", shifts).
+			Where("nattach = ?", 1).
+			Where("cow_id > ?", 0).
+			Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+			Order("load_address").
+			Find(&milkOriginalList).Error; err != nil {
+			zaplog.Error("UpdateMilkCyclBar", zap.Any("err", err))
+			return
+		}
+
+		// 如果没有数据,直接进入下一班次
+		if len(milkOriginalList) == 0 {
+			continue
+		}
+
+		// 2. 计算牛舍信息
+		// 初始化变量
+		//var currentPenName = ""
+		var currentPenId int32 = 0
+		var count int = 0
+		var barId int32 = 0
+
+		// 创建牛舍信息映射
+		barInfoMap := make(map[int32]map[string]interface{})
+		loadAddressMap := make(map[int64]int32)
+
+		// 遍历记录,计算牛舍信息
+		for _, record := range milkOriginalList {
+			// 计算load_address
+			loadAddress := int64(record.Load*100) + record.DetachedAddress
+
+			// 如果intBarCode变化,重置计数器
+			if record.PenId != currentPenId {
+				count = 1
+				//currentPenName = record.PenName
+				currentPenId = record.PenId
+			} else {
+				count++
+			}
+
+			// 如果连续7头牛以上,记录为牛舍
+			if count >= 7 {
+				barId++
+				barInfoMap[barId] = map[string]interface{}{
+					"pen_name":     record.PenName,
+					"pen_id":       record.PenId,
+					"load":         record.Load,
+					"load_address": loadAddress,
+				}
+				loadAddressMap[loadAddress] = barId
+			}
+		}
+
+		// 如果没有牛舍信息,直接进入下一班次
+		if len(barInfoMap) == 0 {
+			continue
+		}
+
+		// 3. 更新牛舍信息
+		// 获取第一个牛舍信息
+		firstPenId := int32(1)
+		firstBarInfo := barInfoMap[firstPenId]
+		firstPenName := firstBarInfo["pen_name"].(string)
+		firstPenId = firstBarInfo["pen_id"].(int32)
+
+		// 遍历牛舍信息,更新记录
+		for i := 2; i <= len(barInfoMap); i++ {
+			// 获取当前牛舍信息
+			currentBarInfo := barInfoMap[int32(i)]
+			currentVarBarCode := currentBarInfo["pen_name"].(string)
+			currentIntBarCode := currentBarInfo["pen_id"].(int32)
+			currentLoadAddress := currentBarInfo["load_address"].(int64)
+
+			// 计算上一个牛舍的结束地址
+			var lastLoadAddress int64 = 0
+			for _, record := range milkOriginalList {
+				loadAddress := int64(record.Load*100) + record.DetachedAddress
+				if loadAddress < currentLoadAddress && record.PenName == currentVarBarCode {
+					// 计算连续4头牛的位置
+					if record.PenName == currentVarBarCode {
+						count++
+						if count == 4 {
+							lastLoadAddress = loadAddress - 3
+							break
+						}
+					} else {
+						count = 1
+					}
+				}
+			}
+
+			// 更新上一个牛舍的记录
+			if lastLoadAddress > 0 {
+				if err := e.DB.Model(new(model.MilkOriginal)).
+					Where("pasture_id = ?", pastureId).
+					Where("milk_hall_number = ?", hall.Name).
+					Where("shifts = ?", shifts).
+					Where("nattach = ?", 1).
+					Where("cow_id > ?", 0).
+					Where("var_bar_milked IS NULL").
+					Where("(load * 100 + detached_address) < ?", lastLoadAddress).
+					Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+					Updates(map[string]interface{}{
+						"pen_name": firstPenName,
+						"pen_id":   firstPenId,
+					}).Error; err != nil {
+					zaplog.Error("UpdateMilkCyclBar", zap.Any("err", err))
+					return
+				}
+			}
+
+			// 更新当前牛舍信息
+			firstPenName = currentVarBarCode
+			firstPenId = currentIntBarCode
+		}
+
+		// 4. 更新最后一个牛舍的记录
+		// 获取最后一个牛舍的最大load_address
+		var maxLoadAddress int64 = 0
+		for _, record := range milkOriginalList {
+			loadAddress := int64(record.Load*100) + record.DetachedAddress
+			if record.PenName == firstPenName && record.PenId == firstPenId && record.PenName == "" {
+				if loadAddress > maxLoadAddress {
+					maxLoadAddress = loadAddress
+				}
+			}
+		}
+
+		// 更新最后一个牛舍的记录
+		if maxLoadAddress > 0 {
+			if err := e.DB.Model(new(model.MilkOriginal)).
+				Where("pasture_id = ?", pastureId).
+				Where("milk_hall_number = ?", hall.Name).
+				Where("shifts = ?", shifts).
+				Where("var_bar_milked IS NULL").
+				Where("(load * 100 + detached_address) <= ?", maxLoadAddress).
+				Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+				Updates(map[string]interface{}{
+					"pen_name": firstPenName,
+					"pen_id":   firstPenId,
+				}).Error; err != nil {
+				zaplog.Error("UpdateMilkCyclBar", zap.Any("err", err))
+				return
+			}
+		}
+
+		// 5. 更新未识别的记录
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Where("pasture_id = ?", pastureId).
+			Where("milk_hall_number = ?", hall.Name).
+			Where("shifts = ?", shifts).
+			Where("var_bar_milked IS NULL").
+			Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+			Updates(map[string]interface{}{
+				"is_identify": pasturePb.IsShow_No,
+			}).Error; err != nil {
+			zaplog.Error("UpdateMilkCyclBar", zap.Any("err", err))
+			return
+		}
+	}
+}

+ 190 - 0
module/crontab/milk_original_update_gea_more.go

@@ -0,0 +1,190 @@
+package crontab
+
+import (
+	"kpt-pasture/model"
+	"kpt-pasture/util"
+
+	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
+	"go.uber.org/zap"
+)
+
+// UpdateMilkLoad2 (UPDATE milkweight m SET m.nattach=1 WHERE m.wid BETWEEN xdminwid AND xdmaxwid AND m.milkdate=xcurdate AND m.nattach=0 AND m.station=xvarName;)
+func (e *Entry) UpdateMilkLoad2(pastureId int64, milkClassConfig *MilkClassConfig, hall *model.MilkHall) {
+	milkOriginalList := make([]*model.MilkOriginal, 0)
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Where("pasture_id = ?", pastureId).
+		Where("nattach = ?", 0).
+		Where("milk_hall_number = ?", hall.Name).
+		Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+		Find(&milkOriginalList).Error; err != nil {
+		return
+	}
+	for _, v := range milkOriginalList {
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Where("id = ?", v.Id).
+			Update("nattach", 1).Error; err != nil {
+			zaplog.Error("UpdateMilkLoad2", zap.Any("err", err))
+			return
+		}
+	}
+}
+
+// UpdateMilkLoad3 更新重复套杯牛只圈数
+func (e *Entry) UpdateMilkLoad3(pastureId int64, milkClassConfig *MilkClassConfig, hall *model.MilkHall) {
+	milkOriginalList := make([]*model.MilkOriginal, 0)
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Where("pasture_id = ?", pastureId).
+		Where("milk_hall_number = ?", hall.Name).
+		Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+		Order("shifts,detached_address,id").
+		Find(&milkOriginalList).Error; err != nil {
+		zaplog.Error("UpdateMilkLoad3", zap.Any("err", err))
+		return
+	}
+
+	// 如果没有数据,直接返回
+	if len(milkOriginalList) == 0 {
+		return
+	}
+
+	// 初始化变量,模拟SQL中的变量
+	//var currentShifts int32 = 6
+	var currentAddress int64 = 0
+	var currentLoad int32 = 1
+
+	// 创建需要更新的记录映射
+	updateMap := make(map[int64]int32)
+
+	// 遍历排序后的记录,计算正确的load值
+	for _, record := range milkOriginalList {
+		// 如果nattach=1或者detached_address变化,则更新currentLoad
+		if record.Nattach == 1 || currentAddress != record.DetachedAddress {
+			currentLoad = record.Load
+		}
+
+		// 更新当前变量值
+		//currentShifts = record.Shifts
+		currentAddress = record.DetachedAddress
+
+		// 如果nattach=2,则记录需要更新的load值
+		if record.Nattach == 2 {
+			updateMap[record.Id] = currentLoad
+		}
+	}
+
+	// 更新数据库中的load字段
+	for id, load := range updateMap {
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Where("id = ?", id).
+			Update("load", load).Error; err != nil {
+			zaplog.Error("UpdateMilkLoad3", zap.Any("err", err), zap.Any("id", id), zap.Any("load", load))
+			return
+		}
+	}
+}
+
+// UpdateMilkCowIdResetZero 假定2.5分钟内套杯为重复套杯,3分钟外为其它牛只,清除错误牛号
+func (e *Entry) UpdateMilkCowIdResetZero(pastureId int64, milkClassConfig *MilkClassConfig, hall *model.MilkHall) {
+	// 1. 首先查询重复套杯的牛只记录
+	// 查询条件:同一牧场、同一奶厅、同一班次、同一脱杯地址、同一牛号有多条记录
+	duplicateCowRecords := make([]*model.BaseRecords, 0)
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Select("milk_date, shifts, detached_address, cow_id, COUNT(0) as count").
+		Where("pasture_id = ?", pastureId).
+		Where("milk_hall_number = ?", hall.Name).
+		Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+		Where("cow_id > ?", 0).
+		Group("milk_date, shifts, detached_address, cow_id").
+		Having("count > ?", 1).
+		Find(&duplicateCowRecords).Error; err != nil {
+		zaplog.Error("UpdateMilkCowIdResetZero", zap.Any("err", err))
+		return
+	}
+
+	// 如果没有重复套杯的记录,直接返回
+	if len(duplicateCowRecords) == 0 {
+		return
+	}
+
+	// 2. 查询需要处理的详细记录
+	baseRecords := make([]*model.BaseRecords, 0)
+	for _, record := range duplicateCowRecords {
+		var records []*model.BaseRecords
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Select("id, attach_adjust_time, detached_address, shifts, ear_number, milk_date, recognition_time").
+			Where("pasture_id = ?", pastureId).
+			Where("milk_hall_number = ?", hall.Name).
+			Where("milk_date = ?", record.MilkDate).
+			Where("shifts = ?", record.Shifts).
+			Where("detached_address = ?", record.DetachedAddress).
+			Where("cow_id = ?", record.CowId).
+			Where("id BETWEEN ? AND ?", milkClassConfig.OldUpdateMaxId+1, milkClassConfig.CurrentMaxId).
+			Order("shifts, detached_address, cow_id, id").
+			Find(&records).Error; err != nil {
+			zaplog.Error("UpdateMilkCowIdResetZero", zap.Any("err", err))
+			continue
+		}
+		baseRecords = append(baseRecords, records...)
+	}
+
+	// 如果没有详细记录,直接返回
+	if len(baseRecords) == 0 {
+		return
+	}
+
+	// 3. 初始化变量,模拟SQL中的变量
+	var currentCowId int64 = 999999
+	//var currentLoad int32 = 0
+	var currentAddress int32 = 999
+	var currentShifts int32 = 6
+	var currentDetachTime string = "2001-01-01 01:01:01"
+
+	// 4. 创建需要更新的记录映射
+	updateMap := make(map[int64]bool)
+
+	// 5. 遍历排序后的记录,计算需要清除牛号的记录
+	for _, record := range baseRecords {
+		// 解析时间
+		attachAdjustTime, err := util.TimeParseLocal(model.LayoutTime, record.AttachAdjust)
+		if err != nil {
+			continue
+		}
+		detachTime, err := util.TimeParseLocal(model.LayoutTime, currentDetachTime)
+		if err != nil {
+			continue
+		}
+
+		// 计算时间差(秒)
+		timeDiff := int64(attachAdjustTime.Sub(detachTime).Seconds())
+
+		// 判断是否需要清除牛号
+		// 条件:同一牛号、同一脱杯地址、同一班次,且时间差小于180秒(3分钟)
+		if record.CowId == currentCowId && record.DetachedAddress == currentAddress && record.Shifts == currentShifts && timeDiff < 180 {
+			// 这是重复套杯,不需要清除牛号
+		} else if record.CowId == currentCowId {
+			// 这是同一牛号但不同脱杯地址或班次,需要清除牛号
+			updateMap[record.Id] = true
+		}
+
+		// 更新当前变量值
+		currentCowId = record.CowId
+		currentAddress = record.DetachedAddress
+		currentShifts = record.Shifts
+		currentDetachTime = record.AttachAdjust
+	}
+
+	// 6. 更新数据库中的记录,清除牛号
+	for id := range updateMap {
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Where("id = ?", id).
+			Updates(map[string]interface{}{
+				"cow_id":     0,
+				"ear_number": "",
+				"pen_id":     0,
+				"pen_name":   "",
+			}).Error; err != nil {
+			zaplog.Error("UpdateMilkCowIdResetZero", zap.Any("err", err), zap.Any("id", id))
+			return
+		}
+	}
+}

+ 12 - 2
module/crontab/neck_ring_estrus.go

@@ -68,12 +68,14 @@ func (e *Entry) EntryCowEstrus(pastureId int64) (err error) {
 
 // CowEstrusWarning 发情预警
 func (e *Entry) CowEstrusWarning(pastureId int64, xToday *XToday, nowTime time.Time) {
-	neckActiveHabitList := make([]*model.NeckActiveHabit, 0) // todo 需要考虑到数据量太大的情况
+	isAlreadIds := make([]int64, 0)
+	neckActiveHabitList := make([]*model.NeckActiveHabit, 0)
 	if err := e.DB.Model(new(model.NeckActiveHabit)).
-		Where("heat_date = ?", nowTime.Format(model.LayoutDate2)).
+		Where("heat_date <= ?", nowTime.Format(model.LayoutDate2)).
 		Where("pasture_id = ?", pastureId).
 		Where("filter_high > 0 AND change_filter > ?", model.DefaultChangeFilter).
 		Where("cow_id > ?", 0).
+		Where("is_estrus = ?", pasturePb.IsShow_No).
 		Where(e.DB.Where("calving_age >= ?", MinCalvingAge).Or("lact = ?", MinLact)). // 排除产后20天内的发情牛
 		Order("cow_id").
 		Find(&neckActiveHabitList).Error; err != nil {
@@ -83,6 +85,7 @@ func (e *Entry) CowEstrusWarning(pastureId int64, xToday *XToday, nowTime time.T
 
 	neckActiveHabitMap := make(map[int64][]*model.NeckActiveHabit)
 	for _, habit := range neckActiveHabitList {
+		isAlreadIds = append(isAlreadIds, habit.Id)
 		cft := calculateCFT(habit)
 		if cft < float32(xToday.ActiveLow-XAdjust21) {
 			continue
@@ -188,6 +191,13 @@ func (e *Entry) CowEstrusWarning(pastureId int64, xToday *XToday, nowTime time.T
 			zaplog.Error("CowEstrusWarningNew", zap.Any("eventEstrusList", neckRingEstrusList), zap.Any("err", err))
 		}
 	}
+	if len(isAlreadIds) > 0 {
+		if err := e.DB.Model(new(model.NeckActiveHabit)).
+			Where("id IN (?)", isAlreadIds).
+			Update("is_estrus", pasturePb.IsShow_Ok).Error; err != nil {
+			zaplog.Error("CowEstrusWarning", zap.Any("err", err))
+		}
+	}
 }
 
 func (e *Entry) UpdateNewNeckRingEstrus(pastureId int64, nowTime time.Time) {

+ 1 - 2
module/crontab/sql.go

@@ -104,8 +104,7 @@ func (e *Entry) GetPenMapList(pastureId int64) (map[int32]*model.Pen, error) {
 func (e *Entry) GetBeforeThreeDaysCowEstrus(cowId int64, activeTime string) *model.NeckRingEstrus {
 	neckRingEstrus := &model.NeckRingEstrus{}
 	if err := e.DB.Model(new(model.NeckRingEstrus)).
-		Select("MAX(max_high) as max_high,cow_id,MAX(day_high) as day_high").
-		Select("MAX(IF(check_result=1,3,check_result)) AS check_result").
+		Select("MAX(max_high) as max_high, cow_id, MAX(day_high) as day_high, MAX(IF(check_result=1,3,check_result)) AS check_result,active_time").
 		Where("cow_id = ?", cowId).
 		Where("active_time >= ?", activeTime).
 		First(neckRingEstrus).Error; err != nil {

+ 20 - 0
util/util_more.go

@@ -3,7 +3,10 @@ package util
 import (
 	"bytes"
 	"fmt"
+	"io"
 	"math"
+	"mime/multipart"
+	"os"
 	"strconv"
 	"strings"
 	"time"
@@ -122,3 +125,20 @@ func FindMinTime(times ...time.Time) time.Time {
 	}
 	return min
 }
+
+func SaveUploadedFile(file *multipart.FileHeader, dst string) error {
+	src, err := file.Open()
+	if err != nil {
+		return err
+	}
+	defer src.Close()
+
+	out, err := os.Create(dst)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+
+	_, err = io.Copy(out, src)
+	return err
+}