
- 在Package.swift中注释掉旧的swift-composable-architecture依赖,并添加swift-case-paths依赖。 - 在Podfile中将iOS平台版本更新至16.0,并移除QCloudCOSXML/Transfer依赖,改为使用QCloudCOSXML。 - 更新Podfile.lock以反映依赖变更,确保项目依赖的准确性。 - 新增架构分析需求文档,明确项目架构评估和改进建议。 - 在多个文件中实现async/await语法,提升异步操作的可读性和性能。 - 更新日志输出方法,确保在调试模式下提供一致的调试信息。 - 优化多个视图组件,提升用户体验和代码可维护性。
370 lines
15 KiB
Swift
370 lines
15 KiB
Swift
import Foundation
|
||
import ComposableArchitecture
|
||
|
||
// MARK: - API Service Protocol
|
||
|
||
/// API 服务协议,定义了网络请求的核心接口
|
||
///
|
||
/// 该协议支持泛型请求,可以处理任何符合 `APIRequestProtocol` 的请求类型
|
||
/// 并返回对应的响应类型。这种设计提供了类型安全和灵活性。
|
||
///
|
||
/// 使用示例:
|
||
/// ```swift
|
||
/// let apiService: APIServiceProtocol = LiveAPIService()
|
||
/// let request = ConfigRequest()
|
||
/// let response = try await apiService.request(request)
|
||
/// ```
|
||
protocol APIServiceProtocol: Sendable {
|
||
/// 发起网络请求
|
||
/// - Parameter request: 符合 APIRequestProtocol 的请求对象
|
||
/// - Returns: 请求对应的响应对象
|
||
/// - Throws: APIError 或其他网络相关错误
|
||
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response
|
||
}
|
||
|
||
// MARK: - Live API Service Implementation
|
||
|
||
/// 实际的 API 服务实现类
|
||
///
|
||
/// 该类负责处理真实的网络请求,包括:
|
||
/// - URL 构建和参数处理
|
||
/// - 请求头设置和认证
|
||
/// - 请求体编码和签名生成
|
||
/// - 响应解析和错误处理
|
||
/// - 日志记录和性能监控
|
||
///
|
||
/// 特性:
|
||
/// - 支持 GET/POST/PUT/DELETE 等 HTTP 方法
|
||
/// - 自动添加基础参数和安全签名
|
||
/// - 完整的错误处理和重试机制
|
||
/// - 详细的请求/响应日志记录
|
||
/// - 防止资源超限的保护机制
|
||
struct LiveAPIService: APIServiceProtocol, Sendable {
|
||
private let session: URLSession
|
||
private let baseURL: String
|
||
// 缓存主 actor 配置,避免并发隔离问题
|
||
private static let cachedBaseURL: String = APIConfiguration.baseURL
|
||
private static let cachedTimeout: TimeInterval = APIConfiguration.timeout
|
||
|
||
/// 初始化 API 服务
|
||
/// - Parameter baseURL: API 服务器基础 URL,默认使用静态缓存
|
||
init(baseURL: String = LiveAPIService.cachedBaseURL) {
|
||
self.baseURL = baseURL
|
||
|
||
// 配置 URLSession 以防止资源超限问题
|
||
let config = URLSessionConfiguration.default
|
||
config.timeoutIntervalForRequest = LiveAPIService.cachedTimeout
|
||
config.timeoutIntervalForResource = LiveAPIService.cachedTimeout * 2
|
||
config.waitsForConnectivity = true
|
||
config.allowsCellularAccess = true
|
||
|
||
// 设置数据大小限制
|
||
config.urlCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
|
||
|
||
self.session = URLSession(configuration: config)
|
||
}
|
||
|
||
/// 发起网络请求的核心方法
|
||
///
|
||
/// 该方法处理完整的请求生命周期:
|
||
/// 1. 构建请求 URL 和参数
|
||
/// 2. 设置请求头和认证信息
|
||
/// 3. 处理请求体和签名生成
|
||
/// 4. 发起网络请求
|
||
/// 5. 解析响应数据
|
||
/// 6. 记录日志和性能指标
|
||
///
|
||
/// - Parameter request: 符合 APIRequestProtocol 的请求对象
|
||
/// - Returns: 解析后的响应对象
|
||
/// - Throws: APIError 包含详细的错误信息
|
||
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response {
|
||
let startTime = Date()
|
||
|
||
// 开始 Loading 管理
|
||
let loadingId = await APILoadingManager.shared.startLoading(
|
||
shouldShowLoading: request.shouldShowLoading,
|
||
shouldShowError: request.shouldShowError
|
||
)
|
||
|
||
// 构建 URL
|
||
guard let url = await buildURL(for: request) else {
|
||
await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.invalidURL.localizedDescription)
|
||
throw APIError.invalidURL
|
||
}
|
||
|
||
// 构建 URLRequest
|
||
var urlRequest = URLRequest(url: url)
|
||
urlRequest.httpMethod = request.method.rawValue
|
||
urlRequest.timeoutInterval = request.timeout
|
||
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||
|
||
// 设置请求头(必须 await 获取)
|
||
var headers = await APIConfiguration.defaultHeaders()
|
||
if let customHeaders = request.headers {
|
||
headers.merge(customHeaders) { _, new in new }
|
||
}
|
||
|
||
// 添加自定义请求头支持
|
||
if let additionalHeaders = request.customHeaders {
|
||
headers.merge(additionalHeaders) { _, new in new }
|
||
}
|
||
|
||
for (key, value) in headers {
|
||
urlRequest.setValue(value, forHTTPHeaderField: key)
|
||
}
|
||
|
||
// 处理请求体
|
||
var requestBody: Data? = nil
|
||
if request.method != .GET, let bodyParams = request.bodyParameters {
|
||
do {
|
||
var finalBody = bodyParams
|
||
|
||
// 如果需要包含基础参数,则先合并所有参数,再统一生成签名
|
||
if request.includeBaseParameters {
|
||
// 第一步:创建基础参数实例(不包含签名)
|
||
var baseParams = await BaseRequest()
|
||
|
||
// 第二步:基于所有参数(bodyParams + 基础参数)统一生成签名
|
||
baseParams.generateSignature(with: bodyParams)
|
||
|
||
// 第三步:将包含正确签名的基础参数合并到最终请求体
|
||
let baseDict = try baseParams.toDictionary()
|
||
finalBody.merge(baseDict) { _, new in new } // 基础参数(包括签名)优先
|
||
debugInfoSync("🔐 签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)")
|
||
}
|
||
|
||
requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: [])
|
||
urlRequest.httpBody = requestBody
|
||
// urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||
if let httpBody = urlRequest.httpBody,
|
||
let bodyString = String(data: httpBody, encoding: .utf8) {
|
||
debugInfoSync("HTTP Body: \(bodyString)")
|
||
}
|
||
} catch {
|
||
let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)")
|
||
await APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription)
|
||
throw encodingError
|
||
}
|
||
}
|
||
|
||
// 记录请求日志,传递完整的 headers 信息
|
||
await APILogger
|
||
.logRequest(request, url: url, body: requestBody, finalHeaders: headers)
|
||
|
||
do {
|
||
// 发起请求
|
||
let (data, response) = try await session.data(for: urlRequest)
|
||
let duration = Date().timeIntervalSince(startTime)
|
||
|
||
// 检查响应
|
||
guard let httpResponse = response as? HTTPURLResponse else {
|
||
let networkError = APIError.networkError("无效的响应类型")
|
||
await APILoadingManager.shared.setError(loadingId, errorMessage: networkError.localizedDescription)
|
||
throw networkError
|
||
}
|
||
|
||
// 检查数据大小
|
||
if data.count > APIConfiguration.maxDataSize {
|
||
await APILogger
|
||
.logError(APIError.resourceTooLarge, url: url, duration: duration)
|
||
await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription)
|
||
throw APIError.resourceTooLarge
|
||
}
|
||
|
||
// 记录响应日志
|
||
await APILogger
|
||
.logResponse(data: data, response: httpResponse, duration: duration)
|
||
|
||
// 性能警告
|
||
await APILogger.logPerformanceWarning(duration: duration)
|
||
|
||
// 检查 HTTP 状态码
|
||
guard 200...299 ~= httpResponse.statusCode else {
|
||
let errorMessage = extractErrorMessage(from: data)
|
||
let httpError = APIError.httpError(statusCode: httpResponse.statusCode, message: errorMessage)
|
||
await APILoadingManager.shared.setError(loadingId, errorMessage: httpError.localizedDescription)
|
||
throw httpError
|
||
}
|
||
|
||
// 检查数据是否为空
|
||
guard !data.isEmpty else {
|
||
await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.noData.localizedDescription)
|
||
throw APIError.noData
|
||
}
|
||
|
||
// 解析响应数据
|
||
do {
|
||
let decoder = JSONDecoder()
|
||
let decodedResponse = try decoder.decode(T.Response.self, from: data)
|
||
await APILogger.logDecodedResponse(decodedResponse, type: T.Response.self)
|
||
|
||
// 请求成功,完成 loading
|
||
await APILoadingManager.shared.finishLoading(loadingId)
|
||
|
||
return decodedResponse
|
||
} catch {
|
||
let decodingError = APIError.decodingError("响应解析失败: \(error.localizedDescription)")
|
||
await APILoadingManager.shared.setError(loadingId, errorMessage: decodingError.localizedDescription)
|
||
throw decodingError
|
||
}
|
||
|
||
} catch let error as APIError {
|
||
let duration = Date().timeIntervalSince(startTime)
|
||
await APILogger.logError(error, url: url, duration: duration)
|
||
await APILoadingManager.shared.setError(loadingId, errorMessage: error.localizedDescription)
|
||
throw error
|
||
} catch {
|
||
let duration = Date().timeIntervalSince(startTime)
|
||
let apiError = mapSystemError(error)
|
||
await APILogger.logError(apiError, url: url, duration: duration)
|
||
await APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription)
|
||
throw apiError
|
||
}
|
||
}
|
||
|
||
// MARK: - Private Helper Methods
|
||
|
||
/// 构建完整的请求 URL
|
||
///
|
||
/// 该方法负责:
|
||
/// - 拼接基础 URL 和端点路径
|
||
/// - 处理查询参数
|
||
/// - 为 GET 请求添加基础参数和签名
|
||
///
|
||
/// - Parameter request: API 请求对象
|
||
/// - Returns: 构建完成的 URL,如果构建失败则返回 nil
|
||
@MainActor private func buildURL<T: APIRequestProtocol>(for request: T) -> URL? {
|
||
guard var urlComponents = URLComponents(string: baseURL + request.endpoint) else {
|
||
return nil
|
||
}
|
||
|
||
// 处理查询参数
|
||
var queryItems: [URLQueryItem] = []
|
||
|
||
// 对于 GET 请求,将基础参数添加到查询参数中
|
||
if request.method == .GET && request.includeBaseParameters {
|
||
do {
|
||
// 第一步:创建基础参数实例(不包含签名)
|
||
var baseParams = BaseRequest()
|
||
|
||
// 第二步:基于所有参数(queryParams + 基础参数)统一生成签名
|
||
let queryParamsDict = request.queryParameters ?? [:]
|
||
baseParams.generateSignature(with: queryParamsDict)
|
||
|
||
// 第三步:将包含正确签名的基础参数添加到查询参数
|
||
let baseDict = try baseParams.toDictionary()
|
||
for (key, value) in baseDict {
|
||
queryItems.append(URLQueryItem(name: key, value: "\(value)"))
|
||
}
|
||
|
||
debugInfoSync("🔐 GET请求签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)")
|
||
} catch {
|
||
debugWarnSync("警告:无法添加基础参数到查询字符串")
|
||
}
|
||
}
|
||
|
||
// 添加自定义查询参数
|
||
if let customParams = request.queryParameters {
|
||
for (key, value) in customParams {
|
||
queryItems.append(URLQueryItem(name: key, value: value))
|
||
}
|
||
}
|
||
|
||
if !queryItems.isEmpty {
|
||
urlComponents.queryItems = queryItems
|
||
}
|
||
|
||
return urlComponents.url
|
||
}
|
||
|
||
/// 从响应数据中提取错误消息
|
||
///
|
||
/// 尝试从 JSON 响应中提取错误信息,支持多种常见的错误字段格式
|
||
///
|
||
/// - Parameter data: 响应数据
|
||
/// - Returns: 提取到的错误消息,如果没有找到则返回 nil
|
||
private func extractErrorMessage(from data: Data) -> String? {
|
||
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||
return nil
|
||
}
|
||
|
||
// 尝试多种可能的错误消息字段
|
||
if let message = json["message"] as? String {
|
||
return message
|
||
} else if let error = json["error"] as? String {
|
||
return error
|
||
} else if let msg = json["msg"] as? String {
|
||
return msg
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
/// 将系统错误映射为 API 错误
|
||
///
|
||
/// 将 URLError 等系统级错误转换为统一的 APIError 类型,
|
||
/// 便于上层代码进行统一的错误处理
|
||
///
|
||
/// - Parameter error: 系统错误
|
||
/// - Returns: 映射后的 APIError
|
||
private func mapSystemError(_ error: Error) -> APIError {
|
||
if let urlError = error as? URLError {
|
||
switch urlError.code {
|
||
case .timedOut:
|
||
return .timeout
|
||
case .cannotConnectToHost, .notConnectedToInternet:
|
||
return .networkError(urlError.localizedDescription)
|
||
case .dataLengthExceedsMaximum:
|
||
return .resourceTooLarge
|
||
default:
|
||
return .networkError(urlError.localizedDescription)
|
||
}
|
||
}
|
||
|
||
return .unknown(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
// MARK: - Mock API Service (for testing)
|
||
|
||
/// 并发安全的 Mock API Service
|
||
actor MockAPIServiceActor: APIServiceProtocol, Sendable {
|
||
private var mockResponses: [String: Any] = [:]
|
||
|
||
func setMockResponse<T>(for endpoint: String, response: T) {
|
||
mockResponses[endpoint] = response
|
||
}
|
||
|
||
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response {
|
||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 秒
|
||
if let mockResponse = mockResponses[request.endpoint] as? T.Response {
|
||
return mockResponse
|
||
}
|
||
throw APIError.noData
|
||
}
|
||
}
|
||
|
||
// MARK: - TCA Dependency Integration
|
||
private enum APIServiceKey: DependencyKey {
|
||
static let liveValue: any APIServiceProtocol & Sendable = LiveAPIService()
|
||
static let testValue: any APIServiceProtocol & Sendable = MockAPIServiceActor()
|
||
}
|
||
|
||
extension DependencyValues {
|
||
var apiService: (any APIServiceProtocol & Sendable) {
|
||
get { self[APIServiceKey.self] }
|
||
set { self[APIServiceKey.self] = newValue }
|
||
}
|
||
}
|
||
|
||
// MARK: - BaseRequest Dictionary Conversion
|
||
extension BaseRequest {
|
||
func toDictionary() throws -> [String: Any] {
|
||
let data = try JSONEncoder().encode(self)
|
||
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
|
||
throw APIError.decodingError("无法转换基础参数为字典")
|
||
}
|
||
return dictionary
|
||
}
|
||
}
|