瀏覽代碼

feat: 添加社区扫码功能、登录跳过功能、蓝牙模块修复等

- 添加社区界面扫码功能(CommunityFragment)
- 添加登录跳过功能用于测试(MainActivity, AuthManager)
- 修复Android蓝牙模块握手响应逻辑
- 集成二维码扫描模块(QRCodeManager)
- 更新SocketIO模块实现
- 优化代码结构和模块组织
wangmeng 2 月之前
父節點
當前提交
61f7e8114b
共有 80 個文件被更改,包括 4184 次插入1472 次删除
  1. 3 0
      .gitignore
  2. 2 1
      app/build.gradle
  3. 4 0
      app/src/main/AndroidManifest.xml
  4. 151 9
      app/src/main/java/com/narutohuo/xindazhou/MainActivity.kt
  5. 1 1
      app/src/main/java/com/narutohuo/xindazhou/XinDaZhouApplication.kt
  6. 68 6
      app/src/main/java/com/narutohuo/xindazhou/community/ui/CommunityFragment.kt
  7. 32 50
      base-common/src/main/java/com/narutohuo/xindazhou/common/launch/AppInitializer.kt
  8. 1 1
      app/src/main/java/com/narutohuo/xindazhou/common/main/ui/MainFragment.kt
  9. 0 157
      app/src/main/java/com/narutohuo/xindazhou/user/ui/login/LoginFragment.kt
  10. 1 1
      app/src/main/java/com/narutohuo/xindazhou/user/ui/profile/UserFragment.kt
  11. 0 119
      app/src/main/java/com/narutohuo/xindazhou/user/ui/register/RegisterFragment.kt
  12. 63 85
      app/src/main/java/com/narutohuo/xindazhou/user/ui/viewmodel/UserViewModel.kt
  13. 5 5
      app/src/main/java/com/narutohuo/xindazhou/user/ui/viewmodel/UserViewModelFactory.kt
  14. 69 0
      app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/VehicleFragment.kt
  15. 2 1
      app/src/main/res/layout/activity_main.xml
  16. 26 10
      app/src/main/res/navigation/nav_graph.xml
  17. 11 0
      base-common/build.gradle
  18. 95 1
      base-common/src/main/java/com/narutohuo/xindazhou/common/auth/AuthManager.kt
  19. 0 99
      base-common/src/main/java/com/narutohuo/xindazhou/common/auth/LoginActivity.kt
  20. 25 0
      base-common/src/main/java/com/narutohuo/xindazhou/common/auth/NavigationCallback.kt
  21. 0 135
      base-common/src/main/java/com/narutohuo/xindazhou/common/auth/RegisterActivity.kt
  22. 1 1
      base-common/src/main/java/com/narutohuo/xindazhou/common/auth/datasource/remote/AuthRemoteDataSource.kt
  23. 41 38
      base-common/src/main/java/com/narutohuo/xindazhou/common/auth/storage/TokenStore.kt
  24. 11 7
      base-common/src/main/java/com/narutohuo/xindazhou/common/auth/ui/LoginFragment.kt
  25. 12 9
      base-common/src/main/java/com/narutohuo/xindazhou/common/auth/ui/RegisterFragment.kt
  26. 120 0
      base-common/src/main/java/com/narutohuo/xindazhou/common/auth/utils/JWTUtil.kt
  27. 13 43
      base-common/src/main/java/com/narutohuo/xindazhou/common/config/ConfigManager.kt
  28. 8 7
      base-common/src/main/java/com/narutohuo/xindazhou/common/config/ServerConfigManager.kt
  29. 3 1
      base-common/src/main/java/com/narutohuo/xindazhou/common/crash/CrashHelper.kt
  30. 34 31
      base-common/src/main/java/com/narutohuo/xindazhou/common/launch/AppLaunchManager.kt
  31. 0 79
      base-common/src/main/java/com/narutohuo/xindazhou/common/launch/LaunchActivity.kt
  32. 35 0
      base-common/src/main/java/com/narutohuo/xindazhou/common/launch/OnboardingManager.kt
  33. 16 0
      base-common/src/main/java/com/narutohuo/xindazhou/common/launch/model/OnboardingItem.kt
  34. 211 0
      base-common/src/main/java/com/narutohuo/xindazhou/common/launch/ui/OnboardingFragment.kt
  35. 1 1
      base-common/src/main/java/com/narutohuo/xindazhou/common/network/ApiBaseRemoteDataSource.kt
  36. 141 0
      base-common/src/main/java/com/narutohuo/xindazhou/common/network/ApiManager.kt
  37. 0 77
      base-common/src/main/java/com/narutohuo/xindazhou/common/network/ApiServiceFactory.kt
  38. 76 2
      base-common/src/main/java/com/narutohuo/xindazhou/common/router/RouterHelper.kt
  39. 405 91
      base-common/src/main/java/com/narutohuo/xindazhou/common/socketio/SocketIOManager.kt
  40. 243 0
      base-common/src/main/java/com/narutohuo/xindazhou/common/socketio/impl/SocketIOClient.kt
  41. 40 0
      base-common/src/main/java/com/narutohuo/xindazhou/common/socketio/model/SocketIOEvent.kt
  42. 17 0
      base-common/src/main/java/com/narutohuo/xindazhou/common/socketio/model/SocketIOResponse.kt
  43. 27 100
      base-common/src/main/java/com/narutohuo/xindazhou/common/storage/StorageManager.kt
  44. 7 0
      base-common/src/main/java/com/narutohuo/xindazhou/common/ui/StatusBarHelper.kt
  45. 0 44
      base-common/src/main/java/com/narutohuo/xindazhou/common/version/VersionCheckActivity.kt
  46. 9 15
      base-common/src/main/java/com/narutohuo/xindazhou/common/version/VersionUpdateManager.kt
  47. 1 1
      base-common/src/main/java/com/narutohuo/xindazhou/common/version/datasource/remote/VersionRemoteDataSource.kt
  48. 20 0
      base-common/src/main/res/drawable/tab_indicator.xml
  49. 68 0
      base-common/src/main/res/layout/fragment_onboarding.xml
  50. 59 0
      base-common/src/main/res/layout/item_onboarding.xml
  51. 4 1
      base-core/src/main/java/com/narutohuo/xindazhou/core/storage/IStorage.kt
  52. 124 0
      base-core/src/main/java/com/narutohuo/xindazhou/core/storage/StorageImpl.kt
  53. 17 0
      capability-ble/src/main/java/com/narutohuo/xindazhou/ble/config/BLEConstants.kt
  54. 94 5
      capability-ble/src/main/java/com/narutohuo/xindazhou/ble/impl/BLEServiceImpl.kt
  55. 121 21
      capability-ble/src/main/java/com/narutohuo/xindazhou/ble/util/BleConnector.kt
  56. 8 9
      capability-ble/src/main/java/com/narutohuo/xindazhou/ble/util/BlePacketSender.kt
  57. 78 11
      capability-ble/src/main/java/com/narutohuo/xindazhou/ble/util/BleScanner.kt
  58. 501 0
      capability-ble/集成说明.html
  59. 11 2
      capability-qrcode/build.gradle
  60. 9 3
      capability-qrcode/src/main/AndroidManifest.xml
  61. 22 0
      capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/api/QRCodeManager.kt
  62. 0 40
      capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/api/QRCodeService.kt
  63. 33 0
      capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/factory/QRCodeManagerFactory.kt
  64. 73 0
      capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/impl/QRCodeManagerImpl.kt
  65. 0 80
      capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/impl/QRCodeServiceImpl.kt
  66. 94 0
      capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/ui/QRCodeScanActivity.kt
  67. 57 0
      capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/ui/QRCodeScanFragment.kt
  68. 11 0
      capability-qrcode/src/main/res/drawable/ic_back_white.xml
  69. 26 0
      capability-qrcode/src/main/res/drawable/scan_corner.xml
  70. 8 0
      capability-qrcode/src/main/res/drawable/scan_frame_bg.xml
  71. 9 0
      capability-qrcode/src/main/res/drawable/scan_line.xml
  72. 5 0
      capability-qrcode/src/main/res/drawable/scan_mask.xml
  73. 5 0
      capability-qrcode/src/main/res/drawable/scan_mask_side.xml
  74. 121 0
      capability-qrcode/src/main/res/layout/activity_qrcode_scan.xml
  75. 7 0
      capability-qrcode/src/main/res/values/colors.xml
  76. 10 0
      capability-qrcode/src/main/res/values/strings.xml
  77. 5 1
      capability-socketio/build.gradle
  78. 103 51
      capability-socketio/src/main/java/com/narutohuo/xindazhou/socketio/impl/SocketIOServiceImpl.kt
  79. 177 0
      capability-socketio/src/main/java/com/narutohuo/xindazhou/socketio/manager/SocketIOManager.kt
  80. 273 20
      capability-socketio/src/main/java/com/narutohuo/xindazhou/socketio/repository/SocketIORepository.kt

+ 3 - 0
.gitignore

@@ -136,3 +136,6 @@ Desktop.ini
 # Temporary files
 capability-share.zip
 *.zip
+
+# BleUnlock-Android (不要提交)
+BleUnlock-Android/

+ 2 - 1
app/build.gradle

@@ -103,7 +103,8 @@ dependencies {
     
     // SDK模块(能力层)- 只依赖模块本身,不要依赖模块内部的库!
     implementation project(':capability-ble')
-    implementation project(':capability-socketio')
+    // Socket.IO 已迁移到 base-common,不再需要 capability-socketio 模块
+    // implementation project(':capability-socketio')
     implementation project(':capability-push')
     implementation project(':capability-qrcode')
     implementation project(':capability-share')

+ 4 - 0
app/src/main/AndroidManifest.xml

@@ -59,6 +59,8 @@
         <!-- 注意:极光推送配置已移到 capability-push 模块的 AndroidManifest.xml 中 -->
         
         
+        <!-- 主界面 Activity(启动 Activity,单 Activity 架构) -->
+        <!-- 使用 Navigation Component 管理所有页面导航,包括登录页和注册页 -->
         <activity
             android:name=".MainActivity"
             android:exported="true"
@@ -68,6 +70,8 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        
+        <!-- 注意:LoginActivity 和 RegisterActivity 已移除,改为使用 Fragment(单 Activity 架构) -->
 
         <!-- FileProvider 用于 Android 7.0+ 安装APK -->
         <provider

+ 151 - 9
app/src/main/java/com/narutohuo/xindazhou/MainActivity.kt

@@ -1,32 +1,174 @@
 package com.narutohuo.xindazhou
 
+import android.os.Bundle
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.NavHostFragment
+import com.narutohuo.xindazhou.common.auth.AuthManager
+import com.narutohuo.xindazhou.common.auth.NavigationCallback
+import com.narutohuo.xindazhou.common.log.LogHelper
 import com.narutohuo.xindazhou.common.ui.BaseActivity
 import com.narutohuo.xindazhou.common.version.VersionUpdateManager
 import com.narutohuo.xindazhou.databinding.ActivityMainBinding
+import kotlinx.coroutines.launch
 
 /**
- * 主界面 Activity
+ * 主界面 Activity(单 Activity 架构)
  * 
  * 职责:
- * 1. 显示主界面(MainFragment)
- * 2. 版本检查(启动时检查是否有新版本)
+ * 1. 作为应用的唯一 Activity,使用 Navigation Component 管理所有页面导航
+ * 2. 启动时检查登录状态,动态导航到相应页面
+ * 3. 版本检查(启动时检查是否有新版本)
  * 
  * 注意:
- * - 登录状态判断已在 LaunchActivity 中处理
- * - 版本检查独立处理,不影响页面显示
+ * - 这是单 Activity 架构,所有页面都是 Fragment
+ * - 登录页(LoginFragment)和注册页(RegisterFragment)都是 Fragment
+ * - 主界面(MainFragment)使用 Navigation Component 管理内部导航
+ * - 实现 NavigationCallback 接口,处理 Fragment 的导航请求
  */
-class MainActivity : BaseActivity<ActivityMainBinding>() {
+class MainActivity : BaseActivity<ActivityMainBinding>(), NavigationCallback {
+    
+    private val TAG = "MainActivity"
+    private var navHostFragment: NavHostFragment? = null
     
     override fun getViewBinding(): ActivityMainBinding {
         return ActivityMainBinding.inflate(layoutInflater)
     }
     
-    override fun initView() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        
         // 版本检查(独立处理,不影响页面显示)
         VersionUpdateManager.checkUpdate(this)
+    }
+    
+    override fun onStart() {
+        super.onStart()
+        
+        // 在 onStart 中初始化导航并检查登录状态
+        // 此时 Fragment 已经完全初始化,NavController 已可用
+        initNavigationAndCheckLogin()
+    }
+    
+    override fun initView() {
+        // Navigation 初始化在 onStart 中完成
+        // 这里可以添加其他初始化逻辑
+    }
+    
+    /**
+     * 初始化 Navigation Component 并检查登录状态
+     * 
+     * 默认起始目的地是 loginFragment(在 nav_graph.xml 中设置)
+     * 如果已登录,则导航到 mainFragment
+     */
+    private fun initNavigationAndCheckLogin() {
+        // 获取 Navigation Host Fragment
+        if (navHostFragment == null) {
+            navHostFragment = supportFragmentManager.findFragmentById(R.id.navHostFragment) as? NavHostFragment
+        }
+        
+        val navController = navHostFragment?.navController ?: run {
+            LogHelper.w(TAG, "NavController 未找到,延迟检查登录状态")
+            // 如果 NavController 还未初始化,延迟执行
+            binding.root.post {
+                initNavigationAndCheckLogin()
+            }
+            return
+        }
+        
+        // 测试模式:直接导航到主界面,跳过登录检查
+        val currentDestination = navController.currentDestination?.id
+        if (currentDestination == R.id.loginFragment || currentDestination == R.id.registerFragment) {
+            LogHelper.d(TAG, "测试模式:直接导航到主界面,跳过登录检查")
+            navigateToMain()
+        } else {
+            LogHelper.d(TAG, "测试模式:当前已在主界面")
+        }
         
-        // 主界面内容由 Navigation Component 管理
-        // nav_graph.xml 中的 startDestination 已经设置为 mainFragment
+        /* 原来的登录检查逻辑(已注释,测试时使用上面的代码)
+        lifecycleScope.launch {
+            try {
+                // 检查登录状态(支持自动刷新 Token)
+                val isLoggedIn = AuthManager.isLoggedInAsync()
+                
+                // 检查当前目的地,避免重复导航
+                val currentDestination = navController.currentDestination?.id
+                
+                if (isLoggedIn) {
+                    // 已登录,如果当前在登录页或注册页,则导航到主界面
+                    if (currentDestination == R.id.loginFragment || currentDestination == R.id.registerFragment) {
+                        LogHelper.d(TAG, "用户已登录,从登录页/注册页导航到主界面")
+                        navigateToMain()
+                    } else {
+                        LogHelper.d(TAG, "用户已登录,当前已在主界面")
+                    }
+                } else {
+                    // 未登录,如果当前在主界面,则导航到登录页
+                    if (currentDestination == R.id.mainFragment) {
+                        LogHelper.d(TAG, "用户未登录,从主界面导航到登录页")
+                        navigateToLogin()
+                    } else {
+                        LogHelper.d(TAG, "用户未登录,当前已在登录页")
+                    }
+                }
+                
+            } catch (e: Exception) {
+                LogHelper.e(TAG, "检查登录状态失败", e)
+                // 出错时保持在当前页面
+            }
+        }
+        */
+    }
+    
+    // ==================== NavigationCallback 接口实现 ====================
+    
+    override fun navigateToMain() {
+        val navController = navHostFragment?.navController ?: return
+        try {
+            val currentDestination = navController.currentDestination?.id
+            // 根据当前目的地选择正确的 action
+            when (currentDestination) {
+                R.id.loginFragment -> {
+                    // 从登录页导航到主界面,清除返回栈
+                    navController.navigate(R.id.action_loginFragment_to_mainFragment)
+                }
+                R.id.registerFragment -> {
+                    // 从注册页导航到主界面,清除返回栈
+                    navController.navigate(R.id.action_registerFragment_to_mainFragment)
+                }
+                else -> {
+                    // 如果已经在主界面,不需要导航
+                    LogHelper.d(TAG, "已在主界面,跳过导航")
+                }
+            }
+        } catch (e: Exception) {
+            LogHelper.e(TAG, "导航到主界面失败", e)
+        }
+    }
+    
+    override fun navigateToLogin() {
+        val navController = navHostFragment?.navController ?: return
+        try {
+            // 导航到登录页,清除注册页的返回栈
+            val currentDestination = navController.currentDestination?.id
+            if (currentDestination == R.id.registerFragment) {
+                navController.navigate(R.id.action_registerFragment_to_loginFragment)
+            } else {
+                // 如果已经在登录页,不需要导航
+                LogHelper.d(TAG, "已在登录页,跳过导航")
+            }
+        } catch (e: Exception) {
+            LogHelper.e(TAG, "导航到登录页失败", e)
+        }
+    }
+    
+    override fun navigateToRegister() {
+        val navController = navHostFragment?.navController ?: return
+        try {
+            // 导航到注册页
+            navController.navigate(R.id.action_loginFragment_to_registerFragment)
+        } catch (e: Exception) {
+            LogHelper.e(TAG, "导航到注册页失败", e)
+        }
     }
 }
 

+ 1 - 1
app/src/main/java/com/narutohuo/xindazhou/XinDaZhouApplication.kt

@@ -1,7 +1,7 @@
 package com.narutohuo.xindazhou
 
 import android.app.Application
-import com.narutohuo.xindazhou.common.launch.AppInitializer
+import com.narutohuo.xindazhou.launch.AppInitializer
 
 /**
  * 应用程序入口

+ 68 - 6
app/src/main/java/com/narutohuo/xindazhou/community/ui/CommunityFragment.kt

@@ -4,8 +4,12 @@ import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.Button
+import android.widget.LinearLayout
 import android.widget.TextView
+import android.widget.Toast
 import androidx.fragment.app.Fragment
+import com.narutohuo.xindazhou.qrcode.factory.QRCodeManagerFactory
 
 /**
  * 社区Fragment
@@ -14,17 +18,75 @@ import androidx.fragment.app.Fragment
  */
 class CommunityFragment : Fragment() {
 
+    private val qrCodeManager = QRCodeManagerFactory.getInstance()
+
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
         savedInstanceState: Bundle?
     ): View? {
-        // 临时使用简单的 TextView,后续可以替换为实际布局
-        val textView = TextView(requireContext())
-        textView.text = "社区页面\n(待实现)"
-        textView.textSize = 18f
-        textView.gravity = android.view.Gravity.CENTER
-        return textView
+        // 创建布局
+        val layout = LinearLayout(requireContext()).apply {
+            orientation = LinearLayout.VERTICAL
+            gravity = android.view.Gravity.CENTER
+            setPadding(32, 32, 32, 32)
+        }
+
+        // 标题
+        val textView = TextView(requireContext()).apply {
+            text = "社区页面"
+            textSize = 18f
+            gravity = android.view.Gravity.CENTER
+            layoutParams = LinearLayout.LayoutParams(
+                LinearLayout.LayoutParams.MATCH_PARENT,
+                LinearLayout.LayoutParams.WRAP_CONTENT
+            ).apply {
+                setMargins(0, 0, 0, 32)
+            }
+        }
+        layout.addView(textView)
+
+        // 扫码按钮
+        val scanButton = Button(requireContext()).apply {
+            text = "扫码"
+            layoutParams = LinearLayout.LayoutParams(
+                LinearLayout.LayoutParams.WRAP_CONTENT,
+                LinearLayout.LayoutParams.WRAP_CONTENT
+            )
+            setOnClickListener {
+                startScanQRCode()
+            }
+        }
+        layout.addView(scanButton)
+
+        return layout
+    }
+
+    /**
+     * 开始扫码
+     */
+    private fun startScanQRCode() {
+        val activity = requireActivity()
+        qrCodeManager.scanQRCode(activity) { response ->
+            if (response.success) {
+                val qrCodeContent = response.data
+                Toast.makeText(
+                    requireContext(),
+                    "扫码成功:$qrCodeContent",
+                    Toast.LENGTH_SHORT
+                ).show()
+                // TODO: 处理扫码结果
+            } else {
+                val error = response.errorMessage
+                if (error != null) {
+                    Toast.makeText(
+                        requireContext(),
+                        "扫码失败:$error",
+                        Toast.LENGTH_SHORT
+                    ).show()
+                }
+            }
+        }
     }
 }
 

+ 32 - 50
base-common/src/main/java/com/narutohuo/xindazhou/common/launch/AppInitializer.kt

@@ -1,20 +1,24 @@
-package com.narutohuo.xindazhou.common.launch
+package com.narutohuo.xindazhou.launch
 
 import android.app.Application
 import com.alibaba.android.arouter.launcher.ARouter
 import com.narutohuo.xindazhou.common.auth.AuthManager
 import com.narutohuo.xindazhou.common.config.ServerConfigManager
 import com.narutohuo.xindazhou.common.log.LogHelper
-import com.narutohuo.xindazhou.common.network.ApiServiceFactory
-import com.narutohuo.xindazhou.common.network.NetworkHelper
+import com.narutohuo.xindazhou.common.network.ApiManager
 import com.narutohuo.xindazhou.common.socketio.SocketIOManager
 import com.narutohuo.xindazhou.common.version.VersionUpdateManager
+import com.narutohuo.xindazhou.core.storage.StorageImpl
+import com.narutohuo.xindazhou.share.factory.ShareServiceFactory
 
 /**
  * 应用初始化管理器
  * 
  * 统一管理所有模块的初始化,简化 Application 代码
  * 
+ * 位置:app 模块(应用层)
+ * 职责:协调所有层的初始化(base-core、base-common、capability-*)
+ * 
  * 使用方式:
  * ```kotlin
  * // 在 Application.onCreate() 中调用
@@ -40,25 +44,38 @@ object AppInitializer {
         }
         
         try {
-            // 1. 初始化 ARouter(必须在其他初始化之前)
+            // 1. 初始化统一存储(必须在其他初始化之前,因为其他模块可能依赖存储)
+            StorageImpl.init(application.applicationContext)
+            LogHelper.d(TAG, "统一存储初始化完成")
+            
+            // 2. 初始化 ARouter(必须在其他初始化之前)
             initARouter(application)
             
-            // 2. 初始化配置管理器
+            // 3. 初始化配置管理器(base-common)
             ServerConfigManager.init(application.applicationContext)
             
-            // 3. 初始化认证管理器(会自动初始化 TokenStore)
-            AuthManager.init(application.applicationContext)
+            // 4. 初始化网络管理器(base-common)
+            // ApiManager.initialize() 会自动从 ServerConfigManager 读取服务器地址并初始化 NetworkHelper
+            // Token Provider 稍后由 AuthManager 设置
+            ApiManager.initialize(application)
+            LogHelper.d(TAG, "✅ 网络管理器初始化完成")
             
-            // 4. 初始化网络管理器
-            initNetwork(application)
+            // 5. 初始化认证管理器(base-common,会自动初始化 TokenStore,TokenStore 使用 StorageImpl)
+            // AuthManager.init() 会自动配置 NetworkHelper 的 Token Provider
+            AuthManager.init(application.applicationContext)
+            LogHelper.d(TAG, "✅ 认证管理器初始化完成")
             
-            // 5. 初始化版本更新管理器
+            // 6. 初始化版本更新管理器(base-common)
             VersionUpdateManager.init(application.applicationContext)
+            LogHelper.d(TAG, "✅ 版本更新管理器初始化完成")
             
-            // 6. 初始化 SocketIO 管理器(自动处理生命周期和重连)
-            SocketIOManager.init(application)
+            // 7. SocketIO 管理器(base-common)
+            // SocketIOManager 会自动处理连接、Token 刷新、重连等
+            // 业务层可以直接订阅消息,无需关心连接细节
+            SocketIOManager.initialize(application)
+            LogHelper.d(TAG, "✅ SocketIO 管理器初始化完成(自动处理连接和Token刷新)")
             
-            // 7. 初始化分享服务(可选,失败不影响应用运行)
+            // 8. 初始化分享服务(capability-share,可选
             initShareService(application)
             
             initialized = true
@@ -86,49 +103,14 @@ object AppInitializer {
     }
     
     /**
-     * 初始化网络管理器
-     */
-    private fun initNetwork(application: Application) {
-        val isDebug = isDebugMode(application)
-        val baseUrl = ServerConfigManager.getHttpServerUrl()
-        
-        // 初始化网络管理器
-        NetworkHelper.init(
-            baseUrl = baseUrl,
-            isDebug = isDebug,
-            enableLogging = isDebug
-        )
-        
-        // 配置 Token 提供器
-        NetworkHelper.setTokenProvider {
-            AuthManager.getAccessToken()
-        }
-        
-        // 初始化 ApiServiceFactory 的 baseUrlProvider
-        // 这样所有使用 ApiServiceFactory.create() 的地方都可以自动获取 baseUrl
-        ApiServiceFactory.baseUrlProvider = {
-            ServerConfigManager.getHttpServerUrl()
-        }
-        
-        LogHelper.d(TAG, "网络管理器初始化完成")
-    }
-    
-    /**
      * 初始化分享服务
      * 
-     * 分享服务初始化失败不影响应用运行,但分享功能将不可用
-     * 使用反射调用,避免 base-common 强依赖 capability-share 模块
+     * app 模块直接依赖 capability-share,可以直接导入使用,不需要反射
      */
     private fun initShareService(application: Application) {
         try {
-            // 使用反射调用,避免强依赖
-            val shareServiceFactoryClass = Class.forName("com.narutohuo.xindazhou.share.factory.ShareServiceFactory")
-            val initMethod = shareServiceFactoryClass.getMethod("init", Application::class.java)
-            initMethod.invoke(null, application)
+            ShareServiceFactory.init(application)
             LogHelper.d(TAG, "分享服务初始化成功")
-        } catch (e: ClassNotFoundException) {
-            // 分享服务模块未引入,跳过初始化(这是正常的,因为分享服务是可选的)
-            LogHelper.d(TAG, "分享服务模块未引入,跳过初始化")
         } catch (e: Exception) {
             // 分享服务初始化失败不影响应用运行,但分享功能将不可用
             LogHelper.e(TAG, "分享服务初始化失败,分享功能将不可用", e)

+ 1 - 1
app/src/main/java/com/narutohuo/xindazhou/common/main/ui/MainFragment.kt

@@ -1,4 +1,4 @@
-package com.narutohuo.xindazhou.common.main.ui
+package com.narutohuo.xindazhou.main.ui
 
 import android.os.Bundle
 import android.view.LayoutInflater

+ 0 - 157
app/src/main/java/com/narutohuo/xindazhou/user/ui/login/LoginFragment.kt

@@ -1,157 +0,0 @@
-package com.narutohuo.xindazhou.user.ui.login
-
-import android.os.Bundle
-import android.text.TextUtils
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Toast
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import androidx.navigation.fragment.findNavController
-import com.google.android.material.button.MaterialButton
-import com.google.android.material.floatingactionbutton.FloatingActionButton
-import com.google.android.material.textfield.TextInputEditText
-import com.narutohuo.xindazhou.R
-import com.narutohuo.xindazhou.common.dialog.ServerConfigDialog
-import com.narutohuo.xindazhou.share.factory.ShareServiceFactory
-import com.narutohuo.xindazhou.share.model.ShareContent
-import com.narutohuo.xindazhou.share.model.ShareResponse
-import com.narutohuo.xindazhou.user.ui.constant.UiConstants
-import com.narutohuo.xindazhou.user.ui.viewmodel.LoginViewModel
-import com.narutohuo.xindazhou.user.ui.viewmodel.LoginState
-import com.narutohuo.xindazhou.user.ui.viewmodel.LoginViewModelFactory
-import kotlinx.coroutines.launch
-
-/**
- * 登录Fragment
- */
-class LoginFragment : Fragment() {
-    
-    private val viewModel: LoginViewModel by viewModels { 
-        LoginViewModelFactory(requireContext().applicationContext as android.app.Application)
-    }
-    
-    private lateinit var etMobile: TextInputEditText
-    private lateinit var etPassword: TextInputEditText
-    private lateinit var btnLogin: MaterialButton
-    private lateinit var btnRegister: MaterialButton
-    private lateinit var ivServerConfig: View
-    private lateinit var progressBar: View
-    private lateinit var fabShare: FloatingActionButton
-    
-    // 延迟初始化,避免在 Fragment 构造函数中调用 ARouter(此时可能还未完全初始化)
-    private val shareService by lazy { ShareServiceFactory.getInstance() }
-    
-    override fun onCreateView(
-        inflater: LayoutInflater,
-        container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        return inflater.inflate(R.layout.fragment_login, container, false)
-    }
-    
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        super.onViewCreated(view, savedInstanceState)
-        
-        etMobile = view.findViewById(R.id.etMobile)
-        etPassword = view.findViewById(R.id.etPassword)
-        btnLogin = view.findViewById(R.id.btnLogin)
-        btnRegister = view.findViewById(R.id.btnRegister)
-        ivServerConfig = view.findViewById(R.id.ivServerConfig)
-        progressBar = view.findViewById(R.id.progressBar)
-        fabShare = view.findViewById(R.id.fabShare)
-        
-        // 登录按钮
-        btnLogin.setOnClickListener {
-            val mobile = etMobile.text?.toString()?.trim() ?: ""
-            val password = etPassword.text?.toString() ?: ""
-            
-            if (TextUtils.isEmpty(mobile)) {
-                Toast.makeText(requireContext(), UiConstants.MSG_MOBILE_EMPTY, Toast.LENGTH_SHORT).show()
-                return@setOnClickListener
-            }
-            
-            if (TextUtils.isEmpty(password)) {
-                Toast.makeText(requireContext(), UiConstants.MSG_PASSWORD_EMPTY, Toast.LENGTH_SHORT).show()
-                return@setOnClickListener
-            }
-            
-            viewModel.login(mobile, password)
-        }
-        
-        // 注册按钮
-        btnRegister.setOnClickListener {
-            findNavController().navigate(R.id.action_loginFragment_to_registerFragment)
-        }
-        
-        // 服务器配置按钮
-        ivServerConfig.setOnClickListener {
-            val dialog = ServerConfigDialog()
-            dialog.show(parentFragmentManager, "ServerConfigDialog")
-        }
-        
-        // 分享按钮(测试用)
-        fabShare.setOnClickListener {
-            testShare()
-        }
-        
-        // 观察登录状态
-        lifecycleScope.launch {
-            viewModel.loginState.collect { state ->
-                when (state) {
-                    is LoginState.Idle -> {
-                        progressBar.visibility = View.GONE
-                        btnLogin.isEnabled = true
-                    }
-                    is LoginState.Loading -> {
-                        progressBar.visibility = View.VISIBLE
-                        btnLogin.isEnabled = false
-                    }
-                    is LoginState.Success -> {
-                        progressBar.visibility = View.GONE
-                        btnLogin.isEnabled = true
-                        Toast.makeText(requireContext(), state.message, Toast.LENGTH_SHORT).show()
-                        // 导航到主界面
-                        findNavController().navigate(R.id.action_loginFragment_to_mainFragment)
-                    }
-                    is LoginState.Error -> {
-                        progressBar.visibility = View.GONE
-                        btnLogin.isEnabled = true
-                        Toast.makeText(requireContext(), state.message, Toast.LENGTH_SHORT).show()
-                    }
-                }
-            }
-        }
-    }
-    
-    /**
-     * 测试分享功能
-     */
-    private fun testShare() {
-        val content = ShareContent.builder()
-            .setTitle("新大洲智能车控")
-            .setDescription("分享给您一个有趣的内容,这是分享功能测试")
-            .setUrl("https://www.xindazhou.com")
-            .build()
-        
-        shareService.share(requireActivity(), content) { response: ShareResponse ->
-            if (response.success) {
-                val platform = response.data
-                Toast.makeText(
-                    requireContext(),
-                    "分享成功: ${platform?.name ?: "未知平台"}",
-                    Toast.LENGTH_SHORT
-                ).show()
-            } else {
-                Toast.makeText(
-                    requireContext(),
-                    "分享失败: ${response.errorMessage}",
-                    Toast.LENGTH_SHORT
-                ).show()
-            }
-        }
-    }
-}
-

+ 1 - 1
app/src/main/java/com/narutohuo/xindazhou/user/ui/profile/UserFragment.kt

@@ -26,7 +26,7 @@ import kotlinx.coroutines.launch
  * 架构:MVVM
  * - View (Fragment): 只负责 UI 初始化和状态观察
  * - ViewModel: 管理业务逻辑和状态
- * - Repository: 数据层(通过 SocketIORepositoryFactory 获取,完全封装在 capability-socketio 模块中
+ * - SocketIOManager: Socket.IO 管理器(base-common,已在 AppInitializer 中统一初始化
  */
 class UserFragment : Fragment() {
 

+ 0 - 119
app/src/main/java/com/narutohuo/xindazhou/user/ui/register/RegisterFragment.kt

@@ -1,119 +0,0 @@
-package com.narutohuo.xindazhou.user.ui.register
-
-import android.os.Bundle
-import android.text.TextUtils
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Toast
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import androidx.navigation.fragment.findNavController
-import com.google.android.material.button.MaterialButton
-import com.google.android.material.textfield.TextInputEditText
-import com.narutohuo.xindazhou.R
-import com.narutohuo.xindazhou.user.ui.constant.UiConstants
-import com.narutohuo.xindazhou.user.ui.viewmodel.RegisterViewModel
-import com.narutohuo.xindazhou.user.ui.viewmodel.RegisterState
-import com.narutohuo.xindazhou.user.ui.viewmodel.RegisterViewModelFactory
-import kotlinx.coroutines.launch
-
-/**
- * 注册Fragment
- */
-class RegisterFragment : Fragment() {
-    
-    private val viewModel: RegisterViewModel by viewModels { 
-        RegisterViewModelFactory(requireContext().applicationContext as android.app.Application)
-    }
-    
-    private lateinit var etMobile: TextInputEditText
-    private lateinit var etPassword: TextInputEditText
-    private lateinit var etConfirmPassword: TextInputEditText
-    private lateinit var btnRegister: MaterialButton
-    private lateinit var btnBackToLogin: MaterialButton
-    private lateinit var progressBar: View
-    
-    override fun onCreateView(
-        inflater: LayoutInflater,
-        container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        return inflater.inflate(R.layout.fragment_register, container, false)
-    }
-    
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        super.onViewCreated(view, savedInstanceState)
-        
-        etMobile = view.findViewById(R.id.etMobile)
-        etPassword = view.findViewById(R.id.etPassword)
-        etConfirmPassword = view.findViewById(R.id.etConfirmPassword)
-        btnRegister = view.findViewById(R.id.btnRegister)
-        btnBackToLogin = view.findViewById(R.id.btnBackToLogin)
-        progressBar = view.findViewById(R.id.progressBar)
-        
-        // 注册按钮
-        btnRegister.setOnClickListener {
-            val mobile = etMobile.text?.toString()?.trim() ?: ""
-            val password = etPassword.text?.toString() ?: ""
-            val confirmPassword = etConfirmPassword.text?.toString() ?: ""
-            
-            if (TextUtils.isEmpty(mobile)) {
-                Toast.makeText(requireContext(), UiConstants.MSG_MOBILE_EMPTY, Toast.LENGTH_SHORT).show()
-                return@setOnClickListener
-            }
-            
-            if (TextUtils.isEmpty(password)) {
-                Toast.makeText(requireContext(), UiConstants.MSG_PASSWORD_EMPTY, Toast.LENGTH_SHORT).show()
-                return@setOnClickListener
-            }
-            
-            if (password.length < 6 || password.length > 16) {
-                Toast.makeText(requireContext(), UiConstants.MSG_PASSWORD_INVALID, Toast.LENGTH_SHORT).show()
-                return@setOnClickListener
-            }
-            
-            if (password != confirmPassword) {
-                Toast.makeText(requireContext(), UiConstants.MSG_PASSWORD_NOT_MATCH, Toast.LENGTH_SHORT).show()
-                return@setOnClickListener
-            }
-            
-            viewModel.register(mobile, password)
-        }
-        
-        // 返回登录按钮
-        btnBackToLogin.setOnClickListener {
-            findNavController().popBackStack()
-        }
-        
-        // 观察注册状态
-        lifecycleScope.launch {
-            viewModel.registerState.collect { state ->
-                when (state) {
-                    is RegisterState.Idle -> {
-                        progressBar.visibility = View.GONE
-                        btnRegister.isEnabled = true
-                    }
-                    is RegisterState.Loading -> {
-                        progressBar.visibility = View.VISIBLE
-                        btnRegister.isEnabled = false
-                    }
-                    is RegisterState.Success -> {
-                        progressBar.visibility = View.GONE
-                        btnRegister.isEnabled = true
-                        Toast.makeText(requireContext(), state.message, Toast.LENGTH_SHORT).show()
-                        // 注册成功后自动登录,导航到主界面
-                        findNavController().navigate(R.id.action_registerFragment_to_mainFragment)
-                    }
-                    is RegisterState.Error -> {
-                        progressBar.visibility = View.GONE
-                        btnRegister.isEnabled = true
-                        Toast.makeText(requireContext(), state.message, Toast.LENGTH_SHORT).show()
-                    }
-                }
-            }
-        }
-    }
-}
-

+ 63 - 85
app/src/main/java/com/narutohuo/xindazhou/user/ui/viewmodel/UserViewModel.kt

@@ -3,11 +3,9 @@ package com.narutohuo.xindazhou.user.ui.viewmodel
 import android.app.Application
 import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.viewModelScope
-import com.narutohuo.xindazhou.socketio.factory.SocketIORepositoryFactory
-import com.narutohuo.xindazhou.socketio.model.SocketIOResponse
-import com.narutohuo.xindazhou.socketio.repository.SocketIORepository
-import com.narutohuo.xindazhou.common.config.ServerConfigManager
-import com.narutohuo.xindazhou.common.auth.AuthManager
+import com.narutohuo.xindazhou.common.socketio.SocketIOManager
+import com.narutohuo.xindazhou.common.socketio.model.SocketIOResponse
+import com.narutohuo.xindazhou.common.socketio.model.SocketIOEvent
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -28,10 +26,12 @@ data class UserUiState(
  * 用户ViewModel
  * 
  * 负责管理 SocketIO 连接状态、日志列表、报警消息等业务逻辑
+ * 
+ * 注意:SocketIOManager 已经在 AppInitializer 中统一初始化
+ * 这里只需要订阅消息即可,不需要再调用 initialize()
  */
 class UserViewModel(
-    application: Application,
-    private val socketIORepository: SocketIORepository
+    application: Application
 ) : AndroidViewModel(application) {
     
     private val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
@@ -45,45 +45,47 @@ class UserViewModel(
     private val _errorMessage = MutableStateFlow<String?>(null)
     val errorMessage: SharedFlow<String?> = _errorMessage.asSharedFlow()
     
+    // 订阅任务(用于管理订阅协程,断开连接时可以取消)
+    private val subscriptionJobs = mutableListOf<kotlinx.coroutines.Job>()
+    
     init {
         // 先初始化连接状态(同步获取当前状态)
         updateConnectionStatus()
         // 然后观察连接状态变化(异步监听)
         observeConnectionState()
-        // 观察日志消息
-        observeLogMessage()
-        // 观察车辆控制响应
-        observeVehicleControlResponse()
-        // 观察车辆报警
-        observeVehicleAlarm()
     }
     
     /**
-     * 连接 SocketIO
+     * 开始订阅消息
+     * 
+     * 注意:SocketIOManager 已经在 AppInitializer 中统一初始化
+     * 这里只需要订阅消息即可,如果未连接,subscribe() 会自动处理连接
      */
     fun connect() {
         viewModelScope.launch {
-            addLog("正在连接SocketIO服务器...")
-            val socketUrl = ServerConfigManager.getSocketIOUrl()
-            val token = AuthManager.getAccessToken()
+            addLog("开始订阅 SocketIO 消息...")
             
-            if (token.isNullOrEmpty()) {
-                val errorMsg = "未登录,无法获取Token"
-                addLog(errorMsg)
-                _errorMessage.value = errorMsg
-                return@launch
-            }
+            // 订阅连接状态变化(已经在 observeConnectionState 中设置,这里确保状态同步)
+            updateConnectionStatus()
             
-            socketIORepository.connect(socketUrl, token)
-                .onSuccess {
-                    addLog("连接请求已发送")
+            // 订阅车辆报警事件
+            // subscribe() 会自动处理连接(如果未连接会调用 checkAndReconnect)
+            val alarmJob = launch {
+                SocketIOManager.shared.subscribe(SocketIOEvent.VEHICLE_ALARM).collect { response ->
+                    handleVehicleAlarm(response)
                 }
-                .onFailure { e ->
-                    val errorMsg = "连接失败:${e.message}"
-                    addLog(errorMsg)
-                    addLog("异常详情:${e.javaClass.simpleName}")
-                    _errorMessage.value = errorMsg
+            }
+            subscriptionJobs.add(alarmJob)
+            
+            // 订阅车辆控制响应
+            val controlJob = launch {
+                SocketIOManager.shared.subscribe(SocketIOEvent.VEHICLE_CONTROL_RESPONSE).collect { response ->
+                    addLog("📥 收到响应: ${response.data}")
                 }
+            }
+            subscriptionJobs.add(controlJob)
+            
+            addLog("✅ SocketIO 消息订阅已启动(SocketIOManager 已在 AppInitializer 中初始化)")
         }
     }
     
@@ -92,15 +94,12 @@ class UserViewModel(
      */
     fun disconnect() {
         viewModelScope.launch {
-            socketIORepository.disconnect()
-                .onSuccess {
-                    addLog("已断开连接")
-                }
-                .onFailure { e ->
-                    val errorMsg = "断开连接失败:${e.message}"
-                    addLog(errorMsg)
-                    _errorMessage.value = errorMsg
-                }
+            // 取消所有订阅任务
+            subscriptionJobs.forEach { it.cancel() }
+            subscriptionJobs.clear()
+            
+            SocketIOManager.shared.disconnect()
+            addLog("已断开连接")
             updateConnectionStatus()
         }
     }
@@ -110,60 +109,39 @@ class UserViewModel(
      */
     fun sendFindVehicleCommand() {
         viewModelScope.launch {
-            socketIORepository.sendVehicleControl("find_vehicle", 1L)
-                .onSuccess {
-                    addLog("📤 发送寻车指令:command=find_vehicle")
-                }
-                .onFailure { e ->
-                    val errorMsg = e.message ?: "发送失败"
-                    addLog("错误:$errorMsg")
-                    _errorMessage.value = errorMsg
-                }
-        }
-    }
-    
-    /**
-     * 观察连接状态
-     */
-    private fun observeConnectionState() {
-        viewModelScope.launch {
-            socketIORepository.connectionState.collect { isConnected ->
-                _uiState.value = _uiState.value.copy(isConnected = isConnected)
-            }
-        }
-    }
-    
-    /**
-     * 观察日志消息
-     */
-    private fun observeLogMessage() {
-        viewModelScope.launch {
-            socketIORepository.logMessage.collect { message ->
-                addLog(message)
+            if (!SocketIOManager.shared.isConnected()) {
+                _errorMessage.value = "未连接,请先连接SocketIO"
+                return@launch
             }
+            
+            val data = mapOf(
+                "command" to "find_vehicle",
+                "vehicleId" to 1
+            )
+            
+            SocketIOManager.shared.emit(SocketIOEvent.VEHICLE_CONTROL, data)
+            addLog("📤 发送寻车指令:command=find_vehicle")
         }
     }
     
     /**
-     * 观察车辆控制响应
+     * 处理车辆报警
      */
-    private fun observeVehicleControlResponse() {
-        viewModelScope.launch {
-            socketIORepository.vehicleControlResponse.collect { response ->
-                // 响应已在 logMessage 中处理,这里可以添加其他业务逻辑
-            }
-        }
+    private fun handleVehicleAlarm(response: SocketIOResponse) {
+        val time = dateFormat.format(Date(response.timestamp))
+        addLog("🚨 收到车辆报警!")
+        addLog("   时间: $time")
+        addLog("   数据: ${response.data}")
+        _alarmMessage.value = response
     }
     
     /**
-     * 观察车辆报警
+     * 观察连接状态
      */
-    private fun observeVehicleAlarm() {
+    private fun observeConnectionState() {
         viewModelScope.launch {
-            socketIORepository.vehicleAlarm.collect { response ->
-                val time = dateFormat.format(Date(response.timestamp))
-                addLog("   时间: $time")
-                _alarmMessage.value = response
+            SocketIOManager.shared.connectionState.collect { isConnected ->
+                _uiState.value = _uiState.value.copy(isConnected = isConnected)
             }
         }
     }
@@ -173,7 +151,7 @@ class UserViewModel(
      */
     private fun updateConnectionStatus() {
         _uiState.value = _uiState.value.copy(
-            isConnected = socketIORepository.isConnected()
+            isConnected = SocketIOManager.shared.isConnected()
         )
     }
     
@@ -184,7 +162,7 @@ class UserViewModel(
         val time = dateFormat.format(Date())
         val logEntry = "[$time] $message"
         _uiState.value = _uiState.value.copy(
-            logs = _uiState.value.logs + logEntry
+            logs = (_uiState.value.logs + logEntry).takeLast(100) // 限制日志数量,最多保留100条
         )
     }
     

+ 5 - 5
app/src/main/java/com/narutohuo/xindazhou/user/ui/viewmodel/UserViewModelFactory.kt

@@ -3,11 +3,12 @@ package com.narutohuo.xindazhou.user.ui.viewmodel
 import android.app.Application
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
-import com.narutohuo.xindazhou.socketio.factory.SocketIORepositoryFactory
-import com.narutohuo.xindazhou.socketio.repository.SocketIORepository
 
 /**
- * UserViewModel工厂类
+ * UserViewModel 工厂类
+ * 
+ * 注意:现在 UserViewModel 不再需要 SocketIORepository 参数
+ * SocketIOManager 已经在 AppInitializer 中统一初始化,直接使用单例即可
  */
 class UserViewModelFactory(
     private val application: Application
@@ -16,8 +17,7 @@ class UserViewModelFactory(
     @Suppress("UNCHECKED_CAST")
     override fun <T : ViewModel> create(modelClass: Class<T>): T {
         if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
-            val socketIORepository = SocketIORepositoryFactory.getInstance()
-            return UserViewModel(application, socketIORepository) as T
+            return UserViewModel(application) as T
         }
         throw IllegalArgumentException("Unknown ViewModel class")
     }

+ 69 - 0
app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/VehicleFragment.kt

@@ -1,5 +1,8 @@
 package com.narutohuo.xindazhou.vehicle.ui
 
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -10,6 +13,8 @@ import android.widget.RadioButton
 import android.widget.Switch
 import android.widget.TextView
 import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
 import androidx.fragment.app.Fragment
 import com.narutohuo.xindazhou.R
 import com.narutohuo.xindazhou.ble.api.BLEService
@@ -22,6 +27,7 @@ import com.narutohuo.xindazhou.ble.model.BLEDevice
 import com.narutohuo.xindazhou.ble.model.BLEResponse
 import com.narutohuo.xindazhou.ble.model.SystemControlInstruction
 import com.narutohuo.xindazhou.common.log.LogHelper
+import com.narutohuo.xindazhou.core.log.ILog
 import org.json.JSONObject
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
@@ -126,6 +132,27 @@ class VehicleFragment : Fragment() {
 
     private var rootView: View? = null
 
+    // 权限请求器
+    private val permissionLauncher = registerForActivityResult(
+        ActivityResultContracts.RequestMultiplePermissions()
+    ) { permissions ->
+        val allGranted = permissions.all { it.value }
+        if (allGranted) {
+            ILog.d("VehicleFragment", "✅ 所有权限已授予,开始连接设备")
+            performConnectToDevice()
+        } else {
+            val deniedPermissions = permissions.filter { !it.value }.keys.joinToString(", ")
+            ILog.e("VehicleFragment", "❌ 权限被拒绝: $deniedPermissions")
+            Toast.makeText(
+                context,
+                "需要蓝牙权限才能连接设备,请在设置中授予权限",
+                Toast.LENGTH_LONG
+            ).show()
+            btnConnectBluetooth.isEnabled = true
+            tvBluetoothStatus.text = "权限被拒绝"
+        }
+    }
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
@@ -224,6 +251,8 @@ class VehicleFragment : Fragment() {
     private fun setupClickListeners() {
         // 蓝牙连接/断开
         btnConnectBluetooth.setOnClickListener {
+            Toast.makeText(context, "按钮被点击了", Toast.LENGTH_SHORT).show()
+            ILog.e("VehicleFragment", "🔴🔴🔴 按钮被点击了")
             if (bleService.isConnected()) {
                 // 断开连接
                 bleService.disconnect()
@@ -830,6 +859,44 @@ class VehicleFragment : Fragment() {
      * 连接到设备(一键完成扫描+连接+握手)
      */
     private fun connectToDevice() {
+        ILog.d("VehicleFragment", "========== 开始连接设备 ==========")
+        
+        // 检查并请求权限
+        val requiredPermissions = mutableListOf<String>()
+        
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            // Android 12+ 需要新权限
+            requiredPermissions.add(Manifest.permission.BLUETOOTH_SCAN)
+            requiredPermissions.add(Manifest.permission.BLUETOOTH_CONNECT)
+        } else {
+            // Android 11 及以下需要旧权限
+            requiredPermissions.add(Manifest.permission.BLUETOOTH)
+            requiredPermissions.add(Manifest.permission.BLUETOOTH_ADMIN)
+            requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
+        }
+        
+        // 检查权限是否已授予
+        val missingPermissions = requiredPermissions.filter { permission ->
+            ContextCompat.checkSelfPermission(requireContext(), permission) != PackageManager.PERMISSION_GRANTED
+        }
+        
+        if (missingPermissions.isNotEmpty()) {
+            ILog.w("VehicleFragment", "缺少权限: ${missingPermissions.joinToString(", ")}")
+            ILog.d("VehicleFragment", "请求权限...")
+            btnConnectBluetooth.isEnabled = false
+            tvBluetoothStatus.text = "请求权限..."
+            permissionLauncher.launch(missingPermissions.toTypedArray())
+            return
+        }
+        
+        ILog.d("VehicleFragment", "✅ 所有权限已授予")
+        performConnectToDevice()
+    }
+    
+    /**
+     * 执行连接设备(权限已授予后调用)
+     */
+    private fun performConnectToDevice() {
         btnConnectBluetooth.isEnabled = false
         tvBluetoothStatus.text = "正在扫描设备..."
         
@@ -837,6 +904,8 @@ class VehicleFragment : Fragment() {
         val userId = ByteArray(16) { it.toByte() } // 示例:实际应该从用户信息获取
         val userType = BLEConstants.USER_TYPE_OWNER
         
+        ILog.d("VehicleFragment", "开始扫描并连接设备...")
+        
         // 一键完成:初始化监听 + 扫描 + 连接 + 握手
         bleService.initializeAndConnect(
             userId = userId,

+ 2 - 1
app/src/main/res/layout/activity_main.xml

@@ -6,7 +6,8 @@
     android:layout_height="match_parent"
     tools:context=".MainActivity">
 
-    <!-- Navigation Fragment容器 -->
+    <!-- 主界面 Navigation Fragment容器 -->
+    <!-- 包含 MainFragment,MainFragment 内部使用底部导航栏管理各个业务模块 -->
     <androidx.fragment.app.FragmentContainerView
         android:id="@+id/navHostFragment"
         android:name="androidx.navigation.fragment.NavHostFragment"

+ 26 - 10
app/src/main/res/navigation/nav_graph.xml

@@ -5,26 +5,41 @@
     android:id="@+id/nav_graph"
     app:startDestination="@id/loginFragment">
 
-    <!-- 登录Fragment -->
+    <!-- 登录Fragment -->
     <fragment
         android:id="@+id/loginFragment"
-        android:name="com.narutohuo.xindazhou.user.ui.login.LoginFragment"
-        android:label="登录">
-        <action
-            android:id="@+id/action_loginFragment_to_registerFragment"
-            app:destination="@id/registerFragment" />
+        android:name="com.narutohuo.xindazhou.common.auth.ui.LoginFragment"
+        android:label="登录"
+        tools:layout="@layout/fragment_login">
+        
+        <!-- 登录成功后导航到主界面 -->
         <action
             android:id="@+id/action_loginFragment_to_mainFragment"
             app:destination="@id/mainFragment"
             app:popUpTo="@id/loginFragment"
             app:popUpToInclusive="true" />
+        
+        <!-- 跳转到注册页 -->
+        <action
+            android:id="@+id/action_loginFragment_to_registerFragment"
+            app:destination="@id/registerFragment" />
     </fragment>
 
-    <!-- 注册Fragment -->
+    <!-- 注册Fragment -->
     <fragment
         android:id="@+id/registerFragment"
-        android:name="com.narutohuo.xindazhou.user.ui.register.RegisterFragment"
-        android:label="注册">
+        android:name="com.narutohuo.xindazhou.common.auth.ui.RegisterFragment"
+        android:label="注册"
+        tools:layout="@layout/fragment_register">
+        
+        <!-- 注册成功后返回登录页 -->
+        <action
+            android:id="@+id/action_registerFragment_to_loginFragment"
+            app:destination="@id/loginFragment"
+            app:popUpTo="@id/registerFragment"
+            app:popUpToInclusive="true" />
+        
+        <!-- 注册成功后直接进入主界面(可选) -->
         <action
             android:id="@+id/action_registerFragment_to_mainFragment"
             app:destination="@id/mainFragment"
@@ -33,9 +48,10 @@
     </fragment>
 
     <!-- 主界面Fragment -->
+    <!-- 包含底部导航栏(TabBar)和各个业务模块的Fragment容器 -->
     <fragment
         android:id="@+id/mainFragment"
-        android:name="com.narutohuo.xindazhou.common.main.ui.MainFragment"
+        android:name="com.narutohuo.xindazhou.main.ui.MainFragment"
         android:label="主界面"
         tools:layout="@layout/fragment_main" />
 

+ 11 - 0
base-common/build.gradle

@@ -41,6 +41,17 @@ dependencies {
     // RecyclerView (用于级联选择器)
     implementation("androidx.recyclerview:recyclerview:1.3.2")
     
+    // ViewPager2 (用于引导页)
+    implementation("androidx.viewpager2:viewpager2:1.0.0")
+    
+    // AndroidX Lifecycle(用于 ProcessLifecycleOwner)
+    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+    implementation("androidx.lifecycle:lifecycle-process:2.7.0")
+    
+    // SocketIO 客户端库
+    // 使用 2.1.2 版本,支持 Socket.IO 2.x/3.x(服务器端使用 socketio4j 3.0.1,支持 Engine.IO v1-v3)
+    implementation("io.socket:socket.io-client:2.1.2")
+    
     // 注意:Gson、Retrofit、OkHttp、Glide、Coroutines、Timber 已通过 base-core 传递,无需重复依赖
     // 日志通过 base-core 的 ILog 接口统一管理,无需单独依赖 Timber
 }

+ 95 - 1
base-common/src/main/java/com/narutohuo/xindazhou/common/auth/AuthManager.kt

@@ -9,7 +9,10 @@ import com.narutohuo.xindazhou.common.auth.datasource.remote.AuthRemoteDataSourc
 import com.narutohuo.xindazhou.common.auth.datasource.remote.AuthRemoteDataSourceImpl
 import com.narutohuo.xindazhou.common.auth.repository.AuthRepository
 import com.narutohuo.xindazhou.common.auth.storage.TokenStore
+import com.narutohuo.xindazhou.common.auth.utils.JWTUtil
 import com.narutohuo.xindazhou.common.log.LogHelper
+import com.narutohuo.xindazhou.common.network.NetworkHelper
+import com.narutohuo.xindazhou.common.socketio.SocketIOManager
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -54,6 +57,7 @@ object AuthManager {
      * 
      * 需要在 Application.onCreate() 中调用
      * 会自动初始化 TokenStore 并使用默认的 AuthLocalDataSourceImpl
+     * 会自动配置 NetworkHelper 的 Token Provider
      * 
      * @param context 应用上下文
      */
@@ -64,18 +68,33 @@ object AuthManager {
         val defaultLocalDataSource = AuthLocalDataSourceImpl()
         this.localDataSource = defaultLocalDataSource
         this.repository = AuthRepository(remoteDataSource, defaultLocalDataSource)
+        
+        // 配置 NetworkHelper 的 Token Provider(使用 TokenStore 的同步方法)
+        NetworkHelper.setTokenProvider {
+            TokenStore.getAccessToken()
+        }
+        
+        LogHelper.d("AuthManager", "认证管理器初始化完成,Token Provider 已配置")
     }
     
     /**
      * 初始化认证管理器(使用自定义实现)
      * 
      * 如果需要自定义本地数据源,可以使用此方法
+     * 会自动配置 NetworkHelper 的 Token Provider
      * 
      * @param localDataSource 自定义的本地数据源
      */
     fun init(localDataSource: AuthLocalDataSource) {
         this.localDataSource = localDataSource
         this.repository = AuthRepository(remoteDataSource, localDataSource)
+        
+        // 配置 NetworkHelper 的 Token Provider(使用 TokenStore 的同步方法)
+        NetworkHelper.setTokenProvider {
+            TokenStore.getAccessToken()
+        }
+        
+        LogHelper.d("AuthManager", "认证管理器初始化完成(自定义数据源),Token Provider 已配置")
     }
     
     /**
@@ -101,6 +120,13 @@ object AuthManager {
                 }
                 
                 val result = repo.login(mobile, password)
+                
+                // 登录成功后,自动触发 Socket.IO 连接
+                result.onSuccess {
+                    LogHelper.d("AuthManager", "登录成功,自动触发 Socket.IO 连接")
+                    SocketIOManager.ensureConnected()
+                }
+                
                 onResult(result as Result<com.narutohuo.xindazhou.common.auth.model.LoginResponse>)
             } catch (e: Exception) {
                 LogHelper.e("AuthManager", "登录异常", e)
@@ -132,6 +158,13 @@ object AuthManager {
                 }
                 
                 val result = repo.register(mobile, password)
+                
+                // 注册成功后,自动触发 Socket.IO 连接
+                result.onSuccess {
+                    LogHelper.d("AuthManager", "注册成功,自动触发 Socket.IO 连接")
+                    SocketIOManager.ensureConnected()
+                }
+                
                 onResult(result as Result<com.narutohuo.xindazhou.common.auth.model.LoginResponse>)
             } catch (e: Exception) {
                 LogHelper.e("AuthManager", "注册异常", e)
@@ -172,7 +205,10 @@ object AuthManager {
     // ========== 便捷方法 ==========
     
     /**
-     * 检查是否已登录
+     * 检查是否已登录(同步,仅检查 Access Token,不刷新)
+     * 
+     * 注意:此方法只检查 Access Token 是否存在且未过期,不会尝试刷新
+     * 如果需要自动刷新功能,请使用 isLoggedInAsync()
      * 
      * @return true 表示已登录
      */
@@ -181,6 +217,60 @@ object AuthManager {
     }
     
     /**
+     * 检查是否已登录(异步,支持自动刷新)
+     * 
+     * 检查 Access Token 是否存在且未过期
+     * 如果 Access Token 过期,会尝试使用 Refresh Token 刷新
+     * 只有在 Refresh Token 也过期或刷新失败时,才返回 false
+     * 
+     * @return true 表示已登录
+     */
+    suspend fun isLoggedInAsync(): Boolean {
+        // 1. 先检查 Access Token 是否存在
+        val accessToken = TokenStore.getAccessToken()
+        
+        // 2. 如果 Access Token 不存在,检查是否有 Refresh Token
+        if (accessToken.isNullOrEmpty()) {
+            val refreshToken = TokenStore.getRefreshToken()
+            if (!refreshToken.isNullOrEmpty()) {
+                // 有 Refresh Token,尝试刷新 Access Token
+                LogHelper.d("AuthManager", "Access Token 不存在,尝试使用 Refresh Token 刷新...")
+                val newToken = refreshTokenIfNeeded()
+                return !newToken.isNullOrEmpty()
+            }
+            // 没有 Refresh Token,返回 false
+            return false
+        }
+        
+        // 3. 检查 Access Token 是否过期
+        if (!JWTUtil.isTokenValid(accessToken)) {
+            // Access Token 已过期,检查是否有 Refresh Token
+            val refreshToken = TokenStore.getRefreshToken()
+            if (!refreshToken.isNullOrEmpty()) {
+                // 有 Refresh Token,尝试刷新 Access Token
+                LogHelper.d("AuthManager", "Access Token 已过期,尝试使用 Refresh Token 刷新...")
+                val newToken = refreshTokenIfNeeded()
+                
+                // 如果刷新成功,验证新 Token 是否有效
+                if (!newToken.isNullOrEmpty() && JWTUtil.isTokenValid(newToken)) {
+                    LogHelper.d("AuthManager", "Access Token 刷新成功,用户仍然处于登录状态")
+                    return true
+                } else {
+                    LogHelper.w("AuthManager", "Access Token 刷新失败或新 Token 无效,用户需要重新登录")
+                    return false
+                }
+            } else {
+                // 没有 Refresh Token,返回 false
+                LogHelper.w("AuthManager", "Access Token 已过期且没有 Refresh Token,用户需要重新登录")
+                return false
+            }
+        }
+        
+        // 4. Access Token 存在且未过期,返回 true
+        return true
+    }
+    
+    /**
      * 获取当前 Access Token
      * 
      * @return Access Token,如果未登录返回 null
@@ -231,6 +321,10 @@ object AuthManager {
             result.getOrNull()?.let { loginResponse ->
                 // Token 已由 AuthRepository 自动保存
                 LogHelper.d("AuthManager", "Token 刷新成功")
+                
+                // Token 刷新成功后,确保 Socket.IO 使用新 Token 重连
+                SocketIOManager.ensureConnected()
+                
                 onResult?.invoke(Result.success(loginResponse.accessToken))
                 loginResponse.accessToken
             } ?: run {

+ 0 - 99
base-common/src/main/java/com/narutohuo/xindazhou/common/auth/LoginActivity.kt

@@ -1,99 +0,0 @@
-package com.narutohuo.xindazhou.common.auth
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.FragmentActivity
-import com.narutohuo.xindazhou.common.auth.ui.LoginFragment
-import com.narutohuo.xindazhou.common.log.LogHelper
-import com.narutohuo.xindazhou.common.ui.ActivityManager
-
-/**
- * 登录页 Activity(完整封装,可直接使用)
- * 
- * 使用方式:
- * 通过 Intent 启动,传入主界面 Activity 类名:
- * ```kotlin
- * val intent = Intent(context, LoginActivity::class.java).apply {
- *     putExtra(LoginActivity.EXTRA_MAIN_ACTIVITY_CLASS, MainActivity::class.java.name)
- * }
- * startActivity(intent)
- * ```
- */
-class LoginActivity : AppCompatActivity() {
-    
-    companion object {
-        /**
-         * Intent 参数:主界面 Activity 类名
-         */
-        const val EXTRA_MAIN_ACTIVITY_CLASS = "extra_main_activity_class"
-    }
-    
-    private var mainActivityClass: Class<out FragmentActivity>? = null
-    
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        
-        // 注册到 ActivityManager
-        ActivityManager.addActivity(this)
-        
-        // 从 Intent 获取主界面 Activity 类名
-        val mainActivityClassName = intent.getStringExtra(EXTRA_MAIN_ACTIVITY_CLASS)
-        
-        if (mainActivityClassName.isNullOrEmpty()) {
-            LogHelper.e("LoginActivity", "缺少参数:EXTRA_MAIN_ACTIVITY_CLASS")
-            finish()
-            return
-        }
-        
-        // 加载主界面 Activity 类
-        try {
-            @Suppress("UNCHECKED_CAST")
-            mainActivityClass = Class.forName(mainActivityClassName) as? Class<out FragmentActivity>
-            if (mainActivityClass == null) {
-                LogHelper.e("LoginActivity", "主界面 Activity 类不是 FragmentActivity 的子类:$mainActivityClassName")
-                finish()
-                return
-            }
-        } catch (e: Exception) {
-            LogHelper.e("LoginActivity", "加载主界面 Activity 类失败:$mainActivityClassName", e)
-            finish()
-            return
-        }
-        
-        // 设置简单的布局,只包含 Fragment 容器
-        val container = android.widget.FrameLayout(this).apply {
-            id = android.R.id.content
-        }
-        setContentView(container)
-        
-        // 显示登录 Fragment
-        if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
-            supportFragmentManager.beginTransaction()
-                .replace(android.R.id.content, LoginFragment())
-                .commit()
-        }
-    }
-    
-    /**
-     * 登录成功后的回调(公开方法,Fragment 可以调用)
-     */
-    fun handleLoginSuccess() {
-        val targetClass = mainActivityClass ?: run {
-            LogHelper.e("LoginActivity", "主界面 Activity 类未设置")
-            return
-        }
-        
-        val intent = Intent(this, targetClass).apply {
-            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
-        }
-        startActivity(intent)
-        finish()
-    }
-    
-    override fun onDestroy() {
-        super.onDestroy()
-        // 从 ActivityManager 中移除
-        ActivityManager.removeActivity(this)
-    }
-}

+ 25 - 0
base-common/src/main/java/com/narutohuo/xindazhou/common/auth/NavigationCallback.kt

@@ -0,0 +1,25 @@
+package com.narutohuo.xindazhou.common.auth
+
+/**
+ * 导航回调接口
+ * 
+ * 用于单 Activity 架构中,Fragment 通过接口回调触发导航
+ * Activity 实现此接口,处理具体的导航逻辑
+ */
+interface NavigationCallback {
+    /**
+     * 导航到主界面
+     */
+    fun navigateToMain()
+    
+    /**
+     * 导航到登录页
+     */
+    fun navigateToLogin()
+    
+    /**
+     * 导航到注册页
+     */
+    fun navigateToRegister()
+}
+

+ 0 - 135
base-common/src/main/java/com/narutohuo/xindazhou/common/auth/RegisterActivity.kt

@@ -1,135 +0,0 @@
-package com.narutohuo.xindazhou.common.auth
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.FragmentActivity
-import com.narutohuo.xindazhou.common.auth.ui.RegisterFragment
-import com.narutohuo.xindazhou.common.log.LogHelper
-import com.narutohuo.xindazhou.common.ui.ActivityManager
-
-/**
- * 注册页 Activity(完整封装,可直接使用)
- * 
- * 使用方式:
- * 通过 Intent 启动,传入必要的参数:
- * ```kotlin
- * val intent = Intent(context, RegisterActivity::class.java).apply {
- *     putExtra(RegisterActivity.EXTRA_LOGIN_ACTIVITY_CLASS, LoginActivity::class.java.name)
- *     putExtra(RegisterActivity.EXTRA_MAIN_ACTIVITY_CLASS, MainActivity::class.java.name)
- *     putExtra(RegisterActivity.EXTRA_GO_TO_MAIN_AFTER_REGISTER, false) // 注册后是否直接进入主界面
- * }
- * startActivity(intent)
- * ```
- */
-class RegisterActivity : AppCompatActivity() {
-    
-    companion object {
-        /**
-         * Intent 参数:登录页 Activity 类名
-         */
-        const val EXTRA_LOGIN_ACTIVITY_CLASS = "extra_login_activity_class"
-        
-        /**
-         * Intent 参数:主界面 Activity 类名
-         */
-        const val EXTRA_MAIN_ACTIVITY_CLASS = "extra_main_activity_class"
-        
-        /**
-         * Intent 参数:注册成功后是否直接跳转到主界面(默认 false,跳转到登录页)
-         */
-        const val EXTRA_GO_TO_MAIN_AFTER_REGISTER = "extra_go_to_main_after_register"
-    }
-    
-    private var loginActivityClass: Class<out FragmentActivity>? = null
-    private var mainActivityClass: Class<out FragmentActivity>? = null
-    private var goToMainAfterRegister: Boolean = false
-    
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        
-        // 注册到 ActivityManager
-        ActivityManager.addActivity(this)
-        
-        // 从 Intent 获取参数
-        val loginActivityClassName = intent.getStringExtra(EXTRA_LOGIN_ACTIVITY_CLASS)
-        val mainActivityClassName = intent.getStringExtra(EXTRA_MAIN_ACTIVITY_CLASS)
-        goToMainAfterRegister = intent.getBooleanExtra(EXTRA_GO_TO_MAIN_AFTER_REGISTER, false)
-        
-        if (loginActivityClassName.isNullOrEmpty()) {
-            LogHelper.e("RegisterActivity", "缺少参数:EXTRA_LOGIN_ACTIVITY_CLASS")
-            finish()
-            return
-        }
-        
-        if (mainActivityClassName.isNullOrEmpty()) {
-            LogHelper.e("RegisterActivity", "缺少参数:EXTRA_MAIN_ACTIVITY_CLASS")
-            finish()
-            return
-        }
-        
-        // 加载 Activity 类
-        try {
-            @Suppress("UNCHECKED_CAST")
-            loginActivityClass = Class.forName(loginActivityClassName) as? Class<out FragmentActivity>
-            if (loginActivityClass == null) {
-                LogHelper.e("RegisterActivity", "登录页 Activity 类不是 FragmentActivity 的子类:$loginActivityClassName")
-                finish()
-                return
-            }
-            
-            @Suppress("UNCHECKED_CAST")
-            mainActivityClass = Class.forName(mainActivityClassName) as? Class<out FragmentActivity>
-            if (mainActivityClass == null) {
-                LogHelper.e("RegisterActivity", "主界面 Activity 类不是 FragmentActivity 的子类:$mainActivityClassName")
-                finish()
-                return
-            }
-        } catch (e: Exception) {
-            LogHelper.e("RegisterActivity", "加载 Activity 类失败", e)
-            finish()
-            return
-        }
-        
-        // 设置简单的布局,只包含 Fragment 容器
-        val container = android.widget.FrameLayout(this).apply {
-            id = android.R.id.content
-        }
-        setContentView(container)
-        
-        // 显示注册 Fragment
-        if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
-            supportFragmentManager.beginTransaction()
-                .replace(android.R.id.content, RegisterFragment())
-                .commit()
-        }
-    }
-    
-    /**
-     * 注册成功后的回调(公开方法,Fragment 可以调用)
-     */
-    fun handleRegisterSuccess() {
-        val targetClass = if (goToMainAfterRegister) {
-            mainActivityClass
-        } else {
-            loginActivityClass
-        }
-        
-        if (targetClass == null) {
-            LogHelper.e("RegisterActivity", "目标 Activity 类未设置")
-            return
-        }
-        
-        val intent = Intent(this, targetClass).apply {
-            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
-        }
-        startActivity(intent)
-        finish()
-    }
-    
-    override fun onDestroy() {
-        super.onDestroy()
-        // 从 ActivityManager 中移除
-        ActivityManager.removeActivity(this)
-    }
-}

+ 1 - 1
base-common/src/main/java/com/narutohuo/xindazhou/common/auth/datasource/remote/AuthRemoteDataSource.kt

@@ -37,7 +37,7 @@ interface AuthRemoteDataSource {
 class AuthRemoteDataSourceImpl : ApiBaseRemoteDataSource(), AuthRemoteDataSource {
     
     private val authApi: AuthApi by lazy {
-        com.narutohuo.xindazhou.common.network.ApiServiceFactory.create<AuthApi>()
+        com.narutohuo.xindazhou.common.network.ApiManager.create<AuthApi>()
     }
     
     override suspend fun login(request: LoginRequest): Result<LoginResponse> {

+ 41 - 38
base-common/src/main/java/com/narutohuo/xindazhou/common/auth/storage/TokenStore.kt

@@ -1,18 +1,20 @@
 package com.narutohuo.xindazhou.common.auth.storage
 
 import android.content.Context
-import android.content.SharedPreferences
+import com.narutohuo.xindazhou.common.auth.utils.JWTUtil
+import com.narutohuo.xindazhou.core.storage.StorageImpl
 
 /**
  * Token 存储管理器
  * 
  * 负责 Token 的存储、读取和清除
- * 独立模块,可在 base-common 中独立运行
+ * 底层使用 base-core 的 StorageImpl 统一存储
  * 
  * 使用方式:
  * ```kotlin
- * // 在 Application 中初始化
- * TokenStore.init(context)
+ * // 在 Application 中初始化(StorageImpl 和 TokenStore 都需要初始化)
+ * StorageImpl.init(context)
+ * TokenStore.init(context)  // 可选,如果 StorageImpl 已初始化,这个可以省略
  * 
  * // 保存 Token
  * TokenStore.saveToken(accessToken, refreshToken, userId)
@@ -25,34 +27,27 @@ import android.content.SharedPreferences
  * ```
  */
 object TokenStore {
-    private const val PREFS_NAME = "xdz_auth_prefs"
     private const val KEY_ACCESS_TOKEN = "access_token"
     private const val KEY_REFRESH_TOKEN = "refresh_token"
     private const val KEY_USER_ID = "user_id"
     private const val KEY_SERVER_URL = "server_url"
     private const val KEY_SOCKET_URL = "socket_url"
     
-    private var prefs: SharedPreferences? = null
-    private var initialized = false
-    
     /**
-     * 初始化 TokenStore
+     * 初始化 TokenStore(可选)
      * 
-     * 需要在 Application.onCreate() 中调用
+     * 如果 StorageImpl 已初始化,此方法可以省略
      * 
      * @param context 应用上下文
      */
     fun init(context: Context) {
-        prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
-        initialized = true
+        // 确保 StorageImpl 已初始化
+        if (!StorageImpl.isInitialized()) {
+            StorageImpl.init(context)
+        }
     }
     
     /**
-     * 检查是否已初始化
-     */
-    fun isInitialized(): Boolean = initialized
-    
-    /**
      * 保存Token
      * 
      * @param accessToken 访问令牌
@@ -60,12 +55,9 @@ object TokenStore {
      * @param userId 用户ID
      */
     fun saveToken(accessToken: String, refreshToken: String, userId: Long) {
-        prefs?.edit()?.apply {
-            putString(KEY_ACCESS_TOKEN, accessToken)
-            putString(KEY_REFRESH_TOKEN, refreshToken)
-            putLong(KEY_USER_ID, userId)
-            apply()
-        }
+        StorageImpl.putString(KEY_ACCESS_TOKEN, accessToken)
+        StorageImpl.putString(KEY_REFRESH_TOKEN, refreshToken)
+        StorageImpl.putLong(KEY_USER_ID, userId)
     }
     
     /**
@@ -74,7 +66,8 @@ object TokenStore {
      * @return Access Token,如果不存在返回 null
      */
     fun getAccessToken(): String? {
-        return prefs?.getString(KEY_ACCESS_TOKEN, null)
+        val token = StorageImpl.getString(KEY_ACCESS_TOKEN)
+        return if (token.isEmpty()) null else token
     }
     
     /**
@@ -83,7 +76,8 @@ object TokenStore {
      * @return Refresh Token,如果不存在返回 null
      */
     fun getRefreshToken(): String? {
-        return prefs?.getString(KEY_REFRESH_TOKEN, null)
+        val token = StorageImpl.getString(KEY_REFRESH_TOKEN)
+        return if (token.isEmpty()) null else token
     }
     
     /**
@@ -92,28 +86,35 @@ object TokenStore {
      * @return 用户ID,如果不存在返回 0
      */
     fun getUserId(): Long {
-        return prefs?.getLong(KEY_USER_ID, 0) ?: 0
+        return StorageImpl.getLong(KEY_USER_ID)
     }
     
     /**
-     * 判断是否已登录
+     * 判断是否已登录(仅检查 Access Token,不刷新)
+     * 
+     * 注意:此方法只检查 Access Token 是否存在且未过期,不会尝试刷新
+     * 如果需要自动刷新功能,请使用 AuthManager.isLoggedInAsync()
+     * 
+     * 检查 Token 是否存在且未过期
      * 
      * @return true 表示已登录(有有效的 Access Token)
      */
     fun isLoggedIn(): Boolean {
-        return !getAccessToken().isNullOrEmpty()
+        val token = getAccessToken()
+        if (token.isNullOrEmpty()) {
+            return false
+        }
+        // 检查 Token 是否过期(不刷新)
+        return JWTUtil.isTokenValid(token)
     }
     
     /**
      * 清除Token(登出)
      */
     fun clearToken() {
-        prefs?.edit()?.apply {
-            remove(KEY_ACCESS_TOKEN)
-            remove(KEY_REFRESH_TOKEN)
-            remove(KEY_USER_ID)
-            apply()
-        }
+        StorageImpl.remove(KEY_ACCESS_TOKEN)
+        StorageImpl.remove(KEY_REFRESH_TOKEN)
+        StorageImpl.remove(KEY_USER_ID)
     }
     
     /**
@@ -122,7 +123,7 @@ object TokenStore {
      * @param url 服务器地址
      */
     fun saveServerUrl(url: String) {
-        prefs?.edit()?.putString(KEY_SERVER_URL, url)?.apply()
+        StorageImpl.putString(KEY_SERVER_URL, url)
     }
     
     /**
@@ -131,7 +132,8 @@ object TokenStore {
      * @return 服务器地址,如果不存在返回 null
      */
     fun getServerUrl(): String? {
-        return prefs?.getString(KEY_SERVER_URL, null)
+        val url = StorageImpl.getString(KEY_SERVER_URL)
+        return if (url.isEmpty()) null else url
     }
     
     /**
@@ -140,7 +142,7 @@ object TokenStore {
      * @param url Socket地址
      */
     fun saveSocketUrl(url: String) {
-        prefs?.edit()?.putString(KEY_SOCKET_URL, url)?.apply()
+        StorageImpl.putString(KEY_SOCKET_URL, url)
     }
     
     /**
@@ -149,7 +151,8 @@ object TokenStore {
      * @return Socket地址,如果不存在返回 null
      */
     fun getSocketUrl(): String? {
-        return prefs?.getString(KEY_SOCKET_URL, null)
+        val url = StorageImpl.getString(KEY_SOCKET_URL)
+        return if (url.isEmpty()) null else url
     }
 }
 

+ 11 - 7
base-common/src/main/java/com/narutohuo/xindazhou/common/auth/ui/LoginFragment.kt

@@ -9,15 +9,17 @@ import android.widget.Toast
 import androidx.fragment.app.Fragment
 import com.google.android.material.button.MaterialButton
 import com.google.android.material.textfield.TextInputEditText
-import com.narutohuo.xindazhou.common.auth.AuthManager
-import com.narutohuo.xindazhou.common.auth.LoginActivity
 import com.narutohuo.xindazhou.common.R
+import com.narutohuo.xindazhou.common.auth.AuthManager
+import com.narutohuo.xindazhou.common.auth.NavigationCallback
 import com.narutohuo.xindazhou.common.dialog.ServerConfigDialog
 import kotlinx.coroutines.launch
 import androidx.lifecycle.lifecycleScope
 
 /**
  * 登录 Fragment(完整封装)
+ * 
+ * 使用单 Activity 架构,通过 NavigationCallback 接口触发导航
  */
 class LoginFragment : Fragment() {
     
@@ -29,6 +31,9 @@ class LoginFragment : Fragment() {
     // 【测试功能】服务器配置按钮
     private var ivServerConfig: View? = null
     
+    private val navigationCallback: NavigationCallback?
+        get() = activity as? NavigationCallback
+    
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
@@ -80,8 +85,8 @@ class LoginFragment : Fragment() {
                     
                     result.onSuccess {
                         Toast.makeText(requireContext(), "登录成功", Toast.LENGTH_SHORT).show()
-                        // 通知 Activity 登录成功
-                        (activity as? LoginActivity)?.handleLoginSuccess()
+                        // 登录成功后,通过接口回调触发导航到主界面
+                        navigationCallback?.navigateToMain()
                     }.onFailure { error ->
                         Toast.makeText(requireContext(), error.message ?: "登录失败", Toast.LENGTH_SHORT).show()
                     }
@@ -91,9 +96,8 @@ class LoginFragment : Fragment() {
         
         // 注册按钮
         btnRegister.setOnClickListener {
-            // 跳转到注册页
-            val intent = android.content.Intent(requireContext(), com.narutohuo.xindazhou.common.auth.RegisterActivity::class.java)
-            startActivity(intent)
+            // 通过接口回调触发导航到注册页
+            navigationCallback?.navigateToRegister()
         }
     }
 }

+ 12 - 9
base-common/src/main/java/com/narutohuo/xindazhou/common/auth/ui/RegisterFragment.kt

@@ -10,13 +10,15 @@ import androidx.fragment.app.Fragment
 import androidx.lifecycle.lifecycleScope
 import com.google.android.material.button.MaterialButton
 import com.google.android.material.textfield.TextInputEditText
-import com.narutohuo.xindazhou.common.auth.AuthManager
-import com.narutohuo.xindazhou.common.auth.RegisterActivity
 import com.narutohuo.xindazhou.common.R
+import com.narutohuo.xindazhou.common.auth.AuthManager
+import com.narutohuo.xindazhou.common.auth.NavigationCallback
 import kotlinx.coroutines.launch
 
 /**
  * 注册 Fragment(完整封装)
+ * 
+ * 使用单 Activity 架构,通过 NavigationCallback 接口触发导航
  */
 class RegisterFragment : Fragment() {
     
@@ -27,6 +29,9 @@ class RegisterFragment : Fragment() {
     private lateinit var btnBackToLogin: MaterialButton
     private lateinit var progressBar: View
     
+    private val navigationCallback: NavigationCallback?
+        get() = activity as? NavigationCallback
+    
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
@@ -82,9 +87,9 @@ class RegisterFragment : Fragment() {
                     btnRegister.isEnabled = true
                     
                     result.onSuccess {
-                        Toast.makeText(requireContext(), "注册成功", Toast.LENGTH_SHORT).show()
-                        // 通知 Activity 注册成功
-                        (activity as? RegisterActivity)?.handleRegisterSuccess()
+                        Toast.makeText(requireContext(), "注册成功,请登录", Toast.LENGTH_SHORT).show()
+                        // 注册成功后,通过接口回调触发导航到登录页
+                        navigationCallback?.navigateToLogin()
                     }.onFailure { error ->
                         Toast.makeText(requireContext(), error.message ?: "注册失败", Toast.LENGTH_SHORT).show()
                     }
@@ -94,10 +99,8 @@ class RegisterFragment : Fragment() {
         
         // 返回登录按钮
         btnBackToLogin.setOnClickListener {
-            // 跳转到登录页
-            val intent = android.content.Intent(requireContext(), com.narutohuo.xindazhou.common.auth.LoginActivity::class.java)
-            startActivity(intent)
-            activity?.finish()
+            // 通过接口回调触发导航到登录页
+            navigationCallback?.navigateToLogin()
         }
     }
 }

+ 120 - 0
base-common/src/main/java/com/narutohuo/xindazhou/common/auth/utils/JWTUtil.kt

@@ -0,0 +1,120 @@
+package com.narutohuo.xindazhou.common.auth.utils
+
+import android.util.Base64
+import org.json.JSONObject
+
+/**
+ * JWT 工具类(客户端解析,不验证签名)
+ * 
+ * 用于解析 JWT Token 并检查是否过期
+ * 注意:客户端不需要验证签名(签名验证由服务端完成),只需要解析 payload 检查 exp 字段
+ */
+object JWTUtil {
+    
+    /**
+     * 解析 JWT Token 并检查是否过期
+     * 
+     * @param token JWT Token
+     * @return true 如果 Token 未过期,false 如果过期或无法解析
+     */
+    fun isTokenValid(token: String?): Boolean {
+        if (token.isNullOrEmpty()) {
+            return false
+        }
+        
+        // JWT 格式:header.payload.signature
+        val parts = token.split(".")
+        if (parts.size != 3) {
+            return false
+        }
+        
+        // 解析 payload(第二部分)
+        val payload = parts[1]
+        
+        try {
+            // Base64URL 解码
+            val payloadBytes = base64UrlDecode(payload) ?: return false
+            val payloadJson = JSONObject(String(payloadBytes))
+            
+            // 获取 exp 字段(过期时间,Unix 时间戳,单位:秒)
+            if (!payloadJson.has("exp")) {
+                return false
+            }
+            
+            val exp = payloadJson.getLong("exp")
+            
+            // 检查是否过期(exp 是 Unix 时间戳,单位:秒)
+            val currentTime = System.currentTimeMillis() / 1000 // 转换为秒
+            return exp > currentTime
+        } catch (e: Exception) {
+            // 解析失败,认为 Token 无效
+            return false
+        }
+    }
+    
+    /**
+     * 解析 JWT Token 并获取过期时间
+     * 
+     * @param token JWT Token
+     * @return 过期时间(Unix 时间戳,单位:秒),如果无法解析返回 null
+     */
+    fun getExpiresAt(token: String?): Long? {
+        if (token.isNullOrEmpty()) {
+            return null
+        }
+        
+        // JWT 格式:header.payload.signature
+        val parts = token.split(".")
+        if (parts.size != 3) {
+            return null
+        }
+        
+        // 解析 payload(第二部分)
+        val payload = parts[1]
+        
+        try {
+            // Base64URL 解码
+            val payloadBytes = base64UrlDecode(payload) ?: return null
+            val payloadJson = JSONObject(String(payloadBytes))
+            
+            // 获取 exp 字段(过期时间,Unix 时间戳,单位:秒)
+            if (!payloadJson.has("exp")) {
+                return null
+            }
+            
+            return payloadJson.getLong("exp")
+        } catch (e: Exception) {
+            // 解析失败
+            return null
+        }
+    }
+    
+    /**
+     * Base64URL 解码
+     * 
+     * Base64URL 是 Base64 的变体,用于 URL 安全:
+     * - 将 '+' 替换为 '-'
+     * - 将 '/' 替换为 '_'
+     * - 移除填充字符 '='
+     */
+    private fun base64UrlDecode(base64Url: String): ByteArray? {
+        return try {
+            // 转换为标准 Base64
+            var base64 = base64Url
+                .replace("-", "+")
+                .replace("_", "/")
+            
+            // 添加填充字符
+            val remainder = base64.length % 4
+            if (remainder > 0) {
+                base64 += "=".repeat(4 - remainder)
+            }
+            
+            // Base64 解码
+            Base64.decode(base64, Base64.NO_WRAP)
+        } catch (e: Exception) {
+            null
+        }
+    }
+}
+

+ 13 - 43
base-common/src/main/java/com/narutohuo/xindazhou/common/config/ConfigManager.kt

@@ -1,46 +1,21 @@
 package com.narutohuo.xindazhou.common.config
 
 import android.content.Context
-import android.content.SharedPreferences
 import com.narutohuo.xindazhou.core.log.ILog
+import com.narutohuo.xindazhou.core.storage.StorageImpl
 
 /**
  * 配置管理器
  * 
  * 业务层封装,提供便捷的配置管理方式
- * 直接使用 SharedPreferences 实现配置存储
- * 
- * 使用方式:
- * ```kotlin
- * // 初始化(在 Application 中)
- * ConfigManager.init(applicationContext)
- * 
- * // 获取配置
- * val apiUrl = ConfigManager.getString("api_url", "https://api.example.com")
- * val isDebug = ConfigManager.getBoolean("is_debug", false)
- * 
- * // 设置配置
- * ConfigManager.setString("api_url", "https://api.example.com")
- * ConfigManager.setBoolean("is_debug", true)
- * 
- * // 环境切换
- * val env = ConfigManager.getEnvironment()  // dev, test, prod
- * ConfigManager.setEnvironment("prod")
- * 
- * // 功能开关
- * val enabled = ConfigManager.isFeatureEnabled("new_feature")
- * ConfigManager.setFeatureEnabled("new_feature", true)
- * ```
+ * 底层使用 StorageImpl 统一存储
  */
 object ConfigManager {
     
     private const val TAG = "ConfigManager"
-    private const val PREFS_NAME = "app_config"
     private const val KEY_ENVIRONMENT = "environment"
     private const val KEY_FEATURE_PREFIX = "feature_"
     
-    private var sharedPreferences: SharedPreferences? = null
-    
     /**
      * 初始化配置管理器
      * 需要在 Application 中调用
@@ -48,19 +23,14 @@ object ConfigManager {
      * @param context 上下文
      */
     fun init(context: Context) {
-        sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
-    }
-    
-    private fun getPrefs(): SharedPreferences {
-        return sharedPreferences ?: throw IllegalStateException(
-            "ConfigManager 未初始化,请在 Application 中调用 ConfigManager.init(context)"
-        )
+        // 确保 StorageImpl 已初始化
+        if (!StorageImpl.isInitialized()) {
+            StorageImpl.init(context)
+        }
     }
     
     // ========== 基础配置操作 ==========
     
-    // ========== 基础配置操作 ==========
-    
     /**
      * 获取字符串配置
      * 
@@ -69,7 +39,7 @@ object ConfigManager {
      * @return 配置值
      */
     fun getString(key: String, defaultValue: String = ""): String {
-        return getPrefs().getString(key, defaultValue) ?: defaultValue
+        return StorageImpl.getString(key, defaultValue)
     }
     
     /**
@@ -80,7 +50,7 @@ object ConfigManager {
      * @return 配置值
      */
     fun getInt(key: String, defaultValue: Int = 0): Int {
-        return getPrefs().getInt(key, defaultValue)
+        return StorageImpl.getInt(key, defaultValue)
     }
     
     /**
@@ -91,7 +61,7 @@ object ConfigManager {
      * @return 配置值
      */
     fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
-        return getPrefs().getBoolean(key, defaultValue)
+        return StorageImpl.getBoolean(key, defaultValue)
     }
     
     /**
@@ -101,7 +71,7 @@ object ConfigManager {
      * @param value 配置值
      */
     fun setString(key: String, value: String) {
-        getPrefs().edit().putString(key, value).apply()
+        StorageImpl.putString(key, value)
     }
     
     /**
@@ -111,7 +81,7 @@ object ConfigManager {
      * @param value 配置值
      */
     fun setInt(key: String, value: Int) {
-        getPrefs().edit().putInt(key, value).apply()
+        StorageImpl.putInt(key, value)
     }
     
     /**
@@ -121,7 +91,7 @@ object ConfigManager {
      * @param value 配置值
      */
     fun setBoolean(key: String, value: Boolean) {
-        getPrefs().edit().putBoolean(key, value).apply()
+        StorageImpl.putBoolean(key, value)
     }
     
     // ========== 环境切换 ==========
@@ -170,7 +140,7 @@ object ConfigManager {
      * 清除所有配置
      */
     fun clearAll() {
-        getPrefs().edit().clear().apply()
+        StorageImpl.clear()
     }
 }
 

+ 8 - 7
base-common/src/main/java/com/narutohuo/xindazhou/common/config/ServerConfigManager.kt

@@ -1,8 +1,8 @@
 package com.narutohuo.xindazhou.common.config
 
 import android.content.Context
-import android.content.SharedPreferences
 import com.narutohuo.xindazhou.core.log.ILog
+import com.narutohuo.xindazhou.core.storage.StorageImpl
 
 /**
  * 服务器配置管理器
@@ -14,7 +14,6 @@ import com.narutohuo.xindazhou.core.log.ILog
  */
 object ServerConfigManager {
     
-    private const val PREFS_NAME = "server_config"
     private const val KEY_SERVER_IP = "server_ip"
     private const val DEFAULT_SERVER_IP = "192.168.1.107"
     
@@ -23,13 +22,14 @@ object ServerConfigManager {
     private const val PORT_HTTP_MESSAGE = 18082
     private const val PORT_SOCKETIO = 9090
     
-    private var prefs: SharedPreferences? = null
-    
     /**
      * 初始化(在Application中调用)
      */
     fun init(context: Context) {
-        prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+        // 确保 StorageImpl 已初始化
+        if (!StorageImpl.isInitialized()) {
+            StorageImpl.init(context)
+        }
         ILog.d("ServerConfigManager", "初始化完成,当前IP: ${getServerIp()}")
     }
     
@@ -37,14 +37,15 @@ object ServerConfigManager {
      * 获取服务器IP地址
      */
     fun getServerIp(): String {
-        return prefs?.getString(KEY_SERVER_IP, DEFAULT_SERVER_IP) ?: DEFAULT_SERVER_IP
+        val ip = StorageImpl.getString(KEY_SERVER_IP)
+        return if (ip.isEmpty()) DEFAULT_SERVER_IP else ip
     }
     
     /**
      * 设置服务器IP地址
      */
     fun setServerIp(ip: String) {
-        prefs?.edit()?.putString(KEY_SERVER_IP, ip)?.apply()
+        StorageImpl.putString(KEY_SERVER_IP, ip)
         ILog.d("ServerConfigManager", "服务器IP已更新为: $ip")
     }
     

+ 3 - 1
base-common/src/main/java/com/narutohuo/xindazhou/common/crash/CrashHelper.kt

@@ -1,6 +1,7 @@
 package com.narutohuo.xindazhou.common.crash
 
 import android.app.Application
+import androidx.core.content.pm.PackageInfoCompat
 import com.narutohuo.xindazhou.core.log.ILog
 import java.io.File
 import java.io.FileWriter
@@ -167,7 +168,8 @@ object CrashHelper {
     private fun getAppVersion(application: Application): String {
         return try {
             val packageInfo = application.packageManager.getPackageInfo(application.packageName, 0)
-            "${packageInfo.versionName} (${packageInfo.versionCode})"
+            val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
+            "${packageInfo.versionName} ($versionCode)"
         } catch (e: Exception) {
             "未知"
         }

+ 34 - 31
base-common/src/main/java/com/narutohuo/xindazhou/common/launch/AppLaunchManager.kt

@@ -1,34 +1,47 @@
 package com.narutohuo.xindazhou.common.launch
 
-import android.content.Intent
 import androidx.fragment.app.FragmentActivity
 import androidx.lifecycle.lifecycleScope
+import com.narutohuo.xindazhou.common.auth.NavigationCallback
 import com.narutohuo.xindazhou.common.log.LogHelper
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 
 /**
- * 应用启动管理器
+ * 应用启动管理器(单 Activity 架构)
  * 
- * 只负责登录状态判断和页面跳转,不处理版本检查
+ * 只负责登录状态判断和页面导航,不处理版本检查
  * 版本检查由 VersionUpdateManager 独立处理
+ * 
+ * 使用方式(在 MainActivity 中):
+ * ```kotlin
+ * override fun onCreate(savedInstanceState: Bundle?) {
+ *     super.onCreate(savedInstanceState)
+ *     
+ *     // 检查登录状态并导航
+ *     AppLaunchManager.handleLaunch(
+ *         activity = this,
+ *         isLoggedIn = { AuthManager.isLoggedInAsync() },
+ *         navigationCallback = this, // MainActivity 实现 NavigationCallback
+ *         splashDelay = 500
+ *     )
+ * }
+ * ```
  */
 object AppLaunchManager {
     
     /**
-     * 处理应用启动逻辑(根据登录状态跳转)
+     * 处理应用启动逻辑(根据登录状态导航,单 Activity 架构
      * 
-     * @param activity Activity 实例
-     * @param isLoggedIn 判断是否已登录的函数
-     * @param mainActivityClass 主界面 Activity 类
-     * @param loginActivityClass 登录页 Activity 类
+     * @param activity Activity 实例(通常是 MainActivity)
+     * @param isLoggedIn 判断是否已登录的函数(支持异步,可以自动刷新 Token)
+     * @param navigationCallback 导航回调接口(用于导航到主界面或登录页)
      * @param splashDelay 启动画面显示时长(毫秒,默认 500ms)
      */
     fun handleLaunch(
         activity: FragmentActivity,
-        isLoggedIn: () -> Boolean,
-        mainActivityClass: Class<out FragmentActivity>,
-        loginActivityClass: Class<out FragmentActivity>,
+        isLoggedIn: suspend () -> Boolean,
+        navigationCallback: NavigationCallback,
         splashDelay: Long = 500
     ) {
         activity.lifecycleScope.launch {
@@ -36,33 +49,23 @@ object AppLaunchManager {
                 // 显示启动画面
                 delay(splashDelay)
                 
-                // 根据登录状态跳转
+                // 根据登录状态导航(支持异步刷新 Token)
                 if (isLoggedIn()) {
-                    // 已登录,跳转到主界面
-                    val intent = Intent(activity, mainActivityClass).apply {
-                        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
-                    }
-                    activity.startActivity(intent)
-                    activity.finish()
+                    // 已登录,导航到主界面
+                    LogHelper.d("AppLaunchManager", "用户已登录,导航到主界面")
+                    navigationCallback.navigateToMain()
                 } else {
-                    // 未登录,跳转到登录页
-                    val intent = Intent(activity, loginActivityClass).apply {
-                        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
-                    }
-                    activity.startActivity(intent)
-                    activity.finish()
+                    // 未登录,导航到登录页
+                    LogHelper.d("AppLaunchManager", "用户未登录,导航到登录页")
+                    navigationCallback.navigateToLogin()
                 }
             } catch (e: Exception) {
                 LogHelper.e("AppLaunchManager", "启动处理失败", e)
-                // 出错时默认跳转到登录页
+                // 出错时默认导航到登录页
                 try {
-                    val intent = Intent(activity, loginActivityClass).apply {
-                        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
-                    }
-                    activity.startActivity(intent)
-                    activity.finish()
+                    navigationCallback.navigateToLogin()
                 } catch (ex: Exception) {
-                    LogHelper.e("AppLaunchManager", "跳转登录页失败", ex)
+                    LogHelper.e("AppLaunchManager", "导航到登录页失败", ex)
                 }
             }
         }

+ 0 - 79
base-common/src/main/java/com/narutohuo/xindazhou/common/launch/LaunchActivity.kt

@@ -1,79 +0,0 @@
-package com.narutohuo.xindazhou.common.launch
-
-import android.os.Bundle
-import android.view.View
-import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.FragmentActivity
-import com.narutohuo.xindazhou.common.ui.ActivityManager
-
-/**
- * 启动页 Activity(完整封装)
- * 
- * 职责:
- * 1. 显示启动画面
- * 2. 判断登录状态并跳转到对应页面
- * 3. 自动处理启动逻辑
- * 
- * 使用方式:
- * 在 app 模块中继承此类,实现 getMainActivityClass() 和 getLoginActivityClass() 方法
- * 
- * 示例:
- * ```kotlin
- * class SplashActivity : LaunchActivity() {
- *     override fun getMainActivityClass() = MainActivity::class.java
- *     override fun getLoginActivityClass() = LoginActivity::class.java
- *     override fun isLoggedIn() = TokenManager.isLoggedIn()
- * }
- * ```
- */
-abstract class LaunchActivity : AppCompatActivity() {
-    
-    /**
-     * 获取主界面 Activity 类(子类实现)
-     */
-    protected abstract fun getMainActivityClass(): Class<out FragmentActivity>
-    
-    /**
-     * 获取登录页 Activity 类(子类实现)
-     */
-    protected abstract fun getLoginActivityClass(): Class<out FragmentActivity>
-    
-    /**
-     * 判断是否已登录(子类实现)
-     */
-    protected abstract fun isLoggedIn(): Boolean
-    
-    /**
-     * 启动画面显示时长(毫秒,默认 500ms,子类可重写)
-     */
-    protected open val splashDelay: Long = 500
-    
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        
-        // 注册到 ActivityManager
-        ActivityManager.addActivity(this)
-        
-        // 设置简单的启动页布局
-        val root = View(this).apply {
-            setBackgroundColor(android.graphics.Color.WHITE)
-        }
-        setContentView(root)
-        
-        // 自动处理启动逻辑
-        AppLaunchManager.handleLaunch(
-            activity = this,
-            isLoggedIn = { isLoggedIn() },
-            mainActivityClass = getMainActivityClass(),
-            loginActivityClass = getLoginActivityClass(),
-            splashDelay = splashDelay
-        )
-    }
-    
-    override fun onDestroy() {
-        super.onDestroy()
-        // 从 ActivityManager 中移除
-        ActivityManager.removeActivity(this)
-    }
-}
-

+ 35 - 0
base-common/src/main/java/com/narutohuo/xindazhou/common/launch/OnboardingManager.kt

@@ -0,0 +1,35 @@
+package com.narutohuo.xindazhou.common.launch
+
+import com.narutohuo.xindazhou.common.storage.StorageManager
+
+/**
+ * 引导页管理器
+ * 
+ * 用于管理引导页的显示状态,记住用户是否已经看过引导页
+ */
+object OnboardingManager {
+    
+    private const val KEY_HAS_SEEN_ONBOARDING = "has_seen_onboarding"
+    
+    /**
+     * 检查是否已经看过引导页
+     */
+    fun hasSeenOnboarding(): Boolean {
+        return StorageManager.getBoolean(KEY_HAS_SEEN_ONBOARDING, false)
+    }
+    
+    /**
+     * 标记已经看过引导页
+     */
+    fun markOnboardingAsSeen() {
+        StorageManager.putBoolean(KEY_HAS_SEEN_ONBOARDING, true)
+    }
+    
+    /**
+     * 重置引导页状态(用于测试)
+     */
+    fun resetOnboarding() {
+        StorageManager.putBoolean(KEY_HAS_SEEN_ONBOARDING, false)
+    }
+}
+

+ 16 - 0
base-common/src/main/java/com/narutohuo/xindazhou/common/launch/model/OnboardingItem.kt

@@ -0,0 +1,16 @@
+package com.narutohuo.xindazhou.common.launch.model
+
+/**
+ * 引导页数据模型
+ */
+data class OnboardingItem(
+    /** 标题 */
+    val title: String,
+    /** 描述 */
+    val description: String,
+    /** 图片资源 ID(可选) */
+    val imageResId: Int? = null,
+    /** 图片 URL(可选) */
+    val imageUrl: String? = null
+)
+

+ 211 - 0
base-common/src/main/java/com/narutohuo/xindazhou/common/launch/ui/OnboardingFragment.kt

@@ -0,0 +1,211 @@
+package com.narutohuo.xindazhou.common.launch.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.tabs.TabLayoutMediator
+import com.narutohuo.xindazhou.common.R
+import com.narutohuo.xindazhou.common.auth.NavigationCallback
+import com.narutohuo.xindazhou.common.launch.OnboardingManager
+import com.narutohuo.xindazhou.common.launch.model.OnboardingItem
+
+/**
+ * 引导页 Fragment
+ * 
+ * 首次安装后显示的引导页,用户可以滑动浏览多张引导图片
+ * 最后一页有"立即体验"按钮,点击后进入主界面或登录页
+ * 
+ * 使用方式:
+ * ```kotlin
+ * // 使用默认引导页数据
+ * val fragment = OnboardingFragment()
+ * 
+ * // 或者自定义引导页数据
+ * val fragment = OnboardingFragment()
+ * fragment.setOnboardingItems(listOf(
+ *     OnboardingItem(title = "标题1", description = "描述1"),
+ *     OnboardingItem(title = "标题2", description = "描述2")
+ * ))
+ * ```
+ */
+open class OnboardingFragment : Fragment() {
+    
+    private lateinit var viewPager: ViewPager2
+    private lateinit var tabIndicator: TabLayout
+    private lateinit var btnStart: MaterialButton
+    private lateinit var btnSkip: MaterialButton
+    
+    private val navigationCallback: NavigationCallback?
+        get() = activity as? NavigationCallback
+    
+    // 默认引导页数据(可以在 app 模块中通过方法注入自定义数据)
+    private var onboardingItems: List<OnboardingItem> = emptyList()
+    
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_onboarding, container, false)
+    }
+    
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        
+        viewPager = view.findViewById(R.id.viewPager)
+        tabIndicator = view.findViewById(R.id.tabIndicator)
+        btnStart = view.findViewById(R.id.btnStart)
+        btnSkip = view.findViewById(R.id.btnSkip)
+        
+        // 如果子类没有提供数据,使用默认数据
+        if (onboardingItems.isEmpty()) {
+            onboardingItems = getDefaultOnboardingItems()
+        }
+        
+        // 设置 ViewPager2 适配器
+        val adapter = OnboardingPagerAdapter(onboardingItems)
+        viewPager.adapter = adapter
+        
+        // 连接 TabLayout 和 ViewPager2
+        TabLayoutMediator(tabIndicator, viewPager) { _, _ -> }.attach()
+        
+        // 初始化按钮显示状态(第一页显示"跳过"按钮)
+        updateButtonVisibility(0)
+        
+        // 监听页面变化,在最后一页显示"立即体验"按钮
+        viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+            override fun onPageSelected(position: Int) {
+                super.onPageSelected(position)
+                updateButtonVisibility(position)
+            }
+        })
+        
+        // "立即体验"按钮点击事件
+        btnStart.setOnClickListener {
+            finishOnboarding()
+        }
+        
+        // "跳过"按钮点击事件
+        btnSkip.setOnClickListener {
+            finishOnboarding()
+        }
+    }
+    
+    /**
+     * 更新按钮显示状态
+     */
+    private fun updateButtonVisibility(position: Int) {
+        // 如果是最后一页,显示"立即体验"按钮,隐藏"跳过"按钮
+        if (position == onboardingItems.size - 1) {
+            btnStart.visibility = View.VISIBLE
+            btnSkip.visibility = View.GONE
+        } else {
+            btnStart.visibility = View.GONE
+            btnSkip.visibility = View.VISIBLE
+        }
+    }
+    
+    /**
+     * 完成引导页,标记已看过并导航到下一步
+     */
+    private fun finishOnboarding() {
+        // 标记已经看过引导页
+        OnboardingManager.markOnboardingAsSeen()
+        
+        // 通过接口回调触发导航(由 MainActivity 处理具体导航逻辑)
+        // MainActivity 会根据登录状态导航到登录页或主界面
+        navigationCallback?.navigateToLogin()
+    }
+    
+    /**
+     * 设置引导页数据(可选,子类可以调用此方法设置自定义数据)
+     */
+    fun setOnboardingItems(items: List<OnboardingItem>) {
+        this.onboardingItems = items
+        // 如果 ViewPager 已经创建,更新适配器
+        if (::viewPager.isInitialized && viewPager.adapter != null) {
+            (viewPager.adapter as? OnboardingPagerAdapter)?.updateItems(items)
+        }
+    }
+    
+    /**
+     * 获取默认引导页数据
+     * 
+     * 子类可以重写此方法提供自定义的引导页数据
+     */
+    protected open fun getDefaultOnboardingItems(): List<OnboardingItem> {
+        return listOf(
+            OnboardingItem(
+                title = "欢迎使用新大洲本田",
+                description = "探索更多精彩功能,开启您的智能出行体验"
+            ),
+            OnboardingItem(
+                title = "智能车辆管理",
+                description = "实时查看车辆状态,轻松管理您的爱车"
+            ),
+            OnboardingItem(
+                title = "便捷服务",
+                description = "一键预约维修、保养等服务,让用车更简单"
+            )
+        )
+    }
+    
+    /**
+     * ViewPager2 适配器
+     */
+    private class OnboardingPagerAdapter(
+        private var items: List<OnboardingItem>
+    ) : RecyclerView.Adapter<OnboardingPagerAdapter.ViewHolder>() {
+        
+        fun updateItems(newItems: List<OnboardingItem>) {
+            items = newItems
+            notifyDataSetChanged()
+        }
+        
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+            val view = LayoutInflater.from(parent.context)
+                .inflate(R.layout.item_onboarding, parent, false)
+            return ViewHolder(view)
+        }
+        
+        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+            val item = items[position]
+            holder.bind(item)
+        }
+        
+        override fun getItemCount(): Int = items.size
+        
+        class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+            private val ivImage: ImageView = itemView.findViewById(R.id.ivImage)
+            private val tvTitle: TextView = itemView.findViewById(R.id.tvTitle)
+            private val tvDescription: TextView = itemView.findViewById(R.id.tvDescription)
+            
+            fun bind(item: OnboardingItem) {
+                tvTitle.text = item.title
+                tvDescription.text = item.description
+                
+                // 设置图片(如果有)
+                if (item.imageResId != null) {
+                    ivImage.setImageResource(item.imageResId)
+                    ivImage.visibility = View.VISIBLE
+                } else if (!item.imageUrl.isNullOrEmpty()) {
+                    // 使用图片加载库加载网络图片(这里需要根据项目使用的图片加载库来实现)
+                    // 例如使用 Glide: Glide.with(ivImage).load(item.imageUrl).into(ivImage)
+                    ivImage.visibility = View.VISIBLE
+                } else {
+                    // 没有图片时隐藏 ImageView
+                    ivImage.visibility = View.GONE
+                }
+            }
+        }
+    }
+}
+

+ 1 - 1
base-common/src/main/java/com/narutohuo/xindazhou/common/network/ApiBaseRemoteDataSource.kt

@@ -19,7 +19,7 @@ import java.net.SocketTimeoutException
  * 使用方式:
  * ```kotlin
  * class AuthRemoteDataSource : ApiBaseRemoteDataSource() {
- *     private val authApi: AuthApi = ApiServiceFactory.create()
+ *     private val authApi: AuthApi = ApiManager.create()
  *     
  *     suspend fun login(request: LoginRequest): Result<LoginResponse> {
  *         return executeRequest(

+ 141 - 0
base-common/src/main/java/com/narutohuo/xindazhou/common/network/ApiManager.kt

@@ -0,0 +1,141 @@
+package com.narutohuo.xindazhou.common.network
+
+import com.narutohuo.xindazhou.common.config.ServerConfigManager
+import com.narutohuo.xindazhou.common.log.LogHelper
+import com.narutohuo.xindazhou.core.network.NetworkManager
+import retrofit2.Retrofit
+
+/**
+ * API 管理器
+ * 
+ * 统一创建和管理 API 接口实例,避免重复代码
+ * 
+ * 与 AuthManager、SocketIOManager 类似,提供统一的 API 封装
+ * 自动处理配置初始化、URL 刷新等,外部只需要调用 create() 即可
+ * 
+ * 使用方式:
+ * ```kotlin
+ * // 在 AppInitializer 中初始化(自动从 ServerConfigManager 读取服务器地址)
+ * ApiManager.initialize(application)
+ * 
+ * // 然后就可以直接使用,无需再设置 baseUrl
+ * val authApi = ApiManager.create<AuthApi>()
+ * 
+ * // 或者使用自定义 baseUrl
+ * val authApi = ApiManager.create<AuthApi>("https://api.example.com")
+ * ```
+ */
+object ApiManager {
+    
+    private const val TAG = "ApiManager"
+    
+    private var cachedBaseURL: String? = null
+    private var isInitialized = false
+    
+    /**
+     * baseUrl 提供器(默认配置)
+     * 
+     * 在 initialize() 中自动设置,从 ServerConfigManager 读取服务器地址
+     * 也可以手动设置自定义提供器
+     * 
+     * 示例:
+     * ```kotlin
+     * ApiManager.baseUrlProvider = { 
+     *     ServerConfigManager.getHttpServerUrl() 
+     * }
+     * ```
+     */
+    var baseUrlProvider: (() -> String)? = null
+    
+    /**
+     * 初始化 API 管理器(从存储读取服务器地址)
+     * 
+     * 自动从 ServerConfigManager 读取服务器地址并缓存,同时初始化 NetworkHelper
+     * 自动检测调试模式,所有参数使用默认值,业务层只需一行代码即可完成初始化
+     * 
+     * @param application Application 实例(用于检测调试模式)
+     */
+    fun initialize(application: android.app.Application) {
+        if (isInitialized) {
+            LogHelper.d(TAG, "已初始化,跳过")
+            return
+        }
+        
+        // 从 ServerConfigManager 读取服务器地址
+        val serverURL = ServerConfigManager.getHttpServerUrl()
+        cachedBaseURL = serverURL
+        
+        // 设置 baseURLProvider 返回缓存的地址
+        this.baseUrlProvider = {
+            cachedBaseURL ?: serverURL
+        }
+        
+        // 自动检测调试模式
+        val isDebug = try {
+            val buildConfigClass = Class.forName("${application.packageName}.BuildConfig")
+            val debugField = buildConfigClass.getField("DEBUG")
+            debugField.getBoolean(null)
+        } catch (e: Exception) {
+            LogHelper.e(TAG, "无法获取 BuildConfig.DEBUG,默认返回 false", e)
+            false
+        }
+        
+        // 同时初始化 NetworkHelper(使用相同的 baseURL)
+        NetworkHelper.init(
+            baseUrl = serverURL,
+            isDebug = isDebug,
+            enableLogging = isDebug
+        )
+        
+        isInitialized = true
+        LogHelper.d(TAG, "初始化完成,baseURL: $serverURL, isDebug: $isDebug")
+    }
+    
+    /**
+     * 刷新 BaseURL(从存储重新读取)
+     * 
+     * 当服务器地址在存储中更新后,可以调用此方法刷新
+     */
+    fun refreshBaseURL() {
+        val serverURL = ServerConfigManager.getHttpServerUrl()
+        cachedBaseURL = serverURL
+        LogHelper.d(TAG, "BaseURL 已刷新: $serverURL")
+    }
+    
+    /**
+     * 创建 API 接口实例(工厂方法)
+     * 
+     * @param baseUrl 基础URL,如果为 null 则使用 baseUrlProvider
+     * @return API 接口实例
+     * @throws IllegalStateException 如果 baseUrl 和 baseUrlProvider 都未设置
+     */
+    inline fun <reified T> create(baseUrl: String? = null): T {
+        // 优先级:传入的 baseUrl > baseUrlProvider > 抛出异常
+        val url = baseUrl ?: baseUrlProvider?.invoke()
+            ?: throw IllegalStateException(
+                "baseUrl 未设置。请先调用 ApiManager.initialize() " +
+                "或 ApiManager.baseUrlProvider = { ... } " +
+                "或在 create() 中传入 baseUrl 参数"
+            )
+        
+        // 使用 NetworkManager 创建 Retrofit 实例
+        val retrofit = NetworkManager.getRetrofit(
+            com.narutohuo.xindazhou.core.network.NetworkConfig.Builder()
+                .baseUrl(url)
+                .build()
+        )
+        
+        return retrofit.create(T::class.java)
+    }
+    
+    /**
+     * 使用指定的 Retrofit 实例创建 API 接口实例
+     * 
+     * @param retrofit Retrofit 实例
+     * @return API 接口实例
+     */
+    inline fun <reified T> create(retrofit: Retrofit): T {
+        return retrofit.create(T::class.java)
+    }
+}
+

+ 0 - 77
base-common/src/main/java/com/narutohuo/xindazhou/common/network/ApiServiceFactory.kt

@@ -1,77 +0,0 @@
-package com.narutohuo.xindazhou.common.network
-
-import com.narutohuo.xindazhou.core.network.NetworkManager
-import retrofit2.Retrofit
-
-/**
- * API 服务工厂(工厂模式)
- * 
- * 统一创建 API 服务实例,避免重复代码
- * 
- * 使用方式:
- * ```kotlin
- * // 方式1:使用默认 baseUrl(需要在 Application 中设置 baseUrlProvider)
- * ApiServiceFactory.baseUrlProvider = { "https://api.example.com" }
- * val authApi = ApiServiceFactory.create<AuthApi>()
- * 
- * // 方式2:使用自定义 baseUrl
- * val authApi = ApiServiceFactory.create<AuthApi>("https://api.example.com")
- * 
- * // 方式3:使用指定的 Retrofit 实例
- * val retrofit = NetworkManager.getRetrofit(...)
- * val authApi = ApiServiceFactory.create<AuthApi>(retrofit)
- * ```
- */
-object ApiServiceFactory {
-    
-    /**
-     * baseUrl 提供器(默认配置)
-     * 
-     * 在 Application 中设置一次即可,后续调用 create() 时如果不传 baseUrl,
-     * 将使用此提供器返回的 baseUrl
-     * 
-     * 示例:
-     * ```kotlin
-     * ApiServiceFactory.baseUrlProvider = { 
-     *     ServerConfigManager.getHttpServerUrl() 
-     * }
-     * ```
-     */
-    var baseUrlProvider: (() -> String)? = null
-    
-    /**
-     * 创建 API 服务实例(工厂方法)
-     * 
-     * @param baseUrl 基础URL,如果为 null 则使用 baseUrlProvider
-     * @return API 服务实例
-     * @throws IllegalStateException 如果 baseUrl 和 baseUrlProvider 都未设置
-     */
-    inline fun <reified T> create(baseUrl: String? = null): T {
-        // 优先级:传入的 baseUrl > baseUrlProvider > 抛出异常
-        val url = baseUrl ?: baseUrlProvider?.invoke()
-            ?: throw IllegalStateException(
-                "baseUrl 未设置。请先调用 ApiServiceFactory.baseUrlProvider = { ... } " +
-                "或在 create() 中传入 baseUrl 参数"
-            )
-        
-        // 使用 NetworkManager 创建 Retrofit 实例
-        val retrofit = NetworkManager.getRetrofit(
-            com.narutohuo.xindazhou.core.network.NetworkConfig.Builder()
-                .baseUrl(url)
-                .build()
-        )
-        
-        return retrofit.create(T::class.java)
-    }
-    
-    /**
-     * 使用指定的 Retrofit 实例创建 API 服务
-     * 
-     * @param retrofit Retrofit 实例
-     * @return API 服务实例
-     */
-    inline fun <reified T> create(retrofit: Retrofit): T {
-        return retrofit.create(T::class.java)
-    }
-}
-

+ 76 - 2
base-common/src/main/java/com/narutohuo/xindazhou/common/router/RouterHelper.kt

@@ -1,7 +1,10 @@
 package com.narutohuo.xindazhou.common.router
 
 import android.app.Activity
+import android.content.Intent
 import android.os.Bundle
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
 import androidx.fragment.app.Fragment
 import com.alibaba.android.arouter.launcher.ARouter
 
@@ -95,13 +98,84 @@ object RouterHelper {
     }
     
     /**
-     * 带结果回调跳转(Fragment)
+     * 注册带结果回调的路由跳转(Fragment - 新 API
      * 
+     * 使用 ActivityResultLauncher,推荐使用此方法
+     * 
+     * 使用方式:
+     * ```kotlin
+     * // 在 Fragment 中注册
+     * private val resultLauncher = RouterHelper.registerForResult(this) { result ->
+     *     if (result.resultCode == Activity.RESULT_OK) {
+     *         val data = result.data
+     *         // 处理结果
+     *     }
+     * }
+     * 
+     * // 跳转
+     * RouterHelper.navigateForResult(resultLauncher, "/user/select") {
+     *     putString("type", "user")
+     * }
+     * ```
+     * 
+     * @param fragment Fragment 实例
+     * @param onResult 结果回调
+     * @return ActivityResultLauncher,配合 navigateForResult(launcher, path, params) 使用
+     */
+    fun registerForResult(
+        fragment: Fragment,
+        onResult: (androidx.activity.result.ActivityResult) -> Unit
+    ): ActivityResultLauncher<Intent> {
+        return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+            onResult(result)
+        }
+    }
+    
+    /**
+     * 带结果回调跳转(使用 ActivityResultLauncher - 新 API)
+     * 
+     * 配合 registerForResult() 使用
+     * 
+     * @param launcher ActivityResultLauncher(通过 registerForResult() 获取)
+     * @param path 路由路径
+     * @param params 参数构建器(可选)
+     */
+    fun navigateForResult(
+        launcher: ActivityResultLauncher<Intent>,
+        path: String,
+        params: (Bundle.() -> Unit)? = null
+    ) {
+        val postcard = ARouter.getInstance().build(path)
+        
+        params?.let {
+            val bundle = Bundle().apply(it)
+            postcard.with(bundle)
+        }
+        
+        val intent = postcard.navigation() as? Intent
+        if (intent != null) {
+            launcher.launch(intent)
+        }
+    }
+    
+    /**
+     * 带结果回调跳转(Fragment - 已废弃)
+     * 
+     * @deprecated 使用 registerForResult() 替代
      * @param fragment Fragment 实例
      * @param path 路由路径
      * @param requestCode 请求码
      * @param params 参数构建器(可选)
      */
+    @Deprecated(
+        message = "使用 registerForResult() 替代",
+        replaceWith = ReplaceWith(
+            "val launcher = RouterHelper.registerForResult(fragment) { result -> /* 处理结果 */ }\n" +
+            "val intent = ARouter.getInstance().build(path).with(bundle).navigation() as Intent\n" +
+            "launcher.launch(intent)"
+        )
+    )
+    @Suppress("DEPRECATION")
     fun navigateForResult(
         fragment: Fragment,
         path: String,
@@ -115,7 +189,7 @@ object RouterHelper {
             postcard.with(bundle)
         }
         
-        fragment.startActivityForResult(postcard.navigation() as android.content.Intent, requestCode)
+        fragment.startActivityForResult(postcard.navigation() as Intent, requestCode)
     }
     
     /**

+ 405 - 91
base-common/src/main/java/com/narutohuo/xindazhou/common/socketio/SocketIOManager.kt

@@ -1,144 +1,353 @@
 package com.narutohuo.xindazhou.common.socketio
 
 import android.app.Application
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.OnLifecycleEvent
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
 import com.narutohuo.xindazhou.common.auth.AuthManager
+import com.narutohuo.xindazhou.common.auth.utils.JWTUtil
 import com.narutohuo.xindazhou.common.config.ServerConfigManager
 import com.narutohuo.xindazhou.common.log.LogHelper
+import com.narutohuo.xindazhou.common.socketio.impl.SocketIOClient
+import com.narutohuo.xindazhou.common.socketio.model.SocketIOResponse
+import com.narutohuo.xindazhou.core.storage.StorageImpl
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.launch
 
 /**
- * SocketIO 连接管理器
+ * SocketIO 管理器(业务封装层)
  * 
- * 负责自动管理 SocketIO 连接的生命周期:
- * - App 进入前台时自动重连
- * - Token 过期时自动刷新并重连
- * - 监听应用生命周期
+ * 与 AuthManager、VersionUpdateManager 类似,提供统一的 Socket.IO 封装
+ * 自动处理连接、重连、Token 刷新等,外部只需要订阅即可
+ * 
+ * 职责:
+ * ✅ 自动处理连接(包括 Token 刷新)
+ * ✅ 自动处理重连
+ * ✅ 提供消息订阅接口
+ * ✅ 外部只需要订阅即可
  * 
  * 使用方式:
  * ```kotlin
- * // 在 Application.onCreate() 中初始化
- * SocketIOManager.init(application)
+ * // 在 AppInitializer 中初始化(可选,会自动连接)
+ * SocketIOManager.initialize(application)
+ * 
+ * // 在 ViewModel 中订阅消息
+ * viewModelScope.launch {
+ *     SocketIOManager.shared.subscribe("community_message").collect { response ->
+ *         // 处理社区消息
+ *     }
+ * }
  * ```
  */
-object SocketIOManager : LifecycleObserver {
+object SocketIOManager : DefaultLifecycleObserver {
     
     private const val TAG = "SocketIOManager"
     
-    private var application: Application? = null
+    // 单例实例
+    val shared = SocketIOManager
+    
+    private val socketClient = SocketIOClient.getInstance()
     private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
-    private var initialized = false
+    
+    private var application: Application? = null
+    private var serverURL: String? = null
+    private var isInitialized = false
+    
+    // 连接状态(使用 replay = 1 确保新订阅者能立即获取最新状态)
+    private val _connectionState = MutableSharedFlow<Boolean>(extraBufferCapacity = 1, replay = 1)
+    val connectionState: SharedFlow<Boolean> = _connectionState.asSharedFlow()
+    
+    init {
+        // 初始化连接状态(同步获取当前状态)
+        _connectionState.tryEmit(isConnected())
+        
+        // 订阅连接事件
+        subscribeConnectionEvents()
+    }
+    
+    // 消息订阅(使用 SharedFlow)
+    private val eventFlows = mutableMapOf<String, MutableSharedFlow<SocketIOResponse>>()
+    
+    // Token 刷新防抖
+    @Volatile
+    private var isRefreshingToken = false
+    
+    @Volatile
+    private var lastTokenRefreshTime = 0L
+    
+    // 重连防抖
+    @Volatile
+    private var isReconnecting = false
+    
+    @Volatile
+    private var lastReconnectTime = 0L
     
     /**
-     * 初始化 SocketIO 管理器
+     * 初始化并自动连接(如果用户已登录)
      * 
-     * 需要在 Application.onCreate() 中调用
-     * 会自动注册生命周期监听
+     * 在 AppInitializer 中调用一次即可
+     * 后续会自动处理连接、重连、Token 刷新等
      * 
-     * @param app Application 实例
+     * 注意:
+     * - 如果用户未登录,不会立即连接,等待登录成功后调用 ensureConnected()
+     * - 如果用户已登录,会自动连接
+     * 
+     * @param application Application 实例(用于生命周期监听)
      */
-    fun init(app: Application) {
-        if (initialized) {
-            LogHelper.w(TAG, "SocketIOManager 已初始化,跳过重复初始化")
+    fun initialize(application: Application) {
+        if (isInitialized) {
+            LogHelper.d(TAG, "已初始化,跳过")
             return
         }
         
-        application = app
-        initialized = true
+        this.application = application
+        isInitialized = true
         
-        // 注册 App 生命周期监听(使用反射,避免强依赖)
+        // 注册 App 生命周期监听
         try {
-            val processLifecycleOwnerClass = Class.forName("androidx.lifecycle.ProcessLifecycleOwner")
-            val getMethod = processLifecycleOwnerClass.getMethod("get")
-            val lifecycleOwner = getMethod.invoke(null)
-            val lifecycleField = lifecycleOwner.javaClass.getMethod("getLifecycle")
-            val lifecycle = lifecycleField.invoke(lifecycleOwner)
-            val addObserverMethod = lifecycle.javaClass.getMethod("addObserver", LifecycleObserver::class.java)
-            addObserverMethod.invoke(lifecycle, this)
-            LogHelper.d(TAG, "SocketIOManager 初始化完成")
+            ProcessLifecycleOwner.get().lifecycle.addObserver(this)
+            LogHelper.d(TAG, "生命周期监听已注册")
         } catch (e: Exception) {
             LogHelper.e(TAG, "注册生命周期监听失败", e)
         }
+        
+        // 如果用户已登录,自动连接
+        appScope.launch {
+            if (AuthManager.isLoggedIn()) {
+                LogHelper.d(TAG, "用户已登录,自动连接 Socket.IO...")
+                connect()
+            } else {
+                LogHelper.d(TAG, "用户未登录,等待登录成功后连接")
+                // 更新连接状态为 false(用户未登录)
+                _connectionState.emit(false)
+            }
+        }
+        
+        LogHelper.d(TAG, "SocketIOManager 初始化完成(自动处理连接和Token刷新)")
     }
     
     /**
-     * App 进入前台时调用
+     * 确保已连接(如果未连接则自动连接)
      * 
-     * 检查 SocketIO 连接状态,如果断开则自动重连
-     * 如果连接失败(可能是 Token 过期),会自动刷新 Token 后重连
+     * 在用户登录成功后调用,确保 Socket.IO 已连接
+     * 如果已连接,则不做任何操作
+     * 如果未连接,会自动处理连接(包括 Token 刷新)
+     * 
+     * 使用场景:
+     * - 用户登录成功后调用
+     * - 应用启动后检查连接状态时调用
+     * - 前台重连时调用
      */
-    @OnLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_START)
-    fun onAppForeground() {
-        LogHelper.d(TAG, "App 进入前台,检查 SocketIO 连接状态")
+    fun ensureConnected() {
+        appScope.launch {
+            // 检查是否已登录
+            if (!AuthManager.isLoggedIn()) {
+                LogHelper.d(TAG, "用户未登录,无法连接")
+                _connectionState.emit(false)
+                return@launch
+            }
+            
+            // 如果已连接,更新状态并返回
+            if (isConnected()) {
+                LogHelper.d(TAG, "已连接,状态正常")
+                _connectionState.emit(true)
+                return@launch
+            }
+            
+            // 未连接,尝试重连
+            LogHelper.d(TAG, "检测到未连接,开始重连...")
+            checkAndReconnect()
+        }
+    }
+    
+    /**
+     * 连接 Socket.IO(自动处理 Token 刷新)
+     */
+    private suspend fun connect() {
+        LogHelper.d(TAG, "=== Socket.IO 连接开始 ===")
         
-        // 检查是否已登录
-        if (!AuthManager.isLoggedIn()) {
-            LogHelper.d(TAG, "用户未登录,跳过 SocketIO 重连")
+        // 获取服务器地址
+        val url = serverURL ?: ServerConfigManager.getSocketIOUrl()
+        if (url.isEmpty()) {
+            LogHelper.e(TAG, "服务器地址未配置,无法连接")
+            _connectionState.emit(false)
             return
         }
         
-        // 检查连接状态,如果断开则重连
+        this.serverURL = url
+        
+        // 获取 Token,如果为空或过期则刷新
+        var token = StorageImpl.getString("access_token")
+        
+        // 如果 Token 为空,尝试刷新
+        if (token.isEmpty()) {
+            LogHelper.d(TAG, "Token 为空,尝试刷新...")
+            token = refreshTokenIfNeeded() ?: ""
+            if (token.isEmpty()) {
+                LogHelper.e(TAG, "Token 为空且刷新失败,无法连接")
+                _connectionState.emit(false)
+                return
+            }
+        }
+        
+        // 如果 Token 已过期,尝试刷新
+        if (!JWTUtil.isTokenValid(token)) {
+            LogHelper.w(TAG, "Token 已过期,尝试刷新...")
+            val expiresAt = JWTUtil.getExpiresAt(token)
+            if (expiresAt != null) {
+                val expiresDate = java.util.Date(expiresAt * 1000) // 转换为毫秒
+                LogHelper.d(TAG, "Token 过期时间: $expiresDate")
+            }
+            token = refreshTokenIfNeeded() ?: token // 如果刷新失败,仍然尝试使用旧 Token(由服务端判断)
+            
+            // 如果刷新后仍然无效,返回
+            if (token.isEmpty() || !JWTUtil.isTokenValid(token)) {
+                LogHelper.e(TAG, "Token 刷新失败或仍然无效,无法连接")
+                _connectionState.emit(false)
+                return
+            }
+            LogHelper.d(TAG, "Token 刷新成功,使用新 Token 连接...")
+        }
+        
+        LogHelper.d(TAG, "正在连接到服务器: $url")
+        socketClient.connect(url, token)
+    }
+    
+    /**
+     * 断开连接
+     */
+    fun disconnect() {
+        LogHelper.d(TAG, "断开 Socket.IO 连接")
+        socketClient.disconnect()
         appScope.launch {
-            try {
-                // 使用反射获取 SocketIORepository,避免强依赖
-                val factoryClass = Class.forName("com.narutohuo.xindazhou.socketio.factory.SocketIORepositoryFactory")
-                val getInstanceMethod = factoryClass.getMethod("getInstance")
-                val socketIORepository = getInstanceMethod.invoke(null)
-                
-                val socketUrl = ServerConfigManager.getSocketIOUrl()
-                var token = AuthManager.getAccessToken()
-                
-                if (token.isNullOrEmpty()) {
-                    LogHelper.w(TAG, "Token 为空,无法重连 SocketIO")
-                    return@launch
+            _connectionState.emit(false)
+        }
+    }
+    
+    /**
+     * 检查连接状态
+     */
+    fun isConnected(): Boolean {
+        return socketClient.isConnected()
+    }
+    
+    /**
+     * 自动重连(如果断开)
+     */
+    private suspend fun checkAndReconnect() {
+        if (isConnected()) {
+            return
+        }
+        
+        // 防重复重连(5秒内最多重连一次)
+        val currentTime = System.currentTimeMillis()
+        if (isReconnecting || (currentTime - lastReconnectTime < 5000)) {
+            LogHelper.d(TAG, "正在重连或刚重连过,跳过")
+            return
+        }
+        
+        isReconnecting = true
+        lastReconnectTime = currentTime
+        
+        try {
+            LogHelper.d(TAG, "检测到断开连接,开始重连...")
+            
+            // 如果未初始化,先初始化
+            if (!isInitialized) {
+                LogHelper.d(TAG, "检测到未初始化,先执行初始化...")
+                val app = application ?: run {
+                    LogHelper.e(TAG, "Application 未设置,无法初始化")
+                    return
                 }
-                
-                // 检查是否已连接
-                val isConnectedMethod = socketIORepository.javaClass.getMethod("isConnected")
-                val isConnected = isConnectedMethod.invoke(socketIORepository) as Boolean
-                
-                if (isConnected) {
-                    LogHelper.d(TAG, "SocketIO 已连接,无需重连")
-                    return@launch
+                initialize(app)
+                return
+            }
+            
+            // 刷新 Token 后重连
+            val newToken = refreshTokenIfNeeded()
+            if (!newToken.isNullOrEmpty()) {
+                LogHelper.d(TAG, "Token 已刷新,使用新 Token 重连...")
+                delay(1000) // 延迟1秒,避免过于频繁的重连尝试
+                connect()
+            } else {
+                LogHelper.e(TAG, "Token 刷新失败,无法重连")
+            }
+        } finally {
+            isReconnecting = false
+        }
+    }
+    
+    /**
+     * 订阅消息(返回 SharedFlow)
+     * 
+     * 各个业务模块使用此方法订阅需要的消息类型
+     * 
+     * 注意:
+     * - 如果未连接,会自动尝试连接
+     * - 如果用户未登录,会等待登录成功后连接
+     * 
+     * 示例:
+     * ```kotlin
+     * // CommunityViewModel.kt
+     * viewModelScope.launch {
+     *     SocketIOManager.shared.subscribe("community_message").collect { response ->
+     *         // 处理社区消息
+     *     }
+     * }
+     * ```
+     */
+    fun subscribe(eventName: String): SharedFlow<SocketIOResponse> {
+        return eventFlows.getOrPut(eventName) {
+            MutableSharedFlow<SocketIOResponse>(extraBufferCapacity = 1).also { flow ->
+                // 订阅 Socket.IO 事件
+                socketClient.on(eventName) { response ->
+                    appScope.launch {
+                        flow.emit(response)
+                    }
                 }
                 
-                // 先尝试连接
-                LogHelper.d(TAG, "SocketIO 未连接,开始重连...")
-                val checkAndReconnectMethod = socketIORepository.javaClass.getMethod(
-                    "checkAndReconnect",
-                    String::class.java,
-                    String::class.java
-                )
-                checkAndReconnectMethod.invoke(socketIORepository, socketUrl, token)
-                
-                // 等待 2 秒,检查连接是否成功
-                delay(2000)
-                
-                // 如果连接仍然失败,可能是 Token 过期,尝试刷新 Token 后重连
-                val isConnectedAfter = isConnectedMethod.invoke(socketIORepository) as Boolean
-                if (!isConnectedAfter) {
-                    LogHelper.d(TAG, "连接失败,可能是 Token 过期,尝试刷新 Token 后重连")
-                    val refreshedToken = AuthManager.refreshTokenIfNeeded()
-                    if (!refreshedToken.isNullOrEmpty() && refreshedToken != token) {
-                        LogHelper.d(TAG, "Token 已刷新,使用新 Token 重连")
-                        checkAndReconnectMethod.invoke(socketIORepository, socketUrl, refreshedToken)
-                    } else {
-                        LogHelper.w(TAG, "Token 刷新失败或未刷新,无法重连")
-                    }
-                } else {
-                    LogHelper.d(TAG, "SocketIO 重连成功")
+                // 如果未连接,尝试连接(使用 ensureConnected 统一处理)
+                if (!isConnected()) {
+                    ensureConnected()
                 }
-            } catch (e: ClassNotFoundException) {
-                LogHelper.d(TAG, "SocketIO 模块未引入,跳过重连")
-            } catch (e: Exception) {
-                LogHelper.e(TAG, "SocketIO 重连失败", e)
             }
+        }.asSharedFlow()
+    }
+    
+    /**
+     * 发送消息
+     */
+    fun emit(eventName: String, data: Any) {
+        if (!isConnected()) {
+            LogHelper.w(TAG, "Socket.IO 未连接,无法发送事件: $eventName,尝试重新连接...")
+            appScope.launch {
+                checkAndReconnect()
+            }
+            return
         }
+        
+        socketClient.emit(eventName, data)
+    }
+    
+    // MARK: - 生命周期监听
+    
+    /**
+     * App 进入前台时调用
+     * 
+     * 检查 SocketIO 连接状态,如果断开则自动重连
+     * 确保应用回到前台时,Socket.IO 连接状态正常
+     */
+    override fun onStart(owner: LifecycleOwner) {
+        LogHelper.d(TAG, "App 进入前台,检查 SocketIO 连接状态")
+        
+        // 使用 ensureConnected() 统一处理连接逻辑
+        ensureConnected()
     }
     
     /**
@@ -149,10 +358,115 @@ object SocketIOManager : LifecycleObserver {
      * 2. 用户可能需要在后台接收消息
      * 3. 系统会在内存不足时自动清理
      */
-    @OnLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_STOP)
-    fun onAppBackground() {
+    override fun onStop(owner: LifecycleOwner) {
         LogHelper.d(TAG, "App 进入后台")
         // 不断开连接,保持连接以便接收消息
     }
+    
+    // MARK: - Private Methods
+    
+    /**
+     * 订阅连接事件
+     */
+    private fun subscribeConnectionEvents() {
+        socketClient.on("connect") { response ->
+            appScope.launch {
+                _connectionState.emit(true)
+                LogHelper.d(TAG, "✅ Socket.IO 连接成功")
+            }
+        }
+        
+        socketClient.on("disconnect") { response ->
+            appScope.launch {
+                _connectionState.emit(false)
+                LogHelper.d(TAG, "❌ Socket.IO 断开连接: ${response.data}")
+                
+                // 自动重连(延迟2秒)
+                delay(2000)
+                checkAndReconnect()
+            }
+        }
+        
+        socketClient.on("connect_error") { response ->
+            appScope.launch {
+                _connectionState.emit(false)
+                val errorMsg = response.data
+                LogHelper.e(TAG, "❌ Socket.IO 连接错误: $errorMsg")
+                
+                // 如果是 Token 错误,刷新后重连
+                if (errorMsg.contains("token", ignoreCase = true) || 
+                    errorMsg.contains("unauthorized", ignoreCase = true)) {
+                    refreshTokenAndReconnect()
+                } else {
+                    // 其他错误也尝试重连
+                    delay(2000)
+                    checkAndReconnect()
+                }
+            }
+        }
+    }
+    
+    /**
+     * 刷新 Token 并重连
+     */
+    private suspend fun refreshTokenAndReconnect() {
+        val newToken = refreshTokenIfNeeded()
+        if (!newToken.isNullOrEmpty()) {
+            LogHelper.d(TAG, "Token 已刷新,使用新 Token 重连...")
+            delay(1000) // 延迟1秒
+            connect()
+        } else {
+            LogHelper.e(TAG, "Token 刷新失败,无法重连")
+        }
+    }
+    
+    /**
+     * 刷新 Token(如果需要)
+     * 
+     * 使用 AuthManager 处理 Token 刷新
+     */
+    private suspend fun refreshTokenIfNeeded(): String? {
+        // 防止短时间内多次刷新(5秒内最多刷新一次)
+        val currentTime = System.currentTimeMillis()
+        if (isRefreshingToken) {
+            LogHelper.d(TAG, "正在刷新 Token,等待...")
+            // 等待刷新完成,最多等待3秒
+            var waitCount = 0
+            while (isRefreshingToken && waitCount < 30) {
+                delay(100) // 0.1秒
+                waitCount++
+            }
+            // 刷新完成后,从存储中获取最新的 Token
+            return StorageImpl.getString("access_token").takeIf { it.isNotEmpty() }
+        }
+        
+        if (currentTime - lastTokenRefreshTime < 5000) {
+            LogHelper.d(TAG, "刚刷新过 Token(${(currentTime - lastTokenRefreshTime) / 1000}秒前),使用存储中的 Token")
+            return StorageImpl.getString("access_token").takeIf { it.isNotEmpty() }
+        }
+        
+        return try {
+            isRefreshingToken = true
+            lastTokenRefreshTime = currentTime
+            
+            LogHelper.d(TAG, "开始刷新 Token...")
+            
+            // 使用 base-common 的 AuthManager
+            val newToken = AuthManager.refreshTokenIfNeeded()
+            
+            if (!newToken.isNullOrEmpty()) {
+                LogHelper.d(TAG, "Token 刷新成功")
+                newToken
+            } else {
+                LogHelper.w(TAG, "Token 刷新失败(返回 null)")
+                null
+            }
+        } catch (e: Exception) {
+            LogHelper.e(TAG, "Token 刷新异常", e)
+            null
+        } finally {
+            isRefreshingToken = false
+        }
+    }
 }
 

+ 243 - 0
base-common/src/main/java/com/narutohuo/xindazhou/common/socketio/impl/SocketIOClient.kt

@@ -0,0 +1,243 @@
+package com.narutohuo.xindazhou.common.socketio.impl
+
+import com.google.gson.Gson
+import com.narutohuo.xindazhou.common.log.LogHelper
+import com.narutohuo.xindazhou.common.socketio.model.SocketIOResponse
+import io.socket.client.IO
+import io.socket.client.Socket
+import org.json.JSONObject
+
+/**
+ * SocketIO 客户端(单例模式)
+ * 
+ * Socket.IO 连接是全局唯一的,多个界面需要共享同一个连接
+ * 这是底层 Socket.IO 客户端的封装,由 SocketIOManager 使用
+ */
+internal class SocketIOClient private constructor() {
+    
+    companion object {
+        @Volatile
+        private var INSTANCE: SocketIOClient? = null
+        
+        /**
+         * 获取 SocketIO 客户端单例
+         */
+        @JvmStatic
+        internal fun getInstance(): SocketIOClient {
+            return INSTANCE ?: synchronized(this) {
+                INSTANCE ?: SocketIOClient().also { INSTANCE = it }
+            }
+        }
+        
+        /**
+         * 销毁单例(用于测试或需要重新初始化时)
+         */
+        @JvmStatic
+        internal fun destroyInstance() {
+            INSTANCE?.disconnect()
+            INSTANCE = null
+        }
+    }
+    
+    private var socket: Socket? = null
+    // 存储事件监听器,以便在 socket 创建后注册
+    private val eventListeners = mutableMapOf<String, MutableList<(SocketIOResponse) -> Unit>>()
+    
+    fun connect(serverUrl: String, token: String) {
+        try {
+            LogHelper.d("SocketIOClient", "=== SocketIO 连接开始 ===")
+            LogHelper.d("SocketIOClient", "服务器地址: $serverUrl")
+            LogHelper.d("SocketIOClient", "Token 长度: ${token.length}")
+            
+            // 如果已连接,先断开
+            if (socket?.connected() == true) {
+                LogHelper.d("SocketIOClient", "检测到已有连接,先断开")
+                disconnect()
+            }
+            
+            // 初始化 Socket 连接
+            val options = IO.Options().apply {
+                // 添加认证 token(通过 HTTP Headers)
+                extraHeaders = mapOf("Authorization" to listOf("Bearer $token"))
+                // 添加 token 作为查询参数(备用方案)
+                query = "token=$token&EIO=1" // 使用 Engine.IO v1 以匹配服务器
+                // 禁用自动重连,改为手动重连(原因:自动重连无法更新 Token)
+                reconnection = false
+            }
+            
+            LogHelper.d("SocketIOClient", "连接配置: Authorization=Bearer $token, query=token=$token&EIO=1")
+            
+            LogHelper.d("SocketIOClient", "创建 Socket 实例...")
+            socket = IO.socket(serverUrl, options)
+            
+            // 预先注册所有已保存的事件监听器(连接前注册)
+            LogHelper.d("SocketIOClient", "connect: 预先注册 ${eventListeners.size} 个事件类型的监听器...")
+            if (eventListeners.isEmpty()) {
+                LogHelper.w("SocketIOClient", "⚠️ 警告:eventListeners 为空,没有监听器需要预注册!")
+            } else {
+                registerAllEventListeners()
+            }
+            
+            // 添加连接事件监听
+            socket?.on(Socket.EVENT_CONNECT) {
+                LogHelper.d("SocketIOClient", "✅ SocketIO 连接成功")
+                
+                // 关键:连接成功后,再次注册所有已保存的事件监听器
+                // 这样可以确保即使监听器是在连接之后才注册的,也能正确工作
+                LogHelper.d("SocketIOClient", "连接成功:再次注册所有事件监听器(${eventListeners.size} 个事件类型)...")
+                registerAllEventListeners()
+                
+                triggerEvent("connect", "{}")
+            }
+            
+            socket?.on(Socket.EVENT_DISCONNECT) {
+                LogHelper.d("SocketIOClient", "❌ SocketIO 断开连接")
+                triggerEvent("disconnect", "{}")
+            }
+            
+            socket?.on(Socket.EVENT_CONNECT_ERROR) { args ->
+                val errorMsg = args?.getOrNull(0)?.toString() ?: "连接错误"
+                LogHelper.e("SocketIOClient", "❌ SocketIO 连接错误: $errorMsg")
+                triggerEvent("connect_error", errorMsg)
+            }
+            
+            LogHelper.d("SocketIOClient", "开始连接 SocketIO 服务器...")
+            socket?.connect()
+            LogHelper.d("SocketIOClient", "connect() 方法调用完成")
+        } catch (e: Exception) {
+            LogHelper.e("SocketIOClient", "❌ SocketIO 连接异常", e)
+        }
+    }
+    
+    fun disconnect() {
+        socket?.disconnect()
+        socket = null
+        // 注意:不清除 eventListeners,这样重新连接时可以自动注册
+        LogHelper.d("SocketIOClient", "已断开 SocketIO 连接")
+    }
+    
+    fun isConnected(): Boolean {
+        return socket?.connected() == true
+    }
+    
+    fun on(event: String, callback: (SocketIOResponse) -> Unit) {
+        // 保存事件监听器
+        eventListeners.getOrPut(event) { mutableListOf() }.add(callback)
+        LogHelper.d("SocketIOClient", "on: 保存事件监听器: $event (当前该事件有 ${eventListeners[event]?.size} 个监听器)")
+        
+        // 如果 socket 已连接,立即注册该事件的所有监听器(重新注册所有,确保不丢失)
+        if (socket != null && socket!!.connected()) {
+            LogHelper.d("SocketIOClient", "on: Socket 已连接,重新注册该事件的所有监听器: $event")
+            // 先移除该事件的所有旧监听器,然后重新注册所有 callbacks
+            socket?.off(event)
+            val callbacks = eventListeners[event] ?: return
+            callbacks.forEach { cb ->
+                registerEventListener(event, cb)
+            }
+        } else {
+            LogHelper.d("SocketIOClient", "on: Socket 未连接,监听器已保存,将在连接成功后自动注册")
+        }
+    }
+    
+    fun off(event: String) {
+        socket?.off(event)
+        eventListeners.remove(event)
+    }
+    
+    fun emit(event: String, data: Any) {
+        try {
+            val jsonData = when (data) {
+                is String -> data
+                is JSONObject -> data
+                is Map<*, *> -> {
+                    // 将 Kotlin Map 转换为 JSONObject
+                    JSONObject().apply {
+                        @Suppress("UNCHECKED_CAST")
+                        (data as Map<String, Any>).forEach { (key, value) ->
+                            put(key, value)
+                        }
+                    }
+                }
+                else -> {
+                    // 其他类型先转为 JSON 字符串,再转为 JSONObject
+                    JSONObject(Gson().toJson(data))
+                }
+            }
+            
+            socket?.emit(event, jsonData)
+            LogHelper.d("SocketIOClient", "发送事件: $event")
+        } catch (e: Exception) {
+            LogHelper.e("SocketIOClient", "发送事件失败: $event", e)
+        }
+    }
+    
+    // ========== 内部方法 ==========
+    
+    /**
+     * 注册所有已保存的事件监听器
+     * 
+     * 参考 iOS 实现:不先 off,直接为每个 callback 注册监听器
+     */
+    private fun registerAllEventListeners() {
+        if (socket == null) {
+            LogHelper.w("SocketIOClient", "registerAllEventListeners: Socket 为 null,无法注册监听器")
+            return
+        }
+        LogHelper.d("SocketIOClient", "registerAllEventListeners: 开始注册 ${eventListeners.size} 个事件类型的监听器...")
+        eventListeners.forEach { (event, callbacks) ->
+            LogHelper.d("SocketIOClient", "registerAllEventListeners: 注册事件监听器: $event (${callbacks.size} 个回调)")
+            // 参考 iOS:不先 off,直接为每个 callback 注册监听器
+            // 这样每个 callback 都会收到事件,不会互相覆盖
+            callbacks.forEach { callback ->
+                registerEventListener(event, callback)
+            }
+        }
+        LogHelper.d("SocketIOClient", "registerAllEventListeners: 所有事件监听器注册完成")
+    }
+    
+    /**
+     * 注册单个事件监听器
+     */
+    private fun registerEventListener(event: String, callback: (SocketIOResponse) -> Unit) {
+        socket?.on(event) { args ->
+            try {
+                // Socket.IO Java 客户端库接收数据时,服务端发送的 Map 会被自动序列化为 JSONObject
+                // 统一转换为 JSON 字符串格式
+                val data = if (args.isNotEmpty()) {
+                    when (val arg = args[0]) {
+                        is String -> arg
+                        is JSONObject -> arg.toString()
+                        else -> Gson().toJson(arg)
+                    }
+                } else {
+                    "{}"
+                }
+                
+                val response = SocketIOResponse(
+                    event = event,
+                    data = data,
+                    timestamp = System.currentTimeMillis()
+                )
+                
+                LogHelper.d("SocketIOClient", "收到事件: $event, data=${response.data}")
+                callback(response)
+            } catch (e: Exception) {
+                LogHelper.e("SocketIOClient", "处理事件失败: $event", e)
+            }
+        }
+    }
+    
+    /**
+     * 触发事件(用于连接/断开等内置事件)
+     */
+    private fun triggerEvent(event: String, data: String) {
+        val callbacks = eventListeners[event] ?: return
+        val response = SocketIOResponse(
+            event = event,
+            data = data,
+            timestamp = System.currentTimeMillis()
+        )
+        callbacks.forEach { it(response) }
+    }
+}
+

+ 40 - 0
base-common/src/main/java/com/narutohuo/xindazhou/common/socketio/model/SocketIOEvent.kt

@@ -0,0 +1,40 @@
+package com.narutohuo.xindazhou.common.socketio.model
+
+/**
+ * SocketIO 事件类型常量
+ * 
+ * 定义 SocketIO 通信中使用的事件名称
+ * 建议使用这些常量而不是硬编码字符串
+ */
+object SocketIOEvent {
+    // ========== 连接事件(SocketIO 内置事件) ==========
+    /** 连接成功 */
+    const val CONNECT = "connect"
+    /** 断开连接 */
+    const val DISCONNECT = "disconnect"
+    /** 连接错误 */
+    const val CONNECT_ERROR = "connect_error"
+    
+    // ========== 车辆相关事件(业务事件) ==========
+    /** 车辆状态更新 */
+    const val VEHICLE_STATUS = "vehicle_status"
+    /** 车辆控制指令 */
+    const val VEHICLE_CONTROL = "vehicle_control"
+    /** 车辆控制响应 */
+    const val VEHICLE_CONTROL_RESPONSE = "vehicle_control_response"
+    /** 车辆报警 */
+    const val VEHICLE_ALARM = "vehicle_alarm"
+    /** 车辆位置更新 */
+    const val VEHICLE_LOCATION = "vehicle_location"
+    
+    // ========== 消息相关事件(业务事件) ==========
+    /** 普通消息 */
+    const val MESSAGE = "message"
+    /** 站内信 */
+    const val INBOX_MESSAGE = "inbox_message"
+    /** 系统消息 */
+    const val SYSTEM_MESSAGE = "system_message"
+    /** 社区消息 */
+    const val COMMUNITY_MESSAGE = "community_message"
+}
+

+ 17 - 0
base-common/src/main/java/com/narutohuo/xindazhou/common/socketio/model/SocketIOResponse.kt

@@ -0,0 +1,17 @@
+package com.narutohuo.xindazhou.common.socketio.model
+
+/**
+ * SocketIO 响应模型
+ * 
+ * 用于封装 SocketIO 服务器推送的实时数据
+ * 
+ * @param event 事件名称(如 "vehicle_status", "vehicle_control" 等)
+ * @param data 数据内容(JSON 字符串格式)
+ * @param timestamp 时间戳(接收时间)
+ */
+data class SocketIOResponse(
+    val event: String,
+    val data: String,
+    val timestamp: Long = System.currentTimeMillis()
+)
+

+ 27 - 100
base-common/src/main/java/com/narutohuo/xindazhou/common/storage/StorageManager.kt

@@ -1,181 +1,108 @@
 package com.narutohuo.xindazhou.common.storage
 
-import com.alibaba.android.arouter.launcher.ARouter
-import com.narutohuo.xindazhou.core.log.ILog
-import com.narutohuo.xindazhou.core.storage.IStorage
+import com.narutohuo.xindazhou.core.storage.StorageImpl
 
 /**
  * 存储管理器
  * 
  * 业务层封装,提供便捷的存储管理方式
- * 内部使用 IStorage 接口(由 app 模块实现)
- * 
- * 使用方式:
- * ```kotlin
- * // 键值存储
- * StorageManager.putString("user_name", "张三")
- * val userName = StorageManager.getString("user_name", "")
- * StorageManager.putInt("user_age", 25)
- * val userAge = StorageManager.getInt("user_age", 0)
- * 
- * // 文件存储
- * StorageManager.saveFile("/path/to/file.txt", data)
- * val fileData = StorageManager.readFile("/path/to/file.txt")
- * StorageManager.deleteFile("/path/to/file.txt")
- * 
- * // 清除数据
- * StorageManager.remove("user_name")
- * StorageManager.clear()
- * ```
+ * 底层使用 base-core 的 StorageImpl 统一存储
  */
 object StorageManager {
     
-    private const val TAG = "StorageManager"
-    
-    /**
-     * 获取 IStorage 实现实例
-     * 通过 ARouter 获取,如果未注册则返回 null
-     */
-    private fun getStorage(): IStorage? {
-        return try {
-            ARouter.getInstance().navigation(IStorage::class.java)
-        } catch (e: Exception) {
-            ILog.w(TAG, "IStorage 未注册,请确保 app 模块已实现并注册 IStorage")
-            null
-        }
-    }
-    
     // ========== 键值存储 ==========
     
     /**
      * 保存字符串
-     * 
-     * @param key 键
-     * @param value 值
      */
     fun putString(key: String, value: String) {
-        getStorage()?.putString(key, value) ?: run {
-            ILog.w(TAG, "putString 失败:IStorage 未注册")
-        }
+        StorageImpl.putString(key, value)
     }
     
     /**
      * 获取字符串
-     * 
-     * @param key 键
-     * @param defaultValue 默认值
-     * @return 值
      */
     fun getString(key: String, defaultValue: String = ""): String {
-        return getStorage()?.getString(key, defaultValue) ?: defaultValue
+        return StorageImpl.getString(key, defaultValue)
     }
     
     /**
      * 保存整数
-     * 
-     * @param key 键
-     * @param value 值
      */
     fun putInt(key: String, value: Int) {
-        getStorage()?.putInt(key, value) ?: run {
-            ILog.w(TAG, "putInt 失败:IStorage 未注册")
-        }
+        StorageImpl.putInt(key, value)
     }
     
     /**
      * 获取整数
-     * 
-     * @param key 键
-     * @param defaultValue 默认值
-     * @return 值
      */
     fun getInt(key: String, defaultValue: Int = 0): Int {
-        return getStorage()?.getInt(key, defaultValue) ?: defaultValue
+        return StorageImpl.getInt(key, defaultValue)
+    }
+    
+    /**
+     * 保存长整数
+     */
+    fun putLong(key: String, value: Long) {
+        StorageImpl.putLong(key, value)
+    }
+    
+    /**
+     * 获取长整数
+     */
+    fun getLong(key: String, defaultValue: Long = 0L): Long {
+        return StorageImpl.getLong(key, defaultValue)
     }
     
     /**
      * 保存布尔值
-     * 
-     * @param key 键
-     * @param value 值
      */
     fun putBoolean(key: String, value: Boolean) {
-        getStorage()?.putBoolean(key, value) ?: run {
-            ILog.w(TAG, "putBoolean 失败:IStorage 未注册")
-        }
+        StorageImpl.putBoolean(key, value)
     }
     
     /**
      * 获取布尔值
-     * 
-     * @param key 键
-     * @param defaultValue 默认值
-     * @return 值
      */
     fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
-        return getStorage()?.getBoolean(key, defaultValue) ?: defaultValue
+        return StorageImpl.getBoolean(key, defaultValue)
     }
     
     /**
      * 删除指定键
-     * 
-     * @param key 键
      */
     fun remove(key: String) {
-        getStorage()?.remove(key) ?: run {
-            ILog.w(TAG, "remove 失败:IStorage 未注册")
-        }
+        StorageImpl.remove(key)
     }
     
     /**
      * 清除所有数据
      */
     fun clear() {
-        getStorage()?.clear() ?: run {
-            ILog.w(TAG, "clear 失败:IStorage 未注册")
-        }
+        StorageImpl.clear()
     }
     
     // ========== 文件存储 ==========
     
     /**
      * 保存文件
-     * 
-     * @param path 文件路径
-     * @param data 文件数据
-     * @return true 表示保存成功
      */
     fun saveFile(path: String, data: ByteArray): Boolean {
-        return getStorage()?.saveFile(path, data) ?: run {
-            ILog.w(TAG, "saveFile 失败:IStorage 未注册")
-            false
-        }
+        return StorageImpl.saveFile(path, data)
     }
     
     /**
      * 读取文件
-     * 
-     * @param path 文件路径
-     * @return 文件数据,如果文件不存在或读取失败返回 null
      */
     fun readFile(path: String): ByteArray? {
-        return getStorage()?.readFile(path) ?: run {
-            ILog.w(TAG, "readFile 失败:IStorage 未注册")
-            null
-        }
+        return StorageImpl.readFile(path)
     }
     
     /**
      * 删除文件
-     * 
-     * @param path 文件路径
-     * @return true 表示删除成功
      */
     fun deleteFile(path: String): Boolean {
-        return getStorage()?.deleteFile(path) ?: run {
-            ILog.w(TAG, "deleteFile 失败:IStorage 未注册")
-            false
-        }
+        return StorageImpl.deleteFile(path)
     }
 }
 

+ 7 - 0
base-common/src/main/java/com/narutohuo/xindazhou/common/ui/StatusBarHelper.kt

@@ -36,6 +36,7 @@ object StatusBarHelper {
      * @param activity Activity 实例
      * @param color 颜色值(如 Color.BLUE 或 0xFF000000.toInt())
      */
+    @Suppress("DEPRECATION")
     fun setStatusBarColor(activity: Activity, color: Int) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
             activity.window.statusBarColor = color
@@ -48,6 +49,7 @@ object StatusBarHelper {
      * @param activity Activity 实例
      * @param isLight true 表示浅色文字(深色背景),false 表示深色文字(浅色背景)
      */
+    @Suppress("DEPRECATION")
     fun setStatusBarLightMode(activity: Activity, isLight: Boolean) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
             // Android 11+ 使用 WindowInsetsController
@@ -73,6 +75,7 @@ object StatusBarHelper {
      * @param activity Activity 实例
      * @param isLight 状态栏文字是否为浅色(默认 false,深色文字)
      */
+    @Suppress("DEPRECATION")
     fun setImmersiveStatusBar(activity: Activity, isLight: Boolean = false) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
             val window = activity.window
@@ -91,6 +94,7 @@ object StatusBarHelper {
      * @param activity Activity 实例
      * @param color 颜色值
      */
+    @Suppress("DEPRECATION")
     fun setNavigationBarColor(activity: Activity, color: Int) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
             activity.window.navigationBarColor = color
@@ -103,6 +107,7 @@ object StatusBarHelper {
      * @param activity Activity 实例
      * @param isLight true 表示浅色文字,false 表示深色文字
      */
+    @Suppress("DEPRECATION")
     fun setNavigationBarLightMode(activity: Activity, isLight: Boolean) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
             val window = activity.window
@@ -125,6 +130,7 @@ object StatusBarHelper {
      * 
      * @param activity Activity 实例
      */
+    @Suppress("DEPRECATION")
     fun setFullScreen(activity: Activity) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
             val window = activity.window
@@ -147,6 +153,7 @@ object StatusBarHelper {
      * 
      * @param activity Activity 实例
      */
+    @Suppress("DEPRECATION")
     fun exitFullScreen(activity: Activity) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
             val window = activity.window

+ 0 - 44
base-common/src/main/java/com/narutohuo/xindazhou/common/version/VersionCheckActivity.kt

@@ -1,44 +0,0 @@
-package com.narutohuo.xindazhou.common.version
-
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import com.narutohuo.xindazhou.common.ui.ActivityManager
-
-/**
- * 版本检查 Activity(通用)
- * 
- * 职责:
- * 1. 启动时自动检查版本更新
- * 2. 显示更新对话框
- * 3. 处理强制更新
- * 
- * 使用方式:
- * 在 app 模块中继承此类,或直接使用
- * 
- * 注意:版本检查逻辑已封装在 VersionUpdateManager 中
- */
-open class VersionCheckActivity : AppCompatActivity() {
-    
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        
-        // 注册到 ActivityManager
-        ActivityManager.addActivity(this)
-        
-        // 自动检查版本更新
-        VersionUpdateManager.checkUpdate(
-            activity = this,
-            onForceUpdateDismiss = {
-                // 强制更新时,关闭对话框后退出应用
-                finish()
-            }
-        )
-    }
-    
-    override fun onDestroy() {
-        super.onDestroy()
-        // 从 ActivityManager 中移除
-        ActivityManager.removeActivity(this)
-    }
-}
-

+ 9 - 15
base-common/src/main/java/com/narutohuo/xindazhou/common/version/VersionUpdateManager.kt

@@ -3,12 +3,12 @@ package com.narutohuo.xindazhou.common.version
 import android.app.Activity
 import android.content.Context
 import android.content.pm.PackageManager
-import android.os.Build
+import androidx.core.content.pm.PackageInfoCompat
 import androidx.fragment.app.FragmentActivity
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import com.narutohuo.xindazhou.common.log.LogHelper
-import com.narutohuo.xindazhou.common.network.ApiServiceFactory
+import com.narutohuo.xindazhou.common.network.ApiManager
 import com.narutohuo.xindazhou.common.version.datasource.remote.VersionRemoteDataSourceImpl
 import com.narutohuo.xindazhou.common.version.repository.VersionRepository
 import com.narutohuo.xindazhou.common.version.ui.UpdateDialogFragment
@@ -48,11 +48,11 @@ object VersionUpdateManager {
      * @param platform 平台类型(默认 1-安卓)
      */
     fun init(context: Context, platform: Int = PLATFORM_ANDROID) {
-        // 初始化 ApiServiceFactory 的 baseUrlProvider(如果还未设置)
-        if (ApiServiceFactory.baseUrlProvider == null) {
+        // 初始化 ApiManager 的 baseUrlProvider(如果还未设置)
+        if (ApiManager.baseUrlProvider == null) {
             // 这里需要从 ServerConfigManager 获取,但为了避免循环依赖,
-            // 建议在 Application 中先设置 ApiServiceFactory.baseUrlProvider
-            LogHelper.w("VersionUpdateManager", "ApiServiceFactory.baseUrlProvider 未设置,版本检查可能失败")
+            // 建议在 Application 中先设置 ApiManager.baseUrlProvider
+            LogHelper.w("VersionUpdateManager", "ApiManager.baseUrlProvider 未设置,版本检查可能失败")
         }
     }
     
@@ -181,15 +181,9 @@ object VersionUpdateManager {
     fun getCurrentVersionCode(context: Context): Int {
         return try {
             val packageManager = context.packageManager
-            val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
-                // API 28+ 使用 longVersionCode
-                packageManager.getPackageInfo(context.packageName, 0).longVersionCode.toInt()
-            } else {
-                // API 26-27 使用 versionCode
-                @Suppress("DEPRECATION")
-                packageManager.getPackageInfo(context.packageName, 0).versionCode
-            }
-            packageInfo
+            val packageInfo = packageManager.getPackageInfo(context.packageName, 0)
+            // 使用 PackageInfoCompat 统一处理,兼容所有版本
+            PackageInfoCompat.getLongVersionCode(packageInfo).toInt()
         } catch (e: PackageManager.NameNotFoundException) {
             // 记录异常
             LogHelper.e("VersionUpdateManager", "无法获取版本号", e)

+ 1 - 1
base-common/src/main/java/com/narutohuo/xindazhou/common/version/datasource/remote/VersionRemoteDataSource.kt

@@ -28,7 +28,7 @@ interface VersionRemoteDataSource {
 class VersionRemoteDataSourceImpl : ApiBaseRemoteDataSource(), VersionRemoteDataSource {
     
     private val versionApi: VersionApi by lazy {
-        com.narutohuo.xindazhou.common.network.ApiServiceFactory.create<VersionApi>()
+        com.narutohuo.xindazhou.common.network.ApiManager.create<VersionApi>()
     }
     
     override suspend fun getLatestVersion(

+ 20 - 0
base-common/src/main/res/drawable/tab_indicator.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true">
+        <shape android:shape="oval">
+            <solid android:color="@android:color/black" />
+            <size
+                android:width="8dp"
+                android:height="8dp" />
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="oval">
+            <solid android:color="@android:color/darker_gray" />
+            <size
+                android:width="8dp"
+                android:height="8dp" />
+        </shape>
+    </item>
+</selector>
+

+ 68 - 0
base-common/src/main/res/layout/fragment_onboarding.xml

@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/white">
+
+    <!-- ViewPager2 显示引导页 -->
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/viewPager"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toTopOf="@id/indicatorContainer"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <!-- 指示器和按钮容器 -->
+    <LinearLayout
+        android:id="@+id/indicatorContainer"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:padding="24dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent">
+
+        <!-- 页面指示器(圆点) -->
+        <com.google.android.material.tabs.TabLayout
+            android:id="@+id/tabIndicator"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginBottom="24dp"
+            app:tabBackground="@drawable/tab_indicator"
+            app:tabGravity="center"
+            app:tabIndicatorHeight="0dp"
+            app:tabPaddingEnd="8dp"
+            app:tabPaddingStart="8dp"
+            tools:visibility="visible" />
+
+        <!-- "立即体验" 按钮 -->
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/btnStart"
+            android:layout_width="match_parent"
+            android:layout_height="56dp"
+            android:text="立即体验"
+            android:textSize="16sp"
+            android:visibility="gone" />
+
+        <!-- "跳过" 按钮 -->
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/btnSkip"
+            style="@style/Widget.MaterialComponents.Button.TextButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="end"
+            android:layout_marginTop="8dp"
+            android:text="跳过"
+            android:textSize="14sp"
+            android:visibility="visible" />
+
+    </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
+

+ 59 - 0
base-common/src/main/res/layout/item_onboarding.xml

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/white"
+    android:padding="24dp">
+
+    <!-- 引导图片 -->
+    <ImageView
+        android:id="@+id/ivImage"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="80dp"
+        android:scaleType="fitCenter"
+        android:contentDescription="引导页图片"
+        app:layout_constraintBottom_toTopOf="@id/tvTitle"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_chainStyle="packed"
+        tools:src="@android:drawable/ic_menu_gallery" />
+
+    <!-- 标题 -->
+    <TextView
+        android:id="@+id/tvTitle"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="32dp"
+        android:gravity="center"
+        android:textColor="@android:color/black"
+        android:textSize="24sp"
+        android:textStyle="bold"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/ivImage"
+        tools:text="欢迎使用新大洲本田" />
+
+    <!-- 描述 -->
+    <TextView
+        android:id="@+id/tvDescription"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:layout_marginStart="32dp"
+        android:layout_marginEnd="32dp"
+        android:gravity="center"
+        android:lineSpacingExtra="4dp"
+        android:textColor="@android:color/darker_gray"
+        android:textSize="16sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tvTitle"
+        app:layout_constraintBottom_toBottomOf="parent"
+        tools:text="这里有精彩的功能介绍,让您快速了解应用的使用方法" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
+

+ 4 - 1
base-core/src/main/java/com/narutohuo/xindazhou/core/storage/IStorage.kt

@@ -4,7 +4,8 @@ package com.narutohuo.xindazhou.core.storage
  * 存储服务接口
  * 统一封装键值存储、文件存储、数据库存储
  * 
- * 所有SDK模块通过此接口进行数据存储,实现类由app模块提供
+ * 实现类:StorageImpl(在 base-core 中提供)
+ * 所有模块都可以直接使用 StorageImpl 进行数据存储
  */
 interface IStorage {
     /**
@@ -14,6 +15,8 @@ interface IStorage {
     fun getString(key: String, defaultValue: String = ""): String
     fun putInt(key: String, value: Int)
     fun getInt(key: String, defaultValue: Int = 0): Int
+    fun putLong(key: String, value: Long)
+    fun getLong(key: String, defaultValue: Long = 0L): Long
     fun putBoolean(key: String, value: Boolean)
     fun getBoolean(key: String, defaultValue: Boolean = false): Boolean
     fun remove(key: String)

+ 124 - 0
base-core/src/main/java/com/narutohuo/xindazhou/core/storage/StorageImpl.kt

@@ -0,0 +1,124 @@
+package com.narutohuo.xindazhou.core.storage
+
+import android.content.Context
+import android.content.SharedPreferences
+import java.io.File
+
+/**
+ * 存储服务实现类(基于 SharedPreferences)
+ * 
+ * 统一提供键值存储和文件存储功能
+ * 单例模式,需要在 Application 中初始化
+ * 
+ * 使用方式:
+ * ```kotlin
+ * // 在 Application 中初始化
+ * StorageImpl.init(context)
+ * 
+ * // 使用存储
+ * StorageImpl.putString("key", "value")
+ * val value = StorageImpl.getString("key")
+ * ```
+ */
+object StorageImpl : IStorage {
+    
+    private const val PREFS_NAME = "xdz_storage_prefs"
+    private var prefs: SharedPreferences? = null
+    private var context: Context? = null
+    private var initialized = false
+    
+    /**
+     * 初始化存储服务
+     * 
+     * 需要在 Application.onCreate() 中调用
+     * 
+     * @param ctx 应用上下文
+     */
+    fun init(ctx: Context) {
+        context = ctx.applicationContext
+        prefs = context?.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+        initialized = true
+    }
+    
+    /**
+     * 检查是否已初始化
+     */
+    fun isInitialized(): Boolean = initialized
+    
+    // ========== 键值存储 ==========
+    
+    override fun putString(key: String, value: String) {
+        prefs?.edit()?.putString(key, value)?.apply()
+    }
+    
+    override fun getString(key: String, defaultValue: String): String {
+        return prefs?.getString(key, defaultValue) ?: defaultValue
+    }
+    
+    override fun putInt(key: String, value: Int) {
+        prefs?.edit()?.putInt(key, value)?.apply()
+    }
+    
+    override fun getInt(key: String, defaultValue: Int): Int {
+        return prefs?.getInt(key, defaultValue) ?: defaultValue
+    }
+    
+    override fun putLong(key: String, value: Long) {
+        prefs?.edit()?.putLong(key, value)?.apply()
+    }
+    
+    override fun getLong(key: String, defaultValue: Long): Long {
+        return prefs?.getLong(key, defaultValue) ?: defaultValue
+    }
+    
+    override fun putBoolean(key: String, value: Boolean) {
+        prefs?.edit()?.putBoolean(key, value)?.apply()
+    }
+    
+    override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
+        return prefs?.getBoolean(key, defaultValue) ?: defaultValue
+    }
+    
+    override fun remove(key: String) {
+        prefs?.edit()?.remove(key)?.apply()
+    }
+    
+    override fun clear() {
+        prefs?.edit()?.clear()?.apply()
+    }
+    
+    // ========== 文件存储 ==========
+    
+    override fun saveFile(path: String, data: ByteArray): Boolean {
+        return try {
+            val ctx = context ?: return false
+            val file = File(ctx.filesDir, path)
+            file.parentFile?.mkdirs()
+            file.writeBytes(data)
+            true
+        } catch (e: Exception) {
+            false
+        }
+    }
+    
+    override fun readFile(path: String): ByteArray? {
+        return try {
+            val ctx = context ?: return null
+            val file = File(ctx.filesDir, path)
+            if (file.exists()) file.readBytes() else null
+        } catch (e: Exception) {
+            null
+        }
+    }
+    
+    override fun deleteFile(path: String): Boolean {
+        return try {
+            val ctx = context ?: return false
+            val file = File(ctx.filesDir, path)
+            if (file.exists()) file.delete() else false
+        } catch (e: Exception) {
+            false
+        }
+    }
+}
+

+ 17 - 0
capability-ble/src/main/java/com/narutohuo/xindazhou/ble/config/BLEConstants.kt

@@ -177,5 +177,22 @@ object BLEConstants {
      * 连接成功后握手超时时间(秒)
      */
     const val HANDSHAKE_TIMEOUT_SECONDS = 3
+    
+    // ========== 设备过滤配置 ==========
+    
+    /**
+     * 目标设备名称(用于扫描过滤,可选)
+     * 如果为 null,则不按名称过滤
+     * 默认值:"SDH-Smart"(参考 demo)
+     */
+    val TARGET_DEVICE_NAME: String? = "SDH-Smart"
+    
+    /**
+     * 目标设备地址(用于扫描过滤,可选)
+     * 如果为 null,则不按地址过滤
+     * 格式:XX:XX:XX:XX:XX:XX(不区分大小写)
+     * 默认值:"C0:D4:32:83:61:3A"(参考 demo)
+     */
+    val TARGET_DEVICE_ADDRESS: String? = "C0:D4:32:83:61:3A"
 }
 

+ 94 - 5
capability-ble/src/main/java/com/narutohuo/xindazhou/ble/impl/BLEServiceImpl.kt

@@ -11,6 +11,7 @@ import com.narutohuo.xindazhou.ble.util.*
 import com.narutohuo.xindazhou.core.log.ILog
 import org.json.JSONObject
 import java.util.TimeZone
+import java.util.UUID
 import java.util.concurrent.ConcurrentHashMap
 import kotlin.random.Random
 
@@ -98,7 +99,9 @@ class BLEServiceImpl private constructor(
             },
             onScanFinished = {
                 ILog.d(tag, "扫描完成")
-            }
+            },
+            filterDeviceName = BLEConstants.TARGET_DEVICE_NAME,
+            filterDeviceAddress = BLEConstants.TARGET_DEVICE_ADDRESS
         )
         scanner?.startScan()
     }
@@ -109,6 +112,13 @@ class BLEServiceImpl private constructor(
     }
     
     override fun connect(device: BLEDevice, userId: ByteArray, userType: Byte, callback: (BLEResponse) -> Unit) {
+        ILog.d(tag, "========== 开始连接流程 ==========")
+        ILog.d(tag, "设备名称: ${device.name}")
+        ILog.d(tag, "设备地址: ${device.address}")
+        ILog.d(tag, "用户类型: $userType")
+        ILog.d(tag, "用户ID长度: ${userId.size}")
+        ILog.d(tag, "====================================")
+        
         // 保存用户信息和回调
         pendingUserId = userId
         pendingUserType = userType
@@ -119,14 +129,33 @@ class BLEServiceImpl private constructor(
         // 生成加密密钥(基于MAC地址)
         val macBytes = device.address.replace(":", "").hexToByteArray()
         encryptKey = BLECrypto.generateAESKey(macBytes)
+        ILog.d(tag, "加密密钥已生成")
+        
+        val adapter = android.bluetooth.BluetoothAdapter.getDefaultAdapter()
+        if (adapter == null) {
+            ILog.e(tag, "❌ BluetoothAdapter 为 null")
+            callback(BLEResponse(success = false, errorMessage = "蓝牙适配器未初始化"))
+            return
+        }
         
-        val androidDevice = android.bluetooth.BluetoothAdapter.getDefaultAdapter()
-            ?.getRemoteDevice(device.address)
-            ?: run {
-                callback(BLEResponse(success = false, errorMessage = "无法获取设备对象"))
+        if (!adapter.isEnabled) {
+            ILog.e(tag, "❌ 蓝牙未开启")
+            callback(BLEResponse(success = false, errorMessage = "蓝牙未开启"))
             return
         }
         
+        ILog.d(tag, "✅ 蓝牙适配器状态正常")
+        
+        val androidDevice = try {
+            adapter.getRemoteDevice(device.address)
+        } catch (e: IllegalArgumentException) {
+            ILog.e(tag, "❌ 无效的设备地址: ${device.address}", e)
+            callback(BLEResponse(success = false, errorMessage = "无效的设备地址: ${device.address}"))
+            return
+        }
+        
+        ILog.d(tag, "✅ 获取到 BluetoothDevice 对象")
+        
         connector = BleConnector(
             context = context,
             device = androidDevice,
@@ -138,6 +167,10 @@ class BLEServiceImpl private constructor(
                     splitter = packetSplitter,
                     onPairing = { pairing ->
                         ILog.d(tag, "收到配对请求,状态: $pairing")
+                    },
+                    onHandshakeRequest = { random ->
+                        // 收到设备的握手请求,发送握手应答
+                        handleHandshakeResponse(random, userId, userType)
                     }
                 )
                 
@@ -182,6 +215,7 @@ class BLEServiceImpl private constructor(
             }
         )
         
+        ILog.d(tag, "✅ BleConnector 已创建,开始连接...")
         connector?.connect()
     }
     
@@ -295,6 +329,61 @@ class BLEServiceImpl private constructor(
     }
     
     /**
+     * 处理握手响应(收到设备的握手请求后调用)
+     */
+    private fun handleHandshakeResponse(random: Int, userId: ByteArray, userType: Byte) {
+        if (!isConnected() || packetSender == null || encryptKey == null) {
+            ILog.e(tag, "无法发送握手应答:未连接或密钥未生成")
+            return
+        }
+        
+        try {
+            // 将 random 转为偶数位十六进制字符串
+            val randomHex = random.toEvenHex()
+            ILog.d(tag, "握手随机数(十六进制): $randomHex")
+            
+            // 将十六进制字符串转为 ByteArray
+            val randomBytes = randomHex.hexToByteArray()
+            
+            // 使用 AES 加密随机数
+            val encryptedRandomBytes = BLECrypto.encrypt(randomBytes, encryptKey!!)
+            
+            // 将加密后的 ByteArray 转为十六进制字符串
+            val encryptedRandomHex = encryptedRandomBytes.toHexString()
+            ILog.d(tag, "加密后的随机数(十六进制): $encryptedRandomHex")
+            
+            // 构建握手应答 JSON
+            val json = JSONObject().apply {
+                put("TY", 3)  // 类型:握手
+                put("IN", 3)  // 指令:握手应答
+                put("ID", UUID.randomUUID().toString())
+                put("TS", System.currentTimeMillis())
+                put("C", JSONObject().apply {
+                    put("D", JSONObject().apply {
+                        put("EncryptedRandom", encryptedRandomHex)
+                        put("UserType", userType.toInt())
+                        put("UserID", userId.toHexString())
+                    })
+                })
+            }
+            
+            ILog.d(tag, "发送握手应答")
+            
+            // 发送握手应答(不等待回调,因为这是响应,不是请求)
+            sendRawData(json.toString().toByteArray(Charsets.UTF_8)) { response ->
+                if (response.success) {
+                    ILog.d(tag, "握手应答发送成功")
+                } else {
+                    ILog.e(tag, "握手应答发送失败: ${response.errorMessage}")
+                }
+            }
+            
+        } catch (e: Exception) {
+            ILog.e(tag, "处理握手响应失败", e)
+        }
+    }
+    
+    /**
      * 执行握手(内部方法,连接成功后自动调用)
      */
     private fun performHandshake(userId: ByteArray, userType: Byte) {

+ 121 - 21
capability-ble/src/main/java/com/narutohuo/xindazhou/ble/util/BleConnector.kt

@@ -40,14 +40,27 @@ class BleConnector(
      */
     @SuppressLint("MissingPermission")
     fun connect() {
+        ILog.d(tag, "========== 开始连接设备 ==========")
+        ILog.d(tag, "设备地址: ${device.address}")
+        ILog.d(tag, "设备名称: ${device.name ?: "未知"}")
+        ILog.d(tag, "设备类型: ${device.type}")
+        ILog.d(tag, "绑定状态: ${device.bondState} (10=NONE, 11=BONDING, 12=BONDED)")
+        ILog.d(tag, "====================================")
+        
         try {
             gatt = device.connectGatt(context, false, gattCallback)
-            ILog.d(tag, "开始连接设备: ${device.address}")
+            if (gatt != null) {
+                ILog.d(tag, "✅ connectGatt() 调用成功,gatt 对象已创建")
+                ILog.d(tag, "等待 onConnectionStateChange 回调...")
+            } else {
+                ILog.e(tag, "❌ connectGatt() 返回 null")
+                onError("连接失败: connectGatt() 返回 null")
+            }
         } catch (e: SecurityException) {
-            ILog.e(tag, "连接权限不足", e)
+            ILog.e(tag, "连接权限不足", e)
             onError("连接权限不足: ${e.message}")
         } catch (e: Exception) {
-            ILog.e(tag, "连接失败", e)
+            ILog.e(tag, "连接失败", e)
             onError("连接失败: ${e.message}")
         }
     }
@@ -78,62 +91,144 @@ class BleConnector(
         override fun onConnectionStateChange(
             gatt: BluetoothGatt, status: Int, newState: Int
         ) {
-            if (newState == BluetoothProfile.STATE_CONNECTED) {
-                ILog.d(tag, "已连接 ${device.address},开始发现服务")
-                gatt.discoverServices()  // 触发 onServicesDiscovered
-            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
-                ILog.d(tag, "已断开连接")
-                onDisconnected()
-                disconnect()
+            ILog.d(tag, "========== 连接状态变化 ==========")
+            ILog.d(tag, "设备地址: ${device.address}")
+            ILog.d(tag, "状态码: $status (0=SUCCESS)")
+            ILog.d(tag, "新状态: $newState (1=DISCONNECTED, 2=CONNECTING, 3=CONNECTED)")
+            ILog.d(tag, "====================================")
+            
+            when (newState) {
+                BluetoothProfile.STATE_CONNECTED -> {
+                    ILog.d(tag, "✅ 已连接设备 ${device.address},开始发现服务")
+                    try {
+                        gatt.discoverServices()  // 触发 onServicesDiscovered
+                        ILog.d(tag, "已调用 discoverServices()")
+                    } catch (e: SecurityException) {
+                        ILog.e(tag, "❌ 发现服务权限不足", e)
+                        onError("发现服务权限不足: ${e.message}")
+                    } catch (e: Exception) {
+                        ILog.e(tag, "❌ 发现服务失败", e)
+                        onError("发现服务失败: ${e.message}")
+                    }
+                }
+                BluetoothProfile.STATE_DISCONNECTED -> {
+                    ILog.d(tag, "已断开连接 (状态码: $status)")
+                    onDisconnected()
+                    disconnect()
+                }
+                BluetoothProfile.STATE_CONNECTING -> {
+                    ILog.d(tag, "正在连接...")
+                }
+                BluetoothProfile.STATE_DISCONNECTING -> {
+                    ILog.d(tag, "正在断开连接...")
+                }
+                else -> {
+                    ILog.w(tag, "未知状态: $newState")
+                }
             }
         }
         
         @SuppressLint("MissingPermission")
         override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
+            ILog.d(tag, "========== 服务发现回调 ==========")
+            ILog.d(tag, "状态码: $status (0=GATT_SUCCESS)")
+            ILog.d(tag, "====================================")
+            
             if (status != BluetoothGatt.GATT_SUCCESS) {
+                ILog.e(tag, "❌ 发现服务失败,状态码: $status")
                 onError("发现服务失败 status=$status")
                 return
             }
             
-            ILog.d(tag, "onServicesDiscovered: 发现服务")
+            ILog.d(tag, "✅ onServicesDiscovered: 发现服务成功")
+            
+            // 打印所有服务
+            val allServices = gatt.services
+            ILog.d(tag, "服务列表 (${allServices.size} 个):")
+            allServices.forEach { svc ->
+                ILog.d(tag, "  服务 UUID: ${svc.uuid}")
+                val characteristics = svc.characteristics
+                ILog.d(tag, "    特征列表 (${characteristics.size} 个):")
+                characteristics.forEach { char ->
+                    ILog.d(tag, "      特征 UUID: ${char.uuid}")
+                }
+            }
             
             val targetServiceUuid = BLEConstants.SERVICE_UUID
             val targetWriteCharUuid = BLEConstants.WRITE_CHARACTERISTIC_UUID
             val targetNotifyCharUuid = BLEConstants.NOTIFY_CHARACTERISTIC_UUID
             val charConfigUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
             
+            ILog.d(tag, "目标服务 UUID: $targetServiceUuid")
+            ILog.d(tag, "目标写特征 UUID: $targetWriteCharUuid")
+            ILog.d(tag, "目标通知特征 UUID: $targetNotifyCharUuid")
+            
             val service = this@BleConnector.gatt!!.getService(targetServiceUuid)
             
-            writeChar = service?.getCharacteristic(targetWriteCharUuid)
-            notifyChar = service?.getCharacteristic(targetNotifyCharUuid)
+            if (service == null) {
+                val availableServices = allServices.map { it.uuid.toString() }.joinToString(", ")
+                ILog.e(tag, "❌❌❌ 未找到目标服务 $targetServiceUuid")
+                ILog.e(tag, "🔴🔴🔴 可用服务列表: $availableServices")
+                android.util.Log.e(tag, "❌❌❌ 未找到目标服务 $targetServiceUuid")
+                android.util.Log.e(tag, "🔴🔴🔴 可用服务列表: $availableServices")
+                onError("未找到目标服务 $targetServiceUuid,可用服务: $availableServices")
+                return
+            }
+            
+            ILog.d(tag, "✅ 找到目标服务: ${service.uuid}")
+            
+            writeChar = service.getCharacteristic(targetWriteCharUuid)
+            notifyChar = service.getCharacteristic(targetNotifyCharUuid)
             
             if (writeChar == null) {
-                onError("未找到写特征 $targetWriteCharUuid")
+                val availableChars = service.characteristics.map { it.uuid.toString() }.joinToString(", ")
+                ILog.e(tag, "❌ 未找到写特征 $targetWriteCharUuid")
+                ILog.e(tag, "可用特征: $availableChars")
+                onError("未找到写特征 $targetWriteCharUuid,可用特征: $availableChars")
                 return
             }
             
+            ILog.d(tag, "✅ 找到写特征: ${writeChar!!.uuid}")
+            
             if (notifyChar == null) {
-                onError("未找到通知特征 $targetNotifyCharUuid")
+                val availableChars = service.characteristics.map { it.uuid.toString() }.joinToString(", ")
+                ILog.e(tag, "❌ 未找到通知特征 $targetNotifyCharUuid")
+                ILog.e(tag, "可用特征: $availableChars")
+                onError("未找到通知特征 $targetNotifyCharUuid,可用特征: $availableChars")
                 return
             }
             
+            ILog.d(tag, "✅ 找到通知特征: ${notifyChar!!.uuid}")
+            
             // 启用通知
             try {
+                ILog.d(tag, "开始启用通知...")
                 this@BleConnector.gatt!!.setCharacteristicNotification(notifyChar, true)
+                ILog.d(tag, "已调用 setCharacteristicNotification(true)")
+                
                 val descriptor = notifyChar?.getDescriptor(charConfigUuid)
-                descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
+                if (descriptor == null) {
+                    ILog.e(tag, "❌ 未找到 CCCD descriptor (UUID: $charConfigUuid)")
+                    onError("未找到 CCCD descriptor")
+                    return
+                }
+                
+                ILog.d(tag, "找到 CCCD descriptor,准备写入...")
+                descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
                 this@BleConnector.gatt!!.writeDescriptor(descriptor)
+                ILog.d(tag, "已调用 writeDescriptor()")
                 
-                ILog.d(tag, "已启用通知")
+                ILog.d(tag, "✅ 通知设置完成,等待 CCCD 写入回调")
+                ILog.d(tag, "✅ 连接流程完成,调用 onConnected 回调")
                 
                 // 连接成功,回调
                 onConnected(this@BleConnector.gatt!!, writeChar!!)
                 
             } catch (e: SecurityException) {
-                ILog.e(tag, "启用通知权限不足", e)
+                ILog.e(tag, "启用通知权限不足", e)
                 onError("启用通知权限不足: ${e.message}")
             } catch (e: Exception) {
-                ILog.e(tag, "启用通知失败", e)
+                ILog.e(tag, "启用通知失败", e)
                 onError("启用通知失败: ${e.message}")
             }
         }
@@ -154,10 +249,15 @@ class BleConnector(
             status: Int
         ) {
             super.onDescriptorWrite(gatt, descriptor, status)
+            ILog.d(tag, "========== CCCD 写入回调 ==========")
+            ILog.d(tag, "状态码: $status (0=GATT_SUCCESS)")
+            ILog.d(tag, "Descriptor UUID: ${descriptor?.uuid}")
+            ILog.d(tag, "====================================")
+            
             if (status == BluetoothGatt.GATT_SUCCESS) {
-                ILog.d(tag, "CCCD 写入成功")
+                ILog.d(tag, "CCCD 写入成功,通知已启用")
             } else {
-                ILog.e(tag, "CCCD 写入失败,状态码: $status")
+                ILog.e(tag, "CCCD 写入失败,状态码: $status")
             }
         }
         

+ 8 - 9
capability-ble/src/main/java/com/narutohuo/xindazhou/ble/util/BlePacketSender.kt

@@ -28,7 +28,8 @@ class BlePacketSender(
     private val maxPayloadPerPacket: Int = 20,
     private val ackTimeoutMs: Long = 1000L,
     private val maxRetry: Int = 3,
-    private val onPairing: ((Int) -> Unit)? = null
+    private val onPairing: ((Int) -> Unit)? = null,
+    private val onHandshakeRequest: ((Int) -> Unit)? = null
 ) {
     
     private val tag = "BlePacketSender"
@@ -96,11 +97,10 @@ class BlePacketSender(
         
         ILog.d(tag, "发送数据: result=$writeResult, packet=${packet.toHexString()}")
         
-        if (!writeResult) {
-            // 写入函数本身失败(比如 GATT 已断开),直接进入重试逻辑
-            handleWriteFailure(packet, retryCount, "writeCharacteristic 返回 false")
-            return
-        }
+        // 注意:即使 writeResult 为 false,也启动超时计时器
+        // 因为在 Android BLE 中,使用 WRITE_TYPE_NO_RESPONSE 时,writeCharacteristic 可能返回 false
+        // 但写入操作可能在后续完成,并通过 onCharacteristicWrite 回调确认
+        // 如果 writeResult 为 false 且后续没有 onCharacteristicWrite 回调,超时后会重试
         
         // 2️⃣ 启动超时计时
         CoroutineScope(Dispatchers.Main).launch {
@@ -280,9 +280,8 @@ class BlePacketSender(
             val random = d.getInt("Random")
             ILog.d(tag, "收到握手请求,随机数: $random")
             
-            // 这里应该由业务层处理握手响应
-            // 暂时只记录日志
-            ILog.d(tag, "握手请求已接收,等待业务层处理")
+            // 通知业务层处理握手响应
+            onHandshakeRequest?.invoke(random)
             
         } catch (e: Exception) {
             ILog.e(tag, "处理握手请求失败", e)

+ 78 - 11
capability-ble/src/main/java/com/narutohuo/xindazhou/ble/util/BleScanner.kt

@@ -22,7 +22,9 @@ import com.narutohuo.xindazhou.core.log.ILog
 class BleScanner(
     private val context: Context,
     private val onDeviceFound: (BluetoothDevice) -> Unit,
-    private val onScanFinished: () -> Unit
+    private val onScanFinished: () -> Unit,
+    private val filterDeviceName: String? = null,
+    private val filterDeviceAddress: String? = null
 ) {
     
     private val tag = "BleScanner"
@@ -37,15 +39,47 @@ class BleScanner(
     
     private val scanCallback = object : ScanCallback() {
         override fun onScanResult(callbackType: Int, result: ScanResult) {
-            result.device?.let {
-                onDeviceFound(it)
-                stopScan()
+            val device = result.device
+            if (device != null) {
+                ILog.d(tag, "========== 发现设备 ==========")
+                ILog.d(tag, "设备名称: ${device.name ?: "未知"}")
+                ILog.d(tag, "设备地址: ${device.address}")
+                ILog.d(tag, "RSSI: ${result.rssi}")
+                ILog.d(tag, "扫描类型: $callbackType (1=CALLBACK_TYPE_ALL_MATCHES, 2=BATCH)")
+                ILog.d(tag, "====================================")
+                
+                // 设备过滤(OR逻辑:名称匹配或地址匹配)
+                val nameMatches = filterDeviceName?.let { 
+                    device.name?.equals(it, ignoreCase = true) == true 
+                } ?: false
+                
+                val addressMatches = filterDeviceAddress?.let { 
+                    device.address.equals(it, ignoreCase = true) 
+                } ?: false
+                
+                val shouldConnect = if (filterDeviceName != null || filterDeviceAddress != null) {
+                    // 如果设置了过滤条件,则必须匹配(OR逻辑)
+                    nameMatches || addressMatches
+                } else {
+                    // 如果没有设置过滤条件,则连接所有设备
+                    true
+                }
+                
+                if (shouldConnect) {
+                    ILog.d(tag, "✅ 设备匹配过滤条件(名称匹配=$nameMatches, 地址匹配=$addressMatches),准备连接")
+                    onDeviceFound(device)
+                    stopScan()
+                } else {
+                    ILog.d(tag, "⏭️ 设备不匹配过滤条件,跳过")
+                }
             }
         }
         
         override fun onBatchScanResults(results: List<ScanResult>) {
+            ILog.d(tag, "批量扫描结果: ${results.size} 个设备")
             results.forEach {
                 it.device?.let { device ->
+                    ILog.d(tag, "发现设备: ${device.name ?: "未知"}, 地址: ${device.address}, RSSI: ${it.rssi}")
                     onDeviceFound(device)
                     stopScan()
                 }
@@ -53,7 +87,10 @@ class BleScanner(
         }
         
         override fun onScanFailed(errorCode: Int) {
-            ILog.e(tag, "BLE scan failed, error=$errorCode")
+            ILog.e(tag, "========== 扫描失败 ==========")
+            ILog.e(tag, "错误码: $errorCode")
+            ILog.e(tag, "1=SCAN_FAILED_ALREADY_STARTED, 2=SCAN_FAILED_APPLICATION_REGISTRATION_FAILED, 3=SCAN_FAILED_INTERNAL_ERROR, 4=SCAN_FAILED_FEATURE_UNSUPPORTED, 5=SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES")
+            ILog.e(tag, "====================================")
             stopScan()
         }
     }
@@ -63,9 +100,28 @@ class BleScanner(
      */
     @android.annotation.SuppressLint("MissingPermission")
     fun startScan(filterNamePrefix: String? = null, timeoutMs: Long = 10_000L) {
-        if (scanning) return
+        ILog.d(tag, "========== 开始扫描 ==========")
+        ILog.d(tag, "过滤前缀: ${filterNamePrefix ?: "无"}")
+        ILog.d(tag, "超时时间: ${timeoutMs}ms")
+        ILog.d(tag, "蓝牙适配器状态: ${if (bluetoothAdapter.isEnabled) "已开启" else "已关闭"}")
+        ILog.d(tag, "====================================")
+        
+        if (scanning) {
+            ILog.w(tag, "⚠️ 扫描已在进行中,忽略重复请求")
+            return
+        }
+        
+        if (!bluetoothAdapter.isEnabled) {
+            ILog.e(tag, "❌ 蓝牙未开启,无法扫描")
+            return
+        }
         
         scanner = bluetoothAdapter.bluetoothLeScanner
+        if (scanner == null) {
+            ILog.e(tag, "❌ BluetoothLeScanner 为 null")
+            return
+        }
+        
         val settings = ScanSettings.Builder()
             .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
             .build()
@@ -73,20 +129,26 @@ class BleScanner(
         val filters = mutableListOf<ScanFilter>()
         filterNamePrefix?.let {
             filters.add(ScanFilter.Builder().setDeviceName(it).build())
+            ILog.d(tag, "已设置设备名称过滤: $it")
         }
         
         try {
             scanner?.startScan(filters, settings, scanCallback)
             scanning = true
-            ILog.d(tag, "开始扫描BLE设备,过滤前缀: $filterNamePrefix")
+            ILog.d(tag, "✅ 扫描已启动,等待设备发现...")
             
             // 超时自动停止
-            Handler(Looper.getMainLooper()).postDelayed({ stopScan() }, timeoutMs)
+            Handler(Looper.getMainLooper()).postDelayed({
+                if (scanning) {
+                    ILog.w(tag, "⏰ 扫描超时 (${timeoutMs}ms),自动停止")
+                    stopScan()
+                }
+            }, timeoutMs)
         } catch (e: SecurityException) {
-            ILog.e(tag, "扫描权限不足", e)
+            ILog.e(tag, "扫描权限不足", e)
             scanning = false
         } catch (e: Exception) {
-            ILog.e(tag, "启动扫描失败", e)
+            ILog.e(tag, "启动扫描失败", e)
             scanning = false
         }
     }
@@ -96,7 +158,12 @@ class BleScanner(
      */
     @android.annotation.SuppressLint("MissingPermission")
     fun stopScan() {
-        if (!scanning) return
+        if (!scanning) {
+            ILog.d(tag, "stopScan() 被调用,但未在扫描中")
+            return
+        }
+        
+        ILog.d(tag, "停止扫描...")
         
         try {
             scanner?.stopScan(scanCallback)

File diff suppressed because it is too large
+ 501 - 0
capability-ble/集成说明.html


+ 11 - 2
capability-qrcode/build.gradle

@@ -25,7 +25,16 @@ dependencies {
     // base-core 提供接口定义和基础实现(ILog、NetworkManager)
     implementation(project(":base-core"))
     
-    // 华为扫码SDK(待添加具体版本)
-    // implementation 'com.huawei.hms:scan:xxx'
+    // 华为扫码SDK
+    implementation("com.huawei.hms:scan:2.12.0.300")
+    
+    // AppCompat(用于 Activity 主题)
+    implementation("androidx.appcompat:appcompat:1.6.1")
+    
+    // Fragment(用于桥接 ActivityResultLauncher)
+    implementation("androidx.fragment:fragment-ktx:1.6.2")
+    
+    // Activity Result API
+    implementation("androidx.activity:activity-ktx:1.8.2")
 }
 

+ 9 - 3
capability-qrcode/src/main/AndroidManifest.xml

@@ -2,8 +2,14 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <!-- 相机权限(用于扫码) -->
     <uses-permission android:name="android.permission.CAMERA" />
-    <!-- 存储权限(用于保存扫码结果) -->
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    
+    <application>
+        <!-- 二维码扫描 Activity -->
+        <activity
+            android:name=".ui.QRCodeScanActivity"
+            android:screenOrientation="portrait"
+            android:theme="@style/Theme.AppCompat.NoActionBar"
+            android:exported="false" />
+    </application>
 </manifest>
 

+ 22 - 0
capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/api/QRCodeManager.kt

@@ -0,0 +1,22 @@
+package com.narutohuo.xindazhou.qrcode.api
+
+import android.app.Activity
+import com.narutohuo.xindazhou.qrcode.model.QRCodeResponse
+
+/**
+ * 二维码管理器接口
+ * 
+ * 提供二维码扫描能力
+ * 使用华为扫码SDK实现
+ */
+interface QRCodeManager {
+    
+    /**
+     * 扫描二维码
+     * 
+     * @param activity Activity上下文
+     * @param callback 扫描结果回调,返回QRCodeResponse
+     */
+    fun scanQRCode(activity: Activity, callback: (QRCodeResponse) -> Unit)
+}
+

+ 0 - 40
capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/api/QRCodeService.kt

@@ -1,40 +0,0 @@
-package com.narutohuo.xindazhou.qrcode.api
-
-import android.app.Activity
-import android.graphics.Bitmap
-import com.narutohuo.xindazhou.qrcode.model.QRCodeResponse
-
-/**
- * 二维码服务接口
- * 
- * 提供二维码生成和扫描能力
- * 使用华为扫码SDK实现
- */
-interface QRCodeService {
-    
-    /**
-     * 生成二维码
-     * 
-     * @param content 二维码内容
-     * @param size 二维码尺寸(像素)
-     * @return 二维码Bitmap
-     */
-    fun generateQRCode(content: String, size: Int): Bitmap?
-    
-    /**
-     * 扫描二维码
-     * 
-     * @param activity Activity上下文
-     * @param callback 扫描结果回调,返回QRCodeResponse
-     */
-    fun scanQRCode(activity: Activity, callback: (QRCodeResponse) -> Unit)
-    
-    /**
-     * 识别图片中的二维码
-     * 
-     * @param bitmap 图片Bitmap
-     * @param callback 识别结果回调,返回QRCodeResponse
-     */
-    fun recognizeQRCode(bitmap: Bitmap, callback: (QRCodeResponse) -> Unit)
-}
-

+ 33 - 0
capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/factory/QRCodeManagerFactory.kt

@@ -0,0 +1,33 @@
+package com.narutohuo.xindazhou.qrcode.factory
+
+import com.narutohuo.xindazhou.qrcode.api.QRCodeManager
+import com.narutohuo.xindazhou.qrcode.impl.QRCodeManagerImpl
+
+/**
+ * 二维码管理器工厂类
+ * 
+ * 提供统一的获取二维码管理器实例的方式
+ * 使用单例模式,方便业务层调用
+ */
+object QRCodeManagerFactory {
+    
+    /**
+     * 创建或获取二维码管理器实例(单例)
+     * 
+     * @return QRCodeManager实例(单例)
+     */
+    fun create(): QRCodeManager {
+        return QRCodeManagerImpl.getInstance()
+    }
+    
+    /**
+     * 获取二维码管理器实例(便捷方法)
+     * 
+     * @return QRCodeManager实例(单例)
+     */
+    @JvmStatic
+    fun getInstance(): QRCodeManager {
+        return create()
+    }
+}
+

+ 73 - 0
capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/impl/QRCodeManagerImpl.kt

@@ -0,0 +1,73 @@
+package com.narutohuo.xindazhou.qrcode.impl
+
+import android.app.Activity
+import android.util.Log
+import androidx.fragment.app.FragmentActivity
+import com.narutohuo.xindazhou.qrcode.api.QRCodeManager
+import com.narutohuo.xindazhou.qrcode.model.QRCodeResponse
+import com.narutohuo.xindazhou.qrcode.ui.QRCodeScanFragment
+
+/**
+ * 二维码管理器实现类(单例模式)
+ * 
+ * 使用单例方便管理,多个界面可以共享同一个扫码管理器
+ * 
+ * 使用华为 Customized View Mode 实现精美的自定义扫码界面
+ * 内部使用 Fragment 桥接 ActivityResultLauncher,业务层无需处理 onActivityResult
+ */
+class QRCodeManagerImpl private constructor() : QRCodeManager {
+    
+    companion object {
+        @Volatile
+        private var INSTANCE: QRCodeManagerImpl? = null
+        
+        /**
+         * 获取二维码管理器单例
+         */
+        @JvmStatic
+        fun getInstance(): QRCodeManagerImpl {
+            return INSTANCE ?: synchronized(this) {
+                INSTANCE ?: QRCodeManagerImpl().also { INSTANCE = it }
+            }
+        }
+        
+        /**
+         * 销毁单例(用于测试)
+         */
+        @JvmStatic
+        fun destroyInstance() {
+            INSTANCE = null
+        }
+    }
+    
+    private val tag = "QRCodeManager"
+    
+    override fun scanQRCode(activity: Activity, callback: (QRCodeResponse) -> Unit) {
+        Log.d(tag, "scanQRCode: 启动扫码界面")
+        
+        // 如果 Activity 是 FragmentActivity,使用 Fragment 桥接(推荐方式)
+        if (activity is FragmentActivity) {
+            QRCodeScanFragment.startScan(activity) { result ->
+                if (result != null) {
+                    callback(QRCodeResponse(
+                        success = true,
+                        data = result
+                    ))
+                } else {
+                    callback(QRCodeResponse(
+                        success = false,
+                        errorMessage = "用户取消扫码"
+                    ))
+                }
+            }
+        } else {
+            // 如果不是 FragmentActivity,降级使用 startActivityForResult
+            // 这种情况需要业务层在 onActivityResult 中处理
+            callback(QRCodeResponse(
+                success = false,
+                errorMessage = "当前 Activity 不支持扫码功能,请使用 FragmentActivity"
+            ))
+        }
+    }
+}
+

+ 0 - 80
capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/impl/QRCodeServiceImpl.kt

@@ -1,80 +0,0 @@
-package com.narutohuo.xindazhou.qrcode.impl
-
-import android.app.Activity
-import android.graphics.Bitmap
-import android.util.Log
-import com.narutohuo.xindazhou.qrcode.api.QRCodeService
-import com.narutohuo.xindazhou.qrcode.model.QRCodeResponse
-
-/**
- * 二维码服务实现类(单例模式)
- * 
- * 使用单例方便管理,多个界面可以共享同一个扫码服务
- * 
- * 注意:具体实现需要集成华为扫码SDK
- * 当前为基础框架,具体逻辑待实现
- */
-class QRCodeServiceImpl private constructor() : QRCodeService {
-    
-    companion object {
-        @Volatile
-        private var INSTANCE: QRCodeServiceImpl? = null
-        
-        /**
-         * 获取二维码服务单例
-         */
-        @JvmStatic
-        fun getInstance(): QRCodeServiceImpl {
-            return INSTANCE ?: synchronized(this) {
-                INSTANCE ?: QRCodeServiceImpl().also { INSTANCE = it }
-            }
-        }
-        
-        /**
-         * 销毁单例(用于测试)
-         */
-        @JvmStatic
-        fun destroyInstance() {
-            INSTANCE = null
-        }
-    }
-    
-    private val tag = "QRCodeService"
-    
-    override fun generateQRCode(content: String, size: Int): Bitmap? {
-        Log.d(tag, "generateQRCode: content=$content, size=$size")
-        // TODO: 使用华为扫码SDK生成二维码
-        // val options = HmsBuildBitmapOption.Creator().setBitmapBackgroundColor(Color.WHITE).create()
-        // return ScanUtil.generateQRCode(content, size, size, options)
-        return null
-    }
-    
-    override fun scanQRCode(activity: Activity, callback: (QRCodeResponse) -> Unit) {
-        Log.d(tag, "scanQRCode: 启动扫码(待实现)")
-        // TODO: 使用华为扫码SDK启动扫码界面
-        // val options = HmsScanAnalyzerOptions.Creator().setHmsScanTypes(HmsScan.QRCODE_SCAN_TYPE).create()
-        // ScanUtil.startScan(activity, REQUEST_CODE_SCAN, options)
-        callback(
-            QRCodeResponse(
-                success = false,
-                data = null,
-                errorMessage = "Not implemented"
-            )
-        )
-    }
-    
-    override fun recognizeQRCode(bitmap: Bitmap, callback: (QRCodeResponse) -> Unit) {
-        Log.d(tag, "recognizeQRCode: 识别二维码(待实现)")
-        // TODO: 使用华为扫码SDK识别图片中的二维码
-        // val options = HmsScanAnalyzerOptions.Creator().setHmsScanTypes(HmsScan.QRCODE_SCAN_TYPE).create()
-        // val results = ScanUtil.decodeWithBitmap(bitmap, options)
-        callback(
-            QRCodeResponse(
-                success = false,
-                data = null,
-                errorMessage = "Not implemented"
-            )
-        )
-    }
-}
-

+ 94 - 0
capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/ui/QRCodeScanActivity.kt

@@ -0,0 +1,94 @@
+package com.narutohuo.xindazhou.qrcode.ui
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import com.huawei.hms.hmsscankit.ScanUtil
+import com.huawei.hms.ml.scan.HmsScan
+import com.huawei.hms.ml.scan.HmsScanAnalyzerOptions
+
+/**
+ * 二维码扫描 Activity
+ * 
+ * 使用华为 Default View Mode 实现扫码界面
+ */
+class QRCodeScanActivity : AppCompatActivity() {
+    
+    companion object {
+        const val EXTRA_RESULT = "scan_result"
+        const val REQUEST_CODE_SCAN = 1001
+        const val REQUEST_CODE_CAMERA_PERMISSION = 1002
+        
+        /**
+         * 启动扫码界面
+         */
+        fun start(activity: Activity, requestCode: Int = REQUEST_CODE_SCAN) {
+            val intent = Intent(activity, QRCodeScanActivity::class.java)
+            activity.startActivityForResult(intent, requestCode)
+        }
+    }
+    
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        
+        // 检查相机权限
+        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+            != PackageManager.PERMISSION_GRANTED
+        ) {
+            ActivityCompat.requestPermissions(
+                this,
+                arrayOf(Manifest.permission.CAMERA),
+                REQUEST_CODE_CAMERA_PERMISSION
+            )
+        } else {
+            startScan()
+        }
+    }
+    
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<out String>,
+        grantResults: IntArray
+    ) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+        if (requestCode == REQUEST_CODE_CAMERA_PERMISSION) {
+            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                startScan()
+            } else {
+                Toast.makeText(this, "需要相机权限才能扫码", Toast.LENGTH_SHORT).show()
+                finish()
+            }
+        }
+    }
+    
+    private fun startScan() {
+        val options = HmsScanAnalyzerOptions.Creator()
+            .setHmsScanTypes(HmsScan.QRCODE_SCAN_TYPE)
+            .create()
+        
+        ScanUtil.startScan(this, REQUEST_CODE_SCAN, options)
+    }
+    
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        super.onActivityResult(requestCode, resultCode, data)
+        if (requestCode == REQUEST_CODE_SCAN) {
+            if (resultCode == Activity.RESULT_OK && data != null) {
+                @Suppress("DEPRECATION")
+                val hmsScan = data.getParcelableExtra<HmsScan>(ScanUtil.RESULT)
+                if (hmsScan != null) {
+                    val intent = Intent().apply {
+                        putExtra(EXTRA_RESULT, hmsScan.originalValue)
+                    }
+                    setResult(Activity.RESULT_OK, intent)
+                }
+            }
+            finish()
+        }
+    }
+}

+ 57 - 0
capability-qrcode/src/main/java/com/narutohuo/xindazhou/qrcode/ui/QRCodeScanFragment.kt

@@ -0,0 +1,57 @@
+package com.narutohuo.xindazhou.qrcode.ui
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.fragment.app.Fragment
+
+/**
+ * 用于桥接 ActivityResultLauncher 的透明 Fragment
+ * 
+ * 这个 Fragment 不显示任何 UI,只用于处理 Activity Result
+ */
+class QRCodeScanFragment : Fragment() {
+    
+    private var launcher: ActivityResultLauncher<Intent>? = null
+    private var callback: ((String?) -> Unit)? = null
+    
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        
+        launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+            val data = result.data
+            val scanResult = if (result.resultCode == android.app.Activity.RESULT_OK && data != null) {
+                data.getStringExtra(QRCodeScanActivity.EXTRA_RESULT)
+            } else {
+                null
+            }
+            callback?.invoke(scanResult)
+            // 使用完后移除 Fragment
+            parentFragmentManager.beginTransaction().remove(this).commit()
+        }
+    }
+    
+    fun startScan(intent: Intent, callback: (String?) -> Unit) {
+        this.callback = callback
+        launcher?.launch(intent)
+    }
+    
+    companion object {
+        private const val TAG = "QRCodeScanFragment"
+        
+        /**
+         * 在 Activity 中添加 Fragment 并启动扫码
+         */
+        fun startScan(activity: androidx.fragment.app.FragmentActivity, callback: (String?) -> Unit) {
+            val fragment = QRCodeScanFragment()
+            activity.supportFragmentManager.beginTransaction()
+                .add(fragment, TAG)
+                .commitNow()
+            
+            val intent = Intent(activity, QRCodeScanActivity::class.java)
+            fragment.startScan(intent, callback)
+        }
+    }
+}
+

+ 11 - 0
capability-qrcode/src/main/res/drawable/ic_back_white.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FFFFFF"
+        android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
+</vector>
+

+ 26 - 0
capability-qrcode/src/main/res/drawable/scan_corner.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- 左上角 -->
+    <item>
+        <shape>
+            <solid android:color="#00FFFFFF" />
+        </shape>
+    </item>
+    <!-- 上边线 -->
+    <item android:bottom="36dp">
+        <shape>
+            <stroke
+                android:width="4dp"
+                android:color="#00E5FF" />
+        </shape>
+    </item>
+    <!-- 左边线 -->
+    <item android:right="36dp">
+        <shape>
+            <stroke
+                android:width="4dp"
+                android:color="#00E5FF" />
+        </shape>
+    </item>
+</layer-list>
+

+ 8 - 0
capability-qrcode/src/main/res/drawable/scan_frame_bg.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#00000000" />
+    <stroke
+        android:width="1dp"
+        android:color="#33FFFFFF" />
+</shape>
+

+ 9 - 0
capability-qrcode/src/main/res/drawable/scan_line.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:startColor="#00FFFFFF"
+        android:centerColor="#FFFFFFFF"
+        android:endColor="#00FFFFFF"
+        android:angle="0" />
+</shape>
+

+ 5 - 0
capability-qrcode/src/main/res/drawable/scan_mask.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#80000000" />
+</shape>
+

+ 5 - 0
capability-qrcode/src/main/res/drawable/scan_mask_side.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#80000000" />
+</shape>
+

+ 121 - 0
capability-qrcode/src/main/res/layout/activity_qrcode_scan.xml

@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#000000">
+
+    <!-- 相机预览区域(RemoteView会添加到这里) -->
+    <FrameLayout
+        android:id="@+id/scan_frame"
+        android:layout_width="280dp"
+        android:layout_height="280dp"
+        android:layout_gravity="center"
+        android:background="@drawable/scan_frame_bg" />
+
+    <!-- 扫描线 -->
+    <ImageView
+        android:id="@+id/scan_line"
+        android:layout_width="280dp"
+        android:layout_height="3dp"
+        android:layout_gravity="center"
+        android:src="@drawable/scan_line"
+        android:contentDescription="扫描线" />
+
+    <!-- 四个角的装饰 -->
+    <View
+        android:id="@+id/corner_top_left"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:layout_gravity="center"
+        android:layout_marginTop="-140dp"
+        android:layout_marginStart="-140dp"
+        android:background="@drawable/scan_corner" />
+
+    <View
+        android:id="@+id/corner_top_right"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:layout_gravity="center"
+        android:layout_marginTop="-140dp"
+        android:layout_marginEnd="-140dp"
+        android:rotation="90"
+        android:background="@drawable/scan_corner" />
+
+    <View
+        android:id="@+id/corner_bottom_left"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:layout_gravity="center"
+        android:layout_marginBottom="-140dp"
+        android:layout_marginStart="-140dp"
+        android:rotation="270"
+        android:background="@drawable/scan_corner" />
+
+    <View
+        android:id="@+id/corner_bottom_right"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:layout_gravity="center"
+        android:layout_marginBottom="-140dp"
+        android:layout_marginEnd="-140dp"
+        android:rotation="180"
+        android:background="@drawable/scan_corner" />
+
+    <!-- 提示文字 -->
+    <TextView
+        android:id="@+id/hint_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal|bottom"
+        android:layout_marginBottom="120dp"
+        android:text="将二维码放入框内,即可自动扫描"
+        android:textColor="#FFFFFF"
+        android:textSize="14sp"
+        android:shadowColor="#80000000"
+        android:shadowDx="0"
+        android:shadowDy="1"
+        android:shadowRadius="2" />
+
+    <!-- 返回按钮 -->
+    <ImageView
+        android:id="@+id/btn_back"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_gravity="top|start"
+        android:layout_marginTop="16dp"
+        android:layout_marginStart="16dp"
+        android:src="@drawable/ic_back_white"
+        android:background="?attr/selectableItemBackgroundBorderless"
+        android:contentDescription="返回"
+        android:padding="12dp" />
+
+    <!-- 顶部遮罩 -->
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_gravity="top"
+        android:background="@drawable/scan_mask" />
+
+    <!-- 底部遮罩 -->
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_gravity="bottom"
+        android:background="@drawable/scan_mask" />
+
+    <!-- 左侧遮罩 -->
+    <View
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"
+        android:background="@drawable/scan_mask_side" />
+
+    <!-- 右侧遮罩 -->
+    <View
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="end"
+        android:background="@drawable/scan_mask_side" />
+
+</FrameLayout>
+

+ 7 - 0
capability-qrcode/src/main/res/values/colors.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="scan_corner_color">#00E5FF</color>
+    <color name="scan_mask_color">#80000000</color>
+    <color name="scan_hint_text_color">#FFFFFF</color>
+</resources>
+

+ 10 - 0
capability-qrcode/src/main/res/values/strings.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">QRCode</string>
+    <string name="scan_hint">将二维码放入框内,即可自动扫描</string>
+    <string name="camera_permission_required">需要相机权限才能扫码</string>
+    <string name="scan_init_failed">初始化扫码失败</string>
+    <string name="scan_canceled">用户取消扫码</string>
+    <string name="scan_result_empty">扫码结果为空</string>
+</resources>
+

+ 5 - 1
capability-socketio/build.gradle

@@ -22,10 +22,14 @@ android {
 
 dependencies {
     // 直接依赖 base-core(能力层只需要 base-core 的接口和基础能力)
-    // base-core 提供接口定义和基础实现(ILog、NetworkManager
+    // base-core 提供接口定义和基础实现(ILog、StorageImpl
     // base-core 会传递 Gson、Coroutines、ILog 等依赖
     implementation(project(":base-core"))
     
+    // AndroidX Lifecycle(用于 ProcessLifecycleOwner)
+    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+    implementation("androidx.lifecycle:lifecycle-process:2.7.0")
+    
     // SocketIO客户端库
     // Maven Central最新版本:2.1.2(Java/Android客户端最高版本)
     // netty-socketio 2.0.13 支持 Socket.IO 2.x 客户端

+ 103 - 51
capability-socketio/src/main/java/com/narutohuo/xindazhou/socketio/impl/SocketIOServiceImpl.kt

@@ -52,6 +52,10 @@ class SocketIOServiceImpl private constructor() : SocketIOService {
             ILog.d("SocketIOService", "=== SocketIO连接开始 ===")
             ILog.d("SocketIOService", "服务器地址: $serverUrl")
             ILog.d("SocketIOService", "Token长度: ${token.length}")
+            ILog.d("SocketIOService", "当前已保存的监听器数量: ${eventListeners.size} 个事件类型")
+            eventListeners.forEach { (event, callbacks) ->
+                ILog.d("SocketIOService", "  - $event: ${callbacks.size} 个回调")
+            }
             
             // 如果已连接,先断开
             if (socket?.connected() == true) {
@@ -75,41 +79,27 @@ class SocketIOServiceImpl private constructor() : SocketIOService {
             
             ILog.d("SocketIOService", "创建Socket实例...")
             socket = IO.socket(serverUrl, options)
+            ILog.d("SocketIOService", "Socket实例创建完成,socket != null: ${socket != null}")
             
-            // 注册所有已保存的事件监听器
-            eventListeners.forEach { (event, callbacks) ->
-                callbacks.forEach { callback ->
-                    socket?.on(event) { args ->
-                        try {
-                            // Socket.IO Java 客户端库接收数据时,服务端发送的 Map 会被自动序列化为 JSONObject
-                            // 统一转换为 JSON 字符串格式,符合 SocketIOResponse.data 的类型定义
-                            val data = if (args.isNotEmpty()) {
-                                when (val arg = args[0]) {
-                                    is String -> arg
-                                    is JSONObject -> arg.toString() // JSONObject.toString() 返回标准 JSON 字符串
-                                    else -> Gson().toJson(arg)
-                                }
-                            } else {
-                                "{}"
-                            }
-                            
-                            val response = SocketIOResponse(
-                                event = event,
-                                data = data,
-                                timestamp = System.currentTimeMillis()
-                            )
-                            
-                            callback(response)
-                        } catch (e: Exception) {
-                            ILog.e("SocketIOService", "Error handling event: $event", e)
-                        }
-                    }
-                }
+            // 预先注册所有已保存的事件监听器(连接前注册,连接成功后会在 EVENT_CONNECT 中再次注册确保正确)
+            // 注意:即使这里注册了,连接成功后也会再次注册,确保所有监听器都正确注册
+            ILog.d("SocketIOService", "connect: 预先注册 ${eventListeners.size} 个事件类型的监听器...")
+            if (eventListeners.isEmpty()) {
+                ILog.w("SocketIOService", "⚠️ 警告:eventListeners 为空,没有监听器需要注册!")
+            } else {
+                registerAllEventListeners(requireConnected = false)
             }
             
             // 添加连接事件监听(触发已注册的回调)
+            // 关键:参考 iOS 实现,在连接成功后再次注册所有监听器,确保所有监听器都已注册
             socket?.on(Socket.EVENT_CONNECT) {
                 ILog.d("SocketIOService", "✅ SocketIO连接成功")
+                
+                // 关键:连接成功后,再次注册所有已保存的事件监听器
+                // 这样可以确保即使监听器是在连接之后才注册的,也能正确工作
+                ILog.d("SocketIOService", "连接成功:再次注册所有事件监听器(${eventListeners.size} 个事件类型)...")
+                registerAllEventListeners(requireConnected = true)
+                
                 // 触发已注册的 "connect" 事件回调
                 val callbacks = eventListeners["connect"] ?: return@on
                 val response = SocketIOResponse(
@@ -168,32 +158,38 @@ class SocketIOServiceImpl private constructor() : SocketIOService {
     override fun on(event: String, callback: (SocketIOResponse) -> Unit) {
         // 保存事件监听器
         eventListeners.getOrPut(event) { mutableListOf() }.add(callback)
+        ILog.d("SocketIOService", "on: 保存事件监听器: $event (当前该事件有 ${eventListeners[event]?.size} 个监听器)")
         
-        // 如果socket已创建,立即注册
-        socket?.on(event) { args ->
-            try {
-                // Socket.IO Java 客户端库接收数据时,服务端发送的 Map 会被自动序列化为 JSONObject
-                // 统一转换为 JSON 字符串格式,符合 SocketIOResponse.data 的类型定义
-                val data = if (args.isNotEmpty()) {
-                    when (val arg = args[0]) {
-                        is String -> arg
-                        is JSONObject -> arg.toString() // JSONObject.toString() 返回标准 JSON 字符串
-                        else -> Gson().toJson(arg)
+        // 如果socket已连接,立即注册监听器
+        if (socket != null && socket!!.connected()) {
+            ILog.d("SocketIOService", "on: Socket 已连接,立即注册事件监听器: $event")
+            // 注册新的监听器(Socket.IO 的 on() 会添加监听器,不会替换)
+            socket?.on(event) { args ->
+                try {
+                    val data = if (args.isNotEmpty()) {
+                        when (val arg = args[0]) {
+                            is String -> arg
+                            is JSONObject -> arg.toString()
+                            else -> Gson().toJson(arg)
+                        }
+                    } else {
+                        "{}"
                     }
-                } else {
-                    "{}"
+                    
+                    val response = SocketIOResponse(
+                        event = event,
+                        data = data,
+                        timestamp = System.currentTimeMillis()
+                    )
+                    
+                    ILog.d("SocketIOService", "收到事件: $event, data=${response.data}")
+                    callback(response)
+                } catch (e: Exception) {
+                    ILog.e("SocketIOService", "Error handling event: $event", e)
                 }
-                
-                val response = SocketIOResponse(
-                    event = event,
-                    data = data,
-                    timestamp = System.currentTimeMillis()
-                )
-                
-                callback(response)
-            } catch (e: Exception) {
-                ILog.e("SocketIOService", "Error handling event: $event", e)
             }
+        } else {
+            ILog.d("SocketIOService", "on: Socket 未连接,监听器已保存,将在连接成功后自动注册")
         }
     }
     
@@ -228,5 +224,61 @@ class SocketIOServiceImpl private constructor() : SocketIOService {
             ILog.e("SocketIOService", "Error emitting event: $event", e)
         }
     }
+    
+    // ========== 内部方法 ==========
+    
+    /**
+     * 注册所有已保存的事件监听器
+     * 
+     * 参考 iOS 实现:在连接成功后调用此方法,确保所有监听器都已注册
+     * 
+     * @param requireConnected 是否要求 socket 已连接(连接前注册时设为 false,连接成功后注册时设为 true)
+     */
+    private fun registerAllEventListeners(requireConnected: Boolean = true) {
+        if (socket == null) {
+            ILog.w("SocketIOService", "registerAllEventListeners: Socket 为 null,无法注册监听器")
+            return
+        }
+        
+        if (requireConnected && !socket!!.connected()) {
+            ILog.w("SocketIOService", "registerAllEventListeners: Socket 未连接,无法注册监听器")
+            return
+        }
+        
+        ILog.d("SocketIOService", "registerAllEventListeners: 开始注册 ${eventListeners.size} 个事件类型的监听器...")
+        eventListeners.forEach { (event, callbacks) ->
+            ILog.d("SocketIOService", "registerAllEventListeners: 注册事件监听器: $event (${callbacks.size} 个回调)")
+            // 关键:先移除该事件的所有旧监听器,避免重复注册
+            socket?.off(event)
+            // 然后为每个回调注册监听器
+            callbacks.forEach { callback ->
+                socket?.on(event) { args ->
+                    try {
+                        val data = if (args.isNotEmpty()) {
+                            when (val arg = args[0]) {
+                                is String -> arg
+                                is JSONObject -> arg.toString()
+                                else -> Gson().toJson(arg)
+                            }
+                        } else {
+                            "{}"
+                        }
+                        
+                        val response = SocketIOResponse(
+                            event = event,
+                            data = data,
+                            timestamp = System.currentTimeMillis()
+                        )
+                        
+                        ILog.d("SocketIOService", "收到事件: $event, data=${response.data}")
+                        callback(response)
+                    } catch (e: Exception) {
+                        ILog.e("SocketIOService", "Error handling event: $event", e)
+                    }
+                }
+            }
+        }
+        ILog.d("SocketIOService", "registerAllEventListeners: 所有事件监听器注册完成")
+    }
 }
 

+ 177 - 0
capability-socketio/src/main/java/com/narutohuo/xindazhou/socketio/manager/SocketIOManager.kt

@@ -0,0 +1,177 @@
+package com.narutohuo.xindazhou.socketio.manager
+
+import android.app.Application
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import com.narutohuo.xindazhou.core.log.ILog
+import com.narutohuo.xindazhou.core.storage.StorageImpl
+import com.narutohuo.xindazhou.socketio.factory.SocketIORepositoryFactory
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ * SocketIO 连接管理器
+ * 
+ * 负责自动管理 SocketIO 连接的生命周期:
+ * - App 进入前台时自动重连
+ * - Token 过期时自动刷新并重连
+ * - 监听应用生命周期
+ * 
+ * 使用方式:
+ * ```kotlin
+ * // 在 Application.onCreate() 中初始化
+ * SocketIOManager.init(application)
+ * ```
+ */
+object SocketIOManager : DefaultLifecycleObserver {
+    
+    private const val TAG = "SocketIOManager"
+    
+    private var application: Application? = null
+    private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+    private var initialized = false
+    
+    /**
+     * 登录状态检查回调(由业务层设置)
+     * 避免能力层直接依赖业务层的 AuthManager
+     */
+    var isLoggedInProvider: (() -> Boolean)? = null
+    
+    /**
+     * Token 刷新回调(由业务层设置)
+     * 避免能力层直接依赖业务层的 AuthManager
+     * 注意:这是一个 suspend 函数,因为 Token 刷新是异步操作
+     */
+    var refreshTokenProvider: (suspend () -> String?)? = null
+    
+    /**
+     * 初始化 SocketIO 管理器
+     * 
+     * 需要在 Application.onCreate() 中调用
+     * 会自动注册生命周期监听
+     * 
+     * @param app Application 实例
+     */
+    fun init(app: Application) {
+        if (initialized) {
+            ILog.w(TAG, "SocketIOManager 已初始化,跳过重复初始化")
+            return
+        }
+        
+        application = app
+        initialized = true
+        
+        // 注册 App 生命周期监听(直接依赖,不使用反射)
+        try {
+            ProcessLifecycleOwner.get().lifecycle.addObserver(this)
+            ILog.d(TAG, "SocketIOManager 初始化完成(生命周期监听已注册)")
+        } catch (e: Exception) {
+            ILog.e(TAG, "注册生命周期监听失败", e)
+        }
+    }
+    
+    /**
+     * 设置回调函数
+     * 
+     * 由业务层调用,设置登录状态检查和 Token 刷新回调
+     * 可以在 init 之后调用,也可以随时更新
+     * 
+     * 关键:同时设置到 SocketIORepository,这样 SocketIORepository 内部可以自动刷新 Token
+     * 
+     * @param isLoggedInProvider 登录状态检查回调(可选)
+     * @param refreshTokenProvider Token 刷新回调(可选)
+     */
+    fun setCallbacks(
+        isLoggedInProvider: (() -> Boolean)? = null,
+        refreshTokenProvider: (suspend () -> String?)? = null
+    ) {
+        if (isLoggedInProvider != null) {
+            this.isLoggedInProvider = isLoggedInProvider
+            ILog.d(TAG, "登录状态检查回调已设置")
+        }
+        if (refreshTokenProvider != null) {
+            this.refreshTokenProvider = refreshTokenProvider
+            // 关键:同时设置到 SocketIORepository,让 SocketIORepository 内部自动处理 Token 刷新
+            com.narutohuo.xindazhou.socketio.repository.SocketIORepository.refreshTokenProvider = refreshTokenProvider
+            ILog.d(TAG, "Token 刷新回调已设置(已同步到 SocketIORepository)")
+        }
+    }
+    
+    /**
+     * App 进入前台时调用
+     * 
+     * 检查 SocketIO 连接状态,如果断开则自动重连
+     * 如果连接失败(可能是 Token 过期),会自动刷新 Token 后重连
+     */
+    override fun onStart(owner: LifecycleOwner) {
+        ILog.d(TAG, "App 进入前台,检查 SocketIO 连接状态")
+        
+        // 检查是否已登录(通过回调函数,避免直接依赖 AuthManager)
+        if (isLoggedInProvider?.invoke() != true) {
+            ILog.d(TAG, "用户未登录,跳过 SocketIO 重连")
+            return
+        }
+        
+        // 检查连接状态,如果断开则重连
+        appScope.launch {
+            try {
+                // 直接获取 SocketIORepository(不使用反射)
+                val socketIORepository = SocketIORepositoryFactory.getInstance()
+                
+                // 检查是否已连接
+                if (socketIORepository.isConnected()) {
+                    ILog.d(TAG, "SocketIO 已连接,无需重连")
+                    return@launch
+                }
+                
+                // 检查 token 是否存在
+                val token = StorageImpl.getString("access_token")
+                if (token.isEmpty()) {
+                    ILog.w(TAG, "Token 为空,无法重连 SocketIO")
+                    return@launch
+                }
+                
+                // 先尝试连接(不传参数,让它从存储中获取)
+                ILog.d(TAG, "SocketIO 未连接,开始重连...")
+                socketIORepository.checkAndReconnect(null, null)
+                
+                // 等待 2 秒,检查连接是否成功
+                delay(2000)
+                
+                // 如果连接仍然失败,可能是 Token 过期,尝试刷新 Token 后重连
+                if (!socketIORepository.isConnected()) {
+                    ILog.d(TAG, "连接失败,可能是 Token 过期,尝试刷新 Token 后重连")
+                    val refreshedToken = refreshTokenProvider?.invoke() // 在协程作用域中调用 suspend 函数
+                    if (!refreshedToken.isNullOrEmpty() && refreshedToken != token) {
+                        ILog.d(TAG, "Token 已刷新,使用新 Token 重连")
+                        socketIORepository.checkAndReconnect(null, null)
+                    } else {
+                        ILog.w(TAG, "Token 刷新失败或未刷新,无法重连")
+                    }
+                } else {
+                    ILog.d(TAG, "SocketIO 重连成功")
+                }
+            } catch (e: Exception) {
+                ILog.e(TAG, "SocketIO 重连失败", e)
+            }
+        }
+    }
+    
+    /**
+     * App 进入后台时调用
+     * 
+     * 注意:通常不断开 SocketIO 连接,因为:
+     * 1. SocketIO 连接是轻量级的
+     * 2. 用户可能需要在后台接收消息
+     * 3. 系统会在内存不足时自动清理
+     */
+    override fun onStop(owner: LifecycleOwner) {
+        ILog.d(TAG, "App 进入后台")
+        // 不断开连接,保持连接以便接收消息
+    }
+}
+

+ 273 - 20
capability-socketio/src/main/java/com/narutohuo/xindazhou/socketio/repository/SocketIORepository.kt

@@ -1,6 +1,7 @@
 package com.narutohuo.xindazhou.socketio.repository
 
 import com.narutohuo.xindazhou.core.log.ILog
+import com.narutohuo.xindazhou.core.storage.StorageImpl
 import com.narutohuo.xindazhou.socketio.api.SocketIOService
 import com.narutohuo.xindazhou.socketio.factory.SocketIOServiceFactory
 import com.narutohuo.xindazhou.socketio.model.SocketIOEvent
@@ -12,12 +13,18 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.delay
 
 /**
  * SocketIO 数据仓库
  * 
  * 负责统一管理 SocketIO 连接和数据获取
  * 完全封装在 capability-socketio 模块内部,app 模块通过 Factory 获取实例
+ * 
+ * 自动处理 Token 刷新:
+ * - 连接前自动刷新 Token(如果需要)
+ * - 连接失败时自动刷新 Token 并重连
+ * - 断开连接时,如果是 Token 过期导致,自动刷新并重连
  */
 class SocketIORepository(
     private val socketService: SocketIOService = SocketIOServiceFactory.getInstance()
@@ -25,6 +32,16 @@ class SocketIORepository(
     
     companion object {
         private const val TAG = "SocketIORepository"
+        
+        // AuthManager 的类名(通过反射调用,避免直接依赖 base-common)
+        private const val AUTH_MANAGER_CLASS = "com.narutohuo.xindazhou.common.auth.AuthManager"
+        
+        // refreshTokenIfNeeded 方法名
+        private const val REFRESH_TOKEN_METHOD = "refreshTokenIfNeeded"
+        
+        // Token 刷新回调(由 SocketIOManager 设置)
+        @Volatile
+        var refreshTokenProvider: (suspend () -> String?)? = null
     }
     
     // 使用 extraBufferCapacity 确保事件不丢失
@@ -43,6 +60,22 @@ class SocketIORepository(
     // 用于在回调中发送事件的协程作用域
     private val eventScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
     
+    // 用于防止重复刷新 Token 的标记(避免短时间内多次刷新)
+    @Volatile
+    private var isRefreshingToken = false
+    
+    // 记录最近一次 Token 刷新的时间(毫秒)
+    @Volatile
+    private var lastTokenRefreshTime = 0L
+    
+    // 用于防止重复重连的标记(避免断开后多次触发重连)
+    @Volatile
+    private var isReconnecting = false
+    
+    // 记录最近一次重连的时间(毫秒)
+    @Volatile
+    private var lastReconnectTime = 0L
+    
     init {
         subscribeEvents()
     }
@@ -50,17 +83,49 @@ class SocketIORepository(
     /**
      * 连接 SocketIO
      * 
-     * @param serverUrl SocketIO 服务器地址
-     * @param token JWT Token(用于身份验证)
+     * 自动处理 Token 刷新:
+     * 1. 连接前先尝试刷新 Token(如果提供了刷新回调)
+     * 2. 使用最新的 Token 连接
+     * 
+     * 关键:即使传入了 token,如果提供了刷新回调,也会先刷新一下
+     * 这样可以确保使用最新的、未过期的 Token
+     * 
+     * @param serverUrl SocketIO 服务器地址(如果为 null,则从存储中获取)
+     * @param token JWT Token(如果为 null,则从存储中获取或刷新)
      */
-    suspend fun connect(serverUrl: String, token: String): Result<Unit> {
+    suspend fun connect(serverUrl: String? = null, token: String? = null): Result<Unit> {
         return try {
-            if (token.isNullOrEmpty()) {
-                return Result.failure(Exception("未登录,无法获取Token"))
+            val finalServerUrl = serverUrl ?: StorageImpl.getString("socket_url")
+            var finalToken = token
+            
+            if (finalServerUrl.isEmpty()) {
+                return Result.failure(Exception("服务器地址未配置,无法连接"))
+            }
+            
+            // 关键:如果提供了 Token 刷新回调,连接前总是先刷新 Token(确保使用最新的、未过期的 Token)
+            // AuthManager.refreshTokenIfNeeded 会判断是否需要刷新,如果 Token 未过期则返回当前 Token
+            if (refreshTokenProvider != null) {
+                ILog.d(TAG, "connect: 连接前自动刷新 Token(如果需要)...")
+                val refreshedToken = tryRefreshToken()
+                if (!refreshedToken.isNullOrEmpty()) {
+                    finalToken = refreshedToken
+                    ILog.d(TAG, "connect: Token 已刷新/确认,新 Token 长度: ${refreshedToken.length}")
+                } else {
+                    // 如果刷新失败,使用传入的 token 或存储中的 token
+                    finalToken = token ?: StorageImpl.getString("access_token")
+                    ILog.w(TAG, "connect: Token 刷新失败,使用原有 Token")
+                }
+            } else {
+                // 如果没有提供刷新回调,使用传入的 token 或存储中的 token
+                finalToken = token ?: StorageImpl.getString("access_token")
+            }
+            
+            if (finalToken.isNullOrEmpty()) {
+                return Result.failure(Exception("未登录(Token 为空),无法连接"))
             }
             
-            ILog.d(TAG, "connect: 正在连接 $serverUrl")
-            socketService.connect(serverUrl, token)
+            ILog.d(TAG, "connect: 正在连接 $finalServerUrl, Token 长度: ${finalToken.length}")
+            socketService.connect(finalServerUrl, finalToken)
             Result.success(Unit)
         } catch (e: Exception) {
             ILog.e(TAG, "connect: 连接失败", e)
@@ -93,12 +158,13 @@ class SocketIORepository(
      * 检查连接状态,如果断开则自动重连
      * 
      * 用于 App 进入前台时自动重连的场景
+     * 自动处理 Token 刷新:如果连接失败,会自动尝试刷新 Token 后重连
      * 
-     * @param serverUrl SocketIO 服务器地址
-     * @param token JWT Token(用于身份验证
-     * @return 如果已连接返回 true,如果需要重连则返回 false(重连是异步的)
+     * @param serverUrl SocketIO 服务器地址(如果为 null,则从存储中获取)
+     * @param token JWT Token(如果为 null,则从存储中获取或刷新
+     * @return 如果已连接返回 true,如果正在重连返回 false(重连是异步的)
      */
-    suspend fun checkAndReconnect(serverUrl: String, token: String): Boolean {
+    suspend fun checkAndReconnect(serverUrl: String? = null, token: String? = null): Boolean {
         return if (socketService.isConnected()) {
             // 已连接,无需重连
             ILog.d(TAG, "checkAndReconnect: 已连接,无需重连")
@@ -106,18 +172,178 @@ class SocketIORepository(
         } else {
             // 未连接,尝试重连
             ILog.d(TAG, "checkAndReconnect: 未连接,开始重连...")
-            connect(serverUrl, token)
-                .onSuccess {
+            
+            // 第一次尝试连接(connect 方法内部会自动刷新 Token 如果需要)
+            val result = connect(serverUrl, token)
+            
+            if (result.isSuccess) {
+                // 等待 2 秒,检查连接是否真正成功
+                delay(2000)
+                
+                if (socketService.isConnected()) {
                     ILog.d(TAG, "checkAndReconnect: 重连成功")
+                    return true
+                } else {
+                    // 连接失败,可能是 Token 过期,尝试刷新 Token 后重连
+                    ILog.w(TAG, "checkAndReconnect: 连接后仍未成功,可能是 Token 过期,尝试刷新 Token...")
                 }
-                .onFailure { e ->
-                    ILog.e(TAG, "checkAndReconnect: 重连失败", e)
+            } else {
+                // 连接失败,可能是 Token 过期
+                ILog.w(TAG, "checkAndReconnect: 首次连接失败: ${result.exceptionOrNull()?.message}")
+            }
+            
+            // 第二次尝试:刷新 Token 后重连(如果提供了刷新回调)
+            if (refreshTokenProvider != null) {
+                val refreshedToken = tryRefreshToken()
+                if (!refreshedToken.isNullOrEmpty()) {
+                    ILog.d(TAG, "checkAndReconnect: Token 已刷新,使用新 Token 重连...")
+                    val retryResult = connect(serverUrl, refreshedToken)
+                    if (retryResult.isSuccess) {
+                        delay(2000)
+                        if (socketService.isConnected()) {
+                            ILog.d(TAG, "checkAndReconnect: 刷新 Token 后重连成功")
+                            return true
+                        }
+                    }
                 }
+            }
+            
+            ILog.e(TAG, "checkAndReconnect: 重连失败(已尝试刷新 Token)")
             false
         }
     }
     
     /**
+     * 处理自动重连逻辑(统一处理 disconnect 和 connect_error 事件)
+     * 
+     * 关键:这是自动重连机制,完全在 capability-socketio 内部处理
+     * 如果连接断开,自动刷新 Token 并重连(如果提供了刷新回调)
+     * 
+     * @param source 触发来源(用于日志记录)
+     */
+    private suspend fun handleAutoReconnect(source: String) {
+        // 防重复重连:如果正在重连或刚重连过(5秒内),跳过
+        val currentTime = System.currentTimeMillis()
+        if (isReconnecting || (currentTime - lastReconnectTime < 5000)) {
+            ILog.d(TAG, "$source: 正在重连或刚重连过(${(currentTime - lastReconnectTime) / 1000}秒前),跳过自动重连")
+            return
+        }
+        
+        // 如果没有提供刷新回调,无法自动重连
+        if (refreshTokenProvider == null) {
+            ILog.d(TAG, "$source: 未提供 Token 刷新回调,无法自动重连")
+            return
+        }
+        
+        // 如果已经连接,无需重连
+        if (socketService.isConnected()) {
+            ILog.d(TAG, "$source: 已连接,无需重连")
+            return
+        }
+        
+        // 延迟 2 秒后尝试重连(给服务端一些时间,避免立即重连导致问题)
+        delay(2000)
+        
+        // 再次检查连接状态(可能在此期间已经连接了)
+        if (socketService.isConnected()) {
+            ILog.d(TAG, "$source: 延迟期间已连接,跳过重连")
+            return
+        }
+        
+        isReconnecting = true
+        lastReconnectTime = System.currentTimeMillis()
+        
+        try {
+            ILog.d(TAG, "$source: 检测到断开连接,尝试刷新 Token 并重连...")
+            val refreshedToken = tryRefreshToken()
+            if (!refreshedToken.isNullOrEmpty()) {
+                // 使用新 Token 重连
+                val result = connect(null, refreshedToken)
+                if (result.isSuccess) {
+                    // 等待 2 秒,确认连接是否真正成功
+                    delay(2000)
+                    if (socketService.isConnected()) {
+                        ILog.d(TAG, "$source: 刷新 Token 后重连成功 ✅")
+                        _logMessage.tryEmit("🔄 自动刷新 Token 并重连成功")
+                    } else {
+                        ILog.w(TAG, "$source: 刷新 Token 后重连失败(连接未建立)")
+                    }
+                } else {
+                    ILog.w(TAG, "$source: 刷新 Token 后重连失败: ${result.exceptionOrNull()?.message}")
+                }
+            } else {
+                ILog.w(TAG, "$source: Token 刷新失败,无法重连")
+            }
+        } catch (e: Exception) {
+            ILog.e(TAG, "$source: 自动重连异常", e)
+        } finally {
+            // 延迟重置标记,避免短时间内重复重连
+            delay(3000)
+            isReconnecting = false
+        }
+    }
+    
+    /**
+     * 尝试刷新 Token(带防重复刷新机制)
+     * 
+     * 关键:即使 Token 未过期,也会调用刷新回调(由 AuthManager 判断是否需要刷新)
+     * AuthManager.refreshTokenIfNeeded 会判断 Token 是否过期,如果未过期则返回当前 Token
+     * 
+     * @return 新的 Access Token,如果刷新失败返回 null
+     */
+    private suspend fun tryRefreshToken(): String? {
+        // 防止短时间内多次刷新(避免频繁请求,5秒内最多刷新一次)
+        val currentTime = System.currentTimeMillis()
+        if (isRefreshingToken) {
+            ILog.d(TAG, "tryRefreshToken: 正在刷新中,等待...")
+            // 等待刷新完成,最多等待 3 秒
+            var waitCount = 0
+            while (isRefreshingToken && waitCount < 30) {
+                delay(100)
+                waitCount++
+            }
+            // 刷新完成后,从存储中获取最新的 Token
+            return StorageImpl.getString("access_token").takeIf { it.isNotEmpty() }
+        }
+        
+        if (currentTime - lastTokenRefreshTime < 5000) {
+            ILog.d(TAG, "tryRefreshToken: 刚刷新过(${(currentTime - lastTokenRefreshTime) / 1000}秒前),使用存储中的 Token")
+            return StorageImpl.getString("access_token").takeIf { it.isNotEmpty() }
+        }
+        
+        return try {
+            isRefreshingToken = true
+            lastTokenRefreshTime = currentTime
+            
+            ILog.d(TAG, "tryRefreshToken: 开始刷新 Token(如果需要)...")
+            val newToken = refreshTokenProvider?.invoke()
+            
+            if (!newToken.isNullOrEmpty()) {
+                ILog.d(TAG, "tryRefreshToken: Token 刷新成功,新 Token 长度: ${newToken.length}")
+                // 注意:Token 已经由 AuthManager.refreshTokenIfNeeded 保存到 StorageImpl,这里不需要再次保存
+                // 但为了确保一致性,我们可以从 StorageImpl 重新读取
+                val storedToken = StorageImpl.getString("access_token")
+                if (storedToken.isNotEmpty() && storedToken == newToken) {
+                    ILog.d(TAG, "tryRefreshToken: Token 已保存到存储,验证一致")
+                } else {
+                    ILog.w(TAG, "tryRefreshToken: 警告:存储中的 Token 与刷新返回的 Token 不一致")
+                }
+                newToken
+            } else {
+                ILog.w(TAG, "tryRefreshToken: Token 刷新失败(返回 null),尝试使用存储中的 Token")
+                // 刷新失败,尝试使用存储中的 Token
+                StorageImpl.getString("access_token").takeIf { it.isNotEmpty() }
+            }
+        } catch (e: Exception) {
+            ILog.e(TAG, "tryRefreshToken: Token 刷新异常", e)
+            // 刷新异常,尝试使用存储中的 Token
+            StorageImpl.getString("access_token").takeIf { it.isNotEmpty() }
+        } finally {
+            isRefreshingToken = false
+        }
+    }
+    
+    /**
      * 发送车辆控制指令
      */
     suspend fun sendVehicleControl(command: String, vehicleId: Long): Result<Unit> {
@@ -144,6 +370,8 @@ class SocketIORepository(
      * 订阅 SocketIO 事件
      */
     private fun subscribeEvents() {
+        ILog.d(TAG, "subscribeEvents: 开始注册事件监听器...")
+        
         // 订阅连接成功事件
         socketService.on(SocketIOEvent.CONNECT) { response ->
             ILog.d(TAG, "onConnect - 触发连接状态更新")
@@ -156,30 +384,55 @@ class SocketIORepository(
         }
         
         // 订阅断开连接事件
+        // 关键:断开连接时,自动刷新 Token 并重连(如果是 Token 过期导致)
+        // 完全在 capability-socketio 内部自动处理,不需要外部干预
         socketService.on(SocketIOEvent.DISCONNECT) { response ->
-            ILog.d(TAG, "onDisconnect")
+            val disconnectReason = response.data
+            ILog.d(TAG, "onDisconnect: reason=$disconnectReason")
+            
             eventScope.launch {
                 _connectionState.emit(false)
             }
-            _logMessage.tryEmit("❌ 断开连接")
+            _logMessage.tryEmit("❌ 断开连接: $disconnectReason")
+            
+            // 如果提供了 Token 刷新回调,且未连接,尝试刷新 Token 并重连
+            // 关键:这里是自动重连机制,完全在 capability-socketio 内部处理
+            eventScope.launch(Dispatchers.IO) {
+                handleAutoReconnect("onDisconnect")
+            }
         }
         
         // 订阅连接错误事件
+        // 关键:连接错误时,自动刷新 Token 并重连(可能是 Token 过期)
+        // 完全在 capability-socketio 内部自动处理,不需要外部干预
         socketService.on(SocketIOEvent.CONNECT_ERROR) { response ->
-            ILog.e(TAG, "onConnectError: ${response.data}")
+            val errorMsg = response.data
+            ILog.e(TAG, "onConnectError: $errorMsg")
+            
             eventScope.launch {
                 _connectionState.emit(false)
             }
-            _logMessage.tryEmit("❌ 连接错误:${response.data}")
+            _logMessage.tryEmit("❌ 连接错误:$errorMsg")
+            
+            // 如果提供了 Token 刷新回调,且未连接,尝试刷新 Token 并重连
+            // 关键:这里是自动重连机制,完全在 capability-socketio 内部处理
+            eventScope.launch(Dispatchers.IO) {
+                handleAutoReconnect("onConnectError")
+            }
         }
         
         // 订阅车辆控制响应事件
+        // 注意:使用常量而不是硬编码字符串,但 capability-socketio 模块的 SocketIOEvent 没有定义此常量
+        // 所以暂时使用硬编码字符串,应该与 base-common 模块的 SocketIOEvent.VEHICLE_CONTROL_RESPONSE 保持一致
         socketService.on("vehicle_control_response") { response ->
-            ILog.d(TAG, "onVehicleControlResponse: ${response.data}")
+            ILog.d(TAG, "onVehicleControlResponse: ✅ 收到车辆控制响应事件!")
+            ILog.d(TAG, "onVehicleControlResponse: event=${response.event}, data=${response.data}, timestamp=${response.timestamp}")
             _logMessage.tryEmit("📩 收到车辆控制响应:${response.data}")
             _vehicleControlResponse.tryEmit(response)
         }
         
+        ILog.d(TAG, "subscribeEvents: 事件监听器注册完成,已注册 vehicle_control_response 监听器")
+        
         // 订阅车辆报警事件
         socketService.on(SocketIOEvent.VEHICLE_ALARM) { response ->
             ILog.d(TAG, "onVehicleAlarm: ${response.data}")