import Foundation /// OTA 升级记录持久化存储。与 RN `bleStore.upgradeRecords` 1:1 对齐: /// - 复合主键 `(sn, firmwareId)` 的 upsert /// - 30 天 TTL:过期但保留为审计轨迹(标记 `reported=true, outcome=nil`) /// - sn 级 in-flight 互斥锁,防止并发重复上报 final class UpgradeRecordStore { private static let storageKey = "duooomi.firmware_upgrade_records" /// 30 天毫秒 static let ttlMillis: Int64 = 30 * 24 * 60 * 60 * 1000 private let defaults: UserDefaults private let queue = DispatchQueue(label: "duooomi.upgradeRecord", attributes: .concurrent) private var records: [UpgradeRecord] private var inFlight: Set = [] /// mutation 后主线程回调当前快照(SDK 用它通过 delegate 通知集成方)。 var onChange: (([UpgradeRecord]) -> Void)? init(defaults: UserDefaults = .standard) { self.defaults = defaults if let data = defaults.data(forKey: Self.storageKey), let decoded = try? JSONDecoder().decode([UpgradeRecord].self, from: data) { self.records = decoded } else { self.records = [] } } // MARK: - Read var all: [UpgradeRecord] { queue.sync { records } } /// 取该 sn 下所有未上报的记录,按 triggeredAt 升序(旧→新)。 func pending(sn: String) -> [UpgradeRecord] { queue.sync { records.filter { $0.sn == sn && !$0.reported } .sorted { $0.triggeredAt < $1.triggeredAt } } } // MARK: - Mutation /// 复合主键 `(sn, firmwareId)`:已存在 → 覆盖并复位 reported/outcome;不存在 → 追加。 func upsert(_ record: UpgradeRecord) { guard !record.sn.isEmpty, !record.firmwareId.isEmpty else { return } queue.async(flags: .barrier) { if let idx = self.records.firstIndex(where: { $0.sn == record.sn && $0.firmwareId == record.firmwareId }) { self.records[idx] = record } else { self.records.append(record) } self.persistLocked() } } /// 标记某条记录已上报(线程安全)。 func markReported(sn: String, firmwareId: String, outcome: UpgradeOutcome?) { queue.async(flags: .barrier) { guard let idx = self.records.firstIndex(where: { $0.sn == sn && $0.firmwareId == firmwareId }) else { return } let original = self.records[idx] self.records[idx] = UpgradeRecord( sn: original.sn, firmwareId: original.firmwareId, fromVersion: original.fromVersion, targetVersion: original.targetVersion, triggeredAt: original.triggeredAt, reported: true, reportedAt: Self.nowMillis(), outcome: outcome ) self.persistLocked() } } // MARK: - In-flight Mutex (sn 级) /// 尝试占用 sn 互斥锁。返回 false 表示已有进行中的上报,调用方应直接 return。 func tryAcquire(sn: String) -> Bool { queue.sync(flags: .barrier) { () -> Bool in if inFlight.contains(sn) { return false } inFlight.insert(sn) return true } } func release(sn: String) { queue.async(flags: .barrier) { self.inFlight.remove(sn) } } // MARK: - Helpers static func nowMillis() -> Int64 { Int64(Date().timeIntervalSince1970 * 1000) } /// 持久化 + 主线程回调 onChange。**必须在 barrier 内调用**。 private func persistLocked() { if let data = try? JSONEncoder().encode(records) { defaults.set(data, forKey: Self.storageKey) } if let onChange = onChange { let snapshot = records DispatchQueue.main.async { onChange(snapshot) } } } }