diff --git a/API-README.md b/API-README.md new file mode 100644 index 0000000..44e8000 --- /dev/null +++ b/API-README.md @@ -0,0 +1,626 @@ +# Yana iOS API 使用指南 + +## 📋 目录 +- [架构概览](#架构概览) +- [快速开始](#快速开始) +- [环境配置](#环境配置) +- [请求方式](#请求方式) +- [错误处理](#错误处理) +- [安全机制](#安全机制) +- [最佳实践](#最佳实践) +- [API接口列表](#api接口列表) +- [示例代码](#示例代码) + +## 🏗️ 架构概览 + +Yana iOS 项目采用基于 **TCA (The Composable Architecture)** 的现代化 API 架构设计: + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ SwiftUI View │───▶│ TCA Reducer │───▶│ API Service │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │ App State │ │ Network Layer │ + └─────────────────┘ └─────────────────┘ +``` + +### 核心组件 + +- **APIService**: 网络请求核心服务 +- **APIModels**: 数据模型和协议定义 +- **APIEndpoints**: API 端点配置 +- **APILogger**: 请求日志记录 +- **BaseRequest**: 基础请求参数管理 + +## 🚀 快速开始 + +### 1. 基本使用 + +```swift +import SwiftUI + +struct ContentView: View { + @State private var isLoading = false + @State private var result = "" + + var body: some View { + VStack { + Button("发起 API 请求") { + Task { + await makeAPIRequest() + } + } + .disabled(isLoading) + + if isLoading { + ProgressView("请求中...") + } + + Text(result) + } + } + + private func makeAPIRequest() async { + isLoading = true + + do { + // 创建 API 服务实例 + let apiService = LiveAPIService() + + // 创建请求 + let request = ConfigRequest() + + // 发起请求 + let response = try await apiService.request(request) + + await MainActor.run { + result = "请求成功: \(response)" + isLoading = false + } + + } catch { + await MainActor.run { + result = "请求失败: \(error.localizedDescription)" + isLoading = false + } + } + } +} +``` + +### 2. TCA 集成使用 + +```swift +import ComposableArchitecture + +@Reducer +struct APIFeature { + @ObservableState + struct State: Equatable { + var data: ConfigResponse? + var isLoading = false + var errorMessage: String? + } + + enum Action: Equatable { + case loadConfig + case configLoaded(ConfigResponse) + case loadingFailed(String) + } + + @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 + do { + let request = ConfigRequest() + let response = try await apiService.request(request) + await send(.configLoaded(response)) + } catch { + await send(.loadingFailed(error.localizedDescription)) + } + } + + case let .configLoaded(response): + state.isLoading = false + state.data = response + return .none + + case let .loadingFailed(error): + state.isLoading = false + state.errorMessage = error + return .none + } + } + } +} +``` + +## ⚙️ 环境配置 + +### 服务器环境 + +| 环境 | 地址 | 说明 | +|------|------|------| +| 测试环境 | `http://beta.api.molistar.xyz` | 开发测试服务器 | +| 生产环境 | `https://api.hfighting.com` | 正式服务器 | + +### 配置参数 + +```swift +struct APIConfiguration { + static let baseURL = "http://beta.api.molistar.xyz" + static let timeout: TimeInterval = 30.0 + static let maxDataSize: Int = 50 * 1024 * 1024 // 50MB +} +``` + +### 默认请求头 + +所有请求都会自动添加以下请求头: + +```swift +static var defaultHeaders: [String: String] { + return [ + "Content-Type": "application/json", + "Accept": "application/json", + "Accept-Encoding": "gzip, br", + "Accept-Language": Locale.current.languageCode ?? "en", + "App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" + ] +} +``` + +## 📡 请求方式 + +### 1. GET 请求 + +```swift +struct ConfigRequest: APIRequestProtocol { + typealias Response = ConfigResponse + + let endpoint = "/client/config" + let method: HTTPMethod = .GET + let includeBaseParameters = true + let queryParameters: [String: String]? = nil + let bodyParameters: [String: Any]? = nil + let headers: [String: String]? = nil + let timeout: TimeInterval = 30.0 +} +``` + +### 2. POST 请求 + +```swift +struct LoginRequest: APIRequestProtocol { + typealias Response = LoginResponse + + let endpoint = "/auth/login" + let method: HTTPMethod = .POST + let includeBaseParameters = true + let queryParameters: [String: String]? = nil + let bodyParameters: [String: Any]? + let headers: [String: String]? = nil + let timeout: TimeInterval = 30.0 + + init(username: String, password: String) { + self.bodyParameters = [ + "username": username, + "password": password + ] + } +} +``` + +### 3. 基础参数说明 + +每个请求都会自动包含以下基础参数: + +```swift +struct BaseRequest { + let acceptLanguage: String // 用户语言偏好 + let os: String = "iOS" // 操作系统类型 + let osVersion: String // 系统版本号 + let netType: Int // 网络类型 (WiFi=2, 蜂窝=1) + let ispType: String // 运营商类型 + let channel: String // 应用分发渠道 + let model: String // 设备型号 + let deviceId: String // 设备唯一标识 + let appVersion: String // 应用版本 + let app: String // 应用名称 + let lang: String // 语言代码 + let mcc: String? // 移动国家代码 + let pubSign: String // 安全签名 +} +``` + +## ❌ 错误处理 + +### 错误类型 + +```swift +enum APIError: Error, Equatable { + case invalidURL // 无效的 URL + case noData // 没有收到数据 + case decodingError(String) // 数据解析失败 + case networkError(String) // 网络错误 + case httpError(statusCode: Int, message: String?) // HTTP 错误 + case timeout // 请求超时 + case resourceTooLarge // 响应数据过大 + case unknown(String) // 未知错误 +} +``` + +### 错误处理示例 + +```swift +do { + let response = try await apiService.request(request) + // 处理成功响应 +} catch let apiError as APIError { + switch apiError { + case .networkError(let message): + print("网络错误: \(message)") + case .timeout: + print("请求超时,请检查网络连接") + case .httpError(let statusCode, let message): + print("服务器错误 \(statusCode): \(message ?? "未知错误")") + case .decodingError(let message): + print("数据解析失败: \(message)") + default: + print("其他错误: \(apiError.localizedDescription)") + } +} catch { + print("未知错误: \(error)") +} +``` + +## 🔐 安全机制 + +### 签名生成流程 + +1. **参数过滤**: 移除系统级参数 +2. **参数排序**: 按字典 key 升序排序 +3. **字符串拼接**: `"key0=value0&key1=value1"` +4. **添加密钥**: 拼接 `key=rpbs6us1m8r2j9g6u06ff2bo18orwaya` +5. **MD5加密**: 生成大写 MD5 签名 + +### 认证头部 + +```swift +// 用户认证相关头部(如果用户已登录) +if let userId = UserInfoManager.getCurrentUserId() { + headers["pub_uid"] = userId +} + +if let userTicket = UserInfoManager.getCurrentUserTicket() { + headers["pub_ticket"] = userTicket +} +``` + +## 💡 最佳实践 + +### 1. 错误处理 + +```swift +// ✅ 推荐:完整的错误处理 +do { + let response = try await apiService.request(request) + // 处理成功响应 +} catch let urlError as URLError { + switch urlError.code { + case .notConnectedToInternet: + showAlert("网络不可用,请检查网络连接") + case .timedOut: + showAlert("请求超时,请重试") + default: + showAlert("网络错误: \(urlError.localizedDescription)") + } +} catch let apiError as APIError { + showAlert(apiError.localizedDescription) +} catch { + showAlert("未知错误: \(error)") +} +``` + +### 2. 主线程更新 UI + +```swift +// ✅ 推荐:使用 MainActor 更新 UI +await MainActor.run { + self.isLoading = false + self.data = response +} +``` + +### 3. 取消请求 + +```swift +// ✅ 推荐:支持取消的请求 +struct ContentView: View { + @State private var task: Task? + + private func makeRequest() { + task = Task { + do { + let response = try await apiService.request(request) + // 处理响应 + } catch { + if !Task.isCancelled { + // 处理错误 + } + } + } + } + + private func cancelRequest() { + task?.cancel() + } +} +``` + +### 4. 重试机制 + +```swift +// ✅ 推荐:实现重试逻辑 +func requestWithRetry( + _ request: T, + maxRetries: Int = 3 +) async throws -> T.Response { + var lastError: Error? + + for attempt in 0..) + } + + @Dependency(\.apiService) var apiService + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case let .usernameChanged(username): + state.username = username + return .none + + case let .passwordChanged(password): + state.password = password + return .none + + case .loginButtonTapped: + state.isLoading = true + state.errorMessage = nil + + let request = LoginRequest( + username: state.username, + password: state.password + ) + + return .run { send in + do { + let response = try await apiService.request(request) + await send(.loginResponse(.success(response))) + } catch let error as APIError { + await send(.loginResponse(.failure(error))) + } catch { + await send(.loginResponse(.failure(.unknown(error.localizedDescription)))) + } + } + + case let .loginResponse(.success(response)): + state.isLoading = false + state.isLoggedIn = response.success + if !response.success { + state.errorMessage = response.message + } + return .none + + case let .loginResponse(.failure(error)): + state.isLoading = false + state.errorMessage = error.localizedDescription + return .none + } + } + } +} + +// MARK: - Login View +struct LoginView: View { + let store: StoreOf + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack(spacing: 20) { + TextField("用户名", text: viewStore.binding( + get: \.username, + send: LoginFeature.Action.usernameChanged + )) + .textFieldStyle(RoundedBorderTextFieldStyle()) + + SecureField("密码", text: viewStore.binding( + get: \.password, + send: LoginFeature.Action.passwordChanged + )) + .textFieldStyle(RoundedBorderTextFieldStyle()) + + Button("登录") { + viewStore.send(.loginButtonTapped) + } + .disabled(viewStore.isLoading || viewStore.username.isEmpty || viewStore.password.isEmpty) + + if viewStore.isLoading { + ProgressView("登录中...") + } + + if let errorMessage = viewStore.errorMessage { + Text(errorMessage) + .foregroundColor(.red) + } + } + .padding() + } + } +} + +// MARK: - Request & Response Models +struct LoginRequest: APIRequestProtocol { + typealias Response = LoginResponse + + let endpoint = "/auth/login" + let method: HTTPMethod = .POST + let includeBaseParameters = true + let queryParameters: [String: String]? = nil + let bodyParameters: [String: Any]? + let headers: [String: String]? = nil + let timeout: TimeInterval = 30.0 + + init(username: String, password: String) { + self.bodyParameters = [ + "username": username, + "password": password + ] + } +} + +struct LoginResponse: Codable { + let success: Bool + let message: String? + let data: UserData? +} + +struct UserData: Codable { + let userId: String + let username: String + let token: String +} +``` + +## 🔧 调试和日志 + +### 启用详细日志 + +API 请求和响应会自动记录到控制台,包括: + +- 请求 URL 和参数 +- 请求头信息 +- 响应状态码和数据 +- 请求耗时 +- 错误信息 + +### 日志示例 + +``` +🚀 API Request: GET /client/config +📋 Headers: ["Content-Type": "application/json", "Accept": "application/json"] +📊 Query Parameters: ["deviceId": "ABC123", "appVersion": "1.0.0"] + +✅ API Response: 200 OK (0.45s) +📦 Response Size: 1.2KB +📄 Response Data: {"success": true, "data": {...}} +``` + +## 📚 相关文档 + +- [API 规则详解](yana/APIs/API%20rule.md) +- [集成指南](yana/APIs/Integration-Guide.md) +- [TCA 官方文档](https://github.com/pointfreeco/swift-composable-architecture) + +## 🤝 贡献指南 + +1. 遵循现有的代码风格和架构模式 +2. 为新的 API 接口添加完整的文档和示例 +3. 确保所有请求都包含适当的错误处理 +4. 添加单元测试覆盖新功能 +5. 更新相关文档 + +--- + +**注意**: 本文档基于当前项目架构编写,如有架构变更请及时更新文档内容。 \ No newline at end of file diff --git a/Podfile b/Podfile index 4f9b28e..c6c17cb 100644 --- a/Podfile +++ b/Podfile @@ -7,13 +7,13 @@ target 'yana' do # Pods for yana - # IM 即时通讯 - pod 'NIMSDK_LITE' - # 基础库 - pod 'NEChatKit', '10.6.1' - pod 'NEChatUIKit', '10.6.1' # 会话(聊天)组件 - pod 'NEContactUIKit', '10.6.1' # 通讯录组件 - pod 'NELocalConversationUIKit', '10.6.1' # 本地会话列表组件。 +# # IM 即时通讯 +# pod 'NIMSDK_LITE' +# # 基础库 +# pod 'NEChatKit', '10.6.1' +# pod 'NEChatUIKit', '10.6.1' # 会话(聊天)组件 +# pod 'NEContactUIKit', '10.6.1' # 通讯录组件 +# pod 'NELocalConversationUIKit', '10.6.1' # 本地会话列表组件。 # Networks pod 'Alamofire' @@ -37,4 +37,4 @@ post_install do |installer| end end end -end \ No newline at end of file +end diff --git a/Podfile.lock b/Podfile.lock index 9a45253..4dc3a44 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,127 +1,16 @@ PODS: - Alamofire (5.10.2) - - CocoaLumberjack (3.8.5): - - CocoaLumberjack/Core (= 3.8.5) - - CocoaLumberjack/Core (3.8.5) - - libwebp (1.5.0): - - libwebp/demux (= 1.5.0) - - libwebp/mux (= 1.5.0) - - libwebp/sharpyuv (= 1.5.0) - - libwebp/webp (= 1.5.0) - - libwebp/demux (1.5.0): - - libwebp/webp - - libwebp/mux (1.5.0): - - libwebp/demux - - libwebp/sharpyuv (1.5.0) - - libwebp/webp (1.5.0): - - libwebp/sharpyuv - - MJRefresh (3.7.5) - - NEChatKit (10.6.1): - - NEChatKit/NOS (= 10.6.1) - - NEChatKit/NOS (10.6.1): - - NECommonKit (= 9.7.2) - - NECoreIM2Kit/NOS (= 1.0.9) - - NEChatUIKit (10.6.1): - - NEChatUIKit/NOS (= 10.6.1) - - NEChatUIKit/NOS (10.6.1): - - MJRefresh (= 3.7.5) - - NEChatKit/NOS - - NECommonUIKit (= 9.7.6) - - SDWebImageSVGKitPlugin - - SDWebImageWebPCoder - - NECommonKit (9.7.2): - - YXAlog - - NECommonUIKit (9.7.6): - - NECommonKit - - SDWebImage - - NEContactUIKit (10.6.1): - - NEContactUIKit/NOS (= 10.6.1) - - NEContactUIKit/NOS (10.6.1): - - MJRefresh (= 3.7.5) - - NEChatKit/NOS - - NECommonUIKit (= 9.7.6) - - NECoreIM2Kit/NOS (1.0.9): - - NECoreKit (= 9.7.5) - - NIMSDK_LITE (= 10.8.20) - - YXAlog (= 1.0.9) - - NECoreKit (9.7.5): - - YXAlog - - NELocalConversationUIKit (10.6.1): - - NELocalConversationUIKit/NOS (= 10.6.1) - - NELocalConversationUIKit/NOS (10.6.1): - - MJRefresh (= 3.7.5) - - NEChatKit/NOS - - NECommonUIKit (= 9.7.6) - - NIMSDK_LITE (10.8.20): - - NIMSDK_LITE/NOS (= 10.8.20) - - YXArtemis_XCFramework - - NIMSDK_LITE/NOS (10.8.20): - - YXArtemis_XCFramework - - SDWebImage (5.21.0): - - SDWebImage/Core (= 5.21.0) - - SDWebImage/Core (5.21.0) - - SDWebImageSVGKitPlugin (1.4.0): - - SDWebImage/Core (~> 5.10) - - SVGKit (~> 3.0) - - SDWebImageWebPCoder (0.14.6): - - libwebp (~> 1.0) - - SDWebImage/Core (~> 5.17) - - SVGKit (3.0.0): - - CocoaLumberjack (~> 3.0) - - YXAlog (1.0.9) - - YXArtemis_XCFramework (1.1.4) DEPENDENCIES: - Alamofire - - NEChatKit (= 10.6.1) - - NEChatUIKit (= 10.6.1) - - NEContactUIKit (= 10.6.1) - - NELocalConversationUIKit (= 10.6.1) - - NIMSDK_LITE SPEC REPOS: trunk: - Alamofire - - CocoaLumberjack - - libwebp - - MJRefresh - - NEChatKit - - NEChatUIKit - - NECommonKit - - NECommonUIKit - - NEContactUIKit - - NECoreIM2Kit - - NECoreKit - - NELocalConversationUIKit - - NIMSDK_LITE - - SDWebImage - - SDWebImageSVGKitPlugin - - SDWebImageWebPCoder - - SVGKit - - YXAlog - - YXArtemis_XCFramework SPEC CHECKSUMS: Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 - CocoaLumberjack: 6a459bc897d6d80bd1b8c78482ec7ad05dffc3f0 - libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - MJRefresh: fdf5e979eb406a0341468932d1dfc8b7f9fce961 - NEChatKit: c36d5824242fcbff0790bfa76316faabf09df8df - NEChatUIKit: 8b431a7d1ec5fbe7c4d079b9ae0dc5062cd5e146 - NECommonKit: f2359393571fcc105a7fc2fb0367a71319606042 - NECommonUIKit: b5373164800ff138dd075abac90e95379603bb60 - NEContactUIKit: 532609b8da3d2a7f274489e6e6109c6f8b774505 - NECoreIM2Kit: 0faffb84b4a2ac0fcc3705dbf4e72f022c01320f - NECoreKit: 0ccc64f01c8fdc7266f5a4df41de67447db18503 - NELocalConversationUIKit: 2f9208763b4f855d3cb3e3e105e733b020594f19 - NIMSDK_LITE: 22740bf6e2660cb7bafc40f8293fa04d3a77948e - SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 - SDWebImageSVGKitPlugin: 7542dd07c344ec3415ded0461a1161a6f087e0c9 - SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 - SVGKit: 1ad7513f8c74d9652f94ed64ddecda1a23864dea - YXAlog: 6fdd73102ba0a16933dd7bef426d6011d913c041 - YXArtemis_XCFramework: d298161285aa9cf0c99800b17847dc99aef60617 -PODFILE CHECKSUM: 1d74a8886888ebdfb5a6d41769a74dd0a3026dec +PODFILE CHECKSUM: 4ccb5fbbedd3dcb71c35d00e7bfd0d280d4ced88 COCOAPODS: 1.16.2 diff --git a/README.md b/README.md index ef3eca9..351566e 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,23 @@ yana/ - 通讯录管理 - 本地会话列表 +## API 使用 + +项目提供了完整的 API 架构,基于 TCA (The Composable Architecture) 设计: + +- 📖 **[API 使用指南](API-README.md)** - 完整的 API 使用文档 +- 🔧 **[API 规则详解](yana/APIs/API%20rule.md)** - API 请求配置和安全机制 +- 🚀 **[集成指南](yana/APIs/Integration-Guide.md)** - API 集成和最佳实践 + +### 快速开始 + +```swift +// 基本 API 请求示例 +let apiService = LiveAPIService() +let request = ConfigRequest() +let response = try await apiService.request(request) +``` + ## 注意事项 - 项目使用 CocoaPods 管理依赖 diff --git a/yana.xcodeproj/project.pbxproj b/yana.xcodeproj/project.pbxproj index 40eaec5..37b637a 100644 --- a/yana.xcodeproj/project.pbxproj +++ b/yana.xcodeproj/project.pbxproj @@ -47,6 +47,8 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = yanaAPITests; sourceTree = ""; }; @@ -254,14 +256,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n"; diff --git a/yana.xcodeproj/xcuserdata/edwinqqq.xcuserdatad/xcschemes/xcschememanagement.plist b/yana.xcodeproj/xcuserdata/edwinqqq.xcuserdatad/xcschemes/xcschememanagement.plist index 770c8f3..9674f9b 100644 --- a/yana.xcodeproj/xcuserdata/edwinqqq.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/yana.xcodeproj/xcuserdata/edwinqqq.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ yana.xcscheme_^#shared#^_ orderHint - 23 + 3 diff --git a/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 00abd21..47d6a64 100644 --- a/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -148,5 +148,21 @@ landmarkType = "7"> + + + + diff --git a/yana/APIs/API rule.md b/yana/APIs/API rule.md new file mode 100644 index 0000000..e8a4884 --- /dev/null +++ b/yana/APIs/API rule.md @@ -0,0 +1,180 @@ +# YuMi iOS 项目 API 请求配置分析 + +## 📋 目录 +- [主机地址配置](#主机地址配置) +- [网络基础配置](#网络基础配置) +- [自定义HTTP Headers](#自定义http-headers) +- [默认请求参数](#默认请求参数) +- [安全签名机制](#安全签名机制) +- [请求内容类型](#请求内容类型) +- [SSL安全配置](#ssl安全配置) +- [特殊功能](#特殊功能) +- [应用信息配置](#应用信息配置) + +## 🌐 主机地址配置 + +| 环境 | 地址 | 说明 | +|------|------|------| +| 生产环境 | `https://api.hfighting.com` | 正式服务器 | +| 测试环境 | `http://beta.api.molistar.xyz` | 开发测试服务器 | +| 图片服务 | `https://image.hfighting.com` | 静态资源服务器 | + +**环境切换机制:** +- 通过 `kIsProductionEnvironment` 用户偏好设置控制 +- DEBUG 模式下可动态切换环境 +- 发布版本强制使用生产环境 + +## 🔧 网络基础配置 + +```objective-c +// AFHTTPSessionManager 优化配置 +NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; +configuration.HTTPShouldUsePipelining = YES; // 启用 HTTP/2 pipelining +configuration.HTTPMaximumConnectionsPerHost = 15; // 提升并发连接数到15 + +// 超时设置 +manager.requestSerializer.timeoutInterval = 60; // 默认超时60秒 +manager.requestSerializer.HTTPShouldHandleCookies = YES; // 启用Cookie处理 +``` + +## 📋 自定义HTTP Headers + +### 认证相关 Headers +| Header 名称 | 值来源 | 说明 | +|-------------|--------|------| +| `pub_uid` | `[AccountInfoStorage instance].getUid` | 用户唯一标识符 | +| `pub_ticket` | `[AccountInfoStorage instance].getTicket` | 用户身份认证票据 | +| `Accept-Language` | `[NSBundle uploadLanguageText]` | 用户语言偏好 | +| `App-Version` | `PI_App_Version` | 应用版本号 (1.0.28.1) | + +### 压缩相关 Headers +| Header 名称 | 值 | 说明 | +|-------------|-----|------| +| `Accept-Encoding` | `"gzip, br"` | 支持 gzip 和 Brotli 压缩 | +| `Content-Encoding` | `"gzip"` | POST 请求数据压缩 | +| `Content-Type` | `"application/json; charset=UTF-8"` | 特殊接口使用 | + +## 🎯 默认请求参数 + +每个 API 请求都会自动添加以下基础参数: + +```objective-c +NSDictionary *defaultBasciParame = @{ + @"Accept-Language": [NSBundle uploadLanguageText], // 界面语言 + @"os": @"iOS", // 操作系统类型 + @"osVersion": [YYUtility systemVersion], // 系统版本号 + @"netType": ([YYUtility networkStatus] == ReachableViaWiFi) ? @2 : @1, // 网络类型 + @"ispType": @([YYUtility carrierIdentifier]), // 运营商类型 + @"channel": [YYUtility getAppSource] ?: @"", // 应用分发渠道 + @"model": [YYUtility modelType], // 设备型号 + @"deviceId": [YYUtility deviceUniqueIdentification], // 设备唯一标识 + @"appVersion": [YYUtility appVersion], // 应用版本 + @"app": [YYUtility appName], // 应用名称 + @"lang": [YYUtility getLanguage], // 语言代码 + @"mcc": [YYUtility getMobileCountryCode] // 移动国家代码(条件性添加) +}; +``` + +### 参数说明 +- **netType**: WiFi=2, 蜂窝网络=1 +- **channel**: 默认 "appstore",支持 "TestFlight" +- **mcc**: 移动国家代码,值为 "65535" 时不添加 + +## 🔐 安全签名机制 + +### 签名生成流程 +1. **参数过滤**: 移除系统级参数 + ```objective-c + // 被移除的参数 + @[@"Accept-Language", @"pub_uid", @"appVersion", @"appVersionCode", + @"channel", @"deviceId", @"ispType", @"netType", @"os", + @"osVersion", @"app", @"ticket", @"client", @"lang", @"mcc"] + ``` + +2. **参数排序**: 按字典 key 升序排序 + +3. **字符串拼接**: `"key0=value0&key1=value1&key2=value2"` + +4. **添加密钥**: 拼接 `key=PARAMSSECRET` + +5. **MD5加密**: 生成大写 MD5 签名 + +6. **添加签名**: 以 `pub_sign` 参数名添加到请求中 + +## 📊 请求内容类型 + +支持的响应内容类型: +```objective-c +@"application/json" // 主要 JSON 响应 +@"text/json" // JSON 文本格式 +@"text/javascript" // JavaScript 格式 +@"text/html" // HTML 响应 +@"text/plain" // 纯文本 +@"image/jpeg" // JPEG 图片 +@"image/png" // PNG 图片 +``` + +## 🛡️ SSL安全配置 + +```objective-c +manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone]; +manager.securityPolicy.allowInvalidCertificates = NO; // 不允许无效证书 +manager.securityPolicy.validatesDomainName = YES; // 验证域名 +``` + +## ⚡ 特殊功能 + +### 1. 网络状态检测 +- 自动检测网络连接状态 +- 离线时立即返回失败回调 +- 避免无效请求 + +### 2. 动态超时设置 +```objective-c +// 通过参数动态设置超时时间 +@{@"NeedChangeTimeOut": @30} // 设置30秒超时 +``` + +### 3. 数据压缩 +- POST 请求自动进行 Gzip 压缩 +- 减少网络传输数据量 +- 提升请求效率 + +### 4. 错误追踪 +- 集成 Bugly 错误上报 +- 5xx 错误自动上报 +- 包含调用堆栈信息 + +### 5. 参数解码 +- `MSParamsDecode` 处理参数加密 +- 自动生成安全签名 +- 保护 API 请求安全 + +## 📱 应用信息配置 + +| 配置项 | 值 | 说明 | +|--------|-----|------| +| 应用版本 | `1.0.28.1` | 内置版本号 | +| 默认渠道 | `appstore` | App Store 渠道 | +| 测试渠道 | `TestFlight` | TestFlight 测试 | +| 图片域名 | `https://image.hfighting.com` | 静态资源服务 | + +## 🏗️ 架构特点 + +1. **统一管理**: `HttpRequestHelper` 类统一管理所有网络请求 +2. **模块化**: API 接口按功能模块分类 (`Api+Mine`, `Api+DressUp` 等) +3. **安全性**: 多层安全机制保护 API 调用 +4. **性能优化**: HTTP/2 支持、连接复用、数据压缩 +5. **错误处理**: 完善的错误处理和上报机制 +6. **环境切换**: 支持开发和生产环境无缝切换 + +## 📝 总结 + +YuMi iOS 项目的 API 架构设计了完整的网络请求体系,包含: +- 🔐 **安全机制**: 用户认证、参数签名、SSL验证 +- 📊 **设备信息**: 完整的设备和应用信息收集 +- ⚡ **性能优化**: HTTP/2、连接池、数据压缩 +- 🛠️ **开发支持**: 环境切换、错误追踪、调试日志 +- 🏗️ **架构清晰**: 模块化设计、统一管理、易于维护 + +这种设计确保了 API 请求的安全性、稳定性和高性能,为应用提供了可靠的网络服务基础。 \ No newline at end of file diff --git a/yana/APIs/APIConstants.swift b/yana/APIs/APIConstants.swift index 1b23734..035c248 100644 --- a/yana/APIs/APIConstants.swift +++ b/yana/APIs/APIConstants.swift @@ -1,10 +1,23 @@ import Foundation +/// API 常量定义 +/// +/// 集中管理 API 相关的常量值,包括: +/// - 服务器地址 +/// - 通用请求头 +/// - API 端点路径 +/// - 通用参数 +/// +/// 注意:此文件与 APIConfiguration 有部分重复, +/// 建议后续重构时统一到 APIConfiguration 中 enum APIConstants { // MARK: - Base URLs + /// 测试环境服务器地址 static let baseURL = "http://beta.api.molistar.xyz" // MARK: - Common Headers + /// 通用请求头配置 + /// 包含基础的 Content-Type、Accept 和平台信息 static let defaultHeaders: [String: String] = [ "Content-Type": "application/json", "Accept": "application/json", @@ -13,12 +26,20 @@ enum APIConstants { ] // MARK: - Endpoints + /// API 端点路径定义 + /// + /// 注意:建议使用 APIEndpoints.swift 中的枚举定义, + /// 此处保留是为了兼容性 enum Endpoints { - static let clientInit = "/client/config" + /// 客户端初始化接口 + static let clientInit = "/client/init" + /// 用户登录接口 static let login = "/user/login" } // MARK: - Common Parameters + /// 通用请求参数 + /// 当前为空,实际参数通过 BaseRequest 自动添加 static let commonParameters: [String: String] = [: // "platform": "ios", // "version": "1.0.0" diff --git a/yana/APIs/APIEndpoints.swift b/yana/APIs/APIEndpoints.swift index b4c99c3..366b149 100644 --- a/yana/APIs/APIEndpoints.swift +++ b/yana/APIs/APIEndpoints.swift @@ -1,8 +1,21 @@ import Foundation // MARK: - API Endpoints + +/// API 端点枚举 +/// +/// 定义了应用中所有可用的 API 端点路径。 +/// 使用枚举确保端点路径的类型安全和统一管理。 +/// +/// 添加新端点时,只需在此枚举中添加新的 case 即可。 +/// +/// 使用示例: +/// ```swift +/// let configPath = APIEndpoint.config.path // "/client/config" +/// ``` enum APIEndpoint: String, CaseIterable { case config = "/client/config" + case configInit = "/client/init" case login = "/auth/login" // 可以继续添加其他端点 @@ -12,20 +25,53 @@ enum APIEndpoint: String, CaseIterable { } // MARK: - API Configuration + +/// API 配置结构体 +/// +/// 集中管理 API 相关的配置参数,包括: +/// - 服务器地址配置 +/// - 请求超时设置 +/// - 数据大小限制 +/// - 默认请求头设置 +/// +/// 配置特点: +/// - 支持不同环境的服务器地址 +/// - 防止资源超限的保护机制 +/// - 自动添加认证和设备信息头部 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" - ] + /// 默认请求头配置 + /// + /// 返回所有 API 请求都需要的基础请求头,包括: + /// - Content-Type 和 Accept 头部 + /// - 压缩支持配置 + /// - 语言和版本信息 + /// - 用户认证信息(如果已登录) + /// + /// 这些头部会自动添加到每个请求中,确保服务器能够正确处理请求 + static var defaultHeaders: [String: String] { + var headers = [ + "Content-Type": "application/json", + "Accept": "application/json", + "Accept-Encoding": "gzip, br", + "Accept-Language": Locale.current.languageCode ?? "en", + "App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" + ] + + // 添加用户认证相关 headers(如果存在) + if let userId = UserInfoManager.getCurrentUserId() { + headers["pub_uid"] = userId + } + + if let userTicket = UserInfoManager.getCurrentUserTicket() { + headers["pub_ticket"] = userTicket + } + + return headers + } } // MARK: - Request Models diff --git a/yana/APIs/APIModels.swift b/yana/APIs/APIModels.swift index 8b2a702..d6ccdd5 100644 --- a/yana/APIs/APIModels.swift +++ b/yana/APIs/APIModels.swift @@ -2,6 +2,11 @@ import Foundation import ComposableArchitecture // MARK: - HTTP Method + +/// HTTP 请求方法枚举 +/// +/// 定义了 API 请求支持的 HTTP 方法类型 +/// 每个方法都有对应的字符串值,用于构建 URLRequest enum HTTPMethod: String, CaseIterable { case GET = "GET" case POST = "POST" @@ -11,6 +16,17 @@ enum HTTPMethod: String, CaseIterable { } // MARK: - API Error Types + +/// API 错误类型枚举 +/// +/// 定义了 API 请求过程中可能出现的各种错误类型, +/// 每种错误都包含详细的描述信息,便于调试和用户提示 +/// +/// 错误类型包括: +/// - 网络相关错误(超时、连接失败等) +/// - 数据相关错误(解析失败、数据过大等) +/// - HTTP 状态码错误 +/// - 其他未知错误 enum APIError: Error, Equatable { case invalidURL case noData @@ -44,31 +60,50 @@ enum APIError: Error, Equatable { } // MARK: - Base Request Parameters + +/// 基础请求参数结构体 +/// +/// 包含所有 API 请求都需要的基础参数,如设备信息、应用信息、网络状态等。 +/// 这些参数会自动添加到每个 API 请求中,确保服务器能够获取到完整的客户端信息。 +/// +/// 主要功能: +/// - 自动收集设备和应用信息 +/// - 生成安全签名 +/// - 支持不同环境的配置 +/// +/// 使用示例: +/// ```swift +/// var baseRequest = BaseRequest() +/// baseRequest.generateSignature(with: ["key": "value"]) +/// ``` struct BaseRequest: Codable { let acceptLanguage: String let os: String = "iOS" let osVersion: String + let netType: Int let ispType: String - let channel: String = "molistar_enterprise" + let channel: String let model: String let deviceId: String let appVersion: String - let app: String = "youmi" + let app: String + let lang: String let mcc: String? let spType: String? - let pubSign: String + var 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 os, osVersion, netType, ispType, channel, model, deviceId + case appVersion, app, lang, mcc, spType case pubSign = "pub_sign" } init() { // 获取系统首选语言 - self.acceptLanguage = Locale.current.languageCode ?? "en" + let preferredLanguage = Locale.current.languageCode ?? "en" + self.acceptLanguage = preferredLanguage + self.lang = preferredLanguage // 获取系统版本 self.osVersion = UIDevice.current.systemVersion @@ -76,24 +111,154 @@ struct BaseRequest: Codable { // 获取设备型号 self.model = UIDevice.current.model - // 生成设备ID (这里使用 identifierForVendor,实际项目中可能需要更稳定的方案) + // 生成设备ID 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 + // 应用名称 + self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "yana" - // 生成签名(这里使用时间戳的 MD5,实际项目中需要根据具体签名规则) - let timestamp = String(Int(Date().timeIntervalSince1970)) - self.pubSign = timestamp.md5() + // 网络类型检测(WiFi=2, 蜂窝网络=1) + self.netType = NetworkTypeDetector.getCurrentNetworkType() + + // 运营商信息 + let carrierInfo = CarrierInfoManager.getCarrierInfo() + self.ispType = carrierInfo.ispType + self.mcc = carrierInfo.mcc == "65535" ? nil : carrierInfo.mcc + self.spType = self.mcc + + // 渠道信息 + #if DEBUG + self.channel = "TestFlight" + #else + self.channel = "appstore" + #endif + + // 生成符合规范的签名(先生成基础参数字典,然后计算签名) + self.pubSign = "" // 临时值,稍后计算 + } + + /// 生成符合 API 规则的安全签名 + /// + /// 该方法按照服务器要求的签名算法生成请求签名: + /// 1. 合并基础参数和请求参数 + /// 2. 过滤掉系统级参数 + /// 3. 按 key 升序排序 + /// 4. 拼接参数字符串 + /// 5. 添加密钥并生成 MD5 签名 + /// + /// - Parameter requestParams: 请求特定的参数字典 + mutating func generateSignature(with requestParams: [String: Any] = [:]) { + // 1. 合并基础参数和请求参数 + var allParams = requestParams + + // 添加基础参数到字典中 + allParams["Accept-Language"] = self.acceptLanguage + allParams["os"] = self.os + allParams["osVersion"] = self.osVersion + allParams["netType"] = self.netType + allParams["ispType"] = self.ispType + allParams["channel"] = self.channel + allParams["model"] = self.model + allParams["deviceId"] = self.deviceId + allParams["appVersion"] = self.appVersion + allParams["app"] = self.app + allParams["lang"] = self.lang + if let mcc = self.mcc { + allParams["mcc"] = mcc + } + if let spType = self.spType { + allParams["spType"] = spType + } + + // 2. 移除系统级参数(根据 API rule) + let systemParams = [ + "Accept-Language", "pub_uid", "appVersion", "appVersionCode", + "channel", "deviceId", "ispType", "netType", "os", + "osVersion", "app", "ticket", "client", "lang", "mcc" + ] + + var filteredParams = allParams + for param in systemParams { + filteredParams.removeValue(forKey: param) + } + + // 3. 按 key 升序排序并拼接 + let sortedKeys = filteredParams.keys.sorted() + let paramString = sortedKeys.map { key in + "\(key)=\(filteredParams[key] ?? "")" + }.joined(separator: "&") + + // 4. 添加密钥 + let keyString = "key=rpbs6us1m8r2j9g6u06ff2bo18orwaya" + let finalString = paramString.isEmpty ? keyString : "\(paramString)&\(keyString)" + + // 5. 生成大写 MD5 签名 + self.pubSign = finalString.md5().uppercased() + } +} + +// MARK: - Network Type Detector +struct NetworkTypeDetector { + static func getCurrentNetworkType() -> Int { + // WiFi = 2, 蜂窝网络 = 1 + // 这里是简化实现,实际应该检测网络状态 + return 1 // 默认蜂窝网络 + } +} + +// MARK: - Carrier Info Manager +struct CarrierInfoManager { + struct CarrierInfo { + let ispType: String + let mcc: String? + } + + static func getCarrierInfo() -> CarrierInfo { + // 简化实现,实际应该获取真实运营商信息 + return CarrierInfo(ispType: "65535", mcc: nil) + } +} + +// MARK: - User Info Manager (for Headers) +struct UserInfoManager { + static func getCurrentUserId() -> String? { + // 从存储中获取当前用户 ID + // 实际实现应该从 AccountInfoStorage 或类似的地方获取 + return nil + } + + static func getCurrentUserTicket() -> String? { + // 从存储中获取当前用户认证票据 + // 实际实现应该从 AccountInfoStorage 或类似的地方获取 + return nil } } // MARK: - API Request Protocol + +/// API 请求协议 +/// +/// 定义了所有 API 请求必须实现的接口,提供了类型安全的请求定义方式。 +/// 每个具体的 API 请求都应该实现这个协议。 +/// +/// 协议要求: +/// - Response: 关联类型,定义响应数据的类型 +/// - endpoint: API 端点路径 +/// - method: HTTP 请求方法 +/// - 可选的查询参数、请求体参数、请求头等 +/// +/// 使用示例: +/// ```swift +/// struct LoginRequest: APIRequestProtocol { +/// typealias Response = LoginResponse +/// let endpoint = "/auth/login" +/// let method: HTTPMethod = .POST +/// // ... 其他属性 +/// } +/// ``` protocol APIRequestProtocol { associatedtype Response: Codable @@ -135,3 +300,4 @@ extension String { // 需要导入 CommonCrypto import CommonCrypto + diff --git a/yana/APIs/APIService.swift b/yana/APIs/APIService.swift index f3f1bab..5bc14bb 100644 --- a/yana/APIs/APIService.swift +++ b/yana/APIs/APIService.swift @@ -2,15 +2,49 @@ import Foundation import ComposableArchitecture // MARK: - API Service Protocol + +/// API 服务协议,定义了网络请求的核心接口 +/// +/// 该协议支持泛型请求,可以处理任何符合 `APIRequestProtocol` 的请求类型 +/// 并返回对应的响应类型。这种设计提供了类型安全和灵活性。 +/// +/// 使用示例: +/// ```swift +/// let apiService: APIServiceProtocol = LiveAPIService() +/// let request = ConfigRequest() +/// let response = try await apiService.request(request) +/// ``` protocol APIServiceProtocol { + /// 发起网络请求 + /// - Parameter request: 符合 APIRequestProtocol 的请求对象 + /// - Returns: 请求对应的响应对象 + /// - Throws: APIError 或其他网络相关错误 func request(_ request: T) async throws -> T.Response } // MARK: - Live API Service Implementation + +/// 实际的 API 服务实现类 +/// +/// 该类负责处理真实的网络请求,包括: +/// - URL 构建和参数处理 +/// - 请求头设置和认证 +/// - 请求体编码和签名生成 +/// - 响应解析和错误处理 +/// - 日志记录和性能监控 +/// +/// 特性: +/// - 支持 GET/POST/PUT/DELETE 等 HTTP 方法 +/// - 自动添加基础参数和安全签名 +/// - 完整的错误处理和重试机制 +/// - 详细的请求/响应日志记录 +/// - 防止资源超限的保护机制 struct LiveAPIService: APIServiceProtocol { private let session: URLSession private let baseURL: String + /// 初始化 API 服务 + /// - Parameter baseURL: API 服务器基础 URL,默认使用配置中的地址 init(baseURL: String = APIConfiguration.baseURL) { self.baseURL = baseURL @@ -27,6 +61,19 @@ struct LiveAPIService: APIServiceProtocol { self.session = URLSession(configuration: config) } + /// 发起网络请求的核心方法 + /// + /// 该方法处理完整的请求生命周期: + /// 1. 构建请求 URL 和参数 + /// 2. 设置请求头和认证信息 + /// 3. 处理请求体和签名生成 + /// 4. 发起网络请求 + /// 5. 解析响应数据 + /// 6. 记录日志和性能指标 + /// + /// - Parameter request: 符合 APIRequestProtocol 的请求对象 + /// - Returns: 解析后的响应对象 + /// - Throws: APIError 包含详细的错误信息 func request(_ request: T) async throws -> T.Response { let startTime = Date() @@ -57,7 +104,9 @@ struct LiveAPIService: APIServiceProtocol { // 如果需要包含基础参数,则合并 var finalBody = bodyParams if request.includeBaseParameters { - let baseParams = BaseRequest() + var baseParams = BaseRequest() + // 生成符合 API rule 的签名 + baseParams.generateSignature(with: bodyParams) let baseDict = try baseParams.toDictionary() finalBody.merge(baseDict) { existing, _ in existing } } @@ -129,6 +178,15 @@ struct LiveAPIService: APIServiceProtocol { // MARK: - Private Helper Methods + /// 构建完整的请求 URL + /// + /// 该方法负责: + /// - 拼接基础 URL 和端点路径 + /// - 处理查询参数 + /// - 为 GET 请求添加基础参数和签名 + /// + /// - Parameter request: API 请求对象 + /// - Returns: 构建完成的 URL,如果构建失败则返回 nil private func buildURL(for request: T) -> URL? { guard var urlComponents = URLComponents(string: baseURL + request.endpoint) else { return nil @@ -140,7 +198,10 @@ struct LiveAPIService: APIServiceProtocol { // 对于 GET 请求,将基础参数添加到查询参数中 if request.method == .GET && request.includeBaseParameters { do { - let baseParams = BaseRequest() + var baseParams = BaseRequest() + // 为 GET 请求生成签名(合并查询参数) + let queryParamsDict = request.queryParameters ?? [:] + baseParams.generateSignature(with: queryParamsDict) let baseDict = try baseParams.toDictionary() for (key, value) in baseDict { queryItems.append(URLQueryItem(name: key, value: "\(value)")) @@ -164,6 +225,12 @@ struct LiveAPIService: APIServiceProtocol { return urlComponents.url } + /// 从响应数据中提取错误消息 + /// + /// 尝试从 JSON 响应中提取错误信息,支持多种常见的错误字段格式 + /// + /// - Parameter data: 响应数据 + /// - Returns: 提取到的错误消息,如果没有找到则返回 nil private func extractErrorMessage(from data: Data) -> String? { guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil @@ -181,6 +248,13 @@ struct LiveAPIService: APIServiceProtocol { return nil } + /// 将系统错误映射为 API 错误 + /// + /// 将 URLError 等系统级错误转换为统一的 APIError 类型, + /// 便于上层代码进行统一的错误处理 + /// + /// - Parameter error: 系统错误 + /// - Returns: 映射后的 APIError private func mapSystemError(_ error: Error) -> APIError { if let urlError = error as? URLError { switch urlError.code { @@ -200,6 +274,20 @@ struct LiveAPIService: APIServiceProtocol { } // MARK: - Mock API Service (for testing) + +/// 模拟 API 服务,用于测试和开发 +/// +/// 该类提供了一个可配置的模拟 API 服务,可以: +/// - 设置预定义的响应数据 +/// - 模拟网络延迟 +/// - 用于单元测试和 UI 预览 +/// +/// 使用示例: +/// ```swift +/// var mockService = MockAPIService() +/// mockService.setMockResponse(for: "/client/config", response: mockConfigResponse) +/// let response = try await mockService.request(ConfigRequest()) +/// ``` struct MockAPIService: APIServiceProtocol { private var mockResponses: [String: Any] = [:] diff --git a/yana/Features/ConfigFeature.swift b/yana/Features/ConfigFeature.swift index 09dc5df..9989c3b 100644 --- a/yana/Features/ConfigFeature.swift +++ b/yana/Features/ConfigFeature.swift @@ -92,4 +92,4 @@ struct ConfigFeature { } } } -} \ No newline at end of file +} diff --git a/yana/Features/LoginFeature.swift b/yana/Features/LoginFeature.swift index 1f47eb2..812e5e1 100644 --- a/yana/Features/LoginFeature.swift +++ b/yana/Features/LoginFeature.swift @@ -32,52 +32,52 @@ struct LoginFeature { } 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 - } - } +// 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 +}