wanshenmean
7 小时以前 5bf10c1dafe485d506ec534f98e5220a3b83dd17
feat(WCS&WMS): 机械手扫码NG处理与线体条码读取与添加批量MES绑定解绑接口

- RobotBarcodeGenerator 改为从DB地址读取电芯条码
- RobotSocketState 新增 IsScanNG 属性跟踪扫码状态
- SendSocketRobotPickAsync 增加 isScanNG 参数
- 组盘放货时判断扫码NG则放至NG口
- 修复 HandlePutFinished 任务计数与状态更新逻辑

feat(WCS): 调用批量组盘拆盘确认接口

- ConfigKey 新增 SplitPalletConfirm 和 GroupPalletConfirm
- ApiRouteCacheWarmupHostedService 添加路由映射
- RobotTaskProcessor 新增 PostSplitPalletConfirmAsync 和 PostGroupPalletConfirmAsync
- allpickfinished: 拆盘任务和换盘任务(Phase==0)调用批量拆盘确认
- allputfinished: 组盘任务和换盘任务(Phase==0)调用批量组盘确认

fix(StockService): 使用ExecuteCommandAsync替代ExecuteCommand保持异步一致性

feat(db): 新增Dt_SplitTemp拆盘临时表

feat(StockController): 新增SplitPalletConfirm和GroupPalletConfirm接口路由

feat(SplitPalletAsync): 添加临时表幂等写入逻辑

fix(StockService): 修复批量确认方法代码质量问题

- 使用ExecuteCommandAsync替代ExecuteCommand
- 添加QueryData结果空值检查
- 补充XML文档注释

feat(StockService): 实现SplitPalletConfirmAsync和GroupPalletConfirmAsync

feat(IStockService): 新增SplitPalletConfirmAsync和GroupPalletConfirmAsync接口

feat(DTO): 新增批量组盘拆盘确认请求DTO

feat(Stock): 新增Dt_SplitTemp拆盘临时表实体

docs: 添加批量MES绑定接口实施计划

docs: 添加批量MES绑定解绑接口设计文档
已添加6个文件
已修改11个文件
1154 ■■■■■ 文件已修改
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/HttpEnum/ConfigKey.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/HostedService/ApiRouteCacheWarmupHostedService.cs 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotBarcodeGenerator.cs 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotSocketState.cs 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs 84 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotPrefixCommandHandler.cs 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/Database/Scripts/20260416_Dt_SplitTemp.sql 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/GroupPalletConfirmRequestDto.cs 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/SplitPalletConfirmRequestDto.cs 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_IStockService/IStockService.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Model/Models/Stock/Dt_SplitTemp.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockController.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/plans/2026-04-16-BatchMesBinding-Plan.md 485 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/specs/2026-04-16-BatchMesBinding-Design.md 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/HttpEnum/ConfigKey.cs
@@ -82,7 +82,17 @@
        /// <summary>
        /// ç©ºæ‰˜ç›˜å‡ºåº“完成(根据任务号和托盘号通知WMS空托盘出库完成)
        /// </summary>
        OutboundFinishTaskTray
        OutboundFinishTaskTray,
        /// <summary>
        /// æ‰¹é‡æ‹†ç›˜ç¡®è®¤
        /// </summary>
        SplitPalletConfirm,
        /// <summary>
        /// æ‰¹é‡ç»„盘确认
        /// </summary>
        GroupPalletConfirm
        #endregion WMS接口
    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/HostedService/ApiRouteCacheWarmupHostedService.cs
@@ -28,7 +28,9 @@
            (nameof(ConfigKey.CreateRobotGroupPalletTask), "Task/CreateRobotGroupPalletTask"),
            (nameof(ConfigKey.CreateRobotChangePalletTask), "Task/CreateRobotChangePalletTask"),
            (nameof(ConfigKey.CreateRobotSplitPalletTask), "Task/CreateRobotSplitPalletTask"),
            (nameof(ConfigKey.OutboundFinishTaskTray), "Task/OutboundFinishTaskTray")
            (nameof(ConfigKey.OutboundFinishTaskTray), "Task/OutboundFinishTaskTray"),
            (nameof(ConfigKey.SplitPalletConfirm), "Stock/SplitPalletConfirm"),
            (nameof(ConfigKey.GroupPalletConfirm), "Stock/GroupPalletConfirm")
        };
        private readonly ICacheService _cache;
@@ -53,7 +55,7 @@
                warmedCount++;
            }
            _logger.LogInformation(":API路由缓存预热完成。计数={Count}", warmedCount);
            _logger.LogInformation("��API·�ɻ���Ԥ����ɡ�����={Count}", warmedCount);
            return Task.CompletedTask;
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotBarcodeGenerator.cs
@@ -1,36 +1,27 @@
using Masuit.Tools;
using WIDESEAWCS_QuartzJob;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æœºæ¢°æ‰‹æ¡ç ç”Ÿæˆå™¨ - è´Ÿè´£ç”Ÿæˆæ‰˜ç›˜æ¡ç 
    /// æœºæ¢°æ‰‹æ¡ç è¯»å–器 - è´Ÿè´£è¯»å–电芯条码
    /// </summary>
    /// <remarks>
    /// æ¡ç æ ¼å¼ï¼šå‰ç¼€ï¼ˆå¯é€‰ï¼‰+ æ—¥æœŸï¼ˆyyyyMMdd)+ æ—¶é—´ï¼ˆHHmmss)+ éšæœºæ•°ï¼ˆ100-999)
    /// ä¾‹å¦‚:TRAY20260326093045123
    /// </remarks>
    public static class RobotBarcodeGenerator
    {
        /// <summary>
        /// ç”Ÿæˆæ‰˜ç›˜æ¡ç 
        /// è¯»å–线体条码
        /// </summary>
        /// <param name="prefix">条码前缀,默认为空字符串,例如 "TRAY"</param>
        /// <returns>生成的条码字符串,格式:前缀+日期+时间+随机数</returns>
        /// <param name="prefix">DB点位,例如 "DB40.990"</param>
        /// <returns>读取到的电芯条码</returns>
        public static string GenerateTrayBarcode(string prefix = "")
        {
            // èŽ·å–å½“å‰æ—¥æœŸï¼Œæ ¼å¼åŒ–ä¸º yyyyMMdd(8位数字)
            // ä¾‹å¦‚:20260326
            string datePart = DateTime.Now.ToString("yyyyMMdd");
            // èŽ·å–å½“å‰æ—¶é—´ï¼ˆæ—¶åˆ†ç§’ï¼‰ï¼Œæ ¼å¼åŒ–ä¸º HHmmss(6位数字)
            // ä¾‹å¦‚:093045 è¡¨ç¤º 09:30:45
            string timePart = DateTime.Now.ToString("HHmmss");
            // ç”Ÿæˆ3位随机数,范围 100-999,确保条码唯一性
            // ä½¿ç”¨ Random.Shared èŽ·å–çº¿ç¨‹å®‰å…¨çš„éšæœºæ•°ç”Ÿæˆå™¨
            string randomPart = Random.Shared.Next(100, 1000).ToString();
            // ç»„合所有部分:前缀 + æ—¥æœŸ + æ—¶é—´ + éšæœºæ•°
            // å¦‚果前缀为空,则直接返回日期+时间+随机数的组合
            return prefix + datePart + timePart + randomPart;
            var device = Storage.Devices.Where(d => d.DeviceName == "A区_一注输送线").FirstOrDefault();
            if (!device.IsNullOrEmpty() && device != null && device.Communicator.IsConnected)
            {
                var trayBarcode = device.Communicator.Read<string>(prefix);
                return trayBarcode;
            }
            return "";
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotSocketState.cs
@@ -203,5 +203,13 @@
        /// 4: æ”¾å‡ç”µèŠ¯åˆ°5号位(流向B Phase2)
        /// </remarks>
        public int ChangePalletPhase { get; set; }
        /// <summary>
        /// æ˜¯å¦æ‰«ç NG
        /// </summary>
        /// <remarks>
        /// æ‹‰å¸¦çº¿ä¸Šç”µèŠ¯æ‰«ç æ˜¯å¦NG。
        /// </remarks>
        public bool IsScanNG { get; set; } = false;
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs
@@ -164,7 +164,8 @@
        /// </remarks>
        /// <param name="task">要下发的任务对象</param>
        /// <param name="state">机器人当前状态</param>
        public async Task SendSocketRobotPickAsync(Dt_RobotTask task, RobotSocketState state)
        /// <param name="isScanNG">是否扫码NG</param>
        public async Task SendSocketRobotPickAsync(Dt_RobotTask task, RobotSocketState state, bool isScanNG)
        {
            // æž„建取货指令,格式:Pickbattery,{源地址}
            string taskString = $"Pickbattery,{task.RobotSourceAddress}";
@@ -183,6 +184,11 @@
                // å°†ä»»åŠ¡å…³è”åˆ°çŠ¶æ€å¯¹è±¡
                state.CurrentTask = task;
                if(isScanNG)
                {
                    state.IsScanNG = true;
                }
                // ä¿æŒåŽŸè¯­ä¹‰ï¼šä»…åœ¨çŠ¶æ€å®‰å…¨å†™å…¥æˆåŠŸåŽå†æ›´æ–°ä»»åŠ¡çŠ¶æ€
                // è¿™æ ·å¯ä»¥ç¡®ä¿çŠ¶æ€å’Œä»»åŠ¡è®°å½•çš„ä¸€è‡´æ€§
@@ -514,37 +520,37 @@
            }
            // è§£æžè¿”回的任务信息
            var taskInfos = JsonConvert.DeserializeObject<List<Dt_Task>>(content.Data.ToJson() ?? string.Empty) ?? new List<Dt_Task>();
            var taskInfo = taskInfos.FirstOrDefault();
            //var taskInfos = JsonConvert.DeserializeObject<List<Dt_Task>>(content.Data.ToJson() ?? string.Empty) ?? new List<Dt_Task>();
            //var taskInfo = taskInfos.FirstOrDefault();
            // èŽ·å–æºåœ°å€
            string sourceAddress = taskDTO.SourceAddress;
            //// èŽ·å–æºåœ°å€
            //string sourceAddress = taskDTO.SourceAddress;
            // æŸ¥æ‰¾æºåœ°å€å¯¹åº”的输送线设备
            IDevice? device = Storage.Devices.FirstOrDefault(x => x.DeviceProDTOs.Any(d => d.DeviceChildCode == sourceAddress));
            //// æŸ¥æ‰¾æºåœ°å€å¯¹åº”的输送线设备
            //IDevice? device = Storage.Devices.FirstOrDefault(x => x.DeviceProDTOs.Any(d => d.DeviceChildCode == sourceAddress));
            if (device != null)
            {
                // å°†è®¾å¤‡è½¬æ¢ä¸ºè¾“送线类型
                CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
            //if (device != null)
            //{
            //    // å°†è®¾å¤‡è½¬æ¢ä¸ºè¾“送线类型
            //    CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
                // è®¾ç½®è¾“送线的目标地址
                conveyorLine.SetValue(ConveyorLineDBNameNew.Target, taskInfo.NextAddress, sourceAddress);
            //    // è®¾ç½®è¾“送线的目标地址
            //    conveyorLine.SetValue(ConveyorLineDBNameNew.Target, taskInfo.NextAddress, sourceAddress);
                // è®¾ç½®è¾“送线的任务号
                conveyorLine.SetValue(ConveyorLineDBNameNew.TaskNo, taskInfo.TaskNum, sourceAddress);
            //    // è®¾ç½®è¾“送线的任务号
            //    conveyorLine.SetValue(ConveyorLineDBNameNew.TaskNo, taskInfo.TaskNum, sourceAddress);
                // è§¦å‘输送线开始执行(写入 WCS_ACK = 1)
                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, (short)1, sourceAddress);
            //    // è§¦å‘输送线开始执行(写入 WCS_ACK = 1)
            //    conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, (short)1, sourceAddress);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€åˆ°ä¸‹ä¸€é˜¶æ®µ
                if (_taskService.UpdateTaskStatusToNext(taskInfo).Status)
                {
                    _logger.LogInformation("HandleInboundTaskAsync:入库任务处理成功,任务号: {TaskNum}", taskInfo.TaskNum);
                    QuartzLogger.Info($"HandleInboundTaskAsync:入库任务处理成功,任务号: {taskInfo.TaskNum}", state.RobotCrane?.DeviceName ?? "Unknown");
                    return true;
                }
            }
            //    // æ›´æ–°ä»»åŠ¡çŠ¶æ€åˆ°ä¸‹ä¸€é˜¶æ®µ
            //    if (_taskService.UpdateTaskStatusToNext(taskInfo).Status)
            //    {
            //        _logger.LogInformation("HandleInboundTaskAsync:入库任务处理成功,任务号: {TaskNum}", taskInfo.TaskNum);
            //        QuartzLogger.Info($"HandleInboundTaskAsync:入库任务处理成功,任务号: {taskInfo.TaskNum}", state.RobotCrane?.DeviceName ?? "Unknown");
            //        return true;
            //    }
            //}
            return false;
        }
@@ -632,5 +638,33 @@
        {
            return _httpClientHelper.Post<WebResponseContent>(configKey, stockDTO.ToJson());
        }
        /// <summary>
        /// è°ƒç”¨æ‰¹é‡æ‹†ç›˜ç¡®è®¤ API
        /// </summary>
        /// <remarks>
        /// å½“拆盘任务全部取完时调用,一次性上传整个托盘的解绑数据到 MES。
        /// </remarks>
        /// <param name="palletCode">源托盘号</param>
        /// <returns>HTTP å“åº”结果</returns>
        public HttpResponseResult<WebResponseContent> PostSplitPalletConfirmAsync(string palletCode)
        {
            var request = new { PalletCode = palletCode };
            return _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.SplitPalletConfirm), request.ToJson());
        }
        /// <summary>
        /// è°ƒç”¨æ‰¹é‡ç»„盘确认 API
        /// </summary>
        /// <remarks>
        /// å½“组盘任务全部放完时调用,一次性上传整个托盘的绑定数据到 MES。
        /// </remarks>
        /// <param name="palletCode">目标托盘号</param>
        /// <returns>HTTP å“åº”结果</returns>
        public HttpResponseResult<WebResponseContent> PostGroupPalletConfirmAsync(string palletCode)
        {
            var request = new { PalletCode = palletCode };
            return _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.GroupPalletConfirm), request.ToJson());
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotPrefixCommandHandler.cs
@@ -145,20 +145,27 @@
                // ä»Žæ•°æ®åº“重新查询当前任务(确保获取最新状态)
                var task = await _robotTaskService.Repository.QueryFirstAsync(x => x.RobotTaskId == state.CurrentTask.RobotTaskId);
                // æ ¹æ®å‘½ä»¤å‰ç¼€åˆ†å‘处理
                if (cmd.StartsWith("pickfinished"))
                if (task != null)
                {
                    // å¤„理取货完成
                    await HandlePickFinishedAsync(state, positions, task);
                }
                else if (cmd.StartsWith("putfinished"))
                {
                    // å¤„理放货完成
                    await HandlePutFinishedAsync(state, positions, task);
                }
                    // æ ¹æ®å‘½ä»¤å‰ç¼€åˆ†å‘处理
                    if (cmd.StartsWith("pickfinished"))
                    {
                        // å¤„理取货完成
                        await HandlePickFinishedAsync(state, positions, task);
                    }
                    else if (cmd.StartsWith("putfinished"))
                    {
                        // å¤„理放货完成
                        await HandlePutFinishedAsync(state, positions, task);
                    }
                // å›žå†™åŽŸæ¶ˆæ¯åˆ°å®¢æˆ·ç«¯ï¼ˆä¿æŒåŽŸæœ‰è¡Œä¸ºï¼‰
                await _socketClientGateway.SendMessageAsync(client, message);
                    // å›žå†™åŽŸæ¶ˆæ¯åˆ°å®¢æˆ·ç«¯ï¼ˆä¿æŒåŽŸæœ‰è¡Œä¸ºï¼‰
                    await _socketClientGateway.SendMessageAsync(client, message);
                }
                else
                {
                    Console.WriteLine($"RobotJob HandleAsync Warning: Current task not found for RobotTaskId {state.CurrentTask.RobotTaskId}");
                }
            }
            catch (Exception ex)
            {
@@ -306,9 +313,12 @@
                    putSuccess = result.Data.Status && result.IsSuccess;
                    // å¢žåŠ ä»»åŠ¡è®¡æ•°
                    state.RobotTaskTotalNum += positions.Length;
                    if (task != null)
                        task.RobotTaskTotalNum -= positions.Length;
                    if (!state.IsScanNG)
                    {
                        state.RobotTaskTotalNum += positions.Length;
                        if (task != null)
                            task.RobotTaskTotalNum -= positions.Length;
                    }
                }
            }
@@ -317,6 +327,7 @@
            {
                // æ›´æ–°å½“前动作为"放货完成"
                state.CurrentAction = "PutFinished";
                state.IsScanNG = false;
                // éžç»„盘任务时增加计数(组盘任务已在上面递增)
                if (!state.IsGroupPallet)
@@ -325,18 +336,18 @@
                    if (task != null)
                        task.RobotTaskTotalNum -= positions.Length;
                }
            }
            // å¦‚果任务存在
            if (task != null)
            {
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人放货完成"
                task.RobotTaskState = TaskRobotStatusEnum.RobotPutFinish.GetHashCode();
                // å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis
                if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                // å¦‚果任务存在
                if (task != null)
                {
                    await _robotTaskService.Repository.UpdateDataAsync(task);
                    // æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人放货完成"
                    task.RobotTaskState = TaskRobotStatusEnum.RobotPutFinish.GetHashCode();
                    // å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis
                    if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                    {
                        await _robotTaskService.Repository.UpdateDataAsync(task);
                    }
                }
            }
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs
@@ -143,6 +143,14 @@
                        {
                            if (state.ChangePalletPhase == 0)
                            {
                                // è°ƒç”¨æ‰¹é‡æ‹†ç›˜ç¡®è®¤æŽ¥å£ï¼ˆæ¢ç›˜å–完阶段)
                                var sourcePallet = state.CurrentTask.RobotSourceAddressPalletCode;
                                var confirmResult = _taskProcessor.PostSplitPalletConfirmAsync(sourcePallet);
                                if (!confirmResult.IsSuccess)
                                {
                                    QuartzLogger.Error($"批量拆盘确认失败: {confirmResult.ErrorMessage}", state.RobotCrane?.DeviceName ?? "Unknown");
                                }
                                // æ‰€æœ‰é˜¶æ®µå®Œæˆï¼Œå¤„理入库
                                if (await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true))
                                {
@@ -165,6 +173,14 @@
                        // æ‹†ç›˜ä»»åŠ¡ï¼šç›´æŽ¥å¤„ç†å…¥åº“
                        if (robotTaskType == RobotTaskTypeEnum.SplitPallet)
                        {
                            // è°ƒç”¨æ‰¹é‡æ‹†ç›˜ç¡®è®¤æŽ¥å£
                            var sourcePallet = state.CurrentTask.RobotSourceAddressPalletCode;
                            var confirmResult = _taskProcessor.PostSplitPalletConfirmAsync(sourcePallet);
                            if (!confirmResult.IsSuccess)
                            {
                                QuartzLogger.Error($"批量拆盘确认失败: {confirmResult.ErrorMessage}", state.RobotCrane?.DeviceName ?? "Unknown");
                            }
                            if (await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true))
                            {
                                // å…¥åº“成功,删除任务记录
@@ -198,6 +214,14 @@
                        {
                            if (state.ChangePalletPhase == 0)
                            {
                                // è°ƒç”¨æ‰¹é‡ç»„盘确认接口(换盘放完阶段)
                                var targetPallet = state.CurrentTask.RobotTargetAddressPalletCode;
                                var confirmResult = _taskProcessor.PostGroupPalletConfirmAsync(targetPallet);
                                if (!confirmResult.IsSuccess)
                                {
                                    QuartzLogger.Error($"批量组盘确认失败: {confirmResult.ErrorMessage}", state.RobotCrane?.DeviceName ?? "Unknown");
                                }
                                // æ‰€æœ‰é˜¶æ®µå®Œæˆï¼Œå¤„理入库
                                if (await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false))
                                {
@@ -226,6 +250,14 @@
                        // ç»„盘任务:直接处理入库
                        if (robotTaskType == RobotTaskTypeEnum.GroupPallet)
                        {
                            // è°ƒç”¨æ‰¹é‡ç»„盘确认接口
                            var targetPallet = state.CurrentTask.RobotTargetAddressPalletCode;
                            var confirmResult = _taskProcessor.PostGroupPalletConfirmAsync(targetPallet);
                            if (!confirmResult.IsSuccess)
                            {
                                QuartzLogger.Error($"批量组盘确认失败: {confirmResult.ErrorMessage}", state.RobotCrane?.DeviceName ?? "Unknown");
                            }
                            // å¤„理入库任务回传
                            // useSourceAddress: false è¡¨ç¤ºä½¿ç”¨ç›®æ ‡åœ°å€ï¼ˆç»„盘场景)
                            if (await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false))
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs
@@ -5,6 +5,7 @@
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_Tasks.SocketServer;
using WIDESEAWCS_Tasks.Workflow.Abstractions;
namespace WIDESEAWCS_Tasks.Workflow
@@ -163,11 +164,12 @@
        {
            string taskString;
            var state = _stateManager.GetState(ipAddress);
            // æ¢ç›˜ä»»åŠ¡ä½¿ç”¨æ‰¹æ¬¡æ ¼å¼
            if (task.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode())
            {
                int targetNormalCount = task.RobotTaskTotalNum;
                var state = _stateManager.GetState(ipAddress);
                int currentCompletedCount = state?.RobotTaskTotalNum ?? 0;
                bool isFlowA = task.RobotSourceAddressLineCode is "11001" or "11010";
@@ -229,7 +231,20 @@
            else
            {
                // éžæ¢ç›˜ä»»åŠ¡ï¼šä½¿ç”¨åŽŸæœ‰æ ¼å¼
                taskString = $"Putbattery,{task.RobotTargetAddress}";
                if (state != null && state.IsGroupPallet && task.RobotTaskType == RobotTaskTypeEnum.GroupPallet.GetHashCode())
                {
                    // ç»„盘任务:放货需判断是否NG,如果NG则放到NG口
                    if (state.IsScanNG)
                    {
                        taskString = $"Putbattery,NG";
                    }
                    else
                    {
                        taskString = $"Putbattery,{task.RobotTargetAddress}";
                    }
                }
                else
                    taskString = $"Putbattery,{task.RobotTargetAddress}";
            }
            bool result = await _clientManager.SendToClientAsync(ipAddress, taskString);
@@ -301,46 +316,49 @@
            // å¦‚果是组盘任务
            if (task.RobotTaskType == RobotTaskTypeEnum.GroupPallet.GetHashCode())
            {
                // ç”Ÿæˆæ‰˜ç›˜æ¡ç å‰ç¼€
                const string prefix = "TRAY";
                // ç”Ÿæˆä¸¤ä¸ªæ‰˜ç›˜æ¡ç ï¼ˆç”¨äºŽç»„盘操作)(测试用,后续读取线体条码)
                string trayBarcode1 = RobotBarcodeGenerator.GenerateTrayBarcode(prefix);
                string trayBarcode2 = RobotBarcodeGenerator.GenerateTrayBarcode(prefix);
                // è¯»å–线体电芯条码
                string trayBarcode1 = RobotBarcodeGenerator.GenerateTrayBarcode("DB40.990");
                string trayBarcode2 = RobotBarcodeGenerator.GenerateTrayBarcode("DB40.1020");
                // å¦‚果条码生成成功
                if (!string.IsNullOrEmpty(trayBarcode1) && !string.IsNullOrEmpty(trayBarcode2))
                {
                    if(stateForUpdate.CellBarcode.Contains(trayBarcode1)|| stateForUpdate.CellBarcode.Contains(trayBarcode2))
                    if (stateForUpdate.CellBarcode.Contains(trayBarcode1) || stateForUpdate.CellBarcode.Contains(trayBarcode2))
                    {
                        _logger.LogError("HandlePutFinishedStateAsync:生成的托盘条码已存在,可能存在重复,任务号: {TaskNum}", task.RobotTaskNum);
                        QuartzLogger.Error($"生成的托盘条码已存在,可能存在重复", stateForUpdate.RobotCrane.DeviceName);
                        _logger.LogError("HandlePutFinishedStateAsync:读取的托盘条码已存在,可能存在重复,任务号: {TaskNum}", task.RobotTaskNum);
                        QuartzLogger.Error($"读取的托盘条码已存在,可能存在重复", stateForUpdate.RobotCrane.DeviceName);
                        // æ¡ç é‡å¤ï¼Œè®°å½•错误日志并停止后续操作(后续放货时会用到这些条码信息,供后续放货时使用,调试后可能会取消此逻辑)
                        return;
                        // å‘送取货指令 æ ‡è®°æ‰«ç NG,放货时不使用这些条码,并放入NG口
                        await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate, true);
                    }
                    else
                    {
                        _logger.LogInformation("HandlePutFinishedStateAsync:生成的托盘条码唯一,继续执行,任务号: {TaskNum}", task.RobotTaskNum);
                        QuartzLogger.Info($"生成的托盘条码唯一,继续执行", stateForUpdate.RobotCrane.DeviceName);
                        _logger.LogInformation("HandlePutFinishedStateAsync:读取的托盘条码唯一,继续执行,任务号: {TaskNum}", task.RobotTaskNum);
                        QuartzLogger.Info($"读取的托盘条码唯一,继续执行", stateForUpdate.RobotCrane.DeviceName);
                        // å°†æ¡ç æ·»åŠ åˆ°çŠ¶æ€ä¸­ï¼Œä¾›åŽç»­æ”¾è´§æ—¶ä½¿ç”¨
                        stateForUpdate.CellBarcode.Add(trayBarcode1);
                        stateForUpdate.CellBarcode.Add(trayBarcode2);
                    }
                    // è®°å½•日志:生成托盘条码成功
                    _logger.LogInformation("HandlePutFinishedStateAsync:生成托盘条码成功: {Barcode1}+{Barcode2},任务号: {TaskNum}", trayBarcode1, trayBarcode2, task.RobotTaskNum);
                    QuartzLogger.Info($"生成托盘条码成功: {trayBarcode1}+{trayBarcode2}", stateForUpdate.RobotCrane.DeviceName);
                    // è®°å½•日志:读取托盘条码成功
                    _logger.LogInformation("HandlePutFinishedStateAsync:读取托盘条码成功: {Barcode1}+{Barcode2},任务号: {TaskNum}", trayBarcode1, trayBarcode2, task.RobotTaskNum);
                    QuartzLogger.Info($"读取托盘条码成功: {trayBarcode1}+{trayBarcode2}", stateForUpdate.RobotCrane.DeviceName);
                    // å‘送取货指令
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate);
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate, false);
                }
                else
                {
                    // æ¡ç ç”Ÿæˆå¤±è´¥ï¼Œè®°å½•错误日志
                    _logger.LogError("HandlePutFinishedStateAsync:生成托盘条码失败,任务号: {TaskNum}", task.RobotTaskNum);
                    QuartzLogger.Error($"生成托盘条码失败", stateForUpdate.RobotCrane.DeviceName);
                    // æ¡ç è¯»å–失败,记录错误日志
                    _logger.LogError("HandlePutFinishedStateAsync:读取托盘条码失败,任务号: {TaskNum}", task.RobotTaskNum);
                    QuartzLogger.Error($"读取托盘条码失败", stateForUpdate.RobotCrane.DeviceName);
                    // å‘送取货指令 æ ‡è®°æ‰«ç NG,放货时不使用这些条码,并放入NG口
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate, true);
                }
            }
            else if (task.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode())
@@ -355,7 +373,7 @@
                // ç›®æ ‡æ•°é‡ä¸º48:直接走原有逻辑,不进入批次模式
                if (targetNormalCount == targetTotal)
                {
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate);
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate, false);
                    return;
                }
@@ -524,7 +542,7 @@
            else
            {
                // éžç»„盘任务,直接发送取货指令
                await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate);
                await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate, false);
            }
        }
    }
Code/WMS/WIDESEA_WMSServer/Database/Scripts/20260416_Dt_SplitTemp.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
-- æ‹†ç›˜ä¸´æ—¶è¡¨ï¼šç”¨äºŽæš‚存拆盘任务电芯列表,供批量确认时使用
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Dt_SplitTemp]') AND type in (N'U'))
BEGIN
    CREATE TABLE [dbo].[Dt_SplitTemp](
        [Id] [int] IDENTITY(1,1) NOT NULL,
        [PalletCode] [nvarchar](50) NOT NULL,
        [SfcList] [nvarchar](max) NOT NULL,
        [CreateTime] [datetime] NOT NULL DEFAULT GETDATE(),
        CONSTRAINT [PK_Dt_SplitTemp] PRIMARY KEY CLUSTERED ([Id] ASC)
    );
    -- å”¯ä¸€ç´¢å¼•防止同一托盘重复写入
    CREATE UNIQUE NONCLUSTERED INDEX [IX_Dt_SplitTemp_PalletCode] ON [dbo].[Dt_SplitTemp]([PalletCode] ASC);
END
GO
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/GroupPalletConfirmRequestDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
namespace WIDESEA_DTO.Stock
{
    /// <summary>
    /// æ‰¹é‡ç»„盘确认请求DTO
    /// </summary>
    public class GroupPalletConfirmRequestDto
    {
        /// <summary>
        /// ç›®æ ‡æ‰˜ç›˜å·
        /// </summary>
        public string PalletCode { get; set; }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/SplitPalletConfirmRequestDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
namespace WIDESEA_DTO.Stock
{
    /// <summary>
    /// æ‰¹é‡æ‹†ç›˜ç¡®è®¤è¯·æ±‚DTO
    /// </summary>
    public class SplitPalletConfirmRequestDto
    {
        /// <summary>
        /// æºæ‰˜ç›˜å·
        /// </summary>
        public string PalletCode { get; set; }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_IStockService/IStockService.cs
@@ -55,5 +55,19 @@
        /// <param name="stock">库存信息数据传输对象</param>
        /// <returns>操作结果</returns>
        Task<WebResponseContent> UpdateStockInfoAsync(StockInfoDTO stock);
        /// <summary>
        /// æ‰¹é‡æ‹†ç›˜ç¡®è®¤ - ä¸€æ¬¡æ€§è°ƒç”¨MES解绑整个托盘
        /// </summary>
        /// <param name="palletCode">源托盘号</param>
        /// <returns>操作结果</returns>
        Task<WebResponseContent> SplitPalletConfirmAsync(string palletCode);
        /// <summary>
        /// æ‰¹é‡ç»„盘确认 - ä¸€æ¬¡æ€§è°ƒç”¨MES绑定整个托盘
        /// </summary>
        /// <param name="palletCode">目标托盘号</param>
        /// <returns>操作结果</returns>
        Task<WebResponseContent> GroupPalletConfirmAsync(string palletCode);
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_Model/Models/Stock/Dt_SplitTemp.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
using SqlSugar;
using WIDESEA_Core.DB.Models;
namespace WIDESEA_Model.Models
{
    /// <summary>
    /// æ‹†ç›˜ä¸´æ—¶è¡¨ - ç”¨äºŽæš‚存拆盘任务电芯列表,供批量确认时使用
    /// </summary>
    [SugarTable(nameof(Dt_SplitTemp), "拆盘临时表")]
    public class Dt_SplitTemp
    {
        /// <summary>
        /// ä¸»é”®
        /// </summary>
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true, ColumnDescription = "主键")]
        public int Id { get; set; }
        /// <summary>
        /// æ‰˜ç›˜å·
        /// </summary>
        [SugarColumn(IsNullable = false, Length = 50, ColumnDescription = "托盘号")]
        public string PalletCode { get; set; }
        /// <summary>
        /// ç”µèŠ¯æ¡ç åˆ—è¡¨ï¼ˆJSON格式)
        /// </summary>
        [SugarColumn(IsNullable = false, Length = -1, ColumnDescription = "电芯条码列表JSON")]
        public string SfcList { get; set; }
        /// <summary>
        /// åˆ›å»ºæ—¶é—´
        /// </summary>
        [SugarColumn(IsNullable = false, ColumnDescription = "创建时间")]
        public DateTime CreateTime { get; set; } = DateTime.Now;
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs
@@ -1,4 +1,5 @@
using SqlSugar;
using Newtonsoft.Json;
using SqlSugar;
using WIDESEA_Common.Constants;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
@@ -41,6 +42,11 @@
        public IWarehouseService _warehouseService { get; }
        /// <summary>
        /// SqlSugar客户端(用于临时表操作)
        /// </summary>
        public ISqlSugarClient SqlSugarClient { get; }
        /// <summary>
        /// Mes接口服务
        /// </summary>
        public IMesService _mesService { get; }
@@ -58,7 +64,8 @@
            IStockInfoDetail_HtyService stockInfoDetail_HtyService,
            IStockInfo_HtyService stockInfo_HtyService,
            IMesService mesService,
            IWarehouseService warehouseService)
            IWarehouseService warehouseService,
            ISqlSugarClient sqlSugarClient)
        {
            StockInfoDetailService = stockInfoDetailService;
            StockInfoService = stockInfoService;
@@ -66,6 +73,7 @@
            StockInfo_HtyService = stockInfo_HtyService;
            _mesService = mesService;
            _warehouseService = warehouseService;
            SqlSugarClient = sqlSugarClient;
        }
        /// <summary>
@@ -179,11 +187,11 @@
                    result = StockInfoService.Repository.AddData(entity, x => x.Details);
                    if (!result) return content.Error("组盘失败");
                    //var mesResult = _mesService.BindContainer(bindRequest);
                    //if (mesResult == null || mesResult.Data == null || !mesResult.Data.IsSuccess)
                    //{
                    //    return content.Error($"组盘成功,但MES绑定失败: {mesResult?.Data?.Msg ?? mesResult?.ErrorMessage ?? "未知错误"}");
                    //}
                    var mesResult = _mesService.BindContainer(bindRequest);
                    if (mesResult == null || mesResult.Data == null || !mesResult.Data.IsSuccess)
                    {
                        return content.Error($"组盘成功,但MES绑定失败: {mesResult?.Data?.Msg ?? mesResult?.ErrorMessage ?? "未知错误"}");
                    }
                    return content.OK("组盘成功");
                });
            }
@@ -303,6 +311,30 @@
            {
                if (stock == null || string.IsNullOrWhiteSpace(stock.SourcePalletNo))
                    return content.Error("源托盘号不能为空");
                // å¹‚等写入:检查临时表是否已有该托盘记录,无则写入
                var existingTemp = SqlSugarClient.Queryable<Dt_SplitTemp>()
                    .Where(t => t.PalletCode == stock.SourcePalletNo)
                    .First();
                if (existingTemp == null)
                {
                    // æŸ¥è¯¢è¯¥æ‰˜ç›˜å½“前所有电芯,存入临时表
                    var sourceStockForTemp = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.SourcePalletNo);
                    if (sourceStockForTemp != null)
                    {
                        var allDetails = StockInfoDetailService.Repository.QueryData(d => d.StockId == sourceStockForTemp.Id);
                        if (allDetails != null && allDetails.Any())
                        {
                            var sfcListJson = JsonConvert.SerializeObject(allDetails.Select(d => d.SerialNumber).ToList());
                            await SqlSugarClient.Insertable(new Dt_SplitTemp
                            {
                                PalletCode = stock.SourcePalletNo,
                                SfcList = sfcListJson,
                                CreateTime = DateTime.Now
                            }).ExecuteCommandAsync();
                        }
                    }
                }
                return await ExecuteWithinTransactionAsync(async () =>
                {
@@ -426,5 +458,105 @@
                OutboundDate = s.OutboundDate
            }).ToList();
        }
        /// <summary>
        /// æ‰¹é‡æ‹†ç›˜ç¡®è®¤ - ä¸€æ¬¡æ€§è°ƒç”¨MES解绑整个托盘
        /// </summary>
        /// <param name="palletCode">源托盘号</param>
        /// <returns>操作结果</returns>
        public async Task<WebResponseContent> SplitPalletConfirmAsync(string palletCode)
        {
            WebResponseContent content = new WebResponseContent();
            try
            {
                if (string.IsNullOrWhiteSpace(palletCode))
                    return content.Error("托盘号不能为空");
                // 1. ä»Žä¸´æ—¶è¡¨è¯»å–电芯列表
                var tempRecord = SqlSugarClient.Queryable<Dt_SplitTemp>()
                    .Where(t => t.PalletCode == palletCode)
                    .First();
                if (tempRecord == null)
                    return content.Error("未找到拆盘临时记录,请先执行拆盘操作");
                var sfcList = JsonConvert.DeserializeObject<List<string>>(tempRecord.SfcList);
                if (sfcList == null || !sfcList.Any())
                    return content.Error("临时表中电芯列表为空");
                // 2. è°ƒç”¨MES解绑
                var unbindRequest = new UnBindContainerRequest
                {
                    EquipmentCode = StockConstants.MES_EQUIPMENT_CODE,
                    ResourceCode = StockConstants.MES_RESOURCE_CODE,
                    LocalTime = DateTime.Now,
                    ContainCode = palletCode,
                    SfcList = sfcList
                };
                var unbindResult = _mesService.UnBindContainer(unbindRequest);
                if (unbindResult == null || unbindResult.Data == null || !unbindResult.Data.IsSuccess)
                {
                    return content.Error($"MES解绑失败: {unbindResult?.Data?.Msg ?? unbindResult?.ErrorMessage ?? "未知错误"}");
                }
                // 3. åˆ é™¤ä¸´æ—¶è¡¨è®°å½•
                await SqlSugarClient.Deleteable<Dt_SplitTemp>().Where(t => t.PalletCode == palletCode).ExecuteCommandAsync();
                return content.OK("批量拆盘确认成功");
            }
            catch (Exception ex)
            {
                return content.Error($"批量拆盘确认失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æ‰¹é‡ç»„盘确认 - ä¸€æ¬¡æ€§è°ƒç”¨MES绑定整个托盘
        /// </summary>
        /// <param name="palletCode">目标托盘号</param>
        /// <returns>操作结果</returns>
        public async Task<WebResponseContent> GroupPalletConfirmAsync(string palletCode)
        {
            WebResponseContent content = new WebResponseContent();
            try
            {
                if (string.IsNullOrWhiteSpace(palletCode))
                    return content.Error("托盘号不能为空");
                // 1. æŸ¥è¯¢è¯¥æ‰˜ç›˜ä¸‹çš„æ‰€æœ‰ç”µèŠ¯æ˜Žç»†
                var stockInfo = StockInfoService.Repository.QueryFirst(s => s.PalletCode == palletCode);
                if (stockInfo == null)
                    return content.Error("托盘不存在");
                var details = StockInfoDetailService.Repository.QueryData(d => d.StockId == stockInfo.Id);
                if (details == null || !details.Any())
                    return content.Error("托盘下无电芯数据");
                // 2. è°ƒç”¨MES绑定
                var bindRequest = new BindContainerRequest
                {
                    ContainerCode = palletCode,
                    EquipmentCode = StockConstants.MES_EQUIPMENT_CODE,
                    ResourceCode = StockConstants.MES_RESOURCE_CODE,
                    LocalTime = DateTime.Now,
                    OperationType = StockConstants.MES_BIND_OPERATION_TYPE,
                    ContainerSfcList = details.Select(d => new ContainerSfcItem
                    {
                        Sfc = d.SerialNumber,
                        Location = d.InboundOrderRowNo.ToString()
                    }).ToList()
                };
                var bindResult = _mesService.BindContainer(bindRequest);
                if (bindResult == null || bindResult.Data == null || !bindResult.Data.IsSuccess)
                {
                    return content.Error($"MES绑定失败: {bindResult?.Data?.Msg ?? bindResult?.ErrorMessage ?? "未知错误"}");
                }
                return content.OK("批量组盘确认成功");
            }
            catch (Exception ex)
            {
                return content.Error($"批量组盘确认失败: {ex.Message}");
            }
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockController.cs
@@ -63,5 +63,27 @@
        {
            return await Service.UpdateStockInfoAsync(stock);
        }
        /// <summary>
        /// æ‰¹é‡æ‹†ç›˜ç¡®è®¤ - WCS拆盘任务全部取完时调用
        /// </summary>
        /// <param name="dto">拆盘确认请求</param>
        /// <returns>操作结果</returns>
        [HttpPost("SplitPalletConfirm"), AllowAnonymous]
        public async Task<WebResponseContent> SplitPalletConfirm([FromBody] SplitPalletConfirmRequestDto dto)
        {
            return await Service.SplitPalletConfirmAsync(dto.PalletCode);
        }
        /// <summary>
        /// æ‰¹é‡ç»„盘确认 - WCS组盘任务全部放完时调用
        /// </summary>
        /// <param name="dto">组盘确认请求</param>
        /// <returns>操作结果</returns>
        [HttpPost("GroupPalletConfirm"), AllowAnonymous]
        public async Task<WebResponseContent> GroupPalletConfirm([FromBody] GroupPalletConfirmRequestDto dto)
        {
            return await Service.GroupPalletConfirmAsync(dto.PalletCode);
        }
    }
}
Code/docs/superpowers/plans/2026-04-16-BatchMesBinding-Plan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,485 @@
# æ‰¹é‡ MES ç»‘定与解绑接口实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** æ–°å¢žä¸¤ä¸ª WMS æŽ¥å£ï¼ˆSplitPalletConfirm、GroupPalletConfirm),供 WCS åœ¨ä»»åŠ¡é˜¶æ®µå®Œæˆæ—¶ä¸€æ¬¡æ€§ä¸Šä¼ æ‰˜ç›˜çº§åˆ«çš„ MES ç»‘定/解绑数据,减少 MES è°ƒç”¨æ¬¡æ•°ã€‚
**Architecture:**
- `Dt_SplitTemp` ä¸´æ—¶è¡¨ï¼šæ‹†ç›˜å¼€å§‹æ—¶å¹‚等写入托盘电芯列表,Confirm æ—¶è¯»å–并删除
- `SplitPalletAsync` æ”¹é€ ï¼šæ¯æ¬¡è°ƒç”¨æ—¶æ£€æŸ¥ä¸´æ—¶è¡¨ï¼Œæ— è®°å½•则写入,有记录则跳过
- `SplitPalletConfirm`:从临时表读取电芯 â†’ è°ƒç”¨ MES UnBindContainer â†’ åˆ é™¤ä¸´æ—¶è¡¨è®°å½•
- `GroupPalletConfirm`:按托盘号查 Dt_StockInfoDetail â†’ è°ƒç”¨ MES BindContainer
**Tech Stack:** .NET 6/8, C#, SqlSugar ORM, ASP.NET Core WebAPI
---
## æ–‡ä»¶å˜æ›´æ¦‚览
| æ“ä½œ | æ–‡ä»¶ |
|------|------|
| æ–°å¢ž | `WIDESEA_Model/Models/Stock/Dt_SplitTemp.cs` |
| æ–°å¢ž | `WIDESEA_DTO/Stock/SplitPalletConfirmRequestDto.cs` |
| æ–°å¢ž | `WIDESEA_DTO/Stock/GroupPalletConfirmRequestDto.cs` |
| ä¿®æ”¹ | `WIDESEA_IStockService/IStockService.cs` |
| ä¿®æ”¹ | `WIDESEA_StockService/StockService.cs` |
| ä¿®æ”¹ | `WIDESEA_WMSServer/Controllers/Stock/StockController.cs` |
| ä¿®æ”¹ | æ•°æ®åº“:新增 `Dt_SplitTemp` è¡¨ |
---
## Task 1: æ–°å»ºä¸´æ—¶è¡¨å®žä½“ Dt_SplitTemp
**Files:**
- Create: `WMS/WIDESEA_WMSServer/WIDESEA_Model/Models/Stock/Dt_SplitTemp.cs`
- [ ] **Step 1: åˆ›å»º Dt_SplitTemp å®žä½“**
```csharp
using SqlSugar;
using WIDESEA_Core.DB.Models;
namespace WIDESEA_Model.Models
{
    /// <summary>
    /// æ‹†ç›˜ä¸´æ—¶è¡¨ - ç”¨äºŽæš‚存拆盘任务电芯列表,供批量确认时使用
    /// </summary>
    [SugarTable(nameof(Dt_SplitTemp), "拆盘临时表")]
    public class Dt_SplitTemp
    {
        /// <summary>
        /// ä¸»é”®
        /// </summary>
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true, ColumnDescription = "主键")]
        public int Id { get; set; }
        /// <summary>
        /// æ‰˜ç›˜å·
        /// </summary>
        [SugarColumn(IsNullable = false, Length = 50, ColumnDescription = "托盘号")]
        public string PalletCode { get; set; }
        /// <summary>
        /// ç”µèŠ¯æ¡ç åˆ—è¡¨ï¼ˆJSON格式)
        /// </summary>
        [SugarColumn(IsNullable = false, Length = -1, ColumnDescription = "电芯条码列表JSON")]
        public string SfcList { get; set; }
        /// <summary>
        /// åˆ›å»ºæ—¶é—´
        /// </summary>
        [SugarColumn(IsNullable = false, ColumnDescription = "创建时间")]
        public DateTime CreateTime { get; set; } = DateTime.Now;
    }
}
```
- [ ] **Step 2: Commit**
```bash
git add WMS/WIDESEA_WMSServer/WIDESEA_Model/Models/Stock/Dt_SplitTemp.cs
git commit -m "feat(Stock): æ–°å¢žDt_SplitTemp拆盘临时表实体
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
## Task 2: æ–°å»ºè¯·æ±‚ DTO
**Files:**
- Create: `WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/SplitPalletConfirmRequestDto.cs`
- Create: `WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/GroupPalletConfirmRequestDto.cs`
- [ ] **Step 1: åˆ›å»º SplitPalletConfirmRequestDto**
```csharp
namespace WIDESEA_DTO.Stock
{
    /// <summary>
    /// æ‰¹é‡æ‹†ç›˜ç¡®è®¤è¯·æ±‚DTO
    /// </summary>
    public class SplitPalletConfirmRequestDto
    {
        /// <summary>
        /// æºæ‰˜ç›˜å·
        /// </summary>
        public string PalletCode { get; set; }
    }
}
```
- [ ] **Step 2: åˆ›å»º GroupPalletConfirmRequestDto**
```csharp
namespace WIDESEA_DTO.Stock
{
    /// <summary>
    /// æ‰¹é‡ç»„盘确认请求DTO
    /// </summary>
    public class GroupPalletConfirmRequestDto
    {
        /// <summary>
        /// ç›®æ ‡æ‰˜ç›˜å·
        /// </summary>
        public string PalletCode { get; set; }
    }
}
```
- [ ] **Step 3: Commit**
```bash
git add WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/SplitPalletConfirmRequestDto.cs WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/GroupPalletConfirmRequestDto.cs
git commit -m "feat(DTO): æ–°å¢žæ‰¹é‡ç»„盘拆盘确认请求DTO
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
## Task 3: ä¿®æ”¹ IStockService æŽ¥å£
**Files:**
- Modify: `WMS/WIDESEA_WMSServer/WIDESEA_IStockService/IStockService.cs`(在接口末尾添加两个新方法)
- [ ] **Step 1: åœ¨ IStockService æŽ¥å£æ·»åŠ ä¸¤ä¸ªæ–°æ–¹æ³•å£°æ˜Ž**
在 `UpdateStockInfoAsync` æ–¹æ³•声明之后、接口结束 `}` ä¹‹å‰æ·»åŠ ï¼š
```csharp
/// <summary>
/// æ‰¹é‡æ‹†ç›˜ç¡®è®¤ - ä¸€æ¬¡æ€§è°ƒç”¨MES解绑整个托盘
/// </summary>
/// <param name="palletCode">源托盘号</param>
/// <returns>操作结果</returns>
Task<WebResponseContent> SplitPalletConfirmAsync(string palletCode);
/// <summary>
/// æ‰¹é‡ç»„盘确认 - ä¸€æ¬¡æ€§è°ƒç”¨MES绑定整个托盘
/// </summary>
/// <param name="palletCode">目标托盘号</param>
/// <returns>操作结果</returns>
Task<WebResponseContent> GroupPalletConfirmAsync(string palletCode);
```
- [ ] **Step 2: Commit**
```bash
git add WMS/WIDESEA_WMSServer/WIDESEA_IStockService/IStockService.cs
git commit -m "feat(IStockService): æ–°å¢žSplitPalletConfirmAsync和GroupPalletConfirmAsync接口
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
## Task 4: ä¿®æ”¹ StockService å®žçް - SplitPalletConfirmAsync å’Œ GroupPalletConfirmAsync
**Files:**
- Modify: `WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockService.cs`
- [ ] **Step 1: æ·»åŠ  ISqlSugarClient æ³¨å…¥å’Œ Dt_SplitTemp å®žä½“**
在 `StockService` ç±»ä¸­æ·»åŠ ï¼š
```csharp
using SqlSugar;
using WIDESEA_Model.Models;
using Newtonsoft.Json;
```
在类中添加属性:
```csharp
/// <summary>
/// SqlSugar客户端(用于临时表操作)
/// </summary>
public ISqlSugarClient SqlSugarClient { get; }
```
构造函数中注入:
```csharp
public StockService(
    ...,
    ISqlSugarClient sqlSugarClient)  // æ·»åŠ åˆ°å‚æ•°æœ«å°¾
{
    ...
    SqlSugarClient = sqlSugarClient;
}
```
- [ ] **Step 2: å®žçް SplitPalletConfirmAsync æ–¹æ³•**
在类末尾(`UpdateStockInfoAsync` æ–¹æ³•之后、`CreateDetailHistory` ä¹‹å‰ï¼‰æ·»åŠ ï¼š
```csharp
/// <summary>
/// æ‰¹é‡æ‹†ç›˜ç¡®è®¤ - ä¸€æ¬¡æ€§è°ƒç”¨MES解绑整个托盘
/// </summary>
/// <param name="palletCode">源托盘号</param>
/// <returns>操作结果</returns>
public async Task<WebResponseContent> SplitPalletConfirmAsync(string palletCode)
{
    WebResponseContent content = new WebResponseContent();
    try
    {
        if (string.IsNullOrWhiteSpace(palletCode))
            return content.Error("托盘号不能为空");
        // 1. ä»Žä¸´æ—¶è¡¨è¯»å–电芯列表
        var tempRecord = SqlSugarClient.Queryable<Dt_SplitTemp>()
            .Where(t => t.PalletCode == palletCode)
            .First();
        if (tempRecord == null)
            return content.Error("未找到拆盘临时记录,请先执行拆盘操作");
        var sfcList = JsonConvert.DeserializeObject<List<string>>(tempRecord.SfcList);
        if (sfcList == null || !sfcList.Any())
            return content.Error("临时表中电芯列表为空");
        // 2. è°ƒç”¨MES解绑
        var unbindRequest = new UnBindContainerRequest
        {
            EquipmentCode = StockConstants.MES_EQUIPMENT_CODE,
            ResourceCode = StockConstants.MES_RESOURCE_CODE,
            LocalTime = DateTime.Now,
            ContainCode = palletCode,
            SfcList = sfcList
        };
        var unbindResult = _mesService.UnBindContainer(unbindRequest);
        if (unbindResult == null || unbindResult.Data == null || !unbindResult.Data.IsSuccess)
        {
            return content.Error($"MES解绑失败: {unbindResult?.Data?.Msg ?? unbindResult?.ErrorMessage ?? "未知错误"}");
        }
        // 3. åˆ é™¤ä¸´æ—¶è¡¨è®°å½•
        SqlSugarClient.Deleteable<Dt_SplitTemp>().Where(t => t.PalletCode == palletCode).ExecuteCommand();
        return content.OK("批量拆盘确认成功");
    }
    catch (Exception ex)
    {
        return content.Error($"批量拆盘确认失败: {ex.Message}");
    }
}
/// <summary>
/// æ‰¹é‡ç»„盘确认 - ä¸€æ¬¡æ€§è°ƒç”¨MES绑定整个托盘
/// </summary>
/// <param name="palletCode">目标托盘号</param>
/// <returns>操作结果</returns>
public async Task<WebResponseContent> GroupPalletConfirmAsync(string palletCode)
{
    WebResponseContent content = new WebResponseContent();
    try
    {
        if (string.IsNullOrWhiteSpace(palletCode))
            return content.Error("托盘号不能为空");
        // 1. æŸ¥è¯¢è¯¥æ‰˜ç›˜ä¸‹çš„æ‰€æœ‰ç”µèŠ¯æ˜Žç»†
        var stockInfo = StockInfoService.Repository.QueryFirst(s => s.PalletCode == palletCode);
        if (stockInfo == null)
            return content.Error("托盘不存在");
        var details = StockInfoDetailService.Repository.QueryData(d => d.StockId == stockInfo.Id);
        if (!details.Any())
            return content.Error("托盘下无电芯数据");
        // 2. è°ƒç”¨MES绑定
        var bindRequest = new BindContainerRequest
        {
            ContainerCode = palletCode,
            EquipmentCode = StockConstants.MES_EQUIPMENT_CODE,
            ResourceCode = StockConstants.MES_RESOURCE_CODE,
            LocalTime = DateTime.Now,
            OperationType = StockConstants.MES_BIND_OPERATION_TYPE,
            ContainerSfcList = details.Select(d => new ContainerSfcItem
            {
                Sfc = d.SerialNumber,
                Location = d.InboundOrderRowNo.ToString()
            }).ToList()
        };
        var bindResult = _mesService.BindContainer(bindRequest);
        if (bindResult == null || bindResult.Data == null || !bindResult.Data.IsSuccess)
        {
            return content.Error($"MES绑定失败: {bindResult?.Data?.Msg ?? bindResult?.ErrorMessage ?? "未知错误"}");
        }
        return content.OK("批量组盘确认成功");
    }
    catch (Exception ex)
    {
        return content.Error($"批量组盘确认失败: {ex.Message}");
    }
}
```
- [ ] **Step 3: Commit**
```bash
git add WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockService.cs
git commit -m "feat(StockService): å®žçްSplitPalletConfirmAsync和GroupPalletConfirmAsync
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
## Task 5: ä¿®æ”¹ SplitPalletAsync - æ·»åŠ ä¸´æ—¶è¡¨å¹‚ç­‰å†™å…¥é€»è¾‘
**Files:**
- Modify: `WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockService.cs`
- [ ] **Step 1: åœ¨ SplitPalletAsync æ–¹æ³•开头添加临时表写入逻辑**
在 `SplitPalletAsync` æ–¹æ³•çš„ `try` å—开头(`if (stock == null ...` ä¹‹åŽï¼‰ã€åœ¨äº‹åŠ¡ `ExecuteWithinTransactionAsync` è°ƒç”¨ä¹‹å‰ï¼Œæ·»åŠ ï¼š
```csharp
// å¹‚等写入:检查临时表是否已有该托盘记录,无则写入
var existingTemp = SqlSugarClient.Queryable<Dt_SplitTemp>()
    .Where(t => t.PalletCode == stock.SourcePalletNo)
    .First();
if (existingTemp == null)
{
    // æŸ¥è¯¢è¯¥æ‰˜ç›˜å½“前所有电芯,存入临时表
    var sourceStockForTemp = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.SourcePalletNo);
    if (sourceStockForTemp != null)
    {
        var allDetails = StockInfoDetailService.Repository.QueryData(d => d.StockId == sourceStockForTemp.Id);
        if (allDetails.Any())
        {
            var sfcListJson = JsonConvert.SerializeObject(allDetails.Select(d => d.SerialNumber).ToList());
            SqlSugarClient.Insertable(new Dt_SplitTemp
            {
                PalletCode = stock.SourcePalletNo,
                SfcList = sfcListJson,
                CreateTime = DateTime.Now
            }).ExecuteCommand();
        }
    }
}
```
注意:这段代码在 `return await ExecuteWithinTransactionAsync(...)` ä¹‹å‰æ‰§è¡Œï¼Œä¸åœ¨äº‹åŠ¡å†…ã€‚
- [ ] **Step 2: Commit**
```bash
git add WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockService.cs
git commit -m "feat(SplitPalletAsync): æ·»åŠ ä¸´æ—¶è¡¨å¹‚ç­‰å†™å…¥é€»è¾‘
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
## Task 6: ä¿®æ”¹ StockController - æ·»åŠ æ–°è·¯ç”±
**Files:**
- Modify: `WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockController.cs`
- [ ] **Step 1: åœ¨ StockController æ·»åŠ ä¸¤ä¸ªæ–°è·¯ç”±**
在 `UpdateStockInfoAsync` æ–¹æ³•之后、类结束 `}` ä¹‹å‰æ·»åŠ ï¼š
```csharp
/// <summary>
/// æ‰¹é‡æ‹†ç›˜ç¡®è®¤ - WCS拆盘任务全部取完时调用
/// </summary>
/// <param name="dto">拆盘确认请求</param>
/// <returns>操作结果</returns>
[HttpPost("SplitPalletConfirm"), AllowAnonymous]
public async Task<WebResponseContent> SplitPalletConfirm([FromBody] SplitPalletConfirmRequestDto dto)
{
    return await Service.SplitPalletConfirmAsync(dto.PalletCode);
}
/// <summary>
/// æ‰¹é‡ç»„盘确认 - WCS组盘任务全部放完时调用
/// </summary>
/// <param name="dto">组盘确认请求</param>
/// <returns>操作结果</returns>
[HttpPost("GroupPalletConfirm"), AllowAnonymous]
public async Task<WebResponseContent> GroupPalletConfirm([FromBody] GroupPalletConfirmRequestDto dto)
{
    return await Service.GroupPalletConfirmAsync(dto.PalletCode);
}
```
同时在文件顶部添加 using:
```csharp
using WIDESEA_DTO.Stock;
```
- [ ] **Step 2: Commit**
```bash
git add WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockController.cs
git commit -m "feat(StockController): æ–°å¢žSplitPalletConfirm和GroupPalletConfirm接口路由
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
## Task 7: æ•°æ®åº“变更脚本
**Files:**
- Create: `WMS/WIDESEA_WMSServer/Database/Scripts/20260416_Dt_SplitTemp.sql`
- [ ] **Step 1: åˆ›å»ºä¸´æ—¶è¡¨ DDL è„šæœ¬**
```sql
-- æ‹†ç›˜ä¸´æ—¶è¡¨ï¼šç”¨äºŽæš‚存拆盘任务电芯列表,供批量确认时使用
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Dt_SplitTemp]') AND type in (N'U'))
BEGIN
    CREATE TABLE [dbo].[Dt_SplitTemp](
        [Id] [int] IDENTITY(1,1) NOT NULL,
        [PalletCode] [nvarchar](50) NOT NULL,
        [SfcList] [nvarchar](max) NOT NULL,
        [CreateTime] [datetime] NOT NULL DEFAULT GETDATE(),
        CONSTRAINT [PK_Dt_SplitTemp] PRIMARY KEY CLUSTERED ([Id] ASC)
    );
    -- å¯é€‰ï¼šæ·»åŠ å”¯ä¸€ç´¢å¼•é˜²æ­¢åŒä¸€æ‰˜ç›˜é‡å¤å†™å…¥
    CREATE UNIQUE NONCLUSTERED INDEX [IX_Dt_SplitTemp_PalletCode] ON [dbo].[Dt_SplitTemp]([PalletCode] ASC);
END
GO
```
- [ ] **Step 2: Commit**
```bash
git add WMS/WIDESEA_WMSServer/Database/Scripts/20260416_Dt_SplitTemp.sql
git commit -m "feat(db): æ–°å¢žDt_SplitTemp拆盘临时表
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
## Task 8: æž„建验证
- [ ] **Step 1: è¿è¡Œ dotnet build éªŒè¯ç¼–译通过**
```bash
cd D:\Git\ShanMeiXinNengYuan\Code\WMS\WIDESEA_WMSServer
dotnet build WIDESEA_WMSServer.sln
```
Expected: Build succeeded with no errors.
---
## è‡ªæ£€æ¸…单
- [ ] æ‰€æœ‰ public æ–¹æ³•均有 XML æ–‡æ¡£æ³¨é‡Š
- [ ] `Dt_SplitTemp.SfcList` ä½¿ç”¨ `nvarchar(max)` å­˜å‚¨ JSON
- [ ] `SplitPalletConfirmAsync` è¯»å–临时表后删除记录
- [ ] `SplitPalletAsync` ä¸­çš„临时表写入在事务外执行
- [ ] `GroupPalletConfirmAsync` ä»Ž `Dt_StockInfoDetail` æŸ¥ç”µèŠ¯ï¼Œä¸æŸ¥ä¸´æ—¶è¡¨
- [ ] ä¸¤ä¸ªæ–° Controller æ–¹æ³•均标记 `[AllowAnonymous]`
- [ ] æ•°æ®åº“脚本含 IF NOT EXISTS é˜²æ­¢é‡å¤åˆ›å»º
Code/docs/superpowers/specs/2026-04-16-BatchMesBinding-Design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
# æ‰¹é‡ MES ç»‘定与解绑接口设计
## èƒŒæ™¯
当前 `StockSerivce` ä¸­ `GroupPalletAsync`、`ChangePalletAsync`、`SplitPalletAsync` æ¯æ¬¡è°ƒç”¨éƒ½ä¼šè§¦å‘一次 MES æŽ¥å£ã€‚
WCS æœºå™¨äººä»»åŠ¡æŒ‰æ‰¹æ¬¡å–æ”¾ï¼Œä¸€ä¸ªæ‰˜ç›˜å¯èƒ½éœ€è¦å¤šæ¬¡ MES è°ƒç”¨ï¼ˆå¦‚换盘需要先解绑再绑定)。为减少 MES è°ƒç”¨æ¬¡æ•°ï¼Œæ–°å¢žä¸¤ä¸ªæ‰¹é‡ç¡®è®¤æŽ¥å£ä¾› WCS åœ¨ä»»åŠ¡é˜¶æ®µå®Œæˆæ—¶ä¸€æ¬¡æ€§ä¸Šä¼ æ‰˜ç›˜çº§åˆ«çš„ç»‘å®š/解绑数据。
## æ–°å¢žæŽ¥å£
### 1. SplitPalletConfirm â€” æ‰¹é‡æ‹†ç›˜ç¡®è®¤
**触发时机**:WCS æ‹†ç›˜ä»»åŠ¡/换盘任务全部取完
**WCS è°ƒç”¨æ–¹å¼**:`POST /api/Stock/SplitPalletConfirm`
**请求参数**:
```csharp
public class SplitPalletConfirmRequest
{
    /// <summary>
    /// æºæ‰˜ç›˜å·
    /// </summary>
    public string PalletCode { get; set; }
}
```
**处理流程**:
1. WMS æ ¹æ® `PalletCode` ä»Žä¸´æ—¶è¡¨ `Dt_SplitTemp` è¯»å–预存电芯列表
2. è°ƒç”¨ MES `UnBindContainer`(一次性上传整托电芯)
3. åˆ é™¤ä¸´æ—¶è¡¨ `Dt_SplitTemp` ä¸­å¯¹åº”记录
**临时表写入时机**:`SplitPalletAsync` æ¯æ¬¡è¢«è°ƒç”¨æ—¶ï¼Œå…ˆæ£€æŸ¥ `Dt_SplitTemp` ä¸­æ˜¯å¦å­˜åœ¨è¯¥æ‰˜ç›˜è®°å½•;不存在则将当前托盘对应的所有电芯条码写入临时表;已存在则跳过写入。
**临时表结构**(`Dt_SplitTemp`):
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| Id | int | ä¸»é”® |
| PalletCode | string | æ‰˜ç›˜å· |
| SfcList | string | ç”µèŠ¯æ¡ç åˆ—è¡¨ï¼ˆJSON数组) |
| CreateTime | DateTime | åˆ›å»ºæ—¶é—´ |
---
### 2. GroupPalletConfirm â€” æ‰¹é‡ç»„盘确认
**触发时机**:WCS ç»„盘任务/换盘任务全部放完
**WCS è°ƒç”¨æ–¹å¼**:`POST /api/Stock/GroupPalletConfirm`
**请求参数**:
```csharp
public class GroupPalletConfirmRequest
{
    /// <summary>
    /// ç›®æ ‡æ‰˜ç›˜å·
    /// </summary>
    public string PalletCode { get; set; }
}
```
**处理流程**:
1. WMS æ ¹æ® `PalletCode` æŸ¥è¯¢ `Dt_StockInfoDetail` ä¸­è¯¥æ‰˜ç›˜ä¸‹çš„æ‰€æœ‰ç”µèŠ¯æ˜Žç»†
2. è°ƒç”¨ MES `BindContainer`(一次性上传整托电芯绑定)
3. è¿”回结果
**注意**:电芯数据在组盘任务放货过程中已由 WCS é€šè¿‡å…¶ä»–接口写入 `Dt_StockInfoDetail`,WMS ä¸éœ€è¦é¢å¤–存储
---
## æ¢ç›˜ä»»åŠ¡å®Œæ•´æµç¨‹
```
换盘任务:
  å…¨éƒ¨å–完 â†’ SplitPalletConfirm(源托盘) â†’ MES UnBindContainer
  å…¨éƒ¨æ”¾å®Œ â†’ GroupPalletConfirm(目标托盘) â†’ MES BindContainer
```
## çŽ°æœ‰æŽ¥å£å¤„ç†
- `GroupPalletAsync`、`ChangePalletAsync`、`SplitPalletAsync` ä¿ç•™
- æ–°æŽ¥å£ä¸ŽçŽ°æœ‰æŽ¥å£å¹¶å­˜ï¼ŒWCS æ ¹æ®ä»»åŠ¡åœºæ™¯é€‰æ‹©è°ƒç”¨
- çŽ°æœ‰æŽ¥å£ç»§ç»­æ‰¿æ‹…å•æ¬¡/非批量场景的 MES è°ƒç”¨
## WCS ä¾§æ”¹é€ è¦ç‚¹
- æ‹†ç›˜/换盘任务开始时,WCS è°ƒç”¨çŽ°æœ‰ `SplitPalletAsync` æŽ¥å£ï¼›WMS åœ¨ `SplitPalletAsync` å†…部先检查 `Dt_SplitTemp` æ˜¯å¦å·²æœ‰è¯¥æ‰˜ç›˜è®°å½•,无则写入,有则跳过(幂等写入)
- ç»„盘任务全部放完时调用 `GroupPalletConfirm`
- æ¢ç›˜ä»»åŠ¡å…¨éƒ¨å–å®Œæ—¶è°ƒç”¨ `SplitPalletConfirm`,全部放完时调用 `GroupPalletConfirm`
## æ–‡ä»¶å˜æ›´
| æ“ä½œ | æ–‡ä»¶ |
|------|------|
| æ–°å¢ž | `WIDESEA_DTO/Stock/SplitPalletConfirmRequestDto.cs` |
| æ–°å¢ž | `WIDESEA_DTO/Stock/GroupPalletConfirmRequestDto.cs` |
| æ–°å¢ž | `WIDESEA_Model/Models/Dt_SplitTemp.cs` |
| ä¿®æ”¹ | `WIDESEA_IStockService/IStockService.cs`(新增接口定义) |
| ä¿®æ”¹ | `WIDESEA_StockService/StockService.cs`(实现批量确认逻辑) |
| ä¿®æ”¹ | `WIDESEA_WMSServer/Controllers/Stock/StockInfoDetailController.cs`(新增 API è·¯ç”±ï¼‰ |
| ä¿®æ”¹ | æ•°æ®åº“:新增 `Dt_SplitTemp` è¡¨ |
## é£Žé™©ä¸Žçº¦æŸ
- ä¸´æ—¶è¡¨ `Dt_SplitTemp` éœ€è¦æœ‰æ¸…理机制,防止异常情况下数据残留
- MES æŽ¥å£è°ƒç”¨å¤±è´¥æ—¶ï¼Œä¸´æ—¶è¡¨æ•°æ®ä¸å›žæ»šï¼Œä¸‹æ¬¡é‡è¯•时可能重复解绑,需 MES ä¾§å¹‚等支持