using MailKit.Search;
|
using Microsoft.AspNetCore.Http;
|
using Microsoft.Extensions.Logging;
|
using Newtonsoft.Json;
|
using OfficeOpenXml;
|
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
|
using SqlSugar;
|
using System;
|
using System.Collections;
|
using System.Collections.Concurrent;
|
using System.Collections.Generic;
|
using System.Linq;
|
using System.Text;
|
using System.Threading.Tasks;
|
using System.Transactions;
|
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.CodeConfigEnum;
|
using WIDESEA_Core.DB;
|
using WIDESEA_Core.Helper;
|
using WIDESEA_Core.Seed;
|
using WIDESEA_DTO.Basic;
|
using WIDESEA_DTO.Stock;
|
using WIDESEA_IBasicService;
|
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>
|
// 方法增加了 string? SupplierCode 可选参数
|
public async Task<WebResponseContent> PalletOutboundTask(int num, int locationType, string? supplierCode = null)
|
{
|
WebResponseContent content = new WebResponseContent();
|
try
|
{
|
Dictionary<string, SqlSugar.OrderByType> orderByDict = new Dictionary<string, SqlSugar.OrderByType>()
|
{
|
{ nameof(Dt_LocationInfo.Layer), SqlSugar.OrderByType.Asc },
|
{ nameof(Dt_LocationInfo.Row), SqlSugar.OrderByType.Asc },
|
{ nameof(Dt_LocationInfo.Column), SqlSugar.OrderByType.Asc },
|
{ nameof(Dt_LocationInfo.Depth), SqlSugar.OrderByType.Desc },
|
};
|
|
var query = _stockRepository.Db.Queryable<Dt_StockInfo>()
|
.Where(x => x.PalletType == PalletTypeEnum.Empty.ObjToInt()
|
&& x.StockStatus == StockStatusEmun.入库完成.ObjToInt())
|
.WhereIF(locationType != 0, x => x.LocationType == locationType)
|
.LeftJoin<Dt_LocationInfo>((s, l) => s.LocationCode == l.LocationCode);
|
|
if (!await query.AnyAsync())
|
{
|
return WebResponseContent.Instance.Error("未找到空托盘库存");
|
}
|
|
bool isFirstOrder = true;
|
foreach (var item in orderByDict)
|
{
|
string fieldName = item.Key.Equals("Column", StringComparison.OrdinalIgnoreCase)
|
? $"l.[{item.Key}]"
|
: $"l.{item.Key}";
|
|
string sortSql = $"{fieldName} {(item.Value == SqlSugar.OrderByType.Asc ? "ASC" : "DESC")}";
|
|
if (isFirstOrder)
|
{
|
query = query.OrderBy(sortSql);
|
isFirstOrder = false;
|
}
|
else
|
{
|
query.OrderBy(sortSql);
|
}
|
}
|
|
var stockInfos = await query.Take(num).ToListAsync();
|
|
_unitOfWorkManage.BeginTran();
|
|
Dt_PlasticContainerLedger? todayLedger = null;
|
var today = DateTime.Now.Date;
|
var tomorrow = today.AddDays(1);
|
|
if (!string.IsNullOrWhiteSpace(supplierCode))
|
{
|
todayLedger = await _stockRepository.Db.Queryable<Dt_PlasticContainerLedger>()
|
.Where(x => x.SupplyCode == supplierCode)
|
.Where(x => x.CreateDate >= today && x.CreateDate < tomorrow)
|
.FirstAsync();
|
}
|
|
string allTaskNums = string.Empty;
|
|
foreach (var stockInfo in stockInfos)
|
{
|
Dt_LocationInfo locationInfo = await _locationInfoService.Repository.QueryFirstAsync(x => x.LocationCode == stockInfo.LocationCode);
|
if (locationInfo == null)
|
{
|
_unitOfWorkManage.RollbackTran();
|
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 taskId = await BaseDal.AddDataAsync(task);
|
task.TaskId = taskId;
|
allTaskNums += task.TaskNum + ",";
|
|
|
stockInfo.StockStatus = StockStatusEmun.出库锁定.ObjToInt();
|
_stockService.StockInfoService.UpdateData(stockInfo);
|
|
int beforeStatus = locationInfo.LocationStatus;
|
locationInfo.LocationStatus = LocationStatusEnum.Lock.ObjToInt();
|
_locationInfoService.UpdateData(locationInfo);
|
|
_recordService.LocationStatusChangeRecordSetvice.AddLocationStatusChangeRecord(
|
locationInfo, beforeStatus, StockChangeType.Outbound.ObjToInt(), "", task.TaskNum);
|
}
|
|
if (!string.IsNullOrWhiteSpace(supplierCode))
|
{
|
var ledgerList = new List<Dt_PlasticContainerLedger>();
|
|
foreach (var stock in stockInfos)
|
{
|
ledgerList.Add(new Dt_PlasticContainerLedger
|
{
|
SupplyCode = supplierCode,
|
PalletCode = stock.PalletCode,
|
CreateDate = DateTime.Now,
|
Creater = App.User?.ToString()
|
});
|
}
|
_plasticContainerLedger.AddData(ledgerList);
|
}
|
|
_unitOfWorkManage.CommitTran();
|
return content.OK("空托出库成功!");
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error(ex.Message);
|
}
|
}
|
|
|
/// <summary>
|
/// 实时搜索供应商编码(支持输入 r 自动匹配,下拉框用)
|
/// </summary>
|
/// <param name="keyword">输入的关键词(如 r)</param>
|
/// <returns>匹配的供应商编码列表</returns>
|
public async Task<WebResponseContent> SearchSupplierCode(string keyword)
|
{
|
try
|
{
|
if (string.IsNullOrWhiteSpace(keyword))
|
return WebResponseContent.Instance.OK("没有匹配到该供应商编号");
|
|
var list = await _stockRepository.Db.Queryable<Dt_SupplierInfo>()
|
.Where(x => x.SupplierShortName.StartsWith(keyword))
|
.OrderBy(x => x.SupplierShortName)
|
.Take(20)
|
.Select(x => x.SupplierShortName)
|
.ToListAsync();
|
|
return WebResponseContent.Instance.OK(data:list);
|
}
|
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) 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();
|
return WebResponseContent.Instance.OK();
|
//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;
|
}
|
|
|
|
#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 = SqlSugarHelper.DbWMS.Queryable<Dt_OutboundOrder>().Includes(x => x.Details).Where(x => x.Details.Any(o => o.Id == orderDetailId)).First();
|
|
if (outboundOrder == null)
|
{
|
return WebResponseContent.Instance.Error("找不到出库单据");
|
}
|
if (outboundOrder.Details == null || !outboundOrder.Details.Any())
|
{
|
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() ||
|
outboundOrderDetail.OrderDetailStatus == OrderDetailStatusEnum.Outbound.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,station);
|
}
|
}
|
|
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();
|
return WebResponseContent.Instance.OK();
|
//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")
|
.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
|
|
/// <summary>
|
/// 选定库存生成盘点单出库
|
/// </summary>
|
/// <param name="id"></param>
|
/// <returns></returns>
|
public async Task<WebResponseContent> TakeOutbound(List<StockViewDTO> stockViews, string outStation)
|
{
|
var content = new WebResponseContent();
|
var errorMessages = new List<string>();
|
|
try
|
{
|
if (stockViews == null || !stockViews.Any())
|
{
|
content.Error("未获取到库存数据");
|
content.Data = new { total = 0, success = 0, fail = 0, failMsg = errorMessages };
|
return content;
|
}
|
|
var ids = stockViews.Select(x => x.StockId).ToList();
|
var stockInfos = _stockRepository.Db.Queryable<Dt_StockInfo>()
|
.Where(x => ids.Contains(x.Id))
|
.Includes(x => x.Details)
|
.ToList();
|
|
var locCodes = stockInfos.Select(x => x.LocationCode).ToList();
|
var locationInfos = _locationInfoService.Db.Queryable<Dt_LocationInfo>()
|
.Where(x => locCodes.Contains(x.LocationCode))
|
.ToList();
|
|
var finalSuccessStocks = new List<Dt_StockInfo>();
|
var finalSuccessLocations = new List<Dt_LocationInfo>();
|
|
foreach (var stock in stockInfos)
|
{
|
try
|
{
|
if (stock.Details == null || !stock.Details.Any())
|
{
|
errorMessages.Add($"托盘【{stock.PalletCode}】:无库存明细");
|
continue;
|
}
|
|
var loc = locationInfos.FirstOrDefault(x => x.LocationCode == stock.LocationCode);
|
if (loc == null)
|
{
|
errorMessages.Add($"托盘【{stock.PalletCode}】:库位不存在");
|
continue;
|
}
|
|
if (loc.EnableStatus != EnableStatusEnum.Normal.ObjToInt() ||
|
loc.LocationStatus != LocationStatusEnum.InStock.ObjToInt() ||
|
stock.StockStatus != StockStatusEmun.入库完成.ObjToInt())
|
{
|
errorMessages.Add($"托盘【{stock.PalletCode}】:状态不允许出库");
|
continue;
|
}
|
|
finalSuccessStocks.Add(stock);
|
finalSuccessLocations.Add(loc);
|
}
|
catch (Exception ex)
|
{
|
errorMessages.Add($"托盘【{stock.PalletCode}】异常:{ex.Message}");
|
continue;
|
}
|
}
|
|
if (finalSuccessStocks.Any())
|
{
|
var takeStockOrder = new Dt_TakeStockOrder
|
{
|
WarehouseId = finalSuccessStocks.First().WarehouseId,
|
TakeStockStatus = TakeStockStatusEnum.盘点中.ObjToInt(),
|
OrderNo = CreateCodeByRule(nameof(RuleCodeEnum.PDCodeRule)),
|
AllPalletCode = string.Join(",", finalSuccessStocks.Select(x => x.PalletCode)),
|
Remark = outStation
|
};
|
|
var tasks = GetTasks(finalSuccessStocks, TaskTypeEnum.OutInventory, outStation);
|
if (tasks == null || tasks.Count <= 0)
|
{
|
errorMessages.Add("任务生成失败");
|
}
|
else
|
{
|
finalSuccessStocks.ForEach(x => x.StockStatus = StockStatusEmun.盘点出库锁定.ObjToInt());
|
finalSuccessLocations.ForEach(x => x.LocationStatus = LocationStatusEnum.Lock.ObjToInt());
|
tasks.ForEach(x => x.OrderNo = takeStockOrder.OrderNo);
|
|
_unitOfWorkManage.BeginTran();
|
_stockRepository.UpdateData(finalSuccessStocks);
|
_takeStockOrder.AddData(takeStockOrder);
|
BaseDal.AddData(tasks);
|
_locationInfoService.UpdateData(finalSuccessLocations);
|
_unitOfWorkManage.CommitTran();
|
}
|
}
|
|
content.OK($"处理完成:成功【{finalSuccessStocks.Count}】条,失败【{errorMessages.Count}】条");
|
content.Data = new
|
{
|
total = stockInfos.Count,
|
success = finalSuccessStocks.Count,
|
fail = errorMessages.Count,
|
failMsg = errorMessages
|
};
|
return content;
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
content.Error($"系统异常:{ex.Message}");
|
content.Data = new
|
{
|
total = stockViews?.Count ?? 0,
|
success = 0,
|
fail = 1,
|
failMsg = new List<string> { ex.Message }
|
};
|
return content;
|
}
|
}
|
|
public async Task<WebResponseContent> BatchOutboundByExcel(IFormFile file, string outStation)
|
{
|
WebResponseContent content = new WebResponseContent();
|
List<StockViewDTO> successStockViews = new List<StockViewDTO>();
|
List<string> noStockMaterialCodes = new List<string>();
|
|
try
|
{
|
if (file == null || file.Length == 0)
|
return content.Error("上传失败:请选择需要导入的Excel文件");
|
|
var ext = Path.GetExtension(file.FileName).ToLower();
|
if (ext != ".xls" && ext != ".xlsx")
|
return content.Error("上传失败:仅支持 .xls 和 .xlsx 格式的Excel文件,请重新上传");
|
|
try
|
{
|
using var stream = file.OpenReadStream();
|
var excelList = ReadExcel(stream);
|
|
if (!excelList.Any())
|
return content.Error("导入失败:Excel文件中未读取到任何有效数据");
|
|
foreach (var item in excelList)
|
{
|
var matchedStocks = _stockRepository.Db.Queryable<Dt_StockInfo>()
|
.Includes(x => x.Details)
|
.Where(x =>
|
x.Details.Any(d =>
|
d.WarehouseCode == item.WarehouseCode
|
&& d.MaterielCode == item.MaterialCode
|
)
|
)
|
.ToList();
|
|
if (matchedStocks.Any())
|
{
|
successStockViews.AddRange(matchedStocks.Select(s => new StockViewDTO
|
{
|
StockId = s.Id
|
}));
|
}
|
else
|
{
|
noStockMaterialCodes.Add(item.MaterialCode);
|
}
|
}
|
|
// 执行出库
|
if (successStockViews.Any())
|
{
|
var result = await TakeOutbound(successStockViews, outStation);
|
return result;
|
}
|
|
// 无任何可出库数据
|
var msg = "未查询到任何可出库库存";
|
if (noStockMaterialCodes.Any())
|
{
|
msg += $",无库存料号:{string.Join("、", noStockMaterialCodes)}";
|
}
|
return content.Error(msg);
|
}
|
catch (Exception ex)
|
{
|
return content.Error($"Excel解析失败:{ex.Message}");
|
}
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return content.Error($"批量出库失败:系统异常,{ex.Message}");
|
}
|
}
|
/// <summary>
|
/// 单据生成方法
|
/// </summary>
|
static object lock_code = new object();
|
public string CreateCodeByRule(string ruleCode)
|
{
|
lock (lock_code)
|
{
|
|
string code = string.Empty;
|
DateTime dateTime = DateTime.Now;
|
DateTime now = DateTime.Now;
|
try
|
{
|
if (string.IsNullOrEmpty(ruleCode))
|
throw new ArgumentNullException(nameof(ruleCode));
|
SqlSugarClient sugarClient = new SqlSugarClient(new ConnectionConfig
|
{
|
IsAutoCloseConnection = true,
|
DbType = DbType.SqlServer,
|
ConnectionString = DBContext.ConnectionString
|
});
|
Dt_CodeRuleConfig codeRuleConfig = sugarClient.Queryable<Dt_CodeRuleConfig>().Where(x => x.RuleCode == ruleCode).First();
|
if (codeRuleConfig == null)
|
throw new ArgumentNullException(nameof(codeRuleConfig));
|
if (codeRuleConfig.ModifyDate != null)
|
{
|
dateTime = Convert.ToDateTime(codeRuleConfig.ModifyDate);
|
}
|
else
|
{
|
dateTime = Convert.ToDateTime(codeRuleConfig.CreateDate);
|
}
|
|
if (now.Year == dateTime.Year && now.Month == dateTime.Month && now.Day == dateTime.Day)
|
{
|
now = dateTime;
|
codeRuleConfig.CurrentVal = Convert.ToInt32(codeRuleConfig.CurrentVal) + 1;
|
}
|
else
|
{
|
codeRuleConfig.CurrentVal = 1;
|
}
|
codeRuleConfig.ModifyDate = DateTime.Now;
|
code = codeRuleConfig.StartStr + codeRuleConfig.Format;
|
code = code.Replace($"[{CodeFormatTypeEnum.YYYY}]", now.Year.ToString().PadLeft(4, '0'));
|
code = code.Replace($"[{CodeFormatTypeEnum.MM}]", now.Month.ToString().PadLeft(2, '0'));
|
code = code.Replace($"[{CodeFormatTypeEnum.DD}]", now.Day.ToString().PadLeft(2, '0'));
|
code = code.Replace($"[{CodeFormatTypeEnum.ST}]", codeRuleConfig.StartStr?.ToString() ?? "");
|
code = code.Replace($"[{CodeFormatTypeEnum.NUM}]", codeRuleConfig.CurrentVal.ToString().PadLeft(codeRuleConfig.Length, '0'));
|
Dictionary<string, object> keyValuePairs = new Dictionary<string, object>() { { nameof(codeRuleConfig.CurrentVal), codeRuleConfig.CurrentVal }, { nameof(codeRuleConfig.Id), codeRuleConfig.Id }, { nameof(codeRuleConfig.ModifyDate), DateTime.Now } };
|
sugarClient.Updateable(keyValuePairs).AS(MainDb.CodeRuleConfig).WhereColumns(nameof(codeRuleConfig.Id)).ExecuteCommand();
|
sugarClient.Updateable(codeRuleConfig);
|
|
}
|
catch (Exception ex)
|
{
|
|
}
|
return code;
|
}
|
}
|
|
/// <summary>
|
/// 选定库存同区域移库
|
/// </summary>
|
/// <param name="id"></param>
|
/// <returns></returns>
|
public async Task<WebResponseContent> AreaOutbound(List<StockViewDTO> stockViews)
|
{
|
WebResponseContent content = new WebResponseContent();
|
try
|
{
|
List<int> ids = stockViews.Select(x => x.StockId).ToList();
|
//获取库存
|
List<Dt_StockInfo> stockInfos = _stockRepository.Db.Queryable<Dt_StockInfo>().Where(x => ids.Contains(x.Id)).Includes(x => x.Details).ToList();
|
if (stockInfos.Count != stockViews.Count)
|
{
|
StockViewDTO? stockViewDTO = stockViews.FirstOrDefault(x => !stockInfos.Select(x => x.PalletCode).ToList().Contains(x.PalletCode));
|
return content.Error($"未找到{stockViewDTO?.PalletCode}库存");
|
}
|
//获取货位
|
List<string> locStrs = stockInfos.Select(x => x.LocationCode).ToList();
|
List<Dt_LocationInfo> locationInfos = _locationInfoService.Db.Queryable<Dt_LocationInfo>().Where(x => locStrs.Contains(x.LocationCode)).ToList();
|
if (stockInfos.Count != locationInfos.Count)
|
{
|
string? locStr = locStrs.FirstOrDefault(x => !locationInfos.Select(x => x.LocationCode).ToList().Contains(x));
|
return content.Error($"未找到{locStr}货位数据");
|
}
|
|
foreach (var item in stockInfos)
|
{
|
Dt_LocationInfo? locationInfo = locationInfos.FirstOrDefault(x => x.LocationCode == item.LocationCode);
|
if (locationInfo == null || (locationInfo.EnableStatus == EnableStatusEnum.Disable.ObjToInt() || locationInfo.EnableStatus != EnableStatusEnum.Normal.ObjToInt()) || item.StockStatus != StockStatusEmun.入库完成.ObjToInt())
|
{
|
return content.Error($"{item.PalletCode}货位或库存状态不满足出库条件");
|
}
|
}
|
List<Dt_Task> tasks = AreaGetTasks(stockInfos, TaskTypeEnum.AreaRelocation);
|
if (tasks == null || tasks.Count <= 0)
|
{
|
return content.Error($"生成任务失败");
|
}
|
stockInfos.ForEach(x =>
|
{
|
x.StockStatus = StockStatusEmun.出库锁定.ObjToInt();
|
});
|
tasks.ForEach(x =>
|
{
|
x.OrderNo = "无单据移库";
|
});
|
locationInfos.ForEach(x =>
|
{
|
x.LocationStatus = LocationStatusEnum.Lock.ObjToInt();
|
});
|
_unitOfWorkManage.BeginTran();
|
//更新库存状态
|
_stockRepository.UpdateData(stockInfos);
|
//新建任务
|
BaseDal.AddData(tasks);
|
_locationInfoService.UpdateData(locationInfos);
|
_unitOfWorkManage.CommitTran();
|
content.OK();
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return await Task.FromResult(WebResponseContent.Instance.Error(ex.Message));
|
}
|
return content;
|
}
|
|
public List<Dt_Task> AreaGetTasks(List<Dt_StockInfo> stockInfos, TaskTypeEnum taskType)
|
{
|
// 使用 TransactionScope 包裹整个操作,确保所有数据库操作在同一事务中
|
using (var scope = new TransactionScope(TransactionScopeOption.Required,
|
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
|
{
|
try
|
{
|
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);
|
var newLocation = _locationInfoService.AssignLocation(stockInfo.LocationType);
|
|
if (newLocation == null)
|
{
|
throw new Exception($"在{stockInfo.PalletCode}时没有空闲库位可进行同区域移库,请检查或减少移库料箱");
|
}
|
|
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 = newLocation.LocationCode,
|
TaskStatus = TaskStatusEnum.New.ObjToInt(),
|
TaskType = taskType.ObjToInt(),
|
PalletType = stockInfo.PalletType,
|
WarehouseId = stockInfo.WarehouseId,
|
};
|
tasks.Add(task);
|
}
|
|
newLocation.LocationStatus = LocationStatusEnum.Lock.ObjToInt();
|
_locationInfoService.UpdateData(newLocation);
|
}
|
}
|
|
// 提交事务
|
scope.Complete();
|
return tasks;
|
}
|
catch (Exception ex)
|
{
|
// TransactionScope 会自动回滚事务,无需手动 Rollback
|
throw new Exception(ex.Message);
|
}
|
}
|
}
|
|
/// <summary>
|
/// 选定库存跨区域移库
|
/// </summary>
|
/// <param name="stockViews"></param>
|
/// <param name="targetLocationType">目标货位类型</param>
|
/// <returns></returns>
|
public async Task<WebResponseContent> CrossAreaOutbound(List<StockViewDTO> stockViews, int targetLocationType)
|
{
|
WebResponseContent content = new WebResponseContent();
|
try
|
{
|
if(targetLocationType == (int)LocationTypeEnum.Electronic)
|
{
|
return content.Error("电子仓不允许跨区域移库");
|
}
|
List<int> ids = stockViews.Select(x => x.StockId).ToList();
|
List<Dt_StockInfo> stockInfos = _stockRepository.Db.Queryable<Dt_StockInfo>().Where(x => ids.Contains(x.Id)).Includes(x => x.Details).ToList();
|
|
if (stockInfos.Count != stockViews.Count)
|
{
|
StockViewDTO? stockViewDTO = stockViews.FirstOrDefault(x => !stockInfos.Select(x => x.PalletCode).Contains(x.PalletCode));
|
return content.Error($"未找到{stockViewDTO?.PalletCode}库存");
|
}
|
|
List<string> locStrs = stockInfos.Select(x => x.LocationCode).ToList();
|
List<Dt_LocationInfo> locationInfos = _locationInfoService.Db.Queryable<Dt_LocationInfo>().Where(x => locStrs.Contains(x.LocationCode)).ToList();
|
|
if (stockInfos.Count != locationInfos.Count)
|
{
|
string? locStr = locStrs.FirstOrDefault(x => !locationInfos.Select(x => x.LocationCode).Contains(x));
|
return content.Error($"未找到{locStr}货位数据");
|
}
|
|
foreach (var item in stockInfos)
|
{
|
if (item.PalletType != PalletTypeEnum.Empty.ObjToInt())
|
{
|
return content.Error($"托盘【{item.PalletCode}】非空箱,仅空箱允许跨区域移库!");
|
}
|
Dt_LocationInfo? locationInfo = locationInfos.FirstOrDefault(x => x.LocationCode == item.LocationCode);
|
if (locationInfo == null || locationInfo.EnableStatus != EnableStatusEnum.Normal.ObjToInt() || item.StockStatus != StockStatusEmun.入库完成.ObjToInt())
|
{
|
return content.Error($"{item.PalletCode}货位或库存状态不满足出库条件");
|
}
|
}
|
|
List<Dt_Task> tasks = CrossAreaGetTasks(stockInfos, targetLocationType, TaskTypeEnum.CrossAreaRelocation);
|
|
if (tasks == null || tasks.Count <= 0)
|
{
|
return content.Error("生成跨区域移库任务失败");
|
}
|
|
stockInfos.ForEach(x =>
|
{
|
x.StockStatus = StockStatusEmun.出库锁定.ObjToInt();
|
});
|
|
tasks.ForEach(x =>
|
{
|
x.OrderNo = "跨区域移库";
|
});
|
|
locationInfos.ForEach(x =>
|
{
|
x.LocationStatus = LocationStatusEnum.Lock.ObjToInt();
|
});
|
|
_unitOfWorkManage.BeginTran();
|
_stockRepository.UpdateData(stockInfos);
|
BaseDal.AddData(tasks);
|
_locationInfoService.UpdateData(locationInfos);
|
_unitOfWorkManage.CommitTran();
|
|
content.OK();
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return await Task.FromResult(WebResponseContent.Instance.Error(ex.Message));
|
}
|
return content;
|
}
|
|
|
/// <summary>
|
/// 生成跨区域移库任务
|
/// </summary>
|
/// <param name="stockInfos">库存列表</param>
|
/// <param name="targetAreaCode">目标区域编码</param>
|
/// <param name="taskType">任务类型</param>
|
/// <returns></returns>
|
public List<Dt_Task> CrossAreaGetTasks(List<Dt_StockInfo> stockInfos, int targetLocationType, TaskTypeEnum taskType)
|
{
|
using (var scope = new TransactionScope(TransactionScopeOption.Required,
|
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
|
{
|
try
|
{
|
List<Dt_Task> tasks = new List<Dt_Task>();
|
|
foreach (var stockInfo in stockInfos)
|
{
|
Dt_LocationInfo newLocation = _locationInfoService.AssignLocation(targetLocationType);
|
|
if (newLocation == null)
|
{
|
throw new Exception($"{stockInfo.PalletCode} 没有空闲货位可进行跨区域移库");
|
}
|
|
if (!tasks.Exists(x => x.PalletCode == stockInfo.PalletCode))
|
{
|
Dt_Task task = new()
|
{
|
CurrentAddress = stockInfo.LocationCode,
|
Grade = 0,
|
PalletCode = stockInfo.PalletCode,
|
NextAddress = "",
|
Roadway = newLocation.RoadwayNo,
|
SourceAddress = stockInfo.LocationCode,
|
TargetAddress = newLocation.LocationCode,
|
TaskStatus = TaskStatusEnum.New.ObjToInt(),
|
TaskType = taskType.ObjToInt(),
|
PalletType = stockInfo.PalletType,
|
WarehouseId = stockInfo.WarehouseId,
|
};
|
tasks.Add(task);
|
}
|
|
newLocation.LocationStatus = LocationStatusEnum.Lock.ObjToInt();
|
_locationInfoService.UpdateData(newLocation);
|
}
|
|
scope.Complete();
|
return tasks;
|
}
|
catch (Exception ex)
|
{
|
throw new Exception(ex.Message);
|
}
|
}
|
}
|
|
public static List<ExcelInventoryOutboundDto> ReadExcel(Stream stream)
|
{
|
var list = new List<ExcelInventoryOutboundDto>();
|
|
using (var package = new ExcelPackage(stream))
|
{
|
var sheet = package.Workbook.Worksheets.FirstOrDefault();
|
if (sheet == null) return list;
|
|
// 正确获取行数
|
int rowCount = sheet.Dimension?.End.Row ?? 0;
|
|
// 第2行开始读
|
for (int row = 2; row <= rowCount; row++)
|
{
|
string location = sheet.Cells[row, 8].Value?.ToString()?.Trim() ?? "";
|
string materialCode = sheet.Cells[row, 2].Value?.ToString()?.Trim() ?? "";
|
|
if (string.IsNullOrEmpty(location) || string.IsNullOrEmpty(materialCode))
|
continue;
|
|
list.Add(new ExcelInventoryOutboundDto
|
{
|
WarehouseCode = location,
|
MaterialCode = materialCode
|
});
|
}
|
}
|
|
return list;
|
}
|
}
|
|
}
|