using Microsoft.Extensions.Logging;
|
using Newtonsoft.Json;
|
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
|
using System;
|
using System.Collections.Concurrent;
|
using System.Collections.Generic;
|
using System.Linq;
|
using System.Text;
|
using System.Threading.Tasks;
|
using WIDESEA_BasicService;
|
using WIDESEA_Common.CommonEnum;
|
using WIDESEA_Common.LocationEnum;
|
using WIDESEA_Common.OrderEnum;
|
using WIDESEA_Common.OtherEnum;
|
using WIDESEA_Common.StockEnum;
|
using WIDESEA_Common.TaskEnum;
|
using WIDESEA_Core;
|
using WIDESEA_Core.Helper;
|
using WIDESEA_DTO.Basic;
|
using WIDESEA_DTO.Stock;
|
using WIDESEA_Model.Models;
|
using WIDESEA_Model.Models.Basic;
|
using WIDESEA_Model.Models.Check;
|
using WIDESEA_Model.Models.Outbound;
|
|
namespace WIDESEA_TaskInfoService
|
{
|
public partial class TaskService
|
{
|
/// <summary>
|
/// 空托盘出库任务
|
/// </summary>
|
/// <param name="inTask"></param>
|
/// <returns></returns>
|
public async Task<WebResponseContent> PalletOutboundTask(int num, int locationType)
|
{
|
WebResponseContent content = new WebResponseContent();
|
try
|
{
|
var stockInfos = _stockRepository.Db.Queryable<Dt_StockInfo>().Where(x => x.PalletType == PalletTypeEnum.Empty.ObjToInt() && x.StockStatus == StockStatusEmun.入库完成.ObjToInt()).WhereIF(locationType != 0, x => x.LocationType == locationType).Take(num).ToList();
|
|
if (stockInfos.Count() == 0)
|
{
|
return WebResponseContent.Instance.Error("未找到空托盘库存");
|
}
|
foreach (var stockInfo in stockInfos)
|
{
|
Dt_LocationInfo locationInfo = _locationInfoService.Repository.QueryFirst(x => x.LocationCode == stockInfo.LocationCode);
|
if (locationInfo == null)
|
{
|
return WebResponseContent.Instance.Error("未找到空托盘库存对应的货位信息");
|
}
|
|
Dt_Task task = new Dt_Task()
|
{
|
CurrentAddress = stockInfo.LocationCode,
|
Grade = 0,
|
NextAddress = "1-2",
|
PalletCode = stockInfo.PalletCode,
|
Roadway = locationInfo.RoadwayNo,
|
SourceAddress = stockInfo.LocationCode,
|
TargetAddress = "1-2",
|
TaskStatus = TaskStatusEnum.New.ObjToInt(),
|
TaskType = TaskTypeEnum.OutEmpty.ObjToInt(),
|
WarehouseId = stockInfo.WarehouseId,
|
PalletType = stockInfo.PalletType
|
|
};
|
int beforeStatus = locationInfo.LocationStatus;
|
_unitOfWorkManage.BeginTran();
|
stockInfo.StockStatus = StockStatusEmun.出库锁定.ObjToInt();
|
locationInfo.LocationStatus = LocationStatusEnum.Lock.ObjToInt();
|
|
int taskId = BaseDal.AddData(task);
|
task.TaskId = taskId;
|
|
_stockService.StockInfoService.UpdateData(stockInfo);
|
|
_locationInfoService.UpdateData(locationInfo);
|
|
_recordService.LocationStatusChangeRecordSetvice.AddLocationStatusChangeRecord(locationInfo, beforeStatus, StockChangeType.Outbound.ObjToInt(), "", task.TaskNum);
|
|
_unitOfWorkManage.CommitTran();
|
|
TaskModel esstask = new TaskModel()
|
{
|
taskType = "carry",
|
taskGroupCode = "",
|
groupPriority = 0,
|
tasks = new List<TasksType>
|
{
|
new()
|
{
|
taskCode=task.TaskNum.ToString(),
|
taskPriority=0,
|
taskDescribe=new TaskDescribeType{
|
containerCode=stockInfo.PalletCode,
|
containerType= "CT_KUBOT_STANDARD",
|
fromLocationCode=stockInfo.LocationCode??"",
|
toStationCode="",
|
toLocationCode="1-2",
|
deadline=0,storageTag=""
|
}
|
}
|
}
|
};
|
var result = await _eSSApiService.CreateTaskAsync(esstask);
|
|
_logger.LogInformation("创建任务PalletOutboundTask 返回: " + result);
|
}
|
return content.OK("空托出库成功!");
|
}
|
catch (Exception ex)
|
{
|
return WebResponseContent.Instance.Error(ex.Message);
|
}
|
}
|
|
|
/// <summary>
|
/// 出库任务数据处理
|
/// </summary>
|
/// <param name="orderDetailId"></param>
|
/// <param name="stockSelectViews"></param>
|
/// <returns></returns>
|
/// <exception cref="Exception"></exception>
|
public (List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?) OutboundTaskDataHandle(int[] keys, string outStation)
|
{
|
List<Dt_Task> tasks = new List<Dt_Task>();
|
List<Dt_OutboundOrderDetail> outboundOrderDetails = _outboundOrderDetailService.Repository.QueryData(x => keys.Contains(x.Id));
|
|
if (outboundOrderDetails == null || outboundOrderDetails.Count == 0)
|
{
|
throw new Exception("未找到出库单明细信息");
|
}
|
//if (outboundOrderDetails.FirstOrDefault(x => x.OrderDetailStatus > OrderDetailStatusEnum.New.ObjToInt() && x.OrderDetailStatus != OrderDetailStatusEnum.AssignOverPartial.ObjToInt()) != null)
|
//{
|
// throw new Exception("所选出库单明细存在出库中或已完成");
|
//}
|
|
if (outboundOrderDetails.FirstOrDefault(x => x.OrderDetailStatus > OrderDetailStatusEnum.Outbound.ObjToInt() && x.OrderDetailStatus != OrderDetailStatusEnum.AssignOverPartial.ObjToInt()) != null)
|
{
|
throw new Exception("所选出库单明细存在已完成状态,无法重新分配");
|
}
|
|
List<Dt_StockInfo>? stockInfos = null;
|
List<Dt_OutboundOrderDetail>? orderDetails = null;
|
List<Dt_OutStockLockInfo>? outStockLockInfos = null;
|
List<Dt_LocationInfo>? locationInfos = null;
|
|
CleanupPreviousInvalidLocks(outboundOrderDetails);
|
// 开启事务,使用数据库行级锁
|
using (var transaction = _outboundOrderDetailService.Db.Ado.UseTran())
|
{
|
try
|
{
|
// 使用悲观锁锁定订单明细
|
var lockedOrderDetails = new List<Dt_OutboundOrderDetail>();
|
foreach (var key in keys)
|
{
|
var detail = _outboundOrderDetailService.Db.Ado.SqlQuerySingle<Dt_OutboundOrderDetail>(
|
"SELECT * FROM Dt_OutboundOrderDetail WITH (UPDLOCK, ROWLOCK) WHERE Id = @Id",
|
new { Id = key });
|
|
if (detail != null)
|
{
|
lockedOrderDetails.Add(detail);
|
}
|
}
|
|
if (!lockedOrderDetails.Any())
|
{
|
throw new Exception("未找到出库单明细信息");
|
}
|
(List<Dt_StockInfo>, List<Dt_OutboundOrderDetail>, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>) result = _outboundOrderDetailService.AssignStockOutbound(outboundOrderDetails);
|
if (result.Item1 != null && result.Item1.Count > 0)
|
{
|
Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == outboundOrderDetails.FirstOrDefault().OrderId);
|
TaskTypeEnum typeEnum = outboundOrder.OrderType switch
|
{
|
(int)OutOrderTypeEnum.Issue => TaskTypeEnum.Outbound,
|
(int)OutOrderTypeEnum.Allocate => TaskTypeEnum.OutAllocate,
|
(int)OutOrderTypeEnum.Quality => TaskTypeEnum.OutQuality,
|
_ => TaskTypeEnum.Outbound
|
};
|
tasks = GetTasks(result.Item1, typeEnum, outStation);
|
tasks.ForEach(x =>
|
{
|
x.OrderNo = outboundOrder.OrderNo;
|
});
|
result.Item2.ForEach(x =>
|
{
|
x.OrderDetailStatus = OrderDetailStatusEnum.Outbound.ObjToInt();
|
});
|
result.Item3.ForEach(x =>
|
{
|
x.Status = OutLockStockStatusEnum.出库中.ObjToInt();
|
});
|
|
stockInfos = result.Item1;
|
orderDetails = result.Item2;
|
outStockLockInfos = result.Item3;
|
locationInfos = result.Item4;
|
transaction.CommitTran();
|
}
|
else
|
{
|
transaction.RollbackTran();
|
throw new Exception("无库存");
|
}
|
}
|
catch (Exception)
|
{
|
transaction.RollbackTran();
|
throw;
|
}
|
return (tasks, stockInfos, orderDetails, outStockLockInfos, locationInfos);
|
}
|
}
|
/// <summary>
|
/// 清理之前的无效锁定记录
|
/// </summary>
|
private void CleanupPreviousInvalidLocks(List<Dt_OutboundOrderDetail> orderDetails)
|
{
|
var orderIds = orderDetails.Select(x => x.OrderId).Distinct().ToList();
|
var orderNos = _outboundOrderService.Db.Queryable<Dt_OutboundOrder>()
|
.Where(x => orderIds.Contains(x.Id))
|
.Select(x => x.OrderNo)
|
.ToList();
|
|
// 清理状态为"已释放"或"回库中"的旧锁定记录
|
foreach (var orderNo in orderNos)
|
{
|
_outStockLockInfoService.Db.Updateable<Dt_OutStockLockInfo>()
|
.SetColumns(x => new Dt_OutStockLockInfo
|
{
|
Status = (int)OutLockStockStatusEnum.已释放
|
})
|
.Where(x => x.OrderNo == orderNo &&
|
(x.Status == (int)OutLockStockStatusEnum.回库中 ||
|
x.Status == (int)OutLockStockStatusEnum.已释放))
|
.ExecuteCommand();
|
}
|
}
|
|
/// <summary>
|
/// 生成出库任务后数据更新到数据库
|
/// </summary>
|
/// <param name="tasks"></param>
|
/// <param name="stockInfos"></param>
|
/// <param name="outboundOrderDetails"></param>
|
/// <param name="outStockLockInfos"></param>
|
/// <param name="locationInfos"></param>
|
/// <returns></returns>
|
public async Task<WebResponseContent> GenerateOutboundTaskDataUpdateAsync(List<Dt_Task> tasks, List<Dt_StockInfo>? stockInfos = null, List<Dt_OutboundOrderDetail>? outboundOrderDetails = null, List<Dt_OutStockLockInfo>? outStockLockInfos = null, List<Dt_LocationInfo>? locationInfos = null)
|
{
|
try
|
{
|
_unitOfWorkManage.BeginTran();
|
|
BaseDal.AddData(tasks);
|
if (stockInfos != null && stockInfos.Count > 0 && outboundOrderDetails != null && outboundOrderDetails.Count > 0 && outStockLockInfos != null && outStockLockInfos.Count > 0 && locationInfos != null && locationInfos.Count > 0)
|
{
|
stockInfos.ForEach(x =>
|
{
|
x.StockStatus = StockStatusEmun.出库锁定.ObjToInt();
|
});
|
outboundOrderDetails.ForEach(x =>
|
{
|
x.OrderDetailStatus = OrderDetailStatusEnum.Outbound.ObjToInt();
|
});
|
Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == outboundOrderDetails.FirstOrDefault().OrderId);
|
if (outboundOrder.OrderStatus != OutOrderStatusEnum.出库中.ObjToInt())
|
{
|
_outboundOrderService.Repository.UpdateData(outboundOrder);
|
}
|
else
|
{
|
outboundOrder.OrderStatus = OutOrderStatusEnum.出库中.ObjToInt();
|
}
|
outboundOrder.Operator = App.User.UserName;
|
_outboundOrderService.Repository.UpdateData(outboundOrder);
|
WebResponseContent content = _outboundOrderDetailService.LockOutboundStockDataUpdate(stockInfos, outboundOrderDetails, outStockLockInfos, locationInfos, tasks: tasks);
|
|
if (!content.Status)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return content;
|
}
|
}
|
else if (outboundOrderDetails != null && outboundOrderDetails.Count > 0)
|
{
|
outboundOrderDetails.ForEach(x =>
|
{
|
x.OrderDetailStatus = OrderDetailStatusEnum.Outbound.ObjToInt();
|
});
|
Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == outboundOrderDetails.FirstOrDefault().OrderId);
|
if (outboundOrder.OrderStatus != OutOrderStatusEnum.出库中.ObjToInt())
|
{
|
_outboundOrderService.Repository.UpdateData(outboundOrder);
|
}
|
else
|
{
|
outboundOrder.OrderStatus = OutOrderStatusEnum.出库中.ObjToInt();
|
}
|
outboundOrder.Operator = App.User.UserName;
|
_outboundOrderService.Repository.UpdateData(outboundOrder);
|
_outboundOrderDetailService.Repository.UpdateData(outboundOrderDetails);
|
}
|
_unitOfWorkManage.CommitTran();
|
TaskModel esstask = new TaskModel()
|
{
|
taskType = "carry",
|
taskGroupCode = "",
|
groupPriority = 0,
|
tasks = new List<TasksType>()
|
};
|
|
foreach (var task in tasks)
|
{
|
esstask.
|
tasks.Add(new TasksType
|
{
|
taskCode = task.TaskNum.ToString(),
|
taskPriority = 0,
|
taskDescribe = new TaskDescribeType
|
{
|
containerCode = task.PalletCode,
|
containerType = "CT_KUBOT_STANDARD",
|
fromLocationCode = task.SourceAddress ?? "",
|
toStationCode = "",
|
toLocationCode = task.TargetAddress,
|
deadline = 0,
|
storageTag = ""
|
}
|
}
|
);
|
}
|
var result = await _eSSApiService.CreateTaskAsync(esstask);
|
|
_logger.LogInformation("创建任务PalletOutboundTask 返回: " + result);
|
if (result)
|
{
|
return WebResponseContent.Instance.OK();
|
}
|
else
|
{
|
return WebResponseContent.Instance.Error("下发机器人任务失败!");
|
}
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error(ex.Message);
|
}
|
}
|
|
/// <summary>
|
/// 库存数据转出库任务
|
/// </summary>
|
/// <param name="stockInfos"></param>
|
/// <returns></returns>
|
public List<Dt_Task> GetTasks(List<Dt_StockInfo> stockInfos, TaskTypeEnum taskType, string outStation)
|
{
|
List<Dt_Task> tasks = new List<Dt_Task>();
|
List<Dt_LocationInfo> locationInfos = _locationInfoService.Repository.QueryData(x => stockInfos.Select(x => x.LocationCode).Contains(x.LocationCode));
|
for (int i = 0; i < stockInfos.Count; i++)
|
{
|
Dt_StockInfo stockInfo = stockInfos[i];
|
|
if (stockInfo != null)
|
{
|
Dt_LocationInfo? locationInfo = locationInfos.FirstOrDefault(x => x.LocationCode == stockInfo.LocationCode);
|
if (!tasks.Exists(x => x.PalletCode == stockInfo.PalletCode))
|
{
|
Dt_Task task = new()
|
{
|
CurrentAddress = stockInfo.LocationCode,
|
Grade = 0,
|
PalletCode = stockInfo.PalletCode,
|
NextAddress = "",
|
Roadway = locationInfo.RoadwayNo,
|
SourceAddress = stockInfo.LocationCode,
|
TargetAddress = outStation,
|
TaskStatus = TaskStatusEnum.New.ObjToInt(),
|
TaskType = taskType.ObjToInt(),
|
// TaskNum = BaseDal.GetTaskNum(nameof(SequenceEnum.SeqTaskNum)),
|
PalletType = stockInfo.PalletType,
|
WarehouseId = stockInfo.WarehouseId,
|
|
};
|
//if (taskType != TaskTypeEnum.OutEmpty)
|
//{
|
// task.MaterielCode = stockInfo.Details?.Where(x => x.StockId == stockInfo.Id).FirstOrDefault()?.MaterielCode;
|
// task.Quantity = (float)stockInfo.Details?.Where(x => x.StockId == stockInfo.Id).Sum(x => x.StockQuantity);
|
// task.BatchNo = stockInfo.Details?.Where(x => x.StockId == stockInfo.Id).FirstOrDefault()?.BatchNo;
|
//}
|
|
tasks.Add(task);
|
}
|
}
|
}
|
return tasks;
|
}
|
|
public List<Dt_Task> GetTasks(List<Dt_StockInfo> stockInfos, TaskTypeEnum taskType)
|
{
|
List<Dt_Task> tasks = new List<Dt_Task>();
|
List<Dt_LocationInfo> locationInfos = _locationInfoService.Repository.QueryData(x => stockInfos.Select(x => x.LocationCode).Contains(x.LocationCode));
|
for (int i = 0; i < stockInfos.Count; i++)
|
{
|
Dt_StockInfo stockInfo = stockInfos[i];
|
|
if (stockInfo != null)
|
{
|
Dt_LocationInfo? locationInfo = locationInfos.FirstOrDefault(x => x.LocationCode == stockInfo.LocationCode);
|
if (!tasks.Exists(x => x.PalletCode == stockInfo.PalletCode))
|
{
|
Dt_Task task = new()
|
{
|
CurrentAddress = stockInfo.LocationCode,
|
Grade = 0,
|
PalletCode = stockInfo.PalletCode,
|
NextAddress = "",
|
Roadway = locationInfo.RoadwayNo,
|
SourceAddress = stockInfo.LocationCode,
|
TargetAddress = "",
|
TaskStatus = TaskStatusEnum.New.ObjToInt(),
|
TaskType = taskType.ObjToInt(),
|
//TaskNum = BaseDal.GetTaskNum(nameof(SequenceEnum.SeqTaskNum)),
|
PalletType = stockInfo.PalletType,
|
WarehouseId = stockInfo.WarehouseId,
|
|
};
|
//if (taskType != TaskTypeEnum.OutEmpty)
|
//{
|
// task.MaterielCode = stockInfo.Details?.Where(x => x.StockId == stockInfo.Id).FirstOrDefault()?.MaterielCode;
|
// task.Quantity = (float)stockInfo.Details?.Where(x => x.StockId == stockInfo.Id).Sum(x => x.StockQuantity);
|
// task.BatchNo = stockInfo.Details?.Where(x => x.StockId == stockInfo.Id).FirstOrDefault()?.BatchNo;
|
//}
|
//if (stockInfo.StockLength > 0)
|
//{
|
// task.TaskLength = stockInfo.StockLength;
|
//}
|
tasks.Add(task);
|
}
|
}
|
}
|
return tasks;
|
}
|
|
#region 内存锁管理器
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _normalmaterialLocks =
|
new ConcurrentDictionary<string, SemaphoreSlim>();
|
private static readonly ConcurrentDictionary<string, DateTime> _normallockLastUsed =
|
new ConcurrentDictionary<string, DateTime>();
|
private static readonly object _normalcleanupLock = new object();
|
private static DateTime _normallastCleanupTime = DateTime.MinValue;
|
|
/// <summary>
|
/// 获取物料级内存锁
|
/// </summary>
|
private SemaphoreSlim GetNormalMaterialSemaphore(string materialCode, string batchNo, string supplyCode)
|
{
|
// 创建锁键:物料+批次+供应商
|
string lockKey = $"MaterialLock_{materialCode}_{batchNo}_{supplyCode}";
|
|
// 清理长时间不用的锁(每小时清理一次)
|
var now = DateTime.Now;
|
if ((now - _normallastCleanupTime).TotalHours >= 1)
|
{
|
lock (_normalcleanupLock)
|
{
|
if ((now - _normallastCleanupTime).TotalHours >= 1)
|
{
|
var keysToRemove = _normallockLastUsed
|
.Where(kvp => (now - kvp.Value).TotalHours > 2)
|
.Select(kvp => kvp.Key)
|
.ToList();
|
|
foreach (var key in keysToRemove)
|
{
|
if (_normalmaterialLocks.TryRemove(key, out var _semaphore))
|
{
|
_semaphore.Dispose();
|
}
|
_normallockLastUsed.TryRemove(key, out _);
|
}
|
|
_normallastCleanupTime = now;
|
}
|
}
|
}
|
|
// 获取或创建信号量
|
var semaphore = _normalmaterialLocks.GetOrAdd(lockKey, _ => new SemaphoreSlim(1, 1));
|
_normallockLastUsed[lockKey] = now;
|
|
return semaphore;
|
}
|
|
/// <summary>
|
/// 更新内存锁最后使用时间
|
/// </summary>
|
private void UpdateNormalMaterialLockUsedTime(string materialCode, string batchNo, string supplyCode)
|
{
|
string lockKey = $"MaterialLock_{materialCode}_{batchNo}_{supplyCode}";
|
_normallockLastUsed[lockKey] = DateTime.Now;
|
}
|
#endregion
|
/// <summary>
|
/// 生成出库任务
|
/// </summary>
|
/// <param name="keys">出库单明细主键</param>
|
/// <returns></returns>
|
public async Task<WebResponseContent> GenerateOutboundTasksAsync(int[] keys, string outStation)
|
{
|
try
|
{
|
List<Dt_Task> tasks = new List<Dt_Task>();
|
List<StockSelectViewDTO> stockSelectViews = new List<StockSelectViewDTO>();
|
List<Dt_StockInfo> stockInfos = new List<Dt_StockInfo>();
|
List<Dt_OutboundOrderDetail> outboundOrderDetails = new List<Dt_OutboundOrderDetail>();
|
List<Dt_OutStockLockInfo> outStockLockInfos = new List<Dt_OutStockLockInfo>();
|
List<Dt_LocationInfo> locationInfos = new List<Dt_LocationInfo>();
|
// 先获取所有订单明细,确定需要锁定的物料
|
var orderDetails = _outboundOrderDetailService.Repository.QueryData(x => keys.Contains(x.Id));
|
if (orderDetails == null || orderDetails.Count == 0)
|
{
|
return WebResponseContent.Instance.Error("未找到出库单明细信息");
|
}
|
|
// 获取所有需要锁定的物料分组
|
var materialGroups = orderDetails
|
.GroupBy(x => new { x.MaterielCode, x.BatchNo, x.SupplyCode })
|
.Select(g => new
|
{
|
g.Key.MaterielCode,
|
g.Key.BatchNo,
|
g.Key.SupplyCode,
|
Count = g.Count()
|
})
|
.ToList();
|
|
// 按顺序获取所有物料的内存锁(按物料代码排序以避免死锁)
|
var semaphores = new List<SemaphoreSlim>();
|
var acquiredLocks = new List<(string MaterialCode, string BatchNo, string SupplyCode)>();
|
|
try
|
{
|
foreach (var group in materialGroups.OrderBy(g => g.MaterielCode).ThenBy(g => g.BatchNo))
|
{
|
var semaphore = GetMaterialSemaphore(group.MaterielCode, group.BatchNo, group.SupplyCode);
|
|
// 等待获取锁,最多等待30秒
|
bool lockAcquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(30));
|
|
if (!lockAcquired)
|
{
|
// 如果获取锁失败,释放已获取的所有锁
|
foreach (var acquiredSemaphore in semaphores)
|
{
|
acquiredSemaphore.Release();
|
}
|
return WebResponseContent.Instance.Error($"物料[{group.MaterielCode}]批次[{group.BatchNo}]分配繁忙,请稍后重试");
|
}
|
|
semaphores.Add(semaphore);
|
acquiredLocks.Add((group.MaterielCode, group.BatchNo, group.SupplyCode));
|
}
|
|
(List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?) result = OutboundTaskDataHandle(keys, outStation);
|
if (result.Item2 != null && result.Item2.Count > 0)
|
{
|
stockInfos.AddRange(result.Item2);
|
}
|
if (result.Item3 != null && result.Item3.Count > 0)
|
{
|
outboundOrderDetails.AddRange(result.Item3);
|
}
|
if (result.Item4 != null && result.Item4.Count > 0)
|
{
|
outStockLockInfos.AddRange(result.Item4);
|
}
|
if (result.Item5 != null && result.Item5.Count > 0)
|
{
|
locationInfos.AddRange(result.Item5);
|
}
|
if (result.Item1 != null && result.Item1.Count > 0)
|
{
|
tasks.AddRange(result.Item1);
|
}
|
|
WebResponseContent content = await GenerateOutboundTaskDataUpdateAsync(tasks, stockInfos, outboundOrderDetails, outStockLockInfos, locationInfos);
|
return content;
|
}
|
finally
|
{
|
// 释放所有内存锁并更新使用时间
|
foreach (var semaphore in semaphores)
|
{
|
semaphore.Release();
|
}
|
|
foreach (var lockInfo in acquiredLocks)
|
{
|
UpdateMaterialLockUsedTime(lockInfo.MaterialCode, lockInfo.BatchNo, lockInfo.SupplyCode);
|
}
|
}
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error(ex.Message);
|
}
|
}
|
|
/// <summary>
|
/// 智仓调智仓
|
/// </summary>
|
/// <param name="orderDetailId"></param>
|
/// <param name="stockSelectViews"></param>
|
/// <param name="station"></param>
|
/// <returns></returns>
|
public async Task<WebResponseContent> GenerateAllocatOutboundTask(int orderDetailId, List<StockSelectViewDTO> stockSelectViews, string station = null)
|
{
|
try
|
{
|
var allocorder = _allocateOrderRepository.Db.Queryable<Dt_AllocateOrder>().Includes(x => x.Details).Where(x => x.Details.Any(o => o.Id == orderDetailId)).First();
|
//var allocorder = _allocateOrderDetailRepository.Db.Queryable<Dt_AllocateOrderDetail>()
|
// .LeftJoin<Dt_AllocateOrder>((detail, order) => detail.OrderId == order.Id)
|
// .Where((detail, order) => order.Id == orderDetailId)
|
// .Select((detail, order) => order)
|
// .First();
|
if (allocorder == null)
|
{
|
return WebResponseContent.Instance.Error("找不到单据");
|
}
|
var outboundOrder = _outboundOrderService.Db.Queryable<Dt_OutboundOrder>().Includes(x => x.Details).First(x => x.OrderNo == allocorder.OrderNo);
|
if (outboundOrder == null)
|
{
|
return WebResponseContent.Instance.Error("找不到出库单据");
|
}
|
|
var orderdetail = outboundOrder.Details.Where(outItem => allocorder.Details.Any(allocItem => allocItem.MaterielCode == outItem.MaterielCode && allocItem.LineNo == outItem.lineNo
|
&& allocItem.BarcodeQty == outItem.BarcodeQty && allocItem.WarehouseCode == outItem.WarehouseCode && allocItem.BarcodeUnit == outItem.BarcodeUnit)).First();
|
if (orderdetail == null)
|
{
|
return WebResponseContent.Instance.Error("找不到出库明细单据");
|
}
|
|
(List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?) result = OutboundTaskDataHandle(outboundOrder.Details.First().Id, stockSelectViews, station);
|
|
WebResponseContent content = await GenerateOutboundTaskDataUpdate(result.Item1, result.Item2, result.Item3, result.Item4, result.Item5);
|
|
return content;
|
}
|
catch (Exception ex)
|
{
|
return WebResponseContent.Instance.Error(ex.Message);
|
}
|
}
|
/// <summary>
|
/// 生成出库任务
|
/// </summary>
|
/// <param name="orderDetailId"></param>
|
/// <param name="stockSelectViews"></param>
|
/// <returns></returns>
|
public async Task<WebResponseContent> GenerateOutboundTask(int orderDetailId, List<StockSelectViewDTO> stockSelectViews, string station = null)
|
{
|
try
|
{
|
var orderNo = _reCheckOrderRepository.Db.Queryable<Dt_ReCheckOrder>().First(x => x.Id == orderDetailId)?.OrderNo;
|
var outboundOrder = _outboundOrderService.Db.Queryable<Dt_OutboundOrder>().Includes(x => x.Details).First(x => x.UpperOrderNo == orderNo);
|
if (outboundOrder == null)
|
{
|
return WebResponseContent.Instance.Error("找不到单据");
|
}
|
(List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?) result = OutboundTaskDataHandle(outboundOrder.Details.First().Id, stockSelectViews, station);
|
|
WebResponseContent content = await GenerateOutboundTaskDataUpdate(result.Item1, result.Item2, result.Item3, result.Item4, result.Item5);
|
|
return content;
|
}
|
catch (Exception ex)
|
{
|
return WebResponseContent.Instance.Error(ex.Message);
|
}
|
}
|
|
/// <summary>
|
/// 出库任务数据处理
|
/// </summary>
|
/// <param name="orderDetailId"></param>
|
/// <param name="stockSelectViews"></param>
|
/// <returns></returns>
|
/// <exception cref="Exception"></exception>
|
public (List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?) OutboundTaskDataHandle(int orderDetailId, List<StockSelectViewDTO> stockSelectViews, string station = null)
|
{
|
List<Dt_Task> tasks = new List<Dt_Task>();
|
Dt_OutboundOrderDetail outboundOrderDetail = _outboundOrderDetailService.Repository.QueryFirst(x => x.Id == orderDetailId);
|
|
if (outboundOrderDetail == null)
|
{
|
throw new Exception("未找到出库单明细信息");
|
}
|
|
//if (stockSelectViews.Sum(x => x.UseableQuantity) > outboundOrderDetail.OrderQuantity - outboundOrderDetail.LockQuantity)
|
//{
|
// throw new Exception("选择数量超出单据数量");
|
//}
|
List<Dt_StockInfo>? stockInfos = null;
|
Dt_OutboundOrderDetail? orderDetail = null;
|
List<Dt_OutStockLockInfo>? outStockLockInfos = null;
|
List<Dt_LocationInfo>? locationInfos = null;
|
if (outboundOrderDetail.OrderDetailStatus == OrderDetailStatusEnum.New.ObjToInt())
|
{
|
(List<Dt_StockInfo>, Dt_OutboundOrderDetail, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>) result = _outboundOrderDetailService.AssignStockOutbound(outboundOrderDetail, stockSelectViews);
|
if (result.Item1 != null && result.Item1.Count > 0)
|
{
|
Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == outboundOrderDetail.OrderId);
|
TaskTypeEnum typeEnum = outboundOrder.OrderType switch
|
{
|
(int)OutOrderTypeEnum.Issue => TaskTypeEnum.Outbound,
|
(int)OutOrderTypeEnum.Allocate => TaskTypeEnum.OutAllocate,
|
(int)OutOrderTypeEnum.Quality => TaskTypeEnum.OutQuality,
|
_ => TaskTypeEnum.Outbound
|
};
|
tasks = GetTasks(result.Item1, typeEnum, station);
|
result.Item2.OrderDetailStatus = OrderDetailStatusEnum.Outbound.ObjToInt();
|
result.Item3.ForEach(x =>
|
{
|
x.Status = OutLockStockStatusEnum.出库中.ObjToInt();
|
});
|
|
stockInfos = result.Item1;
|
orderDetail = result.Item2;
|
outStockLockInfos = result.Item3;
|
locationInfos = result.Item4;
|
}
|
else
|
{
|
throw new Exception("无库存");
|
}
|
}
|
else
|
{
|
List<Dt_OutStockLockInfo> stockLockInfos = _outStockLockInfoService.GetByOrderDetailId(outboundOrderDetail.OrderId, OutLockStockStatusEnum.已分配);
|
if (stockLockInfos != null && stockLockInfos.Count > 0)
|
{
|
List<Dt_StockInfo> stocks = _stockService.StockInfoService.GetStockInfosByPalletCodes(stockLockInfos.Select(x => x.PalletCode).Distinct().ToList());
|
tasks = GetTasks(stocks, TaskTypeEnum.Outbound);
|
}
|
}
|
|
return (tasks, stockInfos, orderDetail == null ? null : new List<Dt_OutboundOrderDetail> { orderDetail }, outStockLockInfos, locationInfos);
|
}
|
|
/// <summary>
|
/// 生成出库任务后数据更新到数据库
|
/// </summary>
|
/// <param name="tasks"></param>
|
/// <param name="stockInfos"></param>
|
/// <param name="outboundOrderDetails"></param>
|
/// <param name="outStockLockInfos"></param>
|
/// <param name="locationInfos"></param>
|
/// <returns></returns>
|
public async Task<WebResponseContent> GenerateOutboundTaskDataUpdate(List<Dt_Task> tasks, List<Dt_StockInfo>? stockInfos = null, List<Dt_OutboundOrderDetail>? outboundOrderDetails = null, List<Dt_OutStockLockInfo>? outStockLockInfos = null, List<Dt_LocationInfo>? locationInfos = null)
|
{
|
try
|
{
|
_unitOfWorkManage.BeginTran();
|
|
BaseDal.AddData(tasks);
|
if (stockInfos != null && stockInfos.Count > 0 && outboundOrderDetails != null && outboundOrderDetails.Count > 0 && outStockLockInfos != null && outStockLockInfos.Count > 0 && locationInfos != null && locationInfos.Count > 0)
|
{
|
stockInfos.ForEach(x =>
|
{
|
x.StockStatus = StockStatusEmun.出库锁定.ObjToInt();
|
});
|
outboundOrderDetails.ForEach(x =>
|
{
|
x.OrderDetailStatus = OrderDetailStatusEnum.Outbound.ObjToInt();
|
});
|
Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == outboundOrderDetails.FirstOrDefault().OrderId);
|
if (outboundOrder.OrderStatus != OutOrderStatusEnum.出库中.ObjToInt())
|
{
|
_outboundOrderService.Repository.UpdateData(outboundOrder);
|
}
|
WebResponseContent content = _outboundOrderDetailService.LockOutboundStockDataUpdate(stockInfos, outboundOrderDetails, outStockLockInfos, locationInfos, tasks: tasks);
|
|
if (!content.Status)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return content;
|
}
|
}
|
else if (outboundOrderDetails != null && outboundOrderDetails.Count > 0)
|
{
|
outboundOrderDetails.ForEach(x =>
|
{
|
x.OrderDetailStatus = OrderDetailStatusEnum.Outbound.ObjToInt();
|
});
|
Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == outboundOrderDetails.FirstOrDefault().OrderId);
|
if (outboundOrder.OrderStatus != OutOrderStatusEnum.出库中.ObjToInt())
|
{
|
_outboundOrderService.Repository.UpdateData(outboundOrder);
|
}
|
_outboundOrderDetailService.Repository.UpdateData(outboundOrderDetails);
|
}
|
_unitOfWorkManage.CommitTran();
|
//PushTasksToWCS(tasks);
|
TaskModel esstask = new TaskModel()
|
{
|
taskType = "carry",
|
taskGroupCode = "",
|
groupPriority = 0,
|
tasks = new List<TasksType>()
|
};
|
|
foreach (var task in tasks)
|
{
|
esstask.
|
tasks.Add(new TasksType
|
{
|
taskCode = task.TaskNum.ToString(),
|
taskPriority = 0,
|
taskDescribe = new TaskDescribeType
|
{
|
containerCode = task.PalletCode,
|
containerType = "CT_KUBOT_STANDARD",
|
fromLocationCode = task.SourceAddress ?? "",
|
toStationCode = "",
|
toLocationCode = task.TargetAddress,
|
deadline = 0,
|
storageTag = ""
|
}
|
}
|
);
|
}
|
var result = await _eSSApiService.CreateTaskAsync(esstask);
|
|
_logger.LogInformation("创建任务PalletOutboundTask 返回: " + result);
|
if (result)
|
{
|
return WebResponseContent.Instance.OK();
|
}
|
else
|
{
|
return WebResponseContent.Instance.Error("下发机器人任务失败!");
|
}
|
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error(ex.Message);
|
}
|
|
}
|
|
|
#region 分批分配库存
|
#region 内存锁管理器
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _materialLocks =
|
new ConcurrentDictionary<string, SemaphoreSlim>();
|
private static readonly ConcurrentDictionary<string, DateTime> _lockLastUsed =
|
new ConcurrentDictionary<string, DateTime>();
|
private static readonly object _cleanupLock = new object();
|
private static DateTime _lastCleanupTime = DateTime.MinValue;
|
|
/// <summary>
|
/// 获取物料级内存锁
|
/// </summary>
|
private SemaphoreSlim GetMaterialSemaphore(string materialCode, string batchNo, string supplyCode)
|
{
|
// 创建锁键:物料+批次+供应商
|
string lockKey = $"MaterialLock_{materialCode}_{batchNo}_{supplyCode}";
|
|
// 清理长时间不用的锁(每小时清理一次)
|
var now = DateTime.Now;
|
if ((now - _lastCleanupTime).TotalHours >= 1)
|
{
|
lock (_cleanupLock)
|
{
|
if ((now - _lastCleanupTime).TotalHours >= 1)
|
{
|
var keysToRemove = _lockLastUsed
|
.Where(kvp => (now - kvp.Value).TotalHours > 2)
|
.Select(kvp => kvp.Key)
|
.ToList();
|
|
foreach (var key in keysToRemove)
|
{
|
if (_materialLocks.TryRemove(key, out var _semaphore))
|
{
|
_semaphore.Dispose();
|
}
|
_lockLastUsed.TryRemove(key, out _);
|
}
|
|
_lastCleanupTime = now;
|
}
|
}
|
}
|
|
// 获取或创建信号量
|
var semaphore = _materialLocks.GetOrAdd(lockKey, _ => new SemaphoreSlim(1, 1));
|
_lockLastUsed[lockKey] = now;
|
|
return semaphore;
|
}
|
|
/// <summary>
|
/// 释放内存锁并更新最后使用时间
|
/// </summary>
|
private void UpdateMaterialLockUsedTime(string materialCode, string batchNo, string supplyCode)
|
{
|
string lockKey = $"MaterialLock_{materialCode}_{batchNo}_{supplyCode}";
|
_lockLastUsed[lockKey] = DateTime.Now;
|
}
|
#endregion
|
/// <summary>
|
/// 分批分配库存
|
/// </summary>
|
public async Task<WebResponseContent> GenerateOutboundBatchTasksAsync(int orderDetailId, decimal batchQuantity, string outStation)
|
{
|
try
|
{
|
// 先获取订单明细信息,确定物料
|
var orderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
|
.FirstAsync(x => x.Id == orderDetailId);
|
|
if (orderDetail == null)
|
return WebResponseContent.Instance.Error("未找到订单明细信息");
|
|
// 获取物料级内存锁
|
var semaphore = GetMaterialSemaphore(orderDetailId + orderDetail.MaterielCode, orderDetail.BatchNo, orderDetail.SupplyCode);
|
|
// 等待获取内存锁,最多等待30秒
|
bool memoryLockAcquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(30));
|
|
if (!memoryLockAcquired)
|
return WebResponseContent.Instance.Error("系统繁忙,请稍后重试");
|
|
try
|
{
|
List<Dt_Task> tasks = new List<Dt_Task>();
|
List<Dt_StockInfo> stockInfos = new List<Dt_StockInfo>();
|
List<Dt_OutboundOrderDetail> outboundOrderDetails = new List<Dt_OutboundOrderDetail>();
|
List<Dt_OutStockLockInfo> outStockLockInfos = new List<Dt_OutStockLockInfo>();
|
List<Dt_LocationInfo> locationInfos = new List<Dt_LocationInfo>();
|
|
(List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?) result = await BatchAllocateStockDataHandle(orderDetailId, batchQuantity, outStation);
|
|
if (result.Item2 != null && result.Item2.Count > 0)
|
{
|
stockInfos.AddRange(result.Item2);
|
}
|
if (result.Item3 != null && result.Item3.Count > 0)
|
{
|
outboundOrderDetails.AddRange(result.Item3);
|
}
|
if (result.Item4 != null && result.Item4.Count > 0)
|
{
|
outStockLockInfos.AddRange(result.Item4);
|
}
|
if (result.Item5 != null && result.Item5.Count > 0)
|
{
|
locationInfos.AddRange(result.Item5);
|
}
|
if (result.Item1 != null && result.Item1.Count > 0)
|
{
|
tasks.AddRange(result.Item1);
|
}
|
|
WebResponseContent content = await GenerateOutboundTaskDataUpdateAsync(tasks, stockInfos, outboundOrderDetails, outStockLockInfos, locationInfos);
|
return content;
|
}
|
finally
|
{
|
// 释放内存锁
|
semaphore.Release();
|
UpdateMaterialLockUsedTime(orderDetail.MaterielCode, orderDetail.BatchNo, orderDetail.SupplyCode);
|
}
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
_logger.LogError($"分批分配库存失败 - OrderDetailId: {orderDetailId}, Quantity: {batchQuantity}, Error: {ex.Message}");
|
return WebResponseContent.Instance.Error($"分批分配失败:{ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 分批分配库存数据处理
|
/// </summary>
|
public async Task<(List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?)>
|
BatchAllocateStockDataHandle(int orderDetailId, decimal batchQuantity, string outStation)
|
{
|
List<Dt_Task> tasks = new List<Dt_Task>();
|
|
// 获取订单明细
|
var outboundOrderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>().With("UPDLOCK, ROWLOCK")
|
.FirstAsync(x => x.Id == orderDetailId);
|
|
if (outboundOrderDetail == null)
|
{
|
throw new Exception("未找到出库单明细信息");
|
}
|
var outboundOrder = await _outboundOrderService.Db.Queryable<Dt_OutboundOrder>().FirstAsync(x => x.Id == outboundOrderDetail.OrderId);
|
if (outboundOrder == null)
|
{
|
throw new Exception("未找到出库单信息");
|
}
|
// 验证订单明细状态
|
if (outboundOrderDetail.OrderDetailStatus > OrderDetailStatusEnum.Outbound.ObjToInt() &&
|
outboundOrderDetail.OrderDetailStatus != OrderDetailStatusEnum.AssignOverPartial.ObjToInt())
|
{
|
throw new Exception("所选出库单明细存在出库中或已完成");
|
}
|
|
// 验证分配数量
|
decimal allocatedQty = outboundOrderDetail.AllocatedQuantity;
|
decimal overOutQty = outboundOrderDetail.OverOutQuantity;
|
decimal needOutQty = outboundOrderDetail.NeedOutQuantity;
|
decimal availableQty = needOutQty - allocatedQty - overOutQty;
|
|
if (availableQty <= 0)
|
throw new Exception("无可分配数量");
|
|
if (batchQuantity > availableQty)
|
throw new Exception($"分配数量不能超过可分配数量{availableQty}");
|
|
List<Dt_StockInfo>? stockInfos = null;
|
List<Dt_OutboundOrderDetail>? orderDetails = null;
|
List<Dt_OutStockLockInfo>? outStockLockInfos = null;
|
List<Dt_LocationInfo>? locationInfos = null;
|
|
// 生成批次号
|
string batchNo = await GenerateBatchNo();
|
|
// 分配库存
|
(List<Dt_StockInfo>, List<Dt_OutboundOrderDetail>, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>) allocateResult =
|
await _outboundOrderDetailService.AssignStockForBatch(outboundOrderDetail, batchQuantity, batchNo);
|
|
if (allocateResult.Item1 != null && allocateResult.Item1.Count > 0)
|
{
|
// 创建分批记录
|
await CreateBatchRecord(outboundOrder.OrderNo, orderDetailId, batchQuantity, batchNo);
|
|
TaskTypeEnum typeEnum = outboundOrder.OrderType switch
|
{
|
(int)OutOrderTypeEnum.Issue => TaskTypeEnum.Outbound,
|
(int)OutOrderTypeEnum.Allocate => TaskTypeEnum.OutAllocate,
|
(int)OutOrderTypeEnum.Quality => TaskTypeEnum.OutQuality,
|
_ => TaskTypeEnum.Outbound
|
};
|
|
tasks = GetTasks(allocateResult.Item1, typeEnum, outStation);
|
tasks.ForEach(x =>
|
{
|
x.OrderNo = outboundOrder.OrderNo;
|
});
|
|
allocateResult.Item2.ForEach(x =>
|
{
|
x.OrderDetailStatus = OrderDetailStatusEnum.Outbound.ObjToInt();
|
});
|
|
allocateResult.Item3.ForEach(x =>
|
{
|
x.Status = OutLockStockStatusEnum.出库中.ObjToInt();
|
});
|
|
stockInfos = allocateResult.Item1;
|
orderDetails = allocateResult.Item2;
|
outStockLockInfos = allocateResult.Item3;
|
locationInfos = allocateResult.Item4;
|
}
|
else
|
{
|
throw new Exception("无库存");
|
}
|
|
return (tasks, stockInfos, orderDetails, outStockLockInfos, locationInfos);
|
}
|
|
|
|
|
|
/// <summary>
|
/// 更新订单明细状态
|
/// </summary>
|
private void UpdateOrderDetailStatus(List<Dt_OutboundOrderDetail> details, decimal allocatedQuantity, decimal needQuantity)
|
{
|
foreach (var detail in details)
|
{
|
// 根据分配情况更新状态
|
if (allocatedQuantity >= needQuantity)
|
{
|
detail.OrderDetailStatus = OrderDetailStatusEnum.Outbound.ObjToInt();
|
}
|
else
|
{
|
detail.OrderDetailStatus = OrderDetailStatusEnum.AssignOverPartial.ObjToInt();
|
}
|
}
|
}
|
|
|
|
private async Task<string> GenerateBatchNo()
|
{
|
var batchNo = UniqueValueGenerator.Generate();
|
|
return $"Out{batchNo} ";
|
}
|
|
private async Task<Dt_OutboundBatch> CreateBatchRecord(string orderNo, int orderDetailId, decimal batchQuantity, string batchNo)
|
{
|
var batchRecord = new Dt_OutboundBatch
|
{
|
BatchNo = batchNo,
|
OrderNo = orderNo,
|
OrderDetailId = orderDetailId,
|
BatchQuantity = batchQuantity,
|
BatchStatus = (int)BatchStatusEnum.分配中,
|
Operator = App.User.UserName
|
};
|
|
await _OutboundBatchRepository.Db.Insertable(batchRecord).ExecuteCommandAsync();
|
return batchRecord;
|
}
|
|
#endregion
|
|
|
}
|
}
|