Winform開發框架之通用定時服務管理2---如何開發定時服務應用


在上篇隨筆《Winform開發框架之通用定時服務管理》介紹了我的框架體系中,通用定時服務管理模塊的設計以及一些相關功能的展示。我們在做項目的時候,或多或少需要和其他外部系統或者接口進行數據交互,有些是單向的獲取,有些是雙向的操作。這個定時操作(可能是間隔的時間,也可以能是定在某一個時刻,也可以能是讓它在某天某時刻運行),那么這就需要定時服務程序來管理了,通常我們把他寄宿在Windows服務里面(這也是一種最佳的方式),這種方式最好的地方,就是它的生命周期可以隨着電腦的啟動而啟動,而且很少需要用戶干預。

1、通用定時服務管理模塊框架設計

首先我們回顧一下上面文章對通用定時服務管理模塊的設計思路。

整個定時服務的插件設計框架如下所示。

Windows定時服務-文件視圖如下所示:

 

 

2、如何進行定時服務應用的開發

雖然看起來還是有那么幾個文件,其實由於是整個框架是基於插件式的架構,因此但我們開發定時服務應用的時候(如最底下的有黑框的部分),只需要引用插件模塊WHC.MyTimingPlugIn.dll並實現ITimingPlugIn接口即可,如上面的WarningService.dll就是一個典型的例子。

這個WarningService項目就是一個很好的測試例子,它只有一個類,類實現接口ITimingPlugIn。

    public class TestService : ITimingPlugIn
    {
        #region ITimingPlugIn 成員

        /// <summary>
        /// 操作進度狀態事件
        /// </summary>
        public event ProgressChangedEventHandler ProgressChanged;

        public bool Excute()
        {
            Thread.Sleep(1000);
            if (ProgressChanged != null)
            {
                ProgressChanged(this, new ProgressChangedEventArgs(20, "操作進行中...20%"));
            }

            //實際工作處理1

            Thread.Sleep(1000);
            if (ProgressChanged != null)
            {
                ProgressChanged(this, new ProgressChangedEventArgs(50, "操作進行中...50%"));
            }

            //實際工作處理2

            Thread.Sleep(1000);
            if (ProgressChanged != null)
            {
                ProgressChanged(this, new ProgressChangedEventArgs(80, "操作進行中...80%"));
            }

            //實際工作處理3:輸出內容,作為處理的記錄
            LogTextHelper.WriteLine(string.Format("{0}......{1}", this.Name, DateTime.Now));
            Thread.Sleep(1000);
            
            if (ProgressChanged != null)
            {
                ProgressChanged(this, new ProgressChangedEventArgs(100, "操作完成"));
            }

            return true;
        }

        /// <summary>
        /// 插件程序名稱
        /// </summary>
        public string Name
        {
            get;
            set;
        }

        /// <summary>
        /// 插件詳細配置
        /// </summary>
        public PlugInSetting Setting
        {
            get;
            set;
        }

        #endregion
    }

 

開發編譯通過后,我們需要為該定時服務應用,在插件XML配置上增加一個這樣的說明,讓服務程序能夠正常加載並識別。

<?xml version="1.0"?>
<ArrayOfPlugInSetting>
  <PlugInSetting>
    <!--插件程序名稱-->
    <Name>測試名稱</Name>
    <!--插件描述內容-->
    <Description>測試描述</Description>
    <!--運行同步服務的間隔時間(單位:分鍾)-->
    <ServiceCycleMinutes>1</ServiceCycleMinutes>
    <!--Windows服務在固定時刻(0~23時刻)運行-->
    <ServiceRunAtHour>23</ServiceRunAtHour>
    <!--Windows服務在每月指定天運行,小時按ServiceRunAtHour的值-->
    <ServiceRunAtDay>1</ServiceRunAtDay>
    <!--運行模式,0為間隔分鍾運行  1為固定時刻運行, 2為按月某天和小時  其他值為禁用-->
    <RunMode>0</RunMode>
    <!--插件的類型名稱:插件類名,程序集名稱-->
    <PlugInTypeName>WarningServcie.TestService,WarningServcie</PlugInTypeName>
  </PlugInSetting>
</ArrayOfPlugInSetting>

 

上面工作完成后,在正式部署到服務器正式環境前,我們需要檢查開發模塊是否正常執行,是否正常運行里面的邏輯。那就用到管理程序了。

運行這個程序,除了它具有安裝服務、卸載服務、啟動、停止、重新啟動、刷新服務等這些服務操作外,還提供了DOS測試的功能,讓我們在沒有部署到Windows服務前,直接進行邏輯測試,很方便的哦。

運行,我們可以看到DOS窗口的輸出內容了(我們在實現內部調用的

if (ProgressChanged != null)
{
ProgressChanged(this, new ProgressChangedEventArgs(20, "操作進行中...20%"));
}

就是為了在DOS或者其他通知界面上顯示日志信息的。

當然,為了驗證處理邏輯,剛才我們也在內部做了記錄,我們查看Log/Log.txt日志文件后,我們看到輸出的結果如下所示。我們通過日志進行記錄,這樣可以詳細看到具體的操作了。

以上就是一個簡單測試應用的實現和測試過程,其他的定時應用服務也是這樣實現插件接口就可以部署了。界面管理、服務管理、插件管理這些都是通用模塊負責的工作,不需要變動和修改。

3、基於實際WebService同步程序的開發

上面介紹的是一個簡單的例子,在我們實際開發過程中,比這個例子肯定負責很多,不過過程是一樣的,下面我們來看看我一個實際定時業務應用的具體開發吧。

和上面例子一樣,添加一個項目,應用插件模塊dll,然后實現接口的內容,不同的是這里引用了一個WebService進行操作,因為需要把Webservice里面的數據保存到本地數據庫里面(也就是所謂的同步接口)。

以上的就是一個定時服務應用的類,它實現ITimingPlugIn接口,關鍵的部分就是如何實現Execute接口了。

由於Webservice接口返回的數據字段比較多,有些可能不一定是我們需要存儲的,在實際中發現,有些如PK字段,還有一些以Specified結尾的字段,好像是自動增加上去的。這些我們可能都不需要寫到數據庫里面去,而且以后WebService可能會增加一些字段,所以得到下面的結論。

1)不能以反射WebService實體類作為基准字段存儲。

2)考慮盡可能通用的保存數據,最好字段不用每次都硬編碼到代碼,因為很多內容的。

3)可以通過獲取數據庫表的字段作為基准,如果webService實體類也存在該字段,作為寫入依據。

從上面的幾個經驗總結來看,我們需要寫一個以數據庫表為基准,編寫一個通用的服務保存機制函數,來看看如何實現的。

首先通過WebService定義,創建好需要寫入的數據庫字段(字段必須和WebService同名哦)

然后引入一個輔助類,來獲取數據庫表字段的信息列表。

    /// <summary>
    /// 通過獲取表的元數據方式獲取字段信息的輔助類
    /// </summary>
    public class TableSchemaHelper
    {
        /// <summary>
        /// 獲取指定表的元數據,包括字段名稱、類型等等
        /// </summary>
        /// <param name="tableName">數據庫表名</param>
        /// <returns></returns>
        private static DataTable GetReaderSchema(string tableName)
        {
            DataTable schemaTable = null;

            string sql = string.Format("Select * FROM {0}", tableName);
            Database db = DatabaseFactory.CreateDatabase();
            DbCommand command = db.GetSqlStringCommand(sql);
            try
            {
                using (IDataReader reader = db.ExecuteReader(command))
                {
                    schemaTable = reader.GetSchemaTable();
                }
            }
            catch (Exception ex)
            {
                LogTextHelper.Error(ex);
            }

            return schemaTable;
        }

        /// <summary>
        /// 獲取指定數據表字段
        /// </summary>
        /// <param name="tableName">數據庫表名</param>
        /// <returns></returns>
        public static List<string> GetColumns(string tableName)
        {
            DataTable schemaTable = GetReaderSchema(tableName);

            List<string> list = new List<string>();
            foreach (DataRow dr in schemaTable.Rows)
            {
                string columnName = dr["ColumnName"].ToString();
                list.Add(columnName);
            }
            return list;
        }

        /// <summary>
        /// 獲取指定數據表字段,並轉化為小寫列表
        /// </summary>
        /// <param name="tableName">數據庫表名</param>
        /// <returns></returns>
        public static List<string> GetColumnsLower(string tableName)
        {
            DataTable schemaTable = GetReaderSchema(tableName);

            List<string> list = new List<string>();
            foreach (DataRow dr in schemaTable.Rows)
            {
                string columnName = dr["ColumnName"].ToString();
                list.Add(columnName.ToLower());
            }
            return list;
        }

具體的接口實現函數如下所示。

        public bool Excute()
        {
            ShareAccService service = new ShareAccService();
            var list = service.getAccList4Deal(unitLogo, undertaker_c, 1, 10);

            int count = list.Length;
            int i = 1;
            foreach (shareEmitfileSimpleVO obj in list)
            {
                int step = Convert.ToInt32(100 * Convert.ToDouble(i) / list.Length);

                //保存列表信息
                SaveToDatabase(obj, "MAYOR_EMIT_LIST", "ACCID", obj.accid, step);
                
                //獲取並保存明細信息
                shareAccVO accVo = service.getDetailAcc4Deal(unitLogo, undertaker_c, obj.accid, obj.emitid);
                SaveToDatabase(accVo, "MAYOR_EMIT_DETAIL", "ACCID", accVo.accid, step);

                //保存明細信息里面的復函信息
                if (accVo.shareEmitfileVOs != null)
                {
                    
                    foreach (shareEmitfileVO fileVo in accVo.shareEmitfileVOs)
                    {
                        SaveToDatabase(accVo, "MAYOR_EMIT_ANSWER", "id", fileVo.id, step);
                    }
                }

                //保存附件列表信息
                List<string> attachList = new List<string>();
                if (accVo.shareAttachmentVOs != null)
                {
                    foreach (shareAttachmentVO attach in accVo.shareAttachmentVOs)
                    {
                        attachList.Add(attach.id);
                        SaveToDatabase(attach, "MAYOR_EMIT_FILE", "id", attach.id, step);
                    }
                }
                i++;

            }
            return true;
        }

我們注意到,關鍵的邏輯其實就是看SaveToDatabase如何實現通用的數據庫保存

首先,我們需要通過反射方法看看具體有哪些實體類字段屬性。

 

PropertyInfo[] proList = ReflectionUtil.GetProperties(obj);

然后看看數據庫字段列表

List<string> columnList = TableSchemaHelper.GetColumnsLower(targetTable);

獲得這兩個信息,我們就可以對他們進行比較,然后確定哪些數據寫入數據庫里面(安全的寫入數據庫)。

                PropertyInfo[] proList = ReflectionUtil.GetProperties(obj);
                Dictionary<string, object> recordField = new Dictionary<string, object>();

                try
                {
                    //為了避免接口變化以及WebService對象的一些額外字段干擾,以數據庫字段為准
                    #region 組裝字段語句
                    List<string> columnList = TableSchemaHelper.GetColumnsLower(targetTable);
                    foreach (PropertyInfo info in proList)
                    {
                        if (!recordField.ContainsKey(info.Name) && columnList.Contains(info.Name.ToLower()))
                        {
                            recordField.Add(info.Name, ReflectionUtil.GetProperty(obj, info.Name));
                        }
                    }

其中recordField就是記錄了,我們需要寫入數據庫的字段名稱和值,這樣我們就方便構建相應的操作語句和參數值了。

參數化語句構建如下所示:

                    string fields = ""; // 字段名
                    string vals = ""; // 字段值
                    foreach (string field in recordField.Keys)
                    {
                        fields += string.Format("{0},", field);
                        vals += string.Format(":{0},", field);
                    }
                    fields = fields.Trim(',');//除去前后的逗號
                    vals = vals.Trim(',');//除去前后的逗號
                    string sql = string.Format("INSERT INTO {0} ({1}) VALUES ({2})", targetTable, fields, vals); 
                    #endregion

當然,我們寫入前,為了避免重復同步,因此需要對以存在的記錄進行判斷,重復的跳過。

                    string existSql = string.Format("Select Count(*) from {0} WHERE {1}='{2}' ", targetTable, primaryKey, keyValue);
                    Database db = DatabaseFactory.CreateDatabase();
                    DbCommand command = db.GetSqlStringCommand(existSql);
                    bool hasExist = Convert.ToInt32(db.ExecuteScalar(command)) > 0;
                    if (!hasExist)
                    {

在數據庫不存在的記錄要添加到數據庫里面,如下代碼所示。

                        //不存在則要插入
                        command = db.GetSqlStringCommand(sql);

                        foreach (string field in recordField.Keys)
                        {
                            object val = recordField[field];
                            val = val ?? DBNull.Value;
                            if (val is DateTime)
                            {
                                if (Convert.ToDateTime(val) <= Convert.ToDateTime("1753-1-1"))
                                {
                                    val = DBNull.Value;
                                }
                            }

                            db.AddInParameter(command, field, TypeToDbType(val.GetType()), val);
                        }
                        
                        bool result = db.ExecuteNonQuery(command) > 0;
                        if (result)
                        {
                            string tips = string.Format("操作表【{0}】的記錄成功,{1}={2}", targetTable, primaryKey, keyValue);
                            if (ProgressChanged != null)
                            {
                                ProgressChanged(this, new ProgressChangedEventArgs(step, tips));
                            }
                        }

完整的SaveToDatabase函數如下所示(為了他人方便,也方便自己日后使用。呵呵...):

        /// <summary>
        /// 保存指定的記錄對象到數據庫
        /// </summary>
        private void SaveToDatabase(object obj, string targetTable, string primaryKey, string keyValue, int step)
        {
            if (string.IsNullOrEmpty(targetTable) || string.IsNullOrEmpty(primaryKey) ||
                string.IsNullOrEmpty(keyValue)) 
                return;

            if (ProgressChanged != null)
            {
                ProgressChanged(this, new ProgressChangedEventArgs(step, string.Format("正在處理表【{0}】{1} = {2} 的記錄。", targetTable, primaryKey, keyValue)));
            }

            if (obj != null)
            {
                PropertyInfo[] proList = ReflectionUtil.GetProperties(obj);
                Dictionary<string, object> recordField = new Dictionary<string, object>();

                try
                {
                    //為了避免接口變化以及WebService對象的一些額外字段干擾,以數據庫字段為准
                    #region 組裝字段語句
                    List<string> columnList = TableSchemaHelper.GetColumnsLower(targetTable);
                    foreach (PropertyInfo info in proList)
                    {
                        if (!recordField.ContainsKey(info.Name) && columnList.Contains(info.Name.ToLower()))
                        {
                            recordField.Add(info.Name, ReflectionUtil.GetProperty(obj, info.Name));
                        }
                    }

                    string fields = ""; // 字段名
                    string vals = ""; // 字段值
                    foreach (string field in recordField.Keys)
                    {
                        fields += string.Format("{0},", field);
                        vals += string.Format(":{0},", field);
                    }
                    fields = fields.Trim(',');//除去前后的逗號
                    vals = vals.Trim(',');//除去前后的逗號
                    string sql = string.Format("INSERT INTO {0} ({1}) VALUES ({2})", targetTable, fields, vals); 
                    #endregion
                                        
                    #region 判斷指定鍵值的記錄是否存在並寫入新的

                    string existSql = string.Format("Select Count(*) from {0} WHERE {1}='{2}' ", targetTable, primaryKey, keyValue);
                    Database db = DatabaseFactory.CreateDatabase();
                    DbCommand command = db.GetSqlStringCommand(existSql);
                    bool hasExist = Convert.ToInt32(db.ExecuteScalar(command)) > 0;
                    if (!hasExist)
                    {
                        //不存在則要插入
                        command = db.GetSqlStringCommand(sql);

                        foreach (string field in recordField.Keys)
                        {
                            object val = recordField[field];
                            val = val ?? DBNull.Value;
                            if (val is DateTime)
                            {
                                if (Convert.ToDateTime(val) <= Convert.ToDateTime("1753-1-1"))
                                {
                                    val = DBNull.Value;
                                }
                            }

                            db.AddInParameter(command, field, TypeToDbType(val.GetType()), val);
                        }
                        
                        bool result = db.ExecuteNonQuery(command) > 0;
                        if (result)
                        {
                            string tips = string.Format("操作表【{0}】的記錄成功,{1}={2}", targetTable, primaryKey, keyValue);
                            if (ProgressChanged != null)
                            {
                                ProgressChanged(this, new ProgressChangedEventArgs(step, tips));
                            }
                        }
                    } 
                    #endregion
                }
                catch (Exception ex)
                {
                    string tips = string.Format("操作表【{0}】的記錄出錯,{1}={2}", targetTable, primaryKey, keyValue);
                    if (ProgressChanged != null)
                    {
                        ProgressChanged(this, new ProgressChangedEventArgs(step, tips));
                    }
                    LogTextHelper.Error(tips);
                    string description = "";
                    foreach (string field in recordField.Keys)
                    {
                        description += string.Format("{0}={1},", field, recordField[field]);
                    }
                    LogTextHelper.Info(description);

                    LogTextHelper.Error(ex);
                }
            }
        }

開發完成后,我們在添加XML插件配置信息,如下所示

<?xml version="1.0"?>
<ArrayOfPlugInSetting>
  <PlugInSetting>
    <!--插件程序名稱-->
    <Name>測試名稱</Name>
    <!--插件描述內容-->
    <Description>測試描述</Description>
    <!--運行同步服務的間隔時間(單位:分鍾)-->
    <ServiceCycleMinutes>1</ServiceCycleMinutes>
    <!--Windows服務在固定時刻(0~23時刻)運行-->
    <ServiceRunAtHour>23</ServiceRunAtHour>
    <!--Windows服務在每月指定天運行,小時按ServiceRunAtHour的值-->
    <ServiceRunAtDay>1</ServiceRunAtDay>
    <!--運行模式,0為間隔分鍾運行  1為固定時刻運行, 2為按月某天和小時  其他值為禁用-->
    <RunMode>0</RunMode>
    <!--插件的類型名稱:插件類名,程序集名稱-->
    <PlugInTypeName>WarningServcie.TestService,WarningServcie</PlugInTypeName>
  </PlugInSetting>
  
  <PlugInSetting>
      <!--插件程序名稱-->
    <Name>熱線數據同步</Name>
    <!--插件描述內容-->
    <Description>測試描述</Description>
     <!--運行同步服務的間隔時間(單位:分鍾)-->
    <ServiceCycleMinutes>1</ServiceCycleMinutes>
    <!--Windows服務在固定時刻(0~23時刻)運行-->
    <ServiceRunAtHour>23</ServiceRunAtHour>
     <!--Windows服務在每月指定天運行,小時按ServiceRunAtHour的值-->
    <ServiceRunAtDay>1</ServiceRunAtDay>
    <!--運行模式,0為間隔分鍾運行 1為固定時刻運行, 2為按月某天和小時 其他值為禁用-->
    <RunMode>1</RunMode>
    <!--插件的類型名稱:插件類名,程序集名稱-->
    <PlugInTypeName>MayorHotlineService.DownDataService,MayorHotlineService</PlugInTypeName>
  </PlugInSetting>
</ArrayOfPlugInSetting>

這樣開發完成后,我們需要對服務進行一次測試,確認邏輯正常。

最后利用管理界面的安裝服務,把它部署上去就可以了,這樣它就以Windows服務的形式進行運行,不用再進行干預了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM