在Web微信应用中使用博客园RSS以及Quartz.NET实现博客文章内容的定期推送功能

本篇随笔介绍在Web微信应用中使用博客园RSS以及Quartz.NET实现博客文章内容的定期推送功能,首先对Quartz.NET进行一个简单的介绍和代码分析,掌握对作业调度的处理,然后对博客园RSS内容的处理如何获取,并结合微信消息的群发接口进行内容的发送,从而构建了一个在Web应用中利用作业调度来进行消息发送的业务模型。

Quartz.NET是一个开源的作业调度框架,非常适合在平时的工作中,定时轮询数据库同步,定时邮件通知,定时处理数据等。 Quartz.NET允许开发人员根据时间间隔(或天)来调度作业。它实现了作业和触发器的多对多关系,还能把多个作业与不同的触发器关联。整合了 Quartz.NET的应用程序可以重用来自不同事件的作业,还可以为一个事件组合多个作业。

1、Quartz.NET的使用

Quartz框架的一些基础概念解释:

Scheduler 作业调度器。

IJob 作业接口,继承并实现Execute, 编写执行的具体作业逻辑。

JobBuilder 根据设置,生成一个详细作业信息(JobDetail)。

TriggerBuilder 根据规则,生产对应的Trigger

官方的使用案例代码如下所示

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        Common.Logging.LogManager.Adapter = new Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter { Level = Common.Logging.LogLevel.Info };

        // Grab the Scheduler instance from the Factory 
        IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();

        // and start it off
        scheduler.Start();

        // define the job and tie it to our HelloJob class
        IJobDetail job = JobBuilder.Create<HelloJob>()
            .WithIdentity("job1", "group1")
            .Build();

        // Trigger the job to run now, and then repeat every 10 seconds
        ITrigger trigger = TriggerBuilder.Create()
            .WithIdentity("trigger1", "group1")
            .StartNow()
            .WithSimpleSchedule(x => x
                .WithIntervalInSeconds(10)
                .RepeatForever())
            .Build();

        // Tell quartz to schedule the job using our trigger
        scheduler.ScheduleJob(job, trigger);

        // some sleep to show what's happening
        Thread.Sleep(TimeSpan.FromSeconds(60));

        // and last shut down the scheduler when you are ready to close your program
        scheduler.Shutdown();
    }
    catch (SchedulerException se)
    {
        Console.WriteLine(se);
    }

    Console.WriteLine("Finished");
}

启动定义一个HelloJOb的对象,如下代码所示

    public class HelloJob : IJob
    {
        public void Execute(IJobExecutionContext context)
        {
            Console.WriteLine("Greetings from HelloJob!");
        }
    }

2、Quartz的cron表达式

cron expressions 整体上还是非常容易理解的,只有一点需要注意:"?"号的用法,看下文可以知道“?”可以用在 day of month 和 day of week中,他主要是为了解决如下场景,如:每月的1号的每小时的31分钟,正确的表达式是:* 31 * 1 * ?,而不能是:* 31 * 1 * *,因为这样代表每周的任意一天。

由7段构成:秒 分 时 日 月 星期 年(可选)
"-" :表示范围 MON-WED表示星期一到星期三
"," :表示列举 MON,WEB表示星期一和星期三
"*" :表是“每”,每月,每天,每周,每年等
"/" :表示增量:0/15(处于分钟段里面) 每15分钟,在0分以后开始,3/20 每20分钟,从3分钟以后开始
"?" :只能出现在日,星期段里面,表示不指定具体的值
"L" :只能出现在日,星期段里面,是Last的缩写,一个月的最后一天,一个星期的最后一天(星期六)
"W" :表示工作日,距离给定值最近的工作日
"#" :表示一个月的第几个星期几,例如:"6#3"表示每个月的第三个星期五(1=SUN...6=FRI,7=SAT)

官方cron表达式实例

表达式 代表意义
0 0 12 * * ? 每天中午12点触发
0 15 10 ? * * 每天上午10:15触发
0 15 10 * * ? 每天上午10:15触发
0 15 10 * * ? * 每天上午10:15触发
0 15 10 * * ? 2005 2005年的每天上午10:15触发
0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
0 15 10 15 * ? 每月15日上午10:15触发
0 15 10 L * ? 每月最后一日的上午10:15触发
0 15 10 L-2 * ? Fire at 10:15am on the 2nd-to-last last day of every month
0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
0 15 10 ? * 6L Fire at 10:15am on the last Friday of every month
0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
0 0 12 1/5 * ? Fire at 12pm (noon) every 5 days every month, starting on the first day of the month.
0 11 11 11 11 ? Fire every November 11th at 11:11am.

3、Quartz.NET的应用案例

我曾经在统一接口的Web API后台,使用了这个Quartz.NET来实现站场信息的同步处理,这样可以把其他供应商提供的接口数据,同步到本地,可以加快数据的检索和处理效率。

具体代码如下所示。

首先是在Global.asax的后台代码里面进行同步代码处理。

public class WebApiApplication : System.Web.HttpApplication
{
    IScheduler scheduler = null;

    protected void Application_Start()
    {
        GlobalConfiguration.Configuration.EnableCors();
        GlobalConfiguration.Configure(WebApiConfig.Register);

        //创建执行同步的处理
        ISchedulerFactory sf = new StdSchedulerFactory();
        scheduler = sf.GetScheduler();

        CalendarTask();
        CreateOnceJob();

        //启动所有的任务
        scheduler.Start();
    }
    protected void Application_End(object sender, EventArgs e)
    {
        if(scheduler != null)
        {
            scheduler.Shutdown(true);
        }
    }

    /// <summary>
    /// 创建同步任务
    /// </summary>
    private void CalendarTask()
    {
        IJobDetail job = JobBuilder.Create<StationSyncJob>()
             .WithIdentity("StationSyncJob", "group1")
             .Build();

        //每天凌晨1点执行一次:0 0 1 * * ?
        ICronTrigger trigger = (ICronTrigger)TriggerBuilder.Create()
                                                  .WithIdentity("trigger1", "group1")       //"0 34,36,38,40 * * * ?"
                                                  .WithCronSchedule("0 0 1 * * ?")//"0 0 1 * * ?"
                                                  .Build();

        DateTimeOffset ft = scheduler.ScheduleJob(job, trigger);
        LogTextHelper.Info(string.Format("您在 {0} 时候创建了Quartz任务", DateTime.Now));
    }

    private void CreateOnceJob()
    {
        IJobDetail onceJob = JobBuilder.Create<StationSyncJob>()
                             .WithIdentity("onceJob", "group1")
                             .Build();
        //启动的时候运行一次
        DateTimeOffset startTime = DateBuilder.NextGivenSecondDate(null, 30);
        ISimpleTrigger simpleTrigger = (ISimpleTrigger)TriggerBuilder.Create()
                                                      .WithIdentity("simpleOnce", "group1")
                                                      .StartAt(startTime)
                                                      .Build();
        DateTimeOffset ft = scheduler.ScheduleJob(onceJob, simpleTrigger);
    }

}

其中同步站场信息的Job实现如下所示(这里是通过调用第三方接口获取数据,然后把它们保存到本地,这个定时服务设定在每天的一个是时间点上执行,如凌晨1点时刻)。

    /// <summary>
    /// 同步站场信息
    /// </summary>
    public class StationSyncJob : IJob
    {
        public void Execute(IJobExecutionContext context)
        {
            LogTextHelper.Info(string.Format("您在 {0} 时候调用【同步站场信息】一次", DateTime.Now));

            StationDetailResult result = new StationDetailResult();
            try
            {
                QueryStationJson json = new QueryStationJson();//空查询,一次性查询所有

                BaseDataAgent agent = new BaseDataAgent();
                result = agent.QueryStationDetail(json);
                if(result != null && result.success)
                {
                    foreach(StationDetailJson detail in result.data)
                    {
                        StationInfo info = detail.ConvertInfo();
                        try
                        {
                            BLLFactory<Station>.Instance.InsertIfNew(info);
                        }
                        catch(Exception ex)
                        {
                            LogTextHelper.Error(ex);
                            LogTextHelper.Info(info.ToJson());
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                result.errmsg = ex.Message;
                result.success = false;

                LogTextHelper.Error(ex);
            }
        }
    }

4、博客的RSS

原则上我们可以利用任何RSS来源来获取响应的博客内容,这里我以自己博客园的RSS源进行介绍使用,我们每个博客园的账号都有一个如下的连接,提供我们最新的博客列表信息。



打开连接,可以看到它的内容就是最新显示的博客内容,如下所示



处理RSS的内容,我们使用内置的SyndicationFeed对象来处理即可,非常方便。
string url = "http://feed.cnblogs.com/blog/u/12391/rss";
XmlReader reader = XmlReader.Create(url);
SyndicationFeed feed = SyndicationFeed.Load(reader);
reader.Close();

上面代码就是获取到对应的RSS内容,然后把它们转换为XMLReader进行解析即可。

然后可以通过一个遍历的处理就可以获取到其中各个的XML节点内容了,非常方便。

foreach (SyndicationItem item in feed.Items)
{
    var id = item.Id;
    string subject = item.Title.Text;    
    string summary = item.Summary.Text;
 }

5、在微信应用中发送博客内容

通过上面的RSS读取操作,我们可以获得对应的博客内容,如果我们需要每周给客户发送一些内容,那么这些就可以通过上面RSS源进行处理发送了。
关于发送文本消息的处理,可以参考我的随笔文章《C#开发微信门户及应用(3)--文本消息和图文消息的应答
这里我就直接应用上面的接口对内容进行处理发送,具体接口的逻辑就不再罗列。

/// <summary>
/// 获取博客园文章(RSS)并发送文本给指定的用户
/// </summary>
private void GetCnblogsArticles()
{
    string url = "http://feed.cnblogs.com/blog/u/12391/rss";
    XmlReader reader = XmlReader.Create(url);
    SyndicationFeed feed = SyndicationFeed.Load(reader);
    reader.Close();

    ICustomerApi api = new CustomerApi();
    foreach (SyndicationItem item in feed.Items)
    {
        Console.WriteLine(item.ToJson());
        var id = item.Id;
        string subject = item.Title.Text;    
        string summary = item.Summary.Text;


        var content = string.Format("<a href='{0}'>{1}</a>", id, subject);
        CommonResult result = api.SendText(token, openId, content);
        Console.WriteLine("发送内容:" + (result.Success ? "成功" : "失败:" + result.ErrorMessage));
    }
}

得到的界面效果如下所示。



但是这样的效果还是有点差强人意,我们知道微信里面有图文消息的接口,可以利用图文消息的接口进行发送,则更加美观一些。
调整后的代码如下所示。

/// <summary>
/// 发送博客图文消息给指定用户
/// </summary>
private void SendBlogsNews()
{
    List<ArticleEntity> list = new List<ArticleEntity>();

    string url = "http://feed.cnblogs.com/blog/u/12391/rss";
    XmlReader reader = XmlReader.Create(url);
    SyndicationFeed feed = SyndicationFeed.Load(reader);
    reader.Close();

    int i = 0;
    foreach (SyndicationItem item in feed.Items)
    {
        list.Add(
            new ArticleEntity
            {
                Title = item.Title.Text,
                Description = item.Summary.Text,
                PicUrl = i == 0 ? "http://www.iqidi.com/Content/Images/cnblogs_whc.png" : "http://www.iqidi.com/Content/Images/frame_web.png",
                Url = item.Id
            });
        if(i >= 8)
        {
            break;
        }
        i++;
    }

    ICustomerApi customerApi = new CustomerApi();
    var result = customerApi.SendNews(token, openId, list);
}

这样就是发送图文消息的代码,需要重新构建一个实体类集合进行发送,得到发送的效果如下所示。


整体的界面效果就是我们需要的效果了,不过如果我们需要使用批量发送给订阅用户的话,那么我们需要使用消息的群发接口,群发的消息接口封装如需了解,可以参考文章《C#开发微信门户及应用(30)--消息的群发处理和预览功能》。
整个群发消息的逻辑代码如下所示,主要逻辑就是获取博客文章,并上传文章的图片,接着上传需要群发的图文消息资源,最后调用群发接口进行消息的发送即可。

private void BatchSendBlogNews()
{
    List<NewsUploadJson> list = new List<NewsUploadJson>();

    string url = "http://feed.cnblogs.com/blog/u/12391/rss";
    XmlReader reader = XmlReader.Create(url);
    SyndicationFeed feed = SyndicationFeed.Load(reader);
    reader.Close();

    //上传图片获取MediaId
    IMediaApi mediaApi = new MediaApi();
    var result1 = mediaApi.UploadTempMedia(token, UploadMediaFileType.image, @"E:\我的网站资料\iqidiSoftware\content\images\cnblogs_whc.png");//"http://www.iqidi.com/Content/Images/cnblogs_whc.png");
    var result2 = mediaApi.UploadTempMedia(token, UploadMediaFileType.image, @"E:\我的网站资料\iqidiSoftware\content\images\frame_web.png");//"http://www.iqidi.com/Content/Images/frame_web.png");
    if (result1 != null && result2 != null)
    {
        int i = 0;
        foreach (SyndicationItem item in feed.Items)
        {
            list.Add(
                new NewsUploadJson
                {
                    author = "伍华聪",
                    title = item.Title.Text,
                     content = item.Summary.Text,
                    //digest = item.Summary.Text,
                    thumb_media_id = i == 0 ? result1.media_id : result2.media_id,
                    content_source_url = item.Id,
                });
            if (i >= 8)
            {
                break;
            }
            i++;
        }
    }

    if (list.Count > 0)
    {
        UploadJsonResult resultNews = mediaApi.UploadNews(token, list);
        if (resultNews != null)
        {                    
            IMassSendApi massApi = new MassSendApi();
            var result = massApi.SendByGroup(token, MassMessageType.mpnews, resultNews.media_id, "0", true);
        }
        else
        {
            Console.WriteLine("上传图文消息失败");
        }
    }
}

群发的消息在微信上看到内容和前面的差不多,不过点击并不会直接跳转链接,而是进去到一个详细内容的页面里面,只有单击阅读原文才进行跳转URL,如下所示。


6.结合Quartz.NET实现博客文章内容的定期推送功能

在Web微信应用中使用博客RSS以及Quartz.NET实现文章内容的定期推送功能,我们需要结合Quartz.NET的作业调度处理、微信接口的内容发送,以及博文RSS内容的获取处理,三者整合进行实现整个功能。
首先我们根据上面的代码,设计好调度的Job内容,如下所示。



然后,在Web应用的Global.asa的后台代码里面,编写代码启动作业调度即可。
而根据前面Corn表达式的说明,我们要每周定时发送一次的的规则,如下所示。
每周星期天凌晨1点实行一次: 0 0 1 ? * L
这样我们最终的Globa.asa后台代码如下所示。

public class Global : HttpApplication
{
    private IScheduler scheduler = null;

    void Application_Start(object sender, EventArgs e)
    {
        // 在应用程序启动时运行的代码
        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);

        BundleConfig.RegisterBundles(BundleTable.Bundles);


        //构造调度对象,并创建对应的调度任务
        scheduler = StdSchedulerFactory.GetDefaultScheduler();
        CalendarTask();

        //启动所有的任务
        scheduler.Start();
    }

    protected void Application_End(object sender, EventArgs e)
    {
        if (scheduler != null)
        {
            scheduler.Shutdown(true);
        }
    }


    private void CalendarTask()
    {
        IJobDetail job = JobBuilder.Create<BlogArticleSendJob>()
             .WithIdentity("BlogArticleSendJob", "group1")
             .Build();

        //每周星期天凌晨1点实行一次:0 0 1 ? * L
        ICronTrigger trigger = (ICronTrigger)TriggerBuilder.Create()
                                                  .WithIdentity("trigger1", "group1")
                                                  .WithCronSchedule("0 0 1 ? * L")//0 0 1 ? * L
                                                  .Build();

        DateTimeOffset ft = scheduler.ScheduleJob(job, trigger);
        LogTextHelper.Info(string.Format("您在 {0} 时候创建了Quartz任务", DateTime.Now));
    }

综合上面的思路,我们可以利用Quartz.NET做成更多的数据同步任务调度,另外在微信应用中,我们也可以整合很多组件或者控件,来实现更加弹性化的业务支持,如消息群发、访客汇总,内容同步等处理。

以上就是我的一些组件代码的应用思路,其实我们只要涉猎更广一些,很多东西可以使用拿来主义,经过自己的整合优化,可以为我们服务的更好。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容