You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Firestore未投票Poll筛选与数据模型设计技术咨询

针对你用Firestore做投票系统遇到的两个问题,我给你整理了实战性的解决方案,都是基于Firestore的最佳实践来的:

1. 数据模型设计方案

根据用户只能参与未投过的投票这个核心规则,推荐两种数据模型设计,适配不同的业务规模:

基础版:User文档存储已投票ID(适合中小规模场景)

这种方案简单直接,维护成本低:

  • Poll集合(polls:每个文档对应一个投票,包含筛选条件和基本信息:
    {
      "id": "poll_123",
      "title": "你最喜欢的编程语言?",
      "options": ["Java", "Kotlin", "Swift"],
      "ageRange": [18, 40],
      "gender": {
        "male": true,
        "female": true,
        "other": true
      },
      "createdAt": 1716201600000, // 用时间戳类型方便排序
      "voteCount": 0 // 可选,用于快速统计总票数
    }
    
  • User集合(users:每个用户文档记录基本信息,以及已参与的投票ID数组:
    {
      "id": "user_456",
      "dateOfBirth": "1998-05-15",
      "gender": "male",
      "votedPolls": ["poll_123", "poll_789"] // 存储已投过的Poll ID
    }
    

进阶版:独立Vote集合(适合高并发/大量投票场景)

如果用户投票次数极多(比如上千次),User文档的votedPolls数组可能接近Firestore单文档1MB的限制,这时可以拆分出独立的投票记录集合:

  • Vote集合(votes:每个文档记录一次投票行为:
    {
      "userId": "user_456",
      "pollId": "poll_123",
      "votedAt": 1716205200000,
      "selectedOption": "Kotlin"
    }
    
    这种方式可以避免单个User文档过大,查询用户已投票列表时,只需查询votes集合中该用户的所有记录,提取pollId即可。

2. 解决“筛选用户未投票Poll”的问题

Firestore确实不支持!=或者whereNotArrayContains这类反向查询,结合你的现有查询逻辑,推荐以下方案:

方案一:客户端过滤(最常用,简单高效)

先执行你现有的条件查询(年龄+性别筛选),获取符合要求的Poll列表,然后在客户端过滤掉用户已投过的Poll:

// 先拿到用户已投票的ID列表
val userVotedPolls = user?.votedPolls ?: emptyList()

// 执行原有的Firestore查询
db.collection(POLLS)
    .whereArrayContains(Poll.AGE_RANGE, DateTimeUtils.calculateAge(user?.dateOfBirth))
    .whereEqualTo("gender.${user?.gender}", true)
    .orderBy(Poll.CREATED_AT)
    .limit(QUEUE_LIMIT)
    .get()
    .addOnSuccessListener { querySnapshot ->
        // 客户端过滤掉已投票的Poll
        val availablePolls = querySnapshot.documents.mapNotNull { it.toObject(Poll::class.java) }
            .filter { poll -> !userVotedPolls.contains(poll.id) }
        
        // 这里处理可用的投票列表
        updateVoteUI(availablePolls)
    }

这种方案在Poll数量不是特别大的时候完全够用,而且开发成本低。

方案二:分页补充查询(适合大规模Poll场景)

如果一次查询返回的Poll里大部分都是用户已投过的,导致可用数量不足,可以结合分页继续查询:

private fun fetchAvailablePolls(lastDocument: DocumentSnapshot? = null) {
    val userVotedPolls = user?.votedPolls ?: emptyList()
    var query = db.collection(POLLS)
        .whereArrayContains(Poll.AGE_RANGE, DateTimeUtils.calculateAge(user?.dateOfBirth))
        .whereEqualTo("gender.${user?.gender}", true)
        .orderBy(Poll.CREATED_AT)
        .limit(QUEUE_LIMIT)
    
    if (lastDocument != null) {
        query = query.startAfter(lastDocument)
    }
    
    query.get().addOnSuccessListener { snapshot ->
        val filtered = snapshot.documents.mapNotNull { it.toObject(Poll::class.java) }
            .filter { !userVotedPolls.contains(it.id) }
        
        if (filtered.size < QUEUE_LIMIT && !snapshot.isEmpty) {
            // 如果可用数量不够,继续分页查询
            fetchAvailablePolls(snapshot.last())
        } else {
            // 处理最终的可用列表
            updateVoteUI(filtered)
        }
    }
}

避坑提示:不要在Poll文档中存已投票用户列表

很多人会想在Poll文档里加votedUsers数组,但如果一个投票有几千甚至上万人参与,这个数组会迅速超过Firestore单文档的大小限制,导致后续写入失败,绝对要避免这种设计。

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

火山引擎 最新活动