
- 注释掉Podfile中的Alamofire依赖,更新Podfile.lock以反映更改。 - 在yana/APIs/API-README.md中新增自动认证Header机制的详细文档,描述其工作原理、实现细节及最佳实践。 - 在yana/yanaApp.swift中将print语句替换为debugInfo以增强调试信息的输出。 - 在API相关文件中实现用户认证状态检查和相关header的自动添加逻辑,提升API请求的安全性和用户体验。 - 更新多个文件中的日志输出,确保在DEBUG模式下提供详细的调试信息。
661 lines
22 KiB
Swift
661 lines
22 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"
|
||
}
|
||
|
||
init() {
|
||
// 获取系统首选语言
|
||
let preferredLanguage = Locale.current.languageCode ?? "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 {
|
||
private static let keychain = KeychainManager.shared
|
||
|
||
// MARK: - Storage Keys
|
||
private enum StorageKeys {
|
||
static let accountModel = "account_model"
|
||
static let userInfo = "user_info"
|
||
}
|
||
|
||
// MARK: - 内存缓存
|
||
private static var accountModelCache: AccountModel?
|
||
private static var userInfoCache: UserInfo?
|
||
private static let cacheQueue = DispatchQueue(label: "com.yana.userinfo.cache", attributes: .concurrent)
|
||
|
||
// MARK: - User ID Management (基于 AccountModel)
|
||
static func getCurrentUserId() -> String? {
|
||
return getAccountModel()?.uid
|
||
}
|
||
|
||
// MARK: - Access Token Management (基于 AccountModel)
|
||
static func getAccessToken() -> String? {
|
||
return getAccountModel()?.accessToken
|
||
}
|
||
|
||
// MARK: - Ticket Management (优先从 AccountModel 获取)
|
||
private static var currentTicket: String?
|
||
|
||
static func getCurrentUserTicket() -> String? {
|
||
// 优先从 AccountModel 获取 ticket(确保一致性)
|
||
if let accountTicket = getAccountModel()?.ticket, !accountTicket.isEmpty {
|
||
return accountTicket
|
||
}
|
||
|
||
// 备选:从内存获取(用于兼容性)
|
||
return currentTicket
|
||
}
|
||
|
||
static func saveTicket(_ ticket: String) {
|
||
currentTicket = ticket
|
||
debugInfo("💾 保存 Ticket 到内存")
|
||
}
|
||
|
||
static func clearTicket() {
|
||
currentTicket = nil
|
||
debugInfo("🗑️ 清除 Ticket")
|
||
}
|
||
|
||
// MARK: - User Info Management
|
||
static func saveUserInfo(_ userInfo: UserInfo) {
|
||
cacheQueue.async(flags: .barrier) {
|
||
do {
|
||
try keychain.store(userInfo, forKey: StorageKeys.userInfo)
|
||
userInfoCache = userInfo
|
||
debugInfo("💾 保存用户信息成功")
|
||
} catch {
|
||
debugError("❌ 保存用户信息失败: \(error)")
|
||
}
|
||
}
|
||
}
|
||
|
||
static func getUserInfo() -> UserInfo? {
|
||
return cacheQueue.sync {
|
||
// 先检查缓存
|
||
if let cached = userInfoCache {
|
||
return cached
|
||
}
|
||
|
||
// 从 Keychain 读取
|
||
do {
|
||
let userInfo = try keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo)
|
||
userInfoCache = userInfo
|
||
return userInfo
|
||
} catch {
|
||
debugError("❌ 读取用户信息失败: \(error)")
|
||
return nil
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Complete Authentication Data Management
|
||
/// 保存完整的认证信息(OAuth Token + Ticket + 用户信息)
|
||
static func saveCompleteAuthenticationData(
|
||
accessToken: String,
|
||
ticket: String,
|
||
uid: Int?,
|
||
userInfo: UserInfo?
|
||
) {
|
||
// 创建新的 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
|
||
)
|
||
|
||
saveAccountModel(accountModel)
|
||
saveTicket(ticket)
|
||
|
||
if let userInfo = userInfo {
|
||
saveUserInfo(userInfo)
|
||
}
|
||
|
||
debugInfo("✅ 完整认证信息保存成功")
|
||
}
|
||
|
||
/// 检查是否有有效的认证信息
|
||
static func hasValidAuthentication() -> Bool {
|
||
return getAccessToken() != nil && getCurrentUserTicket() != nil
|
||
}
|
||
|
||
/// 清除所有认证信息
|
||
static func clearAllAuthenticationData() {
|
||
clearAccountModel()
|
||
clearUserInfo()
|
||
clearTicket()
|
||
|
||
debugInfo("🗑️ 清除所有认证信息")
|
||
}
|
||
|
||
/// 尝试恢复 Ticket(用于应用重启后)
|
||
static func restoreTicketIfNeeded() async -> Bool {
|
||
guard let accessToken = getAccessToken(),
|
||
getCurrentUserTicket() == nil else {
|
||
return false
|
||
}
|
||
|
||
debugInfo("🔄 尝试使用 Access Token 恢复 Ticket...")
|
||
|
||
// 这里需要注入 APIService 依赖,暂时返回 false
|
||
// 实际实现中应该调用 TicketHelper.createTicketRequest
|
||
return false
|
||
}
|
||
|
||
// MARK: - Account Model Management
|
||
/// 保存 AccountModel
|
||
/// - Parameter accountModel: 要保存的账户模型
|
||
static func saveAccountModel(_ accountModel: AccountModel) {
|
||
cacheQueue.async(flags: .barrier) {
|
||
do {
|
||
try keychain.store(accountModel, forKey: StorageKeys.accountModel)
|
||
accountModelCache = accountModel
|
||
|
||
// 同步更新 ticket 到内存
|
||
if let ticket = accountModel.ticket {
|
||
saveTicket(ticket)
|
||
}
|
||
|
||
debugInfo("💾 AccountModel 保存成功")
|
||
} catch {
|
||
debugError("❌ AccountModel 保存失败: \(error)")
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 获取 AccountModel
|
||
/// - Returns: 存储的账户模型,如果不存在或解析失败返回 nil
|
||
static func getAccountModel() -> AccountModel? {
|
||
return cacheQueue.sync {
|
||
// 先检查缓存
|
||
if let cached = accountModelCache {
|
||
return cached
|
||
}
|
||
|
||
// 从 Keychain 读取
|
||
do {
|
||
let accountModel = try keychain.retrieve(AccountModel.self, forKey: StorageKeys.accountModel)
|
||
accountModelCache = accountModel
|
||
return accountModel
|
||
} catch {
|
||
debugError("❌ 读取 AccountModel 失败: \(error)")
|
||
return nil
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 更新 AccountModel 中的 ticket
|
||
/// - Parameter ticket: 新的票据
|
||
static func updateAccountModelTicket(_ ticket: String) {
|
||
guard var accountModel = getAccountModel() else {
|
||
debugError("❌ 无法更新 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
|
||
)
|
||
|
||
saveAccountModel(accountModel)
|
||
saveTicket(ticket) // 同时更新内存中的 ticket
|
||
}
|
||
|
||
/// 检查是否有有效的 AccountModel
|
||
/// - Returns: 是否存在有效的账户模型
|
||
static func hasValidAccountModel() -> Bool {
|
||
guard let accountModel = getAccountModel() else {
|
||
return false
|
||
}
|
||
return accountModel.hasValidAuthentication
|
||
}
|
||
|
||
/// 清除 AccountModel
|
||
static func clearAccountModel() {
|
||
cacheQueue.async(flags: .barrier) {
|
||
do {
|
||
try keychain.delete(forKey: StorageKeys.accountModel)
|
||
accountModelCache = nil
|
||
debugInfo("🗑️ AccountModel 已清除")
|
||
} catch {
|
||
debugError("❌ 清除 AccountModel 失败: \(error)")
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 清除用户信息
|
||
static func clearUserInfo() {
|
||
cacheQueue.async(flags: .barrier) {
|
||
do {
|
||
try keychain.delete(forKey: StorageKeys.userInfo)
|
||
userInfoCache = nil
|
||
debugInfo("🗑️ UserInfo 已清除")
|
||
} catch {
|
||
debugError("❌ 清除 UserInfo 失败: \(error)")
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 清除所有缓存(用于测试或重置)
|
||
static func clearAllCache() {
|
||
cacheQueue.async(flags: .barrier) {
|
||
accountModelCache = nil
|
||
userInfoCache = nil
|
||
debugInfo("🗑️ 清除所有内存缓存")
|
||
}
|
||
}
|
||
|
||
/// 预加载缓存(提升首次访问性能)
|
||
static func preloadCache() {
|
||
cacheQueue.async {
|
||
// 预加载 AccountModel
|
||
_ = getAccountModel()
|
||
// 预加载 UserInfo
|
||
_ = getUserInfo()
|
||
debugInfo("🚀 缓存预加载完成")
|
||
}
|
||
}
|
||
|
||
// MARK: - Authentication Validation
|
||
|
||
/// 检查当前认证状态是否有效
|
||
/// - Returns: 认证状态结果
|
||
static func checkAuthenticationStatus() -> AuthenticationStatus {
|
||
return cacheQueue.sync {
|
||
guard let accountModel = getAccountModel() else {
|
||
debugInfo("🔍 认证检查:未找到 AccountModel")
|
||
return .notFound
|
||
}
|
||
|
||
// 检查 uid 是否有效
|
||
guard let uid = accountModel.uid, !uid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||
debugInfo("🔍 认证检查:uid 无效")
|
||
return .invalid
|
||
}
|
||
|
||
// 检查 ticket 是否有效
|
||
guard let ticket = accountModel.ticket, !ticket.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||
debugInfo("🔍 认证检查:ticket 无效")
|
||
return .invalid
|
||
}
|
||
|
||
// 可选:检查 access token 是否有效
|
||
guard let accessToken = accountModel.accessToken, !accessToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||
debugInfo("🔍 认证检查:access token 无效")
|
||
return .invalid
|
||
}
|
||
|
||
debugInfo("🔍 认证检查:认证有效 - 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() {
|
||
#if DEBUG
|
||
debugInfo("\n🧪 开始测试认证 header 功能")
|
||
|
||
// 测试1:未登录状态
|
||
debugInfo("📝 测试1:未登录状态")
|
||
clearAllAuthenticationData()
|
||
let headers1 = APIConfiguration.defaultHeaders
|
||
let hasAuthHeaders1 = headers1.keys.contains("pub_uid") || headers1.keys.contains("pub_ticket")
|
||
debugInfo(" 认证 headers 存在: \(hasAuthHeaders1) (应该为 false)")
|
||
|
||
// 测试2:模拟登录状态
|
||
debugInfo("📝 测试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"
|
||
)
|
||
saveAccountModel(testAccount)
|
||
|
||
let headers2 = APIConfiguration.defaultHeaders
|
||
let hasUid = headers2["pub_uid"] == "12345"
|
||
let hasTicket = headers2["pub_ticket"] == "test-ticket-12345678901234567890"
|
||
debugInfo(" pub_uid 正确: \(hasUid) (应该为 true)")
|
||
debugInfo(" pub_ticket 正确: \(hasTicket) (应该为 true)")
|
||
|
||
// 测试3:清理测试数据
|
||
debugInfo("📝 测试3:清理测试数据")
|
||
clearAllAuthenticationData()
|
||
debugInfo("✅ 认证 header 测试完成\n")
|
||
#endif
|
||
}
|
||
}
|
||
|
||
// MARK: - API Request Protocol
|
||
|
||
/// API 请求协议
|
||
///
|
||
/// 定义了所有 API 请求必须实现的接口,提供了类型安全的请求定义方式。
|
||
/// 每个具体的 API 请求都应该实现这个协议。
|
||
///
|
||
/// 协议要求:
|
||
/// - Response: 关联类型,定义响应数据的类型
|
||
/// - endpoint: API 端点路径
|
||
/// - method: HTTP 请求方法
|
||
/// - 可选的查询参数、请求体参数、请求头等
|
||
///
|
||
/// 使用示例:
|
||
/// ```swift
|
||
/// struct LoginRequest: APIRequestProtocol {
|
||
/// typealias Response = LoginResponse
|
||
/// let endpoint = "/auth/login"
|
||
/// let method: HTTPMethod = .POST
|
||
/// // ... 其他属性
|
||
/// }
|
||
/// ```
|
||
protocol APIRequestProtocol {
|
||
associatedtype Response: Codable
|
||
|
||
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
|
||
|