
- 在ContentView中根据编译模式初始化日志级别,确保调试信息的灵活性。 - 在APILogger中使用actor封装日志级别,增强并发安全性。 - 新增通用底部Tab栏组件,优化MainPage中的底部导航逻辑,提升代码可维护性。 - 移除冗余的AppRootView和MainView,简化视图结构,提升代码整洁性。
313 lines
13 KiB
Swift
313 lines
13 KiB
Swift
import Foundation
|
||
|
||
// MARK: - API Logger
|
||
class APILogger {
|
||
enum LogLevel {
|
||
case none
|
||
case basic
|
||
case detailed
|
||
}
|
||
|
||
// 使用 actor 封装可变全局状态以保证并发安全
|
||
actor Config {
|
||
static let shared = Config()
|
||
#if DEBUG
|
||
private var level: LogLevel = .detailed
|
||
#else
|
||
private var level: LogLevel = .none
|
||
#endif
|
||
func get() -> LogLevel { level }
|
||
func set(_ newLevel: LogLevel) { level = newLevel }
|
||
}
|
||
|
||
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
|
||
static func logRequest<T: APIRequestProtocol>(
|
||
_ request: T,
|
||
url: URL,
|
||
body: Data?,
|
||
finalHeaders: [String: String]? = nil
|
||
) {
|
||
#if !DEBUG
|
||
return
|
||
#else
|
||
Task {
|
||
let level = await Config.shared.get()
|
||
guard level != .none else { return }
|
||
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")
|
||
|
||
// 显示最终的完整 headers(包括默认 headers 和自定义 headers)
|
||
if let headers = finalHeaders, !headers.isEmpty {
|
||
if level == .detailed {
|
||
debugInfoSync("📋 Final Headers (包括默认 + 自定义):")
|
||
let masked = maskHeaders(headers)
|
||
for (key, value) in masked.sorted(by: { $0.key < $1.key }) {
|
||
debugInfoSync(" \(key): \(value)")
|
||
}
|
||
} else if level == .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 {
|
||
debugInfoSync("📋 Custom Headers:")
|
||
let masked = maskHeaders(customHeaders)
|
||
for (key, value) in masked.sorted(by: { $0.key < $1.key }) {
|
||
debugInfoSync(" \(key): \(value)")
|
||
}
|
||
} else {
|
||
debugInfoSync("📋 Headers: 使用默认 headers")
|
||
}
|
||
|
||
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 level == .detailed {
|
||
let pretty = maskedBodyString(from: body)
|
||
debugInfoSync("📦 Request Body: \n\(pretty)")
|
||
|
||
// 仅提示包含基础参数,避免跨 actor 读取 UIKit 信息
|
||
if request.includeBaseParameters {
|
||
debugInfoSync("📱 Base Parameters: 已自动注入")
|
||
}
|
||
} else if level == .basic {
|
||
let size = body?.count ?? 0
|
||
debugInfoSync("📦 Request Body: \(formatBytes(size))")
|
||
|
||
// 基础模式也显示是否包含基础参数
|
||
if request.includeBaseParameters {
|
||
debugInfoSync("📱 Base Parameters: 已自动注入")
|
||
}
|
||
}
|
||
debugInfoSync("=====================================")
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
// MARK: - Response Logging
|
||
static func logResponse(data: Data, response: HTTPURLResponse, duration: TimeInterval) {
|
||
#if !DEBUG
|
||
return
|
||
#else
|
||
Task {
|
||
let level = await Config.shared.get()
|
||
guard level != .none else { return }
|
||
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))")
|
||
|
||
if level == .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)")
|
||
}
|
||
|
||
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("=====================================")
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
// MARK: - Error Logging
|
||
static func logError(_ error: Error, url: URL?, duration: TimeInterval) {
|
||
#if !DEBUG
|
||
return
|
||
#else
|
||
Task {
|
||
let level = await Config.shared.get()
|
||
guard level != .none else { return }
|
||
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)")
|
||
}
|
||
|
||
if let apiError = error as? APIError {
|
||
debugErrorSync("🚨 API Error: \(apiError.localizedDescription)")
|
||
} else {
|
||
debugErrorSync("🚨 System Error: \(error.localizedDescription)")
|
||
}
|
||
|
||
if level == .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")
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
// MARK: - Decoded Response Logging
|
||
static func logDecodedResponse<T>(_ response: T, type: T.Type) {
|
||
#if !DEBUG
|
||
return
|
||
#else
|
||
Task {
|
||
let level = await Config.shared.get()
|
||
guard level == .detailed else { return }
|
||
logQueue.async {
|
||
let timestamp = dateFormatter.string(from: Date())
|
||
debugInfoSync("🎯 [Decoded Response] [\(timestamp)] Type: \(type)")
|
||
debugInfoSync("=====================================\n")
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
// MARK: - Helper Methods
|
||
private static func formatBytes(_ bytes: Int) -> String {
|
||
let formatter = ByteCountFormatter()
|
||
formatter.allowedUnits = [.useKB, .useMB]
|
||
formatter.countStyle = .file
|
||
return formatter.string(fromByteCount: Int64(bytes))
|
||
}
|
||
|
||
// MARK: - Performance Logging
|
||
static func logPerformanceWarning(duration: TimeInterval, threshold: TimeInterval = 5.0) {
|
||
#if !DEBUG
|
||
return
|
||
#else
|
||
Task {
|
||
let level = await Config.shared.get()
|
||
guard level != .none && duration > threshold else { return }
|
||
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")
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
}
|