// // QiniuUploader.swift // ylgamehall // // 七牛上传 actor wrapper。包 `QNUploadManager` 为 async/await。 // // ⚠️ canImport(QiniuSDK) 守卫:在 Xcode 把 https://github.com/qiniu/objc-sdk // 添加到 Swift Package Dependencies 之前,本文件编译为 no-op,upload 永远抛 // QiniuUploadError.sdkNotLinked。后续 handler 可降级到 stub 路径。 // // 上传完成后返回 UploadedFile,字段映射给 H5 `getaudiourl` / `recordSuccess`。 // import Foundation #if canImport(QiniuSDK) import QiniuSDK #endif public struct UploadedFile: Sendable { public let fileUrl: String // 公网访问 URL(http://{domain}/{key}) public let fileName: String // 原始本地文件名 public let fileKey: String // 七牛 key(path-on-cdn) public let timeSec: Int // 录音时长(秒) } public enum QiniuUploadError: Error, Sendable { case sdkNotLinked case fileNotFound(String) case uploadFailed(any Error) case responseInvalid } public actor QiniuUploader { public static let shared = QiniuUploader() public init() {} /// 上传文件。 /// - Parameters: /// - localFile: 本地文件 URL(amr 录音文件) /// - timeSec: 录音时长,回传给 H5 `getaudiourl` 用 /// - Returns: UploadedFile(含公网 URL) public func upload(_ localFile: URL, timeSec: Int) async throws -> UploadedFile { #if canImport(QiniuSDK) let fm = FileManager.default guard fm.fileExists(atPath: localFile.path) else { throw QiniuUploadError.fileNotFound(localFile.path) } // 七牛 key 直接用本地文件名(调用方负责命名)。对齐 daoqi 录音文件命名规则: // - 大厅:yyyyMMddHHmmss%08X.amr(NewRootVC.m:1908,无下划线) // - 子游戏:yyyyMMddHHmmss_%08X.amr(gameController.m:2269,有下划线) // H5 通过 audiourl 转发给对端,对端按文件名规律假设——必须严格对齐。 let key = QiniuConfig.recordingDirectory + localFile.lastPathComponent // 客户端自签 token(参 QiniuTokenSigner / msext QiniuManager 同款算法) let token = await QiniuTokenSigner.uploadToken(key: key) // 预取 CDN 域名(远端注入到 QiniuConfig actor),闭包里同步拼 URL 避免再 await let cdnDomain = await QiniuConfig.shared.cdnDomain // 七牛 SDK 8.x:`QNUploadManager.init()` 被标 `kQNDeprecated`,必须走 // `initWithConfiguration:`。用 defaultConfigurationV2(v2 已替代旧 defaultConfiguration)。 // initWithConfiguration: ObjC 端 instancetype 桥到 Swift 仍为可选; // 实际几乎不会失败(内部仅初始化无 IO),失败按 responseInvalid 抛出。 let cfg = QNConfiguration.defaultConfigurationV2() guard let mgr = QNUploadManager(configuration: cfg) else { throw QiniuUploadError.responseInvalid } let fileName = localFile.lastPathComponent return try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in mgr.putFile( localFile.path, key: key, token: token, complete: { info, savedKey, _ in if let info, info.isOK, let savedKey { let url = "http://\(cdnDomain)/\(savedKey)" cont.resume(returning: UploadedFile( fileUrl: url, fileName: fileName, fileKey: savedKey, timeSec: timeSec )) } else if let error = info?.error { cont.resume(throwing: QiniuUploadError.uploadFailed(error)) } else { cont.resume(throwing: QiniuUploadError.responseInvalid) } }, option: nil ) } #else throw QiniuUploadError.sdkNotLinked #endif } /// 取消所有进行中任务(接 BackGameDataHandler 清理钩子用)。 /// QNUploadManager 没有「取消全部」API,单任务用 QNUploadOption.cancellationSignal 实现; /// 当前实现以 task 粒度由 Swift Task.cancel 传递(withCheckedThrowingContinuation /// 已在 cancellation 时 throw CancellationError),无需额外状态。 public func cancelInFlight() { // no-op(依赖 Task.cancel 传播);如未来需要硬 cancel,改为持有 QNUploadOption 并触发 cancellationSignal } }