Firestore批量更新异常:仅最后一个库存文档更新生效
背景
进入CashRegisterViewController后,为购物车中每个商品获取对应的idParent和idDocument,对库存文档执行数量递减更新,或对测试品文档更新字段。
问题现象
购物车中有多个目录库存商品时,仅最后一个文档的数量被递减,之前的更新被覆盖;多个测试品的更新则正常生效。
观察结果
更新库存文档时,控制台显示数量递减计算正确,但Firestore仅应用最后一个更新;测试品更新工作正常。
疑问
- 为何库存更新仅对最后一个文档生效?
- 如何修复以确保所有库存更新正确应用?
相关代码
import UIKit import FirebaseFirestore class CashRegisterViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{ @IBOutlet weak var tableView: UITableView! @IBOutlet weak var clientNameLabel: UILabel! @IBOutlet weak var clientPointsTextField: DesignableTextField! var client: Client? var idParent: String? var idDocument: String? var category: String? var selectedSegmentIndex: Int? override func viewDidLoad() { super.viewDidLoad() // Configurez la tableView tableView.dataSource = self tableView.delegate = self if let client = client { clientNameLabel.text = "\(client.nom) \(client.prénom)" clientPointsTextField.text = "\(client.points)" } else { clientNameLabel.text = "Acheteur non client" clientPointsTextField.text = "0" clientNameLabel.isHidden = true } // Charger les articles du panier depuis ShoppingCart tableView.reloadData() } private func removeItem(at index: Int) { ShoppingCart.shared.removeItem(at: index) tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) } private func duplicateItem(at index: Int) { let itemToDuplicate = ShoppingCart.shared.items[index] ShoppingCart.shared.addItem(itemToDuplicate) tableView.insertRows(at: [IndexPath(row: ShoppingCart.shared.items.count - 1, section: 0)], with: .automatic) } @IBAction func paymentTypeAction(_ sender: Any) { let paymentAlert = UIAlertController(title: "", message: "Choisissez le type de paiement", preferredStyle: .alert) let card = UIAlertAction(title: "Carte", style: .default, handler: nil) let cash = UIAlertAction(title: "Espèce", style: .default, handler: nil) let payPal = UIAlertAction(title: "PayPal", style: .default, handler: nil) paymentAlert.addAction(card) paymentAlert.addAction(cash) paymentAlert.addAction(payPal) self.present(paymentAlert, animated: true, completion: nil) } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return ShoppingCart.shared.items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "CashCell", for: indexPath) as? CashTableViewCell else { fatalError("La cellule de type CashTableViewCell n'a pas pu être défilée.") } let index = indexPath.row let item = ShoppingCart.shared.items[index] cell.ReferenceLabel.text = item.reference cell.typeLabel.text = item.type cell.priceLabel.text = String(format: "%.2f", item.price) //cell.addButton.isHidden = false cell.addButton.isHidden = false cell.deleteButton.isHidden = false cell.onDeleteButtonTapped = { [weak self] in self?.removeItem(at: index) } cell.onAddButtonTapped = { [weak self] in self?.duplicateItem(at: index) } return cell } //Cadeau @IBAction func cadeauButtonTapped(_ sender: UIButton) { showGiftOptionsAlert() } private func showGiftOptionsAlert() { let alert = UIAlertController(title: "Choisissez un cadeau", message: "", preferredStyle: .alert) let encensAction = UIAlertAction(title: "encens", style: .default) { _ in self.navigateToHouseCatalogue(segmentIndex: 3, applyPriceFilter: true) // Index pour Encens } let desodorisantAction = UIAlertAction(title: "désodorisant", style: .default) { _ in self.navigateToHouseCatalogue(segmentIndex: 0) // Index pour Désodorisant } alert.addAction(encensAction) alert.addAction(desodorisantAction) self.present(alert, animated: true, completion: nil) } private func navigateToHouseCatalogue(segmentIndex: Int, applyPriceFilter: Bool = false) { let storyboard = UIStoryboard(name: "Main", bundle: nil) if let houseCatalogueVC = storyboard.instantiateViewController(withIdentifier: "HouseCatalogueViewController") as? HouseCatalogueViewController { houseCatalogueVC.selectedSegmentIndex = segmentIndex houseCatalogueVC.applyPriceFilterForEncens = applyPriceFilter houseCatalogueVC.isGiftNavigation = true navigationController?.pushViewController(houseCatalogueVC, animated: true) } } private func handleGiftSelection(_ giftType: String) { print("Cadeau sélectionné : \(giftType)") let segmentIndex: Int switch giftType { case "Encens": segmentIndex = 3 // Index pour le segment Encens case "Désodorisant": segmentIndex = 0 // Index pour le segment Désodorisant default: return } navigateToHouseCatalogue(segmentIndex: segmentIndex) } @IBAction func clearCartButtonTapped(_ sender: Any) { ShoppingCart.shared.clearCart() tableView.reloadData() } @IBAction func saveButtonTapped(_ sender: Any) { let db = Firestore.firestore() let batch = db.batch() // Crée un batch pour les mises à jour groupées let dispatchGroup = DispatchGroup() // Utilisé pour synchroniser les tâches asynchrones // Pour les mises à jour des testeurs for item in ShoppingCart.shared.items { let reference = item.reference let type = item.type switch item.origin { case .tester: dispatchGroup.enter() // Entrée dans le groupe pour chaque tâche asynchrone // Filtre les documents dans la collection "testeurs" par référence let initialQuery = db.collection("testeurs") .whereField("reference", isEqualTo: reference) initialQuery.getDocuments { (querySnapshot, error) in if let error = error { print("Erreur lors de la récupération des documents du testeur: \(error.localizedDescription)") dispatchGroup.leave() // Sortie du groupe en cas d'erreur return } guard let documents = querySnapshot?.documents else { print("Aucun document trouvé pour la référence \(reference).") dispatchGroup.leave() // Sortie du groupe si aucun document trouvé return } // Filtrage par type et tri par date en mémoire let filteredAndSortedDocuments = documents.filter { document in let data = document.data() let documentType = data["type"] as? String let documentDate = data["date"] as? Timestamp return documentType == type && documentDate != nil }.sorted { ($0.data()["date"] as? Timestamp)?.seconds ?? 0 > ($1.data()["date"] as? Timestamp)?.seconds ?? 0 } if let mostRecentDocument = filteredAndSortedDocuments.first { // Met à jour le champ "state" en "vendu" dans le batch pour le document le plus récent batch.updateData(["state": "vendu"], forDocument: mostRecentDocument.reference) } dispatchGroup.leave() // Sortie du groupe après la mise à jour } case .stock: guard let idParent = self.idParent, let idDocument = self.idDocument, let category = self.category else { print("Variables nécessaires non définies pour le stock") continue // Passer à l'article suivant } dispatchGroup.enter() // Entrée dans le groupe pour chaque tâche asynchrone let documentRef = db.collection("stocks").document(idParent).collection("produits").document(idDocument) documentRef.getDocument { [weak self] (documentSnapshot, error) in if let error = error { print("Erreur lors de la récupération du document du produit: \(error.localizedDescription)") dispatchGroup.leave() // Sortie du groupe en cas d'erreur return } guard let productData = documentSnapshot?.data(), let currentQuantity = productData["quantité"] as? Int else { print("Quantité non trouvée dans le document ou n'est pas un entier") dispatchGroup.leave() // Sortie du groupe si quantité non trouvée return } if currentQuantity > 0 { let newQuantity = currentQuantity - 1 batch.updateData(["quantité": newQuantity], forDocument: documentRef) print("Quantité mise à jour pour le produit: \(documentRef), nouvelle quantité: \(newQuantity)") } else { print("Quantité insuffisante pour décrémenter.") } dispatchGroup.leave() // Sortie du groupe après la mise à jour } } } // Appeler batch.commit après que toutes les mises à jour soient terminées dispatchGroup.notify(queue: .main) { print("Commit du batch de mises à jour") batch.commit { (error) in if let error = error { print("Erreur lors de la mise à jour des documents: \(error.localizedDescription)") } else { print("Documents mis à jour avec succès.") } } // Nettoyer le panier après l'enregistrement ShoppingCart.shared.clearCart() self.tableView.reloadData() } } }
问题根源
你代码中的idParent、idDocument是全局变量,遍历购物车商品时,所有库存类型的商品都会使用这两个变量的最后一次赋值结果,导致所有库存更新都指向同一个文档,最终只有最后一次更新生效。而测试品更新是通过商品自身的reference和type查询文档,每个商品对应独立逻辑,因此没有问题。
修复步骤
给购物车Item模型添加库存ID属性
修改购物车商品模型(如ShoppingCartItem),让每个商品携带自己对应的库存文档ID:struct ShoppingCartItem { // 原有属性 let reference: String let type: String let price: Double let origin: ItemOrigin // .tester或.stock // 添加库存关联属性 let idParent: String? let idDocument: String? let category: String? }在商品加入购物车时,将对应的库存ID赋值给这些属性。
修改批量更新逻辑,使用商品自身的ID
在saveButtonTapped方法的.stock分支中,从当前item获取库存ID,替代全局变量:case .stock: guard let idParent = item.idParent, let idDocument = item.idDocument, let category = item.category else { print("当前商品缺少库存相关ID信息") continue } dispatchGroup.enter() let documentRef = db.collection("stocks").document(idParent).collection("produits").document(idDocument) documentRef.getDocument { [weak self] (documentSnapshot, error) in // 原有更新逻辑保持不变 dispatchGroup.leave() }移除全局库存ID变量
既然每个商品都已携带库存ID,全局的idParent、idDocument、category变量可以删除,避免混淆。
额外优化建议
使用Firestore的FieldValue.increment替代先查询再更新的逻辑,既能避免并发更新冲突,又能简化代码:
// 替换原有查询后更新的代码 let documentRef = db.collection("stocks").document(idParent).collection("produits").document(idDocument) batch.updateData(["quantité": FieldValue.increment(Int64(-1))], forDocument: documentRef)
注意:确保quantité字段为整数类型,且使用Int64与Firestore的增量API兼容。
内容的提问来源于stack exchange,提问作者Hind Khayat

