147 lines
4.7 KiB
Go
147 lines
4.7 KiB
Go
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
|
||
}
|