Files
go_blog/handlers/admin_analytics.go
kevinandClaude Fable 5 da7a39c1c8 feat: implement comprehensive reading analytics system with bot detection
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>
2026-06-22 19:57:12 +08:00

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)
}
}