feat(chat): implement batch mark-as-read and message sync data
Refactor unread count logic to use arithmetic calculation (maxSeq - readSeq) instead of manual incrementing/decrementing. This simplifies the cache management and improves consistency. New features: - Batch mark-as-read functionality for multiple conversations. - Lightweight sync data endpoint to retrieve conversation metadata (maxSeq and last message timestamp) for client synchronization. - Optimized batch retrieval of conversation max sequences using Redis MGet. Technical changes: - Deprecated `IncrementUnread` and `ClearUnread` in `ConversationCache`. - Added `GetConvMaxSeqBatch` to reduce network round-trips. - Added `HandleMarkReadAll` and `HandleGetSyncData` handlers. - Updated `ChatService` to support batch operations and sync data retrieval.
This commit is contained in:
162
internal/cache/conversation_cache.go
vendored
162
internal/cache/conversation_cache.go
vendored
@@ -606,6 +606,48 @@ func (c *ConversationCache) GetConvMaxSeq(ctx context.Context, convID string) (i
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// GetConvMaxSeqBatch 批量获取多个会话的 maxSeq(MGet 减少网络往返)
|
||||
func (c *ConversationCache) GetConvMaxSeqBatch(ctx context.Context, 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.GetConvMaxSeq(ctx, convID); 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(MessageSeqKey(convID))
|
||||
}
|
||||
|
||||
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)
|
||||
if seq > 0 {
|
||||
result[convIDs[i]] = seq
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ComputeUnreadCount 计算单个会话未读数:maxSeq - hasReadSeq
|
||||
func (c *ConversationCache) ComputeUnreadCount(ctx context.Context, convID, userID string) (int64, error) {
|
||||
maxSeq, err := c.GetConvMaxSeq(ctx, convID)
|
||||
@@ -650,128 +692,34 @@ func (c *ConversationCache) ComputeAllUnreadCount(ctx context.Context, userID st
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// IncrementUnread 递增用户在某会话的未读数(Redis Hash + 总数计数器)
|
||||
// IncrementUnread 递增用户在某会话的未读数(已废弃:改用 seq 算术计算)
|
||||
// Deprecated: Unread count is now computed via arithmetic (maxSeq - readSeq).
|
||||
func (c *ConversationCache) IncrementUnread(ctx context.Context, userID, convID string) error {
|
||||
hashKey := UnreadHashKey(userID)
|
||||
totalKey := UnreadTotalKey(userID)
|
||||
|
||||
_, err := c.cache.HIncrBy(ctx, hashKey, convID, 1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hincrby unread failed: %w", err)
|
||||
}
|
||||
|
||||
_, err = c.cache.Incr(ctx, totalKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incr unread total failed: %w", err)
|
||||
}
|
||||
|
||||
_ = c.cache.Expire(ctx, hashKey, c.settings.UnreadTTL*10)
|
||||
_ = c.cache.Expire(ctx, totalKey, c.settings.UnreadTTL*10)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearUnread 清零用户在某会话的未读数(原子操作,防止与 IncrementUnread 竞态)
|
||||
// ClearUnread 清零用户在某会话的未读数(已废弃:改用 seq 算术计算)
|
||||
// Deprecated: No longer needed. Unread is computed from seq arithmetic.
|
||||
func (c *ConversationCache) ClearUnread(ctx context.Context, userID, convID string) error {
|
||||
hashKey := UnreadHashKey(userID)
|
||||
totalKey := UnreadTotalKey(userID)
|
||||
|
||||
redisCache, ok := c.cache.(*RedisCache)
|
||||
if !ok {
|
||||
// miniredis/内存模式:保持原逻辑(非原子,但无并发竞态风险)
|
||||
currentStr, err := c.cache.HGet(ctx, hashKey, convID)
|
||||
if err != nil {
|
||||
currentStr = "0"
|
||||
}
|
||||
current := int64(0)
|
||||
if v, parseErr := fmt.Sscanf(currentStr, "%d", ¤t); parseErr != nil || v != 1 {
|
||||
current = 0
|
||||
}
|
||||
if err := c.cache.HSet(ctx, hashKey, convID, "0"); err != nil {
|
||||
return fmt.Errorf("hset clear unread failed: %w", err)
|
||||
}
|
||||
if current > 0 {
|
||||
c.cache.IncrBySeq(ctx, totalKey, -current)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
rdb := redisCache.GetRedisClient().GetClient()
|
||||
nHashKey := normalizeKey(hashKey)
|
||||
nTotalKey := normalizeKey(totalKey)
|
||||
|
||||
script := redis.NewScript(`
|
||||
local current = tonumber(redis.call('HGET', KEYS[1], ARGV[1]))
|
||||
if current == nil or current <= 0 then
|
||||
return 0
|
||||
end
|
||||
redis.call('HSET', KEYS[1], ARGV[1], '0')
|
||||
local newTotal = tonumber(redis.call('INCRBY', KEYS[2], -current))
|
||||
if newTotal < 0 then
|
||||
redis.call('SET', KEYS[2], '0')
|
||||
end
|
||||
return current
|
||||
`)
|
||||
_, err := script.Run(ctx, rdb, []string{nHashKey, nTotalKey}, convID).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("lua clear unread failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUnreadCountFromHash 从 Redis Hash 获取单个会话未读数
|
||||
// GetUnreadCountFromHash 从 Redis Hash 获取单个会话未读数(已废弃)
|
||||
// Deprecated: Use ComputeUnreadCount instead.
|
||||
func (c *ConversationCache) GetUnreadCountFromHash(ctx context.Context, userID, convID string) (int64, error) {
|
||||
hashKey := UnreadHashKey(userID)
|
||||
val, err := c.cache.HGet(ctx, hashKey, convID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var count int64
|
||||
fmt.Sscanf(val, "%d", &count)
|
||||
return count, nil
|
||||
return 0, ErrKeyNotFound
|
||||
}
|
||||
|
||||
// GetAllUnreadCounts 从 Redis Hash 获取用户所有会话的未读数
|
||||
// GetAllUnreadCounts 从 Redis Hash 获取用户所有会话的未读数(已废弃)
|
||||
// Deprecated: Use ComputeAllUnreadCount instead.
|
||||
func (c *ConversationCache) GetAllUnreadCounts(ctx context.Context, userID string) (map[string]int64, error) {
|
||||
hashKey := UnreadHashKey(userID)
|
||||
all, err := c.cache.HGetAll(ctx, hashKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]int64, len(all))
|
||||
for k, v := range all {
|
||||
var count int64
|
||||
fmt.Sscanf(v, "%d", &count)
|
||||
result[k] = count
|
||||
}
|
||||
return result, nil
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
|
||||
// GetTotalUnread 从 Redis 获取用户总未读数
|
||||
// GetTotalUnread 从 Redis 获取用户总未读数(已废弃)
|
||||
// Deprecated: Use ComputeAllUnreadCount instead.
|
||||
func (c *ConversationCache) GetTotalUnread(ctx context.Context, userID string) (int64, error) {
|
||||
totalKey := UnreadTotalKey(userID)
|
||||
|
||||
redisCache, ok := c.cache.(*RedisCache)
|
||||
if ok {
|
||||
val, err := redisCache.GetRedisClient().GetClient().Get(ctx, normalizeKey(totalKey)).Int64()
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
raw, ok := c.cache.Get(totalKey)
|
||||
if !ok {
|
||||
return 0, nil
|
||||
}
|
||||
switch v := raw.(type) {
|
||||
case int64:
|
||||
return v, nil
|
||||
case string:
|
||||
var n int64
|
||||
fmt.Sscanf(v, "%d", &n)
|
||||
return n, nil
|
||||
}
|
||||
return 0, nil
|
||||
return 0, ErrKeyNotFound
|
||||
}
|
||||
|
||||
// GetNextSeq 获取会话的下一个 seq 值(原子递增)
|
||||
|
||||
Reference in New Issue
Block a user