你的位置:首页 > ASP.net教程

[ASP.net教程]一步步开发自己的博客 番外篇(7、异步记录日志 和 文章阅读量统计)

前言 

离前一篇《一步步开发自己的博客  .NET版(6、手机端的兼容)》都个把月了。

当时写完第六篇,很多人问“剧终”了?你还有好多实现没有讲解分析呢。我说没呢,后期还会有第二版、第三版...至于还没有分析到,后期补上。你看,我今天不就来了么。

前段时间写代码,手都写的酸痛酸痛的,歇息了几天,好多了。然后,又捣鼓了一下webapi。这也是个没有接触过的知识。跌跌撞撞的整了点东西出来。有兴趣写移动端的同学可以联系我,大家一起学习。API文档和测试地址:http://haojima.net/SwaggerUI 

其他的就不多说了,进入今天的主题,异步记录日志和文章阅读量统计。

     

异步记录日志

我们常用日志记录,无非就是,数据库记录和文本日志记录。而今天我要说的是,文本日志记录。

最简单的文本记录: File.WriteAllText(path,messg); 使用静态类File的WriteAllText 如果文件存在则覆盖,传入文件路径和消息内容。ok,完事。

当然,我们不能每次都覆盖上一次的记录。 File.AppendAllText(path,messg); 那么我们可是在原有内容追加。这里,我们不用关系文件流是否关闭,使用静态类File的这两个方法都会自动帮我们关闭。

如果,我们是使用的winfrom单线程。那么,基本的日志记录就这个两个方法 完全可以搞定。

但是,如果是web程序就不一样了,天生的多线程。多个线程同时访问一个文件,肯定是会报错的。不信你试试。

那我们怎么解决这个问题?有人会说,加锁呗。锁肯定是要加,不过要看怎么加了。如果加到写文件内容的时候肯定是不合适的。因为写文件要打开文件流,比较耗时。我们可以先把要写的日志,统一存内存,然后单线程从内存取数据,写到文本。当然,写内存也可能会多线程并发,这个时候,我们就可以把锁加到写内存的地方。这里大家就不用担心了,写内存的速度是非常快的,和直接写文件那差的可不是一两个档次的问题了。

我们刚才说存内存,怎么存?当然是存集合了。有个数据类型 Queue 为什么要用它。因为它是队列,有个特点:先进先出。我们取数据的时候就是去的最早存进去的数据了。

使用:存数据 Queue myQ = new Queue(); myQ.Enqueue("The");//入队    取数据 var t = myQ.Dequeue(); 直接在取值的时候就把值在队列中移除了。这样正好免了我手动移除。

那么,很简单。我们记日志的时候就先把日志往 Queue 里存,然后单独开个进程取值存值写文件里。ok,完事。

刚才说了,我们要加锁。是的,要加锁。因为 Queue 并不是线程安全数据。我们在写数据和读数据的时候都要加锁。

static object myLock= new object();...lock (myLock)   logQueue.Enqueue(logmede);//存...lock (myLock)   var m = logQueue.Dequeue();//取

 

我之前在网上查资料说不能多线程同时写入队列,经测试其实是不能同时读和写队列。所以在Dequeue取的时候也要锁定同一个对象

思路大体就是这样了。当然,我们还可以扩展很多的东西。如:定时删除指定过期日志、分文件大小存储日志、自动增加的日志文件命名...等等。

写到这里,估计又有大把大把的人要来批判我了。又在造轮子。日志框架那么多,干嘛还要自己写。浪费时间.....等。

没错,我确实是在造轮子。我不想解释太多了。累...   大神请略过....

下面给出,我的具体实现代码。分为四个文件 LogMode 包含文件名、日志内容 LogHelper 存队列、写文件 LogConfig 读取相关配置 LogSave 外部直接调用

using System;using System.Collections.Generic;using System.ComponentModel;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Web;namespace CommonLib.HiLog{  /// <summary>  /// 日志模型  /// </summary>  internal class LogModel  {       #region logFileName    private string _logFileName;    /// <summary>    /// 日志文件名字    /// </summary>    public string logFileName    {      get { return _logFileName + "_" + DateTime.Now.ToString("yyyyMMdd"); }      set { _logFileName = value; }    }    #endregion    #region logMessg    private string _logMessg;    /// <summary>    /// 日志内容    /// </summary>    public string logMessg    {      get      {        return "----begin-------" + DateTime.Now.ToString() + "----Queue.Count:" + LogHelper.LogQueue.Count + "-----------------------------------\r\n\r\n"          + _logMessg          + "\r\n\r\n----end----------" + DateTime.Now.ToString() + "----Queue.Count:" + LogHelper.LogQueue.Count + "-----------------------------------"          + "\r\n\r\n\r\n";      }      set { _logMessg = value; }    }    #endregion  }}

View Code
using System;using System.Collections.Generic;using System.IO;using System.Linq;using System.Text;using System.Threading;using System.Threading.Tasks;namespace CommonLib.HiLog{  /// <summary>  /// 日志操作辅助类  /// [email protected]  /// 创建20150104 修改20151003  /// </summary>  internal class LogHelper  {    /// <summary>    /// 消息队列    /// </summary>    private static Queue<LogModel> logQueue = new Queue<LogModel>();    /// <summary>    /// 消息队列 对外只读    /// </summary>    public static Queue<LogModel> LogQueue    {      get { return LogHelper.logQueue; }    }    /// <summary>    /// 标志锁    /// </summary>    static string myLock = "true";    /// <summary>    /// 写入日志文件(异步单线程 记录日志)    /// </summary>    /// <param name="logmede"></param>    public static void logWrite(LogModel logmede)    {      // 这里需要锁上 不然会出现:源数组长度不足。请检查 srcIndex 和长度以及数组的下限。异常        //网上有资料说 http://blog.csdn.net/greatbody/article/details/26135057 不能多线程同时写入队列      //其实 不仅仅 不能同时写入队列 也不能同时读和写如队列 所以 在Dequeue 取的时候也要锁定一个对象      lock (myLock)        logQueue.Enqueue(logmede);      logStartWrite();    }    /// <summary>    /// 文件编码格式    /// </summary>    public static Encoding encoding = Encoding.Default;    /// <summary>    /// 是否开始自动记录日志    /// </summary>    private static bool isStart = false;    /// <summary>    /// 用来 标识 最好一次 检测是否 需要 清理 日志文件 时间    /// </summary>    private static DateTime time = DateTime.MinValue;    /// <summary>    /// 每个日志文件夹 对应的文件下标    /// </summary>    private static Dictionary<string, int> logFileNum = new Dictionary<string, int>();    /// <summary>    /// 开始把队列消息写入文件    /// </summary>    private static void logStartWrite()    {      if (isStart)        return;      isStart = true;      Task.Run(() =>      {        while (true)        {          if (LogHelper.logQueue.Count >= 1)          {            LogModel m = null;            lock (myLock)              m = LogHelper.logQueue.Dequeue();            if (m == null)              continue;            if (string.IsNullOrEmpty(LogConfig.logFilePath))              throw new Exception("请先初始化日志保存路径LogModel._logFilePath");            TestingInvalid();            if (!Directory.Exists(LogConfig.logFilePath + m.logFileName + @"\"))              Directory.CreateDirectory(LogConfig.logFilePath + m.logFileName + @"\");            // int i = m.logFileNum;            if (!logFileNum.Keys.Contains(m.logFileName))              logFileNum.Add(m.logFileName, 0);            //部分 日志 文件路径            string SectionfileFullName = LogConfig.logFilePath + m.logFileName + @"\" + m.logFileName + "_" + logFileNum[m.logFileName].ToString("000") + ".txt";            //最新的写了内容的 部分 日志文件路径            string TopSectionfileFullName = SectionfileFullName;            // 需要实时更新的 最新日志文件 路径            string LogfileFullNqme = LogConfig.logFilePath + m.logFileName + @"\" + m.logFileName + ".txt";            FileInfo file = new FileInfo(SectionfileFullName);            while (file.Exists && file.Length >= LogConfig.SectionlogFileSize)            {              TopSectionfileFullName = SectionfileFullName;              logFileNum[m.logFileName]++;              SectionfileFullName = LogConfig.logFilePath + m.logFileName + @"\" + m.logFileName + "_" + logFileNum[m.logFileName].ToString("000") + ".txt";              file = new FileInfo(SectionfileFullName);            }            try            {              if (!file.Exists)//如果不存在 这个文件 就说明需要 创建新的部分日志文件了              {                //因为SectionfileFullName路径的文件不存在  所以创建                File.WriteAllText(SectionfileFullName, m.logMessg, encoding);                FileInfo Logfile = new FileInfo(LogfileFullNqme);                if (Logfile.Exists && Logfile.Length >= LogConfig.FileSize)                  //先清空 然后加上 上一个部分文件的内容                  File.WriteAllText(LogfileFullNqme, File.ReadAllText(TopSectionfileFullName, encoding), encoding);//如果存在则覆盖                            }              else                File.AppendAllText(SectionfileFullName, m.logMessg, encoding);//累加              //追加这次内容 到动态更新的日志文件              File.AppendAllText(LogfileFullNqme, m.logMessg, encoding);            }            catch (Exception ex)            {              throw ex;            }          }          else          {            isStart = false;//标记下次可执行            break;//跳出循环          }        }      });    }    /// <summary>    /// 检测 并删除 之前之外的 日志文件    /// </summary>    public static void TestingInvalid()    {      #region 检测 并删除 之前之外的 日志文件      if (time.AddMinutes(LogConfig.TestingInterval) <= DateTime.Now)// 时间内 检测一次      {        try        {          time = DateTime.Now;          List<string> keyNames = new List<string>();          foreach (var logFileName in logFileNum.Keys)          {            CreatePath(LogConfig.logFilePath + logFileName + @"\");            DirectoryInfo dir = new DirectoryInfo(LogConfig.logFilePath + logFileName + @"\");            if (dir.CreationTime.AddMinutes(LogConfig.DelInterval) <= DateTime.Now)//删除 设定时间 之前的日志              foreach (var fileInfo in dir.GetFiles())              {                if (fileInfo.LastWriteTime.AddMinutes(LogConfig.DelInterval) <= DateTime.Now)//最后修改时间算起                  File.Delete(fileInfo.FullName);              }            if (dir.GetFiles().Length == 0)              keyNames.Add(logFileName);//临时存储没有日志文件的文件夹          }          foreach (var key in keyNames)//删除没有日志文件的文件夹          {            logFileNum.Remove(key);            Directory.Delete(LogConfig.logFilePath + key + @"\", false);          }        }        catch (Exception ex)        {          LogSave.ErrLogSave("手动捕获[检测并删除日志出错!]", ex, "记录日志出错");        }              }      #endregion    }    #region 创建路径    /// <summary>    /// 创建路径    /// </summary>    /// <param name="paht"></param>    /// <returns></returns>    public static bool CreatePath(string paht)    {      if (!Directory.Exists(paht))      {        Directory.CreateDirectory(paht);        return true;      }      return false;    }    #endregion  }}

View Code
using System;using System.Collections.Generic;using System.Configuration;using System.Data;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Web;namespace CommonLib.HiLog{  /// <summary>  /// 日志相关配置    /// </summary>  public static class LogConfig  {    #region 辅助方法    /// <summary>    /// GetAppSettings    /// </summary>    /// <param name="key"></param>    /// <returns></returns>    public static string GetAppSettings(string key)    {      if (ConfigurationManager.AppSettings.AllKeys.Contains(key))        return ConfigurationManager.AppSettings[key].ToString();      return string.Empty;    }    /// <summary>    /// 计算字符串 转 计算结果    /// </summary>    /// <param name="v"></param>    /// <returns></returns>    public static string toCompute(this string v)    {      return new DataTable().Compute(v, "").ToString();    }    #endregion    #region 静态属性和字段    #region logFilePath 路径    /// <summary>    /// 日志要存的路径 默认路径:网站根目录 + Log 文件夹    /// 在程序第一次启动是设置    /// </summary>        private static string _logFilePath;    /// <summary>    /// 日志要存的路径 默认路径:网站根目录 + Log 文件夹    /// 在程序第一次启动是设置    /// </summary>      public static string logFilePath    {      get      {        if (string.IsNullOrEmpty(_logFilePath))        {          try          {            _logFilePath = HttpContext.Current.Server.MapPath("~/");          }          catch (Exception)          {            try            {              _logFilePath = System.Windows.Forms.Application.StartupPath + @"\";            }            catch (Exception)            {              throw new Exception("请先初始化要保存的路径:LogModel._logFilePath");            }          }        }        return _logFilePath;      }      set      {        _logFilePath = value;      }    }    #endregion    #region 检测间隔时间(分钟)    private static int _TestingInterval;    /// <summary>    /// 检测间隔时间(分钟) 默认:一天    /// 配置:appSettings->Log_TestingInterval 单位:秒    /// </summary>    public static int TestingInterval    {      get      {        if (_TestingInterval <= 0)        {          var Log_TestingInterval = GetAppSettings("Log_TestingInterval");          if (string.IsNullOrEmpty(Log_TestingInterval))            _TestingInterval = 1 * 60 * 24;          else            _TestingInterval = Convert.ToInt32(Log_TestingInterval.toCompute());        }        return _TestingInterval;      }    }    #endregion    #region 删除 N分钟(最后修改时间)之前的的日志    private static int _DelInterval;    /// <summary>    /// 删除 N分钟(最后修改时间)之前的的日志 默认:15天    /// 配置:appSettings->Log_DelInterval 单位:秒    /// </summary>    public static int DelInterval    {      get      {        if (_DelInterval <= 0)        {          var Log_DelInterval = GetAppSettings("Log_DelInterval");          if (string.IsNullOrEmpty(Log_DelInterval))            _DelInterval = 1 * 60 * 24 * 15;          else            _DelInterval = Convert.ToInt32(Log_DelInterval.toCompute());        }        return _DelInterval;      }    }    #endregion    #region 部分日志文件大小(Byte)    private static int _SectionlogFileSize;    /// <summary>    /// 部分日志文件大小(Byte) 默认:1024Byte * 1024 * 1 = 1MB    /// 配置:appSettings->Log_SectionlogFileSize 单位:Byte    /// </summary>    public static int SectionlogFileSize    {      get      {        if (_SectionlogFileSize <= 0)        {          var Log_SectionlogFileSize = GetAppSettings("Log_SectionlogFileSize");          if (string.IsNullOrEmpty(Log_SectionlogFileSize))            _SectionlogFileSize = 1024 * 1024 * 1;          else            _SectionlogFileSize = Convert.ToInt32(Log_SectionlogFileSize.toCompute());        }        return _SectionlogFileSize;      }    }    #endregion    #region 变动文件大小(Byte)    private static int _FileSize;    /// <summary>    /// 变动文件大小(Byte) 默认:1024 * 1024 * 4 = 4M    /// 配置:appSettings->Log_FileSize 单位:Byte    /// </summary>    public static int FileSize    {      get      {        if (_FileSize <= 0)        {          var Log_FileSize = GetAppSettings("Log_FileSize");          if (string.IsNullOrEmpty(Log_FileSize))            _FileSize = 1024 * 1024 * 4;          else            _FileSize = Convert.ToInt32(Log_FileSize.toCompute());        }        return _FileSize;      }    }    #endregion    #endregion  }}

View Code
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Web;namespace CommonLib.HiLog{  /// <summary>  /// 异步单线程  /// </summary>  public class LogSave  {    /// <summary>    /// 获得Exception 的详细信息    /// </summary>    /// <param name="ex"></param>    /// <returns></returns>    public static string GetExceptionInfo(Exception ex)    {      StringBuilder str = new StringBuilder();      str.Append("错误信息:" + ex.Message);      str.Append("\r\n错误源:" + ex.Source);      str.Append("\r\n异常方法:" + ex.TargetSite);      str.Append("\r\n堆栈信息:" + ex.StackTrace);      return str.ToString();    }    /// <summary>    /// 系统 自动 捕捉异常    /// 保存异常详细信息     /// 包括: 浏览器 浏览器版本 操作系统 页面 Exception    /// </summary>    /// <param name="ex"></param>    /// <param name="fileName">文件名 默认:SysErr</param>    public static void SysErrLogSave(Exception ex, string fileName = null)    {      StringBuilder str = new StringBuilder();      string ip = "";      if (HttpContext.Current.Request.ServerVariables.Get("HTTP_X_FORWARDED_FOR") != null)        ip = HttpContext.Current.Request.ServerVariables.Get("HTTP_X_FORWARDED_FOR").ToString().Trim();      else        ip = HttpContext.Current.Request.ServerVariables.Get("Remote_Addr").ToString().Trim();      str.Append("Ip:" + ip);      str.Append("\r\n浏览器:" + HttpContext.Current.Request.Browser.Browser.ToString());      str.Append("\r\n浏览器版本:" + HttpContext.Current.Request.Browser.MajorVersion.ToString());      str.Append("\r\n操作系统:" + HttpContext.Current.Request.Browser.Platform.ToString());      str.Append("\r\n页面:" + HttpContext.Current.Request.Url.ToString());      str.Append("\r\n" + GetExceptionInfo(ex));      LogHelper.logWrite(new LogModel()      {        logFileName = "SysErr" + fileName ?? string.Empty,        logMessg = str.ToString()      });    }    /// <summary>    /// 异常日志记录    /// </summary>    /// <param name="strmes"></param>    /// <param name="ex"></param>    public static void ErrLogSave(string strmes, Exception ex, string fileName = null)    {      StringBuilder str = new StringBuilder();      str.Append(strmes);      if (ex != null)        str.Append("\r\n" + GetExceptionInfo(ex));      LogHelper.logWrite(new LogModel()      {        logFileName = fileName ?? "Err",        logMessg = str.ToString()      });    }    /// <summary>    /// 警告日志记录    /// </summary>    /// <param name="str"></param>    public static void WarnLogSave(string str, string fileName = null)    {      if (str != null && !string.IsNullOrEmpty(str.Trim()))        LogHelper.logWrite(new LogModel()        {          logFileName = fileName ?? "Warn",          logMessg = str        });    }    /// <summary>    /// 追踪日志记录    /// </summary>    /// <param name="str"></param>    public static void TrackLogSave(string str, string fileName = null)    {      if (str != null && !string.IsNullOrEmpty(str.Trim()))        LogHelper.logWrite(new LogModel()        {          logFileName = fileName ?? "Track",          logMessg = str        });    }    /// <summary>    /// 追踪日志记录    /// </summary>    /// <param name="str"></param>    public static void TrackLogSave(string str)    {      if (!string.IsNullOrEmpty(str.Trim()))        LogHelper.logWrite(new LogModel()        {          logFileName = "SqlTrack",          logMessg = str        });    }  }}

View Code

写好之后,下次我在别的项目里面就直接引用。

如果你使用的是EF,那么我再告诉你一个小秘密。 DbContext 中的 Database.Log 可以直接记录所有EF执行的sql语句和参数。

使用如: dbContext.Database.Log = LogSave.TrackLogSave;  而LogSave.TrackLogSave我们在上面已经封装过。

效果图1  效果图2 

文章阅读量统计

我在一开始就琢磨着怎么统计阅读量。之前也在http://www.cnblogs.com/zhaopei/p/4744846.html的最后提出了这个疑问。

遗憾的是,并没有谁告诉我更好的解决方案。

好吧,靠人不如靠己。还是自己瞎折腾吧。

但是,实现方式还是使用的我自己的提出的“如果实在是找不到好的解决方案,我打算用 IP+系统版本+浏览器版本号+.... 作为“联合主键”,如果“主键”24小时内重复两次以上,则不统计,如果cookie存在也不统计。”

1、我们在每次浏览器访问的时候都种下cookie,并设置过期时间为24小时。下次,浏览器访问的时候。我们检测如果存在我们种下的cookie。则直接忽略。

2、如果没有带上我们的cookie。我们就先组合“联合主键”。然后检测24小时内的记录有没有这个“联合主键”。如果有,则忽略,否则在原有阅读量的基础上加一,然后存入“联合主键”。

这里的"联合主键"有个小技巧。大家肯定都发现了,这个主键有点长。存数据库有点浪费空间(我数据库本来就只有50M),然后查询检索应该也会慢些吧(并不清楚)。我们想想,其实我们要的不是这么长一串东东。其实,我们只要得到这串东西代表的唯一性就可以了。那么我们可以用到md5,咱不管你是1G、2G还是高清或是无码。统统给你返回一定长度字符串(我取的是16位小写)。

随着数据的增加,这个统计阅读量的表数据,肯定是所有表中最大的。然而,我们统计阅读量是在,点击访问文章的时候,然后在统计阅读量这个环节卡太久,给人的感觉就是这个页面访问太慢,体验不好。

然而,我们每次统计都需要检测数据库里面是否存在,且数据量还不小。那我们只有再开个进程来做统计。

具体实现代码:

#region 判断是否阅读过 如果没有 这在BlogReadInfo 插入一条标识信息private bool IsRead(Blogs.ModelDB.Blogs blogobj, string md5){  if (blogobj.BlogReadInfo.Where(t => t.MD5 == md5 && t.LastTime.AddHours(24) > DateTime.Now).Count() > 0)    return true;  else  {    //BLL.    blogobj.BlogReadInfo.Add(new Blogs.ModelDB.BlogReadInfo()    {      MD5 = md5,      IsDel = false,      BlogsId = blogobj.Id,      CreateTime = DateTime.Now,      UpTime = DateTime.Now,      LastTime = DateTime.Now    });    return false;  }}#endregion

#region 统计阅读量 异步调用方法delegate void SaveReadDelegate(ModelDB.Blogs blogobj, string md5);private void SaveReadNum(ModelDB.Blogs blogobj, string md5){  LogSave.TrackLogSave(GetUserDistinguish(Request, false), "ReadBlogLog");  var isup = true;  BLL.BlogsBLL blogbll = new BLL.BlogsBLL();  var blogtemp = blogbll.GetList(t => t.Id == blogobj.Id, isAsNoTracking: false).FirstOrDefault();  if (blogtemp.BlogReadNum == null)    blogtemp.BlogReadNum = 1;  else if (!IsRead(blogtemp, md5))    blogtemp.BlogReadNum++;  else    isup = false;  if (isup)    BLL.BlogCommentSetBLL.StaticSave();}

#region 获取客户端标识(伪)/// <summary>/// 获取客户端标识 用来判断是否已经阅读过此文章/// </summary>/// <param name="requestt"></param>/// <param name="IsMD5">是否已经md5加密</param>/// <returns></returns>private string GetUserDistinguish(HttpRequestBase requestt, bool IsMD5 = true){  //request  StringBuilder str = new StringBuilder();  string ip = "";  if (requestt.ServerVariables.AllKeys.Contains("HTTP_X_FORWARDED_FOR") && requestt.ServerVariables.Get("HTTP_X_FORWARDED_FOR") != null)    ip = requestt.ServerVariables.Get("HTTP_X_FORWARDED_FOR").ToString().Trim();  else    ip = requestt.ServerVariables.Get("Remote_Addr").ToString().Trim();  str.Append("Ip:" + ip);  str.Append("\r\n浏览器:" + requestt.Browser.Browser.ToString());  str.Append("\r\n浏览器版本:" + requestt.Browser.MajorVersion.ToString());  str.Append("\r\n操作系统:" + requestt.Browser.Platform.ToString());  str.Append("\r\n页面:" + requestt.Url.ToString());  //str.Append("客户端IP:" + requestt.UserHostAddress);  str.Append("\r\n用户信息:" + User);  str.Append("\r\n浏览器标识:" + requestt.Browser.Id);  str.Append("\r\n浏览器版本号:" + requestt.Browser.Version);  str.Append("\r\n浏览器是不是测试版本:" + requestt.Browser.Beta);  //str.Append("<br/>浏览器的分辨率(像素):" + Request["width"].ToString() + "*" + Request["height"].ToString());//1280/1024              str.Append("\r\n是不是win16系统:" + requestt.Browser.Win16);  str.Append("\r\n是不是win32系统:" + requestt.Browser.Win32);  if (IsMD5)    return str.ToString().GetMd5_16();  else    return str.ToString();}#endregion

(当然,这个方式统计也不一定准。请求头信息改改就被伪造了。)

然后我们通过委托从线程池抓去线程异步调用

//........................异步调用....................new SaveReadDelegate(SaveReadNum).BeginInvoke(blogobj, GetUserDistinguish(Request), null, null);

ok,统计完事。 

 

如果您对本篇文章感兴趣,那就麻烦您点个赞,您的鼓励将是我的动力。      

当然您还可以加入QQ群:469075305讨论。

如果您有更好的处理方式,希望不要吝啬赐教。

一步步开发自己的博客 .NET版系列:http://www.cnblogs.com/zhaopei/tag/Hi-Blogs/

本文链接:http://www.cnblogs.com/zhaopei/p/4887573.html 


去加拿大旅游需要多少钱加拿大旅游大概多少钱到加拿大旅游要花多少钱去加拿大签证要几天办理加拿大签证费用2015北戴河自助游攻略 2015广州国际购物节旅游攻略 深圳周边两个摘果好去处 2015天津塘沽一日旅游攻略 河源热水漂流具体位置?河源怎么去热水漂流? 长隆水上乐园夜场开放时间?广州长隆水上乐园夜场几点开始? 三亚大洞天图片?海南三亚大洞天简介? 芦笛岩官网?桂林芦笛岩门票网站价格? 金色海岸游艇价格?深圳金色海岸游艇费用如何? 九道谷漂流买2送1是真的吗?三水九道谷漂流近期活动介绍? 2015九道谷漂流教师节活动?佛山三水九道谷漂流教师节有什么优惠? 中秋节到九道谷漂流有什么优惠活动?佛山三水九道谷漂流优惠政策? 品品茶菜 北京市清雅茶宴指南(组图) 三大城市周边 五一带你玩出精彩(组图) 上海旅游局称世博期间不会出现天价酒店 空中看世博 6家最优上海高空餐厅(组图) IDT71V416YS15BEI8 Datasheet IDT71V416YS15BEI8 Datasheet ICS348RIPT Datasheet ICS348RIPT Datasheet IDT7015L17PF Datasheet IDT7015L17PF Datasheet 东莞到楚雄黑井古镇旅游 东莞到楚雄黑井古镇旅游 东莞到楚雄黑井古镇旅游 东莞到楚雄龙江公园旅游 东莞到楚雄龙江公园旅游 东莞到楚雄龙江公园旅游 东莞到楚雄禄丰恐龙旅游 东莞到楚雄禄丰恐龙旅游 东莞到楚雄禄丰恐龙旅游