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>
49 lines
1.5 KiB
Go
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
|
|
}
|