From b8567b0f4e9c72bdb15454a6c5ad4e5dabd172a4 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 15 Jul 2026 00:39:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(client):=20=E5=88=87=E6=8D=A2=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E6=98=BE=E7=A4=BA=E4=B8=8A=E4=B8=80=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E2=80=94=E2=80=94=E7=99=BB=E5=BD=95/?= =?UTF-8?q?=E7=99=BB=E5=87=BA=E6=8C=89=E4=BC=9A=E8=AF=9D=E7=BA=AA=E5=85=83?= =?UTF-8?q?=E9=87=8D=E5=BB=BA=20ProviderScope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 张三登录后看到汤宇飞的订单列表(含已删行):业务 provider 的 notifier 实例 与 _cache 兜底缓存跨账号存活,拉取失败时把上一账号数据端给新账号(10 个 列表 provider 同款模式)。根治:会话纪元 sessionEpoch(Riverpod 体系外 ValueNotifier)作 ProviderScope 的 key,login/logout 后自增重建整棵状态树。 logout 的撤销会话 POST 改为不阻塞,确保清盘先于 scope 重建,防止新 scope restore 读到旧 token 把上一账号登回来。 Co-Authored-By: Claude Fable 5 --- client/lib/core/auth/auth_state.dart | 29 ++++++++++++++++--------- client/lib/core/auth/session_epoch.dart | 12 ++++++++++ client/lib/main.dart | 12 +++++++++- 3 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 client/lib/core/auth/session_epoch.dart diff --git a/client/lib/core/auth/auth_state.dart b/client/lib/core/auth/auth_state.dart index f829726..8c50df1 100644 --- a/client/lib/core/auth/auth_state.dart +++ b/client/lib/core/auth/auth_state.dart @@ -1,9 +1,12 @@ +import 'dart:async'; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../config/app_config.dart'; import '../config/app_constants.dart'; +import 'session_epoch.dart'; const _kAccessToken = 'access_token'; const _kRefreshToken = 'refresh_token'; @@ -118,6 +121,9 @@ class AuthNotifier extends StateNotifier { }); state = AuthState(initialized: true, user: user); debugPrint('[Auth] login() state set, isLoggedIn=${state.isLoggedIn}'); + // 持久化与状态就绪后重建 ProviderScope(防跨账号内存残留)。 + // 新 scope 的 restore() 从刚写入的 SharedPreferences 恢复本会话。 + sessionEpoch.value++; } /// 续期后写回令牌。后端会**轮换 refresh token**(jti 轮换 + 盗用检测), @@ -157,18 +163,19 @@ class AuthNotifier extends StateNotifier { // 先作废在途的令牌持久化并立即本地登出,使任何并发的续期写入在落盘前被丢弃。 _sessionGen++; state = const AuthState(initialized: true); - // 尽力通知后端撤销当前会话(离线/失败均忽略,不阻塞本地登出) + // 尽力通知后端撤销当前会话(离线/失败均忽略)。不 await: + // ① 不阻塞本地登出;② 不推迟下方的清盘与 scope 重建——若重建先于清盘, + // 新 scope 的 restore() 会读到旧 token 把上一账号自动登回来。 if (token != null && token.isNotEmpty) { - try { - await Dio(BaseOptions( - baseUrl: AppConfig.apiBaseUrl, - connectTimeout: AppConstants.authProbeTimeout, - receiveTimeout: AppConstants.authProbeTimeout, - headers: {'Authorization': 'Bearer $token'}, - )).post('/auth/logout'); - } catch (_) { + unawaited(Dio(BaseOptions( + baseUrl: AppConfig.apiBaseUrl, + connectTimeout: AppConstants.authProbeTimeout, + receiveTimeout: AppConstants.authProbeTimeout, + headers: {'Authorization': 'Bearer $token'}, + )).post('/auth/logout').catchError((Object _) { // ignore: 后端不可达或会话已失效都无所谓 - } + return Response(requestOptions: RequestOptions()); + })); } // 经队列串行清空 token,排在任何先前入队的续期写入之后,确保最终为已登出状态。 await _runStorage(() async { @@ -182,6 +189,8 @@ class AuthNotifier extends StateNotifier { await prefs.remove(_kShopId); await prefs.remove(_kRole); }); + // 清盘完成后重建 ProviderScope,抹掉上一账号的全部内存态。 + sessionEpoch.value++; } } diff --git a/client/lib/core/auth/session_epoch.dart b/client/lib/core/auth/session_epoch.dart new file mode 100644 index 0000000..dd0bc6f --- /dev/null +++ b/client/lib/core/auth/session_epoch.dart @@ -0,0 +1,12 @@ +import 'package:flutter/foundation.dart'; + +/// 会话纪元:每次显式登录 / 登出成功后自增(见 AuthNotifier.login/logout)。 +/// +/// main.dart 以其为 ProviderScope 的 key —— 纪元变化即整棵 Riverpod 状态树销毁重建, +/// 所有业务 provider(含 notifier 实例字段里的分页 / 筛选 / _cache 兜底缓存)随账号 +/// 切换彻底清零,杜绝跨账号内存残留(如订单列表显示上一账号数据的泄漏)。 +/// +/// 必须在 Riverpod 体系外(scope 重建时它自身要存活),故用裸 ValueNotifier。 +/// 只在 login()/logout() 主动 bump;启动恢复持久化会话(restore())不 bump, +/// 否则「重建 → restore → 又 bump」死循环。 +final ValueNotifier sessionEpoch = ValueNotifier(0); diff --git a/client/lib/main.dart b/client/lib/main.dart index 6151bfc..4c958b7 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'core/auth/auth_state.dart'; +import 'core/auth/session_epoch.dart'; import 'core/config/app_info.dart'; import 'core/errors/error_reporter.dart'; import 'core/platform/single_instance.dart' as single_instance; @@ -40,7 +41,16 @@ void main() { } await AppInfo.load(); // 从配置文件加载「关于我们」品牌信息 - runApp(const ProviderScope(child: JiuApp())); + // 会话纪元变化(登录/登出)→ 换 key 重建整个 ProviderScope: + // 所有业务 provider 状态(分页/筛选/notifier 内缓存)随账号切换彻底清零, + // 杜绝跨账号内存残留。auth 会话由新 scope 的 restore() 从持久化恢复。 + runApp(ValueListenableBuilder( + valueListenable: sessionEpoch, + builder: (_, epoch, __) => ProviderScope( + key: ValueKey(epoch), + child: const JiuApp(), + ), + )); }, (error, stack) { debugPrint('═══ Zone Error ═══════════════════════════════');