first commit for e-party

This commit is contained in:
edwinQQQ
2025-07-07 14:19:07 +08:00
parent 007c10daaf
commit 5926906f3c
14 changed files with 1248 additions and 201 deletions

626
API-README.md Normal file
View 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
View File

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

View File

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

View File

@@ -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 管理依赖

View File

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

View File

@@ -7,7 +7,7 @@
<key>yana.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>23</integer>
<integer>3</integer>
</dict>
</dict>
</dict>

View File

@@ -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
View 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 请求的安全性、稳定性和高性能,为应用提供了可靠的网络服务基础。

View File

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

View File

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

View File

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

View File

@@ -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] = [:]

View File

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