实现修改批次绑定的猪栏

This commit is contained in:
2025-10-04 00:47:27 +08:00
parent d03163a189
commit 8bb0a54f18
13 changed files with 498 additions and 27 deletions

View File

@@ -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("猪批次相关接口注册成功 (需要认证和审计)")

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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]"`
}

View File

@@ -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
})
}

View File

@@ -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
}