Swift下Firestore数据结构化实现客户关联数据存取
Firestore 数据结构设计方案
针对客户、通话记录、关联文档三类数据,采用根集合+关联外键的设计,避免深层嵌套带来的单文档大小限制、查询分页不便等问题,结构如下:
- 根集合1:
customers,存储客户基础信息,每个文档ID为客户唯一标识 - 根集合2:
call_records,存储通话记录,每个文档新增customerId字段,存储关联客户的文档ID - 根集合3:
customer_documents,存储客户文档元数据,每个文档新增customerId字段,存储关联客户的文档ID,同时存文件下载URL、文件名、上传时间等字段
不推荐把通话记录、文档直接嵌套在客户文档下:Firestore单文档有1MB大小上限,后续做历史记录分页、跨客户数据统计都会受限制。
Swift 侧代码改造实现
第一步:修正数据模型,新增关联字段
注意修正原代码中CodingKeys的拼写错误(原代码写为CodingKyes会导致Codable解析失败)。
客户模型
import FirebaseFirestoreSwift import Firebase struct Customer: Identifiable, Codable { @DocumentID var id: String? var fullName: String var phoneNum: String var email: String var address: String var profession: String var age: String var dateOfBirth: String enum CodingKeys: String, CodingKey { case id case fullName case phoneNum case email case address case profession case age case dateOfBirth } }
通话记录模型
struct CallRecord: Identifiable, Codable { @DocumentID var id: String? // 新增客户关联字段 var customerId: String var title: String var description: String var callDate: String enum CodingKeys: String, CodingKey { case id case customerId case title case description case callDate } }
客户文档模型
struct CustomerDocument: Identifiable, Codable { @DocumentID var id: String? // 新增客户关联字段 var customerId: String var fileName: String var downloadURL: String var uploadTime: Timestamp enum CodingKeys: String, CodingKey { case id case customerId case fileName case downloadURL case uploadTime } }
第二步:关联数据写入实现
通话记录写入
跳转至通话记录添加页时,把当前选中的客户ID传入,写入时带上关联字段即可:
// 传入当前选中的客户实例 selectedCustomer: Customer let newCall = CallRecord( customerId: selectedCustomer.id!, title: titleTextField.text ?? "", description: descTextView.text ?? "", callDate: DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .short) ) do { try db.collection("call_records").addDocument(from: newCall) } catch { print("通话记录写入失败: \(error.localizedDescription)") }
文件上传+文档元数据写入
修正原代码中未等下载URL返回就写库、Storage路径用客户名易重名覆盖的问题,上传时按客户ID做路径隔离,拿到URL后写入关联记录:
private func uploadCustomerFile(fileURL: URL, customer: Customer) { let fileName = fileURL.lastPathComponent // Storage路径用客户唯一ID分层,避免重名覆盖 let storageRef = storage.reference().child("customer_files/\(customer.id!)/\(fileName)") do { let fileData = try Data(contentsOf: fileURL) _ = storageRef.putData(fileData, metadata: nil) { [weak self] metadata, error in guard error == nil else { print("文件上传失败: \(error!.localizedDescription)") return } // 拿到可访问的下载地址后,再写入Firestore关联记录 storageRef.downloadURL { url, error in guard let downloadURL = url else { return } let newDoc = CustomerDocument( customerId: customer.id!, fileName: fileName, downloadURL: downloadURL.absoluteString, uploadTime: Timestamp(date: Date()) ) do { try self?.db.collection("customer_documents").addDocument(from: newDoc) } catch { print("文档记录写入失败: \(error.localizedDescription)") } } } } catch { print("本地文件读取失败: \(error.localizedDescription)") } }
第三步:关联数据查询实现
查询指定客户的关联数据时,直接用whereField按customerId过滤即可,支持实时监听,不需要手动做文档引用的嵌套查询:
// 加载指定客户的所有通话记录 func loadCallRecords(for customer: Customer, completion: @escaping ([CallRecord]) -> Void) { db.collection("call_records") .whereField("customerId", isEqualTo: customer.id!) .order(by: "callDate", descending: true) .addSnapshotListener { snapshot, error in guard let documents = snapshot?.documents else { print("通话记录查询失败: \(error?.localizedDescription ?? "")") completion([]) return } // 直接用Codable解码,不需要手动逐字段解析 let records = documents.compactMap { try? $0.data(as: CallRecord.self) } completion(records) } } // 加载指定客户的所有关联文档 func loadCustomerDocuments(for customer: Customer, completion: @escaping ([CustomerDocument]) -> Void) { db.collection("customer_documents") .whereField("customerId", isEqualTo: customer.id!) .order(by: "uploadTime", descending: true) .addSnapshotListener { snapshot, error in guard let documents = snapshot?.documents else { print("文档查询失败: \(error?.localizedDescription ?? "")") completion([]) return } let docs = documents.compactMap { try? $0.data(as: CustomerDocument.self) } completion(docs) } }
原代码中手动从
document.data()逐字段解析的逻辑冗余度很高,直接用FirestoreSwift提供的data(as:)方法做Codable解码即可,和你参考的Codable教程逻辑完全一致。
内容的提问来源于stack exchange,提问作者Mariglen Meta
相关产品推荐
相关产品推荐

