You need to enable JavaScript to run this app. 如何结合Firebase Auth与Firestore实现用户专属的餐计划收藏功能?
优惠活动
大模型
产品
解决方案
定价
更多

如何结合Firebase Auth与Firestore实现用户专属的餐计划收藏功能?

个人专属餐计划星标实现方案

核心设计调整思路

原来的全局isStarred字段设计本身不符合多用户场景,这个字段不属于公共餐计划的属性,属于每个用户的个人偏好,因此需要从公共餐计划集合中完全移除该字段,换成独立存储的用户收藏关系。

两种存储方案对比

  • 方案1:在users/{userID}用户文档里新增starredMealPlanIDs: [String]数组,存储当前用户收藏的所有餐计划docID
    • 适合轻量场景,但存在单文档大小限制、并发写入易冲突、扩展能力弱的问题
  • 方案推荐:在每个用户文档下新建独立的favorites子集合,路径为users/{userID}/favorites/{mealPlanDocID}
    • 无大小限制,查询灵活,易扩展自定义收藏字段,安全规则易管控

具体代码改造步骤

1. 基础数据结构调整

  • 公共mealPlans集合:删除所有文档里的全局isStarred字段,只保留餐计划本身的公共属性
  • MealPlan结构体保留isStarred字段,但这个值不再从公共数据库读取,而是根据当前用户的收藏列表动态填充
  • User结构体无需额外大改,因为用户的身份ID直接从Firebase Auth获取,不需要额外冗余存储

2. 核心方法改造

改造星标状态更新方法

原来的方法是修改全局餐计划的isStarred字段,现在改成写入/删除当前用户的收藏子集合文档:

func updateStarredStatus(_ mealPlanDocID: String, _ isStarred: Bool) {
    // 获取当前登录用户的唯一ID(Firebase Auth提供)
    guard let currentUserID = Auth.auth().currentUser?.uid else { return }
    let db = Firestore.firestore()
    // 定位到当前用户收藏列表下,对应餐计划的文档
    let favoriteDocRef = db
        .collection("users")
        .document(currentUserID)
        .collection("favorites")
        .document(mealPlanDocID)
    
    if isStarred {
        // 标星:创建收藏文档,存储收藏时间等元数据
        favoriteDocRef.setData([
            "starredAt": Timestamp(date: Date())
        ])
    } else {
        // 取消标星:删除对应收藏文档
        favoriteDocRef.delete()
    }
}

改造餐计划查询方法

查询时同时监听公共餐计划集合和当前用户的收藏子集合,动态匹配每个餐计划的星标状态,支持星标筛选:

func getMealPlans(_ starredOnly: Bool = false) {
    // 移除旧的数据库监听
    listener?.remove()
    guard let currentUserID = Auth.auth().currentUser?.uid else {
        self.delegate?.mealPlansRetrieved(mealPlans: [])
        return
    }
    let db = Firestore.firestore()
    
    // 临时缓存两个数据源的快照
    var mealPlanDocs: [QueryDocumentSnapshot]?
    var userFavoriteIDSet: Set<String> = []
    
    // 组装餐计划列表的通用逻辑
    func assembleMealPlanList() {
        guard let docs = mealPlanDocs else { return }
        var mealPlans = [MealPlan]()
        
        for doc in docs {
            let mealPlanID = doc.documentID
            // 星标筛选模式下,跳过用户未收藏的餐计划
            if starredOnly && !userFavoriteIDSet.contains(mealPlanID) {
                continue
            }
            // 解析餐计划,星标状态从用户收藏集合同步
            let mealPlan = MealPlan(
                docID: mealPlanID,
                title: doc["title"] as! String,
                recipeSource: doc["recipeSource"] as? String,
                estimatedTime: doc["estimatedTime"] as? String,
                coverImageView: doc["coverImageView"] as? String,
                equipment: doc["equipment"] as? [String],
                instructions: doc["instructions"] as? [String],
                isStarred: userFavoriteIDSet.contains(mealPlanID)
            )
            mealPlans.append(mealPlan)
        }
        
        DispatchQueue.main.async {
            self.delegate?.mealPlansRetrieved(mealPlans: mealPlans)
        }
    }
    
    // 监听当前用户的收藏子集合,实时同步星标列表
    db.collection("users")
        .document(currentUserID)
        .collection("favorites")
        .addSnapshotListener { [weak self] snapshot, error in
            guard let snapshot = snapshot else { return }
            // 把所有收藏的餐计划ID存为集合,做快速匹配
            userFavoriteIDSet = Set(snapshot.documents.map { $0.documentID })
            // 如果已经拿到了全量餐计划数据,就重新组装列表
            if mealPlanDocs != nil {
                assembleMealPlanList()
            }
        }
    
    // 监听公共餐计划集合
    listener = db.collection("mealPlans")
        .addSnapshotListener { snapshot, error in
            guard let snapshot = snapshot, error == nil else { return }
            mealPlanDocs = snapshot.documents
            // 如果已经拿到了用户的收藏列表,就重新组装列表
            assembleMealPlanList()
        }
}

3. 安全规则配置

配置Firestore安全规则,保证用户只能操作自己的收藏数据,公共餐计划仅可读:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 公共餐计划所有人可读,仅管理员可写
    match /mealPlans/{docID} {
      allow read: if true;
      allow write: if request.auth.token.admin == true;
    }
    // 用户收藏列表仅本人可读写
    match /users/{userID}/favorites/{mealPlanID} {
      allow read, write: if request.auth.uid == userID;
    }
  }
}

注意事项

  • 原代码中在餐计划文档里冗余存docID字段完全没必要,文档本身的documentID就是唯一标识,直接使用即可,减少冗余存储
  • 单个餐计划详情/列表项上的星标按钮触发时,直接调用改造后的updateStarredStatus方法即可,列表的实时状态会通过之前配置的监听自动更新,不需要手动刷新
  • 如果后续需要给收藏加自定义标签、收藏备注之类的扩展,直接在favorites子集合的文档里加字段即可,不需要修改核心结构
  • 最初设想的把收藏ID存在User文档数组里的方案,在收藏量不大的场景下也能跑通,但长期可维护性远低于独立子集合的方案

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.31 04:45:09