feat: 更新API相关逻辑及视图结构
- 在Info.plist中新增API签名密钥配置。 - 将Splash视图替换为SplashV2,优化启动逻辑和用户体验。 - 更新API请求中的User-Agent逻辑,使用UserAgentProvider提供的动态值。 - 在APILogger中添加敏感信息脱敏处理,增强安全性。 - 新增CreateFeedPage视图,支持用户发布动态功能。 - 更新MainPage和Splash视图的导航逻辑,整合统一的AppRoute管理。 - 移除冗余的SplashFeature视图,提升代码整洁性和可维护性。
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
| 环境 | 地址 | 说明 |
|
||||
|------|------|------|
|
||||
| 生产环境 | `https://api.epartylive.com` | 正式服务器 |
|
||||
| 测试环境 | `http://beta.api.molistar.xyz` | 开发测试服务器 |
|
||||
| 测试环境 | `http://beta.api.pekolive.com` | 开发测试服务器 |
|
||||
| 图片服务 | `https://image.hfighting.com` | 静态资源服务器 |
|
||||
|
||||
**环境切换机制:**
|
||||
|
@@ -102,7 +102,7 @@ struct APIConfiguration {
|
||||
"Accept-Encoding": "gzip, br",
|
||||
"Accept-Language": Locale.current.language.languageCode?.identifier ?? "en",
|
||||
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
|
||||
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 17+; Scale/2.00)"
|
||||
"User-Agent": await UserAgentProvider.userAgent()
|
||||
]
|
||||
// 检查用户认证状态并添加相关 headers
|
||||
let authStatus = await UserInfoManager.checkAuthenticationStatus()
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - API Logger
|
||||
@MainActor
|
||||
class APILogger {
|
||||
enum LogLevel {
|
||||
case none
|
||||
@@ -10,19 +9,82 @@ class APILogger {
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static var logLevel: LogLevel = .detailed
|
||||
static let logLevel: LogLevel = .detailed
|
||||
#else
|
||||
static var logLevel: LogLevel = .none
|
||||
static let logLevel: LogLevel = .none
|
||||
#endif
|
||||
|
||||
private static let logQueue = DispatchQueue(label: "com.yana.api.logger", qos: .utility)
|
||||
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm:ss.SSS"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
// MARK: - Redaction
|
||||
/// 需要脱敏的敏感字段(统一小写匹配)
|
||||
private static let sensitiveKeys: Set<String> = [
|
||||
"authorization", "token", "access-token", "access_token", "refresh-token", "refresh_token",
|
||||
"password", "passwd", "secret", "pub_ticket", "ticket", "set-cookie", "cookie"
|
||||
]
|
||||
/// 对字符串做中间遮罩,保留前后若干字符
|
||||
private static func maskString(_ value: String, keepPrefix: Int = 3, keepSuffix: Int = 2) -> String {
|
||||
guard !value.isEmpty else { return value }
|
||||
if value.count <= keepPrefix + keepSuffix { return String(repeating: "*", count: value.count) }
|
||||
let start = value.startIndex
|
||||
let prefixEnd = value.index(start, offsetBy: keepPrefix)
|
||||
let suffixStart = value.index(value.endIndex, offsetBy: -keepSuffix)
|
||||
let prefix = value[start..<prefixEnd]
|
||||
let suffix = value[suffixStart..<value.endIndex]
|
||||
return String(prefix) + String(repeating: "*", count: max(0, value.distance(from: prefixEnd, to: suffixStart))) + String(suffix)
|
||||
}
|
||||
/// 对 headers 进行脱敏
|
||||
private static func maskHeaders(_ headers: [String: String]) -> [String: String] {
|
||||
var masked: [String: String] = [:]
|
||||
for (key, value) in headers {
|
||||
if sensitiveKeys.contains(key.lowercased()) {
|
||||
masked[key] = maskString(value)
|
||||
} else {
|
||||
masked[key] = value
|
||||
}
|
||||
}
|
||||
return masked
|
||||
}
|
||||
/// 递归地对 JSON 对象进行脱敏
|
||||
private static func redactJSONObject(_ obj: Any) -> Any {
|
||||
if let dict = obj as? [String: Any] {
|
||||
var newDict: [String: Any] = [:]
|
||||
for (k, v) in dict {
|
||||
if sensitiveKeys.contains(k.lowercased()) {
|
||||
if let str = v as? String { newDict[k] = maskString(str) }
|
||||
else { newDict[k] = "<redacted>" }
|
||||
} else {
|
||||
newDict[k] = redactJSONObject(v)
|
||||
}
|
||||
}
|
||||
return newDict
|
||||
} else if let arr = obj as? [Any] {
|
||||
return arr.map { redactJSONObject($0) }
|
||||
} else {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
/// 将请求体 Data 以 Pretty JSON(脱敏后)或摘要形式输出
|
||||
private static func maskedBodyString(from body: Data?) -> String {
|
||||
guard let body = body, !body.isEmpty else { return "No body" }
|
||||
if let json = try? JSONSerialization.jsonObject(with: body, options: []) {
|
||||
let redacted = redactJSONObject(json)
|
||||
if let pretty = try? JSONSerialization.data(withJSONObject: redacted, options: [.prettyPrinted]),
|
||||
let prettyString = String(data: pretty, encoding: .utf8) {
|
||||
return prettyString
|
||||
}
|
||||
}
|
||||
return "<non-json body> (\(body.count) bytes)"
|
||||
}
|
||||
|
||||
// MARK: - Request Logging
|
||||
@MainActor static func logRequest<T: APIRequestProtocol>(
|
||||
static func logRequest<T: APIRequestProtocol>(
|
||||
_ request: T,
|
||||
url: URL,
|
||||
body: Data?,
|
||||
@@ -34,85 +96,70 @@ class APILogger {
|
||||
return
|
||||
#endif
|
||||
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
logQueue.async {
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
debugInfoSync("\n🚀 [API Request] [\(timestamp)] ==================")
|
||||
debugInfoSync("📍 Endpoint: \(request.endpoint)")
|
||||
debugInfoSync("🔗 Full URL: \(url.absoluteString)")
|
||||
debugInfoSync("📝 Method: \(request.method.rawValue)")
|
||||
debugInfoSync("⏰ Timeout: \(request.timeout)s")
|
||||
|
||||
print("\n🚀 [API Request] [\(timestamp)] ==================")
|
||||
print("📍 Endpoint: \(request.endpoint)")
|
||||
print("🔗 Full URL: \(url.absoluteString)")
|
||||
print("📝 Method: \(request.method.rawValue)")
|
||||
print("⏰ Timeout: \(request.timeout)s")
|
||||
|
||||
// 显示最终的完整 headers(包括默认 headers 和自定义 headers)
|
||||
if let headers = finalHeaders, !headers.isEmpty {
|
||||
if logLevel == .detailed {
|
||||
print("📋 Final Headers (包括默认 + 自定义):")
|
||||
for (key, value) in headers.sorted(by: { $0.key < $1.key }) {
|
||||
print(" \(key): \(value)")
|
||||
}
|
||||
} else if logLevel == .basic {
|
||||
print("📋 Headers: \(headers.count) 个 headers")
|
||||
// 只显示重要的 headers
|
||||
let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"]
|
||||
for key in importantHeaders {
|
||||
if let value = headers[key] {
|
||||
print(" \(key): \(value)")
|
||||
// 显示最终的完整 headers(包括默认 headers 和自定义 headers)
|
||||
if let headers = finalHeaders, !headers.isEmpty {
|
||||
if logLevel == .detailed {
|
||||
debugInfoSync("📋 Final Headers (包括默认 + 自定义):")
|
||||
let masked = maskHeaders(headers)
|
||||
for (key, value) in masked.sorted(by: { $0.key < $1.key }) {
|
||||
debugInfoSync(" \(key): \(value)")
|
||||
}
|
||||
} else if logLevel == .basic {
|
||||
debugInfoSync("📋 Headers: \(headers.count) 个 headers")
|
||||
// 只显示重要的 headers
|
||||
let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"]
|
||||
let masked = maskHeaders(headers)
|
||||
for key in importantHeaders {
|
||||
if let value = masked[key] {
|
||||
debugInfoSync(" \(key): \(value)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let customHeaders = request.headers, !customHeaders.isEmpty {
|
||||
print("📋 Custom Headers:")
|
||||
for (key, value) in customHeaders.sorted(by: { $0.key < $1.key }) {
|
||||
print(" \(key): \(value)")
|
||||
}
|
||||
} else {
|
||||
print("📋 Headers: 使用默认 headers")
|
||||
}
|
||||
|
||||
if let queryParams = request.queryParameters, !queryParams.isEmpty {
|
||||
print("🔍 Query Parameters:")
|
||||
for (key, value) in queryParams.sorted(by: { $0.key < $1.key }) {
|
||||
print(" \(key): \(value)")
|
||||
}
|
||||
}
|
||||
|
||||
if logLevel == .detailed {
|
||||
if let body = body {
|
||||
print("📦 Request Body (\(body.count) bytes):")
|
||||
if let jsonObject = try? JSONSerialization.jsonObject(with: body, options: []),
|
||||
let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
|
||||
let prettyString = String(data: prettyData, encoding: .utf8) {
|
||||
print(prettyString)
|
||||
} else if let rawString = String(data: body, encoding: .utf8) {
|
||||
print(rawString)
|
||||
} else {
|
||||
print("Binary data")
|
||||
} else if let customHeaders = request.headers, !customHeaders.isEmpty {
|
||||
debugInfoSync("📋 Custom Headers:")
|
||||
let masked = maskHeaders(customHeaders)
|
||||
for (key, value) in masked.sorted(by: { $0.key < $1.key }) {
|
||||
debugInfoSync(" \(key): \(value)")
|
||||
}
|
||||
} else {
|
||||
print("📦 Request Body: No body")
|
||||
debugInfoSync("📋 Headers: 使用默认 headers")
|
||||
}
|
||||
|
||||
// 显示基础参数信息(仅详细模式)
|
||||
if request.includeBaseParameters {
|
||||
print("📱 Base Parameters: 自动注入设备和应用信息")
|
||||
let baseParams = BaseRequest()
|
||||
print(" Device: \(baseParams.model), OS: \(baseParams.os) \(baseParams.osVersion)")
|
||||
print(" App: \(baseParams.app) v\(baseParams.appVersion)")
|
||||
print(" Language: \(baseParams.acceptLanguage)")
|
||||
}
|
||||
} else if logLevel == .basic {
|
||||
if let body = body {
|
||||
print("📦 Request Body: \(formatBytes(body.count))")
|
||||
} else {
|
||||
print("📦 Request Body: No body")
|
||||
if let queryParams = request.queryParameters, !queryParams.isEmpty {
|
||||
debugInfoSync("🔍 Query Parameters:")
|
||||
for (key, value) in queryParams.sorted(by: { $0.key < $1.key }) {
|
||||
let masked = sensitiveKeys.contains(key.lowercased()) ? maskString(value) : value
|
||||
debugInfoSync(" \(key): \(masked)")
|
||||
}
|
||||
}
|
||||
|
||||
// 基础模式也显示是否包含基础参数
|
||||
if request.includeBaseParameters {
|
||||
print("📱 Base Parameters: 已自动注入")
|
||||
if logLevel == .detailed {
|
||||
let pretty = maskedBodyString(from: body)
|
||||
debugInfoSync("📦 Request Body: \n\(pretty)")
|
||||
|
||||
// 仅提示包含基础参数,避免跨 actor 读取 UIKit 信息
|
||||
if request.includeBaseParameters {
|
||||
debugInfoSync("📱 Base Parameters: 已自动注入")
|
||||
}
|
||||
} else if logLevel == .basic {
|
||||
let size = body?.count ?? 0
|
||||
debugInfoSync("📦 Request Body: \(formatBytes(size))")
|
||||
|
||||
// 基础模式也显示是否包含基础参数
|
||||
if request.includeBaseParameters {
|
||||
debugInfoSync("📱 Base Parameters: 已自动注入")
|
||||
}
|
||||
}
|
||||
debugInfoSync("=====================================")
|
||||
}
|
||||
|
||||
print("=====================================")
|
||||
}
|
||||
|
||||
// MARK: - Response Logging
|
||||
@@ -123,36 +170,41 @@ class APILogger {
|
||||
return
|
||||
#endif
|
||||
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
let statusEmoji = response.statusCode < 400 ? "✅" : "❌"
|
||||
logQueue.async {
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
let statusEmoji = response.statusCode < 400 ? "✅" : "❌"
|
||||
debugInfoSync("\n\(statusEmoji) [API Response] [\(timestamp)] ===================")
|
||||
debugInfoSync("⏱️ Duration: \(String(format: "%.3f", duration))s")
|
||||
debugInfoSync("📊 Status Code: \(response.statusCode)")
|
||||
debugInfoSync("🔗 URL: \(response.url?.absoluteString ?? "Unknown")")
|
||||
debugInfoSync("📏 Data Size: \(formatBytes(data.count))")
|
||||
|
||||
print("\n\(statusEmoji) [API Response] [\(timestamp)] ===================")
|
||||
print("⏱️ Duration: \(String(format: "%.3f", duration))s")
|
||||
print("📊 Status Code: \(response.statusCode)")
|
||||
print("🔗 URL: \(response.url?.absoluteString ?? "Unknown")")
|
||||
print("📏 Data Size: \(formatBytes(data.count))")
|
||||
if logLevel == .detailed {
|
||||
debugInfoSync("📋 Response Headers:")
|
||||
// 将 headers 转为 [String:String] 后脱敏
|
||||
var headers: [String: String] = [:]
|
||||
for (k, v) in response.allHeaderFields { headers["\(k)"] = "\(v)" }
|
||||
let masked = maskHeaders(headers)
|
||||
for (key, value) in masked.sorted(by: { $0.key < $1.key }) {
|
||||
debugInfoSync(" \(key): \(value)")
|
||||
}
|
||||
|
||||
if logLevel == .detailed {
|
||||
print("📋 Response Headers:")
|
||||
for (key, value) in response.allHeaderFields.sorted(by: { "\($0.key)" < "\($1.key)" }) {
|
||||
print(" \(key): \(value)")
|
||||
}
|
||||
|
||||
print("📦 Response Data:")
|
||||
if data.isEmpty {
|
||||
print(" Empty response")
|
||||
} else if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
|
||||
let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
|
||||
let prettyString = String(data: prettyData, encoding: .utf8) {
|
||||
print(prettyString)
|
||||
} else if let rawString = String(data: data, encoding: .utf8) {
|
||||
print(rawString)
|
||||
} else {
|
||||
print(" Binary data (\(data.count) bytes)")
|
||||
debugInfoSync("📦 Response Data:")
|
||||
if data.isEmpty {
|
||||
debugInfoSync(" Empty response")
|
||||
} else if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
|
||||
let prettyData = try? JSONSerialization.data(withJSONObject: redactJSONObject(jsonObject), options: .prettyPrinted),
|
||||
let prettyString = String(data: prettyData, encoding: .utf8) {
|
||||
debugInfoSync(prettyString)
|
||||
} else if let _ = String(data: data, encoding: .utf8) {
|
||||
// 对非 JSON 文本响应不做内容回显,避免泄漏
|
||||
debugInfoSync("<non-json text> (\(data.count) bytes)")
|
||||
} else {
|
||||
debugInfoSync(" Binary data (\(data.count) bytes)")
|
||||
}
|
||||
}
|
||||
debugInfoSync("=====================================")
|
||||
}
|
||||
|
||||
print("=====================================")
|
||||
}
|
||||
|
||||
// MARK: - Error Logging
|
||||
@@ -163,43 +215,43 @@ class APILogger {
|
||||
return
|
||||
#endif
|
||||
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
|
||||
print("\n❌ [API Error] [\(timestamp)] ======================")
|
||||
print("⏱️ Duration: \(String(format: "%.3f", duration))s")
|
||||
if let url = url {
|
||||
print("🔗 URL: \(url.absoluteString)")
|
||||
}
|
||||
|
||||
if let apiError = error as? APIError {
|
||||
print("🚨 API Error: \(apiError.localizedDescription)")
|
||||
} else {
|
||||
print("🚨 System Error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
if logLevel == .detailed {
|
||||
if let urlError = error as? URLError {
|
||||
print("🔍 URLError Code: \(urlError.code.rawValue)")
|
||||
print("🔍 URLError Localized: \(urlError.localizedDescription)")
|
||||
|
||||
// 详细的网络错误分析
|
||||
switch urlError.code {
|
||||
case .timedOut:
|
||||
print("💡 建议:检查网络连接或增加超时时间")
|
||||
case .notConnectedToInternet:
|
||||
print("💡 建议:检查网络连接")
|
||||
case .cannotConnectToHost:
|
||||
print("💡 建议:检查服务器地址和端口")
|
||||
case .resourceUnavailable:
|
||||
print("💡 建议:检查 API 端点是否正确")
|
||||
default:
|
||||
break
|
||||
}
|
||||
logQueue.async {
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
debugErrorSync("\n❌ [API Error] [\(timestamp)] ======================")
|
||||
debugErrorSync("⏱️ Duration: \(String(format: "%.3f", duration))s")
|
||||
if let url = url {
|
||||
debugErrorSync("🔗 URL: \(url.absoluteString)")
|
||||
}
|
||||
print("🔍 Full Error: \(error)")
|
||||
}
|
||||
|
||||
print("=====================================\n")
|
||||
if let apiError = error as? APIError {
|
||||
debugErrorSync("🚨 API Error: \(apiError.localizedDescription)")
|
||||
} else {
|
||||
debugErrorSync("🚨 System Error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
if logLevel == .detailed {
|
||||
if let urlError = error as? URLError {
|
||||
debugInfoSync("🔍 URLError Code: \(urlError.code.rawValue)")
|
||||
debugInfoSync("🔍 URLError Localized: \(urlError.localizedDescription)")
|
||||
|
||||
// 详细的网络错误分析
|
||||
switch urlError.code {
|
||||
case .timedOut:
|
||||
debugWarnSync("💡 建议:检查网络连接或增加超时时间")
|
||||
case .notConnectedToInternet:
|
||||
debugWarnSync("💡 建议:检查网络连接")
|
||||
case .cannotConnectToHost:
|
||||
debugWarnSync("💡 建议:检查服务器地址和端口")
|
||||
case .resourceUnavailable:
|
||||
debugWarnSync("💡 建议:检查 API 端点是否正确")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
debugInfoSync("🔍 Full Error: \(error)")
|
||||
}
|
||||
debugErrorSync("=====================================\n")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Decoded Response Logging
|
||||
@@ -210,9 +262,11 @@ class APILogger {
|
||||
return
|
||||
#endif
|
||||
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
print("🎯 [Decoded Response] [\(timestamp)] Type: \(type)")
|
||||
print("=====================================\n")
|
||||
logQueue.async {
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
debugInfoSync("🎯 [Decoded Response] [\(timestamp)] Type: \(type)")
|
||||
debugInfoSync("=====================================\n")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
@@ -231,10 +285,12 @@ class APILogger {
|
||||
return
|
||||
#endif
|
||||
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
print("\n⚠️ [Performance Warning] [\(timestamp)] ============")
|
||||
print("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)")
|
||||
print("💡 建议:检查网络条件或优化 API 响应")
|
||||
print("================================================\n")
|
||||
logQueue.async {
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
debugWarnSync("\n⚠️ [Performance Warning] [\(timestamp)] ============")
|
||||
debugWarnSync("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)")
|
||||
debugWarnSync("💡 建议:检查网络条件或优化 API 响应")
|
||||
debugWarnSync("================================================\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
// MARK: - HTTP Method
|
||||
|
||||
@@ -205,8 +204,9 @@ struct BaseRequest: Codable {
|
||||
"\(key)=\(String(describing: filteredParams[key] ?? ""))"
|
||||
}.joined(separator: "&")
|
||||
|
||||
// 4. 添加密钥
|
||||
let keyString = "key=rpbs6us1m8r2j9g6u06ff2bo18orwaya"
|
||||
// 4. 添加密钥(从配置提供者获取)
|
||||
let key = SigningKeyProvider.signingKey()
|
||||
let keyString = "key=\(key)"
|
||||
let finalString = paramString.isEmpty ? keyString : "\(paramString)&\(keyString)"
|
||||
|
||||
// 5. 生成大写 MD5 签名
|
||||
@@ -217,9 +217,8 @@ struct BaseRequest: Codable {
|
||||
// MARK: - Network Type Detector
|
||||
struct NetworkTypeDetector {
|
||||
static func getCurrentNetworkType() -> Int {
|
||||
// WiFi = 2, 蜂窝网络 = 1
|
||||
// 这里是简化实现,实际应该检测网络状态
|
||||
return 2 // 默认蜂窝网络
|
||||
// WiFi = 2, 蜂窝网络 = 1, 其他/无网络 = 0
|
||||
return NetworkMonitor.shared.currentType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +237,6 @@ struct CarrierInfoManager {
|
||||
|
||||
// MARK: - User Info Manager (for Headers)
|
||||
struct UserInfoManager {
|
||||
@MainActor
|
||||
private static let keychain = KeychainManager.shared
|
||||
|
||||
// MARK: - Storage Keys
|
||||
@@ -287,7 +285,7 @@ struct UserInfoManager {
|
||||
// MARK: - User Info Management
|
||||
static func saveUserInfo(_ userInfo: UserInfo) async {
|
||||
do {
|
||||
try await keychain.store(userInfo, forKey: StorageKeys.userInfo)
|
||||
try keychain.store(userInfo, forKey: StorageKeys.userInfo)
|
||||
await cacheActor.setUserInfo(userInfo)
|
||||
debugInfoSync("💾 保存用户信息成功")
|
||||
} catch {
|
||||
@@ -302,7 +300,7 @@ struct UserInfoManager {
|
||||
}
|
||||
// 从 Keychain 读取
|
||||
do {
|
||||
let userInfo = try await keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo)
|
||||
let userInfo = try keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo)
|
||||
await cacheActor.setUserInfo(userInfo)
|
||||
return userInfo
|
||||
} catch {
|
||||
@@ -377,7 +375,7 @@ struct UserInfoManager {
|
||||
/// - Parameter accountModel: 要保存的账户模型
|
||||
static func saveAccountModel(_ accountModel: AccountModel) async {
|
||||
do {
|
||||
try await keychain.store(accountModel, forKey: StorageKeys.accountModel)
|
||||
try keychain.store(accountModel, forKey: StorageKeys.accountModel)
|
||||
await cacheActor.setAccountModel(accountModel)
|
||||
|
||||
// 同步更新 ticket 到内存
|
||||
@@ -400,7 +398,7 @@ struct UserInfoManager {
|
||||
}
|
||||
// 从 Keychain 读取
|
||||
do {
|
||||
let accountModel = try await keychain.retrieve(
|
||||
let accountModel = try keychain.retrieve(
|
||||
AccountModel.self,
|
||||
forKey: StorageKeys.accountModel
|
||||
)
|
||||
@@ -448,7 +446,7 @@ struct UserInfoManager {
|
||||
/// 清除 AccountModel
|
||||
static func clearAccountModel() async {
|
||||
do {
|
||||
try await keychain.delete(forKey: StorageKeys.accountModel)
|
||||
try keychain.delete(forKey: StorageKeys.accountModel)
|
||||
await cacheActor.clearAccountModel()
|
||||
debugInfoSync("🗑️ AccountModel 已清除")
|
||||
} catch {
|
||||
@@ -459,7 +457,7 @@ struct UserInfoManager {
|
||||
/// 清除用户信息
|
||||
static func clearUserInfo() async {
|
||||
do {
|
||||
try await keychain.delete(forKey: StorageKeys.userInfo)
|
||||
try keychain.delete(forKey: StorageKeys.userInfo)
|
||||
await cacheActor.clearUserInfo()
|
||||
debugInfoSync("🗑️ UserInfo 已清除")
|
||||
} catch {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
// MARK: - API Service Protocol
|
||||
|
||||
@@ -136,10 +135,7 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
|
||||
requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: [])
|
||||
urlRequest.httpBody = requestBody
|
||||
// urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
if let httpBody = urlRequest.httpBody,
|
||||
let bodyString = String(data: httpBody, encoding: .utf8) {
|
||||
debugInfoSync("HTTP Body: \(bodyString)")
|
||||
}
|
||||
// HTTP Body 的详细输出由 APILogger 统一处理(带脱敏)。这里不再重复输出。
|
||||
} catch {
|
||||
let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)")
|
||||
await APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription)
|
||||
@@ -148,8 +144,7 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
|
||||
}
|
||||
|
||||
// 记录请求日志,传递完整的 headers 信息
|
||||
await APILogger
|
||||
.logRequest(request, url: url, body: requestBody, finalHeaders: headers)
|
||||
APILogger.logRequest(request, url: url, body: requestBody, finalHeaders: headers)
|
||||
|
||||
do {
|
||||
// 发起请求
|
||||
@@ -165,18 +160,16 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
|
||||
|
||||
// 检查数据大小
|
||||
if data.count > APIConfiguration.maxDataSize {
|
||||
await APILogger
|
||||
.logError(APIError.resourceTooLarge, url: url, duration: duration)
|
||||
APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration)
|
||||
await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription)
|
||||
throw APIError.resourceTooLarge
|
||||
}
|
||||
|
||||
// 记录响应日志
|
||||
await APILogger
|
||||
.logResponse(data: data, response: httpResponse, duration: duration)
|
||||
APILogger.logResponse(data: data, response: httpResponse, duration: duration)
|
||||
|
||||
// 性能警告
|
||||
await APILogger.logPerformanceWarning(duration: duration)
|
||||
APILogger.logPerformanceWarning(duration: duration)
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
guard 200...299 ~= httpResponse.statusCode else {
|
||||
@@ -196,7 +189,7 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
let decodedResponse = try decoder.decode(T.Response.self, from: data)
|
||||
await APILogger.logDecodedResponse(decodedResponse, type: T.Response.self)
|
||||
APILogger.logDecodedResponse(decodedResponse, type: T.Response.self)
|
||||
|
||||
// 请求成功,完成 loading
|
||||
await APILoadingManager.shared.finishLoading(loadingId)
|
||||
@@ -210,13 +203,13 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
|
||||
|
||||
} catch let error as APIError {
|
||||
let duration = Date().timeIntervalSince(startTime)
|
||||
await APILogger.logError(error, url: url, duration: duration)
|
||||
APILogger.logError(error, url: url, duration: duration)
|
||||
await APILoadingManager.shared.setError(loadingId, errorMessage: error.localizedDescription)
|
||||
throw error
|
||||
} catch {
|
||||
let duration = Date().timeIntervalSince(startTime)
|
||||
let apiError = mapSystemError(error)
|
||||
await APILogger.logError(apiError, url: url, duration: duration)
|
||||
APILogger.logError(apiError, url: url, duration: duration)
|
||||
await APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription)
|
||||
throw apiError
|
||||
}
|
||||
@@ -300,6 +293,14 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
|
||||
return error
|
||||
} else if let msg = json["msg"] as? String {
|
||||
return msg
|
||||
} else if let detail = json["detail"] as? String {
|
||||
return detail
|
||||
} else if let errorDescription = json["error_description"] as? String {
|
||||
return errorDescription
|
||||
} else if let errorDict = json["error"] as? [String: Any], let nestedMsg = errorDict["message"] as? String {
|
||||
return nestedMsg
|
||||
} else if let errors = json["errors"] as? [[String: Any]], let firstMsg = errors.first? ["message"] as? String {
|
||||
return firstMsg
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -349,7 +350,9 @@ actor MockAPIServiceActor: APIServiceProtocol, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TCA Dependency Integration
|
||||
// MARK: - TCA Dependency Integration (optional)
|
||||
#if canImport(ComposableArchitecture)
|
||||
import ComposableArchitecture
|
||||
private enum APIServiceKey: DependencyKey {
|
||||
static let liveValue: any APIServiceProtocol & Sendable = LiveAPIService()
|
||||
static let testValue: any APIServiceProtocol & Sendable = MockAPIServiceActor()
|
||||
@@ -361,6 +364,7 @@ extension DependencyValues {
|
||||
set { self[APIServiceKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - BaseRequest Dictionary Conversion
|
||||
extension BaseRequest {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
// MARK: - 响应数据模型
|
||||
|
||||
|
@@ -5,17 +5,17 @@ enum AppEnvironment {
|
||||
|
||||
struct AppConfig {
|
||||
static let current: AppEnvironment = {
|
||||
// #if DEBUG
|
||||
// return .development
|
||||
// #else
|
||||
#if DEBUG
|
||||
return .development
|
||||
#else
|
||||
return .production
|
||||
// #endif
|
||||
#endif
|
||||
}()
|
||||
|
||||
static var baseURL: String {
|
||||
switch current {
|
||||
case .development:
|
||||
return "http://beta.api.molistar.xyz"
|
||||
return "http://beta.api.molistar.xyz";//"http://beta.api.pekolive.com"
|
||||
case .production:
|
||||
return "https://api.epartylive.com"
|
||||
}
|
||||
|
@@ -1,114 +0,0 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
@Reducer
|
||||
struct SplashFeature {
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var isLoading = true
|
||||
var shouldShowMainApp = false
|
||||
var authenticationStatus: UserInfoManager.AuthenticationStatus = .notFound
|
||||
var isCheckingAuthentication = false
|
||||
|
||||
// 新增:导航目标
|
||||
var navigationDestination: NavigationDestination?
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:导航目标枚举
|
||||
enum NavigationDestination: Equatable {
|
||||
case login // 跳转到登录页面
|
||||
case main // 跳转到主页面
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
case onAppear
|
||||
case splashFinished
|
||||
case checkAuthentication
|
||||
case authenticationChecked(UserInfoManager.AuthenticationStatus)
|
||||
|
||||
// 新增:用户信息获取 actions
|
||||
case fetchUserInfo
|
||||
case userInfoFetched(Bool)
|
||||
|
||||
// 新增:导航 actions
|
||||
case navigateToLogin
|
||||
case navigateToMain
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService // 新增:API服务依赖
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
state.isLoading = true
|
||||
state.shouldShowMainApp = false
|
||||
state.authenticationStatus = UserInfoManager.AuthenticationStatus.notFound
|
||||
state.isCheckingAuthentication = false
|
||||
state.navigationDestination = nil
|
||||
|
||||
// 1秒延迟后显示主应用 (iOS 15.5+ 兼容)
|
||||
return .run { send in
|
||||
try await Task.sleep(nanoseconds: 1_000_000_000) // 1秒 = 1,000,000,000 纳秒
|
||||
await send(.splashFinished)
|
||||
}
|
||||
case .splashFinished:
|
||||
state.isLoading = false
|
||||
|
||||
// Splash 完成后,开始检查认证状态
|
||||
return .send(.checkAuthentication)
|
||||
|
||||
case .checkAuthentication:
|
||||
state.isCheckingAuthentication = true
|
||||
|
||||
// 异步检查认证状态
|
||||
return .run { send in
|
||||
let authStatus = await UserInfoManager.checkAuthenticationStatus()
|
||||
await send(.authenticationChecked(authStatus))
|
||||
}
|
||||
|
||||
case let .authenticationChecked(status):
|
||||
state.isCheckingAuthentication = false
|
||||
state.authenticationStatus = status
|
||||
|
||||
// 根据认证状态决定下一步操作
|
||||
if status.canAutoLogin {
|
||||
debugInfoSync("🎉 自动登录成功,开始获取用户信息")
|
||||
// 新增:认证成功后自动获取用户信息
|
||||
return .send(.fetchUserInfo)
|
||||
} else {
|
||||
debugInfoSync("🔑 需要手动登录")
|
||||
return .send(.navigateToLogin)
|
||||
}
|
||||
|
||||
case .fetchUserInfo:
|
||||
// 新增:获取用户信息
|
||||
return .run { send in
|
||||
let success = await UserInfoManager.autoFetchUserInfoOnAppLaunch(apiService: apiService)
|
||||
await send(.userInfoFetched(success))
|
||||
}
|
||||
|
||||
case let .userInfoFetched(success):
|
||||
if success {
|
||||
debugInfoSync("✅ 用户信息获取成功,进入主页")
|
||||
} else {
|
||||
debugInfoSync("⚠️ 用户信息获取失败,但仍进入主页")
|
||||
}
|
||||
return .send(.navigateToMain)
|
||||
|
||||
case .navigateToLogin:
|
||||
state.navigationDestination = .login
|
||||
return .none
|
||||
|
||||
case .navigateToMain:
|
||||
state.navigationDestination = .main
|
||||
state.shouldShowMainApp = true
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,5 +15,7 @@
|
||||
<array>
|
||||
<string>Bayon-Regular.ttf</string>
|
||||
</array>
|
||||
<key>API_SIGNING_KEY</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
89
yana/MVVM/CreateFeedPage.swift
Normal file
89
yana/MVVM/CreateFeedPage.swift
Normal file
@@ -0,0 +1,89 @@
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class CreateFeedViewModel: ObservableObject {
|
||||
@Published var content: String = ""
|
||||
@Published var selectedImages: [UIImage] = []
|
||||
@Published var isPublishing: Bool = false
|
||||
@Published var errorMessage: String? = nil
|
||||
var canPublish: Bool { !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !selectedImages.isEmpty }
|
||||
}
|
||||
|
||||
struct CreateFeedPage: View {
|
||||
@StateObject private var viewModel = CreateFeedViewModel()
|
||||
let onDismiss: () -> Void
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { _ in
|
||||
ZStack {
|
||||
Color(hex: 0x0C0527).ignoresSafeArea()
|
||||
VStack(spacing: 16) {
|
||||
HStack {
|
||||
Button(action: onDismiss) {
|
||||
Image(systemName: "xmark")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
}
|
||||
Spacer()
|
||||
Text(LocalizedString("createFeed.title", comment: "Image & Text Publish"))
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
Spacer()
|
||||
Button(action: publish) {
|
||||
if viewModel.isPublishing {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
} else {
|
||||
Text(LocalizedString("createFeed.publish", comment: "Publish"))
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
}
|
||||
}
|
||||
.disabled(!viewModel.canPublish || viewModel.isPublishing)
|
||||
.opacity((!viewModel.canPublish || viewModel.isPublishing) ? 0.6 : 1)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 8).fill(Color(hex: 0x1C143A))
|
||||
if viewModel.content.isEmpty {
|
||||
Text(LocalizedString("createFeed.enterContent", comment: "Enter Content"))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
TextEditor(text: $viewModel.content)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.scrollContentBackground(.hidden)
|
||||
.frame(height: 200)
|
||||
}
|
||||
.frame(height: 200)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
if let error = viewModel.errorMessage {
|
||||
Text(error)
|
||||
.foregroundColor(.red)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
}
|
||||
|
||||
private func publish() {
|
||||
viewModel.isPublishing = true
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
viewModel.isPublishing = false
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ class LoginViewModel: ObservableObject {
|
||||
|
||||
// MARK: - Callbacks
|
||||
var onLoginSuccess: (() -> Void)?
|
||||
private var hasSentSuccess: Bool = false
|
||||
|
||||
// MARK: - Public Methods
|
||||
func onIDLoginTapped() {
|
||||
@@ -47,22 +48,20 @@ class LoginViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
func onLoginCompleted() {
|
||||
guard !hasSentSuccess else { return }
|
||||
isAnyLoginCompleted = true
|
||||
showIDLogin = false
|
||||
showEmailLogin = false
|
||||
hasSentSuccess = true
|
||||
onLoginSuccess?()
|
||||
}
|
||||
|
||||
func onBackFromIDLogin() {
|
||||
showIDLogin = false
|
||||
if isAnyLoginCompleted {
|
||||
onLoginSuccess?()
|
||||
}
|
||||
}
|
||||
|
||||
func onBackFromEmailLogin() {
|
||||
showEmailLogin = false
|
||||
if isAnyLoginCompleted {
|
||||
onLoginSuccess?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,78 +72,76 @@ struct LoginPage: View {
|
||||
let onLoginSuccess: () -> Void
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
backgroundView
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
backgroundView
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Image("top")
|
||||
.resizable()
|
||||
.aspectRatio(375/400, contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
HStack {
|
||||
Text(LocalizedString("login.app_title", comment: ""))
|
||||
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
|
||||
.foregroundColor(.white)
|
||||
.padding(.leading, 20)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.bottom, 20) // 距离 top 图片底部的间距
|
||||
VStack(spacing: 0) {
|
||||
Image("top")
|
||||
.resizable()
|
||||
.aspectRatio(375/400, contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
HStack {
|
||||
Text(LocalizedString("login.app_title", comment: ""))
|
||||
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
|
||||
.foregroundColor(.white)
|
||||
.padding(.leading, 20)
|
||||
Spacer()
|
||||
|
||||
bottomSection
|
||||
}
|
||||
.padding(.bottom, 20)
|
||||
|
||||
// 语言设置按钮 - 固定在页面右上角
|
||||
languageSettingsButton
|
||||
.position(x: geometry.size.width - 40, y: 60)
|
||||
Spacer()
|
||||
|
||||
APILoadingEffectView()
|
||||
bottomSection
|
||||
}
|
||||
|
||||
// 语言设置按钮 - 固定在页面右上角
|
||||
languageSettingsButton
|
||||
.position(x: geometry.size.width - 40, y: 60)
|
||||
|
||||
APILoadingEffectView()
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.navigationBarHidden(true)
|
||||
.navigationDestination(isPresented: $viewModel.showIDLogin) {
|
||||
IDLoginPage(
|
||||
onBack: {
|
||||
viewModel.onBackFromIDLogin()
|
||||
},
|
||||
onLoginSuccess: {
|
||||
viewModel.onLoginCompleted()
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
.navigationDestination(isPresented: $viewModel.showIDLogin) {
|
||||
IDLoginPage(
|
||||
onBack: {
|
||||
viewModel.onBackFromIDLogin()
|
||||
},
|
||||
onLoginSuccess: {
|
||||
viewModel.onLoginCompleted()
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.navigationDestination(isPresented: $viewModel.showEmailLogin) {
|
||||
EMailLoginPage(
|
||||
onBack: {
|
||||
viewModel.onBackFromEmailLogin()
|
||||
},
|
||||
onLoginSuccess: {
|
||||
viewModel.onLoginCompleted()
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showLanguageSettings) {
|
||||
LanguageSettingsView(isPresented: $viewModel.showLanguageSettings)
|
||||
}
|
||||
.webView(
|
||||
isPresented: $viewModel.showUserAgreement,
|
||||
url: APIConfiguration.webURL(for: .userAgreement)
|
||||
}
|
||||
.navigationDestination(isPresented: $viewModel.showEmailLogin) {
|
||||
EMailLoginPage(
|
||||
onBack: {
|
||||
viewModel.onBackFromEmailLogin()
|
||||
},
|
||||
onLoginSuccess: {
|
||||
viewModel.onLoginCompleted()
|
||||
}
|
||||
)
|
||||
.webView(
|
||||
isPresented: $viewModel.showPrivacyPolicy,
|
||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||||
)
|
||||
.alert(LocalizedString("login.agreement_alert_title", comment: ""), isPresented: $viewModel.showAgreementAlert) {
|
||||
Button(LocalizedString("login.agreement_alert_confirm", comment: "")) { }
|
||||
} message: {
|
||||
Text(LocalizedString("login.agreement_alert_message", comment: ""))
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showLanguageSettings) {
|
||||
LanguageSettingsView(isPresented: $viewModel.showLanguageSettings)
|
||||
}
|
||||
.webView(
|
||||
isPresented: $viewModel.showUserAgreement,
|
||||
url: APIConfiguration.webURL(for: .userAgreement)
|
||||
)
|
||||
.webView(
|
||||
isPresented: $viewModel.showPrivacyPolicy,
|
||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||||
)
|
||||
.alert(LocalizedString("login.agreement_alert_title", comment: ""), isPresented: $viewModel.showAgreementAlert) {
|
||||
Button(LocalizedString("login.agreement_alert_confirm", comment: "")) { }
|
||||
} message: {
|
||||
Text(LocalizedString("login.agreement_alert_message", comment: ""))
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.onLoginSuccess = onLoginSuccess
|
||||
|
@@ -31,9 +31,10 @@ struct MainPage: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: String.self) { destination in
|
||||
switch destination {
|
||||
case "setting":
|
||||
.navigationDestination(for: String.self) { _ in EmptyView() }
|
||||
.navigationDestination(for: AppRoute.self) { route in
|
||||
switch route {
|
||||
case .setting:
|
||||
SettingPage(
|
||||
onBack: {
|
||||
viewModel.navigationPath.removeLast()
|
||||
@@ -43,6 +44,12 @@ struct MainPage: View {
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
case .publish:
|
||||
CreateFeedPage(
|
||||
onDismiss: {
|
||||
viewModel.navigationPath.removeLast()
|
||||
}
|
||||
)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
|
@@ -1,182 +0,0 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
// MARK: - Splash ViewModel
|
||||
|
||||
@MainActor
|
||||
class SplashViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var isLoading = true
|
||||
@Published var shouldShowMainApp = false
|
||||
@Published var authenticationStatus: UserInfoManager.AuthenticationStatus = .notFound
|
||||
@Published var isCheckingAuthentication = false
|
||||
@Published var navigationDestination: NavigationDestination?
|
||||
|
||||
// MARK: - Private Properties
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Navigation Destination
|
||||
enum NavigationDestination: Equatable {
|
||||
case login
|
||||
case main
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
init() {
|
||||
setupBindings()
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
func onAppear() {
|
||||
isLoading = true
|
||||
shouldShowMainApp = false
|
||||
authenticationStatus = .notFound
|
||||
isCheckingAuthentication = false
|
||||
navigationDestination = nil
|
||||
|
||||
// 1秒延迟后显示主应用
|
||||
Task {
|
||||
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
await MainActor.run {
|
||||
self.splashFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func splashFinished() {
|
||||
isLoading = false
|
||||
checkAuthentication()
|
||||
}
|
||||
|
||||
func checkAuthentication() {
|
||||
isCheckingAuthentication = true
|
||||
|
||||
Task {
|
||||
let authStatus = await UserInfoManager.checkAuthenticationStatus()
|
||||
await MainActor.run {
|
||||
self.authenticationChecked(authStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func authenticationChecked(_ status: UserInfoManager.AuthenticationStatus) {
|
||||
isCheckingAuthentication = false
|
||||
authenticationStatus = status
|
||||
|
||||
if status.canAutoLogin {
|
||||
debugInfoSync("🎉 自动登录成功,开始获取用户信息")
|
||||
fetchUserInfo()
|
||||
} else {
|
||||
debugInfoSync("🔑 需要手动登录")
|
||||
navigateToLogin()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchUserInfo() {
|
||||
Task {
|
||||
let success = await UserInfoManager.autoFetchUserInfoOnAppLaunch(apiService: LiveAPIService())
|
||||
await MainActor.run {
|
||||
self.userInfoFetched(success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userInfoFetched(_ success: Bool) {
|
||||
if success {
|
||||
debugInfoSync("✅ 用户信息获取成功,进入主页")
|
||||
} else {
|
||||
debugInfoSync("⚠️ 用户信息获取失败,但仍进入主页")
|
||||
}
|
||||
navigateToMain()
|
||||
}
|
||||
|
||||
func navigateToLogin() {
|
||||
navigationDestination = .login
|
||||
}
|
||||
|
||||
func navigateToMain() {
|
||||
navigationDestination = .main
|
||||
shouldShowMainApp = true
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func setupBindings() {
|
||||
// 可以在这里设置 Combine 绑定
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Splash View
|
||||
|
||||
struct Splash: View {
|
||||
@StateObject private var viewModel = SplashViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Group {
|
||||
// 根据导航目标显示不同页面
|
||||
if let navigationDestination = viewModel.navigationDestination {
|
||||
switch navigationDestination {
|
||||
case .login:
|
||||
// 显示登录页面
|
||||
LoginPage(
|
||||
onLoginSuccess: {
|
||||
// 登录成功后导航到主页面
|
||||
viewModel.navigateToMain()
|
||||
}
|
||||
)
|
||||
case .main:
|
||||
// 显示主应用页面
|
||||
MainPage(
|
||||
onLogout: {
|
||||
viewModel.navigateToLogin()
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 显示启动画面
|
||||
splashContent
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.onAppear()
|
||||
}
|
||||
|
||||
// API Loading 效果视图 - 显示在所有内容之上
|
||||
APILoadingEffectView()
|
||||
}
|
||||
}
|
||||
|
||||
// 启动画面内容
|
||||
private var splashContent: some View {
|
||||
ZStack {
|
||||
// 背景图片 - 全屏显示
|
||||
LoginBackgroundView()
|
||||
|
||||
VStack(spacing: 32) {
|
||||
Spacer()
|
||||
.frame(height: 200) // 与 storyboard 中的约束对应
|
||||
|
||||
// Logo 图片 - 100x100
|
||||
Image("logo")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
// 应用标题 - 白色,40pt字体
|
||||
Text(LocalizedString("splash.title", comment: "E-Parti"))
|
||||
.font(.system(size: 40, weight: .regular))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// Splash()
|
||||
//}
|
||||
|
||||
#Preview {
|
||||
Splash()
|
||||
}
|
63
yana/MVVM/SplashV2.swift
Normal file
63
yana/MVVM/SplashV2.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SplashV2: View {
|
||||
@State private var showLogin = false
|
||||
@State private var showMain = false
|
||||
@State private var hasCheckedAuth = false
|
||||
private let splashTransitionAnimation: Animation = .easeInOut(duration: 0.25)
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if showMain {
|
||||
MainPage(onLogout: {
|
||||
showMain = false
|
||||
showLogin = true
|
||||
})
|
||||
} else if showLogin {
|
||||
NavigationStack {
|
||||
LoginPage(onLoginSuccess: {
|
||||
showMain = true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ZStack {
|
||||
LoginBackgroundView()
|
||||
VStack(spacing: 32) {
|
||||
Spacer().frame(height: 200)
|
||||
Image("logo")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 100, height: 100)
|
||||
Text(LocalizedString("splash.title", comment: "E-Parti"))
|
||||
.font(.system(size: 40, weight: .regular))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
guard !hasCheckedAuth else { return }
|
||||
hasCheckedAuth = true
|
||||
Task { @MainActor in
|
||||
debugInfoSync("🚀 SplashV2 启动,开始检查登录缓存")
|
||||
let status = await UserInfoManager.checkAuthenticationStatus()
|
||||
if status.canAutoLogin {
|
||||
debugInfoSync("✅ 检测到可自动登录,尝试预取用户信息")
|
||||
_ = await UserInfoManager.autoFetchUserInfoOnAppLaunch(apiService: LiveAPIService())
|
||||
withAnimation(splashTransitionAnimation) {
|
||||
showMain = true
|
||||
}
|
||||
} else {
|
||||
debugInfoSync("🔑 未登录或缓存无效,进入登录页")
|
||||
withAnimation(splashTransitionAnimation) {
|
||||
showLogin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -56,9 +56,9 @@ class MainViewModel: ObservableObject {
|
||||
func onTopRightButtonTapped() {
|
||||
switch selectedTab {
|
||||
case .feed:
|
||||
onAddButtonTapped?()
|
||||
navigationPath.append(AppRoute.publish)
|
||||
case .me:
|
||||
navigationPath.append("setting")
|
||||
navigationPath.append(AppRoute.setting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11
yana/Utils/Navigation/AppRoute.swift
Normal file
11
yana/Utils/Navigation/AppRoute.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
/// 应用统一路由定义
|
||||
enum AppRoute: Hashable {
|
||||
case login
|
||||
case main
|
||||
case setting
|
||||
case publish
|
||||
}
|
||||
|
||||
|
38
yana/Utils/Network/APIService+Combine.swift
Normal file
38
yana/Utils/Network/APIService+Combine.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import Foundation
|
||||
@preconcurrency import Combine
|
||||
|
||||
// 以 @unchecked Sendable 包装 Future 的 promise,安全地跨并发域传递
|
||||
private final class PromiseBox<Output, Failure: Error>: @unchecked Sendable {
|
||||
private let fulfill: (Result<Output, Failure>) -> Void
|
||||
init(_ fulfill: @escaping (Result<Output, Failure>) -> Void) { self.fulfill = fulfill }
|
||||
func complete(_ result: Result<Output, Failure>) { fulfill(result) }
|
||||
}
|
||||
|
||||
extension APIServiceProtocol {
|
||||
/// 将 async/await 的请求桥接为 Combine Publisher
|
||||
/// - Parameter request: 符合 APIRequestProtocol 的请求对象
|
||||
/// - Returns: AnyPublisher<T.Response, APIError>
|
||||
func requestPublisher<T: APIRequestProtocol>(_ request: T) -> AnyPublisher<T.Response, APIError> {
|
||||
Deferred {
|
||||
Future { promise in
|
||||
let box = PromiseBox<T.Response, APIError>(promise)
|
||||
Task(priority: .userInitiated) {
|
||||
let result: Result<T.Response, APIError>
|
||||
do {
|
||||
let value = try await self.request(request)
|
||||
result = .success(value)
|
||||
} catch let apiError as APIError {
|
||||
result = .failure(apiError)
|
||||
} catch {
|
||||
result = .failure(.unknown(error.localizedDescription))
|
||||
}
|
||||
box.complete(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
|
54
yana/Utils/Network/DeviceContext.swift
Normal file
54
yana/Utils/Network/DeviceContext.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
struct DeviceContext: Sendable {
|
||||
let languageCode: String
|
||||
let osName: String
|
||||
let osVersion: String
|
||||
let deviceModel: String
|
||||
let deviceId: String
|
||||
let appName: String
|
||||
let appVersion: String
|
||||
let channel: String
|
||||
let screenScale: String
|
||||
|
||||
static let shared: DeviceContext = {
|
||||
// 仅在主线程读取一次 UIKit/Bundle 信息
|
||||
let language = Locale.current.language.languageCode?.identifier ?? "en"
|
||||
let osName = "iOS"
|
||||
let osVersion = UIDevice.current.systemVersion
|
||||
let deviceModel = UIDevice.current.model
|
||||
let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
||||
let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "eparty"
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
||||
#if DEBUG
|
||||
let channel = "molistar_enterprise"
|
||||
#else
|
||||
let channel = "appstore"
|
||||
#endif
|
||||
let scale = String(format: "%.2f", Double(UIScreen.main.scale))
|
||||
|
||||
return DeviceContext(
|
||||
languageCode: language,
|
||||
osName: osName,
|
||||
osVersion: osVersion,
|
||||
deviceModel: deviceModel,
|
||||
deviceId: deviceId,
|
||||
appName: appName,
|
||||
appVersion: appVersion,
|
||||
channel: channel,
|
||||
screenScale: scale
|
||||
)
|
||||
}()
|
||||
}
|
||||
|
||||
enum UserAgentProvider {
|
||||
@MainActor
|
||||
static func userAgent() -> String {
|
||||
let ctx = DeviceContext.shared
|
||||
return "\(ctx.appName)/\(ctx.appVersion) (\(ctx.deviceModel); \(ctx.osName) \(ctx.osVersion); Scale/\(ctx.screenScale))"
|
||||
}
|
||||
}
|
||||
|
||||
|
33
yana/Utils/Network/NetworkMonitor.swift
Normal file
33
yana/Utils/Network/NetworkMonitor.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
/// 监听系统网络状态并缓存最近结果
|
||||
/// WiFi=2, 蜂窝=1, 未知/无网络=0(与现有代码语义对齐)
|
||||
final class NetworkMonitor: @unchecked Sendable {
|
||||
static let shared = NetworkMonitor()
|
||||
private let monitor = NWPathMonitor()
|
||||
private let queue = DispatchQueue(label: "com.yana.network.monitor")
|
||||
private var _currentType: Int = 2 // 默认与历史保持一致
|
||||
var currentType: Int { _currentType }
|
||||
private init() {
|
||||
monitor.pathUpdateHandler = { [weak self] path in
|
||||
guard let self = self else { return }
|
||||
let type: Int
|
||||
if path.status == .satisfied {
|
||||
if path.usesInterfaceType(.wifi) { type = 2 }
|
||||
else if path.usesInterfaceType(.cellular) { type = 1 }
|
||||
else { type = 0 }
|
||||
} else {
|
||||
type = 0
|
||||
}
|
||||
// 更新缓存(主线程或任一队列均可,这里选择主线程与 UI 一致)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?._currentType = type
|
||||
}
|
||||
}
|
||||
monitor.start(queue: queue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -12,11 +12,10 @@ import Security
|
||||
/// - 完善的错误处理
|
||||
/// - 线程安全操作
|
||||
/// - 可配置的访问控制级别
|
||||
@MainActor
|
||||
final class KeychainManager {
|
||||
final class KeychainManager: @unchecked Sendable {
|
||||
|
||||
// MARK: - 单例
|
||||
@MainActor static let shared = KeychainManager()
|
||||
static let shared = KeychainManager()
|
||||
private init() {}
|
||||
|
||||
// MARK: - 配置常量
|
||||
|
32
yana/Utils/Security/SigningKeyProvider.swift
Normal file
32
yana/Utils/Security/SigningKeyProvider.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
|
||||
/// 提供 API 签名密钥的统一入口
|
||||
/// - 优先从 Info.plist 读取键 `API_SIGNING_KEY`
|
||||
/// - Debug 环境下若缺失,回退到历史密钥以避免开发阶段中断,同时输出告警
|
||||
/// - Release 环境下若缺失,输出错误并返回空字符串(应在发布前配置)
|
||||
enum SigningKeyProvider {
|
||||
/// Info.plist 中的键名
|
||||
private static let plistKey = "API_SIGNING_KEY"
|
||||
|
||||
/// 获取签名密钥
|
||||
static func signingKey() -> String {
|
||||
if let key = Bundle.main.object(forInfoDictionaryKey: plistKey) as? String,
|
||||
!key.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return key
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// 仅在 Debug 回退,避免打断本地调试;请尽快在 Info.plist 配置 API_SIGNING_KEY
|
||||
let legacy = "rpbs6us1m8r2j9g6u06ff2bo18orwaya"
|
||||
debugWarnSync("⚠️ API_SIGNING_KEY 未配置,Debug 使用历史回退密钥(请尽快配置 Info.plist)")
|
||||
return legacy
|
||||
#else
|
||||
debugErrorSync("❌ 缺少 API_SIGNING_KEY,请在 Info.plist 中配置")
|
||||
assertionFailure("Missing API_SIGNING_KEY in Info.plist")
|
||||
return ""
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,91 +0,0 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct SplashView: View {
|
||||
let store: StoreOf<SplashFeature>
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Group {
|
||||
// 根据导航目标显示不同页面
|
||||
if let navigationDestination = store.navigationDestination {
|
||||
switch navigationDestination {
|
||||
case .login:
|
||||
// 显示登录页面
|
||||
LoginView(
|
||||
store: Store(
|
||||
initialState: LoginFeature.State()
|
||||
) {
|
||||
LoginFeature()
|
||||
},
|
||||
onLoginSuccess: {
|
||||
// 登录成功后导航到主页面
|
||||
store.send(.navigateToMain)
|
||||
}
|
||||
)
|
||||
case .main:
|
||||
// 显示主应用页面
|
||||
MainView(
|
||||
store: Store(
|
||||
initialState: MainFeature.State()
|
||||
) {
|
||||
MainFeature()
|
||||
},
|
||||
onLogout: {
|
||||
store.send(.navigateToLogin)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 显示启动画面
|
||||
splashContent
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
|
||||
// API Loading 效果视图 - 显示在所有内容之上
|
||||
APILoadingEffectView()
|
||||
}
|
||||
}
|
||||
|
||||
// 启动画面内容
|
||||
private var splashContent: some View {
|
||||
ZStack {
|
||||
// 背景图片 - 全屏显示
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
VStack(spacing: 32) {
|
||||
Spacer()
|
||||
.frame(height: 200) // 与 storyboard 中的约束对应
|
||||
|
||||
// Logo 图片 - 100x100
|
||||
Image("logo")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
// 应用标题 - 白色,40pt字体
|
||||
Text(LocalizedString("splash.title", comment: "E-Parti"))
|
||||
.font(.system(size: 40, weight: .regular))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// SplashView(
|
||||
// store: Store(
|
||||
// initialState: SplashFeature.State()
|
||||
// ) {
|
||||
// SplashFeature()
|
||||
// }
|
||||
// )
|
||||
//}
|
@@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
@main
|
||||
struct yanaApp: App {
|
||||
@@ -24,7 +23,7 @@ struct yanaApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
Splash()
|
||||
SplashV2()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user