Skip to content

Commit d13bb79

Browse files
author
barry
committed
fix: device offline when switch network
1 parent 21c2570 commit d13bb79

10 files changed

Lines changed: 226 additions & 111 deletions

File tree

QuickDesk/src/controller/MainController.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,24 @@ MainController::MainController(QObject* parent)
9797
m_hostServerStatus = ServerStatus::Reconnecting;
9898
}
9999
emit hostServerStatusChanged();
100+
101+
// Re-bind device after signaling reconnect (e.g. after a network
102+
// switch). The signaling server clears the device's online flag
103+
// on every host WS disconnect; when we come back, we must make
104+
// sure logged_in is still true so the device shows online in
105+
// the user's device list. autoBindDevice is idempotent.
106+
const bool wasConnected = m_lastSignalingConnected;
107+
const bool nowConnected = (state == "connected");
108+
m_lastSignalingConnected = nowConnected;
109+
if (nowConnected && !wasConnected && m_authManager &&
110+
m_authManager->isLoggedIn()) {
111+
QString deviceId = m_hostManager->deviceId();
112+
if (!deviceId.isEmpty() && m_cloudDeviceManager) {
113+
LOG_INFO("Signaling reconnected, re-binding device: {}",
114+
deviceId.toStdString());
115+
m_cloudDeviceManager->autoBindDevice(deviceId);
116+
}
117+
}
100118
});
101119

102120
// Listen to Client signaling state to update client server status

QuickDesk/src/controller/MainController.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ private slots:
273273
ServerStatus::Status m_hostServerStatus = ServerStatus::Disconnected;
274274
ServerStatus::Status m_clientServerStatus = ServerStatus::Disconnected;
275275
QString m_primaryDeviceId; // Track primary device for client signaling status
276+
277+
// Tracks the last-observed host signaling state so we can detect
278+
// the transition into "connected" (e.g. after a network switch)
279+
// and re-bind the device to restore logged_in on the server.
280+
bool m_lastSignalingConnected = false;
276281

277282
// Access code auto-refresh timer
278283
QTimer m_accessCodeRefreshTimer;

QuickDesk/src/manager/CloudDeviceManager.cpp

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -133,50 +133,6 @@ void CloudDeviceManager::unbindDevice(const QString& deviceId)
133133
});
134134
}
135135

136-
void CloudDeviceManager::deviceLogin(const QString& deviceId)
137-
{
138-
if (!m_authManager->isLoggedIn() || deviceId.isEmpty()) return;
139-
140-
QUrl url(httpBaseUrl() + "api/v1/user/devices/" + deviceId + "/login");
141-
auto headers = authHeaders();
142-
143-
infra::HttpRequest::instance().sendPostRequest(
144-
url, headers, QString(), kRequestTimeoutMs,
145-
[this, deviceId](int statusCode, const std::string& errorMsg, const std::string& data) {
146-
Q_UNUSED(data);
147-
QMetaObject::invokeMethod(this, [statusCode, errorMsg, deviceId]() {
148-
if (statusCode != 200 || !errorMsg.empty()) {
149-
LOG_WARN("[CloudDeviceManager] deviceLogin failed: {}", errorMsg);
150-
return;
151-
}
152-
LOG_INFO("[CloudDeviceManager] Device login marked: {}", deviceId.toStdString());
153-
});
154-
});
155-
}
156-
157-
void CloudDeviceManager::deviceLogout(const QString& deviceId)
158-
{
159-
if (deviceId.isEmpty()) return;
160-
// Allow calling even if not logged in (token may still be valid briefly)
161-
QString token = m_authManager->token();
162-
if (token.isEmpty()) return;
163-
164-
QUrl url(httpBaseUrl() + "api/v1/user/devices/" + deviceId + "/logout");
165-
auto headers = authHeaders();
166-
167-
infra::HttpRequest::instance().sendPostRequest(
168-
url, headers, QString(), kRequestTimeoutMs,
169-
[this, deviceId](int statusCode, const std::string& errorMsg, const std::string& data) {
170-
Q_UNUSED(data);
171-
QMetaObject::invokeMethod(this, [statusCode, errorMsg, deviceId]() {
172-
if (statusCode != 200 || !errorMsg.empty()) {
173-
LOG_WARN("[CloudDeviceManager] deviceLogout failed: {}", errorMsg);
174-
return;
175-
}
176-
LOG_INFO("[CloudDeviceManager] Device logout marked: {}", deviceId.toStdString());
177-
});
178-
});
179-
}
180136

181137
void CloudDeviceManager::setDeviceRemark(const QString& deviceId, const QString& remark)
182138
{

QuickDesk/src/manager/CloudDeviceManager.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ class CloudDeviceManager : public QObject {
2626
Q_INVOKABLE void fetchMyDevices();
2727
Q_INVOKABLE void autoBindDevice(const QString& deviceId);
2828
Q_INVOKABLE void unbindDevice(const QString& deviceId);
29-
Q_INVOKABLE void deviceLogin(const QString& deviceId);
30-
Q_INVOKABLE void deviceLogout(const QString& deviceId);
3129
Q_INVOKABLE void setDeviceRemark(const QString& deviceId, const QString& remark);
3230
Q_INVOKABLE void syncAccessCode(const QString& deviceId, const QString& accessCode);
3331
Q_INVOKABLE QString getDeviceAccessCode(const QString& deviceId) const;

SignalingServer/cmd/signaling/main.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,30 @@ func main() {
9696

9797
apiHandler.SetWSHandler(wsHandler)
9898

99+
// Cold-start recovery: any device still flagged logged_in=true from a
100+
// previous server run is stale (no host WebSocket is connected yet).
101+
// Clear it so the device list doesn't show phantom logged-in states.
102+
// Hosts that are still online will re-establish their signaling
103+
// WebSocket and the client will call AutoBindDevice again to restore
104+
// logged_in=true for the correct (current) user.
105+
if res := db.Model(&models.Device{}).
106+
Where("logged_in = ?", true).
107+
Update("logged_in", false); res.Error != nil {
108+
log.Printf("Startup: failed to reset stale logged_in flags: %v", res.Error)
109+
} else if res.RowsAffected > 0 {
110+
log.Printf("Startup: reset %d stale logged_in flag(s)", res.RowsAffected)
111+
}
112+
113+
// Also mark all devices offline on startup; real host connections will
114+
// flip them back to online as they reconnect.
115+
if res := db.Model(&models.Device{}).
116+
Where("online = ?", true).
117+
Update("online", false); res.Error != nil {
118+
log.Printf("Startup: failed to reset stale online flags: %v", res.Error)
119+
} else if res.RowsAffected > 0 {
120+
log.Printf("Startup: reset %d stale online flag(s)", res.RowsAffected)
121+
}
122+
99123
gin.SetMode(gin.ReleaseMode)
100124
router := gin.New()
101125
router.Use(gin.Recovery())
@@ -159,8 +183,6 @@ func main() {
159183
userAPI.GET("/devices/logs", userDeviceHandler.GetUserDeviceLogs)
160184
userAPI.PUT("/devices/:device_id/access-code", userDeviceHandler.UpdateAccessCode)
161185
userAPI.PUT("/devices/:device_id/remark", userDeviceHandler.UpdateDeviceRemark)
162-
userAPI.POST("/devices/:device_id/login", userDeviceHandler.DeviceLogin)
163-
userAPI.POST("/devices/:device_id/logout", userDeviceHandler.DeviceLogout)
164186
userAPI.GET("/favorites", userDeviceHandler.GetFavorites)
165187
userAPI.POST("/favorites", userDeviceHandler.AddFavorite)
166188
userAPI.PUT("/favorites/:device_id", userDeviceHandler.UpdateFavorite)

SignalingServer/internal/handler/user_device_handler.go

Lines changed: 37 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ func (h *UserDeviceHandler) UnbindDevice(c *gin.Context) {
4141

4242
binding.Status = false
4343
h.db.Save(&binding)
44+
45+
// When a user explicitly unbinds a device they own, also clear the
46+
// device-level logged_in flag so the device is not shown as an active
47+
// login anywhere. Only clear if the device is still owned by this user
48+
// (don't stomp on a concurrent takeover).
49+
h.db.Model(&models.Device{}).
50+
Where("device_id = ? AND user_id = ?", req.DeviceID, authedUserID).
51+
Update("logged_in", false)
52+
53+
h.notifySync(authedUserID, gin.H{
54+
"type": "device_logged_out",
55+
"device_id": req.DeviceID,
56+
})
57+
4458
recomputeDeviceCount(h.db, authedUserID)
4559

4660
c.JSON(http.StatusOK, gin.H{"message": "设备解绑成功"})
@@ -200,17 +214,37 @@ func (h *UserDeviceHandler) AutoBindDevice(c *gin.Context) {
200214
return
201215
}
202216

217+
// Account takeover: if the device is currently bound to a different
218+
// user (e.g. previous user crashed without logging out, or the device
219+
// is being re-used by another account), transfer ownership and notify
220+
// the previous owner that the device is no longer logged in for them.
221+
// This is the canonical way to recover from "zombie logged_in=true"
222+
// states left behind by abnormal exits.
223+
var previousUserID uint
203224
if device.UserID != nil && *device.UserID != 0 && *device.UserID != authedUserID {
204-
c.JSON(http.StatusConflict, gin.H{"error": "设备已绑定其他用户"})
205-
return
225+
previousUserID = *device.UserID
226+
// Deactivate the previous user's binding row so it stops
227+
// appearing in their device list.
228+
h.db.Model(&models.UserDevice{}).
229+
Where("user_id = ? AND device_id = ?", previousUserID, req.DeviceID).
230+
Update("status", false)
231+
recomputeDeviceCount(h.db, previousUserID)
206232
}
207233

208-
// Update device ownership
234+
// Update device ownership and mark as logged in for the new user.
209235
h.db.Model(&models.Device{}).Where("device_id = ?", req.DeviceID).Updates(map[string]interface{}{
210236
"user_id": authedUserID,
211237
"logged_in": true,
212238
})
213239

240+
// Notify the previous owner (if any) that the device left their account.
241+
if previousUserID != 0 {
242+
h.notifySync(previousUserID, gin.H{
243+
"type": "device_logged_out",
244+
"device_id": req.DeviceID,
245+
})
246+
}
247+
214248
// Upsert UserDevice: reactivate if exists but inactive, create otherwise
215249
var existing models.UserDevice
216250
result := h.db.Where("user_id = ? AND device_id = ?", authedUserID, req.DeviceID).First(&existing)
@@ -434,60 +468,3 @@ func (h *UserDeviceHandler) RemoveFavorite(c *gin.Context) {
434468

435469
c.JSON(http.StatusOK, gin.H{"message": "取消收藏成功"})
436470
}
437-
438-
// DeviceLogin handles POST /api/v1/user/devices/:device_id/login
439-
// Called when a user logs in on a device, marks the device as actively logged in.
440-
func (h *UserDeviceHandler) DeviceLogin(c *gin.Context) {
441-
deviceID := c.Param("device_id")
442-
userIDVal, _ := c.Get("authed_user_id")
443-
authedUserID := userIDVal.(uint)
444-
445-
var device models.Device
446-
if result := h.db.Where("device_id = ?", deviceID).First(&device); result.Error != nil {
447-
c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"})
448-
return
449-
}
450-
451-
if device.UserID != nil && *device.UserID != 0 && *device.UserID != authedUserID {
452-
c.JSON(http.StatusForbidden, gin.H{"error": "无权操作此设备"})
453-
return
454-
}
455-
456-
result := h.db.Model(&models.Device{}).Where("device_id = ?", deviceID).Update("logged_in", true)
457-
log.Printf("DeviceLogin: device_id=%s, rows_affected=%d, error=%v", deviceID, result.RowsAffected, result.Error)
458-
459-
h.notifySync(authedUserID, gin.H{
460-
"type": "device_logged_in",
461-
"device_id": deviceID,
462-
})
463-
464-
c.JSON(http.StatusOK, gin.H{"message": "设备登录状态已更新"})
465-
}
466-
467-
// DeviceLogout handles POST /api/v1/user/devices/:device_id/logout
468-
// Called when a user logs out on a device, marks the device as not logged in.
469-
func (h *UserDeviceHandler) DeviceLogout(c *gin.Context) {
470-
deviceID := c.Param("device_id")
471-
userIDVal, _ := c.Get("authed_user_id")
472-
authedUserID := userIDVal.(uint)
473-
474-
var device models.Device
475-
if result := h.db.Where("device_id = ?", deviceID).First(&device); result.Error != nil {
476-
c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"})
477-
return
478-
}
479-
480-
if device.UserID != nil && *device.UserID != 0 && *device.UserID != authedUserID {
481-
c.JSON(http.StatusForbidden, gin.H{"error": "无权操作此设备"})
482-
return
483-
}
484-
485-
h.db.Model(&models.Device{}).Where("device_id = ?", deviceID).Update("logged_in", false)
486-
487-
h.notifySync(authedUserID, gin.H{
488-
"type": "device_logged_out",
489-
"device_id": deviceID,
490-
})
491-
492-
c.JSON(http.StatusOK, gin.H{"message": "设备登出状态已更新"})
493-
}

SignalingServer/internal/handler/ws_handler.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,15 @@ func (h *WSHandler) HandleWebSocket(c *gin.Context) {
238238
h.NotifyDeviceOnlineStatus(deviceID, true)
239239
defer func() {
240240
h.deviceService.SetDeviceOnline(context.Background(), deviceID, false)
241-
// Clear logged_in when device disconnects from signaling
242-
h.db.Model(&models.Device{}).Where("device_id = ?", deviceID).Update("logged_in", false)
241+
// NOTE: Do NOT clear logged_in here.
242+
// logged_in represents the user-level binding state (set by
243+
// AutoBindDevice, cleared by UnbindDevice / user logout /
244+
// takeover by another account). A transient signaling
245+
// WebSocket disconnect (e.g. network switch, Wi-Fi roam)
246+
// must not be treated as a logout, otherwise the device
247+
// stays "logged_in=false" after reconnect and is shown as
248+
// offline in the user's device list even though the host is
249+
// back online.
243250
h.NotifyDeviceOnlineStatus(deviceID, false)
244251
}()
245252
}

WebClient/src/App.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import { useI18n } from 'vue-i18n'
6565
import { useRouter } from 'vue-router'
6666
import LoginDialog from './components/LoginDialog.vue'
6767
import { userApi } from './api/userApi'
68+
import { userSync } from './api/userSync'
6869
import { authState, updateAuthState } from './store/auth'
6970
7071
const { locale, t } = useI18n()
@@ -112,10 +113,14 @@ function onLoginSuccess() {
112113
showToast(t('user.loginSuccess'), 'success')
113114
// Fetch features for SMS toggle
114115
fetchFeatures()
116+
// Start real-time device/favorite sync
117+
userSync.start()
115118
}
116119
117120
async function onLogout() {
118121
showUserMenu.value = false
122+
// Stop sync first so we don't race the WebSocket against token clearing
123+
userSync.stop()
119124
await userApi.logout()
120125
updateAuthState()
121126
router.push('/remote')
@@ -153,6 +158,8 @@ async function restoreSession() {
153158
id: r.data.id, username: r.data.username, phone: r.data.phone, email: r.data.email,
154159
}))
155160
updateAuthState()
161+
// Resume real-time sync after a token-validated session restore
162+
userSync.start()
156163
} else {
157164
userApi.clearSession()
158165
updateAuthState()

0 commit comments

Comments
 (0)