import Foundation /// 主控端 HTTP 客户端:对接安全信令服务器的账号体系与自助接口。 /// /// - login(username,password) → accessToken + refreshToken(一次性,ses_ 前缀); /// - refresh(refreshToken) → 新 accessToken(服务端可能轮换 refreshToken); /// - verify() → 校验 accessToken 是否仍有效; /// - bindings() → 本机可连接的被控端列表(仅已绑定设备); /// - turnCredentials() → TURN 短期凭证(服务端开启时返回 iceServers)。 struct ApiClient { /// 服务端 HTTP 基址(与信令同源)。部署时通过 Build Setting / Info.plist 注入。 static var baseURL: String { // 优先读取 Info.plist 中的 APIBase,缺省回退到信号服务器同源 HTTPS。 if let v = Bundle.main.object(forInfoDictionaryKey: "APIBase") as? String, !v.isEmpty { return v } return "https://www.ttstd.com" } /// 登录:成功返回 accessToken / refreshToken / expireAt。 static func login(username: String, password: String) async throws -> [String: Any] { let body: [String: Any] = ["username": username, "password": password] return try await post("/api/auth/login", body: body) } /// 刷新令牌:body 需带 refreshToken;成功返回新的 accessToken(可能轮换 refreshToken)。 static func refresh(refreshToken: String) async throws -> [String: Any] { let body: [String: Any] = ["refreshToken": refreshToken] return try await post("/api/auth/refresh", body: body) } /// 校验当前 accessToken 是否有效。 static func verify(accessToken: String) async throws -> [String: Any] { return try await get("/api/client/verify", accessToken: accessToken) } /// 拉取本机可连接的被控端绑定列表(仅已绑定设备)。 static func bindings(accessToken: String) async throws -> [String: Any] { return try await get("/api/client/bindings", accessToken: accessToken) } /// 拉取 TURN 短期凭证(服务端未开启时抛错,调用方忽略)。 static func turnCredentials(accessToken: String) async throws -> [String: Any] { return try await get("/api/client/turn-credentials", accessToken: accessToken) } // MARK: - Private private static func post(_ path: String, body: [String: Any]) async throws -> [String: Any] { var req = URLRequest(url: URL(string: baseURL + path)!) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try JSONSerialization.data(withJSONObject: body) return try await perform(req) } private static func get(_ path: String, accessToken: String) async throws -> [String: Any] { var req = URLRequest(url: URL(string: baseURL + path)!) req.httpMethod = "GET" req.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") return try await perform(req) } private static func perform(_ request: URLRequest) async throws -> [String: Any] { let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse else { throw ApiError(message: "无效响应") } if http.statusCode == 401 { throw ApiError(code: "UNAUTHORIZED", message: "令牌失效", httpCode: 401) } guard (200..<300).contains(http.statusCode) else { let msg = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?["error"] as? String ?? "HTTP \(http.statusCode)" throw ApiError(message: msg) } return (try? JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:] } } struct ApiError: Error { let code: String let message: String let httpCode: Int init(code: String = "ERROR", message: String, httpCode: Int = -1) { self.code = code self.message = message self.httpCode = httpCode } var localizedDescription: String { message } }