You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue+Firebase多用户一对一聊天的消息过滤实现咨询

多用户一对一聊天实现问题(Firestore + Vue)

需求背景

需要实现普通用户间的一对一聊天功能,仅对话双方可查看聊天内容。此前仅实现过管理员与用户的聊天,对普通用户间的聊天逻辑不清晰。

当前尝试代码

发送消息的Vue组件代码

<script setup>
const { ChatMess, error } = getChatMess(firebaseAuth.currentUser.uid);
const route = useRoute()
const { user } = getUser()
const message = ref('')
const Activate = ref(false)
const NewMessage = ref(true)
const NewMessage2 = ref(false)

const handleSubmit = async () => {  
  const NewChat = {
      name: user.value.displayName,
      message: message.value,
      createdAt: timestamp(),
  } 
  const NewMessages = {
    NewMessage: NewMessage.value,
  }
  const NewMessages2 = {
    NewMessage: NewMessage2.value,
  }
  message.value = ''
  const res = await projectFirestore.collection('Subscribed').doc(firebaseAuth.currentUser.uid).collection('messages').doc(route.params.id).set({
    name: user.value.displayName,
    message: message.value,
    createdAt: timestamp(),
  }, { merge: true })

 const messages = ref(null)

return { handleSubmit, message }
}
</script>

获取消息的工具函数

import { projectFirestore } from "../Firebase/Config";
import { ref, watchEffect } from "vue"

const getChatMess = (id) => {
    const ChatMess = ref(null)
    const error = ref(null)

    let collectionRef = projectFirestore.collection('Subscribed').doc(id).collection('messages').orderBy('createdAt') 
    const unsub = collectionRef.onSnapshot(snap => {
        console.log(snap)
        let results = []
        snap.docs.forEach(doc => {
          doc.data().createdAt && results.push({...doc.data(), id: doc.id})
        });
        
        ChatMess.value = results
        error.value = null 
        console.log(ChatMess)
      }, err => {
        console.log(err.message)
        ChatMess.value = null
        error.value = 'could not fetch the data'
      })
    
      watchEffect((onInvalidate) => {
        onInvalidate(() => unsub());
      });

    return { ChatMess, error }
}

export default getChatMess

当前数据库结构

Subscribed集合下以用户UID为文档ID,每个用户文档内包含messages子集合,子集合中以对话方UID为文档ID存储消息内容。

遇到的问题

  • 发送新消息时会覆盖旧消息;
  • 使用set({NewChat}, {merge:true})时,消息字段会被存入Map类型,无法通过v-for遍历展示。

考虑过的方案

  1. 为每个对话方创建独立集合,每条消息作为新文档存入,但不知如何通过route.params.id过滤消息;
  2. 编写Firestore规则,仅允许包含双方UID的文档被读取,但不会编写此类规则。

可行实现方案

一、优化数据库结构

一对一聊天的标准做法是为每对用户生成唯一对话ID(按字典序拼接双方UID,确保无论谁发起对话,ID一致),然后创建独立的chats集合:

  • chats集合:每个文档对应一个对话,文档ID为生成的唯一对话ID,文档字段包含participants(存储双方UID数组)、updatedAt(对话更新时间);
  • 每个对话文档下的messages子集合:每条消息作为独立文档存储,字段包含senderId(发送者UID)、senderName(发送者昵称)、content(消息内容)、createdAt(发送时间)。

二、Firestore安全规则

限制用户仅能访问包含自己UID的对话及消息:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 对话集合访问限制
    match /chats/{chatId} {
      allow read, write: if request.auth != null && request.auth.uid in resource.data.participants;
      
      // 消息子集合访问限制
      match /messages/{messageId} {
        allow read, write: if request.auth != null && exists(/databases/$(database)/documents/chats/$(chatId)) && request.auth.uid in get(/databases/$(database)/documents/chats/$(chatId)).data.participants;
      }
    }
  }
}

三、代码调整

1. 生成唯一对话ID的工具函数

function generateChatId(uid1, uid2) {
  return uid1 < uid2 ? `${uid1}_${uid2}` : `${uid2}_${uid1}`;
}

2. 修改发送消息的逻辑

<script setup>
import { projectFirestore, timestamp } from "../Firebase/Config";
import { ref } from "vue";
import { useRoute } from "vue-router";
import { firebaseAuth } from "../Firebase/Config";
import { getUser } from "./your-user-composable";

const route = useRoute();
const { user } = getUser();
const message = ref('');

const generateChatId = (uid1, uid2) => {
  return uid1 < uid2 ? `${uid1}_${uid2}` : `${uid2}_${uid1}`;
};

const handleSubmit = async () => {
  if (!message.value.trim()) return;
  
  const currentUserId = firebaseAuth.currentUser.uid;
  const recipientUserId = route.params.id;
  const chatId = generateChatId(currentUserId, recipientUserId);
  
  // 确保对话文档存在(不存在则创建)
  await projectFirestore.collection('chats').doc(chatId).set({
    participants: [currentUserId, recipientUserId],
    updatedAt: timestamp()
  }, { merge: true });
  
  // 添加新消息到子集合
  await projectFirestore.collection('chats').doc(chatId).collection('messages').add({
    senderId: currentUserId,
    senderName: user.value.displayName,
    content: message.value,
    createdAt: timestamp()
  });
  
  message.value = '';
};

return { handleSubmit, message };
</script>

3. 修改获取消息的逻辑

import { projectFirestore } from "../Firebase/Config";
import { ref, watchEffect } from "vue";

const getChatMessages = (chatId) => {
  const chatMessages = ref(null);
  const error = ref(null);
  
  let collectionRef = projectFirestore
    .collection('chats')
    .doc(chatId)
    .collection('messages')
    .orderBy('createdAt');
  
  const unsub = collectionRef.onSnapshot(snap => {
    let results = [];
    snap.docs.forEach(doc => {
      doc.data().createdAt && results.push({ ...doc.data(), id: doc.id });
    });
    chatMessages.value = results;
    error.value = null;
  }, err => {
    console.log(err.message);
    chatMessages.value = null;
    error.value = '无法获取消息';
  });
  
  watchEffect((onInvalidate) => {
    onInvalidate(() => unsub());
  });
  
  return { chatMessages, error };
};

export default getChatMessages;

4. 在组件中调用获取消息

<script setup>
import { firebaseAuth } from "../Firebase/Config";
import { useRoute } from "vue-router";
import getChatMessages from "./getChatMessages";

const route = useRoute();
const currentUserId = firebaseAuth.currentUser.uid;
const recipientUserId = route.params.id;
const chatId = generateChatId(currentUserId, recipientUserId);
const { chatMessages, error } = getChatMessages(chatId);
</script>

四、扩展:获取用户所有对话列表

const getUserChats = (userId) => {
  const chats = ref(null);
  const error = ref(null);
  
  let collectionRef = projectFirestore
    .collection('chats')
    .where('participants', 'array-contains', userId)
    .orderBy('updatedAt', 'desc');
  
  const unsub = collectionRef.onSnapshot(snap => {
    let results = [];
    snap.docs.forEach(doc => {
      results.push({ ...doc.data(), id: doc.id });
    });
    chats.value = results;
    error.value = null;
  }, err => {
    console.log(err.message);
    chats.value = null;
    error.value = '无法获取对话列表';
  });
  
  watchEffect((onInvalidate) => {
    onInvalidate(() => unsub());
  });
  
  return { chats, error };
};

内容的提问来源于stack exchange,提问作者Epimetheus14

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 08:12:06