wanshenmean
12 小时以前 d0dfd9606e3ccc6f5cd4647270328b52b2e5a384
feat: 添加堆垛机TargetAddress输送线站台检查

当 TrySelectOutboundTask 因 TargetAddress 输送线站台不空闲返回 null 时,
先尝试同 NextAddress 的其他出库任务,再尝试不同 NextAddress 的任务

refactor(StackerCraneTaskSelector): 优化 TrySelectOutboundTask 检查顺序

将本地站台检查(TargetAddress、NextAddress)提前到 WMS 移库检查之前,
避免站台不可用时发起不必要的 HTTP 调用

docs: 添加TargetAddress不可用时继续搜索实施计划

docs: 添加TargetAddress不可用时继续搜索设计文档

feat(StackerCraneTaskSelector): 在 TrySelectOutboundTask 中调用 TargetAddress 站台空闲检查

检查顺序:先检查 NextAddress 出库站台,再检查 TargetAddress 输送线站台
若 TargetAddress 站台不空闲(CV_State != 2),任务不可选

feat(StackerCraneTaskSelector): 新增 TargetAddress 输送线站台空闲检查方法

IsTargetAddressConveyorStationAvailable 通过路由查找输送线设备,读取 CV_State 判断是否空闲(CV_State == 2)

docs: 添加堆垛机TargetAddress站台检查实施计划

docs: 添加堆垛机TargetAddress输送线站台检查设计文档
已添加4个文件
已修改1个文件
614 ■■■■■ 文件已修改
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/plans/2026-04-21-stacker-crane-target-address-check.md 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/plans/2026-04-21-stacker-crane-target-address-search.md 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/specs/2026-04-21-stacker-crane-target-address-check-design.md 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/specs/2026-04-21-stacker-crane-target-address-search-design.md 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs
@@ -9,6 +9,7 @@
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_QuartzJob.ConveyorLine.Enum;
using WIDESEAWCS_QuartzJob.Models;
using WIDESEAWCS_QuartzJob.Service;
@@ -62,7 +63,7 @@
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        /// <param name="logger">日志记录器</param>
        public StackerCraneTaskSelector(ITaskService taskService, IRouterService routerService, HttpClientHelper httpClientHelper, ILogger logger)
            : this(taskService, routerService, taskNum => QueryTransferTask(httpClientHelper, taskNum), logger)
            : this(taskService, routerService, taskNum => QueryTransferTask(httpClientHelper, taskNum, logger), logger)
        {
        }
@@ -146,6 +147,23 @@
                return selectedTask;
            }
            // ===== TargetAddress ä¸å¯ç”¨æ—¶ï¼Œå…ˆå°è¯•同 NextAddress çš„其他任务 =====
            var sameStationTasks = _taskService
                .QueryStackerCraneOutTasks(deviceCode, new List<string> { candidateTask.NextAddress })
                .Where(x => x.TaskId != candidateTask.TaskId)
                .ToList();
            foreach (var sameStationTask in sameStationTasks)
            {
                selectedTask = TrySelectOutboundTask(sameStationTask);
                if (selectedTask != null)
                {
                    QuartzLogHelper.LogDebug(_logger, $"选中同站台备选出库任务,任务号: {selectedTask.TaskNum}", commonStackerCrane.DeviceName);
                    return selectedTask;
                }
            }
            // ===== åŒ NextAddress æ— å¯ç”¨ä»»åŠ¡ï¼Œå°è¯•ä¸åŒ NextAddress çš„任务 =====
            // æŸ¥æ‰¾å…¶ä»–可用的出库站台
            var otherOutStationCodes = _routerService
                .QueryNextRoutes(deviceCode, candidateTask.NextAddress, candidateTask.TaskType)
@@ -184,7 +202,21 @@
        /// <returns>可选中的任务,或 null(站台不可用)</returns>
        private Dt_Task? TrySelectOutboundTask(Dt_Task outboundTask)
        {
            // å¯¹äºŽæ‰€æœ‰å‡ºåº“任务,必须先调用 WMS åˆ¤æ–­æ˜¯å¦éœ€è¦ç§»åº“
            // å…ˆè¿›è¡Œæœ¬åœ°ç«™å°æ£€æŸ¥ï¼ˆPLC è¯»å–,快速),避免不必要的 WMS HTTP è°ƒç”¨
            // åˆ¤æ–­ TargetAddress è¾“送线站台是否空闲
            if (!IsTargetAddressConveyorStationAvailable(outboundTask))
            {
                return null;
            }
            // åˆ¤æ–­ NextAddress å‡ºåº“站台是否可用
            if (!IsOutTaskStationAvailable(outboundTask))
            {
                return null;
            }
            // ç«™å°æ£€æŸ¥é€šè¿‡åŽï¼Œè°ƒç”¨ WMS åˆ¤æ–­æ˜¯å¦éœ€è¦ç§»åº“
            var taskAfterTransferCheck = _transferCheck(outboundTask.TaskNum) ?? outboundTask;
            var taskGroup = taskAfterTransferCheck.TaskType.GetTaskTypeGroup();
@@ -206,8 +238,7 @@
                return taskAfterTransferCheck;
            }
            // åˆ¤æ–­å‡ºåº“站台是否可用
            return IsOutTaskStationAvailable(taskAfterTransferCheck) ? taskAfterTransferCheck : null;
            return taskAfterTransferCheck;
        }
        /// <summary>
@@ -219,18 +250,24 @@
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        /// <param name="taskNum">任务号</param>
        /// <returns>如果需要移库返回移库任务,否则返回 null</returns>
        private static Dt_Task? QueryTransferTask(HttpClientHelper httpClientHelper, int taskNum)
        private static Dt_Task? QueryTransferTask(HttpClientHelper httpClientHelper, int taskNum, ILogger logger)
        {
            // è°ƒç”¨ WMS çš„移库检查接口
            string configKey = nameof(ConfigKey.TransferCheck);
            string requestParam = taskNum.ToString();
            var response = httpClientHelper.Post<WebResponseContent>(
                nameof(ConfigKey.TransferCheck),
                taskNum.ToString());
                configKey,
                requestParam);
            // æ£€æŸ¥å“åº”是否成功
            if (response == null || !response.IsSuccess || response.Data == null || !response.Data.Status || response.Data.Data == null)
            {
                QuartzLogHelper.LogError(logger, $"调用WMS接口失败,接口:【{configKey}】,请求参数:【{requestParam}】,错误信息:【{(response?.Data?.Message ?? "无响应")}】", "StackerCraneTaskSelector");
                return null;
            }
            QuartzLogHelper.LogInfo(logger, $"调用WMS接口成功,接口:【{configKey}】,响应数据:【{response.Data.Data}】", "StackerCraneTaskSelector");
            // è§£æžè¿”回的任务数据
            var taskJson = response.Data.Data.ToString();
@@ -313,5 +350,47 @@
            return isOccupied;
        }
        /// <summary>
        /// åˆ¤æ–­ TargetAddress è¾“送线站台是否空闲
        /// </summary>
        /// <param name="task">出库任务</param>
        /// <returns>站台空闲(CV_State == 2)返回 true</returns>
        private bool IsTargetAddressConveyorStationAvailable([NotNull] Dt_Task task)
        {
            // ç¡®å®šä»»åŠ¡ç±»åž‹
            int taskType = task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty
                ? StackerCraneConst.EmptyPalletTaskType
                : task.TaskType;
            // é€šè¿‡è·¯ç”±æŸ¥æ‰¾ TargetAddress å¯¹åº”的设备信息
            Dt_Router? router = _routerService.QueryNextRoute(task.Roadway, task.TargetAddress, taskType);
            if (router == null)
            {
                QuartzLogHelper.LogWarn(_logger, "IsTargetAddressConveyorStationAvailable:未找到 TargetAddress è·¯ç”±ä¿¡æ¯ï¼ŒTargetAddress: {TargetAddress},任务号: {TaskNum}",
                    $"IsTargetAddressConveyorStationAvailable:未找到 TargetAddress è·¯ç”±ä¿¡æ¯ï¼ŒTargetAddress: {task.TargetAddress}", task.Roadway, task.TargetAddress, task.TaskNum);
                return false;
            }
            // æŸ¥æ‰¾è¾“送线设备
            IDevice? device = Storage.Devices.FirstOrDefault(x => x.DeviceCode == router.ChildPosiDeviceCode);
            if (device == null)
            {
                QuartzLogHelper.LogWarn(_logger, "IsTargetAddressConveyorStationAvailable:未找到输送线设备,ChildPosiDeviceCode: {ChildPosiDeviceCode},任务号: {TaskNum}",
                    $"IsTargetAddressConveyorStationAvailable:未找到输送线设备,ChildPosiDeviceCode: {router.ChildPosiDeviceCode}", task.Roadway, router.ChildPosiDeviceCode, task.TaskNum);
                return false;
            }
            // è½¬æ¢ä¸ºè¾“送线设备
            CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
            // è¯»å– CV_State,CV_State == 2 è¡¨ç¤ºç©ºé—²
            byte cvState = conveyorLine.GetValue<ConveyorLineStatus, byte>(ConveyorLineStatus.CV_State, task.TargetAddress);
            bool isAvailable = cvState == 2;
            QuartzLogHelper.LogInfo(_logger, "IsTargetAddressConveyorStationAvailable:TargetAddress: {TargetAddress},CV_State: {CV_State},是否空闲: {IsAvailable},任务号: {TaskNum}",
                $"IsTargetAddressConveyorStationAvailable:TargetAddress: {task.TargetAddress},CV_State: {cvState},是否空闲: {isAvailable}", task.Roadway, task.TargetAddress, cvState, isAvailable, task.TaskNum);
            return isAvailable;
        }
    }
}
Code/docs/superpowers/plans/2026-04-21-stacker-crane-target-address-check.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,186 @@
# å †åž›æœº TargetAddress è¾“送线站台空闲检查 å®žæ–½è®¡åˆ’
> **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:** åœ¨ `StackerCraneTaskSelector.TrySelectOutboundTask` ä¸­æ–°å¢ž `TargetAddress` è¾“送线站台空闲检查(CV_State == 2 è¡¨ç¤ºç©ºé—²ï¼‰ï¼Œä¸å¯ç”¨åˆ™è¿”回 null。
**Architecture:** åœ¨ `StackerCraneTaskSelector` ç±»ä¸­æ–°å¢ž `IsTargetAddressConveyorStationAvailable` ç§æœ‰æ–¹æ³•,通过 `_routerService.QueryNextRoute` èŽ·å–è·¯ç”±ä¿¡æ¯ï¼Œä»Ž `Storage.Devices` æ‰¾åˆ°è¾“送线设备,调用 `GetValue<ConveyorLineStatus, byte>` è¯»å– `CV_State` åˆ¤æ–­æ˜¯å¦ç©ºé—²ã€‚
**Tech Stack:** C# / .NET 6+,SqlSugar ORM,Serilog
---
## æ¶‰åŠæ–‡ä»¶
- ä¿®æ”¹: `WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs`
---
## Task 1: æ–°å¢ž IsTargetAddressConveyorStationAvailable æ–¹æ³•
**Files:**
- Modify: `WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs`
- [ ] **Step 1: ç¡®è®¤çŽ°æœ‰ using å‘½åç©ºé—´**
打开 `StackerCraneTaskSelector.cs`,检查是否已包含以下 using:
- `WIDESEAWCS_QuartzJob.ConveyorLine.Enum.ConveyorLineStatus`
- `WIDESEAWCS_Model.Models.Dt_Router`
如缺少,添加:
```csharp
using WIDESEAWCS_QuartzJob.ConveyorLine.Enum;
using WIDESEAWCS_Model.Models;
```
- [ ] **Step 2: åœ¨ç±»æœ«å°¾ï¼ˆ`IsOutTaskStationAvailable` æ–¹æ³•之后,`}` ä¹‹å‰ï¼‰æ–°å¢žæ–¹æ³•**
```csharp
/// <summary>
/// åˆ¤æ–­ TargetAddress è¾“送线站台是否空闲
/// </summary>
/// <param name="task">出库任务</param>
/// <returns>站台空闲(CV_State == 2)返回 true</returns>
private bool IsTargetAddressConveyorStationAvailable([NotNull] Dt_Task task)
{
    // ç¡®å®šä»»åŠ¡ç±»åž‹
    int taskType = task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty
        ? StackerCraneConst.EmptyPalletTaskType
        : task.TaskType;
    // é€šè¿‡è·¯ç”±æŸ¥æ‰¾ TargetAddress å¯¹åº”的设备信息
    Dt_Router? router = _routerService.QueryNextRoute(task.Roadway, task.TargetAddress, taskType);
    if (router == null)
    {
        QuartzLogHelper.LogWarn(_logger, "IsTargetAddressConveyorStationAvailable:未找到 TargetAddress è·¯ç”±ä¿¡æ¯ï¼ŒTargetAddress: {TargetAddress},任务号: {TaskNum}",
            $"IsTargetAddressConveyorStationAvailable:未找到 TargetAddress è·¯ç”±ä¿¡æ¯ï¼ŒTargetAddress: {task.TargetAddress}", task.Roadway, task.TargetAddress, task.TaskNum);
        return false;
    }
    // æŸ¥æ‰¾è¾“送线设备
    IDevice? device = Storage.Devices.FirstOrDefault(x => x.DeviceCode == router.ChildPosiDeviceCode);
    if (device == null)
    {
        QuartzLogHelper.LogWarn(_logger, "IsTargetAddressConveyorStationAvailable:未找到输送线设备,ChildPosiDeviceCode: {ChildPosiDeviceCode},任务号: {TaskNum}",
            $"IsTargetAddressConveyorStationAvailable:未找到输送线设备,ChildPosiDeviceCode: {router.ChildPosiDeviceCode}", task.Roadway, router.ChildPosiDeviceCode, task.TaskNum);
        return false;
    }
    // è½¬æ¢ä¸ºè¾“送线设备
    CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
    // è¯»å– CV_State,CV_State == 2 è¡¨ç¤ºç©ºé—²
    byte cvState = conveyorLine.GetValue<ConveyorLineStatus, byte>(ConveyorLineStatus.CV_State, task.TargetAddress);
    bool isAvailable = cvState == 2;
    QuartzLogHelper.LogInfo(_logger, "IsTargetAddressConveyorStationAvailable:TargetAddress: {TargetAddress},CV_State: {CV_State},是否空闲: {IsAvailable},任务号: {TaskNum}",
        $"IsTargetAddressConveyorStationAvailable:TargetAddress: {task.TargetAddress},CV_State: {cvState},是否空闲: {isAvailable}", task.Roadway, task.TargetAddress, cvState, isAvailable, task.TaskNum);
    return isAvailable;
}
```
- [ ] **Step 3: Commit**
```bash
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs
git commit -m "feat(StackerCraneTaskSelector): æ–°å¢ž TargetAddress è¾“送线站台空闲检查方法
IsTargetAddressConveyorStationAvailable é€šè¿‡è·¯ç”±æŸ¥æ‰¾è¾“送线设备,读取 CV_State åˆ¤æ–­æ˜¯å¦ç©ºé—²ï¼ˆCV_State == 2)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
```
---
## Task 2: åœ¨ TrySelectOutboundTask ä¸­è°ƒç”¨æ–°æ–¹æ³•
**Files:**
- Modify: `WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs`
- [ ] **Step 1: æ‰¾åˆ° TrySelectOutboundTask æ–¹æ³•中的 return è¯­å¥**
当前方法末尾(line ~210):
```csharp
return IsOutTaskStationAvailable(taskAfterTransferCheck) ? taskAfterTransferCheck : null;
```
- [ ] **Step 2: åœ¨ return è¯­å¥ä¹‹å‰æ’å…¥ TargetAddress æ£€æŸ¥é€»è¾‘**
在 `return IsOutTaskStationAvailable(...)` ä¹‹å‰æ’入:
```csharp
// åˆ¤æ–­ TargetAddress è¾“送线站台是否空闲
if (!IsTargetAddressConveyorStationAvailable(taskAfterTransferCheck))
{
    return null;
}
```
修改后:
```csharp
// åˆ¤æ–­ TargetAddress è¾“送线站台是否空闲
if (!IsTargetAddressConveyorStationAvailable(taskAfterTransferCheck))
{
    return null;
}
// åˆ¤æ–­å‡ºåº“站台是否可用
return IsOutTaskStationAvailable(taskAfterTransferCheck) ? taskAfterTransferCheck : null;
```
- [ ] **Step 3: éªŒè¯ä¿®æ”¹ä½ç½®æ­£ç¡®**
确认修改后的 `TrySelectOutboundTask` æ–¹æ³•末尾逻辑为:
```csharp
// åˆ¤æ–­ TargetAddress è¾“送线站台是否空闲
if (!IsTargetAddressConveyorStationAvailable(taskAfterTransferCheck))
{
    return null;
}
// åˆ¤æ–­å‡ºåº“站台是否可用
return IsOutTaskStationAvailable(taskAfterTransferCheck) ? taskAfterTransferCheck : null;
```
- [ ] **Step 4: Commit**
```bash
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs
git commit -m "feat(StackerCraneTaskSelector): åœ¨ TrySelectOutboundTask ä¸­è°ƒç”¨ TargetAddress ç«™å°ç©ºé—²æ£€æŸ¥
检查顺序:先检查 NextAddress å‡ºåº“站台,再检查 TargetAddress è¾“送线站台
若 TargetAddress ç«™å°ä¸ç©ºé—²ï¼ˆCV_State != 2),任务不可选
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
```
---
## Task 3: éªŒè¯æž„建
- [ ] **Step 1: æ‰§è¡Œæž„建验证**
```bash
cd D:/Git/ShanMeiXinNengYuan/Code
dotnet build WCS/WIDESEAWCS_Server/WIDESEAWCS_Server.sln
```
预期:无编译错误
- [ ] **Step 2: å¦‚有错误,分析并修复**
常见错误:
- ç¼ºå°‘ using â†’ æ·»åŠ å‘½åç©ºé—´
- ç±»åž‹è½¬æ¢å¼‚常 â†’ ç¡®è®¤è®¾å¤‡ç±»åž‹ä¸º `CommonConveyorLine`
- æ–¹æ³•未找到 â†’ ç¡®è®¤æ–¹æ³•签名正确
---
## éªŒè¯æ¸…单
- [ ] `IsTargetAddressConveyorStationAvailable` æ–¹æ³•已添加在 `IsOutTaskStationAvailable` æ–¹æ³•之后
- [ ] `TrySelectOutboundTask` æ–¹æ³•在判断 `IsOutTaskStationAvailable` ä¹‹å‰ï¼Œå…ˆåˆ¤æ–­ `IsTargetAddressConveyorStationAvailable`
- [ ] æ–°å¢žæ–¹æ³•包含完整的日志记录(Warn å’Œ Info)
- [ ] `CV_State == 2` ä½œä¸ºç©ºé—²åˆ¤æ–­æ¡ä»¶
- [ ] `task.TargetAddress` ç›´æŽ¥ä½œä¸º `GetValue` çš„设备子编号
- [ ] æž„建通过,无编译错误
- [ ] å·²æäº¤ä¸¤ä¸ª commit(方法新增 + è°ƒç”¨ç‚¹ï¼‰
Code/docs/superpowers/plans/2026-04-21-stacker-crane-target-address-search.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,143 @@
# å †åž›æœº TargetAddress ä¸å¯ç”¨æ—¶ç»§ç»­æœç´¢ å®žæ–½è®¡åˆ’
> **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:** åœ¨ `SelectTask` æ–¹æ³•中,当 `TrySelectOutboundTask` å›  `TargetAddress` ä¸å¯ç”¨è¿”回 null æ—¶ï¼Œå¢žåŠ åŒ NextAddress å…¶ä»–任务的搜索逻辑。
**Architecture:** åœ¨ `SelectTask` çš„ `TrySelectOutboundTask(candidateTask)` è¿”回 null ä¹‹åŽã€çŽ°æœ‰å¤‡é€‰ä»»åŠ¡å¾ªçŽ¯ä¹‹å‰ï¼Œæ’å…¥åŒ NextAddress ä»»åŠ¡æœç´¢ã€‚
**Tech Stack:** C# / .NET 6+,SqlSugar ORM,Serilog
---
## æ¶‰åŠæ–‡ä»¶
- ä¿®æ”¹: `WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs`
---
## Task 1: ä¿®æ”¹ SelectTask æ–¹æ³• - æ·»åŠ åŒ NextAddress ä»»åŠ¡æœç´¢
**Files:**
- Modify: `WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs`
- [ ] **Step 1: æ‰¾åˆ° SelectTask æ–¹æ³•中的目标位置**
打开 `StackerCraneTaskSelector.cs`,找到 `SelectTask` æ–¹æ³•中的以下代码(约 lines 143-166):
```csharp
// å°è¯•选择出库任务(可能需要移库检查和站台可用性判断)
Dt_Task? selectedTask = TrySelectOutboundTask(candidateTask);
if (selectedTask != null)
{
    QuartzLogHelper.LogDebug(_logger, $"选中出库任务,任务号: {selectedTask.TaskNum}", commonStackerCrane.DeviceName);
    return selectedTask;
}
// æŸ¥æ‰¾å…¶ä»–可用的出库站台
var otherOutStationCodes = _routerService
    .QueryNextRoutes(deviceCode, candidateTask.NextAddress, candidateTask.TaskType)
    .Select(x => x.ChildPosi)
    .ToList();
// æŸ¥è¯¢å…¶ä»–站台的出库任务
var tasks = _taskService.QueryStackerCraneOutTasks(deviceCode, otherOutStationCodes);
foreach (var alternativeTask in tasks)
{
    selectedTask = TrySelectOutboundTask(alternativeTask);
    if (selectedTask != null)
    {
        QuartzLogHelper.LogDebug(_logger, $"选中备选出库任务,任务号: {selectedTask.TaskNum}", commonStackerCrane.DeviceName);
        return selectedTask;
    }
}
```
**Step 2: åœ¨ `// æŸ¥æ‰¾å…¶ä»–可用的出库站台` ä¹‹å‰æ’入同 NextAddress æœç´¢é€»è¾‘**
将:
```csharp
// æŸ¥æ‰¾å…¶ä»–可用的出库站台
var otherOutStationCodes = _routerService
    ...
```
替换为:
```csharp
// ===== TargetAddress ä¸å¯ç”¨æ—¶ï¼Œå…ˆå°è¯•同 NextAddress çš„其他任务 =====
var sameStationTasks = _taskService
    .QueryStackerCraneOutTasks(deviceCode, new List<string> { candidateTask.NextAddress })
    .Where(x => x.TaskId != candidateTask.TaskId)
    .ToList();
foreach (var sameStationTask in sameStationTasks)
{
    selectedTask = TrySelectOutboundTask(sameStationTask);
    if (selectedTask != null)
    {
        QuartzLogHelper.LogDebug(_logger, $"选中同站台备选出库任务,任务号: {selectedTask.TaskNum}", commonStackerCrane.DeviceName);
        return selectedTask;
    }
}
// ===== åŒ NextAddress æ— å¯ç”¨ä»»åŠ¡ï¼Œå°è¯•ä¸åŒ NextAddress çš„任务 =====
// æŸ¥æ‰¾å…¶ä»–可用的出库站台
var otherOutStationCodes = _routerService
    .QueryNextRoutes(deviceCode, candidateTask.NextAddress, candidateTask.TaskType)
    .Select(x => x.ChildPosi)
    .ToList();
// æŸ¥è¯¢å…¶ä»–站台的出库任务
var tasks = _taskService.QueryStackerCraneOutTasks(deviceCode, otherOutStationCodes);
foreach (var alternativeTask in tasks)
{
    selectedTask = TrySelectOutboundTask(alternativeTask);
    if (selectedTask != null)
    {
        QuartzLogHelper.LogDebug(_logger, $"选中备选出库任务,任务号: {selectedTask.TaskNum}", commonStackerCrane.DeviceName);
        return selectedTask;
    }
}
```
- [ ] **Step 3: Commit**
```bash
git add WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs
git commit -m "feat(StackerCraneTaskSelector): TargetAddress不可用时搜索同NextAddress的其他任务
当 TrySelectOutboundTask å›  TargetAddress è¾“送线站台不空闲返回 null æ—¶ï¼Œ
先尝试同 NextAddress çš„其他出库任务,再尝试不同 NextAddress çš„任务
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
```
---
## Task 2: éªŒè¯æž„建
- [ ] **Step 1: æ‰§è¡Œæž„建验证**
```bash
cd D:/Git/ShanMeiXinNengYuan/Code
dotnet build WCS/WIDESEAWCS_Server/WIDESEAWCS_Server.sln
```
预期:无编译错误(测试项目的预存在错误可忽略)
- [ ] **Step 2: å¦‚有错误,分析并修复**
常见错误:
- ç±»åž‹ä¸åŒ¹é… â†’ ç¡®è®¤ `QueryStackerCraneOutTasks` è¿”回 `List<Dt_Task>`
- ç¼ºå°‘ using â†’ æ·»åŠ  `using System.Linq;`
---
## éªŒè¯æ¸…单
- [ ] åŒ NextAddress ä»»åŠ¡æœç´¢é€»è¾‘å·²æ·»åŠ åœ¨ `TrySelectOutboundTask` è¿”回 null ä¹‹åŽ
- [ ] ä½¿ç”¨ `TaskId` è¿‡æ»¤æŽ’除当前任务
- [ ] åŒ NextAddress æ— å¯ç”¨ä»»åŠ¡åŽç»§ç»­å°è¯•ä¸åŒ NextAddress çš„任务
- [ ] æ—¥å¿—记录"同站台备选出库任务"
- [ ] æž„建通过,无编译错误
- [ ] å·²æäº¤ commit
Code/docs/superpowers/specs/2026-04-21-stacker-crane-target-address-check-design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
# å †åž›æœºå‡ºåº“任务 TargetAddress è¾“送线站台空闲检查 è®¾è®¡æ–‡æ¡£
## 1. èƒŒæ™¯ä¸Žç›®æ ‡
在 `StackerCraneTaskSelector.TrySelectOutboundTask` æ–¹æ³•中,当前只检查出库站台的 `NextAddress` æ˜¯å¦å¯ç”¨ï¼ˆ`IsOutTaskStationAvailable`)。需求:增加对 `TargetAddress` è¾“送线站台的空闲状态检查。只有当 `TargetAddress` è¾“送线站台空闲(`CV_State == 2`)时,才认为该出库任务真正可选。
## 2. è®¾è®¡æ–¹æ¡ˆ
### 2.1 æ–°å¢žç§æœ‰æ–¹æ³•
在 `StackerCraneTaskSelector` ç±»ä¸­æ–°å¢žæ–¹æ³• `IsTargetAddressConveyorStationAvailable`:
```csharp
/// <summary>
/// åˆ¤æ–­ TargetAddress è¾“送线站台是否空闲
/// </summary>
/// <param name="task">出库任务</param>
/// <returns>站台空闲返回 true,否则返回 false</returns>
private bool IsTargetAddressConveyorStationAvailable([NotNull] Dt_Task task)
{
    // ç¡®å®šä»»åŠ¡ç±»åž‹
    int taskType = task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty
        ? StackerCraneConst.EmptyPalletTaskType
        : task.TaskType;
    // é€šè¿‡è·¯ç”±æŸ¥æ‰¾ TargetAddress å¯¹åº”的设备信息
    Dt_Router? router = _routerService.QueryNextRoute(task.Roadway, task.TargetAddress, taskType);
    if (router == null)
    {
        QuartzLogHelper.LogWarn(_logger, "IsTargetAddressConveyorStationAvailable:未找到 TargetAddress è·¯ç”±ä¿¡æ¯ï¼ŒTargetAddress: {TargetAddress},任务号: {TaskNum}",
            $"IsTargetAddressConveyorStationAvailable:未找到 TargetAddress è·¯ç”±ä¿¡æ¯ï¼ŒTargetAddress: {task.TargetAddress}", task.Roadway, task.TargetAddress, task.TaskNum);
        return false;
    }
    // æŸ¥æ‰¾è¾“送线设备
    IDevice? device = Storage.Devices.FirstOrDefault(x => x.DeviceCode == router.ChildPosiDeviceCode);
    if (device == null)
    {
        QuartzLogHelper.LogWarn(_logger, "IsTargetAddressConveyorStationAvailable:未找到输送线设备,ChildPosiDeviceCode: {ChildPosiDeviceCode},任务号: {TaskNum}",
            $"IsTargetAddressConveyorStationAvailable:未找到输送线设备,ChildPosiDeviceCode: {router.ChildPosiDeviceCode}", task.Roadway, router.ChildPosiDeviceCode, task.TaskNum);
        return false;
    }
    // è½¬æ¢ä¸ºè¾“送线设备
    CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
    // è¯»å– CV_State,CV_State == 2 è¡¨ç¤ºç©ºé—²
    byte cvState = conveyorLine.GetValue<ConveyorLineStatus, byte>(ConveyorLineStatus.CV_State, task.TargetAddress);
    bool isAvailable = cvState == 2;
    QuartzLogHelper.LogInfo(_logger, "IsTargetAddressConveyorStationAvailable:TargetAddress: {TargetAddress},CV_State: {CV_State},是否空闲: {IsAvailable},任务号: {TaskNum}",
        $"IsTargetAddressConveyorStationAvailable:TargetAddress: {task.TargetAddress},CV_State: {cvState},是否空闲: {isAvailable}", task.Roadway, task.TargetAddress, cvState, isAvailable, task.TaskNum);
    return isAvailable;
}
```
### 2.2 è°ƒç”¨ç‚¹ä¿®æ”¹
在 `TrySelectOutboundTask` æ–¹æ³•中,在 `return IsOutTaskStationAvailable(taskAfterTransferCheck) ? taskAfterTransferCheck : null;` ä¹‹å‰æ’入:
```csharp
// åˆ¤æ–­ TargetAddress è¾“送线站台是否空闲
if (!IsTargetAddressConveyorStationAvailable(taskAfterTransferCheck))
{
    return null;
}
```
### 2.3 æµç¨‹å›¾
```
出库任务进入 TrySelectOutboundTask
    â†“
移库检查(_transferCheck)
    â†“
是否移库任务?是 â†’ è¿”回任务
    â†“(否)
IsOutTaskStationAvailable(NextAddress å‡ºåº“站台) â†’ ä¸å¯ç”¨ â†’ è¿”回 null
    â†“(可用)
IsTargetAddressConveyorStationAvailable(TargetAddress è¾“送线站台) â†’ ä¸å¯ç”¨ â†’ è¿”回 null
    â†“(可用,即 CV_State == 2)
返回任务
```
## 3. æ¶‰åŠçš„命名空间
新增方法需要 using:
- `WIDESEAWCS_QuartzJob.ConveyorLine.Enum.ConveyorLineStatus`(`ConveyorLineStatus.CV_State` æžšä¸¾ï¼‰
- `WIDESEAWCS_Model.Models.Dt_Router`(路由实体)
- `WIDESEAWCS_Core.Storage`(设备存储)
## 4. é£Žé™©ä¸Žçº¦æŸ
- `CV_State == 2` è¡¨ç¤ºç©ºé—²æ˜¯ä¸šåŠ¡çº¦å®šï¼Œéœ€ä¸Ž PLC ä¾§åè®®ä¿æŒä¸€è‡´
- å¦‚设备未连接或协议配置缺失,`GetValue` ä¼šæŠ›å‡ºå¼‚常,当前设计让异常向上传播,调用方通过 try-catch å…œåº•
Code/docs/superpowers/specs/2026-04-21-stacker-crane-target-address-search-design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
# å †åž›æœºå‡ºåº“任务 TargetAddress ä¸å¯ç”¨æ—¶ç»§ç»­æœç´¢ è®¾è®¡æ–‡æ¡£
## 1. èƒŒæ™¯ä¸Žç›®æ ‡
在 `StackerCraneTaskSelector.SelectTask` æ–¹æ³•中,当 `TrySelectOutboundTask` å›  `TargetAddress` è¾“送线站台不空闲(`CV_State != 2`)而返回 null æ—¶ï¼Œéœ€è¦ç»§ç»­æœç´¢å…¶ä»–可选的出库任务,而不是直接返回 null æˆ–仅尝试不同 NextAddress çš„任务。
## 2. è®¾è®¡æ–¹æ¡ˆ
### 2.1 æœç´¢é¡ºåº
当 TargetAddress ä¸å¯ç”¨æ—¶ï¼ŒæŒ‰ä»¥ä¸‹é¡ºåºæœç´¢å¤‡é€‰ä»»åŠ¡ï¼š
1. **同 NextAddress çš„其他任务** - æŸ¥è¯¢ä¸Žå½“前任务 NextAddress ç›¸åŒçš„其他出库任务,尝试其 TargetAddress æ˜¯å¦å¯ç”¨
2. **不同 NextAddress çš„任务** - æŸ¥è¯¢å…¶ä»–站台(N extAddress ä¸åŒï¼‰çš„出库任务(现有逻辑)
### 2.2 ä»£ç ä¿®æ”¹
在 `SelectTask` æ–¹æ³•中,`TrySelectOutboundTask(candidateTask)` è¿”回 null ä¹‹åŽã€çŽ°æœ‰å¤‡é€‰ä»»åŠ¡å¾ªçŽ¯ä¹‹å‰ï¼Œæ’å…¥åŒ NextAddress ä»»åŠ¡æœç´¢é€»è¾‘ï¼š
```csharp
// å°è¯•选择出库任务
Dt_Task? selectedTask = TrySelectOutboundTask(candidateTask);
if (selectedTask != null)
{
    return selectedTask;
}
// ===== TargetAddress ä¸å¯ç”¨æ—¶ï¼Œå…ˆå°è¯•同 NextAddress çš„其他任务 =====
// æŸ¥è¯¢ä¸Žå½“前任务 NextAddress ç›¸åŒçš„其他出库任务
var sameStationTasks = _taskService
    .QueryStackerCraneOutTasks(deviceCode, new List<string> { candidateTask.NextAddress })
    .Where(x => x.TaskId != candidateTask.TaskId)
    .ToList();
foreach (var sameStationTask in sameStationTasks)
{
    selectedTask = TrySelectOutboundTask(sameStationTask);
    if (selectedTask != null)
    {
        QuartzLogHelper.LogDebug(_logger, $"选中同站台备选出库任务,任务号: {selectedTask.TaskNum}", commonStackerCrane.DeviceName);
        return selectedTask;
    }
}
// ===== åŒ NextAddress æ— å¯ç”¨ä»»åŠ¡ï¼Œå°è¯•ä¸åŒ NextAddress çš„任务(现有逻辑)=====
var otherOutStationCodes = _routerService
    .QueryNextRoutes(deviceCode, candidateTask.NextAddress, candidateTask.TaskType)
    .Select(x => x.ChildPosi)
    .ToList();
var tasks = _taskService.QueryStackerCraneOutTasks(deviceCode, otherOutStationCodes);
foreach (var alternativeTask in tasks)
{
    selectedTask = TrySelectOutboundTask(alternativeTask);
    if (selectedTask != null)
    {
        QuartzLogHelper.LogDebug(_logger, $"选中备选出库任务,任务号: {selectedTask.TaskNum}", commonStackerCrane.DeviceName);
        return selectedTask;
    }
}
```
### 2.3 æ— éœ€æ–°å¢žæ–¹æ³•
利用现有的 `QueryStackerCraneOutTasks(deviceCode, stationCodes)` æ–¹æ³•,传入单站台列表即可查询同 NextAddress çš„任务。过滤掉当前任务(`TaskId` ä¸åŒï¼‰é¿å…é‡å¤å°è¯•。
### 2.4 æµç¨‹å›¾
```
SelectTask
    â†“
TrySelectOutboundTask(candidateTask)
    â†“
TargetAddress å¯ç”¨ï¼Ÿå¦ â†’ è¿”回 null
    â†“
查询同 NextAddress çš„其他出库任务
    â†“
遍历同站台任务
  â†’ TrySelectOutboundTask(task) â†’ å¯ç”¨ â†’ è¿”回任务
  â†’ ä¸å¯ç”¨ â†’ ç»§ç»­ä¸‹ä¸€ä¸ª
    â†“
同 NextAddress æ— å¯ç”¨ä»»åŠ¡
    â†“
查询不同 NextAddress çš„出库任务(现有逻辑)
    â†“
遍历 â†’ è¿”回可用任务 / æ— å¯ç”¨ â†’ è¿”回 null
```
## 3. æ¶‰åŠçš„命名空间
无需新增 using,现有方法签名已满足。
## 4. é£Žé™©ä¸Žçº¦æŸ
- `TaskId` ä½œä¸ºå”¯ä¸€æ ‡è¯†ç”¨äºŽè¿‡æ»¤å·²å°è¯•任务,假设任务表中无重复 TaskId
- `QueryStackerCraneOutTasks` è¿”回的任务列表需要考虑排序(如果有业务规则决定优先级)
- å¦‚果同 NextAddress æœ‰å¤§é‡ä»»åŠ¡ï¼Œå¯èƒ½å¢žåŠ é€‰æ‹©å»¶è¿Ÿ