From 8cbe313c89a3e6f4406ae98690fbdf234c1dcf33 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 18:27:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20=E7=8C=AA=E8=88=8D?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E8=B7=AF=E7=94=B1=E7=BB=84=20=E5=92=8C=20?= =?UTF-8?q?=E7=8C=AA=E5=9C=88=E7=9B=B8=E5=85=B3=E8=B7=AF=E7=94=B1=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/api/api.go | 29 ++ .../controller/management/feed_controller.go | 1 + .../management/medication_controller.go | 1 + .../controller/management/pig_controller.go | 1 + .../management/pig_farm_controller.go | 426 ++++++++++++++++++ internal/app/service/pig_farm_service.go | 116 +++++ internal/core/application.go | 28 +- internal/infra/database/postgres.go | 18 +- internal/infra/models/feed.go | 45 +- internal/infra/models/medication.go | 15 +- internal/infra/models/models.go | 35 +- internal/infra/models/pig.go | 15 +- .../infra/repository/pig_farm_repository.go | 157 +++++++ 13 files changed, 840 insertions(+), 47 deletions(-) create mode 100644 internal/app/controller/management/feed_controller.go create mode 100644 internal/app/controller/management/medication_controller.go create mode 100644 internal/app/controller/management/pig_controller.go create mode 100644 internal/app/controller/management/pig_farm_controller.go create mode 100644 internal/app/service/pig_farm_service.go create mode 100644 internal/infra/repository/pig_farm_repository.go diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 57106bd..8af8009 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -17,9 +17,11 @@ import ( _ "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/management" "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/middleware" + "git.huangwc.com/pig/pig-farm-controller/internal/app/service" "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" @@ -45,6 +47,7 @@ type API struct { userController *user.Controller // 用户控制器实例 deviceController *device.Controller // 设备控制器实例 planController *plan.Controller // 计划控制器实例 + pigFarmController *management.PigFarmController // 猪场管理控制器实例 listenHandler webhook.ListenHandler // 设备上行事件监听器 analysisTaskManager *task.AnalysisPlanTaskManager // 计划触发器管理器实例 } @@ -58,6 +61,7 @@ func NewAPI(cfg config.ServerConfig, areaControllerRepository repository.AreaControllerRepository, deviceTemplateRepository repository.DeviceTemplateRepository, // 添加设备模板仓库 planRepository repository.PlanRepository, + pigFarmService service.PigFarmService, userActionLogRepository repository.UserActionLogRepository, tokenService token.TokenService, auditService audit.Service, // 注入审计服务 @@ -90,6 +94,8 @@ func NewAPI(cfg config.ServerConfig, deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, logger), // 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员 planController: plan.NewController(logger, planRepository, analysisTaskManager), + // 在 NewAPI 中初始化猪场管理控制器 + pigFarmController: management.NewPigFarmController(logger, pigFarmService), } api.setupRoutes() // 设置所有路由 @@ -192,6 +198,29 @@ func (a *API) setupRoutes() { planGroup.POST("/:id/stop", a.planController.StopPlan) } a.logger.Info("计划相关接口注册成功 (需要认证和审计)") + + // 猪舍相关路由组 + pigHouseGroup := authGroup.Group("/pighouses") + { + 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) + } + a.logger.Info("猪圈相关接口注册成功 (需要认证和审计)") + } } diff --git a/internal/app/controller/management/feed_controller.go b/internal/app/controller/management/feed_controller.go new file mode 100644 index 0000000..bd32643 --- /dev/null +++ b/internal/app/controller/management/feed_controller.go @@ -0,0 +1 @@ +package management diff --git a/internal/app/controller/management/medication_controller.go b/internal/app/controller/management/medication_controller.go new file mode 100644 index 0000000..bd32643 --- /dev/null +++ b/internal/app/controller/management/medication_controller.go @@ -0,0 +1 @@ +package management diff --git a/internal/app/controller/management/pig_controller.go b/internal/app/controller/management/pig_controller.go new file mode 100644 index 0000000..bd32643 --- /dev/null +++ b/internal/app/controller/management/pig_controller.go @@ -0,0 +1 @@ +package management diff --git a/internal/app/controller/management/pig_farm_controller.go b/internal/app/controller/management/pig_farm_controller.go new file mode 100644 index 0000000..e08c08c --- /dev/null +++ b/internal/app/controller/management/pig_farm_controller.go @@ -0,0 +1,426 @@ +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" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// --- 数据传输对象 (DTOs) --- + +// 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"` + Status models.PenStatus `json:"status" 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"` +} + +// --- 控制器定义 --- + +// 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 CreatePigHouseRequest true "猪舍信息" +// @Success 201 {object} controller.Response{data=PigHouseResponse} "创建成功" +// @Router /api/v1/pighouses [post] +func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) { + const action = "创建猪舍" + var req 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 := 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=PigHouseResponse} "获取成功" +// @Router /api/v1/pighouses/{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, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪舍失败", action, "业务逻辑失败", id) + return + } + + resp := 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=[]PigHouseResponse} "获取成功" +// @Router /api/v1/pighouses [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 []PigHouseResponse + for _, house := range houses { + resp = append(resp, 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 UpdatePigHouseRequest true "猪舍信息" +// @Success 200 {object} controller.Response{data=PigHouseResponse} "更新成功" +// @Router /api/v1/pighouses/{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 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, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req) + return + } + + resp := 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/pighouses/{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, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", 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 CreatePenRequest true "猪栏信息" +// @Success 201 {object} controller.Response{data=PenResponse} "创建成功" +// @Router /api/v1/pens [post] +func (c *PigFarmController) CreatePen(ctx *gin.Context) { + const action = "创建猪栏" + var req 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, req.Status) + if err != nil { + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req) + return + } + + resp := 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=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, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪栏失败", action, "业务逻辑失败", id) + return + } + + resp := 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=[]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 []PenResponse + for _, pen := range pens { + resp = append(resp, 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 UpdatePenRequest true "猪栏信息" +// @Success 200 {object} controller.Response{data=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 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, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req) + return + } + + resp := 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, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", 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) +} diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go new file mode 100644 index 0000000..5912360 --- /dev/null +++ b/internal/app/service/pig_farm_service.go @@ -0,0 +1,116 @@ +package service + +import ( + "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, status models.PenStatus) (*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 +} + +type pigFarmService struct { + logger *logs.Logger + repo repository.PigFarmRepository +} + +// NewPigFarmService 创建一个新的 PigFarmService 实例 +func NewPigFarmService(repo repository.PigFarmRepository, logger *logs.Logger) PigFarmService { + return &pigFarmService{ + logger: logger, + repo: repo, + } +} + +// --- PigHouse Implementation --- + +func (s *pigFarmService) CreatePigHouse(name, description string) (*models.PigHouse, error) { + house := &models.PigHouse{ + Name: name, + Description: description, + } + err := s.repo.CreatePigHouse(house) + return house, err +} + +func (s *pigFarmService) GetPigHouseByID(id uint) (*models.PigHouse, error) { + return s.repo.GetPigHouseByID(id) +} + +func (s *pigFarmService) ListPigHouses() ([]models.PigHouse, error) { + return s.repo.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, + } + err := s.repo.UpdatePigHouse(house) + if err != nil { + return nil, err + } + // 返回更新后的完整信息 + return s.repo.GetPigHouseByID(id) +} + +func (s *pigFarmService) DeletePigHouse(id uint) error { + return s.repo.DeletePigHouse(id) +} + +// --- Pen Implementation --- + +func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) { + pen := &models.Pen{ + PenNumber: penNumber, + HouseID: houseID, + Capacity: capacity, + Status: status, + } + err := s.repo.CreatePen(pen) + return pen, err +} + +func (s *pigFarmService) GetPenByID(id uint) (*models.Pen, error) { + return s.repo.GetPenByID(id) +} + +func (s *pigFarmService) ListPens() ([]models.Pen, error) { + return s.repo.ListPens() +} + +func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) { + pen := &models.Pen{ + Model: gorm.Model{ID: id}, + PenNumber: penNumber, + HouseID: houseID, + Capacity: capacity, + Status: status, + } + err := s.repo.UpdatePen(pen) + if err != nil { + return nil, err + } + // 返回更新后的完整信息 + return s.repo.GetPenByID(id) +} + +func (s *pigFarmService) DeletePen(id uint) error { + return s.repo.DeletePen(id) +} diff --git a/internal/core/application.go b/internal/core/application.go index 37dad89..427279f 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -8,6 +8,7 @@ import ( "time" "git.huangwc.com/pig/pig-farm-controller/internal/app/api" + "git.huangwc.com/pig/pig-farm-controller/internal/app/service" "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" @@ -58,39 +59,23 @@ func NewApplication(configPath string) (*Application, error) { // 初始化 Token 服务 tokenService := token.NewTokenService([]byte(cfg.App.JWTSecret)) - // 初始化用户仓库 + // --- 仓库对象初始化 --- userRepo := repository.NewGormUserRepository(storage.GetDB()) - - // 初始化设备仓库 deviceRepo := repository.NewGormDeviceRepository(storage.GetDB()) - - // 初始化区域主控仓库 areaControllerRepo := repository.NewGormAreaControllerRepository(storage.GetDB()) - - // 初始化设备模板仓库 deviceTemplateRepo := repository.NewGormDeviceTemplateRepository(storage.GetDB()) - - // 初始化计划仓库 planRepo := repository.NewGormPlanRepository(storage.GetDB()) - - // 初始化待执行任务仓库 + pigFarmRepo := repository.NewGormPigFarmRepository(storage.GetDB()) pendingTaskRepo := repository.NewGormPendingTaskRepository(storage.GetDB()) - - // 初始化执行日志仓库 executionLogRepo := repository.NewGormExecutionLogRepository(storage.GetDB()) - - // 初始化传感器数据仓库 sensorDataRepo := repository.NewGormSensorDataRepository(storage.GetDB()) - - // 初始化命令下发历史仓库 deviceCommandLogRepo := repository.NewGormDeviceCommandLogRepository(storage.GetDB()) - - // 初始化待采集请求仓库 pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB()) - - // 初始化审计日志仓库 userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB()) + // --- 业务逻辑处理器初始化 --- + pigFarmService := service.NewPigFarmService(pigFarmRepo, logger) + // 初始化审计服务 auditService := audit.NewService(userActionLogRepo, logger) @@ -135,6 +120,7 @@ func NewApplication(configPath string) (*Application, error) { areaControllerRepo, deviceTemplateRepo, planRepo, + pigFarmService, userActionLogRepo, tokenService, auditService, diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 06ad895..5cb9ef6 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -160,11 +160,16 @@ func (ps *PostgresStorage) creatingHyperTable() error { {models.TaskExecutionLog{}, "created_at"}, {models.PendingCollection{}, "created_at"}, {models.UserActionLog{}, "time"}, + {models.RawMaterialPurchase{}, "purchase_date"}, + {models.RawMaterialStockLog{}, "happened_at"}, + {models.FeedUsageRecord{}, "recorded_at"}, + {models.GroupMedicationLog{}, "happened_at"}, + {models.PigBatchLog{}, "happened_at"}, } for _, table := range tablesToConvert { tableName := table.model.TableName() - chunkInterval := "1 day" // 统一设置为1天 + chunkInterval := "7 days" // 统一设置为7天 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) if err := ps.db.Exec(sql).Error; err != nil { @@ -193,7 +198,7 @@ func (ps *PostgresStorage) applyCompressionPolicies() error { for _, policy := range policies { tableName := policy.model.TableName() - compressAfter := "3 days" // 统一设置为2天后(即进入第3天)开始压缩 + compressAfter := "15 days" // 统一设置为15天后开始压缩 // 1. 开启表的压缩设置,并指定分段列 ps.logger.Infow("为表启用压缩设置", "table", tableName, "segment_by", policy.segmentColumn) @@ -239,14 +244,5 @@ func (ps *PostgresStorage) creatingIndex() error { } 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 } diff --git a/internal/infra/models/feed.go b/internal/infra/models/feed.go index cf0a188..5a51699 100644 --- a/internal/infra/models/feed.go +++ b/internal/infra/models/feed.go @@ -19,16 +19,27 @@ type RawMaterial struct { Quantity float64 `gorm:"not null;comment:库存总量, 单位: g"` } +func (RawMaterial) TableName() string { + return "raw_materials" +} + // RawMaterialPurchase 记录了原料的每一次采购。 type RawMaterialPurchase struct { - gorm.Model + ID uint `gorm:"primaryKey"` 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:"not null;comment:采购日期"` + PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (RawMaterialPurchase) TableName() string { + return "raw_material_purchases" } // StockLogSourceType 定义了库存日志来源的类型 @@ -45,13 +56,20 @@ const ( // RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。 type RawMaterialStockLog struct { - gorm.Model + ID uint `gorm:"primaryKey"` 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:"not null;comment:业务发生时间"` + HappenedAt time.Time `gorm:"primaryKey;comment:业务发生时间"` Remarks string `gorm:"comment:备注, 如主动领取的理由等"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (RawMaterialStockLog) TableName() string { + return "raw_material_stock_logs" } // FeedFormula 代表饲料配方。 @@ -63,6 +81,10 @@ type FeedFormula struct { Components []FeedFormulaComponent `gorm:"foreignKey:FeedFormulaID"` } +func (FeedFormula) TableName() string { + return "feed_formulas" +} + // FeedFormulaComponent 代表配方中的一种原料及其占比。 type FeedFormulaComponent struct { gorm.Model @@ -72,17 +94,28 @@ type FeedFormulaComponent struct { 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 + ID uint `gorm:"primaryKey"` 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:"not null;comment:记录时间"` + RecordedAt time.Time `gorm:"primaryKey;comment:记录时间"` OperatorID uint `gorm:"not null;comment:操作员"` Remarks string `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (FeedUsageRecord) TableName() string { + return "feed_usage_records" } diff --git a/internal/infra/models/medication.go b/internal/infra/models/medication.go index aa914b6..395ecdf 100644 --- a/internal/infra/models/medication.go +++ b/internal/infra/models/medication.go @@ -67,6 +67,10 @@ type Medication struct { Instructions datatypes.JSON `gorm:"type:jsonb;comment:使用说明" json:"instructions"` } +func (Medication) TableName() string { + return "medications" +} + // MedicationReasonType 定义了用药原因 type MedicationReasonType string @@ -78,7 +82,7 @@ const ( // GroupMedicationLog 记录了对整个猪批次的用药情况 type GroupMedicationLog struct { - gorm.Model + ID uint `gorm:"primaryKey"` PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` MedicationID uint `gorm:"not null;index;comment:关联的药品ID"` Medication Medication `gorm:"foreignKey:MedicationID"` // 预加载药品信息 @@ -87,5 +91,12 @@ type GroupMedicationLog struct { Reason MedicationReasonType `gorm:"size:20;not null;comment:用药原因"` Description string `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"` Operator string `gorm:"size:50;comment:操作员"` - HappenedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:用药时间"` + HappenedAt time.Time `gorm:"primaryKey;comment:用药时间"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (GroupMedicationLog) TableName() string { + return "group_medication_logs" } diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 8f21ca5..a2f9b07 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -12,20 +12,45 @@ import ( // 这个函数用于在数据库初始化时自动迁移所有的表结构。 func GetAllModels() []interface{} { return []interface{}{ + // Core Models &User{}, + &UserActionLog{}, + + // Device Models &Device{}, + &AreaController{}, + &DeviceTemplate{}, + &SensorData{}, + &DeviceCommandLog{}, + + // Plan & Task Models &Plan{}, &SubPlan{}, &Task{}, &PlanExecutionLog{}, &TaskExecutionLog{}, &PendingTask{}, - &SensorData{}, - &DeviceCommandLog{}, &PendingCollection{}, - &AreaController{}, - &DeviceTemplate{}, - &UserActionLog{}, + + // Farm Asset Models + &PigHouse{}, + &Pen{}, + + // Pig & Batch Models + &PigBatch{}, + &PigBatchLog{}, + + // Feed Models + &RawMaterial{}, + &RawMaterialPurchase{}, + &RawMaterialStockLog{}, + &FeedFormula{}, + &FeedFormulaComponent{}, + &FeedUsageRecord{}, + + // Medication Models + &Medication{}, + &GroupMedicationLog{}, } } diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index caf51a0..0ee4e76 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -44,6 +44,10 @@ type PigBatch struct { Pens []Pen `gorm:"foreignKey:PigBatchID;comment:所在圈舍ID"` } +func (PigBatch) TableName() string { + return "pig_batches" +} + // LogChangeType 定义了猪批次数量变更的类型 type LogChangeType string @@ -60,7 +64,7 @@ const ( // PigBatchLog 记录了猪批次数量或状态的每一次变更 type PigBatchLog struct { - gorm.Model + ID uint `gorm:"primaryKey"` PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"` ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"` @@ -70,5 +74,12 @@ type PigBatchLog struct { BeforeSickCount int `gorm:"not null;comment:变更前病猪数"` AfterSickCount int `gorm:"not null;comment:变更后病猪数"` Operator string `gorm:"size:50;comment:操作员"` - HappenedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:事件发生时间"` + HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (PigBatchLog) TableName() string { + return "pig_batch_logs" } diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go new file mode 100644 index 0000000..5341594 --- /dev/null +++ b/internal/infra/repository/pig_farm_repository.go @@ -0,0 +1,157 @@ +package repository + +import ( + "errors" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +var ( + ErrHouseContainsPens = errors.New("cannot delete a pig house that still contains pens") + ErrHouseNotFound = errors.New("the specified pig house does not exist") +) + +// PigFarmRepository 定义了与猪场资产(猪舍、猪栏)相关的数据库操作接口 +type PigFarmRepository interface { + // PigHouse methods + CreatePigHouse(house *models.PigHouse) error + GetPigHouseByID(id uint) (*models.PigHouse, error) + ListPigHouses() ([]models.PigHouse, error) + UpdatePigHouse(house *models.PigHouse) error + DeletePigHouse(id uint) error + + // Pen methods + CreatePen(pen *models.Pen) error + GetPenByID(id uint) (*models.Pen, error) + ListPens() ([]models.Pen, error) + UpdatePen(pen *models.Pen) error + DeletePen(id uint) 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 --- + +func (r *gormPigFarmRepository) CreatePigHouse(house *models.PigHouse) error { + return r.db.Create(house).Error +} + +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 +} + +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 +} + +func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) error { + result := r.db.Model(&models.PigHouse{}).Where("id = ?", house.ID).Updates(house) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + +func (r *gormPigFarmRepository) DeletePigHouse(id uint) error { + return r.db.Transaction(func(tx *gorm.DB) error { + var penCount int64 + if err := tx.Model(&models.Pen{}).Where("house_id = ?", id).Count(&penCount).Error; err != nil { + return err + } + if penCount > 0 { + return ErrHouseContainsPens + } + + result := tx.Delete(&models.PigHouse{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil + }) +} + +// --- Pen Implementation --- + +func (r *gormPigFarmRepository) CreatePen(pen *models.Pen) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // 验证所属猪舍是否存在 + if err := tx.First(&models.PigHouse{}, pen.HouseID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrHouseNotFound + } + return err + } + return tx.Create(pen).Error + }) +} + +func (r *gormPigFarmRepository) GetPenByID(id uint) (*models.Pen, error) { + var pen models.Pen + if err := r.db.First(&pen, id).Error; err != nil { + return nil, err + } + return &pen, nil +} + +func (r *gormPigFarmRepository) ListPens() ([]models.Pen, error) { + var pens []models.Pen + if err := r.db.Find(&pens).Error; err != nil { + return nil, err + } + return pens, nil +} + +func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // 验证所属猪舍是否存在 + if err := tx.First(&models.PigHouse{}, pen.HouseID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrHouseNotFound + } + return err + } + + result := tx.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil + }) +} + +func (r *gormPigFarmRepository) DeletePen(id uint) error { + result := r.db.Delete(&models.Pen{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +}