fix: MQTT连接时序竞争导致扫码绑定失败
- MqttManager: 防止重复创建client实例,加connecting标记和client存在检查 - SplashFragment: MQTT连接和API检查并行,等MQTT就绪后再导航 - BindFragment: 等待MqttConnected事件再显示状态,不立即检查 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,12 +36,18 @@ class MqttManager @Inject constructor(
|
||||
/** MQTT 异步客户端 */
|
||||
private var client: MqttAsyncClient? = null
|
||||
|
||||
/** 是否正在连接中(防止重复创建 client) */
|
||||
private var connecting = false
|
||||
|
||||
/** 是否已连接 */
|
||||
val isConnected: Boolean get() = client?.isConnected == true
|
||||
|
||||
/**
|
||||
* 连接 MQTT Broker
|
||||
* 使用 DevicePrefs.imei 作为 clientId,TCP 协议连接
|
||||
*
|
||||
* 关键:不重复创建 client。如果 client 已存在(连接中/已连接/自动重连中),
|
||||
* 直接复用,避免覆盖正在连接的实例导致连接丢失。
|
||||
*/
|
||||
fun connect() {
|
||||
val imei = devicePrefs.imei
|
||||
@@ -56,7 +62,21 @@ class MqttManager @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
// 正在连接中,不重复发起
|
||||
if (connecting) {
|
||||
Timber.d("MQTT: 正在连接中,跳过")
|
||||
return
|
||||
}
|
||||
|
||||
// client 已存在(可能在自动重连中),不重新创建
|
||||
if (client != null) {
|
||||
Timber.d("MQTT: client 已存在(可能在自动重连中),跳过")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
connecting = true
|
||||
|
||||
// 构建 TCP 连接地址
|
||||
val serverUri = "tcp://${EnvConfig.mqttHost}:${MqttConfig.PORT}"
|
||||
Timber.d("MQTT: 连接 $serverUri, clientId=$imei")
|
||||
@@ -82,6 +102,7 @@ class MqttManager @Inject constructor(
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "MQTT: 连接异常")
|
||||
connecting = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +110,8 @@ class MqttManager @Inject constructor(
|
||||
fun disconnect() {
|
||||
try {
|
||||
client?.disconnect()
|
||||
client = null
|
||||
connecting = false
|
||||
Timber.d("MQTT: 已断开")
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "MQTT: 断开异常")
|
||||
@@ -98,12 +121,16 @@ class MqttManager @Inject constructor(
|
||||
/** 连接回调:成功后订阅 3 个 Topic */
|
||||
private val connectCallback = object : IMqttActionListener {
|
||||
override fun onSuccess(asyncActionToken: IMqttToken?) {
|
||||
connecting = false
|
||||
Timber.d("MQTT: 连接成功")
|
||||
emitEvent(AppEvent.MqttConnected)
|
||||
subscribeTopics()
|
||||
}
|
||||
|
||||
override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
|
||||
connecting = false
|
||||
// 连接失败,清除 client 以允许下次重新创建
|
||||
client = null
|
||||
Timber.e(exception, "MQTT: 连接失败")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.xiaoqu.watch.event.AppEvent
|
||||
import com.xiaoqu.watch.event.EventBus
|
||||
import com.xiaoqu.watch.ui.common.BaseFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -163,17 +164,39 @@ class BindFragment : BaseFragment<FragmentBindBinding>() {
|
||||
binding.loadingWrap.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
/** 显示 MQTT 连接状态(调试用,确认扫码时连接是否正常) */
|
||||
/**
|
||||
* 显示 MQTT 连接状态(调试用)
|
||||
* 如果已连接直接显示;否则等待 MqttConnected 事件(最多 8 秒超时)
|
||||
*/
|
||||
private fun showMqttStatus() {
|
||||
val connected = mqttManager.isConnected
|
||||
val imei = devicePrefs.imei
|
||||
val msg = if (connected) "MQTT已连接 (IMEI=$imei)" else "MQTT未连接!"
|
||||
android.widget.Toast.makeText(requireContext(), msg, android.widget.Toast.LENGTH_LONG).show()
|
||||
Timber.d("绑定页: $msg")
|
||||
if (mqttManager.isConnected) {
|
||||
// 已连接,直接显示
|
||||
showMqttToast(true)
|
||||
return
|
||||
}
|
||||
|
||||
// 未连接则尝试重连
|
||||
if (!connected) {
|
||||
// MQTT 还在连接中(Splash 刚发起异步连接),等待连接成功事件
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
// 先尝试重连(防止 Splash 连接失败的情况)
|
||||
mqttManager.connect()
|
||||
|
||||
// 等待 MqttConnected 事件,最多 8 秒
|
||||
val connected = kotlinx.coroutines.withTimeoutOrNull(8000L) {
|
||||
eventBus.events.first { it is AppEvent.MqttConnected }
|
||||
true
|
||||
} ?: false
|
||||
|
||||
if (isAdded) {
|
||||
showMqttToast(connected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示 MQTT 连接状态 Toast */
|
||||
private fun showMqttToast(connected: Boolean) {
|
||||
val imei = devicePrefs.imei
|
||||
val msg = if (connected) "MQTT已连接 (IMEI=$imei)" else "MQTT未连接! 请检查网络"
|
||||
android.widget.Toast.makeText(requireContext(), msg, android.widget.Toast.LENGTH_LONG).show()
|
||||
Timber.d("绑定页: $msg")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,18 @@ import androidx.navigation.fragment.findNavController
|
||||
import com.xiaoqu.watch.R
|
||||
import com.xiaoqu.watch.data.prefs.DevicePrefs
|
||||
import com.xiaoqu.watch.data.prefs.UserPrefs
|
||||
import com.xiaoqu.watch.event.AppEvent
|
||||
import com.xiaoqu.watch.event.EventBus
|
||||
import com.xiaoqu.watch.network.ApiResult
|
||||
import com.xiaoqu.watch.network.api.CommonApi
|
||||
import com.xiaoqu.watch.network.safeApiCall
|
||||
import com.xiaoqu.watch.service.manager.MqttManager
|
||||
import com.xiaoqu.watch.util.DeviceUtil
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -32,6 +37,7 @@ class SplashFragment : Fragment() {
|
||||
@Inject lateinit var userPrefs: UserPrefs
|
||||
@Inject lateinit var mqttManager: MqttManager
|
||||
@Inject lateinit var commonApi: CommonApi
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -47,11 +53,29 @@ class SplashFragment : Fragment() {
|
||||
// 1. 初始化设备信息(首次启动时写入 SP)
|
||||
initDevicePrefs()
|
||||
|
||||
// 2. 连接 MQTT(定制 ROM + Launcher 应用,系统不会杀进程)
|
||||
mqttManager.connect()
|
||||
// 2. 连接 MQTT + 检查绑定状态 并行执行,等 MQTT 就绪后再导航
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
// 并行:MQTT 连接 + API 绑定检查
|
||||
mqttManager.connect()
|
||||
|
||||
// 3. 检查绑定状态并导航
|
||||
checkBindStatus()
|
||||
// 等待 MQTT 连接成功(最多 10 秒),同时检查绑定状态
|
||||
val mqttReady = async {
|
||||
if (mqttManager.isConnected) return@async true
|
||||
withTimeoutOrNull(10_000L) {
|
||||
eventBus.events.first { it is AppEvent.MqttConnected }
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
val bindResult = async { checkBindStatusAsync() }
|
||||
|
||||
// 等待两者都完成
|
||||
val mqttOk = mqttReady.await()
|
||||
Timber.d("Splash: MQTT 就绪=$mqttOk")
|
||||
|
||||
// 根据绑定结果导航
|
||||
navigateByResult(bindResult.await())
|
||||
}
|
||||
}
|
||||
|
||||
/** 首次启动时初始化设备信息到 device_prefs */
|
||||
@@ -69,61 +93,54 @@ class SplashFragment : Fragment() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查绑定状态:
|
||||
* 检查绑定状态(挂起函数,返回是否已绑定)
|
||||
* 1. 调用 API 获取最新绑定状态
|
||||
* 2. 有数据 → 已绑定 → 更新 UserPrefs → 导航到首页
|
||||
* 3. 无数据 → 未绑定 → 导航到绑定页
|
||||
* 2. 有数据 → 已绑定 → 更新 UserPrefs → 返回 true
|
||||
* 3. 无数据 → 未绑定 → 返回 false
|
||||
* 4. 网络异常 → 用本地 isBound 兜底
|
||||
*/
|
||||
private fun checkBindStatus() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val imei = devicePrefs.imei
|
||||
private suspend fun checkBindStatusAsync(): Boolean {
|
||||
val imei = devicePrefs.imei
|
||||
|
||||
// 调用 API 检查绑定状态
|
||||
val result = safeApiCall { commonApi.getWatchByImei(imei) }
|
||||
// 调用 API 检查绑定状态
|
||||
val result = safeApiCall { commonApi.getWatchByImei(imei) }
|
||||
|
||||
when (result) {
|
||||
is ApiResult.Success -> {
|
||||
// API 返回数据 → 已绑定,存入 UserPrefs
|
||||
val data = result.data
|
||||
if (data != null && data.userId > 0) {
|
||||
Timber.d("Splash: API 返回已绑定 userId=${data.userId}")
|
||||
userPrefs.saveUser(
|
||||
userId = data.userId,
|
||||
mobile = data.mobile,
|
||||
userName = data.userName,
|
||||
headUrl = data.headUrl
|
||||
)
|
||||
navigateToHome()
|
||||
} else {
|
||||
// 返回数据无效,视为未绑定
|
||||
Timber.d("Splash: API 返回数据无效,视为未绑定")
|
||||
navigateToBind()
|
||||
}
|
||||
return when (result) {
|
||||
is ApiResult.Success -> {
|
||||
val data = result.data
|
||||
if (data != null && data.userId > 0) {
|
||||
Timber.d("Splash: API 返回已绑定 userId=${data.userId}")
|
||||
userPrefs.saveUser(
|
||||
userId = data.userId,
|
||||
mobile = data.mobile,
|
||||
userName = data.userName,
|
||||
headUrl = data.headUrl
|
||||
)
|
||||
true
|
||||
} else {
|
||||
Timber.d("Splash: API 返回数据无效,视为未绑定")
|
||||
false
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
if (result.code == 1) {
|
||||
// code=1 未绑定
|
||||
Timber.d("Splash: API 返回未绑定")
|
||||
navigateToBind()
|
||||
} else {
|
||||
// 其他错误,用本地状态兜底
|
||||
Timber.w("Splash: API 错误 code=${result.code}, 用本地状态兜底")
|
||||
navigateByLocalState()
|
||||
}
|
||||
}
|
||||
is ApiResult.NetworkError -> {
|
||||
// 网络异常,用本地状态兜底
|
||||
Timber.w("Splash: 网络异常, 用本地状态兜底")
|
||||
navigateByLocalState()
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
if (result.code == 1) {
|
||||
Timber.d("Splash: API 返回未绑定")
|
||||
false
|
||||
} else {
|
||||
Timber.w("Splash: API 错误 code=${result.code}, 用本地状态兜底")
|
||||
userPrefs.isBound
|
||||
}
|
||||
}
|
||||
is ApiResult.NetworkError -> {
|
||||
Timber.w("Splash: 网络异常, 用本地状态兜底")
|
||||
userPrefs.isBound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据本地 UserPrefs 绑定状态导航(离线兜底) */
|
||||
private fun navigateByLocalState() {
|
||||
if (userPrefs.isBound) {
|
||||
/** 根据绑定结果导航 */
|
||||
private fun navigateByResult(isBound: Boolean) {
|
||||
if (isBound) {
|
||||
navigateToHome()
|
||||
} else {
|
||||
navigateToBind()
|
||||
|
||||
Reference in New Issue
Block a user