用c#开发微信 (13) 微统计 - 阅读分享统计系统 3 UI设计及后台处理

时间:2022-01-29 03:57:43

 

微信平台自带的统计功能太简单,有时我们需要统计有哪些微信个人用户阅读、分享了微信公众号的手机网页,以及微信个人用户访问手机网页的来源:朋友圈分享访问、好友分享消息访问等。本系统实现了手机网页阅读、分享与来源统计及手机网页在朋友圈的传播路径分析。

本系统使用最传统的三层架构。本文是微统计的第三篇,主要介绍如下内容:

 

1. 为页面HighCharts画图控件提供数据

2. 接收分享记录信息并保存到数据库

3. 访问记录统计图

4. 阅读统计界面

5. 处理文字请求

前端开发框架使用Bootstrap,没有注明前台的页面表示前台不用显示任何内容

1. 为页面HighCharts画图控件提供数据 Data.aspx

public partial class Data : System.Web.UI.Page
        {
            protected void Page_Load(object sender, EventArgs e)
            {
                string result = "";
                string typeStr = System.Web.HttpContext.Current.Request.QueryString["type"];
                if (!string.IsNullOrEmpty(typeStr))
                {
                    switch (typeStr)
                    {
                        case "navChart": //页面访问图
                            result = JsonConvert.SerializeObject(GetPageNavStatistics());
                            break;
                        case "shareChart": //页面分享图
                            result = JsonConvert.SerializeObject(GetPageShareStatistics());
                            break;
                    }
                }
                //将HighCharts绘图所需的数据返回给页面
                HttpResponse response = System.Web.HttpContext.Current.Response;
                response.ContentType = "application/json";
                response.Write(result);
                response.End();
            }
 
            /// <summary>
            /// 获取页面访问统计信息
            /// </summary>
            /// <returns></returns>
            private ChartData GetPageNavStatistics()
            {
                //取过去两天的数据进行统计
                DateTime startTime = DateTime.Now.AddDays(-3);
                DateTime endTime = DateTime.Now.AddDays(1);
                List<PageNavEntity> temp = new PageNavBll().GetPageNavList();
                List<decimal> statistics = new List<decimal>();
                //HighCharts时间轴的起始时间
                ChartData chartData = new ChartData
                {
                    StartYear = startTime.Year,
                    StartDay = startTime.Day,
                    StartMonth = startTime.Month
                };
                //生成按小时统计的数据
                while (startTime < endTime)
                {
                    statistics.Add(temp.FindAll(e => e.VisitTime >= startTime && e.VisitTime < startTime.AddHours(1)).Count());
                    startTime = startTime.AddHours(1);
                }
                chartData.Statistics = statistics.ToArray();
                return chartData;
            }
 
            /// <summary>
            /// 获取页面分享统计信息
            /// </summary>
            /// <returns></returns>
            private ChartData GetPageShareStatistics()
            {
                //取过去两天的数据进行统计
                DateTime startTime = DateTime.Now.AddDays(-3);
                DateTime endTime = DateTime.Now.AddDays(1);
                List<PageShareEntity> temp = new PageShareBll().GetPageShareList();
                List<decimal> statistics = new List<decimal>();
                //HighCharts时间轴的起始时间
                ChartData chartData = new ChartData
                {
                    StartYear = startTime.Year,
                    StartDay = startTime.Day,
                    StartMonth = startTime.Month
                };
                //生成按小时统计的数据
                while (startTime < endTime)
                {
                    statistics.Add(temp.FindAll(e => e.ShareTime >= startTime && e.ShareTime < startTime.AddHours(1)).Count());
                    startTime = startTime.AddHours(1);
                }
                chartData.Statistics = statistics.ToArray();
                return chartData;
            }
        }

2. 接收分享记录信息并保存到数据库 Share.aspx

public partial class Share : System.Web.UI.Page
    {
        ILog m_Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
 
        protected void Page_Load(object sender, EventArgs e)
        {
            string typeStr = Request.QueryString["type"];
 
            m_Log.Info("share type: " + typeStr);
            m_Log.Info("share url: " + Request["url"]);
 
            if (!string.IsNullOrEmpty(typeStr))
            {
                //识别分享类型
                ShareType type = ShareType.Unknown;
                switch (typeStr)
                {
                    case "timeline":
                        type = ShareType.Timeline;
                        break;
                    case "friend":
                        type = ShareType.Friend;
                        break;
                }
                //构造分享记录
                var pageShare = new PageShareEntity()
                {
                    Id = Guid.NewGuid(),
                    Url = GetOrigenalUrl(Request["url"]),
                    ParentShareOpenId = Request["s"],
                    ShareOpenId = Request["u"],
                    From = type,
                    ShareTime = DateTime.Now
                };
                //保存分享记录
               bool insertShare = new PageShareBll().InsertPageShare(pageShare);
 
               m_Log.Info("insert share: " + insertShare.ToString());
            }
        }
 
        /// <summary>
        /// 获取不含统计相关参数的页面地址
        /// </summary>
        /// <param name="url">网址</param>
        /// <returns>不含统计相关参数的页面地址</returns>
        private string GetOrigenalUrl(string url)
        {
            url = System.Web.HttpUtility.UrlDecode(url);
            Uri uri = new Uri(url);
            StringBuilder urlBuilder = new StringBuilder();
            //获取不含QueryString的URL
            urlBuilder.Append("http://")
                .Append(uri.Host)
                .Append(uri.AbsolutePath)
                .Append("?");
            //构造移除统计相关参数的Query
            Dictionary<string, string> queryString = uri.Query.Replace("?", "").Split('&').Where(p => !string.IsNullOrEmpty(p)).ToDictionary(p => p.Split('=')[0], p => p.Split('=')[1].Split('#')[0]);
            foreach (var key in queryString.Keys)
            {
                if (key != "s" && key != "u" && key != "from" && key != "code" && key != "state")
                {
                    urlBuilder.Append(key).Append("=").Append(queryString[key]).Append("&");
                }
            }
            return urlBuilder.ToString();
        }
    }

当发送朋友或朋友圈时保存分享数据。

 

3. 访问记录统计图 StatisticsPage.aspx

前台:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="StatisticsPage.aspx.cs" Inherits="Statistics.StatisticsPage" %>
 
<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>统计</title>
    <%-- Bootstrap --%>
    <link href="Css/bootstrap.min.css" rel="stylesheet" type="text/css" />
    <script src="Scripts/bootstrap.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery-1.9.1.min.js" type="text/javascript"></script>
    <%-- HighCharts用于图表显示 --%>
    <link href="Css/highcharts/charts.css" rel="stylesheet" type="text/css" />
    <script src="Scripts/HighCharts/highcharts.js" type="text/javascript"></script>
    <script src="Scripts/HighCharts/highcharts-more.js" type="text/javascript"></script>
    <script src="Scripts/HighCharts/publiclinecharts.js" type="text/javascript"></script>
</head>
<body>
    <div class="container">
        <div class="row-fluid">
            <div class="span12">
                <h3>访问记录</h3>
                <%-- 访问记录统计图 --%>
                <div class="box">
                    <div class="box-content">
                        <div class="row" style="margin-top: 30px; ">
                            <div class="area">
                                <div id="page-nav-chart">
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <%-- 访问记录列表 --%>
                <div class="maincontentinner1" >
                    <div id="Div12" class="dataTables_wrapper">
                        <table id="page-nav-table" class="table table-bordered responsive dataTable">
                            <%-- 访问记录列表列名 --%>
                            <thead>
                                <tr>
                                    <th>页面地址
                                    </th>
                                    <th>访问来源
                                    </th>
                                    <th>访问者openid
                                    </th>
                                    <th>分享自openid
                                    </th>
                                    <th>访问时间
                                    </th>
                                </tr>
                            </thead>
                            <tbody id="page-nav-table-body">
                                <%-- 一行一行生成访问记录列表 --%>
                                <% foreach (Statistics.ViewEntity.PageNavEntity entity in (ViewState["NavList"] as List<Statistics.ViewEntity.PageNavEntity>))
                                   { %>
                                <tr class="gradeX odd">
                                    <td>
                                        <%= entity.Url%>
                                        </td>
                                    <td class=" ">
                                        <%= entity.From.ToString()%>
                                        </td>
                                    <td class=" ">
                                        <%= entity.NavOpenId%>
                                        </td>
                                    <td class=" ">
                                        <%= entity.ShareOpenId%>
                                        </td>
                                    <td class=" ">
                                        <%= entity.VisitTime.ToString()%>
                                        </td>
                                </tr>
                                <% } %>
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script>
        //图表参数
        var pageNavChartOpts = {
            getStatisticsUrl: 'Data.aspx?type=navChart', //读取数据的访问地址
            titletext: "",
            ytext: "",
            startyear: 0,
            startmonth: 0,
            startday: 0,
            lineinterval: 3600 * 1000, //竖线以1小时为间隔显示
            pointInterval: 3600 * 1000,//点以1小时为间隔显示
            countArray: [],
            formid: "page-nav-chart", //图表容器ID
            seriesname: "访问次数",
            unit: "次"
        };
        jQuery(function () {
            //使用HighCharts绘制图表
            highcharts.extFunction.PreDrawMethod = function (repJson) {
                pageNavChartOpts.startyear = repJson.StartYear;
                pageNavChartOpts.startmonth = repJson.StartMonth;
                pageNavChartOpts.startday = repJson.StartDay;
                highcharts.displayMode = repJson.DisplayMode;
                pageNavChartOpts.lineinterval = repJson.LineInterval;
                pageNavChartOpts.pointInterval = repJson.PointInterval;
            };
            highcharts.init(pageNavChartOpts);
        });
    </script>
</body>
</html>

用HighCharts来图表显示数据。

 

后台:

protected void Page_Load(object sender, EventArgs e)
{
    //传递给页面显示的记录列表
    ViewState["NavList"] = new PageNavBll().GetPageNavList();
    ViewState["ShareList"] = new PageShareBll().GetPageShareList();
}

通过业务逻辑层获取数据。

 

4. 阅读统计界面 WeixinPageIndex.aspx

前台:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WeixinPageIndex.aspx.cs" Inherits="Statistics.WeixinPageIndex" %>
 
<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <script src="Scripts/jquery-1.9.1.min.js"></script>
    <script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
</head>
<body>
    <form id="form1" type="post" runat="server">
        <div>
            <%-- 所有的跳转页面,加*问者与分享者的OpenId --%>
            <a href="WeixinPageSubPage.aspx?u=<%= ViewState["navOpenId"] as string %>&s=<%= ViewState["shareOpenId"] as string %>">WeixinPageSubPage</a>
 
        </div>
    </form>
</body>
</html>
<script>
    var url = location.href;
    alert(url);
    wx.config({
        debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
        appId: '<%= appID %>', // 必填,公众号的唯一标识
        timestamp: '<%= timestamp %>', // 必填,生成签名的时间戳
        nonceStr: '<%= nonceStr %>', // 必填,生成签名的随机串
        signature: '<%= signature %>',// 必填,签名,见附录1
        // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
        jsApiList: [
        'onMenuShareAppMessage'
        ]
    });
 
    friendcallback = function (res) {
        var shareUrl = "Share.aspx?type=friend&url=" + encodeURIComponent(url) + "&u=" + "<%= ViewState["navOpenId"] as string %>" + "&s=" + "<%= ViewState["shareOpenId"] as string %>";
 
        //AJAX请求
        $.ajax({
            type: "get",
            url: shareUrl,
            beforeSend: function () {
            },
            success: function () {
            },
            complete: function () {
            },
            error: function () {
            }
        });
    };
 
    wx.ready(function () {
        wx.onMenuShareAppMessage({
            title: '用c#开发微信 系列汇总',
            desc: '网上开发微信开发的教程很多,但c#相对较少。这里列出了我所有c#开发微信的文章,方便自己随时查阅。如果可能,我尽量附上源码,这样就可以直接发布运行看效果,更好地理解原理。',
            link: url,
            imgUrl: 'http://demo.open.weixin.qq.com/jssdk/images/p2166127561.jpg',
            trigger: function (res) {
            },
            success: function (res) {
                friendcallback(res);
            },
            cancel: function (res) {
            },
            fail: function (res) {
                alert(JSON.stringify(res));
            }
        });
        wx.onMenuShareTimeline({
            title: '用c#开发微信 系列汇总',
            desc: '网上开发微信开发的教程很多,但c#相对较少。这里列出了我所有c#开发微信的文章,方便自己随时查阅。如果可能,我尽量附上源码,这样就可以直接发布运行看效果,更好地理解原理。',
            link: url,
            imgUrl: 'http://demo.open.weixin.qq.com/jssdk/images/p2166127561.jpg',
            trigger: function (res) {
            },
            success: function (res) {
                friendcallback(res);
            },
            cancel: function (res) {
            },
            fail: function (res) {
                alert(JSON.stringify(res));
            }
        });
    });
 
</script>

利用JS-SDK来获取分享者,并通过Share页面来保存分享数据。

 

 

后台:

public partial class WeixinPageIndex : System.Web.UI.Page
    {
        static ILog m_Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
 
        public string timestamp = string.Empty;
        public string nonceStr = string.Empty;
        public string signature = string.Empty;
 
        /// <summary>
        /// 从微信公众平台获取的开发者凭据
        /// </summary>
        public readonly string appID = ConfigurationManager.AppSettings["appID"];
        /// <summary>
        /// 从微信公众平台获取的开发者凭据
        /// </summary>
        readonly string appSecret = ConfigurationManager.AppSettings["appSecret"];
 
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                #region 1. Get wx.config
 
                string ticket = string.Empty;
                timestamp = JSSDKHelper.GetTimestamp();
                nonceStr = JSSDKHelper.GetNoncestr();
                JSSDKHelper jssdkhelper = new JSSDKHelper();
 
                try
                {
                    ticket = JsApiTicketContainer.TryGetTicket(appID, appSecret);
                    signature = jssdkhelper.GetSignature(ticket, nonceStr, timestamp, Request.Url.AbsoluteUri.ToString());
                }
                catch (ErrorJsonResultException ex)
                {
                    m_Log.Error("errorcode:" + ex.JsonResult.errcode.ToString() + "  errmsg: " + ex.JsonResult.errmsg, ex);
                }
 
                #endregion
 
                NameValueCollection parameters = System.Web.HttpContext.Current.Request.Params;
 
                m_Log.Info("URL: " + Request.Url.ToString());
 
                //取得链接中的分享者OpenId
                string shareOpenId = parameters["s"];
                //获取访问者的OpenId
                string navOpenId = GetNavOpenId();
 
                if (navOpenId != null)
                {
                    NavStatistics(navOpenId, shareOpenId);
                    //传递给页面的访问者OpenId
                    ViewState["navOpenId"] = navOpenId;
                    //传递给页面的分享者OpenId
                    ViewState["shareOpenId"] = shareOpenId;
                }
 
                m_Log.Info("timestamp: " + timestamp + " nocestr: " + nonceStr + " singnature: " + signature);
                m_Log.Info("nav open id: " + navOpenId + " share open id: " + shareOpenId);
            }
        }
 
        /// <summary>
        /// 记录页面访问
        /// </summary>
        /// <param name="navOpenId">访问者微信openid</param>
        /// <param name="shareOpenId">当访问来源为朋友圈时的分享者微信openid</param>
        private void NavStatistics(string navOpenId, string shareOpenId)
        {
            //获取访问来源
            NavFrom fromType = GetNavFromType();
            //构造访问记录
            var pageNav = new PageNavEntity()
            {
                Id = Guid.NewGuid(),
                Url = GetOrigenalUrl(),
                NavOpenId = navOpenId,
                ShareOpenId = navOpenId == shareOpenId ? "" : shareOpenId,
                From = fromType,
                VisitTime = DateTime.Now
            };
            //访问记录写入数据库
            new PageNavBll().InsertPageNav(pageNav);
        }
 
        /// <summary>
        /// 判断页面访问来源类型
        /// </summary>
        /// <returns></returns>
        private static NavFrom GetNavFromType()
        {
            //网址中的参数集合
            NameValueCollection parameters = System.Web.HttpContext.Current.Request.Params;
            string fromStr = parameters["from"]; //发送给朋友、分享到朋友圈的链接会含有from参数
 
            m_Log.Info("from: " + fromStr);
 
            NavFrom fromType;
            if (!Enum.TryParse<NavFrom>(fromStr, true, out fromType)) //通过判断from参数,识别页面访问是来自于发送给朋友的链接还是分享到朋友圈的链接
            {
                //获取HTTP访问头中的User-Agent参数的值
                string agent = System.Web.HttpContext.Current.Request.Headers["User-Agent"];
                if (agent.Contains(NavFrom.MicroMessenger.ToString())) //判断页面是否是在微信内置浏览器中打开
                    fromType = NavFrom.MicroMessenger;
                else
                    fromType = NavFrom.Other;
            }
            return fromType;
        }
 
        /// <summary>
        /// 获取不含统计相关参数的页面地址
        /// </summary>
        /// <returns>不含统计相关参数的页面地址</returns>
        private string GetOrigenalUrl()
        {
            StringBuilder urlBuilder = new StringBuilder();
            //获取不含QueryString的URL
            urlBuilder.Append("http://")
                .Append(System.Web.HttpContext.Current.Request.Url.Host)
                .Append(System.Web.HttpContext.Current.Request.Url.AbsolutePath)
                .Append("?");
            //构造移除统计相关参数的Query
            foreach (var key in System.Web.HttpContext.Current.Request.QueryString.AllKeys)
            {
                if (key != "s" && key != "u" && key != "from" && key != "code" && key != "state")
                {
                    urlBuilder.Append(key).Append("=").Append(System.Web.HttpContext.Current.Request.QueryString[key]).Append("&");
                }
            }
            return urlBuilder.ToString();
        }
 
        /// <summary>
        /// 获取访问者openId
        /// </summary>
        private string GetNavOpenId()
        {
            NameValueCollection parameters = System.Web.HttpContext.Current.Request.Params;
            //获取链接中的openId
            string navOpenId = parameters["u"];
            #region 如果是从微信浏览器浏览,获取真实的微信OpenId
            if (!string.IsNullOrEmpty(appID) && !string.IsNullOrEmpty(appSecret))
            {
                string accessSource = System.Web.HttpContext.Current.Request.ServerVariables["HTTP_USER_AGENT"];
 
                if (accessSource.Contains("MicroMessenger")) //如果是从微信打开页面
                {
                    string[] cookieKeys = new[] { CookieHelper.COOKIE_NAME };
                    Dictionary<string, string> realIdCookie = CookieHelper.GetLoginCookies(cookieKeys); //获取保存在Cookie中的OpenId
                    //如果Cookie中不存在OpenId,或者链接中的openId与Cookie中的OpenId不一致,链接中的openId为分享者的OpenId,需要获取当前用户的真实OpenId
                    if (NeedGetReadOpenId(parameters, realIdCookie))
                    {
                        if (parameters["code"] == null)
                        {
                            // 先去获取code,并记录分享者
                            string snsapi_baseUrl = GoCodeUrl(navOpenId);
                            if (!string.IsNullOrEmpty(snsapi_baseUrl))
                            {
                                CookieHelper.CleanLoginCookie(cookieKeys);
                                //跳转到微信网页授权页面
                                System.Web.HttpContext.Current.Response.Redirect(snsapi_baseUrl, true);
                                System.Web.HttpContext.Current.Response.End();
                                return null;
                            }
                        }
                        else
                        {
                            m_Log.Info("code: " + parameters["code"].ToString());
 
                            OAuthAccessTokenResult tokenResult = GetRealOpenId(parameters["code"].ToString());
                            if (null != tokenResult && !string.IsNullOrEmpty(tokenResult.openid))
                            {
                                m_Log.Info("tokenResult.openid: " + tokenResult.openid);
 
                                navOpenId = tokenResult.openid;
                                // 获取到的当前访问者的OpenId保存到cookie里
                                CookieHelper.CleanLoginCookie(cookieKeys);
                                realIdCookie[CookieHelper.COOKIE_NAME] = tokenResult.openid;
                                CookieHelper.WriteLoginCookies(realIdCookie, DateTime.MinValue);
                            }
                        }
                    }
                }
            }
            #endregion
            return navOpenId;
        }
 
        /// <summary>
        /// 如果Cookie中存在OpenId且链接中的openId与Cookie中的OpenId一致
        /// 则不需要调用网页授权接口,链接中的openId即为当前访问者的真实OpenId
        /// </summary>
        /// <param name="parameters"></param>
        /// <param name="realIdCookie"></param>
        /// <returns></returns>
        private bool NeedGetReadOpenId(NameValueCollection parameters, Dictionary<string, string> realIdCookie)
        {
            string referer = System.Web.HttpContext.Current.Request.ServerVariables["HTTP_REFERER"];
            string openId = null;
            if (realIdCookie != null)
            {
                if (realIdCookie.ContainsKey(CookieHelper.COOKIE_NAME))
                {
                    openId = realIdCookie[CookieHelper.COOKIE_NAME];
                }
            }
 
            m_Log.Info("NeedGetReadOpenId openid: " + openId + " referer: " + referer + " u: " + parameters["u"].ToString());
 
            if (!string.IsNullOrEmpty(referer) && openId == parameters["u"].ToString())
                return false;
            else
                return true;
        }
 
        /// <summary>
        /// 网页授权接口第一步
        /// 跳转到获取code的url
        /// </summary>
        /// <param name="shareOpenId">当访问来源为朋友圈时的分享者微信openid</param>
        private string GoCodeUrl(string shareOpenId)
        {
            string url = System.Web.HttpContext.Current.Request.Url.AbsoluteUri + "&s=" + shareOpenId; //添加分享者OpenId
            return OAuthApi.GetAuthorizeUrl(appID, url, "STATE", OAuthScope.snsapi_base);
        }
 
        /// <summary>
        /// 网页授权接口第二步
        /// 解析code并获取当前访问者真正的openId
        /// </summary>
        /// <param name="parameters">url参数</param>
        /// <returns>真正的openId</returns>
        private OAuthAccessTokenResult GetRealOpenId(string code)
        {
            OAuthAccessTokenResult result = new OAuthAccessTokenResult();
            try
            {
                result = OAuthApi.GetAccessToken(appID, appSecret, code);
            }
            catch (Exception ex)
            {
                m_Log.Error(ex.Message, ex);
            }
            return result;
        }
    }

保存访问者记录,识别访问者和分享者。

 

5. 处理文字请求

在CustomMessageHandler里处理文字请求,详细的使用方法可参考《用c#开发微信(2)扫描二维码,用户授权后获取用户基本信息 (源码下载)》:

public override IResponseMessageBase OnTextRequest(RequestMessageText requestMessage)
        {
            var responseMessage = CreateResponseMessage<ResponseMessageNews>();
            responseMessage.Articles.Add(new Article()
            {
                Title = "首页",
                Description = "点击进入首页",
                PicUrl = "",
                Url = System.Configuration.ConfigurationManager.AppSettings["site"] + "/WeixinPageIndex.aspx?u=" + requestMessage.FromUserName
            });
            return responseMessage;
        }

这里把访问者的OpenId带上,为了方便识别访问者和分享者。

 

 

所有界面如下:

用c#开发微信 (13) 微统计 - 阅读分享统计系统 3 UI设计及后台处理

未完待续!!!

 

 

用c#开发微信 系列汇总