- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文 - 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等 - 代码、字符串字面量、日志消息保持英文原文,零逻辑改动 - go build/vet 通过,go test -count=1 ./... 全绿
124 lines
3.1 KiB
Go
124 lines
3.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go_blog/models"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ViewAnalyticsPage 渲染展示文章浏览统计的后台分析页面。
|
|
func ViewAnalyticsPage(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
tr := getTr(c)
|
|
|
|
// 从查询参数解析筛选条件
|
|
articleTitle := c.Query("article_title")
|
|
userIDStr := c.Query("user_id")
|
|
ip := c.Query("ip")
|
|
showBots := c.Query("show_bots") == "1"
|
|
page := 1
|
|
if p := c.Query("page"); p != "" {
|
|
if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
|
|
page = parsed
|
|
}
|
|
}
|
|
|
|
// 构建浏览记录查询
|
|
query := db.Model(&models.ArticleView{}).
|
|
Preload("Article").
|
|
Preload("User").
|
|
Order("created_at DESC")
|
|
|
|
// 按文章标题筛选(联表 articles)
|
|
if articleTitle != "" {
|
|
query = query.Joins("JOIN articles ON article_views.article_id = articles.id").
|
|
Where("articles.title LIKE ?", "%"+articleTitle+"%")
|
|
}
|
|
if userIDStr != "" {
|
|
query = query.Where("user_id = ?", userIDStr)
|
|
}
|
|
if ip != "" {
|
|
query = query.Where("ip LIKE ?", "%"+ip+"%")
|
|
}
|
|
if !showBots {
|
|
query = query.Where("is_bot = ?", false)
|
|
}
|
|
|
|
// 分页
|
|
pageSize := 50
|
|
offset := (page - 1) * pageSize
|
|
var views []models.ArticleView
|
|
var totalCount int64
|
|
query.Count(&totalCount)
|
|
query.Limit(pageSize).Offset(offset).Find(&views)
|
|
|
|
hasMore := int64(offset+len(views)) < totalCount
|
|
|
|
// 计算全局统计
|
|
var stats struct {
|
|
TotalViews int64
|
|
UniqueIPs int64
|
|
UniqueUsers int64
|
|
BotViews int64
|
|
HumanViews int64
|
|
}
|
|
db.Model(&models.ArticleView{}).Count(&stats.TotalViews)
|
|
db.Model(&models.ArticleView{}).Where("is_bot = ?", true).Count(&stats.BotViews)
|
|
db.Model(&models.ArticleView{}).Where("is_bot = ?", false).Count(&stats.HumanViews)
|
|
db.Model(&models.ArticleView{}).Distinct("ip").Count(&stats.UniqueIPs)
|
|
db.Model(&models.ArticleView{}).Where("user_id IS NOT NULL").Distinct("user_id").Count(&stats.UniqueUsers)
|
|
|
|
// 按文章统计
|
|
type ArticleStat struct {
|
|
ArticleID uint
|
|
Title string
|
|
ViewCount int64
|
|
UniqueIPs int64
|
|
BotViews int64
|
|
HumanViews int64
|
|
}
|
|
var articleStats []ArticleStat
|
|
|
|
db.Raw(`
|
|
SELECT
|
|
av.article_id,
|
|
a.title,
|
|
COUNT(*) as view_count,
|
|
COUNT(DISTINCT av.ip) as unique_ips,
|
|
SUM(CASE WHEN av.is_bot THEN 1 ELSE 0 END) as bot_views,
|
|
SUM(CASE WHEN NOT av.is_bot THEN 1 ELSE 0 END) as human_views
|
|
FROM article_views av
|
|
LEFT JOIN articles a ON av.article_id = a.id
|
|
WHERE a.deleted_at IS NULL
|
|
GROUP BY av.article_id, a.title
|
|
ORDER BY view_count DESC
|
|
LIMIT 20
|
|
`).Scan(&articleStats)
|
|
|
|
data := DefaultData(c)
|
|
data["Title"] = tr["analytics_views_title"]
|
|
data["Views"] = views
|
|
data["Stats"] = stats
|
|
data["ArticleStats"] = articleStats
|
|
data["Filters"] = gin.H{
|
|
"ArticleTitle": articleTitle,
|
|
"UserID": userIDStr,
|
|
"IP": ip,
|
|
"ShowBots": showBots,
|
|
"Page": page,
|
|
}
|
|
data["Pagination"] = gin.H{
|
|
"Page": page,
|
|
"NextPage": page + 1,
|
|
"HasMore": hasMore,
|
|
"Total": totalCount,
|
|
}
|
|
|
|
c.HTML(http.StatusOK, "analytics_views", data)
|
|
}
|
|
}
|