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)
|
exists, err := client.BucketExists(ctx, cfg.Bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to check bucket: %w", err)
|
// Access Denied 或网络不通时,跳过 bucket 检查,假定 bucket 已存在
|
||||||
}
|
} else if !exists {
|
||||||
|
|
||||||
if !exists {
|
|
||||||
if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{
|
if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{
|
||||||
Region: cfg.Region,
|
Region: cfg.Region,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|||||||
@@ -350,26 +350,27 @@ type MessageResponse struct {
|
|||||||
|
|
||||||
// ConversationResponse 会话响应
|
// ConversationResponse 会话响应
|
||||||
type ConversationResponse struct {
|
type ConversationResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
IsPinned bool `json:"is_pinned"`
|
IsPinned bool `json:"is_pinned"`
|
||||||
Group *GroupResponse `json:"group,omitempty"`
|
NotificationMuted bool `json:"notification_muted"`
|
||||||
LastSeq int64 `json:"last_seq"`
|
Group *GroupResponse `json:"group,omitempty"`
|
||||||
LastMessage *MessageResponse `json:"last_message"`
|
LastSeq int64 `json:"last_seq"`
|
||||||
LastMessageAt string `json:"last_message_at"`
|
LastMessage *MessageResponse `json:"last_message"`
|
||||||
UnreadCount int `json:"unread_count"`
|
LastMessageAt string `json:"last_message_at"`
|
||||||
Participants []*UserResponse `json:"participants,omitempty"` // 私聊时使用
|
UnreadCount int `json:"unread_count"`
|
||||||
MemberCount int `json:"member_count,omitempty"` // 群聊时使用
|
Participants []*UserResponse `json:"participants,omitempty"` // 私聊时使用
|
||||||
CreatedAt string `json:"created_at"`
|
MemberCount int `json:"member_count,omitempty"` // 群聊时使用
|
||||||
UpdatedAt string `json:"updated_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConversationParticipantResponse 会话参与者响应
|
// ConversationParticipantResponse 会话参与者响应
|
||||||
type ConversationParticipantResponse struct {
|
type ConversationParticipantResponse struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
LastReadSeq int64 `json:"last_read_seq"`
|
LastReadSeq int64 `json:"last_read_seq"`
|
||||||
Muted bool `json:"muted"`
|
NotificationMuted bool `json:"notification_muted"`
|
||||||
IsPinned bool `json:"is_pinned"`
|
IsPinned bool `json:"is_pinned"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Auth DTOs ====================
|
// ==================== Auth DTOs ====================
|
||||||
@@ -453,18 +454,19 @@ type ConversationListResponse struct {
|
|||||||
|
|
||||||
// ConversationDetailResponse 会话详情响应
|
// ConversationDetailResponse 会话详情响应
|
||||||
type ConversationDetailResponse struct {
|
type ConversationDetailResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
IsPinned bool `json:"is_pinned"`
|
IsPinned bool `json:"is_pinned"`
|
||||||
LastSeq int64 `json:"last_seq"`
|
NotificationMuted bool `json:"notification_muted"`
|
||||||
LastMessage *MessageResponse `json:"last_message"`
|
LastSeq int64 `json:"last_seq"`
|
||||||
LastMessageAt string `json:"last_message_at"`
|
LastMessage *MessageResponse `json:"last_message"`
|
||||||
UnreadCount int64 `json:"unread_count"`
|
LastMessageAt string `json:"last_message_at"`
|
||||||
Participants []*UserResponse `json:"participants"`
|
UnreadCount int64 `json:"unread_count"`
|
||||||
MyLastReadSeq int64 `json:"my_last_read_seq"` // 当前用户的已读位置
|
Participants []*UserResponse `json:"participants"`
|
||||||
OtherLastReadSeq int64 `json:"other_last_read_seq"` // 对方用户的已读位置
|
MyLastReadSeq int64 `json:"my_last_read_seq"` // 当前用户的已读位置
|
||||||
CreatedAt string `json:"created_at"`
|
OtherLastReadSeq int64 `json:"other_last_read_seq"` // 对方用户的已读位置
|
||||||
UpdatedAt string `json:"updated_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnreadCountResponse 未读数响应
|
// UnreadCountResponse 未读数响应
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func ConvertMessageToResponse(message *model.Message) *MessageResponse {
|
|||||||
// participants: 会话参与者列表(用户信息)
|
// participants: 会话参与者列表(用户信息)
|
||||||
// unreadCount: 当前用户的未读消息数
|
// unreadCount: 当前用户的未读消息数
|
||||||
// lastMessage: 最后一条消息
|
// 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 {
|
if conv == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -55,22 +55,23 @@ func ConvertConversationToResponse(conv *model.Conversation, participants []*mod
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &ConversationResponse{
|
return &ConversationResponse{
|
||||||
ID: conv.ID,
|
ID: conv.ID,
|
||||||
Type: string(conv.Type),
|
Type: string(conv.Type),
|
||||||
IsPinned: isPinned,
|
IsPinned: isPinned,
|
||||||
Group: groupResponse,
|
NotificationMuted: notificationMuted,
|
||||||
LastSeq: conv.LastSeq,
|
Group: groupResponse,
|
||||||
LastMessage: ConvertMessageToResponse(lastMessage),
|
LastSeq: conv.LastSeq,
|
||||||
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
LastMessage: ConvertMessageToResponse(lastMessage),
|
||||||
UnreadCount: unreadCount,
|
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
||||||
Participants: participantResponses,
|
UnreadCount: unreadCount,
|
||||||
CreatedAt: FormatTime(conv.CreatedAt),
|
Participants: participantResponses,
|
||||||
UpdatedAt: FormatTime(conv.UpdatedAt),
|
CreatedAt: FormatTime(conv.CreatedAt),
|
||||||
|
UpdatedAt: FormatTime(conv.UpdatedAt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertConversationToDetailResponse 将Conversation转换为ConversationDetailResponse
|
// 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 {
|
if conv == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -81,18 +82,19 @@ func ConvertConversationToDetailResponse(conv *model.Conversation, participants
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &ConversationDetailResponse{
|
return &ConversationDetailResponse{
|
||||||
ID: conv.ID,
|
ID: conv.ID,
|
||||||
Type: string(conv.Type),
|
Type: string(conv.Type),
|
||||||
IsPinned: isPinned,
|
IsPinned: isPinned,
|
||||||
LastSeq: conv.LastSeq,
|
NotificationMuted: notificationMuted,
|
||||||
LastMessage: ConvertMessageToResponse(lastMessage),
|
LastSeq: conv.LastSeq,
|
||||||
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
LastMessage: ConvertMessageToResponse(lastMessage),
|
||||||
UnreadCount: unreadCount,
|
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
||||||
Participants: participantResponses,
|
UnreadCount: unreadCount,
|
||||||
MyLastReadSeq: myLastReadSeq,
|
Participants: participantResponses,
|
||||||
OtherLastReadSeq: otherLastReadSeq,
|
MyLastReadSeq: myLastReadSeq,
|
||||||
CreatedAt: FormatTime(conv.CreatedAt),
|
OtherLastReadSeq: otherLastReadSeq,
|
||||||
UpdatedAt: FormatTime(conv.UpdatedAt),
|
CreatedAt: FormatTime(conv.CreatedAt),
|
||||||
|
UpdatedAt: FormatTime(conv.UpdatedAt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ func ConvertMessagesToResponse(messages []*model.Message) []*MessageResponse {
|
|||||||
func ConvertConversationsToResponse(convs []*model.Conversation) []*ConversationResponse {
|
func ConvertConversationsToResponse(convs []*model.Conversation) []*ConversationResponse {
|
||||||
result := make([]*ConversationResponse, 0, len(convs))
|
result := make([]*ConversationResponse, 0, len(convs))
|
||||||
for _, conv := range 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
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,16 +97,17 @@ func (h *MessageHandler) GetConversations(c *gin.Context) {
|
|||||||
var resp *dto.ConversationResponse
|
var resp *dto.ConversationResponse
|
||||||
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
||||||
isPinned := myParticipant != nil && myParticipant.IsPinned
|
isPinned := myParticipant != nil && myParticipant.IsPinned
|
||||||
|
notificationMuted := myParticipant != nil && myParticipant.NotificationMuted
|
||||||
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
|
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
|
||||||
// 群聊:实时计算群成员数量
|
// 群聊:实时计算群成员数量
|
||||||
memberCount, _ := h.groupService.GetMemberCount(*conv.GroupID)
|
memberCount, _ := h.groupService.GetMemberCount(*conv.GroupID)
|
||||||
// 创建响应并设置member_count
|
// 创建响应并设置member_count
|
||||||
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned)
|
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned, notificationMuted)
|
||||||
resp.MemberCount = memberCount
|
resp.MemberCount = memberCount
|
||||||
} else {
|
} else {
|
||||||
// 私聊:获取参与者信息
|
// 私聊:获取参与者信息
|
||||||
participants, _ := h.getConversationParticipants(c.Request.Context(), conv.ID, userID)
|
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
|
result[i] = resp
|
||||||
}
|
}
|
||||||
@@ -153,8 +154,9 @@ func (h *MessageHandler) CreateConversation(c *gin.Context) {
|
|||||||
participants := []*model.User{targetUser}
|
participants := []*model.User{targetUser}
|
||||||
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
||||||
isPinned := myParticipant != nil && myParticipant.IsPinned
|
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 获取会话详情
|
// GetConversationByID 获取会话详情
|
||||||
@@ -188,18 +190,20 @@ func (h *MessageHandler) GetConversationByID(c *gin.Context) {
|
|||||||
// 获取当前用户的已读位置
|
// 获取当前用户的已读位置
|
||||||
myLastReadSeq := int64(0)
|
myLastReadSeq := int64(0)
|
||||||
isPinned := false
|
isPinned := false
|
||||||
|
notificationMuted := false
|
||||||
allParticipants, _ := h.messageService.GetConversationParticipants(conversationID)
|
allParticipants, _ := h.messageService.GetConversationParticipants(conversationID)
|
||||||
for _, p := range allParticipants {
|
for _, p := range allParticipants {
|
||||||
if p.UserID == userID {
|
if p.UserID == userID {
|
||||||
myLastReadSeq = p.LastReadSeq
|
myLastReadSeq = p.LastReadSeq
|
||||||
isPinned = p.IsPinned
|
isPinned = p.IsPinned
|
||||||
|
notificationMuted = p.NotificationMuted
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取对方用户的已读位置
|
// 获取对方用户的已读位置
|
||||||
otherLastReadSeq := int64(0)
|
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 获取消息列表
|
// GetMessages 获取消息列表
|
||||||
@@ -451,16 +455,17 @@ func (h *MessageHandler) HandleGetConversationList(c *gin.Context) {
|
|||||||
var resp *dto.ConversationResponse
|
var resp *dto.ConversationResponse
|
||||||
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
||||||
isPinned := myParticipant != nil && myParticipant.IsPinned
|
isPinned := myParticipant != nil && myParticipant.IsPinned
|
||||||
|
notificationMuted := myParticipant != nil && myParticipant.NotificationMuted
|
||||||
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
|
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
|
||||||
// 群聊:实时计算群成员数量
|
// 群聊:实时计算群成员数量
|
||||||
memberCount, _ := h.groupService.GetMemberCount(*conv.GroupID)
|
memberCount, _ := h.groupService.GetMemberCount(*conv.GroupID)
|
||||||
// 创建响应并设置member_count
|
// 创建响应并设置member_count
|
||||||
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned)
|
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned, notificationMuted)
|
||||||
resp.MemberCount = memberCount
|
resp.MemberCount = memberCount
|
||||||
} else {
|
} else {
|
||||||
// 私聊:获取参与者信息
|
// 私聊:获取参与者信息
|
||||||
participants, _ := h.getConversationParticipants(c.Request.Context(), conv.ID, userID)
|
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
|
result[i] = resp
|
||||||
}
|
}
|
||||||
@@ -704,8 +709,9 @@ func (h *MessageHandler) HandleCreateConversation(c *gin.Context) {
|
|||||||
participants := []*model.User{targetUser}
|
participants := []*model.User{targetUser}
|
||||||
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
||||||
isPinned := myParticipant != nil && myParticipant.IsPinned
|
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 获取会话详情
|
// HandleGetConversation 获取会话详情
|
||||||
@@ -738,18 +744,20 @@ func (h *MessageHandler) HandleGetConversation(c *gin.Context) {
|
|||||||
// 获取当前用户的已读位置
|
// 获取当前用户的已读位置
|
||||||
myLastReadSeq := int64(0)
|
myLastReadSeq := int64(0)
|
||||||
isPinned := false
|
isPinned := false
|
||||||
|
notificationMuted := false
|
||||||
allParticipants, _ := h.messageService.GetConversationParticipants(conversationID)
|
allParticipants, _ := h.messageService.GetConversationParticipants(conversationID)
|
||||||
for _, p := range allParticipants {
|
for _, p := range allParticipants {
|
||||||
if p.UserID == userID {
|
if p.UserID == userID {
|
||||||
myLastReadSeq = p.LastReadSeq
|
myLastReadSeq = p.LastReadSeq
|
||||||
isPinned = p.IsPinned
|
isPinned = p.IsPinned
|
||||||
|
notificationMuted = p.NotificationMuted
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取对方用户的已读位置
|
// 获取对方用户的已读位置
|
||||||
otherLastReadSeq := int64(0)
|
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 获取会话消息
|
// 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 ====================
|
// ==================== Cursor Pagination Handlers ====================
|
||||||
|
|
||||||
// GetMessagesByCursor 游标分页获取会话消息
|
// GetMessagesByCursor 游标分页获取会话消息
|
||||||
@@ -993,15 +1035,16 @@ func (h *MessageHandler) GetConversationsByCursor(c *gin.Context) {
|
|||||||
var resp *dto.ConversationResponse
|
var resp *dto.ConversationResponse
|
||||||
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
|
||||||
isPinned := myParticipant != nil && myParticipant.IsPinned
|
isPinned := myParticipant != nil && myParticipant.IsPinned
|
||||||
|
notificationMuted := myParticipant != nil && myParticipant.NotificationMuted
|
||||||
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
|
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
|
||||||
// 群聊:实时计算群成员数量
|
// 群聊:实时计算群成员数量
|
||||||
memberCount, _ := h.groupService.GetMemberCount(*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
|
resp.MemberCount = memberCount
|
||||||
} else {
|
} else {
|
||||||
// 私聊:获取参与者信息
|
// 私聊:获取参与者信息
|
||||||
participants, _ := h.getConversationParticipants(c.Request.Context(), conv.ID, userID)
|
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
|
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类型)
|
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保持一致
|
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位置
|
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"` // 是否置顶会话(用户维度)
|
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"` // 仅自己删除会话时使用,收到新消息后自动恢复
|
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"`
|
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)
|
return nil, fmt.Errorf("failed to create S3 client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查bucket是否存在
|
// 检查bucket是否存在,权限不足时跳过检查(不阻塞启动)
|
||||||
exists, err := client.BucketExists(ctx, cfg.Bucket)
|
exists, err := client.BucketExists(ctx, cfg.Bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to check bucket: %w", err)
|
// Access Denied 或网络不通时,跳过 bucket 检查,假定 bucket 已存在
|
||||||
}
|
// 后续上传时会暴露具体错误
|
||||||
|
} else if !exists {
|
||||||
if !exists {
|
|
||||||
if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{
|
if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{
|
||||||
Region: cfg.Region,
|
Region: cfg.Region,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type MessageRepository interface {
|
|||||||
GetParticipant(conversationID string, userID string) (*model.ConversationParticipant, error)
|
GetParticipant(conversationID string, userID string) (*model.ConversationParticipant, error)
|
||||||
UpdateLastReadSeq(conversationID string, userID string, lastReadSeq int64) error
|
UpdateLastReadSeq(conversationID string, userID string, lastReadSeq int64) error
|
||||||
UpdatePinned(conversationID string, userID string, isPinned bool) error
|
UpdatePinned(conversationID string, userID string, isPinned bool) error
|
||||||
|
UpdateNotificationMuted(conversationID string, userID string, notificationMuted bool) error
|
||||||
GetUnreadCount(conversationID string, userID string) (int64, error)
|
GetUnreadCount(conversationID string, userID string) (int64, error)
|
||||||
UpdateConversationLastSeq(conversationID string, seq int64) error
|
UpdateConversationLastSeq(conversationID string, seq int64) error
|
||||||
GetNextSeq(conversationID string) (int64, error)
|
GetNextSeq(conversationID string) (int64, error)
|
||||||
@@ -337,6 +338,36 @@ func (r *messageRepository) UpdatePinned(conversationID string, userID string, i
|
|||||||
return nil
|
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 获取未读消息数
|
// GetUnreadCount 获取未读消息数
|
||||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||||
func (r *messageRepository) GetUnreadCount(conversationID string, userID string) (int64, error) {
|
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/messages", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleSendMessage) // 发送消息
|
||||||
conversations.POST("/:id/read", r.messageHandler.HandleMarkRead) // 标记已读
|
conversations.POST("/:id/read", r.messageHandler.HandleMarkRead) // 标记已读
|
||||||
conversations.PUT("/:id/pinned", r.messageHandler.HandleSetConversationPinned) // 置顶设置
|
conversations.PUT("/:id/pinned", r.messageHandler.HandleSetConversationPinned) // 置顶设置
|
||||||
|
conversations.PUT("/:id/notification_muted", r.messageHandler.HandleSetConversationNotificationMuted) // 免打扰设置
|
||||||
conversations.GET("/unread/count", r.messageHandler.GetUnreadCount) // 未读数
|
conversations.GET("/unread/count", r.messageHandler.GetUnreadCount) // 未读数
|
||||||
conversations.POST("/:id/typing", r.messageHandler.HandleTyping) // 输入状态
|
conversations.POST("/:id/typing", r.messageHandler.HandleTyping) // 输入状态
|
||||||
conversations.DELETE("/:id/self", r.messageHandler.HandleDeleteConversationForSelf) // 删除会话(仅自己)
|
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)
|
GetConversationList(ctx context.Context, userID string, page, pageSize int) ([]*model.Conversation, int64, error)
|
||||||
GetConversationByID(ctx context.Context, conversationID string, userID string) (*model.Conversation, error)
|
GetConversationByID(ctx context.Context, conversationID string, userID string) (*model.Conversation, error)
|
||||||
DeleteConversationForSelf(ctx context.Context, conversationID string, userID string) 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)
|
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
|
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)
|
// SendMessage 发送消息(使用 segments)
|
||||||
func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error) {
|
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