From 128bf36c888354f3246e8beee143a67389110e43 Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Thu, 17 Jul 2025 18:47:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E5=92=8C=E9=A1=B9=E7=9B=AE=E9=85=8D=E7=BD=AE=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在Package.swift中注释掉旧的swift-composable-architecture依赖,并添加swift-case-paths依赖。 - 在Podfile中将iOS平台版本更新至16.0,并移除QCloudCOSXML/Transfer依赖,改为使用QCloudCOSXML。 - 更新Podfile.lock以反映依赖变更,确保项目依赖的准确性。 - 新增架构分析需求文档,明确项目架构评估和改进建议。 - 在多个文件中实现async/await语法,提升异步操作的可读性和性能。 - 更新日志输出方法,确保在调试模式下提供一致的调试信息。 - 优化多个视图组件,提升用户体验和代码可维护性。 --- .../architecture-analysis/requirements.md | 51 +++ Package.swift | 5 +- Podfile | 4 +- Podfile.lock | 18 +- yana.xcodeproj/project.pbxproj | 59 ++- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../xcshareddata/swiftpm/Package.resolved | 10 +- .../xcdebugger/Breakpoints_v2.xcbkptlist | 172 +++++++++ yana/APIs/APIEndpoints.swift | 20 +- yana/APIs/APILogger.swift | 8 +- yana/APIs/APIModels.swift | 345 +++++++++--------- yana/APIs/APIService.swift | 104 +++--- yana/APIs/LoginModels.swift | 64 ++-- yana/AppDelegate.swift | 4 +- yana/Configs/AppConfig.swift | 8 +- yana/Configs/ClientConfig.swift | 5 +- yana/ContentView.swift | 290 +++++++-------- yana/Features/CreateFeedFeature.swift | 112 ++---- yana/Features/EMailLoginFeature.swift | 23 +- yana/Features/FeedFeature.swift | 43 ++- yana/Features/HomeFeature.swift | 25 +- yana/Features/IDLoginFeature.swift | 92 ++--- yana/Features/LoginFeature.swift | 43 +-- yana/Features/RecoverPasswordFeature.swift | 40 +- yana/Features/SettingFeature.swift | 29 +- yana/Features/SplashFeature.swift | 16 +- yana/Managers/LogManager.swift | 79 +++- .../APILoading/APILoadingEffectView.swift | 24 +- yana/Utils/APILoading/APILoadingManager.swift | 108 ++---- yana/Utils/Extensions/String+HashTest.swift | 36 +- yana/Utils/FontManager.swift | 6 +- yana/Utils/LocalizationManager.swift | 59 ++- yana/Utils/Security/DESEncryptOCTest.swift | 24 +- .../Utils/Security/DataMigrationManager.swift | 49 +-- yana/Utils/Security/KeychainManager.swift | 19 +- yana/Views/Components/UserAgreementView.swift | 56 +-- yana/Views/CreateFeedView.swift | 159 +------- yana/Views/EMailLoginView.swift | 10 +- yana/Views/FeedView.swift | 136 ++++--- yana/Views/IDLoginView.swift | 14 +- yana/Views/LanguageSettingsView.swift | 9 +- yana/Views/LoginView.swift | 11 +- yana/Views/MeView.swift | 13 +- yana/Views/RecoverPasswordView.swift | 35 +- yana/yanaApp.swift | 2 +- yanaAPITests/yanaAPITests.swift | 8 +- 46 files changed, 1250 insertions(+), 1203 deletions(-) create mode 100644 .kiro/specs/architecture-analysis/requirements.md diff --git a/.kiro/specs/architecture-analysis/requirements.md b/.kiro/specs/architecture-analysis/requirements.md new file mode 100644 index 0000000..8cd53d3 --- /dev/null +++ b/.kiro/specs/architecture-analysis/requirements.md @@ -0,0 +1,51 @@ +# Requirements Document + +## Introduction + +This document outlines the requirements for analyzing the architecture of the Yana iOS application and providing recommendations for improvements. The analysis will focus on evaluating the current architecture, identifying strengths and weaknesses, and suggesting enhancements to improve code quality, maintainability, and scalability. + +## Requirements + +### Requirement 1 + +**User Story:** As a developer, I want to understand the current architecture of the Yana iOS application, so that I can identify areas for improvement. + +#### Acceptance Criteria + +1. WHEN analyzing the project structure THEN the system SHALL identify the main architectural patterns used +2. WHEN reviewing the codebase THEN the system SHALL document the key components and their relationships +3. WHEN examining the dependencies THEN the system SHALL list all major frameworks and libraries used +4. WHEN evaluating the project organization THEN the system SHALL assess the folder structure and file organization + +### Requirement 2 + +**User Story:** As a developer, I want to identify strengths and weaknesses in the current architecture, so that I can leverage strengths and address weaknesses. + +#### Acceptance Criteria + +1. WHEN reviewing the architecture THEN the system SHALL highlight positive architectural decisions +2. WHEN analyzing the code structure THEN the system SHALL identify potential architectural issues +3. WHEN examining the codebase THEN the system SHALL evaluate code consistency and adherence to best practices +4. WHEN assessing the architecture THEN the system SHALL identify potential bottlenecks or scalability concerns + +### Requirement 3 + +**User Story:** As a developer, I want specific recommendations for architectural improvements, so that I can enhance the application's maintainability and scalability. + +#### Acceptance Criteria + +1. WHEN providing recommendations THEN the system SHALL suggest specific architectural improvements +2. WHEN suggesting changes THEN the system SHALL explain the benefits of each recommendation +3. WHEN recommending improvements THEN the system SHALL consider the existing technology stack and constraints +4. WHEN proposing architectural changes THEN the system SHALL prioritize recommendations based on impact and effort + +### Requirement 4 + +**User Story:** As a developer, I want to understand how to implement the recommended architectural improvements, so that I can effectively enhance the application. + +#### Acceptance Criteria + +1. WHEN recommending architectural changes THEN the system SHALL provide implementation guidance +2. WHEN suggesting improvements THEN the system SHALL include code examples where appropriate +3. WHEN proposing architectural changes THEN the system SHALL outline a phased approach for implementation +4. WHEN recommending improvements THEN the system SHALL consider backward compatibility and migration strategies \ No newline at end of file diff --git a/Package.swift b/Package.swift index ea74478..29dde17 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,8 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.8.0"), +// .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.8.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths.git", branch: "main") ], targets: [ .target( @@ -29,4 +30,4 @@ let package = Package( dependencies: ["yana"] ), ] -) \ No newline at end of file +) diff --git a/Podfile b/Podfile index 8b4e890..0a799a4 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,5 @@ # Uncomment the next line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '16.0' target 'yana' do # Comment the next line if you don't want to use dynamic frameworks @@ -19,7 +19,7 @@ target 'yana' do pod 'Alamofire' # 腾讯云 COS 精简版 SDK - pod 'QCloudCOSXML/Transfer' + pod 'QCloudCOSXML' end post_install do |installer| diff --git a/Podfile.lock b/Podfile.lock index fb31f4d..42a7110 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,24 +1,32 @@ PODS: - Alamofire (5.10.2) - - QCloudCore/WithoutMTA (6.5.1) - - QCloudCOSXML/Transfer (6.5.1): - - QCloudCore/WithoutMTA (= 6.5.1) + - QCloudCore (6.5.1): + - QCloudCore/Default (= 6.5.1) + - QCloudCore/Default (6.5.1): + - QCloudTrack/Beacon (= 6.5.1) + - QCloudCOSXML (6.5.1): + - QCloudCOSXML/Default (= 6.5.1) + - QCloudCOSXML/Default (6.5.1): + - QCloudCore (= 6.5.1) + - QCloudTrack/Beacon (6.5.1) DEPENDENCIES: - Alamofire - - QCloudCOSXML/Transfer + - QCloudCOSXML SPEC REPOS: trunk: - Alamofire - QCloudCore - QCloudCOSXML + - QCloudTrack SPEC CHECKSUMS: Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798 QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7 + QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8 -PODFILE CHECKSUM: cdb7b0983805213c0c4148aeaed5b1e5ea5345ab +PODFILE CHECKSUM: cd339c4c75faaa076936a34cac2be597c64f138a COCOAPODS: 1.16.2 diff --git a/yana.xcodeproj/project.pbxproj b/yana.xcodeproj/project.pbxproj index 6ebf229..d0d80fa 100644 --- a/yana.xcodeproj/project.pbxproj +++ b/yana.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C91082DE85A3E00384527 /* SystemConfiguration.framework */; }; 4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C910A2DE85A4F00384527 /* CoreFoundation.framework */; }; 4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 4C4C912F2DE864F000384527 /* ComposableArchitecture */; }; + 4CE9EFEA2E28FC3B0078D046 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE9EFE92E28FC3B0078D046 /* CasePaths */; }; + 4CE9EFEC2E28FC3B0078D046 /* CasePathsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE9EFEB2E28FC3B0078D046 /* CasePathsCore */; }; DF26704F4C1F2ABA0405A0C3 /* Pods_yana.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9DBA38268C435BE06F39AE2 /* Pods_yana.framework */; }; /* End PBXBuildFile section */ @@ -65,7 +67,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4CE9EFEA2E28FC3B0078D046 /* CasePaths in Frameworks */, 4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */, + 4CE9EFEC2E28FC3B0078D046 /* CasePathsCore in Frameworks */, 4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */, 4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */, DF26704F4C1F2ABA0405A0C3 /* Pods_yana.framework in Frameworks */, @@ -144,6 +148,7 @@ 4C3E651C2DB61F7A00E5A455 /* Frameworks */, 4C3E651D2DB61F7A00E5A455 /* Resources */, 0C5E1A8360250314246001A5 /* [CP] Embed Pods Frameworks */, + 0BECA6AFA61BE910372F6299 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -186,7 +191,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1630; - LastUpgradeCheck = 1630; + LastUpgradeCheck = 1640; TargetAttributes = { 4C3E651E2DB61F7A00E5A455 = { CreatedOnToolsVersion = 16.3; @@ -209,6 +214,7 @@ minimizedProjectReferenceProxies = 1; packageReferences = ( 4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, + 4CE9EFE82E28FC3B0078D046 /* XCRemoteSwiftPackageReference "swift-case-paths" */, ); preferredProjectObjectVersion = 77; productRefGroup = 4C3E65202DB61F7A00E5A455 /* Products */; @@ -239,6 +245,27 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0BECA6AFA61BE910372F6299 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 0C5E1A8360250314246001A5 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -315,6 +342,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -379,6 +407,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -467,7 +496,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -483,7 +512,8 @@ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h"; - SWIFT_VERSION = 5.9; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -524,7 +554,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -540,7 +570,8 @@ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h"; - SWIFT_VERSION = 5.9; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; @@ -632,6 +663,14 @@ minimumVersion = 1.20.2; }; }; + 4CE9EFE82E28FC3B0078D046 /* XCRemoteSwiftPackageReference "swift-case-paths" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-case-paths"; + requirement = { + branch = main; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -640,6 +679,16 @@ package = 4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; productName = ComposableArchitecture; }; + 4CE9EFE92E28FC3B0078D046 /* CasePaths */ = { + isa = XCSwiftPackageProductDependency; + package = 4CE9EFE82E28FC3B0078D046 /* XCRemoteSwiftPackageReference "swift-case-paths" */; + productName = CasePaths; + }; + 4CE9EFEB2E28FC3B0078D046 /* CasePathsCore */ = { + isa = XCSwiftPackageProductDependency; + package = 4CE9EFE82E28FC3B0078D046 /* XCRemoteSwiftPackageReference "swift-case-paths" */; + productName = CasePathsCore; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4C3E65172DB61F7A00E5A455 /* Project object */; diff --git a/yana.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/yana.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 830c524..daecfa0 100644 --- a/yana.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/yana.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8", + "originHash" : "411c4947a4ddec377a0aac37852b26ccdf5b2dd58cb99bedfb2d4c108efd60fd", "pins" : [ { "identity" : "combine-schedulers", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25", - "version" : "1.7.0" + "branch" : "main", + "revision" : "9810c8d6c2914de251e072312f01d3bf80071852" } }, { diff --git a/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved b/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved index 830c524..1a9b7e4 100644 --- a/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8", + "originHash" : "411c4947a4ddec377a0aac37852b26ccdf5b2dd58cb99bedfb2d4c108efd60fd", "pins" : [ { "identity" : "combine-schedulers", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25", - "version" : "1.7.0" + "branch" : "main", + "revision" : "9810c8d6c2914de251e072312f01d3bf80071852" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4", - "version" : "2.3.0" + "revision" : "ae208d1a5cf33aee1d43734ea780a09ada6e2a21", + "version" : "2.3.1" } }, { diff --git a/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 0f8d13c..90819c1 100644 --- a/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -3,4 +3,176 @@ uuid = "A60FAB2A-3184-45B2-920F-A3D7A086CF95" type = "0" version = "2.0"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yana/APIs/APIEndpoints.swift b/yana/APIs/APIEndpoints.swift index 8020659..0dd47da 100644 --- a/yana/APIs/APIEndpoints.swift +++ b/yana/APIs/APIEndpoints.swift @@ -85,40 +85,36 @@ struct APIConfiguration { /// - 用户认证信息(如果已登录) /// /// 这些头部会自动添加到每个请求中,确保服务器能够正确处理请求 - static var defaultHeaders: [String: String] { + static func defaultHeaders() async -> [String: String] { var headers = [ "Content-Type": "application/json", "Accept": "application/json", "Accept-Encoding": "gzip, br", - "Accept-Language": Locale.current.languageCode ?? "en", + "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 16.4; Scale/2.00)" ] - // 检查用户认证状态并添加相关 headers - let authStatus = UserInfoManager.checkAuthenticationStatus() - + let authStatus = await UserInfoManager.checkAuthenticationStatus() if authStatus.canAutoLogin { // 添加用户认证相关 headers(仅在 AccountModel 有效时) - if let userId = UserInfoManager.getCurrentUserId() { + if let userId = await UserInfoManager.getCurrentUserId() { headers["pub_uid"] = userId #if DEBUG - debugInfo("🔐 添加认证 header: pub_uid = \(userId)") + debugInfoSync("🔐 添加认证 header: pub_uid = \(userId)") #endif } - - if let userTicket = UserInfoManager.getCurrentUserTicket() { + if let userTicket = await UserInfoManager.getCurrentUserTicket() { headers["pub_ticket"] = userTicket #if DEBUG - debugInfo("🔐 添加认证 header: pub_ticket = \(userTicket.prefix(20))...") + debugInfoSync("🔐 添加认证 header: pub_ticket = \(userTicket.prefix(20))...") #endif } } else { #if DEBUG - debugInfo("🔐 跳过认证 header 添加 - 认证状态: \(authStatus.description)") + debugInfoSync("🔐 跳过认证 header 添加 - 认证状态: \(authStatus.description)") #endif } - return headers } } diff --git a/yana/APIs/APILogger.swift b/yana/APIs/APILogger.swift index 0921e76..862b519 100644 --- a/yana/APIs/APILogger.swift +++ b/yana/APIs/APILogger.swift @@ -1,6 +1,7 @@ import Foundation // MARK: - API Logger +@MainActor class APILogger { enum LogLevel { case none @@ -21,7 +22,12 @@ class APILogger { }() // MARK: - Request Logging - static func logRequest(_ request: T, url: URL, body: Data?, finalHeaders: [String: String]? = nil) { + @MainActor static func logRequest( + _ request: T, + url: URL, + body: Data?, + finalHeaders: [String: String]? = nil + ) { #if DEBUG guard logLevel != .none else { return } #else diff --git a/yana/APIs/APIModels.swift b/yana/APIs/APIModels.swift index ecfbaf9..5967da8 100644 --- a/yana/APIs/APIModels.swift +++ b/yana/APIs/APIModels.swift @@ -111,9 +111,10 @@ struct BaseRequest: Codable { case pubSign = "pub_sign" } + @MainActor init() { // 获取系统首选语言 - let preferredLanguage = Locale.current.languageCode ?? "en" + let preferredLanguage = Locale.current.language.languageCode?.identifier ?? "en" self.acceptLanguage = preferredLanguage self.lang = preferredLanguage @@ -237,6 +238,7 @@ struct CarrierInfoManager { // MARK: - User Info Manager (for Headers) struct UserInfoManager { + @MainActor private static let keychain = KeychainManager.shared // MARK: - Storage Keys @@ -246,72 +248,66 @@ struct UserInfoManager { } // MARK: - 内存缓存 - private static var accountModelCache: AccountModel? - private static var userInfoCache: UserInfo? + // 已迁移到 UserInfoCacheActor private static let cacheQueue = DispatchQueue(label: "com.yana.userinfo.cache", attributes: .concurrent) // MARK: - User ID Management (基于 AccountModel) - static func getCurrentUserId() -> String? { - return getAccountModel()?.uid + static func getCurrentUserId() async -> String? { + return await getAccountModel()?.uid } // MARK: - Access Token Management (基于 AccountModel) - static func getAccessToken() -> String? { - return getAccountModel()?.accessToken + static func getAccessToken() async -> String? { + return await getAccountModel()?.accessToken } // MARK: - Ticket Management (优先从 AccountModel 获取) - private static var currentTicket: String? + // 已迁移到 UserInfoCacheActor - static func getCurrentUserTicket() -> String? { + static func getCurrentUserTicket() async -> String? { // 优先从 AccountModel 获取 ticket(确保一致性) - if let accountTicket = getAccountModel()?.ticket, !accountTicket.isEmpty { + if let accountTicket = await getAccountModel()?.ticket, !accountTicket.isEmpty { return accountTicket } - // 备选:从内存获取(用于兼容性) - return currentTicket + // 备选:从 actor 获取(用于兼容性) + return await cacheActor.getCurrentTicket() } - static func saveTicket(_ ticket: String) { - currentTicket = ticket - debugInfo("💾 保存 Ticket 到内存") + static func saveTicket(_ ticket: String) async { + await cacheActor.setCurrentTicket(ticket) + debugInfoSync("💾 保存 Ticket 到内存") } - static func clearTicket() { - currentTicket = nil - debugInfo("🗑️ 清除 Ticket") + static func clearTicket() async { + await cacheActor.clearCurrentTicket() + debugInfoSync("🗑️ 清除 Ticket") } // MARK: - User Info Management - static func saveUserInfo(_ userInfo: UserInfo) { - cacheQueue.async(flags: .barrier) { - do { - try keychain.store(userInfo, forKey: StorageKeys.userInfo) - userInfoCache = userInfo - debugInfo("💾 保存用户信息成功") - } catch { - debugError("❌ 保存用户信息失败: \(error)") - } + static func saveUserInfo(_ userInfo: UserInfo) async { + do { + try await keychain.store(userInfo, forKey: StorageKeys.userInfo) + await cacheActor.setUserInfo(userInfo) + debugInfoSync("💾 保存用户信息成功") + } catch { + debugErrorSync("❌ 保存用户信息失败: \(error)") } } - static func getUserInfo() -> UserInfo? { - return cacheQueue.sync { - // 先检查缓存 - if let cached = userInfoCache { - return cached - } - - // 从 Keychain 读取 - do { - let userInfo = try keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo) - userInfoCache = userInfo - return userInfo - } catch { - debugError("❌ 读取用户信息失败: \(error)") - return nil - } + static func getUserInfo() async -> UserInfo? { + // 先检查缓存 + if let cached = await cacheActor.getUserInfo() { + return cached + } + // 从 Keychain 读取 + do { + let userInfo = try await keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo) + await cacheActor.setUserInfo(userInfo) + return userInfo + } catch { + debugErrorSync("❌ 读取用户信息失败: \(error)") + return nil } } @@ -322,7 +318,7 @@ struct UserInfoManager { ticket: String, uid: Int?, userInfo: UserInfo? - ) { + ) async { // 创建新的 AccountModel let accountModel = AccountModel( uid: uid != nil ? "\(uid!)" : nil, @@ -336,38 +332,40 @@ struct UserInfoManager { ticket: ticket ) - saveAccountModel(accountModel) - saveTicket(ticket) + await saveAccountModel(accountModel) + await saveTicket(ticket) if let userInfo = userInfo { - saveUserInfo(userInfo) + await saveUserInfo(userInfo) } - debugInfo("✅ 完整认证信息保存成功") + debugInfoSync("✅ 完整认证信息保存成功") } /// 检查是否有有效的认证信息 - static func hasValidAuthentication() -> Bool { - return getAccessToken() != nil && getCurrentUserTicket() != nil + static func hasValidAuthentication() async -> Bool { + let token = await getAccessToken() + let ticket = await getCurrentUserTicket() + return token != nil && ticket != nil } /// 清除所有认证信息 - static func clearAllAuthenticationData() { - clearAccountModel() - clearUserInfo() - clearTicket() + static func clearAllAuthenticationData() async { + await clearAccountModel() + await clearUserInfo() + await clearTicket() - debugInfo("🗑️ 清除所有认证信息") + debugInfoSync("🗑️ 清除所有认证信息") } /// 尝试恢复 Ticket(用于应用重启后) static func restoreTicketIfNeeded() async -> Bool { - guard let accessToken = getAccessToken(), - getCurrentUserTicket() == nil else { + guard let _ = await getAccessToken(), + await getCurrentUserTicket() == nil else { return false } - debugInfo("🔄 尝试使用 Access Token 恢复 Ticket...") + debugInfoSync("🔄 尝试使用 Access Token 恢复 Ticket...") // 这里需要注入 APIService 依赖,暂时返回 false // 实际实现中应该调用 TicketHelper.createTicketRequest @@ -377,50 +375,48 @@ struct UserInfoManager { // MARK: - Account Model Management /// 保存 AccountModel /// - Parameter accountModel: 要保存的账户模型 - static func saveAccountModel(_ accountModel: AccountModel) { - cacheQueue.async(flags: .barrier) { - do { - try keychain.store(accountModel, forKey: StorageKeys.accountModel) - accountModelCache = accountModel - - // 同步更新 ticket 到内存 - if let ticket = accountModel.ticket { - saveTicket(ticket) - } - - debugInfo("💾 AccountModel 保存成功") - } catch { - debugError("❌ AccountModel 保存失败: \(error)") + static func saveAccountModel(_ accountModel: AccountModel) async { + do { + try await keychain.store(accountModel, forKey: StorageKeys.accountModel) + await cacheActor.setAccountModel(accountModel) + + // 同步更新 ticket 到内存 + if let ticket = accountModel.ticket { + await saveTicket(ticket) } + + debugInfoSync("💾 AccountModel 保存成功") + } catch { + debugErrorSync("❌ AccountModel 保存失败: \(error)") } } /// 获取 AccountModel /// - Returns: 存储的账户模型,如果不存在或解析失败返回 nil - static func getAccountModel() -> AccountModel? { - return cacheQueue.sync { - // 先检查缓存 - if let cached = accountModelCache { - return cached - } - - // 从 Keychain 读取 - do { - let accountModel = try keychain.retrieve(AccountModel.self, forKey: StorageKeys.accountModel) - accountModelCache = accountModel - return accountModel - } catch { - debugError("❌ 读取 AccountModel 失败: \(error)") - return nil - } + static func getAccountModel() async -> AccountModel? { + // 先检查缓存 + if let cached = await cacheActor.getAccountModel() { + return cached + } + // 从 Keychain 读取 + do { + let accountModel = try await keychain.retrieve( + AccountModel.self, + forKey: StorageKeys.accountModel + ) + await cacheActor.setAccountModel(accountModel) + return accountModel + } catch { + debugErrorSync("❌ 读取 AccountModel 失败: \(error)") + return nil } } /// 更新 AccountModel 中的 ticket /// - Parameter ticket: 新的票据 - static func updateAccountModelTicket(_ ticket: String) { - guard var accountModel = getAccountModel() else { - debugError("❌ 无法更新 ticket:AccountModel 不存在") + static func updateAccountModelTicket(_ ticket: String) async { + guard var accountModel = await getAccountModel() else { + debugErrorSync("❌ 无法更新 ticket:AccountModel 不存在") return } @@ -436,97 +432,78 @@ struct UserInfoManager { ticket: ticket ) - saveAccountModel(accountModel) - saveTicket(ticket) // 同时更新内存中的 ticket + await saveAccountModel(accountModel) + await saveTicket(ticket) // 同时更新内存中的 ticket } /// 检查是否有有效的 AccountModel /// - Returns: 是否存在有效的账户模型 - static func hasValidAccountModel() -> Bool { - guard let accountModel = getAccountModel() else { + static func hasValidAccountModel() async -> Bool { + guard let accountModel = await getAccountModel() else { return false } return accountModel.hasValidAuthentication } /// 清除 AccountModel - static func clearAccountModel() { - cacheQueue.async(flags: .barrier) { - do { - try keychain.delete(forKey: StorageKeys.accountModel) - accountModelCache = nil - debugInfo("🗑️ AccountModel 已清除") - } catch { - debugError("❌ 清除 AccountModel 失败: \(error)") - } + static func clearAccountModel() async { + do { + try await keychain.delete(forKey: StorageKeys.accountModel) + await cacheActor.clearAccountModel() + debugInfoSync("🗑️ AccountModel 已清除") + } catch { + debugErrorSync("❌ 清除 AccountModel 失败: \(error)") } } /// 清除用户信息 - static func clearUserInfo() { - cacheQueue.async(flags: .barrier) { - do { - try keychain.delete(forKey: StorageKeys.userInfo) - userInfoCache = nil - debugInfo("🗑️ UserInfo 已清除") - } catch { - debugError("❌ 清除 UserInfo 失败: \(error)") - } + static func clearUserInfo() async { + do { + try await keychain.delete(forKey: StorageKeys.userInfo) + await cacheActor.clearUserInfo() + debugInfoSync("🗑️ UserInfo 已清除") + } catch { + debugErrorSync("❌ 清除 UserInfo 失败: \(error)") } } /// 清除所有缓存(用于测试或重置) - static func clearAllCache() { - cacheQueue.async(flags: .barrier) { - accountModelCache = nil - userInfoCache = nil - debugInfo("🗑️ 清除所有内存缓存") - } + static func clearAllCache() async { + await cacheActor.clearAccountModel() + await cacheActor.clearUserInfo() + debugInfoSync("🗑️ 清除所有内存缓存") } /// 预加载缓存(提升首次访问性能) - static func preloadCache() { - cacheQueue.async { - // 预加载 AccountModel - _ = getAccountModel() - // 预加载 UserInfo - _ = getUserInfo() - debugInfo("🚀 缓存预加载完成") - } + static func preloadCache() async { + await cacheActor.setAccountModel(await getAccountModel()) + await cacheActor.setUserInfo(await getUserInfo()) + debugInfoSync("🚀 缓存预加载完成") } // MARK: - Authentication Validation /// 检查当前认证状态是否有效 /// - Returns: 认证状态结果 - static func checkAuthenticationStatus() -> AuthenticationStatus { - return cacheQueue.sync { - guard let accountModel = getAccountModel() else { - debugInfo("🔍 认证检查:未找到 AccountModel") - return .notFound - } - - // 检查 uid 是否有效 - guard let uid = accountModel.uid, !uid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - debugInfo("🔍 认证检查:uid 无效") - return .invalid - } - - // 检查 ticket 是否有效 - guard let ticket = accountModel.ticket, !ticket.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - debugInfo("🔍 认证检查:ticket 无效") - return .invalid - } - - // 可选:检查 access token 是否有效 - guard let accessToken = accountModel.accessToken, !accessToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - debugInfo("🔍 认证检查:access token 无效") - return .invalid - } - - debugInfo("🔍 认证检查:认证有效 - uid: \(uid), ticket: \(ticket.prefix(10))...") - return .valid + static func checkAuthenticationStatus() async -> AuthenticationStatus { + guard let accountModel = await getAccountModel() else { + debugInfoSync("🔍 认证检查:未找到 AccountModel") + return .notFound } + guard let uid = accountModel.uid, !uid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + debugInfoSync("🔍 认证检查:uid 无效") + return .invalid + } + guard let ticket = accountModel.ticket, !ticket.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + debugInfoSync("🔍 认证检查:ticket 无效") + return .invalid + } + guard let accessToken = accountModel.accessToken, !accessToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + debugInfoSync("🔍 认证检查:access token 无效") + return .invalid + } + debugInfoSync("🔍 认证检查:认证有效 - uid: \(uid), ticket: \(ticket.prefix(10))...") + return .valid } /// 认证状态枚举 @@ -556,19 +533,19 @@ struct UserInfoManager { /// 测试认证 header 功能(仅用于调试) /// 模拟用户登录状态并验证 header 添加逻辑 - static func testAuthenticationHeaders() { + static func testAuthenticationHeaders() async { #if DEBUG - debugInfo("\n🧪 开始测试认证 header 功能") + debugInfoSync("\n🧪 开始测试认证 header 功能") // 测试1:未登录状态 - debugInfo("📝 测试1:未登录状态") - clearAllAuthenticationData() - let headers1 = APIConfiguration.defaultHeaders + debugInfoSync("📝 测试1:未登录状态") + await clearAllAuthenticationData() + let headers1 = await APIConfiguration.defaultHeaders() let hasAuthHeaders1 = headers1.keys.contains("pub_uid") || headers1.keys.contains("pub_ticket") - debugInfo(" 认证 headers 存在: \(hasAuthHeaders1) (应该为 false)") + debugInfoSync(" 认证 headers 存在: \(hasAuthHeaders1) (应该为 false)") // 测试2:模拟登录状态 - debugInfo("📝 测试2:模拟登录状态") + debugInfoSync("📝 测试2:模拟登录状态") let testAccount = AccountModel( uid: "12345", jti: "test-jti", @@ -580,22 +557,48 @@ struct UserInfoManager { scope: "read write", ticket: "test-ticket-12345678901234567890" ) - saveAccountModel(testAccount) + await saveAccountModel(testAccount) - let headers2 = APIConfiguration.defaultHeaders + let headers2 = await APIConfiguration.defaultHeaders() let hasUid = headers2["pub_uid"] == "12345" let hasTicket = headers2["pub_ticket"] == "test-ticket-12345678901234567890" - debugInfo(" pub_uid 正确: \(hasUid) (应该为 true)") - debugInfo(" pub_ticket 正确: \(hasTicket) (应该为 true)") + debugInfoSync(" pub_uid 正确: \(hasUid) (应该为 true)") + debugInfoSync(" pub_ticket 正确: \(hasTicket) (应该为 true)") // 测试3:清理测试数据 - debugInfo("📝 测试3:清理测试数据") - clearAllAuthenticationData() - debugInfo("✅ 认证 header 测试完成\n") + debugInfoSync("📝 测试3:清理测试数据") + await clearAllAuthenticationData() + debugInfoSync("✅ 认证 header 测试完成\n") #endif } } +// MARK: - User Info Cache Actor +actor UserInfoCacheActor { + private var accountModelCache: AccountModel? + private var userInfoCache: UserInfo? + private var currentTicket: String? + + // AccountModel + func getAccountModel() -> AccountModel? { accountModelCache } + func setAccountModel(_ model: AccountModel?) { accountModelCache = model } + func clearAccountModel() { accountModelCache = nil } + + // UserInfo + func getUserInfo() -> UserInfo? { userInfoCache } + func setUserInfo(_ info: UserInfo?) { userInfoCache = info } + func clearUserInfo() { userInfoCache = nil } + + // Ticket + func getCurrentTicket() -> String? { currentTicket } + func setCurrentTicket(_ ticket: String?) { currentTicket = ticket } + func clearCurrentTicket() { currentTicket = nil } +} + +extension UserInfoManager { + static let cacheActor = UserInfoCacheActor() +} + // MARK: - API Request Protocol /// API 请求协议 @@ -604,7 +607,7 @@ struct UserInfoManager { /// 每个具体的 API 请求都应该实现这个协议。 /// /// 协议要求: -/// - Response: 关联类型,定义响应数据的类型 +/// - Response: 关联类型,定义响应数据的类型,必须 Sendable /// - endpoint: API 端点路径 /// - method: HTTP 请求方法 /// - 可选的查询参数、请求体参数、请求头等 @@ -618,8 +621,8 @@ struct UserInfoManager { /// // ... 其他属性 /// } /// ``` -protocol APIRequestProtocol { - associatedtype Response: Codable +protocol APIRequestProtocol: Sendable { + associatedtype Response: Codable & Sendable var endpoint: String { get } var method: HTTPMethod { get } diff --git a/yana/APIs/APIService.swift b/yana/APIs/APIService.swift index 3f92f6a..11e0bd2 100644 --- a/yana/APIs/APIService.swift +++ b/yana/APIs/APIService.swift @@ -14,7 +14,7 @@ import ComposableArchitecture /// let request = ConfigRequest() /// let response = try await apiService.request(request) /// ``` -protocol APIServiceProtocol { +protocol APIServiceProtocol: Sendable { /// 发起网络请求 /// - Parameter request: 符合 APIRequestProtocol 的请求对象 /// - Returns: 请求对应的响应对象 @@ -39,19 +39,22 @@ protocol APIServiceProtocol { /// - 完整的错误处理和重试机制 /// - 详细的请求/响应日志记录 /// - 防止资源超限的保护机制 -struct LiveAPIService: APIServiceProtocol { +struct LiveAPIService: APIServiceProtocol, Sendable { private let session: URLSession private let baseURL: String + // 缓存主 actor 配置,避免并发隔离问题 + private static let cachedBaseURL: String = APIConfiguration.baseURL + private static let cachedTimeout: TimeInterval = APIConfiguration.timeout /// 初始化 API 服务 - /// - Parameter baseURL: API 服务器基础 URL,默认使用配置中的地址 - init(baseURL: String = APIConfiguration.baseURL) { + /// - Parameter baseURL: API 服务器基础 URL,默认使用静态缓存 + init(baseURL: String = LiveAPIService.cachedBaseURL) { self.baseURL = baseURL // 配置 URLSession 以防止资源超限问题 let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = APIConfiguration.timeout - config.timeoutIntervalForResource = APIConfiguration.timeout * 2 + config.timeoutIntervalForRequest = LiveAPIService.cachedTimeout + config.timeoutIntervalForResource = LiveAPIService.cachedTimeout * 2 config.waitsForConnectivity = true config.allowsCellularAccess = true @@ -78,14 +81,14 @@ struct LiveAPIService: APIServiceProtocol { let startTime = Date() // 开始 Loading 管理 - let loadingId = APILoadingManager.shared.startLoading( + let loadingId = await APILoadingManager.shared.startLoading( shouldShowLoading: request.shouldShowLoading, shouldShowError: request.shouldShowError ) // 构建 URL - guard let url = buildURL(for: request) else { - APILoadingManager.shared.setError(loadingId, errorMessage: APIError.invalidURL.localizedDescription) + guard let url = await buildURL(for: request) else { + await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.invalidURL.localizedDescription) throw APIError.invalidURL } @@ -95,8 +98,8 @@ struct LiveAPIService: APIServiceProtocol { urlRequest.timeoutInterval = request.timeout urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") - // 设置请求头 - var headers = APIConfiguration.defaultHeaders + // 设置请求头(必须 await 获取) + var headers = await APIConfiguration.defaultHeaders() if let customHeaders = request.headers { headers.merge(customHeaders) { _, new in new } } @@ -119,7 +122,7 @@ struct LiveAPIService: APIServiceProtocol { // 如果需要包含基础参数,则先合并所有参数,再统一生成签名 if request.includeBaseParameters { // 第一步:创建基础参数实例(不包含签名) - var baseParams = BaseRequest() + var baseParams = await BaseRequest() // 第二步:基于所有参数(bodyParams + 基础参数)统一生成签名 baseParams.generateSignature(with: bodyParams) @@ -127,8 +130,7 @@ struct LiveAPIService: APIServiceProtocol { // 第三步:将包含正确签名的基础参数合并到最终请求体 let baseDict = try baseParams.toDictionary() finalBody.merge(baseDict) { _, new in new } // 基础参数(包括签名)优先 - - debugInfo("🔐 签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)") + debugInfoSync("🔐 签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)") } requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: []) @@ -136,17 +138,18 @@ struct LiveAPIService: APIServiceProtocol { // urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") if let httpBody = urlRequest.httpBody, let bodyString = String(data: httpBody, encoding: .utf8) { - debugInfo("HTTP Body: \(bodyString)") + debugInfoSync("HTTP Body: \(bodyString)") } } catch { let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)") - APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription) + await APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription) throw encodingError } } // 记录请求日志,传递完整的 headers 信息 - APILogger.logRequest(request, url: url, body: requestBody, finalHeaders: headers) + await APILogger + .logRequest(request, url: url, body: requestBody, finalHeaders: headers) do { // 发起请求 @@ -156,34 +159,36 @@ struct LiveAPIService: APIServiceProtocol { // 检查响应 guard let httpResponse = response as? HTTPURLResponse else { let networkError = APIError.networkError("无效的响应类型") - APILoadingManager.shared.setError(loadingId, errorMessage: networkError.localizedDescription) + await APILoadingManager.shared.setError(loadingId, errorMessage: networkError.localizedDescription) throw networkError } // 检查数据大小 if data.count > APIConfiguration.maxDataSize { - APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration) - APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription) + await APILogger + .logError(APIError.resourceTooLarge, url: url, duration: duration) + await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription) throw APIError.resourceTooLarge } // 记录响应日志 - APILogger.logResponse(data: data, response: httpResponse, duration: duration) + await APILogger + .logResponse(data: data, response: httpResponse, duration: duration) // 性能警告 - APILogger.logPerformanceWarning(duration: duration) + await APILogger.logPerformanceWarning(duration: duration) // 检查 HTTP 状态码 guard 200...299 ~= httpResponse.statusCode else { let errorMessage = extractErrorMessage(from: data) let httpError = APIError.httpError(statusCode: httpResponse.statusCode, message: errorMessage) - APILoadingManager.shared.setError(loadingId, errorMessage: httpError.localizedDescription) + await APILoadingManager.shared.setError(loadingId, errorMessage: httpError.localizedDescription) throw httpError } // 检查数据是否为空 guard !data.isEmpty else { - APILoadingManager.shared.setError(loadingId, errorMessage: APIError.noData.localizedDescription) + await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.noData.localizedDescription) throw APIError.noData } @@ -191,28 +196,28 @@ struct LiveAPIService: APIServiceProtocol { do { let decoder = JSONDecoder() let decodedResponse = try decoder.decode(T.Response.self, from: data) - APILogger.logDecodedResponse(decodedResponse, type: T.Response.self) + await APILogger.logDecodedResponse(decodedResponse, type: T.Response.self) // 请求成功,完成 loading - APILoadingManager.shared.finishLoading(loadingId) + await APILoadingManager.shared.finishLoading(loadingId) return decodedResponse } catch { let decodingError = APIError.decodingError("响应解析失败: \(error.localizedDescription)") - APILoadingManager.shared.setError(loadingId, errorMessage: decodingError.localizedDescription) + await APILoadingManager.shared.setError(loadingId, errorMessage: decodingError.localizedDescription) throw decodingError } } catch let error as APIError { let duration = Date().timeIntervalSince(startTime) - APILogger.logError(error, url: url, duration: duration) - APILoadingManager.shared.setError(loadingId, errorMessage: error.localizedDescription) + await 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) - APILogger.logError(apiError, url: url, duration: duration) - APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription) + await APILogger.logError(apiError, url: url, duration: duration) + await APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription) throw apiError } } @@ -228,7 +233,7 @@ struct LiveAPIService: APIServiceProtocol { /// /// - Parameter request: API 请求对象 /// - Returns: 构建完成的 URL,如果构建失败则返回 nil - private func buildURL(for request: T) -> URL? { + @MainActor private func buildURL(for request: T) -> URL? { guard var urlComponents = URLComponents(string: baseURL + request.endpoint) else { return nil } @@ -252,9 +257,9 @@ struct LiveAPIService: APIServiceProtocol { queryItems.append(URLQueryItem(name: key, value: "\(value)")) } - debugInfo("🔐 GET请求签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)") + debugInfoSync("🔐 GET请求签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)") } catch { - debugWarn("警告:无法添加基础参数到查询字符串") + debugWarnSync("警告:无法添加基础参数到查询字符串") } } @@ -322,46 +327,31 @@ struct LiveAPIService: APIServiceProtocol { // MARK: - Mock API Service (for testing) -/// 模拟 API 服务,用于测试和开发 -/// -/// 该类提供了一个可配置的模拟 API 服务,可以: -/// - 设置预定义的响应数据 -/// - 模拟网络延迟 -/// - 用于单元测试和 UI 预览 -/// -/// 使用示例: -/// ```swift -/// var mockService = MockAPIService() -/// mockService.setMockResponse(for: "/client/config", response: mockConfigResponse) -/// let response = try await mockService.request(ConfigRequest()) -/// ``` -struct MockAPIService: APIServiceProtocol { +/// 并发安全的 Mock API Service +actor MockAPIServiceActor: APIServiceProtocol, Sendable { private var mockResponses: [String: Any] = [:] - - mutating func setMockResponse(for endpoint: String, response: T) { + + func setMockResponse(for endpoint: String, response: T) { mockResponses[endpoint] = response } - + func request(_ request: T) async throws -> T.Response { - // 模拟网络延迟 try await Task.sleep(nanoseconds: 500_000_000) // 0.5 秒 - if let mockResponse = mockResponses[request.endpoint] as? T.Response { return mockResponse } - throw APIError.noData } } // MARK: - TCA Dependency Integration private enum APIServiceKey: DependencyKey { - static let liveValue: APIServiceProtocol = LiveAPIService() - static let testValue: APIServiceProtocol = MockAPIService() + static let liveValue: any APIServiceProtocol & Sendable = LiveAPIService() + static let testValue: any APIServiceProtocol & Sendable = MockAPIServiceActor() } extension DependencyValues { - var apiService: APIServiceProtocol { + var apiService: (any APIServiceProtocol & Sendable) { get { self[APIServiceKey.self] } set { self[APIServiceKey.self] = newValue } } diff --git a/yana/APIs/LoginModels.swift b/yana/APIs/LoginModels.swift index af2b7dd..b170f0d 100644 --- a/yana/APIs/LoginModels.swift +++ b/yana/APIs/LoginModels.swift @@ -78,7 +78,7 @@ struct IDLoginAPIRequest: APIRequestProtocol { let method: HTTPMethod = .POST let includeBaseParameters = true let queryParameters: [String: String]? - let bodyParameters: [String: Any]? = nil + var bodyParameters: [String: Any]? { nil } let timeout: TimeInterval = 30.0 /// 初始化ID登录请求 @@ -98,14 +98,6 @@ struct IDLoginAPIRequest: APIRequestProtocol { "client_id": clientId, "grant_type": grantType ]; -// self.bodyParameters = [ -// "phone": phone, -// "password": password, -// "client_secret": clientSecret, -// "version": version, -// "client_id": clientId, -// "grant_type": grantType -// ]; } } @@ -186,21 +178,21 @@ struct LoginHelper { /// - userID: 原始用户ID /// - password: 原始密码 /// - Returns: 配置好的API请求,如果加密失败返回nil - static func createIDLoginRequest(userID: String, password: String) -> IDLoginAPIRequest? { + static func createIDLoginRequest(userID: String, password: String) async -> IDLoginAPIRequest? { // 使用DES加密ID和密码 let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26" guard let encryptedID = DESEncrypt.encryptUseDES(userID, key: encryptionKey), let encryptedPassword = DESEncrypt.encryptUseDES(password, key: encryptionKey) else { - debugError("❌ DES加密失败") + debugErrorSync("❌ DES加密失败") return nil } - debugInfo("🔐 DES加密成功") - debugInfo(" 原始ID: \(userID)") - debugInfo(" 加密后ID: \(encryptedID)") - debugInfo(" 原始密码: \(password)") - debugInfo(" 加密后密码: \(encryptedPassword)") + await debugInfoSync("🔐 DES加密成功") + await debugInfoSync(" 原始ID: \(userID)") + await debugInfoSync(" 加密后ID: \(encryptedID)") + await debugInfoSync(" 原始密码: \(password)") + await debugInfoSync(" 加密后密码: \(encryptedPassword)") return IDLoginAPIRequest( phone: userID, @@ -219,7 +211,7 @@ struct TicketAPIRequest: APIRequestProtocol { let method: HTTPMethod = .POST let includeBaseParameters = true let queryParameters: [String: String]? - let bodyParameters: [String: Any]? = nil + var bodyParameters: [String: Any]? { nil } let timeout: TimeInterval = 30.0 let customHeaders: [String: String]? @@ -292,13 +284,13 @@ struct TicketHelper { /// - accessToken: OAuth 访问令牌 /// - uid: 用户唯一标识 static func debugTicketRequest(accessToken: String, uid: Int?) { - debugInfo("🎫 Ticket 请求调试信息") - debugInfo(" AccessToken: \(accessToken)") - debugInfo(" UID: \(uid?.description ?? "nil")") - debugInfo(" Endpoint: /oauth/ticket") - debugInfo(" Method: POST") - debugInfo(" Headers: pub_uid = \(uid?.description ?? "nil")") - debugInfo(" Parameters: access_token=\(accessToken), issue_type=multi") + debugInfoSync("🎫 Ticket 请求调试信息") + debugInfoSync(" AccessToken: \(accessToken)") + debugInfoSync(" UID: \(uid?.description ?? "nil")") + debugInfoSync(" Endpoint: /oauth/ticket") + debugInfoSync(" Method: POST") + debugInfoSync(" Headers: pub_uid = \(uid?.description ?? "nil")") + debugInfoSync(" Parameters: access_token=\(accessToken), issue_type=multi") } } @@ -315,7 +307,7 @@ struct EmailGetCodeRequest: APIRequestProtocol { let method: HTTPMethod = .POST let includeBaseParameters = true let queryParameters: [String: String]? - let bodyParameters: [String: Any]? = nil + var bodyParameters: [String: Any]? { nil } let timeout: TimeInterval = 30.0 /// 初始化邮箱验证码获取请求 @@ -356,7 +348,7 @@ struct EmailLoginRequest: APIRequestProtocol { let method: HTTPMethod = .POST let includeBaseParameters = true let queryParameters: [String: String]? - let bodyParameters: [String: Any]? = nil + var bodyParameters: [String: Any]? { nil } let timeout: TimeInterval = 30.0 /// 初始化邮箱验证码登录请求 @@ -389,13 +381,13 @@ extension LoginHelper { let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26" guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else { - debugError("❌ 邮箱DES加密失败") + debugErrorSync("❌ 邮箱DES加密失败") return nil } - debugInfo("🔐 邮箱DES加密成功") - debugInfo(" 原始邮箱: \(email)") - debugInfo(" 加密邮箱: \(encryptedEmail)") + debugInfoSync("🔐 邮箱DES加密成功") + debugInfoSync(" 原始邮箱: \(email)") + debugInfoSync(" 加密邮箱: \(encryptedEmail)") return EmailGetCodeRequest(emailAddress: email, type: 1) } @@ -405,18 +397,18 @@ extension LoginHelper { /// - email: 原始邮箱地址 /// - code: 验证码 /// - Returns: 配置好的API请求,如果加密失败返回nil - static func createEmailLoginRequest(email: String, code: String) -> EmailLoginRequest? { + static func createEmailLoginRequest(email: String, code: String) async -> EmailLoginRequest? { let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26" guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else { - debugError("❌ 邮箱DES加密失败") + await debugErrorSync("❌ 邮箱DES加密失败") return nil } - debugInfo("🔐 邮箱验证码登录DES加密成功") - debugInfo(" 原始邮箱: \(email)") - debugInfo(" 加密邮箱: \(encryptedEmail)") - debugInfo(" 验证码: \(code)") + await debugInfoSync("🔐 邮箱验证码登录DES加密成功") + await debugInfoSync(" 原始邮箱: \(email)") + await debugInfoSync(" 加密邮箱: \(encryptedEmail)") + await debugInfoSync(" 验证码: \(code)") return EmailLoginRequest(email: encryptedEmail, code: code) } diff --git a/yana/AppDelegate.swift b/yana/AppDelegate.swift index 557aac1..071d4f5 100644 --- a/yana/AppDelegate.swift +++ b/yana/AppDelegate.swift @@ -2,13 +2,13 @@ import UIKit //import NIMSDK class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) async -> Bool { // 执行数据迁移(从 UserDefaults 到 Keychain) DataMigrationManager.performStartupMigration() // 预加载用户信息缓存 - UserInfoManager.preloadCache() + await UserInfoManager.preloadCache() // 开启网络监控 // NetworkManager.shared.networkStatusChanged = { status in diff --git a/yana/Configs/AppConfig.swift b/yana/Configs/AppConfig.swift index fe28aa5..4e60e80 100644 --- a/yana/Configs/AppConfig.swift +++ b/yana/Configs/AppConfig.swift @@ -4,7 +4,7 @@ enum Environment { } struct AppConfig { - static var current: Environment = { + static let current: Environment = { #if DEBUG return .development #else @@ -43,9 +43,9 @@ struct AppConfig { } // 运行时切换环境(用于测试) - static func switchEnvironment(to env: Environment) { - current = env - } +// static func switchEnvironment(to env: Environment) { +// current = env +// } // 网络调试配置 static var enableNetworkDebug: Bool { diff --git a/yana/Configs/ClientConfig.swift b/yana/Configs/ClientConfig.swift index e79f814..9668164 100644 --- a/yana/Configs/ClientConfig.swift +++ b/yana/Configs/ClientConfig.swift @@ -2,17 +2,18 @@ import Foundation import UIKit // 用于设备信息 @_exported import Alamofire // 全局导入 +@MainActor final class ClientConfig { static let shared = ClientConfig() private init() {} func initializeClient() { - debugInfo("✅ 开始初始化客户端 - URL: \(AppConfig.baseURL)/client/init") + debugInfoSync("✅ 开始初始化客户端 - URL: \(AppConfig.baseURL)/client/init") callClientInitAPI() // 调用新方法 } func callClientInitAPI() { - debugInfo("🆕 使用GET方法调用初始化接口") + debugInfoSync("🆕 使用GET方法调用初始化接口") // let queryParams = [ // "debug": "1", diff --git a/yana/ContentView.swift b/yana/ContentView.swift index 87db961..5f1c9d4 100644 --- a/yana/ContentView.swift +++ b/yana/ContentView.swift @@ -28,6 +28,144 @@ enum UILogLevel: String, CaseIterable { case detailed = "详细日志" } +struct LoginTabView: View { + let store: StoreOf + let initStore: StoreOf + @Binding var selectedLogLevel: APILogger.LogLevel + + var body: some View { + VStack { + // 日志级别选择器 + VStack(alignment: .leading, spacing: 8) { + Text("日志级别:") + .font(.headline) + .foregroundColor(.primary) + Picker("日志级别", selection: $selectedLogLevel) { + Text("无日志").tag(APILogger.LogLevel.none) + Text("基础日志").tag(APILogger.LogLevel.basic) + Text("详细日志").tag(APILogger.LogLevel.detailed) + } + .pickerStyle(SegmentedPickerStyle()) + } + .padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(10) + Spacer() + VStack(spacing: 20) { + Text("eparty") + .font(.largeTitle) + .fontWeight(.bold) + VStack(spacing: 15) { + TextField("账号", text: Binding( + get: { store.account }, + set: { store.send(.updateAccount($0)) } + )) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocorrectionDisabled(true) + SecureField("密码", text: Binding( + get: { store.password }, + set: { store.send(.updatePassword($0)) } + )) + .textFieldStyle(RoundedBorderTextFieldStyle()) + } + .padding(.horizontal) + if let error = store.error { + Text(error) + .foregroundColor(.red) + .font(.caption) + .multilineTextAlignment(.center) + .padding(.horizontal) + } + VStack(spacing: 10) { + Button(action: { + store.send(.login) + }) { + HStack { + if store.isLoading { + ProgressView() + .scaleEffect(0.8) + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + } + Text(store.isLoading ? "登录中..." : "登录") + } + .frame(maxWidth: .infinity) + .padding() + .background(store.isLoading ? Color.gray : Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + .disabled(store.isLoading || store.account.isEmpty || store.password.isEmpty) + Button(action: { + initStore.send(.initialize) + }) { + HStack { + if initStore.isLoading { + ProgressView() + .scaleEffect(0.8) + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + } + Text(initStore.isLoading ? "测试中..." : "测试初始化") + } + .frame(maxWidth: .infinity) + .padding() + .background(initStore.isLoading ? Color.gray : Color.green) + .foregroundColor(.white) + .cornerRadius(10) + } + .disabled(initStore.isLoading) + if let response = initStore.response { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("API 测试结果:") + .font(.headline) + .foregroundColor(.primary) + } + ScrollView { + VStack(alignment: .leading, spacing: 4) { + Text("状态: \(response.status)") + if let message = response.message { + Text("消息: \(message)") + } + if let data = response.data { + Text("版本: \(data.version ?? "未知")") + Text("时间戳: \(data.timestamp ?? 0)") + if let config = data.config { + Text("配置:") + ForEach(Array(config.keys), id: \.self) { key in + Text(" \(key): \(config[key] ?? "")") + } + } + } + } + .font(.system(.caption, design: .monospaced)) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(8) + .background(Color.gray.opacity(0.1)) + .cornerRadius(8) + } + .frame(maxHeight: 200) + } + .padding() + .background(Color.gray.opacity(0.05)) + .cornerRadius(10) + } + if let error = initStore.error { + Text(error) + .foregroundColor(.red) + .font(.caption) + .multilineTextAlignment(.center) + .padding() + } + } + .padding(.horizontal) + } + Spacer() + } + .padding() + } +} + struct ContentView: View { let store: StoreOf let initStore: StoreOf @@ -38,155 +176,11 @@ struct ContentView: View { var body: some View { WithPerceptionTracking { TabView(selection: $selectedTab) { - // 原有登录界面 - VStack { - // 日志级别选择器 - VStack(alignment: .leading, spacing: 8) { - Text("日志级别:") - .font(.headline) - .foregroundColor(.primary) - - Picker("日志级别", selection: $selectedLogLevel) { - Text("无日志").tag(APILogger.LogLevel.none) - Text("基础日志").tag(APILogger.LogLevel.basic) - Text("详细日志").tag(APILogger.LogLevel.detailed) - } - .pickerStyle(SegmentedPickerStyle()) + LoginTabView(store: store, initStore: initStore, selectedLogLevel: $selectedLogLevel) + .tabItem { + Label("登录", systemImage: "person.circle") } - .padding() - .background(Color.gray.opacity(0.1)) - .cornerRadius(10) - - Spacer() - - VStack(spacing: 20) { - Text("eparty") - .font(.largeTitle) - .fontWeight(.bold) - - VStack(spacing: 15) { - TextField("账号", text: Binding( - get: { store.account }, - set: { store.send(.updateAccount($0)) } - )) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .autocorrectionDisabled(true) - - SecureField("密码", text: Binding( - get: { store.password }, - set: { store.send(.updatePassword($0)) } - )) - .textFieldStyle(RoundedBorderTextFieldStyle()) - } - .padding(.horizontal) - - if let error = store.error { - Text(error) - .foregroundColor(.red) - .font(.caption) - .multilineTextAlignment(.center) - .padding(.horizontal) - } - - VStack(spacing: 10) { - Button(action: { - store.send(.login) - }) { - HStack { - if store.isLoading { - ProgressView() - .scaleEffect(0.8) - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - } - Text(store.isLoading ? "登录中..." : "登录") - } - .frame(maxWidth: .infinity) - .padding() - .background(store.isLoading ? Color.gray : Color.blue) - .foregroundColor(.white) - .cornerRadius(10) - } - .disabled(store.isLoading || store.account.isEmpty || store.password.isEmpty) - - Button(action: { - initStore.send(.initialize) - }) { - HStack { - if initStore.isLoading { - ProgressView() - .scaleEffect(0.8) - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - } - Text(initStore.isLoading ? "测试中..." : "测试初始化") - } - .frame(maxWidth: .infinity) - .padding() - .background(initStore.isLoading ? Color.gray : Color.green) - .foregroundColor(.white) - .cornerRadius(10) - } - .disabled(initStore.isLoading) - - // API 测试结果显示区域 - if let response = initStore.response { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("API 测试结果:") - .font(.headline) - .foregroundColor(.primary) - } - - ScrollView { - VStack(alignment: .leading, spacing: 4) { - Text("状态: \(response.status)") - if let message = response.message { - Text("消息: \(message)") - } - if let data = response.data { - Text("版本: \(data.version ?? "未知")") - Text("时间戳: \(data.timestamp ?? 0)") - if let config = data.config { - Text("配置:") - ForEach(Array(config.keys), id: \.self) { key in - Text(" \(key): \(config[key] ?? "")") - } - } - } - } - .font(.system(.caption, design: .monospaced)) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(8) - .background(Color.gray.opacity(0.1)) - .cornerRadius(8) - } - .frame(maxHeight: 200) - } - .padding() - .background(Color.gray.opacity(0.05)) - .cornerRadius(10) - } - - if let error = initStore.error { - Text(error) - .foregroundColor(.red) - .font(.caption) - .multilineTextAlignment(.center) - .padding() - } - } - .padding(.horizontal) - } - - Spacer() - } - .padding() - .tabItem { - Label("登录", systemImage: "person.circle") - } - .tag(0) - - // 新的 API 配置测试界面 + .tag(0) ConfigView(store: configStore) .tabItem { Label("API 测试", systemImage: "network") diff --git a/yana/Features/CreateFeedFeature.swift b/yana/Features/CreateFeedFeature.swift index bb67308..3197ede 100644 --- a/yana/Features/CreateFeedFeature.swift +++ b/yana/Features/CreateFeedFeature.swift @@ -1,11 +1,7 @@ import Foundation import ComposableArchitecture import SwiftUI - -// 条件导入 PhotosUI (iOS 16.0+) -#if canImport(PhotosUI) import PhotosUI -#endif @Reducer struct CreateFeedFeature { @@ -13,52 +9,33 @@ struct CreateFeedFeature { struct State: Equatable { var content: String = "" var processedImages: [UIImage] = [] - var isLoading: Bool = false var errorMessage: String? = nil var characterCount: Int = 0 - - // iOS 16+ PhotosPicker 支持 - #if canImport(PhotosUI) && swift(>=5.7) var selectedImages: [PhotosPickerItem] = [] - #endif - - // iOS 15 UIImagePickerController 支持 - var showingImagePicker: Bool = false - - var canAddMoreImages: Bool { processedImages.count < 9 } - var canPublish: Bool { !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading } + var isLoading: Bool = false } - + enum Action { case contentChanged(String) case publishButtonTapped case publishResponse(Result) case clearError case dismissView - - // iOS 16+ PhotosPicker Actions - #if canImport(PhotosUI) && swift(>=5.7) case photosPickerItemsChanged([PhotosPickerItem]) case processPhotosPickerItems([PhotosPickerItem]) - #endif - - // iOS 15 UIImagePickerController Actions - case showImagePicker - case hideImagePicker - case imageSelected(UIImage) - case removeImage(Int) + case updateProcessedImages([UIImage]) } - + @Dependency(\.apiService) var apiService @Dependency(\.dismiss) var dismiss - + var body: some ReducerOf { Reduce { state, action in switch action { @@ -66,72 +43,47 @@ struct CreateFeedFeature { state.content = newContent state.characterCount = newContent.count return .none - - #if canImport(PhotosUI) && swift(>=5.7) case .photosPickerItemsChanged(let items): state.selectedImages = items return .run { send in await send(.processPhotosPickerItems(items)) } - case .processPhotosPickerItems(let items): - return .run { [currentImages = state.processedImages] send in + let currentImages = state.processedImages + return .run { send in var newImages = currentImages - for item in items { - if let data = try? await item.loadTransferable(type: Data.self), - let image = UIImage(data: data) { - if newImages.count < 9 { - newImages.append(image) - } + guard let data = try? await item.loadTransferable(type: Data.self), + let image = UIImage(data: data) else { continue } + if newImages.count < 9 { + newImages.append(image) } } - await MainActor.run { - state.processedImages = newImages + send(.updateProcessedImages(newImages)) } } - #endif - - case .showImagePicker: - state.showingImagePicker = true + case .updateProcessedImages(let images): + state.processedImages = images return .none - - case .hideImagePicker: - state.showingImagePicker = false - return .none - - case .imageSelected(let image): - if state.processedImages.count < 9 { - state.processedImages.append(image) - } - state.showingImagePicker = false - return .none - case .removeImage(let index): guard index < state.processedImages.count else { return .none } state.processedImages.remove(at: index) - #if canImport(PhotosUI) && swift(>=5.7) if index < state.selectedImages.count { state.selectedImages.remove(at: index) } - #endif return .none - case .publishButtonTapped: guard state.canPublish else { state.errorMessage = "请输入内容" return .none } - state.isLoading = true state.errorMessage = nil - let request = PublishDynamicRequest( content: state.content.trimmingCharacters(in: .whitespacesAndNewlines), images: state.processedImages ) - return .run { send in do { let response = try await apiService.request(request) @@ -140,27 +92,21 @@ struct CreateFeedFeature { await send(.publishResponse(.failure(error))) } } - case .publishResponse(.success(let response)): state.isLoading = false - if response.code == 200 { - // 发布成功,关闭页面 return .send(.dismissView) } else { state.errorMessage = response.message.isEmpty ? "发布失败" : response.message return .none } - case .publishResponse(.failure(let error)): state.isLoading = false state.errorMessage = error.localizedDescription return .none - case .clearError: state.errorMessage = nil return .none - case .dismissView: return .run { _ in await dismiss() @@ -170,47 +116,57 @@ struct CreateFeedFeature { } } +extension CreateFeedFeature.Action: Equatable { + static func == (lhs: CreateFeedFeature.Action, rhs: CreateFeedFeature.Action) -> Bool { + switch (lhs, rhs) { + case let (.contentChanged(a), .contentChanged(b)): + return a == b + case (.publishButtonTapped, .publishButtonTapped): + return true + case (.clearError, .clearError): + return true + case (.dismissView, .dismissView): + return true + case let (.removeImage(a), .removeImage(b)): + return a == b + default: + return false + } + } +} + // MARK: - 发布动态相关模型 -/// 发布动态请求 struct PublishDynamicRequest: APIRequestProtocol { typealias Response = PublishDynamicResponse - - let endpoint: String = "/dynamic/square/publish" // 假设的发布端点 + let endpoint: String = "/dynamic/square/publish" let method: HTTPMethod = .POST let includeBaseParameters: Bool = true let queryParameters: [String: String]? = nil let timeout: TimeInterval = 30.0 - let content: String let images: [UIImage] let type: Int // 0: 纯文字, 2: 图片 - init(content: String, images: [UIImage] = []) { self.content = content self.images = images self.type = images.isEmpty ? 0 : 2 } - var bodyParameters: [String: Any]? { var params: [String: Any] = [ "content": content, "type": type ] - - // 如果有图片,需要转换为base64或上传到服务器 if !images.isEmpty { let imageData = images.compactMap { image in image.jpegData(compressionQuality: 0.8)?.base64EncodedString() } params["images"] = imageData } - return params } } -/// 发布动态响应 struct PublishDynamicResponse: Codable { let code: Int let message: String diff --git a/yana/Features/EMailLoginFeature.swift b/yana/Features/EMailLoginFeature.swift index 344c0b6..24fabc4 100644 --- a/yana/Features/EMailLoginFeature.swift +++ b/yana/Features/EMailLoginFeature.swift @@ -48,12 +48,12 @@ struct EMailLoginFeature { case .getVerificationCodeTapped: guard !state.email.isEmpty else { - state.errorMessage = "email_login.email_required".localized + state.errorMessage = NSLocalizedString("email_login.email_required", comment: "") return .none } guard ValidationHelper.isValidEmail(state.email) else { - state.errorMessage = "email_login.invalid_email".localized + state.errorMessage = NSLocalizedString("email_login.invalid_email", comment: "") return .none } @@ -98,12 +98,12 @@ struct EMailLoginFeature { case .loginButtonTapped(let email, let verificationCode): guard !email.isEmpty && !verificationCode.isEmpty else { - state.errorMessage = "email_login.fields_required".localized + state.errorMessage = NSLocalizedString("email_login.fields_required", comment: "") return .none } guard ValidationHelper.isValidEmail(email) else { - state.errorMessage = "email_login.invalid_email".localized + state.errorMessage = NSLocalizedString("email_login.invalid_email", comment: "") return .none } @@ -112,7 +112,7 @@ struct EMailLoginFeature { return .run { send in do { - guard let request = LoginHelper.createEmailLoginRequest(email: email, code: verificationCode) else { + guard let request = await LoginHelper.createEmailLoginRequest(email: email, code: verificationCode) else { await send(.loginResponse(.failure(APIError.encryptionFailed))) return } @@ -149,14 +149,11 @@ struct EMailLoginFeature { case .loginResponse(.success(let accountModel)): state.isLoading = false - - // 保存AccountModel到本地存储 - UserInfoManager.saveAccountModel(accountModel) - - // 发送成功通知,触发导航到主界面 - NotificationCenter.default.post(name: .ticketSuccess, object: nil) - - return .none + // Effect 保存AccountModel并发送通知 + return .run { _ in + await UserInfoManager.saveAccountModel(accountModel) + NotificationCenter.default.post(name: .ticketSuccess, object: nil) + } case .loginResponse(.failure(let error)): state.isLoading = false diff --git a/yana/Features/FeedFeature.swift b/yana/Features/FeedFeature.swift index 847c7c8..3e87a9f 100644 --- a/yana/Features/FeedFeature.swift +++ b/yana/Features/FeedFeature.swift @@ -16,9 +16,10 @@ struct FeedFeature { // CreateFeedView 相关状态 var isShowingCreateFeed = false + var createFeedState: CreateFeedFeature.State? = nil } - enum Action: Equatable { + enum Action { case onAppear case loadLatestMoments case loadMoreMoments @@ -30,6 +31,7 @@ struct FeedFeature { case showCreateFeed case dismissCreateFeed case createFeedCompleted + indirect case createFeed(CreateFeedFeature.Action) } @Dependency(\.apiService) var apiService @@ -38,6 +40,10 @@ struct FeedFeature { Reduce { state, action in switch action { case .onAppear: +#if DEBUG + return .none + #endif + // 只在首次出现时触发加载 guard !state.isInitialized else { return .none } state.isInitialized = true @@ -83,49 +89,49 @@ struct FeedFeature { state.isLoading = false // 添加调试日志 - debugInfo("📱 FeedFeature: API 响应成功") - debugInfo("📱 FeedFeature: response.code = \(response.code)") - debugInfo("📱 FeedFeature: response.message = \(response.message)") - debugInfo("📱 FeedFeature: response.data = \(response.data != nil ? "有数据" : "无数据")") + debugInfoSync("📱 FeedFeature: API 响应成功") + debugInfoSync("📱 FeedFeature: response.code = \(response.code)") + debugInfoSync("📱 FeedFeature: response.message = \(response.message)") + debugInfoSync("📱 FeedFeature: response.data = \(response.data != nil ? "有数据" : "无数据")") // 检查响应状态 guard response.code == 200, let data = response.data else { let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message state.error = errorMsg - debugError("❌ FeedFeature: API 响应失败 - code: \(response.code), message: \(errorMsg)") + debugErrorSync("❌ FeedFeature: API 响应失败 - code: \(response.code), message: \(errorMsg)") return .none } // 添加数据调试日志 - debugInfo("📱 FeedFeature: data.dynamicList.count = \(data.dynamicList.count)") - debugInfo("📱 FeedFeature: data.nextDynamicId = \(data.nextDynamicId)") + debugInfoSync("📱 FeedFeature: data.dynamicList.count = \(data.dynamicList.count)") + debugInfoSync("📱 FeedFeature: data.nextDynamicId = \(data.nextDynamicId)") // 判断是刷新还是加载更多 let isRefresh = state.nextDynamicId == 0 - debugInfo("📱 FeedFeature: isRefresh = \(isRefresh)") + debugInfoSync("📱 FeedFeature: isRefresh = \(isRefresh)") if isRefresh { // 刷新:替换所有数据 state.moments = data.dynamicList - debugInfo(" FeedFeature: 刷新数据,moments.count = \(state.moments.count)") + debugInfoSync(" FeedFeature: 刷新数据,moments.count = \(state.moments.count)") } else { // 加载更多:追加到现有数据 let oldCount = state.moments.count state.moments.append(contentsOf: data.dynamicList) - debugInfo(" FeedFeature: 加载更多,moments.count: \(oldCount) -> \(state.moments.count)") + debugInfoSync(" FeedFeature: 加载更多,moments.count: \(oldCount) -> \(state.moments.count)") } // 更新分页状态 state.nextDynamicId = data.nextDynamicId state.hasMoreData = !data.dynamicList.isEmpty - debugInfo("📱 FeedFeature: 更新完成 - nextDynamicId: \(state.nextDynamicId), hasMoreData: \(state.hasMoreData)") + debugInfoSync("📱 FeedFeature: 更新完成 - nextDynamicId: \(state.nextDynamicId), hasMoreData: \(state.hasMoreData)") return .none case let .momentsResponse(.failure(error)): state.isLoading = false state.error = error.localizedDescription - debugError("❌ FeedFeature: API 请求失败 - \(error.localizedDescription)") + debugErrorSync("❌ FeedFeature: API 请求失败 - \(error.localizedDescription)") return .none case .clearError: @@ -142,17 +148,28 @@ struct FeedFeature { case .showCreateFeed: state.isShowingCreateFeed = true + // 初始化 createFeedState + state.createFeedState = CreateFeedFeature.State() return .none case .dismissCreateFeed: state.isShowingCreateFeed = false + state.createFeedState = nil return .none case .createFeedCompleted: state.isShowingCreateFeed = false + state.createFeedState = nil // 发布完成后刷新动态列表 return .send(.loadLatestMoments) + case .createFeed: + // 子模块 Action 由作用域 reducer 处理 + return .none } } + // 子模块作用域 reducer + ifLet(\State.createFeedState, action: /Action.createFeed) { + CreateFeedFeature() + } } } diff --git a/yana/Features/HomeFeature.swift b/yana/Features/HomeFeature.swift index e57f4aa..a0c2bd4 100644 --- a/yana/Features/HomeFeature.swift +++ b/yana/Features/HomeFeature.swift @@ -18,7 +18,7 @@ struct HomeFeature { var feedState = FeedFeature.State() } - enum Action: Equatable { + enum Action { case onAppear case loadUserInfo case userInfoLoaded(UserInfo?) @@ -56,8 +56,10 @@ struct HomeFeature { case .loadUserInfo: // 从本地存储加载用户信息 - let userInfo = UserInfoManager.getUserInfo() - return .send(.userInfoLoaded(userInfo)) + return .run { send in + let userInfo = await UserInfoManager.getUserInfo() + await send(.userInfoLoaded(userInfo)) + } case let .userInfoLoaded(userInfo): state.userInfo = userInfo @@ -65,8 +67,10 @@ struct HomeFeature { case .loadAccountModel: // 从本地存储加载账户信息 - let accountModel = UserInfoManager.getAccountModel() - return .send(.accountModelLoaded(accountModel)) + return .run { send in + let accountModel = await UserInfoManager.getAccountModel() + await send(.accountModelLoaded(accountModel)) + } case let .accountModelLoaded(accountModel): state.accountModel = accountModel @@ -76,12 +80,11 @@ struct HomeFeature { return .send(.logout) case .logout: - // 清除所有认证数据 - UserInfoManager.clearAllAuthenticationData() - - // 发送通知返回登录页面 - NotificationCenter.default.post(name: .homeLogout, object: nil) - return .none + // 清除所有认证数据并发送通知 + return .run { _ in + await UserInfoManager.clearAllAuthenticationData() + NotificationCenter.default.post(name: .homeLogout, object: nil) + } case .settingDismissed: state.isSettingPresented = false diff --git a/yana/Features/IDLoginFeature.swift b/yana/Features/IDLoginFeature.swift index 2af51db..5689087 100644 --- a/yana/Features/IDLoginFeature.swift +++ b/yana/Features/IDLoginFeature.swift @@ -56,7 +56,6 @@ struct IDLoginFeature { case .togglePasswordVisibility: state.isPasswordVisible.toggle() return .none - case let .loginButtonTapped(userID, password): state.userID = userID state.password = password @@ -64,17 +63,13 @@ struct IDLoginFeature { state.errorMessage = nil state.ticketError = nil state.loginStep = .authenticating - - // 实现真实的ID登录API调用 + // 真实登录 API 调用 Effect return .run { send in do { - // 使用LoginHelper创建加密的登录请求 - guard let loginRequest = LoginHelper.createIDLoginRequest(userID: userID, password: password) else { + guard let loginRequest = await LoginHelper.createIDLoginRequest(userID: userID, password: password) else { await send(.loginResponse(.failure(APIError.decodingError("加密失败")))) return } - - // 发起登录请求 let response = try await apiService.request(loginRequest) await send(.loginResponse(.success(response))) } catch { @@ -85,35 +80,21 @@ struct IDLoginFeature { } } } - case .forgotPasswordTapped: - // TODO: 处理忘记密码 return .none - case .backButtonTapped: - // 由父级处理返回逻辑 return .none - case let .loginResponse(.success(response)): state.isLoading = false if response.isSuccess { - // OAuth 认证成功,清除错误信息 state.errorMessage = nil - - // 从响应数据创建 AccountModel if let loginData = response.data, let accountModel = AccountModel.from(loginData: loginData) { state.accountModel = accountModel - - // 保存用户信息(如果有) + // 触发 Effect 保存 userInfo(如有) if let userInfo = loginData.userInfo { - UserInfoManager.saveUserInfo(userInfo) + return .run { _ in await UserInfoManager.saveUserInfo(userInfo) } } - - debugInfo("✅ ID 登录 OAuth 认证成功") - debugInfo("🔑 Access Token: \(accountModel.accessToken ?? "nil")") - debugInfo("🆔 用户 UID: \(accountModel.uid ?? "nil")") - // 自动获取 ticket return .send(.requestTicket(accessToken: accountModel.accessToken!)) } else { @@ -125,90 +106,77 @@ struct IDLoginFeature { state.loginStep = .failed } return .none - case let .loginResponse(.failure(error)): state.isLoading = false state.errorMessage = error.localizedDescription state.loginStep = .failed return .none - case let .requestTicket(accessToken): state.isTicketLoading = true state.ticketError = nil state.loginStep = .gettingTicket - - return .run { [accountModel = state.accountModel] send in + // 先拷贝所需字段,避免并发捕获 + let uid: Int? = { + if let am = state.accountModel, let uidStr = am.uid { return Int(uidStr) } else { return nil } + }() + return .run { send in do { - // 从 AccountModel 获取 uid,转换为 Int 类型 - let uid = accountModel?.uid != nil ? Int(accountModel!.uid!) : nil let ticketRequest = TicketHelper.createTicketRequest(accessToken: accessToken, uid: uid) let response = try await apiService.request(ticketRequest) await send(.ticketResponse(.success(response))) } catch { - debugError("❌ ID登录 Ticket 获取失败: \(error)") + debugErrorSync("❌ ID登录 Ticket 获取失败: \(error)") await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription)))) } } - case let .ticketResponse(.success(response)): state.isTicketLoading = false if response.isSuccess { state.ticketError = nil state.loginStep = .completed - - debugInfo("✅ ID 登录完整流程成功") - debugInfo("🎫 Ticket 获取成功: \(response.ticket ?? "nil")") - - // 更新 AccountModel 中的 ticket 并保存 - if let ticket = response.ticket { - if var accountModel = state.accountModel { - accountModel.ticket = ticket - state.accountModel = accountModel - - // 保存完整的 AccountModel - UserInfoManager.saveAccountModel(accountModel) - - // 发送 Ticket 获取成功通知,触发导航到主页面 - NotificationCenter.default.post(name: .ticketSuccess, object: nil) - } else { - debugError("❌ AccountModel 不存在,无法保存 ticket") - state.ticketError = "内部错误:账户信息丢失" - state.loginStep = .failed + debugInfoSync("✅ ID 登录完整流程成功") + debugInfoSync("🎫 Ticket 获取成功: \(response.ticket ?? "nil")") + // --- 并发安全修正:彻底避免 Effect 闭包捕获 state/accountModel --- + if let ticket = response.ticket, let oldAccountModel = state.accountModel { + // 用 withTicket 生成新 struct,闭包只捕获 newAccountModel + let newAccountModel = oldAccountModel.withTicket(ticket) + state.accountModel = newAccountModel + // 只捕获 newAccountModel,绝不捕获 state + return .run { _ in + // 这里不能捕获 state/accountModel,否则 Swift 并发会报错 + await UserInfoManager.saveAccountModel(newAccountModel) } - } else { + } else if response.ticket == nil { state.ticketError = "Ticket 为空" state.loginStep = .failed + } else { + debugErrorSync("❌ AccountModel 不存在,无法保存 ticket") + state.ticketError = "内部错误:账户信息丢失" + state.loginStep = .failed } - } else { state.ticketError = response.errorMessage state.loginStep = .failed } return .none - case let .ticketResponse(.failure(error)): state.isTicketLoading = false state.ticketError = error.localizedDescription state.loginStep = .failed - debugError("❌ ID 登录 Ticket 获取失败: \(error.localizedDescription)") + debugErrorSync("❌ ID 登录 Ticket 获取失败: \(error.localizedDescription)") return .none - case .clearTicketError: state.ticketError = nil return .none - case .resetLogin: state.isLoading = false state.isTicketLoading = false state.errorMessage = nil state.ticketError = nil - state.accountModel = nil // 清除 AccountModel + state.accountModel = nil state.loginStep = .initial - - // 清除本地存储的认证信息 - UserInfoManager.clearAllAuthenticationData() - - return .none + // Effect 清除认证信息 + return .run { _ in await UserInfoManager.clearAllAuthenticationData() } } } } diff --git a/yana/Features/LoginFeature.swift b/yana/Features/LoginFeature.swift index 23d74c4..177ffaa 100644 --- a/yana/Features/LoginFeature.swift +++ b/yana/Features/LoginFeature.swift @@ -81,7 +81,7 @@ struct LoginFeature { return .run { [account = state.account, password = state.password] send in do { // 使用LoginHelper创建加密的登录请求 - guard let loginRequest = LoginHelper.createIDLoginRequest(userID: account, password: password) else { + guard let loginRequest = await LoginHelper.createIDLoginRequest(userID: account, password: password) else { await send(.loginResponse(.failure(APIError.decodingError("加密失败")))) return } @@ -108,10 +108,9 @@ struct LoginFeature { if let loginData = response.data, let accountModel = AccountModel.from(loginData: loginData) { state.accountModel = accountModel - - debugInfo("✅ OAuth 认证成功") - debugInfo("🔑 Access Token: \(accountModel.accessToken ?? "nil")") - debugInfo("🆔 用户 UID: \(accountModel.uid ?? "nil")") + debugInfoSync("✅ OAuth 认证成功") + debugInfoSync("🔑 Access Token: \(accountModel.accessToken ?? "nil")") + debugInfoSync("🆔 用户 UID: \(accountModel.uid ?? "nil")") // 自动获取 ticket return .send(.requestTicket(accessToken: accountModel.accessToken!)) @@ -144,7 +143,7 @@ struct LoginFeature { let response = try await apiService.request(ticketRequest) await send(.ticketResponse(.success(response))) } catch { - debugError("❌ Ticket 获取失败: \(error)") + debugErrorSync("❌ Ticket 获取失败: \(error)") await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription)))) } } @@ -155,22 +154,21 @@ struct LoginFeature { state.ticketError = nil state.loginStep = .completed - debugInfo("✅ 完整登录流程成功") - debugInfo("🎫 Ticket 获取成功: \(response.ticket ?? "nil")") + debugInfoSync("✅ 完整登录流程成功") + debugInfoSync("🎫 Ticket 获取成功: \(response.ticket ?? "nil")") // 更新 AccountModel 中的 ticket 并保存 if let ticket = response.ticket { - if var accountModel = state.accountModel { - accountModel.ticket = ticket - state.accountModel = accountModel - - // 保存完整的 AccountModel - UserInfoManager.saveAccountModel(accountModel) - - // 发送 Ticket 获取成功通知,触发导航到主页面 - NotificationCenter.default.post(name: .ticketSuccess, object: nil) + if let oldAccountModel = state.accountModel { + let newAccountModel = oldAccountModel.withTicket(ticket) + state.accountModel = newAccountModel + // Effect 保存完整的 AccountModel + return .run { _ in + await UserInfoManager.saveAccountModel(newAccountModel) + NotificationCenter.default.post(name: .ticketSuccess, object: nil) + } } else { - debugError("❌ AccountModel 不存在,无法保存 ticket") + debugErrorSync("❌ AccountModel 不存在,无法保存 ticket") state.ticketError = "内部错误:账户信息丢失" state.loginStep = .failed } @@ -189,7 +187,7 @@ struct LoginFeature { state.isTicketLoading = false state.ticketError = error.localizedDescription state.loginStep = .failed - debugError("❌ Ticket 获取失败: \(error.localizedDescription)") + debugErrorSync("❌ Ticket 获取失败: \(error.localizedDescription)") return .none case .clearTicketError: @@ -203,11 +201,8 @@ struct LoginFeature { state.ticketError = nil state.accountModel = nil // 清除 AccountModel state.loginStep = .initial - - // 清除本地存储的认证信息 - UserInfoManager.clearAllAuthenticationData() - - return .none + // Effect 清除本地存储的认证信息 + return .run { _ in await UserInfoManager.clearAllAuthenticationData() } case .idLogin: // IDLogin动作由子feature处理 diff --git a/yana/Features/RecoverPasswordFeature.swift b/yana/Features/RecoverPasswordFeature.swift index e2d7782..7e8c470 100644 --- a/yana/Features/RecoverPasswordFeature.swift +++ b/yana/Features/RecoverPasswordFeature.swift @@ -57,12 +57,12 @@ struct RecoverPasswordFeature { case .getVerificationCodeTapped: guard !state.email.isEmpty else { - state.errorMessage = "recover_password.email_required".localized + state.errorMessage = NSLocalizedString("recover_password.email_required", comment: "") return .none } guard ValidationHelper.isValidEmail(state.email) else { - state.errorMessage = "recover_password.invalid_email".localized + state.errorMessage = NSLocalizedString("recover_password.invalid_email", comment: "") return .none } @@ -101,23 +101,23 @@ struct RecoverPasswordFeature { if let apiError = error as? APIError { state.errorMessage = apiError.localizedDescription } else { - state.errorMessage = "recover_password.code_send_failed".localized + state.errorMessage = NSLocalizedString("recover_password.code_send_failed", comment: "") } return .none case .resetPasswordTapped: guard !state.email.isEmpty && !state.verificationCode.isEmpty && !state.newPassword.isEmpty else { - state.errorMessage = "recover_password.fields_required".localized + state.errorMessage = NSLocalizedString("recover_password.fields_required", comment: "") return .none } guard ValidationHelper.isValidEmail(state.email) else { - state.errorMessage = "recover_password.invalid_email".localized + state.errorMessage = NSLocalizedString("recover_password.invalid_email", comment: "") return .none } guard ValidationHelper.isValidPassword(state.newPassword) else { - state.errorMessage = "recover_password.invalid_password".localized + state.errorMessage = NSLocalizedString("recover_password.invalid_password", comment: "") return .none } @@ -160,7 +160,7 @@ struct RecoverPasswordFeature { if let apiError = error as? APIError { state.errorMessage = apiError.localizedDescription } else { - state.errorMessage = "recover_password.reset_failed".localized + state.errorMessage = NSLocalizedString("recover_password.reset_failed", comment: "") } return .none @@ -199,7 +199,7 @@ struct ResetPasswordResponse: Codable, Equatable { /// 错误消息(如果有) var errorMessage: String { - return message ?? "recover_password.reset_failed".localized + return message ?? NSLocalizedString("recover_password.reset_failed", comment: "") } } @@ -211,7 +211,7 @@ struct ResetPasswordRequest: APIRequestProtocol { let method: HTTPMethod = .POST let includeBaseParameters = true let queryParameters: [String: String]? - let bodyParameters: [String: Any]? = nil + var bodyParameters: [String: Any]? { nil } let timeout: TimeInterval = 30.0 /// 初始化密码重置请求 @@ -238,13 +238,13 @@ struct RecoverPasswordHelper { let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26" guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else { - debugError("❌ 邮箱DES加密失败") + debugErrorSync("❌ 邮箱DES加密失败") return nil } - debugInfo("🔐 密码恢复邮箱DES加密成功") - debugInfo(" 原始邮箱: \(email)") - debugInfo(" 加密邮箱: \(encryptedEmail)") + debugInfoSync("🔐 密码恢复邮箱DES加密成功") + debugInfoSync(" 原始邮箱: \(email)") + debugInfoSync(" 加密邮箱: \(encryptedEmail)") // 使用type=3表示密码重置验证码 return EmailGetCodeRequest(emailAddress: email, type: 3) @@ -261,16 +261,16 @@ struct RecoverPasswordHelper { guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey), let encryptedPassword = DESEncrypt.encryptUseDES(newPassword, key: encryptionKey) else { - debugError("❌ 密码重置DES加密失败") + debugErrorSync("❌ 密码重置DES加密失败") return nil } - debugInfo("🔐 密码重置DES加密成功") - debugInfo(" 原始邮箱: \(email)") - debugInfo(" 加密邮箱: \(encryptedEmail)") - debugInfo(" 验证码: \(code)") - debugInfo(" 原始新密码: \(newPassword)") - debugInfo(" 加密新密码: \(encryptedPassword)") + debugInfoSync("🔐 密码重置DES加密成功") + debugInfoSync(" 原始邮箱: \(email)") + debugInfoSync(" 加密邮箱: \(encryptedEmail)") + debugInfoSync(" 验证码: \(code)") + debugInfoSync(" 原始新密码: \(newPassword)") + debugInfoSync(" 加密新密码: \(encryptedPassword)") return ResetPasswordRequest( email: email, diff --git a/yana/Features/SettingFeature.swift b/yana/Features/SettingFeature.swift index ba70a51..546c0e4 100644 --- a/yana/Features/SettingFeature.swift +++ b/yana/Features/SettingFeature.swift @@ -32,16 +32,20 @@ struct SettingFeature { ) case .loadUserInfo: - let userInfo = UserInfoManager.getUserInfo() - return .send(.userInfoLoaded(userInfo)) + return .run { send in + let userInfo = await UserInfoManager.getUserInfo() + await send(.userInfoLoaded(userInfo)) + } case let .userInfoLoaded(userInfo): state.userInfo = userInfo return .none case .loadAccountModel: - let accountModel = UserInfoManager.getAccountModel() - return .send(.accountModelLoaded(accountModel)) + return .run { send in + let accountModel = await UserInfoManager.getAccountModel() + await send(.accountModelLoaded(accountModel)) + } case let .accountModelLoaded(accountModel): state.accountModel = accountModel @@ -52,18 +56,15 @@ struct SettingFeature { case .logout: state.isLoading = true - - // 清除所有认证数据 - UserInfoManager.clearAllAuthenticationData() - - // 发送通知返回登录页面 - NotificationCenter.default.post(name: .homeLogout, object: nil) - return .none + return .run { _ in + await UserInfoManager.clearAllAuthenticationData() + NotificationCenter.default.post(name: .homeLogout, object: nil) + } case .dismissTapped: - // 发送关闭设置页面的通知 - NotificationCenter.default.post(name: .settingsDismiss, object: nil) - return .none + return .run { _ in + NotificationCenter.default.post(name: .settingsDismiss, object: nil) + } } } } diff --git a/yana/Features/SplashFeature.swift b/yana/Features/SplashFeature.swift index 9c3c2d8..feac106 100644 --- a/yana/Features/SplashFeature.swift +++ b/yana/Features/SplashFeature.swift @@ -32,7 +32,6 @@ struct SplashFeature { try await Task.sleep(nanoseconds: 1_000_000_000) // 1秒 = 1,000,000,000 纳秒 await send(.splashFinished) } - case .splashFinished: state.isLoading = false state.shouldShowMainApp = true @@ -45,20 +44,25 @@ struct SplashFeature { // 异步检查认证状态 return .run { send in - let authStatus = UserInfoManager.checkAuthenticationStatus() + let authStatus = await UserInfoManager.checkAuthenticationStatus() await send(.authenticationChecked(authStatus)) } case let .authenticationChecked(status): +#if DEBUG + debugInfoSync("🔑 需要手动登录") + NotificationCenter.default.post(name: .autoLoginFailed, object: nil) + return .none +#endif state.isCheckingAuthentication = false state.authenticationStatus = status - + // 根据认证状态发送相应的导航通知 if status.canAutoLogin { - debugInfo("🎉 自动登录成功,进入主页") + debugInfoSync("🎉 自动登录成功,进入主页") NotificationCenter.default.post(name: .autoLoginSuccess, object: nil) } else { - debugInfo("🔑 需要手动登录") + debugInfoSync("🔑 需要手动登录") NotificationCenter.default.post(name: .autoLoginFailed, object: nil) } @@ -66,4 +70,4 @@ struct SplashFeature { } } } -} \ No newline at end of file +} diff --git a/yana/Managers/LogManager.swift b/yana/Managers/LogManager.swift index cd0ab9d..c2aaade 100644 --- a/yana/Managers/LogManager.swift +++ b/yana/Managers/LogManager.swift @@ -9,6 +9,7 @@ public enum LogLevel: Int { case error } +@MainActor public class LogManager { /// 单例 public static let shared = LogManager() @@ -45,43 +46,99 @@ public class LogManager { } // MARK: - 原有快捷方法(保持向后兼容) +@MainActor public func logVerbose(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { LogManager.shared.log(.verbose, message(), onlyRelease: onlyRelease) } +@MainActor public func logDebug(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { LogManager.shared.log(.debug, message(), onlyRelease: onlyRelease) } +@MainActor public func logInfo(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { LogManager.shared.log(.info, message(), onlyRelease: onlyRelease) } +@MainActor public func logWarn(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { LogManager.shared.log(.warn, message(), onlyRelease: onlyRelease) } +@MainActor public func logError(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { LogManager.shared.log(.error, message(), onlyRelease: onlyRelease) } // MARK: - 新的DEBUG专用快捷方法(推荐使用) -public func debugVerbose(_ message: @autoclosure () -> String) { - LogManager.shared.debugLog(.verbose, message()) +public func debugVerbose(_ message: @autoclosure () -> String) async { + let msg = message() + await MainActor.run { + LogManager.shared.debugLog(.verbose, msg) + } } -public func debugLog(_ message: @autoclosure () -> String) { - LogManager.shared.debugLog(.debug, message()) +public func debugLog(_ message: @autoclosure () -> String) async { + let msg = message() + await MainActor.run { + LogManager.shared.debugLog(.debug, msg) + } } -public func debugInfo(_ message: @autoclosure () -> String) { - LogManager.shared.debugLog(.info, message()) +public func debugInfo(_ message: @autoclosure () -> String) async { + let msg = message() + await MainActor.run { + LogManager.shared.debugLog(.info, msg) + } } -public func debugWarn(_ message: @autoclosure () -> String) { - LogManager.shared.debugLog(.warn, message()) +public func debugWarn(_ message: @autoclosure () -> String) async { + let msg = message() + await MainActor.run { + LogManager.shared.debugLog(.warn, msg) + } } -public func debugError(_ message: @autoclosure () -> String) { - LogManager.shared.debugLog(.error, message()) -} \ No newline at end of file +public func debugError(_ message: @autoclosure () -> String) async { + let msg = message() + await MainActor.run { + LogManager.shared.debugLog(.error, msg) + } +} + +// fire-and-forget 同步重载(方法名加 Sync 后缀) +public func debugVerboseSync(_ message: @autoclosure () -> String) { + let msg = message() + Task { + await debugVerbose(msg) + } +} + +public func debugLogSync(_ message: @autoclosure () -> String) { + let msg = message() + Task { + await debugLog(msg) + } +} + +public func debugInfoSync(_ message: @autoclosure () -> String) { + let msg = message() + Task { + await debugInfo(msg) + } +} + +public func debugWarnSync(_ message: @autoclosure () -> String) { + let msg = message() + Task { + await debugWarn(msg) + } +} + +public func debugErrorSync(_ message: @autoclosure () -> String) { + let msg = message() + Task { + await debugError(msg) + } +} diff --git a/yana/Utils/APILoading/APILoadingEffectView.swift b/yana/Utils/APILoading/APILoadingEffectView.swift index 017a7e4..9e26f23 100644 --- a/yana/Utils/APILoading/APILoadingEffectView.swift +++ b/yana/Utils/APILoading/APILoadingEffectView.swift @@ -18,25 +18,25 @@ struct APILoadingEffectView: View { if let firstItem = getFirstDisplayItem() { SingleLoadingView(item: firstItem) .onAppear { - debugInfo("🔍 Loading item appeared: \(firstItem.id)") + debugInfoSync("🔍 Loading item appeared: \(firstItem.id)") } .onDisappear { - debugInfo("🔍 Loading item disappeared: \(firstItem.id)") + debugInfoSync("🔍 Loading item disappeared: \(firstItem.id)") } } } .allowsHitTesting(false) // 不阻挡用户点击 .ignoresSafeArea(.all) // 覆盖整个屏幕 .onReceive(loadingManager.$loadingItems) { items in - debugInfo("🔍 Loading items updated: \(items.count) items") + debugInfoSync("🔍 Loading items updated: \(items.count) items") } } /// 安全地获取第一个需要显示的项目 private func getFirstDisplayItem() -> APILoadingItem? { guard Thread.isMainThread else { - debugWarn("⚠️ getFirstDisplayItem called from background thread") - return nil + debugWarnSync("⚠️ getFirstDisplayItem called from background thread") + return nil } return loadingManager.loadingItems.first { $0.shouldDisplay } @@ -151,7 +151,7 @@ struct APILoadingEffectView_Previews: PreviewProvider { .font(.title) Button("测试按钮") { - debugInfo("按钮被点击了!") + debugInfoSync("按钮被点击了!") } .padding() .background(Color.blue) @@ -169,12 +169,12 @@ struct APILoadingEffectView_Previews: PreviewProvider { let manager = APILoadingManager.shared // 添加 loading - let id1 = await manager.startLoading() + let id1 = manager.startLoading() // 2秒后添加错误 DispatchQueue.main.asyncAfter(deadline: .now() + 2) { Task { - await manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置") + manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置") } } } @@ -197,13 +197,13 @@ private struct PreviewStateModifier: ViewModifier { let manager = APILoadingManager.shared if showLoading { - let _ = await manager.startLoading() + let _ = manager.startLoading() } if showError { - let id = await manager.startLoading() + let id = manager.startLoading() try? await Task.sleep(nanoseconds: 1_000_000_000) // 1秒 - await manager.setError(id, errorMessage: errorMessage) + manager.setError(id, errorMessage: errorMessage) } } } @@ -224,4 +224,4 @@ extension View { )) } } -#endif \ No newline at end of file +#endif diff --git a/yana/Utils/APILoading/APILoadingManager.swift b/yana/Utils/APILoading/APILoadingManager.swift index 0e67440..0e0ac5c 100644 --- a/yana/Utils/APILoading/APILoadingManager.swift +++ b/yana/Utils/APILoading/APILoadingManager.swift @@ -11,24 +11,18 @@ import Combine /// - 管理 loading 和错误信息的显示 /// - 自动清理过期的错误信息 /// - 提供线程安全的状态更新 +@MainActor class APILoadingManager: ObservableObject { - // MARK: - Properties - /// 单例实例 static let shared = APILoadingManager() - /// 当前活动的加载项 @Published private(set) var loadingItems: [APILoadingItem] = [] - /// 错误清理任务 private var errorCleanupTasks: [UUID: DispatchWorkItem] = [:] - /// 私有初始化器,确保单例 private init() {} - // MARK: - Public Methods - /// 开始显示 loading /// - Parameters: /// - shouldShowLoading: 是否显示 loading 动画 @@ -36,127 +30,74 @@ class APILoadingManager: ObservableObject { /// - Returns: 唯一的加载 ID,用于后续更新状态 func startLoading(shouldShowLoading: Bool = true, shouldShowError: Bool = true) -> UUID { let loadingId = UUID() - let loadingItem = APILoadingItem( id: loadingId, state: .loading, shouldShowError: shouldShowError, shouldShowLoading: shouldShowLoading ) - - // 🚨 重要:必须在主线程更新 @Published 属性,否则会崩溃! - DispatchQueue.main.async { [weak self] in - self?.loadingItems.append(loadingItem) - } - + loadingItems.append(loadingItem) return loadingId } - /// 更新 loading 状态为成功 /// - Parameter id: 加载 ID func finishLoading(_ id: UUID) { - DispatchQueue.main.async { [weak self] in - self?.removeLoading(id) - } + removeLoading(id) } - /// 更新 loading 状态为错误 /// - Parameters: /// - id: 加载 ID /// - errorMessage: 错误信息 func setError(_ id: UUID, errorMessage: String) { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // 查找并更新项目 - if let index = self.loadingItems.firstIndex(where: { $0.id == id }) { - let currentItem = self.loadingItems[index] - - // 只有需要显示错误时才更新状态 - if currentItem.shouldShowError { - let errorItem = APILoadingItem( - id: id, - state: .error(message: errorMessage), - shouldShowError: true, - shouldShowLoading: currentItem.shouldShowLoading - ) - self.loadingItems[index] = errorItem - - // 设置自动清理 - self.setupErrorCleanup(for: id) - } else { - // 不显示错误,直接移除 - self.loadingItems.removeAll { $0.id == id } - } - } + guard let index = loadingItems.firstIndex(where: { $0.id == id }) else { return } + let currentItem = loadingItems[index] + if currentItem.shouldShowError { + let errorItem = APILoadingItem( + id: id, + state: .error(message: errorMessage), + shouldShowError: true, + shouldShowLoading: currentItem.shouldShowLoading + ) + loadingItems[index] = errorItem + setupErrorCleanup(for: id) + } else { + loadingItems.removeAll { $0.id == id } } } - /// 手动移除特定的加载项 /// - Parameter id: 加载 ID private func removeLoading(_ id: UUID) { cancelErrorCleanup(for: id) - // 🚨 重要:必须在主线程更新 @Published 属性 - if Thread.isMainThread { - loadingItems.removeAll { $0.id == id } - } else { - DispatchQueue.main.async { [weak self] in - self?.loadingItems.removeAll { $0.id == id } - } - } + loadingItems.removeAll { $0.id == id } } - /// 清空所有加载项(用于应急情况) func clearAll() { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // 取消所有清理任务 - self.errorCleanupTasks.values.forEach { $0.cancel() } - self.errorCleanupTasks.removeAll() - - // 清空所有项目 - self.loadingItems.removeAll() - } + errorCleanupTasks.values.forEach { $0.cancel() } + errorCleanupTasks.removeAll() + loadingItems.removeAll() } - // MARK: - Computed Properties - /// 是否有正在显示的 loading var hasActiveLoading: Bool { - if Thread.isMainThread { - return loadingItems.contains { $0.state == .loading && $0.shouldDisplay } - } else { - return false - } + loadingItems.contains { $0.state == .loading && $0.shouldDisplay } } - /// 是否有正在显示的错误 var hasActiveError: Bool { - if Thread.isMainThread { - return loadingItems.contains { $0.isError && $0.shouldDisplay } - } else { - return false - } + loadingItems.contains { $0.isError && $0.shouldDisplay } } - // MARK: - Private Methods - /// 设置错误信息自动清理 /// - Parameter id: 加载 ID private func setupErrorCleanup(for id: UUID) { let workItem = DispatchWorkItem { [weak self] in self?.removeLoading(id) } - errorCleanupTasks[id] = workItem - DispatchQueue.main.asyncAfter( deadline: .now() + APILoadingConfiguration.errorDisplayDuration, execute: workItem ) } - /// 取消错误清理任务 /// - Parameter id: 加载 ID private func cancelErrorCleanup(for id: UUID) { @@ -168,14 +109,14 @@ class APILoadingManager: ObservableObject { // MARK: - Convenience Extensions extension APILoadingManager { - /// 便捷方法:执行带 loading 的异步操作 /// - Parameters: /// - shouldShowLoading: 是否显示 loading /// - shouldShowError: 是否显示错误 /// - operation: 异步操作 /// - Returns: 操作结果 - func withLoading( + @MainActor + func withLoading( shouldShowLoading: Bool = true, shouldShowError: Bool = true, operation: @escaping () async throws -> T @@ -184,7 +125,6 @@ extension APILoadingManager { shouldShowLoading: shouldShowLoading, shouldShowError: shouldShowError ) - do { let result = try await operation() finishLoading(loadingId) diff --git a/yana/Utils/Extensions/String+HashTest.swift b/yana/Utils/Extensions/String+HashTest.swift index 68f67ce..dd4f4cb 100644 --- a/yana/Utils/Extensions/String+HashTest.swift +++ b/yana/Utils/Extensions/String+HashTest.swift @@ -6,7 +6,7 @@ struct StringHashTest { /// 测试哈希方法 static func runTests() { - debugInfo("🧪 开始测试字符串哈希方法...") + debugInfoSync("🧪 开始测试字符串哈希方法...") let testStrings = [ "hello world", @@ -16,27 +16,27 @@ struct StringHashTest { ] for testString in testStrings { - debugInfo("\n📝 测试字符串: \"\(testString)\"") + debugInfoSync("\n📝 测试字符串: \"\(testString)\"") // 测试 MD5 let md5Result = testString.md5() - debugInfo(" MD5: \(md5Result)") + debugInfoSync(" MD5: \(md5Result)") // 测试 SHA256 (iOS 13+) if #available(iOS 13.0, *) { let sha256Result = testString.sha256() - debugInfo(" SHA256: \(sha256Result)") + debugInfoSync(" SHA256: \(sha256Result)") } else { - debugInfo(" SHA256: 不支持 (需要 iOS 13+)") + debugInfoSync(" SHA256: 不支持 (需要 iOS 13+)") } } - debugInfo("\n✅ 哈希方法测试完成") + debugInfoSync("\n✅ 哈希方法测试完成") } /// 验证已知的哈希值 static func verifyKnownHashes() { - debugInfo("\n🔍 验证已知哈希值...") + debugInfoSync("\n🔍 验证已知哈希值...") // 验证 "hello world" 的 MD5 应该是 "5d41402abc4b2a76b9719d911017c592" let testString = "hello world" @@ -44,11 +44,11 @@ struct StringHashTest { let actualMD5 = testString.md5() if actualMD5 == expectedMD5 { - debugInfo("✅ MD5 验证通过: \(actualMD5)") + debugInfoSync("✅ MD5 验证通过: \(actualMD5)") } else { - debugError("❌ MD5 验证失败:") - debugError(" 期望: \(expectedMD5)") - debugError(" 实际: \(actualMD5)") + debugErrorSync("❌ MD5 验证失败:") + debugErrorSync(" 期望: \(expectedMD5)") + debugErrorSync(" 实际: \(actualMD5)") } // 验证 SHA256 @@ -57,11 +57,11 @@ struct StringHashTest { let actualSHA256 = testString.sha256() if actualSHA256 == expectedSHA256 { - debugInfo("✅ SHA256 验证通过: \(actualSHA256)") + debugInfoSync("✅ SHA256 验证通过: \(actualSHA256)") } else { - debugError("❌ SHA256 验证失败:") - debugError(" 期望: \(expectedSHA256)") - debugError(" 实际: \(actualSHA256)") + debugErrorSync("❌ SHA256 验证失败:") + debugErrorSync(" 期望: \(expectedSHA256)") + debugErrorSync(" 实际: \(actualSHA256)") } } } @@ -75,9 +75,9 @@ struct StringHashTest { StringHashTest.verifyKnownHashes() // 或者在开发时快速测试 - debugInfo("Test MD5:", "hello".md5()) + debugInfoSync("Test MD5:", "hello".md5()) if #available(iOS 13.0, *) { - debugInfo("Test SHA256:", "hello".sha256()) + debugInfoSync("Test SHA256:", "hello".sha256()) } - */ \ No newline at end of file + */ diff --git a/yana/Utils/FontManager.swift b/yana/Utils/FontManager.swift index 357032d..38d5231 100644 --- a/yana/Utils/FontManager.swift +++ b/yana/Utils/FontManager.swift @@ -67,11 +67,11 @@ struct FontManager { /// 打印所有可用字体(调试用) static func printAllAvailableFonts() { - debugInfo("=== 所有可用字体 ===") + debugInfoSync("=== 所有可用字体 ===") for font in getAllAvailableFonts() { - debugInfo(font) + debugInfoSync(font) } - debugInfo("==================") + debugInfoSync("==================") } } diff --git a/yana/Utils/LocalizationManager.swift b/yana/Utils/LocalizationManager.swift index 464b618..696fa36 100644 --- a/yana/Utils/LocalizationManager.swift +++ b/yana/Utils/LocalizationManager.swift @@ -8,6 +8,7 @@ import SwiftUI /// - 应用全局默认语言为英文,不依赖系统语言设置 /// - 用户可通过语言设置界面手动切换到其他支持的语言 /// - 用户的语言选择会保存在UserDefaults中,下次启动时保持 +@MainActor class LocalizationManager: ObservableObject { // MARK: - 单例 @@ -42,9 +43,9 @@ class LocalizationManager: ObservableObject { didSet { do { try KeychainManager.shared.storeString(currentLanguage.rawValue, forKey: "AppLanguage") - } catch { - debugError("❌ 保存语言设置失败: \(error)") - } + } catch { + debugErrorSync("❌ 保存语言设置失败: \(error)") + } // 通知视图更新 objectWillChange.send() } @@ -56,7 +57,7 @@ class LocalizationManager: ObservableObject { do { savedLanguage = try KeychainManager.shared.retrieveString(forKey: "AppLanguage") } catch { - debugError("❌ 读取语言设置失败: \(error)") + debugErrorSync("❌ 读取语言设置失败: \(error)") savedLanguage = nil } @@ -113,34 +114,26 @@ class LocalizationManager: ObservableObject { } // MARK: - SwiftUI Extensions -extension View { - /// 应用本地化字符串 - /// - Parameter key: 本地化 key - /// - Returns: 带有本地化文本的视图 - func localized(_ key: String) -> some View { - self.modifier(LocalizedTextModifier(key: key)) - } -} - -/// 本地化文本修饰器 -struct LocalizedTextModifier: ViewModifier { - let key: String - @ObservedObject private var localizationManager = LocalizationManager.shared - - func body(content: Content) -> some View { - content - } -} +// extension View { +// /// 应用本地化字符串 +// /// - Parameter key: 本地化 key +// /// - Returns: 带有本地化文本的视图 +// @MainActor +// func localized(_ key: String) -> some View { +// self.modifier(LocalizedTextModifier(key: key)) +// } +// } // MARK: - 便捷方法 -extension String { - /// 获取本地化字符串 - var localized: String { - return LocalizationManager.shared.localizedString(self) - } - - /// 获取本地化字符串(带参数) - func localized(arguments: CVarArg...) -> String { - return LocalizationManager.shared.localizedString(self, arguments: arguments) - } -} \ No newline at end of file +// extension String { +// /// 获取本地化字符串 +// @MainActor +// var localized: String { +// return LocalizationManager.shared.localizedString(self) +// } +// /// 获取本地化字符串(带参数) +// @MainActor +// func localized(arguments: CVarArg...) -> String { +// return LocalizationManager.shared.localizedString(self, arguments: arguments) +// } +// } diff --git a/yana/Utils/Security/DESEncryptOCTest.swift b/yana/Utils/Security/DESEncryptOCTest.swift index 2f9341c..4e817d0 100644 --- a/yana/Utils/Security/DESEncryptOCTest.swift +++ b/yana/Utils/Security/DESEncryptOCTest.swift @@ -5,8 +5,8 @@ struct DESEncryptOCTest { /// 测试 OC 版本的 DES 加密功能 static func testOCDESEncryption() { - debugInfo("🧪 开始测试 OC 版本的 DES 加密...") - debugInfo(String(repeating: "=", count: 50)) + debugInfoSync("🧪 开始测试 OC 版本的 DES 加密...") + debugInfoSync(String(repeating: "=", count: 50)) let key = "1ea53d260ecf11e7b56e00163e046a26" let testCases = [ @@ -19,25 +19,25 @@ struct DESEncryptOCTest { for testCase in testCases { if let encrypted = DESEncrypt.encryptUseDES(testCase, key: key) { - debugInfo("✅ 加密成功:") - debugInfo(" 原文: \"\(testCase)\"") - debugInfo(" 密文: \(encrypted)") + debugInfoSync("✅ 加密成功:") + debugInfoSync(" 原文: \"\(testCase)\"") + debugInfoSync(" 密文: \(encrypted)") // 测试解密 if let decrypted = DESEncrypt.decryptUseDES(encrypted, key: key) { let isMatch = decrypted == testCase - debugInfo(" 解密: \"\(decrypted)\" \(isMatch ? "✅" : "❌")") + debugInfoSync(" 解密: \"\(decrypted)\" \(isMatch ? "✅" : "❌")") } else { - debugError(" 解密: 失败 ❌") + debugErrorSync(" 解密: 失败 ❌") } } else { - debugError("❌ 加密失败: \"\(testCase)\"") + debugErrorSync("❌ 加密失败: \"\(testCase)\"") } - debugInfo("") + debugInfoSync("") } - debugInfo(String(repeating: "=", count: 50)) - debugInfo("🏁 OC版本DES加密测试完成") + debugInfoSync(String(repeating: "=", count: 50)) + debugInfoSync("🏁 OC版本DES加密测试完成") } } @@ -48,4 +48,4 @@ extension DESEncryptOCTest { DESEncryptOCTest.testOCDESEncryption() } } -#endif \ No newline at end of file +#endif diff --git a/yana/Utils/Security/DataMigrationManager.swift b/yana/Utils/Security/DataMigrationManager.swift index 2731633..56fa5d2 100644 --- a/yana/Utils/Security/DataMigrationManager.swift +++ b/yana/Utils/Security/DataMigrationManager.swift @@ -10,6 +10,7 @@ import Foundation /// 2. 迁移到 Keychain /// 3. 验证迁移结果 /// 4. 清理旧数据 +@MainActor final class DataMigrationManager { // MARK: - 单例 @@ -54,23 +55,23 @@ final class DataMigrationManager { /// 执行数据迁移 /// - Returns: 迁移结果 func performMigration() -> MigrationResult { - debugInfo("🔄 开始检查数据迁移...") + debugInfoSync("🔄 开始检查数据迁移...") // 检查是否已经迁移过 if isMigrationCompleted() { - debugInfo("✅ 数据已经迁移过,跳过迁移") + debugInfoSync("✅ 数据已经迁移过,跳过迁移") return .alreadyMigrated } // 检查是否有需要迁移的数据 let legacyData = collectLegacyData() if legacyData.isEmpty { - debugInfo("ℹ️ 没有发现需要迁移的数据") + debugInfoSync("ℹ️ 没有发现需要迁移的数据") markMigrationCompleted() return .noDataToMigrate } - debugInfo("📦 发现需要迁移的数据: \(legacyData.keys.joined(separator: ", "))") + debugInfoSync("📦 发现需要迁移的数据: \(legacyData.keys.joined(separator: ", "))") do { // 执行迁移 @@ -85,11 +86,11 @@ final class DataMigrationManager { // 标记迁移完成 markMigrationCompleted() - debugInfo("✅ 数据迁移完成") + debugInfoSync("✅ 数据迁移完成") return .completed } catch { - debugError("❌ 数据迁移失败: \(error)") + debugErrorSync("❌ 数据迁移失败: \(error)") return .failed(error) } } @@ -157,9 +158,9 @@ final class DataMigrationManager { do { let accountModel = try JSONDecoder().decode(AccountModel.self, from: accountModelData) try keychain.store(accountModel, forKey: "account_model") - debugInfo("✅ AccountModel 迁移成功") + debugInfoSync("✅ AccountModel 迁移成功") } catch { - debugError("❌ AccountModel 迁移失败: \(error)") + debugErrorSync("❌ AccountModel 迁移失败: \(error)") // 如果 AccountModel 迁移失败,尝试从独立字段重建 try migrateAccountModelFromIndependentFields(legacyData) } @@ -173,9 +174,9 @@ final class DataMigrationManager { do { let userInfo = try JSONDecoder().decode(UserInfo.self, from: userInfoData) try keychain.store(userInfo, forKey: "user_info") - debugInfo("✅ UserInfo 迁移成功") + debugInfoSync("✅ UserInfo 迁移成功") } catch { - debugError("❌ UserInfo 迁移失败: \(error)") + debugErrorSync("❌ UserInfo 迁移失败: \(error)") throw error } } @@ -183,7 +184,7 @@ final class DataMigrationManager { // 迁移语言设置 if let appLanguage = legacyData[LegacyStorageKeys.appLanguage] as? String { try keychain.storeString(appLanguage, forKey: "AppLanguage") - debugInfo("✅ 语言设置迁移成功") + debugInfoSync("✅ 语言设置迁移成功") } } @@ -191,7 +192,7 @@ final class DataMigrationManager { private func migrateAccountModelFromIndependentFields(_ legacyData: [String: Any]) throws { guard let userId = legacyData[LegacyStorageKeys.userId] as? String, let accessToken = legacyData[LegacyStorageKeys.accessToken] as? String else { - debugInfo("ℹ️ 没有足够的独立字段来重建 AccountModel") + debugInfoSync("ℹ️ 没有足够的独立字段来重建 AccountModel") return } @@ -208,7 +209,7 @@ final class DataMigrationManager { ) try KeychainManager.shared.store(accountModel, forKey: "account_model") - debugInfo("✅ 从独立字段重建 AccountModel 成功") + debugInfoSync("✅ 从独立字段重建 AccountModel 成功") } /// 验证迁移结果 @@ -240,7 +241,7 @@ final class DataMigrationManager { } } - debugInfo("✅ 迁移数据验证成功") + debugInfoSync("✅ 迁移数据验证成功") } /// 清理旧数据 @@ -249,11 +250,11 @@ final class DataMigrationManager { for key in keys { userDefaults.removeObject(forKey: key) - debugInfo("🗑️ 清理旧数据: \(key)") + debugInfoSync("🗑️ 清理旧数据: \(key)") } userDefaults.synchronize() - debugInfo("✅ 旧数据清理完成") + debugInfoSync("✅ 旧数据清理完成") } } @@ -287,13 +288,13 @@ extension DataMigrationManager { switch migrationResult { case .completed: - debugInfo("🎉 应用启动时数据迁移完成") + debugInfoSync("🎉 应用启动时数据迁移完成") case .alreadyMigrated: break // 静默处理 case .noDataToMigrate: break // 静默处理 case .failed(let error): - debugError("⚠️ 应用启动时数据迁移失败: \(error)") + debugErrorSync("⚠️ 应用启动时数据迁移失败: \(error)") // 这里可以添加错误上报或降级策略 } } @@ -307,9 +308,9 @@ extension DataMigrationManager { /// 调试:打印旧数据信息 func debugPrintLegacyData() { let legacyData = collectLegacyData() - debugInfo("🔍 旧版本数据:") + debugInfoSync("🔍 旧版本数据:") for (key, value) in legacyData { - debugInfo(" - \(key): \(type(of: value))") + debugInfoSync(" - \(key): \(type(of: value))") } } @@ -322,7 +323,7 @@ extension DataMigrationManager { userDefaults.set("zh-Hans", forKey: LegacyStorageKeys.appLanguage) userDefaults.synchronize() - debugInfo("🧪 已创建测试用的旧版本数据") + debugInfoSync("🧪 已创建测试用的旧版本数据") } /// 调试:清除所有迁移相关数据 @@ -331,7 +332,7 @@ extension DataMigrationManager { do { try KeychainManager.shared.clearAll() } catch { - debugError("❌ 清除 Keychain 数据失败: \(error)") + debugErrorSync("❌ 清除 Keychain 数据失败: \(error)") } // 清除 UserDefaults 数据 @@ -350,7 +351,7 @@ extension DataMigrationManager { } userDefaults.synchronize() - debugInfo("🧪 已清除所有迁移相关数据") + debugInfoSync("🧪 已清除所有迁移相关数据") } } -#endif \ No newline at end of file +#endif diff --git a/yana/Utils/Security/KeychainManager.swift b/yana/Utils/Security/KeychainManager.swift index 9eedad7..019584f 100644 --- a/yana/Utils/Security/KeychainManager.swift +++ b/yana/Utils/Security/KeychainManager.swift @@ -12,10 +12,11 @@ import Security /// - 完善的错误处理 /// - 线程安全操作 /// - 可配置的访问控制级别 +@MainActor final class KeychainManager { // MARK: - 单例 - static let shared = KeychainManager() + @MainActor static let shared = KeychainManager() private init() {} // MARK: - 配置常量 @@ -108,7 +109,7 @@ final class KeychainManager { throw KeychainError.keychainOperationFailed(status) } - debugInfo("🔐 Keychain 存储成功: \(key)") + debugInfoSync("🔐 Keychain 存储成功: \(key)") } /// 从 Keychain 检索 Codable 对象 @@ -137,7 +138,7 @@ final class KeychainManager { // 4. 解码数据 do { let object = try JSONDecoder().decode(type, from: data) - debugInfo("🔐 Keychain 读取成功: \(key)") + debugInfoSync("🔐 Keychain 读取成功: \(key)") return object } catch { throw KeychainError.decodingFailed(error) @@ -176,7 +177,7 @@ final class KeychainManager { switch status { case errSecSuccess: - debugInfo("🔐 Keychain 更新成功: \(key)") + debugInfoSync("🔐 Keychain 更新成功: \(key)") case errSecItemNotFound: // 如果项目不存在,则创建新项目 @@ -196,7 +197,7 @@ final class KeychainManager { switch status { case errSecSuccess: - debugInfo("🔐 Keychain 删除成功: \(key)") + debugInfoSync("🔐 Keychain 删除成功: \(key)") case errSecItemNotFound: // 项目不存在,视为删除成功 @@ -231,7 +232,7 @@ final class KeychainManager { switch status { case errSecSuccess, errSecItemNotFound: - debugInfo("🔐 Keychain 清除完成") + debugInfoSync("🔐 Keychain 清除完成") default: throw KeychainError.keychainOperationFailed(status) @@ -353,10 +354,10 @@ extension KeychainManager { /// 打印所有存储的键(仅用于调试) func debugPrintAllKeys() { let keys = debugListAllKeys() - debugInfo("🔐 Keychain 中存储的键:") + debugInfoSync("🔐 Keychain 中存储的键:") for key in keys { - debugInfo(" - \(key)") + debugInfoSync(" - \(key)") } } } -#endif \ No newline at end of file +#endif diff --git a/yana/Views/Components/UserAgreementView.swift b/yana/Views/Components/UserAgreementView.swift index 0b1a341..7f1c87e 100644 --- a/yana/Views/Components/UserAgreementView.swift +++ b/yana/Views/Components/UserAgreementView.swift @@ -38,20 +38,20 @@ struct UserAgreementView: View { // MARK: - Private Methods private func createAttributedText() -> AttributedString { - var attributedString = AttributedString("login.agreement_policy".localized) + var attributedString = AttributedString(NSLocalizedString("login.agreement_policy", comment: "")) // 设置默认颜色 attributedString.foregroundColor = Color(hex: 0x666666) // 找到并设置 "用户协议" 的样式和链接 - if let userServiceRange = attributedString.range(of: "login.agreement".localized) { + if let userServiceRange = attributedString.range(of: NSLocalizedString("login.agreement", comment: "")) { attributedString[userServiceRange].foregroundColor = Color(hex: 0x8A4FFF) attributedString[userServiceRange].underlineStyle = .single attributedString[userServiceRange].link = URL(string: "user-service-agreement") } // 找到并设置 "隐私政策" 的样式和链接 - if let privacyPolicyRange = attributedString.range(of: "login.policy".localized) { + if let privacyPolicyRange = attributedString.range(of: NSLocalizedString("login.policy", comment: "")) { attributedString[privacyPolicyRange].foregroundColor = Color(hex: 0x8A4FFF) attributedString[privacyPolicyRange].underlineStyle = .single attributedString[privacyPolicyRange].link = URL(string: "privacy-policy") @@ -61,28 +61,28 @@ struct UserAgreementView: View { } } -#Preview { - VStack(spacing: 20) { - UserAgreementView( - isAgreed: .constant(true), - onUserServiceTapped: { - debugInfo("User Service Agreement tapped") - }, - onPrivacyPolicyTapped: { - debugInfo("Privacy Policy tapped") - } - ) - - UserAgreementView( - isAgreed: .constant(true), - onUserServiceTapped: { - debugInfo("User Service Agreement tapped") - }, - onPrivacyPolicyTapped: { - debugInfo("Privacy Policy tapped") - } - ) - } - .padding() - .background(Color.gray.opacity(0.1)) -} +//#Preview { +// VStack(spacing: 20) { +// UserAgreementView( +// isAgreed: .constant(true), +// onUserServiceTapped: { +// debugInfoSync("User Service Agreement tapped") +// }, +// onPrivacyPolicyTapped: { +// debugInfoSync("Privacy Policy tapped") +// } +// ) +// +// UserAgreementView( +// isAgreed: .constant(true), +// onUserServiceTapped: { +// debugInfoSync("User Service Agreement tapped") +// }, +// onPrivacyPolicyTapped: { +// debugInfoSync("Privacy Policy tapped") +// } +// ) +// } +// .padding() +// .background(Color.gray.opacity(0.1)) +//} diff --git a/yana/Views/CreateFeedView.swift b/yana/Views/CreateFeedView.swift index ab7cc6a..2517b8d 100644 --- a/yana/Views/CreateFeedView.swift +++ b/yana/Views/CreateFeedView.swift @@ -1,17 +1,13 @@ import SwiftUI import ComposableArchitecture - -// 条件导入 PhotosUI (iOS 16.0+) -#if canImport(PhotosUI) import PhotosUI -#endif struct CreateFeedView: View { let store: StoreOf var body: some View { WithPerceptionTracking { - NavigationView { + NavigationStack { GeometryReader { geometry in ZStack { // 背景渐变 @@ -59,7 +55,7 @@ struct CreateFeedView: View { Text("\(store.characterCount)/500") .font(.system(size: 12)) .foregroundColor( - store.isCharacterLimitExceeded ? .red : .white.opacity(0.6) + store.characterCount > 500 ? .red : .white.opacity(0.6) ) } } @@ -69,32 +65,17 @@ struct CreateFeedView: View { // 图片选择区域 VStack(alignment: .leading, spacing: 12) { if !store.processedImages.isEmpty || store.canAddMoreImages { - if #available(iOS 16.0, *) { - #if canImport(PhotosUI) - ModernImageSelectionGrid( - images: store.processedImages, - selectedItems: store.selectedImages, - canAddMore: store.canAddMoreImages, - onItemsChanged: { items in - store.send(.photosPickerItemsChanged(items)) - }, - onRemoveImage: { index in - store.send(.removeImage(index)) - } - ) - #endif - } else { - LegacyImageSelectionGrid( - images: store.processedImages, - canAddMore: store.canAddMoreImages, - onAddImage: { - store.send(.showImagePicker) - }, - onRemoveImage: { index in - store.send(.removeImage(index)) - } - ) - } + ModernImageSelectionGrid( + images: store.processedImages, + selectedItems: store.selectedImages, + canAddMore: store.canAddMoreImages, + onItemsChanged: { items in + store.send(.photosPickerItemsChanged(items)) + }, + onRemoveImage: { index in + store.send(.removeImage(index)) + } + ) } } .padding(.horizontal, 20) @@ -112,7 +93,7 @@ struct CreateFeedView: View { } // 错误提示 - if let error = store.error { + if let error = store.errorMessage { Text(error) .font(.system(size: 14)) .foregroundColor(.red) @@ -133,7 +114,7 @@ struct CreateFeedView: View { store.send(.publishButtonTapped) }) { HStack { - if store.isPublishing { + if store.isLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(0.8) @@ -159,8 +140,8 @@ struct CreateFeedView: View { ) ) .cornerRadius(25) - .disabled(store.isPublishing || (!store.isContentValid && !store.isLoading)) - .opacity(store.isPublishing || (!store.isContentValid && !store.isLoading) ? 0.6 : 1.0) + .disabled(store.isLoading || !store.canPublish) + .opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0) } .padding(.horizontal, 20) .padding(.bottom, geometry.safeAreaInsets.bottom + 20) @@ -182,27 +163,17 @@ struct CreateFeedView: View { Button("发布") { store.send(.publishButtonTapped) } - .foregroundColor(store.isContentValid ? .white : .white.opacity(0.5)) - .disabled(!store.isContentValid || store.isPublishing) + .foregroundColor(store.canPublish ? .white : .white.opacity(0.5)) + .disabled(!store.canPublish || store.isLoading) } } } .preferredColorScheme(.dark) - .sheet(isPresented: .init( - get: { store.showingImagePicker }, - set: { _ in store.send(.hideImagePicker) } - )) { - ImagePickerView { image in - store.send(.imageSelected(image)) - } - } } } } // MARK: - iOS 16+ 图片选择网格组件 -#if canImport(PhotosUI) -@available(iOS 16.0, *) struct ModernImageSelectionGrid: View { let images: [UIImage] let selectedItems: [PhotosPickerItem] @@ -261,98 +232,6 @@ struct ModernImageSelectionGrid: View { } } } -#endif - -// MARK: - iOS 15 兼容图片选择网格组件 -struct LegacyImageSelectionGrid: View { - let images: [UIImage] - let canAddMore: Bool - let onAddImage: () -> Void - let onRemoveImage: (Int) -> Void - - private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3) - - var body: some View { - LazyVGrid(columns: columns, spacing: 8) { - // 显示已选择的图片 - ForEach(Array(images.enumerated()), id: \.offset) { index, image in - ZStack(alignment: .topTrailing) { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(height: 100) - .clipped() - .cornerRadius(8) - - // 删除按钮 - Button(action: { - onRemoveImage(index) - }) { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 20)) - .foregroundColor(.white) - .background(Color.black.opacity(0.6)) - .clipShape(Circle()) - } - .padding(4) - } - } - - // 添加图片按钮 - if canAddMore { - Button(action: onAddImage) { - RoundedRectangle(cornerRadius: 8) - .fill(Color.white.opacity(0.1)) - .frame(height: 100) - .overlay( - Image(systemName: "plus") - .font(.system(size: 40)) - .foregroundColor(.white.opacity(0.6)) - ) - } - } - } - } -} - -// MARK: - UIImagePicker 包装器 -struct ImagePickerView: UIViewControllerRepresentable { - let onImageSelected: (UIImage) -> Void - @Environment(\.presentationMode) var presentationMode - - func makeUIViewController(context: Context) -> UIImagePickerController { - let picker = UIImagePickerController() - picker.delegate = context.coordinator - picker.sourceType = .photoLibrary - picker.allowsEditing = false - return picker - } - - func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { - let parent: ImagePickerView - - init(_ parent: ImagePickerView) { - self.parent = parent - } - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - if let image = info[.originalImage] as? UIImage { - parent.onImageSelected(image) - } - parent.presentationMode.wrappedValue.dismiss() - } - - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - parent.presentationMode.wrappedValue.dismiss() - } - } -} // MARK: - 预览 #Preview { diff --git a/yana/Views/EMailLoginView.swift b/yana/Views/EMailLoginView.swift index 9d9f2c5..adf3b48 100644 --- a/yana/Views/EMailLoginView.swift +++ b/yana/Views/EMailLoginView.swift @@ -31,7 +31,7 @@ struct EMailLoginView: View { } else if codeCountdown > 0 { return "\(codeCountdown)S" } else { - return "email_login.get_code".localized + return NSLocalizedString("email_login.get_code", comment: "") } } @@ -70,7 +70,7 @@ struct EMailLoginView: View { .frame(height: 60) // 标题 - Text("email_login.title".localized) + Text(NSLocalizedString("email_login.title", comment: "")) .font(.system(size: 28, weight: .medium)) .foregroundColor(.white) .padding(.bottom, 80) @@ -89,7 +89,7 @@ struct EMailLoginView: View { TextField("", text: $email) .placeholder(when: email.isEmpty) { - Text("placeholder.enter_email".localized) + Text(NSLocalizedString("placeholder.enter_email", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) @@ -114,7 +114,7 @@ struct EMailLoginView: View { HStack { TextField("", text: $verificationCode) .placeholder(when: verificationCode.isEmpty) { - Text("placeholder.enter_verification_code".localized) + Text(NSLocalizedString("placeholder.enter_verification_code", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) @@ -178,7 +178,7 @@ struct EMailLoginView: View { .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(0.8) } - Text(store.isLoading ? "email_login.logging_in".localized : "email_login.login_button".localized) + Text(store.isLoading ? NSLocalizedString("email_login.logging_in", comment: "") : NSLocalizedString("email_login.login_button", comment: "")) .font(.system(size: 18, weight: .semibold)) .foregroundColor(.white) } diff --git a/yana/Views/FeedView.swift b/yana/Views/FeedView.swift index 942edee..a873f84 100644 --- a/yana/Views/FeedView.swift +++ b/yana/Views/FeedView.swift @@ -1,6 +1,64 @@ import SwiftUI import ComposableArchitecture +struct FeedTopBarView: View { + let store: StoreOf + var body: some View { + HStack { + Spacer() + Text("Enjoy your Life Time") + .font(.system(size: 22, weight: .semibold)) + .foregroundColor(.white) + Spacer() + Button(action: { + store.send(.showCreateFeed) + }) { + Image("add icon") + .frame(width: 36, height: 36) + } + } + .padding(.horizontal, 20) + } +} + +struct FeedMomentsListView: View { + let store: StoreOf + var body: some View { + WithPerceptionTracking { + LazyVStack(spacing: 16) { + if store.moments.isEmpty { + VStack(spacing: 12) { + Image(systemName: "heart.text.square") + .font(.system(size: 40)) + .foregroundColor(.white.opacity(0.6)) + Text("暂无动态内容") + .font(.system(size: 16)) + .foregroundColor(.white.opacity(0.8)) + if let error = store.error { + Text("错误: \(error)") + .font(.system(size: 12)) + .foregroundColor(.red.opacity(0.8)) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) + } + } + .padding(.top, 40) + } else { + ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in + OptimizedDynamicCardView( + moment: moment, + allMoments: store.moments, + currentIndex: index + ) + } + } + } + } + .padding(.horizontal, 16) + .padding(.top, 30) + } +} + struct FeedView: View { let store: StoreOf @@ -9,80 +67,18 @@ struct FeedView: View { GeometryReader { geometry in ScrollView { VStack(spacing: 20) { - // 顶部区域 - 标题和加号按钮 - HStack { - Spacer() - - // 标题 - Text("Enjoy your Life Time") - .font(.system(size: 22, weight: .semibold)) - .foregroundColor(.white) - - Spacer() - - // 右侧加号按钮 - Button(action: { - store.send(.showCreateFeed) - }) { - Image("add icon") - .frame(width: 36, height: 36) - } - } - .padding(.horizontal, 20) - - // 心脏图标 + FeedTopBarView(store: store) Image(systemName: "heart.fill") .font(.system(size: 60)) .foregroundColor(.red) .padding(.top, 40) - - // 励志文字 Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.") .font(.system(size: 16)) .multilineTextAlignment(.center) .foregroundColor(.white.opacity(0.9)) .padding(.horizontal, 30) .padding(.top, 20) - - // 真实动态数据 - 直接使用store状态 - WithPerceptionTracking { - LazyVStack(spacing: 16) { - if store.moments.isEmpty { - // 空状态 - VStack(spacing: 12) { - Image(systemName: "heart.text.square") - .font(.system(size: 40)) - .foregroundColor(.white.opacity(0.6)) - - Text("暂无动态内容") - .font(.system(size: 16)) - .foregroundColor(.white.opacity(0.8)) - - if let error = store.error { - Text("错误: \(error)") - .font(.system(size: 12)) - .foregroundColor(.red.opacity(0.8)) - .multilineTextAlignment(.center) - .padding(.horizontal, 20) - } - } - .padding(.top, 40) - } else { - // 显示真实动态数据 - ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in - OptimizedDynamicCardView( - moment: moment, - allMoments: store.moments, - currentIndex: index - ) - } - } - } - } - .padding(.horizontal, 16) - .padding(.top, 30) - - // 加载状态 - 直接使用store状态 + FeedMomentsListView(store: store) if store.isLoading { HStack { ProgressView() @@ -93,8 +89,6 @@ struct FeedView: View { } .padding(.top, 20) } - - // 底部安全区域 Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) } } @@ -108,14 +102,8 @@ struct FeedView: View { get: { store.isShowingCreateFeed }, set: { _ in store.send(.dismissCreateFeed) } )) { - CreateFeedView( - store: Store(initialState: CreateFeedFeature.State()) { - CreateFeedFeature() - } - ) - .onDisappear { - // 当CreateFeedView消失时,可能需要刷新数据 - // 这里可以根据需要添加逻辑 + if let createFeedStore = store.scope(state: \.createFeedState, action: \.createFeed) { + CreateFeedView(store: createFeedStore) } } } diff --git a/yana/Views/IDLoginView.swift b/yana/Views/IDLoginView.swift index fcda328..0e0a8e7 100644 --- a/yana/Views/IDLoginView.swift +++ b/yana/Views/IDLoginView.swift @@ -46,7 +46,7 @@ struct IDLoginView: View { .frame(height: 60) // 标题 - Text("id_login.title".localized) + Text(NSLocalizedString("id_login.title", comment: "")) .font(.system(size: 28, weight: .medium)) .foregroundColor(.white) .padding(.bottom, 80) @@ -65,7 +65,7 @@ struct IDLoginView: View { TextField("", text: $userID) // 使用SwiftUI的绑定 .placeholder(when: userID.isEmpty) { - Text("placeholder.enter_id".localized) + Text(NSLocalizedString("placeholder.enter_id", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) @@ -88,7 +88,7 @@ struct IDLoginView: View { if isPasswordVisible { TextField("", text: $password) // 使用SwiftUI的绑定 .placeholder(when: password.isEmpty) { - Text("placeholder.enter_password".localized) + Text(NSLocalizedString("placeholder.enter_password", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) @@ -96,7 +96,7 @@ struct IDLoginView: View { } else { SecureField("", text: $password) // 使用SwiftUI的绑定 .placeholder(when: password.isEmpty) { - Text("placeholder.enter_password".localized) + Text(NSLocalizedString("placeholder.enter_password", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) @@ -122,7 +122,7 @@ struct IDLoginView: View { Button(action: { showRecoverPassword = true }) { - Text("id_login.forgot_password".localized) + Text(NSLocalizedString("id_login.forgot_password", comment: "")) .font(.system(size: 14)) .foregroundColor(.white.opacity(0.8)) } @@ -156,7 +156,7 @@ struct IDLoginView: View { .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(0.8) } - Text(store.isLoading ? "id_login.logging_in".localized : "id_login.login_button".localized) + Text(store.isLoading ? NSLocalizedString("id_login.logging_in", comment: "") : NSLocalizedString("id_login.login_button", comment: "")) .font(.system(size: 18, weight: .semibold)) .foregroundColor(.white) } @@ -207,7 +207,7 @@ struct IDLoginView: View { #if DEBUG // 移除测试用的硬编码凭据 - debugInfo("🐛 Debug模式: 已移除硬编码测试凭据") + debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据") #endif } } diff --git a/yana/Views/LanguageSettingsView.swift b/yana/Views/LanguageSettingsView.swift index 0afad22..b90fb02 100644 --- a/yana/Views/LanguageSettingsView.swift +++ b/yana/Views/LanguageSettingsView.swift @@ -10,7 +10,7 @@ struct LanguageSettingsView: View { } var body: some View { - NavigationView { + NavigationStack { List { Section { ForEach(LocalizationManager.SupportedLanguage.allCases, id: \.rawValue) { language in @@ -47,13 +47,6 @@ struct LanguageSettingsView: View { .navigationTitle("语言设置 / Language") .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("返回 / Back") { - isPresented = false - } - } - } } } } diff --git a/yana/Views/LoginView.swift b/yana/Views/LoginView.swift index 215ec4d..77e57e7 100644 --- a/yana/Views/LoginView.swift +++ b/yana/Views/LoginView.swift @@ -3,7 +3,7 @@ import ComposableArchitecture // PreferenceKey 用于传递图片高度 struct ImageHeightPreferenceKey: PreferenceKey { - static var defaultValue: CGFloat = 0 + static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) } @@ -21,7 +21,7 @@ struct LoginView: View { @State private var showEmailLogin = false // 新增:邮箱登录导航状态 var body: some View { - NavigationView { + NavigationStack { GeometryReader { geometry in ZStack { // 使用与 splash 相同的背景图片 @@ -44,7 +44,7 @@ struct LoginView: View { ) // E-PARTI 文本,底部对齐"top"图片底部,间距20 HStack { - Text("login.app_title".localized) + Text(NSLocalizedString("login.app_title", comment: "")) .font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width)) .foregroundColor(.white) .padding(.leading, 20) @@ -78,7 +78,7 @@ struct LoginView: View { LoginButton( iconName: "person.circle.fill", iconColor: .green, - title: "login.id_login".localized + title: NSLocalizedString("login.id_login", comment: "") ) { showIDLogin = true // 直接设置SwiftUI状态 } @@ -86,7 +86,7 @@ struct LoginView: View { LoginButton( iconName: "envelope.fill", iconColor: .blue, - title: "login.email_login".localized + title: NSLocalizedString("login.email_login", comment: "") ) { showEmailLogin = true // 显示邮箱登录界面 } @@ -153,7 +153,6 @@ struct LoginView: View { } .navigationBarHidden(true) } - .navigationViewStyle(StackNavigationViewStyle()) .sheet(isPresented: $showLanguageSettings) { LanguageSettingsView(isPresented: $showLanguageSettings) } diff --git a/yana/Views/MeView.swift b/yana/Views/MeView.swift index b194c89..27d951d 100644 --- a/yana/Views/MeView.swift +++ b/yana/Views/MeView.swift @@ -78,7 +78,7 @@ struct MeView: View { .alert("确认退出", isPresented: $showLogoutConfirmation) { Button("取消", role: .cancel) { } Button("退出", role: .destructive) { - performLogout() + Task { await performLogout() } } } message: { Text("确定要退出登录吗?") @@ -86,16 +86,13 @@ struct MeView: View { } // MARK: - 退出登录方法 - private func performLogout() { - debugInfo("🔓 开始执行退出登录...") - + private func performLogout() async { + debugInfoSync("🔓 开始执行退出登录...") // 清除所有认证数据(包括 keychain 中的内容) - UserInfoManager.clearAllAuthenticationData() - + await UserInfoManager.clearAllAuthenticationData() // 发送通知重置 window root 为 login view NotificationCenter.default.post(name: .homeLogout, object: nil) - - debugInfo("✅ 退出登录完成") + debugInfoSync("✅ 退出登录完成") } } diff --git a/yana/Views/RecoverPasswordView.swift b/yana/Views/RecoverPasswordView.swift index 1cd1462..3d048fd 100644 --- a/yana/Views/RecoverPasswordView.swift +++ b/yana/Views/RecoverPasswordView.swift @@ -1,5 +1,6 @@ import SwiftUI import ComposableArchitecture +import Combine struct RecoverPasswordView: View { let store: StoreOf @@ -13,7 +14,7 @@ struct RecoverPasswordView: View { // 验证码倒计时状态 @State private var countdown: Int = 0 - @State private var countdownTimer: Timer? + private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() // 计算确认按钮是否可用 private var isConfirmButtonEnabled: Bool { @@ -32,7 +33,7 @@ struct RecoverPasswordView: View { } else if countdown > 0 { return "\(countdown)s" } else { - return "recover_password.get_code".localized + return NSLocalizedString("recover_password.get_code", comment: "") } } @@ -66,7 +67,7 @@ struct RecoverPasswordView: View { .frame(height: 60) // 标题 - Text("recover_password.title".localized) + Text(NSLocalizedString("recover_password.title", comment: "")) .font(.system(size: 28, weight: .medium)) .foregroundColor(.white) .padding(.bottom, 80) @@ -85,7 +86,7 @@ struct RecoverPasswordView: View { TextField("", text: $email) .placeholder(when: email.isEmpty) { - Text("recover_password.placeholder_email".localized) + Text(NSLocalizedString("recover_password.placeholder_email", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) @@ -108,7 +109,7 @@ struct RecoverPasswordView: View { HStack { TextField("", text: $verificationCode) .placeholder(when: verificationCode.isEmpty) { - Text("recover_password.placeholder_verification_code".localized) + Text(NSLocalizedString("recover_password.placeholder_verification_code", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) @@ -158,7 +159,7 @@ struct RecoverPasswordView: View { if isNewPasswordVisible { TextField("", text: $newPassword) .placeholder(when: newPassword.isEmpty) { - Text("recover_password.placeholder_new_password".localized) + Text(NSLocalizedString("recover_password.placeholder_new_password", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) @@ -166,7 +167,7 @@ struct RecoverPasswordView: View { } else { SecureField("", text: $newPassword) .placeholder(when: newPassword.isEmpty) { - Text("recover_password.placeholder_new_password".localized) + Text(NSLocalizedString("recover_password.placeholder_new_password", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) @@ -211,7 +212,7 @@ struct RecoverPasswordView: View { .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(0.8) } - Text(store.isResetLoading ? "recover_password.resetting".localized : "recover_password.confirm_button".localized) + Text(store.isResetLoading ? NSLocalizedString("recover_password.resetting", comment: "") : NSLocalizedString("recover_password.confirm_button", comment: "")) .font(.system(size: 18, weight: .semibold)) .foregroundColor(.white) } @@ -244,15 +245,13 @@ struct RecoverPasswordView: View { newPassword = "" isNewPasswordVisible = false countdown = 0 - stopCountdown() - #if DEBUG email = "exzero@126.com" store.send(.emailChanged(email)) #endif } .onDisappear { - stopCountdown() + countdown = 0 } .onChange(of: email) { newEmail in store.send(.emailChanged(newEmail)) @@ -275,24 +274,20 @@ struct RecoverPasswordView: View { onBack() } } + .onReceive(timer) { _ in + if countdown > 0 { + countdown -= 1 + } + } } // MARK: - Private Methods private func startCountdown() { countdown = 60 - countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in - if countdown > 0 { - countdown -= 1 - } else { - stopCountdown() - } - } } private func stopCountdown() { - countdownTimer?.invalidate() - countdownTimer = nil countdown = 0 } } diff --git a/yana/yanaApp.swift b/yana/yanaApp.swift index 890cca8..fecd363 100644 --- a/yana/yanaApp.swift +++ b/yana/yanaApp.swift @@ -21,7 +21,7 @@ struct yanaApp: App { } #endif - debugInfo("🛠 原生URLSession测试开始") + debugInfoSync("🛠 原生URLSession测试开始") } var body: some Scene { diff --git a/yanaAPITests/yanaAPITests.swift b/yanaAPITests/yanaAPITests.swift index cb75126..67c85cc 100644 --- a/yanaAPITests/yanaAPITests.swift +++ b/yanaAPITests/yanaAPITests.swift @@ -163,10 +163,10 @@ final class yanaAPITests: XCTestCase { XCTAssertEqual(accountModel?.uid, "3184", "真实API数据的UID应该正确") XCTAssertTrue(accountModel?.hasValidAuthentication ?? false, "真实API数据应该有有效认证") - debugInfo("✅ 真实API数据测试通过") - debugInfo(" UID: \(accountModel?.uid ?? "nil")") - debugInfo(" Access Token存在: \(accountModel?.accessToken != nil)") - debugInfo(" Token类型: \(accountModel?.tokenType ?? "nil")") + debugInfoSync("✅ 真实API数据测试通过") + debugInfoSync(" UID: \(accountModel?.uid ?? "nil")") + debugInfoSync(" Access Token存在: \(accountModel?.accessToken != nil)") + debugInfoSync(" Token类型: \(accountModel?.tokenType ?? "nil")") } catch { XCTFail("解析真实API数据失败: \(error)")