Compare commits
	
		
			66 Commits
		
	
	
		
			f814e682cf
			...
			759caadb21
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 759caadb21 | |||
| 4250f27e11 | |||
| 77ab434d17 | |||
| 21661eb748 | |||
| 5e84b473f6 | |||
| e142405bb3 | |||
| 632bd20e7d | |||
| aac0324616 | |||
| 18b45b223c | |||
| 035da5293b | |||
| 1290676fe4 | |||
| 73de8ad04f | |||
| 9a7b765b71 | |||
| 4fb8729a2a | |||
| 84c22e342c | |||
| 691810c591 | |||
| 67b45d2e05 | |||
| 0576a790dd | |||
| 5e49cd3f95 | |||
| efbe7d167c | |||
| 51b776f393 | |||
| 189d532ac9 | |||
| 3b109d1547 | |||
| 648a790cec | |||
| 1b026d6106 | |||
| 91e18c432c | |||
| 59b6977367 | |||
| c49844feea | |||
| 448b721af5 | |||
| 759b31bce3 | |||
| c76c976cc8 | |||
| 1652df1533 | |||
| b1e1dcdcad | |||
| 47c72dff3e | |||
| 811c6a09c5 | |||
| cb20732205 | |||
| 2aa0f09079 | |||
| 9c35372720 | |||
| b6e68e861b | |||
| b3933b6d63 | |||
| 01327eb8d2 | |||
| 6d080d250d | |||
| 740e14e6cc | |||
| 8d9e4286b0 | |||
| 5403be5e7d | |||
| 1bc36f5e10 | |||
| 8bb0a54f18 | |||
| d03163a189 | |||
| 9875994df8 | |||
| c27b5bd708 | |||
| 4e17ddf638 | |||
| d273932693 | |||
| fadc1e2535 | |||
| c4fb237604 | |||
| 645c92978b | |||
| c50366f670 | |||
| 258e350c35 | |||
| aced495cd6 | |||
| 25e9e07cc8 | |||
| 6cc6d719e1 | |||
| 8cbe313c89 | |||
| 5754a1d94c | |||
| 609aee2513 | |||
| 829f0a6253 | |||
| 0b8b37511e | |||
| 3cc88a5248 | 
							
								
								
									
										52
									
								
								.golangci.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								.golangci.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					# .golangci.yml - 为你的项目量身定制的 linter 配置
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					linters-settings:
 | 
				
			||||||
 | 
					  # 这里可以对特定的 linter 进行微调
 | 
				
			||||||
 | 
					  errcheck:
 | 
				
			||||||
 | 
					    # 检查未处理的错误,但可以排除一些常见的、我们确认无需处理的函数
 | 
				
			||||||
 | 
					    exclude-functions:
 | 
				
			||||||
 | 
					      - io/ioutil.ReadFile
 | 
				
			||||||
 | 
					      - io.Copy
 | 
				
			||||||
 | 
					      - io.WriteString
 | 
				
			||||||
 | 
					      - os.Create
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					linters:
 | 
				
			||||||
 | 
					  # 明确我们想要禁用的 linter
 | 
				
			||||||
 | 
					  disable:
 | 
				
			||||||
 | 
					    # --- 暂时禁用的“干扰项” ---
 | 
				
			||||||
 | 
					    - godox # 禁用对 TODO, FIXME 注释的检查,让我们能专注于代码
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # --- 暂时禁用的“风格/复杂度”检查器 ---
 | 
				
			||||||
 | 
					    - gocyclo # 暂时不检查圈复杂度
 | 
				
			||||||
 | 
					    - funlen  # 暂时不检查函数长度
 | 
				
			||||||
 | 
					    - lll     # 暂时不检查行长度
 | 
				
			||||||
 | 
					    - wsl     # 检查多余的空格和换行,可以后期再处理
 | 
				
			||||||
 | 
					    - gocritic # 这个检查器包含很多子项,有些可能过于严格,可以先禁用,或在下面精细配置
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # 排除路径:分析这些文件但不报告问题(使用 regex 匹配)
 | 
				
			||||||
 | 
					  exclusions:
 | 
				
			||||||
 | 
					    paths:
 | 
				
			||||||
 | 
					      # 排除 docs/ 目录(匹配路径以 docs/ 开头)
 | 
				
			||||||
 | 
					      - '^docs/'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # 精细排除规则:用于特定文件/文本的 linter 排除
 | 
				
			||||||
 | 
					    rules:
 | 
				
			||||||
 | 
					      # 排除对 main.go 中 log.Fatalf 的抱怨(仅针对 goconst linter)
 | 
				
			||||||
 | 
					      - path: '^main\.go$'
 | 
				
			||||||
 | 
					        text: "log.Fatalf"
 | 
				
			||||||
 | 
					        linters:
 | 
				
			||||||
 | 
					          - goconst
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # 你也可以明确启用你认为最重要的检查器,形成一个“白名单”
 | 
				
			||||||
 | 
					  # enable:
 | 
				
			||||||
 | 
					  #   - govet
 | 
				
			||||||
 | 
					  #   - errcheck
 | 
				
			||||||
 | 
					  #   - staticcheck
 | 
				
			||||||
 | 
					  #   - unused
 | 
				
			||||||
 | 
					  #   - gosimple
 | 
				
			||||||
 | 
					  #   - ineffassign
 | 
				
			||||||
 | 
					  #   - typecheck
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					run:
 | 
				
			||||||
 | 
					  # 完全跳过测试文件分析(不解析、不报告任何问题)
 | 
				
			||||||
 | 
					  tests: false
 | 
				
			||||||
							
								
								
									
										8
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								Makefile
									
									
									
									
									
								
							@@ -13,6 +13,7 @@ help:
 | 
				
			|||||||
	@echo "	 swag         Generate swagger docs"
 | 
						@echo "	 swag         Generate swagger docs"
 | 
				
			||||||
	@echo "  help         Show this help message"
 | 
						@echo "  help         Show this help message"
 | 
				
			||||||
	@echo "  proto        Generate protobuf files"
 | 
						@echo "  proto        Generate protobuf files"
 | 
				
			||||||
 | 
						@echo "	 lint         Lint the code"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# 运行应用
 | 
					# 运行应用
 | 
				
			||||||
.PHONY: run
 | 
					.PHONY: run
 | 
				
			||||||
@@ -44,4 +45,9 @@ swag:
 | 
				
			|||||||
# 生成protobuf文件
 | 
					# 生成protobuf文件
 | 
				
			||||||
.PHONY: proto
 | 
					.PHONY: proto
 | 
				
			||||||
proto:
 | 
					proto:
 | 
				
			||||||
	protoc --go_out=internal/app/service/device/proto --go_opt=paths=source_relative --go-grpc_out=internal/app/service/device/proto --go-grpc_opt=paths=source_relative -Iinternal/app/service/device/proto internal/app/service/device/proto/device.proto
 | 
						protoc --go_out=internal/domain/device/proto --go_opt=paths=source_relative --go-grpc_out=internal/domain/device/proto --go-grpc_opt=paths=source_relative -Iinternal/domain/device/proto internal/domain/device/proto/device.proto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 运行代码检查
 | 
				
			||||||
 | 
					.PHONY: lint
 | 
				
			||||||
 | 
					lint:
 | 
				
			||||||
 | 
						golangci-lint run ./...
 | 
				
			||||||
							
								
								
									
										2791
									
								
								docs/docs.go
									
									
									
									
									
								
							
							
						
						
									
										2791
									
								
								docs/docs.go
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										2791
									
								
								docs/swagger.json
									
									
									
									
									
								
							
							
						
						
									
										2791
									
								
								docs/swagger.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										2029
									
								
								docs/swagger.yaml
									
									
									
									
									
								
							
							
						
						
									
										2029
									
								
								docs/swagger.yaml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -17,13 +17,15 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	_ "git.huangwc.com/pig/pig-farm-controller/docs" // 引入 swag 生成的 docs
 | 
						_ "git.huangwc.com/pig/pig-farm-controller/docs" // 引入 swag 生成的 docs
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/device"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/device"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/management"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/task"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/webhook"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/transport"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
@@ -45,7 +47,9 @@ type API struct {
 | 
				
			|||||||
	userController      *user.Controller               // 用户控制器实例
 | 
						userController      *user.Controller               // 用户控制器实例
 | 
				
			||||||
	deviceController    *device.Controller             // 设备控制器实例
 | 
						deviceController    *device.Controller             // 设备控制器实例
 | 
				
			||||||
	planController      *plan.Controller               // 计划控制器实例
 | 
						planController      *plan.Controller               // 计划控制器实例
 | 
				
			||||||
	listenHandler       transport.ListenHandler       // 设备上行事件监听器
 | 
						pigFarmController   *management.PigFarmController  // 猪场管理控制器实例
 | 
				
			||||||
 | 
						pigBatchController  *management.PigBatchController // 猪群控制器实例
 | 
				
			||||||
 | 
						listenHandler       webhook.ListenHandler          // 设备上行事件监听器
 | 
				
			||||||
	analysisTaskManager *task.AnalysisPlanTaskManager  // 计划触发器管理器实例
 | 
						analysisTaskManager *task.AnalysisPlanTaskManager  // 计划触发器管理器实例
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -58,10 +62,12 @@ func NewAPI(cfg config.ServerConfig,
 | 
				
			|||||||
	areaControllerRepository repository.AreaControllerRepository,
 | 
						areaControllerRepository repository.AreaControllerRepository,
 | 
				
			||||||
	deviceTemplateRepository repository.DeviceTemplateRepository, // 添加设备模板仓库
 | 
						deviceTemplateRepository repository.DeviceTemplateRepository, // 添加设备模板仓库
 | 
				
			||||||
	planRepository repository.PlanRepository,
 | 
						planRepository repository.PlanRepository,
 | 
				
			||||||
 | 
						pigFarmService service.PigFarmService,
 | 
				
			||||||
 | 
						pigBatchService service.PigBatchService, // 添加猪群服务
 | 
				
			||||||
	userActionLogRepository repository.UserActionLogRepository,
 | 
						userActionLogRepository repository.UserActionLogRepository,
 | 
				
			||||||
	tokenService token.TokenService,
 | 
						tokenService token.TokenService,
 | 
				
			||||||
	auditService audit.Service, // 注入审计服务
 | 
						auditService audit.Service, // 注入审计服务
 | 
				
			||||||
	listenHandler transport.ListenHandler,
 | 
						listenHandler webhook.ListenHandler,
 | 
				
			||||||
	analysisTaskManager *task.AnalysisPlanTaskManager) *API {
 | 
						analysisTaskManager *task.AnalysisPlanTaskManager) *API {
 | 
				
			||||||
	// 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式)
 | 
						// 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式)
 | 
				
			||||||
	// 从配置中获取 Gin 模式
 | 
						// 从配置中获取 Gin 模式
 | 
				
			||||||
@@ -90,6 +96,10 @@ func NewAPI(cfg config.ServerConfig,
 | 
				
			|||||||
		deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, logger),
 | 
							deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, logger),
 | 
				
			||||||
		// 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员
 | 
							// 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员
 | 
				
			||||||
		planController: plan.NewController(logger, planRepository, analysisTaskManager),
 | 
							planController: plan.NewController(logger, planRepository, analysisTaskManager),
 | 
				
			||||||
 | 
							// 在 NewAPI 中初始化猪场管理控制器
 | 
				
			||||||
 | 
							pigFarmController: management.NewPigFarmController(logger, pigFarmService),
 | 
				
			||||||
 | 
							// 在 NewAPI 中初始化猪群控制器
 | 
				
			||||||
 | 
							pigBatchController: management.NewPigBatchController(logger, pigBatchService),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	api.setupRoutes() // 设置所有路由
 | 
						api.setupRoutes() // 设置所有路由
 | 
				
			||||||
@@ -104,97 +114,143 @@ func (a *API) setupRoutes() {
 | 
				
			|||||||
	// 这些路由不需要身份验证
 | 
						// 这些路由不需要身份验证
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 用户注册和登录
 | 
						// 用户注册和登录
 | 
				
			||||||
	a.engine.POST("/api/v1/users", a.userController.CreateUser)
 | 
						a.engine.POST("/api/v1/users", a.userController.CreateUser)  // 注册新用户
 | 
				
			||||||
	a.engine.POST("/api/v1/users/login", a.userController.Login)
 | 
						a.engine.POST("/api/v1/users/login", a.userController.Login) // 用户登录
 | 
				
			||||||
	a.logger.Info("公开接口注册成功:用户注册、登录")
 | 
						a.logger.Info("公开接口注册成功:用户注册、登录")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 注册 pprof 路由
 | 
						// 注册 pprof 路由
 | 
				
			||||||
	pprofGroup := a.engine.Group("/debug/pprof")
 | 
						pprofGroup := a.engine.Group("/debug/pprof")
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		pprofGroup.GET("/", gin.WrapF(pprof.Index))
 | 
							pprofGroup.GET("/", gin.WrapF(pprof.Index))                   // pprof 索引页
 | 
				
			||||||
		pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline))
 | 
							pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline))          // pprof 命令行参数
 | 
				
			||||||
		pprofGroup.GET("/profile", gin.WrapF(pprof.Profile))
 | 
							pprofGroup.GET("/profile", gin.WrapF(pprof.Profile))          // pprof CPU profile
 | 
				
			||||||
		pprofGroup.POST("/symbol", gin.WrapF(pprof.Symbol))
 | 
							pprofGroup.POST("/symbol", gin.WrapF(pprof.Symbol))           // pprof 符号查找 (POST)
 | 
				
			||||||
		pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol))
 | 
							pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol))            // pprof 符号查找 (GET)
 | 
				
			||||||
		pprofGroup.GET("/trace", gin.WrapF(pprof.Trace))
 | 
							pprofGroup.GET("/trace", gin.WrapF(pprof.Trace))              // pprof 跟踪
 | 
				
			||||||
		pprofGroup.GET("/allocs", gin.WrapH(pprof.Handler("allocs")))
 | 
							pprofGroup.GET("/allocs", gin.WrapH(pprof.Handler("allocs"))) // pprof 内存分配
 | 
				
			||||||
		pprofGroup.GET("/block", gin.WrapH(pprof.Handler("block")))
 | 
							pprofGroup.GET("/block", gin.WrapH(pprof.Handler("block")))   // pprof 阻塞
 | 
				
			||||||
		pprofGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
 | 
							pprofGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
 | 
				
			||||||
		pprofGroup.GET("/heap", gin.WrapH(pprof.Handler("heap")))
 | 
							pprofGroup.GET("/heap", gin.WrapH(pprof.Handler("heap")))   // pprof 堆内存
 | 
				
			||||||
		pprofGroup.GET("/mutex", gin.WrapH(pprof.Handler("mutex")))
 | 
							pprofGroup.GET("/mutex", gin.WrapH(pprof.Handler("mutex"))) // pprof 互斥锁
 | 
				
			||||||
		pprofGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
 | 
							pprofGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	a.logger.Info("pprof 接口注册成功")
 | 
						a.logger.Info("pprof 接口注册成功")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 上行事件监听路由
 | 
						// 上行事件监听路由
 | 
				
			||||||
	a.engine.POST("/upstream", func(c *gin.Context) {
 | 
						a.engine.POST("/upstream", gin.WrapH(a.listenHandler.Handler())) // 处理设备上行事件
 | 
				
			||||||
		h := a.listenHandler.Handler()
 | 
					 | 
				
			||||||
		h.ServeHTTP(c.Writer, c.Request)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	a.logger.Info("上行事件监听接口注册成功")
 | 
						a.logger.Info("上行事件监听接口注册成功")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 添加 Swagger UI 路由, Swagger UI可在 /swagger/index.html 上找到
 | 
						// 添加 Swagger UI 路由, Swagger UI可在 /swagger/index.html 上找到
 | 
				
			||||||
	a.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
 | 
						a.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // Swagger UI 接口
 | 
				
			||||||
	a.logger.Info("Swagger UI 接口注册成功")
 | 
						a.logger.Info("Swagger UI 接口注册成功")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// --- Authenticated Routes ---
 | 
						// --- Authenticated Routes ---
 | 
				
			||||||
	// 所有在此注册的路由都需要通过 JWT 身份验证
 | 
						// 所有在此注册的路由都需要通过 JWT 身份验证
 | 
				
			||||||
	authGroup := a.engine.Group("/api/v1")
 | 
						authGroup := a.engine.Group("/api/v1")
 | 
				
			||||||
	authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证
 | 
						authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证中间件
 | 
				
			||||||
	authGroup.Use(middleware.AuditLogMiddleware(a.auditService))         // 2. 审计日志
 | 
						authGroup.Use(middleware.AuditLogMiddleware(a.auditService))         // 2. 审计日志中间件
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// 用户相关路由组
 | 
							// 用户相关路由组
 | 
				
			||||||
		userGroup := authGroup.Group("/users")
 | 
							userGroup := authGroup.Group("/users")
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			userGroup.GET("/:id/history", a.userController.ListUserHistory)
 | 
								userGroup.GET("/:id/history", a.userController.ListUserHistory) // 获取用户操作历史
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		a.logger.Info("用户相关接口注册成功 (需要认证和审计)")
 | 
							a.logger.Info("用户相关接口注册成功 (需要认证和审计)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 设备相关路由组
 | 
							// 设备相关路由组
 | 
				
			||||||
		deviceGroup := authGroup.Group("/devices")
 | 
							deviceGroup := authGroup.Group("/devices")
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			deviceGroup.POST("", a.deviceController.CreateDevice)
 | 
								deviceGroup.POST("", a.deviceController.CreateDevice)       // 创建设备
 | 
				
			||||||
			deviceGroup.GET("", a.deviceController.ListDevices)
 | 
								deviceGroup.GET("", a.deviceController.ListDevices)         // 获取设备列表
 | 
				
			||||||
			deviceGroup.GET("/:id", a.deviceController.GetDevice)
 | 
								deviceGroup.GET("/:id", a.deviceController.GetDevice)       // 获取单个设备
 | 
				
			||||||
			deviceGroup.PUT("/:id", a.deviceController.UpdateDevice)
 | 
								deviceGroup.PUT("/:id", a.deviceController.UpdateDevice)    // 更新设备
 | 
				
			||||||
			deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice)
 | 
								deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) // 删除设备
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		a.logger.Info("设备相关接口注册成功 (需要认证和审计)")
 | 
							a.logger.Info("设备相关接口注册成功 (需要认证和审计)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 区域主控相关路由组
 | 
							// 区域主控相关路由组
 | 
				
			||||||
		areaControllerGroup := authGroup.Group("/area-controllers")
 | 
							areaControllerGroup := authGroup.Group("/area-controllers")
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			areaControllerGroup.POST("", a.deviceController.CreateAreaController)
 | 
								areaControllerGroup.POST("", a.deviceController.CreateAreaController)       // 创建区域主控
 | 
				
			||||||
			areaControllerGroup.GET("", a.deviceController.ListAreaControllers)
 | 
								areaControllerGroup.GET("", a.deviceController.ListAreaControllers)         // 获取区域主控列表
 | 
				
			||||||
			areaControllerGroup.GET("/:id", a.deviceController.GetAreaController)
 | 
								areaControllerGroup.GET("/:id", a.deviceController.GetAreaController)       // 获取单个区域主控
 | 
				
			||||||
			areaControllerGroup.PUT("/:id", a.deviceController.UpdateAreaController)
 | 
								areaControllerGroup.PUT("/:id", a.deviceController.UpdateAreaController)    // 更新区域主控
 | 
				
			||||||
			areaControllerGroup.DELETE("/:id", a.deviceController.DeleteAreaController)
 | 
								areaControllerGroup.DELETE("/:id", a.deviceController.DeleteAreaController) // 删除区域主控
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		a.logger.Info("区域主控相关接口注册成功 (需要认证和审计)")
 | 
							a.logger.Info("区域主控相关接口注册成功 (需要认证和审计)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 设备模板相关路由组
 | 
							// 设备模板相关路由组
 | 
				
			||||||
		deviceTemplateGroup := authGroup.Group("/device-templates")
 | 
							deviceTemplateGroup := authGroup.Group("/device-templates")
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			deviceTemplateGroup.POST("", a.deviceController.CreateDeviceTemplate)
 | 
								deviceTemplateGroup.POST("", a.deviceController.CreateDeviceTemplate)       // 创建设备模板
 | 
				
			||||||
			deviceTemplateGroup.GET("", a.deviceController.ListDeviceTemplates)
 | 
								deviceTemplateGroup.GET("", a.deviceController.ListDeviceTemplates)         // 获取设备模板列表
 | 
				
			||||||
			deviceTemplateGroup.GET("/:id", a.deviceController.GetDeviceTemplate)
 | 
								deviceTemplateGroup.GET("/:id", a.deviceController.GetDeviceTemplate)       // 获取单个设备模板
 | 
				
			||||||
			deviceTemplateGroup.PUT("/:id", a.deviceController.UpdateDeviceTemplate)
 | 
								deviceTemplateGroup.PUT("/:id", a.deviceController.UpdateDeviceTemplate)    // 更新设备模板
 | 
				
			||||||
			deviceTemplateGroup.DELETE("/:id", a.deviceController.DeleteDeviceTemplate)
 | 
								deviceTemplateGroup.DELETE("/:id", a.deviceController.DeleteDeviceTemplate) // 删除设备模板
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		a.logger.Info("设备模板相关接口注册成功 (需要认证和审计)")
 | 
							a.logger.Info("设备模板相关接口注册成功 (需要认证和审计)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 计划相关路由组
 | 
							// 计划相关路由组
 | 
				
			||||||
		planGroup := authGroup.Group("/plans")
 | 
							planGroup := authGroup.Group("/plans")
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			planGroup.POST("", a.planController.CreatePlan)
 | 
								planGroup.POST("", a.planController.CreatePlan)          // 创建计划
 | 
				
			||||||
			planGroup.GET("", a.planController.ListPlans)
 | 
								planGroup.GET("", a.planController.ListPlans)            // 获取计划列表
 | 
				
			||||||
			planGroup.GET("/:id", a.planController.GetPlan)
 | 
								planGroup.GET("/:id", a.planController.GetPlan)          // 获取单个计划
 | 
				
			||||||
			planGroup.PUT("/:id", a.planController.UpdatePlan)
 | 
								planGroup.PUT("/:id", a.planController.UpdatePlan)       // 更新计划
 | 
				
			||||||
			planGroup.DELETE("/:id", a.planController.DeletePlan)
 | 
								planGroup.DELETE("/:id", a.planController.DeletePlan)    // 删除计划
 | 
				
			||||||
			planGroup.POST("/:id/start", a.planController.StartPlan)
 | 
								planGroup.POST("/:id/start", a.planController.StartPlan) // 启动计划
 | 
				
			||||||
			planGroup.POST("/:id/stop", a.planController.StopPlan)
 | 
								planGroup.POST("/:id/stop", a.planController.StopPlan)   // 停止计划
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		a.logger.Info("计划相关接口注册成功 (需要认证和审计)")
 | 
							a.logger.Info("计划相关接口注册成功 (需要认证和审计)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 猪舍相关路由组
 | 
				
			||||||
 | 
							pigHouseGroup := authGroup.Group("/pig-houses")
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								pigHouseGroup.POST("", a.pigFarmController.CreatePigHouse)       // 创建猪舍
 | 
				
			||||||
 | 
								pigHouseGroup.GET("", a.pigFarmController.ListPigHouses)         // 获取猪舍列表
 | 
				
			||||||
 | 
								pigHouseGroup.GET("/:id", a.pigFarmController.GetPigHouse)       // 获取单个猪舍
 | 
				
			||||||
 | 
								pigHouseGroup.PUT("/:id", a.pigFarmController.UpdatePigHouse)    // 更新猪舍
 | 
				
			||||||
 | 
								pigHouseGroup.DELETE("/:id", a.pigFarmController.DeletePigHouse) // 删除猪舍
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							a.logger.Info("猪舍相关接口注册成功 (需要认证和审计)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 猪圈相关路由组
 | 
				
			||||||
 | 
							penGroup := authGroup.Group("/pens")
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								penGroup.POST("", a.pigFarmController.CreatePen)                 // 创建猪圈
 | 
				
			||||||
 | 
								penGroup.GET("", a.pigFarmController.ListPens)                   // 获取猪圈列表
 | 
				
			||||||
 | 
								penGroup.GET("/:id", a.pigFarmController.GetPen)                 // 获取单个猪圈
 | 
				
			||||||
 | 
								penGroup.PUT("/:id", a.pigFarmController.UpdatePen)              // 更新猪圈
 | 
				
			||||||
 | 
								penGroup.DELETE("/:id", a.pigFarmController.DeletePen)           // 删除猪圈
 | 
				
			||||||
 | 
								penGroup.PUT("/:id/status", a.pigFarmController.UpdatePenStatus) // 更新猪圈状态
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							a.logger.Info("猪圈相关接口注册成功 (需要认证和审计)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 猪群相关路由组
 | 
				
			||||||
 | 
							pigBatchGroup := authGroup.Group("/pig-batches")
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								pigBatchGroup.POST("", a.pigBatchController.CreatePigBatch)                                                   // 创建猪群
 | 
				
			||||||
 | 
								pigBatchGroup.GET("", a.pigBatchController.ListPigBatches)                                                    // 获取猪群列表
 | 
				
			||||||
 | 
								pigBatchGroup.GET("/:id", a.pigBatchController.GetPigBatch)                                                   // 获取单个猪群
 | 
				
			||||||
 | 
								pigBatchGroup.PUT("/:id", a.pigBatchController.UpdatePigBatch)                                                // 更新猪群
 | 
				
			||||||
 | 
								pigBatchGroup.DELETE("/:id", a.pigBatchController.DeletePigBatch)                                             // 删除猪群
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/assign-pens", a.pigBatchController.AssignEmptyPensToBatch)                           // 为猪群分配空栏
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:fromBatchID/reclassify-pen", a.pigBatchController.ReclassifyPenToNewBatch)              // 将猪栏划拨到新群
 | 
				
			||||||
 | 
								pigBatchGroup.DELETE("/:batchID/remove-pen/:penID", a.pigBatchController.RemoveEmptyPenFromBatch)             // 从猪群移除空栏
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/move-pigs-into-pen", a.pigBatchController.MovePigsIntoPen)                           // 将猪只从“虚拟库存”移入指定猪栏
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/sell-pigs", a.pigBatchController.SellPigs)                                           // 处理卖猪业务
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/buy-pigs", a.pigBatchController.BuyPigs)                                             // 处理买猪业务
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:sourceBatchID/transfer-across-batches", a.pigBatchController.TransferPigsAcrossBatches) // 跨猪群调栏
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/transfer-within-batch", a.pigBatchController.TransferPigsWithinBatch)                // 群内调栏
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/record-sick-pigs", a.pigBatchController.RecordSickPigs)                              // 记录新增病猪事件
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/record-sick-pig-recovery", a.pigBatchController.RecordSickPigRecovery)               // 记录病猪康复事件
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/record-sick-pig-death", a.pigBatchController.RecordSickPigDeath)                     // 记录病猪死亡事件
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/record-sick-pig-cull", a.pigBatchController.RecordSickPigCull)                       // 记录病猪淘汰事件
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/record-death", a.pigBatchController.RecordDeath)                                     // 记录正常猪只死亡事件
 | 
				
			||||||
 | 
								pigBatchGroup.POST("/:id/record-cull", a.pigBatchController.RecordCull)                                       // 记录正常猪只淘汰事件
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							a.logger.Info("猪群相关接口注册成功 (需要认证和审计)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										47
									
								
								internal/app/controller/auth_utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								internal/app/controller/auth_utils.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					package controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						// ErrUserNotFoundInContext 表示在 gin.Context 中未找到用户信息。
 | 
				
			||||||
 | 
						ErrUserNotFoundInContext = errors.New("context中未找到用户信息")
 | 
				
			||||||
 | 
						// ErrInvalidUserType 表示从 gin.Context 中获取的用户信息类型不正确。
 | 
				
			||||||
 | 
						ErrInvalidUserType = errors.New("context中用户信息类型不正确")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetOperatorIDFromContext 从 gin.Context 中提取操作者ID。
 | 
				
			||||||
 | 
					// 假设操作者ID是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 ID 字段。
 | 
				
			||||||
 | 
					func GetOperatorIDFromContext(c *gin.Context) (uint, error) {
 | 
				
			||||||
 | 
						userVal, exists := c.Get(models.ContextUserKey.String())
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							return 0, ErrUserNotFoundInContext
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user, ok := userVal.(*models.User)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return 0, ErrInvalidUserType
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return user.ID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetOperatorFromContext 从 gin.Context 中提取操作者。
 | 
				
			||||||
 | 
					// 假设操作者是由 AuthMiddleware 存储到 context 中的 *models.User 对象的  字段。
 | 
				
			||||||
 | 
					func GetOperatorFromContext(c *gin.Context) (*models.User, error) {
 | 
				
			||||||
 | 
						userVal, exists := c.Get(models.ContextUserKey.String())
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							return nil, ErrUserNotFoundInContext
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user, ok := userVal.(*models.User)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, ErrInvalidUserType
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return user, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,12 +3,11 @@ package device
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
@@ -34,243 +33,11 @@ func NewController(
 | 
				
			|||||||
	return &Controller{
 | 
						return &Controller{
 | 
				
			||||||
		deviceRepo:         deviceRepo,
 | 
							deviceRepo:         deviceRepo,
 | 
				
			||||||
		areaControllerRepo: areaControllerRepo,
 | 
							areaControllerRepo: areaControllerRepo,
 | 
				
			||||||
		deviceTemplateRepo: deviceTemplateRepo, // 初始化设备模板仓库
 | 
							deviceTemplateRepo: deviceTemplateRepo,
 | 
				
			||||||
		logger:             logger,
 | 
							logger:             logger,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Request DTOs ---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// CreateDeviceRequest 定义了创建设备时需要传入的参数
 | 
					 | 
				
			||||||
type CreateDeviceRequest struct {
 | 
					 | 
				
			||||||
	Name             string                 `json:"name" binding:"required"`
 | 
					 | 
				
			||||||
	DeviceTemplateID uint                   `json:"device_template_id" binding:"required"`
 | 
					 | 
				
			||||||
	AreaControllerID uint                   `json:"area_controller_id" binding:"required"`
 | 
					 | 
				
			||||||
	Location         string                 `json:"location,omitempty"`
 | 
					 | 
				
			||||||
	Properties       map[string]interface{} `json:"properties,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// UpdateDeviceRequest 定义了更新设备时需要传入的参数
 | 
					 | 
				
			||||||
type UpdateDeviceRequest struct {
 | 
					 | 
				
			||||||
	Name             string                 `json:"name" binding:"required"`
 | 
					 | 
				
			||||||
	DeviceTemplateID uint                   `json:"device_template_id" binding:"required"`
 | 
					 | 
				
			||||||
	AreaControllerID uint                   `json:"area_controller_id" binding:"required"`
 | 
					 | 
				
			||||||
	Location         string                 `json:"location,omitempty"`
 | 
					 | 
				
			||||||
	Properties       map[string]interface{} `json:"properties,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// CreateAreaControllerRequest 定义了创建区域主控时需要传入的参数
 | 
					 | 
				
			||||||
type CreateAreaControllerRequest struct {
 | 
					 | 
				
			||||||
	Name       string                 `json:"name" binding:"required"`
 | 
					 | 
				
			||||||
	NetworkID  string                 `json:"network_id" binding:"required"`
 | 
					 | 
				
			||||||
	Location   string                 `json:"location,omitempty"`
 | 
					 | 
				
			||||||
	Properties map[string]interface{} `json:"properties,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// UpdateAreaControllerRequest 定义了更新区域主控时需要传入的参数
 | 
					 | 
				
			||||||
type UpdateAreaControllerRequest struct {
 | 
					 | 
				
			||||||
	Name       string                 `json:"name" binding:"required"`
 | 
					 | 
				
			||||||
	NetworkID  string                 `json:"network_id" binding:"required"`
 | 
					 | 
				
			||||||
	Location   string                 `json:"location,omitempty"`
 | 
					 | 
				
			||||||
	Properties map[string]interface{} `json:"properties,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// CreateDeviceTemplateRequest 定义了创建设备模板时需要传入的参数
 | 
					 | 
				
			||||||
type CreateDeviceTemplateRequest struct {
 | 
					 | 
				
			||||||
	Name         string                   `json:"name" binding:"required"`
 | 
					 | 
				
			||||||
	Manufacturer string                   `json:"manufacturer,omitempty"`
 | 
					 | 
				
			||||||
	Description  string                   `json:"description,omitempty"`
 | 
					 | 
				
			||||||
	Category     models.DeviceCategory    `json:"category" binding:"required"`
 | 
					 | 
				
			||||||
	Commands     map[string]interface{}   `json:"commands" binding:"required"`
 | 
					 | 
				
			||||||
	Values       []models.ValueDescriptor `json:"values,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// UpdateDeviceTemplateRequest 定义了更新设备模板时需要传入的参数
 | 
					 | 
				
			||||||
type UpdateDeviceTemplateRequest struct {
 | 
					 | 
				
			||||||
	Name         string                   `json:"name" binding:"required"`
 | 
					 | 
				
			||||||
	Manufacturer string                   `json:"manufacturer,omitempty"`
 | 
					 | 
				
			||||||
	Description  string                   `json:"description,omitempty"`
 | 
					 | 
				
			||||||
	Category     models.DeviceCategory    `json:"category" binding:"required"`
 | 
					 | 
				
			||||||
	Commands     map[string]interface{}   `json:"commands" binding:"required"`
 | 
					 | 
				
			||||||
	Values       []models.ValueDescriptor `json:"values,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Response DTOs ---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// DeviceResponse 定义了返回给客户端的单个设备信息的结构
 | 
					 | 
				
			||||||
type DeviceResponse struct {
 | 
					 | 
				
			||||||
	ID                 uint                   `json:"id"`
 | 
					 | 
				
			||||||
	Name               string                 `json:"name"`
 | 
					 | 
				
			||||||
	DeviceTemplateID   uint                   `json:"device_template_id"`
 | 
					 | 
				
			||||||
	DeviceTemplateName string                 `json:"device_template_name"`
 | 
					 | 
				
			||||||
	AreaControllerID   uint                   `json:"area_controller_id"`
 | 
					 | 
				
			||||||
	AreaControllerName string                 `json:"area_controller_name"`
 | 
					 | 
				
			||||||
	Location           string                 `json:"location"`
 | 
					 | 
				
			||||||
	Properties         map[string]interface{} `json:"properties"`
 | 
					 | 
				
			||||||
	CreatedAt          string                 `json:"created_at"`
 | 
					 | 
				
			||||||
	UpdatedAt          string                 `json:"updated_at"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// AreaControllerResponse 定义了返回给客户端的单个区域主控信息的结构
 | 
					 | 
				
			||||||
type AreaControllerResponse struct {
 | 
					 | 
				
			||||||
	ID         uint                   `json:"id"`
 | 
					 | 
				
			||||||
	Name       string                 `json:"name"`
 | 
					 | 
				
			||||||
	NetworkID  string                 `json:"network_id"`
 | 
					 | 
				
			||||||
	Location   string                 `json:"location"`
 | 
					 | 
				
			||||||
	Status     string                 `json:"status"`
 | 
					 | 
				
			||||||
	Properties map[string]interface{} `json:"properties"`
 | 
					 | 
				
			||||||
	CreatedAt  string                 `json:"created_at"`
 | 
					 | 
				
			||||||
	UpdatedAt  string                 `json:"updated_at"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// DeviceTemplateResponse 定义了返回给客户端的单个设备模板信息的结构
 | 
					 | 
				
			||||||
type DeviceTemplateResponse struct {
 | 
					 | 
				
			||||||
	ID           uint                     `json:"id"`
 | 
					 | 
				
			||||||
	Name         string                   `json:"name"`
 | 
					 | 
				
			||||||
	Manufacturer string                   `json:"manufacturer"`
 | 
					 | 
				
			||||||
	Description  string                   `json:"description"`
 | 
					 | 
				
			||||||
	Category     models.DeviceCategory    `json:"category"`
 | 
					 | 
				
			||||||
	Commands     map[string]interface{}   `json:"commands"`
 | 
					 | 
				
			||||||
	Values       []models.ValueDescriptor `json:"values"`
 | 
					 | 
				
			||||||
	CreatedAt    string                   `json:"created_at"`
 | 
					 | 
				
			||||||
	UpdatedAt    string                   `json:"updated_at"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- DTO 转换函数 ---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// newDeviceResponse 从数据库模型创建一个新的设备响应 DTO
 | 
					 | 
				
			||||||
func newDeviceResponse(device *models.Device) (*DeviceResponse, error) {
 | 
					 | 
				
			||||||
	if device == nil {
 | 
					 | 
				
			||||||
		return nil, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var props map[string]interface{}
 | 
					 | 
				
			||||||
	if len(device.Properties) > 0 && string(device.Properties) != "null" {
 | 
					 | 
				
			||||||
		if err := device.ParseProperties(&props); err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("解析设备属性失败 (ID: %d): %w", device.ID, err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 确保 DeviceTemplate 和 AreaController 已预加载
 | 
					 | 
				
			||||||
	deviceTemplateName := ""
 | 
					 | 
				
			||||||
	if device.DeviceTemplate.ID != 0 {
 | 
					 | 
				
			||||||
		deviceTemplateName = device.DeviceTemplate.Name
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	areaControllerName := ""
 | 
					 | 
				
			||||||
	if device.AreaController.ID != 0 {
 | 
					 | 
				
			||||||
		areaControllerName = device.AreaController.Name
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &DeviceResponse{
 | 
					 | 
				
			||||||
		ID:                 device.ID,
 | 
					 | 
				
			||||||
		Name:               device.Name,
 | 
					 | 
				
			||||||
		DeviceTemplateID:   device.DeviceTemplateID,
 | 
					 | 
				
			||||||
		DeviceTemplateName: deviceTemplateName,
 | 
					 | 
				
			||||||
		AreaControllerID:   device.AreaControllerID,
 | 
					 | 
				
			||||||
		AreaControllerName: areaControllerName,
 | 
					 | 
				
			||||||
		Location:           device.Location,
 | 
					 | 
				
			||||||
		Properties:         props,
 | 
					 | 
				
			||||||
		CreatedAt:          device.CreatedAt.Format(time.RFC3339),
 | 
					 | 
				
			||||||
		UpdatedAt:          device.UpdatedAt.Format(time.RFC3339),
 | 
					 | 
				
			||||||
	}, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// newListDeviceResponse 从数据库模型切片创建一个新的设备列表响应 DTO 切片
 | 
					 | 
				
			||||||
func newListDeviceResponse(devices []*models.Device) ([]*DeviceResponse, error) {
 | 
					 | 
				
			||||||
	list := make([]*DeviceResponse, 0, len(devices))
 | 
					 | 
				
			||||||
	for _, device := range devices {
 | 
					 | 
				
			||||||
		resp, err := newDeviceResponse(device)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		list = append(list, resp)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return list, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// newAreaControllerResponse 从数据库模型创建一个新的区域主控响应 DTO
 | 
					 | 
				
			||||||
func newAreaControllerResponse(ac *models.AreaController) (*AreaControllerResponse, error) {
 | 
					 | 
				
			||||||
	if ac == nil {
 | 
					 | 
				
			||||||
		return nil, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var props map[string]interface{}
 | 
					 | 
				
			||||||
	if len(ac.Properties) > 0 && string(ac.Properties) != "null" {
 | 
					 | 
				
			||||||
		if err := json.Unmarshal(ac.Properties, &props); err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("解析区域主控属性失败 (ID: %d): %w", ac.ID, err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &AreaControllerResponse{
 | 
					 | 
				
			||||||
		ID:         ac.ID,
 | 
					 | 
				
			||||||
		Name:       ac.Name,
 | 
					 | 
				
			||||||
		NetworkID:  ac.NetworkID,
 | 
					 | 
				
			||||||
		Location:   ac.Location,
 | 
					 | 
				
			||||||
		Status:     ac.Status,
 | 
					 | 
				
			||||||
		Properties: props,
 | 
					 | 
				
			||||||
		CreatedAt:  ac.CreatedAt.Format(time.RFC3339),
 | 
					 | 
				
			||||||
		UpdatedAt:  ac.UpdatedAt.Format(time.RFC3339),
 | 
					 | 
				
			||||||
	}, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// newListAreaControllerResponse 从数据库模型切片创建一个新的区域主控列表响应 DTO 切片
 | 
					 | 
				
			||||||
func newListAreaControllerResponse(acs []*models.AreaController) ([]*AreaControllerResponse, error) {
 | 
					 | 
				
			||||||
	list := make([]*AreaControllerResponse, 0, len(acs))
 | 
					 | 
				
			||||||
	for _, ac := range acs {
 | 
					 | 
				
			||||||
		resp, err := newAreaControllerResponse(ac)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		list = append(list, resp)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return list, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// newDeviceTemplateResponse 从数据库模型创建一个新的设备模板响应 DTO
 | 
					 | 
				
			||||||
func newDeviceTemplateResponse(dt *models.DeviceTemplate) (*DeviceTemplateResponse, error) {
 | 
					 | 
				
			||||||
	if dt == nil {
 | 
					 | 
				
			||||||
		return nil, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var commands map[string]interface{}
 | 
					 | 
				
			||||||
	if err := dt.ParseCommands(&commands); err != nil {
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("解析设备模板命令失败 (ID: %d): %w", dt.ID, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var values []models.ValueDescriptor
 | 
					 | 
				
			||||||
	if dt.Category == models.CategorySensor {
 | 
					 | 
				
			||||||
		if err := dt.ParseValues(&values); err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("解析设备模板值描述符失败 (ID: %d): %w", dt.ID, err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &DeviceTemplateResponse{
 | 
					 | 
				
			||||||
		ID:           dt.ID,
 | 
					 | 
				
			||||||
		Name:         dt.Name,
 | 
					 | 
				
			||||||
		Manufacturer: dt.Manufacturer,
 | 
					 | 
				
			||||||
		Description:  dt.Description,
 | 
					 | 
				
			||||||
		Category:     dt.Category,
 | 
					 | 
				
			||||||
		Commands:     commands,
 | 
					 | 
				
			||||||
		Values:       values,
 | 
					 | 
				
			||||||
		CreatedAt:    dt.CreatedAt.Format(time.RFC3339),
 | 
					 | 
				
			||||||
		UpdatedAt:    dt.UpdatedAt.Format(time.RFC3339),
 | 
					 | 
				
			||||||
	}, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// newListDeviceTemplateResponse 从数据库模型切片创建一个新的设备模板列表响应 DTO 切片
 | 
					 | 
				
			||||||
func newListDeviceTemplateResponse(dts []*models.DeviceTemplate) ([]*DeviceTemplateResponse, error) {
 | 
					 | 
				
			||||||
	list := make([]*DeviceTemplateResponse, 0, len(dts))
 | 
					 | 
				
			||||||
	for _, dt := range dts {
 | 
					 | 
				
			||||||
		resp, err := newDeviceTemplateResponse(dt)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		list = append(list, resp)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return list, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Controller Methods: Devices ---
 | 
					// --- Controller Methods: Devices ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateDevice godoc
 | 
					// CreateDevice godoc
 | 
				
			||||||
@@ -279,12 +46,12 @@ func newListDeviceTemplateResponse(dts []*models.DeviceTemplate) ([]*DeviceTempl
 | 
				
			|||||||
// @Tags         设备管理
 | 
					// @Tags         设备管理
 | 
				
			||||||
// @Accept       json
 | 
					// @Accept       json
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        device body CreateDeviceRequest true "设备信息"
 | 
					// @Param        device body dto.CreateDeviceRequest true "设备信息"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=DeviceResponse}
 | 
					// @Success      200 {object} controller.Response{data=dto.DeviceResponse}
 | 
				
			||||||
// @Router       /api/v1/devices [post]
 | 
					// @Router       /api/v1/devices [post]
 | 
				
			||||||
func (c *Controller) CreateDevice(ctx *gin.Context) {
 | 
					func (c *Controller) CreateDevice(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "创建设备"
 | 
						const actionType = "创建设备"
 | 
				
			||||||
	var req CreateDeviceRequest
 | 
						var req dto.CreateDeviceRequest
 | 
				
			||||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
				
			||||||
@@ -325,9 +92,9 @@ func (c *Controller) CreateDevice(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newDeviceResponse(createdDevice)
 | 
						resp, err := dto.NewDeviceResponse(createdDevice)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, createdDevice)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但响应生成失败", actionType, "响应序列化失败", createdDevice)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但响应生成失败", actionType, "响应序列化失败", createdDevice)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -342,7 +109,7 @@ func (c *Controller) CreateDevice(ctx *gin.Context) {
 | 
				
			|||||||
// @Tags         设备管理
 | 
					// @Tags         设备管理
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        id path string true "设备ID"
 | 
					// @Param        id path string true "设备ID"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=DeviceResponse}
 | 
					// @Success      200 {object} controller.Response{data=dto.DeviceResponse}
 | 
				
			||||||
// @Router       /api/v1/devices/{id} [get]
 | 
					// @Router       /api/v1/devices/{id} [get]
 | 
				
			||||||
func (c *Controller) GetDevice(ctx *gin.Context) {
 | 
					func (c *Controller) GetDevice(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "获取设备"
 | 
						const actionType = "获取设备"
 | 
				
			||||||
@@ -371,7 +138,7 @@ func (c *Controller) GetDevice(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newDeviceResponse(device)
 | 
						resp, err := dto.NewDeviceResponse(device)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, device)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, device)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: 内部数据格式错误", actionType, "响应序列化失败", device)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: 内部数据格式错误", actionType, "响应序列化失败", device)
 | 
				
			||||||
@@ -387,7 +154,7 @@ func (c *Controller) GetDevice(ctx *gin.Context) {
 | 
				
			|||||||
// @Description  获取系统中所有设备的列表
 | 
					// @Description  获取系统中所有设备的列表
 | 
				
			||||||
// @Tags         设备管理
 | 
					// @Tags         设备管理
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=[]DeviceResponse}
 | 
					// @Success      200 {object} controller.Response{data=[]dto.DeviceResponse}
 | 
				
			||||||
// @Router       /api/v1/devices [get]
 | 
					// @Router       /api/v1/devices [get]
 | 
				
			||||||
func (c *Controller) ListDevices(ctx *gin.Context) {
 | 
					func (c *Controller) ListDevices(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "获取设备列表"
 | 
						const actionType = "获取设备列表"
 | 
				
			||||||
@@ -398,7 +165,7 @@ func (c *Controller) ListDevices(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newListDeviceResponse(devices)
 | 
						resp, err := dto.NewListDeviceResponse(devices)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Devices: %+v", actionType, err, devices)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, Devices: %+v", actionType, err, devices)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: 内部数据格式错误", actionType, "响应序列化失败", devices)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: 内部数据格式错误", actionType, "响应序列化失败", devices)
 | 
				
			||||||
@@ -416,8 +183,8 @@ func (c *Controller) ListDevices(ctx *gin.Context) {
 | 
				
			|||||||
// @Accept       json
 | 
					// @Accept       json
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        id path string true "设备ID"
 | 
					// @Param        id path string true "设备ID"
 | 
				
			||||||
// @Param        device body UpdateDeviceRequest true "要更新的设备信息"
 | 
					// @Param        device body dto.UpdateDeviceRequest true "要更新的设备信息"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=DeviceResponse}
 | 
					// @Success      200 {object} controller.Response{data=dto.DeviceResponse}
 | 
				
			||||||
// @Router       /api/v1/devices/{id} [put]
 | 
					// @Router       /api/v1/devices/{id} [put]
 | 
				
			||||||
func (c *Controller) UpdateDevice(ctx *gin.Context) {
 | 
					func (c *Controller) UpdateDevice(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "更新设备"
 | 
						const actionType = "更新设备"
 | 
				
			||||||
@@ -440,7 +207,7 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var req UpdateDeviceRequest
 | 
						var req dto.UpdateDeviceRequest
 | 
				
			||||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
				
			||||||
@@ -479,7 +246,7 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newDeviceResponse(updatedDevice)
 | 
						resp, err := dto.NewDeviceResponse(updatedDevice)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, updatedDevice)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, updatedDevice)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但响应生成失败", actionType, "响应序列化失败", updatedDevice)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但响应生成失败", actionType, "响应序列化失败", updatedDevice)
 | 
				
			||||||
@@ -539,12 +306,12 @@ func (c *Controller) DeleteDevice(ctx *gin.Context) {
 | 
				
			|||||||
// @Tags         区域主控管理
 | 
					// @Tags         区域主控管理
 | 
				
			||||||
// @Accept       json
 | 
					// @Accept       json
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        areaController body CreateAreaControllerRequest true "区域主控信息"
 | 
					// @Param        areaController body dto.CreateAreaControllerRequest true "区域主控信息"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=AreaControllerResponse}
 | 
					// @Success      200 {object} controller.Response{data=dto.AreaControllerResponse}
 | 
				
			||||||
// @Router       /api/v1/area-controllers [post]
 | 
					// @Router       /api/v1/area-controllers [post]
 | 
				
			||||||
func (c *Controller) CreateAreaController(ctx *gin.Context) {
 | 
					func (c *Controller) CreateAreaController(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "创建区域主控"
 | 
						const actionType = "创建区域主控"
 | 
				
			||||||
	var req CreateAreaControllerRequest
 | 
						var req dto.CreateAreaControllerRequest
 | 
				
			||||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
				
			||||||
@@ -577,7 +344,7 @@ func (c *Controller) CreateAreaController(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newAreaControllerResponse(ac)
 | 
						resp, err := dto.NewAreaControllerResponse(ac)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控创建成功,但响应生成失败", actionType, "响应序列化失败", ac)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控创建成功,但响应生成失败", actionType, "响应序列化失败", ac)
 | 
				
			||||||
@@ -594,7 +361,7 @@ func (c *Controller) CreateAreaController(ctx *gin.Context) {
 | 
				
			|||||||
// @Tags         区域主控管理
 | 
					// @Tags         区域主控管理
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        id path string true "区域主控ID"
 | 
					// @Param        id path string true "区域主控ID"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=AreaControllerResponse}
 | 
					// @Success      200 {object} controller.Response{data=dto.AreaControllerResponse}
 | 
				
			||||||
// @Router       /api/v1/area-controllers/{id} [get]
 | 
					// @Router       /api/v1/area-controllers/{id} [get]
 | 
				
			||||||
func (c *Controller) GetAreaController(ctx *gin.Context) {
 | 
					func (c *Controller) GetAreaController(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "获取区域主控"
 | 
						const actionType = "获取区域主控"
 | 
				
			||||||
@@ -619,7 +386,7 @@ func (c *Controller) GetAreaController(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newAreaControllerResponse(ac)
 | 
						resp, err := dto.NewAreaControllerResponse(ac)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, ac)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, ac)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: 内部数据格式错误", actionType, "响应序列化失败", ac)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: 内部数据格式错误", actionType, "响应序列化失败", ac)
 | 
				
			||||||
@@ -635,7 +402,7 @@ func (c *Controller) GetAreaController(ctx *gin.Context) {
 | 
				
			|||||||
// @Description  获取系统中所有区域主控的列表
 | 
					// @Description  获取系统中所有区域主控的列表
 | 
				
			||||||
// @Tags         区域主控管理
 | 
					// @Tags         区域主控管理
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=[]AreaControllerResponse}
 | 
					// @Success      200 {object} controller.Response{data=[]dto.AreaControllerResponse}
 | 
				
			||||||
// @Router       /api/v1/area-controllers [get]
 | 
					// @Router       /api/v1/area-controllers [get]
 | 
				
			||||||
func (c *Controller) ListAreaControllers(ctx *gin.Context) {
 | 
					func (c *Controller) ListAreaControllers(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "获取区域主控列表"
 | 
						const actionType = "获取区域主控列表"
 | 
				
			||||||
@@ -646,7 +413,7 @@ func (c *Controller) ListAreaControllers(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newListAreaControllerResponse(acs)
 | 
						resp, err := dto.NewListAreaControllerResponse(acs)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, AreaControllers: %+v", actionType, err, acs)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, AreaControllers: %+v", actionType, err, acs)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: 内部数据格式错误", actionType, "响应序列化失败", acs)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: 内部数据格式错误", actionType, "响应序列化失败", acs)
 | 
				
			||||||
@@ -664,8 +431,8 @@ func (c *Controller) ListAreaControllers(ctx *gin.Context) {
 | 
				
			|||||||
// @Accept       json
 | 
					// @Accept       json
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        id path string true "区域主控ID"
 | 
					// @Param        id path string true "区域主控ID"
 | 
				
			||||||
// @Param        areaController body UpdateAreaControllerRequest true "要更新的区域主控信息"
 | 
					// @Param        areaController body dto.UpdateAreaControllerRequest true "要更新的区域主控信息"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=AreaControllerResponse}
 | 
					// @Success      200 {object} controller.Response{data=dto.AreaControllerResponse}
 | 
				
			||||||
// @Router       /api/v1/area-controllers/{id} [put]
 | 
					// @Router       /api/v1/area-controllers/{id} [put]
 | 
				
			||||||
func (c *Controller) UpdateAreaController(ctx *gin.Context) {
 | 
					func (c *Controller) UpdateAreaController(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "更新区域主控"
 | 
						const actionType = "更新区域主控"
 | 
				
			||||||
@@ -690,7 +457,7 @@ func (c *Controller) UpdateAreaController(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var req UpdateAreaControllerRequest
 | 
						var req dto.UpdateAreaControllerRequest
 | 
				
			||||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
				
			||||||
@@ -721,7 +488,7 @@ func (c *Controller) UpdateAreaController(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newAreaControllerResponse(existingAC)
 | 
						resp, err := dto.NewAreaControllerResponse(existingAC)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, existingAC)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, existingAC)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控更新成功,但响应生成失败", actionType, "响应序列化失败", existingAC)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控更新成功,但响应生成失败", actionType, "响应序列化失败", existingAC)
 | 
				
			||||||
@@ -781,12 +548,12 @@ func (c *Controller) DeleteAreaController(ctx *gin.Context) {
 | 
				
			|||||||
// @Tags         设备模板管理
 | 
					// @Tags         设备模板管理
 | 
				
			||||||
// @Accept       json
 | 
					// @Accept       json
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        deviceTemplate body CreateDeviceTemplateRequest true "设备模板信息"
 | 
					// @Param        deviceTemplate body dto.CreateDeviceTemplateRequest true "设备模板信息"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=DeviceTemplateResponse}
 | 
					// @Success      200 {object} controller.Response{data=dto.DeviceTemplateResponse}
 | 
				
			||||||
// @Router       /api/v1/device-templates [post]
 | 
					// @Router       /api/v1/device-templates [post]
 | 
				
			||||||
func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
 | 
					func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "创建设备模板"
 | 
						const actionType = "创建设备模板"
 | 
				
			||||||
	var req CreateDeviceTemplateRequest
 | 
						var req dto.CreateDeviceTemplateRequest
 | 
				
			||||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
				
			||||||
@@ -828,7 +595,7 @@ func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newDeviceTemplateResponse(deviceTemplate)
 | 
						resp, err := dto.NewDeviceTemplateResponse(deviceTemplate)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板创建成功,但响应生成失败", actionType, "响应序列化失败", deviceTemplate)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板创建成功,但响应生成失败", actionType, "响应序列化失败", deviceTemplate)
 | 
				
			||||||
@@ -845,7 +612,7 @@ func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
 | 
				
			|||||||
// @Tags         设备模板管理
 | 
					// @Tags         设备模板管理
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        id path string true "设备模板ID"
 | 
					// @Param        id path string true "设备模板ID"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=DeviceTemplateResponse}
 | 
					// @Success      200 {object} controller.Response{data=dto.DeviceTemplateResponse}
 | 
				
			||||||
// @Router       /api/v1/device-templates/{id} [get]
 | 
					// @Router       /api/v1/device-templates/{id} [get]
 | 
				
			||||||
func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
 | 
					func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "获取设备模板"
 | 
						const actionType = "获取设备模板"
 | 
				
			||||||
@@ -870,7 +637,7 @@ func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newDeviceTemplateResponse(deviceTemplate)
 | 
						resp, err := dto.NewDeviceTemplateResponse(deviceTemplate)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, deviceTemplate)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, deviceTemplate)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplate)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplate)
 | 
				
			||||||
@@ -886,7 +653,7 @@ func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
 | 
				
			|||||||
// @Description  获取系统中所有设备模板的列表
 | 
					// @Description  获取系统中所有设备模板的列表
 | 
				
			||||||
// @Tags         设备模板管理
 | 
					// @Tags         设备模板管理
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=[]DeviceTemplateResponse}
 | 
					// @Success      200 {object} controller.Response{data=[]dto.DeviceTemplateResponse}
 | 
				
			||||||
// @Router       /api/v1/device-templates [get]
 | 
					// @Router       /api/v1/device-templates [get]
 | 
				
			||||||
func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
 | 
					func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "获取设备模板列表"
 | 
						const actionType = "获取设备模板列表"
 | 
				
			||||||
@@ -897,7 +664,7 @@ func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newListDeviceTemplateResponse(deviceTemplates)
 | 
						resp, err := dto.NewListDeviceTemplateResponse(deviceTemplates)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplates: %+v", actionType, err, deviceTemplates)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplates: %+v", actionType, err, deviceTemplates)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplates)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplates)
 | 
				
			||||||
@@ -915,8 +682,8 @@ func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
 | 
				
			|||||||
// @Accept       json
 | 
					// @Accept       json
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        id path string true "设备模板ID"
 | 
					// @Param        id path string true "设备模板ID"
 | 
				
			||||||
// @Param        deviceTemplate body UpdateDeviceTemplateRequest true "要更新的设备模板信息"
 | 
					// @Param        deviceTemplate body dto.UpdateDeviceTemplateRequest true "要更新的设备模板信息"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=DeviceTemplateResponse}
 | 
					// @Success      200 {object} controller.Response{data=dto.DeviceTemplateResponse}
 | 
				
			||||||
// @Router       /api/v1/device-templates/{id} [put]
 | 
					// @Router       /api/v1/device-templates/{id} [put]
 | 
				
			||||||
func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
 | 
					func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "更新设备模板"
 | 
						const actionType = "更新设备模板"
 | 
				
			||||||
@@ -941,7 +708,7 @@ func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var req UpdateDeviceTemplateRequest
 | 
						var req dto.UpdateDeviceTemplateRequest
 | 
				
			||||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
				
			||||||
@@ -981,7 +748,7 @@ func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := newDeviceTemplateResponse(existingDeviceTemplate)
 | 
						resp, err := dto.NewDeviceTemplateResponse(existingDeviceTemplate)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板更新成功,但响应生成失败", actionType, "响应序列化失败", existingDeviceTemplate)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板更新成功,但响应生成失败", actionType, "响应序列化失败", existingDeviceTemplate)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,7 @@ type MockDeviceRepository struct {
 | 
				
			|||||||
	mock.Mock
 | 
						mock.Mock
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Create 模拟 DeviceRepository 的 Create 方法
 | 
					// CreateTx 模拟 DeviceRepository 的 CreateTx 方法
 | 
				
			||||||
func (m *MockDeviceRepository) Create(device *models.Device) error {
 | 
					func (m *MockDeviceRepository) Create(device *models.Device) error {
 | 
				
			||||||
	args := m.Called(device)
 | 
						args := m.Called(device)
 | 
				
			||||||
	return args.Error(0)
 | 
						return args.Error(0)
 | 
				
			||||||
@@ -169,7 +169,7 @@ func TestCreateDevice(t *testing.T) {
 | 
				
			|||||||
				Properties: controller.Properties(`{"lora_address":"0x1234"}`),
 | 
									Properties: controller.Properties(`{"lora_address":"0x1234"}`),
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			mockRepoSetup: func(m *MockDeviceRepository) {
 | 
								mockRepoSetup: func(m *MockDeviceRepository) {
 | 
				
			||||||
				m.On("Create", mock.MatchedBy(func(dev *models.Device) bool {
 | 
									m.On("CreateTx", mock.MatchedBy(func(dev *models.Device) bool {
 | 
				
			||||||
					// 检查 Name 字段
 | 
										// 检查 Name 字段
 | 
				
			||||||
					nameMatch := dev.Name == "主控A"
 | 
										nameMatch := dev.Name == "主控A"
 | 
				
			||||||
					// 检查 Type 字段
 | 
										// 检查 Type 字段
 | 
				
			||||||
@@ -215,7 +215,7 @@ func TestCreateDevice(t *testing.T) {
 | 
				
			|||||||
				Properties: controller.Properties(`{"bus_id":1,"bus_address":10}`),
 | 
									Properties: controller.Properties(`{"bus_id":1,"bus_address":10}`),
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			mockRepoSetup: func(m *MockDeviceRepository) {
 | 
								mockRepoSetup: func(m *MockDeviceRepository) {
 | 
				
			||||||
				m.On("Create", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
 | 
									m.On("CreateTx", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
 | 
				
			||||||
					arg := args.Get(0).(*models.Device)
 | 
										arg := args.Get(0).(*models.Device)
 | 
				
			||||||
					arg.ID = 2
 | 
										arg.ID = 2
 | 
				
			||||||
					arg.CreatedAt = time.Now()
 | 
										arg.CreatedAt = time.Now()
 | 
				
			||||||
@@ -259,7 +259,7 @@ func TestCreateDevice(t *testing.T) {
 | 
				
			|||||||
				Type: models.DeviceTypeDevice,
 | 
									Type: models.DeviceTypeDevice,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			mockRepoSetup: func(m *MockDeviceRepository) {
 | 
								mockRepoSetup: func(m *MockDeviceRepository) {
 | 
				
			||||||
				m.On("Create", mock.Anything).Return(errors.New("db error")).Once()
 | 
									m.On("CreateTx", mock.Anything).Return(errors.New("db error")).Once()
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			expectedStatus:   http.StatusOK,
 | 
								expectedStatus:   http.StatusOK,
 | 
				
			||||||
			expectedCode:     controller.CodeInternalError,
 | 
								expectedCode:     controller.CodeInternalError,
 | 
				
			||||||
@@ -276,9 +276,9 @@ func TestCreateDevice(t *testing.T) {
 | 
				
			|||||||
				Properties: controller.Properties(`{invalid json}`),
 | 
									Properties: controller.Properties(`{invalid json}`),
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			mockRepoSetup: func(m *MockDeviceRepository) {
 | 
								mockRepoSetup: func(m *MockDeviceRepository) {
 | 
				
			||||||
				// 期望 Create 方法被调用,并返回一个模拟的数据库错误
 | 
									// 期望 CreateTx 方法被调用,并返回一个模拟的数据库错误
 | 
				
			||||||
				// 这个错误模拟的是数据库层因为 Properties 字段的 JSON 格式无效而拒绝保存
 | 
									// 这个错误模拟的是数据库层因为 Properties 字段的 JSON 格式无效而拒绝保存
 | 
				
			||||||
				m.On("Create", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) {
 | 
									m.On("CreateTx", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) {
 | 
				
			||||||
					dev := args.Get(0).(*models.Device)
 | 
										dev := args.Get(0).(*models.Device)
 | 
				
			||||||
					assert.Equal(t, "无效JSON设备", dev.Name)
 | 
										assert.Equal(t, "无效JSON设备", dev.Name)
 | 
				
			||||||
					assert.Equal(t, models.DeviceTypeDevice, dev.Type)
 | 
										assert.Equal(t, models.DeviceTypeDevice, dev.Type)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										238
									
								
								internal/app/controller/management/controller_helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								internal/app/controller/management/controller_helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,238 @@
 | 
				
			|||||||
 | 
					package management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// mapAndSendError 统一映射服务层错误并发送响应。
 | 
				
			||||||
 | 
					// 这个函数将服务层返回的错误转换为控制器层应返回的HTTP状态码和审计信息。
 | 
				
			||||||
 | 
					func mapAndSendError(c *PigBatchController, ctx *gin.Context, action string, err error, id uint) {
 | 
				
			||||||
 | 
						if errors.Is(err, service.ErrPigBatchNotFound) ||
 | 
				
			||||||
 | 
							errors.Is(err, service.ErrPenNotFound) ||
 | 
				
			||||||
 | 
							errors.Is(err, service.ErrPenNotAssociatedWithBatch) {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
 | 
				
			||||||
 | 
						} else if errors.Is(err, service.ErrInvalidOperation) ||
 | 
				
			||||||
 | 
							errors.Is(err, service.ErrPigBatchActive) ||
 | 
				
			||||||
 | 
							errors.Is(err, service.ErrPigBatchNotActive) ||
 | 
				
			||||||
 | 
							errors.Is(err, service.ErrPenOccupiedByOtherBatch) ||
 | 
				
			||||||
 | 
							errors.Is(err, service.ErrPenStatusInvalidForAllocation) ||
 | 
				
			||||||
 | 
							errors.Is(err, service.ErrPenNotEmpty) {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							c.logger.Errorf("操作[%s]业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "操作失败", action, err.Error(), id)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// idExtractorFunc 定义了一个函数类型,用于从gin.Context中提取主ID。
 | 
				
			||||||
 | 
					type idExtractorFunc func(ctx *gin.Context) (uint, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// extractOperatorAndPrimaryID 封装了从gin.Context中提取操作员ID和主ID的通用逻辑。
 | 
				
			||||||
 | 
					// 它负责处理ID提取过程中的错误,并发送相应的HTTP响应。
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 参数:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	c: *PigBatchController - 控制器实例,用于访问其日志。
 | 
				
			||||||
 | 
					//	ctx: *gin.Context - Gin上下文。
 | 
				
			||||||
 | 
					//	action: string - 当前操作的描述,用于日志和审计。
 | 
				
			||||||
 | 
					//	idExtractor: idExtractorFunc - 可选函数,用于从ctx中提取主ID。如果为nil,则尝试从":id"路径参数中提取。
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 返回值:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	operatorID: uint - 提取到的操作员ID。
 | 
				
			||||||
 | 
					//	primaryID: uint - 提取到的主ID。
 | 
				
			||||||
 | 
					//	ok: bool - 如果ID提取成功且没有发送错误响应,则为true。
 | 
				
			||||||
 | 
					func extractOperatorAndPrimaryID(
 | 
				
			||||||
 | 
						c *PigBatchController,
 | 
				
			||||||
 | 
						ctx *gin.Context,
 | 
				
			||||||
 | 
						action string,
 | 
				
			||||||
 | 
						idExtractor idExtractorFunc,
 | 
				
			||||||
 | 
					) (operatorID uint, primaryID uint, ok bool) {
 | 
				
			||||||
 | 
						// 1. 获取操作员ID
 | 
				
			||||||
 | 
						operatorID, err := controller.GetOperatorIDFromContext(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
 | 
				
			||||||
 | 
							return 0, 0, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 提取主ID
 | 
				
			||||||
 | 
						if idExtractor != nil {
 | 
				
			||||||
 | 
							primaryID, err = idExtractor(ctx)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error())
 | 
				
			||||||
 | 
								return 0, 0, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else { // 默认从 ":id" 路径参数提取
 | 
				
			||||||
 | 
							idParam := ctx.Param("id")
 | 
				
			||||||
 | 
							if idParam == "" { // 有些端点可能没有 "id" 参数,例如列表或创建操作
 | 
				
			||||||
 | 
								// 如果没有ID参数且没有自定义提取器,primaryID保持为0,这对于某些操作是可接受的
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								parsedID, err := strconv.ParseUint(idParam, 10, 32)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam)
 | 
				
			||||||
 | 
									return 0, 0, false
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								primaryID = uint(parsedID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return operatorID, primaryID, true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleAPIRequest 封装了控制器中处理带有请求体和路径参数的API请求的通用逻辑。
 | 
				
			||||||
 | 
					// 它负责请求体绑定、操作员ID获取、服务层调用、错误映射和响应发送。
 | 
				
			||||||
 | 
					func handleAPIRequest[Req any](
 | 
				
			||||||
 | 
						c *PigBatchController,
 | 
				
			||||||
 | 
						ctx *gin.Context,
 | 
				
			||||||
 | 
						action string,
 | 
				
			||||||
 | 
						reqDTO Req,
 | 
				
			||||||
 | 
						serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) error,
 | 
				
			||||||
 | 
						successMsg string,
 | 
				
			||||||
 | 
						idExtractor idExtractorFunc,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// 1. 绑定请求体
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindJSON(&reqDTO); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 提取操作员ID和主ID
 | 
				
			||||||
 | 
						operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return // 错误已在 extractOperatorAndPrimaryID 中处理
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 执行服务层逻辑
 | 
				
			||||||
 | 
						err := serviceExecutor(ctx, operatorID, primaryID, reqDTO)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							mapAndSendError(c, ctx, action, err, primaryID)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 4. 发送成功响应
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleNoBodyAPIRequest 封装了处理不带请求体,但有路径参数和操作员ID的API请求的通用逻辑。
 | 
				
			||||||
 | 
					func handleNoBodyAPIRequest(
 | 
				
			||||||
 | 
						c *PigBatchController,
 | 
				
			||||||
 | 
						ctx *gin.Context,
 | 
				
			||||||
 | 
						action string,
 | 
				
			||||||
 | 
						serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint) error,
 | 
				
			||||||
 | 
						successMsg string,
 | 
				
			||||||
 | 
						idExtractor idExtractorFunc,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// 1. 提取操作员ID和主ID
 | 
				
			||||||
 | 
						operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return // 错误已在 extractOperatorAndPrimaryID 中处理
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 执行服务层逻辑
 | 
				
			||||||
 | 
						err := serviceExecutor(ctx, operatorID, primaryID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							mapAndSendError(c, ctx, action, err, primaryID)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 发送成功响应
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleAPIRequestWithResponse 封装了控制器中处理带有请求体、路径参数并返回响应DTO的API请求的通用逻辑。
 | 
				
			||||||
 | 
					func handleAPIRequestWithResponse[Req any, Resp any](
 | 
				
			||||||
 | 
						c *PigBatchController,
 | 
				
			||||||
 | 
						ctx *gin.Context,
 | 
				
			||||||
 | 
						action string,
 | 
				
			||||||
 | 
						reqDTO Req,
 | 
				
			||||||
 | 
						serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) (Resp, error), // serviceExecutor现在返回Resp
 | 
				
			||||||
 | 
						successMsg string,
 | 
				
			||||||
 | 
						idExtractor idExtractorFunc,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// 1. 绑定请求体
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindJSON(&reqDTO); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 提取操作员ID和主ID
 | 
				
			||||||
 | 
						operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return // 错误已在 extractOperatorAndPrimaryID 中处理
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 执行服务层逻辑
 | 
				
			||||||
 | 
						respDTO, err := serviceExecutor(ctx, operatorID, primaryID, reqDTO)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							mapAndSendError(c, ctx, action, err, primaryID)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 4. 发送成功响应
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleNoBodyAPIRequestWithResponse 封装了处理不带请求体,但有路径参数和操作员ID,并返回响应DTO的API请求的通用逻辑。
 | 
				
			||||||
 | 
					func handleNoBodyAPIRequestWithResponse[Resp any](
 | 
				
			||||||
 | 
						c *PigBatchController,
 | 
				
			||||||
 | 
						ctx *gin.Context,
 | 
				
			||||||
 | 
						action string,
 | 
				
			||||||
 | 
						serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint) (Resp, error), // serviceExecutor现在返回Resp
 | 
				
			||||||
 | 
						successMsg string,
 | 
				
			||||||
 | 
						idExtractor idExtractorFunc,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// 1. 提取操作员ID和主ID
 | 
				
			||||||
 | 
						operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return // 错误已在 extractOperatorAndPrimaryID 中处理
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 执行服务层逻辑
 | 
				
			||||||
 | 
						respDTO, err := serviceExecutor(ctx, operatorID, primaryID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							mapAndSendError(c, ctx, action, err, primaryID)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 发送成功响应
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleQueryAPIRequestWithResponse 封装了处理带有查询参数并返回响应DTO的API请求的通用逻辑。
 | 
				
			||||||
 | 
					func handleQueryAPIRequestWithResponse[Query any, Resp any](
 | 
				
			||||||
 | 
						c *PigBatchController,
 | 
				
			||||||
 | 
						ctx *gin.Context,
 | 
				
			||||||
 | 
						action string,
 | 
				
			||||||
 | 
						queryDTO Query,
 | 
				
			||||||
 | 
						serviceExecutor func(ctx *gin.Context, operatorID uint, query Query) (Resp, error), // serviceExecutor现在接收queryDTO
 | 
				
			||||||
 | 
						successMsg string,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// 1. 绑定查询参数
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindQuery(&queryDTO); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数", action, "查询参数绑定失败", queryDTO)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 获取操作员ID
 | 
				
			||||||
 | 
						operatorID, err := controller.GetOperatorIDFromContext(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 执行服务层逻辑
 | 
				
			||||||
 | 
						respDTO, err := serviceExecutor(ctx, operatorID, queryDTO)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							// 对于列表查询,通常没有primaryID,所以传递0
 | 
				
			||||||
 | 
							mapAndSendError(c, ctx, action, err, 0)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 4. 发送成功响应
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								internal/app/controller/management/feed_controller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								internal/app/controller/management/feed_controller.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					package management
 | 
				
			||||||
@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					package management
 | 
				
			||||||
							
								
								
									
										251
									
								
								internal/app/controller/management/pig_batch_controller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								internal/app/controller/management/pig_batch_controller.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,251 @@
 | 
				
			|||||||
 | 
					package management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchController 负责处理猪批次相关的API请求
 | 
				
			||||||
 | 
					type PigBatchController struct {
 | 
				
			||||||
 | 
						logger  *logs.Logger
 | 
				
			||||||
 | 
						service service.PigBatchService
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewPigBatchController 创建一个新的 PigBatchController 实例
 | 
				
			||||||
 | 
					func NewPigBatchController(logger *logs.Logger, service service.PigBatchService) *PigBatchController {
 | 
				
			||||||
 | 
						return &PigBatchController{
 | 
				
			||||||
 | 
							logger:  logger,
 | 
				
			||||||
 | 
							service: service,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigBatch godoc
 | 
				
			||||||
 | 
					// @Summary      创建猪批次
 | 
				
			||||||
 | 
					// @Description  创建一个新的猪批次
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        body body dto.PigBatchCreateDTO true "猪批次信息"
 | 
				
			||||||
 | 
					// @Success      201 {object} controller.Response{data=dto.PigBatchResponseDTO} "创建成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "创建猪批次"
 | 
				
			||||||
 | 
						var req dto.PigBatchCreateDTO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequestWithResponse(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
 | 
				
			||||||
 | 
								// 对于创建操作,primaryID通常不从路径中获取,而是由服务层生成
 | 
				
			||||||
 | 
								return c.service.CreatePigBatch(operatorID, req)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"创建成功",
 | 
				
			||||||
 | 
							nil, // 无需自定义ID提取器,primaryID将为0
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPigBatch godoc
 | 
				
			||||||
 | 
					// @Summary      获取单个猪批次
 | 
				
			||||||
 | 
					// @Description  根据ID获取单个猪批次信息
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id} [get]\
 | 
				
			||||||
 | 
					func (c *PigBatchController) GetPigBatch(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "获取猪批次"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleNoBodyAPIRequestWithResponse(
 | 
				
			||||||
 | 
							c, ctx, action,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint) (*dto.PigBatchResponseDTO, error) {
 | 
				
			||||||
 | 
								return c.service.GetPigBatch(primaryID)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"获取成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePigBatch godoc
 | 
				
			||||||
 | 
					// @Summary      更新猪批次
 | 
				
			||||||
 | 
					// @Description  更新一个已存在的猪批次信息
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.PigBatchUpdateDTO true "猪批次信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response{data=dto.PigBatchResponseDTO} "更新成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id} [put]
 | 
				
			||||||
 | 
					func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "更新猪批次"
 | 
				
			||||||
 | 
						var req dto.PigBatchUpdateDTO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequestWithResponse(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
 | 
				
			||||||
 | 
								return c.service.UpdatePigBatch(primaryID, req)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"更新成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeletePigBatch godoc
 | 
				
			||||||
 | 
					// @Summary      删除猪批次
 | 
				
			||||||
 | 
					// @Description  根据ID删除一个猪批次
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "删除成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id} [delete]
 | 
				
			||||||
 | 
					func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "删除猪批次"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleNoBodyAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint) error {
 | 
				
			||||||
 | 
								return c.service.DeletePigBatch(primaryID)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"删除成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPigBatches godoc
 | 
				
			||||||
 | 
					// @Summary      获取猪批次列表
 | 
				
			||||||
 | 
					// @Description  获取所有猪批次的列表,支持按活跃状态筛选
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        is_active query bool false "是否活跃 (true/false)"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response{data=[]dto.PigBatchResponseDTO} "获取成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches [get]
 | 
				
			||||||
 | 
					func (c *PigBatchController) ListPigBatches(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "获取猪批次列表"
 | 
				
			||||||
 | 
						var query dto.PigBatchQueryDTO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleQueryAPIRequestWithResponse(
 | 
				
			||||||
 | 
							c, ctx, action, &query,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, query *dto.PigBatchQueryDTO) ([]*dto.PigBatchResponseDTO, error) {
 | 
				
			||||||
 | 
								return c.service.ListPigBatches(query.IsActive)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"获取成功",
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AssignEmptyPensToBatch godoc
 | 
				
			||||||
 | 
					// @Summary      为猪批次分配空栏
 | 
				
			||||||
 | 
					// @Description  将一个或多个空闲猪栏分配给指定的猪批次
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.AssignEmptyPensToBatchRequest true "待分配的猪栏ID列表"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "分配成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/assign-pens [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) AssignEmptyPensToBatch(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "为猪批次分配空栏"
 | 
				
			||||||
 | 
						var req dto.AssignEmptyPensToBatchRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.AssignEmptyPensToBatchRequest) error {
 | 
				
			||||||
 | 
								return c.service.AssignEmptyPensToBatch(primaryID, req.PenIDs, operatorID)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"分配成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReclassifyPenToNewBatch godoc
 | 
				
			||||||
 | 
					// @Summary      将猪栏划拨到新批次
 | 
				
			||||||
 | 
					// @Description  将一个猪栏(连同其中的猪只)从一个批次整体划拨到另一个批次
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        fromBatchID path int true "源猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.ReclassifyPenToNewBatchRequest true "划拨请求信息 (包含目标批次ID、猪栏ID和备注)"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "划拨成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{fromBatchID}/reclassify-pen [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) ReclassifyPenToNewBatch(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "划拨猪栏到新批次"
 | 
				
			||||||
 | 
						var req dto.ReclassifyPenToNewBatchRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.ReclassifyPenToNewBatchRequest) error {
 | 
				
			||||||
 | 
								// primaryID 在这里是 fromBatchID
 | 
				
			||||||
 | 
								return c.service.ReclassifyPenToNewBatch(primaryID, req.ToBatchID, req.PenID, operatorID, req.Remarks)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"划拨成功",
 | 
				
			||||||
 | 
							func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":fromBatchID" 路径参数提取
 | 
				
			||||||
 | 
								idParam := ctx.Param("fromBatchID")
 | 
				
			||||||
 | 
								parsedID, err := strconv.ParseUint(idParam, 10, 32)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return 0, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return uint(parsedID), nil
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RemoveEmptyPenFromBatch godoc
 | 
				
			||||||
 | 
					// @Summary      从猪批次移除空栏
 | 
				
			||||||
 | 
					// @Description  将一个空闲猪栏从指定的猪批次中移除
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        batchID path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        penID path int true "待移除的猪栏ID"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "移除成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{batchID}/remove-pen/{penID} [delete]
 | 
				
			||||||
 | 
					func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "从猪批次移除空栏"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleNoBodyAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint) error {
 | 
				
			||||||
 | 
								// primaryID 在这里是 batchID
 | 
				
			||||||
 | 
								penIDParam := ctx.Param("penID")
 | 
				
			||||||
 | 
								penID, err := strconv.ParseUint(penIDParam, 10, 32)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err // 返回错误,因为 penID 格式无效
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return c.service.RemoveEmptyPenFromBatch(primaryID, uint(penID))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"移除成功",
 | 
				
			||||||
 | 
							func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":batchID" 路径参数提取
 | 
				
			||||||
 | 
								idParam := ctx.Param("batchID")
 | 
				
			||||||
 | 
								parsedID, err := strconv.ParseUint(idParam, 10, 32)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return 0, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return uint(parsedID), nil
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MovePigsIntoPen godoc
 | 
				
			||||||
 | 
					// @Summary      将猪只从“虚拟库存”移入指定猪栏
 | 
				
			||||||
 | 
					// @Description  将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.MovePigsIntoPenRequest true "移入猪只请求信息 (包含目标猪栏ID、数量和备注)"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "移入成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/move-pigs-into-pen [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) MovePigsIntoPen(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "将猪只移入猪栏"
 | 
				
			||||||
 | 
						var req dto.MovePigsIntoPenRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.MovePigsIntoPenRequest) error {
 | 
				
			||||||
 | 
								return c.service.MovePigsIntoPen(primaryID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"移入成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,150 @@
 | 
				
			|||||||
 | 
					package management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigs godoc
 | 
				
			||||||
 | 
					// @Summary      记录新增病猪事件
 | 
				
			||||||
 | 
					// @Description  记录猪批次中新增病猪的数量、治疗地点和发生时间
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.RecordSickPigsRequest true "记录病猪请求信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "记录成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/record-sick-pigs [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) RecordSickPigs(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "记录新增病猪事件"
 | 
				
			||||||
 | 
						var req dto.RecordSickPigsRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigsRequest) error {
 | 
				
			||||||
 | 
								return c.service.RecordSickPigs(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"记录成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigRecovery godoc
 | 
				
			||||||
 | 
					// @Summary      记录病猪康复事件
 | 
				
			||||||
 | 
					// @Description  记录猪批次中病猪康复的数量、治疗地点和发生时间
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.RecordSickPigRecoveryRequest true "记录病猪康复请求信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "记录成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/record-sick-pig-recovery [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) RecordSickPigRecovery(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "记录病猪康复事件"
 | 
				
			||||||
 | 
						var req dto.RecordSickPigRecoveryRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigRecoveryRequest) error {
 | 
				
			||||||
 | 
								return c.service.RecordSickPigRecovery(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"记录成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigDeath godoc
 | 
				
			||||||
 | 
					// @Summary      记录病猪死亡事件
 | 
				
			||||||
 | 
					// @Description  记录猪批次中病猪死亡的数量、治疗地点和发生时间
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.RecordSickPigDeathRequest true "记录病猪死亡请求信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "记录成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/record-sick-pig-death [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) RecordSickPigDeath(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "记录病猪死亡事件"
 | 
				
			||||||
 | 
						var req dto.RecordSickPigDeathRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigDeathRequest) error {
 | 
				
			||||||
 | 
								return c.service.RecordSickPigDeath(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"记录成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigCull godoc
 | 
				
			||||||
 | 
					// @Summary      记录病猪淘汰事件
 | 
				
			||||||
 | 
					// @Description  记录猪批次中病猪淘汰的数量、治疗地点和发生时间
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.RecordSickPigCullRequest true "记录病猪淘汰请求信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "记录成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/record-sick-pig-cull [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) RecordSickPigCull(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "记录病猪淘汰事件"
 | 
				
			||||||
 | 
						var req dto.RecordSickPigCullRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigCullRequest) error {
 | 
				
			||||||
 | 
								return c.service.RecordSickPigCull(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"记录成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordDeath godoc
 | 
				
			||||||
 | 
					// @Summary      记录正常猪只死亡事件
 | 
				
			||||||
 | 
					// @Description  记录猪批次中正常猪只死亡的数量和发生时间
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.RecordDeathRequest true "记录正常猪只死亡请求信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "记录成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/record-death [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) RecordDeath(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "记录正常猪只死亡事件"
 | 
				
			||||||
 | 
						var req dto.RecordDeathRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordDeathRequest) error {
 | 
				
			||||||
 | 
								return c.service.RecordDeath(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"记录成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordCull godoc
 | 
				
			||||||
 | 
					// @Summary      记录正常猪只淘汰事件
 | 
				
			||||||
 | 
					// @Description  记录猪批次中正常猪只淘汰的数量和发生时间
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.RecordCullRequest true "记录正常猪只淘汰请求信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "记录成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/record-cull [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) RecordCull(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "记录正常猪只淘汰事件"
 | 
				
			||||||
 | 
						var req dto.RecordCullRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordCullRequest) error {
 | 
				
			||||||
 | 
								return c.service.RecordCull(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"记录成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					package management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SellPigs godoc
 | 
				
			||||||
 | 
					// @Summary      处理卖猪的业务逻辑
 | 
				
			||||||
 | 
					// @Description  记录猪批次中的猪只出售事件
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.SellPigsRequest true "卖猪请求信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "卖猪成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/sell-pigs [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) SellPigs(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "卖猪"
 | 
				
			||||||
 | 
						var req dto.SellPigsRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.SellPigsRequest) error {
 | 
				
			||||||
 | 
								return c.service.SellPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"卖猪成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuyPigs godoc
 | 
				
			||||||
 | 
					// @Summary      处理买猪的业务逻辑
 | 
				
			||||||
 | 
					// @Description  记录猪批次中的猪只购买事件
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.BuyPigsRequest true "买猪请求信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "买猪成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/buy-pigs [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) BuyPigs(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "买猪"
 | 
				
			||||||
 | 
						var req dto.BuyPigsRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.BuyPigsRequest) error {
 | 
				
			||||||
 | 
								return c.service.BuyPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"买猪成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					package management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TransferPigsAcrossBatches godoc
 | 
				
			||||||
 | 
					// @Summary      跨猪群调栏
 | 
				
			||||||
 | 
					// @Description  将指定数量的猪只从一个猪群的猪栏调动到另一个猪群的猪栏
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        sourceBatchID path int true "源猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.TransferPigsAcrossBatchesRequest true "跨群调栏请求信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "调栏成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{sourceBatchID}/transfer-across-batches [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) TransferPigsAcrossBatches(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "跨猪群调栏"
 | 
				
			||||||
 | 
						var req dto.TransferPigsAcrossBatchesRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.TransferPigsAcrossBatchesRequest) error {
 | 
				
			||||||
 | 
								// primaryID 在这里是 sourceBatchID
 | 
				
			||||||
 | 
								return c.service.TransferPigsAcrossBatches(primaryID, req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"调栏成功",
 | 
				
			||||||
 | 
							func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":sourceBatchID" 路径参数提取
 | 
				
			||||||
 | 
								idParam := ctx.Param("sourceBatchID")
 | 
				
			||||||
 | 
								parsedID, err := strconv.ParseUint(idParam, 10, 32)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return 0, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return uint(parsedID), nil
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TransferPigsWithinBatch godoc
 | 
				
			||||||
 | 
					// @Summary      群内调栏
 | 
				
			||||||
 | 
					// @Description  将指定数量的猪只在同一个猪群的不同猪栏间调动
 | 
				
			||||||
 | 
					// @Tags         猪群管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪批次ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.TransferPigsWithinBatchRequest true "群内调栏请求信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "调栏成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-batches/{id}/transfer-within-batch [post]
 | 
				
			||||||
 | 
					func (c *PigBatchController) TransferPigsWithinBatch(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "群内调栏"
 | 
				
			||||||
 | 
						var req dto.TransferPigsWithinBatchRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleAPIRequest(
 | 
				
			||||||
 | 
							c, ctx, action, &req,
 | 
				
			||||||
 | 
							func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.TransferPigsWithinBatchRequest) error {
 | 
				
			||||||
 | 
								// primaryID 在这里是 batchID
 | 
				
			||||||
 | 
								return c.service.TransferPigsWithinBatch(primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"调栏成功",
 | 
				
			||||||
 | 
							nil, // 默认从 ":id" 路径参数提取ID
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								internal/app/controller/management/pig_controller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								internal/app/controller/management/pig_controller.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					package management
 | 
				
			||||||
							
								
								
									
										443
									
								
								internal/app/controller/management/pig_farm_controller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										443
									
								
								internal/app/controller/management/pig_farm_controller.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,443 @@
 | 
				
			|||||||
 | 
					package management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- 控制器定义 ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigFarmController 负责处理猪舍和猪栏相关的API请求
 | 
				
			||||||
 | 
					type PigFarmController struct {
 | 
				
			||||||
 | 
						logger  *logs.Logger
 | 
				
			||||||
 | 
						service service.PigFarmService
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewPigFarmController 创建一个新的 PigFarmController 实例
 | 
				
			||||||
 | 
					func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) *PigFarmController {
 | 
				
			||||||
 | 
						return &PigFarmController{
 | 
				
			||||||
 | 
							logger:  logger,
 | 
				
			||||||
 | 
							service: service,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- 猪舍 (PigHouse) API 实现 ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigHouse godoc
 | 
				
			||||||
 | 
					// @Summary      创建猪舍
 | 
				
			||||||
 | 
					// @Description  创建一个新的猪舍
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        body body dto.CreatePigHouseRequest true "猪舍信息"
 | 
				
			||||||
 | 
					// @Success      201 {object} controller.Response{data=dto.PigHouseResponse} "创建成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-houses [post]
 | 
				
			||||||
 | 
					func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "创建猪舍"
 | 
				
			||||||
 | 
						var req dto.CreatePigHouseRequest
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						house, err := c.service.CreatePigHouse(req.Name, req.Description)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪舍失败", action, "业务逻辑失败", req)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp := dto.PigHouseResponse{
 | 
				
			||||||
 | 
							ID:          house.ID,
 | 
				
			||||||
 | 
							Name:        house.Name,
 | 
				
			||||||
 | 
							Description: house.Description,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPigHouse godoc
 | 
				
			||||||
 | 
					// @Summary      获取单个猪舍
 | 
				
			||||||
 | 
					// @Description  根据ID获取单个猪舍信息
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪舍ID"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response{data=dto.PigHouseResponse} "获取成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-houses/{id} [get]
 | 
				
			||||||
 | 
					func (c *PigFarmController) GetPigHouse(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "获取猪舍"
 | 
				
			||||||
 | 
						id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						house, err := c.service.GetPigHouseByID(uint(id))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, service.ErrHouseNotFound) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪舍失败", action, "业务逻辑失败", id)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp := dto.PigHouseResponse{
 | 
				
			||||||
 | 
							ID:          house.ID,
 | 
				
			||||||
 | 
							Name:        house.Name,
 | 
				
			||||||
 | 
							Description: house.Description,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPigHouses godoc
 | 
				
			||||||
 | 
					// @Summary      获取猪舍列表
 | 
				
			||||||
 | 
					// @Description  获取所有猪舍的列表
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response{data=[]dto.PigHouseResponse} "获取成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-houses [get]
 | 
				
			||||||
 | 
					func (c *PigFarmController) ListPigHouses(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "获取猪舍列表"
 | 
				
			||||||
 | 
						houses, err := c.service.ListPigHouses()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var resp []dto.PigHouseResponse
 | 
				
			||||||
 | 
						for _, house := range houses {
 | 
				
			||||||
 | 
							resp = append(resp, dto.PigHouseResponse{
 | 
				
			||||||
 | 
								ID:          house.ID,
 | 
				
			||||||
 | 
								Name:        house.Name,
 | 
				
			||||||
 | 
								Description: house.Description,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePigHouse godoc
 | 
				
			||||||
 | 
					// @Summary      更新猪舍
 | 
				
			||||||
 | 
					// @Description  更新一个已存在的猪舍信息
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪舍ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.UpdatePigHouseRequest true "猪舍信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response{data=dto.PigHouseResponse} "更新成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-houses/{id} [put]
 | 
				
			||||||
 | 
					func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "更新猪舍"
 | 
				
			||||||
 | 
						id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var req dto.UpdatePigHouseRequest
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						house, err := c.service.UpdatePigHouse(uint(id), req.Name, req.Description)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, service.ErrHouseNotFound) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp := dto.PigHouseResponse{
 | 
				
			||||||
 | 
							ID:          house.ID,
 | 
				
			||||||
 | 
							Name:        house.Name,
 | 
				
			||||||
 | 
							Description: house.Description,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeletePigHouse godoc
 | 
				
			||||||
 | 
					// @Summary      删除猪舍
 | 
				
			||||||
 | 
					// @Description  根据ID删除一个猪舍
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪舍ID"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "删除成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pig-houses/{id} [delete]
 | 
				
			||||||
 | 
					func (c *PigFarmController) DeletePigHouse(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "删除猪舍"
 | 
				
			||||||
 | 
						id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := c.service.DeletePigHouse(uint(id)); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, service.ErrHouseNotFound) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// 检查是否是业务逻辑错误
 | 
				
			||||||
 | 
							if errors.Is(err, service.ErrHouseContainsPens) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- 猪栏 (Pen) API 实现 ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePen godoc
 | 
				
			||||||
 | 
					// @Summary      创建猪栏
 | 
				
			||||||
 | 
					// @Description  创建一个新的猪栏
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        body body dto.CreatePenRequest true "猪栏信息"
 | 
				
			||||||
 | 
					// @Success      201 {object} controller.Response{data=dto.PenResponse} "创建成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pens [post]
 | 
				
			||||||
 | 
					func (c *PigFarmController) CreatePen(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "创建猪栏"
 | 
				
			||||||
 | 
						var req dto.CreatePenRequest
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pen, err := c.service.CreatePen(req.PenNumber, req.HouseID, req.Capacity)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							// 检查是否是业务逻辑错误
 | 
				
			||||||
 | 
							if errors.Is(err, service.ErrHouseNotFound) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), req)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp := dto.PenResponse{
 | 
				
			||||||
 | 
							ID:         pen.ID,
 | 
				
			||||||
 | 
							PenNumber:  pen.PenNumber,
 | 
				
			||||||
 | 
							HouseID:    pen.HouseID,
 | 
				
			||||||
 | 
							Capacity:   pen.Capacity,
 | 
				
			||||||
 | 
							Status:     pen.Status,
 | 
				
			||||||
 | 
							PigBatchID: *pen.PigBatchID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPen godoc
 | 
				
			||||||
 | 
					// @Summary      获取单个猪栏
 | 
				
			||||||
 | 
					// @Description  根据ID获取单个猪栏信息
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪栏ID"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response{data=dto.PenResponse} "获取成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pens/{id} [get]
 | 
				
			||||||
 | 
					func (c *PigFarmController) GetPen(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "获取猪栏"
 | 
				
			||||||
 | 
						id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pen, err := c.service.GetPenByID(uint(id))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, service.ErrPenNotFound) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪栏失败", action, "业务逻辑失败", id)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp := dto.PenResponse{
 | 
				
			||||||
 | 
							ID:         pen.ID,
 | 
				
			||||||
 | 
							PenNumber:  pen.PenNumber,
 | 
				
			||||||
 | 
							HouseID:    pen.HouseID,
 | 
				
			||||||
 | 
							Capacity:   pen.Capacity,
 | 
				
			||||||
 | 
							Status:     pen.Status,
 | 
				
			||||||
 | 
							PigBatchID: *pen.PigBatchID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPens godoc
 | 
				
			||||||
 | 
					// @Summary      获取猪栏列表
 | 
				
			||||||
 | 
					// @Description  获取所有猪栏的列表
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response{data=[]dto.PenResponse} "获取成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pens [get]
 | 
				
			||||||
 | 
					func (c *PigFarmController) ListPens(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "获取猪栏列表"
 | 
				
			||||||
 | 
						pens, err := c.service.ListPens()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var resp []dto.PenResponse
 | 
				
			||||||
 | 
						for _, pen := range pens {
 | 
				
			||||||
 | 
							resp = append(resp, dto.PenResponse{
 | 
				
			||||||
 | 
								ID:         pen.ID,
 | 
				
			||||||
 | 
								PenNumber:  pen.PenNumber,
 | 
				
			||||||
 | 
								HouseID:    pen.HouseID,
 | 
				
			||||||
 | 
								Capacity:   pen.Capacity,
 | 
				
			||||||
 | 
								Status:     pen.Status,
 | 
				
			||||||
 | 
								PigBatchID: *pen.PigBatchID,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePen godoc
 | 
				
			||||||
 | 
					// @Summary      更新猪栏
 | 
				
			||||||
 | 
					// @Description  更新一个已存在的猪栏信息
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪栏ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.UpdatePenRequest true "猪栏信息"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response{data=dto.PenResponse} "更新成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pens/{id} [put]
 | 
				
			||||||
 | 
					func (c *PigFarmController) UpdatePen(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "更新猪栏"
 | 
				
			||||||
 | 
						id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var req dto.UpdatePenRequest
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pen, err := c.service.UpdatePen(uint(id), req.PenNumber, req.HouseID, req.Capacity, req.Status)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, service.ErrPenNotFound) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// 其他业务逻辑错误可以在这里添加处理
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp := dto.PenResponse{
 | 
				
			||||||
 | 
							ID:         pen.ID,
 | 
				
			||||||
 | 
							PenNumber:  pen.PenNumber,
 | 
				
			||||||
 | 
							HouseID:    pen.HouseID,
 | 
				
			||||||
 | 
							Capacity:   pen.Capacity,
 | 
				
			||||||
 | 
							Status:     pen.Status,
 | 
				
			||||||
 | 
							PigBatchID: *pen.PigBatchID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeletePen godoc
 | 
				
			||||||
 | 
					// @Summary      删除猪栏
 | 
				
			||||||
 | 
					// @Description  根据ID删除一个猪栏
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪栏ID"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response "删除成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pens/{id} [delete]
 | 
				
			||||||
 | 
					func (c *PigFarmController) DeletePen(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "删除猪栏"
 | 
				
			||||||
 | 
						id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := c.service.DeletePen(uint(id)); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, service.ErrPenNotFound) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// 检查是否是业务逻辑错误
 | 
				
			||||||
 | 
							if errors.Is(err, service.ErrPenInUse) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePenStatus godoc
 | 
				
			||||||
 | 
					// @Summary      更新猪栏状态
 | 
				
			||||||
 | 
					// @Description  更新指定猪栏的当前状态
 | 
				
			||||||
 | 
					// @Tags         猪场管理
 | 
				
			||||||
 | 
					// @Accept       json
 | 
				
			||||||
 | 
					// @Produce      json
 | 
				
			||||||
 | 
					// @Param        id path int true "猪栏ID"
 | 
				
			||||||
 | 
					// @Param        body body dto.UpdatePenStatusRequest true "新的猪栏状态"
 | 
				
			||||||
 | 
					// @Success      200 {object} controller.Response{data=dto.PenResponse} "更新成功"
 | 
				
			||||||
 | 
					// @Router       /api/v1/pens/{id}/status [put]
 | 
				
			||||||
 | 
					func (c *PigFarmController) UpdatePenStatus(ctx *gin.Context) {
 | 
				
			||||||
 | 
						const action = "更新猪栏状态"
 | 
				
			||||||
 | 
						id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var req dto.UpdatePenStatusRequest
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pen, err := c.service.UpdatePenStatus(uint(id), req.Status)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, service.ErrPenNotFound) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if errors.Is(err, service.ErrPenStatusInvalidForOccupiedPen) || errors.Is(err, service.ErrPenStatusInvalidForUnoccupiedPen) {
 | 
				
			||||||
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
				
			||||||
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪栏状态失败", action, err.Error(), id)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp := dto.PenResponse{
 | 
				
			||||||
 | 
							ID:         pen.ID,
 | 
				
			||||||
 | 
							PenNumber:  pen.PenNumber,
 | 
				
			||||||
 | 
							HouseID:    pen.HouseID,
 | 
				
			||||||
 | 
							Capacity:   pen.Capacity,
 | 
				
			||||||
 | 
							Status:     pen.Status,
 | 
				
			||||||
 | 
							PigBatchID: *pen.PigBatchID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,459 +0,0 @@
 | 
				
			|||||||
package plan_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
					 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan"
 | 
					 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
					 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
					 | 
				
			||||||
	"gorm.io/datatypes"
 | 
					 | 
				
			||||||
	"gorm.io/gorm"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestPlanToResponse(t *testing.T) {
 | 
					 | 
				
			||||||
	t.Run("nil plan", func(t *testing.T) {
 | 
					 | 
				
			||||||
		response := plan.PlanToResponse(nil)
 | 
					 | 
				
			||||||
		assert.Nil(t, response)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("basic plan without associations", func(t *testing.T) {
 | 
					 | 
				
			||||||
		planModel := &models.Plan{
 | 
					 | 
				
			||||||
			Model:          gorm.Model{ID: 1},
 | 
					 | 
				
			||||||
			Name:           "Test Plan",
 | 
					 | 
				
			||||||
			Description:    "A test plan",
 | 
					 | 
				
			||||||
			ExecutionType:  models.PlanExecutionTypeAutomatic,
 | 
					 | 
				
			||||||
			CronExpression: "0 0 * * *",
 | 
					 | 
				
			||||||
			ContentType:    models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		response := plan.PlanToResponse(planModel)
 | 
					 | 
				
			||||||
		assert.NotNil(t, response)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(1), response.ID)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Test Plan", response.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, "A test plan", response.Description)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanExecutionTypeAutomatic, response.ExecutionType)
 | 
					 | 
				
			||||||
		assert.Equal(t, "0 0 * * *", response.CronExpression)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanContentTypeTasks, response.ContentType)
 | 
					 | 
				
			||||||
		assert.Empty(t, response.SubPlans)
 | 
					 | 
				
			||||||
		assert.Empty(t, response.Tasks)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("plan with sub plans", func(t *testing.T) {
 | 
					 | 
				
			||||||
		childPlan := &models.Plan{
 | 
					 | 
				
			||||||
			Model:       gorm.Model{ID: 2},
 | 
					 | 
				
			||||||
			Name:        "Child Plan",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel := &models.Plan{
 | 
					 | 
				
			||||||
			Model:       gorm.Model{ID: 1},
 | 
					 | 
				
			||||||
			Name:        "Parent Plan",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeSubPlans,
 | 
					 | 
				
			||||||
			SubPlans: []models.SubPlan{
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Model:          gorm.Model{ID: 10},
 | 
					 | 
				
			||||||
					ParentPlanID:   1,
 | 
					 | 
				
			||||||
					ChildPlanID:    2,
 | 
					 | 
				
			||||||
					ExecutionOrder: 1,
 | 
					 | 
				
			||||||
					ChildPlan:      childPlan,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		response := plan.PlanToResponse(planModel)
 | 
					 | 
				
			||||||
		assert.NotNil(t, response)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(1), response.ID)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Parent Plan", response.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanContentTypeSubPlans, response.ContentType)
 | 
					 | 
				
			||||||
		assert.Len(t, response.SubPlans, 1)
 | 
					 | 
				
			||||||
		assert.Empty(t, response.Tasks)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		subPlanResp := response.SubPlans[0]
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(10), subPlanResp.ID)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(1), subPlanResp.ParentPlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(2), subPlanResp.ChildPlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, subPlanResp.ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.NotNil(t, subPlanResp.ChildPlan)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Child Plan", subPlanResp.ChildPlan.Name)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("plan with tasks", func(t *testing.T) {
 | 
					 | 
				
			||||||
		params := datatypes.JSON([]byte(`{"device_id": 1, "value": 25}`))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel := &models.Plan{
 | 
					 | 
				
			||||||
			Model:       gorm.Model{ID: 1},
 | 
					 | 
				
			||||||
			Name:        "Task Plan",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
			Tasks: []models.Task{
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Model:          gorm.Model{ID: 10},
 | 
					 | 
				
			||||||
					PlanID:         1,
 | 
					 | 
				
			||||||
					Name:           "Task 1",
 | 
					 | 
				
			||||||
					Description:    "First task",
 | 
					 | 
				
			||||||
					ExecutionOrder: 1,
 | 
					 | 
				
			||||||
					Type:           models.TaskTypeWaiting,
 | 
					 | 
				
			||||||
					Parameters:     params,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		response := plan.PlanToResponse(planModel)
 | 
					 | 
				
			||||||
		assert.NotNil(t, response)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(1), response.ID)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Task Plan", response.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanContentTypeTasks, response.ContentType)
 | 
					 | 
				
			||||||
		assert.Len(t, response.Tasks, 1)
 | 
					 | 
				
			||||||
		assert.Empty(t, response.SubPlans)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		taskResp := response.Tasks[0]
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(10), taskResp.ID)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(1), taskResp.PlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Task 1", taskResp.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, "First task", taskResp.Description)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, taskResp.ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.TaskTypeWaiting, taskResp.Type)
 | 
					 | 
				
			||||||
		assert.Equal(t, controller.Properties(params), taskResp.Parameters)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestPlanFromCreateRequest(t *testing.T) {
 | 
					 | 
				
			||||||
	t.Run("nil request", func(t *testing.T) {
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromCreateRequest(nil)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.Nil(t, planModel)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("basic plan without associations", func(t *testing.T) {
 | 
					 | 
				
			||||||
		req := &plan.CreatePlanRequest{
 | 
					 | 
				
			||||||
			Name:           "Test Plan",
 | 
					 | 
				
			||||||
			Description:    "A test plan",
 | 
					 | 
				
			||||||
			ExecutionType:  models.PlanExecutionTypeAutomatic,
 | 
					 | 
				
			||||||
			CronExpression: "0 0 * * *",
 | 
					 | 
				
			||||||
			ContentType:    models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromCreateRequest(req)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.NotNil(t, planModel)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Test Plan", planModel.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, "A test plan", planModel.Description)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanExecutionTypeAutomatic, planModel.ExecutionType)
 | 
					 | 
				
			||||||
		assert.Equal(t, "0 0 * * *", planModel.CronExpression)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
					 | 
				
			||||||
		assert.Empty(t, planModel.SubPlans)
 | 
					 | 
				
			||||||
		assert.Empty(t, planModel.Tasks)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("plan with sub plan IDs", func(t *testing.T) {
 | 
					 | 
				
			||||||
		req := &plan.CreatePlanRequest{
 | 
					 | 
				
			||||||
			Name:        "Parent Plan",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeSubPlans,
 | 
					 | 
				
			||||||
			SubPlanIDs:  []uint{2, 3},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromCreateRequest(req)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.NotNil(t, planModel)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Parent Plan", planModel.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanContentTypeSubPlans, planModel.ContentType)
 | 
					 | 
				
			||||||
		assert.Len(t, planModel.SubPlans, 2)
 | 
					 | 
				
			||||||
		assert.Empty(t, planModel.Tasks)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(2), planModel.SubPlans[0].ChildPlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, planModel.SubPlans[0].ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(3), planModel.SubPlans[1].ChildPlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, 2, planModel.SubPlans[1].ExecutionOrder)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("plan with tasks", func(t *testing.T) {
 | 
					 | 
				
			||||||
		params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		req := &plan.CreatePlanRequest{
 | 
					 | 
				
			||||||
			Name:        "Task Plan",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
			Tasks: []plan.TaskRequest{
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Name:           "Task 1",
 | 
					 | 
				
			||||||
					Description:    "First task",
 | 
					 | 
				
			||||||
					ExecutionOrder: 1,
 | 
					 | 
				
			||||||
					Type:           models.TaskTypeWaiting,
 | 
					 | 
				
			||||||
					Parameters:     params,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromCreateRequest(req)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.NotNil(t, planModel)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Task Plan", planModel.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
					 | 
				
			||||||
		assert.Len(t, planModel.Tasks, 1)
 | 
					 | 
				
			||||||
		assert.Empty(t, planModel.SubPlans)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		task := planModel.Tasks[0]
 | 
					 | 
				
			||||||
		assert.Equal(t, "Task 1", task.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, "First task", task.Description)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, task.ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.TaskTypeWaiting, task.Type)
 | 
					 | 
				
			||||||
		assert.Equal(t, datatypes.JSON(params), task.Parameters)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("plan with tasks with gapped execution order", func(t *testing.T) {
 | 
					 | 
				
			||||||
		req := &plan.CreatePlanRequest{
 | 
					 | 
				
			||||||
			Name:        "Task Plan with Gaps",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
			Tasks: []plan.TaskRequest{
 | 
					 | 
				
			||||||
				{Name: "Task 3", ExecutionOrder: 5},
 | 
					 | 
				
			||||||
				{Name: "Task 1", ExecutionOrder: 2},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromCreateRequest(req)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.NotNil(t, planModel)
 | 
					 | 
				
			||||||
		assert.Len(t, planModel.Tasks, 2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// After ReorderSteps, tasks are sorted by their original ExecutionOrder and then re-numbered.
 | 
					 | 
				
			||||||
		assert.Equal(t, "Task 1", planModel.Tasks[0].Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, planModel.Tasks[0].ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Task 3", planModel.Tasks[1].Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, 2, planModel.Tasks[1].ExecutionOrder)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("plan with duplicate task execution order", func(t *testing.T) {
 | 
					 | 
				
			||||||
		req := &plan.CreatePlanRequest{
 | 
					 | 
				
			||||||
			Name:        "Invalid Plan",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
			Tasks: []plan.TaskRequest{
 | 
					 | 
				
			||||||
				{Name: "Task 1", ExecutionOrder: 1},
 | 
					 | 
				
			||||||
				{Name: "Task 2", ExecutionOrder: 1}, // Duplicate order
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromCreateRequest(req)
 | 
					 | 
				
			||||||
		assert.Error(t, err)
 | 
					 | 
				
			||||||
		assert.Contains(t, err.Error(), "任务执行顺序重复")
 | 
					 | 
				
			||||||
		assert.Nil(t, planModel)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestPlanFromUpdateRequest(t *testing.T) {
 | 
					 | 
				
			||||||
	t.Run("nil request", func(t *testing.T) {
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromUpdateRequest(nil)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.Nil(t, planModel)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("basic plan without associations", func(t *testing.T) {
 | 
					 | 
				
			||||||
		req := &plan.UpdatePlanRequest{
 | 
					 | 
				
			||||||
			Name:           "Updated Plan",
 | 
					 | 
				
			||||||
			Description:    "An updated plan",
 | 
					 | 
				
			||||||
			ExecutionType:  models.PlanExecutionTypeManual,
 | 
					 | 
				
			||||||
			CronExpression: "0 30 * * *",
 | 
					 | 
				
			||||||
			ContentType:    models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromUpdateRequest(req)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.NotNil(t, planModel)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Updated Plan", planModel.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, "An updated plan", planModel.Description)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanExecutionTypeManual, planModel.ExecutionType)
 | 
					 | 
				
			||||||
		assert.Equal(t, "0 30 * * *", planModel.CronExpression)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
					 | 
				
			||||||
		assert.Empty(t, planModel.SubPlans)
 | 
					 | 
				
			||||||
		assert.Empty(t, planModel.Tasks)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("plan with sub plan IDs", func(t *testing.T) {
 | 
					 | 
				
			||||||
		req := &plan.UpdatePlanRequest{
 | 
					 | 
				
			||||||
			Name:        "Updated Parent Plan",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeSubPlans,
 | 
					 | 
				
			||||||
			SubPlanIDs:  []uint{2, 3},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromUpdateRequest(req)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.NotNil(t, planModel)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		assert.Equal(t, "Updated Parent Plan", planModel.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanContentTypeSubPlans, planModel.ContentType)
 | 
					 | 
				
			||||||
		assert.Len(t, planModel.SubPlans, 2)
 | 
					 | 
				
			||||||
		assert.Empty(t, planModel.Tasks)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(2), planModel.SubPlans[0].ChildPlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, planModel.SubPlans[0].ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(3), planModel.SubPlans[1].ChildPlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, 2, planModel.SubPlans[1].ExecutionOrder)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("plan with tasks", func(t *testing.T) {
 | 
					 | 
				
			||||||
		params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		req := &plan.UpdatePlanRequest{
 | 
					 | 
				
			||||||
			Name:        "Updated Task Plan",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
			Tasks: []plan.TaskRequest{
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Name:           "Task 1",
 | 
					 | 
				
			||||||
					Description:    "First task",
 | 
					 | 
				
			||||||
					ExecutionOrder: 1,
 | 
					 | 
				
			||||||
					Type:           models.TaskTypeWaiting,
 | 
					 | 
				
			||||||
					Parameters:     params,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromUpdateRequest(req)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.NotNil(t, planModel)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Updated Task Plan", planModel.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
					 | 
				
			||||||
		assert.Len(t, planModel.Tasks, 1)
 | 
					 | 
				
			||||||
		assert.Empty(t, planModel.SubPlans)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		task := planModel.Tasks[0]
 | 
					 | 
				
			||||||
		assert.Equal(t, "Task 1", task.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, task.ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.Equal(t, datatypes.JSON(params), task.Parameters)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("plan with duplicate task execution order", func(t *testing.T) {
 | 
					 | 
				
			||||||
		req := &plan.UpdatePlanRequest{
 | 
					 | 
				
			||||||
			Name:        "Invalid Updated Plan",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
			Tasks: []plan.TaskRequest{
 | 
					 | 
				
			||||||
				{Name: "Task 1", ExecutionOrder: 1},
 | 
					 | 
				
			||||||
				{Name: "Task 2", ExecutionOrder: 1}, // Duplicate order
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromUpdateRequest(req)
 | 
					 | 
				
			||||||
		assert.Error(t, err)
 | 
					 | 
				
			||||||
		assert.Contains(t, err.Error(), "任务执行顺序重复")
 | 
					 | 
				
			||||||
		assert.Nil(t, planModel)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("plan with tasks with gapped execution order", func(t *testing.T) {
 | 
					 | 
				
			||||||
		req := &plan.UpdatePlanRequest{
 | 
					 | 
				
			||||||
			Name:        "Updated Task Plan with Gaps",
 | 
					 | 
				
			||||||
			ContentType: models.PlanContentTypeTasks,
 | 
					 | 
				
			||||||
			Tasks: []plan.TaskRequest{
 | 
					 | 
				
			||||||
				{Name: "Task 3", ExecutionOrder: 5},
 | 
					 | 
				
			||||||
				{Name: "Task 1", ExecutionOrder: 2},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		planModel, err := plan.PlanFromUpdateRequest(req)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.NotNil(t, planModel)
 | 
					 | 
				
			||||||
		assert.Len(t, planModel.Tasks, 2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// After ReorderSteps, tasks are sorted by their original ExecutionOrder and then re-numbered.
 | 
					 | 
				
			||||||
		assert.Equal(t, "Task 1", planModel.Tasks[0].Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, planModel.Tasks[0].ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Task 3", planModel.Tasks[1].Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, 2, planModel.Tasks[1].ExecutionOrder)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestSubPlanToResponse(t *testing.T) {
 | 
					 | 
				
			||||||
	t.Run("nil sub plan", func(t *testing.T) {
 | 
					 | 
				
			||||||
		response := plan.SubPlanToResponse(nil)
 | 
					 | 
				
			||||||
		assert.Equal(t, plan.SubPlanResponse{}, response)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("sub plan without child plan", func(t *testing.T) {
 | 
					 | 
				
			||||||
		subPlan := &models.SubPlan{
 | 
					 | 
				
			||||||
			Model:          gorm.Model{ID: 10},
 | 
					 | 
				
			||||||
			ParentPlanID:   1,
 | 
					 | 
				
			||||||
			ChildPlanID:    2,
 | 
					 | 
				
			||||||
			ExecutionOrder: 1,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		response := plan.SubPlanToResponse(subPlan)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(10), response.ID)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(1), response.ParentPlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(2), response.ChildPlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, response.ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.Nil(t, response.ChildPlan)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("sub plan with child plan", func(t *testing.T) {
 | 
					 | 
				
			||||||
		childPlan := &models.Plan{
 | 
					 | 
				
			||||||
			Model: gorm.Model{ID: 2},
 | 
					 | 
				
			||||||
			Name:  "Child Plan",
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		subPlan := &models.SubPlan{
 | 
					 | 
				
			||||||
			Model:          gorm.Model{ID: 10},
 | 
					 | 
				
			||||||
			ParentPlanID:   1,
 | 
					 | 
				
			||||||
			ChildPlanID:    2,
 | 
					 | 
				
			||||||
			ExecutionOrder: 1,
 | 
					 | 
				
			||||||
			ChildPlan:      childPlan,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		response := plan.SubPlanToResponse(subPlan)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(10), response.ID)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(1), response.ParentPlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(2), response.ChildPlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, response.ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.NotNil(t, response.ChildPlan)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Child Plan", response.ChildPlan.Name)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestTaskToResponse(t *testing.T) {
 | 
					 | 
				
			||||||
	t.Run("nil task", func(t *testing.T) {
 | 
					 | 
				
			||||||
		response := plan.TaskToResponse(nil)
 | 
					 | 
				
			||||||
		assert.Equal(t, plan.TaskResponse{}, response)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("task with parameters", func(t *testing.T) {
 | 
					 | 
				
			||||||
		params := datatypes.JSON([]byte(`{"device_id": 1, "value": 25}`))
 | 
					 | 
				
			||||||
		task := &models.Task{
 | 
					 | 
				
			||||||
			Model:          gorm.Model{ID: 10},
 | 
					 | 
				
			||||||
			PlanID:         1,
 | 
					 | 
				
			||||||
			Name:           "Test Task",
 | 
					 | 
				
			||||||
			Description:    "A test task",
 | 
					 | 
				
			||||||
			ExecutionOrder: 1,
 | 
					 | 
				
			||||||
			Type:           models.TaskTypeWaiting,
 | 
					 | 
				
			||||||
			Parameters:     params,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		response := plan.TaskToResponse(task)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(10), response.ID)
 | 
					 | 
				
			||||||
		assert.Equal(t, uint(1), response.PlanID)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Test Task", response.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, "A test task", response.Description)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, response.ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.TaskTypeWaiting, response.Type)
 | 
					 | 
				
			||||||
		assert.Equal(t, controller.Properties(params), response.Parameters)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestTaskFromRequest(t *testing.T) {
 | 
					 | 
				
			||||||
	t.Run("nil request", func(t *testing.T) {
 | 
					 | 
				
			||||||
		task := plan.TaskFromRequest(nil)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.Task{}, task)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("task with parameters", func(t *testing.T) {
 | 
					 | 
				
			||||||
		params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`))
 | 
					 | 
				
			||||||
		req := &plan.TaskRequest{
 | 
					 | 
				
			||||||
			Name:           "Test Task",
 | 
					 | 
				
			||||||
			Description:    "A test task",
 | 
					 | 
				
			||||||
			ExecutionOrder: 1,
 | 
					 | 
				
			||||||
			Type:           models.TaskTypeWaiting,
 | 
					 | 
				
			||||||
			Parameters:     params,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		task := plan.TaskFromRequest(req)
 | 
					 | 
				
			||||||
		assert.Equal(t, "Test Task", task.Name)
 | 
					 | 
				
			||||||
		assert.Equal(t, "A test task", task.Description)
 | 
					 | 
				
			||||||
		assert.Equal(t, 1, task.ExecutionOrder)
 | 
					 | 
				
			||||||
		assert.Equal(t, models.TaskTypeWaiting, task.Type)
 | 
					 | 
				
			||||||
		assert.Equal(t, datatypes.JSON(params), task.Parameters)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -5,7 +5,8 @@ import (
 | 
				
			|||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
				
			||||||
	task "git.huangwc.com/pig/pig-farm-controller/internal/app/service/task"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
@@ -13,80 +14,6 @@ import (
 | 
				
			|||||||
	"gorm.io/gorm"
 | 
						"gorm.io/gorm"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- 请求和响应 DTO 定义 ---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// CreatePlanRequest 定义创建计划请求的结构体
 | 
					 | 
				
			||||||
type CreatePlanRequest struct {
 | 
					 | 
				
			||||||
	Name           string                   `json:"name" binding:"required" example:"猪舍温度控制计划"`
 | 
					 | 
				
			||||||
	Description    string                   `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
					 | 
				
			||||||
	ExecutionType  models.PlanExecutionType `json:"execution_type" binding:"required" example:"automatic"`
 | 
					 | 
				
			||||||
	ExecuteNum     uint                     `json:"execute_num,omitempty" example:"10"`
 | 
					 | 
				
			||||||
	CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
					 | 
				
			||||||
	SubPlanIDs     []uint                   `json:"sub_plan_ids,omitempty"`
 | 
					 | 
				
			||||||
	Tasks          []TaskRequest            `json:"tasks,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// PlanResponse 定义计划详情响应的结构体
 | 
					 | 
				
			||||||
type PlanResponse struct {
 | 
					 | 
				
			||||||
	ID             uint                     `json:"id" example:"1"`
 | 
					 | 
				
			||||||
	Name           string                   `json:"name" example:"猪舍温度控制计划"`
 | 
					 | 
				
			||||||
	Description    string                   `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
					 | 
				
			||||||
	ExecutionType  models.PlanExecutionType `json:"execution_type" example:"automatic"`
 | 
					 | 
				
			||||||
	Status         models.PlanStatus        `json:"status" example:"0"`
 | 
					 | 
				
			||||||
	ExecuteNum     uint                     `json:"execute_num" example:"10"`
 | 
					 | 
				
			||||||
	ExecuteCount   uint                     `json:"execute_count" example:"0"`
 | 
					 | 
				
			||||||
	CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
					 | 
				
			||||||
	ContentType    models.PlanContentType   `json:"content_type" example:"tasks"`
 | 
					 | 
				
			||||||
	SubPlans       []SubPlanResponse        `json:"sub_plans,omitempty"`
 | 
					 | 
				
			||||||
	Tasks          []TaskResponse           `json:"tasks,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ListPlansResponse 定义获取计划列表响应的结构体
 | 
					 | 
				
			||||||
type ListPlansResponse struct {
 | 
					 | 
				
			||||||
	Plans []PlanResponse `json:"plans"`
 | 
					 | 
				
			||||||
	Total int            `json:"total" example:"100"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// UpdatePlanRequest 定义更新计划请求的结构体
 | 
					 | 
				
			||||||
type UpdatePlanRequest struct {
 | 
					 | 
				
			||||||
	Name           string                   `json:"name" example:"猪舍温度控制计划V2"`
 | 
					 | 
				
			||||||
	Description    string                   `json:"description" example:"更新后的描述"`
 | 
					 | 
				
			||||||
	ExecutionType  models.PlanExecutionType `json:"execution_type" example:"automatic"`
 | 
					 | 
				
			||||||
	ExecuteNum     uint                     `json:"execute_num,omitempty" example:"10"`
 | 
					 | 
				
			||||||
	CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
					 | 
				
			||||||
	SubPlanIDs     []uint                   `json:"sub_plan_ids,omitempty"`
 | 
					 | 
				
			||||||
	Tasks          []TaskRequest            `json:"tasks,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SubPlanResponse 定义子计划响应结构体
 | 
					 | 
				
			||||||
type SubPlanResponse struct {
 | 
					 | 
				
			||||||
	ID             uint          `json:"id" example:"1"`
 | 
					 | 
				
			||||||
	ParentPlanID   uint          `json:"parent_plan_id" example:"1"`
 | 
					 | 
				
			||||||
	ChildPlanID    uint          `json:"child_plan_id" example:"2"`
 | 
					 | 
				
			||||||
	ExecutionOrder int           `json:"execution_order" example:"1"`
 | 
					 | 
				
			||||||
	ChildPlan      *PlanResponse `json:"child_plan,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TaskRequest 定义任务请求结构体
 | 
					 | 
				
			||||||
type TaskRequest struct {
 | 
					 | 
				
			||||||
	Name           string                 `json:"name" example:"打开风扇"`
 | 
					 | 
				
			||||||
	Description    string                 `json:"description" example:"打开1号风扇"`
 | 
					 | 
				
			||||||
	ExecutionOrder int                    `json:"execution_order" example:"1"`
 | 
					 | 
				
			||||||
	Type           models.TaskType        `json:"type" example:"waiting"`
 | 
					 | 
				
			||||||
	Parameters     map[string]interface{} `json:"parameters,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TaskResponse 定义任务响应结构体
 | 
					 | 
				
			||||||
type TaskResponse struct {
 | 
					 | 
				
			||||||
	ID             int                    `json:"id" example:"1"`
 | 
					 | 
				
			||||||
	PlanID         uint                   `json:"plan_id" example:"1"`
 | 
					 | 
				
			||||||
	Name           string                 `json:"name" example:"打开风扇"`
 | 
					 | 
				
			||||||
	Description    string                 `json:"description" example:"打开1号风扇"`
 | 
					 | 
				
			||||||
	ExecutionOrder int                    `json:"execution_order" example:"1"`
 | 
					 | 
				
			||||||
	Type           models.TaskType        `json:"type" example:"waiting"`
 | 
					 | 
				
			||||||
	Parameters     map[string]interface{} `json:"parameters,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Controller 定义 ---
 | 
					// --- Controller 定义 ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Controller 定义了计划相关的控制器
 | 
					// Controller 定义了计划相关的控制器
 | 
				
			||||||
@@ -113,11 +40,11 @@ func NewController(logger *logs.Logger, planRepo repository.PlanRepository, anal
 | 
				
			|||||||
// @Tags         计划管理
 | 
					// @Tags         计划管理
 | 
				
			||||||
// @Accept       json
 | 
					// @Accept       json
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        plan body CreatePlanRequest true "计划信息"
 | 
					// @Param        plan body dto.CreatePlanRequest true "计划信息"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=plan.PlanResponse} "业务码为201代表创建成功"
 | 
					// @Success      200 {object} controller.Response{data=dto.PlanResponse} "业务码为201代表创建成功"
 | 
				
			||||||
// @Router       /api/v1/plans [post]
 | 
					// @Router       /api/v1/plans [post]
 | 
				
			||||||
func (c *Controller) CreatePlan(ctx *gin.Context) {
 | 
					func (c *Controller) CreatePlan(ctx *gin.Context) {
 | 
				
			||||||
	var req CreatePlanRequest
 | 
						var req dto.CreatePlanRequest
 | 
				
			||||||
	const actionType = "创建计划"
 | 
						const actionType = "创建计划"
 | 
				
			||||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
				
			||||||
@@ -126,7 +53,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 使用已有的转换函数,它已经包含了验证和重排逻辑
 | 
						// 使用已有的转换函数,它已经包含了验证和重排逻辑
 | 
				
			||||||
	planToCreate, err := PlanFromCreateRequest(&req)
 | 
						planToCreate, err := dto.NewPlanFromCreateRequest(&req)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
 | 
				
			||||||
@@ -155,7 +82,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 使用已有的转换函数将创建后的模型转换为响应对象
 | 
						// 使用已有的转换函数将创建后的模型转换为响应对象
 | 
				
			||||||
	resp, err := PlanToResponse(planToCreate)
 | 
						resp, err := dto.NewPlanToResponse(planToCreate)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败", actionType, "响应序列化失败", planToCreate)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败", actionType, "响应序列化失败", planToCreate)
 | 
				
			||||||
@@ -173,7 +100,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) {
 | 
				
			|||||||
// @Tags         计划管理
 | 
					// @Tags         计划管理
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        id path int true "计划ID"
 | 
					// @Param        id path int true "计划ID"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=plan.PlanResponse} "业务码为200代表成功获取"
 | 
					// @Success      200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表成功获取"
 | 
				
			||||||
// @Router       /api/v1/plans/{id} [get]
 | 
					// @Router       /api/v1/plans/{id} [get]
 | 
				
			||||||
func (c *Controller) GetPlan(ctx *gin.Context) {
 | 
					func (c *Controller) GetPlan(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "获取计划详情"
 | 
						const actionType = "获取计划详情"
 | 
				
			||||||
@@ -202,7 +129,7 @@ func (c *Controller) GetPlan(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 3. 将模型转换为响应 DTO
 | 
						// 3. 将模型转换为响应 DTO
 | 
				
			||||||
	resp, err := PlanToResponse(plan)
 | 
						resp, err := dto.NewPlanToResponse(plan)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误", actionType, "响应序列化失败", plan)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误", actionType, "响应序列化失败", plan)
 | 
				
			||||||
@@ -219,7 +146,7 @@ func (c *Controller) GetPlan(ctx *gin.Context) {
 | 
				
			|||||||
// @Description  获取所有计划的列表
 | 
					// @Description  获取所有计划的列表
 | 
				
			||||||
// @Tags         计划管理
 | 
					// @Tags         计划管理
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=plan.ListPlansResponse} "业务码为200代表成功获取列表"
 | 
					// @Success      200 {object} controller.Response{data=[]dto.PlanResponse} "业务码为200代表成功获取列表"
 | 
				
			||||||
// @Router       /api/v1/plans [get]
 | 
					// @Router       /api/v1/plans [get]
 | 
				
			||||||
func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
					func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "获取计划列表"
 | 
						const actionType = "获取计划列表"
 | 
				
			||||||
@@ -232,9 +159,9 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 2. 将模型转换为响应 DTO
 | 
						// 2. 将模型转换为响应 DTO
 | 
				
			||||||
	planResponses := make([]PlanResponse, 0, len(plans))
 | 
						planResponses := make([]dto.PlanResponse, 0, len(plans))
 | 
				
			||||||
	for _, p := range plans {
 | 
						for _, p := range plans {
 | 
				
			||||||
		resp, err := PlanToResponse(&p)
 | 
							resp, err := dto.NewPlanToResponse(&p)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, p)
 | 
								c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, p)
 | 
				
			||||||
			controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误", actionType, "响应序列化失败", p)
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误", actionType, "响应序列化失败", p)
 | 
				
			||||||
@@ -244,7 +171,7 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 3. 构造并发送成功响应
 | 
						// 3. 构造并发送成功响应
 | 
				
			||||||
	resp := ListPlansResponse{
 | 
						resp := dto.ListPlansResponse{
 | 
				
			||||||
		Plans: planResponses,
 | 
							Plans: planResponses,
 | 
				
			||||||
		Total: len(planResponses),
 | 
							Total: len(planResponses),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -259,8 +186,8 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
				
			|||||||
// @Accept       json
 | 
					// @Accept       json
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        id path int true "计划ID"
 | 
					// @Param        id path int true "计划ID"
 | 
				
			||||||
// @Param        plan body UpdatePlanRequest true "更新后的计划信息"
 | 
					// @Param        plan body dto.UpdatePlanRequest true "更新后的计划信息"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=plan.PlanResponse} "业务码为200代表更新成功"
 | 
					// @Success      200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表更新成功"
 | 
				
			||||||
// @Router       /api/v1/plans/{id} [put]
 | 
					// @Router       /api/v1/plans/{id} [put]
 | 
				
			||||||
func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
					func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "更新计划"
 | 
						const actionType = "更新计划"
 | 
				
			||||||
@@ -274,7 +201,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 2. 绑定请求体
 | 
						// 2. 绑定请求体
 | 
				
			||||||
	var req UpdatePlanRequest
 | 
						var req dto.UpdatePlanRequest
 | 
				
			||||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
				
			||||||
@@ -282,7 +209,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 3. 将请求转换为模型(转换函数带校验)
 | 
						// 3. 将请求转换为模型(转换函数带校验)
 | 
				
			||||||
	planToUpdate, err := PlanFromUpdateRequest(&req)
 | 
						planToUpdate, err := dto.NewPlanFromUpdateRequest(&req)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
 | 
							c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
 | 
				
			||||||
@@ -306,8 +233,8 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
				
			|||||||
			controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
 | 
								controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		c.logger.Errorf("%s: 获取计划详情失败: %v, ID: %d", actionType, err, id)
 | 
							c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情时发生内部错误", actionType, "数据库查询失败", id)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -337,7 +264,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 7. 将模型转换为响应 DTO
 | 
						// 7. 将模型转换为响应 DTO
 | 
				
			||||||
	resp, err := PlanToResponse(updatedPlan)
 | 
						resp, err := dto.NewPlanToResponse(updatedPlan)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
 | 
							c.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
 | 
				
			||||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败", actionType, "响应序列化失败", updatedPlan)
 | 
							controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败", actionType, "响应序列化失败", updatedPlan)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,8 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
@@ -31,50 +32,6 @@ func NewController(userRepo repository.UserRepository, auditRepo repository.User
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- DTOs ---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// CreateUserRequest 定义创建用户请求的结构体
 | 
					 | 
				
			||||||
type CreateUserRequest struct {
 | 
					 | 
				
			||||||
	Username string `json:"username" binding:"required" example:"newuser"`
 | 
					 | 
				
			||||||
	Password string `json:"password" binding:"required" example:"password123"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LoginRequest 定义登录请求的结构体
 | 
					 | 
				
			||||||
type LoginRequest struct {
 | 
					 | 
				
			||||||
	// Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号
 | 
					 | 
				
			||||||
	Identifier string `json:"identifier" binding:"required" example:"testuser"`
 | 
					 | 
				
			||||||
	Password   string `json:"password" binding:"required" example:"password123"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// CreateUserResponse 定义创建用户成功响应的结构体
 | 
					 | 
				
			||||||
type CreateUserResponse struct {
 | 
					 | 
				
			||||||
	Username string `json:"username" example:"newuser"`
 | 
					 | 
				
			||||||
	ID       uint   `json:"id" example:"1"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LoginResponse 定义登录成功响应的结构体
 | 
					 | 
				
			||||||
type LoginResponse struct {
 | 
					 | 
				
			||||||
	Username string `json:"username" example:"testuser"`
 | 
					 | 
				
			||||||
	ID       uint   `json:"id" example:"1"`
 | 
					 | 
				
			||||||
	Token    string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// HistoryResponse 定义单条操作历史的响应结构体
 | 
					 | 
				
			||||||
type HistoryResponse struct {
 | 
					 | 
				
			||||||
	UserID         uint        `json:"user_id" example:"101"`
 | 
					 | 
				
			||||||
	Username       string      `json:"username" example:"testuser"`
 | 
					 | 
				
			||||||
	ActionType     string      `json:"action_type" example:"更新设备"`
 | 
					 | 
				
			||||||
	Description    string      `json:"description" example:"设备更新成功"`
 | 
					 | 
				
			||||||
	TargetResource interface{} `json:"target_resource"`
 | 
					 | 
				
			||||||
	Time           string      `json:"time"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ListHistoryResponse 定义操作历史列表的响应结构体
 | 
					 | 
				
			||||||
type ListHistoryResponse struct {
 | 
					 | 
				
			||||||
	History []HistoryResponse `json:"history"`
 | 
					 | 
				
			||||||
	Total   int64             `json:"total" example:"100"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Controller Methods ---
 | 
					// --- Controller Methods ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateUser godoc
 | 
					// CreateUser godoc
 | 
				
			||||||
@@ -83,11 +40,11 @@ type ListHistoryResponse struct {
 | 
				
			|||||||
// @Tags         用户管理
 | 
					// @Tags         用户管理
 | 
				
			||||||
// @Accept       json
 | 
					// @Accept       json
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        user body CreateUserRequest true "用户信息"
 | 
					// @Param        user body dto.CreateUserRequest true "用户信息"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=user.CreateUserResponse} "业务码为201代表创建成功"
 | 
					// @Success      200 {object} controller.Response{data=dto.CreateUserResponse} "业务码为201代表创建成功"
 | 
				
			||||||
// @Router       /api/v1/users [post]
 | 
					// @Router       /api/v1/users [post]
 | 
				
			||||||
func (c *Controller) CreateUser(ctx *gin.Context) {
 | 
					func (c *Controller) CreateUser(ctx *gin.Context) {
 | 
				
			||||||
	var req CreateUserRequest
 | 
						var req dto.CreateUserRequest
 | 
				
			||||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
		c.logger.Errorf("创建用户: 参数绑定失败: %v", err)
 | 
							c.logger.Errorf("创建用户: 参数绑定失败: %v", err)
 | 
				
			||||||
		controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
 | 
							controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
 | 
				
			||||||
@@ -114,7 +71,7 @@ func (c *Controller) CreateUser(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", CreateUserResponse{
 | 
						controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", dto.CreateUserResponse{
 | 
				
			||||||
		Username: user.Username,
 | 
							Username: user.Username,
 | 
				
			||||||
		ID:       user.ID,
 | 
							ID:       user.ID,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
@@ -126,11 +83,11 @@ func (c *Controller) CreateUser(ctx *gin.Context) {
 | 
				
			|||||||
// @Tags         用户管理
 | 
					// @Tags         用户管理
 | 
				
			||||||
// @Accept       json
 | 
					// @Accept       json
 | 
				
			||||||
// @Produce      json
 | 
					// @Produce      json
 | 
				
			||||||
// @Param        credentials body LoginRequest true "登录凭证"
 | 
					// @Param        credentials body dto.LoginRequest true "登录凭证"
 | 
				
			||||||
// @Success      200 {object} controller.Response{data=user.LoginResponse} "业务码为200代表登录成功"
 | 
					// @Success      200 {object} controller.Response{data=dto.LoginResponse} "业务码为200代表登录成功"
 | 
				
			||||||
// @Router       /api/v1/users/login [post]
 | 
					// @Router       /api/v1/users/login [post]
 | 
				
			||||||
func (c *Controller) Login(ctx *gin.Context) {
 | 
					func (c *Controller) Login(ctx *gin.Context) {
 | 
				
			||||||
	var req LoginRequest
 | 
						var req dto.LoginRequest
 | 
				
			||||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
		c.logger.Errorf("登录: 参数绑定失败: %v", err)
 | 
							c.logger.Errorf("登录: 参数绑定失败: %v", err)
 | 
				
			||||||
		controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
 | 
							controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
 | 
				
			||||||
@@ -162,7 +119,7 @@ func (c *Controller) Login(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", LoginResponse{
 | 
						controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", dto.LoginResponse{
 | 
				
			||||||
		Username: user.Username,
 | 
							Username: user.Username,
 | 
				
			||||||
		ID:       user.ID,
 | 
							ID:       user.ID,
 | 
				
			||||||
		Token:    tokenString,
 | 
							Token:    tokenString,
 | 
				
			||||||
@@ -178,7 +135,7 @@ func (c *Controller) Login(ctx *gin.Context) {
 | 
				
			|||||||
// @Param        page query     int    false "页码" default(1)
 | 
					// @Param        page query     int    false "页码" default(1)
 | 
				
			||||||
// @Param        page_size query int    false "每页大小" default(10)
 | 
					// @Param        page_size query int    false "每页大小" default(10)
 | 
				
			||||||
// @Param        action_type query string false "按操作类型过滤"
 | 
					// @Param        action_type query string false "按操作类型过滤"
 | 
				
			||||||
// @Success      200  {object}  controller.Response{data=user.ListHistoryResponse} "业务码为200代表成功获取"
 | 
					// @Success      200  {object}  controller.Response{data=dto.ListHistoryResponse} "业务码为200代表成功获取"
 | 
				
			||||||
// @Router       /api/v1/users/{id}/history [get]
 | 
					// @Router       /api/v1/users/{id}/history [get]
 | 
				
			||||||
func (c *Controller) ListUserHistory(ctx *gin.Context) {
 | 
					func (c *Controller) ListUserHistory(ctx *gin.Context) {
 | 
				
			||||||
	const actionType = "获取用户操作历史"
 | 
						const actionType = "获取用户操作历史"
 | 
				
			||||||
@@ -221,9 +178,9 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 4. 将数据库模型转换为响应 DTO
 | 
						// 4. 将数据库模型转换为响应 DTO
 | 
				
			||||||
	historyResponses := make([]HistoryResponse, 0, len(logs))
 | 
						historyResponses := make([]dto.HistoryResponse, 0, len(logs))
 | 
				
			||||||
	for _, log := range logs {
 | 
						for _, log := range logs {
 | 
				
			||||||
		historyResponses = append(historyResponses, HistoryResponse{
 | 
							historyResponses = append(historyResponses, dto.HistoryResponse{
 | 
				
			||||||
			UserID:         log.UserID,
 | 
								UserID:         log.UserID,
 | 
				
			||||||
			Username:       log.Username,
 | 
								Username:       log.Username,
 | 
				
			||||||
			ActionType:     log.ActionType,
 | 
								ActionType:     log.ActionType,
 | 
				
			||||||
@@ -234,7 +191,7 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 5. 发送成功响应
 | 
						// 5. 发送成功响应
 | 
				
			||||||
	resp := ListHistoryResponse{
 | 
						resp := dto.ListHistoryResponse{
 | 
				
			||||||
		History: historyResponses,
 | 
							History: historyResponses,
 | 
				
			||||||
		Total:   total,
 | 
							Total:   total,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@ type MockUserRepository struct {
 | 
				
			|||||||
	mock.Mock
 | 
						mock.Mock
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Create 模拟 UserRepository 的 Create 方法
 | 
					// CreateTx 模拟 UserRepository 的 CreateTx 方法
 | 
				
			||||||
func (m *MockUserRepository) Create(user *models.User) error {
 | 
					func (m *MockUserRepository) Create(user *models.User) error {
 | 
				
			||||||
	args := m.Called(user)
 | 
						args := m.Called(user)
 | 
				
			||||||
	return args.Error(0)
 | 
						return args.Error(0)
 | 
				
			||||||
@@ -90,8 +90,8 @@ func TestCreateUser(t *testing.T) {
 | 
				
			|||||||
				Password: "password123",
 | 
									Password: "password123",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			mockRepoSetup: func(m *MockUserRepository) {
 | 
								mockRepoSetup: func(m *MockUserRepository) {
 | 
				
			||||||
				// 模拟 Create 成功
 | 
									// 模拟 CreateTx 成功
 | 
				
			||||||
				m.On("Create", mock.AnythingOfType("*models.User")).Return(nil).Run(func(args mock.Arguments) {
 | 
									m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(nil).Run(func(args mock.Arguments) {
 | 
				
			||||||
					// 模拟数据库自动填充 ID
 | 
										// 模拟数据库自动填充 ID
 | 
				
			||||||
					userArg := args.Get(0).(*models.User)
 | 
										userArg := args.Get(0).(*models.User)
 | 
				
			||||||
					userArg.ID = 1 // 设置一个非零的 ID
 | 
										userArg.ID = 1 // 设置一个非零的 ID
 | 
				
			||||||
@@ -114,7 +114,7 @@ func TestCreateUser(t *testing.T) {
 | 
				
			|||||||
				Password: "123", // 密码少于6位
 | 
									Password: "123", // 密码少于6位
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			mockRepoSetup: func(m *MockUserRepository) {
 | 
								mockRepoSetup: func(m *MockUserRepository) {
 | 
				
			||||||
				// 不会调用 Create 或 FindByUsername
 | 
									// 不会调用 CreateTx 或 FindByUsername
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			expectedResponse: map[string]interface{}{
 | 
								expectedResponse: map[string]interface{}{
 | 
				
			||||||
				"code":    float64(controller.CodeBadRequest),
 | 
									"code":    float64(controller.CodeBadRequest),
 | 
				
			||||||
@@ -128,7 +128,7 @@ func TestCreateUser(t *testing.T) {
 | 
				
			|||||||
				Password: "password123",
 | 
									Password: "password123",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			mockRepoSetup: func(m *MockUserRepository) {
 | 
								mockRepoSetup: func(m *MockUserRepository) {
 | 
				
			||||||
				// 不会调用 Create 或 FindByUsername
 | 
									// 不会调用 CreateTx 或 FindByUsername
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			expectedResponse: map[string]interface{}{
 | 
								expectedResponse: map[string]interface{}{
 | 
				
			||||||
				"code":    float64(controller.CodeBadRequest),
 | 
									"code":    float64(controller.CodeBadRequest),
 | 
				
			||||||
@@ -143,8 +143,8 @@ func TestCreateUser(t *testing.T) {
 | 
				
			|||||||
				Password: "password123",
 | 
									Password: "password123",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			mockRepoSetup: func(m *MockUserRepository) {
 | 
								mockRepoSetup: func(m *MockUserRepository) {
 | 
				
			||||||
				// 模拟 Create 失败,因为用户名已存在
 | 
									// 模拟 CreateTx 失败,因为用户名已存在
 | 
				
			||||||
				m.On("Create", mock.AnythingOfType("*models.User")).Return(errors.New("duplicate entry")).Once()
 | 
									m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("duplicate entry")).Once()
 | 
				
			||||||
				// 模拟 FindByUsername 找到用户,确认是用户名重复
 | 
									// 模拟 FindByUsername 找到用户,确认是用户名重复
 | 
				
			||||||
				m.On("FindByUsername", "existinguser").Return(&models.User{Username: "existinguser"}, nil).Once()
 | 
									m.On("FindByUsername", "existinguser").Return(&models.User{Username: "existinguser"}, nil).Once()
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
@@ -161,8 +161,8 @@ func TestCreateUser(t *testing.T) {
 | 
				
			|||||||
				Password: "password123",
 | 
									Password: "password123",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			mockRepoSetup: func(m *MockUserRepository) {
 | 
								mockRepoSetup: func(m *MockUserRepository) {
 | 
				
			||||||
				// 模拟 Create 失败,通用数据库错误
 | 
									// 模拟 CreateTx 失败,通用数据库错误
 | 
				
			||||||
				m.On("Create", mock.AnythingOfType("*models.User")).Return(errors.New("database error")).Once()
 | 
									m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("database error")).Once()
 | 
				
			||||||
				// 模拟 FindByUsername 找不到用户,确认不是用户名重复
 | 
									// 模拟 FindByUsername 找不到用户,确认不是用户名重复
 | 
				
			||||||
				m.On("FindByUsername", "db_error_user").Return(nil, gorm.ErrRecordNotFound).Once()
 | 
									m.On("FindByUsername", "db_error_user").Return(nil, gorm.ErrRecordNotFound).Once()
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										142
									
								
								internal/app/dto/device_converter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								internal/app/dto/device_converter.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
				
			|||||||
 | 
					package dto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewDeviceResponse 从数据库模型创建一个新的设备响应 DTO
 | 
				
			||||||
 | 
					func NewDeviceResponse(device *models.Device) (*DeviceResponse, error) {
 | 
				
			||||||
 | 
						if device == nil {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var props map[string]interface{}
 | 
				
			||||||
 | 
						if len(device.Properties) > 0 && string(device.Properties) != "null" {
 | 
				
			||||||
 | 
							if err := device.ParseProperties(&props); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("解析设备属性失败 (ID: %d): %w", device.ID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 确保 DeviceTemplate 和 AreaController 已预加载
 | 
				
			||||||
 | 
						deviceTemplateName := ""
 | 
				
			||||||
 | 
						if device.DeviceTemplate.ID != 0 {
 | 
				
			||||||
 | 
							deviceTemplateName = device.DeviceTemplate.Name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						areaControllerName := ""
 | 
				
			||||||
 | 
						if device.AreaController.ID != 0 {
 | 
				
			||||||
 | 
							areaControllerName = device.AreaController.Name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &DeviceResponse{
 | 
				
			||||||
 | 
							ID:                 device.ID,
 | 
				
			||||||
 | 
							Name:               device.Name,
 | 
				
			||||||
 | 
							DeviceTemplateID:   device.DeviceTemplateID,
 | 
				
			||||||
 | 
							DeviceTemplateName: deviceTemplateName,
 | 
				
			||||||
 | 
							AreaControllerID:   device.AreaControllerID,
 | 
				
			||||||
 | 
							AreaControllerName: areaControllerName,
 | 
				
			||||||
 | 
							Location:           device.Location,
 | 
				
			||||||
 | 
							Properties:         props,
 | 
				
			||||||
 | 
							CreatedAt:          device.CreatedAt.Format(time.RFC3339),
 | 
				
			||||||
 | 
							UpdatedAt:          device.UpdatedAt.Format(time.RFC3339),
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewListDeviceResponse 从数据库模型切片创建一个新的设备列表响应 DTO 切片
 | 
				
			||||||
 | 
					func NewListDeviceResponse(devices []*models.Device) ([]*DeviceResponse, error) {
 | 
				
			||||||
 | 
						list := make([]*DeviceResponse, 0, len(devices))
 | 
				
			||||||
 | 
						for _, device := range devices {
 | 
				
			||||||
 | 
							resp, err := NewDeviceResponse(device)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							list = append(list, resp)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return list, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewAreaControllerResponse 从数据库模型创建一个新的区域主控响应 DTO
 | 
				
			||||||
 | 
					func NewAreaControllerResponse(ac *models.AreaController) (*AreaControllerResponse, error) {
 | 
				
			||||||
 | 
						if ac == nil {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var props map[string]interface{}
 | 
				
			||||||
 | 
						if len(ac.Properties) > 0 && string(ac.Properties) != "null" {
 | 
				
			||||||
 | 
							if err := json.Unmarshal(ac.Properties, &props); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("解析区域主控属性失败 (ID: %d): %w", ac.ID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &AreaControllerResponse{
 | 
				
			||||||
 | 
							ID:         ac.ID,
 | 
				
			||||||
 | 
							Name:       ac.Name,
 | 
				
			||||||
 | 
							NetworkID:  ac.NetworkID,
 | 
				
			||||||
 | 
							Location:   ac.Location,
 | 
				
			||||||
 | 
							Status:     ac.Status,
 | 
				
			||||||
 | 
							Properties: props,
 | 
				
			||||||
 | 
							CreatedAt:  ac.CreatedAt.Format(time.RFC3339),
 | 
				
			||||||
 | 
							UpdatedAt:  ac.UpdatedAt.Format(time.RFC3339),
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewListAreaControllerResponse 从数据库模型切片创建一个新的区域主控列表响应 DTO 切片
 | 
				
			||||||
 | 
					func NewListAreaControllerResponse(acs []*models.AreaController) ([]*AreaControllerResponse, error) {
 | 
				
			||||||
 | 
						list := make([]*AreaControllerResponse, 0, len(acs))
 | 
				
			||||||
 | 
						for _, ac := range acs {
 | 
				
			||||||
 | 
							resp, err := NewAreaControllerResponse(ac)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							list = append(list, resp)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return list, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewDeviceTemplateResponse 从数据库模型创建一个新的设备模板响应 DTO
 | 
				
			||||||
 | 
					func NewDeviceTemplateResponse(dt *models.DeviceTemplate) (*DeviceTemplateResponse, error) {
 | 
				
			||||||
 | 
						if dt == nil {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var commands map[string]interface{}
 | 
				
			||||||
 | 
						if err := dt.ParseCommands(&commands); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("解析设备模板命令失败 (ID: %d): %w", dt.ID, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var values []models.ValueDescriptor
 | 
				
			||||||
 | 
						if dt.Category == models.CategorySensor {
 | 
				
			||||||
 | 
							if err := dt.ParseValues(&values); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("解析设备模板值描述符失败 (ID: %d): %w", dt.ID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &DeviceTemplateResponse{
 | 
				
			||||||
 | 
							ID:           dt.ID,
 | 
				
			||||||
 | 
							Name:         dt.Name,
 | 
				
			||||||
 | 
							Manufacturer: dt.Manufacturer,
 | 
				
			||||||
 | 
							Description:  dt.Description,
 | 
				
			||||||
 | 
							Category:     dt.Category,
 | 
				
			||||||
 | 
							Commands:     commands,
 | 
				
			||||||
 | 
							Values:       values,
 | 
				
			||||||
 | 
							CreatedAt:    dt.CreatedAt.Format(time.RFC3339),
 | 
				
			||||||
 | 
							UpdatedAt:    dt.UpdatedAt.Format(time.RFC3339),
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewListDeviceTemplateResponse 从数据库模型切片创建一个新的设备模板列表响应 DTO 切片
 | 
				
			||||||
 | 
					func NewListDeviceTemplateResponse(dts []*models.DeviceTemplate) ([]*DeviceTemplateResponse, error) {
 | 
				
			||||||
 | 
						list := make([]*DeviceTemplateResponse, 0, len(dts))
 | 
				
			||||||
 | 
						for _, dt := range dts {
 | 
				
			||||||
 | 
							resp, err := NewDeviceTemplateResponse(dt)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							list = append(list, resp)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return list, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										96
									
								
								internal/app/dto/device_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								internal/app/dto/device_dto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					package dto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateDeviceRequest 定义了创建设备时需要传入的参数
 | 
				
			||||||
 | 
					type CreateDeviceRequest struct {
 | 
				
			||||||
 | 
						Name             string                 `json:"name" binding:"required"`
 | 
				
			||||||
 | 
						DeviceTemplateID uint                   `json:"device_template_id" binding:"required"`
 | 
				
			||||||
 | 
						AreaControllerID uint                   `json:"area_controller_id" binding:"required"`
 | 
				
			||||||
 | 
						Location         string                 `json:"location,omitempty"`
 | 
				
			||||||
 | 
						Properties       map[string]interface{} `json:"properties,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateDeviceRequest 定义了更新设备时需要传入的参数
 | 
				
			||||||
 | 
					type UpdateDeviceRequest struct {
 | 
				
			||||||
 | 
						Name             string                 `json:"name" binding:"required"`
 | 
				
			||||||
 | 
						DeviceTemplateID uint                   `json:"device_template_id" binding:"required"`
 | 
				
			||||||
 | 
						AreaControllerID uint                   `json:"area_controller_id" binding:"required"`
 | 
				
			||||||
 | 
						Location         string                 `json:"location,omitempty"`
 | 
				
			||||||
 | 
						Properties       map[string]interface{} `json:"properties,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateAreaControllerRequest 定义了创建区域主控时需要传入的参数
 | 
				
			||||||
 | 
					type CreateAreaControllerRequest struct {
 | 
				
			||||||
 | 
						Name       string                 `json:"name" binding:"required"`
 | 
				
			||||||
 | 
						NetworkID  string                 `json:"network_id" binding:"required"`
 | 
				
			||||||
 | 
						Location   string                 `json:"location,omitempty"`
 | 
				
			||||||
 | 
						Properties map[string]interface{} `json:"properties,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateAreaControllerRequest 定义了更新区域主控时需要传入的参数
 | 
				
			||||||
 | 
					type UpdateAreaControllerRequest struct {
 | 
				
			||||||
 | 
						Name       string                 `json:"name" binding:"required"`
 | 
				
			||||||
 | 
						NetworkID  string                 `json:"network_id" binding:"required"`
 | 
				
			||||||
 | 
						Location   string                 `json:"location,omitempty"`
 | 
				
			||||||
 | 
						Properties map[string]interface{} `json:"properties,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateDeviceTemplateRequest 定义了创建设备模板时需要传入的参数
 | 
				
			||||||
 | 
					type CreateDeviceTemplateRequest struct {
 | 
				
			||||||
 | 
						Name         string                   `json:"name" binding:"required"`
 | 
				
			||||||
 | 
						Manufacturer string                   `json:"manufacturer,omitempty"`
 | 
				
			||||||
 | 
						Description  string                   `json:"description,omitempty"`
 | 
				
			||||||
 | 
						Category     models.DeviceCategory    `json:"category" binding:"required"`
 | 
				
			||||||
 | 
						Commands     map[string]interface{}   `json:"commands" binding:"required"`
 | 
				
			||||||
 | 
						Values       []models.ValueDescriptor `json:"values,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateDeviceTemplateRequest 定义了更新设备模板时需要传入的参数
 | 
				
			||||||
 | 
					type UpdateDeviceTemplateRequest struct {
 | 
				
			||||||
 | 
						Name         string                   `json:"name" binding:"required"`
 | 
				
			||||||
 | 
						Manufacturer string                   `json:"manufacturer,omitempty"`
 | 
				
			||||||
 | 
						Description  string                   `json:"description,omitempty"`
 | 
				
			||||||
 | 
						Category     models.DeviceCategory    `json:"category" binding:"required"`
 | 
				
			||||||
 | 
						Commands     map[string]interface{}   `json:"commands" binding:"required"`
 | 
				
			||||||
 | 
						Values       []models.ValueDescriptor `json:"values,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeviceResponse 定义了返回给客户端的单个设备信息的结构
 | 
				
			||||||
 | 
					type DeviceResponse struct {
 | 
				
			||||||
 | 
						ID                 uint                   `json:"id"`
 | 
				
			||||||
 | 
						Name               string                 `json:"name"`
 | 
				
			||||||
 | 
						DeviceTemplateID   uint                   `json:"device_template_id"`
 | 
				
			||||||
 | 
						DeviceTemplateName string                 `json:"device_template_name"`
 | 
				
			||||||
 | 
						AreaControllerID   uint                   `json:"area_controller_id"`
 | 
				
			||||||
 | 
						AreaControllerName string                 `json:"area_controller_name"`
 | 
				
			||||||
 | 
						Location           string                 `json:"location"`
 | 
				
			||||||
 | 
						Properties         map[string]interface{} `json:"properties"`
 | 
				
			||||||
 | 
						CreatedAt          string                 `json:"created_at"`
 | 
				
			||||||
 | 
						UpdatedAt          string                 `json:"updated_at"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AreaControllerResponse 定义了返回给客户端的单个区域主控信息的结构
 | 
				
			||||||
 | 
					type AreaControllerResponse struct {
 | 
				
			||||||
 | 
						ID         uint                   `json:"id"`
 | 
				
			||||||
 | 
						Name       string                 `json:"name"`
 | 
				
			||||||
 | 
						NetworkID  string                 `json:"network_id"`
 | 
				
			||||||
 | 
						Location   string                 `json:"location"`
 | 
				
			||||||
 | 
						Status     string                 `json:"status"`
 | 
				
			||||||
 | 
						Properties map[string]interface{} `json:"properties"`
 | 
				
			||||||
 | 
						CreatedAt  string                 `json:"created_at"`
 | 
				
			||||||
 | 
						UpdatedAt  string                 `json:"updated_at"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeviceTemplateResponse 定义了返回给客户端的单个设备模板信息的结构
 | 
				
			||||||
 | 
					type DeviceTemplateResponse struct {
 | 
				
			||||||
 | 
						ID           uint                     `json:"id"`
 | 
				
			||||||
 | 
						Name         string                   `json:"name"`
 | 
				
			||||||
 | 
						Manufacturer string                   `json:"manufacturer"`
 | 
				
			||||||
 | 
						Description  string                   `json:"description"`
 | 
				
			||||||
 | 
						Category     models.DeviceCategory    `json:"category"`
 | 
				
			||||||
 | 
						Commands     map[string]interface{}   `json:"commands"`
 | 
				
			||||||
 | 
						Values       []models.ValueDescriptor `json:"values"`
 | 
				
			||||||
 | 
						CreatedAt    string                   `json:"created_at"`
 | 
				
			||||||
 | 
						UpdatedAt    string                   `json:"updated_at"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										160
									
								
								internal/app/dto/pig_batch_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								internal/app/dto/pig_batch_dto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
				
			|||||||
 | 
					package dto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchCreateDTO 定义了创建猪批次的请求结构
 | 
				
			||||||
 | 
					type PigBatchCreateDTO struct {
 | 
				
			||||||
 | 
						BatchNumber  string                    `json:"batch_number" binding:"required"`        // 批次编号,必填
 | 
				
			||||||
 | 
						OriginType   models.PigBatchOriginType `json:"origin_type" binding:"required"`         // 批次来源,必填
 | 
				
			||||||
 | 
						StartDate    time.Time                 `json:"start_date" binding:"required"`          // 批次开始日期,必填
 | 
				
			||||||
 | 
						InitialCount int                       `json:"initial_count" binding:"required,min=1"` // 初始数量,必填,最小为1
 | 
				
			||||||
 | 
						Status       models.PigBatchStatus     `json:"status" binding:"required"`              // 批次状态,必填
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchUpdateDTO 定义了更新猪批次的请求结构
 | 
				
			||||||
 | 
					type PigBatchUpdateDTO struct {
 | 
				
			||||||
 | 
						BatchNumber  *string                    `json:"batch_number"`  // 批次编号,可选
 | 
				
			||||||
 | 
						OriginType   *models.PigBatchOriginType `json:"origin_type"`   // 批次来源,可选
 | 
				
			||||||
 | 
						StartDate    *time.Time                 `json:"start_date"`    // 批次开始日期,可选
 | 
				
			||||||
 | 
						EndDate      *time.Time                 `json:"end_date"`      // 批次结束日期,可选
 | 
				
			||||||
 | 
						InitialCount *int                       `json:"initial_count"` // 初始数量,可选
 | 
				
			||||||
 | 
						Status       *models.PigBatchStatus     `json:"status"`        // 批次状态,可选
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchQueryDTO 定义了查询猪批次的请求结构
 | 
				
			||||||
 | 
					type PigBatchQueryDTO struct {
 | 
				
			||||||
 | 
						IsActive *bool `json:"is_active" form:"is_active"` // 是否活跃,可选,用于URL查询参数
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchResponseDTO 定义了猪批次信息的响应结构
 | 
				
			||||||
 | 
					type PigBatchResponseDTO struct {
 | 
				
			||||||
 | 
						ID           uint                      `json:"id"`            // 批次ID
 | 
				
			||||||
 | 
						BatchNumber  string                    `json:"batch_number"`  // 批次编号
 | 
				
			||||||
 | 
						OriginType   models.PigBatchOriginType `json:"origin_type"`   // 批次来源
 | 
				
			||||||
 | 
						StartDate    time.Time                 `json:"start_date"`    // 批次开始日期
 | 
				
			||||||
 | 
						EndDate      time.Time                 `json:"end_date"`      // 批次结束日期
 | 
				
			||||||
 | 
						InitialCount int                       `json:"initial_count"` // 初始数量
 | 
				
			||||||
 | 
						Status       models.PigBatchStatus     `json:"status"`        // 批次状态
 | 
				
			||||||
 | 
						IsActive     bool                      `json:"is_active"`     // 是否活跃
 | 
				
			||||||
 | 
						CreateTime   time.Time                 `json:"create_time"`   // 创建时间
 | 
				
			||||||
 | 
						UpdateTime   time.Time                 `json:"update_time"`   // 更新时间
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AssignEmptyPensToBatchRequest 用于为猪批次分配空栏的请求体
 | 
				
			||||||
 | 
					type AssignEmptyPensToBatchRequest struct {
 | 
				
			||||||
 | 
						PenIDs []uint `json:"penIDs" binding:"required,min=1" example:"[1,2,3]"` // 待分配的猪栏ID列表
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReclassifyPenToNewBatchRequest 用于将猪栏划拨到新批次的请求体
 | 
				
			||||||
 | 
					type ReclassifyPenToNewBatchRequest struct {
 | 
				
			||||||
 | 
						ToBatchID uint   `json:"toBatchID" binding:"required"` // 目标猪批次ID
 | 
				
			||||||
 | 
						PenID     uint   `json:"penID" binding:"required"`     // 待划拨的猪栏ID
 | 
				
			||||||
 | 
						Remarks   string `json:"remarks"`                      // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RemoveEmptyPenFromBatchRequest 用于从猪批次移除空栏的请求体
 | 
				
			||||||
 | 
					type RemoveEmptyPenFromBatchRequest struct {
 | 
				
			||||||
 | 
						PenID uint `json:"penID" binding:"required"` // 待移除的猪栏ID
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MovePigsIntoPenRequest 用于将猪只从“虚拟库存”移入指定猪栏的请求体
 | 
				
			||||||
 | 
					type MovePigsIntoPenRequest struct {
 | 
				
			||||||
 | 
						ToPenID  uint   `json:"toPenID" binding:"required"`        // 目标猪栏ID
 | 
				
			||||||
 | 
						Quantity int    `json:"quantity" binding:"required,min=1"` // 移入猪只数量
 | 
				
			||||||
 | 
						Remarks  string `json:"remarks"`                           // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SellPigsRequest 用于处理卖猪的请求体
 | 
				
			||||||
 | 
					type SellPigsRequest struct {
 | 
				
			||||||
 | 
						PenID      uint      `json:"penID" binding:"required"`            // 猪栏ID
 | 
				
			||||||
 | 
						Quantity   int       `json:"quantity" binding:"required,min=1"`   // 卖出猪只数量
 | 
				
			||||||
 | 
						UnitPrice  float64   `json:"unitPrice" binding:"required,min=0"`  // 单价
 | 
				
			||||||
 | 
						TotalPrice float64   `json:"totalPrice" binding:"required,min=0"` // 总价
 | 
				
			||||||
 | 
						TraderName string    `json:"traderName" binding:"required"`       // 交易方名称
 | 
				
			||||||
 | 
						TradeDate  time.Time `json:"tradeDate" binding:"required"`        // 交易日期
 | 
				
			||||||
 | 
						Remarks    string    `json:"remarks"`                             // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuyPigsRequest 用于处理买猪的请求体
 | 
				
			||||||
 | 
					type BuyPigsRequest struct {
 | 
				
			||||||
 | 
						PenID      uint      `json:"penID" binding:"required"`            // 猪栏ID
 | 
				
			||||||
 | 
						Quantity   int       `json:"quantity" binding:"required,min=1"`   // 买入猪只数量
 | 
				
			||||||
 | 
						UnitPrice  float64   `json:"unitPrice" binding:"required,min=0"`  // 单价
 | 
				
			||||||
 | 
						TotalPrice float64   `json:"totalPrice" binding:"required,min=0"` // 总价
 | 
				
			||||||
 | 
						TraderName string    `json:"traderName" binding:"required"`       // 交易方名称
 | 
				
			||||||
 | 
						TradeDate  time.Time `json:"tradeDate" binding:"required"`        // 交易日期
 | 
				
			||||||
 | 
						Remarks    string    `json:"remarks"`                             // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TransferPigsAcrossBatchesRequest 用于跨猪群调栏的请求体
 | 
				
			||||||
 | 
					type TransferPigsAcrossBatchesRequest struct {
 | 
				
			||||||
 | 
						DestBatchID uint   `json:"destBatchID" binding:"required"`    // 目标猪批次ID
 | 
				
			||||||
 | 
						FromPenID   uint   `json:"fromPenID" binding:"required"`      // 源猪栏ID
 | 
				
			||||||
 | 
						ToPenID     uint   `json:"toPenID" binding:"required"`        // 目标猪栏ID
 | 
				
			||||||
 | 
						Quantity    uint   `json:"quantity" binding:"required,min=1"` // 调栏猪只数量
 | 
				
			||||||
 | 
						Remarks     string `json:"remarks"`                           // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TransferPigsWithinBatchRequest 用于群内调栏的请求体
 | 
				
			||||||
 | 
					type TransferPigsWithinBatchRequest struct {
 | 
				
			||||||
 | 
						FromPenID uint   `json:"fromPenID" binding:"required"`      // 源猪栏ID
 | 
				
			||||||
 | 
						ToPenID   uint   `json:"toPenID" binding:"required"`        // 目标猪栏ID
 | 
				
			||||||
 | 
						Quantity  uint   `json:"quantity" binding:"required,min=1"` // 调栏猪只数量
 | 
				
			||||||
 | 
						Remarks   string `json:"remarks"`                           // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigsRequest 用于记录新增病猪事件的请求体
 | 
				
			||||||
 | 
					type RecordSickPigsRequest struct {
 | 
				
			||||||
 | 
						PenID             uint                                    `json:"penID" binding:"required"`             // 猪栏ID
 | 
				
			||||||
 | 
						Quantity          int                                     `json:"quantity" binding:"required,min=1"`    // 病猪数量
 | 
				
			||||||
 | 
						TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
 | 
				
			||||||
 | 
						HappenedAt        time.Time                               `json:"happenedAt" binding:"required"`        // 发生时间
 | 
				
			||||||
 | 
						Remarks           string                                  `json:"remarks"`                              // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigRecoveryRequest 用于记录病猪康复事件的请求体
 | 
				
			||||||
 | 
					type RecordSickPigRecoveryRequest struct {
 | 
				
			||||||
 | 
						PenID             uint                                    `json:"penID" binding:"required"`             // 猪栏ID
 | 
				
			||||||
 | 
						Quantity          int                                     `json:"quantity" binding:"required,min=1"`    // 康复猪数量
 | 
				
			||||||
 | 
						TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
 | 
				
			||||||
 | 
						HappenedAt        time.Time                               `json:"happenedAt" binding:"required"`        // 发生时间
 | 
				
			||||||
 | 
						Remarks           string                                  `json:"remarks"`                              // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigDeathRequest 用于记录病猪死亡事件的请求体
 | 
				
			||||||
 | 
					type RecordSickPigDeathRequest struct {
 | 
				
			||||||
 | 
						PenID             uint                                    `json:"penID" binding:"required"`             // 猪栏ID
 | 
				
			||||||
 | 
						Quantity          int                                     `json:"quantity" binding:"required,min=1"`    // 死亡猪数量
 | 
				
			||||||
 | 
						TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
 | 
				
			||||||
 | 
						HappenedAt        time.Time                               `json:"happenedAt" binding:"required"`        // 发生时间
 | 
				
			||||||
 | 
						Remarks           string                                  `json:"remarks"`                              // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigCullRequest 用于记录病猪淘汰事件的请求体
 | 
				
			||||||
 | 
					type RecordSickPigCullRequest struct {
 | 
				
			||||||
 | 
						PenID             uint                                    `json:"penID" binding:"required"`             // 猪栏ID
 | 
				
			||||||
 | 
						Quantity          int                                     `json:"quantity" binding:"required,min=1"`    // 淘汰猪数量
 | 
				
			||||||
 | 
						TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
 | 
				
			||||||
 | 
						HappenedAt        time.Time                               `json:"happenedAt" binding:"required"`        // 发生时间
 | 
				
			||||||
 | 
						Remarks           string                                  `json:"remarks"`                              // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordDeathRequest 用于记录正常猪只死亡事件的请求体
 | 
				
			||||||
 | 
					type RecordDeathRequest struct {
 | 
				
			||||||
 | 
						PenID      uint      `json:"penID" binding:"required"`          // 猪栏ID
 | 
				
			||||||
 | 
						Quantity   int       `json:"quantity" binding:"required,min=1"` // 死亡猪数量
 | 
				
			||||||
 | 
						HappenedAt time.Time `json:"happenedAt" binding:"required"`     // 发生时间
 | 
				
			||||||
 | 
						Remarks    string    `json:"remarks"`                           // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordCullRequest 用于记录正常猪只淘汰事件的请求体
 | 
				
			||||||
 | 
					type RecordCullRequest struct {
 | 
				
			||||||
 | 
						PenID      uint      `json:"penID" binding:"required"`          // 猪栏ID
 | 
				
			||||||
 | 
						Quantity   int       `json:"quantity" binding:"required,min=1"` // 淘汰猪数量
 | 
				
			||||||
 | 
						HappenedAt time.Time `json:"happenedAt" binding:"required"`     // 发生时间
 | 
				
			||||||
 | 
						Remarks    string    `json:"remarks"`                           // 备注
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										52
									
								
								internal/app/dto/pig_farm_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								internal/app/dto/pig_farm_dto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					package dto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigHouseResponse 定义了猪舍信息的响应结构
 | 
				
			||||||
 | 
					type PigHouseResponse struct {
 | 
				
			||||||
 | 
						ID          uint   `json:"id"`
 | 
				
			||||||
 | 
						Name        string `json:"name"`
 | 
				
			||||||
 | 
						Description string `json:"description"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PenResponse 定义了猪栏信息的响应结构
 | 
				
			||||||
 | 
					type PenResponse struct {
 | 
				
			||||||
 | 
						ID         uint             `json:"id"`
 | 
				
			||||||
 | 
						PenNumber  string           `json:"pen_number"`
 | 
				
			||||||
 | 
						HouseID    uint             `json:"house_id"`
 | 
				
			||||||
 | 
						Capacity   int              `json:"capacity"`
 | 
				
			||||||
 | 
						Status     models.PenStatus `json:"status"`
 | 
				
			||||||
 | 
						PigBatchID uint             `json:"pig_batch_id"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigHouseRequest 定义了创建猪舍的请求结构
 | 
				
			||||||
 | 
					type CreatePigHouseRequest struct {
 | 
				
			||||||
 | 
						Name        string `json:"name" binding:"required"`
 | 
				
			||||||
 | 
						Description string `json:"description"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePigHouseRequest 定义了更新猪舍的请求结构
 | 
				
			||||||
 | 
					type UpdatePigHouseRequest struct {
 | 
				
			||||||
 | 
						Name        string `json:"name" binding:"required"`
 | 
				
			||||||
 | 
						Description string `json:"description"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePenRequest 定义了创建猪栏的请求结构
 | 
				
			||||||
 | 
					type CreatePenRequest struct {
 | 
				
			||||||
 | 
						PenNumber string `json:"pen_number" binding:"required"`
 | 
				
			||||||
 | 
						HouseID   uint   `json:"house_id" binding:"required"`
 | 
				
			||||||
 | 
						Capacity  int    `json:"capacity" binding:"required"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePenRequest 定义了更新猪栏的请求结构
 | 
				
			||||||
 | 
					type UpdatePenRequest struct {
 | 
				
			||||||
 | 
						PenNumber string           `json:"pen_number" binding:"required"`
 | 
				
			||||||
 | 
						HouseID   uint             `json:"house_id" binding:"required"`
 | 
				
			||||||
 | 
						Capacity  int              `json:"capacity" binding:"required"`
 | 
				
			||||||
 | 
						Status    models.PenStatus `json:"status" binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePenStatusRequest 定义了更新猪栏状态的请求结构
 | 
				
			||||||
 | 
					type UpdatePenStatusRequest struct {
 | 
				
			||||||
 | 
						Status models.PenStatus `json:"status" binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中" example:"病猪栏"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package plan
 | 
					package dto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
@@ -7,8 +7,8 @@ import (
 | 
				
			|||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PlanToResponse 将Plan模型转换为PlanResponse
 | 
					// NewPlanToResponse 将Plan模型转换为PlanResponse
 | 
				
			||||||
func PlanToResponse(plan *models.Plan) (*PlanResponse, error) {
 | 
					func NewPlanToResponse(plan *models.Plan) (*PlanResponse, error) {
 | 
				
			||||||
	if plan == nil {
 | 
						if plan == nil {
 | 
				
			||||||
		return nil, nil
 | 
							return nil, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -52,8 +52,8 @@ func PlanToResponse(plan *models.Plan) (*PlanResponse, error) {
 | 
				
			|||||||
	return response, nil
 | 
						return response, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证
 | 
					// NewPlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证
 | 
				
			||||||
func PlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
 | 
					func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
 | 
				
			||||||
	if req == nil {
 | 
						if req == nil {
 | 
				
			||||||
		return nil, nil
 | 
							return nil, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -104,8 +104,8 @@ func PlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
 | 
				
			|||||||
	return plan, nil
 | 
						return plan, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型,并进行业务规则验证
 | 
					// NewPlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型,并进行业务规则验证
 | 
				
			||||||
func PlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
 | 
					func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
 | 
				
			||||||
	if req == nil {
 | 
						if req == nil {
 | 
				
			||||||
		return nil, nil
 | 
							return nil, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -171,7 +171,7 @@ func SubPlanToResponse(subPlan *models.SubPlan) (SubPlanResponse, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// 如果有完整的子计划数据,也进行转换
 | 
						// 如果有完整的子计划数据,也进行转换
 | 
				
			||||||
	if subPlan.ChildPlan != nil {
 | 
						if subPlan.ChildPlan != nil {
 | 
				
			||||||
		childPlanResp, err := PlanToResponse(subPlan.ChildPlan)
 | 
							childPlanResp, err := NewPlanToResponse(subPlan.ChildPlan)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return SubPlanResponse{}, err
 | 
								return SubPlanResponse{}, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
							
								
								
									
										75
									
								
								internal/app/dto/plan_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								internal/app/dto/plan_dto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					package dto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePlanRequest 定义创建计划请求的结构体
 | 
				
			||||||
 | 
					type CreatePlanRequest struct {
 | 
				
			||||||
 | 
						Name           string                   `json:"name" binding:"required" example:"猪舍温度控制计划"`
 | 
				
			||||||
 | 
						Description    string                   `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
				
			||||||
 | 
						ExecutionType  models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"`
 | 
				
			||||||
 | 
						ExecuteNum     uint                     `json:"execute_num,omitempty" example:"10"`
 | 
				
			||||||
 | 
						CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
				
			||||||
 | 
						SubPlanIDs     []uint                   `json:"sub_plan_ids,omitempty"`
 | 
				
			||||||
 | 
						Tasks          []TaskRequest            `json:"tasks,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PlanResponse 定义计划详情响应的结构体
 | 
				
			||||||
 | 
					type PlanResponse struct {
 | 
				
			||||||
 | 
						ID             uint                     `json:"id" example:"1"`
 | 
				
			||||||
 | 
						Name           string                   `json:"name" example:"猪舍温度控制计划"`
 | 
				
			||||||
 | 
						Description    string                   `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
				
			||||||
 | 
						ExecutionType  models.PlanExecutionType `json:"execution_type" example:"自动"`
 | 
				
			||||||
 | 
						Status         models.PlanStatus        `json:"status" example:"已启用"`
 | 
				
			||||||
 | 
						ExecuteNum     uint                     `json:"execute_num" example:"10"`
 | 
				
			||||||
 | 
						ExecuteCount   uint                     `json:"execute_count" example:"0"`
 | 
				
			||||||
 | 
						CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
				
			||||||
 | 
						ContentType    models.PlanContentType   `json:"content_type" example:"任务"`
 | 
				
			||||||
 | 
						SubPlans       []SubPlanResponse        `json:"sub_plans,omitempty"`
 | 
				
			||||||
 | 
						Tasks          []TaskResponse           `json:"tasks,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPlansResponse 定义获取计划列表响应的结构体
 | 
				
			||||||
 | 
					type ListPlansResponse struct {
 | 
				
			||||||
 | 
						Plans []PlanResponse `json:"plans"`
 | 
				
			||||||
 | 
						Total int            `json:"total" example:"100"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePlanRequest 定义更新计划请求的结构体
 | 
				
			||||||
 | 
					type UpdatePlanRequest struct {
 | 
				
			||||||
 | 
						Name           string                   `json:"name" example:"猪舍温度控制计划V2"`
 | 
				
			||||||
 | 
						Description    string                   `json:"description" example:"更新后的描述"`
 | 
				
			||||||
 | 
						ExecutionType  models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"`
 | 
				
			||||||
 | 
						ExecuteNum     uint                     `json:"execute_num,omitempty" example:"10"`
 | 
				
			||||||
 | 
						CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
				
			||||||
 | 
						SubPlanIDs     []uint                   `json:"sub_plan_ids,omitempty"`
 | 
				
			||||||
 | 
						Tasks          []TaskRequest            `json:"tasks,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SubPlanResponse 定义子计划响应结构体
 | 
				
			||||||
 | 
					type SubPlanResponse struct {
 | 
				
			||||||
 | 
						ID             uint          `json:"id" example:"1"`
 | 
				
			||||||
 | 
						ParentPlanID   uint          `json:"parent_plan_id" example:"1"`
 | 
				
			||||||
 | 
						ChildPlanID    uint          `json:"child_plan_id" example:"2"`
 | 
				
			||||||
 | 
						ExecutionOrder int           `json:"execution_order" example:"1"`
 | 
				
			||||||
 | 
						ChildPlan      *PlanResponse `json:"child_plan,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TaskRequest 定义任务请求结构体
 | 
				
			||||||
 | 
					type TaskRequest struct {
 | 
				
			||||||
 | 
						Name           string                 `json:"name" example:"打开风扇"`
 | 
				
			||||||
 | 
						Description    string                 `json:"description" example:"打开1号风扇"`
 | 
				
			||||||
 | 
						ExecutionOrder int                    `json:"execution_order" example:"1"`
 | 
				
			||||||
 | 
						Type           models.TaskType        `json:"type" example:"等待"`
 | 
				
			||||||
 | 
						Parameters     map[string]interface{} `json:"parameters,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TaskResponse 定义任务响应结构体
 | 
				
			||||||
 | 
					type TaskResponse struct {
 | 
				
			||||||
 | 
						ID             int                    `json:"id" example:"1"`
 | 
				
			||||||
 | 
						PlanID         uint                   `json:"plan_id" example:"1"`
 | 
				
			||||||
 | 
						Name           string                 `json:"name" example:"打开风扇"`
 | 
				
			||||||
 | 
						Description    string                 `json:"description" example:"打开1号风扇"`
 | 
				
			||||||
 | 
						ExecutionOrder int                    `json:"execution_order" example:"1"`
 | 
				
			||||||
 | 
						Type           models.TaskType        `json:"type" example:"等待"`
 | 
				
			||||||
 | 
						Parameters     map[string]interface{} `json:"parameters,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										43
									
								
								internal/app/dto/user_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								internal/app/dto/user_dto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					package dto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateUserRequest 定义创建用户请求的结构体
 | 
				
			||||||
 | 
					type CreateUserRequest struct {
 | 
				
			||||||
 | 
						Username string `json:"username" binding:"required" example:"newuser"`
 | 
				
			||||||
 | 
						Password string `json:"password" binding:"required" example:"password123"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LoginRequest 定义登录请求的结构体
 | 
				
			||||||
 | 
					type LoginRequest struct {
 | 
				
			||||||
 | 
						// Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号
 | 
				
			||||||
 | 
						Identifier string `json:"identifier" binding:"required" example:"testuser"`
 | 
				
			||||||
 | 
						Password   string `json:"password" binding:"required" example:"password123"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateUserResponse 定义创建用户成功响应的结构体
 | 
				
			||||||
 | 
					type CreateUserResponse struct {
 | 
				
			||||||
 | 
						Username string `json:"username" example:"newuser"`
 | 
				
			||||||
 | 
						ID       uint   `json:"id" example:"1"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LoginResponse 定义登录成功响应的结构体
 | 
				
			||||||
 | 
					type LoginResponse struct {
 | 
				
			||||||
 | 
						Username string `json:"username" example:"testuser"`
 | 
				
			||||||
 | 
						ID       uint   `json:"id" example:"1"`
 | 
				
			||||||
 | 
						Token    string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HistoryResponse 定义单条操作历史的响应结构体
 | 
				
			||||||
 | 
					type HistoryResponse struct {
 | 
				
			||||||
 | 
						UserID         uint        `json:"user_id" example:"101"`
 | 
				
			||||||
 | 
						Username       string      `json:"username" example:"testuser"`
 | 
				
			||||||
 | 
						ActionType     string      `json:"action_type" example:"更新设备"`
 | 
				
			||||||
 | 
						Description    string      `json:"description" example:"设备更新成功"`
 | 
				
			||||||
 | 
						TargetResource interface{} `json:"target_resource"`
 | 
				
			||||||
 | 
						Time           string      `json:"time"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListHistoryResponse 定义操作历史列表的响应结构体
 | 
				
			||||||
 | 
					type ListHistoryResponse struct {
 | 
				
			||||||
 | 
						History []HistoryResponse `json:"history"`
 | 
				
			||||||
 | 
						Total   int64             `json:"total" example:"100"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -6,7 +6,7 @@ import (
 | 
				
			|||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ import (
 | 
				
			|||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										315
									
								
								internal/app/service/pig_batch_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								internal/app/service/pig_batch_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,315 @@
 | 
				
			|||||||
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
				
			||||||
 | 
						domain_pig "git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchService 接口定义保持不变,继续作为应用层对外的契约。
 | 
				
			||||||
 | 
					type PigBatchService interface {
 | 
				
			||||||
 | 
						CreatePigBatch(operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error)
 | 
				
			||||||
 | 
						GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error)
 | 
				
			||||||
 | 
						UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error)
 | 
				
			||||||
 | 
						DeletePigBatch(id uint) error
 | 
				
			||||||
 | 
						ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Pig Pen Management
 | 
				
			||||||
 | 
						AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error
 | 
				
			||||||
 | 
						ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error
 | 
				
			||||||
 | 
						RemoveEmptyPenFromBatch(batchID uint, penID uint) error
 | 
				
			||||||
 | 
						MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Trade Sub-service
 | 
				
			||||||
 | 
						SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
 | 
				
			||||||
 | 
						BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Transfer Sub-service
 | 
				
			||||||
 | 
						TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
 | 
				
			||||||
 | 
						TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Sick Pig Management
 | 
				
			||||||
 | 
						RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
						RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
						RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
						RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Normal Pig Management
 | 
				
			||||||
 | 
						RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
						RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// pigBatchService 的实现现在依赖于领域服务接口。
 | 
				
			||||||
 | 
					type pigBatchService struct {
 | 
				
			||||||
 | 
						logger        *logs.Logger
 | 
				
			||||||
 | 
						domainService domain_pig.PigBatchService // 依赖注入领域服务
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewPigBatchService 构造函数被修改,以注入领域服务。
 | 
				
			||||||
 | 
					func NewPigBatchService(domainService domain_pig.PigBatchService, logger *logs.Logger) PigBatchService {
 | 
				
			||||||
 | 
						return &pigBatchService{
 | 
				
			||||||
 | 
							logger:        logger,
 | 
				
			||||||
 | 
							domainService: domainService,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// toPigBatchResponseDTO 负责将领域模型转换为应用层DTO,这个职责保留在应用层。
 | 
				
			||||||
 | 
					func (s *pigBatchService) toPigBatchResponseDTO(batch *models.PigBatch) *dto.PigBatchResponseDTO {
 | 
				
			||||||
 | 
						if batch == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &dto.PigBatchResponseDTO{
 | 
				
			||||||
 | 
							ID:           batch.ID,
 | 
				
			||||||
 | 
							BatchNumber:  batch.BatchNumber,
 | 
				
			||||||
 | 
							OriginType:   batch.OriginType,
 | 
				
			||||||
 | 
							StartDate:    batch.StartDate,
 | 
				
			||||||
 | 
							EndDate:      batch.EndDate,
 | 
				
			||||||
 | 
							InitialCount: batch.InitialCount,
 | 
				
			||||||
 | 
							Status:       batch.Status,
 | 
				
			||||||
 | 
							IsActive:     batch.IsActive(),
 | 
				
			||||||
 | 
							CreateTime:   batch.CreatedAt,
 | 
				
			||||||
 | 
							UpdateTime:   batch.UpdatedAt,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigBatch 现在将请求委托给领域服务处理。
 | 
				
			||||||
 | 
					func (s *pigBatchService) CreatePigBatch(operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
 | 
				
			||||||
 | 
						// 1. DTO -> 领域模型
 | 
				
			||||||
 | 
						batch := &models.PigBatch{
 | 
				
			||||||
 | 
							BatchNumber:  dto.BatchNumber,
 | 
				
			||||||
 | 
							OriginType:   dto.OriginType,
 | 
				
			||||||
 | 
							StartDate:    dto.StartDate,
 | 
				
			||||||
 | 
							InitialCount: dto.InitialCount,
 | 
				
			||||||
 | 
							Status:       dto.Status,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 调用领域服务
 | 
				
			||||||
 | 
						createdBatch, err := s.domainService.CreatePigBatch(operatorID, batch)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 创建猪批次失败: %v", err)
 | 
				
			||||||
 | 
							return nil, MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 领域模型 -> DTO
 | 
				
			||||||
 | 
						return s.toPigBatchResponseDTO(createdBatch), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPigBatch 从领域服务获取数据并转换为DTO,同时处理错误转换。
 | 
				
			||||||
 | 
					func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) {
 | 
				
			||||||
 | 
						batch, err := s.domainService.GetPigBatch(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Warnf("应用层: 获取猪批次失败, ID: %d, 错误: %v", id, err)
 | 
				
			||||||
 | 
							return nil, MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return s.toPigBatchResponseDTO(batch), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePigBatch 协调获取、更新和保存的流程,并处理错误转换。
 | 
				
			||||||
 | 
					func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
 | 
				
			||||||
 | 
						// 1. 先获取最新的领域模型
 | 
				
			||||||
 | 
						existingBatch, err := s.domainService.GetPigBatch(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Warnf("应用层: 更新猪批次失败,获取原批次信息错误, ID: %d, 错误: %v", id, err)
 | 
				
			||||||
 | 
							return nil, MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 将DTO中的变更应用到模型上
 | 
				
			||||||
 | 
						if dto.BatchNumber != nil {
 | 
				
			||||||
 | 
							existingBatch.BatchNumber = *dto.BatchNumber
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if dto.OriginType != nil {
 | 
				
			||||||
 | 
							existingBatch.OriginType = *dto.OriginType
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if dto.StartDate != nil {
 | 
				
			||||||
 | 
							existingBatch.StartDate = *dto.StartDate
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if dto.EndDate != nil {
 | 
				
			||||||
 | 
							existingBatch.EndDate = *dto.EndDate
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if dto.InitialCount != nil {
 | 
				
			||||||
 | 
							existingBatch.InitialCount = *dto.InitialCount
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if dto.Status != nil {
 | 
				
			||||||
 | 
							existingBatch.Status = *dto.Status
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 调用领域服务执行更新
 | 
				
			||||||
 | 
						updatedBatch, err := s.domainService.UpdatePigBatch(existingBatch)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 更新猪批次失败, ID: %d, 错误: %v", id, err)
 | 
				
			||||||
 | 
							return nil, MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 4. 转换并返回结果
 | 
				
			||||||
 | 
						return s.toPigBatchResponseDTO(updatedBatch), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeletePigBatch 将删除操作委托给领域服务,并转换领域错误为应用层错误。
 | 
				
			||||||
 | 
					func (s *pigBatchService) DeletePigBatch(id uint) error {
 | 
				
			||||||
 | 
						err := s.domainService.DeletePigBatch(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 删除猪批次失败, ID: %d, 错误: %v", id, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPigBatches 从领域服务获取列表并进行转换。
 | 
				
			||||||
 | 
					func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) {
 | 
				
			||||||
 | 
						batches, err := s.domainService.ListPigBatches(isActive)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 批量查询猪批次失败: %v", err)
 | 
				
			||||||
 | 
							return nil, MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var responseDTOs []*dto.PigBatchResponseDTO
 | 
				
			||||||
 | 
						for _, batch := range batches {
 | 
				
			||||||
 | 
							responseDTOs = append(responseDTOs, s.toPigBatchResponseDTO(batch))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return responseDTOs, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AssignEmptyPensToBatch 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error {
 | 
				
			||||||
 | 
						err := s.domainService.AssignEmptyPensToBatch(batchID, penIDs, operatorID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 为猪批次分配空栏失败, 批次ID: %d, 错误: %v", batchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReclassifyPenToNewBatch 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error {
 | 
				
			||||||
 | 
						err := s.domainService.ReclassifyPenToNewBatch(fromBatchID, toBatchID, penID, operatorID, remarks)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 划拨猪栏到新批次失败, 源批次ID: %d, 错误: %v", fromBatchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RemoveEmptyPenFromBatch 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) error {
 | 
				
			||||||
 | 
						err := s.domainService.RemoveEmptyPenFromBatch(batchID, penID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 从猪批次移除空栏失败, 批次ID: %d, 猪栏ID: %d, 错误: %v", batchID, penID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MovePigsIntoPen 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error {
 | 
				
			||||||
 | 
						err := s.domainService.MovePigsIntoPen(batchID, toPenID, quantity, operatorID, remarks)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 将猪只移入猪栏失败, 批次ID: %d, 目标猪栏ID: %d, 错误: %v", batchID, toPenID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SellPigs 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
 | 
				
			||||||
 | 
						err := s.domainService.SellPigs(batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 卖猪失败, 批次ID: %d, 错误: %v", batchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuyPigs 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
 | 
				
			||||||
 | 
						err := s.domainService.BuyPigs(batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 买猪失败, 批次ID: %d, 错误: %v", batchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TransferPigsAcrossBatches 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
 | 
				
			||||||
 | 
						err := s.domainService.TransferPigsAcrossBatches(sourceBatchID, destBatchID, fromPenID, toPenID, quantity, operatorID, remarks)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 跨群调栏失败, 源批次ID: %d, 错误: %v", sourceBatchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TransferPigsWithinBatch 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
 | 
				
			||||||
 | 
						err := s.domainService.TransferPigsWithinBatch(batchID, fromPenID, toPenID, quantity, operatorID, remarks)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 群内调栏失败, 批次ID: %d, 错误: %v", batchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigs 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						err := s.domainService.RecordSickPigs(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 记录病猪事件失败, 批次ID: %d, 错误: %v", batchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigRecovery 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						err := s.domainService.RecordSickPigRecovery(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 记录病猪康复事件失败, 批次ID: %d, 错误: %v", batchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigDeath 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						err := s.domainService.RecordSickPigDeath(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 记录病猪死亡事件失败, 批次ID: %d, 错误: %v", batchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigCull 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						err := s.domainService.RecordSickPigCull(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 记录病猪淘汰事件失败, 批次ID: %d, 错误: %v", batchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordDeath 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						err := s.domainService.RecordDeath(operatorID, batchID, penID, quantity, happenedAt, remarks)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 记录正常猪只死亡事件失败, 批次ID: %d, 错误: %v", batchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordCull 委托给领域服务
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						err := s.domainService.RecordCull(operatorID, batchID, penID, quantity, happenedAt, remarks)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							s.logger.Errorf("应用层: 记录正常猪只淘汰事件失败, 批次ID: %d, 错误: %v", batchID, err)
 | 
				
			||||||
 | 
							return MapDomainError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										257
									
								
								internal/app/service/pig_farm_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								internal/app/service/pig_farm_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,257 @@
 | 
				
			|||||||
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigFarmService 提供了猪场资产管理的业务逻辑
 | 
				
			||||||
 | 
					type PigFarmService interface {
 | 
				
			||||||
 | 
						// PigHouse methods
 | 
				
			||||||
 | 
						CreatePigHouse(name, description string) (*models.PigHouse, error)
 | 
				
			||||||
 | 
						GetPigHouseByID(id uint) (*models.PigHouse, error)
 | 
				
			||||||
 | 
						ListPigHouses() ([]models.PigHouse, error)
 | 
				
			||||||
 | 
						UpdatePigHouse(id uint, name, description string) (*models.PigHouse, error)
 | 
				
			||||||
 | 
						DeletePigHouse(id uint) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Pen methods
 | 
				
			||||||
 | 
						CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error)
 | 
				
			||||||
 | 
						GetPenByID(id uint) (*models.Pen, error)
 | 
				
			||||||
 | 
						ListPens() ([]models.Pen, error)
 | 
				
			||||||
 | 
						UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error)
 | 
				
			||||||
 | 
						DeletePen(id uint) error
 | 
				
			||||||
 | 
						// UpdatePenStatus 更新猪栏状态
 | 
				
			||||||
 | 
						UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type pigFarmService struct {
 | 
				
			||||||
 | 
						logger          *logs.Logger
 | 
				
			||||||
 | 
						farmRepository  repository.PigFarmRepository
 | 
				
			||||||
 | 
						penRepository   repository.PigPenRepository
 | 
				
			||||||
 | 
						batchRepository repository.PigBatchRepository
 | 
				
			||||||
 | 
						uow             repository.UnitOfWork // 工作单元,用于事务管理
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewPigFarmService 创建一个新的 PigFarmService 实例
 | 
				
			||||||
 | 
					func NewPigFarmService(farmRepository repository.PigFarmRepository,
 | 
				
			||||||
 | 
						penRepository repository.PigPenRepository,
 | 
				
			||||||
 | 
						batchRepository repository.PigBatchRepository,
 | 
				
			||||||
 | 
						uow repository.UnitOfWork,
 | 
				
			||||||
 | 
						logger *logs.Logger) PigFarmService {
 | 
				
			||||||
 | 
						return &pigFarmService{
 | 
				
			||||||
 | 
							logger:          logger,
 | 
				
			||||||
 | 
							farmRepository:  farmRepository,
 | 
				
			||||||
 | 
							penRepository:   penRepository,
 | 
				
			||||||
 | 
							batchRepository: batchRepository,
 | 
				
			||||||
 | 
							uow:             uow,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- PigHouse Implementation ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigFarmService) CreatePigHouse(name, description string) (*models.PigHouse, error) {
 | 
				
			||||||
 | 
						house := &models.PigHouse{
 | 
				
			||||||
 | 
							Name:        name,
 | 
				
			||||||
 | 
							Description: description,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err := s.farmRepository.CreatePigHouse(house)
 | 
				
			||||||
 | 
						return house, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigFarmService) GetPigHouseByID(id uint) (*models.PigHouse, error) {
 | 
				
			||||||
 | 
						return s.farmRepository.GetPigHouseByID(id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigFarmService) ListPigHouses() ([]models.PigHouse, error) {
 | 
				
			||||||
 | 
						return s.farmRepository.ListPigHouses()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*models.PigHouse, error) {
 | 
				
			||||||
 | 
						house := &models.PigHouse{
 | 
				
			||||||
 | 
							Model:       gorm.Model{ID: id},
 | 
				
			||||||
 | 
							Name:        name,
 | 
				
			||||||
 | 
							Description: description,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						rowsAffected, err := s.farmRepository.UpdatePigHouse(house)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if rowsAffected == 0 {
 | 
				
			||||||
 | 
							return nil, ErrHouseNotFound
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 返回更新后的完整信息
 | 
				
			||||||
 | 
						return s.farmRepository.GetPigHouseByID(id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigFarmService) DeletePigHouse(id uint) error {
 | 
				
			||||||
 | 
						// 业务逻辑:检查猪舍是否包含猪栏
 | 
				
			||||||
 | 
						penCount, err := s.farmRepository.CountPensInHouse(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if penCount > 0 {
 | 
				
			||||||
 | 
							return ErrHouseContainsPens
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 调用仓库层进行删除
 | 
				
			||||||
 | 
						rowsAffected, err := s.farmRepository.DeletePigHouse(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if rowsAffected == 0 {
 | 
				
			||||||
 | 
							return ErrHouseNotFound
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Pen Implementation ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error) {
 | 
				
			||||||
 | 
						// 业务逻辑:验证所属猪舍是否存在
 | 
				
			||||||
 | 
						_, err := s.farmRepository.GetPigHouseByID(houseID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return nil, ErrHouseNotFound
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pen := &models.Pen{
 | 
				
			||||||
 | 
							PenNumber: penNumber,
 | 
				
			||||||
 | 
							HouseID:   houseID,
 | 
				
			||||||
 | 
							Capacity:  capacity,
 | 
				
			||||||
 | 
							Status:    models.PenStatusEmpty,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = s.penRepository.CreatePen(pen)
 | 
				
			||||||
 | 
						return pen, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigFarmService) GetPenByID(id uint) (*models.Pen, error) {
 | 
				
			||||||
 | 
						return s.penRepository.GetPenByID(id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigFarmService) ListPens() ([]models.Pen, error) {
 | 
				
			||||||
 | 
						return s.penRepository.ListPens()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) {
 | 
				
			||||||
 | 
						// 业务逻辑:验证所属猪舍是否存在
 | 
				
			||||||
 | 
						_, err := s.farmRepository.GetPigHouseByID(houseID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return nil, ErrHouseNotFound
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pen := &models.Pen{
 | 
				
			||||||
 | 
							Model:     gorm.Model{ID: id},
 | 
				
			||||||
 | 
							PenNumber: penNumber,
 | 
				
			||||||
 | 
							HouseID:   houseID,
 | 
				
			||||||
 | 
							Capacity:  capacity,
 | 
				
			||||||
 | 
							Status:    status,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						rowsAffected, err := s.penRepository.UpdatePen(pen)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if rowsAffected == 0 {
 | 
				
			||||||
 | 
							return nil, ErrPenNotFound
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 返回更新后的完整信息
 | 
				
			||||||
 | 
						return s.penRepository.GetPenByID(id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigFarmService) DeletePen(id uint) error {
 | 
				
			||||||
 | 
						// 业务逻辑:检查猪栏是否被活跃批次使用
 | 
				
			||||||
 | 
						pen, err := s.penRepository.GetPenByID(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return ErrPenNotFound // 猪栏不存在
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 检查猪栏是否关联了活跃批次
 | 
				
			||||||
 | 
						// 注意:pen.PigBatchID 是指针类型,需要检查是否为 nil
 | 
				
			||||||
 | 
						if pen.PigBatchID != nil && *pen.PigBatchID != 0 {
 | 
				
			||||||
 | 
							pigBatch, err := s.batchRepository.GetPigBatchByID(*pen.PigBatchID)
 | 
				
			||||||
 | 
							if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// 如果批次活跃,则不能删除猪栏
 | 
				
			||||||
 | 
							if pigBatch != nil && pigBatch.IsActive() {
 | 
				
			||||||
 | 
								return ErrPenInUse
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 调用仓库层进行删除
 | 
				
			||||||
 | 
						rowsAffected, err := s.penRepository.DeletePen(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if rowsAffected == 0 {
 | 
				
			||||||
 | 
							return ErrPenNotFound
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePenStatus 更新猪栏状态
 | 
				
			||||||
 | 
					func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error) {
 | 
				
			||||||
 | 
						var updatedPen *models.Pen
 | 
				
			||||||
 | 
						err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							pen, err := s.penRepository.GetPenByIDTx(tx, id)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPenNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								s.logger.Errorf("更新猪栏状态失败: 获取猪栏 %d 信息错误: %v", id, err)
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 信息失败: %w", id, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 业务逻辑:根据猪栏的 PigBatchID 和当前状态,判断是否允许设置为 newStatus
 | 
				
			||||||
 | 
							if pen.PigBatchID != nil && *pen.PigBatchID != 0 { // 猪栏已被批次使用
 | 
				
			||||||
 | 
								if newStatus == models.PenStatusEmpty { // 猪栏已被批次使用,不能直接设置为空闲
 | 
				
			||||||
 | 
									return ErrPenStatusInvalidForOccupiedPen
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else { // 猪栏未被批次使用 (PigBatchID == nil)
 | 
				
			||||||
 | 
								if newStatus == models.PenStatusOccupied { // 猪栏未被批次使用,不能设置为使用中
 | 
				
			||||||
 | 
									return ErrPenStatusInvalidForUnoccupiedPen
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 如果新状态与旧状态相同,则无需更新
 | 
				
			||||||
 | 
							if pen.Status == newStatus {
 | 
				
			||||||
 | 
								updatedPen = pen // 返回原始猪栏,因为没有实际更新
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							updates := map[string]interface{}{
 | 
				
			||||||
 | 
								"status": newStatus,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := s.penRepository.UpdatePenFieldsTx(tx, id, updates); err != nil {
 | 
				
			||||||
 | 
								s.logger.Errorf("更新猪栏 %d 状态失败: %v", id, err)
 | 
				
			||||||
 | 
								return fmt.Errorf("更新猪栏 %d 状态失败: %w", id, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 获取更新后的猪栏信息
 | 
				
			||||||
 | 
							updatedPen, err = s.penRepository.GetPenByIDTx(tx, id)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								s.logger.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %v", id, err)
 | 
				
			||||||
 | 
								return fmt.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %w", id, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return updatedPen, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										55
									
								
								internal/app/service/pig_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								internal/app/service/pig_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						domain_pig "git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						ErrHouseContainsPens                = errors.New("无法删除包含猪栏的猪舍")
 | 
				
			||||||
 | 
						ErrHouseNotFound                    = errors.New("指定的猪舍不存在")
 | 
				
			||||||
 | 
						ErrPenInUse                         = errors.New("猪栏正在被活跃批次使用,无法删除")
 | 
				
			||||||
 | 
						ErrPenNotFound                      = errors.New("指定的猪栏不存在")
 | 
				
			||||||
 | 
						ErrPenStatusInvalidForOccupiedPen   = errors.New("猪栏已被批次使用,无法设置为非使用中状态")
 | 
				
			||||||
 | 
						ErrPenStatusInvalidForUnoccupiedPen = errors.New("猪栏未被批次使用,无法设置为使用中状态")
 | 
				
			||||||
 | 
						ErrPigBatchNotFound                 = errors.New("指定的猪批次不存在")
 | 
				
			||||||
 | 
						ErrPigBatchActive                   = errors.New("活跃的猪批次不能被删除")
 | 
				
			||||||
 | 
						ErrPigBatchNotActive                = errors.New("猪批次不处于活跃状态,无法修改关联猪栏")
 | 
				
			||||||
 | 
						ErrPenOccupiedByOtherBatch          = errors.New("猪栏已被其他批次使用")
 | 
				
			||||||
 | 
						ErrPenStatusInvalidForAllocation    = errors.New("猪栏状态不允许分配")
 | 
				
			||||||
 | 
						ErrPenNotAssociatedWithBatch        = errors.New("猪栏未与该批次关联")
 | 
				
			||||||
 | 
						ErrPenNotEmpty                      = errors.New("猪栏内仍有猪只")
 | 
				
			||||||
 | 
						ErrInvalidOperation                 = errors.New("非法操作")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MapDomainError 将领域层的错误转换为应用服务层的公共错误。
 | 
				
			||||||
 | 
					func MapDomainError(err error) error {
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case errors.Is(err, domain_pig.ErrPigBatchNotFound):
 | 
				
			||||||
 | 
							return ErrPigBatchNotFound
 | 
				
			||||||
 | 
						case errors.Is(err, domain_pig.ErrPigBatchActive):
 | 
				
			||||||
 | 
							return ErrPigBatchActive
 | 
				
			||||||
 | 
						case errors.Is(err, domain_pig.ErrPigBatchNotActive):
 | 
				
			||||||
 | 
							return ErrPigBatchNotActive
 | 
				
			||||||
 | 
						case errors.Is(err, domain_pig.ErrPenOccupiedByOtherBatch):
 | 
				
			||||||
 | 
							return ErrPenOccupiedByOtherBatch
 | 
				
			||||||
 | 
						case errors.Is(err, domain_pig.ErrPenStatusInvalidForAllocation):
 | 
				
			||||||
 | 
							return ErrPenStatusInvalidForAllocation
 | 
				
			||||||
 | 
						case errors.Is(err, domain_pig.ErrPenNotAssociatedWithBatch):
 | 
				
			||||||
 | 
							return ErrPenNotAssociatedWithBatch
 | 
				
			||||||
 | 
						case errors.Is(err, domain_pig.ErrPenNotFound):
 | 
				
			||||||
 | 
							return ErrPenNotFound
 | 
				
			||||||
 | 
						case errors.Is(err, domain_pig.ErrPenNotEmpty):
 | 
				
			||||||
 | 
							return ErrPenNotEmpty
 | 
				
			||||||
 | 
						case errors.Is(err, domain_pig.ErrInvalidOperation):
 | 
				
			||||||
 | 
							return ErrInvalidOperation
 | 
				
			||||||
 | 
						// 可以添加更多领域错误到应用层错误的映射
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return err // 对于未知的领域错误,直接返回
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package transport
 | 
					package webhook
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/base64"
 | 
						"encoding/base64"
 | 
				
			||||||
@@ -7,7 +7,7 @@ import (
 | 
				
			|||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device/proto"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/device/proto"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package transport
 | 
					package webhook
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package transport
 | 
					package webhook
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import "net/http"
 | 
					import "net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -8,11 +8,13 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/api"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/api"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/webhook"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/task"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/transport"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/database"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/database"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
@@ -58,44 +60,46 @@ func NewApplication(configPath string) (*Application, error) {
 | 
				
			|||||||
	//  初始化 Token 服务
 | 
						//  初始化 Token 服务
 | 
				
			||||||
	tokenService := token.NewTokenService([]byte(cfg.App.JWTSecret))
 | 
						tokenService := token.NewTokenService([]byte(cfg.App.JWTSecret))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//  初始化用户仓库
 | 
						// --- 仓库对象初始化 ---
 | 
				
			||||||
	userRepo := repository.NewGormUserRepository(storage.GetDB())
 | 
						userRepo := repository.NewGormUserRepository(storage.GetDB())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	//  初始化设备仓库
 | 
					 | 
				
			||||||
	deviceRepo := repository.NewGormDeviceRepository(storage.GetDB())
 | 
						deviceRepo := repository.NewGormDeviceRepository(storage.GetDB())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 初始化区域主控仓库
 | 
					 | 
				
			||||||
	areaControllerRepo := repository.NewGormAreaControllerRepository(storage.GetDB())
 | 
						areaControllerRepo := repository.NewGormAreaControllerRepository(storage.GetDB())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 初始化设备模板仓库
 | 
					 | 
				
			||||||
	deviceTemplateRepo := repository.NewGormDeviceTemplateRepository(storage.GetDB())
 | 
						deviceTemplateRepo := repository.NewGormDeviceTemplateRepository(storage.GetDB())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	//  初始化计划仓库
 | 
					 | 
				
			||||||
	planRepo := repository.NewGormPlanRepository(storage.GetDB())
 | 
						planRepo := repository.NewGormPlanRepository(storage.GetDB())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	//	初始化待执行任务仓库
 | 
					 | 
				
			||||||
	pendingTaskRepo := repository.NewGormPendingTaskRepository(storage.GetDB())
 | 
						pendingTaskRepo := repository.NewGormPendingTaskRepository(storage.GetDB())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	//  初始化执行日志仓库
 | 
					 | 
				
			||||||
	executionLogRepo := repository.NewGormExecutionLogRepository(storage.GetDB())
 | 
						executionLogRepo := repository.NewGormExecutionLogRepository(storage.GetDB())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	//  初始化传感器数据仓库
 | 
					 | 
				
			||||||
	sensorDataRepo := repository.NewGormSensorDataRepository(storage.GetDB())
 | 
						sensorDataRepo := repository.NewGormSensorDataRepository(storage.GetDB())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 初始化命令下发历史仓库
 | 
					 | 
				
			||||||
	deviceCommandLogRepo := repository.NewGormDeviceCommandLogRepository(storage.GetDB())
 | 
						deviceCommandLogRepo := repository.NewGormDeviceCommandLogRepository(storage.GetDB())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 初始化待采集请求仓库
 | 
					 | 
				
			||||||
	pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB())
 | 
						pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 初始化审计日志仓库
 | 
					 | 
				
			||||||
	userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB())
 | 
						userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB())
 | 
				
			||||||
 | 
						pigBatchRepo := repository.NewGormPigBatchRepository(storage.GetDB())
 | 
				
			||||||
 | 
						pigBatchLogRepo := repository.NewGormPigBatchLogRepository(storage.GetDB())
 | 
				
			||||||
 | 
						pigFarmRepo := repository.NewGormPigFarmRepository(storage.GetDB())
 | 
				
			||||||
 | 
						pigPenRepo := repository.NewGormPigPenRepository(storage.GetDB())
 | 
				
			||||||
 | 
						pigTransferLogRepo := repository.NewGormPigTransferLogRepository(storage.GetDB())
 | 
				
			||||||
 | 
						pigTradeRepo := repository.NewGormPigTradeRepository(storage.GetDB())
 | 
				
			||||||
 | 
						pigSickPigLogRepo := repository.NewGormPigSickLogRepository(storage.GetDB())
 | 
				
			||||||
 | 
						medicationLogRepo := repository.NewGormMedicationLogRepository(storage.GetDB())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 初始化事务管理器
 | 
				
			||||||
 | 
						unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 初始化猪群管理领域
 | 
				
			||||||
 | 
						pigPenTransferManager := pig.NewPigPenTransferManager(pigPenRepo, pigTransferLogRepo, pigBatchRepo)
 | 
				
			||||||
 | 
						pigTradeManager := pig.NewPigTradeManager(pigTradeRepo)
 | 
				
			||||||
 | 
						pigSickManager := pig.NewSickPigManager(pigSickPigLogRepo, medicationLogRepo)
 | 
				
			||||||
 | 
						pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigBatchLogRepo, unitOfWork,
 | 
				
			||||||
 | 
							pigPenTransferManager, pigTradeManager, pigSickManager)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// --- 业务逻辑处理器初始化 ---
 | 
				
			||||||
 | 
						pigFarmService := service.NewPigFarmService(pigFarmRepo, pigPenRepo, pigBatchRepo, unitOfWork, logger)
 | 
				
			||||||
 | 
						pigBatchService := service.NewPigBatchService(pigBatchDomain, logger)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 初始化审计服务
 | 
						// 初始化审计服务
 | 
				
			||||||
	auditService := audit.NewService(userActionLogRepo, logger)
 | 
						auditService := audit.NewService(userActionLogRepo, logger)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 初始化设备上行监听器
 | 
						// 初始化设备上行监听器
 | 
				
			||||||
	listenHandler := transport.NewChirpStackListener(logger, sensorDataRepo, deviceRepo, areaControllerRepo, deviceCommandLogRepo, pendingCollectionRepo)
 | 
						listenHandler := webhook.NewChirpStackListener(logger, sensorDataRepo, deviceRepo, areaControllerRepo, deviceCommandLogRepo, pendingCollectionRepo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 初始化计划触发器管理器
 | 
						// 初始化计划触发器管理器
 | 
				
			||||||
	analysisPlanTaskManager := task.NewAnalysisPlanTaskManager(planRepo, pendingTaskRepo, executionLogRepo, logger)
 | 
						analysisPlanTaskManager := task.NewAnalysisPlanTaskManager(planRepo, pendingTaskRepo, executionLogRepo, logger)
 | 
				
			||||||
@@ -135,6 +139,8 @@ func NewApplication(configPath string) (*Application, error) {
 | 
				
			|||||||
		areaControllerRepo,
 | 
							areaControllerRepo,
 | 
				
			||||||
		deviceTemplateRepo,
 | 
							deviceTemplateRepo,
 | 
				
			||||||
		planRepo,
 | 
							planRepo,
 | 
				
			||||||
 | 
							pigFarmService,
 | 
				
			||||||
 | 
							pigBatchService,
 | 
				
			||||||
		userActionLogRepo,
 | 
							userActionLogRepo,
 | 
				
			||||||
		tokenService,
 | 
							tokenService,
 | 
				
			||||||
		auditService,
 | 
							auditService,
 | 
				
			||||||
@@ -263,7 +269,7 @@ func (app *Application) initializePendingTasks(
 | 
				
			|||||||
		if plan.ExecutionType == models.PlanExecutionTypeManual ||
 | 
							if plan.ExecutionType == models.PlanExecutionTypeManual ||
 | 
				
			||||||
			(plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteCount >= plan.ExecuteNum) {
 | 
								(plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteCount >= plan.ExecuteNum) {
 | 
				
			||||||
			// 更新计划状态为已停止
 | 
								// 更新计划状态为已停止
 | 
				
			||||||
			plan.Status = models.PlanStatusStopeed
 | 
								plan.Status = models.PlanStatusStopped
 | 
				
			||||||
			logger.Infof("计划 #%d 状态已更新为 '执行完毕'。", plan.ID)
 | 
								logger.Infof("计划 #%d 状态已更新为 '执行完毕'。", plan.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device/proto"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/device/proto"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
@@ -354,7 +354,7 @@ const file_device_proto_rawDesc = "" +
 | 
				
			|||||||
	"\n" +
 | 
						"\n" +
 | 
				
			||||||
	"MethodType\x12\x0f\n" +
 | 
						"MethodType\x12\x0f\n" +
 | 
				
			||||||
	"\vINSTRUCTION\x10\x00\x12\v\n" +
 | 
						"\vINSTRUCTION\x10\x00\x12\v\n" +
 | 
				
			||||||
	"\aCOLLECT\x10\x01B#Z!internal/app/service/device/protob\x06proto3"
 | 
						"\aCOLLECT\x10\x01B\x1eZ\x1cinternal/domain/device/protob\x06proto3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	file_device_proto_rawDescOnce sync.Once
 | 
						file_device_proto_rawDescOnce sync.Once
 | 
				
			||||||
@@ -4,7 +4,7 @@ package device;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import "google/protobuf/any.proto";
 | 
					import "google/protobuf/any.proto";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
option go_package = "internal/app/service/device/proto";
 | 
					option go_package = "internal/domain/device/proto";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- 通用指令结构 ---
 | 
					// --- 通用指令结构 ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										175
									
								
								internal/domain/pig/pen_transfer_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								internal/domain/pig/pen_transfer_manager.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,175 @@
 | 
				
			|||||||
 | 
					package pig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigPenTransferManager 定义了与猪只位置转移相关的底层数据库操作。
 | 
				
			||||||
 | 
					// 它是一个内部服务,被主服务 PigBatchService 调用。
 | 
				
			||||||
 | 
					type PigPenTransferManager interface {
 | 
				
			||||||
 | 
						// LogTransfer 在数据库中创建一条猪只迁移日志。
 | 
				
			||||||
 | 
						LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// GetPenByID 用于获取猪栏的详细信息,供上层服务进行业务校验。
 | 
				
			||||||
 | 
						GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// GetPensByBatchID 获取一个猪群当前关联的所有猪栏。
 | 
				
			||||||
 | 
						GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// UpdatePenFields 更新一个猪栏的指定字段。
 | 
				
			||||||
 | 
						UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// GetCurrentPigsInPen 通过汇总猪只迁移日志,计算给定猪栏中的当前猪只数量。
 | 
				
			||||||
 | 
						GetCurrentPigsInPen(tx *gorm.DB, penID uint) (int, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数
 | 
				
			||||||
 | 
						GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchID uint) (int, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。
 | 
				
			||||||
 | 
						ReleasePen(tx *gorm.DB, penID uint) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。
 | 
				
			||||||
 | 
					// 它作为调栏管理器,处理底层的数据库交互。
 | 
				
			||||||
 | 
					type pigPenTransferManager struct {
 | 
				
			||||||
 | 
						penRepo      repository.PigPenRepository
 | 
				
			||||||
 | 
						logRepo      repository.PigTransferLogRepository
 | 
				
			||||||
 | 
						pigBatchRepo repository.PigBatchRepository
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewPigPenTransferManager 是 pigPenTransferManager 的构造函数。
 | 
				
			||||||
 | 
					func NewPigPenTransferManager(penRepo repository.PigPenRepository, logRepo repository.PigTransferLogRepository, pigBatchRepo repository.PigBatchRepository) PigPenTransferManager {
 | 
				
			||||||
 | 
						return &pigPenTransferManager{
 | 
				
			||||||
 | 
							penRepo:      penRepo,
 | 
				
			||||||
 | 
							logRepo:      logRepo,
 | 
				
			||||||
 | 
							pigBatchRepo: pigBatchRepo,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LogTransfer 实现了在数据库中创建迁移日志的逻辑。
 | 
				
			||||||
 | 
					func (s *pigPenTransferManager) LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error {
 | 
				
			||||||
 | 
						return s.logRepo.CreatePigTransferLog(tx, log)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPenByID 实现了获取猪栏信息的逻辑。
 | 
				
			||||||
 | 
					func (s *pigPenTransferManager) GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) {
 | 
				
			||||||
 | 
						return s.penRepo.GetPenByIDTx(tx, penID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPensByBatchID 实现了获取猪群关联猪栏列表的逻辑。
 | 
				
			||||||
 | 
					func (s *pigPenTransferManager) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) {
 | 
				
			||||||
 | 
						return s.penRepo.GetPensByBatchIDTx(tx, batchID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePenFields 实现了更新猪栏字段的逻辑。
 | 
				
			||||||
 | 
					func (s *pigPenTransferManager) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error {
 | 
				
			||||||
 | 
						return s.penRepo.UpdatePenFieldsTx(tx, penID, updates)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetCurrentPigsInPen 实现了计算猪栏当前猪只数量的逻辑。
 | 
				
			||||||
 | 
					func (s *pigPenTransferManager) GetCurrentPigsInPen(tx *gorm.DB, penID uint) (int, error) {
 | 
				
			||||||
 | 
						// 1. 通过猪栏ID查出所属猪群信息
 | 
				
			||||||
 | 
						pen, err := s.penRepo.GetPenByIDTx(tx, penID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return 0, ErrPenNotFound
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 如果猪栏没有关联任何猪群,那么猪只数必为0
 | 
				
			||||||
 | 
						if pen.PigBatchID == nil || *pen.PigBatchID == 0 {
 | 
				
			||||||
 | 
							return 0, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						currentBatchID := *pen.PigBatchID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 根据猪群ID获取猪群的起始日期
 | 
				
			||||||
 | 
						batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, currentBatchID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return 0, ErrPigBatchNotFound
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						batchStartDate := batch.StartDate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 调用仓库方法,获取从猪群开始至今,该猪栏的所有倒序日志
 | 
				
			||||||
 | 
						logs, err := s.logRepo.GetLogsForPenSince(tx, penID, batchStartDate)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 如果没有日志,猪只数为0
 | 
				
			||||||
 | 
						if len(logs) == 0 {
 | 
				
			||||||
 | 
							return 0, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 4. 在内存中筛选出最后一段连续日志,并进行计算
 | 
				
			||||||
 | 
						var totalPigs int
 | 
				
			||||||
 | 
						// 再次确认当前猪群ID,以最新的日志为准,防止在极小时间窗口内猪栏被快速切换
 | 
				
			||||||
 | 
						latestBatchID := *pen.PigBatchID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, log := range logs {
 | 
				
			||||||
 | 
							// 一旦发现日志不属于最新的猪群,立即停止计算
 | 
				
			||||||
 | 
							if log.PigBatchID != latestBatchID {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							totalPigs += log.Quantity
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return totalPigs, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数
 | 
				
			||||||
 | 
					// 该方法通过遍历猪群下的每个猪栏,并调用 GetCurrentPigsInPen 来累加存栏数。
 | 
				
			||||||
 | 
					func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchID uint) (int, error) {
 | 
				
			||||||
 | 
						// 1. 获取该批次下所有猪栏的列表
 | 
				
			||||||
 | 
						pensInBatch, err := s.GetPensByBatchID(tx, batchID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("获取猪群 %d 下属猪栏失败: %w", batchID, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						totalPigs := 0
 | 
				
			||||||
 | 
						// 2. 遍历每个猪栏,累加其存栏数
 | 
				
			||||||
 | 
						for _, pen := range pensInBatch {
 | 
				
			||||||
 | 
							pigsInPen, err := s.GetCurrentPigsInPen(tx, pen.ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return 0, fmt.Errorf("获取猪栏 %d 存栏数失败: %w", pen.ID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							totalPigs += pigsInPen
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return totalPigs, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。
 | 
				
			||||||
 | 
					// 此操作通常在猪栏被清空后调用。
 | 
				
			||||||
 | 
					func (s *pigPenTransferManager) ReleasePen(tx *gorm.DB, penID uint) error {
 | 
				
			||||||
 | 
						// 1. 获取猪栏信息
 | 
				
			||||||
 | 
						pen, err := s.penRepo.GetPenByIDTx(tx, penID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 更新猪栏字段
 | 
				
			||||||
 | 
						// 将 pig_batch_id 设置为 nil (SQL NULL)
 | 
				
			||||||
 | 
						// 将 status 设置为 PenStatusEmpty
 | 
				
			||||||
 | 
						updates := map[string]interface{}{
 | 
				
			||||||
 | 
							"pig_batch_id": nil, // 使用 nil 来表示 SQL NULL
 | 
				
			||||||
 | 
							"status":       models.PenStatusEmpty,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := s.penRepo.UpdatePenFieldsTx(tx, penID, updates); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("释放猪栏 %v 失败: %w", pen.PenNumber, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										165
									
								
								internal/domain/pig/pig_batch_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								internal/domain/pig/pig_batch_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
				
			|||||||
 | 
					package pig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- 业务错误定义 ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						// ErrPigBatchNotFound 表示当尝试访问一个不存在的猪批次时发生的错误。
 | 
				
			||||||
 | 
						ErrPigBatchNotFound = errors.New("指定的猪批次不存在")
 | 
				
			||||||
 | 
						// ErrPigBatchActive 表示当尝试对一个活跃的猪批次执行不允许的操作(如删除)时发生的错误。
 | 
				
			||||||
 | 
						ErrPigBatchActive = errors.New("活跃的猪批次不能被删除")
 | 
				
			||||||
 | 
						// ErrPigBatchNotActive 表示当猪批次不处于活跃状态,但执行了需要其活跃的操作时发生的错误。
 | 
				
			||||||
 | 
						ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏")
 | 
				
			||||||
 | 
						// ErrPenOccupiedByOtherBatch 表示当尝试将一个已经被其他批次占用的猪栏分配给新批次时发生的错误。
 | 
				
			||||||
 | 
						ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次使用")
 | 
				
			||||||
 | 
						// ErrPenStatusInvalidForAllocation 表示猪栏的当前状态(例如,'维修中')不允许被分配。
 | 
				
			||||||
 | 
						ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配")
 | 
				
			||||||
 | 
						// ErrPenNotFound 表示猪栏不存在
 | 
				
			||||||
 | 
						ErrPenNotFound = errors.New("指定的猪栏不存在")
 | 
				
			||||||
 | 
						// ErrPenNotAssociatedWithBatch 表示猪栏未与该批次关联
 | 
				
			||||||
 | 
						ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联")
 | 
				
			||||||
 | 
						// ErrPenNotEmpty 表示猪栏内仍有猪只,不允许执行当前操作。
 | 
				
			||||||
 | 
						ErrPenNotEmpty = errors.New("猪栏内仍有猪只,无法执行此操作")
 | 
				
			||||||
 | 
						// ErrInvalidOperation 非法操作
 | 
				
			||||||
 | 
						ErrInvalidOperation = errors.New("非法操作")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- 领域服务接口 ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchService 定义了猪批次管理的核心业务逻辑接口。
 | 
				
			||||||
 | 
					// 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。
 | 
				
			||||||
 | 
					type PigBatchService interface {
 | 
				
			||||||
 | 
						// CreatePigBatch 创建猪批次,并记录初始日志。
 | 
				
			||||||
 | 
						CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error)
 | 
				
			||||||
 | 
						// GetPigBatch 获取单个猪批次。
 | 
				
			||||||
 | 
						GetPigBatch(id uint) (*models.PigBatch, error)
 | 
				
			||||||
 | 
						// UpdatePigBatch 更新猪批次信息。
 | 
				
			||||||
 | 
						UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error)
 | 
				
			||||||
 | 
						// DeletePigBatch 删除猪批次,包含业务规则校验。
 | 
				
			||||||
 | 
						DeletePigBatch(id uint) error
 | 
				
			||||||
 | 
						// ListPigBatches 批量查询猪批次。
 | 
				
			||||||
 | 
						ListPigBatches(isActive *bool) ([]*models.PigBatch, error)
 | 
				
			||||||
 | 
						// AssignEmptyPensToBatch 为猪群分配空栏
 | 
				
			||||||
 | 
						AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error
 | 
				
			||||||
 | 
						// MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏
 | 
				
			||||||
 | 
						MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error
 | 
				
			||||||
 | 
						// ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群
 | 
				
			||||||
 | 
						ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error
 | 
				
			||||||
 | 
						// RemoveEmptyPenFromBatch 将一个猪栏移除出猪群,此方法需要在猪栏为空的情况下执行。
 | 
				
			||||||
 | 
						RemoveEmptyPenFromBatch(batchID uint, penID uint) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。
 | 
				
			||||||
 | 
						GetCurrentPigQuantity(batchID uint) (int, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ---交易子服务---
 | 
				
			||||||
 | 
						// SellPigs 处理卖猪的业务逻辑。
 | 
				
			||||||
 | 
						SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
 | 
				
			||||||
 | 
						// BuyPigs 处理买猪的业务逻辑。
 | 
				
			||||||
 | 
						BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ---调栏子服务 ---
 | 
				
			||||||
 | 
						TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
 | 
				
			||||||
 | 
						TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// --- 病猪管理相关方法 ---
 | 
				
			||||||
 | 
						// RecordSickPigs 记录新增病猪事件。
 | 
				
			||||||
 | 
						RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
						// RecordSickPigRecovery 记录病猪康复事件。
 | 
				
			||||||
 | 
						RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
						// RecordSickPigDeath 记录病猪死亡事件。
 | 
				
			||||||
 | 
						RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
						// RecordSickPigCull 记录病猪淘汰事件。
 | 
				
			||||||
 | 
						RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// --- 正常猪只管理相关方法 ---
 | 
				
			||||||
 | 
						// RecordDeath 记录正常猪只死亡事件。
 | 
				
			||||||
 | 
						RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
						// RecordCull 记录正常猪只淘汰事件。
 | 
				
			||||||
 | 
						RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// pigBatchService 是 PigBatchService 接口的具体实现。
 | 
				
			||||||
 | 
					// 它作为猪群领域的主服务,封装了所有业务逻辑。
 | 
				
			||||||
 | 
					type pigBatchService struct {
 | 
				
			||||||
 | 
						pigBatchRepo    repository.PigBatchRepository    // 猪批次仓库
 | 
				
			||||||
 | 
						pigBatchLogRepo repository.PigBatchLogRepository // 猪批次日志仓库
 | 
				
			||||||
 | 
						uow             repository.UnitOfWork            // 工作单元,用于管理事务
 | 
				
			||||||
 | 
						transferSvc     PigPenTransferManager            // 调栏子服务
 | 
				
			||||||
 | 
						tradeSvc        PigTradeManager                  // 交易子服务
 | 
				
			||||||
 | 
						sickSvc         SickPigManager                   // 病猪子服务
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewPigBatchService 是 pigBatchService 的构造函数。
 | 
				
			||||||
 | 
					// 它通过依赖注入的方式,创建并返回一个 PigBatchService 接口的实例。
 | 
				
			||||||
 | 
					func NewPigBatchService(
 | 
				
			||||||
 | 
						pigBatchRepo repository.PigBatchRepository,
 | 
				
			||||||
 | 
						pigBatchLogRepo repository.PigBatchLogRepository,
 | 
				
			||||||
 | 
						uow repository.UnitOfWork,
 | 
				
			||||||
 | 
						transferSvc PigPenTransferManager,
 | 
				
			||||||
 | 
						tradeSvc PigTradeManager,
 | 
				
			||||||
 | 
						sickSvc SickPigManager,
 | 
				
			||||||
 | 
					) PigBatchService {
 | 
				
			||||||
 | 
						return &pigBatchService{
 | 
				
			||||||
 | 
							pigBatchRepo:    pigBatchRepo,
 | 
				
			||||||
 | 
							pigBatchLogRepo: pigBatchLogRepo,
 | 
				
			||||||
 | 
							uow:             uow,
 | 
				
			||||||
 | 
							transferSvc:     transferSvc,
 | 
				
			||||||
 | 
							tradeSvc:        tradeSvc,
 | 
				
			||||||
 | 
							sickSvc:         sickSvc,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) error {
 | 
				
			||||||
 | 
						return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 检查猪批次是否存在且活跃
 | 
				
			||||||
 | 
							batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPigBatchNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !batch.IsActive() {
 | 
				
			||||||
 | 
								return ErrPigBatchNotActive
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 检查猪栏是否存在
 | 
				
			||||||
 | 
							pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPenNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 检查猪栏是否与当前批次关联
 | 
				
			||||||
 | 
							if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return ErrPenNotAssociatedWithBatch
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 检查猪栏是否为空
 | 
				
			||||||
 | 
							pigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if pigsInPen > 0 {
 | 
				
			||||||
 | 
								return ErrPenNotEmpty
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 5. 释放猪栏 (将 pig_batch_id 设置为 nil,状态设置为空闲)
 | 
				
			||||||
 | 
							if err := s.transferSvc.ReleasePen(tx, penID); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										196
									
								
								internal/domain/pig/pig_batch_service_method.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								internal/domain/pig/pig_batch_service_method.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,196 @@
 | 
				
			|||||||
 | 
					package pig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- 领域服务实现 ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigBatch 实现了创建猪批次的逻辑,并同时创建初始批次日志。
 | 
				
			||||||
 | 
					func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) {
 | 
				
			||||||
 | 
						// 业务规则可以在这里添加,例如检查批次号是否唯一等
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var createdBatch *models.PigBatch
 | 
				
			||||||
 | 
						err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 创建猪批次
 | 
				
			||||||
 | 
							// 注意: 此处依赖一个假设存在的 pigBatchRepo.CreatePigBatchTx 方法
 | 
				
			||||||
 | 
							var err error
 | 
				
			||||||
 | 
							createdBatch, err = s.pigBatchRepo.CreatePigBatchTx(tx, batch)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("创建猪批次失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 创建初始批次日志
 | 
				
			||||||
 | 
							initialLog := &models.PigBatchLog{
 | 
				
			||||||
 | 
								PigBatchID:  createdBatch.ID,
 | 
				
			||||||
 | 
								HappenedAt:  time.Now(),
 | 
				
			||||||
 | 
								ChangeType:  models.ChangeTypeCorrection, // 初始创建可视为一种校正
 | 
				
			||||||
 | 
								ChangeCount: createdBatch.InitialCount,
 | 
				
			||||||
 | 
								Reason:      fmt.Sprintf("创建了新的猪批次 %s,初始数量 %d", createdBatch.BatchNumber, createdBatch.InitialCount),
 | 
				
			||||||
 | 
								BeforeCount: 0, // 初始创建前数量为0
 | 
				
			||||||
 | 
								AfterCount:  createdBatch.InitialCount,
 | 
				
			||||||
 | 
								OperatorID:  operatorID,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 记录批次日志
 | 
				
			||||||
 | 
							if err := s.pigBatchLogRepo.CreateTx(tx, initialLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("记录初始批次日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return createdBatch, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPigBatch 实现了获取单个猪批次的逻辑。
 | 
				
			||||||
 | 
					func (s *pigBatchService) GetPigBatch(id uint) (*models.PigBatch, error) {
 | 
				
			||||||
 | 
						batch, err := s.pigBatchRepo.GetPigBatchByID(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return nil, ErrPigBatchNotFound
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return batch, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePigBatch 实现了更新猪批次的逻辑。
 | 
				
			||||||
 | 
					func (s *pigBatchService) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) {
 | 
				
			||||||
 | 
						// 可以在这里添加更新前的业务校验
 | 
				
			||||||
 | 
						updatedBatch, rowsAffected, err := s.pigBatchRepo.UpdatePigBatch(batch)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if rowsAffected == 0 {
 | 
				
			||||||
 | 
							return nil, ErrPigBatchNotFound // 如果没有行被更新,可能意味着记录不存在
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return updatedBatch, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeletePigBatch 实现了删除猪批次的逻辑,并包含业务规则校验。
 | 
				
			||||||
 | 
					func (s *pigBatchService) DeletePigBatch(id uint) error {
 | 
				
			||||||
 | 
						return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 获取猪批次信息
 | 
				
			||||||
 | 
							batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, id) // 使用事务内方法
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPigBatchNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 核心业务规则:检查猪批次是否为活跃状态
 | 
				
			||||||
 | 
							if batch.IsActive() {
 | 
				
			||||||
 | 
								return ErrPigBatchActive // 如果活跃,则不允许删除
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 释放所有关联的猪栏
 | 
				
			||||||
 | 
							// 获取该批次下所有猪栏
 | 
				
			||||||
 | 
							pensInBatch, err := s.transferSvc.GetPensByBatchID(tx, id)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪批次 %d 关联猪栏失败: %w", id, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 逐一释放猪栏
 | 
				
			||||||
 | 
							for _, pen := range pensInBatch {
 | 
				
			||||||
 | 
								if err := s.transferSvc.ReleasePen(tx, pen.ID); err != nil {
 | 
				
			||||||
 | 
									return fmt.Errorf("释放猪栏 %d 失败: %w", pen.ID, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 执行删除猪批次
 | 
				
			||||||
 | 
							rowsAffected, err := s.pigBatchRepo.DeletePigBatchTx(tx, id)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if rowsAffected == 0 {
 | 
				
			||||||
 | 
								return ErrPigBatchNotFound
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPigBatches 实现了批量查询猪批次的逻辑。
 | 
				
			||||||
 | 
					func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) {
 | 
				
			||||||
 | 
						return s.pigBatchRepo.ListPigBatches(isActive)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetCurrentPigQuantity 实现了获取指定猪批次的当前猪只数量的逻辑。
 | 
				
			||||||
 | 
					func (s *pigBatchService) GetCurrentPigQuantity(batchID uint) (int, error) {
 | 
				
			||||||
 | 
						var getErr error
 | 
				
			||||||
 | 
						var quantity int
 | 
				
			||||||
 | 
						err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							quantity, getErr = s.getCurrentPigQuantityTx(tx, batchID)
 | 
				
			||||||
 | 
							return getErr
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return quantity, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getCurrentPigQuantityTx 实现了获取指定猪批次的当前猪只数量的逻辑。
 | 
				
			||||||
 | 
					func (s *pigBatchService) getCurrentPigQuantityTx(tx *gorm.DB, batchID uint) (int, error) {
 | 
				
			||||||
 | 
						// 1. 获取猪批次初始信息
 | 
				
			||||||
 | 
						batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return 0, ErrPigBatchNotFound
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("获取猪批次 %d 初始信息失败: %w", batchID, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 尝试获取该批次的最后一条日志记录
 | 
				
			||||||
 | 
						lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								// 如果没有找到任何日志记录(除了初始创建),则当前数量就是初始数量
 | 
				
			||||||
 | 
								return batch.InitialCount, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("获取猪批次 %d 最后一条日志失败: %w", batchID, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 如果找到最后一条日志,则当前数量为该日志的 AfterCount
 | 
				
			||||||
 | 
						return lastLog.AfterCount, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigBatchService) UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error {
 | 
				
			||||||
 | 
						return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							return s.updatePigBatchQuantityTx(tx, operatorID, batchID, changeType, changeAmount, changeReason, happenedAt)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *pigBatchService) updatePigBatchQuantityTx(tx *gorm.DB, operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error {
 | 
				
			||||||
 | 
						lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 检查数量不应该减到小于零
 | 
				
			||||||
 | 
						if changeAmount < 0 {
 | 
				
			||||||
 | 
							if lastLog.AfterCount+changeAmount < 0 {
 | 
				
			||||||
 | 
								return ErrInvalidOperation
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						pigBatchLog := &models.PigBatchLog{
 | 
				
			||||||
 | 
							PigBatchID:  batchID,
 | 
				
			||||||
 | 
							ChangeType:  changeType,
 | 
				
			||||||
 | 
							ChangeCount: changeAmount,
 | 
				
			||||||
 | 
							Reason:      changeReason,
 | 
				
			||||||
 | 
							BeforeCount: lastLog.AfterCount,
 | 
				
			||||||
 | 
							AfterCount:  lastLog.AfterCount + changeAmount,
 | 
				
			||||||
 | 
							OperatorID:  operatorID,
 | 
				
			||||||
 | 
							HappenedAt:  happenedAt,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s.pigBatchLogRepo.CreateTx(tx, pigBatchLog)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										383
									
								
								internal/domain/pig/pig_batch_service_pen_transfer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										383
									
								
								internal/domain/pig/pig_batch_service_pen_transfer.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,383 @@
 | 
				
			|||||||
 | 
					package pig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// executeTransferAndLog 是一个私有辅助方法,用于封装创建和记录迁移日志的通用逻辑。
 | 
				
			||||||
 | 
					func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatchID, fromPenID, toPenID uint, quantity int, transferType models.PigTransferType, operatorID uint, remarks string) error {
 | 
				
			||||||
 | 
						// 通用校验:任何调出操作都不能超过源猪栏的当前存栏数
 | 
				
			||||||
 | 
						if quantity < 0 { // 当调出时才需要检查
 | 
				
			||||||
 | 
							currentPigsInFromPen, err := s.transferSvc.GetCurrentPigsInPen(tx, fromPenID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取源猪栏 %d 当前猪只数失败: %w", fromPenID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if currentPigsInFromPen+quantity < 0 {
 | 
				
			||||||
 | 
								return fmt.Errorf("调出数量 %d 超过源猪栏 %d 当前存栏数 %d", -quantity, fromPenID, currentPigsInFromPen)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 1. 生成关联ID
 | 
				
			||||||
 | 
						correlationID := uuid.New().String()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 创建调出日志
 | 
				
			||||||
 | 
						logOut := &models.PigTransferLog{
 | 
				
			||||||
 | 
							TransferTime:  time.Now(),
 | 
				
			||||||
 | 
							PigBatchID:    fromBatchID,
 | 
				
			||||||
 | 
							PenID:         fromPenID,
 | 
				
			||||||
 | 
							Quantity:      -quantity, // 调出为负数
 | 
				
			||||||
 | 
							Type:          transferType,
 | 
				
			||||||
 | 
							CorrelationID: correlationID,
 | 
				
			||||||
 | 
							OperatorID:    operatorID,
 | 
				
			||||||
 | 
							Remarks:       remarks,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 创建调入日志
 | 
				
			||||||
 | 
						logIn := &models.PigTransferLog{
 | 
				
			||||||
 | 
							TransferTime:  time.Now(),
 | 
				
			||||||
 | 
							PigBatchID:    toBatchID,
 | 
				
			||||||
 | 
							PenID:         toPenID,
 | 
				
			||||||
 | 
							Quantity:      quantity, // 调入为正数
 | 
				
			||||||
 | 
							Type:          transferType,
 | 
				
			||||||
 | 
							CorrelationID: correlationID,
 | 
				
			||||||
 | 
							OperatorID:    operatorID,
 | 
				
			||||||
 | 
							Remarks:       remarks,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 4. 调用子服务记录日志
 | 
				
			||||||
 | 
						if err := s.transferSvc.LogTransfer(tx, logOut); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("记录调出日志失败: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := s.transferSvc.LogTransfer(tx, logIn); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("记录调入日志失败: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TransferPigsWithinBatch 实现了同一个猪群内部的调栏业务。
 | 
				
			||||||
 | 
					func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
 | 
				
			||||||
 | 
						if fromPenID == toPenID {
 | 
				
			||||||
 | 
							return errors.New("源猪栏和目标猪栏不能相同")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if quantity == 0 {
 | 
				
			||||||
 | 
							return errors.New("迁移数量不能为零")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 核心业务规则校验
 | 
				
			||||||
 | 
							fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取源猪栏信息失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							toPen, err := s.transferSvc.GetPenByID(tx, toPenID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取目标猪栏信息失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if fromPen.PigBatchID == nil || *fromPen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("源猪栏 %d 不属于指定的猪群 %d", fromPenID, batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if toPen.PigBatchID != nil && *toPen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("目标猪栏 %d 已被其他猪群占用", toPenID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 调用通用辅助方法执行日志记录
 | 
				
			||||||
 | 
							err = s.executeTransferAndLog(tx, batchID, batchID, fromPenID, toPenID, int(quantity), "群内调栏", operatorID, remarks)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 群内调栏,猪群总数不变
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TransferPigsAcrossBatches 实现了跨猪群的调栏业务。
 | 
				
			||||||
 | 
					func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
 | 
				
			||||||
 | 
						if sourceBatchID == destBatchID {
 | 
				
			||||||
 | 
							return errors.New("源猪群和目标猪群不能相同")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if quantity == 0 {
 | 
				
			||||||
 | 
							return errors.New("迁移数量不能为零")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 核心业务规则校验
 | 
				
			||||||
 | 
							// 1.1 校验猪群存在
 | 
				
			||||||
 | 
							if _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, sourceBatchID); err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return fmt.Errorf("源猪群 %d 不存在", sourceBatchID)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取源猪群信息失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, destBatchID); err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return fmt.Errorf("目标猪群 %d 不存在", destBatchID)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取目标猪群信息失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 1.2 校验猪栏归属
 | 
				
			||||||
 | 
							fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取源猪栏信息失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if fromPen.PigBatchID == nil || *fromPen.PigBatchID != sourceBatchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("源猪栏 %d 不属于源猪群 %d", fromPenID, sourceBatchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 调用通用辅助方法执行猪只物理转移的日志记录
 | 
				
			||||||
 | 
							err = s.executeTransferAndLog(tx, sourceBatchID, destBatchID, fromPenID, toPenID, int(quantity), "跨群调栏", operatorID, remarks)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 通过创建批次日志来修改猪群总数,确保数据可追溯
 | 
				
			||||||
 | 
							now := time.Now()
 | 
				
			||||||
 | 
							// 3.1 记录源猪群数量减少
 | 
				
			||||||
 | 
							reasonOut := fmt.Sprintf("跨群调栏: %d头猪从批次 %d 调出至批次 %d。备注: %s", quantity, sourceBatchID, destBatchID, remarks)
 | 
				
			||||||
 | 
							err = s.updatePigBatchQuantityTx(tx, operatorID, sourceBatchID, models.ChangeTypeTransferOut, -int(quantity), reasonOut, now)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新源猪群 %d 数量失败: %w", sourceBatchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3.2 记录目标猪群数量增加
 | 
				
			||||||
 | 
							reasonIn := fmt.Sprintf("跨群调栏: %d头猪从批次 %d 调入。备注: %s", quantity, sourceBatchID, remarks)
 | 
				
			||||||
 | 
							err = s.updatePigBatchQuantityTx(tx, operatorID, destBatchID, models.ChangeTypeTransferIn, int(quantity), reasonIn, now)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新目标猪群 %d 数量失败: %w", destBatchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AssignEmptyPensToBatch 为猪群分配空栏
 | 
				
			||||||
 | 
					func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error {
 | 
				
			||||||
 | 
						return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 验证猪批次是否存在且活跃
 | 
				
			||||||
 | 
							pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPigBatchNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪批次信息失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !pigBatch.IsActive() {
 | 
				
			||||||
 | 
								return ErrPigBatchNotActive
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 遍历并校验每一个待分配的猪栏
 | 
				
			||||||
 | 
							for _, penID := range penIDs {
 | 
				
			||||||
 | 
								pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
										return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 核心业务规则:校验猪栏是否完全空闲
 | 
				
			||||||
 | 
								if pen.Status != models.PenStatusEmpty {
 | 
				
			||||||
 | 
									return fmt.Errorf("猪栏 %s 状态不为空 (%s),无法分配", pen.PenNumber, pen.Status)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if pen.PigBatchID != nil {
 | 
				
			||||||
 | 
									return fmt.Errorf("猪栏 %s 已被其他批次 %d 占用,无法分配", pen.PenNumber, *pen.PigBatchID)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 3. 更新猪栏的归属
 | 
				
			||||||
 | 
								updates := map[string]interface{}{
 | 
				
			||||||
 | 
									"pig_batch_id": &batchID,
 | 
				
			||||||
 | 
									"status":       models.PenStatusOccupied,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil {
 | 
				
			||||||
 | 
									return fmt.Errorf("分配猪栏 %d 失败: %w", penID, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏
 | 
				
			||||||
 | 
					func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error {
 | 
				
			||||||
 | 
						if quantity <= 0 {
 | 
				
			||||||
 | 
							return errors.New("迁移数量必须大于零")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 验证猪批次是否存在且活跃
 | 
				
			||||||
 | 
							pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPigBatchNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪批次信息失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !pigBatch.IsActive() {
 | 
				
			||||||
 | 
								return ErrPigBatchNotActive
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 校验目标猪栏
 | 
				
			||||||
 | 
							toPen, err := s.transferSvc.GetPenByID(tx, toPenID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return fmt.Errorf("目标猪栏 %d 不存在: %w", toPenID, ErrPenNotFound)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取目标猪栏 %d 信息失败: %w", toPenID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 校验目标猪栏的归属和状态
 | 
				
			||||||
 | 
							if toPen.PigBatchID == nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("目标猪栏 %s 不属于当前批次 %s", toPen.PenNumber, batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if toPen.PigBatchID != nil && *toPen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("目标猪栏 %s 已被其他批次 %d 占用,无法移入", toPen.PenNumber, *toPen.PigBatchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 校验猪群中有足够的“未分配”猪只
 | 
				
			||||||
 | 
							currentBatchTotal, err := s.getCurrentPigQuantityTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪群 %d 当前总数量失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 获取该批次下所有猪栏的当前总存栏数
 | 
				
			||||||
 | 
							totalPigsInPens, err := s.transferSvc.GetTotalPigsInPensForBatchTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("计算猪群 %d 下属猪栏总存栏失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							unassignedPigs := currentBatchTotal - totalPigsInPens
 | 
				
			||||||
 | 
							if unassignedPigs < quantity {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪群 %d 未分配猪只不足,当前未分配 %d 头,需要移入 %d 头", batchID, unassignedPigs, quantity)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 记录转移日志
 | 
				
			||||||
 | 
							logIn := &models.PigTransferLog{
 | 
				
			||||||
 | 
								TransferTime: time.Now(),
 | 
				
			||||||
 | 
								PigBatchID:   batchID,
 | 
				
			||||||
 | 
								PenID:        toPenID,
 | 
				
			||||||
 | 
								Quantity:     quantity,                       // 调入为正数
 | 
				
			||||||
 | 
								Type:         models.PigTransferTypeInternal, // 首次入栏
 | 
				
			||||||
 | 
								OperatorID:   operatorID,
 | 
				
			||||||
 | 
								Remarks:      remarks,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.transferSvc.LogTransfer(tx, logIn); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("记录入栏日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群
 | 
				
			||||||
 | 
					func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error {
 | 
				
			||||||
 | 
						if fromBatchID == toBatchID {
 | 
				
			||||||
 | 
							return errors.New("源猪群和目标猪群不能相同")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 核心业务规则校验
 | 
				
			||||||
 | 
							// 1.1 校验猪群存在
 | 
				
			||||||
 | 
							fromBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, fromBatchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return fmt.Errorf("源猪群 %d 不存在", fromBatchID)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取源猪群信息失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							toBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, toBatchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return fmt.Errorf("目标猪群 %d 不存在", toBatchID)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取目标猪群信息失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 1.2 校验猪栏归属
 | 
				
			||||||
 | 
							pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if pen.PigBatchID == nil || *pen.PigBatchID != fromBatchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %v 不属于源猪群 %v,无法划拨", pen.PenNumber, fromBatch.BatchNumber)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 获取猪栏当前存栏数
 | 
				
			||||||
 | 
							quantity, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %v 存栏数失败: %w", pen.PenNumber, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 更新猪栏的归属
 | 
				
			||||||
 | 
							updates := map[string]interface{}{
 | 
				
			||||||
 | 
								"pig_batch_id": &toBatchID,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新猪栏 %v 归属失败: %w", pen.PenNumber, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// 如果猪栏是空的,则只进行归属变更,不影响猪群数量
 | 
				
			||||||
 | 
							if quantity == 0 {
 | 
				
			||||||
 | 
								return nil // 空栏划拨,不涉及猪只数量变更
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 记录猪只从旧批次“迁出”的猪栏日志
 | 
				
			||||||
 | 
							correlationID := uuid.New().String()
 | 
				
			||||||
 | 
							logOut := &models.PigTransferLog{
 | 
				
			||||||
 | 
								TransferTime:  time.Now(),
 | 
				
			||||||
 | 
								PigBatchID:    fromBatchID,
 | 
				
			||||||
 | 
								PenID:         penID,
 | 
				
			||||||
 | 
								Quantity:      -quantity, // 迁出为负数
 | 
				
			||||||
 | 
								Type:          models.PigTransferTypeCrossBatch,
 | 
				
			||||||
 | 
								CorrelationID: correlationID,
 | 
				
			||||||
 | 
								OperatorID:    operatorID,
 | 
				
			||||||
 | 
								Remarks:       fmt.Sprintf("整栏划拨迁出: %d头猪从批次 %v 随猪栏 %v 划拨至批次 %v。备注: %s", quantity, fromBatch.BatchNumber, pen.PenNumber, toBatch.BatchNumber, remarks),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.transferSvc.LogTransfer(tx, logOut); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("记录猪栏 %d 迁出日志失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 5. 记录猪只到新批次“迁入”的猪栏日志
 | 
				
			||||||
 | 
							logIn := &models.PigTransferLog{
 | 
				
			||||||
 | 
								TransferTime:  time.Now(),
 | 
				
			||||||
 | 
								PigBatchID:    toBatchID,
 | 
				
			||||||
 | 
								PenID:         penID,
 | 
				
			||||||
 | 
								Quantity:      quantity, // 迁入为正数
 | 
				
			||||||
 | 
								Type:          models.PigTransferTypeCrossBatch,
 | 
				
			||||||
 | 
								CorrelationID: correlationID,
 | 
				
			||||||
 | 
								OperatorID:    operatorID,
 | 
				
			||||||
 | 
								Remarks:       fmt.Sprintf("整栏划拨迁入: %v头猪随猪栏 %v 从批次 %v 划拨入。备注: %s", quantity, fromBatch.BatchNumber, pen.PenNumber, remarks),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.transferSvc.LogTransfer(tx, logIn); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("记录猪栏 %d 迁入日志失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 7. 通过创建批次日志来修改猪群总数,确保数据可追溯
 | 
				
			||||||
 | 
							now := time.Now()
 | 
				
			||||||
 | 
							// 7.1 记录源猪群数量减少
 | 
				
			||||||
 | 
							reasonOutBatch := fmt.Sprintf("整栏划拨: %d头猪随猪栏 %v 从批次 %v 划拨至批次 %v。备注: %s", quantity, pen.PenNumber, fromBatch.BatchNumber, toBatchID, remarks)
 | 
				
			||||||
 | 
							err = s.updatePigBatchQuantityTx(tx, operatorID, fromBatchID, models.ChangeTypeTransferOut, -quantity, reasonOutBatch, now)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新源猪群 %v 数量失败: %w", fromBatch.BatchNumber, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 7.2 记录目标猪群数量增加
 | 
				
			||||||
 | 
							reasonInBatch := fmt.Sprintf("整栏划拨: %v头猪随猪栏 %v 从批次 %v 划拨入。备注: %s", quantity, pen.PenNumber, fromBatch.BatchNumber, remarks)
 | 
				
			||||||
 | 
							err = s.updatePigBatchQuantityTx(tx, operatorID, toBatchID, models.ChangeTypeTransferIn, quantity, reasonInBatch, now)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新目标猪群 %v 数量失败: %w", toBatch.BatchNumber, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										483
									
								
								internal/domain/pig/pig_batch_service_pig_sick.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										483
									
								
								internal/domain/pig/pig_batch_service_pig_sick.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,483 @@
 | 
				
			|||||||
 | 
					package pig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigs 记录新增病猪事件。
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						if quantity <= 0 {
 | 
				
			||||||
 | 
							return errors.New("新增病猪数量必须大于0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						// 1. 开启事务
 | 
				
			||||||
 | 
						err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1.1 检查批次是否活跃
 | 
				
			||||||
 | 
							batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPigBatchNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !batch.IsActive() {
 | 
				
			||||||
 | 
								return fmt.Errorf("批次 %d 不活跃,无法记录病猪事件", batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 1.2 检查猪栏是否关联
 | 
				
			||||||
 | 
							pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPenNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 1.3 检查剩余健康猪不能少于即将转化的病猪数量
 | 
				
			||||||
 | 
							totalPigsInBatch, err := s.getCurrentPigQuantityTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 总猪只数量失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							healthyPigs := totalPigsInBatch - currentSickPigs
 | 
				
			||||||
 | 
							if healthyPigs < quantity {
 | 
				
			||||||
 | 
								return fmt.Errorf("健康猪数量不足,当前健康猪 %d 头,尝试记录病猪 %d 头", healthyPigs, quantity)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 1.4 创建病猪日志
 | 
				
			||||||
 | 
							sickLog := &models.PigSickLog{
 | 
				
			||||||
 | 
								PigBatchID:        batchID,
 | 
				
			||||||
 | 
								PenID:             penID,
 | 
				
			||||||
 | 
								ChangeCount:       quantity, // 新增病猪,ChangeCount 为正数
 | 
				
			||||||
 | 
								Reason:            models.SickPigReasonTypeIllness,
 | 
				
			||||||
 | 
								TreatmentLocation: treatmentLocation,
 | 
				
			||||||
 | 
								Remarks:           remarks,
 | 
				
			||||||
 | 
								OperatorID:        operatorID,
 | 
				
			||||||
 | 
								HappenedAt:        happenedAt,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("处理病猪日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("记录新增病猪事件失败: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigRecovery 记录病猪康复事件。
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						if quantity <= 0 {
 | 
				
			||||||
 | 
							return errors.New("康复猪只数量必须大于0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 检查批次是否活跃
 | 
				
			||||||
 | 
							batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPigBatchNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !batch.IsActive() {
 | 
				
			||||||
 | 
								return fmt.Errorf("批次 %d 不活跃,无法记录病猪康复事件", batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 检查猪栏是否关联
 | 
				
			||||||
 | 
							pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPenNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 检查当前病猪数量是否足够康复
 | 
				
			||||||
 | 
							currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if currentSickPigs < quantity {
 | 
				
			||||||
 | 
								return fmt.Errorf("当前病猪数量不足,当前病猪 %d 头,尝试康复 %d 头", currentSickPigs, quantity)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 创建病猪日志
 | 
				
			||||||
 | 
							sickLog := &models.PigSickLog{
 | 
				
			||||||
 | 
								PigBatchID:        batchID,
 | 
				
			||||||
 | 
								PenID:             penID,
 | 
				
			||||||
 | 
								ChangeCount:       -quantity, // 康复病猪,ChangeCount 为负数
 | 
				
			||||||
 | 
								Reason:            models.SickPigReasonTypeRecovery,
 | 
				
			||||||
 | 
								TreatmentLocation: treatmentLocation,
 | 
				
			||||||
 | 
								Remarks:           remarks,
 | 
				
			||||||
 | 
								OperatorID:        operatorID,
 | 
				
			||||||
 | 
								HappenedAt:        happenedAt,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("处理病猪康复日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("记录病猪康复事件失败: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigDeath 记录病猪死亡事件。
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						if quantity <= 0 {
 | 
				
			||||||
 | 
							return errors.New("死亡猪只数量必须大于0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 检查批次是否活跃
 | 
				
			||||||
 | 
							batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPigBatchNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !batch.IsActive() {
 | 
				
			||||||
 | 
								return fmt.Errorf("批次 %d 不活跃,无法记录病猪死亡事件", batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 检查猪栏是否关联
 | 
				
			||||||
 | 
							pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPenNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 检查当前病猪数量是否足够死亡
 | 
				
			||||||
 | 
							currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if currentSickPigs < quantity {
 | 
				
			||||||
 | 
								return fmt.Errorf("当前病猪数量不足,当前病猪 %d 头,尝试记录死亡 %d 头", currentSickPigs, quantity)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 检查猪栏内猪只数量是否足够死亡
 | 
				
			||||||
 | 
							currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if currentPigsInPen < quantity {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录死亡 %d 头", penID, currentPigsInPen, quantity)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 5. 创建病猪日志 (减少病猪数量)
 | 
				
			||||||
 | 
							sickLog := &models.PigSickLog{
 | 
				
			||||||
 | 
								PigBatchID:        batchID,
 | 
				
			||||||
 | 
								PenID:             penID,
 | 
				
			||||||
 | 
								ChangeCount:       -quantity, // 死亡病猪,ChangeCount 为负数
 | 
				
			||||||
 | 
								Reason:            models.SickPigReasonTypeDeath,
 | 
				
			||||||
 | 
								TreatmentLocation: treatmentLocation,
 | 
				
			||||||
 | 
								Remarks:           remarks,
 | 
				
			||||||
 | 
								OperatorID:        operatorID,
 | 
				
			||||||
 | 
								HappenedAt:        happenedAt,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("处理病猪死亡日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 6. 更新批次总猪只数量 (减少批次总数)
 | 
				
			||||||
 | 
							if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeDeath, -quantity, remarks, happenedAt); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 7. 记录猪只转移日志 (减少猪栏内猪只数量)
 | 
				
			||||||
 | 
							transferLog := &models.PigTransferLog{
 | 
				
			||||||
 | 
								TransferTime: happenedAt,
 | 
				
			||||||
 | 
								PigBatchID:   batchID,
 | 
				
			||||||
 | 
								PenID:        penID,
 | 
				
			||||||
 | 
								Quantity:     -quantity, // 减少猪只数量
 | 
				
			||||||
 | 
								Type:         models.PigTransferTypeDeath,
 | 
				
			||||||
 | 
								OperatorID:   operatorID,
 | 
				
			||||||
 | 
								Remarks:      remarks,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("记录猪只死亡转移日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("记录病猪死亡事件失败: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordSickPigCull 记录病猪淘汰事件。
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						if quantity <= 0 {
 | 
				
			||||||
 | 
							return errors.New("淘汰猪只数量必须大于0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 检查批次是否活跃
 | 
				
			||||||
 | 
							batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPigBatchNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !batch.IsActive() {
 | 
				
			||||||
 | 
								return fmt.Errorf("批次 %d 不活跃,无法记录病猪淘汰事件", batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 检查猪栏是否关联
 | 
				
			||||||
 | 
							pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPenNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 检查当前病猪数量是否足够淘汰
 | 
				
			||||||
 | 
							currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if currentSickPigs < quantity {
 | 
				
			||||||
 | 
								return fmt.Errorf("当前病猪数量不足,当前病猪 %d 头,尝试淘汰 %d 头", currentSickPigs, quantity)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 检查猪栏内猪只数量是否足够淘汰
 | 
				
			||||||
 | 
							currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if currentPigsInPen < quantity {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录淘汰 %d 头", penID, currentPigsInPen, quantity)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 5. 创建病猪日志 (减少病猪数量)
 | 
				
			||||||
 | 
							sickLog := &models.PigSickLog{
 | 
				
			||||||
 | 
								PigBatchID:        batchID,
 | 
				
			||||||
 | 
								PenID:             penID,
 | 
				
			||||||
 | 
								ChangeCount:       -quantity, // 淘汰病猪,ChangeCount 为负数
 | 
				
			||||||
 | 
								Reason:            models.SickPigReasonTypeEliminate,
 | 
				
			||||||
 | 
								TreatmentLocation: treatmentLocation,
 | 
				
			||||||
 | 
								Remarks:           remarks,
 | 
				
			||||||
 | 
								OperatorID:        operatorID,
 | 
				
			||||||
 | 
								HappenedAt:        happenedAt,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("处理病猪淘汰日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 6. 更新批次总猪只数量 (减少批次总数)
 | 
				
			||||||
 | 
							if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeCull, -quantity, remarks, happenedAt); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 7. 记录猪只转移日志 (减少猪栏内猪只数量)
 | 
				
			||||||
 | 
							transferLog := &models.PigTransferLog{
 | 
				
			||||||
 | 
								TransferTime: happenedAt,
 | 
				
			||||||
 | 
								PigBatchID:   batchID,
 | 
				
			||||||
 | 
								PenID:        penID,
 | 
				
			||||||
 | 
								Quantity:     -quantity,                  // 减少猪只数量
 | 
				
			||||||
 | 
								Type:         models.PigTransferTypeCull, // 淘汰类型
 | 
				
			||||||
 | 
								OperatorID:   operatorID,
 | 
				
			||||||
 | 
								Remarks:      remarks,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("记录猪只淘汰转移日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("记录病猪淘汰事件失败: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordDeath 记录正常猪只死亡事件。
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						if quantity <= 0 {
 | 
				
			||||||
 | 
							return errors.New("死亡猪只数量必须大于0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 检查批次是否活跃
 | 
				
			||||||
 | 
							batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPigBatchNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !batch.IsActive() {
 | 
				
			||||||
 | 
								return fmt.Errorf("批次 %d 不活跃,无法记录死亡事件", batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 检查猪栏是否关联
 | 
				
			||||||
 | 
							pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPenNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 检查猪栏内猪只数量是否足够死亡
 | 
				
			||||||
 | 
							currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if currentPigsInPen < quantity {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录死亡 %d 头", penID, currentPigsInPen, quantity)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 更新批次总猪只数量 (减少批次总数)
 | 
				
			||||||
 | 
							if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeDeath, -quantity, remarks, happenedAt); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 5. 记录猪只转移日志 (减少猪栏内猪只数量)
 | 
				
			||||||
 | 
							transferLog := &models.PigTransferLog{
 | 
				
			||||||
 | 
								TransferTime: happenedAt,
 | 
				
			||||||
 | 
								PigBatchID:   batchID,
 | 
				
			||||||
 | 
								PenID:        penID,
 | 
				
			||||||
 | 
								Quantity:     -quantity, // 减少猪只数量
 | 
				
			||||||
 | 
								Type:         models.PigTransferTypeDeath,
 | 
				
			||||||
 | 
								OperatorID:   operatorID,
 | 
				
			||||||
 | 
								Remarks:      remarks,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("记录猪只死亡转移日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("记录正常猪只死亡事件失败: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecordCull 记录正常猪只淘汰事件。
 | 
				
			||||||
 | 
					func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
 | 
				
			||||||
 | 
						if quantity <= 0 {
 | 
				
			||||||
 | 
							return errors.New("淘汰猪只数量必须大于0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 1. 检查批次是否活跃
 | 
				
			||||||
 | 
							batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPigBatchNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !batch.IsActive() {
 | 
				
			||||||
 | 
								return fmt.Errorf("批次 %d 不活跃,无法记录淘汰事件", batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 检查猪栏是否关联
 | 
				
			||||||
 | 
							pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPenNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 检查猪栏内猪只数量是否足够淘汰
 | 
				
			||||||
 | 
							currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if currentPigsInPen < quantity {
 | 
				
			||||||
 | 
								return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录淘汰 %d 头", penID, currentPigsInPen, quantity)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 更新批次总猪只数量 (减少批次总数)
 | 
				
			||||||
 | 
							if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeCull, -quantity, remarks, happenedAt); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 5. 记录猪只转移日志 (减少猪栏内猪只数量)
 | 
				
			||||||
 | 
							transferLog := &models.PigTransferLog{
 | 
				
			||||||
 | 
								TransferTime: happenedAt,
 | 
				
			||||||
 | 
								PigBatchID:   batchID,
 | 
				
			||||||
 | 
								PenID:        penID,
 | 
				
			||||||
 | 
								Quantity:     -quantity, // 减少猪只数量
 | 
				
			||||||
 | 
								Type:         models.PigTransferTypeCull,
 | 
				
			||||||
 | 
								OperatorID:   operatorID,
 | 
				
			||||||
 | 
								Remarks:      remarks,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("记录猪只淘汰转移日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("记录正常猪只淘汰事件失败: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										154
									
								
								internal/domain/pig/pig_batch_service_pig_trade.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								internal/domain/pig/pig_batch_service_pig_trade.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,154 @@
 | 
				
			|||||||
 | 
					package pig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SellPigs 处理批量销售猪的业务逻辑。
 | 
				
			||||||
 | 
					func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
 | 
				
			||||||
 | 
						return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							if quantity <= 0 {
 | 
				
			||||||
 | 
								return errors.New("销售数量必须大于0")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 1. 校验猪栏信息
 | 
				
			||||||
 | 
							pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPenNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 校验猪栏是否属于该批次
 | 
				
			||||||
 | 
							if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return ErrPenNotAssociatedWithBatch
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 业务校验:检查销售数量是否超过猪栏当前猪只数
 | 
				
			||||||
 | 
							currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 当前猪只数失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if quantity > currentPigsInPen {
 | 
				
			||||||
 | 
								return fmt.Errorf("销售数量 %d 超过猪栏 %d 当前猪只数 %d", quantity, penID, currentPigsInPen)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 记录销售交易 (财务)
 | 
				
			||||||
 | 
							sale := &models.PigSale{
 | 
				
			||||||
 | 
								PigBatchID: batchID,
 | 
				
			||||||
 | 
								SaleDate:   tradeDate,
 | 
				
			||||||
 | 
								Buyer:      traderName,
 | 
				
			||||||
 | 
								Quantity:   quantity,
 | 
				
			||||||
 | 
								UnitPrice:  unitPrice,
 | 
				
			||||||
 | 
								TotalPrice: tatalPrice, // 总价不一定是单价x数量, 所以要传进来
 | 
				
			||||||
 | 
								Remarks:    remarks,
 | 
				
			||||||
 | 
								OperatorID: operatorID,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.tradeSvc.SellPig(tx, sale); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("记录销售交易失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 创建猪只转移日志 (物理)
 | 
				
			||||||
 | 
							transferLog := &models.PigTransferLog{
 | 
				
			||||||
 | 
								TransferTime: tradeDate,
 | 
				
			||||||
 | 
								PigBatchID:   batchID,
 | 
				
			||||||
 | 
								PenID:        penID,
 | 
				
			||||||
 | 
								Quantity:     -quantity, // 销售导致数量减少
 | 
				
			||||||
 | 
								Type:         models.PigTransferTypeSale,
 | 
				
			||||||
 | 
								OperatorID:   operatorID,
 | 
				
			||||||
 | 
								Remarks:      fmt.Sprintf("销售给 %s", traderName),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("创建猪只转移日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 5. 记录批次数量变更日志 (逻辑)
 | 
				
			||||||
 | 
							if err := s.updatePigBatchQuantityTx(tx, operatorID, batchID, models.ChangeTypeSale, -quantity,
 | 
				
			||||||
 | 
								fmt.Sprintf("猪批次 %d 从猪栏 %d 销售 %d 头猪给 %s", batchID, penID, quantity, traderName),
 | 
				
			||||||
 | 
								tradeDate); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新猪批次数量失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuyPigs 处理批量购买猪的业务逻辑。
 | 
				
			||||||
 | 
					func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, totalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
 | 
				
			||||||
 | 
						return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							if quantity <= 0 {
 | 
				
			||||||
 | 
								return errors.New("采购数量必须大于0")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 1. 校验猪栏信息
 | 
				
			||||||
 | 
							pen, err := s.transferSvc.GetPenByID(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
									return ErrPenNotFound
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 校验猪栏是否属于该批次
 | 
				
			||||||
 | 
							if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
 | 
				
			||||||
 | 
								return ErrPenNotAssociatedWithBatch
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 业务校验:检查猪栏容量,如果超出,在备注中记录警告
 | 
				
			||||||
 | 
							currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("获取猪栏 %d 当前猪只数失败: %w", penID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							transferRemarks := fmt.Sprintf("从 %s 采购", traderName)
 | 
				
			||||||
 | 
							if currentPigsInPen+quantity > pen.Capacity {
 | 
				
			||||||
 | 
								warning := fmt.Sprintf("[警告]猪栏容量超出: 当前 %d, 采购 %d, 容量 %d.", currentPigsInPen, quantity, pen.Capacity)
 | 
				
			||||||
 | 
								transferRemarks = fmt.Sprintf("%s %s", transferRemarks, warning)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 3. 记录采购交易 (财务)
 | 
				
			||||||
 | 
							purchase := &models.PigPurchase{
 | 
				
			||||||
 | 
								PigBatchID:   batchID,
 | 
				
			||||||
 | 
								PurchaseDate: tradeDate,
 | 
				
			||||||
 | 
								Supplier:     traderName,
 | 
				
			||||||
 | 
								Quantity:     quantity,
 | 
				
			||||||
 | 
								UnitPrice:    unitPrice,
 | 
				
			||||||
 | 
								TotalPrice:   totalPrice, // 总价不一定是单价x数量, 所以要传进来
 | 
				
			||||||
 | 
								Remarks:      remarks,    // 用户传入的备注
 | 
				
			||||||
 | 
								OperatorID:   operatorID,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.tradeSvc.BuyPig(tx, purchase); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("记录采购交易失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 4. 创建猪只转移日志 (物理)
 | 
				
			||||||
 | 
							transferLog := &models.PigTransferLog{
 | 
				
			||||||
 | 
								TransferTime: tradeDate,
 | 
				
			||||||
 | 
								PigBatchID:   batchID,
 | 
				
			||||||
 | 
								PenID:        penID,
 | 
				
			||||||
 | 
								Quantity:     quantity, // 采购导致数量增加
 | 
				
			||||||
 | 
								Type:         models.PigTransferTypePurchase,
 | 
				
			||||||
 | 
								OperatorID:   operatorID,
 | 
				
			||||||
 | 
								Remarks:      transferRemarks, // 包含系统生成的备注和潜在的警告
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("创建猪只转移日志失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 5. 记录批次数量变更日志 (逻辑)
 | 
				
			||||||
 | 
							if err := s.updatePigBatchQuantityTx(tx, operatorID, batchID, models.ChangeTypeBuy, quantity,
 | 
				
			||||||
 | 
								fmt.Sprintf("猪批次 %d 在猪栏 %d 采购 %d 头猪从 %s", batchID, penID, quantity, traderName),
 | 
				
			||||||
 | 
								tradeDate); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新猪批次数量失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										127
									
								
								internal/domain/pig/pig_sick_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								internal/domain/pig/pig_sick_manager.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
				
			|||||||
 | 
					package pig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SickPigManager 定义了与病猪管理相关的操作接口。
 | 
				
			||||||
 | 
					// 这是一个领域服务,负责协调病猪记录、用药等业务逻辑。
 | 
				
			||||||
 | 
					type SickPigManager interface {
 | 
				
			||||||
 | 
						// ProcessSickPigLog 处理病猪相关的日志事件。
 | 
				
			||||||
 | 
						// log 包含事件的基本信息,如 PigBatchID, PenID, PigIDs, ChangeCount, Reason, TreatmentLocation, Remarks, OperatorID, HappenedAt。
 | 
				
			||||||
 | 
						// Manager 内部会计算并填充 BeforeCount 和 AfterCount,并进行必要的业务校验和副作用处理。
 | 
				
			||||||
 | 
						ProcessSickPigLog(tx *gorm.DB, log *models.PigSickLog) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// GetCurrentSickPigCount 获取指定批次当前患病猪只的总数
 | 
				
			||||||
 | 
						GetCurrentSickPigCount(tx *gorm.DB, batchID uint) (int, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// sickPigManager 是 SickPigManager 接口的具体实现。
 | 
				
			||||||
 | 
					// 它依赖于仓库接口来执行数据持久化操作。
 | 
				
			||||||
 | 
					type sickPigManager struct {
 | 
				
			||||||
 | 
						sickLogRepo       repository.PigSickLogRepository
 | 
				
			||||||
 | 
						medicationLogRepo repository.MedicationLogRepository
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewSickPigManager 是 sickPigManager 的构造函数。
 | 
				
			||||||
 | 
					func NewSickPigManager(
 | 
				
			||||||
 | 
						sickLogRepo repository.PigSickLogRepository,
 | 
				
			||||||
 | 
						medicationLogRepo repository.MedicationLogRepository,
 | 
				
			||||||
 | 
					) SickPigManager {
 | 
				
			||||||
 | 
						return &sickPigManager{
 | 
				
			||||||
 | 
							sickLogRepo:       sickLogRepo,
 | 
				
			||||||
 | 
							medicationLogRepo: medicationLogRepo,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *sickPigManager) ProcessSickPigLog(tx *gorm.DB, log *models.PigSickLog) error {
 | 
				
			||||||
 | 
						// 1. 输入校验
 | 
				
			||||||
 | 
						if log == nil {
 | 
				
			||||||
 | 
							return errors.New("病猪日志不能为空")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 关键字段校验
 | 
				
			||||||
 | 
						var missingFields []string
 | 
				
			||||||
 | 
						if log.PigBatchID == 0 {
 | 
				
			||||||
 | 
							missingFields = append(missingFields, "PigBatchID")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if log.ChangeCount == 0 {
 | 
				
			||||||
 | 
							missingFields = append(missingFields, "ChangeCount")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if log.Reason == "" {
 | 
				
			||||||
 | 
							missingFields = append(missingFields, "Reason")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if log.TreatmentLocation == "" {
 | 
				
			||||||
 | 
							missingFields = append(missingFields, "TreatmentLocation")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if log.HappenedAt.IsZero() {
 | 
				
			||||||
 | 
							missingFields = append(missingFields, "HappenedAt")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if log.OperatorID == 0 {
 | 
				
			||||||
 | 
							missingFields = append(missingFields, "OperatorID")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if log.PenID == 0 {
 | 
				
			||||||
 | 
							missingFields = append(missingFields, "PenID")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(missingFields) > 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("以下关键字段不能为空或零值: %v", missingFields)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 业务规则校验 - ChangeCount 与 Reason 的一致性
 | 
				
			||||||
 | 
						switch log.Reason {
 | 
				
			||||||
 | 
						case models.SickPigReasonTypeIllness, models.SickPigReasonTypeTransferIn:
 | 
				
			||||||
 | 
							if log.ChangeCount < 0 {
 | 
				
			||||||
 | 
								return fmt.Errorf("原因 '%s' 的 ChangeCount 必须为正数", log.Reason)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case models.SickPigReasonTypeRecovery, models.SickPigReasonTypeDeath, models.SickPigReasonTypeEliminate, models.SickPigReasonTypeTransferOut:
 | 
				
			||||||
 | 
							if log.ChangeCount > 0 {
 | 
				
			||||||
 | 
								return fmt.Errorf("原因 '%s' 的 ChangeCount 必须为负数", log.Reason)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case models.SickPigReasonTypeOther:
 | 
				
			||||||
 | 
							// 其他原因,ChangeCount 可以是任意值,但不能为0
 | 
				
			||||||
 | 
							if log.ChangeCount == 0 {
 | 
				
			||||||
 | 
								return errors.New("原因 '其他' 的 ChangeCount 不能为零")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return fmt.Errorf("未知的病猪日志原因类型: %s", log.Reason)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 获取当前病猪数量 (BeforeCount)
 | 
				
			||||||
 | 
						beforeCount, err := s.GetCurrentSickPigCount(tx, log.PigBatchID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", log.PigBatchID, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						log.BeforeCount = beforeCount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 计算变化后的数量 (AfterCount)
 | 
				
			||||||
 | 
						log.AfterCount = log.BeforeCount + log.ChangeCount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 4. 业务规则校验 - 数量合法性
 | 
				
			||||||
 | 
						if log.AfterCount < 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("操作后病猪数量不能为负数,当前 %d,变化 %d", log.BeforeCount, log.ChangeCount)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 5. 持久化 PigSickLog
 | 
				
			||||||
 | 
						if err := s.sickLogRepo.CreatePigSickLogTx(tx, log); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("创建 PigSickLog 失败: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *sickPigManager) GetCurrentSickPigCount(tx *gorm.DB, batchID uint) (int, error) {
 | 
				
			||||||
 | 
						lastLog, err := s.sickLogRepo.GetLastLogByBatchTx(tx, batchID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return 0, nil // 如果没有找到任何日志,表示当前病猪数量为0
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("获取批次 %d 的最新病猪日志失败: %w", batchID, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return lastLog.AfterCount, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										46
									
								
								internal/domain/pig/pig_trade_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								internal/domain/pig/pig_trade_manager.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					package pig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" // 引入基础设施层的仓库接口
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigTradeManager 定义了与猪只交易相关的操作接口。
 | 
				
			||||||
 | 
					// 这是一个领域服务,负责协调业务逻辑。
 | 
				
			||||||
 | 
					type PigTradeManager interface {
 | 
				
			||||||
 | 
						// SellPig 处理卖猪的业务逻辑,通过仓库接口创建 PigSale 记录。
 | 
				
			||||||
 | 
						SellPig(tx *gorm.DB, sale *models.PigSale) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// BuyPig 处理买猪的业务逻辑,通过仓库接口创建 PigPurchase 记录。
 | 
				
			||||||
 | 
						BuyPig(tx *gorm.DB, purchase *models.PigPurchase) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// pigTradeManager 是 PigTradeManager 接口的具体实现。
 | 
				
			||||||
 | 
					// 它依赖于 repository.PigTradeRepository 接口来执行数据持久化操作。
 | 
				
			||||||
 | 
					type pigTradeManager struct {
 | 
				
			||||||
 | 
						tradeRepo repository.PigTradeRepository // 依赖于基础设施层定义的仓库接口
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewPigTradeManager 是 pigTradeManager 的构造函数。
 | 
				
			||||||
 | 
					func NewPigTradeManager(tradeRepo repository.PigTradeRepository) PigTradeManager {
 | 
				
			||||||
 | 
						return &pigTradeManager{
 | 
				
			||||||
 | 
							tradeRepo: tradeRepo,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SellPig 实现了卖猪的逻辑。
 | 
				
			||||||
 | 
					// 它通过调用 tradeRepo 来持久化销售记录。
 | 
				
			||||||
 | 
					func (s *pigTradeManager) SellPig(tx *gorm.DB, sale *models.PigSale) error {
 | 
				
			||||||
 | 
						// 在此处可以添加更复杂的卖猪前置校验或业务逻辑
 | 
				
			||||||
 | 
						// 例如:检查猪只库存、更新猪只状态等。
 | 
				
			||||||
 | 
						return s.tradeRepo.CreatePigSaleTx(tx, sale)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuyPig 实现了买猪的逻辑。
 | 
				
			||||||
 | 
					// 它通过调用 tradeRepo 来持久化采购记录。
 | 
				
			||||||
 | 
					func (s *pigTradeManager) BuyPig(tx *gorm.DB, purchase *models.PigPurchase) error {
 | 
				
			||||||
 | 
						// 在此处可以添加更复杂的买猪前置校验或业务逻辑
 | 
				
			||||||
 | 
						// 例如:检查资金、更新猪只状态等。
 | 
				
			||||||
 | 
						return s.tradeRepo.CreatePigPurchaseTx(tx, purchase)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,7 +5,7 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
@@ -5,7 +5,7 @@ import (
 | 
				
			|||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
@@ -435,7 +435,7 @@ func (s *Scheduler) handlePlanCompletion(planLogID uint) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// 如果是自动计划且达到执行次数上限,或计划是手动类型,则更新计划状态为已停止
 | 
						// 如果是自动计划且达到执行次数上限,或计划是手动类型,则更新计划状态为已停止
 | 
				
			||||||
	if (plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteNum > 0 && newExecuteCount >= plan.ExecuteNum) || plan.ExecutionType == models.PlanExecutionTypeManual {
 | 
						if (plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteNum > 0 && newExecuteCount >= plan.ExecuteNum) || plan.ExecutionType == models.PlanExecutionTypeManual {
 | 
				
			||||||
		newStatus = models.PlanStatusStopeed
 | 
							newStatus = models.PlanStatusStopped
 | 
				
			||||||
		s.logger.Infof("计划 %d 已完成执行,状态更新为 '执行完毕'。", topLevelPlanID)
 | 
							s.logger.Infof("计划 %d 已完成执行,状态更新为 '执行完毕'。", topLevelPlanID)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -160,11 +160,22 @@ func (ps *PostgresStorage) creatingHyperTable() error {
 | 
				
			|||||||
		{models.TaskExecutionLog{}, "created_at"},
 | 
							{models.TaskExecutionLog{}, "created_at"},
 | 
				
			||||||
		{models.PendingCollection{}, "created_at"},
 | 
							{models.PendingCollection{}, "created_at"},
 | 
				
			||||||
		{models.UserActionLog{}, "time"},
 | 
							{models.UserActionLog{}, "time"},
 | 
				
			||||||
 | 
							{models.RawMaterialPurchase{}, "purchase_date"},
 | 
				
			||||||
 | 
							{models.RawMaterialStockLog{}, "happened_at"},
 | 
				
			||||||
 | 
							{models.FeedUsageRecord{}, "recorded_at"},
 | 
				
			||||||
 | 
							{models.MedicationLog{}, "happened_at"},
 | 
				
			||||||
 | 
							{models.PigBatchLog{}, "happened_at"},
 | 
				
			||||||
 | 
							{models.WeighingBatch{}, "weighing_time"},
 | 
				
			||||||
 | 
							{models.WeighingRecord{}, "weighing_time"},
 | 
				
			||||||
 | 
							{models.PigTransferLog{}, "transfer_time"},
 | 
				
			||||||
 | 
							{models.PigSickLog{}, "happened_at"},
 | 
				
			||||||
 | 
							{models.PigPurchase{}, "purchase_date"},
 | 
				
			||||||
 | 
							{models.PigSale{}, "sale_date"},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, table := range tablesToConvert {
 | 
						for _, table := range tablesToConvert {
 | 
				
			||||||
		tableName := table.model.TableName()
 | 
							tableName := table.model.TableName()
 | 
				
			||||||
		chunkInterval := "1 day" // 统一设置为1天
 | 
							chunkInterval := "1 days" // 统一设置为1天
 | 
				
			||||||
		ps.logger.Infow("准备将表转换为超表", "table", tableName, "chunk_interval", chunkInterval)
 | 
							ps.logger.Infow("准备将表转换为超表", "table", tableName, "chunk_interval", chunkInterval)
 | 
				
			||||||
		sql := fmt.Sprintf("SELECT create_hypertable('%s', '%s', chunk_time_interval => INTERVAL '%s', if_not_exists => TRUE);", tableName, table.timeColumn, chunkInterval)
 | 
							sql := fmt.Sprintf("SELECT create_hypertable('%s', '%s', chunk_time_interval => INTERVAL '%s', if_not_exists => TRUE);", tableName, table.timeColumn, chunkInterval)
 | 
				
			||||||
		if err := ps.db.Exec(sql).Error; err != nil {
 | 
							if err := ps.db.Exec(sql).Error; err != nil {
 | 
				
			||||||
@@ -189,6 +200,17 @@ func (ps *PostgresStorage) applyCompressionPolicies() error {
 | 
				
			|||||||
		{models.TaskExecutionLog{}, "task_id"},
 | 
							{models.TaskExecutionLog{}, "task_id"},
 | 
				
			||||||
		{models.PendingCollection{}, "device_id"},
 | 
							{models.PendingCollection{}, "device_id"},
 | 
				
			||||||
		{models.UserActionLog{}, "user_id"},
 | 
							{models.UserActionLog{}, "user_id"},
 | 
				
			||||||
 | 
							{models.RawMaterialPurchase{}, "raw_material_id"},
 | 
				
			||||||
 | 
							{models.RawMaterialStockLog{}, "raw_material_id"},
 | 
				
			||||||
 | 
							{models.FeedUsageRecord{}, "pen_id"},
 | 
				
			||||||
 | 
							{models.MedicationLog{}, "pig_batch_id"},
 | 
				
			||||||
 | 
							{models.PigBatchLog{}, "pig_batch_id"},
 | 
				
			||||||
 | 
							{models.WeighingBatch{}, "pig_batch_id"},
 | 
				
			||||||
 | 
							{models.WeighingRecord{}, "weighing_batch_id"},
 | 
				
			||||||
 | 
							{models.PigTransferLog{}, "pig_batch_id"},
 | 
				
			||||||
 | 
							{models.PigSickLog{}, "pig_batch_id"},
 | 
				
			||||||
 | 
							{models.PigPurchase{}, "pig_batch_id"},
 | 
				
			||||||
 | 
							{models.PigSale{}, "pig_batch_id"},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, policy := range policies {
 | 
						for _, policy := range policies {
 | 
				
			||||||
@@ -239,14 +261,5 @@ func (ps *PostgresStorage) creatingIndex() error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	ps.logger.Info("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)")
 | 
						ps.logger.Info("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 为 devices 表的 properties 字段创建 GIN 索引
 | 
					 | 
				
			||||||
	//ps.logger.Info("正在为 devices 表的 properties 字段创建 GIN 索引")
 | 
					 | 
				
			||||||
	//ginDevicePropertiesIndexSQL := "CREATE INDEX IF NOT EXISTS idx_devices_properties_gin ON devices USING GIN (properties);"
 | 
					 | 
				
			||||||
	//if err := ps.db.Exec(ginDevicePropertiesIndexSQL).Error; err != nil {
 | 
					 | 
				
			||||||
	//	ps.logger.Errorw("为 devices 的 properties 字段创建 GIN 索引失败", "error", err)
 | 
					 | 
				
			||||||
	//	return fmt.Errorf("为 devices 的 properties 字段创建 GIN 索引失败: %w", err)
 | 
					 | 
				
			||||||
	//}
 | 
					 | 
				
			||||||
	//ps.logger.Info("成功为 devices 的 properties 字段创建 GIN 索引 (或已存在)")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,9 +15,9 @@ type DeviceCategory string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	// CategoryActuator 代表一个执行器,可以被控制(例如:风机、阀门)
 | 
						// CategoryActuator 代表一个执行器,可以被控制(例如:风机、阀门)
 | 
				
			||||||
	CategoryActuator DeviceCategory = "actuator"
 | 
						CategoryActuator DeviceCategory = "执行器"
 | 
				
			||||||
	// CategorySensor 代表一个传感器,用于报告测量值(例如:温度计)
 | 
						// CategorySensor 代表一个传感器,用于报告测量值(例如:温度计)
 | 
				
			||||||
	CategorySensor DeviceCategory = "sensor"
 | 
						CategorySensor DeviceCategory = "传感器"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ValueDescriptor 描述了传感器可以报告的单个数值。
 | 
					// ValueDescriptor 描述了传感器可以报告的单个数值。
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,11 +17,11 @@ const (
 | 
				
			|||||||
type ExecutionStatus string
 | 
					type ExecutionStatus string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	ExecutionStatusStarted   ExecutionStatus = "started"   // 开始执行
 | 
						ExecutionStatusStarted   ExecutionStatus = "已开始" // 开始执行
 | 
				
			||||||
	ExecutionStatusCompleted ExecutionStatus = "completed" // 执行完成
 | 
						ExecutionStatusCompleted ExecutionStatus = "已完成" // 执行完成
 | 
				
			||||||
	ExecutionStatusFailed    ExecutionStatus = "failed"    // 执行失败
 | 
						ExecutionStatusFailed    ExecutionStatus = "失败"  // 执行失败
 | 
				
			||||||
	ExecutionStatusCancelled ExecutionStatus = "cancelled" // 执行取消
 | 
						ExecutionStatusCancelled ExecutionStatus = "已取消" // 执行取消
 | 
				
			||||||
	ExecutionStatusWaiting   ExecutionStatus = "waiting"   // 等待执行 (用于预写日志)
 | 
						ExecutionStatusWaiting   ExecutionStatus = "等待中" // 等待执行 (用于预写日志)
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PlanExecutionLog 记录整个计划的一次执行历史
 | 
					// PlanExecutionLog 记录整个计划的一次执行历史
 | 
				
			||||||
@@ -92,9 +92,9 @@ func (log *TaskExecutionLog) AfterFind(tx *gorm.DB) (err error) {
 | 
				
			|||||||
type PendingCollectionStatus string
 | 
					type PendingCollectionStatus string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	PendingStatusPending   PendingCollectionStatus = "pending"   // 请求已发送,等待设备响应
 | 
						PendingStatusPending   PendingCollectionStatus = "等待中" // 请求已发送,等待设备响应
 | 
				
			||||||
	PendingStatusFulfilled PendingCollectionStatus = "fulfilled" // 已收到设备响应并成功处理
 | 
						PendingStatusFulfilled PendingCollectionStatus = "已完成" // 已收到设备响应并成功处理
 | 
				
			||||||
	PendingStatusTimedOut  PendingCollectionStatus = "timed_out" // 请求超时,未收到设备响应
 | 
						PendingStatusTimedOut  PendingCollectionStatus = "已超时" // 请求超时,未收到设备响应
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DeviceCommandLog 记录所有“发后即忘”的下行指令日志。
 | 
					// DeviceCommandLog 记录所有“发后即忘”的下行指令日志。
 | 
				
			||||||
@@ -160,8 +160,8 @@ func (PendingCollection) TableName() string {
 | 
				
			|||||||
type AuditStatus string
 | 
					type AuditStatus string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	AuditStatusSuccess AuditStatus = "success"
 | 
						AuditStatusSuccess AuditStatus = "成功"
 | 
				
			||||||
	AuditStatusFailed  AuditStatus = "failed"
 | 
						AuditStatusFailed  AuditStatus = "失败"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- 审计日志相关上下文键 ---
 | 
					// --- 审计日志相关上下文键 ---
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										39
									
								
								internal/infra/models/farm_asset.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/infra/models/farm_asset.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
						猪场固定资产相关模型
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigHouse 定义了猪舍,是猪栏的集合
 | 
				
			||||||
 | 
					type PigHouse struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						Name        string `gorm:"size:100;not null;unique;comment:猪舍名称, 如 '育肥舍A栋'"`
 | 
				
			||||||
 | 
						Description string `gorm:"size:255;comment:描述信息"`
 | 
				
			||||||
 | 
						Pens        []Pen  `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PenStatus 定义了猪栏的当前状态
 | 
				
			||||||
 | 
					type PenStatus string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						PenStatusEmpty      PenStatus = "空闲"
 | 
				
			||||||
 | 
						PenStatusOccupied   PenStatus = "使用中"
 | 
				
			||||||
 | 
						PenStatusSickPen    PenStatus = "病猪栏"
 | 
				
			||||||
 | 
						PenStatusRecovering PenStatus = "康复栏"
 | 
				
			||||||
 | 
						PenStatusCleaning   PenStatus = "清洗消毒"
 | 
				
			||||||
 | 
						PenStatusUnderMaint PenStatus = "维修中"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Pen 是猪栏的物理实体模型, 是所有空间相关数据的“锚点”
 | 
				
			||||||
 | 
					type Pen struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						PenNumber  string    `gorm:"not null;comment:猪栏的唯一编号, 如 A-01"`
 | 
				
			||||||
 | 
						HouseID    uint      `gorm:"index;comment:所属猪舍ID"`
 | 
				
			||||||
 | 
						PigBatchID *uint     `gorm:"index;comment:关联的猪批次ID"`
 | 
				
			||||||
 | 
						Capacity   int       `gorm:"not null;comment:设计容量 (头)"`
 | 
				
			||||||
 | 
						Status     PenStatus `gorm:"not null;index;comment:猪栏当前状态"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										113
									
								
								internal/infra/models/feed.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								internal/infra/models/feed.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
				
			|||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
						饲料和饲喂相关的模型
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RawMaterial 代表饲料的原料。
 | 
				
			||||||
 | 
					// 建议:所有重量单位统一存储 (例如, 全部使用 'g'),便于计算和避免转换错误。
 | 
				
			||||||
 | 
					type RawMaterial struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						Name        string  `gorm:"size:100;unique;not null;comment:原料名称"`
 | 
				
			||||||
 | 
						Description string  `gorm:"size:255;comment:描述"`
 | 
				
			||||||
 | 
						Quantity    float64 `gorm:"not null;comment:库存总量, 单位: g"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (RawMaterial) TableName() string {
 | 
				
			||||||
 | 
						return "raw_materials"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RawMaterialPurchase 记录了原料的每一次采购。
 | 
				
			||||||
 | 
					type RawMaterialPurchase struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						RawMaterialID uint        `gorm:"not null;index;comment:关联的原料ID"`
 | 
				
			||||||
 | 
						RawMaterial   RawMaterial `gorm:"foreignKey:RawMaterialID"`
 | 
				
			||||||
 | 
						Supplier      string      `gorm:"size:100;comment:供应商"`
 | 
				
			||||||
 | 
						Amount        float64     `gorm:"not null;comment:采购数量, 单位: g"`
 | 
				
			||||||
 | 
						UnitPrice     float64     `gorm:"comment:单价"`
 | 
				
			||||||
 | 
						TotalPrice    float64     `gorm:"comment:总价"`
 | 
				
			||||||
 | 
						PurchaseDate  time.Time   `gorm:"primaryKey;comment:采购日期"`
 | 
				
			||||||
 | 
						CreatedAt     time.Time
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (RawMaterialPurchase) TableName() string {
 | 
				
			||||||
 | 
						return "raw_material_purchases"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// StockLogSourceType 定义了库存日志来源的类型
 | 
				
			||||||
 | 
					type StockLogSourceType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						StockLogSourcePurchase      StockLogSourceType = "采购入库"
 | 
				
			||||||
 | 
						StockLogSourceFeeding       StockLogSourceType = "饲喂出库"
 | 
				
			||||||
 | 
						StockLogSourceDeteriorate   StockLogSourceType = "变质出库"
 | 
				
			||||||
 | 
						StockLogSourceSale          StockLogSourceType = "售卖出库"
 | 
				
			||||||
 | 
						StockLogSourceMiscellaneous StockLogSourceType = "杂用领取"
 | 
				
			||||||
 | 
						StockLogSourceManual        StockLogSourceType = "手动盘点"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。
 | 
				
			||||||
 | 
					type RawMaterialStockLog struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						RawMaterialID uint               `gorm:"not null;index;comment:关联的原料ID"`
 | 
				
			||||||
 | 
						ChangeAmount  float64            `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"`
 | 
				
			||||||
 | 
						SourceType    StockLogSourceType `gorm:"size:50;not null;index;comment:库存变动来源类型"`
 | 
				
			||||||
 | 
						SourceID      uint               `gorm:"not null;index;comment:来源记录的ID (如 RawMaterialPurchase.ID 或 FeedUsageRecord.ID)"`
 | 
				
			||||||
 | 
						HappenedAt    time.Time          `gorm:"primaryKey;comment:业务发生时间"`
 | 
				
			||||||
 | 
						Remarks       string             `gorm:"comment:备注, 如主动领取的理由等"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (RawMaterialStockLog) TableName() string {
 | 
				
			||||||
 | 
						return "raw_material_stock_logs"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FeedFormula 代表饲料配方。
 | 
				
			||||||
 | 
					// 对于没有配方的外购饲料,可以将其视为一种特殊的 RawMaterial, 并为其创建一个仅包含它自己的 FeedFormula。
 | 
				
			||||||
 | 
					type FeedFormula struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						Name        string                 `gorm:"size:100;unique;not null;comment:配方名称"`
 | 
				
			||||||
 | 
						Description string                 `gorm:"size:255;comment:描述"`
 | 
				
			||||||
 | 
						Components  []FeedFormulaComponent `gorm:"foreignKey:FeedFormulaID"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (FeedFormula) TableName() string {
 | 
				
			||||||
 | 
						return "feed_formulas"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FeedFormulaComponent 代表配方中的一种原料及其占比。
 | 
				
			||||||
 | 
					type FeedFormulaComponent struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						FeedFormulaID uint        `gorm:"not null;index;comment:外键到 FeedFormula"`
 | 
				
			||||||
 | 
						RawMaterialID uint        `gorm:"not null;index;comment:外键到 RawMaterial"`
 | 
				
			||||||
 | 
						RawMaterial   RawMaterial `gorm:"foreignKey:RawMaterialID"`
 | 
				
			||||||
 | 
						Percentage    float64     `gorm:"not null;comment:该原料在配方中的百分比 (0-1.0)"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (FeedFormulaComponent) TableName() string {
 | 
				
			||||||
 | 
						return "feed_formula_components"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FeedUsageRecord 代表饲料使用记录。
 | 
				
			||||||
 | 
					// 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula,
 | 
				
			||||||
 | 
					// 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。
 | 
				
			||||||
 | 
					type FeedUsageRecord struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						PenID         uint        `gorm:"not null;index;comment:关联的猪栏ID"`
 | 
				
			||||||
 | 
						Pen           Pen         `gorm:"foreignKey:PenID"`
 | 
				
			||||||
 | 
						FeedFormulaID uint        `gorm:"not null;index;comment:使用的饲料配方ID"`
 | 
				
			||||||
 | 
						FeedFormula   FeedFormula `gorm:"foreignKey:FeedFormulaID"`
 | 
				
			||||||
 | 
						Amount        float64     `gorm:"not null;comment:使用数量, 单位: g"`
 | 
				
			||||||
 | 
						RecordedAt    time.Time   `gorm:"primaryKey;comment:记录时间"`
 | 
				
			||||||
 | 
						OperatorID    uint        `gorm:"not null;comment:操作员"`
 | 
				
			||||||
 | 
						Remarks       string      `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (FeedUsageRecord) TableName() string {
 | 
				
			||||||
 | 
						return "feed_usage_records"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										99
									
								
								internal/infra/models/medication.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								internal/infra/models/medication.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gorm.io/datatypes"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
						所有与药品、疫苗和用药记录相关的模型
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MedicationType 定义了兽药的类型
 | 
				
			||||||
 | 
					type MedicationType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						Powder    MedicationType = "粉剂"
 | 
				
			||||||
 | 
						Injection MedicationType = "针剂"
 | 
				
			||||||
 | 
						Vaccine   MedicationType = "疫苗"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MedicationCategory 定义了兽药的种类
 | 
				
			||||||
 | 
					type MedicationCategory string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						Tetracycline      MedicationCategory = "四环素类"
 | 
				
			||||||
 | 
						Sulfonamide       MedicationCategory = "磺胺类"
 | 
				
			||||||
 | 
						Penicillin        MedicationCategory = "青霉素类"
 | 
				
			||||||
 | 
						Macrolide         MedicationCategory = "大环内酯类"
 | 
				
			||||||
 | 
						Quinolone         MedicationCategory = "喹诺酮类"
 | 
				
			||||||
 | 
						Anthelmintic      MedicationCategory = "驱虫药"
 | 
				
			||||||
 | 
						Disinfectant      MedicationCategory = "消毒药"
 | 
				
			||||||
 | 
						BiologicalProduct MedicationCategory = "生物制品"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MixType 定义了粉剂药物该如何混合
 | 
				
			||||||
 | 
					type MixType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						MixFeed  = "饲料加药"
 | 
				
			||||||
 | 
						MixWater = "水中加药"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PowderInstructions 定义了粉剂使用说明.
 | 
				
			||||||
 | 
					// 在程序中, 可以将 Medication.Instructions 字段反序列化为此结构进行操作.
 | 
				
			||||||
 | 
					type PowderInstructions struct {
 | 
				
			||||||
 | 
						// 出栏前停药期
 | 
				
			||||||
 | 
						WithdrawalPeriod time.Duration `json:"withdrawal_period"`
 | 
				
			||||||
 | 
						// 拌料使用计量, 每千克体重用多少克药, 单位: g/kg
 | 
				
			||||||
 | 
						BodyWeightDosageUsed float64 `json:"body_weight_dosage_used"`
 | 
				
			||||||
 | 
						// 拌料使用剂量, 每升水加多少克药或每千克饲料干重加多少克药, 单位: g/kg(L)
 | 
				
			||||||
 | 
						MixDosageUsed float64 `json:"mix_dosage_used"`
 | 
				
			||||||
 | 
						// 拌料使用方式, 兑水/拌料
 | 
				
			||||||
 | 
						MixType MixType `json:"mix_type"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Medication 定义了兽药/疫苗的基本信息模型
 | 
				
			||||||
 | 
					type Medication struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						Name                          string             `gorm:"size:100;not null;comment:药品名称" json:"name"`
 | 
				
			||||||
 | 
						Type                          MedicationType     `gorm:"size:20;not null;comment:兽药类型 (粉剂, 针剂, 疫苗)" json:"type"`
 | 
				
			||||||
 | 
						Category                      MedicationCategory `gorm:"size:30;not null;comment:兽药种类 (四环素类, 磺胺类等)" json:"category"`
 | 
				
			||||||
 | 
						DosagePerUnit                 float64            `gorm:"size:50;comment:一份药物的计量 (针剂计量单位为毫升, 粉剂为克)" json:"dosage_per_unit"`
 | 
				
			||||||
 | 
						ActiveIngredientConcentration float64            `gorm:"size:50;comment:有效成分含量百分比" json:"active_ingredient_concentration"`
 | 
				
			||||||
 | 
						Manufacturer                  string             `gorm:"size:100;comment:生产厂家" json:"manufacturer"`
 | 
				
			||||||
 | 
						Instructions                  datatypes.JSON     `gorm:"type:jsonb;comment:使用说明" json:"instructions"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (Medication) TableName() string {
 | 
				
			||||||
 | 
						return "medications"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MedicationReasonType 定义了用药原因
 | 
				
			||||||
 | 
					type MedicationReasonType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						ReasonTypePreventive MedicationReasonType = "预防"
 | 
				
			||||||
 | 
						ReasonTypeTreatment  MedicationReasonType = "治疗"
 | 
				
			||||||
 | 
						ReasonTypeHealthCare MedicationReasonType = "保健"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MedicationLog 记录了对整个猪批次的用药情况
 | 
				
			||||||
 | 
					type MedicationLog struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						PigBatchID   uint                 `gorm:"not null;index;comment:关联的猪批次ID"`
 | 
				
			||||||
 | 
						MedicationID uint                 `gorm:"not null;index;comment:关联的药品ID"`
 | 
				
			||||||
 | 
						Medication   Medication           `gorm:"foreignKey:MedicationID"` // 预加载药品信息
 | 
				
			||||||
 | 
						DosageUsed   float64              `gorm:"not null;comment:使用的总剂量 (单位由药品决定,如g或ml)"`
 | 
				
			||||||
 | 
						TargetCount  int                  `gorm:"not null;comment:用药对象数量"`
 | 
				
			||||||
 | 
						Reason       MedicationReasonType `gorm:"size:20;not null;comment:用药原因"`
 | 
				
			||||||
 | 
						Description  string               `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"`
 | 
				
			||||||
 | 
						OperatorID   uint                 `gorm:"comment:操作员ID"`
 | 
				
			||||||
 | 
						HappenedAt   time.Time            `gorm:"primaryKey;comment:用药时间"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (MedicationLog) TableName() string {
 | 
				
			||||||
 | 
						return "medication_logs"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -12,20 +12,53 @@ import (
 | 
				
			|||||||
// 这个函数用于在数据库初始化时自动迁移所有的表结构。
 | 
					// 这个函数用于在数据库初始化时自动迁移所有的表结构。
 | 
				
			||||||
func GetAllModels() []interface{} {
 | 
					func GetAllModels() []interface{} {
 | 
				
			||||||
	return []interface{}{
 | 
						return []interface{}{
 | 
				
			||||||
 | 
							// Core Models
 | 
				
			||||||
		&User{},
 | 
							&User{},
 | 
				
			||||||
 | 
							&UserActionLog{},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Device Models
 | 
				
			||||||
		&Device{},
 | 
							&Device{},
 | 
				
			||||||
 | 
							&AreaController{},
 | 
				
			||||||
 | 
							&DeviceTemplate{},
 | 
				
			||||||
 | 
							&SensorData{},
 | 
				
			||||||
 | 
							&DeviceCommandLog{},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Plan & Task Models
 | 
				
			||||||
		&Plan{},
 | 
							&Plan{},
 | 
				
			||||||
		&SubPlan{},
 | 
							&SubPlan{},
 | 
				
			||||||
		&Task{},
 | 
							&Task{},
 | 
				
			||||||
		&PlanExecutionLog{},
 | 
							&PlanExecutionLog{},
 | 
				
			||||||
		&TaskExecutionLog{},
 | 
							&TaskExecutionLog{},
 | 
				
			||||||
		&PendingTask{},
 | 
							&PendingTask{},
 | 
				
			||||||
		&SensorData{},
 | 
					 | 
				
			||||||
		&DeviceCommandLog{},
 | 
					 | 
				
			||||||
		&PendingCollection{},
 | 
							&PendingCollection{},
 | 
				
			||||||
		&AreaController{},
 | 
					
 | 
				
			||||||
		&DeviceTemplate{},
 | 
							// Farm Asset Models
 | 
				
			||||||
		&UserActionLog{},
 | 
							&PigHouse{},
 | 
				
			||||||
 | 
							&Pen{},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Pig & Batch Models
 | 
				
			||||||
 | 
							&PigBatch{},
 | 
				
			||||||
 | 
							&PigBatchLog{},
 | 
				
			||||||
 | 
							&WeighingBatch{},
 | 
				
			||||||
 | 
							&WeighingRecord{},
 | 
				
			||||||
 | 
							&PigTransferLog{},
 | 
				
			||||||
 | 
							&PigSickLog{},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Pig Buy & Sell
 | 
				
			||||||
 | 
							&PigPurchase{},
 | 
				
			||||||
 | 
							&PigSale{},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Feed Models
 | 
				
			||||||
 | 
							&RawMaterial{},
 | 
				
			||||||
 | 
							&RawMaterialPurchase{},
 | 
				
			||||||
 | 
							&RawMaterialStockLog{},
 | 
				
			||||||
 | 
							&FeedFormula{},
 | 
				
			||||||
 | 
							&FeedFormulaComponent{},
 | 
				
			||||||
 | 
							&FeedUsageRecord{},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Medication Models
 | 
				
			||||||
 | 
							&Medication{},
 | 
				
			||||||
 | 
							&MedicationLog{},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										108
									
								
								internal/infra/models/pig_batch.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								internal/infra/models/pig_batch.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
				
			|||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
						和猪只、猪群本身相关的模型
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchStatus 定义了猪批次所处的不同阶段或状态
 | 
				
			||||||
 | 
					type PigBatchStatus string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						BatchStatusWeaning   PigBatchStatus = "保育" // 从断奶到保育结束
 | 
				
			||||||
 | 
						BatchStatusGrowing   PigBatchStatus = "生长" // 生长育肥阶段
 | 
				
			||||||
 | 
						BatchStatusFinishing PigBatchStatus = "育肥" // 最后的育肥阶段
 | 
				
			||||||
 | 
						BatchStatusForSale   PigBatchStatus = "待售" // 达到出栏标准
 | 
				
			||||||
 | 
						BatchStatusSold      PigBatchStatus = "已出售"
 | 
				
			||||||
 | 
						BatchStatusArchived  PigBatchStatus = "已归档" // 批次结束(如全群淘汰等)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchOriginType 定义了猪批次的来源
 | 
				
			||||||
 | 
					type PigBatchOriginType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						OriginTypeSelfFarrowed PigBatchOriginType = "自繁"
 | 
				
			||||||
 | 
						OriginTypePurchased    PigBatchOriginType = "外购"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatch 是猪批次的核心模型,代表了一群被共同管理的猪
 | 
				
			||||||
 | 
					type PigBatch struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						BatchNumber  string             `gorm:"size:50;not null;uniqueIndex;comment:批次编号,如 2024-W25-A01"`
 | 
				
			||||||
 | 
						OriginType   PigBatchOriginType `gorm:"size:20;not null;comment:批次来源 (自繁, 外购)"`
 | 
				
			||||||
 | 
						StartDate    time.Time          `gorm:"not null;comment:批次开始日期 (如转入日或购买日)"`
 | 
				
			||||||
 | 
						EndDate      time.Time          `gorm:"not null;comment:批次结束日期 (全部淘汰或售出)"`
 | 
				
			||||||
 | 
						InitialCount int                `gorm:"not null;comment:初始数量"`
 | 
				
			||||||
 | 
						Status       PigBatchStatus     `gorm:"size:20;not null;index;comment:批次状态"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (PigBatch) TableName() string {
 | 
				
			||||||
 | 
						return "pig_batches"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsActive 判断猪批次是否处于活跃状态
 | 
				
			||||||
 | 
					func (pb PigBatch) IsActive() bool {
 | 
				
			||||||
 | 
						return pb.Status != BatchStatusSold && pb.Status != BatchStatusArchived
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LogChangeType 定义了猪批次数量变更的类型
 | 
				
			||||||
 | 
					type LogChangeType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						ChangeTypeDeath       LogChangeType = "死亡"
 | 
				
			||||||
 | 
						ChangeTypeCull        LogChangeType = "淘汰"
 | 
				
			||||||
 | 
						ChangeTypeSale        LogChangeType = "销售"
 | 
				
			||||||
 | 
						ChangeTypeBuy         LogChangeType = "购买"
 | 
				
			||||||
 | 
						ChangeTypeTransferIn  LogChangeType = "转入"
 | 
				
			||||||
 | 
						ChangeTypeTransferOut LogChangeType = "转出"
 | 
				
			||||||
 | 
						ChangeTypeCorrection  LogChangeType = "盘点校正"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchLog 记录了猪批次数量或状态的每一次变更
 | 
				
			||||||
 | 
					type PigBatchLog struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						PigBatchID  uint          `gorm:"not null;index;comment:关联的猪批次ID"`
 | 
				
			||||||
 | 
						ChangeType  LogChangeType `gorm:"size:20;not null;comment:变更类型"`
 | 
				
			||||||
 | 
						ChangeCount int           `gorm:"not null;comment:数量变化,负数表示减少"`
 | 
				
			||||||
 | 
						Reason      string        `gorm:"size:255;comment:变更原因描述"`
 | 
				
			||||||
 | 
						BeforeCount int           `gorm:"not null;comment:变更前总数"`
 | 
				
			||||||
 | 
						AfterCount  int           `gorm:"not null;comment:变更后总数"`
 | 
				
			||||||
 | 
						OperatorID  uint          `gorm:"comment:操作员ID"`
 | 
				
			||||||
 | 
						HappenedAt  time.Time     `gorm:"primaryKey;comment:事件发生时间"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (PigBatchLog) TableName() string {
 | 
				
			||||||
 | 
						return "pig_batch_logs"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// WeighingBatch 记录了一次批次称重的信息
 | 
				
			||||||
 | 
					type WeighingBatch struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						WeighingTime time.Time `gorm:"primaryKey;comment:称重时间"`
 | 
				
			||||||
 | 
						Description  string    `gorm:"size:255;comment:批次称重描述"`
 | 
				
			||||||
 | 
						PigBatchID   uint      `gorm:"not null;index;comment:关联的猪批次ID"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (WeighingBatch) TableName() string {
 | 
				
			||||||
 | 
						return "weighing_batches"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// WeighingRecord 记录了单次称重信息
 | 
				
			||||||
 | 
					type WeighingRecord struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						Weight          float64   `gorm:"not null;comment:单只猪重量 (kg)"`
 | 
				
			||||||
 | 
						WeighingBatchID uint      `gorm:"not null;index;comment:关联的批次称重ID"`
 | 
				
			||||||
 | 
						PenID           uint      `gorm:"not null;index;comment:所在猪圈ID"`
 | 
				
			||||||
 | 
						OperatorID      uint      `gorm:"not null;comment:操作员ID"`
 | 
				
			||||||
 | 
						Remark          string    `gorm:"size:255;comment:备注"`
 | 
				
			||||||
 | 
						WeighingTime    time.Time `gorm:"primaryKey;comment:称重时间"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (WeighingRecord) TableName() string {
 | 
				
			||||||
 | 
						return "weighing_records"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										47
									
								
								internal/infra/models/pig_sick.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								internal/infra/models/pig_sick.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchSickPigTreatmentLocation 定义了病猪治疗地点
 | 
				
			||||||
 | 
					type PigBatchSickPigTreatmentLocation string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						TreatmentLocationOnSite  PigBatchSickPigTreatmentLocation = "原地治疗"
 | 
				
			||||||
 | 
						TreatmentLocationSickBay PigBatchSickPigTreatmentLocation = "病猪栏治疗"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchSickPigReasonType 定义了病猪变化的原因类型
 | 
				
			||||||
 | 
					type PigBatchSickPigReasonType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						SickPigReasonTypeIllness     PigBatchSickPigReasonType = "患病" // 猪只患病
 | 
				
			||||||
 | 
						SickPigReasonTypeRecovery    PigBatchSickPigReasonType = "康复" // 猪只康复
 | 
				
			||||||
 | 
						SickPigReasonTypeDeath       PigBatchSickPigReasonType = "死亡" // 猪只死亡
 | 
				
			||||||
 | 
						SickPigReasonTypeEliminate   PigBatchSickPigReasonType = "淘汰" // 猪只淘汰
 | 
				
			||||||
 | 
						SickPigReasonTypeTransferIn  PigBatchSickPigReasonType = "转入" // 病猪转入当前批次
 | 
				
			||||||
 | 
						SickPigReasonTypeTransferOut PigBatchSickPigReasonType = "转出" // 病猪转出当前批次 (例如转到其他批次或出售)
 | 
				
			||||||
 | 
						SickPigReasonTypeOther       PigBatchSickPigReasonType = "其他" // 其他原因
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigSickLog 记录了猪批次中病猪数量的变化日志
 | 
				
			||||||
 | 
					type PigSickLog struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						PigBatchID        uint                             `gorm:"primaryKey;comment:关联的猪批次ID"`
 | 
				
			||||||
 | 
						PenID             uint                             `gorm:"not null;index;comment:所在猪圈ID"`
 | 
				
			||||||
 | 
						ChangeCount       int                              `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"`
 | 
				
			||||||
 | 
						Reason            PigBatchSickPigReasonType        `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"`
 | 
				
			||||||
 | 
						BeforeCount       int                              `gorm:"comment:变化前的数量"`
 | 
				
			||||||
 | 
						AfterCount        int                              `gorm:"comment:变化后的数量"`
 | 
				
			||||||
 | 
						Remarks           string                           `gorm:"size:255;comment:备注"`
 | 
				
			||||||
 | 
						TreatmentLocation PigBatchSickPigTreatmentLocation `gorm:"size:50;comment:治疗地点"`
 | 
				
			||||||
 | 
						OperatorID        uint                             `gorm:"comment:操作员ID"`
 | 
				
			||||||
 | 
						HappenedAt        time.Time                        `gorm:"primaryKey;comment:事件发生时间"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (PigSickLog) TableName() string {
 | 
				
			||||||
 | 
						return "pig_sick_logs"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										41
									
								
								internal/infra/models/pig_trade.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								internal/infra/models/pig_trade.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigPurchase 记录了猪只采购信息
 | 
				
			||||||
 | 
					type PigPurchase struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						PigBatchID   uint      `gorm:"not null;index;comment:关联的猪批次ID"`
 | 
				
			||||||
 | 
						PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"`
 | 
				
			||||||
 | 
						Supplier     string    `gorm:"comment:供应商"`
 | 
				
			||||||
 | 
						Quantity     int       `gorm:"not null;comment:采购数量"`
 | 
				
			||||||
 | 
						UnitPrice    float64   `gorm:"not null;comment:单价"`
 | 
				
			||||||
 | 
						TotalPrice   float64   `gorm:"not null;comment:总价"`
 | 
				
			||||||
 | 
						Remarks      string    `gorm:"size:255;comment:备注"`
 | 
				
			||||||
 | 
						OperatorID   uint      `gorm:"comment:操作员ID"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (PigPurchase) TableName() string {
 | 
				
			||||||
 | 
						return "pig_purchases"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigSale 记录了猪只销售信息
 | 
				
			||||||
 | 
					type PigSale struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						PigBatchID uint      `gorm:"not null;index;comment:关联的猪批次ID"`
 | 
				
			||||||
 | 
						SaleDate   time.Time `gorm:"primaryKey;comment:销售日期"`
 | 
				
			||||||
 | 
						Buyer      string    `gorm:"comment:购买方"`
 | 
				
			||||||
 | 
						Quantity   int       `gorm:"not null;comment:销售数量"`
 | 
				
			||||||
 | 
						UnitPrice  float64   `gorm:"not null;comment:单价"`
 | 
				
			||||||
 | 
						TotalPrice float64   `gorm:"not null;comment:总价"`
 | 
				
			||||||
 | 
						Remarks    string    `gorm:"size:255;comment:备注"`
 | 
				
			||||||
 | 
						OperatorID uint      `gorm:"comment:操作员ID"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (PigSale) TableName() string {
 | 
				
			||||||
 | 
						return "pig_sales"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								internal/infra/models/pig_transfer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/infra/models/pig_transfer.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigTransferType 定义了猪只迁移的类型
 | 
				
			||||||
 | 
					type PigTransferType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						PigTransferTypeInternal             PigTransferType = "群内调栏" // 同一猪群内猪栏间的调动
 | 
				
			||||||
 | 
						PigTransferTypeCrossBatch           PigTransferType = "跨群调栏" // 不同猪群间的调动
 | 
				
			||||||
 | 
						PigTransferTypeSale                 PigTransferType = "销售"   // 猪只售出
 | 
				
			||||||
 | 
						PigTransferTypeDeath                PigTransferType = "死亡"   // 猪只死亡
 | 
				
			||||||
 | 
						PigTransferTypeCull                 PigTransferType = "淘汰"   // 猪只淘汰
 | 
				
			||||||
 | 
						PigTransferTypePurchase             PigTransferType = "新购入"  // 新购入猪只
 | 
				
			||||||
 | 
						PigTransferTypeDeliveryRoomTransfor PigTransferType = "产房转入" // 产房转入
 | 
				
			||||||
 | 
						// 可以根据业务需求添加更多类型,例如:转出到其他农场等
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigTransferLog 记录了每一次猪只数量在猪栏间的变动事件。
 | 
				
			||||||
 | 
					// 它作为事件溯源的基础,用于推算任意时间点猪栏的猪只数量。
 | 
				
			||||||
 | 
					type PigTransferLog struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						TransferTime  time.Time       `gorm:"primaryKey;comment:迁移发生时间" json:"transfer_time"`         // 迁移发生时间,作为联合主键
 | 
				
			||||||
 | 
						PigBatchID    uint            `gorm:"primaryKey;comment:关联的猪群ID" json:"pig_batch_id"`         // 关联的猪群ID,作为联合主键
 | 
				
			||||||
 | 
						PenID         uint            `gorm:"primaryKey;comment:发生变动的猪栏ID" json:"pen_id"`             // 发生变动的猪栏ID,作为联合主键
 | 
				
			||||||
 | 
						Quantity      int             `gorm:"not null;comment:变动数量(正数表示增加,负数表示减少)" json:"quantity"`   // 变动数量(正数表示增加,负数减少)
 | 
				
			||||||
 | 
						Type          PigTransferType `gorm:"not null;comment:变动类型" json:"type"`                      // 变动类型,使用枚举类型
 | 
				
			||||||
 | 
						CorrelationID string          `gorm:"comment:用于关联一次完整操作(如一次调栏会产生两条日志)" json:"correlation_id"` // 用于关联一次完整操作
 | 
				
			||||||
 | 
						OperatorID    uint            `gorm:"not null;comment:操作员ID" json:"operator_id"`              // 操作员ID
 | 
				
			||||||
 | 
						Remarks       string          `gorm:"comment:备注" json:"remarks"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p PigTransferLog) TableName() string {
 | 
				
			||||||
 | 
						return "pig_transfer_logs"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -15,25 +15,25 @@ import (
 | 
				
			|||||||
type PlanExecutionType string
 | 
					type PlanExecutionType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	PlanExecutionTypeAutomatic PlanExecutionType = "automatic" // 自动执行 (包含定时和循环)
 | 
						PlanExecutionTypeAutomatic PlanExecutionType = "自动" // 自动执行 (包含定时和循环)
 | 
				
			||||||
	PlanExecutionTypeManual    PlanExecutionType = "manual"    // 手动执行
 | 
						PlanExecutionTypeManual    PlanExecutionType = "手动" // 手动执行
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PlanContentType 定义了计划包含的内容类型
 | 
					// PlanContentType 定义了计划包含的内容类型
 | 
				
			||||||
type PlanContentType string
 | 
					type PlanContentType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	PlanContentTypeSubPlans PlanContentType = "sub_plans" // 计划包含子计划
 | 
						PlanContentTypeSubPlans PlanContentType = "子计划" // 计划包含子计划
 | 
				
			||||||
	PlanContentTypeTasks    PlanContentType = "tasks"     // 计划包含任务
 | 
						PlanContentTypeTasks    PlanContentType = "任务"  // 计划包含任务
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TaskType 定义了任务的类型,每个类型可以对应 task 包中的一个具体动作
 | 
					// TaskType 定义了任务的类型,每个类型可以对应 task 包中的一个具体动作
 | 
				
			||||||
type TaskType string
 | 
					type TaskType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	TaskPlanAnalysis          TaskType = "plan_analysis"       // 解析Plan的Task列表并添加到待执行队列的特殊任务
 | 
						TaskPlanAnalysis          TaskType = "计划分析" // 解析Plan的Task列表并添加到待执行队列的特殊任务
 | 
				
			||||||
	TaskTypeWaiting           TaskType = "waiting"             // 等待任务
 | 
						TaskTypeWaiting           TaskType = "等待"   // 等待任务
 | 
				
			||||||
	TaskTypeReleaseFeedWeight TaskType = "release_feed_weight" // 下料口释放指定重量任务
 | 
						TaskTypeReleaseFeedWeight TaskType = "下料"   // 下料口释放指定重量任务
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// -- Task Parameters --
 | 
					// -- Task Parameters --
 | 
				
			||||||
@@ -42,13 +42,14 @@ const (
 | 
				
			|||||||
	ParamsPlanID = "plan_id"
 | 
						ParamsPlanID = "plan_id"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type PlanStatus uint8
 | 
					// PlanStatus 定义了计划的状态
 | 
				
			||||||
 | 
					type PlanStatus string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	PlanStatusDisabled PlanStatus = 0 // 禁用计划
 | 
						PlanStatusDisabled PlanStatus = "已禁用"  // 禁用计划
 | 
				
			||||||
	PlanStatusEnabled  PlanStatus = 1 // 启用计划
 | 
						PlanStatusEnabled  PlanStatus = "已启用"  // 启用计划
 | 
				
			||||||
	PlanStatusStopeed  PlanStatus = 2 // 执行完毕
 | 
						PlanStatusStopped  PlanStatus = "执行完毕" // 执行完毕
 | 
				
			||||||
	PlanStatusFailed   PlanStatus = 3 // 执行失败
 | 
						PlanStatusFailed   PlanStatus = "执行失败" // 执行失败
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Plan 代表系统中的一个计划,可以包含子计划或任务
 | 
					// Plan 代表系统中的一个计划,可以包含子计划或任务
 | 
				
			||||||
@@ -58,7 +59,7 @@ type Plan struct {
 | 
				
			|||||||
	Name          string            `gorm:"not null" json:"name"`
 | 
						Name          string            `gorm:"not null" json:"name"`
 | 
				
			||||||
	Description   string            `json:"description"`
 | 
						Description   string            `json:"description"`
 | 
				
			||||||
	ExecutionType PlanExecutionType `gorm:"not null;index" json:"execution_type"`
 | 
						ExecutionType PlanExecutionType `gorm:"not null;index" json:"execution_type"`
 | 
				
			||||||
	Status        PlanStatus        `gorm:"default:0;index" json:"status"`  // 计划是否被启动
 | 
						Status        PlanStatus        `gorm:"default:'已禁用';index" json:"status"` // 计划是否被启动
 | 
				
			||||||
	ExecuteNum    uint              `gorm:"default:0" json:"execute_num"`      // 计划预期执行次数
 | 
						ExecuteNum    uint              `gorm:"default:0" json:"execute_num"`      // 计划预期执行次数
 | 
				
			||||||
	ExecuteCount  uint              `gorm:"default:0" json:"execute_count"`    // 执行计数器
 | 
						ExecuteCount  uint              `gorm:"default:0" json:"execute_count"`    // 执行计数器
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,11 +10,11 @@ import (
 | 
				
			|||||||
type SensorType string
 | 
					type SensorType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	SensorTypeSignalMetrics SensorType = "signal_metrics" // 信号强度
 | 
						SensorTypeSignalMetrics SensorType = "信号强度" // 信号强度
 | 
				
			||||||
	SensorTypeBatteryLevel  SensorType = "battery_level"  // 电池电量
 | 
						SensorTypeBatteryLevel  SensorType = "电池电量" // 电池电量
 | 
				
			||||||
	SensorTypeTemperature   SensorType = "temperature"    // 温度
 | 
						SensorTypeTemperature   SensorType = "温度"   // 温度
 | 
				
			||||||
	SensorTypeHumidity      SensorType = "humidity"       // 湿度
 | 
						SensorTypeHumidity      SensorType = "湿度"   // 湿度
 | 
				
			||||||
	SensorTypeWeight        SensorType = "weight"         // 重量
 | 
						SensorTypeWeight        SensorType = "重量"   // 重量
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SignalMetrics 存储信号强度数据
 | 
					// SignalMetrics 存储信号强度数据
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,7 +96,7 @@ func (r *gormExecutionLogRepository) CreateTaskExecutionLogsInBatch(logs []*mode
 | 
				
			|||||||
	if len(logs) == 0 {
 | 
						if len(logs) == 0 {
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// GORM 的 Create 传入一个切片指针会执行批量插入。
 | 
						// GORM 的 CreateTx 传入一个切片指针会执行批量插入。
 | 
				
			||||||
	return r.db.Create(&logs).Error
 | 
						return r.db.Create(&logs).Error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								internal/infra/repository/group_medication_log_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								internal/infra/repository/group_medication_log_repository.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MedicationLogRepository 定义了与群体用药日志模型相关的数据库操作接口。
 | 
				
			||||||
 | 
					type MedicationLogRepository interface {
 | 
				
			||||||
 | 
						CreateMedicationLog(log *models.MedicationLog) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gormMedicationLogRepository 是 MedicationLogRepository 接口的 GORM 实现。
 | 
				
			||||||
 | 
					type gormMedicationLogRepository struct {
 | 
				
			||||||
 | 
						db *gorm.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewGormMedicationLogRepository 创建一个新的 MedicationLogRepository GORM 实现实例。
 | 
				
			||||||
 | 
					func NewGormMedicationLogRepository(db *gorm.DB) MedicationLogRepository {
 | 
				
			||||||
 | 
						return &gormMedicationLogRepository{db: db}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateMedicationLog 创建一条新的群体用药日志记录
 | 
				
			||||||
 | 
					func (r *gormMedicationLogRepository) CreateMedicationLog(log *models.MedicationLog) error {
 | 
				
			||||||
 | 
						return r.db.Create(log).Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										55
									
								
								internal/infra/repository/pig_batch_log_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								internal/infra/repository/pig_batch_log_repository.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time" // 引入 time 包
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchLogRepository 定义了与猪批次日志相关的数据库操作接口。
 | 
				
			||||||
 | 
					type PigBatchLogRepository interface {
 | 
				
			||||||
 | 
						// CreateTx 在指定的事务中创建一条新的猪批次日志。
 | 
				
			||||||
 | 
						CreateTx(tx *gorm.DB, log *models.PigBatchLog) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// GetLogsByBatchIDAndDateRangeTx 在指定的事务中,获取指定批次在特定时间范围内的所有日志记录。
 | 
				
			||||||
 | 
						GetLogsByBatchIDAndDateRangeTx(tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// GetLastLogByBatchIDTx 在指定的事务中,获取某批次的最后一条日志记录。
 | 
				
			||||||
 | 
						GetLastLogByBatchIDTx(tx *gorm.DB, batchID uint) (*models.PigBatchLog, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gormPigBatchLogRepository 是 PigBatchLogRepository 的 GORM 实现。
 | 
				
			||||||
 | 
					type gormPigBatchLogRepository struct {
 | 
				
			||||||
 | 
						db *gorm.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewGormPigBatchLogRepository 创建一个新的 PigBatchLogRepository 实例。
 | 
				
			||||||
 | 
					func NewGormPigBatchLogRepository(db *gorm.DB) PigBatchLogRepository {
 | 
				
			||||||
 | 
						return &gormPigBatchLogRepository{db: db}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Create 实现了创建猪批次日志的逻辑。
 | 
				
			||||||
 | 
					func (r *gormPigBatchLogRepository) CreateTx(tx *gorm.DB, log *models.PigBatchLog) error {
 | 
				
			||||||
 | 
						return tx.Create(log).Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetLogsByBatchIDAndDateRangeTx 实现了在指定的事务中,获取指定批次在特定时间范围内的所有日志记录的逻辑。
 | 
				
			||||||
 | 
					func (r *gormPigBatchLogRepository) GetLogsByBatchIDAndDateRangeTx(tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error) {
 | 
				
			||||||
 | 
						var logs []*models.PigBatchLog
 | 
				
			||||||
 | 
						err := tx.Where("pig_batch_id = ? AND created_at >= ? AND created_at <= ?", batchID, startDate, endDate).Find(&logs).Error
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return logs, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetLastLogByBatchIDTx 实现了在指定的事务中,获取某批次的最后一条日志记录的逻辑。
 | 
				
			||||||
 | 
					func (r *gormPigBatchLogRepository) GetLastLogByBatchIDTx(tx *gorm.DB, batchID uint) (*models.PigBatchLog, error) {
 | 
				
			||||||
 | 
						var log models.PigBatchLog
 | 
				
			||||||
 | 
						err := tx.Where("pig_batch_id = ?", batchID).Order("id DESC").First(&log).Error
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &log, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										102
									
								
								internal/infra/repository/pig_batch_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								internal/infra/repository/pig_batch_repository.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigBatchRepository 定义了与猪批次相关的数据库操作接口
 | 
				
			||||||
 | 
					type PigBatchRepository interface {
 | 
				
			||||||
 | 
						CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error)
 | 
				
			||||||
 | 
						CreatePigBatchTx(tx *gorm.DB, batch *models.PigBatch) (*models.PigBatch, error)
 | 
				
			||||||
 | 
						GetPigBatchByID(id uint) (*models.PigBatch, error)
 | 
				
			||||||
 | 
						GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error)
 | 
				
			||||||
 | 
						// UpdatePigBatch 更新一个猪批次,返回更新后的批次、受影响的行数和错误
 | 
				
			||||||
 | 
						UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, int64, error)
 | 
				
			||||||
 | 
						// DeletePigBatch 根据ID删除一个猪批次,返回受影响的行数和错误
 | 
				
			||||||
 | 
						DeletePigBatch(id uint) (int64, error)
 | 
				
			||||||
 | 
						DeletePigBatchTx(tx *gorm.DB, id uint) (int64, error)
 | 
				
			||||||
 | 
						ListPigBatches(isActive *bool) ([]*models.PigBatch, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gormPigBatchRepository 是 PigBatchRepository 的 GORM 实现
 | 
				
			||||||
 | 
					type gormPigBatchRepository struct {
 | 
				
			||||||
 | 
						db *gorm.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewGormPigBatchRepository 创建一个新的 PigBatchRepository GORM 实现实例
 | 
				
			||||||
 | 
					func NewGormPigBatchRepository(db *gorm.DB) PigBatchRepository {
 | 
				
			||||||
 | 
						return &gormPigBatchRepository{db: db}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigBatch 创建一个新的猪批次
 | 
				
			||||||
 | 
					func (r *gormPigBatchRepository) CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) {
 | 
				
			||||||
 | 
						return r.CreatePigBatchTx(r.db, batch)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigBatchTx 在指定的事务中,创建一个新的猪批次
 | 
				
			||||||
 | 
					func (r *gormPigBatchRepository) CreatePigBatchTx(tx *gorm.DB, batch *models.PigBatch) (*models.PigBatch, error) {
 | 
				
			||||||
 | 
						if err := tx.Create(batch).Error; err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return batch, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPigBatchByID 根据ID获取单个猪批次
 | 
				
			||||||
 | 
					func (r *gormPigBatchRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) {
 | 
				
			||||||
 | 
						return r.GetPigBatchByIDTx(r.db, id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePigBatch 更新一个猪批次
 | 
				
			||||||
 | 
					func (r *gormPigBatchRepository) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, int64, error) {
 | 
				
			||||||
 | 
						result := r.db.Model(&models.PigBatch{}).Where("id = ?", batch.ID).Updates(batch)
 | 
				
			||||||
 | 
						if result.Error != nil {
 | 
				
			||||||
 | 
							return nil, 0, result.Error
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 返回更新后的批次、受影响的行数和错误
 | 
				
			||||||
 | 
						return batch, result.RowsAffected, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeletePigBatch 根据ID删除一个猪批次 (GORM 会执行软删除)
 | 
				
			||||||
 | 
					func (r *gormPigBatchRepository) DeletePigBatch(id uint) (int64, error) {
 | 
				
			||||||
 | 
						return r.DeletePigBatchTx(r.db, id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *gormPigBatchRepository) DeletePigBatchTx(tx *gorm.DB, id uint) (int64, error) {
 | 
				
			||||||
 | 
						result := tx.Delete(&models.PigBatch{}, id)
 | 
				
			||||||
 | 
						if result.Error != nil {
 | 
				
			||||||
 | 
							return 0, result.Error
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 返回受影响的行数和错误
 | 
				
			||||||
 | 
						return result.RowsAffected, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPigBatches 批量查询猪批次,支持根据 IsActive 筛选
 | 
				
			||||||
 | 
					func (r *gormPigBatchRepository) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) {
 | 
				
			||||||
 | 
						var batches []*models.PigBatch
 | 
				
			||||||
 | 
						query := r.db.Model(&models.PigBatch{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if isActive != nil {
 | 
				
			||||||
 | 
							if *isActive {
 | 
				
			||||||
 | 
								// 查询活跃的批次:状态不是已出售或已归档
 | 
				
			||||||
 | 
								query = query.Where("status NOT IN (?) ", []models.PigBatchStatus{models.BatchStatusSold, models.BatchStatusArchived})
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// 查询非活跃的批次:状态是已出售或已归档
 | 
				
			||||||
 | 
								query = query.Where("status IN (?) ", []models.PigBatchStatus{models.BatchStatusSold, models.BatchStatusArchived})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := query.Find(&batches).Error; err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return batches, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPigBatchByIDTx 在指定的事务中,通过ID获取单个猪批次
 | 
				
			||||||
 | 
					func (r *gormPigBatchRepository) GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) {
 | 
				
			||||||
 | 
						var batch models.PigBatch
 | 
				
			||||||
 | 
						if err := tx.First(&batch, id).Error; err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &batch, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										79
									
								
								internal/infra/repository/pig_farm_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								internal/infra/repository/pig_farm_repository.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigFarmRepository 定义了与猪场资产(猪舍、猪栏)相关的数据库操作接口
 | 
				
			||||||
 | 
					type PigFarmRepository interface {
 | 
				
			||||||
 | 
						// PigHouse methods
 | 
				
			||||||
 | 
						CreatePigHouse(house *models.PigHouse) error
 | 
				
			||||||
 | 
						GetPigHouseByID(id uint) (*models.PigHouse, error)
 | 
				
			||||||
 | 
						ListPigHouses() ([]models.PigHouse, error)
 | 
				
			||||||
 | 
						// UpdatePigHouse 更新一个猪舍,返回受影响的行数和错误
 | 
				
			||||||
 | 
						UpdatePigHouse(house *models.PigHouse) (int64, error)
 | 
				
			||||||
 | 
						// DeletePigHouse 根据ID删除一个猪舍,返回受影响的行数和错误
 | 
				
			||||||
 | 
						DeletePigHouse(id uint) (int64, error)
 | 
				
			||||||
 | 
						CountPensInHouse(houseID uint) (int64, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现
 | 
				
			||||||
 | 
					type gormPigFarmRepository struct {
 | 
				
			||||||
 | 
						db *gorm.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewGormPigFarmRepository 创建一个新的 PigFarmRepository GORM 实现实例
 | 
				
			||||||
 | 
					func NewGormPigFarmRepository(db *gorm.DB) PigFarmRepository {
 | 
				
			||||||
 | 
						return &gormPigFarmRepository{db: db}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- PigHouse Implementation ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigHouse 创建一个新的猪舍
 | 
				
			||||||
 | 
					func (r *gormPigFarmRepository) CreatePigHouse(house *models.PigHouse) error {
 | 
				
			||||||
 | 
						return r.db.Create(house).Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPigHouseByID 根据ID获取单个猪舍
 | 
				
			||||||
 | 
					func (r *gormPigFarmRepository) GetPigHouseByID(id uint) (*models.PigHouse, error) {
 | 
				
			||||||
 | 
						var house models.PigHouse
 | 
				
			||||||
 | 
						if err := r.db.First(&house, id).Error; err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &house, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPigHouses 列出所有猪舍
 | 
				
			||||||
 | 
					func (r *gormPigFarmRepository) ListPigHouses() ([]models.PigHouse, error) {
 | 
				
			||||||
 | 
						var houses []models.PigHouse
 | 
				
			||||||
 | 
						if err := r.db.Find(&houses).Error; err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return houses, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePigHouse 更新一个猪舍,返回受影响的行数和错误
 | 
				
			||||||
 | 
					func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) (int64, error) {
 | 
				
			||||||
 | 
						result := r.db.Model(&models.PigHouse{}).Where("id = ?", house.ID).Updates(house)
 | 
				
			||||||
 | 
						if result.Error != nil {
 | 
				
			||||||
 | 
							return 0, result.Error
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return result.RowsAffected, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeletePigHouse 根据ID删除一个猪舍,返回受影响的行数和错误
 | 
				
			||||||
 | 
					func (r *gormPigFarmRepository) DeletePigHouse(id uint) (int64, error) {
 | 
				
			||||||
 | 
						result := r.db.Delete(&models.PigHouse{}, id)
 | 
				
			||||||
 | 
						if result.Error != nil {
 | 
				
			||||||
 | 
							return 0, result.Error
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return result.RowsAffected, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CountPensInHouse 统计猪舍中的猪栏数量
 | 
				
			||||||
 | 
					func (r *gormPigFarmRepository) CountPensInHouse(houseID uint) (int64, error) {
 | 
				
			||||||
 | 
						var count int64
 | 
				
			||||||
 | 
						err := r.db.Model(&models.Pen{}).Where("house_id = ?", houseID).Count(&count).Error
 | 
				
			||||||
 | 
						return count, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										97
									
								
								internal/infra/repository/pig_pen_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								internal/infra/repository/pig_pen_repository.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigPenRepository 定义了与猪栏模型相关的数据库操作接口。
 | 
				
			||||||
 | 
					type PigPenRepository interface {
 | 
				
			||||||
 | 
						CreatePen(pen *models.Pen) error
 | 
				
			||||||
 | 
						// GetPenByID 根据ID获取单个猪栏 (非事务性)
 | 
				
			||||||
 | 
						GetPenByID(id uint) (*models.Pen, error)
 | 
				
			||||||
 | 
						// GetPenByIDTx 根据ID获取单个猪栏 (事务性)
 | 
				
			||||||
 | 
						GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error)
 | 
				
			||||||
 | 
						ListPens() ([]models.Pen, error)
 | 
				
			||||||
 | 
						// UpdatePen 更新一个猪栏,返回受影响的行数和错误
 | 
				
			||||||
 | 
						UpdatePen(pen *models.Pen) (int64, error)
 | 
				
			||||||
 | 
						// DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误
 | 
				
			||||||
 | 
						DeletePen(id uint) (int64, error)
 | 
				
			||||||
 | 
						// GetPensByBatchIDTx 根据批次ID获取所有关联的猪栏 (事务性)
 | 
				
			||||||
 | 
						GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error)
 | 
				
			||||||
 | 
						// UpdatePenFieldsTx 更新猪栏的指定字段 (事务性)
 | 
				
			||||||
 | 
						UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gormPigPenRepository 是 PigPenRepository 接口的 GORM 实现。
 | 
				
			||||||
 | 
					type gormPigPenRepository struct {
 | 
				
			||||||
 | 
						db *gorm.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewGormPigPenRepository 创建一个新的 PigPenRepository GORM 实现实例。
 | 
				
			||||||
 | 
					func NewGormPigPenRepository(db *gorm.DB) PigPenRepository {
 | 
				
			||||||
 | 
						return &gormPigPenRepository{db: db}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePen 创建一个新的猪栏
 | 
				
			||||||
 | 
					func (r *gormPigPenRepository) CreatePen(pen *models.Pen) error {
 | 
				
			||||||
 | 
						return r.db.Create(pen).Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPenByID 根据ID获取单个猪栏 (非事务性)
 | 
				
			||||||
 | 
					func (r *gormPigPenRepository) GetPenByID(id uint) (*models.Pen, error) {
 | 
				
			||||||
 | 
						return r.GetPenByIDTx(r.db, id) // 非Tx方法直接调用Tx方法
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPenByIDTx 在指定的事务中,通过ID获取单个猪栏信息。
 | 
				
			||||||
 | 
					func (r *gormPigPenRepository) GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error) {
 | 
				
			||||||
 | 
						var pen models.Pen
 | 
				
			||||||
 | 
						if err := tx.First(&pen, id).Error; err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &pen, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPens 列出所有猪栏
 | 
				
			||||||
 | 
					func (r *gormPigPenRepository) ListPens() ([]models.Pen, error) {
 | 
				
			||||||
 | 
						var pens []models.Pen
 | 
				
			||||||
 | 
						if err := r.db.Find(&pens).Error; err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return pens, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePen 更新一个猪栏,返回受影响的行数和错误
 | 
				
			||||||
 | 
					func (r *gormPigPenRepository) UpdatePen(pen *models.Pen) (int64, error) {
 | 
				
			||||||
 | 
						result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen)
 | 
				
			||||||
 | 
						if result.Error != nil {
 | 
				
			||||||
 | 
							return 0, result.Error
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return result.RowsAffected, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误
 | 
				
			||||||
 | 
					func (r *gormPigPenRepository) DeletePen(id uint) (int64, error) {
 | 
				
			||||||
 | 
						result := r.db.Delete(&models.Pen{}, id)
 | 
				
			||||||
 | 
						if result.Error != nil {
 | 
				
			||||||
 | 
							return 0, result.Error
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return result.RowsAffected, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPensByBatchIDTx 在指定的事务中,获取一个猪群当前关联的所有猪栏。
 | 
				
			||||||
 | 
					func (r *gormPigPenRepository) GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) {
 | 
				
			||||||
 | 
						var pens []*models.Pen
 | 
				
			||||||
 | 
						// 注意:PigBatchID 是指针类型,需要处理 nil 值
 | 
				
			||||||
 | 
						result := tx.Where("pig_batch_id = ?", batchID).Find(&pens)
 | 
				
			||||||
 | 
						if result.Error != nil {
 | 
				
			||||||
 | 
							return nil, result.Error
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return pens, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePenFieldsTx 在指定的事务中,更新一个猪栏的指定字段。
 | 
				
			||||||
 | 
					func (r *gormPigPenRepository) UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error {
 | 
				
			||||||
 | 
						result := tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates)
 | 
				
			||||||
 | 
						return result.Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										53
									
								
								internal/infra/repository/pig_sick_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								internal/infra/repository/pig_sick_repository.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigSickLogRepository 定义了与病猪日志模型相关的数据库操作接口。
 | 
				
			||||||
 | 
					type PigSickLogRepository interface {
 | 
				
			||||||
 | 
						// CreatePigSickLog 创建一条新的病猪日志记录
 | 
				
			||||||
 | 
						CreatePigSickLog(log *models.PigSickLog) error
 | 
				
			||||||
 | 
						CreatePigSickLogTx(tx *gorm.DB, log *models.PigSickLog) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// GetLastLogByBatchTx 在事务中获取指定批次和猪栏的最新一条 PigSickLog 记录
 | 
				
			||||||
 | 
						GetLastLogByBatchTx(tx *gorm.DB, batchID uint) (*models.PigSickLog, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gormPigSickLogRepository 是 PigSickLogRepository 接口的 GORM 实现。
 | 
				
			||||||
 | 
					type gormPigSickLogRepository struct {
 | 
				
			||||||
 | 
						db *gorm.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewGormPigSickLogRepository 创建一个新的 PigSickLogRepository GORM 实现实例。
 | 
				
			||||||
 | 
					func NewGormPigSickLogRepository(db *gorm.DB) PigSickLogRepository {
 | 
				
			||||||
 | 
						return &gormPigSickLogRepository{db: db}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigSickLog 创建一条新的病猪日志记录
 | 
				
			||||||
 | 
					func (r *gormPigSickLogRepository) CreatePigSickLog(log *models.PigSickLog) error {
 | 
				
			||||||
 | 
						return r.CreatePigSickLogTx(r.db, log)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (r *gormPigSickLogRepository) CreatePigSickLogTx(tx *gorm.DB, log *models.PigSickLog) error {
 | 
				
			||||||
 | 
						return tx.Create(log).Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetLastLogByBatchTx 在事务中获取指定批次和猪栏的最新一条 PigSickLog 记录
 | 
				
			||||||
 | 
					func (r *gormPigSickLogRepository) GetLastLogByBatchTx(tx *gorm.DB, batchID uint) (*models.PigSickLog, error) {
 | 
				
			||||||
 | 
						var lastLog models.PigSickLog
 | 
				
			||||||
 | 
						err := tx.
 | 
				
			||||||
 | 
							Where("pig_batch_id = ?", batchID).
 | 
				
			||||||
 | 
							Order("happened_at DESC"). // 按时间降序排列
 | 
				
			||||||
 | 
							First(&lastLog).Error      // 获取第一条记录 (即最新一条)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
				
			||||||
 | 
								return nil, gorm.ErrRecordNotFound // 明确返回记录未找到错误
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &lastLog, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								internal/infra/repository/pig_trade_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								internal/infra/repository/pig_trade_repository.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigTradeRepository 定义了猪只交易数据持久化的接口。
 | 
				
			||||||
 | 
					// 领域服务通过此接口与数据层交互,实现解耦。
 | 
				
			||||||
 | 
					type PigTradeRepository interface {
 | 
				
			||||||
 | 
						// CreatePigSaleTx 在数据库中创建一条猪只销售记录。
 | 
				
			||||||
 | 
						CreatePigSaleTx(tx *gorm.DB, sale *models.PigSale) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// CreatePigPurchaseTx 在数据库中创建一条猪只采购记录。
 | 
				
			||||||
 | 
						CreatePigPurchaseTx(tx *gorm.DB, purchase *models.PigPurchase) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gormPigTradeRepository 是 PigTradeRepository 接口的 GORM 实现。
 | 
				
			||||||
 | 
					type gormPigTradeRepository struct {
 | 
				
			||||||
 | 
						db *gorm.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewGormPigTradeRepository 创建一个新的 PigTradeRepository GORM 实现实例。
 | 
				
			||||||
 | 
					func NewGormPigTradeRepository(db *gorm.DB) PigTradeRepository {
 | 
				
			||||||
 | 
						return &gormPigTradeRepository{db: db}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigSaleTx 实现了在数据库中创建猪只销售记录的逻辑。
 | 
				
			||||||
 | 
					func (r *gormPigTradeRepository) CreatePigSaleTx(tx *gorm.DB, sale *models.PigSale) error {
 | 
				
			||||||
 | 
						return tx.Create(sale).Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigPurchaseTx 实现了在数据库中创建猪只采购记录的逻辑。
 | 
				
			||||||
 | 
					func (r *gormPigTradeRepository) CreatePigPurchaseTx(tx *gorm.DB, purchase *models.PigPurchase) error {
 | 
				
			||||||
 | 
						return tx.Create(purchase).Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								internal/infra/repository/pig_transfer_log_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/infra/repository/pig_transfer_log_repository.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PigTransferLogRepository 定义了猪只迁移日志数据持久化的接口。
 | 
				
			||||||
 | 
					type PigTransferLogRepository interface {
 | 
				
			||||||
 | 
						// CreatePigTransferLog 在数据库中创建一条猪只迁移日志记录。
 | 
				
			||||||
 | 
						CreatePigTransferLog(tx *gorm.DB, log *models.PigTransferLog) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// GetLogsForPenSince 获取指定猪栏自特定时间点以来的所有迁移日志,按时间倒序排列。
 | 
				
			||||||
 | 
						GetLogsForPenSince(tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gormPigTransferLogRepository 是 PigTransferLogRepository 接口的 GORM 实现。
 | 
				
			||||||
 | 
					type gormPigTransferLogRepository struct {
 | 
				
			||||||
 | 
						db *gorm.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewGormPigTransferLogRepository 创建一个新的 PigTransferLogRepository GORM 实现实例。
 | 
				
			||||||
 | 
					func NewGormPigTransferLogRepository(db *gorm.DB) PigTransferLogRepository {
 | 
				
			||||||
 | 
						return &gormPigTransferLogRepository{db: db}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePigTransferLog 实现了在数据库中创建猪只迁移日志记录的逻辑。
 | 
				
			||||||
 | 
					func (r *gormPigTransferLogRepository) CreatePigTransferLog(tx *gorm.DB, log *models.PigTransferLog) error {
 | 
				
			||||||
 | 
						return tx.Create(log).Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetLogsForPenSince 实现了获取猪栏自特定时间点以来所有迁移日志的逻辑。
 | 
				
			||||||
 | 
					func (r *gormPigTransferLogRepository) GetLogsForPenSince(tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error) {
 | 
				
			||||||
 | 
						var logs []*models.PigTransferLog
 | 
				
			||||||
 | 
						err := tx.Where("pen_id = ? AND transfer_time >= ?", penID, since).Order("transfer_time DESC").Find(&logs).Error
 | 
				
			||||||
 | 
						return logs, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										60
									
								
								internal/infra/repository/unit_of_work.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								internal/infra/repository/unit_of_work.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnitOfWork 定义了工作单元接口,用于抽象事务管理
 | 
				
			||||||
 | 
					type UnitOfWork interface {
 | 
				
			||||||
 | 
						// ExecuteInTransaction 在一个数据库事务中执行给定的函数。
 | 
				
			||||||
 | 
						// 如果函数返回错误,事务将被回滚;否则,事务将被提交。
 | 
				
			||||||
 | 
						// tx 参数是当前事务的 GORM DB 实例,应传递给所有仓库方法。
 | 
				
			||||||
 | 
						ExecuteInTransaction(fn func(tx *gorm.DB) error) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gormUnitOfWork 是 UnitOfWork 接口的 GORM 实现
 | 
				
			||||||
 | 
					type gormUnitOfWork struct {
 | 
				
			||||||
 | 
						db     *gorm.DB
 | 
				
			||||||
 | 
						logger *logs.Logger // 添加日志记录器
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewGormUnitOfWork 创建一个新的 gormUnitOfWork 实例
 | 
				
			||||||
 | 
					func NewGormUnitOfWork(db *gorm.DB, logger *logs.Logger) UnitOfWork {
 | 
				
			||||||
 | 
						return &gormUnitOfWork{db: db, logger: logger}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ExecuteInTransaction 实现了 UnitOfWork 接口的事务执行逻辑
 | 
				
			||||||
 | 
					func (u *gormUnitOfWork) ExecuteInTransaction(fn func(tx *gorm.DB) error) error {
 | 
				
			||||||
 | 
						tx := u.db.Begin()
 | 
				
			||||||
 | 
						if tx.Error != nil {
 | 
				
			||||||
 | 
							u.logger.Errorf("开启数据库事务失败: %v", tx.Error) // 记录错误日志
 | 
				
			||||||
 | 
							return fmt.Errorf("开启事务失败: %w", tx.Error)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if r := recover(); r != nil {
 | 
				
			||||||
 | 
								tx.Rollback()
 | 
				
			||||||
 | 
								u.logger.Errorf("事务中发生 panic,已回滚: %v", r) // 记录 panic 日志
 | 
				
			||||||
 | 
							} else if tx.Error != nil { // 如果函数执行过程中返回错误,或者事务本身有错误,则回滚
 | 
				
			||||||
 | 
								tx.Rollback()
 | 
				
			||||||
 | 
								u.logger.Errorf("事务执行失败,已回滚: %v", tx.Error) // 记录错误日志
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 执行业务逻辑函数
 | 
				
			||||||
 | 
						if err := fn(tx); err != nil {
 | 
				
			||||||
 | 
							tx.Rollback()
 | 
				
			||||||
 | 
							return err // 返回业务逻辑函数中的错误
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 提交事务
 | 
				
			||||||
 | 
						if err := tx.Commit().Error; err != nil {
 | 
				
			||||||
 | 
							u.logger.Errorf("提交数据库事务失败: %v", err) // 记录错误日志
 | 
				
			||||||
 | 
							return fmt.Errorf("提交事务失败: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user