Files
youle_app_ios_v2/ylgamehall/Source/Network/QiniuUploader.swift
T
joywayerandClaude Opus 4.7 1d7e258130 Phase 4.E + Phase 5 + Phase 3.C 七牛上传:代码侧 SDK 接入(SPM 优先 / Vendor 兜底)
选型(CLAUDE.md ADR-006「SPM 优先 + Vendor xcframework 兜底」):
- 微信 → SPM(Tencent 官方 https://github.com/Tencent/WechatOpenSDK-XCFramework);
  项目方下载的 WechatOpenSDK-NoPay.xcframework 已撤出 Vendor,docs/res 内副本作离线备份
- 高德 → Vendor 手动(官方未提供 SPM;Vendor/AMap/{AMapFoundationKit,AMapLocationKit}.framework
  随仓库分发,fat .framework 同时含 x86_64 + arm64,Do Not Embed)
- 七牛 → SPM(https://github.com/qiniu/objc-sdk v8.9.x)

代码侧(全部 #if canImport 守卫,Xcode 加 SDK 前编译为 no-op):
- Source/SDK/WeChat/WeChatSDK.swift:WXApi.registerApp + handleOpenURL(沿用 msext AppID
  wx586a9b321e56efb7,universalLink 空字符串走非 ULAPI 路径)
- Source/SDK/WeChat/WeChatManager.swift:WXApiDelegate + state UUID 配对的 authorize +
  FIFO 串行 share async/await wrapper
- Source/Login/WeChatAuth.swift:客户端直拼 sns/oauth2/access_token + sns/userinfo →
  7 字段(Province 大写 P / city 经 danbian 去单引号),沿用 msext 同款路径
- Source/SDK/AMap/AMapWrapper.swift:iOS 14+ 隐私合规 3 步 + apiKey 注入
- Source/Location/LocationService.swift:actor + AMapLocationManager 异步包装 + 9 字段
- Source/Network/QiniuConfig.swift:4 项常量沿用 msext(AccessKey/SecretKey/Bucket/Domain)
- Source/Network/QiniuTokenSigner.swift:纯 Swift CryptoKit HMAC-SHA1 + Base64URL 自签
  token(与 msext QiniuManager.m:200-230 等价,不依赖 Qiniu SDK 工作)
- Source/Network/QiniuUploader.swift:actor 包 QNUploadManager async/await

Handler 升级:
- AccreditLoginHandler:拉起授权 → 反向 callback sharelogin 7 字段
- WechatShare:真实链接分享(type=2/3 截图待 Phase 4.F)
- StartLocationHandler:真实定位 → 9 字段(latitude/longitude string, province 小写 p)

生命周期:
- AppDelegate.didFinishLaunchingWithOptions:WeChatSDK.register + AMapWrapper.bootstrap
- SceneDelegate.openURLContexts:WeChatSDK.handleOpenURL 接入回调
- BackGameDataHandler:子游戏 backgameData pop 时 WXApi.delegate=nil + LocationService.stop()

Info.plist:CFBundleURLTypes 加 wx586a9b321e56efb7;LSApplicationQueriesSchemes 追加
weixin/weixinULAPI/weixinURLParamsAPI;NSLocationWhenInUseUsageDescription +
NSMicrophoneUsageDescription(gamehallname 中文文案)。

.gitignore:排除 docs/res/{AMap_iOS_Loc_ALL,objc-sdk-8.9.2}/(235MB 项目方下载副本,
已走 SPM/Vendor 接入不需要副本)。

文档:docs/SDK-Integration-Guide.md 重写 §A 用户手动 Xcode UI 三步(SPM × 2 +
Add Files × 2);Vendor/{AMap,WechatSDK}/README.md 记录各自接入路径选型。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 22:22:23 +08:00

98 lines
3.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// QiniuUploader.swift
// ylgamehall
//
// 七牛上传 actor wrapper。包 `QNUploadManager` 为 async/await。
//
// ⚠️ canImport(QiniuSDK) 守卫:在 Xcode 把 https://github.com/qiniu/objc-sdk
// 添加到 Swift Package Dependencies 之前,本文件编译为 no-opupload 永远抛
// QiniuUploadError.sdkNotLinked。后续 handler 可降级到 stub 路径。
//
// 上传完成后返回 UploadedFile,字段映射给 H5 `getaudiourl` / `recordSuccess`。
//
import Foundation
#if canImport(QiniuSDK)
import QiniuSDK
#endif
public struct UploadedFile: Sendable {
public let fileUrl: String // 公网访问 URLhttp://{domain}/{key}
public let fileName: String // 原始本地文件名
public let fileKey: String // 七牛 keypath-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: 本地文件 URLamr 录音文件)
/// - 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`{recordingDirectory}{uuid}.{ext}`(与 msext QiniuManager.m:55-60 等价)
let uuid = UUID().uuidString.lowercased().replacingOccurrences(of: "-", with: "")
let ext = localFile.pathExtension
let key = QiniuConfig.recordingDirectory + uuid + (ext.isEmpty ? "" : ".\(ext)")
// 客户端自签 token(参 QiniuTokenSigner / msext QiniuManager 同款算法)
let token = QiniuTokenSigner.uploadToken(key: key)
return try await withCheckedThrowingContinuation { (cont: CheckedContinuation<UploadedFile, Error>) in
let mgr = QNUploadManager()
let fileName = localFile.lastPathComponent
mgr.putFile(
localFile.path,
key: key,
token: token,
complete: { info, savedKey, _ in
if let info, info.isOK, let savedKey {
let url = QiniuConfig.publicURL(forKey: 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
}
}