编辑 | blame | 历史 | 原始文档

换盘任务批次指令与双流向实现计划

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: 升级换盘任务取货/放货指令格式为批次模式,支持 PickTotalNum/PutTotalNum,并根据 RobotSourceAddressLineCode 区分两种流向

Architecture:
- RobotSocketState 新增批次状态字段(CurrentBatchIndex, ChangePalletPhase)
- RobotTaskProcessor 新增批次指令构建方法(BuildBatchRange, SendPickWithBatchAsync, SendPutWithBatchAsync)
- RobotWorkflowOrchestrator 重写 HandlePutFinishedStateAsync 和 HandlePickFinishedStateAsync 实现两种流向的状态机
- RobotPrefixCommandHandler 和 RobotSimpleCommandHandler 根据阶段区分处理逻辑
- 假电芯服务层新增 MarkAsAvailable 方法用于释放点位

Tech Stack: C# / .NET 8, SqlSugar ORM, Redis缓存


File Structure

WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/
  = RobotSocketState.cs                # +CurrentBatchIndex, +ChangePalletPhase
  = RobotTaskProcessor.cs              # +BuildBatchRange, +SendPickWithBatchAsync, +SendPutWithBatchAsync
  = Workflow/RobotWorkflowOrchestrator.cs  # 重写 HandlePutFinishedStateAsync + HandlePickFinishedStateAsync
  = Workflow/RobotPrefixCommandHandler.cs  # putfinished/pickfinished 阶段判断
  = Workflow/RobotSimpleCommandHandler.cs  # allpickfinished/allputfinished 阶段守卫

WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoService/
  = IFakeBatteryPositionService.cs     # +MarkAsAvailable

WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/
  = FakeBatteryPositionService.cs      # +MarkAsAvailable

WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoRepository/
  = IFakeBatteryPositionRepository.cs  # +MarkAsAvailable

WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoRepository/
  = FakeBatteryPositionRepository.cs   # +MarkAsAvailable

Task 1: RobotSocketState 新增批次状态字段

Files:
- Modify: WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotSocketState.cs

  • [ ] Step 1: 添加 CurrentBatchIndex 和 ChangePalletPhase 属性

IsInFakeBatteryMode 属性后添加:

        /// <summary>
        /// 是否处于假电芯补充模式
        /// </summary>
        public bool IsInFakeBatteryMode { get; set; }

        /// <summary>
        /// 当前批次起始编号(用于递增计算取货/放货编号)
        /// </summary>
        /// <remarks>
        /// 在批次模式下,每批取货/放货的起始编号从1开始递增。
        /// 用于计算 {start}-{end} 格式中的 start 值。
        /// </remarks>
        public int CurrentBatchIndex { get; set; } = 1;

        /// <summary>
        /// 换盘任务当前阶段
        /// </summary>
        /// <remarks>
        /// 阶段定义:
        /// 0: 未开始
        /// 1: 取正常电芯(流向B) / 取假电芯(流向A)
        /// 2: 放正常电芯(流向B) / 放假电芯(流向A)
        /// 3: 取假电芯(流向B Phase2)
        /// 4: 放假电芯到5号位(流向B Phase2)
        /// </remarks>
        public int ChangePalletPhase { get; set; }
  • [ ] Step 2: 验证编译
cd D:\Git\ShanMeiXinNengYuan\Code\WCS\WIDESEAWCS_Server
dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj

Expected: 0 errors

  • [ ] Step 3: Commit
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotSocketState.cs
git commit -m "feat(Robot): RobotSocketState 新增 CurrentBatchIndex 和 ChangePalletPhase"

Task 2: 假电芯仓储层新增 MarkAsAvailable 方法

Files:
- Modify: WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoRepository/IFakeBatteryPositionRepository.cs
- Modify: WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoRepository/FakeBatteryPositionRepository.cs

  • [ ] Step 1: 在接口中添加 MarkAsAvailable 方法声明(在 MarkAsUsed 方法后)

IFakeBatteryPositionRepository.csMarkAsUsed 方法后添加:

        /// <summary>
        /// 标记指定点位为可用(释放点位)
        /// </summary>
        /// <param name="positions">点位索引列表</param>
        /// <returns>是否成功</returns>
        bool MarkAsAvailable(List<int> positions);
  • [ ] Step 2: 在仓储实现中添加 MarkAsAvailable 方法(在 MarkAsUsed 方法后)

FakeBatteryPositionRepository.csMarkAsUsed 方法后添加:

        /// <inheritdoc/>
        public bool MarkAsAvailable(List<int> positions)
        {
            if (positions == null || positions.Count == 0)
                return true;

            return Db.Updateable<Dt_FakeBatteryPosition>()
                .SetColumns(x => x.IsUsed, false)
                .Where(x => positions.Contains(x.PositionIndex))
                .ExecuteCommand() > 0;
        }
  • [ ] Step 3: 验证编译
dotnet build WIDESEAWCS_Server.sln

Expected: 0 errors

  • [ ] Step 4: Commit
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoRepository/IFakeBatteryPositionRepository.cs
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoRepository/FakeBatteryPositionRepository.cs
git commit -m "feat(Robot): 假电芯仓储层新增 MarkAsAvailable 方法"

Task 3: 假电芯服务层新增 MarkAsAvailable 方法

Files:
- Modify: WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoService/IFakeBatteryPositionService.cs
- Modify: WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/FakeBatteryPositionService.cs

  • [ ] Step 1: 在服务接口中添加 MarkAsAvailable 方法声明(在 MarkAsUsed 后)

IFakeBatteryPositionService.csMarkAsUsed 方法后添加:

        /// <summary>
        /// 标记指定点位为可用(释放点位)
        /// </summary>
        /// <param name="positions">点位索引列表</param>
        /// <returns>是否成功</returns>
        bool MarkAsAvailable(List<int> positions);
  • [ ] Step 2: 在服务实现中添加 MarkAsAvailable 方法
        /// <inheritdoc/>
        public bool MarkAsAvailable(List<int> positions)
        {
            return BaseDal.MarkAsAvailable(positions);
        }
  • [ ] Step 3: 验证编译
dotnet build WIDESEAWCS_Server.sln

Expected: 0 errors

  • [ ] Step 4: Commit
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoService/IFakeBatteryPositionService.cs
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/FakeBatteryPositionService.cs
git commit -m "feat(Robot): 假电芯服务层新增 MarkAsAvailable 方法"

Task 4: RobotTaskProcessor 新增批次指令辅助方法

注意: _fakeBatteryPositionService 字段已在上一轮迭代中添加,无需重复注入。

Files:
- Modify: WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs

  • [ ] Step 1: 添加 BuildBatchRange 辅助方法

GetNextAvailableFakeBatteryPositions 方法后添加:

        /// <summary>
        /// 计算批次编号范围
        /// </summary>
        /// <remarks>
        /// 返回格式:(start, end)
        /// - remaining >= 4: (currentIndex, currentIndex + 3)
        /// - remaining > 1: (currentIndex, currentIndex + remaining - 1)
        /// - remaining == 1: (currentIndex, 0)  -- 单个物品用 0 表示 end
        /// </remarks>
        /// <param name="currentIndex">当前批次起始编号</param>
        /// <param name="remaining">剩余数量</param>
        /// <returns>(start, end) 元组</returns>
        public (int Start, int End) BuildBatchRange(int currentIndex, int remaining)
        {
            if (remaining >= 4)
                return (currentIndex, currentIndex + 3);
            else if (remaining > 1)
                return (currentIndex, currentIndex + remaining - 1);
            else  // remaining == 1
                return (currentIndex, 0);
        }
  • [ ] Step 2: 添加 SendPickWithBatchAsync 方法
        /// <summary>
        /// 下发取货指令(带批次格式和总数)
        /// </summary>
        /// <remarks>
        /// 发送顺序:
        /// 1. PickTotalNum,{N} -- 真实电芯总数
        /// 2. Pickbattery,{位置},{start}-{end} -- 批次取货指令
        ///
        /// 下发成功后更新任务状态为"机器人执行中"。
        /// </remarks>
        /// <param name="task">要下发的任务对象</param>
        /// <param name="state">机器人当前状态</param>
        /// <param name="position">取货位置</param>
        /// <param name="batchStart">批次起始编号</param>
        /// <param name="batchEnd">批次结束编号</param>
        public async Task SendPickWithBatchAsync(Dt_RobotTask task, RobotSocketState state, string position, int batchStart, int batchEnd)
        {
            // 先发送总数指令
            string totalNumCmd = $"PickTotalNum,{task.RobotTaskTotalNum}";
            await _socketClientGateway.SendToClientAsync(state.IPAddress, totalNumCmd);

            // 再发送批次取货指令
            string range = batchEnd == 0 ? $"{batchStart}-0" : $"{batchStart}-{batchEnd}";
            string taskString = $"Pickbattery,{position},{range}";

            bool result = await _socketClientGateway.SendToClientAsync(state.IPAddress, taskString);

            if (result)
            {
                _logger.LogInformation("下发批次取货指令成功,指令: {TaskString},批次: {Range},设备: {DeviceName}",
                    taskString, range, state.RobotCrane?.DeviceName);
                QuartzLogger.Info($"下发批次取货指令成功,指令: {taskString},批次: {range}", state.RobotCrane?.DeviceName);

                task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
                state.CurrentTask = task;

                if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                {
                    await _robotTaskService.UpdateRobotTaskAsync(task);
                }
            }
            else
            {
                _logger.LogError("下发批次取货指令失败,指令: {TaskString},设备: {DeviceName}", taskString, state.RobotCrane?.DeviceName);
                QuartzLogger.Error($"下发批次取货指令失败,指令: {taskString}", state.RobotCrane?.DeviceName);
            }
        }
  • [ ] Step 3: 添加 SendPutWithBatchAsync 方法
        /// <summary>
        /// 下发放货指令(带批次格式和总数)
        /// </summary>
        /// <remarks>
        /// 发送顺序:
        /// 1. PutTotalNum,{N} -- 真实电芯总数
        /// 2. Putbattery,{位置},{start}-{end} -- 批次放货指令
        ///
        /// 下发成功后更新任务状态为"机器人执行中"。
        /// </remarks>
        /// <param name="task">要下发的任务对象</param>
        /// <param name="state">机器人当前状态</param>
        /// <param name="position">放货位置</param>
        /// <param name="batchStart">批次起始编号</param>
        /// <param name="batchEnd">批次结束编号</param>
        public async Task SendPutWithBatchAsync(Dt_RobotTask task, RobotSocketState state, string position, int batchStart, int batchEnd)
        {
            // 先发送总数指令
            string totalNumCmd = $"PutTotalNum,{task.RobotTaskTotalNum}";
            await _socketClientGateway.SendToClientAsync(state.IPAddress, totalNumCmd);

            // 再发送批次放货指令
            string range = batchEnd == 0 ? $"{batchStart}-0" : $"{batchStart}-{batchEnd}";
            string taskString = $"Putbattery,{position},{range}";

            bool result = await _socketClientGateway.SendToClientAsync(state.IPAddress, taskString);

            if (result)
            {
                _logger.LogInformation("下发放货指令成功,指令: {TaskString},批次: {Range},设备: {DeviceName}",
                    taskString, range, state.RobotCrane?.DeviceName);
                QuartzLogger.Info($"下发放货指令成功,指令: {taskString},批次: {range}", state.RobotCrane?.DeviceName);

                task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
                state.CurrentTask = task;

                if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                {
                    await _robotTaskService.UpdateRobotTaskAsync(task);
                }
            }
            else
            {
                _logger.LogError("下发放货指令失败,指令: {TaskString},设备: {DeviceName}", taskString, state.RobotCrane?.DeviceName);
                QuartzLogger.Error($"下发放货指令失败,指令: {taskString}", state.RobotCrane?.DeviceName);
            }
        }
  • [ ] Step 4: 验证编译
dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj

Expected: 0 errors

  • [ ] Step 5: Commit
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs
git commit -m "feat(Robot): RobotTaskProcessor 新增批次指令辅助方法"

Task 5: RobotSimpleCommandHandler 添加阶段守卫

Files:
- Modify: WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs

  • [ ] Step 1: 修改 allpickfinished 分支

找到 case "allpickfinished": 分支,替换为:

                // 全部取货完成
                case "allpickfinished":
                    {
                        state.CurrentAction = "AllPickFinished";

                        var currentTask = state.CurrentTask;
                        if (currentTask == null)
                            return false;

                        var robotTaskType = (RobotTaskTypeEnum)currentTask.RobotTaskType;

                        // 换盘任务:仅当所有阶段完成时才处理入库
                        if (robotTaskType == RobotTaskTypeEnum.ChangePallet)
                        {
                            if (state.ChangePalletPhase == 0)
                            {
                                // 所有阶段完成,处理入库
                                if (await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true))
                                {
                                    _taskProcessor.DeleteTask(currentTask.RobotTaskId);
                                    await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Swap,diskFinished");
                                    QuartzLogger.Info($"发送消息:【Swap,diskFinished】", state.RobotCrane.DeviceName);

                                    // 重置批次状态
                                    state.ChangePalletPhase = 0;
                                    state.CurrentBatchIndex = 1;
                                    state.IsInFakeBatteryMode = false;
                                    return true;
                                }
                            }
                            // 中间阶段不处理,仅更新状态
                            return true;
                        }

                        // 拆盘任务:直接处理入库
                        if (robotTaskType == RobotTaskTypeEnum.SplitPallet)
                        {
                            if (await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true))
                            {
                                _taskProcessor.DeleteTask(currentTask.RobotTaskId);
                                await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Swap,diskFinished");
                                QuartzLogger.Info($"发送消息:【Swap,diskFinished】", state.RobotCrane.DeviceName);
                                return true;
                            }
                        }
                        return false;
                    }
  • [ ] Step 2: 修改 allputfinished 分支

找到 case "allputfinished": 分支,替换为:

                // 全部放货完成
                case "allputfinished":
                    {
                        state.CurrentAction = "AllPutFinished";

                        var currentTask = state.CurrentTask;
                        if (currentTask == null)
                            return false;

                        var robotTaskType = (RobotTaskTypeEnum)currentTask.RobotTaskType;

                        // 换盘任务:仅当所有阶段完成时才处理入库
                        if (robotTaskType == RobotTaskTypeEnum.ChangePallet)
                        {
                            if (state.ChangePalletPhase == 0)
                            {
                                // 所有阶段完成,处理入库
                                if (await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false))
                                {
                                    _taskProcessor.DeleteTask(currentTask.RobotTaskId);
                                    state.CurrentTask = null;
                                    state.RobotTaskTotalNum = 0;
                                    state.CellBarcode = new List<string>();

                                    await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                                    QuartzLogger.Info($"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);

                                    // 重置批次状态
                                    state.ChangePalletPhase = 0;
                                    state.CurrentBatchIndex = 1;
                                    state.IsInFakeBatteryMode = false;
                                    return true;
                                }
                            }
                            // 中间阶段不处理,仅更新状态
                            return true;
                        }

                        // 组盘任务:直接处理入库
                        if (robotTaskType == RobotTaskTypeEnum.GroupPallet)
                        {
                            if (await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false))
                            {
                                _taskProcessor.DeleteTask(currentTask.RobotTaskId);
                                state.CurrentTask = null;
                                state.RobotTaskTotalNum = 0;
                                state.CellBarcode = new List<string>();

                                await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                                QuartzLogger.Info($"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                                return true;
                            }
                        }
                        return false;
                    }
  • [ ] Step 3: 验证编译
dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj

Expected: 0 errors

  • [ ] Step 4: Commit
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs
git commit -m "feat(Robot): RobotSimpleCommandHandler 换盘任务添加 ChangePalletPhase 阶段守卫"

Task 6: RobotPrefixCommandHandler 修改 putfinished/pickfinished 处理逻辑

Files:
- Modify: WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotPrefixCommandHandler.cs

  • [ ] Step 1: 添加 IFakeBatteryPositionService 依赖

在类顶部添加字段和构造函数参数:

        /// <summary>
        /// 假电芯位置服务
        /// </summary>
        private readonly IFakeBatteryPositionService _fakeBatteryPositionService;

        public RobotPrefixCommandHandler(
            IRobotTaskService robotTaskService,
            RobotTaskProcessor taskProcessor,
            RobotStateManager stateManager,
            ISocketClientGateway socketClientGateway,
            IFakeBatteryPositionService fakeBatteryPositionService)
        {
            _robotTaskService = robotTaskService;
            _taskProcessor = taskProcessor;
            _stateManager = stateManager;
            _socketClientGateway = socketClientGateway;
            _fakeBatteryPositionService = fakeBatteryPositionService;
        }
  • [ ] Step 2: 修改 HandlePutFinishedAsync 方法中的放货处理逻辑

找到 if (putSuccess) 块(现有代码约在第258行附近),替换其中的放货成功处理逻辑为:

            // 如果放货成功
            if (putSuccess)
            {
                state.CurrentAction = "PutFinished";

                // 判断是否为换盘任务且处于批次模式
                var isChangePallet = state.CurrentTask?.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode();
                var isFlowA = state.CurrentTask?.RobotSourceAddressLineCode is "11001" or "11010";

                if (isChangePallet)
                {
                    if (state.ChangePalletPhase == 2)
                    {
                        if (isFlowA)
                        {
                            // 流向A Phase 2:放假电芯到目标托盘,不调用 API,不递增计数
                            // 不做任何额外处理,仅更新状态
                        }
                        else
                        {
                            // 流向B Phase 2:放正常电芯,递增计数
                            state.RobotTaskTotalNum += positions.Length;
                            if (task != null)
                                task.RobotTaskTotalNum -= positions.Length;
                        }
                    }
                    else if (state.ChangePalletPhase == 4)
                    {
                        // 流向B Phase 4:放假电芯到5号位,不调用 API,不递增计数,释放点位
                        _fakeBatteryPositionService.MarkAsAvailable(positions.ToList());
                    }
                    else
                    {
                        // 非批次模式或非换盘任务:递增计数
                        state.RobotTaskTotalNum += positions.Length;
                        if (task != null)
                            task.RobotTaskTotalNum -= positions.Length;
                    }
                }
                else
                {
                    // 非换盘任务:原有逻辑
                    state.RobotTaskTotalNum += positions.Length;
                    if (task != null)
                        task.RobotTaskTotalNum -= positions.Length;
                }
            }
  • [ ] Step 3: 修改 HandlePickFinishedAsync 方法

找到 if (state.IsSplitPallet) 块(现有代码约在第173-198行),替换为:

            // 如果是拆盘任务
            if (state.IsSplitPallet)
            {
                var stockDTO = RobotTaskProcessor.BuildStockDTO(state, positions);
                state.LastPickPositions = positions;
                var result = _taskProcessor.PostSplitPalletAsync(stockDTO);

                if (result.Data.Status && result.IsSuccess)
                {
                    state.CurrentAction = "PickFinished";
                }
            }
            // 换盘任务取假电芯时(Phase 3)不调用拆盘 API
            else if (state.CurrentTask?.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode()
                     && state.ChangePalletPhase == 3)
            {
                state.CurrentAction = "PickFinished";
                state.LastPickPositions = positions;
            }
            else
            {
                state.CurrentAction = "PickFinished";
                state.LastPickPositions = positions;
            }
  • [ ] Step 4: 验证编译
dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj

Expected: 0 errors

  • [ ] Step 5: Commit
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotPrefixCommandHandler.cs
git commit -m "feat(Robot): RobotPrefixCommandHandler 换盘任务根据阶段区分处理逻辑"

Task 7: RobotWorkflowOrchestrator 重写 HandlePutFinishedStateAsync 的 ChangePallet 分支

Files:
- Modify: WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs

  • [ ] Step 1: 替换 ChangePallet 分支逻辑

找到 else if (task.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode()) 分支,将其完整替换为:

            else if (task.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode())
            {
                const int targetTotal = 48;
                int targetNormalCount = task.RobotTaskTotalNum;
                int currentCompletedCount = stateForUpdate.RobotTaskTotalNum;

                // 判断流向(null-safe)
                bool isFlowA = task.RobotSourceAddressLineCode is "11001" or "11010";

                // 目标数量为48:直接走原有逻辑,不进入批次模式
                if (targetNormalCount == targetTotal)
                {
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate);
                    return;
                }

                // 初始化批次模式
                if (stateForUpdate.ChangePalletPhase == 0)
                {
                    stateForUpdate.ChangePalletPhase = 1;
                    stateForUpdate.CurrentBatchIndex = 1;
                    _logger.LogInformation("HandlePutFinishedStateAsync:换盘任务进入批次模式,任务号: {TaskNum},流向: {Flow}",
                        task.RobotTaskNum, isFlowA ? "A" : "B");
                }

                // ==================== 流向A:补假电芯到目标托盘 ====================
                if (isFlowA)
                {
                    // Phase 1: 取假电芯(从5号位,使用 PositionIndex)
                    if (stateForUpdate.ChangePalletPhase == 1)
                    {
                        int remaining = targetTotal - currentCompletedCount;
                        if (remaining <= 0)
                        {
                            stateForUpdate.ChangePalletPhase = 0;
                            stateForUpdate.CurrentBatchIndex = 1;
                            stateForUpdate.IsInFakeBatteryMode = false;
                            _logger.LogInformation("HandlePutFinishedStateAsync:流向A完成,任务号: {TaskNum}", task.RobotTaskNum);
                            return;
                        }

                        int pickCount = Math.Min(4, remaining);
                        var positions = _taskProcessor.GetNextAvailableFakeBatteryPositions(pickCount);
                        if (positions.Count == 0)
                        {
                            _logger.LogError("HandlePutFinishedStateAsync:无可用假电芯点位,任务号: {TaskNum}", task.RobotTaskNum);
                            return;
                        }

                        await _taskProcessor.SendSocketRobotFakeBatteryPickAsync(task, stateForUpdate, positions);
                        stateForUpdate.ChangePalletPhase = 2;
                    }
                    // Phase 2: 放假电芯到目标托盘(从 targetNormalCount+1 开始递增)
                    else if (stateForUpdate.ChangePalletPhase == 2)
                    {
                        int remaining = targetTotal - currentCompletedCount;
                        if (remaining <= 0)
                        {
                            stateForUpdate.ChangePalletPhase = 0;
                            stateForUpdate.CurrentBatchIndex = 1;
                            stateForUpdate.IsInFakeBatteryMode = false;
                            _logger.LogInformation("HandlePutFinishedStateAsync:流向A完成,任务号: {TaskNum}", task.RobotTaskNum);
                            return;
                        }

                        // 计算放货批次编号:从 targetNormalCount + 1 开始
                        int batchStart = targetNormalCount + 1 + (stateForUpdate.CurrentBatchIndex - 1);
                        int putCount = Math.Min(4, remaining);
                        var (start, end) = _taskProcessor.BuildBatchRange(batchStart, putCount);

                        await _taskProcessor.SendPutWithBatchAsync(task, stateForUpdate, task.RobotTargetAddress, start, end);

                        stateForUpdate.CurrentBatchIndex += putCount;
                        stateForUpdate.ChangePalletPhase = 1;
                    }
                }
                // ==================== 流向B:取正常电芯 + 回收假电芯 ====================
                else
                {
                    // Phase 1: 取正常电芯(从源地址,从1开始递增)
                    if (stateForUpdate.ChangePalletPhase == 1)
                    {
                        int remainingNormal = targetNormalCount - currentCompletedCount;

                        int pickCount = Math.Min(4, remainingNormal);
                        var (start, end) = _taskProcessor.BuildBatchRange(stateForUpdate.CurrentBatchIndex, pickCount);

                        await _taskProcessor.SendPickWithBatchAsync(task, stateForUpdate, task.RobotSourceAddress, start, end);

                        // 递增批次索引(如果刚好取完最后一个,索引会超过目标,但putfinished回来时会切换阶段)
                        stateForUpdate.CurrentBatchIndex += pickCount;

                        // 切换到 Phase 2
                        stateForUpdate.ChangePalletPhase = 2;
                    }
                    // Phase 2: 放正常电芯到目标托盘(放货编号与取货编号一致)
                    else if (stateForUpdate.ChangePalletPhase == 2)
                    {
                        int remainingNormal = targetNormalCount - currentCompletedCount;
                        if (remainingNormal <= 0)
                        {
                            // 正常电芯放完,切换到 Phase 3
                            stateForUpdate.ChangePalletPhase = 3;
                            stateForUpdate.CurrentBatchIndex = targetNormalCount + 1;
                            _logger.LogInformation("HandlePutFinishedStateAsync:正常电芯全部放完,进入Phase 3回收假电芯,任务号: {TaskNum}", task.RobotTaskNum);
                            return;
                        }

                        // 计算本批放货编号:基于 currentCompletedCount 推导批次起始(与取货编号一致)
                        // batchStart = ((currentCompletedCount - 1) / 4) * 4 + 1
                        int batchStart = ((currentCompletedCount - 1) / 4) * 4 + 1;
                        int putCount = Math.Min(4, remainingNormal);
                        var (start, end) = _taskProcessor.BuildBatchRange(batchStart, putCount);

                        await _taskProcessor.SendPutWithBatchAsync(task, stateForUpdate, task.RobotTargetAddress, start, end);

                        stateForUpdate.ChangePalletPhase = 1;
                    }
                    // Phase 3: 取假电芯(从源地址,从 targetNormalCount+1 开始递增)
                    else if (stateForUpdate.ChangePalletPhase == 3)
                    {
                        int fakeCount = targetTotal - targetNormalCount;
                        int completedFake = Math.Max(0, currentCompletedCount - targetNormalCount);
                        int remainingFake = fakeCount - completedFake;

                        if (remainingFake <= 0)
                        {
                            stateForUpdate.ChangePalletPhase = 0;
                            stateForUpdate.CurrentBatchIndex = 1;
                            stateForUpdate.IsInFakeBatteryMode = false;
                            _logger.LogInformation("HandlePutFinishedStateAsync:流向B完成,任务号: {TaskNum}", task.RobotTaskNum);
                            return;
                        }

                        int pickCount = Math.Min(4, remainingFake);
                        var (start, end) = _taskProcessor.BuildBatchRange(stateForUpdate.CurrentBatchIndex, pickCount);

                        await _taskProcessor.SendPickWithBatchAsync(task, stateForUpdate, task.RobotSourceAddress, start, end);

                        stateForUpdate.CurrentBatchIndex += pickCount;
                        stateForUpdate.ChangePalletPhase = 4;
                    }
                    // Phase 4: 放假电芯到5号位(使用 PositionIndex)
                    else if (stateForUpdate.ChangePalletPhase == 4)
                    {
                        int fakeCount = targetTotal - targetNormalCount;
                        int completedFake = Math.Max(0, currentCompletedCount - targetNormalCount);
                        int remainingFake = fakeCount - completedFake;

                        if (remainingFake <= 0)
                        {
                            stateForUpdate.ChangePalletPhase = 0;
                            stateForUpdate.CurrentBatchIndex = 1;
                            stateForUpdate.IsInFakeBatteryMode = false;
                            _logger.LogInformation("HandlePutFinishedStateAsync:流向B完成,任务号: {TaskNum}", task.RobotTaskNum);
                            return;
                        }

                        var positions = _taskProcessor.GetNextAvailableFakeBatteryPositions(Math.Min(4, remainingFake));
                        if (positions.Count == 0)
                        {
                            _logger.LogError("HandlePutFinishedStateAsync:无可用假电芯点位,任务号: {TaskNum}", task.RobotTaskNum);
                            return;
                        }

                        int start = positions.Min();
                        int end = positions.Max();

                        await _taskProcessor.SendPutWithBatchAsync(task, stateForUpdate, "5", start, end);

                        stateForUpdate.ChangePalletPhase = 3;
                    }
                }
            }
  • [ ] Step 2: 验证编译
dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj

Expected: 0 errors

  • [ ] Step 3: Commit
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs
git commit -m "feat(Robot): RobotWorkflowOrchestrator 重写 HandlePutFinishedStateAsync 实现批次指令与双流向"

Task 8: RobotWorkflowOrchestrator 重写 HandlePickFinishedStateAsync 的 ChangePallet 分支

Files:
- Modify: WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs

  • [ ] Step 1: 修改 HandlePickFinishedStateAsync 方法

找到 HandlePickFinishedStateAsync 方法(现有代码约第162-199行),将整个方法替换为:

        /// <summary>
        /// 处理取货完成后的放货指令
        /// </summary>
        /// <remarks>
        /// 根据任务类型决定放货指令格式:
        /// - 换盘任务(ChangePallet):使用批次格式 SendPutWithBatchAsync
        /// - 组盘任务(GroupPallet):使用原有格式 Putbattery,{目标地址}
        /// - 其他任务:使用原有格式
        /// </remarks>
        /// <param name="task">当前任务</param>
        /// <param name="ipAddress">机器人 IP 地址</param>
        private async Task HandlePickFinishedStateAsync(Dt_RobotTask task, string ipAddress)
        {
            string taskString;

            // 换盘任务使用批次格式
            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";

                // 流向A:放假电芯到目标托盘(Phase 2)
                if (isFlowA && state?.ChangePalletPhase == 2)
                {
                    int remaining = 48 - currentCompletedCount;
                    if (remaining <= 0) return;

                    // 计算批次编号:从 targetNormalCount + 1 开始
                    int batchStart = targetNormalCount + 1;
                    int putCount = Math.Min(4, remaining);
                    var (start, end) = _taskProcessor.BuildBatchRange(batchStart, putCount);

                    await _taskProcessor.SendPutWithBatchAsync(task, state, task.RobotTargetAddress, start, end);
                    return;
                }

                // 流向B Phase 4:放假电芯到5号位
                if (!isFlowA && state?.ChangePalletPhase == 4)
                {
                    int fakeCount = 48 - targetNormalCount;
                    int completedFake = Math.Max(0, currentCompletedCount - targetNormalCount);
                    int remainingFake = fakeCount - completedFake;

                    if (remainingFake <= 0) return;

                    var positions = _taskProcessor.GetNextAvailableFakeBatteryPositions(Math.Min(4, remainingFake));
                    if (positions.Count == 0)
                    {
                        _logger.LogError("HandlePickFinishedStateAsync:无可用假电芯点位,任务号: {TaskNum}", task.RobotTaskNum);
                        return;
                    }

                    int start = positions.Min();
                    int end = positions.Max();

                    await _taskProcessor.SendPutWithBatchAsync(task, state, "5", start, end);
                    return;
                }

                // 流向B Phase 2:放正常电芯到目标托盘
                if (!isFlowA && state?.ChangePalletPhase == 2)
                {
                    int remainingNormal = targetNormalCount - currentCompletedCount;
                    if (remainingNormal <= 0) return;

                    int batchStart = state.CurrentBatchIndex - (state.CurrentBatchIndex - 1) / 4 * 4;
                    int putCount = Math.Min(4, remainingNormal);
                    var (start, end) = _taskProcessor.BuildBatchRange(batchStart, putCount);

                    await _taskProcessor.SendPutWithBatchAsync(task, state, task.RobotTargetAddress, start, end);
                    return;
                }

                // 默认:使用原有格式
                taskString = $"Putbattery,{task.RobotTargetAddress}";
            }
            else
            {
                // 非换盘任务:使用原有格式
                taskString = $"Putbattery,{task.RobotTargetAddress}";
            }

            bool result = await _clientManager.SendToClientAsync(ipAddress, taskString);

            if (result)
            {
                _logger.LogInformation("HandlePickFinishedStateAsync:下发放货指令成功,指令: {TaskString},任务号: {TaskNum}", taskString, task.RobotTaskNum);
                QuartzLogger.Info($"下发放货指令成功,指令: {taskString}", task.RobotRoadway);

                task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();

                var stateToUpdate = _stateManager.GetState(ipAddress);
                if (stateToUpdate != null)
                {
                    stateToUpdate.CurrentTask = task;

                    if (_stateManager.TryUpdateStateSafely(ipAddress, stateToUpdate))
                    {
                        await _robotTaskService.UpdateRobotTaskAsync(task);
                    }
                }
            }
            else
            {
                _logger.LogError("HandlePickFinishedStateAsync:下发放货指令失败,指令: {TaskString},任务号: {TaskNum}", taskString, task.RobotTaskNum);
                QuartzLogger.Error($"下发放货指令失败,指令: {taskString}", task.RobotRoadway);
            }
        }
  • [ ] Step 2: 验证编译
dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj

Expected: 0 errors

  • [ ] Step 3: Commit
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs
git commit -m "feat(Robot): RobotWorkflowOrchestrator 重写 HandlePickFinishedStateAsync 支持批次指令"

Task 9: 注册依赖注入

Files:
- Modify: AutofacModuleRegister.cs 或检查现有注册

  • [ ] Step 1: 检查 FakeBatteryPositionService 是否已自动注册
grep -r "FakeBatteryPositionService" D:\Git\ShanMeiXinNengYuan\Code\WCS\WIDESEAWCS_Server\WIDESEAWCS_Core --include="*.cs"

Expected: 如果项目使用 AsImplementedInterfaces() 自动扫描,该服务已自动注册。

  • [ ] Step 2: 检查 RobotPrefixCommandHandler 构造函数变更后是否需要额外注册

由于仅新增了依赖参数,如果项目使用 Autofac 自动装配(参数自动注入),则无需修改。如果需要手动注册,添加:

// 确保 IFakeBatteryPositionService 已被注册(通常通过 AsImplementedInterfaces 自动完成)
// RobotPrefixCommandHandler 的新构造函数参数会自动装配
  • [ ] Step 3: 验证编译
dotnet build WIDESEAWCS_Server.sln

Expected: 0 errors

  • [ ] Step 4: Commit(仅当有实际改动时才提交)
git add <修改的文件>
git commit -m "feat(Robot): 注册依赖注入(如有改动)"

Task 10: 验证整体构建

  • [ ] Step 1: 完整构建
cd D:\Git\ShanMeiXinNengYuan\Code
dotnet build WCS/WIDESEAWCS_Server/WIDESEAWCS_Server.sln

Expected: 0 errors, 0 warnings

  • [ ] Step 2: 检查测试
dotnet test WCS/WIDESEAWCS_Tests/WIDESEAWCS_Tests.csproj

Expected: 现有测试通过

  • [ ] Step 3: 提交完成标记
git commit --allow-empty -m "feat(Robot): 换盘任务批次指令与双流向实现完成"

附录:测试验证步骤

手动验证流程

  1. 流向A测试(RobotSourceAddressLineCode = 11001)
  • targetNormalCount = 11
  • 预期:Phase1 取假电芯(Pickbattery,5,{pos})→ Phase2 放假电芯(Putbattery,{目标},12-15)→ 循环直到48
  1. 流向B测试(RobotSourceAddressLineCode != 11001/11010)
  • targetNormalCount = 11
  • 预期:Phase1 取正常(Pickbattery,{源},1-4)→ Phase2 放正常(Putbattery,{目标},1-4)→ 循环 → Phase3 取假(Pickbattery,{源},12-15)→ Phase4 放假到5号位(Putbattery,5,{pos})→ 循环直到回收完37个假电芯
  1. 边界条件
  • targetNormalCount = 48:走原有逻辑
  • targetNormalCount = 1:批次 1-0 格式

Plan complete.