2023年2月

数据的导入导出,在很多系统里面都比较常见,这个导入导出的操作,在Winform里面比较容易实现,我曾经在之前的一篇文章《
Winform开发框架之通用数据导入导出操作
》介绍了在Winform里面的通用导入导出模块的设计和开发过程,但在Web上我们应该如何实现呢?本文主要介绍利用MVC4+EasyUI的特点,并结合文件上传控件Uploadify 的使用,实现文件上传后马上进行处理并显示,然后确认后把数据写入数据库的过程。

我们知道,Web上对Excel的处理和Winform的有所差异,如果是在Web上处理,我们需要把Excel文档上传到服务器上,然后读取文件进行显示,所以第一步是实现文件的上传操作,关于文件上传控件,具体可以参考我的文章《
基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用
》。

1、导入数据的界面效果展示

在Winform里面,我们处理Excel数据导入的界面如下所示。

在Web上的主界面如下所示。

导入界面如下所示。

2、Web数据导入的处理逻辑和代码

为了实现Web上的数据导入导出操作,我们需要增加两个按钮,一个是导入按钮,一个是导出按钮。

 <ahref="javascript:void(0)"class="easyui-linkbutton"id="btnImport"iconcls="icon-excel"onclick="ShowImport()">导入</a>
 <ahref="javascript:void(0)"class="easyui-linkbutton"id="btnExport"iconcls="icon-excel"onclick="ShowExport()">导出</a>

导入的JS处理代码如下所示。

        //显示导入界面
        functionShowImport() {
$.showWindow({
title:
'客户联系人-Excel数据导入',
useiframe:
true,
width:
1024,
height:
700,
content:
'url:/Contact/Import',
buttons: [{
text:
'取消',
iconCls:
'icon-cancel',
handler:
function(win) {
win.close();
}
}]
});
}

上面主要就是弹出一个窗口(上面的导入数据窗口),用来方便客户选择Excel文件并保存数据或者下载导入模板等操作的。

然后在Import.cshtml的视图代码里面,我们需要初始化Datagrid和相关的界面元素,初始化DataGrid的代码如下所示。

        //实现对DataGird控件的绑定操作
        functionInitGrid() {var guid = $("#AttachGUID").val();
$(
'#grid').datagrid({ //定位到Table标签,Table标签的ID是grid url: '/Contact/GetExcelData?guid=' + guid, //指向后台的Action来获取当前用户的信息的Json格式的数据 title: '客户联系人-Excel数据导入',
iconCls:
'icon-view',
height:
400,
width:
function () { return document.body.clientWidth * 0.9 },//自动宽度 ..................

上面红色部分的内容,就是我们在文件顺利上传到服务器上的时候,根据一个guid的参数初始化DataGrid的列表数据。

下面是附件上传控件uploadify的初始化脚本代码,其中红色部分注意一下,我们需要上传的是一个文件,并且不允许多选,限定上传文件的类型为xls。

文件上传完成后,首先调用CheckExcelColumns控制器函数来检查是否匹配导入模板的字段,如果匹配通过,加载Excel并展示数据到Datagrid里面,否则提示用户按模板格式录入数据。

    <script type="text/javascript">$(function () {//添加界面的附件管理
            $('#file_upload').uploadify({'swf': '/Content/JQueryTools/uploadify/uploadify.swf',  //FLash文件路径
                'buttonText': '浏  览',                                 //按钮文本
                'uploader': '/FileUpload/Upload',                       //处理ASHX页面
                '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) {    //所有队列完成后事件
                    var guid = $("#AttachGUID").val();
ViewUpFiles(guid,
"div_files");//提示用户Excel格式是否正常,如果正常加载数据 $.ajax({
url:
'/Contact/CheckExcelColumns?guid=' +guid,
type:
'get',
dataType:
'json',
success: function (data) {
if(data.Success) {
InitGrid();
//重新刷新表格数据 $.messager.alert("提示", "文件已上传,数据加载完毕!");
}
else{
$.messager.alert(
"提示", "上传的Excel文件检查不通过。请根据页面右上角的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里面包含了完整的字段即可。

        /// <summary>
        ///检查Excel文件的字段是否包含了必须的字段/// </summary>
        /// <param name="guid">附件的GUID</param>
        /// <returns></returns>
        public ActionResult CheckExcelColumns(stringguid)
{
CommonResult result
= newCommonResult();try{
DataTable dt
=ConvertExcelFileToTable(guid);if (dt != null)
{
//检查列表是否包含必须的字段 result.Success =DataTableHelper.ContainAllColumns(dt, columnString);
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
result.ErrorMessage
=ex.Message;
}
returnToJsonContent(result);
}

而在InitGrid的初始化中的这个GetExcelData的控制器方法如下所示。
主要的逻辑就是获取到Excel,并把Excel里面的数据转换为DataTable,最后初始化为实体类列表,并返回给调用页面就可以了。

       /// <summary>
        ///获取服务器上的Excel文件,并把它转换为实体列表返回给客户端/// </summary>
        /// <param name="guid">附件的GUID</param>
        /// <returns></returns>
        public ActionResult GetExcelData(stringguid)
{
if (string.IsNullOrEmpty(guid))
{
return null;
}

List
<ContactInfo> list = new List<ContactInfo>();
DataTable table
=ConvertExcelFileToTable(guid);if (table != null)
{
#region 数据转换 int i = 1;foreach (DataRow dr intable.Rows)
{
string customerName = dr["客户名称"].ToString();if (string.IsNullOrEmpty(customerName))
{
continue;//客户名称为空,记录跳过 }

CustomerInfo customerInfo
= BLLFactory<Customer>.Instance.FindByName(customerName);if (customerInfo == null)
{
continue;//客户名称不存在,记录跳过 }ContactInfo info= newContactInfo();
info.Customer_ID
= customerInfo.ID;//客户ID info.HandNo = dr["编号"].ToString();
info.Name
= dr["姓名"].ToString();..............................//增加一个特殊字段的转义 info.Data1 = BLLFactory<Customer>.Instance.GetCustomerName(info.Customer_ID);

list.Add(info);
}
#endregion} var result = new { total = list.Count, rows =list };returnJsonDate(result);
}

3、Web上数据的导出操作

刚才介绍了数据的导入操作,数据的导出操作相对简单一些,它的JS函数操作如下所示。

        //导出Excel数据
        varexportCondition;functionShowExport() {var url = "/Contact/Export";
$.ajax({
type:
"POST",
url: url,
data: exportCondition,
success:
function(filePath) {var downUrl = '/FileUpload/DownloadFile?file=' +filePath;
window.location
=downUrl;
}
});
}

虽然数据的导出比较简单一点,但是由于我们需要使用POST方式对数据条件进行提交,因此不像普通的方式下载文件Window.Open(url)就可以实现文件下载了。如果POST方式提交了参数,那么返回的数据即使是文件流,也无法进行有效的下载。

从上面的脚本我们可以看到,里面的exportCondition就是我们需要提交到服务器的条件,服务器根据这个条件进行检索数据,并返回一个Excel文件就可以了。

由于使用ajax这种POST方式无法直接下载文件流,因此,我们需要先根据条件,在服务器上生成文件,返回一个文件路径,再次通过DownloadFile方法进行文件的下载才可以。

因此这个传递的条件也是很重要的,在查询操作的时候,我们可以把对应的条件传递给它。

        //绑定搜索按钮的的点击事件
        functionBindSearchEvent() {//按条件进行查询数据,首先我们得到数据的值
            $("#btnSearch").click(function() {//得到用户输入的参数
                //取值有几种方式:$("#id").combobox('getValue'), $("#id").datebox('getValue'), $("#id").val(),combotree('getValue')
                //字段增加WHC_前缀字符,避免传递如URL这样的Request关键字冲突
                var queryData ={
WHC_Name: $(
"#txtName").val(),
WHC_OfficePhone: $(
"#txtOfficePhone").val(),
WHC_Mobile: $(
"#txtMobile").val(),
WHC_Address: $(
"#txtAddress").val(),
WHC_Email: $(
"#txtEmail").val(),
WHC_Note: $(
"#txtNote").val()
}
//将值传递给DataGrid InitGrid(queryData);//传递给导出操作 exportCondition = queryData;return false;
});
}

在我们选定某个树的节点的时候,我们也可以传递自定义的条件给它。

        //根据消息分组加载指定列表
        functionloadByGroupTree(node) {//赋值给特殊字段,公司和部门查询的时候选择其中一个
            var queryParams = $('#grid').datagrid('options').queryParams;var condition = "{ id: \"" + node.id +"\", groupname:\"" + node.text +"\", userid:\"" + @Session["UserId"] + "\" }";
queryParams.CustomedCondition
= condition;//提供给datagrid的条件 exportCondition = { CustomedCondition: condition };//提供给导出的条件 $("#grid").datagrid("reload");
$(
'#grid').datagrid('uncheckAll');
}

后台的Export控制器方法主要的逻辑如下所示。

最终是返回一个生成好的文件地址。

最后给一个方法直接下载文件就可以了。

        /// <summary>
        ///根据路径下载文件,主要用于生成的文件的下载/// </summary>
        /// <param name="filePath">文件路径</param>
        /// <returns></returns>
        public ActionResult DownloadFile(stringfile)
{
string realPath =Server.MapPath(file);string saveFileName =FileUtil.GetFileName(realPath);

Response.WriteFile(realPath);
Response.Charset
= "GB2312";
Response.ContentEncoding
= Encoding.GetEncoding("GB2312");
Response.ContentType
= "application/ms-excel/msword";
Response.AppendHeader(
"Content-Disposition", "attachment;filename=" +HttpUtility.UrlEncode(saveFileName));
Response.Flush();
Response.End();
return new FileStreamResult(Response.OutputStream, "application/ms-excel/msword");
}

导出的Excel界面效果如下所示。

由于篇幅的原因,这个导入导出的操作就介绍到这里,希望有问题大家共同探讨。

基于MVC4+EasyUI的Web开发框架的系列文章:

基于MVC4+EasyUI的Web开发框架形成之旅--总体介绍


基于MVC4+EasyUI的Web开发框架形成之旅--MVC控制器的设计

基于MVC4+EasyUI的Web开发框架形成之旅--界面控件的使用

基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用

基于MVC4+EasyUI的Web开发框架形成之旅--框架总体界面介绍

基于MVC4+EasyUI的Web开发框架形成之旅--基类控制器CRUD的操作

基于MVC4+EasyUI的Web开发框架形成之旅--权限控制

基于MVC4+EasyUI的Web开发框架经验总结(1)-利用jQuery Tags Input 插件显示选择记录

基于MVC4+EasyUI的Web开发框架经验总结(2)- 使用EasyUI的树控件构建Web界面

基于MVC4+EasyUI的Web开发框架经验总结(3)- 使用Json实体类构建菜单数据

基于MVC4+EasyUI的Web开发框架经验总结(4)--使用图表控件Highcharts

基于MVC4+EasyUI的Web开发框架经验总结(5)--使用HTML编辑控件CKEditor和CKFinder

基于MVC4+EasyUI的Web开发框架经验总结(6)--在页面中应用下拉列表的处理

基于MVC4+EasyUI的Web开发框架经验总结(7)--实现省份、城市、行政区三者联动

基于MVC4+EasyUI的Web开发框架经验总结(8)--实现Office文档的预览

基于MVC4+EasyUI的Web开发框架经验总结(9)--在Datagrid里面实现外键字段的转义操作

基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出

基于MVC4+EasyUI的Web开发框架经验总结(11)--使用Bundles处理简化页面代码

基于MVC4+EasyUI的Web开发框架经验总结(12)--利用Jquery处理数据交互的几种方式

基于MVC4+EasyUI的Web开发框架经验总结(13)--DataGrid控件实现自动适应宽带高度

基于MVC4+EasyUI的Web开发框架经验总结(14)--自动生成图标样式文件和图标的选择操作

在很多系统,我们都知道,Excel数据的导入导出操作是必不可少的一个功能,这种功能能够给使用者和外部进行数据交换,也能批量迅速的录入数据到系统中;但在一些系统中,为了方便,可能把很多个基础表或者相关的数据综合到一个Excel表格文件里面,然后希望通过接口进行导入,这种需求处理就显得比较复杂一点了。本文探讨在我的客户关系管理系统中,对于单个Excel表格中,集合了客户基础数据及相关数据的导入和导出操作的处理。

1、导入导出的需求分析

本随笔主要介绍如何在系统中,导入单一文件中的数据到系统中,这个文件包含了基础数据和相关数据的导入和导出操作,一般来说这样的操作对于导入数据已经足够简便了,但是,有时候数据很多的情况下,我们可能需要每次选定文件也是一个麻烦的事情。因此指定目录进行批量数据的导入操作也是一个好的需求,可以进一步简化用户的数据导入操作。

下面我们就来介绍,导入、批量导入和导出的三个重要的操作,如图所示。

导入的数据,是一个Excel,它要求包含几个不同表的数据,导入操作一次性完成数据的导入,Excel文件的格式如下所示。

2、数据导入操作的界面设计及处理

我们知道,要一次性导入几个表的数据,需要先读取Excel获取各个Sheet(工作表)的数据,然后把它转换为DataTable的数据对象,这样我们就可以根据它的字段赋值给对应的实体类,然后调用业务逻辑处理将数据写入数据库即可。

为了直观的给使用者查看将要导入的数据,我们把需要导入到数据库的数据,展现在界面上,供客户确认,如果没有问题,就可以进行导入操作。由于我们需要操作多个数据表,因此有效读取Excel里面的Sheet就是第一步工作。

查看Excel数据的操作代码如下所示,主要的逻辑就是调用Apose.Cell的封装类进行处理

AsposeExcelTools.ExcelFileToDataSet(this.txtFilePath.Text, out myDs, out error);

把Excel文件里面多个Sheet的数据转换为DataSet,然后每个进行依次的处理,展示代码如下所示。

        private voidViewData()
{
if (this.txtFilePath.Text == "")
{
MessageDxUtil.ShowTips(
"请选择指定的Excel文件");return;
}
try{

myDs.Tables.Clear();
myDs.Clear();
this.gridCustomer.DataSource = null;string error = "";
AsposeExcelTools.ExcelFileToDataSet(
this.txtFilePath.Text, out myDs, outerror);this.gridCustomer.DataSource = myDs.Tables[0];this.gridView1.PopulateColumns();this.gridFollow.DataSource = myDs.Tables[1];this.gridView2.PopulateColumns();this.gridContact.DataSource = myDs.Tables[2];this.gridView3.PopulateColumns();this.gridSupplier.DataSource = myDs.Tables[3];this.gridView4.PopulateColumns();
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}

由于导入过程中需要耗费一定的时间,因此我们可以通过后台线程结合进度条的方式提示用户,界面设计效果如下效果所示。

刚才说到,保存数据,我们把它放到后台线程BackgroudWorker进行处理即可,处理代码如下所示。

        private void btnSaveData_Click(objectsender, EventArgs e)
{
if(worker.IsBusy)return;if (this.txtFilePath.Text == "")
{
MessageDxUtil.ShowTips(
"请选择指定的Excel文件");return;
}
if (MessageDxUtil.ShowYesNoAndWarning("该操作将把数据导入到系统数据库中,您确定是否继续?") ==DialogResult.Yes)
{
if (myDs != null && myDs.Tables[0].Rows.Count > 0)
{
this.progressBar1.Visible = true;
worker.RunWorkerAsync();
}
}
}

后台线程操作的主要业务逻辑代码如下所示,就是依次把不同的数据进行解析,并保存即可。

        void worker_DoWork(objectsender, DoWorkEventArgs e)
{
if (myDs != null && myDs.Tables.Count >= 4 && myDs.Tables[0].Rows.Count > 0)
{
try{
ImportCustomerDataHelper helper
= newImportCustomerDataHelper();
helper.LoginUserInfo
=LoginUserInfo;//写入或更新客户信息 string customerID = helper.UpdateCustomer(myDs.Tables[0]);if (!string.IsNullOrEmpty(customerID))
{
helper.AddFollow(customerID, myDs.Tables[
1], worker);
helper.AddContact(customerID, myDs.Tables[
2], worker);
helper.AddSupplier(customerID, myDs.Tables[
3], worker);

e.Result
= "操作完成";
}
else{
e.Result
= "操作失败";
}
}
catch(Exception ex)
{
e.Result
=ex.Message;
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.ToString());
}
}
else{
e.Result
= "请检查数据记录是否存在";
}
}

3、数据批量导入操作

虽然上面可以一次性导入客户和其相关数据,但是还是一次性导入一个Excel,如果对于客户数据比较多的情况下,一次次导入操作也是很繁琐的事情,因此客户提出,需要按照目录把所有相关的Excel数据一次性导入,这种导入有个问题就是我们不能再中途干预导入操作,因此为了数据的安全性,我提供一个界面让客户选择目录,然后把目录里面的Excel文件列出来,然后在让客户确认是否进一步导入。

上面操作的实现代码我逐一介绍,首先第一步是需要递归列出目录下面的Excel文件,然后显示出来供用户确认导入的清单。

        private void btnSelectPath_Click(objectsender, EventArgs e)
{
string mydocDir =Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);string selectPath =FileDialogHelper.OpenDir(mydocDir);if (!string.IsNullOrEmpty(selectPath))
{
//清空就记录 this.lstPath.Items.Clear();string[] fileArray = Directory.GetFiles(selectPath, "*.xls", SearchOption.AllDirectories);if (fileArray != null && fileArray.Length > 0)
{
foreach (string file infileArray)
{
string fileName =Path.GetFileName(file);this.lstPath.Items.Add(newCListItem(fileName, file));
}
}
}
}

当用户确认操作的时候,提示客户确认是否进行,确认后将统一批量导入列表里面的文件,这个地方也是为了方便,使用后台线程进行数据的导出操作,并在过程中提供进度条的指示。

        private void btnConfirm_Click(objectsender, EventArgs e)
{
if(worker.IsBusy)return;if (this.lstPath.Items.Count > 0)
{
if (MessageDxUtil.ShowYesNoAndTips("您确认导入列表的Excel文件吗?") ==System.Windows.Forms.DialogResult.Yes)
{
List
<string> fileList = new List<string>();foreach (object item in this.lstPath.Items)
{
CListItem fileItem
= item asCListItem;if (fileItem != null)
{
fileList.Add(fileItem.Value);
}
}
this.progressBar1.Visible = true;
worker.RunWorkerAsync(fileList);
}
}
}

这个后台线程的处理逻辑和单个文件导入的操作差不多,只不过这里需要增加一个文件列表的遍历处理而已,具体代码如下所示。

        void worker_DoWork(objectsender, DoWorkEventArgs e)
{
List
<string> fileList = e.Argument as List<string>;if (fileList == null || fileList.Count == 0) return;bool hasError = false;
ImportCustomerDataHelper helper
= newImportCustomerDataHelper();
helper.LoginUserInfo
=LoginUserInfo;foreach (string file infileList)
{
DataSet myDs
= newDataSet();string error = "";
AsposeExcelTools.ExcelFileToDataSet(file,
out myDs, outerror);if (myDs != null && myDs.Tables.Count >= 4 && myDs.Tables[0].Rows.Count > 0)
{
try{//写入或更新客户信息 string customerID = helper.UpdateCustomer(myDs.Tables[0]);if (!string.IsNullOrEmpty(customerID))
{
helper.AddFollow(customerID, myDs.Tables[
1], worker);
helper.AddContact(customerID, myDs.Tables[2], worker);
helper.AddSupplier(customerID, myDs.Tables[3
], worker);
}
}
catch(Exception ex)
{
hasError
= true;
LogTextHelper.Error(ex);
}
}
}
string msg = "操作完成";if(hasError)
{
msg
+= ",导入出现错误。具体可以查看log.txt日志记录。";
}
e.Result
=msg;
}

和上面的单个文件导入一样,我们这里使用了一个封装类ImportCustomerDataHelper,用来对数据进行转换实体类,然后保存到数据库的操作过程,下面我们来简单看看里面的处理代码

    /// <summary>
    ///客户数据的批量导入和普通导入的操作逻辑代码/// </summary>
    public classImportCustomerDataHelper
{
/// <summary> ///登陆用户信息/// </summary> public LoginUserInfo LoginUserInfo { get; set; }/// <summary> ///写入或更新客户数据,如果成功更新返回ID值/// </summary> /// <param name="dataTable">客户数据表</param> /// <returns></returns> public stringUpdateCustomer(DataTable dataTable)
{
bool success = false;bool converted = false;
DateTime dtDefault
= Convert.ToDateTime("1900-01-01");
DateTime dt;
string result = "";

DataRow dr
= dataTable.Rows[0];if (dr != null)
{
string customerName = dr["客户名称"].ToString();
CustomerInfo info
= CallerFactory<ICustomerService>.Instance.FindByName(customerName);bool isNew = false;if (info == null)
{
info
= newCustomerInfo();
isNew
= true;
}

info.Name
=customerName;
info.HandNo
= dr["客户编号"].ToString();
info.SimpleName
= dr["客户简称"].ToString();..........................info.IsPublic= dr["公开与否"].ToString().ToBoolean();
info.Satisfaction
= dr["客户满意度"].ToString().ToInt32();info.TransactionCount= dr["交易次数"].ToString().ToInt32();
info.TransactionTotal
= dr["交易金额"].ToString().ToDecimal();info.Creator= dr["客户所属人员"].ToString();
converted
= DateTime.TryParse(dr["创建时间"].ToString(), outdt);if (converted && dt >dtDefault)
{
info.CreateTime
=dt;
}
info.Editor
=LoginUserInfo.ID.ToString();
info.EditTime
=DateTime.Now; if(isNew)
{
info.Dept_ID
=LoginUserInfo.DeptId;
info.Company_ID
=LoginUserInfo.CompanyId;
success
= CallerFactory<ICustomerService>.Instance.Insert(info);
}
else{
success
= CallerFactory<ICustomerService>.Instance.Update(info, info.ID);
}
if(success)
{
result
=info.ID;
}
}
returnresult;
}

...........................

4、数据的导出操作

导出操作,我们根据用户的选择,可以一次性导出多个Excel文件,每个Excel文件包含客户的基础信息,也包含相关数据,它们的格式和导入的格式保持一致即可,这样方便数据的交换处理。

导出操作,我们需要把客户的选择信息转换为需要导出的对象列表数据,然后绑定到Excel里面即可,因此我们的Excel里面,可以通过自定义模板,指定列的数据属性就可以绑定好数据了。

获取选择的客户信息的代码如下所示。

                List<CustomerInfo> list = new List<CustomerInfo>();foreach (int iRow inrowSelected)
{
string ID = this.winGridViewPager1.GridView1.GetRowCellDisplayText(iRow, "ID");
CustomerInfo info
= CallerFactory<ICustomerService>.Instance.FindByID(ID);if (info != null)
{
list.Add(info);
}
}

前面介绍了,我们将使用自定义模板,在模板文件里面的对应字段下面,绑定一个参数属性就可以了,通过Aspose.Cell的操作处理,我们就很方便把数据导出到Excel里面了,而里面的字段还可以很方便实现自由的裁剪操作。

自定义模板文件效果如下所示。

导出客户以及相关信息的主要核心代码如下所示。

                            #region 导出操作
                            //依次每个客户数据导出一个文件
                            string ownerUserName = CallerFactory<IUserService>.Instance.GetFullNameByID(customerInfo.Creator.ToInt32());string filePath =Path.Combine(selectPath, ownerUserName);
DirectoryUtil.AssertDirExist(filePath);

Dictionary
<string, object> dict = new Dictionary<string, object>();
dict.Add(
"Customer", new List<CustomerInfo>() { customerInfo });//需要构造一个列表绑定 List<FollowInfo> followList = CallerFactory<IFollowService>.Instance.Find(string.Format("Customer_ID ='{0}'", customerInfo.ID));
dict.Add(
"Follow", followList);

List
<ContactInfo> contactList = CallerFactory<IContactService>.Instance.FindByCustomer(customerInfo.ID);
dict.Add(
"Contact", contactList);

PagerInfo pagerInfo
= null;
List
<SupplierInfo> supplierList = CallerFactory<ISupplierService>.Instance.FindByCustomer(customerInfo.ID, "", refpagerInfo);
dict.Add(
"Supplier", supplierList);string templateFile = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "客户综合资料-导出模板.xls");if (!File.Exists(templateFile))
{
throw new ArgumentException(templateFile, string.Format("{0} 文件不存在,", Path.GetFileName(templateFile)));
}
string saveFileName = string.Format("{0}.xls", customerInfo.Name);string saveFilePath =Path.Combine(filePath, saveFileName);

WorkbookDesigner designer
= new WorkbookDesigner();
designer.Workbook = new Workbook(templateFile);
foreach (string key in
dict.Keys)
{
designer.SetDataSource(key, dict[key]);
}
designer.Process();
designer.Workbook.Save(saveFilePath, SaveFormat.Excel97To2003);
#endregion

这样利用Aspose.Cell的处理操作,通过绑定相关的数据对象,我们就很容易实现数据导出到符合我们预期格式的Excel里面去了,这样操作高效、代码干净,Excel格式也非常符合我们的要求。

以上就是在客户关系管理系统里面碰到特殊的数据导入导出需求的介绍和实现,希望大家相互交流,共同把软件开发过程中,数据导入导出操作的使用体验做到最好,更符合我们客户使用的习惯和需求。

关于数据权限的控制,可能我们在做很多大型一点的系统都会碰到过,可能每个人设计和解决问题的思路都有所不同,本文介绍我自己框架里面的解决思路。从上一篇《
如何在应用系统中实现数据权限的控制功能
》里面我们可能对权限控制和数据权限的控制有了一个初步的了解,本文接着进一步介绍在应用系统中,如何集成数据权限的控制功能。

1、数据权限实现思路分析

为了实现数据权限的控制,我们需要在通用的权限系统里面保存好对应角色具有哪些组织机构的数据权限,然后在应用系统中调用API进行过滤数据处理即可。

为了实现以上的功能需求,我们需要在权限系统里面,角色控制哪里增加一个数据权限的数据存储。

实际的应用系统,当用户登陆成功后,我们获取并记录好其可以管理的公司或者部门,如果是主管的角色,可能有多个公司的数据可以管理,那么可以在程序的顶部,让用户选择管理那个公司的数据即可,如果切换公司,那么刷新现有的界面数据显示就可以了。

在用户成功登陆后,我们可以记录用户的相关权限控制信息,如他所能控制数据的公司或者部门,把它记录下来。

                        Portal.gc.CompanyList = BLLFactory<RoleData>.Instance.GetBelongCompanysByUser(info.ID);
List
<int> deptList = BLLFactory<RoleData>.Instance.GetBelongDeptsByUser(info.ID);
Portal.gc.DeptList
= deptList;

然后存储用户默认的公司ID,并根据用户是否为管理员(超级管理员、公司管理员),然后构造一个通用的过滤条件,放到全局缓存里面,方便各个模块使用,如下代码所示。

                        //设置选定的公司ID(默认为用户所在公司的ID)
                        Cache.Instance["SelectedCompanyID"] =info.Company_ID;//设置过滤条件给界面基类使用
                        string filterCondition = string.Format("Company_ID = '{0}'", info.Company_ID);if (!Portal.gc.IsAdmin)
{
if (deptList.Count > 0)
{
filterCondition
+= string.Format("AND Dept_ID IN ({0})", string.Join(",", deptList));
}
else{
filterCondition
+= string.Format("AND Creator = '{0}'", info.ID);
}
}
Cache.Instance[
"DataFilterCondition"] = filterCondition;

在主界面的时候,我们可以根据用户所能管理的公司数据,在顶部初始化公司列表,方便切换选择,以下是初始化的代码。

            //添加受管理的公司机构//判断如果用户管理的公司数据多于一个,那么就显示选择单位列表,并绑定公司数据
            if (Portal.gc.CompanyList.Count > 1)
{
this.repositoryCompanyItem.Items.Clear();foreach (int company inPortal.gc.CompanyList)
{
OUInfo companyInfo
= BLLFactory<OU>.Instance.FindByID(company);if (companyInfo != null)
{
this.repositoryCompanyItem.Items.Add(newCListItem(companyInfo.Name, companyInfo.ID.ToString()));
}
}
//多于一个显示公司下拉列表 this.barCompanyItem.Visibility =DevExpress.XtraBars.BarItemVisibility.Always;
}
else{//只有一个公司时候,屏蔽公司选择列表 this.barCompanyItem.Visibility =DevExpress.XtraBars.BarItemVisibility.Never;
}

如果多于一个公司,那么正常的需求是可以切换公司来查看其它公司的数据的,要实现这个功能,那么就需要修改登陆的那个全局的过滤条件:Cache.Instance["DataFilterCondition"]了。

我们来看看代码的实现,其主要的逻辑就是获取用户选择的公司ID,然后根据公司、部门信息,重新构建一个全局的过滤条件,并重新缓存到对应的键值里面去,供后面的窗体实现数据的过滤更新。

                CListItem item = this.barCompanyItem.EditValue asCListItem;if (item != null)
{
//设置选定的公司ID Cache.Instance["SelectedCompanyID"] =item.Value;
SetSelectedCompanyName();
//设置过滤条件给界面基类使用 string filterCondition = string.Format("Company_ID = '{0}'", item.Value);if (!Portal.gc.IsAdmin)
{
if (Portal.gc.DeptList.Count > 0)
{
filterCondition
+= string.Format("AND Dept_ID IN ({0})", string.Join(",", Portal.gc.DeptList));
}
else{
filterCondition
+= string.Format("AND Creator = '{0}'", Portal.gc.UserInfo.ID);
}
}
Cache.Instance[
"DataFilterCondition"] = filterCondition;

如果需要对已有的窗体实现数据更新,那么遍历窗体,并统一实现数据刷新即可。

                    //遍历全部窗口,更新
                    foreach (WHC.Framework.BaseUI.BaseDock form in this.MdiChildren)
{
form.SelectedCompanyID
=item.Value;
form.DataFilterCondition
=filterCondition;
form.FormOnLoad();
}
string message = string.Format("您已经切换数据显示:{0}", item.Text);
MessageDxUtil.ShowTips(message);

2、窗体数据过滤的实现

从上面的步骤代码,我们可以看到如何构建一个全局的过滤条件,但是我们获取数据的时候,如何才能实现数据权限的控制,让用户所能看到的数据在可控的范围内呢?

我们知道,一般窗体数据列表的绑定操作类似如下代码所示

        /// <summary>
        ///绑定列表数据/// </summary>
        private voidBindData()
{
//entity this.winGridViewPager1.DisplayColumns =displayColumns;this.winGridViewPager1.ColumnNameAlias = CallerFactory<ICustomerService>.Instance.GetColumnNameAlias();//字段列显示名称转义 string where =GetConditionSql();
PagerInfo pagerInfo
= this.winGridViewPager1.PagerInfo;
List
<CustomerInfo> list = CallerFactory<ICustomerService>.Instance.FindWithPager(where, refpagerInfo);this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<CustomerInfo>(list);this.winGridViewPager1.PrintTitle = "客户信息列表";
}

所以主要的数据控制,就在函数GetConditionSql()里面了,那么这个里面,我们如何整合前面的过滤条件呢?

下面是一个案例代码。

        /// <summary>
        ///根据查询条件构造查询语句/// </summary> 
        private stringGetConditionSql()
{
//如果存在高级查询对象信息,则使用高级查询条件,否则使用主表条件查询 SearchCondition condition =advanceCondition;if (condition == null)
{
condition
= newSearchCondition();if(customGridLookUpEdit1.EditValue != null)
{
condition.AddCondition(
"ID", customGridLookUpEdit1.EditValue.ToString(), SqlOperator.Equal);
}
condition.AddCondition(
"Deleted", 0, SqlOperator.Equal);//不显示删除的 }string where = condition.BuildConditionSql().Replace("Where", "");//如果是单击节点得到的条件,则使用树列表的,否则使用查询条件的 if (!string.IsNullOrEmpty(treeConditionSql))
{
where = treeConditionSql + "AND Deleted = 0";//不显示删除的 }//数据权限的过滤:过滤规则,如果指定公司,以公司过滤,如果进一步指定部门,以公司+部门进行过滤;否则以个人的数据展示//如果过滤条件不为空,那么需要进行过滤
if (!string.IsNullOrEmpty(this.DataFilterCondition))
{
where += string.Format(" AND {0}", this.DataFilterCondition);
}
return where;
}

我们主要关注下上面红色部分即可,因为我们已经加上了标准的过滤条件了,这样我们就可以看到自己管理的数据了。

为了实现统一的数据控制,我们要求整个业务表的设计,需要引入下面几个标准的字段,这样就能很好使用过滤条件进行数据的过滤了。

前面也介绍到了,窗体可以统一刷新,其奥秘就是它们遵循统一的一个数据加载接口,我们初始化窗体数据的函数代码如下所示。

        /// <summary>
        ///编写初始化窗体的实现,可以用于刷新/// </summary>
        public override voidFormOnLoad()
{
InitDictItem();

BindData();
InitCustomerPage();
}

所以它们就能够统一调用FormOnLoad来统一刷新数据,就是这个道理。

                    //遍历全部窗口,更新
                    foreach (WHC.Framework.BaseUI.BaseDock form in this.MdiChildren)
{
form.SelectedCompanyID
=item.Value;
form.DataFilterCondition
=filterCondition;
form.FormOnLoad();
}

以上就是我对数据权限控制的一些心得和实现思路,希望大家能够体会其中的思路,并批判性的提出意见和建议。

在很多项目中,可能会有要求对一些数据的阅读状态进行记录,用户阅读过或者未阅读过,都做不同的标识,方便了解数据的状态。如在我的客户关系管理系统中,对于客户的状态进行跟踪,如果最近联系时间超过配置天数的,那么特别显示出来。类似的应用场景,还有很多地方应用到,如对于通知公告、流程记录、内部信息等状态查看都可能是这样的类型。那么如何解决这些通用的需求呢,是需要每个都设置一个表来记录这些状态吗?

1、应用需求场景

前面说了,我们可能在一些数据上需要记录不同用户的阅读状态,如下面是我客户关系管理系统里面,对于最近没有联系的客户列表,其中对他们的查看状态进行特别显示。

当然,在我们业务系统里面,可能还有其他类似的场景。

对于这些相似的需求我们把这些应用场景的状态,用一个表来存储它的数据变化就可以做到了,我们设计一个表TB_InformationStatus来存储这些数据的状态。

上面的Information_ID就是对应不同表数据的ID,Status为我们需要记录的状态,User_ID为对应使用人员,这样对于不同业务表,不同的人员都可以把他们的数据记录起来,供我们处理显示了。

2、功能实现

对于这个信息状态的记录表,我们需要定义几个接口来进行信息的处理。

        /// <summary>
        ///设置状态/// </summary>
        /// <param name="UserID">用户ID</param>
        /// <param name="InfoType">信息类型</param>
        /// <param name="InfoID">信息主键ID</param>
        /// <param name="Status">状态:0未读 1已读</param>
        void SetStatus(string UserID, InformationCategory InfoType, string InfoID, intStatus);/// <summary>
        ///匹配状态/// </summary>
        /// <param name="UserID">用户ID</param>
        /// <param name="InfoType">信息类型</param>
        /// <param name="InfoID">信息主键ID</param>
        /// <param name="Status">状态:0未读 1已读</param>
        /// <returns></returns>
        bool CheckStatus(string UserID, InformationCategory InfoType, string InfoID, intStatus);/// <summary>
        ///查看指定的记录是否已读/// </summary>
        /// <param name="UserID">用户ID</param>
        /// <param name="InfoType">信息类型</param>
        /// <param name="InfoID">信息主键ID</param>
        /// <returns></returns>
        bool IsReadedStatus(string UserID, InformationCategory InfoType, string InfoID);

我们设计了上面的辅助表TB_InformationStatus来存储这些数据的状态,但并没有改变主表的字段数据,但是我们在显示主表的数据的时候,联合处理一下就可以了。

以客户信息为例,我们联合处理,获得的数据,依旧是客户信息的列表,如下代码所示。

        /// <summary>
        ///获取用户的最近未联系客户列表/// </summary>
        /// <returns></returns>
        private List<CustomerInfo>GetUnContactList()
{
string KeyName = "FollowExpireDays";int FollowExpireDays =config.AppConfigGet(KeyName).ToInt32();if (FollowExpireDays < 1)
{
FollowExpireDays
= 1;
}
List
<CustomerInfo> list = BLLFactory<Customer>.Instance.GetUnContactList(FollowExpireDays, LoginUserInfo.ID.ToString());returnlist;
}

在业务层,我们只需要构造我们的过滤条件获取到用户的数据,并处理它状态就可以了。

                condition.AddCondition("LastContactDate", today.AddDays(-1 *FollowExpireDays), SqlOperator.LessThanOrEqual);
condition.AddCondition(
"Deleted", 0, SqlOperator.Equal);//不显示删除的 condition.AddCondition("Creator", userId, SqlOperator.Equal);//仅仅选择该用户的记录 string where = condition.BuildConditionSql().Replace("Where", "");

List
<CustomerInfo> list = baseDal.Find(where);foreach (CustomerInfo info inlist)
{
bool readed = BLLFactory<InformationStatus>.Instance.IsReadedStatus(userId, InformationCategory.客户联系, info.ID);
info.Data1 = readed ? "已读" : "未读"
;
}

上面代码的Data1是我们实体类基类的属性,这里我们很方面用它来记录状态,否则我们需要把实体类集合转换为DataTable类型了。

这样我们返回的数据就带有这个记录的阅读状态,我们只需要在显示的时候,把Data1属性的别名修改一下就可以了。

        /// <summary>
        ///绑定列表数据/// </summary>
        private voidBindData()
{
this.winGridViewPager1.DisplayColumns =displayColumns;this.winGridViewPager1.ColumnNameAlias = BLLFactory<Customer>.Instance.GetColumnNameAlias();//字段列显示名称转义 this.winGridViewPager1.AddColumnAlias("Data1", "查看状态");

List
<CustomerInfo> list =GetUnContactList();this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<CustomerInfo>(list);this.winGridViewPager1.PrintTitle = "未联系客户信息列表";
}

3、功能扩展

为了更有效展示不同类型客户的记录,我们可能需要设置普通客户7天需要跟进,VIP客户5天跟进,高级VIP客户3天跟进的时效,也就是对于同一个记录,不同属性类型,可能要求不同。

我们如果要实现这个需求,那么就需要再另外一个表里面记录客户类型和间隔天数的数据了。

然后在业务逻辑层处理返回未联系客户的时候,对他们进行分别处理,获取数据后进行合并,如下代码所示。

        /// <summary>
        ///获得指定间隔时间内未联系的客户列表/// </summary>
        /// <param name="unContactDays">和最后联系日期的间隔天数</param>
        /// <param name="userId">当前用户</param>
        /// <returns></returns>
        public List<CustomerInfo> GetUnContactList(int unContactDays, stringuserId)
{
List
<CustomerInfo> listAll = new List<CustomerInfo>();//根据用户配置的信息进行逐条处理,然后合并记录 List<CustomerAlarmInfo> alarmList = BLLFactory<CustomerAlarm>.Instance.FindByUser(userId);foreach (CustomerAlarmInfo alarmInfo inalarmList)
{
//如果存在高级查询对象信息,则使用高级查询条件,否则使用主表条件查询 SearchCondition condition = newSearchCondition();
DateTime today
= Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd"));int FollowExpireDays =alarmInfo.Days;if (FollowExpireDays < 1)
{
FollowExpireDays
= 1;
}

condition.AddCondition(
"Grade", alarmInfo.Grade, SqlOperator.Equal);
condition.AddCondition(
"LastContactDate", today.AddDays(-1 *FollowExpireDays), SqlOperator.LessThanOrEqual);
condition.AddCondition(
"Deleted", 0, SqlOperator.Equal);//不显示删除的 condition.AddCondition("Creator", userId, SqlOperator.Equal);//仅仅选择该用户的记录 string where = condition.BuildConditionSql().Replace("Where", "");

List
<CustomerInfo> list = baseDal.Find(where);foreach (CustomerInfo info inlist)
{
bool readed = BLLFactory<InformationStatus>.Instance.IsReadedStatus(userId, InformationCategory.客户联系, info.ID);
info.Data1
= readed ? "已读" : "未读";
}

listAll.AddRange(list);
}
returnlistAll;
}

对于混合架构上的应用,我们注意到接口的地方,需要使用一个枚举的参数(信息类别名称),我们在接口定义的时候,需要特别声明几个地方,否则容易出现错误。

枚举的对象需要声明一下的。

    /// <summary>
    ///信息分类/// </summary>
[DataContract]public enumInformationCategory 
{
[EnumMember]
客户联系,

[EnumMember]
通知公告,

[EnumMember]
其他
};

定义的WCF接口,用到了枚举类型的参数,也需要特别声明枚举的类型

[ServiceContract]
[ServiceKnownType(
typeof(InformationCategory))]public interface IInformationStatusService : IBaseService<InformationStatusInfo>{/// <summary> ///设置状态/// </summary> /// <param name="UserID">用户ID</param> /// <param name="InfoType">信息类型</param> /// <param name="InfoID">信息主键ID</param> /// <param name="Status">状态:0未读 1已读</param> [OperationContract]void SetStatus(string UserID, InformationCategory InfoType, string InfoID, intStatus);/// <summary> ///匹配状态/// </summary> /// <param name="UserID">用户ID</param> /// <param name="InfoType">信息类型</param> /// <param name="InfoID">信息主键ID</param> /// <param name="Status">状态:0未读 1已读</param> /// <returns></returns> [OperationContract]bool CheckStatus(string UserID, InformationCategory InfoType, string InfoID, intStatus);/// <summary> ///查看指定的记录是否已读/// </summary> /// <param name="UserID">用户ID</param> /// <param name="InfoType">信息类型</param> /// <param name="InfoID">信息主键ID</param> /// <returns></returns> [OperationContract]bool IsReadedStatus(string UserID, InformationCategory InfoType, stringInfoID);
}

注意上这些,使用枚举就一切都顺利了。

我们在很多情况下,可能都是某种组织的会员,如健身、游泳馆、超市、美容店等其他连锁店,这些针对会员的管理和消费管理,从而提供给会员更多的优惠,一般通过积分的方式实现。本文主要从一个开发者的角度,对会员系统进行的设计开发进行剖析,希望能与大家一起探讨,实现更多的思想碰撞。

如果系统是在一个店铺使用的,那么使用单机版本的操作模式即可,如可以使用Winform + SQLite/Access方式,实现数据的访问,并且方便软件复制和备份工作,如果需要性能好一点或者数据更加安全一点,可以采用独立的数据库方式,如采用一个独立的机器部署SqlServer数据库或者Mysql数据库,Oracle数据库就没太大必要了。

如果系统是在一系列连锁店中使用的,那么可以采用Winform+WCF服务方式,实现数据的分布式访问方式,这样数据就不会保存在本地,和B/S通过浏览器的方式很类似,但是Winform客户端能提供更丰富的界面体验效果。当然,我们每一家的连锁店就需要能够上网,随时进行数据的交换处理。

还有一种方式,是离线式的服务,就是弥补第二种方式在断开网络的时候不能工作的缺点,这种方式即使在网络断开,也能照常运营,网络通畅的时候,通过手工进行数据的提交就可以了。由于现在网络一般比较方便,所以这种方式一般采用的不多,只在特殊情况下采用。

1、系统用例的设计

我们知道,会员管理的主要目的就是以会员为中心,实现相关数据的管理。会员管理包括有会员本身的信息管理、会员收费管理、积分管理(积分增减、积分兑换、积分转账)、挂失管理、换卡管理、余额转账、商品管理、消费管理等等,围绕着会员管理展开,通过多个职能操作,实现相关数据的录入和管理。

2、系统数据库设计

数据库的设计,也主要是围绕着会员信息进行的,会员信息是作为所有会员相关记录的外键引用。为了避免数据库表的阅读困难,会员管理的相关表,使用“MS_”前缀声明。

除了以下的表外,还包括了会员的打折设置信息,积分奖励设置,以及用于会员消费的商品信息,及会员消费的记录信息(包括消费主表和明细表记录)。

为了篇幅的介绍,我主要列出会员的主表信息作为讨论参考。

表主要使用字符型的ID作为表的主键,保存的时候,ID自动使用GUID作为数据存储,由于考虑了可能多个连锁店的情况,因此,我们需要增加一个Creator, CreateTime, Editor,EditTime, Dept_ID, Company_ID的通用字段,方便存储用户的相关表记录信息,这样我们在数据过滤以及报表查询的时候,会方便很多。

3、系统模块化设计

当然会员的信息还可以扩展更多,我们一般是以一个通用的会员管理来实现这个模块,从而可以在整个大系统中进行整合和使用。而一般我们都有自己的平台模块积累,在业务层只需要整合现有的一些底层模块作为支持,业务系统我们独立开发即可,大概的构造如下所示。

当然,我们随着系统的开发,我们可能需要整合两个以上的系统(或者底层业务模块)到一个大系统里面,这种要求就需要我们所有的
系统
模块,都可以通过松耦合、插件化整合的方式实现使用的。

本文主要介绍一个会员系统开发的整体思路和设计,随着开发的深入,可能会继续分享一些相关的开发心得。