Files
e-party-iOS/yana/APIs/APIService.swift
edwinQQQ 007c10daaf feat: 添加Swift Package管理和API功能模块
新增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以排除构建文件和临时文件。
2025-06-04 17:25:21 +08:00

244 lines
8.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}