Asp.Net Core2.0 基於QuartzNet任務管理系統

Quartz.NET官網地址:https://www.quartz-scheduler.net/html

Quartz.NET文檔地址:https://www.quartz-scheduler.net/documentation/index.htmlgit

Quartz.NET是一個開源的做業調度框架,是OpenSymphonyQuartz API的.NET移植,它用C#寫成,可用於winformasp.net應用中。它提供了巨大的靈活性而不犧牲簡單性。你可以用它來爲執行一個做業而建立簡單的或複雜的調度。它有不少特徵,如:數據庫支持,集羣,插件,支持cron-like表達式等等。github

如今Quartz.NET3.0已支持Asp.Net Core,3.0新功能以下:sql

新功能數據庫

  • 具備異步/等待支持的基於任務的做業,內部以異步/等待方式工做
  • 支持.NET Core / netstandard 2.0和.NET Framework 4.5.2及更高版本
  • 經過提供程序名稱SQLite-Microsoft支持Microsoft.Data.Sqlite,舊的提供程序SQLite也仍然有效
  • 增長了SQL Server內存優化表和Quartz.Impl.AdoJobStore.UpdateLockRowSemaphoreMOT的初步支持
  • Common.Logging從相關性中刪除
  • ILMerge進程中刪除的C5集合再也不須要
  • 在插件啓動時添加對做業調度XML文件的急切驗證的支持
  • TimeZoneUtil中添加對額外的自定義時區解析器功能的支持

變化後端

  • 做業和插件如今位於獨立的程序集NuGetQuartz.JobsQuartz.Plugins
  • ADO.NET提供者名稱已被簡化,提供者名稱沒有版本,例如SqlServer-20 => SqlServer
  • API方法已被從新使用,主要使用IReadOnlyCollection,這隱藏了兩個HashSets和List小號
  • LibLog一直隱藏於內部(ILog等),就像它本來打算的那樣
  • SimpleThreadPool消失了,舊的擁有的線程消失了
  • 調度程序方法已更改成基於任務,請記住等待它們
  • IJob接口如今返回一個任務
  • 一些IList屬性已更改成IReadOnlyList以正確反映意圖
  • SQL Server CE支持已被刪除
  • DailyCalendar如今將日期時間用於排除的日期,並具備ISet接口來訪問它們
  • IObjectSerializer有新的方法,void Initialize(),必須實現
  • IInterruptableJob取消了上下文的CancellationToken

Quartz API的關鍵接口和類是api

  • IScheduler - 與調度程序交互的主要API。
  • IJob - 您但願由調度程序執行的組件實現的接口。
  • IJobDetail - 用於定義做業的實例。
  • ITrigger - 定義執行給定Job的時間表的組件。
  • JobBuilder - 用於定義/構建定義做業實例的JobDetail實例。
  • TriggerBuilder - 用於定義/構建觸發器實例

1、Quartz.NET基本使用

一、新建Asp.Net Core 項目,使用NuGet添加Quartz,或使用程序包管理器引用,命令以下:
Install-Package Quartz

 若是你想添加JSON序列化,只須要以一樣的方式添加Quartz.Serialization.Json包。框架

二、簡單實例,代碼以下:
using Five.QuartzNetJob.ExecuteJobTask.Service;
using Quartz;
using Quartz.Impl;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Threading.Tasks;

namespace Five.QuartzNetJob.Web.Controllers
{
    public class TestTask
    {
        public async Task StartTestAsync() 
        {
            try
            {
                // 從工廠中獲取調度程序實例
                NameValueCollection props = new NameValueCollection
                {
                    { "quartz.serializer.type", "binary" }
                };
                StdSchedulerFactory factory = new StdSchedulerFactory(props);
                IScheduler scheduler = await factory.GetScheduler();
               
                // 開啓調度器
                await scheduler.Start();

                // 定義這個工做,並將其綁定到咱們的IJob實現類
                IJobDetail job = JobBuilder.Create<HelloJob>()
                    .WithIdentity("job1", "group1")
                    .Build();

                // 觸發做業當即運行,而後每10秒重複一次,無限循環
                ITrigger trigger = TriggerBuilder.Create()
                    .WithIdentity("trigger1", "group1")
                    .StartNow()
                    .WithSimpleSchedule(x => x
                        .WithIntervalInSeconds(10)
                        .RepeatForever())
                    .Build();

                // 告訴Quartz使用咱們的觸發器來安排做業
                await scheduler.ScheduleJob(job, trigger);

                // 等待60秒
                await Task.Delay(TimeSpan.FromSeconds(60));

                // 關閉調度程序
                await scheduler.Shutdown();
            }
            catch (SchedulerException se)
            {
                await Console.Error.WriteLineAsync(se.ToString());
            }
        }
    }
}

 HelloJob內容以下:asp.net

using Quartz;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Five.QuartzNetJob.ExecuteJobTask.Service
{
    public class HelloJob : IJob
    {
        public  Task Execute(IJobExecutionContext context)
        {
             Console.Out.WriteLineAsync("Greetings from HelloJob!");
            return Task.CompletedTask;
        }
    }
}

執行效果以下:異步

Quartz的版本3.0.3中刪除了IJob實現的異地調用,也就是不支持asyncawait異步調用,3.0.2版本支持異步調用。

2、觸發器類型

 一、SimpleTrigger觸發器(簡單觸發器)

SimpleTrigger的屬性包括:開始時間和結束時間,重複計數和重複間隔。重複計數能夠是零,一個正整數或常數值SimpleTrigger.RepeatIndefinitely。重複時間間隔屬性必須是TimeSpan.Zero或正的TimeSpan值。請注意,重複間隔爲0會致使觸發器的「重複計數」觸發同時發生。

SimpleTrigger實例使用TriggerBuilder(用於觸發器的主屬性)和WithSimpleSchedule擴展方法(用於SimpleTrigger特定的屬性)構建。

在特定的時間內創建觸發器,無需重複,代碼以下:

// 觸發器構建器默認建立一個簡單的觸發器,實際上返回一個ITrigger
ISimpleTrigger trigger = (ISimpleTrigger)TriggerBuilder.Create()
    .WithIdentity("trigger1", "group1")
    .StartAt(DateTime.Now) //指定開始時間爲當前系統時間
    .ForJob("job1", "group1") //經過JobKey識別做業
    .Build();

在特定的時間創建觸發器,而後每十秒鐘重複十次:

// 觸發器構建器默認建立一個簡單的觸發器,實際上返回一個ITrigger
ITrigger trigger = trigger = TriggerBuilder.Create()
   .WithIdentity("trigger2", "group2")
   .StartAt(DateTime.Now) // 指定開始時間
   .WithSimpleSchedule(x => x
   .WithIntervalInSeconds(10)
   .WithRepeatCount(10)) // 請注意,重複10次將總共重複11次
   .ForJob("job2", "group2") //經過JobKey識別做業                   
   .Build();

構建一個觸發器,將在將來五分鐘內觸發一次:

// 觸發器構建器默認建立一個簡單的觸發器,實際上返回一個ITrigger
ITrigger trigger = trigger = (ISimpleTrigger)TriggerBuilder.Create()
   .WithIdentity("trigger3", "group3")
   .StartAt(DateBuilder.FutureDate(5, IntervalUnit.Minute)) //使用DateBuilder未來建立一個時間日期
   .ForJob("job3", "group3") //經過JobKey識別做業
   .Build();

創建一個如今當即觸發的觸發器,而後每隔五分鐘重複一次,直到22:00:

// 觸發器構建器默認建立一個簡單的觸發器,實際上返回一個ITrigger
ITrigger trigger = trigger = TriggerBuilder.Create()
   .WithIdentity("trigger4", "group4")
   .WithSimpleSchedule(x => x
        .WithIntervalInMinutes(5)//每5秒執行一次
        .RepeatForever())
   .EndAt(DateBuilder.DateOf(22, 0, 0))//晚上22點結束
   .Build();

創建一個觸發器,在一個小時後觸發,而後每2小時重複一次:

// 觸發器構建器默認建立一個簡單的觸發器,實際上返回一個ITrigger
ITrigger  trigger = TriggerBuilder.Create()
   .WithIdentity("trigger5") // 因爲未指定組,所以「trigger5」將位於默認分組中
   .StartAt(DateBuilder.EvenHourDate(null)) // 獲取下個一小時時間                 
   .WithSimpleSchedule(x => x
        .WithIntervalInHours(2)//執行間隔2小時
        .RepeatForever())
   .Build(); 

所以簡單的任務調度使用SimpleTrigger徹底夠用,若是SimpleTrigger仍是不能知足您的需求請往下看。

二、CronTrigger觸發器

若是你須要一個基於相似日曆的概念而不是精確指定的SimpleTrigger時間間隔的工做調度計劃,CronTriggers一般比SimpleTrigger更有用。

使用CronTrigger,您能夠在每週一,週三的上午9點至上午10點之間指定開始時間表,例如「每星期五中午」或「每一個工做日和上午9點30分」,或者「每5分鐘」和星期五」。

即便如此,就像SimpleTrigger同樣,CronTrigger有一個startTime,它指定了時間表的生效時間,還有一個(可選的)endTime,用於指定應該中止時間表的時間。

這裏不在詳細介紹Cron

Cron表達式在線生成器:http://cron.qqe2.com/

Cron表達式詳細介紹:https://www.jianshu.com/p/e9ce1a7e1ed1

天天早上8點到下午5點創建一個觸發器,每隔一分鐘就會觸發一次:

// 觸發器構建器默認建立一個簡單的觸發器,實際上返回一個ITrigger
ITrigger trigger = TriggerBuilder.Create()
   .WithIdentity("Job1", "group1")
   .WithCronSchedule("0 0/2 8-17 * * ?")//使用Cron表達式
   .ForJob("Job1", "group1")
   .Build();

創建一個觸發器,天天在上午10:42開始執行:

// 觸發器構建器默認建立一個簡單的觸發器,實際上返回一個ITrigger
ITrigger trigger = TriggerBuilder.Create()
   .WithIdentity("Job2", "group2")
   .WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(10, 42)) // 在這裏使用CronScheduleBuilder的靜態輔助方法
   .ForJob("Job2", "group2")
   .Build();

構建一個觸發器,將在星期三上午10:42在除系統默認值以外的TimeZone中觸發:

// 觸發器構建器默認建立一個簡單的觸發器,實際上返回一個ITrigger
ITrigger trigger = TriggerBuilder.Create()
   .WithIdentity("Job3", "group3")
   .WithCronSchedule("0 42 10 ? * WED", x => x
   .InTimeZone(TimeZoneInfo.FindSystemTimeZoneById("Central America Standard Time")))
   .ForJob("Job3", "group3")
   .Build();

總結: Quartz.NET的3.0版本跟以前的版本api接口變化並不大。只是在3.0版本中添加了異步調用,並支持.net core。簡單的任務調度使用官網中的實例便可知足需求。

3、後端項目管理

在網上找到不少關於asp.net 的任務管理,但沒有找到.net core 相關的後端任務管理。

我就根據官網還有網上dalao開源分享的項目實現了個簡單的後端任務。

項目地址:https://github.com/Potato-MC/QuartzNetJob

項目效果圖請看:http://www.cnblogs.com/miskis/p/8484252.html

項目結構

項目使用asp.net core 2.0--->adminlte-2.4.2-->SqlSugar-4.6.4.3。

數據庫表可以使用SqlSugar來生成。

public IActionResult Index()
{
    //生成表
    //var db = DataHelper.GetInstance();
    //db.CodeFirst.InitTables(typeof(OperateLog), typeof(OperateLog));
    return View();
 }

 

結構以下:

Five.QuartzNetJob.DataService.DataHelper----------------------------------是ORM層,使用的是開源框架SqlSugar(官網地址:http://www.codeisbug.com/Doc/8

Five.QuartzNetJob.DataService.Models---------------------------------------是實體類

Five.QuartzNetJob.ExecuteJobTask.Service---------------------------------IJob實現層

QuartzNet.Entity---------------------------------------------------------------------調度中心相關實體類

QuartzNet2.Core--------------------------------------------------------------------非.Net Core版本的調度管理中心,使用的是.net framework 4.6

Five.QuartzNetJob.Utils.Tool-----------------------------------------------------通用工具類庫

Five.QuartzNetJob.Web-----------------------------------------------------------後端

項目很簡單,就只實現了增刪改查。

統一管理任務調度,項目運行時開啓任務調度並執行已開啓的任務。

由於項目太過於簡單,就不在詳細介紹。

下面貼出任務調度中心代碼,歡迎各位dalao發表意見:

using QuartzNet.Entity;
using Quartz;
using Quartz.Impl;
using System;
using System.Collections.Specialized;
using System.Threading.Tasks;
using Five.QuartzNetJob.Utils.Tool;
using System.Reflection;
using System.Collections.Generic;

namespace QuartzNet3.Core
{
    /// <summary>
    /// 任務調度中心
    /// </summary>
    public class SchedulerCenter
    {
        /// <summary>
        /// 任務調度對象
        /// </summary>
        public static readonly SchedulerCenter Instance;
        static SchedulerCenter()
        {
            Instance = new SchedulerCenter();
        }
        private Task<IScheduler> _scheduler;

        /// <summary>
        /// 返回任務計劃(調度器)
        /// </summary>
        /// <returns></returns>
        private Task<IScheduler> Scheduler
        {
            get
            {
                if (this._scheduler != null)
                {
                    return this._scheduler;
                }
                // 從Factory中獲取Scheduler實例
                NameValueCollection props = new NameValueCollection
                {
                    { "quartz.serializer.type", "binary" },
                    //如下配置須要數據庫表配合使用,表結構sql地址:https://github.com/quartznet/quartznet/tree/master/database/tables
                    //{ "quartz.jobStore.type","Quartz.Impl.AdoJobStore.JobStoreTX, Quartz"},
                    //{ "quartz.jobStore.driverDelegateType","Quartz.Impl.AdoJobStore.StdAdoDelegate, Quartz"},
                    //{ "quartz.jobStore.tablePrefix","QRTZ_"},
                    //{ "quartz.jobStore.dataSource","myDS"},
                    //{ "quartz.dataSource.myDS.connectionString",AppSettingHelper.MysqlConnection},//鏈接字符串
                    //{ "quartz.dataSource.myDS.provider","MySql"},
                    //{ "quartz.jobStore.useProperties","true"}

                };
                StdSchedulerFactory factory = new StdSchedulerFactory(props);
                return this._scheduler = factory.GetScheduler();
            }
        }
      
        /// <summary>
        /// 運行指定的計劃(映射處理IJob實現類)
        /// </summary>
        /// <param name="jobGroup">任務分組</param>
        /// <param name="jobName">任務名稱</param>
        /// <returns></returns>
        public async Task<BaseQuartzNetResult> RunScheduleJob<T>(string jobGroup, string jobName) where T : ScheduleManage
        {
            BaseQuartzNetResult result;
            //開啓調度器
            await this.Scheduler.Result.Start();
            //建立指定泛型類型參數指定的類型實例
            T t = Activator.CreateInstance<T>();
            //獲取任務實例
            ScheduleEntity scheduleModel = t.GetScheduleModel(jobGroup, jobName);
            //添加任務
            var addResult = AddScheduleJob(scheduleModel).Result;
            if (addResult.Code == 1000)
            {
                scheduleModel.Status = EnumType.JobStatus.已啓用;
                t.UpdateScheduleStatus(scheduleModel);
                //用給定的密鑰恢復(取消暫停)IJobDetail
                await this.Scheduler.Result.ResumeJob(new JobKey(jobName, jobGroup));
                result = new BaseQuartzNetResult
                {
                    Code = 1000,
                    Msg = "啓動成功"
                };
            }
            else
            {
                result = new BaseQuartzNetResult
                {
                    Code = -1
                };
            }
            return result;
        }
        /// <summary>
        /// 添加一個工做調度(映射程序集指定IJob實現類)
        /// </summary>
        /// <param name="m"></param>
        /// <returns></returns>
        private async Task<BaseQuartzNetResult> AddScheduleJob(ScheduleEntity m)
        {
            var result = new BaseQuartzNetResult();
            try
            {

                //檢查任務是否已存在
                var jk = new JobKey(m.JobName, m.JobGroup);
                if (await this.Scheduler.Result.CheckExists(jk))
                {
                    //刪除已經存在任務
                    await this.Scheduler.Result.DeleteJob(jk);
                }
                //反射獲取任務執行類
                var jobType = FileHelper.GetAbsolutePath(m.AssemblyName, m.AssemblyName + "." + m.ClassName);
                // 定義這個工做,並將其綁定到咱們的IJob實現類
                IJobDetail job = new JobDetailImpl(m.JobName, m.JobGroup, jobType);
                //IJobDetail job = JobBuilder.CreateForAsync<T>().WithIdentity(m.JobName, m.JobGroup).Build();
                // 建立觸發器
                ITrigger trigger;
                //校驗是否正確的執行週期表達式
                if (!string.IsNullOrEmpty(m.Cron) && CronExpression.IsValidExpression(m.Cron))
                {
                    trigger = CreateCronTrigger(m);
                }
                else
                {
                    trigger = CreateSimpleTrigger(m);
                }

                // 告訴Quartz使用咱們的觸發器來安排做業
                await this.Scheduler.Result.ScheduleJob(job, trigger);

                result.Code = 1000;
            }
            catch (Exception ex)
            {
                await Console.Out.WriteLineAsync(string.Format("添加任務出錯{0}", ex.Message));
                result.Code = 1001;
                result.Msg = ex.Message;
            }
            return result;
        }
        /// <summary>
        /// 運行指定的計劃(泛型指定IJob實現類)
        /// </summary>
        /// <param name="jobGroup">任務分組</param>
        /// <param name="jobName">任務名稱</param>
        /// <returns></returns>
        public async Task<BaseQuartzNetResult> RunScheduleJob<T, V>(string jobGroup, string jobName) where T : ScheduleManage, new() where V : IJob
        {
            BaseQuartzNetResult result;
            //開啓調度器
            await this.Scheduler.Result.Start();
            //建立指定泛型類型參數指定的類型實例
            T t = Activator.CreateInstance<T>();
            //獲取任務實例
            ScheduleEntity scheduleModel = t.GetScheduleModel(jobGroup, jobName);
            //添加任務
            var addResult = AddScheduleJob<V>(scheduleModel).Result;
            if (addResult.Code == 1000)
            {
                scheduleModel.Status = EnumType.JobStatus.已啓用;
                t.UpdateScheduleStatus(scheduleModel);
                //用給定的密鑰恢復(取消暫停)IJobDetail
                await this.Scheduler.Result.ResumeJob(new JobKey(jobName, jobGroup));
                result = new BaseQuartzNetResult
                {
                    Code = 1000,
                    Msg = "啓動成功"
                };
            }
            else
            {
                result = new BaseQuartzNetResult
                {
                    Code = -1
                };
            }
            return result;
        }
        /// <summary>
        /// 添加任務調度(指定IJob實現類)
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="m"></param>
        /// <returns></returns>
        private async Task<BaseQuartzNetResult> AddScheduleJob<T>(ScheduleEntity m) where T : IJob
        {
            var result = new BaseQuartzNetResult();
            try
            {

                //檢查任務是否已存在
                var jk = new JobKey(m.JobName, m.JobGroup);
                if (await this.Scheduler.Result.CheckExists(jk))
                {
                    //刪除已經存在任務
                    await this.Scheduler.Result.DeleteJob(jk);
                }
                //反射獲取任務執行類
                // var jobType = FileHelper.GetAbsolutePath(m.AssemblyName, m.AssemblyName + "." + m.ClassName);
                // 定義這個工做,並將其綁定到咱們的IJob實現類
                //IJobDetail job = new JobDetailImpl(m.JobName, m.JobGroup, jobType);
                IJobDetail job = JobBuilder.CreateForAsync<T>().WithIdentity(m.JobName, m.JobGroup).Build();
                // 建立觸發器
                ITrigger trigger;
                //校驗是否正確的執行週期表達式
                if (!string.IsNullOrEmpty(m.Cron) && CronExpression.IsValidExpression(m.Cron))
                {
                    trigger = CreateCronTrigger(m);
                }
                else
                {
                    trigger = CreateSimpleTrigger(m);
                }

                // 告訴Quartz使用咱們的觸發器來安排做業
                await this.Scheduler.Result.ScheduleJob(job, trigger);

                result.Code = 1000;
            }
            catch (Exception ex)
            {
                await Console.Out.WriteLineAsync(string.Format("添加任務出錯", ex.Message));
                result.Code = 1001;
                result.Msg = ex.Message;
            }
            return result;
        }
        /// <summary>
        /// 暫停指定的計劃
        /// </summary>
        /// <param name="jobGroup">任務分組</param>
        /// <param name="jobName">任務名稱</param>
        /// <param name="isDelete">中止並刪除任務</param>
        /// <returns></returns>
        public BaseQuartzNetResult StopScheduleJob<T>(string jobGroup, string jobName, bool isDelete = false) where T : ScheduleManage, new()
        {
            BaseQuartzNetResult result;
            try
            {
                this.Scheduler.Result.PauseJob(new JobKey(jobName, jobGroup));
                if (isDelete)
                {
                    Activator.CreateInstance<T>().RemoveScheduleModel(jobGroup, jobName);
                }
                result = new BaseQuartzNetResult
                {
                    Code = 1000,
                    Msg = "中止任務計劃成功!"
                };
            }
            catch (Exception ex)
            {
                result = new BaseQuartzNetResult
                {
                    Code = -1,
                    Msg = "中止任務計劃失敗"
                };
            }
            return result;
        }
        /// <summary>
        /// 恢復運行暫停的任務
        /// </summary>
        /// <param name="jobName">任務名稱</param>
        /// <param name="jobGroup">任務分組</param>
        public async void ResumeJob(string jobName, string jobGroup)
        {
            try
            {
                //檢查任務是否存在
                var jk = new JobKey(jobName, jobGroup);
                if (await this.Scheduler.Result.CheckExists(jk))
                {
                    //任務已經存在則暫停任務
                    await this.Scheduler.Result.ResumeJob(jk);
                    await Console.Out.WriteLineAsync(string.Format("任務「{0}」恢復運行", jobName));
                }
            }
            catch (Exception ex)
            {
                await Console.Out.WriteLineAsync(string.Format("恢復任務失敗!{0}", ex));
            }
        }
       
        /// <summary>
        /// 中止任務調度
        /// </summary>
        public async void StopScheduleAsync()
        {
            try
            {
                //判斷調度是否已經關閉
                if (!this.Scheduler.Result.IsShutdown)
                {
                    //等待任務運行完成
                    await this.Scheduler.Result.Shutdown();
                    await Console.Out.WriteLineAsync("任務調度中止!");
                }
            }
            catch (Exception ex)
            {
                await Console.Out.WriteLineAsync(string.Format("任務調度中止失敗!", ex));
            }
        }
        /// <summary>
        /// 建立類型Simple的觸發器
        /// </summary>
        /// <param name="m"></param>
        /// <returns></returns>
        private ITrigger CreateSimpleTrigger(ScheduleEntity m)
        {
            //做業觸發器
            if (m.RunTimes > 0)
            {
                return TriggerBuilder.Create()
               .WithIdentity(m.JobName, m.JobGroup)
               .StartAt(m.BeginTime)//開始時間
               .EndAt(m.EndTime)//結束數據
               .WithSimpleSchedule(x => x
                   .WithIntervalInSeconds(m.IntervalSecond)//執行時間間隔,單位秒
                   .WithRepeatCount(m.RunTimes))//執行次數、默認從0開始
                   .ForJob(m.JobName, m.JobGroup)//做業名稱
               .Build();
            }
            else
            {
                return TriggerBuilder.Create()
               .WithIdentity(m.JobName, m.JobGroup)
               .StartAt(m.BeginTime)//開始時間
               .EndAt(m.EndTime)//結束數據
               .WithSimpleSchedule(x => x
                   .WithIntervalInSeconds(m.IntervalSecond)//執行時間間隔,單位秒
                   .RepeatForever())//無限循環
                   .ForJob(m.JobName, m.JobGroup)//做業名稱
               .Build();
            }

        }
        /// <summary>
        /// 建立類型Cron的觸發器
        /// </summary>
        /// <param name="m"></param>
        /// <returns></returns>
        private ITrigger CreateCronTrigger(ScheduleEntity m)
        {
            // 做業觸發器
            return TriggerBuilder.Create()
                   .WithIdentity(m.JobName, m.JobGroup)
                   .StartAt(m.BeginTime)//開始時間
                   .EndAt(m.EndTime)//結束時間
                   .WithCronSchedule(m.Cron)//指定cron表達式
                   .ForJob(m.JobName, m.JobGroup)//做業名稱
                   .Build();
        }
    }
}
View Code

 總結:

開發已個小項目搞了很久才搞完,期間零零散散的開發,仍是太懶散了!

平時積累不夠,仍是太菜了!!!!!

相關文章
相關標籤/搜索