Files
e-party-iOS/yana/APIs/APIService.swift
edwinQQQ 8b4eb9cb7e feat: 更新API相关逻辑及视图结构
- 在Info.plist中新增API签名密钥配置。
- 将Splash视图替换为SplashV2,优化启动逻辑和用户体验。
- 更新API请求中的User-Agent逻辑,使用UserAgentProvider提供的动态值。
- 在APILogger中添加敏感信息脱敏处理,增强安全性。
- 新增CreateFeedPage视图,支持用户发布动态功能。
- 更新MainPage和Splash视图的导航逻辑,整合统一的AppRoute管理。
- 移除冗余的SplashFeature视图,提升代码整洁性和可维护性。
2025-09-17 16:37:21 +08:00

379 lines
15 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
// 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")
// HTTP Body APILogger
} catch {
let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)")
await 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("无效的响应类型")
await APILoadingManager.shared.setError(loadingId, errorMessage: networkError.localizedDescription)
throw networkError
}
//
if data.count > APIConfiguration.maxDataSize {
APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration)
await 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)
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)
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)
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)
APILogger.logError(apiError, url: url, duration: duration)
await APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription)
throw apiError
}
}
// MARK: -
func updateUser(request: UpdateUserRequest) async throws -> UpdateUserResponse {
try await self.request(request)
}
// 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
} else if let detail = json["detail"] as? String {
return detail
} else if let errorDescription = json["error_description"] as? String {
return errorDescription
} else if let errorDict = json["error"] as? [String: Any], let nestedMsg = errorDict["message"] as? String {
return nestedMsg
} else if let errors = json["errors"] as? [[String: Any]], let firstMsg = errors.first? ["message"] as? String {
return firstMsg
}
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 (optional)
#if canImport(ComposableArchitecture)
import ComposableArchitecture
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 }
}
}
#endif
// 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
}
}