Files
youle_app_ios_v2/ylgamehall/Source/Network/QiniuUploader.swift
T
joywayerandClaude Opus 4.7 83d9502dee ADR-009 凭证集中化:AppSecrets.plist + 七牛运行参数走 RemoteConfig.audio_*(契约影响)
【数据流改造(三分制)】
- 微信 AppID  唯一源 = Info.plist CFBundleURLTypes (URLName=weixin first scheme)
- 应用级凭证 唯一源 = AppSecrets.plist (wxAppSecret/qiniuAccessKey/qiniuSecretKey)
- 七牛运行参数 唯一源 = RemoteConfig 顶层 audio_domain / audio_bucket(远端动态注入)

【契约影响】
- ChannelConfig.plist:11 key → 10 key,移除 qiniudomain(ADR-007 守护规则同步)
- BundleConfig:删除 qiniuDomain 属性
- RemoteConfig:顶层新增可选字段 audioDomain / audioBucket(JSON snake_case 自动归一化)
- 启动期:WebContainerViewController parsed 分支校验 audio_domain/audio_bucket 非空,
  缺失抛 BootError.audioConfigMissing 弹 modal 永停(与 showmessage 同等致命)
- 上线前置:测试 / 生产远端 .txt 配置必须先补 audio_domain / audio_bucket 两个顶层 key
- WeChatSDK.appID / WeChatAuth.appSecret / QiniuConfig.* 调用方零签名变化

【新增】
- ylgamehall/Resources/AppSecrets.plist(3 key)
- ylgamehall/Source/Resource/AppSecrets.swift(单例加载,对齐 BundleConfig 模式)
- QiniuConfig 改 actor:cdnDomain/bucketName 进 actor 状态 + update(...) async;
  accessKey/secretKey 仍 nonisolated(直接读 AppSecrets)
- QiniuTokenSigner.uploadToken() 改 async(bucketName 来自 actor)
- QiniuUploader 预取 cdnDomain 闭包外,SDK 同步 callback 内直接拼 URL

【删除】
- WeChatSDK.swift  static let appID  硬编码 → Info.plist 启动期解析
- WeChatAuth.swift static let appSecret 硬编码 → AppSecrets.shared.wxAppSecret
- QiniuConfig 中 accessKey / secretKey / bucketName / cdnDomain 四处硬编码
- ChannelConfig.plist 的 qiniudomain 字段(plist 与代码双源僵尸字段)

【文档同步】
- Plan:新增 ADR-009 + ADR-007 守护规则改 10 key + §236 BundleConfig 描述
- Design §3.4 多处 "11 项" → "10 项";§7.0.3 plist 示例 + BundleConfig 代码骨架
  + Scripts/inject_channel.sh 同步
- SDK-Integration-Guide §0 凭证位置改三分制 + §尾"七牛域名读取"加新外壳路径
- Verification-Checklist L69 "(11 项)" → "(10 项)"

参考契约章节:docs/Development-Plan.md ADR-009、docs/H5-Native-Implementation-Design.md §7.0.3
BuildProject 通过,Xcode 即时诊断 0 警告。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 21:07:55 +08:00

109 lines
4.6 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 直接用本地文件名(调用方负责命名)。对齐 daoqi 录音文件命名规则:
// - 大厅:yyyyMMddHHmmss%08X.amrNewRootVC.m:1908,无下划线)
// - 子游戏:yyyyMMddHHmmss_%08X.amrgameController.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:`。用 defaultConfigurationV2v2 已替代旧 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<UploadedFile, Error>) 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
}
}