import SwiftUI import PhotosUI import CommonCrypto /// 处理图片上传到远程服务器的类 /// 支持上传图片并获取服务器返回的file_id public class ImageUploaderGetID: ObservableObject { // MARK: - 类型定义 private let session: URLSession private let networkService: NetworkServiceProtocol private let networkHandler: (_ path: String, _ parameters: [String: Any], _ completion: @escaping (Result) -> Void) -> Void public init(session: URLSession = .shared, networkService: NetworkServiceProtocol? = nil) { self.session = session let service = networkService ?? NetworkService.shared self.networkService = service self.networkHandler = { path, parameters, completion in service.postWithToken( path: path, parameters: parameters, completion: { (result: Result) in switch result { case .success(let response): // Convert the response back to Data for the completion handler do { let data = try JSONEncoder().encode(response) completion(.success(data)) } catch { completion(.failure(.invalidResponseData)) } case .failure(let error): completion(.failure(.networkError(error))) } } ) } } /// 测试专用的初始化方法 /// - Parameters: /// - session: 用于测试的URLSession /// - networkService: 用于测试的NetworkService /// - networkHandler: 用于测试的网络请求处理器 internal init( session: URLSession, networkService: NetworkServiceProtocol? = nil, networkHandler: @escaping (String, [String: Any], @escaping (Result) -> Void) -> Void ) { self.session = session self.networkService = networkService ?? NetworkService.shared self.networkHandler = networkHandler } /// 上传结果 public struct UploadResult: Codable { public let fileUrl: String public let fileName: String public let fileSize: Int public let fileId: String public init(fileUrl: String, fileName: String, fileSize: Int, fileId: String) { self.fileUrl = fileUrl self.fileName = fileName self.fileSize = fileSize self.fileId = fileId } } /// 上传过程中可能发生的错误 public enum UploadError: LocalizedError { case invalidImageData case invalidURL case serverError(String) case invalidResponse case uploadFailed(Error?) case invalidFileId case invalidResponseData case networkError(Error) case decodingError(Error) case unauthorized public var errorDescription: String? { switch self { case .invalidImageData: return "无效的图片数据" case .invalidURL: return "无效的URL" case .serverError(let message): return "服务器错误: \(message)" case .invalidResponse: return "无效的服务器响应" case .uploadFailed(let error): return "上传失败: \(error?.localizedDescription ?? "未知错误")" case .invalidFileId: return "无效的文件ID" case .invalidResponseData: return "无效的响应数据" case .networkError(let error): return "网络错误: \(error.localizedDescription)" case .decodingError(let error): return "解码错误: \(error.localizedDescription)" case .unauthorized: return "认证失败,需要重新登录" } } } // MARK: - 公开方法 /// 上传图片到服务器 public func uploadImage( _ image: UIImage, progress: @escaping (Double) -> Void, completion: @escaping (Result) -> Void ) { print("🔄 开始准备上传图片...") guard let imageData = image.jpegData(compressionQuality: 0.7) else { let error = UploadError.invalidImageData print("❌ 错误:\(error.localizedDescription)") completion(.failure(error)) return } requestUploadURL(fileName: "image_\(UUID().uuidString).jpg", fileData: imageData) { [weak self] result in guard let self = self else { return } switch result { case .success(let response): print("📤 获取到上传URL,开始上传文件...") guard let uploadURL = URL(string: response.data.uploadUrl) else { let error = UploadError.invalidURL print("❌ [ImageUploader] 上传URL格式无效: \(response.data.uploadUrl)") completion(.failure(error)) return } self.uploadFile( fileData: imageData, to: uploadURL, mimeType: "image/jpeg", onProgress: { uploadProgress in print("📊 上传进度: \(Int(uploadProgress * 100))%") progress(uploadProgress) }, completion: { [weak self] uploadResult in switch uploadResult { case .success: self?.confirmUpload( fileId: response.data.fileId, fileName: "image_\(UUID().uuidString).jpg", fileSize: imageData.count, completion: completion ) case .failure(let error): completion(.failure(error)) } } ) case .failure(let error): completion(.failure(error)) } } } /// 上传视频文件到服务器 /// - Parameters: /// - videoURL: 视频文件的本地URL /// - progress: 上传进度回调 (0.0 到 1.0) /// - completion: 完成回调,返回上传结果或错误 public func uploadVideo( _ videoURL: URL, progress: @escaping (Double) -> Void, completion: @escaping (Result) -> Void ) { print("🎥 开始准备上传视频...") do { let videoData = try Data(contentsOf: videoURL) let fileName = "video_\(UUID().uuidString).mp4" requestUploadURL( fileName: fileName, fileData: videoData ) { [weak self] result in guard let self = self else { return } switch result { case .success(let response): print("📤 获取到视频上传URL,开始上传文件...") guard let uploadURL = URL(string: response.data.uploadUrl) else { let error = UploadError.invalidURL print("❌ [ImageUploader] 视频上传URL格式无效: \(response.data.uploadUrl)") completion(.failure(error)) return } self.uploadFile( fileData: videoData, to: uploadURL, mimeType: "video/mp4", onProgress: progress, completion: { [weak self] uploadResult in guard let self = self else { return } switch uploadResult { case .success: self.confirmUpload( fileId: response.data.fileId, fileName: fileName, fileSize: videoData.count, completion: completion ) case .failure(let error): completion(.failure(error)) } } ) case .failure(let error): completion(.failure(error)) } } } catch { print("❌ 读取视频文件失败: \(error.localizedDescription)") completion(.failure(.uploadFailed(error))) } } // MARK: - 私有方法 /// 发起网络请求 private func request( path: String, parameters: [String: Any], completion: @escaping (Result) -> Void ) { networkHandler(path, parameters) { (result: Result) in switch result { case .success(let data): do { let decoded = try JSONDecoder().decode(T.self, from: data) completion(.success(decoded)) } catch { print("❌ [ImageUploader] 解析响应失败: \(error)") completion(.failure(.invalidResponseData)) } case .failure(let error): print("❌ [ImageUploader] 网络请求失败: \(error)") completion(.failure(error)) } } } /// 请求上传URL private func requestUploadURL( fileName: String, fileData: Data, completion: @escaping (Result) -> Void ) { let parameters: [String: Any] = [ "filename": fileName, "content_type": "image/jpeg", "file_size": fileData.count ] print(""" 📝 [ImageUploader] 开始请求上传URL 文件名: \(fileName) 📏 文件大小: \(fileData.count) 字节 📋 参数: \(parameters) """) request(path: "/file/generate-upload-url", parameters: parameters, completion: completion) } /// 上传文件到指定URL public func uploadFile( fileData: Data, to uploadURL: URL, mimeType: String = "application/octet-stream", onProgress: @escaping (Double) -> Void, completion: @escaping (Result) -> Void ) -> URLSessionUploadTask? { print(""" ⬆️ [ImageUploader] 开始上传文件 🔗 目标URL: \(uploadURL.absoluteString) 📏 文件大小: \(fileData.count) 字节 📋 MIME类型: \(mimeType) """) // 创建请求 var request = URLRequest(url: uploadURL) request.httpMethod = "PUT" // 设置请求头 var headers: [String: String] = [ "Content-Type": mimeType, "Content-Length": String(fileData.count) ] // 添加认证头 if let token = KeychainHelper.getAccessToken() { headers["Authorization"] = "Bearer \(token)" print("🔑 [ImageUploader] 添加认证头,Token: \(token.prefix(10))...") } else { print("⚠️ [ImageUploader] 未找到认证Token") } // 设置所有请求头 headers.forEach { key, value in request.setValue(value, forHTTPHeaderField: key) } // 创建上传任务 let task = networkService.upload( request: request, fileData: fileData, onProgress: onProgress, completion: { [weak self] result in guard let self = self else { return } switch result { case .success(let data, let response): guard let httpResponse = response as? HTTPURLResponse else { print("❌ [ImageUploader] 无效的响应") completion(.failure(.invalidResponse)) return } let statusCode = httpResponse.statusCode let responseBody = data.flatMap { String(data: $0, encoding: .utf8) } ?? "" print(""" 📡 [ImageUploader] 服务器响应 🔢 状态码: \(statusCode) 🔗 请求URL: \(uploadURL.absoluteString) 📦 响应: \(responseBody) """) switch statusCode { case 200...299: print("✅ [ImageUploader] 文件上传成功") completion(.success(())) case 401: print("🔑 [ImageUploader] 认证失败,需要重新登录") completion(.failure(.unauthorized)) case 403: let errorMessage = "访问被拒绝 (403) - URL: \(uploadURL.absoluteString)" print("❌ [ImageUploader] \(errorMessage)") completion(.failure(.serverError(errorMessage))) default: let errorMessage = "服务器返回错误: \(statusCode)" print("❌ [ImageUploader] \(errorMessage)") completion(.failure(.serverError(errorMessage))) } case .failure(let error): print("❌ [ImageUploader] 上传失败: \(error.localizedDescription)") completion(.failure(.networkError(error))) } } ) // 开始上传 task?.resume() return task } /// 确认上传完成 private func confirmUpload( fileId: String, fileName: String, fileSize: Int, completion: @escaping (Result) -> Void ) { let parameters: [String: Any] = [ "file_id": fileId, "file_name": fileName, "file_size": fileSize ] print(""" 📨 [ImageUploader] 开始确认上传完成 📁 文件ID: \(fileId) 📝 文件名: \(fileName) 📏 文件大小: \(fileSize) 字节 📋 参数: \(parameters) """) struct ConfirmUploadResponse: Codable { let code: Int let message: String let data: [String: String]? } request(path: "/file/confirm-upload", parameters: parameters) { (result: Result) in switch result { case .success(_): let uploadResult = UploadResult( fileUrl: "\(APIConfig.baseURL)/files/\(fileId)", fileName: fileName, fileSize: fileSize, fileId: fileId ) print(""" ✅ [ImageUploader] 文件上传确认成功 📁 文件ID: \(fileId) 🔗 文件URL: \(uploadResult.fileUrl) 📝 文件名: \(fileName) 📏 文件大小: \(fileSize) 字节 """) completion(.success(uploadResult)) case .failure(let error): print("❌ [ImageUploader] 文件上传确认失败: \(error.localizedDescription)") completion(.failure(error)) } } } // MARK: - 辅助方法 private func calculateSpeed(bytes: Int, seconds: TimeInterval) -> String { guard seconds > 0 else { return "0 KB/s" } let bytesPerSecond = Double(bytes) / seconds if bytesPerSecond >= 1024 * 1024 { return String(format: "%.1f MB/s", bytesPerSecond / (1024 * 1024)) } else { return String(format: "%.1f KB/s", bytesPerSecond / 1024) } } } // MARK: - Data Extension for MD5 extension Data { func md5Base64EncodedString() -> String? { #if canImport(CommonCrypto) var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) _ = self.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in if let baseAddress = ptr.baseAddress, ptr.count > 0 { CC_MD5(baseAddress, CC_LONG(self.count), &digest) } } return Data(digest).base64EncodedString() #else return nil #endif } } // MARK: - 响应模型 private struct UploadURLResponse: Codable { let code: Int let message: String? let data: UploadData struct UploadData: Codable { let fileId: String let filePath: String let uploadUrl: String let expiresIn: Int enum CodingKeys: String, CodingKey { case fileId = "file_id" case filePath = "file_path" case uploadUrl = "upload_url" case expiresIn = "expires_in" } } } private struct EmptyResponse: Codable {} // MARK: - URLSessionTask 扩展 private class TaskObserver: NSObject { private weak var task: URLSessionTask? private var handlers: [() -> Void] = [] init(task: URLSessionTask) { self.task = task super.init() task.addObserver(self, forKeyPath: #keyPath(URLSessionTask.state), options: .new, context: nil) } func addHandler(_ handler: @escaping () -> Void) { handlers.append(handler) } override func observeValue( forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer? ) { guard keyPath == #keyPath(URLSessionTask.state), let task = task, task.state == .completed else { return } DispatchQueue.main.async { [weak self] in self?.handlers.forEach { $0() } self?.cleanup() } } private func cleanup() { task?.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.state)) handlers.removeAll() } deinit { cleanup() } } private extension URLSessionTask { private static var taskObserverKey: UInt8 = 0 private var taskObserver: TaskObserver? { get { return objc_getAssociatedObject(self, &Self.taskObserverKey) as? TaskObserver } set { objc_setAssociatedObject(self, &Self.taskObserverKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } func addCompletionHandler(_ handler: @escaping () -> Void) { if #available(iOS 11.0, *) { if let observer = taskObserver { observer.addHandler(handler) } else { let observer = TaskObserver(task: self) observer.addHandler(handler) taskObserver = observer } } else { let name = NSNotification.Name("TaskCompleted\(self.taskIdentifier)") NotificationCenter.default.addObserver( forName: name, object: self, queue: .main ) { _ in handler() } } } }