daShangDao_psiServer/service/process.go

4487 lines
147 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}
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(&currentInventory).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(&currentInventory).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 || salesOrder.Status == constant.SalesStatusCancelled {
return fmt.Errorf("订单状态不允许取消,当前状态: %s", getSalesStatusText(salesOrder.Status))
}
var orderItems []models.SalesOrderItem
if err := tx.Where("sales_order_id = ? AND is_del = 0", orderID).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(&salesOrder).Updates(map[string]interface{}{
"status": constant.SalesStatusCancelled,
"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", orderID).
Updates(map[string]interface{}{
"allocated_quantity": 0,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("重置订单明细已分配数量失败: %v", err)
}
if salesOrder.Status == constant.SalesStatusAllocated || salesOrder.Status == constant.SalesStatusPicking {
var waveHeader models.WaveHeader
if err := tx.Where("related_order_id = ? AND direction = ? AND is_del = 0",
orderID, constant.DirectionOutbound).First(&waveHeader).Error; err == nil {
if err := tx.Model(&models.WaveHeader{}).Where("id = ?", waveHeader.ID).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 = ?", waveHeader.ID).Updates(map[string]interface{}{
"status": constant.WaveStatusCancelled,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("更新关联波次任务状态失败: %v", err)
}
}
}
return 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,
},
},
"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,
},
},
"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{}
}