128 lines
4.7 KiB
Swift
128 lines
4.7 KiB
Swift
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?
|
||
}
|