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

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] = [
"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

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

@@ -92,4 +92,4 @@ struct ConfigFeature {
}
}
}
}
}

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