import Foundation import ComposableArchitecture import QCloudCOSXML // MARK: - 腾讯云 COS 管理器 /// 腾讯云 COS 管理器 /// /// 负责管理腾讯云 COS 相关的操作,包括: /// - Token 获取和缓存 /// - 文件上传、下载、删除 /// - 凭证管理和过期处理 @MainActor class COSManager: ObservableObject { static let shared = COSManager() private init() {} // 幂等初始化标记 private static var isCOSInitialized = false // 幂等初始化方法 private func ensureCOSInitialized(tokenData: TcTokenData) { guard !Self.isCOSInitialized else { return } let configuration = QCloudServiceConfiguration() let endpoint = QCloudCOSXMLEndPoint() endpoint.regionName = tokenData.region endpoint.useHTTPS = true if tokenData.accelerate { endpoint.suffix = "cos.accelerate.myqcloud.com" } configuration.endpoint = endpoint QCloudCOSXMLService.registerDefaultCOSXML(with: configuration) QCloudCOSTransferMangerService.registerDefaultCOSTransferManger(with: configuration) Self.isCOSInitialized = true debugInfoSync("✅ COS服务已初始化,region: \(tokenData.region)") } // MARK: - Token 管理 /// 当前缓存的 Token 信息 private var cachedToken: TcTokenData? private var tokenExpirationDate: Date? /// 获取腾讯云 COS Token /// - Parameter apiService: API 服务实例 /// - Returns: Token 数据,如果获取失败返回 nil func getToken(apiService: any APIServiceProtocol & Sendable) async -> TcTokenData? { // 检查缓存是否有效 if let cached = cachedToken, let expiration = tokenExpirationDate, Date() < expiration { debugInfoSync("🔐 使用缓存的 COS Token") return cached } // 清除过期缓存 clearCachedToken() // 请求新的 Token debugInfoSync("🔐 开始请求腾讯云 COS Token...") do { let request = TcTokenRequest() let response: TcTokenResponse = try await apiService.request(request) guard response.code == 200, let tokenData = response.data else { debugInfoSync("❌ COS Token 请求失败: \(response.message)") return nil } // 缓存 Token 和过期时间 cachedToken = tokenData tokenExpirationDate = tokenData.expirationDate debugInfoSync("✅ COS Token 获取成功") debugInfoSync(" - 存储桶: \(tokenData.bucket)") debugInfoSync(" - 地域: \(tokenData.region)") debugInfoSync(" - 过期时间: \(tokenData.expirationDate)") debugInfoSync(" - 剩余时间: \(tokenData.remainingTime)秒") return tokenData } catch { debugInfoSync("❌ COS Token 请求异常: \(error.localizedDescription)") return nil } } /// 缓存 Token 信息 /// - Parameter tokenData: Token 数据 private func cacheToken(_ tokenData: TcTokenData) async { cachedToken = tokenData // 解析过期时间(假设 expiration 是 ISO 8601 格式) if let expirationDate = ISO8601DateFormatter().date(from: String(tokenData.expireTime)) { // 提前 5 分钟过期,确保安全 tokenExpirationDate = expirationDate.addingTimeInterval(-300) } else { // 如果解析失败,设置默认过期时间(1小时) tokenExpirationDate = Date().addingTimeInterval(3600) } debugInfoSync("💾 COS Token 已缓存,过期时间: \(tokenExpirationDate?.description ?? "未知")") } /// 清除缓存的 Token private func clearCachedToken() { cachedToken = nil tokenExpirationDate = nil debugInfoSync("🗑️ 清除缓存的 COS Token") } /// 强制刷新 Token func refreshToken(apiService: any APIServiceProtocol & Sendable) async -> TcTokenData? { clearCachedToken() return await getToken(apiService: apiService) } // MARK: - 只读属性 /// 外部安全访问 Token var token: TcTokenData? { cachedToken } // MARK: - 调试信息 /// 获取当前 Token 状态信息 func getTokenStatus() -> String { if let _ = cachedToken, let expiration = tokenExpirationDate { let isExpired = Date() >= expiration return "Token 状态: \(isExpired ? "已过期" : "有效"), 过期时间: \(expiration)" } else { return "Token 状态: 未缓存" } } // MARK: - 上传功能 /// 上传图片到腾讯云 COS /// - Parameters: /// - imageData: 图片数据 /// - apiService: API 服务实例 /// - Returns: 上传成功的云地址,如果失败返回 nil func uploadImage(_ imageData: Data, apiService: any APIServiceProtocol & Sendable) async -> String? { guard let tokenData = await getToken(apiService: apiService) else { debugInfoSync("❌ 无法获取 COS Token") return nil } // 上传前确保COS服务已初始化 ensureCOSInitialized(tokenData: tokenData) // 初始化 COS 配置 let credential = QCloudCredential() credential.secretID = tokenData.secretId // 打印secretKey原始内容,去除首尾空白 let rawSecretKey = tokenData.secretKey.trimmingCharacters(in: .whitespacesAndNewlines) debugInfoSync("secretKey原始内容: [\(rawSecretKey)]") credential.secretKey = rawSecretKey credential.token = tokenData.sessionToken credential.startDate = tokenData.startDate credential.expirationDate = tokenData.expirationDate let request = QCloudCOSXMLUploadObjectRequest() request.bucket = tokenData.bucket request.regionName = tokenData.region request.credential = credential // 生成唯一 key let fileExtension = "jpg" // 假设为 JPG,可根据实际调整 let key = "images/\(UUID().uuidString).\(fileExtension)" request.object = key request.body = imageData as AnyObject //监听上传进度 request.sendProcessBlock = { (bytesSent, totalBytesSent, totalBytesExpectedToSend) in debugInfoSync("\(bytesSent), \(totalBytesSent), \(totalBytesExpectedToSend)") // bytesSent 本次要发送的字节数(一个大文件可能要分多次发送) // totalBytesSent 已发送的字节数 // totalBytesExpectedToSend 本次上传要发送的总字节数(即一个文件大小) }; // 设置加速 if tokenData.accelerate { request.enableQuic = true // endpoint 增加 "cos.accelerate.myqcloud.com" } // 使用 async/await 包装上传回调 return await withCheckedContinuation { continuation in request.setFinish { result, error in if let error = error { debugInfoSync("❌ 图片上传失败: \(error.localizedDescription)") continuation.resume(returning: " ?????????? ") } else { // 构建云地址 let domain = tokenData.customDomain.isEmpty ? "\(tokenData.bucket).cos.\(tokenData.region).myqcloud.com" : tokenData.customDomain let cloudURL = "https://\(domain)/\(key)" debugInfoSync("✅ 图片上传成功: \(cloudURL)") continuation.resume(returning: cloudURL) } } QCloudCOSTransferMangerService.defaultCOSTransferManager().uploadObject(request) } } } // MARK: - 调试扩展 extension COSManager { /// 测试 Token 获取功能(仅用于调试) func testTokenRetrieval(apiService: any APIServiceProtocol & Sendable) async { #if DEBUG debugInfoSync("\n🧪 开始测试腾讯云 COS Token 获取功能") let token = await getToken(apiService: apiService) if let tokenData = token { debugInfoSync("✅ Token 获取成功") debugInfoSync(" bucket: \(tokenData.bucket)") debugInfoSync(" Expiration: \(tokenData.expireTime)") debugInfoSync(" Token: \(tokenData.sessionToken.prefix(20))...") debugInfoSync(" SecretId: \(tokenData.secretId.prefix(20))...") } else { debugInfoSync("❌ Token 获取失败") } debugInfoSync("📊 Token 状态: \(getTokenStatus())") debugInfoSync("✅ 腾讯云 COS Token 测试完成\n") #endif } }