package service import ( "time" "github.com/shopspring/decimal" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/internal/repository" ) type InboundService struct { inboundRepo *repository.InboundRepo materialSvc *MaterialService stockRepo *repository.StockRepo } func NewInboundService() *InboundService { return &InboundService{ inboundRepo: repository.NewInboundRepo(), materialSvc: NewMaterialService(), stockRepo: repository.NewStockRepo(), } } // resolveWarningEnabled 决定本次写入预警规则时是否启用。调用方已经确认阈值非 nil。 // 未显式传 warning_enabled 时默认启用,保持老客户端"带上阈值即启用"的行为。 func resolveWarningEnabled(enabled *bool) bool { if enabled == nil { return true } return *enabled } func (s *InboundService) Create(organizationID int64, req *model.CreateInboundRequest, operator string) (*model.InboundOrder, error) { order := &model.InboundOrder{ InboundDate: time.Now(), Operator: operator, Handler: req.Handler, Supplier: req.Supplier, Remark: req.Remark, Items: make([]model.InboundItem, len(req.Items)), } // Validate all items first (outside transaction) for i, item := range req.Items { exists, err := s.materialSvc.Exists(organizationID, item.MaterialID) if err != nil { return nil, err } if !exists { return nil, ErrMaterialNotFound } active, err := s.materialSvc.IsActive(organizationID, item.MaterialID) if err != nil { return nil, err } if !active { return nil, ErrMaterialObsolete } if item.WarningStock != nil && item.WarningStock.IsNegative() { return nil, ErrInvalidWarningStock } order.Items[i] = model.InboundItem{ MaterialID: item.MaterialID, Quantity: item.Quantity, } } // 库存维度由服务端按当前公司解析:本公司的唯一启用仓库和库位。缺失或停用时返回 // ErrInventoryScopeUnavailable,Handler 映射为 409。 scope, err := ResolveInventoryScope(organizationID, true) if err != nil { return nil, err } // 单据归属只取服务端解析出的当前公司维度,请求体里伪造的归属一律被覆盖。 order.OrganizationID = scope.OrganizationID order.WarehouseID = scope.WarehouseID order.LocationID = scope.LocationID // Start transaction tx, err := repository.BeginTx() if err != nil { return nil, err } defer func() { if err != nil { tx.Rollback() } }() // Create inbound order within transaction err = s.inboundRepo.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] // 这里必须用赋值而不是 := ,否则 err 会被遮蔽,defer 里的回滚判断看不到它。 var material *model.Material material, err = s.materialSvc.GetByID(organizationID, item.MaterialID) if err != nil { return nil, err } item.MaterialCode = material.MaterialCode item.MaterialName = material.Name err = s.stockRepo.UpdateStockTx(tx, scope, item.MaterialID, decimal.NewFromInt(int64(item.Quantity))) if err != nil { return nil, err } // 入库明细上带的预警阈值在同一个事务里写入,保证物料的第一个阈值和第一行库存 // 同时落库。阈值为 nil 表示本次不改动已存的值;阈值为 0 是合法配置,含义是 // "库存降到 0 时预警"。 if warningStock := req.Items[i].WarningStock; warningStock != nil { err = s.stockRepo.UpdateWarningStockTx(tx, scope, item.MaterialID, *warningStock, resolveWarningEnabled(req.Items[i].WarningEnabled)) if err != nil { return nil, err } } stockLog := &model.StockLog{ OrganizationID: scope.OrganizationID, WarehouseID: scope.WarehouseID, LocationID: scope.LocationID, MaterialID: item.MaterialID, Type: "in", Quantity: item.Quantity, RelatedOrderType: "inbound", 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 *InboundService) GetByID(organizationID, id int64) (*model.InboundOrder, error) { return s.inboundRepo.GetByID(organizationID, id) } func (s *InboundService) List(organizationID int64, search string, limit, offset int) ([]model.InboundOrder, int64, error) { return s.inboundRepo.List(organizationID, search, limit, offset) } func (s *InboundService) GetByMaterialID(organizationID, materialID int64) ([]model.InboundRecord, error) { return s.inboundRepo.GetByMaterialID(organizationID, materialID) } func (s *InboundService) Delete(organizationID, id int64) error { // Start transaction tx, err := repository.BeginTx() if err != nil { return err } defer func() { if err != nil { tx.Rollback() } }() // 在事务中锁定当前公司的原单据;别的公司的单据在这里命中 ErrNoRows(Handler 映射 404), // 不会去冲销别家公司的库存。 order, err := s.inboundRepo.GetByIDForUpdateTx(tx, organizationID, id) if err != nil { return err } // 冲销必须在当前公司启用的库存维度上进行。 scope, err := ResolveInventoryScope(organizationID, true) if err != nil { return err } // Reverse stock for each item within transaction for _, item := range order.Items { err = s.stockRepo.UpdateStockTx(tx, scope, item.MaterialID, decimal.NewFromInt(int64(-item.Quantity))) if err != nil { return err } } // Delete stock logs within transaction(只删本公司的流水) err = s.stockRepo.DeleteLogsTx(tx, organizationID, "inbound", id) if err != nil { return err } // Delete the inbound order (items will be cascade deleted) err = s.inboundRepo.DeleteTx(tx, organizationID, id) if err != nil { return err } // Commit transaction if err = tx.Commit(); err != nil { return err } return nil } func (s *InboundService) Update(organizationID, id int64, req *model.UpdateInboundRequest) (*model.InboundOrder, error) { // Get old order data(带组织条件,别家公司的单据在这里就命中 ErrNoRows) oldOrder, err := s.inboundRepo.GetByID(organizationID, id) if err != nil { return nil, err } // 阈值校验放在事务之外,避免为非法输入白开一个事务 for _, item := range req.Items { active, err := s.materialSvc.IsActive(organizationID, item.MaterialID) if err != nil { return nil, err } if !active { return nil, ErrMaterialObsolete } if item.WarningStock != nil && item.WarningStock.IsNegative() { return nil, ErrInvalidWarningStock } } // 库存维度由服务端按当前公司解析;缺失或停用时无法改写库存。 scope, err := ResolveInventoryScope(organizationID, true) if err != nil { return nil, err } // Start transaction tx, err := repository.BeginTx() if err != nil { return nil, err } defer func() { if err != nil { tx.Rollback() } }() // 在事务中锁定当前公司的原单据,并以锁下的原明细为准冲销;锁只加在本公司单据上。 oldOrder, err = s.inboundRepo.GetByIDForUpdateTx(tx, organizationID, id) if err != nil { return nil, err } // Delete old stock logs err = s.stockRepo.DeleteLogsTx(tx, organizationID, "inbound", id) if err != nil { return nil, err } // Update order in repository err = s.inboundRepo.UpdateTx(tx, organizationID, id, req) if err != nil { return nil, err } // 旧明细(冲销)与新明细(补货)合并后按物料 ID 统一升序处理,逐物料先冲销旧量、 // 再补新量。两个并发的 Update 交换物料时也会以相同全局顺序加锁,不会死锁。 oldQtyByMaterial := make(map[int64]int) involved := make(map[int64]bool) for _, item := range oldOrder.Items { oldQtyByMaterial[item.MaterialID] += item.Quantity involved[item.MaterialID] = true } newItemIndexes := make(map[int64][]int) for i := range req.Items { materialID := req.Items[i].MaterialID newItemIndexes[materialID] = append(newItemIndexes[materialID], i) involved[materialID] = true } materialIDs := make([]int64, 0, len(involved)) for materialID := range involved { materialIDs = append(materialIDs, materialID) } for _, idx := range materialSortedIndexes(materialIDs) { materialID := materialIDs[idx] if qty := oldQtyByMaterial[materialID]; qty > 0 { _, err = s.stockRepo.GetStockForUpdateTx(tx, scope, materialID) if err != nil { return nil, err } err = s.stockRepo.UpdateStockTx(tx, scope, materialID, decimal.NewFromInt(int64(-qty))) if err != nil { return nil, err } } for _, i := range newItemIndexes[materialID] { item := req.Items[i] _, err = s.stockRepo.GetStockForUpdateTx(tx, scope, materialID) if err != nil { return nil, err } err = s.stockRepo.UpdateStockTx(tx, scope, materialID, decimal.NewFromInt(int64(item.Quantity))) if err != nil { return nil, err } // 阈值随明细一起更新,nil 表示不改动已存的值,避免编辑旧入库单时把刚调好的 // 阈值覆盖回旧数;启用状态同理,nil 表示不改动。 if item.WarningStock != nil { err = s.stockRepo.UpdateWarningStockTx(tx, scope, materialID, *item.WarningStock, resolveWarningEnabled(item.WarningEnabled)) if err != nil { return nil, err } } stockLog := &model.StockLog{ OrganizationID: scope.OrganizationID, WarehouseID: scope.WarehouseID, LocationID: scope.LocationID, MaterialID: materialID, Type: "in", Quantity: item.Quantity, RelatedOrderType: "inbound", 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.inboundRepo.GetByID(organizationID, id) if err != nil { return nil, err } updatedOrder.Status = "completed" return updatedOrder, nil }