我的建议是编写一个简单的应用程序,使用Quartz.NET。
创建2个作业:
- 第一个每天触发一次,从数据库中读取当天计划的所有待通知时间,并基于它们创建一些触发器。
- 第二个注册了这些触发器(由第一个作业准备),发送你的通知。
此外,
我强烈建议你为此创建Windows服务,而不是让孤独的控制台应用程序不断运行。它可能会被有权访问服务器的某人意外终止。此外,如果服务器重新启动,您必须手动记住再次打开此类应用程序,而服务可以配置为自动启动。
如果您正在使用Web应用程序,则始终可以在IIS应用程序池进程中托管此逻辑,尽管这是一种非常糟糕的想法。这是因为默认情况下,该进程会定期重启,因此您应更改其默认配置以确保它仍然在夜间工作时仍在工作,而应用程序未使用。除非您的计划任务将被终止。
更新(代码示例):
管理器类,用于调度和取消作业的内部逻辑。出于安全原因,实现为单例:
internal class ScheduleManager
{
private static readonly ScheduleManager _instance = new ScheduleManager();
private readonly IScheduler _scheduler;
private ScheduleManager()
{
var properties = new NameValueCollection();
properties["quartz.scheduler.instanceName"] = "notifier";
properties["quartz.threadPool.type"] = "Quartz.Simpl.SimpleThreadPool, Quartz";
properties["quartz.threadPool.threadCount"] = "5";
properties["quartz.threadPool.threadPriority"] = "Normal";
var sf = new StdSchedulerFactory(properties);
_scheduler = sf.GetScheduler();
_scheduler.Start();
}
public static ScheduleManager Instance
{
get { return _instance; }
}
public void Schedule(IJobDetail job, ITrigger trigger)
{
_scheduler.ScheduleJob(job, trigger);
}
public void Unschedule(TriggerKey key)
{
_scheduler.UnscheduleJob(key);
}
}
首要任务是从数据库中收集所需信息并安排通知(第二个任务):
internal class Setup : IJob
{
public void Execute(IJobExecutionContext context)
{
try
{
foreach (var kvp in DbMock.ScheduleMap)
{
var email = kvp.Value;
var notify = new JobDetailImpl(email, "emailgroup", typeof(Notify))
{
JobDataMap = new JobDataMap {{"email", email}}
};
var time = new DateTimeOffset(DateTime.Parse(kvp.Key).ToUniversalTime());
var trigger = new SimpleTriggerImpl(email, "emailtriggergroup", time);
ScheduleManager.Instance.Schedule(notify, trigger);
}
Console.WriteLine("{0}: all jobs scheduled for today", DateTime.Now);
}
catch (Exception e) { }
}
}
第二份工作,用于发送电子邮件:
internal class Notify: IJob
{
public void Execute(IJobExecutionContext context)
{
try
{
var email = context.MergedJobDataMap.GetString("email");
SendEmail(email);
ScheduleManager.Instance.Unschedule(new TriggerKey(email));
}
catch (Exception e) { }
}
private void SendEmail(string email)
{
Console.WriteLine("{0}: sending email to {1}...", DateTime.Now, email);
}
}
仅为这个特定的示例目的而创建的数据库模拟:
internal class DbMock
{
public static IDictionary<string, string> ScheduleMap =
new Dictionary<string, string>
{
{"00:01", "foo@gmail.com"},
{"00:02", "bar@yahoo.com"}
};
}
应用程序的主要入口:
public class Program
{
public static void Main()
{
FireStarter.Execute();
}
}
public class FireStarter
{
public static void Execute()
{
var setup = new JobDetailImpl("setup", "setupgroup", typeof(Setup));
var midnight = new CronTriggerImpl("setuptrigger", "setuptriggergroup",
"setup", "setupgroup",
DateTime.UtcNow, null, "0 0 0 * * ?");
ScheduleManager.Instance.Schedule(setup, midnight);
}
}
输出:
如果你要使用服务,只需将此主要逻辑放入OnStart
方法中(我建议启动实际逻辑时使用单独的线程而不是等待服务启动,这样可以避免可能的超时 - 在此特定示例中显然不是这样,但总体而言需要这样做):
protected override void OnStart(string[] args)
{
try
{
var thread = new Thread(x => WatchThread(new ThreadStart(FireStarter.Execute)));
thread.Start();
}
catch (Exception e) { }
}
如果是这样,请将逻辑封装在某个包装器中,例如WatchThread,它将捕获来自线程的任何错误:
private void WatchThread(object pointer)
{
try
{
((Delegate) pointer).DynamicInvoke();
}
catch (Exception e) { }
}