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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 11:48:16