feat(api): implement exam and grade synchronization modules
Add new functionality for managing and synchronizing academic grades and exam schedules. This includes new models, repositories, services, and HTTP handlers, along with updated gRPC definitions and dependency injection configuration. Additionally, improve the reliability of unread message count tracking by implementing an atomic Lua script for Redis operations in the conversation cache.
This commit is contained in:
58
internal/cache/conversation_cache.go
vendored
58
internal/cache/conversation_cache.go
vendored
@@ -670,31 +670,51 @@ func (c *ConversationCache) IncrementUnread(ctx context.Context, userID, convID
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearUnread 清零用户在某会话的未读数
|
||||
// ClearUnread 清零用户在某会话的未读数(原子操作,防止与 IncrementUnread 竞态)
|
||||
func (c *ConversationCache) ClearUnread(ctx context.Context, userID, convID string) error {
|
||||
hashKey := UnreadHashKey(userID)
|
||||
totalKey := UnreadTotalKey(userID)
|
||||
|
||||
// 读取当前未读数
|
||||
currentStr, err := c.cache.HGet(ctx, hashKey, convID)
|
||||
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 {
|
||||
currentStr = "0"
|
||||
return fmt.Errorf("lua clear unread failed: %w", err)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user