# 换盘任务批次指令与双流向实现计划 > **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` 属性后添加: ```csharp /// /// 是否处于假电芯补充模式 /// public bool IsInFakeBatteryMode { get; set; } /// /// 当前批次起始编号(用于递增计算取货/放货编号) /// /// /// 在批次模式下,每批取货/放货的起始编号从1开始递增。 /// 用于计算 {start}-{end} 格式中的 start 值。 /// public int CurrentBatchIndex { get; set; } = 1; /// /// 换盘任务当前阶段 /// /// /// 阶段定义: /// 0: 未开始 /// 1: 取正常电芯(流向B) / 取假电芯(流向A) /// 2: 放正常电芯(流向B) / 放假电芯(流向A) /// 3: 取假电芯(流向B Phase2) /// 4: 放假电芯到5号位(流向B Phase2) /// public int ChangePalletPhase { get; set; } ``` - [ ] **Step 2: 验证编译** ```bash cd D:\Git\ShanMeiXinNengYuan\Code\WCS\WIDESEAWCS_Server dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj ``` Expected: 0 errors - [ ] **Step 3: Commit** ```bash 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.cs` 的 `MarkAsUsed` 方法后添加: ```csharp /// /// 标记指定点位为可用(释放点位) /// /// 点位索引列表 /// 是否成功 bool MarkAsAvailable(List positions); ``` - [ ] **Step 2: 在仓储实现中添加 MarkAsAvailable 方法(在 MarkAsUsed 方法后)** 在 `FakeBatteryPositionRepository.cs` 的 `MarkAsUsed` 方法后添加: ```csharp /// public bool MarkAsAvailable(List positions) { if (positions == null || positions.Count == 0) return true; return Db.Updateable() .SetColumns(x => x.IsUsed, false) .Where(x => positions.Contains(x.PositionIndex)) .ExecuteCommand() > 0; } ``` - [ ] **Step 3: 验证编译** ```bash dotnet build WIDESEAWCS_Server.sln ``` Expected: 0 errors - [ ] **Step 4: Commit** ```bash 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.cs` 的 `MarkAsUsed` 方法后添加: ```csharp /// /// 标记指定点位为可用(释放点位) /// /// 点位索引列表 /// 是否成功 bool MarkAsAvailable(List positions); ``` - [ ] **Step 2: 在服务实现中添加 MarkAsAvailable 方法** ```csharp /// public bool MarkAsAvailable(List positions) { return BaseDal.MarkAsAvailable(positions); } ``` - [ ] **Step 3: 验证编译** ```bash dotnet build WIDESEAWCS_Server.sln ``` Expected: 0 errors - [ ] **Step 4: Commit** ```bash 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` 方法后添加: ```csharp /// /// 计算批次编号范围 /// /// /// 返回格式:(start, end) /// - remaining >= 4: (currentIndex, currentIndex + 3) /// - remaining > 1: (currentIndex, currentIndex + remaining - 1) /// - remaining == 1: (currentIndex, 0) -- 单个物品用 0 表示 end /// /// 当前批次起始编号 /// 剩余数量 /// (start, end) 元组 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 方法** ```csharp /// /// 下发取货指令(带批次格式和总数) /// /// /// 发送顺序: /// 1. PickTotalNum,{N} -- 真实电芯总数 /// 2. Pickbattery,{位置},{start}-{end} -- 批次取货指令 /// /// 下发成功后更新任务状态为"机器人执行中"。 /// /// 要下发的任务对象 /// 机器人当前状态 /// 取货位置 /// 批次起始编号 /// 批次结束编号 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 方法** ```csharp /// /// 下发放货指令(带批次格式和总数) /// /// /// 发送顺序: /// 1. PutTotalNum,{N} -- 真实电芯总数 /// 2. Putbattery,{位置},{start}-{end} -- 批次放货指令 /// /// 下发成功后更新任务状态为"机器人执行中"。 /// /// 要下发的任务对象 /// 机器人当前状态 /// 放货位置 /// 批次起始编号 /// 批次结束编号 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: 验证编译** ```bash dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj ``` Expected: 0 errors - [ ] **Step 5: Commit** ```bash 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":` 分支,替换为: ```csharp // 全部取货完成 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":` 分支,替换为: ```csharp // 全部放货完成 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(); 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(); await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished"); QuartzLogger.Info($"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName); return true; } } return false; } ``` - [ ] **Step 3: 验证编译** ```bash dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj ``` Expected: 0 errors - [ ] **Step 4: Commit** ```bash 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 依赖** 在类顶部添加字段和构造函数参数: ```csharp /// /// 假电芯位置服务 /// 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行附近),替换其中的放货成功处理逻辑为: ```csharp // 如果放货成功 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行),替换为: ```csharp // 如果是拆盘任务 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: 验证编译** ```bash dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj ``` Expected: 0 errors - [ ] **Step 5: Commit** ```bash 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())` 分支,将其完整替换为: ```csharp 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: 验证编译** ```bash dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj ``` Expected: 0 errors - [ ] **Step 3: Commit** ```bash 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行),将整个方法替换为: ```csharp /// /// 处理取货完成后的放货指令 /// /// /// 根据任务类型决定放货指令格式: /// - 换盘任务(ChangePallet):使用批次格式 SendPutWithBatchAsync /// - 组盘任务(GroupPallet):使用原有格式 Putbattery,{目标地址} /// - 其他任务:使用原有格式 /// /// 当前任务 /// 机器人 IP 地址 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: 验证编译** ```bash dotnet build WIDESEAWCS_Tasks/WIDESEAWCS_Tasks.csproj ``` Expected: 0 errors - [ ] **Step 3: Commit** ```bash 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 是否已自动注册** ```bash grep -r "FakeBatteryPositionService" D:\Git\ShanMeiXinNengYuan\Code\WCS\WIDESEAWCS_Server\WIDESEAWCS_Core --include="*.cs" ``` Expected: 如果项目使用 `AsImplementedInterfaces()` 自动扫描,该服务已自动注册。 - [ ] **Step 2: 检查 RobotPrefixCommandHandler 构造函数变更后是否需要额外注册** 由于仅新增了依赖参数,如果项目使用 Autofac 自动装配(参数自动注入),则无需修改。如果需要手动注册,添加: ```csharp // 确保 IFakeBatteryPositionService 已被注册(通常通过 AsImplementedInterfaces 自动完成) // RobotPrefixCommandHandler 的新构造函数参数会自动装配 ``` - [ ] **Step 3: 验证编译** ```bash dotnet build WIDESEAWCS_Server.sln ``` Expected: 0 errors - [ ] **Step 4: Commit**(仅当有实际改动时才提交) ```bash git add <修改的文件> git commit -m "feat(Robot): 注册依赖注入(如有改动)" ``` --- ## Task 10: 验证整体构建 - [ ] **Step 1: 完整构建** ```bash cd D:\Git\ShanMeiXinNengYuan\Code dotnet build WCS/WIDESEAWCS_Server/WIDESEAWCS_Server.sln ``` Expected: 0 errors, 0 warnings - [ ] **Step 2: 检查测试** ```bash dotnet test WCS/WIDESEAWCS_Tests/WIDESEAWCS_Tests.csproj ``` Expected: 现有测试通过 - [ ] **Step 3: 提交完成标记** ```bash git commit --allow-empty -m "feat(Robot): 换盘任务批次指令与双流向实现完成" ``` --- ## 附录:测试验证步骤 ### 手动验证流程 1. **流向A测试(RobotSourceAddressLineCode = 11001)** - targetNormalCount = 11 - 预期:Phase1 取假电芯(Pickbattery,5,{pos})→ Phase2 放假电芯(Putbattery,{目标},12-15)→ 循环直到48 2. **流向B测试(RobotSourceAddressLineCode != 11001/11010)** - targetNormalCount = 11 - 预期:Phase1 取正常(Pickbattery,{源},1-4)→ Phase2 放正常(Putbattery,{目标},1-4)→ 循环 → Phase3 取假(Pickbattery,{源},12-15)→ Phase4 放假到5号位(Putbattery,5,{pos})→ 循环直到回收完37个假电芯 3. **边界条件** - targetNormalCount = 48:走原有逻辑 - targetNormalCount = 1:批次 1-0 格式 --- **Plan complete.**