change - 包材入库调度逻辑模拟实现

dev
WenJY 21 hours ago
parent 79be7d4e8a
commit fc74a33327

@ -142,6 +142,7 @@ public class TaskCreateService
materialCode = materialCode,
taskSteps = details.Count,
taskDetails = details,
isFlag = 1
};
}
@ -221,6 +222,7 @@ public class TaskCreateService
taskType = taskType,
taskCategory = taskCategory,
taskStatus = 1,
isFlag = 1
};
#endregion

@ -31,16 +31,14 @@ namespace Sln.Wcs.Repository
{
public class Repository<T> : SimpleClient<T> where T : class, new()
{
public ITenant itenant = null; //多租户事务、GetConnection、IsAnyConnection等功能
public ITenant itenant = null;
public Repository(ISqlSugarClient db)
{
itenant = db.AsTenant(); //用来处理事务
base.Context = db.AsTenant().GetConnectionScopeWithAttr<T>(); //获取子Db
//base.Context = db.AsTenant().GetConnectionScope("core");
//如果不想通过注入多个仓储
//用到ChangeRepository或者Db.GetMyRepository需要看标题4写法
itenant = db.AsTenant();
// 直接用 SqlSugarScope 作为 Context它内部管理连接池、天然线程安全
// SqlSugarScope 根据 [Tenant] 特性自动路由到正确的数据库
Context = (SqlSugarClient)db;
}
}
}

@ -0,0 +1,202 @@
using System.Collections.Concurrent;
using Sln.Wcs.Model.Domain;
using Sln.Wcs.Repository.service;
using Sln.Wcs.Serilog;
namespace Sln.Wcs.Strategy;
/// <summary>
/// 包材入库任务执行器 (taskCategory=1, taskType=1)
/// 轮询待执行任务 → 多任务并行、单任务内串行逐条下发明细
/// </summary>
public class MaterialInStoreExecutor
{
private readonly SerilogHelper _logger;
private readonly ILiveTaskQueueService _taskQueueService;
private readonly ILiveTaskDetailService _taskDetailService;
// 每栋楼提升机限 2 台并发AGV 不限
private static readonly ConcurrentDictionary<int, SemaphoreSlim> HoistSemaphores = new();
// DB 写操作互斥锁SqlSugar Context 共享导致写入不能并发)
private static readonly object DbWriteLock = new();
// 最大同时执行任务数
private static readonly SemaphoreSlim TaskSemaphore = new(10, 10);
private readonly object _lock = new();
private CancellationTokenSource? _cts;
private volatile bool _isRunning;
public bool IsRunning => _isRunning;
public MaterialInStoreExecutor(
SerilogHelper logger,
ILiveTaskQueueService taskQueueService,
ILiveTaskDetailService taskDetailService)
{
_logger = logger;
_taskQueueService = taskQueueService;
_taskDetailService = taskDetailService;
}
public void Start()
{
lock (_lock)
{
if (_isRunning) return;
_cts = new CancellationTokenSource();
_isRunning = true;
}
Task.Run(() => RunLoopAsync(_cts!.Token));
_logger.Info("包材入库调度已启动");
}
public void Stop()
{
CancellationTokenSource? cts;
lock (_lock)
{
if (!_isRunning) return;
cts = _cts;
_isRunning = false;
}
cts?.Cancel();
_logger.Info("包材入库调度已停止");
}
private async Task RunLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try { await Task.Run(() => ExecuteAsync(ct), ct); }
catch (OperationCanceledException) { break; }
catch (Exception ex) { _logger.Error($"调度异常: {ex.Message}"); }
try { await Task.Delay(5000, ct); }
catch (OperationCanceledException) { break; }
}
}
private void ExecuteAsync(CancellationToken ct)
{
var tasks = _taskQueueService.getLiveTaskQueues(x =>
x.taskType == 1 && x.taskCategory == 1 && x.taskStatus == 1);
if (tasks.Count == 0) return;
_logger.Info($"查询到 {tasks.Count} 条待执行包材入库任务");
var threads = new List<Thread>();
foreach (var task in tasks)
{
var captured = task;
var t = new Thread(() =>
{
TaskSemaphore.Wait();
try
{
if (ct.IsCancellationRequested) return;
_logger.Info($"[线程启动] {captured.taskCode}");
ProcessOneAsync(captured, ct).GetAwaiter().GetResult();
}
catch (Exception ex) { _logger.Error($"任务 {captured.taskCode} 线程异常: {ex.Message}"); }
finally { TaskSemaphore.Release(); }
})
{ IsBackground = true };
t.Start();
threads.Add(t);
}
foreach (var t in threads)
t.Join();
}
private async Task ProcessOneAsync(LiveTaskQueue task, CancellationToken ct)
{
_logger.Info($"开始执行 {task.taskCode},共 {task.taskDetails.Count} 条明细");
lock (DbWriteLock) { task.taskStatus = 2; _taskQueueService.Update(task); }
foreach (var detail in task.taskDetails.OrderBy(d => d.objId))
{
if (ct.IsCancellationRequested) return;
if (detail.taskStatus is 2 or 3)
{
_logger.Info($" 明细 {detail.objId} 已处理,跳过");
continue;
}
lock (DbWriteLock) { detail.taskStatus = 2; _taskDetailService.Update(detail); }
var devName = detail.deviceType switch { 1 => "AGV", 2 => "提升机", _ => "输送线" };
_logger.Info($" → {devName}下发 {detail.taskCode}-{detail.objId}: {detail.startPoint} → {detail.endPoint}");
// 模拟执行在锁外,多任务并行不受影响
var ok = detail.deviceType switch
{
1 => await DispatchAgvAsync(detail),
2 => await DispatchHoistAsync(detail),
_ => await DispatchConveyorAsync(detail)
};
if (ok)
{
lock (DbWriteLock) { detail.taskStatus = 3; _taskDetailService.Update(detail); }
_logger.Info($" ✓ {detail.objId} 完成");
}
else
{
lock (DbWriteLock) { detail.taskStatus = 1; _taskDetailService.Update(detail); }
_logger.Error($" ✗ {detail.objId} 失败,中断任务");
return;
}
}
lock (DbWriteLock) { task.taskStatus = 3; _taskQueueService.Update(task); }
_logger.Info($"任务 {task.taskCode} 执行完成");
}
private async Task<bool> DispatchAgvAsync(LiveTaskDetail detail)
{
// AGV 不限并发,直接下发
_logger.Info($" [AGV] 开始 {detail.startPoint} → {detail.endPoint}");
await Task.Delay(10_000); // 模拟执行 10s
_logger.Info($" [AGV] 完成 {detail.startPoint} → {detail.endPoint}");
return true;
}
private async Task<bool> DispatchHoistAsync(LiveTaskDetail detail)
{
// 每栋楼提升机限 2 台并发
var building = ExtractBuilding(detail.startPoint);
var semaphore = HoistSemaphores.GetOrAdd(building, _ => new SemaphoreSlim(2, 2));
await semaphore.WaitAsync();
try
{
_logger.Info($" [提升机] {building}#楼 开始 (可用:{semaphore.CurrentCount}) {detail.startPoint} → {detail.endPoint}");
await Task.Delay(20_000); // 模拟执行 20s
_logger.Info($" [提升机] {building}#楼 完成 {detail.startPoint} → {detail.endPoint}");
return true;
}
finally
{
semaphore.Release();
}
}
private Task<bool> DispatchConveyorAsync(LiveTaskDetail detail)
{
_logger.Info($" [输送线] {detail.startPoint} → {detail.endPoint}");
return Task.FromResult(true);
}
/// <summary>从接驳位编码中提取楼栋号,如 "13#_L1_HOIST" → 13</summary>
private static int ExtractBuilding(string pointCode)
{
var match = System.Text.RegularExpressions.Regex.Match(pointCode, @"^(\d+)");
return match.Success ? int.Parse(match.Groups[1].Value) : 0;
}
}

@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
using NeoSmart.Caching.Sqlite;
using Sln.Wcs.Repository;
using Sln.Wcs.Serilog;
using Sln.Wcs.Strategy;
using Sln.Wcs.UI.Services;
using Sln.Wcs.UI.ViewModels;
using Sln.Wcs.UI.Views;
@ -59,6 +60,7 @@ public partial class App : Application
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")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Strategy.dll")),
};
services.Scan(scan => scan.FromAssemblies(assemblies)
@ -99,6 +101,7 @@ public partial class App : Application
// 引擎进程管理
services.AddSingleton<IEngineProcessService, EngineProcessService>();
services.AddSingleton<IAgvEngineProcessService, AgvEngineProcessService>();
services.AddSingleton<MaterialInStoreExecutor>();
var serviceProvider = services.BuildServiceProvider();

@ -37,6 +37,7 @@
<ProjectReference Include="..\Sln.Wcs.Model\Sln.Wcs.Model.csproj" />
<ProjectReference Include="..\Sln.Wcs.Repository\Sln.Wcs.Repository.csproj" />
<ProjectReference Include="..\Sln.Wcs.Business\Sln.Wcs.Business.csproj" />
<ProjectReference Include="..\Sln.Wcs.Strategy\Sln.Wcs.Strategy.csproj" />
<ProjectReference Include="..\Sln.Wcs.Serilog\Sln.Wcs.Serilog.csproj" />
<ProjectReference Include="..\Sln.Wcs.HoistServer\Sln.Wcs.HoistServer.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>

@ -4,6 +4,7 @@ using System.IO;
using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Sln.Wcs.Strategy;
using Sln.Wcs.UI.Services;
namespace Sln.Wcs.UI.ViewModels;
@ -12,6 +13,7 @@ public partial class SystemMonitorViewModel : ObservableObject
{
private readonly IEngineProcessService _hoistEngine;
private readonly IAgvEngineProcessService _agvEngine;
private readonly MaterialInStoreExecutor _dispatchExecutor;
[ObservableProperty]
private ObservableCollection<LogEntry> _logs = new();
@ -22,6 +24,11 @@ public partial class SystemMonitorViewModel : ObservableObject
[ObservableProperty]
private bool _autoScroll = true;
// ---- Dispatch ----
[ObservableProperty] private bool _isDispatchRunning;
[ObservableProperty] private string _dispatchStatusText = "已停止";
public string DispatchStatusColor => IsDispatchRunning ? "#00E676" : "#FF5252";
// ---- Hoist ----
[ObservableProperty] private bool _isHoistRunning;
[ObservableProperty] private string _hoistStatusText = "已停止";
@ -34,16 +41,22 @@ public partial class SystemMonitorViewModel : ObservableObject
private const int MaxLogs = 5000;
public SystemMonitorViewModel(IEngineProcessService hoistEngine, IAgvEngineProcessService agvEngine)
public SystemMonitorViewModel(
IEngineProcessService hoistEngine,
IAgvEngineProcessService agvEngine,
MaterialInStoreExecutor dispatchExecutor)
{
_hoistEngine = hoistEngine;
_agvEngine = agvEngine;
_dispatchExecutor = dispatchExecutor;
_hoistEngine.StateChanged += () => RefreshHoist();
_hoistEngine.OutputReceived += msg => AddLog($"[提升机] {msg}");
_agvEngine.StateChanged += () => RefreshAgv();
_agvEngine.OutputReceived += msg => AddLog($"[AGV] {msg}");
StartDispatchCommand = new RelayCommand(StartDispatch, () => !IsDispatchRunning);
StopDispatchCommand = new RelayCommand(StopDispatch, () => IsDispatchRunning);
StartHoistCommand = new AsyncRelayCommand(StartHoistAsync, () => !IsHoistRunning);
StopHoistCommand = new RelayCommand(StopHoist, () => IsHoistRunning);
StartAgvCommand = new AsyncRelayCommand(StartAgvAsync, () => !IsAgvRunning);
@ -108,6 +121,32 @@ public partial class SystemMonitorViewModel : ObservableObject
}
private void StopAgv() { _agvEngine.Stop(); AddLog("HikRoBotServer 已停止"); }
// ---- Dispatch Commands ----
public IRelayCommand StartDispatchCommand { get; }
public IRelayCommand StopDispatchCommand { get; }
private void StartDispatch()
{
_dispatchExecutor.Start();
RefreshDispatch();
}
private void StopDispatch()
{
_dispatchExecutor.Stop();
RefreshDispatch();
}
private void RefreshDispatch()
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
IsDispatchRunning = _dispatchExecutor.IsRunning;
DispatchStatusText = _dispatchExecutor.IsRunning ? "运行中" : "已停止";
OnPropertyChanged(nameof(DispatchStatusColor));
StartDispatchCommand.NotifyCanExecuteChanged();
StopDispatchCommand.NotifyCanExecuteChanged();
});
}
private void AddLog(string msg, string level = "INFO")
{
Logs.Add(new LogEntry { Time = DateTime.Now, Message = msg, Level = level });

@ -1,10 +1,28 @@
<UserControl x:CompileBindings="False" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sln.Wcs.UI.Views.SystemMonitorView">
<Grid RowDefinitions="Auto,Auto,Auto,*" Margin="20,14,20,14" VerticalAlignment="Stretch">
<!-- Hoist Engine -->
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*" Margin="20,14,20,14" VerticalAlignment="Stretch">
<!-- Task Dispatch -->
<Border Grid.Row="0" Background="{DynamicResource CardBgBrush}" Padding="10,8" CornerRadius="6"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Margin="0,0,0,8">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center" Margin="0,0,12,0">
<Ellipse Width="10" Height="10" Fill="{Binding DispatchStatusColor}" VerticalAlignment="Center" />
<TextBlock Text="包材入库调度" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryTextBrush}" VerticalAlignment="Center" />
</StackPanel>
<Border Grid.Column="1" Background="{DynamicResource PrimaryBgBrush}" CornerRadius="3" Padding="6,3" VerticalAlignment="Center" HorizontalAlignment="Left">
<TextBlock Text="{Binding DispatchStatusText}" FontSize="11" Foreground="{DynamicResource AccentTextBrush}" />
</Border>
<Button Grid.Column="2" Content="&#x25B6; 启动" Command="{Binding StartDispatchCommand}"
Background="#1B5E20" Foreground="White" FontSize="11" Padding="12,5" Margin="0,0,6,0" />
<Button Grid.Column="3" Content="&#x25A0; 停止" Command="{Binding StopDispatchCommand}"
Background="#B71C1C" Foreground="White" FontSize="11" Padding="12,5" />
</Grid>
</Border>
<!-- Hoist Engine -->
<Border Grid.Row="1" Background="{DynamicResource CardBgBrush}" Padding="10,8" CornerRadius="6"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Margin="0,0,0,8">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center" Margin="0,0,12,0">
<Ellipse Width="10" Height="10" Fill="{Binding HoistStatusColor}" VerticalAlignment="Center" />
@ -21,7 +39,7 @@
</Border>
<!-- AGV Engine -->
<Border Grid.Row="1" Background="{DynamicResource CardBgBrush}" Padding="10,8" CornerRadius="6"
<Border Grid.Row="2" Background="{DynamicResource CardBgBrush}" Padding="10,8" CornerRadius="6"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Margin="0,0,0,10">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center" Margin="0,0,12,0">
@ -39,7 +57,7 @@
</Border>
<!-- Toolbar -->
<Grid Grid.Row="2" ColumnDefinitions="Auto,*,Auto,Auto,Auto" Margin="0,0,0,8">
<Grid Grid.Row="3" ColumnDefinitions="Auto,*,Auto,Auto,Auto" Margin="0,0,0,8">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6">
<Ellipse Width="8" Height="8" Fill="#00E676" VerticalAlignment="Center" />
<TextBlock Text="控制台日志" FontSize="15" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryTextBrush}" VerticalAlignment="Center" />
@ -54,7 +72,7 @@
</Grid>
<!-- Log List -->
<Border Grid.Row="3" Background="{DynamicResource CardBgBrush}" CornerRadius="6" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
<Border Grid.Row="4" Background="{DynamicResource CardBgBrush}" CornerRadius="6" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
<ListBox ItemsSource="{Binding Logs}" Background="Transparent" Foreground="#BCC8D6" BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate>

@ -0,0 +1,228 @@
# 任务执行调度 — 当前实现记录
> 记录时间2026-06-13
> 状态:包材入库 (category=1, type=1) 任务驱动并行执行已跑通
---
## 一、整体架构
```
UI (SystemMonitorView)
├─ [▶ 启动] / [■ 停止] 包材入库调度
└─ SystemMonitorViewModel
└─ MaterialInStoreExecutor (Singleton)
Sln.Wcs.Strategy / MaterialInStoreExecutor
├─ 轮询待执行任务 (每 5 秒)
├─ 多任务并行 (Thread最多 10 个)
├─ 单任务内串行 (逐条 detail)
└─ 设备下发 (AGV/提升机/输送线)
Sln.Wcs.Business / TaskCreateService
└─ 拓扑路由 → 自动生成 LiveTaskQueue + List<LiveTaskDetail>
```
## 二、任务创建
### 输入
| 参数 | 说明 | 示例 |
|------|------|------|
| startBuilding | 起始楼栋 | 13 |
| startFloor | 起始楼层 | 1 |
| startLocation | 起始位置号 | 101 |
| endBuilding | 目标楼栋 | 15 |
| endFloor | 目标楼层 | 3 |
| endLocation | 目标位置号 | 102 |
| taskType | 1入库 / 2出库 | 1 |
| taskCategory | 1包材 / 2成品 / 3托盘 | 1 |
| palletBarcode | 托盘条码 | PALLET001 |
| materialCode | 物料编码 | M001 |
### 输出
```
LiveTaskQueue
├─ taskCode: 自动生成 (yyyyMMddHHmmss + 随机数)
├─ taskType, taskCategory
├─ startPoint, endPoint: 自动拼接为 {building}#_L{floor}_{location}
├─ taskStatus: 1 (待执行)
├─ taskSteps: 明细数量
└─ taskDetails: List<LiveTaskDetail>
```
### 拓扑规则
- 所有楼栋 13# / 14# / 15# 均为 1~5F
- 每栋楼有 1 台提升机,可直达任意楼层(全连接)
- 跨栋仅 2F 连通:
- (13,2) ↔ (14,2):廊桥交接,两段 AGV13# AGV → BRIDGE_13_1414# AGV 从 BRIDGE_13_14 接走)
- (14,2) ↔ (15,2)14# AGV 可驶入 15#,不生成过渡段
### 示例13栋1楼101 → 15栋3楼102
```
路由节点: (13,1) → (13,2) → (14,2) → (15,2) → (15,3)
Detail 1: AGV 13#_L1_101 → 13#_L1_HOIST
Detail 2: 提升机 13#_L1_HOIST → 13#_L2_HOIST
Detail 3: AGV 13#_L2_HOIST → BRIDGE_13_14 (13# AGV)
Detail 4: AGV BRIDGE_13_14 → 15#_L2_HOIST (14# AGV 直达)
Detail 5: 提升机 15#_L2_HOIST → 15#_L3_HOIST
Detail 6: AGV 15#_L3_HOIST → 15#_L3_102
```
## 三、任务执行
### 执行器MaterialInStoreExecutor
**位置**`Sln.Wcs.Strategy/MaterialInStoreExecutor.cs`
**查询条件**
```csharp
taskType == 1 && taskCategory == 1 && taskStatus == 1
```
**轮询间隔**5 秒
### 主循环
```
Start()
└─ Task.Run → RunLoopAsync
└─ while (!cancelled):
ExecuteAsync() ← 阻塞等待所有任务线程完成
await Task.Delay(5000)
```
### 并行策略
```
ExecuteAsync:
查询所有待执行任务
→ foreach task → new Thread:
TaskSemaphore.Wait() ← 最多 10 线程
ProcessOneAsync(task).GetAwaiter().GetResult()
TaskSemaphore.Release()
→ Thread.Join() 等待全部完成
```
### 单任务处理
```
ProcessOneAsync(task):
🔒 lock → taskStatus = 2, Update(task)
foreach detail (按 objId 排序):
if detail 已完成 → continue
🔒 lock → detailStatus = 2, Update(detail)
🔓
下发设备 (根据 deviceType):
1 → DispatchAgvAsync (10s 模拟延迟)
2 → DispatchHoistAsync (20s 模拟延迟,按楼栋限 2 并发)
0 → DispatchConveyorAsync
🔒 lock → detailStatus = 3, Update(detail)
🔓
🔒 lock → taskStatus = 3, Update(task)
```
### 并发控制
| 控制点 | 机制 | 值 |
|--------|------|-----|
| 任务级并发 | `SemaphoreSlim` | 最多 10 个 |
| 提升机并发 | `ConcurrentDictionary<int, SemaphoreSlim>` | 每栋楼 2 台 |
| AGV 并发 | 不限 | — |
| DB 写入 | `lock (DbWriteLock)` 串行化 | — |
| 启停状态 | `lock (_lock)` + `volatile bool` | — |
### 线程安全
- **启停**`lock (_lock)` 保护 `_cts` / `_isRunning`
- **DB 写入**`lock (DbWriteLock)` 保护 SqlSugar UpdateContext 为单例,多线程写入冲突)
- **提升机信号量**`ConcurrentDictionary` 线程安全
- **任务信号量**`SemaphoreSlim` 本身线程安全
- **延迟模拟**`await Task.Delay` 在锁外,多任务并行不受影响
## 四、仓储线程安全
**位置**`Sln.Wcs.Repository/Repository.cs`
```csharp
public Repository(ISqlSugarClient db)
{
itenant = db.AsTenant();
Context = (SqlSugarClient)db; // 直接用 SqlSugarScope内部管理连接池
}
```
`SqlSugarScope` 注册为 Singleton通过 `[Tenant]` 特性自动路由数据库。
## 五、设备下发(当前为模拟)
| 设备 | 方法 | 延迟 | 并发限制 |
|------|------|------|---------|
| AGV | `DispatchAgvAsync` | 10 秒 | 无 |
| 提升机 | `DispatchHoistAsync` | 20 秒 | 每栋楼 2 台 |
| 输送线 | `DispatchConveyorAsync` | 无 | 无 |
后续对接AGV → `HikRoBotDispatchHub.ReciveTask`,提升机 → `HoistDispatchHub.TaskDispatch`
## 六、UI 控制
**系统监控页面** — 顶部卡片:
```
┌──────────────────────────────────────────────┐
│ ● 包材入库调度 运行中 [▶ 启动] [■ 停止] │
└──────────────────────────────────────────────┘
```
按钮绑定:
- 启动 → `MaterialInStoreExecutor.Start()`
- 停止 → `MaterialInStoreExecutor.Stop()`
- 状态指示灯:绿色 = 运行中,红色 = 已停止
## 七、DI 注册
**App.axaml.cs**
```csharp
// Assembly 扫描
Assembly.LoadFrom("Sln.Wcs.Strategy.dll")
// Singleton 注册
services.AddSingleton<MaterialInStoreExecutor>();
```
**依赖关系**
```
MaterialInStoreExecutor (Singleton)
├─ SerilogHelper (Singleton)
├─ ILiveTaskQueueService (Transient, 来自 Scrutor 扫描)
└─ ILiveTaskDetailService (Transient, 来自 Scrutor 扫描)
```
## 八、关键文件
| 文件 | 说明 |
|------|------|
| `Sln.Wcs.Business/TaskCreateService.cs` | 拓扑路由 + 任务创建 |
| `Sln.Wcs.Strategy/MaterialInStoreExecutor.cs` | 包材入库执行器 |
| `Sln.Wcs.Repository/Repository.cs` | 仓储基类(线程安全改造) |
| `Sln.Wcs.UI/ViewModels/SystemMonitorViewModel.cs` | 系统监控 VM启停控制 |
| `Sln.Wcs.UI/Views/SystemMonitorView.axaml` | 系统监控页面 |
| `Sln.Wcs.UI/App.axaml.cs` | DI 注册 |
| `接驳位驱动调度方案.md` | 调度方案设计文档 |
## 九、待优化项
1. **真实设备对接**:替换 `DispatchAgvAsync` / `DispatchHoistAsync` 中的模拟延迟为实际 API 调用
2. **扩展其他类型**:成品入库/出库、托盘入库/出库等(参考 MaterialInStoreExecutor 模板)
3. **DB 写入优化**:去掉 `lock (DbWriteLock)`,从根本上解决 SqlSugar 多线程写入问题(可能需要连接串配置 `Max Pool Size` 或改造仓储层)
4. **任务优先级**:当前 FIFO可按优先级排序
5. **异常恢复**:任务中途停止后重新启动的状态恢复
6. **手动触发**UI 上支持手动输入接驳位 + RFID 触发单条明细执行

@ -0,0 +1,170 @@
# 任务驱动的任务调度方案
## 一、核心思路
```
不是一次性下发全部步骤,也不是等待接驳位信号
任务创建后 (status=1)
→ 执行器轮询到 → 启动线程
→ 单任务内逐条串行下发明细
→ 前一条完成 → 自动执行下一条
→ 全部完成 → 任务结束
多任务之间并行执行,互不干扰
```
## 二、执行流程
```
Start() 启动后台轮询
└─ RunLoopAsync (每 5 秒一轮)
└─ ExecuteAsync
├─ 查询 taskType=1, taskCategory=1, taskStatus=1
├─ foreach task → new Thread:
│ │
│ ├─ TaskSemaphore.Wait() ← 最多 10 线程
│ │
│ └─ ProcessOneAsync(task):
│ │
│ ├─ taskStatus → 2
│ │
│ ├─ foreach detail (按 objId):
│ │ │
│ │ ├─ detailStatus → 2
│ │ ├─ 下发设备
│ │ │ 1 → AGV (不限并发)
│ │ │ 2 → 提升机 (每栋限 2)
│ │ │ 0 → 输送线
│ │ ├─ 等待执行完成
│ │ └─ detailStatus → 3
│ │
│ └─ taskStatus → 3
└─ Thread.Join() 等本轮全部完成 → 5 秒后下一轮
```
## 三、并发模型
```
任务 A: Detail 1 → Detail 2 → Detail 3 → ...
任务 B: Detail 1 → Detail 2 → Detail 3 → ... 三个任务同时跑
任务 C: Detail 1 → Detail 2 → Detail 3 → ...
单任务内: 串行 (前一条完成才执行下一条)
多任务间: 并行 (各自独立线程)
```
### 限制
| 控制项 | 机制 | 值 |
|--------|------|-----|
| 最大并行任务数 | `SemaphoreSlim` | 10 |
| 提升机并行/栋 | `ConcurrentDictionary<int, SemaphoreSlim>` | 2 |
| AGV 并行 | 无限制 | — |
| DB 写入 | `lock (DbWriteLock)` | 串行 |
## 四、单任务处理详情
```
ProcessOneAsync(task):
Step 1 ─ 标记任务执行中
lock (DbWriteLock):
task.taskStatus = 2
Update(task)
Step 2 ─ 逐条处理明细 (按 objId 排序)
for each detail:
跳过已完成 (status 2 or 3)
lock (DbWriteLock):
detail.taskStatus = 2
Update(detail)
↓ 下发设备 (锁外,可并行)
deviceType:
1 → DispatchAgvAsync AGV 模拟 10s
2 → DispatchHoistAsync 提升机模拟 20s
0 → DispatchConveyorAsync 输送线
成功:
lock (DbWriteLock):
detail.taskStatus = 3
Update(detail)
失败:
lock (DbWriteLock):
detail.taskStatus = 1 回退待重试
Update(detail)
return (中断任务)
Step 3 ─ 标记任务完成
lock (DbWriteLock):
task.taskStatus = 3
Update(task)
```
## 五、设备下发
| 设备类型 | deviceType | 方法 | 模拟延迟 | 并发控制 |
|---------|-----------|------|---------|---------|
| AGV | 1 | `DispatchAgvAsync` | 10s | 无限制 |
| 提升机 | 2 | `DispatchHoistAsync` | 20s | 每栋 2 台 |
| 输送线 | 0 | `DispatchConveyorAsync` | 即时 | — |
### 提升机并发控制
```csharp
// 从 startPoint 提取楼栋号: "14#_L2_HOIST" → 14
var building = ExtractBuilding(detail.startPoint);
// 获取该楼栋信号量,首次自动创建 SemaphoreSlim(2, 2)
var semaphore = HoistSemaphores.GetOrAdd(building, _ => new SemaphoreSlim(2, 2));
await semaphore.WaitAsync(); // 等待有空闲提升机
// ... 执行 ...
semaphore.Release(); // 释放
```
## 六、执行示例
3 个包材入库任务,每个 6 条明细:
```
T+0s [线程1] 任务A Detail1 AGV开始 [线程2] 任务B Detail1 AGV开始 [线程3] 任务C Detail1 AGV开始
T+10s [线程1] 任务A Detail1 完成 [线程2] 任务B Detail1 完成 [线程3] 任务C Detail1 完成
T+10s [线程1] 任务A Detail2 提升机开始 [线程2] 任务B Detail2 提升机开始 [线程3] 等待(13#提升机满)
T+30s [线程1] 任务A Detail2 完成 [线程2] 任务B Detail2 完成 [线程3] 任务C Detail2 提升机开始
T+30s [线程1] 任务A Detail3 AGV开始 [线程2] 任务B Detail3 AGV开始
...并行继续...
```
## 七、状态流转
```
LiveTaskQueue.taskStatus:
1 (待执行) → 2 (执行中) → 3 (已完成)
LiveTaskDetail.taskStatus:
1 (待执行) → 2 (执行中) → 3 (已完成)
↘ 失败回退 → 1 (待重试)
```
## 八、UI 控制
系统监控页面顶部卡片,点击启动/停止:
```
┌──────────────────────────────────────────────────┐
│ ● 包材入库调度 运行中/已停止 [▶ 启动] [■ 停止] │
└──────────────────────────────────────────────────┘
```
- `Start()` → 创建 `CancellationTokenSource`,启动 `RunLoopAsync`
- `Stop()` → 取消 token等待当前线程结束后标记停止
Loading…
Cancel
Save