2026-04-22 16:01:59 +08:00
package repository
2026-03-09 21:28:58 +08:00
import (
2026-04-22 16:01:59 +08:00
"with_you/internal/model"
"with_you/internal/pkg/cursor"
2026-03-12 08:38:14 +08:00
"context"
2026-03-26 01:50:54 +08:00
"errors"
2026-03-12 08:38:14 +08:00
"fmt"
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
"strconv"
2026-03-12 08:38:14 +08:00
"strings"
2026-03-09 21:28:58 +08:00
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
2026-03-26 18:14:16 +08:00
// MessageRepository 消息仓储接口
type MessageRepository interface {
CreateMessage ( msg * model . Message ) error
GetConversation ( id string ) ( * model . Conversation , error )
GetOrCreatePrivateConversation ( user1ID , user2ID string ) ( * model . Conversation , error )
CreateConversationWithParticipants ( conv * model . Conversation , participants [ ] string ) error
GetConversations ( userID string , page , pageSize int ) ( [ ] * model . Conversation , int64 , error )
GetMessages ( conversationID string , page , pageSize int ) ( [ ] * model . Message , int64 , error )
GetMessagesAfterSeq ( conversationID string , afterSeq int64 , limit int ) ( [ ] * model . Message , error )
GetMessagesBeforeSeq ( conversationID string , beforeSeq int64 , limit int ) ( [ ] * model . Message , error )
GetConversationParticipants ( conversationID string ) ( [ ] * model . ConversationParticipant , error )
GetParticipant ( conversationID string , userID string ) ( * model . ConversationParticipant , error )
UpdateLastReadSeq ( conversationID string , userID string , lastReadSeq int64 ) error
UpdatePinned ( conversationID string , userID string , isPinned bool ) error
2026-04-25 21:22:52 +08:00
UpdateNotificationMuted ( conversationID string , userID string , notificationMuted bool ) error
2026-03-26 18:14:16 +08:00
GetUnreadCount ( conversationID string , userID string ) ( int64 , error )
UpdateConversationLastSeq ( conversationID string , seq int64 ) error
GetNextSeq ( conversationID string ) ( int64 , error )
CreateMessageWithSeq ( msg * model . Message ) error
GetAllUnreadCount ( userID string ) ( int64 , error )
GetMessageByID ( messageID string ) ( * model . Message , error )
CountMessagesBySenderInConversation ( conversationID , senderID string ) ( int64 , error )
UpdateMessageStatus ( messageID string , status model . MessageStatus ) error
RecallMessage ( messageID string , userID string ) error
GetOrCreateSystemParticipant ( userID string ) ( * model . ConversationParticipant , error )
GetSystemMessagesUnreadCount ( userID string ) ( int64 , error )
MarkAllSystemMessagesAsRead ( userID string ) error
GetConversationByGroupID ( groupID string ) ( * model . Conversation , error )
RemoveParticipant ( conversationID string , userID string ) error
AddParticipant ( conversationID string , userID string ) error
DeleteConversationByGroupID ( groupID string ) error
HideConversationForUser ( conversationID , userID string ) error
BatchWriteMessages ( ctx context . Context , messages [ ] * model . Message ) error
BatchUpdateParticipants ( ctx context . Context , updates [ ] ParticipantUpdate ) error
UpdateConversationLastSeqWithContext ( ctx context . Context , convID string , lastSeq int64 , lastMsgTime time . Time ) error
BatchWriteMessagesWithTx ( tx * gorm . DB , messages [ ] * model . Message ) error
BatchUpdateParticipantsWithTx ( tx * gorm . DB , updates [ ] ParticipantUpdate ) error
UpdateConversationLastSeqWithTx ( tx * gorm . DB , convID string , lastSeq int64 , lastMsgTime time . Time ) error
GetMessagesByCursor ( ctx context . Context , conversationID string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Message ] , error )
GetConversationsByCursor ( ctx context . Context , userID string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Conversation ] , error )
}
// messageRepository 消息仓储实现
type messageRepository struct {
2026-03-09 21:28:58 +08:00
db * gorm . DB
}
// NewMessageRepository 创建消息仓储
2026-03-26 18:14:16 +08:00
func NewMessageRepository ( db * gorm . DB ) MessageRepository {
return & messageRepository { db : db }
2026-03-09 21:28:58 +08:00
}
// CreateMessage 创建消息
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) CreateMessage ( msg * model . Message ) error {
2026-03-09 21:28:58 +08:00
return r . db . Create ( msg ) . Error
}
// GetConversation 获取会话
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetConversation ( id string ) ( * model . Conversation , error ) {
2026-03-09 21:28:58 +08:00
var conv model . Conversation
err := r . db . Preload ( "Group" ) . First ( & conv , "id = ?" , id ) . Error
if err != nil {
return nil , err
}
return & conv , nil
}
2026-03-26 18:14:16 +08:00
// CreateConversationWithParticipants 创建会话并添加参与者(在事务中)
func ( r * messageRepository ) CreateConversationWithParticipants ( conv * model . Conversation , participants [ ] string ) error {
return r . db . Transaction ( func ( tx * gorm . DB ) error {
if err := tx . Create ( conv ) . Error ; err != nil {
return err
}
for _ , userID := range participants {
participant := model . ConversationParticipant {
ConversationID : conv . ID ,
UserID : userID ,
2026-03-30 04:49:35 +08:00
LastReadSeq : 0 ,
2026-03-26 18:14:16 +08:00
}
if err := tx . Create ( & participant ) . Error ; err != nil {
return err
}
}
return nil
} )
}
2026-03-09 21:28:58 +08:00
// GetOrCreatePrivateConversation 获取或创建私聊会话
// 使用参与者关系表来管理会话
// userID 参数为 string 类型( UUID格式) , 与JWT中user_id保持一致
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetOrCreatePrivateConversation ( user1ID , user2ID string ) ( * model . Conversation , error ) {
2026-03-09 21:28:58 +08:00
var conv model . Conversation
// 查找两个用户共同参与的私聊会话
err := r . db . Table ( "conversations c" ) .
Joins ( "INNER JOIN conversation_participants cp1 ON c.id = cp1.conversation_id AND cp1.user_id = ?" , user1ID ) .
Joins ( "INNER JOIN conversation_participants cp2 ON c.id = cp2.conversation_id AND cp2.user_id = ?" , user2ID ) .
Where ( "c.type = ?" , model . ConversationTypePrivate ) .
First ( & conv ) . Error
if err == nil {
_ = r . db . Model ( & model . ConversationParticipant { } ) .
Where ( "conversation_id = ? AND user_id IN ?" , conv . ID , [ ] string { user1ID , user2ID } ) .
Update ( "hidden_at" , nil ) . Error
return & conv , nil
}
if err != gorm . ErrRecordNotFound {
return nil , err
}
// 没找到会话,创建新会话
conv = model . Conversation {
Type : model . ConversationTypePrivate ,
}
// 使用事务创建会话和参与者
err = r . db . Transaction ( func ( tx * gorm . DB ) error {
if err := tx . Create ( & conv ) . Error ; err != nil {
return err
}
// 创建参与者记录 - UserID 存储为 string (UUID)
participants := [ ] model . ConversationParticipant {
{ ConversationID : conv . ID , UserID : user1ID } ,
{ ConversationID : conv . ID , UserID : user2ID } ,
}
if err := tx . Create ( & participants ) . Error ; err != nil {
return err
}
return nil
} )
return & conv , err
}
// GetConversations 获取用户会话列表
// userID 参数为 string 类型( UUID格式) , 与JWT中user_id保持一致
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetConversations ( userID string , page , pageSize int ) ( [ ] * model . Conversation , int64 , error ) {
2026-03-09 21:28:58 +08:00
var convs [ ] * model . Conversation
var total int64
2026-04-25 15:03:37 +08:00
// 获取总数(私聊仅统计有消息的会话,群聊始终显示)
2026-03-09 21:28:58 +08:00
r . db . Model ( & model . ConversationParticipant { } ) .
2026-04-25 13:00:49 +08:00
Joins ( "INNER JOIN conversations c ON c.id = conversation_participants.conversation_id" ) .
2026-04-25 15:03:37 +08:00
Where ( "conversation_participants.user_id = ? AND conversation_participants.hidden_at IS NULL AND (c.type != ? OR c.last_seq > 0)" , userID , model . ConversationTypePrivate ) .
2026-03-09 21:28:58 +08:00
Count ( & total )
if total == 0 {
return convs , total , nil
}
offset := ( page - 1 ) * pageSize
err := r . db . Model ( & model . Conversation { } ) .
Joins ( "INNER JOIN conversation_participants cp ON conversations.id = cp.conversation_id" ) .
2026-04-25 15:03:37 +08:00
Where ( "cp.user_id = ? AND cp.hidden_at IS NULL AND (conversations.type != ? OR conversations.last_seq > 0)" , userID , model . ConversationTypePrivate ) .
2026-03-09 21:28:58 +08:00
Preload ( "Group" ) .
Offset ( offset ) .
Limit ( pageSize ) .
Order ( "cp.is_pinned DESC" ) .
Order ( "conversations.updated_at DESC" ) .
Find ( & convs ) . Error
return convs , total , err
}
// GetMessages 获取会话消息
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetMessages ( conversationID string , page , pageSize int ) ( [ ] * model . Message , int64 , error ) {
2026-03-09 21:28:58 +08:00
var messages [ ] * model . Message
var total int64
r . db . Model ( & model . Message { } ) . Where ( "conversation_id = ?" , conversationID ) . Count ( & total )
offset := ( page - 1 ) * pageSize
err := r . db . Where ( "conversation_id = ?" , conversationID ) .
Offset ( offset ) .
Limit ( pageSize ) .
Order ( "seq DESC" ) .
Find ( & messages ) . Error
2026-03-17 10:10:49 +08:00
if err == nil && len ( messages ) > 0 {
model . BatchDecryptMessagesParallel ( messages )
}
2026-03-09 21:28:58 +08:00
return messages , total , err
}
// GetMessagesAfterSeq 获取指定seq之后的消息( 用于增量同步)
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetMessagesAfterSeq ( conversationID string , afterSeq int64 , limit int ) ( [ ] * model . Message , error ) {
2026-03-09 21:28:58 +08:00
var messages [ ] * model . Message
err := r . db . Where ( "conversation_id = ? AND seq > ?" , conversationID , afterSeq ) .
Order ( "seq ASC" ) .
Limit ( limit ) .
Find ( & messages ) . Error
2026-03-17 10:10:49 +08:00
if err == nil && len ( messages ) > 0 {
model . BatchDecryptMessagesParallel ( messages )
}
2026-03-09 21:28:58 +08:00
return messages , err
}
// GetMessagesBeforeSeq 获取指定seq之前的历史消息( 用于下拉加载更多)
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetMessagesBeforeSeq ( conversationID string , beforeSeq int64 , limit int ) ( [ ] * model . Message , error ) {
2026-03-09 21:28:58 +08:00
var messages [ ] * model . Message
err := r . db . Where ( "conversation_id = ? AND seq < ?" , conversationID , beforeSeq ) .
Order ( "seq DESC" ) . // 降序获取最新消息在前
Limit ( limit ) .
Find ( & messages ) . Error
// 反转回正序
for i , j := 0 , len ( messages ) - 1 ; i < j ; i , j = i + 1 , j - 1 {
messages [ i ] , messages [ j ] = messages [ j ] , messages [ i ]
}
2026-03-17 10:10:49 +08:00
if err == nil && len ( messages ) > 0 {
model . BatchDecryptMessagesParallel ( messages )
}
2026-03-09 21:28:58 +08:00
return messages , err
}
// GetConversationParticipants 获取会话参与者
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetConversationParticipants ( conversationID string ) ( [ ] * model . ConversationParticipant , error ) {
2026-03-09 21:28:58 +08:00
var participants [ ] * model . ConversationParticipant
err := r . db . Where ( "conversation_id = ?" , conversationID ) . Find ( & participants ) . Error
return participants , err
}
// GetParticipant 获取用户在会话中的参与者信息
// userID 参数为 string 类型( UUID格式) , 与JWT中user_id保持一致
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetParticipant ( conversationID string , userID string ) ( * model . ConversationParticipant , error ) {
2026-03-09 21:28:58 +08:00
var participant model . ConversationParticipant
err := r . db . Where ( "conversation_id = ? AND user_id = ?" , conversationID , userID ) . First ( & participant ) . Error
if err != nil {
// 如果找不到参与者,尝试添加(修复没有参与者记录的问题)
2026-03-26 01:50:54 +08:00
if errors . Is ( err , gorm . ErrRecordNotFound ) {
2026-03-09 21:28:58 +08:00
// 检查会话是否存在
var conv model . Conversation
2026-03-12 08:38:14 +08:00
if err := r . db . Where ( "id = ?" , conversationID ) . First ( & conv ) . Error ; err == nil {
2026-03-09 21:28:58 +08:00
// 会话存在,添加参与者
participant = model . ConversationParticipant {
ConversationID : conversationID ,
UserID : userID ,
}
if err := r . db . Create ( & participant ) . Error ; err != nil {
return nil , err
}
return & participant , nil
}
}
return nil , err
}
return & participant , nil
}
// UpdateLastReadSeq 更新已读位置
// userID 参数为 string 类型( UUID格式) , 与JWT中user_id保持一致
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) UpdateLastReadSeq ( conversationID string , userID string , lastReadSeq int64 ) error {
2026-03-09 21:28:58 +08:00
result := r . db . Model ( & model . ConversationParticipant { } ) .
Where ( "conversation_id = ? AND user_id = ?" , conversationID , userID ) .
Update ( "last_read_seq" , lastReadSeq )
if result . Error != nil {
return result . Error
}
// 如果没有更新任何记录,说明参与者记录不存在,需要插入
if result . RowsAffected == 0 {
// 尝试插入新记录(跨数据库 upsert)
err := r . db . Clauses ( clause . OnConflict {
Columns : [ ] clause . Column {
{ Name : "conversation_id" } ,
{ Name : "user_id" } ,
} ,
2026-03-30 04:49:35 +08:00
DoUpdates : clause . Assignments ( map [ string ] any {
2026-03-09 21:28:58 +08:00
"last_read_seq" : lastReadSeq ,
"updated_at" : gorm . Expr ( "CURRENT_TIMESTAMP" ) ,
} ) ,
} ) . Create ( & model . ConversationParticipant {
ConversationID : conversationID ,
UserID : userID ,
LastReadSeq : lastReadSeq ,
} ) . Error
if err != nil {
return err
}
}
return nil
}
// UpdatePinned 更新会话置顶状态(用户维度)
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) UpdatePinned ( conversationID string , userID string , isPinned bool ) error {
2026-03-09 21:28:58 +08:00
result := r . db . Model ( & model . ConversationParticipant { } ) .
Where ( "conversation_id = ? AND user_id = ?" , conversationID , userID ) .
Update ( "is_pinned" , isPinned )
if result . Error != nil {
return result . Error
}
if result . RowsAffected == 0 {
return r . db . Clauses ( clause . OnConflict {
Columns : [ ] clause . Column {
{ Name : "conversation_id" } ,
{ Name : "user_id" } ,
} ,
2026-03-30 04:49:35 +08:00
DoUpdates : clause . Assignments ( map [ string ] any {
2026-03-09 21:28:58 +08:00
"is_pinned" : isPinned ,
"updated_at" : gorm . Expr ( "CURRENT_TIMESTAMP" ) ,
} ) ,
} ) . Create ( & model . ConversationParticipant {
ConversationID : conversationID ,
UserID : userID ,
IsPinned : isPinned ,
} ) . Error
}
return nil
}
2026-04-25 21:22:52 +08:00
// UpdateNotificationMuted 更新会话免打扰状态(用户维度)
func ( r * messageRepository ) UpdateNotificationMuted ( conversationID string , userID string , notificationMuted bool ) error {
result := r . db . Model ( & model . ConversationParticipant { } ) .
Where ( "conversation_id = ? AND user_id = ?" , conversationID , userID ) .
Update ( "muted" , notificationMuted )
if result . Error != nil {
return result . Error
}
if result . RowsAffected == 0 {
return r . db . Clauses ( clause . OnConflict {
Columns : [ ] clause . Column {
{ Name : "conversation_id" } ,
{ Name : "user_id" } ,
} ,
DoUpdates : clause . Assignments ( map [ string ] any {
"muted" : notificationMuted ,
"updated_at" : gorm . Expr ( "CURRENT_TIMESTAMP" ) ,
} ) ,
} ) . Create ( & model . ConversationParticipant {
ConversationID : conversationID ,
UserID : userID ,
NotificationMuted : notificationMuted ,
} ) . Error
}
return nil
}
2026-03-09 21:28:58 +08:00
// GetUnreadCount 获取未读消息数
// userID 参数为 string 类型( UUID格式) , 与JWT中user_id保持一致
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetUnreadCount ( conversationID string , userID string ) ( int64 , error ) {
2026-03-09 21:28:58 +08:00
var participant model . ConversationParticipant
err := r . db . Where ( "conversation_id = ? AND user_id = ?" , conversationID , userID ) . First ( & participant ) . Error
if err != nil {
return 0 , err
}
var count int64
err = r . db . Model ( & model . Message { } ) .
Where ( "conversation_id = ? AND sender_id != ? AND seq > ?" , conversationID , userID , participant . LastReadSeq ) .
Count ( & count ) . Error
return count , err
}
// UpdateConversationLastSeq 更新会话的最后消息seq和时间
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) UpdateConversationLastSeq ( conversationID string , seq int64 ) error {
2026-03-09 21:28:58 +08:00
return r . db . Model ( & model . Conversation { } ) .
Where ( "id = ?" , conversationID ) .
2026-03-30 04:49:35 +08:00
Updates ( map [ string ] any {
2026-03-09 21:28:58 +08:00
"last_seq" : seq ,
"last_msg_time" : gorm . Expr ( "CURRENT_TIMESTAMP" ) ,
} ) . Error
}
// GetNextSeq 获取会话的下一个seq值
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetNextSeq ( conversationID string ) ( int64 , error ) {
2026-03-09 21:28:58 +08:00
var conv model . Conversation
2026-03-12 08:38:14 +08:00
err := r . db . Select ( "last_seq" ) . Where ( "id = ?" , conversationID ) . First ( & conv ) . Error
2026-03-09 21:28:58 +08:00
if err != nil {
return 0 , err
}
return conv . LastSeq + 1 , nil
}
// CreateMessageWithSeq 创建消息并更新seq( 事务操作)
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) CreateMessageWithSeq ( msg * model . Message ) error {
2026-03-09 21:28:58 +08:00
return r . db . Transaction ( func ( tx * gorm . DB ) error {
// 获取当前seq并+1
var conv model . Conversation
2026-03-12 08:38:14 +08:00
if err := tx . Select ( "last_seq" ) . Where ( "id = ?" , msg . ConversationID ) . First ( & conv ) . Error ; err != nil {
2026-03-09 21:28:58 +08:00
return err
}
msg . Seq = conv . LastSeq + 1
// 创建消息
if err := tx . Create ( msg ) . Error ; err != nil {
return err
}
// 更新会话的last_seq
if err := tx . Model ( & model . Conversation { } ) .
Where ( "id = ?" , msg . ConversationID ) .
2026-03-30 04:49:35 +08:00
Updates ( map [ string ] any {
2026-03-09 21:28:58 +08:00
"last_seq" : msg . Seq ,
"last_msg_time" : gorm . Expr ( "CURRENT_TIMESTAMP" ) ,
} ) . Error ; err != nil {
return err
}
// 新消息到达后,自动恢复被“仅自己删除”的会话
if err := tx . Model ( & model . ConversationParticipant { } ) .
Where ( "conversation_id = ?" , msg . ConversationID ) .
Update ( "hidden_at" , nil ) . Error ; err != nil {
return err
}
return nil
} )
}
// GetAllUnreadCount 获取用户所有会话的未读消息总数
// userID 参数为 string 类型( UUID格式) , 与JWT中user_id保持一致
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetAllUnreadCount ( userID string ) ( int64 , error ) {
2026-03-09 21:28:58 +08:00
var totalUnread int64
err := r . db . Table ( "conversation_participants AS cp" ) .
Joins ( "LEFT JOIN messages AS m ON m.conversation_id = cp.conversation_id AND m.sender_id <> ? AND m.seq > cp.last_read_seq AND m.deleted_at IS NULL" , userID ) .
Where ( "cp.user_id = ?" , userID ) .
Select ( "COALESCE(COUNT(m.id), 0)" ) .
Scan ( & totalUnread ) . Error
return totalUnread , err
}
// GetMessageByID 根据ID获取消息
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetMessageByID ( messageID string ) ( * model . Message , error ) {
2026-03-09 21:28:58 +08:00
var message model . Message
err := r . db . First ( & message , "id = ?" , messageID ) . Error
if err != nil {
return nil , err
}
2026-03-17 10:10:49 +08:00
// 单条消息直接解密
_ = message . Decrypt ( )
2026-03-09 21:28:58 +08:00
return & message , nil
}
// CountMessagesBySenderInConversation 统计会话中某用户已发送消息数
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) CountMessagesBySenderInConversation ( conversationID , senderID string ) ( int64 , error ) {
2026-03-09 21:28:58 +08:00
var count int64
err := r . db . Model ( & model . Message { } ) .
Where ( "conversation_id = ? AND sender_id = ?" , conversationID , senderID ) .
Count ( & count ) . Error
return count , err
}
// UpdateMessageStatus 更新消息状态
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) UpdateMessageStatus ( messageID string , status model . MessageStatus ) error {
2026-03-09 21:28:58 +08:00
return r . db . Model ( & model . Message { } ) .
Where ( "id = ?" , messageID ) .
Update ( "status" , status ) . Error
}
2026-03-26 18:14:16 +08:00
// RecallMessage 撤回消息( 2分钟内)
func ( r * messageRepository ) RecallMessage ( messageID string , userID string ) error {
return r . db . Model ( & model . Message { } ) .
Where ( "id = ? AND sender_id = ?" , messageID , userID ) .
2026-03-30 04:49:35 +08:00
Updates ( map [ string ] any {
2026-03-26 18:14:16 +08:00
"status" : model . MessageStatusRecalled ,
"segments" : model . MessageSegments { } ,
} ) . Error
}
2026-03-09 21:28:58 +08:00
// GetOrCreateSystemParticipant 获取或创建用户在系统会话中的参与者记录
// 系统会话是虚拟会话,但需要参与者记录来跟踪已读状态
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetOrCreateSystemParticipant ( userID string ) ( * model . ConversationParticipant , error ) {
2026-03-09 21:28:58 +08:00
var participant model . ConversationParticipant
err := r . db . Where ( "conversation_id = ? AND user_id = ?" ,
model . SystemConversationID , userID ) . First ( & participant ) . Error
if err == nil {
return & participant , nil
}
if err != gorm . ErrRecordNotFound {
return nil , err
}
// 自动创建参与者记录
participant = model . ConversationParticipant {
ConversationID : model . SystemConversationID ,
UserID : userID ,
LastReadSeq : 0 ,
}
if err := r . db . Create ( & participant ) . Error ; err != nil {
return nil , err
}
return & participant , nil
}
// GetSystemMessagesUnreadCount 获取系统消息未读数
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetSystemMessagesUnreadCount ( userID string ) ( int64 , error ) {
2026-03-09 21:28:58 +08:00
// 获取或创建参与者记录
participant , err := r . GetOrCreateSystemParticipant ( userID )
if err != nil {
return 0 , err
}
// 计算未读数:查询 seq > last_read_seq 的消息
var count int64
err = r . db . Model ( & model . Message { } ) .
Where ( "conversation_id = ? AND seq > ?" ,
model . SystemConversationID , participant . LastReadSeq ) .
Count ( & count ) . Error
return count , err
}
// MarkAllSystemMessagesAsRead 标记所有系统消息已读
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) MarkAllSystemMessagesAsRead ( userID string ) error {
2026-03-09 21:28:58 +08:00
// 获取系统会话的最新 seq
var maxSeq int64
err := r . db . Model ( & model . Message { } ) .
Where ( "conversation_id = ?" , model . SystemConversationID ) .
Select ( "COALESCE(MAX(seq), 0)" ) .
Scan ( & maxSeq ) . Error
if err != nil {
return err
}
// 使用跨数据库 upsert 方式更新或创建参与者记录
return r . db . Clauses ( clause . OnConflict {
Columns : [ ] clause . Column {
{ Name : "conversation_id" } ,
{ Name : "user_id" } ,
} ,
2026-03-30 04:49:35 +08:00
DoUpdates : clause . Assignments ( map [ string ] any {
2026-03-09 21:28:58 +08:00
"last_read_seq" : maxSeq ,
"updated_at" : gorm . Expr ( "CURRENT_TIMESTAMP" ) ,
} ) ,
} ) . Create ( & model . ConversationParticipant {
ConversationID : model . SystemConversationID ,
UserID : userID ,
LastReadSeq : maxSeq ,
} ) . Error
}
// GetConversationByGroupID 通过群组ID获取会话
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetConversationByGroupID ( groupID string ) ( * model . Conversation , error ) {
2026-03-09 21:28:58 +08:00
var conv model . Conversation
err := r . db . Where ( "group_id = ?" , groupID ) . First ( & conv ) . Error
if err != nil {
return nil , err
}
return & conv , nil
}
// RemoveParticipant 移除会话参与者
// 当用户退出群聊时,需要同时移除其在对应会话中的参与者记录
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) RemoveParticipant ( conversationID string , userID string ) error {
2026-03-09 21:28:58 +08:00
return r . db . Where ( "conversation_id = ? AND user_id = ?" , conversationID , userID ) .
Delete ( & model . ConversationParticipant { } ) . Error
}
// AddParticipant 添加会话参与者
// 当用户加入群聊时,需要同时将其添加到对应会话的参与者记录
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) AddParticipant ( conversationID string , userID string ) error {
2026-03-09 21:28:58 +08:00
// 先检查是否已经是参与者
var count int64
err := r . db . Model ( & model . ConversationParticipant { } ) .
Where ( "conversation_id = ? AND user_id = ?" , conversationID , userID ) .
Count ( & count ) . Error
if err != nil {
return err
}
// 如果已经是参与者,直接返回
if count > 0 {
return nil
}
// 添加参与者
participant := model . ConversationParticipant {
ConversationID : conversationID ,
UserID : userID ,
LastReadSeq : 0 ,
}
return r . db . Create ( & participant ) . Error
}
// DeleteConversationByGroupID 删除群组对应的会话及其参与者
// 当解散群组时调用
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) DeleteConversationByGroupID ( groupID string ) error {
2026-03-09 21:28:58 +08:00
// 获取群组对应的会话
conv , err := r . GetConversationByGroupID ( groupID )
if err != nil {
// 如果会话不存在,直接返回
2026-03-26 01:50:54 +08:00
if errors . Is ( err , gorm . ErrRecordNotFound ) {
2026-03-09 21:28:58 +08:00
return nil
}
return err
}
return r . db . Transaction ( func ( tx * gorm . DB ) error {
// 删除会话参与者
if err := tx . Where ( "conversation_id = ?" , conv . ID ) . Delete ( & model . ConversationParticipant { } ) . Error ; err != nil {
return err
}
// 删除会话中的消息
if err := tx . Where ( "conversation_id = ?" , conv . ID ) . Delete ( & model . Message { } ) . Error ; err != nil {
return err
}
// 删除会话
if err := tx . Delete ( & model . Conversation { } , "id = ?" , conv . ID ) . Error ; err != nil {
return err
}
return nil
} )
}
// HideConversationForUser 仅对当前用户隐藏会话(私聊删除)
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) HideConversationForUser ( conversationID , userID string ) error {
2026-03-09 21:28:58 +08:00
now := time . Now ( )
return r . db . Model ( & model . ConversationParticipant { } ) .
Where ( "conversation_id = ? AND user_id = ?" , conversationID , userID ) .
Update ( "hidden_at" , & now ) . Error
}
2026-03-12 08:38:14 +08:00
// ParticipantUpdate 参与者更新数据
type ParticipantUpdate struct {
ConversationID string
UserID string
LastReadSeq int64
}
// BatchWriteMessages 批量写入消息
// 使用 GORM 的 CreateInBatches 实现高效批量插入
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) BatchWriteMessages ( ctx context . Context , messages [ ] * model . Message ) error {
2026-03-12 08:38:14 +08:00
if len ( messages ) == 0 {
return nil
}
return r . db . WithContext ( ctx ) . CreateInBatches ( messages , 100 ) . Error
}
// BatchUpdateParticipants 批量更新参与者(使用 CASE WHEN 优化)
// 使用单条 SQL 更新多条记录,避免循环执行 UPDATE
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) BatchUpdateParticipants ( ctx context . Context , updates [ ] ParticipantUpdate ) error {
2026-03-12 08:38:14 +08:00
if len ( updates ) == 0 {
return nil
}
// 构建 CASE WHEN 批量更新 SQL
// UPDATE conversation_participants
// SET last_read_seq = CASE
// WHEN (conversation_id = '1' AND user_id = 'a') THEN 10
// WHEN (conversation_id = '2' AND user_id = 'b') THEN 20
// END,
// updated_at = ?
// WHERE (conversation_id = '1' AND user_id = 'a')
// OR (conversation_id = '2' AND user_id = 'b')
var cases [ ] string
var whereClauses [ ] string
2026-03-30 04:49:35 +08:00
var args [ ] any
2026-03-12 08:38:14 +08:00
for _ , u := range updates {
cases = append ( cases , "WHEN (conversation_id = ? AND user_id = ?) THEN ?" )
whereClauses = append ( whereClauses , "(conversation_id = ? AND user_id = ?)" )
args = append ( args , u . ConversationID , u . UserID , u . LastReadSeq , u . ConversationID , u . UserID )
}
sql := fmt . Sprintf ( `
UPDATE conversation_participants
SET last_read_seq = CASE % s END ,
updated_at = ?
WHERE % s
` , strings . Join ( cases , " " ) , strings . Join ( whereClauses , " OR " ) )
args = append ( args , time . Now ( ) )
return r . db . WithContext ( ctx ) . Exec ( sql , args ... ) . Error
}
// UpdateConversationLastSeqWithContext 更新会话最后消息序号
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) UpdateConversationLastSeqWithContext ( ctx context . Context , convID string , lastSeq int64 , lastMsgTime time . Time ) error {
2026-03-12 08:38:14 +08:00
return r . db . WithContext ( ctx ) .
Model ( & model . Conversation { } ) .
Where ( "id = ?" , convID ) .
2026-03-30 04:49:35 +08:00
Updates ( map [ string ] any {
2026-03-12 08:38:14 +08:00
"last_seq" : lastSeq ,
"last_msg_time" : lastMsgTime ,
"updated_at" : time . Now ( ) ,
} ) . Error
}
// BatchWriteMessagesWithTx 在事务中批量写入消息
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) BatchWriteMessagesWithTx ( tx * gorm . DB , messages [ ] * model . Message ) error {
2026-03-12 08:38:14 +08:00
if len ( messages ) == 0 {
return nil
}
return tx . CreateInBatches ( messages , 100 ) . Error
}
// BatchUpdateParticipantsWithTx 在事务中批量更新参与者
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) BatchUpdateParticipantsWithTx ( tx * gorm . DB , updates [ ] ParticipantUpdate ) error {
2026-03-12 08:38:14 +08:00
if len ( updates ) == 0 {
return nil
}
var cases [ ] string
var whereClauses [ ] string
2026-03-30 04:49:35 +08:00
var args [ ] any
2026-03-12 08:38:14 +08:00
for _ , u := range updates {
cases = append ( cases , "WHEN (conversation_id = ? AND user_id = ?) THEN ?" )
whereClauses = append ( whereClauses , "(conversation_id = ? AND user_id = ?)" )
args = append ( args , u . ConversationID , u . UserID , u . LastReadSeq , u . ConversationID , u . UserID )
}
sql := fmt . Sprintf ( `
UPDATE conversation_participants
SET last_read_seq = CASE % s END ,
updated_at = ?
WHERE % s
` , strings . Join ( cases , " " ) , strings . Join ( whereClauses , " OR " ) )
args = append ( args , time . Now ( ) )
return tx . Exec ( sql , args ... ) . Error
}
// UpdateConversationLastSeqWithTx 在事务中更新会话最后消息序号
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) UpdateConversationLastSeqWithTx ( tx * gorm . DB , convID string , lastSeq int64 , lastMsgTime time . Time ) error {
2026-03-12 08:38:14 +08:00
return tx . Model ( & model . Conversation { } ) .
Where ( "id = ?" , convID ) .
2026-03-30 04:49:35 +08:00
Updates ( map [ string ] any {
2026-03-12 08:38:14 +08:00
"last_seq" : lastSeq ,
"last_msg_time" : lastMsgTime ,
"updated_at" : time . Now ( ) ,
} ) . Error
}
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
// ========== Cursor Pagination Methods ==========
// GetMessagesByCursor 游标分页获取会话消息
// 消息按 seq DESC 排序
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetMessagesByCursor ( ctx context . Context , conversationID string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Message ] , error ) {
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
// 构建基础查询
query := r . db . WithContext ( ctx ) . Model ( & model . Message { } ) . Where ( "conversation_id = ?" , conversationID )
// 使用游标构建器,消息按 seq DESC 排序
builder := cursor . NewBuilder ( query , cursor . SortBySeqDesc ) .
WithCursor ( req . Cursor , req . Direction ) .
WithPageSize ( req . PageSize )
if builder . Error ( ) != nil {
// 无效游标,返回空列表
2026-04-07 00:07:40 +08:00
return cursor . NewCursorPageResult ( [ ] * model . Message { } , "" , "" , false ) , nil
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
}
// 执行查询
var messages [ ] * model . Message
if err := builder . Build ( ) . Find ( & messages ) . Error ; err != nil {
return nil , err
}
// 解密消息
if len ( messages ) > 0 {
model . BatchDecryptMessagesParallel ( messages )
}
// 构建响应
pageSize := builder . GetPageSize ( )
hasMore := cursor . HasMore ( len ( messages ) , pageSize )
if hasMore {
messages = messages [ : pageSize ]
}
// 生成游标
var nextCursor , prevCursor string
if len ( messages ) > 0 {
// 下一页游标
if hasMore {
lastMsg := messages [ len ( messages ) - 1 ]
nextCursor = cursor . NewCursor (
strconv . FormatInt ( lastMsg . Seq , 10 ) ,
lastMsg . ID ,
cursor . SortBySeqDesc ,
) . Encode ( )
}
// 上一页游标
firstMsg := messages [ 0 ]
prevCursor = cursor . NewCursor (
strconv . FormatInt ( firstMsg . Seq , 10 ) ,
firstMsg . ID ,
cursor . SortBySeqDesc ,
) . Encode ( )
}
return cursor . NewCursorPageResult ( messages , nextCursor , prevCursor , hasMore ) , nil
}
// GetConversationsByCursor 游标分页获取用户会话列表
// 会话按置顶优先,然后按 updated_at DESC 排序
// 注意:会话列表的排序涉及 conversation_participants 表的 is_pinned 和 updated_at 字段
2026-03-26 18:14:16 +08:00
func ( r * messageRepository ) GetConversationsByCursor ( ctx context . Context , userID string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Conversation ] , error ) {
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
// 构建基础查询 - 关联 conversation_participants 表
query := r . db . WithContext ( ctx ) . Model ( & model . Conversation { } ) .
Joins ( "INNER JOIN conversation_participants cp ON conversations.id = cp.conversation_id" ) .
2026-04-25 15:03:37 +08:00
Where ( "cp.user_id = ? AND cp.hidden_at IS NULL AND (conversations.type != ? OR conversations.last_seq > 0)" , userID , model . ConversationTypePrivate ) .
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
Preload ( "Group" )
// 会话列表需要特殊处理:先按置顶排序,再按更新时间排序
// 由于涉及多字段排序( is_pinned DESC, updated_at DESC) , 需要自定义处理
builder := cursor . NewBuilder ( query , cursor . SortByUpdatedAtDesc ) .
WithCursor ( req . Cursor , req . Direction ) .
WithPageSize ( req . PageSize )
if builder . Error ( ) != nil {
// 无效游标,返回空列表
2026-04-07 00:07:40 +08:00
return cursor . NewCursorPageResult ( [ ] * model . Conversation { } , "" , "" , false ) , nil
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
}
// 执行查询 - 需要添加置顶排序
var conversations [ ] * model . Conversation
query = builder . Build ( )
// 添加置顶排序(在游标排序之前)
query = query . Order ( "cp.is_pinned DESC" ) . Order ( "conversations.updated_at DESC" )
if err := query . Find ( & conversations ) . Error ; err != nil {
return nil , err
}
// 构建响应
pageSize := builder . GetPageSize ( )
hasMore := cursor . HasMore ( len ( conversations ) , pageSize )
if hasMore {
conversations = conversations [ : pageSize ]
}
// 生成游标
var nextCursor , prevCursor string
if len ( conversations ) > 0 {
// 下一页游标
if hasMore {
lastConv := conversations [ len ( conversations ) - 1 ]
nextCursor = cursor . NewCursor (
cursor . FormatTime ( lastConv . UpdatedAt ) ,
lastConv . ID ,
cursor . SortByUpdatedAtDesc ,
) . Encode ( )
}
// 上一页游标
firstConv := conversations [ 0 ]
prevCursor = cursor . NewCursor (
cursor . FormatTime ( firstConv . UpdatedAt ) ,
firstConv . ID ,
cursor . SortByUpdatedAtDesc ,
) . Encode ( )
}
return cursor . NewCursorPageResult ( conversations , nextCursor , prevCursor , hasMore ) , nil
}