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