pan
2025-11-29 fcdbb4d6cc8eb3629b871a4945ff2da599d64107
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundBatchPickingService.cs
@@ -6,11 +6,13 @@
using System.Text;
using System.Threading.Tasks;
using WIDESEA_BasicService;
using WIDESEA_Common.CommonEnum;
using WIDESEA_Common.OrderEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
using WIDESEA_DTO.Outbound;
using WIDESEA_IAllocateService;
using WIDESEA_IBasicService;
using WIDESEA_IOutboundService;
@@ -18,6 +20,7 @@
using WIDESEA_Model.Models;
using WIDESEA_Model.Models.Basic;
using WIDESEA_Model.Models.Outbound;
using static WIDESEA_OutboundService.OutboundBatchPickingService;
namespace WIDESEA_OutboundService
{
@@ -61,7 +64,7 @@
        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) : base(BaseDal)
            IRepository<Dt_Task> taskRepository, IESSApiService eSSApiService, ILogger<OutboundPickingService> logger, IInvokeMESService invokeMESService, IDailySequenceService dailySequenceService, IAllocateService allocateService, IRepository<Dt_OutboundBatch> outboundBatchRepository) : base(BaseDal)
        {
            _unitOfWorkManage = unitOfWorkManage;
            _stockInfoService = stockInfoService;
@@ -78,9 +81,341 @@
            _invokeMESService = invokeMESService;
            _dailySequenceService = dailySequenceService;
            _allocateService = allocateService;
            _outboundBatchRepository = outboundBatchRepository;
        }
        // <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> RemoveEmptyPallet(string orderNo, string palletCode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                //  éªŒè¯æ‰˜ç›˜æ˜¯å¦å¯ä»¥å–走(必须全部完成拣选)
                var validationResult = await ValidateEmptyPalletRemoval(orderNo, palletCode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var completedLocks = validationResult.Data;
                // æ¸…理锁定记录(标记为已完成)
                await CleanupCompletedLocks(completedLocks);
                // æ›´æ–°ç›¸å…³è®¢å•状态
                await UpdateOrderStatusAfterPalletRemoval(orderNo);
                //  è®°å½•操作历史
                // await RecordEmptyPalletRemoval(orderNo, palletCode, completedLocks);
                _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}");
            }
        }
        /// <summary>
        /// éªŒè¯ç©ºç®±å–走条件
        /// </summary>
        private async Task<ValidationResult<List<Dt_OutStockLockInfo>>> ValidateEmptyPalletRemoval(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 ValidationResult<List<Dt_OutStockLockInfo>>.Error("该托盘没有锁定记录");
            // æ£€æŸ¥æ˜¯å¦æœ‰æœªå®Œæˆçš„锁定记录
            var unfinishedLocks = lockInfos.Where(x =>
                x.Status == (int)OutLockStockStatusEnum.出库中 ||
                x.Status == (int)OutLockStockStatusEnum.回库中).ToList();
            if (unfinishedLocks.Any())
            {
                var unfinishedCount = unfinishedLocks.Count;
                var unfinishedQty = unfinishedLocks.Sum(x => x.AssignQuantity - x.PickedQty);
                return ValidationResult<List<Dt_OutStockLockInfo>>.Error(
                    $"托盘还有{unfinishedCount}条未完成记录,剩余数量{unfinishedQty},不能取走空箱");
            }
            // èŽ·å–å·²å®Œæˆçš„é”å®šè®°å½•
            var completedLocks = lockInfos.Where(x =>
                x.Status == (int)OutLockStockStatusEnum.拣选完成).ToList();
            if (!completedLocks.Any())
                return ValidationResult<List<Dt_OutStockLockInfo>>.Error("该托盘没有已完成拣选的记录");
            return ValidationResult<List<Dt_OutStockLockInfo>>.Success(completedLocks);
        }
        /// <summary>
        /// æ¸…理已完成的锁定记录
        /// </summary>
        private async Task CleanupCompletedLocks(List<Dt_OutStockLockInfo> completedLocks)
        {
            foreach (var lockInfo in completedLocks)
            {
                // æ ‡è®°é”å®šè®°å½•为已取走(可以新增状态或直接删除,根据业务需求)
                // è¿™é‡Œæˆ‘们将其状态更新为"已取走",并记录取走时间
                lockInfo.Status = (int)OutLockStockStatusEnum.已取走;
                lockInfo.Operator = App.User.UserName;
                await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
                // åŒæ—¶æ¸…理对应的库存记录状态
                await CleanupStockInfo(lockInfo);
            }
        }
        /// <summary>
        /// æ¸…理库存信息
        /// </summary>
        private async Task CleanupStockInfo(Dt_OutStockLockInfo lockInfo)
        {
            var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == lockInfo.CurrentBarcode && x.StockId == lockInfo.StockId);
            if (stockDetail != null)
            {
                // å¦‚果库存已经出库完成,标记为已清理
                if (stockDetail.Status == (int)StockStatusEmun.出库完成)
                {
                    stockDetail.Status = (int)StockStatusEmun.已清理;
                    await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
                }
            }
        }
        /// <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
        #region è¾…助方法
        private string GetPalletStatusText(PalletStatusEnum status)
        {
            return status switch
            {
                PalletStatusEnum.未开始 => "未开始",
                PalletStatusEnum.拣选中 => "拣选中",
                PalletStatusEnum.已完成 => "已完成",
                PalletStatusEnum.无任务 => "无任务",
                _ => "未知"
            };
        }
        #endregion
        #region åˆ†æ‰¹åˆ†æ‹£
        /// <summary>
@@ -216,8 +551,10 @@
        #region å–消拆包
        #region å–消拆包 - ä¿®å¤ç‰ˆæœ¬
        /// <summary>
        /// å–消拆包
        /// å–消拆包 - æ”¯æŒå¤šæ¬¡æ‹†åŒ…的情况
        /// </summary>
        public async Task<WebResponseContent> CancelSplitPackage(string orderNo, string palletCode, string newBarcode)
        {
@@ -225,15 +562,29 @@
            {
                _unitOfWorkManage.BeginTran();
                // æŸ¥æ‰¾æ‹†åŒ…记录并验证
                // 1. æŸ¥æ‰¾æ‹†åŒ…记录并验证
                var validationResult = await ValidateCancelSplitRequest(orderNo, palletCode, newBarcode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var (splitRecord, newLockInfo, newStockDetail) = validationResult.Data;
                // æ‰§è¡Œå–消拆包逻辑
                await ExecuteCancelSplitLogic(splitRecord, newLockInfo, newStockDetail);
                // 2. æŸ¥æ‰¾åŽŸå§‹é”å®šä¿¡æ¯
                var originalLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .FirstAsync(x => x.Id == splitRecord.OutStockLockInfoId);
                // 3. æ£€æŸ¥è¯¥æ¡ç æ˜¯å¦è¢«å†æ¬¡æ‹†åŒ…
                var childSplitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                    .Where(x => x.OriginalBarcode == newBarcode && !x.IsReverted)
                    .ToListAsync();
                if (childSplitRecords.Any())
                {
                    return WebResponseContent.Instance.Error("该条码已被再次拆包,请先取消后续的拆包操作");
                }
                // 4. æ‰§è¡Œå–消拆包逻辑
                await ExecuteCancelSplitLogic(splitRecord, originalLockInfo, newLockInfo, newStockDetail);
                _unitOfWorkManage.CommitTran();
@@ -242,10 +593,353 @@
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"取消拆包失败 - OrderNo: {orderNo}, Barcode: {newBarcode}, Error: {ex.Message}");
                _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)
        {
            // 1. æ¢å¤åŽŸé”å®šä¿¡æ¯
            // æ³¨æ„ï¼šè¿™é‡Œéœ€è¦ç´¯åŠ ï¼Œè€Œä¸æ˜¯ç®€å•çš„èµ‹å€¼ï¼Œå› ä¸ºå¯èƒ½æœ‰å¤šæ¬¡æ‹†åŒ…
            originalLockInfo.AssignQuantity += splitRecord.SplitQty;
            originalLockInfo.OrderQuantity += splitRecord.SplitQty;
            // å¦‚果原锁定信息的状态是拣选完成,需要重新设置为出库中
            if (originalLockInfo.Status == (int)OutLockStockStatusEnum.拣选完成)
            {
                originalLockInfo.Status = (int)OutLockStockStatusEnum.出库中;
            }
            await _outStockLockInfoService.Db.Updateable(originalLockInfo).ExecuteCommandAsync();
            // 2. æ¢å¤åŽŸåº“å­˜æ˜Žç»†
            var originalStock = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == splitRecord.OriginalBarcode && x.StockId == splitRecord.StockId);
            originalStock.StockQuantity += splitRecord.SplitQty;
            // å¦‚果原库存状态是出库完成,需要重新设置为出库锁定
            if (originalStock.Status == (int)StockStatusEmun.出库完成)
            {
                originalStock.Status = (int)StockStatusEmun.出库锁定;
            }
            await _stockInfoDetailService.Db.Updateable(originalStock).ExecuteCommandAsync();
            // 3. åˆ é™¤æ–°é”å®šä¿¡æ¯
            await _outStockLockInfoService.Db.Deleteable<Dt_OutStockLockInfo>()
                .Where(x => x.Id == newLockInfo.Id)
                .ExecuteCommandAsync();
            // 4. åˆ é™¤æ–°åº“存明细
            await _stockInfoDetailService.Db.Deleteable<Dt_StockInfoDetail>()
                .Where(x => x.Barcode == newLockInfo.CurrentBarcode)
                .ExecuteCommandAsync();
            // 5. æ ‡è®°æ‹†åŒ…记录为已撤销
            splitRecord.IsReverted = true;
            splitRecord.RevertTime = DateTime.Now;
            splitRecord.RevertOperator = App.User.UserName;
            await _splitPackageService.Db.Updateable(splitRecord).ExecuteCommandAsync();
            // 6. æ£€æŸ¥å¹¶æ›´æ–°æ‰¹æ¬¡å’Œè®¢å•状态
            await CheckAndUpdateBatchStatus(originalLockInfo.BatchNo);
            await CheckAndUpdateOrderStatus(originalLockInfo.OrderNo);
        }
        /// <summary>
        /// éªŒè¯å–消拆包请求 - å¢žå¼ºç‰ˆæœ¬
        /// </summary>
        private async Task<ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateCancelSplitRequest(
            string orderNo, string palletCode, string 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("未找到拆包记录");
            // æŸ¥æ‰¾æ–°é”å®šä¿¡æ¯
            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("未找到新锁定信息");
            // æ£€æŸ¥æ–°æ¡ç æ˜¯å¦å·²è¢«åˆ†æ‹£
            var pickingRecord = await Db.Queryable<Dt_PickingRecord>()
                .Where(x => x.Barcode == newBarcode && !x.IsCancelled)
                .FirstAsync();
            if (pickingRecord != null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("该条码已被分拣,无法取消拆包");
            // æ£€æŸ¥æ–°æ¡ç æ˜¯å¦è¢«å†æ¬¡æ‹†åŒ…
            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));
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error(
                    $"该条码已被再次拆包,生成的新条码:{childBarcodes},请先取消后续拆包");
            }
            var newStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == 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();
                // 1. æŸ¥æ‰¾æ‰€æœ‰ç›¸å…³çš„æ‹†åŒ…记录(形成拆包链)
                var splitChain = await GetSplitPackageChain(orderNo, startBarcode);
                if (!splitChain.Any())
                    return WebResponseContent.Instance.Error("未找到拆包记录");
                // 2. æŒ‰æ‹†åŒ…顺序倒序取消(从最新的开始取消)
                var reversedChain = splitChain.OrderByDescending(x => x.SplitTime).ToList();
                foreach (var splitRecord in reversedChain)
                {
                    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>
        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)
        {
            // æŸ¥æ‰¾ç›¸å…³æ•°æ®
            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);
        }
        #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
                    }).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 DTOç±»
        public class SplitPackageChainInfoDto
        {
            public string OriginalBarcode { get; set; }
            public string RootBarcode { get; set; } // æ–°å¢žï¼šæ ¹æ¡ç 
            public int TotalSplitTimes { get; set; }
            public string ChainType { get; set; } // "root" æˆ– "branch"
            public List<SplitChainItemDto> SplitChain { get; set; }
        }
        public class SplitChainItemDto
        {
            public DateTime SplitTime { get; set; }
            public string OriginalBarcode { get; set; }
            public string NewBarcode { get; set; }
            public decimal SplitQuantity { get; set; }
            public string Operator { get; set; }
            public bool IsReverted { get; set; }
        }
        #endregion
        #endregion
@@ -337,11 +1031,11 @@
                    $"库存数量不足,需要:{lockInfo.AssignQuantity},实际:{stockDetail.StockQuantity}");
            var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
                .FirstAsync(x => x.BatchNo == lockInfo.BatchNo);
                .FirstAsync(x => x.BatchNo == lockInfo.OutboundBatchNo);
            return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Success((lockInfo, orderDetail, stockDetail, batch));
        }
        private async Task<ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateSplitRequest(
            string orderNo, string palletCode, string originalBarcode, decimal splitQuantity)
        {
@@ -366,42 +1060,7 @@
            return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Success((lockInfo, stockDetail));
        }
        private async Task<ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateCancelSplitRequest(
            string orderNo, string palletCode, string 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("未找到拆包记录");
            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("未找到新锁定信息");
            // æ£€æŸ¥æ–°æ¡ç æ˜¯å¦å·²è¢«åˆ†æ‹£
            var pickingRecord = await Db.Queryable<Dt_PickingRecord>()
                .Where(x => x.Barcode == newBarcode && !x.IsCancelled)
                .FirstAsync();
            if (pickingRecord != null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("该条码已被分拣,无法取消拆包");
            var newStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == newBarcode);
            return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Success((splitRecord, newLockInfo, newStockDetail));
        }
        #endregion
        #region æ ¸å¿ƒé€»è¾‘方法
@@ -479,7 +1138,13 @@
                Barcode = newBarcode,
                Status = (int)StockStatusEmun.出库锁定,
                SupplyCode = stockDetail.SupplyCode,
                Unit = stockDetail.Unit
                Unit = stockDetail.Unit,
                BarcodeQty=stockDetail.BarcodeQty,
                BarcodeUnit=stockDetail.BarcodeUnit,
                BusinessType=stockDetail.BusinessType,
                InboundOrderRowNo=stockDetail.InboundOrderRowNo,
            };
            await _stockInfoDetailService.Db.Insertable(newStockDetail).ExecuteCommandAsync();
@@ -487,23 +1152,39 @@
            stockDetail.StockQuantity -= splitQuantity;
            await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
            // åˆ›å»ºæ–°é”å®šä¿¡æ¯
            var newLockInfo = new Dt_OutStockLockInfo
            {
                OrderNo = lockInfo.OrderNo,
                OrderDetailId = lockInfo.OrderDetailId,
                OutboundBatchNo = lockInfo.OutboundBatchNo,
                BatchNo = lockInfo.BatchNo,
                MaterielCode = lockInfo.MaterielCode,
                MaterielName = lockInfo.MaterielName,
                StockId = lockInfo.StockId,
                OrderQuantity = splitQuantity,
                //OriginalQuantity = quantity,
                AssignQuantity = splitQuantity,
                PickedQty = 0,
                LocationCode = lockInfo.LocationCode,
                PalletCode = palletCode,
                PalletCode = lockInfo.PalletCode,
                TaskNum = lockInfo.TaskNum,
                Status = (int)OutLockStockStatusEnum.出库中,
                Unit = lockInfo.Unit,
                SupplyCode = lockInfo.SupplyCode,
                OrderType = lockInfo.OrderType,
                CurrentBarcode = newBarcode,
               // OriginalLockQuantity = quantity,
                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();
            // æ›´æ–°åŽŸé”å®šä¿¡æ¯