feat(chat): implement sequence-based unread count tracking
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.
This commit is contained in:
166
internal/cache/conversation_cache.go
vendored
166
internal/cache/conversation_cache.go
vendored
@@ -484,6 +484,172 @@ func (c *ConversationCache) InvalidateUnreadCount(userID, convID string) {
|
||||
c.cache.Delete(UnreadDetailKey(userID, convID))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 已读位置缓存(OpenIM 风格 hasReadSeq)
|
||||
// ============================================================
|
||||
|
||||
// readSeqExpire 已读位置缓存 TTL
|
||||
const readSeqExpire = 30 * 24 * time.Hour
|
||||
|
||||
// SetUserReadSeq 设置用户在某会话的已读位置(防回退:仅当新值大于当前值时更新)
|
||||
func (c *ConversationCache) SetUserReadSeq(ctx context.Context, convID, userID string, seq int64) error {
|
||||
key := UserReadSeqKey(convID, userID)
|
||||
|
||||
redisCache, ok := c.cache.(*RedisCache)
|
||||
if !ok {
|
||||
// 非 Redis 模式,直接设置
|
||||
c.cache.Set(key, seq, readSeqExpire)
|
||||
return nil
|
||||
}
|
||||
|
||||
rdb := redisCache.GetRedisClient().GetClient()
|
||||
nKey := normalizeKey(key)
|
||||
// Lua 脚本:仅当新 seq > 当前值时才更新,防回退
|
||||
script := redis.NewScript(`
|
||||
local current = tonumber(redis.call('GET', KEYS[1]))
|
||||
if current and current >= tonumber(ARGV[1]) then
|
||||
return 0
|
||||
end
|
||||
redis.call('SET', KEYS[1], ARGV[1])
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[2])
|
||||
return 1
|
||||
`)
|
||||
_, err := script.Run(ctx, rdb, []string{nKey}, seq, int64(readSeqExpire.Seconds())).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUserReadSeq 获取用户在某会话的已读位置
|
||||
func (c *ConversationCache) GetUserReadSeq(ctx context.Context, convID, userID string) (int64, error) {
|
||||
key := UserReadSeqKey(convID, userID)
|
||||
|
||||
redisCache, ok := c.cache.(*RedisCache)
|
||||
if !ok {
|
||||
raw, ok := c.cache.Get(key)
|
||||
if !ok {
|
||||
return 0, ErrKeyNotFound
|
||||
}
|
||||
switch v := raw.(type) {
|
||||
case int64:
|
||||
return v, nil
|
||||
case string:
|
||||
var n int64
|
||||
fmt.Sscanf(v, "%d", &n)
|
||||
return n, nil
|
||||
}
|
||||
return 0, ErrKeyNotFound
|
||||
}
|
||||
|
||||
rdb := redisCache.GetRedisClient().GetClient()
|
||||
nKey := normalizeKey(key)
|
||||
val, err := rdb.Get(ctx, nKey).Int64()
|
||||
if err != nil {
|
||||
return 0, ErrKeyNotFound
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// GetUserReadSeqs 批量获取用户在多个会话的已读位置
|
||||
func (c *ConversationCache) GetUserReadSeqs(ctx context.Context, userID string, convIDs []string) (map[string]int64, error) {
|
||||
if len(convIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
redisCache, ok := c.cache.(*RedisCache)
|
||||
if !ok {
|
||||
result := make(map[string]int64, len(convIDs))
|
||||
for _, convID := range convIDs {
|
||||
if seq, err := c.GetUserReadSeq(ctx, convID, userID); err == nil {
|
||||
result[convID] = seq
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rdb := redisCache.GetRedisClient().GetClient()
|
||||
keys := make([]string, len(convIDs))
|
||||
for i, convID := range convIDs {
|
||||
keys[i] = normalizeKey(UserReadSeqKey(convID, userID))
|
||||
}
|
||||
|
||||
vals, err := rdb.MGet(ctx, keys...).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]int64, len(convIDs))
|
||||
for i, val := range vals {
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
var seq int64
|
||||
fmt.Sscanf(fmt.Sprint(val), "%d", &seq)
|
||||
result[convIDs[i]] = seq
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetConvMaxSeq 获取会话的最大 seq(复用 msg_seq 计数器,读取当前值)
|
||||
func (c *ConversationCache) GetConvMaxSeq(ctx context.Context, convID string) (int64, error) {
|
||||
seqKey := MessageSeqKey(convID)
|
||||
|
||||
redisCache, ok := c.cache.(*RedisCache)
|
||||
if !ok {
|
||||
return 0, ErrKeyNotFound
|
||||
}
|
||||
|
||||
rdb := redisCache.GetRedisClient().GetClient()
|
||||
nKey := normalizeKey(seqKey)
|
||||
val, err := rdb.Get(ctx, nKey).Int64()
|
||||
if err != nil {
|
||||
return 0, ErrKeyNotFound
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// ComputeUnreadCount 计算单个会话未读数:maxSeq - hasReadSeq
|
||||
func (c *ConversationCache) ComputeUnreadCount(ctx context.Context, convID, userID string) (int64, error) {
|
||||
maxSeq, err := c.GetConvMaxSeq(ctx, convID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
readSeq, err := c.GetUserReadSeq(ctx, convID, userID)
|
||||
if err != nil {
|
||||
// 未读到已读位置,视为 0,返回 maxSeq
|
||||
return maxSeq, nil
|
||||
}
|
||||
unread := maxSeq - readSeq
|
||||
if unread < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return unread, nil
|
||||
}
|
||||
|
||||
// ComputeAllUnreadCount 计算所有会话未读总数:sum(maxSeq - hasReadSeq)
|
||||
func (c *ConversationCache) ComputeAllUnreadCount(ctx context.Context, userID string, convIDs []string, maxSeqs map[string]int64) (int64, error) {
|
||||
readSeqs, err := c.GetUserReadSeqs(ctx, userID, convIDs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var total int64
|
||||
for _, convID := range convIDs {
|
||||
maxSeq, ok := maxSeqs[convID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
readSeq, hasRead := readSeqs[convID]
|
||||
if !hasRead {
|
||||
total += maxSeq
|
||||
continue
|
||||
}
|
||||
unread := maxSeq - readSeq
|
||||
if unread > 0 {
|
||||
total += unread
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// IncrementUnread 递增用户在某会话的未读数(Redis Hash + 总数计数器)
|
||||
func (c *ConversationCache) IncrementUnread(ctx context.Context, userID, convID string) error {
|
||||
hashKey := UnreadHashKey(userID)
|
||||
|
||||
13
internal/cache/keys.go
vendored
13
internal/cache/keys.go
vendored
@@ -29,6 +29,9 @@ const (
|
||||
PrefixUnreadHash = "unread:hash"
|
||||
PrefixUnreadTotal = "unread:total"
|
||||
|
||||
// 已读位置相关(OpenIM 风格:缓存 hasReadSeq,用于 O(1) 未读数计算)
|
||||
PrefixUserReadSeq = "user_read_seq"
|
||||
|
||||
// 用户相关
|
||||
PrefixUserInfo = "users:info"
|
||||
PrefixUserMe = "users:me"
|
||||
@@ -244,3 +247,13 @@ func MessageIdempotentKey(senderID, clientMsgID string) string {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -758,7 +758,7 @@ func (h *MessageHandler) HandleGetConversation(c *gin.Context) {
|
||||
// 获取参与者信息
|
||||
participants, _ := h.getConversationParticipants(c.Request.Context(), conversationID, userID)
|
||||
|
||||
// 获取当前用户的已读位置
|
||||
// 获取当前用户的已读位置(优先从 Redis 缓存读取)
|
||||
myLastReadSeq := int64(0)
|
||||
isPinned := false
|
||||
notificationMuted := false
|
||||
@@ -772,7 +772,7 @@ func (h *MessageHandler) HandleGetConversation(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取对方用户的已读位置
|
||||
// 获取对方用户的已读位置(优先从 Redis 缓存读取)
|
||||
otherLastReadSeq := int64(0)
|
||||
for _, p := range allParticipants {
|
||||
if p.UserID != userID {
|
||||
|
||||
@@ -93,8 +93,8 @@ type IOSNotif struct {
|
||||
Alert any `json:"alert,omitempty"`
|
||||
Sound any `json:"sound,omitempty"`
|
||||
Badge string `json:"badge,omitempty"`
|
||||
MutableContent bool `json:"mutable-content,omitempty"`
|
||||
ContentAvailable bool `json:"content-available,omitempty"`
|
||||
MutableContent *bool `json:"mutable-content,omitempty"`
|
||||
ContentAvailable *bool `json:"content-available,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
ThreadID string `json:"thread-id,omitempty"`
|
||||
Extras map[string]any `json:"extras,omitempty"`
|
||||
|
||||
@@ -278,27 +278,41 @@ func (r *messageRepository) GetParticipant(conversationID string, userID string)
|
||||
return &participant, nil
|
||||
}
|
||||
|
||||
// UpdateLastReadSeq 更新已读位置
|
||||
// UpdateLastReadSeq 更新已读位置(防回退:仅当新值大于当前值时才更新)
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *messageRepository) UpdateLastReadSeq(conversationID string, userID string, lastReadSeq int64) error {
|
||||
// 使用条件更新防止已读位置回退
|
||||
result := r.db.Model(&model.ConversationParticipant{}).
|
||||
Where("conversation_id = ? AND user_id = ?", conversationID, userID).
|
||||
Where("conversation_id = ? AND user_id = ? AND last_read_seq < ?", conversationID, userID, lastReadSeq).
|
||||
Update("last_read_seq", lastReadSeq)
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// 如果没有更新任何记录,说明参与者记录不存在,需要插入
|
||||
// 如果没有更新任何记录,可能是参与者记录不存在,尝试插入
|
||||
if result.RowsAffected == 0 {
|
||||
// 尝试插入新记录(跨数据库 upsert)
|
||||
err := r.db.Clauses(clause.OnConflict{
|
||||
// 检查是否已有记录但 seq 不需要更新
|
||||
var existingSeq int64
|
||||
err := r.db.Model(&model.ConversationParticipant{}).
|
||||
Where("conversation_id = ? AND user_id = ?", conversationID, userID).
|
||||
Select("last_read_seq").Scan(&existingSeq).Error
|
||||
if err == nil && existingSeq >= lastReadSeq {
|
||||
// 已有记录且 seq 不需要回退,直接返回成功
|
||||
return nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 参与者记录不存在,插入新记录
|
||||
err = r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{
|
||||
{Name: "conversation_id"},
|
||||
{Name: "user_id"},
|
||||
},
|
||||
DoUpdates: clause.Assignments(map[string]any{
|
||||
"last_read_seq": lastReadSeq,
|
||||
"last_read_seq": gorm.Expr("CASE WHEN last_read_seq < ? THEN ? ELSE last_read_seq END", lastReadSeq, lastReadSeq),
|
||||
"updated_at": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||
}),
|
||||
}).Create(&model.ConversationParticipant{
|
||||
|
||||
@@ -60,6 +60,9 @@ type ChatService interface {
|
||||
// 批量查询(解决 N+1 问题)
|
||||
GetUnreadCountBatch(ctx context.Context, userID string, convIDs []string) (map[string]int64, error)
|
||||
GetLastMessagesBatch(ctx context.Context, convIDs []string) (map[string]*model.Message, error)
|
||||
|
||||
// 已读位置(OpenIM 风格缓存)
|
||||
GetUserReadSeq(ctx context.Context, conversationID string, userID string) (int64, error)
|
||||
}
|
||||
|
||||
// chatServiceImpl 聊天服务实现
|
||||
@@ -355,6 +358,12 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
||||
return nil, fmt.Errorf("failed to save message: %w", err)
|
||||
}
|
||||
|
||||
// 发送者自动已读:将发送者的 lastReadSeq 更新到消息 seq
|
||||
_ = s.repo.UpdateLastReadSeq(conversationID, senderID, message.Seq)
|
||||
if s.conversationCache != nil {
|
||||
_ = s.conversationCache.SetUserReadSeq(context.Background(), conversationID, senderID, message.Seq)
|
||||
}
|
||||
|
||||
// 新消息会改变分页结果,先失效分页缓存,避免读到旧列表
|
||||
if s.conversationCache != nil {
|
||||
s.conversationCache.InvalidateMessagePages(conversationID)
|
||||
@@ -404,7 +413,7 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
||||
if p.UserID == senderID {
|
||||
continue
|
||||
}
|
||||
if totalUnread, uErr := s.repo.GetAllUnreadCount(p.UserID); uErr == nil {
|
||||
if totalUnread, uErr := s.GetAllUnreadCount(ctx, p.UserID); uErr == nil {
|
||||
s.publishToUsers([]string{p.UserID}, "conversation_unread", map[string]any{
|
||||
"conversation_id": conversationID,
|
||||
"total_unread": totalUnread,
|
||||
@@ -590,8 +599,10 @@ func (s *chatServiceImpl) MarkAsRead(ctx context.Context, conversationID string,
|
||||
return fmt.Errorf("failed to update last read seq: %w", err)
|
||||
}
|
||||
|
||||
// 2. DB 写入成功后,清除 Redis 未读数
|
||||
// 2. 更新 Redis hasReadSeq 缓存(防回退)
|
||||
if s.conversationCache != nil {
|
||||
_ = s.conversationCache.SetUserReadSeq(ctx, conversationID, userID, seq)
|
||||
// 清除旧式 Hash 未读数缓存
|
||||
if clearErr := s.conversationCache.ClearUnread(ctx, userID, conversationID); clearErr != nil {
|
||||
zap.L().Warn("clear unread from redis hash failed",
|
||||
zap.String("userID", userID),
|
||||
@@ -611,17 +622,23 @@ func (s *chatServiceImpl) MarkAsRead(ctx context.Context, conversationID string,
|
||||
if pErr == nil {
|
||||
detailType := "private"
|
||||
groupID := ""
|
||||
if conv, convErr := s.getConversation(ctx, conversationID); convErr == nil && conv.Type == model.ConversationTypeGroup {
|
||||
conv, _ := s.getConversation(ctx, conversationID)
|
||||
if conv != nil && conv.Type == model.ConversationTypeGroup {
|
||||
detailType = "group"
|
||||
if conv.GroupID != nil {
|
||||
groupID = *conv.GroupID
|
||||
}
|
||||
}
|
||||
targetIDs := make([]string, 0, len(participants))
|
||||
for _, p := range participants {
|
||||
targetIDs = append(targetIDs, p.UserID)
|
||||
|
||||
// 私聊:通知所有参与者(含对方);群聊:只通知自己
|
||||
readTargets := []string{userID}
|
||||
if conv != nil && conv.Type == model.ConversationTypePrivate {
|
||||
readTargets = make([]string, 0, len(participants))
|
||||
for _, p := range participants {
|
||||
readTargets = append(readTargets, p.UserID)
|
||||
}
|
||||
}
|
||||
s.publishToUsers(targetIDs, "message_read", map[string]any{
|
||||
s.publishToUsers(readTargets, "message_read", map[string]any{
|
||||
"detail_type": detailType,
|
||||
"conversation_id": conversationID,
|
||||
"group_id": groupID,
|
||||
@@ -629,7 +646,7 @@ func (s *chatServiceImpl) MarkAsRead(ctx context.Context, conversationID string,
|
||||
"seq": seq,
|
||||
})
|
||||
}
|
||||
if totalUnread, uErr := s.repo.GetAllUnreadCount(userID); uErr == nil {
|
||||
if totalUnread, uErr := s.GetAllUnreadCount(ctx, userID); uErr == nil {
|
||||
s.publishToUsers([]string{userID}, "conversation_unread", map[string]any{
|
||||
"conversation_id": conversationID,
|
||||
"total_unread": totalUnread,
|
||||
@@ -650,13 +667,15 @@ func (s *chatServiceImpl) GetUnreadCount(ctx context.Context, conversationID str
|
||||
return 0, fmt.Errorf("failed to get participant: %w", err)
|
||||
}
|
||||
|
||||
// 优先从 Redis Hash 读取
|
||||
// 优先算术计算:maxSeq - hasReadSeq(OpenIM 风格,O(1) 无 DB)
|
||||
if s.conversationCache != nil {
|
||||
count, err := s.conversationCache.GetUnreadCountFromHash(ctx, userID, conversationID)
|
||||
if err == nil {
|
||||
if count, err := s.conversationCache.ComputeUnreadCount(ctx, conversationID, userID); err == nil {
|
||||
return count, nil
|
||||
}
|
||||
// 降级到 Redis Hash
|
||||
if count, err := s.conversationCache.GetUnreadCountFromHash(ctx, userID, conversationID); err == nil {
|
||||
return count, nil
|
||||
}
|
||||
// 降级到旧缓存路径
|
||||
return s.conversationCache.GetUnreadCount(ctx, userID, conversationID)
|
||||
}
|
||||
|
||||
@@ -665,8 +684,24 @@ func (s *chatServiceImpl) GetUnreadCount(ctx context.Context, conversationID str
|
||||
|
||||
// GetAllUnreadCount 获取所有会话的未读消息总数
|
||||
func (s *chatServiceImpl) GetAllUnreadCount(ctx context.Context, userID string) (int64, error) {
|
||||
// 优先从 Redis Hash 读取
|
||||
// 优先算术计算:sum(maxSeq - hasReadSeq)(OpenIM 风格)
|
||||
if s.conversationCache != nil {
|
||||
if convs, _, err := s.GetConversationList(ctx, userID, 1, 200); err == nil && len(convs) > 0 {
|
||||
convIDs := make([]string, len(convs))
|
||||
maxSeqs := make(map[string]int64, len(convs))
|
||||
for i, conv := range convs {
|
||||
convIDs[i] = conv.ID
|
||||
if maxSeq, err := s.conversationCache.GetConvMaxSeq(ctx, conv.ID); err == nil {
|
||||
maxSeqs[conv.ID] = maxSeq
|
||||
}
|
||||
}
|
||||
if len(maxSeqs) > 0 {
|
||||
if total, err := s.conversationCache.ComputeAllUnreadCount(ctx, userID, convIDs, maxSeqs); err == nil {
|
||||
return total, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// 降级到 Redis Hash
|
||||
if total, err := s.conversationCache.GetTotalUnread(ctx, userID); err == nil {
|
||||
return total, nil
|
||||
}
|
||||
@@ -886,6 +921,55 @@ func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conv
|
||||
|
||||
// GetUnreadCountBatch 批量获取用户在多个会话中的未读数
|
||||
func (s *chatServiceImpl) GetUnreadCountBatch(ctx context.Context, userID string, convIDs []string) (map[string]int64, error) {
|
||||
// 优先算术计算:maxSeq - hasReadSeq(OpenIM 风格)
|
||||
if s.conversationCache != nil && len(convIDs) > 0 {
|
||||
maxSeqs := make(map[string]int64, len(convIDs))
|
||||
for _, convID := range convIDs {
|
||||
if maxSeq, err := s.conversationCache.GetConvMaxSeq(ctx, convID); err == nil {
|
||||
maxSeqs[convID] = maxSeq
|
||||
}
|
||||
}
|
||||
if len(maxSeqs) > 0 {
|
||||
readSeqs, err := s.conversationCache.GetUserReadSeqs(ctx, userID, convIDs)
|
||||
if err == nil {
|
||||
result := make(map[string]int64, len(convIDs))
|
||||
for _, convID := range convIDs {
|
||||
maxSeq, ok := maxSeqs[convID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
readSeq, hasRead := readSeqs[convID]
|
||||
if !hasRead {
|
||||
result[convID] = maxSeq
|
||||
} else {
|
||||
unread := maxSeq - readSeq
|
||||
if unread < 0 {
|
||||
unread = 0
|
||||
}
|
||||
result[convID] = unread
|
||||
}
|
||||
}
|
||||
// 补齐缺失的会话(未命中缓存时归零)
|
||||
for _, convID := range convIDs {
|
||||
if _, exists := result[convID]; !exists {
|
||||
result[convID] = 0
|
||||
}
|
||||
}
|
||||
// 只有全部会话都算出来了才回返回,否则降级
|
||||
allComputed := true
|
||||
for _, convID := range convIDs {
|
||||
if _, exists := maxSeqs[convID]; !exists {
|
||||
allComputed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allComputed {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 降级到 DB
|
||||
return s.repo.GetUnreadCountBatch(ctx, userID, convIDs)
|
||||
}
|
||||
|
||||
@@ -893,3 +977,18 @@ func (s *chatServiceImpl) GetUnreadCountBatch(ctx context.Context, userID string
|
||||
func (s *chatServiceImpl) GetLastMessagesBatch(ctx context.Context, convIDs []string) (map[string]*model.Message, error) {
|
||||
return s.repo.GetLastMessagesBatch(ctx, convIDs)
|
||||
}
|
||||
|
||||
// GetUserReadSeq 获取用户在某会话的已读位置(优先从 Redis 缓存读取)
|
||||
func (s *chatServiceImpl) GetUserReadSeq(ctx context.Context, conversationID string, userID string) (int64, error) {
|
||||
if s.conversationCache != nil {
|
||||
if seq, err := s.conversationCache.GetUserReadSeq(ctx, conversationID, userID); err == nil {
|
||||
return seq, nil
|
||||
}
|
||||
}
|
||||
// 降级到 DB
|
||||
participant, err := s.getParticipant(ctx, conversationID, userID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return participant.LastReadSeq, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user