package service import ( "errors" "time" "github.com/shopspring/decimal" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/internal/repository" ) var ErrInsufficientStock = errors.New("insufficient stock") type OutboundService struct { outboundRepo *repository.OutboundRepo materialSvc *MaterialService stockRepo *repository.StockRepo } func NewOutboundService() *OutboundService { return &OutboundService{ outboundRepo: repository.NewOutboundRepo(), materialSvc: NewMaterialService(), stockRepo: repository.NewStockRepo(), } } func (s *OutboundService) Create(req *model.CreateOutboundRequest, operator string) (*model.OutboundOrder, error) { order := &model.OutboundOrder{ OutboundDate: time.Now(), Operator: operator, Handler: operator, Receiver: req.Receiver, Remark: req.Remark, Items: make([]model.OutboundItem, len(req.Items)), } // Validate all items first (outside transaction) for i, item := range req.Items { exists, err := s.materialSvc.Exists(item.MaterialID) if err != nil { return nil, err } if !exists { return nil, ErrMaterialNotFound } active, err := s.materialSvc.IsActive(item.MaterialID) if err != nil { return nil, err } if !active { return nil, ErrMaterialObsolete } currentStock, err := s.stockRepo.GetStock(item.MaterialID) if err != nil { return nil, err } if currentStock.LessThan(decimal.NewFromInt(int64(item.Quantity))) { return nil, ErrInsufficientStock } order.Items[i] = model.OutboundItem{ MaterialID: item.MaterialID, Quantity: item.Quantity, } } // Start transaction for stock update (prevents race condition) tx, err := repository.BeginTx() if err != nil { return nil, err } defer func() { if err != nil { tx.Rollback() } }() // Re-check stock with row lock (SELECT FOR UPDATE) to prevent overselling for i := range order.Items { item := &order.Items[i] lockedStock, err := s.stockRepo.GetStockForUpdateTx(tx, item.MaterialID) if err != nil { return nil, err } if lockedStock.LessThan(decimal.NewFromInt(int64(item.Quantity))) { err = ErrInsufficientStock return nil, err } } // Create outbound order err = s.outboundRepo.CreateTx(tx, order) if err != nil { return nil, err } // Update stock and create logs within transaction for i := range order.Items { item := &order.Items[i] material, err := s.materialSvc.GetByID(item.MaterialID) if err != nil { return nil, err } item.MaterialCode = material.MaterialCode item.MaterialName = material.Name // Update stock within transaction err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(-item.Quantity))) if err != nil { return nil, err } // Create stock log within transaction stockLog := &model.StockLog{ MaterialID: item.MaterialID, Type: "out", Quantity: item.Quantity, RelatedOrderType: "outbound", RelatedOrderID: order.ID, } err = s.stockRepo.CreateLogTx(tx, stockLog) if err != nil { return nil, err } } // Commit transaction if err = tx.Commit(); err != nil { return nil, err } order.Status = "completed" return order, nil } func (s *OutboundService) GetByID(id int64) (*model.OutboundOrder, error) { return s.outboundRepo.GetByID(id) } func (s *OutboundService) List(search string, limit, offset int) ([]model.OutboundOrder, int64, error) { return s.outboundRepo.List(search, limit, offset) } func (s *OutboundService) GetByMaterialID(materialID int64) ([]model.OutboundRecord, error) { return s.outboundRepo.GetByMaterialID(materialID) } func (s *OutboundService) Delete(id int64) error { // Get the outbound order with items order, err := s.outboundRepo.GetByID(id) if err != nil { return err } // Start transaction tx, err := repository.BeginTx() if err != nil { return err } defer func() { if err != nil { tx.Rollback() } }() // Reverse stock for each item (add back) within transaction for _, item := range order.Items { err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(item.Quantity))) if err != nil { return err } } // Delete stock logs within transaction err = s.stockRepo.DeleteLogsTx(tx, "outbound", id) if err != nil { return err } // Delete the outbound order (items will be cascade deleted) err = s.outboundRepo.DeleteTx(tx, id) if err != nil { return err } // Commit transaction if err = tx.Commit(); err != nil { return err } return nil } func (s *OutboundService) Update(id int64, req *model.UpdateOutboundRequest, operator string) (*model.OutboundOrder, error) { // Get old order data oldOrder, err := s.outboundRepo.GetByID(id) if err != nil { return nil, err } // Validate all new items first (outside transaction) for _, item := range req.Items { exists, err := s.materialSvc.Exists(item.MaterialID) if err != nil { return nil, err } if !exists { return nil, ErrMaterialNotFound } active, err := s.materialSvc.IsActive(item.MaterialID) if err != nil { return nil, err } if !active { return nil, ErrMaterialObsolete } currentStock, err := s.stockRepo.GetStock(item.MaterialID) if err != nil { return nil, err } // Calculate effective stock: current + old outbound quantity - new outbound quantity oldQty := 0 for _, oldItem := range oldOrder.Items { if oldItem.MaterialID == item.MaterialID { oldQty = oldItem.Quantity break } } effectiveStock := currentStock.Add(decimal.NewFromInt(int64(oldQty))) if effectiveStock.LessThan(decimal.NewFromInt(int64(item.Quantity))) { return nil, ErrInsufficientStock } } // Start transaction tx, err := repository.BeginTx() if err != nil { return nil, err } defer func() { if err != nil { tx.Rollback() } }() // Reverse stock for old items (add back what was previously subtracted) for _, item := range oldOrder.Items { err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(item.Quantity))) if err != nil { return nil, err } } // Re-check stock with row lock (SELECT FOR UPDATE) to prevent overselling for _, item := range req.Items { lockedStock, err := s.stockRepo.GetStockForUpdateTx(tx, item.MaterialID) if err != nil { return nil, err } oldQty := 0 for _, oldItem := range oldOrder.Items { if oldItem.MaterialID == item.MaterialID { oldQty = oldItem.Quantity break } } effectiveStock := lockedStock.Add(decimal.NewFromInt(int64(oldQty))) if effectiveStock.LessThan(decimal.NewFromInt(int64(item.Quantity))) { err = ErrInsufficientStock return nil, err } } // Delete old stock logs err = s.stockRepo.DeleteLogsTx(tx, "outbound", id) if err != nil { return nil, err } // Update order in repository // 客户端不能指定操作人;编辑也由当前登录用户接管操作人和兼容字段 handler。 req.Handler = operator req.Operator = operator err = s.outboundRepo.UpdateTx(tx, id, req) if err != nil { return nil, err } // Deduct stock for new items and create logs for _, item := range req.Items { err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(-item.Quantity))) if err != nil { return nil, err } stockLog := &model.StockLog{ MaterialID: item.MaterialID, Type: "out", Quantity: item.Quantity, RelatedOrderType: "outbound", RelatedOrderID: id, } err = s.stockRepo.CreateLogTx(tx, stockLog) if err != nil { return nil, err } } // Commit transaction if err = tx.Commit(); err != nil { return nil, err } // Get updated order with material details updatedOrder, err := s.outboundRepo.GetByID(id) if err != nil { return nil, err } updatedOrder.Status = "completed" return updatedOrder, nil }