171 lines
5.7 KiB
Go
171 lines
5.7 KiB
Go
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
|
||
}
|