Files
pig-farm-controller/internal/domain/inventory/inventory_service.go
2025-11-25 18:10:28 +08:00

171 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package inventory
import (
"context"
"errors"
"fmt"
"sync"
"time"
"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"
)
// 定义领域特定的错误
var (
ErrRawMaterialNotFound = errors.New("原料不存在")
ErrInsufficientStock = errors.New("原料库存不足")
)
// InventoryCoreService 定义了库存领域的核心业务服务接口
type InventoryCoreService interface {
// AdjustStock 调整指定原料的库存
AdjustStock(ctx context.Context, rawMaterialID uint32, changeAmount float32, sourceType models.StockLogSourceType, sourceID *uint32, remarks string) (*models.RawMaterialStockLog, error)
// GetCurrentStock 获取单个原料的当前库存量
GetCurrentStock(ctx context.Context, rawMaterialID uint32) (float32, error)
// BatchGetCurrentStock 批量获取多个原料的当前库存量
BatchGetCurrentStock(ctx context.Context, rawMaterialIDs []uint32) (map[uint32]float32, error)
// ListStockLogs 分页查询库存变动日志
ListStockLogs(ctx context.Context, opts repository.StockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error)
}
// inventoryCoreServiceImpl 是 InventoryCoreService 的实现
type inventoryCoreServiceImpl struct {
ctx context.Context
uow repository.UnitOfWork
rawMatRepo repository.RawMaterialRepository
// 全局库存调整锁,确保所有 AdjustStock 操作串行执行
adjustStockMutex sync.Mutex
}
// NewInventoryCoreService 创建一个新的 InventoryCoreService 实例
func NewInventoryCoreService(ctx context.Context, uow repository.UnitOfWork, rawMatRepo repository.RawMaterialRepository) InventoryCoreService {
return &inventoryCoreServiceImpl{
ctx: ctx,
uow: uow,
rawMatRepo: rawMatRepo,
}
}
// AdjustStock 调整指定原料的库存
func (s *inventoryCoreServiceImpl) AdjustStock(ctx context.Context, rawMaterialID uint32, changeAmount float32, sourceType models.StockLogSourceType, sourceID *uint32, remarks string) (*models.RawMaterialStockLog, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "AdjustStock")
// 使用全局锁确保所有库存调整操作串行执行
s.adjustStockMutex.Lock()
defer s.adjustStockMutex.Unlock()
var createdLog *models.RawMaterialStockLog
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 在事务中创建 RawMaterialRepository 的新实例
txRawMatRepo := repository.NewGormRawMaterialRepository(serviceCtx, tx)
// 1. 检查原料是否存在
_, err := txRawMatRepo.GetRawMaterialByID(serviceCtx, rawMaterialID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrRawMaterialNotFound
}
return fmt.Errorf("检查原料是否存在时出错: %w", err)
}
// 2. 获取当前库存 (在程序锁的保护下,这里是安全的)
latestLog, err := txRawMatRepo.GetLatestRawMaterialStockLog(serviceCtx, rawMaterialID)
if err != nil {
return fmt.Errorf("获取最新库存日志失败: %w", err)
}
var beforeQuantity float32 = 0
if latestLog != nil {
beforeQuantity = latestLog.AfterQuantity
}
// 3. 计算新库存并检查是否充足
afterQuantity := beforeQuantity + changeAmount
if afterQuantity < 0 {
return ErrInsufficientStock
}
// 4. 创建新的库存日志
newLog := &models.RawMaterialStockLog{
RawMaterialID: rawMaterialID,
ChangeAmount: changeAmount,
BeforeQuantity: beforeQuantity,
AfterQuantity: afterQuantity,
SourceType: sourceType,
SourceID: sourceID,
HappenedAt: time.Now(),
Remarks: remarks,
}
if err := txRawMatRepo.CreateRawMaterialStockLog(serviceCtx, newLog); err != nil {
return fmt.Errorf("创建库存日志失败: %w", err)
}
createdLog = newLog
return nil
})
if err != nil {
return nil, err // 直接返回事务中发生的错误
}
return createdLog, nil
}
// GetCurrentStock 获取单个原料的当前库存量
func (s *inventoryCoreServiceImpl) GetCurrentStock(ctx context.Context, rawMaterialID uint32) (float32, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentStock")
latestLog, err := s.rawMatRepo.GetLatestRawMaterialStockLog(serviceCtx, rawMaterialID)
if err != nil {
return 0, fmt.Errorf("获取最新库存日志失败: %w", err)
}
if latestLog == nil {
// 如果没有日志说明从未入库库存为0
return 0, nil
}
return latestLog.AfterQuantity, nil
}
// BatchGetCurrentStock 批量获取多个原料的当前库存量
func (s *inventoryCoreServiceImpl) BatchGetCurrentStock(ctx context.Context, rawMaterialIDs []uint32) (map[uint32]float32, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "BatchGetCurrentStock")
logMap, err := s.rawMatRepo.BatchGetLatestStockLogsForMaterials(serviceCtx, rawMaterialIDs)
if err != nil {
return nil, fmt.Errorf("批量获取最新库存日志失败: %w", err)
}
stockMap := make(map[uint32]float32, len(rawMaterialIDs))
for _, id := range rawMaterialIDs {
if log, ok := logMap[id]; ok {
stockMap[id] = log.AfterQuantity
} else {
// 如果某个原料在 logMap 中不存在说明它没有任何库存记录库存为0
stockMap[id] = 0
}
}
return stockMap, nil
}
// ListStockLogs 分页查询库存变动日志
func (s *inventoryCoreServiceImpl) ListStockLogs(ctx context.Context, opts repository.StockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListStockLogs")
logs, total, err := s.rawMatRepo.ListStockLogs(serviceCtx, opts, page, pageSize)
if err != nil {
return nil, 0, fmt.Errorf("获取库存日志列表失败: %w", err)
}
return logs, total, nil
}