feat: 新增文章阅读量计数(已登录去重 + 访客防刷)

- Post 模型新增 ViewsCount 冗余计数字段
- 新建 PostReadLog 表(user_id + post_id 唯一索引),已登录用户每篇文章只计一次
- 新建 PostGuestReadLog 表(visitor_id + post_id 唯一索引),访客基于 Cookie 标识去重
- Repository 层新增 RecordRead / RecordGuestRead / IncrementViewsCount
- Service 层编排去重-计数逻辑,计数失败不影响页面渲染
- Controller 层 ShowPage + ShowAPI 触发计数,仅 approved 状态生效
- 双层防刷:唯一索引去重 + 2 秒冷却间隔
- 访客 Cookie (visitor_id) 基于 crypto/rand 生成,30 天有效期
This commit is contained in:
2026-06-02 00:02:48 +08:00
parent b869602185
commit b9d90fe7ec
8 changed files with 145 additions and 3 deletions

View File

@ -1,9 +1,12 @@
package repository
import (
"time"
"metazone.cc/metalab/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// PostRepo 帖子数据访问
@ -189,3 +192,52 @@ func (r *PostRepo) Restore(id uint) error {
}
return nil
}
// readCooldown 两次阅读记录最小间隔
const readCooldown = 2 * time.Second
// RecordRead 记录已登录用户阅读文章(防刷量:取去重+冷却)
// 返回 true 表示首次阅读新记录false 表示重复或冷却中
func (r *PostRepo) RecordRead(userID, postID uint) (bool, error) {
// 冷却检查:同用户两次阅读之间至少间隔 2 秒
var lastRead model.PostReadLog
if err := r.db.Where("user_id = ?", userID).
Order("read_at DESC").First(&lastRead).Error; err == nil {
if time.Since(lastRead.ReadAt) < readCooldown {
return false, nil
}
}
result := r.db.Clauses(clause.OnConflict{DoNothing: true}).
Create(&model.PostReadLog{UserID: userID, PostID: postID})
if result.Error != nil {
return false, result.Error
}
return result.RowsAffected > 0, nil
}
// RecordGuestRead 记录访客阅读文章(基于 Cookie visitor_id 去重+冷却)
// 返回 true 表示首次阅读新记录false 表示重复或冷却中
func (r *PostRepo) RecordGuestRead(visitorID string, postID uint) (bool, error) {
// 冷却检查:同访客两次阅读之间至少间隔 2 秒
var lastRead model.PostGuestReadLog
if err := r.db.Where("visitor_id = ?", visitorID).
Order("read_at DESC").First(&lastRead).Error; err == nil {
if time.Since(lastRead.ReadAt) < readCooldown {
return false, nil
}
}
result := r.db.Clauses(clause.OnConflict{DoNothing: true}).
Create(&model.PostGuestReadLog{VisitorID: visitorID, PostID: postID})
if result.Error != nil {
return false, result.Error
}
return result.RowsAffected > 0, nil
}
// IncrementViewsCount 阅读数 +1
func (r *PostRepo) IncrementViewsCount(postID uint) error {
return r.db.Model(&model.Post{}).Where("id = ?", postID).
Update("views_count", gorm.Expr("views_count + 1")).Error
}