feat: 更新项目配置和功能模块

- 修改Package.swift以支持iOS 15和macOS 12。
- 更新swift-tca-architecture-guidelines.mdc中的alwaysApply设置为false。
- 注释掉AppDelegate中的NIMSDK导入,移除不再使用的NIMConfigurationManager和NIMSessionManager文件。
- 添加新的API相关文件,包括EMailLoginFeature、IDLoginFeature和相关视图,增强登录功能。
- 更新APIConstants和APIEndpoints以反映新的API路径。
- 添加本地化支持文件,包含英文和中文简体的本地化字符串。
- 新增字体管理和安全工具类,支持AES和DES加密。
- 更新Xcode项目配置,调整版本号和启动画面设置。
This commit is contained in:
edwinQQQ
2025-07-09 16:14:19 +08:00
parent 5926906f3c
commit c470dba79c
71 changed files with 4000 additions and 522 deletions

View File

@@ -15,7 +15,7 @@
| 环境 | 地址 | 说明 |
|------|------|------|
| 生产环境 | `https://api.hfighting.com` | 正式服务器 |
| 生产环境 | `https://api.epartylive.com` | 正式服务器 |
| 测试环境 | `http://beta.api.molistar.xyz` | 开发测试服务器 |
| 图片服务 | `https://image.hfighting.com` | 静态资源服务器 |
@@ -177,4 +177,4 @@ YuMi iOS 项目的 API 架构设计了完整的网络请求体系,包含:
- 🛠️ **开发支持**: 环境切换、错误追踪、调试日志
- 🏗️ **架构清晰**: 模块化设计、统一管理、易于维护
这种设计确保了 API 请求的安全性、稳定性和高性能,为应用提供了可靠的网络服务基础。
这种设计确保了 API 请求的安全性、稳定性和高性能,为应用提供了可靠的网络服务基础。

View File

@@ -3,17 +3,13 @@ import Foundation
/// API
///
/// API
/// -
/// -
/// - API
/// -
///
/// APIConfiguration
/// baseURLAppConfig
/// APIConfiguration
enum APIConstants {
// MARK: - Base URLs
///
static let baseURL = "http://beta.api.molistar.xyz"
// MARK: - Common Headers
///
@@ -34,7 +30,7 @@ enum APIConstants {
///
static let clientInit = "/client/init"
///
static let login = "/user/login"
static let login = "/oauth/token"
}
// MARK: - Common Parameters

View File

@@ -16,8 +16,11 @@ import Foundation
enum APIEndpoint: String, CaseIterable {
case config = "/client/config"
case configInit = "/client/init"
case login = "/auth/login"
//
case login = "/oauth/token"
case ticket = "/oauth/ticket"
// Web
case userAgreement = "/modules/rule/protocol.html"
case privacyPolicy = "/modules/rule/privacy-wap.html"
var path: String {
return self.rawValue
@@ -39,10 +42,38 @@ enum APIEndpoint: String, CaseIterable {
/// -
/// -
struct APIConfiguration {
static let baseURL = "http://beta.api.molistar.xyz"
static var baseURL: String { AppConfig.baseURL }
static let timeout: TimeInterval = 30.0
static let maxDataSize: Int = 50 * 1024 * 1024 // 50MB
/// URL
/// - Parameter endpoint: API
/// - Returns: URL
static func fullURL(for endpoint: APIEndpoint) -> String {
return baseURL + endpoint.path
}
/// URL
/// - Parameter endpoint: API
/// - Returns: URL nil
static func url(for endpoint: APIEndpoint) -> URL? {
return URL(string: fullURL(for: endpoint))
}
/// Web URL
/// - Parameter endpoint: API
/// - Returns: Web URL
static func fullWebURL(for endpoint: APIEndpoint) -> String {
return baseURL + AppConfig.webPathPrefix + endpoint.path
}
/// Web URL
/// - Parameter endpoint: API
/// - Returns: Web URL nil
static func webURL(for endpoint: APIEndpoint) -> URL? {
return URL(string: fullWebURL(for: endpoint))
}
///
///
/// API
@@ -58,7 +89,8 @@ struct APIConfiguration {
"Accept": "application/json",
"Accept-Encoding": "gzip, br",
"Accept-Language": Locale.current.languageCode ?? "en",
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 16.4; Scale/2.00)"
]
// headers

View File

@@ -118,7 +118,7 @@ struct BaseRequest: Codable {
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
//
self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "yana"
self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "eparty"
// WiFi=2, =1
self.netType = NetworkTypeDetector.getCurrentNetworkType()
@@ -131,7 +131,7 @@ struct BaseRequest: Codable {
//
#if DEBUG
self.channel = "TestFlight"
self.channel = "molistar_enterprise"
#else
self.channel = "appstore"
#endif
@@ -186,9 +186,10 @@ struct BaseRequest: Codable {
}
// 3. key
// "key0=value0&key1=value1&key2=value2"
let sortedKeys = filteredParams.keys.sorted()
let paramString = sortedKeys.map { key in
"\(key)=\(filteredParams[key] ?? "")"
"\(key)=\(String(describing: filteredParams[key] ?? ""))"
}.joined(separator: "&")
// 4.
@@ -205,7 +206,7 @@ struct NetworkTypeDetector {
static func getCurrentNetworkType() -> Int {
// WiFi = 2, = 1
//
return 1 //
return 2 //
}
}
@@ -224,16 +225,136 @@ struct CarrierInfoManager {
// MARK: - User Info Manager (for Headers)
struct UserInfoManager {
static func getCurrentUserId() -> String? {
// ID
// AccountInfoStorage
return nil
private static let userDefaults = UserDefaults.standard
// MARK: - Storage Keys
private enum StorageKeys {
static let userId = "user_id"
static let accessToken = "access_token"
static let ticket = "user_ticket"
static let userInfo = "user_info"
}
// MARK: - User ID Management
static func getCurrentUserId() -> String? {
return userDefaults.string(forKey: StorageKeys.userId)
}
static func saveUserId(_ userId: String) {
userDefaults.set(userId, forKey: StorageKeys.userId)
userDefaults.synchronize()
print("💾 保存用户ID: \(userId)")
}
// MARK: - Access Token Management
static func getAccessToken() -> String? {
return userDefaults.string(forKey: StorageKeys.accessToken)
}
static func saveAccessToken(_ accessToken: String) {
userDefaults.set(accessToken, forKey: StorageKeys.accessToken)
userDefaults.synchronize()
print("💾 保存 Access Token")
}
// MARK: - Ticket Management ()
private static var currentTicket: String?
static func getCurrentUserTicket() -> String? {
//
// AccountInfoStorage
return nil
return currentTicket
}
static func saveTicket(_ ticket: String) {
currentTicket = ticket
print("💾 保存 Ticket 到内存")
}
static func clearTicket() {
currentTicket = nil
print("🗑️ 清除 Ticket")
}
// MARK: - User Info Management
static func saveUserInfo(_ userInfo: UserInfo) {
do {
let data = try JSONEncoder().encode(userInfo)
userDefaults.set(data, forKey: StorageKeys.userInfo)
userDefaults.synchronize()
// ID
if let userId = userInfo.userId {
saveUserId(userId)
}
print("💾 保存用户信息成功")
} catch {
print("❌ 保存用户信息失败: \(error)")
}
}
static func getUserInfo() -> UserInfo? {
guard let data = userDefaults.data(forKey: StorageKeys.userInfo) else {
return nil
}
do {
return try JSONDecoder().decode(UserInfo.self, from: data)
} catch {
print("❌ 解析用户信息失败: \(error)")
return nil
}
}
// MARK: - Complete Authentication Data Management
/// OAuth Token + Ticket +
static func saveCompleteAuthenticationData(
accessToken: String,
ticket: String,
uid: Int?, // String?Int?
userInfo: UserInfo?
) {
saveAccessToken(accessToken)
saveTicket(ticket)
if let uid = uid {
saveUserId("\(uid)") //
}
if let userInfo = userInfo {
saveUserInfo(userInfo)
}
print("✅ 完整认证信息保存成功")
}
///
static func hasValidAuthentication() -> Bool {
return getAccessToken() != nil && getCurrentUserTicket() != nil
}
///
static func clearAllAuthenticationData() {
userDefaults.removeObject(forKey: StorageKeys.userId)
userDefaults.removeObject(forKey: StorageKeys.accessToken)
userDefaults.removeObject(forKey: StorageKeys.userInfo)
clearTicket()
userDefaults.synchronize()
print("🗑️ 清除所有认证信息")
}
/// Ticket
static func restoreTicketIfNeeded() async -> Bool {
guard let accessToken = getAccessToken(),
getCurrentUserTicket() == nil else {
return false
}
print("🔄 尝试使用 Access Token 恢复 Ticket...")
// APIService false
// TicketHelper.createTicketRequest
return false
}
}
@@ -267,6 +388,7 @@ protocol APIRequestProtocol {
var queryParameters: [String: String]? { get }
var bodyParameters: [String: Any]? { get }
var headers: [String: String]? { get }
var customHeaders: [String: String]? { get } //
var timeout: TimeInterval { get }
var includeBaseParameters: Bool { get }
}
@@ -275,6 +397,7 @@ extension APIRequestProtocol {
var timeout: TimeInterval { 30.0 }
var includeBaseParameters: Bool { true }
var headers: [String: String]? { nil }
var customHeaders: [String: String]? { nil } //
}
// MARK: - Generic API Response
@@ -285,19 +408,5 @@ struct APIResponse<T: Codable>: Codable {
let code: Int?
}
// MARK: - String MD5 Extension
extension String {
func md5() -> String {
let data = Data(self.utf8)
let hash = data.withUnsafeBytes { bytes -> [UInt8] in
var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash)
return hash
}
return hash.map { String(format: "%02x", $0) }.joined()
}
}
// CommonCrypto
import CommonCrypto
// String+MD5 Utils/Extensions/String+MD5.swift

View File

@@ -93,6 +93,11 @@ struct LiveAPIService: APIServiceProtocol {
headers.merge(customHeaders) { _, new in new }
}
//
if let additionalHeaders = request.customHeaders {
headers.merge(additionalHeaders) { _, new in new }
}
for (key, value) in headers {
urlRequest.setValue(value, forHTTPHeaderField: key)
}

236
yana/APIs/LoginModels.swift Normal file
View File

@@ -0,0 +1,236 @@
import Foundation
// MARK: - ID Login Request Model
struct IDLoginAPIRequest: APIRequestProtocol {
typealias Response = IDLoginResponse
let endpoint = APIEndpoint.login.path // 使
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil
let timeout: TimeInterval = 30.0
/// ID
/// - Parameters:
/// - phone: DESID/
/// - password: DES
/// - clientSecret: "uyzjdhds"
/// - version: "1"
/// - clientId: ID"erban-client"
/// - grantType: "password"
init(phone: String, password: String, clientSecret: String = "uyzjdhds", version: String = "1", clientId: String = "erban-client", grantType: String = "password") {
self.queryParameters = [
"phone": phone,
"password": password,
"client_secret": clientSecret,
"version": version,
"client_id": clientId,
"grant_type": grantType
];
// self.bodyParameters = [
// "phone": phone,
// "password": password,
// "client_secret": clientSecret,
// "version": version,
// "client_id": clientId,
// "grant_type": grantType
// ]
}
}
// MARK: - ID Login Response Model
struct IDLoginResponse: Codable, Equatable {
let status: String?
let message: String?
let code: Int?
let data: IDLoginData?
///
var isSuccess: Bool {
return code == 200 || status?.lowercased() == "success"
}
///
var errorMessage: String {
return message ?? "登录失败,请重试"
}
}
// MARK: - ID Login Data Model
struct IDLoginData: Codable, Equatable {
let accessToken: String?
let refreshToken: String?
let tokenType: String?
let expiresIn: Int?
let scope: String?
let userInfo: UserInfo?
let uid: Int? // String?Int?API
let netEaseToken: String? // token
let jti: String? // JWT token identifier
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case refreshToken = "refresh_token"
case tokenType = "token_type"
case expiresIn = "expires_in"
case scope
case userInfo = "user_info"
case uid
case netEaseToken
case jti
}
}
// MARK: - User Info Model
struct UserInfo: Codable, Equatable {
let userId: String?
let username: String?
let nickname: String?
let avatar: String?
let email: String?
let phone: String?
let status: String?
let createTime: String?
let updateTime: String?
enum CodingKeys: String, CodingKey {
case userId = "user_id"
case username
case nickname
case avatar
case email
case phone
case status
case createTime = "create_time"
case updateTime = "update_time"
}
}
// MARK: - Login Helper
struct LoginHelper {
/// ID
/// DES
/// - Parameters:
/// - userID: ID
/// - password:
/// - Returns: APInil
static func createIDLoginRequest(userID: String, password: String) -> IDLoginAPIRequest? {
// 使DESID
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedID = DESEncrypt.encryptUseDES(userID, key: encryptionKey),
let encryptedPassword = DESEncrypt.encryptUseDES(password, key: encryptionKey) else {
print("❌ DES加密失败")
return nil
}
print("🔐 DES加密成功")
print(" 原始ID: \(userID)")
print(" 加密后ID: \(encryptedID)")
print(" 原始密码: \(password)")
print(" 加密后密码: \(encryptedPassword)")
return IDLoginAPIRequest(
phone: userID,
password: encryptedPassword
)
}
}
// MARK: - Ticket API Models
/// Ticket
struct TicketAPIRequest: APIRequestProtocol {
typealias Response = TicketResponse
let endpoint = "/oauth/ticket"
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil
let timeout: TimeInterval = 30.0
let customHeaders: [String: String]?
/// Ticket
/// - Parameters:
/// - accessToken: OAuth 访
/// - issueType: "multi"
/// - uid:
init(accessToken: String, issueType: String = "multi", uid: Int? = nil) {
self.queryParameters = [
"access_token": accessToken,
"issue_type": issueType
]
//
var headers: [String: String] = [:]
if let uid = uid {
headers["pub_uid"] = "\(uid)" //
}
self.customHeaders = headers.isEmpty ? nil : headers
}
}
/// Ticket
struct TicketResponse: Codable, Equatable {
let code: Int?
let message: String?
let data: TicketData?
///
var isSuccess: Bool {
return code == 200
}
///
var errorMessage: String {
return message ?? "Ticket 获取失败,请重试"
}
/// Ticket
var ticket: String? {
return data?.tickets?.first?.ticket
}
}
/// Ticket
struct TicketData: Codable, Equatable {
let tickets: [TicketInfo]?
}
/// Ticket
struct TicketInfo: Codable, Equatable {
let ticket: String?
}
// MARK: - Ticket Helper
struct TicketHelper {
/// Ticket
/// - Parameters:
/// - accessToken: OAuth 访
/// - uid:
/// - Returns: Ticket API
static func createTicketRequest(accessToken: String, uid: Int?) -> TicketAPIRequest {
return TicketAPIRequest(accessToken: accessToken, uid: uid)
}
/// Ticket
/// - Parameters:
/// - accessToken: OAuth 访
/// - uid:
static func debugTicketRequest(accessToken: String, uid: Int?) {
print("🎫 Ticket 请求调试信息")
print(" AccessToken: \(accessToken)")
print(" UID: \(uid?.description ?? "nil")")
print(" Endpoint: /oauth/ticket")
print(" Method: POST")
print(" Headers: pub_uid = \(uid?.description ?? "nil")")
print(" Parameters: access_token=\(accessToken), issue_type=multi")
}
}
// MARK: - LoginResponse
typealias LoginResponse = IDLoginResponse

262
yana/APIs/oauth flow.md Normal file
View File

@@ -0,0 +1,262 @@
# OAuth/Ticket 认证系统 API 文档
## 概述
本文档描述了 YuMi 应用中 OAuth 认证和 Ticket 会话管理的完整流程。系统采用两阶段认证机制:
1. **OAuth 阶段**:用户登录获取 `access_token`
2. **Ticket 阶段**:使用 `access_token` 获取业务会话 `ticket`
## 认证流程架构
### 核心组件
- **AccountInfoStorage**: 负责账户信息和 ticket 的本地存储
- **HttpRequestHelper**: 网络请求管理,自动添加认证头
- **Api+Login**: 登录相关 API 接口
- **Api+Main**: Ticket 获取相关 API 接口
### 认证数据模型
#### AccountModel
```objc
@interface AccountModel : PIBaseModel
@property (nonatomic, assign) NSString *uid; // 用户唯一标识
@property (nonatomic, copy) NSString *jti; // JWT ID
@property (nonatomic, copy) NSString *token_type; // Token 类型
@property (nonatomic, copy) NSString *refresh_token; // 刷新令牌
@property (nonatomic, copy) NSString *netEaseToken; // 网易云信令牌
@property (nonatomic, copy) NSString *access_token; // OAuth 访问令牌
@property (nonatomic, assign) NSNumber *expires_in; // 过期时间
@end
```
## API 接口详情
### 1. OAuth 登录接口
#### 1.1 手机验证码登录
```objc
+ (void)loginWithCode:(HttpRequestHelperCompletion)completion
phone:(NSString *)phone
code:(NSString *)code
client_secret:(NSString *)client_secret
version:(NSString *)version
client_id:(NSString *)client_id
grant_type:(NSString *)grant_type
phoneAreaCode:(NSString *)phoneAreaCode;
```
**接口路径**: `POST /oauth/token`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| phone | String | 是 | 手机号DES加密 |
| code | String | 是 | 验证码 |
| client_secret | String | 是 | 客户端密钥,固定值:"uyzjdhds" |
| version | String | 是 | 版本号,固定值:"1" |
| client_id | String | 是 | 客户端ID固定值"erban-client" |
| grant_type | String | 是 | 授权类型,验证码登录为:"sms_code" |
| phoneAreaCode | String | 是 | 手机区号 |
**返回数据**: AccountModel 对象
#### 1.2 手机密码登录
```objc
+ (void)loginWithPassword:(HttpRequestHelperCompletion)completion
phone:(NSString *)phone
password:(NSString *)password
client_secret:(NSString *)client_secret
version:(NSString *)version
client_id:(NSString *)client_id
grant_type:(NSString *)grant_type;
```
**接口路径**: `POST /oauth/token`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| phone | String | 是 | 手机号DES加密 |
| password | String | 是 | 密码DES加密 |
| client_secret | String | 是 | 客户端密钥 |
| version | String | 是 | 版本号 |
| client_id | String | 是 | 客户端ID |
| grant_type | String | 是 | 授权类型,密码登录为:"password" |
#### 1.3 第三方登录
```objc
+ (void)loginWithThirdPart:(HttpRequestHelperCompletion)completion
openid:(NSString *)openid
unionid:(NSString *)unionid
access_token:(NSString *)access_token
type:(NSString *)type;
```
**接口路径**: `POST /acc/third/login`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| openid | String | 是 | 第三方平台用户唯一标识 |
| unionid | String | 是 | 第三方平台联合ID |
| access_token | String | 是 | 第三方平台访问令牌 |
| type | String | 是 | 第三方平台类型1:Apple, 2:Facebook, 3:Google等 |
### 2. Ticket 获取接口
#### 2.1 获取 Ticket
```objc
+ (void)requestTicket:(HttpRequestHelperCompletion)completion
access_token:(NSString *)accessToken
issue_type:(NSString *)issueType;
```
**接口路径**: `POST /oauth/ticket`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| access_token | String | 是 | OAuth 登录获取的访问令牌 |
| issue_type | String | 是 | 签发类型,固定值:"multi" |
**返回数据**:
```json
{
"code": 200,
"data": {
"tickets": [
{
"ticket": "eyJhbGciOiJIUzI1NiJ9..."
}
]
}
}
```
### 3. HTTP 请求头配置
所有业务 API 请求都会自动添加以下请求头:
```objc
// 在 HttpRequestHelper 中自动配置
- (void)setupHeader {
AFHTTPSessionManager *client = [HttpRequestHelper requestManager];
// 用户ID头
if ([[AccountInfoStorage instance] getUid].length > 0) {
[client.requestSerializer setValue:[[AccountInfoStorage instance] getUid]
forHTTPHeaderField:@"pub_uid"];
}
// Ticket 认证头
if ([[AccountInfoStorage instance] getTicket].length > 0) {
[client.requestSerializer setValue:[[AccountInfoStorage instance] getTicket]
forHTTPHeaderField:@"pub_ticket"];
}
// 其他公共头
[client.requestSerializer setValue:[NSBundle uploadLanguageText]
forHTTPHeaderField:@"Accept-Language"];
[client.requestSerializer setValue:PI_App_Version
forHTTPHeaderField:@"App-Version"];
}
```
## 使用流程
### 完整登录流程示例
```objc
// 1. 用户登录获取 access_token
[Api loginWithCode:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200) {
// 保存账户信息
AccountModel *accountModel = [AccountModel modelWithDictionary:data.data];
[[AccountInfoStorage instance] saveAccountInfo:accountModel];
// 2. 使用 access_token 获取 ticket
[Api requestTicket:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200) {
NSArray *tickets = [data.data valueForKey:@"tickets"];
NSString *ticket = [tickets[0] valueForKey:@"ticket"];
// 保存 ticket
[[AccountInfoStorage instance] saveTicket:ticket];
// 3. 登录成功,可以进行业务操作
[self navigateToMainPage];
}
} access_token:accountModel.access_token issue_type:@"multi"];
}
} phone:encryptedPhone
code:verificationCode
client_secret:@"uyzjdhds"
version:@"1"
client_id:@"erban-client"
grant_type:@"sms_code"
phoneAreaCode:areaCode];
```
### 自动登录流程
```objc
- (void)autoLogin {
// 检查本地是否有账户信息
AccountModel *accountModel = [[AccountInfoStorage instance] getCurrentAccountInfo];
if (accountModel == nil || accountModel.access_token == nil) {
[self tokenInvalid]; // 跳转到登录页
return;
}
// 检查是否有有效的 ticket
if ([[AccountInfoStorage instance] getTicket].length > 0) {
[[self getView] autoLoginSuccess];
return;
}
// 使用 access_token 重新获取 ticket
[Api requestTicket:^(BaseModel * _Nonnull data) {
NSArray *tickets = [data.data valueForKey:@"tickets"];
NSString *ticket = [tickets[0] valueForKey:@"ticket"];
[[AccountInfoStorage instance] saveTicket:ticket];
[[self getView] autoLoginSuccess];
} fail:^(NSInteger code, NSString * _Nullable msg) {
[self logout]; // ticket 获取失败,重新登录
} access_token:accountModel.access_token issue_type:@"multi"];
}
```
## 错误处理
### 401 未授权错误
当接收到 401 状态码时,系统会自动处理:
```objc
// 在 HttpRequestHelper 中
if (response && response.statusCode == 401) {
failure(response.statusCode, YMLocalizedString(@"HttpRequestHelper7"));
// 通常需要重新登录
}
```
### Ticket 过期处理
- Ticket 过期时服务器返回 401 错误
- 客户端应该使用保存的 `access_token` 重新获取 ticket
- 如果 `access_token` 也过期,则需要用户重新登录
## 安全注意事项
1. **数据加密**: 敏感信息(手机号、密码)使用 DES 加密传输
2. **本地存储**:
- `access_token` 存储在文件系统中
- `ticket` 存储在内存中,应用重启需重新获取
3. **请求头**: 所有业务请求自动携带 `pub_uid``pub_ticket`
4. **错误处理**: 建立完善的 401 错误重试机制
## 相关文件
- `YuMi/Structure/MVP/Model/AccountInfoStorage.h/m` - 账户信息存储管理
- `YuMi/Modules/YMLogin/Api/Api+Login.h/m` - 登录相关接口
- `YuMi/Modules/YMTabbar/Api/Api+Main.h/m` - Ticket 获取接口
- `YuMi/Network/HttpRequestHelper.h/m` - 网络请求管理
- `YuMi/Structure/MVP/Model/AccountModel.h/m` - 账户数据模型

1
yana/APIs/oauth flow.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB