如何避免预约按钮场景下Firestore写入竞态条件?
解决方案
1. 加载状态与按钮交互优化
不需要两个变量管理禁用状态,单个isLoading变量即可满足需求,点击按钮后直接将按钮替换为加载Spinner,既能阻止重复点击,也能给用户明确的操作反馈,修改后的ReservationButtonView示例:
struct ReservationButtonView: View { var workoutClass: WorkoutClass @ObservedObject var viewModel: WorkoutClassViewModel @EnvironmentObject var authViewModel: AuthViewModel var body: some View { if viewModel.isLoading { ProgressView() .font(.title) .tint(.white) } else if checkIsReserved(uid: authViewModel.user?.uid ?? "", reservedUsers: workoutClass.reservedUsers ?? []) { Button(action: { viewModel.isLoading = true viewModel.removeReservation( documentId: workoutClass.id!, reservedUserDetails: ["uid": authViewModel.user?.uid as Any, "photoURL": authViewModel.user?.photoURL?.absoluteString ?? "" as Any, "displayName": authViewModel.user?.displayName ?? "Bruin Fitness Member" as Any], uid: authViewModel.user?.uid ?? "" ) }){ Label( title: { Text("取消预约") .font(.title) }, icon: { Image(systemName: "person.badge.minus") .font(.title) } ) } } else { Button(action: { viewModel.isLoading = true viewModel.addReservation( documentId: workoutClass.id!, reservedUserDetails: ["uid": authViewModel.user?.uid as Any, "photoURL": authViewModel.user?.photoURL?.absoluteString ?? "" as Any, "displayName": authViewModel.user?.displayName ?? "Bruin Fitness Member" as Any], uid: authViewModel.user?.uid ?? "" ) }){ Label( title: { Text("预约") .font(.title) }, icon: { Image(systemName: "person.badge.plus") .font(.title) } ) } } } }
把原来的.disabled去掉,直接根据isLoading切换显示Spinner,交互更清晰。
2. 取消预约场景免事务实现方案
可以不用事务,直接使用Firestore带条件的更新操作即可保证数据一致性,核心逻辑是更新前先判断用户确实在预约列表中,条件不满足则更新直接失败,不会误改reservationCnt:
// 优化后的removeReservation方法,无需事务 func removeReservation(documentId: String, reservedUserDetails: [String: Any], uid: String){ let decrement = FieldValue.increment(-1) let removeUser = FieldValue.arrayRemove([reservedUserDetails]) let classReference = db.document("schedules/Redwood City/dates/\(self.stateDate.dbDateFormat)/classes/\(documentId)") // 带条件更新:只有用户uid在预约列表中时才执行更新 classReference.updateData([ "reservationCnt": decrement, "reservedUsers": removeUser ]) { error in defer { self.isLoading = false } if let error = error { // 条件不满足或网络错误时打印日志 print("取消预约失败:\(error.localizedDescription)") } } }
同理添加预约也可以用带条件的更新替代事务,判断用户不在预约列表中才执行增量和加人操作,性能比事务更好。
3. 现有逻辑风险修复
你当前的代码存在一个隐藏问题:arrayRemove是全对象匹配逻辑,如果后续用户修改了昵称、头像,存储在reservedUsers中的用户信息和当前传入的reservedUserDetails不一致,会导致删除失败。建议的优化方案:
- 方案一:
reservedUsers数组只存储用户uid,用户头像、昵称等信息从用户表实时查询,避免冗余数据同步问题 - 方案二:用户修改个人信息时,同步更新该用户所有预约课程中的用户信息
4. 最终流程优化建议
- 前端第一层防护:点击后立刻切加载状态,阻止重复触发
- 服务端第二层防护:用带条件的更新保证计数和预约列表的一致性,无需事务
- 状态同步:监听课程文档的实时快照,按钮的预约/取消状态直接和服务端返回的
reservedUsers绑定,避免本地状态和服务端不一致
内容的提问来源于stack exchange,提问作者Thomas Burke
相关产品推荐
相关产品推荐

