From 8bb0a54f1838805d85fc4b6c67704e279c2653b7 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 4 Oct 2025 00:47:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BF=AE=E6=94=B9=E6=89=B9?= =?UTF-8?q?=E6=AC=A1=E7=BB=91=E5=AE=9A=E7=9A=84=E7=8C=AA=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 68 ++++++++ docs/swagger.json | 68 ++++++++ docs/swagger.yaml | 45 ++++++ internal/app/api/api.go | 1 + .../management/pig_batch_controller.go | 45 ++++++ .../management/pig_farm_controller.go | 8 +- internal/app/dto/pig_batch_dto.go | 7 +- internal/app/service/pig_batch_service.go | 149 ++++++++++++++++-- internal/app/service/pig_farm_service.go | 4 +- internal/core/application.go | 5 +- internal/infra/models/farm_asset.go | 2 +- .../infra/repository/pig_farm_repository.go | 65 +++++++- internal/infra/repository/unit_of_work.go | 58 +++++++ 13 files changed, 498 insertions(+), 27 deletions(-) create mode 100644 internal/infra/repository/unit_of_work.go diff --git a/docs/docs.go b/docs/docs.go index 2f528e9..82ab941 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1085,6 +1085,71 @@ const docTemplate = `{ } } }, + "/api/v1/pig-batches/{id}/pens": { + "put": { + "description": "更新指定猪批次当前关联的猪栏列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "更新猪批次关联的猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪批次关联的猪栏ID列表", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchUpdatePensRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "400": { + "description": "请求参数错误或无效的ID格式", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "404": { + "description": "猪批次或猪栏不存在", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "409": { + "description": "业务逻辑冲突 (如猪栏已被占用)", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/pig-houses": { "get": { "description": "获取所有猪舍的列表", @@ -2280,6 +2345,9 @@ const docTemplate = `{ } } }, + "dto.PigBatchUpdatePensRequest": { + "type": "object" + }, "dto.PigHouseResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 7ce156b..44ea4f2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1074,6 +1074,71 @@ } } }, + "/api/v1/pig-batches/{id}/pens": { + "put": { + "description": "更新指定猪批次当前关联的猪栏列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "更新猪批次关联的猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪批次关联的猪栏ID列表", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchUpdatePensRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "400": { + "description": "请求参数错误或无效的ID格式", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "404": { + "description": "猪批次或猪栏不存在", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "409": { + "description": "业务逻辑冲突 (如猪栏已被占用)", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/pig-houses": { "get": { "description": "获取所有猪舍的列表", @@ -2269,6 +2334,9 @@ } } }, + "dto.PigBatchUpdatePensRequest": { + "type": "object" + }, "dto.PigHouseResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4bf7ca4..0292e48 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -401,6 +401,8 @@ definitions: - $ref: '#/definitions/models.PigBatchStatus' description: 批次状态,可选 type: object + dto.PigBatchUpdatePensRequest: + type: object dto.PigHouseResponse: properties: description: @@ -1438,6 +1440,49 @@ paths: summary: 更新猪批次 tags: - 猪批次管理 + /api/v1/pig-batches/{id}/pens: + put: + consumes: + - application/json + description: 更新指定猪批次当前关联的猪栏列表 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 猪批次关联的猪栏ID列表 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.PigBatchUpdatePensRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + $ref: '#/definitions/controller.Response' + "400": + description: 请求参数错误或无效的ID格式 + schema: + $ref: '#/definitions/controller.Response' + "404": + description: 猪批次或猪栏不存在 + schema: + $ref: '#/definitions/controller.Response' + "409": + description: 业务逻辑冲突 (如猪栏已被占用) + schema: + $ref: '#/definitions/controller.Response' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/controller.Response' + summary: 更新猪批次关联的猪栏 + tags: + - 猪批次管理 /api/v1/pig-houses: get: description: 获取所有猪舍的列表 diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 6c00efd..fc70039 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -233,6 +233,7 @@ func (a *API) setupRoutes() { pigBatchGroup.GET("/:id", a.pigBatchController.GetPigBatch) pigBatchGroup.PUT("/:id", a.pigBatchController.UpdatePigBatch) pigBatchGroup.DELETE("/:id", a.pigBatchController.DeletePigBatch) + pigBatchGroup.PUT("/:id/pens", a.pigBatchController.UpdatePigBatchPens) } a.logger.Info("猪批次相关接口注册成功 (需要认证和审计)") diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go index 538947f..5965c49 100644 --- a/internal/app/controller/management/pig_batch_controller.go +++ b/internal/app/controller/management/pig_batch_controller.go @@ -188,3 +188,48 @@ func (c *PigBatchController) ListPigBatches(ctx *gin.Context) { controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", respDTOs, action, "获取成功", respDTOs) } + +// UpdatePigBatchPens godoc +// @Summary 更新猪批次关联的猪栏 +// @Description 更新指定猪批次当前关联的猪栏列表 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.PigBatchUpdatePensRequest true "猪批次关联的猪栏ID列表" +// @Success 200 {object} controller.Response "更新成功" +// @Failure 400 {object} controller.Response "请求参数错误或无效的ID格式" +// @Failure 404 {object} controller.Response "猪批次或猪栏不存在" +// @Failure 409 {object} controller.Response "业务逻辑冲突 (如猪栏已被占用)" +// @Failure 500 {object} controller.Response "内部服务器错误" +// @Router /api/v1/pig-batches/{id}/pens [put] +func (c *PigBatchController) UpdatePigBatchPens(ctx *gin.Context) { + const action = "更新猪批次关联猪栏" + batchID, 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.PigBatchUpdatePensRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + err = c.service.UpdatePigBatchPens(uint(batchID), req.PenIDs) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenOccupiedByOtherBatch) || errors.Is(err, service.ErrPenStatusInvalidForAllocation) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪批次关联猪栏失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", nil, action, "更新成功", batchID) +} diff --git a/internal/app/controller/management/pig_farm_controller.go b/internal/app/controller/management/pig_farm_controller.go index 9f37005..fc0defa 100644 --- a/internal/app/controller/management/pig_farm_controller.go +++ b/internal/app/controller/management/pig_farm_controller.go @@ -229,7 +229,7 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) { HouseID: pen.HouseID, Capacity: pen.Capacity, Status: pen.Status, - PigBatchID: pen.PigBatchID, + PigBatchID: *pen.PigBatchID, } controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp) } @@ -267,7 +267,7 @@ func (c *PigFarmController) GetPen(ctx *gin.Context) { HouseID: pen.HouseID, Capacity: pen.Capacity, Status: pen.Status, - PigBatchID: pen.PigBatchID, + PigBatchID: *pen.PigBatchID, } controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp) } @@ -296,7 +296,7 @@ func (c *PigFarmController) ListPens(ctx *gin.Context) { HouseID: pen.HouseID, Capacity: pen.Capacity, Status: pen.Status, - PigBatchID: pen.PigBatchID, + PigBatchID: *pen.PigBatchID, }) } @@ -344,7 +344,7 @@ func (c *PigFarmController) UpdatePen(ctx *gin.Context) { HouseID: pen.HouseID, Capacity: pen.Capacity, Status: pen.Status, - PigBatchID: pen.PigBatchID, + PigBatchID: *pen.PigBatchID, } controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp) } diff --git a/internal/app/dto/pig_batch_dto.go b/internal/app/dto/pig_batch_dto.go index c90bedd..1587bd1 100644 --- a/internal/app/dto/pig_batch_dto.go +++ b/internal/app/dto/pig_batch_dto.go @@ -3,7 +3,7 @@ package dto import ( "time" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" // 导入 models 包以使用 PigBatchOriginType 和 PigBatchStatus + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" ) // PigBatchCreateDTO 定义了创建猪批次的请求结构 @@ -43,3 +43,8 @@ type PigBatchResponseDTO struct { CreateTime time.Time `json:"create_time"` // 创建时间 UpdateTime time.Time `json:"update_time"` // 更新时间 } + +// PigBatchUpdatePensRequest 用于更新猪批次关联猪栏的请求体 +type PigBatchUpdatePensRequest struct { + PenIDs []uint `json:"penIDs" binding:"required,min=0" example:"[1,2,3]"` +} diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index 5b6eb13..f75f0ac 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -2,6 +2,7 @@ package service import ( "errors" + "fmt" "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" @@ -12,8 +13,13 @@ import ( ) var ( - ErrPigBatchNotFound = errors.New("指定的猪批次不存在") - ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") // 新增错误:活跃的猪批次不能被删除 + ErrPigBatchNotFound = errors.New("指定的猪批次不存在") + ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") + ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏") + ErrPenNotFound = errors.New("指定的猪栏不存在") + ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次占用") + ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") + ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") ) // PigBatchService 提供了猪批次管理的业务逻辑 @@ -23,18 +29,24 @@ type PigBatchService interface { UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) DeletePigBatch(id uint) error ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) + // UpdatePigBatchPens 更新猪批次关联的猪栏 + UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error } type pigBatchService struct { - logger *logs.Logger - repo repository.PigBatchRepository + logger *logs.Logger + pigBatchRepo repository.PigBatchRepository // 猪批次仓库 + pigFarmRepo repository.PigFarmRepository // 猪场资产仓库 (包含猪栏操作) + uow repository.UnitOfWork // 工作单元,用于事务管理 } // NewPigBatchService 创建一个新的 PigBatchService 实例 -func NewPigBatchService(repo repository.PigBatchRepository, logger *logs.Logger) PigBatchService { +func NewPigBatchService(pigBatchRepo repository.PigBatchRepository, pigFarmRepo repository.PigFarmRepository, uow repository.UnitOfWork, logger *logs.Logger) PigBatchService { return &pigBatchService{ - logger: logger, - repo: repo, + logger: logger, + pigBatchRepo: pigBatchRepo, + pigFarmRepo: pigFarmRepo, + uow: uow, } } @@ -67,7 +79,7 @@ func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBa Status: dto.Status, } - createdBatch, err := s.repo.CreatePigBatch(batch) + createdBatch, err := s.pigBatchRepo.CreatePigBatch(batch) if err != nil { s.logger.Errorf("创建猪批次失败: %v", err) return nil, err @@ -78,7 +90,7 @@ func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBa // GetPigBatch 处理获取单个猪批次的业务逻辑 func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) { - batch, err := s.repo.GetPigBatchByID(id) + batch, err := s.pigBatchRepo.GetPigBatchByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrPigBatchNotFound @@ -92,7 +104,7 @@ func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) // UpdatePigBatch 处理更新猪批次的业务逻辑 func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) { - existingBatch, err := s.repo.GetPigBatchByID(id) + existingBatch, err := s.pigBatchRepo.GetPigBatchByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrPigBatchNotFound @@ -121,7 +133,7 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (* existingBatch.Status = *dto.Status } - updatedBatch, err := s.repo.UpdatePigBatch(existingBatch) + updatedBatch, err := s.pigBatchRepo.UpdatePigBatch(existingBatch) if err != nil { s.logger.Errorf("更新猪批次失败,ID: %d, 错误: %v", id, err) return nil, err @@ -133,7 +145,7 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (* // DeletePigBatch 处理删除猪批次的业务逻辑 func (s *pigBatchService) DeletePigBatch(id uint) error { // 1. 获取猪批次信息 - batch, err := s.repo.GetPigBatchByID(id) + batch, err := s.pigBatchRepo.GetPigBatchByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrPigBatchNotFound @@ -148,7 +160,7 @@ func (s *pigBatchService) DeletePigBatch(id uint) error { } // 3. 执行删除操作 - err = s.repo.DeletePigBatch(id) + err = s.pigBatchRepo.DeletePigBatch(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) || errors.New("未找到要删除的猪批次").Error() == err.Error() { return ErrPigBatchNotFound @@ -161,7 +173,7 @@ func (s *pigBatchService) DeletePigBatch(id uint) error { // ListPigBatches 处理批量查询猪批次的业务逻辑 func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) { - batches, err := s.repo.ListPigBatches(isActive) + batches, err := s.pigBatchRepo.ListPigBatches(isActive) if err != nil { s.logger.Errorf("批量查询猪批次失败,错误: %v", err) return nil, err @@ -174,3 +186,112 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchRespons return responseDTOs, nil } + +// UpdatePigBatchPens 更新猪批次关联的猪栏 +func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { + // 使用工作单元执行事务 + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 验证猪批次 + pigBatch, err := s.pigFarmRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + s.logger.Errorf("更新猪批次猪栏失败: 获取猪批次信息错误,ID: %d, 错误: %v", batchID, err) + return fmt.Errorf("获取猪批次信息失败: %w", err) + } + + if !pigBatch.IsActive() { + return ErrPigBatchNotActive + } + + // 2. 获取当前关联的猪栏 + currentPens, err := s.pigFarmRepo.GetPensByBatchID(tx, batchID) + if err != nil { + s.logger.Errorf("更新猪批次猪栏失败: 获取当前关联猪栏错误,批次ID: %d, 错误: %v", batchID, err) + return fmt.Errorf("获取当前关联猪栏失败: %w", err) + } + + currentPenMap := make(map[uint]models.Pen) + currentPenIDsSet := make(map[uint]struct{}) + for _, pen := range currentPens { + currentPenMap[pen.ID] = pen + currentPenIDsSet[pen.ID] = struct{}{} // 用于快速查找 + } + + // 3. 构建期望猪栏集合 + desiredPenIDsSet := make(map[uint]struct{}) + for _, penID := range desiredPenIDs { + desiredPenIDsSet[penID] = struct{}{} // 用于快速查找 + } + + // 4. 计算需要添加和移除的猪栏 + var pensToRemove []uint + for penID := range currentPenIDsSet { + if _, found := desiredPenIDsSet[penID]; !found { + pensToRemove = append(pensToRemove, penID) + } + } + + var pensToAdd []uint + for _, penID := range desiredPenIDs { + if _, found := currentPenIDsSet[penID]; !found { + pensToAdd = append(pensToAdd, penID) + } + } + + // 5. 处理移除猪栏 + for _, penID := range pensToRemove { + currentPen := currentPenMap[penID] + // 验证:确保猪栏确实与当前批次关联 + if currentPen.PigBatchID == nil || *currentPen.PigBatchID != batchID { + s.logger.Warnf("尝试移除未与批次 %d 关联的猪栏 %d", batchID, penID) + return fmt.Errorf("猪栏 %d 未与该批次关联,无法移除", penID) + } + + updates := make(map[string]interface{}) + updates["pig_batch_id"] = nil // 总是将 PigBatchID 设为 nil + + // 只有当猪栏当前状态是“占用”时,才将其状态改回“空闲” + if currentPen.Status == models.PenStatusOccupied { + updates["status"] = models.PenStatusEmpty + } + + if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { + s.logger.Errorf("更新猪批次猪栏失败: 移除猪栏 %d 失败: %v", penID, err) + return fmt.Errorf("移除猪栏 %d 失败: %w", penID, err) + } + } + + // 6. 处理添加猪栏 + for _, penID := range pensToAdd { + actualPen, err := s.pigFarmRepo.GetPenByIDTx(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) + } + s.logger.Errorf("更新猪批次猪栏失败: 获取猪栏 %d 信息错误: %v", penID, err) + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) + } + + // 验证:猪栏必须是“空闲”状态且未被任何批次占用,才能被分配 + if actualPen.Status != models.PenStatusEmpty { + return fmt.Errorf("猪栏 %s 状态为 %s,无法分配: %w", actualPen.PenNumber, actualPen.Status, ErrPenStatusInvalidForAllocation) + } + if actualPen.PigBatchID != nil { + return fmt.Errorf("猪栏 %s 已被其他批次 %d 占用,无法分配: %w", actualPen.PenNumber, *actualPen.PigBatchID, ErrPenOccupiedByOtherBatch) + } + + updates := map[string]interface{}{ + "pig_batch_id": &batchID, // 将 PigBatchID 设为当前批次ID的指针 + "status": models.PenStatusOccupied, // 分配后,状态变为“占用” + } + if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { + s.logger.Errorf("更新猪批次猪栏失败: 添加猪栏 %d 失败: %v", penID, err) + return fmt.Errorf("添加猪栏 %d 失败: %w", penID, err) + } + } + + return nil + }) +} diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index 433e5e3..11478df 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -163,8 +163,8 @@ func (s *pigFarmService) DeletePen(id uint) error { } // 检查猪栏是否关联了活跃批次 - if pen.PigBatchID != 0 { - pigBatch, err := s.repo.GetPigBatchByID(pen.PigBatchID) + if *pen.PigBatchID != 0 { + pigBatch, err := s.repo.GetPigBatchByID(*pen.PigBatchID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } diff --git a/internal/core/application.go b/internal/core/application.go index 02039e2..80c2764 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -74,9 +74,12 @@ func NewApplication(configPath string) (*Application, error) { userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB()) pigBatchRepo := repository.NewGormPigBatchRepository(storage.GetDB()) + // 初始化事务管理器 + unitOfWork := repository.NewGormUnitOfWork(storage.GetDB()) + // --- 业务逻辑处理器初始化 --- pigFarmService := service.NewPigFarmService(pigFarmRepo, logger) - pigBatchService := service.NewPigBatchService(pigBatchRepo, logger) + pigBatchService := service.NewPigBatchService(pigBatchRepo, pigFarmRepo, unitOfWork, logger) // 初始化审计服务 auditService := audit.NewService(userActionLogRepo, logger) diff --git a/internal/infra/models/farm_asset.go b/internal/infra/models/farm_asset.go index 90266ed..28e7c05 100644 --- a/internal/infra/models/farm_asset.go +++ b/internal/infra/models/farm_asset.go @@ -33,7 +33,7 @@ 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"` + PigBatchID *uint `gorm:"index;comment:关联的猪批次ID"` Capacity int `gorm:"not null;comment:设计容量 (头)"` Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"` } diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go index 366bd33..24b17c5 100644 --- a/internal/infra/repository/pig_farm_repository.go +++ b/internal/infra/repository/pig_farm_repository.go @@ -17,13 +17,23 @@ type PigFarmRepository interface { // Pen methods 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(pen *models.Pen) error DeletePen(id uint) error + // GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) + GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models.Pen, error) + // UpdatePenFields 更新猪栏的指定字段 (事务性) + UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error // PigBatch methods + // GetPigBatchByID 根据ID获取单个猪批次 (非事务性) GetPigBatchByID(id uint) (*models.PigBatch, error) + // GetPigBatchByIDTx 根据ID获取单个猪批次 (事务性) + GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) } // gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现 @@ -38,10 +48,12 @@ func NewGormPigFarmRepository(db *gorm.DB) PigFarmRepository { // --- 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 { @@ -50,6 +62,7 @@ func (r *gormPigFarmRepository) GetPigHouseByID(id uint) (*models.PigHouse, erro return &house, nil } +// ListPigHouses 列出所有猪舍 func (r *gormPigFarmRepository) ListPigHouses() ([]models.PigHouse, error) { var houses []models.PigHouse if err := r.db.Find(&houses).Error; err != nil { @@ -58,28 +71,31 @@ func (r *gormPigFarmRepository) ListPigHouses() ([]models.PigHouse, error) { return houses, nil } +// UpdatePigHouse 更新一个猪舍 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 gorm.ErrRecordNotFound // 未找到要更新的猪舍或数据未改变 } return nil } +// DeletePigHouse 根据ID删除一个猪舍 func (r *gormPigFarmRepository) DeletePigHouse(id uint) error { result := r.db.Delete(&models.PigHouse{}, id) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound + return gorm.ErrRecordNotFound // 未找到要删除的猪舍 } return 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 @@ -88,10 +104,12 @@ func (r *gormPigFarmRepository) CountPensInHouse(houseID uint) (int64, error) { // --- Pen Implementation --- +// CreatePen 创建一个新的猪栏 func (r *gormPigFarmRepository) CreatePen(pen *models.Pen) error { return r.db.Create(pen).Error } +// GetPenByID 根据ID获取单个猪栏 (非事务性) func (r *gormPigFarmRepository) GetPenByID(id uint) (*models.Pen, error) { var pen models.Pen if err := r.db.First(&pen, id).Error; err != nil { @@ -100,6 +118,16 @@ func (r *gormPigFarmRepository) GetPenByID(id uint) (*models.Pen, error) { return &pen, nil } +// GetPenByIDTx 根据ID获取单个猪栏 (事务性) +func (r *gormPigFarmRepository) 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 *gormPigFarmRepository) ListPens() ([]models.Pen, error) { var pens []models.Pen if err := r.db.Find(&pens).Error; err != nil { @@ -108,30 +136,50 @@ func (r *gormPigFarmRepository) ListPens() ([]models.Pen, error) { return pens, nil } +// UpdatePen 更新一个猪栏 func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) error { result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound + return gorm.ErrRecordNotFound // 未找到要更新的猪栏或数据未改变 } return nil } +// DeletePen 根据ID删除一个猪栏 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 gorm.ErrRecordNotFound // 未找到要删除的猪栏 } return nil } +// GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) +func (r *gormPigFarmRepository) GetPensByBatchID(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 +} + +// UpdatePenFields 更新猪栏的指定字段 (事务性) +func (r *gormPigFarmRepository) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error { + result := tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates) + return result.Error +} + // --- PigBatch Implementation --- +// GetPigBatchByID 根据ID获取单个猪批次 (非事务性) func (r *gormPigFarmRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) { var batch models.PigBatch if err := r.db.First(&batch, id).Error; err != nil { @@ -139,3 +187,12 @@ func (r *gormPigFarmRepository) GetPigBatchByID(id uint) (*models.PigBatch, erro } return &batch, nil } + +// GetPigBatchByIDTx 根据ID获取单个猪批次 (事务性) +func (r *gormPigFarmRepository) 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 +} diff --git a/internal/infra/repository/unit_of_work.go b/internal/infra/repository/unit_of_work.go new file mode 100644 index 0000000..0d45573 --- /dev/null +++ b/internal/infra/repository/unit_of_work.go @@ -0,0 +1,58 @@ +package repository + +import ( + "fmt" + + "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 +} + +// NewGormUnitOfWork 创建一个新的 gormUnitOfWork 实例 +func NewGormUnitOfWork(db *gorm.DB) UnitOfWork { + return &gormUnitOfWork{db: db} +} + +// ExecuteInTransaction 实现了 UnitOfWork 接口的事务执行逻辑 +func (u *gormUnitOfWork) ExecuteInTransaction(fn func(tx *gorm.DB) error) error { + tx := u.db.Begin() + if tx.Error != nil { + return fmt.Errorf("开启事务失败: %w", tx.Error) + } + + defer func() { + if r := recover(); r != nil { + tx.Rollback() + // 可以选择在此处记录 panic 日志 + // u.logger.Errorf("事务中发生 panic,已回滚: %v", r) + } 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 { + return fmt.Errorf("提交事务失败: %w", err) + } + + return nil +}