2023年2月

在很多系统模块里面,我们可能都需要进行一定的数据交换处理,也就是数据的导入或者导出操作,这样的批量处理能给系统用户更好的操作体验,也提高了用户录入数据的效率。我在较早时期的EasyUI的Web框架上,也介绍过通过Excel进行的数据导入导出操作,随笔文章为《
基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出
》,本文基于Bootstrap的框架基础上,再对这个模块进行更新处理,以及Office文档或者图片等附件的查看处理。

1、数据的导入操作

一般系统模块里面,都有数据导入和导出操作,因此在界面自动生成的时候,我都倾向于给用户自动生成这些标准的查询、导入、导出等操作功能,界面效果如下所示。

导入操作,在Bootstrap框架里面,我把它作为一个层的,都统一放在index.cshtml文件里面,这样可以使得整个界面的处理更加紧密一点,示例代码如下所示。

下面这些代码一般情况下,都是自动生成的,包括所需的全部字段,我们一般是根据需要进行字段的裁剪,以适应我们的业务和实际需要。

<!--导入数据操作层-->
<divid="import"class="modal fade bs-modal-lg"tabindex="-1"role="dialog"aria-labelledby="myModalLabel"aria-hidden="true">
    <divclass="modal-dialog modal-lg">
        <divclass="modal-content">
            <divclass="modal-header bg-primary">
                <buttontype="button"class="close"data-dismiss="modal"aria-hidden="true"></button>
                <h4class="modal-title">文件导入</h4>
            </div>
            <divclass="modal-body">
                <divstyle="text-align:right;padding:5px">
                    <ahref="~/Content/Template/User-模板.xls"onclick="javascript:Preview();">
                        <imgalt="测试用户信息-模板"src="~/Content/images/ico_excel.png" />
                        <spanstyle="font-size:larger;font-weight:200;color:red">User-模板.xls</span>
                    </a>
                </div>
                <hr/>
                <formid="ffImport"method="post">
                    <divtitle="Excel导入操作"style="padding: 5px"data-options="iconCls:'icon-key'">
                        <inputclass="easyui-validatebox"type="hidden"id="AttachGUID"name="AttachGUID" />    
                        <inputid="file_upload"name="file_upload"type="file"multiple="multiple">                    
                        <ahref="javascript:;"class="btn btn-primary"id="btnUpload"onclick="javascript: $('#file_upload').uploadify('upload', '*')">上传</a>
                        <ahref="javascript:;"class="btn btn-default"id="btnCancelUpload"onclick="javascript: $('#file_upload').uploadify('cancel', '*')">取消</a>

                        <divid="fileQueue"class="fileQueue"></div>
                        <br/>                    
                        <hrstyle="width:98%" />                    
                        <divid="div_files"></div>
                        <br/>                    
                    </div>
                </form>

                <!--数据显示表格-->
                <tableid="gridImport"class="table table-striped table-bordered table-hover"cellpadding="0"cellspacing="0"border="0"class="display"width="100%">
                    <theadid="gridImport_head">
                        <tr>
                            <thclass="table-checkbox"style="width:40px"><inputclass="group-checkable"type="checkbox"onclick="selectAll(this)"></th>
                            <th>用户编码</th>
                            <th>用户名/登录名</th>
                            <th>真实姓名</th>
                            <th>职务头衔</th>
                            <th>移动电话</th>
                            <th>办公电话</th>
                            <th>邮件地址</th>
                            <th>性别</th>
                            <th>QQ号码</th>
                            <th>备注</th>
                        </tr>
                    </thead>
                    <tbodyid="gridImport_body"></tbody>
                </table>
            </div>
            <divclass="modal-footer">
                <buttontype="button"class="btn btn-default"data-dismiss="modal">关闭</button>
                <buttontype="button"class="btn btn-primary"onclick="SaveImport()">保存</button>
            </div>
        </div>
    </div>
</div>

我们如果要显示导入操作界面,那么只需要把这个层显示出来即可,如下脚本所示。

        //显示导入界面
        functionShowImport() {
$(
"#import").modal("show");
}

这里的文件上传处理,主要使用了Uploadify的这个控件进行处理的,当然也可以利用我前面介绍过的File Input上传控件进行处理,都可以很好实现这些导入操作。

一般情况下的Uploadify控件的初始化代码如下所示

        $(function() {//添加界面的附件管理
            $('#file_upload').uploadify({'swf': '/Content/JQueryTools/uploadify/uploadify.swf',  //FLash文件路径
                'buttonText': '浏  览',                                 //按钮文本
                'uploader': '/FileUpload/Upload',                       //处理上传的页面
                'queueID': 'fileQueue',                         //队列的ID
                'queueSizeLimit': 1,                            //队列最多可上传文件数量,默认为999
                'auto': false,                                  //选择文件后是否自动上传,默认为true
                'multi': false,                                 //是否为多选,默认为true
                'removeCompleted': true,                        //是否完成后移除序列,默认为true
                'fileSizeLimit': '10MB',                        //单个文件大小,0为无限制,可接受KB,MB,GB等单位的字符串值
                'fileTypeDesc': 'Excel Files',                  //文件描述
                'fileTypeExts': '*.xls',                        //上传的文件后缀过滤器
                'onQueueComplete': function (event, data) {     //所有队列完成后事件
                    //业务处理代码
                    //提示用户Excel格式是否正常,如果正常加载数据
},'onUploadStart': function(file) {
InitUpFile();
//上传文件前 ,重置GUID,每次不同 $("#file_upload").uploadify("settings", 'formData', { 'folder': '数据导入文件', 'guid': $("#AttachGUID").val() }); //动态传参数 },'onUploadError': function(event, queueId, fileObj, errorObj) {//alert(errorObj.type + ":" + errorObj.info); }
});
});

关键的逻辑就是:

//业务处理代码

一般情况下,我们在这里已经在服务器里面获得了Excel文件了,因此需要对这个文件的格式进行处理,如果格式正确,那么我们把数据显示出来,供导入用户进行记录的选择,决定导入那些记录即可。

处理检查Excel数据格式的代码如下所示。

                    //提示用户Excel格式是否正常,如果正常加载数据
$.ajax({
url:
'/User/CheckExcelColumns?guid=' +guid,
type:
'get',
dataType:
'json',
success:
function(data) {if(data.Success) {
InitGrid();
//重新刷新表格数据 showToast("文件已上传,数据加载完毕!");
}
else{
showToast(
"上传的Excel文件检查不通过。请根据页面右上角的Excel模板格式进行数据录入。", "error");
}
}
});

我们就是在后台增加一个CheckExcelColumns的方法,用来检查Excel文件的字段格式的,只有符合格式要求的文件,才被获取数据并显示在界面上。

显示在界面上的JS代码,也就是主要把Excel文件的内容提取出来,并绑定在Table元素上即可。

        //根据条件查询并绑定结果
        functionInitGrid() {var guid = $("#AttachGUID").val();var url = "/User/GetExcelData?guid=" +guid;
$.getJSON(url,
function(data) {
$(
"#gridImport_body").html("");

$.each(data.rows,
function(i, item) {var tr = "<tr>";
tr
+= "<td><input class='checkboxes' type=\"checkbox\" name=\"checkbox\" ></td>";
tr
+= "<td>" + item.HandNo + "</td>";
tr
+= "<td>" + item.Name + "</td>";
tr
+= "<td>" + item.FullName + "</td>";
tr
+= "<td>" + item.Title + "</td>";
tr
+= "<td>" + item.MobilePhone + "</td>";
tr
+= "<td>" + item.OfficePhone + "</td>";
tr
+= "<td>" + item.Email + "</td>";
tr
+= "<td>" + item.Gender + "</td>";
tr
+= "<td>" + item.QQ + "</td>";
tr
+= "<td>" + item.Note + "</td>";

tr
+= "</tr>";
$(
"#gridImport_body").append(tr);
});
});
}

为了更进一步获取用户导入到具体的部门,那么我们还可以弹出一个对话框用然后选择具体的信息,最后才提交数据到后台进行处理。

操作代码如下所示。

        //保存导入的数据
        functionSaveImport() {//赋值给对象
            $("#Company_ID3").select2("val", @Session["Company_ID"]).trigger('change');
$(
"#Dept_ID3").select2("val", @Session["Dept_ID"]).trigger('change');


$(
"#selectDept").modal("show");
}

这样我们确认保存的时候,只需要通过Ajax把数据提交给后台处理即可,具体JS代码如下所示。

$.ajax({
url:
'/User/SaveExcelData',
type:
'post',
dataType:
'json',
contentType:
'application/json;charset=utf-8',
traditional:
true,
success:
function(data) {if(data.Success) {//保存成功 1.关闭弹出层,2.清空记录显示 3.刷新主列表 showToast("保存成功");

$(
"#import").modal("hide");
$(bodyTag).html(
"");
Refresh();
}
else{
showToast(
"保存失败:" + data.ErrorMessage, "error");
}
},
data: postData
});

Uploadify的处理在我之前基于EasyUI的界面里面也有说明,有兴趣可以参考《
基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出
》、《
基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用
》以及《
Web开发中的文件上传组件uploadify的使用
》。

2、数据的导出操作

数据的导出操作相对比较简单,一般情况下,我们把数据写入一个固定的Excel表里面,然后提供URL给用户下载即可。

        //导出Excel数据
        functionShowExport() {var url = "/User/Export";var condition = $("#ffSearch").serialize();//获取条件
executeExport(url, condition);//执行导出
        }

具体的逻辑代码如下所示

//执行导出操作,输出文件
functionexecuteExport(url, condition) {
$.ajax({
type:
"POST",
url: url,
data: condition,
success:
function(filePath) {var downUrl = '/FileUpload/DownloadFile?file=' +filePath;
window.location
=downUrl;
}
});
}

3、附件的查看处理

多数情况下,我们可能需要查看上传的文件,包括Office文档、图片等可以进行预览的,是在不行,可以提供下载本地打开查看。

我在基于EasyUI的Web开发也介绍了Office的预览处理:《
基于MVC4+EasyUI的Web开发框架经验总结(8)--实现Office文档的预览
》,这里我们改进一下即可实现具体的Office预览和图片查看功能了。

上篇文件介绍了Office的预览有两种途径,一种是利用微软Office的预览地址进行预览,一种是用控件生成HTML进行预览,两种可以结合使用,根据需要进行配置即可。

        /// <summary>
        ///根据附件ID,获取对应查看的视图URL。///一般规则如果是图片文件,返回视图URL地址'/FileUpload/ViewAttach';///如果是Office文件(word、PPT、Excel)等,可以通过微软的在线查看地址进行查看:'http://view.officeapps.live.com/op/view.aspx?src=',///也可以进行本地生成HTML文件查看。如果是其他文件,可以直接下载地址。/// </summary>
        /// <param name="id">附件的ID</param>
        /// <returns></returns>
        public ActionResult GetAttachViewUrl(stringid)
{
string viewUrl = "";
FileUploadInfo info
= BLLFactory<FileUpload>.Instance.FindByID(id);if (info != null)
{
string ext = info.FileExtend.Trim('.').ToLower();string filePath =GetFilePath(info);bool officeInternetView = false;//是否使用互联网在线预览 string hostName = HttpUtility.UrlPathEncode("http://www.iqidi.com/");//可以配置一下,如果有必要 if (ext == "xls" || ext == "xlsx" || ext == "doc" || ext == "docx" || ext == "ppt" || ext == "pptx")
{
if(officeInternetView)
{
//返回一个微软在线浏览Office的地址,需要加上互联网域名或者公网IP地址 viewUrl = string.Format("http://view.officeapps.live.com/op/view.aspx?src={0}{1}", hostName, filePath);
}
else{#region 动态第一次生成文件 //检查本地Office文件是否存在,如不存在,先生成文件,然后返回路径供查看 string webPath = string.Format("/GenerateFiles/Office/{0}.htm", info.ID);string generateFilePath =Server.MapPath(webPath);if (!FileUtil.FileIsExist(generateFilePath))
{
string templateFile = BLLFactory<FileUpload>.Instance.GetFilePath(info);
templateFile
= Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, templateFile.Replace("\\", "/"));if (ext == "doc" || ext == "docx")
{
Aspose.Words.Document doc
= newAspose.Words.Document(templateFile);
doc.Save(generateFilePath, Aspose.Words.SaveFormat.Html);
}
else if (ext == "xls" || ext == "xlsx")
{
Workbook workbook
= newWorkbook(templateFile);
workbook.Save(generateFilePath, SaveFormat.Html);
}
else if (ext == "ppt" || ext == "pptx")
{
templateFile
= templateFile.Replace("/", "\\");
PresentationEx pres
= newPresentationEx(templateFile);
pres.Save(generateFilePath, Aspose.Slides.Export.SaveFormat.Html);
}
}
#endregionviewUrl=webPath;
}
}
else{
viewUrl
=filePath;
}
}
returnContent(viewUrl);
}

通过这个后台处理代码,我们可以正确知道Office预览的时候,使用的是哪个URL了。

这样在前端页面,我们只需要判断具体是那种文件,然后进行展示即可了。

        if(type =="image") {var imgContent = '<img src="'+ viewUrl + '" />';
$(
"#divViewFile").html(imgContent);
$(
"#file").modal("show");
}
else{
$.ajax({
type:
'GET',
url: viewUrl,
//async: false, //同步 //dataType: 'json', success: function(json) {
$(
"#divViewFile").html(json);
$(
"#file").modal("show");
},
error:
function(xhr, status, error) {
showError(
"操作失败" + xhr.responseText); //xhr.responseText }
});
}

其中的代码

$("#file").modal("show");

是我们调用全局对话框,用来展示具体的内容的,效果如下所示。

word文档预览效果如下所示:

或者我们查看图片文件的时候,可以获得界面效果如下所示:

以上就是 数据的导入、导出及附件的查看处理的介绍内容,希望对大家学习有帮助。

如果有兴趣,可以继续参考系列文章:

基于Metronic的Bootstrap开发框架经验总结(1)-框架总览及菜单模块的处理

基于Metronic的Bootstrap开发框架经验总结(2)--列表分页处理和插件JSTree的使用


基于Metronic的Bootstrap开发框架经验总结(3)--下拉列表Select2插件的使用

基于Metronic的Bootstrap开发框架经验总结(4)--Bootstrap图标的提取和利用

基于Metronic的Bootstrap开发框架经验总结(5)--Bootstrap文件上传插件File Input的使用


基于Metronic的Bootstrap开发框架经验总结(6)--对话框及提示框的处理和优化

基于Metronic的Bootstrap开发框架经验总结(7)--数据的导入、导出及附件的查看处理


基于Metronic的Bootstrap开发框架经验总结(8)--框架功能总体界面介绍


基于Metronic的Bootstrap开发框架经验总结(9)--实现Web页面内容的打印预览和保存操作

在前面介绍了一系列的《基于Metronic的Bootstrap开发框架经验总结》的随笔文章,随笔主要是介绍各个知识点的内容,对框架的总体性界面没有很好的阐述,本篇随笔主要介绍这个Bootstrap框架的总体性功能界面,介绍其中用到的知识点和整体性的界面。希望读者对框架有一个更加直观、真实的认识了解,界面设计以及相关思路可以借鉴提高,也可以对相关的内容进行相互探讨,共同提高。

1、《基于Metronic的Bootstrap开发框架》技术特点

1)采用最新最炫的Bootstrap响应式框架技术

整个基于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模块进行数据访问的控制,使得数据访问更方便轻松。

整体框架开发采用Visual Studuio 2013以及页面编辑工具Sublime Text结合开发,页面以及后台代码,通过代码生成工具Database2Sharp进行快速开发,实现整体性开发的最大效率提高。

框架的总体结构如下所示:

控制器设计:
Bootstrap开发框架沿用了我的《
Winform开发框架
》和《
基于EasyUI的Web框架》
的很多架构设计思路和特点,对Controller进行了封装。使得控制器能够获得很好的继承关系,并能以更少的代码,更高效的开发效率,实现Web项目的开发工作,整个控制器的设计思路如下所示。

权限控制:
良好的控制器设计规则,可以为Web开发框架本身提供了很好用户访问控制和权限控制,使得用户界面呈现菜单、Web界面的按钮和内容、Action的提交控制,均能在总体权限功能分配和控制之下。

代码快速生成:
良好的架构使得无论在业务逻辑层、控制器层、Web界面的UI层,均能提供统一的代码逻辑,这些代码均能通过代码生成工具Database2Sharp进行生成。Web界面代码可以充分利用代码生成工具Database2Sharp的元数据信息,实现Web界面的快速生成。有效减少出错的几率,提高Web界面编码的开发效率和乐趣,更可以使得企业内部的编码模式进行高效的统一。

Enterprise Library代码生成,可以快速生成除界面外的整体性的框架代码,Bootstrap的Web界面代码生成,可以快速生成基于Metronic的Bootstrap的前端界面代码和后台控制器代码,界面部分包括查询、分页、数据展示、数据导入导出、新增、编辑、查看、删除等基础功能界面,生成后我们可以基于这个基础上进行简单、快速的修改即可符合实际需要,极大提高我们Web界面的开发效率。

框架布局:
以下是我整体性项目的总的效果图。

【系统菜单栏】的内容,是动态从数据库里面获取的菜单;【系统顶栏】放置一些信息展示,以及提供用户对个人数据快速处理,如查看个人信息、注销、锁屏等操作内容;内容区一般包括【树列表区】、【条件查询区】和【列表数据及分页】内容,内容区域主要是可视化展示的数据,可以通过树列表控件、表格控件进行展示,一般数据还有增删改查、以及分页的需要,因此需要整合各种功能的处理。另外,用户的数据,除了查询展示外,还需要有导入、导出等相关操作,这些是常规性的数据处理功能。

菜单的处理和展示:一般为了管理方便,菜单分为三级,选中的菜单和别的菜单样式有所区分,菜单可以折叠最小化,效果如下所示。


2、《基于Metronic的Bootstrap开发框架》模块界面介绍

2.1 首页图表模块界面

2.2 系统顶栏功能

2.3 行业动态功能(政策法规、通知公告、动态信息)

政策法规/通知公告/动态信息 列表界面

编辑界面如下所示:

查看内容界面如下所示:

2.4 客户信息管理

客户列表界面如下所示:

客户信息编辑界面:

客户信息导入界面:

2.5 客户联系人管理

客户联系人添加/编辑界面

客户联系人查看界面

附件信息界面

2.6 通讯录管理

通讯录列表

通讯录编辑界面如下所示。

2.7 权限管理

1) 系统用户 列表界面

系统用户导入界面

系统用户编辑界面

系统用户的肖像上传和编辑

系统用户删除确认对话框。

系统用户的RDLC报表界面。

2)机构管理界面

组织机构包含用户编辑界面

3)用户角色管理

角色可操作功能集合展示

角色可访问数据权限控制:

角色包含机构管理:

4)系统功能管理

5)系统菜单管理

编辑菜单信息界面:

选择菜单图标界面:

6)系统登录日志管理

7)通用字典管理

8)菜单图标管理

9)图片相册管理

编辑图片界面如下所示:

图片查看界面

如果有兴趣,可以继续参考系列文章:

基于Metronic的Bootstrap开发框架经验总结(1)-框架总览及菜单模块的处理

基于Metronic的Bootstrap开发框架经验总结(2)--列表分页处理和插件JSTree的使用


基于Metronic的Bootstrap开发框架经验总结(3)--下拉列表Select2插件的使用

基于Metronic的Bootstrap开发框架经验总结(4)--Bootstrap图标的提取和利用

基于Metronic的Bootstrap开发框架经验总结(5)--Bootstrap文件上传插件File Input的使用


基于Metronic的Bootstrap开发框架经验总结(6)--对话框及提示框的处理和优化

基于Metronic的Bootstrap开发框架经验总结(7)--数据的导入、导出及附件的查看处理


基于Metronic的Bootstrap开发框架经验总结(8)--框架功能总体界面介绍


基于Metronic的Bootstrap开发框架经验总结(9)--实现Web页面内容的打印预览和保存操作

在我前面介绍的WebAPI文章里面,介绍了WebAPI的架构设计方面的内容,其中提出了现在流行的WebAPI优先的路线,这种也是我们开发多应用(APP、微信、微网站、商城、以及Winform等方面的整合)的时候值得考虑的线路之一。一般情况下,由于HTTP协议的安全性,传递的参数容易被拦截,从而可能导致潜在的危险,所以一般WebAPI接口层都采用了HTTPS协议的,也就是采用SSL层来对数据进行安全性的加密的。

1、HTTPS基础知识介绍

1)
HTTPS

HTTPS(全称:
Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的
HTTP通道,简单讲是
HTTP的安全版。即
HTTP下加入
SSL层,
HTTPS的安全基础是
SSL,因此加密的详细内容就需要
SSL。 它是一个
URI scheme(抽象标识符体系),句法类同
http:体系。用于安全的
HTTP数据传输。
https:URL表明它使用了
HTTPS,但
HTTPS存在不同于
HTTP的默认端口及一个加密
/身份验证层(在
HTTP与
TCP之间)。这个系统的最初研发由网景公司进行,提供了身份验证与加密通讯方法,现在它被广泛用于万维网上安全敏感的通讯,例如交易支付方面。

2)HTTPS和
HTTP的区别

一、
https协议需要到
ca申请证书,一般免费证书很少,需要交费。

二、
http是超文本传输协议,信息是明文传输,
https 则是具有安全性的
ssl加密传输协议。

三、
http和
https使用的是完全不同的连接方式,用的端口也不一样,前者是
80,后者是
443。

四、
http的连接很简单,是无状态的;
HTTPS协议是由
SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比
http协议安全。

3)https的实现原理

有两种基本的加解密算法类型

1)对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有
DES、
AES等;

2)非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有
RSA、
DSA等。

https的通信过程

4) https通信的优点

1)客户端产生的密钥只有客户端和服务器端能得到;

2)加密的数据只有客户端和服务器端才能得到明文;

3)客户端到服务端的通信是安全的。

2、SSL
基础知识介绍

1)SSL安全套接层协议(Secure Socket Layer)

为Netscape所研发,用以保障在Internet上数据传输之安全,利用数据加密(Encryption)技术,可确保数据在网络上之传输过程中不会被截取及窃听。目前一般通用之规格为40 bit之安全标准,美国则已推出128 bit之更高安全标准,但限制出境。只要3.0版本以上之IE或Netscape浏览器即可支持SSL。

当前版本为3.0。它已被广泛地用于Web浏览器与服务器之间的身份认证和加密数据传输。

SSL协议位于TCP/IP协议与各种应用层协议之间,是一种国际标准的加密及身份认证通信协议,为TCP提供一个可靠的端到端的安全服务,为两个通讯个体之间提供保密性和完整性(身份鉴别)。SSL协议可分为两层:SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

2)SSL协议特点

1)SSL协议可用于保护正常运行于TCP之上的任何应用协议,如HTTP、FTP、SMTP或Telnet的通信,最常见的是用SSL来保护HTTP的通信。

2)SSL协议的优点在于它是与应用层协议无关的。高层的应用协议(如HTTP、FTP、Telnet等)能透明地建立于SSL协议之上。

3)SSL协议在应用层协议之前就已经完成加密算法、通信密钥的协商以及服务器的认证工作。在此之后应用层协议所传送的数据都会被加密,从而保证通信的安全性。

4)SSL协议使用通信双方的客户证书以及CA根证书,允许客户/服务器应用以一种不能被偷听的方式通信,在通信双方间建立起了一条安全的、可信任的通信通道。

5)该协议使用密钥对传送数据加密,许多网站都是通过这种协议从客户端接收信用卡编号等保密信息。常用于交易过程中。

3)SSL功能

1)客户对服务器的身份认证:

SSL服务器允许客户的浏览器使用标准的公钥加密技术和一些可靠的认证中心(CA)的证书,来确认服务器的合法性。

2)服务器对客户的身份认证:

也可通过公钥技术和证书进行认证,也可通过用户名,password来认证。

3)建立服务器与客户之间安全的数据通道:

SSL要求客户与服务器之间的所有发送的数据都被发送端加密、接收端解密,同时还检查数据的完整性。

3、支持SSL的CA证书购买

上面介绍一些基础知识,我们可以简单的概括一下,就是引入了HTTPS,可以很好解决接口参数的安全性问题。这些在很多大厂商的接口里面,都是使用HTTPS协议的,如腾讯微信、支付宝等接口,特别对于支付内容,使用HTTPS是必须的。

对于支持HTTPS,核心的问题就是解决证书的问题,必须是由第三方权威机构颁发的证书,这样才能达到不被伪造的可能性,一般我们采用的是CA证书,这些证书是需要通过付费购买的(天下没有免费的午餐),证书的购买的供应商网站有很多,可以选择自己合适的进行购买。

1)国外的GoDaddy平台购买证书过程

这个参考博客园站长dudu的介绍内容,这个goDaddy是支持支付宝用美元购买的,使用平台是英文,可以购买域名、证书等服务,是一个影响力较大的厂商。

1) 打开godaddy.com网站,通过菜单进入Products -> SSL&Security -> SSL Certificates,选择Protect All Subdomains("Wildcard"), 在Pick your plan type中,选择Standard(Validates domain ownership),然后完成购买。

2)进入My Account -> SSL CERTIFICATES,创建证书(Certificate),创建时将之前得到的CSR内容复制到“CSR文本框”中。

3)接入来进入GoDaddy的审批流程,在审批过程中需要验证域名的所有者(dns填加记录或上传html文件至网站目录),验证成功后很快就会生成CA证书。

4)下载CA证书文件至生成CSR的服务器上。

普通证书费用是63美元左右,可以通过支付宝进行交易。

2)国内沃通平台购买证书过程

这个是纯中文版本的平台,比较方便使用,费用也比上面的贵一些,分的级别好像也多一些,相对上面那个国外的GoDaddy的三个种类证书,这个产品线分了6个类型。最低的也要接近500块,相对GoDaddy来说,费用要多一些了。由于是初步使用,也就购买了这个使用了。

申请购买也很简单,一步步按提示操作填写内容即可,大致分为这几步:

1)输入购买的证书类型,以及年限等资料;

2)输入域名的信息,以及需要域名对应的邮件进行验证;

3)提交购买订单后,使用在线支付或者使用公司账号汇款到指定账号;

4)客服验证后,技术人员提供证书生成操作,我们在列表里面尽快使用证书密码下载证书。

最后成功后,在订单信息里面,有这样的列表,提供了两个SHA1和SHA2两种加密协议的证书,官方建议使用SHA2。

4、
Web API应用支持HTTPS

有了证书,我们在云服务器上(如阿里云)的IIS里面的证书模块里面,可以导入已有的证书。

导入证书成功后,我们可以看到列表里面有具体的证书了,同时双击可以查看证书详细信息。

创建一个网站,并指定使用这个证书,端口采用443即可。

一般情况下,我们创建这个步骤后,你可以使用类似这样地址:https://www.iqidi.com/测试下具体的网站是否已经开通了,如果可以正常访问,那说明你的443端口已经启动侦听了,如果没有,可能是没有启动,或者是防火墙禁止了,一般情况下,我们还是需要在防火墙里面增加443端口的入站规则,设置为允许,否则可能被屏蔽了。

如果都设置了,可以通过DOS命令行来检查端口是否已经开始侦听了。

查询443端口监听

netstat -ano | find "443"

如果还是有问题,可以请求CA证书的供应商或者云供应商的技术人员帮你看看,一般都是会配合你解决的,我的开始也是折腾了他们一会,最后也莫名其妙的可以了。

最后成功后,在Chrome浏览器里面打开地址就有绿色的标志了。

由于目前我的API平台还在搭建完善中,因此这个域名展示还是没有东西可以展示的。

最后的构架会如前面Web API框架图所示,做到一个统一的整合方案里面。

由于Web API层作为一个公共的接口层,我们就很好保证了各个界面应用层的数据一致性,如果考虑到响应式的集成处理,我们甚至可以把微信应用、APP应用、Web应用做层一套Web程序,即使为了利用各自应用的特殊性,也可以把这些应用做的很相似,这样就给用户提供了一个统一的界面表示方式,极大提高客户使用的界面体验效果,用户几乎不需要额外的界面学习,就可以熟悉整个应用体系的各个模块使用。

从上面的架构分析来看,我们的Web API作为核心层,可以在上面开发我们各种企业业务应用,

在目前比较热门的会员管理、客户管理等方面,结合微信的应用催化剂,就可以做的更加符合移动的潮流,从而实现我们“互联网+”的应用落地。

在前阵子,我对实体框架进行了一定的研究,然后把整个学习的过程开了一个系列,以逐步深入的方式解读实体框架的相关技术,期间每每碰到一些新的问题需要潜入研究。本文继续前面的主题介绍,着重从整体性的来总结一下实体框架的一些方面,希望针对这些实际问题,和大家进行学习交流。

我的整个实体框架的学习和研究,是以我的Winform框架顺利升级到这个实体框架基础上为一个阶段终结,这个阶段事情很多,从开始客运联网售票的WebAPI平台的开发,到微软实体框架的深入研究,以及《基于Metronic的Bootstrap开发框架经验总结》的主题学习和分享等等方面,都混到一起来了,多个主题之间穿插着写一些随笔,也是希望把自己的学习过程进行记录总结,不用等到最后全部忘记了。

1、实体框架主键的类型约束问题

在我们搭建整个实体框架的过程中,我们一般都是抽象封装处理很多基础的增删改查、分页等常见的数据处理功能,如下所示。

        /// <summary>
        ///更新对象属性到数据库中/// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="key">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Update(T t, objectkey);/// <summary>
        ///更新对象属性到数据库中(异步)/// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="key">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        Task<bool> UpdateAsync(T t, objectkey);/// <summary>
        ///根据指定对象的ID,从数据库中删除指定对象/// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Delete(objectid);/// <summary>
        ///根据指定对象的ID,从数据库中删除指定对象(异步)/// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        Task<bool> DeleteAsync(objectid);/// <summary>
        ///查询数据库,返回指定ID的对象/// </summary>
        /// <param name="id">ID主键的值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        T FindByID(objectid);/// <summary>
        ///查询数据库,返回指定ID的对象(异步)/// </summary>
        /// <param name="id">ID主键的值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        Task<T> FindByIDAsync(object id);

上面的外键统一定义为object类型,因为我们为了主键类型通用的考虑。

在实际上表的外键类型可能是很多种的,如可能是常见的字符类型,也可能是int类型,也可能是long类型等等。如果我们更新、查找、删除整形类型的记录的时候,那么可能机会出现错误:

The argument types 'Edm.Int32' and 'Edm.String' are incompatible for this operation.

这些错误就是主键类型不匹配导致的,我们操作这些接口的时候,一定要传入对应类型给它们,才能正常的处理。

本来想尝试在内部进行转换处理为正确的类型的,不过没有找到很好的解决方案来识别和处理,因此最好的解决方法,就是我们调用这些有object类型主键的接口时,传入正确的类型即可。

RoleInfo info = CallerFactory<IRoleService>.Instance.FindByID(currentID.ToInt32());if (info != null)
{
info
=SetRoleInfo(info);
CallerFactory
<IRoleService>.Instance.Update(info, info.ID);

RefreshTreeView();
}

又或者是下面的代码:

        /// <summary>
        ///分页控件删除操作/// </summary>
        private void winGridViewPager1_OnDeleteSelected(objectsender, EventArgs e)
{
if (MessageDxUtil.ShowYesNoAndTips("您确定删除选定的记录么?") ==DialogResult.No)
{
return;
}
int[] rowSelected = this.winGridViewPager1.GridView1.GetSelectedRows();foreach (int iRow inrowSelected)
{
string ID = this.winGridViewPager1.GridView1.GetRowCellDisplayText(iRow, "ID");
CallerFactory
<IDistrictService>.Instance.Delete(ID.ToInt64());
}

BindData();
}

2、递归函数的处理

在很多时候,我们都会用到递归函数的处理,这样能够使得我们把整个列表的内容都合理的提取出来,是我们开发常见的知识点之一。

不过一般在处理LINQ的时候,它的递归函数的处理和我们普通的做法有一些差异。

例如我们如果要获取一个树形机构列表,如果我们指定了一个开始的机构节点ID,我们需要递归获取下面的所有层次的集合的时候,常规的做法如下所示。

        /// <summary>
        ///根据指定机构节点ID,获取其下面所有机构列表/// </summary>
        /// <param name="parentId">指定机构节点ID</param>
        /// <returns></returns>
        public List<OUInfo> GetAllOUsByParent(intparentId)
{
List
<OUInfo> list = new List<OUInfo>();string sql = string.Format("Select * From {0} Where Deleted <> 1 Order By PID, Name", tableName);

DataTable dt
=SqlTable(sql);string sort = string.Format("{0} {1}", GetSafeFileName(sortField), isDescending ? "DESC" : "ASC");
DataRow[] dataRows
= dt.Select(string.Format("PID = {0}", parentId), sort);for (int i = 0; i < dataRows.Length; i++)
{
string id = dataRows[i]["ID"].ToString();
list.AddRange(GetOU(id, dt));
}
returnlist;
}
private List<OUInfo> GetOU(stringid, DataTable dt)
{
List
<OUInfo> list = new List<OUInfo>();

OUInfo ouInfo
= this.FindByID(id);
list.Add(ouInfo);
string sort = string.Format("{0} {1}", GetSafeFileName(sortField), isDescending ? "DESC" : "ASC");
DataRow[] dChildRows
= dt.Select(string.Format("PID={0}", id), sort);for (int i = 0; i < dChildRows.Length; i++)
{
string childId = dChildRows[i]["ID"].ToString();
List
<OUInfo> childList =GetOU(childId, dt);
list.AddRange(childList);
}
returnlist;
}

这里面的大概思路就是把符合条件的集合全部弄到DataTable集合里面,然后再在里面进行检索,也就是递归获取里面的内容。

上面是常规的做法,可以看出代码量还是太多了,如果使用LINQ,就不需要这样了,而且也不能这样处理。

使用实体框架后,主要就是利用LINQ进行一些集合的操作,这些LINQ的操作虽然有点难度,不过学习清楚了,处理起来也是比较方便的。

在数据访问层,处理上面同等的功能,LINQ操作代码如下所示。

        /// <summary>
        ///根据指定机构节点ID,获取其下面所有机构列表/// </summary>
        /// <param name="parentId">指定机构节点ID</param>
        /// <returns></returns>
        public IList<Ou> GetAllOUsByParent(intparentId)
{
//递归获取指定PID及下面所有所有的OU var query = this.GetQueryable().Where(s => s.PID == parentId).Where(s => !s.Deleted.HasValue || s.Deleted == 0).OrderBy(s => s.PID).OrderBy(s =>s.Name);return query.ToList().Concat(query.ToList().SelectMany(t =>GetAllOUsByParent(t.ID))).ToList();
}

基本上,可以看到就是两行代码了,是不是很神奇,它们实现的功能完全一致。

不过,也不是所有的LINQ递归函数都可以做的非常简化,有些递归函数,我们还是需要使用常规的思路进行处理。

        /// <summary>
        ///获取树形结构的机构列表/// </summary>
        public IList<OuNodeInfo>GetTree()
{
IList
<OuNodeInfo> returnList = new List<OuNodeInfo>();
IList
<Ou> list = this.GetQueryable().Where(p => p.PID == -1).OrderBy(s => s.PID).OrderBy(s =>s.Name).ToList();if (list != null)
{
foreach (Ou info in list.Where(s => s.PID == -1))
{
OuNodeInfo nodeInfo
=GetNode(info);
returnList.Add(nodeInfo);
}
}
returnreturnList;
}

不过相对来说,LINQ已经给我们带来的非常大的便利了。

3、日期字段类型转换的错误处理

我们在做一些表的时候,一般情况下都会有日期类型存在,如我们的生日,创建、编辑日期等,一般我们数据库可能用的是datetime类型,如果这个日期的类型内容在下面这个区间的话:

"0001-01-01 到 9999-12-31"(公元元年 1 月 1 日到公元 9999 年 12 月 31 日)

我们可能就会得到下面的错误:

从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值

一般之所以会报错数据类型转换产生一个超出范围的值,都是因为数据的大小和范围超出要转换的目标的原因。我们先看datetime2和datetime这两个数据类型的具体区别在哪里。

官方MSDN对于datetime2的说明
:定义结合了 24 小时制时间的日期。 可将 datetime2 视作现有 datetime 类型的扩展,其数据范围更大,默认的小数精度更高,并具有可选的用户定义的精度。

这里值的注意的是datetime2的日期范围是"0001-01-01 到 9999-12-31"(公元元年 1 月 1 日到公元 9999 年 12 月 31 日)。而datetime的日期范围是:”1753 年 1 月 1 日到 9999 年 12 月 31 日“。这里的日期范围就是造成“从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值”这个错误的原因!!!

在c#中,如果实体类的属性没有赋值,一般都会取默认值,比如int类型的默认值为0,string类型默认值为null, 那DateTime的默认值呢?由于DateTime的默认值为"0001-01-01",所以entity framework在进行数据库操作的时候,在传入数据的时会自动将原本是datetime类型的数据字段转换为datetime2类型(因为0001-01-01这个时间超出了数据库中datetime的最小日期范围),然后在进行数据库操作。问题来了,虽然EF已经把要保存的数据自动转为了datetime2类型,但是数据库中表的字段还是datetime类型!所以将datetime2类型的数据添加到数据库中datetime类型的字段里去,就会报错并提示转换超出范围。

解决方法如下所示:

这个问题的解决方法:

  1. C#代码中 DateTime类型的字段在作为参数传入到数据库前记得赋值,并且的日期要大于1753年1月1日。
  2. C#代码中 将原本是DateTime类型的字段修改为DateTime?类型,由于可空类型的默认值都是为null,所以传入数据库就可以不用赋值,数据库中的datetime类型也是支持null值的。
  3. 修改数据库中表的字段类型,将datetime类型修改为datetime2类型

例如,我在实体框架里面,对用户表的日期类型字段进行初始化,这样就能保证我存储数据的时候,默认值是不会有问题的。

    /// <summary>
    ///系统用户信息,数据实体对象/// </summary>
    public classUser
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicUser()
{
this.ID= 0;//从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值//避免这个问题,可以初始化日期字段 DateTime defaultDate = Convert.ToDateTime("1900-1-1");this.Birthday =defaultDate;this.LastLoginTime =defaultDate;this.LastPasswordTime =defaultDate;this.CurrentLoginTime =defaultDate;this.EditTime =DateTime.Now;this.CreateTime =DateTime.Now;
}

有时候,虽然这样设置了,但是在界面可能给这个日期字段设置了不合理的值,也可能产生问题。那么我们对于这种情况,判断一下,如果小于某个值,我们给它一个默认值。

4、实体框架的界面处理

在界面调整这块,我们还是尽可能保持着的Enterprise Library的Winform界面样式,也就是混合型或者普通Winform的界面效果。不过这里我们是以混合式框架进行整合测试,因此实体框架的各个方面的调用处理基本上保持一致。

不过由于实体框架里面,实体类避免耦合的原因,我们引入了DTO的概念,并使用了AutoMapper组件进行了Entity与DTO的相互映射,具体介绍可以参考《
Entity Framework 实体框架的形成之旅--数据传输模型DTO和实体模型Entity的分离与联合

》。

因此我们在界面操作的都是DTO对象类型了,我们在定义的时候,为了避免更多的改动,依旧使用***Info这样的类名称作为DTO对象的名称,***代表表名对象。

在混合式框架的界面表现层,它们的数据对象的处理基本上保持和原来的代码差不多。

        /// <summary>
        ///新增状态下的数据保存/// </summary>
        /// <returns></returns>
        public override boolSaveAddNew()
{
UserInfoinfo
= tempInfo;//必须使用存在的局部变量,因为部分信息可能被附件使用 SetInfo(info);
info.Creator
=Portal.gc.UserInfo.FullName;
info.Creator_ID
=Portal.gc.UserInfo.ID.ToString();
info.CreateTime
=DateTime.Now;try{#region 新增数据 bool succeed = CallerFactory<IUserService>.Instance.Insert(info);if(succeed)
{
//可添加其他关联操作 return true;
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
return false;
}

但我们需要在WCF服务层说明他们之间的映射关系,方便进行内部的转换处理。

在实体框架界面层的查询中,我们也不在使用部分SQL的条件做法了,采用更加安全的基于DTO的LINQ表达式进行封装,最后传递给后台的也就是一个LINQ对象(非传统方式的实体LINQ,那样在分布式处理中会出错)。

如查询条件的封装处理如下所示:

        /// <summary>
        ///根据查询条件构造查询语句/// </summary> 
        privateExpressionNode GetConditionSql()
{
Expression
<Func<UserInfo, bool>> expression = p => true;if (!string.IsNullOrEmpty(this.txtHandNo.Text))
{
expression
= expression.And(x => x.HandNo.Equals(this.txtHandNo.Text));
}
if (!string.IsNullOrEmpty(this.txtName.Text))
{
expression
= expression.And(x => x.Name.Contains(this.txtName.Text));
}
.........................................
//如果是公司管理员,增加公司标识 if(Portal.gc.UserInRole(RoleInfo.CompanyAdminName))
{
expression
= expression.And(x => x.Company_ID ==Portal.gc.UserInfo.Company_ID);
}
//如果是单击节点得到的条件,则使用树列表的,否则使用查询条件的 if (treeCondition != null)
{
expression
=treeCondition;
}
//如非选定,只显示正常用户 if (!this.chkIncludeDelete.Checked)
{
expression
= expression.And(x => x.Deleted == 0);
}
returnexpression.ToExpressionNode();
}

而分页查询的处理,依旧和原来的风格差不多,只不过这里的Where条件为ExpressionNode 对象了,如代码所示、

ExpressionNode where =GetConditionSql();
PagerInfo PagerInfo
= this.winGridViewPager1.PagerInfo;
IList
<UserInfo> list = CallerFactory<IUserService>.Instance.FindWithPager(where, ref PagerInfo);this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<UserInfo>(list);this.winGridViewPager1.PrintTitle = "系统用户信息报表";

最后我们来看看整个实体框架的结构和界面的效果介绍。

界面效果如下所示:

代码结构如下所示:

架构设计的效果图如下所示:

在《
Web API应用架构设计分析(1)
》和《
Web API应用架构设计分析(2)
》中对WebAPI的架构进行了一定的剖析,在当今移动优先的口号下,传统平台都纷纷开发了属于自己的Web API平台,方便各种终端系统的接入,很多企业的需求都是以Web API优先的理念来设计整个企业应用体系的。Web API作为整个纽带的核心,在整个核心层需要考虑到统一性、稳定性、以及安全性等方面因素。本文主要介绍,Web API应用架构,在Winform整合中的角色,以及如何实现在Winform混合架构里面的整合案例。

1、Web API介绍回顾

Web API 是一种应用接口框架,它能够构建HTTP服务以支撑更广泛的客户端(包括浏览器,手机和平板电脑等移动设备)的框架,
ASP.NET Web API 是一种用于在 .NET Framework 上构建 RESTful 应用程序的理想平台。在目前发达的应用场景下,我们往往需要接入Winform客户端、APP程序、网站程序、以及目前热火朝天的微信应用等,这些数据应该可以由同一个服务提供,这个就是我们所需要构建的Web API平台。由于Web API层作为一个公共的接口层,我们就很好保证了各个界面应用层的数据一致性。

通过上面的了解,我们可以知道,所有外部的应用,其实都可以基于一个相同的Web API核心开展的,如下图所示。

在当前大平台,大应用的背景下,可以基于一个整体的平台,构建很多应用生态链,这样就可以把Web API作为核心层,可以在上面开发我们各种企业业务应用了。

2、Web API在Winform框架中的整合

在Winform界面里面,我们除了可以利用直接访问数据库方式,以及采用访问分布式WCF服务的方式接入,还可以使得它能够访问Web API的数据服务,从而构建成一个适应性更加广泛、功能更加强大的混合式开发框架模式;对于Web API,由于它提供的是一种无状态的接口访问,而且往往Web API一般为了多种客户端接入的需要,可能需要发布在公网上进行访问,因此我们需要更加注重Web API接口层的安全性。

除了直连数据库访问的传统模式,WCF分布式访问的WCF服务访问模式,还可以接入API分布式访问的Web API接口模式,他们的关系构成了一个完整的Winform应用体系,如下图所示。

混合式框架的实现细节,就是通过一个类似开关模式的配置模块,确定是采用直接访问数据库方式,还是访问WCF服务的方式,它们两者是统一到一个Facade接口门面层上,如果考虑到Web API层,基于混合式的架构,也就是在这个Facade接口门面层上增加多一个Web API的接口的封装成即可。具体整个框架的架构图如下所示。

3、Web API访问的安全性考虑

由于Web API是基于互联网的应用,因此安全性要远比在本地访问数据库的要严格的多,基于通用的做法,一般采用几道门槛来处理这些问题,一个是基于CA证书的HTTPS进行数据传输,防止数据被窃听,具体可以参考《
Web API应用支持HTTPS的经验总结
》;二是采用参数加密签名方式传递,对传递的参数,增加一个加密签名,在服务器端验证签名内容,防止被篡改;三是对一般的接口访问,都需要使用用户身份的token进行校验,只要检查通过才允许访问数据。

Web API接口的访问方式,大概可以分为几类:

1)一个是使用用户令牌,通过Web API接口进行数据访问。这种方式,可以有效识别用户的身份,为用户接口返回用户相关的数据,如包括用户信息维护、密码修改、或者用户联系人等与用户身份相关的数据。

2)一种是使用安全签名进行数据提交。这种方式提交的数据,URL连接的签名参数是经过安全一定规则的加密的,服务器收到数据后也经过同样规则的安全加密,确认数据没有被中途篡改后,再进行数据修改处理。因此我们可以为不同接入方式,如Web/APP/Winfrom等不同接入方式指定不同的加密秘钥,但是秘钥是双方约定的,并不在网络连接上传输,连接传输的一般是这个接入的AppID,服务器通过这个AppID来进行签名参数的加密对比,这种方式,类似微信后台的回调处理机制,它们就是经过这样的处理。

3)一种方式是提供公开的接口调用,不需要传入用户令牌、或者对参数进行加密签名的,这种接口一般较少,只是提供一些很常规的数据显示而已。

基于上面的考虑,我们一般需要设计Web API对象的接口的时候,需要考虑安全性的原因,也就是需要增加更多的一些字段信息了。

如可以在增删改这些接口,除了传入token信息外(标识具体用户),也还是需要传入签名信息,如下接口所示。

        /// <summary>
        ///插入指定对象到数据库中/// </summary>
        /// <param name="info">指定的对象</param>
        /// <returns>执行操作是否成功。</returns>
        public virtual CommonResult Insert(T info, string token, string signature, string timestamp, string nonce, string appid)

上面接口,除了info对象为对象创建的参数外,其他几个参数,都是为了安全性的考虑而加入的。

在接口里面,我们就需要对用户的权限和签名信息进行校验,然后在进行下一步的数据处理,如果校验权限和参数完整性不通过,则会被拦截,不执行数据库的处理了。

            //如果用户token检查不通过,则抛出MyApiException异常。//检查用户是否有权限,否则抛出MyDenyAccessException异常
            base.CheckAuthorized(AuthorizeKey.InsertKey, token, signature, timestamp, nonce, appid);

除了这些对数据修改的特殊性接口,有时候我们还需要查找等类似的,不对数据产生变化的接口,只需要传入令牌即可,如下接口所示。

        /// <summary>
        ///查询数据库,检查是否存在指定ID的对象/// </summary>
        /// <param name="id">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
[HttpGet]public virtual T FindByID(string id, stringtoken)
{
//如果用户token检查不通过,则抛出MyApiException异常。//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ViewKey, token);

T info
=baseBLL.FindByID(id);returninfo;
}

我们可以看到,上面还是会对token进行校验,不过少了很多签名所需的日期标识、随机数,完整性校验签名,应用ID等参数。

我们会根据用户的token进行解析,如果是正常的token并可以通过解析,那么获取对应用户的权限,判断是否可以进行下一步处理即可。

如果顺利通过,那么访问数据库,把所需的数据返回给调用者即可。

上面提到了用户令牌,用户令牌是一个类似实际生活的通行证,是通过用户名、密码等信息获取到的一个安全令牌,可以在多个接口进行传递的字符串,较少密码参数的传输,提高安全性。

这个用户令牌,一般由单独的接口产生,我们一般放到AuthController里面,这个控制器负责用户令牌相关的处理调用。

        /// <summary>
        ///注册用户获取访问令牌接口/// </summary>
        /// <param name="username">用户登录名称</param>
        /// <param name="password">用户密码</param>
        /// <param name="signature">加密签名字符串</param>
        /// <param name="timestamp">时间戳</param>
        /// <param name="nonce">随机数</param>
        /// <param name="appid">应用接入ID</param>
        /// <returns></returns>
        TokenResult GetAccessToken(string username, stringpassword,string signature, string timestamp, string nonce, string appid);

如下代码是具体业务模块里面,说明如何获取用于操作各种接口的token令牌的,当然,实际环境下,一般都会使用HTTPS协议获取数据了,演示代码如下所示。

                string appid = "myapi_123456";string appsecret = "mySecret_2856FB9DBE31";//使用API方式,需要在缓存里面设置特殊的信息
                var url = "http://localhost:9001/api/Auth/GetAccessToken" + GetSignatureUrl(appid, appsecret) + "&username=admin&password=";
TokenResult result
= JsonHelper<TokenResult>.ConvertJson(url);if(result == null)
{
MessageDxUtil.ShowError(
"获取授权信息出错,请检查地址是否正确!");
}

由于Web API的调用,都是一种无状态方式的调用方式,我们通过token来传递我们的用户信息,这样我们只需要验证Token就可以了。JWT的令牌生成逻辑如下所示

令牌生成后,我们需要在Web API调用处理前,对令牌进行校验,确保令牌是正确有效的。

除了令牌的规则,还有一个是加密签名的处理,加密签名需要客户端和服务器端约定相同的秘钥,一般由Web API统一分配,然后传输的时候,客户端使用应用ID即可。

加密签名在服务端(Web API端)的验证流程参考微信的接口的处理方式,处理逻辑如下所示。

1)检查timestamp 与系统时间是否相差在合理时间内,如10分钟。
2)将appSecret、timestamp、nonce三个参数进行字典序排序
3)将三个参数字符串拼接成一个字符串进行SHA1加密
4)加密后的字符串可与signature对比,若匹配则标识该次请求来源于某应用端,请求是合法的。

4、Web API基类设计分析

上面介绍了一些Web API控制器的职能,一般情况下,我们设计一个架构,还需要考虑到基类对象之间的重用关系,尽可能把接口抽象到基类层面上去,减少子类的开发代码量,降低维护成本。

基于上面的目的,参考了我的Web开发框架对于MVC控制器的设计思路

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

我们关键的核心就是设计好
BusinessController<B, T>
这个基类,里面设计了大量的通用接口,包括常规的增删改查、分页等处理接口,那么子类继承过来就可以直接拥有这些接口了,多方便啊。

5)Web API客户端(混合式Winform框架模块)的调用

上面介绍了Web API服务端平台的架构设计思路,通过上面的整合,我们减轻了开发重复功能的增删改查等基础功能的控制器代码,把这些接口抽象到接口里面即可实现。

但是我们具体应该如何遵循统一接口层Facade层的约定,然后统一调用WebAPI层的接口,做到悄无声息的从不同的数据源里面获取数据,展示在客户端里面呢。

上面我们分析到,整个混合式Winform框架模块里面,设计方面考虑了数据的获取方面:包含了直接从数据库获取,从WCF服务获取,以及Web API层的数据获取三部分内容,当然还可以有更多的数据接入模式(如WebService等),设计效果如下所示。

所有的数据接入,我们在Facade层都统一到接口里面,客户端的调用也统一到了CallerFactory<T>这个泛型工厂里面,我们根据配置的不同,从不同的模块里面加载,从而实现不同数据源的动态获取了。

如下逻辑就是CallerFactory<T>泛型工厂类的加载逻辑,如下所示:

为了实现简化客户端调用的封装,我们一般也把常规的通用操作封装一下,如下是我原先混合框架里面的设计思路,里面的封装都是通过***Caller的类来进行数据的访问的,这些类统一实现一定关系的集成封装。

为了简化说明调用接口的处理,这里把上面的关系进行了简化,并加入了Web API的调用封装类的处理,几种访问模式下的调用端封装继承关系,如下设计图所示。

最底层的几个DictDataCaller分别是不同访问方式下的接口调用封装类,对于Web API来说,它的访问代码就是如下所示。

        public override bool Delete(stringkey)
{
var action = "Delete";string url = GetPostUrlWithToken(action) +string.Format("&id={0}", key);

CommonResult result
= JsonHelper<CommonResult>.ConvertJson(url);returnresult.Success;
}
public List<DictDataInfo> FindByTypeID(stringdictTypeId)
{
var action = "FindByTypeID";string url = GetTokenUrl(action) + string.Format("&dictTypeId={0}", dictTypeId);

List
<DictDataInfo> result = JsonHelper<List<DictDataInfo>>.ConvertJson(url);returnresult;
}

第一个Delete函数是基类提供的,这里进行了重写,一般情况下,不需要处理就具备增删改分页等基础接口的调用封装了。

由于所有的实现类都实现继承了统一的Facade层的接口,那么统一调用也就是自然而然的事情了。所以在Winform界面里面,所有的调用都是使用CallerFactory<T>进行了统一的处理,数据访问的不同不影响接口的处理, 三种方式的数据调用,统一都是下面的代码进行处理。

DictDataInfo info = CallerFactory<IDictDataService>.Instance.FindByID(ID);if (info != null)
{
SetInfo(info);
try{bool succeed = CallerFactory<IDictDataService>.Instance.Update(info, info.ID.ToString());returnsucceed;
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}

系列文章如下所示:

Web API应用架构在Winform混合框架中的应用(1)

Web API应用架构在Winform混合框架中的应用(2)--自定义异常结果的处理

Web API接口设计经验总结

Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解

Web API应用架构在Winform混合框架中的应用(4)--利用代码生成工具快速开发整套应用

Web API应用架构在Winform混合框架中的应用(5)--系统级别字典和公司级别字典并存的处理方式