From 3167ca8a74a103415e79fb6b300de7301f2d99a6 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 30 Jun 2026 07:55:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(server):=20=E8=AE=BE=E5=A4=87=E3=80=8C?= =?UTF-8?q?=E5=9C=A8=E7=BA=BF=E3=80=8D=E5=87=86=E7=A1=AE=E5=8F=8D=E6=98=A0?= =?UTF-8?q?=20app=20=E8=BF=90=E8=A1=8C=E4=B8=AD=20=E2=80=94=20=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E8=BD=AE=E8=AF=A2=E5=88=B7=20last=5Fseen=20+=20?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E7=BC=A9=E5=88=B0=2090s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原 online=last_seen 在 3min 内,而 last_seen 只在连接/用量上报时刷 → app 运行≠刷新, 显示成'最近连过'而非'正在运行';关 app 后还'在线'到窗口过期。修:SessionActive(每 15s 轮询)顺便 TouchLastSeen → 运行中的 app 持续刷新;onlineWindow 3min→90s,关 app ~90s 转离线。 Co-Authored-By: Claude Opus 4.8 --- server/internal/devices/service.go | 10 +++++++--- server/internal/devices/store.go | 11 +++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/server/internal/devices/service.go b/server/internal/devices/service.go index ca72609..035feca 100644 --- a/server/internal/devices/service.go +++ b/server/internal/devices/service.go @@ -83,9 +83,11 @@ type Device struct { LastLogin *string `json:"last_login"` // most recent session created_at; null when none } -// onlineWindow: a device is "online" if its last_seen (touched by connect + -// periodic usage reports) is within this window. -const onlineWindow = 3 * time.Minute +// onlineWindow: a device is "online" if its last_seen is within this window. +// last_seen is bumped by connect、用量上报,以及 ~15s 的会话轮询(SessionActive)—— +// 后者让「在线」准确反映 app 正在运行。窗口取 90s(覆盖几次丢失的 15s 轮询), +// app 一关 ~90s 内转离线。 +const onlineWindow = 90 * time.Second func toAPIDevice(d DeviceRow) Device { out := Device{UUID: d.UUID, Name: d.Name, Platform: d.Platform} @@ -359,6 +361,8 @@ func (svc *Service) SessionActive(ctx context.Context, userID int64, deviceUUID if dev == nil || dev.UserID != userID { return true, nil // 未知 / 非本人设备:不据此登出 } + // 设备在轮询 = app 正在运行:刷 last_seen,让「在线」状态准确(不再只反映上次连接)。 + _ = svc.store.TouchLastSeen(ctx, dev.ID) active, err := svc.sessions.HasActiveSession(ctx, userID, dev.ID) if err != nil { return false, apierr.ErrInternal diff --git a/server/internal/devices/store.go b/server/internal/devices/store.go index 4c8a0bd..d8488c3 100644 --- a/server/internal/devices/store.go +++ b/server/internal/devices/store.go @@ -153,6 +153,17 @@ func (s *Store) insertDeviceTx(ctx context.Context, tx *sql.Tx, uuid string, use }, nil } +// TouchLastSeen bumps a device's last_seen to now (non-tx, best-effort). Called by +// the ~15s session poll so "online" tracks an actually-running app, not just the +// last connect/usage report. +func (s *Store) TouchLastSeen(ctx context.Context, deviceID int64) error { + if _, err := s.db.ExecContext(ctx, + `UPDATE devices SET last_seen=? WHERE id=?`, time.Now().UTC(), deviceID); err != nil { + return fmt.Errorf("store.TouchLastSeen: %w", err) + } + return nil +} + // touchLastSeenTx updates a device's last_seen to now inside tx, and refreshes // client_version when a non-empty one is supplied (latest reported wins). func (s *Store) touchLastSeenTx(ctx context.Context, tx *sql.Tx, deviceID int64, clientVersion string) error {