// // UploadService.swift // QuickLocation // import Foundation import RxSwift import SwiftyJSON import CommonCrypto enum UploadFileKind { case jpeg case mp4 case wav var fileExtension: String { switch self { case .jpeg: return "jpg" case .mp4: return "mp4" case .wav: return "wav" } } var mimeType: String { switch self { case .jpeg: return "image/jpeg" case .mp4: return "video/mp4" case .wav: return "audio/wav" } } } struct UploadService { private static let boundary = "YLQH" private static let jiaMiKey = "857d69d374694c1de46486d3bdaaac43" static func upload(_ data: Data, kind: UploadFileKind, scene: String? = nil) -> Observable { Observable.create { observer in do { let request = try makeRequest(fileData: data, kind: kind, scene: scene) let session = URLSession(configuration: .default) let task = session.uploadTask(with: request.request, from: request.body) { data, response, error in if let error { DispatchQueue.main.async { observer.onError(error) } return } guard let data else { DispatchQueue.main.async { observer.onError(makeError("上传失败")) } return } let decrypted = ApiManager.decryptIfNeeded(data) ?? data let json = JSON(decrypted) let code = json["code"].intValue guard code == 0 || json["success"].boolValue else { let message = json["message"].string ?? "上传失败" DispatchQueue.main.async { observer.onError(makeError(message)) } return } let nestedId = json["data"]["id"].stringValue let nestedFileId = json["data"]["file_id"].stringValue let dataString = json["data"].stringValue let fileId: String? = { if !nestedId.isEmpty { return nestedId } if !nestedFileId.isEmpty { return nestedFileId } if !dataString.isEmpty, json["data"].type != .dictionary { return dataString } return nil }() guard let fileId, !fileId.isEmpty else { DispatchQueue.main.async { observer.onError(makeError("上传成功但未返回文件 id")) } return } DispatchQueue.main.async { observer.onNext(fileId) observer.onCompleted() } } task.resume() return Disposables.create { task.cancel() } } catch { observer.onError(error) return Disposables.create() } } } private static func makeRequest(fileData: Data, kind: UploadFileKind, scene: String?) throws -> (request: URLRequest, body: Data) { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMddHHmmss" let fileName = "\(formatter.string(from: Date())).\(kind.fileExtension)" let body = multipartBody(fileData: fileData, fileName: fileName, mimeType: kind.mimeType) var query: [String: String] = [ "timestamp": String(format: "%.0lf", Date().timeIntervalSince1970), "nonce": randomString(length: 32) ] if let scene, !scene.isEmpty { query["scene"] = scene } let queryString = sortedQueryString(query) let signature = sign(queryString: queryString, body: body) let signedQuery = queryString + "&signature=" + signature let encodedQuery = signedQuery.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? signedQuery let base = AppNetworkConfig.shared.baseURL guard let url = URL(string: base + "api/user/upload?" + encodedQuery) else { throw makeError("上传地址无效") } var request = URLRequest(url: url) request.httpMethod = "POST" request.timeoutInterval = 180 var headers = ApiManager.shared.httpHeader() headers["Content-Type"] = "multipart/form-data; boundary=\(boundary)" headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } return (request, body) } private static func multipartBody(fileData: Data, fileName: String, mimeType: String) -> Data { var data = Data() func append(_ string: String) { if let part = string.data(using: .utf8) { data.append(part) } } append("--\(boundary)\r\n") append("Content-Disposition:form-data; name=\"file\"; filename=\"\(fileName)\"\r\n") append("Content-Type:\(mimeType)\r\n") append("\r\n") data.append(fileData) append("\r\n") append("--\(boundary)--\r\n") return data } private static func sign(queryString: String, body: Data) -> String { var combined = Data() combined.append(Data((queryString + "&").utf8)) combined.append(body) combined.append(Data(("&" + md5(jiaMiKey)).utf8)) return md5(combined) } private static func sortedQueryString(_ dict: [String: String]) -> String { dict.sorted { $0.key < $1.key } .map { "\($0.key)=\($0.value)" } .joined(separator: "&") } private static func randomString(length: Int) -> String { let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" return String((0.. String { md5(Data(string.utf8)) } private static func md5(_ data: Data) -> String { var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) data.withUnsafeBytes { buffer in _ = CC_MD5(buffer.baseAddress, CC_LONG(data.count), &digest) } return digest.map { String(format: "%02x", $0) }.joined() } private static func makeError(_ message: String) -> NSError { NSError( domain: "UploadService", code: -1, userInfo: [NSLocalizedDescriptionKey: message] ) } }