瀏覽代碼

Merge branch 'feature/event' of xuyiping/kpt-pasture into develop

xuyiping 5 天之前
父節點
當前提交
606d34acca
共有 85 個文件被更改,包括 4270 次插入829 次删除
  1. 2 1
      Dockerfile
  2. 1 1
      Makefile
  3. 44 19
      README.md
  4. 1 1
      config/app.go
  5. 2 0
      dep/dep.go
  6. 3 3
      docker-compose.yml
  7. 22 20
      go.mod
  8. 1328 138
      go.sum
  9. 59 0
      http/handler/cow/cow.go
  10. 10 0
      http/handler/dashboard/dashboard.go
  11. 0 1
      http/handler/event/event_base.go
  12. 42 0
      http/handler/feeding/feeding.go
  13. 7 5
      http/middleware/auth.go
  14. 138 0
      http/middleware/i18n.go
  15. 2 0
      http/route/cow_api.go
  16. 1 0
      http/route/dashboard_api.go
  17. 19 0
      http/route/feeding_api.go
  18. 1 0
      http/route/route.go
  19. 2 2
      http/route/system_api.go
  20. 100 0
      locales/en.json
  21. 100 0
      locales/zh.json
  22. 7 1
      migrator/v0001_demo.sql
  23. 2 0
      model/app_pasture_list.go
  24. 38 0
      model/config_abortion_reasons.go
  25. 43 2
      model/cow.go
  26. 7 2
      model/data_waring.go
  27. 25 5
      model/event_cow_treatment.go
  28. 11 11
      model/event_enter.go
  29. 18 0
      model/event_sale.go
  30. 79 1
      model/event_sale_cow.go
  31. 2 0
      model/event_weight.go
  32. 1 0
      model/neck_active_habit.go
  33. 0 33
      model/neck_ring_bar_change.go
  34. 4 0
      model/neck_ring_bind_log.go
  35. 3 0
      model/neck_ring_health_warning.go
  36. 7 7
      model/neck_ring_original.go
  37. 30 0
      model/neck_ring_pen_change.go
  38. 1 1
      model/outbound.go
  39. 6 1
      model/outbound_detail.go
  40. 12 3
      model/system_user.go
  41. 1 1
      module/backend/analysis.go
  42. 78 41
      module/backend/analysis_breed.go
  43. 14 3
      module/backend/analysis_more.go
  44. 4 4
      module/backend/analysis_other.go
  45. 41 23
      module/backend/calendar.go
  46. 4 4
      module/backend/config_data.go
  47. 4 4
      module/backend/config_data_base.go
  48. 18 1
      module/backend/config_data_breed.go
  49. 59 16
      module/backend/cow.go
  50. 52 23
      module/backend/dashboard.go
  51. 43 0
      module/backend/dashboard_more.go
  52. 12 4
      module/backend/data_warning.go
  53. 1 1
      module/backend/enum_map.go
  54. 22 1
      module/backend/enum_options.go
  55. 44 17
      module/backend/event_base.go
  56. 65 57
      module/backend/event_base_more.go
  57. 42 7
      module/backend/event_breed.go
  58. 11 5
      module/backend/event_breed_more.go
  59. 13 5
      module/backend/event_breed_more_more.go
  60. 355 48
      module/backend/event_check.go
  61. 17 0
      module/backend/event_cow_log.go
  62. 99 43
      module/backend/event_health.go
  63. 26 25
      module/backend/event_health_more.go
  64. 75 0
      module/backend/feeding.go
  65. 124 28
      module/backend/goods.go
  66. 142 0
      module/backend/indicators.go
  67. 20 7
      module/backend/interface.go
  68. 63 16
      module/backend/neck_ring_warning.go
  69. 6 3
      module/backend/pasture.go
  70. 67 16
      module/backend/sql.go
  71. 100 25
      module/backend/system_service.go
  72. 1 1
      module/crontab/health_waning.go
  73. 1 0
      module/crontab/model.go
  74. 96 48
      module/crontab/neck_ring_calculate.go
  75. 51 55
      module/crontab/neck_ring_estrus.go
  76. 10 0
      module/crontab/neck_ring_health.go
  77. 65 9
      module/crontab/neck_ring_merge.go
  78. 71 0
      module/crontab/neck_ring_merge_test.go
  79. 5 2
      module/crontab/sql.go
  80. 131 0
      service/httpclient/http.go
  81. 20 0
      service/httpclient/interface.go
  82. 4 3
      service/wechat/http.go
  83. 7 0
      util/util.go
  84. 6 24
      util/util_test.go
  85. 0 1
      util/util_test1.go

+ 2 - 1
Dockerfile

@@ -25,9 +25,10 @@ RUN rm -f /etc/localtime \
 && echo "Asia/Shanghai" > /etc/timezone
 
 COPY --from=0 /app/kpt-pasture/config/ /app/kpt-pasture/config/
+COPY --from=0 /app/kpt-pasture/locales/ /app/kpt-pasture/locales/
 COPY --from=0  /app/kpt-pasture/kptPasture /app/kpt-pasture/kptPasture
 
 EXPOSE 8090
-VOLUME ["/app/kpt-pasture/logger","/app/kpt-pasture/config","/app/kpt-pasture/files"]
+VOLUME ["/app/kpt-pasture/logger","/app/kpt-pasture/config","/app/kpt-pasture/files","/app/kpt-pasture/locales"]
 
 CMD ["/app/kpt-pasture/kptPasture","http"]

+ 1 - 1
Makefile

@@ -18,4 +18,4 @@ lint:
 build:
 	rm -rf bin
 	mkdir -p bin
-	GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -o bin/kptTmrGroup -ldflags "-X kpt.kptyun.cn:3000/kpt-event/kpt-pasture/pod.appVersion=${version}" main.go
+	GOARCH=amd64 GOOS=windows CGO_ENABLED=0 go build -o bin/kptTmrGroup -ldflags "-X kpt.kptyun.cn:3000/kpt-event/kpt-pasture/pod.appVersion=${version}" main.go

+ 44 - 19
README.md

@@ -4,15 +4,14 @@ kpt-pasture- 科湃腾牧场管理系统
 
 ## Requirements
 
-- Go >= 1.17
+- Go >= 1.19
 - MySQL >= 5.7
-- Docker CE >= 19.03
-- Docker compose
+- Docker CE >= 26.1.3
+- Docker compose >= 2.27.0
 
 ## Develops
 
 本地开始开发前,请先阅读 README 和 Makefile,标准使用流程:
-
 需要设置的环境变量:
 
 - export APP_ENVIRONMENT=test
@@ -39,18 +38,44 @@ lint:
 5. app_pasture_list
 6. app_pasture_receiver
 
-
-todo列表:
-- [x] module/crontab/crontab.go 中119行[Limit(100)] 待优化,case为产后日期类型待测试
-- [x] 后台添加配种数据时候,不知道该牛只是同期还是自然发情还是人工揭发?
-- [x] 青年牛转后备牛事件(到达主动停配期主动转?)
-- [x] 后备牛到达主动停配期后的牛只放在哪个模块(配种清单,发情清单)
-- [x] 发情清单和配种清单更新机制
-- [ ] 前后端部署架构【k8s,docker-compose,docker-swarm】namespace隔离,需要考虑的问题【1.一次性任务,2. 定时任务 3. 数据收集,4. 日志收集 5. 报警介入】
-- [x] 所有事件录入梳理【批量录入,excel导入,信息人员与操作人员统一规范】
-- [x] 药品优化成药品名称关联生产商
-- [x] 框架logrus日志优化【未按照指定天数的日志自动删除,待验证】
-- [x] 犊牛的牛只品种是根据母牛的品种来确定,还是根据公牛来确定?【目前是根据母牛品种来确定】
-
-脖环发情算法梳理:
-- [x] 处理异常上报数据(frameid > 12)
+## 项目文档:
+- cmd -容器启动命令参数入口
+- config -配置文件入口
+- dep -容器依赖注入模块
+- files -静态文件入口
+- http -http 相关
+    - debug -调试相关
+    - handler -路由处理
+    - middleware -中间件
+    - router -路由配置
+    - util -工具类
+    - server.go -服务启动入口
+- locales -国际化
+- migrator -数据库迁移
+- model -数据库模型
+- module -业务模块
+  - asynq -异步任务
+  - backend -后台管理
+  - crontab -定时任务
+  - mqtt -mqtt服务
+- service -服务层
+  - alert -报警服务
+  - excel -excel服务
+  - asyncsvc -异步服务
+  - httpclient -http客户端
+  - milk - 奶台服务
+  - mqtt -mqtt服务
+  - redis -redis服务
+  - sso -文件存储服务
+  - wechat -微信小程序服务
+- store -数据存储层
+- temp -临时文件
+- test -测试文件
+- util -工具类
+- .drone.yml 持续集成文件
+- .gitignore 忽略文件
+- docker-compose.yml 容器配置文件
+- Dockerfile 容器配置文件
+- Makefile 编译文件
+- README.md 项目描述文件
+- main.go 项目入口文件

+ 1 - 1
config/app.go

@@ -241,7 +241,7 @@ func init() {
 		switch appEnv {
 		case "test":
 			err = Initialize("app.test.yaml", cfg)
-		case "development":
+		case "development", "develop":
 			err = Initialize("app.develop.yaml", cfg)
 		case "production":
 			err = Initialize("app.production.yaml", cfg)

+ 2 - 0
dep/dep.go

@@ -7,6 +7,7 @@ import (
 	"kpt-pasture/module/crontab"
 	moduleMqtt "kpt-pasture/module/mqtt"
 	"kpt-pasture/service/asynqsvc"
+	"kpt-pasture/service/httpclient"
 	"kpt-pasture/service/redis"
 	"kpt-pasture/service/sso"
 	"kpt-pasture/service/wechat"
@@ -40,5 +41,6 @@ func Options() []di.HubOption {
 		crontab.Module,
 		moduleMqtt.Module,
 		mqttstore.Module,
+		httpclient.Module,
 	}
 }

+ 3 - 3
docker-compose.yml

@@ -12,7 +12,7 @@ services:
     environment:
       - APP_ENVIRONMENT=production
       - PASTURE_WORK_DIR=/app/kpt-pasture
-    command: [ "/app/kpt-pasture/kptPasture","crontab" ]
+    command: [ "/app/kpt-pasture/kptPasture","crontab"]
   kpt-pasture-http:
     privileged: true
     container_name: xdmy001_kpt_pasture_http
@@ -28,7 +28,7 @@ services:
     environment:
       - APP_ENVIRONMENT=production
       - PASTURE_WORK_DIR=/app/kpt-pasture
-    command: [ "/app/kpt-pasture/kptPasture","http" ]
+    command: [ "/app/kpt-pasture/kptPasture","http"]
   kpt-pasture-mqtt:
     privileged: true
     container_name: xdmy001_kpt_pasture_mqtt
@@ -41,4 +41,4 @@ services:
     environment:
       - APP_ENVIRONMENT=production
       - PASTURE_WORK_DIR=/app/kpt-pasture
-    command: [ "/app/kpt-pasture/kptPasture","mqtt" ]
+    command: [ "/app/kpt-pasture/kptPasture","mqtt"]

+ 22 - 20
go.mod

@@ -3,7 +3,7 @@ module kpt-pasture
 go 1.17
 
 require (
-	gitee.com/xuyiping_admin/go_proto v0.0.0-20250616080546-3ebf4d3f0874
+	gitee.com/xuyiping_admin/go_proto v0.0.0-20250731013344-bd687f491881
 	gitee.com/xuyiping_admin/pkg v0.0.0-20250613101634-36c36a2d27d0
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/eclipse/paho.mqtt.golang v1.4.3
@@ -34,16 +34,8 @@ require (
 
 require (
 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
-	github.com/gorilla/websocket v1.5.3 // indirect
-	github.com/nyaruka/phonenumbers v1.1.7 // indirect
-	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
-	golang.org/x/arch v0.3.0 // indirect
-)
-
-require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
-	github.com/bytedance/sonic v1.10.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
 	github.com/chenzhuoyu/iasm v0.9.0 // indirect
@@ -54,17 +46,16 @@ require (
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/go-playground/validator/v10 v10.15.5 // indirect
-	github.com/go-redis/redis/v8 v8.11.2 // indirect
+	github.com/go-redis/redis/v8 v8.11.5 // indirect
 	github.com/go-sql-driver/mysql v1.7.0 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/google/uuid v1.6.0 // indirect
+	github.com/gorilla/websocket v1.5.3 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/huandu/xstrings v1.4.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/jinzhu/copier v0.3.5
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -78,18 +69,21 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
-	github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+	github.com/nicksnyder/go-i18n/v2 v2.0.2 // indirect
+	github.com/nyaruka/phonenumbers v1.1.7 // indirect
+	github.com/onsi/gomega v1.27.1 // indirect
+	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/client_model v0.3.0 // indirect
-	github.com/prometheus/common v0.42.0 // indirect; indirec
+	github.com/prometheus/client_model v0.5.0 // indirect
+	github.com/prometheus/common v0.42.0 // indirect
 	github.com/prometheus/procfs v0.10.1 // indirect
 	github.com/richardlehane/mscfb v1.0.4 // indirect
 	github.com/richardlehane/msoleps v1.0.3 // indirect
 	github.com/robfig/cron v1.2.0 // indirect
 	github.com/robfig/cron/v3 v3.0.1 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
-	github.com/spf13/afero v1.9.5 // indirect
+	github.com/spf13/afero v1.10.0 // indirect
 	github.com/spf13/cast v1.5.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
@@ -97,16 +91,13 @@ require (
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.11 // indirect
 	github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
-	github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
-	github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
 	github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.8.0 // indirect
+	golang.org/x/arch v0.3.0 // indirect
 	golang.org/x/crypto v0.23.0 // indirect
-	golang.org/x/net v0.25.0 // indirect
 	golang.org/x/sys v0.20.0 // indirect
 	golang.org/x/text v0.15.0 // indirect
-	golang.org/x/time v0.3.0
 	google.golang.org/appengine v1.6.8 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect
 	google.golang.org/grpc v1.64.0 // indirect
@@ -114,3 +105,14 @@ require (
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
+
+require (
+	github.com/bytedance/sonic v1.10.1 // indirect
+	github.com/go-playground/validator/v10 v10.15.5 // indirect
+	github.com/jinzhu/copier v0.3.5
+	github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+	github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
+	github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
+	golang.org/x/net v0.25.0 // indirect
+	golang.org/x/time v0.5.0
+)

文件差異過大導致無法顯示
+ 1328 - 138
go.sum


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

@@ -217,3 +217,62 @@ func LongTermInfertility(c *gin.Context) {
 	}
 	ginutil.JSONResp(c, res)
 }
+
+func AlreadySale(c *gin.Context) {
+	var req pasturePb.AlreadySalesReportRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.StartAt, valid.Required),
+		valid.Field(&req.EndAt, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	pagination := &pasturePb.PaginationModel{
+		Page:       int32(c.GetInt(middleware.Page)),
+		PageSize:   int32(c.GetInt(middleware.PageSize)),
+		PageOffset: int32(c.GetInt(middleware.PageOffset)),
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.AlreadySale(c, &req, pagination)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}
+
+func CanSale(c *gin.Context) {
+	var req pasturePb.CanSalesReportRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	if err := valid.ValidateStruct(&req,
+		valid.Field(&req.WeightStart, valid.Required),
+		valid.Field(&req.WeightEnd, valid.Required),
+		valid.Field(&req.CurrentPrice, valid.Required),
+	); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	pagination := &pasturePb.PaginationModel{
+		Page:       int32(c.GetInt(middleware.Page)),
+		PageSize:   int32(c.GetInt(middleware.PageSize)),
+		PageOffset: int32(c.GetInt(middleware.PageOffset)),
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.CanSale(c, &req, pagination)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}

+ 10 - 0
http/handler/dashboard/dashboard.go

@@ -124,3 +124,13 @@ func Equipment(c *gin.Context) {
 
 	ginutil.JSONResp(c, res)
 }
+
+func OutNumber(c *gin.Context) {
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.OutNumber(c)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+
+	ginutil.JSONResp(c, res)
+}

+ 0 - 1
http/handler/event/event_base.go

@@ -53,7 +53,6 @@ func EnterEventCreate(c *gin.Context) {
 		valid.Field(&req.PenId, valid.Required),
 		valid.Field(&req.OperationId, valid.Required),
 		valid.Field(&req.Weight, valid.Required),
-		valid.Field(&req.BatchNumber, valid.Required),
 	); err != nil {
 		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
 		return

+ 42 - 0
http/handler/feeding/feeding.go

@@ -0,0 +1,42 @@
+package feeding
+
+import (
+	"kpt-pasture/http/middleware"
+	"net/http"
+
+	feedingPb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+	"gitee.com/xuyiping_admin/pkg/apierr"
+	"gitee.com/xuyiping_admin/pkg/ginutil"
+	"github.com/gin-gonic/gin"
+)
+
+func GetFeedingHomepage(c *gin.Context) {
+	var req feedingPb.FeedingHomepageRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+	res, err := middleware.BackendOperation(c).OpsService.GetFeedingHomepage(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}
+
+// feeding/management
+
+func GetFeedingManagement(c *gin.Context) {
+	var req feedingPb.FeedingManagementRequest
+	if err := ginutil.BindProto(c, &req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	res, err := middleware.BackendOperation(c).OpsService.GetFeedingManagement(c, &req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	ginutil.JSONResp(c, res)
+}

+ 7 - 5
http/middleware/auth.go

@@ -14,11 +14,13 @@ import (
 )
 
 const (
-	Authorization = "Authorization"
-	ToKenPrefix   = "Bearer "
-	UserName      = "userName"
-	FarmId        = "FarmId"
-	XRequestId    = "X-Request-Id"
+	Authorization   = "Authorization"
+	ToKenPrefix     = "Bearer "
+	UserName        = "userName"
+	FarmId          = "FarmId"
+	XRequestId      = "X-Request-Id"
+	LanguageContent = "languageContent"
+	Language        = "language"
 )
 
 func GetToken(c *gin.Context) string {

+ 138 - 0
http/middleware/i18n.go

@@ -0,0 +1,138 @@
+package middleware
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
+	"gitee.com/xuyiping_admin/pkg/xerr"
+	"github.com/gin-gonic/gin"
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+	"go.uber.org/zap"
+	"golang.org/x/text/language"
+)
+
+var (
+	localesDir      = "./locales"
+	bundle          *i18n.Bundle
+	defaultLanguage = language.Chinese
+	supported       = map[string]bool{ // 支持的语言列表
+		"zh": true, // 中文
+		"en": true, // 英文
+		// 添加其他支持的语言...
+	}
+)
+
+// I18N 初始化国际化中间件
+func I18N() gin.HandlerFunc {
+	bundle = i18n.NewBundle(defaultLanguage)
+	bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
+
+	// 加载翻译文件
+	if err := loadTranslations(); err != nil {
+		zaplog.Error("Failed to load translations", zap.Error(err))
+	}
+
+	return func(c *gin.Context) {
+		lang := detectLanguage(c.Request)
+		localize := i18n.NewLocalizer(bundle, lang)
+		c.Set(LanguageContent, localize)
+		c.Set(Language, lang)
+		c.Next()
+	}
+}
+
+// loadTranslations 加载所有翻译文件
+func loadTranslations() error {
+	absPath, err := filepath.Abs(localesDir)
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+
+	files, err := ioutil.ReadDir(absPath)
+	if err != nil {
+		return xerr.WithStack(fmt.Errorf("failed to read files dir: %w", err))
+	}
+
+	if len(files) == 0 {
+		return xerr.Custom("no found in files directory")
+	}
+
+	loaded := false
+	for _, entry := range files {
+		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
+			continue
+		}
+
+		filePath := path.Join(absPath, entry.Name())
+		data, err := os.ReadFile(filePath)
+		if err != nil {
+			zaplog.Error("Failed to read translation file",
+				zap.String("file", filePath),
+				zap.Error(err))
+			continue
+		}
+
+		messageFile, err := bundle.ParseMessageFileBytes(data, filePath)
+		if err != nil {
+			zaplog.Error("Failed to parse translation file",
+				zap.String("file", filePath),
+				zap.Error(err))
+			continue
+		}
+
+		zaplog.Info("Loaded translation",
+			zap.String("language", messageFile.Tag.String()),
+			zap.Any("messageFile", messageFile))
+		loaded = true
+	}
+
+	if !loaded {
+		return xerr.Custom("no valid translation files were loaded")
+	}
+	return nil
+}
+
+// detectLanguage 检测客户端语言偏好
+func detectLanguage(r *http.Request) string {
+	// 1. 检查查询参数
+	if lang := r.URL.Query().Get("language"); lang != "" {
+		return normalizeLanguage(lang)
+	}
+
+	// 2. 检查 Cookie
+	if langCookie, err := r.Cookie("language"); err == nil && langCookie.Value != "" {
+		return normalizeLanguage(langCookie.Value)
+	}
+
+	// 3. 检查 Accept-Language 头
+	if acceptLang := r.Header.Get("Accept-Language"); acceptLang != "" {
+		if langs := strings.Split(acceptLang, ","); len(langs) > 0 {
+			return normalizeLanguage(langs[0])
+		}
+	}
+	// 默认返回中文
+	return defaultLanguage.String()
+}
+
+// normalizeLanguage 规范化语言代码
+func normalizeLanguage(lang string) string {
+	// 去除权重值 (如 en;q=0.9 -> en)
+	if parts := strings.Split(lang, ";"); len(parts) > 0 {
+		lang = parts[0]
+	}
+
+	// 转换为小写并去除地区代码 (en-US -> en)
+	lang = strings.ToLower(strings.Split(lang, "-")[0])
+
+	if supported[lang] {
+		return lang
+	}
+	return defaultLanguage.String()
+}

+ 2 - 0
http/route/cow_api.go

@@ -24,5 +24,7 @@ func CowAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		searchRoute := authRouteGroup(s, "/api/v1/search/")
 		searchRoute.POST("/indicators/comparison", cow.IndicatorsComparison)
 		searchRoute.POST("/cow/long/term/infertility", cow.LongTermInfertility)
+		searchRoute.POST("/already/sale", cow.AlreadySale)
+		searchRoute.POST("/can/sale", cow.CanSale)
 	}
 }

+ 1 - 0
http/route/dashboard_api.go

@@ -21,5 +21,6 @@ func DashboardApi(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		dashboardRoute.POST("/data/warning/set", dashboard.DataWarningSet)
 		dashboardRoute.GET("/todo/count", dashboard.TodoCount)
 		dashboardRoute.GET("/equipment/list", dashboard.Equipment)
+		dashboardRoute.GET("/out/number", dashboard.OutNumber)
 	}
 }

+ 19 - 0
http/route/feeding_api.go

@@ -0,0 +1,19 @@
+package route
+
+import (
+	"kpt-pasture/http/handler/feeding"
+
+	"github.com/gin-gonic/gin"
+)
+
+func FeedingAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
+	return func(s *gin.Engine) {
+		for _, opt := range opts {
+			opt(s)
+		}
+		feedingRoute := authRouteGroup(s, "/api/v1/feeding/")
+
+		feedingRoute.GET("tmr/data", feeding.GetFeedingHomepage)
+		feedingRoute.GET("management", feeding.GetFeedingManagement)
+	}
+}

+ 1 - 0
http/route/route.go

@@ -19,6 +19,7 @@ func HTTPServerRoute(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		MilkManageAPI(opts...),
 		WarningAPI(opts...),
 		TestAPI(opts...),
+		FeedingAPI(opts...),
 	}
 
 	return func(s *gin.Engine) {

+ 2 - 2
http/route/system_api.go

@@ -17,7 +17,7 @@ func SystemAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		s.NoRoute(handler.Handle404)
 		// Health Check
 		s.GET("/check", handler.Health)
-		s.POST("/api/v1/login", system.Login)
+		s.POST("/api/v1/login", middleware.I18N(), system.Login)
 		s.Static("/api/v1/files", "./files")
 
 		// system API 组  系统用户
@@ -61,6 +61,6 @@ func SystemAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 func authRouteGroup(s *gin.Engine, relativePath string) *gin.RouterGroup {
 	group := s.Group(relativePath)
 	// 中间件鉴权
-	group.Use(middleware.RequireAdmin(), middleware.GinLogger(), middleware.Pagination())
+	group.Use(middleware.I18N(), middleware.RequireAdmin(), middleware.GinLogger(), middleware.Pagination())
 	return group
 }

+ 100 - 0
locales/en.json

@@ -0,0 +1,100 @@
+{
+  "auth.unPasture": "The current user is not configured with relevant pasture data. Please contact the administrator!",
+  "auth.unLogin": "Please log in first!",
+  "auth.noUser": "User does not exist!",
+  "auth.reLogin": "Incorrect user login information. Please log out and log in again!",
+  "auth.userDelete": "The current user data has been deleted!",
+  "auth.userDisable": "The current user is disabled. Please contact the administrator!",
+  "auth.pregnancyDays": "Please configure the gestation days in the system settings basic parameters!",
+  "auth.noCow": "This cattle does not exist!",
+  "auth.errorCow": "Incorrect cattle information: {{.earNumber}}",
+  "auth.errorDateRange": "Incorrect date range",
+  "auth.wrongPassword": "Wrong password!",
+  "auth.noPermission": "No access permission!",
+  "auth.errorToken": "Invalid Token",
+  "auth.errorPasture": "Pasture information error!",
+  "auth.userNameExist": "User already exists!",
+  "auth.userRoleNotExist": "User role does not exist!",
+  "auth.noMenuPermission": "The user role has no menu permissions!",
+  "auth.menuIdNotEmpty": "Menu ID cannot be empty!",
+  "auth.checkOperation": "Please check the operator information!",
+  "auth.getOperationError": "Failed to get operator information!",
+  "dataWarning.notSupported": "This warning data is not currently supported!",
+  "dataWarning.selectData": "Please select warning data!",
+  "dataWarning.defaultDataNotExist": "Default warning data does not exist. Please contact the engineer!",
+  "dataWarning.defaultDataError": "Default warning data error. Please contact the engineer!",
+  "dataWarning.dataNotExist": "Warning data does not exist. Please contact the engineer!",
+  "dataWarning.dataError": "Warning data is incorrect. Please contact the engineer!",
+  "dataWarning.conditionGroupNotEmpty": "Condition group cannot be empty!",
+  "validate.startEndDateTime": "Start time cannot be later than end time!",
+  "validate.wrongCowSaleTime": "Cattle ID: {{.earNumber}}, sale time cannot be earlier than the cattle's birth time!",
+  "validate.dataError": "Data exception!",
+  "validate.immuneTime": "Cattle ID: {{.earNumber}}, immunization time cannot be earlier than the cattle's birth time!",
+  "validate.birthTime": "Cattle ID: {{.earNumber}}, calving time cannot be earlier than the cattle's birth time!",
+  "validate.lactationCount": "Calf information does not match the number of births",
+  "validate.noPregnancy": "This cow has not been bred!",
+  "validate.noMotherCow": "This cow information does not exist!",
+  "validate.noSameTimePlan": "Cattle ID: {{.earNumber}}, no synchronization plan today!",
+  "validate.matingTimeBirthTime": "Cattle ID: {{.earNumber}}, The breeding time cannot be earlier than the birth time of the cattle!",
+  "validate.matingTimeLastCalvingTime": "Cattle ID: {{.earNumber}}, The mating time cannot be earlier than the latest calving time of the cow!",
+  "validate.pregnantCheckMatingTime": "Cattle ID: {{.earNumber}}, The pregnancy test date must be later than the last mating date!",
+  "validate.dataLimit": "A maximum of 50 pieces of data can be submitted at once",
+  "validate.cowSex": "Cattle ID: {{.earNumber}},Not a cow!",
+  "validate.birthTimeEnterTime": "The birth time of cattle cannot exceed the entry time!",
+  "validate.matingTimeLastMatingTime": "Cattle ID: {{.earNumber}},mating date: {{.matingDate}},cannot be earlier than the last mating date: {{.lastMatingDate}}",
+  "validate.matingTimeLastAbortionTime": "Cattle ID: {{.earNumber}},mating date: {{.matingDate}},cannot be earlier than the last abortion date: {{.lastAbortionDate}}!",
+  "validate.matingDataError": "Cattle ID: {{.earNumber}},the latest mating data is abnormal!",
+  "validate.breedStatusError": "Cattle ID: {{.earNumber}},the current status is:{{.breedStatus}},cannot conduct breeding!",
+  "validate.cannotPregnantCheck": "Cattle ID: {{.earNumber}} Not participating in mating, unable to undergo pregnancy testing!",
+  "validate.notFoundPregnantCheckData": "No pregnancy test data was found for the {{.earNumber}} cow!",
+  "validate.notFoundMatingData": "No breeding data was found for the {{.earNumber}} cow!",
+  "validate.deathTime": "The death time of Cow {{.earNumber}} cannot be earlier than the birth time!",
+  "validate.deathTimeLastEvent": "Cattle: {{.earNumber}}, death time cannot be earlier than the last event time!",
+  "validate.unMatingDate": "Cattle: {{.earNumber}}, breeding time cannot be earlier than birth time!",
+  "validate.estrusDateBirthDate": "Cattle: {{.earNumber}}, estrus time cannot be less than birth time!",
+  "validate.estrusDateLastCalvingDate": "Cattle: {{.earNumber}}, estrus time cannot be less than the last calving time!",
+  "validate.estrusDataAlready": "The cow: {{.earNumber}}, has already submitted estrus data today!",
+  "validate.matingDataItemAlready": "The breeding list already has the data for this cow: {{.earNumber}}, and the breeding data has been submitted today!",
+  "validate.CowNotPregnant": "Cow: {{.earNumber}}, not pregnant!",
+  "validate.AbortionDateBirthDate": "Cattle: {{.earNumber}}, abortion time cannot be earlier than the cow's birth time!",
+  "analysis.wrongMethod": "Incorrect statistical method!",
+  "analysis.wrongLact": "Incorrect lactation interval symbol!",
+  "analysis.wrongDateRange": "Start time cannot be later than end time!",
+  "analysis.xAndY": "X-axis and Y-axis cannot be the same!",
+  "analysis.wrongXY": "Incorrect XY-axis data!",
+  "cow.earNumber": "Please select ear tag number!",
+  "cow.dataError": "Data error: {{.earNumber}}!",
+  "cow.inputCow": "Please enter cattle ID or collar number!",
+  "cow.noCow": "This cattle does not exist!",
+  "cow.errorCow": "Cattle information error: {{.earNumber}}!",
+  "cow.wrongCow": "Cattle ID: {{.earNumber}}, onset time cannot be earlier than the cattle's birth time!",
+  "cow.errorCowById": "Incorrect cattle information: {{.cowId}}!",
+  "cow.errorCowData": "Abnormal cattle data!",
+  "cow.cowNotExist": "This cattle: {{.earNumber}} does not exist!",
+  "cow.neckRingNumberBind": "This cattle: {{.earNumber}} has already been bound to collar: {{.neckRingNumber}}!",
+  "cow.selectCow": "Please select relevant cattle!",
+  "cow.errorData": "Abnormal cattle data!",
+  "cow.Exists": "The cow already exists!",
+  "health.cowExist": "This cattle already has this disease!",
+  "health.createPrescriptionFail": "Failed to create prescription!",
+  "health.treatmentTimeError": "Treatment time cannot be later than diagnosis time!",
+  "health.errorCowData": "Abnormal cattle data!",
+  "goods.selectNeckRing": "Please select collar data!",
+  "goods.neckRingAlreadyBind": "This collar: {{.neckRingNumber}} has already been bound to cattle: {{.earNumber}}",
+  "goods.neckRingNotBind": "This collar: {{.neckRingNumber}} is not bound to any cattle!",
+  "goods.selectOutGoods": "Please select goods for outbound!",
+  "goods.outGoodsNotExist": "This outbound order does not exist!",
+  "goods.outGoodsNotAllowEdit": "Not the applicant, no permission to modify this outbound order!",
+  "goods.outGoodsNotAllowDelete": "Not the applicant, no permission to delete this outbound order!",
+  "goods.outGoodsNotEdit": "This outbound order cannot be modified!",
+  "goods.goodsCount": "Please fill in the quantity of goods!",
+  "goods.unknownOutGoodsType": "Unknown outbound type!",
+  "goods.outGoodsAuditStatusError": "Outbound order audit status exception!",
+  "goods.outGoodsError": "Abnormal outbound order",
+  "goods.outGoodsNotDelete": "This outbound order cannot be deleted!",
+  "goods.dataNotExist": "This data: {{.id}} does not exist!",
+  "goods.frozenSemenNotExist": "The frozen semen information does not exist!",
+  "goods.frozenSemenNotEnough": "Insufficient quantity of frozen semen!",
+  "pasture.dealerNotExist": "This dealer information was not found!",
+  "pasture.dealerError": "Dealer data exception!"
+}

+ 100 - 0
locales/zh.json

@@ -0,0 +1,100 @@
+{
+  "auth.unPasture": "当前用户未配置相关牧场数据,请联系管理员!",
+  "auth.unLogin": "请先登录!",
+  "auth.noUser": "用户不存在!",
+  "auth.reLogin": "用户登录信息有误,请退出重新登录!",
+  "auth.userDelete": "当前用户数据已经删除!",
+  "auth.userDisable": "当前用户已禁用,请联系管理员!",
+  "auth.pregnancyDays": "请在系统设置基础参数配置妊娠天数!",
+  "auth.noCow": "该牛只不存在!",
+  "auth.errorCow": "错误的牛只信息: {{.earNumber}}",
+  "auth.errorDateRange":"错误的日期范围",
+  "auth.wrongPassword": "密码错误!",
+  "auth.noPermission": "无权限访问!",
+  "auth.errorToken": "无效的Token",
+  "auth.errorPasture": "牧场信息错误!",
+  "auth.userNameExist": "用户已存在!",
+  "auth.userRoleNotExist": "用户角色不存在!",
+  "auth.noMenuPermission": "用户角色没有菜单权限!",
+  "auth.menuIdNotEmpty": "菜单ID不能为空!",
+  "auth.checkOperation": "请检查操作人信息!",
+  "auth.getOperationError": "获取操作人员信息失败!",
+  "dataWarning.notSupported": "暂不支持该预警数据!",
+  "dataWarning.selectData": "请选择预警数据!",
+  "dataWarning.defaultDataNotExist": "默认预警数据不存在,请联系工程师!",
+  "dataWarning.defaultDataError": "默认预警数据错误,请联系工程师!",
+  "dataWarning.dataNotExist": "预警数据不存在,请联系工程师!",
+  "dataWarning.dataError": "预警数据有误,请联系工程师!",
+  "dataWarning.conditionGroupNotEmpty": "条件组不能为空!",
+  "validate.startEndDateTime": "开始时间不能大于结束时间!",
+  "validate.wrongCowSaleTime": "牛号: {{.earNumber}},销售时间不能早于牛只出生时间!",
+  "validate.dataError": "数据异常!",
+  "validate.immuneTime": "牛号: {{.earNumber}},免疫时间不能早于牛只出生时间!",
+  "validate.birthTime": "牛号: {{.earNumber}}, 产犊时间不能早于牛只出生时间!",
+  "validate.lactationCount": "犊牛信息与产子数不相符",
+  "validate.noPregnancy": "该母牛未配种!",
+  "validate.noMotherCow": "该母牛信息不存在!",
+  "validate.noSameTimePlan": "该牛号: {{.earNumber}}, 今日没有同期计划!",
+  "validate.matingTimeBirthTime": "牛号: {{.earNumber}}, 配种时间不能早于牛只出生时间!",
+  "validate.matingTimeLastCalvingTime": "牛号: {{.earNumber}}, 配种时间不能早于牛只最近产犊时间!",
+  "validate.pregnantCheckMatingTime": "牛号: {{.earNumber}}, 孕检日期必须大于最后一次配种日期!",
+  "validate.dataLimit": "一次性最多限制提交50条数据!",
+  "validate.cowSex": "牛只: {{.earNumber}},不是母牛!",
+  "validate.birthTimeEnterTime": "牛只出生时间不能大于入场时间!",
+  "validate.matingTimeLastMatingTime": "牛只: {{.earNumber}},配种时间: {{.matingDate}},不能小于上次配种时间: {{.lastMatingDate}}!",
+  "validate.matingTimeLastAbortionTime": "牛只: {{.earNumber}},配种时间: {{.matingDate}},不能小于上次流产时间: {{.lastAbortionDate}}!",
+  "validate.matingDataError": "牛只: {{.earNumber}},最近一次配种数据异常!",
+  "validate.breedStatusError": "牛只: {{.earNumber}},当前状态为:{{.breedStatus}},不能进行配种!",
+  "validate.cannotPregnantCheck": "牛只: {{.earNumber}} 未参加配种,不能进行孕检!",
+  "validate.notFoundPregnantCheckData": "未发现该牛只: %s 孕检数据!",
+  "validate.notFoundMatingData": "未发现该牛只: %s 配种数据!",
+  "validate.deathTime": "牛只: {{.earNumber}},死亡时间不能早于出生时间!",
+  "validate.deathTimeLastEvent": "牛只: {{.earNumber}},死亡时间不能早于上一次事件时间!",
+  "validate.unMatingDate": "牛只: {{.earNumber}},禁止配种时间不能小于出生时间!",
+  "validate.estrusDateBirthDate": "牛只: {{.earNumber}},发情时间不能小于出生时间!",
+  "validate.estrusDateLastCalvingDate": "牛只: {{.earNumber}},发情时间不能小于上次产犊时间!",
+  "validate.estrusDataAlready": "该牛只: {{.earNumber}},今天已经提交过发情数据!",
+  "validate.matingDataItemAlready": "配种清单已经有该牛只数据: {{.earNumber}},今天已经提交过配种数据!",
+  "validate.CowNotPregnant": "牛只: {{.earNumber}},不是怀孕状态!",
+  "validate.AbortionDateBirthDate": "牛只: {{.earNumber}},流产时间不能早于牛只出生时间!",
+  "analysis.wrongMethod": "错误的统计方式!",
+  "analysis.wrongLact": "错误的胎次区间符号!",
+  "analysis.wrongDateRange": "开始时间不能大于结束时间!",
+  "analysis.xAndY": "X轴和Y轴不能相同!",
+  "analysis.wrongXY": "错误的XY轴数据!",
+  "cow.earNumber": "请选择耳号!",
+  "cow.dataError": "数据错误: {{.earNumber}}!",
+  "cow.inputCow": "请输入牛号或项圈号!",
+  "cow.noCow": "该牛不存在!",
+  "cow.errorCow": "牛只信息错误: {{.earNumber}}!",
+  "cow.wrongCow": "牛号: {{.earNumber}},发病时间不能早于牛只出生时间!",
+  "cow.errorCowById": "错误的牛只信息: {{.cowId}}!",
+  "cow.errorCowData":"异常牛只数据!",
+  "cow.cowNotExist": "该牛只: {{.earNumber}} 不存在!",
+  "cow.neckRingNumberBind": "该牛只: {{.earNumber}},已经绑定脖环: {{.neckRingNumber}}!",
+  "cow.selectCow": "请选择相关牛只!",
+  "cow.errorData": "异常牛只数据!",
+  "cow.Exists": "该牛只已存在!",
+  "health.cowExist": "该牛已存在该疾病!",
+  "health.createPrescriptionFail": "创建处方失败!",
+  "health.treatmentTimeError": "治疗时间不能大于确诊时间!",
+  "health.errorCowData": "异常牛只数据!",
+  "goods.selectNeckRing": "请选择要脖环数据!",
+  "goods.neckRingAlreadyBind": "该脖环: {{.neckRingNumber}}已经绑定牛只: {{.earNumber}}",
+  "goods.neckRingNotBind": "该脖环: {{.neckRingNumber}},未绑定牛只!",
+  "goods.selectOutGoods": "请选择要出库商品!",
+  "goods.outGoodsNotExist": "该出库单不存在!",
+  "goods.outGoodsNotAllowEdit": "非申请人,无权修改该出库单!",
+  "goods.outGoodsNotAllowDelete": "非申请人,无权删除该出库单!",
+  "goods.outGoodsNotEdit": "该出库单不能修改!",
+  "goods.goodsCount": "请填写商品数量!",
+  "goods.unknownOutGoodsType": "未知的出库类型!",
+  "goods.outGoodsAuditStatusError": "出库单审核状态异常!",
+  "goods.outGoodsError": "异常出库单",
+  "goods.outGoodsNotDelete": "该出库单无法删除!",
+  "goods.dataNotExist": "该数据: {{.id}}不存在!",
+  "goods.frozenSemenNotExist": "该冻精信息不存在!",
+  "goods.frozenSemenNotEnough": "冻精数量不足!",
+  "pasture.dealerNotExist": "未找到该经销商信息!",
+  "pasture.dealerError": "经销商数据异常!"
+}

+ 7 - 1
migrator/v0001_demo.sql

@@ -3,4 +3,10 @@ CREATE TABLE IF NOT EXISTS `demo` (
    `created_at` bigint(20) unsigned NOT NULL COMMENT '创建时间',
    `updated_at` bigint(20) unsigned NOT NULL COMMENT '更新时间',
    PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='demo';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='demo';
+
+
+
+ALTER TABLE `kpt_pasture`.`app_pasture_list` 
+ADD COLUMN `pasture_id` int(11) NULL COMMENT '饲喂系统牧场id' AFTER `updated_at`,
+ADD COLUMN `pasture_url` varchar(80) NULL COMMENT '饲喂系统url' AFTER `pasture_id`;

+ 2 - 0
model/app_pasture_list.go

@@ -21,6 +21,8 @@ type AppPastureList struct {
 	PlanScale            string                         `json:"planScale"`
 	AppId                string                         `json:"appId"`
 	Status               int32                          `json:"status"`
+	FeedPastureId        int64                          `json:"feedPastureId"`
+	FeedPastureUrl       string                         `json:"feedPastureUrl"`
 	IsShow               pasturePb.IsShow_Kind          `json:"isShow"`
 	ProductionModel      int32                          `json:"productionModel"`
 	Remarks              string                         `json:"remarks"`

+ 38 - 0
model/config_abortion_reasons.go

@@ -0,0 +1,38 @@
+package model
+
+import pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+
+type ConfigAbortionReasons struct {
+	Id        int32                          `json:"id"`
+	PastureId int64                          `json:"pastureId"`
+	Kind      pasturePb.AbortionReasons_Kind `json:"kind"`
+	Zh        string                         `json:"zh"`
+	En        string                         `json:"en"`
+	Category  pasturePb.PastureCategory_Kind `json:"category"`
+	Remarks   string                         `json:"remarks"`
+	IsShow    pasturePb.IsShow_Kind          `json:"is_show"`
+	CreatedAt int64                          `json:"created_at"`
+	UpdatedAt int64                          `json:"updated_at"`
+}
+
+func (c *ConfigAbortionReasons) TableName() string {
+	return "config_abortion_reasons"
+}
+
+type ConfigAbortionReasonsSlice []*ConfigAbortionReasons
+
+func (c ConfigAbortionReasonsSlice) ToPB(lang string) []*pasturePb.ConfigOptionsList {
+	res := make([]*pasturePb.ConfigOptionsList, 0)
+	for _, v := range c {
+		label := v.Zh
+		if lang == "en" {
+			label = v.En
+		}
+		res = append(res, &pasturePb.ConfigOptionsList{
+			Value:    int32(v.Kind),
+			Label:    label,
+			Disabled: true,
+		})
+	}
+	return res
+}

+ 43 - 2
model/cow.go

@@ -48,6 +48,7 @@ type Cow struct {
 	BirthAt               int64                          `json:"birthAt"`               // 出生时间
 	AdmissionAt           int64                          `json:"admissionAt"`           // 入场时间
 	DepartureAt           int64                          `json:"departureAt"`           // 离场时间
+	WeaningWeight         int64                          `json:"weaningWeight"`         // 断奶体重
 	DeparturePrice        float32                        `json:"departurePrice"`        // 离场价格
 	DepartureAvgWeight    int32                          `json:"departureAvgWeight"`    // 离场平均体重
 	FirstMatingAt         int64                          `json:"firstMatingAt"`         // 首次配种时间
@@ -113,6 +114,7 @@ func (c *Cow) EventWeaningUpdate(weaningAt int64, penId int32, currentWeight int
 	c.WeaningAt = weaningAt
 	c.CurrentWeight = currentWeight
 	c.LastWeightAt = weaningAt
+	c.WeaningWeight = currentWeight
 }
 
 // EventPregnantCheckUpdate 孕检更新
@@ -565,6 +567,8 @@ func (c CowSlice) ToPB(
 			PurposeName:                 purposeName,
 			BatchNumber:                 v.BatchNumber,
 			AdmissionWeight:             float32(v.AdmissionWeight / 1000),
+			DepartureAvgWeight:          float32(v.DepartureAvgWeight / 1000),
+			WeaningWeight:               float32(v.WeaningWeight / 1000),
 		}
 	}
 	return res
@@ -711,6 +715,7 @@ func NewEnterCow(pastureId int64, req *pasturePb.EventEnterRequest, penMap map[i
 		LastAbortionAt:      int64(req.AbortionAt),
 		AdmissionPrice:      req.Price,
 		BatchNumber:         req.BatchNumber,
+		NeckRingNumber:      req.NeckRingNumber,
 	}
 	cow.AdmissionAge = cow.GetAdmissionAge()
 	cow.DayAge = cow.GetDayAge()
@@ -824,6 +829,42 @@ func (c CowSlice) LongTermInfertilityToPB(breedStatusMap map[pasturePb.BreedStat
 	return res
 }
 
+func (c CowSlice) CanSaleToPB(cowKindMap map[pasturePb.CowKind_Kind]string) []*pasturePb.CanSalesReport {
+	res := make([]*pasturePb.CanSalesReport, len(c))
+	for i, v := range c {
+		cowKindName, lastWeightAtFormat, admissionAtFormat := "", "", ""
+		if name, ok := cowKindMap[v.CowKind]; ok {
+			cowKindName = name
+		}
+		if v.LastWeightAt > 0 {
+			lastWeightAtFormat = time.Unix(v.LastWeightAt, 0).Local().Format(LayoutDate2)
+		}
+		if v.AdmissionAt > 0 {
+			admissionAtFormat = time.Unix(v.AdmissionAt, 0).Local().Format(LayoutDate2)
+		}
+		res[i] = &pasturePb.CanSalesReport{
+			CowId:              int32(v.Id),
+			EarNumber:          v.EarNumber,
+			BatchNumber:        v.BatchNumber,
+			CowKindName:        cowKindName,
+			PenName:            v.PenName,
+			Weight:             float32(v.CurrentWeight) / 1000,
+			AdmissionAge:       v.AdmissionAge,
+			EnterWeight:        0,
+			EnterPrice:         0,
+			LastWeightAtFormat: lastWeightAtFormat,
+			DayAvgFeedCost:     0,
+			AllFeedCost:        0,
+			AllMedicalCharge:   0,
+			OtherCost:          0,
+			ProfitAndLoss:      0,
+			AdmissionAtFormat:  admissionAtFormat,
+			DayAvgWeight:       0,
+		}
+	}
+	return res
+}
+
 // CowBehaviorCurveResponse 脖环行为数据
 type CowBehaviorCurveResponse struct {
 	Code int32                 `json:"code"`
@@ -843,6 +884,6 @@ type CowBehaviorCurveData struct {
 	RuminaChange     []int32                                 `json:"ruminaChange"`     // 反刍变化
 	LowActivity      int32                                   `json:"lowActivity"`      // 低活动量参数
 	MiddleActivity   int32                                   `json:"middleActivity"`   // 中活动量行数
-	IQR1             []int32                                 `json:"IQR1"`
-	IQR3             []int32                                 `json:"IQR3"`
+	IQR1             []int32                                 `json:"IQR1"`             // 1IQR
+	IQR3             []int32                                 `json:"IQR3"`             // 3IQR
 }

+ 7 - 2
model/data_waring.go

@@ -3,6 +3,8 @@ package model
 import (
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"gitee.com/xuyiping_admin/pkg/xerr"
 
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
@@ -47,7 +49,7 @@ func DataWarningInitData(pastureId int64, dataWarningList []*pasturePb.ConfigOpt
 	return res
 }
 
-func (d *DataWarning) GetWarningColumn() (headers map[string]string, headerSort []string, err error) {
+func (d *DataWarning) GetWarningColumn(i18nTemplate *i18n.Localizer) (headers map[string]string, headerSort []string, err error) {
 	switch d.Kind {
 	case pasturePb.DataWarningType_Sale_Standard:
 		headers = map[string]string{
@@ -179,7 +181,10 @@ func (d *DataWarning) GetWarningColumn() (headers map[string]string, headerSort
 			"cowKindName",
 		}
 	default:
-		return nil, nil, xerr.Custom("暂不支持该预警数据")
+		message, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "dataWarning.notSupported",
+		})
+		return nil, nil, xerr.Custom(message)
 	}
 	return headers, headerSort, nil
 }

+ 25 - 5
model/event_cow_treatment.go

@@ -2,6 +2,7 @@ package model
 
 import (
 	"encoding/json"
+	"time"
 
 	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
 	"go.uber.org/zap"
@@ -56,6 +57,7 @@ func NewEventCowTreatment(
 		PrescriptionName:   prescription.Name,
 		PrescriptionDetail: string(b),
 		TreatmentResult:    req.TreatmentResult,
+		TreatmentAt:        int64(req.TreatmentAt),
 		OperationId:        operation.Id,
 		OperationName:      operation.Name,
 		MessageId:          currentUser.Id,
@@ -74,6 +76,8 @@ func NewEventCowCurableTreatment(pastureId int64, currUser, operationUser *Syste
 		TreatmentResult: pasturePb.TreatmentResult_Curable,
 		OperationId:     operationUser.Id,
 		OperationName:   operationUser.Name,
+		MessageId:       currUser.Id,
+		MessageName:     currUser.Name,
 		Remarks:         remarks,
 		TreatmentAt:     curableAt,
 	}
@@ -92,22 +96,38 @@ func (e EventCowTreatmentSlice) ToPB(eventCowDisease *EventCowDisease) []*pastur
 			}
 		}
 
+		treatmentResult := pasturePb.IsShow_No
+		if v.TreatmentResult == pasturePb.TreatmentResult_Curable {
+			treatmentResult = pasturePb.IsShow_Ok
+		}
+
+		diseaseAtFormat, treatmentAtFormat := "", ""
+		if eventCowDisease.DiseaseAt > 0 {
+			diseaseAtFormat = time.Unix(eventCowDisease.DiseaseAt, 0).Format(LayoutDate2)
+		}
+
+		if v.TreatmentAt > 0 {
+			treatmentAtFormat = time.Unix(v.TreatmentAt, 0).Format(LayoutDate2)
+		}
+
 		res[i] = &pasturePb.EventCowTreatment{
+			Id:                 int32(v.Id),
 			CowId:              int32(v.CowId),
 			CowDiseaseId:       int32(v.CowDiseaseId),
 			PrescriptionId:     v.PrescriptionId,
 			PrescriptionName:   v.PrescriptionName,
-			PrescriptionDetail: prescriptionDetail,
-			TreatmentResult:    0,
+			TreatmentResult:    treatmentResult,
 			OperationId:        int32(v.OperationId),
 			OperationName:      v.OperationName,
 			Remarks:            v.Remarks,
-			CreatedAt:          int32(v.CreatedAt),
-			UpdatedAt:          int32(v.UpdatedAt),
 			DiseaseId:          int32(v.DiseaseId),
-			Id:                 int32(v.Id),
 			DiseaseName:        eventCowDisease.DiseaseName,
 			DiseaseAt:          int32(eventCowDisease.DiseaseAt),
+			PrescriptionDetail: prescriptionDetail,
+			DiseaseAtFormat:    diseaseAtFormat,
+			TreatmentAtFormat:  treatmentAtFormat,
+			CreatedAt:          int32(v.CreatedAt),
+			UpdatedAt:          int32(v.UpdatedAt),
 		}
 	}
 	return res

+ 11 - 11
model/event_enter.go

@@ -45,20 +45,20 @@ type EventEnter struct {
 func (e *EventEnter) TableName() string {
 	return "event_enter"
 }
-func NewEventEnter(pastureId, cowId int64, req *pasturePb.EventEnterRequest) *EventEnter {
+func NewEventEnter(pastureId int64, cowInfo *Cow, req *pasturePb.EventEnterRequest) *EventEnter {
 	return &EventEnter{
 		PastureId:        pastureId,
-		EarNumber:        req.EarNumber,
-		CowId:            cowId,
-		Sex:              req.Sex,
-		BirthAt:          int64(req.BirthAt),
-		CowSource:        req.CowSource,
-		CowType:          req.CowType,
-		BreedStatus:      req.BreedStatus,
-		Lact:             req.Lact,
+		EarNumber:        cowInfo.EarNumber,
+		CowId:            cowInfo.Id,
+		Sex:              cowInfo.Sex,
+		BirthAt:          cowInfo.BirthAt,
+		CowSource:        cowInfo.SourceKind,
+		CowType:          cowInfo.CowType,
+		BreedStatus:      cowInfo.BreedStatus,
+		Lact:             cowInfo.Lact,
 		DayAge:           int32(math.Floor(float64(int32(time.Now().Local().Unix())-req.BirthAt) / 86400)),
-		PenId:            req.PenId,
-		CowKind:          req.CowKind,
+		PenId:            cowInfo.PenId,
+		CowKind:          cowInfo.CowKind,
 		FatherNumber:     req.FatherNumber,
 		MotherNumber:     req.MotherNumber,
 		MatingAt:         int64(req.MatingAt),

+ 18 - 0
model/event_sale.go

@@ -2,6 +2,7 @@ package model
 
 import (
 	"strings"
+	"time"
 
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
 )
@@ -120,6 +121,23 @@ func (e EventSaleSlice) ToPB(eventSaleCarMap map[int64][]*EventSaleCar, eventSal
 	return res
 }
 
+func (e EventSaleSlice) ToPB2(monthRang []string) *pasturePb.OutNumber {
+	res := &pasturePb.OutNumber{
+		Month:  monthRang,
+		Number: make([]int32, 0),
+		Name:   "近6个月的出栏量 ",
+	}
+	for i, m := range monthRang {
+		for _, v := range e {
+			month := time.Unix(v.SaleAt, 0).Format(LayoutMonth)
+			if month == m {
+				res.Number[i] += v.SaleCowCount
+			}
+		}
+	}
+	return res
+}
+
 type EventSaleModel struct {
 	CowList          []*Cow
 	SalesType        pasturePb.SalesType_Kind

+ 79 - 1
model/event_sale_cow.go

@@ -1,14 +1,23 @@
 package model
 
-import pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+import (
+	"fmt"
+	"time"
+
+	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+)
 
 type EventSaleCow struct {
 	Id           int64                      `json:"id"`
+	BatchNumber  string                     `json:"batchNumber"`
 	PastureId    int64                      `json:"pastureId"`
 	SaleId       int64                      `json:"saleId"`
 	CowId        int64                      `json:"cowId"`
 	CowType      pasturePb.CowType_Kind     `json:"cowType"`
+	CowKind      pasturePb.CowKind_Kind     `json:"cowKind"`
 	EarNumber    string                     `json:"earNumber"`
+	PenId        int32                      `json:"penId"`
+	PenName      string                     `json:"penName"`
 	DayAge       int32                      `json:"dayAge"`
 	Lact         int32                      `json:"lact"`
 	PregnancyAge int32                      `json:"pregnancyAge"`
@@ -27,16 +36,20 @@ func (e *EventSaleCow) TableName() string {
 func NewEventSaleCow(pastureId int64, saleId, saleAt int64, cowInfo *Cow) *EventSaleCow {
 	return &EventSaleCow{
 		PastureId:    pastureId,
+		BatchNumber:  cowInfo.BatchNumber,
 		SaleId:       saleId,
 		CowId:        cowInfo.Id,
 		Lact:         cowInfo.Lact,
 		EarNumber:    cowInfo.EarNumber,
+		PenId:        cowInfo.PenId,
+		PenName:      cowInfo.PenName,
 		PregnancyAge: cowInfo.PregnancyAge,
 		BreedStatus:  cowInfo.BreedStatus,
 		LactationAge: cowInfo.LactationAge,
 		AdmissionAge: cowInfo.AdmissionAge,
 		DayAge:       cowInfo.GetEventDayAge(saleAt),
 		CowType:      cowInfo.CowType,
+		CowKind:      cowInfo.CowKind,
 	}
 }
 
@@ -47,3 +60,68 @@ func NewEventSaleCowList(pastureId int64, eventSale *EventSale, cowList []*Cow)
 	}
 	return res
 }
+
+type EventSaleCowSlice []*EventSaleCow
+
+func (e EventSaleCowSlice) ToPB(
+	eventSaleMap map[int64]*EventSale,
+	cowKindMap map[pasturePb.CowKind_Kind]string,
+	cowMap map[int64]*Cow,
+	sourceMap map[pasturePb.CowSource_Kind]string,
+) []*pasturePb.AlreadySalesReport {
+	res := make([]*pasturePb.AlreadySalesReport, 0)
+	for _, v := range e {
+		cowKindName, saleAtFormat, dealerName := "", "", ""
+		if name, ok := cowKindMap[v.CowKind]; ok {
+			cowKindName = name
+		}
+
+		salePrice, saleTotalAmount, saleAllWeight, saleAvgWeight := float32(0), float32(0), float32(0), float32(0)
+		if eventSale, ok := eventSaleMap[v.SaleId]; ok {
+			if eventSale.SaleAt > 0 {
+				saleAtFormat = time.Unix(eventSale.SaleAt, 0).Local().Format(LayoutDate2)
+			}
+			saleTotalAmount = float32(eventSale.SaleAllAmount)
+			dealerName = eventSale.DealerName
+		}
+
+		feedCycle, cowSourceName := "", ""
+		if cowInfo, ok := cowMap[v.CowId]; ok {
+			admissionAtFormat := ""
+			if cowInfo.AdmissionAt > 0 {
+				feedCycle = time.Unix(cowInfo.AdmissionAt, 0).Local().Format(LayoutDate2)
+			}
+			feedCycle = fmt.Sprintf("%s-%s", admissionAtFormat, saleAtFormat)
+
+			if name, ok := sourceMap[cowInfo.SourceKind]; ok {
+				cowSourceName = name
+			}
+		}
+
+		res = append(res, &pasturePb.AlreadySalesReport{
+			CowId:            int32(v.CowId),
+			EarNumber:        v.EarNumber,
+			BatchNumber:      v.BatchNumber,
+			CowKindName:      cowKindName,
+			PenName:          v.PenName,
+			SaleAtFormat:     saleAtFormat,
+			SalePrice:        salePrice,
+			SaleTotalAmount:  saleTotalAmount,
+			SaleAvgWeight:    saleAvgWeight,
+			SaleAllWeight:    saleAllWeight,
+			AdmissionAge:     v.AdmissionAge,
+			CowSourceName:    cowSourceName,
+			EnterWeight:      0,
+			EnterPrice:       0,
+			GrowthWeight:     0,
+			AllFeedCost:      0,
+			DayAvgFeedCost:   0,
+			AllMedicalCharge: 0,
+			OtherCost:        0,
+			DealerName:       dealerName,
+			ProfitAndLoss:    0,
+			FeedCycle:        feedCycle,
+		})
+	}
+	return res
+}

+ 2 - 0
model/event_weight.go

@@ -8,6 +8,8 @@ import (
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
 )
 
+const EnterWeigh = "入场体重"
+
 type EventWeight struct {
 	ID            int64  `json:"id"`
 	PastureId     int64  `json:"pastureId"`

+ 1 - 0
model/neck_active_habit.go

@@ -30,6 +30,7 @@ type NeckActiveHabit struct {
 	EarNumber            string                `json:"earNumber"`
 	Lact                 int32                 `json:"lact"`
 	CalvingAge           int32                 `json:"calvingAge"`
+	PenId                int32                 `json:"penId"`
 	ActiveTime           string                `json:"activeTime"`
 	Frameid              int32                 `json:"frameid"`
 	HeatDate             string                `json:"heatDate"`

+ 0 - 33
model/neck_ring_bar_change.go

@@ -1,33 +0,0 @@
-package model
-
-type NeckRingBarChange struct {
-	Id             int64  `json:"id"`
-	PastureId      int64  `json:"pastureId"`
-	NeckRingNumber string `json:"neckRingNumber"`
-	HeatDate       string `json:"heatDate"`
-	FrameId        int32  `json:"frameId"`
-	PenId          int32  `json:"penId"`
-	PenName        string `json:"penName"`
-	Nb             int32  `json:"nb"`
-	ChangeHigh     int32  `json:"changeHigh"`
-	ChangeFilter   int32  `json:"changeFilter"`
-	CreatedAt      int64  `json:"createdAt"`
-	UpdatedAt      int64  `json:"updatedAt"`
-}
-
-func (n *NeckRingBarChange) TableName() string {
-	return "neck_ring_bar_change"
-}
-
-func NewNeckRingBarChange(neckRingNumber, heatDate string, frameId, nb, changeHigh, changeFilter int32, pen *Pen) *NeckRingBarChange {
-	return &NeckRingBarChange{
-		PastureId:    pen.PastureId,
-		HeatDate:     heatDate,
-		FrameId:      frameId,
-		PenId:        pen.Id,
-		PenName:      pen.Name,
-		ChangeHigh:   changeHigh,
-		ChangeFilter: changeFilter,
-		Nb:           nb,
-	}
-}

+ 4 - 0
model/neck_ring_bind_log.go

@@ -4,6 +4,10 @@ const (
 	OperationNameBind   = "绑定"
 	OperationNameUnbind = "解绑"
 	OperationNameUpdate = "编辑"
+	EventEnterBind      = "入场绑定"
+	EventDieBind        = "死亡解绑"
+	DefaultBind         = "正常绑定"
+	DefaultUnBind       = "正常解绑"
 )
 
 type NeckRingBindLog struct {

+ 3 - 0
model/neck_ring_health_warning.go

@@ -62,6 +62,7 @@ func (n NeckRingHealthWarningSlice) ToPB(
 	warningHealthLevelMap map[pasturePb.WarningHealthLevel_Kind]string,
 	cowMap map[int64]*Cow,
 	eventLogMap map[int64]string,
+	healthStatusMap map[pasturePb.HealthStatus_Kind]string,
 ) []*pasturePb.HealthWarningItem {
 	res := make([]*pasturePb.HealthWarningItem, len(n))
 	for i, v := range n {
@@ -93,6 +94,8 @@ func (n NeckRingHealthWarningSlice) ToPB(
 			data.PenId = cow.PenId
 			data.PenName = cow.PenName
 			data.Lact = cow.Lact
+			data.HealthStatus = cow.HealthStatus
+			data.HealthStatusName = healthStatusMap[cow.HealthStatus]
 		}
 
 		if desc, ok := eventLogMap[v.CowId]; ok {

+ 7 - 7
model/neck_ring_original.go

@@ -126,13 +126,13 @@ func (n *NeckRingOriginalMerge) IsMageData(data *NeckRingOriginal, xframeId int3
 }
 
 func (n *NeckRingOriginalMerge) SumAvg() {
-	n.Rumina = int32(float32(n.Rumina) / float32(n.RecordCount) * float32(n.RecordCount))
-	n.Inactive = int32(float32(n.Inactive) / float32(n.RecordCount) * float32(n.RecordCount))
-	n.Active = int32(float32(n.Active) / float32(n.RecordCount) * float32(n.RecordCount))
-	n.Intake = int32(float32(n.Intake) / float32(n.RecordCount) * float32(n.RecordCount))
-	n.Other = int32(float32(n.Other) / float32(n.RecordCount) * float32(n.RecordCount))
-	n.Gasp = int32(float32(n.Gasp) / float32(n.RecordCount) * float32(n.RecordCount))
-	n.High = int32(float32(n.High) / float32(n.RecordCount) * float32(n.RecordCount))
+	n.Rumina = int32(float32(n.Rumina) / float32(n.RecordCount) * DefaultRecordCount)
+	n.Inactive = int32(float32(n.Inactive) / float32(n.RecordCount) * DefaultRecordCount)
+	n.Active = int32(float32(n.Active) / float32(n.RecordCount) * DefaultRecordCount)
+	n.Intake = int32(float32(n.Intake) / float32(n.RecordCount) * DefaultRecordCount)
+	n.Other = int32(float32(n.Other) / float32(n.RecordCount) * DefaultRecordCount)
+	n.Gasp = int32(float32(n.Gasp) / float32(n.RecordCount) * DefaultRecordCount)
+	n.High = int32(float32(n.High) / float32(n.RecordCount) * DefaultRecordCount)
 	n.Voltage = int32(float32(n.Voltage) / float32(n.RecordCount))
 }
 

+ 30 - 0
model/neck_ring_pen_change.go

@@ -0,0 +1,30 @@
+package model
+
+type NeckRingPenChange struct {
+	Id           int64  `json:"id"`
+	PastureId    int64  `json:"pastureId"`
+	HeatDate     string `json:"heatDate"`
+	Frameid      int32  `json:"frameid"`
+	PenId        int32  `json:"penId"`
+	CowCount     int32  `json:"cowCount"`
+	ChangeHigh   int32  `json:"changeHigh"`
+	ChangeFilter int32  `json:"changeFilter"`
+	CreatedAt    int64  `json:"createdAt"`
+	UpdatedAt    int64  `json:"updatedAt"`
+}
+
+func (n *NeckRingPenChange) TableName() string {
+	return "neck_ring_pen_change"
+}
+
+func NewNeckRingPenChange(pastureId int64, heatDate string, cowCount, frameId, penId, changeHigh, changeFilter int32) *NeckRingPenChange {
+	return &NeckRingPenChange{
+		PastureId:    pastureId,
+		HeatDate:     heatDate,
+		Frameid:      frameId,
+		PenId:        penId,
+		CowCount:     cowCount,
+		ChangeHigh:   changeHigh,
+		ChangeFilter: changeFilter,
+	}
+}

+ 1 - 1
model/outbound.go

@@ -37,7 +37,7 @@ func (o *Outbound) Delete() {
 func NewOutbound(pastureId int64, req *pasturePb.OutboundApplyItem, currentUser *SystemUser) *Outbound {
 	return &Outbound{
 		PastureId:        pastureId,
-		Number:           fmt.Sprintf("%s%s", util.GenerateRandomNumberString(8), time.Now().Local().Format(LayoutDate)),
+		Number:           fmt.Sprintf("%s%s", time.Now().Local().Format(LayoutDate), util.GenerateRandomNumber(12)),
 		OutType:          req.OutType,
 		AuditStatus:      pasturePb.AuditStatus_Pending,
 		ApplicantId:      int32(currentUser.Id),

+ 6 - 1
model/outbound_detail.go

@@ -49,9 +49,13 @@ func NewOutboundDetailList(outboundId int64, req []*pasturePb.OutboundApplyGoods
 
 type OutboundDetailSlice []*OutboundDetail
 
-func (o OutboundDetailSlice) ToPB() []*pasturePb.OutboundApplyGoodsItem {
+func (o OutboundDetailSlice) ToPB(unitMap map[pasturePb.Unit_Kind]string) []*pasturePb.OutboundApplyGoodsItem {
 	res := make([]*pasturePb.OutboundApplyGoodsItem, len(o))
 	for i, v := range o {
+		unitName := ""
+		if unit, ok := unitMap[v.Unit]; ok {
+			unitName = unit
+		}
 		res[i] = &pasturePb.OutboundApplyGoodsItem{
 			GoodsId:     int32(v.GoodsId),
 			GoodsName:   v.GoodsName,
@@ -60,6 +64,7 @@ func (o OutboundDetailSlice) ToPB() []*pasturePb.OutboundApplyGoodsItem {
 			BatchNumber: v.BatchNumber,
 			Price:       float32(v.Price) / 100,
 			Unit:        v.Unit,
+			UnitName:    unitName,
 			Quantity:    uint32(v.Quantity),
 		}
 	}

+ 12 - 3
model/system_user.go

@@ -6,6 +6,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
 )
 
@@ -78,13 +80,20 @@ func (s *SystemUser) GetPastureIds() []int32 {
 }
 
 type UserModel struct {
-	SystemUser *SystemUser
-	AppPasture *AppPastureList
+	SystemUser      *SystemUser
+	AppPasture      *AppPastureList
+	LanguageContent *i18n.Localizer
+	Language        string
 }
 
 type SystemUserSlice []*SystemUser
 
-func (s SystemUserSlice) ToPB(deptList []*SystemDept, roleList []*SystemRole, appPastureList []*AppPastureList, systemUserDepthRoleMap map[int64]*SystemUserDepthRole) []*pasturePb.SearchUserRequest {
+func (s SystemUserSlice) ToPB(
+	deptList []*SystemDept,
+	roleList []*SystemRole,
+	appPastureList []*AppPastureList,
+	systemUserDepthRoleMap map[int64]*SystemUserDepthRole,
+) []*pasturePb.SearchUserRequest {
 	deptMap := make(map[int32]*SystemDept)
 	for _, v := range deptList {
 		deptMap[int32(v.Id)] = v

+ 1 - 1
module/backend/analysis.go

@@ -16,7 +16,7 @@ import (
 func (s *StoreEntry) WeightScatterPlot(ctx context.Context, req *pasturePb.SearchGrowthCurvesRequest, pagination *pasturePb.PaginationModel) (*pasturePb.GrowthCurvesResponse, error) {
 	userModel, err := s.GetUserModel(ctx)
 	if err != nil {
-		return nil, xerr.Custom("当前用户信息错误,请退出重新登录")
+		return nil, xerr.WithStack(err)
 	}
 
 	// 查询数据

+ 78 - 41
module/backend/analysis_breed.go

@@ -7,6 +7,8 @@ import (
 	"kpt-pasture/util"
 	"net/http"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
 	"gitee.com/xuyiping_admin/pkg/xerr"
 )
@@ -21,7 +23,10 @@ func (s *StoreEntry) SingleFactorInfantSurvivalRateAnalysis(ctx context.Context,
 	startTimeUnix := util.TimeParseLocalUnix(req.StartDayTime)
 	endTimeUnix := util.TimeParseLocalEndUnix(req.EndDayTime)
 	if startTimeUnix == 0 || endTimeUnix == 0 || endTimeUnix <= startTimeUnix {
-		return nil, xerr.Custom("开始时间不能大于结束时间")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.startEndDateTime",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 	list := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
 	chart := &pasturePb.SingleFactorPregnancyRateChart{
@@ -33,29 +38,32 @@ func (s *StoreEntry) SingleFactorInfantSurvivalRateAnalysis(ctx context.Context,
 	}
 	switch req.AnalysisMethod {
 	case pasturePb.SingleFactorAnalysisMethod_Cycle:
-		list, err = s.SingleFactorAnalysisMethodCycle(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodCycle(userModel, req)
 	case pasturePb.SingleFactorAnalysisMethod_Months:
-		list, err = s.SingleFactorAnalysisMethodMonths(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodMonths(userModel, req)
 	case pasturePb.SingleFactorAnalysisMethod_Mating_Times:
-		list, err = s.SingleFactorAnalysisMethodMatingTimes(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodMatingTimes(userModel, req)
 	case pasturePb.SingleFactorAnalysisMethod_Breeding_Method:
-		list, err = s.SingleFactorAnalysisMethodBreeding(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodBreeding(userModel, req)
 	case pasturePb.SingleFactorAnalysisMethod_Breeding_Company:
-		list, err = s.SingleFactorAnalysisMethodBreedingCompany(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodBreedingCompany(userModel, req)
 	case pasturePb.SingleFactorAnalysisMethod_Operation:
-		list, err = s.SingleFactorAnalysisMethodOperation(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodOperation(userModel, req)
 	case pasturePb.SingleFactorAnalysisMethod_Mating_Interval:
-		list, err = s.SingleFactorAnalysisMethodMatingInterval(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodMatingInterval(userModel, req)
 	case pasturePb.SingleFactorAnalysisMethod_Bull:
-		list, err = s.SingleFactorAnalysisMethodBull(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodBull(userModel, req)
 	case pasturePb.SingleFactorAnalysisMethod_Breeding_Cycle:
-		list, err = s.SingleFactorAnalysisMethodBreedingCycle(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodBreedingCycle(userModel, req)
 	case pasturePb.SingleFactorAnalysisMethod_Week:
-		list, err = s.SingleFactorAnalysisMethodWeek(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodWeek(userModel, req)
 	case pasturePb.SingleFactorAnalysisMethod_Lact:
-		list, err = s.SingleFactorAnalysisMethodLact(userModel.AppPasture.Id, req)
+		list, err = s.SingleFactorAnalysisMethodLact(userModel, req)
 	default:
-		return nil, xerr.Custom("错误的统计方式")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "analysis.wrongMethod",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	if err != nil {
@@ -108,7 +116,7 @@ func (s *StoreEntry) SingleFactorInfantSurvivalRateAnalysis(ctx context.Context,
 	}, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodCycle(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodCycle(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	dateTimeRange, err := util.GetRangeDayByDays(req.StartDayTime, req.EndDayTime, req.Value)
 	if err != nil {
 		return nil, xerr.WithStack(err)
@@ -139,7 +147,7 @@ func (s *StoreEntry) SingleFactorAnalysisMethodCycle(pastureId int64, req *pastu
 		UNION ALL `, v[0], v[1], pasturePb.MatingResult_Pregnant, pasturePb.MatingResult_Empty, pasturePb.MatingResult_Abort,
 			pasturePb.MatingResult_Unknown, pasturePb.MatingResult_ReMatch, pasturePb.MatingResult_Pregnant,
 			pasturePb.MatingResult_Empty, pasturePb.MatingResult_Unknown, pasturePb.MatingResult_ReMatch,
-			pastureId, pasturePb.IsShow_Ok, startDayTimeUnix, endDayTimeUnix)
+			userModel.AppPasture.Id, pasturePb.IsShow_Ok, startDayTimeUnix, endDayTimeUnix)
 	}
 
 	if len(selectSql) > 0 {
@@ -153,11 +161,14 @@ func (s *StoreEntry) SingleFactorAnalysisMethodCycle(pastureId int64, req *pastu
 	return res, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodMonths(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodMonths(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	startDayTimeUnix := util.TimeParseLocalUnix(req.StartDayTime)
 	endDayTimeUnix := util.TimeParseLocalEndUnix(req.EndDayTime)
 	if startDayTimeUnix == 0 || endDayTimeUnix == 0 || endDayTimeUnix <= startDayTimeUnix {
-		return nil, xerr.Custom("开始时间不能大于结束时间")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.startEndDateTime",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	res := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
@@ -178,7 +189,7 @@ func (s *StoreEntry) SingleFactorAnalysisMethodMonths(pastureId int64, req *past
 			pasturePb.MatingResult_Unknown, pasturePb.MatingResult_ReMatch, pasturePb.MatingResult_Pregnant,
 			pasturePb.MatingResult_Empty, pasturePb.MatingResult_Unknown, pasturePb.MatingResult_ReMatch).
 		Where("status = ?", pasturePb.IsShow_Ok).
-		Where("pasture_id = ?", pastureId).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
 		Where("reality_day BETWEEN ? AND ?", startDayTimeUnix, endDayTimeUnix)
 	if req.CowType > 0 {
 		pref.Where("cow_type = ?", req.CowType)
@@ -205,22 +216,27 @@ func (s *StoreEntry) SingleFactorAnalysisMethodMonths(pastureId int64, req *past
 		case pasturePb.CompareSymbol_Between:
 			pref.Where("lact BETWEEN ? AND ? ", req.LactIntervalStartValue, req.LactIntervalEndValue)
 		default:
-			return nil, xerr.Custom("错误的胎次区间符号")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "analysis.wrongLact",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 	}
 
-	if err := pref.Group("months").Order("months").Find(&res).Error; err != nil {
+	if err := pref.Group("months").
+		Order("months").
+		Find(&res).Error; err != nil {
 		return nil, xerr.WithStack(err)
 	}
 	return res, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodMatingTimes(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodMatingTimes(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	res := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
 	return res, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodBreeding(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodBreeding(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	startDayTimeUnix := util.TimeParseLocalUnix(req.StartDayTime)
 	endDayTimeUnix := util.TimeParseLocalEndUnix(req.EndDayTime)
 	res := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
@@ -249,7 +265,7 @@ func (s *StoreEntry) SingleFactorAnalysisMethodBreeding(pastureId int64, req *pa
 			pasturePb.MatingResult_Unknown, pasturePb.MatingResult_ReMatch, pasturePb.MatingResult_Pregnant,
 			pasturePb.MatingResult_Empty, pasturePb.MatingResult_Unknown, pasturePb.MatingResult_ReMatch,
 		).Where("status = ?", pasturePb.IsShow_Ok).
-		Where("pasture_id = ?", pastureId).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
 		Where("reality_day BETWEEN ? AND ?", startDayTimeUnix, endDayTimeUnix).
 		Where("cow_type = ?", req.CowType).
 		Group("expose_estrus_type").
@@ -260,7 +276,7 @@ func (s *StoreEntry) SingleFactorAnalysisMethodBreeding(pastureId int64, req *pa
 	return res, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodBreedingCompany(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodBreedingCompany(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	res := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
 	startDayTimeUnix := util.TimeParseLocalUnix(req.StartDayTime)
 	endDayTimeUnix := util.TimeParseLocalEndUnix(req.EndDayTime)
@@ -304,11 +320,14 @@ func (s *StoreEntry) SingleFactorAnalysisMethodBreedingCompany(pastureId int64,
 		case pasturePb.CompareSymbol_Between:
 			pref.Where("a.lact BETWEEN ? AND ? ", req.LactIntervalStartValue, req.LactIntervalEndValue)
 		default:
-			return nil, xerr.Custom("错误的胎次区间符号")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "analysis.wrongLact",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 	}
 
-	if err := pref.Where("a.status = ?", pasturePb.IsShow_Ok).Where("pasture_id = ?", pastureId).
+	if err := pref.Where("a.status = ?", pasturePb.IsShow_Ok).Where("pasture_id = ?", userModel.AppPasture.Id).
 		Where("a.reality_day BETWEEN ? AND ?", startDayTimeUnix, endDayTimeUnix).
 		Group("b.producer").
 		Find(&res).Error; err != nil {
@@ -318,7 +337,7 @@ func (s *StoreEntry) SingleFactorAnalysisMethodBreedingCompany(pastureId int64,
 	return res, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodOperation(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodOperation(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	res := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
 	startDayTimeUnix := util.TimeParseLocalUnix(req.StartDayTime)
 	endDayTimeUnix := util.TimeParseLocalEndUnix(req.EndDayTime)
@@ -360,12 +379,15 @@ func (s *StoreEntry) SingleFactorAnalysisMethodOperation(pastureId int64, req *p
 		case pasturePb.CompareSymbol_Between:
 			pref.Where("lact BETWEEN ? AND ? ", req.LactIntervalStartValue, req.LactIntervalEndValue)
 		default:
-			return nil, xerr.Custom("错误的胎次区间符号")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "analysis.wrongLact",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 	}
 
 	if err := pref.Where("status = ?", pasturePb.IsShow_Ok).
-		Where("pasture_id = ?", pastureId).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
 		Where("reality_day BETWEEN ? AND ?", startDayTimeUnix, endDayTimeUnix).
 		Group("operation_id").
 		Find(&res).Error; err != nil {
@@ -375,12 +397,12 @@ func (s *StoreEntry) SingleFactorAnalysisMethodOperation(pastureId int64, req *p
 	return res, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodMatingInterval(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodMatingInterval(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	res := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
 	return res, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodBull(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodBull(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	res := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
 	startDayTimeUnix := util.TimeParseLocalUnix(req.StartDayTime)
 	endDayTimeUnix := util.TimeParseLocalEndUnix(req.EndDayTime)
@@ -423,7 +445,10 @@ func (s *StoreEntry) SingleFactorAnalysisMethodBull(pastureId int64, req *pastur
 		case pasturePb.CompareSymbol_Between:
 			pref.Where("lact BETWEEN ? AND ? ", req.LactIntervalStartValue, req.LactIntervalEndValue)
 		default:
-			return nil, xerr.Custom("错误的胎次区间符号")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "analysis.wrongLact",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 	}
 
@@ -438,12 +463,12 @@ func (s *StoreEntry) SingleFactorAnalysisMethodBull(pastureId int64, req *pastur
 	return res, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodBreedingCycle(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodBreedingCycle(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	res := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
 	return res, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodWeek(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodWeek(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	res := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
 	startDayTimeUnix := util.TimeParseLocalUnix(req.StartDayTime)
 	endDayTimeUnix := util.TimeParseLocalEndUnix(req.EndDayTime)
@@ -496,12 +521,15 @@ func (s *StoreEntry) SingleFactorAnalysisMethodWeek(pastureId int64, req *pastur
 		case pasturePb.CompareSymbol_Between:
 			pref.Where("lact BETWEEN ? AND ? ", req.LactIntervalStartValue, req.LactIntervalEndValue)
 		default:
-			return nil, xerr.Custom("错误的胎次区间符号")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "analysis.wrongLact",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 	}
 
 	if err := pref.Where("status = ?", pasturePb.IsShow_Ok).
-		Where("pasture_id = ?", pastureId).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
 		Where("reality_day BETWEEN ? AND ?", startDayTimeUnix, endDayTimeUnix).
 		Group("statistic_method").
 		Find(&res).Error; err != nil {
@@ -511,7 +539,7 @@ func (s *StoreEntry) SingleFactorAnalysisMethodWeek(pastureId int64, req *pastur
 	return res, nil
 }
 
-func (s *StoreEntry) SingleFactorAnalysisMethodLact(pastureId int64, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
+func (s *StoreEntry) SingleFactorAnalysisMethodLact(userModel *model.UserModel, req *pasturePb.SingleFactorPregnancyRateRequest) ([]*pasturePb.SingleFactorPregnancyRateList, error) {
 	res := make([]*pasturePb.SingleFactorPregnancyRateList, 0)
 	startDayTimeUnix := util.TimeParseLocalUnix(req.StartDayTime)
 	endDayTimeUnix := util.TimeParseLocalEndUnix(req.EndDayTime)
@@ -538,7 +566,7 @@ func (s *StoreEntry) SingleFactorAnalysisMethodLact(pastureId int64, req *pastur
 	}
 
 	if err := pref.Where("status = ?", pasturePb.IsShow_Ok).
-		Where("pasture_id = ?", pastureId).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
 		Where("reality_day BETWEEN ? AND ?", startDayTimeUnix, endDayTimeUnix).
 		Group("lact").
 		Find(&res).Error; err != nil {
@@ -556,15 +584,24 @@ func (s *StoreEntry) MultipleFactorAnalysis(ctx context.Context, req *pasturePb.
 	startTimeUnix := util.TimeParseLocalUnix(req.StartDayTime)
 	endTimeUnix := util.TimeParseLocalEndUnix(req.EndDayTime)
 	if startTimeUnix == 0 || endTimeUnix == 0 || endTimeUnix <= startTimeUnix {
-		return nil, xerr.Custom("开始时间不能大于结束时间")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "analysis.wrongDateRange",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	if req.XAxle == req.YAxle {
-		return nil, xerr.Custom("X轴和Y轴不能相同")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "analysis.xAndY",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	if req.XAxle == 0 || req.YAxle == 0 {
-		return nil, xerr.Custom("错误的XY轴数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "analysis.wrongXY",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	pref := s.DB.Model(new(model.EventMating)).

+ 14 - 3
module/backend/analysis_more.go

@@ -9,6 +9,8 @@ import (
 	"sort"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"gitee.com/xuyiping_admin/pkg/xerr"
 
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
@@ -21,7 +23,10 @@ func (s *StoreEntry) PenBehavior(ctx context.Context, req *pasturePb.BarnBehavio
 	}
 
 	if req.StartAt == 0 || req.EndAt == 0 || req.EndAt < req.StartAt {
-		return nil, xerr.Customf("时间范围错误")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.errorDateRange",
+		})
+		return nil, xerr.Customf(messageId)
 	}
 	startTime := time.Unix(int64(req.StartAt), 0).Local().Format(model.LayoutDate2)
 	endTime := time.Unix(int64(req.EndAt), 0).Local().Format(model.LayoutDate2)
@@ -46,7 +51,10 @@ func (s *StoreEntry) PenBehaviorDaily(ctx context.Context, req *pasturePb.BarnMo
 		return nil, xerr.WithStack(err)
 	}
 	if req.StartAt == 0 || req.EndAt == 0 || req.EndAt < req.StartAt {
-		return nil, xerr.Customf("时间范围错误")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.errorDateRange",
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	startDate := time.Unix(int64(req.StartAt), 0).Local().Format(model.LayoutDate2)
@@ -106,7 +114,10 @@ func (s *StoreEntry) CowBehaviorDistribution(ctx context.Context, req *pasturePb
 
 	// 校验时间必须比当天时间小一天
 	if time.Now().Local().Format(model.LayoutDate2) == req.DateTime {
-		return nil, xerr.Customf("时间范围错误")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.errorDateRange",
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	milkDailList := make([]*model.MilkDaily, 0)

+ 4 - 4
module/backend/analysis_other.go

@@ -306,13 +306,13 @@ func (s *StoreEntry) SaleCowReport(ctx context.Context, req *pasturePb.SaleCowRe
 		Where("sale_at BETWEEN ? AND ?", startDayTimeUnix, endDayTimeUnix).
 		Where("pasture_id = ?", userModel.AppPasture.Id)
 	if req.AnalysisMethod == pasturePb.SaleCowAnalysisMethod_Months {
-		pref.Select(`ROUND(SUM(sale_all_amount) /100,2) AS sale_all_amount,SUM(sale_cow_count) AS sale_all_count,
-		SUM(sale_all_weight) AS sale_all_weight,DATE_FORMAT(FROM_UNIXTIME(sale_at), '%Y-%m') AS statistic_method`)
+		pref.Select(`ROUND(SUM(sale_all_amount)/100,2) AS sale_all_amount,SUM(sale_cow_count) AS sale_all_count,
+		ROUND(SUM(sale_all_weight)/1000,2 ) AS sale_all_weight,DATE_FORMAT(FROM_UNIXTIME(sale_at), '%Y-%m') AS statistic_method`)
 	}
 
 	if req.AnalysisMethod == pasturePb.SaleCowAnalysisMethod_Dealer {
-		pref.Select(`ROUND(SUM(sale_all_amount) /100,2) AS sale_all_amount,SUM(sale_cow_count) AS sale_all_count,
-		SUM(sale_all_weight) AS sale_all_weight,dealer_name as statistic_method`)
+		pref.Select(`ROUND(SUM(sale_all_amount)/100,2) AS sale_all_amount,SUM(sale_cow_count) AS sale_all_count,
+		ROUND(SUM(sale_all_weight)/1000,2) AS sale_all_weight,dealer_name as statistic_method`)
 	}
 
 	if err = pref.Group("statistic_method").

+ 41 - 23
module/backend/calendar.go

@@ -45,27 +45,34 @@ func (s *StoreEntry) CalendarToDoHistoryList(ctx context.Context, pastureId int6
 		DATE_FORMAT(FROM_UNIXTIME(a.plan_day), '%Y-%m-%d') AS plan_day,
 		IF(a.end_day <= 0, '', DATE_FORMAT(FROM_UNIXTIME(a.end_day), '%Y-%m-%d')) AS end_day,
 		IF(a.reality_day <= 0, '', DATE_FORMAT(FROM_UNIXTIME(a.reality_day), '%Y-%m-%d')) AS reality_day,
-		a.remaining_days,b.lact,b.ear_number,a.status as is_finish,a.remarks
+		a.remaining_days,b.lact,b.ear_number,a.status as is_finish,a.remarks,a.operation_name AS operator_name
 		FROM (
-			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,'免疫' as calendar_type_name,1 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
+			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,operation_name,'免疫' as calendar_type_name,
+				1 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
 			FROM event_immunization_plan WHERE ` + whereSql1 + `
 			  UNION ALL
-			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,'同期' as calendar_type_name,2 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
+			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,operation_name,'同期' as calendar_type_name,
+				2 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
 			FROM event_cow_same_time WHERE ` + whereSql1 + `
 			  UNION ALL
-			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,'孕检' as calendar_type_name,4 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
+			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,operation_name,'孕检' as calendar_type_name,
+				4 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
 			FROM event_pregnant_check WHERE ` + whereSql1 + `
 			  UNION ALL
-			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,'断奶' as calendar_type_name,6 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
+			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,operation_name,'断奶' as calendar_type_name,
+				6 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
 			FROM event_weaning WHERE ` + whereSql1 + `
 			  UNION ALL
-			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,'配种' as calendar_type_name,8 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
+			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,operation_name,'配种' as calendar_type_name,
+				8 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
 			FROM event_mating WHERE ` + whereSql1 + `
 			  UNION ALL
-			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,'产犊' as calendar_type_name,9 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
+			SELECT cow_id,plan_day,end_day,reality_day,status,remarks,operation_name,'产犊' as calendar_type_name,
+				9 as calendar_type_kind,TIMESTAMPDIFF(DAY, NOW(), FROM_UNIXTIME(end_day)) AS remaining_days 
 			FROM event_calving WHERE ` + whereSql1 + `
 			  UNION ALL
-			SELECT cow_id,disease_at as plan_day,curable_at as end_day,curable_at as reality_day,health_status as status,remarks,'疾病' as calendar_type_name,7 as calendar_type_kind,0 AS remaining_days 
+			SELECT cow_id,disease_at as plan_day,curable_at as end_day,curable_at as reality_day,health_status as status,
+				remarks,'' as operation_name,'疾病' as calendar_type_name,7 as calendar_type_kind,0 AS remaining_days
 			FROM event_cow_disease WHERE health_status IN (2,3) AND ` + whereSql + `
 		) as a 
 	JOIN cow b ON a.cow_id = b.id `
@@ -292,10 +299,12 @@ 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,
-			HeaderSort: []string{"planDay", "planName", "penName", "dayAge", "earNumber", "planId"},
+			Total:    int32(count),
+			Page:     pagination.Page,
+			PageSize: pagination.PageSize,
+			HeaderSort: []string{
+				"planDay", "planName", "penName", "dayAge", "earNumber", "planId",
+			},
 			Header: map[string]string{
 				"earNumber": "耳标号",
 				"penName":   "栏舍",
@@ -358,9 +367,11 @@ func (s *StoreEntry) SameTimeCowList(ctx context.Context, req *pasturePb.ItemsRe
 			Total:    int32(count),
 			Page:     pagination.Page,
 			PageSize: pagination.PageSize,
-			HeaderSort: []string{"earNumber", "breedStatusName", "cowTypeName", "planDayAtFormat", "penName",
-				"lact", "calvingAge", "abortionAge", "dayAge", "status", "sameTimeTypeName", "matingTimes", "calvingAtFormat",
-				"abortionAtFormat", "sameTimeName"},
+			HeaderSort: []string{
+				"earNumber", "breedStatusName", "cowTypeName", "planDayAtFormat", "penName", "lact",
+				"calvingAge", "abortionAge", "dayAge", "status", "sameTimeTypeName", "matingTimes",
+				"calvingAtFormat", "abortionAtFormat", "sameTimeName",
+			},
 			Header: map[string]string{
 				"earNumber":        "耳标号",
 				"breedStatusName":  "繁殖状态",
@@ -450,8 +461,11 @@ func (s *StoreEntry) PregnancyCheckCowList(ctx context.Context, req *pasturePb.I
 			Total:    int32(count),
 			Page:     pagination.Page,
 			PageSize: pagination.PageSize,
-			HeaderSort: []string{"earNumber", "cowTypeName", "penName", "lact", "dayAge", "breedStatus", "planDay",
-				"checkTypeName", "status", "matingTimes", "calvingAtFormat", "matingAtFormat", "matingAge", "bullId", "pregnancyAge"},
+			HeaderSort: []string{
+				"earNumber", "cowTypeName", "penName", "lact", "dayAge", "breedStatus", "planDay",
+				"checkTypeName", "status", "matingTimes", "calvingAtFormat", "matingAtFormat",
+				"matingAge", "bullId", "pregnancyAge",
+			},
 			Header: map[string]string{
 				"earNumber":       "耳标号",
 				"cowTypeName":     "牛只类型",
@@ -507,10 +521,12 @@ 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,
-			HeaderSort: []string{"earNumber", "penName", "dayAge", "planDayFormat", "birthAtFormat", "currentWeight"},
+			Total:    int32(count),
+			Page:     pagination.Page,
+			PageSize: pagination.PageSize,
+			HeaderSort: []string{
+				"earNumber", "penName", "dayAge", "planDayFormat", "birthAtFormat", "currentWeight",
+			},
 			Header: map[string]string{
 				"earNumber":     "耳标号",
 				"penName":       "栏舍",
@@ -578,8 +594,10 @@ func (s *StoreEntry) MatingCowList(ctx context.Context, req *pasturePb.ItemsRequ
 			Total:    int32(count),
 			Page:     pagination.Page,
 			PageSize: pagination.PageSize,
-			HeaderSort: []string{"earNumber", "dayAge", "lact", "penName", "planDay", "breedStatusName",
-				"cowTypeName", "calvingAge", "abortionAge", "exposeEstrusTypeName", "lastCalvingAtFormat"},
+			HeaderSort: []string{
+				"earNumber", "dayAge", "lact", "penName", "planDay", "breedStatusName", "cowTypeName",
+				"calvingAge", "abortionAge", "exposeEstrusTypeName", "lastCalvingAtFormat",
+			},
 			Header: map[string]string{
 				"earNumber":            "耳标号",
 				"breedStatusName":      "繁殖状态",

+ 4 - 4
module/backend/config_data.go

@@ -126,7 +126,7 @@ func (s *StoreEntry) CowTypeEnumList(optionName, isAll string) []*pasturePb.Conf
 			Disabled: true,
 		}, &pasturePb.ConfigOptionsList{
 			Value:    int32(pasturePb.CowType_Breeding_Calf),
-			Label:    "母牛",
+			Label:    "母牛",
 			Disabled: true,
 		})
 		return cowTypeList
@@ -154,7 +154,7 @@ func (s *StoreEntry) CowTypeEnumList(optionName, isAll string) []*pasturePb.Conf
 		Disabled: true,
 	}, &pasturePb.ConfigOptionsList{
 		Value:    int32(pasturePb.CowType_Breeding_Calf),
-		Label:    "母牛",
+		Label:    "母牛",
 		Disabled: true,
 	}, &pasturePb.ConfigOptionsList{
 		Value:    int32(pasturePb.CowType_Breeding_Bull),
@@ -179,7 +179,7 @@ func (s *StoreEntry) SameTimeCowTypeEnumList(isAll string) []*pasturePb.ConfigOp
 		Disabled: true,
 	}, &pasturePb.ConfigOptionsList{
 		Value:    int32(pasturePb.SameTimeCowType_Breeding_Calf),
-		Label:    "母牛",
+		Label:    "母牛",
 		Disabled: true,
 	})
 	return cowTypeList
@@ -238,7 +238,7 @@ func (s *StoreEntry) ImmunizationCowTypeEnumList(isAll string) []*pasturePb.Conf
 		Disabled: true,
 	}, &pasturePb.ConfigOptionsList{
 		Value:    int32(pasturePb.CowType_Breeding_Calf),
-		Label:    "母牛",
+		Label:    "母牛",
 		Disabled: true,
 	}, &pasturePb.ConfigOptionsList{
 		Value:    int32(pasturePb.CowType_Breeding_Bull),

+ 4 - 4
module/backend/config_data_base.go

@@ -11,7 +11,7 @@ func (s *StoreEntry) OutReasonEnumList(isAll string) []*pasturePb.ConfigOptionsL
 	if isAll == model.IsAllYes {
 		configOptions = append(configOptions,
 			&pasturePb.ConfigOptionsList{
-				Value:    int32(0),
+				Value:    int32(pasturePb.OutReasons_Invalid),
 				Label:    "全部",
 				Disabled: true,
 			})
@@ -121,7 +121,7 @@ func (s *StoreEntry) DeathReasonEnumList(isAll string) []*pasturePb.ConfigOption
 	if isAll == model.IsAllYes {
 		configOptions = append(configOptions,
 			&pasturePb.ConfigOptionsList{
-				Value:    int32(0),
+				Value:    int32(pasturePb.DeathReason_Invalid),
 				Label:    "全部",
 				Disabled: true,
 			})
@@ -199,7 +199,7 @@ func (s *StoreEntry) MatingResultEnumList(isAll string) []*pasturePb.ConfigOptio
 	if isAll == model.IsAllYes {
 		configOptions = append(configOptions,
 			&pasturePb.ConfigOptionsList{
-				Value:    int32(0),
+				Value:    int32(pasturePb.MatingResult_Invalid),
 				Label:    "全部",
 				Disabled: true,
 			})
@@ -233,7 +233,7 @@ func (s *StoreEntry) EventCategoryEnumList(isAll string) []*pasturePb.ConfigOpti
 	if isAll == model.IsAllYes {
 		configOptions = append(configOptions,
 			&pasturePb.ConfigOptionsList{
-				Value:    int32(0),
+				Value:    int32(pasturePb.EventCategory_Invalid),
 				Label:    "全部",
 				Disabled: true,
 			})

+ 18 - 1
module/backend/config_data_breed.go

@@ -328,6 +328,23 @@ func CalendarTypeEnumList(isAll string) []*pasturePb.ConfigOptionsList {
 }
 
 func (s *StoreEntry) AbortionReasonsEnumList(isAll string) []*pasturePb.ConfigOptionsList {
+	/*configAbortionReasonList := make([]*model.ConfigAbortionReasons, 0)
+	pref := s.DB.Model(new(model.ConfigAbortionReasons)).
+		Where("is_show =? ", pasturePb.IsShow_Ok).
+		Where("pasture_id =? ", userModel.AppPasture.Id)
+	if isAll == model.IsAllYes {
+		pref = pref.Where("kind >= ?", pasturePb.AbortionReasons_Invalid)
+	} else {
+		pref = pref.Where("kind > ?", pasturePb.AbortionReasons_Invalid)
+	}
+
+	if err := pref.Order("kind ASC").
+		Find(&configAbortionReasonList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return model.ConfigAbortionReasonsSlice(configAbortionReasonList).ToPB(userModel.Language)*/
+
 	configOptions := make([]*pasturePb.ConfigOptionsList, 0)
 	if isAll == model.IsAllYes {
 		configOptions = append(configOptions, &pasturePb.ConfigOptionsList{
@@ -342,7 +359,7 @@ func (s *StoreEntry) AbortionReasonsEnumList(isAll string) []*pasturePb.ConfigOp
 		Disabled: true,
 	}, &pasturePb.ConfigOptionsList{
 		Value:    int32(pasturePb.AbortionReasons_Malnutrition_Abortion),
-		Label:    "营养不良流产",
+		Label:    "营养不良流产",
 		Disabled: true,
 	}, &pasturePb.ConfigOptionsList{
 		Value:    int32(pasturePb.AbortionReasons_Mycotoxin_Abortion),

+ 59 - 16
module/backend/cow.go

@@ -12,6 +12,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"gorm.io/gorm"
 
 	"gitee.com/xuyiping_admin/pkg/xerr"
@@ -26,7 +28,10 @@ func (s *StoreEntry) Detail(ctx context.Context, req *pasturePb.SearchEventReque
 	}
 
 	if req.EarNumber == "" && req.NeckRingNumber == "" {
-		return nil, xerr.Custom("请输入牛号或项圈号")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.inputCow",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	cowInfo := &model.Cow{}
@@ -34,16 +39,19 @@ func (s *StoreEntry) Detail(ctx context.Context, req *pasturePb.SearchEventReque
 		Where("pasture_id = ?", userModel.AppPasture.Id)
 
 	if req.EarNumber != "" {
-		pref.Where("ear_number = ?", req.EarNumber)
+		pref.Where("ear_number = ?", strings.TrimSpace(req.EarNumber))
 	}
 
 	if req.NeckRingNumber != "" {
-		pref.Where("neck_ring_number = ?", req.NeckRingNumber)
+		pref.Where("neck_ring_number = ?", strings.TrimSpace(req.NeckRingNumber))
 	}
 
 	if err = pref.First(cowInfo).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return nil, xerr.Custom("该牛只未找到")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "cow.noCow",
+			})
+			return nil, xerr.Custom(messageId)
 		} else {
 			return nil, xerr.WithStack(err)
 		}
@@ -58,7 +66,10 @@ func (s *StoreEntry) Detail(ctx context.Context, req *pasturePb.SearchEventReque
 	purposeMap := s.PurposeMap()
 	systemBasic, err := s.GetSystemBasicByName(ctx, userModel.AppPasture.Id, model.PregnancyAge)
 	if err != nil {
-		return nil, xerr.Custom("请在基础参数配置妊娠天数")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.pregnancyDays",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	cowDetails := model.CowSlice([]*model.Cow{cowInfo}).ToPB(
@@ -66,7 +77,10 @@ func (s *StoreEntry) Detail(ctx context.Context, req *pasturePb.SearchEventReque
 		admissionStatusMap, healthStatusMap, purposeMap, systemBasic.MinValue,
 	)
 	if len(cowDetails) != 1 {
-		return nil, xerr.Custom("该牛只未找到")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.noCow",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	data := cowDetails[0]
@@ -85,7 +99,10 @@ func (s *StoreEntry) List(ctx context.Context, req *pasturePb.SearchEventRequest
 
 	systemBasic, err := s.GetSystemBasicByName(ctx, userModel.AppPasture.Id, model.PregnancyAge)
 	if err != nil {
-		return nil, xerr.Custom("请在基础参数配置妊娠天数")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.pregnancyDays",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	cowList := make([]*model.Cow, 0)
@@ -172,7 +189,11 @@ func (s *StoreEntry) EventList(ctx context.Context, req *pasturePb.SearchCowEven
 	eventCowLogList := make([]*model.EventCowLog, 0)
 	cowInfo, err := s.GetCowEventByEarNumber(ctx, userModel.AppPasture.Id, req.EarNumber)
 	if err != nil {
-		return nil, xerr.Customf("错误的牛只信息: %s", req.EarNumber)
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "auth.errorCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	eventCowLog := &model.EventCowLog{CowId: cowInfo.Id}
@@ -215,13 +236,17 @@ func (s *StoreEntry) BehaviorCurve(ctx context.Context, req *pasturePb.CowBehavi
 	}
 	cowInfo, err := s.GetCowEventByEarNumber(ctx, userModel.AppPasture.Id, req.EarNumber)
 	if err != nil {
-		return nil, xerr.Customf("错误的牛只信息: %d", req.CowId)
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "auth.errorCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	nowTime := time.Now().Local()
 	nowDayZero := util.TimeParseLocalUnix(nowTime.Format(model.LayoutDate2))
 	endDataTime := nowTime.Format(model.LayoutDate2)
-	startDataTime := nowTime.AddDate(0, 0, -30).Format(model.LayoutDate2)
+	startDataTime := nowTime.AddDate(0, 0, -50).Format(model.LayoutDate2)
 
 	dayRange, err := util.GetDaysBetween(startDataTime, endDataTime)
 	if err != nil {
@@ -229,7 +254,10 @@ func (s *StoreEntry) BehaviorCurve(ctx context.Context, req *pasturePb.CowBehavi
 	}
 
 	if len(dayRange) <= 0 {
-		return nil, xerr.Customf("错误的日期范围")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.errorDateRange",
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	// 行为曲线数据
@@ -302,6 +330,7 @@ func (s *StoreEntry) BehaviorCurve(ctx context.Context, req *pasturePb.CowBehavi
 		Where("pasture_id = ?", userModel.AppPasture.Id).
 		Where("active_time BETWEEN ? AND ?", fmt.Sprintf("%s 00:00:00", startDataTime), fmt.Sprintf("%s 23:59:59", endDataTime)).
 		Where("is_peak = ?", pasturePb.IsShow_Ok).
+		Group("first_time").
 		Find(&estrusList).Error; err != nil {
 		return nil, xerr.WithStack(err)
 	}
@@ -499,7 +528,11 @@ func (s *StoreEntry) CowGrowthCurve(ctx context.Context, req *pasturePb.CowGrowt
 	}
 	cowInfo, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, req.EarNumber)
 	if err != nil {
-		return nil, xerr.Customf("错误的牛只信息: %s", req.EarNumber)
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "auth.errorCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	weightList := make([]*model.EventWeight, 0)
@@ -535,7 +568,11 @@ func (s *StoreEntry) CowLactCurve(ctx context.Context, req *pasturePb.CowLactCur
 	}
 	cowInfo, err := s.GetCowInfoByCowId(ctx, userModel.AppPasture.Id, int64(req.CowId))
 	if err != nil {
-		return nil, xerr.Customf("错误的牛只信息: %d", req.CowId)
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "auth.errorCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	cowLactList := make([]*model.CowLact, 0)
@@ -607,14 +644,20 @@ func (s *StoreEntry) BehaviorRate(ctx context.Context, req *pasturePb.CowBehavio
 	if err != nil {
 		return nil, xerr.WithStack(err)
 	}
-
 	cowInfo, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, req.EarNumber)
 	if err != nil {
-		return nil, xerr.Customf("错误的牛只信息: %s", req.EarNumber)
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "auth.errorCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	if req.EndAt <= 0 || req.StartAt <= 0 || req.EndAt < req.StartAt {
-		return nil, xerr.Customf("时间范围错误")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.errorDateRange",
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	t1 := time.Unix(int64(req.StartAt), 0).Local().Format(model.LayoutDate2)

+ 52 - 23
module/backend/dashboard.go

@@ -11,6 +11,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"gorm.io/gorm"
 
 	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
@@ -28,12 +30,18 @@ func (s *StoreEntry) DataWarningSet(ctx context.Context, req *pasturePb.IndexDat
 	pastureId := userModel.AppPasture.Id
 
 	if len(req.WarningDataSet) <= 0 {
-		return xerr.Custom("请选择预警数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "dataWarning.selectData",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	defaultDataWarning, _ := s.FindDataWarning(ctx, pastureId, model.DefaultUserId)
 	if len(defaultDataWarning) <= 0 {
-		return xerr.Custom("默认预警数据不存在,请联系管理员!")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "dataWarning.defaultDataNotExist",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	userDataWarningList, err := s.FindDataWarning(ctx, pastureId, userModel.SystemUser.Id)
@@ -42,9 +50,9 @@ func (s *StoreEntry) DataWarningSet(ctx context.Context, req *pasturePb.IndexDat
 	}
 
 	if len(userDataWarningList) <= 0 { // 新增
-		return s.addUserDataWarning(ctx, pastureId, userModel.SystemUser.Id, defaultDataWarning, req.WarningDataSet)
+		return s.addUserDataWarning(userModel, defaultDataWarning, req.WarningDataSet)
 	}
-	return s.updateUserDataWarning(ctx, pastureId, userModel.SystemUser.Id, userDataWarningList, req.WarningDataSet)
+	return s.updateUserDataWarning(userModel, userDataWarningList, req.WarningDataSet)
 }
 
 func (s *StoreEntry) DataWarningList(ctx context.Context) (*pasturePb.IndexDataWarningResponse, error) {
@@ -56,7 +64,10 @@ func (s *StoreEntry) DataWarningList(ctx context.Context) (*pasturePb.IndexDataW
 	pastureId := userModel.AppPasture.Id
 	defaultDataWarning, _ := s.FindDataWarning(ctx, pastureId, model.DefaultUserId)
 	if len(defaultDataWarning) <= 0 {
-		return nil, xerr.Custom("默认预警数据有误,请联系管理员!")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "dataWarning.defaultDataError",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	var isExist bool // 判断是否存在自己的设置的数据
@@ -79,17 +90,17 @@ func (s *StoreEntry) DataWarningList(ctx context.Context) (*pasturePb.IndexDataW
 
 	// 需要重新计算更新的warningId
 	if len(needUpdateWarningIds) > 0 {
-		s.UpdateWarningData(ctx, pastureId, needUpdateWarningIds)
+		s.UpdateWarningData(userModel, needUpdateWarningIds)
 	}
 
 	userDataWarningItems := make([]*model.DataWarningItems, 0)
 	// 计算过后重新获取数据
 	if isExist {
 		userDataWarning, _ = s.FindDataWarning(ctx, pastureId, model.DefaultUserId)
-		userDataWarningItems, _ = s.FindDataWarningItems(ctx, pastureId, model.DefaultUserId)
+		userDataWarningItems, _ = s.FindDataWarningItems(pastureId, model.DefaultUserId)
 	} else {
 		userDataWarning, _ = s.FindDataWarning(ctx, pastureId, userModel.SystemUser.Id)
-		userDataWarningItems, _ = s.FindDataWarningItems(ctx, pastureId, userModel.SystemUser.Id)
+		userDataWarningItems, _ = s.FindDataWarningItems(pastureId, userModel.SystemUser.Id)
 	}
 
 	return &pasturePb.IndexDataWarningResponse{
@@ -109,7 +120,10 @@ func (s *StoreEntry) DataWarningPop(ctx context.Context, req *pasturePb.WarningD
 	}
 
 	if req.Kind <= pasturePb.DataWarningType_Invalid {
-		return nil, xerr.Custom("请选择预警数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "dataWarning.selectData",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	pastureId := userModel.AppPasture.Id
@@ -128,11 +142,14 @@ func (s *StoreEntry) DataWarningPop(ctx context.Context, req *pasturePb.WarningD
 				return nil, xerr.WithStack(err)
 			}
 		} else {
-			return nil, xerr.Custom("预警数据不存在,请联系管理员!")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "dataWarning.dataNotExist",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 	}
 
-	headers, headerSort, err := dataWaringItem.GetWarningColumn()
+	headers, headerSort, err := dataWaringItem.GetWarningColumn(userModel.LanguageContent)
 	if err != nil {
 		return nil, xerr.WithStack(err)
 	}
@@ -152,7 +169,7 @@ func (s *StoreEntry) DataWarningPop(ctx context.Context, req *pasturePb.WarningD
 		},
 	}
 
-	query, params, err := s.BuildQuery(dataWaringItem.Id)
+	query, params, err := s.BuildQuery(userModel, dataWaringItem.Id)
 	if err != nil {
 		zaplog.Error("UpdateWarningData", zap.Any("BuildQuery", err), zap.Any("warningId", dataWaringItem.Id))
 		return resp, nil
@@ -183,7 +200,10 @@ func (s *StoreEntry) DataWarningPop(ctx context.Context, req *pasturePb.WarningD
 	resp.Data.Total = int32(count)
 	systemBasic, err := s.GetSystemBasicByName(ctx, userModel.AppPasture.Id, model.PregnancyAge)
 	if err != nil {
-		return nil, xerr.Custom("请在基础参数配置妊娠天数")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.pregnancyDays",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 	resp.Data.DataList = model.CowSlice(cowList).ToPB(
 		cowTypeMap, breedStatusMap, cowKindMap,
@@ -204,7 +224,7 @@ func (s *StoreEntry) FindDataWarning(ctx context.Context, pastureId, userId int6
 	return dataWarningList, nil
 }
 
-func (s *StoreEntry) FindDataWarningItems(ctx context.Context, pastureId, userId int64) ([]*model.DataWarningItems, error) {
+func (s *StoreEntry) FindDataWarningItems(pastureId, userId int64) ([]*model.DataWarningItems, error) {
 	dataWarningItemsList := make([]*model.DataWarningItems, 0)
 	if err := s.DB.Model(new(model.DataWarningItems)).
 		Where("pasture_id = ?", pastureId).
@@ -215,7 +235,7 @@ func (s *StoreEntry) FindDataWarningItems(ctx context.Context, pastureId, userId
 	return dataWarningItemsList, nil
 }
 
-func (s *StoreEntry) FindDataWarningMap(ctx context.Context, pastureId, userId int64) (map[int64]*model.DataWarning, error) {
+/*func (s *StoreEntry) FindDataWarningMap(ctx context.Context, pastureId, userId int64) (map[int64]*model.DataWarning, error) {
 	dataWarning, err := s.FindDataWarning(ctx, pastureId, userId)
 	if err != nil {
 		return nil, xerr.Custom("默认预警数据有误,请联系管理员!")
@@ -239,15 +259,15 @@ func (s *StoreEntry) FindDataWarningItemsMap(ctx context.Context, userId int64)
 		dataWarningItemsMap[v.Id] = v
 	}
 	return dataWarningItemsMap, nil
-}
+}*/
 
 // UpdateWarningData 更新计算数据
-func (s *StoreEntry) UpdateWarningData(ctx context.Context, pastureId int64, needUpdateWarningIds []int64) {
+func (s *StoreEntry) UpdateWarningData(userModel *model.UserModel, needUpdateWarningIds []int64) {
 	if len(needUpdateWarningIds) <= 0 {
 		return
 	}
 	for _, warningId := range needUpdateWarningIds {
-		query, params, err := s.BuildQuery(warningId)
+		query, params, err := s.BuildQuery(userModel, warningId)
 		if err != nil {
 			zaplog.Error("UpdateWarningData", zap.Any("BuildQuery", err), zap.Any("warningId", warningId))
 		}
@@ -259,7 +279,7 @@ func (s *StoreEntry) UpdateWarningData(ctx context.Context, pastureId int64, nee
 
 		var count int64
 		if err = s.DB.Model(new(model.Cow)).
-			Where("pasture_id = ?", pastureId).
+			Where("pasture_id = ?", userModel.AppPasture.Id).
 			Where(query, params...).
 			Count(&count).Error; err != nil {
 			zaplog.Error("UpdateWarningData", zap.Any("err", err), zap.Any("query", query), zap.Any("params", params))
@@ -277,13 +297,16 @@ func (s *StoreEntry) UpdateWarningData(ctx context.Context, pastureId int64, nee
 }
 
 // 新增用户预警数据
-func (s *StoreEntry) addUserDataWarning(ctx context.Context, pastureId, userId int64, defaultDataWarning []*model.DataWarning, warningDataSet []*pasturePb.WarningDataSet) error {
+func (s *StoreEntry) addUserDataWarning(userModel *model.UserModel, defaultDataWarning []*model.DataWarning, warningDataSet []*pasturePb.WarningDataSet) error {
 	// 将默认预警数据按 Kind 映射
 	defaultDataWarningMap := make(map[pasturePb.DataWarningType_Kind]*model.DataWarning)
 	for _, v := range defaultDataWarning {
 		defaultDataWarningMap[v.Kind] = v
 	}
 
+	pastureId := userModel.AppPasture.Id
+	userId := userModel.SystemUser.Id
+
 	// 在事务中执行新增操作
 	return s.DB.Transaction(func(tx *gorm.DB) error {
 		addedKinds := make(map[pasturePb.DataWarningType_Kind]bool) // 记录已添加的 Kind
@@ -320,7 +343,7 @@ func (s *StoreEntry) addUserDataWarning(ctx context.Context, pastureId, userId i
 }
 
 // 更新用户预警数据
-func (s *StoreEntry) updateUserDataWarning(ctx context.Context, pastureId, userId int64, userDataWarningList []*model.DataWarning, warningDataSet []*pasturePb.WarningDataSet) error {
+func (s *StoreEntry) updateUserDataWarning(userModel *model.UserModel, userDataWarningList []*model.DataWarning, warningDataSet []*pasturePb.WarningDataSet) error {
 	// 将请求数据按 WarningId 和 Id 映射
 	warningIsShowMap := make(map[int32]*pasturePb.WarningDataSet)
 	warningItemDataMap := make(map[int32]*pasturePb.WarningDataSet)
@@ -329,14 +352,20 @@ func (s *StoreEntry) updateUserDataWarning(ctx context.Context, pastureId, userI
 		warningItemDataMap[set.Id] = set
 	}
 
+	pastureId := userModel.AppPasture.Id
+	userId := userModel.SystemUser.Id
+
 	// 获取用户预警项数据
-	userDataWarningItems, err := s.FindDataWarningItems(ctx, pastureId, userId)
+	userDataWarningItems, err := s.FindDataWarningItems(pastureId, userId)
 	if err != nil {
 		return xerr.WithStack(err)
 	}
 
 	if len(userDataWarningItems) == 0 {
-		return xerr.Custom("预警数据有误,请联系管理员!")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "dataWarning.dataError",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	// 在事务中执行更新操作

+ 43 - 0
module/backend/dashboard_more.go

@@ -10,6 +10,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
 	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
 	"gitee.com/xuyiping_admin/pkg/xerr"
@@ -299,3 +301,44 @@ func (s *StoreEntry) Equipment(ctx context.Context) (*pasturePb.EquipmentRespons
 		Data: &pasturePb.EquipmentData{EquipmentList: equipmentList},
 	}, nil
 }
+
+func (s *StoreEntry) OutNumber(ctx context.Context) (*pasturePb.OutNumberResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	nowTime := time.Now().Local()
+	currentMonth := nowTime.Format(model.LayoutMonth)
+	startTime := time.Date(nowTime.Year(), nowTime.Month(), 1, 0, 0, 0, 0, nowTime.Location())
+	startMonth := startTime.AddDate(0, -5, 0).Format(model.LayoutMonth)
+	monthRang, err := util.GetMonthsBetween(startMonth, currentMonth)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	if len(monthRang) != 6 {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.errorDateRange",
+		})
+		return nil, xerr.Customf(messageId)
+	}
+
+	startAt := util.TimeParseLocalUnix(fmt.Sprintf("%s-01", monthRang[0]))
+	endDate, _ := util.GetLastDayOfMonth(monthRang[len(monthRang)-1])
+	endAt := util.TimeParseLocalEndUnix(endDate)
+
+	eventSaleList := make([]*model.EventSale, 0)
+	if err = s.DB.Model(new(model.EventSale)).
+		Select("sale_at, sale_cow_count").
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("sale_at BETWEEN ? AND ?", startAt, endAt).
+		Find(&eventSaleList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	return &pasturePb.OutNumberResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: model.EventSaleSlice(eventSaleList).ToPB2(monthRang),
+	}, nil
+}

+ 12 - 4
module/backend/data_warning.go

@@ -8,17 +8,23 @@ import (
 	"strings"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
 	"gitee.com/xuyiping_admin/pkg/xerr"
 )
 
 func (s *StoreEntry) TestDataWaring(ctx context.Context, userId int64) error {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return xerr.WithStack(err)
+	}
 	dataWarningList := make([]*model.DataWarning, 0)
 	if err := s.DB.Model(new(model.DataWarning)).Where("user_id = ?", userId).Error; err != nil {
 		return xerr.WithStack(err)
 	}
 	for _, v := range dataWarningList {
-		a, params, err := s.BuildQuery(v.Id)
+		a, params, err := s.BuildQuery(userModel, v.Id)
 		if err != nil {
 			return xerr.WithStack(err)
 		}
@@ -107,8 +113,7 @@ func (s *StoreEntry) NeckRingOriginalAsync(ctx context.Context, pastureId int64,
 	return nil
 }
 
-func (s *StoreEntry) BuildQuery(warningId int64) (string, []interface{}, error) {
-
+func (s *StoreEntry) BuildQuery(userModel *model.UserModel, warningId int64) (string, []interface{}, error) {
 	conditionsMap := make(map[int32][]string)
 	params := make([]interface{}, 0)
 	res := make([]*model.DataWarningItems, 0)
@@ -125,7 +130,10 @@ func (s *StoreEntry) BuildQuery(warningId int64) (string, []interface{}, error)
 	}
 
 	if len(conditionsMap) == 0 {
-		return "", nil, xerr.Custom("条件组不能为空")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "dataWarning.conditionGroupNotEmpty",
+		})
+		return "", nil, xerr.Custom(messageId)
 	}
 
 	var sqlConditions []string

+ 1 - 1
module/backend/enum_map.go

@@ -328,7 +328,7 @@ func (s *StoreEntry) BarnTypeMap() map[pasturePb.PenType_Kind]string {
 		pasturePb.PenType_Lactation:        "成母牛舍",
 		pasturePb.PenType_Peripartum:       "围产牛舍",
 		pasturePb.PenType_Dry_Milking:      "干奶牛舍",
-		pasturePb.PenType_Sick_Cow:         "病牛舍",
+		pasturePb.PenType_Sick_Cow:         "病牛舍",
 		pasturePb.PenType_Out:              "淘汰牛舍",
 		pasturePb.PenType_Segregate:        "隔离牛舍",
 		pasturePb.PenType_Bull:             "公牛牛舍",

+ 22 - 1
module/backend/enum_options.go

@@ -190,9 +190,30 @@ func (s *StoreEntry) SystemUserOptions(ctx context.Context, depName string) (*pa
 		return nil, xerr.Custom("部门已经禁用")
 	}
 
+	systemUserDepthRoleList := make([]*model.SystemUserDepthRole, 0)
+	if err = s.DB.Model(new(model.SystemUserDepthRole)).
+		Where("FIND_IN_SET(?,depth_ids) > 0", systemDepth.Id).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Find(&systemUserDepthRoleList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	if len(systemUserDepthRoleList) <= 0 {
+		return &pasturePb.ConfigOptionsListResponse{
+			Code: http.StatusOK,
+			Msg:  "ok",
+			Data: make([]*pasturePb.ConfigOptionsList, 0),
+		}, nil
+	}
+
+	userIds := make([]int64, 0)
+	for _, v := range systemUserDepthRoleList {
+		userIds = append(userIds, v.UserId)
+	}
+
 	systemUserList := make([]*model.SystemUser, 0)
 	if err = s.DB.Table(new(model.SystemUser).TableName()).
-		Where("FIND_IN_SET(?,dept_ids) > 0", systemDepth.Id).
+		Where("id IN ?", userIds).
 		Where("is_delete = ?", pasturePb.IsShow_Ok).
 		Where("is_show = ? ", pasturePb.IsShow_Ok).
 		Find(&systemUserList).Error; err != nil {

+ 44 - 17
module/backend/event_base.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"kpt-pasture/model"
+	"kpt-pasture/util"
 	"net/http"
 	"strconv"
 	"strings"
@@ -89,23 +90,26 @@ func (s *StoreEntry) CreateEnter(ctx context.Context, req *pasturePb.EventEnterR
 		return xerr.WithStack(err)
 	}
 
-	if err = s.EnterCheck(ctx, req); err != nil {
+	if err = s.EnterCheck(userModel, req); err != nil {
 		return xerr.WithStack(err)
 	}
 
+	pastureId := userModel.AppPasture.Id
 	req.MessengerId = int32(userModel.SystemUser.Id)
 	req.MessengerName = userModel.SystemUser.Name
-	if req.OperationId > 0 {
-		systemUser, _ := s.GetSystemUserById(ctx, int64(req.OperationId))
-		req.OperationName = systemUser.Name
+
+	operationSystemUser, err := s.GetSystemUserById(ctx, int64(req.OperationId))
+	if err != nil {
+		return xerr.WithStack(err)
 	}
+	req.OperationName = operationSystemUser.Name
 
-	penMap := s.PenMap(ctx, userModel.AppPasture.Id)
+	penMap := s.PenMap(ctx, pastureId)
 	if len(penMap) <= 0 {
 		return xerr.Customf("请先设置牛舍信息")
 	}
 
-	newCow := model.NewEnterCow(userModel.AppPasture.Id, req, penMap)
+	newCow := model.NewEnterCow(pastureId, req, penMap)
 	if err = s.DB.Transaction(func(tx *gorm.DB) error {
 		// 新增牛只信息
 		if err = tx.Model(new(model.Cow)).Create(newCow).Error; err != nil {
@@ -113,34 +117,47 @@ func (s *StoreEntry) CreateEnter(ctx context.Context, req *pasturePb.EventEnterR
 		}
 
 		// 新增入场事件
-		newEventEnter := model.NewEventEnter(userModel.AppPasture.Id, newCow.Id, req)
+		newEventEnter := model.NewEventEnter(pastureId, newCow, req)
 		if err = tx.Model(new(model.EventEnter)).Create(newEventEnter).Error; err != nil {
 			return xerr.WithStack(err)
 		}
 
 		// 新增胎次数据
-		newCowLact := model.NewCowLact(userModel.AppPasture.Id, newCow)
+		newCowLact := model.NewCowLact(pastureId, newCow)
 		if err = tx.Model(new(model.CowLact)).Create(newCowLact).Error; err != nil {
 			return xerr.WithStack(err)
 		}
 
-		eventWeight := model.NewEventWeight(
-			userModel.AppPasture.Id,
-			newCow,
-			userModel.SystemUser,
+		eventWeight := model.NewEventWeight(pastureId, newCow, userModel.SystemUser,
 			&pasturePb.EventWeight{
 				WeightAt:      req.EnterAt,
-				Remarks:       "入场体重",
+				Remarks:       model.EnterWeigh,
 				OperationId:   req.OperationId,
 				OperationName: req.OperationName,
 				Weight:        req.Weight,
 			})
-		if err = tx.Model(new(model.EventWeight)).Create(eventWeight).Error; err != nil {
+		if err = tx.Model(new(model.EventWeight)).
+			Create(eventWeight).Error; err != nil {
 			return xerr.WithStack(err)
 		}
 
+		// 脖环绑定
+		if newCow.NeckRingNumber != "" {
+			newCowNeckRing := model.NewNeckRing(pastureId, newCow.NeckRingNumber, newCow, operationSystemUser)
+			if err = tx.Model(new(model.NeckRing)).
+				Create(newCowNeckRing).Error; err != nil {
+				return xerr.WithStack(err)
+			}
+
+			newNeckRingBindLog := model.NewNeckRingBindLog(pastureId, newCow.NeckRingNumber, newCow, userModel.SystemUser, model.EventEnterBind)
+			if err = tx.Model(new(model.NeckRingBindLog)).
+				Create(newNeckRingBindLog).Error; err != nil {
+				return xerr.WithStack(err)
+			}
+		}
+
 		// 记录事件日志
-		cowLogs := s.SubmitEventLog(ctx, userModel.AppPasture.Id, newCow, pasturePb.EventType_Enter, req)
+		cowLogs := s.SubmitEventLog(ctx, pastureId, newCow, pasturePb.EventType_Enter, req)
 		if err = tx.Table(cowLogs.TableName()).Create(cowLogs).Error; err != nil {
 			return xerr.WithStack(err)
 		}
@@ -169,7 +186,7 @@ func (s *StoreEntry) GroupTransferList(ctx context.Context, req *pasturePb.Searc
 		Where("a.pasture_id = ?", userModel.AppPasture.Id)
 
 	if req.EarNumber != "" {
-		pref.Where("a.ear_number = ?", req.EarNumber)
+		pref.Where("f.ear_number = ?", req.EarNumber)
 	}
 
 	if req.TransferReasonId > 0 {
@@ -230,13 +247,20 @@ func (s *StoreEntry) CreateGroupTransfer(ctx context.Context, req *pasturePb.Tra
 			if cow.PenId == v.TransferInPenId {
 				return xerr.Custom("转入栏舍和牛只当前栏舍不能一致")
 			}
+
+			transferGroupAt := util.DateTimeParseLocalUnix(v.TransferDate)
+			if cow.GetEventDayAge(transferGroupAt) < 0 {
+				return xerr.Custom("转栏时间不能早于牛只出生时间")
+			}
+
 			operationUser, err := s.GetSystemUserById(ctx, int64(v.OperationId))
 			if err != nil {
 				return xerr.WithStack(err)
 			}
 
 			newEventTransferGroup := model.NewEventTransferGroup(userModel.AppPasture.Id, cow, v, transferReasonMap, userModel.SystemUser, operationUser)
-			if err = tx.Model(new(model.EventTransferGroup)).Create(newEventTransferGroup).Error; err != nil {
+			if err = tx.Model(new(model.EventTransferGroup)).
+				Create(newEventTransferGroup).Error; err != nil {
 				return xerr.WithStack(err)
 			}
 
@@ -389,6 +413,9 @@ func (s *StoreEntry) WeightBatch(ctx context.Context, req *pasturePb.BatchEventW
 				return xerr.WithStack(err)
 			}
 
+			if cow.GetEventDayAge(int64(item.WeightAt)) < 0 {
+				return xerr.Custom("称重时间不能早于牛只出生时间")
+			}
 			// 更新牛只信息
 			cow.EventWeightUpdate(int64(item.Weight*1000), int64(item.Height), int64(item.WeightAt))
 			if err = tx.Model(new(model.Cow)).

+ 65 - 57
module/backend/event_base_more.go

@@ -4,12 +4,11 @@ import (
 	"context"
 	"kpt-pasture/model"
 	"net/http"
-	"strings"
+
+	"github.com/nicksnyder/go-i18n/v2/i18n"
 
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
-	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
 	"gitee.com/xuyiping_admin/pkg/xerr"
-	"go.uber.org/zap"
 	"gorm.io/gorm"
 )
 
@@ -20,51 +19,14 @@ func (s *StoreEntry) DeathBatch(ctx context.Context, req *pasturePb.EventDeathBa
 	}
 
 	if len(req.Items) <= 0 {
-		return xerr.Custom("请选择相关牛只")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.selectCow",
+		})
+		return xerr.Custom(messageId)
 	}
-
-	newEventDeathList := make([]*model.EventDeathModel, 0)
-	for _, item := range req.Items {
-		cow, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, item.EarNumber)
-		if err != nil {
-			zaplog.Error("DeathBatch", zap.Any("item", item), zap.Any("err", err))
-			return xerr.Customf("获取牛只信息失败: %s", item.EarNumber)
-		}
-
-		if name, ok := s.DeadReasonMap()[item.DeathReasonKind]; ok {
-			item.DeathReasonName = name
-		}
-
-		operationUser, err := s.GetSystemUserById(ctx, int64(item.OperationId))
-		if err != nil {
-			zaplog.Error("DeathBatch", zap.Any("item", item), zap.Any("err", err))
-			return xerr.Customf("获取操作人员信息失败: %d", item.OperationId)
-		}
-
-		if name, ok := s.CowDeathDestinationMap()[item.DeathDestinationKind]; ok {
-			item.DeathDestinationName = name
-		}
-
-		newEventDeath := model.NewEventDeath(userModel.AppPasture.Id, cow, item, userModel.SystemUser, operationUser)
-		eventDeathModel := &model.EventDeathModel{
-			Cow:         cow,
-			EventDeath:  newEventDeath,
-			NeckRing:    nil,
-			CalvingCalf: nil,
-		}
-
-		// 犊牛死亡更新
-		if cow.DayAge <= model.CalfDefaultDayAge {
-			calvingCalf, ok := s.IsExistCalvingCalf(userModel.AppPasture.Id, cow.Id)
-			if ok && calvingCalf != nil {
-				eventDeathModel.CalvingCalf = calvingCalf
-			}
-		}
-
-		if neckRing, ok := s.NeckRingIsExist(userModel.AppPasture.Id, item.EarNumber); ok && neckRing != nil {
-			eventDeathModel.NeckRing = neckRing
-		}
-		newEventDeathList = append(newEventDeathList, eventDeathModel)
+	newEventDeathList, err := s.DeathCheck(ctx, userModel, req)
+	if err != nil {
+		return xerr.WithStack(err)
 	}
 
 	if err = s.DB.Transaction(func(tx *gorm.DB) error {
@@ -93,8 +55,16 @@ func (s *StoreEntry) DeathBatch(ctx context.Context, req *pasturePb.EventDeathBa
 					return xerr.WithStack(err)
 				}
 
-				newNeckRingBindLog := model.NewNeckRingBindLog(userModel.AppPasture.Id, item.NeckRing.NeckRingNumber, item.Cow, userModel.SystemUser, "死亡解绑")
-				if err = tx.Model(new(model.NeckRingBindLog)).Create(newNeckRingBindLog).Error; err != nil {
+				newNeckRingBindLog := model.NewNeckRingBindLog(
+					userModel.AppPasture.Id,
+					item.NeckRing.NeckRingNumber,
+					item.Cow,
+					userModel.SystemUser,
+					"死亡解绑",
+				)
+
+				if err = tx.Model(new(model.NeckRingBindLog)).
+					Create(newNeckRingBindLog).Error; err != nil {
 					return xerr.WithStack(err)
 				}
 			}
@@ -168,7 +138,10 @@ func (s *StoreEntry) CowEarNumberUpdate(ctx context.Context, req *pasturePb.Even
 
 	cow, err := s.GetCowInfoByCowId(ctx, userModel.AppPasture.Id, int64(req.CowId))
 	if err != nil {
-		return xerr.Custom("未找到该牛只信息")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.noCow",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	cow.EventEarNumberUpdate(req.EarNumber)
@@ -193,7 +166,10 @@ func (s *StoreEntry) CowSaleCreate(ctx context.Context, req *pasturePb.EventCowS
 		return xerr.WithStack(err)
 	}
 	if len(cowList) != len(req.EarNumbers) {
-		return xerr.Custom("数据异常")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.errorData",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if len(cowList) <= 0 {
@@ -204,13 +180,19 @@ func (s *StoreEntry) CowSaleCreate(ctx context.Context, req *pasturePb.EventCowS
 	if err = s.DB.Model(new(model.SaleDealer)).
 		Where("id = ?", req.DealerId).
 		First(dealerInfo).Error; err != nil {
-		return xerr.Custom("经销商数据异常")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "pasture.dealerError",
+		})
+		return xerr.Custom(messageId)
 	}
 	req.DealerName = dealerInfo.Name
 
 	operationUser, err := s.GetSystemUserById(ctx, int64(req.OperationId))
 	if err != nil {
-		return xerr.Custom("获取操作人员信息失败")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.getOperationError",
+		})
+		return xerr.Custom(messageId)
 	}
 	req.OperationName = operationUser.Name
 
@@ -228,6 +210,14 @@ func (s *StoreEntry) CowSaleCreate(ctx context.Context, req *pasturePb.EventCowS
 	eventCowLogList := make([]*model.EventCowLog, 0)
 	neckRingList := make([]*model.NeckRing, 0)
 	for _, cow := range cowList {
+		if cow.GetEventDayAge(int64(req.SaleAt)) < 0 {
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID:    "validate.wrongCowSaleTime",
+				TemplateData: map[string]interface{}{"earNumber": cow.EarNumber},
+			})
+			return xerr.Customf(messageId)
+		}
+
 		var newEventCowLog *model.EventCowLog
 		if req.SalesType == pasturePb.SalesType_Sales {
 			newEventCowLog = s.SubmitEventLog(ctx, userModel.AppPasture.Id, cow, pasturePb.EventType_Sale, req)
@@ -297,7 +287,7 @@ func (s *StoreEntry) CowSaleCreate(ctx context.Context, req *pasturePb.EventCowS
 					Updates(neckRing).Error; err != nil {
 					return xerr.WithStack(err)
 				}
-				newNeckRingBindLog := model.NewNeckRingBindLog(userModel.AppPasture.Id, neckRing.NeckRingNumber, cowInfo, userModel.SystemUser, "死亡解绑")
+				newNeckRingBindLog := model.NewNeckRingBindLog(userModel.AppPasture.Id, neckRing.NeckRingNumber, cowInfo, userModel.SystemUser, model.EventDieBind)
 				if err = tx.Model(new(model.NeckRingBindLog)).Create(newNeckRingBindLog).Error; err != nil {
 					return xerr.WithStack(err)
 				}
@@ -440,20 +430,30 @@ func (s *StoreEntry) ImmunizationBatch(ctx context.Context, req *pasturePb.Immun
 	}
 
 	if len(req.EarNumbers) <= 0 {
-		return xerr.Custom("请选择相关牛只数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.selectCow",
+		})
+		return xerr.Custom(messageId)
+	}
+
+	if len(req.EarNumbers) > 50 {
+		return xerr.Custom("最多只能添加50条数据")
 	}
 
 	eventImmunizationList := make([]*model.EventImmunizationPlan, 0)
 	if err = s.DB.Model(new(model.EventImmunizationPlan)).
 		Where("pasture_id = ?", userModel.AppPasture.Id).
 		Where("plan_id = ?", req.PlanId).
-		Where("ear_number IN (?)", strings.Join(req.EarNumbers, ",")).
+		Where("ear_number IN (?)", req.EarNumbers).
 		Find(&eventImmunizationList).Error; err != nil {
 		return xerr.WithStack(err)
 	}
 
 	if len(eventImmunizationList) != len(req.EarNumbers) {
-		return xerr.Custom("数据异常")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.dataError",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	drugsInfo := &model.Drugs{}
@@ -476,6 +476,14 @@ func (s *StoreEntry) ImmunizationBatch(ctx context.Context, req *pasturePb.Immun
 				return xerr.WithStack(err)
 			}
 
+			if cowInfo.GetEventDayAge(int64(req.ImmunizationAt)) < 0 {
+				messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+					MessageID:    "validate.immuneTime",
+					TemplateData: map[string]interface{}{"earNumber": cowInfo.EarNumber},
+				})
+				return xerr.Customf(messageId)
+			}
+
 			// 更新数据
 			eventImmunization.EventUpdate(int64(req.ImmunizationAt), cowInfo, drugsInfo, req.Usage, operationUser, userModel.SystemUser, req.Remarks)
 			if err = tx.Model(new(model.EventImmunizationPlan)).

+ 42 - 7
module/backend/event_breed.go

@@ -9,6 +9,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
 	"go.uber.org/zap"
 
@@ -79,20 +81,41 @@ func (s *StoreEntry) CalvingCreate(ctx context.Context, req *pasturePb.EventCalv
 	cow, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, req.EarNumber)
 	if err != nil {
 		zaplog.Error("CalvingCreate", zap.Any("cow_id", req.CowId), zap.Any("err", err))
-		return xerr.Custom("请选择相关牛只")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.selectCow",
+		})
+		return xerr.Custom(messageId)
+	}
+
+	if cow.GetEventDayAge(int64(req.CalvingAt)) < 0 {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "validate.birthTime",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if len(req.CalfItemList) != int(req.ChildNumber) {
-		return xerr.Custom("犊牛信息与产子数不相符")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.lactationCount",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if cow.IsPregnant != pasturePb.IsShow_Ok || cow.BreedStatus != pasturePb.BreedStatus_Pregnant {
-		return xerr.Custom("该母牛未配种")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.noPregnancy",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	operationUser, err := s.GetSystemUserById(ctx, int64(req.OperationId))
 	if err != nil {
-		return xerr.Customf("获取操作人员信息失败: %s", err.Error())
+		zaplog.Error("CalvingCreate", zap.Any("req", req), zap.Any("err", err))
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.getOperationError",
+		})
+		return xerr.Customf(messageId)
 	}
 	req.OperationName = operationUser.Name
 
@@ -103,7 +126,10 @@ func (s *StoreEntry) CalvingCreate(ctx context.Context, req *pasturePb.EventCalv
 		Where("lact = ?", cow.Lact).
 		Where("status = ?", pasturePb.IsShow_No).
 		First(newEventCalving).Error; err != nil {
-		return xerr.Custom("该母牛信息不存在")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.noMotherCow",
+		})
+		return xerr.Custom(messageId)
 	}
 	if err = s.DB.Transaction(func(tx *gorm.DB) error {
 		// 更新产犊事件表
@@ -231,7 +257,10 @@ func (s *StoreEntry) SameTimeBatch(ctx context.Context, req *pasturePb.EventSame
 
 	operationUser, err := s.GetSystemUserById(ctx, int64(req.OperationId))
 	if err != nil {
-		return xerr.Customf("异常数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.getOperationError",
+		})
+		return xerr.Customf(messageId)
 	}
 	req.OperationName = operationUser.Name
 
@@ -251,7 +280,13 @@ func (s *StoreEntry) SameTimeBatch(ctx context.Context, req *pasturePb.EventSame
 			return xerr.WithStack(err)
 		}
 		if time.Unix(eventCowSameTime.PlanDay, 0).Local().Format(model.LayoutDate2) != nowTime {
-			return xerr.Customf("该牛只不是今日计划: %s", eventCowSameTime.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.noSameTimePlan",
+				TemplateData: map[string]interface{}{
+					"earNumber": eventCowSameTime.EarNumber,
+				},
+			})
+			return xerr.Customf(messageId)
 		}
 		eventCowSameTimeList = append(eventCowSameTimeList, eventCowSameTime)
 	}

+ 11 - 5
module/backend/event_breed_more.go

@@ -69,7 +69,7 @@ func (s *StoreEntry) PregnantCheckCreateBatch(ctx context.Context, req *pastureP
 		return xerr.WithStack(err)
 	}
 
-	pregnantCheckBatchModelList, err := s.PregnantCheckDataCheck(ctx, userModel.AppPasture.Id, req)
+	pregnantCheckBatchModelList, err := s.PregnantCheckDataCheck(ctx, userModel, req)
 	if err != nil {
 		return xerr.WithStack(err)
 	}
@@ -339,7 +339,7 @@ func (s *StoreEntry) MatingBatch(ctx context.Context, req *pasturePb.EventMating
 		return xerr.WithStack(err)
 	}
 
-	eventMatingCheckModelList, err := s.MatingCreateCheck(ctx, userModel.AppPasture.Id, req)
+	eventMatingCheckModelList, err := s.MatingCreateCheck(ctx, userModel, req)
 	if err != nil {
 		return xerr.WithStack(err)
 	}
@@ -523,6 +523,10 @@ func (s *StoreEntry) WeaningBatch(ctx context.Context, req *pasturePb.EventWeani
 				return xerr.WithStack(err)
 			}
 
+			if cowInfo.GetEventDayAge(int64(item.WeaningAt)) < 0 {
+				return xerr.Customf("牛号: %s,断奶时间不能早于牛只出生时间", cowInfo.EarNumber)
+			}
+
 			eventWeaning := &model.EventWeaning{}
 			if err = s.DB.Model(new(model.EventWeaning)).
 				Where("ear_number = ?", item.EarNumber).
@@ -541,15 +545,17 @@ func (s *StoreEntry) WeaningBatch(ctx context.Context, req *pasturePb.EventWeani
 				item.Remarks, item.PenId, operation, userModel.SystemUser,
 			)
 			if err = tx.Model(new(model.EventWeaning)).
-				Select("status", "reality_day", "operation_id", "operation_name", "message_id", "message_name", "remarks", "after_pen_id", "birth_weight", "birth_at").
-				Where("id = ?", eventWeaning.Id).
+				Select(
+					"status", "reality_day", "operation_id", "operation_name",
+					"message_id", "message_name", "remarks", "after_pen_id", "birth_weight", "birth_at",
+				).Where("id = ?", eventWeaning.Id).
 				Updates(eventWeaning).Error; err != nil {
 				return xerr.WithStack(err)
 			}
 
 			cowInfo.EventWeaningUpdate(int64(item.WeaningAt), item.PenId, int64(item.Weight*1000))
 			if err = tx.Model(new(model.Cow)).
-				Select("pen_id", "current_weight", "weaning_at", "last_weight_at").
+				Select("pen_id", "current_weight", "weaning_at", "last_weight_at", "weaning_weight").
 				Where("id = ?", cowInfo.Id).
 				Updates(cowInfo).Error; err != nil {
 				return xerr.WithStack(err)

+ 13 - 5
module/backend/event_breed_more_more.go

@@ -53,6 +53,11 @@ func (s *StoreEntry) DryMilkList(ctx context.Context, req *pasturePb.SearchEvent
 }
 
 func (s *StoreEntry) DryMilkBatch(ctx context.Context, req *pasturePb.EventMilkBatch) error {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return xerr.WithStack(err)
+	}
+
 	if len(req.Items) <= 0 {
 		return nil
 	}
@@ -61,11 +66,6 @@ func (s *StoreEntry) DryMilkBatch(ctx context.Context, req *pasturePb.EventMilkB
 		return xerr.Custom("最多只能添加50条数据")
 	}
 
-	userModel, err := s.GetUserModel(ctx)
-	if err != nil {
-		return xerr.WithStack(err)
-	}
-
 	penMap := s.PenMap(ctx, userModel.AppPasture.Id)
 	if err = s.DB.Transaction(func(tx *gorm.DB) error {
 		for _, v := range req.Items {
@@ -74,6 +74,10 @@ func (s *StoreEntry) DryMilkBatch(ctx context.Context, req *pasturePb.EventMilkB
 				return xerr.WithStack(err)
 			}
 
+			if cowInfo.GetEventDayAge(int64(v.DryMilkAt)) < 0 {
+				return xerr.Customf("牛号: %s,干奶时间不能早于牛只出生时间", cowInfo.EarNumber)
+			}
+
 			cowInfo.EventDryMilkUpdate(int64(v.DryMilkAt), penMap[v.PenId])
 			if err = tx.Select("milk_kind", "last_dry_milk_at", "pen_id", "pen_name").
 				Where("id = ?", cowInfo.Id).
@@ -253,6 +257,10 @@ func (s *StoreEntry) UnForbiddenMating(ctx context.Context, req *pasturePb.Event
 				return xerr.WithStack(err)
 			}
 
+			if cowInfo.GetEventDayAge(int64(item.UnForbiddenMatingAt)) < 0 {
+				return xerr.Customf("牛号: %s,解禁时间不能早于牛只出生时间", cowInfo.EarNumber)
+			}
+
 			// 牛只解配更新
 			cowInfo.UnForbiddenMatingUpdate()
 			if err = tx.Model(new(model.Cow)).

+ 355 - 48
module/backend/event_check.go

@@ -7,6 +7,8 @@ import (
 	"kpt-pasture/util"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"gorm.io/gorm"
 
 	"go.uber.org/zap"
@@ -43,33 +45,73 @@ type AbortionCheckBatchModel struct {
 	IsLact        pasturePb.IsShow_Kind
 }
 
-func (s *StoreEntry) EnterCheck(ctx context.Context, req *pasturePb.EventEnterRequest) error {
+func (s *StoreEntry) EnterCheck(userModel *model.UserModel, req *pasturePb.EventEnterRequest) error {
 	var count int64
-	if err := s.DB.Model(new(model.Cow)).Where("ear_number = ?", req.EarNumber).Count(&count).Error; err != nil {
+	if err := s.DB.Model(new(model.Cow)).
+		Where("ear_number = ?", req.EarNumber).
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Count(&count).Error; err != nil {
 		return xerr.WithStack(err)
 	}
 	if count > 0 {
-		return xerr.Custom("该牛只已存在")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.Exists",
+		})
+		return xerr.Custom(messageId)
 	}
+
+	if req.BirthAt > req.EnterAt {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.birthTimeEnterTime",
+		})
+		return xerr.Custom(messageId)
+	}
+
 	return nil
 }
 
-func (s *StoreEntry) MatingCreateCheck(ctx context.Context, pastureId int64, req *pasturePb.EventMatingBatch) ([]*model.EventMatingCheckBatchModel, error) {
+func (s *StoreEntry) MatingCreateCheck(ctx context.Context, userModel *model.UserModel, req *pasturePb.EventMatingBatch) ([]*model.EventMatingCheckBatchModel, error) {
 	if len(req.Items) <= 0 {
-		return nil, xerr.Custom("请选择相关牛只")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.selectCow",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	if len(req.Items) > 50 {
-		return nil, xerr.Custom("最多只能选择50只牛只")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.dataLimit",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 	eventMatingCheckBatchModelList := make([]*model.EventMatingCheckBatchModel, 0)
 
 	for _, v := range req.Items {
-		cowInfo, err := s.GetCowInfoByEarNumber(ctx, pastureId, v.EarNumber)
+		cowInfo, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, v.EarNumber)
 		if err != nil {
 			return nil, xerr.WithStack(err)
 		}
 
+		if cowInfo.GetEventDayAge(int64(v.MatingAt)) < 0 {
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.matingTimeBirthTime",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
+		}
+
+		if int64(v.MatingAt) < cowInfo.LastCalvingAt {
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.matingTimeLastCalvingTime",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
+		}
+
 		operationUser, err := s.GetSystemUserById(ctx, int64(v.OperationId))
 		if err != nil {
 			return nil, xerr.WithStack(err)
@@ -78,35 +120,83 @@ func (s *StoreEntry) MatingCreateCheck(ctx context.Context, pastureId int64, req
 		frozenSemen := &model.FrozenSemen{}
 		if err = s.DB.Where("bull_id = ?", v.FrozenSemenNumber).
 			First(frozenSemen).Error; err != nil {
-			return nil, xerr.Custom("未找到冻精信息")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "goods.frozenSemenNotExist",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 
 		if frozenSemen.Quantity < v.FrozenSemenCount {
-			return nil, xerr.Custom("冻精数量不足")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "goods.frozenSemenNotEnough",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 
 		if cowInfo.Sex != pasturePb.Genders_Female {
-			return nil, xerr.Customf("牛只: %d,不是母牛", cowInfo.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.cowSex",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		if int64(v.MatingAt) < cowInfo.LastMatingAt {
-			return nil, xerr.Customf("牛只: %s,最近一次配种时间: %d,不能小于本次配种时间: %d", cowInfo.EarNumber, cowInfo.LastMatingAt, v.MatingAt)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.matingTimeLastMatingTime",
+				TemplateData: map[string]interface{}{
+					"earNumber":      cowInfo.EarNumber,
+					"matingTime":     time.Unix(int64(v.MatingAt), 0).Format(model.LayoutDate2),
+					"lastMatingTime": time.Unix(cowInfo.LastMatingAt, 0).Format(model.LayoutDate2),
+				},
+			})
+
+			return nil, xerr.Customf(messageId)
 		}
 
 		if int64(v.MatingAt) < cowInfo.LastPregnantCheckAt {
-			return nil, xerr.Customf("牛只: %s,最近一次孕检时间: %d,不能小于本次配种时间: %d", cowInfo.EarNumber, cowInfo.LastPregnantCheckAt, v.MatingAt)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.validate.pregnantCheckMatingTime",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		if int64(v.MatingAt) < cowInfo.LastAbortionAt {
-			return nil, xerr.Customf("牛只: %s,最近一次流产时间: %d,不能小于本次配种时间: %d", cowInfo.EarNumber, cowInfo.LastAbortionAt, v.MatingAt)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.matingTimeLastAbortionTime",
+				TemplateData: map[string]interface{}{
+					"earNumber":        cowInfo.EarNumber,
+					"lastAbortionDate": time.Unix(cowInfo.LastAbortionAt, 0).Format(model.LayoutDate2),
+					"matingDate":       time.Unix(int64(v.MatingAt), 0).Format(model.LayoutDate2),
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		if int64(v.MatingAt) < cowInfo.BirthAt {
-			return nil, xerr.Customf("牛只: %s,出生时间: %d,不能小于本次配种时间: %d", cowInfo.EarNumber, cowInfo.BirthAt, v.MatingAt)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.matingTimeBirthTime",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		if cowInfo.BreedStatus == pasturePb.BreedStatus_Pregnant || cowInfo.BreedStatus == pasturePb.BreedStatus_No_Mating {
-			return nil, xerr.Customf("牛只: %s,当前状态为: %s,不能进行配种", cowInfo.EarNumber, cowInfo.BreedStatus.String())
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.breedStatusError",
+				TemplateData: map[string]interface{}{
+					"earNumber":   cowInfo.EarNumber,
+					"breedStatus": cowInfo.BreedStatus.String(),
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 		eventMatingCheckBatchModelList = append(eventMatingCheckBatchModelList, &model.EventMatingCheckBatchModel{
 			Cow:              cowInfo,
@@ -122,41 +212,91 @@ func (s *StoreEntry) MatingCreateCheck(ctx context.Context, pastureId int64, req
 	return eventMatingCheckBatchModelList, nil
 }
 
-func (s *StoreEntry) PregnantCheckDataCheck(ctx context.Context, pastureId int64, req *pasturePb.EventPregnantCheckBatch) ([]*PregnantCheckBatchModel, error) {
+func (s *StoreEntry) PregnantCheckDataCheck(ctx context.Context, userModel *model.UserModel, req *pasturePb.EventPregnantCheckBatch) ([]*PregnantCheckBatchModel, error) {
 	if len(req.Items) <= 0 {
-		return nil, xerr.Custom("请选择相关牛只数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.selectCow",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	if len(req.Items) > 50 {
-		return nil, xerr.Custom("一次性最多限制提交50牛数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.dataLimit",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	pregnantCheckBatchModelList := make([]*PregnantCheckBatchModel, 0)
 	cowInfo := &model.Cow{}
 	var err error
 	for _, item := range req.Items {
-		cowInfo, err = s.GetCowInfoByEarNumber(ctx, pastureId, item.EarNumber)
+		cowInfo, err = s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, item.EarNumber)
 		if err != nil {
 			return nil, xerr.WithStack(err)
 		}
 
 		if cowInfo.Sex != pasturePb.Genders_Female {
-			return nil, xerr.Customf("牛只: %d,不是母牛", cowInfo.Id)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.cowSex",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
+		}
+
+		if int64(item.PregnantCheckAt) > cowInfo.LastMatingAt {
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.pregnantCheckMatingTime",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
+		}
+
+		if cowInfo.GetEventDayAge(int64(item.PregnantCheckAt)) < 0 {
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.pregnantCheckBirthTime",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		operationUser, err := s.GetSystemUserById(ctx, int64(item.OperationId))
 		if err != nil {
-			zaplog.Error("PregnantCheckDataCheck", zap.Any("id", item.OperationId), zap.Any("error", err.Error()))
-			return nil, xerr.Customf("获取操作人员信息失败")
+			zaplog.Error("PregnantCheckDataCheck",
+				zap.Any("id", item.OperationId),
+				zap.Any("error", err.Error()),
+			)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.getOperationError",
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
-		// 过滤掉没有配种状态的牛只
-		if cowInfo.BreedStatus != pasturePb.BreedStatus_Breeding && cowInfo.BreedStatus != pasturePb.BreedStatus_Pregnant {
-			return nil, xerr.Customf("牛只: %s 未参加配种,不能进行孕检", cowInfo.EarNumber)
+		if cowInfo.LastMatingAt <= 0 {
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.matingDataError",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
-		if cowInfo.LastMatingAt <= 0 {
-			return nil, xerr.Customf("牛只: %s,最近一次配种数据异常", cowInfo.EarNumber)
+		// 过滤掉没有配种状态的牛只
+		if cowInfo.BreedStatus != pasturePb.BreedStatus_Breeding && cowInfo.BreedStatus != pasturePb.BreedStatus_Pregnant {
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.cannotPregnantCheck",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		itemEventPregnantCheck, err := s.FindEventPregnantCheckIsExIstByCowId(ctx, cowInfo)
@@ -165,16 +305,34 @@ func (s *StoreEntry) PregnantCheckDataCheck(ctx context.Context, pastureId int64
 		}
 
 		if itemEventPregnantCheck.Id <= 0 {
-			return nil, xerr.Customf("未发现该牛只: %s 孕检数据", cowInfo.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.notFoundPregnantCheckData",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
-		lastEventMating, err := s.FindLastEventMatingByCowId(ctx, pastureId, cowInfo.Id)
+		lastEventMating, err := s.FindLastEventMatingByCowId(ctx, userModel.AppPasture.Id, cowInfo.Id)
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return nil, xerr.Customf("未发现该牛只: %s 配种数据", cowInfo.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.notFoundMatingData",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		if lastEventMating == nil || lastEventMating.Status == pasturePb.IsShow_No {
-			return nil, xerr.Customf("未发现该牛只: %s 配种数据", cowInfo.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.notFoundMatingData",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		pregnantCheckBatchModelList = append(pregnantCheckBatchModelList, &PregnantCheckBatchModel{
@@ -210,23 +368,43 @@ func (s *StoreEntry) EstrusCheckDataCheck(ctx context.Context, userModel *model.
 	for _, item := range req.Items {
 		cowInfo, err := s.GetCowInfoByEarNumber(ctx, pastureId, item.EarNumber)
 		if err != nil {
-			return nil, xerr.Custom("牛只信息不存在")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "cow.cowNotExist",
+				TemplateData: map[string]interface{}{
+					"earNumber": item.EarNumber,
+				},
+			})
+			return nil, xerr.Custom(messageId)
 		}
 
 		if cowInfo.Sex != pasturePb.Genders_Female {
-			return nil, xerr.Custom("该牛只不是母牛")
-		}
-
-		if item.EstrusAt <= 0 {
-			return nil, xerr.Custom("发情时间不能为空")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "cow.cowSex",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Custom(messageId)
 		}
 
 		if int64(item.EstrusAt) <= cowInfo.BirthAt {
-			return nil, xerr.Custom("发情时间不能小于出生时间")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.estrusDateBirthDate",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		if int64(item.EstrusAt) <= cowInfo.LastCalvingAt {
-			return nil, xerr.Custom("发情时间不能小于产犊时间")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.estrusDateLastCalvingDate",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Custom(messageId)
 		}
 
 		estrusAt := time.Unix(int64(item.EstrusAt), 0).Local().Format(model.LayoutDate2)
@@ -235,7 +413,13 @@ func (s *StoreEntry) EstrusCheckDataCheck(ctx context.Context, userModel *model.
 		isExists := s.FindEventEstrusByCowId(pastureId, cowInfo.Id, estrusLocalStartTime, estrusLocalEndTime)
 		// 如果存在,并且当天已经提交过则跳过
 		if isExists {
-			return nil, xerr.Customf("该牛只:%s,今天已经提交过发情数据", item.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.estrusDataAlready",
+				TemplateData: map[string]interface{}{
+					"earNumber": item.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		operationUser, _ := s.GetSystemUserById(ctx, int64(item.OperationId))
@@ -248,7 +432,13 @@ func (s *StoreEntry) EstrusCheckDataCheck(ctx context.Context, userModel *model.
 		if item.IsMating == pasturePb.IsShow_Ok {
 			isExists = s.FindEventMatingByCowId(pastureId, cowInfo.Id)
 			if isExists {
-				return nil, xerr.Customf("配种清单已经有该牛只数据 :%s,今天已经提交过配种数据", item.EarNumber)
+				messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+					MessageID: "validate.matingDataItemAlready",
+					TemplateData: map[string]interface{}{
+						"earNumber": item.EarNumber,
+					},
+				})
+				return nil, xerr.Customf(messageId)
 			}
 			newEventMating := model.NewEventMating(pastureId, cowInfo, time.Now().Local().Unix(), exposeEstrusType)
 			res.EventMatingList = append(res.EventMatingList, newEventMating)
@@ -266,10 +456,16 @@ func (s *StoreEntry) EstrusCheckDataCheck(ctx context.Context, userModel *model.
 
 func (s *StoreEntry) AbortionEventDataCheck(ctx context.Context, userModel *model.UserModel, items []*pasturePb.EventAbortionItem) ([]*AbortionCheckBatchModel, error) {
 	if len(items) <= 0 {
-		return nil, xerr.Custom("请选择相关数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.selectCow",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 	if len(items) > 50 {
-		return nil, xerr.Custom("一次性最多限制提交50牛数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "validate.dataLimit",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 	abortionCheckBatchModelList := make([]*AbortionCheckBatchModel, 0)
 	for _, item := range items {
@@ -279,15 +475,43 @@ func (s *StoreEntry) AbortionEventDataCheck(ctx context.Context, userModel *mode
 		}
 
 		if cow.Sex != pasturePb.Genders_Female {
-			return nil, xerr.Customf("牛只: %s,不是母牛", cow.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "cow.cowSex",
+				TemplateData: map[string]interface{}{
+					"earNumber": cow.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		if cow.IsPregnant != pasturePb.IsShow_Ok {
-			return nil, xerr.Customf("牛只: %s,不是怀孕状态", cow.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.CowNotPregnant",
+				TemplateData: map[string]interface{}{
+					"earNumber": cow.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		if cow.BreedStatus != pasturePb.BreedStatus_Pregnant {
-			return nil, xerr.Customf("牛只: %s,不是怀孕状态", cow.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.CowNotPregnant",
+				TemplateData: map[string]interface{}{
+					"earNumber": cow.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
+		}
+
+		if cow.GetEventDayAge(int64(item.AbortionAt)) < 0 {
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.AbortionDateBirthDate",
+				TemplateData: map[string]interface{}{
+					"earNumber": item.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		operationUser, err := s.GetSystemUserById(ctx, int64(item.OperationId))
@@ -318,11 +542,23 @@ func (s *StoreEntry) ForbiddenMatingCheck(ctx context.Context, userModel *model.
 		}
 
 		if cowInfo.Sex != pasturePb.Genders_Female {
-			return nil, xerr.Customf("牛只: %s,不是母牛", cowInfo.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "cow.cowSex",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		if v.ForbiddenMatingAt < int32(cowInfo.BirthAt) {
-			return nil, xerr.Customf("牛只: %s,禁止配种时间不能小于出生时间", cowInfo.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.unMatingDate",
+				TemplateData: map[string]interface{}{
+					"earNumber": cowInfo.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
 		}
 
 		operationUser, err := s.GetSystemUserById(ctx, int64(v.OperationId))
@@ -342,3 +578,74 @@ func (s *StoreEntry) ForbiddenMatingCheck(ctx context.Context, userModel *model.
 	}
 	return res, nil
 }
+
+func (s *StoreEntry) DeathCheck(ctx context.Context, userModel *model.UserModel, req *pasturePb.EventDeathBatch) ([]*model.EventDeathModel, error) {
+	newEventDeathList := make([]*model.EventDeathModel, 0)
+	for _, item := range req.Items {
+		cow, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, item.EarNumber)
+		if err != nil {
+			return nil, xerr.WithStack(err)
+		}
+
+		if cow.BirthAt <= int64(item.DeathAt) {
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.deathTime",
+				TemplateData: map[string]interface{}{
+					"earNumber": cow.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
+		}
+
+		lastEventLog, _ := s.GetEventCowLog(ctx, userModel.AppPasture.Id, cow.Id)
+		if lastEventLog != nil && int64(item.DeathAt) < lastEventLog.EventAt {
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "validate.deathTimeLastEvent",
+				TemplateData: map[string]interface{}{
+					"earNumber": cow.EarNumber,
+				},
+			})
+			return nil, xerr.Customf(messageId)
+		}
+
+		if name, ok := s.DeadReasonMap()[item.DeathReasonKind]; ok {
+			item.DeathReasonName = name
+		}
+
+		operationUser, err := s.GetSystemUserById(ctx, int64(item.OperationId))
+		if err != nil {
+			zaplog.Error("DeathBatch", zap.Any("item", item), zap.Any("err", err))
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.getOperationError",
+			})
+			return nil, xerr.Customf(messageId)
+		}
+
+		if name, ok := s.CowDeathDestinationMap()[item.DeathDestinationKind]; ok {
+			item.DeathDestinationName = name
+		}
+
+		newEventDeath := model.NewEventDeath(userModel.AppPasture.Id, cow, item, userModel.SystemUser, operationUser)
+		eventDeathModel := &model.EventDeathModel{
+			Cow:         cow,
+			EventDeath:  newEventDeath,
+			NeckRing:    nil,
+			CalvingCalf: nil,
+		}
+
+		// 犊牛死亡更新
+		if cow.DayAge <= model.CalfDefaultDayAge {
+			calvingCalf, ok := s.IsExistCalvingCalf(userModel.AppPasture.Id, cow.Id)
+			if ok && calvingCalf != nil {
+				eventDeathModel.CalvingCalf = calvingCalf
+			}
+		}
+
+		if neckRing, ok := s.NeckRingIsExist(userModel.AppPasture.Id, item.EarNumber); ok && neckRing != nil {
+			eventDeathModel.NeckRing = neckRing
+		}
+		newEventDeathList = append(newEventDeathList, eventDeathModel)
+	}
+
+	return newEventDeathList, nil
+}

+ 17 - 0
module/backend/event_cow_log.go

@@ -2,6 +2,7 @@ package backend
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"kpt-pasture/model"
 	"kpt-pasture/util"
@@ -9,6 +10,8 @@ import (
 	"strings"
 	"time"
 
+	"gorm.io/gorm"
+
 	"gitee.com/xuyiping_admin/pkg/xerr"
 
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
@@ -213,6 +216,20 @@ func (s *StoreEntry) SubmitEventLog(ctx context.Context, pastureId int64, cow *m
 	return model.NewEventCowLog(newEventCowLogModel)
 }
 
+func (s *StoreEntry) GetEventCowLog(ctx context.Context, pastureId, cowId int64) (*model.EventCowLog, error) {
+	eventLog := &model.EventCowLog{CowId: cowId}
+	if err := s.DB.Table(eventLog.TableName()).
+		Where("cow_id = ?", cowId).
+		Where("pasture_id = ?", pastureId).
+		Order("event_at DESC").
+		First(&eventLog).Error; err != nil {
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, xerr.WithStack(err)
+		}
+	}
+	return eventLog, nil
+}
+
 func (s *StoreEntry) UpdateMatingResultEventCowLogByCowId(ctx context.Context, cowId int64, newResult string) error {
 	newEventCowLog := &model.EventCowLog{CowId: cowId}
 	if err := s.DB.Table(newEventCowLog.TableName()).

+ 99 - 43
module/backend/event_health.go

@@ -7,6 +7,8 @@ import (
 	"net/http"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
 	"go.uber.org/zap"
 
@@ -29,8 +31,7 @@ func (s *StoreEntry) CowDiseaseList(ctx context.Context, req *pasturePb.SearchEv
 		Joins(fmt.Sprintf("JOIN %s AS b on a.cow_id = b.id", new(model.Cow).TableName())).
 		Select("a.*,b.pen_name").
 		Where("a.pasture_id = ?", userModel.AppPasture.Id).
-		Where("a.health_status != ?", pasturePb.HealthStatus_Curable).
-		Where("b.admission_status != ?", pasturePb.AdmissionStatus_Admission)
+		Where("b.admission_status = ?", pasturePb.AdmissionStatus_Admission)
 
 	if len(req.CowIds) > 0 {
 		pref.Where("a.cow_id IN ?", req.CowIds)
@@ -56,8 +57,9 @@ func (s *StoreEntry) CowDiseaseList(ctx context.Context, req *pasturePb.SearchEv
 		pref.Where("a.disease_at BETWEEN ? AND ?", req.DiseasedStartAt, req.DiseaseEndAt)
 	}
 
-	if err = pref.Order("a.id DESC").
-		Count(&count).Limit(int(pagination.PageSize)).
+	if err = pref.Order("b.health_status").
+		Count(&count).
+		Limit(int(pagination.PageSize)).
 		Offset(int(pagination.PageOffset)).
 		Find(&cowDiseaseList).Error; err != nil {
 		return nil, xerr.WithStack(err)
@@ -87,12 +89,27 @@ func (s *StoreEntry) CowDiseaseCreate(ctx context.Context, req *pasturePb.EventC
 	// 牛只信息
 	cow, err := s.GetCowInfoByEarNumber(ctx, pastureId, req.EarNumber)
 	if err != nil {
-		return xerr.Customf("牛只信息错误: %d", req.CowId)
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "cow.errorCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return xerr.Customf(messageId)
+	}
+
+	if cow.GetEventDayAge(int64(req.DiseaseAt)) < 0 {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "cow.wrongCow",
+			TemplateData: map[string]interface{}{"earNumber": req.EarNumber},
+		})
+		return xerr.Customf(messageId)
 	}
 
 	operationUser, err := s.GetSystemUserById(ctx, int64(req.OperationId))
 	if err != nil {
-		return xerr.Customf("请检查操作人信息")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.checkOperation",
+		})
+		return xerr.Customf(messageId)
 	}
 
 	disease, err := s.GetDiseaseById(ctx, pastureId, req.DiseaseId)
@@ -100,37 +117,27 @@ func (s *StoreEntry) CowDiseaseCreate(ctx context.Context, req *pasturePb.EventC
 		return xerr.WithStack(err)
 	}
 
-	// 牛只疾病信息
-	newEventCowDisease := model.NewEventCowDisease(pastureId, cow, disease, req, operationUser, userModel.SystemUser)
+	var alreadyCount int64
+	if err = s.DB.Model(new(model.EventCowDisease)).
+		Where("cow_id = ?", cow.Id).
+		Where("disease_id = ?", disease.Id).
+		Where(s.DB.Where("health_status = ?", pasturePb.HealthStatus_Disease).Or("health_status = ?", pasturePb.HealthStatus_Treatment)).
+		Count(&alreadyCount).Error; err != nil {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "health.cowExist",
+		})
+		return xerr.Customf(messageId)
+	}
 
-	defer func() {
-		// 更新牛只健康状态
-		if newEventCowDisease.HealthStatus == pasturePb.HealthStatus_Disease || newEventCowDisease.HealthStatus == pasturePb.HealthStatus_Treatment {
-			cow.EventHealthStatusUpdate(pasturePb.HealthStatus_Disease)
-			if err = s.DB.Model(new(model.Cow)).
-				Select("health_status").
-				Where("id = ?", req.CowId).
-				Updates(cow).Error; err != nil {
-				zaplog.Error("CowDiseaseCreate", zap.Any("EventHealthStatusUpdate", err))
-			}
-		}
+	if alreadyCount > 0 {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "health.cowExist",
+		})
+		return xerr.Customf(messageId)
+	}
 
-		// 更新栏舍信息
-		if req.PenId > 0 {
-			penMap := s.PenMap(ctx, userModel.AppPasture.Id)
-			penData, ok := penMap[req.PenId]
-			if !ok {
-				return
-			}
-			cow.EventPenUpdate(penData)
-			if err = s.DB.Model(new(model.Cow)).
-				Select("pen_id", "pen_name").
-				Where("id = ?", cow.Id).
-				Updates(cow).Error; err != nil {
-				zaplog.Error("CowDiseaseCreate", zap.Any("EventPenUpdate", err))
-			}
-		}
-	}()
+	// 牛只疾病信息
+	newEventCowDisease := model.NewEventCowDisease(pastureId, cow, disease, req, operationUser, userModel.SystemUser)
 	// PC端h和脖环揭发直接跳过诊断过程
 	if source == model.SourcePC || req.ExposeDiseaseType == pasturePb.ExposeDiseaseType_Neck_Ring {
 		newEventCowDisease.DiagnosedResult = pasturePb.IsShow_Ok
@@ -154,7 +161,7 @@ func (s *StoreEntry) CowDiseaseCreate(ctx context.Context, req *pasturePb.EventC
 	prescription := &model.Prescription{}
 	prescriptionDetail := make([]*pasturePb.PrescriptionDrugsList, 0)
 	// 获取处方信息
-	if req.PrescriptionId > 0 && len(req.PrescriptionDetail) <= 0 {
+	if req.PrescriptionId > 0 {
 		prescription, err = s.GetPrescriptionById(ctx, pastureId, req.PrescriptionId)
 		if err != nil {
 			return xerr.WithStack(err)
@@ -182,7 +189,10 @@ func (s *StoreEntry) CowDiseaseCreate(ctx context.Context, req *pasturePb.EventC
 		if err = s.DB.Model(new(model.Prescription)).
 			Create(prescription).Error; err != nil {
 			zaplog.Error("CowDiseaseCreate", zap.Any("err", err), zap.Any("prescription", prescription))
-			return xerr.Customf("创建处方错误: %s", err.Error())
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "health.createPrescriptionFail",
+			})
+			return xerr.Customf(messageId)
 		}
 
 		newPrescriptionDrugs := model.NewPrescriptionDrugs(pastureId, prescription.Id, req.PrescriptionDetail)
@@ -238,6 +248,30 @@ func (s *StoreEntry) CowDiseaseCreate(ctx context.Context, req *pasturePb.EventC
 				return xerr.WithStack(err)
 			}
 		}
+		// 更新牛只健康状态
+		if newEventCowDisease.HealthStatus == pasturePb.HealthStatus_Disease || newEventCowDisease.HealthStatus == pasturePb.HealthStatus_Treatment {
+			cow.EventHealthStatusUpdate(newEventCowDisease.HealthStatus)
+			if err = s.DB.Model(new(model.Cow)).
+				Select("health_status").
+				Where("id = ?", req.CowId).
+				Updates(cow).Error; err != nil {
+				zaplog.Error("CowDiseaseCreate", zap.Any("EventHealthStatusUpdate", err))
+			}
+		}
+
+		// 更新栏舍信息
+		if req.PenId > 0 {
+			penMap := s.PenMap(ctx, userModel.AppPasture.Id)
+			if penData, ok := penMap[req.PenId]; ok {
+				cow.EventPenUpdate(penData)
+				if err = s.DB.Model(new(model.Cow)).
+					Select("pen_id", "pen_name").
+					Where("id = ?", cow.Id).
+					Updates(cow).Error; err != nil {
+					zaplog.Error("CowDiseaseCreate", zap.Any("EventPenUpdate", err))
+				}
+			}
+		}
 
 		// 4. 如果是脖环揭发
 		if req.ExposeDiseaseType == pasturePb.ExposeDiseaseType_Neck_Ring {
@@ -261,7 +295,11 @@ func (s *StoreEntry) CowDiseaseDiagnose(ctx context.Context, req *pasturePb.CowD
 
 	cow, err := s.GetCowInfoByCowId(ctx, userModel.AppPasture.Id, int64(req.CowId))
 	if err != nil {
-		return xerr.Customf("错误的牛只信息: %d", req.CowId)
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID:    "cow.errorCowById",
+			TemplateData: map[string]interface{}{"cowId": req.CowId},
+		})
+		return xerr.Customf(messageId)
 	}
 
 	eventCowDisease := &model.EventCowDisease{}
@@ -272,11 +310,17 @@ func (s *StoreEntry) CowDiseaseDiagnose(ctx context.Context, req *pasturePb.CowD
 		Where("pasture_id = ?", userModel.AppPasture.Id).
 		First(eventCowDisease).Error; err != nil {
 		zaplog.Error("CowDiseaseDiagnose", zap.Any("req", req), zap.Any("userModel", userModel))
-		return xerr.Custom("异常牛只数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.errorCowData",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if eventCowDisease == nil || eventCowDisease.Id <= 0 {
-		return xerr.Custom("异常牛只数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "health.errorCowData",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if req.DiagnosedResult == pasturePb.IsShow_No {
@@ -340,7 +384,6 @@ func (s *StoreEntry) CowDiseaseTreatment(ctx context.Context, req *pasturePb.Cow
 	}
 
 	pastureId := userModel.AppPasture.Id
-
 	cow, err := s.GetCowInfoByCowId(ctx, pastureId, int64(req.CowId))
 	if err != nil {
 		return xerr.WithStack(err)
@@ -349,7 +392,10 @@ func (s *StoreEntry) CowDiseaseTreatment(ctx context.Context, req *pasturePb.Cow
 	// 操作人信息
 	operationUser, err := s.GetSystemUserById(ctx, int64(req.OperationId))
 	if err != nil {
-		return xerr.Customf("操作人数据异常: %d", req.OperationId)
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.checkOperation",
+		})
+		return xerr.Customf(messageId)
 	}
 
 	// 处方信息
@@ -383,7 +429,17 @@ func (s *StoreEntry) CowDiseaseTreatment(ctx context.Context, req *pasturePb.Cow
 
 	if eventCowDisease.HealthStatus != pasturePb.HealthStatus_Disease &&
 		eventCowDisease.HealthStatus != pasturePb.HealthStatus_Treatment {
-		return xerr.Custom("异常牛只数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.errorCowData",
+		})
+		return xerr.Custom(messageId)
+	}
+
+	if eventCowDisease.DiagnosedAt < int64(req.TreatmentAt) {
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "health.treatmentTimeError",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	// 处方详情

+ 26 - 25
module/backend/event_health_more.go

@@ -35,8 +35,15 @@ func (s *StoreEntry) CowDiseaseTreatmentDetail(ctx context.Context, req *pasture
 		pref.Where("disease_id = ?", req.DiseaseId)
 	}
 
-	if req.DiseaseStartAt > 0 && req.DiseaseEndAt > 0 && req.DiseaseStartAt <= req.DiseaseEndAt {
-		pref.Where("disease_at BETWEEN ? AND ?", req.DiseaseStartAt, req.DiseaseEndAt)
+	result := &pasturePb.EventCowTreatmentDetailResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &pasturePb.EventCowTreatmentDetail{
+			List:     make([]*pasturePb.EventCowTreatment, 0),
+			Total:    0,
+			PageSize: pagination.PageSize,
+			Page:     pagination.Page,
+		},
 	}
 
 	if err = pref.Count(&count).
@@ -45,40 +52,32 @@ func (s *StoreEntry) CowDiseaseTreatmentDetail(ctx context.Context, req *pasture
 		Order("id desc").
 		First(&eventCowDisease).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return &pasturePb.EventCowTreatmentDetailResponse{
-				Code: http.StatusOK,
-				Msg:  "ok",
-				Data: &pasturePb.EventCowTreatmentDetail{
-					List:     make([]*pasturePb.EventCowTreatment, 0),
-					Total:    0,
-					PageSize: pagination.PageSize,
-					Page:     pagination.Page,
-				},
-			}, nil
+			return result, nil
 		} else {
 			return nil, xerr.WithStack(err)
 		}
 	}
 
 	eventCowTreatmentList := make([]*model.EventCowTreatment, 0)
-	if err = s.DB.Model(new(model.EventCowTreatment)).
+	pref2 := s.DB.Model(new(model.EventCowTreatment)).
 		Where("cow_disease_id = ?", req.Id).
-		Where("cow_id = ?", req.CowId).
+		Where("cow_id = ?", req.CowId)
+
+	if req.TreatmentStartAt > 0 && req.TreatmentEndAt > 0 && req.TreatmentStartAt <= req.TreatmentEndAt {
+		pref2.Where("treatment_at BETWEEN ? AND ?", req.TreatmentStartAt, req.TreatmentEndAt+86400)
+	}
+
+	if err = pref2.
 		Order("id desc").
 		Find(&eventCowTreatmentList).Error; err != nil {
 		return nil, xerr.WithStack(err)
 	}
 
-	return &pasturePb.EventCowTreatmentDetailResponse{
-		Code: http.StatusOK,
-		Msg:  "ok",
-		Data: &pasturePb.EventCowTreatmentDetail{
-			List:     model.EventCowTreatmentSlice(eventCowTreatmentList).ToPB(eventCowDisease),
-			Total:    int32(count),
-			PageSize: pagination.Page,
-			Page:     pagination.PageSize,
-		},
-	}, nil
+	result.Data.List = model.EventCowTreatmentSlice(eventCowTreatmentList).ToPB(eventCowDisease)
+	result.Data.Total = int32(count)
+	result.Data.PageSize = pagination.Page
+	result.Data.Page = pagination.PageSize
+	return result, nil
 }
 
 func (s *StoreEntry) DiseaseSuggestPrescription(ctx context.Context, diseaseId int64) (*pasturePb.ConfigOptionsListResponse, error) {
@@ -142,6 +141,9 @@ func (s *StoreEntry) CowDiseaseCurable(ctx context.Context, req *pasturePb.Event
 
 	eventCowTreatmentList := make([]*model.EventCowTreatment, 0)
 	for _, v := range eventCowDiseaseList {
+		if v.LastTreatmentAt > int64(req.CurableAt) {
+			return xerr.Customf("牛只: %s,治愈时间不能小于上次治疗时间", v.EarNumber)
+		}
 		newEventCowTreatment := model.NewEventCowCurableTreatment(userModel.AppPasture.Id, userModel.SystemUser, operationUser, v, req.Remarks, int64(req.CurableAt))
 		eventCowTreatmentList = append(eventCowTreatmentList, newEventCowTreatment)
 	}
@@ -219,7 +221,6 @@ func (s *StoreEntry) NeckRingUpdateHealth(ctx context.Context, pastureId, cowId
 	neckRingHealth := &model.NeckRingHealth{}
 	if err := s.DB.Model(new(model.NeckRingHealth)).
 		Where("pasture_id = ?", pastureId).
-		Where("is_show = ?", pasturePb.IsShow_Ok).
 		Where("id = ?", neckRingHealthWarning.NeckRingHealthId).
 		First(neckRingHealth).Error; err != nil {
 		return xerr.WithStack(err)

+ 75 - 0
module/backend/feeding.go

@@ -0,0 +1,75 @@
+package backend
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"kpt-pasture/service/httpclient"
+
+	feedingPb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+	"gitee.com/xuyiping_admin/pkg/xerr"
+)
+
+var FeedingHeaders = []*httpclient.Header{
+	{
+		Key:   "Content-Type",
+		Value: "application/json",
+	}, {
+		Key:   "Accept",
+		Value: "application/json",
+	},
+}
+
+func (s *StoreEntry) GetFeedingHomepage(ctx context.Context, req *feedingPb.FeedingHomepageRequest) (*feedingPb.FeedingHomepageResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	pasture := userModel.AppPasture
+	if pasture.FeedPastureId == 0 {
+		return nil, xerr.Customf("饲喂数据未配置")
+	}
+
+	url := fmt.Sprintf("%d/feeding/tmrdata", pasture.FeedPastureId)
+	res := &feedingPb.FeedingHomepageResponse{
+		Code: 0,
+		Msg:  "",
+		Data: make([]*feedingPb.FeedingHomepageData, 0),
+	}
+	result, err := s.HttpClient.DoGet(url, FeedingHeaders)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	if err = json.Unmarshal(result, res); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	return res, nil
+}
+
+func (s *StoreEntry) GetFeedingManagement(ctx context.Context, req *feedingPb.FeedingManagementRequest) (*feedingPb.FeedingManagementResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	pasture := userModel.AppPasture
+	if pasture.FeedPastureId == 0 {
+		return nil, xerr.Customf("饲喂数据未配置")
+	}
+
+	url := fmt.Sprintf("%d/feeding/management?typea=%s&startdate=%s&enddate=%s", pasture.FeedPastureId, req.Typea, req.Startdate, req.Enddate)
+	res := &feedingPb.FeedingManagementResponse{
+		Code: 0,
+		Msg:  "",
+		Data: []*feedingPb.FeedingManagementData{},
+	}
+	result, err := s.HttpClient.DoGet(url, FeedingHeaders)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	if err = json.Unmarshal(result, res); err != nil {
+		return nil, xerr.WithStack(err)
+	}
+	return res, nil
+}

+ 124 - 28
module/backend/goods.go

@@ -7,6 +7,8 @@ import (
 	"net/http"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"gorm.io/gorm"
 
 	"gitee.com/xuyiping_admin/pkg/xerr"
@@ -134,16 +136,24 @@ func (s *StoreEntry) NeckRingCreateOrUpdate(ctx context.Context, req *pasturePb.
 	}
 
 	if req.Items == nil || len(req.Items) == 0 {
-		return xerr.Custom("请选择要脖环数据")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "goods.selectNeckRing",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if err = s.DB.Transaction(func(tx *gorm.DB) error {
 		for _, item := range req.Items {
 			number := ""
-
 			cowInfo, err := s.GetCowInfoByEarNumber(ctx, userModel.AppPasture.Id, item.EarNumber)
 			if err != nil {
-				return xerr.Customf("该牛: %s 不存在", item.EarNumber)
+				messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+					MessageID: "cow.cowNotExist",
+					TemplateData: map[string]interface{}{
+						"earNumber": item.EarNumber,
+					},
+				})
+				return xerr.Customf(messageId)
 			}
 
 			newNeckRingLog := model.NewNeckRingBindLog(userModel.AppPasture.Id, item.Number, cowInfo, userModel.SystemUser, "")
@@ -152,11 +162,25 @@ func (s *StoreEntry) NeckRingCreateOrUpdate(ctx context.Context, req *pasturePb.
 			switch req.Status {
 			case pasturePb.NeckRingOperationStatus_Bind: // 绑定
 				if ok && neckRing.IsBind == pasturePb.NeckRingIsBind_Bind {
-					return xerr.Customf("该脖环: %s已经绑定牛只: %s", item.Number, neckRing.EarNumber)
+					messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+						MessageID: "goods.neckRingAlreadyBind",
+						TemplateData: map[string]interface{}{
+							"neckRingNumber": item.Number,
+							"earNumber":      neckRing.EarNumber,
+						},
+					})
+					return xerr.Customf(messageId)
 				}
 
 				if cowInfo.NeckRingNumber != "" {
-					return xerr.Customf("该牛只: %s,已经绑定:%s", cowInfo.EarNumber, cowInfo.NeckRingNumber)
+					messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+						MessageID: "cow.neckRingNumberBind",
+						TemplateData: map[string]interface{}{
+							"neckRingNumber": cowInfo.NeckRingNumber,
+							"earNumber":      cowInfo.EarNumber,
+						},
+					})
+					return xerr.Customf(messageId)
 				}
 
 				newNeckRing := model.NewNeckRing(userModel.AppPasture.Id, item.Number, cowInfo, userModel.SystemUser)
@@ -165,10 +189,17 @@ func (s *StoreEntry) NeckRingCreateOrUpdate(ctx context.Context, req *pasturePb.
 				}
 				number = item.Number
 				newNeckRingLog.OperationName = model.OperationNameBind
+				newNeckRingLog.Remarks = model.DefaultBind
 				// 解绑
 			case pasturePb.NeckRingOperationStatus_UnBind:
 				if ok && neckRing.IsBind != pasturePb.NeckRingIsBind_Bind {
-					return xerr.Customf("该脖环: %s,未绑定牛只", item.Number)
+					messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+						MessageID: "goods.neckRingNotBind",
+						TemplateData: map[string]interface{}{
+							"neckRingNumber": item.Number,
+						},
+					})
+					return xerr.Customf(messageId)
 				}
 				if err = tx.Model(new(model.NeckRing)).
 					Where("id = ?", neckRing.Id).
@@ -181,10 +212,18 @@ func (s *StoreEntry) NeckRingCreateOrUpdate(ctx context.Context, req *pasturePb.
 					return xerr.WithStack(err)
 				}
 				newNeckRingLog.OperationName = model.OperationNameUnbind
+				newNeckRingLog.Remarks = model.DefaultUnBind
 				// 编辑
 			case pasturePb.NeckRingOperationStatus_Edit:
 				if cowInfo.NeckRingNumber != "" && item.Number != cowInfo.NeckRingNumber {
-					return xerr.Customf("该牛只: %s,已经绑定:%s", cowInfo.EarNumber, cowInfo.NeckRingNumber)
+					messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+						MessageID: "cow.neckRingNumberBind",
+						TemplateData: map[string]interface{}{
+							"neckRingNumber": cowInfo.NeckRingNumber,
+							"earNumber":      cowInfo.EarNumber,
+						},
+					})
+					return xerr.Customf(messageId)
 				}
 
 				if err = tx.Model(new(model.NeckRing)).
@@ -286,20 +325,32 @@ func (s *StoreEntry) OutboundApply(ctx context.Context, req *pasturePb.OutboundA
 	}
 
 	if len(req.Goods) <= 0 {
-		return xerr.Custom("请选择要出库商品")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "goods.selectOutGoods",
+		})
+		return xerr.Custom(messageId)
 	}
 	var outbound *model.Outbound
 	if req.Id > 0 {
 		outbound, err = s.GetOutboundById(ctx, userModel.AppPasture.Id, int64(req.Id))
 		if err != nil || outbound == nil || outbound.Id <= 0 {
-			return xerr.Customf("该出库单不存在")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "goods.outGoodsNotExist",
+			})
+			return xerr.Customf(messageId)
 		}
 		if userModel.SystemUser.Id != int64(outbound.ApplicantId) {
-			return xerr.Custom("非申请人,无权修改该出库单")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "goods.outGoodsNotAllowEdit",
+			})
+			return xerr.Custom(messageId)
 		}
 
 		if outbound.AuditStatus != pasturePb.AuditStatus_Pending {
-			return xerr.Custom("该出库单不能修改")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "goods.outGoodsNotEdit",
+			})
+			return xerr.Custom(messageId)
 		}
 	} else {
 		// 创建出库申请
@@ -310,10 +361,16 @@ func (s *StoreEntry) OutboundApply(ctx context.Context, req *pasturePb.OutboundA
 	case pasturePb.OutType_Drugs:
 		for _, v := range req.Goods {
 			if v.Quantity <= 0 {
-				return xerr.Custom("请填写商品数量")
+				messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+					MessageID: "goods.goodsCount",
+				})
+				return xerr.Custom(messageId)
 			}
 			if v.GoodsId <= 0 {
-				return xerr.Custom("请选择要出库商品")
+				messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+					MessageID: "goods.selectOutGoods",
+				})
+				return xerr.Custom(messageId)
 			}
 
 			newDrugs := &model.Drugs{}
@@ -337,10 +394,16 @@ func (s *StoreEntry) OutboundApply(ctx context.Context, req *pasturePb.OutboundA
 	case pasturePb.OutType_Medical_Equipment:
 		for _, v := range req.Goods {
 			if v.Quantity <= 0 {
-				return xerr.Custom("请填写商品数量")
+				messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+					MessageID: "goods.goodsCount",
+				})
+				return xerr.Custom(messageId)
 			}
 			if v.GoodsId <= 0 {
-				return xerr.Custom("请选择要出库商品")
+				messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+					MessageID: "goods.selectOutGoods",
+				})
+				return xerr.Custom(messageId)
 			}
 
 			newMedicalEquipment := &model.MedicalEquipment{}
@@ -362,10 +425,12 @@ func (s *StoreEntry) OutboundApply(ctx context.Context, req *pasturePb.OutboundA
 			})
 		}
 	default:
-		return xerr.Custom("未知的出库类型")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "goods.unknownOutGoodsType",
+		})
+		return xerr.Custom(messageId)
 	}
 	unitMap := s.UnitMap()
-
 	if err = s.DB.Transaction(func(tx *gorm.DB) error {
 		if req.Id > 0 {
 			if err = tx.Model(new(model.Outbound)).
@@ -404,13 +469,14 @@ func (s *StoreEntry) OutboundList(ctx context.Context, req *pasturePb.SearchOutb
 	var count int64 = 0
 	outboundList := make([]*model.Outbound, 0)
 	pref := s.DB.Model(new(model.Outbound)).
-		Where("pasture_id = ?", userModel.AppPasture.Id)
+		Where("pasture_id = ?", userModel.AppPasture.Id).
+		Where("audit_status < ?", pasturePb.AuditStatus_Delete)
 	if req.OutType > 0 {
 		pref.Where("out_type = ?", req.OutType)
 	}
 
 	if req.StartDayTime > 0 && req.EndDayTime > 0 && req.EndDayTime >= req.StartDayTime {
-		pref.Where("applicant_at BETWEEN ? AND ?", req.StartDayTime, req.EndDayTime)
+		pref.Where("applicant_at BETWEEN ? AND ?", req.StartDayTime, req.EndDayTime+86400)
 	}
 
 	if req.Number != "" {
@@ -464,15 +530,24 @@ func (s *StoreEntry) OutboundAudit(ctx context.Context, req *pasturePb.OutboundA
 	}
 
 	if outbound == nil {
-		return xerr.Custom("出库单不存在")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "goods.outGoodsNotExist",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if req.AuditStatus != pasturePb.AuditStatus_Pass && req.AuditStatus != pasturePb.AuditStatus_Reject && req.AuditStatus != pasturePb.AuditStatus_Cancel {
-		return xerr.Custom("审核状态异常")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "goods.outGoodsAuditStatusError",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if outbound.AuditStatus != pasturePb.AuditStatus_Pending {
-		return xerr.Custom("异常出库单")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "goods.outGoodsError",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	outboundDetails, err := s.GetOutboundDetailByOutboundId(ctx, outbound.Id)
@@ -481,7 +556,10 @@ func (s *StoreEntry) OutboundAudit(ctx context.Context, req *pasturePb.OutboundA
 	}
 
 	if len(outboundDetails) <= 0 {
-		return xerr.Custom("出库单商品不存在")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "goods.outGoodsNotExist",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if err = s.DB.Transaction(func(tx *gorm.DB) error {
@@ -551,6 +629,8 @@ func (s *StoreEntry) OutboundDetail(ctx context.Context, id int64) (*pasturePb.O
 	if outbound.ExamineAt > 0 {
 		examineAtFormat = time.Unix(outbound.ExamineAt, 0).Local().Format(model.LayoutTime)
 	}
+
+	unitMap := s.UnitMap()
 	return &pasturePb.OutboundDetailResponse{
 		Code: http.StatusOK,
 		Msg:  "ok",
@@ -569,7 +649,7 @@ func (s *StoreEntry) OutboundDetail(ctx context.Context, id int64) (*pasturePb.O
 			ExamineAtFormat:   examineAtFormat,
 			GoodsItem: &pasturePb.OutboundApplyItem{
 				OutType:          outbound.OutType,
-				Goods:            model.OutboundDetailSlice(outboundLogs).ToPB(),
+				Goods:            model.OutboundDetailSlice(outboundLogs).ToPB(unitMap),
 				ApplicantRemarks: outbound.ApplicantRemarks,
 			},
 		},
@@ -588,20 +668,30 @@ func (s *StoreEntry) OutboundDelete(ctx context.Context, id int64) error {
 	}
 
 	if outbound == nil {
-		return xerr.Custom("出库单不存在")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "goods.outGoodsNotExist",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if !(outbound.AuditStatus == pasturePb.AuditStatus_Pending || outbound.AuditStatus == pasturePb.AuditStatus_Cancel) {
-		return xerr.Custom("出库单无法删除")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "goods.outGoodsNotDelete",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if userModel.SystemUser.Id != int64(outbound.ApplicantId) {
-		return xerr.Custom("非申请人,无权删除出库单")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "goods.outGoodsNotAllowDelete",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	outbound.Delete()
 	if err = s.DB.Model(new(model.Outbound)).
 		Select("audit_status").
+		Where("id = ?", outbound.Id).
 		Updates(outbound).Error; err != nil {
 		return xerr.WithStack(err)
 	}
@@ -662,7 +752,13 @@ func (s *StoreEntry) FrozenSemenCreate(ctx context.Context, req *pasturePb.Searc
 			Where("pasture_id = ?", userModel.AppPasture.Id).
 			First(histData).
 			Error; err != nil {
-			return xerr.Customf("该数据不存在:%d", req.Id)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "goods.dataNotExist",
+				TemplateData: map[string]interface{}{
+					"id": req.Id,
+				},
+			})
+			return xerr.Customf(messageId)
 		}
 
 		histData.UpdateData(req)

+ 142 - 0
module/backend/indicators.go

@@ -2,6 +2,7 @@ package backend
 
 import (
 	"context"
+	"fmt"
 	"kpt-pasture/model"
 	"kpt-pasture/util"
 	"net/http"
@@ -135,3 +136,144 @@ func (s *StoreEntry) LongTermInfertility(ctx context.Context, req *pasturePb.Lon
 	}, nil
 
 }
+
+func (s *StoreEntry) AlreadySale(ctx context.Context, req *pasturePb.AlreadySalesReportRequest, pagination *pasturePb.PaginationModel) (*pasturePb.AlreadySalesReportResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	pastureId := userModel.AppPasture.Id
+	eventSaleList := make([]*model.EventSale, 0)
+	pref := s.DB.Table(fmt.Sprintf("%s AS a", new(model.EventSale).TableName())).
+		Select("a.id,a.dealer_name,a.sale_kind,a.sale_price,a.sale_all_weight,a.sale_all_amount,sale_cow_count,a.sale_at,a.remarks").
+		Joins(fmt.Sprintf("JOIN %s AS b on b.sale_id = a.id", new(model.EventSaleCow).TableName())).
+		Where("a.pasture_id = ?", pastureId).
+		Where("a.sale_at BETWEEN ? AND ?", req.StartAt, req.EndAt)
+
+	if req.BatchNumber != "" {
+		pref.Where("b.batch_number = ?", req.BatchNumber)
+	}
+
+	if len(req.PenId) > 0 {
+		pref.Where("b.pen_id IN ?", req.PenId)
+	}
+
+	if req.CowKind > pasturePb.CowKind_Invalid {
+		pref.Where("b.cow_kind = ?", req.CowKind)
+	}
+
+	if err = pref.
+		Limit(int(pagination.PageSize)).
+		Offset(int(pagination.PageOffset)).
+		Group("a.id").
+		Find(&eventSaleList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	result := &pasturePb.AlreadySalesReportResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &pasturePb.AlreadySalesReportData{
+			List:     make([]*pasturePb.AlreadySalesReport, 0),
+			Total:    0,
+			PageSize: pagination.PageSize,
+			Page:     pagination.Page,
+		},
+	}
+
+	if len(eventSaleList) <= 0 {
+		return result, nil
+	}
+
+	saleIds := make([]int64, 0)
+	eventSaleMap := make(map[int64]*model.EventSale)
+	for _, v := range eventSaleList {
+		saleIds = append(saleIds, v.Id)
+		eventSaleMap[v.Id] = v
+	}
+
+	eventSaleCowList := make([]*model.EventSaleCow, 0)
+	if err = s.DB.Model(new(model.EventSaleCow)).
+		Where("sale_id IN ?", saleIds).
+		Where("pasture_id = ?", pastureId).
+		Find(&eventSaleCowList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	cowIds := make([]int64, 0)
+	for _, v := range eventSaleCowList {
+		cowIds = append(cowIds, v.CowId)
+	}
+
+	cowList := make([]*model.Cow, 0)
+	if err = s.DB.Model(new(model.Cow)).
+		Where("id IN ?", cowIds).
+		Where("pasture_id = ?", pastureId).
+		Find(&cowList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	cowMap := make(map[int64]*model.Cow)
+	for _, v := range cowList {
+		cowMap[v.Id] = v
+	}
+
+	sourceMap := s.CowSourceMap()
+	cowKindMap := s.CowKindMap()
+	result.Data.Total = int32(len(eventSaleCowList))
+	result.Data.List = model.EventSaleCowSlice(eventSaleCowList).ToPB(eventSaleMap, cowKindMap, cowMap, sourceMap)
+	return result, nil
+}
+
+func (s *StoreEntry) CanSale(ctx context.Context, req *pasturePb.CanSalesReportRequest, pagination *pasturePb.PaginationModel) (*pasturePb.CanSalesReportResponse, error) {
+	userModel, err := s.GetUserModel(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	pastureId := userModel.AppPasture.Id
+	cowList := make([]*model.Cow, 0)
+	var count int64
+	pref := s.DB.Model(new(model.Cow)).
+		Where("pasture_id = ?", pastureId).
+		Where("current_weight BETWEEN ? AND ?", req.WeightStart*1000, req.WeightEnd*1000)
+
+	if req.BatchNumber != "" {
+		pref.Where("batch_number = ?", req.BatchNumber)
+	}
+
+	if req.CowKind > pasturePb.CowKind_Invalid {
+		pref.Where("cow_kind = ?", req.CowKind)
+	}
+
+	if len(req.PenId) > 0 {
+		pref.Where("pen_id IN ?", req.PenId)
+	}
+
+	if err = pref.Count(&count).
+		Limit(int(pagination.PageSize)).
+		Offset(int(pagination.PageOffset)).
+		Find(&cowList).Error; err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
+	result := &pasturePb.CanSalesReportResponse{
+		Code: http.StatusOK,
+		Msg:  "ok",
+		Data: &pasturePb.CanSalesReportData{
+			List:     make([]*pasturePb.CanSalesReport, 0),
+			Total:    0,
+			PageSize: pagination.PageSize,
+			Page:     pagination.Page,
+		},
+	}
+
+	if len(cowList) <= 0 {
+		return result, nil
+	}
+	cowKindMap := s.CowKindMap()
+	result.Data.Total = int32(count)
+	result.Data.List = model.CowSlice(cowList).CanSaleToPB(cowKindMap)
+	return result, nil
+}

+ 20 - 7
module/backend/interface.go

@@ -5,6 +5,7 @@ import (
 	"kpt-pasture/config"
 	"kpt-pasture/model"
 	"kpt-pasture/service/asynqsvc"
+	"kpt-pasture/service/httpclient"
 	"kpt-pasture/service/wechat"
 	"kpt-pasture/store/kptstore"
 	"mime/multipart"
@@ -24,10 +25,11 @@ type Hub struct {
 type StoreEntry struct {
 	dig.In
 
-	Cfg         *config.AppConfig
-	DB          *kptstore.DB
-	HttpClient  *wechat.ClientService
-	AsynqClient asynqsvc.Client
+	Cfg          *config.AppConfig
+	DB           *kptstore.DB
+	WeChatClient *wechat.ClientService
+	AsynqClient  asynqsvc.Client
+	HttpClient   *httpclient.Service
 }
 
 func NewStore(store StoreEntry) KptService {
@@ -36,9 +38,11 @@ func NewStore(store StoreEntry) KptService {
 
 func NewStoreEntry(cfg *config.AppConfig, Db *kptstore.DB) *StoreEntry {
 	return &StoreEntry{
-		Cfg:        cfg,
-		DB:         Db,
-		HttpClient: nil,
+		Cfg:          cfg,
+		DB:           Db,
+		WeChatClient: nil,
+		AsynqClient:  nil,
+		HttpClient:   nil,
 	}
 }
 
@@ -56,6 +60,7 @@ type KptService interface {
 	MilkHallService      // 奶厅数据相关
 	UploadService        // 上传文件相关
 	WarningService       // 预警相关
+	FeedingService       // 饲喂相关
 	TestService          // 测试相关
 }
 
@@ -258,6 +263,8 @@ type CowService interface {
 
 	IndicatorsComparison(ctx context.Context, req *pasturePb.IndicatorsComparisonRequest) (*model.IndicatorsComparisonResponse, error)
 	LongTermInfertility(ctx context.Context, req *pasturePb.LongTermInfertilityRequest, pagination *pasturePb.PaginationModel) (*pasturePb.LongTermInfertilityResponse, error)
+	AlreadySale(ctx context.Context, req *pasturePb.AlreadySalesReportRequest, pagination *pasturePb.PaginationModel) (*pasturePb.AlreadySalesReportResponse, error)
+	CanSale(ctx context.Context, req *pasturePb.CanSalesReportRequest, pagination *pasturePb.PaginationModel) (*pasturePb.CanSalesReportResponse, error)
 }
 
 //go:generate mockgen -destination mock/GoodsService.go -package kptservicemock kpt-pasture/module/backend GoodsService
@@ -313,6 +320,7 @@ type DashboardService interface {
 	DataWarningPop(ctx context.Context, req *pasturePb.WarningDataListRequest, pagination *pasturePb.PaginationModel) (*model.WarningDataPopResponse, error)
 	CalendarToDoCount(ctx context.Context) (*pasturePb.TodoCountResponse, error)
 	Equipment(ctx context.Context) (*pasturePb.EquipmentResponse, error)
+	OutNumber(ctx context.Context) (*pasturePb.OutNumberResponse, error)
 }
 
 //go:generate mockgen -destination mock/WorkService.go -package kptservicemock kpt-pasture/module/backend WorkService
@@ -359,6 +367,11 @@ type UploadService interface {
 	ImportExcel2(ctx context.Context, data [][]string, excelHeader []string) error
 }
 
+type FeedingService interface {
+	GetFeedingHomepage(ctx context.Context, req *pasturePb.FeedingHomepageRequest) (*pasturePb.FeedingHomepageResponse, error)
+	GetFeedingManagement(ctx context.Context, req *pasturePb.FeedingManagementRequest) (*pasturePb.FeedingManagementResponse, error)
+}
+
 type TestService interface {
 	CowNeckRingNumberBound(ctx context.Context, pagination *pasturePb.PaginationModel) error
 	CowNeckRingNumberBound2(ctx context.Context, pagination *pasturePb.PaginationModel) error

+ 63 - 16
module/backend/neck_ring_warning.go

@@ -6,8 +6,11 @@ import (
 	"kpt-pasture/model"
 	"kpt-pasture/util"
 	"net/http"
+	"sort"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"go.uber.org/zap"
 	"gorm.io/gorm"
 
@@ -22,7 +25,7 @@ func (s *StoreEntry) NeckRingWarningEstrusOrAbortionCowList(ctx context.Context,
 		return nil, xerr.WithStack(err)
 	}
 
-	var count int64
+	var count int32
 	neckRingEstrusList := make([]*model.NeckRingEstrusWarning, 0)
 	var pref *gorm.DB
 	switch req.Kind {
@@ -50,13 +53,17 @@ func (s *StoreEntry) NeckRingWarningEstrusOrAbortionCowList(ctx context.Context,
 		pref.Where("b.pen_id IN ?", req.PenIds)
 	}
 
-	if err = pref.Group("a.cow_id").
-		Order("a.level DESC").
-		Count(&count).
-		Limit(int(pagination.PageSize)).
-		Offset(int(pagination.PageOffset)).
-		Find(&neckRingEstrusList).Error; err != nil {
-		return nil, xerr.WithStack(err)
+	// 按照发情时间和发情等级倒叙排序
+	if req.MatingWindowPeriod > 0 {
+		if err = pref.Group("a.cow_id").
+			Find(&neckRingEstrusList).Error; err != nil {
+			return nil, xerr.WithStack(err)
+		}
+	} else {
+		if err = pref.Group("a.cow_id").
+			Find(&neckRingEstrusList).Error; err != nil {
+			return nil, xerr.WithStack(err)
+		}
 	}
 
 	cowMap := make(map[int64]*model.Cow)
@@ -71,21 +78,39 @@ func (s *StoreEntry) NeckRingWarningEstrusOrAbortionCowList(ctx context.Context,
 		for _, log := range lastEventLogList {
 			eventLogMap[log.CowId] = log.EventDescription
 		}
-	}
 
-	if len(cowIds) > 0 {
 		cowList, _ := s.GetCowInfoByCowIds(ctx, userModel.AppPasture.Id, cowIds)
 		for _, cow := range cowList {
 			cowMap[cow.Id] = cow
 		}
 	}
+	list := model.NeckRingEstrusWarningSlice(neckRingEstrusList).ToPB(cowMap, eventLogMap, req.MatingWindowPeriod)
+	// 排序 先按照高峰时间正序排,然后再按照发情等级倒叙排
+	sort.Slice(list, func(i, j int) bool {
+		if list[i].PostPeakTimeForHours != list[j].PostPeakTimeForHours {
+			return list[i].PostPeakTimeForHours < list[j].PostPeakTimeForHours
+		}
+		return list[i].Level > list[j].Level
+	})
+	// 分页
+	count = int32(len(list))
+	if count > pagination.PageOffset {
+		end := pagination.PageOffset + pagination.PageSize
+		if end > count {
+			end = count
+		}
+		list = list[pagination.PageOffset:end]
+	} else {
+		// 如果偏移量已超过列表长度,返回空列表
+		list = list[:0]
+	}
 
 	return &pasturePb.EstrusResponse{
 		Code: http.StatusOK,
 		Msg:  "ok",
 		Data: &pasturePb.EstrusData{
-			List:     model.NeckRingEstrusWarningSlice(neckRingEstrusList).ToPB(cowMap, eventLogMap, req.MatingWindowPeriod),
-			Total:    int32(count),
+			List:     list,
+			Total:    count,
 			PageSize: pagination.PageSize,
 			Page:     pagination.Page,
 		},
@@ -129,6 +154,7 @@ func (s *StoreEntry) NeckRingWarningHealthCowList(ctx context.Context, req *past
 	}
 
 	warningHealthLevelMap := s.WarningHealthLevelMap()
+	healthStatusMap := s.HealthStatusMap()
 	cowMap := make(map[int64]*model.Cow)
 	eventLogMap := make(map[int64]string)
 	cowIds := make([]int64, 0)
@@ -145,7 +171,11 @@ func (s *StoreEntry) NeckRingWarningHealthCowList(ctx context.Context, req *past
 		for _, cow := range cowList {
 			cowMap[cow.Id] = cow
 		}
+
+		// 获取牛的牛当前脖环数据
+
 	}
+
 	return &pasturePb.HealthWarningResponse{
 		Code: http.StatusOK,
 		Msg:  "ok",
@@ -153,7 +183,12 @@ func (s *StoreEntry) NeckRingWarningHealthCowList(ctx context.Context, req *past
 			Total:    int32(count),
 			Page:     pagination.Page,
 			PageSize: pagination.PageSize,
-			List:     model.NeckRingHealthWarningSlice(neckWaringHealthList).ToPB(warningHealthLevelMap, cowMap, eventLogMap),
+			List: model.NeckRingHealthWarningSlice(neckWaringHealthList).ToPB(
+				warningHealthLevelMap,
+				cowMap,
+				eventLogMap,
+				healthStatusMap,
+			),
 		},
 	}, nil
 
@@ -165,7 +200,10 @@ func (s *StoreEntry) NeckRingNoEstrusBatch(ctx context.Context, req *pasturePb.N
 		return xerr.WithStack(err)
 	}
 	if len(req.EarNumbers) <= 0 {
-		return xerr.Custom("请选择牛号")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.earNumber",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	nowTime := time.Now().Local()
@@ -200,7 +238,13 @@ func (s *StoreEntry) NeckRingNoEstrusBatch(ctx context.Context, req *pasturePb.N
 			Where("is_show = ?", pasturePb.IsShow_Ok).
 			First(neckRingEstrus).Error; err != nil {
 			zaplog.Error("NeckRingNoEstrusBatch", zap.Any("err", err), zap.Any("neckRingEstrusWarning", v))
-			return xerr.Customf("数据异常: %s", v.EarNumber)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "cow.dataError",
+				TemplateData: map[string]interface{}{
+					"earNumber": v.EarNumber,
+				},
+			})
+			return xerr.Customf(messageId)
 		}
 		neckRingEstrusIds = append(neckRingEstrusIds, neckRingEstrus.Id)
 	}
@@ -245,7 +289,10 @@ func (s *StoreEntry) NeckRingNoDiseaseBatch(ctx context.Context, req *pasturePb.
 		return xerr.WithStack(err)
 	}
 	if len(req.EarNumbers) <= 0 {
-		return xerr.Custom("请选择牛号")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "cow.earNumber",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	nowTime := time.Now().Local()

+ 6 - 3
module/backend/pasture.go

@@ -7,6 +7,8 @@ import (
 	"kpt-pasture/model"
 	"net/http"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"gitee.com/xuyiping_admin/pkg/xerr"
 	"gorm.io/gorm"
 
@@ -459,7 +461,6 @@ func (s *StoreEntry) SearchDealerList(crx context.Context, req *pasturePb.Search
 	if err != nil {
 		return nil, xerr.WithStack(err)
 	}
-
 	saleDealerList := make([]*model.SaleDealer, 0)
 	pref := s.DB.Model(new(model.SaleDealer)).
 		Where("pasture_id = ?", userModel.AppPasture.Id)
@@ -471,7 +472,6 @@ func (s *StoreEntry) SearchDealerList(crx context.Context, req *pasturePb.Search
 	if err = pref.Where("is_show = ?", pasturePb.IsShow_Ok).Find(&saleDealerList).Error; err != nil {
 		return nil, xerr.WithStack(err)
 	}
-
 	return &pasturePb.SearchDealerResponse{
 		Code: http.StatusOK,
 		Msg:  "ok",
@@ -493,7 +493,10 @@ func (s *StoreEntry) DeleteDealer(crx context.Context, id int64) error {
 		Where("id = ? and pasture_id = ?", id, userModel.AppPasture.Id).
 		First(saleDealer).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return xerr.Custom("未找到该经销商信息")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "pasture.dealerNotExist",
+			})
+			return xerr.Custom(messageId)
 		}
 		return xerr.WithStack(err)
 	}

+ 67 - 16
module/backend/sql.go

@@ -7,13 +7,13 @@ import (
 	"kpt-pasture/model"
 	"strings"
 
-	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
-	"go.uber.org/zap"
-
-	operationPb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/operation"
-
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+	operationPb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/operation"
+	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
 	"gitee.com/xuyiping_admin/pkg/xerr"
+
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+	"go.uber.org/zap"
 	"gorm.io/gorm"
 )
 
@@ -27,9 +27,17 @@ func (s *StoreEntry) GetUserModel(ctx context.Context) (*model.UserModel, error)
 		return nil, xerr.WithStack(err)
 	}
 
+	i18nTemplate, err := s.GetI18nTemplate(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
+
 	systemUserPastureIds := strings.Split(systemUser.PastureIds, ",")
 	if len(systemUserPastureIds) == 0 {
-		return nil, xerr.Custom("当前用户未配置相关牧场数据,请联系管理员!")
+		message, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.unPasture",
+		})
+		return nil, xerr.Custom(message)
 	}
 
 	var info bool
@@ -38,13 +46,18 @@ func (s *StoreEntry) GetUserModel(ctx context.Context) (*model.UserModel, error)
 			info = true
 		}
 	}
+
 	if !info {
-		return nil, xerr.Custom("该用户未没有该牧场操作权限,请联系管理员开通牧场权限")
+		message, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.unPasture",
+		})
+		return nil, xerr.Custom(message)
 	}
 
 	return &model.UserModel{
-		SystemUser: systemUser,
-		AppPasture: appPasture,
+		SystemUser:      systemUser,
+		AppPasture:      appPasture,
+		LanguageContent: i18nTemplate,
 	}, nil
 }
 
@@ -54,6 +67,10 @@ func (s *StoreEntry) GetCurrentSystemUser(ctx context.Context) (*model.SystemUse
 	if err != nil {
 		return nil, xerr.WithStack(err)
 	}
+	i18nTemplate, err := s.GetI18nTemplate(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
 	// 根据用户token获取用户数据
 	systemUser := &model.SystemUser{Name: userName}
 	if err = s.DB.Model(new(model.SystemUser)).
@@ -62,16 +79,28 @@ func (s *StoreEntry) GetCurrentSystemUser(ctx context.Context) (*model.SystemUse
 		zaplog.Error("GetCurrentSystemUser", zap.Any("err", err), zap.Any("userName", userName))
 
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return nil, xerr.Custom("当前登录用户数据不存在")
+			messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.noUser",
+			})
+			return nil, xerr.Custom(messageId)
 		} else {
-			return nil, xerr.Custom("用户登录信息有误,请退出重新登录")
+			messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.reLogin",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 	}
 	if systemUser.IsDelete != pasturePb.IsShow_Ok {
-		return nil, xerr.Custom("当前用户数据已经删除")
+		messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.userDelete",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 	if systemUser.IsShow != pasturePb.IsShow_Ok {
-		return nil, xerr.Custom("当前用户已禁用")
+		messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.userDisable",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 	return systemUser, nil
 }
@@ -85,9 +114,9 @@ func (s *StoreEntry) GetSystemUserDepthRole(ctx context.Context, pastureId, user
 		zaplog.Error("GetSystemUserDepthRole", zap.Any("err", err), zap.Any("pastureId", pastureId), zap.Any("userId", userId))
 
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return nil, xerr.Custom("当前用户未绑定角色数据,请重新绑定")
+			return nil, xerr.Custom("当前用户未绑定角色数据,请重新绑定!")
 		} else {
-			return nil, xerr.Custom("用户信息有误")
+			return nil, xerr.Custom("用户信息有误!")
 		}
 	}
 
@@ -120,6 +149,28 @@ func (s *StoreEntry) GetCurrentUserName(ctx context.Context) (string, error) {
 	}
 }
 
+// GetI18nTemplate 获取用户多语言对应文件
+func (s *StoreEntry) GetI18nTemplate(ctx context.Context) (*i18n.Localizer, error) {
+	lg := ctx.Value(LanguageContent)
+	if lg == nil {
+		return nil, xerr.Customf("language error")
+	}
+	lag, ok := lg.(*i18n.Localizer)
+	if !ok {
+		return nil, xerr.Customf("language error")
+	}
+	return lag, nil
+}
+
+// GetCurrentUserLanguage 获取用户语言标志
+func (s *StoreEntry) GetCurrentUserLanguage(ctx context.Context) string {
+	lg := ctx.Value(Language)
+	if lg != nil {
+		return lg.(string)
+	}
+	return "zh"
+}
+
 // GetFarmId 获取当前牧场Id
 func (s *StoreEntry) GetFarmId(ctx context.Context) string {
 	farmId := ctx.Value(CurrentFarmId)
@@ -208,7 +259,7 @@ func (s *StoreEntry) GetCowInfoByEarNumber(ctx context.Context, pastureId int64,
 		Where("admission_status = ?", pasturePb.AdmissionStatus_Admission).
 		First(cowInfo).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return nil, xerr.Customf("该牛只数据不存在: %s", earNumber)
+			return nil, xerr.Customf("The data for cow: %s does not exist!", earNumber)
 		} else {
 			return nil, xerr.WithStack(err)
 		}

+ 100 - 25
module/backend/system_service.go

@@ -10,6 +10,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
 	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
 	"go.uber.org/zap"
 
@@ -23,35 +25,56 @@ import (
 const (
 	CurrentUserName = "userName"
 	CurrentFarmId   = "FarmId"
+	LanguageContent = "languageContent"
+	Language        = "language"
 )
 
 // Login 用户登录
 func (s *StoreEntry) Login(ctx context.Context, req *pasturePb.SearchUserRequest) (*pasturePb.SystemUserResponse, error) {
+	i18nTemplate, err := s.GetI18nTemplate(ctx)
+	if err != nil {
+		return nil, xerr.WithStack(err)
+	}
 	systemUser := &model.SystemUser{}
-	if err := s.DB.Model(new(model.SystemUser)).
+	if err = s.DB.Model(new(model.SystemUser)).
 		Where("name = ?", req.Name).
 		First(systemUser).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return nil, xerr.Customf("用户不存在: %s", req.Name)
+			messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.noUser",
+			})
+			return nil, xerr.Customf(messageId)
 		} else {
 			return nil, xerr.WithStack(err)
 		}
 	}
 
 	if systemUser.Password != req.Password {
-		return nil, xerr.Customf("密码错误,来自用户:%s", req.Name)
+		messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.wrongPassword",
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	if systemUser.IsShow == pasturePb.IsShow_No {
-		return nil, xerr.Customf("该账号已被禁用,请联系管理员")
+		messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.userDisable",
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	if systemUser.IsDelete == pasturePb.IsShow_No {
-		return nil, xerr.Customf("该账号已被删除,请联系管理员")
+		messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.userDelete",
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	if len(systemUser.PastureIds) <= 0 {
-		return nil, xerr.Custom("当前用户未配置相关牧场数据,请联系管理员!")
+		messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.unPasture",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	jwtToken := jwt.NewJWTTokenGen(s.Cfg.AppName, s.Cfg.JwtTokenKeyConfig.PrivateKey)
@@ -60,13 +83,19 @@ func (s *StoreEntry) Login(ctx context.Context, req *pasturePb.SearchUserRequest
 		return nil, xerr.WithStack(err)
 	}
 	if token == "" {
-		return nil, xerr.Custom("获取token错误")
+		messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.errorToken",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	expires := time.Now().Local().Add(time.Duration(s.Cfg.JwtExpireTime) * time.Second).Format(util.LayoutTime)
 	farmList, err := s.FindPastureListByIds(ctx, systemUser.GetPastureIds())
 	if err != nil || len(farmList) == 0 {
-		return nil, xerr.Custom("牧场信息错误")
+		messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.errorPasture",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	systemUserDepthRole, err := s.GetSystemUserDepthRole(ctx, farmList[0].Id, systemUser.Id)
@@ -197,7 +226,10 @@ func (s *StoreEntry) DeleteSystemUser(ctx context.Context, userId int64) error {
 	systemUser := &model.SystemUser{Id: userId}
 	if err = s.DB.Model(new(model.SystemUser)).First(systemUser).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return xerr.Custom("该用户不存在")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.noUser",
+			})
+			return xerr.Custom(messageId)
 		}
 		return xerr.WithStack(err)
 	}
@@ -251,7 +283,7 @@ func (s *StoreEntry) IsShowSystemUser(ctx context.Context, userId int64) error {
 
 // SystemUserCreateOrUpdate 创建或者更新系统用户
 func (s *StoreEntry) SystemUserCreateOrUpdate(ctx context.Context, req *pasturePb.SearchUserRequest) error {
-	_, err := s.GetUserModel(ctx)
+	userModel, err := s.GetUserModel(ctx)
 	if err != nil {
 		return xerr.WithStack(err)
 	}
@@ -324,7 +356,10 @@ func (s *StoreEntry) SystemUserCreateOrUpdate(ctx context.Context, req *pastureP
 		}
 
 		if count > 0 {
-			return xerr.Customf("系统中该用户名称已经存在: %s_%s", req.Name, req.Mobile)
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.userNameExist",
+			})
+			return xerr.Customf(messageId)
 		}
 
 		newSystemUser := model.NewSystemUser(req, pastureIds, req.PastureDepthList)
@@ -373,7 +408,10 @@ func (s *StoreEntry) GetSystemUserMenu(ctx context.Context) (*pasturePb.SystemUs
 	}
 
 	if len(systemRoleList) <= 0 {
-		return nil, xerr.Custom("该用户角色不存在")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.userRoleNotExist",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	systemRoleMenuList := make([]*model.SystemRoleMenu, 0)
@@ -384,7 +422,10 @@ func (s *StoreEntry) GetSystemUserMenu(ctx context.Context) (*pasturePb.SystemUs
 	}
 
 	if len(systemRoleMenuList) <= 0 {
-		return nil, xerr.Custom("该用户角色没有菜单权限")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.noMenuPermission",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	// 菜单Id
@@ -409,17 +450,24 @@ func (s *StoreEntry) GetSystemUserMenu(ctx context.Context) (*pasturePb.SystemUs
 
 // ResetPasswordSystemUser 重置系统用户密码
 func (s *StoreEntry) ResetPasswordSystemUser(ctx context.Context, req *pasturePb.ResetUserPasswordRequest) error {
+	i18nTemplate, err := s.GetI18nTemplate(ctx)
+	if err != nil {
+		return xerr.WithStack(err)
+	}
 	systemUser := &model.SystemUser{}
-	if err := s.DB.Model(new(model.SystemUser)).
+	if err = s.DB.Model(new(model.SystemUser)).
 		Where("id = ?", req.Id).
 		First(systemUser).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return xerr.Custom("该用户不存在")
+			messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.noUser",
+			})
+			return xerr.Custom(messageId)
 		}
 		return xerr.WithStack(err)
 	}
 
-	if err := s.DB.Model(systemUser).
+	if err = s.DB.Model(systemUser).
 		Update("password", req.Password).Error; err != nil {
 		return xerr.WithStack(err)
 	}
@@ -437,13 +485,19 @@ func (s *StoreEntry) SystemUserRole(ctx context.Context, userId int64) (*pasture
 		Where("is_delete = ?", pasturePb.IsShow_Ok).
 		First(systemUser).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return nil, xerr.Custom("该用户不存在")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.noUser",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 		return nil, xerr.WithStack(err)
 	}
 
 	if systemUser.IsShow == pasturePb.IsShow_No {
-		return nil, xerr.Custom("该用户已禁用")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.userDisable",
+		})
+		return nil, xerr.Custom(messageId)
 	}
 
 	systemUserDepthRole := &model.SystemUserDepthRole{}
@@ -452,7 +506,10 @@ func (s *StoreEntry) SystemUserRole(ctx context.Context, userId int64) (*pasture
 		Where("pasture_id = ?", userModel.AppPasture.Id).
 		First(systemUserDepthRole).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return nil, xerr.Custom("该用户没有角色")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.userRoleNotExist",
+			})
+			return nil, xerr.Custom(messageId)
 		}
 		return nil, xerr.WithStack(err)
 	}
@@ -476,17 +533,26 @@ func (s *StoreEntry) SystemUserRoleSave(ctx context.Context, req *pasturePb.Syst
 	if err = s.DB.Model(new(model.SystemUser)).
 		First(systemUser).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return xerr.Custom("该用户不存在")
+			messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+				MessageID: "auth.noUser",
+			})
+			return xerr.Custom(messageId)
 		}
 		return xerr.WithStack(err)
 	}
 
 	if systemUser.IsDelete == pasturePb.IsShow_No {
-		return xerr.Custom("该用户已删除")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.userDelete",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	if systemUser.IsShow == pasturePb.IsShow_No {
-		return xerr.Custom("该用户已禁用")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.userDisable",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	systemRoleList := make([]*model.SystemRole, 0)
@@ -499,7 +565,10 @@ func (s *StoreEntry) SystemUserRoleSave(ctx context.Context, req *pasturePb.Syst
 	}
 
 	if len(systemRoleList) <= 0 {
-		return xerr.Custom("该用户没有角色")
+		messageId, _ := userModel.LanguageContent.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.userRoleNotExist",
+		})
+		return xerr.Custom(messageId)
 	}
 
 	roleIdsStr := ""
@@ -512,7 +581,6 @@ func (s *StoreEntry) SystemUserRoleSave(ctx context.Context, req *pasturePb.Syst
 	}
 
 	if err = s.DB.Transaction(func(tx *gorm.DB) error {
-
 		var systemUserDepthRoleCount int64
 		if err = tx.Model(new(model.SystemUserDepthRole)).
 			Where("user_id = ?", systemUser.Id).
@@ -547,7 +615,14 @@ func (s *StoreEntry) SystemUserRoleSave(ctx context.Context, req *pasturePb.Syst
 
 func (s *StoreEntry) GetMenusWithParents(ctx context.Context, pastureId int64, menuIds []int64) ([]*model.SystemMenu, error) {
 	if len(menuIds) <= 0 {
-		return nil, xerr.Customf("菜单id不能为空")
+		i18nTemplate, err := s.GetI18nTemplate(ctx)
+		if err != nil {
+			return nil, xerr.Customf("菜单id不能为空")
+		}
+		messageId, _ := i18nTemplate.Localize(&i18n.LocalizeConfig{
+			MessageID: "auth.menuIdNotEmpty",
+		})
+		return nil, xerr.Customf(messageId)
 	}
 
 	menuIds = util.DeduplicateInt64(menuIds)

+ 1 - 1
module/crontab/health_waning.go

@@ -104,7 +104,7 @@ func (e *Entry) FindNewNeckRingHealthWarning(pastureId int64, healthValue int32)
 			zap.Any("healthValue", healthValue),
 			zap.Any("cowInfo", cowInfo),
 		)
-		if newScore > healthValue {
+		if newScore >= healthValue {
 			continue
 		}
 

+ 1 - 0
module/crontab/model.go

@@ -65,6 +65,7 @@ type FilterData struct {
 	ChangeFilter   int32
 	RuminaFilter   int32
 	ChewFilter     int32
+	ChangeHigh     int32
 }
 
 type SumHabit struct {

+ 96 - 48
module/crontab/neck_ring_calculate.go

@@ -20,7 +20,7 @@ func (e *Entry) NeckRingCalculate() error {
 		return nil
 	}
 	for _, pasture := range pastureList {
-		if err := e.EntryUpdateActiveHabit(pasture.Id); err != nil {
+		if err := e.EntryUpdateActiveHabit(pasture); err != nil {
 			zaplog.Error("NeckRingCalculate", zap.Any("err", err), zap.Any("pasture", pasture))
 		}
 		zaplog.Info(fmt.Sprintf("NeckRingCalculate Success %d", pasture.Id))
@@ -28,7 +28,8 @@ func (e *Entry) NeckRingCalculate() error {
 	return nil
 }
 
-func (e *Entry) EntryUpdateActiveHabit(pastureId int64) (err error) {
+func (e *Entry) EntryUpdateActiveHabit(appPasture *model.AppPastureList) (err error) {
+	pastureId := appPasture.Id
 	// 获取这段执行数据内最大日期和最小日期
 	xToday, err := e.XToday(pastureId)
 	if err != nil {
@@ -47,7 +48,6 @@ func (e *Entry) EntryUpdateActiveHabit(pastureId int64) (err error) {
 		zaplog.Error("NeckRingCalculate", zap.Any("pastureId", pastureId), zap.Any("FirstFilterUpdate", err), zap.Any("xToday", xToday))
 	}
 
-	zaplog.Info("NeckRingCalculate", zap.Any("pastureId", pastureId), zap.Any("xToday", xToday), zap.Any("processIds", processIds))
 	if len(processIds) <= 0 {
 		return nil
 	}
@@ -58,7 +58,7 @@ func (e *Entry) EntryUpdateActiveHabit(pastureId int64) (err error) {
 	e.SecondUpdateChangeFilter(pastureId, processIds, xToday)
 
 	// 活动量校正系数和健康评分
-	e.FilterCorrectAndScoreUpdate(pastureId, processIds, xToday)
+	e.FilterCorrectAndScoreUpdate(appPasture, processIds, xToday)
 
 	// 更新 ChangeFilter
 	e.UpdateChangeFilter(pastureId, processIds)
@@ -67,7 +67,7 @@ func (e *Entry) EntryUpdateActiveHabit(pastureId int64) (err error) {
 	e.UpdateFilterCorrect(pastureId, processIds)
 
 	// 插入群体校正表
-	e.UpdateChangeAdJust(pastureId, xToday)
+	e.UpdateChangeAdJust(pastureId, processIds)
 
 	// 更新 Cft
 	e.UpdateCft(pastureId, processIds)
@@ -171,7 +171,7 @@ func (e *Entry) FirstFilterUpdate(pastureId int64, xToDay *XToday) (processIds [
 		processIds = append(processIds, v.Id)
 		// 更新过滤值
 		if err = e.DB.Model(new(model.NeckActiveHabit)).
-			Select("filter_high", "filter_rumina", "filter_chew", "cow_id", "lact", "calving_age", "ear_number", "week_high").
+			Select("filter_high", "filter_rumina", "filter_chew", "cow_id", "lact", "calving_age", "ear_number", "pen_id", "week_high").
 			Where("id = ?", v.Id).
 			Updates(map[string]interface{}{
 				"filter_high":   firstFilterData.FilterHigh,
@@ -181,6 +181,7 @@ func (e *Entry) FirstFilterUpdate(pastureId int64, xToDay *XToday) (processIds [
 				"lact":          cowInfo.Lact,
 				"calving_age":   cowInfo.CalvingAge,
 				"ear_number":    cowInfo.EarNumber,
+				"pen_id":        cowInfo.PenId,
 				"week_high":     cowWeeklyActive,
 			}).Error; err != nil {
 			zaplog.Error("FirstFilterUpdate",
@@ -265,18 +266,6 @@ func (e *Entry) SecondUpdateChangeFilter(pastureId int64, processIds []int64, xT
 			chewFilter = 50
 		}
 
-		zaplog.Info("SecondUpdateChangeFilter",
-			zap.Any("NeckActiveHabit", v),
-			zap.Any("discount", discount),
-			zap.Any("xChangeDiscount", xChangeDiscount),
-			zap.Any("xRuminaDisc", xRuminaDisc),
-			zap.Any("chewFilterDiscount", chewFilterDiscount),
-			zap.Any("secondFilterData", secondFilterData),
-			zap.Any("changeFilter", changeFilter),
-			zap.Any("ruminaFilter", ruminaFilter),
-			zap.Any("chewFilter", chewFilter),
-		)
-
 		if err := e.DB.Model(new(model.NeckActiveHabit)).
 			Select("change_filter", "rumina_filter", "chew_filter").
 			Where("id = ?", v.Id).
@@ -291,7 +280,7 @@ func (e *Entry) SecondUpdateChangeFilter(pastureId int64, processIds []int64, xT
 }
 
 // FilterCorrectAndScoreUpdate 计算活动量变化趋势校正值(活跃度校正)和健康评分
-func (e *Entry) FilterCorrectAndScoreUpdate(pastureId int64, processIds []int64, xToday *XToday) {
+func (e *Entry) FilterCorrectAndScoreUpdate(appPasture *model.AppPastureList, processIds []int64, xToday *XToday) {
 	beginDayDate := time.Now().Local()
 	before7DayDate := beginDayDate.AddDate(0, 0, -7).Format(model.LayoutDate2)
 	before1DayDate := beginDayDate.AddDate(0, 0, -1).Format(model.LayoutDate2)
@@ -299,14 +288,14 @@ func (e *Entry) FilterCorrectAndScoreUpdate(pastureId int64, processIds []int64,
 	neckActiveHabitList := make([]*model.NeckActiveHabit, 0)
 	if err := e.DB.Model(new(model.NeckActiveHabit)).
 		Where("id IN (?)", processIds).
-		Where("pasture_id = ?", pastureId).
+		Where("pasture_id = ?", appPasture.Id).
 		Find(&neckActiveHabitList).Error; err != nil {
 		zaplog.Error("ActivityVolumeChanges-1", zap.Any("error", err), zap.Any("xToday", xToday))
 		return
 	}
 
 	for _, v := range neckActiveHabitList {
-		cowScore := calculateScore(v)
+		cowScore := CalculateScore(appPasture, v)
 		if err := e.DB.Model(new(model.NeckActiveHabit)).
 			Where("id = ?", v.Id).
 			Update("score", cowScore).Error; err != nil {
@@ -318,7 +307,7 @@ func (e *Entry) FilterCorrectAndScoreUpdate(pastureId int64, processIds []int64,
 			Select("neck_ring_number", "AVG(IF(change_filter>=60, 60, change_filter)) as avg_filter",
 				"ROUND(STD(IF(change_filter>=60, 60, change_filter))) as std_filter", "COUNT(1) as nb").
 			Where("heat_date BETWEEN ? AND ?", before7DayDate, before1DayDate).
-			Where("pasture_id = ?", pastureId).
+			Where("pasture_id = ?", appPasture.Id).
 			Where(e.DB.Where("high > ?", 12).Or("rumina >= ?", xToday.Rumina)).
 			Where("active_time <= ?", beginDayDate.Add(-12*time.Hour).Format(model.LayoutTime)).
 			Where("change_filter > ?", MinChangeFilter).
@@ -330,12 +319,10 @@ func (e *Entry) FilterCorrectAndScoreUpdate(pastureId int64, processIds []int64,
 		}
 
 		if activityVolume != nil && activityVolume.NeckRingNumber != "" {
-			//filterCorrect := model.DefaultFilterCorrect - int(math.Floor(activityVolume.AvgFilter/3+float64(activityVolume.StdFilter)/2))
 			filterCorrect := model.DefaultFilterCorrect - int(math.Round(activityVolume.AvgFilter/3+float64(int(math.Round(activityVolume.StdFilter))/2)))
 			// 活动量校正系数
 			if err := e.DB.Model(new(model.NeckActiveHabit)).
 				Where("id = ?", v.Id).
-				//Where("neck_ring_number = ?", v.NeckRingNumber).
 				Update("filter_correct", filterCorrect).Error; err != nil {
 				zaplog.Error("ActivityVolumeChanges-2", zap.Any("error", err), zap.Any("xToday", xToday))
 				continue
@@ -373,38 +360,99 @@ func (e *Entry) UpdateFilterCorrect(pastureId int64, processIds []int64) {
 }
 
 // UpdateChangeAdJust 更新群体校正数据
-func (e *Entry) UpdateChangeAdJust(pastureId int64, xToday *XToday) {
-	res := make([]*model.NeckRingBarChange, 0)
+func (e *Entry) UpdateChangeAdJust(pastureId int64, processIds []int64) {
+	neckRingPenChangeList := make([]*model.NeckRingPenChange, 0)
 	yesterday := time.Now().Local().AddDate(0, 0, -1).Format(model.LayoutDate2)
-	if err := e.DB.Table(fmt.Sprintf("%s as h", new(model.NeckActiveHabit).TableName())).
-		Select(`h.neck_ring_number,h.heat_date, h.frameid, c.pen_id, c.pen_name, COUNT(*) as nb,
-		ROUND(AVG(h.change_high)) as change_high, ROUND(AVG(h.change_filter)) as change_filter`).
-		Joins("JOIN cow as c ON h.cow_id = c.id").
-		Where("h.pasture_id = ?", pastureId).
-		Where("h.heat_date >= ?", yesterday).
-		Where("h.cow_id > ?", 0).
-		Where("c.pen_id > ?", 0).
-		Group("h.heat_date, h.frameid, c.pen_id").
-		Order("h.heat_date, h.frameid, c.pen_id").
-		Find(&res).Error; err != nil {
-		zaplog.Error("UpdateChangeAdJust", zap.Any("error", err), zap.Any("xToday", xToday))
-	}
-
-	for _, v := range res {
-		if math.Abs(float64(v.ChangeFilter)) < 10 {
-			continue
+	if err := e.DB.Model(new(model.NeckActiveHabit)).
+		Select(`heat_date,frameid,pen_id,COUNT(*) AS cow_count,ROUND(AVG(change_high)) AS change_high,ROUND(AVG(change_filter)) AS change_filter`).
+		Where("pasture_id = ?", pastureId).
+		Where("heat_date >= ?", yesterday).
+		Where("cow_id > ?", 0).
+		Where("pen_id > ?", 0).
+		Group("heat_date,frameid,pen_id").
+		Order("heat_date,frameid,pen_id").
+		Find(&neckRingPenChangeList).Error; err != nil {
+		zaplog.Error("UpdateChangeAdJust", zap.Any("error", err), zap.Any("pastureId", pastureId))
+	}
+
+	for _, v := range neckRingPenChangeList {
+		var count int64
+		if err := e.DB.Model(new(model.NeckRingPenChange)).
+			Where("pasture_id = ?", pastureId).
+			Where("heat_date = ?", v.HeatDate).
+			Where("frameid = ?", v.Frameid).
+			Where("pen_id = ?", v.PenId).
+			Count(&count).Error; err != nil {
+			zaplog.Error("UpdateChangeAdJust", zap.Any("error", err), zap.Any("v", v), zap.Any("pastureId", pastureId))
+		}
+		// 有就更新,没有就新增
+		if count > 0 {
+			if err := e.DB.Model(new(model.NeckRingPenChange)).
+				Where("pasture_id = ?", pastureId).
+				Where("heat_date = ?", v.HeatDate).
+				Where("frameid = ?", v.Frameid).
+				Where("pen_id = ?", v.PenId).
+				Updates(map[string]interface{}{
+					"cow_count":     v.CowCount,
+					"change_high":   v.ChangeHigh,
+					"change_filter": v.ChangeFilter,
+				}).Error; err != nil {
+				zaplog.Error("UpdateChangeAdJust", zap.Any("error", err), zap.Any("v", v), zap.Any("pastureId", pastureId))
+			}
+		} else {
+			neckRingPenChange := model.NewNeckRingPenChange(pastureId, v.HeatDate, v.CowCount, v.Frameid, v.PenId, v.ChangeHigh, v.ChangeFilter)
+			if err := e.DB.Model(new(model.NeckRingPenChange)).
+				Create(neckRingPenChange).Error; err != nil {
+				zaplog.Error("UpdateChangeAdJust",
+					zap.Any("error", err),
+					zap.Any("neckRingPenChange", neckRingPenChange),
+					zap.Any("pastureId", pastureId),
+				)
+			}
 		}
-		if err := e.DB.Model(new(model.NeckActiveHabit)).
+	}
+
+	neckActiveHabitList := make([]*model.NeckActiveHabit, 0)
+	if err := e.DB.Model(new(model.NeckActiveHabit)).
+		Where("id IN (?)", processIds).
+		Where("pasture_id = ?", pastureId).
+		Find(&neckActiveHabitList).Error; err != nil {
+		zaplog.Error("UpdateChangeAdJust", zap.Any("error", err))
+	}
+
+	if len(neckActiveHabitList) <= 0 {
+		return
+	}
+
+	for _, v := range neckActiveHabitList {
+		neckRingPenChange := &model.NeckRingPenChange{}
+		if err := e.DB.Model(new(model.NeckRingPenChange)).
 			Where("pasture_id = ?", pastureId).
-			Where("neck_ring_number = ?", v.NeckRingNumber).
 			Where("heat_date = ?", v.HeatDate).
-			Where("frameid = ?", v.FrameId).
-			Update("change_adjust", v.ChangeFilter).Error; err != nil {
-			zaplog.Error("UpdateChangeAdJust-1", zap.Any("error", err), zap.Any("xToday", xToday))
+			Where("frameid = ?", v.Frameid).
+			Where("pen_id = ?", v.PenId).
+			First(&neckRingPenChange).Error; err != nil {
+			zaplog.Error("UpdateChangeAdJust", zap.Any("error", err), zap.Any("v", v), zap.Any("pastureId", pastureId))
+			continue
+		}
+
+		if neckRingPenChange == nil || neckRingPenChange.Id <= 0 {
+			continue
+		}
+
+		if neckRingPenChange.ChangeFilter < 10 {
+			continue
+		}
+
+		if err := e.DB.Model(new(model.NeckActiveHabit)).
+			Where("id = ?", v.Id).
+			Update("change_adjust", neckRingPenChange.ChangeFilter).Error; err != nil {
+			zaplog.Error("UpdateChangeAdJust", zap.Any("error", err), zap.Any("v", v), zap.Any("neckRingPenChange", neckRingPenChange))
 		}
 	}
 }
 
+// UpdateCft 更新群体校正修正
 func (e *Entry) UpdateCft(pastureId int64, processIds []int64) {
 	neckActiveHabitList := make([]*model.NeckActiveHabit, 0)
 	if err := e.DB.Model(new(model.NeckActiveHabit)).

+ 51 - 55
module/crontab/neck_ring_estrus.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"kpt-pasture/model"
 	"kpt-pasture/util"
+	"sort"
 	"time"
 
 	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
@@ -70,8 +71,10 @@ func (e *Entry) EntryCowEstrus(pastureId int64) (err error) {
 func (e *Entry) CowEstrusWarning(pastureId int64, xToday *XToday, nowTime time.Time) {
 	cft := xToday.ActiveLow - XAdjust21
 	neckActiveHabitList := make([]*model.NeckActiveHabit, 0)
+	habitStartDate := nowTime.AddDate(0, 0, -1).Format(model.LayoutDate2)
+	habitEndDate := nowTime.Format(model.LayoutDate2)
 	if err := e.DB.Model(new(model.NeckActiveHabit)).
-		Where("heat_date BETWEEN ? AND ?", nowTime.AddDate(0, 0, -1).Format(model.LayoutDate2), nowTime.Format(model.LayoutDate2)).
+		Where("heat_date BETWEEN ? AND ?", habitStartDate, habitEndDate). // todo 查询的时间范围可以优化成当前时间的前12个小时的
 		Where("pasture_id = ?", pastureId).
 		Where("filter_high > 0 AND change_filter > ?", model.DefaultChangeFilter).
 		Where("cow_id > ?", 0).
@@ -85,7 +88,6 @@ func (e *Entry) CowEstrusWarning(pastureId int64, xToday *XToday, nowTime time.T
 
 	neckActiveHabitMap := make(map[int64][]*model.NeckActiveHabit)
 	for _, habit := range neckActiveHabitList {
-		zaplog.Info("CowEstrusWarning", zap.Any("habit", habit))
 		if neckActiveHabitMap[habit.CowId] == nil {
 			neckActiveHabitMap[habit.CowId] = make([]*model.NeckActiveHabit, 0)
 		}
@@ -95,10 +97,12 @@ func (e *Entry) CowEstrusWarning(pastureId int64, xToday *XToday, nowTime time.T
 	neckRingEstrusList := make([]*model.NeckRingEstrus, 0)
 	for cowId, cowHabitList := range neckActiveHabitMap {
 		// 最近3天最大发情记录,小于该变化趋势的不再插入
-		before3Data := e.GetBeforeThreeDaysCowEstrus(cowId, nowTime.AddDate(0, 0, -2).Format(model.LayoutTime))
-
+		before3StartDate := nowTime.AddDate(0, 0, -2).Format(model.LayoutTime)
+		before3Data := e.GetBeforeThreeDaysCowEstrus(pastureId, cowId, before3StartDate)
 		// 判断最近50天内是否存在发情记录(发情等级>=2),如果18~25天@xadjust21,如果36~50天@xadjust42
-		cow21Estrus := e.GetTwoEstrus(pastureId, cowId, nowTime.AddDate(0, 0, -100).Format(model.LayoutTime), nowTime.AddDate(0, 0, -2).Format(model.LayoutTime))
+		cow21EstrusStartDate := nowTime.AddDate(0, 0, -100).Format(model.LayoutTime)
+		cow21EstrusEndDate := nowTime.AddDate(0, 0, -2).Format(model.LayoutTime)
+		cow21Estrus := e.GetTwoEstrus(pastureId, cowId, cow21EstrusStartDate, cow21EstrusEndDate)
 		if cow21Estrus.ActiveDate != "" {
 			activeDateTime, _ := util.TimeParseLocal(model.LayoutTime, cow21Estrus.ActiveDate)
 			if activeDateTime.Unix() >= nowTime.AddDate(0, 0, -25).Unix() && activeDateTime.Unix() <= nowTime.AddDate(0, 0, -18).Unix() {
@@ -146,7 +150,6 @@ func (e *Entry) CowEstrusWarning(pastureId int64, xToday *XToday, nowTime time.T
 			dayHigh := int32(maxCft) + cow21Estrus.HadJust
 			lastEstrusDate := cow21Estrus.ActiveDate
 			checkResult := getResult(before3Data, maxCft, cow21Estrus)
-			isPeak := pasturePb.IsShow_Ok
 			activeTime := lastActiveDate.Format(model.LayoutTime)
 
 			if e.HistoryNeckRingEstrus(pastureId, cowInfo.NeckRingNumber, activeTime) {
@@ -158,7 +161,6 @@ func (e *Entry) CowEstrusWarning(pastureId int64, xToday *XToday, nowTime time.T
 				zap.Any("b48", b48),
 				zap.Any("checkResult", checkResult),
 				zap.Any("isShow", isShow),
-				zap.Any("isPeak", isPeak),
 				zap.Any("lastEstrusDate", lastEstrusDate),
 				zap.Any("activeDate", lastActiveDate),
 				zap.Any("dayHigh", dayHigh),
@@ -174,7 +176,7 @@ func (e *Entry) CowEstrusWarning(pastureId int64, xToday *XToday, nowTime time.T
 			newNeckRingEstrus.ActiveTime = activeTime
 			newNeckRingEstrus.DayHigh = dayHigh
 			newNeckRingEstrus.MaxHigh = maxHigh
-			newNeckRingEstrus.IsPeak = isPeak
+			newNeckRingEstrus.IsPeak = pasturePb.IsShow_No
 			neckRingEstrusList = append(neckRingEstrusList, newNeckRingEstrus)
 		}
 	}
@@ -191,17 +193,20 @@ func (e *Entry) CowEstrusWarning(pastureId int64, xToday *XToday, nowTime time.T
 // UpdateNewNeckRingEstrus 更新牛只首次发情时间和是否是高峰期
 func (e *Entry) UpdateNewNeckRingEstrus(pastureId int64, xToday *XToday, nowTime time.Time) {
 	e.UpdateEstrusFirstTime1(pastureId)
-	e.UpdateEstrusIsPeak(pastureId)
-	e.UpdateEstrusFirstTime2(pastureId, xToday, nowTime)
+	e.UpdateEstrusFirstTime2(pastureId, xToday)
 	e.UpdateEstrusFirstTime3(pastureId, nowTime)
+	e.UpdateEstrusIsPeak(pastureId)
 }
 
 // UpdateEstrusFirstTime1 更新牛只首次发情时间
 func (e *Entry) UpdateEstrusFirstTime1(pastureId int64) {
 	// 获取牛只首次发情时间为空的记录
 	neckRingEstrusList := e.FindNeckRingEstrusByFirstTimeEmpty(pastureId)
+	zaplog.Info("UpdateEstrusFirstTime1", zap.Any("neckRingEstrusList", neckRingEstrusList))
+
 	for _, v := range neckRingEstrusList {
-		cowEstrusStartData := e.FindCowEstrusFirstTime1(pastureId, v.CowId)
+		cowEstrusStartData := e.FindCowEstrusFirstTime1(pastureId, v)
+		zaplog.Info("UpdateEstrusFirstTime1", zap.Any("cowEstrusStartData", cowEstrusStartData))
 		if cowEstrusStartData != nil && cowEstrusStartData.FirstTime != "" {
 			if err := e.DB.Model(new(model.NeckRingEstrus)).
 				Where("id = ?", v.Id).
@@ -216,20 +221,22 @@ func (e *Entry) UpdateEstrusFirstTime1(pastureId int64) {
 	}
 }
 
-func (e *Entry) UpdateEstrusFirstTime2(pastureId int64, xToday *XToday, nowTime time.Time) {
+func (e *Entry) UpdateEstrusFirstTime2(pastureId int64, xToday *XToday) {
 	neckRingEstrusList := e.FindNeckRingEstrusByFirstTimeEmpty(pastureId)
 	for _, v := range neckRingEstrusList {
+		if v.FirstTime != "" {
+			continue
+		}
+
 		// 获取牛只最近12小时内的活动记录
 		activeTime, _ := util.TimeParseLocal(model.LayoutTime, v.ActiveTime)
 		startTime := activeTime.Add(-12 * time.Hour)
-
 		// 查询符合条件的活动记录
 		var firstTime string
 		if err := e.DB.Model(new(model.NeckActiveHabit)).
 			Select("MIN(active_time) as first_time").
 			Where("pasture_id = ?", pastureId).
 			Where("cow_id = ?", v.CowId).
-			Where("heat_date = ?", activeTime.Format(model.LayoutDate2)).
 			Where("active_time BETWEEN ? AND ?", startTime.Format(model.LayoutTime), v.ActiveTime).
 			Where("cft >= ?", xToday.ActiveLow).
 			Scan(&firstTime).Error; err != nil {
@@ -263,11 +270,12 @@ func (e *Entry) UpdateEstrusFirstTime3(pastureId int64, xToday time.Time) {
 
 func (e *Entry) UpdateEstrusIsPeak(pastureId int64) {
 	neckRingEstrusList := make([]*model.NeckRingEstrus, 0)
+	nowTime := time.Now().Local()
+	firstTime := fmt.Sprintf("%s 00:00:00", nowTime.AddDate(0, 0, -3).Format(model.LayoutDate2))
 	if err := e.DB.Model(new(model.NeckRingEstrus)).
-		Where("first_time >= ?", time.Now().Local().AddDate(0, 0, -3).Format(model.LayoutTime)).
+		Where("first_time >= ?", firstTime).
 		Where("active_time != ?", "").
 		Where("pasture_id = ?", pastureId).
-		Order("cow_id,first_time,active_time ASC").
 		Find(&neckRingEstrusList).Error; err != nil {
 		zaplog.Error("UpdateEstrusIsPeak", zap.Any("Find", err))
 	}
@@ -278,43 +286,40 @@ func (e *Entry) UpdateEstrusIsPeak(pastureId int64) {
 
 	neckRingEstrusFirstMap := make(map[string][]*model.NeckRingEstrus)
 	for _, v := range neckRingEstrusList {
-		if neckRingEstrusFirstMap[fmt.Sprintf("%s_%d", v.FirstTime, v.CowId)] == nil {
-			neckRingEstrusFirstMap[fmt.Sprintf("%s_%d", v.FirstTime, v.CowId)] = make([]*model.NeckRingEstrus, 0)
+		prefix := fmt.Sprintf("%s_%d", v.FirstTime, v.CowId)
+		if neckRingEstrusFirstMap[prefix] == nil {
+			neckRingEstrusFirstMap[prefix] = make([]*model.NeckRingEstrus, 0)
 		}
-		neckRingEstrusFirstMap[fmt.Sprintf("%s_%d", v.FirstTime, v.CowId)] = append(neckRingEstrusFirstMap[fmt.Sprintf("%s_%d", v.FirstTime, v.CowId)], v)
+		neckRingEstrusFirstMap[prefix] = append(neckRingEstrusFirstMap[prefix], v)
 	}
 
-	nowTime := time.Now().Local()
 	peakIsShow := make([]int64, 0)
-	peakIsNo := make([]int64, 0)
 	for _, estrusItems := range neckRingEstrusFirstMap {
 		eLen := len(estrusItems)
 		if eLen <= 0 {
 			continue
 		}
+		// 倒序排序
+		sort.Slice(estrusItems, func(i, j int) bool {
+			return estrusItems[i].ActiveTime < estrusItems[j].ActiveTime
+		})
 		// 判断是否是高峰期,和当前时间相比,如果超过2个小就是高峰期
 		lastItem := estrusItems[eLen-1]
-		lastActiveTime, err := time.Parse(model.LayoutTime, lastItem.ActiveTime)
-		if err != nil {
-			zaplog.Error("UpdateEstrusIsPeak", zap.Any("Parse", err), zap.Any("lastItem", lastItem))
-			continue
-		}
-		sub := nowTime.Sub(lastActiveTime.Local()).Hours()
-		if sub > 2 {
+		lastActiveTime := util.DateTimeParseLocalUnix2(lastItem.ActiveTime)
+		sub := nowTime.Sub(lastActiveTime).Hours()
+		if sub > 4 && lastItem.IsPeak == pasturePb.IsShow_No {
 			peakIsShow = append(peakIsShow, lastItem.Id)
-			for i := 0; i < eLen-1; i++ {
-				peakIsNo = append(peakIsNo, estrusItems[i].Id)
-			}
-		} else {
-			peakIsNo = append(peakIsNo, lastItem.Id)
 		}
-	}
 
-	zaplog.Info("UpdateEstrusIsPeak",
-		zap.Any("pastureId", pastureId),
-		zap.Any("peakIsShow", peakIsShow),
-		zap.Any("peakIsNo", peakIsNo),
-	)
+		zaplog.Info("UpdateEstrusIsPeak01",
+			zap.Any("pastureId", pastureId),
+			zap.Any("estrusItems", estrusItems),
+			zap.Any("peakIsShow", peakIsShow),
+			zap.Any("lastActiveTime", lastActiveTime),
+			zap.Any("sub", sub),
+			zap.Any("nowTime", nowTime.Format(model.LayoutTime)),
+		)
+	}
 
 	if len(peakIsShow) > 0 {
 		if err := e.DB.Model(new(model.NeckRingEstrus)).
@@ -324,29 +329,20 @@ func (e *Entry) UpdateEstrusIsPeak(pastureId int64) {
 			zaplog.Error("UpdateEstrusIsPeak", zap.Any("Update", err))
 		}
 	}
-
-	if len(peakIsNo) > 0 {
-		if err := e.DB.Model(new(model.NeckRingEstrus)).
-			Where("id IN ?", peakIsNo).
-			Where("pasture_id = ?", pastureId).
-			Update("is_peak", pasturePb.IsShow_No).Error; err != nil {
-			zaplog.Error("UpdateEstrusIsPeak", zap.Any("Update", err))
-		}
-	}
 }
 
 // FindCowEstrusFirstTime1 查找牛只昨天是否有发情数据
-func (e *Entry) FindCowEstrusFirstTime1(pastureId, cowId int64) *EstrusStartData {
+func (e *Entry) FindCowEstrusFirstTime1(pastureId int64, neckRingEstrus *model.NeckRingEstrus) *EstrusStartData {
 	firstTimeEventEstrus := &EstrusStartData{}
-	nowTime := time.Now().Local()
-	startDate := fmt.Sprintf("%s 00:00:00", nowTime.AddDate(0, 0, -1).Format(model.LayoutDate2))
-	endDate := fmt.Sprintf("%s 23:59:59", nowTime.Format(model.LayoutDate2))
+	activeAt := util.DateTimeParseLocalUnix2(neckRingEstrus.ActiveTime)
+	startDate := fmt.Sprintf("%s 00:00:00", activeAt.AddDate(0, 0, -1).Format(model.LayoutDate2))
 	if err := e.DB.Model(new(model.NeckRingEstrus)).
 		Select("cow_id,first_time").
-		Where("active_time BETWEEN ? AND ?", startDate, endDate).
+		Where("active_time BETWEEN ? AND ?", startDate, neckRingEstrus.ActiveTime).
 		Where("pasture_id = ?", pastureId).
-		Where("cow_id = ?", cowId).
-		Order("first_time ASC").
+		Where("cow_id = ?", neckRingEstrus.CowId).
+		Where("id != ?", neckRingEstrus.Id).
+		Order("first_time,id ASC").
 		First(&firstTimeEventEstrus).Error; err != nil {
 		return nil
 	}

+ 10 - 0
module/crontab/neck_ring_health.go

@@ -51,15 +51,25 @@ func (e *Entry) updateNeckRingHealth(pastureId int64, healthWarningList []*model
 		isMove := e.isEventCowLog(pastureId, v.CowId, startAt, endAt, pasturePb.EventType_Transfer_Ben)
 		if isMove {
 			v.IsTransferGroup = pasturePb.IsShow_Ok
+		} else {
+			v.IsTransferGroup = pasturePb.IsShow_No
 		}
+
 		isDryMilk := e.isEventCowLog(pastureId, v.CowId, startAt, endAt, pasturePb.EventType_Dry_Milk)
 		if isDryMilk {
 			v.IsDryMilk = pasturePb.IsShow_Ok
+		} else {
+			v.IsDryMilk = pasturePb.IsShow_No
 		}
 		isImmunization := e.isEventCowLog(pastureId, v.CowId, startAt, endAt, pasturePb.EventType_Immunication)
 		if isImmunization {
 			v.IsImmunization = pasturePb.IsShow_Ok
+		} else {
+			v.IsImmunization = pasturePb.IsShow_No
 		}
+		v.IsShow = pasturePb.IsShow_Ok
+		v.IsWorse = pasturePb.IsShow_Invalid
+		v.CheckResult = pasturePb.CheckResult_Pending
 	}
 	zaplog.Info("HealthWarning", zap.Any("healthWarningList", healthWarningList))
 	if err := e.DB.Model(new(model.NeckRingHealth)).

+ 65 - 9
module/crontab/neck_ring_merge.go

@@ -219,8 +219,8 @@ func computeIfPositiveElse(newValue, prevFilterValue float64, weightPrev, weight
 	return math.Ceil((prevFilterValue * weightPrev) + (weightNew * newValue))
 }
 
-// 计算 score 的逻辑
-func calculateScore(habit *model.NeckActiveHabit) int {
+// CalculateScore 计算 score 的逻辑
+func CalculateScore(appPasture *model.AppPastureList, habit *model.NeckActiveHabit) int {
 	// 第一部分逻辑
 	var part1 float64
 	switch {
@@ -231,13 +231,27 @@ func calculateScore(habit *model.NeckActiveHabit) int {
 	case habit.CalvingAge >= 2 && habit.CalvingAge <= 13:
 		part1 = math.Min((float64(habit.SumRumina+habit.SumIntake)-(100+math.Min(7, float64(habit.CalvingAge))*60))/10*2, 0)
 	case habit.ChangeFilter > -99:
-		part1 = math.Min(0, math.Min(getValueOrDefault(float64(habit.ChangeFilter), 0), getValueOrDefault(float64(habit.SumMinHigh), 0)))*0.2 +
-			math.Min(0, math.Min(getValueOrDefault(float64(habit.ChangeFilter), 0), getValueOrDefault(float64(habit.SumMinChew), 0)))*0.2 +
-			getRuminaSumIntakeSumScore(float64(habit.SumRumina+habit.SumIntake)) + getAdditionalScore(habit)
+		if appPasture.Category == pasturePb.PastureCategory_Beef {
+			part1 = getBeefSoreV1(habit) +
+				getBeefSportsRuminaScore(habit) +
+				getBeefRuminaSumIntakeSumScore(habit) +
+				getBeefAdditionalScore(habit)
+		} else {
+			part1 = getCowSoreV1(habit) +
+				getCowSportsRuminaScore(habit) +
+				getCowRuminaSumIntakeSumScore(habit) +
+				getCowAdditionalScore(habit)
+		}
 	default:
 		part1 = -299
 	}
 
+	fmt.Println("part1", part1)
+	fmt.Println("v1", getBeefSoreV1(habit))
+	fmt.Println("v2", getBeefSportsRuminaScore(habit))
+	fmt.Println("v3", getBeefRuminaSumIntakeSumScore(habit))
+	fmt.Println("v4", getBeefAdditionalScore(habit))
+
 	// 第二部分逻辑
 	var part2 float64
 	versionMod := habit.FirmwareVersion % 100
@@ -260,8 +274,28 @@ func getValueOrDefault(value, defaultValue float64) float64 {
 	return defaultValue
 }
 
-// 计算累计反刍得分
-func getRuminaSumIntakeSumScore(sum float64) float64 {
+func getCowSoreV1(habit *model.NeckActiveHabit) float64 {
+	return math.Min(0, math.Min(getValueOrDefault(float64(habit.ChangeFilter), 0), getValueOrDefault(float64(habit.SumMinHigh), 0))) * 0.2
+}
+
+func getBeefSoreV1(habit *model.NeckActiveHabit) float64 {
+	return math.Min(0, math.Min(getValueOrDefault(float64(habit.ChangeFilter)-math.Min(float64(habit.ChangeAdjust), 0), 0), getValueOrDefault(float64(habit.SumMinHigh), 0))) * 0.2
+}
+
+// 奶牛运动得反刍分
+// LEAST(0, IF(h.chew_filter>-99, h.chew_filter, 0), IF(h.sum_min_chew>-99, h.sum_min_chew, 0))*0.2
+func getCowSportsRuminaScore(habit *model.NeckActiveHabit) float64 {
+	return math.Min(0, math.Min(getValueOrDefault(float64(habit.ChangeFilter), 0), getValueOrDefault(float64(habit.SumMinChew), 0))) * 0.2
+}
+
+// 肉牛运动得反刍分
+func getBeefSportsRuminaScore(habit *model.NeckActiveHabit) float64 {
+	return math.Min(0, math.Min(getValueOrDefault(float64(habit.ChangeFilter), 0), getValueOrDefault(float64(habit.SumMinChew), 0))) * 0.2
+}
+
+// 奶牛累计反刍得分
+func getCowRuminaSumIntakeSumScore(habit *model.NeckActiveHabit) float64 {
+	sum := float64(habit.SumRumina + habit.SumIntake)
 	switch {
 	case sum < 80:
 		return -30
@@ -274,8 +308,14 @@ func getRuminaSumIntakeSumScore(sum float64) float64 {
 	}
 }
 
-// 计算额外得分
-func getAdditionalScore(habit *model.NeckActiveHabit) float64 {
+// 肉牛累计反刍得分
+func getBeefRuminaSumIntakeSumScore(habit *model.NeckActiveHabit) float64 {
+	sum := float64(habit.SumRumina + habit.SumIntake)
+	return math.Min(0, (sum-380)/10)
+}
+
+// 奶牛当前变化趋势是否是峰值
+func getCowAdditionalScore(habit *model.NeckActiveHabit) float64 {
 	var score float64
 	if (habit.SumRumina+habit.SumIntake < 280 || habit.SumMinHigh+habit.SumMinChew < -50) && habit.SumMaxHigh > 50 {
 		score += 10
@@ -285,3 +325,19 @@ func getAdditionalScore(habit *model.NeckActiveHabit) float64 {
 	}
 	return score
 }
+
+// 肉牛当前变化趋势是否是峰值
+// + IF(((h.sum_rumina + h.sum_intake )<280 OR (h.sum_min_high +h.sum_min_chew)<-50) AND h.sum_max_high>50, 10, 0)
+// + IF((h.change_filter -LEAST(h.change_adjust,0))<-30 AND (h.change_filter -LEAST(h.change_adjust,0))<=h.sum_min_high AND h.chew_filter<-30 AND h.chew_filter<=h.sum_min_chew, -5, 0)
+func getBeefAdditionalScore(habit *model.NeckActiveHabit) float64 {
+	var score float64
+	if (habit.SumRumina+habit.SumIntake < 280 || habit.SumMinHigh+habit.SumMinChew < -50) && habit.SumMaxHigh > 50 {
+		score += 10
+	}
+	if float64(habit.ChangeFilter)-math.Min(float64(habit.ChangeAdjust), 0) < -30 &&
+		float64(habit.ChangeFilter)-math.Min(float64(habit.ChangeAdjust), 0) <= float64(habit.SumMinHigh) &&
+		habit.ChewFilter < -30 && habit.ChewFilter <= habit.SumMinChew {
+		score -= 5
+	}
+	return score
+}

+ 71 - 0
module/crontab/neck_ring_merge_test.go

@@ -0,0 +1,71 @@
+package crontab
+
+import (
+	"fmt"
+	"kpt-pasture/model"
+	"testing"
+
+	pasturePb "gitee.com/xuyiping_admin/go_proto/proto/go/backend/cow"
+)
+
+func TestEntry_CalculateScore(t *testing.T) {
+	app := &model.AppPastureList{
+		Id:       4,
+		Category: pasturePb.PastureCategory_Beef,
+	}
+
+	habit := &model.NeckActiveHabit{
+		Id:                   5287253,
+		PastureId:            4,
+		NeckRingNumber:       "324",
+		CowId:                11246,
+		EarNumber:            "210868",
+		Lact:                 3,
+		CalvingAge:           312,
+		PenId:                16,
+		ActiveTime:           "2025-07-09",
+		Frameid:              7,
+		HeatDate:             "2025-07-09 11:00:00",
+		Rumina:               9,
+		Intake:               21,
+		Inactive:             74,
+		Gasp:                 0,
+		Other:                0,
+		High:                 642,
+		Active:               36,
+		FilterHigh:           974,
+		FilterRumina:         10,
+		FilterChew:           29,
+		WeekHigh:             1112,
+		HighHabit:            1755,
+		RuminaHabit:          23,
+		IntakeHabit:          38,
+		ChewHabit:            49,
+		InactiveHabit:        36,
+		OtherHabit:           0,
+		ChangeHigh:           -45,
+		ChangeRumina:         -57,
+		ChangeChew:           -41,
+		ChangeAdjust:         0,
+		ChangeFilter:         53,
+		RuminaFilter:         -57,
+		ChewFilter:           -39,
+		FilterCorrect:        95,
+		SumRumina:            129,
+		SumIntake:            106,
+		SumInactive:          456,
+		SumActive:            668,
+		SumMinHigh:           -55,
+		SumMaxHigh:           96,
+		SumMinChew:           -68,
+		BeforeThreeSumRumina: 508,
+		BeforeThreeSumIntake: 159,
+		Score:                0,
+		IsShow:               1,
+		Cft:                  60.35,
+		Voltage:              301,
+		RecordCount:          6,
+		FirmwareVersion:      57,
+	}
+	fmt.Println(CalculateScore(app, habit))
+}

+ 5 - 2
module/crontab/sql.go

@@ -101,11 +101,12 @@ func (e *Entry) GetPenMapList(pastureId int64) (map[int32]*model.Pen, error) {
 }
 
 // GetBeforeThreeDaysCowEstrus 获取值得时间之前三天内最大发情记录
-func (e *Entry) GetBeforeThreeDaysCowEstrus(cowId int64, activeTime string) *model.NeckRingEstrus {
+func (e *Entry) GetBeforeThreeDaysCowEstrus(pastureId, 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, MAX(IF(check_result=1,3,check_result)) AS check_result,active_time").
 		Where("cow_id = ?", cowId).
+		Where("pasture_id = ?", pastureId).
 		Where("active_time >= ?", activeTime).
 		First(neckRingEstrus).Error; err != nil {
 		return neckRingEstrus
@@ -247,7 +248,8 @@ func (e *Entry) GetMinIdByHeatDate(heatDate string, defaultId int64) (int64, err
 func (e *Entry) FindFilterData(pastureId int64, neckRingNumber, heatDate string, frameId int32) *FilterData {
 	firstFilterData := &FilterData{}
 	if err := e.DB.Model(new(model.NeckActiveHabit)).
-		Select("neck_ring_number", "filter_high", "filter_rumina", "filter_chew", "change_filter", "rumina_filter", "chew_filter").
+		Select("neck_ring_number", "filter_high", "filter_rumina", "filter_chew",
+			"change_filter", "rumina_filter", "chew_filter", "change_high").
 		Where("neck_ring_number = ?", neckRingNumber).
 		Where("heat_date = ?", heatDate).
 		Where("frameid = ?", frameId).
@@ -357,6 +359,7 @@ func (e *Entry) FindNeckRingEstrusByFirstTimeEmpty(pastureId int64) []*model.Nec
 		Where("pasture_id = ?", pastureId).
 		Find(&neckRingEstrusList).Error; err != nil {
 		zaplog.Error("FindNeckRingEstrusFirstTime", zap.Any("err", err))
+		return neckRingEstrusList
 	}
 	return neckRingEstrusList
 }

+ 131 - 0
service/httpclient/http.go

@@ -0,0 +1,131 @@
+package httpclient
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"time"
+
+	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
+	"gitee.com/xuyiping_admin/pkg/xerr"
+	"go.uber.org/zap"
+)
+
+type Service struct {
+	authClient *http.Client
+}
+
+func NewClientService() *Service {
+	return &Service{
+		authClient: &http.Client{
+			Timeout: time.Duration(60) * time.Second,
+		},
+	}
+}
+
+type Header struct {
+	Key   string `json:"key"`
+	Value string `json:"value"`
+}
+
+func (c *Service) doRequest(req *http.Request) ([]byte, error) {
+	resp, err := c.authClient.Do(req)
+	if err != nil {
+		var nErr net.Error
+		if errors.As(err, &nErr) && nErr.Timeout() {
+			for i := 0; i < 3; i++ {
+				time.Sleep(15 * time.Second)
+				resp, err = c.authClient.Do(req)
+				if err != nil {
+					zaplog.Error("ClientService", zap.Any("for", i), zap.Any("authClient.Do", err))
+					if i == 3 {
+						return nil, xerr.WithStack(err)
+					}
+				} else {
+					break
+				}
+			}
+		} else {
+			zaplog.Error("ClientService", zap.Any("authClient.Do", err))
+			return nil, xerr.WithStack(err)
+		}
+	}
+
+	defer resp.Body.Close()
+	b, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		zaplog.Error("ClientService", zap.Any("ioutil.ReadAll", err))
+		return nil, xerr.WithStack(err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		if len(b) > 0 {
+			return nil, xerr.Customf("err:%v,body:%s", err, string(b))
+		} else {
+			return nil, xerr.Customf("err:%v", err)
+		}
+	}
+	return b, nil
+}
+
+func (c *Service) DoGet(url string, headers []*Header) ([]byte, error) {
+	req, err := http.NewRequest(http.MethodGet, url, nil)
+	if err != nil {
+		zaplog.Error("ClientService", zap.Any("DoGet", err))
+		return nil, err
+	}
+	req.Header.Add("Accept", "application/json")
+	req.Header.Add("Content-Type", "application/json")
+	if headers != nil {
+		for _, v := range headers {
+			req.Header.Add(v.Key, v.Value)
+		}
+	}
+	return c.doRequest(req)
+}
+
+func (c *Service) DoPut(url string, body interface{}, headers []*Header) ([]byte, error) {
+	b, err := json.Marshal(body)
+	if err != nil {
+		zaplog.Error("ClientService", zap.Any("DoPost-Marshal", err))
+		return nil, xerr.WithStack(err)
+	}
+
+	req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(b))
+	if err != nil {
+		zaplog.Error("ClientService", zap.Any("DoGet", err))
+		return nil, err
+	}
+	req.Header.Add("Accept", "application/json")
+	req.Header.Add("Content-Type", "application/json")
+	if headers != nil {
+		for _, v := range headers {
+			req.Header.Add(v.Key, v.Value)
+		}
+	}
+	return c.doRequest(req)
+}
+
+func (c *Service) DoPost(url string, body interface{}, headers []*Header) ([]byte, error) {
+	b, err := json.Marshal(body)
+	if err != nil {
+		zaplog.Error("ClientService", zap.Any("DoPost-Marshal", err))
+		return nil, xerr.WithStack(err)
+	}
+
+	req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(b))
+	if err != nil {
+		zaplog.Error("ClientService", zap.Any("NewRequest", err))
+		return nil, xerr.WithStack(err)
+	}
+	req.Header.Add("Accept", "application/json")
+	req.Header.Add("Content-Type", "application/json")
+	if headers != nil {
+		for _, v := range headers {
+			req.Header.Add(v.Key, v.Value)
+		}
+	}
+	return c.doRequest(req)
+}

+ 20 - 0
service/httpclient/interface.go

@@ -0,0 +1,20 @@
+package httpclient
+
+import (
+	"kpt-pasture/config"
+	"net/http"
+
+	"gitee.com/xuyiping_admin/pkg/di"
+)
+
+type ClientService interface {
+	doRequest(req *http.Request) ([]byte, error)
+	DoGet(url string) ([]byte, error)
+	DoPost(url string, body interface{}) ([]byte, error)
+}
+
+var Module = di.Provide(NewService)
+
+func NewService(cfg *config.AppConfig) *Service {
+	return NewClientService()
+}

+ 4 - 3
service/wechat/http.go

@@ -3,12 +3,13 @@ package wechat
 import (
 	"bytes"
 	"encoding/json"
-	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
-	"gitee.com/xuyiping_admin/pkg/xerr"
 	"io/ioutil"
 	"net/http"
 	"time"
 
+	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
+	"gitee.com/xuyiping_admin/pkg/xerr"
+
 	"go.uber.org/zap"
 )
 
@@ -23,7 +24,7 @@ func NewClientService(appid, secret string) *ClientService {
 		AppID:  appid,
 		Secret: secret,
 		authClient: &http.Client{
-			Timeout: time.Duration(5) * time.Second,
+			Timeout: time.Duration(60) * time.Second,
 		},
 	}
 }

+ 7 - 0
util/util.go

@@ -159,6 +159,13 @@ func DateTimeParseLocalUnix(DayTime string) int64 {
 	return theTime.Unix()
 }
 
+// DateTimeParseLocalUnix2
+// eg 2025-10-13 15:04:05 => 1676998245
+func DateTimeParseLocalUnix2(DayTime string) time.Time {
+	theTime, _ := TimeParseLocal(LayoutTime, DayTime)
+	return theTime
+}
+
 // GetMonthRemainDay 获取当前月还剩几天
 func GetMonthRemainDay() int {
 	now := time.Now().Local()

+ 6 - 24
util/util_test.go

@@ -535,28 +535,10 @@ type WeeklyActiveModel struct {
 }
 
 func Test_demo(t *testing.T) {
-	/*str := "insert into system_pasture_menu (pasture_id,menu_id,created_at,updated_at) values"
-	for i := 36; i <= 135; i++ {
-		str += fmt.Sprintf("(11,%d,1748922530,1748922530),", i)
-	}
-
-	fmt.Println(strings.TrimRight(str, ","))
-
-	str1 := "insert into system_role_menu (role_id,menu_id,created_at,updated_at) values"
-	for i := 36; i <= 135; i++ {
-		str1 += fmt.Sprintf("(11,%d,1748922530,1748922530),", i)
-	}
-
-	fmt.Println(strings.TrimRight(str1, ","))
-
-	xToday := time.Now().Local()
-	activeTime, _ := TimeParseLocal(LayoutTime, "2025-06-18 03:00:00")
-	if activeTime.Before(xToday) || activeTime.After(xToday.AddDate(0, 0, 1)) {
-		fmt.Println("1")
-	} else {
-		fmt.Println("2")
-	}*/
-
-	num := 0.0334345466563453467868989087765434213344544345678
-	fmt.Println(RoundToTwoDecimals(num))
+	nowTime := time.Now().Local()
+	currentMonth := nowTime.Format(LayoutMonth)
+	startTime := time.Date(nowTime.Year(), nowTime.Month(), 1, 0, 0, 0, 0, nowTime.Location())
+	startMonth := startTime.AddDate(0, -5, 0).Format(LayoutMonth)
+	monthRang, _ := GetMonthsBetween(startMonth, currentMonth)
+	fmt.Println("currentMonth:", currentMonth, "startMonth:", startMonth, "monthRang", monthRang)
 }

+ 0 - 1
util/util_test1.go

@@ -1 +0,0 @@
-package util

部分文件因文件數量過多而無法顯示