From 8b4eb9cb7e52222f70e138377fe3231d12434622 Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Wed, 17 Sep 2025 16:37:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0API=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=8F=8A=E8=A7=86=E5=9B=BE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在Info.plist中新增API签名密钥配置。 - 将Splash视图替换为SplashV2,优化启动逻辑和用户体验。 - 更新API请求中的User-Agent逻辑,使用UserAgentProvider提供的动态值。 - 在APILogger中添加敏感信息脱敏处理,增强安全性。 - 新增CreateFeedPage视图,支持用户发布动态功能。 - 更新MainPage和Splash视图的导航逻辑,整合统一的AppRoute管理。 - 移除冗余的SplashFeature视图,提升代码整洁性和可维护性。 --- yana/APIs/API rule.md | 2 +- yana/APIs/APIEndpoints.swift | 2 +- yana/APIs/APILogger.swift | 342 +++++++++++-------- yana/APIs/APIModels.swift | 24 +- yana/APIs/APIService.swift | 36 +- yana/APIs/DynamicsModels.swift | 1 - yana/Configs/AppConfig.swift | 10 +- yana/Features/SplashFeature.swift | 114 ------- yana/Info.plist | 2 + yana/MVVM/CreateFeedPage.swift | 89 +++++ yana/MVVM/LoginPage.swift | 135 ++++---- yana/MVVM/MainPage.swift | 13 +- yana/MVVM/Splash.swift | 182 ---------- yana/MVVM/SplashV2.swift | 63 ++++ yana/MVVM/ViewModel/MainViewModel.swift | 4 +- yana/Utils/Navigation/AppRoute.swift | 11 + yana/Utils/Network/APIService+Combine.swift | 38 +++ yana/Utils/Network/DeviceContext.swift | 54 +++ yana/Utils/Network/NetworkMonitor.swift | 33 ++ yana/Utils/Security/KeychainManager.swift | 5 +- yana/Utils/Security/SigningKeyProvider.swift | 32 ++ yana/Views/SplashView.swift | 91 ----- yana/yanaApp.swift | 3 +- 23 files changed, 640 insertions(+), 646 deletions(-) delete mode 100644 yana/Features/SplashFeature.swift create mode 100644 yana/MVVM/CreateFeedPage.swift delete mode 100644 yana/MVVM/Splash.swift create mode 100644 yana/MVVM/SplashV2.swift create mode 100644 yana/Utils/Navigation/AppRoute.swift create mode 100644 yana/Utils/Network/APIService+Combine.swift create mode 100644 yana/Utils/Network/DeviceContext.swift create mode 100644 yana/Utils/Network/NetworkMonitor.swift create mode 100644 yana/Utils/Security/SigningKeyProvider.swift delete mode 100644 yana/Views/SplashView.swift diff --git a/yana/APIs/API rule.md b/yana/APIs/API rule.md index 1863dcb..9253cd0 100644 --- a/yana/APIs/API rule.md +++ b/yana/APIs/API rule.md @@ -16,7 +16,7 @@ | 环境 | 地址 | 说明 | |------|------|------| | 生产环境 | `https://api.epartylive.com` | 正式服务器 | -| 测试环境 | `http://beta.api.molistar.xyz` | 开发测试服务器 | +| 测试环境 | `http://beta.api.pekolive.com` | 开发测试服务器 | | 图片服务 | `https://image.hfighting.com` | 静态资源服务器 | **环境切换机制:** diff --git a/yana/APIs/APIEndpoints.swift b/yana/APIs/APIEndpoints.swift index 1042fdf..31146f7 100644 --- a/yana/APIs/APIEndpoints.swift +++ b/yana/APIs/APIEndpoints.swift @@ -102,7 +102,7 @@ struct APIConfiguration { "Accept-Encoding": "gzip, br", "Accept-Language": Locale.current.language.languageCode?.identifier ?? "en", "App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0", - "User-Agent": "YuMi/20.20.61 (iPhone; iOS 17+; Scale/2.00)" + "User-Agent": await UserAgentProvider.userAgent() ] // 检查用户认证状态并添加相关 headers let authStatus = await UserInfoManager.checkAuthenticationStatus() diff --git a/yana/APIs/APILogger.swift b/yana/APIs/APILogger.swift index 862b519..92193e3 100644 --- a/yana/APIs/APILogger.swift +++ b/yana/APIs/APILogger.swift @@ -1,7 +1,6 @@ import Foundation // MARK: - API Logger -@MainActor class APILogger { enum LogLevel { case none @@ -10,19 +9,82 @@ class APILogger { } #if DEBUG - static var logLevel: LogLevel = .detailed + static let logLevel: LogLevel = .detailed #else - static var logLevel: LogLevel = .none + static let logLevel: LogLevel = .none #endif + private static let logQueue = DispatchQueue(label: "com.yana.api.logger", qos: .utility) + private static let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "HH:mm:ss.SSS" return formatter }() + + // MARK: - Redaction + /// 需要脱敏的敏感字段(统一小写匹配) + private static let sensitiveKeys: Set = [ + "authorization", "token", "access-token", "access_token", "refresh-token", "refresh_token", + "password", "passwd", "secret", "pub_ticket", "ticket", "set-cookie", "cookie" + ] + /// 对字符串做中间遮罩,保留前后若干字符 + private static func maskString(_ value: String, keepPrefix: Int = 3, keepSuffix: Int = 2) -> String { + guard !value.isEmpty else { return value } + if value.count <= keepPrefix + keepSuffix { return String(repeating: "*", count: value.count) } + let start = value.startIndex + let prefixEnd = value.index(start, offsetBy: keepPrefix) + let suffixStart = value.index(value.endIndex, offsetBy: -keepSuffix) + let prefix = value[start.. [String: String] { + var masked: [String: String] = [:] + for (key, value) in headers { + if sensitiveKeys.contains(key.lowercased()) { + masked[key] = maskString(value) + } else { + masked[key] = value + } + } + return masked + } + /// 递归地对 JSON 对象进行脱敏 + private static func redactJSONObject(_ obj: Any) -> Any { + if let dict = obj as? [String: Any] { + var newDict: [String: Any] = [:] + for (k, v) in dict { + if sensitiveKeys.contains(k.lowercased()) { + if let str = v as? String { newDict[k] = maskString(str) } + else { newDict[k] = "" } + } else { + newDict[k] = redactJSONObject(v) + } + } + return newDict + } else if let arr = obj as? [Any] { + return arr.map { redactJSONObject($0) } + } else { + return obj + } + } + /// 将请求体 Data 以 Pretty JSON(脱敏后)或摘要形式输出 + private static func maskedBodyString(from body: Data?) -> String { + guard let body = body, !body.isEmpty else { return "No body" } + if let json = try? JSONSerialization.jsonObject(with: body, options: []) { + let redacted = redactJSONObject(json) + if let pretty = try? JSONSerialization.data(withJSONObject: redacted, options: [.prettyPrinted]), + let prettyString = String(data: pretty, encoding: .utf8) { + return prettyString + } + } + return " (\(body.count) bytes)" + } // MARK: - Request Logging - @MainActor static func logRequest( + static func logRequest( _ request: T, url: URL, body: Data?, @@ -34,85 +96,70 @@ class APILogger { return #endif - let timestamp = dateFormatter.string(from: Date()) - - print("\n🚀 [API Request] [\(timestamp)] ==================") - print("📍 Endpoint: \(request.endpoint)") - print("🔗 Full URL: \(url.absoluteString)") - print("📝 Method: \(request.method.rawValue)") - print("⏰ Timeout: \(request.timeout)s") - - // 显示最终的完整 headers(包括默认 headers 和自定义 headers) - if let headers = finalHeaders, !headers.isEmpty { - if logLevel == .detailed { - print("📋 Final Headers (包括默认 + 自定义):") - for (key, value) in headers.sorted(by: { $0.key < $1.key }) { - print(" \(key): \(value)") - } - } else if logLevel == .basic { - print("📋 Headers: \(headers.count) 个 headers") - // 只显示重要的 headers - let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"] - for key in importantHeaders { - if let value = headers[key] { - print(" \(key): \(value)") + logQueue.async { + let timestamp = dateFormatter.string(from: Date()) + debugInfoSync("\n🚀 [API Request] [\(timestamp)] ==================") + debugInfoSync("📍 Endpoint: \(request.endpoint)") + debugInfoSync("🔗 Full URL: \(url.absoluteString)") + debugInfoSync("📝 Method: \(request.method.rawValue)") + debugInfoSync("⏰ Timeout: \(request.timeout)s") + + // 显示最终的完整 headers(包括默认 headers 和自定义 headers) + if let headers = finalHeaders, !headers.isEmpty { + if logLevel == .detailed { + debugInfoSync("📋 Final Headers (包括默认 + 自定义):") + let masked = maskHeaders(headers) + for (key, value) in masked.sorted(by: { $0.key < $1.key }) { + debugInfoSync(" \(key): \(value)") + } + } else if logLevel == .basic { + debugInfoSync("📋 Headers: \(headers.count) 个 headers") + // 只显示重要的 headers + let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"] + let masked = maskHeaders(headers) + for key in importantHeaders { + if let value = masked[key] { + debugInfoSync(" \(key): \(value)") + } } } - } - } else if let customHeaders = request.headers, !customHeaders.isEmpty { - print("📋 Custom Headers:") - for (key, value) in customHeaders.sorted(by: { $0.key < $1.key }) { - print(" \(key): \(value)") - } - } else { - print("📋 Headers: 使用默认 headers") - } - - if let queryParams = request.queryParameters, !queryParams.isEmpty { - print("🔍 Query Parameters:") - for (key, value) in queryParams.sorted(by: { $0.key < $1.key }) { - print(" \(key): \(value)") - } - } - - if logLevel == .detailed { - if let body = body { - print("📦 Request Body (\(body.count) bytes):") - if let jsonObject = try? JSONSerialization.jsonObject(with: body, options: []), - let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted), - let prettyString = String(data: prettyData, encoding: .utf8) { - print(prettyString) - } else if let rawString = String(data: body, encoding: .utf8) { - print(rawString) - } else { - print("Binary data") + } else if let customHeaders = request.headers, !customHeaders.isEmpty { + debugInfoSync("📋 Custom Headers:") + let masked = maskHeaders(customHeaders) + for (key, value) in masked.sorted(by: { $0.key < $1.key }) { + debugInfoSync(" \(key): \(value)") } } else { - print("📦 Request Body: No body") + debugInfoSync("📋 Headers: 使用默认 headers") } - // 显示基础参数信息(仅详细模式) - if request.includeBaseParameters { - print("📱 Base Parameters: 自动注入设备和应用信息") - let baseParams = BaseRequest() - print(" Device: \(baseParams.model), OS: \(baseParams.os) \(baseParams.osVersion)") - print(" App: \(baseParams.app) v\(baseParams.appVersion)") - print(" Language: \(baseParams.acceptLanguage)") - } - } else if logLevel == .basic { - if let body = body { - print("📦 Request Body: \(formatBytes(body.count))") - } else { - print("📦 Request Body: No body") + if let queryParams = request.queryParameters, !queryParams.isEmpty { + debugInfoSync("🔍 Query Parameters:") + for (key, value) in queryParams.sorted(by: { $0.key < $1.key }) { + let masked = sensitiveKeys.contains(key.lowercased()) ? maskString(value) : value + debugInfoSync(" \(key): \(masked)") + } } - // 基础模式也显示是否包含基础参数 - if request.includeBaseParameters { - print("📱 Base Parameters: 已自动注入") + if logLevel == .detailed { + let pretty = maskedBodyString(from: body) + debugInfoSync("📦 Request Body: \n\(pretty)") + + // 仅提示包含基础参数,避免跨 actor 读取 UIKit 信息 + if request.includeBaseParameters { + debugInfoSync("📱 Base Parameters: 已自动注入") + } + } else if logLevel == .basic { + let size = body?.count ?? 0 + debugInfoSync("📦 Request Body: \(formatBytes(size))") + + // 基础模式也显示是否包含基础参数 + if request.includeBaseParameters { + debugInfoSync("📱 Base Parameters: 已自动注入") + } } + debugInfoSync("=====================================") } - - print("=====================================") } // MARK: - Response Logging @@ -123,36 +170,41 @@ class APILogger { return #endif - let timestamp = dateFormatter.string(from: Date()) - let statusEmoji = response.statusCode < 400 ? "✅" : "❌" - - print("\n\(statusEmoji) [API Response] [\(timestamp)] ===================") - print("⏱️ Duration: \(String(format: "%.3f", duration))s") - print("📊 Status Code: \(response.statusCode)") - print("🔗 URL: \(response.url?.absoluteString ?? "Unknown")") - print("📏 Data Size: \(formatBytes(data.count))") - - if logLevel == .detailed { - print("📋 Response Headers:") - for (key, value) in response.allHeaderFields.sorted(by: { "\($0.key)" < "\($1.key)" }) { - print(" \(key): \(value)") - } + logQueue.async { + let timestamp = dateFormatter.string(from: Date()) + let statusEmoji = response.statusCode < 400 ? "✅" : "❌" + debugInfoSync("\n\(statusEmoji) [API Response] [\(timestamp)] ===================") + debugInfoSync("⏱️ Duration: \(String(format: "%.3f", duration))s") + debugInfoSync("📊 Status Code: \(response.statusCode)") + debugInfoSync("🔗 URL: \(response.url?.absoluteString ?? "Unknown")") + debugInfoSync("📏 Data Size: \(formatBytes(data.count))") - print("📦 Response Data:") - if data.isEmpty { - print(" Empty response") - } else if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), - let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted), - let prettyString = String(data: prettyData, encoding: .utf8) { - print(prettyString) - } else if let rawString = String(data: data, encoding: .utf8) { - print(rawString) - } else { - print(" Binary data (\(data.count) bytes)") + if logLevel == .detailed { + debugInfoSync("📋 Response Headers:") + // 将 headers 转为 [String:String] 后脱敏 + var headers: [String: String] = [:] + for (k, v) in response.allHeaderFields { headers["\(k)"] = "\(v)" } + let masked = maskHeaders(headers) + for (key, value) in masked.sorted(by: { $0.key < $1.key }) { + debugInfoSync(" \(key): \(value)") + } + + debugInfoSync("📦 Response Data:") + if data.isEmpty { + debugInfoSync(" Empty response") + } else if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), + let prettyData = try? JSONSerialization.data(withJSONObject: redactJSONObject(jsonObject), options: .prettyPrinted), + let prettyString = String(data: prettyData, encoding: .utf8) { + debugInfoSync(prettyString) + } else if let _ = String(data: data, encoding: .utf8) { + // 对非 JSON 文本响应不做内容回显,避免泄漏 + debugInfoSync(" (\(data.count) bytes)") + } else { + debugInfoSync(" Binary data (\(data.count) bytes)") + } } + debugInfoSync("=====================================") } - - print("=====================================") } // MARK: - Error Logging @@ -163,43 +215,43 @@ class APILogger { return #endif - let timestamp = dateFormatter.string(from: Date()) - - print("\n❌ [API Error] [\(timestamp)] ======================") - print("⏱️ Duration: \(String(format: "%.3f", duration))s") - if let url = url { - print("🔗 URL: \(url.absoluteString)") - } - - if let apiError = error as? APIError { - print("🚨 API Error: \(apiError.localizedDescription)") - } else { - print("🚨 System Error: \(error.localizedDescription)") - } - - if logLevel == .detailed { - if let urlError = error as? URLError { - print("🔍 URLError Code: \(urlError.code.rawValue)") - print("🔍 URLError Localized: \(urlError.localizedDescription)") - - // 详细的网络错误分析 - switch urlError.code { - case .timedOut: - print("💡 建议:检查网络连接或增加超时时间") - case .notConnectedToInternet: - print("💡 建议:检查网络连接") - case .cannotConnectToHost: - print("💡 建议:检查服务器地址和端口") - case .resourceUnavailable: - print("💡 建议:检查 API 端点是否正确") - default: - break - } + logQueue.async { + let timestamp = dateFormatter.string(from: Date()) + debugErrorSync("\n❌ [API Error] [\(timestamp)] ======================") + debugErrorSync("⏱️ Duration: \(String(format: "%.3f", duration))s") + if let url = url { + debugErrorSync("🔗 URL: \(url.absoluteString)") } - print("🔍 Full Error: \(error)") + + if let apiError = error as? APIError { + debugErrorSync("🚨 API Error: \(apiError.localizedDescription)") + } else { + debugErrorSync("🚨 System Error: \(error.localizedDescription)") + } + + if logLevel == .detailed { + if let urlError = error as? URLError { + debugInfoSync("🔍 URLError Code: \(urlError.code.rawValue)") + debugInfoSync("🔍 URLError Localized: \(urlError.localizedDescription)") + + // 详细的网络错误分析 + switch urlError.code { + case .timedOut: + debugWarnSync("💡 建议:检查网络连接或增加超时时间") + case .notConnectedToInternet: + debugWarnSync("💡 建议:检查网络连接") + case .cannotConnectToHost: + debugWarnSync("💡 建议:检查服务器地址和端口") + case .resourceUnavailable: + debugWarnSync("💡 建议:检查 API 端点是否正确") + default: + break + } + } + debugInfoSync("🔍 Full Error: \(error)") + } + debugErrorSync("=====================================\n") } - - print("=====================================\n") } // MARK: - Decoded Response Logging @@ -210,9 +262,11 @@ class APILogger { return #endif - let timestamp = dateFormatter.string(from: Date()) - print("🎯 [Decoded Response] [\(timestamp)] Type: \(type)") - print("=====================================\n") + logQueue.async { + let timestamp = dateFormatter.string(from: Date()) + debugInfoSync("🎯 [Decoded Response] [\(timestamp)] Type: \(type)") + debugInfoSync("=====================================\n") + } } // MARK: - Helper Methods @@ -231,10 +285,12 @@ class APILogger { return #endif - let timestamp = dateFormatter.string(from: Date()) - print("\n⚠️ [Performance Warning] [\(timestamp)] ============") - print("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)") - print("💡 建议:检查网络条件或优化 API 响应") - print("================================================\n") + logQueue.async { + let timestamp = dateFormatter.string(from: Date()) + debugWarnSync("\n⚠️ [Performance Warning] [\(timestamp)] ============") + debugWarnSync("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)") + debugWarnSync("💡 建议:检查网络条件或优化 API 响应") + debugWarnSync("================================================\n") + } } } diff --git a/yana/APIs/APIModels.swift b/yana/APIs/APIModels.swift index 6c0d52d..d1885f1 100644 --- a/yana/APIs/APIModels.swift +++ b/yana/APIs/APIModels.swift @@ -1,5 +1,4 @@ import Foundation -import ComposableArchitecture // MARK: - HTTP Method @@ -205,8 +204,9 @@ struct BaseRequest: Codable { "\(key)=\(String(describing: filteredParams[key] ?? ""))" }.joined(separator: "&") - // 4. 添加密钥 - let keyString = "key=rpbs6us1m8r2j9g6u06ff2bo18orwaya" + // 4. 添加密钥(从配置提供者获取) + let key = SigningKeyProvider.signingKey() + let keyString = "key=\(key)" let finalString = paramString.isEmpty ? keyString : "\(paramString)&\(keyString)" // 5. 生成大写 MD5 签名 @@ -217,9 +217,8 @@ struct BaseRequest: Codable { // MARK: - Network Type Detector struct NetworkTypeDetector { static func getCurrentNetworkType() -> Int { - // WiFi = 2, 蜂窝网络 = 1 - // 这里是简化实现,实际应该检测网络状态 - return 2 // 默认蜂窝网络 + // WiFi = 2, 蜂窝网络 = 1, 其他/无网络 = 0 + return NetworkMonitor.shared.currentType } } @@ -238,7 +237,6 @@ struct CarrierInfoManager { // MARK: - User Info Manager (for Headers) struct UserInfoManager { - @MainActor private static let keychain = KeychainManager.shared // MARK: - Storage Keys @@ -287,7 +285,7 @@ struct UserInfoManager { // MARK: - User Info Management static func saveUserInfo(_ userInfo: UserInfo) async { do { - try await keychain.store(userInfo, forKey: StorageKeys.userInfo) + try keychain.store(userInfo, forKey: StorageKeys.userInfo) await cacheActor.setUserInfo(userInfo) debugInfoSync("💾 保存用户信息成功") } catch { @@ -302,7 +300,7 @@ struct UserInfoManager { } // 从 Keychain 读取 do { - let userInfo = try await keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo) + let userInfo = try keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo) await cacheActor.setUserInfo(userInfo) return userInfo } catch { @@ -377,7 +375,7 @@ struct UserInfoManager { /// - Parameter accountModel: 要保存的账户模型 static func saveAccountModel(_ accountModel: AccountModel) async { do { - try await keychain.store(accountModel, forKey: StorageKeys.accountModel) + try keychain.store(accountModel, forKey: StorageKeys.accountModel) await cacheActor.setAccountModel(accountModel) // 同步更新 ticket 到内存 @@ -400,7 +398,7 @@ struct UserInfoManager { } // 从 Keychain 读取 do { - let accountModel = try await keychain.retrieve( + let accountModel = try keychain.retrieve( AccountModel.self, forKey: StorageKeys.accountModel ) @@ -448,7 +446,7 @@ struct UserInfoManager { /// 清除 AccountModel static func clearAccountModel() async { do { - try await keychain.delete(forKey: StorageKeys.accountModel) + try keychain.delete(forKey: StorageKeys.accountModel) await cacheActor.clearAccountModel() debugInfoSync("🗑️ AccountModel 已清除") } catch { @@ -459,7 +457,7 @@ struct UserInfoManager { /// 清除用户信息 static func clearUserInfo() async { do { - try await keychain.delete(forKey: StorageKeys.userInfo) + try keychain.delete(forKey: StorageKeys.userInfo) await cacheActor.clearUserInfo() debugInfoSync("🗑️ UserInfo 已清除") } catch { diff --git a/yana/APIs/APIService.swift b/yana/APIs/APIService.swift index 983c10c..eedc0ff 100644 --- a/yana/APIs/APIService.swift +++ b/yana/APIs/APIService.swift @@ -1,5 +1,4 @@ import Foundation -import ComposableArchitecture // MARK: - API Service Protocol @@ -136,10 +135,7 @@ struct LiveAPIService: APIServiceProtocol, Sendable { requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: []) urlRequest.httpBody = requestBody // urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - if let httpBody = urlRequest.httpBody, - let bodyString = String(data: httpBody, encoding: .utf8) { - debugInfoSync("HTTP Body: \(bodyString)") - } + // HTTP Body 的详细输出由 APILogger 统一处理(带脱敏)。这里不再重复输出。 } catch { let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)") await APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription) @@ -148,8 +144,7 @@ struct LiveAPIService: APIServiceProtocol, Sendable { } // 记录请求日志,传递完整的 headers 信息 - await APILogger - .logRequest(request, url: url, body: requestBody, finalHeaders: headers) + APILogger.logRequest(request, url: url, body: requestBody, finalHeaders: headers) do { // 发起请求 @@ -165,18 +160,16 @@ struct LiveAPIService: APIServiceProtocol, Sendable { // 检查数据大小 if data.count > APIConfiguration.maxDataSize { - await APILogger - .logError(APIError.resourceTooLarge, url: url, duration: duration) + APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration) await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription) throw APIError.resourceTooLarge } // 记录响应日志 - await APILogger - .logResponse(data: data, response: httpResponse, duration: duration) + APILogger.logResponse(data: data, response: httpResponse, duration: duration) // 性能警告 - await APILogger.logPerformanceWarning(duration: duration) + APILogger.logPerformanceWarning(duration: duration) // 检查 HTTP 状态码 guard 200...299 ~= httpResponse.statusCode else { @@ -196,7 +189,7 @@ struct LiveAPIService: APIServiceProtocol, Sendable { do { let decoder = JSONDecoder() let decodedResponse = try decoder.decode(T.Response.self, from: data) - await APILogger.logDecodedResponse(decodedResponse, type: T.Response.self) + APILogger.logDecodedResponse(decodedResponse, type: T.Response.self) // 请求成功,完成 loading await APILoadingManager.shared.finishLoading(loadingId) @@ -210,13 +203,13 @@ struct LiveAPIService: APIServiceProtocol, Sendable { } catch let error as APIError { let duration = Date().timeIntervalSince(startTime) - await APILogger.logError(error, url: url, duration: duration) + 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) - await APILogger.logError(apiError, url: url, duration: duration) + APILogger.logError(apiError, url: url, duration: duration) await APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription) throw apiError } @@ -300,6 +293,14 @@ struct LiveAPIService: APIServiceProtocol, Sendable { 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 @@ -349,7 +350,9 @@ actor MockAPIServiceActor: APIServiceProtocol, Sendable { } } -// MARK: - TCA Dependency Integration +// 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() @@ -361,6 +364,7 @@ extension DependencyValues { set { self[APIServiceKey.self] = newValue } } } +#endif // MARK: - BaseRequest Dictionary Conversion extension BaseRequest { diff --git a/yana/APIs/DynamicsModels.swift b/yana/APIs/DynamicsModels.swift index b675fdb..ce57f30 100644 --- a/yana/APIs/DynamicsModels.swift +++ b/yana/APIs/DynamicsModels.swift @@ -1,5 +1,4 @@ import Foundation -import ComposableArchitecture // MARK: - 响应数据模型 diff --git a/yana/Configs/AppConfig.swift b/yana/Configs/AppConfig.swift index 5166e0a..290dbc2 100644 --- a/yana/Configs/AppConfig.swift +++ b/yana/Configs/AppConfig.swift @@ -5,17 +5,17 @@ enum AppEnvironment { struct AppConfig { static let current: AppEnvironment = { -// #if DEBUG -// return .development -// #else + #if DEBUG + return .development + #else return .production -// #endif + #endif }() static var baseURL: String { switch current { case .development: - return "http://beta.api.molistar.xyz" + return "http://beta.api.molistar.xyz";//"http://beta.api.pekolive.com" case .production: return "https://api.epartylive.com" } diff --git a/yana/Features/SplashFeature.swift b/yana/Features/SplashFeature.swift deleted file mode 100644 index 6c70321..0000000 --- a/yana/Features/SplashFeature.swift +++ /dev/null @@ -1,114 +0,0 @@ -import Foundation -import ComposableArchitecture - -@Reducer -struct SplashFeature { - @ObservableState - struct State: Equatable { - var isLoading = true - var shouldShowMainApp = false - var authenticationStatus: UserInfoManager.AuthenticationStatus = .notFound - var isCheckingAuthentication = false - - // 新增:导航目标 - var navigationDestination: NavigationDestination? - - init() { - // 默认初始化 - } - } - - // 新增:导航目标枚举 - enum NavigationDestination: Equatable { - case login // 跳转到登录页面 - case main // 跳转到主页面 - } - - enum Action: Equatable { - case onAppear - case splashFinished - case checkAuthentication - case authenticationChecked(UserInfoManager.AuthenticationStatus) - - // 新增:用户信息获取 actions - case fetchUserInfo - case userInfoFetched(Bool) - - // 新增:导航 actions - case navigateToLogin - case navigateToMain - } - - @Dependency(\.apiService) var apiService // 新增:API服务依赖 - - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .onAppear: - state.isLoading = true - state.shouldShowMainApp = false - state.authenticationStatus = UserInfoManager.AuthenticationStatus.notFound - state.isCheckingAuthentication = false - state.navigationDestination = nil - - // 1秒延迟后显示主应用 (iOS 15.5+ 兼容) - return .run { send in - try await Task.sleep(nanoseconds: 1_000_000_000) // 1秒 = 1,000,000,000 纳秒 - await send(.splashFinished) - } - case .splashFinished: - state.isLoading = false - - // Splash 完成后,开始检查认证状态 - return .send(.checkAuthentication) - - case .checkAuthentication: - state.isCheckingAuthentication = true - - // 异步检查认证状态 - return .run { send in - let authStatus = await UserInfoManager.checkAuthenticationStatus() - await send(.authenticationChecked(authStatus)) - } - - case let .authenticationChecked(status): - state.isCheckingAuthentication = false - state.authenticationStatus = status - - // 根据认证状态决定下一步操作 - if status.canAutoLogin { - debugInfoSync("🎉 自动登录成功,开始获取用户信息") - // 新增:认证成功后自动获取用户信息 - return .send(.fetchUserInfo) - } else { - debugInfoSync("🔑 需要手动登录") - return .send(.navigateToLogin) - } - - case .fetchUserInfo: - // 新增:获取用户信息 - return .run { send in - let success = await UserInfoManager.autoFetchUserInfoOnAppLaunch(apiService: apiService) - await send(.userInfoFetched(success)) - } - - case let .userInfoFetched(success): - if success { - debugInfoSync("✅ 用户信息获取成功,进入主页") - } else { - debugInfoSync("⚠️ 用户信息获取失败,但仍进入主页") - } - return .send(.navigateToMain) - - case .navigateToLogin: - state.navigationDestination = .login - return .none - - case .navigateToMain: - state.navigationDestination = .main - state.shouldShowMainApp = true - return .none - } - } - } -} diff --git a/yana/Info.plist b/yana/Info.plist index 3aae323..37862f5 100644 --- a/yana/Info.plist +++ b/yana/Info.plist @@ -15,5 +15,7 @@ Bayon-Regular.ttf + API_SIGNING_KEY + diff --git a/yana/MVVM/CreateFeedPage.swift b/yana/MVVM/CreateFeedPage.swift new file mode 100644 index 0000000..e24da90 --- /dev/null +++ b/yana/MVVM/CreateFeedPage.swift @@ -0,0 +1,89 @@ +import SwiftUI + +@MainActor +final class CreateFeedViewModel: ObservableObject { + @Published var content: String = "" + @Published var selectedImages: [UIImage] = [] + @Published var isPublishing: Bool = false + @Published var errorMessage: String? = nil + var canPublish: Bool { !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !selectedImages.isEmpty } +} + +struct CreateFeedPage: View { + @StateObject private var viewModel = CreateFeedViewModel() + let onDismiss: () -> Void + + var body: some View { + GeometryReader { _ in + ZStack { + Color(hex: 0x0C0527).ignoresSafeArea() + VStack(spacing: 16) { + HStack { + Button(action: onDismiss) { + Image(systemName: "xmark") + .foregroundColor(.white) + .font(.system(size: 18, weight: .medium)) + } + Spacer() + Text(LocalizedString("createFeed.title", comment: "Image & Text Publish")) + .foregroundColor(.white) + .font(.system(size: 18, weight: .medium)) + Spacer() + Button(action: publish) { + if viewModel.isPublishing { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + } else { + Text(LocalizedString("createFeed.publish", comment: "Publish")) + .foregroundColor(.white) + .font(.system(size: 14, weight: .medium)) + } + } + .disabled(!viewModel.canPublish || viewModel.isPublishing) + .opacity((!viewModel.canPublish || viewModel.isPublishing) ? 0.6 : 1) + } + .padding(.horizontal, 16) + .padding(.top, 12) + + ZStack(alignment: .topLeading) { + RoundedRectangle(cornerRadius: 8).fill(Color(hex: 0x1C143A)) + if viewModel.content.isEmpty { + Text(LocalizedString("createFeed.enterContent", comment: "Enter Content")) + .foregroundColor(.white.opacity(0.5)) + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + TextEditor(text: $viewModel.content) + .foregroundColor(.white) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .scrollContentBackground(.hidden) + .frame(height: 200) + } + .frame(height: 200) + .padding(.horizontal, 20) + + if let error = viewModel.errorMessage { + Text(error) + .foregroundColor(.red) + .font(.system(size: 14)) + } + + Spacer() + } + } + } + .navigationBarBackButtonHidden(true) + } + + private func publish() { + viewModel.isPublishing = true + Task { @MainActor in + try? await Task.sleep(nanoseconds: 500_000_000) + viewModel.isPublishing = false + onDismiss() + } + } +} + + diff --git a/yana/MVVM/LoginPage.swift b/yana/MVVM/LoginPage.swift index e1377c4..f637992 100644 --- a/yana/MVVM/LoginPage.swift +++ b/yana/MVVM/LoginPage.swift @@ -16,6 +16,7 @@ class LoginViewModel: ObservableObject { // MARK: - Callbacks var onLoginSuccess: (() -> Void)? + private var hasSentSuccess: Bool = false // MARK: - Public Methods func onIDLoginTapped() { @@ -47,22 +48,20 @@ class LoginViewModel: ObservableObject { } func onLoginCompleted() { + guard !hasSentSuccess else { return } isAnyLoginCompleted = true + showIDLogin = false + showEmailLogin = false + hasSentSuccess = true onLoginSuccess?() } func onBackFromIDLogin() { showIDLogin = false - if isAnyLoginCompleted { - onLoginSuccess?() - } } func onBackFromEmailLogin() { showEmailLogin = false - if isAnyLoginCompleted { - onLoginSuccess?() - } } } @@ -73,78 +72,76 @@ struct LoginPage: View { let onLoginSuccess: () -> Void var body: some View { - NavigationStack { - GeometryReader { geometry in - ZStack { - backgroundView - - VStack(spacing: 0) { - Image("top") - .resizable() - .aspectRatio(375/400, contentMode: .fit) - .frame(maxWidth: .infinity) + GeometryReader { geometry in + ZStack { + backgroundView + + VStack(spacing: 0) { + Image("top") + .resizable() + .aspectRatio(375/400, contentMode: .fit) + .frame(maxWidth: .infinity) - HStack { - Text(LocalizedString("login.app_title", comment: "")) - .font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width)) - .foregroundColor(.white) - .padding(.leading, 20) - Spacer() - } - .padding(.bottom, 20) // 距离 top 图片底部的间距 - + HStack { + Text(LocalizedString("login.app_title", comment: "")) + .font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width)) + .foregroundColor(.white) + .padding(.leading, 20) Spacer() - - bottomSection } + .padding(.bottom, 20) - // 语言设置按钮 - 固定在页面右上角 - languageSettingsButton - .position(x: geometry.size.width - 40, y: 60) + Spacer() - APILoadingEffectView() + bottomSection } + + // 语言设置按钮 - 固定在页面右上角 + languageSettingsButton + .position(x: geometry.size.width - 40, y: 60) + + APILoadingEffectView() } - .ignoresSafeArea() + } + .ignoresSafeArea() + .navigationBarHidden(true) + .navigationDestination(isPresented: $viewModel.showIDLogin) { + IDLoginPage( + onBack: { + viewModel.onBackFromIDLogin() + }, + onLoginSuccess: { + viewModel.onLoginCompleted() + } + ) .navigationBarHidden(true) - .navigationDestination(isPresented: $viewModel.showIDLogin) { - IDLoginPage( - onBack: { - viewModel.onBackFromIDLogin() - }, - onLoginSuccess: { - viewModel.onLoginCompleted() - } - ) - .navigationBarHidden(true) - } - .navigationDestination(isPresented: $viewModel.showEmailLogin) { - EMailLoginPage( - onBack: { - viewModel.onBackFromEmailLogin() - }, - onLoginSuccess: { - viewModel.onLoginCompleted() - } - ) - .navigationBarHidden(true) - } - .sheet(isPresented: $viewModel.showLanguageSettings) { - LanguageSettingsView(isPresented: $viewModel.showLanguageSettings) - } - .webView( - isPresented: $viewModel.showUserAgreement, - url: APIConfiguration.webURL(for: .userAgreement) + } + .navigationDestination(isPresented: $viewModel.showEmailLogin) { + EMailLoginPage( + onBack: { + viewModel.onBackFromEmailLogin() + }, + onLoginSuccess: { + viewModel.onLoginCompleted() + } ) - .webView( - isPresented: $viewModel.showPrivacyPolicy, - url: APIConfiguration.webURL(for: .privacyPolicy) - ) - .alert(LocalizedString("login.agreement_alert_title", comment: ""), isPresented: $viewModel.showAgreementAlert) { - Button(LocalizedString("login.agreement_alert_confirm", comment: "")) { } - } message: { - Text(LocalizedString("login.agreement_alert_message", comment: "")) - } + .navigationBarHidden(true) + } + .sheet(isPresented: $viewModel.showLanguageSettings) { + LanguageSettingsView(isPresented: $viewModel.showLanguageSettings) + } + .webView( + isPresented: $viewModel.showUserAgreement, + url: APIConfiguration.webURL(for: .userAgreement) + ) + .webView( + isPresented: $viewModel.showPrivacyPolicy, + url: APIConfiguration.webURL(for: .privacyPolicy) + ) + .alert(LocalizedString("login.agreement_alert_title", comment: ""), isPresented: $viewModel.showAgreementAlert) { + Button(LocalizedString("login.agreement_alert_confirm", comment: "")) { } + } message: { + Text(LocalizedString("login.agreement_alert_message", comment: "")) } .onAppear { viewModel.onLoginSuccess = onLoginSuccess diff --git a/yana/MVVM/MainPage.swift b/yana/MVVM/MainPage.swift index c10af81..0849d16 100644 --- a/yana/MVVM/MainPage.swift +++ b/yana/MVVM/MainPage.swift @@ -31,9 +31,10 @@ struct MainPage: View { } } } - .navigationDestination(for: String.self) { destination in - switch destination { - case "setting": + .navigationDestination(for: String.self) { _ in EmptyView() } + .navigationDestination(for: AppRoute.self) { route in + switch route { + case .setting: SettingPage( onBack: { viewModel.navigationPath.removeLast() @@ -43,6 +44,12 @@ struct MainPage: View { } ) .navigationBarHidden(true) + case .publish: + CreateFeedPage( + onDismiss: { + viewModel.navigationPath.removeLast() + } + ) default: EmptyView() } diff --git a/yana/MVVM/Splash.swift b/yana/MVVM/Splash.swift deleted file mode 100644 index 58ea7a9..0000000 --- a/yana/MVVM/Splash.swift +++ /dev/null @@ -1,182 +0,0 @@ -import SwiftUI -import Combine - -// MARK: - Splash ViewModel - -@MainActor -class SplashViewModel: ObservableObject { - // MARK: - Published Properties - @Published var isLoading = true - @Published var shouldShowMainApp = false - @Published var authenticationStatus: UserInfoManager.AuthenticationStatus = .notFound - @Published var isCheckingAuthentication = false - @Published var navigationDestination: NavigationDestination? - - // MARK: - Private Properties - private var cancellables = Set() - - // MARK: - Navigation Destination - enum NavigationDestination: Equatable { - case login - case main - } - - // MARK: - Initialization - init() { - setupBindings() - } - - // MARK: - Public Methods - func onAppear() { - isLoading = true - shouldShowMainApp = false - authenticationStatus = .notFound - isCheckingAuthentication = false - navigationDestination = nil - - // 1秒延迟后显示主应用 - Task { - try await Task.sleep(nanoseconds: 1_000_000_000) - await MainActor.run { - self.splashFinished() - } - } - } - - func splashFinished() { - isLoading = false - checkAuthentication() - } - - func checkAuthentication() { - isCheckingAuthentication = true - - Task { - let authStatus = await UserInfoManager.checkAuthenticationStatus() - await MainActor.run { - self.authenticationChecked(authStatus) - } - } - } - - func authenticationChecked(_ status: UserInfoManager.AuthenticationStatus) { - isCheckingAuthentication = false - authenticationStatus = status - - if status.canAutoLogin { - debugInfoSync("🎉 自动登录成功,开始获取用户信息") - fetchUserInfo() - } else { - debugInfoSync("🔑 需要手动登录") - navigateToLogin() - } - } - - func fetchUserInfo() { - Task { - let success = await UserInfoManager.autoFetchUserInfoOnAppLaunch(apiService: LiveAPIService()) - await MainActor.run { - self.userInfoFetched(success) - } - } - } - - func userInfoFetched(_ success: Bool) { - if success { - debugInfoSync("✅ 用户信息获取成功,进入主页") - } else { - debugInfoSync("⚠️ 用户信息获取失败,但仍进入主页") - } - navigateToMain() - } - - func navigateToLogin() { - navigationDestination = .login - } - - func navigateToMain() { - navigationDestination = .main - shouldShowMainApp = true - } - - // MARK: - Private Methods - private func setupBindings() { - // 可以在这里设置 Combine 绑定 - } -} - -// MARK: - Splash View - -struct Splash: View { - @StateObject private var viewModel = SplashViewModel() - - var body: some View { - ZStack { - Group { - // 根据导航目标显示不同页面 - if let navigationDestination = viewModel.navigationDestination { - switch navigationDestination { - case .login: - // 显示登录页面 - LoginPage( - onLoginSuccess: { - // 登录成功后导航到主页面 - viewModel.navigateToMain() - } - ) - case .main: - // 显示主应用页面 - MainPage( - onLogout: { - viewModel.navigateToLogin() - } - ) - } - } else { - // 显示启动画面 - splashContent - } - } - .onAppear { - viewModel.onAppear() - } - - // API Loading 效果视图 - 显示在所有内容之上 - APILoadingEffectView() - } - } - - // 启动画面内容 - private var splashContent: some View { - ZStack { - // 背景图片 - 全屏显示 - LoginBackgroundView() - - VStack(spacing: 32) { - Spacer() - .frame(height: 200) // 与 storyboard 中的约束对应 - - // Logo 图片 - 100x100 - Image("logo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 100, height: 100) - - // 应用标题 - 白色,40pt字体 - Text(LocalizedString("splash.title", comment: "E-Parti")) - .font(.system(size: 40, weight: .regular)) - .foregroundColor(.white) - - Spacer() - } - } - } -} - -//#Preview { -// Splash() -//} - -#Preview { - Splash() -} \ No newline at end of file diff --git a/yana/MVVM/SplashV2.swift b/yana/MVVM/SplashV2.swift new file mode 100644 index 0000000..12fb5ad --- /dev/null +++ b/yana/MVVM/SplashV2.swift @@ -0,0 +1,63 @@ +import SwiftUI + +struct SplashV2: View { + @State private var showLogin = false + @State private var showMain = false + @State private var hasCheckedAuth = false + private let splashTransitionAnimation: Animation = .easeInOut(duration: 0.25) + + var body: some View { + Group { + if showMain { + MainPage(onLogout: { + showMain = false + showLogin = true + }) + } else if showLogin { + NavigationStack { + LoginPage(onLoginSuccess: { + showMain = true + }) + } + } else { + ZStack { + LoginBackgroundView() + VStack(spacing: 32) { + Spacer().frame(height: 200) + Image("logo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + Text(LocalizedString("splash.title", comment: "E-Parti")) + .font(.system(size: 40, weight: .regular)) + .foregroundColor(.white) + Spacer() + } + } + .onAppear { + guard !hasCheckedAuth else { return } + hasCheckedAuth = true + Task { @MainActor in + debugInfoSync("🚀 SplashV2 启动,开始检查登录缓存") + let status = await UserInfoManager.checkAuthenticationStatus() + if status.canAutoLogin { + debugInfoSync("✅ 检测到可自动登录,尝试预取用户信息") + _ = await UserInfoManager.autoFetchUserInfoOnAppLaunch(apiService: LiveAPIService()) + withAnimation(splashTransitionAnimation) { + showMain = true + } + } else { + debugInfoSync("🔑 未登录或缓存无效,进入登录页") + withAnimation(splashTransitionAnimation) { + showLogin = true + } + } + } + } + } + } + .ignoresSafeArea() + } +} + + diff --git a/yana/MVVM/ViewModel/MainViewModel.swift b/yana/MVVM/ViewModel/MainViewModel.swift index 3e5b76d..450a7f8 100644 --- a/yana/MVVM/ViewModel/MainViewModel.swift +++ b/yana/MVVM/ViewModel/MainViewModel.swift @@ -56,9 +56,9 @@ class MainViewModel: ObservableObject { func onTopRightButtonTapped() { switch selectedTab { case .feed: - onAddButtonTapped?() + navigationPath.append(AppRoute.publish) case .me: - navigationPath.append("setting") + navigationPath.append(AppRoute.setting) } } } diff --git a/yana/Utils/Navigation/AppRoute.swift b/yana/Utils/Navigation/AppRoute.swift new file mode 100644 index 0000000..a37104b --- /dev/null +++ b/yana/Utils/Navigation/AppRoute.swift @@ -0,0 +1,11 @@ +import Foundation + +/// 应用统一路由定义 +enum AppRoute: Hashable { + case login + case main + case setting + case publish +} + + diff --git a/yana/Utils/Network/APIService+Combine.swift b/yana/Utils/Network/APIService+Combine.swift new file mode 100644 index 0000000..0c9bbec --- /dev/null +++ b/yana/Utils/Network/APIService+Combine.swift @@ -0,0 +1,38 @@ +import Foundation +@preconcurrency import Combine + +// 以 @unchecked Sendable 包装 Future 的 promise,安全地跨并发域传递 +private final class PromiseBox: @unchecked Sendable { + private let fulfill: (Result) -> Void + init(_ fulfill: @escaping (Result) -> Void) { self.fulfill = fulfill } + func complete(_ result: Result) { fulfill(result) } +} + +extension APIServiceProtocol { + /// 将 async/await 的请求桥接为 Combine Publisher + /// - Parameter request: 符合 APIRequestProtocol 的请求对象 + /// - Returns: AnyPublisher + func requestPublisher(_ request: T) -> AnyPublisher { + Deferred { + Future { promise in + let box = PromiseBox(promise) + Task(priority: .userInitiated) { + let result: Result + do { + let value = try await self.request(request) + result = .success(value) + } catch let apiError as APIError { + result = .failure(apiError) + } catch { + result = .failure(.unknown(error.localizedDescription)) + } + box.complete(result) + } + } + } + .subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .eraseToAnyPublisher() + } +} + + diff --git a/yana/Utils/Network/DeviceContext.swift b/yana/Utils/Network/DeviceContext.swift new file mode 100644 index 0000000..c7dfafb --- /dev/null +++ b/yana/Utils/Network/DeviceContext.swift @@ -0,0 +1,54 @@ +import Foundation +import UIKit + +@MainActor +struct DeviceContext: Sendable { + let languageCode: String + let osName: String + let osVersion: String + let deviceModel: String + let deviceId: String + let appName: String + let appVersion: String + let channel: String + let screenScale: String + + static let shared: DeviceContext = { + // 仅在主线程读取一次 UIKit/Bundle 信息 + let language = Locale.current.language.languageCode?.identifier ?? "en" + let osName = "iOS" + let osVersion = UIDevice.current.systemVersion + let deviceModel = UIDevice.current.model + let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString + let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "eparty" + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" + #if DEBUG + let channel = "molistar_enterprise" + #else + let channel = "appstore" + #endif + let scale = String(format: "%.2f", Double(UIScreen.main.scale)) + + return DeviceContext( + languageCode: language, + osName: osName, + osVersion: osVersion, + deviceModel: deviceModel, + deviceId: deviceId, + appName: appName, + appVersion: appVersion, + channel: channel, + screenScale: scale + ) + }() +} + +enum UserAgentProvider { + @MainActor + static func userAgent() -> String { + let ctx = DeviceContext.shared + return "\(ctx.appName)/\(ctx.appVersion) (\(ctx.deviceModel); \(ctx.osName) \(ctx.osVersion); Scale/\(ctx.screenScale))" + } +} + + diff --git a/yana/Utils/Network/NetworkMonitor.swift b/yana/Utils/Network/NetworkMonitor.swift new file mode 100644 index 0000000..a80bbd8 --- /dev/null +++ b/yana/Utils/Network/NetworkMonitor.swift @@ -0,0 +1,33 @@ +import Foundation +import Network + +/// 监听系统网络状态并缓存最近结果 +/// WiFi=2, 蜂窝=1, 未知/无网络=0(与现有代码语义对齐) +final class NetworkMonitor: @unchecked Sendable { + static let shared = NetworkMonitor() + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "com.yana.network.monitor") + private var _currentType: Int = 2 // 默认与历史保持一致 + var currentType: Int { _currentType } + private init() { + monitor.pathUpdateHandler = { [weak self] path in + guard let self = self else { return } + let type: Int + if path.status == .satisfied { + if path.usesInterfaceType(.wifi) { type = 2 } + else if path.usesInterfaceType(.cellular) { type = 1 } + else { type = 0 } + } else { + type = 0 + } + // 更新缓存(主线程或任一队列均可,这里选择主线程与 UI 一致) + DispatchQueue.main.async { [weak self] in + self?._currentType = type + } + } + monitor.start(queue: queue) + } +} + + + diff --git a/yana/Utils/Security/KeychainManager.swift b/yana/Utils/Security/KeychainManager.swift index 019584f..a6bf811 100644 --- a/yana/Utils/Security/KeychainManager.swift +++ b/yana/Utils/Security/KeychainManager.swift @@ -12,11 +12,10 @@ import Security /// - 完善的错误处理 /// - 线程安全操作 /// - 可配置的访问控制级别 -@MainActor -final class KeychainManager { +final class KeychainManager: @unchecked Sendable { // MARK: - 单例 - @MainActor static let shared = KeychainManager() + static let shared = KeychainManager() private init() {} // MARK: - 配置常量 diff --git a/yana/Utils/Security/SigningKeyProvider.swift b/yana/Utils/Security/SigningKeyProvider.swift new file mode 100644 index 0000000..f7b8cbf --- /dev/null +++ b/yana/Utils/Security/SigningKeyProvider.swift @@ -0,0 +1,32 @@ +import Foundation + +/// 提供 API 签名密钥的统一入口 +/// - 优先从 Info.plist 读取键 `API_SIGNING_KEY` +/// - Debug 环境下若缺失,回退到历史密钥以避免开发阶段中断,同时输出告警 +/// - Release 环境下若缺失,输出错误并返回空字符串(应在发布前配置) +enum SigningKeyProvider { + /// Info.plist 中的键名 + private static let plistKey = "API_SIGNING_KEY" + + /// 获取签名密钥 + static func signingKey() -> String { + if let key = Bundle.main.object(forInfoDictionaryKey: plistKey) as? String, + !key.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return key + } + + #if DEBUG + // 仅在 Debug 回退,避免打断本地调试;请尽快在 Info.plist 配置 API_SIGNING_KEY + let legacy = "rpbs6us1m8r2j9g6u06ff2bo18orwaya" + debugWarnSync("⚠️ API_SIGNING_KEY 未配置,Debug 使用历史回退密钥(请尽快配置 Info.plist)") + return legacy + #else + debugErrorSync("❌ 缺少 API_SIGNING_KEY,请在 Info.plist 中配置") + assertionFailure("Missing API_SIGNING_KEY in Info.plist") + return "" + #endif + } +} + + + diff --git a/yana/Views/SplashView.swift b/yana/Views/SplashView.swift deleted file mode 100644 index e3b80cd..0000000 --- a/yana/Views/SplashView.swift +++ /dev/null @@ -1,91 +0,0 @@ -import SwiftUI -import ComposableArchitecture - -struct SplashView: View { - let store: StoreOf - - var body: some View { - ZStack { - Group { - // 根据导航目标显示不同页面 - if let navigationDestination = store.navigationDestination { - switch navigationDestination { - case .login: - // 显示登录页面 - LoginView( - store: Store( - initialState: LoginFeature.State() - ) { - LoginFeature() - }, - onLoginSuccess: { - // 登录成功后导航到主页面 - store.send(.navigateToMain) - } - ) - case .main: - // 显示主应用页面 - MainView( - store: Store( - initialState: MainFeature.State() - ) { - MainFeature() - }, - onLogout: { - store.send(.navigateToLogin) - } - ) - } - } else { - // 显示启动画面 - splashContent - } - } - .onAppear { - store.send(.onAppear) - } - - // API Loading 效果视图 - 显示在所有内容之上 - APILoadingEffectView() - } - } - - // 启动画面内容 - private var splashContent: some View { - ZStack { - // 背景图片 - 全屏显示 - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .ignoresSafeArea(.all) - - VStack(spacing: 32) { - Spacer() - .frame(height: 200) // 与 storyboard 中的约束对应 - - // Logo 图片 - 100x100 - Image("logo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 100, height: 100) - - // 应用标题 - 白色,40pt字体 - Text(LocalizedString("splash.title", comment: "E-Parti")) - .font(.system(size: 40, weight: .regular)) - .foregroundColor(.white) - - Spacer() - } - } - } -} - -//#Preview { -// SplashView( -// store: Store( -// initialState: SplashFeature.State() -// ) { -// SplashFeature() -// } -// ) -//} diff --git a/yana/yanaApp.swift b/yana/yanaApp.swift index b1fa62d..512c6e6 100644 --- a/yana/yanaApp.swift +++ b/yana/yanaApp.swift @@ -6,7 +6,6 @@ // import SwiftUI -import ComposableArchitecture @main struct yanaApp: App { @@ -24,7 +23,7 @@ struct yanaApp: App { var body: some Scene { WindowGroup { - Splash() + SplashV2() } } }