feat: 登录

This commit is contained in:
jinyaqiu 2025-08-20 12:25:57 +08:00
parent fdd2715ec8
commit 6a05bd0dc2
4 changed files with 297 additions and 286 deletions

View File

@ -0,0 +1,43 @@
import Foundation
/// API
struct BaseResponse<T: Codable>: 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<LoginResponseData>

View File

@ -1,5 +1,23 @@
import Foundation 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 { enum NetworkError: Error {
case invalidURL case invalidURL
case noData case noData
@ -41,6 +59,9 @@ class NetworkService {
"Accept": "application/json" "Accept": "application/json"
] ]
private var isRefreshing = false
private var requestsToRetry: [(URLRequest, (Result<Data, NetworkError>) -> Void, Int)] = []
private init() {} private init() {}
// MARK: - // MARK: -
@ -51,8 +72,13 @@ class NetworkService {
headers: [String: String]? = nil, headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void completion: @escaping (Result<T, NetworkError>) -> Void
) { ) {
// ID
let requestId = RequestIdentifier.next()
// URL // 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)) completion(.failure(.invalidURL))
return return
} }
@ -81,24 +107,39 @@ class NetworkService {
do { do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters) request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
} catch { } catch {
print("❌ [Network][#\(requestId)][\(method) \(path)] 参数序列化失败: \(error.localizedDescription)")
completion(.failure(.other(error))) completion(.failure(.other(error)))
return return
} }
} }
// //
print("🌐 [Network] \(method) \(url.absoluteString)") print("""
if let headers = request.allHTTPHeaderFields { 🌐 [Network][#\(requestId)][\(method) \(path)]
print("📤 Headers: \(headers)") 🔗 URL: \(url.absoluteString)
} 📤 Headers: \(request.allHTTPHeaderFields ?? [:])
if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) { 📦 Body: \(request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? "")
print("📦 Body: \(bodyString)") """)
}
// //
let startTime = Date()
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in 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<T, NetworkError>) in
completion(result)
}
)
} }
// //
@ -106,44 +147,119 @@ class NetworkService {
} }
private func handleResponse<T: Decodable>( private func handleResponse<T: Decodable>(
requestId: Int,
method: String,
path: String,
data: Data?, data: Data?,
response: URLResponse?, response: URLResponse?,
error: Error?, error: Error?,
request: URLRequest,
duration: String,
completion: @escaping (Result<T, NetworkError>) -> Void completion: @escaping (Result<T, NetworkError>) -> Void
) { ) {
// //
if let httpResponse = response as? HTTPURLResponse { if let httpResponse = response as? HTTPURLResponse {
print("📥 [Network] Status: \(httpResponse.statusCode) \(HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))") let statusCode = httpResponse.statusCode
if let headers = httpResponse.allHeaderFields as? [String: Any] { let statusMessage = HTTPURLResponse.localizedString(forStatusCode: statusCode)
print("📥 Headers: \(headers)")
// 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(httpResponse.statusCode) { if !(200...299).contains(statusCode) {
print("❌ [Network] 请求失败,状态码: \(httpResponse.statusCode)") let errorMessage = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
if let data = data, let errorResponse = String(data: data, encoding: .utf8) { print("""
print("❌ [Network] 错误响应: \(errorResponse)") [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 { if let error = error {
print("❌ [Network] 网络请求错误: \(error.localizedDescription)") print("""
[Network][#\(requestId)][\(method) \(path)]
: \(duration)
🔍 : \(error.localizedDescription)
""")
completion(.failure(.networkError(error))) completion(.failure(.networkError(error)))
return return
} }
// //
guard let data = data else { guard let data = data else {
print("❌ [Network] 没有收到数据") print("""
[Network][#\(requestId)][\(method) \(path)]
: \(duration)
""")
completion(.failure(.noData)) completion(.failure(.noData))
return return
} }
// //
if let responseString = String(data: data, encoding: .utf8) { if let responseString = String(data: data, encoding: .utf8) {
print("📥 [Network] 响应数据: \(responseString)") print("""
📥 [Network][#\(requestId)][\(method) \(path)] :
\(responseString.prefix(1000))\(responseString.count > 1000 ? "..." : "")
""")
} }
do { do {
@ -152,17 +268,86 @@ class NetworkService {
let result = try decoder.decode(T.self, from: data) let result = try decoder.decode(T.self, from: data)
completion(.success(result)) completion(.success(result))
} catch let decodingError as DecodingError { } catch let decodingError as DecodingError {
print("❌ [Network] JSON解析失败: \(decodingError.localizedDescription)") print("""
if let dataString = String(data: data, encoding: .utf8) { [Network][#\(requestId)][\(method) \(path)] JSON解析失败
print("📋 [Network] 原始响应: \(dataString)") 🔍 : \(decodingError.localizedDescription)
} 📦 : \(String(data: data, encoding: .utf8) ?? "")
""")
completion(.failure(.decodingError(decodingError))) completion(.failure(.decodingError(decodingError)))
} catch { } catch {
print("❌ [Network] 未知错误: \(error.localizedDescription)") print("""
[Network][#\(requestId)][\(method) \(path)]
🔍 : \(error.localizedDescription)
""")
completion(.failure(.unknownError(error))) 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: - // MARK: -
/// GET /// GET
@ -185,6 +370,20 @@ class NetworkService {
request("POST", path: path, parameters: parameters, headers: headers, completion: completion) request("POST", path: path, parameters: parameters, headers: headers, completion: completion)
} }
/// POST Token
func postWithToken<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)"
}
post(path: path, parameters: parameters, headers: headers, completion: completion)
}
/// DELETE /// DELETE
func delete<T: Decodable>( func delete<T: Decodable>(
path: String, path: String,

View File

@ -202,7 +202,7 @@ class TokenManager {
let paddingLength = requiredLength - length let paddingLength = requiredLength - length
if paddingLength > 0 { if paddingLength > 0 {
let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
base64 += padding base64 = base64 + padding
} }
return Data(base64Encoded: base64) return Data(base64Encoded: base64)
@ -212,7 +212,7 @@ class TokenManager {
/// - Parameter completion: /// - Parameter completion:
/// - success: /// - success:
/// - error: /// - error:
private func refreshToken(completion: @escaping (Bool, Error?) -> Void) { func refreshToken(completion: @escaping (Bool, Error?) -> Void) {
// //
guard let refreshToken = KeychainHelper.getRefreshToken(), !refreshToken.isEmpty else { 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<TokenResponse, NetworkError>) in (result: Result<TokenResponse, NetworkError>) in
switch result { switch result {
@ -312,109 +312,6 @@ private struct IdentityCheckResponse: Codable {
} }
} }
// MARK: - NetworkService
/// NetworkServicetoken
extension NetworkService {
// MARK: -
/// token
/// - Parameters:
/// - method: HTTPGET/POST/PUT/DELETE
/// - path:
/// - parameters:
/// - headers:
/// - completion:
func requestWithToken<T: Decodable>(
_ method: String,
path: String,
parameters: [String: Any]? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> 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: - 便
/// tokenGET
func getWithToken<T: Decodable>(
path: String,
parameters: [String: Any]? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
requestWithToken("GET", path: path, parameters: parameters, headers: headers, completion: completion)
}
/// tokenPOST
func postWithToken<T: Decodable>(
path: String,
parameters: [String: Any]? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
requestWithToken("POST", path: path, parameters: parameters, headers: headers, completion: completion)
}
/// tokenPUT
func putWithToken<T: Decodable>(
path: String,
parameters: [String: Any]? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
requestWithToken("PUT", path: path, parameters: parameters, headers: headers, completion: completion)
}
/// tokenDELETE
func deleteWithToken<T: Decodable>(
path: String,
parameters: [String: Any]? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
requestWithToken("DELETE", path: path, parameters: parameters, headers: headers, completion: completion)
}
}
// MARK: - // MARK: -
/// 使 /// 使
extension Notification.Name { extension Notification.Name {

View File

@ -2,6 +2,7 @@ import SwiftUI
import AuthenticationServices import AuthenticationServices
import Alamofire import Alamofire
import CryptoKit import CryptoKit
import Foundation
/// - /// -
struct LoginView: View { struct LoginView: View {
@ -146,9 +147,6 @@ struct LoginView: View {
private func handleAppleSignIn(result: Result<ASAuthorization, Error>) { private func handleAppleSignIn(result: Result<ASAuthorization, Error>) {
print("🔵 [Apple Sign In] 开始处理登录结果...") print("🔵 [Apple Sign In] 开始处理登录结果...")
DispatchQueue.main.async {
self.isLoggedIn = true
}
switch result { switch result {
case .success(let authResults): case .success(let authResults):
print("✅ [Apple Sign In] 登录授权成功") print("✅ [Apple Sign In] 登录授权成功")
@ -195,12 +193,6 @@ struct LoginView: View {
print("🔵 [Apple ID] 准备调用后端认证...") print("🔵 [Apple ID] 准备调用后端认证...")
authenticateWithBackend( authenticateWithBackend(
userId: appleIDCredential.user,
email: appleIDCredential.email ?? "",
name: [appleIDCredential.fullName?.givenName,
appleIDCredential.fullName?.familyName]
.compactMap { $0 }
.joined(separator: " "),
identityToken: identityToken, identityToken: identityToken,
authCode: authCode authCode: authCode
) )
@ -209,169 +201,49 @@ struct LoginView: View {
// MARK: - Network // MARK: - Network
private func authenticateWithBackend( private func authenticateWithBackend(
userId: String,
email: String,
name: String,
identityToken: String, identityToken: String,
authCode: String? authCode: String?
) { ) {
isLoading = true isLoading = true
print("🔵 [Backend] 开始后端认证...") 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] = [ var parameters: [String: Any] = [
"provider": "Apple",
"token": identityToken, "token": identityToken,
"userId": userId, "provider": "Apple",
"email": email,
"name": name,
] ]
if let authCode = authCode { if let authCode = authCode {
parameters["authorization_code"] = authCode parameters["authorization_code"] = authCode
} }
print("📤 [Backend] 请求URL: \(endpoint)") NetworkService.shared.post(
print("📤 [Backend] 请求参数: \(parameters)") path: "/iam/login/oauth",
parameters: parameters
var request = URLRequest(url: url) ) { (result: Result<AuthResponse, NetworkError>) in
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
DispatchQueue.main.async { DispatchQueue.main.async {
self.isLoading = false switch result {
case .success(let authResponse):
// 1. print("✅ [Backend] 认证成功")
if let error = error {
print("❌ [Backend] 请求失败: \(error.localizedDescription)") // token
self.handleAuthenticationError(error) if let loginInfo = authResponse.data?.userLoginInfo {
return KeychainHelper.saveAccessToken(loginInfo.accessToken)
} KeychainHelper.saveRefreshToken(loginInfo.refreshToken)
// userId, nickname
// 2. print("👤 用户ID: \(loginInfo.userId)")
guard let httpResponse = response as? HTTPURLResponse else { print("👤 昵称: \(loginInfo.nickname)")
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)")
} }
// 5. self.isLoggedIn = true
do {
// JSON case .failure(let error):
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { print("❌ [Backend] 认证失败: \(error.localizedDescription)")
print("✅ [Backend] 响应解析成功") self.errorMessage = error.localizedDescription
print("📦 [Backend] 响应内容: \(json)") self.showError = true
self.isLoading = false
//
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)
} }
} }
} }
task.resume()
} }
// MARK: - Helpers // MARK: - Helpers