import Foundation /// 服务端 shipment-sn 软绑/软解绑:本地 BLE 硬绑成功后由 SDK 内部调用,集成方无需手动调起。 final class ShipmentSnDeviceService { private let config: DuooomiBleConfig private let session: URLSession init(config: DuooomiBleConfig, session: URLSession = .shared) { self.config = config self.session = session } func bind( sn: String, bindUserId: String, completion: @escaping (Result) -> Void ) { post( path: "/api/auth/loomart/shipment-sn/device/bind", body: ["sn": sn, "bindUserId": bindUserId], completion: completion ) } func unbind( sn: String, bindUserId: String? = nil, completion: @escaping (Result) -> Void ) { var body: [String: String] = ["sn": sn] if let bindUserId = bindUserId, !bindUserId.isEmpty { body["bindUserId"] = bindUserId } post( path: "/api/auth/loomart/shipment-sn/device/unbind", body: body, completion: completion ) } private func post( path: String, body: [String: String], completion: @escaping (Result) -> Void ) { guard let url = URL(string: config.apiHost.absoluteString + path) else { completion(.failure(DuooomiBleError.softBindFailed("URL 拼装失败"))) return } var req = URLRequest(url: url) req.httpMethod = "POST" req.timeoutInterval = 10 req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue("application/json", forHTTPHeaderField: "Accept") req.setValue(config.apiKey, forHTTPHeaderField: "x-api-key") req.setValue(config.brand, forHTTPHeaderField: "x-owner") do { req.httpBody = try JSONEncoder().encode(body) } catch { completion(.failure(error)) return } session.dataTask(with: req) { data, resp, error in if let error = error { completion(.failure(DuooomiBleError.softBindFailed(error.localizedDescription))) return } guard let http = resp as? HTTPURLResponse else { completion(.failure(DuooomiBleError.softBindFailed("无响应"))) return } guard (200...299).contains(http.statusCode) else { let message = Self.parseErrorMessage(data: data) ?? "HTTP \(http.statusCode)" completion(.failure(DuooomiBleError.softBindFailed(message))) return } // 后端响应 `{ success, data }`;2xx 即视为成功,不强制解 data 字段。 completion(.success(())) }.resume() } /// 剥离 `API error: ` 前缀,必要时从 JSON 中抽 `message` 字段,与 RN 端 stripApiErrorPrefix 行为对齐。 private static func parseErrorMessage(data: Data?) -> String? { guard let data = data, let raw = String(data: data, encoding: .utf8) else { return nil } var stripped = raw if stripped.hasPrefix("API error:") { stripped = String(stripped.dropFirst("API error:".count)).trimmingCharacters(in: .whitespaces) } if stripped.hasPrefix("{"), let json = try? JSONSerialization.jsonObject(with: Data(stripped.utf8)) as? [String: Any] { if let msg = json["message"] as? String { return msg } if let err = json["error"] as? [String: Any], let msg = err["message"] as? String { return msg } } return stripped.isEmpty ? nil : stripped } }