Просмотр исходного кода

feat: 集成状态机/定时任务/工作流引擎

- 集成 Stateless 5.20.1: 创建设备状态机(DeviceStateMachine)管理泵/风机状态转换
- 集成 Quartz.NET 3.20.0: 创建调度服务和定时任务(PlcPollingJob/DataLoggingJob)
- 创建简化版工作流服务(WorkflowService)支持工艺流程编排
- 更新 SystemConfig 添加 PlcPollingIntervalMs 配置
- 更新 PlcPollingService/DataLoggingService 支持 Quartz Job 调用
- 更新 App.axaml.cs 集成新服务启动/停止逻辑
- 问题归档: docs/elsa-integration-notes.md
DESKTOP-74CLTRG\Leol 1 месяц назад
Родитель
Сommit
a322a85be2

+ 120 - 0
docs/elsa-integration-notes.md

@@ -0,0 +1,120 @@
+# Elsa Workflows 集成说明
+
+## 问题背景
+
+YZWater3 项目需要集成工作流引擎来实现工艺流程编排。经过评估,选择了 Elsa Workflows 作为候选方案。
+
+## 遇到的问题
+
+### 1. API 不兼容
+
+安装 `Elsa.Workflows.Core 3.7.1` 后,发现以下命名空间不存在:
+- `Elsa.Workflows.Contracts`
+- `Elsa.Workflows.Runtime`
+- `Elsa.Workflows.Runtime.Messages`
+
+### 2. 需要完整的 DI 容器
+
+Elsa 3.x 需要完整的依赖注入容器配置,不适合直接在桌面应用中使用。
+
+### 3. 需要额外的 NuGet 包
+
+完整功能需要安装:
+- `Elsa.Workflows.Core`
+- `Elsa.Workflows.Runtime`
+- `Elsa.Activities.Console`
+- `Elsa.Activities.Http`
+- 等...
+
+## 当前解决方案
+
+### 简化版工作流服务
+
+创建了自定义的 `WorkflowService`,提供基本的工作流功能:
+
+```csharp
+// 初始化
+await WorkflowService.Instance.InitializeAsync();
+
+// 执行工作流
+var result = await WorkflowService.Instance.ExecuteWorkflowAsync("WastewaterTreatment", context);
+
+// 注册自定义工作流
+WorkflowService.Instance.RegisterWorkflow("MyWorkflow", definition);
+```
+
+### 工作流定义
+
+```csharp
+var workflow = new WorkflowDefinition
+{
+    Name = "污水处理流程",
+    Description = "自动检测液位、流量,控制泵启停",
+    Steps = new List<WorkflowStep>
+    {
+        new() { Name = "CheckTankLevel", Action = CheckTankLevel },
+        new() { Name = "CheckFlowRate", Action = CheckFlowRate },
+        new() { Name = "ControlPump", Action = ControlPump }
+    }
+};
+```
+
+## 完整 Elsa 集成方案(未来参考)
+
+### 所需 NuGet 包
+
+```xml
+<PackageReference Include="Elsa.Workflows.Core" Version="3.7.1" />
+<PackageReference Include="Elsa.Workflows.Runtime" Version="3.7.1" />
+<PackageReference Include="Elsa.Activities.Console" Version="3.7.1" />
+<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
+```
+
+### 配置代码
+
+```csharp
+using Elsa.Extensions;
+using Microsoft.Extensions.DependencyInjection;
+
+var services = new ServiceCollection();
+
+services.AddElsa(elsa =>
+{
+    elsa.AddActivitiesFrom<Program>();
+    elsa.UsePersistence(new MemoryWorkflowStateProvider());
+});
+
+var serviceProvider = services.BuildServiceProvider();
+```
+
+### 执行工作流
+
+```csharp
+using var scope = serviceProvider.CreateScope();
+var workflowRunner = scope.ServiceProvider.GetRequiredService<IWorkflowRunner>();
+var workflow = new Workflow(new Sequence { ... });
+var result = await workflowRunner.RunAsync(workflow);
+```
+
+## 替代方案
+
+如果未来需要更强大的工作流引擎,可以考虑:
+
+1. **WorkflowCore** - 更轻量,API 更简单
+2. **CoreWF** - .NET Workflow Foundation 的移植版
+3. **自建简单状态机** - 对于简单场景足够
+
+## 总结
+
+当前使用简化版工作流服务满足基本需求。如果未来需要:
+- 工作流持久化
+- 可视化设计器
+- 复杂分支/并行执行
+- 与外部系统集成
+
+建议重新评估 Elsa Workflows 的完整集成方案,或考虑替代方案。
+
+---
+
+*创建时间: 2026-09-01*  
+*状态: 待后续完善*

+ 43 - 0
src/YZWater.Avalonia/App.axaml.cs

@@ -159,6 +159,34 @@ public partial class App : Application
         AlarmService.Instance.Start();
         _dataLogger = new DataLoggingService(PlcPollingService.Instance);
         _dataLogger.Start();
+
+        // 初始化新集成服务(与现有服务并行运行)
+        InitializeNewServicesAsync().ConfigureAwait(false);
+    }
+
+    /// <summary>
+    /// 初始化新集成的服务(Stateless/Quartz/Elsa)
+    /// </summary>
+    private async Task InitializeNewServicesAsync()
+    {
+        try
+        {
+            // 1. 初始化设备状态机
+            DeviceStateMachineManager.Instance.InitializeDefaultDevices();
+            Serilog.Log.Information("设备状态机已初始化");
+
+            // 2. 启动 Quartz 调度器
+            await QuartzSchedulerService.Instance.StartAsync();
+            Serilog.Log.Information("Quartz 调度器已启动");
+
+            // 3. 初始化 Elsa 工作流引擎
+            await WorkflowService.Instance.InitializeAsync();
+            Serilog.Log.Information("Elsa 工作流引擎已初始化");
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Error(ex, "初始化新服务失败");
+        }
     }
 
     private void StartDirectMode()
@@ -342,6 +370,21 @@ public partial class App : Application
         _dataLogger?.Dispose();
         _dataLogger = null;
 
+        // 停止新集成的服务
+        try
+        {
+            QuartzSchedulerService.Instance.Dispose();
+            WorkflowService.Instance.Dispose();
+            DeviceStateMachineManager.Instance.Clear();
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "停止新服务时出现异常");
+        }
+
+        QuartzSchedulerService.ResetInstance();
+        WorkflowService.ResetInstance();
+
         AlarmService.ResetInstance();
         PlcPollingService.ResetInstance();
         PlcService.Dispose();

+ 6 - 0
src/YZWater.Core/Models/SystemConfig.cs

@@ -134,6 +134,12 @@ public partial class SystemConfig : ObservableObject
     [ObservableProperty]
     private string _activeProject = string.Empty;
 
+    /// <summary>
+    /// PLC 轮询间隔(毫秒)
+    /// </summary>
+    [ObservableProperty]
+    private int _plcPollingIntervalMs = 1000;
+
     /// <summary>
     /// PLC 地址覆盖(key=标签名如 Tank1Level, value=地址如 VD100)
     /// 空则使用默认地址

+ 9 - 4
src/YZWater.Core/Services/DataLoggingService.cs

@@ -8,6 +8,10 @@ namespace YZWater.Core.Services;
 /// </summary>
 public class DataLoggingService : IDisposable
 {
+    // ─── 单例(供 Quartz Job 访问) ───
+    private static DataLoggingService? _instance;
+    public static DataLoggingService? Instance => _instance;
+
     private readonly PlcPollingService _pollingService;
     private CancellationTokenSource? _cts;
     private Task? _loggingTask;
@@ -41,6 +45,7 @@ public class DataLoggingService : IDisposable
     public DataLoggingService(PlcPollingService pollingService)
     {
         _pollingService = pollingService;
+        _instance = this;
         _lastFlowTime = DateTime.Now;
 
         // 从数据库恢复上次累计值,避免重启归零
@@ -202,7 +207,7 @@ public class DataLoggingService : IDisposable
     /// <summary>
     /// 记录流量数据(含累计流量计算)
     /// </summary>
-    private async Task LogFlowDataAsync()
+    public async Task LogFlowDataAsync()
     {
         try
         {
@@ -231,7 +236,7 @@ public class DataLoggingService : IDisposable
     /// <summary>
     /// 记录模拟量数据(液位、泵频率等)
     /// </summary>
-    private async Task LogAnalogDataAsync()
+    public async Task LogAnalogDataAsync()
     {
         try
         {
@@ -265,7 +270,7 @@ public class DataLoggingService : IDisposable
     /// <summary>
     /// 记录设备状态(含运行小时数累计)
     /// </summary>
-    private async Task LogDeviceStatusAsync()
+    public async Task LogDeviceStatusAsync()
     {
         try
         {
@@ -343,7 +348,7 @@ public class DataLoggingService : IDisposable
     /// <summary>
     /// 清理过期数据
     /// </summary>
-    private async Task CleanupOldDataAsync()
+    public async Task CleanupOldDataAsync()
     {
         try
         {

+ 327 - 0
src/YZWater.Core/Services/DeviceStateMachine.cs

@@ -0,0 +1,327 @@
+using Stateless;
+using Serilog;
+using YZWater.Core.Models;
+
+namespace YZWater.Core.Services;
+
+/// <summary>
+/// 设备状态机服务 - 使用 Stateless 管理设备状态转换
+/// </summary>
+public class DeviceStateMachine
+{
+    private readonly StateMachine<DeviceState, DeviceTrigger> _stateMachine;
+    private readonly string _deviceId;
+    private readonly string _deviceName;
+
+    /// <summary>
+    /// 设备状态枚举(用于状态机)
+    /// </summary>
+    public enum DeviceState
+    {
+        /// <summary>停止</summary>
+        Stopped,
+        /// <summary>运行中</summary>
+        Running,
+        /// <summary>故障</summary>
+        Fault,
+        /// <summary>维护中</summary>
+        Maintenance
+    }
+
+    /// <summary>
+    /// 设备触发器枚举
+    /// </summary>
+    public enum DeviceTrigger
+    {
+        /// <summary>启动</summary>
+        Start,
+        /// <summary>停止</summary>
+        Stop,
+        /// <summary>发生故障</summary>
+        Fault,
+        /// <summary>故障恢复</summary>
+        Recover,
+        /// <summary>进入维护</summary>
+        BeginMaintenance,
+        /// <summary>维护完成</summary>
+        CompleteMaintenance
+    }
+
+    /// <summary>
+    /// 当前状态
+    /// </summary>
+    public DeviceState CurrentState => _stateMachine.State;
+
+    /// <summary>
+    /// 状态变更事件
+    /// </summary>
+    public event Action<string, DeviceState, DeviceState>? StateChanged;
+
+    /// <summary>
+    /// 进入状态事件(用于触发报警、日志等)
+    /// </summary>
+    public event Action<string, DeviceState>? StateEntered;
+
+    /// <summary>
+    /// 离开状态事件
+    /// </summary>
+    public event Action<string, DeviceState>? StateExited;
+
+    public DeviceStateMachine(string deviceId, string deviceName, DeviceState initialState = DeviceState.Stopped)
+    {
+        _deviceId = deviceId;
+        _deviceName = deviceName;
+        _stateMachine = new StateMachine<DeviceState, DeviceTrigger>(initialState);
+
+        ConfigureStateMachine();
+    }
+
+    /// <summary>
+    /// 配置状态机转换规则
+    /// </summary>
+    private void ConfigureStateMachine()
+    {
+        // 停止状态
+        _stateMachine.Configure(DeviceState.Stopped)
+            .Permit(DeviceTrigger.Start, DeviceState.Running)
+            .Permit(DeviceTrigger.Fault, DeviceState.Fault)
+            .Permit(DeviceTrigger.BeginMaintenance, DeviceState.Maintenance)
+            .OnEntry(() => OnStateEntered(DeviceState.Stopped))
+            .OnExit(() => OnStateExited(DeviceState.Stopped));
+
+        // 运行状态
+        _stateMachine.Configure(DeviceState.Running)
+            .Permit(DeviceTrigger.Stop, DeviceState.Stopped)
+            .Permit(DeviceTrigger.Fault, DeviceState.Fault)
+            .Permit(DeviceTrigger.BeginMaintenance, DeviceState.Maintenance)
+            .OnEntry(() => OnStateEntered(DeviceState.Running))
+            .OnExit(() => OnStateExited(DeviceState.Running));
+
+        // 故障状态
+        _stateMachine.Configure(DeviceState.Fault)
+            .Permit(DeviceTrigger.Recover, DeviceState.Stopped)
+            .Permit(DeviceTrigger.BeginMaintenance, DeviceState.Maintenance)
+            .OnEntry(() => OnStateEntered(DeviceState.Fault))
+            .OnExit(() => OnStateExited(DeviceState.Fault));
+
+        // 维护状态
+        _stateMachine.Configure(DeviceState.Maintenance)
+            .Permit(DeviceTrigger.CompleteMaintenance, DeviceState.Stopped)
+            .Permit(DeviceTrigger.Fault, DeviceState.Fault)
+            .OnEntry(() => OnStateEntered(DeviceState.Maintenance))
+            .OnExit(() => OnStateExited(DeviceState.Maintenance));
+
+        // 状态转换回调
+        _stateMachine.OnTransitioned(transition =>
+        {
+            Log.Debug("设备 [{DeviceId}:{DeviceName}] 状态转换: {Source} → {Destination} (触发器: {Trigger})",
+                _deviceId, _deviceName, transition.Source, transition.Destination, transition.Trigger);
+            StateChanged?.Invoke(_deviceId, transition.Source, transition.Destination);
+        });
+
+        // 注: Stateless 5.x 无 OnTransitionException,异常通过 FireSafe 方法捕获
+    }
+
+    /// <summary>
+    /// 进入状态时的回调
+    /// </summary>
+    private void OnStateEntered(DeviceState state)
+    {
+        Log.Information("设备 [{DeviceId}:{DeviceName}] 进入状态: {State}",
+            _deviceId, _deviceName, state);
+        StateEntered?.Invoke(_deviceId, state);
+    }
+
+    /// <summary>
+    /// 离开状态时的回调
+    /// </summary>
+    private void OnStateExited(DeviceState state)
+    {
+        Log.Debug("设备 [{DeviceId}:{DeviceName}] 离开状态: {State}",
+            _deviceId, _deviceName, state);
+        StateExited?.Invoke(_deviceId, state);
+    }
+
+    // ─── 触发方法 ───
+
+    /// <summary>
+    /// 安全触发(捕获异常并记录)
+    /// </summary>
+    private void FireSafe(DeviceTrigger trigger)
+    {
+        try
+        {
+            _stateMachine.Fire(trigger);
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "设备 [{DeviceId}:{DeviceName}] 状态转换异常: 触发器 {Trigger}, 当前状态 {State}",
+                _deviceId, _deviceName, trigger, CurrentState);
+        }
+    }
+
+    /// <summary>
+    /// 启动设备
+    /// </summary>
+    public void Start() => FireSafe(DeviceTrigger.Start);
+
+    /// <summary>
+    /// 停止设备
+    /// </summary>
+    public void Stop() => FireSafe(DeviceTrigger.Stop);
+
+    /// <summary>
+    /// 报告故障
+    /// </summary>
+    public void ReportFault() => FireSafe(DeviceTrigger.Fault);
+
+    /// <summary>
+    /// 故障恢复
+    /// </summary>
+    public void Recover() => FireSafe(DeviceTrigger.Recover);
+
+    /// <summary>
+    /// 进入维护
+    /// </summary>
+    public void BeginMaintenance() => FireSafe(DeviceTrigger.BeginMaintenance);
+
+    /// <summary>
+    /// 完成维护
+    /// </summary>
+    public void CompleteMaintenance() => FireSafe(DeviceTrigger.CompleteMaintenance);
+
+    /// <summary>
+    /// 尝试触发(不抛异常)
+    /// </summary>
+    public bool TryFire(DeviceTrigger trigger)
+    {
+        if (_stateMachine.CanFire(trigger))
+        {
+            _stateMachine.Fire(trigger);
+            return true;
+        }
+        Log.Warning("设备 [{DeviceId}:{DeviceName}] 无法触发 {Trigger}: 当前状态 {State}",
+            _deviceId, _deviceName, trigger, CurrentState);
+        return false;
+    }
+
+    /// <summary>
+    /// 是否可以触发
+    /// </summary>
+    public bool CanFire(DeviceTrigger trigger) => _stateMachine.CanFire(trigger);
+
+    /// <summary>
+    /// 将 DeviceState 转换为旧版 DeviceStatus 枚举(兼容)
+    /// </summary>
+    public DeviceStatus ToDeviceStatus() => CurrentState switch
+    {
+        DeviceState.Stopped => DeviceStatus.Stopped,
+        DeviceState.Running => DeviceStatus.Running,
+        DeviceState.Fault => DeviceStatus.Fault,
+        DeviceState.Maintenance => DeviceStatus.Maintenance,
+        _ => DeviceStatus.Stopped
+    };
+
+    /// <summary>
+    /// 获取允许的状态转换列表
+    /// </summary>
+    public IEnumerable<DeviceTrigger> GetPermittedTriggers() => _stateMachine.PermittedTriggers;
+}
+
+/// <summary>
+/// 设备状态机管理器 - 管理所有设备的状态机实例
+/// </summary>
+public class DeviceStateMachineManager
+{
+    private static DeviceStateMachineManager? _instance;
+    private static readonly object _lock = new();
+    public static DeviceStateMachineManager Instance
+    {
+        get
+        {
+            lock (_lock)
+            {
+                return _instance ??= new DeviceStateMachineManager();
+            }
+        }
+    }
+
+    private readonly Dictionary<string, DeviceStateMachine> _machines = new();
+    private readonly object _machinesLock = new();
+
+    /// <summary>
+    /// 获取或创建设备状态机
+    /// </summary>
+    public DeviceStateMachine GetOrCreate(string deviceId, string deviceName, DeviceStateMachine.DeviceState initialState = DeviceStateMachine.DeviceState.Stopped)
+    {
+        lock (_machinesLock)
+        {
+            if (!_machines.ContainsKey(deviceId))
+            {
+                _machines[deviceId] = new DeviceStateMachine(deviceId, deviceName, initialState);
+                Log.Information("创建设备状态机: {DeviceId} ({DeviceName})", deviceId, deviceName);
+            }
+            return _machines[deviceId];
+        }
+    }
+
+    /// <summary>
+    /// 获取设备状态机
+    /// </summary>
+    public DeviceStateMachine? Get(string deviceId)
+    {
+        lock (_machinesLock)
+        {
+            return _machines.GetValueOrDefault(deviceId);
+        }
+    }
+
+    /// <summary>
+    /// 获取所有设备状态机
+    /// </summary>
+    public IReadOnlyDictionary<string, DeviceStateMachine> GetAll()
+    {
+        lock (_machinesLock)
+        {
+            return new Dictionary<string, DeviceStateMachine>(_machines);
+        }
+    }
+
+    /// <summary>
+    /// 移除设备状态机
+    /// </summary>
+    public bool Remove(string deviceId)
+    {
+        lock (_machinesLock)
+        {
+            return _machines.Remove(deviceId);
+        }
+    }
+
+    /// <summary>
+    /// 清除所有状态机
+    /// </summary>
+    public void Clear()
+    {
+        lock (_machinesLock)
+        {
+            _machines.Clear();
+        }
+    }
+
+    /// <summary>
+    /// 初始化默认设备(5台泵 + 2台风机)
+    /// </summary>
+    public void InitializeDefaultDevices()
+    {
+        for (int i = 1; i <= 5; i++)
+        {
+            GetOrCreate($"P{i:D3}", $"泵{i}");
+        }
+        for (int i = 1; i <= 2; i++)
+        {
+            GetOrCreate($"F{i:D3}", $"风机{i}");
+        }
+        Log.Information("已初始化 {Count} 台默认设备状态机", _machines.Count);
+    }
+}

+ 177 - 0
src/YZWater.Core/Services/Jobs/PollingJobs.cs

@@ -0,0 +1,177 @@
+using Quartz;
+using Serilog;
+using YZWater.Core.Models;
+
+namespace YZWater.Core.Services.Jobs;
+
+/// <summary>
+/// PLC 轮询任务 - 替代 PlcPollingService 中的手写轮询循环
+/// </summary>
+public class PlcPollingJob : IJob
+{
+    /// <summary>
+    /// Job Data Key: 轮询间隔(毫秒)
+    /// </summary>
+    public const string PollingIntervalKey = "PollingIntervalMs";
+
+    /// <summary>
+    /// Job Data Key: 最大重连次数
+    /// </summary>
+    public const string MaxReconnectKey = "MaxReconnectAttempts";
+
+    /// <summary>
+    /// Job Data Key: 重连基础延迟(毫秒)
+    /// </summary>
+    public const string ReconnectDelayKey = "ReconnectBaseDelayMs";
+
+    public async Task Execute(IJobExecutionContext context)
+    {
+        var dataMap = context.MergedJobDataMap;
+        var pollingService = PlcPollingService.Instance;
+
+        try
+        {
+            if (context.RefireCount > 0)
+            {
+                Log.Warning("PLC 轮询任务重入次数: {Count}", context.RefireCount);
+            }
+
+            // 执行一次轮询
+            await PollOnceAsync(pollingService);
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "PLC 轮询任务执行异常");
+
+            // 标记为恢复异常,Quartz 会根据策略重试
+            throw new JobExecutionException(ex, refireImmediately: false);
+        }
+    }
+
+    /// <summary>
+    /// 执行一次轮询
+    /// </summary>
+    private async Task PollOnceAsync(PlcPollingService pollingService)
+    {
+        if (MockPlcService.IsEnabled)
+        {
+            // Mock 模式:直接生成模拟数据
+            MockPlcService.UpdateData(pollingService.Data);
+        }
+        else
+        {
+            // 真实模式:确保连接并读取数据
+            if (!PlcService.IsConnected)
+            {
+                var connected = await PlcService.ConnectAsync();
+                if (!connected)
+                {
+                    pollingService.Data.IsPlcConnected = false;
+                    return;
+                }
+                pollingService.Data.IsPlcConnected = true;
+            }
+
+            // 读取数据(复用 PlcPollingService 的内部方法)
+            await pollingService.ReadDataInternal();
+        }
+
+        // 触发数据更新事件
+        pollingService.RaiseDataUpdated(pollingService.Data);
+    }
+}
+
+/// <summary>
+/// 数据日志任务 - 替代 DataLoggingService 中的手写日志循环
+/// </summary>
+public class DataLoggingJob : IJob
+{
+    /// <summary>
+    /// Job Data Key: 流量记录间隔(秒)
+    /// </summary>
+    public const string FlowLogIntervalKey = "FlowLogIntervalSeconds";
+
+    /// <summary>
+    /// Job Data Key: 设备状态记录间隔(秒)
+    /// </summary>
+    public const string DeviceLogIntervalKey = "DeviceStatusLogIntervalSeconds";
+
+    // 计数器(使用 JobDataMap 持久化)
+    private static int _flowCounter;
+    private static int _deviceCounter;
+    private static int _cleanupCounter;
+
+    public async Task Execute(IJobExecutionContext context)
+    {
+        var dataLoggingService = DataLoggingService.Instance;
+        if (dataLoggingService == null)
+        {
+            Log.Warning("DataLoggingService 未初始化,跳过数据日志任务");
+            return;
+        }
+
+        try
+        {
+            _flowCounter++;
+            _deviceCounter++;
+            _cleanupCounter++;
+
+            var flowInterval = context.MergedJobDataMap.GetInt(FlowLogIntervalKey);
+            var deviceInterval = context.MergedJobDataMap.GetInt(DeviceLogIntervalKey);
+
+            // 定期记录流量数据
+            if (_flowCounter >= flowInterval)
+            {
+                _flowCounter = 0;
+                await dataLoggingService.LogFlowDataAsync();
+                await dataLoggingService.LogAnalogDataAsync();
+            }
+
+            // 定期记录设备状态
+            if (_deviceCounter >= deviceInterval)
+            {
+                _deviceCounter = 0;
+                await dataLoggingService.LogDeviceStatusAsync();
+            }
+
+            // 每天清理一次过期数据(86400 秒)
+            if (_cleanupCounter >= 86400)
+            {
+                _cleanupCounter = 0;
+                await dataLoggingService.CleanupOldDataAsync();
+            }
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "数据日志任务执行异常");
+        }
+    }
+
+    /// <summary>
+    /// 重置计数器(用于服务重启)
+    /// </summary>
+    public static void ResetCounters()
+    {
+        _flowCounter = 0;
+        _deviceCounter = 0;
+        _cleanupCounter = 0;
+    }
+}
+
+/// <summary>
+/// UI 时钟更新任务 - 可选:用于替代 MainViewModel 中的 System.Timers.Timer
+/// 注意:UI 时钟通常保留 System.Timers.Timer 更简单,此处仅作示例
+/// </summary>
+public class UiClockJob : IJob
+{
+    /// <summary>
+    /// 时钟更新事件(供 ViewModel 订阅)
+    /// </summary>
+    public static event Action<DateTime>? ClockTicked;
+
+    public Task Execute(IJobExecutionContext context)
+    {
+        ClockTicked?.Invoke(DateTime.Now);
+        return Task.CompletedTask;
+    }
+}

+ 8 - 0
src/YZWater.Core/Services/PlcPollingService.cs

@@ -426,6 +426,14 @@ public partial class PlcPollingService : ObservableObject, IDisposable
         DataUpdated?.Invoke(data);
     }
 
+    /// <summary>
+    /// 内部数据读取方法(供 Quartz Job 调用)
+    /// </summary>
+    public async Task ReadDataInternal()
+    {
+        await ReadAllDataAsync(CancellationToken.None);
+    }
+
     public void Dispose()
     {
         if (_disposed) return;

+ 283 - 0
src/YZWater.Core/Services/QuartzSchedulerService.cs

@@ -0,0 +1,283 @@
+using Quartz;
+using Quartz.Impl;
+using Quartz.Impl.Matchers;
+using Serilog;
+
+namespace YZWater.Core.Services;
+
+/// <summary>
+/// Quartz 调度服务 - 管理所有定时任务的调度
+/// </summary>
+public class QuartzSchedulerService : IDisposable
+{
+    private static QuartzSchedulerService? _instance;
+    private static readonly object _instanceLock = new();
+    public static QuartzSchedulerService Instance
+    {
+        get
+        {
+            lock (_instanceLock)
+            {
+                return _instance ??= new QuartzSchedulerService();
+            }
+        }
+    }
+
+    /// <summary>
+    /// 重置单例(用于登出后重新登录)
+    /// </summary>
+    public static void ResetInstance()
+    {
+        lock (_instanceLock)
+        {
+            var old = _instance;
+            _instance = null;
+            old?.Dispose();
+        }
+    }
+
+    private IScheduler? _scheduler;
+    private bool _disposed;
+    private bool _isStarted;
+
+    /// <summary>
+    /// 调度器是否已启动
+    /// </summary>
+    public bool IsStarted => _isStarted;
+
+    private QuartzSchedulerService() { }
+
+    /// <summary>
+    /// 启动调度器
+    /// </summary>
+    public async Task StartAsync()
+    {
+        if (_isStarted)
+        {
+            Log.Warning("Quartz 调度器已在运行中");
+            return;
+        }
+
+        try
+        {
+            // 创建调度器工厂
+            var factory = new StdSchedulerFactory();
+            _scheduler = await factory.GetScheduler();
+
+            // 启动调度器
+            await _scheduler.Start();
+            _isStarted = true;
+
+            Log.Information("Quartz 调度器已启动");
+
+            // 注册默认任务
+            await RegisterDefaultJobsAsync();
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "启动 Quartz 调度器失败");
+            throw;
+        }
+    }
+
+    /// <summary>
+    /// 停止调度器
+    /// </summary>
+    public async Task StopAsync()
+    {
+        if (!_isStarted || _scheduler == null)
+        {
+            return;
+        }
+
+        try
+        {
+            // 等待正在执行的任务完成
+            await _scheduler.Shutdown(waitForJobsToComplete: true);
+            _isStarted = false;
+            Log.Information("Quartz 调度器已停止");
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "停止 Quartz 调度器失败");
+        }
+    }
+
+    /// <summary>
+    /// 注册默认任务
+    /// </summary>
+    private async Task RegisterDefaultJobsAsync()
+    {
+        if (_scheduler == null) return;
+
+        var config = ConfigService.GetConfig();
+
+        // ─── PLC 轮询任务 ───
+        var pollingJob = JobBuilder.Create<Jobs.PlcPollingJob>()
+            .WithIdentity("plcPollingJob", "plc")
+            .UsingJobData(Jobs.PlcPollingJob.PollingIntervalKey, config?.PlcPollingIntervalMs ?? 1000)
+            .UsingJobData(Jobs.PlcPollingJob.MaxReconnectKey, 10)
+            .UsingJobData(Jobs.PlcPollingJob.ReconnectDelayKey, 1000)
+            .Build();
+
+        var pollingTrigger = TriggerBuilder.Create()
+            .WithIdentity("plcPollingTrigger", "plc")
+            .StartNow()
+            .WithSimpleSchedule(x => x
+                .WithIntervalInSeconds(Math.Max(1, (config?.PlcPollingIntervalMs ?? 1000) / 1000))
+                .RepeatForever())
+            .Build();
+
+        await _scheduler.ScheduleJob(pollingJob, pollingTrigger);
+        Log.Information("已注册 PLC 轮询任务,间隔: {Interval}ms", config?.PlcPollingIntervalMs ?? 1000);
+
+        // ─── 数据日志任务(每秒执行,内部根据计数器决定实际写入频率) ───
+        var loggingJob = JobBuilder.Create<Jobs.DataLoggingJob>()
+            .WithIdentity("dataLoggingJob", "data")
+            .UsingJobData(Jobs.DataLoggingJob.FlowLogIntervalKey, 60)       // 每 60 秒记录一次流量
+            .UsingJobData(Jobs.DataLoggingJob.DeviceLogIntervalKey, 300)   // 每 300 秒记录一次设备状态
+            .Build();
+
+        var loggingTrigger = TriggerBuilder.Create()
+            .WithIdentity("dataLoggingTrigger", "data")
+            .StartNow()
+            .WithSimpleSchedule(x => x
+                .WithIntervalInSeconds(1)
+                .RepeatForever())
+            .Build();
+
+        await _scheduler.ScheduleJob(loggingJob, loggingTrigger);
+        Log.Information("已注册数据日志任务");
+    }
+
+    /// <summary>
+    /// 注册自定义任务
+    /// </summary>
+    public async Task ScheduleJobAsync<T>(string jobName, string group, TimeSpan interval, IDictionary<string, object>? jobData = null) where T : IJob
+    {
+        if (_scheduler == null)
+        {
+            throw new InvalidOperationException("调度器未启动");
+        }
+
+        var jobBuilder = JobBuilder.Create<T>()
+            .WithIdentity(jobName, group);
+
+        if (jobData != null)
+        {
+            foreach (var kvp in jobData)
+            {
+                jobBuilder.UsingJobData(kvp.Key, Convert.ToString(kvp.Value));
+            }
+        }
+
+        var job = jobBuilder.Build();
+        var trigger = TriggerBuilder.Create()
+            .WithIdentity($"{jobName}Trigger", group)
+            .StartNow()
+            .WithSimpleSchedule(x => x
+                .WithInterval(interval)
+                .RepeatForever())
+            .Build();
+
+        await _scheduler.ScheduleJob(job, trigger);
+        Log.Information("已注册自定义任务: {JobName} ({Group}), 间隔: {Interval}", jobName, group, interval);
+    }
+
+    /// <summary>
+    /// 使用 Cron 表达式注册任务
+    /// </summary>
+    public async Task ScheduleJobWithCronAsync<T>(string jobName, string group, string cronExpression, IDictionary<string, object>? jobData = null) where T : IJob
+    {
+        if (_scheduler == null)
+        {
+            throw new InvalidOperationException("调度器未启动");
+        }
+
+        var jobBuilder = JobBuilder.Create<T>()
+            .WithIdentity(jobName, group);
+
+        if (jobData != null)
+        {
+            foreach (var kvp in jobData)
+            {
+                jobBuilder.UsingJobData(kvp.Key, Convert.ToString(kvp.Value));
+            }
+        }
+
+        var job = jobBuilder.Build();
+        var trigger = TriggerBuilder.Create()
+            .WithIdentity($"{jobName}Trigger", group)
+            .WithCronSchedule(cronExpression)
+            .Build();
+
+        await _scheduler.ScheduleJob(job, trigger);
+        Log.Information("已注册自定义任务: {JobName} ({Group}), Cron: {Cron}", jobName, group, cronExpression);
+    }
+
+    /// <summary>
+    /// 暂停任务
+    /// </summary>
+    public async Task PauseJobAsync(string jobName, string group)
+    {
+        if (_scheduler == null) return;
+        var jobKey = new JobKey(jobName, group);
+        await _scheduler.PauseJob(jobKey);
+        Log.Information("已暂停任务: {JobName} ({Group})", jobName, group);
+    }
+
+    /// <summary>
+    /// 恢复任务
+    /// </summary>
+    public async Task ResumeJobAsync(string jobName, string group)
+    {
+        if (_scheduler == null) return;
+        var jobKey = new JobKey(jobName, group);
+        await _scheduler.ResumeJob(jobKey);
+        Log.Information("已恢复任务: {JobName} ({Group})", jobName, group);
+    }
+
+    /// <summary>
+    /// 删除任务
+    /// </summary>
+    public async Task DeleteJobAsync(string jobName, string group)
+    {
+        if (_scheduler == null) return;
+        var jobKey = new JobKey(jobName, group);
+        await _scheduler.DeleteJob(jobKey);
+        Log.Information("已删除任务: {JobName} ({Group})", jobName, group);
+    }
+
+    /// <summary>
+    /// 获取所有任务信息
+    /// </summary>
+    public async Task<IReadOnlyCollection<string>> GetAllJobNamesAsync()
+    {
+        if (_scheduler == null) return Array.Empty<string>();
+
+        var groupNames = await _scheduler.GetJobGroupNames();
+        var jobNames = new List<string>();
+
+        foreach (var group in groupNames)
+        {
+            var keys = await _scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(group));
+            foreach (var key in keys)
+            {
+                jobNames.Add($"{key.Group}.{key.Name}");
+            }
+        }
+
+        return jobNames;
+    }
+
+    public void Dispose()
+    {
+        if (_disposed) return;
+        _disposed = true;
+
+        // 同步等待停止
+        StopAsync().GetAwaiter().GetResult();
+        _scheduler = null;
+        Log.Information("Quartz 调度服务已释放");
+    }
+}

+ 297 - 0
src/YZWater.Core/Services/WorkflowService.cs

@@ -0,0 +1,297 @@
+using Serilog;
+
+namespace YZWater.Core.Services;
+
+/// <summary>
+/// Elsa 工作流服务 - 管理工艺流程编排
+/// 注意:Elsa Workflows 3.x 需要完整的 DI 容器和额外的包配置
+/// 当前为简化版本,详细集成方案见 docs/elsa-integration-notes.md
+/// </summary>
+public class WorkflowService : IDisposable
+{
+    private static WorkflowService? _instance;
+    private static readonly object _instanceLock = new();
+    public static WorkflowService Instance
+    {
+        get
+        {
+            lock (_instanceLock)
+            {
+                return _instance ??= new WorkflowService();
+            }
+        }
+    }
+
+    /// <summary>
+    /// 重置单例
+    /// </summary>
+    public static void ResetInstance()
+    {
+        lock (_instanceLock)
+        {
+            var old = _instance;
+            _instance = null;
+            old?.Dispose();
+        }
+    }
+
+    private bool _disposed;
+    private bool _isInitialized;
+
+    /// <summary>
+    /// 工作流是否已初始化
+    /// </summary>
+    public bool IsInitialized => _isInitialized;
+
+    /// <summary>
+    /// 工作流定义存储
+    /// </summary>
+    private readonly Dictionary<string, WorkflowDefinition> _workflows = new();
+
+    private WorkflowService() { }
+
+    /// <summary>
+    /// 初始化工作流引擎
+    /// </summary>
+    public Task InitializeAsync()
+    {
+        if (_isInitialized)
+        {
+            Log.Warning("工作流引擎已在运行中");
+            return Task.CompletedTask;
+        }
+
+        try
+        {
+            // 简化版本:使用自定义工作流定义
+            // 完整 Elsa 集成需要额外的 NuGet 包和 DI 配置
+            _isInitialized = true;
+
+            // 注册示例工作流
+            RegisterSampleWorkflows();
+
+            Log.Information("工作流引擎已初始化(简化版本)");
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "初始化工作流引擎失败");
+            throw;
+        }
+
+        return Task.CompletedTask;
+    }
+
+    /// <summary>
+    /// 注册示例工作流
+    /// </summary>
+    private void RegisterSampleWorkflows()
+    {
+        // 污水处理流程
+        _workflows["WastewaterTreatment"] = new WorkflowDefinition
+        {
+            Name = "污水处理流程",
+            Description = "自动检测液位、流量,控制泵启停",
+            Steps = new List<WorkflowStep>
+            {
+                new() { Name = "CheckTankLevel", Description = "检查水箱液位", Action = CheckTankLevel },
+                new() { Name = "CheckFlowRate", Description = "检查进水流量", Action = CheckFlowRate },
+                new() { Name = "ControlPump", Description = "控制泵启停", Action = ControlPump },
+                new() { Name = "LogStatus", Description = "记录设备状态", Action = LogStatus }
+            }
+        };
+
+        // 报警处理流程
+        _workflows["AlarmHandling"] = new WorkflowDefinition
+        {
+            Name = "报警处理流程",
+            Description = "报警触发后的标准处理流程",
+            Steps = new List<WorkflowStep>
+            {
+                new() { Name = "AcknowledgeAlarm", Description = "确认报警", Action = AcknowledgeAlarm },
+                new() { Name = "EvaluateSeverity", Description = "评估严重程度", Action = EvaluateSeverity },
+                new() { Name = "TakeAction", Description = "执行处理动作", Action = TakeAction },
+                new() { Name = "RecordEvent", Description = "记录事件", Action = RecordEvent }
+            }
+        };
+
+        Log.Information("已注册 {Count} 个示例工作流", _workflows.Count);
+    }
+
+    /// <summary>
+    /// 执行工作流
+    /// </summary>
+    public Task<WorkflowResult> ExecuteWorkflowAsync(string workflowName, IDictionary<string, object>? context = null)
+    {
+        if (!_isInitialized)
+        {
+            throw new InvalidOperationException("工作流引擎未初始化");
+        }
+
+        if (!_workflows.TryGetValue(workflowName, out var definition))
+        {
+            throw new KeyNotFoundException($"工作流 '{workflowName}' 未找到");
+        }
+
+        Log.Information("开始执行工作流: {Name}", definition.Name);
+
+        var result = new WorkflowResult
+        {
+            WorkflowName = workflowName,
+            StartTime = DateTime.Now,
+            Status = WorkflowStatus.Running
+        };
+
+        try
+        {
+            foreach (var step in definition.Steps)
+            {
+                Log.Debug("  执行步骤: {Step}", step.Name);
+                step.Action?.Invoke(context);
+            }
+
+            result.Status = WorkflowStatus.Completed;
+            result.EndTime = DateTime.Now;
+            var duration = result.EndTime - result.StartTime;
+            Log.Information("工作流执行完成: {Name}, 耗时: {Duration}ms",
+                definition.Name, duration?.TotalMilliseconds ?? 0);
+        }
+        catch (Exception ex)
+        {
+            result.Status = WorkflowStatus.Failed;
+            result.ErrorMessage = ex.Message;
+            result.EndTime = DateTime.Now;
+            Log.Error(ex, "工作流执行失败: {Name}", definition.Name);
+        }
+
+        return Task.FromResult(result);
+    }
+
+    /// <summary>
+    /// 获取所有已注册的工作流
+    /// </summary>
+    public IReadOnlyList<WorkflowDefinition> GetAllWorkflows()
+    {
+        return _workflows.Values.ToList();
+    }
+
+    /// <summary>
+    /// 获取工作流定义
+    /// </summary>
+    public WorkflowDefinition? GetWorkflow(string name)
+    {
+        return _workflows.GetValueOrDefault(name);
+    }
+
+    /// <summary>
+    /// 注册自定义工作流
+    /// </summary>
+    public void RegisterWorkflow(string name, WorkflowDefinition definition)
+    {
+        _workflows[name] = definition;
+        Log.Information("已注册自定义工作流: {Name}", name);
+    }
+
+    // ─── 示例工作流动作 ───
+
+    private void CheckTankLevel(IDictionary<string, object>? context)
+    {
+        var level = 0f;
+        if (context != null && context.TryGetValue("TankLevel", out var obj))
+            level = Convert.ToSingle(obj);
+        Log.Debug("    检查液位: {Level:F1}%", level);
+        if (level > 80) Log.Warning("    液位过高!");
+        else if (level < 20) Log.Warning("    液位过低!");
+    }
+
+    private void CheckFlowRate(IDictionary<string, object>? context)
+    {
+        var rate = 0f;
+        if (context != null && context.TryGetValue("InflowRate", out var obj))
+            rate = Convert.ToSingle(obj);
+        Log.Debug("    检查流量: {Rate:F1} m³/h", rate);
+        if (rate > 100) Log.Warning("    流量过高!");
+    }
+
+    private void ControlPump(IDictionary<string, object>? context)
+    {
+        Log.Debug("    控制泵启停");
+    }
+
+    private void LogStatus(IDictionary<string, object>? context)
+    {
+        Log.Debug("    记录设备状态");
+    }
+
+    private void AcknowledgeAlarm(IDictionary<string, object>? context)
+    {
+        Log.Debug("    确认报警");
+    }
+
+    private void EvaluateSeverity(IDictionary<string, object>? context)
+    {
+        Log.Debug("    评估严重程度");
+    }
+
+    private void TakeAction(IDictionary<string, object>? context)
+    {
+        Log.Debug("    执行处理动作");
+    }
+
+    private void RecordEvent(IDictionary<string, object>? context)
+    {
+        Log.Debug("    记录事件");
+    }
+
+    public void Dispose()
+    {
+        if (_disposed) return;
+        _disposed = true;
+        _workflows.Clear();
+        _isInitialized = false;
+        Log.Information("工作流服务已释放");
+    }
+}
+
+/// <summary>
+/// 工作流定义
+/// </summary>
+public class WorkflowDefinition
+{
+    public string Name { get; set; } = string.Empty;
+    public string Description { get; set; } = string.Empty;
+    public List<WorkflowStep> Steps { get; set; } = new();
+}
+
+/// <summary>
+/// 工作流步骤
+/// </summary>
+public class WorkflowStep
+{
+    public string Name { get; set; } = string.Empty;
+    public string Description { get; set; } = string.Empty;
+    public Action<IDictionary<string, object>?>? Action { get; set; }
+}
+
+/// <summary>
+/// 工作流执行结果
+/// </summary>
+public class WorkflowResult
+{
+    public string WorkflowName { get; set; } = string.Empty;
+    public WorkflowStatus Status { get; set; }
+    public DateTime StartTime { get; set; }
+    public DateTime? EndTime { get; set; }
+    public string? ErrorMessage { get; set; }
+}
+
+/// <summary>
+/// 工作流状态
+/// </summary>
+public enum WorkflowStatus
+{
+    Pending,
+    Running,
+    Completed,
+    Failed,
+    Cancelled
+}

+ 2 - 0
src/YZWater.Core/YZWater.Core.csproj

@@ -9,6 +9,7 @@
 
   <ItemGroup>
     <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
+    <PackageReference Include="Quartz" Version="3.20.0" />
     <PackageReference Include="SqlSugarCore" Version="5.1.4.217" />
     <PackageReference Include="HslCommunication" Version="12.9.2" />
     <PackageReference Include="LiveChartsCore" Version="2.1.0-dev-798" />
@@ -17,6 +18,7 @@
     <PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
     <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
     <PackageReference Include="SkiaSharp" Version="4.151.1" />
+    <PackageReference Include="Stateless" Version="5.20.1" />
   </ItemGroup>
 
 </Project>