first commit for e-party
This commit is contained in:
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] = [
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
// "User-Agent": "yana-iOS/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0")"
|
||||
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 18.3.1; Scale/3.00)",
|
||||
"Accept-Language": "zh-Hant",
|
||||
"Accept-Encoding": "gzip, br"
|
||||
]
|
||||
/// 默认请求头配置
|
||||
///
|
||||
/// 返回所有 API 请求都需要的基础请求头,包括:
|
||||
/// - Content-Type 和 Accept 头部
|
||||
/// - 压缩支持配置
|
||||
/// - 语言和版本信息
|
||||
/// - 用户认证信息(如果已登录)
|
||||
///
|
||||
/// 这些头部会自动添加到每个请求中,确保服务器能够正确处理请求
|
||||
static var defaultHeaders: [String: String] {
|
||||
var headers = [
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip, br",
|
||||
"Accept-Language": Locale.current.languageCode ?? "en",
|
||||
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
||||
]
|
||||
|
||||
// 添加用户认证相关 headers(如果存在)
|
||||
if let userId = UserInfoManager.getCurrentUserId() {
|
||||
headers["pub_uid"] = userId
|
||||
}
|
||||
|
||||
if let userTicket = UserInfoManager.getCurrentUserTicket() {
|
||||
headers["pub_ticket"] = userTicket
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Request Models
|
||||
|
@@ -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] = [:]
|
||||
|
||||
|
@@ -92,4 +92,4 @@ struct ConfigFeature {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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