diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index af39aa9..7635d3a 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -20,7 +20,8 @@
"Bash(dotnet /Users/wenxiansheng/.nuget/packages/avalonia/11.1.5/lib/netstandard2.0/Avalonia.Base.dll type list)",
"Bash(dotnet new *)",
"Bash(dotnet add *)",
- "Bash(python3 *)"
+ "Bash(python3 *)",
+ "Bash(git restore *)"
]
}
}
diff --git a/Sln.Wcs.Business/TaskCreateService.cs b/Sln.Wcs.Business/TaskCreateService.cs
new file mode 100644
index 0000000..d09332d
--- /dev/null
+++ b/Sln.Wcs.Business/TaskCreateService.cs
@@ -0,0 +1,227 @@
+using Sln.Wcs.Business.Domain.Enum;
+using Sln.Wcs.Model.Domain;
+using Sln.Wcs.Serilog;
+
+namespace Sln.Wcs.Business;
+
+///
+/// 接驳位拓扑驱动的任务创建服务
+/// 根据起止楼栋/楼层/位置,自动生成 LiveTaskQueue 及其子任务明细序列
+///
+public class TaskCreateService
+{
+ private readonly SerilogHelper _logger;
+
+ public TaskCreateService(SerilogHelper logger) => _logger = logger;
+
+ #region 拓扑配置
+
+ private record TopoNode(int Building, int Floor);
+
+ ///
+ /// 拓扑邻接表:(栋,层) → 可达的相邻节点
+ /// 所有楼栋 1~5F,本栋提升机可直达任意楼层(全连接)
+ /// 跨栋仅 2F 连通: (13,2)↔(14,2) 廊桥交接, (14,2)↔(15,2) AGV直连
+ ///
+ private static readonly Dictionary> Topology = BuildTopology();
+
+ private static Dictionary> BuildTopology()
+ {
+ var dict = new Dictionary>();
+
+ // 每个楼栋内全连接(提升机直达任意楼层)
+ foreach (var building in new[] { 13, 14, 15 })
+ {
+ for (int f1 = 1; f1 <= 5; f1++)
+ {
+ var neighbors = new List();
+ for (int f2 = 1; f2 <= 5; f2++)
+ {
+ if (f1 != f2)
+ neighbors.Add(new(building, f2));
+ }
+ dict[new(building, f1)] = neighbors;
+ }
+ }
+
+ // 跨栋: 仅 2F 连通
+ dict[new(13, 2)].Add(new(14, 2)); // 13↔14 廊桥交接
+ dict[new(14, 2)].Add(new(13, 2));
+ dict[new(14, 2)].Add(new(15, 2)); // 14↔15 AGV直连
+ dict[new(15, 2)].Add(new(14, 2));
+
+ return dict;
+ }
+
+ #endregion
+
+ ///
+ /// 创建任务:输入楼栋/楼层/位置号,自动生成明细序列
+ ///
+ /// 起始楼栋 (13/14/15)
+ /// 起始楼层
+ /// 起始位置号,如 "01"
+ /// 目标楼栋 (13/14/15)
+ /// 目标楼层
+ /// 目标位置号,如 "02"
+ /// 任务类型: 1-入库, 2-出库
+ /// 任务类别: 1-包材, 2-成品, 3-托盘
+ /// 托盘条码
+ /// 物料编码
+ /// 完整 LiveTaskQueue(含明细列表)
+ public LiveTaskQueue CreateTask(
+ int startBuilding, int startFloor, string startLocation,
+ int endBuilding, int endFloor, string endLocation,
+ int taskType, int taskCategory,
+ string palletBarcode, string materialCode)
+ {
+ var startPoint = PosCode(startBuilding, startFloor, startLocation);
+ var endPoint = PosCode(endBuilding, endFloor, endLocation);
+
+ _logger.Info($"创建任务 - {startPoint} ({startBuilding}#_{startFloor}F) → {endPoint} ({endBuilding}#_{endFloor}F)");
+
+ // Step 1: BFS 找最短路径(节点序列)
+ var nodes = FindPath(startBuilding, startFloor, endBuilding, endFloor);
+ _logger.Info($"路由节点: {string.Join(" → ", nodes.Select(n => $"({n.Building},{n.Floor})"))}");
+
+ // Step 2: 沿节点路径逐段生成明细
+ // 规则: 同层移动=AGV, 跨层=提升机, 跨栋(13↔14)=廊桥交接两段AGV
+ var details = new List();
+ int seq = 1;
+ var taskCode = GenerateTaskCode();
+ string currentPos = startPoint; // 当前货物所在接驳位
+
+ for (int i = 0; i < nodes.Count - 1; i++)
+ {
+ var (b1, f1) = nodes[i];
+ var (b2, f2) = nodes[i + 1];
+
+ if (b1 != b2) // 跨栋
+ {
+ if (IsBridgeRequired(b1, b2))
+ {
+ // 13↔14: 廊桥交接 — 13# AGV 送到廊桥,等待 14# AGV
+ details.Add(NewDetail(taskCode, palletBarcode, materialCode,
+ taskType, taskCategory, seq++, currentPos, "BRIDGE_13_14", deviceType: 1));
+ currentPos = "BRIDGE_13_14";
+ }
+ // 14↔15: 14# AGV 可直接驶入 15#,不生成过渡段,由下一步直达
+ }
+ else if (f1 != f2) // 同栋跨层 → AGV到提升机入口 + 提升机
+ {
+ var hoistEntry = HoistPoint(b1, f1);
+ var hoistExit = HoistPoint(b2, f2);
+
+ details.Add(NewDetail(taskCode, palletBarcode, materialCode,
+ taskType, taskCategory, seq++, currentPos, hoistEntry, deviceType: 1));
+ details.Add(NewDetail(taskCode, palletBarcode, materialCode,
+ taskType, taskCategory, seq++, hoistEntry, hoistExit, deviceType: 2));
+
+ currentPos = hoistExit;
+ }
+ }
+
+ // Step 3: 最终 AGV 送达终点(同栋同层时直接一条 AGV)
+ if (details.Count == 0 || currentPos != endPoint)
+ {
+ details.Add(NewDetail(taskCode, palletBarcode, materialCode,
+ taskType, taskCategory, seq, currentPos, endPoint, deviceType: 1));
+ }
+
+ _logger.Info($"生成 {details.Count} 条明细");
+
+ return new LiveTaskQueue
+ {
+ taskCode = taskCode,
+ taskType = taskType,
+ taskCategory = taskCategory,
+ taskStatus = 1,
+ startPoint = startPoint,
+ endPoint = endPoint,
+ palletBarcode = palletBarcode,
+ materialCode = materialCode,
+ taskSteps = details.Count,
+ taskDetails = details,
+ };
+ }
+
+ #region 内部方法
+
+ /// BFS 最短路径
+ private static List FindPath(int b_s, int f_s, int b_e, int f_e)
+ {
+ var start = new TopoNode(b_s, f_s);
+ var end = new TopoNode(b_e, f_e);
+ if (start == end) return new List { start };
+
+ // 校验起止节点是否在拓扑中
+ if (!Topology.ContainsKey(start))
+ throw new InvalidOperationException($"起始位置不在拓扑中: {b_s}#_{f_s}F,可用节点: {string.Join(", ", Topology.Keys.Select(n => $"{n.Building}#_{n.Floor}F"))}");
+ if (!Topology.ContainsKey(end))
+ throw new InvalidOperationException($"目标位置不在拓扑中: {b_e}#_{f_e}F,可用节点: {string.Join(", ", Topology.Keys.Select(n => $"{n.Building}#_{n.Floor}F"))}");
+
+ var visited = new HashSet { start };
+ var queue = new Queue>();
+ queue.Enqueue(new List { start });
+
+ while (queue.Count > 0)
+ {
+ var path = queue.Dequeue();
+ var current = path[^1];
+
+ if (!Topology.TryGetValue(current, out var neighbors)) continue;
+
+ foreach (var next in neighbors)
+ {
+ if (visited.Contains(next)) continue;
+ var newPath = new List(path) { next };
+ if (next == end) return newPath;
+ visited.Add(next);
+ queue.Enqueue(newPath);
+ }
+ }
+
+ throw new InvalidOperationException($"无路径: ({b_s},{f_s}) → ({b_e},{f_e})");
+ }
+
+ /// 是否需要廊桥交接
+ private static bool IsBridgeRequired(int b1, int b2) =>
+ (b1 == 13 && b2 == 14) || (b1 == 14 && b2 == 13);
+
+ /// 拼接位置编码: 楼栋#_L楼层_位置号
+ private static string PosCode(int building, int floor, string location) =>
+ $"{building}#_L{floor}_{location}";
+
+ /// 提升机接驳位
+ private static string HoistPoint(int building, int floor) =>
+ $"{building}#_L{floor}_HOIST";
+
+ /// 普通接驳位(非提升机、非廊桥)
+ private static string DockingPoint(int building, int floor) =>
+ $"{building}#_L{floor}_01";
+
+ /// 生成任务编号
+ private static string GenerateTaskCode() =>
+ DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(10, 99);
+
+ /// 创建一条 LiveTaskDetail
+ private static LiveTaskDetail NewDetail(
+ string taskCode, string palletBarcode, string materialCode,
+ int taskType, int taskCategory, int seq,
+ string startPoint, string endPoint, int deviceType) =>
+ new()
+ {
+ taskCode = taskCode,
+ objId = seq,
+ palletBarcode = palletBarcode,
+ materialCode = materialCode,
+ startPoint = startPoint,
+ endPoint = endPoint,
+ deviceType = deviceType,
+ taskType = taskType,
+ taskCategory = taskCategory,
+ taskStatus = 1,
+ };
+
+ #endregion
+}
diff --git a/Sln.Wcs.UI/App.axaml.cs b/Sln.Wcs.UI/App.axaml.cs
index b32f224..30d2fc9 100644
--- a/Sln.Wcs.UI/App.axaml.cs
+++ b/Sln.Wcs.UI/App.axaml.cs
@@ -58,6 +58,7 @@ public partial class App : Application
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Common.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Cache.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Repository.dll")),
+ Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Business.dll")),
};
services.Scan(scan => scan.FromAssemblies(assemblies)
@@ -92,6 +93,7 @@ public partial class App : Application
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
// 引擎进程管理
diff --git a/Sln.Wcs.UI/Sln.Wcs.UI.csproj b/Sln.Wcs.UI/Sln.Wcs.UI.csproj
index 3d45452..878a35f 100644
--- a/Sln.Wcs.UI/Sln.Wcs.UI.csproj
+++ b/Sln.Wcs.UI/Sln.Wcs.UI.csproj
@@ -36,6 +36,7 @@
+
diff --git a/Sln.Wcs.UI/ViewModels/NavigationViewModel.cs b/Sln.Wcs.UI/ViewModels/NavigationViewModel.cs
index f2f6ee7..d9efab9 100644
--- a/Sln.Wcs.UI/ViewModels/NavigationViewModel.cs
+++ b/Sln.Wcs.UI/ViewModels/NavigationViewModel.cs
@@ -59,6 +59,7 @@ public partial class NavigationViewModel : ObservableObject
}));
TopMenuItems.Add(new TopMenuItem("任务管理", new List
{
+ new("创建任务", () => NavigateTo("任务管理")),
new("任务队列", () => NavigateTo("任务管理")),
new("任务明细", () => NavigateTo("任务管理")),
}));
diff --git a/Sln.Wcs.UI/ViewModels/Task/CreateTaskViewModel.cs b/Sln.Wcs.UI/ViewModels/Task/CreateTaskViewModel.cs
new file mode 100644
index 0000000..17e1af6
--- /dev/null
+++ b/Sln.Wcs.UI/ViewModels/Task/CreateTaskViewModel.cs
@@ -0,0 +1,82 @@
+using Avalonia.Controls;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Sln.Wcs.Business;
+using Sln.Wcs.Repository.service;
+
+namespace Sln.Wcs.UI.ViewModels.Task;
+
+public partial class CreateTaskViewModel : ObservableObject
+{
+ private readonly TaskCreateService _taskCreateService;
+ private readonly ILiveTaskQueueService _taskQueueService;
+
+ public string PageTitle => "手动创建任务";
+
+ // ---- 起点 ----
+ [ObservableProperty] private int _startBuilding = 13;
+ [ObservableProperty] private int _startFloor = 1;
+ [ObservableProperty] private string _startLocation = "01";
+
+ // ---- 终点 ----
+ [ObservableProperty] private int _endBuilding = 15;
+ [ObservableProperty] private int _endFloor = 5;
+ [ObservableProperty] private string _endLocation = "01";
+
+ // ---- 任务属性 ----
+ [ObservableProperty] private int _taskType = 1; // 1-入库 2-出库
+ [ObservableProperty] private int _taskCategory = 1; // 1-包材 2-成品 3-托盘
+ [ObservableProperty] private string _palletBarcode = string.Empty;
+ [ObservableProperty] private string _materialCode = string.Empty;
+
+ // ---- 状态 ----
+ [ObservableProperty] private string _statusText = string.Empty;
+ [ObservableProperty] private bool _isBusy;
+
+ public CreateTaskViewModel(TaskCreateService taskCreateService, ILiveTaskQueueService taskQueueService)
+ {
+ _taskCreateService = taskCreateService;
+ _taskQueueService = taskQueueService;
+ }
+
+ public Avalonia.Controls.Control CreateView() => new Views.Task.CreateTaskView();
+
+ [RelayCommand]
+ private async System.Threading.Tasks.Task CreateAsync()
+ {
+ if (string.IsNullOrWhiteSpace(PalletBarcode) || string.IsNullOrWhiteSpace(MaterialCode))
+ {
+ StatusText = "托盘条码和物料编码不能为空";
+ return;
+ }
+
+ IsBusy = true;
+ StatusText = "正在生成任务...";
+
+ try
+ {
+ var taskQueue = await System.Threading.Tasks.Task.Run(() =>
+ _taskCreateService.CreateTask(
+ StartBuilding, StartFloor, StartLocation,
+ EndBuilding, EndFloor, EndLocation,
+ TaskType, TaskCategory,
+ PalletBarcode, MaterialCode));
+
+ StatusText = $"路由生成完成,共 {taskQueue.taskSteps} 条明细,正在保存...";
+
+ var saved = await System.Threading.Tasks.Task.Run(() => _taskQueueService.InsertTaskQueue(taskQueue));
+
+ StatusText = saved
+ ? $"✓ 任务创建成功: {taskQueue.taskCode},共 {taskQueue.taskSteps} 条明细"
+ : "✗ 保存失败";
+ }
+ catch (Exception ex)
+ {
+ StatusText = $"✗ 创建失败: {ex.Message}";
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Task/CreateTaskView.axaml b/Sln.Wcs.UI/Views/Task/CreateTaskView.axaml
new file mode 100644
index 0000000..3d58872
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Task/CreateTaskView.axaml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Task/CreateTaskView.axaml.cs b/Sln.Wcs.UI/Views/Task/CreateTaskView.axaml.cs
new file mode 100644
index 0000000..8d2a9b4
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Task/CreateTaskView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace Sln.Wcs.UI.Views.Task;
+
+public partial class CreateTaskView : UserControl
+{
+ public CreateTaskView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/接驳位驱动调度方案.md b/接驳位驱动调度方案.md
new file mode 100644
index 0000000..8baa787
--- /dev/null
+++ b/接驳位驱动调度方案.md
@@ -0,0 +1,390 @@
+# 接驳位驱动的任务调度方案
+
+## 一、核心概念
+
+| 概念 | 物理含义 | 数据对应 |
+|------|---------|---------|
+| 接驳位 | 货物放置/取走的物理位置 | `LiveTaskDetail.startPoint / endPoint`,设备参数中 `接驳位状态`(0空闲/1占用) |
+| RFID 条码 | 托盘上的电子标签 | `LiveTaskDetail.palletBarcode` |
+| 任务 | 一个完整的搬运工单 | `LiveTaskQueue` |
+| 子任务步骤 | 任务中的一段搬运步骤 | `LiveTaskDetail`(`objId` 决定先后顺序) |
+
+### 任务与子任务的关系
+
+一个 `LiveTaskQueue` 包含多个 `LiveTaskDetail`,按 `objId` 从小到大串行执行。前一步的 `endPoint` 等于下一步的 `startPoint`,通过接驳位串联:
+
+```
+LiveTaskQueue (任务, taskCode = "T001")
+ │
+ ├─ LiveTaskDetail objId=1: AGV 13#_L1_01 → BRIDGE_13_14 (子任务步骤1)
+ ├─ LiveTaskDetail objId=2: AGV BRIDGE_13_14 → 14#_L1_HOIST (子任务步骤2)
+ ├─ LiveTaskDetail objId=3: 提升机 14#_L1_HOIST → 14#_L2_HOIST (子任务步骤3)
+ ├─ LiveTaskDetail objId=4: AGV 14#_L2_HOIST → 15#_L2_HOIST (子任务步骤4)
+ ├─ LiveTaskDetail objId=5: 提升机 15#_L2_HOIST → 15#_L4_HOIST (子任务步骤5)
+ └─ LiveTaskDetail objId=6: AGV 15#_L4_HOIST → 15#_L4_03 (子任务步骤6)
+```
+
+### 数据库关键字段
+
+- **LiveTaskQueue**: `taskCode`, `taskType`(1入库/2出库), `taskCategory`(1包材/2成品/3托盘), `taskStatus`(1待执行/2执行中/3已完成), `startPoint`, `endPoint`
+- **LiveTaskDetail**: `taskCode`, `objId`, `palletBarcode`, `startPoint`, `endPoint`, `deviceType`(0输送线/1AGV/2提升机), `taskStatus`
+- **BaseDeviceParam**: `deviceCode`, `paramName`(如"接驳位状态"), `paramValue`(0空闲/1占用)
+
+---
+
+## 二、现场拓扑
+
+### 楼栋与设备
+
+```
+ 廊桥(2F)
+ ┌───────────┐
+ │ AGV交接 │
+ └─────┬─────┘
+ │
+ 13#(1F) ──13#提升机── 13#(2F)─┘ 14#(2F) ──AGV── 15#(2F) ──15#提升机── 15#(1F~5F)
+ (1F↔2F) │
+ 14#提升机(1F↔2F)
+ │
+ 14#(1F)
+```
+
+### AGV 运行区域
+
+| 区域 | 行驶范围 | 说明 |
+|------|---------|------|
+| 13# AGV | 13# 栋 1F~2F | 不能驶入 14# |
+| 14# AGV | 14# 栋 1F~2F + 至 15# 2F | 负责中间段搬运 |
+| 15# AGV | 仅在 15# 栋内 | 不能驶入 14# |
+
+### 核心约束
+
+- **AGV 不能跨栋**:13# 和 14# 之间在 2F 有廊桥交接点,两边 AGV 在此完成货物交换
+- **所有楼栋均为 1~5F**,各栋有 1 台提升机连通所有楼层
+- **跨栋必须经 2F**:不管起点在几楼,跨栋必须经 2F 的廊桥(13↔14)或 AGV 通道(14↔15)
+
+### 接驳位点表
+
+| 接驳位 | 编码 | 所属 AGV |
+|--------|------|---------|
+| 13# 各层普通点 | `13#_L{f}_{n}` | 13# AGV |
+| 13# 提升机入口/出口 | `13#_L{f}_HOIST` | 13# AGV |
+| 廊桥交接点(2F) | `BRIDGE_13_14` | 13# AGV ↔ 14# AGV 交接 |
+| 14# 各层普通点 | `14#_L{f}_{n}` | 14# AGV |
+| 14# 提升机入口/出口 | `14#_L{f}_HOIST` | 14# AGV |
+| 15# 提升机 2F 入口 | `15#_L2_HOIST` | 14# AGV ↔ 15# AGV |
+| 15# 提升机各层出口 | `15#_L{f}_HOIST` | 15# AGV |
+| 15# 各层普通点 | `15#_L{f}_{n}` | 15# AGV |
+
+### 拓扑连通图
+
+```
+节点: (building, floor)
+
+边 (提升机, 同栋跨层):
+ (13,1) ←→ (13,2)
+ (14,1) ←→ (14,2)
+ (15,2) ←→ (15,1)
+ (15,2) ←→ (15,3)
+ (15,2) ←→ (15,4)
+ (15,2) ←→ (15,5)
+
+边 (AGV, 同层跨栋):
+ (13,2) ←→ (14,2) (廊桥交接)
+ (14,2) ←→ (15,2) (AGV直连)
+```
+
+---
+
+## 三、任务创建方案 —— 路径分解与明细生成
+
+### 输入
+
+```
+起始楼栋: startBuilding (13/14/15)
+起始楼层: startFloor (1~5)
+起始位置: startLocation (位置号,如 "01")
+结束楼栋: endBuilding (13/14/15)
+结束楼层: endFloor (1~5)
+结束位置: endLocation (位置号,如 "02")
+```
+
+系统自动拼接为 `{building}#_L{floor}_{location}` 格式。
+
+### 输出
+
+返回一个完整的 `LiveTaskQueue`,包含按 `objId` 排序的 `List`。
+
+### 生成算法
+
+```
+输入: (b_s, f_s, loc_s), (b_e, f_e, loc_e)
+
+Step 1 — BFS 最短路径
+ 在拓扑图中找 (b_s, f_s) → ... → (b_e, f_e) 的节点序列
+
+Step 2 — 沿节点路径逐段生成明细
+ currentPos = 实际起点
+
+ for each 相邻节点 (b1,f1) → (b2,f2):
+
+ 跨栋 (b1 != b2):
+ if 13↔14 (廊桥交接):
+ 生成 [AGV] currentPos → BRIDGE_13_14
+ 生成 [AGV] BRIDGE_13_14 → 接驳位(b2,f2)
+ 注: 廊桥在 2F,两边都须先经提升机到达 2F
+ else (14↔15, AGV直连):
+ 生成 [AGV] currentPos → 接驳位(b2,f2)
+ currentPos = 接驳位(b2,f2)
+
+ 同栋跨层 (b1 == b2, f1 != f2):
+ 生成 [AGV] currentPos → 提升机入口(b,f1)
+ 生成 [提升机] 提升机入口(b,f1) → 提升机出口(b,f2)
+ currentPos = 提升机出口(b,f2)
+
+Step 3 — 最终送达
+ if currentPos != 实际终点:
+ 生成 [AGV] currentPos → 实际终点
+```
+
+ ┌─ 同栋同层 (b1 == b2 && f1 == f2) ──────────────┐
+ │
+ │ 跳过(不会出现在最短路径中)
+ │
+ └────────────────────────────────────────────────┘
+
+Step 3 — 替换首尾实际坐标
+
+ 首条 Detail.startPoint → pt_s
+ 尾条 Detail.endPoint → pt_e
+ 中间节点起点/终点 → 使用对应的接驳位编码
+```
+
+### 各场景示例(修正后——廊桥在 2F)
+
+| 场景 | 输入 | 生成 Detail 序列 |
+|------|------|-----------------|
+| **同栋同层** | 14,1,"01" → 14,1,"05" | `[1] AGV: 14#_L1_01 → 14#_L1_05` |
+| **同栋跨层** | 14,1,"01" → 14,2,"03" | `[1] AGV: 14#_L1_01 → 14#_L1_HOIST` `[2] 提升机: 14#_L1_HOIST → 14#_L2_HOIST` `[3] AGV: 14#_L2_HOIST → 14#_L2_03` |
+| **13→14 (均为1F)** | 13,1,"01" → 14,1,"03" | `[1] AGV: 13#_L1_01 → 13#_L1_HOIST` `[2] 提升机: 13#_L1_HOIST → 13#_L2_HOIST` `[3] AGV: 13#_L2_HOIST → BRIDGE_13_14` `[4] AGV: BRIDGE_13_14 → 14#_L2_01` `[5] AGV: 14#_L2_01 → 14#_L2_HOIST` `[6] 提升机: 14#_L2_HOIST → 14#_L1_HOIST` `[7] AGV: 14#_L1_HOIST → 14#_L1_03` |
+| **13→14 (目标2F)** | 13,1,"01" → 14,2,"05" | `[1] AGV: 13#_L1_01 → 13#_L1_HOIST` `[2] 提升机: 13#_L1_HOIST → 13#_L2_HOIST` `[3] AGV: 13#_L2_HOIST → BRIDGE_13_14` `[4] AGV: BRIDGE_13_14 → 14#_L2_01` `[5] AGV: 14#_L2_01 → 14#_L2_05` |
+| **14→15** | 14,1,"01" → 15,4,"02" | `[1] AGV: 14#_L1_01 → 14#_L1_HOIST` `[2] 提升机: 14#_L1_HOIST → 14#_L2_HOIST` `[3] AGV: 14#_L2_HOIST → 15#_L2_01` `[4] AGV: 15#_L2_01 → 15#_L2_HOIST` `[5] 提升机: 15#_L2_HOIST → 15#_L4_HOIST` `[6] AGV: 15#_L4_HOIST → 15#_L4_02` |
+| **13→15 完整链路** | 13,1,"01" → 15,4,"02" | `[1] AGV: 13#_L1_01 → 13#_L1_HOIST` `[2] 提升机: 13#_L1_HOIST → 13#_L2_HOIST` `[3] AGV: 13#_L2_HOIST → BRIDGE_13_14` `[4] AGV: BRIDGE_13_14 → 14#_L2_01` `[5] AGV: 14#_L2_01 → 15#_L2_01` `[6] AGV: 15#_L2_01 → 15#_L2_HOIST` `[7] 提升机: 15#_L2_HOIST → 15#_L4_HOIST` `[8] AGV: 15#_L4_HOIST → 15#_L4_02` |
+| **14→15 2F直连** | 14,2,"01" → 15,2,"03" | `[1] AGV: 14#_L2_01 → 15#_L2_03` |
+
+### 伪代码
+
+```
+LiveTaskQueue CreateTask(
+ int b_s, int f_s, string loc_s,
+ int b_e, int f_e, string loc_e,
+ int taskType, int taskCategory, string palletBarcode, string materialCode)
+{
+ var startPoint = $"{b_s}#_L{f_s}_{loc_s}";
+ var endPoint = $"{b_e}#_L{f_e}_{loc_e}";
+ var nodes = BFS(b_s, f_s, b_e, f_e); // 拓扑最短路径
+ string currentPos = startPoint;
+
+ for (int i = 0; i < nodes.Count - 1; i++)
+ {
+ var (b1, f1) = nodes[i];
+ var (b2, f2) = nodes[i + 1];
+
+ if (b1 != b2) // 跨栋
+ {
+ if (13↔14)
+ {
+ Add [AGV] currentPos → BRIDGE_13_14;
+ Add [AGV] BRIDGE_13_14 → DockingPoint(b2,f2);
+ }
+ else // 14↔15
+ {
+ Add [AGV] currentPos → DockingPoint(b2,f2);
+ }
+ currentPos = DockingPoint(b2,f2);
+ }
+ else if (f1 != f2) // 同栋跨层
+ {
+ var entry = $"{b1}#_L{f1}_HOIST";
+ var exit = $"{b2}#_L{f2}_HOIST";
+ Add [AGV] currentPos → entry;
+ Add [提升机] entry → exit;
+ currentPos = exit;
+ }
+ }
+
+ if (details.Count == 0 || currentPos != endPoint)
+ Add [AGV] currentPos → endPoint;
+
+ return new LiveTaskQueue { ... };
+}
+### 拓扑路由表
+
+```
+FindPath(b_s, f_s, b_e, f_e):
+
+ 以 BFS 在拓扑图中搜索,典型路径:
+
+ (13,1) → (13,2): [(13,1), (13,2)]
+ (13,1) → (14,1): [(13,1), (13,2), (14,2), (14,1)]
+ (13,1) → (14,2): [(13,1), (13,2), (14,2)]
+ (13,1) → (15,2): [(13,1), (13,2), (14,2), (15,2)]
+ (13,1) → (15,4): [(13,1), (13,2), (14,2), (15,2), (15,4)]
+ (14,1) → (15,4): [(14,1), (14,2), (15,2), (15,4)]
+ (15,4) → (13,1): [(15,4), (15,2), (14,2), (13,2), (13,1)]
+ 反向: 路径取反
+```
+
+---
+
+## 四、调度触发链路
+
+```
+人工放货到接驳位
+ → PLC 传感器检测到货物 → 接驳位状态 = 1
+ → PLC 读 RFID → 得到托盘条码
+ → WCS 收到事件: (positionCode, rfidBarcode)
+ → 进入匹配调度逻辑
+```
+
+## 五、匹配调度逻辑
+
+```
+输入: positionCode (接驳位编码), rfidBarcode (RFID条码)
+
+Step 1 ─ 匹配明细
+ 查 LiveTaskDetail WHERE
+ palletBarcode == rfidBarcode
+ AND startPoint == positionCode
+ AND taskStatus == 1 (待执行)
+
+ 未匹配 → 告警,返回
+
+ 匹配到 detail
+
+Step 2 ─ 前序依赖校验
+ 查同 taskCode 下 objId < detail.objId 的所有明细
+ 全部 taskStatus == 3 (已完成) ?
+
+ 否 → 等待(不应发生,正常前序完成才会到这一步),返回
+
+Step 3 ─ 接驳位加锁
+ 获取该接驳位的锁(信号量)
+ 二次确认 detail.taskStatus 仍为 1(防并发)
+
+Step 4 ─ 标记执行中
+ detail.taskStatus = 2
+ 若 LiveTaskQueue.taskStatus == 1 → 也改为 2
+
+Step 5 ─ 下发设备
+ detail.deviceType:
+ 1 → AGV
+ 2 → 提升机
+ 0 → 输送线
+
+Step 6 ─ 结果处理
+ 成功:
+ detail.taskStatus = 3
+ 释放接驳位 (status = 0)
+ 检查整个 task 是否全部完成 → taskStatus = 3
+
+ 失败:
+ detail.taskStatus 回退为 1
+ 下次重试
+```
+
+## 六、链条式执行(以 13#_1F → 15#_4F 为例)
+
+```
+Task: P001, 13#_L1_01 → 15#_L4_02
+ 路径节点: (13,1) → (13,2) → (14,2) → (15,2) → (15,4)
+
+ Detail 1: AGV 13#_L1_01 → 13#_L1_HOIST
+ Detail 2: 提升机 13#_L1_HOIST → 13#_L2_HOIST
+ Detail 3: AGV 13#_L2_HOIST → BRIDGE_13_14
+ Detail 4: AGV BRIDGE_13_14 → 14#_L2_01
+ Detail 5: AGV 14#_L2_01 → 15#_L2_01
+ Detail 6: AGV 15#_L2_01 → 15#_L2_HOIST
+ Detail 7: 提升机 15#_L2_HOIST → 15#_L4_HOIST
+ Detail 8: AGV 15#_L4_HOIST → 15#_L4_02
+
+════════════════════ 时间线 ════════════════════
+
+ T0: 人工放货到 13#_L1_01,读 RFID
+ → 匹配 Detail 1,无前序 ✓ → 13# AGV 取走
+ → 13#_L1_01 释放
+
+ T1: 13# AGV 运到 13#_L1_HOIST,放下
+ → 匹配 Detail 2,前序 1 ✓
+ → 13# 提升机启动,13#_L1_HOIST 释放
+
+ T2: 提升机运到 2F,到达 13#_L2_HOIST
+ → 匹配 Detail 3,前序 1,2 ✓
+ → 13# AGV 取走,运往廊桥
+
+ T3: 13# AGV 到达 BRIDGE_13_14 (2F廊桥)
+ → 匹配 Detail 4,前序 1~3 ✓
+ → 14# AGV 取走,BRIDGE_13_14 释放
+
+ T4: 14# AGV 运到 14#_L2_01,放下
+ → 匹配 Detail 5,前序 1~4 ✓
+ → 14# AGV 运往 15# (同层2F,直连)
+
+ T5: 14# AGV 到达 15#_L2_01,放下
+ → 匹配 Detail 6,前序 1~5 ✓
+ → 15# AGV 转运到提升机入口
+
+ T6: 15# AGV 到达 15#_L2_HOIST,放下
+ → 匹配 Detail 7,前序 1~6 ✓
+ → 15# 提升机启动
+
+ T7: 提升机运到 4F,到达 15#_L4_HOIST
+ → 匹配 Detail 8,前序 1~7 ✓
+ → 15# AGV 取走,运到 15#_L4_02
+
+ 任务 P001 全部完成 ✓
+```
+
+## 七、并发模型
+
+```
+接驳位 A ─┐
+接驳位 B ─┤ 各自独立并行(不同锁保护)
+接驳位 C ─┘
+
+同一接驳位内 → 串行(同一把锁保护)
+```
+
+不同任务在不同接驳位上可以同时执行,互不干扰。
+
+## 八、异常场景
+
+| 场景 | 处理 |
+|------|------|
+| RFID + 位置匹配不到待执行明细 | 货物放错位置或任务未创建 → 告警 |
+| 前序明细未完成但货物已到 | 等待 |
+| 设备下发失败 | detail 状态回退为 1,下次循环重试 |
+| 同一明细被并发处理 | Step 3 二次确认防御 |
+| 接驳位长时间占用不释放 | 需超时告警机制(后续) |
+| 全部明细完成 | task 标记为已完成(status = 3) |
+
+## 九、依赖设备信息
+
+### 设备类型 (deviceType)
+
+| 值 | 类型 | 对应服务 |
+|----|------|---------|
+| 0 | 输送线 | PLC 直接控制 |
+| 1 | AGV | HikRoBotServer (端口 5200) |
+| 2 | 提升机 | HoistServer (端口 5100) |
+
+### 客户端 API
+
+**HoistServer** (端口 5100):
+- `/api/hoist/receive-pallet` - 接收托盘
+- `/api/hoist/task-run` - 提升机启动
+- `/api/task/dispatch` - 下发调度任务
+- `/api/hoist/free` - 获取空闲提升机
+
+**HikRoBotServer** (端口 5200):
+- `/api/task/receive` - 下发 AGV 任务