feat(conversations): add notification mute functionality for conversations
- Rename Muted field to NotificationMuted across model, DTOs and converters - Add UpdateNotificationMuted repository method with upsert support - Add SetConversationNotificationMuted service method - Add PUT /:id/notification_muted route for setting mute status - Improve S3 storage resilience by skipping bucket check on access denied
This commit is contained in:
@@ -35,10 +35,8 @@ func NewS3(cfg *S3Config) (*minio.Client, error) {
|
||||
|
||||
exists, err := client.BucketExists(ctx, cfg.Bucket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check bucket: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
// Access Denied 或网络不通时,跳过 bucket 检查,假定 bucket 已存在
|
||||
} else if !exists {
|
||||
if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{
|
||||
Region: cfg.Region,
|
||||
}); err != nil {
|
||||
|
||||
@@ -350,26 +350,27 @@ type MessageResponse struct {
|
||||
|
||||
// ConversationResponse 会话响应
|
||||
type ConversationResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
Group *GroupResponse `json:"group,omitempty"`
|
||||
LastSeq int64 `json:"last_seq"`
|
||||
LastMessage *MessageResponse `json:"last_message"`
|
||||
LastMessageAt string `json:"last_message_at"`
|
||||
UnreadCount int `json:"unread_count"`
|
||||
Participants []*UserResponse `json:"participants,omitempty"` // 私聊时使用
|
||||
MemberCount int `json:"member_count,omitempty"` // 群聊时使用
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
NotificationMuted bool `json:"notification_muted"`
|
||||
Group *GroupResponse `json:"group,omitempty"`
|
||||
LastSeq int64 `json:"last_seq"`
|
||||
LastMessage *MessageResponse `json:"last_message"`
|
||||
LastMessageAt string `json:"last_message_at"`
|
||||
UnreadCount int `json:"unread_count"`
|
||||
Participants []*UserResponse `json:"participants,omitempty"` // 私聊时使用
|
||||
MemberCount int `json:"member_count,omitempty"` // 群聊时使用
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ConversationParticipantResponse 会话参与者响应
|
||||
type ConversationParticipantResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
LastReadSeq int64 `json:"last_read_seq"`
|
||||
Muted bool `json:"muted"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
UserID string `json:"user_id"`
|
||||
LastReadSeq int64 `json:"last_read_seq"`
|
||||
NotificationMuted bool `json:"notification_muted"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
}
|
||||
|
||||
// ==================== Auth DTOs ====================
|
||||
@@ -453,18 +454,19 @@ type ConversationListResponse struct {
|
||||
|
||||
// ConversationDetailResponse 会话详情响应
|
||||
type ConversationDetailResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
LastSeq int64 `json:"last_seq"`
|
||||
LastMessage *MessageResponse `json:"last_message"`
|
||||
LastMessageAt string `json:"last_message_at"`
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
Participants []*UserResponse `json:"participants"`
|
||||
MyLastReadSeq int64 `json:"my_last_read_seq"` // 当前用户的已读位置
|
||||
OtherLastReadSeq int64 `json:"other_last_read_seq"` // 对方用户的已读位置
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
NotificationMuted bool `json:"notification_muted"`
|
||||
LastSeq int64 `json:"last_seq"`
|
||||
LastMessage *MessageResponse `json:"last_message"`
|
||||
LastMessageAt string `json:"last_message_at"`
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
Participants []*UserResponse `json:"participants"`
|
||||
MyLastReadSeq int64 `json:"my_last_read_seq"` // 当前用户的已读位置
|
||||
OtherLastReadSeq int64 `json:"other_last_read_seq"` // 对方用户的已读位置
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// UnreadCountResponse 未读数响应
|
||||
|
||||
@@ -38,7 +38,7 @@ func ConvertMessageToResponse(message *model.Message) *MessageResponse {
|
||||
// participants: 会话参与者列表(用户信息)
|
||||
// unreadCount: 当前用户的未读消息数
|
||||
// lastMessage: 最后一条消息
|
||||
func ConvertConversationToResponse(conv *model.Conversation, participants []*model.User, unreadCount int, lastMessage *model.Message, isPinned bool) *ConversationResponse {
|
||||
func ConvertConversationToResponse(conv *model.Conversation, participants []*model.User, unreadCount int, lastMessage *model.Message, isPinned bool, notificationMuted bool) *ConversationResponse {
|
||||
if conv == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -55,22 +55,23 @@ func ConvertConversationToResponse(conv *model.Conversation, participants []*mod
|
||||
}
|
||||
|
||||
return &ConversationResponse{
|
||||
ID: conv.ID,
|
||||
Type: string(conv.Type),
|
||||
IsPinned: isPinned,
|
||||
Group: groupResponse,
|
||||
LastSeq: conv.LastSeq,
|
||||
LastMessage: ConvertMessageToResponse(lastMessage),
|
||||
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
||||
UnreadCount: unreadCount,
|
||||
Participants: participantResponses,
|
||||
CreatedAt: FormatTime(conv.CreatedAt),
|
||||
UpdatedAt: FormatTime(conv.UpdatedAt),
|
||||
ID: conv.ID,
|
||||
Type: string(conv.Type),
|
||||
IsPinned: isPinned,
|
||||
NotificationMuted: notificationMuted,
|
||||
Group: groupResponse,
|
||||
LastSeq: conv.LastSeq,
|
||||
LastMessage: ConvertMessageToResponse(lastMessage),
|
||||
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
||||
UnreadCount: unreadCount,
|
||||
Participants: participantResponses,
|
||||
CreatedAt: FormatTime(conv.CreatedAt),
|
||||
UpdatedAt: FormatTime(conv.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertConversationToDetailResponse 将Conversation转换为ConversationDetailResponse
|
||||
func ConvertConversationToDetailResponse(conv *model.Conversation, participants []*model.User, unreadCount int64, lastMessage *model.Message, myLastReadSeq int64, otherLastReadSeq int64, isPinned bool) *ConversationDetailResponse {
|
||||
func ConvertConversationToDetailResponse(conv *model.Conversation, participants []*model.User, unreadCount int64, lastMessage *model.Message, myLastReadSeq int64, otherLastReadSeq int64, isPinned bool, notificationMuted bool) *ConversationDetailResponse {
|
||||
if conv == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -81,18 +82,19 @@ func ConvertConversationToDetailResponse(conv *model.Conversation, participants
|
||||
}
|
||||
|
||||
return &ConversationDetailResponse{
|
||||
ID: conv.ID,
|
||||
Type: string(conv.Type),
|
||||
IsPinned: isPinned,
|
||||
LastSeq: conv.LastSeq,
|
||||
LastMessage: ConvertMessageToResponse(lastMessage),
|
||||
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
||||
UnreadCount: unreadCount,
|
||||
Participants: participantResponses,
|
||||
MyLastReadSeq: myLastReadSeq,
|
||||
OtherLastReadSeq: otherLastReadSeq,
|
||||
CreatedAt: FormatTime(conv.CreatedAt),
|
||||
UpdatedAt: FormatTime(conv.UpdatedAt),
|
||||
ID: conv.ID,
|
||||
Type: string(conv.Type),
|
||||
IsPinned: isPinned,
|
||||
NotificationMuted: notificationMuted,
|
||||
LastSeq: conv.LastSeq,
|
||||
LastMessage: ConvertMessageToResponse(lastMessage),
|
||||
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
||||
UnreadCount: unreadCount,
|
||||
Participants: participantResponses,
|
||||
MyLastReadSeq: myLastReadSeq,
|
||||
OtherLastReadSeq: otherLastReadSeq,
|
||||
CreatedAt: FormatTime(conv.CreatedAt),
|
||||
UpdatedAt: FormatTime(conv.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ func ConvertMessagesToResponse(messages []*model.Message) []*MessageResponse {
|
||||
func ConvertConversationsToResponse(convs []*model.Conversation) []*ConversationResponse {
|
||||
result := make([]*ConversationResponse, 0, len(convs))
|
||||
for _, conv := range convs {
|
||||
result = append(result, ConvertConversationToResponse(conv, nil, 0, nil, false))
|
||||
result = append(result, ConvertConversationToResponse(conv, nil, 0, nil, false, false))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -97,16 +97,17 @@ func (h *MessageHandler) GetConversations(c *gin.Context) {
|
||||
var resp *dto.ConversationResponse
|
||||
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
||||
isPinned := myParticipant != nil && myParticipant.IsPinned
|
||||
notificationMuted := myParticipant != nil && myParticipant.NotificationMuted
|
||||
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
|
||||
// 群聊:实时计算群成员数量
|
||||
memberCount, _ := h.groupService.GetMemberCount(*conv.GroupID)
|
||||
// 创建响应并设置member_count
|
||||
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned)
|
||||
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned, notificationMuted)
|
||||
resp.MemberCount = memberCount
|
||||
} else {
|
||||
// 私聊:获取参与者信息
|
||||
participants, _ := h.getConversationParticipants(c.Request.Context(), conv.ID, userID)
|
||||
resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned)
|
||||
resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned, notificationMuted)
|
||||
}
|
||||
result[i] = resp
|
||||
}
|
||||
@@ -153,8 +154,9 @@ func (h *MessageHandler) CreateConversation(c *gin.Context) {
|
||||
participants := []*model.User{targetUser}
|
||||
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
||||
isPinned := myParticipant != nil && myParticipant.IsPinned
|
||||
notificationMuted := myParticipant != nil && myParticipant.NotificationMuted
|
||||
|
||||
response.Success(c, dto.ConvertConversationToResponse(conv, participants, 0, nil, isPinned))
|
||||
response.Success(c, dto.ConvertConversationToResponse(conv, participants, 0, nil, isPinned, notificationMuted))
|
||||
}
|
||||
|
||||
// GetConversationByID 获取会话详情
|
||||
@@ -188,18 +190,20 @@ func (h *MessageHandler) GetConversationByID(c *gin.Context) {
|
||||
// 获取当前用户的已读位置
|
||||
myLastReadSeq := int64(0)
|
||||
isPinned := false
|
||||
notificationMuted := false
|
||||
allParticipants, _ := h.messageService.GetConversationParticipants(conversationID)
|
||||
for _, p := range allParticipants {
|
||||
if p.UserID == userID {
|
||||
myLastReadSeq = p.LastReadSeq
|
||||
isPinned = p.IsPinned
|
||||
notificationMuted = p.NotificationMuted
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 获取对方用户的已读位置
|
||||
otherLastReadSeq := int64(0)
|
||||
response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned))
|
||||
response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned, notificationMuted))
|
||||
}
|
||||
|
||||
// GetMessages 获取消息列表
|
||||
@@ -451,16 +455,17 @@ func (h *MessageHandler) HandleGetConversationList(c *gin.Context) {
|
||||
var resp *dto.ConversationResponse
|
||||
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
||||
isPinned := myParticipant != nil && myParticipant.IsPinned
|
||||
notificationMuted := myParticipant != nil && myParticipant.NotificationMuted
|
||||
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
|
||||
// 群聊:实时计算群成员数量
|
||||
memberCount, _ := h.groupService.GetMemberCount(*conv.GroupID)
|
||||
// 创建响应并设置member_count
|
||||
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned)
|
||||
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned, notificationMuted)
|
||||
resp.MemberCount = memberCount
|
||||
} else {
|
||||
// 私聊:获取参与者信息
|
||||
participants, _ := h.getConversationParticipants(c.Request.Context(), conv.ID, userID)
|
||||
resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned)
|
||||
resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned, notificationMuted)
|
||||
}
|
||||
result[i] = resp
|
||||
}
|
||||
@@ -704,8 +709,9 @@ func (h *MessageHandler) HandleCreateConversation(c *gin.Context) {
|
||||
participants := []*model.User{targetUser}
|
||||
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
||||
isPinned := myParticipant != nil && myParticipant.IsPinned
|
||||
notificationMuted := myParticipant != nil && myParticipant.NotificationMuted
|
||||
|
||||
response.Success(c, dto.ConvertConversationToResponse(conv, participants, 0, nil, isPinned))
|
||||
response.Success(c, dto.ConvertConversationToResponse(conv, participants, 0, nil, isPinned, notificationMuted))
|
||||
}
|
||||
|
||||
// HandleGetConversation 获取会话详情
|
||||
@@ -738,18 +744,20 @@ func (h *MessageHandler) HandleGetConversation(c *gin.Context) {
|
||||
// 获取当前用户的已读位置
|
||||
myLastReadSeq := int64(0)
|
||||
isPinned := false
|
||||
notificationMuted := false
|
||||
allParticipants, _ := h.messageService.GetConversationParticipants(conversationID)
|
||||
for _, p := range allParticipants {
|
||||
if p.UserID == userID {
|
||||
myLastReadSeq = p.LastReadSeq
|
||||
isPinned = p.IsPinned
|
||||
notificationMuted = p.NotificationMuted
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 获取对方用户的已读位置
|
||||
otherLastReadSeq := int64(0)
|
||||
response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned))
|
||||
response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned, notificationMuted))
|
||||
}
|
||||
|
||||
// HandleGetMessages 获取会话消息
|
||||
@@ -903,6 +911,40 @@ func (h *MessageHandler) HandleSetConversationPinned(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// HandleSetConversationNotificationMuted 设置会话免打扰状态
|
||||
// PUT /api/v1/conversations/:id/notification_muted
|
||||
func (h *MessageHandler) HandleSetConversationNotificationMuted(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
conversationID := getIDParam(c, "id")
|
||||
if conversationID == "" {
|
||||
response.BadRequest(c, "conversation id is required")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
NotificationMuted bool `json:"notification_muted"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.chatService.SetConversationNotificationMuted(c.Request.Context(), conversationID, userID, req.NotificationMuted); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "conversation notification_muted status updated", gin.H{
|
||||
"conversation_id": conversationID,
|
||||
"notification_muted": req.NotificationMuted,
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Cursor Pagination Handlers ====================
|
||||
|
||||
// GetMessagesByCursor 游标分页获取会话消息
|
||||
@@ -993,15 +1035,16 @@ func (h *MessageHandler) GetConversationsByCursor(c *gin.Context) {
|
||||
var resp *dto.ConversationResponse
|
||||
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
||||
isPinned := myParticipant != nil && myParticipant.IsPinned
|
||||
notificationMuted := myParticipant != nil && myParticipant.NotificationMuted
|
||||
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
|
||||
// 群聊:实时计算群成员数量
|
||||
memberCount, _ := h.groupService.GetMemberCount(*conv.GroupID)
|
||||
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned)
|
||||
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned, notificationMuted)
|
||||
resp.MemberCount = memberCount
|
||||
} else {
|
||||
// 私聊:获取参与者信息
|
||||
participants, _ := h.getConversationParticipants(c.Request.Context(), conv.ID, userID)
|
||||
resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned)
|
||||
resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned, notificationMuted)
|
||||
}
|
||||
items[i] = resp
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ type ConversationParticipant struct {
|
||||
ConversationID string `gorm:"not null;size:20;uniqueIndex:idx_conversation_user,priority:1;index:idx_cp_conversation_user,priority:1" json:"conversation_id"` // 雪花算法ID(string类型)
|
||||
UserID string `gorm:"column:user_id;type:varchar(50);not null;uniqueIndex:idx_conversation_user,priority:2;index:idx_cp_conversation_user,priority:2;index:idx_cp_user_hidden_pinned_updated,priority:1" json:"user_id"` // UUID格式,与JWT中user_id保持一致
|
||||
LastReadSeq int64 `gorm:"default:0" json:"last_read_seq"` // 已读到的seq位置
|
||||
Muted bool `gorm:"default:false" json:"muted"` // 是否免打扰
|
||||
NotificationMuted bool `gorm:"column:muted;default:false" json:"notification_muted"` // 是否免打扰
|
||||
IsPinned bool `gorm:"default:false;index:idx_cp_user_hidden_pinned_updated,priority:3" json:"is_pinned"` // 是否置顶会话(用户维度)
|
||||
HiddenAt *time.Time `gorm:"index:idx_cp_user_hidden_pinned_updated,priority:2" json:"hidden_at,omitempty"` // 仅自己删除会话时使用,收到新消息后自动恢复
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
|
||||
@@ -32,13 +32,12 @@ func New(cfg *config.S3Config) (*Client, error) {
|
||||
return nil, fmt.Errorf("failed to create S3 client: %w", err)
|
||||
}
|
||||
|
||||
// 检查bucket是否存在
|
||||
// 检查bucket是否存在,权限不足时跳过检查(不阻塞启动)
|
||||
exists, err := client.BucketExists(ctx, cfg.Bucket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check bucket: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
// Access Denied 或网络不通时,跳过 bucket 检查,假定 bucket 已存在
|
||||
// 后续上传时会暴露具体错误
|
||||
} else if !exists {
|
||||
if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{
|
||||
Region: cfg.Region,
|
||||
}); err != nil {
|
||||
|
||||
@@ -28,6 +28,7 @@ type MessageRepository interface {
|
||||
GetParticipant(conversationID string, userID string) (*model.ConversationParticipant, error)
|
||||
UpdateLastReadSeq(conversationID string, userID string, lastReadSeq int64) error
|
||||
UpdatePinned(conversationID string, userID string, isPinned bool) error
|
||||
UpdateNotificationMuted(conversationID string, userID string, notificationMuted bool) error
|
||||
GetUnreadCount(conversationID string, userID string) (int64, error)
|
||||
UpdateConversationLastSeq(conversationID string, seq int64) error
|
||||
GetNextSeq(conversationID string) (int64, error)
|
||||
@@ -337,6 +338,36 @@ func (r *messageRepository) UpdatePinned(conversationID string, userID string, i
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetUnreadCount 获取未读消息数
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *messageRepository) GetUnreadCount(conversationID string, userID string) (int64, error) {
|
||||
|
||||
@@ -373,6 +373,7 @@ func (r *Router) setupRoutes() {
|
||||
conversations.POST("/:id/messages", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleSendMessage) // 发送消息
|
||||
conversations.POST("/:id/read", r.messageHandler.HandleMarkRead) // 标记已读
|
||||
conversations.PUT("/:id/pinned", r.messageHandler.HandleSetConversationPinned) // 置顶设置
|
||||
conversations.PUT("/:id/notification_muted", r.messageHandler.HandleSetConversationNotificationMuted) // 免打扰设置
|
||||
conversations.GET("/unread/count", r.messageHandler.GetUnreadCount) // 未读数
|
||||
conversations.POST("/:id/typing", r.messageHandler.HandleTyping) // 输入状态
|
||||
conversations.DELETE("/:id/self", r.messageHandler.HandleDeleteConversationForSelf) // 删除会话(仅自己)
|
||||
|
||||
@@ -27,7 +27,8 @@ type ChatService interface {
|
||||
GetConversationList(ctx context.Context, userID string, page, pageSize int) ([]*model.Conversation, int64, error)
|
||||
GetConversationByID(ctx context.Context, conversationID string, userID string) (*model.Conversation, error)
|
||||
DeleteConversationForSelf(ctx context.Context, conversationID string, userID string) error
|
||||
SetConversationPinned(ctx context.Context, conversationID string, userID string, isPinned bool) error
|
||||
SetConversationPinned(ctx context.Context, conversationID string, userID string, isPinned bool) error
|
||||
SetConversationNotificationMuted(ctx context.Context, conversationID string, userID string, notificationMuted bool) error
|
||||
|
||||
// 消息操作
|
||||
SendMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error)
|
||||
@@ -215,6 +216,32 @@ func (s *chatServiceImpl) SetConversationPinned(ctx context.Context, conversatio
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetConversationNotificationMuted 设置会话免打扰状态(用户维度)
|
||||
func (s *chatServiceImpl) SetConversationNotificationMuted(ctx context.Context, conversationID string, userID string, notificationMuted bool) error {
|
||||
participant, err := s.getParticipant(ctx, conversationID, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("conversation not found or no permission")
|
||||
}
|
||||
return fmt.Errorf("failed to get participant: %w", err)
|
||||
}
|
||||
if participant.ConversationID == "" {
|
||||
return errors.New("conversation not found or no permission")
|
||||
}
|
||||
|
||||
if err := s.repo.UpdateNotificationMuted(conversationID, userID, notificationMuted); err != nil {
|
||||
return fmt.Errorf("failed to update notification_muted status: %w", err)
|
||||
}
|
||||
|
||||
// 失效缓存
|
||||
if s.conversationCache != nil {
|
||||
s.conversationCache.InvalidateParticipant(conversationID, userID)
|
||||
s.conversationCache.InvalidateConversationList(userID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMessage 发送消息(使用 segments)
|
||||
func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error) {
|
||||
// 首先验证会话是否存在
|
||||
|
||||
Reference in New Issue
Block a user