import Foundation /// 提供 ISO 8601 格式的北京时间字符串(如 `2026-05-07T16:58:00+08:00`),用于 bind / setPlayMode payload 的 `time` 字段。 /// /// 三级 fallback: /// 1. 苏宁公开时间 API 的 body(`sysTime2` 或 `sysTime1`) /// 2. 苏宁 response.headers["Date"](HTTP RFC 1123) /// 3. 本地 `Date()` + 8h /// /// 5 秒内单飞复用,避免 bind/setPlayMode 连发时重复请求。 final class BeijingTimeProvider { private static let suningURL = URL(string: "https://quan.suning.com/getSysTime.do")! private static let cacheTTL: TimeInterval = 5 private static let fetchTimeout: TimeInterval = 3 private static let beijingOffsetSeconds: TimeInterval = 8 * 3600 private let session: URLSession private let queue = DispatchQueue(label: "duooomi.ble.beijingTime") private var cached: (value: String, at: Date)? private var inflight: [(String) -> Void] = [] private var fetching = false init(session: URLSession = .shared) { self.session = session } /// 异步获取北京时间字符串。回调一定在主队列触发。 func fetch(completion: @escaping (String) -> Void) { queue.async { [weak self] in guard let self = self else { return } if let cached = self.cached, Date().timeIntervalSince(cached.at) < Self.cacheTTL { self.dispatch(cached.value, to: completion) return } self.inflight.append(completion) if self.fetching { return } self.fetching = true self.performFetch { value in self.queue.async { self.cached = (value, Date()) let pending = self.inflight self.inflight = [] self.fetching = false pending.forEach { self.dispatch(value, to: $0) } } } } } private func dispatch(_ value: String, to completion: @escaping (String) -> Void) { DispatchQueue.main.async { completion(value) } } private func performFetch(_ done: @escaping (String) -> Void) { var req = URLRequest(url: Self.suningURL) req.timeoutInterval = Self.fetchTimeout req.httpMethod = "GET" session.dataTask(with: req) { [weak self] data, resp, _ in guard let self = self else { return } if let data = data, let body = try? JSONDecoder().decode(SuningTimeBody.self, from: data), let value = self.formatSuningBody(body) { done(value) return } if let http = resp as? HTTPURLResponse, let dateHeader = http.headerValue(caseInsensitive: "Date"), let value = self.formatHTTPDateHeader(dateHeader) { done(value) return } done(self.formatLocal()) }.resume() } private func formatSuningBody(_ body: SuningTimeBody) -> String? { if let s = body.sysTime2, s.range(of: #"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$"#, options: .regularExpression) != nil { return s.replacingOccurrences(of: " ", with: "T") + "+08:00" } if let s = body.sysTime1, s.count == 14, Int(s) != nil { let y = s.prefix(4) let mo = s.dropFirst(4).prefix(2) let d = s.dropFirst(6).prefix(2) let h = s.dropFirst(8).prefix(2) let mi = s.dropFirst(10).prefix(2) let se = s.dropFirst(12).prefix(2) return "\(y)-\(mo)-\(d)T\(h):\(mi):\(se)+08:00" } return nil } private func formatHTTPDateHeader(_ raw: String) -> String? { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone(identifier: "GMT") formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" guard let date = formatter.date(from: raw) else { return nil } return Self.formatBeijing(date) } private func formatLocal() -> String { Self.formatBeijing(Date()) } private static func formatBeijing(_ date: Date) -> String { var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(secondsFromGMT: Int(beijingOffsetSeconds))! let c = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) return String( format: "%04d-%02d-%02dT%02d:%02d:%02d+08:00", c.year ?? 1970, c.month ?? 1, c.day ?? 1, c.hour ?? 0, c.minute ?? 0, c.second ?? 0 ) } } private struct SuningTimeBody: Decodable { let sysTime1: String? let sysTime2: String? }