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:
@ -2,6 +2,8 @@ package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
@ -109,6 +111,17 @@ func (ctrl *PostController) ShowPage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 已登录用户阅读计数(已发布帖子,自动去重+防刷)
|
||||
if uid > 0 && post.Status == model.PostStatusApproved {
|
||||
ctrl.postService.RecordRead(uid, post.ID)
|
||||
}
|
||||
|
||||
// 访客阅读计数(基于 Cookie 标识去重+防刷)
|
||||
if uid == 0 && post.Status == model.PostStatusApproved {
|
||||
vid := ctrl.getVisitorID(c)
|
||||
ctrl.postService.RecordGuestRead(vid, post.ID)
|
||||
}
|
||||
|
||||
ctrl.applyShortcode(post)
|
||||
|
||||
// Open Graph 社交分享数据
|
||||
@ -207,15 +220,27 @@ func (ctrl *PostController) ShowAPI(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
uid, role, _ := common.GetGinUser(c)
|
||||
|
||||
// 权限控制:非 approved 帖子仅作者和 moderator+ 可见
|
||||
if post.Status != model.PostStatusApproved {
|
||||
uid, role, _ := common.GetGinUser(c)
|
||||
if !ctrl.postService.IsPostAccessible(uid, role, post.UserID) {
|
||||
common.Error(c, http.StatusNotFound, "帖子不存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 已登录用户阅读计数(已发布帖子,自动去重+防刷)
|
||||
if uid > 0 && post.Status == model.PostStatusApproved {
|
||||
ctrl.postService.RecordRead(uid, post.ID)
|
||||
}
|
||||
|
||||
// 访客阅读计数(基于 Cookie 标识去重+防刷)
|
||||
if uid == 0 && post.Status == model.PostStatusApproved {
|
||||
vid := ctrl.getVisitorID(c)
|
||||
ctrl.postService.RecordGuestRead(vid, post.ID)
|
||||
}
|
||||
|
||||
ctrl.applyShortcode(post)
|
||||
|
||||
common.Ok(c, post)
|
||||
@ -343,6 +368,27 @@ func (ctrl *PostController) UploadImage(c *gin.Context) {
|
||||
common.VditorUploadOk(c, map[string]string{header.Filename: url})
|
||||
}
|
||||
|
||||
// visitorCookieName Cookie 名称
|
||||
const visitorCookieName = "visitor_id"
|
||||
|
||||
// visitorCookieMaxAge 访客 Cookie 有效期(30 天)
|
||||
const visitorCookieMaxAge = 30 * 24 * 3600
|
||||
|
||||
// getVisitorID 获取或创建访客标识 Cookie
|
||||
func (ctrl *PostController) getVisitorID(c *gin.Context) string {
|
||||
if cookie, err := c.Cookie(visitorCookieName); err == nil && cookie != "" {
|
||||
return cookie
|
||||
}
|
||||
// 生成 16 字节随机 hex(32 字符)
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return ""
|
||||
}
|
||||
vid := hex.EncodeToString(b)
|
||||
c.SetCookie(visitorCookieName, vid, visitorCookieMaxAge, "/", "", false, true)
|
||||
return vid
|
||||
}
|
||||
|
||||
// applyShortcode 处理帖子正文中的 shortcode 标记,替换为 HTML 占位符
|
||||
func (ctrl *PostController) applyShortcode(post *model.Post) {
|
||||
if ctrl.shortcodeSvc != nil {
|
||||
|
||||
Reference in New Issue
Block a user