feat: 接入论坛评论树缓存

This commit is contained in:
Losita
2026-05-05 11:10:13 +08:00
parent c42f0c5b8c
commit 2204fac84e
5 changed files with 280 additions and 25 deletions

View File

@@ -0,0 +1,146 @@
package dao
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
forumcontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/taskclassforum"
"github.com/go-redis/redis/v8"
)
const commentTreeCacheTTL = 2 * time.Minute
type commentTreeCachePayload struct {
Items []forumcontracts.ForumCommentNode `json:"items"`
Page forumcontracts.PageResult `json:"page"`
}
// CommentTreeCache 承载计划广场评论树的 Redis 缓存能力。
//
// 职责边界:
// 1. 只负责评论树读模型的 JSON 缓存和版本号失效,不读写 MySQL
// 2. 不计算当前用户是否可删除评论,避免把用户视角写进共享缓存;
// 3. Redis 异常向上返回,由 service 层决定是否降级回源 DB。
type CommentTreeCache struct {
client *redis.Client
ttl time.Duration
}
func NewCommentTreeCache(client *redis.Client) *CommentTreeCache {
return &CommentTreeCache{
client: client,
ttl: commentTreeCacheTTL,
}
}
func commentTreeVersionKey(postID uint64) string {
return fmt.Sprintf("forum:comments:%d:version", postID)
}
func commentTreeDataKey(postID uint64, version int64, sort string, page int, pageSize int) string {
return fmt.Sprintf(
"forum:comments:%d:v%d:sort:%s:page:%d:size:%d",
postID,
version,
strings.TrimSpace(sort),
page,
pageSize,
)
}
// GetCommentTree 读取指定帖子、排序和分页维度下的评论树缓存。
//
// 返回值语义:
// 1. hit=true 表示命中缓存items/page 可直接用于返回前的用户视角补全;
// 2. hit=false 且 error=nil 表示未命中,调用方应回源 DB
// 3. error 非空表示 Redis 或 JSON 异常,调用方应记录日志并回源 DB。
func (c *CommentTreeCache) GetCommentTree(ctx context.Context, postID uint64, page int, pageSize int, sort string) ([]forumcontracts.ForumCommentNode, forumcontracts.PageResult, bool, error) {
if c == nil || c.client == nil {
return nil, forumcontracts.PageResult{}, false, errors.New("评论树缓存未初始化")
}
version, err := c.currentCommentTreeVersion(ctx, postID)
if err != nil {
return nil, forumcontracts.PageResult{}, false, err
}
raw, err := c.client.Get(ctx, commentTreeDataKey(postID, version, sort, page, pageSize)).Result()
if errors.Is(err, redis.Nil) {
return nil, forumcontracts.PageResult{}, false, nil
}
if err != nil {
return nil, forumcontracts.PageResult{}, false, err
}
var payload commentTreeCachePayload
if err = json.Unmarshal([]byte(raw), &payload); err != nil {
return nil, forumcontracts.PageResult{}, false, err
}
if payload.Items == nil {
payload.Items = []forumcontracts.ForumCommentNode{}
}
return payload.Items, payload.Page, true, nil
}
// SetCommentTree 写入指定帖子、排序和分页维度下的评论树缓存。
//
// 步骤说明:
// 1. 先读取当前版本号,保证写入 key 与后续读取 key 一致;
// 2. 再序列化去个性化后的评论树,避免缓存里带入某个用户的 can_delete
// 3. 最后写入短 TTL让版本失效失败时也能靠自然过期兜底。
func (c *CommentTreeCache) SetCommentTree(ctx context.Context, postID uint64, page int, pageSize int, sort string, items []forumcontracts.ForumCommentNode, pageResult forumcontracts.PageResult) error {
if c == nil || c.client == nil {
return errors.New("评论树缓存未初始化")
}
version, err := c.currentCommentTreeVersion(ctx, postID)
if err != nil {
return err
}
if items == nil {
items = []forumcontracts.ForumCommentNode{}
}
data, err := json.Marshal(commentTreeCachePayload{
Items: items,
Page: pageResult,
})
if err != nil {
return err
}
return c.client.Set(ctx, commentTreeDataKey(postID, version, sort, page, pageSize), data, c.ttl).Err()
}
// BumpCommentTreeVersion 递增帖子评论树版本号,让旧分页缓存自然失效。
//
// 职责边界:
// 1. 只做版本递增,不扫描删除旧 data key避免写评论时阻塞 Redis
// 2. 旧 data key 依赖短 TTL 自动回收;
// 3. 当 version key 不存在时 INCR 会从 1 开始,能够让默认 v0 缓存失效。
func (c *CommentTreeCache) BumpCommentTreeVersion(ctx context.Context, postID uint64) error {
if c == nil || c.client == nil {
return errors.New("评论树缓存未初始化")
}
return c.client.Incr(ctx, commentTreeVersionKey(postID)).Err()
}
func (c *CommentTreeCache) currentCommentTreeVersion(ctx context.Context, postID uint64) (int64, error) {
raw, err := c.client.Get(ctx, commentTreeVersionKey(postID)).Result()
if errors.Is(err, redis.Nil) {
return 0, nil
}
if err != nil {
return 0, err
}
version, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err != nil {
return 0, err
}
if version < 0 {
return 0, nil
}
return version, nil
}