提交版本

This commit is contained in:
凌尛 2026-06-29 16:00:59 +08:00
parent 07d1dcbce2
commit b02fe84356
3 changed files with 148 additions and 59 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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
}
// 执行事务(使用指定数据库)