
新增Package.swift和Package.resolved文件以支持Swift Package管理,创建API相关文件(API.swift、APICaller.swift、APIConstants.swift、APIEndpoints.swift、APIService.swift、APILogger.swift、APIModels.swift、Integration-Guide.md)以实现API请求管理和网络交互功能,增强项目的功能性和可扩展性。同时更新.gitignore以排除构建文件和临时文件。
244 lines
8.6 KiB
Swift
244 lines
8.6 KiB
Swift
import Foundation
|
||
import ComposableArchitecture
|
||
|
||
// MARK: - API Service Protocol
|
||
protocol APIServiceProtocol {
|
||
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response
|
||
}
|
||
|
||
// MARK: - Live API Service Implementation
|
||
struct LiveAPIService: APIServiceProtocol {
|
||
private let session: URLSession
|
||
private let baseURL: String
|
||
|
||
init(baseURL: String = APIConfiguration.baseURL) {
|
||
self.baseURL = baseURL
|
||
|
||
// 配置 URLSession 以防止资源超限问题
|
||
let config = URLSessionConfiguration.default
|
||
config.timeoutIntervalForRequest = APIConfiguration.timeout
|
||
config.timeoutIntervalForResource = APIConfiguration.timeout * 2
|
||
config.waitsForConnectivity = true
|
||
config.allowsCellularAccess = true
|
||
|
||
// 设置数据大小限制
|
||
config.urlCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
|
||
|
||
self.session = URLSession(configuration: config)
|
||
}
|
||
|
||
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response {
|
||
let startTime = Date()
|
||
|
||
// 构建 URL
|
||
guard let url = buildURL(for: request) else {
|
||
throw APIError.invalidURL
|
||
}
|
||
|
||
// 构建 URLRequest
|
||
var urlRequest = URLRequest(url: url)
|
||
urlRequest.httpMethod = request.method.rawValue
|
||
urlRequest.timeoutInterval = request.timeout
|
||
|
||
// 设置请求头
|
||
var headers = APIConfiguration.defaultHeaders
|
||
if let customHeaders = request.headers {
|
||
headers.merge(customHeaders) { _, 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 {
|
||
let baseParams = BaseRequest()
|
||
let baseDict = try baseParams.toDictionary()
|
||
finalBody.merge(baseDict) { existing, _ in existing }
|
||
}
|
||
|
||
requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: [])
|
||
urlRequest.httpBody = requestBody
|
||
} catch {
|
||
throw APIError.decodingError("请求体编码失败: \(error.localizedDescription)")
|
||
}
|
||
}
|
||
|
||
// 记录请求日志,传递完整的 headers 信息
|
||
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 {
|
||
throw APIError.networkError("无效的响应类型")
|
||
}
|
||
|
||
// 检查数据大小
|
||
if data.count > APIConfiguration.maxDataSize {
|
||
APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration)
|
||
throw APIError.resourceTooLarge
|
||
}
|
||
|
||
// 记录响应日志
|
||
APILogger.logResponse(data: data, response: httpResponse, duration: duration)
|
||
|
||
// 性能警告
|
||
APILogger.logPerformanceWarning(duration: duration)
|
||
|
||
// 检查 HTTP 状态码
|
||
guard 200...299 ~= httpResponse.statusCode else {
|
||
let errorMessage = extractErrorMessage(from: data)
|
||
throw APIError.httpError(statusCode: httpResponse.statusCode, message: errorMessage)
|
||
}
|
||
|
||
// 检查数据是否为空
|
||
guard !data.isEmpty else {
|
||
throw APIError.noData
|
||
}
|
||
|
||
// 解析响应数据
|
||
do {
|
||
let decoder = JSONDecoder()
|
||
let decodedResponse = try decoder.decode(T.Response.self, from: data)
|
||
APILogger.logDecodedResponse(decodedResponse, type: T.Response.self)
|
||
return decodedResponse
|
||
} catch {
|
||
throw APIError.decodingError("响应解析失败: \(error.localizedDescription)")
|
||
}
|
||
|
||
} catch let error as APIError {
|
||
let duration = Date().timeIntervalSince(startTime)
|
||
APILogger.logError(error, url: url, duration: duration)
|
||
throw error
|
||
} catch {
|
||
let duration = Date().timeIntervalSince(startTime)
|
||
let apiError = mapSystemError(error)
|
||
APILogger.logError(apiError, url: url, duration: duration)
|
||
throw apiError
|
||
}
|
||
}
|
||
|
||
// MARK: - Private Helper Methods
|
||
|
||
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 {
|
||
let baseParams = BaseRequest()
|
||
let baseDict = try baseParams.toDictionary()
|
||
for (key, value) in baseDict {
|
||
queryItems.append(URLQueryItem(name: key, value: "\(value)"))
|
||
}
|
||
} catch {
|
||
print("警告:无法添加基础参数到查询字符串")
|
||
}
|
||
}
|
||
|
||
// 添加自定义查询参数
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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)
|
||
struct MockAPIService: APIServiceProtocol {
|
||
private var mockResponses: [String: Any] = [:]
|
||
|
||
mutating 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: APIServiceProtocol = LiveAPIService()
|
||
static let testValue: APIServiceProtocol = MockAPIService()
|
||
}
|
||
|
||
extension DependencyValues {
|
||
var apiService: APIServiceProtocol {
|
||
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
|
||
}
|
||
} |