Bladeren bron

feat: 补充 fireBase 的相关配置,以及谷歌登录逻辑,增加个人信息拉取逻辑

陈文艺 4 maanden geleden
bovenliggende
commit
2a2bb58e4a
33 gewijzigde bestanden met toevoegingen van 1320 en 270 verwijderingen
  1. 22 4
      Lanu.xcodeproj/project.pbxproj
  2. 17 6
      Lanu/AppDelegate.swift
  3. 72 0
      Lanu/Common/Logger/LNLogger.swift
  4. 1 1
      Lanu/Common/Logger/LNLoggerFormater.swift
  5. 3 6
      Lanu/Common/Storage/LNUserDefaults.swift
  6. 1 1
      Lanu/Common/Storage/LNUserDefaultsKey.swift
  7. 76 0
      Lanu/Common/Utils/AppUtils.swift
  8. 22 0
      Lanu/Common/Utils/String+Extension.swift
  9. 32 0
      Lanu/Common/Utils/UIView+Extension.swift
  10. 1 1
      Lanu/Common/Views/Base/LNBaseViewController.swift
  11. 2 1
      Lanu/Common/Views/Base/LNNavigationController.swift
  12. 12 0
      Lanu/Config_Debug.xcconfig
  13. 12 0
      Lanu/Config_Release.xcconfig
  14. 30 0
      Lanu/GoogleService-Info-Debug.plist
  15. 5 9
      Lanu/GoogleService-Info-Release.plist
  16. 28 0
      Lanu/Info.plist
  17. 45 0
      Lanu/Manager/Account/LNAccountManager.swift
  18. 30 0
      Lanu/Manager/Account/Network/LNHttpManager+Login.swift
  19. 22 0
      Lanu/Manager/Account/Network/LNLoginResponse.swift
  20. 7 10
      Lanu/Manager/LNEventDeliver.swift
  21. 0 229
      Lanu/Manager/Network/LNHTTPManager.swift
  22. 375 0
      Lanu/Manager/Network/LNHttpManager.swift
  23. 1 1
      Lanu/Manager/Network/LNNetworkConfig.swift
  24. 176 0
      Lanu/Manager/Network/Monitor/LNNetworkMonitor.swift
  25. 11 0
      Lanu/Manager/Network/Response/LNHttpEmptyResponse.swift
  26. 0 0
      Lanu/Manager/Network/Response/LNHttpResponse.swift
  27. 92 0
      Lanu/Manager/Profile/LNProfileManager.swift
  28. 31 0
      Lanu/Manager/Profile/LNUserProfileInfo.swift
  29. 45 0
      Lanu/Manager/Profile/Network/LNHttpManager+Profile.swift
  30. 28 0
      Lanu/Manager/Profile/Network/LNProfileResponse.swift
  31. 39 1
      Lanu/SceneDelegate.swift
  32. 74 0
      Lanu/Views/Login/LNLoginViewController.swift
  33. 8 0
      Lanu/Views/Main/LNMainViewController.swift

+ 22 - 4
Lanu.xcodeproj/project.pbxproj

@@ -34,27 +34,41 @@
 				AppDelegate.swift,
 				Assets.xcassets,
 				Common/Config/LNAppConfig.swift,
+				Common/Logger/LNLogger.swift,
 				Common/Logger/LNLoggerFormater.swift,
 				Common/Storage/LNUserDefaults.swift,
 				Common/Storage/LNUserDefaultsKey.swift,
 				"Common/Theme/UIColor+Theme.swift",
 				"Common/Theme/UIFont+Theme.swift",
+				Common/Utils/AppUtils.swift,
 				"Common/Utils/String+Extension.swift",
 				"Common/Utils/UIColor+Extension.swift",
+				"Common/Utils/UIView+Extension.swift",
 				Common/Views/Base/LNBaseViewController.swift,
 				Common/Views/Base/LNNavigationController.swift,
-				"GoogleService-Info.plist",
+				Config_Debug.xcconfig,
+				Config_Release.xcconfig,
+				"GoogleService-Info-Debug.plist",
+				"GoogleService-Info-Release.plist",
 				Localizable.xcstrings,
 				Manager/Account/LNAccountManager.swift,
+				"Manager/Account/Network/LNHttpManager+Login.swift",
+				Manager/Account/Network/LNLoginResponse.swift,
 				Manager/LNDelayTask.swift,
 				Manager/LNEventDeliver.swift,
-				Manager/Network/LNHTTPManager.swift,
-				Manager/Network/LNHttpResponse.swift,
+				Manager/Network/LNHttpManager.swift,
 				Manager/Network/LNNetworkConfig.swift,
+				Manager/Network/Monitor/LNNetworkMonitor.swift,
+				Manager/Network/Response/LNHttpEmptyResponse.swift,
+				Manager/Network/Response/LNHttpResponse.swift,
 				Manager/Order/LNOrderManager.swift,
 				Manager/Profile/LNProfileManager.swift,
+				Manager/Profile/LNUserProfileInfo.swift,
+				"Manager/Profile/Network/LNHttpManager+Profile.swift",
+				Manager/Profile/Network/LNProfileResponse.swift,
 				Manager/Purchase/LNPurchaseManager.swift,
 				SceneDelegate.swift,
+				Views/Login/LNLoginViewController.swift,
 				Views/Main/LNMainViewController.swift,
 			);
 			target = FBFE13BF2EBC39B000DCE6E9 /* Lanu */;
@@ -260,7 +274,7 @@
 					"@executable_path/Frameworks",
 				);
 				MARKETING_VERSION = 1.0;
-				PRODUCT_BUNDLE_IDENTIFIER = com.jiehe.lanu;
+				PRODUCT_BUNDLE_IDENTIFIER = com.jiehe.lanu.debug;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
 				SWIFT_APPROACHABLE_CONCURRENCY = YES;
@@ -308,6 +322,8 @@
 		};
 		FBFE13D62EBC39B100DCE6E9 /* Debug */ = {
 			isa = XCBuildConfiguration;
+			baseConfigurationReferenceAnchor = FB1A37952EBE04E40063ED8C /* Lanu */;
+			baseConfigurationReferenceRelativePath = Config_Debug.xcconfig;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@@ -373,6 +389,8 @@
 		};
 		FBFE13D72EBC39B100DCE6E9 /* Release */ = {
 			isa = XCBuildConfiguration;
+			baseConfigurationReferenceAnchor = FB1A37952EBE04E40063ED8C /* Lanu */;
+			baseConfigurationReferenceRelativePath = Config_Release.xcconfig;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;

+ 17 - 6
Lanu/AppDelegate.swift

@@ -6,10 +6,9 @@
 //
 
 import UIKit
-import CocoaLumberjack
-import CocoaLumberjackSwiftLogBackend
-import Logging
+import CocoaLumberjackSwift
 import Firebase
+import GoogleSignIn
 
 @main
 class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -17,7 +16,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
         // Override point for customization after application launch.
         
         setupLogger()
-        FirebaseApp.configure()
+        setupFirebase()
+        setupGoogleSignIn()
+        LNNetworkMonitor.startMonitoring()
         
         LNEventDeliver.notifyAppLaunchFinished()
         
@@ -40,6 +41,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 }
 
 extension AppDelegate {
+    private func setupFirebase() {
+        let plistName = Bundle.main.object(forInfoDictionaryKey: "FireBaseConfigPath") as! String
+        let plistPath = Bundle.main.path(forResource: plistName, ofType: "plist")!
+        let options = FirebaseOptions(contentsOfFile: plistPath)
+        FirebaseApp.configure(options: options!)
+    }
+    
+    private func setupGoogleSignIn() {
+        let clientID = Bundle.main.object(forInfoDictionaryKey: "GoogleClientID") as! String
+        GIDSignIn.sharedInstance.configuration = GIDConfiguration(clientID: clientID)
+    }
+    
     private func setupLogger() {
         let formatter = LNLoggerFormater()
         
@@ -55,7 +68,5 @@ extension AppDelegate {
         fileLogger.logFileManager.maximumNumberOfLogFiles = 7 // 最多保存 7 个文件
         fileLogger.maximumFileSize = 5 * 1024 * 1024 // 5M 最大限制
         DDLog.add(fileLogger)
-        
-        LoggingSystem.bootstrapWithCocoaLumberjack()
     }
 }

+ 72 - 0
Lanu/Common/Logger/LNLogger.swift

@@ -0,0 +1,72 @@
+//
+//  LNLogger.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/11.
+//
+
+import Foundation
+import CocoaLumberjackSwift
+
+/// 日志工具类,基于 CocoaLumberjack 实现,支持类似 print 的参数格式
+enum Log {
+    private static func buildLogMessage(items: Any..., separator: String) -> String {
+        let logs = items.map { String(describing: $0) }
+        return logs.joined(separator: separator)
+    }
+    
+    static func v(
+        _ items: Any...,
+        separator: String = " ",
+        terminator: String = "\n",
+        file: StaticString = #file,
+        function: StaticString = #function,
+        line: UInt = #line
+    ) {
+        DDLogVerbose("\(buildLogMessage(items: items, separator: separator))", level: .verbose, file: file, function: function, line: line)
+    }
+    
+    static func d(
+        _ items: Any...,
+        separator: String = " ",
+        terminator: String = "\n",
+        file: StaticString = #file,
+        function: StaticString = #function,
+        line: UInt = #line
+    ) {
+        DDLogDebug("\(buildLogMessage(items: items, separator: separator))", level: .debug, file: file, function: function, line: line)
+    }
+    
+    static func i(
+        _ items: Any...,
+        separator: String = " ",
+        terminator: String = "\n",
+        file: StaticString = #file,
+        function: StaticString = #function,
+        line: UInt = #line
+    ) {
+        DDLogInfo("\(buildLogMessage(items: items, separator: separator))", level: .info, file: file, function: function, line: line)
+    }
+    
+    static func w(
+        _ items: Any...,
+        separator: String = " ",
+        terminator: String = "\n",
+        file: StaticString = #file,
+        function: StaticString = #function,
+        line: UInt = #line
+    ) {
+        DDLogWarn("\(buildLogMessage(items: items, separator: separator))", level: .warning, file: file, function: function, line: line)
+    }
+    
+    static func e(
+        _ items: Any...,
+        separator: String = " ",
+        terminator: String = "\n",
+        file: StaticString = #file,
+        function: StaticString = #function,
+        line: UInt = #line
+    ) {
+        DDLogError("\(buildLogMessage(items: items, separator: separator))", level: .error, file: file, function: function, line: line)
+    }
+}

+ 1 - 1
Lanu/Common/Logger/LNLoggerFormater.swift

@@ -32,6 +32,6 @@ class LNLoggerFormater: NSObject, DDLogFormatter {
         let function = logMessage.function ?? ""
         let line = logMessage.line
         
-        return String(format: "[%@] [%@] [%@:%ld] %@", timeStr, levelStr, function, line, logMessage.message)
+        return String(format: "[%@] [%@] %@:%ld %@", timeStr, levelStr, function, line, logMessage.message)
     }
 }

+ 3 - 6
Lanu/Common/Storage/LNUserDefaults.swift

@@ -46,9 +46,7 @@ class LNUserDefaultsManager {
             // 更新缓存
             cache[key.rawValue] = value
         } catch {
-#if DEBUG
-            print("存储失败(key: \(key)):\(error)")
-#endif
+            Log.e("存储失败(key: \(key)):\(error)")
         }
     }
     
@@ -71,9 +69,8 @@ class LNUserDefaultsManager {
             cache[key.rawValue] = decoded
             return decoded
         } catch {
-#if DEBUG
-            print("读取失败(key: \(key.rawValue)):\(error)")
-#endif
+            Log.e("读取失败(key: \(key.rawValue)):\(error)")
+            
             cache.removeValue(forKey: key.rawValue)
             return nil
         }

+ 1 - 1
Lanu/Common/Storage/LNUserDefaultsKey.swift

@@ -10,5 +10,5 @@ import Foundation
 enum LNUserDefaultsKey: String {
     case appEnv
     
-    case uid
+    case token
 }

+ 76 - 0
Lanu/Common/Utils/AppUtils.swift

@@ -0,0 +1,76 @@
+//
+//  AppUtils.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/10.
+//
+
+import Foundation
+import UIKit
+
+// 获取当前应用的包名(Bundle Identifier)
+var curAppBundleIdentifier: String = {
+    guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
+        return "未知版本"
+    }
+    return bundleIdentifier
+}()
+
+// 获取应用当前版本号
+var curAppVersion: String = {
+    guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
+        return "未知版本"
+    }
+    return version
+}()
+
+// 获取应用构建号
+var curBuildVersion: String = {
+    guard let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
+        return "未知构建号"
+    }
+    return build
+}()
+
+// 获取系统版本
+var curSystemVersion: String = {
+    return UIDevice.current.systemVersion
+}()
+
+// 获取设备型号名称(如"iPhone 13")
+var curDeviceModelName: String = {
+    var systemInfo = utsname()
+    uname(&systemInfo)
+    let machineMirror = Mirror(reflecting: systemInfo.machine)
+    let identifier = machineMirror.children.reduce("") { identifier, element in
+        guard let value = element.value as? Int8, value != 0 else { return identifier }
+        return identifier + String(UnicodeScalar(UInt8(value)))
+    }
+    return identifier
+}()
+
+// 获取设备唯一标识符(注意:iOS 10以后无法获取真正的UDID,这里返回的是identifierForVendor)
+var curDeviceId: String = {
+    guard let deviceId = UIDevice.current.identifierForVendor?.uuidString else {
+        return "无法获取设备ID"
+    }
+    return deviceId
+}()
+
+// 获取当前时区
+var curTimeZone: String = {
+    return TimeZone.current.identifier
+}()
+
+// 获取当前时间
+var curTime: TimeInterval {
+    Date().timeIntervalSince1970
+}
+
+var curTimeInMicro: TimeInterval {
+    curTime * 1_000
+}
+
+var curTimeInNano: TimeInterval {
+    curTime * 1_000_000_000
+}

+ 22 - 0
Lanu/Common/Utils/String+Extension.swift

@@ -6,6 +6,7 @@
 //
 
 import Foundation
+import CommonCrypto
 
 
 extension String {
@@ -17,3 +18,24 @@ extension String {
         self = String(format: NSLocalizedString(key, comment: ""), with)
     }
 }
+
+extension String {
+    /// 计算字符串的 MD5 哈希值(小写 32 位)
+    var md5: String {
+        // 将字符串转换为 UTF-8 数据
+        guard let data = self.data(using: .utf8) else {
+            return ""
+        }
+        
+        // 创建 MD5 上下文
+        var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
+        
+        // 计算 MD5 哈希
+        data.withUnsafeBytes { buffer in
+            _ = CC_MD5(buffer.baseAddress, CC_LONG(buffer.count), &digest)
+        }
+        
+        // 将哈希结果转换为 32 位小写十六进制字符串
+        return digest.map { String(format: "%02x", $0) }.joined()
+    }
+}

+ 32 - 0
Lanu/Common/Utils/UIView+Extension.swift

@@ -0,0 +1,32 @@
+//
+//  UIView+Extension.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/11.
+//
+
+import Foundation
+import UIKit
+
+// MARK: 点击响应
+extension UIView {
+    private class BlockTapGestureRecognizer: UITapGestureRecognizer {
+        private var actionBlock: (() -> Void)?
+        
+        init(action block: (() -> Void)?) {
+            super.init(target: nil, action: nil)
+            self.actionBlock = block
+            // 设置目标和动作
+            addTarget(self, action: #selector(handleTap(_:)))
+        }
+        
+        @objc private func handleTap(_ gesture: BlockTapGestureRecognizer) {
+            actionBlock?()
+        }
+    }
+    
+    func onTap(_ block: @escaping () -> Void) {
+        let tap = BlockTapGestureRecognizer(action: block)
+        addGestureRecognizer(tap)
+    }
+}

+ 1 - 1
Lanu/Common/Views/Base/LNBaseViewController.swift

@@ -38,7 +38,7 @@ class LNBaseViewController: UIViewController {
         
         navigationController?.setNavigationBarHidden(!showNavigationBar, animated: animated)
         
-        if !showNavigationBar,
+        if showNavigationBar,
            let navBar = navigationController?.navigationBar {
             // 1. 配置外观(iOS 15+ 必须用 UINavigationBarAppearance)
             let appearance = UINavigationBarAppearance()

+ 2 - 1
Lanu/Common/Views/Base/LNNavigationController.swift

@@ -37,7 +37,8 @@ extension LNNavigationController: UIGestureRecognizerDelegate {
         return viewController.enableDragBack
     }
     
-    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
+    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
+                           shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
         true
     }
 }

+ 12 - 0
Lanu/Config_Debug.xcconfig

@@ -0,0 +1,12 @@
+//
+//  Config_Debug.xcconfig
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/11.
+//
+
+// Configuration settings file format documentation can be found at:
+// https://developer.apple.com/documentation/xcode/adding-a-build-configuration-file-to-your-project
+
+GOOGLE_CLIENT_ID = 111172807665-kq59ppl79227tt6v89j0skp7pemc6qsq.apps.googleusercontent.com
+FIREBASE_PLIST = GoogleService-Info-Debug

+ 12 - 0
Lanu/Config_Release.xcconfig

@@ -0,0 +1,12 @@
+//
+//  Config_Release.xcconfig
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/11.
+//
+
+// Configuration settings file format documentation can be found at:
+// https://developer.apple.com/documentation/xcode/adding-a-build-configuration-file-to-your-project
+
+GOOGLE_CLIENT_ID = 345957325655-1ihv2037eti1ahc3hd7v2dl7vad7f1k2.apps.googleusercontent.com
+FIREBASE_PLIST = GoogleService-Info-Release

+ 30 - 0
Lanu/GoogleService-Info-Debug.plist

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>API_KEY</key>
+	<string>AIzaSyBtN_3w3baBjp3Su8F1J0i4AwBAr6SmTfo</string>
+	<key>GCM_SENDER_ID</key>
+	<string>111172807665</string>
+	<key>PLIST_VERSION</key>
+	<string>1</string>
+	<key>BUNDLE_ID</key>
+	<string>com.jiehe.lanu.debug</string>
+	<key>PROJECT_ID</key>
+	<string>lanu-debug-171d9</string>
+	<key>STORAGE_BUCKET</key>
+	<string>lanu-debug-171d9.firebasestorage.app</string>
+	<key>IS_ADS_ENABLED</key>
+	<false/>
+	<key>IS_ANALYTICS_ENABLED</key>
+	<false/>
+	<key>IS_APPINVITE_ENABLED</key>
+	<true/>
+	<key>IS_GCM_ENABLED</key>
+	<true/>
+	<key>IS_SIGNIN_ENABLED</key>
+	<true/>
+	<key>GOOGLE_APP_ID</key>
+	<string>1:111172807665:ios:25866375d5dfb2cae2f250</string>
+</dict>
+</plist>

+ 5 - 9
Lanu/GoogleService-Info.plist → Lanu/GoogleService-Info-Release.plist

@@ -3,21 +3,17 @@
 <plist version="1.0">
 <dict>
 	<key>API_KEY</key>
-	<string>AIzaSyBBGJ4YI86F3C9ocEjL4FlPMCcHIu5XcqY</string>
+	<string>AIzaSyBH_FsA0EFqdYYVIVdeqRxWEChET3teA6Q</string>
 	<key>GCM_SENDER_ID</key>
-	<string>803531107809</string>
-	<key>CLIENT_ID</key>
-	<string>803531107809-nor99n3lbc6i77oauv3l1rt8oqv6o5g2.apps.googleusercontent.com</string>
-	<key>REVERSED_CLIENT_ID</key>
-	<string>com.googleusercontent.apps.803531107809-nor99n3lbc6i77oauv3l1rt8oqv6o5g2</string>
+	<string>345957325655</string>
 	<key>PLIST_VERSION</key>
 	<string>1</string>
 	<key>BUNDLE_ID</key>
 	<string>com.jiehe.lanu</string>
 	<key>PROJECT_ID</key>
-	<string>lanu-ios</string>
+	<string>lanu-9333d</string>
 	<key>STORAGE_BUCKET</key>
-	<string>lanu-ios.firebasestorage.app</string>
+	<string>lanu-9333d.firebasestorage.app</string>
 	<key>IS_ADS_ENABLED</key>
 	<false></false>
 	<key>IS_ANALYTICS_ENABLED</key>
@@ -29,6 +25,6 @@
 	<key>IS_SIGNIN_ENABLED</key>
 	<true></true>
 	<key>GOOGLE_APP_ID</key>
-	<string>1:803531107809:ios:e2213d8839e907a4f476a0</string>
+	<string>1:345957325655:ios:88b7f04be06bc382673c12</string>
 </dict>
 </plist>

+ 28 - 0
Lanu/Info.plist

@@ -2,6 +2,34 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>NSAppTransportSecurity</key>
+	<dict>
+		<key>NSAllowsArbitraryLoads</key>
+		<true/>
+	</dict>
+	<key>FireBaseConfigPath</key>
+	<string>$(FIREBASE_PLIST)</string>
+	<key>GoogleClientID</key>
+	<string>$(GOOGLE_CLIENT_ID)</string>
+	<key>CFBundleURLTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>com.googleusercontent.apps.345957325655-1ihv2037eti1ahc3hd7v2dl7vad7f1k2</string>
+			</array>
+		</dict>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>com.googleusercontent.apps.111172807665-kq59ppl79227tt6v89j0skp7pemc6qsq</string>
+			</array>
+		</dict>
+	</array>
 	<key>UIApplicationSceneManifest</key>
 	<dict>
 		<key>UIApplicationSupportsMultipleScenes</key>

+ 45 - 0
Lanu/Manager/Account/LNAccountManager.swift

@@ -19,6 +19,51 @@ extension LNUserMainEvent {
 }
 
 class LNAccountManager {
+    private(set) static var token = LNUserDefaults[.token, ""] {
+        didSet { LNUserDefaults[.token] = token }
+    }
+    private(set) static var uid = ""
+    
+    static var wasLogin: Bool {
+        !token.isEmpty
+    }
+    
+    static func loginByGoogle(data: String, completion: @escaping (LNHttpError?) -> Void) {
+        LNHttpManager.shared.loginByGoogle(data: data) { response, err in
+            guard err == nil, let response else {
+                completion(err)
+                return
+            }
+            token = response.token
+            uid = response.userProfile.id
+            completion(nil)
+            
+            notifyUserLogin()
+        }
+    }
+    
+#if DEBUG
+    static func loginByEmail(email: String, completion: @escaping (LNHttpError?) -> Void) {
+        LNHttpManager.shared.loginByEmail(email: email) { response, err in
+            guard err == nil, let response else {
+                completion(err)
+                return
+            }
+            token = response.token
+            uid = response.userProfile.id
+            completion(nil)
+            
+            notifyUserLogin()
+        }
+    }
+#endif
+}
+
+extension LNAccountManager {
+    static func clean() {
+        token = ""
+        uid = ""
+    }
 }
 
 extension LNAccountManager {

+ 30 - 0
Lanu/Manager/Account/Network/LNHttpManager+Login.swift

@@ -0,0 +1,30 @@
+//
+//  LNHttpManager+Login.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/11.
+//
+
+import Foundation
+
+
+let kNetPath_Login_Google = "/user/login/google/enter"
+let kNetPath_Login_Email = "/user/login/email/enter"
+
+let kNetPath_Logout = "/user/logout"
+
+extension LNHttpManager {
+    func loginByGoogle(data: String, completion: @escaping (LNLoginResponseVO?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Login_Google, params: ["data": data], completion: completion)
+    }
+    
+#if DEBUG
+    func loginByEmail(email: String, completion: @escaping (LNLoginResponseVO?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Login_Email, params: ["email": email], completion: completion)
+    }
+#endif
+    
+    func logout(completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_Logout, completion: completion)
+    }
+}

+ 22 - 0
Lanu/Manager/Account/Network/LNLoginResponse.swift

@@ -0,0 +1,22 @@
+//
+//  LNLoginResponse.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/11.
+//
+
+import Foundation
+import AutoCodable
+
+@AutoCodable
+class LNLoginUserInfoVO: Decodable {
+    var id: String = ""
+    
+    init() { }
+}
+
+@AutoCodable
+class LNLoginResponseVO: Decodable {
+    var token: String = ""
+    var userProfile: LNLoginUserInfoVO = LNLoginUserInfoVO()
+}

+ 7 - 10
Lanu/Manager/LNEventDeliver.swift

@@ -38,21 +38,18 @@ class LNEventDeliver {
         lock.unlock()
     }
     
-    static func notifyEvent(_ event: (AnyObject) -> Void) {
+    static func notifyEvent(_ queue: DispatchQueue = .main,
+                            _ event: @escaping (AnyObject) -> Void, ) {
         lock.lock()
         let allObservers = observers.allObjects
         lock.unlock()
         
-        allObservers.forEach { event($0) }
-    }
-    
-    static func notifyEvent(_ queue: DispatchQueue, _ event: @escaping (AnyObject) -> Void, ) {
-        lock.lock()
-        let allObservers = observers.allObjects
-        lock.unlock()
-        
-        queue.async {
+        if queue == .main, Thread.isMainThread {
             allObservers.forEach { event($0) }
+        } else {
+            queue.async {
+                allObservers.forEach { event($0) }
+            }
         }
     }
 }

+ 0 - 229
Lanu/Manager/Network/LNHTTPManager.swift

@@ -1,229 +0,0 @@
-//
-//  LNHTTPManager.swift
-//  Lanu
-//
-//  Created by OneeChan on 2025/11/6.
-//
-
-import Foundation
-
-/// HTTP请求方法枚举
-enum HTTPMethod: String {
-    case get = "GET"
-    case post = "POST"
-    case put = "PUT"
-    case delete = "DELETE"
-}
-
-/// HTTP请求错误枚举
-enum HTTPError: Error, LocalizedError {
-    case invalidURL
-    case invalidResponse
-    case networkError(Error)
-    case parsingError(Error)
-    case statusCode(Int)
-    case serverError(String)
-    
-    var errorDescription: String? {
-        switch self {
-        case .invalidURL:
-            return "无效的URL"
-        case .invalidResponse:
-            return "无效的响应"
-        case .networkError(let error):
-            return "网络错误: \(error.localizedDescription)"
-        case .parsingError(let error):
-            return "解析错误: \(error.localizedDescription)"
-        case .statusCode(let code):
-            return "请求失败,状态码: \(code)"
-        case .serverError(let error):
-            return error
-        }
-    }
-}
-
-/// HTTP请求管理器
-class LNHTTPManager {
-    static let shared = LNHTTPManager()
-    private let session: URLSession
-    
-    private init() {
-        let configuration = URLSessionConfiguration.default
-        configuration.timeoutIntervalForRequest = 30
-        configuration.timeoutIntervalForResource = 60
-        session = URLSession(configuration: configuration)
-    }
-    
-    /// 通用HTTP请求方法
-    /// - Parameters:
-    ///   - urlString: 请求URL字符串
-    ///   - method: HTTP方法
-    ///   - parameters: 请求参数
-    ///   - headers: 请求头
-    ///   - completion: 完成回调
-    func request<T: Decodable>(
-        path: String,
-        method: HTTPMethod = .get,
-        parameters: [String: Any]? = nil,
-        headers: [String: String]? = nil,
-        completion: @escaping (Result<T?, HTTPError>) -> Void
-    ) {
-        let commonHeader: [String: String] = [
-            "Content-Type": " application/json"
-        ]
-        
-        let mergedHeader = commonHeader.merging(headers ?? [:]) { $1 }
-        
-        // 检查URL是否有效
-        guard let url = buildURL(from: path, method: method, parameters: parameters) else {
-            completion(.failure(.invalidURL))
-            return
-        }
-        
-        // 创建请求
-        var request = URLRequest(url: url)
-        request.httpMethod = method.rawValue
-        
-        // 设置请求头
-        mergedHeader.forEach { key, value in
-            request.addValue(value, forHTTPHeaderField: key)
-        }
-        
-        // 设置请求体(POST, PUT等方法)
-        if method != .get, let parameters = parameters {
-            request.httpBody = try? JSONSerialization.data(withJSONObject: parameters)
-        }
-        
-        // 执行请求
-        let task = session.dataTask(with: request) { [weak self] data, response, error in
-            guard let self else { return }
-            
-            // 处理网络错误
-            if let error = error {
-#if DEBUG
-                print("receive \(request.url?.absoluteString ?? "") error: \(error.localizedDescription)")
-#endif
-                completion(.failure(.networkError(error)))
-                return
-            }
-            
-            // 检查响应是否有效
-            guard let httpResponse = response as? HTTPURLResponse else {
-#if DEBUG
-                print("receive \(request.url?.absoluteString ?? "") response error")
-#endif
-                completion(.failure(.invalidResponse))
-                return
-            }
-            
-#if DEBUG
-            print("receive \(request.url?.absoluteString ?? "") code:\(httpResponse.statusCode)")
-            if let data {
-                print("data \(String(data: data, encoding: .utf8) ?? "")")
-            }
-#endif
-            
-            // 检查状态码
-            guard 200...299 ~= httpResponse.statusCode else {
-                completion(.failure(.statusCode(httpResponse.statusCode)))
-                return
-            }
-            
-            // 处理响应数据
-            guard let data = data else {
-                // 如果没有数据但状态码正常,尝试返回空对象
-                if let emptyData = "{}".data(using: .utf8),
-                   let result = try? JSONDecoder().decode(T.self, from: emptyData) {
-                    completion(.success(result))
-                } else {
-                    completion(.failure(.invalidResponse))
-                }
-                return
-            }
-            
-            // 解析JSON数据
-            self.parseJSON(data: data, completion: completion)
-        }
-        
-#if DEBUG
-        print(
-            "send \(request.httpMethod ?? "") - \(request.url?.absoluteString ?? "")"
-        )
-        if let body = request.httpBody {
-            print("\(String(data: body, encoding: .utf8) ?? "")")
-        }
-#endif
-        task.resume()
-    }
-    
-    /// 构建请求URL(处理GET参数)
-    private func buildURL(from path: String, method: HTTPMethod, parameters: [String: Any]?) -> URL? {
-        guard var urlComponents = URLComponents(string: LNNetworkConfig.host + (path.starts(with: "/") ? path : "/\(path)")) else {
-            return nil
-        }
-        
-        // GET方法的参数拼接到URL上
-        if method == .get, let parameters = parameters {
-            urlComponents.queryItems = parameters.map { key, value in
-                URLQueryItem(name: key, value: "\(value)")
-            }
-        }
-        
-        return urlComponents.url
-    }
-    
-    /// 解析JSON数据
-    private func parseJSON<T: Decodable>(data: Data, completion: @escaping (Result<T?, HTTPError>) -> Void) {
-        do {
-            let decoder = JSONDecoder()
-            decoder.keyDecodingStrategy = .convertFromSnakeCase // 处理蛇形命名
-            let result = try decoder.decode(LNHttpResponse<T>.self, from: data)
-            if result.code != 0 {
-                completion(.failure(.serverError(result.msg)))
-            } else {
-                completion(.success(result.data))
-            }
-        } catch {
-            completion(.failure(.parsingError(error)))
-        }
-    }
-}
-
-// MARK: - 便捷请求方法扩展
-extension LNHTTPManager {
-    /// 发送GET请求
-    func get<T: Decodable>(
-        path: String,
-        params: [String: Any]? = nil,
-        completion: @escaping (Result<T?, HTTPError>) -> Void
-    ) {
-        request(path: path, method: .get, parameters: params, completion: completion)
-    }
-    
-    /// 发送POST请求
-    func post<T: Decodable>(
-        path: String,
-        params: [String: Any]? = nil,
-        completion: @escaping (Result<T?, HTTPError>) -> Void
-    ) {
-        request(path: path, method: .post, parameters: params, completion: completion)
-    }
-    
-    /// 发送PUT请求
-    func put<T: Decodable>(
-        path: String,
-        params: [String: Any]? = nil,
-        completion: @escaping (Result<T?, HTTPError>) -> Void
-    ) {
-        request(path: path, method: .put, parameters: params, completion: completion)
-    }
-    
-    /// 发送DELETE请求
-    func delete<T: Decodable>(
-        path: String,
-        params: [String: Any]? = nil,
-        completion: @escaping (Result<T?, HTTPError>) -> Void
-    ) {
-        request(path: path, method: .delete, parameters: params, completion: completion)
-    }
-}

+ 375 - 0
Lanu/Manager/Network/LNHttpManager.swift

@@ -0,0 +1,375 @@
+//
+//  LNHttpManager.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/6.
+//
+
+import Foundation
+
+/// HTTP请求方法枚举
+enum HTTPMethod: String {
+    case get = "GET"
+    case post = "POST"
+    case put = "PUT"
+    case delete = "DELETE"
+}
+
+/// HTTP请求错误枚举
+enum LNHttpError: Error, LocalizedError {
+    case invalidURL
+    case invalidResponse
+    case networkError(Error)
+    case parsingError(Error)
+    case statusCode(Int)
+    case serverError(String)
+    
+    var errorDescription: String {
+        switch self {
+        case .invalidURL:
+            return "无效的URL"
+        case .invalidResponse:
+            return "无效的响应"
+        case .networkError(let error):
+            return "网络错误: \(error.localizedDescription)"
+        case .parsingError(let error):
+            return "解析错误: \(error.localizedDescription)"
+        case .statusCode(let code):
+            return "请求失败,状态码: \(code)"
+        case .serverError(let error):
+            return error
+        }
+    }
+}
+
+/// HTTP请求管理器
+class LNHttpManager {
+    static let shared = LNHttpManager()
+    private let session: URLSession
+    
+    private init() {
+        let configuration = URLSessionConfiguration.default
+        configuration.timeoutIntervalForRequest = 30
+        configuration.timeoutIntervalForResource = 60
+        session = URLSession(configuration: configuration)
+    }
+    
+    /// 通用HTTP请求方法
+    /// - Parameters:
+    ///   - urlString: 请求URL字符串
+    ///   - method: HTTP方法
+    ///   - parameters: 请求参数
+    ///   - headers: 请求头
+    ///   - completion: 完成回调
+    func request<T: Decodable>(
+        path: String,
+        method: HTTPMethod = .get,
+        parameters: [String: Any]? = nil,
+        headers: [String: String]? = nil,
+        completion: @escaping (T?, LNHttpError?) -> Void
+    ) {
+        var sign = ""
+        var commonHeader: [String: String] = [:]
+        
+        let id = "\(Int(curTimeInNano))"
+        sign += id
+        commonHeader["id"] = id
+        
+        let udid = curDeviceId
+        sign += udid
+        commonHeader["udid"] = udid
+        
+        let app = curAppBundleIdentifier
+        sign += app
+        commonHeader["app"] = app
+        
+        let device = curDeviceModelName
+        sign += device
+        commonHeader["device"] = device
+        
+        let platform = "2"
+        sign += platform
+        commonHeader["platform"] = platform
+        
+        let channel = "appStore"
+        sign += channel
+        commonHeader["channel"] = channel
+        
+        let api = "1"
+        sign += api
+        commonHeader["api"] = api
+        
+        let version = curBuildVersion
+        sign += version
+        commonHeader["version"] = version
+        
+        let network = LNNetworkMonitor.curNetworkType.desc
+        sign += network
+        commonHeader["network"] = network
+        
+        let time = "\(Int(curTimeInMicro))"
+        sign += time
+        commonHeader["time"] = time
+        
+        let token = LNAccountManager.token
+        if !token.isEmpty {
+            sign += token
+            commonHeader["token"] = token
+        }
+        
+        let secret = "abc|abc|edg|9527|1234"
+        sign += secret
+        
+        let mergedHeader = commonHeader.merging(headers ?? [:]) { $1 }
+        
+        // 检查URL是否有效
+        guard let url = buildURL(from: path, method: method, parameters: parameters) else {
+            completion(nil, .invalidURL)
+            return
+        }
+        
+        // 创建请求
+        var request = URLRequest(url: url)
+        request.httpMethod = method.rawValue
+        
+        // 设置请求头
+        mergedHeader.forEach { key, value in
+            request.addValue(value, forHTTPHeaderField: key)
+        }
+        
+        // 设置请求体(POST, PUT等方法)
+        if method != .get, let parameters = parameters {
+            let body = try? JSONSerialization
+                .data(withJSONObject: parameters)
+            request.httpBody = body
+            if let body, let bodyStr = String(data: body, encoding: .utf8) {
+                sign += bodyStr
+            }
+        }
+        request.addValue(sign.md5, forHTTPHeaderField: "sign")
+        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+        
+        // 执行请求
+        let task = session.dataTask(with: request) {
+            [weak self] data,
+            response,
+            error in
+            guard let self else { return }
+            
+            // 处理网络错误
+            if let error = error {
+                Log.d("receive \(request.url?.absoluteString ?? "") error: \(error.localizedDescription)")
+                completion(nil, .networkError(error))
+                return
+            }
+            
+            // 检查响应是否有效
+            guard let httpResponse = response as? HTTPURLResponse else {
+                Log.d("receive \(request.url?.absoluteString ?? "") response error")
+                
+                completion(nil, .invalidResponse)
+                return
+            }
+            
+            if let data {
+                Log.d("receive \(request.url?.absoluteString ?? "") code:\(httpResponse.statusCode) data \(String(data: data, encoding: .utf8) ?? "")")
+            } else {
+                Log.d("receive \(request.url?.absoluteString ?? "") code:\(httpResponse.statusCode)")
+            }
+            
+            // 检查状态码
+            guard 200...299 ~= httpResponse.statusCode else {
+                completion(nil, .statusCode(httpResponse.statusCode))
+                return
+            }
+            
+            // 处理响应数据
+            guard let data = data else {
+                completion(nil, .invalidResponse)
+                return
+            }
+            
+            // 解析JSON数据
+            self.parseJSON(data: data, completion: completion)
+        }
+        
+        if let body = request.httpBody {
+            Log.d("send \(request.httpMethod ?? "") - \(request.url?.absoluteString ?? "") \(String(data: body, encoding: .utf8) ?? "")")
+        } else {
+            Log.d("send \(request.httpMethod ?? "") - \(request.url?.absoluteString ?? "")")
+        }
+        task.resume()
+    }
+    
+    /// 构建请求URL(处理GET参数)
+    private func buildURL(from path: String, method: HTTPMethod, parameters: [String: Any]?) -> URL? {
+        guard var urlComponents = URLComponents(string: LNNetworkConfig.host + (path.starts(with: "/") ? path : "/\(path)")) else {
+            return nil
+        }
+        
+        // GET方法的参数拼接到URL上
+        if method == .get, let parameters = parameters {
+            urlComponents.queryItems = parameters.map { key, value in
+                URLQueryItem(name: key, value: "\(value)")
+            }
+        }
+        
+        return urlComponents.url
+    }
+    
+    /// 解析JSON数据
+    private func parseJSON<T: Decodable>(
+        data: Data,
+        completion: @escaping (T?, LNHttpError?) -> Void
+    ) {
+        do {
+            let decoder = JSONDecoder()
+            decoder.keyDecodingStrategy = .convertFromSnakeCase // 处理蛇形命名
+            let result = try decoder.decode(LNHttpResponse<T>.self, from: data)
+            if result.code != 0 {
+                completion(nil, .serverError(result.msg))
+            } else {
+                completion(result.data, nil)
+            }
+        } catch {
+            completion(nil, .parsingError(error))
+        }
+    }
+}
+
+// MARK: - 便捷请求方法扩展
+extension LNHttpManager {
+    /// 发送GET请求
+    func get<T: Decodable>(
+        path: String,
+        params: [String: Any]? = nil,
+        completion: @escaping (T?, LNHttpError?) -> Void
+    ) {
+        request(
+            path: path,
+            method: .get,
+            parameters: params,
+            completion: completion
+        )
+    }
+    
+    /// 发送POST请求
+    func post<T: Decodable>(
+        path: String,
+        params: [String: Any]? = nil,
+        completion: @escaping (T?, LNHttpError?) -> Void
+    ) {
+        request(
+            path: path,
+            method: .post,
+            parameters: params,
+            completion: completion
+        )
+    }
+    
+    /// 发送PUT请求
+    func put<T: Decodable>(
+        path: String,
+        params: [String: Any]? = nil,
+        completion: @escaping (T?, LNHttpError?) -> Void
+    ) {
+        request(
+            path: path,
+            method: .put,
+            parameters: params,
+            completion: completion
+        )
+    }
+    
+    /// 发送DELETE请求
+    func delete<T: Decodable>(
+        path: String,
+        params: [String: Any]? = nil,
+        completion: @escaping (T?, LNHttpError?) -> Void
+    ) {
+        request(
+            path: path,
+            method: .delete,
+            parameters: params,
+            completion: completion
+        )
+    }
+}
+
+extension LNHttpManager {
+    func request(
+        path: String,
+        method: HTTPMethod = .get,
+        parameters: [String: Any]? = nil,
+        headers: [String: String]? = nil,
+        completion: @escaping (LNHttpError?) -> Void
+    ) {
+        let handler: (
+            LNHttpEmptyResponse?, LNHttpError?
+        ) -> Void = { _, err in
+            completion(err)
+        }
+        request(
+            path: path,
+            method: method,
+            parameters: parameters,
+            headers: headers,
+            completion: handler
+        )
+    }
+    /// 发送GET请求
+    func get(
+        path: String,
+        params: [String: Any]? = nil,
+        completion: @escaping (LNHttpError?) -> Void
+    ) {
+        request(
+            path: path,
+            method: .get,
+            parameters: params,
+            completion: completion
+        )
+    }
+    
+    /// 发送POST请求
+    func post(
+        path: String,
+        params: [String: Any]? = nil,
+        completion: @escaping (LNHttpError?) -> Void
+    ) {
+        request(
+            path: path,
+            method: .post,
+            parameters: params,
+            completion: completion
+        )
+    }
+    
+    /// 发送PUT请求
+    func put(
+        path: String,
+        params: [String: Any]? = nil,
+        completion: @escaping (LNHttpError?) -> Void
+    ) {
+        request(
+            path: path,
+            method: .put,
+            parameters: params,
+            completion: completion
+        )
+    }
+    
+    /// 发送DELETE请求
+    func delete(
+        path: String,
+        params: [String: Any]? = nil,
+        completion: @escaping (LNHttpError?) -> Void
+    ) {
+        request(
+            path: path,
+            method: .delete,
+            parameters: params,
+            completion: completion
+        )
+    }
+}

+ 1 - 1
Lanu/Manager/Network/LNNetworkConfig.swift

@@ -10,6 +10,6 @@ import Foundation
 
 class LNNetworkConfig {
     static let host: String = {
-        LNAppConfig.curEnv == .test ? "http://test-api.lanu.live" : ""
+        LNAppConfig.curEnv == .test ? "http://test-api.lanu.live" : "http://test-api.lanu.live"
     }()
 }

+ 176 - 0
Lanu/Manager/Network/Monitor/LNNetworkMonitor.swift

@@ -0,0 +1,176 @@
+//
+//  LNNetworkMonitor.swift
+//  AirTennis
+//
+//  Created by OneeChan on 2025/11/2.
+//
+
+import Foundation
+import Network
+import CoreTelephony
+
+protocol LNNetworkMonitorNotify {
+    func onNetworkStateChanged(state: LNNetworkState)
+}
+extension LNNetworkMonitorNotify {
+    func onNetworkStateChanged(state: LNNetworkState) {}
+}
+
+enum LNNetworkState {
+    case noNetwork
+    case available
+}
+
+enum LNNetworkType {
+    case unknown          // 未知
+    case notConnected     // 无网络
+    case wifi             // WiFi
+    case cellular2G       // 2G
+    case cellular3G       // 3G
+    case cellular4G       // 4G
+    case cellular5G       // 5G
+    
+    var desc: String {
+        switch self {
+        case .unknown: "unknown"
+        case .notConnected: "notConnected"
+        case .wifi: "wifi"
+        case .cellular2G: "2g"
+        case .cellular3G: "3g"
+        case .cellular4G: "4g"
+        case .cellular5G: "5g"
+        }
+    }
+}
+
+class LNNetworkMonitor {
+    // 创建网络路径监听器(可指定监听的网络类型,如 .cellular、.wifi 等,nil 表示监听所有类型)
+    private static let monitor = NWPathMonitor()
+    // 蜂窝网络信息管理器
+    private static let telephonyInfo = CTTelephonyNetworkInfo()
+    // 用于在主线程处理回调
+    private static let queue = DispatchQueue(label: "LNNetworkMonitor")
+    
+    private(set) static var curState: LNNetworkState = .noNetwork {
+        didSet {
+            if oldValue != curState {
+                notifyNeetworkStateChanged()
+            }
+        }
+    }
+    
+    static func startMonitoring() {
+        // 设置路径变化的回调
+        monitor.pathUpdateHandler = { path in
+            // 检查当前网络的授权状态
+            switch path.status {
+            case .satisfied:
+                // 网络已连接(可能已授权)
+                curState = .available
+            case .unsatisfied:
+                // 网络未连接(可能因授权问题导致)
+                Log.d("网络未连接,可能未授权或无网络")
+                curState = .noNetwork
+            case .requiresConnection:
+                // 正在连接中(可能需要等待授权)
+                Log.d("正在连接网络...")
+                curState = .noNetwork
+            @unknown default:
+                Log.d("未知网络状态")
+                curState = .noNetwork
+            }
+            
+//            // 更精确的授权状态判断(通过 path.isExpensive 等属性辅助判断)
+//            if path.isExpensive {
+//                print("当前网络为蜂窝网络或付费网络")
+//            }
+//            
+//            // 检查是否受限于低数据模式
+//            if path.isConstrained {
+//                print("网络受限于低数据模式")
+//            }
+        }
+        
+        // 开始监听(指定处理队列)
+        monitor.start(queue: queue)
+    }
+    
+    // 停止监听(例如在页面销毁时调用)
+    static func stopMonitoring() {
+        monitor.cancel()
+    }
+    
+    static var curNetworkType: LNNetworkType {
+        let path = monitor.currentPath
+        
+        guard path.status == .satisfied else {
+            return .notConnected
+        }
+        
+        if path.usesInterfaceType(.wifi) {
+            return .wifi
+        }
+        
+        if path.usesInterfaceType(.cellular) {
+            return cellularNetworkType
+        }
+        
+        return .unknown
+    }
+    
+    // 替代 deprecated 的 currentRadioAccessTechnology
+    private static var cellularNetworkType: LNNetworkType {
+        // iOS 13+ 推荐使用 serviceCurrentRadioAccessTechnology(支持多 SIM 卡)
+        if let serviceRadioMap = telephonyInfo.serviceCurrentRadioAccessTechnology {
+            // 遍历所有 SIM 卡的网络类型(取第一个有效类型)
+            for (_, radioType) in serviceRadioMap {
+                return mapRadioTypeToNetworkType(radioType)
+            }
+        }
+        
+        // 兼容旧版本(虽然已弃用,但可作为降级方案)
+        if let legacyRadioType = telephonyInfo.currentRadioAccessTechnology {
+            return mapRadioTypeToNetworkType(legacyRadioType)
+        }
+        
+        return .unknown
+    }
+    
+    // 映射无线电类型到网络类型
+    private static func mapRadioTypeToNetworkType(_ radioType: String) -> LNNetworkType {
+        switch radioType {
+        // 2G 类型
+        case CTRadioAccessTechnologyGPRS,
+             CTRadioAccessTechnologyEdge,
+             CTRadioAccessTechnologyCDMA1x:
+            return .cellular2G
+            
+        // 3G 类型
+        case CTRadioAccessTechnologyWCDMA,
+             CTRadioAccessTechnologyHSDPA,
+             CTRadioAccessTechnologyHSUPA,
+             CTRadioAccessTechnologyCDMAEVDORev0,
+             CTRadioAccessTechnologyCDMAEVDORevA,
+             CTRadioAccessTechnologyCDMAEVDORevB,
+             CTRadioAccessTechnologyeHRPD:
+            return .cellular3G
+            
+        // 4G LTE
+        case CTRadioAccessTechnologyLTE:
+            return .cellular4G
+            
+        // 5G(iOS 14.1+ 支持)
+        case CTRadioAccessTechnologyNR:
+            return .cellular5G
+            
+        default:
+            return .unknown
+        }
+    }
+}
+
+extension LNNetworkMonitor {
+    private static func notifyNeetworkStateChanged() {
+        LNEventDeliver.notifyEvent { ($0 as? LNNetworkMonitorNotify)?.onNetworkStateChanged(state: curState) }
+    }
+}

+ 11 - 0
Lanu/Manager/Network/Response/LNHttpEmptyResponse.swift

@@ -0,0 +1,11 @@
+//
+//  LNHttpEmptyResponse.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/10.
+//
+
+import Foundation
+
+
+struct LNHttpEmptyResponse: Decodable { }

+ 0 - 0
Lanu/Manager/Network/LNHttpResponse.swift → Lanu/Manager/Network/Response/LNHttpResponse.swift


+ 92 - 0
Lanu/Manager/Profile/LNProfileManager.swift

@@ -6,3 +6,95 @@
 //
 
 import Foundation
+
+
+protocol LNProfileManagerNotify {
+    func onUserInfoChanged(userInfo: LNUserProfileInfo)
+}
+
+
+class LNProfileManager {
+    static let shared = LNProfileManager()
+    
+    private(set) static var myUserInfo: LNUserProfileInfo = LNUserProfileInfo()
+    
+    private let lock = NSLock()
+    private var profileCached: [String: LNUserProfileInfo] = [:]
+    
+    typealias fetchProfileBlock = (LNUserProfileInfo?) -> Void
+    private var requests: [String: [fetchProfileBlock]] = [:]
+    
+    private init() {
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func userInfo(for uid: String, forceNet: Bool = false,
+                  queue: DispatchQueue = DispatchQueue.global(),
+                  completion: @escaping (LNUserProfileInfo?) -> Void) {
+        lock.lock()
+        if let cached = profileCached[uid] {
+            lock.unlock()
+            completion(cached)
+            return
+        }
+        var array = requests[uid] ?? []
+        array.append(completion)
+        requests[uid] = array
+        
+        if array.count > 1 {
+            lock.unlock()
+            return
+        }
+        lock.unlock()
+        
+        // fetch
+    }
+}
+
+extension LNProfileManager {
+    func reloadMyProfile() {
+        LNHttpManager.shared.getMyProfile { res, err in
+            guard err == nil, let res else { return }
+            
+            Self.myUserInfo.update(by: res.userProfile)
+            
+            self.lock.lock()
+            self.profileCached[res.userProfile.id] = Self.myUserInfo
+            self.lock.unlock()
+            
+            self.notifyUserInfoChanged(newInfo: Self.myUserInfo)
+        }
+    }
+    
+    func modifyMyProfile(age: Int? = nil, avatar: String? = nil,
+                         nickname: String? = nil, gender: Int? = nil,
+                         voiceBar: String? = nil,
+                         completion: @escaping (LNMyProfileResponseVO?, LNHttpError?) -> Void) {
+        LNHttpManager.shared.modifyMyProfile(age: age, avatar: avatar,
+                                             nickname: nickname, gender: gender,
+                                             voiceBar: voiceBar) { res, err in
+            if err == nil, let res {
+                Self.myUserInfo.update(by: res.userProfile)
+                
+                self.lock.lock()
+                self.profileCached[res.userProfile.id] = Self.myUserInfo
+                self.lock.unlock()
+                
+                self.notifyUserInfoChanged(newInfo: Self.myUserInfo)
+            }
+            completion(res, err)
+        }
+    }
+}
+
+extension LNProfileManager: LNUserMainEvent {
+    func onUserLogin() {
+        reloadMyProfile()
+    }
+}
+
+extension LNProfileManager {
+    func notifyUserInfoChanged(newInfo: LNUserProfileInfo) {
+        LNEventDeliver.notifyEvent { ($0 as? LNProfileManagerNotify)?.onUserInfoChanged(userInfo: newInfo) }
+    }
+}

+ 31 - 0
Lanu/Manager/Profile/LNUserProfileInfo.swift

@@ -0,0 +1,31 @@
+//
+//  LNUserProfileInfo.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/12.
+//
+
+import Foundation
+
+
+class LNUserProfileInfo {
+    var id: String = ""
+    var userNo: String = ""
+    var avatar: String = ""
+    var nickname: String = ""
+    var age: Int = 0
+    var gender: Int = 0
+    var intro: String = ""
+    var playmate: Bool = false
+    
+    func update(by profile: LNUserProfileVO) {
+        id = profile.id
+        userNo = profile.userNo
+        avatar = profile.avatar
+        nickname = profile.nickname
+        age = profile.age
+        gender = profile.gender
+        intro = profile.intro
+        playmate = profile.playmate
+    }
+}

+ 45 - 0
Lanu/Manager/Profile/Network/LNHttpManager+Profile.swift

@@ -0,0 +1,45 @@
+//
+//  LNHttpManager+Profile.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/12.
+//
+
+import Foundation
+
+
+let kNetPath_Profile_MyInfo = "/user/my/info"
+let kNetPath_Profile_EditMyInfo = "/user/my/info/edit"
+
+extension LNHttpManager {
+    func getMyProfile(completion: @escaping (LNMyProfileResponseVO?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Profile_MyInfo, completion: completion)
+    }
+    
+    func modifyMyProfile(age: Int? = nil, avatar: String? = nil,
+                         nickname: String? = nil, gender: Int? = nil,
+                         voiceBar: String? = nil,
+                         completion: @escaping (LNMyProfileResponseVO?, LNHttpError?) -> Void) {
+        var params: [String: Any] = [:]
+        if let age {
+            params["age"] = age
+        }
+        if let avatar {
+            params["avatar"] = avatar
+        }
+        if let nickname {
+            params["nickname"] = nickname
+        }
+        if let gender {
+            params["gender"] = gender
+        }
+        if let voiceBar {
+            params["voiceBar"] = voiceBar
+        }
+        guard !params.isEmpty else {
+            completion(nil, nil)
+            return
+        }
+        post(path: kNetPath_Profile_EditMyInfo, params: params, completion: completion)
+    }
+}

+ 28 - 0
Lanu/Manager/Profile/Network/LNProfileResponse.swift

@@ -0,0 +1,28 @@
+//
+//  LNProfileResponse.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/12.
+//
+
+import Foundation
+import AutoCodable
+
+@AutoCodable
+class LNUserProfileVO: Decodable {
+    var id: String = ""
+    var userNo: String = ""
+    var avatar: String = ""
+    var nickname: String = ""
+    var age: Int = 0
+    var gender: Int = 0
+    var intro: String = ""
+    var playmate: Bool = false
+    
+    init() { }
+}
+
+@AutoCodable
+class LNMyProfileResponseVO: Decodable {
+    var userProfile: LNUserProfileVO = LNUserProfileVO()
+}

+ 39 - 1
Lanu/SceneDelegate.swift

@@ -20,8 +20,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         
         window = UIWindow(windowScene: sceneWindow)
         window?.backgroundColor = .white
-        window?.rootViewController = LNNavigationController(rootViewController: LNMainViewController())
+        if LNAccountManager.wasLogin {
+            window?.rootViewController = LNNavigationController(rootViewController: LNMainViewController())
+        } else {
+            window?.rootViewController = LNNavigationController(rootViewController: LNLoginViewController())
+        }
         window?.makeKeyAndVisible()
+        
+        LNEventDeliver.addObserver(self)
     }
 
     func sceneDidDisconnect(_ scene: UIScene) {
@@ -53,3 +59,35 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
     }
 }
 
+extension SceneDelegate: LNNetworkMonitorNotify {
+    func onNetworkStateChanged(state: LNNetworkState) {
+        if state == .available {
+            autoLoginIfNeed()
+        } else {
+            Log.d("network invailable")
+        }
+    }
+}
+
+extension SceneDelegate: LNUserMainEvent {
+    func onUserLogin() {
+        guard let navVC = window?.rootViewController as? LNNavigationController else { return }
+        if navVC.viewControllers.first is LNMainViewController { return }
+        window?.rootViewController = LNNavigationController(rootViewController: LNMainViewController())
+    }
+    
+    func onUserLogout() {
+        guard let navVC = window?.rootViewController as? LNNavigationController else { return }
+        if navVC.viewControllers.first is LNLoginViewController { return }
+        window?.rootViewController = LNNavigationController(rootViewController: LNLoginViewController())
+    }
+}
+
+extension SceneDelegate {
+    private func autoLoginIfNeed() {
+        guard LNAccountManager.wasLogin,
+              LNNetworkMonitor.curState == .available else { return }
+        
+//        LNAccountManager.loginByToken(completion: { success in })
+    }
+}

+ 74 - 0
Lanu/Views/Login/LNLoginViewController.swift

@@ -0,0 +1,74 @@
+//
+//  LNLoginViewController.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/11.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+import GoogleSignIn
+
+class LNLoginViewController: LNBaseViewController {
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+    }
+}
+
+extension LNLoginViewController {
+    private func setupViews() {
+        showNavigationBar = false
+        
+        let container = UIView()
+        
+        let google = buildGoogleLogin()
+        container.addSubview(google)
+        google.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        view.addSubview(container)
+        container.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+    }
+    
+    private func buildGoogleLogin() -> UIView {
+        let loginButton = GIDSignInButton()
+        loginButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            GIDSignIn.sharedInstance.signIn(withPresenting: self) { [weak self] result, err in
+                guard let self else { return }
+                guard err == nil, let result else { return }
+                guard let token = result.user.idToken?.tokenString else { return }
+                LNAccountManager.loginByGoogle(data: token) { [weak self] err in
+                    guard let self else { return }
+                }
+            }
+        }), for: .touchUpInside)
+        
+        return loginButton
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNLoginViewControllerPreview: UIViewControllerRepresentable {
+    func makeUIViewController(context: Context) -> some UIViewController {
+        LNNavigationController(rootViewController: LNLoginViewController())
+    }
+    
+    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
+        
+    }
+}
+
+#Preview(body: {
+    LNLoginViewControllerPreview()
+})
+#endif

+ 8 - 0
Lanu/Views/Main/LNMainViewController.swift

@@ -7,11 +7,19 @@
 
 import Foundation
 import UIKit
+import SnapKit
 
 class LNMainViewController: LNBaseViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
         
         showNavigationBar = false
+        
+        let title = UILabel()
+        title.text = "这个是首页"
+        view.addSubview(title)
+        title.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
     }
 }