2023年2月

在有些系统应用里面,我们需要对应用服务器、数据库服务器、文件服务器进行分开,文件路径等信息存储在数据库服务器里面,但文件内容则存储在文件服务器里面,通过使用FTP进行文件的上传下载,从而实现更加高效的IO输出,也分担了服务器的压力。本篇随笔介绍使用公用类库里面的FTPHelper辅助类,实现文件的上传下载等基础操作。

1、FTPHelper辅助类的接口说明

首先我们为了方便辅助类的使用,使用了构造函数进行对象的参数初始化,如下所示。

我们整个FTPHelper辅助类,是使用FTP命令和FTP服务器进行交换,因此需要如果要获取文件或目录信息,那么就需要对这些信息进行转换为合适格式的内容,这里我们定义一个结构体用来承载这些信息。

这样我们可以顺利把FTP服务器的字符串返回(DOS格式或者Unix格式字符串)它们转换为对应的结构体,方便使用了。

整个辅助函数,提供了一些基础的函数来操作FTP文件或者目录,如下所示

        /// <summary>
        ///仅获取文件列表/// </summary>
        /// <returns>获取的文件列表</returns>
        public List<FileStruct>ListFiles()/// <summary>
        ///仅获取目录列表/// </summary>
        /// <returns>获取到的目录列表</returns>
        public List<FileStruct> ListDirectories()    

以及

        /// <summary>
        ///关闭FTP服务器的所有连接/// </summary>
        public voidDisconnect()/// <summary>
        ///连接到FTP服务器/// </summary>
        /// <param name="server">FTP服务器的IP或者主机名</param>
        /// <param name="port">FTP服务器端口</param>
        /// <param name="user">登录用户名</param>
        /// <param name="pass">登录密码</param>
        public void Connect(string server, int port, string user, stringpass)/// <summary>
        ///获取FTP的当前工作目录/// </summary>
        public stringGetWorkingDirectory()/// <summary>
        ///在FTP服务器上改变目录/// </summary>
        public void ChangeDir(stringpath)/// <summary>
        ///在FTP服务器上创建新的目录/// </summary>
        public void MakeDir(stringdir)/// <summary>
        ///在FTP服务器上移除目录/// </summary>
        public void RemoveDir(stringdir)/// <summary>
        ///在FTP上移除一个文件/// </summary>
        public void RemoveFile(stringfilename)/// <summary>
        ///在FTP服务器上重命名一个文件/// </summary>
        /// <param name="oldfilename">旧文件名</param>
        /// <param name="newfilename">新文件名</param>
        public void RenameFile(string oldfilename, stringnewfilename)/// <summary>
        ///打开一个存在的文件(支持续传)上传/// </summary>
        /// <param name="filename">本地上传的文件(包含文件路径)</param>
        /// <param name="remote_filename">存储在FTP上的文件名称</param>
        /// <param name="resume">如果存在,指定是否续传</param>
        public void OpenUpload(string filename, string remote_filename, boolresume)/// <summary>
        ///打开下载一个文件/// </summary>
        /// <param name="remote_filename">FTP上的远端文件名</param>
        /// <param name="local_filename">另存为的文件名称(包含文件路径)</param>
        /// <param name="resume">如果存在,指定是否续传</param>
        public void OpenDownload(string remote_filename, string local_filename, boolresume)/// <summary>
        ///上传文件,循环操作直到文件全部上传完毕/// </summary>
        /// <returns>发送的字节数组大小</returns>
        public longDoUpload()/// <summary>
        ///下载文件,循环操作直到文件全部下载完毕/// </summary>
        /// <returns>收到的字节数组大小</returns>
        public longDoDownload()

2、FTPHelper辅助类的使用

基于上述的接口,我们编写了一个使用Demo,主要用来介绍这个辅助类的各种接口使用。

获取到的测试结果输出如下所示

虽然我们在附件管理里面采用了更为通用的《附件管理模块》,统一上传文件到服务器上进行管理,并提供下载管理等内容,连接如下:


Winform开发框架之附件管理应用
》。其他公用类库的使用说明,具体可以参考文章《
公用类库使用帮助

不过我们开发的时候,为了方便,也可以使用 FTPHelper辅助类进行简单的文件上传下载操作。

本篇随笔介绍在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(objectsender, 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 classHelloJob : IJob
{
public voidExecute(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 classWebApiApplication : System.Web.HttpApplication
{
IScheduler scheduler
= null;protected voidApplication_Start()
{
GlobalConfiguration.Configuration.EnableCors();
GlobalConfiguration.Configure(WebApiConfig.Register);
//创建执行同步的处理 ISchedulerFactory sf = newStdSchedulerFactory();
scheduler
=sf.GetScheduler();

CalendarTask();
CreateOnceJob();
//启动所有的任务 scheduler.Start();
}
protected void Application_End(objectsender, EventArgs e)
{
if(scheduler != null)
{
scheduler.Shutdown(
true);
}
}
/// <summary> ///创建同步任务/// </summary> private voidCalendarTask()
{
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 voidCreateOnceJob()
{
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 classStationSyncJob : IJob
{
public voidExecute(IJobExecutionContext context)
{
LogTextHelper.Info(
string.Format("您在 {0} 时候调用【同步站场信息】一次", DateTime.Now));

StationDetailResult result
= newStationDetailResult();try{
QueryStationJson json
= new QueryStationJson();//空查询,一次性查询所有 BaseDataAgent agent= newBaseDataAgent();
result
=agent.QueryStationDetail(json);if(result != null &&result.success)
{
foreach(StationDetailJson detail inresult.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 infeed.Items)
{
var id =item.Id;string subject =item.Title.Text;string summary =item.Summary.Text;
}

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

通过上面的RSS读取操作,我们可以获得对应的博客内容,如果我们需要每周给客户发送一些内容,那么这些就可以通过上面RSS源进行处理发送了。

关于发送文本消息的处理,可以参考我的随笔文章《
C#开发微信门户及应用(3)--文本消息和图文消息的应答

这里我就直接应用上面的接口对内容进行处理发送,具体接口的逻辑就不再罗列。

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

ICustomerApi api
= newCustomerApi();foreach (SyndicationItem item infeed.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 voidSendBlogsNews()
{
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 infeed.Items)
{
list.Add(
newArticleEntity
{
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
= newCustomerApi();var result =customerApi.SendNews(token, openId, list);
}

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

整体的界面效果就是我们需要的效果了,不过如果我们需要使用批量发送给订阅用户的话,那么我们需要使用消息的群发接口,群发的消息接口封装如需了解,可以参考文章《
C#开发微信门户及应用(30)--消息的群发处理和预览功能
》。

整个群发消息的逻辑代码如下所示,主要逻辑就是获取博客文章,并上传文章的图片,接着上传需要群发的图文消息资源,最后调用群发接口进行消息的发送即可。

        private voidBatchSendBlogNews()
{
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 = newMediaApi();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 infeed.Items)
{
list.Add(
newNewsUploadJson
{
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
= newMassSendApi();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 classGlobal : HttpApplication
{
private IScheduler scheduler = null;void Application_Start(objectsender, EventArgs e)
{
//在应用程序启动时运行的代码 AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);

BundleConfig.RegisterBundles(BundleTable.Bundles);
//构造调度对象,并创建对应的调度任务 scheduler = StdSchedulerFactory.GetDefaultScheduler();
CalendarTask();
//启动所有的任务 scheduler.Start();
}
protected void Application_End(objectsender, EventArgs e)
{
if (scheduler != null)
{
scheduler.Shutdown(
true);
}
}
private voidCalendarTask()
{
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做成更多的数据同步任务调度,另外在微信应用中,我们也可以整合很多组件或者控件,来实现更加弹性化的业务支持,如消息群发、访客汇总,内容同步等处理。

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

在我们接触的很多项目中,如果有一些参考性的项目框架,那么做起开发来,事半功倍,一般来说搭建或者积累这些框架性的项目,非一日之功。一般我们可以把具体的项目分为Winfrom、Web、微信、或者Socket等方面,具体项目界面表现不同,但是界面层底下的应该都是差不多的结构,能够支持多种数据库的接入,提供更好的接口封装,以便重用,实现最优化代码的处理。

软件系统随着业务的发展,变得越来越复杂,不同领域的业务所涉及到的知识、内容、问题非常非常多。如果每次都从头开发,那都是一个很漫长的事情,且并不一定能将它做好。基于稳定成熟的框架,开发项目,技术和时间都有保障,同时可以利用前人高效的工具进行辅助开发,从而事半功倍。

1、框架的主要特点和要求

从一些资料来解读下开发框架的特点,如下所示:代码模板化、重用、高内聚(封装)、规范、可扩展、可维护、协作开发、通用性。

1、代码模板化

框架一般都有统一的代码风格,同一分层的不同类代码,都是大同小异的模板化结构,方便使用模板工具统一生成,减少大量重复代码的编写。在学习时通常只要理解某一层有代表性的一个类,就等于了解了同一层的其他大部分类结构和功能,容易上手。团队中不同的人员采用类同的调用风格进行编码,很大程度提高了代码的可读性,方便维护与管理。

2、重用

开发框架一般层次清晰,不同开发人员开发时都会根据具体功能放到相同的位置,加上配合相应的开发文档,代码重用会非常高,想要调用什么功能直接进对应的位置去查找相关函数,而不是每个开发人员各自编写一套相同的方法。

3、高内聚(封装)

框架中的功能会实现高内聚,开发人员将各种需要的功能封装在不同的层中,给大家调用,而大家在调用时不需要清楚这些方法里面是如果实现的,只需要关注输出的结果是否是自己想要的就可以了。

4、规范

框架开发时,必须根据严格执行代码开发规范要求,做好命名、注释、架构分层、编码、文档编写等规范要求。因为你开发出来的框架并不一定只有你自己在用,要让别人更加容易理解与掌握,这些内容是非常重要的。

5、可扩展

开发框架时必须要考虑可扩展性,当业务逻辑更加复杂、数量记录量爆增、并发量增大时,能否通过一些小的调整就能适应?还是需要将整个框架推倒重新开发?当然对于中小型项目框架,也不必考虑太多这些内容,当个人能力和经验足够时水到渠成,自然就会注意到很多开发细节。

7、可维护

成熟的框架,对于二次开发或现有功能的维护来说,操作上应该都是非常方便的。比如项目要添加、修改或删除一个字段或相关功能,只需要简单的操作,十来分钟或不用花太多的工夫就可以搞定。新增一个数据表和对应的功能,也可以快速的完成。功能的变动修改,不会对系统产生不利的影响。代码不存在硬编码等等,保证软件开发的生产效率和质量。

8、协作开发

有了开发框架,我们才能组织大大小小的团队更好的进行协作开发,成熟的框架将大大减轻项目开发的难度,加快开发速度,降低开发费用,减轻维护难度。

9、通用性

同一行业或领域的框架,功能都是大同小异的,不用做太大的改动就可以应用到类似的项目中。在框架中,我们一般都会实现一些同质化的基础功能,比如权限管理、角色管理、菜单管理、日志管理、异常处理......或该行业中所要使用到的通用功能,使框架能应用到某一行业或领域中,而不是只针对某公司某业务而设定(当然也肯定存在那些特定功能的应用框架,这只是非常少的特殊情况,不在我们的考虑范围)。

2、基于Winform混合式开发框架和Bootstrap开发框架的基础分析

在很多场合,一般如果是做Winform桌面的应用,我会推荐客户使用《Winform混合式开发框架》,如果是基于Web的项目开发,一般推荐客户采用《Bootstrap开发框架》。

1) 《混合式开发框架》混合了传统《Winform开发框架》、《WCF开发框架》和Web API接口框架的特点,可以在直接访问数据库、利用WCF服务获取数据、利用Web API服务获取数据三者之间自由切换,统一了系统界面层对业务服务的调用模式,所有组件模块均实现三种方式的调用,是一种弹性化非常好的框架应用,既可用于单机版软件或者基于局域网内的应用软件,也可以用于分布式技术的互联网环境应用,是一种成熟稳定、安全高效的技术框架。

由于混合型框架,既可以用于传统Winform系统开发,也可以用于WCF分布式系统开发,还可以用于轻型高效的Web API的分布式系统开发(目前广泛应用的接口方式),因此环境适应性强;而且由于模块具有这些特点,可重用性更高,特别对于通用性的模块,更是具有无可替代的优越性。

2) 基于Metronic的Bootstrap开发框架

该开发框架界面部分采用较新的Bootstrap技术,采用当前最新的Bootstrap3.x,集成了众多功能强大的Bootstrap插件。

Bootstrap
是一个前端的技术框架,很多平台都可以采用,JAVA/PHP/.NET都可以用来做前端界面,整合JQuery可以实现非常丰富的界面效果,目前也有很多Bootstrap的插件能够提供给大家使用,本框架集合了众多最为优秀的插件,能给我们Web的用户体验提升到一个前所未有的水平。

Metronic
是一个国外的基于HTML、JS等技术的Bootstrap开发框架整合,整合了很多Bootstrap的前端技术和插件的使用,是一个非常不错的技术框架。本框架以这个为基础,结合我对MVC的Web框架的研究,整合了基于MVC的Bootstrap开发框架,使之能够符合实际项目的结构需要。

框架后台采用基于C#的MVC技术,是目前.NET开发最为成熟流行的技术,框架后台数据库支持Oracle、SqlServer、MySql、Sqlite、Access等常规数据库,可通过配置进行自由切换,使用Enterprise Library模块进行数据访问的控制,使得数据访问更方便轻松。

3、框架的各个特点的说明

上面介绍了开发框架的一些共性,概括总结几点:代码模板化、重用、高内聚(封装)、规范、可扩展、可维护、协作开发、通用性。

1)代码模板化

我们的框架系列,无论是Winform开发框架,Web开发框架、Bootstrap开发框架等框架系列,都是一脉相承的特点,所有类代码都统一的代码风格,各个项目以及分层目录都是按照统一的命名方式进行构建,易读易理解,整个框架的代码,包括项目底层BLL、DAL、Entity等层的代码,以及Winform界面层、或者Web界面层,都可以利用工具统一生成(我们根据项目界面特点构建对应的模板文件),减少大量重复代码的编写。

这些结构利于使用代码生成工具(Database2Sharp,我们为框架快速开发量身定做的软件工具)进行项目代码的生成

2) 重用

由于我们在项目开发中,采用了一些模块化的方式来组装项目,可以简单的理解为积木组合的方式。如权限管理模块、字典管理模块、自动更新模块、人员管理模块、通讯录管理模块、分页控件、公用类库等方面,这些可以在项目中根据情况采用对应的模块,非常高效,整个主体框架也有对应接入这些模块的案例代码供参考,因此可以更加直观明了。

而另一方面,我们在对类的重用方面,也是精益求精,尽量做到精简重复代码,提高开发效率的目的。

界面类提供封装继承,减少界面的一些繁琐设置

基础业务类,我们同样也是采用重用基类方式,实现接口函数的封装处理。

以及业务类继承关系,当然还有其他更多的,基本上都提供一些基类来处理,简化代码。

3) 高内聚(封装)

从上面小节可以看出,整个框架宏观方面分为多个功能模块,可以自由搭配使用;微观方面都有一个继承关系的基类来对相关的代码进行封装处理,这样开发人员将各种需要的功能封装在不同的模块或者基类里面,从而实现高内聚的处理。

Web API的控制器设计对象继承关系,如下所示:

4) 规范

我们的整个框架系列,严格执行代码开发规范要求,做好命名、注释、架构分层、编码、文档编写等规范要求,这样我们只需要了解一个项目的结构,其他项目依次类推,就可以明确各个分层的意义,一叶知秋,管中窥豹就是这个道理,我们可以通过这些知识积累,指导我们其他同事进行项目开发,大家在规范上统一一致就可以步调统一,从而在单位内部拥有更高的开发效率。

其他框架的特点,如可扩展、可维护、协作开发、通用性,也就不再赘述,基本上,整个框架就是为了方便构建项目而搭建,大的方面可以使用开发好的模块,小的方面可以继承封装好的类库,整个调用的逻辑过程也是经过实践的考验,在众多国内外公司的项目中使用过,同时也获得非常多的反馈,共同促进整个框架体系的发展。

在前面介绍很多的微信框架,基本上都采用EasyUI的界面来搭建的微信框架,如随笔《
C#开发微信门户及应用(8)-微信门户应用管理系统功能介绍
》介绍的一样,不过随着微信的H5应用越来越多,因此转换为更适合做H5页面应用框架的Bootstrap开发框架,本文介绍的整个微信模块都是基于Bootstrap的应用。

微信开发包括公众号、企业号、微信小程序等方面的开发内容,需要对腾信的微信API接口进行封装:包括事件、菜单、订阅用户、多媒体文件、图文消息、消息群发、微信支付和企业红包、摇一摇设备、语义理解、微信小店、微信卡劵等相关接口处理,另外还包括扫一扫、图库和照片管理、地理位置、H5页面开发等内容,以及企业号通讯录接口、成员消息相互发送等方面接口。

1、系统的重要特性总结

微信门户应用管理系统,采用基于
MVC + Bootstrap + Ajax + Enterprise Library
的技术路线,界面层采用Boostrap + Metronic组合的前端框架,数据访问层支持Oracle、SQLServer、MySQL、PostgreSQL等数据库。在微信门户系统里面,实现下面这些功能操作:

1)实现菜单的动态配置及更新到服务器上;

2)动态定义事件和响应消息,实现对不同行业,不同需求的菜单动作响应;

3)动态的应答指令配置处理,实现整套应答链的消息处理;

4)获取订阅用户和用户分组信息,并可以实现用户分组信息的维护等操作;

5)管理并更新多媒体文件、图文消息等内容,方便为客户推送消息做准备。

6)使用向选定订阅用户或者分组进行消息的群发功能。

7)提供对用户语音的识别,并将识别结果和事件处理进行对接,实现语音内容的处理。

8)提供对地理位置的记录和扩展应用,实现对百度天气、热映影片、影院、旅游、交通消息等方面的第三方应用的对接。

9)记录用户输入的会话记录,并可以根据会话记录向客户进行消息的发送。

10)集成微信扫一扫的功能,实现条码和二维码扫码的功能整合,可以在后台整合自身应处理;。

11)集成微信其他最新功能,包括拍照和图库选择、快速发送地理位置等功能;

12)集成获取关注成员的唯一ID,更新用户备注,获取客服聊天记录等功能。

13)集成微信支付和企业红包、摇一摇设备、语义理解、微信小店、微信卡劵等相关接口处理。

14)实现企业通讯录的管理,包括部门管理、成员管理、标签管理,使得我们可以更方便同步企业号信息;

15)实现企业号无限制的消息发送,可以使企业和关注成员之间沟通更顺畅;

16)集成企业号文本消息、图片消息、文件消息、语音消息、视频消息、图文消息等内容的发送整合,统一处理接口;

17)实现企业号菜单的动态配置及更新到服务器上,动态定义菜单事件和响应消息,实现不同的菜单动作响应;

18)管理并更新企业号的多媒体文件,方便同步到自己文件服务器进行存储和处理;

19)使用重定向功能获订阅用户信息,实现企业成员身份的认证和处理。

20)平台和微信开放平台整合扫描二维码登录,通过扫码绑定并直接登陆系统。

……

2、系统功能介绍

2.1)系统登录

在系统登录界面输入账号和密码后(默认账号admin,密码为空),即可以超级管理员身份进入管理系统的主界面。

系统登录后,通过水平菜单进行后台功能管理。

2.2)微信账号管理

系统支持多微信账号的接入管理和使用,同时支持订阅号、公众号、企业号、小程序的账号配置。

可以在系统右上角进行账号的切换管理。

2.3)菜单管理

在系统中管理菜单,并通过把菜单提交到服务器上,实现菜单的动态配置和生成,能够为我们系统适应各种的需要,实现灵活的处理。

微信菜单的添加界面如下所示。

微信菜单的修改界面如下所示

微信菜单定义是存储在数据库里面,如果需要提交到微信服务器上并生效,则需要调用微信API接口进行处理,我在页面的Controller控制器里增加一个提交到服务器的处理方法。

在微信服务账号的门户上,菜单的表现效果如下所示。

我们知道,微信的服务器架起了客户手机和开发者服务器的一个桥梁。当我们在微信服务器上的公众账号创建了相应的菜单,通过消息的传递和响应,就能实现了与用户的交互操作,下面是它的消息流程图。

2.4)菜单事件的处理

对于动态生成的菜单,大多数情况下是用作Click的方式,也就是需要定义每个菜单的事件响应操作,我们使用微信的话,可以了解到,微信的处理事件,一般可以响应用户文本消息、图片消息、图文消息等内容,常规下,一般使用文本消息或者图文消息居多。

为了进一步实现响应内容的重用,我们把菜单的事件定义和内容定义进行分开管理,事件定义可以使用多个文本消息,也可以使用多个图文消息进行组合,这样可以实现更加灵活的使用环境。

添加事件定义如下所示

事件的响应内容编码,可以选择输入或者从“编辑”按钮中选择,当选择“编辑”按钮进行选择的时候,系统弹出一个对话框供用户对事件的响应内容编码选择。

完成选择后,回到原来的新增界面,将会看到返回的记录就是我们选择的记录。

微信事件的编辑界面如下所示,类似新增界面的内容。

2.5)微信消息内容管理

上面说到,菜单的事件通过关联事件编码进行处理,而事件本身可以组合多个消息内容,因此消息内容是响应客户操作的最小单元,它们可以是一条文本消息、图文消息,也可以是多条消息的组合(同类型的话)。

为了方便管理,我把消息分为了图文、指令、文本类型,如果需要,还可以根据需要把它细化为其他类型的消息。

消息内容的添加界面如下所示。

文本消息的手机上界面效果如下所示。

这里不管是文本消息还是图文消息,我们统一以图文消息的定义来定义消息,如果是文本消息,我们只需要获取描述内容作为消息的主体即可。

图文消息的编辑界面如下所示,主要就是填写完整的内容和图片,以及页面详细的链接即可。

上面的这个客户关系管理系统的消息,在手机上显示的界面效果如下所示,单击链接,可以切换到消息跳转链接地址的。

2.6)应答指令的维护

应答指令的维护,有点类似于事件的管理,主要就是定义一些用到的指令,方便构建应答系统的响应链,从而实现一步步的操作指令。

在后台设置好应答指令后,系统就能根据应答指令链进行处理了。首先我们需要提供一个进入应答链的提示界面,如下所示。

但我们在菜单选择应答系统后,系统返回一个文本提示界面,如下所示。

这个界面里面提示了一些按键,包括几个固定的按键和一些业务按键,输入简单的1~6可以对选择进行响应。

我们看到上面的界面,输入指令1后,系统进入下一层的应答指令,然后又列出几个可供输入的按键和内容提示。

当我们继续输入业务按键1后,响应的是一个图文消息,也是关于按键的详细说明。

这个时候,我们也还可以输入*号按键,返回上一级菜单的。

2.7)客服管理功能

输入0则转入了客服对话模式,后续您发的任何消息,将会转发到多客服系统里面了。

当用户发送消息后,客服助手就能及时收到消息并处理和客户的应答了。

通过使用多客服的客户端,这样处理消息交互起来非常方便,能获得客户的对话信息了,在电脑客户端上,看到的界面如下所示。

手机上的谈话截图如下所示。

这样就能够通过多途径,及时响应客户的信息了。

2.8)订阅用户管理

为了更有效管理订阅用户以及分组信息,我们可以从微信服务器上获取相关的信息,供我们了解关注的用户信息,也可以为后续的群发消息做准备。

订阅用户的管理如下所示,默认可以通过用户的地区进行查看,地区根据:国家-省份-城市这样的级别进行展开。

也可以根据标签查看,标签可以进行维护,以及为标签加入或者移除人员,界面如下图所示。

以及可以根据分组查看,如下所示。

订阅用户可以从微信服务器上进行同步到本地,单击同步数据,可以把服务器上的用户数据下载到本地进行更新或者写入。

双击可以查看订阅用户信息,查看订阅用户的详细信息界面如下所示。

2.9)用户标签管理

微信公众号,仿照企业号的思路,增加了标签管理的功能,对关注的粉丝可以设置标签管理,实现更加方便的分组管理功能。开发者可以使用用户标签管理的相关接口,实现对公众号的标签进行创建、查询、修改、删除等操作,也可以对用户进行打标签、取消标签等操作。

标签管理是替代用户组管理的一种标签,可以很好管理订阅的用户,我们可以通过封装好的微信接口很容易的获取腾讯服务器上用户标签列表,然后进行管理,如下图所示。

同时前面提到了,可以在订阅用户按标签进行维护的,如下所示。

编辑标签信息界面如下所示。

2.10)多媒体管理

多媒体管理是指把本地文件上传到微信服务器上进行保存,方便信息的发送等操作。微信要求,某些信息,必须是先上传到服务器上,然后才能使用它的媒体ID进行发送的。

文件成功上传到服务器后,在列表里面的“文件上传标识,就是一串BASE64的编码数据,同时有一个上传的时间戳(因为微信服务器只保留了3天的媒体数据,超过期限的数据会被自动删除。

同时,在列表的上面,有两个重要的功能:上传选定的记录,重新上传过期的记录。方便我们对自己多媒体文件的重新更新操作。

添加界面操作如下所示,其中引入了附件上传的控件进行文件的操作,非常方便。同时上传成功的文件,会在列表中列出。

多媒体文件可以是下面几种方式:图片、语音、视频、缩略图。

保存后的数据记录,文件上传标识和时间戳都是空的,我们如果要使用,必须把他们上传到微信的服务器上,然后根据它的MediaId进行信息的发送,上传选定的记录操作界面如下所示。

多媒体文件顺利上传后,记录的信息如下所示。

2.11)图文消息处理

图文消息分为单图文消息和多图文消息两种,单图文消息如下所示。

多图文消息如下所示:

和多媒体数据管理一样,图文消息也是通过同样的方式进行管理,先上传到服务器,然后在进行消息的发送操作,多媒体消息一样有时间方面的限制要求,具体在我们的微信门户平台里面管理界面如下所示。

添加图文消息界面如下所示,保存后,可以在编辑界面中的“其他图文列表”里面,继续添加多图文的消息内容。

在添加界面中,选择图文消息的缩略图,都是通过选定指定的,已经上传到服务器上图片或者缩略图资源才可以的。

添加后的多图文列表,可以进行查看管理。

保存记录后,然后继续上传,上传后的记录界面如下所示,成功后返回一个上传后的服务器标识和时间戳,否则提示错误。

2.12)会话消息管理

为了方便记录客户的输入和发送信息,我们在微信门户管理平台里面记录用户的输入数据,具体会话消息管理界面如下所示。

我们可以双击最近48小时内的任何一条记录,可以给关注的客户进行消息的发送操作,如果消息发送成功,用户在手机的微信账号里面就能收到相关的发送消息了。

2.13)群发消息管理

为了对客户进行相应的营销操作,有时候我们需要对指定的群主或者人员进行消息的群发,让客户经常性的了解我们产品的信息和活动。

由于群发消息,除了文本消息,可以直接编辑发送外,其他数据,必须要求是上传到服务器的多媒体文件或者图文消息内容,因此前面的多媒体管理和图文消息管理,就是主要为了群发消息的目的引入的。有了上面的多媒体和多图文信息,我们从平台里面选择记录即可进行发送,从而省却麻烦的连带工作,实现高效的信息群发操作。

群发的消息,可以按群发分组进行查看,也可以按照消息类型进行查看,使得我们管理起来根据方便。

添加图文消息,可以选择文本消息、图文消息、图片消息等内容,根据不同的内容,界面提供不同的选择操作。

消息的群发类型分为两种,一种是根据分组,那么从平台里面选择对应的分组即可;一种是根据用户的OpenID进行发送,提供给用户输入。主要的操作界面如下所示。

2.14)使用语音处理

我们知道,微信最开始就是做语音聊天而使得其更加流行的,因此语音的识别处理自然也就成为微信交流的一个重要途径,微信的开发接口,也提供了对语音的消息请求处理。这里主要介绍如何利用语音的识别,对C#开发的微信门户应用的整个事件链的处理操作,使得在我们的微信账号里面,更加方便和多元化对用户的输入进行处理。

在系统后台里面,会检查是否获得了微信的语音识别结果,如果获得,那么这个时候,就是和处理用户文本输入的操作差不多了,语音输入的处理逻辑如下所示。

首先我根据识别结果,寻找是否用户读出了微信门户的菜单名称,如果根据语音结果找到对应的菜单记录,那么我们执行菜单事件(如果是URL的View类型菜单,我们没办法重定向到指定的链接,因此给出一个链接文本提示,给用户单击进入;如果没有找到菜单记录,那么我们就把语音识别结果作为一般的事件进行处理,如果事件逻辑没有处理,那么我们最后给出一个默认的语音应答提示结果就可以了。

微信门户测试界面效果如下所示。


为了方便对客户会话的记录,我的微信门户后台,会记录用户的语音输入内容,如下所示。

2.15)使用地理位置扩展相关应用

我们知道,地理位置信息可以用来做很多相关的应用,除了我们可以知道用户所在的位置,还可以关联出一些地理位置的应用,如天气,热映影片,附近景点,附近影院,交通事件等等,反正所有和地理位置相关的信息,我们都可以根据需要做一些扩展应用。这里主要介绍利用地理位置信息,如何构建使用这些应用的操作。

1
)微信的地理位置信息
在使用前,我们先来看看微信的接口,为我们定义了那些关于与地理位置的信息。其实地理位置的信息,微信分为了两个方面,一个是接收用户的地理位置请求,一个是用户允许上报地理位置操作,定时发送的地理位置信息。

地理位置的上报操作,就是在输入的地方,选择+号进行添加地理位置,然后选择当前或者指定的地理位置地图,具体操作如下所示。


首先对用户地理位置的请求,我根据数据库配置给出了一个用户选择的指令提示,如下所示。

为了对地理位置请求的处理,我定义了一个用于处理这个操作的指令操作

这样整个地理位置的指令操作,就在应答链里面进行很好的跳转管理了。几个应用扩展的界面效果如下所示。



2.16)微信扫码登录

在现今很多网站里面,都使用了微信开放平台的扫码登录认证处理,这样做相当于把身份认证交给较为权威的第三方进行认证,在应用网站里面可以不需要存储用户的密码了。

在用户列表的二维码连接上,单击可以对用户进行微信扫码绑定,这样用户可以在扫码登录处直接扫码登录,不需要输入账号密码。

一旦成功绑定用户微信,在微信端会定位到一个绑定成功的页面(自定义的H5页面),如下所示。

如果用户已经进行了二维码绑定,则可以在管理界面进行取消绑定,这样可以解绑用户账号和微信之间的关联。

2.17)微信摇一摇红包功能

摇一摇周边红包接口是为线下商户提供的发红包功能。用户可以在商家门店等线下场所通过摇一摇周边领取商家发放的红包,在线上转发分享无效。

开发者可通过接口开发摇一摇红包功能,特点包括:

  1. 可选择使用模板加载页或自定义Html5页面调起微信原生红包页面(详见创建红包活动中use_template字段,1为使用模板,2为使用自定义Html5页面)
  2. 原生红包页面拆红包,无需通过公众号消息下发
  3. 提供关注公众号能力,用户可自行选择是否关注(裂变红包分享时无效)
  4. 完成页面可配置跳转链接,可跳转商户的其他自定义Html5页面
  5. 同一个用户在单个红包活动中只能领取1次红包

用户侧交互流程

常规的摇一摇红包的流程如下所示,这里没有使用用户自定义的模板,也就是使用系统内置的(努力加载中。。。)的页面,红包需要自己拆开。

红包组件接口调用流程

  1. 申请红包接口权限:登录摇一摇周边商户后台
    https://zb.weixin.qq.com
    ,进入开发者支持,申请开通摇一摇红包组件接口;
  2. 红包预下单:调用微信支付的api进行红包预下单,告知需要发放的红包金额,人数,生成红包ticket;
  3. 创建活动并录入红包信息:调用摇周边平台的api录入创建红包活动并录入信息,传入预下单时生成的红包ticket;
  4. 调用jsapi抽红包:在摇出的页面中通过调用jsapi抽红包,抽中红包的用户可以拆红包;
  5. 调用以上接口时,红包提供商户和红包发放商户公众号要求一致。

摇一摇红包的处理过程可以先的流程说明,申请权限后,需要在摇一摇后台配置相关的红包处理页面,然后通过红包接口处理提交红包数据,最后通过摇一摇的设备摇出界面,使用JSAPI实现抽取红包的操作,具体过程如下所示。

其中红包接口处理,是调用一系列的红包接口实现的,包括红包预下单、创建红包活动、录入红包信息等操作,如下所示。

其中微信支付的相关配置信息在账号的信息里面

利用摇一摇以及微信红包接口,使用手机摇一摇获得红包的过程界面效果如下所示。

对封装好的微信接口,接口成功调用后,我们可以在公众号的对话里面看到红包的信息结果,如下是整个红包发送及拆开的过程。

另外我们也对裂变红包进行了接口的封装,如果我们需要发送裂变红包的时候,直接调用裂变红包的接口即可实现红包发送的操作。

2.18)微信H5页面及JSDK开发

在我们开发微信页面的时候,需要大量用到了各种呈现的效果,一般可以使用
Boostrap的效果来设计不同的页面,不过微信团队也提供很多这方面的资源,包括
JSSDK的接口,以及
Weui的页面样式和相关功能页面,给我们提供了很大的便利。

1) JSSDK

微信JS-SDK是
微信公众平台
面向网页开发者提供的基于微信内的网页开发工具包。通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。

目前JSSDK支持的接口分类包括下面几类:基础接口、分享接口、图像接口、音频接口、智能接口、设备信息、地理位置、摇一摇周边、界面操作、微信扫一扫、微信小店、微信卡券、微信支付,随着微信功能的全部整合,估计更多的接口会陆续开放出来。

2)WeUI和Jquery WeUI

WeUI
是一套同微信原生视觉体验一致的基础样式库,由微信官方设计团队为微信内网页开发量身设计,可以令用户的使用感知更加统一。在微信网页开发中使用 WeUI,有如下优势:

  • 同微信客户端一致的视觉效果,令所有微信用户都能更容易地使用你的网站
  • 便捷获取快速使用,降低开发和设计成本
  • 微信设计团队精心打造,清晰明确,简洁大方

该样式库目前包含 button、cell、dialog、progress、toast、article、icon 等各式元素,已经在
GitHub
上开源。访问
http://weui.github.io/weui/
或微信扫码即可预览。

jQuery WeUI
中使用的是官方 WeUI 的 CSS 代码,并提供了 jQuery/Zepto 版本的 API 实现。JQuery WeUI相对于在官方WeUI的基础上做了一些功能扩展,已丰富界面设计和相关功能,因此我们可以考虑直接基于JQuery  WeUI的基础上进行页面开发即可。

在我前面的一些案例中,都利用了We UI样式来进行很多微信H5页面的功能设计,包括微信支付页面、签到页面等等。

如微信支付页面如下所示:



以及签到页面效果如下所示。


当然我们可以根据业务需要,增加很多这样和微信色调样式一致的页面,这个就是利用WeUI样式带来的界面体验一致性的好处。

本篇主要介绍微信H5页面开发的经验总结,上面提到了利用JSSDK和WeUI来对微信应用的H5页面进行开发,因此下面的相关效果也就是利用这些技术进行处理的。

字典数据的绑定:

和常规网页功能一样,我们在设计微信页面应用的时候,很多数据也是来源字典数据的,而且需要把它们动态绑定在页面上,微信页面的
JQuery WeUI提供了一些列表字典数据的展示效果如下所示。我们在微信框架里面也使用了这些页面组件,实现字典的绑定处理,如下所示。

同时,我们则可以在微信后台对数据字典进行维护即可进行实时的数据更新。

3、设备维修案例功能介绍

本节介绍的是设备维修案例的微信应用场景,该需求主要围绕固定资产(如医疗设备)的微信应用展开,包括录入及查询资产信息、资产盘点、设备的维修保养、日常巡检、维修、计量检测等事务。

整个案例微信端应用采用的是H5页面以及微信的JSDK进行相关的接口开发,符合微信的界面风格。后端管理就是本后台管理系统。

3.1 组织机构及用户分类(角色)

在系统中,根据系统应用,建立了相关的组织机构,以及对应的用户分类(即用户角色),根据不同的角色可以授权不同的功能管理界面。

组织机构如下所示。

用户角色如下所示:

系统根据角色对菜单进行不同的显示设置。


超级管理员
(账号admin,密码为空)登陆系统,可以对整体微信功能进行设置管理。


普通管理员
(账号gl,密码为12345678)登录系统,可以对相关的设备信息进行维护管理,如下所示。

3.2 微信菜单

在微信端,我们需要根据我们的应用,设置一些功能菜单,我们可以通过后台进行菜单的设置,然后通过接口提交到微信服务器即可。

这样我们提交菜单后,在公众号上就可以对相关的功能进行操作了。

3.3
设备添加及查看

在我们应用系统中,一般都为每个设备指定一个设备编码,我们通过生成二维码后,可以通过微信扫码进行设备信息的添加或者完善,以及相关的操作。

设备二维码采用连接+设备编码的方式生成,如下所示:

设备二维码的生成测试页面地址是:/h5/Test,测试生成二维码的界面效果如下所示。

有了这些带有链接地址的二维码,当用户使用微信扫码操作的时候,会指定调整到对应的连接,实现相应的设备添加、维修保养、日常巡检、维修、计量检测等事务。

在微信端使用对应的功能菜单扫码(如使用【设备管理】=>【设备信息录入】扫码上面的设备二维码),那么就可以进行设备信息的添加操作了。

如果是设备信息查看,那么在微信端列出主要的设备信息;

也可以单击进入查看详细设备明细信息。

3.4 设备其他管理

设备盘点和设备计量如下所示:

预防性维护和设备计量界面如下所示。

3.5
设备管理

设备相关信息可以在微信后台进行详细查看,如下界面是所有设备的信息列表,可以进行相关的查询、修改、删除、查看明细等操作。

设备详细信息查看,在后台管理界面效果如下所示。

以及设备相关的维修信息:

4、微信接口封装介绍

4.1)微信API封装管理

以上所有在后台管理的功能,都是通过对微信API的封装管理实现的,我们在开发顶层的应用前,已经把所有的微信提供的接口进行了有效的封装。微信的相关接口的管理。

公众号接口的分类图如下所示。

企业号接口的分类图如下所示:

在各种程序模块里面,我们所有处理的数据,基本上都是通过自定义实体类的方式进行传递(
注意:实体类是我根据程序开发需要自己定义的,非微信本身的实体类
),这样非常方便我们处理操作,否则每次需要解析不同的消息内容,很容易出现问题,这样强类型的数据类型,提高了我们开发微信应用的强壮型和高效性。这些实体类的对象有一定的继承关系的,他们的继承关系如下所示。

微信的回复消息处理,它也是继承自BaseMessage实体类的(
同样,下图的实体类及其继承关系也是自定义的,方便程序开发
),它的关系如下所示

4.2)微信框架项目模块

我们在开发微信相关的应用的时候,一般需要完善的基础模块支持,包括微信公众号,微信企业号,微信小程序等,以及一些业务模块的支持,一般随着功能的增多,我们需要非常清晰的界定他们的关系。模块的分拆以及合并往往需要考虑的代码的重用,而且尽量做到简单而不重复。

1)公众号模块封装

微信开发,我们首先需要利用我们的语言(这里是利用
C#语言),为所有用到的
API接口实现进一步的封装,方便使用,微信
API模块包含的内容很多,大概可以分为下面的项目。

有了这些接口功能的封装类,只是万里长征的第一步,我们还需要围绕这些接口,以及我们的业务模块实现更多交互功能的。

我们在WHC.Weixin.Data模块里面,定义了包含公众号的消息分派处理接口,这个分派接口是对接收来自微信服务器的各种消息事件进行响应;另外该模块还包含一些常规的数据存储,如关注用户、菜单、文章内容等方面数据的存储,如下所示。

当然,这个WHC.Weixin.Data是集大成者,它需要使用WHC.Weixin.API的项目内容来做数据提交,同时也是需要使用内部的数据存储处理模块。

2)企业号模块封装

企业号的做法和公众号类似,也是需要对微信提供的各种
API进行封装,方便我们后面的接口调用,不过企业号目前支持的功能相对公众号少一些,大概包括有基础接口、企业号应用接口、菜单管理、通讯录管理、消息管理、摇一摇周边等模块。随着企业号功能的逐步完善和加入,可能腾讯会加入更多的一些功能模块。

同样我们参考微信公众号的做法,也是建立一个数据存储管理的项目,作为微信消息事件的处理入口,同时也管理存储一些必须的数据,包括需要同步的用户、标签、部门等数据。

随着微信公众号和企业号的功能逐渐统一,很多接口的交互数据几乎是一样的,因此我们可以把公用的实体类部分作为一个独立的项目,方便公众号和企业号两个项目的共同使用,这个项目命名为
WHC.Common.Entity

它们几个项目关系如下所示。

项目目录如下所示,包括了基础模块、摇一摇红包、菜单及多媒体管理模块、消息请求模块、消息应答模块、摇一摇周边、微信支付等。

由于微信支付的接口实现,是在企业号和公众号相对比较独立的一个API接口群,因此我们可以微信支付部分独立作为一个接口实现来处理,公众号或者企业号需要的时候,包含进去使用即可。

我们把它命名为WHC.Common.API项目。

整个插件业务接口包括:百度的地理位置解析接口、电影院信息查询、天气信息查询、交通信息查询、旅游信息查询等,还有短信、邮件发送等常规接口,都可以使用这种方式进行处理。接口的效果展示如下所示。

因此上面这些以WHC.Common命名的项目,基本上就是可以通用在公众号和企业号两边的项目模块了,它们包含前面介绍过的几个模块,如下所示。

当然,除了这些之外,我们做项目,一般还涉及到一些基础功能模块,如公用类库,以及附件管理、通讯录管理、权限管理模块等内容,我们可以把后者几个模块放在一起,组成基础模块。

3)微信界面项目

微信界面部分是前面模块组件的综合使用,在微信应用里面,一般需要使用80端口和微信服务器做交互,而这个同时往往也是我们项目的端口地址。

1.1.

设备添加及查看

在一些数据的即时查询场景中,我们可能需要对输入信息进行模糊查询并进行选择,例如在一些文本输入场景,如输入某个站点编码或者设备编码,然后获取符合的列表供用户选择的场景,本篇随笔介绍在DevExpress程序中使用PopupContainerEdit和PopupContainer实现数据展示。

1、回顾SearchLookupEdit控件使用

在DevExpress中,我们如果需要好的体验效果也可以用SearchLookupEdit来实现数据的查询及展示,不过这个控件,需要提前准备好数据源,然后是基于固定的数据源进行搜索的,如下所示。

这种可以在编辑框里面输入数据,并且可以实时根据输入的内容进行过滤,是一种比较好的搜索体验,不过不好的地方就是数据需要提前预先加载,如果数据库有成千上万条记录,那么这种方式弊端就比较明显了,因此不是很适合大数据,而且能够即时进行数据搜索展示的场景。

2、使用ButtonEdit的方式进行搜索

除了第一点的搜索方式外,也可以使用一种文本和按钮合并的控件来实现数据的查询选择,控件名称为ButtonEdit,界面效果如下所示。

当我们单击文本输入的右侧按钮控件后,可以让它弹出一个对话框进行数据的选择,对话框窗体里面可以根据条件进行数据的分页查询,这种方式可以很好实现多条件的查询选择,双击记录选择好就关闭窗体界面即可。

上面的按钮在设计界面里面,为相关的事件添加代码即可。

实现上面功能界面的代码很简单,如下所示。

        private void txtOfferNum_Properties_Click(objectsender, EventArgs e)
{
FrmSelectOffer dlg
= newFrmSelectOffer();if(dlg.ShowDialog() ==System.Windows.Forms.DialogResult.OK)
{
var info =dlg.OfferInfo;if(info != null)
{
this.txtOfferNum.Text = info.OfferNum;

3、使用PopupContainerEdit和PopupContainer

除了上面界面的选择方式外,在DevExpress里面,我们也可以使用 PopupContainerEdit和PopupContainer实现数据展示,这种方式好处就是可以在录入的时候进行及时查询,而且数据是即时加载的,不会一次性加载所有的数据,为了演示这种方式的界面处理,我做了一个小案例,如下所示。

这种方式的展示,会及时列出相关的数据,在表格控件上选择后返回主界面。

如果按键Esc,那么关闭弹出层并切换到输入层,重新输入,回车后进行查询。

首先在代码处理中,需要对输入控件的按键进行处理。

        /// <summary>
        ///对按键进行相关处理/// </summary>
        private void popupContainerEdit1_KeyPress(objectsender, KeyPressEventArgs e)
{
this.popupContainerEdit1.ShowPopup();//回车的时候绑定数据源,并设置 if (e.KeyChar == '\r')
{
BindData();
this.gridView1.Focus();
canAcceptReturn
= false;
}
else{this.ActiveControl = this.popupContainerEdit1;this.popupContainerEdit1.Focus();
}
}

在输入回车的时候,我们执行数据查询操作。

我们这里测试了对数据字典的查询显示,只是为了演示数据的即时查询操作。

        /// <summary>
        ///绑定GridView的数据源/// </summary>
        private voidBindData()
{
var value = this.popupContainerEdit1.Text;this.lblName.Text =value;string condition = string.Format("Name like '%{0}%'", value);var list = BLLFactory<DictData>.Instance.Find(condition);this.gridView1.Columns.Clear();this.gridView1.CreateColumn("Name", "名称", 200, false);this.gridView1.CreateColumn("Value", "字典值", 200, false);this.gridView1.CreateColumn("Seq", "排序", 80, false);this.gridControl1.DataSource =list;
}

为了实现在列表中单击或者使用回车键进行选择,我们对相关的事件进行了处理。

        private void gridView1_RowClick(objectsender, DevExpress.XtraGrid.Views.Grid.RowClickEventArgs e)
{
GetSelectValue();
}
private void gridControl1_KeyUp(objectsender, KeyEventArgs e)
{
if (e.KeyCode ==Keys.Enter)
{
if(canAcceptReturn)
{
GetSelectValue();
}

canAcceptReturn
= true;
}
}
        private void GetSelectValue(bool closePopup = true)
{
var value = string.Concat(this.gridView1.GetFocusedRowCellValue("Name"));if(closePopup)
{
this.popupContainerEdit1.ClosePopup();
}
this.popupContainerEdit1.Text =value;
}

一旦容器焦点消失,我们让焦点重新回到输入控件上,如下代码实现。

        private void popupContainerControl1_Leave(objectsender, EventArgs e)
{
//容器退出的时候,重新定位焦点到编辑框 this.popupContainerEdit1.Focus();
}

整个案例代码如下所示。

    public partial classForm1 : DevExpress.XtraEditors.XtraForm
{
/// <summary> ///设置一个标识,是否在GridView中可以接受回车键/// </summary> bool canAcceptReturn = false;publicForm1()
{
InitializeComponent();
}
/// <summary> ///对按键进行相关处理/// </summary> private void popupContainerEdit1_KeyPress(objectsender, KeyPressEventArgs e)
{
this.popupContainerEdit1.ShowPopup();//回车的时候绑定数据源,并设置 if (e.KeyChar == '\r')
{
BindData();
this.gridView1.Focus();
canAcceptReturn
= false;
}
else{this.ActiveControl = this.popupContainerEdit1;this.popupContainerEdit1.Focus();
}
}
/// <summary> ///绑定GridView的数据源/// </summary> private voidBindData()
{
var value = this.popupContainerEdit1.Text;this.lblName.Text =value;string condition = string.Format("Name like '%{0}%'", value);var list = BLLFactory<DictData>.Instance.Find(condition);this.gridView1.Columns.Clear();this.gridView1.CreateColumn("Name", "名称", 200, false);this.gridView1.CreateColumn("Value", "字典值", 200, false);this.gridView1.CreateColumn("Seq", "排序", 80, false);this.gridControl1.DataSource =list;
}
private void gridView1_RowClick(objectsender, DevExpress.XtraGrid.Views.Grid.RowClickEventArgs e)
{
GetSelectValue();
}
private void gridControl1_KeyUp(objectsender, KeyEventArgs e)
{
if (e.KeyCode ==Keys.Enter)
{
if(canAcceptReturn)
{
GetSelectValue();
}

canAcceptReturn
= true;
}
}
private void GetSelectValue(bool closePopup = true)
{
var value = string.Concat(this.gridView1.GetFocusedRowCellValue("Name"));if(closePopup)
{
this.popupContainerEdit1.ClosePopup();
}
this.popupContainerEdit1.Text =value;
}
private void popupContainerControl1_Leave(objectsender, EventArgs e)
{
//容器退出的时候,重新定位焦点到编辑框 this.popupContainerEdit1.Focus();
}
private void Form1_Load(objectsender, EventArgs e)
{
}
}