2023年2月

最近花了不少时间在重构和进一步提炼我的Web开发框架上,力求在用户体验和界面设计方面,和Winform开发框架保持一致,而在Web上,我主要采用EasyUI的前端界面处理技术,走MVC的技术路线,在重构完善过程中,很多细节花费不少时间进行研究和提炼,一步步走过来,也积累了不少经验,本系列将主要介绍我在进一步完善我的Web框架基础上积累的经验进行分享,本随笔主要介绍使用EasyUI的树控件构建Web界面的相关经验。

在很多界面设计上,我们可能都需要引入树列表控件,这个控件可以用zTree来实现,也可以用EasyUI的内置树控件进行展示,由于历史原因,我原来倾向于使用zTree,最新把它全部修改为EasyUI的树控件,并进行了完善优化,发现代码更加简洁明快,非常不错。

1、在界面上使用EasyUI的树控件

一般情况下,使用EasyUI的树控件,代码很简单,脚本代码如下所示,主要就是通过调用url来获得Json数据,然后就可以显示了,通过onClick就可以响应用户单击节点的操作,每个节点有id, text, iconCls, checked,state,children等属性。

1)树控件的Json数据绑定

            $('#treeDept').tree({
url:
'/User/GetMyDeptTreeJson?userId=@Session["UserId"]',
onClick:
function(node) {
loadData(node.id);
}
});

2)树控件的折叠和展开

树控件的展开和折叠,可以通过定义两个通用的脚本进行处理,如下所示。

        functionexpandAll(treeName) {var node = $('#' + treeName).tree('getSelected');if(node) {
$(
'#' + treeName).tree('expandAll', node.target);
}
else{
$(
'#' + treeName).tree('expandAll');
}
}
functioncollapseAll(treeName) {var node = $('#' + treeName).tree('getSelected');if(node) {
$(
'#' + treeName).tree('collapseAll', node.target);
}
else{
$(
'#' + treeName).tree('collapseAll');
}
}

然后,在页面加载完毕后,绑定指定的按钮控件就可以了吗,如下代码所示。

        //初始化对象
        $(document).ready(function() {//初始化机构分类
initOUCategorys();//机构基础信息
initDeptTreeview();
$(
"#deptExpand").bind("click", function () {
expandAll("treeDept");
});
$("#deptCollapse").bind("click", function () {
collapseAll("treeDept"
);
});


$(
"#loading").center(); //loading的图片显示居中 });

3)树控件的复选框显示

树控件默认是没有复选框的,它可以通过属性checkbox设置让它进行展示的,如下代码是我项目里面的代码。

其中cascadeCheck是否让树控件级联的,默认是级联,也就是只要父控件被选中,所有其子控件都会被选中,我们可以设置它为false,让它不级联,这样在很多情况下是需要的。

            $('#treeFunctionView').tree({
checkbox:
true,
cascadeCheck:
false,
url:
'/Function/GetRoleFunctionByUser?userId=@Session["UserId"]',
onClick:
function(node) {//}
});

4)树控件的全选和全不选择

这个全部不选的特性,我找了很多文章,都没有找到,其实后来才发现,我们对树的节点理解有偏差,认识到后,实现起来也很容易。

如取消全部节点的选中状态,代码如下所示。它的方法getChecked是返回所有的节点,而不是一个节点。它们把全部选中的节点放到一个结合里面,不像Winform里面,树节点需要递归查询,这里只需要一个for循环就可以展开了,我这里把所有勾选的节点,设置为非勾选状态就可以实现取消全部树节点勾选状态了。

        functionunCheckTree(tree) {var nodes = $('#' + tree).tree('getChecked');if(nodes) {for (var i = 0; i < nodes.length; i++) {
$(
'#' + tree).tree('uncheck', nodes[i].target);
}
}
}

我们知道,很多树控件,为了方便操作,都提供了一个全选或者全部不选的操作,这个在EasyUI的树控件里面,也是很容易实现的。这里的getChildren和上面的意思类似,也是返回所有的子节点,不需要在进行递归,用一个for循环就可以遍历全部节点和其下面的多级子节点了。也就是说,它是一个二维的数据,不用递归查询。

        functioncheckAllTree(tree, checked) {var children = $('#' + tree).tree('getChildren');for (var i = 0; i < children.length; i++) {if(checked) {
$(
'#' + tree).tree('check', children[i].target);
}
else{
$(
'#' + tree).tree('uncheck', children[i].target);
}
}
}

5)下拉列表的树控件初始化

除了普通的树列表,还有一种比较特殊的树控件,就是在ComboTree,也就是在下拉列表中集成树控件,它的操作和普通的树控件差不多,很多事件属性都一样,它的使用代码如下所示。

        //初始化公司
        functioninitCompany() {
$(
'#txtCompany_ID').combotree({
url:
'/User/GetMyCompanyTreeJson?userId=@Session["UserId"]',
valueField:
'id',
textField:
'text',
required:
true,
onClick:
function(node) {//}
});
}

2、树控件的优化

1)普通的Json数据生成

前面说了,我们为了方便,一般使用Json数据和javascript打交道,而EasyUI的树控件支持很好地的Json链接绑定,因此我们只需要在对应的控制器里面实现json数据的生成即可,如果是一开始想要确定的Json数据,一般也是通过手工生成的居多,如下代码所示。

public ActionResult GetTreeJson()
{
string folder
= "/Content/JqueryEasyUI/themes/icons/customed/" + "organ.png";
string leaf
= "/Content/JqueryEasyUI/themes/icons/customed/" + "organ.png";
string json
= GetTreeJson(-1, folder, leaf);
json
= json.Trim(',');return Content(string.Format("[{0}]", json));
}
/// <summary> /// 递归获取树形信息 /// </summary> /// <param name="PID"></param> /// <returns></returns> private string GetTreeJson(intPID, string folderIcon, string leafIcon)
{
string condition
= string.Format("PID={0}", PID);
List
<OUInfo> nodeList = BLLFactory<OU>.Instance.Find(condition);
StringBuilder content
= newStringBuilder();
foreach (OUInfo model
innodeList)
{
int ParentID = (model.PID == -1 ? 0: model.PID);//string tempMenu = string.Format("{{ id:{0}, pId:{1}, name:\"{2}\",icon:\"{3}\" }},", model.ID, ParentID, model.Name, imgsrc); string subMenu = this.GetTreeJson(model.ID, folderIcon, leafIcon);
string parentMenu
= string.Format("{{ \"id\":{0}, \"pId\":{1}, \"name\":\"{2}\" ", model.ID, ParentID, model.Name);if(string.IsNullOrEmpty(subMenu))
{
if (!string.IsNullOrEmpty(leafIcon))
{
parentMenu
+= string.Format(",\"icon\":\"{0}\" }},", leafIcon);
}
else{
parentMenu
+= "},";
}
}
else{if (!string.IsNullOrEmpty(folderIcon))
{
parentMenu
+= string.Format(",\"icon\":\"{0}\" }},", folderIcon);
}
else{
parentMenu
+= "},";
}
}

content.AppendLine(parentMenu.Trim());
content.AppendLine(subMenu.Trim());
}
returncontent.ToString().Trim();
}

上面的代码很好实现了根据数据库结构的关系,生成Json数据,但是感觉部分硬编码,凑出来的数据,始终感觉不太理想,如果我们要简化,该如何操作呢?

2)简洁美观的Json数据生成

本小节继续上面的议题,看如何简化json的生成,因为我们需要很多这样的json操作,如果采用上面的方法,我感觉很容易出错,而且也不太美观。为了解决这个问题,我们可以通过定义一个json数据的实体类,用来承载相关的信息,如下定义所示。

    /// <summary>
    /// 定义EasyUI树的相关数据,方便控制器生成Json数据进行传递
    /// </summary>
[DataContract]
[Serializable]
public class EasyTreeData
{
/// <summary> /// ID /// </summary> [DataMember]
public string id { get; set; }
/// <summary> /// 节点名称 /// </summary> [DataMember]
public string text { get; set; }
/// <summary> /// 是否展开 /// </summary> [DataMember]
public string state { get; set; }
/// <summary> /// 图标样式 /// </summary> [DataMember]
public string iconCls { get; set; }
/// <summary> /// 子节点集合 /// </summary> [DataMember]
public List
<EasyTreeData>children { get; set; }/// <summary> /// 默认构造函数 /// </summary> public EasyTreeData()
{
this.children = new List<EasyTreeData>();this.state = "open";
}
/// <summary> /// 常用构造函数 /// </summary> public EasyTreeData(string id, string text, string iconCls = "", string state = "open")
:
this()
{
this.id =id;this.text =text;this.state =state;this.iconCls =iconCls;
}
/// <summary> /// 常用构造函数 /// </summary> public EasyTreeData(int id, string text, string iconCls = "", string state = "open")
:
this()
{
this.id =id.ToString();this.text =text;this.state =state;this.iconCls =iconCls;
}
}

然后,我们在需要生成Json数据的地方,使用这个实体类进行承载,然后把它列表生成Json就可以了,很简单了,呵呵。

        /// <summary>
        /// 根据用户获取对应人员层次的树Json
        /// </summary>
        /// <param name="deptId">用户所在部门</param>
        /// <returns></returns>
        public ActionResult GetUserTreeJson(intdeptId)
{
List
<EasyTreeData> treeList = new List<EasyTreeData>();
treeList.Insert(
0, new EasyTreeData(-1, "无"));

List
<UserInfo> list = BLLFactory<User>.Instance.FindByDept(deptId);
foreach (UserInfo info
inlist)
{
treeList.Add(
new EasyTreeData(info.ID, info.FullName, "icon-user"));
}

string json
=ToJson(treeList);returnContent(json);
}

如果需要递归的操作,一样的方式处理就可以了。

        /// <summary>
        /// 获取用户的部门树结构(分级需要)
        /// </summary>
        /// <param name="userId">用户ID</param>
        /// <returns></returns>
        public ActionResult GetMyDeptTreeJson(intuserId)
{
StringBuilder content
= newStringBuilder();
UserInfo userInfo
= BLLFactory<User>.Instance.FindByID(userId);if (userInfo != null)
{
OUInfo groupInfo
=GetMyTopGroup(userInfo);if (groupInfo != null)
{
List
<OUNodeInfo> list = BLLFactory<OU>.Instance.GetTreeByID(groupInfo.ID);

EasyTreeData treeData
= newEasyTreeData(groupInfo.ID, groupInfo.Name, GetIconcls(groupInfo.Category));
GetTreeDataWithOUNode(list, treeData);

content.Append(base.ToJson(treeData));
}
}
string json
= string.Format("[{0}]", content.ToString().Trim(','));returnContent(json);
}

上面使用EasyTreeData来承载数据,然后构建列表,其本身就是一个多层级的树对象,然后一个ToJson的方法就可以把列表对象完美转换为Jason数据了。

这里的ToJson,主要就是调用JavaScriptSerializer 对象进行的操作,如下所示。

        /// <summary>
        /// 把对象为json字符串
        /// </summary>
        /// <param name="obj">待序列号对象</param>
        /// <returns></returns>
protected string ToJson(object obj)
{
string jsonData
= (newJavaScriptSerializer()).Serialize(obj);returnjsonData;
}

3、树控件效果展示

在介绍如何使用它之后,我们来看看我几个场景中使用树控件进行的展示效果,方便我们加深上面EasyUI树控件使用的了解。

1)组织机构列表如下所示:

2)角色树列表展示

3)功能树列表展示

4)菜单树列表展示

5)登陆日志树列表展示

6)下拉列表树展示



基于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)--自动生成图标样式文件和图标的选择操作

最近花了不少时间在重构和进一步提炼我的Web开发框架上,力求在用户体验和界面设计方面,和Winform开发框架保持一致,而在Web上,我主要采用EasyUI的前端界面处理技术,走MVC的技术路线,在重构完善过程中,很多细节花费不少时间进行研究和提炼,一步步走过来,也积累了不少经验,本系列将主要介绍我在进一步完善我的Web框架基础上积累的经验进行分享,本随笔主要介绍使用如何使用Json实体类构建菜单数据,然后在主界面中进行使用。

菜单的界面效果如下所示,菜单分为一级菜单、二级菜单、三级菜单,他们各自在位置上是不同的定义,这个界面布局规定三级菜单就是最小的菜单节点了,也就是叶子节点。

要实现以上的菜单,需要把菜单定义成相关的Json数据,然后通过脚本把它们添加到界面里面去,如下数据和脚本就是定义相关的菜单数据的。

    <script type="text/javascript">
        var _menus ={"default": [
{
"menuid": "1", "icon": "icon-computer", "menuname": "权限管理","menus": [
{
"menuid": "13", "menuname": "用户管理", "icon": "icon-user", "url": "/User/Index"},
{
"menuid": "14", "menuname": "组织机构管理", "icon": "icon-organ", "url": "/OU/Index"},
{
"menuid": "15", "menuname": "角色管理", "icon": "icon-group-key", "url": "/Role/Index"},
{
"menuid": "16", "menuname": "功能管理", "icon": "icon-key", "url": "/Function/Index"},
{
"menuid": "17", "menuname": "登陆日志", "icon": "icon-view", "url": "/LoginLog/Index"}
]
},
{
"menuid": "2", "icon": "icon-user", "menuname": "其他管理","menus": [{ "menuid": "21", "menuname": "修改密码", "icon": "icon-lock", "url": "javascript:ShowPasswordDialog()"}
]
}
],
"point": [
{
"menuid": "3", "icon": "icon-computer", "menuname": "事务中心","menus": [
{
"menuid": "33", "menuname": "测试菜单1", "icon": "icon-user", "url": "../Commonpage/building.htm"},
{
"menuid": "34", "menuname": "测试菜单2", "icon": "icon-organ", "url": "../Commonpage/building.htm"},
{
"menuid": "35", "menuname": "测试菜单3", "icon": "icon-group-key", "url": "../Commonpage/building.htm"},
{
"menuid": "36", "menuname": "测试菜单4", "icon": "icon-key", "url": "../Commonpage/building.htm"}
]
},
{
"menuid": "4", "icon": "icon-user", "menuname": "其他菜单","menus": [{ "menuid": "41", "menuname": "测试菜单5", "icon": "icon-lock", "url": "../Commonpage/building.htm"}]
}
]
};
functionshowSubMenu(url, title, menuCategory, defaultIcon) {if (defaultIcon == null || defaultIcon == "") {
defaultIcon
= "icon-table";
}
addTab(title, url,
"icon " +defaultIcon);
Clearnav();
if (menuCategory != "") {
addNav(_menus[menuCategory]);
}
}
</script>

从上面的菜单Json数据来看,它是一个字典的Json数据列表,在Web界面上,通过下面的代码可以展开上面Json定义的二级菜单。

<li><ahref="#"onclick="showSubMenu('/User/Index', '用户管理', 'default')">权限管理</a></li>

虽然上面的定义的数据能够解决菜单的显示问题,但是对于我们需要动态控制的菜单,显然做不到,因此需要把上面的json数据,通过菜单控制器进行动态生成才可以,然后在脚本里面通过Jquery的方式获取Json数据,如下所示。

        var _menus ={};//同步获取
$.ajax({
type:
'GET',
url:
'/Menu/GetMenuData?r=' +Math.random(),
async:
false,//同步 dataType: 'json',
success:
function(json) {
_menus
=json;
},
error:
function(xhr, status, error) {
alert(
"操作失败"); //xhr.responseText }
});

上面的GetMenuData方法,通过后台的控制器进行动态生成的,它的代码如下所示

        /// <summary>
        ///获取树形展示数据/// </summary>
        /// <returns></returns>
        publicActionResult GetMenuData()
{
string json = GetTreeJson("-1", "", "");
json
= json.Trim(',');return Content(string.Format("[{0}]", json));
}
/// <summary> ///递归获取树形信息/// </summary> /// <returns></returns> private string GetTreeJson(string PID, string folderIcon, stringleafIcon)
{
string condition = string.Format("PID='{0}'", PID);
List
<MenuInfo> nodeList = BLLFactory<Menu>.Instance.Find(condition);
StringBuilder content
= newStringBuilder();foreach (MenuInfo model innodeList)
{
string ParentID = (model.PID == "-1" ? "0": model.PID); string subMenu = this.GetTreeJson(model.ID, folderIcon, leafIcon);string parentMenu = string.Format("{{ \"id\":\"{0}\", \"pId\":\"{1}\", \"name\":\"{2}\"", model.ID, ParentID, model.Name);if (string.IsNullOrEmpty(subMenu))
{
if (!string.IsNullOrEmpty(leafIcon))
{
parentMenu
+= string.Format(",\"icon\":\"{0}\" }},", leafIcon);
}
else{
parentMenu
+= "},";
}
}
else{if (!string.IsNullOrEmpty(folderIcon))
{
parentMenu
+= string.Format(",\"icon\":\"{0}\" }},", folderIcon);
}
else{
parentMenu
+= "},";
}
}

content.AppendLine(parentMenu.Trim());
content.AppendLine(subMenu.Trim());
}
returncontent.ToString().Trim();
}

不过对于上面的代码,我觉得虽然能解决问题,能够正确生成相关的Json代码,但是感觉不够优雅,我不喜欢使用拼凑方法构建数据。

前面看了Menu的Json脚本,我说过他是一个字典类型的Json数据格式,那么我们是否可以通过字典和实体信息来承载,然后直接通过ToJson方法出来呢?答案是可以的。

        /// <summary>
        ///获取菜单的树形展示数据/// </summary>
        /// <returns></returns>
        publicActionResult GetMenuData()
{
Dictionary
<string, List<MenuData>> dict = new Dictionary<string, List<MenuData>>();

List
<MenuInfo> list = BLLFactory<Menu>.Instance.GetTopMenu(MyConstants.SystemType);int i = 0;foreach (MenuInfo info inlist)
{
if (!HasFunction(info.FunctionId))
{
continue;
}
List
<MenuData> treeList = new List<MenuData>();
List
<MenuNodeInfo> nodeList = BLLFactory<Menu>.Instance.GetTreeByID(info.ID);foreach (MenuNodeInfo nodeInfo innodeList)
{
if (!HasFunction(nodeInfo.FunctionId))
{
continue;
}
MenuData menuData
= new MenuData(nodeInfo.ID, nodeInfo.Name, string.IsNullOrEmpty(nodeInfo.WebIcon) ? "icon-computer": nodeInfo.WebIcon);foreach (MenuNodeInfo subNodeInfo innodeInfo.Children)
{
if (!HasFunction(subNodeInfo.FunctionId))
{
continue;
}
string icon = string.IsNullOrEmpty(subNodeInfo.WebIcon) ? "icon-organ": subNodeInfo.WebIcon;
menuData.menus.Add(
newMenuData(subNodeInfo.ID, subNodeInfo.Name, icon, subNodeInfo.Url));
}
treeList.Add(menuData);
}
//添加到字典里面,如果是第一个,默认用default名称 string dictName = (i++ == 0) ? "default": info.ID;
dict.Add(dictName, treeList);
}
string content =ToJson(dict);return Content(content.Trim(','));
}

上面的代码,通过MenuData的对象数据,来承载相关的菜单信息,然后把它添加到字典Dictionary<string, List<MenuData>> dict 里面就可以了,这样的代码,没有那么多拼凑出来的感觉,是不是很好看呢?把对象转换为Json数据,直接通过ToJson就可以解决了,很简单吧。

而菜单的权限控制,就是通过集合权限管理进行判断,父菜单如果没有权限,就直接跳过,不在继续生成下面的子菜单,权限判断的如下所示。

if (!HasFunction(info.FunctionId))
{
continue;
}

当然,在界面上展开二级菜单的操作界面,也应该通过脚本动态进行生成的,这样才能做到所有的内容动态构建。

        <ulclass="navigation"style="display:block">@Html.Raw(@ViewBag.HeaderScript)</ul>

上面使用ViewBag对象进行传递脚本内容到界面上,其实后台生成的操作,是一行HTML代码就是了,代码类似下面的内容。

<li><ahref="#"onclick="showSubMenu('/User/Index', '用户管理', 'default')">权限管理</a></li>

最后出来的效果,就是博客开始介绍的界面截图,没有任何变化,但是代码我们已经经过了几步的优化整理,看起来很清爽,更能实现动态变化了。

有空可以回顾下其他两篇的经验总结内容:

基于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)--自动生成图标样式文件和图标的选择操作

在上个月的对C#开发微信门户及应用做了介绍,写过了几篇的随笔进行分享,由于时间关系,间隔了一段时间没有继续写这个系列的博客了,并不是对这个方面停止了研究,而是继续深入探索这方面的技术,为了更好的应用起来,专心做好底层的技术开发。

微信的很重要的一个特点就是能够利用其平台庞大的用户群体,因此很容易整合在CRM(客户关系管理)系统里面,服务号和订阅好都能够向关注者推送相关的产品消息,还能和48小时内响应消息和事件的活跃用户进行交互对话,因此用户信息是微信API里面非常重要的一环,本随笔主要介绍获取关注用户、查看用户信息、分组管理等方面的开发应用。

1、关注用户列表及用户分组信息

在微信的管理平台上,我们可以看到自己账号的关注者用户,以及用户分组信息,如下所示。

上面的管理界面,能看到关注者用户的基础信息,但是使用微信API获取到的是一个称之为OpenID的列表,我们先了解这个东西是什么?微信API的说明给出下面的解析:

关注者列表由一串OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的
。对于不同公众号,同一用户的openid不同
)组成。
公众号可通过本接口来根据OpenID获取用户基本信息,包括昵称、头像、性别、所在城市、语言和关注时间。

上面的解析意思很清楚了,就是一个用户关注我们的公众号,那么不管他是第几次关注,对我们公众号来说,都是一个确定的值;但是,一个用户对其他公众号,却有着其他不同的OpenID。

微信提供了为数不多的几个关键字信息,用来记录用户的相关内容,根据用户的相关定义,我们定义一个实体类,用来放置获取回来的用户信息。

    /// <summary>
    ///高级接口获取的用户信息。///在关注者与公众号产生消息交互后,公众号可获得关注者的OpenID///(加密后的微信号,每个用户对每个公众号的OpenID是唯一的。对于不同公众号,同一用户的openid不同)。///公众号可通过本接口来根据OpenID获取用户基本信息,包括昵称、头像、性别、所在城市、语言和关注时间。/// </summary>
    public classUserJson : BaseJsonResult
{
/// <summary> ///用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号,拉取不到其余信息。/// </summary> public int subscribe { get; set; }/// <summary> ///用户的标识,对当前公众号唯一/// </summary> public string openid { get; set; }/// <summary> ///用户的昵称/// </summary> public string nickname { get; set; }/// <summary> ///用户的性别,值为1时是男性,值为2时是女性,值为0时是未知/// </summary> public int sex { get; set; }/// <summary> ///用户的语言,简体中文为zh_CN/// </summary> public string language { get; set; }/// <summary> ///用户所在城市/// </summary> public string city { get; set; }/// <summary> ///用户所在省份/// </summary> public string province { get; set; }/// <summary> ///用户所在国家/// </summary> public string country { get; set; }/// <summary> ///用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空/// </summary> public string headimgurl { get; set; }/// <summary> ///用户关注时间,为时间戳。如果用户曾多次关注,则取最后关注时间/// </summary> public long subscribe_time { get; set; }
}

根据分组信息定义,我们定义一个分组的实体类信息。

    /// <summary>
    ///分组信息/// </summary>
    public classGroupJson : BaseJsonResult
{
/// <summary> ///分组id,由微信分配/// </summary> public int id { get; set; }/// <summary> ///分组名字,UTF8编码/// </summary> public string name { get; set; }
}

2、获取AIP调用者的的Token

在做微信API的开发,很多时候,我们都需要传入一个AccessToken,这个就是区分调用者和记录会话信息的字符串,因此,在学习所有API开发之前,我们需要很好理解这个访问控制参数。

这个对象的定义,我们可以从微信的API说明中了解。

access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。正常情况下
access_token有效期为7200秒
,重复获取将导致上次获取的access_token失效。
由于获取access_token的api调用次数非常有限,建议开发者全局存储与更新access_token,频繁刷新access_token会导致api调用受限,影响自身业务。

根据上面的说明定义,我们可以看到,它是一个和身份,以及会话时间有关的一个参数,而且它的产生次数有限制,因此要求我们需要对它进行缓存并重复利用,在会话到期之前,我们应该尽可能重用这个参数,避免反复请求,增加服务器压力,以及调用的时间。

我定义了一个方法,用来构造生成相关的Access Token,而且它具有缓存的功能,但具体如何缓存及使用,对我API的调用是透明的,我们只要用的时候,就对它调用就是了。

        ///获取凭证接口/// </summary>
        /// <param name="appid">第三方用户唯一凭证</param>
        /// <param name="secret">第三方用户唯一凭证密钥,既appsecret</param>
        string GetAccessToken(string appid, string secret);

缓存主要是基于.NET4增加的类库MemoryCache,这个是一个非常不错的缓存类。

我的获取AccessToken的操作实现代码如下所示。

        /// <summary>
        ///获取每次操作微信API的Token访问令牌/// </summary>
        /// <param name="appid">应用ID</param>
        /// <param name="secret">开发者凭据</param>
        /// <returns></returns>
        public string GetAccessToken(string appid, stringsecret)
{
//正常情况下access_token有效期为7200秒,这里使用缓存设置短于这个时间即可 string access_token = MemoryCacheHelper.GetCacheItem<string>("access_token", delegate()
{
string grant_type = "client_credential";var url = string.Format("https://api.weixin.qq.com/cgi-bin/token?grant_type={0}&appid={1}&secret={2}",
grant_type, appid, secret);

HttpHelper helper
= newHttpHelper();string result =helper.GetHtml(url);string regex = "\"access_token\":\"(?<token>.*?)\""; string token = CRegex.GetText(result, regex, "token");returntoken;
},
new TimeSpan(0, 0, 7000)//7000秒过期 );returnaccess_token;
}

由于我们知道,AccessToken默认是7200秒过期,因此在这个时间段里面,我们尽可能使用缓存来记录它的值,如果超过了这个时间,我们调用这个方法的时候,它会自动重新获取一个新的值给我们了。

3、获取关注用户列表

获取关注用户列表,一次拉取API调用,最多拉取10000个关注者的OpenID,可以通过多次拉取的方式来满足需求。
微信的接口定义如下所示。

http请求方式: GET(请使用https协议)
https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID

这个接口返回的数据是

{"total":2,"count":2,"data":{"openid":["","OPENID1","OPENID2"]},"next_openid":"NEXT_OPENID"}

根据返回的Json数据定义,我们还需要定义两个实体类,用来存放返回的结果。

    /// <summary>
    ///获取关注用户列表的Json结果/// </summary>
    public classUserListJsonResult : BaseJsonResult
{
/// <summary> ///关注该公众账号的总用户数/// </summary> public int total { get; set; }/// <summary> ///拉取的OPENID个数,最大值为10000/// </summary> public int count { get; set; }/// <summary> ///列表数据,OPENID的列表/// </summary> public OpenIdListData data { get; set; }/// <summary> ///拉取列表的后一个用户的OPENID/// </summary> public string next_openid { get; set; }
}
/// <summary> ///列表数据,OPENID的列表/// </summary> public classOpenIdListData
{
/// <summary> ///OPENID的列表/// </summary> public List<string> openid { get; set; }
}

为了获取相关的用户信息,我定义了一个接口,用来获取用户的信息,接口定义如下所示。

    /// <summary>
    ///微信用户管理的API接口/// </summary>
    public interfaceIUserApi
{
/// <summary> ///获取关注用户列表/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="nextOpenId">第一个拉取的OPENID,不填默认从头开始拉取</param> /// <returns></returns> List<string> GetUserList(string accessToken, string nextOpenId = null);/// <summary> ///获取用户基本信息/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="openId">普通用户的标识,对当前公众号唯一</param> /// <param name="lang">返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语</param> UserJson GetUserDetail(string accessToken, string openId, Language lang = Language.zh_CN);

然后在实现类里面,我们分别对上面两个接口进行实现,获取用户列表信息如下所示。

        /// <summary>
        ///获取关注用户列表/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="nextOpenId">第一个拉取的OPENID,不填默认从头开始拉取</param>
        /// <returns></returns>
        public List<string> GetUserList(string accessToken, string nextOpenId = null)
{
List
<string> list = new List<string>();string url = string.Format("https://api.weixin.qq.com/cgi-bin/user/get?access_token={0}", accessToken);if (!string.IsNullOrEmpty(nextOpenId))
{
url
+= "&next_openid=" +nextOpenId;
}

UserListJsonResult result
= JsonHelper<UserListJsonResult>.ConvertJson(url);if (result != null && result.data != null)
{
list.AddRange(result.data.openid);
}
returnlist;
}

我们看到,转换的逻辑已经放到了JsonHelper里面去了,这个辅助类里面分别对数值进行了获取内容,验证返回值,然后转换正确实体类几个部分的操作。

获取内容,通过辅助类HttpHelper进行,这个在我的公用类库里面,里面的逻辑主要就是通过HttpRequest进行数据的获取操作
,不在赘述

HttpHelper helper = newHttpHelper();string content = helper.GetHtml(url);

由于返回的内容,我们需要判断它是否正确返回所需的结果,如果没有,抛出自定义的相关异常,方便处理,具体如下所示。

        /// <summary>
        ///检查返回的记录,如果返回没有错误,或者结果提示成功,则不抛出异常/// </summary>
        /// <param name="content">返回的结果</param>
        /// <returns></returns>
        private static bool VerifyErrorCode(stringcontent)
{
if (content.Contains("errcode"))
{
ErrorJsonResult errorResult
= JsonConvert.DeserializeObject<ErrorJsonResult>(content);//非成功操作才记录异常,因为有些操作是返回正常的结果({"errcode": 0, "errmsg": "ok"}) if (errorResult != null && errorResult.errcode !=ReturnCode.请求成功)
{
string error = string.Format("微信请求发生错误!错误代码:{0},说明:{1}", (int)errorResult.errcode, errorResult.errmsg);
LogTextHelper.Error(errorResult);
throw new WeixinException(error);//抛出错误 }
}
return true;
}

然后转换为相应的格式,就是通过Json.NET的类库进行转换。

            T result = JsonConvert.DeserializeObject<T>(content);return result;

这样我们就可以在ConvertJson函数实体里面,完整的进行处理和转换了,转换完整的函数代码如下所示。

    /// <summary>
    ///Json字符串操作辅助类/// </summary>
    public class JsonHelper<T> where T : class, new()
{
/// <summary> ///检查返回的记录,如果返回没有错误,或者结果提示成功,则不抛出异常/// </summary> /// <param name="content">返回的结果</param> /// <returns></returns> private static bool VerifyErrorCode(stringcontent)
{
if (content.Contains("errcode"))
{
ErrorJsonResult errorResult
= JsonConvert.DeserializeObject<ErrorJsonResult>(content);//非成功操作才记录异常,因为有些操作是返回正常的结果({"errcode": 0, "errmsg": "ok"}) if (errorResult != null && errorResult.errcode !=ReturnCode.请求成功)
{
string error = string.Format("微信请求发生错误!错误代码:{0},说明:{1}", (int)errorResult.errcode, errorResult.errmsg);
LogTextHelper.Error(errorResult);
throw new WeixinException(error);//抛出错误 }
}
return true;
}
/// <summary> ///转换Json字符串到具体的对象/// </summary> /// <param name="url">返回Json数据的链接地址</param> /// <returns></returns> public static T ConvertJson(stringurl)
{
HttpHelper helper
= newHttpHelper();string content =helper.GetHtml(url);
VerifyErrorCode(content);

T result
= JsonConvert.DeserializeObject<T>(content);returnresult;
}
}

调用这个API的界面层代码如下所示(测试代码)

            IUserApi userBLL = newUserApi();
List
<string> userList = userBLL.GetUserList(token)

4、获取用户详细信息

上面的获取列表操作,相对比较简单,而且不用POST任何数据,因此通过Get协议就能获取到所需的数据。

本小节继续介绍获取用户详细信息的操作,这个操作也是通过GET协议就可以完成的。

这个API的调用定义如下所示:

http请求方式: GET
https:
//api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

通过传入一个OpenId,我们就能很好获取到用户的相关信息了。

前面小节我们已经定义了它的接口,说明了传入及返回值,根据定义,它的实现函数如下所示。

        /// <summary>
        ///获取用户基本信息/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="openId">普通用户的标识,对当前公众号唯一</param>
        /// <param name="lang">返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语</param>
        public UserJson GetUserDetail(string accessToken, string openId, Language lang =Language.zh_CN)
{
string url = string.Format("https://api.weixin.qq.com/cgi-bin/user/info?access_token={0}&openid={1}&lang={2}",
accessToken, openId, lang.ToString());

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

最后,我们结合获取用户列表和获取用户详细信息的两个API,我们看看调用的代码(测试代码)。

        private void btnGetUsers_Click(objectsender, EventArgs e)
{
IUserApi userBLL
= newUserApi();
List
<string> userList =userBLL.GetUserList(token);foreach (string openId inuserList)
{
UserJson userInfo
=userBLL.GetUserDetail(token, openId);if (userInfo != null)
{
string tips = string.Format("{0}:{1}", userInfo.nickname, userInfo.openid);
Console.WriteLine(tips);
}
}
}

本系列相关文章如下:

C#开发微信门户及应用(25)-微信企业号的客户端管理功能

C#开发微信门户及应用(24)-微信小店货架信息管理

C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试

C#开发微信门户及应用(22)-微信小店的开发和使用

C#开发微信门户及应用(21)-微信企业号的消息和事件的接收处理及解密

C#开发微信门户及应用(20)-微信企业号的菜单管理

C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)

C#开发微信门户及应用(18)-微信企业号的通讯录管理开发之成员管理

C#开发微信门户及应用(17)-微信企业号的通讯录管理开发之部门管理

C#开发微信门户及应用(16)-微信企业号的配置和使用

C#开发微信门户及应用(15)-微信菜单增加扫一扫、发图片、发地理位置功能

C#开发微信门户及应用(14)-在微信菜单中采用重定向获取用户数据

C#开发微信门户及应用(13)-使用地理位置扩展相关应用

C#开发微信门户及应用(12)-使用语音处理

C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍

C#开发微信门户及应用(10)--在管理系统中同步微信用户分组信息

C#开发微信门户及应用(9)-微信门户菜单管理及提交到微信服务器

C#开发微信门户及应用(8)-微信门户应用管理系统功能介绍

C#开发微信门户及应用(7)-微信多客服功能及开发集成

C#开发微信门户及应用(6)--微信门户菜单的管理操作

C#开发微信门户及应用(5)--用户分组信息管理

C#开发微信门户及应用(4)--关注用户列表及详细信息管理

C#开发微信门户及应用(3)--文本消息和图文消息的应答


C#开发微信门户及应用(2)--微信消息的处理和应答


C#开发微信门户及应用(1)--开始使用微信接口

在上个月的对C#开发微信门户及应用做了介绍,写过了几篇的随笔进行分享,由于时间关系,间隔了一段时间没有继续写这个系列的博客了,并不是对这个方面停止了研究,而是继续深入探索这方面的技术,为了更好的应用起来,专心做好底层的技术开发。本篇继续上一篇的介绍,主要介绍分组管理方面的开发应用,这篇的内容和上一篇,作为一个完整的用户信息和分组信息管理的组合。

1、用户分组管理内容

用户分组的引入,主要是方便管理关注者列表,以及方便向不同的组别发送消息的操作的,
一个公众账号,最多支持创建500个分组。

用户分组管理,包含下面几个方面的内容:

1 创建分组
2 查询所有分组
3 查询用户所在分组
4 修改分组名
5 移动用户分组

微信对于创建分组的定义如下所示。

http请求方式: POST(请使用https协议)
https:
//api.weixin.qq.com/cgi-bin/groups/create?access_token=ACCESS_TOKEN POST数据格式:json
POST数据例子:{
"group":{"name":"test"}}

正常返回的结果如下所示。

{"group": {"id": 107,"name": "test"}
}

其他接口,也是类似的方式,通过POST一些参数进去URL里面,获取返回的Json数据。

前面随笔定义了GroupJson的实体类信息如下所示。

    /// <summary>
    /// 分组信息
    /// </summary>
public class GroupJson : BaseJsonResult
{
/// <summary> /// 分组id,由微信分配 /// </summary> public intid { get; set; }/// <summary> /// 分组名字,UTF8编码 /// </summary> public string name { get; set; }
}

根据以上几个接口的定义,我定义了几个接口,并把它们归纳到用户管理的API接口里面。

        /// <summary>
        ///查询所有分组/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <returns></returns>
        List<GroupJson> GetGroupList(stringaccessToken);/// <summary>
        ///创建分组/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="name">分组名称</param>
        /// <returns></returns>
        GroupJson CreateGroup(string accessToken, stringname);/// <summary>
        ///查询用户所在分组/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="openid">用户的OpenID</param>
        /// <returns></returns>
        int GetUserGroupId(string accessToken, stringopenid);/// <summary>
        ///修改分组名/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="id">分组id,由微信分配</param>
        /// <param name="name">分组名字(30个字符以内)</param>
        /// <returns></returns>
        CommonResult UpdateGroupName(string accessToken, int id, stringname);/// <summary>
        ///移动用户分组/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="openid">用户的OpenID</param>
        /// <param name="to_groupid">分组id</param>
        /// <returns></returns>
        CommonResult MoveUserToGroup(string accessToken, string openid, int to_groupid);

2、用户分组管理接口的实现

2.1 创建用户分组

为了解析如何实现创建用户分组的POST数据操作,我们来一步步了解创建用户的具体过程。

首先需要创建一个动态定义的实体类信息,它包含几个需要提及的属性,如下所示。

            string url = string.Format("https://api.weixin.qq.com/cgi-bin/groups/create?access_token={0}", accessToken);var data = new{
group
= new{
name
=name
}
};
string postData = data.ToJson();

其中我们把对象转换为合适的Json数据操作,放到了扩展方法ToJson里面了,这个主要就是方便把动态定义的实体类转换Json内容,主要就是调用Json.NET的序列号操作。

        /// <summary>
        ///把对象为json字符串/// </summary>
        /// <param name="obj">待序列号对象</param>
        /// <returns></returns>
        public static string ToJson(this objectobj)
{
returnJsonConvert.SerializeObject(obj, Formatting.Indented);
}

准备好Post的数据后,我们就进一步看看获取数据并转换为合适格式的操作代码。

            GroupJson group = null;
CreateGroupResult result
= JsonHelper<CreateGroupResult>.ConvertJson(url, postData);if (result != null)
{
group
=result.group;
}

其中POST数据并转换为合适格式实体类的操作,放在了ConvertJson方法里面,这个方法的定义如下所示,里面的HttpHelper是我公用类库的辅助类,主要就是调用底层的httpWebRequest对象方法,进行数据的提交,并获取返回结果。

        /// <summary>
        ///转换Json字符串到具体的对象/// </summary>
        /// <param name="url">返回Json数据的链接地址</param>
        /// <param name="postData">POST提交的数据</param>
        /// <returns></returns>
        public static T ConvertJson(string url, stringpostData)
{
HttpHelper helper
= newHttpHelper();string content = helper.GetHtml(url, postData, true);
VerifyErrorCode(content);

T result
= JsonConvert.DeserializeObject<T>(content);returnresult;
}

这样,完整的创建用户分组的操作函数如下所示。

        /// <summary>
        ///创建分组/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="name">分组名称</param>
        /// <returns></returns>
        public GroupJson CreateGroup(string accessToken, stringname)
{
string url = string.Format("https://api.weixin.qq.com/cgi-bin/groups/create?access_token={0}", accessToken);var data = new{
group
= new{
name
=name
}
};
string postData =data.ToJson();

GroupJson group
= null;
CreateGroupResult result
= JsonHelper<CreateGroupResult>.ConvertJson(url, postData);if (result != null)
{
group
=result.group;
}
returngroup;
}

2.2 查询所有分组

查询所有分组,可以把服务器上的分组全部获取下来,也就是每个分组的ID和名称。

        /// <summary>
        ///查询所有分组/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <returns></returns>
        public List<GroupJson> GetGroupList(stringaccessToken)
{
string url = string.Format("https://api.weixin.qq.com/cgi-bin/groups/get?access_token={0}", accessToken);

List
<GroupJson> list = new List<GroupJson>();
GroupListJsonResult result
= JsonHelper<GroupListJsonResult>.ConvertJson(url);if (result != null && result.groups != null)
{
list.AddRange(result.groups);
}
returnlist;
}

2.3 查询用户所在分组

每个用户都属于一个分组,默认在
未分组
这个分组里面,我们可以通过API获取用户的分组信息,也就是获取所在用户分组的ID。

        /// <summary>
        ///查询用户所在分组/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="openid">用户的OpenID</param>
        /// <returns></returns>
        public int GetUserGroupId(string accessToken, stringopenid)
{
string url = string.Format("https://api.weixin.qq.com/cgi-bin/groups/getid?access_token={0}", accessToken);var data = new{
openid
=openid
};
string postData =data.ToJson();int groupId = -1;
GroupIdJsonResult result
= JsonHelper<GroupIdJsonResult>.ConvertJson(url, postData);if (result != null)
{
groupId
=result.groupid;
}
returngroupId;
}

2.4 修改分组名称

也可以在实际中,调整用户所在的分组,操作代码如下。

        /// <summary>
        ///修改分组名/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="id">分组id,由微信分配</param>
        /// <param name="name">分组名字(30个字符以内)</param>
        /// <returns></returns>
        public CommonResult UpdateGroupName(string accessToken, int id, stringname)
{
string url = string.Format("https://api.weixin.qq.com/cgi-bin/groups/update?access_token={0}", accessToken);var data = new{
group
= new{
id
=id,
name
=name
}
};
string postData =data.ToJson();returnHelper.GetExecuteResult(url, postData);
}

这里的返回值CommonResult是,一个实体类,包含了bool的成功与否的标志,以及String类型的错误信息(如果有的话)。

对于这个GetExecuteResult函数体,里面主要就是提交数据,然后获取结果,并根据结果进行处理的函数。

        /// <summary>
        ///通用的操作结果/// </summary>
        /// <param name="url">网页地址</param>
        /// <param name="postData">提交的数据内容</param>
        /// <returns></returns>
        public static CommonResult GetExecuteResult(string url, string postData = null)
{
CommonResult success
= newCommonResult();try{
ErrorJsonResult result;
if (postData != null)
{
result
= JsonHelper<ErrorJsonResult>.ConvertJson(url, postData);
}
else{
result
= JsonHelper<ErrorJsonResult>.ConvertJson(url);
}
if (result != null)
{
success.Success
= (result.errcode ==ReturnCode.请求成功);
success.ErrorMessage
=result.errmsg;
}
}
catch(WeixinException ex)
{
success.ErrorMessage
=ex.Message;
}
returnsuccess;
}
}

上面红色部分的意思,就是转换为实体类的时候,如果错误是微信里面定义的,那么记录错误信息,其他异常我不处理(也就是抛出去)。

2.5 移动用户到新的分组

移动用户到新的分组的操作和上面小节的差不多,具体看代码。

        /// <summary>
        ///移动用户分组/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="openid">用户的OpenID</param>
        /// <param name="to_groupid">分组id</param>
        /// <returns></returns>
        public CommonResult MoveUserToGroup(string accessToken, string openid, intto_groupid)
{
string url = string.Format("https://api.weixin.qq.com/cgi-bin/groups/members/update?access_token={0}", accessToken);var data = new{
openid
=openid,
to_groupid
=to_groupid
};
string postData =data.ToJson();returnHelper.GetExecuteResult(url, postData);
}

3、用户分组接口的调用

上面小节,定义并实现了用户分组的各类接口,所有的用户相关的都已经毫无保留贴出代码,它的调用操作如下代码所示(测试代码)。

        private void btnGetGroupList_Click(objectsender, EventArgs e)
{
IUserApi userBLL
= newUserApi();
List
<GroupJson> list =userBLL.GetGroupList(token);foreach (GroupJson info inlist)
{
string tips = string.Format("{0}:{1}", info.name, info.id);
Console.WriteLine(tips);
}
}
private void btnFindUserGroup_Click(objectsender, EventArgs e)
{
IUserApi userBLL
= newUserApi();int groupId =userBLL.GetUserGroupId(token, openId);string tips = string.Format("GroupId:{0}", groupId);
Console.WriteLine(tips);
}
private void btnCreateGroup_Click(objectsender, EventArgs e)
{
IUserApi userBLL
= newUserApi();
GroupJson info
= userBLL.CreateGroup(token, "创建测试分组");if (info != null)
{
string tips = string.Format("GroupId:{0} GroupName:{1}", info.id, info.name);
Console.WriteLine(tips);
string newName = "创建测试修改";
CommonResult result
=userBLL.UpdateGroupName(token, info.id, newName);
Console.WriteLine(
"修改分组名称:" + (result.Success ? "成功" : "失败:" +result.ErrorMessage));
}
}
private void btnUpdateGroup_Click(objectsender, EventArgs e)
{
int groupId = 666666;string newName = "创建测试修改";

IUserApi userBLL
= newUserApi();
CommonResult result
=userBLL.UpdateGroupName(token, groupId, newName);
Console.WriteLine(
"修改分组名称:" + (result.Success ? "成功" : "失败:" +result.ErrorMessage));
}
private void btnMoveToGroup_Click(objectsender, EventArgs e)
{
int togroup_id = 666666;//输入分组ID if (togroup_id > 0)
{
IUserApi userBLL
= newUserApi();
CommonResult result
=userBLL.MoveUserToGroup(token, openId, togroup_id);

Console.WriteLine(
"移动用户分组名称:" + (result.Success ? "成功" : "失败:" +result.ErrorMessage));
}
}

了解了上面的代码和调用规则,我们就能通过API进行用户分组信息的管理了。通过在应用程序中集成相关的接口代码,我们就能够很好的控制我们的关注用户列表和用户分组信息。从而为我们下一步用户的信息推送打好基础。

C#开发微信门户及应用(25)-微信企业号的客户端管理功能

C#开发微信门户及应用(24)-微信小店货架信息管理

C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试

C#开发微信门户及应用(22)-微信小店的开发和使用

C#开发微信门户及应用(21)-微信企业号的消息和事件的接收处理及解密

C#开发微信门户及应用(20)-微信企业号的菜单管理

C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)

C#开发微信门户及应用(18)-微信企业号的通讯录管理开发之成员管理

C#开发微信门户及应用(17)-微信企业号的通讯录管理开发之部门管理

C#开发微信门户及应用(16)-微信企业号的配置和使用

C#开发微信门户及应用(15)-微信菜单增加扫一扫、发图片、发地理位置功能

C#开发微信门户及应用(14)-在微信菜单中采用重定向获取用户数据

C#开发微信门户及应用(13)-使用地理位置扩展相关应用

C#开发微信门户及应用(12)-使用语音处理

C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍

C#开发微信门户及应用(10)--在管理系统中同步微信用户分组信息

C#开发微信门户及应用(9)-微信门户菜单管理及提交到微信服务器

C#开发微信门户及应用(8)-微信门户应用管理系统功能介绍

C#开发微信门户及应用(7)-微信多客服功能及开发集成

C#开发微信门户及应用(6)--微信门户菜单的管理操作

C#开发微信门户及应用(5)--用户分组信息管理

C#开发微信门户及应用(4)--关注用户列表及详细信息管理

C#开发微信门户及应用(3)--文本消息和图文消息的应答


C#开发微信门户及应用(2)--微信消息的处理和应答


C#开发微信门户及应用(1)--开始使用微信接口

在很多情况下,事务是个很有用的东西,可以把一系列的操作组合成一个原子粒度的操作,一旦组合中某个地方出错,可以整个干净的进行滚回,不会留下脏数据;除此之外,事务还能提高批量操作的效率,如在本地SQLite数据库里面,批量插入1万条数据,那么使用事务和没有使用事务,速度上至少差别几十到上百倍的差异。既然事务有完整性和速度性的差异,因此,基于上述原因,我们在很多情况下最好使用事务进行操作。本文主要介绍在开发框架中如何整合事务的操作,并介绍在各个分层中的事务使用案例。

由于我介绍的相关框架,主要是采用了微软的Enterprise Library的数据库访问模块,因此它能够很好抽象各种数据库的事务,以适应各种不同数据库的事务处理。使用微软的Enterprise Library模块,可以很好支持SQLSever、Oracle、Mysql、Access、SQLite等数据库。

1、数据访问层中的事务操作

1.1 数据访问层的事务接口定义

由于使用事务操作,因此在底层的模块里面,也就是这里的数据访问层,一般需要一个事务对象的参数。如下代码是一个数据访问层的接口定义。

    /// <summary>
    ///数据访问层的接口/// </summary>
    public interface IBaseDAL<T> whereT : BaseEntity
{
/// <summary> ///插入指定对象到数据库中/// </summary> /// <param name="obj">指定的对象</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回True</returns> bool Insert(T obj, DbTransaction trans = null);/// <summary> ///根据指定对象的ID,从数据库中删除指定对象/// </summary> /// <param name="key">指定对象的ID</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> bool Delete(object key, DbTransaction trans = null);/// <summary> ///更新对象属性到数据库中/// </summary> /// <param name="obj">指定的对象</param> /// <param name="primaryKeyValue">主键的值</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> bool Update(T obj, object primaryKeyValue, DbTransaction trans = null);/// <summary> ///查询数据库,检查是否存在指定ID的对象/// </summary> /// <param name="key">对象的ID值</param> /// <param name="trans">事务对象</param> /// <returns>存在则返回指定的对象,否则返回Null</returns> T FindByID(object key, DbTransaction trans = null);

.....................
//其他操作 }

从上面的代码上,我们可以看到,里面的增删改查等操作,最后都带一个trans的事务对象参数,这个参数默认为null,也就是可选参数的做法,这个方法就提供了两个重载的方法供我们使用。

看到这里,可能有些人提出疑问,为什么查找方法也传入事务对象,这个因为事务是一个排斥性操作,一旦启动了事务,可能这个表的其他操作会被锁定,但在这个事务内操作确实可以允许的,如果你使用SQLite这种单机版的数据库,在一个地方采用事务操作一个表,在事务内部接着不使用事务进行表的任何操作将不会被允许,数据库提示出错信息的。

基于这个原因,所有表操作的接口,都应该提供事务性的操作接口,也就是提供一个事务性的对象参数。在接口的实现里面,判断事务对象是否为空,然后进行相应的处理即可。

1.2 在DAL层自定义函数的事务操作

由于在IBaseDAL里面已经定义了很多事务性的接口,因此数据访问层的基类里面也已经实现了很多相关的基础操作。

因此数据访问层
DAL层里面,如自己定义的实现函数,调用这些基础函数进行处理就可以了。自定义函数对事务的操作处理,代码如下所示。

        /// <summary>
        ///调整客户的组别/// </summary>
        /// <param name="customerId">客户ID</param>
        /// <param name="groupIdList">客户分组Id集合</param>
        /// <returns></returns>
        public bool ModifyCustomerGroup(string customerId, List<string>groupIdList)
{
bool result = false;
DbTransaction trans
= base.CreateTransaction();if (trans != null)
{
string sql = string.Format("Delete from T_CRM_CustomerGroup_Customer where Customer_ID='{0}'", customerId);base.SqlExecute(sql, trans);foreach (string groupId ingroupIdList)
{
sql
= string.Format("Insert into T_CRM_CustomerGroup_Customer(Customer_ID,CustomerGroup_ID) values('{0}', '{1}')", customerId, groupId);base.SqlExecute(sql, trans);
}
try{
trans.Commit();
result
= true;
}
catch{
trans.Rollback();
throw;
}
}
returnresult;
}

2、业务逻辑层的事务操作

业务逻辑层BLL层是对数据访问层的更高一层的封装,它也相应提供相应的事务对象接口,以方便外部的调用。

它的业务类里面,自定义函数对事务的调用操作如下所示。

        /// <summary>
        ///把报价单转换为销售订单/// </summary>
        /// <param name="quotationNo">报价单编号</param>
        /// <returns></returns>
        public bool ConvertToOrder(string orderNo, intuserId)
{
bool result = false;

DbTransaction trans
= baseDal.CreateTransaction();if (trans != null)
{
SellInfo sellInfo
=ConvertSellInfo(orderNo, userId, trans);
List
<OrderDetailInfo> detailList = new List<OrderDetailInfo>();if (sellInfo != null)
{
detailList
=ConvertOrderDetal(sellInfo, orderNo, trans);
}
bool success = BLLFactory<Sell>.Instance.Insert(sellInfo, trans);if(success)
{
foreach (OrderDetailInfo info indetailList)
{
BLLFactory
<OrderDetail>.Instance.Insert(info, trans);
}
}
try{
trans.Commit();
result
= true;
}
catch{
trans.Rollback();
throw;//重新抛出异常 }
}
returnresult;
}

另一例子如下所示。

        /// <summary>
        ///删除报价单及明细信息/// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public bool DeleteQuotationRelated(stringid)
{
bool result = false;
DbTransaction trans
= CreateTransaction();if (trans != null)
{
QuotationInfo info
=baseDal.FindByID(id, trans);if (info != null)
{
List
<QuotationDetailInfo> detailList = BLLFactory<QuotationDetail>.Instance.FindByOrderNo(info.HandNo, trans);foreach (QuotationDetailInfo detailInfo indetailList)
{
BLLFactory
<QuotationDetail>.Instance.Delete(detailInfo.ID, trans);
}
//最后删除主表订单数据 baseDal.Delete(id, trans);try{
trans.Commit();
result
= true;
}
catch(Exception ex)
{
trans.Rollback();
LogTextHelper.Error(ex);
throw;
}
}
}
returnresult;
}
}

3、Winform界面层对事务的调用

由于Winform界面层,直接调用BLL层的相应接口,进行数据的操作的,因此我们也可以在界面层创建相应的事务对象,然后在界面层操作事务。

界面层调用事务处理,操作代码如下所示。

            using (DbTransaction trans = BLLFactory<Function>.Instance.CreateTransaction())
{
try{if (trans != null)
{
bool sucess = BLLFactory<Function>.Instance.Insert(mainInfo, trans);if(sucess)
{
FunctionInfo subInfo
= null;int sortCodeIndex = 1;#region 子功能操作 if(chkAdd.Checked)
{
subInfo
=CreateSubFunction(mainInfo);
subInfo.SortCode
= (sortCodeIndex++).ToString("D2");
subInfo.ControlID
= string.Format("{0}/Add", mainInfo.ControlID);
subInfo.Name
= string.Format("添加{0}", mainInfo.Name);

BLLFactory
<Function>.Instance.Insert(subInfo, trans);
}
if(chkDelete.Checked)
{
subInfo
=CreateSubFunction(mainInfo);
subInfo.SortCode
= (sortCodeIndex++).ToString("D2");
subInfo.ControlID
= string.Format("{0}/Delete", mainInfo.ControlID);
subInfo.Name
= string.Format("删除{0}", mainInfo.Name);
BLLFactory
<Function>.Instance.Insert(subInfo, trans);
}
......................//其他事务操作
#endregiontrans.Commit();
ProcessDataSaved(
this.btnSave, newEventArgs());//this.DialogResult = System.Windows.Forms.DialogResult.OK; MessageDxUtil.ShowTips("保存成功");
}
else{
MessageDxUtil.ShowTips(
"保存失败");
}
}
}
catch(Exception ex)
{
if (trans != null)
{
trans.Rollback();
}


LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}

以上就是我对不同分层中使用事务对象进行各种操作的处理,由于事务对象会对表进行某种锁定操作,因此数据库的整体性能可能有所降低,但是在保证某种组合操作的原子性,以及批量数据库操作这两种方式,该出手时还是要出手,这样能更好提高完整性和处理的高效性。