feat(chat): implement sequence-based unread count tracking
All checks were successful
Build Backend / build (push) Successful in 1m56s
Build Backend / build-docker (push) Successful in 1m9s

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:
2026-05-10 13:36:58 +08:00
parent 43348615c0
commit 628a6acbe9
6 changed files with 315 additions and 23 deletions

View File

@@ -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{