first commit for e-party
This commit is contained in:
626
API-README.md
Normal file
626
API-README.md
Normal file
@@ -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<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` | 正式服务器 |
|
||||
|
||||
### 配置参数
|
||||
|
||||
```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<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. 重试机制
|
||||
|
||||
```swift
|
||||
// ✅ 推荐:实现重试逻辑
|
||||
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` 中添加端点**:
|
||||
```swift
|
||||
enum APIEndpoint: String, CaseIterable {
|
||||
case newEndpoint = "/new/endpoint"
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
2. **创建请求模型**:
|
||||
```swift
|
||||
struct NewRequest: APIRequestProtocol {
|
||||
typealias Response = NewResponse
|
||||
|
||||
let endpoint = "/new/endpoint"
|
||||
let method: HTTPMethod = .POST
|
||||
let includeBaseParameters = true
|
||||
// ... 其他属性
|
||||
}
|
||||
```
|
||||
|
||||
3. **创建响应模型**:
|
||||
```swift
|
||||
struct NewResponse: Codable {
|
||||
let success: Bool
|
||||
let data: SomeDataModel?
|
||||
let message: String?
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 示例代码
|
||||
|
||||
### 完整的登录功能示例
|
||||
|
||||
```swift
|
||||
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": {...}}
|
||||
```
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [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. 更新相关文档
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本文档基于当前项目架构编写,如有架构变更请及时更新文档内容。
|
14
Podfile
14
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'
|
||||
|
113
Podfile.lock
113
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
|
||||
|
17
README.md
17
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 管理依赖
|
||||
|
@@ -47,6 +47,8 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = yanaAPITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -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";
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<key>yana.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>23</integer>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
@@ -148,5 +148,21 @@
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "B01C5DEF-AE4C-4FE7-B7E5-9EED0586DF0E"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "yana/Configs/ClientConfig.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "10"
|
||||
endingLineNumber = "10"
|
||||
landmarkName = "initializeClient()"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
</Breakpoints>
|
||||
</Bucket>
|
||||
|
180
yana/APIs/API rule.md
Normal file
180
yana/APIs/API rule.md
Normal file
@@ -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 请求的安全性、稳定性和高性能,为应用提供了可靠的网络服务基础。
|
@@ -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"
|
||||
|
@@ -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] = [
|
||||
/// 默认请求头配置
|
||||
///
|
||||
/// 返回所有 API 请求都需要的基础请求头,包括:
|
||||
/// - Content-Type 和 Accept 头部
|
||||
/// - 压缩支持配置
|
||||
/// - 语言和版本信息
|
||||
/// - 用户认证信息(如果已登录)
|
||||
///
|
||||
/// 这些头部会自动添加到每个请求中,确保服务器能够正确处理请求
|
||||
static var defaultHeaders: [String: String] {
|
||||
var headers = [
|
||||
"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"
|
||||
"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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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<T: APIRequestProtocol>(_ 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<T: APIRequestProtocol>(_ 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<T: APIRequestProtocol>(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] = [:]
|
||||
|
||||
|
@@ -32,52 +32,52 @@ struct LoginFeature {
|
||||
}
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
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
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user