微信公众号开发C#系列-5、用户和用户组管理-支持同步

1、概述

眼前时下流行的经济有个叫粉丝经济,粉丝带动收益。一个好运营良好的公众号肯定会有一大批的粉丝团,如何挖掘粉丝来产生效益,是微信营销的关键。微信公众号后台本身提供了粉丝(用户)与用户分组的管理,但这些都是存放在微信的服务器,我们不好拿来分析应用。因为我们需要把我们的粉丝放在我们自己的库中,以方便做各种应用分析。微信公众号提供了相应的接口方便我们调用,可方便的把用户同步到本地,这样我们可以自己为用户定义更多的信息,以及与本地的业务更好的对接起来。

2、本地存放微信粉丝与分组的表结构

在微信开发过程中,一般定义表结构我们可以直接把微信返回的对象数据如这儿的用户返回的信息直接存放到我们的库表结构,结构类型可以一至,再加上我们需要额外添加的扩展字段做业务上的处理。如下是我们建立的微信用户与分组的表结构。

微信用户表结构

微信分组表结构

3、主要接口实现方式

本文中所有接口的实现方式我们使用了开源的Senparc.Weixin提供的专业的微信操作SDK来快速完成操作。Senparc.Weixin SDK相关文章可参考文章末尾的参考文章

3.1、同步指定用户到本地

微信的用户信息随时在变,如:用户昵称,地址,头像什么的随时在修改,我们本地库中的数据也应该变才可以。如果用户组非常多,如几万的粉丝,如果每次都通过一键同步所有用户来做维护,效率会非常低,这时我们就需要针对特定的用户手动同步其用户基本信息。这时我们就需要使用微信提供的获取用户基本信息接口来同步指定用户。在关注者与公众号产生消息交互后,公众号可获得关注者的OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的。对于不同公众号,同一用户的openid不同)。公众号可通过本接口来根据OpenID获取用户基本信息,包括昵称、头像、性别、所在城市、语言和关注时间。

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

参数说明

参数	        是否必须	说明
access_token	是	        调用接口凭证
openid	        是	        普通用户的标识,对当前公众号唯一
lang	        否	        返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语

具体的接口详细介绍及使用方法可参考微信公众平台技术文档-获取用户基本信息

使用Senparc.Weixin SDK中的Senparc.Weixin.MP.AdvancedAPIs.UserApi.BatchGetUserInfo批量获取用户基本信息,就可以手动同步选定的用户列表。其实BatchGetUserInfo接口参考:

/// <summary>
/// 批量获取用户基本信息
/// </summary>
/// <param name="accessTokenOrAppId"></param>
/// <param name="userList"></param>
/// <param name="timeOut"></param>
/// <returns></returns>
public static BatchGetUserInfoJson BatchGetUserInfo(string accessTokenOrAppId, List<BatchGetUserInfoData> userList, int timeOut = Config.TIME_OUT)
{
	return ApiHandlerWapper.TryCommonApi(accessToken =>
	{
		string url = string.Format("https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token={0}",
			accessToken);
		var data = new
		{
			user_list = userList,
		};
		return CommonJsonSend.Send<BatchGetUserInfoJson>(accessToken, url, data, timeOut: timeOut);

	}, accessTokenOrAppId);
}

具体业务实现代码参考:

/// <summary>
/// 手动同步用户
/// </summary>
/// <param name="userIds">待同步的用户主键列表(逗号分隔)</param>
/// <returns></returns>
[HttpPost]
[ValidateInput(false)]
[LoginAuthorize]
public ActionResult SyncUser(string userIds)
{
    int returnValue = 0;
    UserInfo curUserInfo = ManageProvider.Provider.Current();
    if (!string.IsNullOrWhiteSpace(userIds))
    {
        //填充数据
        string[] arrs = userIds.Split(',');
        List<BatchGetUserInfoData> list = new List<BatchGetUserInfoData>();
        foreach (var m in arrs)
        {
            list.Add(new BatchGetUserInfoData()
            {
                openid = m,
                lang = "zh-CN",
                LangEnum = Senparc.Weixin.Language.zh_CN
            });
        }

        //批量同步数据
        WeixinOfficialAccountEntity accountModel = RDIFrameworkService.Instance.WeixinBasicService.GetCurrentOfficialAccountEntity(curUserInfo);
        try
        {
            var batchList = Senparc.Weixin.MP.AdvancedAPIs.UserApi.BatchGetUserInfo(accountModel.AccessToken, list);
            foreach (var info in batchList.user_info_list)
            {
                WeixinUserEntity userModel = RDIFrameworkService.Instance.WeixinBasicService.GetUserEntity(curUserInfo, info.openid);
                if (userModel != null)
                {
                    userModel.City = info.city;
                    userModel.OpenId = info.openid;
                    userModel.Id = info.openid;
                    userModel.HeadImgUrl = info.headimgurl;
                    userModel.Subscribe = info.subscribe;
                    userModel.SubscribeTime = DateTimeHelper.GetTimeByLong(info.subscribe_time * 1000);//注意:单位为秒,不是毫秒,要转换为毫秒要乘以1000,这个官网开发文档没有说明。
                    userModel.Language = info.language;
                    userModel.NickName = info.nickname;
                    userModel.Province = info.province;
                    userModel.Sex = info.sex;
                    userModel.UnionId = info.unionid;
                    userModel.Contry = info.country;
                    userModel.Remark = info.remark;
                    userModel.GroupId = BusinessLogic.ConvertToString(info.groupid);
                    returnValue += RDIFrameworkService.Instance.WeixinBasicService.UpdateUser(userModel);
                }
            }
        }
        catch (Exception ex)
        {
            if (ex.Message.Contains("找不到方法"))
            {
                return Content(new JsonMessage { Success = false, Data = "-1", Type = ResultType.Error, Message = "Token已过期..." }.ToString());
            }
            else
            {
                return Content(new JsonMessage { Success = false, Data = "-1", Type = ResultType.Error, Message = RDIFramework.Utilities.RDIFrameworkMessage.MSG3020 + "错误信息:" + ex.Message }.ToString());
            }
        }
        return Content(returnValue > 0
                ? new JsonMessage { Success = true, Data = "1", Type = ResultType.Success, Message = RDIFrameworkMessage.MSG3010 }.ToString()
                : new JsonMessage { Success = false, Data = "0", Type = ResultType.Warning, Message = RDIFrameworkMessage.MSG3020 }.ToString());
    }
    else
    {
        return Content(new JsonMessage { Success = false, Data = "-1", Type = ResultType.Error, Message = RDIFramework.Utilities.RDIFrameworkMessage.MSG3020 }.ToString());
    }
}

3.2、一键同步所有用户到本地

在进行微信公众号开发之初,我们公众号已经有了一些粉丝数,这时我们可能就需要一键把这些用户全部同步到我们本地库。要做到一键同步需要调用两个接口,两步来完成。
具体的接口详细介绍及使用方法可参考微信公众平台技术文档-获取用户列表
第一步:获取关注列列表OpenId,接口为:

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

参数	        是否必须	说明
access_token	是	        调用接口凭证
next_openid	    是	        第一个拉取的OPENID,不填默认从头开始拉取

公众号可通过本接口来获取帐号的关注者列表,关注者列表由一串OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的)组成。一次拉取调用最多拉取10000个关注者的OpenID,可以通过多次拉取的方式来满足需求。
当公众号关注者数量超过10000时,可通过填写next_openid的值,从而多次拉取列表的方式来满足需求。具体而言,就是在调用接口时,将上一次调用得到的返回中的next_openid值,作为下一次调用中的next_openid值。

第二步:通过返回的用户OpenId列表,得到第一个用户的基本信息再同步到本地库中,接口为:

接口调用请求说明
http请求方式: GET
https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
参数说明

参数	        是否必须	说明
access_token	是	        调用接口凭证
openid	        是	        普通用户的标识,对当前公众号唯一
lang	        否	        返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语

具体代码参考,使用Senparc.Weixin SDK中的Senparc.Weixin.MP.AdvancedAPIs.UserApi.Get得到关注者的OpenId列表,再遍历得到的OpenId列表得到用户的基本信息,接口为:Senparc.Weixin.MP.AdvancedAPIs.UserApi.Info
其中UserApi.Get方法实现代码参考:

/// <summary>
/// 获取关注者OpenId信息
/// </summary>
/// <param name="accessTokenOrAppId">AccessToken或AppId(推荐使用AppId,需要先注册)</param>
/// <param name="nextOpenId"></param>
/// <returns></returns>
[ApiBind(NeuChar.PlatformType.WeChat_OfficialAccount, "UserApi.Get", true)]
public static OpenIdResultJson Get(string accessTokenOrAppId, string nextOpenId)
{
	return ApiHandlerWapper.TryCommonApi(accessToken =>
	{
		string url = string.Format("https://api.weixin.qq.com/cgi-bin/user/get?access_token={0}", accessToken.AsUrlData());
		if (!string.IsNullOrEmpty(nextOpenId))
		{
			url += "&next_openid=" + nextOpenId;
		}
		return HttpUtility.Get.GetJson<OpenIdResultJson>(url);

	}, accessTokenOrAppId);
}

通过下面的代码就可以详细明白操作的步骤,除去相关的业务应用,真正的代码很少。

/// <summary>
/// 一键同步当前操作公众号下所有用户
/// 说明:一般用于粉丝没有包含在本地库中的同步到同地库中
/// </summary>        
/// <returns></returns>
[HttpPost]
[ValidateInput(false)]
[LoginAuthorize]
public ActionResult SyncAllUser()
{
    UserInfo curUserInfo = ManageProvider.Provider.Current();
    int returnAddValue = 0, returnUpdateValue = 0;

    try
    {
        //批量同步数据
        WeixinOfficialAccountEntity accountEntity = RDIFrameworkService.Instance.WeixinBasicService.GetCurrentOfficialAccountEntity(curUserInfo);
        //获取关注着OpenId信息
        var openIdResultJson = Senparc.Weixin.MP.AdvancedAPIs.UserApi.Get(accountEntity.AccessToken, "");

        if (openIdResultJson != null && openIdResultJson.count > 0)
        {
            foreach (string openId in openIdResultJson.data.openid)
            {
                UserInfoJson remoteUserInfo = Senparc.Weixin.MP.AdvancedAPIs.UserApi.Info(accountEntity.AccessToken, openId, Senparc.Weixin.Language.zh_CN);
                var localUserEntity = RDIFrameworkService.Instance.WeixinBasicService.GetCurOfficialAccountUserByOpenId(curUserInfo, accountEntity.Id, openId);
                if (localUserEntity == null)
                {
                    if (remoteUserInfo != null)
                    {
                        localUserEntity = new WeixinUserEntity
                        {
                            OpenId = remoteUserInfo.openid,
                            City = remoteUserInfo.city,
                            Id = remoteUserInfo.openid,
                            OfficialAccountId = accountEntity.Id,
                            HeadImgUrl = remoteUserInfo.headimgurl,
                            SubscribeTime = DateTimeHelper.GetTimeByLong(remoteUserInfo.subscribe_time * 1000),//注意:单位为秒,不是毫秒,要转换为毫秒要乘以1000,这个官网开发文档没有说明。
                            Language = remoteUserInfo.language,
                            Subscribe = remoteUserInfo.subscribe,
                            NickName = remoteUserInfo.nickname,
                            Province = remoteUserInfo.province,
                            Sex = remoteUserInfo.sex,
                            Contry = remoteUserInfo.country,
                            Remark = remoteUserInfo.remark,
                            UnionId = remoteUserInfo.unionid,
                            GroupId = BusinessLogic.ConvertToString(remoteUserInfo.groupid, null)
                        };
                        returnAddValue += (string.IsNullOrEmpty(RDIFrameworkService.Instance.WeixinBasicService.AddUser(localUserEntity)) ? 0 : 1);
                    }
                }
                else
                {
                    //取消订阅后又重新订阅了,需要修改本地
                    if (remoteUserInfo != null && localUserEntity.Subscribe != remoteUserInfo.subscribe)
                    {
                        localUserEntity.City = remoteUserInfo.city;
                        localUserEntity.OpenId = remoteUserInfo.openid;
                        localUserEntity.Id = remoteUserInfo.openid;
                        localUserEntity.HeadImgUrl = remoteUserInfo.headimgurl;
                        localUserEntity.Subscribe = remoteUserInfo.subscribe;
                        localUserEntity.SubscribeTime = DateTimeHelper.GetTimeByLong(remoteUserInfo.subscribe_time * 1000);//注意:单位为秒,不是毫秒,要转换为毫秒要乘以1000,这个官网开发文档没有说明。
                        localUserEntity.Language = remoteUserInfo.language;
                        localUserEntity.NickName = remoteUserInfo.nickname;
                        localUserEntity.Province = remoteUserInfo.province;
                        localUserEntity.Sex = remoteUserInfo.sex;
                        localUserEntity.UnionId = remoteUserInfo.unionid;
                        localUserEntity.Contry = remoteUserInfo.country;
                        localUserEntity.Remark = remoteUserInfo.remark;
                        localUserEntity.GroupId = BusinessLogic.ConvertToString(remoteUserInfo.groupid);
                        returnUpdateValue += RDIFrameworkService.Instance.WeixinBasicService.UpdateUser(localUserEntity);
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        if (ex.Message.Contains("找不到方法"))
        {
            return Content(new JsonMessage { Success = false, Data = "-1", Type = ResultType.Error, Message = "Token已过期..." }.ToString());
        }
        else
        {
            return Content(new JsonMessage { Success = false, Data = "-1", Type = ResultType.Error, Message = RDIFramework.Utilities.RDIFrameworkMessage.MSG3020 + "错误信息:" + ex.Message }.ToString());
        }
    }

    return Content((returnAddValue > 0 || returnUpdateValue > 0)
            ? new JsonMessage { Success = true, Data = "1", Type = ResultType.Success, Message = RDIFrameworkMessage.MSG3010 + ",新增:" + returnAddValue.ToString() + "个粉丝,修改:" + returnUpdateValue.ToString() + " 个用户。" }.ToString()
            : new JsonMessage { Success = false, Data = "0", Type = ResultType.Warning, Message = "操作完成,无新增,无修改!" }.ToString());
}

4、关注与取消关注时自动同步本地用户情况

上面的方式都是对已经关注的用户做同步处理。如果我们在用户关注的同时就自动把关注的用户同步到本地库,这样就更加的方便。
当我们关注某些微信公众号的时候,有的公众号会立即给我们回复一条信息。这是如何实现的呢?原来在用户在关注与取消关注公众号时,微信会把这个事件推送到开发者填写的URL。方便开发者给用户下发欢迎消息或者做帐号的解绑。为保护用户数据隐私,开发者收到用户取消关注事件时需要删除该用户的所有信息。

我们是基于微信的第三方平台来做二次开发,开发的依据必须是官方的API也就是开发文档。所以,我们要先查询开发文档来找到关注和取关事件说明。访问url为:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140454
我们的公众号服务接收到微信服务器回传的xml信息,从中获取MsgType和Event的值,可以区分出用户的关注和取消关注的行为,对不同的行为程序可以做出不同的响应。

我们直接使用Senparc.Weixin SDK提供的接口,重载OnEvent_SubscribeRequest-订阅与OnEvent_UnsubscribeRequest-取消订阅事件处理即可。

4.1、订阅(关注)时的处理

关注事件代码参考:

/// <summary>
/// 订阅(关注)事件
/// </summary>
/// <returns></returns>
public override IResponseMessageBase OnEvent_SubscribeRequest(RequestMessageEvent_Subscribe requestMessage)
{
    var responseMessage = ResponseMessageBase.CreateFromRequestMessage<ResponseMessageText>(requestMessage);
    responseMessage.Content = "欢迎关注!";
    return responseMessage;
}

上面的代码只要用户关注了关注号就会自动回复:欢迎关注!
如果要回复图文模式如下图所示:

关注公众号

关注成功后回复

图文回复代码参考:

/// <summary>
/// 订阅(关注)事件
/// </summary>
/// <returns></returns>
public override IResponseMessageBase OnEvent_SubscribeRequest(RequestMessageEvent_Subscribe requestMessage)
{
    var responseMessage = CreateResponseMessage<ResponseMessageNews>();
    responseMessage.Articles.Add(new Article()
    {
        Title = "国思公众号",
        Description = "欢迎关注国思软件公众号,更多内容移步到官网,多谢!",
        PicUrl = "http://www.rdiframework.net/img/weixing-ma.png",
        Url = "http://www.rdiframework.net/"
    });
    return responseMessage;
}

上面的代码就可以实现关注成功后自动图文回复。我们还可以在关注时处理相关的业务逻辑,如:关注成功自动把关注用户同步到本地库。同样的在关注事件中处理,代码参考:

/// <summary>
/// 订阅(关注)事件
/// </summary>
/// <returns></returns>
public override IResponseMessageBase OnEvent_SubscribeRequest(RequestMessageEvent_Subscribe requestMessage)
{
	//获得当前公众号
	WeixinOfficialAccountEntity account = RDIFrameworkService.Instance.WeixinBasicService.GetOfficialAccountEntity();
	//将用户提取到本地数据库
	WeixinUserEntity userEntity = new WeixinUserEntity();

	UserInfoJson userJson = UserApi.Info(account.AccessToken, requestMessage.FromUserName);
	userEntity.Id = BusinessLogic.NewGuid();
	if (!string.IsNullOrEmpty(userJson.openid))
	{
		userEntity.Id = userJson.openid;
	}	
	
	userEntity.OpenId = userJson.openid;
	userEntity.NickName = userJson.nickname;
	userEntity.Sex = userJson.sex;
	userEntity.Language = userJson.language;
	userEntity.City = userJson.city;
	userEntity.Province = userJson.province;
	userEntity.Contry = userJson.country;
	userEntity.HeadImgUrl = userJson.headimgurl;
	userEntity.SubscribeTime = DateTimeHelper.GetTimeByLong(userJson.subscribe_time * 1000);//注意:单位为秒,不是毫秒,要转换为毫秒要乘以1000,这个官网开发文档没有说明。
	userEntity.UnionId = userJson.unionid;
	userEntity.Remark = userJson.remark;
	userEntity.GroupId = userJson.groupid.ToString();
	userEntity.TagIdList = string.Join(",", userJson.tagid_list.ToArray());
	userEntity.Subscribe = userJson.subscribe;
	userEntity.OfficialAccountId = account.Id;
	userEntity.CreateBy = "WeiXinServer";
	string returnValue = RDIFrameworkService.Instance.WeixinBasicService.AddUser(userEntity); //增加用户	

	//订阅回复
	var responseMessage = CreateResponseMessage<ResponseMessageNews>();
	responseMessage.Articles.Add(new Article()
	{
		Title = "国思公众号",
		Description = "欢迎关注国思软件公众号,更多内容移步到官网,多谢!",
		PicUrl = "http://www.rdiframework.net/img/weixing-ma.png",
		Url = "http://www.rdiframework.net/"
	});
	return responseMessage;
}

4.2、取消关注时的处理

取消关注我们可以对事件OnEvent_UnsubscribeRequest做处理。参考代码:

/// <summary>
/// 退订
/// 实际上用户无法收到非订阅账号的消息,所以这里可以随便写。
/// unsubscribe事件的意义在于及时删除网站应用中已经记录的OpenID绑定,消除冗余数据。并且关注用户流失的情况。
/// </summary>
/// <returns></returns>
public override IResponseMessageBase OnEvent_UnsubscribeRequest(RequestMessageEvent_Unsubscribe requestMessage)
{
	var responseMessage = base.CreateResponseMessage<ResponseMessageText>();
	responseMessage.Content = "有空再来";
	return responseMessage;
}

在用户取消关注事件中,我们还可以加入我们自己的业务逻辑,对取消关注的用户做业务上的处理,用户可根据实际的情况来增加自己要处理的业务。

5、用户分组管理

新关注的用户默认会自动分到“未分组”分组中,我们可以根据实现需要建立我们自己的分组并把用户移动到对应的分组中,以方便管理与业务应用的处理。

5.1、创建分组

每个帐号下最多只能创建1000个分组,接口调用说明:

http请求方式: POST(请使用https协议)https://api.weixin.qq.com/shakearound/device/group/add?access_token=ACCESS_TOKEN
POST数据格式:json
POST数据例子:
{
  "group_name":"test"
}

Senparc.Weixin SDK对应代码:
Senparc.Weixin.MP.AdvancedAPIs.GroupsApi.Create("accessToken", "分组名称");

5.2、修改分组

编辑设备分组信息,目前只能修改分组名。接口调用说明:

http请求方式: POST(请使用https协议)https://api.weixin.qq.com/shakearound/device/group/update?access_token=ACCESS_TOKEN
POST数据格式:json
POST数据例子:
{
  "group_id":123,
  "group_name":"test update"
}

Senparc.Weixin SDK对应代码:

Senparc.Weixin.MP.AdvancedAPIs.GroupsApi.Update("accessToken", "分组Id", "分组名称");

5.3、删除分组

删除分组,对应分组中的用户会自动移动到“未分组”分组中。接口调用说明:

http请求方式: POST(请使用https协议)https://api.weixin.qq.com/shakearound/device/group/delete?access_token=ACCESS_TOKEN
POST数据格式:json
POST数据例子:
{
  "group_id":123
}

Senparc.Weixin SDK对应代码:
Senparc.Weixin.MP.AdvancedAPIs.GroupsApi.Delete("accessToken", "分组Id");

5.4、查询分组列表

查询账号下所有的分组。接口调用说明:

http请求方式: POST(请使用https协议)https://api.weixin.qq.com/shakearound/device/group/getlist?access_token=ACCESS_TOKEN
POST数据格式:json
POST数据例子:
{
  "begin": 0,
  "count" 10
}

Senparc.Weixin SDK对应代码:
Senparc.Weixin.MP.AdvancedAPIs.GroupsApi.Get("accessToken");

5.5、移动用户到指定分组

移动用户分组对应的Senparc.Weixin SDK代码:

Senparc.Weixin.MP.AdvancedAPIs.MemberUpdate(accessTokenOrAppId, openId, toGroupId, timeOut = 10000);

6、功能展示

6.1、用户组管理功能展示

用户组管理主界面

用户组管理-新增

用户组管理-一键同步所有分组

6.2、用户列表功能展示

用户列表-主界面

用户列表-移动分组

参考文章

微信公众平台技术文档-官方

Senparc.Weixin SDK + 官网示例源代码

RDIFramework.NET — 基于.NET的快速信息化系统开发框架 — 系列目录

RDIFramework.NET ━ .NET快速信息化系统开发框架 ━ 工作流程组件介绍

RDIFramework.NET框架SOA解决方案(集Windows服务、WinForm形式与IIS形式发布)-分布式应用

RDIFramework.NET代码生成器全新V3.5版本发布-重大升级


一路走来数个年头,感谢RDIFramework.NET框架的支持者与使用者,大家可以通过下面的地址了解详情。

RDIFramework.NET官方网站:http://www.rdiframework.net/

RDIFramework.NET官方博客:http://blog.rdiframework.net/

同时需要说明的,以后的所有技术文章以官方网站为准,欢迎大家收藏!

RDIFramework.NET框架由专业团队长期打造、一直在更新、一直在升级,请放心使用!

欢迎关注RDIFramework.net框架官方公众微信(微信号:guosisoft),及时了解最新动态。

扫描二维码立即关注

微信号:guosisoft

posted @ 2019-04-09 11:11  .NET快速开发框架  阅读(1732)  评论(0编辑  收藏  举报