using Microsoft.Extensions.Logging;
|
using SqlSugar;
|
using System;
|
using System.Collections.Generic;
|
using System.Linq;
|
using System.Text;
|
using System.Threading.Tasks;
|
using WIDESEA_BasicService;
|
using WIDESEA_Common.CommonEnum;
|
using WIDESEA_Common.OrderEnum;
|
using WIDESEA_Common.StockEnum;
|
using WIDESEA_Common.TaskEnum;
|
using WIDESEA_Core;
|
using WIDESEA_Core.BaseRepository;
|
using WIDESEA_Core.BaseServices;
|
using WIDESEA_Core.Enums;
|
using WIDESEA_Core.Helper;
|
using WIDESEA_DTO.Basic;
|
using WIDESEA_DTO.Outbound;
|
using WIDESEA_IAllocateService;
|
using WIDESEA_IBasicService;
|
using WIDESEA_IOutboundService;
|
using WIDESEA_IStockService;
|
using WIDESEA_ITaskInfoService;
|
using WIDESEA_Model.Models;
|
using WIDESEA_Model.Models.Basic;
|
using WIDESEA_Model.Models.Outbound;
|
using static WIDESEA_OutboundService.OutboundBatchPickingService;
|
|
namespace WIDESEA_OutboundService
|
{
|
public class OutboundBatchPickingService : ServiceBase<Dt_PickingRecord, IRepository<Dt_PickingRecord>>, IOutboundBatchPickingService
|
{
|
|
|
private readonly IUnitOfWorkManage _unitOfWorkManage;
|
public IRepository<Dt_PickingRecord> Repository => BaseDal;
|
|
private readonly IStockInfoService _stockInfoService;
|
private readonly IStockService _stockService;
|
private readonly IOutStockLockInfoService _outStockLockInfoService;
|
private readonly IStockInfoDetailService _stockInfoDetailService;
|
private readonly ILocationInfoService _locationInfoService;
|
private readonly IOutboundOrderDetailService _outboundOrderDetailService;
|
private readonly IOutboundOrderService _outboundOrderService;
|
private readonly ISplitPackageService _splitPackageService;
|
private readonly IRepository<Dt_Task> _taskRepository;
|
private readonly IESSApiService _eSSApiService;
|
private readonly IInvokeMESService _invokeMESService;
|
private readonly IDailySequenceService _dailySequenceService;
|
private readonly IAllocateService _allocateService;
|
private readonly IRepository<Dt_OutboundBatch> _outboundBatchRepository;
|
private readonly ITask_HtyService _task_HtyService;
|
private readonly ILogger<OutboundPickingService> _logger;
|
|
private Dictionary<string, string> stations = new Dictionary<string, string>
|
{
|
{"2-1","2-9" },
|
{"3-1","3-9" },
|
|
};
|
|
private Dictionary<string, string> movestations = new Dictionary<string, string>
|
{
|
{"2-1","2-5" },
|
{"3-1","3-5" },
|
|
};
|
|
public OutboundBatchPickingService(IRepository<Dt_PickingRecord> BaseDal, IUnitOfWorkManage unitOfWorkManage, IStockInfoService stockInfoService, IStockService stockService,
|
IOutStockLockInfoService outStockLockInfoService, IStockInfoDetailService stockInfoDetailService, ILocationInfoService locationInfoService,
|
IOutboundOrderDetailService outboundOrderDetailService, ISplitPackageService splitPackageService, IOutboundOrderService outboundOrderService,
|
IRepository<Dt_Task> taskRepository, IESSApiService eSSApiService, ILogger<OutboundPickingService> logger, IInvokeMESService invokeMESService, IDailySequenceService dailySequenceService, IAllocateService allocateService, IRepository<Dt_OutboundBatch> outboundBatchRepository, ITask_HtyService task_HtyService) : base(BaseDal)
|
{
|
_unitOfWorkManage = unitOfWorkManage;
|
_stockInfoService = stockInfoService;
|
_stockService = stockService;
|
_outStockLockInfoService = outStockLockInfoService;
|
_stockInfoDetailService = stockInfoDetailService;
|
_locationInfoService = locationInfoService;
|
_outboundOrderDetailService = outboundOrderDetailService;
|
_splitPackageService = splitPackageService;
|
_outboundOrderService = outboundOrderService;
|
_taskRepository = taskRepository;
|
_eSSApiService = eSSApiService;
|
_logger = logger;
|
_invokeMESService = invokeMESService;
|
_dailySequenceService = dailySequenceService;
|
_allocateService = allocateService;
|
_outboundBatchRepository = outboundBatchRepository;
|
_task_HtyService = task_HtyService;
|
}
|
|
// <summary>
|
/// 获取托盘的锁定信息
|
/// </summary>
|
public async Task<List<PalletLockInfoDto>> GetPalletLockInfos(string orderNo, string palletCode)
|
{
|
var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
|
.Select(x => new
|
{
|
x.Id,
|
x.OrderNo,
|
x.BatchNo,
|
x.MaterielCode,
|
x.CurrentBarcode,
|
x.AssignQuantity,
|
x.PickedQty,
|
x.Status,
|
x.LocationCode,
|
x.PalletCode
|
}).ToListAsync();
|
|
var lockInfoDtos = lockInfos.Select(x => new PalletLockInfoDto
|
{
|
Id = x.Id,
|
OrderNo = x.OrderNo,
|
BatchNo = x.BatchNo,
|
MaterielCode = x.MaterielCode,
|
CurrentBarcode = x.CurrentBarcode,
|
AssignQuantity = x.AssignQuantity,
|
PickedQty = x.PickedQty,
|
Status = x.Status,
|
LocationCode = x.LocationCode,
|
PalletCode = x.PalletCode,
|
CanSplit = (x.Status == (int)OutLockStockStatusEnum.出库中 && x.AssignQuantity - x.PickedQty > 0),
|
CanPick = (x.Status == (int)OutLockStockStatusEnum.出库中 && x.PickedQty < x.AssignQuantity)
|
}).ToList();
|
|
return lockInfoDtos;
|
}
|
#region 查询方法
|
|
/// <summary>
|
/// 获取托盘的已拣选列表
|
/// </summary>
|
public async Task<List<PalletPickedInfoDto>> GetPalletPickedList(string orderNo, string palletCode)
|
{
|
var pickedList = await Db.Queryable<Dt_PickingRecord>()
|
.Where(x => x.OrderNo == orderNo &&
|
x.PalletCode == palletCode &&
|
!x.IsCancelled)
|
.Select(x => new PalletPickedInfoDto
|
{
|
Id = x.Id,
|
OrderNo = x.OrderNo,
|
OrderDetailId = x.OrderDetailId,
|
PalletCode = x.PalletCode,
|
Barcode = x.Barcode,
|
MaterielCode = x.MaterielCode,
|
PickedQty = x.PickQuantity,
|
PickTime = x.PickTime,
|
Operator = x.Operator,
|
LocationCode = x.LocationCode
|
})
|
.ToListAsync();
|
|
return pickedList;
|
}
|
|
/// <summary>
|
/// 获取托盘状态
|
/// </summary>
|
public async Task<PalletStatusDto> GetPalletStatus(string orderNo, string palletCode)
|
{
|
// 获取托盘的锁定信息
|
var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
|
.ToListAsync();
|
|
if (!lockInfos.Any())
|
{
|
return new PalletStatusDto
|
{
|
OrderNo = orderNo,
|
PalletCode = palletCode,
|
Status = (int)PalletStatusEnum.无任务,
|
StatusText = "无任务",
|
TotalItems = 0,
|
CompletedItems = 0,
|
PendingItems = 0
|
};
|
}
|
|
var totalItems = lockInfos.Count;
|
var completedItems = lockInfos.Count(x => x.Status == (int)OutLockStockStatusEnum.拣选完成);
|
var pendingItems = lockInfos.Count(x => x.Status == (int)OutLockStockStatusEnum.出库中);
|
|
var status = PalletStatusEnum.拣选中;
|
if (pendingItems == 0 && completedItems > 0)
|
{
|
status = PalletStatusEnum.已完成;
|
}
|
else if (pendingItems > 0 && completedItems == 0)
|
{
|
status = PalletStatusEnum.未开始;
|
}
|
else if (pendingItems > 0 && completedItems > 0)
|
{
|
status = PalletStatusEnum.拣选中;
|
}
|
|
return new PalletStatusDto
|
{
|
OrderNo = orderNo,
|
PalletCode = palletCode,
|
Status = (int)status,
|
StatusText = GetPalletStatusText(status),
|
TotalItems = totalItems,
|
CompletedItems = completedItems,
|
PendingItems = pendingItems
|
};
|
}
|
|
/// <summary>
|
/// 获取拆包信息
|
/// </summary>
|
public async Task<SplitPackageInfoDto> GetSplitPackageInfo(string orderNo, string palletCode, string barcode)
|
{
|
// 查找锁定信息
|
var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo &&
|
x.PalletCode == palletCode &&
|
x.CurrentBarcode == barcode
|
//&& x.Status == (int)OutLockStockStatusEnum.出库中
|
)
|
.FirstAsync();
|
|
if (lockInfo == null)
|
throw new Exception("未找到有效的锁定信息");
|
|
// 计算剩余可拆数量
|
var remainQuantity = lockInfo.AssignQuantity - lockInfo.PickedQty;
|
|
return new SplitPackageInfoDto
|
{
|
OrderNo = orderNo,
|
PalletCode = palletCode,
|
Barcode = barcode,
|
MaterielCode = lockInfo.MaterielCode,
|
RemainQuantity = remainQuantity,
|
AssignQuantity = lockInfo.AssignQuantity,
|
PickedQty = lockInfo.PickedQty
|
};
|
}
|
|
#endregion
|
|
#region 分批分拣
|
|
|
/// <summary>
|
/// 分批分拣确认 - 修正版(包含拆包后验证)
|
/// </summary>
|
public async Task<WebResponseContent> ConfirmBatchPicking(string orderNo, string palletCode, string barcode)
|
{
|
try
|
{
|
_logger.LogInformation($"【分拣开始】订单: {orderNo}, 托盘: {palletCode}, 条码: {barcode}");
|
|
_unitOfWorkManage.BeginTran();
|
|
// 验证分拣请求
|
var validationResult = await ValidatePickingRequest(orderNo, palletCode, barcode);
|
if (!validationResult.IsValid)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
|
}
|
|
var (lockInfo, orderDetail, stockDetail, batch) = validationResult.Data;
|
|
_logger.LogInformation($"验证通过 - 锁定ID: {lockInfo.Id}, 分配数量: {lockInfo.AssignQuantity}, 已拣选: {lockInfo.PickedQty}");
|
_logger.LogInformation($"库存信息 - 条码: {stockDetail.Barcode}, 库存数量: {stockDetail.StockQuantity}, 出库数量: {stockDetail.OutboundQuantity}");
|
|
|
decimal originalStockQtyBeforeSplit = stockDetail.StockQuantity;
|
decimal originalOutboundQtyBeforeSplit = stockDetail.OutboundQuantity;
|
|
// 记录拆包前的关键数据(用于后续验证)
|
decimal originalAllocatedQty = orderDetail.AllocatedQuantity;
|
decimal originalLockQty = orderDetail.LockQuantity;
|
decimal originalStockQty = stockDetail.StockQuantity;
|
decimal originalOutboundQty = stockDetail.OutboundQuantity;
|
|
// 检查是否需要自动拆包
|
var autoSplitResult = await CheckAndAutoSplitIfNeeded(lockInfo, stockDetail, palletCode);
|
if (autoSplitResult != null)
|
{
|
_logger.LogInformation($"执行了自动拆包,重新获取数据");
|
|
// 重新获取最新的锁定信息和库存信息
|
var refreshedValidation = await ValidatePickingRequest(orderNo, palletCode, barcode);
|
if (!refreshedValidation.IsValid)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error(refreshedValidation.ErrorMessage);
|
}
|
|
(lockInfo, orderDetail, stockDetail, batch) = refreshedValidation.Data;
|
|
|
// 调用自动拆包后验证
|
decimal splitQuantity = autoSplitResult.FirstOrDefault()?.quantityTotal.ObjToDecimal()??0 ;
|
bool autoSplitValid = await ValidateAfterAutoSplit(lockInfo, orderDetail, stockDetail, splitQuantity,originalStockQtyBeforeSplit);
|
|
if (!autoSplitValid)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error("自动拆包后数据验证失败,请检查系统日志");
|
}
|
|
_logger.LogInformation($"自动拆包验证通过,继续执行分拣");
|
}
|
|
// 计算实际拣选数量
|
decimal actualPickedQty = lockInfo.AssignQuantity - lockInfo.PickedQty;
|
|
if (actualPickedQty <= 0)
|
{
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error("该条码已拣选完成,无需重复拣选");
|
}
|
|
_logger.LogInformation($"开始拣选 - 数量: {actualPickedQty}");
|
|
// 执行分拣逻辑
|
var pickingResult = await ExecutePickingLogic(lockInfo, orderDetail, stockDetail, actualPickedQty);
|
|
// 更新批次和订单数据
|
await UpdateBatchAndOrderData(batch, orderDetail, actualPickedQty, orderNo);
|
|
//记录拣选历史
|
await RecordPickingHistory(pickingResult, orderNo, palletCode);
|
|
// 拣选后验证
|
await ValidateAfterPicking(orderNo, palletCode, barcode, actualPickedQty);
|
|
_unitOfWorkManage.CommitTran();
|
|
return WebResponseContent.Instance.OK("分拣成功", autoSplitResult);
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
_logger.LogError($"分拣失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Barcode: {barcode}, Error: {ex.Message}");
|
return WebResponseContent.Instance.Error($"分拣失败:{ex.Message}");
|
}
|
}
|
|
|
/// <summary>
|
/// 自动拆包后验证数据一致性
|
/// </summary>
|
private async Task<bool> ValidateAfterAutoSplit(Dt_OutStockLockInfo lockInfo, Dt_OutboundOrderDetail orderDetail,
|
Dt_StockInfoDetail originalStockDetail, decimal splitQuantity, decimal originalStockQtyBeforeSplit)
|
{
|
try
|
{
|
_logger.LogInformation($"开始自动拆包后验证 - 原条码: {originalStockDetail.Barcode}, 拆包数量: {splitQuantity}");
|
_logger.LogInformation($"拆包前原库存数量: {originalStockQtyBeforeSplit}, 分配数量: {lockInfo.AssignQuantity}");
|
|
bool allValid = true;
|
List<string> validationErrors = new List<string>();
|
|
// 重新获取最新的数据(拆包后的当前状态)
|
var refreshedOrderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
|
.FirstAsync(x => x.Id == orderDetail.Id);
|
|
var refreshedLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.FirstAsync(x => x.Id == lockInfo.Id);
|
|
var refreshedStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Id == originalStockDetail.Id);
|
|
// 验证原库存明细数据
|
decimal expectedOriginalStockQty = lockInfo.AssignQuantity;
|
|
_logger.LogInformation($"库存验证基准:");
|
_logger.LogInformation($" 拆包前原库存: {originalStockQtyBeforeSplit}");
|
_logger.LogInformation($" 分配数量: {lockInfo.AssignQuantity}");
|
_logger.LogInformation($" 拆包数量: {splitQuantity}");
|
_logger.LogInformation($" 期望原库存: {expectedOriginalStockQty}");
|
_logger.LogInformation($" 实际原库存: {refreshedStockDetail.StockQuantity}");
|
|
// 允许少量误差的验证
|
if (Math.Abs(refreshedStockDetail.StockQuantity - expectedOriginalStockQty) > 0.01m)
|
{
|
// 额外检查:如果原库存数量不合理,可能是数据问题
|
if (refreshedStockDetail.StockQuantity < 0)
|
{
|
string error = $"原库存数量异常(负数)!实际: {refreshedStockDetail.StockQuantity}";
|
validationErrors.Add(error);
|
allValid = false;
|
_logger.LogError(error);
|
}
|
else if (refreshedStockDetail.StockQuantity > originalStockQtyBeforeSplit)
|
{
|
string error = $"原库存数量异常(大于拆包前)!拆包前: {originalStockQtyBeforeSplit}, 当前: {refreshedStockDetail.StockQuantity}";
|
validationErrors.Add(error);
|
allValid = false;
|
_logger.LogError(error);
|
}
|
else
|
{
|
// 可能是合理的误差,记录警告但不标记为失败
|
_logger.LogWarning($"原库存数量与期望值有差异,期望: {expectedOriginalStockQty}, 实际: {refreshedStockDetail.StockQuantity}");
|
}
|
}
|
|
// 验证原库存的出库数量是否保持不变
|
if (Math.Abs(refreshedStockDetail.OutboundQuantity - originalStockDetail.OutboundQuantity) > 0.01m)
|
{
|
string error = $"原库存出库数量不应变化!拆包前: {originalStockDetail.OutboundQuantity}, 拆包后: {refreshedStockDetail.OutboundQuantity}";
|
validationErrors.Add(error);
|
allValid = false;
|
_logger.LogError(error);
|
}
|
|
// 验证新库存明细(拆包产生的)
|
// 查找新条码(通过拆包记录)
|
var splitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
|
.Where(x => x.OutStockLockInfoId == lockInfo.Id &&
|
!x.IsReverted &&
|
x.IsAutoSplit == true)
|
.OrderByDescending(x => x.SplitTime)
|
.ToListAsync();
|
|
if (splitRecords.Any())
|
{
|
var latestSplit = splitRecords.First();
|
if (!string.IsNullOrEmpty(latestSplit.NewBarcode))
|
{
|
var newStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == latestSplit.NewBarcode && x.StockId == originalStockDetail.StockId);
|
|
if (newStockDetail != null)
|
{
|
// 新库存的期望值 = 拆包数量
|
decimal expectedNewStockQty = splitQuantity;
|
|
_logger.LogInformation($"新库存验证:");
|
_logger.LogInformation($" 新条码: {newStockDetail.Barcode}");
|
_logger.LogInformation($" 期望数量: {expectedNewStockQty}");
|
_logger.LogInformation($" 实际数量: {newStockDetail.StockQuantity}");
|
_logger.LogInformation($" 出库数量: {newStockDetail.OutboundQuantity} (应为0)");
|
|
if (Math.Abs(newStockDetail.StockQuantity - expectedNewStockQty) > 0.01m)
|
{
|
string error = $"新库存数量不正确!期望: {expectedNewStockQty}, 实际: {newStockDetail.StockQuantity}";
|
validationErrors.Add(error);
|
allValid = false;
|
_logger.LogError(error);
|
}
|
|
// 新库存出库数量应为0
|
if (Math.Abs(newStockDetail.OutboundQuantity - 0) > 0.01m)
|
{
|
string error = $"新库存出库数量不为0!实际: {newStockDetail.OutboundQuantity}";
|
validationErrors.Add(error);
|
allValid = false;
|
_logger.LogError(error);
|
}
|
}
|
}
|
}
|
|
// 4. 验证未分配锁定记录
|
var unallocatedLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.ParentLockId == lockInfo.Id &&
|
x.IsUnallocated == 1 &&
|
x.OrderDetailId == 0)
|
.ToListAsync();
|
|
if (!unallocatedLocks.Any())
|
{
|
string error = $"未找到自动拆包创建的未分配锁定记录";
|
validationErrors.Add(error);
|
allValid = false;
|
_logger.LogError(error);
|
}
|
|
// 验证订单明细数据未改变
|
if (Math.Abs(refreshedOrderDetail.AllocatedQuantity - orderDetail.AllocatedQuantity) > 0.01m)
|
{
|
string error = $"订单明细分配数量异常变化!拆包前: {orderDetail.AllocatedQuantity}, 拆包后: {refreshedOrderDetail.AllocatedQuantity}";
|
validationErrors.Add(error);
|
allValid = false;
|
_logger.LogError(error);
|
}
|
|
// 验证原锁定记录数据未改变(分配数量不变)
|
if (Math.Abs(refreshedLockInfo.AssignQuantity - lockInfo.AssignQuantity) > 0.01m)
|
{
|
string error = $"锁定记录分配数量异常变化!拆包前: {lockInfo.AssignQuantity}, 拆包后: {refreshedLockInfo.AssignQuantity}";
|
validationErrors.Add(error);
|
allValid = false;
|
_logger.LogError(error);
|
}
|
|
// 新增】验证总库存守恒
|
// 拆包前总库存 = 原库存数量
|
// 拆包后总库存 = 原库存现有数量 + 新库存数量
|
decimal totalStockAfterSplit = refreshedStockDetail.StockQuantity;
|
if (splitRecords.Any() && !string.IsNullOrEmpty(splitRecords.First().NewBarcode))
|
{
|
var newStock = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == splitRecords.First().NewBarcode);
|
if (newStock != null)
|
{
|
totalStockAfterSplit += newStock.StockQuantity;
|
}
|
}
|
|
_logger.LogInformation($"库存守恒验证:");
|
_logger.LogInformation($" 拆包前总库存: {originalStockQtyBeforeSplit}");
|
_logger.LogInformation($" 拆包后总库存: {totalStockAfterSplit}");
|
_logger.LogInformation($" 差异: {originalStockQtyBeforeSplit - totalStockAfterSplit}");
|
|
// 允许很小的浮点数误差
|
if (Math.Abs(originalStockQtyBeforeSplit - totalStockAfterSplit) > 0.02m)
|
{
|
string error = $"库存不守恒!拆包前: {originalStockQtyBeforeSplit}, 拆包后总库存: {totalStockAfterSplit}";
|
validationErrors.Add(error);
|
allValid = false;
|
_logger.LogError(error);
|
}
|
|
// 汇总验证结果
|
if (allValid)
|
{
|
_logger.LogInformation($"✅ 自动拆包后验证全部通过");
|
}
|
else
|
{
|
string errorSummary = $"自动拆包后验证失败,发现{validationErrors.Count}个问题:" +
|
string.Join("; ", validationErrors.Take(3));
|
_logger.LogError(errorSummary);
|
|
// 记录详细问题到日志
|
for (int i = 0; i < validationErrors.Count; i++)
|
{
|
_logger.LogError($"问题{i + 1}: {validationErrors[i]}");
|
}
|
}
|
|
return allValid;
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"自动拆包后验证异常: {ex.Message}");
|
return false;
|
}
|
}
|
|
|
/// <summary>
|
/// 拣选后验证
|
/// </summary>
|
private async Task ValidateAfterPicking(string orderNo, string palletCode, string barcode, decimal pickedQty)
|
{
|
try
|
{
|
_logger.LogInformation($"开始拣选后验证");
|
|
// 验证库存明细
|
var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == barcode);
|
|
// 查找该条码的所有拣选记录
|
var pickingRecords = await Db.Queryable<Dt_PickingRecord>()
|
.Where(x => x.Barcode == barcode && x.OrderNo == orderNo && !x.IsCancelled)
|
.ToListAsync();
|
|
decimal totalPickedFromRecords = pickingRecords.Sum(x => x.PickQuantity);
|
|
_logger.LogInformation($"拣选验证 - 条码: {barcode}");
|
_logger.LogInformation($" 库存出库数量: {stockDetail.OutboundQuantity}");
|
_logger.LogInformation($" 拣选记录总和: {totalPickedFromRecords}");
|
|
if (Math.Abs(stockDetail.OutboundQuantity - totalPickedFromRecords) > 0.01m)
|
{
|
_logger.LogError($"拣选数据不一致!库存出库数量({stockDetail.OutboundQuantity}) ≠ 拣选记录总和({totalPickedFromRecords})");
|
|
// 自动修复:以拣选记录总和为准
|
decimal originalOutbound = stockDetail.OutboundQuantity;
|
stockDetail.OutboundQuantity = totalPickedFromRecords;
|
await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
|
|
_logger.LogWarning($"已自动修复出库数量: {originalOutbound} -> {totalPickedFromRecords}");
|
}
|
|
_logger.LogInformation($"拣选后验证完成");
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"拣选后验证失败: {ex.Message}");
|
}
|
}
|
|
|
/// <summary>
|
/// 取消分拣
|
/// </summary>
|
public async Task<WebResponseContent> CancelPicking(string orderNo, string palletCode, string barcode)
|
{
|
try
|
{
|
_unitOfWorkManage.BeginTran();
|
|
// 查找分拣记录
|
var pickingRecord = await Db.Queryable<Dt_PickingRecord>()
|
.Where(x => x.OrderNo == orderNo &&
|
x.PalletCode == palletCode &&
|
x.Barcode == barcode &&
|
!x.IsCancelled)
|
.OrderByDescending(x => x.PickTime)
|
.FirstAsync();
|
|
if (pickingRecord == null)
|
return WebResponseContent.Instance.Error("未找到分拣记录");
|
|
// 恢复锁定信息和库存
|
var revertResult = await RevertPickingData(pickingRecord);
|
|
// 更新批次和订单数据
|
await RevertBatchAndOrderData(pickingRecord, revertResult);
|
|
// 标记分拣记录为已取消
|
pickingRecord.IsCancelled = true;
|
pickingRecord.CancelTime = DateTime.Now;
|
pickingRecord.CancelOperator = App.User.UserName;
|
await Db.Updateable(pickingRecord).ExecuteCommandAsync();
|
|
_unitOfWorkManage.CommitTran();
|
|
return WebResponseContent.Instance.OK("取消分拣成功");
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
_logger.LogError($"取消分拣失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}");
|
return WebResponseContent.Instance.Error($"取消分拣失败:{ex.Message}");
|
}
|
}
|
#endregion
|
|
|
#region 取走空箱逻辑
|
/// <summary>
|
/// 清理库存信息 - 完整修正版
|
/// 确保OutboundQuantity正确清零
|
/// </summary>
|
private async Task CleanupStockInfo(Dt_OutStockLockInfo lockInfo)
|
{
|
try
|
{
|
_logger.LogInformation($"清理库存信息 - 锁定ID: {lockInfo.Id}, 条码: {lockInfo.CurrentBarcode}");
|
|
// 查找锁定的库存明细
|
var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == lockInfo.CurrentBarcode && x.StockId == lockInfo.StockId);
|
|
if (stockDetail != null)
|
{
|
// 记录清理前的状态
|
decimal originalStockQty = stockDetail.StockQuantity;
|
decimal originalOutboundQty = stockDetail.OutboundQuantity;
|
int originalStatus = stockDetail.Status;
|
|
_logger.LogInformation($"清理前状态 - 库存: {originalStockQty}, 出库: {originalOutboundQty}, 状态: {GetStockStatusName(originalStatus)}");
|
|
// 【重要】检查库存数量是否应该为0
|
if (lockInfo.Status == (int)OutLockStockStatusEnum.拣选完成)
|
{
|
if (stockDetail.StockQuantity > 0)
|
{
|
_logger.LogWarning($"拣选完成但库存不为0 - 条码: {stockDetail.Barcode}, 库存: {stockDetail.StockQuantity}");
|
}
|
}
|
|
// 清理库存和出库数量
|
stockDetail.StockQuantity = 0;
|
stockDetail.OutboundQuantity = 0;
|
stockDetail.Status = (int)StockStatusEmun.已清理;
|
|
await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
|
|
_logger.LogInformation($"清理库存明细完成");
|
_logger.LogInformation($" 库存数量: {originalStockQty} -> 0");
|
_logger.LogInformation($" 出库数量: {originalOutboundQty} -> 0");
|
_logger.LogInformation($" 状态: {GetStockStatusName(originalStatus)} -> 已清理");
|
}
|
else
|
{
|
_logger.LogWarning($"未找到条码对应的库存明细: {lockInfo.CurrentBarcode}");
|
}
|
|
// 清理该托盘上的所有库存(避免遗漏)
|
var allStockDetails = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.Where(x => x.StockId == lockInfo.StockId && x.StockQuantity > 0)
|
.ToListAsync();
|
|
if (allStockDetails.Any())
|
{
|
_logger.LogInformation($"清理托盘上其他库存 - 共 {allStockDetails.Count} 条记录");
|
|
foreach (var stock in allStockDetails)
|
{
|
decimal originalQty = stock.StockQuantity;
|
decimal originalOutbound = stock.OutboundQuantity;
|
|
stock.StockQuantity = 0;
|
stock.OutboundQuantity = 0;
|
stock.Status = (int)StockStatusEmun.已清理;
|
|
await _stockInfoDetailService.Db.Updateable(stock).ExecuteCommandAsync();
|
_logger.LogInformation($"清理遗漏库存 - 条码: {stock.Barcode}, 库存: {originalQty}->0, 出库: {originalOutbound}->0");
|
}
|
}
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"清理库存信息失败 - 锁定ID: {lockInfo.Id}, 条码: {lockInfo.CurrentBarcode}, Error: {ex.Message}");
|
// 不抛出异常,继续处理其他记录
|
}
|
}
|
|
/// <summary>
|
/// 更新订单状态
|
/// </summary>
|
private async Task UpdateOrderStatusAfterPalletRemoval(string orderNo)
|
{
|
// 检查订单是否所有托盘都已完成
|
var allLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo)
|
.ToListAsync();
|
|
var unfinishedPallets = allLocks
|
.GroupBy(x => x.PalletCode)
|
.Where(g => g.Any(x => x.Status == (int)OutLockStockStatusEnum.出库中 ||
|
x.Status == (int)OutLockStockStatusEnum.回库中))
|
.ToList();
|
|
// 如果没有未完成的托盘,更新订单状态为出库完成
|
if (!unfinishedPallets.Any())
|
{
|
await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
|
.SetColumns(x => new Dt_OutboundOrder
|
{
|
OrderStatus = (int)OutOrderStatusEnum.出库完成,
|
})
|
.Where(x => x.OrderNo == orderNo)
|
.ExecuteCommandAsync();
|
}
|
}
|
|
/// <summary>
|
/// 记录空箱取走历史
|
/// </summary>
|
private async Task RecordEmptyPalletRemoval(string orderNo, string palletCode, List<Dt_OutStockLockInfo> completedLocks)
|
{
|
//var removalRecord = new Dt_EmptyPalletRemoval
|
//{
|
// OrderNo = orderNo,
|
// PalletCode = palletCode,
|
// RemovalTime = DateTime.Now,
|
// Operator = App.User.UserName,
|
// CompletedItemsCount = completedLocks.Count,
|
// TotalPickedQuantity = completedLocks.Sum(x => x.PickedQty)
|
//};
|
|
//await Db.Insertable(removalRecord).ExecuteCommandAsync();
|
}
|
|
#endregion
|
|
private List<SplitResult> CreateSplitResults(Dt_OutStockLockInfo lockInfo, decimal splitQty, decimal remainQty, string newBarcode, string originalBarcode)
|
{
|
return new List<SplitResult>
|
{
|
new SplitResult
|
{
|
materialCode = lockInfo.MaterielCode,
|
supplierCode = lockInfo.SupplyCode,
|
quantityTotal = splitQty.ToString("F2"),
|
batchNumber = newBarcode,
|
batch = lockInfo.BatchNo,
|
factory = lockInfo.FactoryArea,
|
date = DateTime.Now.ToString("yyyy-MM-dd"),
|
},
|
new SplitResult
|
{
|
materialCode = lockInfo.MaterielCode,
|
supplierCode = lockInfo.SupplyCode,
|
quantityTotal = remainQty.ToString("F2"),
|
batchNumber = originalBarcode,
|
batch = lockInfo.BatchNo,
|
factory = lockInfo.FactoryArea,
|
date = DateTime.Now.ToString("yyyy-MM-dd"),
|
}
|
};
|
}
|
|
|
#region 手动拆包
|
|
/// <summary>
|
/// 手动拆包
|
/// </summary>
|
public async Task<WebResponseContent> ManualSplitPackage(string orderNo, string palletCode, string originalBarcode, decimal splitQuantity)
|
{
|
try
|
{
|
_unitOfWorkManage.BeginTran();
|
|
// 验证拆包请求
|
var validationResult = await ValidateSplitRequest(orderNo, palletCode, originalBarcode, splitQuantity);
|
if (!validationResult.IsValid)
|
return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
|
|
var (lockInfo, stockDetail) = validationResult.Data;
|
|
// 执行拆包逻辑
|
var splitResult = await ExecuteManualSplitLogic(lockInfo, stockDetail, splitQuantity, palletCode);
|
|
_unitOfWorkManage.CommitTran();
|
|
return WebResponseContent.Instance.OK("手动拆包成功", splitResult);
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
_logger.LogError($"手动拆包失败 - OrderNo: {orderNo}, Barcode: {originalBarcode}, Error: {ex.Message}");
|
return WebResponseContent.Instance.Error($"手动拆包失败:{ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 验证拆包请求 - 增强分配数量控制
|
/// </summary>
|
private async Task<ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateSplitRequest(
|
string orderNo, string palletCode, string originalBarcode, decimal splitQuantity)
|
{
|
_logger.LogInformation($"开始验证拆包请求 - 订单: {orderNo}, 托盘: {palletCode}, 原条码: {originalBarcode}, 拆包数量: {splitQuantity}");
|
|
// 查找锁定信息
|
var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo &&
|
x.PalletCode == palletCode &&
|
x.CurrentBarcode == originalBarcode &&
|
x.Status == (int)OutLockStockStatusEnum.出库中)
|
.FirstAsync();
|
|
if (lockInfo == null)
|
{
|
_logger.LogWarning($"未找到有效的锁定信息 - 订单: {orderNo}, 托盘: {palletCode}, 条码: {originalBarcode}");
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到有效的锁定信息");
|
}
|
|
_logger.LogInformation($"找到锁定信息 - 分配数量: {lockInfo.AssignQuantity}, 已拣选: {lockInfo.PickedQty}, 订单数量: {lockInfo.OrderQuantity}");
|
|
// 获取订单明细
|
var orderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
|
.FirstAsync(x => x.Id == lockInfo.OrderDetailId);
|
|
if (orderDetail == null)
|
{
|
_logger.LogWarning($"未找到订单明细 - OrderDetailId: {lockInfo.OrderDetailId}");
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到订单明细");
|
}
|
|
_logger.LogInformation($"找到订单明细 - 已分配数量: {orderDetail.AllocatedQuantity}, 锁定数量: {orderDetail.LockQuantity}");
|
|
// 获取库存信息
|
var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == originalBarcode && x.StockId == lockInfo.StockId);
|
|
if (stockDetail == null)
|
{
|
_logger.LogWarning($"未找到库存信息 - 条码: {originalBarcode}, StockId: {lockInfo.StockId}");
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到对应的库存信息");
|
}
|
|
_logger.LogInformation($"找到库存信息 - 库存数量: {stockDetail.StockQuantity}, 出库数量: {stockDetail.OutboundQuantity}");
|
|
// 验证拆包数量不能大于库存数量
|
if (stockDetail.StockQuantity < splitQuantity)
|
{
|
_logger.LogWarning($"拆包数量大于库存数量 - 拆包数量: {splitQuantity}, 库存数量: {stockDetail.StockQuantity}");
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error($"拆包数量不能大于库存数量,当前库存:{stockDetail.StockQuantity}");
|
}
|
|
// 验证拆包数量不能大于锁定信息的分配数量
|
if (lockInfo.AssignQuantity < splitQuantity)
|
{
|
_logger.LogWarning($"拆包数量大于分配数量 - 拆包数量: {splitQuantity}, 分配数量: {lockInfo.AssignQuantity}");
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error($"拆包数量不能大于分配数量,当前分配数量:{lockInfo.AssignQuantity}");
|
}
|
|
// 验证拆包数量不能大于锁定信息的未拣选数量
|
decimal remainingToPick = lockInfo.AssignQuantity - lockInfo.PickedQty;
|
if (remainingToPick < splitQuantity)
|
{
|
_logger.LogWarning($"拆包数量大于未拣选数量 - 拆包数量: {splitQuantity}, 未拣选数量: {remainingToPick}");
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error($"拆包数量不能大于未拣选数量,当前未拣选:{remainingToPick}");
|
}
|
|
// 验证拆包后原锁定信息的分配数量不会为负数
|
if (lockInfo.AssignQuantity - splitQuantity < 0)
|
{
|
_logger.LogWarning($"拆包后分配数量为负数 - 当前分配数量: {lockInfo.AssignQuantity}, 拆包数量: {splitQuantity}");
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error($"拆包后分配数量不能为负数");
|
}
|
|
// 验证订单明细的分配数量是否足够
|
// 注意:手动拆包不会改变订单明细的分配数量,因为总分配数量不变
|
// 只是从一个锁定信息转移到另一个锁定信息
|
decimal totalLockAssignQuantity = await GetTotalLockAssignQuantity(orderDetail.Id);
|
if (orderDetail.AllocatedQuantity != totalLockAssignQuantity)
|
{
|
_logger.LogWarning($"订单明细分配数量与锁定信息不一致 - 订单明细分配数量: {orderDetail.AllocatedQuantity}, 锁定信息总分配数量: {totalLockAssignQuantity}");
|
}
|
|
_logger.LogInformation($"拆包验证通过 - 原条码: {originalBarcode}, 拆包数量: {splitQuantity}");
|
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Success((lockInfo, stockDetail));
|
}
|
|
/// <summary>
|
/// 获取订单明细的所有锁定信息的总分配数量
|
/// </summary>
|
private async Task<decimal> GetTotalLockAssignQuantity(long orderDetailId)
|
{
|
var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderDetailId == orderDetailId)
|
.ToListAsync();
|
|
return lockInfos.Sum(x => x.AssignQuantity);
|
}
|
|
|
|
/// <summary>
|
/// 执行手动拆包逻辑 - 修复版本
|
/// </summary>
|
private async Task<List<SplitResult>> ExecuteManualSplitLogic(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail,
|
decimal splitQuantity, string palletCode)
|
{
|
_logger.LogInformation($"开始执行手动拆包逻辑 - 原条码: {stockDetail.Barcode}, 拆包数量: {splitQuantity}");
|
|
try
|
{
|
// 验证库存数量是否足够
|
decimal availableStock = stockDetail.StockQuantity;
|
if (availableStock < splitQuantity)
|
{
|
throw new InvalidOperationException($"库存不足,当前库存: {availableStock}, 需要拆包: {splitQuantity}");
|
}
|
|
// 验证拆包数量不能等于或大于原锁定信息分配数量
|
if (splitQuantity >= lockInfo.AssignQuantity)
|
{
|
throw new InvalidOperationException($"拆包数量不能等于或大于原锁定信息分配数量,拆包数量: {splitQuantity}, 原分配数量: {lockInfo.AssignQuantity}");
|
}
|
|
// 计算剩余数量
|
decimal remainQty = lockInfo.AssignQuantity - splitQuantity;
|
|
// 验证剩余数量是否合理
|
if (remainQty <= 0)
|
{
|
throw new InvalidOperationException($"拆包后剩余数量必须大于0,当前剩余: {remainQty}");
|
}
|
|
_logger.LogInformation($"拆包计算 - 原分配数量: {lockInfo.AssignQuantity}, 拆包数量: {splitQuantity}, 剩余数量: {remainQty}");
|
_logger.LogInformation($"原库存信息 - 库存数量: {stockDetail.StockQuantity}, 出库数量: {stockDetail.OutboundQuantity}");
|
|
// 生成新条码
|
string newBarcode = await GenerateNewBarcode();
|
_logger.LogInformation($"生成新条码: {newBarcode}");
|
|
// 记录拆包前的关键数据
|
decimal originalLockAssignQty = lockInfo.AssignQuantity;
|
decimal originalLockOrderQty = lockInfo.OrderQuantity;
|
decimal originalStockQty = stockDetail.StockQuantity;
|
decimal originalOutboundQty = stockDetail.OutboundQuantity;
|
|
// 修复:计算新库存明细的正确数量
|
// 新条码应该只有拆包数量,而不是2倍
|
decimal newStockQuantity = splitQuantity;
|
decimal originalRemainingStockQuantity = originalStockQty - splitQuantity;
|
|
_logger.LogInformation($"数量分配 - 新条码数量: {newStockQuantity}, 原条码剩余数量: {originalRemainingStockQuantity}");
|
|
// 创建新库存明细 - 修复数量问题
|
var newStockDetail = new Dt_StockInfoDetail
|
{
|
StockId = stockDetail.StockId,
|
MaterielCode = stockDetail.MaterielCode,
|
OrderNo = stockDetail.OrderNo,
|
BatchNo = stockDetail.BatchNo,
|
StockQuantity = newStockQuantity, // 修复:使用正确的拆包数量
|
OutboundQuantity = 0, // 新条码初始出库数量为0
|
Barcode = newBarcode,
|
Status = (int)StockStatusEmun.出库锁定,
|
SupplyCode = stockDetail.SupplyCode,
|
Unit = stockDetail.Unit,
|
BarcodeQty = stockDetail.BarcodeQty,
|
BarcodeUnit = stockDetail.BarcodeUnit,
|
BusinessType = stockDetail.BusinessType,
|
InboundOrderRowNo = stockDetail.InboundOrderRowNo,
|
};
|
|
await _stockInfoDetailService.Db.Insertable(newStockDetail).ExecuteCommandAsync();
|
_logger.LogInformation($"创建新库存明细成功 - 条码: {newBarcode}, 库存数量: {newStockQuantity}");
|
|
// 更新原库存明细
|
stockDetail.StockQuantity = originalRemainingStockQuantity;
|
|
// 确保不会为负数
|
if (stockDetail.StockQuantity < 0)
|
{
|
_logger.LogWarning($"原库存数量出现负数,重置为0");
|
stockDetail.StockQuantity = 0;
|
}
|
|
await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
|
_logger.LogInformation($"更新原库存明细 - 条码: {stockDetail.Barcode}, " +
|
$"库存数量: {originalStockQty} -> {stockDetail.StockQuantity}, " +
|
$"出库数量: {originalOutboundQty} -> {stockDetail.OutboundQuantity}");
|
|
// 创建新锁定信息
|
var newLockInfo = new Dt_OutStockLockInfo
|
{
|
OrderNo = lockInfo.OrderNo,
|
OrderDetailId = lockInfo.OrderDetailId,
|
OutboundBatchNo = lockInfo.OutboundBatchNo,
|
MaterielCode = lockInfo.MaterielCode,
|
MaterielName = lockInfo.MaterielName,
|
StockId = lockInfo.StockId,
|
OrderQuantity = splitQuantity,
|
AssignQuantity = splitQuantity,
|
PickedQty = 0,
|
LocationCode = lockInfo.LocationCode,
|
PalletCode = lockInfo.PalletCode,
|
TaskNum = lockInfo.TaskNum,
|
Status = (int)OutLockStockStatusEnum.出库中,
|
Unit = lockInfo.Unit,
|
SupplyCode = lockInfo.SupplyCode,
|
OrderType = lockInfo.OrderType,
|
CurrentBarcode = newBarcode,
|
IsSplitted = 1,
|
ParentLockId = lockInfo.Id,
|
Operator = App.User.UserName,
|
FactoryArea = lockInfo.FactoryArea,
|
lineNo = lockInfo.lineNo,
|
WarehouseCode = lockInfo.WarehouseCode,
|
BarcodeQty = lockInfo.BarcodeQty,
|
BarcodeUnit = lockInfo.BarcodeUnit,
|
};
|
|
await _outStockLockInfoService.Db.Insertable(newLockInfo).ExecuteCommandAsync();
|
_logger.LogInformation($"创建新锁定信息成功 - 条码: {newBarcode}, 分配数量: {splitQuantity}");
|
|
// 更新原锁定信息
|
lockInfo.AssignQuantity = remainQty;
|
lockInfo.OrderQuantity = remainQty;
|
|
await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
|
_logger.LogInformation($"更新原锁定信息 - 分配数量: {originalLockAssignQty} -> {lockInfo.AssignQuantity}, " +
|
$"订单数量: {originalLockOrderQty} -> {lockInfo.OrderQuantity}");
|
|
// 验证拆包后数据一致性
|
await ValidateDataConsistencyAfterSplit(lockInfo.OrderDetailId, originalLockAssignQty, originalLockOrderQty);
|
|
// 记录拆包历史
|
await RecordSplitHistory(lockInfo, stockDetail, splitQuantity, newBarcode, false, originalStockQty);
|
|
// 创建拆包结果列表
|
var splitResults = CreateSplitResults(lockInfo, splitQuantity, remainQty, newBarcode, stockDetail.Barcode);
|
|
_logger.LogInformation($"手动拆包逻辑执行完成 - 原条码: {stockDetail.Barcode}, 新条码: {newBarcode}, 拆包数量: {splitQuantity}");
|
|
return splitResults;
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"手动拆包逻辑执行失败 - 原条码: {stockDetail.Barcode}, 拆包数量: {splitQuantity}, Error: {ex.Message}");
|
throw;
|
}
|
}
|
|
/// <summary>
|
/// 验证拆包后数据一致性
|
/// </summary>
|
private async Task ValidateDataConsistencyAfterSplit(long orderDetailId, decimal expectedAllocatedQty, decimal expectedLockQty)
|
{
|
// 重新获取订单明细数据
|
var orderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
|
.FirstAsync(x => x.Id == orderDetailId);
|
|
if (orderDetail == null)
|
return;
|
|
// 计算所有锁定信息的总分配数量
|
var allLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderDetailId == orderDetailId)
|
.ToListAsync();
|
|
decimal totalLockAssignQty = allLocks.Sum(x => x.AssignQuantity);
|
|
_logger.LogInformation($"数据一致性验证 - 订单明细分配数量: {orderDetail.AllocatedQuantity}, 锁定信息总分配数量: {totalLockAssignQty}");
|
|
// 如果数据不一致,记录警告
|
if (Math.Abs(orderDetail.AllocatedQuantity - totalLockAssignQty) > 0.01m)
|
{
|
_logger.LogWarning($"数据不一致 - 订单明细分配数量: {orderDetail.AllocatedQuantity}, 锁定信息总分配数量: {totalLockAssignQty}");
|
}
|
|
// 验证分配数量没有异常变化
|
if (Math.Abs(orderDetail.AllocatedQuantity - expectedAllocatedQty) > 0.01m)
|
{
|
_logger.LogWarning($"分配数量异常变化 - 期望: {expectedAllocatedQty}, 实际: {orderDetail.AllocatedQuantity}");
|
}
|
|
if (Math.Abs(orderDetail.LockQuantity - expectedLockQty) > 0.01m)
|
{
|
_logger.LogWarning($"锁定数量异常变化 - 期望: {expectedLockQty}, 实际: {orderDetail.LockQuantity}");
|
}
|
}
|
#endregion
|
|
#region 取消拆包
|
|
/// <summary>
|
/// 取消拆包
|
/// </summary>
|
public async Task<WebResponseContent> CancelSplitPackage(string orderNo, string palletCode, string newBarcode)
|
{
|
try
|
{
|
_unitOfWorkManage.BeginTran();
|
|
// 查找拆包记录并验证
|
var validationResult = await ValidateCancelSplitRequest(orderNo, palletCode, newBarcode);
|
if (!validationResult.IsValid)
|
return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
|
|
var (splitRecord, newLockInfo, newStockDetail) = validationResult.Data;
|
|
// 查找原始锁定信息
|
var originalLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.FirstAsync(x => x.Id == splitRecord.OutStockLockInfoId);
|
|
// 检查该条码是否被再次拆包
|
var childSplitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
|
.Where(x => x.OriginalBarcode == newBarcode && !x.IsReverted)
|
.ToListAsync();
|
|
if (childSplitRecords.Any())
|
{
|
return WebResponseContent.Instance.Error("该条码已被再次拆包,请先取消后续的拆包操作");
|
}
|
|
// 执行取消拆包逻辑
|
await ExecuteCancelSplitLogic(splitRecord, originalLockInfo, newLockInfo, newStockDetail);
|
|
_unitOfWorkManage.CommitTran();
|
|
return WebResponseContent.Instance.OK("取消拆包成功");
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
_logger.LogError($"取消拆包失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Barcode: {newBarcode}, Error: {ex.Message}");
|
return WebResponseContent.Instance.Error($"取消拆包失败:{ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 执行取消拆包逻辑
|
/// </summary>
|
private async Task ExecuteCancelSplitLogic(Dt_SplitPackageRecord splitRecord,
|
Dt_OutStockLockInfo originalLockInfo, Dt_OutStockLockInfo newLockInfo,
|
Dt_StockInfoDetail newStockDetail)
|
{
|
_logger.LogInformation($"开始执行取消拆包逻辑 - 原条码: {splitRecord.OriginalBarcode}, 新条码: {splitRecord.NewBarcode}, 拆包数量: {splitRecord.SplitQty}");
|
|
try
|
{
|
// 根据拆包类型调用不同的处理方法
|
if (splitRecord.IsAutoSplit)
|
{
|
await HandleAutoSplitCancel(splitRecord, originalLockInfo, newLockInfo, newStockDetail);
|
}
|
else
|
{
|
await HandleManualSplitCancel(splitRecord, originalLockInfo, newLockInfo, newStockDetail);
|
}
|
|
// 验证取消拆包后数据一致性
|
await ValidateDataConsistencyAfterCancelSplit(originalLockInfo.OrderDetailId,
|
originalLockInfo.AssignQuantity, originalLockInfo.OrderQuantity,
|
splitRecord.IsAutoSplit, splitRecord.SplitQty);
|
|
// 检查并更新批次和订单状态
|
await CheckAndUpdateBatchStatus(originalLockInfo.BatchNo);
|
await CheckAndUpdateOrderStatus(originalLockInfo.OrderNo);
|
|
_logger.LogInformation($"取消拆包逻辑执行完成");
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"取消拆包逻辑执行失败: {ex.Message}");
|
throw;
|
}
|
}
|
|
/// <summary>
|
/// 处理自动拆包取消
|
/// </summary>
|
private async Task HandleAutoSplitCancel(Dt_SplitPackageRecord splitRecord,
|
Dt_OutStockLockInfo originalLockInfo, Dt_OutStockLockInfo newLockInfo,
|
Dt_StockInfoDetail newStockDetail)
|
{
|
_logger.LogInformation($"处理自动拆包取消 - 原条码: {splitRecord.OriginalBarcode}, 新条码: {splitRecord.NewBarcode}");
|
|
// 获取订单明细
|
var orderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
|
.FirstAsync(x => x.Id == originalLockInfo.OrderDetailId);
|
|
if (orderDetail == null)
|
throw new InvalidOperationException("未找到订单明细");
|
|
// 恢复订单明细的分配数量
|
decimal originalAllocatedQty = orderDetail.AllocatedQuantity;
|
decimal originalLockQty = orderDetail.LockQuantity;
|
|
orderDetail.AllocatedQuantity -= splitRecord.SplitQty;
|
orderDetail.LockQuantity -= splitRecord.SplitQty;
|
|
// 边界检查
|
if (orderDetail.AllocatedQuantity < 0) orderDetail.AllocatedQuantity = 0;
|
if (orderDetail.LockQuantity < 0) orderDetail.LockQuantity = 0;
|
|
await _outboundOrderDetailService.Db.Updateable(orderDetail).ExecuteCommandAsync();
|
_logger.LogInformation($"自动拆包取消恢复订单明细 - 分配数量: {originalAllocatedQty} -> {orderDetail.AllocatedQuantity}");
|
|
// 恢复原库存
|
var originalStock = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == splitRecord.OriginalBarcode && x.StockId == splitRecord.StockId);
|
|
if (originalStock != null)
|
{
|
decimal originalStockQty = originalStock.StockQuantity;
|
originalStock.StockQuantity += splitRecord.SplitQty;
|
|
// 如果原库存状态是出库完成,恢复为出库锁定
|
if (originalStock.Status == (int)StockStatusEmun.出库完成)
|
{
|
originalStock.Status = (int)StockStatusEmun.出库锁定;
|
}
|
|
await _stockInfoDetailService.Db.Updateable(originalStock).ExecuteCommandAsync();
|
_logger.LogInformation($"自动拆包取消恢复原库存 - 条码: {originalStock.Barcode}, 数量: {originalStockQty} -> {originalStock.StockQuantity}");
|
}
|
|
// 删除新锁定信息和库存明细
|
await DeleteNewSplitRecords(newLockInfo, newStockDetail);
|
}
|
|
/// <summary>
|
/// 处理手动拆包取消
|
/// </summary>
|
private async Task HandleManualSplitCancel(Dt_SplitPackageRecord splitRecord,
|
Dt_OutStockLockInfo originalLockInfo, Dt_OutStockLockInfo newLockInfo,
|
Dt_StockInfoDetail newStockDetail)
|
{
|
_logger.LogInformation($"处理手动拆包取消 - 原条码: {splitRecord.OriginalBarcode}, 新条码: {splitRecord.NewBarcode}");
|
|
// 恢复原锁定信息的分配数量
|
decimal originalAssignQty = originalLockInfo.AssignQuantity;
|
decimal originalOrderQty = originalLockInfo.OrderQuantity;
|
|
originalLockInfo.AssignQuantity += splitRecord.SplitQty;
|
originalLockInfo.OrderQuantity += splitRecord.SplitQty;
|
|
// 恢复状态
|
if (originalLockInfo.Status == (int)OutLockStockStatusEnum.拣选完成)
|
{
|
originalLockInfo.Status = (int)OutLockStockStatusEnum.出库中;
|
}
|
|
await _outStockLockInfoService.Db.Updateable(originalLockInfo).ExecuteCommandAsync();
|
_logger.LogInformation($"手动拆包取消恢复原锁定信息 - 分配数量: {originalAssignQty} -> {originalLockInfo.AssignQuantity}");
|
|
// 恢复原库存明细
|
var originalStock = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == splitRecord.OriginalBarcode && x.StockId == splitRecord.StockId);
|
|
if (originalStock != null)
|
{
|
decimal originalStockQty = originalStock.StockQuantity;
|
originalStock.StockQuantity += splitRecord.SplitQty;
|
|
// 恢复库存状态
|
if (originalStock.Status == (int)StockStatusEmun.出库完成)
|
{
|
originalStock.Status = (int)StockStatusEmun.出库锁定;
|
}
|
|
await _stockInfoDetailService.Db.Updateable(originalStock).ExecuteCommandAsync();
|
_logger.LogInformation($"手动拆包取消恢复原库存 - 条码: {originalStock.Barcode}, 数量: {originalStockQty} -> {originalStock.StockQuantity}");
|
}
|
|
// 删除新锁定信息和库存明细
|
await DeleteNewSplitRecords(newLockInfo, newStockDetail);
|
}
|
|
/// <summary>
|
/// 删除新拆包记录相关的数据
|
/// </summary>
|
private async Task DeleteNewSplitRecords(Dt_OutStockLockInfo newLockInfo, Dt_StockInfoDetail newStockDetail)
|
{
|
// 删除新锁定信息
|
if (newLockInfo != null)
|
{
|
_logger.LogInformation($"删除新锁定信息 - 条码: {newLockInfo.CurrentBarcode}, 分配数量: {newLockInfo.AssignQuantity}");
|
await _outStockLockInfoService.Db.Deleteable<Dt_OutStockLockInfo>()
|
.Where(x => x.Id == newLockInfo.Id)
|
.ExecuteCommandAsync();
|
}
|
|
// 删除新库存明细
|
if (newStockDetail != null)
|
{
|
_logger.LogInformation($"删除新库存明细 - 条码: {newStockDetail.Barcode}, 库存数量: {newStockDetail.StockQuantity}");
|
await _stockInfoDetailService.Db.Deleteable<Dt_StockInfoDetail>()
|
.Where(x => x.Barcode == newStockDetail.Barcode)
|
.ExecuteCommandAsync();
|
}
|
}
|
|
/// <summary>
|
/// 验证取消拆包后数据一致性 - 最新版本
|
/// </summary>
|
private async Task ValidateDataConsistencyAfterCancelSplit(long orderDetailId, decimal originalAllocatedQty, decimal originalLockQty, bool isAutoSplit, decimal splitQuantity)
|
{
|
// 重新获取订单明细数据
|
var orderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
|
.FirstAsync(x => x.Id == orderDetailId);
|
|
if (orderDetail == null)
|
return;
|
|
// 计算所有锁定信息的总分配数量
|
var allLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderDetailId == orderDetailId)
|
.ToListAsync();
|
|
decimal totalLockAssignQty = allLocks.Sum(x => x.AssignQuantity);
|
|
_logger.LogInformation($"取消拆包后数据一致性验证 - 订单明细分配数量: {orderDetail.AllocatedQuantity}, 锁定信息总分配数量: {totalLockAssignQty}");
|
|
// 根据拆包类型计算期望值
|
decimal expectedAllocatedQty;
|
decimal expectedLockQty;
|
|
if (isAutoSplit)
|
{
|
// 自动拆包取消:分配数量应该减少拆包数量
|
expectedAllocatedQty = originalAllocatedQty - splitQuantity;
|
expectedLockQty = originalLockQty - splitQuantity;
|
|
_logger.LogInformation($"取消自动拆包期望值 - 分配数量: {expectedAllocatedQty}, 锁定数量: {expectedLockQty}");
|
}
|
else
|
{
|
// 手动拆包取消:分配数量应该保持不变
|
expectedAllocatedQty = originalAllocatedQty;
|
expectedLockQty = originalLockQty;
|
|
_logger.LogInformation($"取消手动拆包期望值 - 分配数量: {expectedAllocatedQty}, 锁定数量: {expectedLockQty}");
|
}
|
|
// 边界检查:确保期望值不为负数
|
if (expectedAllocatedQty < 0)
|
{
|
_logger.LogWarning($"期望分配数量为负数,重置为0。计算值: {expectedAllocatedQty}");
|
expectedAllocatedQty = 0;
|
}
|
|
if (expectedLockQty < 0)
|
{
|
_logger.LogWarning($"期望锁定数量为负数,重置为0。计算值: {expectedLockQty}");
|
expectedLockQty = 0;
|
}
|
|
// 验证分配数量
|
if (Math.Abs(orderDetail.AllocatedQuantity - expectedAllocatedQty) > 0.01m)
|
{
|
_logger.LogWarning($"取消拆包后分配数量异常 - 期望: {expectedAllocatedQty}, 实际: {orderDetail.AllocatedQuantity}");
|
}
|
|
// 验证锁定数量
|
if (Math.Abs(orderDetail.LockQuantity - expectedLockQty) > 0.01m)
|
{
|
_logger.LogWarning($"取消拆包后锁定数量异常 - 期望: {expectedLockQty}, 实际: {orderDetail.LockQuantity}");
|
}
|
|
// 验证订单明细分配数量与锁定信息总分配数量的一致性
|
if (Math.Abs(orderDetail.AllocatedQuantity - totalLockAssignQty) > 0.01m)
|
{
|
_logger.LogWarning($"取消拆包后数据不一致 - 订单明细分配数量: {orderDetail.AllocatedQuantity}, 锁定信息总分配数量: {totalLockAssignQty}");
|
}
|
|
// 记录详细的一致性报告
|
_logger.LogInformation($"取消拆包数据一致性报告 - " +
|
$"订单明细分配数量: {orderDetail.AllocatedQuantity}, " +
|
$"订单明细锁定数量: {orderDetail.LockQuantity}, " +
|
$"锁定信息总分配数量: {totalLockAssignQty}, " +
|
$"拆包类型: {(isAutoSplit ? "自动" : "手动")}, " +
|
$"拆包数量: {splitQuantity}");
|
}
|
/// <summary>
|
/// 验证取消拆包请求
|
/// </summary>
|
private async Task<ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateCancelSplitRequest(
|
string orderNo, string palletCode, string newBarcode)
|
{
|
_logger.LogInformation($"开始验证取消拆包请求 - 订单: {orderNo}, 托盘: {palletCode}, 条码: {newBarcode}");
|
|
// 查找拆包记录
|
var splitRecord = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
|
.Where(x => x.NewBarcode == newBarcode &&
|
x.OrderNo == orderNo &&
|
!x.IsReverted)
|
.FirstAsync();
|
|
if (splitRecord == null)
|
return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到拆包记录");
|
|
_logger.LogInformation($"找到拆包记录 - 原条码: {splitRecord.OriginalBarcode}, 新条码: {splitRecord.NewBarcode}, 拆包数量: {splitRecord.SplitQty}");
|
|
// 查找新锁定信息
|
var newLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.CurrentBarcode == newBarcode &&
|
x.PalletCode == palletCode &&
|
x.OrderNo == orderNo)
|
.FirstAsync();
|
|
if (newLockInfo == null)
|
return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到新锁定信息");
|
|
_logger.LogInformation($"找到新锁定信息 - 状态: {newLockInfo.Status}, 已拣选: {newLockInfo.PickedQty}, 分配数量: {newLockInfo.AssignQuantity}");
|
|
// 检查新条码是否已被分拣
|
var newBarcodePickingRecords = await Db.Queryable<Dt_PickingRecord>()
|
.Where(x => x.Barcode == newBarcode && x.OrderNo == orderNo && !x.IsCancelled)
|
.ToListAsync();
|
|
if (newBarcodePickingRecords.Any())
|
{
|
var totalPickedQty = newBarcodePickingRecords.Sum(x => x.PickQuantity);
|
_logger.LogWarning($"新条码 {newBarcode} 已被分拣,总拣选数量: {totalPickedQty}");
|
return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error(
|
$"新条码已被分拣(已拣选数量:{totalPickedQty}),请先取消分拣,然后再取消拆包");
|
}
|
|
// 检查原条码是否已被分拣
|
var originalBarcodePickingRecords = await Db.Queryable<Dt_PickingRecord>()
|
.Where(x => x.Barcode == splitRecord.OriginalBarcode && x.OrderNo == orderNo && !x.IsCancelled)
|
.ToListAsync();
|
|
if (originalBarcodePickingRecords.Any())
|
{
|
var totalPickedQty = originalBarcodePickingRecords.Sum(x => x.PickQuantity);
|
_logger.LogWarning($"原条码 {splitRecord.OriginalBarcode} 已被分拣,总拣选数量: {totalPickedQty}");
|
return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error(
|
$"原条码已被分拣(已拣选数量:{totalPickedQty}),请先取消分拣,然后再取消拆包");
|
}
|
|
_logger.LogInformation($"新旧条码均未被分拣,可以取消拆包");
|
|
// 检查新条码是否被再次拆包
|
var childSplitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
|
.Where(x => x.OriginalBarcode == newBarcode && !x.IsReverted)
|
.ToListAsync();
|
|
if (childSplitRecords.Any())
|
{
|
var childBarcodes = string.Join(", ", childSplitRecords.Select(x => x.NewBarcode));
|
_logger.LogWarning($"条码 {newBarcode} 已被再次拆包,生成的新条码: {childBarcodes}");
|
return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error(
|
$"该条码已被再次拆包,生成的新条码:{childBarcodes},请先取消后续拆包");
|
}
|
|
var newStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == newBarcode);
|
|
if (newStockDetail == null)
|
return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到新库存明细");
|
|
_logger.LogInformation($"取消拆包验证通过 - 条码: {newBarcode}");
|
|
return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Success((splitRecord, newLockInfo, newStockDetail));
|
}
|
|
#endregion
|
|
#region 批量取消拆包链
|
|
/// <summary>
|
/// 批量取消拆包链 - 取消某个条码及其所有后续拆包
|
/// </summary>
|
public async Task<WebResponseContent> CancelSplitPackageChain(string orderNo, string palletCode, string startBarcode)
|
{
|
try
|
{
|
_unitOfWorkManage.BeginTran();
|
|
// 查找所有相关的拆包记录(形成拆包链)
|
var splitChain = await GetSplitPackageChain(orderNo, startBarcode);
|
|
if (!splitChain.Any())
|
return WebResponseContent.Instance.Error("未找到拆包记录");
|
|
_logger.LogInformation($"找到拆包链,共 {splitChain.Count} 条记录");
|
|
// 收集拆包链中涉及的所有条码(包括原条码和新条码)
|
var allBarcodesInChain = new List<string> { startBarcode };
|
allBarcodesInChain.AddRange(splitChain.Select(x => x.NewBarcode));
|
|
// 检查拆包链中是否有已被分拣的条码
|
var pickedBarcodesInfo = await GetPickedBarcodesInfo(orderNo, allBarcodesInChain);
|
|
if (pickedBarcodesInfo.Any())
|
{
|
var pickedBarcodes = string.Join(", ", pickedBarcodesInfo.Select(x => $"{x.Barcode}(已拣选{x.PickedQty})"));
|
return WebResponseContent.Instance.Error(
|
$"以下条码已被分拣,请先取消分拣:{pickedBarcodes}");
|
}
|
|
// 按拆包顺序倒序取消(从最新的开始取消)
|
var reversedChain = splitChain.OrderByDescending(x => x.SplitTime).ToList();
|
|
foreach (var splitRecord in reversedChain)
|
{
|
_logger.LogInformation($"取消拆包记录 - 原条码: {splitRecord.OriginalBarcode}, 新条码: {splitRecord.NewBarcode}");
|
await CancelSingleSplitPackage(splitRecord, palletCode);
|
}
|
|
_unitOfWorkManage.CommitTran();
|
|
return WebResponseContent.Instance.OK($"成功取消拆包链,共{reversedChain.Count}次拆包操作");
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
_logger.LogError($"取消拆包链失败 - OrderNo: {orderNo}, StartBarcode: {startBarcode}, Error: {ex.Message}");
|
return WebResponseContent.Instance.Error($"取消拆包链失败:{ex.Message}");
|
}
|
}
|
|
|
/// <summary>
|
/// 获取已被分拣的条码信息
|
/// </summary>
|
private async Task<List<PickedBarcodeInfo>> GetPickedBarcodesInfo(string orderNo, List<string> barcodes)
|
{
|
var pickedBarcodes = new List<PickedBarcodeInfo>();
|
|
foreach (var barcode in barcodes)
|
{
|
var pickingRecords = await Db.Queryable<Dt_PickingRecord>()
|
.Where(x => x.Barcode == barcode && x.OrderNo == orderNo && !x.IsCancelled)
|
.ToListAsync();
|
|
if (pickingRecords.Any())
|
{
|
var totalPickedQty = pickingRecords.Sum(x => x.PickQuantity);
|
pickedBarcodes.Add(new PickedBarcodeInfo
|
{
|
Barcode = barcode,
|
PickedQty = totalPickedQty,
|
PickRecordCount = pickingRecords.Count
|
});
|
}
|
}
|
|
return pickedBarcodes;
|
}
|
/// <summary>
|
/// 获取拆包链 - 查找某个条码的所有拆包记录(包括后续拆包)
|
/// </summary>
|
public async Task<List<Dt_SplitPackageRecord>> GetSplitPackageChain(string orderNo, string startBarcode)
|
{
|
var allSplitRecords = new List<Dt_SplitPackageRecord>();
|
var visitedBarcodes = new HashSet<string>(); // 防止循环引用
|
|
// 使用队列进行广度优先搜索
|
var queue = new Queue<string>();
|
queue.Enqueue(startBarcode);
|
visitedBarcodes.Add(startBarcode);
|
|
while (queue.Count > 0)
|
{
|
var currentBarcode = queue.Dequeue();
|
|
// 查找以当前条码为原条码的所有拆包记录
|
var splitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
|
.Where(x => x.OriginalBarcode == currentBarcode &&
|
x.OrderNo == orderNo &&
|
!x.IsReverted)
|
.ToListAsync();
|
|
foreach (var record in splitRecords)
|
{
|
// 避免重复处理
|
if (!visitedBarcodes.Contains(record.NewBarcode))
|
{
|
allSplitRecords.Add(record);
|
queue.Enqueue(record.NewBarcode);
|
visitedBarcodes.Add(record.NewBarcode);
|
}
|
}
|
}
|
|
return allSplitRecords;
|
}
|
|
/// <summary>
|
/// 取消单个拆包记录
|
/// </summary>
|
private async Task CancelSingleSplitPackage(Dt_SplitPackageRecord splitRecord, string palletCode)
|
{
|
_logger.LogInformation($"开始取消单个拆包记录 - 原条码: {splitRecord.OriginalBarcode}, 新条码: {splitRecord.NewBarcode}");
|
|
// 再次验证分拣状态(防止并发操作)
|
var newBarcodePickingRecords = await Db.Queryable<Dt_PickingRecord>()
|
.Where(x => x.Barcode == splitRecord.NewBarcode && !x.IsCancelled)
|
.ToListAsync();
|
|
if (newBarcodePickingRecords.Any())
|
{
|
throw new InvalidOperationException($"新条码 {splitRecord.NewBarcode} 在验证后被分拣,无法取消拆包");
|
}
|
|
var originalBarcodePickingRecords = await Db.Queryable<Dt_PickingRecord>()
|
.Where(x => x.Barcode == splitRecord.OriginalBarcode && !x.IsCancelled)
|
.ToListAsync();
|
|
if (originalBarcodePickingRecords.Any())
|
{
|
throw new InvalidOperationException($"原条码 {splitRecord.OriginalBarcode} 在验证后被分拣,无法取消拆包");
|
}
|
|
// 查找相关数据
|
var newLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.CurrentBarcode == splitRecord.NewBarcode &&
|
x.PalletCode == palletCode)
|
.FirstAsync();
|
|
var newStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == splitRecord.NewBarcode);
|
|
var originalLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.FirstAsync(x => x.Id == splitRecord.OutStockLockInfoId);
|
|
// 执行取消逻辑
|
await ExecuteCancelSplitLogic(splitRecord, originalLockInfo, newLockInfo, newStockDetail);
|
|
_logger.LogInformation($"取消单个拆包记录完成 - 原条码: {splitRecord.OriginalBarcode}, 新条码: {splitRecord.NewBarcode}");
|
}
|
#endregion
|
|
#region 拆包信息查询增强
|
|
/// <summary>
|
/// 获取拆包链信息
|
/// </summary>
|
public async Task<WebResponseContent> GetSplitPackageChainInfo(string orderNo, string barcode)
|
{
|
try
|
{
|
var splitChain = await GetSplitPackageChain(orderNo, barcode);
|
|
var chainInfo = new SplitPackageChainInfoDto
|
{
|
OriginalBarcode = barcode,
|
TotalSplitTimes = splitChain.Count,
|
SplitChain = splitChain.Select(x => new SplitChainItemDto
|
{
|
SplitTime = x.SplitTime,
|
OriginalBarcode = x.OriginalBarcode,
|
NewBarcode = x.NewBarcode,
|
SplitQuantity = x.SplitQty,
|
Operator = x.Operator,
|
IsReverted = x.IsReverted,
|
IsAutoSplit = x.IsAutoSplit
|
}).ToList()
|
};
|
|
return WebResponseContent.Instance.OK("获取成功", chainInfo);
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"获取拆包链信息失败 - OrderNo: {orderNo}, Barcode: {barcode}, Error: {ex.Message}");
|
return WebResponseContent.Instance.Error("获取拆包链信息失败");
|
}
|
}
|
|
|
/// <summary>
|
/// 查找根条码
|
/// </summary>
|
public async Task<string> FindRootBarcode(string orderNo, string startBarcode)
|
{
|
var currentBarcode = startBarcode;
|
var visited = new HashSet<string>();
|
|
while (!string.IsNullOrEmpty(currentBarcode) && !visited.Contains(currentBarcode))
|
{
|
visited.Add(currentBarcode);
|
|
// 查找当前条码是否是由其他条码拆包而来
|
var parentRecord = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
|
.Where(x => x.NewBarcode == currentBarcode &&
|
x.OrderNo == orderNo &&
|
!x.IsReverted)
|
.FirstAsync();
|
|
if (parentRecord == null)
|
{
|
// 没有父级拆包记录,说明这是根条码
|
return currentBarcode;
|
}
|
|
currentBarcode = parentRecord.OriginalBarcode;
|
}
|
|
// 如果出现循环引用,返回起始条码
|
return startBarcode;
|
}
|
#endregion
|
|
#region 更新批次状态检查
|
|
/// <summary>
|
/// 检查并更新批次状态
|
/// </summary>
|
private async Task CheckAndUpdateBatchStatus(string batchNo)
|
{
|
var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
|
.FirstAsync(x => x.BatchNo == batchNo);
|
|
if (batch != null)
|
{
|
// 重新计算批次完成数量
|
var batchLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.BatchNo == batchNo)
|
.ToListAsync();
|
|
var completedQuantity = batchLocks.Where(x => x.Status == (int)OutLockStockStatusEnum.拣选完成)
|
.Sum(x => x.PickedQty);
|
|
batch.CompletedQuantity = completedQuantity;
|
|
// 更新批次状态
|
if (batch.CompletedQuantity >= batch.BatchQuantity)
|
{
|
batch.BatchStatus = (int)BatchStatusEnum.已完成;
|
}
|
else if (batch.CompletedQuantity > 0)
|
{
|
batch.BatchStatus = (int)BatchStatusEnum.执行中;
|
}
|
else
|
{
|
batch.BatchStatus = (int)BatchStatusEnum.分配中;
|
}
|
|
await _outboundBatchRepository.Db.Updateable(batch).ExecuteCommandAsync();
|
}
|
}
|
|
#endregion
|
|
#region 统一回库逻辑
|
|
/// <summary>
|
/// 统一回库方法
|
/// </summary>
|
public async Task<WebResponseContent> ExecutePalletReturn(string orderNo, string palletCode, string returnReason = "分批回库")
|
{
|
ReturnTaskInfo returnTaskInfo = null;
|
try
|
{
|
_logger.LogInformation($"【增强回库开始】订单: {orderNo}, 托盘: {palletCode}");
|
|
_unitOfWorkManage.BeginTran();
|
|
// 基础验证
|
if (string.IsNullOrEmpty(orderNo) || string.IsNullOrEmpty(palletCode))
|
return WebResponseContent.Instance.Error("订单号和托盘码不能为空");
|
|
// 获取库存信息
|
var stockInfo = await _stockInfoService.Db.Queryable<Dt_StockInfo>().FirstAsync(x => x.PalletCode == palletCode);
|
|
if (stockInfo == null)
|
return WebResponseContent.Instance.Error($"未找到托盘 {palletCode} 对应的库存信息");
|
|
int stockId = stockInfo.Id;
|
|
// 执行回库前数据验证
|
var validationResult = await ValidateDataBeforeReturn(orderNo, palletCode, stockId);
|
if (!validationResult.IsValid)
|
{
|
_logger.LogWarning($"回库前数据验证失败: {validationResult.ErrorMessage}");
|
}
|
|
// 分析托盘状态
|
var statusAnalysis = await AnalyzePalletStatusForReturn(orderNo, palletCode, stockInfo.Id);
|
|
if (!statusAnalysis.HasItemsToReturn)
|
{
|
try
|
{
|
_logger.LogInformation($"【无回库物品】处理空托盘");
|
var result = await HandleEmptyPalletReturn(orderNo, palletCode, stockInfo);
|
_unitOfWorkManage.CommitTran();
|
|
// 在事务提交后处理ESS命令
|
if (result.Status && result.Data is ReturnTaskInfo taskInfo)
|
{
|
await ProcessESSAfterTransaction(palletCode, taskInfo);
|
}
|
return result;
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
_logger.LogError($"空箱回库ExecutePalletReturn失败: {ex.Message}");
|
return WebResponseContent.Instance.Error($"空箱回库ExecutePalletReturn失败:{ex.Message}");
|
}
|
}
|
|
_logger.LogInformation($"【开始回库】总回库数量: {statusAnalysis.TotalReturnQty}, 条码数: {statusAnalysis.AllBarcodes.Count}");
|
|
// 执行回库操作
|
try
|
{
|
await ExecuteEnhancedReturnOperations(statusAnalysis);
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"主回库方法失败: {ex.Message}");
|
// 尝试简化方法
|
await ExecuteSimpleReturnDataOperations(statusAnalysis);
|
}
|
|
// 更新订单状态
|
await UpdateOrderStatusAfterReturn(orderNo);
|
|
// 创建回库任务
|
try
|
{
|
// 创建回库任务,但不发送ESS命令
|
returnTaskInfo = await CreateReturnTaskWithoutESS(orderNo, palletCode, stockInfo);
|
|
}
|
catch (Exception taskEx)
|
{
|
_logger.LogError($"回库任务CreateReturnTaskWithoutESS创建失败: {taskEx.Message}");
|
// 任务创建失败不影响数据回库
|
}
|
_unitOfWorkManage.CommitTran();
|
|
// 在事务提交后处理ESS命令
|
if (returnTaskInfo != null && returnTaskInfo.ShouldSendESS)
|
{
|
await ProcessESSAfterTransaction(palletCode, returnTaskInfo);
|
}
|
// 回库后验证
|
await ValidateDataAfterReturn(orderNo, palletCode, stockId);
|
|
return WebResponseContent.Instance.OK($"回库成功,回库数量:{statusAnalysis.TotalReturnQty}", new
|
{
|
ReturnQuantity = statusAnalysis.TotalReturnQty,
|
ReturnBarcodes = statusAnalysis.AllBarcodes,
|
Reason = returnReason,
|
PalletCode = palletCode,
|
OrderNo = orderNo
|
});
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
_logger.LogError($"ExecutePalletReturn 回库失败: {ex.Message}");
|
return WebResponseContent.Instance.Error($"ExecutePalletReturn 回库失败:{ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 事务提交后处理ESS命令 - 独立方法,避免事务锁
|
/// </summary>
|
private async Task ProcessESSAfterTransaction(string palletCode, ReturnTaskInfo taskInfo)
|
{
|
try
|
{
|
_logger.LogInformation($"开始处理ESS命令 - 托盘: {palletCode}");
|
|
if (taskInfo == null || !taskInfo.ShouldSendESS || taskInfo.ReturnTask == null)
|
{
|
_logger.LogWarning($"无需发送ESS命令或任务信息不完整");
|
return;
|
}
|
|
// 发送ESS命令
|
await SendESSCommands(palletCode, taskInfo.OriginalTaskTargetAddress, taskInfo.ReturnTask);
|
|
_logger.LogInformation($"ESS命令处理完成 - 托盘: {palletCode}, 任务号: {taskInfo.ReturnTask.TaskNum}");
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"处理ESS命令失败 - 托盘: {palletCode}, Error: {ex.Message}");
|
// 这里不抛出异常,因为数据回库已经成功,ESS命令发送失败可以稍后重试
|
}
|
}
|
/// <summary>
|
/// 创建回库任务(不发送ESS命令)- 用于事务内处理
|
/// </summary>
|
private async Task<ReturnTaskInfo> CreateReturnTaskWithoutESS(string orderNo, string palletCode, Dt_StockInfo stockInfo)
|
{
|
try
|
{
|
// 获取当前任务信息
|
var currentTask = await _taskRepository.Db.Queryable<Dt_Task>()
|
.Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
|
.FirstAsync();
|
|
if (currentTask == null)
|
{
|
_logger.LogWarning($"未找到当前任务 - 订单: {orderNo}, 托盘: {palletCode}");
|
return null;
|
}
|
|
// 分配新货位
|
var newLocation = _locationInfoService.AssignLocation(stockInfo.LocationType);
|
|
var returnTask = new Dt_Task()
|
{
|
CurrentAddress = stations[currentTask.TargetAddress],
|
Grade = 0,
|
PalletCode = palletCode,
|
NextAddress = "",
|
OrderNo = orderNo,
|
Roadway = newLocation.RoadwayNo,
|
SourceAddress = stations[currentTask.TargetAddress],
|
TargetAddress = newLocation.LocationCode,
|
TaskStatus = TaskStatusEnum.New.ObjToInt(),
|
TaskType = TaskTypeEnum.InPick.ObjToInt(),
|
PalletType = stockInfo.PalletType,
|
WarehouseId = currentTask.WarehouseId
|
};
|
|
try
|
{
|
await _taskRepository.Db.Insertable(returnTask).ExecuteCommandAsync();
|
|
_logger.LogInformation($"创建回库任务成功: {returnTask.TaskNum}, 订单: {orderNo}, 托盘: {palletCode}");
|
|
// 删除原始出库任务
|
_logger.LogInformation($"开始删除历史任务: {orderNo}, {currentTask.TaskNum}");
|
var result = _task_HtyService.DeleteAndMoveIntoHty(currentTask, OperateTypeEnum.人工删除);
|
await _taskRepository.Db.Deleteable(currentTask).ExecuteCommandAsync();
|
|
if (!result)
|
{
|
await _taskRepository.Db.Deleteable(currentTask).ExecuteCommandAsync();
|
}
|
_logger.LogInformation($"删除历史任务完成: {currentTask.TaskNum}, 影响行数: {result}");
|
|
// 返回任务信息,但不发送ESS命令
|
return new ReturnTaskInfo
|
{
|
ShouldSendESS = true,
|
PalletCode = palletCode,
|
OriginalTaskTargetAddress = currentTask.TargetAddress,
|
ReturnTask = returnTask
|
};
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"创建回库任务失败 - 订单: {orderNo}, 托盘: {palletCode}, Error: {ex.Message}");
|
throw new Exception($"创建回库任务失败 - 订单: {orderNo}, 托盘: {palletCode}", ex);
|
}
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"创建回库任务失败 - 订单: {orderNo}, 托盘: {palletCode}, Error: {ex.Message}");
|
return null;
|
}
|
}
|
/// <summary>
|
/// 增强的回库前数据验证
|
/// </summary>
|
private async Task<ValidationResult<bool>> ValidateDataBeforeReturn(string orderNo, string palletCode, int stockId)
|
{
|
var errors = new List<string>();
|
|
try
|
{
|
// 验证库存数据
|
var stockDetails = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.Where(x => x.StockId == stockId)
|
.ToListAsync();
|
|
// 检查库存数量是否为负数
|
var negativeStocks = stockDetails.Where(x => x.StockQuantity < 0).ToList();
|
if (negativeStocks.Any())
|
{
|
errors.Add($"发现负数库存: {string.Join(", ", negativeStocks.Select(x => $"{x.Barcode}:{x.StockQuantity}"))}");
|
}
|
|
// 验证锁定记录
|
var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
|
.ToListAsync();
|
|
// 检查已拣选数量是否大于分配数量
|
var invalidLocks = lockInfos.Where(x => x.PickedQty > x.AssignQuantity).ToList();
|
if (invalidLocks.Any())
|
{
|
errors.Add($"发现已拣选数量大于分配数量的锁定记录");
|
}
|
|
// 验证拆包记录
|
var splitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
|
.Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
|
.ToListAsync();
|
|
// 检查循环拆包
|
var barcodeMap = new Dictionary<string, string>();
|
foreach (var record in splitRecords.Where(x => !x.IsReverted))
|
{
|
if (!barcodeMap.ContainsKey(record.OriginalBarcode))
|
barcodeMap[record.OriginalBarcode] = record.NewBarcode;
|
}
|
|
// 检查循环引用
|
foreach (var record in splitRecords)
|
{
|
var current = record.OriginalBarcode;
|
var visited = new HashSet<string>();
|
|
while (barcodeMap.ContainsKey(current))
|
{
|
if (visited.Contains(current))
|
{
|
errors.Add($"发现循环拆包引用: {record.OriginalBarcode}");
|
break;
|
}
|
visited.Add(current);
|
current = barcodeMap[current];
|
}
|
}
|
|
if (errors.Any())
|
{
|
return ValidationResult<bool>.Error($"回库前数据验证发现问题: {string.Join("; ", errors)}");
|
}
|
|
return ValidationResult<bool>.Success(true);
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"数据验证失败: {ex.Message}");
|
return ValidationResult<bool>.Error($"数据验证异常: {ex.Message}");
|
}
|
}
|
|
// <summary>
|
/// 执行增强的回库操作 - 修正版(避免重复处理)
|
/// </summary>
|
private async Task ExecuteEnhancedReturnOperations(PalletStatusAnalysis statusAnalysis)
|
{
|
_logger.LogInformation($"【开始执行回库操作】订单: {statusAnalysis.OrderNo}, 托盘: {statusAnalysis.PalletCode}");
|
|
// 使用本地已处理集合,避免重复
|
var locallyProcessedBarcodes = new HashSet<string>();
|
decimal totalProcessedQty = 0;
|
|
// 处理已分配的锁定记录
|
if (statusAnalysis.HasRemainingLocks)
|
{
|
_logger.LogInformation($"处理已分配锁定记录 - {statusAnalysis.RemainingLocks.Count} 条");
|
foreach (var lockInfo in statusAnalysis.RemainingLocks)
|
{
|
if (string.IsNullOrEmpty(lockInfo.CurrentBarcode) ||
|
locallyProcessedBarcodes.Contains(lockInfo.CurrentBarcode))
|
{
|
_logger.LogInformation($"跳过已处理条码的锁定记录: {lockInfo.CurrentBarcode}");
|
continue;
|
}
|
|
decimal returnQty = lockInfo.AssignQuantity - lockInfo.PickedQty;
|
if (returnQty > 0)
|
{
|
// 【修复】传递 isUnallocated = false
|
await ProcessSingleBarcodeReturn(lockInfo.CurrentBarcode, statusAnalysis.StockId, returnQty, false);
|
locallyProcessedBarcodes.Add(lockInfo.CurrentBarcode);
|
totalProcessedQty += returnQty;
|
_logger.LogInformation($"已处理锁定记录 - 条码: {lockInfo.CurrentBarcode}, 数量: {returnQty}");
|
}
|
}
|
}
|
|
// 处理未分配的锁定记录
|
if (statusAnalysis.HasUnallocatedLocks)
|
{
|
_logger.LogInformation($"处理未分配锁定记录 - {statusAnalysis.UnallocatedLocks.Count} 条");
|
foreach (var lockInfo in statusAnalysis.UnallocatedLocks)
|
{
|
if (string.IsNullOrEmpty(lockInfo.CurrentBarcode) ||
|
locallyProcessedBarcodes.Contains(lockInfo.CurrentBarcode))
|
{
|
_logger.LogInformation($"跳过已处理条码的未分配锁定: {lockInfo.CurrentBarcode}");
|
continue;
|
}
|
|
decimal returnQty = lockInfo.AssignQuantity - lockInfo.PickedQty;
|
if (returnQty > 0)
|
{
|
// 【修复】使用专门的未分配锁定处理方法
|
await ProcessUnallocatedLockReturn(lockInfo, returnQty);
|
locallyProcessedBarcodes.Add(lockInfo.CurrentBarcode);
|
totalProcessedQty += returnQty;
|
_logger.LogInformation($"已处理未分配锁定 - 条码: {lockInfo.CurrentBarcode}, 数量: {returnQty}");
|
}
|
}
|
}
|
|
// 处理未分配的库存货物
|
if (statusAnalysis.HasPalletStockGoods)
|
{
|
_logger.LogInformation($"处理未分配库存货物 - {statusAnalysis.PalletStockGoods.Count} 个");
|
foreach (var stockDetail in statusAnalysis.PalletStockGoods)
|
{
|
if (string.IsNullOrEmpty(stockDetail.Barcode) ||
|
locallyProcessedBarcodes.Contains(stockDetail.Barcode))
|
{
|
_logger.LogInformation($"跳过已处理条码的库存: {stockDetail.Barcode}");
|
continue;
|
}
|
|
decimal returnQty = stockDetail.StockQuantity;
|
if (returnQty > 0)
|
{
|
await ProcessUnallocatedStockReturn(stockDetail);
|
locallyProcessedBarcodes.Add(stockDetail.Barcode);
|
totalProcessedQty += returnQty;
|
_logger.LogInformation($"已处理未分配库存 - 条码: {stockDetail.Barcode}, 数量: {returnQty}");
|
}
|
}
|
}
|
|
// 处理拆包记录 - 只处理未被其他逻辑覆盖的条码
|
if (statusAnalysis.HasSplitRecords && statusAnalysis.SplitReturnQty > 0)
|
{
|
_logger.LogInformation($"处理拆包记录相关库存 - 新增数量: {statusAnalysis.SplitReturnQty}");
|
|
// 只处理在SplitRecords中但不在已处理集合中的条码
|
var splitBarcodes = statusAnalysis.SplitRecords
|
.SelectMany(r => new[] { r.OriginalBarcode, r.NewBarcode })
|
.Where(b => !string.IsNullOrEmpty(b))
|
.Distinct()
|
.ToList();
|
|
foreach (var barcode in splitBarcodes)
|
{
|
if (locallyProcessedBarcodes.Contains(barcode))
|
{
|
_logger.LogInformation($"跳过已处理的拆包条码: {barcode}");
|
continue;
|
}
|
|
var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == barcode && x.StockId == statusAnalysis.StockId);
|
|
if (stockDetail != null && stockDetail.StockQuantity > 0)
|
{
|
decimal returnQty = stockDetail.StockQuantity;
|
await ProcessSingleBarcodeReturn(barcode, statusAnalysis.StockId, returnQty);
|
locallyProcessedBarcodes.Add(barcode);
|
totalProcessedQty += returnQty;
|
_logger.LogInformation($"处理拆包条码 - {barcode}, 数量: {returnQty}");
|
}
|
}
|
}
|
|
_logger.LogInformation($"【回库操作完成】总处理数量: {totalProcessedQty}, 处理条码数: {locallyProcessedBarcodes.Count}");
|
|
// 验证处理数量与预期一致
|
if (Math.Abs(totalProcessedQty - statusAnalysis.TotalReturnQty) > 0.01m)
|
{
|
_logger.LogWarning($"处理数量({totalProcessedQty})与预期({statusAnalysis.TotalReturnQty})不一致,但继续执行");
|
}
|
}
|
|
|
/// <summary>
|
/// 处理单个条码回库(通用方法)- 区分已分配和未分配
|
/// </summary>
|
private async Task ProcessSingleBarcodeReturn(string barcode, int stockId, decimal returnQty, bool isUnallocated = false)
|
{
|
try
|
{
|
_logger.LogInformation($"处理单个条码回库 - {barcode}, 数量: {returnQty}, 是否未分配: {isUnallocated}");
|
|
var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == barcode && x.StockId == stockId);
|
|
if (stockDetail == null)
|
{
|
_logger.LogWarning($"未找到库存明细: {barcode}");
|
return;
|
}
|
|
decimal originalStockQty = stockDetail.StockQuantity;
|
decimal originalOutboundQty = stockDetail.OutboundQuantity;
|
int originalStatus = stockDetail.Status;
|
|
_logger.LogInformation($"回库前状态 - 库存: {originalStockQty}, 出库: {originalOutboundQty}, 状态: {GetStockStatusName(originalStatus)}");
|
|
// 根据是否未分配决定处理逻辑
|
if (isUnallocated)
|
{
|
// 未分配锁定:只恢复状态,不改变库存数量
|
// 出库数量应为0
|
if (stockDetail.OutboundQuantity > 0)
|
{
|
_logger.LogWarning($"未分配锁定的库存出库数量不为0,重置为0 - 条码: {stockDetail.Barcode}, 当前出库: {stockDetail.OutboundQuantity}");
|
stockDetail.OutboundQuantity = 0;
|
}
|
|
// 库存数量保持不变
|
_logger.LogInformation($"未分配锁定回库 - 库存数量保持不变: {stockDetail.StockQuantity}");
|
}
|
else
|
{
|
// 已分配锁定:恢复库存数量
|
stockDetail.StockQuantity += returnQty;
|
|
// 减少出库数量(如果出库数量大于0)
|
if (stockDetail.OutboundQuantity >= returnQty)
|
{
|
stockDetail.OutboundQuantity -= returnQty;
|
}
|
else
|
{
|
// 如果出库数量小于回库数量,说明数据异常,出库数量清零
|
_logger.LogWarning($"出库数量({stockDetail.OutboundQuantity})小于回库数量({returnQty}),清零");
|
stockDetail.OutboundQuantity = 0;
|
}
|
|
_logger.LogInformation($"已分配锁定回库 - 库存数量增加: {originalStockQty} -> {stockDetail.StockQuantity}");
|
}
|
|
// 更新状态
|
stockDetail.Status = (int)StockStatusEmun.入库完成;
|
|
await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
|
|
_logger.LogInformation($"条码回库完成 - {barcode}: 库存 {originalStockQty}->{stockDetail.StockQuantity}, 出库 {originalOutboundQty}->{stockDetail.OutboundQuantity}");
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"处理条码回库失败 - {barcode}: {ex.Message}");
|
throw;
|
}
|
}
|
/// <summary>
|
/// 处理未分配锁定记录回库 - 修复版本
|
/// 未分配锁定记录:只恢复状态,不增加库存数量
|
/// </summary>
|
private async Task ProcessUnallocatedLockReturn(Dt_OutStockLockInfo lockInfo, decimal returnQty)
|
{
|
_logger.LogInformation($"处理未分配锁定回库 - 锁定ID: {lockInfo.Id}, 条码: {lockInfo.CurrentBarcode}, 数量: {returnQty}");
|
|
try
|
{
|
// 验证锁定记录状态
|
if (lockInfo.Status != (int)OutLockStockStatusEnum.出库中 &&
|
lockInfo.Status != (int)OutLockStockStatusEnum.回库中)
|
{
|
_logger.LogWarning($"锁定记录状态不是出库中或回库中,跳过处理 - 状态: {lockInfo.Status}");
|
return;
|
}
|
|
// 获取库存明细
|
var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == lockInfo.CurrentBarcode && x.StockId == lockInfo.StockId);
|
|
if (stockDetail == null)
|
{
|
_logger.LogError($"未找到库存明细 - 条码: {lockInfo.CurrentBarcode}, StockId: {lockInfo.StockId}");
|
throw new InvalidOperationException($"库存明细不存在: {lockInfo.CurrentBarcode}");
|
}
|
|
// 记录原始状态
|
decimal originalStockQty = stockDetail.StockQuantity;
|
decimal originalOutboundQty = stockDetail.OutboundQuantity;
|
int originalStatus = stockDetail.Status;
|
|
_logger.LogInformation($"未分配锁定回库前状态:");
|
_logger.LogInformation($" 库存 - 条码: {stockDetail.Barcode}, 数量: {originalStockQty}, 出库: {originalOutboundQty}, 状态: {GetStockStatusName(originalStatus)}");
|
|
// 对于未分配锁定记录,库存数量不应改变!
|
// 因为库存本来就存在,只是状态被锁定
|
// stockDetail.StockQuantity 保持不变
|
|
// 出库数量应为0(未分配锁定不应该有出库)
|
if (stockDetail.OutboundQuantity > 0)
|
{
|
_logger.LogWarning($"未分配锁定的库存出库数量不为0,重置为0 - 条码: {stockDetail.Barcode}, 当前出库: {stockDetail.OutboundQuantity}");
|
stockDetail.OutboundQuantity = 0;
|
}
|
|
// 更新库存状态为入库完成(恢复为可用状态)
|
stockDetail.Status = (int)StockStatusEmun.入库完成;
|
|
await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
|
|
_logger.LogInformation($"更新未分配库存状态 - 条码: {stockDetail.Barcode}");
|
_logger.LogInformation($" 库存数量: {originalStockQty} -> {stockDetail.StockQuantity} (保持不变)");
|
_logger.LogInformation($" 出库数量: {originalOutboundQty} -> {stockDetail.OutboundQuantity}");
|
_logger.LogInformation($" 状态: {GetStockStatusName(originalStatus)} -> {GetStockStatusName(stockDetail.Status)}");
|
|
// 更新锁定记录状态为已回库
|
lockInfo.Status = (int)OutLockStockStatusEnum.已回库;
|
lockInfo.Operator = App.User.UserName;
|
|
await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
|
|
_logger.LogInformation($"更新未分配锁定状态 - 锁定ID: {lockInfo.Id}, 状态: 出库中 -> 已回库");
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"处理未分配锁定回库失败 - 锁定ID: {lockInfo.Id}, Error: {ex.Message}");
|
throw new InvalidOperationException($"处理未分配锁定回库失败: {ex.Message}", ex);
|
}
|
}
|
|
/// <summary>
|
/// 处理未分配库存回库
|
/// </summary>
|
private async Task ProcessUnallocatedStockReturn(Dt_StockInfoDetail stockDetail)
|
{
|
_logger.LogInformation($"处理未分配库存回库 - 条码: {stockDetail.Barcode}, 数量: {stockDetail.StockQuantity}");
|
|
// 直接更新库存状态
|
stockDetail.Status = (int)StockStatusEmun.入库完成;
|
await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
|
}
|
|
|
/// <summary>
|
/// 回库后数据验证 - 增强版
|
/// </summary>
|
private async Task ValidateDataAfterReturn(string orderNo, string palletCode, int stockId)
|
{
|
try
|
{
|
_logger.LogInformation($"开始回库后数据验证");
|
|
// 验证库存状态和数量
|
var stockDetails = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.Where(x => x.StockId == stockId)
|
.ToListAsync();
|
|
decimal totalStock = stockDetails.Sum(x => x.StockQuantity);
|
|
// 验证库存数量是否合理
|
var unreasonableStocks = stockDetails.Where(x => x.StockQuantity < 0).ToList();
|
if (unreasonableStocks.Any())
|
{
|
_logger.LogError($"发现负数库存数量!条码: {string.Join(", ", unreasonableStocks.Select(x => x.Barcode))}");
|
}
|
|
// 验证锁定记录状态
|
var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
|
.ToListAsync();
|
|
var notReturnedLocks = lockInfos.Where(x =>
|
x.Status != (int)OutLockStockStatusEnum.已回库 &&
|
x.Status != (int)OutLockStockStatusEnum.已取走 &&
|
x.Status != (int)OutLockStockStatusEnum.拣选完成).ToList();
|
|
if (notReturnedLocks.Any())
|
{
|
_logger.LogWarning($"回库后仍有未回库状态的锁定记录: {notReturnedLocks.Count}条");
|
foreach (var lockInfo in notReturnedLocks)
|
{
|
_logger.LogWarning($"未回库锁定 - ID: {lockInfo.Id}, 状态: {GetLockStatusName(lockInfo.Status)}, 条码: {lockInfo.CurrentBarcode}");
|
}
|
}
|
|
// 数据一致性验证
|
decimal totalExpectedReturnQty = lockInfos
|
.Where(x => x.Status == (int)OutLockStockStatusEnum.已回库)
|
.Sum(x => x.AssignQuantity - x.PickedQty);
|
|
_logger.LogInformation($"回库验证汇总:");
|
_logger.LogInformation($" 回库后库存总量: {totalStock}");
|
_logger.LogInformation($" 已回库锁定记录数量: {lockInfos.Count(x => x.Status == (int)OutLockStockStatusEnum.已回库)}");
|
_logger.LogInformation($" 总回库数量(锁定记录计算): {totalExpectedReturnQty}");
|
|
// 验证库存数量与锁定记录的一致性
|
foreach (var lockInfo in lockInfos.Where(x => !string.IsNullOrEmpty(x.CurrentBarcode)))
|
{
|
var stock = stockDetails.FirstOrDefault(x => x.Barcode == lockInfo.CurrentBarcode);
|
if (stock != null)
|
{
|
// 如果锁定记录是已回库状态,对应的库存应该是入库完成状态
|
if (lockInfo.Status == (int)OutLockStockStatusEnum.已回库 &&
|
stock.Status != (int)StockStatusEmun.入库完成)
|
{
|
_logger.LogWarning($"锁定记录已回库但库存状态不正确 - 条码: {lockInfo.CurrentBarcode}, 库存状态: {GetStockStatusName(stock.Status)}");
|
}
|
}
|
}
|
|
_logger.LogInformation($"回库后数据验证完成");
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"回库后验证失败: {ex.Message}");
|
}
|
}
|
|
|
private string GetLockStatusName(int status)
|
{
|
return status switch
|
{
|
1 => "出库中",
|
2 => "拣选完成",
|
3 => "已回库",
|
4 => "已取走",
|
_ => $"未知({status})"
|
};
|
}
|
|
/// <summary>
|
/// 简化回库数据操作(当主方法失败时使用)
|
/// </summary>
|
private async Task ExecuteSimpleReturnDataOperations(PalletStatusAnalysis statusAnalysis)
|
{
|
_logger.LogInformation($"【简化回库】开始执行简化回库操作");
|
|
try
|
{
|
// 获取该托盘的所有条码(包括所有状态的库存)
|
var allStockDetails = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.Where(x => x.StockId == statusAnalysis.StockId)
|
.ToListAsync();
|
|
_logger.LogInformation($"找到 {allStockDetails.Count} 个库存明细记录");
|
|
foreach (var stockDetail in allStockDetails)
|
{
|
// 记录原始状态
|
int originalStatus = stockDetail.Status;
|
decimal originalStockQty = stockDetail.StockQuantity;
|
|
// 将所有出库相关的状态恢复为入库完成
|
if (stockDetail.Status == (int)StockStatusEmun.出库锁定 ||
|
//stockDetail.Status == (int)StockStatusEmun.出库中 ||
|
stockDetail.Status == (int)StockStatusEmun.出库完成)
|
{
|
// 如果是出库完成状态且库存为0,可能需要特殊处理
|
if (stockDetail.Status == (int)StockStatusEmun.出库完成 && stockDetail.StockQuantity == 0)
|
{
|
_logger.LogInformation($"跳过已出库完成的零库存条码: {stockDetail.Barcode}");
|
continue;
|
}
|
|
stockDetail.Status = (int)StockStatusEmun.入库完成;
|
|
// 如果是出库锁定状态但库存为0,重置库存为1(避免零库存问题)
|
if (stockDetail.Status == (int)StockStatusEmun.出库锁定 && stockDetail.StockQuantity == 0)
|
{
|
// 查找是否有对应的锁定记录来确定应该恢复的数量
|
var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.CurrentBarcode == stockDetail.Barcode &&
|
x.StockId == stockDetail.StockId)
|
.FirstAsync();
|
|
if (lockInfo != null)
|
{
|
stockDetail.StockQuantity = lockInfo.AssignQuantity - lockInfo.PickedQty;
|
_logger.LogInformation($"恢复零库存条码的数量 - 条码: {stockDetail.Barcode}, 数量: {stockDetail.StockQuantity}");
|
}
|
}
|
|
await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
|
_logger.LogInformation($"简化回库 - 条码: {stockDetail.Barcode}, 状态: {GetStockStatusName(originalStatus)} -> 入库完成, 数量: {originalStockQty} -> {stockDetail.StockQuantity}");
|
}
|
}
|
|
// 更新所有锁定记录为已回库
|
var allLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == statusAnalysis.OrderNo &&
|
x.PalletCode == statusAnalysis.PalletCode &&
|
(x.Status == (int)OutLockStockStatusEnum.出库中 ||
|
x.Status == (int)OutLockStockStatusEnum.拣选完成))
|
.ToListAsync();
|
|
foreach (var lockInfo in allLocks)
|
{
|
lockInfo.Status = (int)OutLockStockStatusEnum.已回库;
|
lockInfo.Operator = App.User.UserName;
|
|
await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
|
_logger.LogInformation($"简化回库 - 锁定记录: {lockInfo.Id}, 状态: 已回库");
|
}
|
|
_logger.LogInformation($"【简化回库】完成 - 处理 {allStockDetails.Count} 个条码, {allLocks.Count} 条锁定记录");
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"简化回库失败: {ex.Message}");
|
throw;
|
}
|
}
|
|
/// <summary>
|
/// 获取库存状态名称
|
/// </summary>
|
private string GetStockStatusName(int status)
|
{
|
return status switch
|
{
|
1 => "入库确认",
|
2 => "入库完成",
|
3 => "出库锁定",
|
4 => "出库中",
|
5 => "出库完成",
|
6 => "已清理",
|
_ => $"未知({status})"
|
};
|
}
|
|
|
/// <summary>
|
/// 创建回库任务
|
/// </summary>
|
private async Task CreateReturnTask(string orderNo, string palletCode, Dt_StockInfo stockInfo)
|
{
|
// 获取当前任务信息
|
var currentTask = await _taskRepository.Db.Queryable<Dt_Task>()
|
.Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
|
.FirstAsync();
|
|
if (currentTask != null)
|
{
|
// 分配新货位
|
var newLocation = _locationInfoService.AssignLocation(stockInfo.LocationType);
|
|
var returnTask = new Dt_Task()
|
{
|
CurrentAddress = stations[currentTask.TargetAddress],
|
Grade = 0,
|
PalletCode = palletCode,
|
NextAddress = "",
|
OrderNo = orderNo,
|
Roadway = newLocation.RoadwayNo,
|
SourceAddress = stations[currentTask.TargetAddress],
|
TargetAddress = newLocation.LocationCode,
|
TaskStatus = TaskStatusEnum.New.ObjToInt(),
|
TaskType = TaskTypeEnum.InPick.ObjToInt(),
|
PalletType = stockInfo.PalletType,
|
WarehouseId = currentTask.WarehouseId
|
};
|
var targetAddress = currentTask.TargetAddress;
|
|
try
|
{
|
await _taskRepository.Db.Insertable(returnTask).ExecuteCommandAsync();
|
|
_logger.LogInformation($"CreateReturnTaskAndHandleESS 分批删除历史任务: {orderNo} , {currentTask.TaskNum}");
|
// 删除原始出库任务
|
//_taskRepository.DeleteAndMoveIntoHty(originalTask, OperateTypeEnum.自动完成);
|
var result = _task_HtyService.DeleteAndMoveIntoHty(currentTask, OperateTypeEnum.人工删除);
|
await _taskRepository.Db.Deleteable(currentTask).ExecuteCommandAsync();
|
|
if (!result)
|
{
|
await _taskRepository.Db.Deleteable(currentTask).ExecuteCommandAsync();
|
}
|
_logger.LogInformation($"CreateReturnTaskAndHandleESS 分批删除历史任务: {orderNo} , {currentTask.TaskNum},影响行 {result}");
|
|
}
|
catch (Exception ex)
|
{
|
_logger.LogInformation($"创建回库任务失败 - 订单: {orderNo}, 托盘: {palletCode}");
|
throw new Exception($"创建回库任务失败 - 订单: {orderNo}, 托盘: {palletCode}");
|
|
}
|
// 发送ESS命令
|
await SendESSCommands(palletCode, targetAddress, returnTask);
|
|
_logger.LogInformation($"创建回库任务成功 - 订单: {orderNo}, 托盘: {palletCode}");
|
}
|
}
|
|
/// <summary>
|
/// 更新回库后的订单状态
|
/// </summary>
|
private async Task UpdateOrderStatusAfterReturn(string orderNo)
|
{
|
// 检查订单是否还有未完成的锁定记录
|
var activeLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo &&
|
(x.Status == (int)OutLockStockStatusEnum.出库中 ||
|
x.Status == (int)OutLockStockStatusEnum.回库中))
|
.ToListAsync();
|
|
if (!activeLocks.Any())
|
{
|
// 所有锁定记录都已完成或已回库,更新订单状态
|
await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
|
.SetColumns(x => new Dt_OutboundOrder
|
{
|
OrderStatus = (int)OutOrderStatusEnum.出库完成,
|
})
|
.Where(x => x.OrderNo == orderNo)
|
.ExecuteCommandAsync();
|
|
_logger.LogInformation($"更新订单状态为出库完成 - 订单: {orderNo}");
|
}
|
}
|
|
/// <summary>
|
/// 处理空托盘回库
|
/// </summary>
|
private async Task<WebResponseContent> HandleEmptyPalletReturn(string orderNo, string palletCode, Dt_StockInfo stockInfo)
|
{
|
_logger.LogInformation($"处理空托盘回库 - 订单: {orderNo}, 托盘: {palletCode}");
|
|
try
|
{
|
// 清理零库存数据
|
await CleanupZeroStockData(stockInfo.Id);
|
|
// 获取当前任务
|
var currentTask = await GetCurrentTask(orderNo, palletCode);
|
if (currentTask == null)
|
{
|
return WebResponseContent.Instance.Error("未找到当前任务");
|
}
|
// 创建空托盘库存记录
|
var emptyStockInfo = new Dt_StockInfo()
|
{
|
PalletType = PalletTypeEnum.Empty.ObjToInt(),
|
StockStatus = StockStatusEmun.组盘暂存.ObjToInt(),
|
PalletCode = palletCode,
|
LocationType = stockInfo.LocationType
|
};
|
emptyStockInfo.Details = new List<Dt_StockInfoDetail>();
|
_stockInfoService.AddMaterielGroup(emptyStockInfo);
|
|
// 创建回库任务(不发送ESS命令)
|
var returnTaskInfo = await CreateEmptyPalletReturnTask(orderNo, palletCode, emptyStockInfo, currentTask);
|
|
return WebResponseContent.Instance.OK("空托盘回库成功", returnTaskInfo);
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"空托盘回库失败 HandleEmptyPalletReturn: {ex.Message}");
|
return WebResponseContent.Instance.Error($"空托盘回库失败 HandleEmptyPalletReturn: {ex.Message}");
|
}
|
}
|
|
private async Task<Dt_Task> GetCurrentTask(string orderNo, string palletCode)
|
{
|
// 先尝试通过订单号和托盘号查找任务
|
var task = await _taskRepository.Db.Queryable<Dt_Task>()
|
.Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
|
.FirstAsync();
|
|
if (task == null)
|
{
|
// 如果找不到,再通过托盘号查找
|
task = await _taskRepository.Db.Queryable<Dt_Task>()
|
.Where(x => x.PalletCode == palletCode)
|
.FirstAsync();
|
}
|
|
return task;
|
}
|
|
// <summary>
|
/// 创建空托盘回库任务(不发送ESS命令)
|
/// </summary>
|
private async Task<ReturnTaskInfo> CreateEmptyPalletReturnTask(string orderNo, string palletCode, Dt_StockInfo emptyStockInfo, Dt_Task currentTask)
|
{
|
try
|
{
|
// 分配新货位
|
var newLocation = _locationInfoService.AssignLocation(emptyStockInfo.LocationType);
|
|
var returnTask = new Dt_Task()
|
{
|
CurrentAddress = stations[currentTask.TargetAddress],
|
Grade = 0,
|
PalletCode = palletCode,
|
NextAddress = "",
|
OrderNo = orderNo,
|
Roadway = newLocation.RoadwayNo,
|
SourceAddress = stations[currentTask.TargetAddress],
|
TargetAddress = newLocation.LocationCode,
|
TaskStatus = TaskStatusEnum.New.ObjToInt(),
|
TaskType = TaskTypeEnum.InEmpty.ObjToInt(),
|
PalletType = PalletTypeEnum.Empty.ObjToInt(),
|
WarehouseId = currentTask.WarehouseId
|
};
|
|
await _taskRepository.Db.Insertable(returnTask).ExecuteCommandAsync();
|
|
_logger.LogInformation($"创建空托盘回库任务成功: {returnTask.TaskNum}");
|
|
// 删除原始出库任务
|
var result = _task_HtyService.DeleteAndMoveIntoHty(currentTask, OperateTypeEnum.人工删除);
|
await _taskRepository.Db.Deleteable(currentTask).ExecuteCommandAsync();
|
|
if (!result)
|
{
|
await _taskRepository.Db.Deleteable(currentTask).ExecuteCommandAsync();
|
}
|
|
// 返回任务信息
|
return new ReturnTaskInfo
|
{
|
ShouldSendESS = true,
|
PalletCode = palletCode,
|
OriginalTaskTargetAddress = currentTask.TargetAddress,
|
ReturnTask = returnTask
|
};
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"创建空托盘回库任务失败: {ex.Message}");
|
throw;
|
}
|
}
|
|
/// <summary>
|
/// 分析托盘状态用于回库 - 修正版(解决拆包记录重复计算问题)
|
/// </summary>
|
private async Task<PalletStatusAnalysis> AnalyzePalletStatusForReturn(string orderNo, string palletCode, int stockId)
|
{
|
var result = new PalletStatusAnalysis
|
{
|
OrderNo = orderNo,
|
PalletCode = palletCode,
|
StockId = stockId,
|
AllBarcodes = new List<string>(),
|
ProcessedBarcodes = new HashSet<string>()
|
};
|
|
_logger.LogInformation($"【回库分析】开始分析托盘状态 - 订单: {orderNo}, 托盘: {palletCode}, StockId: {stockId}");
|
|
try
|
{
|
// 首先获取托盘上所有的库存明细(基础数据)
|
var allStockDetails = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.Where(x => x.StockId == stockId && x.StockQuantity > 0)
|
.ToListAsync();
|
|
_logger.LogInformation($"找到 {allStockDetails.Count} 个有库存的明细记录");
|
|
// 分析所有锁定记录(已分配和未分配)
|
var allLockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
|
.ToListAsync();
|
|
// 区分已分配和未分配锁定记录
|
var allocatedLocks = allLockInfos
|
.Where(x => x.IsUnallocated != 1 && x.OrderDetailId > 0 &&
|
x.Status == (int)OutLockStockStatusEnum.出库中)
|
.ToList();
|
|
var unallocatedLocks = allLockInfos
|
.Where(x => (x.IsUnallocated == 1 || x.OrderDetailId == 0) &&
|
x.Status == (int)OutLockStockStatusEnum.出库中)
|
.ToList();
|
|
// 处理已分配锁定记录
|
if (allocatedLocks.Any())
|
{
|
result.HasRemainingLocks = true;
|
result.RemainingLocks = allocatedLocks;
|
|
// 对于已分配锁定记录,回库数量是未拣选的部分
|
result.RemainingLocksReturnQty = allocatedLocks.Sum(x =>
|
{
|
var returnQty = x.AssignQuantity - x.PickedQty;
|
return returnQty > 0 ? returnQty : 0;
|
});
|
|
foreach (var lockInfo in allocatedLocks)
|
{
|
if (!string.IsNullOrEmpty(lockInfo.CurrentBarcode))
|
{
|
result.AllBarcodes.Add(lockInfo.CurrentBarcode);
|
result.ProcessedBarcodes.Add(lockInfo.CurrentBarcode);
|
}
|
}
|
_logger.LogInformation($"发现 {allocatedLocks.Count} 条已分配锁定记录,回库数量: {result.RemainingLocksReturnQty}");
|
}
|
|
// 处理未分配锁定记录(自动拆包产生的)
|
if (unallocatedLocks.Any())
|
{
|
result.HasUnallocatedLocks = true;
|
result.UnallocatedLocks = unallocatedLocks;
|
|
// 对于未分配锁定记录,回库数量是它的分配数量(因为未拣选过)
|
// 但实际上,库存本来就存在,只是状态需要恢复
|
result.UnallocatedLocksReturnQty = unallocatedLocks.Sum(x => x.AssignQuantity);
|
|
foreach (var lockInfo in unallocatedLocks)
|
{
|
if (!string.IsNullOrEmpty(lockInfo.CurrentBarcode) &&
|
!result.ProcessedBarcodes.Contains(lockInfo.CurrentBarcode))
|
{
|
result.AllBarcodes.Add(lockInfo.CurrentBarcode);
|
result.ProcessedBarcodes.Add(lockInfo.CurrentBarcode);
|
}
|
}
|
_logger.LogInformation($"发现 {unallocatedLocks.Count} 条未分配锁定记录,回库数量(状态恢复): {result.UnallocatedLocksReturnQty}");
|
}
|
|
// 重新计算总回库数量
|
// 对于已分配锁定:回库数量 = 未拣选数量
|
// 对于未分配锁定:没有实际的库存数量变化,只是状态恢复
|
result.TotalReturnQty = result.RemainingLocksReturnQty; // 只计算已分配锁定的回库数量
|
|
// 记录库存数量(用于验证)
|
decimal totalStockOnPallet = allStockDetails.Sum(x => x.StockQuantity);
|
|
_logger.LogInformation($"回库分析完成:");
|
_logger.LogInformation($" 托盘总库存: {totalStockOnPallet}");
|
_logger.LogInformation($" 已分配锁定回库数量: {result.RemainingLocksReturnQty}");
|
_logger.LogInformation($" 未分配锁定状态恢复数量: {result.UnallocatedLocksReturnQty}");
|
_logger.LogInformation($" 实际物理回库数量: {result.TotalReturnQty}");
|
|
result.HasItemsToReturn = result.TotalReturnQty > 0 || result.UnallocatedLocksReturnQty > 0;
|
result.IsEmptyPallet = !result.HasItemsToReturn;
|
|
return result;
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"回库分析失败 - 订单: {orderNo}, 托盘: {palletCode}, Error: {ex.Message}");
|
throw;
|
}
|
}
|
|
/// <summary>
|
/// 分批回库 - 调用统一回库方法
|
/// </summary>
|
public async Task<WebResponseContent> BatchReturnStock(string orderNo, string palletCode)
|
{
|
return await ExecutePalletReturn(orderNo, palletCode, "分批回库");
|
}
|
|
|
|
/// <summary>
|
/// 取走空箱 -正确处理未分配锁定记录
|
/// </summary>
|
public async Task<WebResponseContent> RemoveEmptyPallet(string orderNo, string palletCode)
|
{
|
try
|
{
|
_logger.LogInformation($"【取走空箱开始】订单: {orderNo}, 托盘: {palletCode}");
|
|
_unitOfWorkManage.BeginTran();
|
|
// 先尝试执行回库操作,确保所有物品都回库
|
_logger.LogInformation($"步骤1: 先执行回库操作");
|
var returnResult = await ExecutePalletReturn(orderNo, palletCode, "取走空箱前回库");
|
|
// 即使回库失败,继续验证空箱条件(可能是真的空托盘)
|
if (!returnResult.Status)
|
{
|
_logger.LogWarning($"回库操作可能失败或无物品: {returnResult.Message}");
|
}
|
|
// 验证空箱取走条件(必须全部完成拣选或已回库)
|
_logger.LogInformation($"步骤2: 验证空箱取走条件");
|
|
// 获取托盘的所有锁定记录(包括已回库和已取走的)
|
var allLockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
|
.ToListAsync();
|
|
if (!allLockInfos.Any())
|
{
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error("该托盘没有锁定记录");
|
}
|
|
// 检查是否有未完成的锁定记录
|
var unfinishedLocks = allLockInfos.Where(x =>
|
x.Status == (int)OutLockStockStatusEnum.出库中 ||
|
x.Status == (int)OutLockStockStatusEnum.回库中).ToList();
|
|
if (unfinishedLocks.Any())
|
{
|
var unfinishedCount = unfinishedLocks.Count;
|
// 区分已分配和未分配
|
var allocatedUnfinished = unfinishedLocks.Where(x => x.IsUnallocated != 1).ToList();
|
var unallocatedUnfinished = unfinishedLocks.Where(x => x.IsUnallocated == 1).ToList();
|
|
string errorMsg = $"托盘还有{unfinishedCount}条未完成记录";
|
if (allocatedUnfinished.Any()) errorMsg += $",其中已分配{allocatedUnfinished.Count}条";
|
if (unallocatedUnfinished.Any()) errorMsg += $",未分配{unallocatedUnfinished.Count}条";
|
|
errorMsg += ",不能取走空箱";
|
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error(errorMsg);
|
}
|
|
// 获取已完成的锁定记录(状态为拣选完成或已取走)
|
var completedLocks = allLockInfos.Where(x =>
|
x.Status == (int)OutLockStockStatusEnum.拣选完成 ||
|
x.Status == (int)OutLockStockStatusEnum.已取走).ToList();
|
|
if (!completedLocks.Any())
|
{
|
// 检查是否都是已回库状态
|
var returnedLocks = allLockInfos.Where(x => x.Status == (int)OutLockStockStatusEnum.已回库).ToList();
|
if (returnedLocks.Any())
|
{
|
_logger.LogInformation($"所有锁定记录都已回库,可以取走空箱");
|
completedLocks = returnedLocks;
|
}
|
else
|
{
|
_unitOfWorkManage.RollbackTran();
|
return WebResponseContent.Instance.Error("该托盘没有已完成拣选或已回库的记录");
|
}
|
}
|
|
_logger.LogInformation($"验证通过,找到 {completedLocks.Count} 条已完成记录");
|
|
// 清理已完成的锁定记录(标记为已取走)
|
_logger.LogInformation($"步骤3: 清理锁定记录");
|
foreach (var lockInfo in completedLocks)
|
{
|
// 只处理状态不是已取走的记录
|
if (lockInfo.Status != (int)OutLockStockStatusEnum.已取走)
|
{
|
lockInfo.Status = (int)OutLockStockStatusEnum.已取走;
|
lockInfo.Operator = App.User.UserName;
|
await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
|
_logger.LogInformation($"锁定记录标记为已取走 - ID: {lockInfo.Id}");
|
}
|
}
|
|
// 清理对应的库存记录状态
|
_logger.LogInformation($"步骤4: 清理库存记录");
|
foreach (var lockInfo in completedLocks)
|
{
|
await CleanupStockInfo(lockInfo);
|
}
|
|
// 更新相关订单状态
|
_logger.LogInformation($"步骤5: 更新订单状态");
|
await UpdateOrderStatusAfterPalletRemoval(orderNo);
|
|
// 记录操作历史
|
//_logger.LogInformation($"步骤6: 记录操作历史");
|
//await RecordEmptyPalletRemoval(orderNo, palletCode, completedLocks);
|
|
_unitOfWorkManage.CommitTran();
|
|
_logger.LogInformation($"【取走空箱成功】订单: {orderNo}, 托盘: {palletCode}");
|
|
return WebResponseContent.Instance.OK("取走空箱成功");
|
}
|
catch (Exception ex)
|
{
|
_unitOfWorkManage.RollbackTran();
|
_logger.LogError($"取走空箱失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}");
|
return WebResponseContent.Instance.Error($"取走空箱失败:{ex.Message}");
|
}
|
}
|
|
#endregion
|
|
#region 辅助方法
|
|
private async Task CleanupZeroStockData(int stockId)
|
{
|
try
|
{
|
// 删除库存数量为0的明细记录
|
var deleteDetailCount = await _stockInfoDetailService.Db.Deleteable<Dt_StockInfoDetail>()
|
.Where(x => x.StockId == stockId && x.StockQuantity == 0)
|
.ExecuteCommandAsync();
|
|
await _stockInfoService.Db.Deleteable<Dt_StockInfo>()
|
.Where(x => x.Id == stockId).ExecuteCommandAsync();
|
|
_logger.LogInformation($"清理零库存明细记录 - StockId: {stockId}, 删除记录数: {deleteDetailCount}");
|
|
|
|
}
|
catch (Exception ex)
|
{
|
_logger.LogWarning($"清理零库存数据失败 - StockId: {stockId}, Error: {ex.Message}");
|
|
}
|
}
|
|
#endregion
|
|
#region 验证方法
|
private async Task<ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>> ValidatePickingRequest(
|
string orderNo, string palletCode, string barcode)
|
{
|
_logger.LogInformation($"开始验证分拣请求 - 订单: {orderNo}, 托盘: {palletCode}, 条码: {barcode}");
|
|
// 查找锁定信息
|
var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.Where(x => x.OrderNo == orderNo &&
|
x.PalletCode == palletCode &&
|
x.CurrentBarcode == barcode &&
|
x.Status == (int)OutLockStockStatusEnum.出库中)
|
.FirstAsync();
|
|
if (lockInfo == null)
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error("未找到有效的锁定信息");
|
|
if (lockInfo.IsUnallocated == 1 || lockInfo.OrderDetailId == 0)
|
{
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error("该条码是未分配锁定记录,不能直接分拣");
|
}
|
|
_logger.LogInformation($"找到锁定信息 - 分配数量: {lockInfo.AssignQuantity}, 已拣选: {lockInfo.PickedQty}");
|
|
// 检查是否已经分拣完成
|
if (lockInfo.PickedQty >= lockInfo.AssignQuantity)
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error("该条码已分拣完成");
|
|
// 获取关联数据
|
var orderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
|
.FirstAsync(x => x.Id == lockInfo.OrderDetailId);
|
|
if (orderDetail == null)
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error("未找到订单明细");
|
|
_logger.LogInformation($"找到订单明细 - 已分配数量: {orderDetail.AllocatedQuantity}, 锁定数量: {orderDetail.LockQuantity}");
|
|
// 检查订单明细的已分配数量是否足够
|
decimal remainingToPick = lockInfo.AssignQuantity - lockInfo.PickedQty;
|
|
// 这里应该检查锁定信息的分配数量,而不是订单明细的分配数量
|
// 因为拆包后,锁定信息的分配数量可能小于订单明细的分配数量
|
if (lockInfo.AssignQuantity < remainingToPick)
|
{
|
_logger.LogWarning($"锁定信息分配数量不足 - 需要拣选: {remainingToPick}, 锁定信息分配数量: {lockInfo.AssignQuantity}");
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error(
|
$"锁定信息分配数量不足,需要拣选:{remainingToPick},锁定信息分配数量:{lockInfo.AssignQuantity}");
|
}
|
|
// 检查锁定数量是否足够
|
if (orderDetail.LockQuantity < remainingToPick)
|
{
|
_logger.LogWarning($"订单明细锁定数量不足 - 需要拣选: {remainingToPick}, 可用锁定数量: {orderDetail.LockQuantity}");
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error(
|
$"订单明细锁定数量不足,需要拣选:{remainingToPick},可用锁定数量:{orderDetail.LockQuantity}");
|
}
|
|
// 检查订单明细分配数量是否为负数
|
if (orderDetail.AllocatedQuantity < 0)
|
{
|
_logger.LogError($"订单明细分配数量为负数 - 当前值: {orderDetail.AllocatedQuantity}");
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error(
|
$"订单明细分配数量异常(负数),请联系管理员处理");
|
}
|
|
var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == barcode && x.StockId == lockInfo.StockId);
|
|
if (stockDetail == null)
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error("未找到对应的库存信息");
|
|
// 验证库存数量
|
if (stockDetail.StockQuantity < remainingToPick)
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error(
|
$"库存数量不足,需要拣选:{remainingToPick},实际库存:{stockDetail.StockQuantity}");
|
|
// 验证库存状态
|
//if (stockDetail.Status != (int)StockStatusEmun.出库锁定)
|
//{
|
// return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error(
|
// $"库存状态异常,当前状态:{stockDetail.Status},期望状态:出库锁定");
|
//}
|
|
// 使用 OutboundBatchNo 查找批次
|
var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
|
.FirstAsync(x => x.BatchNo == lockInfo.OutboundBatchNo);
|
|
_logger.LogInformation($"分拣验证通过 - 条码: {barcode}, 剩余需拣选: {remainingToPick}, 可用库存: {stockDetail.StockQuantity}");
|
|
return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Success((lockInfo, orderDetail, stockDetail, batch));
|
}
|
|
|
/// <summary>
|
/// 检查并执行自动拆包(如果需要)
|
/// </summary>
|
private async Task<List<SplitResult>> CheckAndAutoSplitIfNeeded(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail, string palletCode)
|
{
|
if (lockInfo.IsUnallocated == 1 || lockInfo.OrderDetailId == 0)
|
{
|
_logger.LogInformation($"跳过未分配锁定记录的自动拆包检查 - 锁定ID: {lockInfo.Id}");
|
return null;
|
}
|
|
// 检查是否需要自动拆包的条件:
|
// 1. 库存数量大于分配数量
|
// 2. 锁定信息状态为出库中
|
// 3. 未拣选数量等于分配数量(表示还未开始拣选)
|
bool needAutoSplit = stockDetail.StockQuantity > lockInfo.AssignQuantity &&
|
lockInfo.Status == (int)OutLockStockStatusEnum.出库中 &&
|
lockInfo.PickedQty == 0;
|
|
if (!needAutoSplit)
|
return null;
|
|
// 计算拆包数量 = 库存数量 - 分配数量
|
decimal splitQuantity = stockDetail.StockQuantity - lockInfo.AssignQuantity;
|
|
_logger.LogInformation($"需要自动拆包 - 库存: {stockDetail.StockQuantity}, 分配: {lockInfo.AssignQuantity}, 拆包数量: {splitQuantity}");
|
|
// 执行自动拆包
|
var splitResult = await ExecuteAutoSplitLogic(lockInfo, stockDetail, splitQuantity, palletCode);
|
|
// 将拆包数量传递给调用方,用于验证
|
if (splitResult != null && splitResult.Any())
|
{
|
// 在返回结果中携带拆包数量信息
|
foreach (var result in splitResult)
|
{
|
result.quantityTotal = splitQuantity.ToString("F2");
|
}
|
}
|
|
return splitResult;
|
}
|
|
/// <summary>
|
/// 执行自动拆包逻辑 - 完全修正版
|
/// 原则:只分离物理库存,不改变原订单的任何分配和出库数量
|
/// </summary>
|
private async Task<List<SplitResult>> ExecuteAutoSplitLogic(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail,
|
decimal splitQuantity, string palletCode)
|
{
|
_logger.LogInformation($"开始执行自动拆包逻辑 - 原条码: {stockDetail.Barcode}, 拆包数量: {splitQuantity}");
|
|
try
|
{
|
// 验证拆包数量合理性
|
if (splitQuantity <= 0)
|
throw new InvalidOperationException($"拆包数量必须大于0,当前值: {splitQuantity}");
|
|
if (stockDetail.StockQuantity < lockInfo.AssignQuantity + splitQuantity)
|
throw new InvalidOperationException($"库存数量不足以进行自动拆包,库存: {stockDetail.StockQuantity}, 需要: {lockInfo.AssignQuantity + splitQuantity}");
|
|
// 生成新条码
|
string newBarcode = await GenerateNewBarcode();
|
_logger.LogInformation($"生成新条码: {newBarcode}");
|
|
// 【核心修正】更新原库存明细:只减少物理库存,不影响出库数量
|
decimal originalStockQty = stockDetail.StockQuantity;
|
stockDetail.StockQuantity -= splitQuantity; // 仅库存减少
|
// stockDetail.OutboundQuantity 保持不变!
|
|
await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
|
_logger.LogInformation($"更新原库存明细:条码 {stockDetail.Barcode} 库存 {originalStockQty} -> {stockDetail.StockQuantity},出库数量不变({stockDetail.OutboundQuantity})");
|
|
// 创建新库存明细(多余部分)- 出库数量为0
|
var newStockDetail = new Dt_StockInfoDetail
|
{
|
StockId = stockDetail.StockId,
|
MaterielCode = stockDetail.MaterielCode,
|
OrderNo = stockDetail.OrderNo,
|
BatchNo = stockDetail.BatchNo,
|
StockQuantity = splitQuantity, // 新库存数量
|
OutboundQuantity = 0, // 初始出库数量为0
|
Barcode = newBarcode,
|
Status = (int)StockStatusEmun.出库锁定, // 仍为锁定状态,但未绑定订单
|
SupplyCode = stockDetail.SupplyCode,
|
Unit = stockDetail.Unit,
|
BarcodeQty = stockDetail.BarcodeQty,
|
BarcodeUnit = stockDetail.BarcodeUnit,
|
BusinessType = stockDetail.BusinessType,
|
InboundOrderRowNo = stockDetail.InboundOrderRowNo,
|
};
|
|
await _stockInfoDetailService.Db.Insertable(newStockDetail).ExecuteCommandAsync();
|
_logger.LogInformation($"创建新库存明细:条码 {newBarcode},库存 {splitQuantity},出库 0");
|
|
// 创建新锁定信息 - 标记为未分配
|
var newLockInfo = new Dt_OutStockLockInfo
|
{
|
OrderNo = lockInfo.OrderNo,
|
OrderDetailId = 0, // 不绑定到具体订单明细,表示未分配
|
OutboundBatchNo = lockInfo.OutboundBatchNo,
|
MaterielCode = lockInfo.MaterielCode,
|
MaterielName = lockInfo.MaterielName,
|
StockId = lockInfo.StockId,
|
OrderQuantity = splitQuantity,
|
AssignQuantity = splitQuantity,
|
PickedQty = 0,
|
LocationCode = lockInfo.LocationCode,
|
PalletCode = lockInfo.PalletCode,
|
TaskNum = lockInfo.TaskNum,
|
Status = (int)OutLockStockStatusEnum.出库中,
|
Unit = lockInfo.Unit,
|
SupplyCode = lockInfo.SupplyCode,
|
OrderType = lockInfo.OrderType,
|
CurrentBarcode = newBarcode,
|
IsSplitted = 1,
|
ParentLockId = lockInfo.Id,
|
Operator = App.User.UserName,
|
FactoryArea = lockInfo.FactoryArea,
|
lineNo = lockInfo.lineNo,
|
WarehouseCode = lockInfo.WarehouseCode,
|
BarcodeQty = lockInfo.BarcodeQty,
|
BarcodeUnit = lockInfo.BarcodeUnit,
|
IsUnallocated = 1 // 明确标记为"未分配"的锁定记录
|
};
|
|
await _outStockLockInfoService.Db.Insertable(newLockInfo).ExecuteCommandAsync();
|
_logger.LogInformation($"创建未分配锁定记录:ID {newLockInfo.Id},条码 {newBarcode},数量 {splitQuantity}");
|
|
//原锁定记录和原订单明细数据完全保持不变!
|
// - 不修改 lockInfo 的任何字段
|
// - 不修改关联的 Dt_OutboundOrderDetail 的 AllocatedQuantity 和 LockQuantity
|
|
// 记录拆包历史
|
await RecordSplitHistory(lockInfo, stockDetail, splitQuantity, newBarcode, true, originalStockQty);
|
|
// 创建拆包结果列表
|
var splitResults = CreateSplitResults(lockInfo, splitQuantity, lockInfo.AssignQuantity, newBarcode, stockDetail.Barcode);
|
|
_logger.LogInformation($"自动拆包逻辑执行完成 - 创建了未分配的库存和锁定记录");
|
|
return splitResults;
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"自动拆包逻辑执行失败 - 原条码: {stockDetail.Barcode}, Error: {ex.Message}");
|
throw;
|
}
|
}
|
|
#endregion
|
|
#region 核心逻辑方法
|
|
/// <summary>
|
/// 执行分拣逻辑 - 完全修正版
|
/// 确保OutboundQuantity准确累加,不包含拆包数量
|
/// </summary>
|
private async Task<PickingResult> ExecutePickingLogic(
|
Dt_OutStockLockInfo lockInfo, Dt_OutboundOrderDetail orderDetail,
|
Dt_StockInfoDetail stockDetail, decimal actualPickedQty)
|
{
|
_logger.LogInformation($"开始执行分拣逻辑 - 条码: {stockDetail.Barcode}, 分配数量: {lockInfo.AssignQuantity}, 实际拣选: {actualPickedQty}");
|
|
// 再次验证库存数量
|
if (stockDetail.StockQuantity < actualPickedQty)
|
{
|
throw new InvalidOperationException($"库存数量不足,需要拣选 {actualPickedQty},实际库存 {stockDetail.StockQuantity}");
|
}
|
|
// 记录拣选前的值
|
decimal originalStockQty = stockDetail.StockQuantity;
|
decimal originalOutboundQty = stockDetail.OutboundQuantity;
|
int originalStatus = stockDetail.Status;
|
|
// 确保OutboundQuantity只增加本次拣选数量,不包含其他
|
stockDetail.StockQuantity -= actualPickedQty;
|
stockDetail.OutboundQuantity += actualPickedQty; // 只增加本次拣选数量
|
|
_logger.LogInformation($"更新库存信息 - 条码: {stockDetail.Barcode}");
|
_logger.LogInformation($" 库存数量: {originalStockQty} -> {stockDetail.StockQuantity}");
|
_logger.LogInformation($" 出库数量: {originalOutboundQty} -> {stockDetail.OutboundQuantity}");
|
|
// 更新库存状态
|
if (stockDetail.StockQuantity <= 0)
|
{
|
stockDetail.Status = (int)StockStatusEmun.出库完成;
|
_logger.LogInformation($"库存状态更新为出库完成");
|
}
|
else
|
{
|
stockDetail.Status = (int)StockStatusEmun.出库锁定;
|
_logger.LogInformation($"库存状态保持为出库锁定");
|
}
|
|
await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
|
|
// 更新锁定信息
|
decimal originalPickedQty = lockInfo.PickedQty;
|
lockInfo.PickedQty += actualPickedQty;
|
_logger.LogInformation($"更新锁定信息 - 已拣选数量从 {originalPickedQty} 增加到 {lockInfo.PickedQty}");
|
|
// 判断拣选完成状态
|
if (Math.Abs(lockInfo.PickedQty - lockInfo.AssignQuantity) < 0.001m)
|
{
|
lockInfo.Status = (int)OutLockStockStatusEnum.拣选完成;
|
_logger.LogInformation($"锁定信息状态更新为拣选完成");
|
}
|
else if (lockInfo.PickedQty > 0)
|
{
|
lockInfo.Status = (int)OutLockStockStatusEnum.出库中;
|
_logger.LogInformation($"锁定信息状态保持为出库中(部分拣选)");
|
}
|
|
lockInfo.Operator = App.User.UserName;
|
await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
|
|
// 验证拣选后的数据一致性
|
await ValidatePickingDataConsistency(lockInfo, stockDetail, actualPickedQty);
|
|
_logger.LogInformation($"分拣逻辑执行完成 - 条码: {stockDetail.Barcode}");
|
|
return new PickingResult
|
{
|
FinalLockInfo = lockInfo,
|
ActualPickedQty = actualPickedQty
|
};
|
}
|
|
/// <summary>
|
/// 验证拣选后数据一致性
|
/// </summary>
|
private async Task ValidatePickingDataConsistency(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail, decimal pickedQty)
|
{
|
_logger.LogInformation($"验证拣选数据一致性 - 条码: {stockDetail.Barcode}");
|
|
// 验证库存明细的OutboundQuantity增加量等于拣选数量
|
var refreshedStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Id == stockDetail.Id);
|
|
decimal outboundIncrease = refreshedStockDetail.OutboundQuantity - stockDetail.OutboundQuantity;
|
|
if (Math.Abs(outboundIncrease - pickedQty) > 0.01m)
|
{
|
_logger.LogError($"拣选数据不一致:出库数量增加 {outboundIncrease},但拣选数量是 {pickedQty}");
|
// 确保OutboundQuantity正确
|
refreshedStockDetail.OutboundQuantity = stockDetail.OutboundQuantity + pickedQty;
|
await _stockInfoDetailService.Db.Updateable(refreshedStockDetail).ExecuteCommandAsync();
|
_logger.LogWarning($"已修复出库数量:{stockDetail.OutboundQuantity} -> {refreshedStockDetail.OutboundQuantity}");
|
}
|
|
// 验证锁定记录的已拣选数量
|
var refreshedLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.FirstAsync(x => x.Id == lockInfo.Id);
|
|
if (Math.Abs(refreshedLockInfo.PickedQty - (lockInfo.PickedQty - pickedQty) - pickedQty) > 0.01m)
|
{
|
_logger.LogError($"锁定记录已拣选数量不一致");
|
}
|
|
_logger.LogInformation($"拣选数据一致性验证通过");
|
}
|
|
private async Task<RevertPickingResult> RevertPickingData(Dt_PickingRecord pickingRecord)
|
{
|
_logger.LogInformation($"开始恢复拣选数据 - 拣选记录ID: {pickingRecord.Id}, 条码: {pickingRecord.Barcode}, 拣选数量: {pickingRecord.PickQuantity}");
|
|
// 恢复锁定信息
|
var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
|
.FirstAsync(x => x.Id == pickingRecord.OutStockLockId);
|
|
if (lockInfo == null)
|
{
|
throw new InvalidOperationException($"未找到对应的锁定信息,ID: {pickingRecord.OutStockLockId}");
|
}
|
|
decimal originalPickedQty = lockInfo.PickedQty;
|
decimal originalAssignQty = lockInfo.AssignQuantity; // 记录原始分配数量
|
|
// 只恢复已拣选数量,不修改分配数量
|
lockInfo.PickedQty -= pickingRecord.PickQuantity;
|
|
// 确保已拣选数量不会为负数
|
if (lockInfo.PickedQty < 0)
|
{
|
_logger.LogWarning($"已拣选数量出现负数,重置为0。原值: {lockInfo.PickedQty + pickingRecord.PickQuantity}, 恢复数量: {pickingRecord.PickQuantity}");
|
lockInfo.PickedQty = 0;
|
}
|
|
_logger.LogInformation($"恢复锁定信息 - 已拣选数量从 {originalPickedQty} 减少到 {lockInfo.PickedQty}");
|
_logger.LogInformation($"锁定信息分配数量保持不变: {originalAssignQty}");
|
|
// 恢复锁定状态
|
if (lockInfo.PickedQty <= 0)
|
{
|
lockInfo.Status = (int)OutLockStockStatusEnum.出库中;
|
_logger.LogInformation($"锁定信息状态恢复为出库中");
|
}
|
else if (lockInfo.PickedQty < lockInfo.AssignQuantity)
|
{
|
lockInfo.Status = (int)OutLockStockStatusEnum.出库中; // 部分拣选状态
|
_logger.LogInformation($"锁定信息状态恢复为出库中(部分拣选)");
|
}
|
|
await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
|
|
// 恢复库存信息
|
var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
|
.FirstAsync(x => x.Barcode == pickingRecord.Barcode);
|
|
if (stockDetail == null)
|
{
|
throw new InvalidOperationException($"未找到对应的库存信息,条码: {pickingRecord.Barcode}");
|
}
|
|
decimal originalStockQty = stockDetail.StockQuantity;
|
decimal originalOutboundQty = stockDetail.OutboundQuantity;
|
|
stockDetail.StockQuantity += pickingRecord.PickQuantity;
|
stockDetail.OutboundQuantity -= pickingRecord.PickQuantity;
|
|
// 确保出库数量不会为负数
|
if (stockDetail.OutboundQuantity < 0)
|
{
|
_logger.LogWarning($"出库数量出现负数,重置为0。原值: {stockDetail.OutboundQuantity + pickingRecord.PickQuantity}, 恢复数量: {pickingRecord.PickQuantity}");
|
stockDetail.OutboundQuantity = 0;
|
}
|
|
_logger.LogInformation($"恢复库存信息 - 库存数量从 {originalStockQty} 增加到 {stockDetail.StockQuantity}");
|
_logger.LogInformation($"恢复库存信息 - 出库数量从 {originalOutboundQty} 减少到 {stockDetail.OutboundQuantity}");
|
|
// 恢复库存状态
|
if (stockDetail.StockQuantity > 0)
|
{
|
stockDetail.Status = (int)StockStatusEmun.出库锁定;
|
_logger.LogInformation($"库存状态恢复为出库锁定");
|
}
|
|
await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
|
|
_logger.LogInformation($"恢复拣选数据完成 - 条码: {pickingRecord.Barcode}");
|
|
return new RevertPickingResult
|
{
|
LockInfo = lockInfo,
|
StockDetail = stockDetail
|
};
|
}
|
|
#endregion
|
|
#region 数据更新方法
|
|
/// <summary>
|
/// 更新批次和订单数据 - 修正版
|
/// 确保只更新实际的拣选数量
|
/// </summary>
|
private async Task UpdateBatchAndOrderData(Dt_OutboundBatch batch, Dt_OutboundOrderDetail orderDetail, decimal pickedQty, string orderNo)
|
{
|
_logger.LogInformation($"开始更新批次和订单数据 - 拣选数量: {pickedQty}");
|
|
// 重新获取最新数据(防止并发问题)
|
var latestOrderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
|
.FirstAsync(x => x.Id == orderDetail.Id);
|
|
if (latestOrderDetail == null)
|
throw new InvalidOperationException("未找到订单明细");
|
|
orderDetail = latestOrderDetail;
|
|
// 验证拣选数量合理性
|
if (pickedQty <= 0)
|
{
|
_logger.LogWarning($"拣选数量无效: {pickedQty}");
|
return;
|
}
|
|
// 记录原始值
|
decimal originalOverOutQty = orderDetail.OverOutQuantity;
|
decimal originalAllocatedQty = orderDetail.AllocatedQuantity;
|
decimal originalLockQty = orderDetail.LockQuantity;
|
|
// 更新订单明细
|
orderDetail.OverOutQuantity += pickedQty; // 已出库数量增加
|
orderDetail.AllocatedQuantity -= pickedQty; // 已分配数量减少
|
orderDetail.LockQuantity -= pickedQty; // 锁定数量减少
|
|
// 确保数量不会为负数
|
if (orderDetail.AllocatedQuantity < 0)
|
{
|
_logger.LogWarning($"分配数量出现负数,重置为0。原值: {orderDetail.AllocatedQuantity + pickedQty}");
|
orderDetail.AllocatedQuantity = 0;
|
}
|
|
if (orderDetail.LockQuantity < 0)
|
{
|
_logger.LogWarning($"锁定数量出现负数,重置为0。原值: {orderDetail.LockQuantity + pickedQty}");
|
orderDetail.LockQuantity = 0;
|
}
|
|
// 更新批次分配状态
|
await UpdateBatchAllocateStatus(orderDetail);
|
|
await _outboundOrderDetailService.Db.Updateable(orderDetail).ExecuteCommandAsync();
|
|
_logger.LogInformation($"更新订单明细成功");
|
_logger.LogInformation($" 已出库数量: {originalOverOutQty} -> {orderDetail.OverOutQuantity}");
|
_logger.LogInformation($" 已分配数量: {originalAllocatedQty} -> {orderDetail.AllocatedQuantity}");
|
_logger.LogInformation($" 锁定数量: {originalLockQty} -> {orderDetail.LockQuantity}");
|
|
// 更新批次完成数量
|
if (batch != null)
|
{
|
decimal originalBatchCompletedQty = batch.CompletedQuantity;
|
batch.CompletedQuantity += pickedQty;
|
|
_logger.LogInformation($"更新批次完成数量 - 从 {originalBatchCompletedQty} 增加到 {batch.CompletedQuantity}");
|
|
// 更新批次状态
|
if (batch.CompletedQuantity >= batch.BatchQuantity)
|
{
|
batch.BatchStatus = (int)BatchStatusEnum.已完成;
|
_logger.LogInformation($"批次状态更新为已完成");
|
}
|
else if (batch.CompletedQuantity > 0)
|
{
|
batch.BatchStatus = (int)BatchStatusEnum.执行中;
|
_logger.LogInformation($"批次状态更新为执行中");
|
}
|
|
await _outboundBatchRepository.Db.Updateable(batch).ExecuteCommandAsync();
|
}
|
|
// 检查订单状态
|
await CheckAndUpdateOrderStatus(orderNo);
|
|
_logger.LogInformation($"批次和订单数据更新完成");
|
}
|
|
private async Task RevertBatchAndOrderData(Dt_PickingRecord pickingRecord, RevertPickingResult revertResult)
|
{
|
_logger.LogInformation($"开始恢复批次和订单数据");
|
|
// 恢复批次完成数量
|
var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
|
.FirstAsync(x => x.BatchNo == revertResult.LockInfo.OutboundBatchNo);
|
|
if (batch != null)
|
{
|
decimal originalCompletedQty = batch.CompletedQuantity;
|
batch.CompletedQuantity -= pickingRecord.PickQuantity;
|
if (batch.CompletedQuantity < 0)
|
{
|
batch.CompletedQuantity = 0;
|
_logger.LogWarning($"批次完成数量出现负数,重置为0");
|
}
|
_logger.LogInformation($"恢复批次完成数量 - 从 {originalCompletedQty} 减少到 {batch.CompletedQuantity}");
|
|
// 重新计算批次状态
|
if (batch.CompletedQuantity <= 0)
|
{
|
batch.BatchStatus = (int)BatchStatusEnum.分配中;
|
_logger.LogInformation($"批次状态恢复为分配中");
|
}
|
else if (batch.CompletedQuantity < batch.BatchQuantity)
|
{
|
batch.BatchStatus = (int)BatchStatusEnum.执行中;
|
_logger.LogInformation($"批次状态恢复为执行中");
|
}
|
|
await _outboundBatchRepository.Db.Updateable(batch).ExecuteCommandAsync();
|
}
|
|
// 恢复订单明细
|
var orderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
|
.FirstAsync(x => x.Id == pickingRecord.OrderDetailId);
|
|
if (orderDetail != null)
|
{
|
decimal originalOverOutQty = orderDetail.OverOutQuantity;
|
decimal originalAllocatedQty = orderDetail.AllocatedQuantity;
|
decimal originalLockQty = orderDetail.LockQuantity;
|
|
// 只恢复相关数量,分配数量保持不变
|
orderDetail.OverOutQuantity -= pickingRecord.PickQuantity;
|
orderDetail.AllocatedQuantity += pickingRecord.PickQuantity;
|
orderDetail.LockQuantity += pickingRecord.PickQuantity;
|
if (orderDetail.OverOutQuantity < 0) orderDetail.OverOutQuantity = 0;
|
if (orderDetail.AllocatedQuantity < 0) orderDetail.AllocatedQuantity = 0;
|
if (orderDetail.LockQuantity < 0) orderDetail.LockQuantity = 0;
|
|
_logger.LogInformation($"恢复订单明细 - 已出库数量从 {originalOverOutQty} 减少到 {orderDetail.OverOutQuantity}");
|
_logger.LogInformation($"订单明细分配数量保持不变: {originalAllocatedQty}");
|
_logger.LogInformation($"订单明细锁定数量保持不变: {originalLockQty}");
|
|
await UpdateBatchAllocateStatus(orderDetail);
|
await _outboundOrderDetailService.Db.Updateable(orderDetail).ExecuteCommandAsync();
|
}
|
|
// 重新检查订单状态
|
await CheckAndUpdateOrderStatus(pickingRecord.OrderNo);
|
|
_logger.LogInformation($"恢复批次和订单数据完成");
|
}
|
|
private async Task UpdateBatchAllocateStatus(Dt_OutboundOrderDetail orderDetail)
|
{
|
if (orderDetail.AllocatedQuantity >= orderDetail.NeedOutQuantity)
|
{
|
orderDetail.BatchAllocateStatus = OrderDetailStatusEnum.AssignOver.ObjToInt();
|
}
|
else if (orderDetail.AllocatedQuantity > 0)
|
{
|
orderDetail.BatchAllocateStatus = OrderDetailStatusEnum.AssignOverPartial.ObjToInt();
|
}
|
else
|
{
|
orderDetail.BatchAllocateStatus = OrderDetailStatusEnum.New.ObjToInt();
|
}
|
}
|
|
#endregion
|
|
/// <summary>
|
/// 给ESS下任务
|
/// </summary>
|
/// <param name="palletCode"></param>
|
/// <param name="targetAddress"></param>
|
/// <param name="returnTask"></param>
|
/// <returns></returns>
|
/// <exception cref="Exception"></exception>
|
private async Task SendESSCommands(string palletCode, string targetAddress, Dt_Task returnTask)
|
{
|
try
|
{
|
// 发送流动信号
|
var moveResult = await _eSSApiService.MoveContainerAsync(new WIDESEA_DTO.Basic.MoveContainerRequest
|
{
|
slotCode = movestations[targetAddress],
|
containerCode = palletCode
|
});
|
|
|
var essTask = new TaskModel()
|
{
|
taskType = "putaway",
|
taskGroupCode = "",
|
groupPriority = 0,
|
tasks = new List<TasksType>{ new() {
|
taskCode = returnTask.TaskNum.ToString(),
|
taskPriority = 0,
|
taskDescribe = new TaskDescribeType
|
{
|
containerCode = palletCode,
|
containerType = "CT_KUBOT_STANDARD",
|
fromLocationCode = stations.GetValueOrDefault(targetAddress) ?? "",
|
toStationCode = "",
|
toLocationCode = returnTask.TargetAddress,
|
deadline = 0,
|
storageTag = ""
|
}
|
} }
|
};
|
|
var resultTask = await _eSSApiService.CreateTaskAsync(essTask);
|
_logger.LogInformation($"ReturnRemaining 创建任务成功: {resultTask}");
|
|
}
|
catch (Exception ex)
|
{
|
_logger.LogError($"ReturnRemaining ESS命令发送失败: {ex.Message}");
|
throw new Exception($"ESS系统通信失败: {ex.Message}");
|
}
|
}
|
|
#region 辅助方法
|
|
private async Task<string> GenerateNewBarcode()
|
{
|
var seq = await _dailySequenceService.GetNextSequenceAsync();
|
return "WSLOT" + DateTime.Now.ToString("yyyyMMdd") + seq.ToString().PadLeft(5, '0');
|
}
|
|
private async Task RecordSplitHistory(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail, decimal splitQty, string newBarcode, bool isAutoSplit, decimal? originalStockQuantity = null)
|
{
|
var splitHistory = new Dt_SplitPackageRecord
|
{
|
OrderNo = lockInfo.OrderNo,
|
OutStockLockInfoId = lockInfo.Id,
|
StockId = stockDetail.StockId,
|
Operator = App.User.UserName,
|
OriginalBarcode = stockDetail.Barcode,
|
NewBarcode = newBarcode,
|
SplitQty = splitQty,
|
SplitTime = DateTime.Now,
|
Status = (int)SplitPackageStatusEnum.已拆包,
|
IsAutoSplit = isAutoSplit,
|
// SplitType = isAutoSplit ? "自动拆包" : "手动拆包"
|
OriginalStockQuantity = originalStockQuantity ?? stockDetail.StockQuantity,
|
//RemainingStockQuantity = stockDetail.StockQuantity - splitQty
|
TaskNum = lockInfo.TaskNum
|
};
|
|
await _splitPackageService.Db.Insertable(splitHistory).ExecuteCommandAsync();
|
}
|
|
private async Task RecordPickingHistory(PickingResult result, string orderNo, string palletCode)
|
{
|
var pickingRecord = new Dt_PickingRecord
|
{
|
OrderNo = orderNo,
|
// BatchNo = result.FinalLockInfo.BatchNo,
|
OrderDetailId = result.FinalLockInfo.OrderDetailId,
|
PalletCode = palletCode,
|
Barcode = result.FinalLockInfo.CurrentBarcode,
|
MaterielCode = result.FinalLockInfo.MaterielCode,
|
PickQuantity = result.ActualPickedQty,
|
PickTime = DateTime.Now,
|
Operator = App.User.UserName,
|
OutStockLockId = result.FinalLockInfo.Id,
|
|
BarcodeUnit = result.FinalLockInfo.BarcodeUnit,
|
BarcodeQty = result.FinalLockInfo.BarcodeQty,
|
BatchNo = result.FinalLockInfo.BatchNo,
|
lineNo = result.FinalLockInfo.lineNo,
|
SupplyCode = result.FinalLockInfo.SupplyCode,
|
WarehouseCode = result.FinalLockInfo.WarehouseCode,
|
// IsCancelled = false
|
};
|
|
await Db.Insertable(pickingRecord).ExecuteCommandAsync();
|
}
|
|
private async Task CheckAndUpdateOrderStatus(string orderNo)
|
{
|
var orderDetails = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
|
.LeftJoin<Dt_OutboundOrder>((detail, order) => detail.OrderId == order.Id)
|
.Where((detail, order) => order.OrderNo == orderNo)
|
.Select((detail, order) => detail)
|
.ToListAsync();
|
|
bool allCompleted = orderDetails.All(x => x.OverOutQuantity >= x.NeedOutQuantity);
|
|
var orderStatus = allCompleted ? (int)OutOrderStatusEnum.出库完成 : (int)OutOrderStatusEnum.出库中;
|
|
await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
|
.SetColumns(x => x.OrderStatus == orderStatus)
|
.Where(x => x.OrderNo == orderNo)
|
.ExecuteCommandAsync();
|
}
|
private string GetPalletStatusText(PalletStatusEnum status)
|
{
|
return status switch
|
{
|
PalletStatusEnum.未开始 => "未开始",
|
PalletStatusEnum.拣选中 => "拣选中",
|
PalletStatusEnum.已完成 => "已完成",
|
PalletStatusEnum.无任务 => "无任务",
|
_ => "未知"
|
};
|
}
|
#endregion
|
|
#region DTO类
|
|
/// <summary>
|
/// 回库任务信息类
|
/// </summary>
|
public class ReturnTaskInfo
|
{
|
/// <summary>
|
/// 是否需要发送ESS命令
|
/// </summary>
|
public bool ShouldSendESS { get; set; }
|
|
/// <summary>
|
/// 托盘码
|
/// </summary>
|
public string PalletCode { get; set; }
|
|
/// <summary>
|
/// 原始任务的目标地址
|
/// </summary>
|
public string OriginalTaskTargetAddress { get; set; }
|
|
/// <summary>
|
/// 回库任务
|
/// </summary>
|
public Dt_Task ReturnTask { get; set; }
|
}
|
public class PickedBarcodeInfo
|
{
|
public string Barcode { get; set; }
|
public decimal PickedQty { get; set; }
|
public int PickRecordCount { get; set; }
|
}
|
|
|
public class PickingResult
|
{
|
public Dt_OutStockLockInfo FinalLockInfo { get; set; }
|
public decimal ActualPickedQty { get; set; }
|
}
|
|
public class RevertPickingResult
|
{
|
public Dt_OutStockLockInfo LockInfo { get; set; }
|
public Dt_StockInfoDetail StockDetail { get; set; }
|
}
|
|
public class ValidationResult<T>
|
{
|
public bool IsValid { get; set; }
|
public string ErrorMessage { get; set; }
|
public T Data { get; set; }
|
|
public static ValidationResult<T> Success(T data) => new ValidationResult<T> { IsValid = true, Data = data };
|
public static ValidationResult<T> Error(string message) => new ValidationResult<T> { IsValid = false, ErrorMessage = message };
|
}
|
|
#endregion
|
}
|
}
|