Files
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

49 lines
1.5 KiB
Go

package models
import (
"time"
"gorm.io/gorm"
)
// ArticleView represents a unique article view record.
// Each record tracks one unique visit (by IP or user) to an article.
type ArticleView struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"created_at"`
ArticleID uint `gorm:"not null;index:idx_article_views_article" json:"article_id"`
UserID *uint `gorm:"index:idx_article_views_user" json:"user_id"` // NULL for anonymous users
IP string `gorm:"size:64;not null;index:idx_article_views_ip" json:"ip"`
UserAgent string `gorm:"size:512" json:"user_agent"`
IsBot bool `gorm:"default:false;index:idx_article_views_bot" json:"is_bot"`
Article Article `gorm:"foreignKey:ArticleID" json:"-"`
User *User `gorm:"foreignKey:UserID" json:"-"`
}
// TableName overrides the default GORM table name.
func (ArticleView) TableName() string {
return "article_views"
}
// BeforeCreate hook to ensure we don't create duplicate records.
// This is a safety check in addition to application-level deduplication.
func (av *ArticleView) BeforeCreate(tx *gorm.DB) error {
var count int64
query := tx.Model(&ArticleView{}).
Where("article_id = ? AND ip = ?", av.ArticleID, av.IP)
if av.UserID != nil {
query = query.Where("user_id = ?", *av.UserID)
} else {
query = query.Where("user_id IS NULL")
}
query.Count(&count)
if count > 0 {
// Record already exists, skip creation
return gorm.ErrDuplicatedKey
}
return nil
}