Introduce a new mechanism for tracking read positions using message sequences (hasReadSeq), similar to OpenIM, to allow for O(1) unread count calculations. - Implement `UserReadSeq` caching in Redis with Lua scripts to prevent sequence regression (ensuring updates only occur if the new sequence is greater than the current one). - Update `ConversationCache` to support sequence-based unread count computation (`maxSeq - hasReadSeq`). - Refactor `MessageRepository` to use conditional updates for `last_read_seq` in the database. - Update `ChatService` to automatically update the sender's read sequence when sending a message. - Optimize `MarkAsRead` logic to update both database and Redis sequence caches and refine notification targets for private vs group chats. - Update `jpush` client to use pointer types for boolean fields to properly handle optional values in JSON.
260 lines
8.5 KiB
Go
260 lines
8.5 KiB
Go
package cache
|
||
|
||
import (
|
||
"fmt"
|
||
)
|
||
|
||
// 缓存键前缀常量
|
||
const (
|
||
// 帖子相关
|
||
PrefixPostList = "posts:list"
|
||
PrefixPost = "posts:detail"
|
||
// HotRankZSet 热门榜(有序集合,member=post_id,score=归一化热度,仅存 TopN)
|
||
HotRankZSet = "posts:hot:zset"
|
||
// HotRankTopIDs 热门榜有序 post_id 列表(JSON []string,供分层缓存本地层命中)
|
||
HotRankTopIDs = "posts:hot:top_ids"
|
||
|
||
// 会话相关
|
||
PrefixConversationList = "conversations:list"
|
||
PrefixConversationDetail = "conversations:detail"
|
||
|
||
// 群组相关
|
||
PrefixGroupMembers = "groups:members"
|
||
PrefixGroupInfo = "groups:info"
|
||
|
||
// 未读数相关
|
||
PrefixUnreadSystem = "unread:system"
|
||
PrefixUnreadConversation = "unread:conversation"
|
||
PrefixUnreadDetail = "unread:detail"
|
||
PrefixUnreadHash = "unread:hash"
|
||
PrefixUnreadTotal = "unread:total"
|
||
|
||
// 已读位置相关(OpenIM 风格:缓存 hasReadSeq,用于 O(1) 未读数计算)
|
||
PrefixUserReadSeq = "user_read_seq"
|
||
|
||
// 用户相关
|
||
PrefixUserInfo = "users:info"
|
||
PrefixUserMe = "users:me"
|
||
|
||
// 频道相关(全量列表一条缓存,供 /channels、帖子 channel 填充共用)
|
||
PrefixChannelsAllList = "channels:all_list"
|
||
|
||
// 消息缓存相关
|
||
keyPrefixMsgHash = "msg_hash" // 消息详情 Hash
|
||
keyPrefixMsgIndex = "msg_index" // 消息索引 Sorted Set
|
||
keyPrefixMsgCount = "msg_count" // 消息计数
|
||
keyPrefixMsgSeq = "msg_seq" // Seq 计数器
|
||
keyPrefixMsgPage = "msg_page" // 分页缓存
|
||
keyPrefixMsgIdempotent = "msg_idem" // 消息幂等键
|
||
)
|
||
|
||
// PostListKey 生成帖子列表缓存键
|
||
// postType: 帖子类型 (hot, follow, latest)
|
||
// page: 页码
|
||
// pageSize: 每页数量
|
||
// userID: 用户维度(仅在个性化列表如 follow 场景使用)
|
||
// HotRankZSetKey 热门榜 Redis ZSET 逻辑键(仅存 TopN,会经 normalizeKey 加前缀)
|
||
func HotRankZSetKey() string {
|
||
return HotRankZSet
|
||
}
|
||
|
||
// HotRankTopIDsKey 热门榜 Top 顺序 ID 列表键(会经 normalizeKey 加前缀)
|
||
func HotRankTopIDsKey() string {
|
||
return HotRankTopIDs
|
||
}
|
||
|
||
func HotRankZSetChannelKey(channelID string) string {
|
||
return fmt.Sprintf("%s:ch:%s", HotRankZSet, channelID)
|
||
}
|
||
|
||
func HotRankTopIDsChannelKey(channelID string) string {
|
||
return fmt.Sprintf("%s:ch:%s", HotRankTopIDs, channelID)
|
||
}
|
||
|
||
func PostListKey(postType string, userID string, page, pageSize int) string {
|
||
if userID == "" {
|
||
return fmt.Sprintf("%s:%s:%d:%d", PrefixPostList, postType, page, pageSize)
|
||
}
|
||
return fmt.Sprintf("%s:%s:%s:%d:%d", PrefixPostList, postType, userID, page, pageSize)
|
||
}
|
||
|
||
// PostDetailKey 生成帖子详情缓存键
|
||
func PostDetailKey(postID string) string {
|
||
return fmt.Sprintf("%s:%s", PrefixPost, postID)
|
||
}
|
||
|
||
// ConversationListKey 生成会话列表缓存键
|
||
func ConversationListKey(userID string, page, pageSize int) string {
|
||
return fmt.Sprintf("%s:%s:%d:%d", PrefixConversationList, userID, page, pageSize)
|
||
}
|
||
|
||
// ConversationDetailKey 生成会话详情缓存键
|
||
func ConversationDetailKey(conversationID, userID string) string {
|
||
return fmt.Sprintf("%s:%s:%s", PrefixConversationDetail, conversationID, userID)
|
||
}
|
||
|
||
// GroupMembersKey 生成群组成员缓存键
|
||
func GroupMembersKey(groupID string, page, pageSize int) string {
|
||
return fmt.Sprintf("%s:%s:page:%d:size:%d", PrefixGroupMembers, groupID, page, pageSize)
|
||
}
|
||
|
||
// GroupMembersAllKey 生成群组全量成员ID列表缓存键
|
||
func GroupMembersAllKey(groupID string) string {
|
||
return fmt.Sprintf("%s:all:%s", PrefixGroupMembers, groupID)
|
||
}
|
||
|
||
// GroupInfoKey 生成群组信息缓存键
|
||
func GroupInfoKey(groupID string) string {
|
||
return fmt.Sprintf("%s:%s", PrefixGroupInfo, groupID)
|
||
}
|
||
|
||
// UnreadSystemKey 生成系统消息未读数缓存键
|
||
func UnreadSystemKey(userID string) string {
|
||
return fmt.Sprintf("%s:%s", PrefixUnreadSystem, userID)
|
||
}
|
||
|
||
// UnreadConversationKey 生成会话未读总数缓存键
|
||
func UnreadConversationKey(userID string) string {
|
||
return fmt.Sprintf("%s:%s", PrefixUnreadConversation, userID)
|
||
}
|
||
|
||
// UnreadDetailKey 生成单个会话未读数缓存键
|
||
func UnreadDetailKey(userID, conversationID string) string {
|
||
return fmt.Sprintf("%s:%s:%s", PrefixUnreadDetail, userID, conversationID)
|
||
}
|
||
|
||
// UnreadHashKey 生成用户未读数 Hash 缓存键
|
||
func UnreadHashKey(userID string) string {
|
||
return fmt.Sprintf("%s:%s", PrefixUnreadHash, userID)
|
||
}
|
||
|
||
// UnreadTotalKey 生成用户总未读数计数器键
|
||
func UnreadTotalKey(userID string) string {
|
||
return fmt.Sprintf("%s:%s", PrefixUnreadTotal, userID)
|
||
}
|
||
|
||
// UserInfoKey 生成用户信息缓存键
|
||
func UserInfoKey(userID string) string {
|
||
return fmt.Sprintf("%s:%s", PrefixUserInfo, userID)
|
||
}
|
||
|
||
// UserMeKey 生成当前用户信息缓存键
|
||
func UserMeKey(userID string) string {
|
||
return fmt.Sprintf("%s:%s", PrefixUserMe, userID)
|
||
}
|
||
|
||
// ChannelsAllListKey 频道全量列表缓存键(含未启用,供 MapByIDs 解析历史帖子 channel)
|
||
func ChannelsAllListKey() string {
|
||
return PrefixChannelsAllList
|
||
}
|
||
|
||
// InvalidateChannelList 失效频道列表缓存(管理端增删改频道后调用)
|
||
func InvalidateChannelList(c Cache) {
|
||
c.Delete(ChannelsAllListKey())
|
||
}
|
||
|
||
// InvalidatePostList 失效帖子列表缓存
|
||
func InvalidatePostList(cache Cache) {
|
||
cache.DeleteByPrefix(PrefixPostList)
|
||
}
|
||
|
||
// InvalidatePostDetail 失效帖子详情缓存
|
||
func InvalidatePostDetail(cache Cache, postID string) {
|
||
cache.Delete(PostDetailKey(postID))
|
||
}
|
||
|
||
// InvalidateConversationList 失效会话列表缓存
|
||
func InvalidateConversationList(cache Cache, userID string) {
|
||
cache.DeleteByPrefix(PrefixConversationList + ":" + userID + ":")
|
||
}
|
||
|
||
// InvalidateConversationDetail 失效会话详情缓存
|
||
func InvalidateConversationDetail(cache Cache, conversationID, userID string) {
|
||
cache.Delete(ConversationDetailKey(conversationID, userID))
|
||
}
|
||
|
||
// InvalidateGroupMembers 失效群组成员缓存
|
||
func InvalidateGroupMembers(cache Cache, groupID string) {
|
||
cache.DeleteByPrefix(PrefixGroupMembers + ":" + groupID)
|
||
}
|
||
|
||
// InvalidateGroupInfo 失效群组信息缓存
|
||
func InvalidateGroupInfo(cache Cache, groupID string) {
|
||
cache.Delete(GroupInfoKey(groupID))
|
||
}
|
||
|
||
// InvalidateUnreadSystem 失效系统消息未读数缓存
|
||
func InvalidateUnreadSystem(cache Cache, userID string) {
|
||
cache.Delete(UnreadSystemKey(userID))
|
||
}
|
||
|
||
// InvalidateUnreadConversation 失效会话未读数缓存
|
||
func InvalidateUnreadConversation(cache Cache, userID string) {
|
||
cache.Delete(UnreadConversationKey(userID))
|
||
}
|
||
|
||
// InvalidateUnreadDetail 失效单个会话未读数缓存
|
||
func InvalidateUnreadDetail(cache Cache, userID, conversationID string) {
|
||
cache.Delete(UnreadDetailKey(userID, conversationID))
|
||
}
|
||
|
||
// InvalidateUserInfo 失效用户信息缓存
|
||
func InvalidateUserInfo(cache Cache, userID string) {
|
||
cache.Delete(UserInfoKey(userID))
|
||
cache.Delete(UserMeKey(userID))
|
||
}
|
||
|
||
// ============================================================
|
||
// 消息缓存 Key 生成函数
|
||
// ============================================================
|
||
|
||
// MessageHashKey 消息详情 Hash key
|
||
func MessageHashKey(convID string) string {
|
||
return fmt.Sprintf("%s:%s", keyPrefixMsgHash, convID)
|
||
}
|
||
|
||
// MessageIndexKey 消息索引 Sorted Set key
|
||
func MessageIndexKey(convID string) string {
|
||
return fmt.Sprintf("%s:%s", keyPrefixMsgIndex, convID)
|
||
}
|
||
|
||
// MessageCountKey 消息计数 key
|
||
func MessageCountKey(convID string) string {
|
||
return fmt.Sprintf("%s:%s", keyPrefixMsgCount, convID)
|
||
}
|
||
|
||
// MessageSeqKey Seq 计数器 key
|
||
func MessageSeqKey(convID string) string {
|
||
return fmt.Sprintf("%s:%s", keyPrefixMsgSeq, convID)
|
||
}
|
||
|
||
// MessagePageKey 分页缓存 key
|
||
func MessagePageKey(convID string, page, pageSize int) string {
|
||
return fmt.Sprintf("%s:%s:%d:%d", keyPrefixMsgPage, convID, page, pageSize)
|
||
}
|
||
|
||
// InvalidateMessagePages 失效会话消息分页缓存
|
||
func InvalidateMessagePages(cache Cache, conversationID string) {
|
||
cache.DeleteByPrefix(fmt.Sprintf("%s:%s:", keyPrefixMsgPage, conversationID))
|
||
}
|
||
|
||
// MessageIdempotentKey 消息幂等键 (clientMsgID -> server MsgID)
|
||
func MessageIdempotentKey(senderID, clientMsgID string) string {
|
||
return fmt.Sprintf("%s:%s:%s", keyPrefixMsgIdempotent, senderID, clientMsgID)
|
||
}
|
||
|
||
// PushDedupKey 推送去重键,防止同一消息重复推送给同一用户
|
||
func PushDedupKey(userID, messageID string) string {
|
||
return fmt.Sprintf("push_dedup:%s:%s", userID, messageID)
|
||
}
|
||
|
||
// UserReadSeqKey 用户已读位置缓存键(OpenIM 风格 SEQ_USER_READ:{convID}:{userID})
|
||
func UserReadSeqKey(convID, userID string) string {
|
||
return fmt.Sprintf("%s:%s:%s", PrefixUserReadSeq, convID, userID)
|
||
}
|
||
|
||
// ConvMaxSeqKey 会话最大 seq 缓存键
|
||
func ConvMaxSeqKey(convID string) string {
|
||
return fmt.Sprintf("%s:%s", keyPrefixMsgSeq, convID)
|
||
}
|