如何结合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
相关产品推荐
相关产品推荐

