要求:实现用户对网站网页的访问计时,数据越精确越好,扩展性越方便越好,对服务器的影响越小越好.
适用环境: 在线学习的学习时长的计时,在线考试等的精确计时.
设计思想:
创建一个js文件,在前台页面只需要引入该js,就可对该页实现访问计时.
具体设计与实现:
1,创建表结构
Create Table PageLogInfo
{
[Id] [int] IDENTITY(1,1) NOT NULL,
[Url] [nvarchar](500) NULL,
[Query] [nvarchar](500) NULL,
[Entertime] [datetime] NULL,
[ExitTime] [datetime] NULL,
[UserName] [varchar](100) NULL,
}
Id设为主键
2,完成js文件
var pageId = 0;
var create_http = function () {
if (window.XMLHttpRequest)
return (new XMLHttpRequest());
var arr_t = new Array(
"MSXML2.XMLHTTP.4.0 ",
"MSXML2.XMLHTTP.3.0 ",
"MSXML2.XMLHTTP.2.6 ",
"Microsoft.XMLHTTP ",
"MSXML.XMLHTTP "
);
for (var i = 0; i < arr_t.length; i++) {
try {
xmlhttp_ver = arr_t[i];
return new ActiveXObject(arr_t[i]);
} catch (e) { }
}
return (null);
};
window.onload = function () {
var xmlhttp = create_http();
if (xmlhttp == null) return;
xmlhttp.open("Post", "/LogInfo.Log?action=Enter&url="+document.URL);
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
if (xmlhttp.responseText.length > 0)
pageId = parseInt(xmlhttp.responseText);
}
};
xmlhttp.send(null);
window.onbeforeunload = function () {
var xmlhttp = create_http();
xmlhttp.open("Post", "/LogInfo.Log?action=Exit&Id=" + pageId);
xmlhttp.send(null);
};
};
3,增加httpHandler处理程序:
public class LogHandler:System.Web.IHttpHandler,System.Web.SessionState.IReadOnlySessionState
{
public bool IsReusable
{
get
{
return true;
}
}
public void ProcessRequest(HttpContext context)
{
HttpRequest Request = context.Request;
context.Response.Clear();
switch (Request.QueryString["action"])
{
case "Enter":
string url = Request.QueryString["url"];
string[] strs = url.Split('?');
int id = LogInfo.Create(strs[0], strs.Length > 1 ? strs[1] : null, context.Session.SessionID);
context.Response.Write(id);
break;
case "Exit":
int id = int.Parse(Request.QueryString["Id"]);
LogInfo.UpdatePageLog(id);
break;
}
context.Response.End();
}
4, 完成记录数据的类
public class LogInfo
{
private static LogDataTable logDt = new LogDataTable();
/// <summary>
/// 进入页面
/// </summary>
/// <returns></returns>
public static int CreatePageLog(string url, string query,string SessionID)
{
string commandTxt = @"INSERT INTO PageLogInfo(
Url, Query, EnterTime, ExitTime, UserName) VALUES(
@Url,@Query,@EnterTime,@ExitTime,@UserName);SELECT SCOPE_IDENTITY()";
int ret = int.Parse((数据库访问类).ExecuteScalar(CommandType.Text, commandTxt,
new SqlParameter("@Url", url),
new SqlParameter("@Query", query),
new SqlParameter("@EnterTime", DateTime.Now),
new SqlParameter("@ExitTime", null),
new SqlParameter("@UserName",
当前Session记录的用户名字)
).ToString());
logDt.AddRow(HttpContext.Current.Session.SessionID, ret); //
return ret;
}
/// <summary>
/// 离开页面
/// </summary>
/// <param name="id"></param>
public static void UpdatePageLog(int id)
{
string sql = "UPDATE PageLogInfo SET ExitTime=@ExitTime,UserName=@UserName WHERE Id=" + id;
DBProvider.Instance.GetDBInstance().ExecuteNonQuery(CommandType.Text, sql,
new SqlParameter("@ExitTime", DateTime.Now),
new SqlParameter("@UserName",/*当前session记录的用户名*/)
);
logDt.RemoveID(id);
}
/// <summary>
/// 更新没有离开时间的记录
/// </summary>
/// <param name="sessionId"></param>
public static void UpdateCurrentSession(string sessionId)
{
string ids = logDt.RemoveSession(sessionId);
if (ids.Length > 0)
{
string sql = "UPDATE PageLogInfo SET ExitTime=@ExitTime,UserName=@UserName WHERE ExitTime is null And Id in(" + "ids)";
DBProvider.Instance.GetDBInstance().ExecuteNonQuery(CommandType.Text, sql,
new SqlParameter("@ExitTime", DateTime.Now),
new SqlParameter("@UserName", LogProvider.Instance.UserName),
new SqlParameter("@UserType", LogProvider.Instance.UserType),
new SqlParameter("@Name", LogProvider.Instance.Name)
);
// new SqlParameter("@ids", HttpContext.Current.Session.SessionID));
}
}
}
/// <summary>
/// 用来记录Session与id的表
/// </summary>
internal class LogDataTable : DataTable
{
public LogDataTable()
{
this.Columns.Add("SessionID", typeof(string));
this.Columns.Add("ID", typeof(int));
}
public void AddRow(string sessionID, int id)
{
DataRow row = this.NewRow();
row["SessionID"] = sessionID;
row["ID"] = id;
lock (this)
{
this.Rows.Add(row);
}
}
/// <summary>
/// 如果页面有回发的结束时间,则将该页面的标记移除
/// </summary>
/// <param name="id"></param>
public void RemoveID(int id)
{
lock (this)
{
DataRow[] dr = this.Select("ID=" + id.ToString());
for (int i = 0; i < dr.Length; i++)
this.Rows.Remove(dr[i]);
}
}
public string RemoveSession(string sessionID)
{
string ids = string.Empty;
lock (this)
{
DataRow[] dr = this.Select("SessionID=" + sessionID);
for (int i = dr.Length - 1; i >= 0; i--)
{
ids += "," + dr[i]["ID"].ToString();
this.Rows.Remove(dr[i]);
}
}
if (ids.Length > 0)
ids = ids.Remove(0, 1);
return ids;
}
}
5,增加Global.asax文件,在Session_End里增加
LogInfo.UpdateCurrentSession(Session.SessionID);
6,在Web.config中,增加对httpHandler的调用
<httpHandlers> <add verb="POST" type="......" path="*.Log"/> </httpHandlers>
好了,至此,对页面的计时方案完成。
对设计与实现的一些说明:
1,js通过post提交enter参数,获取服务器为页面分配的唯一标记符号。此处是数据库自增主键。 也可以采用其他方式,为页面分配唯一标识码。
2,.Log是我自定义的一种提交文件格式,为了采用httpHandler,需要将此文件后缀增加到iis的aspnet_isapi中。否则httpHandler不会响应。另外,增加该筛选时将验证文件存在的勾务必去掉,否则会报404错误。使用httpHandler的好处在于,几乎可以忽略js中提交的路径问题。
3,存在部分浏览器在某些情况下不能回发的情况,因此需要在session过期时,更新这些session下的页面结束时间。而由于Session只保证了在现存Session中的唯一性,不保证Session的重复性,所以数据库中不能使用SessionID为更新的识别码。
4,由于在一般意义上,前台时间不可信,所以,采用服务器计时。因为不管是在Enter还是Exit情况下,可以考虑稳定网络中传输数据所占用的时间一致,所以该回发的可信程度极高。
5,因为页面存在登陆等情况,所以在记录离开时间时,也更新Session记录的用户名。
扩展与大型应用:
1,用户访问时间可以更精确。
js文件中增加计时器,在onload时启动,页面失去焦点时停止,页面获得焦点时重新启动。在Exit时回发计时器记录的秒数。为了保证数据的可靠性,可以以此秒数与服务器计算的访问时间比较,如果小于服务器记录秒数,则将提交的数据记录,否则以服务器计时为准。
2,处理iframe的情况
文件中增加js判断该页面是否处于iframe中,如果是,则判断父页是否引用了该js文件,如果引用了,则不回发Enter。
3,如果需要计时的页面很多,服务端可以采用缓存技术,来提高数据响应速度,而不需要直接写库。
4,以后哪个页面需要计时了,直接引入js文件,ok了。
5,可以针对特定页面,可以跑一些任务,来实现计时统计。
一些额外说明:
网络计时特别是web计时,由于浏览器、网络或者人为等因素,不可能做到精准,所以只能尽量去做到准确。