还在为写定时任务发愁吗?传统的Timer类使用复杂,Windows服务部署麻烦,第三方组件又担心稳定性?今天就教你打造一个基于JSON配置的可视化定时任务系统,支持C#脚本、CMD命令、PowerShell多种执行方式,让定时任务管理变得像编辑配置文件一样简单!
本文将手把手教你构建一个生产级的定时任务框架,彻底解决企业级应用中的任务调度难题。
传统方式需要为每个定时任务写一堆Timer代码,任务逻辑与调度逻辑混在一起,维护噩梦!
时间配置写死在代码里,想改个执行频率还得重新编译发布,运维同事要疯了。
任务执行状态、失败重试、日志记录都需要单独实现,工作量巨大。
我们的方案核心优势:

c#using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace AppScheduledWorkerService.Models
{
public class JobConfiguration
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
[JsonPropertyName("cronExpression")]
public string CronExpression { get; set; } = string.Empty;
[JsonPropertyName("enabled")]
public bool Enabled { get; set; } = true;
[JsonPropertyName("timeout")]
public int TimeoutSeconds { get; set; } = 300;
[JsonPropertyName("scriptType")]
public string ScriptType { get; set; } = "csharp"; // csharp, powershell, cmd
[JsonPropertyName("script")]
public string Script { get; set; } = string.Empty;
[JsonPropertyName("parameters")]
public Dictionary<string, object> Parameters { get; set; } = new();
[JsonPropertyName("retryCount")]
public int RetryCount { get; set; } = 0;
[JsonPropertyName("retryInterval")]
public int RetryIntervalSeconds { get; set; } = 60;
[JsonPropertyName("notifyOnSuccess")]
public bool NotifyOnSuccess { get; set; } = false;
[JsonPropertyName("notifyOnError")]
public bool NotifyOnError { get; set; } = true;
[JsonPropertyName("tags")]
public List<string> Tags { get; set; } = new();
}
public class JobsConfiguration
{
[JsonPropertyName("jobs")]
public List<JobConfiguration> Jobs { get; set; } = new();
[JsonPropertyName("globalSettings")]
public GlobalSettings GlobalSettings { get; set; } = new();
}
public class GlobalSettings
{
[JsonPropertyName("logLevel")]
public string LogLevel { get; set; } = "Information";
[JsonPropertyName("maxConcurrentJobs")]
public int MaxConcurrentJobs { get; set; } = 10;
[JsonPropertyName("enableMetrics")]
public bool EnableMetrics { get; set; } = true;
}
}
设计亮点:
*/30 * * * * *表示每30秒执行json{
"globalSettings": {
"logLevel": "Information",
"maxConcurrentJobs": 5,
"enableMetrics": true
},
"jobs": [
{
"id": "test-job",
"name": "测试任务",
"description": "每30秒执行一次的测试任务",
"cronExpression": "*/30 * * * * *",
"enabled": true,
"timeout": 60,
"scriptType": "csharp",
"script": "logger.LogInformation($\"测试任务开始执行,任务ID: {jobId}, 任务名: {jobName}\");\nlogger.LogInformation($\"执行时间: {executionTime:yyyy-MM-dd HH:mm:ss}\");\n\nvar message = GetParameter<string>(\"message\", \"默认消息\");\nvar count = GetParameter<int>(\"count\", 1);\n\nlogger.LogInformation($\"参数 - 消息: {message}, 计数: {count}\");\n\n// 模拟一些工作\nfor (int i = 1; i <= count; i++)\n{\n logger.LogInformation($\"处理步骤 {i}/{count}\");\n await Task.Delay(500, cancellationToken);\n}\n\nlogger.LogInformation(\"测试任务完成\");\nreturn $\"任务执行成功 - {DateTime.Now:HH:mm:ss} - 处理了 {count} 个步骤\";",
"parameters": {
"message": "这是一个测试消息",
"count": 3
},
"retryCount": 1,
"retryIntervalSeconds": 10,
"notifyOnSuccess": true,
"notifyOnError": true,
"tags": [ "test" ]
},
{
"id": "simple-test",
"name": "简单测试",
"description": "每分钟执行的简单测试",
"cronExpression": "0 * * * * *",
"enabled": true,
"timeout": 30,
"scriptType": "csharp",
"script": "logger.LogInformation(\"简单测试开始\");\nvar now = DateTime.Now;\nlogger.LogInformation($\"当前时间: {now:yyyy-MM-dd HH:mm:ss}\");\nreturn $\"简单测试完成: {now:HH:mm:ss}\";",
"parameters": {},
"retryCount": 0,
"retryIntervalSeconds": 10,
"notifyOnSuccess": true,
"notifyOnError": true,
"tags": [ "simple", "test" ]
},
{
"id": "cmd-system-info",
"name": "系统信息检查",
"description": "每10秒执行CMD命令检查系统信息",
"cronExpression": "*/10 * * * * *",
"enabled": true,
"timeout": 30,
"scriptType": "cmd",
"script": "wmic computersystem get TotalPhysicalMemory /format:list | findstr \"=\"",
"parameters": {
"LOG_LEVEL": "INFO",
"CHECK_TYPE": "BASIC"
},
"retryCount": 1,
"retryIntervalSeconds": 5,
"notifyOnSuccess": false,
"notifyOnError": true,
"tags": [ "cmd", "system", "monitoring" ]
},
{
"id": "cmd-simple",
"name": "简单CMD命令",
"description": "每10秒执行的简单CMD命令",
"cronExpression": "*/10 * * * * *",
"enabled": true,
"timeout": 15,
"scriptType": "cmd",
"script": "echo 计算机名: %computername%",
"parameters": {
"TASK_ID": "CMD-SIMPLE-001",
"EXECUTION_COUNT": "AUTO"
},
"retryCount": 0,
"retryIntervalSeconds": 5,
"notifyOnSuccess": true,
"notifyOnError": true,
"tags": [ "cmd", "simple", "frequent" ]
},
{
"id": "cmd-health-check",
"name": "健康状态检查",
"description": "每10秒检查系统健康状态",
"cronExpression": "*/10 * * * * *",
"enabled": false,
"timeout": 20,
"scriptType": "cmd",
"script": "ping www.163.com",
"parameters": {
"CHECK_LEVEL": "BASIC",
"ALERT_THRESHOLD": "80"
},
"retryCount": 1,
"retryIntervalSeconds": 10,
"notifyOnSuccess": false,
"notifyOnError": true,
"tags": [ "cmd", "health", "monitoring", "system" ]
}
]
}
c#private async Task ExecuteCSharpScript(JobConfiguration job, JobResult result, CancellationToken cancellationToken)
{
// 创建脚本上下文,避免在动态脚本中直接使用复杂类型
var scriptParameters = new Dictionary<string, object>(job.Parameters);
// 包装脚本代码,使用简单的参数传递方式
var fullScript = $@"
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using Microsoft.Extensions.Logging;
public class DynamicScript
{{
private readonly ILogger _logger;
private readonly Dictionary<string, object> _parameters;
private readonly CancellationToken _cancellationToken;
private readonly string _jobId;
private readonly string _jobName;
private readonly DateTime _executionTime;
public DynamicScript(ILogger logger, Dictionary<string, object> parameters, CancellationToken cancellationToken,
string jobId, string jobName, DateTime executionTime)
{{
_logger = logger;
_parameters = parameters;
_cancellationToken = cancellationToken;
_jobId = jobId;
_jobName = jobName;
_executionTime = executionTime;
}}
// 辅助属性和方法
public ILogger logger => _logger;
public Dictionary<string, object> parameters => _parameters;
public CancellationToken cancellationToken => _cancellationToken;
public string jobId => _jobId;
public string jobName => _jobName;
public DateTime executionTime => _executionTime;
public T GetParameter<T>(string key, T defaultValue = default(T))
{{
if (_parameters.TryGetValue(key, out var value))
{{
try
{{
if (value == null) return defaultValue;
if (typeof(T) == typeof(string))
return (T)(object)value.ToString();
return (T)Convert.ChangeType(value, typeof(T));
}}
catch
{{
return defaultValue;
}}
}}
return defaultValue;
}}
public async Task<string> Execute()
{{
try
{{
// 用户脚本开始
{job.Script}
// 用户脚本结束
return ""执行完成"";
}}
catch (Exception ex)
{{
_logger.LogError(ex, ""脚本执行过程中发生错误"");
throw;
}}
}}
}}";
var compilation = CSharpCompilation.Create(
$"DynamicAssembly_{Guid.NewGuid():N}",
new[] { CSharpSyntaxTree.ParseText(fullScript) },
GetReferences(),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using var ms = new MemoryStream();
var emitResult = compilation.Emit(ms);
if (!emitResult.Success)
{
var errors = string.Join("\n", emitResult.Diagnostics
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Select(d => d.ToString()));
throw new InvalidOperationException($"脚本编译失败:\n{errors}");
}
ms.Seek(0, SeekOrigin.Begin);
var context = new AssemblyLoadContext($"ScriptContext_{job.Id}_{DateTime.Now:yyyyMMddHHmmss}", true);
try
{
var assembly = context.LoadFromStream(ms);
var type = assembly.GetType("DynamicScript");
// 创建实例,传递所需参数
var instance = Activator.CreateInstance(type!,
_logger, scriptParameters, cancellationToken,
job.Id, job.Name, DateTime.UtcNow);
var method = type!.GetMethod("Execute");
var task = (Task<string>)method!.Invoke(instance, null)!;
result.Output = await task;
}
finally
{
context.Unload();
}
}
技术亮点:
c#private async Task ExecuteCommandScript(JobConfiguration job, JobResult result, CancellationToken cancellationToken)
{
using var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = "/c " + job.Script;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
// 设置环境变量
foreach (var param in job.Parameters)
{
process.StartInfo.EnvironmentVariables[param.Key] = param.Value?.ToString() ?? "";
}
var output = new StringBuilder();
var error = new StringBuilder();
process.OutputDataReceived += (s, e) => {
if (e.Data != null) output.AppendLine(e.Data);
};
process.ErrorDataReceived += (s, e) => {
if (e.Data != null) error.AppendLine(e.Data);
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken);
result.Output = output.ToString();
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"命令执行失败 (退出代码: {process.ExitCode}):\n{error}");
}
}
实战技巧:
c#public IEnumerable<JobConfiguration> GetScheduledJobs(DateTime currentTime)
{
var scheduledJobs = new List<JobConfiguration>();
var utcNow = currentTime.Kind == DateTimeKind.Utc ? currentTime : currentTime.ToUniversalTime();
foreach (var job in _jobsConfiguration.Jobs.Where(j => j.Enabled))
{
if (!_cronCache.TryGetValue(job.Id, out var cronExpression))
continue;
var lastExecution = _lastExecutionTimes.GetValueOrDefault(job.Id, DateTime.MinValue);
if (lastExecution.Kind != DateTimeKind.Utc)
{
lastExecution = lastExecution == DateTime.MinValue ?
DateTime.UtcNow.AddMinutes(-1) : lastExecution.ToUniversalTime();
}
var nextExecution = cronExpression.GetNextOccurrence(lastExecution, TimeZoneInfo.Local);
if (nextExecution.HasValue)
{
var nextLocalTime = TimeZoneInfo.ConvertTimeFromUtc(nextExecution.Value, TimeZoneInfo.Local);
var currentLocalTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, TimeZoneInfo.Local);
if (nextLocalTime <= currentLocalTime)
{
scheduledJobs.Add(job);
_lastExecutionTimes[job.Id] = utcNow;
}
}
}
return scheduledJobs;
}
关键要点:
c#public async Task<JobResult> ExecuteJobWithRetryAsync(JobConfiguration job, CancellationToken cancellationToken)
{
var attempt = 0;
var maxAttempts = job.RetryCount + 1;
while (attempt < maxAttempts)
{
attempt++;
var result = await _scriptExecutor.ExecuteAsync(job, cancellationToken);
result.AttemptNumber = attempt;
if (result.Success || attempt >= maxAttempts)
return result;
_logger.LogWarning("任务 {JobName} 第 {Attempt} 次失败,{DelaySeconds} 秒后重试",
job.Name, attempt, job.RetryIntervalSeconds);
await Task.Delay(TimeSpan.FromSeconds(job.RetryIntervalSeconds), cancellationToken);
}
}
c#private readonly SemaphoreSlim _semaphore;
public ScriptExecutor(IConfiguration configuration)
{
var maxConcurrent = configuration.GetValue<int>("GlobalSettings:MaxConcurrentJobs", 10);
_semaphore = new SemaphoreSlim(maxConcurrent, maxConcurrent);
}
public async Task<JobResult> ExecuteAsync(JobConfiguration job, CancellationToken cancellationToken)
{
await _semaphore.WaitAsync(cancellationToken);
try
{
// 执行任务逻辑
}
finally
{
_semaphore.Release();
}
}
c#using AppScheduledWorkerService.Services;
using AppScheduledWorkerService.Workers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace AppScheduledWorkerService
{
internal class Program
{
static async Task Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
// 注册服务
builder.Services.AddSingleton<IScriptExecutor, ScriptExecutor>();
builder.Services.AddSingleton<IJobScheduler, JobScheduler>();
builder.Services.AddHostedService<ScheduledWorker>();
// 配置日志
builder.Services.AddLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.AddDebug();
if (builder.Environment.IsDevelopment())
{
logging.SetMinimumLevel(LogLevel.Debug);
}
});
var host = builder.Build();
// 优雅关闭处理
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.Register(() =>
{
Console.WriteLine("应用程序正在关闭...");
});
await host.RunAsync();
}
}
}

确保应用有足够权限执行CMD和PowerShell命令:
xml<!-- app.manifest -->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
定期监控任务执行时间和资源消耗:
c#_logger.LogInformation("任务执行完成: {JobName}, 耗时: {Duration}ms, 内存: {Memory}MB",
job.Name, result.Duration.TotalMilliseconds, GC.GetTotalMemory(false) / 1024 / 1024);
支持运行时重新加载配置,无需重启服务:
c#// 监听配置文件变化
var watcher = new FileSystemWatcher(configDirectory, "jobs.json");
watcher.Changed += async (s, e) => await _jobScheduler.ReloadJobsAsync();
这套定时任务框架已经在多个生产环境稳定运行,处理过数万次任务执行,零故障记录。无论是数据处理、系统监控,还是运维自动化,都能完美胜任。
你在项目中是如何处理定时任务的?遇到过哪些坑? 欢迎在评论区分享你的经验,让我们一起打造更完善的解决方案!
觉得这篇文章对你有帮助?请分享给更多需要的同行,让优秀的技术方案惠及更多开发者!
关注我,获取更多C#实战干货,一起在技术路上持续精进!
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!