CSharp: Task Scheduler

1. 项目概述

本文介绍基于 C# .NET 10 开发的 TaskScheduler 定时任务调度器,涵盖项目结构、核心模块、配置管理、任务调度、数据持久化、日志记录以及 WinForms 图形界面等完整实现。

2. 项目结构

项目采用分层架构,主要包含以下模块:

  • Config:应用配置加载与解析(对应 Go 版 config 模块)
  • Domain:任务领域实体与状态枚举
  • JsonIO:任务 JSON 导入导出
  • Logger:日志记录模块
  • Repository:数据持久化层
  • Gui:WinForms 图形界面

3. 配置管理

配置模块负责从 app.yaml 加载应用配置,并支持蛇形命名约定转换,对应 Go 版 config/config.go 的实现。

  • AppConfig 配置类:应用信息、调度参数、数据库、日志等配置段
  • ConfigLoader 加载器:读取 YAML 文件并设置全局单例
  • SnakeCaseNamingConvention:属性名与 YAML 键的蛇形命名转换

4. 领域模型

领域层定义任务实体与状态枚举,字段与数据库表 t_task 严格一致。

  • TaskEntity:任务 ID、名称、cron 表达式、执行命令、超时、重试、启用状态、备注等
  • TaskHistoryEntity:任务执行历史记录
  • TaskStatus 枚举:空闲、运行中

5. 任务 JSON 导入导出

JsonIO 模块实现任务数据的 JSON 序列化与反序列化,支持批量导入与全量导出。

  • ExportAll:将全部任务导出为 JSON 文件
  • ImportFromFile:从 JSON 文件导入任务,返回新增、更新、跳过统计

6. 图形界面(WinForms)

Gui 模块基于 WinForms 构建,提供任务管理、仪表盘、日志查看等交互功能。

  • 任务列表:分页、搜索、状态高亮、下次执行时间计算
  • 仪表盘:统计卡片、柱状图、最近执行记录
  • 工具栏:新增、编辑、删除、立即执行、查看历史、导入导出
  • 系统托盘:关闭最小化、双击恢复、退出菜单

7. 运行效果

程序运行后展示主界面,包含任务列表、仪表盘、日志输出等区域,支持实时刷新与任务操作。

8. 总结

本文完整展示了 TaskScheduler 定时任务调度器的 C# 实现,从配置管理、领域模型到图形界面,覆盖了定时任务系统的核心功能,可作为类似项目的参考实现。

项目结构:

cs 复制代码
/*
 # encoding: utf-8
# 版权所有  2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述:Taskscheduler
# Author    : geovindu,Geovin Du 涂聚文.
# IDE       : vs2026 c# .net 10
# os        : windows 10
# database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
# Datetime  : 2026/08/28 22:16
# User      :  geovindu
# Product   : Visual Studio 2026
# Project   : Taskscheduler
# File      : AppConfig.cs
 */
namespace Taskscheduler.Config;
using System.Text;
using YamlDotNet.Serialization;
/// <summary>
/// 应用配置(对应Go版 config/config.go,app.yaml模板)
/// </summary>
public class AppConfig
{
/// <summary>
/// 应用信息
/// </summary>
public AppSection App { get; set; } = new();
/// &lt;summary&gt;
/// 调度配置
/// &lt;/summary&gt;
public SchedulerSection Scheduler { get; set; } = new();

/// &lt;summary&gt;
/// 数据库配置
/// &lt;/summary&gt;
public DatabaseSection Database { get; set; } = new();

/// &lt;summary&gt;
/// 日志配置
/// &lt;/summary&gt;
public LogSection Log { get; set; } = new();

/// &lt;summary&gt;
/// 应用信息段
/// &lt;/summary&gt;
public class AppSection
{
    /// &lt;summary&gt;
    /// 应用名称
    /// &lt;/summary&gt;
    public string AppName { get; set; } = "TaskScheduler定时任务调度器";
}

/// &lt;summary&gt;
/// 调度段
/// &lt;/summary&gt;
public class SchedulerSection
{
    /// &lt;summary&gt;
    /// 扫描间隔(毫秒)
    /// &lt;/summary&gt;
    public int ScanIntervalMs { get; set; } = 1000;

    /// &lt;summary&gt;
    /// 最大并发任务数
    /// &lt;/summary&gt;
    public int ConcurrentMax { get; set; } = 8;
}

/// &lt;summary&gt;
/// 数据库段
/// &lt;/summary&gt;
public class DatabaseSection
{
    /// &lt;summary&gt;
    /// SQLite数据库文件路径
    /// &lt;/summary&gt;
    public string DbFile { get; set; } = "./task_scheduler.db";
}

/// &lt;summary&gt;
/// 日志段
/// &lt;/summary&gt;
public class LogSection
{
    /// &lt;summary&gt;
    /// 日志目录
    /// &lt;/summary&gt;
    public string LogDir { get; set; } = "./logs";

    /// &lt;summary&gt;
    /// 单文件最大MB
    /// &lt;/summary&gt;
    public int MaxSizeMb { get; set; } = 5;

    /// &lt;summary&gt;
    /// 保留备份数
    /// &lt;/summary&gt;
    public int MaxBackups { get; set; } = 10;
}

/// &lt;summary&gt;
/// 全局配置单例(对应Go版 config.Cfg)
/// &lt;/summary&gt;
public static AppConfig Cfg { get; set; } = new();
}
/// <summary>
/// 蛇形命名约定:属性名 AppName → yaml键 app_name
/// (对应Go版 app.yaml 的 snake_case 风格)
/// </summary>
public class SnakeCaseNamingConvention : INamingConvention
{
public string Apply(string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
var sb = new StringBuilder(value.Length + 8);
for (var i = 0; i < value.Length; i++)
{
var ch = value[i];
if (char.IsUpper(ch))
{
if (i > 0)
{
sb.Append('_');
}
sb.Append(char.ToLowerInvariant(ch));
}
else
{
sb.Append(ch);
}
}
return sb.ToString();
}
/// &lt;summary&gt;
/// 逆向转换:app_name → AppName(YamlDotNet接口要求)
/// &lt;/summary&gt;
/// &lt;param name="value"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public string Reverse(string value)
{
    if (string.IsNullOrEmpty(value))
    {
        return value;
    }
    var sb = new StringBuilder(value.Length);
    var upperNext = false;
    foreach (var ch in value)
    {
        if (ch == '_')
        {
            upperNext = true;
        }
        else if (upperNext)
        {
            sb.Append(char.ToUpperInvariant(ch));
            upperNext = false;
        }
        else
        {
            sb.Append(ch);
        }
    }
    return sb.ToString();
}
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : ConfigLoader.cs
*/
namespace Taskscheduler.Config;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
/// <summary>
/// 配置加载器:从app.yaml加载并设置全局单例 AppConfig.Cfg
/// 对应Go版 config/config.go
/// </summary>
public static class ConfigLoader
{
/// <summary>
/// 从YAML文件加载配置(文件不存在时使用默认值
/// </summary>
/// <param name="yamlFile"></param>
/// <returns></returns>
public static AppConfig Load(string yamlFile = "config/app.yaml")
{
if (File.Exists(yamlFile))
{
try
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(new SnakeCaseNamingConvention())
.Build();
var cfg = deserializer.Deserialize<AppConfig>(File.ReadAllText(yamlFile));
if (cfg != null)
{
AppConfig.Cfg = cfg;
return cfg;
}
}
catch (Exception ex)
{
Console.WriteLine($"配置文件解析失败,使用默认配置: {ex.Message}");
}
}
else
{
Console.WriteLine($"未找到配置文件 {yamlFile},使用默认配置");
}
return AppConfig.Cfg;
}
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : TaskEntity.cs
*/
namespace Taskscheduler.Domain;
/// <summary>
/// 任务领域实体(对应Go版 domain/task_entity.go、Python版 domain/task_entity.py)
/// 字段与Go版/数据库表 t_task 严格一致
/// </summary>
public class TaskEntity
{
/// <summary>
/// 任务ID(0=新增,由数据库自增)
/// </summary>
public long TaskId { get; set; }
/// &lt;summary&gt;
/// 任务名称
/// &lt;/summary&gt;
public string TaskName { get; set; } = "";

/// &lt;summary&gt;
/// cron表达式(标准5字段:分 时 日 月 周)
/// &lt;/summary&gt;
public string CronExpr { get; set; } = "";

/// &lt;summary&gt;
/// 执行命令
/// &lt;/summary&gt;
public string Command { get; set; } = "";

/// &lt;summary&gt;
/// 超时秒数
/// &lt;/summary&gt;
public int TimeoutSec { get; set; }

/// &lt;summary&gt;
/// 最大重试次数
/// &lt;/summary&gt;
public int MaxRetry { get; set; }

/// &lt;summary&gt;
/// 重
/// 试间隔秒数
/// &lt;/summary&gt;
public int RetryGapSec { get; set; }

/// &lt;summary&gt;
/// 是否启用
/// &lt;/summary&gt;
public bool IsEnable { get; set; }

/// &lt;summary&gt;
/// 备注(含"一次性"则执行成功后自动禁用)
/// &lt;/summary&gt;
public string Remark { get; set; } = "";

/// &lt;summary&gt;
/// 任务状态
/// &lt;/summary&gt;
public TaskStatus TaskStatus { get; set; } = TaskStatus.Idle;

/// &lt;summary&gt;
/// 是否为一次性任务(备注含"一次性")
/// &lt;/summary&gt;
public bool IsOneTime =&gt; !string.IsNullOrEmpty(Remark) &amp;&amp; Remark.Contains("一次性", StringComparison.Ordinal);
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : TaskHistoryEntity.cs
*/
namespace Taskscheduler.Domain;
/// <summary>
/// 任务执行历史实体(对应Go版 domain/task_history_entity.go)
/// 字段与数据库表 t_task_history 严格一致
/// </summary>
public class TaskHistoryEntity
{
/// <summary>
/// 历史ID(0=新增,数据库自增)
/// </summary>
public long HistoryId { get; set; }
/// &lt;summary&gt;
/// 任务ID
/// &lt;/summary&gt;
public long TaskId { get; set; }

/// &lt;summary&gt;
/// 执行开始时间
/// &lt;/summary&gt;
public DateTime ExecuteStartTime { get; set; }

/// &lt;summary&gt;
/// 执行结束时间
/// &lt;/summary&gt;
public DateTime ExecuteEndTime { get; set; }

/// &lt;summary&gt;
/// 是否成功
/// &lt;summary&gt;
public bool IsSuccess { get; set; }

/// &lt;summary&gt;
/// 执行输出信息
/// &lt;/summary&gt;
public string ExecuteMsg { get; set; } = "";
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : TaskStatus.cs
*/
namespace Taskscheduler.Domain;
/// <summary>
/// 任务状态枚举(对应Go版 domain/task_status.go)
/// </summary>
public enum TaskStatus
{
/// <summary>
/// 空闲:等待调度
/// </summary>
Idle = 0,
/// &lt;summary&gt;
/// 运行中
/// &lt;/summary&gt;
Running = 1,
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : TaskJsonIO.cs
*/
namespace Taskscheduler.JsonIO;
using System.Text.Json;
using System.Text.Json.Serialization;
using Taskscheduler.Domain;
using Taskscheduler.Repository;
/// <summary>
/// 任务JSON导入导出(对应Go版 jsonio/task_json_io.go)
/// 导出:全部任务导出为JSON文件;导入:读取JSON批量新增/更新任务
/// </summary>
public class TaskJsonIO
{
private readonly TaskRepo _repo = new();
/// &lt;summary&gt;
/// &gt;JSON任务条目(与Go版字段完全一致)
/// &lt;/summary&gt;
public class TaskJsonItem
{
    [JsonPropertyName("task_id")]
    public long TaskId { get; set; }

    [JsonPropertyName("task_name")]
    public string TaskName { get; set; } = "";

    [JsonPropertyName("cron_expr")]
    public string CronExpr { get; set; } = "";

    [JsonPropertyName("command")]
    public string Command { get; set; } = "";

    [JsonPropertyName("timeout_sec")]
    public int TimeoutSec { get; set; }

    [JsonPropertyName("max_retry")]
    public int MaxRetry { get; set; }

    [JsonPropertyName("retry_gap_sec")]
    public int RetryGapSec { get; set; }

    [JsonPropertyName("is_enable")]
    public bool IsEnable { get; set; }

    [JsonPropertyName("remark")]
    public string Remark { get; set; } = "";

    [JsonPropertyName("task_status")]
    public string TaskStatus { get; set; } = "IDLE";
}

/// &lt;summary&gt;
/// 导入统计结果
/// &lt;/summary&gt;
/// &lt;param name="Total"&gt;&lt;/param&gt;
/// &lt;param name="Insert"&gt;&lt;/param&gt;
/// &lt;param name="Update"&gt;&lt;/param&gt;
/// &lt;param name="Skip"&gt;&lt;/param&gt;
public record ExportStat(int Total, int Insert, int Update, int Skip);

/// &lt;summary&gt;
/// 导出全部任务到JSON文件
/// &lt;/summary&gt;
/// &lt;param name="filePath"&gt;&lt;/param&gt;
public void ExportAll(string filePath)
{
    var items = _repo.GetList().Select(e =&gt; new TaskJsonItem
    {
        TaskId = e.TaskId,
        TaskName = e.TaskName,
        CronExpr = e.CronExpr,
        Command = e.Command,
        TimeoutSec = e.TimeoutSec,
        MaxRetry = e.MaxRetry,
        RetryGapSec = e.RetryGapSec,
        IsEnable = e.IsEnable,
        Remark = e.Remark,
        TaskStatus = e.TaskStatus.ToString(),
    }).ToList();
    var json = JsonSerializer.Serialize(items, new JsonSerializerOptions { WriteIndented = true });
    File.WriteAllText(filePath, json);
    Taskscheduler.Logger.AppLogger.Info($"导出完成:{items.Count} 条任务 → {filePath}");
}

/// &lt;summary&gt;
/// 从JSON文件导入任务,返回导入统计
/// &lt;/summary&gt;
/// &lt;param name="filePath"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
/// &lt;exception cref="InvalidOperationException"&gt;&lt;/exception&gt;
public ExportStat ImportFromFile(string filePath)
{
    var items = JsonSerializer.Deserialize&lt;List&lt;TaskJsonItem&gt;&gt;(File.ReadAllText(filePath));
    if (items == null)
    {
        throw new InvalidOperationException("JSON内容为空或格式错误");
    }
    var insert = 0;
    var update = 0;
    var skip = 0;
    foreach (var item in items)
    {
        // 校验必填字段
        if (string.IsNullOrEmpty(item.TaskName) ||
            string.IsNullOrEmpty(item.CronExpr) ||
            string.IsNullOrEmpty(item.Command))
        {
            Taskscheduler.Logger.AppLogger.Warn($"跳过无效条目(名称/cron/命令为空):{item.TaskName}");
            skip++;
            continue;
        }
        var entity = new TaskEntity
        {
            TaskId = item.TaskId,
            TaskName = item.TaskName,
            CronExpr = item.CronExpr,
            Command = item.Command,
            TimeoutSec = item.TimeoutSec,
            MaxRetry = item.MaxRetry,
            RetryGapSec = item.RetryGapSec,
            IsEnable = item.IsEnable,
            Remark = item.Remark,
            TaskStatus = string.Equals(item.TaskStatus, "RUNNING", StringComparison.OrdinalIgnoreCase)
                ? TaskStatus.Running : TaskStatus.Idle,
        };
        try
        {
            _repo.Save(entity);
            if (item.TaskId &lt;= 0)
            {
                insert++;
            }
            else
            {
                update++;
            }
        }
        catch (Exception ex)
        {
            Taskscheduler.Logger.AppLogger.Error($"导入任务失败: {ex.Message}");
            skip++;
        }
    }
    var stat = new ExportStat(items.Count, insert, update, skip);
    Taskscheduler.Logger.AppLogger.Info(
        $"导入完成:总条数={stat.Total} 新增={stat.Insert} 更新={stat.Update} 跳过={stat.Skip}");
    return stat;
}
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : AppLogger.cs
*/
namespace Taskscheduler.Logger;
using System.Text;
/// <summary>
/// 应用日志器(对应Go版 logger/logger.go)
/// 控制台 + 滚动文件输出(按大小切割,保留备份)+ 可选GUI回调
/// </summary>
public static class AppLogger
{
private static readonly object LockObj = new();
private static StreamWriter? _fileWriter;
private static string _logFile = "app.log";
private static int _maxSizeBytes = 5 * 1024 * 1024;
private static int _maxBackups = 10;
/// &lt;summary&gt;
/// 日志事件(GUI订阅,向日志框追加文本)
/// &lt;/summary&gt;
public static event Action&lt;string&gt;? Logged;

/// &lt;summary&gt;初始化日志器&lt;/summary&gt;
/// &lt;param name="logDir"&gt;日志目录&lt;/param&gt;
/// &lt;param name="extraSink"&gt;额外输出目标(如GUI回调),可为null&lt;/param&gt;
public static void Init(string logDir, Action&lt;string&gt;? extraSink = null)
{
    lock (LockObj)
    {
        if (!Directory.Exists(logDir))
        {
            Directory.CreateDirectory(logDir);
        }
        _logFile = Path.Combine(logDir, "app.log");
        // 控制台输出使用UTF-8(保证中文正常)
        Console.OutputEncoding = Encoding.UTF8;
        _fileWriter = new StreamWriter(_logFile, append: true, Encoding.UTF8) { AutoFlush = true };
        if (extraSink != null)
        {
            Logged += extraSink;
        }
        Info("日志系统初始化完成");
    }
}

/// &lt;summary&gt;
/// 记录INFO日志
/// &lt;/summary&gt;
/// &lt;param name="msg"&gt;&lt;/param&gt;
public static void Info(string msg) =&gt; Write("INFO", msg);

/// &lt;summary&gt;
/// 记录ERROR日志
/// &lt;/summary&gt;
/// &lt;param name="msg"&gt;&lt;/param&gt;
public static void Error(string msg) =&gt; Write("ERROR", msg);

/// &lt;summary&gt;
/// 记录WARN日志
/// &lt;/summary&gt;
/// &lt;param name="msg"&gt;&lt;/param&gt;
public static void Warn(string msg) =&gt; Write("WARN", msg);

/// &lt;summary&gt;
/// 写入日志行(含滚动切割)
/// &lt;/summary&gt;
/// &lt;param name="level"&gt;&lt;/param&gt;
/// &lt;param name="msg"&gt;&lt;/param&gt;
private static void Write(string level, string msg)
{
    var line = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss.fff} [{level}] {msg}";
    lock (LockObj)
    {
        RotateIfNeeded();
        _fileWriter?.WriteLine(line);
        Console.WriteLine(line);
        // 通知GUI
        Logged?.Invoke(line + Environment.NewLine);
    }
}

/// &lt;summary&gt;
/// 按大小滚动切割日志文件
/// &lt;/summary&gt;
private static void RotateIfNeeded()
{
    if (_fileWriter == null || !File.Exists(_logFile))
    {
        return;
    }
    var info = new FileInfo(_logFile);
    if (info.Length &lt; _maxSizeBytes)
    {
        return;
    }
    // 关闭当前写入器
    _fileWriter.Dispose();
    _fileWriter = null;
    // 删除最旧备份,后移
    for (var i = _maxBackups; i &gt;= 1; i--)
    {
        var src = i == 1 ? _logFile : $"{_logFile}.{i - 1}";
        var dst = $"{_logFile}.{i}";
        if (File.Exists(src))
        {
            if (File.Exists(dst))
            {
                File.Delete(dst);
            }
            File.Move(src, dst);
        }
    }
    // 重新打开新文件
    _fileWriter = new StreamWriter(_logFile, append: false, Encoding.UTF8) { AutoFlush = true };
    Write0("日志文件已滚动切割");
}

/// &lt;summary&gt;
/// 内部直接写行(用于滚动后说明),避免递归
/// &lt;/summary&gt;
/// &lt;param name="msg"&gt;&lt;/param&gt;
private static void Write0(string msg)
{
    var line = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss.fff} [INFO] {msg}";
    _fileWriter?.WriteLine(line);
    Console.WriteLine(line);
}
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : TaskHistoryRepo.cs
*/
namespace Taskscheduler.Repository;
using Microsoft.Data.Sqlite;
using Taskscheduler.Domain;
using Taskscheduler.Storage;
/// <summary>
/// 任务历史仓储(对应Go版 repository/task_history_repo.go)
/// 写入/查询执行历史,含仪表盘统计方法
/// </summary>
public class TaskHistoryRepo
{
/// <summary>
/// 新增一条执行历史,返回带自增ID的实体
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public TaskHistoryEntity Insert(TaskHistoryEntity entity)
{
using var conn = Db.OpenConnection();
using var cmd = conn.CreateCommand();
cmd.CommandText = """
INSERT INTO t_task_history (task_id, execute_start_time, execute_end_time, is_success, execute_msg)
VALUES (@taskId, @start, @end, @success, @msg)
""";
cmd.Parameters.AddWithValue("@taskId", entity.TaskId);
cmd.Parameters.AddWithValue("@start", Db.FormatTime(entity.ExecuteStartTime));
cmd.Parameters.AddWithValue("@end", Db.FormatTime(entity.ExecuteEndTime));
cmd.Parameters.AddWithValue("@success", entity.IsSuccess ? 1 : 0);
cmd.Parameters.AddWithValue("@msg", entity.ExecuteMsg ?? "");
cmd.ExecuteNonQuery();
using var idCmd = conn.CreateCommand();
idCmd.CommandText = "SELECT last_insert_rowid()";
entity.HistoryId = (long)(idCmd.ExecuteScalar() ?? 0L);
return entity;
}
/// &lt;summary&gt;
/// 按任务ID查询执行历史(ID倒序,最新在前)
/// &lt;/summary&gt;
/// &lt;param name="taskId"&gt;&lt;/param&gt;
/// &lt;param name="limit"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public List&lt;TaskHistoryEntity&gt; ListByTaskId(long taskId, int limit = 50)
{
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = """
        SELECT id, task_id, execute_start_time, execute_end_time, is_success, execute_msg
        FROM t_task_history WHERE task_id = @taskId ORDER BY id DESC LIMIT @limit
        """;
    cmd.Parameters.AddWithValue("@taskId", taskId);
    cmd.Parameters.AddWithValue("@limit", limit);
    return ReadList(cmd);
}

/// &lt;summary&gt;
/// 查询全局最近N条执行记录(仪表盘使用)
/// &lt;/summary&gt;
/// &lt;param name="limit"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public List&lt;TaskHistoryEntity&gt; ListRecent(int limit = 20)
{
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = """
        SELECT id, task_id, execute_start_time, execute_end_time, is_success, execute_msg
        FROM t_task_history ORDER BY id DESC LIMIT @limit
        """;
    cmd.Parameters.AddWithValue("@limit", limit);
    return ReadList(cmd);
}

/// &lt;summary&gt;
/// 统计执行总次数、成功次数、失败次数
/// &lt;/summary&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public (long Total, long Success, long Fail) CountStats()
{
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = "SELECT COUNT(*), COALESCE(SUM(is_success),0) FROM t_task_history";
    using var reader = cmd.ExecuteReader();
    if (!reader.Read())
    {
        return (0, 0, 0);
    }
    var total = reader.GetInt64(0);
    var success = reader.GetInt64(1);
    return (total, success, total - success);
}

/// &lt;summary&gt;
/// 统计今日执行次数
/// &lt;/summary&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public long CountToday()
{
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = "SELECT COUNT(*) FROM t_task_history WHERE execute_start_time LIKE @prefix";
    cmd.Parameters.AddWithValue("@prefix", Db.FormatTime(DateTime.Now)[..10] + "%");
    return (long)(cmd.ExecuteScalar() ?? 0L);
}

/// &lt;summary&gt;
/// 某天执行次数(柱状图数据)
/// &lt;/summary&gt;
/// &lt;param name="Date"&gt;&lt;/param&gt;
/// &lt;param name="Count"&gt;&lt;/param&gt;
public record DayCount(string Date, long Count);

/// &lt;summary&gt;
/// 统计最近days天(含今天)每天执行次数,无记录的天补0
/// &lt;/summary&gt;
/// &lt;param name="days"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public List&lt;DayCount&gt; CountByDay(int days = 7)
{
    // 生成日期序列
    var result = new List&lt;DayCount&gt;(days);
    var indexMap = new Dictionary&lt;string, int&gt;(days);
    for (var i = 0; i &lt; days; i++)
    {
        var date = DateTime.Now.AddDays(-(days - 1 - i)).ToString("yyyy-MM-dd");
        result.Add(new DayCount(date, 0));
        indexMap[date] = i;
    }
    // 查询整个窗口内的聚合(左闭右开:[首日00:00:00, 明日00:00:00))
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = """
        SELECT substr(execute_start_time,1,10) AS d, COUNT(*)
        FROM t_task_history
        WHERE execute_start_time &gt;= @start AND execute_start_time &lt; @end
        GROUP BY d
        """;
    cmd.Parameters.AddWithValue("@start", result[0].Date + " 00:00:00");
    cmd.Parameters.AddWithValue("@end", DateTime.Now.AddDays(1).ToString("yyyy-MM-dd") + " 00:00:00");
    using var reader = cmd.ExecuteReader();
    while (reader.Read())
    {
        var day = reader.GetString(0);
        if (indexMap.TryGetValue(day, out var idx))
        {
            result[idx] = new DayCount(day, reader.GetInt64(1));
        }
    }
    return result;
}

/// &lt;summary&gt;
/// 读取多行结果
/// &lt;/summary&gt;
/// &lt;param name="cmd"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
private static List&lt;TaskHistoryEntity&gt; ReadList(SqliteCommand cmd)
{
    using var reader = cmd.ExecuteReader();
    var list = new List&lt;TaskHistoryEntity&gt;();
    while (reader.Read())
    {
        list.Add(new TaskHistoryEntity
        {
            HistoryId = reader.GetInt64(0),
            TaskId = reader.GetInt64(1),
            ExecuteStartTime = Db.ParseTime(reader.GetString(2)),
            ExecuteEndTime = Db.ParseTime(reader.GetString(3)),
            IsSuccess = reader.GetInt32(4) == 1,
            ExecuteMsg = reader.IsDBNull(5) ? "" : reader.GetString(5),
        });
    }
    return list;
}
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : TaskRepo.cs
*/
namespace Taskscheduler.Repository;
using Microsoft.Data.Sqlite;
using Taskscheduler.Domain;
using Taskscheduler.Storage;
/// <summary>
/// 任务仓储(对应Go版 repository/task_repo.go)
/// 提供 t_task 表完整CRUD,行记录↔领域实体双向转换
/// </summary>
public class TaskRepo
{
/// <summary>
/// 查询全部任务(按ID升序)
/// </summary>
public List<TaskEntity> GetList()
{
using var conn = Db.OpenConnection();
using var cmd = conn.CreateCommand();
cmd.CommandText = """
SELECT id, task_name, cron_expr, command, timeout_sec, max_retry,
retry_gap_sec, is_enable, remark, task_status
FROM t_task ORDER BY id
""";
using var reader = cmd.ExecuteReader();
var list = new List<TaskEntity>();
while (reader.Read())
{
list.Add(MapRow(reader));
}
return list;
}
/// &lt;summary&gt;
/// 查询全部启用任务
///
/// &lt;/summary&gt;
public List&lt;TaskEntity&gt; GetEnabledList()
{
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = """
        SELECT id, task_name, cron_expr, command, timeout_sec, max_retry,
               retry_gap_sec, is_enable, remark, task_status
        FROM t_task WHERE is_enable = 1 ORDER BY id
        """;
    using var reader = cmd.ExecuteReader();
    var list = new List&lt;TaskEntity&gt;();
    while (reader.Read())
    {
        list.Add(MapRow(reader));
    }
    return list;
}

/// &lt;summary&gt;
/// 按关键字统计任务总数(关键字匹配任务名称/备注,为空返回全量)
/// &lt;/summary&gt;
/// &lt;param name="keyword"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public long Count(string? keyword = null)
{
    var kw = string.IsNullOrWhiteSpace(keyword) ? null : keyword.Trim();
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = kw == null
        ? "SELECT COUNT(*) FROM t_task"
        : "SELECT COUNT(*) FROM t_task WHERE task_name LIKE @kw OR remark LIKE @kw";
    if (kw != null)
    {
        cmd.Parameters.AddWithValue("@kw", $"%{kw}%");
    }
    return (long)(cmd.ExecuteScalar() ?? 0L);
}

/// &lt;summary&gt;
/// 关键字分页查询任务列表(page从1开始,pageSize&gt;0)
/// &lt;/summary&gt;
/// &lt;param name="keyword"&gt;&lt;/param&gt;
/// &lt;param name="page"&gt;&lt;/param&gt;
/// &lt;param name="pageSize"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public List&lt;TaskEntity&gt; SearchPage(string? keyword, int page, int pageSize)
{
    if (page &lt; 1)
    {
        page = 1;
    }
    if (pageSize &lt; 1)
    {
        pageSize = 10;
    }
    var kw = string.IsNullOrWhiteSpace(keyword) ? null : keyword.Trim();
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = $"""
        SELECT id, task_name, cron_expr, command, timeout_sec, max_retry,
               retry_gap_sec, is_enable, remark, task_status
        FROM t_task
        {(kw == null ? "" : "WHERE task_name LIKE @kw OR remark LIKE @kw")}
        ORDER BY id
        LIMIT @limit OFFSET @offset
        """;
    if (kw != null)
    {
        cmd.Parameters.AddWithValue("@kw", $"%{kw}%");
    }
    cmd.Parameters.AddWithValue("@limit", pageSize);
    cmd.Parameters.AddWithValue("@offset", (page - 1) * pageSize);
    using var reader = cmd.ExecuteReader();
    var list = new List&lt;TaskEntity&gt;();
    while (reader.Read())
    {
        list.Add(MapRow(reader));
    }
    return list;
}

/// &lt;summary&gt;
/// 按ID查询任务
/// &lt;/summary&gt;
/// &lt;param name="taskId"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public TaskEntity? GetById(long taskId)
{
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = """
        SELECT id, task_name, cron_expr, command, timeout_sec, max_retry,
               retry_gap_sec, is_enable, remark, task_status
        FROM t_task WHERE id = @id
        """;
    cmd.Parameters.AddWithValue("@id", taskId);
    using var reader = cmd.ExecuteReader();
    return reader.Read() ? MapRow(reader) : null;
}

/// &lt;summary&gt;
/// 保存任务:TaskId&lt;=0新增,否则更新;返回带ID的实体
/// &lt;/summary&gt;
/// &lt;param name="entity"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public TaskEntity Save(TaskEntity entity)
{
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    if (entity.TaskId &lt;= 0)
    {
        cmd.CommandText = """
            INSERT INTO t_task (task_name, cron_expr, command, timeout_sec, max_retry,
                                retry_gap_sec, is_enable, remark, task_status)
            VALUES (@name, @cron, @cmd, @timeout, @retry, @gap, @enable, @remark, @status)
            """;
    }
    else
    {
        cmd.CommandText = """
            UPDATE t_task SET task_name=@name, cron_expr=@cron, command=@cmd,
                   timeout_sec=@timeout, max_retry=@retry, retry_gap_sec=@gap,
                   is_enable=@enable, remark=@remark, task_status=@status
            WHERE id=@id
            """;
        cmd.Parameters.AddWithValue("@id", entity.TaskId);
    }
    cmd.Parameters.AddWithValue("@name", entity.TaskName);
    cmd.Parameters.AddWithValue("@cron", entity.CronExpr);
    cmd.Parameters.AddWithValue("@cmd", entity.Command);
    cmd.Parameters.AddWithValue("@timeout", entity.TimeoutSec);
    cmd.Parameters.AddWithValue("@retry", entity.MaxRetry);
    cmd.Parameters.AddWithValue("@gap", entity.RetryGapSec);
    cmd.Parameters.AddWithValue("@enable", entity.IsEnable ? 1 : 0);
    cmd.Parameters.AddWithValue("@remark", entity.Remark ?? "");
    cmd.Parameters.AddWithValue("@status", entity.TaskStatus.ToString());
    cmd.ExecuteNonQuery();
    if (entity.TaskId &lt;= 0)
    {
        using var idCmd = conn.CreateCommand();
        idCmd.CommandText = "SELECT last_insert_rowid()";
        entity.TaskId = (long)(idCmd.ExecuteScalar() ?? 0L);
    }
    return entity;
}

/// &lt;summary&gt;
/// 按ID删除任务
/// &lt;/summary&gt;
/// &lt;param name="taskId"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public bool DeleteById(long taskId)
{
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = "DELETE FROM t_task WHERE id = @id";
    cmd.Parameters.AddWithValue("@id", taskId);
    return cmd.ExecuteNonQuery() &gt; 0;
}

/// &lt;summary&gt;
/// 更新任务状态
/// &lt;/summary&gt;
/// &lt;param name="taskId"&gt;&lt;/param&gt;
/// &lt;param name="status"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public bool UpdateStatus(long taskId, TaskStatus status)
{
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = "UPDATE t_task SET task_status = @status WHERE id = @id";
    cmd.Parameters.AddWithValue("@status", status.ToString());
    cmd.Parameters.AddWithValue("@id", taskId);
    return cmd.ExecuteNonQuery() &gt; 0;
}

/// &lt;summary&gt;
/// 更新任务启用状态
/// &lt;/summary&gt;
/// &lt;param name="taskId"&gt;&lt;/param&gt;
/// &lt;param name="enable"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public bool UpdateEnable(long taskId, bool enable)
{
    using var conn = Db.OpenConnection();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = "UPDATE t_task SET is_enable = @enable WHERE id = @id";
    cmd.Parameters.AddWithValue("@enable", enable ? 1 : 0);
    cmd.Parameters.AddWithValue("@id", taskId);
    return cmd.ExecuteNonQuery() &gt; 0;
}

/// &lt;summary&gt;
/// 数据行→领域实体(列顺序与SQL固定一致)
/// &lt;/summary&gt;
/// &lt;param name="reader"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
private static TaskEntity MapRow(SqliteDataReader reader)
{
    var status = reader.GetString(9).ToUpperInvariant();
    return new TaskEntity
    {
        TaskId = reader.GetInt64(0),
        TaskName = reader.GetString(1),
        CronExpr = reader.GetString(2),
        Command = reader.GetString(3),
        TimeoutSec = reader.GetInt32(4),
        MaxRetry = reader.GetInt32(5),
        RetryGapSec = reader.GetInt32(6),
        IsEnable = reader.GetInt32(7) == 1,
        Remark = reader.IsDBNull(8) ? "" : reader.GetString(8),
        TaskStatus = status == "RUNNING" ? TaskStatus.Running : TaskStatus.Idle,
    };
}
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : SubprocessRunner.cs
*/
namespace Taskscheduler.Runner;
using System.Diagnostics;
/// <summary>
/// 子进程命令执行器(对应Go版 runner/subprocess_runner.go)
/// 调用系统shell执行命令、超时控制、捕获stdout+stderr合并输出
/// Windows 使用 cmd /C,Linux/macOS 使用 sh -c
/// </summary>
public class SubprocessRunner
{
/// <summary>
/// 超时秒数</summary>
public int TimeoutSec { get; }
/// &lt;summary&gt;
/// 创建执行器
/// &lt;/summary&gt;
public SubprocessRunner(int timeoutSec)
{
    TimeoutSec = timeoutSec;
}

/// &lt;summary&gt;
/// 执行结果
///
/// &lt;/summary&gt;
public class RunResult
{
    /// &lt;summary&gt;
    /// 是否成功(退出码0)
    /// &lt;/summary&gt;
    public bool Success { get; set; }

    /// &lt;summary&gt;
    /// 合并输出文本(stdout+stderr)
    ///
    /// &lt;/summary&gt;
    public string Output { get; set; } = "";
}

/// &lt;summary&gt;执行命令&lt;/summary&gt;
/// &lt;param name="taskId"&gt;任务ID(仅日志标识)&lt;/param&gt;
/// &lt;param name="command"&gt;待执行命令&lt;/param&gt;
public RunResult Run(long taskId, string command)
{
    var isWindows = OperatingSystem.IsWindows();
    using var process = new Process();
    process.StartInfo.FileName = isWindows ? "cmd.exe" : "/bin/sh";
    process.StartInfo.Arguments = isWindows ? $"/C {command}" : $"-c \"{command}\"";
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;
    process.StartInfo.CreateNoWindow = true;

    try
    {
        process.Start();
    }
    catch (Exception ex)
    {
        return new RunResult { Success = false, Output = $"启动进程失败: {ex.Message}" };
    }

    // 异步读取,避免缓冲区满死锁
    var stdoutTask = process.StandardOutput.ReadToEndAsync();
    var stderrTask = process.StandardError.ReadToEndAsync();

    // 超时控制
    if (!process.WaitForExit(TimeoutSec * 1000))
    {
        try
        {
            process.Kill(entireProcessTree: true);
        }
        catch
        {
            // 进程可能已退出
        }
        return new RunResult
        {
            Success = false,
            Output = $"【超时】任务task_id={taskId}超过{TimeoutSec}秒被强制终止",
        };
    }

    var output = stdoutTask.GetAwaiter().GetResult() + stderrTask.GetAwaiter().GetResult();
    return new RunResult
    {
        Success = process.ExitCode == 0,
        Output = process.ExitCode == 0 ? output : $"返回码:{process.ExitCode}\n{output}",
    };
}
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : BackgroundScheduler.cs
*/
namespace Taskscheduler.Scheduler;
using Taskscheduler.Application;
using Taskscheduler.Config;
using Taskscheduler.Domain;
using Taskscheduler.Logger;
using Taskscheduler.Repository;
/// <summary>
/// 后台调度线程(对应Go版 scheduler/background_scheduler.go)
/// 职责:
///   1. 定时扫描启用任务
///   2. cron时间判断(上次执行时间为基准,未执行过以启动时间为基准,不补跑)
///   3. 并发数量控制(SemaphoreSlim + 运行标记集合)
///   4. 调用应用层执行任务、更新任务运行状态
///   5. 一次性任务(备注含"一次性")执行成功后自动禁用
/// </summary>
public class BackgroundScheduler
{
private readonly CancellationTokenSource _cts = new();
private readonly DateTime _bootTime = DateTime.Now;
private readonly Dictionary<long, DateTime> _lastRunMap = new();
private readonly HashSet<long> _runningIds = new();
private readonly SemaphoreSlim _semaphore;
private readonly int _scanIntervalMs;
private readonly TaskRepo _taskRepo = new();
private readonly TaskSchedulerService _service = new();
private readonly object _lock = new();
/// &lt;summary&gt;
/// 创建调度线程(从全局配置读取参数)
/// &lt;/summary&gt;
public BackgroundScheduler()
{
    _scanIntervalMs = AppConfig.Cfg.Scheduler.ScanIntervalMs;
    _semaphore = new SemaphoreSlim(AppConfig.Cfg.Scheduler.ConcurrentMax);
}

/// &lt;summary&gt;
/// 启动调度主循环(后台任务,不阻塞)
/// &lt;/summary&gt;
public void Start()
{
    _ = Task.Run(RunLoopAsync);
    AppLogger.Info($"调度主线程启动,扫描间隔={_scanIntervalMs}ms,最大并发={AppConfig.Cfg.Scheduler.ConcurrentMax}");
}

/// &lt;summary&gt;
/// 停止调度
/// &lt;/summary&gt;
public void Stop()
{
    _cts.Cancel();
    AppLogger.Info("调度主线程已退出");
}

/// &lt;summary&gt;
/// 调度主循环
///
/// &lt;/summary&gt;
private async Task RunLoopAsync()
{
    try
    {
        while (!_cts.IsCancellationRequested)
        {
            ScanOnce(DateTime.Now);
            await Task.Delay(_scanIntervalMs, _cts.Token);
        }
    }
    catch (OperationCanceledException)
    {
        // 正常停止
    }
}

/// &lt;summary&gt;
/// 单次调度扫描
/// &lt;/summary&gt;
/// &lt;param name="now"&gt;&lt;/param&gt;
private void ScanOnce(DateTime now)
{
    // 1. 获取启用任务并筛选满足cron条件的
    var pending = new List&lt;TaskEntity&gt;();
    foreach (var task in _taskRepo.GetEnabledList())
    {
        if (CanRunTask(task, now))
        {
            pending.Add(task);
        }
    }
    if (pending.Count == 0)
    {
        return;
    }

    // 2. 并发执行(每个任务独立线程)
    foreach (var task in pending)
    {
        if (!MarkRunning(task.TaskId))
        {
            continue; // 已被其他协程占用
        }
        _ = Task.Run(async () =&gt;
        {
            await _semaphore.WaitAsync();
            try
            {
                await WorkerAsync(task);
            }
            finally
            {
                _semaphore.Release();
                UnmarkRunning(task.TaskId);
            }
        });
    }
}

/// &lt;summary&gt;
/// 判断任务当前是否应执行
/// &lt;/summary&gt;
/// &lt;param name="task"&gt;&lt;/param&gt;
/// &lt;param name="now"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
private bool CanRunTask(TaskEntity task, DateTime now)
{
    if (!task.IsEnable || IsRunning(task.TaskId))
    {
        return false;
    }
    // 基准时间:上次执行时间,否则启动时间
    DateTime baseTime;
    lock (_lock)
    {
        baseTime = _lastRunMap.TryGetValue(task.TaskId, out var last) ? last : _bootTime;
    }
    try
    {
        var next = CronExpression.Parse(task.CronExpr).GetNext(baseTime);
        return next.HasValue &amp;&amp; now &gt;= next.Value;
    }
    catch (Exception ex)
    {
        AppLogger.Error($"cron表达式解析失败 task_id={task.TaskId} cron={task.CronExpr} err={ex.Message}");
        return false;
    }
}

/// &lt;summary&gt;
/// 任务是否运行中
/// &lt;/summary&gt;
/// &lt;param name="taskId"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
private bool IsRunning(long taskId)
{
    lock (_lock)
    {
        return _runningIds.Contains(taskId);
    }
}

/// &lt;summary&gt;
/// 标记任务运行中,返回是否成功占用
/// &lt;/summary&gt;
/// &lt;param name="taskId"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
private bool MarkRunning(long taskId)
{
    lock (_lock)
    {
        if (_runningIds.Contains(taskId))
        {
            return false;
        }
        _runningIds.Add(taskId);
        return true;
    }
}

/// &lt;summary&gt;
/// 释放任务运行标记
/// &lt;/summary&gt;
/// &lt;param name="taskId"&gt;&lt;/param&gt;
private void UnmarkRunning(long taskId)
{
    lock (_lock)
    {
        _runningIds.Remove(taskId);
    }
}

/// &lt;summary&gt;
/// 单个任务工作协程
/// &lt;/summary&gt;
/// &lt;param name="task"&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
private async Task WorkerAsync(TaskEntity task)
{
    try
    {
        // 1. 更新状态 RUNNING
        _taskRepo.UpdateStatus(task.TaskId, TaskStatus.Running);
        // 2. 执行任务(子进程 + 重试 + 写历史)
        _service.ExecuteSingleTask(task);
        // 3. 恢复状态 IDLE
        _taskRepo.UpdateStatus(task.TaskId, TaskStatus.Idle);
        // 4. 记录本次执行时间
        lock (_lock)
        {
            _lastRunMap[task.TaskId] = DateTime.Now;
        }
        // 5. 一次性任务执行成功后自动禁用(真正"只执行一次")
        if (task.IsOneTime)
        {
            _taskRepo.UpdateEnable(task.TaskId, false);
            AppLogger.Info($"一次性任务 task_id={task.TaskId}【{task.TaskName}】执行完成,已自动禁用,不再调度");
        }
    }
    catch (Exception ex)
    {
        AppLogger.Error($"任务执行异常 task_id={task.TaskId}: {ex}");
        try
        {
            _taskRepo.UpdateStatus(task.TaskId, TaskStatus.Idle);
        }
        catch
        {
            // 忽略状态恢复失败
        }
    }
    await Task.CompletedTask;
}
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : CronExpression.cs
*/
namespace Taskscheduler.Scheduler;
using System.Globalization;
/// <summary>
/// 标准5字段cron表达式(分 时 日 月 周)
/// 自实现解析与下次触发计算,行为对齐 robfig/cron(Go版):
///   - 支持 、数字、列表(1,2,3)、范围(1-5)、步长(/5、1-10/2)
///   - 日字段与周字段:任一为时只按另一个匹配;均非时按"或"匹配
///   - 周:0=周日,7也视为周日
/// </summary>
public class CronExpression
{
private readonly int[] _minutes;
private readonly int[] _hours;
private readonly int[] _daysOfMonth;
private readonly int[] _months;
private readonly int[] _daysOfWeek;
private readonly bool _domStar;
private readonly bool _dowStar;
/// <summary>
///
/// </summary>
/// <param name="minutes"></param>
/// <param name="hours"></param>
/// <param name="daysOfMonth"></param>
/// <param name="months"></param>
/// <param name="daysOfWeek"></param>
/// <param name="domStar"></param>
/// <param name="dowStar"></param>
private CronExpression(int[] minutes, int[] hours, int[] daysOfMonth, int[] months,
int[] daysOfWeek, bool domStar, bool dowStar)
{
_minutes = minutes;
_hours = hours;
_daysOfMonth = daysOfMonth;
_months = months;
_daysOfWeek = daysOfWeek;
_domStar = domStar;
_dowStar = dowStar;
}
/// &lt;summary&gt;
/// 解析标准5字段cron表达式
/// &lt;/summary&gt;
public static CronExpression Parse(string expr)
{
    var parts = expr.Split(' ', StringSplitOptions.RemoveEmptyEntries);
    if (parts.Length != 5)
    {
        throw new ArgumentException($"cron表达式必须为5段(分 时 日 月 周): {expr}");
    }
    var minutes = ParseField(parts[0], 0, 59);
    var hours = ParseField(parts[1], 0, 23);
    var dom = ParseField(parts[2], 1, 31);
    var months = ParseField(parts[3], 1, 12);
    var dow = ParseField(parts[4], 0, 7)
        .Select(d =&gt; d % 7)      // 7=周日转0
        .Distinct()
        .OrderBy(d =&gt; d)
        .ToArray();
    return new CronExpression(minutes, hours, dom, months, dow,
        parts[2] == "*", parts[4] == "*");
}

/// &lt;summary&gt;
/// 解析单个字段为允许值集合
/// &lt;/summary&gt;
private static int[] ParseField(string field, int min, int max)
{
    var values = new SortedSet&lt;int&gt;();
    foreach (var part in field.Split(','))
    {
        var step = 1;
        var range = part;
        var slashIdx = part.IndexOf('/');
        if (slashIdx &gt;= 0)
        {
            step = int.Parse(part[(slashIdx + 1)..], CultureInfo.InvariantCulture);
            range = part[..slashIdx];
        }
        int start, end;
        if (range == "*")
        {
            start = min;
            end = max;
        }
        else
        {
            var dashIdx = range.IndexOf('-');
            if (dashIdx &gt;= 0)
            {
                start = int.Parse(range[..dashIdx], CultureInfo.InvariantCulture);
                end = int.Parse(range[(dashIdx + 1)..], CultureInfo.InvariantCulture);
            }
            else
            {
                start = end = int.Parse(range, CultureInfo.InvariantCulture);
            }
        }
        for (var v = start; v &lt;= end; v += step)
        {
            if (v &gt;= min &amp;&amp; v &lt;= max)
            {
                values.Add(v);
            }
        }
    }
    if (values.Count == 0)
    {
        throw new ArgumentException($"cron字段无有效值: {field}");
    }
    return values.ToArray();
}

/// &lt;summary&gt;
/// 计算从from之后的下一次触发时间(不含from本身)
/// 标准5字段cron匹配时刻秒恒为0,因此从"严格大于from的下一整分钟"开始扫描
/// &lt;/summary&gt;
public DateTime? GetNext(DateTime from)
{
    // 取严格大于from的下一整分钟(秒=0)
    var t = from.AddSeconds(1);
    t = new DateTime(t.Year, t.Month, t.Day, t.Hour, t.Minute, 0);
    if (t &lt;= from)
    {
        t = t.AddMinutes(1);
    }

    // 最多查找8年,避免死循环
    for (var guard = 0; guard &lt; 8 * 366; guard++)
    {
        // 月份不匹配 → 跳到下月1日
        if (!_months.Contains(t.Month))
        {
            t = new DateTime(t.Year, t.Month, 1).AddMonths(1);
            continue;
        }
        // 日期不匹配 → 跳到次日
        if (!DayMatches(t))
        {
            t = t.Date.AddDays(1);
            continue;
        }
        // 小时不匹配 → 跳到本日下一匹配小时,无则次日
        if (!_hours.Contains(t.Hour))
        {
            var nextHour = _hours.Where(h =&gt; h &gt; t.Hour).DefaultIfEmpty(-1).First();
            t = nextHour &gt;= 0
                ? new DateTime(t.Year, t.Month, t.Day, nextHour, 0, 0)
                : t.Date.AddDays(1);
            continue;
        }
        // 分钟不匹配 → 跳到本小时下一匹配分钟,无则下一小时
        if (!_minutes.Contains(t.Minute))
        {
            var nextMin = _minutes.Where(m =&gt; m &gt; t.Minute).DefaultIfEmpty(-1).First();
            t = nextMin &gt;= 0
                ? new DateTime(t.Year, t.Month, t.Day, t.Hour, nextMin, 0)
                : new DateTime(t.Year, t.Month, t.Day, t.Hour, 0, 0).AddHours(1);
            continue;
        }
        return t;
    }
    return null;
}

/// &lt;summary&gt;
/// 日期是否匹配(日/周字段规则)
/// &lt;/summary&gt;
private bool DayMatches(DateTime t)
{
    if (_domStar &amp;&amp; _dowStar)
    {
        return true;
    }
    var domOk = _daysOfMonth.Contains(t.Day);
    var dowOk = _daysOfWeek.Contains((int)t.DayOfWeek);
    return _domStar ? dowOk : _dowStar ? domOk : domOk || dowOk;
}
}
/*
encoding: utf-8
版权所有  2026 ©涂聚文有限公司™ ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Taskscheduler
Author    : geovindu,Geovin Du 涂聚文.
IDE       : vs2026 c# .net 10
os        : windows 10
database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
Datetime  : 2026/08/28 22:16
User      :  geovindu
Product   : Visual Studio 2026
Project   : Taskscheduler
File      : Db.cs
*/
namespace Taskscheduler.Storage;
using Microsoft.Data.Sqlite;
/// <summary>
/// SQLite数据库层(对应Go版 storage/db.go)
/// 职责:建表、连接工厂、时间格式化
/// 说明:每次操作独立连接(WAL模式支持并发读写),简单可靠
/// </summary>
public static class Db
{
/// <summary>
/// 数据库文件路径(当前工作目录
/// )</summary>
public static string DbFile { get; private set; } = "task_scheduler.db";
/// &lt;summary&gt;
/// 数据库已初始化标志
/// &lt;/summary&gt;
public static bool IsInitialized { get; private set; }

/// &lt;summary&gt;
/// 初始化数据库:创建连接目录、建表
/// &lt;/summary&gt;
public static void Init(string dbFile)
{
    DbFile = dbFile;
    // 确保数据库所在目录存在
    var dir = Path.GetDirectoryName(Path.GetFullPath(dbFile));
    if (!string.IsNullOrEmpty(dir) &amp;&amp; !Directory.Exists(dir))
    {
        Directory.CreateDirectory(dir);
    }
    using var conn = OpenConnection();
    // 建任务表
    Exec(conn, """
        CREATE TABLE IF NOT EXISTS t_task (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            task_name       TEXT    NOT NULL,
            cron_expr       TEXT    NOT NULL,
            command         TEXT    NOT NULL,
            timeout_sec     INTEGER NOT NULL DEFAULT 300,
            max_retry       INTEGER NOT NULL DEFAULT 0,
            retry_gap_sec   INTEGER NOT NULL DEFAULT 10,
            is_enable       INTEGER NOT NULL DEFAULT 1,
            remark          TEXT    DEFAULT '',
            task_status     TEXT    NOT NULL DEFAULT 'IDLE'
        )
        """);
    // 建执行历史表
    Exec(conn, """
        CREATE TABLE IF NOT EXISTS t_task_history (
            id                  INTEGER PRIMARY KEY AUTOINCREMENT,
            task_id             INTEGER NOT NULL,
            execute_start_time  TEXT    NOT NULL,
            execute_end_time    TEXT    NOT NULL,
            is_success          INTEGER NOT NULL,
            execute_msg         TEXT    DEFAULT ''
        )
        """);
    // 索引:按任务查历史
    Exec(conn, "CREATE INDEX IF NOT EXISTS idx_history_task_id ON t_task_history(task_id)");
    IsInitialized = true;
}

/// &lt;summary&gt;
/// 打开新的数据库连接(WAL模式)
/// &lt;/summary&gt;
public static SqliteConnection OpenConnection()
{
    var conn = new SqliteConnection($"Data Source={DbFile}");
    conn.Open();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = "PRAGMA journal_mode=WAL";
    cmd.ExecuteNonQuery();
    return conn;
}

/// &lt;summary&gt;
/// 执行非查询SQL
/// &lt;/summary&gt;
public static void Exec(SqliteConnection conn, string sql, params (string Name, object Value)[] args)
{
    using var cmd = conn.CreateCommand();
    cmd.CommandText = sql;
    foreach (var (name, value) in args)
    {
        cmd.Parameters.AddWithValue(name, value);
    }
    cmd.ExecuteNonQuery();
}

/// &lt;summary&gt;
/// 格式化时间为数据库存储格式
/// &lt;/summary&gt;
public static string FormatTime(DateTime dt) =&gt; dt.ToString("yyyy-MM-dd HH:mm:ss");

/// &lt;summary&gt;
/// 解析数据库时间字符串
/// &lt;/summary&gt;
public static DateTime ParseTime(string s) =&gt;
    DateTime.TryParse(s, out var dt) ? dt : DateTime.MinValue;
}
cs 复制代码
/*
 # encoding: utf-8
# 版权所有  2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述:Taskscheduler
# Author    : geovindu,Geovin Du 涂聚文.
# IDE       : vs2026 c# .net 10
# os        : windows 10
# database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
# Datetime  : 2026/08/28 22:16
# User      :  geovindu
# Product   : Visual Studio 2026
# Project   : Taskscheduler
# File      : TaskSchedulerService.cs
 */
 
namespace Taskscheduler.Application;
 
using Taskscheduler.Domain;
using Taskscheduler.Repository;
using Taskscheduler.Runner;
using Taskscheduler.Storage;
 
/// <summary>
/// 应用层业务服务:任务执行入口(对应Go版 application/task_scheduler_service.go)
/// 职责:调用子进程执行命令、失败重试、组装历史实体、写入历史仓储
/// </summary>
public class TaskSchedulerService
{
    private readonly TaskHistoryRepo _historyRepo = new();
 
    /// <summary>
    /// 执行单个任务(手动/调度共用)
    /// 流程:子进程执行 → 失败按MaxRetry/RetryGapSec重试 → 写入执行历史
    /// </summary>
    public TaskHistoryEntity? ExecuteSingleTask(TaskEntity task)
    {
        Taskscheduler.Logger.AppLogger.Info($"开始执行任务 task_id={task.TaskId} 任务名称={task.TaskName}");
 
        var startTime = DateTime.Now;
        var runner = new SubprocessRunner(task.TimeoutSec);
 
        var finalSuccess = false;
        var finalMsg = "";
        var currentRetry = 0;
 
        // ---------- 执行 + 重试循环 ----------
        while (true)
        {
            var result = runner.Run(task.TaskId, task.Command);
            if (result.Success)
            {
                finalSuccess = true;
                finalMsg = result.Output;
                break;
            }
            finalMsg = result.Output;
            currentRetry++;
            if (currentRetry > task.MaxRetry)
            {
                break;
            }
            Taskscheduler.Logger.AppLogger.Info(
                $"task_id={task.TaskId} 执行失败,第{currentRetry}次重试,间隔{task.RetryGapSec}秒");
            Thread.Sleep(task.RetryGapSec * 1000);
        }
 
        var endTime = DateTime.Now;
 
        // ---------- 组装并写入历史 ----------
        var history = new TaskHistoryEntity
        {
            TaskId = task.TaskId,
            ExecuteStartTime = startTime,
            ExecuteEndTime = endTime,
            IsSuccess = finalSuccess,
            ExecuteMsg = finalMsg,
        };
        var saved = _historyRepo.Insert(history);
 
        if (finalSuccess)
        {
            Taskscheduler.Logger.AppLogger.Info($"任务执行成功 task_id={task.TaskId}");
        }
        else
        {
            Taskscheduler.Logger.AppLogger.Error(
                $"任务执行失败 task_id={task.TaskId},重试次数={currentRetry},output={finalMsg}");
        }
        return saved;
    }
}
 
 
/*
 # encoding: utf-8
# 版权所有  2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述:Taskscheduler
# Author    : geovindu,Geovin Du 涂聚文.
# IDE       : vs2026 c# .net 10
# os        : windows 10
# database  : mysql 9.0 sql server 2019, postgreSQL 17.0  Oracle 21c Neo4j
# Datetime  : 2026/08/28 22:16
# User      :  geovindu
# Product   : Visual Studio 2026
# Project   : Taskscheduler
# File      : Program.cs
 */
 
namespace Taskscheduler.Cli;
 
using Taskscheduler.Config;
using Taskscheduler.Domain;
using Taskscheduler.JsonIO;
using Taskscheduler.Logger;
using Taskscheduler.Repository;
using Taskscheduler.Runner;
using Taskscheduler.Scheduler;
using Taskscheduler.Storage;
 
/// <summary>
/// CLI入口(对应Go版 main.go,命令一致)
/// 用法:
///   list                     列出全部任务
///   add "名称" "cron" "命令" [--timeout 300] [--retry 0] [--gap 10]
///   del ID                   删除任务
///   enable ID true|false     启用/禁用任务
///   runonce ID               立即执行一次任务
///   run                      启动调度守护(阻塞)
///   history ID               查看任务执行历史
///   import 文件.json         从JSON导入任务
///   export 文件.json         导出全部任务到JSON
/// </summary>
public static class Program
{
    public static int Main(string[] args)
    {
        // ---------- 初始化 ----------
        var cfg = ConfigLoader.Load("config/app.yaml");
        AppLogger.Init(cfg.Log.LogDir);
        Db.Init(cfg.Database.DbFile);
 
        if (args.Length == 0)
        {
            PrintUsage();
            return 0;
        }
 
        var command = args[0].ToLowerInvariant();
        switch (command)
        {
            case "list":
                return ListTasks();
            case "add":
                return AddTask(args);
            case "del":
                return DeleteTask(args);
            case "enable":
                return EnableTask(args);
            case "runonce":
                return RunOnce(args);
            case "run":
                return RunDaemon();
            case "history":
                return ShowHistory(args);
            case "import":
                return Import(args);
            case "export":
                return Export(args);
            default:
                Console.WriteLine($"未知命令: {command}");
                PrintUsage();
                return 1;
        }
    }
 
    /// <summary>
    /// 列出任务
    /// </summary>
    private static int ListTasks()
    {
        var list = new TaskRepo().GetList();
        Console.WriteLine($"{"ID",-5} {"任务名称",-16} {"Cron表达式",-16} {"状态",-8} {"启用",-5} 备注");
        Console.WriteLine(new string('-', 90));
        foreach (var t in list)
        {
            Console.WriteLine($"{t.TaskId,-5} {t.TaskName,-16} {t.CronExpr,-16} {t.TaskStatus,-8} {(t.IsEnable ? "是" : "否"),-5} {Truncate(t.Remark, 28)}");
        }
        Console.WriteLine($"共 {list.Count} 条任务");
        return 0;
    }
 
    /// <summary>
    /// 新增任务
    /// </summary>
    private static int AddTask(string[] args)
    {
        if (args.Length < 4)
        {
            Console.WriteLine("用法: add \"任务名称\" \"cron表达式\" \"执行命令\" [--timeout 300] [--retry 0] [--gap 10] [--remark 备注]");
            return 1;
        }
        var entity = new TaskEntity
        {
            TaskName = args[1],
            CronExpr = args[2],
            Command = args[3],
            TimeoutSec = 300,
            MaxRetry = 0,
            RetryGapSec = 10,
            IsEnable = true,
            Remark = "",
        };
        // 解析可选参数
        for (var i = 4; i < args.Length; i++)
        {
            switch (args[i].ToLowerInvariant())
            {
                case "--timeout" when i + 1 < args.Length:
                    entity.TimeoutSec = int.Parse(args[++i]);
                    break;
                case "--retry" when i + 1 < args.Length:
                    entity.MaxRetry = int.Parse(args[++i]);
                    break;
                case "--gap" when i + 1 < args.Length:
                    entity.RetryGapSec = int.Parse(args[++i]);
                    break;
                case "--remark" when i + 1 < args.Length:
                    entity.Remark = args[++i];
                    break;
            }
        }
        var saved = new TaskRepo().Save(entity);
        AppLogger.Info($"任务已新增 ID={saved.TaskId}");
        Console.WriteLine($"任务已新增 ID={saved.TaskId}");
        return 0;
    }
 
    /// <summary>
    /// 删除任务
    /// </summary>
    private static int DeleteTask(string[] args)
    {
        if (args.Length < 2 || !long.TryParse(args[1], out var id))
        {
            Console.WriteLine("用法: del ID");
            return 1;
        }
        if (new TaskRepo().DeleteById(id))
        {
            AppLogger.Info($"任务 {id} 已删除");
            Console.WriteLine($"任务 {id} 已删除");
            return 0;
        }
        Console.WriteLine($"任务 {id} 不存在");
        return 1;
    }
 
    /// <summary>
    /// 启用/禁用任务
    /// </summary>
    private static int EnableTask(string[] args)
    {
        if (args.Length < 3 || !long.TryParse(args[1], out var id) || !bool.TryParse(args[2], out var enable))
        {
            Console.WriteLine("用法: enable ID true|false");
            return 1;
        }
        if (new TaskRepo().UpdateEnable(id, enable))
        {
            Console.WriteLine($"任务 {id} 已{(enable ? "启用" : "禁用")}");
            return 0;
        }
        Console.WriteLine($"任务 {id} 不存在");
        return 1;
    }
 
    /// <summary>
    /// 立即执行任务一次
    /// </summary>
    private static int RunOnce(string[] args)
    {
        if (args.Length < 2 || !long.TryParse(args[1], out var id))
        {
            Console.WriteLine("用法: runonce ID");
            return 1;
        }
        var task = new TaskRepo().GetById(id);
        if (task == null)
        {
            Console.WriteLine($"任务 {id} 不存在");
            return 1;
        }
        var sw = System.Diagnostics.Stopwatch.StartNew();
        var history = new Application.TaskSchedulerService().ExecuteSingleTask(task);
        sw.Stop();
        if (history != null)
        {
            Console.WriteLine($"任务 {id}【{task.TaskName}】{(history.IsSuccess ? "执行成功" : "执行失败")},耗时{sw.ElapsedMilliseconds}ms,历史ID={history.HistoryId}");
        }
        // 一次性任务执行后自动禁用(与调度线程一致)
        if (task.IsOneTime)
        {
            new TaskRepo().UpdateEnable(id, false);
            AppLogger.Info($"一次性任务 {id} 已自动禁用");
        }
        return history is { IsSuccess: true } ? 0 : 1;
    }
 
    /// <summary>
    /// 启动调度守护(阻塞)
    /// </summary>
    private static int RunDaemon()
    {
        var scheduler = new BackgroundScheduler();
        scheduler.Start();
        AppLogger.Info("调度守护已启动,Ctrl+C 退出");
        var exit = new ManualResetEventSlim(false);
        Console.CancelKeyPress += (_, e) => { e.Cancel = true; exit.Set(); };
        exit.Wait();
        scheduler.Stop();
        return 0;
    }
 
    /// <summary>
    /// 查看任务执行历史
    /// </summary>
    private static int ShowHistory(string[] args)
    {
        if (args.Length < 2 || !long.TryParse(args[1], out var id))
        {
            Console.WriteLine("用法: history ID");
            return 1;
        }
        var list = new TaskHistoryRepo().ListByTaskId(id, 50);
        Console.WriteLine($"{"历史ID",-8} {"执行时间",-20} {"结果",-5} 输出摘要");
        Console.WriteLine(new string('-', 80));
        foreach (var h in list)
        {
            Console.WriteLine($"{h.HistoryId,-8} {h.ExecuteStartTime,-20:yyyy-MM-dd HH:mm:ss} {(h.IsSuccess ? "成功" : "失败"),-5} {Truncate(h.ExecuteMsg, 40)}");
        }
        return 0;
    }
 
    /// <summary>
    /// 导入任务
    /// </summary>
    private static int Import(string[] args)
    {
        if (args.Length < 2)
        {
            Console.WriteLine("用法: import 文件.json");
            return 1;
        }
        try
        {
            var stat = new TaskJsonIO().ImportFromFile(args[1]);
            Console.WriteLine($"导入完成:总条数={stat.Total} 新增={stat.Insert} 更新={stat.Update} 跳过={stat.Skip}");
            return 0;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"导入失败: {ex.Message}");
            return 1;
        }
    }
 
    /// <summary>
    /// 导出任务
    /// </summary>
    private static int Export(string[] args)
    {
        if (args.Length < 2)
        {
            Console.WriteLine("用法: export 文件.json");
            return 1;
        }
        try
        {
            new TaskJsonIO().ExportAll(args[1]);
            Console.WriteLine($"导出完成 → {args[1]}");
            return 0;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"导出失败: {ex.Message}");
            return 1;
        }
    }
 
    /// <summary>
    /// 截断长文本
    /// </summary>
    private static string Truncate(string s, int max)
    {
        if (string.IsNullOrEmpty(s) || s.Length <= max)
        {
            return s ?? "";
        }
        return s[..max] + "...";
    }
 
    /// <summary>
    /// 打印用法
    /// </summary>
    private static void PrintUsage()
    {
        Console.WriteLine("""
            TaskScheduler 定时任务调度器 (C# .NET 10 版)
            用法:
              list                    列出全部任务
              add 名称 cron 命令 [--timeout 300] [--retry 0] [--gap 10] [--remark 备注]
              del ID                  删除任务
              enable ID true|false    启用/禁用任务
              runonce ID              立即执行一次任务
              run                     启动调度守护
              history ID              查看执行历史
              import 文件.json        导入任务
              export 文件.json        导出任务
            """);
    }
}

调用:

cs 复制代码
namespace Taskscheduler.Gui;
using System.Drawing.Drawing2D;
using Taskscheduler.Domain;
using Taskscheduler.JsonIO;
using Taskscheduler.Logger;
using Taskscheduler.Repository;
using Taskscheduler.Scheduler;
/// <summary>
/// 主窗体(对应Go版 gui/main.go + dashboard_view.go 合并)
/// 布局:工具栏 + Notebook多标签页(任务列表 / 仪表盘)+ 底部实时日志 + 托盘最小化
/// 任务列表支持:关键字查询分页(首页/上一页/下一页/末页/每页条数/总条数/总页数)
/// </summary>
public class MainForm : Form
{
// ---------- 核心组件 ----------
private readonly TaskRepo _taskRepo = new();
private readonly TaskHistoryRepo _historyRepo = new();
private readonly BackgroundScheduler _scheduler = new();
// ---------- UI组件 ----------
private readonly TabControl _tabs = new();
private readonly DataGridView _gridTasks = new();
private readonly DataGridView _gridRecent = new();
private readonly RichTextBox _txtLog = new();
private readonly NotifyIcon _notifyIcon = new();
private readonly System.Windows.Forms.Timer _refreshTimer = new();

// 仪表盘统计卡片
private readonly Label _lblTotal = new();
private readonly Label _lblToday = new();
private readonly Label _lblSuccess = new();
private readonly Label _lblFail = new();
private readonly DailyChartControl _chart = new();

// ---------- 任务列表分页与查询 ----------
private TabPage? _tabTasks;
private int _page = 1;
private int _pageSize = 10;
private long _totalCount;
private int _totalPages = 1;
private string _keyword = "";
private readonly TextBox _txtSearch = new();
private readonly ComboBox _cboPageSize = new();
private readonly Label _lblPageInfo = new();
private readonly Label _lblTotalInfo = new();
private readonly Button _btnFirst = new();
private readonly Button _btnPrev = new();
private readonly Button _btnNext = new();
private readonly Button _btnLast = new();

private bool _exiting;

/// &lt;summary&gt;
/// 主窗体构造函数
/// &lt;/summary&gt;
public MainForm()
{
    Text = "TaskScheduler 定时任务调度器 (.NET 10)";
    ClientSize = new Size(1080, 700);
    StartPosition = FormStartPosition.CenterScreen;
    Font = new Font("Microsoft YaHei UI", 9.5f);
    BuildUi();
    SetupTray();
    SetupRefreshTimer();
    SubscribeLogs();
    LoadTasksPage();
    RefreshDashboard();
    _scheduler.Start();
    AppLogger.Info("主界面初始化完成,调度已启动");
}

// ==================== UI构建 ====================

/// &lt;summary&gt;
/// 构建主界面
/// &lt;/summary&gt;
private void BuildUi()
{
    // ---------- 工具栏 ----------
    var toolStrip = new ToolStrip { GripStyle = ToolStripGripStyle.Hidden, Padding = new Padding(6, 2, 6, 2) };
    toolStrip.Items.Add(MakeButton("新增任务", (_, _) =&gt; AddTask()));
    toolStrip.Items.Add(MakeButton("编辑任务", (_, _) =&gt; EditTask()));
    toolStrip.Items.Add(MakeButton("删除任务", (_, _) =&gt; DeleteTask()));
    toolStrip.Items.Add(MakeButton("立即执行", (_, _) =&gt; RunOnce()));
    toolStrip.Items.Add(MakeButton("执行历史", (_, _) =&gt; ShowHistory()));
    toolStrip.Items.Add(new ToolStripSeparator());
    toolStrip.Items.Add(MakeButton("导入", (_, _) =&gt; ImportTasks()));
    toolStrip.Items.Add(MakeButton("导出", (_, _) =&gt; ExportTasks()));
    toolStrip.Items.Add(new ToolStripSeparator());
    toolStrip.Items.Add(MakeButton("任务列表", (_, _) =&gt; _tabs.SelectedIndex = 0));
    toolStrip.Items.Add(MakeButton("仪表盘", (_, _) =&gt; _tabs.SelectedIndex = 1));
    toolStrip.Items.Add(MakeButton("刷新", (_, _) =&gt; { LoadTasksPage(); RefreshDashboard(); }));
    Controls.Add(toolStrip);

    // ---------- 多标签页(三个Tab:任务列表 / 仪表盘 / 执行日志) ----------
    _tabs.Dock = DockStyle.Fill;
    Controls.Add(_tabs);

    // Tab1 任务列表
    _tabTasks = new TabPage("任务列表");
    BuildTaskListTab(_tabTasks);
    _tabs.TabPages.Add(_tabTasks);

    // Tab2 仪表盘
    var tabDash = new TabPage("仪表盘");
    BuildDashboardTab(tabDash);
    _tabs.TabPages.Add(tabDash);

    // Tab3 执行日志(独立页签,替代原底部固定日志框)
    var tabLog = new TabPage("执行日志");
    BuildLogTab(tabLog);
    _tabs.TabPages.Add(tabLog);

    // 关闭时最小化到托盘
    FormClosing += OnFormClosing;
}

/// &lt;summary&gt;
/// 构建执行日志页(清空按钮 + 实时日志框)
/// &lt;/summary&gt;
private void BuildLogTab(TabPage page)
{
    // 顶部工具栏:清空日志
    var logToolbar = new FlowLayoutPanel
    {
        Dock = DockStyle.Top,
        Height = 42,
        Padding = new Padding(10, 7, 0, 0),
        WrapContents = false,
    };
    var btnClearLog = new Button { Text = "清空日志", Width = 90, Margin = new Padding(0, 0, 6, 0) };
    btnClearLog.Click += (_, _) =&gt;
    {
        _txtLog.Clear();
    };
    var logTip = new Label
    {
        Text = "实时输出运行日志,滚动文件按 5MB 自动切割保留 10 份(logs\\app.log)",
        AutoSize = true,
        ForeColor = Color.Gray,
        Margin = new Padding(10, 6, 0, 0),
    };
    logToolbar.Controls.Add(btnClearLog);
    logToolbar.Controls.Add(logTip);
    page.Controls.Add(logToolbar);

    // 日志框(占满整页)
    _txtLog.Dock = DockStyle.Fill;
    _txtLog.ReadOnly = true;
    _txtLog.BackColor = Color.FromArgb(30, 30, 30);
    _txtLog.ForeColor = Color.FromArgb(220, 220, 220);
    _txtLog.Font = new Font("Consolas", 9f);
    _txtLog.BorderStyle = BorderStyle.FixedSingle;
    page.Controls.Add(_txtLog);
}

/// &lt;summary&gt;
/// 构建任务列表页(查询栏 + 表格 + 分页栏)
/// &lt;/summary&gt;
private void BuildTaskListTab(TabPage page)
{
    // ---------- 顶部查询栏 ----------
    var searchPanel = new FlowLayoutPanel
    {
        Dock = DockStyle.Top,
        Height = 42,
        Padding = new Padding(10, 8, 0, 0),
        WrapContents = false,
    };
    var searchLabel = new Label { Text = "搜索:", AutoSize = true, Margin = new Padding(0, 6, 0, 0) };
    _txtSearch.Width = 220;
    _txtSearch.Margin = new Padding(0, 4, 0, 0);
    _txtSearch.KeyDown += (_, e) =&gt;
    {
        if (e.KeyCode == Keys.Enter)
        {
            ApplySearch();
        }
    };
    var btnSearch = new Button { Text = "查询", Width = 70, Margin = new Padding(6, 3, 0, 0) };
    btnSearch.Click += (_, _) =&gt; ApplySearch();
    var btnReset = new Button { Text = "重置", Width = 70, Margin = new Padding(6, 3, 0, 0) };
    btnReset.Click += (_, _) =&gt;
    {
        _txtSearch.Clear();
        _keyword = "";
        _page = 1;
        LoadTasksPage();
    };
    searchPanel.Controls.Add(searchLabel);
    searchPanel.Controls.Add(_txtSearch);
    searchPanel.Controls.Add(btnSearch);
    searchPanel.Controls.Add(btnReset);
    page.Controls.Add(searchPanel);

    // ---------- 底部查询栏 ----------
    var pagerPanel = new FlowLayoutPanel
    {
        Dock = DockStyle.Bottom,
        Height = 40,
        Padding = new Padding(10, 5, 0, 0),
        WrapContents = false,
    };
    _btnFirst.Text = "首页";
    _btnPrev.Text = "上一页";
    _btnNext.Text = "下一页";
    _btnLast.Text = "末页";
    foreach (var btn in new[] { _btnFirst, _btnPrev, _btnNext, _btnLast })
    {
        btn.Width = 74;
        btn.Margin = new Padding(0, 0, 6, 0);
        btn.Click += (_, _) =&gt; OnPageButton(btn);
    }
    _lblPageInfo.AutoSize = true;
    _lblPageInfo.Margin = new Padding(8, 6, 0, 0);
    _lblTotalInfo.AutoSize = true;
    _lblTotalInfo.Margin = new Padding(8, 6, 0, 0);
    var sizeLabel = new Label { Text = "每页", AutoSize = true, Margin = new Padding(12, 6, 0, 0) };
    _cboPageSize.Items.AddRange(new object[] { 10, 20, 50, 100 });
    _cboPageSize.SelectedItem = 10;
    _cboPageSize.Width = 64;
    _cboPageSize.Margin = new Padding(4, 4, 0, 0);
    _cboPageSize.SelectedIndexChanged += (_, _) =&gt;
    {
        if (_cboPageSize.SelectedItem is int n &amp;&amp; n &gt; 0)
        {
            _pageSize = n;
            _page = 1;
            LoadTasksPage();
        }
    };
    var sizeLabel2 = new Label { Text = "条", AutoSize = true, Margin = new Padding(4, 6, 0, 0) };
    pagerPanel.Controls.Add(_btnFirst);
    pagerPanel.Controls.Add(_btnPrev);
    pagerPanel.Controls.Add(_lblPageInfo);
    pagerPanel.Controls.Add(_btnNext);
    pagerPanel.Controls.Add(_btnLast);
    pagerPanel.Controls.Add(_lblTotalInfo);
    pagerPanel.Controls.Add(sizeLabel);
    pagerPanel.Controls.Add(_cboPageSize);
    pagerPanel.Controls.Add(sizeLabel2);
    page.Controls.Add(pagerPanel);

    // ---------- 任务表格 ----------
    _gridTasks.Dock = DockStyle.Fill;
    _gridTasks.ReadOnly = true;
    _gridTasks.AllowUserToAddRows = false;
    _gridTasks.RowHeadersVisible = false;
    _gridTasks.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
    _gridTasks.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
    _gridTasks.BackgroundColor = Color.White;
    _gridTasks.CellDoubleClick += (_, _) =&gt; ShowHistory();
    _gridTasks.Columns.Add("Id", "ID");
    _gridTasks.Columns.Add("Name", "任务名称");
    _gridTasks.Columns.Add("Cron", "Cron表达式");
    _gridTasks.Columns.Add("Cmd", "执行命令");
    _gridTasks.Columns.Add("Timeout", "超时(秒)");
    _gridTasks.Columns.Add("Status", "状态");
    _gridTasks.Columns.Add("Enable", "启用");
    _gridTasks.Columns.Add("Next", "下次执行");
    _gridTasks.Columns.Add("Remark", "备注");
    _gridTasks.Columns["Id"]!.Width = 50;
    _gridTasks.Columns["Name"]!.Width = 120;
    _gridTasks.Columns["Cron"]!.Width = 110;
    _gridTasks.Columns["Cmd"]!.FillWeight = 140;
    _gridTasks.Columns["Timeout"]!.Width = 70;
    _gridTasks.Columns["Status"]!.Width = 60;
    _gridTasks.Columns["Enable"]!.Width = 50;
    _gridTasks.Columns["Next"]!.Width = 140;
    _gridTasks.Columns["Remark"]!.FillWeight = 120;
    page.Controls.Add(_gridTasks);

    var hint = new Label
    {
        Text = "双击任务行查看执行历史    |    备注含"一次性"的任务执行成功后自动禁用    |    搜索支持任务名称/备注关键字",
        Dock = DockStyle.Bottom,
        Height = 24,
        ForeColor = Color.Gray,
        TextAlign = ContentAlignment.MiddleLeft,
        Padding = new Padding(8, 0, 0, 0),
    };
    page.Controls.Add(hint);

    // 切回任务列表页时刷新当前页(保留页码)
    _tabs.SelectedIndexChanged += (_, _) =&gt;
    {
        if (_tabs.SelectedTab == _tabTasks)
        {
            LoadTasksPage();
        }
    };
}

/// &lt;summary&gt;
/// 应用搜索条件并回到第1页
/// &lt;/summary&gt;
private void ApplySearch()
{
    _keyword = _txtSearch.Text.Trim();
    _page = 1;
    LoadTasksPage();
}

/// &lt;summary&gt;
/// 分页按钮统一事件
/// &lt;/summary&gt;
private void OnPageButton(Button btn)
{
    if (btn == _btnFirst)
    {
        _page = 1;
    }
    else if (btn == _btnPrev)
    {
        if (_page &gt; 1)
        {
            _page--;
        }
    }
    else if (btn == _btnNext)
    {
        if (_page &lt; _totalPages)
        {
            _page++;
        }
    }
    else if (btn == _btnLast)
    {
        _page = _totalPages;
    }
    LoadTasksPage();
}

/// &lt;summary&gt;
/// 构建仪表盘页
/// &lt;/summary&gt;
private void BuildDashboardTab(TabPage page)
{
    // ---------- 统计卡片 ----------
    var cardPanel = new FlowLayoutPanel
    {
        Dock = DockStyle.Top,
        Height = 92,
        Padding = new Padding(10, 8, 10, 0),
        WrapContents = true,
    };
    cardPanel.Controls.Add(MakeCard("任务总数", _lblTotal, Color.FromArgb(70, 130, 220)));
    cardPanel.Controls.Add(MakeCard("今日执行", _lblToday, Color.FromArgb(80, 170, 110)));
    cardPanel.Controls.Add(MakeCard("累计成功", _lblSuccess, Color.FromArgb(90, 180, 120)));
    cardPanel.Controls.Add(MakeCard("累计失败", _lblFail, Color.FromArgb(210, 110, 100)));
    page.Controls.Add(cardPanel);

    // ---------- 最近记录 ----------
    var recentPanel = new Panel { Dock = DockStyle.Fill };
    _gridRecent.Dock = DockStyle.Fill;
    _gridRecent.ReadOnly = true;
    _gridRecent.AllowUserToAddRows = false;
    _gridRecent.RowHeadersVisible = false;
    _gridRecent.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
    _gridRecent.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
    _gridRecent.BackgroundColor = Color.White;
    _gridRecent.Columns.Add("Id", "历史ID");
    _gridRecent.Columns.Add("TaskId", "任务ID");
    _gridRecent.Columns.Add("Time", "执行时间");
    _gridRecent.Columns.Add("Result", "结果");
    _gridRecent.Columns.Add("Msg", "输出摘要");
    _gridRecent.Columns["Id"]!.Width = 70;
    _gridRecent.Columns["TaskId"]!.Width = 70;
    _gridRecent.Columns["Time"]!.Width = 150;
    _gridRecent.Columns["Result"]!.Width = 60;
    var recentLabel = new Label
    {
        Text = "最近执行记录(导入任务后尚未执行则暂无记录)",
        Dock = DockStyle.Top,
        Height = 28,
        Font = new Font("Microsoft YaHei UI", 9.5f, FontStyle.Bold),
        Padding = new Padding(8, 4, 0, 0),
        ForeColor = Color.DimGray,
    };
    recentPanel.Controls.Add(_gridRecent);
    recentPanel.Controls.Add(recentLabel);
    page.Controls.Add(recentPanel);

    // ---------- 柱状图(右下) ----------
    var chartPanel = new Panel { Dock = DockStyle.Right, Width = 330 };
    _chart.Dock = DockStyle.Fill;
    chartPanel.Controls.Add(_chart);
    page.Controls.Add(chartPanel);
}

/// &lt;summary&gt;
/// 创建统计卡片
/// &lt;/summary&gt;
private static Panel MakeCard(string title, Label valueLabel, Color color)
{
    var panel = new Panel
    {
        Size = new Size(150, 70),
        Margin = new Padding(6),
        BackColor = Color.White,
    };
    panel.Paint += (_, e) =&gt;
    {
        using var pen = new Pen(color, 3);
        e.Graphics.DrawRectangle(pen, 0, 0, panel.Width - 1, panel.Height - 1);
        e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(18, color.R, color.G, color.B)), 0, 0, panel.Width, panel.Height);
    };
    var titleLabel = new Label
    {
        Text = title,
        Dock = DockStyle.Top,
        Height = 22,
        TextAlign = ContentAlignment.MiddleCenter,
        ForeColor = Color.Gray,
    };
    valueLabel.Text = "0";
    valueLabel.Font = new Font("Microsoft YaHei UI", 15f, FontStyle.Bold);
    valueLabel.ForeColor = color;
    valueLabel.Dock = DockStyle.Fill;
    valueLabel.TextAlign = ContentAlignment.MiddleCenter;
    panel.Controls.Add(valueLabel);
    panel.Controls.Add(titleLabel);
    return panel;
}

/// &lt;summary&gt;
/// 创建工具栏按钮
/// &lt;/summary&gt;
private static ToolStripButton MakeButton(string text, EventHandler onClick)
{
    var btn = new ToolStripButton(text) { DisplayStyle = ToolStripItemDisplayStyle.Text };
    btn.Click += onClick;
    return btn;
}

// ==================== 数据刷新 ====================

/// &lt;summary&gt;
/// 加载当前页任务列表(分页+查询,含"下次执行"列计算)
/// &lt;/summary&gt;
private void LoadTasksPage()
{
    try
    {
        // 1. 统计总数与总页数
        _totalCount = _taskRepo.Count(_keyword);
        _totalPages = (int)Math.Max(1, (_totalCount + _pageSize - 1) / _pageSize);
        // 2. 页码越界自动回退(如删除末页全部任务后)
        if (_page &gt; _totalPages)
        {
            _page = _totalPages;
        }

        var list = _taskRepo.SearchPage(_keyword, _page, _pageSize);
        var now = DateTime.Now;
        _gridTasks.SuspendLayout();
        _gridTasks.Rows.Clear();
        foreach (var t in list)
        {
            string next;
            try
            {
                var n = CronExpression.Parse(t.CronExpr).GetNext(now);
                next = n.HasValue ? n.Value.ToString("yyyy-MM-dd HH:mm:ss") : "-";
            }
            catch
            {
                next = "表达式无效";
            }
            var idx = _gridTasks.Rows.Add(
                t.TaskId, t.TaskName, t.CronExpr, Truncate(t.Command, 40),
                t.TimeoutSec, t.TaskStatus, t.IsEnable ? "是" : "否", next,
                Truncate(t.Remark, 24));
            // 运行中状态行高亮
            if (t.TaskStatus == TaskStatus.Running)
            {
                _gridTasks.Rows[idx].DefaultCellStyle.BackColor = Color.FromArgb(255, 245, 220);
            }
            else if (!t.IsEnable)
            {
                _gridTasks.Rows[idx].DefaultCellStyle.ForeColor = Color.Gray;
            }
        }
        _gridTasks.ResumeLayout();
        UpdatePager();
    }
    catch (Exception ex)
    {
        AppLogger.Error($"刷新任务列表失败: {ex.Message}");
    }
}

/// &lt;summary&gt;
/// 更新分页控件状态
/// &lt;/summary&gt;
private void UpdatePager()
{
    _btnFirst.Enabled = _page &gt; 1;
    _btnPrev.Enabled = _page &gt; 1;
    _btnNext.Enabled = _page &lt; _totalPages;
    _btnLast.Enabled = _page &lt; _totalPages;
    _lblPageInfo.Text = $"第 {_page}/{_totalPages} 页";
    _lblTotalInfo.Text = $"共 {_totalCount} 条";
}

/// &lt;summary&gt;
/// 刷新仪表盘(统计卡片 + 柱状图 + 最近记录)
/// &lt;/summary&gt;
private void RefreshDashboard()
{
    try
    {
        // 卡片1:任务总数(真实任务数量)
        _lblTotal.Text = _taskRepo.Count().ToString();
        // 卡片2-4:执行统计
        var (total, success, fail) = _historyRepo.CountStats();
        _lblToday.Text = _historyRepo.CountToday().ToString();
        _lblSuccess.Text = success.ToString();
        _lblFail.Text = fail.ToString();

        _chart.SetData(_historyRepo.CountByDay(7)
            .Select(d =&gt; (d.Date, d.Count)).ToList());

        var recent = _historyRepo.ListRecent(15);
        _gridRecent.SuspendLayout();
        _gridRecent.Rows.Clear();
        foreach (var h in recent)
        {
            var idx = _gridRecent.Rows.Add(
                h.HistoryId, h.TaskId,
                h.ExecuteStartTime.ToString("yyyy-MM-dd HH:mm:ss"),
                h.IsSuccess ? "成功" : "失败",
                Truncate(h.ExecuteMsg, 50));
            _gridRecent.Rows[idx].DefaultCellStyle.ForeColor = h.IsSuccess
                ? Color.FromArgb(40, 130, 70)
                : Color.FromArgb(200, 70, 60);
        }
        _gridRecent.ResumeLayout();
    }
    catch (Exception ex)
    {
        AppLogger.Error($"刷新仪表盘失败: {ex.Message}");
    }
}

// ==================== 工具栏动作 ====================

/// &lt;summary&gt;
/// 新增任务
/// &lt;/summary&gt;
private void AddTask()
{
    using var dialog = new TaskEditForm();
    if (dialog.ShowDialog(this) == DialogResult.OK &amp;&amp; dialog.Result != null)
    {
        _taskRepo.Save(dialog.Result);
        AppLogger.Info($"新增任务成功: {dialog.Result.TaskName} cron={dialog.Result.CronExpr}");
        _keyword = "";
        _txtSearch.Clear();
        _page = 1;
        LoadTasksPage();
        RefreshDashboard();
    }
}

/// &lt;summary&gt;
/// 编辑选中任务
/// &lt;/summary&gt;
private void EditTask()
{
    var task = SelectedTask();
    if (task == null)
    {
        MessageBox.Show("请先选中要编辑的任务行", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
        return;
    }
    using var dialog = new TaskEditForm(task);
    if (dialog.ShowDialog(this) == DialogResult.OK &amp;&amp; dialog.Result != null)
    {
        _taskRepo.Save(dialog.Result);
        AppLogger.Info($"编辑任务成功: {dialog.Result.TaskName}");
        LoadTasksPage();
        RefreshDashboard();
    }
}

/// &lt;summary&gt;
/// 删除选中任务
/// &lt;/summary&gt;
private void DeleteTask()
{
    var task = SelectedTask();
    if (task == null)
    {
        MessageBox.Show("请先选中要删除的任务行", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
        return;
    }
    if (MessageBox.Show($"确定删除任务【{task.TaskName}】(ID={task.TaskId})?",
            "确认删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
    {
        _taskRepo.DeleteById(task.TaskId);
        AppLogger.Info($"删除任务: ID={task.TaskId} 名称={task.TaskName}");
        LoadTasksPage();
        RefreshDashboard();
    }
}

/// &lt;summary&gt;
/// 立即执行选中任务
/// &lt;/summary&gt;
private void RunOnce()
{
    var task = SelectedTask();
    if (task == null)
    {
        MessageBox.Show("请先选中要执行的任务行", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
        return;
    }
    // 后台执行,不阻塞UI
    _ = Task.Run(() =&gt;
    {
        _taskRepo.UpdateStatus(task.TaskId, TaskStatus.Running);
        try
        {
            var history = new Application.TaskSchedulerService().ExecuteSingleTask(task);
            if (history != null)
            {
                AppLogger.Info($"手动执行完成 task_id={task.TaskId} 结果={(history.IsSuccess ? "成功" : "失败")}");
            }
        }
        finally
        {
            _taskRepo.UpdateStatus(task.TaskId, TaskStatus.Idle);
        }
        // 一次性任务执行成功后自动禁用
        if (task.IsOneTime)
        {
            _taskRepo.UpdateEnable(task.TaskId, false);
            AppLogger.Info($"一次性任务 task_id={task.TaskId} 已自动禁用");
        }
        Invoke(() =&gt; { LoadTasksPage(); RefreshDashboard(); });
    });
}

/// &lt;summary&gt;
/// 查看选中任务历史
/// &lt;/summary&gt;
private void ShowHistory()
{
    var task = SelectedTask();
    if (task == null)
    {
        MessageBox.Show("请先选中要查看历史的任务行", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
        return;
    }
    using var dialog = new HistoryForm(task.TaskId, task.TaskName);
    dialog.ShowDialog(this);
}

/// &lt;summary&gt;
/// 
/// 导入任务
/// &lt;/summary&gt;
private void ImportTasks()
{
    using var open = new OpenFileDialog
    {
        Title = "选择要导入的任务JSON文件",
        Filter = "JSON文件 (*.json)|*.json",
    };
    if (open.ShowDialog(this) != DialogResult.OK)
    {
        return;
    }
    try
    {
        var stat = new TaskJsonIO().ImportFromFile(open.FileName);
        MessageBox.Show($"导入完成:总条数={stat.Total} 新增={stat.Insert} 更新={stat.Update} 跳过={stat.Skip}",
            "导入结果", MessageBoxButtons.OK, MessageBoxIcon.Information);
        _keyword = "";
        _txtSearch.Clear();
        _page = 1;
        LoadTasksPage();
        RefreshDashboard();
    }
    catch (Exception ex)
    {
        MessageBox.Show($"导入失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
        AppLogger.Error($"导入失败: {ex.Message}");
    }
}

/// &lt;summary&gt;
/// 导出任务
/// &lt;/summary&gt;
private void ExportTasks()
{
    using var save = new SaveFileDialog
    {
        Title = "选择导出位置",
        Filter = "JSON文件 (*.json)|*.json",
        FileName = $"task_export_{DateTime.Now:yyyyMMdd_HHmmss}.json",
    };
    if (save.ShowDialog(this) != DialogResult.OK)
    {
        return;
    }
    try
    {
        new TaskJsonIO().ExportAll(save.FileName);
        MessageBox.Show($"导出完成 → {save.FileName}", "导出结果", MessageBoxButtons.OK, MessageBoxIcon.Information);
    }
    catch (Exception ex)
    {
        MessageBox.Show($"导出失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
        AppLogger.Error($"导出失败: {ex.Message}");
    }
}

/// &lt;summary&gt;
/// 获取选中任务
/// &lt;/summary&gt;
private TaskEntity? SelectedTask()
{
    if (_gridTasks.CurrentRow == null ||
        !long.TryParse(_gridTasks.CurrentRow.Cells["Id"]!.Value?.ToString(), out var id))
    {
        return null;
    }
    return _taskRepo.GetById(id);
}

// ==================== 托盘与关闭 ====================

/// &lt;summary&gt;
/// 设置系统托盘(关闭最小化 + 菜单退出)
/// &lt;/summary&gt;
private void SetupTray()
{
    _notifyIcon.Icon = SystemIcons.Application;
    _notifyIcon.Text = "TaskScheduler 定时任务调度器";
    _notifyIcon.Visible = true;
    _notifyIcon.DoubleClick += (_, _) =&gt;
    {
        Show();
        WindowState = FormWindowState.Normal;
        Activate();
    };

    var menu = new ContextMenuStrip();
    menu.Items.Add("显示主窗口", null, (_, _) =&gt;
    {
        Show();
        WindowState = FormWindowState.Normal;
        Activate();
    });
    menu.Items.Add(new ToolStripSeparator());
    menu.Items.Add("退出程序", null, (_, _) =&gt;
    {
        _exiting = true;
        _notifyIcon.Visible = false;
        System.Windows.Forms.Application.Exit();
    });
    _notifyIcon.ContextMenuStrip = menu;
}

/// &lt;summary&gt;
/// 窗体关闭事件:非退出状态下最小化到托盘
/// &lt;/summary&gt;
private void OnFormClosing(object? sender, FormClosingEventArgs e)
{
    if (_exiting)
    {
        // 真正退出:停止调度
        _scheduler.Stop();
        _refreshTimer.Stop();
        return;
    }
    e.Cancel = true;
    Hide();
    _notifyIcon.ShowBalloonTip(1500, "TaskScheduler",
        "程序已最小化到系统托盘,双击图标恢复窗口", ToolTipIcon.Info);
}

// ==================== 日志与定时刷新 ====================

/// &lt;summary&gt;
/// 订阅日志事件,向日志框实时追加
/// &lt;/summary&gt;
private void SubscribeLogs()
{
    AppLogger.Logged += line =&gt;
    {
        if (IsDisposed)
        {
            return;
        }
        try
        {
            if (InvokeRequired)
            {
                Invoke(() =&gt; AppendLog(line));
            }
            else
            {
                AppendLog(line);
            }
        }
        catch
        {
            // 窗体已关闭,忽略
        }
    };
}

/// &lt;summary&gt;
/// 追加日志行(限制最大行数防止内存膨胀,并滚动到底部)
/// &lt;/summary&gt;
private void AppendLog(string line)
{
    const int maxLines = 3000;
    _txtLog.AppendText(line);
    if (_txtLog.Lines.Length &gt; maxLines)
    {
        var overflow = _txtLog.Lines.Length - maxLines;
        _txtLog.Select(0, _txtLog.GetFirstCharIndexFromLine(overflow));
        _txtLog.SelectedText = "";
    }
    _txtLog.SelectionStart = _txtLog.TextLength;
    _txtLog.ScrollToCaret();
}

/// &lt;summary&gt;
/// 仪表盘/任务列表定时自动刷新(5秒)
/// &lt;/summary&gt;
private void SetupRefreshTimer()
{
    _refreshTimer.Interval = 5000;
    _refreshTimer.Tick += (_, _) =&gt;
    {
        LoadTasksPage();
        RefreshDashboard();
    };
    _refreshTimer.Start();
}

/// &lt;summary&gt;
/// 截断长文本
/// &lt;/summary&gt;
private static string Truncate(string s, int max)
{
    if (string.IsNullOrEmpty(s) || s.Length &lt;= max)
    {
        return s ?? "";
    }
    return s[..max] + "...";
}
}

输出:

相关推荐
qq_401700411 小时前
Qt窗体大小控制从resize到自适应
开发语言·qt
半亩码田1 小时前
C#转Python第4.5篇:单元测试:pytest vs xUnit/NUnit
python·单元测试·c#
fengkai45451 小时前
七、华为云网络综合实验 & 运维类及其他服务总结
服务器·开发语言·php
PHP实战开发录1 小时前
PHP日志写满磁盘后接口为什么异常
开发语言·系统架构·php·开发
Zane19941 小时前
两个线程算两遍循环,为什么只快了一点点不是两倍?GIL连环追问
后端·python
重生之小比特1 小时前
【Java SE】程序逻辑控制
java·开发语言·python
小灰灰搞电子1 小时前
Python 函数参数分隔符 *:Keyword-Only Arguments 原理与实践
开发语言·python
只爱喝胡辣汤1 小时前
04-JVM 性能调优参数
后端
染的人1 小时前
Java 10x15cm面单PDF转换A4格式PDF
java·开发语言·pdf