
- 重构用户信息加载逻辑,采用Result类型处理成功与失败的响应,提升错误处理能力。 - 更新状态管理,确保用户信息加载状态与错误信息的准确反映。 - 移除冗余代码,简化用户信息获取流程,增强代码可读性。
242 lines
9.5 KiB
Swift
242 lines
9.5 KiB
Swift
import Foundation
|
||
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<AnyObject>()
|
||
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 prefix = domain.hasPrefix("http") ? "" : "https://"
|
||
let cloudURL = "\(prefix)\(domain)/\(key)"
|
||
debugInfoSync("✅ 图片上传成功: \(cloudURL)")
|
||
continuation.resume(returning: cloudURL)
|
||
}
|
||
}
|
||
QCloudCOSTransferMangerService.defaultCOSTransferManager().uploadObject(request)
|
||
}
|
||
}
|
||
|
||
/// 上传 UIImage 到腾讯云 COS,自动压缩为 JPEG(0.8)
|
||
/// - Parameters:
|
||
/// - image: UIImage 实例
|
||
/// - apiService: API 服务实例
|
||
/// - Returns: 上传成功的云地址,如果失败返回 nil
|
||
func uploadUIImage(_ image: UIImage, apiService: any APIServiceProtocol & Sendable) async -> String? {
|
||
guard let data = image.jpegData(compressionQuality: 0.8) else {
|
||
debugInfoSync("❌ 图片压缩失败,无法生成 JPEG 数据")
|
||
return nil
|
||
}
|
||
return await uploadImage(data, apiService: apiService)
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|
||
}
|