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以排除构建文件和临时文件。
This commit is contained in:
244
yana/APIs/APIService.swift
Normal file
244
yana/APIs/APIService.swift
Normal file
@@ -0,0 +1,244 @@
|
||||
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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user