package chat import ( "context" "errors" "gorm.io/gorm" "gorm.io/gorm/clause" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/chathub" "time" ) func (r *Repository) Messages(ctx context.Context, principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) { page, pageSize = normalizePagination(page, pageSize) db := r.db.WithContext(ctx) var participant *model.ChatParticipant // 管理员可以查看任意会话的消息,普通用户需要是 participant if principal.Type != "admin" { p, err := r.findParticipant(db, principal, conversationID, false) if err != nil { return nil, err } participant = p } else { // 管理员需要验证会话存在 var count int64 if err := db.Model(&model.ChatConversation{}).Where("id = ?", conversationID).Count(&count).Error; err != nil { return nil, err } if count == 0 { return nil, ErrConversationNotFound } } // 构建查询 query := db.Model(&model.ChatMessage{}).Where("conversation_id = ?", conversationID) // 租客只能看到加入时间之后的消息 if principal.Type == "user" && participant != nil && participant.Role == "renter" { query = query.Where("created_at >= ?", participant.JoinedAt) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, err } offset := (page - 1) * pageSize var rows []model.ChatMessage if err := query.Order("id ASC"). Offset(offset). Limit(pageSize). Find(&rows).Error; err != nil { return nil, err } items, err := r.toMessageDTOs(ctx, principal, rows) if err != nil { return nil, err } return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } func (r *Repository) SendMessage(ctx context.Context, principal Principal, conversationID uint64, req SendMessageRequest) (*MessageDTO, error) { var messageID uint64 err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var conversation model.ChatConversation if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&conversation, conversationID).Error; err != nil { return err } if conversation.Status != "active" { return ErrPermissionDenied } var senderRole string var participant *model.ChatParticipant // 管理员可以在任意会话发送消息,无需是 participant if principal.Type == "admin" { // 尝试查找管理员的 participant 记录 var p model.ChatParticipant err := tx.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?", conversationID, principal.Type, principal.ID).First(&p).Error if err == nil { // 管理员是 participant,使用其角色 participant = &p senderRole = p.Role } else if errors.Is(err, gorm.ErrRecordNotFound) { // 管理员不是 participant,使用特殊角色 "admin" senderRole = "admin" } else { return err } } else { // 普通用户必须是 participant p, err := r.findParticipant(tx, principal, conversationID, true) if err != nil { return err } participant = p senderRole = p.Role } message := model.ChatMessage{ ConversationID: conversation.ID, SenderType: principal.Type, SenderID: principal.ID, SenderRole: senderRole, ContentType: "text", Content: req.Content, AttachmentURLS: encodeStringList(req.AttachmentURLS), } if err := tx.Create(&message).Error; err != nil { return err } conversation.LastMessageID = &message.ID conversation.LastMessagePreview = messagePreview(message.Content, req.AttachmentURLS) conversation.LastMessageAt = &message.CreatedAt if err := tx.Save(&conversation).Error; err != nil { return err } // 更新 participant 的已读时间(仅当是 participant 时) if participant != nil { now := time.Now() if err := tx.Model(participant).Update("last_read_at", now).Error; err != nil { return err } } messageID = message.ID return nil }) if err != nil { return nil, err } var message model.ChatMessage if err := r.db.WithContext(ctx).First(&message, messageID).Error; err != nil { return nil, err } items, err := r.toMessageDTOs(ctx, principal, []model.ChatMessage{message}) if err != nil { return nil, err } if len(items) == 0 { return nil, ErrConversationNotFound } // 推送新消息事件给会话中的在线参与者 if r.hub != nil { msg := items[0] event := &chathub.ChatEvent{ Type: "new_message", ConversationID: conversationID, Message: &chathub.MessageData{ ID: msg.ID, ConversationID: msg.ConversationID, SenderType: msg.SenderType, SenderID: msg.SenderID, SenderRole: msg.SenderRole, SenderName: msg.SenderName, ContentType: msg.ContentType, Content: msg.Content, AttachmentURLS: msg.AttachmentURLS, CreatedAt: msg.CreatedAt.Format(time.RFC3339), }, } r.hub.NotifyConversation(conversationID, event) // 同时广播给所有在线客服,使「未分配 / 全部」视图实时刷新; // 参与者客服会重复收到,由前端按消息 id 去重。 r.hub.NotifyAllAdmins(event) } return &items[0], nil } func (r *Repository) MarkRead(ctx context.Context, principal Principal, conversationID uint64) error { now := time.Now() updated := false err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 管理员可以不是 participant,直接返回成功 if principal.Type == "admin" { // 尝试查找 participant 记录,如果有就更新 var participant model.ChatParticipant err := tx.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?", conversationID, principal.Type, principal.ID).First(&participant).Error if err == nil { // 有 participant 记录,更新已读时间 updated = true return tx.Model(&participant).Update("last_read_at", now).Error } else if errors.Is(err, gorm.ErrRecordNotFound) { // 没有 participant 记录,直接返回成功(管理员无需记录已读) return nil } return err } // 普通用户必须是 participant participant, err := r.findParticipant(tx, principal, conversationID, true) if err != nil { return err } updated = true return tx.Model(participant).Update("last_read_at", now).Error }) if err != nil { return err } // 已读时间发生变化时,通知会话内其他参与者刷新「已读」状态。 if updated && r.hub != nil { event := &chathub.ChatEvent{ Type: "conversation_read", ConversationID: conversationID, ReaderType: principal.Type, ReaderID: principal.ID, ReadAt: now.Format(time.RFC3339), } r.hub.NotifyConversation(conversationID, event) // 同时广播给所有在线客服,覆盖未分配会话中非参与者客服的「已读」刷新。 r.hub.NotifyAllAdmins(event) } return nil }