Files
jiu/client/lib/providers/session_heartbeat.dart
wangjia 36c7ad8b43
Deploy Client / build-client-web (push) Successful in 38s
Deploy Client / build-windows (push) Successful in 1m52s
Deploy Client / build-macos (push) Successful in 1m55s
Deploy Client / build-android (push) Successful in 1m0s
Deploy Client / build-ios (push) Successful in 2m47s
Deploy Client / release-deploy-client (push) Successful in 1m21s
chore: release client-v1.0.61
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 08:56:02 +08:00

54 lines
2.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/api/api_client.dart';
import '../core/auth/auth_state.dart';
import '../core/config/app_constants.dart';
import 'license_provider.dart';
/// 登录态心跳:已登录时每 30s 执行一次 **单个** POST /auth/ping
/// - 会话/在线检查:若会话已被撤销(被踢/管理员强制下线),后端返回 401,
/// ApiClient 拦截器触发 refresh→失败→onAuthFailed→logout,因此空闲用户也能在
/// ~30s 内感知到被下线;同时刷新 last_seen_at(在线状态)。
/// - 授权状态检查:ping 响应**回带**当前授权概况(license 字段,与 /license/info 同构),
/// 直接喂给 licenseProvider,使到期/续费/被改动等变化在 ~30s 内反映到横幅/状态栏/设置页。
/// 如此一次心跳即覆盖两件事,**无需**再单独轮询 /license/info(每 30s 少一次请求)。
final sessionHeartbeatProvider = Provider<SessionHeartbeat>((ref) {
final hb = SessionHeartbeat(ref);
ref.onDispose(hb.dispose);
hb.start();
return hb;
});
class SessionHeartbeat {
final Ref _ref;
Timer? _timer;
SessionHeartbeat(this._ref);
void start() {
_timer?.cancel();
_timer = Timer.periodic(AppConstants.heartbeatInterval, (_) => _ping());
}
Future<void> _ping() async {
if (!_ref.read(authStateProvider).isLoggedIn) return;
// 一次 ping 同时完成会话/在线检查与授权概况刷新。
try {
final resp = await _ref.read(apiClientProvider).post('/auth/ping');
// ping 可能已触发登出,故重新判断登录态后再采纳授权概况。
final data = resp.data is Map ? resp.data['data'] : null;
if (data is Map &&
data.containsKey('license') &&
_ref.read(authStateProvider).isLoggedIn) {
_ref.read(licenseProvider.notifier).applyServerInfo(data['license']);
}
} catch (e) {
// 401 已由 ApiClient 拦截器处理(触发登出);其余错误忽略,保留上次授权状态
debugPrint('[Heartbeat] ping failed: $e');
}
}
void dispose() => _timer?.cancel();
}