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服务的形式进行运行,不用再进行干预了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WHC.OrderWater.Commons 伍华聪 公共类源码 价值680元 ----------Database-------------- 1.DataTable帮助类(DataTableHelper.cs) 2.Access数据库文件操作辅助类(JetAccessUtil.cs) 5.查询条件组合辅助类(SearchCondition.cs) 6.查询信息实体类(SearchInfo.cs) 8.Sql命令操作函数(可用于安装程序的时候数据库脚本执行)(SqlScriptHelper.cs) ----------Device-------------- 声音播放辅助类(AudioHelper.cs) 摄像头操作辅助类,包括开启、关闭、抓图、设置等功能(Camera.cs) 提供用于操作【剪切板】的方法(ClipboardHelper.cs) 获取电脑信息(Computer.cs) 提供用户硬件唯一信息的辅助类(FingerprintHelper.cs) 读取指定盘符的硬盘序列号(HardwareInfoHelper.cs) 提供访问键盘当前状态的属性(KeyboardHelper.cs) 全局键盘钩子。这可以用来在全球范围内捕捉键盘输入。(KeyboardHook.cs) 模拟鼠标点击(MouseHelper.cs) 全局鼠标钩子。这可以用来在全球范围内捕获鼠标输入。(MouseHook.cs) MP3文件播放操作辅助类(MP3Helper.cs) 关联文件(ExtensionAttachUtil.cs) 注册文件关联的辅助类(FileAssociationsHelper.cs) 打开、保存文件对话框操作辅助类(FileDialogHelper.cs) 常用的文件操作辅助类FileUtil(FileUtil.cs) INI文件操作辅助类(INIFileUtil.cs) 独立存储操作辅助类(IsolatedStorageHelper.cs) 序列号操作辅助类(Serializer.cs) 获取一个对象,它提供用于访问经常引用的目录的属性。(SpecialDirectories.cs) 简单的Word操作对象(WordCombineUtil.cs) 这个类提供了一些实用的方法来转换XML和对象。(XmlConvertor.cs) XML操作类(XmlHelper.cs) ----------Format-------------- 参数验证的通用验证程序。(ArgumentValidation.cs) 这个类提供了实用方法的字节数组和图像之间的转换。(ByteImageConvertor.cs) byte字节数组操作辅助类(BytesTools.cs) 处理数据类型转换,数制转换、编码转换相关的类(ConvertHelper.cs) CRC校验辅助类(CRCUtils.cs) 枚举操作公共类(EnumHelper.cs) 身份证操作辅助类(IDCardHelper.cs) 检测字符编码的类(IdentifyEncoding.cs) RGB颜色操作辅助类(MyColors.cs) 日期操作类(MyDateTime.cs) 转换人民币大小金额辅助类(RMBUtil.cs) 常用的字符串常量(StringConstants.cs) 简要说明TextHelper。(StringUtil.cs) 获取中文字首字拼写,随机发生器,按指定概率随机执行操作(Util.cs) 各种输入格式验证辅助类(ValidateUtil.cs) ----------Network-------------- Cookie操作辅助类(CookieManger.cs) FTP操作辅助类(FTPHelper.cs) HTML操作类(HttpHelper.cs) 网页抓取帮助(HttpWebRequestHelper.cs) Net(NetworkUtil.cs) IE代理设置辅助类(ProxyHelper.cs) ----------Winform-------------- 跨线程的控件安全访问方式(CallCtrlWithThreadSafety.cs) CheckBoxList(CheckBoxListUtil.cs) 窗口管理类(ChildWinManagement.cs) ChildWinManagement 由马丁·米勒http://msdn.microsoft.com/en-us/library/ms996492.aspx提供一个简单的方法打印工作的一个RichTextBox一个帮手(ExRichTextBoxPrintHelper.cs) 显示,隐藏或关闭动画形式。(FormAnimator.cs) 对窗体进行冻结、解冻操作辅助类(FreezeWindowUtil.cs) 窗体全屏操作辅助类(FullScreenHelper.cs) GDI操作辅助类(GDI.cs) 提供静态方法来读取这两个文件夹和文件的系统图标。(IconReaderHelper.cs) 图片对象比较、缩放、缩略图、水印、压缩、转换、编码等操作辅助类(ImageHelper.cs) 输入法帮助,全角 转换为半角(ImeHelper.cs) Winform提示框 的摘要说明。(MessageUtil.cs) 包含互操作方法调用的应用程序中使用。(NativeMethods.cs) 托盘图标辅助类(NotifyIconHelper.cs) 打印机类(POSPrinter.cs) 图片、光标、图标、位图等资源操作辅助类(ResourceHelper.cs) RTF字符格式辅助类(RTFUtility.cs) 串口开发辅助类(SerialPortUtil.cs) 设置文本属性提供一个ToolStripStatusLabel(SafeToolStripLabel.cs) 只运行一个实例及系统自动启动辅助类(StartupHelper.cs) Web页面预览效果图片抓取辅助类(WebPageCapture.cs) 供Asp.Net直接调用的包装类(WebPreview.cs) 计算机重启、关电源、注销、关闭显示器辅助类(WindowsExitHelper.cs) 简单写了点,还有很多,希望能对大家有帮助 ================================================================================================ 本资料共包含以下附件: WHC.OrderWater.Commons.rar 公共类文档.docx
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值