You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Firestore批量更新异常:仅最后一个库存文档更新生效

Firestore批量更新库存文档仅最后一个生效的问题

背景

进入CashRegisterViewController后,为购物车中每个商品获取对应的idParent和idDocument,对库存文档执行数量递减更新,或对测试品文档更新字段。

问题现象

购物车中有多个目录库存商品时,仅最后一个文档的数量被递减,之前的更新被覆盖;多个测试品的更新则正常生效。

观察结果

更新库存文档时,控制台显示数量递减计算正确,但Firestore仅应用最后一个更新;测试品更新工作正常。

疑问

  1. 为何库存更新仅对最后一个文档生效?
  2. 如何修复以确保所有库存更新正确应用?

相关代码

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查询文档,每个商品对应独立逻辑,因此没有问题。

修复步骤

  1. 给购物车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赋值给这些属性。

  2. 修改批量更新逻辑,使用商品自身的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()
        }
    
  3. 移除全局库存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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 20:39:51