wake-ios/wake/Utils/NetworkService.swift
2025-09-01 19:42:32 +08:00

511 lines
18 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
//
extension Notification.Name {
static let userDidLogoutNotification = Notification.Name("UserDidLogoutNotification")
}
//
private struct RequestIdentifier {
static var currentId: Int = 0
static var lock = NSLock()
static func next() -> Int {
lock.lock()
defer { lock.unlock() }
currentId += 1
return currentId
}
}
public protocol NetworkServiceProtocol {
func postWithToken<T: Decodable>(
path: String,
parameters: [String: Any],
completion: @escaping (Result<T, NetworkError>) -> Void
)
@discardableResult
func upload(
request: URLRequest,
fileData: Data,
onProgress: @escaping (Double) -> Void,
completion: @escaping (Result<(Data?, URLResponse), Error>) -> Void
) -> URLSessionUploadTask?
}
extension NetworkService: NetworkServiceProtocol {
public func postWithToken<T: Decodable>(
path: String,
parameters: [String: Any],
completion: @escaping (Result<T, NetworkError>) -> Void
) {
var headers = [String: String]()
if let token = KeychainHelper.getAccessToken() {
headers["Authorization"] = "Bearer \(token)"
}
post(path: path, parameters: parameters, headers: headers, completion: completion)
}
@discardableResult
public func upload(
request: URLRequest,
fileData: Data,
onProgress: @escaping (Double) -> Void,
completion: @escaping (Result<(Data?, URLResponse), Error>) -> Void
) -> URLSessionUploadTask? {
var request = request
// Set content length header if not already set
if request.value(forHTTPHeaderField: "Content-Length") == nil {
request.setValue("\(fileData.count)", forHTTPHeaderField: "Content-Length")
}
var progressObserver: NSKeyValueObservation?
let task = URLSession.shared.uploadTask(with: request, from: fileData) { [weak self] data, response, error in
// Invalidate the progress observer when the task completes
progressObserver?.invalidate()
if let error = error {
completion(.failure(error))
return
}
guard let response = response else {
completion(.failure(NetworkError.invalidURL))
return
}
completion(.success((data, response)))
}
// Add progress tracking if available
if #available(iOS 11.0, *) {
progressObserver = task.progress.observe(\.fractionCompleted) { progressValue, _ in
DispatchQueue.main.async {
onProgress(progressValue.fractionCompleted)
}
}
}
task.resume()
return task
}
}
public enum NetworkError: Error {
case invalidURL
case noData
case decodingError(Error)
case serverError(String)
case unauthorized
case other(Error)
case networkError(Error)
case unknownError(Error)
case invalidParameters
public var localizedDescription: String {
switch self {
case .invalidURL:
return "无效的URL"
case .noData:
return "没有接收到数据"
case .decodingError(let error):
return "解码错误: \(error.localizedDescription)"
case .serverError(let message):
return "服务器错误: \(message)"
case .unauthorized:
return "未授权,请重新登录"
case .other(let error):
return error.localizedDescription
case .networkError(let error):
return "网络错误: \(error.localizedDescription)"
case .unknownError(let error):
return "未知错误: \(error.localizedDescription)"
case .invalidParameters:
return "无效的参数"
}
}
}
class NetworkService {
static let shared = NetworkService()
//
private let defaultHeaders: [String: String] = [
"Content-Type": "application/json",
"Accept": "application/json"
]
private var isRefreshing = false
private var requestsToRetry: [(URLRequest, (Result<Data, NetworkError>) -> Void, Int)] = []
private init() {}
// MARK: -
private func request<T: Decodable>(
_ method: String,
path: String,
parameters: Any? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
// ID
let requestId = RequestIdentifier.next()
// URL
let fullURL = APIConfig.baseURL + path
guard let url = URL(string: fullURL) else {
print("❌ [Network][#\(requestId)][\(method) \(path)] 无效的URL")
completion(.failure(.invalidURL))
return
}
//
var request = URLRequest(url: url)
request.httpMethod = method
// -
defaultHeaders.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key)
}
// -
if !path.contains("/iam/login/") {
APIConfig.authHeaders.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key)
}
}
//
headers?.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key)
}
// POST/PUT
if let parameters = parameters, (method == "POST" || method == "PUT") {
do {
if JSONSerialization.isValidJSONObject(parameters) {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
} else {
print("❌ [Network][#\(requestId)][\(method) \(path)] 参数不是有效的JSON对象")
completion(.failure(.invalidParameters))
return
}
} catch {
print("❌ [Network][#\(requestId)][\(method) \(path)] 参数序列化失败: \(error.localizedDescription)")
completion(.failure(.other(error)))
return
}
}
//
print("""
🌐 [Network][#\(requestId)][\(method) \(path)] 开始请求
🔗 URL: \(url.absoluteString)
📤 Headers: \(request.allHTTPHeaderFields ?? [:])
📦 Body: \(request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? "")
""")
//
let startTime = Date()
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
let duration = String(format: "%.3fs", Date().timeIntervalSince(startTime))
//
self?.handleResponse(
requestId: requestId,
method: method,
path: path,
data: data,
response: response,
error: error,
request: request,
duration: duration,
completion: { (result: Result<T, NetworkError>) in
completion(result)
}
)
}
//
task.resume()
}
private func handleResponse<T: Decodable>(
requestId: Int,
method: String,
path: String,
data: Data?,
response: URLResponse?,
error: Error?,
request: URLRequest,
duration: String,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
//
if let httpResponse = response as? HTTPURLResponse {
let statusCode = httpResponse.statusCode
let statusMessage = HTTPURLResponse.localizedString(forStatusCode: statusCode)
// 401
if statusCode == 401 {
print("""
🔑 [Network][#\(requestId)][\(method) \(path)] 检测到未授权尝试刷新token...
⏱️ 耗时: \(duration)
""")
//
let dataResult = data.flatMap { Result<Data, NetworkError>.success($0) } ?? .failure(.noData)
self.requestsToRetry.append((request, { result in
switch result {
case .success(let data):
do {
let decoder = JSONDecoder()
let result = try decoder.decode(T.self, from: data)
print("""
✅ [Network][#\(requestId)][\(method) \(path)] 重试成功
⏱️ 总耗时: \(duration) (包含token刷新时间)
""")
completion(.success(result))
} catch let decodingError as DecodingError {
print("""
❌ [Network][#\(requestId)][\(method) \(path)] JSON解析失败
🔍 错误: \(decodingError.localizedDescription)
📦 原始数据: \(String(data: data, encoding: .utf8) ?? "")
""")
completion(.failure(.decodingError(decodingError)))
} catch {
print("""
❌ [Network][#\(requestId)][\(method) \(path)] 未知错误
🔍 错误: \(error.localizedDescription)
""")
completion(.failure(.unknownError(error)))
}
case .failure(let error):
print("""
❌ [Network][#\(requestId)][\(method) \(path)] 重试失败
🔍 错误: \(error.localizedDescription)
""")
completion(.failure(error))
}
}, requestId))
// token
if !isRefreshing {
refreshAndRetryRequests()
}
return
}
//
if !(200...299).contains(statusCode) {
let errorMessage = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
print("""
❌ [Network][#\(requestId)][\(method) \(path)] 请求失败
📊 状态码: \(statusCode) (\(statusMessage))
⏱️ 耗时: \(duration)
🔍 错误响应: \(errorMessage)
""")
completion(.failure(.serverError("状态码: \(statusCode), 响应: \(errorMessage)")))
return
}
//
print("""
✅ [Network][#\(requestId)][\(method) \(path)] 请求成功
📊 状态码: \(statusCode) (\(statusMessage))
⏱️ 耗时: \(duration)
""")
}
//
if let error = error {
print("""
❌ [Network][#\(requestId)][\(method) \(path)] 网络请求失败
⏱️ 耗时: \(duration)
🔍 错误: \(error.localizedDescription)
""")
completion(.failure(.networkError(error)))
return
}
//
guard let data = data else {
print("""
❌ [Network][#\(requestId)][\(method) \(path)] 没有收到数据
⏱️ 耗时: \(duration)
""")
completion(.failure(.noData))
return
}
//
if let responseString = String(data: data, encoding: .utf8) {
print("""
📥 [Network][#\(requestId)][\(method) \(path)] 响应数据:
\(responseString.prefix(1000))\(responseString.count > 1000 ? "..." : "")
""")
}
do {
// JSON
let decoder = JSONDecoder()
let result = try decoder.decode(T.self, from: data)
completion(.success(result))
} catch let decodingError as DecodingError {
print("""
❌ [Network][#\(requestId)][\(method) \(path)] JSON解析失败
🔍 错误: \(decodingError.localizedDescription)
📦 原始数据: \(String(data: data, encoding: .utf8) ?? "")
""")
completion(.failure(.decodingError(decodingError)))
} catch {
print("""
❌ [Network][#\(requestId)][\(method) \(path)] 未知错误
🔍 错误: \(error.localizedDescription)
""")
completion(.failure(.unknownError(error)))
}
}
private func refreshAndRetryRequests() {
guard !isRefreshing else { return }
isRefreshing = true
let refreshStartTime = Date()
print("🔄 [Network] 开始刷新Token...")
TokenManager.shared.refreshToken { [weak self] success, _ in
guard let self = self else { return }
let refreshDuration = String(format: "%.3fs", Date().timeIntervalSince(refreshStartTime))
if success {
print("""
✅ [Network] Token刷新成功
⏱️ 耗时: \(refreshDuration)
🔄 准备重试\(self.requestsToRetry.count)个请求...
""")
//
let requestsToRetry = self.requestsToRetry
self.requestsToRetry.removeAll()
for (request, completion, requestId) in requestsToRetry {
var newRequest = request
if let token = KeychainHelper.getAccessToken() {
newRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
let task = URLSession.shared.dataTask(with: newRequest) { data, response, error in
if let data = data {
completion(.success(data))
} else if let error = error {
completion(.failure(.networkError(error)))
} else {
completion(.failure(.noData))
}
}
task.resume()
}
} else {
print("""
❌ [Network] Token刷新失败
⏱️ 耗时: \(refreshDuration)
🚪 清除登录状态...
""")
// token
TokenManager.shared.clearTokens()
DispatchQueue.main.async {
NotificationCenter.default.post(name: .userDidLogoutNotification, object: nil)
}
//
self.requestsToRetry.forEach { _, completion, _ in
completion(.failure(.unauthorized))
}
self.requestsToRetry.removeAll()
}
self.isRefreshing = false
}
}
// MARK: -
/// GET
func get<T: Decodable>(
path: String,
parameters: [String: Any]? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
request("GET", path: path, parameters: parameters, headers: headers, completion: completion)
}
/// POST
func post<T: Decodable>(
path: String,
parameters: Any? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
var params: Any?
if let parameters = parameters {
if let dict = parameters as? [String: Any] {
params = dict
} else if let array = parameters as? [Any] {
params = array
} else {
print("❌ [Network] POST 请求参数类型不支持")
completion(.failure(.invalidParameters))
return
}
}
request("POST", path: path, parameters: params, headers: headers, completion: completion)
}
/// POST Token
func postWithToken<T: Decodable>(
path: String,
parameters: Any? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
var headers = headers ?? [:]
if let token = KeychainHelper.getAccessToken() {
headers["Authorization"] = "Bearer \(token)"
}
post(path: path, parameters: parameters, headers: headers, completion: completion)
}
/// DELETE
func delete<T: Decodable>(
path: String,
parameters: [String: Any]? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
var headers = headers ?? [:]
if let token = KeychainHelper.getAccessToken() {
headers["Authorization"] = "Bearer \(token)"
}
request("DELETE", path: path, parameters: parameters, headers: headers, completion: completion)
}
/// PUT
func put<T: Decodable>(
path: String,
parameters: [String: Any]? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
request("PUT", path: path, parameters: parameters, headers: headers, completion: completion)
}
}