Files
e-party-iOS/API-README.md
2025-07-07 14:19:07 +08:00

17 KiB

Yana iOS 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. 基本使用

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 集成使用

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<Self> {
        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 正式服务器

配置参数

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 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 请求

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 请求

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. 基础参数说明

每个请求都会自动包含以下基础参数:

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          // 安全签名
}

错误处理

错误类型

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)         // 未知错误
}

错误处理示例

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 签名

认证头部

// 用户认证相关头部(如果用户已登录)
if let userId = UserInfoManager.getCurrentUserId() {
    headers["pub_uid"] = userId
}

if let userTicket = UserInfoManager.getCurrentUserTicket() {
    headers["pub_ticket"] = userTicket
}

💡 最佳实践

1. 错误处理

// ✅ 推荐:完整的错误处理
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

// ✅ 推荐:使用 MainActor 更新 UI
await MainActor.run {
    self.isLoading = false
    self.data = response
}

3. 取消请求

// ✅ 推荐:支持取消的请求
struct ContentView: View {
    @State private var task: Task<Void, Never>?
    
    private func makeRequest() {
        task = Task {
            do {
                let response = try await apiService.request(request)
                // 处理响应
            } catch {
                if !Task.isCancelled {
                    // 处理错误
                }
            }
        }
    }
    
    private func cancelRequest() {
        task?.cancel()
    }
}

4. 重试机制

// ✅ 推荐:实现重试逻辑
func requestWithRetry<T: APIRequestProtocol>(
    _ request: T,
    maxRetries: Int = 3
) async throws -> T.Response {
    var lastError: Error?
    
    for attempt in 0..<maxRetries {
        do {
            return try await apiService.request(request)
        } catch {
            lastError = error
            if attempt < maxRetries - 1 {
                try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
            }
        }
    }
    
    throw lastError!
}

📋 API接口列表

当前可用接口

接口名称 端点 方法 说明
配置获取 /client/config GET 获取客户端配置信息
初始化配置 /client/init GET 获取初始化配置
用户登录 /auth/login POST 用户登录认证

接口扩展

要添加新的 API 接口,请按以下步骤操作:

  1. APIEndpoints.swift 中添加端点:
enum APIEndpoint: String, CaseIterable {
    case newEndpoint = "/new/endpoint"
    // ...
}
  1. 创建请求模型:
struct NewRequest: APIRequestProtocol {
    typealias Response = NewResponse
    
    let endpoint = "/new/endpoint"
    let method: HTTPMethod = .POST
    let includeBaseParameters = true
    // ... 其他属性
}
  1. 创建响应模型:
struct NewResponse: Codable {
    let success: Bool
    let data: SomeDataModel?
    let message: String?
}

📝 示例代码

完整的登录功能示例

import SwiftUI
import ComposableArchitecture

// MARK: - Login Feature
@Reducer
struct LoginFeature {
    @ObservableState
    struct State: Equatable {
        var username = ""
        var password = ""
        var isLoading = false
        var isLoggedIn = false
        var errorMessage: String?
    }
    
    enum Action: Equatable {
        case usernameChanged(String)
        case passwordChanged(String)
        case loginButtonTapped
        case loginResponse(Result<LoginResponse, APIError>)
    }
    
    @Dependency(\.apiService) var apiService
    
    var body: some ReducerOf<Self> {
        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<LoginFeature>
    
    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": {...}}

📚 相关文档

🤝 贡献指南

  1. 遵循现有的代码风格和架构模式
  2. 为新的 API 接口添加完整的文档和示例
  3. 确保所有请求都包含适当的错误处理
  4. 添加单元测试覆盖新功能
  5. 更新相关文档

注意: 本文档基于当前项目架构编写,如有架构变更请及时更新文档内容。