- DuooomiBleConfig.owner → brand(语义:设备品牌名),HTTP header x-owner 不变 - bind() 前置 verifyBrand:缺 deviceInfo 先 getDeviceInfo,与 config.brand 严格匹配,不一致主动断连并抛 .brandMismatch - README 抹除软绑/对账/UpgradeRecord 等内部实现细节,仅保留厂商面向的公开 API(功能代码完全保留) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
6.8 KiB
Swift
172 lines
6.8 KiB
Swift
import Foundation
|
||
|
||
/// 固件升级 RPC:检查升级 / 升级成功上报 / 升级失败上报。
|
||
/// 与 RN `FirmwareController.{checkUpgrade, reportUpgradeSuccessBySn, reportUpgradeFailureBySn}` 1:1 对齐。
|
||
final class FirmwareUpgradeService {
|
||
private let config: DuooomiBleConfig
|
||
private let session: URLSession
|
||
|
||
init(config: DuooomiBleConfig, session: URLSession = .shared) {
|
||
self.config = config
|
||
self.session = session
|
||
}
|
||
|
||
/// 由服务端判断是否需要升级。
|
||
func checkUpgrade(
|
||
sn: String,
|
||
currentVersion: String?,
|
||
completion: @escaping (Result<FirmwareUpgradeCheckResult, Error>) -> Void
|
||
) {
|
||
var body: [String: String] = ["sn": sn]
|
||
if let v = currentVersion, !v.isEmpty { body["currentVersion"] = v }
|
||
post(
|
||
path: "/api/auth/loomart/firmware/upgrade/check",
|
||
body: body
|
||
) { result in
|
||
switch result {
|
||
case .failure(let e): completion(.failure(e))
|
||
case .success(let data):
|
||
do {
|
||
let parsed = try JSONDecoder().decode(FirmwareUpgradeCheckResult.self, from: data)
|
||
completion(.success(parsed))
|
||
} catch {
|
||
completion(.failure(error))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func reportUpgradeSuccess(
|
||
sn: String,
|
||
firmwareId: String,
|
||
fromVersion: String,
|
||
bindUserId: String?,
|
||
completion: @escaping (Result<Void, Error>) -> Void
|
||
) {
|
||
var body: [String: String] = [
|
||
"sn": sn,
|
||
"firmwareId": firmwareId,
|
||
"fromVersion": fromVersion,
|
||
]
|
||
if let u = bindUserId, !u.isEmpty { body["bindUserId"] = u }
|
||
post(path: "/api/auth/loomart/firmware/upgrade/report-success", body: body) { result in
|
||
completion(result.map { _ in () })
|
||
}
|
||
}
|
||
|
||
func reportUpgradeFailure(
|
||
sn: String,
|
||
firmwareId: String,
|
||
fromVersion: String,
|
||
failureReason: String,
|
||
completion: @escaping (Result<Void, Error>) -> Void
|
||
) {
|
||
let body = [
|
||
"sn": sn,
|
||
"firmwareId": firmwareId,
|
||
"fromVersion": fromVersion,
|
||
"failureReason": failureReason,
|
||
]
|
||
post(path: "/api/auth/loomart/firmware/upgrade/report-failure", body: body) { result in
|
||
completion(result.map { _ in () })
|
||
}
|
||
}
|
||
|
||
// MARK: - HTTP
|
||
|
||
private func post(
|
||
path: String,
|
||
body: [String: String],
|
||
completion: @escaping (Result<Data, Error>) -> Void
|
||
) {
|
||
guard let url = URL(string: config.apiHost.absoluteString + path) else {
|
||
completion(.failure(DuooomiBleError.transferFailed("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(error))
|
||
return
|
||
}
|
||
guard let http = resp as? HTTPURLResponse else {
|
||
completion(.failure(DuooomiBleError.transferFailed("无响应")))
|
||
return
|
||
}
|
||
guard (200...299).contains(http.statusCode) else {
|
||
let msg = Self.parseErrorMessage(data: data) ?? "HTTP \(http.statusCode)"
|
||
completion(.failure(DuooomiBleError.transferFailed(msg)))
|
||
return
|
||
}
|
||
// 后端响应 `{ success, data }` 包装:unwrap 后透传 data 字段
|
||
if let data = data, let unwrapped = Self.unwrapData(from: data) {
|
||
completion(.success(unwrapped))
|
||
} else {
|
||
completion(.success(data ?? Data()))
|
||
}
|
||
}.resume()
|
||
}
|
||
|
||
private static func unwrapData(from raw: Data) -> Data? {
|
||
guard let json = try? JSONSerialization.jsonObject(with: raw) as? [String: Any],
|
||
json["success"] as? Bool == true,
|
||
let inner = json["data"] else {
|
||
return nil
|
||
}
|
||
return try? JSONSerialization.data(withJSONObject: inner)
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
// MARK: - Response Model
|
||
|
||
/// 服务端 `/firmware/upgrade/check` 响应。
|
||
/// **判断能否升级看 `targetFirmware` 是否非空**——SDK 不再暴露 `upgradeAvailable` / `reason`
|
||
/// 这种 client 端"判断"语义,回归"拉取最新固件信息"单一职责。
|
||
public struct FirmwareUpgradeCheckResult: Codable, Equatable {
|
||
public let currentSystemVersion: String?
|
||
public let targetFirmware: TargetFirmware?
|
||
|
||
public struct TargetFirmware: Codable, Equatable {
|
||
/// 固件 ID。`UpgradeRecord.firmwareId` 来源、对账上报必带。
|
||
public let id: String
|
||
/// 固件版本字符串(如 "1.2.3")。UI 展示「最新版本」+ 落 `UpgradeRecord.targetVersion`。
|
||
public let version: String
|
||
/// 固件包下载 URL。烧录入参 `upgradeFirmware(fileUrl:)`。
|
||
public let fileUrl: String?
|
||
/// 固件包字节数(字符串编码,避免 Int64 溢出)。可用于 UI 显示「需要下载 xx MB」。
|
||
public let fileSize: String?
|
||
/// 固件包 MD5。集成方有需要可校验下载内容;SDK 不做校验。
|
||
public let fileMd5: String?
|
||
/// 强制升级标记。`true` 表示该版本必须升级(典型场景:安全补丁 / 协议破坏性变更),
|
||
/// 集成方应据此弹不可关闭的升级弹窗、阻断设备其他操作;`false` / `nil` 由用户自行决定。
|
||
/// 服务端可能不返回此字段(旧固件版本),用 Optional 做 Codable 兜底。
|
||
public let forceUpgrade: Bool?
|
||
}
|
||
}
|