From 3b109d15477dc8cce498edf172acb0f399f441b2 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 16:19:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B9=B0=E5=8D=96=E7=8C=AA=E8=A6=81=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E7=8C=AA=E6=A0=8F=E5=AD=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/core/application.go | 2 +- internal/domain/pig/pen_transfer_manager.go | 79 +++++++++++++--- internal/domain/pig/pig_batch.go | 4 +- .../domain/pig/pig_batch_service_pig_trade.go | 94 ++++++++++++++----- .../repository/pig_transfer_log_repository.go | 12 +++ 5 files changed, 153 insertions(+), 38 deletions(-) diff --git a/internal/core/application.go b/internal/core/application.go index a15f3de..25e596f 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -85,7 +85,7 @@ func NewApplication(configPath string) (*Application, error) { unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) // 初始化猪群管理领域 - pigPenTransferManager := pig.NewPigPenTransferManager(pigPenRepo, pigTransferLogRepo) + pigPenTransferManager := pig.NewPigPenTransferManager(pigPenRepo, pigTransferLogRepo, pigBatchRepo) pigTradeManager := pig.NewPigTradeManager(pigTradeRepo) pigSickManager := pig.NewSickPigManager(pigSickPigLogRepo, medicationLogRepo) pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigBatchLogRepo, unitOfWork, diff --git a/internal/domain/pig/pen_transfer_manager.go b/internal/domain/pig/pen_transfer_manager.go index ed16b62..41b7cfb 100644 --- a/internal/domain/pig/pen_transfer_manager.go +++ b/internal/domain/pig/pen_transfer_manager.go @@ -1,6 +1,8 @@ package pig import ( + "errors" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "gorm.io/gorm" @@ -13,54 +15,105 @@ type PigPenTransferManager interface { LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error // GetPenByID 用于获取猪栏的详细信息,供上层服务进行业务校验。 - // 注意: 此方法依赖于您在 PigPenRepository 中添加对应的 GetPenByIDTx 方法。 GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) // GetPensByBatchID 获取一个猪群当前关联的所有猪栏。 - // 注意: 此方法依赖于您在 PigPenRepository 中添加对应的 GetPensByBatchIDTx 方法。 GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) // UpdatePenFields 更新一个猪栏的指定字段。 - // 注意: 此方法依赖于您在 PigPenRepository 中添加对应的 UpdatePenFieldsTx 方法。 UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error + + // GetCurrentPigsInPen 通过汇总猪只迁移日志,计算给定猪栏中的当前猪只数量。 + GetCurrentPigsInPen(tx *gorm.DB, penID uint) (int, error) } // pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。 // 它作为调栏管理器,处理底层的数据库交互。 type pigPenTransferManager struct { - penRepo repository.PigPenRepository - logRepo repository.PigTransferLogRepository + penRepo repository.PigPenRepository + logRepo repository.PigTransferLogRepository + pigBatchRepo repository.PigBatchRepository } // NewPigPenTransferManager 是 pigPenTransferManager 的构造函数。 -// 修改构造函数以接收 PigTransferLogRepository 依赖 -func NewPigPenTransferManager(penRepo repository.PigPenRepository, logRepo repository.PigTransferLogRepository) PigPenTransferManager { +func NewPigPenTransferManager(penRepo repository.PigPenRepository, logRepo repository.PigTransferLogRepository, pigBatchRepo repository.PigBatchRepository) PigPenTransferManager { return &pigPenTransferManager{ - penRepo: penRepo, - logRepo: logRepo, + 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 实现了获取猪栏信息的逻辑。 -// 注意: 此处调用了一个假设存在的方法 GetPenByIDTx。 func (s *pigPenTransferManager) GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) { return s.penRepo.GetPenByIDTx(tx, penID) } // GetPensByBatchID 实现了获取猪群关联猪栏列表的逻辑。 -// 注意: 此处调用了一个假设存在的方法 GetPensByBatchIDTx。 func (s *pigPenTransferManager) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) { return s.penRepo.GetPensByBatchIDTx(tx, batchID) } // UpdatePenFields 实现了更新猪栏字段的逻辑。 -// 注意: 此处调用了一个假设存在的方法 UpdatePenFieldsTx。 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 +} diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index f6a4214..001c0aa 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -51,9 +51,9 @@ type PigBatchService interface { GetCurrentPigQuantity(batchID uint) (int, error) // SellPigs 处理卖猪的业务逻辑。 - SellPigs(batchID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error + 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, 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 UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error } diff --git a/internal/domain/pig/pig_batch_service_pig_trade.go b/internal/domain/pig/pig_batch_service_pig_trade.go index de327a0..d2cec43 100644 --- a/internal/domain/pig/pig_batch_service_pig_trade.go +++ b/internal/domain/pig/pig_batch_service_pig_trade.go @@ -10,32 +10,37 @@ import ( ) // SellPigs 处理批量销售猪的业务逻辑。 -func (s *pigBatchService) SellPigs(batchID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error { +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. 获取猪批次信息 - _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) // 仅用于校验批次是否存在 + // 1. 校验猪栏信息 + pen, err := s.transferSvc.GetPenByID(tx, penID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrPigBatchNotFound + return ErrPenNotFound } - return fmt.Errorf("获取猪批次 %d 信息失败: %w", batchID, err) + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) } - // 2. 业务校验:检查销售数量是否超过当前批次数量 - currentQuantity, err := s.getCurrentPigQuantityTx(tx, batchID) + // 校验猪栏是否属于该批次 + 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", batchID, err) + return fmt.Errorf("获取猪栏 %d 当前猪只数失败: %w", penID, err) } - if quantity > currentQuantity { - return fmt.Errorf("销售数量 %d 超过当前批次 %d 数量 %d", quantity, batchID, currentQuantity) + if quantity > currentPigsInPen { + return fmt.Errorf("销售数量 %d 超过猪栏 %d 当前猪只数 %d", quantity, penID, currentPigsInPen) } - // 3. 记录销售交易 + // 3. 记录销售交易 (财务) sale := &models.PigSale{ PigBatchID: batchID, SaleDate: tradeDate, @@ -50,9 +55,23 @@ func (s *pigBatchService) SellPigs(batchID uint, quantity int, unitPrice float64 return fmt.Errorf("记录销售交易失败: %w", err) } - // 4. 记录批次日志 + // 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 头猪给 %s", batchID, quantity, traderName), + fmt.Sprintf("猪批次 %d 从猪栏 %d 销售 %d 头猪给 %s", batchID, penID, quantity, traderName), tradeDate); err != nil { return fmt.Errorf("更新猪批次数量失败: %w", err) } @@ -62,22 +81,39 @@ func (s *pigBatchService) SellPigs(batchID uint, quantity int, unitPrice float64 } // BuyPigs 处理批量购买猪的业务逻辑。 -func (s *pigBatchService) BuyPigs(batchID uint, quantity int, unitPrice float64, totalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error { +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. 获取猪批次信息 - _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) // 仅用于校验批次是否存在 + // 1. 校验猪栏信息 + pen, err := s.transferSvc.GetPenByID(tx, penID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrPigBatchNotFound + return ErrPenNotFound } - return fmt.Errorf("获取猪批次 %d 信息失败: %w", batchID, err) + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) } - // 3. 记录采购交易 + // 校验猪栏是否属于该批次 + 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, @@ -85,16 +121,30 @@ func (s *pigBatchService) BuyPigs(batchID uint, quantity int, unitPrice float64, Quantity: quantity, UnitPrice: unitPrice, TotalPrice: totalPrice, // 总价不一定是单价x数量, 所以要传进来 - Remarks: remarks, + Remarks: remarks, // 用户传入的备注 OperatorID: operatorID, } if err := s.tradeSvc.BuyPig(tx, purchase); err != nil { return fmt.Errorf("记录采购交易失败: %w", err) } - // 4. 记录批次日志 + // 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 头猪从 %s", batchID, quantity, traderName), + fmt.Sprintf("猪批次 %d 在猪栏 %d 采购 %d 头猪从 %s", batchID, penID, quantity, traderName), tradeDate); err != nil { return fmt.Errorf("更新猪批次数量失败: %w", err) } diff --git a/internal/infra/repository/pig_transfer_log_repository.go b/internal/infra/repository/pig_transfer_log_repository.go index 7854717..4d934fe 100644 --- a/internal/infra/repository/pig_transfer_log_repository.go +++ b/internal/infra/repository/pig_transfer_log_repository.go @@ -1,6 +1,8 @@ package repository import ( + "time" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "gorm.io/gorm" ) @@ -9,6 +11,9 @@ import ( 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 实现。 @@ -25,3 +30,10 @@ func NewGormPigTransferLogRepository(db *gorm.DB) PigTransferLogRepository { 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 +}