Преглед на файлове

milk: hall 奶厅数据

Yi преди 4 дни
родител
ревизия
e1bdeca040

+ 26 - 0
model/milk_configure.go

@@ -0,0 +1,26 @@
+package model
+
+import pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+
+const (
+	FirstClassMilkTime      = "first_class_milk_time"
+	SecondClassMilkTime     = "second_class_milk_time"
+	ThirdClassMilkTime      = "third_class_milk_time"
+	FourthClassMilkTime     = "fourth_class_milk_time"
+	UpdateMilkOriginalMaxId = "update_max_id"
+)
+
+type MilkConfigure struct {
+	Id        int64                 `json:"id"`
+	PastureId int64                 `json:"pastureId"`
+	Name      string                `json:"name"`
+	Value     string                `json:"value"`
+	Remarks   string                `json:"remarks"`
+	IsShow    pasturePb.IsShow_Kind `json:"is_show"`
+	CreatedAt int64                 `json:"createdAt"`
+	UpdatedAt int64                 `json:"updatedAt"`
+}
+
+func (s *MilkConfigure) TableName() string {
+	return "milk_configure"
+}

+ 35 - 2
model/milk_hall.go

@@ -1,8 +1,9 @@
 package model
 
 const (
-	AFI = "afimilk"
-	GEA = "gea"
+	AFIMilk = "afimilk"
+	GEA     = "gea"
+	AFI     = "afi"
 )
 
 type MilkHallBody struct {
@@ -46,3 +47,35 @@ type AFIMilkHallOriginal struct {
 	PeakFlowRate            int32   `json:"peakFlowRate,omitempty"`
 	MilkingBimodality       bool    `json:"milkingBimodality,omitempty"`
 }
+
+type AfiHallOriginal struct {
+	XmilkDate        string  `json:"xmilkDate"`
+	Starttimes       string  `json:"starttimes"`
+	MilkDateTime     string  `json:"milkDateTime"`
+	Attachtimes      string  `json:"attachtimes"`
+	Detachtimes      string  `json:"detachtimes"`
+	Endtimes         string  `json:"endtimes"`
+	Nattach          int     `json:"nattach"`
+	Shifts           int     `json:"shifts"`
+	Load             int     `json:"load"`
+	DetacherAddress  int     `json:"detacher_address"`
+	Varcowcode       string  `json:"varcowcode"`
+	Eidstation       int     `json:"eidstation"`
+	MilkWeight       float32 `json:"milk_weight"`
+	MilkConductivity int     `json:"milk_conductivity"`
+	Duration         float32 `json:"duration"`
+	KickOffs         int     `json:"kickOffs"`
+	ManualDetach     int     `json:"manualDetach"`
+	PeakFlow         float32 `json:"peakFlow"`
+	PeakFlowTime     int     `json:"peakFlowTime"`
+	TakeOffFlow      float32 `json:"takeOffFlow"`
+	LowFlowTime      int     `json:"LowFlowTime"`
+	Flow0to15        int     `json:"flow0to15"`
+	Flow15to30       int     `json:"flow15to30"`
+	Flow30to60       int     `json:"flow30to60"`
+	Flow60to120      int     `json:"flow60to120"`
+}
+
+type GEAMilkHallOriginal struct {
+	Vstr string `json:"vstr"`
+}

+ 53 - 1
model/milk_original.go

@@ -8,6 +8,7 @@ type MilkOriginal struct {
 	EleEarNumber     string  `json:"eleEarNumber"`
 	PenId            int32   `json:"penId"`
 	PenName          string  `json:"penName"`
+	MilkHallBrand    string  `json:"milkHallBrand"`
 	MilkDate         string  `json:"milkDate"`
 	MilkWeight       int64   `json:"milkWeight"`
 	StartTime        string  `json:"startTime"`
@@ -54,9 +55,10 @@ func (m *MilkOriginal) tableName() string {
 func NewAFIMilkOriginal(pastureId int64, milkHallNumber string, req *AFIMilkHallOriginal) *MilkOriginal {
 	return &MilkOriginal{
 		PastureId:        pastureId,
+		MilkHallBrand:    AFI,
 		CowId:            0,
 		EarNumber:        req.AnimalID,
-		EleEarNumber:     "",
+		EleEarNumber:     req.AnimalID,
 		PenId:            0,
 		PenName:          "",
 		MilkDate:         req.SessionDate,
@@ -96,3 +98,53 @@ func NewAFIMilkOriginal(pastureId int64, milkHallNumber string, req *AFIMilkHall
 		Flow60To120:      req.FlowRate60To120,
 	}
 }
+
+func NewGEAMilkOriginal(
+	pastureId int64, milkHallNumber string, detachTime, earNumber, milkDate, attachTime string, milkWeight, detacherAddress int64,
+	duration, peakFlow float64, conductivity int32, eId string, f0t15, f15t30, f30t60, f60t120 int64, manualDetach int8,
+) *MilkOriginal {
+	return &MilkOriginal{
+		PastureId:        pastureId,
+		CowId:            0,
+		EarNumber:        earNumber,
+		EleEarNumber:     eId,
+		PenId:            0,
+		PenName:          "",
+		MilkHallBrand:    "",
+		MilkDate:         milkDate,
+		MilkWeight:       milkWeight,
+		StartTime:        "",
+		InitialTime:      "",
+		AttachTime:       attachTime,
+		AttachAdjustTime: "",
+		DetacherTime:     detachTime,
+		EndTime:          "",
+		DetacherAddress:  detacherAddress,
+		Conductivity:     conductivity,
+		CowActivity:      0,
+		Source:           0,
+		MilkHallNumber:   milkHallNumber,
+		Shifts:           0,
+		Load:             0,
+		Nattach:          0,
+		RecognitionTime:  "",
+		IsYieldLow:       0,
+		PeakFlow:         peakFlow,
+		AvgFlow:          0,
+		Duration:         duration,
+		PearFlowTime:     0,
+		LowFlowTime:      0,
+		YieldPercentage:  0,
+		ActualMilkTime:   "",
+		KickOffs:         false,
+		Blocks:           0,
+		Slips:            0,
+		ManualDetach:     manualDetach,
+		TakeOffFlow:      0,
+		LowMilkFlowPc:    0,
+		Flow0To15:        f0t15,
+		Flow15To30:       f15t30,
+		Flow30To60:       f30t60,
+		Flow60To120:      f60t120,
+	}
+}

+ 27 - 0
module/backend/milk_afimilk.go

@@ -23,5 +23,32 @@ func (a *AFIMILK) SaveData(ctx context.Context, body *model.MilkHallBody) error
 	if err := json.Unmarshal(content, &afiMilkList); err != nil {
 		return xerr.WithStack(err)
 	}
+	pastureData := &model.PastureList{}
+	if err := a.store.DB.Model(pastureData).
+		Where("farm_id = ?", body.FarmId).
+		First(pastureData).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+
+	milkOriginalList := make([]*model.MilkOriginal, 0)
+	for _, afiMilk := range afiMilkList {
+		milkOriginalList = append(milkOriginalList, model.NewAFIMilkOriginal(pastureData.Id, body.MilkHallNumber, afiMilk))
+	}
+
+	if len(milkOriginalList) > 0 {
+		if err := a.store.DB.Model(new(model.MilkOriginal)).
+			Create(milkOriginalList).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+	}
+
+	return nil
+}
+
+type AFI struct {
+	store *StoreEntry
+}
+
+func (a *AFI) SaveData(ctx context.Context, body *model.MilkHallBody) error {
 	return nil
 }

+ 115 - 1
module/backend/milk_gea.go

@@ -2,7 +2,23 @@ package backend
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
 	"kpt-pasture/model"
+	"kpt-pasture/util"
+	"strconv"
+	"strings"
+	"time"
+
+	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
+	"go.uber.org/zap"
+
+	"gitee.com/xuyiping_admin/pkg/xerr"
+)
+
+const (
+	eidStart  = 7
+	eidEndLen = 64 + 7 - eidStart
 )
 
 // GEAMILK gea品牌实现
@@ -11,6 +27,104 @@ type GEAMILK struct {
 }
 
 func (a *GEAMILK) SaveData(ctx context.Context, body *model.MilkHallBody) error {
-	// TODO: 实现品牌1的数据保存逻辑
+	gEAMilkHallOriginalList := make([]*model.GEAMilkHallOriginal, 0)
+	if err := json.Unmarshal(body.Content, &gEAMilkHallOriginalList); err != nil {
+		return xerr.WithStack(err)
+	}
+	nowTime := time.Now()
+	zaplog.Info("GEAMILK", zap.String("nowTime", nowTime.Format(model.LayoutTime)), zap.Any("body", body))
+	pastureData := &model.PastureList{}
+	if err := a.store.DB.Model(new(model.PastureList)).
+		Where("farm_id = ?", body.FarmId).
+		First(pastureData).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+
+	gEAOriginalList := make([]*model.MilkOriginal, 0)
+	for _, gea := range gEAMilkHallOriginalList {
+
+		text := gea.Vstr
+		s1 := strings.TrimSpace(util.Substr(text, 0, 6))
+		milkWeight, _ := strconv.ParseInt(strings.TrimSpace(util.Substr(text, 25, 4)), 10, 64)
+
+		if len(s1) == 0 && milkWeight == 0 {
+			continue
+		}
+
+		hour := strings.TrimSpace(util.Substr(text, 0, 2))
+		minute := strings.TrimSpace(util.Substr(text, 2, 2))
+		second := strings.TrimSpace(util.Substr(text, 4, 2))
+
+		milkDate, attachTime := "", ""
+		hour1 := strings.TrimSpace(util.Substr(text, 37, 2))
+		minute1 := strings.TrimSpace(util.Substr(text, 39, 2))
+		second1 := strings.TrimSpace(util.Substr(text, 41, 2))
+
+		hour2 := strings.TrimSpace(util.Substr(text, 44, 2))
+		minute2 := strings.TrimSpace(util.Substr(text, 46, 2))
+		second2 := strings.TrimSpace(util.Substr(text, 49, 2))
+		if hour == "00" {
+			if hour1 == "23" {
+				milkDate = nowTime.AddDate(0, 0, -1).Format(model.LayoutDate2)
+			} else {
+				milkDate = nowTime.Format(model.LayoutDate2)
+			}
+
+			if hour2 == "23" {
+				attachTime = nowTime.AddDate(0, 0, -1).Format(model.LayoutDate2)
+			} else {
+				attachTime = nowTime.Format(model.LayoutTime)
+			}
+		}
+		milkDate = fmt.Sprintf("%s %s:%s:%s", milkDate, hour1, minute1, second1)
+		attachTime = fmt.Sprintf("%s %s:%s:%s", attachTime, hour2, minute2, second2)
+
+		detachTime := fmt.Sprintf("%s %s:%s:%s", nowTime, hour, minute, second)
+		earNumber := fmt.Sprintf("%s", strings.TrimSpace(util.Substr(text, 7, 6)))
+		detacherAddress, _ := strconv.ParseInt(strings.TrimSpace(util.Substr(text, 20, 2)), 10, 64)
+
+		duration, _ := strconv.ParseFloat(strings.TrimSpace(util.Substr(text, 31, 4)), 64)
+		peakFlow, _ := strconv.ParseFloat(strings.TrimSpace(util.Substr(text, 52, 4)), 64)
+		conductivity, _ := strconv.ParseInt(strings.TrimSpace(util.Substr(text, 57, 4)), 10, 64)
+		timestampDiffSeconds := TimestampDiffSeconds(attachTime, detachTime)
+		// 过滤无奶量且时间间隔小于30秒的记录
+		if milkWeight == 0 && duration == 0 && timestampDiffSeconds < 30 {
+			continue
+		}
+
+		eId := fmt.Sprintf("%s", strings.TrimSpace(util.Substr(text, eidStart, eidEndLen)))
+		f0t15, _ := strconv.ParseInt(strings.TrimSpace(util.Substr(text, 90, 3)), 10, 64)
+		f15t30, _ := strconv.ParseInt(strings.TrimSpace(util.Substr(text, 95, 3)), 10, 64)
+		f30t60, _ := strconv.ParseInt(strings.TrimSpace(util.Substr(text, 100, 3)), 10, 64)
+		f60t120, _ := strconv.ParseInt(strings.TrimSpace(util.Substr(text, 105, 3)), 10, 64)
+		manualDetach, _ := strconv.ParseInt(strings.TrimSpace(util.Substr(text, 109, 1)), 10, 64)
+
+		newGEAOriginal := model.NewGEAMilkOriginal(pastureData.Id, body.MilkHallNumber, detachTime, earNumber, milkDate, attachTime,
+			milkWeight, detacherAddress, duration, peakFlow, int32(conductivity), eId, f0t15, f15t30, f30t60, f60t120, int8(manualDetach))
+		gEAOriginalList = append(gEAOriginalList, newGEAOriginal)
+	}
+
+	if len(gEAOriginalList) > 0 {
+		if err := a.store.DB.Model(new(model.MilkOriginal)).
+			Create(gEAOriginalList).Error; err != nil {
+			return xerr.WithStack(err)
+		}
+	}
+
 	return nil
 }
+
+func TimestampDiffSeconds(startTime, endTime string) int64 {
+	start, err := util.TimeParseLocal(model.LayoutTime, startTime)
+	if err != nil {
+		return 0
+	}
+
+	end, err := util.TimeParseLocal(model.LayoutTime, endTime)
+	if err != nil {
+		return 0
+	}
+
+	diff := end.Sub(start)
+	return int64(diff.Seconds())
+}

+ 3 - 1
module/backend/milk_hall.go

@@ -20,8 +20,10 @@ func (s *StoreEntry) MilkHallOriginal(ctx context.Context, req []byte) error {
 	}
 	var milkBrand MilkBrand
 	switch body.Brand {
-	case model.AFI:
+	case model.AFIMilk:
 		milkBrand = &AFIMILK{store: s}
+	case model.AFI:
+		milkBrand = &AFI{store: s}
 	case model.GEA:
 		milkBrand = &GEAMILK{store: s}
 	default:

+ 2 - 0
module/crontab/interface.go

@@ -44,4 +44,6 @@ type Crontab interface {
 
 	UpdatePenBehavior() error      // 栏舍行为数据
 	UpdatePenBehaviorDaily() error // 栏舍饲养监测
+
+	UpdateMilkOriginal() error
 }

+ 441 - 0
module/crontab/milk_original.go

@@ -0,0 +1,441 @@
+package crontab
+
+import (
+	"fmt"
+	"kpt-pasture/model"
+	"kpt-pasture/util"
+	"strconv"
+	"strings"
+	"time"
+
+	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
+	"go.uber.org/zap"
+)
+
+func (e *Entry) UpdateMilkOriginal() error {
+	pastureList := e.FindPastureList()
+	if pastureList == nil || len(pastureList) == 0 {
+		return nil
+	}
+
+	for _, pasture := range pastureList {
+		e.ProcessMilkOriginal(pasture.Id)
+	}
+
+	return nil
+}
+
+func (e *Entry) ProcessMilkOriginal(pastureId int64) {
+	milkConfigList, err := e.FindMilkConfigure(pastureId)
+	if err != nil {
+		zaplog.Error("MilkOriginal", zap.Any("pastureId", pastureId), zap.Any("err", err))
+		return
+	}
+	milkClassConfig := &MilkClassConfig{}
+	for _, v := range milkConfigList {
+		switch v.Name {
+		case model.FirstClassMilkTime:
+			milkClassConfig.FirstClassMilkTime = v.Value
+		case model.SecondClassMilkTime:
+			milkClassConfig.SecondClassMilkTime = v.Value
+		case model.ThirdClassMilkTime:
+			milkClassConfig.ThirdClassMilkTime = v.Value
+		case model.FourthClassMilkTime:
+			milkClassConfig.FourthClassMilkTime = v.Value
+		case model.UpdateMilkOriginalMaxId:
+			maxId, _ := strconv.ParseInt(v.Value, 10, 64)
+			milkClassConfig.OldUpdateMaxId = maxId
+		}
+	}
+	xDBeg, xBeg1, xBeg2, xBeg3, xBeg4 := parseXBeg(milkClassConfig)
+	e.UpdateShifts(pastureId, xBeg1, xBeg2, xBeg3, xBeg4)
+	e.UpdateMilkDate(pastureId, xDBeg)
+
+	// 获取当前最大Id
+	var currentMaxId int64
+	if err = e.DB.Model(new(model.MilkOriginal)).
+		Select("MAX(id)").
+		Where("pasture_id = ?", pastureId).
+		Where("id > ?", milkClassConfig.OldUpdateMaxId).
+		Scan(&currentMaxId).Error; err != nil {
+		zaplog.Error("DeleteRepeatMilkData GetOriWid", zap.Any("pastureId", pastureId), zap.Any("err", err))
+		return
+	}
+	milkClassConfig.CurrentMaxId = currentMaxId
+
+	// 获取时间范围和ID范围
+	deleteModel := &DeleteMilkOriginal{}
+	selectSql := fmt.Sprintf(`MIN(DATE(TIMESTAMPADD(HOUR, -%d, attach_time))) as x_mind, 
+	MAX(DATE(TIMESTAMPADD(HOUR, -%d, attach_time))) as x_max_d2, MIN(id) as x_min_wid, MAX(id) as x_max_wid`, xDBeg, xDBeg)
+	if err = e.DB.Model(new(model.MilkOriginal)).
+		Select(selectSql).
+		Where("pasture_id = ?", pastureId).
+		Where("id > ?", milkClassConfig.OldUpdateMaxId).
+		First(deleteModel).Error; err != nil {
+		zaplog.Error("DeleteRepeatMilkData", zap.Any("pastureId", pastureId), zap.Any("err", err))
+		return
+	}
+	e.DeleteRepeatMilkData(pastureId, deleteModel, milkClassConfig)
+}
+
+// UpdateShifts 更新班次
+func (e *Entry) UpdateShifts(pastureId int64, xBeg1, xBeg2, xBeg3, xBeg4 int) {
+	milkOriginalList := make([]*model.MilkOriginal, 0)
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Where("pasture_id = ?", pastureId).
+		Where("shifts = ?", 0).
+		Find(&milkOriginalList).Error; err != nil {
+		zaplog.Error("UpdateShifts", zap.Any("pastureId", pastureId), zap.Any("err", err))
+	}
+
+	for _, v := range milkOriginalList {
+		subDetachTime1 := util.Substr(v.DetacherTime, 11, 2)
+		subDetachTime2 := util.Substr(v.DetacherTime, 14, 2)
+
+		subDetachTime1Int, _ := strconv.ParseInt(subDetachTime1, 10, 64)
+		subDetachTime2Int, _ := strconv.ParseInt(subDetachTime2, 10, 64)
+		allDetachTime := int(subDetachTime1Int*100 + subDetachTime2Int)
+
+		// 更新第一班
+		if xBeg2 > xBeg1 {
+			if allDetachTime >= xBeg1 && allDetachTime <= xBeg2 {
+				v.Shifts = 1
+			}
+		} else {
+			if allDetachTime >= xBeg1 || allDetachTime <= xBeg2 {
+				v.Shifts = 1
+			}
+		}
+
+		// 更新第二班
+		if xBeg3 > xBeg2 {
+			if allDetachTime >= xBeg2 && allDetachTime <= xBeg3 {
+				v.Shifts = 2
+			}
+		} else {
+			if allDetachTime >= xBeg2 || allDetachTime <= xBeg3 {
+				v.Shifts = 2
+			}
+		}
+
+		// 更新第四班(如果有)
+		if xBeg4 > 0 && xBeg4 != xBeg1 {
+			if xBeg1 > xBeg4 {
+				if allDetachTime >= xBeg4 && allDetachTime <= xBeg1 {
+					v.Shifts = 4
+				}
+			} else {
+				if allDetachTime >= xBeg4 || allDetachTime <= xBeg1 {
+					v.Shifts = 4
+				}
+			}
+		}
+
+		// 如果还没有分配班次,则分配到第三班
+		if v.Shifts == 0 {
+			v.Shifts = 3
+		}
+		// 批量更新数据库
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Select("shifts").
+			Where("id = ?", v.Id).
+			Updates(v).Error; err != nil {
+			zaplog.Error("UpdateShifts Save", zap.Any("pastureId", pastureId), zap.Any("err", err))
+		}
+	}
+}
+
+// UpdateMilkDate 更换挤奶时间
+func (e *Entry) UpdateMilkDate(pastureId int64, xDBeg int) {
+	milkOriginalList := make([]*model.MilkOriginal, 0)
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Where("pasture_id = ?", pastureId).
+		Where("milk_date = ?", "").
+		Find(&milkOriginalList).Error; err != nil {
+		zaplog.Error("UpdateMilkDate", zap.Any("pastureId", pastureId), zap.Any("err", err))
+		return
+	}
+
+	for _, v := range milkOriginalList {
+		// 获取结束时间,如果为空则使用默认时间
+		var endTime time.Time
+		if v.EndTime == "" {
+			endTime = time.Date(1999, 12, 31, 23, 0, 0, 0, time.Local)
+		}
+
+		// 比较挤奶时间和结束时间,取较晚的时间
+		detacherTime, _ := util.TimeParseLocal(model.LayoutTime, v.DetacherTime)
+		latestTime := detacherTime
+		if endTime.After(detacherTime) {
+			latestTime = endTime
+		}
+
+		// 减去xDBeg小时并获取日期
+		milkDate := latestTime.Add(time.Duration(-xDBeg) * time.Hour).Format(model.LayoutDate2)
+
+		// 更新数据库
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Select("milk_date").
+			Where("id = ?", v.Id).
+			Update("milk_date", milkDate).Error; err != nil {
+			zaplog.Error("UpdateMilkDate", zap.Any("err", err), zap.Any("milkDate", milkDate))
+		}
+	}
+}
+
+// DeleteRepeatMilkData 删除重复数据
+func (e *Entry) DeleteRepeatMilkData(pastureId int64, deleteModel *DeleteMilkOriginal, cfg *MilkClassConfig) {
+	// 获取最小日期对应的最小wid
+	var oriWid int64
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Select("MIN(id)").
+		Where("pasture_id = ?", pastureId).
+		Where("milk_date = ?", deleteModel.XMind).
+		Scan(&oriWid).Error; err != nil {
+		zaplog.Error("DeleteRepeatMilkData GetOriWid", zap.Any("pastureId", pastureId), zap.Any("err", err))
+		return
+	}
+
+	milkOriginalList := make([]*model.MilkOriginal, 0)
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Where("pasture_id = ?", pastureId).
+		Where("id BETWEEN ? AND ?", cfg.OldUpdateMaxId+1, cfg.CurrentMaxId).
+		Find(&milkOriginalList).Error; err != nil {
+		zaplog.Error("DeleteRepeatMilkData", zap.Any("pastureId", pastureId), zap.Any("err", err))
+	}
+
+	for _, v := range milkOriginalList {
+		e.Delete1(v, deleteModel.XMind, cfg)
+		e.Delete2(v, deleteModel.XMind, cfg)
+		e.Delete3(v, deleteModel.XMind, cfg)
+		e.Delete4(v, deleteModel.XMind, cfg)
+	}
+
+	/*// 2. 删除重复记录(除第一条外)
+	if err := e.DB.Exec(`
+		DELETE FROM milk_original
+		WHERE wid IN (
+			SELECT m.wid FROM (
+				SELECT m1.wid, m1.milk_date, m1.shifts, m1.detacher_address, m1.attach_time, m1.milk_weight, m1.pasture_id
+				FROM milk_original m1
+				WHERE m1.wid BETWEEN ? AND ?
+				AND m1.milk_date >= ?
+				AND EXISTS (
+					SELECT 1 FROM milk_original m2
+					WHERE m2.wid BETWEEN ? AND ?
+					AND m2.milk_date = m1.milk_date
+					AND m2.shifts = m1.shifts
+					AND m2.detacher_address = m1.detacher_address
+					AND m2.attach_time = m1.attach_time
+					AND m2.milk_weight = m1.milk_weight
+					AND m2.pasture_id = m1.pasture_id
+					AND m2.wid < m1.wid
+				)
+			) m
+		)`,
+		oriWid, maxWid, minDate, oriWid, maxWid).Error; err != nil {
+		zaplog.Error("DeleteRepeatMilkData DeleteDuplicates", zap.Any("err", err))
+	}
+
+	// 4. 删除各班次开始前无奶量记录
+	if err := e.DB.Exec(`
+		DELETE FROM milk_original
+		WHERE wid IN (
+			SELECT m.wid FROM (
+				SELECT m1.wid, m1.milk_date, m1.shifts, m1.milk_weight,
+						@tot := IF(@tot + m1.milk_weight > 100, 100,
+							IF(m1.shifts = @shifts, @tot, 0) + m1.milk_weight) m_tot,
+						@shifts := m1.shifts
+				FROM milk_original m1, (SELECT @tot := 0, @shifts := 0) aa
+				WHERE m1.wid BETWEEN ? AND ?
+				AND m1.milk_date >= ?
+				AND m1.pasture_id = ?
+				ORDER BY m1.milk_date, m1.shifts, STR_TO_DATE(m1.attach_time, '%Y-%m-%d %H:%i:%s')
+			) m
+			WHERE m.m_tot = 0
+		)`,
+		minWid, maxWid, minDate, pastureId).Error; err != nil {
+		zaplog.Error("DeleteRepeatMilkData DeleteNoMilkAtShift", zap.Any("err", err))
+	}
+
+	// 5. 删除时间异常数据
+	if err := e.DB.Exec(`
+		DELETE FROM milk_original
+		WHERE wid BETWEEN ? AND ?
+		AND milk_date >= '2020-10-01'
+		AND STR_TO_DATE(milk_date_time, '%Y-%m-%d %H:%i:%s') > STR_TO_DATE(detacher_time, '%Y-%m-%d %H:%i:%s')
+		AND STR_TO_DATE(attach_time, '%Y-%m-%d %H:%i:%s') > STR_TO_DATE(detacher_time, '%Y-%m-%d %H:%i:%s')
+		AND HOUR(STR_TO_DATE(attach_time, '%Y-%m-%d %H:%i:%s')) = 23
+		AND pasture_id = ?`,
+		minWid, maxWid, pastureId).Error; err != nil {
+		zaplog.Error("DeleteRepeatMilkData DeleteAbnormalTime", zap.Any("err", err))
+	}*/
+}
+
+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" {
+		return
+	}
+
+	// 2. 检查是否存在符合条件的m2记录
+	var count int64
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Where("wid 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("milk_weight = ?", data.MilkWeight).
+		Where("pasture_id = ?", data.PastureId).
+		Where("RIGHT(attach_time, 8) != '00:00:00'").
+		Count(&count).Error; err != nil {
+		zaplog.Error("Delete1", zap.Any("err", err))
+		return
+	}
+
+	if count > 0 {
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Where("id = ?", data.Id).
+			Delete(data).Error; err != nil {
+			zaplog.Error("Delete1", zap.Any("err", err), zap.Any("data", data))
+		}
+	}
+}
+
+func (e *Entry) Delete2(data *model.MilkOriginal, xMinD string, cfg *MilkClassConfig) {
+	// 1. 检查记录是否在时间范围内
+	if data.MilkDate < xMinD {
+		return
+	}
+
+	// 2. 检查是否存在重复记录(除第一条外)
+	var count int64
+	if err := e.DB.Model(new(model.MilkOriginal)).
+		Where("id BETWEEN ? AND ?", cfg.OldUpdateMaxId+1, cfg.CurrentMaxId).
+		Where("milk_date = ?", data.MilkDate).
+		Where("shifts = ?", data.Shifts).
+		Where("detacher_address = ?", data.DetacherAddress).
+		Where("attach_time = ?", data.AttachTime).
+		Where("milk_weight = ?", data.MilkWeight).
+		Where("pasture_id = ?", data.PastureId).
+		Where("id < ?", data.Id). // 只查找比当前记录更早的记录
+		Count(&count).Error; err != nil {
+		zaplog.Error("Delete2", zap.Any("err", err))
+		return
+	}
+
+	// 3. 如果存在重复记录,则删除当前记录
+	if count > 0 {
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Where("id = ?", data.Id).
+			Delete(data).Error; err != nil {
+			zaplog.Error("Delete2", zap.Any("err", err), zap.Any("data", data))
+		}
+	}
+}
+
+func (e *Entry) Delete3(data *model.MilkOriginal, xMinD string, cfg *MilkClassConfig) {
+	// 1. 检查记录是否在时间范围内
+	if data.MilkDate < xMinD {
+		return
+	}
+
+	// 2. 检查是否为班次开始且无奶量记录
+	var isFirstInShift bool
+	if err := e.DB.Raw(`
+        SELECT 1 FROM (
+            SELECT m.id, m.milk_date, m.shifts, m.milk_weight,
+                @tot := IF(@tot + m.milk_weight > 100, 100, 
+                    IF(m.shifts = @shifts, @tot, 0) + m.milk_weight) m_tot,
+                @shifts := m.shifts
+            FROM milk_original m, (SELECT @tot := 0, @shifts := 0) vars
+            WHERE m.id BETWEEN ? AND ?
+            AND m.milk_date >= ?
+            AND m.pasture_id = ?
+            ORDER BY m.milk_date, m.shifts, m.attach_time
+        ) t 
+        WHERE t.id = ? AND t.m_tot = 0`,
+		cfg.OldUpdateMaxId+1, cfg.CurrentMaxId, xMinD, data.PastureId, data.Id).
+		Scan(&isFirstInShift).Error; err != nil {
+		zaplog.Error("Delete3", zap.Any("err", err))
+		return
+	}
+
+	// 3. 如果是班次开始且无奶量记录,则删除
+	if isFirstInShift {
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Where("id = ?", data.Id).
+			Delete(data).Error; err != nil {
+			zaplog.Error("Delete3", zap.Any("err", err), zap.Any("data", data))
+		}
+	}
+}
+
+func (e *Entry) Delete4(data *model.MilkOriginal, xMinD string, cfg *MilkClassConfig) {
+	// 1. 检查记录是否在时间范围内
+	if data.MilkDate < "2020-10-01" {
+		return
+	}
+
+	// 2. 检查是否为时间异常记录
+	var isAbnormal bool
+	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("SUBSTRING(attach_time, 12, 2) = ?", "23").
+		Where("pasture_id = ?", data.PastureId).
+		Select("1").
+		Scan(&isAbnormal).Error; err != nil {
+		zaplog.Error("Delete4", zap.Any("err", err))
+		return
+	}
+
+	// 3. 如果是时间异常记录,则删除
+	if isAbnormal {
+		if err := e.DB.Model(new(model.MilkOriginal)).
+			Where("id = ?", data.Id).
+			Delete(data).Error; err != nil {
+			zaplog.Error("Delete4", zap.Any("err", err), zap.Any("data", data))
+		}
+	}
+}
+
+func parseXBeg(cfg *MilkClassConfig) (xBeg, xBeg1, xBeg2, xBeg3, xBeg4 int) {
+	xBeg1Parts := strings.Split(cfg.FirstClassMilkTime, ":")
+	if len(xBeg1Parts) < 2 {
+		return
+	}
+	// 提取最后一部分
+	xBeg1LastPart, _ := strconv.Atoi(xBeg1Parts[len(xBeg1Parts)-1])
+	xBeg, _ = strconv.Atoi(xBeg1Parts[0])
+	xBeg1, _ = strconv.Atoi(xBeg1Parts[1])
+	xBeg1 = xBeg1*100 + xBeg1LastPart
+
+	xBeg2Parts := strings.Split(cfg.SecondClassMilkTime, ":")
+	if len(xBeg2Parts) < 2 {
+		return
+	}
+	xBeg2LastPart, _ := strconv.Atoi(xBeg2Parts[len(xBeg2Parts)-1])
+	xBeg2, _ = strconv.Atoi(xBeg2Parts[0])
+	xBeg2 = xBeg2*100 + xBeg2LastPart
+
+	xBeg3Parts := strings.Split(cfg.ThirdClassMilkTime, ":")
+	if len(xBeg3Parts) < 2 {
+		return
+	}
+	xBeg3LastPart, _ := strconv.Atoi(xBeg3Parts[len(xBeg3Parts)-1])
+	xBeg3, _ = strconv.Atoi(xBeg3Parts[0])
+	xBeg3 = xBeg3*100 + xBeg3LastPart
+
+	xBeg4Parts := strings.Split(cfg.FourthClassMilkTime, ":")
+	if len(xBeg4Parts) < 2 {
+		return
+	}
+	xBeg4LastPart, _ := strconv.Atoi(xBeg4Parts[len(xBeg4Parts)-1])
+	xBeg4, _ = strconv.Atoi(xBeg4Parts[0])
+	xBeg4 = xBeg4*100 + xBeg4LastPart
+	return
+}

+ 16 - 0
module/crontab/model.go

@@ -97,3 +97,19 @@ type SecondFilterData struct {
 	RuminaFilter   float64
 	ChewFilter     float64
 }
+
+type MilkClassConfig struct {
+	FirstClassMilkTime  string
+	SecondClassMilkTime string
+	ThirdClassMilkTime  string
+	FourthClassMilkTime string
+	OldUpdateMaxId      int64
+	CurrentMaxId        int64
+}
+
+type DeleteMilkOriginal struct {
+	XMind   string
+	XMaxD2  string
+	XMinWid int64
+	XMaxWid int64
+}

+ 11 - 0
module/crontab/sql.go

@@ -169,6 +169,17 @@ func (e *Entry) FindSystemNeckRingConfigure(pastureId int64) ([]*model.NeckRingC
 	return res, nil
 }
 
+func (e *Entry) FindMilkConfigure(pastureId int64) ([]*model.MilkConfigure, error) {
+	res := make([]*model.MilkConfigure, 0)
+	if err := e.DB.Model(new(model.MilkConfigure)).
+		Where("pasture_id = ?", pastureId).
+		Where("is_show = ?", pasturePb.IsShow_Ok).
+		Find(&res).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	return res, nil
+}
+
 func (e *Entry) GetSystemNeckRingConfigure(pastureId int64, name string) (*model.NeckRingConfigure, error) {
 	res := &model.NeckRingConfigure{}
 	if err := e.DB.Model(new(model.NeckRingConfigure)).

+ 0 - 85
util/util.go

@@ -1,13 +1,10 @@
 package util
 
 import (
-	"bytes"
 	"fmt"
 	"math"
 	"math/rand"
 	"regexp"
-	"strconv"
-	"strings"
 	"time"
 
 	"gitee.com/xuyiping_admin/pkg/xerr"
@@ -520,85 +517,3 @@ func FrameIds(xFrameId int32) []int32 {
 	}
 	return frameIds
 }
-
-func CurrentMaxFrameId() int32 {
-	currentHour := time.Now().Hour()
-	return int32(math.Floor(float64(currentHour/2))) * 10
-}
-
-func ArrayInt32ToStrings(cowIds []int32, cutset string) string {
-	var cows bytes.Buffer
-	if len(cowIds) <= 0 {
-		return cows.String()
-	}
-	for i, v := range cowIds {
-		if i > 0 {
-			cows.WriteString(cutset)
-		}
-		cows.WriteString(strconv.Itoa(int(v))) // 将整数转换为字符串
-	}
-	return cows.String()
-}
-
-// ConvertCowIdsToInt64Slice 将逗号拼接的字符串转换为 int64 切片
-func ConvertCowIdsToInt64Slice(input string) ([]int64, error) {
-	// 如果输入为空,直接返回空切片
-	if input == "" {
-		return []int64{}, nil
-	}
-
-	// 按逗号分割字符串
-	parts := strings.Split(input, ",")
-
-	// 初始化结果切片
-	result := make([]int64, 0, len(parts))
-
-	// 遍历每个部分,转换为 int64
-	for _, part := range parts {
-		// 去除空格
-		part = strings.TrimSpace(part)
-		if part == "" {
-			continue // 忽略空字符串
-		}
-
-		// 转换为 int64
-		num, err := strconv.ParseInt(part, 10, 64)
-		if err != nil {
-			return nil, fmt.Errorf("invalid number: %s", part)
-		}
-
-		// 添加到结果切片
-		result = append(result, num)
-	}
-	return result, nil
-}
-
-// GetMonthStartAndEndTimestamp 获取当前月份的开始时间戳和结束时间戳
-func GetMonthStartAndEndTimestamp() (startTimestamp, endTimestamp int64) {
-	// 获取当前时间
-	now := time.Now()
-
-	// 获取当前月份的第一天
-	startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
-
-	// 获取下一个月份的第一天,然后减去一秒,得到当前月份的最后一天
-	endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-time.Second)
-
-	// 转换为时间戳
-	startTimestamp = startOfMonth.Unix()
-	endTimestamp = endOfMonth.Unix()
-
-	return startTimestamp, endTimestamp
-}
-
-// SubDays 计算两个日期(时间戳)之间的天数差
-func SubDays(startDay, endDay int64) int32 {
-	s1 := time.Unix(startDay, 0)
-	s2 := time.Unix(endDay, 0)
-	return int32(s2.Sub(s1).Hours() / 24)
-}
-
-func TimeParseLocal(layout, dateTime string) (time.Time, error) {
-	loc, _ := time.LoadLocation("Local")
-	return time.ParseInLocation(layout, dateTime, loc)
-}

+ 102 - 0
util/util_more.go

@@ -0,0 +1,102 @@
+package util
+
+import (
+	"bytes"
+	"fmt"
+	"math"
+	"strconv"
+	"strings"
+	"time"
+)
+
+func CurrentMaxFrameId() int32 {
+	currentHour := time.Now().Hour()
+	return int32(math.Floor(float64(currentHour/2))) * 10
+}
+
+func ArrayInt32ToStrings(cowIds []int32, cutset string) string {
+	var cows bytes.Buffer
+	if len(cowIds) <= 0 {
+		return cows.String()
+	}
+	for i, v := range cowIds {
+		if i > 0 {
+			cows.WriteString(cutset)
+		}
+		cows.WriteString(strconv.Itoa(int(v))) // 将整数转换为字符串
+	}
+	return cows.String()
+}
+
+// ConvertCowIdsToInt64Slice 将逗号拼接的字符串转换为 int64 切片
+func ConvertCowIdsToInt64Slice(input string) ([]int64, error) {
+	// 如果输入为空,直接返回空切片
+	if input == "" {
+		return []int64{}, nil
+	}
+
+	// 按逗号分割字符串
+	parts := strings.Split(input, ",")
+
+	// 初始化结果切片
+	result := make([]int64, 0, len(parts))
+
+	// 遍历每个部分,转换为 int64
+	for _, part := range parts {
+		// 去除空格
+		part = strings.TrimSpace(part)
+		if part == "" {
+			continue // 忽略空字符串
+		}
+
+		// 转换为 int64
+		num, err := strconv.ParseInt(part, 10, 64)
+		if err != nil {
+			return nil, fmt.Errorf("invalid number: %s", part)
+		}
+
+		// 添加到结果切片
+		result = append(result, num)
+	}
+	return result, nil
+}
+
+// GetMonthStartAndEndTimestamp 获取当前月份的开始时间戳和结束时间戳
+func GetMonthStartAndEndTimestamp() (startTimestamp, endTimestamp int64) {
+	// 获取当前时间
+	now := time.Now()
+
+	// 获取当前月份的第一天
+	startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
+
+	// 获取下一个月份的第一天,然后减去一秒,得到当前月份的最后一天
+	endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-time.Second)
+
+	// 转换为时间戳
+	startTimestamp = startOfMonth.Unix()
+	endTimestamp = endOfMonth.Unix()
+
+	return startTimestamp, endTimestamp
+}
+
+// SubDays 计算两个日期(时间戳)之间的天数差
+func SubDays(startDay, endDay int64) int32 {
+	s1 := time.Unix(startDay, 0)
+	s2 := time.Unix(endDay, 0)
+	return int32(s2.Sub(s1).Hours() / 24)
+}
+
+func TimeParseLocal(layout, dateTime string) (time.Time, error) {
+	loc, _ := time.LoadLocation("Local")
+	return time.ParseInLocation(layout, dateTime, loc)
+}
+
+func Substr(s string, start, length int) string {
+	// 转换为rune切片以正确处理Unicode字符
+	runes := []rune(s)
+	l := start + length
+	if l > len(runes) {
+		l = len(runes)
+	}
+	return string(runes[start:l])
+}

+ 15 - 0
util/util_test.go

@@ -2,6 +2,7 @@ package util
 
 import (
 	"fmt"
+	"strings"
 	"testing"
 	"time"
 
@@ -506,6 +507,20 @@ func TestGetNeckRingActiveTimer(t *testing.T) {
 	}
 }
 
+func TestSubstr(t *testing.T) {
+	nowTime := time.Now().Format(Layout)
+	text := `102053      0   0   53   12.2   4.7  101415 101538   0.4    0        0 0 ???????? 000000  0.0  0.0  0.0  0.0 0`
+	fmt.Println()
+
+	detachTime := fmt.Sprintf("%s %s:%s:%s", nowTime, Substr(text, 0, 2), Substr(text, 2, 2), Substr(text, 4, 2))
+	varCowCode := fmt.Sprintf("%s", Substr(text, 7, 6))
+	detacherAddress := fmt.Sprintf("%s", Substr(text, 20, 2))
+	milkWeight := fmt.Sprintf("%s", strings.TrimSpace(Substr(text, 25, 4)))
+	s1 := strings.TrimSpace(Substr(text, 0, 6))
+	s2 := strings.TrimSpace(Substr(text, 25, 4))
+	fmt.Println("detachTime", detachTime, varCowCode, detacherAddress, milkWeight, s1, s2)
+}
+
 func Test_demo(t *testing.T) {
 
 	var max time.Time