Featured image of post Hugo | 如何自定义实现网站统计功能

Hugo | 如何自定义实现网站统计功能

一些实现方式

目前看到网上关于网站统计功能的实现方式:

  • 基于busuanzi js的统计功能
  • 基于Waline 的统计功能
  • 基于服务器端的统计功能

关于上面的两种方式大家可以自行在网上搜索,这里就不展开讲了,主要讲一下基于自行服务端的统计功能。

基于服务器端的统计功能

优点

  • 数据源真实可靠(全部来自你自己的数据库)
  • 访问量可以根据业务逻辑定义
  • 可以进行强一致性统计(文章访问量、文章数量、评论数量等)
  • 不依赖第三方服务
  • 后续可以接入监控告警系统,实时观察
  • 后续可以在自研管理平台展示

目前统计功能

  • 网站访问量
  • 文章访问量
  • 文章总访问量
  • 网站运行时间
  • ….

实现方式

采用gin web 框架 https://github.com/gin-gonic/gin

使用 GORM 做数据库对象关系映射 https://github.com/go-gorm/gorm

以处理POST /api/article/visit,其他接口处理基本相同

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func (r *articleRepo) IncreaseViewCount(path string) (int64, error) {
	// 1. 执行 Upsert (存在则更新,不存在则插入),体现原子性操作,避免并发问题
	// SQL: INSERT INTO article_stats ... ON DUPLICATE KEY UPDATE view_count = view_count + 1
	err := r.db.Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "path"}},
		DoUpdates: clause.Assignments(map[string]interface{}{
			"view_count": gorm.Expr("view_count + 1"),
		}),
	}).Create(&model.ArticleStat{Path: path, ViewCount: 1}).Error

	if err != nil {
		return 0, err
	}

	// 2. 查出最新的值返回
	var stat model.ArticleStat
	err = r.db.Select("view_count").Where("path = ?", path).First(&stat).Error
	return stat.ViewCount, err
}

前端逻辑 上报/article/visit 接口时如果是首页(/)或者分页(/page/1)或者搜索(/search/query),也会添加到文章计数,这里排除了这个bug

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
        <script>
            (function() {
                const isLocal = ["localhost", "127.0.0.1"].includes(window.location.hostname);
        
                const API_URL = isLocal 
                    ? "http://localhost:8080/api/article/visit" 
                    : "https://hangops.top/api/article/visit"; 
                
                const path = window.location.pathname;
                // 如果是首页(/)或者分页(/page/1)或者搜索(/search/query),直接什么都不做
                if (path === "/" || 
                    path.includes("/page/") || 
                    path.includes("/search/")) {
                    return;
                }

                fetch(API_URL, {
                    method: 'POST', 
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ path: path }) 
                })
                .then(response => response.json()) 
                .then(data => {
                    const container = document.getElementById("article_views_container");
                    const viewEl = document.getElementById("article_views");
                    
                    if (container && viewEl && data.views) {
                        viewEl.innerText = data.views;
                        
                        container.style.display = "flex";
                    }
                })
                .catch(err => {
                    console.error("阅读量获取失败:", err);
                });
            })();
        </script>       
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy 🚀
文章发布: 58  |  文章总访问: ...
网站稳定运行: ...  |  网站总访问量: ...