From 7da743e2bd2609cc6616878c72f2ae1e05d2b94a Mon Sep 17 00:00:00 2001 From: WenJY Date: Sat, 13 Jun 2026 11:32:38 +0800 Subject: [PATCH] =?UTF-8?q?add=20-=20=E6=89=8B=E5=8A=A8=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=94=9F=E6=88=90=E6=96=B9=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- Sln.Wcs.Business/TaskCreateService.cs | 227 ++++++++++ Sln.Wcs.UI/App.axaml.cs | 2 + Sln.Wcs.UI/Sln.Wcs.UI.csproj | 1 + Sln.Wcs.UI/ViewModels/NavigationViewModel.cs | 1 + .../ViewModels/Task/CreateTaskViewModel.cs | 82 ++++ Sln.Wcs.UI/Views/Task/CreateTaskView.axaml | 110 +++++ Sln.Wcs.UI/Views/Task/CreateTaskView.axaml.cs | 11 + 接驳位驱动调度方案.md | 390 ++++++++++++++++++ 9 files changed, 826 insertions(+), 1 deletion(-) create mode 100644 Sln.Wcs.Business/TaskCreateService.cs create mode 100644 Sln.Wcs.UI/ViewModels/Task/CreateTaskViewModel.cs create mode 100644 Sln.Wcs.UI/Views/Task/CreateTaskView.axaml create mode 100644 Sln.Wcs.UI/Views/Task/CreateTaskView.axaml.cs create mode 100644 接驳位驱动调度方案.md 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +