
- 在APIEndpoints.swift中新增tcToken端点以支持腾讯云COS Token获取。 - 在APIModels.swift中新增TcTokenRequest和TcTokenResponse模型,处理Token请求和响应。 - 在COSManager.swift中实现Token的获取、缓存和过期管理逻辑,提升API请求的安全性。 - 在LanguageSettingsView中添加调试功能,允许测试COS Token获取。 - 在多个视图中更新状态管理和导航逻辑,确保用户体验一致性。 - 在FeedFeature和HomeFeature中优化状态管理,简化视图逻辑。
725 lines
24 KiB
Swift
725 lines
24 KiB
Swift
import Foundation
|
||
import ComposableArchitecture
|
||
|
||
// MARK: - HTTP Method
|
||
|
||
/// HTTP 请求方法枚举
|
||
///
|
||
/// 定义了 API 请求支持的 HTTP 方法类型
|
||
/// 每个方法都有对应的字符串值,用于构建 URLRequest
|
||
enum HTTPMethod: String, CaseIterable {
|
||
case GET = "GET"
|
||
case POST = "POST"
|
||
case PUT = "PUT"
|
||
case DELETE = "DELETE"
|
||
case PATCH = "PATCH"
|
||
}
|
||
|
||
// MARK: - API Error Types
|
||
|
||
/// API 错误类型枚举
|
||
///
|
||
/// 定义了 API 请求过程中可能出现的各种错误类型,
|
||
/// 每种错误都包含详细的描述信息,便于调试和用户提示
|
||
///
|
||
/// 错误类型包括:
|
||
/// - 网络相关错误(超时、连接失败等)
|
||
/// - 数据相关错误(解析失败、数据过大等)
|
||
/// - HTTP 状态码错误
|
||
/// - 其他未知错误
|
||
enum APIError: Error, Equatable {
|
||
case invalidURL
|
||
case noData
|
||
case decodingError(String)
|
||
case networkError(String)
|
||
case httpError(statusCode: Int, message: String?)
|
||
case timeout
|
||
case resourceTooLarge
|
||
case encryptionFailed // 新增:加密失败
|
||
case invalidResponse // 新增:无效响应
|
||
case ticketFailed // 新增:票据获取失败
|
||
case custom(String) // 新增:自定义错误信息
|
||
case unknown(String)
|
||
|
||
var localizedDescription: String {
|
||
switch self {
|
||
case .invalidURL:
|
||
return "无效的 URL"
|
||
case .noData:
|
||
return "没有收到数据"
|
||
case .decodingError(let message):
|
||
return "数据解析失败: \(message)"
|
||
case .networkError(let message):
|
||
return "网络错误: \(message)"
|
||
case .httpError(let statusCode, let message):
|
||
return "HTTP 错误 \(statusCode): \(message ?? "未知错误")"
|
||
case .timeout:
|
||
return "请求超时"
|
||
case .resourceTooLarge:
|
||
return "响应数据过大"
|
||
case .encryptionFailed:
|
||
return "数据加密失败"
|
||
case .invalidResponse:
|
||
return "服务器响应无效"
|
||
case .ticketFailed:
|
||
return "获取会话票据失败"
|
||
case .custom(let message):
|
||
return message
|
||
case .unknown(let message):
|
||
return "未知错误: \(message)"
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Base Request Parameters
|
||
|
||
/// 基础请求参数结构体
|
||
///
|
||
/// 包含所有 API 请求都需要的基础参数,如设备信息、应用信息、网络状态等。
|
||
/// 这些参数会自动添加到每个 API 请求中,确保服务器能够获取到完整的客户端信息。
|
||
///
|
||
/// 主要功能:
|
||
/// - 自动收集设备和应用信息
|
||
/// - 生成安全签名
|
||
/// - 支持不同环境的配置
|
||
///
|
||
/// 使用示例:
|
||
/// ```swift
|
||
/// var baseRequest = BaseRequest()
|
||
/// baseRequest.generateSignature(with: ["key": "value"])
|
||
/// ```
|
||
struct BaseRequest: Codable {
|
||
let acceptLanguage: String
|
||
let os: String = "iOS"
|
||
let osVersion: String
|
||
let netType: Int
|
||
let ispType: String
|
||
let channel: String
|
||
let model: String
|
||
let deviceId: String
|
||
let appVersion: String
|
||
let app: String
|
||
let lang: String
|
||
let mcc: String?
|
||
let spType: String?
|
||
var pubSign: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case acceptLanguage = "Accept-Language"
|
||
case os, osVersion, netType, ispType, channel, model, deviceId
|
||
case appVersion, app, lang, mcc, spType
|
||
case pubSign = "pub_sign"
|
||
}
|
||
|
||
@MainActor
|
||
init() {
|
||
// 获取系统首选语言
|
||
let preferredLanguage = Locale.current.language.languageCode?.identifier ?? "en"
|
||
self.acceptLanguage = preferredLanguage
|
||
self.lang = preferredLanguage
|
||
|
||
// 获取系统版本
|
||
self.osVersion = UIDevice.current.systemVersion
|
||
|
||
// 获取设备型号
|
||
self.model = UIDevice.current.model
|
||
|
||
// 生成设备ID
|
||
self.deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
||
|
||
// 获取应用版本
|
||
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
||
|
||
// 应用名称
|
||
self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "eparty"
|
||
|
||
// 网络类型检测(WiFi=2, 蜂窝网络=1)
|
||
self.netType = NetworkTypeDetector.getCurrentNetworkType()
|
||
|
||
// 运营商信息
|
||
let carrierInfo = CarrierInfoManager.getCarrierInfo()
|
||
self.ispType = carrierInfo.ispType
|
||
self.mcc = carrierInfo.mcc == "65535" ? nil : carrierInfo.mcc
|
||
self.spType = self.mcc
|
||
|
||
// 渠道信息
|
||
#if DEBUG
|
||
self.channel = "molistar_enterprise"
|
||
#else
|
||
self.channel = "appstore"
|
||
#endif
|
||
|
||
// 生成符合规范的签名(先生成基础参数字典,然后计算签名)
|
||
self.pubSign = "" // 临时值,稍后计算
|
||
}
|
||
|
||
/// 生成符合 API 规则的安全签名
|
||
///
|
||
/// 该方法按照服务器要求的签名算法生成请求签名:
|
||
/// 1. 合并基础参数和请求参数
|
||
/// 2. 过滤掉系统级参数
|
||
/// 3. 按 key 升序排序
|
||
/// 4. 拼接参数字符串
|
||
/// 5. 添加密钥并生成 MD5 签名
|
||
///
|
||
/// - Parameter requestParams: 请求特定的参数字典
|
||
mutating func generateSignature(with requestParams: [String: Any] = [:]) {
|
||
// 1. 合并基础参数和请求参数
|
||
var allParams = requestParams
|
||
|
||
// 添加基础参数到字典中
|
||
allParams["Accept-Language"] = self.acceptLanguage
|
||
allParams["os"] = self.os
|
||
allParams["osVersion"] = self.osVersion
|
||
allParams["netType"] = self.netType
|
||
allParams["ispType"] = self.ispType
|
||
allParams["channel"] = self.channel
|
||
allParams["model"] = self.model
|
||
allParams["deviceId"] = self.deviceId
|
||
allParams["appVersion"] = self.appVersion
|
||
allParams["app"] = self.app
|
||
allParams["lang"] = self.lang
|
||
if let mcc = self.mcc {
|
||
allParams["mcc"] = mcc
|
||
}
|
||
if let spType = self.spType {
|
||
allParams["spType"] = spType
|
||
}
|
||
|
||
// 2. 移除系统级参数(根据 API rule)
|
||
let systemParams = [
|
||
"Accept-Language", "pub_uid", "appVersion", "appVersionCode",
|
||
"channel", "deviceId", "ispType", "netType", "os",
|
||
"osVersion", "app", "ticket", "client", "lang", "mcc"
|
||
]
|
||
|
||
var filteredParams = allParams
|
||
for param in systemParams {
|
||
filteredParams.removeValue(forKey: param)
|
||
}
|
||
|
||
// 3. 按 key 升序排序并拼接
|
||
// 拼接格式 "key0=value0&key1=value1&key2=value2"
|
||
let sortedKeys = filteredParams.keys.sorted()
|
||
let paramString = sortedKeys.map { key in
|
||
"\(key)=\(String(describing: filteredParams[key] ?? ""))"
|
||
}.joined(separator: "&")
|
||
|
||
// 4. 添加密钥
|
||
let keyString = "key=rpbs6us1m8r2j9g6u06ff2bo18orwaya"
|
||
let finalString = paramString.isEmpty ? keyString : "\(paramString)&\(keyString)"
|
||
|
||
// 5. 生成大写 MD5 签名
|
||
self.pubSign = finalString.md5().uppercased()
|
||
}
|
||
}
|
||
|
||
// MARK: - Network Type Detector
|
||
struct NetworkTypeDetector {
|
||
static func getCurrentNetworkType() -> Int {
|
||
// WiFi = 2, 蜂窝网络 = 1
|
||
// 这里是简化实现,实际应该检测网络状态
|
||
return 2 // 默认蜂窝网络
|
||
}
|
||
}
|
||
|
||
// MARK: - Carrier Info Manager
|
||
struct CarrierInfoManager {
|
||
struct CarrierInfo {
|
||
let ispType: String
|
||
let mcc: String?
|
||
}
|
||
|
||
static func getCarrierInfo() -> CarrierInfo {
|
||
// 简化实现,实际应该获取真实运营商信息
|
||
return CarrierInfo(ispType: "65535", mcc: nil)
|
||
}
|
||
}
|
||
|
||
// MARK: - User Info Manager (for Headers)
|
||
struct UserInfoManager {
|
||
@MainActor
|
||
private static let keychain = KeychainManager.shared
|
||
|
||
// MARK: - Storage Keys
|
||
private enum StorageKeys {
|
||
static let accountModel = "account_model"
|
||
static let userInfo = "user_info"
|
||
}
|
||
|
||
// MARK: - 内存缓存
|
||
// 已迁移到 UserInfoCacheActor
|
||
private static let cacheQueue = DispatchQueue(label: "com.yana.userinfo.cache", attributes: .concurrent)
|
||
|
||
// MARK: - User ID Management (基于 AccountModel)
|
||
static func getCurrentUserId() async -> String? {
|
||
return await getAccountModel()?.uid
|
||
}
|
||
|
||
// MARK: - Access Token Management (基于 AccountModel)
|
||
static func getAccessToken() async -> String? {
|
||
return await getAccountModel()?.accessToken
|
||
}
|
||
|
||
// MARK: - Ticket Management (优先从 AccountModel 获取)
|
||
// 已迁移到 UserInfoCacheActor
|
||
|
||
static func getCurrentUserTicket() async -> String? {
|
||
// 优先从 AccountModel 获取 ticket(确保一致性)
|
||
if let accountTicket = await getAccountModel()?.ticket, !accountTicket.isEmpty {
|
||
return accountTicket
|
||
}
|
||
|
||
// 备选:从 actor 获取(用于兼容性)
|
||
return await cacheActor.getCurrentTicket()
|
||
}
|
||
|
||
static func saveTicket(_ ticket: String) async {
|
||
await cacheActor.setCurrentTicket(ticket)
|
||
debugInfoSync("💾 保存 Ticket 到内存")
|
||
}
|
||
|
||
static func clearTicket() async {
|
||
await cacheActor.clearCurrentTicket()
|
||
debugInfoSync("🗑️ 清除 Ticket")
|
||
}
|
||
|
||
// MARK: - User Info Management
|
||
static func saveUserInfo(_ userInfo: UserInfo) async {
|
||
do {
|
||
try await keychain.store(userInfo, forKey: StorageKeys.userInfo)
|
||
await cacheActor.setUserInfo(userInfo)
|
||
debugInfoSync("💾 保存用户信息成功")
|
||
} catch {
|
||
debugErrorSync("❌ 保存用户信息失败: \(error)")
|
||
}
|
||
}
|
||
|
||
static func getUserInfo() async -> UserInfo? {
|
||
// 先检查缓存
|
||
if let cached = await cacheActor.getUserInfo() {
|
||
return cached
|
||
}
|
||
// 从 Keychain 读取
|
||
do {
|
||
let userInfo = try await keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo)
|
||
await cacheActor.setUserInfo(userInfo)
|
||
return userInfo
|
||
} catch {
|
||
debugErrorSync("❌ 读取用户信息失败: \(error)")
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// MARK: - Complete Authentication Data Management
|
||
/// 保存完整的认证信息(OAuth Token + Ticket + 用户信息)
|
||
static func saveCompleteAuthenticationData(
|
||
accessToken: String,
|
||
ticket: String,
|
||
uid: Int?,
|
||
userInfo: UserInfo?
|
||
) async {
|
||
// 创建新的 AccountModel
|
||
let accountModel = AccountModel(
|
||
uid: uid != nil ? "\(uid!)" : nil,
|
||
jti: nil,
|
||
tokenType: "bearer",
|
||
refreshToken: nil,
|
||
netEaseToken: nil,
|
||
accessToken: accessToken,
|
||
expiresIn: nil,
|
||
scope: nil,
|
||
ticket: ticket
|
||
)
|
||
|
||
await saveAccountModel(accountModel)
|
||
await saveTicket(ticket)
|
||
|
||
if let userInfo = userInfo {
|
||
await saveUserInfo(userInfo)
|
||
}
|
||
|
||
debugInfoSync("✅ 完整认证信息保存成功")
|
||
}
|
||
|
||
/// 检查是否有有效的认证信息
|
||
static func hasValidAuthentication() async -> Bool {
|
||
let token = await getAccessToken()
|
||
let ticket = await getCurrentUserTicket()
|
||
return token != nil && ticket != nil
|
||
}
|
||
|
||
/// 清除所有认证信息
|
||
static func clearAllAuthenticationData() async {
|
||
await clearAccountModel()
|
||
await clearUserInfo()
|
||
await clearTicket()
|
||
|
||
debugInfoSync("🗑️ 清除所有认证信息")
|
||
}
|
||
|
||
/// 尝试恢复 Ticket(用于应用重启后)
|
||
static func restoreTicketIfNeeded() async -> Bool {
|
||
guard let _ = await getAccessToken(),
|
||
await getCurrentUserTicket() == nil else {
|
||
return false
|
||
}
|
||
|
||
debugInfoSync("🔄 尝试使用 Access Token 恢复 Ticket...")
|
||
|
||
// 这里需要注入 APIService 依赖,暂时返回 false
|
||
// 实际实现中应该调用 TicketHelper.createTicketRequest
|
||
return false
|
||
}
|
||
|
||
// MARK: - Account Model Management
|
||
/// 保存 AccountModel
|
||
/// - Parameter accountModel: 要保存的账户模型
|
||
static func saveAccountModel(_ accountModel: AccountModel) async {
|
||
do {
|
||
try await keychain.store(accountModel, forKey: StorageKeys.accountModel)
|
||
await cacheActor.setAccountModel(accountModel)
|
||
|
||
// 同步更新 ticket 到内存
|
||
if let ticket = accountModel.ticket {
|
||
await saveTicket(ticket)
|
||
}
|
||
|
||
debugInfoSync("💾 AccountModel 保存成功")
|
||
} catch {
|
||
debugErrorSync("❌ AccountModel 保存失败: \(error)")
|
||
}
|
||
}
|
||
|
||
/// 获取 AccountModel
|
||
/// - Returns: 存储的账户模型,如果不存在或解析失败返回 nil
|
||
static func getAccountModel() async -> AccountModel? {
|
||
// 先检查缓存
|
||
if let cached = await cacheActor.getAccountModel() {
|
||
return cached
|
||
}
|
||
// 从 Keychain 读取
|
||
do {
|
||
let accountModel = try await keychain.retrieve(
|
||
AccountModel.self,
|
||
forKey: StorageKeys.accountModel
|
||
)
|
||
await cacheActor.setAccountModel(accountModel)
|
||
return accountModel
|
||
} catch {
|
||
debugErrorSync("❌ 读取 AccountModel 失败: \(error)")
|
||
return nil
|
||
}
|
||
}
|
||
|
||
/// 更新 AccountModel 中的 ticket
|
||
/// - Parameter ticket: 新的票据
|
||
static func updateAccountModelTicket(_ ticket: String) async {
|
||
guard var accountModel = await getAccountModel() else {
|
||
debugErrorSync("❌ 无法更新 ticket:AccountModel 不存在")
|
||
return
|
||
}
|
||
|
||
accountModel = AccountModel(
|
||
uid: accountModel.uid,
|
||
jti: accountModel.jti,
|
||
tokenType: accountModel.tokenType,
|
||
refreshToken: accountModel.refreshToken,
|
||
netEaseToken: accountModel.netEaseToken,
|
||
accessToken: accountModel.accessToken,
|
||
expiresIn: accountModel.expiresIn,
|
||
scope: accountModel.scope,
|
||
ticket: ticket
|
||
)
|
||
|
||
await saveAccountModel(accountModel)
|
||
await saveTicket(ticket) // 同时更新内存中的 ticket
|
||
}
|
||
|
||
/// 检查是否有有效的 AccountModel
|
||
/// - Returns: 是否存在有效的账户模型
|
||
static func hasValidAccountModel() async -> Bool {
|
||
guard let accountModel = await getAccountModel() else {
|
||
return false
|
||
}
|
||
return accountModel.hasValidAuthentication
|
||
}
|
||
|
||
/// 清除 AccountModel
|
||
static func clearAccountModel() async {
|
||
do {
|
||
try await keychain.delete(forKey: StorageKeys.accountModel)
|
||
await cacheActor.clearAccountModel()
|
||
debugInfoSync("🗑️ AccountModel 已清除")
|
||
} catch {
|
||
debugErrorSync("❌ 清除 AccountModel 失败: \(error)")
|
||
}
|
||
}
|
||
|
||
/// 清除用户信息
|
||
static func clearUserInfo() async {
|
||
do {
|
||
try await keychain.delete(forKey: StorageKeys.userInfo)
|
||
await cacheActor.clearUserInfo()
|
||
debugInfoSync("🗑️ UserInfo 已清除")
|
||
} catch {
|
||
debugErrorSync("❌ 清除 UserInfo 失败: \(error)")
|
||
}
|
||
}
|
||
|
||
/// 清除所有缓存(用于测试或重置)
|
||
static func clearAllCache() async {
|
||
await cacheActor.clearAccountModel()
|
||
await cacheActor.clearUserInfo()
|
||
debugInfoSync("🗑️ 清除所有内存缓存")
|
||
}
|
||
|
||
/// 预加载缓存(提升首次访问性能)
|
||
static func preloadCache() async {
|
||
await cacheActor.setAccountModel(await getAccountModel())
|
||
await cacheActor.setUserInfo(await getUserInfo())
|
||
debugInfoSync("🚀 缓存预加载完成")
|
||
}
|
||
|
||
// MARK: - Authentication Validation
|
||
|
||
/// 检查当前认证状态是否有效
|
||
/// - Returns: 认证状态结果
|
||
static func checkAuthenticationStatus() async -> AuthenticationStatus {
|
||
guard let accountModel = await getAccountModel() else {
|
||
debugInfoSync("🔍 认证检查:未找到 AccountModel")
|
||
return .notFound
|
||
}
|
||
guard let uid = accountModel.uid, !uid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||
debugInfoSync("🔍 认证检查:uid 无效")
|
||
return .invalid
|
||
}
|
||
guard let ticket = accountModel.ticket, !ticket.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||
debugInfoSync("🔍 认证检查:ticket 无效")
|
||
return .invalid
|
||
}
|
||
guard let accessToken = accountModel.accessToken, !accessToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||
debugInfoSync("🔍 认证检查:access token 无效")
|
||
return .invalid
|
||
}
|
||
debugInfoSync("🔍 认证检查:认证有效 - uid: \(uid), ticket: \(ticket.prefix(10))...")
|
||
return .valid
|
||
}
|
||
|
||
/// 认证状态枚举
|
||
enum AuthenticationStatus: Equatable {
|
||
case valid // 认证有效,可以自动登录
|
||
case invalid // 认证信息不完整或无效
|
||
case notFound // 未找到认证信息
|
||
|
||
var description: String {
|
||
switch self {
|
||
case .valid:
|
||
return "认证有效"
|
||
case .invalid:
|
||
return "认证无效"
|
||
case .notFound:
|
||
return "未找到认证信息"
|
||
}
|
||
}
|
||
|
||
/// 是否可以自动登录
|
||
var canAutoLogin: Bool {
|
||
return self == .valid
|
||
}
|
||
}
|
||
|
||
// MARK: - Testing and Debugging
|
||
|
||
/// 测试认证 header 功能(仅用于调试)
|
||
/// 模拟用户登录状态并验证 header 添加逻辑
|
||
static func testAuthenticationHeaders() async {
|
||
#if DEBUG
|
||
debugInfoSync("\n🧪 开始测试认证 header 功能")
|
||
|
||
// 测试1:未登录状态
|
||
debugInfoSync("📝 测试1:未登录状态")
|
||
await clearAllAuthenticationData()
|
||
let headers1 = await APIConfiguration.defaultHeaders()
|
||
let hasAuthHeaders1 = headers1.keys.contains("pub_uid") || headers1.keys.contains("pub_ticket")
|
||
debugInfoSync(" 认证 headers 存在: \(hasAuthHeaders1) (应该为 false)")
|
||
|
||
// 测试2:模拟登录状态
|
||
debugInfoSync("📝 测试2:模拟登录状态")
|
||
let testAccount = AccountModel(
|
||
uid: "12345",
|
||
jti: "test-jti",
|
||
tokenType: "bearer",
|
||
refreshToken: nil,
|
||
netEaseToken: nil,
|
||
accessToken: "test-access-token",
|
||
expiresIn: 3600,
|
||
scope: "read write",
|
||
ticket: "test-ticket-12345678901234567890"
|
||
)
|
||
await saveAccountModel(testAccount)
|
||
|
||
let headers2 = await APIConfiguration.defaultHeaders()
|
||
let hasUid = headers2["pub_uid"] == "12345"
|
||
let hasTicket = headers2["pub_ticket"] == "test-ticket-12345678901234567890"
|
||
debugInfoSync(" pub_uid 正确: \(hasUid) (应该为 true)")
|
||
debugInfoSync(" pub_ticket 正确: \(hasTicket) (应该为 true)")
|
||
|
||
// 测试3:清理测试数据
|
||
debugInfoSync("📝 测试3:清理测试数据")
|
||
await clearAllAuthenticationData()
|
||
debugInfoSync("✅ 认证 header 测试完成\n")
|
||
#endif
|
||
}
|
||
}
|
||
|
||
// MARK: - User Info Cache Actor
|
||
actor UserInfoCacheActor {
|
||
private var accountModelCache: AccountModel?
|
||
private var userInfoCache: UserInfo?
|
||
private var currentTicket: String?
|
||
|
||
// AccountModel
|
||
func getAccountModel() -> AccountModel? { accountModelCache }
|
||
func setAccountModel(_ model: AccountModel?) { accountModelCache = model }
|
||
func clearAccountModel() { accountModelCache = nil }
|
||
|
||
// UserInfo
|
||
func getUserInfo() -> UserInfo? { userInfoCache }
|
||
func setUserInfo(_ info: UserInfo?) { userInfoCache = info }
|
||
func clearUserInfo() { userInfoCache = nil }
|
||
|
||
// Ticket
|
||
func getCurrentTicket() -> String? { currentTicket }
|
||
func setCurrentTicket(_ ticket: String?) { currentTicket = ticket }
|
||
func clearCurrentTicket() { currentTicket = nil }
|
||
}
|
||
|
||
extension UserInfoManager {
|
||
static let cacheActor = UserInfoCacheActor()
|
||
}
|
||
|
||
// MARK: - API Request Protocol
|
||
|
||
/// API 请求协议
|
||
///
|
||
/// 定义了所有 API 请求必须实现的接口,提供了类型安全的请求定义方式。
|
||
/// 每个具体的 API 请求都应该实现这个协议。
|
||
///
|
||
/// 协议要求:
|
||
/// - Response: 关联类型,定义响应数据的类型,必须 Sendable
|
||
/// - endpoint: API 端点路径
|
||
/// - method: HTTP 请求方法
|
||
/// - 可选的查询参数、请求体参数、请求头等
|
||
///
|
||
/// 使用示例:
|
||
/// ```swift
|
||
/// struct LoginRequest: APIRequestProtocol {
|
||
/// typealias Response = LoginResponse
|
||
/// let endpoint = "/auth/login"
|
||
/// let method: HTTPMethod = .POST
|
||
/// // ... 其他属性
|
||
/// }
|
||
/// ```
|
||
protocol APIRequestProtocol: Sendable {
|
||
associatedtype Response: Codable & Sendable
|
||
|
||
var endpoint: String { get }
|
||
var method: HTTPMethod { get }
|
||
var queryParameters: [String: String]? { get }
|
||
var bodyParameters: [String: Any]? { get }
|
||
var headers: [String: String]? { get }
|
||
var customHeaders: [String: String]? { get } // 新增:自定义请求头
|
||
var timeout: TimeInterval { get }
|
||
var includeBaseParameters: Bool { get }
|
||
|
||
// MARK: - Loading Configuration
|
||
/// 是否显示 loading 动画,默认 true
|
||
var shouldShowLoading: Bool { get }
|
||
/// 是否显示错误信息,默认 true
|
||
var shouldShowError: Bool { get }
|
||
}
|
||
|
||
extension APIRequestProtocol {
|
||
var timeout: TimeInterval { 30.0 }
|
||
var includeBaseParameters: Bool { true }
|
||
var headers: [String: String]? { nil }
|
||
var customHeaders: [String: String]? { nil } // 新增:默认实现
|
||
|
||
// MARK: - Loading Configuration Defaults
|
||
var shouldShowLoading: Bool { true }
|
||
var shouldShowError: Bool { true }
|
||
}
|
||
|
||
// MARK: - Generic API Response
|
||
struct APIResponse<T: Codable>: Codable {
|
||
let data: T?
|
||
let status: String?
|
||
let message: String?
|
||
let code: Int?
|
||
}
|
||
|
||
// 注意:String+MD5 扩展已移至 Utils/Extensions/String+MD5.swift
|
||
|
||
// MARK: - 腾讯云 COS Token 相关模型
|
||
|
||
/// 腾讯云 COS Token 请求模型
|
||
struct TcTokenRequest: APIRequestProtocol {
|
||
typealias Response = TcTokenResponse
|
||
|
||
let endpoint: String = APIEndpoint.tcToken.path
|
||
let method: HTTPMethod = .GET
|
||
let queryParameters: [String: String]? = nil
|
||
var bodyParameters: [String: Any]? { nil }
|
||
let timeout: TimeInterval = 30.0
|
||
let includeBaseParameters: Bool = true
|
||
let shouldShowLoading: Bool = false // 不显示 loading,避免影响用户体验
|
||
let shouldShowError: Bool = false // 不显示错误,静默处理
|
||
}
|
||
|
||
/// 腾讯云 COS Token 响应模型
|
||
struct TcTokenResponse: Codable, Equatable {
|
||
let code: Int
|
||
let message: String
|
||
let data: TcTokenData?
|
||
let timestamp: Int64
|
||
}
|
||
|
||
/// 腾讯云 COS Token 数据模型
|
||
/// 包含完整的腾讯云 COS 配置信息
|
||
struct TcTokenData: Codable, Equatable {
|
||
let bucket: String // 存储桶名称
|
||
let sessionToken: String // 临时会话令牌
|
||
let region: String // 地域
|
||
let customDomain: String // 自定义域名
|
||
let accelerate: Bool // 是否启用加速
|
||
let appId: String // 应用 ID
|
||
let secretKey: String // 临时密钥
|
||
let expireTime: Int64 // 过期时间戳
|
||
let startTime: Int64 // 开始时间戳
|
||
let secretId: String // 临时密钥 ID
|
||
|
||
/// 检查 Token 是否已过期
|
||
var isExpired: Bool {
|
||
let currentTime = Int64(Date().timeIntervalSince1970)
|
||
return currentTime >= expireTime
|
||
}
|
||
|
||
/// 获取过期时间
|
||
var expirationDate: Date {
|
||
return Date(timeIntervalSince1970: TimeInterval(expireTime))
|
||
}
|
||
|
||
/// 获取开始时间
|
||
var startDate: Date {
|
||
return Date(timeIntervalSince1970: TimeInterval(startTime))
|
||
}
|
||
|
||
/// 获取剩余有效时间(秒)
|
||
var remainingTime: Int64 {
|
||
let currentTime = Int64(Date().timeIntervalSince1970)
|
||
return max(0, expireTime - currentTime)
|
||
}
|
||
}
|
||
|