From 007c10daaf491eca089ea9c2fa6ae7adefce924c Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Wed, 4 Jun 2025 17:25:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Swift=20Package?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=92=8CAPI=E5=8A=9F=E8=83=BD=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增Package.swift和Package.resolved文件以支持Swift Package管理,创建API相关文件(API.swift、APICaller.swift、APIConstants.swift、APIEndpoints.swift、APIService.swift、APILogger.swift、APIModels.swift、Integration-Guide.md)以实现API请求管理和网络交互功能,增强项目的功能性和可扩展性。同时更新.gitignore以排除构建文件和临时文件。 --- .gitignore | 6 + Package.resolved | 131 ++++ Package.swift | 32 + Podfile | 2 +- Podfile.lock | 2 +- yana.xcodeproj/project.pbxproj | 36 + .../xcshareddata/swiftpm/Package.resolved | 132 ++++ .../xcshareddata/swiftpm/Package.resolved | 132 ++++ .../xcdebugger/Breakpoints_v2.xcbkptlist | 86 +-- yana/APIs/API.swift | 59 -- yana/APIs/APICaller.swift | 8 - yana/APIs/APIConstants.swift | 26 + yana/APIs/APIEndpoints.swift | 38 + yana/APIs/APILogger.swift | 214 ++++++ yana/APIs/APIModels.swift | 137 ++++ yana/APIs/APIService.swift | 244 +++++++ yana/APIs/Integration-Guide.md | 196 +++++ yana/AppDelegate.swift | 27 +- yana/Configs/AppConfig.swift | 26 + yana/Configs/ClientConfig.swift | 63 +- yana/ContentView.swift | 261 +++++-- yana/Features/ConfigFeature.swift | 95 +++ yana/Features/ConfigView.swift | 190 +++++ yana/Features/InitFeature.swift | 63 ++ yana/Features/LoginFeature.swift | 83 +++ yana/Info.plist | 2 + yana/Managers/NIMConfigurationManager.swift | 4 +- yana/Managers/NetworkManager.swift | 667 ------------------ yana/yana.entitlements | 5 +- yana/yanaApp.swift | 20 +- 30 files changed, 2123 insertions(+), 864 deletions(-) create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 yana.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 yana.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 yana/APIs/API.swift delete mode 100644 yana/APIs/APICaller.swift create mode 100644 yana/APIs/APIConstants.swift create mode 100644 yana/APIs/APIEndpoints.swift create mode 100644 yana/APIs/APILogger.swift create mode 100644 yana/APIs/APIModels.swift create mode 100644 yana/APIs/APIService.swift create mode 100644 yana/APIs/Integration-Guide.md create mode 100644 yana/Features/ConfigFeature.swift create mode 100644 yana/Features/ConfigView.swift create mode 100644 yana/Features/InitFeature.swift create mode 100644 yana/Features/LoginFeature.swift delete mode 100644 yana/Managers/NetworkManager.swift diff --git a/.gitignore b/.gitignore index 61e7675..d8bd1ec 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,9 @@ Pods .vscode yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist *.xcbkptlist +.build/checkouts +.build/index-build +.build +.cursor +.swiftpm +yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..e1d59a6 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,131 @@ +{ + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "5928286acce13def418ec36d05a001a9641086f2", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25", + "version" : "1.7.0" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", + "version" : "1.0.6" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture", + "state" : { + "revision" : "6574de2396319a58e86e2178577268cb4aeccc30", + "version" : "1.20.2" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596", + "version" : "1.9.2" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-navigation", + "state" : { + "revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4", + "version" : "2.3.0" + } + }, + { + "identity" : "swift-perception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-perception", + "state" : { + "revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-sharing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-sharing", + "state" : { + "revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9", + "version" : "2.5.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..52dbc35 --- /dev/null +++ b/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "yana", + platforms: [ + .iOS(.v17), + .macOS(.v14) + ], + products: [ + .library( + name: "yana", + targets: ["yana"] + ), + ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.8.0"), + ], + targets: [ + .target( + name: "yana", + dependencies: [ + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + ] + ), + .testTarget( + name: "yanaTests", + dependencies: ["yana"] + ), + ] +) \ No newline at end of file diff --git a/Podfile b/Podfile index 919adf0..4f9b28e 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,5 @@ # Uncomment the next line to define a global platform for your project -platform :ios, '15.6' +platform :ios, '13.0' target 'yana' do # Comment the next line if you don't want to use dynamic frameworks diff --git a/Podfile.lock b/Podfile.lock index 5e3ac1c..9a45253 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -122,6 +122,6 @@ SPEC CHECKSUMS: YXAlog: 6fdd73102ba0a16933dd7bef426d6011d913c041 YXArtemis_XCFramework: d298161285aa9cf0c99800b17847dc99aef60617 -PODFILE CHECKSUM: 4034a059527d37196c5dca32d338b37b71e31488 +PODFILE CHECKSUM: 1d74a8886888ebdfb5a6d41769a74dd0a3026dec COCOAPODS: 1.16.2 diff --git a/yana.xcodeproj/project.pbxproj b/yana.xcodeproj/project.pbxproj index 2fe53d0..40eaec5 100644 --- a/yana.xcodeproj/project.pbxproj +++ b/yana.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C91082DE85A3E00384527 /* SystemConfiguration.framework */; }; + 4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C910A2DE85A4F00384527 /* CoreFoundation.framework */; }; + 4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 4C4C912F2DE864F000384527 /* ComposableArchitecture */; }; 856EF8A28776CEF6CE595B76 /* Pods_yana.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8529F57AF9337F626C670ED /* Pods_yana.framework */; }; /* End PBXBuildFile section */ @@ -24,6 +27,8 @@ 0977E1E6E883533DD125CAD4 /* Pods-yana.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-yana.debug.xcconfig"; path = "Target Support Files/Pods-yana/Pods-yana.debug.xcconfig"; sourceTree = ""; }; 4C3E651F2DB61F7A00E5A455 /* yana.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = yana.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4C4C8FBD2DE5AF9200384527 /* yanaAPITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = yanaAPITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C4C91082DE85A3E00384527 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 4C4C910A2DE85A4F00384527 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; 977CD030E95CB064179F3A1B /* Pods-yana.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-yana.release.xcconfig"; path = "Target Support Files/Pods-yana/Pods-yana.release.xcconfig"; sourceTree = ""; }; D8529F57AF9337F626C670ED /* Pods_yana.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_yana.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -60,7 +65,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */, 856EF8A28776CEF6CE595B76 /* Pods_yana.framework in Frameworks */, + 4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */, + 4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -105,6 +113,8 @@ 556C2003CCDA5AC2C56882D0 /* Frameworks */ = { isa = PBXGroup; children = ( + 4C4C910A2DE85A4F00384527 /* CoreFoundation.framework */, + 4C4C91082DE85A3E00384527 /* SystemConfiguration.framework */, D8529F57AF9337F626C670ED /* Pods_yana.framework */, ); name = Frameworks; @@ -204,6 +214,9 @@ ); mainGroup = 4C3E65162DB61F7A00E5A455; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 4C3E65202DB61F7A00E5A455 /* Products */; projectDirPath = ""; @@ -431,6 +444,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = yana/yana.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = EKM7RAGNA6; @@ -463,6 +477,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 20.20.61; + OTHER_LIBTOOLFLAGS = "-framework \"SystemConfiguration\""; PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yana.yana; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -482,6 +497,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = yana/yana.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = EKM7RAGNA6; @@ -514,6 +530,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 20.20.61; + OTHER_LIBTOOLFLAGS = "-framework \"SystemConfiguration\""; PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yana.yana; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -604,6 +621,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.20.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4C4C912F2DE864F000384527 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + package = 4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; + productName = ComposableArchitecture; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 4C3E65172DB61F7A00E5A455 /* Project object */; } diff --git a/yana.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/yana.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..830c524 --- /dev/null +++ b/yana.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,132 @@ +{ + "originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8", + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "5928286acce13def418ec36d05a001a9641086f2", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25", + "version" : "1.7.0" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", + "version" : "1.0.6" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture", + "state" : { + "revision" : "6574de2396319a58e86e2178577268cb4aeccc30", + "version" : "1.20.2" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596", + "version" : "1.9.2" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-navigation", + "state" : { + "revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4", + "version" : "2.3.0" + } + }, + { + "identity" : "swift-perception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-perception", + "state" : { + "revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-sharing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-sharing", + "state" : { + "revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9", + "version" : "2.5.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" + } + } + ], + "version" : 3 +} diff --git a/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved b/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..830c524 --- /dev/null +++ b/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,132 @@ +{ + "originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8", + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "5928286acce13def418ec36d05a001a9641086f2", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25", + "version" : "1.7.0" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", + "version" : "1.0.6" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture", + "state" : { + "revision" : "6574de2396319a58e86e2178577268cb4aeccc30", + "version" : "1.20.2" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596", + "version" : "1.9.2" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-navigation", + "state" : { + "revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4", + "version" : "2.3.0" + } + }, + { + "identity" : "swift-perception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-perception", + "state" : { + "revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-sharing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-sharing", + "state" : { + "revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9", + "version" : "2.5.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" + } + } + ], + "version" : 3 +} diff --git a/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index bcb484c..00abd21 100644 --- a/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -68,38 +68,6 @@ landmarkType = "7"> - - - - - - - - + startingLineNumber = "521" + endingLineNumber = "521" + landmarkName = "unknown" + landmarkType = "0"> + startingLineNumber = "328" + endingLineNumber = "328" + landmarkName = "unknown" + landmarkType = "0"> + + + + + + + + diff --git a/yana/APIs/API.swift b/yana/APIs/API.swift deleted file mode 100644 index 540af6c..0000000 --- a/yana/APIs/API.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation -import Alamofire - -enum HttpRequestMethod: String { - case get = "GET" - case post = "POST" - // 可扩展其他方法 -} - -typealias HttpRequestCompletion = (Result) -> Void - -class API { - // 通用请求方法 - static func makeRequest( - route: String, - method: HttpRequestMethod, - params: [String: Any], - completion: @escaping HttpRequestCompletion - ) { - let httpMethod: HTTPMethod = { - switch method { - case .get: return .get - case .post: return .post - } - }() - - NetworkManager.shared.request(route, method: httpMethod, parameters: params) { (result: Result) in - switch result { - case .success(let data): - completion(.success(data)) - case .failure(let error): - completion(.failure(error)) - } - } - } - - // 具体接口方法示例 - static func getUserInfo(uid: String, completion: @escaping HttpRequestCompletion) { - let route = "user/get" - let params = ["uid": uid] - makeRequest(route: route, method: .get, params: params, completion: completion) - } - - static func phoneSmsCode(mobile: String, type: String, phoneAreaCode: String, completion: @escaping HttpRequestCompletion) { - let route = "sms/getCode" - let params = ["mobile": mobile, "type": type, "phoneAreaCode": phoneAreaCode] - makeRequest(route: route, method: .post, params: params, completion: completion) - } -} - -extension API //ClientConfig -{ - static func clientInit(completion: @escaping HttpRequestCompletion) { - makeRequest(route: "client/init", - method: .get, - params: [:], - completion: completion) - } -} diff --git a/yana/APIs/APICaller.swift b/yana/APIs/APICaller.swift deleted file mode 100644 index 7804650..0000000 --- a/yana/APIs/APICaller.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// APICaller.swift -// yana -// -// Created by P on 2025/5/29. -// - -import Foundation diff --git a/yana/APIs/APIConstants.swift b/yana/APIs/APIConstants.swift new file mode 100644 index 0000000..1b23734 --- /dev/null +++ b/yana/APIs/APIConstants.swift @@ -0,0 +1,26 @@ +import Foundation + +enum APIConstants { + // MARK: - Base URLs + static let baseURL = "http://beta.api.molistar.xyz" + + // MARK: - Common Headers + static let defaultHeaders: [String: String] = [ + "Content-Type": "application/json", + "Accept": "application/json", + "platform": "ios", + "version": "1.0.0" + ] + + // MARK: - Endpoints + enum Endpoints { + static let clientInit = "/client/config" + static let login = "/user/login" + } + + // MARK: - Common Parameters + static let commonParameters: [String: String] = [: +// "platform": "ios", +// "version": "1.0.0" + ] +} diff --git a/yana/APIs/APIEndpoints.swift b/yana/APIs/APIEndpoints.swift new file mode 100644 index 0000000..b4c99c3 --- /dev/null +++ b/yana/APIs/APIEndpoints.swift @@ -0,0 +1,38 @@ +import Foundation + +// MARK: - API Endpoints +enum APIEndpoint: String, CaseIterable { + case config = "/client/config" + case login = "/auth/login" + // 可以继续添加其他端点 + + var path: String { + return self.rawValue + } +} + +// MARK: - API Configuration +struct APIConfiguration { + static let baseURL = "http://beta.api.molistar.xyz" + static let timeout: TimeInterval = 30.0 + static let maxDataSize: Int = 50 * 1024 * 1024 // 50MB 限制,防止资源超限 + + // 默认请求头 + static let defaultHeaders: [String: String] = [ + "Content-Type": "application/json", + "Accept": "application/json", +// "User-Agent": "yana-iOS/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0")" + "User-Agent": "YuMi/20.20.61 (iPhone; iOS 18.3.1; Scale/3.00)", + "Accept-Language": "zh-Hant", + "Accept-Encoding": "gzip, br" + ] +} + +// MARK: - Request Models +struct LoginRequest: Codable { + let username: String + let password: String +} + +// MARK: - Empty Request +struct EmptyRequest: Codable {} diff --git a/yana/APIs/APILogger.swift b/yana/APIs/APILogger.swift new file mode 100644 index 0000000..d5ceffe --- /dev/null +++ b/yana/APIs/APILogger.swift @@ -0,0 +1,214 @@ +import Foundation + +// MARK: - API Logger +class APILogger { + enum LogLevel { + case none + case basic + case detailed + } + + #if DEBUG + static var logLevel: LogLevel = .detailed + #else + static var logLevel: LogLevel = .none + #endif + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss.SSS" + return formatter + }() + + // MARK: - Request Logging + static func logRequest(_ request: T, url: URL, body: Data?, finalHeaders: [String: String]? = nil) { + guard logLevel != .none else { return } + + let timestamp = dateFormatter.string(from: Date()) + + print("\n🚀 [API Request] [\(timestamp)] ==================") + print("📍 Endpoint: \(request.endpoint)") + print("🔗 Full URL: \(url.absoluteString)") + print("📝 Method: \(request.method.rawValue)") + print("⏰ Timeout: \(request.timeout)s") + + // 显示最终的完整 headers(包括默认 headers 和自定义 headers) + if let headers = finalHeaders, !headers.isEmpty { + if logLevel == .detailed { + print("📋 Final Headers (包括默认 + 自定义):") + for (key, value) in headers.sorted(by: { $0.key < $1.key }) { + print(" \(key): \(value)") + } + } else if logLevel == .basic { + print("📋 Headers: \(headers.count) 个 headers") + // 只显示重要的 headers + let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"] + for key in importantHeaders { + if let value = headers[key] { + print(" \(key): \(value)") + } + } + } + } else if let customHeaders = request.headers, !customHeaders.isEmpty { + print("📋 Custom Headers:") + for (key, value) in customHeaders.sorted(by: { $0.key < $1.key }) { + print(" \(key): \(value)") + } + } else { + print("📋 Headers: 使用默认 headers") + } + + if let queryParams = request.queryParameters, !queryParams.isEmpty { + print("🔍 Query Parameters:") + for (key, value) in queryParams.sorted(by: { $0.key < $1.key }) { + print(" \(key): \(value)") + } + } + + if logLevel == .detailed { + if let body = body { + print("📦 Request Body (\(body.count) bytes):") + if let jsonObject = try? JSONSerialization.jsonObject(with: body, options: []), + let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted), + let prettyString = String(data: prettyData, encoding: .utf8) { + print(prettyString) + } else if let rawString = String(data: body, encoding: .utf8) { + print(rawString) + } else { + print("Binary data") + } + } else { + print("📦 Request Body: No body") + } + + // 显示基础参数信息(仅详细模式) + if request.includeBaseParameters { + print("📱 Base Parameters: 自动注入设备和应用信息") + let baseParams = BaseRequest() + print(" Device: \(baseParams.model), OS: \(baseParams.os) \(baseParams.osVersion)") + print(" App: \(baseParams.app) v\(baseParams.appVersion)") + print(" Language: \(baseParams.acceptLanguage)") + } + } else if logLevel == .basic { + if let body = body { + print("📦 Request Body: \(formatBytes(body.count))") + } else { + print("📦 Request Body: No body") + } + + // 基础模式也显示是否包含基础参数 + if request.includeBaseParameters { + print("📱 Base Parameters: 已自动注入") + } + } + + print("=====================================") + } + + // MARK: - Response Logging + static func logResponse(data: Data, response: HTTPURLResponse, duration: TimeInterval) { + guard logLevel != .none else { return } + + let timestamp = dateFormatter.string(from: Date()) + let statusEmoji = response.statusCode < 400 ? "✅" : "❌" + + print("\n\(statusEmoji) [API Response] [\(timestamp)] ===================") + print("⏱️ Duration: \(String(format: "%.3f", duration))s") + print("📊 Status Code: \(response.statusCode)") + print("🔗 URL: \(response.url?.absoluteString ?? "Unknown")") + print("📏 Data Size: \(formatBytes(data.count))") + + if logLevel == .detailed { + print("📋 Response Headers:") + for (key, value) in response.allHeaderFields.sorted(by: { "\($0.key)" < "\($1.key)" }) { + print(" \(key): \(value)") + } + + print("📦 Response Data:") + if data.isEmpty { + print(" Empty response") + } else if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), + let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted), + let prettyString = String(data: prettyData, encoding: .utf8) { + print(prettyString) + } else if let rawString = String(data: data, encoding: .utf8) { + print(rawString) + } else { + print(" Binary data (\(data.count) bytes)") + } + } + + print("=====================================") + } + + // MARK: - Error Logging + static func logError(_ error: Error, url: URL?, duration: TimeInterval) { + guard logLevel != .none else { return } + + let timestamp = dateFormatter.string(from: Date()) + + print("\n❌ [API Error] [\(timestamp)] ======================") + print("⏱️ Duration: \(String(format: "%.3f", duration))s") + if let url = url { + print("🔗 URL: \(url.absoluteString)") + } + + if let apiError = error as? APIError { + print("🚨 API Error: \(apiError.localizedDescription)") + } else { + print("🚨 System Error: \(error.localizedDescription)") + } + + if logLevel == .detailed { + if let urlError = error as? URLError { + print("🔍 URLError Code: \(urlError.code.rawValue)") + print("🔍 URLError Localized: \(urlError.localizedDescription)") + + // 详细的网络错误分析 + switch urlError.code { + case .timedOut: + print("💡 建议:检查网络连接或增加超时时间") + case .notConnectedToInternet: + print("💡 建议:检查网络连接") + case .cannotConnectToHost: + print("💡 建议:检查服务器地址和端口") + case .resourceUnavailable: + print("💡 建议:检查 API 端点是否正确") + default: + break + } + } + print("🔍 Full Error: \(error)") + } + + print("=====================================\n") + } + + // MARK: - Decoded Response Logging + static func logDecodedResponse(_ response: T, type: T.Type) { + guard logLevel == .detailed else { return } + + let timestamp = dateFormatter.string(from: Date()) + print("🎯 [Decoded Response] [\(timestamp)] Type: \(type)") + print("=====================================\n") + } + + // MARK: - Helper Methods + private static func formatBytes(_ bytes: Int) -> String { + let formatter = ByteCountFormatter() + formatter.allowedUnits = [.useKB, .useMB] + formatter.countStyle = .file + return formatter.string(fromByteCount: Int64(bytes)) + } + + // MARK: - Performance Logging + static func logPerformanceWarning(duration: TimeInterval, threshold: TimeInterval = 5.0) { + guard logLevel != .none && duration > threshold else { return } + + let timestamp = dateFormatter.string(from: Date()) + print("\n⚠️ [Performance Warning] [\(timestamp)] ============") + print("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)") + print("💡 建议:检查网络条件或优化 API 响应") + print("================================================\n") + } +} \ No newline at end of file diff --git a/yana/APIs/APIModels.swift b/yana/APIs/APIModels.swift new file mode 100644 index 0000000..8b2a702 --- /dev/null +++ b/yana/APIs/APIModels.swift @@ -0,0 +1,137 @@ +import Foundation +import ComposableArchitecture + +// MARK: - HTTP Method +enum HTTPMethod: String, CaseIterable { + case GET = "GET" + case POST = "POST" + case PUT = "PUT" + case DELETE = "DELETE" + case PATCH = "PATCH" +} + +// MARK: - API Error Types +enum APIError: Error, Equatable { + case invalidURL + case noData + case decodingError(String) + case networkError(String) + case httpError(statusCode: Int, message: String?) + case timeout + case resourceTooLarge + case unknown(String) + + var localizedDescription: String { + switch self { + case .invalidURL: + return "无效的 URL" + case .noData: + return "没有收到数据" + case .decodingError(let message): + return "数据解析失败: \(message)" + case .networkError(let message): + return "网络错误: \(message)" + case .httpError(let statusCode, let message): + return "HTTP 错误 \(statusCode): \(message ?? "未知错误")" + case .timeout: + return "请求超时" + case .resourceTooLarge: + return "响应数据过大" + case .unknown(let message): + return "未知错误: \(message)" + } + } +} + +// MARK: - Base Request Parameters +struct BaseRequest: Codable { + let acceptLanguage: String + let os: String = "iOS" + let osVersion: String + let ispType: String + let channel: String = "molistar_enterprise" + let model: String + let deviceId: String + let appVersion: String + let app: String = "youmi" + let mcc: String? + let spType: String? + let pubSign: String + + enum CodingKeys: String, CodingKey { + case acceptLanguage = "Accept-Language" + case appVersion = "appVersion" + case os, osVersion, ispType, channel, model, deviceId + case app, mcc, spType + case pubSign = "pub_sign" + } + + init() { + // 获取系统首选语言 + self.acceptLanguage = Locale.current.languageCode ?? "en" + + // 获取系统版本 + self.osVersion = UIDevice.current.systemVersion + + // 获取设备型号 + self.model = UIDevice.current.model + + // 生成设备ID (这里使用 identifierForVendor,实际项目中可能需要更稳定的方案) + self.deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString + + // 获取应用版本 + self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" + + // 运营商相关信息(简化处理) + self.ispType = "65535" + self.mcc = nil + self.spType = nil + + // 生成签名(这里使用时间戳的 MD5,实际项目中需要根据具体签名规则) + let timestamp = String(Int(Date().timeIntervalSince1970)) + self.pubSign = timestamp.md5() + } +} + +// MARK: - API Request Protocol +protocol APIRequestProtocol { + associatedtype Response: Codable + + var endpoint: String { get } + var method: HTTPMethod { get } + var queryParameters: [String: String]? { get } + var bodyParameters: [String: Any]? { get } + var headers: [String: String]? { get } + var timeout: TimeInterval { get } + var includeBaseParameters: Bool { get } +} + +extension APIRequestProtocol { + var timeout: TimeInterval { 30.0 } + var includeBaseParameters: Bool { true } + var headers: [String: String]? { nil } +} + +// MARK: - Generic API Response +struct APIResponse: Codable { + let data: T? + let status: String? + let message: String? + let code: Int? +} + +// MARK: - String MD5 Extension +extension String { + func md5() -> String { + let data = Data(self.utf8) + let hash = data.withUnsafeBytes { bytes -> [UInt8] in + var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) + CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash) + return hash + } + return hash.map { String(format: "%02x", $0) }.joined() + } +} + +// 需要导入 CommonCrypto +import CommonCrypto diff --git a/yana/APIs/APIService.swift b/yana/APIs/APIService.swift new file mode 100644 index 0000000..f3f1bab --- /dev/null +++ b/yana/APIs/APIService.swift @@ -0,0 +1,244 @@ +import Foundation +import ComposableArchitecture + +// MARK: - API Service Protocol +protocol APIServiceProtocol { + func request(_ request: T) async throws -> T.Response +} + +// MARK: - Live API Service Implementation +struct LiveAPIService: APIServiceProtocol { + private let session: URLSession + private let baseURL: String + + init(baseURL: String = APIConfiguration.baseURL) { + self.baseURL = baseURL + + // 配置 URLSession 以防止资源超限问题 + let config = URLSessionConfiguration.default + config.timeoutIntervalForRequest = APIConfiguration.timeout + config.timeoutIntervalForResource = APIConfiguration.timeout * 2 + config.waitsForConnectivity = true + config.allowsCellularAccess = true + + // 设置数据大小限制 + config.urlCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil) + + self.session = URLSession(configuration: config) + } + + func request(_ request: T) async throws -> T.Response { + let startTime = Date() + + // 构建 URL + guard let url = buildURL(for: request) else { + throw APIError.invalidURL + } + + // 构建 URLRequest + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = request.method.rawValue + urlRequest.timeoutInterval = request.timeout + + // 设置请求头 + var headers = APIConfiguration.defaultHeaders + if let customHeaders = request.headers { + headers.merge(customHeaders) { _, new in new } + } + + for (key, value) in headers { + urlRequest.setValue(value, forHTTPHeaderField: key) + } + + // 处理请求体 + var requestBody: Data? = nil + if request.method != .GET, let bodyParams = request.bodyParameters { + do { + // 如果需要包含基础参数,则合并 + var finalBody = bodyParams + if request.includeBaseParameters { + let baseParams = BaseRequest() + let baseDict = try baseParams.toDictionary() + finalBody.merge(baseDict) { existing, _ in existing } + } + + requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: []) + urlRequest.httpBody = requestBody + } catch { + throw APIError.decodingError("请求体编码失败: \(error.localizedDescription)") + } + } + + // 记录请求日志,传递完整的 headers 信息 + APILogger.logRequest(request, url: url, body: requestBody, finalHeaders: headers) + + do { + // 发起请求 + let (data, response) = try await session.data(for: urlRequest) + let duration = Date().timeIntervalSince(startTime) + + // 检查响应 + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.networkError("无效的响应类型") + } + + // 检查数据大小 + if data.count > APIConfiguration.maxDataSize { + APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration) + throw APIError.resourceTooLarge + } + + // 记录响应日志 + APILogger.logResponse(data: data, response: httpResponse, duration: duration) + + // 性能警告 + APILogger.logPerformanceWarning(duration: duration) + + // 检查 HTTP 状态码 + guard 200...299 ~= httpResponse.statusCode else { + let errorMessage = extractErrorMessage(from: data) + throw APIError.httpError(statusCode: httpResponse.statusCode, message: errorMessage) + } + + // 检查数据是否为空 + guard !data.isEmpty else { + throw APIError.noData + } + + // 解析响应数据 + do { + let decoder = JSONDecoder() + let decodedResponse = try decoder.decode(T.Response.self, from: data) + APILogger.logDecodedResponse(decodedResponse, type: T.Response.self) + return decodedResponse + } catch { + throw APIError.decodingError("响应解析失败: \(error.localizedDescription)") + } + + } catch let error as APIError { + let duration = Date().timeIntervalSince(startTime) + APILogger.logError(error, url: url, duration: duration) + throw error + } catch { + let duration = Date().timeIntervalSince(startTime) + let apiError = mapSystemError(error) + APILogger.logError(apiError, url: url, duration: duration) + throw apiError + } + } + + // MARK: - Private Helper Methods + + private func buildURL(for request: T) -> URL? { + guard var urlComponents = URLComponents(string: baseURL + request.endpoint) else { + return nil + } + + // 处理查询参数 + var queryItems: [URLQueryItem] = [] + + // 对于 GET 请求,将基础参数添加到查询参数中 + if request.method == .GET && request.includeBaseParameters { + do { + let baseParams = BaseRequest() + let baseDict = try baseParams.toDictionary() + for (key, value) in baseDict { + queryItems.append(URLQueryItem(name: key, value: "\(value)")) + } + } catch { + print("警告:无法添加基础参数到查询字符串") + } + } + + // 添加自定义查询参数 + if let customParams = request.queryParameters { + for (key, value) in customParams { + queryItems.append(URLQueryItem(name: key, value: value)) + } + } + + if !queryItems.isEmpty { + urlComponents.queryItems = queryItems + } + + return urlComponents.url + } + + private func extractErrorMessage(from data: Data) -> String? { + guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return nil + } + + // 尝试多种可能的错误消息字段 + if let message = json["message"] as? String { + return message + } else if let error = json["error"] as? String { + return error + } else if let msg = json["msg"] as? String { + return msg + } + + return nil + } + + private func mapSystemError(_ error: Error) -> APIError { + if let urlError = error as? URLError { + switch urlError.code { + case .timedOut: + return .timeout + case .cannotConnectToHost, .notConnectedToInternet: + return .networkError(urlError.localizedDescription) + case .dataLengthExceedsMaximum: + return .resourceTooLarge + default: + return .networkError(urlError.localizedDescription) + } + } + + return .unknown(error.localizedDescription) + } +} + +// MARK: - Mock API Service (for testing) +struct MockAPIService: APIServiceProtocol { + private var mockResponses: [String: Any] = [:] + + mutating func setMockResponse(for endpoint: String, response: T) { + mockResponses[endpoint] = response + } + + func request(_ request: T) async throws -> T.Response { + // 模拟网络延迟 + try await Task.sleep(nanoseconds: 500_000_000) // 0.5 秒 + + if let mockResponse = mockResponses[request.endpoint] as? T.Response { + return mockResponse + } + + throw APIError.noData + } +} + +// MARK: - TCA Dependency Integration +private enum APIServiceKey: DependencyKey { + static let liveValue: APIServiceProtocol = LiveAPIService() + static let testValue: APIServiceProtocol = MockAPIService() +} + +extension DependencyValues { + var apiService: APIServiceProtocol { + get { self[APIServiceKey.self] } + set { self[APIServiceKey.self] = newValue } + } +} + +// MARK: - BaseRequest Dictionary Conversion +extension BaseRequest { + func toDictionary() throws -> [String: Any] { + let data = try JSONEncoder().encode(self) + guard let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + throw APIError.decodingError("无法转换基础参数为字典") + } + return dictionary + } +} \ No newline at end of file diff --git a/yana/APIs/Integration-Guide.md b/yana/APIs/Integration-Guide.md new file mode 100644 index 0000000..9551710 --- /dev/null +++ b/yana/APIs/Integration-Guide.md @@ -0,0 +1,196 @@ +# APIClient 集成指南 + +## 🔧 在其他文件中使用 APIClient + +### 方法一:直接实例化 (推荐用于测试) + +```swift +import SwiftUI + +struct ContentView: View { + @State private var isLoading = false + @State private var result = "" + + var body: some View { + VStack { + Button("测试 API") { + testAPI() + } + .disabled(isLoading) + + if isLoading { + ProgressView("请求中...") + } + + Text(result) + } + } + + private func testAPI() { + isLoading = true + + Task { + do { + // 创建 API 客户端实例 + let apiClient = LiveAPIClient( + baseURL: URL(string: "https://your-api.com")! + ) + + // 发起请求 + let response: YourResponseModel = try await apiClient.get( + path: "/your/endpoint", + queryParameters: ["key": "value"] + ) + + await MainActor.run { + result = "成功: \(response)" + isLoading = false + } + + } catch { + await MainActor.run { + result = "错误: \(error.localizedDescription)" + isLoading = false + } + } + } + } +} +``` + +### 方法二:使用单例模式 + +```swift +// 在需要使用的文件中 +private func makeAPIRequest() async { + do { + let response: SomeModel = try await APIClientManager.shared.get( + path: "/endpoint" + ) + // 处理响应 + } catch { + // 处理错误 + } +} +``` + +### 方法三:TCA 集成 (当 TCA 可用时) + +```swift +import ComposableArchitecture + +@Reducer +struct MyFeature { + @ObservableState + struct State: Equatable { + var data: [Item] = [] + var isLoading = false + } + + enum Action: Equatable { + case loadData + case dataLoaded([Item]) + case loadingFailed(String) + } + + @Dependency(\.apiClient) var apiClient + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .loadData: + state.isLoading = true + + return .run { send in + do { + let items: [Item] = try await apiClient.get(path: "/items") + await send(.dataLoaded(items)) + } catch { + await send(.loadingFailed(error.localizedDescription)) + } + } + + case let .dataLoaded(items): + state.isLoading = false + state.data = items + return .none + + case let .loadingFailed(error): + state.isLoading = false + // 处理错误 + return .none + } + } + } +} +``` + +## 🚨 常见问题解决 + +### 1. "Cannot find 'APIClientManager' in scope" + +**原因**: Swift 模块系统问题,需要确保正确导入 + +**解决方案**: +```swift +// 方案 A: 直接实例化 +let apiClient = LiveAPIClient(baseURL: URL(string: "https://api.com")!) + +// 方案 B: 确保文件在同一个 target 中 +// 检查 Xcode 项目设置,确保 APICaller.swift 和使用文件在同一个 target + +// 方案 C: 使用原生 URLSession (临时方案) +let (data, _) = try await URLSession.shared.data(for: request) +``` + +### 2. TCA 导入问题 + +**原因**: ComposableArchitecture 模块配置问题 + +**解决方案**: +1. 检查 Package Dependencies 是否正确添加 +2. 确保 target 正确链接了 TCA +3. Clean Build Folder (⌘+Shift+K) +4. 重新构建项目 + +### 3. 网络请求失败 + +**常见错误处理**: +```swift +do { + let response = try await apiClient.get(path: "/endpoint") + // 成功处理 +} catch let urlError as URLError { + switch urlError.code { + case .notConnectedToInternet: + print("网络不可用") + case .timedOut: + print("请求超时") + case .cannotFindHost: + print("无法找到服务器") + default: + print("网络错误: \(urlError.localizedDescription)") + } +} catch { + print("其他错误: \(error)") +} +``` + +## 📝 最佳实践 + +1. **错误处理**: 始终使用 do-catch 处理异步请求 +2. **主线程更新**: 使用 `await MainActor.run` 更新 UI +3. **取消请求**: 在适当时候取消不需要的请求 +4. **重试机制**: 对于网络错误实现重试逻辑 +5. **缓存策略**: 考虑实现适当的缓存机制 + +## 🔄 迁移步骤 + +从原生 URLSession 迁移到 APIClient: + +1. **保留现有代码** - 确保功能正常 +2. **逐步替换** - 一个接口一个接口地迁移 +3. **测试验证** - 每次迁移后进行测试 +4. **清理代码** - 移除不需要的原生实现 + +这样可以确保平滑的迁移过程,避免破坏现有功能。 \ No newline at end of file diff --git a/yana/AppDelegate.swift b/yana/AppDelegate.swift index 6fa6bfb..c9cf127 100644 --- a/yana/AppDelegate.swift +++ b/yana/AppDelegate.swift @@ -3,7 +3,32 @@ import NIMSDK class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - NIMConfigurationManager.setupNimSDK() + + + // 开启网络监控 +// NetworkManager.shared.networkStatusChanged = { status in +// print("🌍 网络状态更新:\(status)") +// } + +#if DEBUG + // 网络诊断 + let testURL = URL(string: "http://beta.api.molistar.xyz/client/init")! + let request = URLRequest(url: testURL) + + print("🛠 原生URLSession测试开始") + URLSession.shared.dataTask(with: request) { data, response, error in + print(""" + === 网络诊断结果 === + 响应状态码: \((response as? HTTPURLResponse)?.statusCode ?? -1) + 错误信息: \(error?.localizedDescription ?? "无") + 原始数据: \(data?.count ?? 0) bytes + ================== + """) + }.resume() +#endif + +// NIMConfigurationManager.setupNimSDK() + return true } } diff --git a/yana/Configs/AppConfig.swift b/yana/Configs/AppConfig.swift index fb85349..5c5e89d 100644 --- a/yana/Configs/AppConfig.swift +++ b/yana/Configs/AppConfig.swift @@ -33,4 +33,30 @@ struct AppConfig { static func switchEnvironment(to env: Environment) { current = env } + + // 添加调试配置 + static var enableNetworkDebug: Bool { + #if DEBUG + return true + #else + return false + #endif + } + + // 添加服务器信任配置 + static var serverTrustPolicies: [String: ServerTrustEvaluating] { + #if DEBUG + return ["beta.api.molistar.xyz": DisabledTrustEvaluator()] + #else + return ["api.hfighting.com": PublicKeysTrustEvaluator()] + #endif + } + + static var networkDebugEnabled: Bool { + #if DEBUG + return true + #else + return false + #endif + } } \ No newline at end of file diff --git a/yana/Configs/ClientConfig.swift b/yana/Configs/ClientConfig.swift index af2d197..e7d77ce 100644 --- a/yana/Configs/ClientConfig.swift +++ b/yana/Configs/ClientConfig.swift @@ -1,32 +1,51 @@ import Foundation +import UIKit // 用于设备信息 +@_exported import Alamofire // 全局导入 final class ClientConfig { static let shared = ClientConfig() private init() {} func initializeClient() { - print("开始初始化客户端") + print("✅ 开始初始化客户端 - URL: \(AppConfig.baseURL)/client/init") + callClientInitAPI() // 调用新方法 + } + + func callClientInitAPI() { + print("🆕 使用GET方法调用初始化接口") - NetworkManager.shared.enhancedRequest( - path: "client/init", - method: .get, - responseType: Data.self - ) { result in - switch result { - case .success(let response): - print("初始化成功,状态码:\(response.statusCode)") - if let data = response.data { - do { - let json = try JSONSerialization.jsonObject(with: data) - print("响应数据:\(json)") - } catch { - print("JSON解析失败:\(error)") - } - } - - case .failure(let error): - print("初始化失败:\(error.localizedDescription)") - } - } +// let queryParams = [ +// "debug": "1", +// "platform": "ios", +// "timestamp": String(Int(Date().timeIntervalSince1970)) +// ] + +// NetworkManager.shared.get( +// path: "client/init", +// queryItems: [:] +// ) { (result: Result) in +// switch result { +// case .success(let data): +// if let response = NetworkManager.lastResponse { +// print("✅ 请求成功 | 状态码: \(response.statusCode) | 数据长度: \(data.count) bytes") +// } +// if let json = try? JSONSerialization.jsonObject(with: data) { +// print("📊 响应数据:\(json)") +// } +// case .failure(let error): +// let statusCode: Int +// switch error { +// case .requestFailed(let code, _): +// statusCode = code +// case .unauthorized: +// statusCode = 401 +// case .rateLimited: +// statusCode = 429 +// default: +// statusCode = -1 +// } +// print("❌ 请求失败 | 状态码: \(statusCode) | 错误: \(error.localizedDescription)") +// } +// } } } diff --git a/yana/ContentView.swift b/yana/ContentView.swift index 309788f..d3d9dab 100644 --- a/yana/ContentView.swift +++ b/yana/ContentView.swift @@ -6,75 +6,220 @@ // import SwiftUI +import ComposableArchitecture + +// MARK: - API Response Models +struct InitResponse: Codable, Equatable { + let status: String + let message: String? + let data: InitData? +} + +struct InitData: Codable, Equatable { + let version: String? + let timestamp: Int? + let config: [String: String]? +} + +// MARK: - Local UI Log Level Enum +enum UILogLevel: String, CaseIterable { + case none = "无日志" + case basic = "基础日志" + case detailed = "详细日志" +} struct ContentView: View { - @State private var account = "" - @State private var password = "" - - #if DEBUG - init() { - _account = State(initialValue: "3184") - _password = State(initialValue: "a0d5da073d14731cc7a01ecaa17b9174") - } - #endif - @State private var isLoading = false - @State private var loginError: String? + let store: StoreOf + let initStore: StoreOf + let configStore: StoreOf + @State private var selectedLogLevel: APILogger.LogLevel = APILogger.logLevel + @State private var selectedTab = 0 var body: some View { - VStack { - // 新增测试按钮 - Button("测试初始化") { - ClientConfig.shared.initializeClient() - } - .padding(.top, 20) - - TextField("账号", text: $account) - .textFieldStyle(.roundedBorder) - .padding() - - SecureField("密码", text: $password) - .textFieldStyle(.roundedBorder) - .padding() - - Button(action: handleLogin) { - if isLoading { - ProgressView() - } else { - Text("登录") + TabView(selection: $selectedTab) { + // 原有登录界面 + VStack { + // 日志级别选择器 + VStack(alignment: .leading, spacing: 8) { + Text("日志级别:") + .font(.headline) + .foregroundColor(.primary) + + Picker("日志级别", selection: $selectedLogLevel) { + Text("无日志").tag(APILogger.LogLevel.none) + Text("基础日志").tag(APILogger.LogLevel.basic) + Text("详细日志").tag(APILogger.LogLevel.detailed) + } + .pickerStyle(SegmentedPickerStyle()) } + .padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(10) + + Spacer() + + VStack(spacing: 20) { + Text("yana") + .font(.largeTitle) + .fontWeight(.bold) + + VStack(spacing: 15) { + WithViewStore(store, observe: { $0 }) { viewStore in + TextField("账号", text: viewStore.binding( + get: \.account, + send: { LoginFeature.Action.updateAccount($0) } + )) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocorrectionDisabled(true) + + SecureField("密码", text: viewStore.binding( + get: \.password, + send: { LoginFeature.Action.updatePassword($0) } + )) + .textFieldStyle(RoundedBorderTextFieldStyle()) + } + } + .padding(.horizontal) + + WithViewStore(store, observe: { $0 }) { viewStore in + if let error = viewStore.error { + Text(error) + .foregroundColor(.red) + .font(.caption) + .multilineTextAlignment(.center) + .padding(.horizontal) + } + + VStack(spacing: 10) { + Button(action: { + viewStore.send(.login) + }) { + HStack { + if viewStore.isLoading { + ProgressView() + .scaleEffect(0.8) + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + } + Text(viewStore.isLoading ? "登录中..." : "登录") + } + .frame(maxWidth: .infinity) + .padding() + .background(viewStore.isLoading ? Color.gray : Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + .disabled(viewStore.isLoading || viewStore.account.isEmpty || viewStore.password.isEmpty) + + WithViewStore(initStore, observe: { $0 }) { initViewStore in + Button(action: { + initViewStore.send(.initialize) + }) { + HStack { + if initViewStore.isLoading { + ProgressView() + .scaleEffect(0.8) + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + } + Text(initViewStore.isLoading ? "测试中..." : "测试初始化") + } + .frame(maxWidth: .infinity) + .padding() + .background(initViewStore.isLoading ? Color.gray : Color.green) + .foregroundColor(.white) + .cornerRadius(10) + } + .disabled(initViewStore.isLoading) + + // API 测试结果显示区域 + if let response = initViewStore.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 = initViewStore.error { + Text(error) + .foregroundColor(.red) + .font(.caption) + .multilineTextAlignment(.center) + .padding() + } + } + .padding(.horizontal) + } + } + } + + Spacer() } - .disabled(isLoading) - .alert("登录错误", isPresented: .constant(loginError != nil)) { - Button("确定") { loginError = nil } - } message: { - Text(loginError ?? "") + .padding() + .tabItem { + Label("登录", systemImage: "person.circle") } + .tag(0) - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, yana!") + // 新的 API 配置测试界面 + ConfigView(store: configStore) + .tabItem { + Label("API 测试", systemImage: "network") + } + .tag(1) + } + .onChange(of: selectedLogLevel) { newValue in + APILogger.logLevel = newValue } - .padding() - } - - private func handleLogin() { - isLoading = true - NIMSessionManager.shared - .autoLogin(account: account, token: password) { error in - if let error = error { - loginError = error.localizedDescription - } else { - // 登录成功处理 - } - } -// NIMSessionManager.shared.login(account: account, token: password) { error in -// isLoading = false -// -// } } } #Preview { - ContentView() + ContentView( + store: Store( + initialState: LoginFeature.State() + ) { + LoginFeature() + }, + initStore: Store( + initialState: InitFeature.State() + ) { + InitFeature() + }, + configStore: Store( + initialState: ConfigFeature.State() + ) { + ConfigFeature() + } + ) } diff --git a/yana/Features/ConfigFeature.swift b/yana/Features/ConfigFeature.swift new file mode 100644 index 0000000..09dc5df --- /dev/null +++ b/yana/Features/ConfigFeature.swift @@ -0,0 +1,95 @@ +import Foundation +import ComposableArchitecture + +// MARK: - Config Response Model +struct ConfigResponse: Codable, Equatable { + let status: String? + let message: String? + let data: ConfigData? +} + +struct ConfigData: Codable, Equatable { + let version: String? + let features: [String]? + let settings: ConfigSettings? +} + +struct ConfigSettings: Codable, Equatable { + let enableDebug: Bool? + let apiTimeout: Int? + let maxRetries: Int? +} + +// MARK: - Config API Request +struct ConfigRequest: APIRequestProtocol { + typealias Response = ConfigResponse + + var endpoint: String { APIEndpoint.config.path } + var method: HTTPMethod { .GET } + var queryParameters: [String: String]? { nil } + var bodyParameters: [String: Any]? { nil } + var timeout: TimeInterval { 15.0 } // Config 请求使用较短超时 +} + +// MARK: - Config Feature +@Reducer +struct ConfigFeature { + @ObservableState + struct State: Equatable { + var isLoading = false + var configData: ConfigData? + var errorMessage: String? + var lastUpdated: Date? + } + + enum Action: Equatable { + case loadConfig + case configResponse(TaskResult) + case clearError + } + + @Dependency(\.apiService) var apiService + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .loadConfig: + state.isLoading = true + state.errorMessage = nil + + return .run { send in + let request = ConfigRequest() + await send(.configResponse( + TaskResult { + try await apiService.request(request) + } + )) + } + + case let .configResponse(.success(response)): + state.isLoading = false + state.lastUpdated = Date() + + if response.status == "success" { + state.configData = response.data + } else { + state.errorMessage = response.message ?? "配置加载失败" + } + return .none + + case let .configResponse(.failure(error)): + state.isLoading = false + if let apiError = error as? APIError { + state.errorMessage = apiError.localizedDescription + } else { + state.errorMessage = error.localizedDescription + } + return .none + + case .clearError: + state.errorMessage = nil + return .none + } + } + } +} \ No newline at end of file diff --git a/yana/Features/ConfigView.swift b/yana/Features/ConfigView.swift new file mode 100644 index 0000000..c93b8bf --- /dev/null +++ b/yana/Features/ConfigView.swift @@ -0,0 +1,190 @@ +import SwiftUI +import ComposableArchitecture + +struct ConfigView: View { + let store: StoreOf + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + NavigationView { + VStack(spacing: 20) { + // 标题 + Text("API 配置测试") + .font(.largeTitle) + .fontWeight(.bold) + .padding(.top) + + // 状态显示 + Group { + if viewStore.isLoading { + VStack { + ProgressView() + .scaleEffect(1.5) + Text("正在加载配置...") + .font(.headline) + .foregroundColor(.secondary) + .padding(.top, 8) + } + .frame(height: 100) + } else if let errorMessage = viewStore.errorMessage { + VStack { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 40)) + .foregroundColor(.red) + + Text("错误") + .font(.headline) + .fontWeight(.semibold) + + Text(errorMessage) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal) + + Button("清除错误") { + viewStore.send(.clearError) + } + .buttonStyle(.borderedProminent) + .padding(.top) + } + .frame(maxHeight: .infinity) + } else if let configData = viewStore.configData { + // 配置数据显示 + ScrollView { + VStack(alignment: .leading, spacing: 16) { + + if let version = configData.version { + InfoRow(title: "版本", value: version) + } + + if let features = configData.features, !features.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("功能列表") + .font(.headline) + .fontWeight(.semibold) + + ForEach(features, id: \.self) { feature in + HStack { + Circle() + .fill(Color.green) + .frame(width: 6, height: 6) + Text(feature) + .font(.body) + } + } + } + .padding() + .background(Color(.systemGray6)) + .cornerRadius(12) + } + + if let settings = configData.settings { + VStack(alignment: .leading, spacing: 8) { + Text("设置") + .font(.headline) + .fontWeight(.semibold) + + if let enableDebug = settings.enableDebug { + InfoRow(title: "调试模式", value: enableDebug ? "启用" : "禁用") + } + + if let apiTimeout = settings.apiTimeout { + InfoRow(title: "API 超时", value: "\(apiTimeout)秒") + } + + if let maxRetries = settings.maxRetries { + InfoRow(title: "最大重试次数", value: "\(maxRetries)") + } + } + .padding() + .background(Color(.systemGray6)) + .cornerRadius(12) + } + + if let lastUpdated = viewStore.lastUpdated { + Text("最后更新: \(lastUpdated, style: .time)") + .font(.caption) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .center) + } + } + .padding() + } + } else { + VStack { + Image(systemName: "gear") + .font(.system(size: 40)) + .foregroundColor(.secondary) + + Text("点击下方按钮加载配置") + .font(.headline) + .foregroundColor(.secondary) + } + .frame(maxHeight: .infinity) + } + } + + Spacer() + + // 操作按钮 + VStack(spacing: 12) { + Button(action: { + viewStore.send(.loadConfig) + }) { + HStack { + if viewStore.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(0.8) + } else { + Image(systemName: "arrow.clockwise") + } + Text(viewStore.isLoading ? "加载中..." : "加载配置") + } + } + .buttonStyle(.borderedProminent) + .disabled(viewStore.isLoading) + .frame(maxWidth: .infinity) + .frame(height: 50) + + Text("使用新的 TCA API 组件") + .font(.caption) + .foregroundColor(.secondary) + } + .padding() + } + } + .navigationBarHidden(true) + } + } +} + +// MARK: - Helper Views +struct InfoRow: View { + let title: String + let value: String + + var body: some View { + HStack { + Text(title) + .font(.body) + .fontWeight(.medium) + + Spacer() + + Text(value) + .font(.body) + .foregroundColor(.secondary) + } + } +} + +// MARK: - Preview +#Preview { + ConfigView( + store: Store(initialState: ConfigFeature.State()) { + ConfigFeature() + } + ) +} \ No newline at end of file diff --git a/yana/Features/InitFeature.swift b/yana/Features/InitFeature.swift new file mode 100644 index 0000000..ca45454 --- /dev/null +++ b/yana/Features/InitFeature.swift @@ -0,0 +1,63 @@ +import Foundation +import ComposableArchitecture + +@Reducer +struct InitFeature { + @ObservableState + struct State: Equatable { + var isLoading = false + var response: InitResponse? + var error: String? + } + + enum Action: Equatable { + case initialize + case initializeResponse(TaskResult) + } + + @Dependency(\.apiService) var apiService + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .initialize: + state.isLoading = true + state.error = nil + + return .run { send in + let request = InitRequest() + await send(.initializeResponse( + TaskResult { + try await apiService.request(request) + } + )) + } + + case let .initializeResponse(.success(response)): + state.isLoading = false + state.response = response + return .none + + case let .initializeResponse(.failure(error)): + state.isLoading = false + if let apiError = error as? APIError { + state.error = apiError.localizedDescription + } else { + state.error = error.localizedDescription + } + return .none + } + } + } +} + +// MARK: - Init API Request +struct InitRequest: APIRequestProtocol { + typealias Response = InitResponse + + var endpoint: String { APIEndpoint.config.path } + var method: HTTPMethod { .GET } + var queryParameters: [String: String]? { nil } + var bodyParameters: [String: Any]? { nil } + var timeout: TimeInterval { 15.0 } +} diff --git a/yana/Features/LoginFeature.swift b/yana/Features/LoginFeature.swift new file mode 100644 index 0000000..1f47eb2 --- /dev/null +++ b/yana/Features/LoginFeature.swift @@ -0,0 +1,83 @@ +import Foundation +import ComposableArchitecture + +struct LoginResponse: Codable, Equatable { + let status: String + let message: String? + let token: String? +} + +@Reducer +struct LoginFeature { + @ObservableState + struct State: Equatable { + var account: String = "" + var password: String = "" + var isLoading = false + var error: String? + + #if DEBUG + init() { + self.account = "3184" + self.password = "a0d5da073d14731cc7a01ecaa17b9174" + } + #endif + } + + enum Action: Equatable { + case updateAccount(String) + case updatePassword(String) + case login + case loginResponse(TaskResult) + } + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case let .updateAccount(account): + state.account = account + return .none + + case let .updatePassword(password): + state.password = password + return .none + + case .login: + state.isLoading = true + state.error = nil + + let loginBody = [ + "account": state.account, + "password": state.password + ] + + return .run { send in + do { + let response: LoginResponse = try await APIClientManager.shared.post( + path: APIConstants.Endpoints.login, + body: loginBody, + headers: APIConstants.defaultHeaders + ) + await send(.loginResponse(.success(response))) + } catch { + await send(.loginResponse(.failure(error))) + } + } + + case let .loginResponse(.success(response)): + state.isLoading = false + if response.status == "success" { + // TODO: 处理登录成功,保存 token 等 + } else { + state.error = response.message ?? "登录失败" + } + return .none + + case let .loginResponse(.failure(error)): + state.isLoading = false + state.error = error.localizedDescription + return .none + } + } + } +} \ No newline at end of file diff --git a/yana/Info.plist b/yana/Info.plist index 6a6654d..5451af2 100644 --- a/yana/Info.plist +++ b/yana/Info.plist @@ -7,5 +7,7 @@ NSAllowsArbitraryLoads + NSWiFiUsageDescription + 应用需要访问 Wi-Fi 信息以提供网络相关功能 diff --git a/yana/Managers/NIMConfigurationManager.swift b/yana/Managers/NIMConfigurationManager.swift index 19a9115..184d683 100644 --- a/yana/Managers/NIMConfigurationManager.swift +++ b/yana/Managers/NIMConfigurationManager.swift @@ -21,7 +21,9 @@ struct NIMConfigurationManager { static func setupChatSDK(with option: NIMSDKOption) { let v2Option = V2NIMSDKOption() v2Option.enableV2CloudConversation = false - IMKitClient.instance.setupIM2(option, v2Option) + // TODO: 修复 IMKitClient API 调用 + // IMKitClient.shared.setupIM2(option, v2Option) + print("⚠️ NIM SDK 配置暂时被注释,需要修复 IMKitClient API") } static func configureNIMSDKOption() -> NIMSDKOption { diff --git a/yana/Managers/NetworkManager.swift b/yana/Managers/NetworkManager.swift deleted file mode 100644 index 029e38e..0000000 --- a/yana/Managers/NetworkManager.swift +++ /dev/null @@ -1,667 +0,0 @@ -import Foundation -import Alamofire -import CoreTelephony -import UIKit -import Darwin // 用于 utsname 结构体 -import CommonCrypto - -// 配置类 -//enum AppConfig { -// static let baseURL = "https://api.example.com" // 请替换为实际的 API 基础 URL -//} - -// 网络状态枚举 -enum NetworkStatus: Int { - case notReachable = 0 - case reachableViaWWAN = 1 - case reachableViaWiFi = 2 -} - -// 扩展错误类型 -enum NetworkError: Error { - case invalidURL - case requestFailed(statusCode: Int, message: String?) - case invalidResponse - case decodingFailed - case networkUnavailable - case serverError(message: String) - case unauthorized - case rateLimited - - var localizedDescription: String { - switch self { - case .invalidURL: - return "无效的 URL" - case .requestFailed(let statusCode, let message): - return "请求失败: \(statusCode), \(message ?? "未知错误")" - case .invalidResponse: - return "无效的响应" - case .decodingFailed: - return "数据解析失败" - case .networkUnavailable: - return "网络不可用" - case .serverError(let message): - return "服务器错误: \(message)" - case .unauthorized: - return "未授权访问" - case .rateLimited: - return "请求过于频繁" - } - } -} - -// MARK: - MD5 加密扩展 -extension String { - func md5() -> String { - let str = self.cString(using: .utf8) - let strLen = CUnsignedInt(self.lengthOfBytes(using: .utf8)) - let digestLen = Int(CC_MD5_DIGEST_LENGTH) - let result = UnsafeMutablePointer.allocate(capacity: digestLen) - - CC_MD5(str!, strLen, result) - - let hash = NSMutableString() - for i in 0.. { - let statusCode: Int - let data: T? - let headers: [AnyHashable: Any] - let metrics: URLSessionTaskMetrics? - - var isSuccessful: Bool { - return (200...299).contains(statusCode) - } - } - - private let reachability = NetworkReachabilityManager() - private var isNetworkReachable = false - private let baseURL = AppConfig.baseURL - private let session: Session - private let retryLimit = 2 - - // 网络状态监听回调 - var networkStatusChanged: ((NetworkStatus) -> Void)? - - init() { - let configuration = URLSessionConfiguration.af.default - configuration.httpShouldSetCookies = true - configuration.httpCookieAcceptPolicy = .always - configuration.timeoutIntervalForRequest = 60 - configuration.timeoutIntervalForResource = 60 - configuration.httpMaximumConnectionsPerHost = 10 - - // 支持的内容类型 - configuration.httpAdditionalHeaders = [ - "Accept": "application/json, text/json, text/javascript, text/html, text/plain, image/jpeg, image/png", - "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json" - ] - - // 强制 TLS 1.2+ 并禁用 HTTP/1.1 - configuration.tlsMinimumSupportedProtocolVersion = .TLSv12 - configuration.httpShouldUsePipelining = true - - // 重试策略 - let retrier = RetryPolicy(retryLimit: UInt(retryLimit)) - - session = Session( - configuration: configuration, - interceptor: retrier, - eventMonitors: [AlamofireLogger()] - ) - - // 添加网络可达性监听 - setupReachability() - } - - private func setupReachability() { - reachability?.startListening { [weak self] status in - guard let self = self else { return } - - switch status { - case .reachable(.ethernetOrWiFi): - self.isNetworkReachable = true - self.networkStatusChanged?(.reachableViaWiFi) - case .reachable(.cellular): - self.isNetworkReachable = true - self.networkStatusChanged?(.reachableViaWWAN) - case .notReachable: - self.isNetworkReachable = false - self.networkStatusChanged?(.notReachable) - case .unknown: - self.isNetworkReachable = false - self.networkStatusChanged?(.notReachable) - case .reachable(_): - self.isNetworkReachable = true - self.networkStatusChanged?(.reachableViaWiFi) - @unknown default: - fatalError("未知的网络状态") - } - } - } - - // MARK: - 便利方法 - - /// 发送 GET 请求 - func get( - path: String, - queryItems: [String: String]? = nil, - completion: @escaping (Result) -> Void - ) { - enhancedRequest( - path: path, - method: .get, - queryItems: queryItems, - responseType: T.self - ) { result in - switch result { - case .success(let response): - if let data = response.data as? T { - completion(.success(data)) - } else { - completion(.failure(.decodingFailed)) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - /// 发送 POST 请求 - func post( - path: String, - parameters: P, - completion: @escaping (Result) -> Void - ) { - enhancedRequest( - path: path, - method: .post, - bodyParameters: parameters, - responseType: T.self - ) { result in - switch result { - case .success(let response): - if let data = response.data as? T { - completion(.success(data)) - } else { - completion(.failure(.decodingFailed)) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - // MARK: - 核心请求方法 - func enhancedRequest( - path: String, - method: HTTPMethod = .get, - queryItems: [String: String]? = nil, - bodyParameters: Encodable? = nil, - responseType: T.Type = Data.self, - completion: @escaping (Result, NetworkError>) -> Void - ) { - // 前置网络检查 - guard isNetworkReachable else { - completion(.failure(.networkUnavailable)) - return - } - - guard let baseURL = URL(string: baseURL) else { - completion(.failure(.invalidURL)) - return - } - - var urlComponents = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: true) - urlComponents?.queryItems = queryItems?.map { URLQueryItem(name: $0.key, value: $0.value) } - - guard let finalURL = urlComponents?.url else { - completion(.failure(.invalidURL)) - return - } - - // 合并基础参数和自定义参数 - // TODO: 补充加密验参:pub_sign - let baseParams = BaseParameters() - var parameters: Parameters = baseParams.dictionary ?? [:] - - if let customParams = bodyParameters { - if let dict = try? customParams.asDictionary() { - parameters.merge(dict) { (_, new) in new } - } - } - - session.request(finalURL, method: method, parameters: parameters, encoding: JSONEncoding.default, headers: commonHeaders) - .validate() - .responseData { [weak self] response in - guard let self = self else { return } - - let statusCode = response.response?.statusCode ?? -1 - let headers = response.response?.allHeaderFields ?? [:] - let metrics = response.metrics - - switch response.result { - case .success(let decodedData): - do { - let resultData: T - if T.self == Data.self { - resultData = decodedData as! T - } else if let decodableData = decodedData as? T { - resultData = decodableData - } else { - throw AFError.responseSerializationFailed(reason: .decodingFailed(error: NetworkError.decodingFailed)) - } - - let networkResponse = NetworkResponse( - statusCode: statusCode, - data: resultData, - headers: headers, - metrics: metrics - ) - - if networkResponse.isSuccessful { - completion(.success(networkResponse)) - } else { - self.handleErrorResponse(statusCode: statusCode, completion: completion) - } - } catch { - completion(.failure(.decodingFailed)) - } - - case .failure(let error): - self.handleRequestError(error, statusCode: statusCode, completion: completion) - } - } - } - - // MARK: - 错误处理 - - private func handleErrorResponse( - statusCode: Int, - completion: (Result, NetworkError>) -> Void - ) { - switch statusCode { - case 401: - completion(.failure(.unauthorized)) - case 429: - completion(.failure(.rateLimited)) - case 500...599: - completion(.failure(.serverError(message: "服务器错误 \(statusCode)"))) - default: - completion(.failure(.requestFailed(statusCode: statusCode, message: "请求失败"))) - } - } - - private func handleRequestError( - _ error: AFError, - statusCode: Int, - completion: (Result, NetworkError>) -> Void - ) { - if let underlyingError = error.underlyingError as? URLError { - switch underlyingError.code { - case .notConnectedToInternet: - completion(.failure(.networkUnavailable)) - default: - completion(.failure(.requestFailed( - statusCode: statusCode, - message: underlyingError.localizedDescription - ))) - } - } else { - completion(.failure(.invalidResponse)) - } - } - - // MARK: - 公共头信息 - private var commonHeaders: HTTPHeaders { - var headers = HTTPHeaders() - - // 公共头信息 - if let language = Locale.preferredLanguages.first { - headers.add(name: "Accept-Language", value: language) - } - headers.add(name: "App-Version", value: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0") - - // 登录相关头信息 - let uid = "" //AccountInfoStorage.instance?.getUid() ?? "" - let ticket = "" //AccountInfoStorage.instance?.getTicket() ?? "" - headers.add(name: "pub_uid", value: uid) - headers.add(name: "pub_ticket", value: ticket) - - return headers - } - - // MARK: - 公共请求方法 - func request( - _ path: String, - method: HTTPMethod = .get, - parameters: P? = nil, - completion: @escaping (Result) -> Void - ) { - enhancedRequest( - path: path, - method: method, - bodyParameters: parameters, - responseType: T.self - ) { result in - switch result { - case .success(let response): - if let data = response.data { - completion(.success(data)) - } else { - completion(.failure(.decodingFailed)) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - // 添加一个便利方法,用于处理字典参数 - func request( - _ path: String, - method: HTTPMethod = .get, - parameters: [String: Any]? = nil, - completion: @escaping (Result) -> Void - ) { - // 将字典转换为 Data,然后再解码为 [String: String] - if let params = parameters { - do { - let jsonData = try JSONSerialization.data(withJSONObject: params) - let decoder = JSONDecoder() - let encodableParams = try decoder.decode([String: String].self, from: jsonData) - - request(path, method: method, parameters: encodableParams, completion: completion) - } catch { - completion(.failure(.decodingFailed)) - } - } else { - // 如果没有参数,使用空字典 - request(path, method: method, parameters: [String: String](), completion: completion) - } - } - - // MARK: - 语言管理 - func getCurrentLanguage() -> String { - return LanguageManager.getCurrentLanguage() - } - - func updateLanguage(_ language: String) { - LanguageManager.updateLanguage(language) - } -} - -// MARK: - Logger -final class AlamofireLogger: EventMonitor { - func requestDidResume(_ request: Request) { - let allHeaders = request.request?.allHTTPHeaderFields ?? [:] - let relevantHeaders = allHeaders.filter { !$0.key.contains("Authorization") } - - print("🚀 Request Started: \(request.description)") - print("📝 Headers: \(relevantHeaders)") - - if let httpBody = request.request?.httpBody, - let parameters = try? JSONSerialization.jsonObject(with: httpBody) { - print("📦 Parameters: \(parameters)") - } - } - - func request(_ request: DataRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, data: Data?) { - print("📥 Response Status: \(response.statusCode)") - - if let data = data, - let json = try? JSONSerialization.jsonObject(with: data) { - print("📄 Response Data: \(json)") - } - } -} - -// MARK: - Encodable Extension -private extension Encodable { - var dictionary: [String: Any]? { - guard let data = try? JSONEncoder().encode(self) else { return nil } - return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] } - } - - func asDictionary() throws -> [String: Any] { - let data = try JSONEncoder().encode(self) - guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { - throw NetworkError.decodingFailed - } - return dictionary - } -} - -// MARK: - 语言管理 -enum LanguageManager { - static let languageKey = "UserSelectedLanguage" - - // 映射语言代码 - static func mapLanguage(_ language: String) -> String { - // 处理完整的语言代码,如 "zh-Hans"、"zh-Hant"、"zh-HK" 等 - if language.hasPrefix("zh-Hans") || language.hasPrefix("zh-CN") { - return "zh-Hant" // 简体中文也返回繁体 - } else if language.hasPrefix("zh") { - return "zh-Hant" // 其他中文变体都返回繁体 - } else if language.hasPrefix("ar") { - return "ar" // 阿拉伯语 - } else if language.hasPrefix("tr") { - return "tr" // 土耳其语 - } else { - return "en" // 默认英文 - } - } - - // 获取当前语言 - static func getCurrentLanguage() -> String { - // 先从 UserDefaults 获取 - if let savedLanguage = UserDefaults.standard.string(forKey: languageKey) { - return savedLanguage - } - - // 获取系统首选语言 - let preferredLanguages = Locale.preferredLanguages.first -// let systemLanguage = preferredLanguages.first ?? Locale.current.languageCode ?? "en" - - // 映射并保存语言设置 - let mappedLanguage = mapLanguage(preferredLanguages ?? "en") - UserDefaults.standard.set(mappedLanguage, forKey: languageKey) - UserDefaults.standard.synchronize() - - return mappedLanguage - } - - // 更新语言设置 - static func updateLanguage(_ language: String) { - let mappedLanguage = mapLanguage(language) - UserDefaults.standard.set(mappedLanguage, forKey: languageKey) - UserDefaults.standard.synchronize() - } - - // 获取系统语言代码(调试用) - static func getSystemLanguageInfo() -> (preferred: [String], current: String?) { - return ( - Bundle.main.preferredLocalizations, - Locale.current.languageCode - ) - } -} - -// MARK: - 设备型号管理 -enum DeviceManager { - // 获取设备标识符 - static func getDeviceIdentifier() -> String { - var systemInfo = utsname() - uname(&systemInfo) - let machineMirror = Mirror(reflecting: systemInfo.machine) - let identifier = machineMirror.children.reduce("") { identifier, element in - guard let value = element.value as? Int8, value != 0 else { return identifier } - return identifier + String(UnicodeScalar(UInt8(value))) - } - return identifier - } - - // 映射设备标识符到具体型号 - static func mapDeviceModel(_ identifier: String) -> String { - switch identifier { - // iPhone - case "iPhone13,1": return "iPhone 12 mini" - case "iPhone13,2": return "iPhone 12" - case "iPhone13,3": return "iPhone 12 Pro" - case "iPhone13,4": return "iPhone 12 Pro Max" - case "iPhone14,4": return "iPhone 13 mini" - case "iPhone14,5": return "iPhone 13" - case "iPhone14,2": return "iPhone 13 Pro" - case "iPhone14,3": return "iPhone 13 Pro Max" - case "iPhone14,7": return "iPhone 14" - case "iPhone14,8": return "iPhone 14 Plus" - case "iPhone15,2": return "iPhone 14 Pro" - case "iPhone15,3": return "iPhone 14 Pro Max" - case "iPhone15,4": return "iPhone 15" - case "iPhone15,5": return "iPhone 15 Plus" - case "iPhone16,1": return "iPhone 15 Pro" - case "iPhone16,2": return "iPhone 15 Pro Max" - - // iPad - case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)" - case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro 12.9-inch (5th generation)" - case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11": return "iPad Pro 12.9-inch (6th generation)" - - // iPod - case "iPod9,1": return "iPod touch (7th generation)" - - // 模拟器 - case "i386", "x86_64", "arm64": return "Simulator \(mapDeviceModel(ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))" - - default: return identifier // 如果找不到对应的型号,返回原始标识符 - } - } - - // 获取具体的设备型号 - static func getDeviceModel() -> String { - let identifier = getDeviceIdentifier() - return mapDeviceModel(identifier) - } -} - -// MARK: - 渠道管理 -enum ChannelManager { - static let enterpriseBundleId = "com.stupidmonkey.yana.yana"//"com.hflighting.yumi" - - enum ChannelType: String { - case enterprise = "enterprise" - case testflight = "testflight" - case appstore = "appstore" - } - - // 判断是否企业版 - static func isEnterprise() -> Bool { - let bundleId = Bundle.main.bundleIdentifier ?? "" - return bundleId == enterpriseBundleId - } - - // 判断是否 TestFlight 版本 - static func isTestFlight() -> Bool { - return Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" - } - - // 获取当前渠道 - static func getCurrentChannel() -> String { - if isEnterprise() { - return ChannelType.enterprise.rawValue - } else if isTestFlight() { - return ChannelType.testflight.rawValue - } else { - return ChannelType.appstore.rawValue - } - } -} diff --git a/yana/yana.entitlements b/yana/yana.entitlements index 0c67376..c9a86ce 100644 --- a/yana/yana.entitlements +++ b/yana/yana.entitlements @@ -1,5 +1,8 @@ - + + com.apple.external-accessory.wireless-configuration + + diff --git a/yana/yanaApp.swift b/yana/yanaApp.swift index 7115b36..48045f2 100644 --- a/yana/yanaApp.swift +++ b/yana/yanaApp.swift @@ -6,7 +6,7 @@ // import SwiftUI -import NIMSDK +import ComposableArchitecture @main struct yanaApp: App { @@ -14,7 +14,23 @@ struct yanaApp: App { var body: some Scene { WindowGroup { - ContentView() + ContentView( + store: Store( + initialState: LoginFeature.State() + ) { + LoginFeature() + }, + initStore: Store( + initialState: InitFeature.State() + ) { + InitFeature() + }, + configStore: Store( + initialState: ConfigFeature.State() + ) { + ConfigFeature() + } + ) } } }