用c#开发微信 (7) 微渠道 - 推广渠道管理系统 2 业务逻辑实现

我们可以使用微信的“生成带参数二维码接口”和 “用户管理接口”,来实现生成能标识不同推广渠道的二维码,记录分配给不同推广渠道二维码被扫描的信息。这样就可以统计和分析不同推广渠道的推广效果。

上次介绍了《用c#开发微信 (6) 微渠道 - 推广渠道管理系统 1 基础架构搭建》,主要介绍了数据访问层的实现。本文是微渠道的第二篇,主要介绍如下内容:

1. 各个实体具体业务实现

2. 同步微信个人用户信息

 

下面是详细的实现方法:

 

1. 各个实体具体业务实现

1) 渠道业务逻辑
public class ChannelBll
{
    /// <summary>
    /// 获取渠道列表
    /// </summary>
    /// <returns></returns>
    public List<ChannelEntity> GetEntities()
    {
        var entities = new ChannelDal().GetByPredicate(p => p.ID > 0).ToList();
        var viewEntity = new ChannelEntity();
        return entities.Select(p => viewEntity.GetViewModel(p)).ToList();
    }
 
    /// <summary>
    /// 根据ID获取渠道
    /// </summary>
    /// <param name="id">渠道ID</param>
    /// <returns></returns>
    public ChannelEntity GetEntityById(int id)
    {
        var entity = new ChannelDal().GetSingleByPredicate(p => p.ID == id);
        var viewEntity = new ChannelEntity();
        return viewEntity.GetViewModel(entity);
    }
 
    /// <summary>
    /// 添加或修改渠道
    /// </summary>
    /// <param name="viewEntity">渠道实体</param>
    /// <returns></returns>
    public bool UpdateOrInsertEntity(ChannelEntity viewEntity)
    {
        if (viewEntity.ID > 0)
        {
            var entity = viewEntity.GetDataEntity(viewEntity);
            var dbEntity = new ChannelDal().GetSingleByPredicate(p => p.ID == entity.ID);
            entity.SceneId = dbEntity.SceneId;
            entity.Qrcode = dbEntity.Qrcode;
            return new ChannelDal().Update(entity);
        }
        else
        {
            //新增渠道时,需要获取渠道的二维码
            GetQrcode(viewEntity);
            var entity = viewEntity.GetDataEntity(viewEntity);
            return new ChannelDal().InsertAndReturn(entity).ID > 0;
        }
    }
 
    /// <summary>
    /// 根据ID删除渠道
    /// </summary>
    /// <param name="id">渠道ID</param>
    /// <returns></returns>
    public bool DeleteEntityById(int id)
    {
        //var entity = new ChannelDal().GetSingleByPredicate(p => p.ID == id);
        return new ChannelDal().Delete(c=>c.ID == id);
    }
 
    /// <summary>
    /// 根据SceneId获取二维码id
    /// </summary>
    /// <param name="sceneId">扫描的二维码的参数</param>
    /// <returns></returns>
    public int GetChannelIdBySceneId(int sceneId)
    {
        var entity = new ChannelDal().GetSingleByPredicate(p => p.SceneId == sceneId);
        return entity == null ? 0 : entity.ID;
    }
 
    /// <summary>
    /// 判断渠道名称是否存在
    /// </summary>
    /// <param name="channelName">渠道名称</param>
    /// <param name="id">渠道ID</param>
    /// <returns></returns>
    public bool IsExitChannelName(string channelName, int id)
    {
        var channelCount = new ChannelDal().GetByPredicate(c => c.Name == channelName && c.ID == id).Count();
        return channelCount > 0;
    }
 
    /// <summary>
    /// 获取渠道的二维码
    /// </summary>
    /// <param name="channelName">渠道实体</param>
    /// <returns></returns>
    private void GetQrcode(ChannelEntity entity)
    {
        //获取微信公众平台接口访问凭据
        string accessToken = AccessTokenContainer.TryGetToken(ConfigurationManager.AppSettings["appID"], ConfigurationManager.AppSettings["appsecret"]);
        //找出一个未被使用的场景值ID,确保不同渠道使用不同的场景值ID
        int scenid = GetNotUsedSmallSceneId();
        if (scenid <= 0 || scenid > 100000)
        {
            throw new Exception("抱歉,您的二维码已经用完,请删除部分后重新添加");
        }
        CreateQrCodeResult createQrCodeResult = QrCodeApi.Create(accessToken, 0, scenid);
        if (!string.IsNullOrEmpty(createQrCodeResult.ticket))
        {
            using (MemoryStream stream = new MemoryStream())
            {
                //根据ticket获取二维码
                QrCodeApi.ShowQrCode(createQrCodeResult.ticket, stream);
                //将获取到的二维码图片转换为Base64String格式
                byte[] imageBytes = stream.ToArray();
                string base64Image = System.Convert.ToBase64String(imageBytes);
                //由于SqlServerCompact数据库限制最长字符4000,本测试项目将二维码保存到磁盘,正式项目中可直接保存到数据库
                string imageFile = "QrcodeImage" + scenid.ToString() + ".img";
                File.WriteAllText(System.Web.HttpContext.Current.Server.MapPath("~/App_Data/") + imageFile, base64Image);
                entity.Qrcode = imageFile;
                entity.SceneId = scenid;
            }
        }
        else
        {
            throw new Exception("抱歉!获取二维码失败");
        }
    }
 
    /// <summary>
    /// 找出没有用的最小SceneId
    /// </summary>
    /// <returns></returns>
    private int GetNotUsedSmallSceneId()
    {
        var listSceneId = new ChannelDal().GetByPredicate(p => p.ID > 0).Select(p => p.SceneId).OrderBy(p => p);
        for (int i = 1; i <= 100000; i++)
        {
            var sceneId = listSceneId.Any(e => e == i);
            if (!sceneId)
            {
                return i;
            }
        }
        return 0;
    }
}

这里的一些增删改查就不说了,需要注意的是:

  • 新增渠道时,要确保场景值ID不重复
  • 为避免每次下载二维码时去请求微信服务器,在新增渠道时,把二维码保存到本地,并在数据库中保存其路径

 

2) 扫描记录业务逻辑

微信公众平台要求微信公众号服务器必须在5秒内返回相应结果,否则会重新发送请求,一共重试三次;为了避免微信公众号服务器重复接收到同一条扫描记录,造成数据重复,导致统计失真,这里将保存扫描记录的操作放到线程池中异步执行,尽快返回相应结果给微信服务器

public class ChannelScanBll
{
/// <summary>
/// 保存扫描记录
/// </summary>
/// <param name="openId">微信用户OpenId</param>
/// <param name="sceneId">扫描的二维码的参数</param>
/// <param name="scanType">扫描类型</param>
public void SaveScan(string openId, int sceneId, ScanType scanType)
{
    //微信公众平台要求微信公众号服务器必须在5秒内返回相应结果,否则会重新发送请求,一共重试三次
    //为了避免微信公众号服务器重复接收到同一条扫描记录,造成数据重复,导致统计失真,这里将保存扫描记录的操作放到线程池中异步执行,尽快返回相应结果给微信服务器
    ThreadPool.QueueUserWorkItem(e =>
    {
        int channelId = new ChannelBll().GetChannelIdBySceneId(sceneId);
        if (channelId <= 0)
        {
            return;
        }
        ChannelScanEntity entity = new ChannelScanEntity()
        {
            ChannelId = channelId,
            ScanTime = DateTime.Now,
            OpenId = openId,
            ScanType = scanType
        };
        new ChannelScanDal().Insert(entity.GetDataEntity(entity));
    });
}
 
/// <summary>
/// 获取渠道的扫描记录
/// </summary>
/// <param name="channelId">渠道ID</param>
/// <returns></returns>
public List<ChannelScanDisplayEntity> GetChannelScanList(int channelId)
{
    //获取渠道扫描记录
    var entities = new ChannelScanDal().GetByPredicate(p => p.ChannelId == channelId).ToList();
    var viewEntity = new ChannelScanEntity();
    var result = entities.Select(p => new ChannelScanDisplayEntity() { ScanEntity = viewEntity.GetViewModel(p) }).ToList();
    //获取每条渠道扫描记录对应的微信用户信息
    var openIds = result.Select(p => p.ScanEntity.OpenId).ToArray();
    //在渠道扫描记录中包含微信用户信息,便于前端页面显示
    var userinfoEntities = new WeixinUserInfoDal().GetByPredicate(p => openIds.Contains(p.OpenId)).ToList();
    var userinfoViewEntity = new WeixinUserInfoEntity();
    var userinfoViewEnities = userinfoEntities.Select(p => userinfoViewEntity.GetViewModel(p)).ToList();
    result.ForEach(e =>
    {
        e.UserInfoEntity = userinfoViewEnities.Where(p => p.OpenId == e.ScanEntity.OpenId).FirstOrDefault();
    });
    return result;
}
}

 

3) 渠道类型业务逻辑
public class ChannelTypeBll
{
    /// <summary>
    /// 获取渠道类型列表
    /// </summary>
    /// <returns></returns>
    public List<ChannelTypeEntity> GetEntities()
    {
        var entities = new ChannelTypeDal().GetByPredicate(p => p.ID > 0).ToList();
        var viewEntity = new ChannelTypeEntity();
        return entities.Select(p => viewEntity.GetViewModel(p)).ToList();
    }
 
    /// <summary>
    /// 根据ID获取渠道类型
    /// </summary>
    /// <param name="id">渠道类型ID</param>
    /// <returns></returns>
    public ChannelTypeEntity GetEntityById(int id)
    {
        var entity = new ChannelTypeDal().GetSingleByPredicate(p => p.ID == id);
        var viewEntity = new ChannelTypeEntity();
        return viewEntity.GetViewModel(entity);
    }
 
    /// <summary>
    /// 添加或修改渠道类型
    /// </summary>
    /// <param name="viewEntity">渠道类型实体</param>
    /// <returns></returns>
    public bool UpdateOrInsertEntity(ChannelTypeEntity viewEntity)
    {
        var entity = viewEntity.GetDataEntity(viewEntity);
        if (entity.ID > 0)
        {
            return new ChannelTypeDal().Update(entity);
        }
        else
        {
            return new ChannelTypeDal().InsertAndReturn(entity).ID > 0;
        }
    }
 
    /// <summary>
    /// 根据ID删除渠道类型
    /// </summary>
    /// <param name="id">渠道类型ID</param>
    /// <returns></returns>
    public bool DeleteEntityById(int id)
    {
        var entity = new ChannelTypeDal().GetSingleByPredicate(p => p.ID == id);
        return new ChannelTypeDal().Delete(entity);
    }
}

 

4) 用户信息业务逻辑
public class WeixinUserInfoBll
{
    /// <summary>
    /// 静态构造函数
    /// </summary>
    static WeixinUserInfoBll()
    {
        WeixinUserInfoSynchronize.Synchronize();
    }
 
    /// <summary>
    /// 获取微信用户信息列表
    /// </summary>
    /// <returns></returns>
    public List<WeixinUserInfoEntity> GetEntities()
    {
        var entities = new WeixinUserInfoDal().GetByPredicate(p => p.OpenId != "").ToList();
        var viewEntity = new WeixinUserInfoEntity();
        return entities.Select(p => viewEntity.GetViewModel(p)).ToList();
    }
}

这里定义一个静态构造函数,用于下面同步微信个人用户信息时,只会开启一个全局唯一的同步线程。

 

2. 同步微信个人用户信息

当微信用户扫描二维码时,只会传递openid,这时就需要调用“用户信息接口”来获取用户的信息。当保存完用户的信息后,有可能用户修改了自己的基本资料,这时就要有个机制去定时同步用户的信息。具体思路如下:

1) 定义一个“同步微信用户信息”的静态类WeixinUserInfoSynchronize

当网页第一次被访问时,开启一个进程内全局唯一的同步的线程,并使用单例模式确保同步线程不会被调用多次,因为网页可能被同时访问。

/// <summary>
/// 同步微信用户信息线程
/// </summary>
private static Thread SynchronizeWeixinUserThread = null;
/// <summary>
/// 锁
/// </summary>
private static object lockSingal = new object();
 
/// <summary>
/// 开启同步微信用户信息线程
/// 单例模式
/// </summary>
public static void Synchronize()
{
    if (SynchronizeWeixinUserThread == null)
    {
        lock (lockSingal)
        {
            if (SynchronizeWeixinUserThread == null)
            {
                // 开启同步微信用户信息的后台线程
                ThreadStart start = new ThreadStart(SynchronizeWeixinUserCircle);
                SynchronizeWeixinUserThread = new Thread(start);
                SynchronizeWeixinUserThread.Start();
            }
        }
    }
}

 

 

2) 定义一个每隔一段时间执行一次微信用户信息同步方法
private static void SynchronizeWeixinUserCircle()
{
    try
    {
        SynchronizeWeixinUser();
        Thread.Sleep(60*60*1000);
    }
    catch (Exception ex)
    {
        m_Log.Error(ex.Message, ex);
    }
}

 

3) 实现微信用户信息同步方法:
  • 首先获取微信公众号所有关注者的OpenId,比较数据库中是否存在
  • 如果不存在就插入
  • 如果存在就更新
  • 如果在数据库中,但不在关注者列表中的OpenId,就要删除这些已取消关注的用户
/// <summary>
/// 微信用户信息同步方法
/// </summary>
/// <returns></returns>
private static void SynchronizeWeixinUser()
{
    OpenIdResultJson weixinOpenIds = GetAllOpenIds();
    
 
    //获取已同步到数据库中的微信用户的OpenId
    List<string> dataOpenList = new WeixinUserInfoDll().LoadEntities(p => p.ID > 0).Select(e => e.OpenId).ToList();
 
    m_Log.Info("获取已同步到数据库中的微信用户的Data OpenId: " + dataOpenList.Count.ToString());
 
    List<string> insertOpenIdList = new List<string>();
    List<string> updateOpenIdList = new List<string>();
    List<string> deleteOpenIdList = new List<string>();
    //判断每个微信用户需要执行的操作
    for (int index = 0; index < weixinOpenIds.data.openid.Count; index++)
    {
        var weixinOpenId = weixinOpenIds.data.openid[index];
        var user = dataOpenList.Find(e => e == weixinOpenId);
        if (user == null)
        {
            //不存在数据库中的,插入
            insertOpenIdList.Add(weixinOpenId);
 
            m_Log.Info("insert open id: " + weixinOpenId);
        }
        else
        {
            //已存在数据库中的,修改
            updateOpenIdList.Add(weixinOpenId);
 
            m_Log.Info("update open id: " + weixinOpenId);
        }
    }
    //已取消关注该微信公众号的,删除
    insertOpenIdList.ForEach(e => dataOpenList.Remove(e));
    updateOpenIdList.ForEach(e => dataOpenList.Remove(e));
    deleteOpenIdList.AddRange(dataOpenList);
 
    //插入失败的openId列表,用于失败重试
    List<string> failedInsert = new List<string>();
    //修改失败的openId列表,用于失败重试
    List<string> failedUpdate = new List<string>();
    //插入新的微信用户
    foreach (var openId in insertOpenIdList)
    {
        ExecuteWeixinUser(openId, new WeixinUserInfoDll().Insert, failedInsert);
    }
    //更新已有微信用户
    foreach (var openId in updateOpenIdList)
    {
        ExecuteWeixinUser(openId, new WeixinUserInfoDll().Update, failedUpdate);
    }
    if (deleteOpenIdList.Count > 0)
    {
        //删除已取消关注该微信公众号的微信用户
        foreach (var openId in deleteOpenIdList)
        {
            new WeixinUserInfoDll().DeleteByOpenId(openId);
        }
    }
    //插入失败,重试一次
    if (failedInsert.Count > 0)
    {
        List<string> fail = new List<string>();
        foreach (var openId in failedInsert)
        {
            ExecuteWeixinUser(openId, new WeixinUserInfoDll().Insert, fail);
        }
    }
    //更新失败,重试一次
    if (failedUpdate.Count > 0)
    {
        List<string> fail = new List<string>();
        foreach (var openId in failedInsert)
        {
            ExecuteWeixinUser(openId, new WeixinUserInfoDll().Update, fail);
        }
    }
}

插入或更新失败,重试一次。

 

4) 获取所有关注者的OpenId信息
private static OpenIdResultJson GetAllOpenIds()
{
    string accessToken = AccessTokenContainer.TryGetToken(ConfigurationManager.AppSettings["appID"], ConfigurationManager.AppSettings["appsecret"]);
    OpenIdResultJson openIdResult = User.List(accessToken, null);
    while (!string.IsNullOrWhiteSpace(openIdResult.next_openid))
    {
        OpenIdResultJson tempResult = User.List(accessToken, openIdResult.next_openid);
        openIdResult.next_openid = tempResult.next_openid;
        if (tempResult.data != null && tempResult.data.openid != null)
        {
            openIdResult.data.openid.AddRange(tempResult.data.openid);
        }
    }
    return openIdResult;
}
 
5) 获取openId对应的用户信息并存入数据库
/// <summary>
/// 获取openId对应的用户信息并存入数据库
/// </summary>
/// <param name="openId">微信用户openId</param>
/// <param name="execute">修改、删除或插入操作</param>
/// <param name="failList">未成功获取到用户信息的openId列表</param>
private static void ExecuteWeixinUser(string openId, GetExecute execute, List<string> failList)
{
    string accessToken = AccessTokenContainer.TryGetToken(ConfigurationManager.AppSettings["appID"], ConfigurationManager.AppSettings["appsecret"]);
    var userInfo = User.Info(accessToken, openId);
    if (userInfo.errcode != ReturnCode.请求成功)
    {
        failList.Add(openId);
 
        m_Log.Warn("fial open id: " + openId);
    }
    else
    {
        WeixinUserInfo entity = new WeixinUserInfo()
        {
            City = userInfo.city,
            Province = userInfo.province,
            Country = userInfo.country,
            HeadImgUrl = userInfo.headimgurl,
            Language = userInfo.language,
            Subscribe_time = userInfo.subscribe_time,
            Sex = (Int16)userInfo.sex,
            NickName = userInfo.nickname,
            OpenId = userInfo.openid
 
        };
 
        m_Log.Info("execute user info: " + userInfo.nickname);
 
        execute(entity);
    }
}

 

 

 

 

 

 

最后BLL层的结构如下:

image

 

未完待续!!!

 

用c#开发微信 系列汇总

posted @ 2015-05-28 09:06  疯吻IT  阅读(2458)  评论(4编辑  收藏  举报