- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文 - 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等 - 代码、字符串字面量、日志消息保持英文原文,零逻辑改动 - go build/vet 通过,go test -count=1 ./... 全绿
49 lines
1.5 KiB
Go
49 lines
1.5 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ArticleView 表示一条唯一的文章浏览记录。
|
|
// 每条记录跟踪一次(按 IP 或用户)对文章的唯一访问。
|
|
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
|
|
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 覆盖 GORM 默认的表名。
|
|
func (ArticleView) TableName() string {
|
|
return "article_views"
|
|
}
|
|
|
|
// BeforeCreate 钩子确保不会创建重复记录。
|
|
// 这是在应用层去重之外的另一道安全校验。
|
|
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 {
|
|
// 记录已存在,跳过创建
|
|
return gorm.ErrDuplicatedKey
|
|
}
|
|
|
|
return nil
|
|
}
|