Files
e-party-iOS/yana/APIs/APIService.swift
edwinQQQ 4a1b814902 feat: 实现数据迁移和用户信息管理优化
- 在AppDelegate中集成数据迁移管理器,支持从UserDefaults迁移到Keychain。
- 重构UserInfoManager,使用Keychain存储用户信息,增加内存缓存以提升性能。
- 添加API加载效果视图,增强用户体验。
- 更新SplashFeature以支持自动登录和认证状态检查。
- 语言设置迁移至Keychain,确保用户设置的安全性。
2025-07-10 17:20:20 +08:00

361 lines
14 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
/// API
///
/// `APIRequestProtocol`
///
///
/// 使
/// ```swift
/// let apiService: APIServiceProtocol = LiveAPIService()
/// let request = ConfigRequest()
/// let response = try await apiService.request(request)
/// ```
protocol APIServiceProtocol {
///
/// - 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 {
private let session: URLSession
private let baseURL: String
/// API
/// - Parameter baseURL: API URL使
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)
}
///
///
///
/// 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 = APILoadingManager.shared.startLoading(
shouldShowLoading: request.shouldShowLoading,
shouldShowError: request.shouldShowError
)
// URL
guard let url = buildURL(for: request) else {
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
//
var headers = 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 = BaseRequest()
// API rule
baseParams.generateSignature(with: bodyParams)
let baseDict = try baseParams.toDictionary()
finalBody.merge(baseDict) { existing, _ in existing }
}
requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: [])
urlRequest.httpBody = requestBody
} catch {
let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)")
APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription)
throw encodingError
}
}
// 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 {
let networkError = APIError.networkError("无效的响应类型")
APILoadingManager.shared.setError(loadingId, errorMessage: networkError.localizedDescription)
throw networkError
}
//
if data.count > APIConfiguration.maxDataSize {
APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration)
APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription)
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)
let httpError = APIError.httpError(statusCode: httpResponse.statusCode, message: errorMessage)
APILoadingManager.shared.setError(loadingId, errorMessage: httpError.localizedDescription)
throw httpError
}
//
guard !data.isEmpty else {
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)
APILogger.logDecodedResponse(decodedResponse, type: T.Response.self)
// loading
APILoadingManager.shared.finishLoading(loadingId)
return decodedResponse
} catch {
let decodingError = APIError.decodingError("响应解析失败: \(error.localizedDescription)")
APILoadingManager.shared.setError(loadingId, errorMessage: decodingError.localizedDescription)
throw decodingError
}
} catch let error as APIError {
let duration = Date().timeIntervalSince(startTime)
APILogger.logError(error, url: url, duration: duration)
APILoadingManager.shared.setError(loadingId, errorMessage: error.localizedDescription)
throw error
} catch {
let duration = Date().timeIntervalSince(startTime)
let apiError = mapSystemError(error)
APILogger.logError(apiError, url: url, duration: duration)
APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription)
throw apiError
}
}
// MARK: - Private Helper Methods
/// URL
///
///
/// - URL
/// -
/// - GET
///
/// - Parameter request: API
/// - Returns: URL nil
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()
// GET
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)"))
}
} 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
}
///
///
/// 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)
/// API
///
/// API
/// -
/// -
/// - UI
///
/// 使
/// ```swift
/// var mockService = MockAPIService()
/// mockService.setMockResponse(for: "/client/config", response: mockConfigResponse)
/// let response = try await mockService.request(ConfigRequest())
/// ```
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
}
}