Ver código fonte

analysis: 流产率分析

Yi 5 meses atrás
pai
commit
9a9476c4db

+ 2 - 1
config/app.develop.yaml

@@ -40,4 +40,5 @@ cron:
   immunization_plan: "0 10 1 * * ?"
   same_time_plan: "0 15 1 * * ?"
   update_same_time: "0 20 1 * * ?"
-  system_basic_crontab: "0 25 1 * * ?"
+  system_basic_crontab: "0 25 1 * * ?"
+  cow_pregnant: "0 00 15 * * ?"

+ 7 - 6
config/app.go

@@ -48,12 +48,13 @@ type CronSetting struct {
 	// 是否启动任务时先跑一次
 	CrontabStartRun bool `yaml:"crontab_start_run"`
 	// CRONTAB 表达式
-	UpdateCowInfo      string `yaml:"update_cow_info"`
-	GenerateWorkOrder  string `yaml:"generate_work_order"`
-	ImmunizationPlan   string `yaml:"immunization_plan"`
-	SameTimePlan       string `yaml:"same_time_plan"`
-	UpdateSameTime     string `yaml:"update_same_time"`
-	SystemBasicCrontab string `yaml:"system_basic_crontab"`
+	UpdateCowInfo      string `yaml:"update_cow_info"`      //  更新牛只信息
+	GenerateWorkOrder  string `yaml:"generate_work_order"`  //  生成工作单
+	ImmunizationPlan   string `yaml:"immunization_plan"`    //  免疫计划
+	SameTimePlan       string `yaml:"same_time_plan"`       //  同期
+	UpdateSameTime     string `yaml:"update_same_time"`     //  更新同期
+	SystemBasicCrontab string `yaml:"system_basic_crontab"` //  系统基础定时任务
+	CowPregnant        string `yaml:"cow_pregnant"`         //  月度牛只怀孕清单
 }
 
 type JwtTokenKeyConfig struct {

+ 1 - 0
config/app.test.yaml

@@ -40,3 +40,4 @@ cron:
   same_time_plan: "0 15 1 * * ?"
   update_same_time: "0 20 1 * * ?"
   system_basic_crontab: "0 25 1 * * ?"
+  cow_pregnant: "0 00 15 * * ?"

+ 5 - 0
dep/di_crontab.go

@@ -71,5 +71,10 @@ func EntryCrontab(dependency CrontabDependency) *cron.Crontab {
 	if err != nil {
 		panic(err)
 	}
+
+	err = newCrontab.Bind("CowPregnant", cs.CowPregnant, dependency.CrontabHub.SystemBasicCrontab)
+	if err != nil {
+		panic(err)
+	}
 	return newCrontab
 }

+ 1 - 1
go.mod

@@ -3,7 +3,7 @@ module kpt-pasture
 go 1.17
 
 require (
-	gitee.com/xuyiping_admin/go_proto v0.0.0-20241017022959-48a2f1c177ac
+	gitee.com/xuyiping_admin/go_proto v0.0.0-20241017071700-cf080afaeec6
 	gitee.com/xuyiping_admin/pkg v0.0.0-20241010101255-0c6bd229b939
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/eko/gocache v1.1.0

+ 4 - 0
go.sum

@@ -86,6 +86,10 @@ gitee.com/xuyiping_admin/go_proto v0.0.0-20241017014945-924b8364d224 h1:l4WMh/85
 gitee.com/xuyiping_admin/go_proto v0.0.0-20241017014945-924b8364d224/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
 gitee.com/xuyiping_admin/go_proto v0.0.0-20241017022959-48a2f1c177ac h1:P8dH2WtcZ3TnQdQHcKooG3IlDXH0XHPi33WPVB0xmKA=
 gitee.com/xuyiping_admin/go_proto v0.0.0-20241017022959-48a2f1c177ac/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20241017053229-f6f500c7072c h1:etsV1/NXiWcXpgBr9juOLyNSfyxOsvi+e3/4E/B3/QE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20241017053229-f6f500c7072c/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20241017071700-cf080afaeec6 h1:ahpnQMqvJDQux/3MTFH2lVgJHyvQwdFVYko9oWc46vA=
+gitee.com/xuyiping_admin/go_proto v0.0.0-20241017071700-cf080afaeec6/go.mod h1:BKrFW6YLDectlQcQk3FYKBeXvjEiodAKJ5rq7O/QiPE=
 gitee.com/xuyiping_admin/pkg v0.0.0-20231218082641-aac597b8a015 h1:dfb5dRd57L2HKjdwLT93UFmPYFPOmEl56gtZmqcNnaE=
 gitee.com/xuyiping_admin/pkg v0.0.0-20231218082641-aac597b8a015/go.mod h1:Fk4GYI/v0IK3XFrm1Gn+VkgCz5Y7mfswD5hsTJYOG6A=
 gitee.com/xuyiping_admin/pkg v0.0.0-20241008063555-121a776fd331 h1:qJcpJ3o+O7uxDqR0I9LijQmi607IKvhf4mGum/ZUPT0=

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

@@ -96,3 +96,27 @@ func PenWeight(c *gin.Context) {
 	}
 	ginutil.JSONResp(c, res)
 }
+
+func AbortionRate(c *gin.Context) {
+	var req pasturePb.AbortionRateRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.StartDayAt, valid.Required),
+		valid.Field(&req.EndDayAt, valid.Required),
+		valid.Field(&req.CowType, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.AbortionRate(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}

+ 1 - 0
http/route/analysis_api.go

@@ -17,5 +17,6 @@ func AnalysisAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		pastureRoute.POST("/weight/range", analysis.WeightRange)
 		pastureRoute.POST("/mating/timely", analysis.MatingTimeLy)
 		pastureRoute.POST("/pen/weight", analysis.PenWeight)
+		pastureRoute.POST("/abortion/rate", analysis.AbortionRate)
 	}
 }

+ 61 - 59
model/cow.go

@@ -10,47 +10,47 @@ import (
 )
 
 type Cow struct {
-	Id                  int64                       `json:"id"`
-	Sex                 pasturePb.Genders_Kind      `json:"sex"`
-	NeckRingNumber      string                      `json:"neckRingNumber"`
-	EarNumber           string                      `json:"earNumber"`
-	EarOldNumber        string                      `json:"earOldNumber"`
-	PenId               int32                       `json:"penId"`
-	Lact                int32                       `json:"lact"`
-	DayAge              int32                       `json:"dayAge"`
-	CalvingAge          int64                       `json:"calvingAge"`
-	PregnancyAge        int64                       `json:"pregnancyAge"` // 怀孕天数 孕检结果有阳性更新,产犊后至0
-	AdmissionAge        int64                       `json:"admissionAge"`
-	AbortionAge         int64                       `json:"abortionAge"` // 流产天数
-	CowType             pasturePb.CowType_Kind      `json:"cowType"`
-	BreedStatus         pasturePb.BreedStatus_Kind  `json:"breedStatus"`
-	CowKind             pasturePb.CowKind_Kind      `json:"cowKind"`
-	BirthWeight         int64                       `json:"birthWeight"`
-	CurrentWeight       int64                       `json:"currentWeight"`
-	AdmissionWeight     int64                       `json:"admissionWeight"`
-	SourceId            pasturePb.CowSource_Kind    `json:"sourceId"`
-	FatherNumber        string                      `json:"fatherNumber"`
-	MotherNumber        string                      `json:"motherNumber"`
-	IsRemove            pasturePb.IsShow_Kind       `json:"isRemove"`
-	IsPregnant          pasturePb.IsShow_Kind       `json:"isPregnant"`
-	HealthStatus        pasturePb.HealthStatus_Kind `json:"healthStatus"`
-	WeaningAt           int64                       `json:"weaningAt"`
-	CalvingAt           int64                       `json:"calvingAt"`
-	BirthAt             int64                       `json:"birthAti"`
-	AdmissionAt         int64                       `json:"admissionAt"`
-	FirstMatingAt       int64                       `json:"firstMatingAt"`
-	LastEstrusAt        int64                       `json:"lastEstrusAt"`
-	LastCalvingAt       int64                       `json:"lastCalvingAt"`
-	LastMatingAt        int64                       `json:"lastMatingAt"`
-	LastBullNumber      string                      `json:"lastBullNumber"`
-	LastPregnantCheckAt int64                       `json:"lastPregnantCheckAt"`
-	LastDryMilkAt       int64                       `json:"lastDryMilkAt"`
-	LastSecondWeight    int64                       `json:"lastSecondWeight"`
-	LastSecondWeightAt  int64                       `json:"lastSecondWeightAt"`
-	LastAbortionAt      int64                       `json:"lastAbortionAt"`
-	LastWeightAt        int64                       `json:"lastWeightAt"`
-	CreatedAt           int64                       `json:"createdAt"`
-	UpdatedAt           int64                       `json:"updatedAt"`
+	Id                  int64                          `json:"id"`
+	Sex                 pasturePb.Genders_Kind         `json:"sex"`
+	NeckRingNumber      string                         `json:"neckRingNumber"`
+	EarNumber           string                         `json:"earNumber"`
+	EarOldNumber        string                         `json:"earOldNumber"`
+	PenId               int32                          `json:"penId"`
+	Lact                int32                          `json:"lact"`
+	DayAge              int32                          `json:"dayAge"`
+	CalvingAge          int64                          `json:"calvingAge"`
+	PregnancyAge        int32                          `json:"pregnancyAge"` // 怀孕天数 孕检结果有阳性更新,产犊后至0
+	AdmissionAge        int32                          `json:"admissionAge"`
+	AbortionAge         int64                          `json:"abortionAge"` // 流产天数
+	CowType             pasturePb.CowType_Kind         `json:"cowType"`
+	BreedStatus         pasturePb.BreedStatus_Kind     `json:"breedStatus"`
+	CowKind             pasturePb.CowKind_Kind         `json:"cowKind"`
+	BirthWeight         int64                          `json:"birthWeight"`
+	CurrentWeight       int64                          `json:"currentWeight"`
+	AdmissionWeight     int64                          `json:"admissionWeight"`
+	SourceId            pasturePb.CowSource_Kind       `json:"sourceId"`
+	FatherNumber        string                         `json:"fatherNumber"`
+	MotherNumber        string                         `json:"motherNumber"`
+	AdmissionStatus     pasturePb.AdmissionStatus_Kind `json:"admissionStatus"`
+	IsPregnant          pasturePb.IsShow_Kind          `json:"isPregnant"`
+	HealthStatus        pasturePb.HealthStatus_Kind    `json:"healthStatus"`
+	WeaningAt           int64                          `json:"weaningAt"`
+	CalvingAt           int64                          `json:"calvingAt"`
+	BirthAt             int64                          `json:"birthAti"`
+	AdmissionAt         int64                          `json:"admissionAt"`
+	FirstMatingAt       int64                          `json:"firstMatingAt"`
+	LastEstrusAt        int64                          `json:"lastEstrusAt"`
+	LastCalvingAt       int64                          `json:"lastCalvingAt"`
+	LastMatingAt        int64                          `json:"lastMatingAt"`
+	LastBullNumber      string                         `json:"lastBullNumber"`
+	LastPregnantCheckAt int64                          `json:"lastPregnantCheckAt"`
+	LastDryMilkAt       int64                          `json:"lastDryMilkAt"`
+	LastSecondWeight    int64                          `json:"lastSecondWeight"`
+	LastSecondWeightAt  int64                          `json:"lastSecondWeightAt"`
+	LastAbortionAt      int64                          `json:"lastAbortionAt"`
+	LastWeightAt        int64                          `json:"lastWeightAt"`
+	CreatedAt           int64                          `json:"createdAt"`
+	UpdatedAt           int64                          `json:"updatedAt"`
 }
 
 func (c *Cow) TableName() string {
@@ -153,7 +153,7 @@ func NewCow(req *pasturePb.EventEnterRequest) *Cow {
 		SourceId:            req.CowSource,
 		FatherNumber:        req.FatherNumber,
 		MotherNumber:        req.MotherNumber,
-		IsRemove:            pasturePb.IsShow_Ok,
+		AdmissionStatus:     pasturePb.AdmissionStatus_Admission,
 		HealthStatus:        pasturePb.HealthStatus_Health,
 		IsPregnant:          isPregnant,
 		WeaningAt:           int64(req.WeaningAt),
@@ -167,19 +167,19 @@ func NewCow(req *pasturePb.EventEnterRequest) *Cow {
 
 func NewCalfCow(motherId int64, fatherNumber string, calf *CalvingCalf) *Cow {
 	return &Cow{
-		Sex:          calf.Sex,
-		EarNumber:    calf.EarNumber,
-		PenId:        calf.PenId,
-		CowType:      pasturePb.CowType_Lactating_Calf, // 哺乳犊牛
-		BreedStatus:  pasturePb.BreedStatus_UnBreed,    // 未配
-		CowKind:      calf.CowKind,                     // 牛只品种
-		BirthWeight:  calf.BirthWeight,
-		BirthAt:      calf.BirthAt,
-		SourceId:     pasturePb.CowSource_Calving, // 产犊方式
-		FatherNumber: fatherNumber,
-		MotherNumber: fmt.Sprintf("%d", motherId),
-		IsRemove:     pasturePb.IsShow_Ok,
-		IsPregnant:   pasturePb.IsShow_No,
+		Sex:             calf.Sex,
+		EarNumber:       calf.EarNumber,
+		PenId:           calf.PenId,
+		CowType:         pasturePb.CowType_Lactating_Calf, // 哺乳犊牛
+		BreedStatus:     pasturePb.BreedStatus_UnBreed,    // 未配
+		CowKind:         calf.CowKind,                     // 牛只品种
+		BirthWeight:     calf.BirthWeight,
+		BirthAt:         calf.BirthAt,
+		SourceId:        pasturePb.CowSource_Calving, // 产犊方式
+		FatherNumber:    fatherNumber,
+		MotherNumber:    fmt.Sprintf("%d", motherId),
+		AdmissionStatus: pasturePb.AdmissionStatus_Admission,
+		IsPregnant:      pasturePb.IsShow_No,
 	}
 }
 
@@ -218,7 +218,9 @@ func (c *Cow) GetCalvingAge() int64 {
 
 // GetDaysPregnant 怀孕天数
 func (c *Cow) GetDaysPregnant() int32 {
-	if c.BreedStatus == pasturePb.BreedStatus_Pregnant && c.IsRemove == pasturePb.IsShow_No && c.IsPregnant == pasturePb.IsShow_Ok {
+	if c.BreedStatus == pasturePb.BreedStatus_Pregnant &&
+		c.AdmissionStatus == pasturePb.AdmissionStatus_Admission &&
+		c.IsPregnant == pasturePb.IsShow_Ok {
 		return int32(math.Floor(float64(time.Now().Unix()-c.LastMatingAt) / 86400))
 	}
 	return 0
@@ -226,7 +228,7 @@ func (c *Cow) GetDaysPregnant() int32 {
 
 // GetLactationDays 泌乳天数
 func (c *Cow) GetLactationDays() int32 {
-	if c.BreedStatus == pasturePb.BreedStatus_Calving && c.IsRemove == pasturePb.IsShow_Ok {
+	if c.BreedStatus == pasturePb.BreedStatus_Calving && c.AdmissionStatus == pasturePb.AdmissionStatus_Admission {
 		return int32(math.Floor(float64(time.Now().Unix()-c.LastCalvingAt) / 86400))
 	}
 	return 0
@@ -234,7 +236,7 @@ func (c *Cow) GetLactationDays() int32 {
 
 // GetAdmissionAge 入场天数
 func (c *Cow) GetAdmissionAge() int32 {
-	if c.AdmissionAt > 0 && c.IsRemove == pasturePb.IsShow_Ok {
+	if c.AdmissionAt > 0 && c.AdmissionStatus == pasturePb.AdmissionStatus_Admission {
 		return int32(math.Floor(float64(time.Now().Unix()-c.AdmissionAt) / 86400))
 	}
 	return 0
@@ -267,7 +269,7 @@ func (c *Cow) GetAverageDailyWeight() float64 {
 }
 
 func (c *Cow) GetAbortionAge() int32 {
-	if c.LastAbortionAt > 0 && c.IsRemove == pasturePb.IsShow_Ok {
+	if c.LastAbortionAt > 0 && c.AdmissionStatus == pasturePb.AdmissionStatus_Admission {
 		return int32(math.Floor(float64(time.Now().Unix()-c.LastAbortionAt) / 86400))
 	}
 	return 0

+ 45 - 0
model/cow_pregnant.go

@@ -0,0 +1,45 @@
+package model
+
+import pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+
+type CowPregnant struct {
+	Id           int64                  `json:"id"`
+	CowId        int64                  `json:"cowId"`
+	Lact         int32                  `json:"lact"`
+	DayAge       int32                  `json:"dayAge"`
+	PenId        int32                  `json:"penId"`
+	AdmissionAge int32                  `json:"admissionAge"`
+	CowType      pasturePb.CowType_Kind `json:"cowType"`
+	PregnancyAge int32                  `json:"pregnancyAge"`
+	CreatedAt    int64                  `json:"createdAt"`
+	UpdatedAt    int64                  `json:"updatedAt"`
+}
+
+func (c *CowPregnant) TableName() string {
+	return "cow_pregnant"
+}
+
+func NewCowPregnant(cow *Cow) *CowPregnant {
+	return &CowPregnant{
+		CowId:        cow.Id,
+		Lact:         cow.Lact,
+		DayAge:       cow.DayAge,
+		PenId:        cow.PenId,
+		AdmissionAge: cow.AdmissionAge,
+		CowType:      cow.CowType,
+		PregnancyAge: cow.PregnancyAge,
+	}
+}
+
+func NewCowPregnantList(cow []*Cow) []*CowPregnant {
+	res := make([]*CowPregnant, len(cow))
+	for i, v := range cow {
+		res[i] = NewCowPregnant(v)
+	}
+	return res
+}
+
+type CowPregnantMonth struct {
+	Month    string `json:"month"`
+	CowCount int32  `json:"cowCount"`
+}

+ 1 - 0
model/event_mating.go

@@ -20,6 +20,7 @@ type EventMating struct {
 	Status            pasturePb.IsShow_Kind           `json:"status"`
 	MatingNumber      int64                           `json:"matingNumber"`
 	MatingResult      pasturePb.MatingResult_Kind     `json:"matingResult"`
+	MatingResultAt    int64                           `json:"matingResultAt"`
 	ExposeEstrusType  pasturePb.ExposeEstrusType_Kind `json:"exposeEstrusType"`
 	FrozenSemenNumber string                          `json:"frozenSemenNumber"`
 	FrozenSemenCount  int32                           `json:"frozenSemenCount"`

+ 1 - 0
model/system_role.go

@@ -24,6 +24,7 @@ const (
 	LayoutTime  = "2006-01-02 15:04:05"
 	LayoutDate  = "20060102"
 	LayoutDate2 = "2006-01-02"
+	LayoutMonth = "2006-01"
 )
 
 type SystemRoleSlice []*SystemRole

+ 104 - 7
module/backend/analysis.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"kpt-pasture/model"
+	"kpt-pasture/util"
 	"net/http"
 	"strings"
 	"time"
@@ -17,7 +18,7 @@ import (
 func (s *StoreEntry) GrowthCurve(ctx context.Context, req *pasturePb.SearchGrowthCurvesRequest) (*pasturePb.GrowthCurvesResponse, error) {
 	// 查询数据
 	cowList := make([]*model.Cow, 0)
-	pref := s.DB.Model(new(model.Cow)).Where("is_remove = ?", pasturePb.IsShow_Ok)
+	pref := s.DB.Model(new(model.Cow)).Where("admission_status = ?", pasturePb.AdmissionStatus_Admission)
 	if req.GetCowId() != "" {
 		pref.Where("id IN ?", strings.Split(req.CowId, ","))
 	}
@@ -85,7 +86,7 @@ func (s *StoreEntry) GrowthCurve(ctx context.Context, req *pasturePb.SearchGrowt
 func (s *StoreEntry) WeightRange(ctx context.Context, req *pasturePb.WeightRangeRequest) (*pasturePb.WeightRangeResponse, error) {
 	cowWeightRange := make([]*model.CowWeightRange, 0)
 
-	prefix := s.DB.Model(new(model.Cow)).Where("is_remove = ?", pasturePb.IsShow_Ok)
+	prefix := s.DB.Model(new(model.Cow)).Where("admission_status = ?", pasturePb.AdmissionStatus_Admission)
 	if req.CowKind > 0 {
 		prefix.Where("cow_kind = ?", req.CowKind)
 	}
@@ -133,7 +134,7 @@ func (s *StoreEntry) WeightRange(ctx context.Context, req *pasturePb.WeightRange
 		data = append(data, v.Count)
 	}
 	// 牛只详情列表
-	pref := s.DB.Model(new(model.Cow)).Where("is_remove = ?", pasturePb.IsShow_Ok)
+	pref := s.DB.Model(new(model.Cow)).Where("admission_status = ?", pasturePb.AdmissionStatus_Admission)
 	if req.CowKind > 0 {
 		pref.Where("cow_kind = ?", req.CowKind)
 	}
@@ -256,8 +257,8 @@ func (s *StoreEntry) PenWeight(ctx context.Context, req *pasturePb.PenWeightRequ
 			CEILING(AVG(current_weight) / 1000 ) AS avg_weight,
 			CEILING(SUM(current_weight) / 1000 ) AS all_weight,
 			COUNT(*) AS cow_count`,
-		).Where("is_remove = ?", pasturePb.IsShow_Ok)
-	if len(req.PenId) > 0 {
+		).Where("admission_status = ?", pasturePb.AdmissionStatus_Admission)
+	if len(req.PenId) > 0 && req.BarId <= 0 {
 		pref.Where("pen_id IN ?", req.PenId)
 	}
 	if err := pref.Group("pen_id").
@@ -285,9 +286,11 @@ func (s *StoreEntry) PenWeight(ctx context.Context, req *pasturePb.PenWeightRequ
 	cowList := make([]*model.Cow, 0)
 	var count int64 = 0
 	prefList := s.DB.Model(new(model.Cow)).
-		Where("is_remove = ?", pasturePb.IsShow_Ok)
+		Where("admission_status = ?", pasturePb.AdmissionStatus_Admission)
 
-	if len(req.PenId) > 0 {
+	if len(req.PenId) <= 0 && req.BarId > 0 {
+		prefList.Where("pen_id IN ?", []int32{req.BarId})
+	} else {
 		prefList.Where("pen_id IN ?", req.PenId)
 	}
 
@@ -310,3 +313,97 @@ func (s *StoreEntry) PenWeight(ctx context.Context, req *pasturePb.PenWeightRequ
 		},
 	}, nil
 }
+
+func (s *StoreEntry) AbortionRate(ctx context.Context, req *pasturePb.AbortionRateRequest) (*pasturePb.AbortionRateResponse, error) {
+	if req.StartDayAt <= 0 || req.EndDayAt <= 0 || req.StartDayAt > req.EndDayAt || req.CowType <= 0 {
+		return nil, xerr.Customf("参数错误")
+	}
+
+	startDay, err := util.GetLastDayOfMonth(time.Unix(int64(req.StartDayAt), 0).Format(model.LayoutMonth))
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	endDay, err := util.GetLastDayOfMonth(time.Unix(int64(req.EndDayAt), 0).Format(model.LayoutMonth))
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	dayTimeList, err := util.GetMonthsInRange(startDay, endDay)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	lastDayForMonth := make([]string, 0)
+	for _, v := range dayTimeList {
+		lastDayTime, _ := util.GetLastDayOfMonth(v)
+		lastDayForMonth = append(lastDayForMonth, lastDayTime)
+	}
+	// 历史每月怀孕牛头数量
+	cowPregnantMonthList := make([]*model.CowPregnantMonth, 0)
+	if err = s.DB.Model(new(model.Cow)).
+		Select(`
+			COUNT(cow_id) AS cow_count,
+			DATE_FORMAT(FROM_UNIXTIME(updated_at),'%Y-%m-%d') as month`,
+		).Where("status = ?", pasturePb.IsShow_Ok).
+		Where("cow_type = ?", req.CowType).
+		Where("DATE_FORMAT(FROM_UNIXTIME(`created_at`),'%Y-%m-%d') IN = ?", lastDayForMonth).
+		Group("month").Find(&cowPregnantMonthList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	// 历史每月流产牛头数量
+	cowAbortionMonthList := make([]*model.CowPregnantMonth, 0)
+	if err = s.DB.Model(new(model.EventAbortion)).
+		Select(`
+			COUNT(cow_id) AS cow_count,
+			DATE_FORMAT(FROM_UNIXTIME(abortion_at),'%Y-%m') as month`,
+		).Where("status = ?", pasturePb.IsShow_Ok).
+		Where("cow_type = ?", req.CowType).
+		Where("DATE_FORMAT(FROM_UNIXTIME(`created_at`),'%Y-%m') IN = ?", dayTimeList).
+		Group("month").Find(&cowAbortionMonthList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	chart := &pasturePb.AbortionRateChart{
+		Header:             make([]string, 0),
+		AbortionCountMonth: make([]int32, 0),
+		PregnantCountMonth: make([]int32, 0),
+		AbortionRateMonth:  make([]float32, 0),
+	}
+
+	table := make([]*pasturePb.AbortionRateTable, 0)
+	for _, v2 := range cowAbortionMonthList {
+		pregnantCountMonth := int32(0)
+		for _, v := range cowPregnantMonthList {
+			if v.Month == v2.Month {
+				v.CowCount = v2.CowCount
+				pregnantCountMonth = v.CowCount
+			}
+		}
+
+		abortionRateMonth := float64(0)
+		if pregnantCountMonth > 0 && v2.CowCount > 0 {
+			abortionRateMonth = util.Ceil(float64(pregnantCountMonth) / float64(v2.CowCount) * 100)
+		}
+
+		chart.Header = append(chart.Header, v2.Month)
+		chart.AbortionCountMonth = append(chart.AbortionCountMonth, v2.CowCount)
+		chart.PregnantCountMonth = append(chart.PregnantCountMonth, pregnantCountMonth)
+		chart.AbortionRateMonth = append(chart.AbortionRateMonth, float32(abortionRateMonth))
+
+		table = append(table, &pasturePb.AbortionRateTable{
+			AbortionCount: v2.CowCount,
+			MonthName:     v2.Month,
+			PregnantCount: pregnantCountMonth,
+			AbortionRate:  float32(abortionRateMonth),
+		})
+	}
+
+	return &pasturePb.AbortionRateResponse{
+		Code:    http.StatusOK,
+		Message: "ok",
+		Data: &pasturePb.AbortionRateData{
+			Chart: chart,
+			Table: table,
+		},
+	}, nil
+}

+ 3 - 3
module/backend/calendar.go

@@ -175,7 +175,7 @@ func (s *StoreEntry) SameTimeCowList(ctx context.Context, dateTime string, pagin
 	pref := s.DB.Table(fmt.Sprintf("%s as a", new(model.EventCowSameTime).TableName())).
 		Select("a.id,a.cow_id,a.status,b.breed_status,b.cow_type,b.pen_id,b.day_age,b.calving_age,b.abortion_age").
 		Joins("left join cow as b on a.cow_id = b.id").
-		Where("b.is_remove = ?", pasturePb.IsShow_Ok).
+		Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
 		Where("a.plan_day <= ?", dateTime).
 		Where("a.status = ?", pasturePb.IsShow_No)
 
@@ -266,7 +266,7 @@ func (s *StoreEntry) WeaningCowList(ctx context.Context, dateTime string, pagina
 	pref := s.DB.Table(fmt.Sprintf("%s as a", new(model.EventCowSameTime).TableName())).
 		Select("a.*,b.day_age").
 		Joins("left join cow as b on a.cow_id = b.id").
-		Where("b.is_remove = ?", pasturePb.IsShow_No).
+		Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
 		Where("a.plan_day <= ?", dateTime).
 		Where("a.status = ?", pasturePb.IsShow_No)
 
@@ -307,7 +307,7 @@ func (s *StoreEntry) MatingCowList(ctx context.Context, dateTime string, paginat
 	pref := s.DB.Table(fmt.Sprintf("%s as a", new(model.EventCowSameTime).TableName())).
 		Select("a.id,a.cow_id,a.status,b.breed_status,b.cow_type,b.pen_id,b.day_age,b.calving_age,b.abortion_age").
 		Joins("left join cow as b on a.cow_id = b.id").
-		Where("b.is_remove = ?", pasturePb.IsShow_No).
+		Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission).
 		Where("a.plan_day <= ?", dateTime).
 		Where("a.status = ?", pasturePb.IsShow_No)
 

+ 1 - 1
module/backend/cow.go

@@ -14,7 +14,7 @@ import (
 func (s *StoreEntry) CowList(ctx context.Context, req *pasturePb.SearchEventRequest, pagination *pasturePb.PaginationModel) (*pasturePb.SearchCowListResponse, error) {
 	cowList := make([]*model.Cow, 0)
 	var count int64 = 0
-	pref := s.DB.Model(&model.Cow{}).Where("is_remove = ?", pasturePb.IsShow_Ok)
+	pref := s.DB.Model(&model.Cow{}).Where("admission_status = ?", pasturePb.AdmissionStatus_Admission)
 
 	if len(req.CowId) > 0 {
 		cowIds := strings.Split(req.CowId, ",")

+ 1 - 1
module/backend/dashboard.go

@@ -14,7 +14,7 @@ func (s *StoreEntry) Bar(ctx context.Context) (*pasturePb.BarCowStructResponse,
 	barCowStructList := make([]*model.BarCowStruct, 0)
 	var count int32 = 0
 	if err := s.DB.Model(new(model.Cow)).Select("COUNT(*) AS number ,cow_type").
-		Where("is_remove = ?", pasturePb.IsShow_Ok).
+		Where("admission_status = ?", pasturePb.AdmissionStatus_Admission).
 		Group("cow_type").
 		Find(&barCowStructList).Error; err != nil {
 		return nil, xerr.WithStack(err)

+ 14 - 4
module/backend/event_breed.go

@@ -233,7 +233,10 @@ func (s *StoreEntry) PregnantCheckCreate(ctx context.Context, req *pasturePb.Eve
 
 			if err = tx.Model(&model.EventMating{}).Where("cow_id IN ?", updatePregnantCowIds).
 				Group("cow_id").Select("MAX(id) as id").
-				Updates(map[string]interface{}{"mating_result": pasturePb.MatingResult_Pregnant}).Error; err != nil {
+				Updates(map[string]interface{}{
+					"mating_result":    pasturePb.MatingResult_Pregnant,
+					"mating_result_at": req.PregnantCheckAt,
+				}).Error; err != nil {
 				return xerr.WithStack(err)
 			}
 		}
@@ -250,7 +253,10 @@ func (s *StoreEntry) PregnantCheckCreate(ctx context.Context, req *pasturePb.Eve
 
 			if err = tx.Model(&model.EventMating{}).Where("cow_id IN ?", updateAbortCowIds).
 				Group("cow_id").Select("MAX(id) as id").
-				Updates(map[string]interface{}{"mating_result": pasturePb.MatingResult_Abort}).Error; err != nil {
+				Updates(map[string]interface{}{
+					"mating_result":    pasturePb.MatingResult_Abort,
+					"mating_result_at": req.PregnantCheckAt,
+				}).Error; err != nil {
 				return xerr.WithStack(err)
 			}
 		}
@@ -267,7 +273,10 @@ func (s *StoreEntry) PregnantCheckCreate(ctx context.Context, req *pasturePb.Eve
 
 			if err = tx.Model(&model.EventMating{}).Where("cow_id IN ?", updateEmptyCowIds).
 				Group("cow_id").Select("MAX(id) as id").
-				Updates(map[string]interface{}{"mating_result": pasturePb.MatingResult_Empty}).Error; err != nil {
+				Updates(map[string]interface{}{
+					"mating_result":    pasturePb.MatingResult_Empty,
+					"mating_result_at": req.PregnantCheckAt,
+				}).Error; err != nil {
 				return xerr.WithStack(err)
 			}
 		}
@@ -640,7 +649,8 @@ func (s *StoreEntry) AbortionCreate(ctx context.Context, req *pasturePb.EventAbo
 		// 更新最近一次配种结果为流产
 		if err = tx.Model(new(model.EventMating)).Where("id = ?", lastCowMating.Id).
 			Updates(map[string]interface{}{
-				"mating_result": pasturePb.MatingResult_Abort,
+				"mating_result":    pasturePb.MatingResult_Abort,
+				"mating_result_at": req.AbortionAt,
 			}).Error; err != nil {
 		}
 		return nil

+ 1 - 0
module/backend/interface.go

@@ -209,6 +209,7 @@ type AnalyseService interface {
 	WeightRange(ctx context.Context, req *pasturePb.WeightRangeRequest) (*pasturePb.WeightRangeResponse, error)
 	MatingTimely(ctx context.Context, req *pasturePb.MatingTimelyRequest) (*model.MatingTimelyResponse, error)
 	PenWeight(ctx context.Context, req *pasturePb.PenWeightRequest, pagination *pasturePb.PaginationModel) (*pasturePb.PenWeightResponse, error)
+	AbortionRate(ctx context.Context, req *pasturePb.AbortionRateRequest) (*pasturePb.AbortionRateResponse, error)
 }
 
 //go:generate mockgen -destination mock/DashboardService.go -package kptservicemock kpt-pasture/module/backend DashboardService

+ 3 - 3
module/backend/sql.go

@@ -98,7 +98,7 @@ func (s *StoreEntry) PenMap(ctx context.Context) map[int32]*model.Pen {
 
 func (s *StoreEntry) GetCowList(ctx context.Context) ([]*model.Cow, error) {
 	cowList := make([]*model.Cow, 0)
-	if err := s.DB.Where("is_remove = ?", pasturePb.IsShow_Ok).Find(&cowList).Error; err != nil {
+	if err := s.DB.Where("admission_status = ?", pasturePb.AdmissionStatus_Admission).Find(&cowList).Error; err != nil {
 		return nil, xerr.WithStack(err)
 	}
 	return cowList, nil
@@ -106,7 +106,7 @@ func (s *StoreEntry) GetCowList(ctx context.Context) ([]*model.Cow, error) {
 
 func (s *StoreEntry) GetCowInfoByCowId(ctx context.Context, cowId int64) (*model.Cow, error) {
 	cowData := &model.Cow{Id: cowId}
-	if err := s.DB.Where("is_remove = ?", pasturePb.IsShow_Ok).First(cowData).Error; err != nil {
+	if err := s.DB.Where("admission_status = ?", pasturePb.AdmissionStatus_Admission).First(cowData).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, xerr.Customf("该牛只数据不存在: %d", cowId)
 		}
@@ -118,7 +118,7 @@ func (s *StoreEntry) GetCowInfoByCowId(ctx context.Context, cowId int64) (*model
 func (s *StoreEntry) GetCowInfoByCowIds(ctx context.Context, cowIds []int64) ([]*model.Cow, error) {
 	cowList := make([]*model.Cow, 0)
 	if err := s.DB.Model(&model.Cow{}).
-		Where("is_remove = ?", pasturePb.IsShow_Ok).
+		Where("admission_status = ?", pasturePb.AdmissionStatus_Admission).
 		Where("id IN ?", cowIds).
 		Find(&cowList).Error; err != nil {
 		return nil, xerr.WithStack(err)

+ 3 - 3
module/crontab/cow_cron.go

@@ -58,7 +58,7 @@ func (e *Entry) GenerateAsynqWorkOrder() error {
 // UpdateCowInfo 牛只基本信息维护
 func (e *Entry) UpdateCowInfo() error {
 	cowList := make([]*model.Cow, 0)
-	if err := e.DB.Where("is_remove = ?", pasturePb.IsShow_Ok).Find(&cowList).Error; err != nil {
+	if err := e.DB.Where("admission_status = ?", pasturePb.AdmissionStatus_Admission).Find(&cowList).Error; err != nil {
 		return err
 	}
 	if ok := e.IsExistCrontabLog(UpdateCowInfo); ok {
@@ -195,7 +195,7 @@ func (e *Entry) SameTimePlan() error {
 		}
 
 		cowList := make([]*model.Cow, 0)
-		pref := e.DB.Where("is_remove = ?", pasturePb.IsShow_Ok)
+		pref := e.DB.Where("admission_status = ?", pasturePb.AdmissionStatus_Admission)
 
 		if sameTime.CowType == pasturePb.SameTimeCowType_Breeding_Calf {
 			pref.Where("calving_age >= ?", sameTime.PostpartumDaysStart).
@@ -299,7 +299,7 @@ func (e *Entry) SystemBasicCrontab() error {
 		}
 
 		cowList := make([]*model.Cow, 0)
-		pref := e.DB.Model(new(model.Cow)).Where("is_remove = ? ", pasturePb.IsShow_Ok)
+		pref := e.DB.Model(new(model.Cow)).Where("admission_status = ? ", pasturePb.AdmissionStatus_Admission)
 
 		switch v.Name {
 		case model.PregnantCheckForFirst:

+ 1 - 0
module/crontab/interface.go

@@ -33,4 +33,5 @@ type Crontab interface {
 	SameTimePlan() error
 	UpdateSameTime() error
 	SystemBasicCrontab() error
+	CowPregnant() error
 }

+ 15 - 0
module/crontab/work_cron.go

@@ -141,3 +141,18 @@ func (e *Entry) GetTowardTaiCowSum() int64 {
 	}
 	return res
 }
+
+// CowPregnant 月度牛只怀孕清单
+func (e *Entry) CowPregnant() error {
+	cowList := make([]*model.Cow, 0)
+	if err := e.DB.Model(new(model.Cow)).
+		Where("admission_status = ?", pasturePb.AdmissionStatus_Admission).
+		Where("is_pregnant = ?", pasturePb.IsShow_Ok).Find(&cowList).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	cowPregnantList := model.NewCowPregnantList(cowList)
+	if err := e.DB.Create(cowPregnantList).Error; err != nil {
+		return xerr.WithStack(err)
+	}
+	return nil
+}

+ 48 - 0
util/util.go

@@ -10,6 +10,7 @@ const (
 	LocationName = "Asia/Shanghai"
 	LayoutTime   = "2006-01-02 15:04:05"
 	Layout       = "2006-01-02"
+	LayoutMonth  = "2006-01"
 )
 
 // TimeParseLocalUnix 获取当天零点的时间戳
@@ -72,3 +73,50 @@ func Ceil(x float64) float64 {
 	}
 	return intPart
 }
+
+// GetLastDayOfMonth
+// 接受一个字符串形式的月份(如 "2024-12"),
+// 并返回该月份的最后一天的日期。
+func GetLastDayOfMonth(month string) (string, error) {
+	// 解析传入的月份
+	layout := "2006-01"
+	t, err := time.Parse(layout, month)
+	if err != nil {
+		return "", err // 如果解析失败,返回错误
+	}
+
+	// 获取下个月的第一天
+	nextMonth := t.AddDate(0, 1, 0)
+
+	// 返回上个月的最后一天
+	lastDay := nextMonth.AddDate(0, 0, -1)
+	return lastDay.Format(Layout), nil
+}
+
+// GetMonthsInRange
+// 接受两个字符串形式的月份(如 "2024-01" 到 "2024-12"),
+// 并返回一个包含这两个月份之间所有月份的字符串切片。
+func GetMonthsInRange(startMonth, endMonth string) ([]string, error) {
+	// 解析起始月份
+	startLayout := "2006-01"
+	startTime, err := time.Parse(startLayout, startMonth)
+	if err != nil {
+		return nil, err
+	}
+
+	// 解析结束月份
+	endTime, err := time.Parse(startLayout, endMonth)
+	if err != nil {
+		return nil, err
+	}
+
+	// 初始化结果切片
+	var months []string
+
+	// 循环添加每个月份直到达到或超过结束月份
+	for curr := startTime; curr.Before(endTime) || curr.Equal(endTime); curr = curr.AddDate(0, 1, 0) {
+		months = append(months, curr.Format("2006-01"))
+	}
+
+	return months, nil
+}

+ 93 - 0
util/util_test.go

@@ -37,3 +37,96 @@ func TestUtil_ConvertParseLocalUnix(t *testing.T) {
 		})
 	}
 }
+
+func TestGetLastDayOfMonth(t *testing.T) {
+	tests := []struct {
+		month string
+		got   string
+	}{
+		{
+			month: "2022-02",
+			got:   "2022-02-28",
+		},
+		{
+			month: "2023-02",
+			got:   "2023-02-28",
+		},
+		{
+			month: "2024-02",
+			got:   "2024-02-29",
+		},
+		{
+			month: "2024-10",
+			got:   "2024-10-31",
+		}, {
+			month: "2024-09",
+			got:   "2024-09-30",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.month, func(t *testing.T) {
+			got, err := GetLastDayOfMonth(tt.month)
+			assert.Nil(t, err)
+			assert.Equal(t, tt.got, got)
+		})
+	}
+}
+
+func TestGetMonthsInRange(t *testing.T) {
+	tests := []struct {
+		startMonth string
+		endMonth   string
+		got        []string
+	}{
+		{
+			startMonth: "2023-01",
+			endMonth:   "2023-03",
+			got: []string{
+				"2023-01",
+				"2023-02",
+				"2023-03",
+			},
+		},
+		{
+			startMonth: "2023-01",
+			endMonth:   "2023-12",
+			got: []string{
+				"2023-01",
+				"2023-02",
+				"2023-03",
+				"2023-04",
+				"2023-05",
+				"2023-06",
+				"2023-07",
+				"2023-08",
+				"2023-09",
+				"2023-10",
+				"2023-11",
+				"2023-12",
+			},
+		},
+		{
+			startMonth: "2023-01",
+			endMonth:   "2023-01",
+			got: []string{
+				"2023-01",
+			},
+		},
+		{
+			startMonth: "2023-01",
+			endMonth:   "2023-01",
+			got: []string{
+				"2023-01",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.startMonth, func(t *testing.T) {
+			got, err := GetMonthsInRange(tt.startMonth, tt.endMonth)
+			assert.Nil(t, err)
+			assert.Equal(t, tt.got, got)
+		})
+	}
+}