diff --git a/controllers/process.go b/controllers/process.go index 6d6bd43..3e08f0a 100644 --- a/controllers/process.go +++ b/controllers/process.go @@ -568,7 +568,7 @@ func (r *ProcessApi) UnlockSalesOrderInventory(c *gin.Context) { aboutID = userInfo.AboutID } - resp, err := processService.UnlockSalesOrderInventory(aboutID, req.AssociationOrderNo, userInfo.Username, userInfo.ID) + resp, err := processService.UnlockSalesOrderInventory(aboutID, req.AssociationOrderNo, req.AssociationOrderID, userInfo.Username, userInfo.ID) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "解锁销售订单库存异常", err, c, req) return diff --git a/models/request/process.go b/models/request/process.go index 7ba9856..2801b65 100644 --- a/models/request/process.go +++ b/models/request/process.go @@ -135,8 +135,9 @@ type CancelSalesOrderRequest struct { // UnlockSalesOrderInventoryRequest 解锁销售订单库存请求 type UnlockSalesOrderInventoryRequest struct { - AboutId int64 `form:"about_id"` // 租户ID(用于确定分库) - AssociationOrderNo string `form:"association_order_no" binding:"required"` // 关联订单号(第三方订单号) + AboutId int64 `form:"about_id"` // 租户ID(用于确定分库) + AssociationOrderNo string `form:"association_order_no"` // 关联订单号(第三方订单号) + AssociationOrderID int64 `form:"association_order_id"` // 关联订单ID(与association_order_no二选一) } type CancelOutboundWaveRequest struct { diff --git a/service/process.go b/service/process.go index c4f59d6..3ecd575 100644 --- a/service/process.go +++ b/service/process.go @@ -1488,45 +1488,38 @@ func (s *ProcessService) CreateSalesOrderWithDetail(req systemReq.SalesOrderCrea salesOrderID = salesOrder.ID - salesOrderItems := make([]models.SalesOrderItem, 0, len(req.Items)) - + // 先锁定库存,再用实际被锁定的商品ID创建明细 + var salesOrderItems []models.SalesOrderItem for _, itemReq := range req.Items { - amount := itemReq.Quantity * itemReq.UnitPrice + lockResults, err := s.lockInventoryByAppearance(tx, invWarehouseID, itemReq.ProductID, itemReq.Quantity, now) + if err != nil { + return fmt.Errorf("锁定库存失败[商品ID=%d]: %v", itemReq.ProductID, err) + } - salesOrderItems = append(salesOrderItems, models.SalesOrderItem{ - SalesOrderID: salesOrderID, - ProductID: itemReq.ProductID, - Quantity: itemReq.Quantity, - AllocatedQuantity: itemReq.Quantity, - ShippedQuantity: 0, - UnitPrice: itemReq.UnitPrice, - Amount: amount, - ReceiverName: req.ReceiverName, - ReceiverPhone: req.ReceiverPhone, - ReceiverAddress: req.ReceiverAddress, - CreatedAt: now, - UpdatedAt: now, - IsDel: 0, - }) + for _, lr := range lockResults { + amount := lr.Quantity * itemReq.UnitPrice + salesOrderItems = append(salesOrderItems, models.SalesOrderItem{ + SalesOrderID: salesOrderID, + ProductID: lr.ProductID, + Quantity: lr.Quantity, + AllocatedQuantity: lr.Quantity, + ShippedQuantity: 0, + UnitPrice: itemReq.UnitPrice, + Amount: amount, + ReceiverName: req.ReceiverName, + ReceiverPhone: req.ReceiverPhone, + ReceiverAddress: req.ReceiverAddress, + CreatedAt: now, + UpdatedAt: now, + IsDel: 0, + }) + } } if err := tx.Create(&salesOrderItems).Error; err != nil { return fmt.Errorf("批量创建销售订单明细失败: %v", err) } - // 锁定库存 - //for _, itemReq := range req.Items { - // if err := s.lockInventory(tx, invWarehouseID, itemReq.ProductID, itemReq.Quantity, now); err != nil { - // return fmt.Errorf("锁定库存失败[商品ID=%d]: %v", itemReq.ProductID, err) - // } - //} - // 锁定库存 - for _, itemReq := range req.Items { - if err := s.lockInventoryByAppearance(tx, invWarehouseID, itemReq.ProductID, itemReq.Quantity, now); err != nil { - return fmt.Errorf("锁定库存失败[商品ID=%d]: %v", itemReq.ProductID, err) - } - } - return nil }) @@ -3323,15 +3316,110 @@ func (s *ProcessService) lockInventory(tx *gorm.DB, warehouseID, productID, quan func (s *ProcessService) unlockInventory(tx *gorm.DB, warehouseID, productID, quantity int64, now int64) error { var inventories []models.Inventory if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). - Where("product_id = ? AND is_del = 0 AND locked_quantity > 0", - productID). + Where("warehouse_id = ? AND product_id = ? AND is_del = 0 AND locked_quantity > 0", + warehouseID, productID). Order("created_at DESC"). Find(&inventories).Error; err != nil { return fmt.Errorf("查询锁定库存失败: %v", err) } if len(inventories) == 0 { - return fmt.Errorf("商品ID=%d无锁定库存", productID) + return fmt.Errorf("商品ID=%d在仓库ID=%d无锁定库存", productID, warehouseID) + } + + remainingUnlock := quantity + for i := range inventories { + if remainingUnlock <= 0 { + break + } + + unlockQty := inventories[i].LockedQuantity + if unlockQty > remainingUnlock { + unlockQty = remainingUnlock + } + + result := tx.Model(&inventories[i]). + Where("locked_quantity >= ?", unlockQty). + UpdateColumns(map[string]interface{}{ + "locked_quantity": gorm.Expr("locked_quantity - ?", unlockQty), + "updated_at": now, + }) + + if result.Error != nil { + return fmt.Errorf("解锁库存失败: %v", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("锁定库存已被其他事务修改,请重试") + } + + // 同步解锁 inventory_detail 表 + var inventoryDetails []models.InventoryDetail + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("warehouse_id = ? AND product_id = ? AND is_del = 0 AND locked_quantity > 0", + warehouseID, productID). + Order("created_at DESC"). + Find(&inventoryDetails).Error; err != nil { + return fmt.Errorf("查询锁定库存明细失败: %v", err) + } + + detailRemainingUnlock := unlockQty + for j := range inventoryDetails { + if detailRemainingUnlock <= 0 { + break + } + + detailUnlockQty := inventoryDetails[j].LockedQuantity + if detailUnlockQty > detailRemainingUnlock { + detailUnlockQty = detailRemainingUnlock + } + + detailResult := tx.Model(&inventoryDetails[j]). + Where("locked_quantity >= ?", detailUnlockQty). + UpdateColumns(map[string]interface{}{ + "locked_quantity": gorm.Expr("locked_quantity - ?", detailUnlockQty), + "updated_at": now, + }) + + if detailResult.Error != nil { + return fmt.Errorf("解锁库存明细失败: %v", detailResult.Error) + } + + if detailResult.RowsAffected == 0 { + return fmt.Errorf("库存明细已被其他事务修改,请重试") + } + + detailRemainingUnlock -= detailUnlockQty + } + + if detailRemainingUnlock > 0 { + return fmt.Errorf("库存明细锁定数量不足,还需解锁:%d", detailRemainingUnlock) + } + + remainingUnlock -= unlockQty + } + + if remainingUnlock > 0 { + return fmt.Errorf("锁定库存不足,还需解锁:%d", remainingUnlock) + } + + return nil +} + +// unlockInventoryNoDetail 仅解锁inventory表,不操作inventory_detail +// 用于lock时也没有操作inventory_detail的场景 +func (s *ProcessService) unlockInventoryNoDetail(tx *gorm.DB, warehouseID, productID, quantity int64, now int64) error { + var inventories []models.Inventory + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("warehouse_id = ? AND product_id = ? AND is_del = 0 AND locked_quantity > 0", + warehouseID, productID). + Order("created_at DESC"). + Find(&inventories).Error; err != nil { + return fmt.Errorf("查询锁定库存失败: %v", err) + } + + if len(inventories) == 0 { + return fmt.Errorf("商品ID=%d在仓库ID=%d无锁定库存", productID, warehouseID) } remainingUnlock := quantity @@ -3502,7 +3590,7 @@ func (s *ProcessService) CancelSalesOrder(orderID int64, operator string, operat // CancelSalesOrder 取消销售订单并释放锁定库存 // ... existing code ... -func (s *ProcessService) UnlockSalesOrderInventory(aboutID int64, associationOrderNo string, operator string, operatorID int64) (*systemRes.UnlockInventoryResponse, error) { +func (s *ProcessService) UnlockSalesOrderInventory(aboutID int64, associationOrderNo string, associationOrderID int64, operator string, operatorID int64) (*systemRes.UnlockInventoryResponse, error) { databaseConn, err := database.GetTenantDB(aboutID) if err != nil { return nil, fmt.Errorf("获取数据库连接失败: %v", err) @@ -3513,19 +3601,11 @@ func (s *ProcessService) UnlockSalesOrderInventory(aboutID int64, associationOrd err = executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { var salesOrder models.SalesOrder - if err := tx.Where("association_order_no = ? AND is_del = 0", associationOrderNo).First(&salesOrder).Error; err != nil { + if err := tx.Where("association_order_id = ? AND association_order_no = ? AND is_del = 0", associationOrderID, associationOrderNo).First(&salesOrder).Error; err != nil { return fmt.Errorf("销售订单不存在: %v", err) } - if salesOrder.Status == constant.SalesStatusShipped { - return fmt.Errorf("订单已发货,无法解锁库存") - } - if salesOrder.Status == constant.SalesStatusCancelled { - return fmt.Errorf("订单已取消,无法解锁库存") - } - if salesOrder.Status != constant.SalesStatusAllocated && salesOrder.Status != constant.SalesStatusPicking { - return fmt.Errorf("订单状态不允许解锁库存,当前状态: %s", getSalesStatusText(salesOrder.Status)) - } + // 状态检查已移除:任何状态均可解锁 var orderItems []models.SalesOrderItem if err := tx.Where("sales_order_id = ? AND is_del = 0", salesOrder.ID).Find(&orderItems).Error; err != nil { @@ -3556,7 +3636,7 @@ func (s *ProcessService) UnlockSalesOrderInventory(aboutID int64, associationOrd var responseItems []systemRes.UnlockInventoryItemResponse for _, item := range orderItems { if item.AllocatedQuantity > 0 { - if err := s.unlockInventory(tx, salesOrder.WarehouseID, item.ProductID, item.AllocatedQuantity, now); err != nil { + if err := s.unlockInventoryNoDetail(tx, salesOrder.WarehouseID, item.ProductID, item.AllocatedQuantity, now); err != nil { return fmt.Errorf("解锁库存失败[商品ID=%d]: %v", item.ProductID, err) } @@ -3770,22 +3850,28 @@ func (s *ProcessService) processInventoryDetailOperationForAdjustment(tx *gorm.D return nil } +type LockResult struct { + ProductID int64 + Quantity int64 +} + // lockInventoryByAppearance 按品相+ISBN匹配所有商品的库存进行锁定 -func (s *ProcessService) lockInventoryByAppearance(tx *gorm.DB, warehouseID, productID, quantity int64, now int64) error { +// 返回实际被锁定的商品ID及对应数量 +func (s *ProcessService) lockInventoryByAppearance(tx *gorm.DB, warehouseID, productID, quantity int64, now int64) ([]LockResult, error) { var product models.Product if err := tx.Where("id = ? AND is_del = 0", productID).First(&product).Error; err != nil { - return fmt.Errorf("查询商品信息失败: %v", err) + return nil, fmt.Errorf("查询商品信息失败: %v", err) } var matchingProductIDs []int64 if err := tx.Model(&models.Product{}). Where("barcode = ? AND appearance = ? AND is_del = 0", product.Barcode, product.Appearance). Pluck("id", &matchingProductIDs).Error; err != nil { - return fmt.Errorf("查询同品相商品失败: %v", err) + return nil, fmt.Errorf("查询同品相商品失败: %v", err) } if len(matchingProductIDs) == 0 { - return fmt.Errorf("无匹配的商品记录") + return nil, fmt.Errorf("无匹配的商品记录") } var inventories []models.Inventory @@ -3794,13 +3880,14 @@ func (s *ProcessService) lockInventoryByAppearance(tx *gorm.DB, warehouseID, pro warehouseID, matchingProductIDs). Order("product_id ASC, expiry_date ASC, created_at ASC"). Find(&inventories).Error; err != nil { - return fmt.Errorf("查询可用库存失败: %v", err) + return nil, fmt.Errorf("查询可用库存失败: %v", err) } if len(inventories) == 0 { - return fmt.Errorf("商品(barcode=%s,appearance=%d)在仓库ID=%d中无可用库存", product.Barcode, product.Appearance, warehouseID) + return nil, fmt.Errorf("商品(barcode=%s,appearance=%d)在仓库ID=%d中无可用库存", product.Barcode, product.Appearance, warehouseID) } + var results []LockResult remainingLock := quantity for i := range inventories { if remainingLock <= 0 { @@ -3825,20 +3912,21 @@ func (s *ProcessService) lockInventoryByAppearance(tx *gorm.DB, warehouseID, pro }) if result.Error != nil { - return fmt.Errorf("锁定库存失败: %v", result.Error) + return nil, fmt.Errorf("锁定库存失败: %v", result.Error) } if result.RowsAffected == 0 { - return fmt.Errorf("库存已被其他事务修改,请重试") + return nil, fmt.Errorf("库存已被其他事务修改,请重试") } + results = append(results, LockResult{ProductID: inventories[i].ProductID, Quantity: lockQty}) remainingLock -= lockQty } if remainingLock > 0 { - return fmt.Errorf("可用库存不足,还需锁定:%d", remainingLock) + return nil, fmt.Errorf("可用库存不足,还需锁定:%d", remainingLock) } - return nil + return results, nil } // 执行事务(使用指定数据库)