From 6a05bd0dc21b4af6da7266b6d4109dae849350ce Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Wed, 20 Aug 2025 12:25:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Models/AuthModels.swift | 43 ++++++ wake/Utils/NetworkService.swift | 255 ++++++++++++++++++++++++++++---- wake/Utils/TokenManager.swift | 109 +------------- wake/View/Login/Login.swift | 176 +++------------------- 4 files changed, 297 insertions(+), 286 deletions(-) create mode 100644 wake/Models/AuthModels.swift diff --git a/wake/Models/AuthModels.swift b/wake/Models/AuthModels.swift new file mode 100644 index 0000000..0f552d0 --- /dev/null +++ b/wake/Models/AuthModels.swift @@ -0,0 +1,43 @@ +import Foundation + +/// API基础响应模型 +struct BaseResponse: Codable { + let code: Int + let data: T? + let message: String? +} + +/// 用户登录信息 +struct UserLoginInfo: Codable { + let userId: String + let accessToken: String + let refreshToken: String + let nickname: String + let account: String + let email: String + let avatarFileUrl: String? + + enum CodingKeys: String, CodingKey { + case userId = "user_id" + case accessToken = "access_token" + case refreshToken = "refresh_token" + case nickname + case account + case email + case avatarFileUrl = "avatar_file_url" + } +} + +/// 登录响应数据 +struct LoginResponseData: Codable { + let userLoginInfo: UserLoginInfo + let isNewUser: Bool + + enum CodingKeys: String, CodingKey { + case userLoginInfo = "user_login_info" + case isNewUser = "is_new_user" + } +} + +/// 认证响应模型 +typealias AuthResponse = BaseResponse diff --git a/wake/Utils/NetworkService.swift b/wake/Utils/NetworkService.swift index ffe53b0..ac67c70 100644 --- a/wake/Utils/NetworkService.swift +++ b/wake/Utils/NetworkService.swift @@ -1,5 +1,23 @@ 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 + } +} + enum NetworkError: Error { case invalidURL case noData @@ -41,6 +59,9 @@ class NetworkService { "Accept": "application/json" ] + private var isRefreshing = false + private var requestsToRetry: [(URLRequest, (Result) -> Void, Int)] = [] + private init() {} // MARK: - 基础请求方法 @@ -51,8 +72,13 @@ class NetworkService { headers: [String: String]? = nil, completion: @escaping (Result) -> Void ) { + // 生成请求ID + let requestId = RequestIdentifier.next() + // 构建URL - guard let url = URL(string: APIConfig.baseURL + path) else { + let fullURL = APIConfig.baseURL + path + guard let url = URL(string: fullURL) else { + print("❌ [Network][#\(requestId)][\(method) \(path)] 无效的URL") completion(.failure(.invalidURL)) return } @@ -81,24 +107,39 @@ class NetworkService { do { request.httpBody = try JSONSerialization.data(withJSONObject: parameters) } catch { + print("❌ [Network][#\(requestId)][\(method) \(path)] 参数序列化失败: \(error.localizedDescription)") completion(.failure(.other(error))) return } } - // 打印请求信息(调试用) - print("🌐 [Network] \(method) \(url.absoluteString)") - if let headers = request.allHTTPHeaderFields { - print("📤 Headers: \(headers)") - } - if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) { - print("📦 Body: \(bodyString)") - } + // 打印请求信息 + 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(data: data, response: response, error: error, completion: completion) + self?.handleResponse( + requestId: requestId, + method: method, + path: path, + data: data, + response: response, + error: error, + request: request, + duration: duration, + completion: { (result: Result) in + completion(result) + } + ) } // 开始请求 @@ -106,44 +147,119 @@ class NetworkService { } private func handleResponse( + requestId: Int, + method: String, + path: String, data: Data?, response: URLResponse?, error: Error?, + request: URLRequest, + duration: String, completion: @escaping (Result) -> Void ) { - // 打印响应信息(调试用) + // 打印响应信息 if let httpResponse = response as? HTTPURLResponse { - print("📥 [Network] Status: \(httpResponse.statusCode) \(HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))") - if let headers = httpResponse.allHeaderFields as? [String: Any] { - print("📥 Headers: \(headers)") + 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.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(httpResponse.statusCode) { - print("❌ [Network] 请求失败,状态码: \(httpResponse.statusCode)") - if let data = data, let errorResponse = String(data: data, encoding: .utf8) { - print("❌ [Network] 错误响应: \(errorResponse)") - } + // 处理其他错误状态码 + 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] 网络请求错误: \(error.localizedDescription)") + print(""" + ❌ [Network][#\(requestId)][\(method) \(path)] 网络请求失败 + ⏱️ 耗时: \(duration) + 🔍 错误: \(error.localizedDescription) + """) completion(.failure(.networkError(error))) return } // 检查数据是否存在 guard let data = data else { - print("❌ [Network] 没有收到数据") + print(""" + ❌ [Network][#\(requestId)][\(method) \(path)] 没有收到数据 + ⏱️ 耗时: \(duration) + """) completion(.failure(.noData)) return } // 打印响应数据(调试用) if let responseString = String(data: data, encoding: .utf8) { - print("📥 [Network] 响应数据: \(responseString)") + print(""" + 📥 [Network][#\(requestId)][\(method) \(path)] 响应数据: + \(responseString.prefix(1000))\(responseString.count > 1000 ? "..." : "") + """) } do { @@ -152,17 +268,86 @@ class NetworkService { let result = try decoder.decode(T.self, from: data) completion(.success(result)) } catch let decodingError as DecodingError { - print("❌ [Network] JSON解析失败: \(decodingError.localizedDescription)") - if let dataString = String(data: data, encoding: .utf8) { - print("📋 [Network] 原始响应: \(dataString)") - } + print(""" + ❌ [Network][#\(requestId)][\(method) \(path)] JSON解析失败 + 🔍 错误: \(decodingError.localizedDescription) + 📦 原始数据: \(String(data: data, encoding: .utf8) ?? "") + """) completion(.failure(.decodingError(decodingError))) } catch { - print("❌ [Network] 未知错误: \(error.localizedDescription)") + 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 请求 @@ -185,6 +370,20 @@ class NetworkService { request("POST", path: path, parameters: parameters, headers: headers, completion: completion) } + /// POST 请求(带Token) + func postWithToken( + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> 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( path: String, diff --git a/wake/Utils/TokenManager.swift b/wake/Utils/TokenManager.swift index 3b11939..ad6c2d9 100644 --- a/wake/Utils/TokenManager.swift +++ b/wake/Utils/TokenManager.swift @@ -202,7 +202,7 @@ class TokenManager { let paddingLength = requiredLength - length if paddingLength > 0 { let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) - base64 += padding + base64 = base64 + padding } return Data(base64Encoded: base64) @@ -212,7 +212,7 @@ class TokenManager { /// - Parameter completion: 刷新完成回调 /// - success: 是否刷新成功 /// - error: 错误信息(如果有) - private func refreshToken(completion: @escaping (Bool, Error?) -> Void) { + func refreshToken(completion: @escaping (Bool, Error?) -> Void) { // 获取刷新令牌 guard let refreshToken = KeychainHelper.getRefreshToken(), !refreshToken.isEmpty else { // 没有可用的刷新令牌 @@ -232,7 +232,7 @@ class TokenManager { ] // 发送刷新请求 - NetworkService.shared.postWithToken(path: "/v1/iam/access-token-refresh", parameters: parameters) { + NetworkService.shared.post(path: "/v1/iam/access-token-refresh", parameters: parameters) { (result: Result) in switch result { @@ -312,109 +312,6 @@ private struct IdentityCheckResponse: Codable { } } -// MARK: - NetworkService扩展 -/// 为NetworkService添加带token验证的请求方法 -extension NetworkService { - // MARK: - 核心请求方法 - - /// 带token验证的网络请求 - /// - Parameters: - /// - method: HTTP方法(GET/POST/PUT/DELETE等) - /// - path: 接口路径 - /// - parameters: 请求参数 - /// - headers: 自定义请求头 - /// - completion: 完成回调 - func requestWithToken( - _ method: String, - path: String, - parameters: [String: Any]? = nil, - headers: [String: String]? = nil, - completion: @escaping (Result) -> Void - ) { - // 1. 验证并刷新token - TokenManager.shared.validateAndRefreshTokenIfNeeded { [weak self] isValid, error in - guard let self = self else { return } - - if isValid { - // 2. token有效,继续发送原始请求 - switch method.uppercased() { - case "GET": - self.get(path: path, parameters: parameters, headers: headers, completion: completion) - case "POST": - self.post(path: path, parameters: parameters, headers: headers, completion: completion) - case "PUT": - self.put(path: path, parameters: parameters, headers: headers, completion: completion) - case "DELETE": - self.delete(path: path, parameters: parameters, headers: headers, completion: completion) - default: - let error = NSError( - domain: "NetworkService", - code: 400, - userInfo: [NSLocalizedDescriptionKey: "不支持的HTTP方法: \(method)"] - ) - completion(.failure(.other(error))) - } - } else { - // 3. token无效,返回未授权错误 - let error = error ?? NSError( - domain: "NetworkService", - code: 401, - userInfo: [NSLocalizedDescriptionKey: "未授权访问"] - ) - - DispatchQueue.main.async { - completion(.failure(.unauthorized)) - } - - // 4. 发送登出通知,让应用处理未授权情况 - NotificationCenter.default.post(name: .userDidLogout, object: nil) - } - } - } - - // MARK: - 便捷方法 - - /// 带token验证的GET请求 - func getWithToken( - path: String, - parameters: [String: Any]? = nil, - headers: [String: String]? = nil, - completion: @escaping (Result) -> Void - ) { - requestWithToken("GET", path: path, parameters: parameters, headers: headers, completion: completion) - } - - /// 带token验证的POST请求 - func postWithToken( - path: String, - parameters: [String: Any]? = nil, - headers: [String: String]? = nil, - completion: @escaping (Result) -> Void - ) { - requestWithToken("POST", path: path, parameters: parameters, headers: headers, completion: completion) - } - - /// 带token验证的PUT请求 - func putWithToken( - path: String, - parameters: [String: Any]? = nil, - headers: [String: String]? = nil, - completion: @escaping (Result) -> Void - ) { - requestWithToken("PUT", path: path, parameters: parameters, headers: headers, completion: completion) - } - - /// 带token验证的DELETE请求 - func deleteWithToken( - path: String, - parameters: [String: Any]? = nil, - headers: [String: String]? = nil, - completion: @escaping (Result) -> Void - ) { - requestWithToken("DELETE", path: path, parameters: parameters, headers: headers, completion: completion) - } -} - // MARK: - 通知名称 /// 定义应用中使用的通知名称 extension Notification.Name { diff --git a/wake/View/Login/Login.swift b/wake/View/Login/Login.swift index 5a5e02d..4069ecd 100644 --- a/wake/View/Login/Login.swift +++ b/wake/View/Login/Login.swift @@ -2,6 +2,7 @@ import SwiftUI import AuthenticationServices import Alamofire import CryptoKit +import Foundation /// 主登录视图 - 处理苹果登录 struct LoginView: View { @@ -146,9 +147,6 @@ struct LoginView: View { private func handleAppleSignIn(result: Result) { print("🔵 [Apple Sign In] 开始处理登录结果...") - DispatchQueue.main.async { - self.isLoggedIn = true - } switch result { case .success(let authResults): print("✅ [Apple Sign In] 登录授权成功") @@ -195,12 +193,6 @@ struct LoginView: View { print("🔵 [Apple ID] 准备调用后端认证...") authenticateWithBackend( - userId: appleIDCredential.user, - email: appleIDCredential.email ?? "", - name: [appleIDCredential.fullName?.givenName, - appleIDCredential.fullName?.familyName] - .compactMap { $0 } - .joined(separator: " "), identityToken: identityToken, authCode: authCode ) @@ -209,169 +201,49 @@ struct LoginView: View { // MARK: - Network private func authenticateWithBackend( - userId: String, - email: String, - name: String, identityToken: String, authCode: String? ) { isLoading = true print("🔵 [Backend] 开始后端认证...") - let endpoint = "\(APIConfig.baseURL)/iam/login/oauth" - guard let url = URL(string: endpoint) else { - print("❌ [Backend] 无效的URL: \(endpoint)") - self.handleAuthenticationError(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的URL"])) - return - } - var parameters: [String: Any] = [ - "provider": "Apple", "token": identityToken, - "userId": userId, - "email": email, - "name": name, + "provider": "Apple", ] if let authCode = authCode { parameters["authorization_code"] = authCode } - print("📤 [Backend] 请求URL: \(endpoint)") - print("📤 [Backend] 请求参数: \(parameters)") - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - do { - request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) - } catch { - print("❌ [Backend] 参数序列化失败: \(error.localizedDescription)") - self.handleAuthenticationError(error) - return - } - - let task = URLSession.shared.dataTask(with: request) { data, response, error in + NetworkService.shared.post( + path: "/iam/login/oauth", + parameters: parameters + ) { (result: Result) in DispatchQueue.main.async { - self.isLoading = false - - // 1. 处理网络错误 - if let error = error { - print("❌ [Backend] 请求失败: \(error.localizedDescription)") - self.handleAuthenticationError(error) - return - } - - // 2. 检查响应 - guard let httpResponse = response as? HTTPURLResponse else { - let error = NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的服务器响应"]) - print("❌ [Backend] \(error.localizedDescription)") - self.handleAuthenticationError(error) - return - } - - // 3. 打印响应状态码和头部信息 - let statusCode = httpResponse.statusCode - print(""" - 📥 [Backend] 响应信息: - - 状态码: \(statusCode) - - URL: \(httpResponse.url?.absoluteString ?? "N/A") - - Headers: \(httpResponse.allHeaderFields) - """) - - // 4. 处理响应数据 - if let data = data { - if let jsonString = String(data: data, encoding: .utf8) { - print("📦 [Backend] 响应内容: \(jsonString)") + switch result { + case .success(let authResponse): + print("✅ [Backend] 认证成功") + + // 保存token等认证信息 + if let loginInfo = authResponse.data?.userLoginInfo { + KeychainHelper.saveAccessToken(loginInfo.accessToken) + KeychainHelper.saveRefreshToken(loginInfo.refreshToken) + // 可以在这里保存其他用户信息,如userId, nickname等 + print("👤 用户ID: \(loginInfo.userId)") + print("👤 昵称: \(loginInfo.nickname)") } - // 5. 解析响应数据 - do { - // 首先解析顶层 JSON - if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { - print("✅ [Backend] 响应解析成功") - print("📦 [Backend] 响应内容: \(json)") - - // 检查状态码 - if let code = json["code"] as? Int, code != 0 { - let errorMsg = json["message"] as? String ?? "未知错误" - print("❌ [Backend] 请求失败: \(errorMsg)") - self.showError(message: errorMsg) - return - } - - // 获取 data 字典 - guard let responseData = json["data"] as? [String: Any] else { - print("⚠️ [Backend] 未找到 data 字段") - self.showError(message: "服务器返回数据格式错误") - return - } - - // 获取 user_login_info 字典 - if let userLoginInfo = responseData["user_login_info"] as? [String: Any] { - print("👤 [Backend] 用户登录信息: \(userLoginInfo)") - - // 保存令牌到 Keychain - if let accessToken = userLoginInfo["access_token"] as? String { - _ = KeychainHelper.saveAccessToken(accessToken) - print("🔑 [Keychain] 访问令牌已保存") - - // 保存用户信息到 UserDefaults - var userInfo: [String: Any] = [ - "user_id": userLoginInfo["user_id"] as? Int64 ?? 0, - "account": userLoginInfo["account"] as? String ?? "", - "nickname": userLoginInfo["nickname"] as? String ?? "", - "avatar": userLoginInfo["avatar_file_url"] as? String ?? "" - ] - UserDefaults.standard.set(userInfo, forKey: "currentUserInfo") - print("👤 [UserDefaults] 用户信息已保存") - } - - if let refreshToken = userLoginInfo["refresh_token"] as? String { - _ = KeychainHelper.saveRefreshToken(refreshToken) - print("🔄 [Keychain] 刷新令牌已保存") - } - - // 处理认证成功 - DispatchQueue.main.async { - self.handleSuccessfulAuthentication() - } - return - } else { - print("⚠️ [Backend] 未找到 user_login_info 字段") - self.showError(message: "登录信息不完整") - } - } - } catch { - print("⚠️ [Backend] 响应解析失败: \(error.localizedDescription)") - self.showError(message: "数据解析失败") - } - } - - // 6. 如果上面的 return 没有执行,说明认证失败 - if statusCode < 200 || statusCode >= 300 { - let errorMessage: String - switch statusCode { - case 400: - errorMessage = "请求参数错误" - case 401: - errorMessage = "认证失败,请重新登录" - case 403: - errorMessage = "权限不足" - case 404: - errorMessage = "请求的接口不存在" - case 500...599: - errorMessage = "服务器内部错误,请稍后重试" - default: - errorMessage = "未知错误 (状态码: \(statusCode))" - } - self.showError(message: errorMessage) + self.isLoggedIn = true + + case .failure(let error): + print("❌ [Backend] 认证失败: \(error.localizedDescription)") + self.errorMessage = error.localizedDescription + self.showError = true + self.isLoading = false } } } - - task.resume() } // MARK: - Helpers