import SwiftUI import AuthenticationServices import Alamofire import CryptoKit /// 主登录视图 - 处理苹果登录 struct LoginView: View { // MARK: - Properties @State private var isLoading = false @State private var showError = false @State private var errorMessage = "" @State private var currentNonce: String? @State private var isLoggedIn = false // MARK: - Body var body: some View { NavigationStack { ZStack { // Background Color(red: 1.0, green: 0.67, blue: 0.15) .edgesIgnoringSafeArea(.all) VStack(alignment: .leading, spacing: 4) { Text("Hi, I'm MeMo!") .font(.largeTitle) .fontWeight(.semibold) .foregroundColor(.black) .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 24) .padding(.top, 44) Text("Welcome~") .font(.largeTitle) .fontWeight(.semibold) .foregroundColor(.black) .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 24) .padding(.bottom, 20) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) VStack(spacing: 16) { Spacer() signInButton() termsAndPrivacyView() } .padding() .alert(isPresented: $showError) { Alert( title: Text("Error"), message: Text(errorMessage), dismissButton: .default(Text("OK")) ) } if isLoading { loadingView() } } .navigationBarHidden(true) .fullScreenCover(isPresented: $isLoggedIn) { NavigationStack { UserInfo() } } } } // MARK: - Views private func signInButton() -> some View { SignInWithAppleButton( onRequest: { request in let nonce = String.randomURLSafeString(length: 32) self.currentNonce = nonce request.requestedScopes = [.fullName, .email] request.nonce = self.sha256(nonce) }, onCompletion: handleAppleSignIn ) .signInWithAppleButtonStyle(.white) .frame(height: 50) .cornerRadius(25) .overlay( RoundedRectangle(cornerRadius: 25) .stroke(Color.black, lineWidth: 1) ) } private func termsAndPrivacyView() -> some View { VStack(spacing: 4) { HStack { Text("By continuing, you agree to our") .font(.caption) .foregroundColor(.secondary) Button("Terms of") { openURL("https://yourwebsite.com/terms") } .font(.caption2) .foregroundColor(.blue) } .multilineTextAlignment(.center) .padding(.horizontal, 24) HStack(spacing: 8) { Button("Service") { openURL("https://yourwebsite.com/terms") } .font(.caption2) .foregroundColor(.blue) Text("and") .foregroundColor(.secondary) .font(.caption) Button("Privacy Policy") { openURL("https://yourwebsite.com/privacy") } .font(.caption2) .foregroundColor(.blue) } .padding(.top, 4) } .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity) .padding(.horizontal, 24) .padding(.bottom, 24) } private func loadingView() -> some View { ZStack { Color.black.opacity(0.4) .edgesIgnoringSafeArea(.all) ProgressView() .scaleEffect(1.5) .progressViewStyle(CircularProgressViewStyle(tint: .white)) } } // MARK: - Authentication 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] 登录授权成功") processAppleIDCredential(authResults.credential) case .failure(let error): print("❌ [Apple Sign In] 登录失败: \(error.localizedDescription)") handleSignInError(error) } } private func processAppleIDCredential(_ credential: ASAuthorizationCredential) { print("🔵 [Apple ID] 开始处理凭证...") guard let appleIDCredential = credential as? ASAuthorizationAppleIDCredential else { print("❌ [Apple ID] 凭证类型不匹配") showError(message: "无法处理Apple ID凭证") return } let userId = appleIDCredential.user let email = appleIDCredential.email ?? "" let fullName = [ appleIDCredential.fullName?.givenName, appleIDCredential.fullName?.familyName ] .compactMap { $0 } .joined(separator: " ") print("ℹ️ [Apple ID] 用户数据 - ID: \(userId), 邮箱: \(email.isEmpty ? "未提供" : email), 姓名: \(fullName.isEmpty ? "未提供" : fullName)") guard let identityTokenData = appleIDCredential.identityToken, let identityToken = String(data: identityTokenData, encoding: .utf8) else { print("❌ [Apple ID] 无法获取身份令牌") showError(message: "无法获取身份令牌") return } var authCode: String? = nil if let authCodeData = appleIDCredential.authorizationCode { authCode = String(data: authCodeData, encoding: .utf8) print("ℹ️ [Apple ID] 获取到授权码") } else { print("ℹ️ [Apple ID] 未获取到授权码(可选)") } 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 ) } // 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, ] 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 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)") } // 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) } } } task.resume() } // MARK: - Helpers private func handleSuccessfulAuthentication() { print("✅ [Auth] 登录成功,准备跳转到用户信息页面...") DispatchQueue.main.async { self.isLoggedIn = true } } private func handleSignInError(_ error: Error) { let errorMessage = (error as NSError).localizedDescription print("❌ [Auth] 登录错误: \(errorMessage)") showError(message: "登录失败: \(error.localizedDescription)") } private func handleAuthenticationError(_ error: Error) { let errorMessage = error.localizedDescription print("❌ [Auth] 认证错误: \(errorMessage)") DispatchQueue.main.async { self.isLoggedIn = false self.showError(message: "登录失败: \(errorMessage)") } } private func showError(message: String) { DispatchQueue.main.async { self.errorMessage = message self.showError = true } } private func openURL(_ string: String) { guard let url = URL(string: string) else { return } UIApplication.shared.open(url) } private func sha256(_ input: String) -> String { let inputData = Data(input.utf8) let hashedData = SHA256.hash(data: inputData) return hashedData.compactMap { String(format: "%02x", $0) }.joined() } } // MARK: - Extensions extension String { static func randomURLSafeString(length: Int) -> String { let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~" var randomString = "" for _ in 0..