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

时间:2023-03-09 04:36:35
用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层的结构如下:

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

未完待续!!!

用c#开发微信 系列汇总