feat: 更新依赖和项目配置,优化代码结构

- 在Package.swift中注释掉旧的swift-composable-architecture依赖,并添加swift-case-paths依赖。
- 在Podfile中将iOS平台版本更新至16.0,并移除QCloudCOSXML/Transfer依赖,改为使用QCloudCOSXML。
- 更新Podfile.lock以反映依赖变更,确保项目依赖的准确性。
- 新增架构分析需求文档,明确项目架构评估和改进建议。
- 在多个文件中实现async/await语法,提升异步操作的可读性和性能。
- 更新日志输出方法,确保在调试模式下提供一致的调试信息。
- 优化多个视图组件,提升用户体验和代码可维护性。
This commit is contained in:
edwinQQQ
2025-07-17 18:47:09 +08:00
parent 4bbb4f8434
commit 128bf36c88
46 changed files with 1250 additions and 1203 deletions

View File

@@ -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

View File

@@ -15,7 +15,8 @@ let package = Package(
), ),
], ],
dependencies: [ 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: [ targets: [
.target( .target(
@@ -29,4 +30,4 @@ let package = Package(
dependencies: ["yana"] dependencies: ["yana"]
), ),
] ]
) )

View File

@@ -1,5 +1,5 @@
# Uncomment the next line to define a global platform for your project # Uncomment the next line to define a global platform for your project
platform :ios, '13.0' platform :ios, '16.0'
target 'yana' do target 'yana' do
# Comment the next line if you don't want to use dynamic frameworks # Comment the next line if you don't want to use dynamic frameworks
@@ -19,7 +19,7 @@ target 'yana' do
pod 'Alamofire' pod 'Alamofire'
# 腾讯云 COS 精简版 SDK # 腾讯云 COS 精简版 SDK
pod 'QCloudCOSXML/Transfer' pod 'QCloudCOSXML'
end end
post_install do |installer| post_install do |installer|

View File

@@ -1,24 +1,32 @@
PODS: PODS:
- Alamofire (5.10.2) - Alamofire (5.10.2)
- QCloudCore/WithoutMTA (6.5.1) - QCloudCore (6.5.1):
- QCloudCOSXML/Transfer (6.5.1): - QCloudCore/Default (= 6.5.1)
- QCloudCore/WithoutMTA (= 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: DEPENDENCIES:
- Alamofire - Alamofire
- QCloudCOSXML/Transfer - QCloudCOSXML
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- Alamofire - Alamofire
- QCloudCore - QCloudCore
- QCloudCOSXML - QCloudCOSXML
- QCloudTrack
SPEC CHECKSUMS: SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798 QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7 QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
PODFILE CHECKSUM: cdb7b0983805213c0c4148aeaed5b1e5ea5345ab PODFILE CHECKSUM: cd339c4c75faaa076936a34cac2be597c64f138a
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View File

@@ -10,6 +10,8 @@
4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C91082DE85A3E00384527 /* SystemConfiguration.framework */; }; 4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C91082DE85A3E00384527 /* SystemConfiguration.framework */; };
4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C910A2DE85A4F00384527 /* CoreFoundation.framework */; }; 4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C910A2DE85A4F00384527 /* CoreFoundation.framework */; };
4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 4C4C912F2DE864F000384527 /* ComposableArchitecture */; }; 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 */; }; DF26704F4C1F2ABA0405A0C3 /* Pods_yana.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9DBA38268C435BE06F39AE2 /* Pods_yana.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -65,7 +67,9 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4CE9EFEA2E28FC3B0078D046 /* CasePaths in Frameworks */,
4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */, 4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */,
4CE9EFEC2E28FC3B0078D046 /* CasePathsCore in Frameworks */,
4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */, 4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */,
4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */, 4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */,
DF26704F4C1F2ABA0405A0C3 /* Pods_yana.framework in Frameworks */, DF26704F4C1F2ABA0405A0C3 /* Pods_yana.framework in Frameworks */,
@@ -144,6 +148,7 @@
4C3E651C2DB61F7A00E5A455 /* Frameworks */, 4C3E651C2DB61F7A00E5A455 /* Frameworks */,
4C3E651D2DB61F7A00E5A455 /* Resources */, 4C3E651D2DB61F7A00E5A455 /* Resources */,
0C5E1A8360250314246001A5 /* [CP] Embed Pods Frameworks */, 0C5E1A8360250314246001A5 /* [CP] Embed Pods Frameworks */,
0BECA6AFA61BE910372F6299 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -186,7 +191,7 @@
attributes = { attributes = {
BuildIndependentTargetsInParallel = 1; BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1630; LastSwiftUpdateCheck = 1630;
LastUpgradeCheck = 1630; LastUpgradeCheck = 1640;
TargetAttributes = { TargetAttributes = {
4C3E651E2DB61F7A00E5A455 = { 4C3E651E2DB61F7A00E5A455 = {
CreatedOnToolsVersion = 16.3; CreatedOnToolsVersion = 16.3;
@@ -209,6 +214,7 @@
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = ( packageReferences = (
4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, 4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */,
4CE9EFE82E28FC3B0078D046 /* XCRemoteSwiftPackageReference "swift-case-paths" */,
); );
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = 4C3E65202DB61F7A00E5A455 /* Products */; productRefGroup = 4C3E65202DB61F7A00E5A455 /* Products */;
@@ -239,6 +245,27 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase 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 */ = { 0C5E1A8360250314246001A5 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -315,6 +342,7 @@
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -379,6 +407,7 @@
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -467,7 +496,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 15.6; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -483,7 +512,8 @@
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h"; 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; TARGETED_DEVICE_FAMILY = 1;
}; };
name = Debug; name = Debug;
@@ -524,7 +554,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 15.6; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -540,7 +570,8 @@
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h"; 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; TARGETED_DEVICE_FAMILY = 1;
}; };
name = Release; name = Release;
@@ -632,6 +663,14 @@
minimumVersion = 1.20.2; 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 */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@@ -640,6 +679,16 @@
package = 4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; package = 4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */;
productName = ComposableArchitecture; 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 */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 4C3E65172DB61F7A00E5A455 /* Project object */; rootObject = 4C3E65172DB61F7A00E5A455 /* Project object */;

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8", "originHash" : "411c4947a4ddec377a0aac37852b26ccdf5b2dd58cb99bedfb2d4c108efd60fd",
"pins" : [ "pins" : [
{ {
"identity" : "combine-schedulers", "identity" : "combine-schedulers",
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths", "location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : { "state" : {
"revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25", "branch" : "main",
"version" : "1.7.0" "revision" : "9810c8d6c2914de251e072312f01d3bf80071852"
} }
}, },
{ {

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8", "originHash" : "411c4947a4ddec377a0aac37852b26ccdf5b2dd58cb99bedfb2d4c108efd60fd",
"pins" : [ "pins" : [
{ {
"identity" : "combine-schedulers", "identity" : "combine-schedulers",
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths", "location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : { "state" : {
"revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25", "branch" : "main",
"version" : "1.7.0" "revision" : "9810c8d6c2914de251e072312f01d3bf80071852"
} }
}, },
{ {
@@ -87,8 +87,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-navigation", "location" : "https://github.com/pointfreeco/swift-navigation",
"state" : { "state" : {
"revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4", "revision" : "ae208d1a5cf33aee1d43734ea780a09ada6e2a21",
"version" : "2.3.0" "version" : "2.3.1"
} }
}, },
{ {

View File

@@ -3,4 +3,176 @@
uuid = "A60FAB2A-3184-45B2-920F-A3D7A086CF95" uuid = "A60FAB2A-3184-45B2-920F-A3D7A086CF95"
type = "0" type = "0"
version = "2.0"> version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "55EE8FC9-7BA5-40D2-B71E-7199EFC95B12"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Views/FeedView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "102"
endingLineNumber = "102"
landmarkName = "body"
landmarkType = "24">
<Locations>
<Location
uuid = "55EE8FC9-7BA5-40D2-B71E-7199EFC95B12 - 6293d947a1803ee3"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (SwiftUI.GeometryProxy) -&gt; &lt;&lt;opaque return type of SwiftUI.View.sheet&lt;&#x3c4;_0_0 where &#x3c4;_1_0: SwiftUI.View&gt;(isPresented: SwiftUI.Binding&lt;Swift.Bool&gt;, onDismiss: Swift.Optional&lt;() -&gt; ()&gt;, content: () -&gt; &#x3c4;_1_0) -&gt; some&gt;&gt;.0 in closure #1 () -&gt; SwiftUI.GeometryReader&lt;&lt;&lt;opaque return type of SwiftUI.View.sheet&lt;&#x3c4;_0_0 where &#x3c4;_1_0: SwiftUI.View&gt;(isPresented: SwiftUI.Binding&lt;Swift.Bool&gt;, onDismiss: Swift.Optional&lt;() -&gt; ()&gt;, content: () -&gt; &#x3c4;_1_0) -&gt; some&gt;&gt;.0&gt; in yana.FeedView.body.getter : some"
moduleName = "yana.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Views/FeedView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "102"
endingLineNumber = "102">
</Location>
<Location
uuid = "55EE8FC9-7BA5-40D2-B71E-7199EFC95B12 - ba104df0a01f94b"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #4 @Sendable () -&gt; Swift.Bool in closure #1 (SwiftUI.GeometryProxy) -&gt; &lt;&lt;opaque return type of SwiftUI.View.sheet&lt;&#x3c4;_0_0 where &#x3c4;_1_0: SwiftUI.View&gt;(isPresented: SwiftUI.Binding&lt;Swift.Bool&gt;, onDismiss: Swift.Optional&lt;() -&gt; ()&gt;, content: () -&gt; &#x3c4;_1_0) -&gt; some&gt;&gt;.0 in closure #1 () -&gt; SwiftUI.GeometryReader&lt;&lt;&lt;opaque return type of SwiftUI.View.sheet&lt;&#x3c4;_0_0 where &#x3c4;_1_0: SwiftUI.View&gt;(isPresented: SwiftUI.Binding&lt;Swift.Bool&gt;, onDismiss: Swift.Optional&lt;() -&gt; ()&gt;, content: () -&gt; &#x3c4;_1_0) -&gt; some&gt;&gt;.0&gt; in yana.FeedView.body.getter : some"
moduleName = "yana.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Views/FeedView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "102"
endingLineNumber = "102">
</Location>
</Locations>
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "3E663F1F-E6A0-45A6-87FC-B05E919ADDEB"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Features/SplashFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "52"
endingLineNumber = "52"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "B1F260B9-69B0-4607-AB2D-F9ECEC954EDF"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "65"
endingLineNumber = "65"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "2308DE52-487A-4A72-9377-A7C0C09DACD4"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "134"
endingLineNumber = "134"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "44D54396-6B42-4B2E-8621-CB59559FCDB1"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Features/LoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "168"
endingLineNumber = "168"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "8732BF66-8904-4DD4-9844-B30786433A70"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "69"
endingLineNumber = "69"
landmarkName = "body"
landmarkType = "24">
<Locations>
<Location
uuid = "8732BF66-8904-4DD4-9844-B30786433A70 - 33fd8ff0f3f68ab7"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "(1) suspend resume partial function for closure #1 @Sendable (ComposableArchitecture.Send&lt;yana.IDLoginFeature.Action&gt;) async -&gt; () in closure #1 (inout yana.IDLoginFeature.State, yana.IDLoginFeature.Action) -&gt; ComposableArchitecture.Effect&lt;yana.IDLoginFeature.Action&gt; in yana.IDLoginFeature.body.getter : some"
moduleName = "yana.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "69"
endingLineNumber = "69">
</Location>
<Location
uuid = "8732BF66-8904-4DD4-9844-B30786433A70 - 8f264cf5d91c4ec9"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "(3) suspend resume partial function for closure #1 @Sendable (ComposableArchitecture.Send&lt;yana.IDLoginFeature.Action&gt;) async -&gt; () in closure #1 (inout yana.IDLoginFeature.State, yana.IDLoginFeature.Action) -&gt; ComposableArchitecture.Effect&lt;yana.IDLoginFeature.Action&gt; in yana.IDLoginFeature.body.getter : some"
moduleName = "yana.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "69"
endingLineNumber = "69">
</Location>
<Location
uuid = "8732BF66-8904-4DD4-9844-B30786433A70 - 8f264cf5d91c4ec9"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "(3) suspend resume partial function for closure #1 @Sendable (ComposableArchitecture.Send&lt;yana.IDLoginFeature.Action&gt;) async -&gt; () in closure #1 (inout yana.IDLoginFeature.State, yana.IDLoginFeature.Action) -&gt; ComposableArchitecture.Effect&lt;yana.IDLoginFeature.Action&gt; in yana.IDLoginFeature.body.getter : some"
moduleName = "yana.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "69"
endingLineNumber = "69">
</Location>
</Locations>
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket> </Bucket>

View File

@@ -85,40 +85,36 @@ struct APIConfiguration {
/// - /// -
/// ///
/// ///
static var defaultHeaders: [String: String] { static func defaultHeaders() async -> [String: String] {
var headers = [ var headers = [
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json", "Accept": "application/json",
"Accept-Encoding": "gzip, br", "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", "App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 16.4; Scale/2.00)" "User-Agent": "YuMi/20.20.61 (iPhone; iOS 16.4; Scale/2.00)"
] ]
// headers // headers
let authStatus = UserInfoManager.checkAuthenticationStatus() let authStatus = await UserInfoManager.checkAuthenticationStatus()
if authStatus.canAutoLogin { if authStatus.canAutoLogin {
// headers AccountModel // headers AccountModel
if let userId = UserInfoManager.getCurrentUserId() { if let userId = await UserInfoManager.getCurrentUserId() {
headers["pub_uid"] = userId headers["pub_uid"] = userId
#if DEBUG #if DEBUG
debugInfo("🔐 添加认证 header: pub_uid = \(userId)") debugInfoSync("🔐 添加认证 header: pub_uid = \(userId)")
#endif #endif
} }
if let userTicket = await UserInfoManager.getCurrentUserTicket() {
if let userTicket = UserInfoManager.getCurrentUserTicket() {
headers["pub_ticket"] = userTicket headers["pub_ticket"] = userTicket
#if DEBUG #if DEBUG
debugInfo("🔐 添加认证 header: pub_ticket = \(userTicket.prefix(20))...") debugInfoSync("🔐 添加认证 header: pub_ticket = \(userTicket.prefix(20))...")
#endif #endif
} }
} else { } else {
#if DEBUG #if DEBUG
debugInfo("🔐 跳过认证 header 添加 - 认证状态: \(authStatus.description)") debugInfoSync("🔐 跳过认证 header 添加 - 认证状态: \(authStatus.description)")
#endif #endif
} }
return headers return headers
} }
} }

View File

@@ -1,6 +1,7 @@
import Foundation import Foundation
// MARK: - API Logger // MARK: - API Logger
@MainActor
class APILogger { class APILogger {
enum LogLevel { enum LogLevel {
case none case none
@@ -21,7 +22,12 @@ class APILogger {
}() }()
// MARK: - Request Logging // MARK: - Request Logging
static func logRequest<T: APIRequestProtocol>(_ request: T, url: URL, body: Data?, finalHeaders: [String: String]? = nil) { @MainActor static func logRequest<T: APIRequestProtocol>(
_ request: T,
url: URL,
body: Data?,
finalHeaders: [String: String]? = nil
) {
#if DEBUG #if DEBUG
guard logLevel != .none else { return } guard logLevel != .none else { return }
#else #else

View File

@@ -111,9 +111,10 @@ struct BaseRequest: Codable {
case pubSign = "pub_sign" case pubSign = "pub_sign"
} }
@MainActor
init() { init() {
// //
let preferredLanguage = Locale.current.languageCode ?? "en" let preferredLanguage = Locale.current.language.languageCode?.identifier ?? "en"
self.acceptLanguage = preferredLanguage self.acceptLanguage = preferredLanguage
self.lang = preferredLanguage self.lang = preferredLanguage
@@ -237,6 +238,7 @@ struct CarrierInfoManager {
// MARK: - User Info Manager (for Headers) // MARK: - User Info Manager (for Headers)
struct UserInfoManager { struct UserInfoManager {
@MainActor
private static let keychain = KeychainManager.shared private static let keychain = KeychainManager.shared
// MARK: - Storage Keys // MARK: - Storage Keys
@@ -246,72 +248,66 @@ struct UserInfoManager {
} }
// MARK: - // MARK: -
private static var accountModelCache: AccountModel? // UserInfoCacheActor
private static var userInfoCache: UserInfo?
private static let cacheQueue = DispatchQueue(label: "com.yana.userinfo.cache", attributes: .concurrent) private static let cacheQueue = DispatchQueue(label: "com.yana.userinfo.cache", attributes: .concurrent)
// MARK: - User ID Management ( AccountModel) // MARK: - User ID Management ( AccountModel)
static func getCurrentUserId() -> String? { static func getCurrentUserId() async -> String? {
return getAccountModel()?.uid return await getAccountModel()?.uid
} }
// MARK: - Access Token Management ( AccountModel) // MARK: - Access Token Management ( AccountModel)
static func getAccessToken() -> String? { static func getAccessToken() async -> String? {
return getAccountModel()?.accessToken return await getAccountModel()?.accessToken
} }
// MARK: - Ticket Management ( AccountModel ) // MARK: - Ticket Management ( AccountModel )
private static var currentTicket: String? // UserInfoCacheActor
static func getCurrentUserTicket() -> String? { static func getCurrentUserTicket() async -> String? {
// AccountModel ticket // AccountModel ticket
if let accountTicket = getAccountModel()?.ticket, !accountTicket.isEmpty { if let accountTicket = await getAccountModel()?.ticket, !accountTicket.isEmpty {
return accountTicket return accountTicket
} }
// // actor
return currentTicket return await cacheActor.getCurrentTicket()
} }
static func saveTicket(_ ticket: String) { static func saveTicket(_ ticket: String) async {
currentTicket = ticket await cacheActor.setCurrentTicket(ticket)
debugInfo("💾 保存 Ticket 到内存") debugInfoSync("💾 保存 Ticket 到内存")
} }
static func clearTicket() { static func clearTicket() async {
currentTicket = nil await cacheActor.clearCurrentTicket()
debugInfo("🗑️ 清除 Ticket") debugInfoSync("🗑️ 清除 Ticket")
} }
// MARK: - User Info Management // MARK: - User Info Management
static func saveUserInfo(_ userInfo: UserInfo) { static func saveUserInfo(_ userInfo: UserInfo) async {
cacheQueue.async(flags: .barrier) { do {
do { try await keychain.store(userInfo, forKey: StorageKeys.userInfo)
try keychain.store(userInfo, forKey: StorageKeys.userInfo) await cacheActor.setUserInfo(userInfo)
userInfoCache = userInfo debugInfoSync("💾 保存用户信息成功")
debugInfo("💾 保存用户信息成功") } catch {
} catch { debugErrorSync("❌ 保存用户信息失败: \(error)")
debugError("❌ 保存用户信息失败: \(error)")
}
} }
} }
static func getUserInfo() -> UserInfo? { static func getUserInfo() async -> UserInfo? {
return cacheQueue.sync { //
// if let cached = await cacheActor.getUserInfo() {
if let cached = userInfoCache { return cached
return cached }
} // Keychain
do {
// Keychain let userInfo = try await keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo)
do { await cacheActor.setUserInfo(userInfo)
let userInfo = try keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo) return userInfo
userInfoCache = userInfo } catch {
return userInfo debugErrorSync("❌ 读取用户信息失败: \(error)")
} catch { return nil
debugError("❌ 读取用户信息失败: \(error)")
return nil
}
} }
} }
@@ -322,7 +318,7 @@ struct UserInfoManager {
ticket: String, ticket: String,
uid: Int?, uid: Int?,
userInfo: UserInfo? userInfo: UserInfo?
) { ) async {
// AccountModel // AccountModel
let accountModel = AccountModel( let accountModel = AccountModel(
uid: uid != nil ? "\(uid!)" : nil, uid: uid != nil ? "\(uid!)" : nil,
@@ -336,38 +332,40 @@ struct UserInfoManager {
ticket: ticket ticket: ticket
) )
saveAccountModel(accountModel) await saveAccountModel(accountModel)
saveTicket(ticket) await saveTicket(ticket)
if let userInfo = userInfo { if let userInfo = userInfo {
saveUserInfo(userInfo) await saveUserInfo(userInfo)
} }
debugInfo("✅ 完整认证信息保存成功") debugInfoSync("✅ 完整认证信息保存成功")
} }
/// ///
static func hasValidAuthentication() -> Bool { static func hasValidAuthentication() async -> Bool {
return getAccessToken() != nil && getCurrentUserTicket() != nil let token = await getAccessToken()
let ticket = await getCurrentUserTicket()
return token != nil && ticket != nil
} }
/// ///
static func clearAllAuthenticationData() { static func clearAllAuthenticationData() async {
clearAccountModel() await clearAccountModel()
clearUserInfo() await clearUserInfo()
clearTicket() await clearTicket()
debugInfo("🗑️ 清除所有认证信息") debugInfoSync("🗑️ 清除所有认证信息")
} }
/// Ticket /// Ticket
static func restoreTicketIfNeeded() async -> Bool { static func restoreTicketIfNeeded() async -> Bool {
guard let accessToken = getAccessToken(), guard let _ = await getAccessToken(),
getCurrentUserTicket() == nil else { await getCurrentUserTicket() == nil else {
return false return false
} }
debugInfo("🔄 尝试使用 Access Token 恢复 Ticket...") debugInfoSync("🔄 尝试使用 Access Token 恢复 Ticket...")
// APIService false // APIService false
// TicketHelper.createTicketRequest // TicketHelper.createTicketRequest
@@ -377,50 +375,48 @@ struct UserInfoManager {
// MARK: - Account Model Management // MARK: - Account Model Management
/// AccountModel /// AccountModel
/// - Parameter accountModel: /// - Parameter accountModel:
static func saveAccountModel(_ accountModel: AccountModel) { static func saveAccountModel(_ accountModel: AccountModel) async {
cacheQueue.async(flags: .barrier) { do {
do { try await keychain.store(accountModel, forKey: StorageKeys.accountModel)
try keychain.store(accountModel, forKey: StorageKeys.accountModel) await cacheActor.setAccountModel(accountModel)
accountModelCache = accountModel
// ticket
// ticket if let ticket = accountModel.ticket {
if let ticket = accountModel.ticket { await saveTicket(ticket)
saveTicket(ticket)
}
debugInfo("💾 AccountModel 保存成功")
} catch {
debugError("❌ AccountModel 保存失败: \(error)")
} }
debugInfoSync("💾 AccountModel 保存成功")
} catch {
debugErrorSync("❌ AccountModel 保存失败: \(error)")
} }
} }
/// AccountModel /// AccountModel
/// - Returns: nil /// - Returns: nil
static func getAccountModel() -> AccountModel? { static func getAccountModel() async -> AccountModel? {
return cacheQueue.sync { //
// if let cached = await cacheActor.getAccountModel() {
if let cached = accountModelCache { return cached
return cached }
} // Keychain
do {
// Keychain let accountModel = try await keychain.retrieve(
do { AccountModel.self,
let accountModel = try keychain.retrieve(AccountModel.self, forKey: StorageKeys.accountModel) forKey: StorageKeys.accountModel
accountModelCache = accountModel )
return accountModel await cacheActor.setAccountModel(accountModel)
} catch { return accountModel
debugError("❌ 读取 AccountModel 失败: \(error)") } catch {
return nil debugErrorSync("❌ 读取 AccountModel 失败: \(error)")
} return nil
} }
} }
/// AccountModel ticket /// AccountModel ticket
/// - Parameter ticket: /// - Parameter ticket:
static func updateAccountModelTicket(_ ticket: String) { static func updateAccountModelTicket(_ ticket: String) async {
guard var accountModel = getAccountModel() else { guard var accountModel = await getAccountModel() else {
debugError("❌ 无法更新 ticketAccountModel 不存在") debugErrorSync("❌ 无法更新 ticketAccountModel 不存在")
return return
} }
@@ -436,97 +432,78 @@ struct UserInfoManager {
ticket: ticket ticket: ticket
) )
saveAccountModel(accountModel) await saveAccountModel(accountModel)
saveTicket(ticket) // ticket await saveTicket(ticket) // ticket
} }
/// AccountModel /// AccountModel
/// - Returns: /// - Returns:
static func hasValidAccountModel() -> Bool { static func hasValidAccountModel() async -> Bool {
guard let accountModel = getAccountModel() else { guard let accountModel = await getAccountModel() else {
return false return false
} }
return accountModel.hasValidAuthentication return accountModel.hasValidAuthentication
} }
/// AccountModel /// AccountModel
static func clearAccountModel() { static func clearAccountModel() async {
cacheQueue.async(flags: .barrier) { do {
do { try await keychain.delete(forKey: StorageKeys.accountModel)
try keychain.delete(forKey: StorageKeys.accountModel) await cacheActor.clearAccountModel()
accountModelCache = nil debugInfoSync("🗑️ AccountModel 已清除")
debugInfo("🗑️ AccountModel 已清除") } catch {
} catch { debugErrorSync("❌ 清除 AccountModel 失败: \(error)")
debugError("❌ 清除 AccountModel 失败: \(error)")
}
} }
} }
/// ///
static func clearUserInfo() { static func clearUserInfo() async {
cacheQueue.async(flags: .barrier) { do {
do { try await keychain.delete(forKey: StorageKeys.userInfo)
try keychain.delete(forKey: StorageKeys.userInfo) await cacheActor.clearUserInfo()
userInfoCache = nil debugInfoSync("🗑️ UserInfo 已清除")
debugInfo("🗑️ UserInfo 已清除") } catch {
} catch { debugErrorSync("❌ 清除 UserInfo 失败: \(error)")
debugError("❌ 清除 UserInfo 失败: \(error)")
}
} }
} }
/// ///
static func clearAllCache() { static func clearAllCache() async {
cacheQueue.async(flags: .barrier) { await cacheActor.clearAccountModel()
accountModelCache = nil await cacheActor.clearUserInfo()
userInfoCache = nil debugInfoSync("🗑️ 清除所有内存缓存")
debugInfo("🗑️ 清除所有内存缓存")
}
} }
/// 访 /// 访
static func preloadCache() { static func preloadCache() async {
cacheQueue.async { await cacheActor.setAccountModel(await getAccountModel())
// AccountModel await cacheActor.setUserInfo(await getUserInfo())
_ = getAccountModel() debugInfoSync("🚀 缓存预加载完成")
// UserInfo
_ = getUserInfo()
debugInfo("🚀 缓存预加载完成")
}
} }
// MARK: - Authentication Validation // MARK: - Authentication Validation
/// ///
/// - Returns: /// - Returns:
static func checkAuthenticationStatus() -> AuthenticationStatus { static func checkAuthenticationStatus() async -> AuthenticationStatus {
return cacheQueue.sync { guard let accountModel = await getAccountModel() else {
guard let accountModel = getAccountModel() else { debugInfoSync("🔍 认证检查:未找到 AccountModel")
debugInfo("🔍 认证检查:未找到 AccountModel") return .notFound
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
} }
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
/// header /// header
static func testAuthenticationHeaders() { static func testAuthenticationHeaders() async {
#if DEBUG #if DEBUG
debugInfo("\n🧪 开始测试认证 header 功能") debugInfoSync("\n🧪 开始测试认证 header 功能")
// 1 // 1
debugInfo("📝 测试1未登录状态") debugInfoSync("📝 测试1未登录状态")
clearAllAuthenticationData() await clearAllAuthenticationData()
let headers1 = APIConfiguration.defaultHeaders let headers1 = await APIConfiguration.defaultHeaders()
let hasAuthHeaders1 = headers1.keys.contains("pub_uid") || headers1.keys.contains("pub_ticket") let hasAuthHeaders1 = headers1.keys.contains("pub_uid") || headers1.keys.contains("pub_ticket")
debugInfo(" 认证 headers 存在: \(hasAuthHeaders1) (应该为 false)") debugInfoSync(" 认证 headers 存在: \(hasAuthHeaders1) (应该为 false)")
// 2 // 2
debugInfo("📝 测试2模拟登录状态") debugInfoSync("📝 测试2模拟登录状态")
let testAccount = AccountModel( let testAccount = AccountModel(
uid: "12345", uid: "12345",
jti: "test-jti", jti: "test-jti",
@@ -580,22 +557,48 @@ struct UserInfoManager {
scope: "read write", scope: "read write",
ticket: "test-ticket-12345678901234567890" 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 hasUid = headers2["pub_uid"] == "12345"
let hasTicket = headers2["pub_ticket"] == "test-ticket-12345678901234567890" let hasTicket = headers2["pub_ticket"] == "test-ticket-12345678901234567890"
debugInfo(" pub_uid 正确: \(hasUid) (应该为 true)") debugInfoSync(" pub_uid 正确: \(hasUid) (应该为 true)")
debugInfo(" pub_ticket 正确: \(hasTicket) (应该为 true)") debugInfoSync(" pub_ticket 正确: \(hasTicket) (应该为 true)")
// 3 // 3
debugInfo("📝 测试3清理测试数据") debugInfoSync("📝 测试3清理测试数据")
clearAllAuthenticationData() await clearAllAuthenticationData()
debugInfo("✅ 认证 header 测试完成\n") debugInfoSync("✅ 认证 header 测试完成\n")
#endif #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 // MARK: - API Request Protocol
/// API /// API
@@ -604,7 +607,7 @@ struct UserInfoManager {
/// API /// API
/// ///
/// ///
/// - Response: /// - Response: Sendable
/// - endpoint: API /// - endpoint: API
/// - method: HTTP /// - method: HTTP
/// - /// -
@@ -618,8 +621,8 @@ struct UserInfoManager {
/// // ... /// // ...
/// } /// }
/// ``` /// ```
protocol APIRequestProtocol { protocol APIRequestProtocol: Sendable {
associatedtype Response: Codable associatedtype Response: Codable & Sendable
var endpoint: String { get } var endpoint: String { get }
var method: HTTPMethod { get } var method: HTTPMethod { get }

View File

@@ -14,7 +14,7 @@ import ComposableArchitecture
/// let request = ConfigRequest() /// let request = ConfigRequest()
/// let response = try await apiService.request(request) /// let response = try await apiService.request(request)
/// ``` /// ```
protocol APIServiceProtocol { protocol APIServiceProtocol: Sendable {
/// ///
/// - Parameter request: APIRequestProtocol /// - Parameter request: APIRequestProtocol
/// - Returns: /// - Returns:
@@ -39,19 +39,22 @@ protocol APIServiceProtocol {
/// - /// -
/// - / /// - /
/// - /// -
struct LiveAPIService: APIServiceProtocol { struct LiveAPIService: APIServiceProtocol, Sendable {
private let session: URLSession private let session: URLSession
private let baseURL: String private let baseURL: String
// actor
private static let cachedBaseURL: String = APIConfiguration.baseURL
private static let cachedTimeout: TimeInterval = APIConfiguration.timeout
/// API /// API
/// - Parameter baseURL: API URL使 /// - Parameter baseURL: API URL使
init(baseURL: String = APIConfiguration.baseURL) { init(baseURL: String = LiveAPIService.cachedBaseURL) {
self.baseURL = baseURL self.baseURL = baseURL
// URLSession // URLSession
let config = URLSessionConfiguration.default let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = APIConfiguration.timeout config.timeoutIntervalForRequest = LiveAPIService.cachedTimeout
config.timeoutIntervalForResource = APIConfiguration.timeout * 2 config.timeoutIntervalForResource = LiveAPIService.cachedTimeout * 2
config.waitsForConnectivity = true config.waitsForConnectivity = true
config.allowsCellularAccess = true config.allowsCellularAccess = true
@@ -78,14 +81,14 @@ struct LiveAPIService: APIServiceProtocol {
let startTime = Date() let startTime = Date()
// Loading // Loading
let loadingId = APILoadingManager.shared.startLoading( let loadingId = await APILoadingManager.shared.startLoading(
shouldShowLoading: request.shouldShowLoading, shouldShowLoading: request.shouldShowLoading,
shouldShowError: request.shouldShowError shouldShowError: request.shouldShowError
) )
// URL // URL
guard let url = buildURL(for: request) else { guard let url = await buildURL(for: request) else {
APILoadingManager.shared.setError(loadingId, errorMessage: APIError.invalidURL.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.invalidURL.localizedDescription)
throw APIError.invalidURL throw APIError.invalidURL
} }
@@ -95,8 +98,8 @@ struct LiveAPIService: APIServiceProtocol {
urlRequest.timeoutInterval = request.timeout urlRequest.timeoutInterval = request.timeout
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
// // await
var headers = APIConfiguration.defaultHeaders var headers = await APIConfiguration.defaultHeaders()
if let customHeaders = request.headers { if let customHeaders = request.headers {
headers.merge(customHeaders) { _, new in new } headers.merge(customHeaders) { _, new in new }
} }
@@ -119,7 +122,7 @@ struct LiveAPIService: APIServiceProtocol {
// //
if request.includeBaseParameters { if request.includeBaseParameters {
// //
var baseParams = BaseRequest() var baseParams = await BaseRequest()
// bodyParams + // bodyParams +
baseParams.generateSignature(with: bodyParams) baseParams.generateSignature(with: bodyParams)
@@ -127,8 +130,7 @@ struct LiveAPIService: APIServiceProtocol {
// //
let baseDict = try baseParams.toDictionary() let baseDict = try baseParams.toDictionary()
finalBody.merge(baseDict) { _, new in new } // finalBody.merge(baseDict) { _, new in new } //
debugInfoSync("🔐 签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)")
debugInfo("🔐 签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)")
} }
requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: []) requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: [])
@@ -136,17 +138,18 @@ struct LiveAPIService: APIServiceProtocol {
// urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") // urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
if let httpBody = urlRequest.httpBody, if let httpBody = urlRequest.httpBody,
let bodyString = String(data: httpBody, encoding: .utf8) { let bodyString = String(data: httpBody, encoding: .utf8) {
debugInfo("HTTP Body: \(bodyString)") debugInfoSync("HTTP Body: \(bodyString)")
} }
} catch { } catch {
let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)") let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)")
APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription)
throw encodingError throw encodingError
} }
} }
// headers // headers
APILogger.logRequest(request, url: url, body: requestBody, finalHeaders: headers) await APILogger
.logRequest(request, url: url, body: requestBody, finalHeaders: headers)
do { do {
// //
@@ -156,34 +159,36 @@ struct LiveAPIService: APIServiceProtocol {
// //
guard let httpResponse = response as? HTTPURLResponse else { guard let httpResponse = response as? HTTPURLResponse else {
let networkError = APIError.networkError("无效的响应类型") let networkError = APIError.networkError("无效的响应类型")
APILoadingManager.shared.setError(loadingId, errorMessage: networkError.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: networkError.localizedDescription)
throw networkError throw networkError
} }
// //
if data.count > APIConfiguration.maxDataSize { if data.count > APIConfiguration.maxDataSize {
APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration) await APILogger
APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription) .logError(APIError.resourceTooLarge, url: url, duration: duration)
await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription)
throw APIError.resourceTooLarge 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 // HTTP
guard 200...299 ~= httpResponse.statusCode else { guard 200...299 ~= httpResponse.statusCode else {
let errorMessage = extractErrorMessage(from: data) let errorMessage = extractErrorMessage(from: data)
let httpError = APIError.httpError(statusCode: httpResponse.statusCode, message: errorMessage) 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 throw httpError
} }
// //
guard !data.isEmpty else { guard !data.isEmpty else {
APILoadingManager.shared.setError(loadingId, errorMessage: APIError.noData.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.noData.localizedDescription)
throw APIError.noData throw APIError.noData
} }
@@ -191,28 +196,28 @@ struct LiveAPIService: APIServiceProtocol {
do { do {
let decoder = JSONDecoder() let decoder = JSONDecoder()
let decodedResponse = try decoder.decode(T.Response.self, from: data) 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 // loading
APILoadingManager.shared.finishLoading(loadingId) await APILoadingManager.shared.finishLoading(loadingId)
return decodedResponse return decodedResponse
} catch { } catch {
let decodingError = APIError.decodingError("响应解析失败: \(error.localizedDescription)") let decodingError = APIError.decodingError("响应解析失败: \(error.localizedDescription)")
APILoadingManager.shared.setError(loadingId, errorMessage: decodingError.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: decodingError.localizedDescription)
throw decodingError throw decodingError
} }
} catch let error as APIError { } catch let error as APIError {
let duration = Date().timeIntervalSince(startTime) let duration = Date().timeIntervalSince(startTime)
APILogger.logError(error, url: url, duration: duration) await APILogger.logError(error, url: url, duration: duration)
APILoadingManager.shared.setError(loadingId, errorMessage: error.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: error.localizedDescription)
throw error throw error
} catch { } catch {
let duration = Date().timeIntervalSince(startTime) let duration = Date().timeIntervalSince(startTime)
let apiError = mapSystemError(error) let apiError = mapSystemError(error)
APILogger.logError(apiError, url: url, duration: duration) await APILogger.logError(apiError, url: url, duration: duration)
APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription)
throw apiError throw apiError
} }
} }
@@ -228,7 +233,7 @@ struct LiveAPIService: APIServiceProtocol {
/// ///
/// - Parameter request: API /// - Parameter request: API
/// - Returns: URL nil /// - Returns: URL nil
private func buildURL<T: APIRequestProtocol>(for request: T) -> URL? { @MainActor private func buildURL<T: APIRequestProtocol>(for request: T) -> URL? {
guard var urlComponents = URLComponents(string: baseURL + request.endpoint) else { guard var urlComponents = URLComponents(string: baseURL + request.endpoint) else {
return nil return nil
} }
@@ -252,9 +257,9 @@ struct LiveAPIService: APIServiceProtocol {
queryItems.append(URLQueryItem(name: key, value: "\(value)")) queryItems.append(URLQueryItem(name: key, value: "\(value)"))
} }
debugInfo("🔐 GET请求签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)") debugInfoSync("🔐 GET请求签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)")
} catch { } catch {
debugWarn("警告:无法添加基础参数到查询字符串") debugWarnSync("警告:无法添加基础参数到查询字符串")
} }
} }
@@ -322,46 +327,31 @@ struct LiveAPIService: APIServiceProtocol {
// MARK: - Mock API Service (for testing) // MARK: - Mock API Service (for testing)
/// API /// Mock API Service
/// actor MockAPIServiceActor: APIServiceProtocol, Sendable {
/// API
/// -
/// -
/// - UI
///
/// 使
/// ```swift
/// var mockService = MockAPIService()
/// mockService.setMockResponse(for: "/client/config", response: mockConfigResponse)
/// let response = try await mockService.request(ConfigRequest())
/// ```
struct MockAPIService: APIServiceProtocol {
private var mockResponses: [String: Any] = [:] private var mockResponses: [String: Any] = [:]
mutating func setMockResponse<T>(for endpoint: String, response: T) { func setMockResponse<T>(for endpoint: String, response: T) {
mockResponses[endpoint] = response mockResponses[endpoint] = response
} }
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response { func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response {
//
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 try await Task.sleep(nanoseconds: 500_000_000) // 0.5
if let mockResponse = mockResponses[request.endpoint] as? T.Response { if let mockResponse = mockResponses[request.endpoint] as? T.Response {
return mockResponse return mockResponse
} }
throw APIError.noData throw APIError.noData
} }
} }
// MARK: - TCA Dependency Integration // MARK: - TCA Dependency Integration
private enum APIServiceKey: DependencyKey { private enum APIServiceKey: DependencyKey {
static let liveValue: APIServiceProtocol = LiveAPIService() static let liveValue: any APIServiceProtocol & Sendable = LiveAPIService()
static let testValue: APIServiceProtocol = MockAPIService() static let testValue: any APIServiceProtocol & Sendable = MockAPIServiceActor()
} }
extension DependencyValues { extension DependencyValues {
var apiService: APIServiceProtocol { var apiService: (any APIServiceProtocol & Sendable) {
get { self[APIServiceKey.self] } get { self[APIServiceKey.self] }
set { self[APIServiceKey.self] = newValue } set { self[APIServiceKey.self] = newValue }
} }

View File

@@ -78,7 +78,7 @@ struct IDLoginAPIRequest: APIRequestProtocol {
let method: HTTPMethod = .POST let method: HTTPMethod = .POST
let includeBaseParameters = true let includeBaseParameters = true
let queryParameters: [String: String]? let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0 let timeout: TimeInterval = 30.0
/// ID /// ID
@@ -98,14 +98,6 @@ struct IDLoginAPIRequest: APIRequestProtocol {
"client_id": clientId, "client_id": clientId,
"grant_type": grantType "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 /// - userID: ID
/// - password: /// - password:
/// - Returns: APInil /// - Returns: APInil
static func createIDLoginRequest(userID: String, password: String) -> IDLoginAPIRequest? { static func createIDLoginRequest(userID: String, password: String) async -> IDLoginAPIRequest? {
// 使DESID // 使DESID
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26" let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedID = DESEncrypt.encryptUseDES(userID, key: encryptionKey), guard let encryptedID = DESEncrypt.encryptUseDES(userID, key: encryptionKey),
let encryptedPassword = DESEncrypt.encryptUseDES(password, key: encryptionKey) else { let encryptedPassword = DESEncrypt.encryptUseDES(password, key: encryptionKey) else {
debugError("❌ DES加密失败") debugErrorSync("❌ DES加密失败")
return nil return nil
} }
debugInfo("🔐 DES加密成功") await debugInfoSync("🔐 DES加密成功")
debugInfo(" 原始ID: \(userID)") await debugInfoSync(" 原始ID: \(userID)")
debugInfo(" 加密后ID: \(encryptedID)") await debugInfoSync(" 加密后ID: \(encryptedID)")
debugInfo(" 原始密码: \(password)") await debugInfoSync(" 原始密码: \(password)")
debugInfo(" 加密后密码: \(encryptedPassword)") await debugInfoSync(" 加密后密码: \(encryptedPassword)")
return IDLoginAPIRequest( return IDLoginAPIRequest(
phone: userID, phone: userID,
@@ -219,7 +211,7 @@ struct TicketAPIRequest: APIRequestProtocol {
let method: HTTPMethod = .POST let method: HTTPMethod = .POST
let includeBaseParameters = true let includeBaseParameters = true
let queryParameters: [String: String]? let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0 let timeout: TimeInterval = 30.0
let customHeaders: [String: String]? let customHeaders: [String: String]?
@@ -292,13 +284,13 @@ struct TicketHelper {
/// - accessToken: OAuth 访 /// - accessToken: OAuth 访
/// - uid: /// - uid:
static func debugTicketRequest(accessToken: String, uid: Int?) { static func debugTicketRequest(accessToken: String, uid: Int?) {
debugInfo("🎫 Ticket 请求调试信息") debugInfoSync("🎫 Ticket 请求调试信息")
debugInfo(" AccessToken: \(accessToken)") debugInfoSync(" AccessToken: \(accessToken)")
debugInfo(" UID: \(uid?.description ?? "nil")") debugInfoSync(" UID: \(uid?.description ?? "nil")")
debugInfo(" Endpoint: /oauth/ticket") debugInfoSync(" Endpoint: /oauth/ticket")
debugInfo(" Method: POST") debugInfoSync(" Method: POST")
debugInfo(" Headers: pub_uid = \(uid?.description ?? "nil")") debugInfoSync(" Headers: pub_uid = \(uid?.description ?? "nil")")
debugInfo(" Parameters: access_token=\(accessToken), issue_type=multi") debugInfoSync(" Parameters: access_token=\(accessToken), issue_type=multi")
} }
} }
@@ -315,7 +307,7 @@ struct EmailGetCodeRequest: APIRequestProtocol {
let method: HTTPMethod = .POST let method: HTTPMethod = .POST
let includeBaseParameters = true let includeBaseParameters = true
let queryParameters: [String: String]? let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0 let timeout: TimeInterval = 30.0
/// ///
@@ -356,7 +348,7 @@ struct EmailLoginRequest: APIRequestProtocol {
let method: HTTPMethod = .POST let method: HTTPMethod = .POST
let includeBaseParameters = true let includeBaseParameters = true
let queryParameters: [String: String]? let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0 let timeout: TimeInterval = 30.0
/// ///
@@ -389,13 +381,13 @@ extension LoginHelper {
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26" let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else { guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else {
debugError("❌ 邮箱DES加密失败") debugErrorSync("❌ 邮箱DES加密失败")
return nil return nil
} }
debugInfo("🔐 邮箱DES加密成功") debugInfoSync("🔐 邮箱DES加密成功")
debugInfo(" 原始邮箱: \(email)") debugInfoSync(" 原始邮箱: \(email)")
debugInfo(" 加密邮箱: \(encryptedEmail)") debugInfoSync(" 加密邮箱: \(encryptedEmail)")
return EmailGetCodeRequest(emailAddress: email, type: 1) return EmailGetCodeRequest(emailAddress: email, type: 1)
} }
@@ -405,18 +397,18 @@ extension LoginHelper {
/// - email: /// - email:
/// - code: /// - code:
/// - Returns: APInil /// - Returns: APInil
static func createEmailLoginRequest(email: String, code: String) -> EmailLoginRequest? { static func createEmailLoginRequest(email: String, code: String) async -> EmailLoginRequest? {
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26" let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else { guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else {
debugError("❌ 邮箱DES加密失败") await debugErrorSync("❌ 邮箱DES加密失败")
return nil return nil
} }
debugInfo("🔐 邮箱验证码登录DES加密成功") await debugInfoSync("🔐 邮箱验证码登录DES加密成功")
debugInfo(" 原始邮箱: \(email)") await debugInfoSync(" 原始邮箱: \(email)")
debugInfo(" 加密邮箱: \(encryptedEmail)") await debugInfoSync(" 加密邮箱: \(encryptedEmail)")
debugInfo(" 验证码: \(code)") await debugInfoSync(" 验证码: \(code)")
return EmailLoginRequest(email: encryptedEmail, code: code) return EmailLoginRequest(email: encryptedEmail, code: code)
} }

View File

@@ -2,13 +2,13 @@ import UIKit
//import NIMSDK //import NIMSDK
class AppDelegate: UIResponder, UIApplicationDelegate { 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 // UserDefaults Keychain
DataMigrationManager.performStartupMigration() DataMigrationManager.performStartupMigration()
// //
UserInfoManager.preloadCache() await UserInfoManager.preloadCache()
// //
// NetworkManager.shared.networkStatusChanged = { status in // NetworkManager.shared.networkStatusChanged = { status in

View File

@@ -4,7 +4,7 @@ enum Environment {
} }
struct AppConfig { struct AppConfig {
static var current: Environment = { static let current: Environment = {
#if DEBUG #if DEBUG
return .development return .development
#else #else
@@ -43,9 +43,9 @@ struct AppConfig {
} }
// //
static func switchEnvironment(to env: Environment) { // static func switchEnvironment(to env: Environment) {
current = env // current = env
} // }
// //
static var enableNetworkDebug: Bool { static var enableNetworkDebug: Bool {

View File

@@ -2,17 +2,18 @@ import Foundation
import UIKit // import UIKit //
@_exported import Alamofire // @_exported import Alamofire //
@MainActor
final class ClientConfig { final class ClientConfig {
static let shared = ClientConfig() static let shared = ClientConfig()
private init() {} private init() {}
func initializeClient() { func initializeClient() {
debugInfo("✅ 开始初始化客户端 - URL: \(AppConfig.baseURL)/client/init") debugInfoSync("✅ 开始初始化客户端 - URL: \(AppConfig.baseURL)/client/init")
callClientInitAPI() // callClientInitAPI() //
} }
func callClientInitAPI() { func callClientInitAPI() {
debugInfo("🆕 使用GET方法调用初始化接口") debugInfoSync("🆕 使用GET方法调用初始化接口")
// let queryParams = [ // let queryParams = [
// "debug": "1", // "debug": "1",

View File

@@ -28,6 +28,144 @@ enum UILogLevel: String, CaseIterable {
case detailed = "详细日志" case detailed = "详细日志"
} }
struct LoginTabView: View {
let store: StoreOf<LoginFeature>
let initStore: StoreOf<InitFeature>
@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 { struct ContentView: View {
let store: StoreOf<LoginFeature> let store: StoreOf<LoginFeature>
let initStore: StoreOf<InitFeature> let initStore: StoreOf<InitFeature>
@@ -38,155 +176,11 @@ struct ContentView: View {
var body: some View { var body: some View {
WithPerceptionTracking { WithPerceptionTracking {
TabView(selection: $selectedTab) { TabView(selection: $selectedTab) {
// LoginTabView(store: store, initStore: initStore, selectedLogLevel: $selectedLogLevel)
VStack { .tabItem {
// Label("登录", systemImage: "person.circle")
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() .tag(0)
.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
ConfigView(store: configStore) ConfigView(store: configStore)
.tabItem { .tabItem {
Label("API 测试", systemImage: "network") Label("API 测试", systemImage: "network")

View File

@@ -1,11 +1,7 @@
import Foundation import Foundation
import ComposableArchitecture import ComposableArchitecture
import SwiftUI import SwiftUI
// PhotosUI (iOS 16.0+)
#if canImport(PhotosUI)
import PhotosUI import PhotosUI
#endif
@Reducer @Reducer
struct CreateFeedFeature { struct CreateFeedFeature {
@@ -13,52 +9,33 @@ struct CreateFeedFeature {
struct State: Equatable { struct State: Equatable {
var content: String = "" var content: String = ""
var processedImages: [UIImage] = [] var processedImages: [UIImage] = []
var isLoading: Bool = false
var errorMessage: String? = nil var errorMessage: String? = nil
var characterCount: Int = 0 var characterCount: Int = 0
// iOS 16+ PhotosPicker
#if canImport(PhotosUI) && swift(>=5.7)
var selectedImages: [PhotosPickerItem] = [] var selectedImages: [PhotosPickerItem] = []
#endif
// iOS 15 UIImagePickerController
var showingImagePicker: Bool = false
var canAddMoreImages: Bool { var canAddMoreImages: Bool {
processedImages.count < 9 processedImages.count < 9
} }
var canPublish: Bool { var canPublish: Bool {
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
} }
var isLoading: Bool = false
} }
enum Action { enum Action {
case contentChanged(String) case contentChanged(String)
case publishButtonTapped case publishButtonTapped
case publishResponse(Result<PublishDynamicResponse, Error>) case publishResponse(Result<PublishDynamicResponse, Error>)
case clearError case clearError
case dismissView case dismissView
// iOS 16+ PhotosPicker Actions
#if canImport(PhotosUI) && swift(>=5.7)
case photosPickerItemsChanged([PhotosPickerItem]) case photosPickerItemsChanged([PhotosPickerItem])
case processPhotosPickerItems([PhotosPickerItem]) case processPhotosPickerItems([PhotosPickerItem])
#endif
// iOS 15 UIImagePickerController Actions
case showImagePicker
case hideImagePicker
case imageSelected(UIImage)
case removeImage(Int) case removeImage(Int)
case updateProcessedImages([UIImage])
} }
@Dependency(\.apiService) var apiService @Dependency(\.apiService) var apiService
@Dependency(\.dismiss) var dismiss @Dependency(\.dismiss) var dismiss
var body: some ReducerOf<Self> { var body: some ReducerOf<Self> {
Reduce { state, action in Reduce { state, action in
switch action { switch action {
@@ -66,72 +43,47 @@ struct CreateFeedFeature {
state.content = newContent state.content = newContent
state.characterCount = newContent.count state.characterCount = newContent.count
return .none return .none
#if canImport(PhotosUI) && swift(>=5.7)
case .photosPickerItemsChanged(let items): case .photosPickerItemsChanged(let items):
state.selectedImages = items state.selectedImages = items
return .run { send in return .run { send in
await send(.processPhotosPickerItems(items)) await send(.processPhotosPickerItems(items))
} }
case .processPhotosPickerItems(let items): case .processPhotosPickerItems(let items):
return .run { [currentImages = state.processedImages] send in let currentImages = state.processedImages
return .run { send in
var newImages = currentImages var newImages = currentImages
for item in items { for item in items {
if let data = try? await item.loadTransferable(type: Data.self), guard let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) { let image = UIImage(data: data) else { continue }
if newImages.count < 9 { if newImages.count < 9 {
newImages.append(image) newImages.append(image)
}
} }
} }
await MainActor.run { await MainActor.run {
state.processedImages = newImages send(.updateProcessedImages(newImages))
} }
} }
#endif case .updateProcessedImages(let images):
state.processedImages = images
case .showImagePicker:
state.showingImagePicker = true
return .none 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): case .removeImage(let index):
guard index < state.processedImages.count else { return .none } guard index < state.processedImages.count else { return .none }
state.processedImages.remove(at: index) state.processedImages.remove(at: index)
#if canImport(PhotosUI) && swift(>=5.7)
if index < state.selectedImages.count { if index < state.selectedImages.count {
state.selectedImages.remove(at: index) state.selectedImages.remove(at: index)
} }
#endif
return .none return .none
case .publishButtonTapped: case .publishButtonTapped:
guard state.canPublish else { guard state.canPublish else {
state.errorMessage = "请输入内容" state.errorMessage = "请输入内容"
return .none return .none
} }
state.isLoading = true state.isLoading = true
state.errorMessage = nil state.errorMessage = nil
let request = PublishDynamicRequest( let request = PublishDynamicRequest(
content: state.content.trimmingCharacters(in: .whitespacesAndNewlines), content: state.content.trimmingCharacters(in: .whitespacesAndNewlines),
images: state.processedImages images: state.processedImages
) )
return .run { send in return .run { send in
do { do {
let response = try await apiService.request(request) let response = try await apiService.request(request)
@@ -140,27 +92,21 @@ struct CreateFeedFeature {
await send(.publishResponse(.failure(error))) await send(.publishResponse(.failure(error)))
} }
} }
case .publishResponse(.success(let response)): case .publishResponse(.success(let response)):
state.isLoading = false state.isLoading = false
if response.code == 200 { if response.code == 200 {
//
return .send(.dismissView) return .send(.dismissView)
} else { } else {
state.errorMessage = response.message.isEmpty ? "发布失败" : response.message state.errorMessage = response.message.isEmpty ? "发布失败" : response.message
return .none return .none
} }
case .publishResponse(.failure(let error)): case .publishResponse(.failure(let error)):
state.isLoading = false state.isLoading = false
state.errorMessage = error.localizedDescription state.errorMessage = error.localizedDescription
return .none return .none
case .clearError: case .clearError:
state.errorMessage = nil state.errorMessage = nil
return .none return .none
case .dismissView: case .dismissView:
return .run { _ in return .run { _ in
await dismiss() 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: - // MARK: -
///
struct PublishDynamicRequest: APIRequestProtocol { struct PublishDynamicRequest: APIRequestProtocol {
typealias Response = PublishDynamicResponse typealias Response = PublishDynamicResponse
let endpoint: String = "/dynamic/square/publish"
let endpoint: String = "/dynamic/square/publish" //
let method: HTTPMethod = .POST let method: HTTPMethod = .POST
let includeBaseParameters: Bool = true let includeBaseParameters: Bool = true
let queryParameters: [String: String]? = nil let queryParameters: [String: String]? = nil
let timeout: TimeInterval = 30.0 let timeout: TimeInterval = 30.0
let content: String let content: String
let images: [UIImage] let images: [UIImage]
let type: Int // 0: , 2: let type: Int // 0: , 2:
init(content: String, images: [UIImage] = []) { init(content: String, images: [UIImage] = []) {
self.content = content self.content = content
self.images = images self.images = images
self.type = images.isEmpty ? 0 : 2 self.type = images.isEmpty ? 0 : 2
} }
var bodyParameters: [String: Any]? { var bodyParameters: [String: Any]? {
var params: [String: Any] = [ var params: [String: Any] = [
"content": content, "content": content,
"type": type "type": type
] ]
// base64
if !images.isEmpty { if !images.isEmpty {
let imageData = images.compactMap { image in let imageData = images.compactMap { image in
image.jpegData(compressionQuality: 0.8)?.base64EncodedString() image.jpegData(compressionQuality: 0.8)?.base64EncodedString()
} }
params["images"] = imageData params["images"] = imageData
} }
return params return params
} }
} }
///
struct PublishDynamicResponse: Codable { struct PublishDynamicResponse: Codable {
let code: Int let code: Int
let message: String let message: String

View File

@@ -48,12 +48,12 @@ struct EMailLoginFeature {
case .getVerificationCodeTapped: case .getVerificationCodeTapped:
guard !state.email.isEmpty else { guard !state.email.isEmpty else {
state.errorMessage = "email_login.email_required".localized state.errorMessage = NSLocalizedString("email_login.email_required", comment: "")
return .none return .none
} }
guard ValidationHelper.isValidEmail(state.email) else { guard ValidationHelper.isValidEmail(state.email) else {
state.errorMessage = "email_login.invalid_email".localized state.errorMessage = NSLocalizedString("email_login.invalid_email", comment: "")
return .none return .none
} }
@@ -98,12 +98,12 @@ struct EMailLoginFeature {
case .loginButtonTapped(let email, let verificationCode): case .loginButtonTapped(let email, let verificationCode):
guard !email.isEmpty && !verificationCode.isEmpty else { guard !email.isEmpty && !verificationCode.isEmpty else {
state.errorMessage = "email_login.fields_required".localized state.errorMessage = NSLocalizedString("email_login.fields_required", comment: "")
return .none return .none
} }
guard ValidationHelper.isValidEmail(email) else { guard ValidationHelper.isValidEmail(email) else {
state.errorMessage = "email_login.invalid_email".localized state.errorMessage = NSLocalizedString("email_login.invalid_email", comment: "")
return .none return .none
} }
@@ -112,7 +112,7 @@ struct EMailLoginFeature {
return .run { send in return .run { send in
do { 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))) await send(.loginResponse(.failure(APIError.encryptionFailed)))
return return
} }
@@ -149,14 +149,11 @@ struct EMailLoginFeature {
case .loginResponse(.success(let accountModel)): case .loginResponse(.success(let accountModel)):
state.isLoading = false state.isLoading = false
// Effect AccountModel
// AccountModel return .run { _ in
UserInfoManager.saveAccountModel(accountModel) await UserInfoManager.saveAccountModel(accountModel)
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
// }
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
return .none
case .loginResponse(.failure(let error)): case .loginResponse(.failure(let error)):
state.isLoading = false state.isLoading = false

View File

@@ -16,9 +16,10 @@ struct FeedFeature {
// CreateFeedView // CreateFeedView
var isShowingCreateFeed = false var isShowingCreateFeed = false
var createFeedState: CreateFeedFeature.State? = nil
} }
enum Action: Equatable { enum Action {
case onAppear case onAppear
case loadLatestMoments case loadLatestMoments
case loadMoreMoments case loadMoreMoments
@@ -30,6 +31,7 @@ struct FeedFeature {
case showCreateFeed case showCreateFeed
case dismissCreateFeed case dismissCreateFeed
case createFeedCompleted case createFeedCompleted
indirect case createFeed(CreateFeedFeature.Action)
} }
@Dependency(\.apiService) var apiService @Dependency(\.apiService) var apiService
@@ -38,6 +40,10 @@ struct FeedFeature {
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case .onAppear: case .onAppear:
#if DEBUG
return .none
#endif
// //
guard !state.isInitialized else { return .none } guard !state.isInitialized else { return .none }
state.isInitialized = true state.isInitialized = true
@@ -83,49 +89,49 @@ struct FeedFeature {
state.isLoading = false state.isLoading = false
// //
debugInfo("📱 FeedFeature: API 响应成功") debugInfoSync("📱 FeedFeature: API 响应成功")
debugInfo("📱 FeedFeature: response.code = \(response.code)") debugInfoSync("📱 FeedFeature: response.code = \(response.code)")
debugInfo("📱 FeedFeature: response.message = \(response.message)") debugInfoSync("📱 FeedFeature: response.message = \(response.message)")
debugInfo("📱 FeedFeature: response.data = \(response.data != nil ? "有数据" : "无数据")") debugInfoSync("📱 FeedFeature: response.data = \(response.data != nil ? "有数据" : "无数据")")
// //
guard response.code == 200, let data = response.data else { guard response.code == 200, let data = response.data else {
let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message
state.error = errorMsg state.error = errorMsg
debugError("❌ FeedFeature: API 响应失败 - code: \(response.code), message: \(errorMsg)") debugErrorSync("❌ FeedFeature: API 响应失败 - code: \(response.code), message: \(errorMsg)")
return .none return .none
} }
// //
debugInfo("📱 FeedFeature: data.dynamicList.count = \(data.dynamicList.count)") debugInfoSync("📱 FeedFeature: data.dynamicList.count = \(data.dynamicList.count)")
debugInfo("📱 FeedFeature: data.nextDynamicId = \(data.nextDynamicId)") debugInfoSync("📱 FeedFeature: data.nextDynamicId = \(data.nextDynamicId)")
// //
let isRefresh = state.nextDynamicId == 0 let isRefresh = state.nextDynamicId == 0
debugInfo("📱 FeedFeature: isRefresh = \(isRefresh)") debugInfoSync("📱 FeedFeature: isRefresh = \(isRefresh)")
if isRefresh { if isRefresh {
// //
state.moments = data.dynamicList state.moments = data.dynamicList
debugInfo(" FeedFeature: 刷新数据moments.count = \(state.moments.count)") debugInfoSync(" FeedFeature: 刷新数据moments.count = \(state.moments.count)")
} else { } else {
// //
let oldCount = state.moments.count let oldCount = state.moments.count
state.moments.append(contentsOf: data.dynamicList) 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.nextDynamicId = data.nextDynamicId
state.hasMoreData = !data.dynamicList.isEmpty state.hasMoreData = !data.dynamicList.isEmpty
debugInfo("📱 FeedFeature: 更新完成 - nextDynamicId: \(state.nextDynamicId), hasMoreData: \(state.hasMoreData)") debugInfoSync("📱 FeedFeature: 更新完成 - nextDynamicId: \(state.nextDynamicId), hasMoreData: \(state.hasMoreData)")
return .none return .none
case let .momentsResponse(.failure(error)): case let .momentsResponse(.failure(error)):
state.isLoading = false state.isLoading = false
state.error = error.localizedDescription state.error = error.localizedDescription
debugError("❌ FeedFeature: API 请求失败 - \(error.localizedDescription)") debugErrorSync("❌ FeedFeature: API 请求失败 - \(error.localizedDescription)")
return .none return .none
case .clearError: case .clearError:
@@ -142,17 +148,28 @@ struct FeedFeature {
case .showCreateFeed: case .showCreateFeed:
state.isShowingCreateFeed = true state.isShowingCreateFeed = true
// createFeedState
state.createFeedState = CreateFeedFeature.State()
return .none return .none
case .dismissCreateFeed: case .dismissCreateFeed:
state.isShowingCreateFeed = false state.isShowingCreateFeed = false
state.createFeedState = nil
return .none return .none
case .createFeedCompleted: case .createFeedCompleted:
state.isShowingCreateFeed = false state.isShowingCreateFeed = false
state.createFeedState = nil
// //
return .send(.loadLatestMoments) return .send(.loadLatestMoments)
case .createFeed:
// Action reducer
return .none
} }
} }
// reducer
ifLet(\State.createFeedState, action: /Action.createFeed) {
CreateFeedFeature()
}
} }
} }

View File

@@ -18,7 +18,7 @@ struct HomeFeature {
var feedState = FeedFeature.State() var feedState = FeedFeature.State()
} }
enum Action: Equatable { enum Action {
case onAppear case onAppear
case loadUserInfo case loadUserInfo
case userInfoLoaded(UserInfo?) case userInfoLoaded(UserInfo?)
@@ -56,8 +56,10 @@ struct HomeFeature {
case .loadUserInfo: case .loadUserInfo:
// //
let userInfo = UserInfoManager.getUserInfo() return .run { send in
return .send(.userInfoLoaded(userInfo)) let userInfo = await UserInfoManager.getUserInfo()
await send(.userInfoLoaded(userInfo))
}
case let .userInfoLoaded(userInfo): case let .userInfoLoaded(userInfo):
state.userInfo = userInfo state.userInfo = userInfo
@@ -65,8 +67,10 @@ struct HomeFeature {
case .loadAccountModel: case .loadAccountModel:
// //
let accountModel = UserInfoManager.getAccountModel() return .run { send in
return .send(.accountModelLoaded(accountModel)) let accountModel = await UserInfoManager.getAccountModel()
await send(.accountModelLoaded(accountModel))
}
case let .accountModelLoaded(accountModel): case let .accountModelLoaded(accountModel):
state.accountModel = accountModel state.accountModel = accountModel
@@ -76,12 +80,11 @@ struct HomeFeature {
return .send(.logout) return .send(.logout)
case .logout: case .logout:
// //
UserInfoManager.clearAllAuthenticationData() return .run { _ in
await UserInfoManager.clearAllAuthenticationData()
// NotificationCenter.default.post(name: .homeLogout, object: nil)
NotificationCenter.default.post(name: .homeLogout, object: nil) }
return .none
case .settingDismissed: case .settingDismissed:
state.isSettingPresented = false state.isSettingPresented = false

View File

@@ -56,7 +56,6 @@ struct IDLoginFeature {
case .togglePasswordVisibility: case .togglePasswordVisibility:
state.isPasswordVisible.toggle() state.isPasswordVisible.toggle()
return .none return .none
case let .loginButtonTapped(userID, password): case let .loginButtonTapped(userID, password):
state.userID = userID state.userID = userID
state.password = password state.password = password
@@ -64,17 +63,13 @@ struct IDLoginFeature {
state.errorMessage = nil state.errorMessage = nil
state.ticketError = nil state.ticketError = nil
state.loginStep = .authenticating state.loginStep = .authenticating
// API Effect
// IDAPI
return .run { send in return .run { send in
do { do {
// 使LoginHelper guard let loginRequest = await LoginHelper.createIDLoginRequest(userID: userID, password: password) else {
guard let loginRequest = LoginHelper.createIDLoginRequest(userID: userID, password: password) else {
await send(.loginResponse(.failure(APIError.decodingError("加密失败")))) await send(.loginResponse(.failure(APIError.decodingError("加密失败"))))
return return
} }
//
let response = try await apiService.request(loginRequest) let response = try await apiService.request(loginRequest)
await send(.loginResponse(.success(response))) await send(.loginResponse(.success(response)))
} catch { } catch {
@@ -85,35 +80,21 @@ struct IDLoginFeature {
} }
} }
} }
case .forgotPasswordTapped: case .forgotPasswordTapped:
// TODO:
return .none return .none
case .backButtonTapped: case .backButtonTapped:
//
return .none return .none
case let .loginResponse(.success(response)): case let .loginResponse(.success(response)):
state.isLoading = false state.isLoading = false
if response.isSuccess { if response.isSuccess {
// OAuth
state.errorMessage = nil state.errorMessage = nil
// AccountModel
if let loginData = response.data, if let loginData = response.data,
let accountModel = AccountModel.from(loginData: loginData) { let accountModel = AccountModel.from(loginData: loginData) {
state.accountModel = accountModel state.accountModel = accountModel
// Effect userInfo
//
if let userInfo = loginData.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 // ticket
return .send(.requestTicket(accessToken: accountModel.accessToken!)) return .send(.requestTicket(accessToken: accountModel.accessToken!))
} else { } else {
@@ -125,90 +106,77 @@ struct IDLoginFeature {
state.loginStep = .failed state.loginStep = .failed
} }
return .none return .none
case let .loginResponse(.failure(error)): case let .loginResponse(.failure(error)):
state.isLoading = false state.isLoading = false
state.errorMessage = error.localizedDescription state.errorMessage = error.localizedDescription
state.loginStep = .failed state.loginStep = .failed
return .none return .none
case let .requestTicket(accessToken): case let .requestTicket(accessToken):
state.isTicketLoading = true state.isTicketLoading = true
state.ticketError = nil state.ticketError = nil
state.loginStep = .gettingTicket 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 { do {
// AccountModel uid Int
let uid = accountModel?.uid != nil ? Int(accountModel!.uid!) : nil
let ticketRequest = TicketHelper.createTicketRequest(accessToken: accessToken, uid: uid) let ticketRequest = TicketHelper.createTicketRequest(accessToken: accessToken, uid: uid)
let response = try await apiService.request(ticketRequest) let response = try await apiService.request(ticketRequest)
await send(.ticketResponse(.success(response))) await send(.ticketResponse(.success(response)))
} catch { } catch {
debugError("❌ ID登录 Ticket 获取失败: \(error)") debugErrorSync("❌ ID登录 Ticket 获取失败: \(error)")
await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription)))) await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription))))
} }
} }
case let .ticketResponse(.success(response)): case let .ticketResponse(.success(response)):
state.isTicketLoading = false state.isTicketLoading = false
if response.isSuccess { if response.isSuccess {
state.ticketError = nil state.ticketError = nil
state.loginStep = .completed state.loginStep = .completed
debugInfoSync("✅ ID 登录完整流程成功")
debugInfo("✅ ID 登录完整流程成功") debugInfoSync("🎫 Ticket 获取成功: \(response.ticket ?? "nil")")
debugInfo("🎫 Ticket 获取成功: \(response.ticket ?? "nil")") // --- Effect state/accountModel ---
if let ticket = response.ticket, let oldAccountModel = state.accountModel {
// AccountModel ticket // withTicket struct newAccountModel
if let ticket = response.ticket { let newAccountModel = oldAccountModel.withTicket(ticket)
if var accountModel = state.accountModel { state.accountModel = newAccountModel
accountModel.ticket = ticket // newAccountModel state
state.accountModel = accountModel return .run { _ in
// state/accountModel Swift
// AccountModel await UserInfoManager.saveAccountModel(newAccountModel)
UserInfoManager.saveAccountModel(accountModel)
// Ticket
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
} else {
debugError("❌ AccountModel 不存在,无法保存 ticket")
state.ticketError = "内部错误:账户信息丢失"
state.loginStep = .failed
} }
} else { } else if response.ticket == nil {
state.ticketError = "Ticket 为空" state.ticketError = "Ticket 为空"
state.loginStep = .failed state.loginStep = .failed
} else {
debugErrorSync("❌ AccountModel 不存在,无法保存 ticket")
state.ticketError = "内部错误:账户信息丢失"
state.loginStep = .failed
} }
} else { } else {
state.ticketError = response.errorMessage state.ticketError = response.errorMessage
state.loginStep = .failed state.loginStep = .failed
} }
return .none return .none
case let .ticketResponse(.failure(error)): case let .ticketResponse(.failure(error)):
state.isTicketLoading = false state.isTicketLoading = false
state.ticketError = error.localizedDescription state.ticketError = error.localizedDescription
state.loginStep = .failed state.loginStep = .failed
debugError("❌ ID 登录 Ticket 获取失败: \(error.localizedDescription)") debugErrorSync("❌ ID 登录 Ticket 获取失败: \(error.localizedDescription)")
return .none return .none
case .clearTicketError: case .clearTicketError:
state.ticketError = nil state.ticketError = nil
return .none return .none
case .resetLogin: case .resetLogin:
state.isLoading = false state.isLoading = false
state.isTicketLoading = false state.isTicketLoading = false
state.errorMessage = nil state.errorMessage = nil
state.ticketError = nil state.ticketError = nil
state.accountModel = nil // AccountModel state.accountModel = nil
state.loginStep = .initial state.loginStep = .initial
// Effect
// return .run { _ in await UserInfoManager.clearAllAuthenticationData() }
UserInfoManager.clearAllAuthenticationData()
return .none
} }
} }
} }

View File

@@ -81,7 +81,7 @@ struct LoginFeature {
return .run { [account = state.account, password = state.password] send in return .run { [account = state.account, password = state.password] send in
do { do {
// 使LoginHelper // 使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("加密失败")))) await send(.loginResponse(.failure(APIError.decodingError("加密失败"))))
return return
} }
@@ -108,10 +108,9 @@ struct LoginFeature {
if let loginData = response.data, if let loginData = response.data,
let accountModel = AccountModel.from(loginData: loginData) { let accountModel = AccountModel.from(loginData: loginData) {
state.accountModel = accountModel state.accountModel = accountModel
debugInfoSync("✅ OAuth 认证成功")
debugInfo("✅ OAuth 认证成功") debugInfoSync("🔑 Access Token: \(accountModel.accessToken ?? "nil")")
debugInfo("🔑 Access Token: \(accountModel.accessToken ?? "nil")") debugInfoSync("🆔 用户 UID: \(accountModel.uid ?? "nil")")
debugInfo("🆔 用户 UID: \(accountModel.uid ?? "nil")")
// ticket // ticket
return .send(.requestTicket(accessToken: accountModel.accessToken!)) return .send(.requestTicket(accessToken: accountModel.accessToken!))
@@ -144,7 +143,7 @@ struct LoginFeature {
let response = try await apiService.request(ticketRequest) let response = try await apiService.request(ticketRequest)
await send(.ticketResponse(.success(response))) await send(.ticketResponse(.success(response)))
} catch { } catch {
debugError("❌ Ticket 获取失败: \(error)") debugErrorSync("❌ Ticket 获取失败: \(error)")
await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription)))) await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription))))
} }
} }
@@ -155,22 +154,21 @@ struct LoginFeature {
state.ticketError = nil state.ticketError = nil
state.loginStep = .completed state.loginStep = .completed
debugInfo("✅ 完整登录流程成功") debugInfoSync("✅ 完整登录流程成功")
debugInfo("🎫 Ticket 获取成功: \(response.ticket ?? "nil")") debugInfoSync("🎫 Ticket 获取成功: \(response.ticket ?? "nil")")
// AccountModel ticket // AccountModel ticket
if let ticket = response.ticket { if let ticket = response.ticket {
if var accountModel = state.accountModel { if let oldAccountModel = state.accountModel {
accountModel.ticket = ticket let newAccountModel = oldAccountModel.withTicket(ticket)
state.accountModel = accountModel state.accountModel = newAccountModel
// Effect AccountModel
// AccountModel return .run { _ in
UserInfoManager.saveAccountModel(accountModel) await UserInfoManager.saveAccountModel(newAccountModel)
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
// Ticket }
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
} else { } else {
debugError("❌ AccountModel 不存在,无法保存 ticket") debugErrorSync("❌ AccountModel 不存在,无法保存 ticket")
state.ticketError = "内部错误:账户信息丢失" state.ticketError = "内部错误:账户信息丢失"
state.loginStep = .failed state.loginStep = .failed
} }
@@ -189,7 +187,7 @@ struct LoginFeature {
state.isTicketLoading = false state.isTicketLoading = false
state.ticketError = error.localizedDescription state.ticketError = error.localizedDescription
state.loginStep = .failed state.loginStep = .failed
debugError("❌ Ticket 获取失败: \(error.localizedDescription)") debugErrorSync("❌ Ticket 获取失败: \(error.localizedDescription)")
return .none return .none
case .clearTicketError: case .clearTicketError:
@@ -203,11 +201,8 @@ struct LoginFeature {
state.ticketError = nil state.ticketError = nil
state.accountModel = nil // AccountModel state.accountModel = nil // AccountModel
state.loginStep = .initial state.loginStep = .initial
// Effect
// return .run { _ in await UserInfoManager.clearAllAuthenticationData() }
UserInfoManager.clearAllAuthenticationData()
return .none
case .idLogin: case .idLogin:
// IDLoginfeature // IDLoginfeature

View File

@@ -57,12 +57,12 @@ struct RecoverPasswordFeature {
case .getVerificationCodeTapped: case .getVerificationCodeTapped:
guard !state.email.isEmpty else { guard !state.email.isEmpty else {
state.errorMessage = "recover_password.email_required".localized state.errorMessage = NSLocalizedString("recover_password.email_required", comment: "")
return .none return .none
} }
guard ValidationHelper.isValidEmail(state.email) else { guard ValidationHelper.isValidEmail(state.email) else {
state.errorMessage = "recover_password.invalid_email".localized state.errorMessage = NSLocalizedString("recover_password.invalid_email", comment: "")
return .none return .none
} }
@@ -101,23 +101,23 @@ struct RecoverPasswordFeature {
if let apiError = error as? APIError { if let apiError = error as? APIError {
state.errorMessage = apiError.localizedDescription state.errorMessage = apiError.localizedDescription
} else { } else {
state.errorMessage = "recover_password.code_send_failed".localized state.errorMessage = NSLocalizedString("recover_password.code_send_failed", comment: "")
} }
return .none return .none
case .resetPasswordTapped: case .resetPasswordTapped:
guard !state.email.isEmpty && !state.verificationCode.isEmpty && !state.newPassword.isEmpty else { 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 return .none
} }
guard ValidationHelper.isValidEmail(state.email) else { guard ValidationHelper.isValidEmail(state.email) else {
state.errorMessage = "recover_password.invalid_email".localized state.errorMessage = NSLocalizedString("recover_password.invalid_email", comment: "")
return .none return .none
} }
guard ValidationHelper.isValidPassword(state.newPassword) else { guard ValidationHelper.isValidPassword(state.newPassword) else {
state.errorMessage = "recover_password.invalid_password".localized state.errorMessage = NSLocalizedString("recover_password.invalid_password", comment: "")
return .none return .none
} }
@@ -160,7 +160,7 @@ struct RecoverPasswordFeature {
if let apiError = error as? APIError { if let apiError = error as? APIError {
state.errorMessage = apiError.localizedDescription state.errorMessage = apiError.localizedDescription
} else { } else {
state.errorMessage = "recover_password.reset_failed".localized state.errorMessage = NSLocalizedString("recover_password.reset_failed", comment: "")
} }
return .none return .none
@@ -199,7 +199,7 @@ struct ResetPasswordResponse: Codable, Equatable {
/// ///
var errorMessage: String { 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 method: HTTPMethod = .POST
let includeBaseParameters = true let includeBaseParameters = true
let queryParameters: [String: String]? let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0 let timeout: TimeInterval = 30.0
/// ///
@@ -238,13 +238,13 @@ struct RecoverPasswordHelper {
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26" let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else { guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else {
debugError("❌ 邮箱DES加密失败") debugErrorSync("❌ 邮箱DES加密失败")
return nil return nil
} }
debugInfo("🔐 密码恢复邮箱DES加密成功") debugInfoSync("🔐 密码恢复邮箱DES加密成功")
debugInfo(" 原始邮箱: \(email)") debugInfoSync(" 原始邮箱: \(email)")
debugInfo(" 加密邮箱: \(encryptedEmail)") debugInfoSync(" 加密邮箱: \(encryptedEmail)")
// 使type=3 // 使type=3
return EmailGetCodeRequest(emailAddress: email, type: 3) return EmailGetCodeRequest(emailAddress: email, type: 3)
@@ -261,16 +261,16 @@ struct RecoverPasswordHelper {
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey), guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey),
let encryptedPassword = DESEncrypt.encryptUseDES(newPassword, key: encryptionKey) else { let encryptedPassword = DESEncrypt.encryptUseDES(newPassword, key: encryptionKey) else {
debugError("❌ 密码重置DES加密失败") debugErrorSync("❌ 密码重置DES加密失败")
return nil return nil
} }
debugInfo("🔐 密码重置DES加密成功") debugInfoSync("🔐 密码重置DES加密成功")
debugInfo(" 原始邮箱: \(email)") debugInfoSync(" 原始邮箱: \(email)")
debugInfo(" 加密邮箱: \(encryptedEmail)") debugInfoSync(" 加密邮箱: \(encryptedEmail)")
debugInfo(" 验证码: \(code)") debugInfoSync(" 验证码: \(code)")
debugInfo(" 原始新密码: \(newPassword)") debugInfoSync(" 原始新密码: \(newPassword)")
debugInfo(" 加密新密码: \(encryptedPassword)") debugInfoSync(" 加密新密码: \(encryptedPassword)")
return ResetPasswordRequest( return ResetPasswordRequest(
email: email, email: email,

View File

@@ -32,16 +32,20 @@ struct SettingFeature {
) )
case .loadUserInfo: case .loadUserInfo:
let userInfo = UserInfoManager.getUserInfo() return .run { send in
return .send(.userInfoLoaded(userInfo)) let userInfo = await UserInfoManager.getUserInfo()
await send(.userInfoLoaded(userInfo))
}
case let .userInfoLoaded(userInfo): case let .userInfoLoaded(userInfo):
state.userInfo = userInfo state.userInfo = userInfo
return .none return .none
case .loadAccountModel: case .loadAccountModel:
let accountModel = UserInfoManager.getAccountModel() return .run { send in
return .send(.accountModelLoaded(accountModel)) let accountModel = await UserInfoManager.getAccountModel()
await send(.accountModelLoaded(accountModel))
}
case let .accountModelLoaded(accountModel): case let .accountModelLoaded(accountModel):
state.accountModel = accountModel state.accountModel = accountModel
@@ -52,18 +56,15 @@ struct SettingFeature {
case .logout: case .logout:
state.isLoading = true state.isLoading = true
return .run { _ in
// await UserInfoManager.clearAllAuthenticationData()
UserInfoManager.clearAllAuthenticationData() NotificationCenter.default.post(name: .homeLogout, object: nil)
}
//
NotificationCenter.default.post(name: .homeLogout, object: nil)
return .none
case .dismissTapped: case .dismissTapped:
// return .run { _ in
NotificationCenter.default.post(name: .settingsDismiss, object: nil) NotificationCenter.default.post(name: .settingsDismiss, object: nil)
return .none }
} }
} }
} }

View File

@@ -32,7 +32,6 @@ struct SplashFeature {
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 = 1,000,000,000 try await Task.sleep(nanoseconds: 1_000_000_000) // 1 = 1,000,000,000
await send(.splashFinished) await send(.splashFinished)
} }
case .splashFinished: case .splashFinished:
state.isLoading = false state.isLoading = false
state.shouldShowMainApp = true state.shouldShowMainApp = true
@@ -45,20 +44,25 @@ struct SplashFeature {
// //
return .run { send in return .run { send in
let authStatus = UserInfoManager.checkAuthenticationStatus() let authStatus = await UserInfoManager.checkAuthenticationStatus()
await send(.authenticationChecked(authStatus)) await send(.authenticationChecked(authStatus))
} }
case let .authenticationChecked(status): case let .authenticationChecked(status):
#if DEBUG
debugInfoSync("🔑 需要手动登录")
NotificationCenter.default.post(name: .autoLoginFailed, object: nil)
return .none
#endif
state.isCheckingAuthentication = false state.isCheckingAuthentication = false
state.authenticationStatus = status state.authenticationStatus = status
// //
if status.canAutoLogin { if status.canAutoLogin {
debugInfo("🎉 自动登录成功,进入主页") debugInfoSync("🎉 自动登录成功,进入主页")
NotificationCenter.default.post(name: .autoLoginSuccess, object: nil) NotificationCenter.default.post(name: .autoLoginSuccess, object: nil)
} else { } else {
debugInfo("🔑 需要手动登录") debugInfoSync("🔑 需要手动登录")
NotificationCenter.default.post(name: .autoLoginFailed, object: nil) NotificationCenter.default.post(name: .autoLoginFailed, object: nil)
} }
@@ -66,4 +70,4 @@ struct SplashFeature {
} }
} }
} }
} }

View File

@@ -9,6 +9,7 @@ public enum LogLevel: Int {
case error case error
} }
@MainActor
public class LogManager { public class LogManager {
/// ///
public static let shared = LogManager() public static let shared = LogManager()
@@ -45,43 +46,99 @@ public class LogManager {
} }
// MARK: - // MARK: -
@MainActor
public func logVerbose(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { public func logVerbose(_ message: @autoclosure () -> String, onlyRelease: Bool = false) {
LogManager.shared.log(.verbose, message(), onlyRelease: onlyRelease) LogManager.shared.log(.verbose, message(), onlyRelease: onlyRelease)
} }
@MainActor
public func logDebug(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { public func logDebug(_ message: @autoclosure () -> String, onlyRelease: Bool = false) {
LogManager.shared.log(.debug, message(), onlyRelease: onlyRelease) LogManager.shared.log(.debug, message(), onlyRelease: onlyRelease)
} }
@MainActor
public func logInfo(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { public func logInfo(_ message: @autoclosure () -> String, onlyRelease: Bool = false) {
LogManager.shared.log(.info, message(), onlyRelease: onlyRelease) LogManager.shared.log(.info, message(), onlyRelease: onlyRelease)
} }
@MainActor
public func logWarn(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { public func logWarn(_ message: @autoclosure () -> String, onlyRelease: Bool = false) {
LogManager.shared.log(.warn, message(), onlyRelease: onlyRelease) LogManager.shared.log(.warn, message(), onlyRelease: onlyRelease)
} }
@MainActor
public func logError(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { public func logError(_ message: @autoclosure () -> String, onlyRelease: Bool = false) {
LogManager.shared.log(.error, message(), onlyRelease: onlyRelease) LogManager.shared.log(.error, message(), onlyRelease: onlyRelease)
} }
// MARK: - DEBUG使 // MARK: - DEBUG使
public func debugVerbose(_ message: @autoclosure () -> String) { public func debugVerbose(_ message: @autoclosure () -> String) async {
LogManager.shared.debugLog(.verbose, message()) let msg = message()
await MainActor.run {
LogManager.shared.debugLog(.verbose, msg)
}
} }
public func debugLog(_ message: @autoclosure () -> String) { public func debugLog(_ message: @autoclosure () -> String) async {
LogManager.shared.debugLog(.debug, message()) let msg = message()
await MainActor.run {
LogManager.shared.debugLog(.debug, msg)
}
} }
public func debugInfo(_ message: @autoclosure () -> String) { public func debugInfo(_ message: @autoclosure () -> String) async {
LogManager.shared.debugLog(.info, message()) let msg = message()
await MainActor.run {
LogManager.shared.debugLog(.info, msg)
}
} }
public func debugWarn(_ message: @autoclosure () -> String) { public func debugWarn(_ message: @autoclosure () -> String) async {
LogManager.shared.debugLog(.warn, message()) let msg = message()
await MainActor.run {
LogManager.shared.debugLog(.warn, msg)
}
} }
public func debugError(_ message: @autoclosure () -> String) { public func debugError(_ message: @autoclosure () -> String) async {
LogManager.shared.debugLog(.error, message()) 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)
}
}

View File

@@ -18,25 +18,25 @@ struct APILoadingEffectView: View {
if let firstItem = getFirstDisplayItem() { if let firstItem = getFirstDisplayItem() {
SingleLoadingView(item: firstItem) SingleLoadingView(item: firstItem)
.onAppear { .onAppear {
debugInfo("🔍 Loading item appeared: \(firstItem.id)") debugInfoSync("🔍 Loading item appeared: \(firstItem.id)")
} }
.onDisappear { .onDisappear {
debugInfo("🔍 Loading item disappeared: \(firstItem.id)") debugInfoSync("🔍 Loading item disappeared: \(firstItem.id)")
} }
} }
} }
.allowsHitTesting(false) // .allowsHitTesting(false) //
.ignoresSafeArea(.all) // .ignoresSafeArea(.all) //
.onReceive(loadingManager.$loadingItems) { items in .onReceive(loadingManager.$loadingItems) { items in
debugInfo("🔍 Loading items updated: \(items.count) items") debugInfoSync("🔍 Loading items updated: \(items.count) items")
} }
} }
/// ///
private func getFirstDisplayItem() -> APILoadingItem? { private func getFirstDisplayItem() -> APILoadingItem? {
guard Thread.isMainThread else { guard Thread.isMainThread else {
debugWarn("⚠️ getFirstDisplayItem called from background thread") debugWarnSync("⚠️ getFirstDisplayItem called from background thread")
return nil return nil
} }
return loadingManager.loadingItems.first { $0.shouldDisplay } return loadingManager.loadingItems.first { $0.shouldDisplay }
@@ -151,7 +151,7 @@ struct APILoadingEffectView_Previews: PreviewProvider {
.font(.title) .font(.title)
Button("测试按钮") { Button("测试按钮") {
debugInfo("按钮被点击了!") debugInfoSync("按钮被点击了!")
} }
.padding() .padding()
.background(Color.blue) .background(Color.blue)
@@ -169,12 +169,12 @@ struct APILoadingEffectView_Previews: PreviewProvider {
let manager = APILoadingManager.shared let manager = APILoadingManager.shared
// loading // loading
let id1 = await manager.startLoading() let id1 = manager.startLoading()
// 2 // 2
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
Task { Task {
await manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置") manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置")
} }
} }
} }
@@ -197,13 +197,13 @@ private struct PreviewStateModifier: ViewModifier {
let manager = APILoadingManager.shared let manager = APILoadingManager.shared
if showLoading { if showLoading {
let _ = await manager.startLoading() let _ = manager.startLoading()
} }
if showError { if showError {
let id = await manager.startLoading() let id = manager.startLoading()
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 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 #endif

View File

@@ -11,24 +11,18 @@ import Combine
/// - loading /// - loading
/// - /// -
/// - 线 /// - 线
@MainActor
class APILoadingManager: ObservableObject { class APILoadingManager: ObservableObject {
// MARK: - Properties // MARK: - Properties
/// ///
static let shared = APILoadingManager() static let shared = APILoadingManager()
/// ///
@Published private(set) var loadingItems: [APILoadingItem] = [] @Published private(set) var loadingItems: [APILoadingItem] = []
/// ///
private var errorCleanupTasks: [UUID: DispatchWorkItem] = [:] private var errorCleanupTasks: [UUID: DispatchWorkItem] = [:]
/// ///
private init() {} private init() {}
// MARK: - Public Methods // MARK: - Public Methods
/// loading /// loading
/// - Parameters: /// - Parameters:
/// - shouldShowLoading: loading /// - shouldShowLoading: loading
@@ -36,127 +30,74 @@ class APILoadingManager: ObservableObject {
/// - Returns: ID /// - Returns: ID
func startLoading(shouldShowLoading: Bool = true, shouldShowError: Bool = true) -> UUID { func startLoading(shouldShowLoading: Bool = true, shouldShowError: Bool = true) -> UUID {
let loadingId = UUID() let loadingId = UUID()
let loadingItem = APILoadingItem( let loadingItem = APILoadingItem(
id: loadingId, id: loadingId,
state: .loading, state: .loading,
shouldShowError: shouldShowError, shouldShowError: shouldShowError,
shouldShowLoading: shouldShowLoading shouldShowLoading: shouldShowLoading
) )
loadingItems.append(loadingItem)
// 🚨 线 @Published
DispatchQueue.main.async { [weak self] in
self?.loadingItems.append(loadingItem)
}
return loadingId return loadingId
} }
/// loading /// loading
/// - Parameter id: ID /// - Parameter id: ID
func finishLoading(_ id: UUID) { func finishLoading(_ id: UUID) {
DispatchQueue.main.async { [weak self] in removeLoading(id)
self?.removeLoading(id)
}
} }
/// loading /// loading
/// - Parameters: /// - Parameters:
/// - id: ID /// - id: ID
/// - errorMessage: /// - errorMessage:
func setError(_ id: UUID, errorMessage: String) { func setError(_ id: UUID, errorMessage: String) {
DispatchQueue.main.async { [weak self] in guard let index = loadingItems.firstIndex(where: { $0.id == id }) else { return }
guard let self = self else { return } let currentItem = loadingItems[index]
if currentItem.shouldShowError {
// let errorItem = APILoadingItem(
if let index = self.loadingItems.firstIndex(where: { $0.id == id }) { id: id,
let currentItem = self.loadingItems[index] state: .error(message: errorMessage),
shouldShowError: true,
// shouldShowLoading: currentItem.shouldShowLoading
if currentItem.shouldShowError { )
let errorItem = APILoadingItem( loadingItems[index] = errorItem
id: id, setupErrorCleanup(for: id)
state: .error(message: errorMessage), } else {
shouldShowError: true, loadingItems.removeAll { $0.id == id }
shouldShowLoading: currentItem.shouldShowLoading
)
self.loadingItems[index] = errorItem
//
self.setupErrorCleanup(for: id)
} else {
//
self.loadingItems.removeAll { $0.id == id }
}
}
} }
} }
/// ///
/// - Parameter id: ID /// - Parameter id: ID
private func removeLoading(_ id: UUID) { private func removeLoading(_ id: UUID) {
cancelErrorCleanup(for: id) cancelErrorCleanup(for: id)
// 🚨 线 @Published loadingItems.removeAll { $0.id == id }
if Thread.isMainThread {
loadingItems.removeAll { $0.id == id }
} else {
DispatchQueue.main.async { [weak self] in
self?.loadingItems.removeAll { $0.id == id }
}
}
} }
/// ///
func clearAll() { func clearAll() {
DispatchQueue.main.async { [weak self] in errorCleanupTasks.values.forEach { $0.cancel() }
guard let self = self else { return } errorCleanupTasks.removeAll()
loadingItems.removeAll()
//
self.errorCleanupTasks.values.forEach { $0.cancel() }
self.errorCleanupTasks.removeAll()
//
self.loadingItems.removeAll()
}
} }
// MARK: - Computed Properties // MARK: - Computed Properties
/// loading /// loading
var hasActiveLoading: Bool { var hasActiveLoading: Bool {
if Thread.isMainThread { loadingItems.contains { $0.state == .loading && $0.shouldDisplay }
return loadingItems.contains { $0.state == .loading && $0.shouldDisplay }
} else {
return false
}
} }
/// ///
var hasActiveError: Bool { var hasActiveError: Bool {
if Thread.isMainThread { loadingItems.contains { $0.isError && $0.shouldDisplay }
return loadingItems.contains { $0.isError && $0.shouldDisplay }
} else {
return false
}
} }
// MARK: - Private Methods // MARK: - Private Methods
/// ///
/// - Parameter id: ID /// - Parameter id: ID
private func setupErrorCleanup(for id: UUID) { private func setupErrorCleanup(for id: UUID) {
let workItem = DispatchWorkItem { [weak self] in let workItem = DispatchWorkItem { [weak self] in
self?.removeLoading(id) self?.removeLoading(id)
} }
errorCleanupTasks[id] = workItem errorCleanupTasks[id] = workItem
DispatchQueue.main.asyncAfter( DispatchQueue.main.asyncAfter(
deadline: .now() + APILoadingConfiguration.errorDisplayDuration, deadline: .now() + APILoadingConfiguration.errorDisplayDuration,
execute: workItem execute: workItem
) )
} }
/// ///
/// - Parameter id: ID /// - Parameter id: ID
private func cancelErrorCleanup(for id: UUID) { private func cancelErrorCleanup(for id: UUID) {
@@ -168,14 +109,14 @@ class APILoadingManager: ObservableObject {
// MARK: - Convenience Extensions // MARK: - Convenience Extensions
extension APILoadingManager { extension APILoadingManager {
/// 便 loading /// 便 loading
/// - Parameters: /// - Parameters:
/// - shouldShowLoading: loading /// - shouldShowLoading: loading
/// - shouldShowError: /// - shouldShowError:
/// - operation: /// - operation:
/// - Returns: /// - Returns:
func withLoading<T>( @MainActor
func withLoading<T: Sendable>(
shouldShowLoading: Bool = true, shouldShowLoading: Bool = true,
shouldShowError: Bool = true, shouldShowError: Bool = true,
operation: @escaping () async throws -> T operation: @escaping () async throws -> T
@@ -184,7 +125,6 @@ extension APILoadingManager {
shouldShowLoading: shouldShowLoading, shouldShowLoading: shouldShowLoading,
shouldShowError: shouldShowError shouldShowError: shouldShowError
) )
do { do {
let result = try await operation() let result = try await operation()
finishLoading(loadingId) finishLoading(loadingId)

View File

@@ -6,7 +6,7 @@ struct StringHashTest {
/// ///
static func runTests() { static func runTests() {
debugInfo("🧪 开始测试字符串哈希方法...") debugInfoSync("🧪 开始测试字符串哈希方法...")
let testStrings = [ let testStrings = [
"hello world", "hello world",
@@ -16,27 +16,27 @@ struct StringHashTest {
] ]
for testString in testStrings { for testString in testStrings {
debugInfo("\n📝 测试字符串: \"\(testString)\"") debugInfoSync("\n📝 测试字符串: \"\(testString)\"")
// MD5 // MD5
let md5Result = testString.md5() let md5Result = testString.md5()
debugInfo(" MD5: \(md5Result)") debugInfoSync(" MD5: \(md5Result)")
// SHA256 (iOS 13+) // SHA256 (iOS 13+)
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
let sha256Result = testString.sha256() let sha256Result = testString.sha256()
debugInfo(" SHA256: \(sha256Result)") debugInfoSync(" SHA256: \(sha256Result)")
} else { } else {
debugInfo(" SHA256: 不支持 (需要 iOS 13+)") debugInfoSync(" SHA256: 不支持 (需要 iOS 13+)")
} }
} }
debugInfo("\n✅ 哈希方法测试完成") debugInfoSync("\n✅ 哈希方法测试完成")
} }
/// ///
static func verifyKnownHashes() { static func verifyKnownHashes() {
debugInfo("\n🔍 验证已知哈希值...") debugInfoSync("\n🔍 验证已知哈希值...")
// "hello world" MD5 "5d41402abc4b2a76b9719d911017c592" // "hello world" MD5 "5d41402abc4b2a76b9719d911017c592"
let testString = "hello world" let testString = "hello world"
@@ -44,11 +44,11 @@ struct StringHashTest {
let actualMD5 = testString.md5() let actualMD5 = testString.md5()
if actualMD5 == expectedMD5 { if actualMD5 == expectedMD5 {
debugInfo("✅ MD5 验证通过: \(actualMD5)") debugInfoSync("✅ MD5 验证通过: \(actualMD5)")
} else { } else {
debugError("❌ MD5 验证失败:") debugErrorSync("❌ MD5 验证失败:")
debugError(" 期望: \(expectedMD5)") debugErrorSync(" 期望: \(expectedMD5)")
debugError(" 实际: \(actualMD5)") debugErrorSync(" 实际: \(actualMD5)")
} }
// SHA256 // SHA256
@@ -57,11 +57,11 @@ struct StringHashTest {
let actualSHA256 = testString.sha256() let actualSHA256 = testString.sha256()
if actualSHA256 == expectedSHA256 { if actualSHA256 == expectedSHA256 {
debugInfo("✅ SHA256 验证通过: \(actualSHA256)") debugInfoSync("✅ SHA256 验证通过: \(actualSHA256)")
} else { } else {
debugError("❌ SHA256 验证失败:") debugErrorSync("❌ SHA256 验证失败:")
debugError(" 期望: \(expectedSHA256)") debugErrorSync(" 期望: \(expectedSHA256)")
debugError(" 实际: \(actualSHA256)") debugErrorSync(" 实际: \(actualSHA256)")
} }
} }
} }
@@ -75,9 +75,9 @@ struct StringHashTest {
StringHashTest.verifyKnownHashes() StringHashTest.verifyKnownHashes()
// //
debugInfo("Test MD5:", "hello".md5()) debugInfoSync("Test MD5:", "hello".md5())
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
debugInfo("Test SHA256:", "hello".sha256()) debugInfoSync("Test SHA256:", "hello".sha256())
} }
*/ */

View File

@@ -67,11 +67,11 @@ struct FontManager {
/// ///
static func printAllAvailableFonts() { static func printAllAvailableFonts() {
debugInfo("=== 所有可用字体 ===") debugInfoSync("=== 所有可用字体 ===")
for font in getAllAvailableFonts() { for font in getAllAvailableFonts() {
debugInfo(font) debugInfoSync(font)
} }
debugInfo("==================") debugInfoSync("==================")
} }
} }

View File

@@ -8,6 +8,7 @@ import SwiftUI
/// - /// -
/// - /// -
/// - UserDefaults /// - UserDefaults
@MainActor
class LocalizationManager: ObservableObject { class LocalizationManager: ObservableObject {
// MARK: - // MARK: -
@@ -42,9 +43,9 @@ class LocalizationManager: ObservableObject {
didSet { didSet {
do { do {
try KeychainManager.shared.storeString(currentLanguage.rawValue, forKey: "AppLanguage") try KeychainManager.shared.storeString(currentLanguage.rawValue, forKey: "AppLanguage")
} catch { } catch {
debugError("❌ 保存语言设置失败: \(error)") debugErrorSync("❌ 保存语言设置失败: \(error)")
} }
// //
objectWillChange.send() objectWillChange.send()
} }
@@ -56,7 +57,7 @@ class LocalizationManager: ObservableObject {
do { do {
savedLanguage = try KeychainManager.shared.retrieveString(forKey: "AppLanguage") savedLanguage = try KeychainManager.shared.retrieveString(forKey: "AppLanguage")
} catch { } catch {
debugError("❌ 读取语言设置失败: \(error)") debugErrorSync("❌ 读取语言设置失败: \(error)")
savedLanguage = nil savedLanguage = nil
} }
@@ -113,34 +114,26 @@ class LocalizationManager: ObservableObject {
} }
// MARK: - SwiftUI Extensions // MARK: - SwiftUI Extensions
extension View { // extension View {
/// // ///
/// - Parameter key: key // /// - Parameter key: key
/// - Returns: // /// - Returns:
func localized(_ key: String) -> some View { // @MainActor
self.modifier(LocalizedTextModifier(key: key)) // 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
}
}
// MARK: - 便 // MARK: - 便
extension String { // extension String {
/// // ///
var localized: String { // @MainActor
return LocalizationManager.shared.localizedString(self) // var localized: String {
} // return LocalizationManager.shared.localizedString(self)
// }
/// // ///
func localized(arguments: CVarArg...) -> String { // @MainActor
return LocalizationManager.shared.localizedString(self, arguments: arguments) // func localized(arguments: CVarArg...) -> String {
} // return LocalizationManager.shared.localizedString(self, arguments: arguments)
} // }
// }

View File

@@ -5,8 +5,8 @@ struct DESEncryptOCTest {
/// OC DES /// OC DES
static func testOCDESEncryption() { static func testOCDESEncryption() {
debugInfo("🧪 开始测试 OC 版本的 DES 加密...") debugInfoSync("🧪 开始测试 OC 版本的 DES 加密...")
debugInfo(String(repeating: "=", count: 50)) debugInfoSync(String(repeating: "=", count: 50))
let key = "1ea53d260ecf11e7b56e00163e046a26" let key = "1ea53d260ecf11e7b56e00163e046a26"
let testCases = [ let testCases = [
@@ -19,25 +19,25 @@ struct DESEncryptOCTest {
for testCase in testCases { for testCase in testCases {
if let encrypted = DESEncrypt.encryptUseDES(testCase, key: key) { if let encrypted = DESEncrypt.encryptUseDES(testCase, key: key) {
debugInfo("✅ 加密成功:") debugInfoSync("✅ 加密成功:")
debugInfo(" 原文: \"\(testCase)\"") debugInfoSync(" 原文: \"\(testCase)\"")
debugInfo(" 密文: \(encrypted)") debugInfoSync(" 密文: \(encrypted)")
// //
if let decrypted = DESEncrypt.decryptUseDES(encrypted, key: key) { if let decrypted = DESEncrypt.decryptUseDES(encrypted, key: key) {
let isMatch = decrypted == testCase let isMatch = decrypted == testCase
debugInfo(" 解密: \"\(decrypted)\" \(isMatch ? "" : "")") debugInfoSync(" 解密: \"\(decrypted)\" \(isMatch ? "" : "")")
} else { } else {
debugError(" 解密: 失败 ❌") debugErrorSync(" 解密: 失败 ❌")
} }
} else { } else {
debugError("❌ 加密失败: \"\(testCase)\"") debugErrorSync("❌ 加密失败: \"\(testCase)\"")
} }
debugInfo("") debugInfoSync("")
} }
debugInfo(String(repeating: "=", count: 50)) debugInfoSync(String(repeating: "=", count: 50))
debugInfo("🏁 OC版本DES加密测试完成") debugInfoSync("🏁 OC版本DES加密测试完成")
} }
} }
@@ -48,4 +48,4 @@ extension DESEncryptOCTest {
DESEncryptOCTest.testOCDESEncryption() DESEncryptOCTest.testOCDESEncryption()
} }
} }
#endif #endif

View File

@@ -10,6 +10,7 @@ import Foundation
/// 2. Keychain /// 2. Keychain
/// 3. /// 3.
/// 4. /// 4.
@MainActor
final class DataMigrationManager { final class DataMigrationManager {
// MARK: - // MARK: -
@@ -54,23 +55,23 @@ final class DataMigrationManager {
/// ///
/// - Returns: /// - Returns:
func performMigration() -> MigrationResult { func performMigration() -> MigrationResult {
debugInfo("🔄 开始检查数据迁移...") debugInfoSync("🔄 开始检查数据迁移...")
// //
if isMigrationCompleted() { if isMigrationCompleted() {
debugInfo("✅ 数据已经迁移过,跳过迁移") debugInfoSync("✅ 数据已经迁移过,跳过迁移")
return .alreadyMigrated return .alreadyMigrated
} }
// //
let legacyData = collectLegacyData() let legacyData = collectLegacyData()
if legacyData.isEmpty { if legacyData.isEmpty {
debugInfo(" 没有发现需要迁移的数据") debugInfoSync(" 没有发现需要迁移的数据")
markMigrationCompleted() markMigrationCompleted()
return .noDataToMigrate return .noDataToMigrate
} }
debugInfo("📦 发现需要迁移的数据: \(legacyData.keys.joined(separator: ", "))") debugInfoSync("📦 发现需要迁移的数据: \(legacyData.keys.joined(separator: ", "))")
do { do {
// //
@@ -85,11 +86,11 @@ final class DataMigrationManager {
// //
markMigrationCompleted() markMigrationCompleted()
debugInfo("✅ 数据迁移完成") debugInfoSync("✅ 数据迁移完成")
return .completed return .completed
} catch { } catch {
debugError("❌ 数据迁移失败: \(error)") debugErrorSync("❌ 数据迁移失败: \(error)")
return .failed(error) return .failed(error)
} }
} }
@@ -157,9 +158,9 @@ final class DataMigrationManager {
do { do {
let accountModel = try JSONDecoder().decode(AccountModel.self, from: accountModelData) let accountModel = try JSONDecoder().decode(AccountModel.self, from: accountModelData)
try keychain.store(accountModel, forKey: "account_model") try keychain.store(accountModel, forKey: "account_model")
debugInfo("✅ AccountModel 迁移成功") debugInfoSync("✅ AccountModel 迁移成功")
} catch { } catch {
debugError("❌ AccountModel 迁移失败: \(error)") debugErrorSync("❌ AccountModel 迁移失败: \(error)")
// AccountModel // AccountModel
try migrateAccountModelFromIndependentFields(legacyData) try migrateAccountModelFromIndependentFields(legacyData)
} }
@@ -173,9 +174,9 @@ final class DataMigrationManager {
do { do {
let userInfo = try JSONDecoder().decode(UserInfo.self, from: userInfoData) let userInfo = try JSONDecoder().decode(UserInfo.self, from: userInfoData)
try keychain.store(userInfo, forKey: "user_info") try keychain.store(userInfo, forKey: "user_info")
debugInfo("✅ UserInfo 迁移成功") debugInfoSync("✅ UserInfo 迁移成功")
} catch { } catch {
debugError("❌ UserInfo 迁移失败: \(error)") debugErrorSync("❌ UserInfo 迁移失败: \(error)")
throw error throw error
} }
} }
@@ -183,7 +184,7 @@ final class DataMigrationManager {
// //
if let appLanguage = legacyData[LegacyStorageKeys.appLanguage] as? String { if let appLanguage = legacyData[LegacyStorageKeys.appLanguage] as? String {
try keychain.storeString(appLanguage, forKey: "AppLanguage") try keychain.storeString(appLanguage, forKey: "AppLanguage")
debugInfo("✅ 语言设置迁移成功") debugInfoSync("✅ 语言设置迁移成功")
} }
} }
@@ -191,7 +192,7 @@ final class DataMigrationManager {
private func migrateAccountModelFromIndependentFields(_ legacyData: [String: Any]) throws { private func migrateAccountModelFromIndependentFields(_ legacyData: [String: Any]) throws {
guard let userId = legacyData[LegacyStorageKeys.userId] as? String, guard let userId = legacyData[LegacyStorageKeys.userId] as? String,
let accessToken = legacyData[LegacyStorageKeys.accessToken] as? String else { let accessToken = legacyData[LegacyStorageKeys.accessToken] as? String else {
debugInfo(" 没有足够的独立字段来重建 AccountModel") debugInfoSync(" 没有足够的独立字段来重建 AccountModel")
return return
} }
@@ -208,7 +209,7 @@ final class DataMigrationManager {
) )
try KeychainManager.shared.store(accountModel, forKey: "account_model") 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 { for key in keys {
userDefaults.removeObject(forKey: key) userDefaults.removeObject(forKey: key)
debugInfo("🗑️ 清理旧数据: \(key)") debugInfoSync("🗑️ 清理旧数据: \(key)")
} }
userDefaults.synchronize() userDefaults.synchronize()
debugInfo("✅ 旧数据清理完成") debugInfoSync("✅ 旧数据清理完成")
} }
} }
@@ -287,13 +288,13 @@ extension DataMigrationManager {
switch migrationResult { switch migrationResult {
case .completed: case .completed:
debugInfo("🎉 应用启动时数据迁移完成") debugInfoSync("🎉 应用启动时数据迁移完成")
case .alreadyMigrated: case .alreadyMigrated:
break // break //
case .noDataToMigrate: case .noDataToMigrate:
break // break //
case .failed(let error): case .failed(let error):
debugError("⚠️ 应用启动时数据迁移失败: \(error)") debugErrorSync("⚠️ 应用启动时数据迁移失败: \(error)")
// //
} }
} }
@@ -307,9 +308,9 @@ extension DataMigrationManager {
/// ///
func debugPrintLegacyData() { func debugPrintLegacyData() {
let legacyData = collectLegacyData() let legacyData = collectLegacyData()
debugInfo("🔍 旧版本数据:") debugInfoSync("🔍 旧版本数据:")
for (key, value) in legacyData { 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.set("zh-Hans", forKey: LegacyStorageKeys.appLanguage)
userDefaults.synchronize() userDefaults.synchronize()
debugInfo("🧪 已创建测试用的旧版本数据") debugInfoSync("🧪 已创建测试用的旧版本数据")
} }
/// ///
@@ -331,7 +332,7 @@ extension DataMigrationManager {
do { do {
try KeychainManager.shared.clearAll() try KeychainManager.shared.clearAll()
} catch { } catch {
debugError("❌ 清除 Keychain 数据失败: \(error)") debugErrorSync("❌ 清除 Keychain 数据失败: \(error)")
} }
// UserDefaults // UserDefaults
@@ -350,7 +351,7 @@ extension DataMigrationManager {
} }
userDefaults.synchronize() userDefaults.synchronize()
debugInfo("🧪 已清除所有迁移相关数据") debugInfoSync("🧪 已清除所有迁移相关数据")
} }
} }
#endif #endif

View File

@@ -12,10 +12,11 @@ import Security
/// - /// -
/// - 线 /// - 线
/// - 访 /// - 访
@MainActor
final class KeychainManager { final class KeychainManager {
// MARK: - // MARK: -
static let shared = KeychainManager() @MainActor static let shared = KeychainManager()
private init() {} private init() {}
// MARK: - // MARK: -
@@ -108,7 +109,7 @@ final class KeychainManager {
throw KeychainError.keychainOperationFailed(status) throw KeychainError.keychainOperationFailed(status)
} }
debugInfo("🔐 Keychain 存储成功: \(key)") debugInfoSync("🔐 Keychain 存储成功: \(key)")
} }
/// Keychain Codable /// Keychain Codable
@@ -137,7 +138,7 @@ final class KeychainManager {
// 4. // 4.
do { do {
let object = try JSONDecoder().decode(type, from: data) let object = try JSONDecoder().decode(type, from: data)
debugInfo("🔐 Keychain 读取成功: \(key)") debugInfoSync("🔐 Keychain 读取成功: \(key)")
return object return object
} catch { } catch {
throw KeychainError.decodingFailed(error) throw KeychainError.decodingFailed(error)
@@ -176,7 +177,7 @@ final class KeychainManager {
switch status { switch status {
case errSecSuccess: case errSecSuccess:
debugInfo("🔐 Keychain 更新成功: \(key)") debugInfoSync("🔐 Keychain 更新成功: \(key)")
case errSecItemNotFound: case errSecItemNotFound:
// //
@@ -196,7 +197,7 @@ final class KeychainManager {
switch status { switch status {
case errSecSuccess: case errSecSuccess:
debugInfo("🔐 Keychain 删除成功: \(key)") debugInfoSync("🔐 Keychain 删除成功: \(key)")
case errSecItemNotFound: case errSecItemNotFound:
// //
@@ -231,7 +232,7 @@ final class KeychainManager {
switch status { switch status {
case errSecSuccess, errSecItemNotFound: case errSecSuccess, errSecItemNotFound:
debugInfo("🔐 Keychain 清除完成") debugInfoSync("🔐 Keychain 清除完成")
default: default:
throw KeychainError.keychainOperationFailed(status) throw KeychainError.keychainOperationFailed(status)
@@ -353,10 +354,10 @@ extension KeychainManager {
/// ///
func debugPrintAllKeys() { func debugPrintAllKeys() {
let keys = debugListAllKeys() let keys = debugListAllKeys()
debugInfo("🔐 Keychain 中存储的键:") debugInfoSync("🔐 Keychain 中存储的键:")
for key in keys { for key in keys {
debugInfo(" - \(key)") debugInfoSync(" - \(key)")
} }
} }
} }
#endif #endif

View File

@@ -38,20 +38,20 @@ struct UserAgreementView: View {
// MARK: - Private Methods // MARK: - Private Methods
private func createAttributedText() -> AttributedString { private func createAttributedText() -> AttributedString {
var attributedString = AttributedString("login.agreement_policy".localized) var attributedString = AttributedString(NSLocalizedString("login.agreement_policy", comment: ""))
// //
attributedString.foregroundColor = Color(hex: 0x666666) 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].foregroundColor = Color(hex: 0x8A4FFF)
attributedString[userServiceRange].underlineStyle = .single attributedString[userServiceRange].underlineStyle = .single
attributedString[userServiceRange].link = URL(string: "user-service-agreement") 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].foregroundColor = Color(hex: 0x8A4FFF)
attributedString[privacyPolicyRange].underlineStyle = .single attributedString[privacyPolicyRange].underlineStyle = .single
attributedString[privacyPolicyRange].link = URL(string: "privacy-policy") attributedString[privacyPolicyRange].link = URL(string: "privacy-policy")
@@ -61,28 +61,28 @@ struct UserAgreementView: View {
} }
} }
#Preview { //#Preview {
VStack(spacing: 20) { // VStack(spacing: 20) {
UserAgreementView( // UserAgreementView(
isAgreed: .constant(true), // isAgreed: .constant(true),
onUserServiceTapped: { // onUserServiceTapped: {
debugInfo("User Service Agreement tapped") // debugInfoSync("User Service Agreement tapped")
}, // },
onPrivacyPolicyTapped: { // onPrivacyPolicyTapped: {
debugInfo("Privacy Policy tapped") // debugInfoSync("Privacy Policy tapped")
} // }
) // )
//
UserAgreementView( // UserAgreementView(
isAgreed: .constant(true), // isAgreed: .constant(true),
onUserServiceTapped: { // onUserServiceTapped: {
debugInfo("User Service Agreement tapped") // debugInfoSync("User Service Agreement tapped")
}, // },
onPrivacyPolicyTapped: { // onPrivacyPolicyTapped: {
debugInfo("Privacy Policy tapped") // debugInfoSync("Privacy Policy tapped")
} // }
) // )
} // }
.padding() // .padding()
.background(Color.gray.opacity(0.1)) // .background(Color.gray.opacity(0.1))
} //}

View File

@@ -1,17 +1,13 @@
import SwiftUI import SwiftUI
import ComposableArchitecture import ComposableArchitecture
// PhotosUI (iOS 16.0+)
#if canImport(PhotosUI)
import PhotosUI import PhotosUI
#endif
struct CreateFeedView: View { struct CreateFeedView: View {
let store: StoreOf<CreateFeedFeature> let store: StoreOf<CreateFeedFeature>
var body: some View { var body: some View {
WithPerceptionTracking { WithPerceptionTracking {
NavigationView { NavigationStack {
GeometryReader { geometry in GeometryReader { geometry in
ZStack { ZStack {
// //
@@ -59,7 +55,7 @@ struct CreateFeedView: View {
Text("\(store.characterCount)/500") Text("\(store.characterCount)/500")
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundColor( .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) { VStack(alignment: .leading, spacing: 12) {
if !store.processedImages.isEmpty || store.canAddMoreImages { if !store.processedImages.isEmpty || store.canAddMoreImages {
if #available(iOS 16.0, *) { ModernImageSelectionGrid(
#if canImport(PhotosUI) images: store.processedImages,
ModernImageSelectionGrid( selectedItems: store.selectedImages,
images: store.processedImages, canAddMore: store.canAddMoreImages,
selectedItems: store.selectedImages, onItemsChanged: { items in
canAddMore: store.canAddMoreImages, store.send(.photosPickerItemsChanged(items))
onItemsChanged: { items in },
store.send(.photosPickerItemsChanged(items)) onRemoveImage: { index in
}, store.send(.removeImage(index))
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))
}
)
}
} }
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
@@ -112,7 +93,7 @@ struct CreateFeedView: View {
} }
// //
if let error = store.error { if let error = store.errorMessage {
Text(error) Text(error)
.font(.system(size: 14)) .font(.system(size: 14))
.foregroundColor(.red) .foregroundColor(.red)
@@ -133,7 +114,7 @@ struct CreateFeedView: View {
store.send(.publishButtonTapped) store.send(.publishButtonTapped)
}) { }) {
HStack { HStack {
if store.isPublishing { if store.isLoading {
ProgressView() ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white)) .progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8) .scaleEffect(0.8)
@@ -159,8 +140,8 @@ struct CreateFeedView: View {
) )
) )
.cornerRadius(25) .cornerRadius(25)
.disabled(store.isPublishing || (!store.isContentValid && !store.isLoading)) .disabled(store.isLoading || !store.canPublish)
.opacity(store.isPublishing || (!store.isContentValid && !store.isLoading) ? 0.6 : 1.0) .opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0)
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
.padding(.bottom, geometry.safeAreaInsets.bottom + 20) .padding(.bottom, geometry.safeAreaInsets.bottom + 20)
@@ -182,27 +163,17 @@ struct CreateFeedView: View {
Button("发布") { Button("发布") {
store.send(.publishButtonTapped) store.send(.publishButtonTapped)
} }
.foregroundColor(store.isContentValid ? .white : .white.opacity(0.5)) .foregroundColor(store.canPublish ? .white : .white.opacity(0.5))
.disabled(!store.isContentValid || store.isPublishing) .disabled(!store.canPublish || store.isLoading)
} }
} }
} }
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
.sheet(isPresented: .init(
get: { store.showingImagePicker },
set: { _ in store.send(.hideImagePicker) }
)) {
ImagePickerView { image in
store.send(.imageSelected(image))
}
}
} }
} }
} }
// MARK: - iOS 16+ // MARK: - iOS 16+
#if canImport(PhotosUI)
@available(iOS 16.0, *)
struct ModernImageSelectionGrid: View { struct ModernImageSelectionGrid: View {
let images: [UIImage] let images: [UIImage]
let selectedItems: [PhotosPickerItem] 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: - // MARK: -
#Preview { #Preview {

View File

@@ -31,7 +31,7 @@ struct EMailLoginView: View {
} else if codeCountdown > 0 { } else if codeCountdown > 0 {
return "\(codeCountdown)S" return "\(codeCountdown)S"
} else { } else {
return "email_login.get_code".localized return NSLocalizedString("email_login.get_code", comment: "")
} }
} }
@@ -70,7 +70,7 @@ struct EMailLoginView: View {
.frame(height: 60) .frame(height: 60)
// //
Text("email_login.title".localized) Text(NSLocalizedString("email_login.title", comment: ""))
.font(.system(size: 28, weight: .medium)) .font(.system(size: 28, weight: .medium))
.foregroundColor(.white) .foregroundColor(.white)
.padding(.bottom, 80) .padding(.bottom, 80)
@@ -89,7 +89,7 @@ struct EMailLoginView: View {
TextField("", text: $email) TextField("", text: $email)
.placeholder(when: email.isEmpty) { .placeholder(when: email.isEmpty) {
Text("placeholder.enter_email".localized) Text(NSLocalizedString("placeholder.enter_email", comment: ""))
.foregroundColor(.white.opacity(0.6)) .foregroundColor(.white.opacity(0.6))
} }
.foregroundColor(.white) .foregroundColor(.white)
@@ -114,7 +114,7 @@ struct EMailLoginView: View {
HStack { HStack {
TextField("", text: $verificationCode) TextField("", text: $verificationCode)
.placeholder(when: verificationCode.isEmpty) { .placeholder(when: verificationCode.isEmpty) {
Text("placeholder.enter_verification_code".localized) Text(NSLocalizedString("placeholder.enter_verification_code", comment: ""))
.foregroundColor(.white.opacity(0.6)) .foregroundColor(.white.opacity(0.6))
} }
.foregroundColor(.white) .foregroundColor(.white)
@@ -178,7 +178,7 @@ struct EMailLoginView: View {
.progressViewStyle(CircularProgressViewStyle(tint: .white)) .progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8) .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)) .font(.system(size: 18, weight: .semibold))
.foregroundColor(.white) .foregroundColor(.white)
} }

View File

@@ -1,6 +1,64 @@
import SwiftUI import SwiftUI
import ComposableArchitecture import ComposableArchitecture
struct FeedTopBarView: View {
let store: StoreOf<FeedFeature>
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<FeedFeature>
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 { struct FeedView: View {
let store: StoreOf<FeedFeature> let store: StoreOf<FeedFeature>
@@ -9,80 +67,18 @@ struct FeedView: View {
GeometryReader { geometry in GeometryReader { geometry in
ScrollView { ScrollView {
VStack(spacing: 20) { VStack(spacing: 20) {
// - FeedTopBarView(store: store)
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)
//
Image(systemName: "heart.fill") Image(systemName: "heart.fill")
.font(.system(size: 60)) .font(.system(size: 60))
.foregroundColor(.red) .foregroundColor(.red)
.padding(.top, 40) .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.") 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)) .font(.system(size: 16))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundColor(.white.opacity(0.9)) .foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 30) .padding(.horizontal, 30)
.padding(.top, 20) .padding(.top, 20)
FeedMomentsListView(store: store)
// - 使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
if store.isLoading { if store.isLoading {
HStack { HStack {
ProgressView() ProgressView()
@@ -93,8 +89,6 @@ struct FeedView: View {
} }
.padding(.top, 20) .padding(.top, 20)
} }
//
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
} }
} }
@@ -108,14 +102,8 @@ struct FeedView: View {
get: { store.isShowingCreateFeed }, get: { store.isShowingCreateFeed },
set: { _ in store.send(.dismissCreateFeed) } set: { _ in store.send(.dismissCreateFeed) }
)) { )) {
CreateFeedView( if let createFeedStore = store.scope(state: \.createFeedState, action: \.createFeed) {
store: Store(initialState: CreateFeedFeature.State()) { CreateFeedView(store: createFeedStore)
CreateFeedFeature()
}
)
.onDisappear {
// CreateFeedView
//
} }
} }
} }

View File

@@ -46,7 +46,7 @@ struct IDLoginView: View {
.frame(height: 60) .frame(height: 60)
// //
Text("id_login.title".localized) Text(NSLocalizedString("id_login.title", comment: ""))
.font(.system(size: 28, weight: .medium)) .font(.system(size: 28, weight: .medium))
.foregroundColor(.white) .foregroundColor(.white)
.padding(.bottom, 80) .padding(.bottom, 80)
@@ -65,7 +65,7 @@ struct IDLoginView: View {
TextField("", text: $userID) // 使SwiftUI TextField("", text: $userID) // 使SwiftUI
.placeholder(when: userID.isEmpty) { .placeholder(when: userID.isEmpty) {
Text("placeholder.enter_id".localized) Text(NSLocalizedString("placeholder.enter_id", comment: ""))
.foregroundColor(.white.opacity(0.6)) .foregroundColor(.white.opacity(0.6))
} }
.foregroundColor(.white) .foregroundColor(.white)
@@ -88,7 +88,7 @@ struct IDLoginView: View {
if isPasswordVisible { if isPasswordVisible {
TextField("", text: $password) // 使SwiftUI TextField("", text: $password) // 使SwiftUI
.placeholder(when: password.isEmpty) { .placeholder(when: password.isEmpty) {
Text("placeholder.enter_password".localized) Text(NSLocalizedString("placeholder.enter_password", comment: ""))
.foregroundColor(.white.opacity(0.6)) .foregroundColor(.white.opacity(0.6))
} }
.foregroundColor(.white) .foregroundColor(.white)
@@ -96,7 +96,7 @@ struct IDLoginView: View {
} else { } else {
SecureField("", text: $password) // 使SwiftUI SecureField("", text: $password) // 使SwiftUI
.placeholder(when: password.isEmpty) { .placeholder(when: password.isEmpty) {
Text("placeholder.enter_password".localized) Text(NSLocalizedString("placeholder.enter_password", comment: ""))
.foregroundColor(.white.opacity(0.6)) .foregroundColor(.white.opacity(0.6))
} }
.foregroundColor(.white) .foregroundColor(.white)
@@ -122,7 +122,7 @@ struct IDLoginView: View {
Button(action: { Button(action: {
showRecoverPassword = true showRecoverPassword = true
}) { }) {
Text("id_login.forgot_password".localized) Text(NSLocalizedString("id_login.forgot_password", comment: ""))
.font(.system(size: 14)) .font(.system(size: 14))
.foregroundColor(.white.opacity(0.8)) .foregroundColor(.white.opacity(0.8))
} }
@@ -156,7 +156,7 @@ struct IDLoginView: View {
.progressViewStyle(CircularProgressViewStyle(tint: .white)) .progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8) .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)) .font(.system(size: 18, weight: .semibold))
.foregroundColor(.white) .foregroundColor(.white)
} }
@@ -207,7 +207,7 @@ struct IDLoginView: View {
#if DEBUG #if DEBUG
// //
debugInfo("🐛 Debug模式: 已移除硬编码测试凭据") debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
#endif #endif
} }
} }

View File

@@ -10,7 +10,7 @@ struct LanguageSettingsView: View {
} }
var body: some View { var body: some View {
NavigationView { NavigationStack {
List { List {
Section { Section {
ForEach(LocalizationManager.SupportedLanguage.allCases, id: \.rawValue) { language in ForEach(LocalizationManager.SupportedLanguage.allCases, id: \.rawValue) { language in
@@ -47,13 +47,6 @@ struct LanguageSettingsView: View {
.navigationTitle("语言设置 / Language") .navigationTitle("语言设置 / Language")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true) .navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("返回 / Back") {
isPresented = false
}
}
}
} }
} }
} }

View File

@@ -3,7 +3,7 @@ import ComposableArchitecture
// PreferenceKey // PreferenceKey
struct ImageHeightPreferenceKey: PreferenceKey { struct ImageHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0 static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue()) value = max(value, nextValue())
} }
@@ -21,7 +21,7 @@ struct LoginView: View {
@State private var showEmailLogin = false // @State private var showEmailLogin = false //
var body: some View { var body: some View {
NavigationView { NavigationStack {
GeometryReader { geometry in GeometryReader { geometry in
ZStack { ZStack {
// 使 splash // 使 splash
@@ -44,7 +44,7 @@ struct LoginView: View {
) )
// E-PARTI "top"20 // E-PARTI "top"20
HStack { HStack {
Text("login.app_title".localized) Text(NSLocalizedString("login.app_title", comment: ""))
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width)) .font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
.foregroundColor(.white) .foregroundColor(.white)
.padding(.leading, 20) .padding(.leading, 20)
@@ -78,7 +78,7 @@ struct LoginView: View {
LoginButton( LoginButton(
iconName: "person.circle.fill", iconName: "person.circle.fill",
iconColor: .green, iconColor: .green,
title: "login.id_login".localized title: NSLocalizedString("login.id_login", comment: "")
) { ) {
showIDLogin = true // SwiftUI showIDLogin = true // SwiftUI
} }
@@ -86,7 +86,7 @@ struct LoginView: View {
LoginButton( LoginButton(
iconName: "envelope.fill", iconName: "envelope.fill",
iconColor: .blue, iconColor: .blue,
title: "login.email_login".localized title: NSLocalizedString("login.email_login", comment: "")
) { ) {
showEmailLogin = true // showEmailLogin = true //
} }
@@ -153,7 +153,6 @@ struct LoginView: View {
} }
.navigationBarHidden(true) .navigationBarHidden(true)
} }
.navigationViewStyle(StackNavigationViewStyle())
.sheet(isPresented: $showLanguageSettings) { .sheet(isPresented: $showLanguageSettings) {
LanguageSettingsView(isPresented: $showLanguageSettings) LanguageSettingsView(isPresented: $showLanguageSettings)
} }

View File

@@ -78,7 +78,7 @@ struct MeView: View {
.alert("确认退出", isPresented: $showLogoutConfirmation) { .alert("确认退出", isPresented: $showLogoutConfirmation) {
Button("取消", role: .cancel) { } Button("取消", role: .cancel) { }
Button("退出", role: .destructive) { Button("退出", role: .destructive) {
performLogout() Task { await performLogout() }
} }
} message: { } message: {
Text("确定要退出登录吗?") Text("确定要退出登录吗?")
@@ -86,16 +86,13 @@ struct MeView: View {
} }
// MARK: - 退 // MARK: - 退
private func performLogout() { private func performLogout() async {
debugInfo("🔓 开始执行退出登录...") debugInfoSync("🔓 开始执行退出登录...")
// keychain // keychain
UserInfoManager.clearAllAuthenticationData() await UserInfoManager.clearAllAuthenticationData()
// window root login view // window root login view
NotificationCenter.default.post(name: .homeLogout, object: nil) NotificationCenter.default.post(name: .homeLogout, object: nil)
debugInfoSync("✅ 退出登录完成")
debugInfo("✅ 退出登录完成")
} }
} }

View File

@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ComposableArchitecture import ComposableArchitecture
import Combine
struct RecoverPasswordView: View { struct RecoverPasswordView: View {
let store: StoreOf<RecoverPasswordFeature> let store: StoreOf<RecoverPasswordFeature>
@@ -13,7 +14,7 @@ struct RecoverPasswordView: View {
// //
@State private var countdown: Int = 0 @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 { private var isConfirmButtonEnabled: Bool {
@@ -32,7 +33,7 @@ struct RecoverPasswordView: View {
} else if countdown > 0 { } else if countdown > 0 {
return "\(countdown)s" return "\(countdown)s"
} else { } else {
return "recover_password.get_code".localized return NSLocalizedString("recover_password.get_code", comment: "")
} }
} }
@@ -66,7 +67,7 @@ struct RecoverPasswordView: View {
.frame(height: 60) .frame(height: 60)
// //
Text("recover_password.title".localized) Text(NSLocalizedString("recover_password.title", comment: ""))
.font(.system(size: 28, weight: .medium)) .font(.system(size: 28, weight: .medium))
.foregroundColor(.white) .foregroundColor(.white)
.padding(.bottom, 80) .padding(.bottom, 80)
@@ -85,7 +86,7 @@ struct RecoverPasswordView: View {
TextField("", text: $email) TextField("", text: $email)
.placeholder(when: email.isEmpty) { .placeholder(when: email.isEmpty) {
Text("recover_password.placeholder_email".localized) Text(NSLocalizedString("recover_password.placeholder_email", comment: ""))
.foregroundColor(.white.opacity(0.6)) .foregroundColor(.white.opacity(0.6))
} }
.foregroundColor(.white) .foregroundColor(.white)
@@ -108,7 +109,7 @@ struct RecoverPasswordView: View {
HStack { HStack {
TextField("", text: $verificationCode) TextField("", text: $verificationCode)
.placeholder(when: verificationCode.isEmpty) { .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.opacity(0.6))
} }
.foregroundColor(.white) .foregroundColor(.white)
@@ -158,7 +159,7 @@ struct RecoverPasswordView: View {
if isNewPasswordVisible { if isNewPasswordVisible {
TextField("", text: $newPassword) TextField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) { .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.opacity(0.6))
} }
.foregroundColor(.white) .foregroundColor(.white)
@@ -166,7 +167,7 @@ struct RecoverPasswordView: View {
} else { } else {
SecureField("", text: $newPassword) SecureField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) { .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.opacity(0.6))
} }
.foregroundColor(.white) .foregroundColor(.white)
@@ -211,7 +212,7 @@ struct RecoverPasswordView: View {
.progressViewStyle(CircularProgressViewStyle(tint: .white)) .progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8) .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)) .font(.system(size: 18, weight: .semibold))
.foregroundColor(.white) .foregroundColor(.white)
} }
@@ -244,15 +245,13 @@ struct RecoverPasswordView: View {
newPassword = "" newPassword = ""
isNewPasswordVisible = false isNewPasswordVisible = false
countdown = 0 countdown = 0
stopCountdown()
#if DEBUG #if DEBUG
email = "exzero@126.com" email = "exzero@126.com"
store.send(.emailChanged(email)) store.send(.emailChanged(email))
#endif #endif
} }
.onDisappear { .onDisappear {
stopCountdown() countdown = 0
} }
.onChange(of: email) { newEmail in .onChange(of: email) { newEmail in
store.send(.emailChanged(newEmail)) store.send(.emailChanged(newEmail))
@@ -275,24 +274,20 @@ struct RecoverPasswordView: View {
onBack() onBack()
} }
} }
.onReceive(timer) { _ in
if countdown > 0 {
countdown -= 1
}
}
} }
// MARK: - Private Methods // MARK: - Private Methods
private func startCountdown() { private func startCountdown() {
countdown = 60 countdown = 60
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if countdown > 0 {
countdown -= 1
} else {
stopCountdown()
}
}
} }
private func stopCountdown() { private func stopCountdown() {
countdownTimer?.invalidate()
countdownTimer = nil
countdown = 0 countdown = 0
} }
} }

View File

@@ -21,7 +21,7 @@ struct yanaApp: App {
} }
#endif #endif
debugInfo("🛠 原生URLSession测试开始") debugInfoSync("🛠 原生URLSession测试开始")
} }
var body: some Scene { var body: some Scene {

View File

@@ -163,10 +163,10 @@ final class yanaAPITests: XCTestCase {
XCTAssertEqual(accountModel?.uid, "3184", "真实API数据的UID应该正确") XCTAssertEqual(accountModel?.uid, "3184", "真实API数据的UID应该正确")
XCTAssertTrue(accountModel?.hasValidAuthentication ?? false, "真实API数据应该有有效认证") XCTAssertTrue(accountModel?.hasValidAuthentication ?? false, "真实API数据应该有有效认证")
debugInfo("✅ 真实API数据测试通过") debugInfoSync("✅ 真实API数据测试通过")
debugInfo(" UID: \(accountModel?.uid ?? "nil")") debugInfoSync(" UID: \(accountModel?.uid ?? "nil")")
debugInfo(" Access Token存在: \(accountModel?.accessToken != nil)") debugInfoSync(" Access Token存在: \(accountModel?.accessToken != nil)")
debugInfo(" Token类型: \(accountModel?.tokenType ?? "nil")") debugInfoSync(" Token类型: \(accountModel?.tokenType ?? "nil")")
} catch { } catch {
XCTFail("解析真实API数据失败: \(error)") XCTFail("解析真实API数据失败: \(error)")