Features: - Add article view tracking with automatic deduplication (per user/IP) - Implement intelligent bot detection (35+ patterns: Google, Bing, Baidu, GPTBot, etc) - Create admin analytics dashboard with statistics and filtering - Display view count and comment count on article cards - Add search-based article filter (replaced dropdown for scalability) Analytics Dashboard: - Global stats: total views, human/bot views, unique IPs/users - Top 20 articles ranking with detailed metrics - Detailed view records with time, article, user, IP, user-agent - Filters: article title search, IP search, show/hide bots - Pagination support (50 records per page) Technical Implementation: - Async view recording (non-blocking) - Dual deduplication (application + database layer) - Database indexes for performance optimization - Batch query for comment counts - Full i18n support (Chinese/English) Files Added: - models/article_view.go: View tracking model - models/bot_detector.go: Bot detection logic - handlers/admin_analytics.go: Analytics page handler - templates/admin/analytics_views.html: Analytics UI - test_analytics.sh: Testing script - Documentation: implementation guide, usage guide, changelog Files Modified: - handlers/home.go: Add view recording and comment count queries - models/db.go: Add ArticleView to auto-migration - main.go: Add analytics routes - i18n/i18n.go: Add 35+ translation keys - templates/admin/dashboard.html: Add analytics entry link - templates/pages/home.html: Display view/comment counts on cards Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
3.2 KiB
Go
124 lines
3.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go_blog/models"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ViewAnalyticsPage renders the admin analytics page showing article view statistics.
|
|
func ViewAnalyticsPage(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
tr := getTr(c)
|
|
|
|
// Parse filters from query params
|
|
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
|
|
}
|
|
}
|
|
|
|
// Build query for view records
|
|
query := db.Model(&models.ArticleView{}).
|
|
Preload("Article").
|
|
Preload("User").
|
|
Order("created_at DESC")
|
|
|
|
// Filter by article title (join with articles table)
|
|
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)
|
|
}
|
|
|
|
// Pagination
|
|
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
|
|
|
|
// Calculate global statistics
|
|
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)
|
|
|
|
// Per-article statistics
|
|
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)
|
|
}
|
|
}
|