package service import ( "encoding/json" "errors" "fmt" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" "net/http" "net/url" "psi/config" "psi/constant" "psi/database" "psi/models" systemReq "psi/models/request" systemRes "psi/models/response" "psi/utils" "strconv" "strings" "time" ) type ProcessService struct{} // WaveItemData 波次商品数据 type WaveItemData struct { ProductID int64 // 商品ID Quantity int64 // 数量 UnitPrice int64 // 单价 LocationID int64 // 库位ID(出库时使用,入库时为0) } // inventoryKey 库存键(用于唯一标识库存记录) type inventoryKey struct { warehouseID int64 // 仓库ID productID int64 // 商品ID batchNo string // 批次号 productionDate int64 // 生产日期 expiryDate int64 // 过期日期 } // inventoryOperation 库存操作 type inventoryOperation struct { key inventoryKey // 库存键 locationID int64 // 库位ID quantity int64 // 数量 } // orderInfo 订单信息 type orderInfo struct { orderID int64 // 订单ID orderNo string // 订单号 warehouseID int64 // 仓库ID status int // 订单状态 } // orderItemInfo 订单商品信息 type orderItemInfo struct { productID int64 // 商品ID locationID int64 // 库位ID batchNo string // 批次号 productionDate int64 // 生产日期 expiryDate int64 // 过期日期 quantity int64 // 数量 } // CreatePurchaseOrderWithWave 创建采购单并生成入库波次 func (s *ProcessService) CreatePurchaseOrderWithWave(req systemReq.PurchaseOrderCreateRequest, creator string, creatorID int64, carCapacity int, db ...*gorm.DB) (int64, int64, error) { databaseConn := database.OptionalDB(db...) // 检查仓库是否绑定了运费模板 var warehouse models.Warehouse if err := databaseConn.Where("id = ? AND is_del = ?", req.WarehouseID, 0).First(&warehouse).Error; err != nil { return 0, 0, fmt.Errorf("仓库不存在: %v", err) } if warehouse.LogisticsID == 0 { return 0, 0, fmt.Errorf("该仓库未绑定运费模板,请先绑定运费模板后再创建采购订单") } if len(req.Items) > carCapacity { return 0, 0, fmt.Errorf("采购订单和波次的明细数量不能超过%d条,当前为%d条", carCapacity, len(req.Items)) } now := time.Now().Unix() poNo := utils.GeneratePoNo() waveNo, err := s.generateWaveNo(constant.DirectionInbound, databaseConn) if err != nil { return 0, 0, fmt.Errorf("生成波次号失败: %v", err) } var totalAmount int64 // 采购金额 for _, item := range req.Items { totalAmount += item.Quantity * item.UnitPrice } var purchaseOrderID int64 // 采购订单ID var waveID int64 // 入库波次ID err = executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { purchaseOrder := models.PurchaseOrder{ PoNo: poNo, //采购订单号 SupplierID: req.SupplierID, //供应商ID WarehouseID: req.WarehouseID, //仓库ID OrderDate: now, //订单日期 ExpectedArrivalDate: req.ExpectedArrivalDate, //预计到货日期 TotalAmount: totalAmount, //采购金额 Status: constant.PurchaseStatusSubmitted, //订单状态 Creator: creator, //创建人 CreatorID: creatorID, //创建人ID Remark: req.Remark, //备注 CreatedAt: now, //创建时间戳 UpdatedAt: now, //更新时间戳 IsDel: 0, //逻辑删除标记 } if err := tx.Create(&purchaseOrder).Error; err != nil { return fmt.Errorf("创建采购订单失败: %v", err) } purchaseOrderID = purchaseOrder.ID waveHeader, err := s.createWaveHeader(tx, waveNo, constant.DirectionInbound, req.WarehouseID, purchaseOrder.ID, creator, creatorID) if err != nil { return err } waveID = waveHeader.ID return nil }) if err != nil { return 0, 0, err } return purchaseOrderID, waveID, nil } // ReleaseWave 提交/追加,生成采购订单明细和波次任务明细 如果波次状态为已创建,则首次提交;如果为已下发,则追加数据 func (s *ProcessService) ReleaseWave(req systemReq.WaveRequest, carCapacity int64, db ...*gorm.DB) (int64, string, error) { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() var waveID int64 // 波次ID var waveNo string // 波次号 err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { var waveHeader models.WaveHeader if err := tx.Where("id = ? AND is_del = 0", req.WaveID).First(&waveHeader).Error; err != nil { return fmt.Errorf("波次不存在: %v", err) } if waveHeader.Direction != constant.DirectionInbound { return fmt.Errorf("该波次不是入库波次,无法提交") } if waveHeader.Status != constant.WaveStatusCreated && waveHeader.Status != constant.WaveStatusReleased { return fmt.Errorf("波次状态不正确,当前状态: %s,只有已创建或已下发状态才能提交", getWaveStatusText(waveHeader.Status)) } var purchaseOrder models.PurchaseOrder if err := tx.Where("id = ? AND is_del = 0", req.RelatedOrderID).First(&purchaseOrder).Error; err != nil { return fmt.Errorf("采购订单不存在: %v", err) } if waveHeader.RelatedOrderID != 0 && waveHeader.RelatedOrderID != purchaseOrder.ID { return fmt.Errorf("波次与采购订单不匹配") } if purchaseOrder.Status != constant.PurchaseStatusDraft && purchaseOrder.Status != constant.PurchaseStatusSubmitted && purchaseOrder.Status != constant.PurchaseStatusApproved { return fmt.Errorf("采购订单状态不正确,当前状态: %s,无法提交", getPurchaseStatusText(purchaseOrder.Status)) } isAppend := waveHeader.Status == constant.WaveStatusReleased var waveTask models.WaveTask var existingTotalQuantity int64 var batchNo string if isAppend { if err := tx.Where("wave_id = ? AND is_del = 0", waveHeader.ID).First(&waveTask).Error; err != nil { return fmt.Errorf("查询波次任务失败: %v", err) } if waveTask.ID == 0 { return fmt.Errorf("波次下没有任务,请先创建任务") } if waveTask.Status >= constant.WaveStatusPicking { return fmt.Errorf("波次任务[%s]状态已进入拣货阶段,无法追加数据", waveTask.TaskNo) } var firstDetail models.WaveTaskDetail if err := tx.Where("wave_task_id = ? AND is_del = 0", waveTask.ID).Order("id ASC").First(&firstDetail).Error; err != nil { return fmt.Errorf("查询任务[%s]现有批次号失败: %v", waveTask.TaskNo, err) } batchNo = firstDetail.BatchNo if err := tx.Model(&models.WaveTaskDetail{}). Where("wave_task_id = ? AND is_del = 0", waveTask.ID). Select("COALESCE(SUM(planned_quantity), 0)"). Scan(&existingTotalQuantity).Error; err != nil { return fmt.Errorf("查询任务[%s]现有数量失败: %v", waveTask.TaskNo, err) } } items := make([]WaveItemData, 0, len(req.Items)) purchaseOrderItems := make([]models.PurchaseOrderItem, 0, len(req.Items)) var additionalAmount int64 var newItemsTotalQuantity int64 for _, itemReq := range req.Items { amount := itemReq.Quantity * itemReq.UnitPrice additionalAmount += amount newItemsTotalQuantity += itemReq.Quantity items = append(items, WaveItemData{ ProductID: itemReq.ProductID, // 产品ID Quantity: itemReq.Quantity, // 数量 UnitPrice: itemReq.UnitPrice, // 单价 LocationID: 0, }) purchaseOrderItems = append(purchaseOrderItems, models.PurchaseOrderItem{ PurchaseOrderID: req.RelatedOrderID, //采购单ID ProductID: itemReq.ProductID, //产品ID Quantity: itemReq.Quantity, // 数量 ReceivedQuantity: 0, //已入库数量 UnitPrice: itemReq.UnitPrice, //单价 Amount: amount, //金额 CreatedAt: now, //创建时间戳 UpdatedAt: now, //更新时间戳 IsDel: 0, //逻辑删除标记 }) } if isAppend { totalAfterAppend := existingTotalQuantity + newItemsTotalQuantity if totalAfterAppend > carCapacity { return fmt.Errorf("波次任务[%s]追加后商品总数为%d,超过小车容量限制%d", waveTask.TaskNo, totalAfterAppend, carCapacity) } err := s.createWaveTaskDetails(tx, waveTask.ID, items, batchNo) if err != nil { return fmt.Errorf("为任务[%s]追加明细失败: %v", waveTask.TaskNo, err) } newTotalAmount := purchaseOrder.TotalAmount + additionalAmount if err := tx.Model(&models.PurchaseOrder{}).Where("id = ?", purchaseOrder.ID).Updates(map[string]interface{}{ "total_amount": newTotalAmount, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新采购订单总金额失败: %v", err) } } else { if newItemsTotalQuantity > carCapacity { return fmt.Errorf("波次任务商品总数为%d,超过小车容量限制%d", newItemsTotalQuantity, carCapacity) } waveTask, err := s.createWaveTaskAndDetails(tx, req.WaveID, constant.TaskTypePutaway, items, req.Assignee, req.AssigneeId, req.CarID, req.CarCode, carCapacity) if err != nil { return err } if err := s.syncTaskToExternal(waveTask.ID, carCapacity, tx); err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "入库任务外部接口", "error": fmt.Sprintf("同步失败: %v", err), }) } } if err := tx.Create(&purchaseOrderItems).Error; err != nil { return fmt.Errorf("批量创建采购订单明细失败: %v", err) } if waveHeader.Status == constant.WaveStatusCreated { if err := s.updateWaveStatusToReleased(tx, waveHeader.ID); err != nil { return fmt.Errorf("更新波次状态失败: %v", err) } } waveID = waveHeader.ID waveNo = waveHeader.WaveNo return nil }) if err != nil { return 0, "", err } return waveID, waveNo, nil } // BindWave 绑定波次,创建入库单 func (s *ProcessService) BindWave(req systemReq.BindWaveRequest, db ...*gorm.DB) (int64, int64, string, error) { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() var receivingOrderID int64 //入库单ID var waveTaskID int64 //入库任务ID var waveTaskBatchNo string //入库任务批次号 err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { var waveHeader models.WaveHeader if err := tx.Where("wave_no = ? AND is_del = 0", req.WaveNo).First(&waveHeader).Error; err != nil { return fmt.Errorf("波次不存在: %v", err) } var waveTask models.WaveTask if err := tx.Where("wave_id = ? AND is_del = 0", waveHeader.ID).First(&waveTask).Error; err != nil { return fmt.Errorf("波次任务不存在: %v", err) } waveTaskID = waveTask.ID if waveTask.Type != constant.TaskTypePutaway { return fmt.Errorf("该任务不是入库任务") } if waveTask.Status != constant.WaveStatusReleased && waveTask.Status != constant.WaveStatusCreated { return fmt.Errorf("波次任务状态不正确,当前状态: %s", getWaveStatusText(waveTask.Status)) } var purchaseOrder models.PurchaseOrder if err := tx.Where("id = ? AND is_del = 0", waveHeader.RelatedOrderID).First(&purchaseOrder).Error; err != nil { return fmt.Errorf("采购订单不存在: %v", err) } receivingNo := utils.GenerateReceivingNo() receivingOrder := models.ReceivingOrder{ ReceivingNo: receivingNo, //入库单号 PurchaseOrderID: purchaseOrder.ID, //采购订单ID WaveTaskID: waveTaskID, //入库任务ID WarehouseID: purchaseOrder.WarehouseID, //仓库ID SupplierID: purchaseOrder.SupplierID, //供应商ID ReceivingDate: now, //入库日期时间戳(秒) Status: constant.ReceivingStatusPending, //状态(1:待收货/pending, ) Operator: req.Operator, //操作人 OperatorID: req.OperatorID, //操作人ID Remark: req.Remark, //备注 CreatedAt: now, //创建时间戳(秒) UpdatedAt: now, //更新时间戳(秒) IsDel: 0, //逻辑删除标记 } if err := tx.Create(&receivingOrder).Error; err != nil { return fmt.Errorf("创建入库单失败: %v", err) } receivingOrderID = receivingOrder.ID if err := tx.Model(&models.WaveTask{}).Where("id = ?", waveTaskID).Updates(map[string]interface{}{ "assignee": req.Operator, "assignee_id": req.OperatorID, }).Error; err != nil { return fmt.Errorf("绑定指派人失败: %v", err) } var waveTaskDetails []models.WaveTaskDetail if err := tx.Where("wave_task_id = ? AND is_del = 0", waveTask.ID).Find(&waveTaskDetails).Error; err != nil { return fmt.Errorf("查询波次任务明细失败: %v", err) } waveTaskBatchNo = waveTaskDetails[0].BatchNo receivingItems := make([]models.ReceivingOrderItem, 0, len(waveTaskDetails)) for _, detail := range waveTaskDetails { receivingItems = append(receivingItems, models.ReceivingOrderItem{ ReceivingOrderID: receivingOrder.ID, //入库单ID ProductID: detail.ProductID, //商品ID LocationID: 0, //库位ID BatchNo: detail.BatchNo, //批次号 ProductionDate: 0, //生产日期时间戳(秒) ExpiryDate: 0, //失效日期时间戳(秒) Quantity: 0, //入库数量(最小单位) CreatedAt: now, //创建时间戳(秒) UpdatedAt: now, //更新时间戳(秒) IsDel: 0, //逻辑删除标记 }) } if err := tx.Create(&receivingItems).Error; err != nil { return fmt.Errorf("批量创建入库单明细失败: %v", err) } if err := s.updateWaveTaskToPicking(tx, waveTask.ID); err != nil { return fmt.Errorf("更新波次任务状态失败: %v", err) } return nil }) if err != nil { return 0, 0, "", err } return receivingOrderID, waveTaskID, waveTaskBatchNo, nil } // GetWaveTaskInfo 获取波次任务信息(兼容入库和出库) func (s *ProcessService) GetWaveTaskInfo(waveTaskID int64, db ...*gorm.DB) (interface{}, error) { databaseConn := database.OptionalDB(db...) var waveTask models.WaveTask if err := databaseConn.Where("id = ? AND is_del = 0", waveTaskID).First(&waveTask).Error; err != nil { return nil, fmt.Errorf("波次任务不存在: %v", err) } var waveHeader models.WaveHeader if err := databaseConn.Where("id = ?", waveTask.WaveID).First(&waveHeader).Error; err != nil { return nil, fmt.Errorf("波次不存在: %v", err) } var waveTaskDetails []models.WaveTaskDetail if err := databaseConn.Where("wave_task_id = ? AND is_del = 0", waveTaskID).Find(&waveTaskDetails).Error; err != nil { return nil, fmt.Errorf("查询波次任务明细失败: %v", err) } productIDs := make([]int64, 0, len(waveTaskDetails)) locationIDs := make([]int64, 0, len(waveTaskDetails)) for _, detail := range waveTaskDetails { productIDs = append(productIDs, detail.ProductID) if detail.LocationID > 0 { locationIDs = append(locationIDs, detail.LocationID) } } var products []models.Product if len(productIDs) > 0 { if err := databaseConn.Where("id IN ? AND is_del = 0", productIDs).Find(&products).Error; err != nil { return nil, fmt.Errorf("查询商品信息失败: %v", err) } } var locations []models.Location if len(locationIDs) > 0 { if err := databaseConn.Where("id IN ? AND is_del = 0", locationIDs).Find(&locations).Error; err != nil { return nil, fmt.Errorf("查询库位信息失败: %v", err) } } productMap := make(map[int64]models.Product) for _, product := range products { productMap[product.ID] = product } locationMap := make(map[int64]models.Location) for _, location := range locations { locationMap[location.ID] = location } items := make([]map[string]interface{}, 0, len(waveTaskDetails)) for _, detail := range waveTaskDetails { product, exists := productMap[detail.ProductID] if !exists { continue } item := map[string]interface{}{ "product_id": detail.ProductID, //商品ID "product_name": product.Name, //商品名称 "product_code": product.Barcode, //商品编码 "planned_quantity": detail.PlannedQuantity, //计划数量(最小单位) "actual_quantity": detail.ActualQuantity, //实际 } if detail.LocationID > 0 { if location, exists := locationMap[detail.LocationID]; exists { item["location_id"] = location.ID item["location_code"] = location.Code } } items = append(items, item) } result := map[string]interface{}{ "wave_task_id": waveTask.ID, //波次任务ID "task_no": waveTask.TaskNo, //波次任务号 "type": waveTask.Type, //波次任务类型(1:入库,2:出) "status": waveTask.Status, //波次任务状态(1:待处理,2:处理中) "assignee": waveTask.Assignee, //指定处理人 "direction": waveHeader.Direction, //方向(1:出库,2:入库) "warehouse_id": waveHeader.WarehouseID, //仓库ID "items": items, //波次任务明细 } return result, nil } // SubmitReceiving 提交入库 func (s *ProcessService) SubmitReceiving(req systemReq.ReceivingSubmitRequest, operator string, operatorID, userID int64, db ...*gorm.DB) error { items := make([]orderItemInfo, 0, len(req.Items)) for _, item := range req.Items { items = append(items, orderItemInfo{ productID: item.ProductID, //商品ID locationID: item.LocationID, //库位ID batchNo: item.BatchNo, //批次号 productionDate: item.ProductionDate, //生产日期时间戳(秒) expiryDate: item.ExpiryDate, //失效日期时间戳(秒) quantity: item.Quantity, //入库数量(最小单位) }) } err := s.submitOrderOperation(req.ReceivingOrderID, req.WaveTaskID, items, operator, operatorID, userID, constant.InventoryChangeInbound, req.Force, db...) if err == nil { s.saveStatist(userID, constant.InventoryChangeInbound, db...) } return err } // SubmitOutbound 提交出库 func (s *ProcessService) SubmitOutbound(req systemReq.OutboundSubmitRequest, operator string, operatorID int64, db ...*gorm.DB) error { items := make([]orderItemInfo, 0, len(req.Items)) for _, item := range req.Items { items = append(items, orderItemInfo{ productID: item.ProductID, //商品ID locationID: item.LocationID, //库位ID batchNo: item.BatchNo, //批次号 productionDate: item.ProductionDate, //生产日期时间戳(秒) expiryDate: item.ExpiryDate, //失效日期时间戳(秒) quantity: item.Quantity, //出库数量(最小单位) }) } err := s.submitOrderOperation(req.OutboundOrderID, req.WaveTaskID, items, operator, operatorID, 0, constant.InventoryChangeOutbound, req.Force, db...) if err == nil { s.saveStatist(operatorID, constant.InventoryChangeOutbound, db...) } return err } // submitOrderOperation 统一的订单提交操作(合并入库和出库逻辑) func (s *ProcessService) submitOrderOperation(orderID, waveTaskID int64, items []orderItemInfo, operator string, operatorID, userID int64, changeType int8, force int8, db ...*gorm.DB) error { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() return executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { if force == 1 { var waveTask models.WaveTask if err := tx.Where("id = ? AND is_del = 0", waveTaskID).First(&waveTask).Error; err != nil { return fmt.Errorf("查询波次任务失败: %v", err) } waveTask.Status = constant.WaveStatusCompleted waveTask.CompletedAt = now waveTask.UpdatedAt = now waveTask.IsForce = 1 if err := tx.Save(&waveTask).Error; err != nil { return fmt.Errorf("更新波次任务状态失败: %v", err) } if err := tx.Model(&models.WaveHeader{}).Where("id = ? AND is_del = 0", waveTask.WaveID).Updates(map[string]interface{}{ "status": constant.WaveStatusCompleted, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新波次主表状态失败: %v", err) } if changeType == constant.InventoryChangeInbound { if err := tx.Model(&models.ReceivingOrder{}).Where("id = ?", orderID).Updates(map[string]interface{}{ "status": constant.ReceivingStatusCompleted, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新入库单状态失败: %v", err) } } else { if err := tx.Model(&models.OutboundOrder{}).Where("id = ?", orderID).Updates(map[string]interface{}{ "status": constant.OutboundStatusCompleted, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新出库单状态失败: %v", err) } } } else { orderInfo, err := s.validateAndGetOrderInfo(tx, orderID, waveTaskID, changeType) if err != nil { return err } productMap, locationMap, err := s.validateProductsAndLocations(tx, items, orderInfo.warehouseID) if err != nil { return err } orderItems, waveTaskDetails, err := s.loadOrderAndWaveDetails(tx, orderID, waveTaskID, changeType) if err != nil { return err } if changeType == constant.InventoryChangeOutbound { if err := s.validateOutboundQuantity(tx, orderID, items); err != nil { return err } } inventoryOpMap, inventoryLogs, err := s.processOrderItems(tx, items, orderInfo, productMap, locationMap, orderItems, waveTaskDetails, operator, operatorID, now, changeType) if err != nil { return err } if err := s.executeInventoryOperations(tx, inventoryOpMap, inventoryLogs, orderInfo.orderNo, operator, operatorID, now, changeType); err != nil { return err } if err := s.batchUpdateOrderItems(tx, orderItems, changeType); err != nil { return err } if err := s.batchUpdateWaveTaskDetails(tx, waveTaskDetails); err != nil { return err } if err := s.updateOrderAndTaskStatus(tx, orderInfo, waveTaskDetails, waveTaskID, now, changeType); err != nil { return err } if changeType == constant.InventoryChangeOutbound { if err := s.updateOutboundOrderSummary(tx, orderID, now); err != nil { return fmt.Errorf("更新出库单汇总信息失败: %v", err) } } if changeType == constant.InventoryChangeInbound { if err := s.updatePurchaseOrderReceivedStatus(tx, orderID, now); err != nil { return fmt.Errorf("更新采购订单收货状态失败: %v", err) } if err := s.syncProductsToExternal(orderID, waveTaskID, userID, items, tx); err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "入库发送任务体", "receiving_order_id": orderID, "wave_task_id": waveTaskID, "error": fmt.Sprintf("发送失败: %v", err), }) } } } return nil }) } // validateAndGetOrderInfo 验证并获取订单信息 func (s *ProcessService) validateAndGetOrderInfo(tx *gorm.DB, orderID, waveTaskID int64, changeType int8) (*orderInfo, error) { if changeType == constant.InventoryChangeInbound { var receivingOrder models.ReceivingOrder if err := tx.Where("id = ? AND is_del = 0", orderID).First(&receivingOrder).Error; err != nil { return nil, fmt.Errorf("入库单不存在: %v", err) } if receivingOrder.Status == constant.ReceivingStatusCompleted { return nil, fmt.Errorf("入库单已完成,无法继续入库") } if receivingOrder.Status == constant.ReceivingStatusCancelled { return nil, fmt.Errorf("入库单已取消") } var waveTask models.WaveTask if err := tx.Where("id = ? AND is_del = 0", waveTaskID).First(&waveTask).Error; err != nil { return nil, fmt.Errorf("波次任务不存在: %v", err) } return &orderInfo{ orderID: receivingOrder.ID, //入库单ID orderNo: receivingOrder.ReceivingNo, //入库单号 warehouseID: receivingOrder.WarehouseID, //仓库ID status: int(receivingOrder.Status), //订单状态 }, nil } else { var outboundOrder models.OutboundOrder if err := tx.Where("id = ? AND is_del = 0", orderID).First(&outboundOrder).Error; err != nil { return nil, fmt.Errorf("出库单不存在: %v", err) } if outboundOrder.Status == constant.OutboundStatusCompleted { return nil, fmt.Errorf("出库单已完成,无法继续出库") } if outboundOrder.Status == constant.OutboundStatusCancelled { return nil, fmt.Errorf("出库单已取消") } var waveTask models.WaveTask if err := tx.Where("id = ? AND is_del = 0", waveTaskID).First(&waveTask).Error; err != nil { return nil, fmt.Errorf("波次任务不存在: %v", err) } return &orderInfo{ orderID: outboundOrder.ID, //出库单ID orderNo: outboundOrder.OutNo, //出库单号 warehouseID: outboundOrder.WarehouseID, //仓库ID status: int(outboundOrder.Status), //订单状态 }, nil } } // validateProductsAndLocations 验证商品和库位 func (s *ProcessService) validateProductsAndLocations(tx *gorm.DB, items []orderItemInfo, warehouseID int64) (map[int64]models.Product, map[int64]models.Location, error) { productIDs := make([]int64, 0, len(items)) locationIDs := make([]int64, 0, len(items)) for _, item := range items { if item.quantity <= 0 { return nil, nil, fmt.Errorf("商品%d的数量必须大于0", item.productID) } productIDs = append(productIDs, item.productID) locationIDs = append(locationIDs, item.locationID) } var products []models.Product if err := tx.Where("id IN ? AND is_del = 0", productIDs).Find(&products).Error; err != nil { return nil, nil, fmt.Errorf("查询商品失败: %v", err) } productMap := make(map[int64]models.Product) for _, p := range products { if p.Status != 1 { return nil, nil, fmt.Errorf("商品%s已停用", p.Name) } productMap[p.ID] = p } var locations []models.Location if err := tx.Where("id IN ? AND is_del = 0", locationIDs).Find(&locations).Error; err != nil { return nil, nil, fmt.Errorf("查询库位失败: %v", err) } locationMap := make(map[int64]models.Location) for _, l := range locations { if l.Status != 1 { return nil, nil, fmt.Errorf("库位%s不可用", l.Code) } if l.WarehouseID != warehouseID { return nil, nil, fmt.Errorf("库位%s不属于该仓库", l.Code) } locationMap[l.ID] = l } return productMap, locationMap, nil } // loadOrderAndWaveDetails 加载订单明细和波次任务明细 func (s *ProcessService) loadOrderAndWaveDetails(tx *gorm.DB, orderID, waveTaskID int64, changeType int8) (map[int64]interface{}, map[int64]*models.WaveTaskDetail, error) { orderItemMap := make(map[int64]interface{}) waveTaskDetailMap := make(map[int64]*models.WaveTaskDetail) if changeType == constant.InventoryChangeInbound { var receivingOrderItems []models.ReceivingOrderItem if err := tx.Where("receiving_order_id = ? AND is_del = 0", orderID).Find(&receivingOrderItems).Error; err != nil { return nil, nil, fmt.Errorf("查询入库单明细失败: %v", err) } for i := range receivingOrderItems { orderItemMap[receivingOrderItems[i].ProductID] = &receivingOrderItems[i] } } else { var outboundOrderItems []models.OutboundOrderItem if err := tx.Where("out_order_id = ? AND is_del = 0", orderID).Order("id ASC").Find(&outboundOrderItems).Error; err != nil { return nil, nil, fmt.Errorf("查询出库单明细失败: %v", err) } // 出库使用索引作为key,保持顺序 for i := range outboundOrderItems { orderItemMap[int64(i)] = &outboundOrderItems[i] } } var waveTaskDetails []models.WaveTaskDetail if err := tx.Where("wave_task_id = ? AND is_del = 0", waveTaskID).Order("id ASC").Find(&waveTaskDetails).Error; err != nil { return nil, nil, fmt.Errorf("查询波次任务明细失败: %v", err) } if changeType == constant.InventoryChangeInbound { // 入库使用商品ID作为key for i := range waveTaskDetails { waveTaskDetailMap[waveTaskDetails[i].ProductID] = &waveTaskDetails[i] } } else { // 出库使用索引作为key,保持顺序 for i := range waveTaskDetails { waveTaskDetailMap[int64(i)] = &waveTaskDetails[i] } } return orderItemMap, waveTaskDetailMap, nil } // processOrderItems 处理订单明细 func (s *ProcessService) processOrderItems(tx *gorm.DB, items []orderItemInfo, orderInfo *orderInfo, productMap map[int64]models.Product, locationMap map[int64]models.Location, orderItemMap map[int64]interface{}, waveTaskDetailMap map[int64]*models.WaveTaskDetail, operator string, operatorID int64, now int64, changeType int8) (map[inventoryKey]*inventoryOperation, []models.InventoryLog, error) { inventoryOpMap := make(map[inventoryKey]*inventoryOperation) if changeType == constant.InventoryChangeInbound { // 入库逻辑:按商品ID匹配 for _, itemReq := range items { if _, exists := productMap[itemReq.productID]; !exists { return nil, nil, fmt.Errorf("商品不存在: %d", itemReq.productID) } _, exists := locationMap[itemReq.locationID] if !exists { return nil, nil, fmt.Errorf("库位不存在: %d", itemReq.locationID) } if orderItem, exists := orderItemMap[itemReq.productID]; exists { receivingItem := orderItem.(*models.ReceivingOrderItem) // 类型断言 receivingItem.LocationID = itemReq.locationID //入库库位ID receivingItem.BatchNo = itemReq.batchNo //批次号 receivingItem.ProductionDate = itemReq.productionDate //生产日期 receivingItem.ExpiryDate = itemReq.expiryDate //到期日期 receivingItem.Quantity += itemReq.quantity // 数量 receivingItem.UpdatedAt = now //更新时间戳(秒) } else { return nil, nil, fmt.Errorf("订单中不存在该商品: %d", itemReq.productID) } waveTaskDetail, exists := waveTaskDetailMap[itemReq.productID] if !exists { return nil, nil, fmt.Errorf("波次任务明细不存在: %d", itemReq.productID) } waveTaskDetail.ActualQuantity += itemReq.quantity // 数量 waveTaskDetail.LocationID = itemReq.locationID //入库库位ID waveTaskDetail.BatchNo = itemReq.batchNo //批次号 waveTaskDetail.UpdatedAt = now //更新时间戳(秒) if waveTaskDetail.ActualQuantity >= waveTaskDetail.PlannedQuantity { waveTaskDetail.Status = constant.WaveStatusReleased } key := inventoryKey{ warehouseID: orderInfo.warehouseID, //仓库ID productID: itemReq.productID, //商品ID batchNo: itemReq.batchNo, //批次号 productionDate: itemReq.productionDate, //生产日期 expiryDate: itemReq.expiryDate, //到期日期 } if op, exists := inventoryOpMap[key]; exists { op.quantity += itemReq.quantity } else { inventoryOpMap[key] = &inventoryOperation{ key: key, locationID: itemReq.locationID, quantity: itemReq.quantity, } } } } else { // 出库逻辑:支持两种情况 // 1. 商品ID都不一样:通过商品ID直接匹配 // 2. 商品ID有重复:在同一商品ID的记录中按数据库顺序依次匹配 // 用于跟踪每个商品ID已使用的索引位置(针对该商品ID的记录列表) productUsedIndex := make(map[int64]int) // 构建商品ID到出库单明细列表的映射(保持数据库顺序) productToOutboundItems := make(map[int64][]*models.OutboundOrderItem) for i := int64(0); i < int64(len(orderItemMap)); i++ { if orderItem, exists := orderItemMap[i]; exists { item := orderItem.(*models.OutboundOrderItem) productToOutboundItems[item.ProductID] = append(productToOutboundItems[item.ProductID], item) } } // 构建商品ID到波次任务明细列表的映射(保持数据库顺序) productToWaveDetails := make(map[int64][]*models.WaveTaskDetail) for i := int64(0); i < int64(len(waveTaskDetailMap)); i++ { if detail, exists := waveTaskDetailMap[i]; exists { productToWaveDetails[detail.ProductID] = append(productToWaveDetails[detail.ProductID], detail) } } for _, itemReq := range items { if _, exists := productMap[itemReq.productID]; !exists { return nil, nil, fmt.Errorf("商品不存在: %d", itemReq.productID) } _, exists := locationMap[itemReq.locationID] if !exists { return nil, nil, fmt.Errorf("库位不存在: %d", itemReq.locationID) } // 获取该商品ID对应的出库单明细列表 outboundItems := productToOutboundItems[itemReq.productID] if len(outboundItems) == 0 { return nil, nil, fmt.Errorf("出库单中不存在商品ID=%d的明细记录", itemReq.productID) } // 获取该商品ID已使用的索引(只在该商品ID的记录范围内使用) usedIdx := productUsedIndex[itemReq.productID] // 如果已使用索引超出范围,说明没有更多可出库的记录 if usedIdx >= len(outboundItems) { return nil, nil, fmt.Errorf("商品ID=%d没有更多可出库的明细记录", itemReq.productID) } // 在该商品ID的记录列表中,从上次使用的位置开始查找第一条未出库的记录 var outboundItem *models.OutboundOrderItem var foundIdx int found := false for i := usedIdx; i < len(outboundItems); i++ { if outboundItems[i].Quantity == 0 { outboundItem = outboundItems[i] foundIdx = i found = true break } } if !found || outboundItem == nil { return nil, nil, fmt.Errorf("商品ID=%d没有可出库的明细记录(所有记录都已出库)", itemReq.productID) } // 更新该商品ID的已使用索引 productUsedIndex[itemReq.productID] = foundIdx + 1 // 更新找到的出库单明细 outboundItem.LocationID = itemReq.locationID //出库库位ID outboundItem.BatchNo = itemReq.batchNo //批次号 outboundItem.ProductionDate = itemReq.productionDate //生产日期 outboundItem.ExpiryDate = itemReq.expiryDate //到期日期 outboundItem.Quantity = itemReq.quantity // 数量 outboundItem.UpdatedAt = now // 获取该商品ID对应的波次任务明细列表 waveDetails := productToWaveDetails[itemReq.productID] if len(waveDetails) == 0 { return nil, nil, fmt.Errorf("波次任务中不存在商品ID=%d的明细记录", itemReq.productID) } // 在该商品ID的波次记录列表中,从上次使用的位置开始查找第一条未出库的记录 waveUsedIdx := productUsedIndex[itemReq.productID] - 1 // 使用与出库单相同的索引位置 var waveTaskDetail *models.WaveTaskDetail if waveUsedIdx < len(waveDetails) && waveDetails[waveUsedIdx].ActualQuantity == 0 { // 直接使用对应位置的波次明细 waveTaskDetail = waveDetails[waveUsedIdx] } else { // 如果对应位置不可用,则从该位置开始查找第一条可用的 found = false for i := waveUsedIdx; i < len(waveDetails); i++ { if waveDetails[i].ActualQuantity == 0 { waveTaskDetail = waveDetails[i] found = true break } } if !found || waveTaskDetail == nil { return nil, nil, fmt.Errorf("商品ID=%d没有可出库的波次任务明细记录", itemReq.productID) } } // 更新找到的波次任务明细 waveTaskDetail.ActualQuantity = itemReq.quantity waveTaskDetail.LocationID = itemReq.locationID waveTaskDetail.BatchNo = itemReq.batchNo waveTaskDetail.UpdatedAt = now if waveTaskDetail.ActualQuantity >= waveTaskDetail.PlannedQuantity { waveTaskDetail.Status = constant.WaveStatusReleased } key := inventoryKey{ warehouseID: orderInfo.warehouseID, productID: itemReq.productID, batchNo: itemReq.batchNo, productionDate: itemReq.productionDate, expiryDate: itemReq.expiryDate, } if op, exists := inventoryOpMap[key]; exists { op.quantity += itemReq.quantity } else { inventoryOpMap[key] = &inventoryOperation{ key: key, locationID: itemReq.locationID, quantity: itemReq.quantity, } } } } return inventoryOpMap, nil, nil } // executeInventoryOperations 执行库存操作 func (s *ProcessService) executeInventoryOperations(tx *gorm.DB, inventoryOpMap map[inventoryKey]*inventoryOperation, inventoryLogs []models.InventoryLog, orderNo string, operator string, operatorID int64, now int64, changeType int8) error { for _, op := range inventoryOpMap { log, err := s.processInventoryOperation(tx, op.key, op.locationID, op.quantity, changeType, orderNo, operator, operatorID, now) if err != nil { return err } if log != nil { inventoryLogs = append(inventoryLogs, *log) } if err := s.processInventoryDetailOperation(tx, op.key, op.locationID, op.quantity, changeType, now); err != nil { return err } } if len(inventoryLogs) > 0 { if err := tx.Create(&inventoryLogs).Error; err != nil { return fmt.Errorf("批量创建库存日志失败: %v", err) } } return nil } // batchUpdateOrderItems 批量更新订单明细 func (s *ProcessService) batchUpdateOrderItems(tx *gorm.DB, orderItemMap map[int64]interface{}, changeType int8) error { var errs []string for _, item := range orderItemMap { if changeType == constant.InventoryChangeInbound { receivingItem := item.(*models.ReceivingOrderItem) // 在更新之前先获取原始数量 var originalQuantity int64 if err := tx.Model(&models.ReceivingOrderItem{}). Where("id = ? AND is_del = 0", receivingItem.ID). Select("quantity"). Scan(&originalQuantity).Error; err != nil { errs = append(errs, fmt.Sprintf("查询入库单明细ID=%d原始数量失败: %v", receivingItem.ID, err)) continue } // 计算本次实际入库数量 = 累加后的数量 - 原始数量 actualInboundQuantity := receivingItem.Quantity - originalQuantity if err := tx.Model(receivingItem).Updates(map[string]interface{}{ "location_id": receivingItem.LocationID, "batch_no": receivingItem.BatchNo, "production_date": receivingItem.ProductionDate, "expiry_date": receivingItem.ExpiryDate, "quantity": receivingItem.Quantity, "updated_at": receivingItem.UpdatedAt, }).Error; err != nil { errs = append(errs, fmt.Sprintf("更新入库单明细ID=%d失败: %v", receivingItem.ID, err)) } if actualInboundQuantity > 0 { if err := tx.Model(&models.PurchaseOrderItem{}). Where("purchase_order_id IN (SELECT purchase_order_id FROM receiving_order WHERE id = ?) AND product_id = ? AND is_del = 0", receivingItem.ReceivingOrderID, receivingItem.ProductID). Updates(map[string]interface{}{ "received_quantity": gorm.Expr("received_quantity + ?", actualInboundQuantity), "updated_at": receivingItem.UpdatedAt, }).Error; err != nil { errs = append(errs, fmt.Sprintf("更新采购订单明细商品ID=%d已收货数量失败: %v", receivingItem.ProductID, err)) } } } else { outboundItem := item.(*models.OutboundOrderItem) if err := tx.Model(outboundItem).Updates(map[string]interface{}{ "location_id": outboundItem.LocationID, "batch_no": outboundItem.BatchNo, "production_date": outboundItem.ProductionDate, "expiry_date": outboundItem.ExpiryDate, "quantity": outboundItem.Quantity, "updated_at": outboundItem.UpdatedAt, }).Error; err != nil { errs = append(errs, fmt.Sprintf("更新出库单明细ID=%d失败: %v", outboundItem.ID, err)) } } } if len(errs) > 0 { return fmt.Errorf("批量更新订单明细失败: %s", strings.Join(errs, "; ")) } return nil } // batchUpdateWaveTaskDetails 批量更新波次任务明细 func (s *ProcessService) batchUpdateWaveTaskDetails(tx *gorm.DB, waveTaskDetailMap map[int64]*models.WaveTaskDetail) error { var errs []string for _, detail := range waveTaskDetailMap { if err := tx.Model(detail).Updates(map[string]interface{}{ "actual_quantity": detail.ActualQuantity, "location_id": detail.LocationID, "batch_no": detail.BatchNo, "status": detail.Status, "updated_at": detail.UpdatedAt, }).Error; err != nil { errs = append(errs, fmt.Sprintf("更新波次任务明细ID=%d失败: %v", detail.ID, err)) } } if len(errs) > 0 { return fmt.Errorf("批量更新波次任务明细失败: %s", strings.Join(errs, "; ")) } return nil } // updateOrderAndTaskStatus 更新订单和任务状态 func (s *ProcessService) updateOrderAndTaskStatus(tx *gorm.DB, orderInfo *orderInfo, waveTaskDetails map[int64]*models.WaveTaskDetail, waveTaskID int64, now int64, changeType int8) error { allCompleted := true for _, detail := range waveTaskDetails { if detail.ActualQuantity < detail.PlannedQuantity { allCompleted = false break } } var waveTask models.WaveTask if err := tx.Where("id = ? AND is_del = 0", waveTaskID).First(&waveTask).Error; err != nil { return fmt.Errorf("查询波次任务失败: %v", err) } if allCompleted { waveTask.Status = constant.WaveStatusCompleted waveTask.CompletedAt = now waveTask.UpdatedAt = now if err := tx.Save(&waveTask).Error; err != nil { return fmt.Errorf("更新波次任务状态失败: %v", err) } if err := tx.Model(&models.WaveHeader{}).Where("id = ? AND is_del = 0", waveTask.WaveID).Updates(map[string]interface{}{ "status": constant.WaveStatusCompleted, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新波次主表状态失败: %v", err) } if changeType == constant.InventoryChangeInbound { if orderInfo.status != constant.ReceivingStatusCompleted { if err := tx.Model(&models.ReceivingOrder{}).Where("id = ?", orderInfo.orderID).Updates(map[string]interface{}{ "status": constant.ReceivingStatusCompleted, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新入库单状态失败: %v", err) } } } else { if orderInfo.status != constant.OutboundStatusCompleted { if err := tx.Model(&models.OutboundOrder{}).Where("id = ?", orderInfo.orderID).Updates(map[string]interface{}{ "status": constant.OutboundStatusCompleted, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新出库单状态失败: %v", err) } } } } else { if changeType == constant.InventoryChangeInbound { if orderInfo.status == constant.ReceivingStatusPending { if err := tx.Model(&models.ReceivingOrder{}).Where("id = ?", orderInfo.orderID).Updates(map[string]interface{}{ "status": constant.ReceivingStatusChecking, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新入库单状态失败: %v", err) } } } else { if orderInfo.status == constant.OutboundStatusCreated { if err := tx.Model(&models.OutboundOrder{}).Where("id = ?", orderInfo.orderID).Updates(map[string]interface{}{ "status": constant.OutboundStatusPicking, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新出库单状态失败: %v", err) } } } } return nil } // updateOutboundOrderSummary 更新出库单汇总信息(总数量和总金额) func (s *ProcessService) updateOutboundOrderSummary(tx *gorm.DB, outboundOrderID int64, now int64) error { var outboundOrderItems []models.OutboundOrderItem if err := tx.Where("out_order_id = ? AND is_del = 0", outboundOrderID).Find(&outboundOrderItems).Error; err != nil { return fmt.Errorf("查询出库单明细失败: %v", err) } totalQuantity := int64(0) totalAmount := int64(0) for _, item := range outboundOrderItems { totalQuantity += item.Quantity totalAmount += item.Quantity * item.UnitPrice } if err := tx.Model(&models.OutboundOrder{}).Where("id = ?", outboundOrderID).Updates(map[string]interface{}{ "total_quantity": totalQuantity, "total_amount": totalAmount, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新出库单汇总信息失败: %v", err) } return nil } // updatePurchaseOrderReceivedStatus 更新采购订单收货状态 func (s *ProcessService) updatePurchaseOrderReceivedStatus(tx *gorm.DB, receivingOrderID int64, now int64) error { var receivingOrder models.ReceivingOrder if err := tx.Where("id = ? AND is_del = 0", receivingOrderID).First(&receivingOrder).Error; err != nil { return fmt.Errorf("查询入库单失败: %v", err) } if receivingOrder.PurchaseOrderID == 0 { return nil } var purchaseOrder models.PurchaseOrder if err := tx.Where("id = ? AND is_del = 0", receivingOrder.PurchaseOrderID).First(&purchaseOrder).Error; err != nil { return fmt.Errorf("查询采购订单失败: %v", err) } if purchaseOrder.Status == constant.PurchaseStatusCancelled { return nil } var purchaseOrderItems []models.PurchaseOrderItem if err := tx.Where("purchase_order_id = ? AND is_del = 0", purchaseOrder.ID).Find(&purchaseOrderItems).Error; err != nil { return fmt.Errorf("查询采购订单明细失败: %v", err) } totalQuantity := int64(0) totalReceivedQuantity := int64(0) for _, item := range purchaseOrderItems { totalQuantity += item.Quantity totalReceivedQuantity += item.ReceivedQuantity } if totalReceivedQuantity == 0 { return nil } newStatus := constant.PurchaseStatusPartialReceived if totalReceivedQuantity >= totalQuantity { newStatus = constant.PurchaseStatusReceived } if purchaseOrder.Status != int8(newStatus) { if err := tx.Model(&models.PurchaseOrder{}).Where("id = ?", purchaseOrder.ID).Updates(map[string]interface{}{ "status": newStatus, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新采购订单状态失败: %v", err) } } return nil } // GetReceivingDetail 获取入库单详情 func (s *ProcessService) GetReceivingDetail(receivingOrderID int64, db ...*gorm.DB) (interface{}, error) { databaseConn := database.OptionalDB(db...) var receivingOrder models.ReceivingOrder if err := databaseConn.Where("id = ? AND is_del = 0", receivingOrderID).First(&receivingOrder).Error; err != nil { return nil, fmt.Errorf("入库单不存在: %v", err) } var receivingItems []models.ReceivingOrderItem if err := databaseConn.Where("receiving_order_id = ? AND is_del = 0", receivingOrderID).Find(&receivingItems).Error; err != nil { return nil, fmt.Errorf("查询入库单明细失败: %v", err) } if len(receivingItems) == 0 { result := map[string]interface{}{ "receiving_order_id": receivingOrder.ID, "receiving_no": receivingOrder.ReceivingNo, "status": receivingOrder.Status, "warehouse_id": receivingOrder.WarehouseID, "supplier_id": receivingOrder.SupplierID, "operator": receivingOrder.Operator, "remark": receivingOrder.Remark, "items": []map[string]interface{}{}, } return result, nil } productIDs := make([]int64, 0, len(receivingItems)) locationIDs := make([]int64, 0) for _, item := range receivingItems { productIDs = append(productIDs, item.ProductID) if item.LocationID > 0 { locationIDs = append(locationIDs, item.LocationID) } } var products []models.Product if err := databaseConn.Where("id IN ? AND is_del = 0", productIDs).Find(&products).Error; err != nil { return nil, fmt.Errorf("查询商品失败: %v", err) } productMap := make(map[int64]models.Product) for _, p := range products { productMap[p.ID] = p } locationMap := make(map[int64]models.Location) if len(locationIDs) > 0 { var locations []models.Location if err := databaseConn.Where("id IN ? AND is_del = 0", locationIDs).Find(&locations).Error; err != nil { return nil, fmt.Errorf("查询库位失败: %v", err) } for _, l := range locations { locationMap[l.ID] = l } } items := make([]map[string]interface{}, 0, len(receivingItems)) for _, item := range receivingItems { product, exists := productMap[item.ProductID] if !exists { continue } locationCode := "" if item.LocationID > 0 { if location, exists := locationMap[item.LocationID]; exists { locationCode = location.Code } } items = append(items, map[string]interface{}{ "id": item.ID, "product_id": item.ProductID, "product_name": product.Name, "product_code": product.Barcode, "location_id": item.LocationID, "location_code": locationCode, "batch_no": item.BatchNo, "production_date": item.ProductionDate, "expiry_date": item.ExpiryDate, "quantity": item.Quantity, }) } result := map[string]interface{}{ "receiving_order_id": receivingOrder.ID, "receiving_no": receivingOrder.ReceivingNo, "status": receivingOrder.Status, "warehouse_id": receivingOrder.WarehouseID, "supplier_id": receivingOrder.SupplierID, "operator": receivingOrder.Operator, "remark": receivingOrder.Remark, "items": items, } return result, nil } // CreateSalesOrderWithDetail 创建销售订单(事务内) func (s *ProcessService) CreateSalesOrderWithDetail(req systemReq.SalesOrderCreateRequest) (int64, error) { databaseConn, err := database.GetTenantDB(req.AboutId) if err != nil { return 0, fmt.Errorf("获取数据库连接失败: %v", err) } if len(req.Items) == 0 { return 0, fmt.Errorf("销售订单明细不能为空") } if len(req.Items) > constant.MaxTaskQuantity { return 0, fmt.Errorf("销售订单明细数量不能超过200条,当前为%d条", len(req.Items)) } now := time.Now().Unix() var salesOrderID int64 var soNo string err = executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { if len(req.Items) == 0 { return fmt.Errorf("销售订单明细不能为空") } // 防重检查:如果 AssociationOrderID 不为0,检查是否已存在相同订单 if req.AssociationOrderID != 0 { var existingOrder models.SalesOrder if err := tx.Where("association_order_id = ? AND is_del = 0", req.AssociationOrderID).First(&existingOrder).Error; err == nil { // 订单已存在,直接返回已有订单ID(幂等处理) salesOrderID = existingOrder.ID return nil } else if err != gorm.ErrRecordNotFound { return fmt.Errorf("查询重复订单失败: %v", err) } } var invWarehouseID int64 for i, item := range req.Items { var inventory models.Inventory if err := tx.Where("product_id = ? AND quantity > 0 AND is_del = 0", item.ProductID). First(&inventory).Error; err != nil { return fmt.Errorf("商品[%d]无可用库存: %v", item.ProductID, err) } if i == 0 { invWarehouseID = inventory.WarehouseID } else if inventory.WarehouseID != invWarehouseID { return fmt.Errorf("所有商品必须属于同一个仓库,商品[%d]与第一个商品仓库不一致", item.ProductID) } } var warehouse models.Warehouse if err := tx.Where("id = ? AND is_del = 0", invWarehouseID).First(&warehouse).Error; err != nil { return fmt.Errorf("仓库不存在或已删除: %v", err) } var totalAmount int64 for _, item := range req.Items { totalAmount += item.Quantity * item.UnitPrice } soNo = utils.GenerateSalesNo() salesOrder := models.SalesOrder{ SoNo: soNo, AssociationOrderId: req.AssociationOrderID, AssociationOrderNo: req.AssociationOrderNo, FromType: req.FromType, ShopType: req.ShopType, CustomerID: req.CustomerID, WarehouseID: invWarehouseID, OrderDate: now, RequiredDeliveryDate: req.RequiredDeliveryDate, TotalAmount: totalAmount, Status: constant.SalesStatusAllocated, SalesPerson: req.SalesPerson, SalesPersonID: req.SalesPersonID, Remark: req.Remark, IsDistribution: req.IsDistribution, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&salesOrder).Error; err != nil { return fmt.Errorf("创建销售订单失败: %v", err) } salesOrderID = salesOrder.ID salesOrderItems := make([]models.SalesOrderItem, 0, len(req.Items)) for _, itemReq := range req.Items { amount := itemReq.Quantity * itemReq.UnitPrice 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, }) } 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 }) if err != nil { return 0, err } return salesOrderID, nil } // CreateOutboundOrder 基于多个销售订单创建出库单 func (s *ProcessService) CreateOutboundOrder(req systemReq.CreateOutboundOrderRequest, operator string, operatorID int64, db ...*gorm.DB) (int64, string, error) { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() var outboundOrderID int64 var outNo string err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { if len(req.SalesOrderIDs) == 0 { return fmt.Errorf("销售订单列表不能为空") } var salesOrders []models.SalesOrder if err := tx.Where("id IN ? AND is_del = 0", req.SalesOrderIDs).Find(&salesOrders).Error; err != nil { return fmt.Errorf("查询销售订单失败: %v", err) } if len(salesOrders) != len(req.SalesOrderIDs) { return fmt.Errorf("部分销售订单不存在") } for _, order := range salesOrders { if order.Status != constant.SalesStatusAllocated { return fmt.Errorf("销售订单[%s]状态不正确,当前状态: %s,只有已分配状态的订单才能创建出库单", order.SoNo, getSalesStatusText(order.Status)) } } warehouseID := salesOrders[0].WarehouseID customerID := salesOrders[0].CustomerID for i, order := range salesOrders[1:] { if order.WarehouseID != warehouseID { return fmt.Errorf("所有销售订单必须属于同一个仓库,订单[%s]与第一个订单仓库不一致", salesOrders[i+1].SoNo) } if order.CustomerID != customerID { return fmt.Errorf("所有销售订单必须属于同一个客户,订单[%s]与第一个订单客户不一致", salesOrders[i+1].SoNo) } } for _, order := range salesOrders { if order.Status >= constant.SalesStatusPicking { return fmt.Errorf("销售订单[%s]已存在出库单或正在处理中", order.SoNo) } } var allSalesItems []models.SalesOrderItem if err := tx.Where("sales_order_id IN ? AND is_del = 0", req.SalesOrderIDs).Find(&allSalesItems).Error; err != nil { return fmt.Errorf("查询销售订单明细失败: %v", err) } if len(allSalesItems) == 0 { return fmt.Errorf("选中的销售订单没有明细数据") } outboundItems := make([]models.OutboundOrderItem, 0, len(allSalesItems)) for _, item := range allSalesItems { unshippedQuantity := item.Quantity - item.ShippedQuantity if unshippedQuantity <= 0 { continue } var inventoryDetail models.InventoryDetail locationID := int64(0) if err := tx.Where("warehouse_id = ? AND product_id = ? AND quantity > 0 AND is_del = 0", warehouseID, item.ProductID). Order("created_at ASC"). First(&inventoryDetail).Error; err == nil { locationID = inventoryDetail.LocationID } fmt.Println() outboundItems = append(outboundItems, models.OutboundOrderItem{ SalesOrderID: item.SalesOrderID, ProductID: item.ProductID, LocationID: locationID, BatchNo: "", Quantity: 0, UnitPrice: item.UnitPrice, CreatedAt: now, UpdatedAt: now, IsDel: 0, }) } if len(outboundItems) == 0 { return fmt.Errorf("所有商品已全部出库,无法创建出库单") } outNo = utils.GenerateOutNo() outboundOrder := models.OutboundOrder{ OutNo: outNo, WaveTaskID: 0, WarehouseID: warehouseID, CustomerID: customerID, TotalQuantity: 0, TotalAmount: 0, Status: constant.OutboundStatusCreated, Operator: operator, OperatorID: operatorID, Remark: req.Remark, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&outboundOrder).Error; err != nil { return fmt.Errorf("创建出库单失败: %v", err) } outboundOrderID = outboundOrder.ID for i := range outboundItems { outboundItems[i].OutOrderID = outboundOrder.ID } if err := tx.Create(&outboundItems).Error; err != nil { return fmt.Errorf("创建出库单明细失败: %v", err) } for _, order := range salesOrders { if err := tx.Model(&models.SalesOrder{}).Where("id = ?", order.ID).Updates(map[string]interface{}{ "status": constant.SalesStatusPicking, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新销售订单[%s]状态失败: %v", order.SoNo, err) } } return nil }) if err != nil { return 0, "", err } //TODO // 分账逻辑写在这里----- return outboundOrderID, outNo, nil } // CreateOutboundWave 基于出库单创建出库波次 func (s *ProcessService) CreateOutboundWave(req systemReq.CreateOutboundWaveRequest, creator string, creatorID int64, db ...*gorm.DB) (int64, error) { databaseConn := database.OptionalDB(db...) var waveID int64 err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { if req.OutboundOrderID == 0 { return fmt.Errorf("出库单ID不能为空") } var outboundOrder models.OutboundOrder if err := tx.Where("id = ? AND is_del = 0", req.OutboundOrderID).First(&outboundOrder).Error; err != nil { return fmt.Errorf("查询出库单失败: %v", err) } if outboundOrder.Status != constant.OutboundStatusCreated { return fmt.Errorf("出库单[%s]状态不正确,当前状态: %s,只有已创建状态的出库单才能创建波次", outboundOrder.OutNo, getOutboundStatusText(outboundOrder.Status)) } if outboundOrder.WaveTaskID > 0 { var existingWave models.WaveHeader if err := tx.Where("id = ?", outboundOrder.WaveTaskID).First(&existingWave).Error; err == nil { return fmt.Errorf("出库单[%s]已存在波次任务,波次号: %s", outboundOrder.OutNo, existingWave.WaveNo) } } var outboundItems []models.OutboundOrderItem if err := tx.Where("out_order_id = ? AND is_del = 0", req.OutboundOrderID).Find(&outboundItems).Error; err != nil { return fmt.Errorf("查询出库单明细失败: %v", err) } if len(outboundItems) == 0 { return fmt.Errorf("出库单没有明细数据") } waveNo, err := s.generateWaveNo(constant.DirectionOutbound, databaseConn) if err != nil { return fmt.Errorf("生成波次号失败: %v", err) } waveHeader, err := s.createWaveHeader(tx, waveNo, constant.DirectionOutbound, outboundOrder.WarehouseID, outboundOrder.ID, creator, creatorID) if err != nil { return err } waveID = waveHeader.ID return nil }) if err != nil { return 0, err } return waveID, nil } // ReleaseOutboundWave 提交出库波次,生成波次任务明细 func (s *ProcessService) ReleaseOutboundWave(req systemReq.WaveRequest, db ...*gorm.DB) (int64, string, error) { databaseConn := database.OptionalDB(db...) var waveID int64 var waveNo string err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { var waveHeader models.WaveHeader if err := tx.Where("id = ? AND is_del = 0", req.WaveID).First(&waveHeader).Error; err != nil { return fmt.Errorf("波次不存在: %v", err) } if waveHeader.Status != constant.WaveStatusCreated { return fmt.Errorf("波次状态不正确,当前状态: %s", getWaveStatusText(waveHeader.Status)) } if waveHeader.Direction != constant.DirectionOutbound { return fmt.Errorf("该波次不是出库波次") } if req.RelatedOrderID == 0 { return fmt.Errorf("出库单ID不能为空") } var outboundOrder models.OutboundOrder if err := tx.Where("id = ? AND is_del = 0", req.RelatedOrderID).First(&outboundOrder).Error; err != nil { return fmt.Errorf("出库单不存在: %v", err) } if outboundOrder.Status != constant.OutboundStatusCreated { return fmt.Errorf("出库单[%s]状态不正确,当前状态: %s,只有已创建状态的出库单才能生成波次", outboundOrder.OutNo, getOutboundStatusText(outboundOrder.Status)) } if outboundOrder.WaveTaskID > 0 { return fmt.Errorf("出库单[%s]已存在波次任务", outboundOrder.OutNo) } var outboundItems []models.OutboundOrderItem if err := tx.Where("out_order_id = ? AND is_del = 0", req.RelatedOrderID).Find(&outboundItems).Error; err != nil { return fmt.Errorf("查询出库单明细失败: %v", err) } if len(outboundItems) == 0 { return fmt.Errorf("出库单没有明细数据") } now := time.Now().Unix() for i := range outboundItems { item := &outboundItems[i] if item.BatchNo != "" && item.ProductionDate > 0 && item.ExpiryDate > 0 { continue } var inventory models.Inventory err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("warehouse_id = ? AND product_id = ? AND is_del = 0 AND quantity > 0 AND locked_quantity > 0", outboundOrder.WarehouseID, item.ProductID). Order("expiry_date ASC, created_at ASC"). First(&inventory).Error if err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("商品ID=%d在仓库ID=%d中无可用库存,请先入库", item.ProductID, outboundOrder.WarehouseID) } return fmt.Errorf("查询库存失败: %v", err) } if item.BatchNo == "" { item.BatchNo = inventory.BatchNo } if item.ProductionDate == 0 { item.ProductionDate = inventory.ProductionDate } if item.ExpiryDate == 0 { item.ExpiryDate = inventory.ExpiryDate } if err := tx.Model(item).Updates(map[string]interface{}{ "batch_no": item.BatchNo, "production_date": item.ProductionDate, "expiry_date": item.ExpiryDate, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新出库单明细批次信息失败: %v", err) } } productMap := make(map[int64]*WaveItemData) for _, item := range outboundItems { if existing, exists := productMap[item.ProductID]; exists { existing.Quantity += item.Quantity } else { productMap[item.ProductID] = &WaveItemData{ ProductID: item.ProductID, Quantity: item.Quantity, UnitPrice: item.UnitPrice, LocationID: item.LocationID, } } } if len(productMap) == 0 { return fmt.Errorf("出库单明细数据无效") } items := make([]WaveItemData, 0, len(productMap)) for _, itemData := range productMap { items = append(items, *itemData) } waveTask, err := s.createWaveTaskAndDetailsForOutbound(tx, req.WaveID, constant.TaskTypePicking, items, outboundItems, req.Assignee, req.AssigneeId, 0, 0, 0) if err != nil { return err } if err := tx.Model(&models.OutboundOrder{}).Where("id = ?", outboundOrder.ID).Updates(map[string]interface{}{ "wave_task_id": waveTask.ID, "status": constant.OutboundStatusCreated, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新出库单状态失败: %v", err) } if err := s.updateWaveStatusToReleased(tx, req.WaveID); err != nil { return fmt.Errorf("更新波次状态失败: %v", err) } waveID = waveHeader.ID waveNo = waveHeader.WaveNo return nil }) if err != nil { return 0, "", err } return waveID, waveNo, nil } // BindOutboundWave 绑定出库波次 func (s *ProcessService) BindOutboundWave(req systemReq.BindWaveRequest, db ...*gorm.DB) (int64, int64, string, error) { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() var outboundOrderID int64 var waveTaskID int64 var waveTaskBatchNo string err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { var waveHeader models.WaveHeader if err := tx.Where("wave_no = ? AND is_del = 0", req.WaveNo).First(&waveHeader).Error; err != nil { return fmt.Errorf("波次不存在: %v", err) } if waveHeader.Direction != constant.DirectionOutbound { return fmt.Errorf("该波次不是出库波次") } var waveTask models.WaveTask if err := tx.Where("wave_id = ? AND is_del = 0", waveHeader.ID).First(&waveTask).Error; err != nil { return fmt.Errorf("波次任务不存在: %v", err) } waveTaskID = waveTask.ID if waveTask.Type != constant.TaskTypePicking { return fmt.Errorf("该任务不是出库拣货任务") } if waveTask.Status != constant.WaveStatusCreated { return fmt.Errorf("波次任务状态不正确,当前状态: %s,只有已创建状态才能绑定出库单", getWaveStatusText(waveTask.Status)) } if waveHeader.RelatedOrderID == 0 { return fmt.Errorf("波次未关联出库单") } var outboundOrder models.OutboundOrder if err := tx.Where("id = ? AND is_del = 0", waveHeader.RelatedOrderID).First(&outboundOrder).Error; err != nil { return fmt.Errorf("出库单不存在: %v", err) } outboundOrderID = outboundOrder.ID if outboundOrder.Status != constant.OutboundStatusCreated { return fmt.Errorf("出库单[%s]状态不正确,当前状态: %s,只有已创建状态才能绑定", outboundOrder.OutNo, getOutboundStatusText(outboundOrder.Status)) } if err := tx.Model(&models.WaveTask{}).Where("id = ?", waveTaskID).Updates(map[string]interface{}{ "assignee": req.Operator, "assignee_id": req.OperatorID, }).Error; err != nil { return fmt.Errorf("绑定指派人失败: %v", err) } var waveTaskDetails []models.WaveTaskDetail if err := tx.Where("wave_task_id = ? AND is_del = 0", waveTask.ID).Find(&waveTaskDetails).Error; err != nil { return fmt.Errorf("查询波次任务明细失败: %v", err) } if len(waveTaskDetails) == 0 { return fmt.Errorf("波次任务没有明细数据") } waveTaskBatchNo = waveTaskDetails[0].BatchNo if err := tx.Model(&models.OutboundOrder{}).Where("id = ?", outboundOrder.ID).Updates(map[string]interface{}{ "status": constant.OutboundStatusPicking, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新出库单状态和操作员失败: %v", err) } if err := s.updateWaveTaskToPicking(tx, waveTask.ID); err != nil { return fmt.Errorf("更新波次任务状态失败: %v", err) } return nil }) if err != nil { return 0, 0, "", err } return outboundOrderID, waveTaskID, waveTaskBatchNo, nil } // GetOutboundDetail 获取出库单详情 func (s *ProcessService) GetOutboundDetail(outboundOrderID int64, db ...*gorm.DB) (interface{}, error) { databaseConn := database.OptionalDB(db...) var outboundOrder models.OutboundOrder if err := databaseConn.Where("id = ? AND is_del = 0", outboundOrderID).First(&outboundOrder).Error; err != nil { return nil, fmt.Errorf("出库单不存在: %v", err) } var outboundOrderItem []models.OutboundOrderItem if err := databaseConn.Where("out_order_id = ? AND is_del = 0", outboundOrderID).Find(&outboundOrderItem).Error; err != nil { return nil, fmt.Errorf("查询出库单明细失败: %v", err) } if len(outboundOrderItem) == 0 { result := map[string]interface{}{ "out_order_id": outboundOrder.ID, "out_no": outboundOrder.OutNo, "status": outboundOrder.Status, "warehouse_id": outboundOrder.WarehouseID, "customer_id": outboundOrder.CustomerID, "operator": outboundOrder.Operator, "remark": outboundOrder.Remark, "items": []map[string]interface{}{}, } return result, nil } productIDs := make([]int64, 0, len(outboundOrderItem)) locationIDs := make([]int64, 0) for _, item := range outboundOrderItem { productIDs = append(productIDs, item.ProductID) if item.LocationID > 0 { locationIDs = append(locationIDs, item.LocationID) } } var products []models.Product if err := databaseConn.Where("id IN ? AND is_del = 0", productIDs).Find(&products).Error; err != nil { return nil, fmt.Errorf("查询商品失败: %v", err) } productMap := make(map[int64]models.Product) for _, p := range products { productMap[p.ID] = p } locationMap := make(map[int64]models.Location) if len(locationIDs) > 0 { var locations []models.Location if err := databaseConn.Where("id IN ? AND is_del = 0", locationIDs).Find(&locations).Error; err != nil { return nil, fmt.Errorf("查询库位失败: %v", err) } for _, l := range locations { locationMap[l.ID] = l } } items := make([]map[string]interface{}, 0, len(outboundOrderItem)) for _, item := range outboundOrderItem { product, exists := productMap[item.ProductID] if !exists { continue } locationCode := "" if item.LocationID > 0 { if location, exists := locationMap[item.LocationID]; exists { locationCode = location.Code } } items = append(items, map[string]interface{}{ "id": item.ID, "product_id": item.ProductID, "product_name": product.Name, "product_code": product.Barcode, "location_id": item.LocationID, "location_code": locationCode, "batch_no": item.BatchNo, "production_date": item.ProductionDate, "expiry_date": item.ExpiryDate, "quantity": item.Quantity, }) } result := map[string]interface{}{ "out_order_id": outboundOrder.ID, "out_no": outboundOrder.OutNo, "status": outboundOrder.Status, "warehouse_id": outboundOrder.WarehouseID, "customer_id": outboundOrder.CustomerID, "operator": outboundOrder.Operator, "remark": outboundOrder.Remark, "items": items, } return result, nil } // CreateShippingOrder 基于多个出库单创建发货单 func (s *ProcessService) CreateShippingOrder(req systemReq.CreateShippingOrderRequest, operator string, operatorID int64, db ...*gorm.DB) (int64, string, error) { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() var shippingOrderID int64 var shippingNo string err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { if len(req.OutboundOrderIDs) == 0 { return fmt.Errorf("出库单列表不能为空") } var outboundOrders []models.OutboundOrder if err := tx.Where("id IN ? AND is_del = 0", req.OutboundOrderIDs).Find(&outboundOrders).Error; err != nil { return fmt.Errorf("查询出库单失败: %v", err) } if len(outboundOrders) != len(req.OutboundOrderIDs) { return fmt.Errorf("部分出库单不存在") } for _, order := range outboundOrders { if order.Status != constant.OutboundStatusCompleted { return fmt.Errorf("出库单[%s]状态不正确,当前状态: %s,只有已完成状态的出库单才能创建发货单", order.OutNo, getOutboundStatusText(order.Status)) } } /*customerID := outboundOrders[0].CustomerID warehouseID := outboundOrders[0].WarehouseID for i, order := range outboundOrders[1:] { if order.CustomerID != customerID { return fmt.Errorf("所有出库单必须属于同一个客户,订单[%s]与第一个订单客户不一致", outboundOrders[i+1].OutNo) } if order.WarehouseID != warehouseID { return fmt.Errorf("所有出库单必须属于同一个仓库,订单[%s]与第一个订单仓库不一致", outboundOrders[i+1].OutNo) } }*/ customerID := outboundOrders[0].CustomerID for i, order := range outboundOrders[1:] { if order.CustomerID != customerID { return fmt.Errorf("所有出库单必须属于同一个客户,订单[%s]与第一个订单客户不一致", outboundOrders[i+1].OutNo) } } for _, order := range outboundOrders { var existingShipping models.ShippingOrder if err := tx.Joins("JOIN shipping_order_item ON shipping_order_item.shipping_order_id = shipping_order.id"). Where("shipping_order_item.outbound_order_item_id IN (SELECT id FROM outbound_order_item WHERE out_order_id = ?)", order.ID). Where("shipping_order.status NOT IN ?", []int8{constant.ShippingStatusCancelled}). First(&existingShipping).Error; err == nil { return fmt.Errorf("出库单[%s]已存在未取消的发货单[%s]", order.OutNo, existingShipping.ShippingNo) } } var allOutboundItems []models.OutboundOrderItem if err := tx.Where("out_order_id IN ? AND is_del = 0", req.OutboundOrderIDs).Find(&allOutboundItems).Error; err != nil { return fmt.Errorf("查询出库单明细失败: %v", err) } if len(allOutboundItems) == 0 { return fmt.Errorf("选中的出库单没有明细数据") } shippingNo = utils.GenerateShippingNo() shippingOrder := models.ShippingOrder{ ShippingNo: shippingNo, CustomerID: customerID, Status: constant.ShippingStatusPending, ExpectedArriveTime: req.ExpectedArriveTime, Operator: operator, CreatedAt: now, UpdatedAt: &now, Remark: req.Remark, } if err := tx.Create(&shippingOrder).Error; err != nil { return fmt.Errorf("创建发货单失败: %v", err) } shippingOrderID = shippingOrder.ID shippingItems := make([]models.ShippingOrderItem, 0, len(allOutboundItems)) for _, item := range allOutboundItems { shippingItems = append(shippingItems, models.ShippingOrderItem{ ShippingOrderID: shippingOrder.ID, OutboundOrderItemID: &item.ID, Quantity: item.Quantity, CreatedAt: now, UpdatedAt: now, }) } if err := tx.Create(&shippingItems).Error; err != nil { return fmt.Errorf("创建发货单明细失败: %v", err) } for _, order := range outboundOrders { if err := tx.Model(&models.OutboundOrder{}).Where("id = ?", order.ID).Updates(map[string]interface{}{ "status": constant.OutboundStatusShipping, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新出库单[%s]状态失败: %v", order.OutNo, err) } } return nil }) if err != nil { return 0, "", err } return shippingOrderID, shippingNo, nil } // UpdateShippingLogistics 更新发货单物流信息并回填销售订单明细 func (s *ProcessService) UpdateShippingLogistics(req systemReq.UpdateShippingLogisticsRequest, operatorID int64, db ...*gorm.DB) error { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { if req.SalesOrderItemID <= 0 { return utils.NewError("销售订单明细ID无效") } if req.LogisticsCompany == "" || req.LogisticsNo == "" { return utils.NewError("物流公司和物流单号不能为空") } updateResult := tx.Model(&models.SalesOrderItem{}). Where("id = ? AND is_del = ?", req.SalesOrderItemID, 0). Updates(map[string]interface{}{ "shipped_quantity": 1, "logistics_company": req.LogisticsCompany, "logistics_no": req.LogisticsNo, "updated_at": now, }) if updateResult.Error != nil { return utils.NewError("更新销售订单明细物流信息失败") } if updateResult.RowsAffected == 0 { return utils.NewError("销售订单明细不存在或已删除") } var salesOrderItem models.SalesOrderItem if err := tx.Where("id = ? AND is_del = ?", req.SalesOrderItemID, 0).First(&salesOrderItem).Error; err != nil { return utils.NewError("查询销售订单明细失败") } if salesOrderItem.SalesOrderID > 0 { if err := tx.Model(&models.SalesOrder{}). Where("id = ? AND is_del = ?", salesOrderItem.SalesOrderID, 0). Update("status", constant.SalesStatusShipped).Error; err != nil { return utils.NewError("更新销售订单状态失败") } } var shippingItems []models.ShippingOrderItem if err := tx.Where("shipping_order_id = ? AND is_del = 0", req.ShippingOrderID).Find(&shippingItems).Error; err != nil { return utils.NewError("查询发货单明细失败") } if len(shippingItems) == 0 { return utils.NewError("发货单明细不存在") } outboundOrderItemIDs := make([]int64, 0, len(shippingItems)) for _, item := range shippingItems { if item.OutboundOrderItemID != nil && *item.OutboundOrderItemID > 0 { outboundOrderItemIDs = append(outboundOrderItemIDs, *item.OutboundOrderItemID) } } if len(outboundOrderItemIDs) == 0 { return utils.NewError("发货单未关联出库单明细") } var outboundItems []models.OutboundOrderItem if err := tx.Where("id IN ? AND is_del = 0", outboundOrderItemIDs).Find(&outboundItems).Error; err != nil { return utils.NewError("查询出库单明细失败") } salesOrderItemIDs := make([]int64, 0, len(outboundItems)) for _, item := range outboundItems { if item.SalesOrderID > 0 { salesOrderItemIDs = append(salesOrderItemIDs, item.SalesOrderID) } } if len(salesOrderItemIDs) > 0 { var salesItems []models.SalesOrderItem if err := tx.Where("id IN ? AND is_del = 0", salesOrderItemIDs).Find(&salesItems).Error; err != nil { return utils.NewError("查询销售订单明细失败") } allShipped := true for _, item := range salesItems { if item.ShippedQuantity == 0 { allShipped = false break } } if allShipped { outboundOrderIDs := make([]int64, 0, len(outboundItems)) for _, item := range outboundItems { if item.OutOrderID > 0 { outboundOrderIDs = append(outboundOrderIDs, item.OutOrderID) } } if len(outboundOrderIDs) > 0 { if err := tx.Model(&models.OutboundOrder{}). Where("id IN ? AND is_del = 0", outboundOrderIDs). Updates(map[string]interface{}{ "status": constant.OutboundStatusShipped, "updated_at": now, }).Error; err != nil { return utils.NewError("更新出库单状态失败") } } if err := tx.Model(&models.ShippingOrder{}). Where("id = ? AND is_del = ?", req.ShippingOrderID, 0). Updates(map[string]interface{}{ "status": constant.ShippingStatusShipped, "operator": operatorID, "updated_at": now, }).Error; err != nil { return utils.NewError("更新发货单状态失败") } } } return nil }) if err != nil { return err } return nil } // AdjustInventory 盘库调整(加库存/减库存) func (s *ProcessService) AdjustInventory(req systemReq.StockCheckAdjustRequest, operator string, operatorID int64, db ...*gorm.DB) error { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() return executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { var product models.Product if err := tx.Where("id = ? AND is_del = 0", req.ProductID).First(&product).Error; err != nil { return fmt.Errorf("商品不存在: %v", err) } if product.Status != 1 { return fmt.Errorf("商品%s已停用", product.Name) } var warehouse models.Warehouse if err := tx.Where("id = ? AND is_del = 0", req.WarehouseID).First(&warehouse).Error; err != nil { return fmt.Errorf("仓库不存在: %v", err) } var location models.Location if err := tx.Where("id = ? AND is_del = 0", req.LocationID).First(&location).Error; err != nil { return fmt.Errorf("库位不存在: %v", err) } if location.Status != 1 { return fmt.Errorf("库位%s不可用", location.Code) } if location.WarehouseID != req.WarehouseID { return fmt.Errorf("库位%s不属于该仓库", location.Code) } checkNo := utils.GenerateStockCheckNo() inventoryKey := inventoryKey{ warehouseID: req.WarehouseID, productID: req.ProductID, batchNo: req.BatchNo, productionDate: 0, expiryDate: 0, } var changeQuantity int64 var remark string if req.AdjustType == 1 { changeQuantity = req.Quantity remark = fmt.Sprintf("盘库加库存:%s", req.Remark) } else { changeQuantity = -req.Quantity remark = fmt.Sprintf("盘库减库存:%s", req.Remark) } // 获取当前系统库存数量 var currentInventory models.Inventory if err := tx.Where("warehouse_id = ? AND product_id = ? AND batch_no = ? AND is_del = 0", req.WarehouseID, req.ProductID, req.BatchNo).First(¤tInventory).Error; err != nil { if err != gorm.ErrRecordNotFound { return fmt.Errorf("查询库存失败: %v", err) } } systemQuantity := currentInventory.Quantity // 创建盘库单主表记录 stockCheck := models.StockCheck{ CheckNo: checkNo, WarehouseID: req.WarehouseID, CheckType: 2, // 2=抽盘 Status: constant.InventoryCheckStatusCompleted, // 3=已完成 TotalItems: 1, CheckedItems: 1, TotalQuantity: systemQuantity, ActualQuantity: systemQuantity + changeQuantity, Operator: operator, OperatorID: operatorID, Remark: req.Remark, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&stockCheck).Error; err != nil { return fmt.Errorf("创建盘库单失败: %v", err) } // 创建盘库单明细表记录 stockCheckItem := models.StockCheckItem{ StockCheckID: stockCheck.ID, ProductID: req.ProductID, LocationID: req.LocationID, BatchNo: req.BatchNo, ProductionDate: 0, ExpiryDate: 0, SystemQuantity: systemQuantity, ActualQuantity: systemQuantity + changeQuantity, DifferenceQuantity: changeQuantity, Status: 2, // 2=已盘点 CheckOperator: operator, CheckOperatorID: operatorID, CheckTime: now, Remark: req.Remark, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&stockCheckItem).Error; err != nil { return fmt.Errorf("创建盘库单明细失败: %v", err) } log, err := s.processInventoryOperationForAdjustment(tx, inventoryKey, req.LocationID, changeQuantity, checkNo, operator, operatorID, now, remark) if err != nil { return fmt.Errorf("处理库存汇总调整失败: %v", err) } if err := s.processInventoryDetailOperationForAdjustment(tx, inventoryKey, req.LocationID, changeQuantity, now); err != nil { return fmt.Errorf("处理库存明细调整失败: %v", err) } if log != nil { if err := tx.Create(log).Error; err != nil { return fmt.Errorf("创建库存流水失败: %v", err) } } return nil }) } // ReturnInventory 盘库退货(基于销售订单明细退货) func (s *ProcessService) ReturnInventory(req systemReq.StockCheckReturnRequest, operator string, operatorID int64, db ...*gorm.DB) error { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() return executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { var salesOrder models.SalesOrder if err := tx.Where("id = ? AND is_del = 0", req.SalesOrderID).First(&salesOrder).Error; err != nil { return fmt.Errorf("销售订单不存在: %v", err) } if salesOrder.Status == constant.SalesStatusCancelled { return fmt.Errorf("销售订单[%s]已取消,无法退货", salesOrder.SoNo) } var salesOrderItem models.SalesOrderItem if err := tx.Where("id = ? AND sales_order_id = ? AND is_del = 0", req.SalesOrderItemID, req.SalesOrderID).First(&salesOrderItem).Error; err != nil { return fmt.Errorf("销售订单明细不存在: %v", err) } if salesOrderItem.ShippedQuantity <= 0 { return fmt.Errorf("销售订单明细没有已发货数量,无法退货") } if salesOrderItem.ShippedQuantity > 1 { return fmt.Errorf("销售订单明细已发货数量异常,当前为%d,预期为1", salesOrderItem.ShippedQuantity) } productID := salesOrderItem.ProductID warehouseID := salesOrder.WarehouseID var product models.Product if err := tx.Where("id = ? AND is_del = 0", productID).First(&product).Error; err != nil { return fmt.Errorf("商品不存在: %v", err) } if product.Status != 1 { return fmt.Errorf("商品[%s]已停用", product.Name) } var inventoryDetail models.InventoryDetail if err := tx.Where("warehouse_id = ? AND product_id = ? AND quantity > 0 AND is_del = 0", warehouseID, productID).Order("created_at ASC").First(&inventoryDetail).Error; err != nil { return fmt.Errorf("未找到可用库存: %v", err) } locationID := inventoryDetail.LocationID batchNo := inventoryDetail.BatchNo var location models.Location if err := tx.Where("id = ? AND is_del = 0", locationID).First(&location).Error; err != nil { return fmt.Errorf("库位不存在: %v", err) } if location.Status != 1 { return fmt.Errorf("库位[%s]不可用", location.Code) } if location.WarehouseID != warehouseID { return fmt.Errorf("库位[%s]不属于该仓库", location.Code) } returnNo := utils.GenerateReturnNo() returnQuantity := int64(1) inventoryKey := inventoryKey{ warehouseID: warehouseID, productID: productID, batchNo: batchNo, productionDate: 0, expiryDate: 0, } var currentInventory models.Inventory if err := tx.Where("warehouse_id = ? AND product_id = ? AND batch_no = ? AND is_del = 0", warehouseID, productID, batchNo).First(¤tInventory).Error; err != nil { if err != gorm.ErrRecordNotFound { return fmt.Errorf("查询库存失败: %v", err) } } systemQuantity := currentInventory.Quantity actualQuantity := systemQuantity + returnQuantity stockCheck := models.StockCheck{ CheckNo: returnNo, WarehouseID: warehouseID, CheckType: 2, Status: constant.InventoryCheckStatusCompleted, TotalItems: 1, CheckedItems: 1, TotalQuantity: systemQuantity, ActualQuantity: actualQuantity, Operator: operator, OperatorID: operatorID, Remark: req.Remark, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&stockCheck).Error; err != nil { return fmt.Errorf("创建盘库单失败: %v", err) } stockCheckItem := models.StockCheckItem{ StockCheckID: stockCheck.ID, ProductID: productID, LocationID: locationID, BatchNo: batchNo, ProductionDate: 0, ExpiryDate: 0, SystemQuantity: systemQuantity, ActualQuantity: actualQuantity, DifferenceQuantity: returnQuantity, Status: constant.InventoryCheckStatusInProgress, CheckOperator: operator, CheckOperatorID: operatorID, CheckTime: now, Remark: req.Remark, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&stockCheckItem).Error; err != nil { return fmt.Errorf("创建盘库单明细失败: %v", err) } log, err := s.processInventoryOperationForAdjustment(tx, inventoryKey, locationID, returnQuantity, returnNo, operator, operatorID, now, fmt.Sprintf("盘库退货:%s", req.Remark)) if err != nil { return fmt.Errorf("处理库存汇总失败: %v", err) } if err := s.processInventoryDetailOperationForAdjustment(tx, inventoryKey, locationID, returnQuantity, now); err != nil { return fmt.Errorf("处理库存明细失败: %v", err) } if log != nil { if err := tx.Create(log).Error; err != nil { return fmt.Errorf("创建库存流水失败: %v", err) } } newShippedQuantity := salesOrderItem.ShippedQuantity - returnQuantity if err := tx.Model(&models.SalesOrderItem{}).Where("id = ?", salesOrderItem.ID).Updates(map[string]interface{}{ "shipped_quantity": newShippedQuantity, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新销售订单明细已发货数量失败: %v", err) } if newShippedQuantity == 0 { var remainingItems []models.SalesOrderItem if err := tx.Where("sales_order_id = ? AND is_del = 0", req.SalesOrderID).Order("id ASC").Find(&remainingItems).Error; err != nil { return fmt.Errorf("查询销售订单其他明细失败: %v", err) } allReturned := true for _, item := range remainingItems { if item.ShippedQuantity > 0 { allReturned = false break } } if allReturned { if err := tx.Model(&models.SalesOrder{}).Where("id = ?", req.SalesOrderID).Updates(map[string]interface{}{ "status": constant.SalesStatusConfirmed, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新销售订单状态失败: %v", err) } } } else { var allItems []models.SalesOrderItem if err := tx.Where("sales_order_id = ? AND is_del = 0", req.SalesOrderID).Order("id ASC").Find(&allItems).Error; err != nil { return fmt.Errorf("查询销售订单所有明细失败: %v", err) } allFullyReturned := true for _, item := range allItems { if item.ShippedQuantity > 0 { allFullyReturned = false break } } if !allFullyReturned && salesOrder.Status == constant.SalesStatusShipped { if err := tx.Model(&models.SalesOrder{}).Where("id = ?", req.SalesOrderID).Updates(map[string]interface{}{ "status": constant.SalesStatusPicking, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新销售订单状态失败: %v", err) } } } return nil }) } // ChangeLocation 出库单切换库位 func (s *ProcessService) ChangeLocation(req systemReq.ChangeLocationRequest, operator string, operatorID int64, db ...*gorm.DB) error { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() // 查询出库单明细 var item models.OutboundOrderItem if err := databaseConn.Where("id = ? AND is_del = 0", req.OutOrderItemID).First(&item).Error; err != nil { return utils.NewError("出库单明细不存在") } // 查询出库单获取仓库ID var outOrder models.OutboundOrder if err := databaseConn.Where("id = ? AND is_del = 0", item.OutOrderID).First(&outOrder).Error; err != nil { return utils.NewError("出库单不存在") } oldLocationID := item.LocationID // 自动查找同一仓库下、同一商品有可用库存的库位(排除当前库位,按库存量降序优先取库存最多的) var inventoryDetail models.InventoryDetail if err := databaseConn.Where("warehouse_id = ? AND product_id = ? AND location_id != ? AND is_del = 0 AND quantity > 0", outOrder.WarehouseID, item.ProductID, oldLocationID). Order("quantity DESC"). First(&inventoryDetail).Error; err != nil { return utils.ErrNoAvailableLocation } newLocationID := inventoryDetail.LocationID // 更新出库单明细的库位 if err := databaseConn.Model(&item).Updates(map[string]interface{}{ "location_id": newLocationID, "updated_at": now, }).Error; err != nil { return utils.NewError("更新出库单明细库位失败: " + err.Error()) } // 记录库位变更日志 log := models.OutboundOrderLocationLog{ OutOrderID: item.OutOrderID, OutOrderItemID: item.ID, ProductID: item.ProductID, OldLocationID: oldLocationID, NewLocationID: newLocationID, BatchNo: item.BatchNo, Operator: operator, OperatorID: operatorID, Remark: req.Remark, CreatedAt: now, } if err := databaseConn.Create(&log).Error; err != nil { return utils.NewError("记录库位变更日志失败: " + err.Error()) } return nil } // 创建波次 func (s *ProcessService) createWaveHeader(tx *gorm.DB, waveNo string, direction int8, warehouseID, relatedOrderID int64, creator string, creatorID int64) (*models.WaveHeader, error) { now := time.Now().Unix() waveHeader := models.WaveHeader{ WaveNo: waveNo, Direction: direction, Type: constant.WaveNormal, WarehouseID: warehouseID, RelatedOrderID: relatedOrderID, Status: constant.WaveStatusCreated, Creator: creator, CreatorID: creatorID, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&waveHeader).Error; err != nil { return nil, fmt.Errorf("创建波次主表失败: %v", err) } return &waveHeader, nil } // 创建波次任务明细 func (s *ProcessService) createWaveTaskDetails(tx *gorm.DB, waveTaskID int64, items []WaveItemData, batchNo string) error { now := time.Now().Unix() details := make([]models.WaveTaskDetail, 0, len(items)) for _, item := range items { details = append(details, models.WaveTaskDetail{ WaveTaskID: waveTaskID, ProductID: item.ProductID, LocationID: 0, BatchNo: batchNo, PlannedQuantity: item.Quantity, ActualQuantity: 0, Status: constant.WaveStatusCreated, CreatedAt: now, UpdatedAt: now, IsDel: 0, }) } if err := tx.Create(&details).Error; err != nil { return fmt.Errorf("创建波次任务明细失败: %v", err) } return nil } // 创建波次任务和明细 func (s *ProcessService) createWaveTaskAndDetails(tx *gorm.DB, waveID int64, taskType int8, items []WaveItemData, assignee string, assigneeID, carId, carCode, carCapacity int64) (*models.WaveTask, error) { now := time.Now().Unix() taskNo := utils.GenerateTaskNo() taskTd := utils.GenerateTaskDetailNo() waveTask := models.WaveTask{ WaveID: waveID, CarId: carId, CarCode: carCode, CarCapacity: carCapacity, TaskNo: taskNo, Type: taskType, Assignee: assignee, AssigneeID: assigneeID, Status: constant.WaveStatusCreated, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&waveTask).Error; err != nil { return nil, fmt.Errorf("创建波次任务失败: %v", err) } details := make([]models.WaveTaskDetail, 0, len(items)) for _, item := range items { details = append(details, models.WaveTaskDetail{ WaveTaskID: waveTask.ID, ProductID: item.ProductID, LocationID: item.LocationID, BatchNo: taskTd, PlannedQuantity: item.Quantity, ActualQuantity: 0, Status: constant.WaveStatusCreated, CreatedAt: now, UpdatedAt: now, IsDel: 0, }) } if err := tx.Create(&details).Error; err != nil { return nil, fmt.Errorf("创建波次任务明细失败: %v", err) } return &waveTask, nil } // createWaveTaskAndDetailsForOutbound 创建出库波次任务和明细(使用入库批次号) func (s *ProcessService) createWaveTaskAndDetailsForOutbound(tx *gorm.DB, waveID int64, taskType int8, items []WaveItemData, outboundItems []models.OutboundOrderItem, assignee string, assigneeID, carId, carCode, carCapacity int64) (*models.WaveTask, error) { now := time.Now().Unix() taskNo := utils.GenerateTaskNo() waveTask := models.WaveTask{ WaveID: waveID, CarId: carId, CarCode: carCode, CarCapacity: carCapacity, TaskNo: taskNo, Type: taskType, Assignee: assignee, AssigneeID: assigneeID, Status: constant.WaveStatusCreated, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&waveTask).Error; err != nil { return nil, fmt.Errorf("创建波次任务失败: %v", err) } // 构建出库单明细映射,key为ID,用于快速获取批次号等信息 outboundItemMap := make(map[int64]models.OutboundOrderItem) for _, item := range outboundItems { outboundItemMap[item.ID] = item } details := make([]models.WaveTaskDetail, 0, len(outboundItems)) for _, outboundItem := range outboundItems { batchNo := outboundItem.BatchNo if batchNo == "" { return nil, fmt.Errorf("出库单明细ID=%d的批次号为空,无法创建出库波次", outboundItem.ID) } // 获取对应的销售订单明细的 allocated_quantity plannedQuantity := int64(0) if outboundItem.SalesOrderID > 0 { var salesOrderItem models.SalesOrderItem if err := tx.Where("sales_order_id = ? AND product_id = ? AND is_del = 0", outboundItem.SalesOrderID, outboundItem.ProductID).First(&salesOrderItem).Error; err == nil { plannedQuantity = salesOrderItem.AllocatedQuantity } } // 如果plannedQuantity为0,跳过该明细 if plannedQuantity <= 0 { continue } details = append(details, models.WaveTaskDetail{ WaveTaskID: waveTask.ID, ProductID: outboundItem.ProductID, LocationID: outboundItem.LocationID, BatchNo: batchNo, PlannedQuantity: plannedQuantity, ActualQuantity: 0, Status: constant.WaveStatusCreated, CreatedAt: now, UpdatedAt: now, IsDel: 0, }) } if len(details) == 0 { return nil, fmt.Errorf("没有有效的出库明细可以创建波次任务") } if err := tx.Create(&details).Error; err != nil { return nil, fmt.Errorf("创建波次任务明细失败: %v", err) } return &waveTask, nil } // 修改波次状态并提交 func (s *ProcessService) updateWaveStatusToReleased(tx *gorm.DB, waveID int64) error { now := time.Now().Unix() return tx.Model(&models.WaveHeader{}).Where("id = ?", waveID).Updates(map[string]interface{}{ "status": constant.WaveStatusReleased, "updated_at": now, }).Error } // 修改波次任务状态并开始拣货 func (s *ProcessService) updateWaveTaskToPicking(tx *gorm.DB, waveTaskID int64) error { now := time.Now().Unix() return tx.Model(&models.WaveTask{}).Where("id = ?", waveTaskID).Updates(map[string]interface{}{ "status": constant.WaveStatusPicking, "started_at": now, "updated_at": now, }).Error } // 处理库存操作(使用原子操作和行级锁保证并发安全) func (s *ProcessService) processInventoryOperation(tx *gorm.DB, opKey inventoryKey, locationID int64, quantity int64, changeType int8, orderNo string, operator string, operatorID int64, now int64) (*models.InventoryLog, error) { if changeType == constant.InventoryChangeInbound { var inventory models.Inventory err := tx.Debug(). Clauses(clause.Locking{Strength: "UPDATE"}). Where("warehouse_id = ? AND product_id = ? AND batch_no = ? AND production_date = ? AND expiry_date = ? AND is_del = 0", opKey.warehouseID, opKey.productID, opKey.batchNo, opKey.productionDate, opKey.expiryDate). First(&inventory).Error if err != nil { if err == gorm.ErrRecordNotFound { inventory = models.Inventory{ WarehouseID: opKey.warehouseID, ProductID: opKey.productID, BatchNo: opKey.batchNo, ProductionDate: opKey.productionDate, ExpiryDate: opKey.expiryDate, Quantity: quantity, LockedQuantity: 0, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&inventory).Error; err != nil { return nil, fmt.Errorf("创建库存记录失败: %v", err) } return &models.InventoryLog{ WarehouseID: opKey.warehouseID, LocationID: locationID, ProductID: opKey.productID, BatchNo: opKey.batchNo, ChangeType: changeType, ChangeQuantity: quantity, BeforeQuantity: 0, AfterQuantity: quantity, RelatedOrderType: constant.OrderTypeReceiving, RelatedOrderNo: orderNo, Operator: operator, OperatorID: operatorID, Remark: fmt.Sprintf("入库单%s首次入库", orderNo), CreatedAt: now, IsDel: 0, }, nil } return nil, fmt.Errorf("查询库存记录失败: %v", err) } beforeQuantity := inventory.Quantity result := tx.Model(&inventory). UpdateColumn("quantity", gorm.Expr("quantity + ?", quantity)). UpdateColumn("updated_at", now) if result.Error != nil { return nil, fmt.Errorf("更新库存记录失败: %v", result.Error) } if result.RowsAffected == 0 { return nil, errors.New("库存更新失败,请重试") } return &models.InventoryLog{ WarehouseID: opKey.warehouseID, LocationID: locationID, ProductID: opKey.productID, BatchNo: opKey.batchNo, ChangeType: changeType, ChangeQuantity: quantity, BeforeQuantity: beforeQuantity, AfterQuantity: beforeQuantity + quantity, RelatedOrderType: constant.OrderTypeReceiving, RelatedOrderNo: orderNo, Operator: operator, OperatorID: operatorID, Remark: fmt.Sprintf("入库单%s入库", orderNo), CreatedAt: now, IsDel: 0, }, nil } else if changeType == constant.InventoryChangeOutbound { var inventory models.Inventory err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("warehouse_id = ? AND product_id = ? AND production_date = ? AND expiry_date = ? AND is_del = 0", opKey.warehouseID, opKey.productID, opKey.productionDate, opKey.expiryDate). First(&inventory).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("库存不存在: 商品ID=%d", opKey.productID) } return nil, fmt.Errorf("查询库存记录失败: %v", err) } beforeQuantity := inventory.Quantity beforeLocked := inventory.LockedQuantity availableQty := inventory.Quantity - inventory.LockedQuantity if availableQty < 0 { return nil, fmt.Errorf("库存数据异常,可用数量为负数: %d", availableQty) } if inventory.Quantity < quantity { return nil, fmt.Errorf("库存不足,当前:%d, 需要:%d", inventory.Quantity, quantity) } actualUnlockQty := quantity if beforeLocked < quantity { actualUnlockQty = beforeLocked } result := tx.Model(&inventory). Where("quantity >= ?", quantity). UpdateColumns(map[string]interface{}{ "quantity": gorm.Expr("quantity - ?", quantity), "locked_quantity": gorm.Expr("GREATEST(locked_quantity - ?, 0)", actualUnlockQty), "updated_at": now, }) if result.Error != nil { return nil, fmt.Errorf("更新库存记录失败: %v", result.Error) } if result.RowsAffected == 0 { return nil, fmt.Errorf("库存不足或已被其他事务修改,请重试") } afterQuantity := beforeQuantity - quantity afterLocked := beforeLocked - actualUnlockQty if afterLocked < 0 { afterLocked = 0 } return &models.InventoryLog{ WarehouseID: opKey.warehouseID, LocationID: locationID, ProductID: opKey.productID, BatchNo: opKey.batchNo, ChangeType: changeType, ChangeQuantity: -quantity, BeforeQuantity: beforeQuantity, AfterQuantity: afterQuantity, RelatedOrderType: constant.OrderTypeSales, RelatedOrderNo: orderNo, Operator: operator, OperatorID: operatorID, Remark: fmt.Sprintf("出库单%s出库%d件,解锁库存:%d->%d", orderNo, quantity, beforeLocked, afterLocked), CreatedAt: now, IsDel: 0, }, nil } return nil, fmt.Errorf("未知的库存变更类型: %d", changeType) } // 处理库存明细操作(使用原子操作和行级锁保证并发安全) func (s *ProcessService) processInventoryDetailOperation(tx *gorm.DB, opKey inventoryKey, locationID int64, quantity int64, changeType int8, now int64) error { if changeType == constant.InventoryChangeInbound { var inventoryDetail models.InventoryDetail err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("warehouse_id = ? AND location_id = ? AND product_id = ? AND batch_no = ? AND production_date = ? AND expiry_date = ? AND is_del = 0", opKey.warehouseID, locationID, opKey.productID, opKey.batchNo, opKey.productionDate, opKey.expiryDate). First(&inventoryDetail).Error if err != nil { if err == gorm.ErrRecordNotFound { inventoryDetail = models.InventoryDetail{ WarehouseID: opKey.warehouseID, LocationID: locationID, ProductID: opKey.productID, BatchNo: opKey.batchNo, ProductionDate: opKey.productionDate, ExpiryDate: opKey.expiryDate, Quantity: quantity, LockedQuantity: 0, CreatedAt: now, UpdatedAt: now, IsDel: 0, } return tx.Create(&inventoryDetail).Error } return fmt.Errorf("查询库存明细失败: %v", err) } result := tx.Model(&inventoryDetail). UpdateColumn("quantity", gorm.Expr("quantity + ?", quantity)). UpdateColumn("updated_at", now) if result.Error != nil { return fmt.Errorf("更新库存明细失败: %v", result.Error) } if result.RowsAffected == 0 { return errors.New("库存明细更新失败,请重试") } return nil } else if changeType == constant.InventoryChangeOutbound { var inventoryDetail models.InventoryDetail err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("warehouse_id = ? AND location_id = ? AND product_id = ? AND batch_no = ? AND production_date = ? AND expiry_date = ? AND is_del = 0", opKey.warehouseID, locationID, opKey.productID, opKey.batchNo, opKey.productionDate, opKey.expiryDate). First(&inventoryDetail).Error if err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("库存明细不存在: 商品ID=%d, 库位ID=%d", opKey.productID, locationID) } return fmt.Errorf("查询库存明细失败: %v", err) } if inventoryDetail.Quantity < quantity { return fmt.Errorf("库位库存不足,可用:%d, 需要:%d", inventoryDetail.Quantity, quantity) } beforeLocked := inventoryDetail.LockedQuantity actualUnlockQty := quantity if beforeLocked < quantity { actualUnlockQty = beforeLocked } result := tx.Model(&inventoryDetail). Where("quantity >= ?", quantity). UpdateColumns(map[string]interface{}{ "quantity": gorm.Expr("quantity - ?", quantity), "locked_quantity": gorm.Expr("GREATEST(locked_quantity - ?, 0)", actualUnlockQty), "updated_at": now, }) if result.Error != nil { return fmt.Errorf("更新库存明细失败: %v", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("库位库存不足或已被其他事务修改,请重试") } return nil } return fmt.Errorf("未知的库存变更类型: %d", changeType) } // validateOutboundQuantity 验证出库数量是否合理 func (s *ProcessService) validateOutboundQuantity(tx *gorm.DB, outboundOrderID int64, items []orderItemInfo) error { var outboundOrder models.OutboundOrder if err := tx.Where("id = ? AND is_del = 0", outboundOrderID).First(&outboundOrder).Error; err != nil { return fmt.Errorf("查询出库单失败: %v", err) } // 如果没有关联波次任务,则无法校验 if outboundOrder.WaveTaskID == 0 { return fmt.Errorf("出库单未关联波次任务,无法校验出库数量") } // 查询波次任务明细获取计划出库数量 var waveTaskDetails []models.WaveTaskDetail if err := tx.Where("wave_task_id = ? AND is_del = 0", outboundOrder.WaveTaskID).Find(&waveTaskDetails).Error; err != nil { return fmt.Errorf("查询波次任务明细失败: %v", err) } // 构建商品ID到计划数量的映射 plannedQtyMap := make(map[int64]int64) for _, detail := range waveTaskDetails { plannedQtyMap[detail.ProductID] = detail.PlannedQuantity } for _, item := range items { plannedQty, exists := plannedQtyMap[item.productID] if !exists { return fmt.Errorf("商品ID=%d不在出库单中", item.productID) } if item.quantity <= 0 { return fmt.Errorf("商品ID=%d的出库数量必须大于0", item.productID) } // 检查实际出库数量是否超过计划出库数量 if item.quantity > plannedQty { return fmt.Errorf("商品ID=%d的实际出库数量(%d)不能超过计划出库数量(%d)", item.productID, item.quantity, plannedQty) } } return nil } // lockInventory 锁定库存(在创建出库波次时调用) func (s *ProcessService) lockInventory(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 quantity > locked_quantity", warehouseID, productID). Order("expiry_date ASC, created_at ASC"). Find(&inventories).Error; err != nil { return fmt.Errorf("查询可用库存失败: %v", err) } if len(inventories) == 0 { return fmt.Errorf("商品ID=%d在仓库ID=%d中无可用库存", productID, warehouseID) } remainingLock := quantity for i := range inventories { if remainingLock <= 0 { break } availableQty := inventories[i].Quantity - inventories[i].LockedQuantity if availableQty <= 0 { continue } lockQty := availableQty if lockQty > remainingLock { lockQty = remainingLock } result := tx.Model(&inventories[i]). Where("quantity - locked_quantity >= ?", lockQty). UpdateColumns(map[string]interface{}{ "locked_quantity": gorm.Expr("locked_quantity + ?", lockQty), "updated_at": now, }) if result.Error != nil { return fmt.Errorf("锁定库存失败: %v", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("库存已被其他事务修改,请重试") } var inventoryDetails []models.InventoryDetail if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("warehouse_id = ? AND product_id = ? AND is_del = 0 AND quantity > locked_quantity", warehouseID, productID). Order("expiry_date ASC, created_at ASC"). Find(&inventoryDetails).Error; err != nil { return fmt.Errorf("查询库存明细失败: %v", err) } detailRemainingLock := lockQty for j := range inventoryDetails { if detailRemainingLock <= 0 { break } detailAvailableQty := inventoryDetails[j].Quantity - inventoryDetails[j].LockedQuantity if detailAvailableQty <= 0 { continue } detailLockQty := detailAvailableQty if detailLockQty > detailRemainingLock { detailLockQty = detailRemainingLock } detailResult := tx.Model(&inventoryDetails[j]). Where("quantity - locked_quantity >= ?", detailLockQty). UpdateColumns(map[string]interface{}{ "locked_quantity": gorm.Expr("locked_quantity + ?", detailLockQty), "updated_at": now, }) if detailResult.Error != nil { return fmt.Errorf("锁定库存明细失败: %v", detailResult.Error) } if detailResult.RowsAffected == 0 { return fmt.Errorf("库存明细已被其他事务修改,请重试") } detailRemainingLock -= detailLockQty } if detailRemainingLock > 0 { return fmt.Errorf("库存明细可用数量不足,还需锁定:%d", detailRemainingLock) } remainingLock -= lockQty } if remainingLock > 0 { return fmt.Errorf("可用库存不足,还需锁定:%d", remainingLock) } return nil } // unlockInventory 解锁库存(在取消订单或波次时调用) 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("warehouse_id = ? AND product_id = ? AND is_del = 0 AND (locked_quantity > 0 or quality > 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 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("锁定库存已被其他事务修改,请重试") } 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 } // CancelOutboundWave 取消出库波次并释放锁定库存 func (s *ProcessService) CancelOutboundWave(waveID int64, operator string, operatorID int64, db ...*gorm.DB) error { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() return executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { var waveHeader models.WaveHeader if err := tx.Where("id = ? AND is_del = 0", waveID).First(&waveHeader).Error; err != nil { return fmt.Errorf("波次不存在: %v", err) } if waveHeader.Direction != constant.DirectionOutbound { return fmt.Errorf("该波次不是出库波次") } if waveHeader.Status == constant.WaveStatusCompleted || waveHeader.Status == constant.WaveStatusCancelled { return fmt.Errorf("波次状态不允许取消,当前状态: %s", getWaveStatusText(waveHeader.Status)) } var waveTask models.WaveTask if err := tx.Where("wave_id = ? AND is_del = 0", waveID).First(&waveTask).Error; err != nil { return fmt.Errorf("波次任务不存在: %v", err) } var waveTaskDetails []models.WaveTaskDetail if err := tx.Where("wave_task_id = ? AND is_del = 0", waveTask.ID).Find(&waveTaskDetails).Error; err != nil { return fmt.Errorf("查询波次任务明细失败: %v", err) } for _, detail := range waveTaskDetails { if detail.PlannedQuantity > 0 { if err := s.unlockInventory(tx, waveHeader.WarehouseID, detail.ProductID, detail.PlannedQuantity, now); err != nil { return fmt.Errorf("解锁库存失败[商品ID=%d]: %v", detail.ProductID, err) } } } if err := tx.Model(&models.WaveHeader{}).Where("id = ?", waveID).Updates(map[string]interface{}{ "status": constant.WaveStatusCancelled, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新波次状态失败: %v", err) } if err := tx.Model(&models.WaveTask{}).Where("wave_id = ?", waveID).Updates(map[string]interface{}{ "status": constant.WaveStatusCancelled, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新波次任务状态失败: %v", err) } if waveHeader.RelatedOrderID > 0 { var salesOrder models.SalesOrder if err := tx.Where("id = ? AND is_del = 0", waveHeader.RelatedOrderID).First(&salesOrder).Error; err == nil { if err := tx.Model(&models.SalesOrder{}).Where("id = ?", salesOrder.ID).Updates(map[string]interface{}{ "status": constant.SalesStatusConfirmed, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新销售订单状态失败: %v", err) } if err := tx.Model(&models.SalesOrderItem{}). Where("sales_order_id = ? AND is_del = 0", salesOrder.ID). Updates(map[string]interface{}{ "allocated_quantity": 0, "updated_at": now, }).Error; err != nil { return fmt.Errorf("重置销售订单明细已分配数量失败: %v", err) } } } return nil }) } // CancelSalesOrder 取消销售订单并释放锁定库存 func (s *ProcessService) CancelSalesOrder(orderID int64, operator string, operatorID int64, db ...*gorm.DB) error { databaseConn := database.OptionalDB(db...) now := time.Now().Unix() return executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { var salesOrder models.SalesOrder if err := tx.Where("id = ? AND is_del = 0", orderID).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("订单已取消") } var orderItems []models.SalesOrderItem if err := tx.Where("sales_order_id = ? AND is_del = 0", salesOrder.ID).Find(&orderItems).Error; err != nil { return fmt.Errorf("查询订单明细失败: %v", err) } for _, item := range orderItems { if item.AllocatedQuantity > 0 { if err := s.unlockInventory(tx, salesOrder.WarehouseID, item.ProductID, item.AllocatedQuantity, now); err != nil { return fmt.Errorf("解锁库存失败[商品ID=%d]: %v", item.ProductID, err) } } } if err := tx.Model(&models.SalesOrderItem{}). Where("sales_order_id = ? AND is_del = 0", salesOrder.ID). Updates(map[string]interface{}{ "allocated_quantity": 0, "updated_at": now, }).Error; err != nil { return fmt.Errorf("重置订单明细已分配数量失败: %v", err) } if err := tx.Model(&salesOrder).Updates(map[string]interface{}{ "status": constant.SalesStatusCancelled, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新销售订单状态失败: %v", err) } return nil }) } // CancelSalesOrder 取消销售订单并释放锁定库存 // ... existing code ... func (s *ProcessService) UnlockSalesOrderInventory(aboutID int64, associationOrderNo string, operator string, operatorID int64) (*systemRes.UnlockInventoryResponse, error) { databaseConn, err := database.GetTenantDB(aboutID) if err != nil { return nil, fmt.Errorf("获取数据库连接失败: %v", err) } now := time.Now().Unix() var unlockResp *systemRes.UnlockInventoryResponse 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 { 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 { return fmt.Errorf("查询订单明细失败: %v", err) } // 收集需要解锁的商品ID var productIDs []int64 for _, item := range orderItems { if item.AllocatedQuantity > 0 { productIDs = append(productIDs, item.ProductID) } } // 查询商品信息 var products []models.Product productMap := make(map[int64]models.Product) if len(productIDs) > 0 { if err := tx.Where("id IN ? AND is_del = 0", productIDs).Find(&products).Error; err != nil { return fmt.Errorf("查询商品信息失败: %v", err) } for _, p := range products { productMap[p.ID] = p } } // 解锁库存并构建响应 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 { return fmt.Errorf("解锁库存失败[商品ID=%d]: %v", item.ProductID, err) } productName := "" productCode := "" if p, ok := productMap[item.ProductID]; ok { productName = p.Name productCode = p.Barcode } responseItems = append(responseItems, systemRes.UnlockInventoryItemResponse{ ProductID: item.ProductID, ProductName: productName, ProductCode: productCode, UnlockedQuantity: item.AllocatedQuantity, }) } } if err := tx.Model(&models.SalesOrderItem{}). Where("sales_order_id = ? AND is_del = 0", salesOrder.ID). Updates(map[string]interface{}{ "allocated_quantity": 0, "updated_at": now, }).Error; err != nil { return fmt.Errorf("重置订单明细已分配数量失败: %v", err) } if err := tx.Model(&salesOrder).Updates(map[string]interface{}{ "status": constant.SalesStatusConfirmed, "updated_at": now, }).Error; err != nil { return fmt.Errorf("更新销售订单状态失败: %v", err) } unlockResp = &systemRes.UnlockInventoryResponse{ SoNo: salesOrder.SoNo, AssociationOrderNo: associationOrderNo, Items: responseItems, } return nil }) if err != nil { return nil, err } return unlockResp, nil } // processInventoryOperationForAdjustment 处理盘库调整的库存汇总操作 func (s *ProcessService) processInventoryOperationForAdjustment(tx *gorm.DB, opKey inventoryKey, locationID int64, quantity int64, orderNo string, operator string, operatorID int64, now int64, remark string) (*models.InventoryLog, error) { var inventory models.Inventory err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("warehouse_id = ? AND product_id = ? AND batch_no = ? AND production_date = ? AND expiry_date = ? AND is_del = 0", opKey.warehouseID, opKey.productID, opKey.batchNo, opKey.productionDate, opKey.expiryDate). First(&inventory).Error if err != nil { if err == gorm.ErrRecordNotFound { if quantity < 0 { return nil, fmt.Errorf("库存不存在,无法减少库存: 商品ID=%d", opKey.productID) } inventory = models.Inventory{ WarehouseID: opKey.warehouseID, ProductID: opKey.productID, BatchNo: opKey.batchNo, ProductionDate: opKey.productionDate, ExpiryDate: opKey.expiryDate, Quantity: quantity, LockedQuantity: 0, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&inventory).Error; err != nil { return nil, fmt.Errorf("创建库存记录失败: %v", err) } return &models.InventoryLog{ WarehouseID: opKey.warehouseID, LocationID: locationID, ProductID: opKey.productID, BatchNo: opKey.batchNo, ChangeType: constant.InventoryChangeAdjustment, ChangeQuantity: quantity, BeforeQuantity: 0, AfterQuantity: quantity, RelatedOrderType: constant.OrderTypeStockCheck, RelatedOrderNo: orderNo, Operator: operator, OperatorID: operatorID, Remark: remark, CreatedAt: now, IsDel: 0, }, nil } return nil, fmt.Errorf("查询库存记录失败: %v", err) } beforeQuantity := inventory.Quantity beforeLocked := inventory.LockedQuantity afterQuantity := beforeQuantity + quantity if afterQuantity < 0 { return nil, fmt.Errorf("库存不足,当前:%d, 调整:%d", beforeQuantity, quantity) } if quantity < 0 { availableQty := beforeQuantity - beforeLocked if availableQty < -quantity { return nil, fmt.Errorf("可用库存不足,当前可用:%d(总库存:%d, 已锁定:%d), 需要减少:%d", availableQty, beforeQuantity, beforeLocked, -quantity) } } result := tx.Model(&inventory). UpdateColumn("quantity", gorm.Expr("quantity + ?", quantity)). UpdateColumn("updated_at", now) if result.Error != nil { return nil, fmt.Errorf("更新库存记录失败: %v", result.Error) } if result.RowsAffected == 0 { return nil, errors.New("库存更新失败,请重试") } return &models.InventoryLog{ WarehouseID: opKey.warehouseID, LocationID: locationID, ProductID: opKey.productID, BatchNo: opKey.batchNo, ChangeType: constant.InventoryChangeAdjustment, ChangeQuantity: quantity, BeforeQuantity: beforeQuantity, AfterQuantity: afterQuantity, RelatedOrderType: constant.OrderTypeStockCheck, RelatedOrderNo: orderNo, Operator: operator, OperatorID: operatorID, Remark: remark, CreatedAt: now, IsDel: 0, }, nil } // processInventoryDetailOperationForAdjustment 处理盘库调整的库存明细操作 func (s *ProcessService) processInventoryDetailOperationForAdjustment(tx *gorm.DB, opKey inventoryKey, locationID int64, quantity int64, now int64) error { var inventoryDetail models.InventoryDetail err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("warehouse_id = ? AND location_id = ? AND product_id = ? AND batch_no = ? AND production_date = ? AND expiry_date = ? AND is_del = 0", opKey.warehouseID, locationID, opKey.productID, opKey.batchNo, opKey.productionDate, opKey.expiryDate). First(&inventoryDetail).Error if err != nil { if err == gorm.ErrRecordNotFound { if quantity < 0 { return fmt.Errorf("库存明细不存在,无法减少库存: 商品ID=%d, 库位ID=%d", opKey.productID, locationID) } inventoryDetail = models.InventoryDetail{ WarehouseID: opKey.warehouseID, LocationID: locationID, ProductID: opKey.productID, BatchNo: opKey.batchNo, ProductionDate: opKey.productionDate, ExpiryDate: opKey.expiryDate, Quantity: quantity, LockedQuantity: 0, CreatedAt: now, UpdatedAt: now, IsDel: 0, } return tx.Create(&inventoryDetail).Error } return fmt.Errorf("查询库存明细失败: %v", err) } beforeQuantity := inventoryDetail.Quantity beforeLocked := inventoryDetail.LockedQuantity afterQuantity := beforeQuantity + quantity if afterQuantity < 0 { return fmt.Errorf("库位库存不足,当前:%d, 调整:%d", beforeQuantity, quantity) } if quantity < 0 { availableQty := beforeQuantity - beforeLocked if availableQty < -quantity { return fmt.Errorf("库位可用库存不足,当前可用:%d(总库存:%d, 已锁定:%d), 需要减少:%d", availableQty, beforeQuantity, beforeLocked, -quantity) } } result := tx.Model(&inventoryDetail). UpdateColumn("quantity", gorm.Expr("quantity + ?", quantity)). UpdateColumn("updated_at", now) if result.Error != nil { return fmt.Errorf("更新库存明细失败: %v", result.Error) } if result.RowsAffected == 0 { return errors.New("库存明细更新失败,请重试") } return nil } // lockInventoryByAppearance 按品相+ISBN匹配所有商品的库存进行锁定 func (s *ProcessService) lockInventoryByAppearance(tx *gorm.DB, warehouseID, productID, quantity int64, now int64) 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) } 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) } if len(matchingProductIDs) == 0 { return fmt.Errorf("无匹配的商品记录") } var inventories []models.Inventory if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("warehouse_id = ? AND product_id IN ? AND is_del = 0 AND quantity > locked_quantity", warehouseID, matchingProductIDs). Order("product_id ASC, expiry_date ASC, created_at ASC"). Find(&inventories).Error; err != nil { return fmt.Errorf("查询可用库存失败: %v", err) } if len(inventories) == 0 { return fmt.Errorf("商品(barcode=%s,appearance=%d)在仓库ID=%d中无可用库存", product.Barcode, product.Appearance, warehouseID) } remainingLock := quantity for i := range inventories { if remainingLock <= 0 { break } availableQty := inventories[i].Quantity - inventories[i].LockedQuantity if availableQty <= 0 { continue } lockQty := availableQty if lockQty > remainingLock { lockQty = remainingLock } result := tx.Model(&inventories[i]). Where("quantity - locked_quantity >= ?", lockQty). UpdateColumns(map[string]interface{}{ "locked_quantity": gorm.Expr("locked_quantity + ?", lockQty), "updated_at": now, }) if result.Error != nil { return fmt.Errorf("锁定库存失败: %v", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("库存已被其他事务修改,请重试") } remainingLock -= lockQty } if remainingLock > 0 { return fmt.Errorf("可用库存不足,还需锁定:%d", remainingLock) } return nil } // 执行事务(使用指定数据库) func executeInTransactionWithDB(db *gorm.DB, txFunc func(*gorm.DB) error) error { tx := db.Begin() var err error defer func() { if r := recover(); r != nil { tx.Rollback() panic(r) } else if err != nil { tx.Rollback() } }() err = txFunc(tx) if err != nil { return err } if err = tx.Commit().Error; err != nil { return err } return nil } // generateWaveNo 生成波次号: // - 入库: WH + 年月日 + 两位序号(01-99),每天从01开始 // - 出库: WH + O + 年月日 + 五位序号(00001-99999),每天从00001开始 func (s *ProcessService) generateWaveNo(direction int8, db ...*gorm.DB) (string, error) { now := time.Now() dateStr := now.Format("20060102") startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() endOfDay := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).Unix() var maxWaveNo string var waveNo string var nextSeq int var prefix string databaseConn := database.OptionalDB(db...) if direction == constant.DirectionInbound { // 入库波次: WH + 年月日 + 两位序号 prefix = "WH" + dateStr + "-" query := "SELECT wave_no FROM wave_header WHERE direction = ? AND created_at >= ? AND created_at <= ? AND wave_no LIKE ? ORDER BY wave_no DESC LIMIT 1 FOR UPDATE" err := databaseConn.Raw(query, constant.DirectionInbound, startOfDay, endOfDay, prefix+"%").Scan(&maxWaveNo).Error if err != nil { return "", fmt.Errorf("查询最大入库波次号失败: %v", err) } nextSeq = 1 if maxWaveNo != "" && len(maxWaveNo) >= 13 { seqStr := maxWaveNo[11:] seq, err := strconv.Atoi(seqStr) if err == nil { nextSeq = seq + 1 } } if nextSeq > 99 { return "", fmt.Errorf("当日入库波次号已达上限99") } waveNo = fmt.Sprintf("WH%s-%02d", dateStr, nextSeq) } else if direction == constant.DirectionOutbound { // 出库波次: WH + 年月日 + 五位序号 prefix = "WH" + dateStr + "-" query := "SELECT wave_no FROM wave_header WHERE direction = ? AND created_at >= ? AND created_at <= ? AND wave_no LIKE ? ORDER BY wave_no DESC LIMIT 1 FOR UPDATE" err := databaseConn.Raw(query, constant.DirectionOutbound, startOfDay, endOfDay, prefix+"%").Scan(&maxWaveNo).Error if err != nil { return "", fmt.Errorf("查询最大出库波次号失败: %v", err) } nextSeq = 1 if maxWaveNo != "" && len(maxWaveNo) >= 16 { seqStr := maxWaveNo[14:] seq, err := strconv.Atoi(seqStr) if err == nil { nextSeq = seq + 1 } } if nextSeq > 99999 { return "", fmt.Errorf("当日出库波次号已达上限99999") } waveNo = fmt.Sprintf("WH%s-%05d", dateStr, nextSeq) } else { return "", fmt.Errorf("不支持的波次方向: %d", direction) } return waveNo, nil } // syncTaskToExternal 同步入库任务到外部接口 func (s *ProcessService) syncTaskToExternal(waveTaskID int64, carCapacity int64, db ...*gorm.DB) error { databaseConn := database.OptionalDB(db...) var waveTask models.WaveTask if err := databaseConn.Where("id = ? AND is_del = 0", waveTaskID).First(&waveTask).Error; err == nil { if waveTask.CarId > 0 { var carShops []models.CarShop if err := databaseConn.Where("car_id = ? AND is_del = 0", waveTask.CarId).Find(&carShops).Error; err == nil { for _, carShop := range carShops { shopType := carShop.ShopType taskType := 7 imgType := 2 params := map[string]string{ "shop_id": strconv.FormatInt(carShop.ShopID, 10), "shop_type": strconv.Itoa(int(carShop.ShopType)), "task_count": strconv.FormatInt(carCapacity, 10), "task_type": strconv.Itoa(taskType), "img_type": strconv.Itoa(imgType), } sign := utils.SignParams(params) params["sign"] = sign //创建任务 url := config.AppConfig.ExternalAPI.SyncTaskURL res, err := utils.SubmitFormData(url, params) if err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "创建外部任务失败", "shop_id": carShop.ShopID, "error": fmt.Sprintf("请求失败: %v", err), }) continue } var resData systemRes.ExternalAPIResponse if err := json.Unmarshal([]byte(res), &resData); err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "解析外部接口响应失败", "shop_id": carShop.ShopID, "error": fmt.Sprintf("JSON解析失败: %v", err), "response": res, }) continue } if resData.Code != "200" { now := time.Now().Unix() logRecord := models.OutTaskLog{ ShopID: carShop.ShopID, WaveTaskID: waveTaskID, OutTaskID: 0, ProductID: 0, ISBN: "", LiveImage: datatypes.JSON("[]"), Stock: 0, SalePrice: 0, Status: 0, Msg: fmt.Sprintf("创建任务接口失败: %v", resData.Msg), CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := databaseConn.Create(&logRecord).Error; err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "保存外部任务日志", "shop_id": carShop.ShopID, "error": fmt.Sprintf("保存日志失败: %v", err), }) } utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "外部接口返回错误", "shop_id": carShop.ShopID, "code": resData.Code, "msg": resData.Msg, "response": res, }) continue } var taskIDInt int64 switch v := resData.Data.(type) { case float64: taskIDInt = int64(v) case string: if parsed, err := strconv.ParseInt(v, 10, 64); err == nil { taskIDInt = parsed } default: taskIDInt = 0 } if taskIDInt > 0 { now := time.Now().Unix() outTask := models.OutTask{ ShopID: carShop.ShopID, WaveTaskID: waveTaskID, OutTaskID: taskIDInt, ShopType: shopType, TaskType: int8(taskType), ImgType: int8(imgType), TaskCount: carCapacity, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := databaseConn.Create(&outTask).Error; err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "保存外部任务记录失败", "shop_id": carShop.ShopID, "task_id": taskIDInt, "wave_task_id": waveTaskID, "error": fmt.Sprintf("数据库插入失败: %v", err), }) } } } } } } return nil } // syncProductsToExternal 入库发送任务体 func (s *ProcessService) syncProductsToExternal(receivingOrderID, waveTaskID, userID int64, orderInfo []orderItemInfo, db ...*gorm.DB) error { databaseConn := database.OptionalDB(db...) var receivingOrder models.ReceivingOrder if err := databaseConn.Where("id = ? AND is_del = 0", receivingOrderID).First(&receivingOrder).Error; err != nil { return fmt.Errorf("查询入库单失败: %v", err) } productIDs := make([]int64, 0, len(orderInfo)) for _, item := range orderInfo { productIDs = append(productIDs, item.productID) } var products []models.Product if err := databaseConn.Where("id IN ? AND is_del = 0", productIDs).Find(&products).Error; err != nil { return fmt.Errorf("查询商品信息失败: %v", err) } var logistics models.Logistics err := databaseConn.Table("logistics"). Select("logistics.fir_price"). Joins("INNER JOIN warehouse ON warehouse.logistics_id = logistics.id AND warehouse.is_del = ?", "0"). Joins("INNER JOIN wave_header ON wave_header.warehouse_id = warehouse.id AND wave_header.is_del = 0"). Joins("INNER JOIN wave_task ON wave_task.wave_id = wave_header.id AND wave_task.is_del = 0"). Where("wave_task.id = ?", waveTaskID). First(&logistics).Error if err != nil { return fmt.Errorf("查询物流运费失败: %v", err) } cost := int64(logistics.FirPrice * 100) productMap := make(map[int64]models.Product) for _, p := range products { productMap[p.ID] = p } var warehouse models.Warehouse if err := databaseConn.Where("id = ? AND is_del = 0", receivingOrder.WarehouseID).First(&warehouse).Error; err != nil { return fmt.Errorf("查询仓库信息失败: %v", err) } locationIDs := make([]int64, 0, len(orderInfo)) for _, item := range orderInfo { if item.locationID > 0 { locationIDs = append(locationIDs, item.locationID) } } var locations []models.Location if len(locationIDs) > 0 { if err := databaseConn.Where("id IN ? AND is_del = 0", locationIDs).Find(&locations).Error; err != nil { return fmt.Errorf("查询库位信息失败: %v", err) } } locationMap := make(map[int64]models.Location) for _, loc := range locations { locationMap[loc.ID] = loc } var waveTask models.WaveTask if err := databaseConn.Where("id = ? AND is_del = 0", waveTaskID).First(&waveTask).Error; err != nil { return fmt.Errorf("查询波次任务失败: %v", err) } var car models.Car if err := databaseConn.Where("id = ? AND is_del = 0", waveTask.CarId).First(&car).Error; err != nil { return fmt.Errorf("查询小车信息失败: %v", err) } var bodyList []string var data []models.OutTaskLog if car.ReleaseType == 2 { // 合并模式:按ISBN合并相同商品的库存 type isbnGroup struct { product models.Product totalQty int64 skuCode string imgList []string } isbnMap := make(map[string]*isbnGroup) for _, item := range orderInfo { product, exists := productMap[item.productID] if !exists { continue } isbn := product.Barcode if group, ok := isbnMap[isbn]; ok { // 已存在该ISBN,累加数量 group.totalQty += item.quantity } else { // 新建ISBN组 imgList := parseImageList(product.LiveImage) skuCode := warehouse.Code if item.locationID > 0 { if location, exists := locationMap[item.locationID]; exists { skuCode = fmt.Sprintf("%s##%s", warehouse.Code, location.Code) } } isbnMap[isbn] = &isbnGroup{ product: product, totalQty: item.quantity, skuCode: skuCode, imgList: imgList, } } } // 根据合并后的数据生成请求体 for _, group := range isbnMap { msgData := map[string]interface{}{ "product_id": group.product.ID, "user_id": fmt.Sprintf("%d", userID), } msgJSON, _ := json.Marshal(msgData) bodyData := map[string]interface{}{ "book_info": map[string]interface{}{ "isbn": group.product.Barcode, "book_name": group.product.Name, "image_object": map[string]interface{}{ "carousel_url_array": group.imgList, "white_background_url": "", "detail_url_object": map[string]interface{}{}, "introduction_url": []string{}, "catalogue_url": []string{}, "live_shooting_url": []string{}, "other_url": []string{}, "default_image_url": func() string { if len(group.imgList) > 0 { return group.imgList[0] } return "" }(), }, }, "detail": map[string]interface{}{ "stock": group.totalQty, "price": group.product.SalePrice, "shipping_cost": cost, "sku_code": group.skuCode, "msg": string(msgJSON), "condition": group.product.Appearance, }, } bodyDataJSON, err := json.Marshal(bodyData) if err != nil { return fmt.Errorf("序列化请求体失败: %v", err) } bodyList = append(bodyList, string(bodyDataJSON)) data = append(data, models.OutTaskLog{ ProductID: group.product.ID, ISBN: group.product.Barcode, LiveImage: group.product.LiveImage, Stock: group.totalQty, SalePrice: group.product.SalePrice, Cost: cost, SkuCode: group.skuCode, }) } } else { for _, item := range orderInfo { product, exists := productMap[item.productID] if !exists { continue } isbn := product.Barcode imgList := parseImageList(product.LiveImage) skuCode := warehouse.Code if item.locationID > 0 { if location, exists := locationMap[item.locationID]; exists { skuCode = fmt.Sprintf("%s##%s", warehouse.Code, location.Code) } } msgData := map[string]interface{}{ "product_id": product.ID, "user_id": fmt.Sprintf("%d", userID), } msgJSON, _ := json.Marshal(msgData) bodyData := map[string]interface{}{ "book_info": map[string]interface{}{ "isbn": isbn, "book_name": product.Name, "image_object": map[string]interface{}{ "carousel_url_array": imgList, "white_background_url": "", "detail_url_object": map[string]interface{}{}, "introduction_url": []string{}, "catalogue_url": []string{}, "live_shooting_url": []string{}, "other_url": []string{}, "default_image_url": func() string { if len(imgList) > 0 { return imgList[0] } return "" }(), }, }, "detail": map[string]interface{}{ "stock": item.quantity, "price": product.SalePrice, "shipping_cost": cost, "sku_code": skuCode, "msg": string(msgJSON), "condition": product.Appearance, }, } bodyDataJSON, err := json.Marshal(bodyData) if err != nil { return fmt.Errorf("序列化请求体失败: %v", err) } bodyList = append(bodyList, string(bodyDataJSON)) data = append(data, models.OutTaskLog{ ProductID: product.ID, ISBN: isbn, LiveImage: product.LiveImage, Stock: item.quantity, SalePrice: product.SalePrice, Cost: cost, SkuCode: skuCode, }) } } if receivingOrder.WaveTaskID > 0 && car.PushType == 2 { var outTask []models.OutTask if err := databaseConn.Where("wave_task_id = ? AND is_del = 0", waveTaskID).Find(&outTask).Error; err == nil { for _, task := range outTask { if task.OutTaskID > 0 { taskID := fmt.Sprintf("%d", task.OutTaskID) allBody := strings.Join(bodyList, "") // 直接无缝拼接(和服务端一致) signParams := map[string]string{ "task_id": taskID, "body": allBody, } sign := utils.SignParams(signParams) // 发送请求 url := config.AppConfig.ExternalAPI.SyncTaskBodyURL resp, err := utils.SubmitMultiBody(url, taskID, bodyList, sign) if err != nil { s.saveOutTaskLog(task, data, fmt.Sprintf("请求外部接口失败: %v", err), databaseConn) return fmt.Errorf("请求外部接口失败: %v", err) } var resData systemRes.ExternalAPIResponse if err := json.Unmarshal([]byte(resp), &resData); err != nil { return fmt.Errorf("解析响应失败: %v", err) } if resData.Code != "200" { s.saveOutTaskLog(task, data, fmt.Sprintf("外部接口返回错误: code=%s, msg=%s", resData.Code, resData.Msg), databaseConn) return fmt.Errorf("外部接口返回错误: code=%s, msg=%s", resData.Code, resData.Msg) } s.saveOutTaskLog(task, data, "成功", databaseConn) } } } } return nil } // saveOutTaskLog 保存外部任务日志 func (s *ProcessService) saveOutTaskLog(outTask models.OutTask, bodyList []models.OutTaskLog, msg string, db *gorm.DB) { now := time.Now().Unix() for _, body := range bodyList { body.ShopID = outTask.ShopID body.WaveTaskID = outTask.WaveTaskID body.OutTaskID = outTask.OutTaskID body.Status = func() int8 { if msg == "成功" { return 1 } else { return 0 } }() body.Msg = msg body.CreatedAt = now body.UpdatedAt = now if err := db.Create(&body).Error; err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "保存外部任务日志", "out_task_id": outTask.OutTaskID, "product_id": body.ProductID, "error": fmt.Sprintf("保存日志失败: %v", err), }) } } } // sendSyncRequest 发送同步请求到外部接口 func (s *ProcessService) sendSyncRequest(request systemReq.ExternalProductSyncRequest) error { apiURL := config.AppConfig.ExternalAPI.SyncProductURL // 构造 data 字段的 JSON 字符串 dataMap := map[string]interface{}{ "shopIds": request.ShopIDs, "data": request.Data, } jsonData, err := json.Marshal(dataMap) if err != nil { return fmt.Errorf("序列化JSON失败: %v", err) } // 使用 form-data 格式,但 data 字段是 JSON 字符串 formData := url.Values{} formData.Set("data", string(jsonData)) timeout := time.Duration(config.AppConfig.ExternalAPI.Timeout) * time.Second client := &http.Client{ Timeout: timeout, } // 打印编码后的字符串 encodedData := formData.Encode() resp, err := client.Post(apiURL, "application/x-www-form-urlencoded", strings.NewReader(encodedData)) if err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "外部接口同步", "error": fmt.Sprintf("发送请求失败: %v", err), "url": apiURL, "data": encodedData, }) return fmt.Errorf("发送请求失败: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "外部接口同步", "error": fmt.Sprintf("接口返回错误状态码: %d", resp.StatusCode), "url": apiURL, "data": encodedData, }) return fmt.Errorf("接口返回错误状态码: %d", resp.StatusCode) } var result systemRes.ExternalAPIResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "外部接口同步", "error": fmt.Sprintf("解析响应失败: %v", err), "url": apiURL, }) return fmt.Errorf("解析响应失败: %v", err) } return nil } // saveStatist 保存统计记录 func (s *ProcessService) saveStatist(userID int64, changeType int8, db ...*gorm.DB) { databaseConn := database.OptionalDB(db...) now := time.Now() // 将日期格式化为 YYYYMMDD 字符串,然后转换为时间戳 dateStr := now.Format("20060102") var statDate int64 if _, err := fmt.Sscanf(dateStr, "%d", &statDate); err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "保存统计记录", "user_id": userID, "error": fmt.Sprintf("日期格式化失败: %v", err), }) return } var statist models.Statist // 查询当天是否已有记录 err := databaseConn.Where("create_by = ? AND stat_date = ? AND is_del = ?", userID, statDate, 0).First(&statist).Error if err != nil { // 记录不存在,创建新记录 currentTime := now.Unix() statist = models.Statist{ CreateBy: userID, StatDate: statDate, CreatedAt: currentTime, UpdatedAt: currentTime, IsDel: 0, } if changeType == constant.InventoryChangeInbound { statist.ReceivingNum = 1 statist.OutboundNum = 0 } else { statist.ReceivingNum = 0 statist.OutboundNum = 1 } if err := databaseConn.Create(&statist).Error; err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "保存统计记录", "user_id": userID, "change_type": changeType, "error": fmt.Sprintf("创建统计记录失败: %v", err), }) } } else { // 记录存在,对应字段+1 updates := map[string]interface{}{ "updated_at": now.Unix(), } if changeType == constant.InventoryChangeInbound { updates["receiving_num"] = gorm.Expr("receiving_num + 1") } else { updates["outbound_num"] = gorm.Expr("outbound_num + 1") } if err := databaseConn.Model(&models.Statist{}).Where("id = ?", statist.ID).Updates(updates).Error; err != nil { utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{ "source": "保存统计记录", "user_id": userID, "change_type": changeType, "error": fmt.Sprintf("更新统计记录失败: %v", err), }) } } } // getWaveStatusText 获取波次状态文本 func getWaveStatusText(status int8) string { switch status { case constant.WaveStatusCreated: return "已创建" case constant.WaveStatusReleased: return "已下发" case constant.WaveStatusPicking: return "拣货中" case constant.WaveStatusCompleted: return "已完成" case constant.WaveStatusCancelled: return "已取消" default: return "未知状态" } } // getPurchaseStatusText 获取采购订单状态文本 func getPurchaseStatusText(status int8) string { switch status { case constant.PurchaseStatusDraft: return "草稿" case constant.PurchaseStatusSubmitted: return "已提交" case constant.PurchaseStatusApproved: return "已审核" case constant.PurchaseStatusPartialReceived: return "部分收货" case constant.PurchaseStatusReceived: return "已收货" case constant.PurchaseStatusCancelled: return "已取消" default: return "未知状态" } } // getOutboundStatusText 获取出库单状态文本 func getOutboundStatusText(status int8) string { switch status { case constant.OutboundStatusCreated: return "已创建" case constant.OutboundStatusPicking: return "拣货中" case constant.OutboundStatusCompleted: return "已完成" case constant.OutboundStatusCancelled: return "已取消" case constant.OutboundStatusShipping: return "发货中" case constant.OutboundStatusShipped: return "已发货" default: return "未知状态" } } // getSalesStatusText 获取销售订单状态文本 func getSalesStatusText(status int8) string { switch status { case constant.SalesStatusDraft: return "草稿" case constant.SalesStatusConfirmed: return "已确认" case constant.SalesStatusAllocated: return "已分配库存" case constant.SalesStatusPicking: return "拣货中" case constant.SalesStatusShipped: return "已发货" case constant.SalesStatusCancelled: return "已取消" default: return "未知状态" } } // parseImageList 解析图片列表,支持JSON数组和逗号分隔字符串两种格式 func parseImageList(liveImage datatypes.JSON) []string { if liveImage == nil || len(liveImage) == 0 { return []string{} } // 尝试解析为字符串数组 var imgList []string if err := json.Unmarshal(liveImage, &imgList); err == nil { // 对每个元素做逗号拆分,兼容 ["url1,url2"] 的情况 result := make([]string, 0, len(imgList)) for _, item := range imgList { parts := strings.Split(item, ",") for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed != "" { result = append(result, trimmed) } } } return result } // 如果解析失败,尝试解析为字符串,然后按逗号分割 var imgStr string if err := json.Unmarshal(liveImage, &imgStr); err == nil { parts := strings.Split(imgStr, ",") result := make([]string, 0, len(parts)) for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed != "" { result = append(result, trimmed) } } return result } // 都不成功,返回空数组 return []string{} }