diff --git a/backend/internal/modules/chat/dto.go b/backend/internal/modules/chat/dto.go index fb2fc8f..2c5e161 100644 --- a/backend/internal/modules/chat/dto.go +++ b/backend/internal/modules/chat/dto.go @@ -51,6 +51,10 @@ type MessageDTO struct { CreatedAt time.Time `json:"created_at"` } +type UnreadCountDTO struct { + UnreadCount int64 `json:"unread_count"` +} + type SendMessageRequest struct { Content string `json:"content"` AttachmentURLS []string `json:"attachment_urls"` diff --git a/backend/internal/modules/chat/handler_user.go b/backend/internal/modules/chat/handler_user.go index 39dc2f8..cb2d0bd 100644 --- a/backend/internal/modules/chat/handler_user.go +++ b/backend/internal/modules/chat/handler_user.go @@ -15,6 +15,20 @@ func (h *Handler) List(c *gin.Context) { h.list(c, Principal{Type: "user", ID: userID}) } +func (h *Handler) UnreadCount(c *gin.Context) { + userID, ok := currentUserID(c) + if !ok { + response.Unauthorized(c, "缺少用户上下文") + return + } + result, err := h.service.CountUnreadMessages(c.Request.Context(), Principal{Type: "user", ID: userID}) + if err != nil { + writeChatError(c, err) + return + } + response.OK(c, result) +} + func (h *Handler) Detail(c *gin.Context) { userID, ok := currentUserID(c) if !ok { diff --git a/backend/internal/modules/chat/participant.go b/backend/internal/modules/chat/participant.go index b3b166a..b8e640e 100644 --- a/backend/internal/modules/chat/participant.go +++ b/backend/internal/modules/chat/participant.go @@ -40,6 +40,18 @@ func (r *Repository) conversationQuery(ctx context.Context, principal Principal) Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id"). Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) } + +func (r *Repository) CountUnreadMessages(ctx context.Context, principal Principal) (int64, error) { + var total int64 + err := r.db.WithContext(ctx).Table("chat_messages AS cm"). + Joins("JOIN chat_participants AS cp ON cp.conversation_id = cm.conversation_id"). + Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID). + Where("NOT (cm.sender_type = ? AND cm.sender_id = ?)", principal.Type, principal.ID). + Where("(cp.last_read_at IS NULL OR cm.created_at > cp.last_read_at)"). + Count(&total).Error + return total, err +} + func (r *Repository) findParticipant(tx *gorm.DB, principal Principal, conversationID uint64, lock bool) (*model.ChatParticipant, error) { var participant model.ChatParticipant db := tx diff --git a/backend/internal/modules/chat/service.go b/backend/internal/modules/chat/service.go index 409aa98..37c7ce7 100644 --- a/backend/internal/modules/chat/service.go +++ b/backend/internal/modules/chat/service.go @@ -29,6 +29,17 @@ func (s *Service) ListConversations(ctx context.Context, principal Principal, pa return s.repo.ListConversations(ctx, principal, page, pageSize) } +func (s *Service) CountUnreadMessages(ctx context.Context, principal Principal) (*UnreadCountDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + total, err := s.repo.CountUnreadMessages(ctx, principal) + if err != nil { + return nil, err + } + return &UnreadCountDTO{UnreadCount: total}, nil +} + func (s *Service) FindConversation(ctx context.Context, principal Principal, id uint64) (*ConversationDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index a6786a1..24287d7 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -403,6 +403,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { chatRoutes.GET("/events", chatHubHandler.UserEvents) } chatRoutes.POST("/support", chatHandler.EnsureSupportConversation) + chatRoutes.GET("/unread-count", chatHandler.UnreadCount) chatRoutes.GET("", chatHandler.List) chatRoutes.GET("/:id", chatHandler.Detail) chatRoutes.GET("/:id/messages", chatHandler.Messages) diff --git a/frontend/src/components/MobileBottomNav.vue b/frontend/src/components/MobileBottomNav.vue index 903221a..cadc0e6 100644 --- a/frontend/src/components/MobileBottomNav.vue +++ b/frontend/src/components/MobileBottomNav.vue @@ -1,7 +1,9 @@