我们可以使用微信的“生成带参数二维码接口”和 “用户管理接口”,来实现生成能标识不同推广渠道的二维码,记录分配给不同推广渠道二维码被扫描的信息。这样就可以统计和分析不同推广渠道的推广效果。
上次介绍了《用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层的结构如下:
未完待续!!!