From 58211d2fb8db0984560db598e4b375dd55a3a5da Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 1 Jul 2026 09:53:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(client):=20=E8=BF=9E=E6=8E=A5=E6=97=B6?= =?UTF-8?q?=E9=95=BF=E6=8C=89=E5=A2=99=E4=B8=8A=E6=97=B6=E9=92=9F=E7=AE=97?= =?UTF-8?q?,=E5=88=87=E5=90=8E=E5=8F=B0/=E9=94=81=E5=B1=8F=E4=B8=8D?= =?UTF-8?q?=E6=BC=8F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS 锁屏/切后台后连接页时长少算:elapsed 原来靠每秒 +1 的 Timer 累加,后台 isolate 被挂起、Timer 停跳 → 后台那段时长漏掉。改为记 _connectedAt 起点, elapsed = now - _connectedAt(墙上时钟),与 timer 是否跳无关;回前台(resumed) 立即 _refreshElapsed 补上。_connectedAt 用 ??= 保留同一会话起点,断开才清。 测试:新增「切后台 300s 时长不漏计」回归(前台 10s + 后台 300s = 310s);全 8 例过。 Co-Authored-By: Claude Opus 4.8 --- client/lib/state/connection_provider.dart | 25 ++++++++++++---- .../test/unit/connection_watchdog_test.dart | 29 +++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/client/lib/state/connection_provider.dart b/client/lib/state/connection_provider.dart index 63118e2..83b04df 100644 --- a/client/lib/state/connection_provider.dart +++ b/client/lib/state/connection_provider.dart @@ -101,7 +101,10 @@ class ConnectionController extends StateNotifier { _lifecycle = AppLifecycleListener( onStateChange: (s) { if (s == AppLifecycleState.resumed) { - if (state.phase == VpnPhase.on && _watchdog == null) _startWatchdog(); + if (state.phase == VpnPhase.on) { + if (_watchdog == null) _startWatchdog(); + _refreshElapsed(); // 回前台按墙上时钟补上后台漏计的时长 + } } else if (s == AppLifecycleState.paused || s == AppLifecycleState.hidden) { _stopWatchdog(); } @@ -115,6 +118,9 @@ class ConnectionController extends StateNotifier { StreamSubscription? _statsSub; ProviderSubscription? _authSub; Timer? _elapsed; + // 本次连接的起始时刻;连接时长按「now - _connectedAt」墙上时钟算,而非每秒 +1 累加 —— + // 否则 app 切后台/锁屏 isolate 被挂起、Timer 停跳,后台那段时长会漏计(iOS 尤甚)。 + DateTime? _connectedAt; Timer? _watchdog; AppLifecycleListener? _lifecycle; bool _probing = false; @@ -377,16 +383,23 @@ class ConnectionController extends StateNotifier { void _startElapsed() { _elapsed?.cancel(); - _elapsed = Timer.periodic(const Duration(seconds: 1), (_) { - if (mounted && state.phase == VpnPhase.on) { - state = state.copyWith(elapsed: state.elapsed + const Duration(seconds: 1)); - } - }); + _connectedAt ??= _now(); // 同一会话保留起点(on 被重复上报也不重置);断开时清空 + _refreshElapsed(); // 立即刷新,避免等 1s + _elapsed = Timer.periodic(const Duration(seconds: 1), (_) => _refreshElapsed()); + } + + /// 按墙上时钟把 elapsed 刷成 now - _connectedAt(切后台回来也准)。 + void _refreshElapsed() { + final at = _connectedAt; + if (mounted && state.phase == VpnPhase.on && at != null) { + state = state.copyWith(elapsed: _now().difference(at)); + } } void _stopElapsed() { _elapsed?.cancel(); _elapsed = null; + _connectedAt = null; } @override diff --git a/client/test/unit/connection_watchdog_test.dart b/client/test/unit/connection_watchdog_test.dart index 6157795..7485082 100644 --- a/client/test/unit/connection_watchdog_test.dart +++ b/client/test/unit/connection_watchdog_test.dart @@ -233,6 +233,35 @@ void main() { await tester.pump(); }); + testWidgets('连接时长按墙上时钟:切后台的时长不漏计', (tester) async { + final bridge = _FakeBridge(); + var fake = DateTime(2026, 1, 1); + final c = makeContainer(bridge, now: () => fake); + addTearDown(bridge.dispose); + addTearDown(c.dispose); + await tester.pumpWidget(UncontrolledProviderScope(container: c, child: const SizedBox())); + c.read(selectedNodeCodeProvider.notifier).select('HK'); + await driveOn(tester, c, bridge); // on:_connectedAt=t0 + // 前台 10s:墙上时钟 +10、timer 跳一次。 + fake = fake.add(const Duration(seconds: 10)); + await tester.pump(const Duration(seconds: 1)); + expect(c.read(connectionProvider).elapsed, const Duration(seconds: 10)); + // 切后台(timer 冻结:只推墙上时钟、不 pump timer 间隔),过 300s。 + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.hidden); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused); + fake = fake.add(const Duration(seconds: 300)); + // 回前台:应按墙上时钟补上后台 300s → 共 310s,而非只有前台的 10s。 + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.hidden); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + await tester.pump(); + expect(c.read(connectionProvider).elapsed, const Duration(seconds: 310), + reason: '后台 300s 应计入连接时长'); + bridge.emit(VpnStatus.off); // 收尾 + await tester.pump(); + }); + testWidgets('节点健康 → 保持连接,不触发任何动作', (tester) async { final bridge = _FakeBridge(); final c = makeContainer(bridge); // 都 up