我們可以使用微信的“生成帶參數二維碼接口”和 “用戶管理接口”,來實現生成能標識不同推廣渠道的二維碼,記錄分配給不同推廣渠道二維碼被掃描的信息。這樣就可以統計和分析不同推廣渠道的推廣效果。
上次介紹了《用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層的結構如下:
未完待續!!!

