Kaynağa Gözat

修改登出功能可用

刘清 1 ay önce
ebeveyn
işleme
064d17929b

+ 2
- 0
lib/core/constants/api_constants.dart Dosyayı Görüntüle

@@ -8,6 +8,7 @@ class ApiConstants {
8 8
   static const String login = '/api/v1/auth/login';
9 9
   static const String register = '/api/v1/auth/register';
10 10
   static const String logout = '/api/v1/auth/logout';
11
+  static const String refresh = '/api/v1/auth/refresh';
11 12
   static const String getCurrentUser = '/api/v1/auth/me';
12 13
   
13 14
   // 用户管理接口
@@ -18,6 +19,7 @@ class ApiConstants {
18 19
   static String getLoginUrl() => '$baseUrl$login';
19 20
   static String getRegisterUrl() => '$baseUrl$register';
20 21
   static String getLogoutUrl() => '$baseUrl$logout';
22
+  static String getRefreshUrl() => '$baseUrl$refresh';
21 23
   static String getCurrentUserUrl() => '$baseUrl$getCurrentUser';
22 24
   static String getUserProfileUrl() => '$baseUrl$getUserProfile';
23 25
   static String getUpdateProfileUrl() => '$baseUrl$updateUserProfile';

+ 16
- 6
lib/data/datasources/local/shared_prefs.dart Dosyayı Görüntüle

@@ -7,16 +7,26 @@ class SharedPrefs {
7 7
   SharedPrefs(this._prefs);
8 8
 
9 9
   // Token管理
10
-  Future<bool> setAuthToken(String token) async {
11
-    return await _prefs.setString(AppConstants.authTokenKey, token);
10
+  Future<bool> setAccessToken(String token) async {
11
+    return await _prefs.setString('access_token', token);
12 12
   }
13 13
 
14
-  String? getAuthToken() {
15
-    return _prefs.getString(AppConstants.authTokenKey);
14
+  Future<bool> setRefreshToken(String token) async {
15
+    return await _prefs.setString('refresh_token', token);
16 16
   }
17 17
 
18
-  Future<bool> removeAuthToken() async {
19
-    return await _prefs.remove(AppConstants.authTokenKey);
18
+  String? getAccessToken() {
19
+    return _prefs.getString('access_token');
20
+  }
21
+
22
+  String? getRefreshToken() {
23
+    return _prefs.getString('refresh_token');
24
+  }
25
+
26
+  Future<bool> removeTokens() async {
27
+    await _prefs.remove('access_token');
28
+    await _prefs.remove('refresh_token');
29
+    return true;
20 30
   }
21 31
 
22 32
   // 用户数据管理

+ 14
- 5
lib/data/datasources/remote/api_client.dart Dosyayı Görüntüle

@@ -78,14 +78,23 @@ class ApiClient {
78 78
     }
79 79
   }
80 80
   
81
-  // 保存token
82
-  Future<void> saveToken(String token) async {
83
-    await _prefs.setString('access_token', token);
81
+  // 保存tokens
82
+  Future<void> saveTokens(String accessToken, String? refreshToken) async {
83
+    await _prefs.setString('access_token', accessToken);
84
+    if (refreshToken != null && refreshToken.isNotEmpty) {
85
+      await _prefs.setString('refresh_token', refreshToken);
86
+    }
84 87
   }
85 88
   
86
-  // 清除token
87
-  Future<void> clearToken() async {
89
+  // 清除所有token
90
+  Future<void> clearTokens() async {
88 91
     await _prefs.remove('access_token');
92
+    await _prefs.remove('refresh_token');
93
+  }
94
+
95
+  // 获取refresh_token
96
+  String? getRefreshToken() {
97
+    return _prefs.getString('refresh_token');
89 98
   }
90 99
   
91 100
   // 检查是否已登录

+ 156
- 24
lib/data/repositories/auth_repository.dart Dosyayı Görüntüle

@@ -19,13 +19,13 @@ class AuthRepository {
19 19
     required SharedPreferences prefs,
20 20
   }) : _apiClient = apiClient, _prefs = prefs;
21 21
   
22
-  // 登录(需要使用安全登录替代)
22
+  // 登录(需要使用安全登录替代)保存双token
23 23
   Future<ApiResponse<User>> login(LoginRequest request) async {
24 24
     try {
25 25
       final response = await _apiClient.post(
26 26
         ApiConstants.getLoginUrl(),
27 27
         request.toJson(),
28
-        withAuth: false,
28
+        withAuth: false,  // 登录不需要认证头
29 29
       );
30 30
       
31 31
       if (response.statusCode == 200) {
@@ -33,8 +33,17 @@ class AuthRepository {
33 33
           json.decode(response.body)
34 34
         );
35 35
         
36
-        // 保存token
37
-        await _apiClient.saveToken(tokenResponse.accessToken);
36
+        // 保存access_token和refresh_token
37
+        await _apiClient.saveTokens(
38
+          tokenResponse.accessToken,
39
+          tokenResponse.refreshToken,
40
+        );
41
+        
42
+        // 保存用户数据
43
+        await _prefs.setString(
44
+          'user_data',
45
+          json.encode(tokenResponse.user.toJson()),
46
+        );
38 47
         
39 48
         return ApiResponse<User>(
40 49
           success: true,
@@ -56,7 +65,7 @@ class AuthRepository {
56 65
     }
57 66
   }
58 67
   
59
-  // 注册(需要使用安全注册替代)
68
+  // 注册(需要使用安全注册替代)保存双token
60 69
   Future<ApiResponse<User>> register(RegisterRequest request) async {
61 70
     try {
62 71
       final response = await _apiClient.post(
@@ -70,8 +79,17 @@ class AuthRepository {
70 79
           json.decode(response.body)
71 80
         );
72 81
         
73
-        // 保存token
74
-        await _apiClient.saveToken(tokenResponse.accessToken);
82
+        // 保存双token
83
+        await _apiClient.saveTokens(
84
+          tokenResponse.accessToken,
85
+          tokenResponse.refreshToken,
86
+        );
87
+        
88
+        // 保存用户数据
89
+        await _prefs.setString(
90
+          'user_data',
91
+          json.encode(tokenResponse.user.toJson()),
92
+        );
75 93
         
76 94
         return ApiResponse<User>(
77 95
           success: true,
@@ -101,16 +119,12 @@ class AuthRepository {
101 119
         username: request.username,
102 120
         password: request.password,
103 121
       );
104
-
105
-      print(ApiConstants.getLoginUrl());
106
-      print(secureRequest.toJson());
107 122
       
108 123
       final response = await _apiClient.post(
109 124
         ApiConstants.getLoginUrl(),
110 125
         secureRequest.toJson(),
111 126
         withAuth: false,
112 127
       );
113
-      print(response.statusCode);
114 128
       
115 129
       // ... 处理响应
116 130
       if (response.statusCode == 200) {
@@ -118,8 +132,17 @@ class AuthRepository {
118 132
           json.decode(response.body)
119 133
         );
120 134
         
121
-        // 保存token
122
-        await _apiClient.saveToken(tokenResponse.accessToken);
135
+        // 保存access_token和refresh_token
136
+        await _apiClient.saveTokens(
137
+          tokenResponse.accessToken,
138
+          tokenResponse.refreshToken,
139
+        );
140
+        
141
+        // 保存用户数据
142
+        await _prefs.setString(
143
+          'user_data',
144
+          json.encode(tokenResponse.user.toJson()),
145
+        );
123 146
         
124 147
         return ApiResponse<User>(
125 148
           success: true,
@@ -169,8 +192,17 @@ class AuthRepository {
169 192
           json.decode(response.body)
170 193
         );
171 194
         
172
-        // 保存token
173
-        await _apiClient.saveToken(tokenResponse.accessToken);
195
+        // 保存双token
196
+        await _apiClient.saveTokens(
197
+          tokenResponse.accessToken,
198
+          tokenResponse.refreshToken,
199
+        );
200
+        
201
+        // 保存用户数据
202
+        await _prefs.setString(
203
+          'user_data',
204
+          json.encode(tokenResponse.user.toJson()),
205
+        );
174 206
         
175 207
         return ApiResponse<User>(
176 208
           success: true,
@@ -202,11 +234,28 @@ class AuthRepository {
202 234
       
203 235
       if (response.statusCode == 200) {
204 236
         final userData = json.decode(response.body);
237
+        final user = User.fromJson(userData);
238
+        
239
+        // 更新本地用户数据
240
+        await _prefs.setString('user_data', json.encode(user.toJson()));
241
+        
205 242
         return ApiResponse<User>(
206 243
           success: true,
207 244
           message: '获取成功',
208
-          data: User.fromJson(userData),
245
+          data: user,
209 246
         );
247
+      } else if (response.statusCode == 401) {
248
+        // Token过期,尝试刷新
249
+        final refreshResult = await _refreshToken();
250
+        if (refreshResult) {
251
+          // 刷新成功,重新获取用户信息
252
+          return await getCurrentUser();
253
+        } else {
254
+          return ApiResponse<User>(
255
+            success: false,
256
+            message: '登录已过期,请重新登录',
257
+          );
258
+        }
210 259
       } else {
211 260
         return ApiResponse<User>(
212 261
           success: false,
@@ -220,28 +269,111 @@ class AuthRepository {
220 269
       );
221 270
     }
222 271
   }
223
-  
224
-  // 登出
225
-  Future<bool> logout() async {
272
+
273
+  // 刷新Token
274
+  Future<bool> _refreshToken() async {
226 275
     try {
276
+      final refreshToken = _apiClient.getRefreshToken();
277
+      
278
+      if (refreshToken == null || refreshToken.isEmpty) {
279
+        return false;
280
+      }
281
+      
227 282
       final response = await _apiClient.post(
228
-        ApiConstants.getLogoutUrl(),
229
-        {},
230
-        withAuth: true,
283
+        ApiConstants.getRefreshUrl(),  // 需要添加这个常量
284
+        {'refresh_token': refreshToken},
285
+        withAuth: false,
231 286
       );
232 287
       
233 288
       if (response.statusCode == 200) {
234
-        await _apiClient.clearToken();
289
+        final data = json.decode(response.body);
290
+        final newAccessToken = data['access_token'];
291
+        final newRefreshToken = data['refresh_token'];
292
+        
293
+        // 保存新的tokens
294
+        await _apiClient.saveTokens(newAccessToken, newRefreshToken);
235 295
         return true;
236 296
       }
297
+      
237 298
       return false;
238 299
     } catch (e) {
239 300
       return false;
240 301
     }
241 302
   }
242 303
   
304
+  // 登出
305
+  Future<ApiResponse<bool>> logout() async {
306
+    try {
307
+      // 获取refresh_token
308
+      final refreshToken = _apiClient.getRefreshToken();
309
+      
310
+      if (refreshToken == null || refreshToken.isEmpty) {
311
+        // 没有refresh_token,只清除本地数据
312
+        await _clearLocalData();
313
+        return ApiResponse<bool>(
314
+          success: true,
315
+          message: '已退出登录',
316
+          data: true,
317
+        );
318
+      }
319
+      
320
+      // 发送登出请求到服务器
321
+      final response = await _apiClient.post(
322
+        ApiConstants.getLogoutUrl(),
323
+        {'refresh_token': refreshToken},
324
+        withAuth: true,  // 需要access_token认证头
325
+      );
326
+      
327
+      // 无论服务器响应如何,都清除本地数据
328
+      await _clearLocalData();
329
+      
330
+      if (response.statusCode == 200) {
331
+        return ApiResponse<bool>(
332
+          success: true,
333
+          message: '已成功退出登录',
334
+          data: true,
335
+        );
336
+      } else {
337
+        // 服务器登出失败,但本地已清除
338
+        return ApiResponse<bool>(
339
+          success: true,
340
+          message: '已退出登录(服务器通信失败)',
341
+          data: true,
342
+        );
343
+      }
344
+    } catch (e) {
345
+      // 网络异常,仍然清除本地数据
346
+      await _clearLocalData();
347
+      return ApiResponse<bool>(
348
+        success: true,
349
+        message: '已退出登录(网络异常)',
350
+        data: true,
351
+      );
352
+    }
353
+  }
354
+
355
+  // 清除本地数据
356
+  Future<void> _clearLocalData() async {
357
+    await _apiClient.clearTokens();
358
+    await _prefs.remove('user_data');
359
+  }
360
+  
243 361
   // 检查登录状态
244 362
   Future<bool> isLoggedIn() async {
245
-    return _apiClient.isLoggedIn();
363
+    // 检查是否有access_token
364
+    if (!_apiClient.isLoggedIn()) {
365
+      return false;
366
+    }
367
+    
368
+    // 可以进一步验证token是否有效
369
+    try {
370
+      final response = await _apiClient.get(
371
+        ApiConstants.getCurrentUserUrl(),
372
+        withAuth: true,
373
+      );
374
+      return response.statusCode == 200;
375
+    } catch (e) {
376
+      return false;
377
+    }
246 378
   }
247 379
 }

+ 3
- 2
lib/main.dart Dosyayı Görüntüle

@@ -1,5 +1,6 @@
1 1
 import 'package:flutter/material.dart';
2 2
 import 'package:provider/provider.dart';
3
+import 'core/constants/route_constants.dart';
3 4
 import 'presentation/providers/auth_provider.dart';
4 5
 import 'presentation/providers/user_provider.dart';
5 6
 import 'presentation/navigation/app_router.dart';
@@ -7,7 +8,7 @@ import 'injection_container.dart' as di;
7 8
 
8 9
 void main() async {
9 10
   WidgetsFlutterBinding.ensureInitialized();
10
-  await di.init();
11
+  await di.init();  // 初始化DI容器
11 12
   runApp(const MyApp());
12 13
 }
13 14
 
@@ -42,7 +43,7 @@ class MyApp extends StatelessWidget {
42 43
           ),
43 44
         ),
44 45
         debugShowCheckedModeBanner: false,
45
-        initialRoute: '/',
46
+        initialRoute: RouteConstants.splash,
46 47
         onGenerateRoute: AppRouter.onGenerateRoute,
47 48
       ),
48 49
     );

+ 21
- 2
lib/presentation/navigation/app_router.dart Dosyayı Görüntüle

@@ -8,26 +8,45 @@ import '../screens/profile/profile_screen.dart';
8 8
 import '../screens/profile/profile_detail_screen.dart';
9 9
 import '../screens/splash_screen.dart';
10 10
 import '../../core/constants/route_constants.dart';
11
+import 'route_guards.dart';
11 12
 
12 13
 class AppRouter {
13 14
   static Route<dynamic> onGenerateRoute(RouteSettings settings) {
14 15
     switch (settings.name) {
15 16
       case RouteConstants.splash:
16 17
         return MaterialPageRoute(builder: (_) => const SplashScreen());
18
+      
17 19
       case RouteConstants.home:
18 20
         return MaterialPageRoute(builder: (_) => const HomeScreen());
21
+      
19 22
       case RouteConstants.login:
20 23
         return MaterialPageRoute(builder: (_) => const LoginScreen());
24
+      
21 25
       case RouteConstants.register:
22 26
         return MaterialPageRoute(builder: (_) => const RegisterScreen());
27
+      
23 28
       case RouteConstants.news:
24 29
         return MaterialPageRoute(builder: (_) => const NewsScreen());
30
+      
25 31
       case RouteConstants.services:
26 32
         return MaterialPageRoute(builder: (_) => const ServicesScreen());
33
+      
27 34
       case RouteConstants.profile:
28
-        return MaterialPageRoute(builder: (_) => const ProfileScreen());
35
+        return MaterialPageRoute(
36
+          builder: (_) => ProtectedRoute(
37
+            routeName: RouteConstants.profile,
38
+            child: const ProfileScreen(),
39
+          ),
40
+        );
41
+      
29 42
       case RouteConstants.profileDetail:
30
-        return MaterialPageRoute(builder: (_) => const ProfileDetailScreen());
43
+        return MaterialPageRoute(
44
+          builder: (_) => ProtectedRoute(
45
+            routeName: RouteConstants.profileDetail,
46
+            child: const ProfileDetailScreen(),
47
+          ),
48
+        );
49
+      
31 50
       default:
32 51
         return MaterialPageRoute(
33 52
           builder: (_) => Scaffold(

+ 15
- 4
lib/presentation/navigation/route_guards.dart Dosyayı Görüntüle

@@ -5,8 +5,13 @@ import '../providers/auth_provider.dart';
5 5
 
6 6
 class ProtectedRoute extends StatelessWidget {
7 7
   final Widget child;
8
+  final String? routeName;
8 9
   
9
-  const ProtectedRoute({Key? key, required this.child}) : super(key: key);
10
+  const ProtectedRoute({
11
+    super.key,
12
+    required this.child,
13
+    this.routeName,
14
+  });
10 15
   
11 16
   @override
12 17
   Widget build(BuildContext context) {
@@ -17,9 +22,15 @@ class ProtectedRoute extends StatelessWidget {
17 22
     } else {
18 23
       return LoginScreen(
19 24
         onSuccess: () {
20
-          Navigator.of(context).pushReplacement(
21
-            MaterialPageRoute(builder: (_) => child),
22
-          );
25
+          // 如果有routeName,可以使用pushReplacementNamed
26
+          if (routeName != null) {
27
+            Navigator.of(context).pushReplacementNamed(routeName!);
28
+          } else {
29
+            // 否则使用原来的方式
30
+            Navigator.of(context).pushReplacement(
31
+              MaterialPageRoute(builder: (_) => child),
32
+            );
33
+          }
23 34
         },
24 35
       );
25 36
     }

+ 21
- 5
lib/presentation/providers/auth_provider.dart Dosyayı Görüntüle

@@ -21,9 +21,9 @@ class AuthProvider with ChangeNotifier {
21 21
   String? get error => _error;
22 22
   
23 23
   Future<void> _loadCurrentUser() async {
24
-    final user = await authRepository.getCurrentUser();
25
-    if (user != null) {
26
-      _user = user.data;
24
+    final response = await authRepository.getCurrentUser();
25
+    if (response.success && response.data != null) {
26
+      _user = response.data;
27 27
       notifyListeners();
28 28
     }
29 29
   }
@@ -77,9 +77,25 @@ class AuthProvider with ChangeNotifier {
77 77
   }
78 78
   
79 79
   Future<void> logout() async {
80
-    await authRepository.logout();
81
-    _user = null;
80
+    _isLoading = true;
81
+    _error = null;
82 82
     notifyListeners();
83
+    
84
+    try {
85
+      final response = await authRepository.logout();
86
+      
87
+      if (response.success) {
88
+        _user = null;
89
+        _error = null;
90
+      } else {
91
+        _error = response.message;
92
+      }
93
+    } catch (e) {
94
+      _error = '登出失败: $e';
95
+    } finally {
96
+      _isLoading = false;
97
+      notifyListeners();
98
+    }
83 99
   }
84 100
   
85 101
   Future<void> checkAuthStatus() async {

+ 8
- 4
lib/presentation/screens/auth/login_screen.dart Dosyayı Görüntüle

@@ -9,7 +9,10 @@ import '../../widgets/common/loading_indicator.dart';
9 9
 class LoginScreen extends StatefulWidget {
10 10
   final VoidCallback? onSuccess;
11 11
   
12
-  const LoginScreen({super.key, this.onSuccess});
12
+  const LoginScreen({
13
+    super.key,
14
+    this.onSuccess,
15
+  });
13 16
   
14 17
   @override
15 18
   State<LoginScreen> createState() => _LoginScreenState();
@@ -17,8 +20,8 @@ class LoginScreen extends StatefulWidget {
17 20
 
18 21
 class _LoginScreenState extends State<LoginScreen> {
19 22
   final _formKey = GlobalKey<FormState>();
20
-  final _emailController = TextEditingController(text: 'test@example.com');
21
-  final _passwordController = TextEditingController(text: '123456');
23
+  final _emailController = TextEditingController(text: 'aaa');
24
+  final _passwordController = TextEditingController(text: 'Heweidabangzi77!');
22 25
   
23 26
   @override
24 27
   void dispose() {
@@ -117,8 +120,9 @@ class _LoginScreenState extends State<LoginScreen> {
117 120
                           _passwordController.text,
118 121
                         );
119 122
                         if (authProvider.isAuthenticated) {
120
-                          widget.onSuccess?.call();
123
+                          // widget.onSuccess?.call();
121 124
                           Navigator.of(context).pushReplacementNamed(RouteConstants.home);
125
+                          // Navigator.of(context).pushNamedAndRemoveUntil(RouteConstants.home);
122 126
                         }
123 127
                       }
124 128
                     },

+ 57
- 9
lib/presentation/screens/profile/profile_screen.dart Dosyayı Görüntüle

@@ -1,5 +1,6 @@
1 1
 import 'package:flutter/material.dart';
2 2
 import 'package:provider/provider.dart';
3
+import '../../../core/constants/route_constants.dart';
3 4
 import '../../providers/auth_provider.dart';
4 5
 import '../../providers/user_provider.dart';
5 6
 import '../../navigation/bottom_nav_bar.dart';
@@ -11,12 +12,8 @@ class ProfileScreen extends StatelessWidget {
11 12
 
12 13
   @override
13 14
   Widget build(BuildContext context) {
14
-    return ProtectedWidget(
15
-      child: _ProfileContent(),
16
-      loadingWidget: const Center(
17
-        child: CircularProgressIndicator(),
18
-      ),
19
-    );
15
+    // 直接显示内容,认证由路由层处理
16
+    return _ProfileContent();
20 17
   }
21 18
 }
22 19
 
@@ -231,9 +228,60 @@ class _ProfileContentState extends State<_ProfileContent> {
231 228
                         title: '退出登录',
232 229
                         subtitle: '安全退出当前账号',
233 230
                         onTap: () async {
234
-                          await authProvider.logout();
235
-                          if (mounted) {
236
-                            Navigator.of(context).pushReplacementNamed('/');
231
+                          // 防止重复点击
232
+                          if (authProvider.isLoading) return;
233
+
234
+                          final shouldLogout = await showDialog<bool>(
235
+                            context: context,
236
+                            builder: (context) {
237
+                              final isIOS = Theme.of(context).platform == TargetPlatform.iOS;
238
+                              
239
+                              if (isIOS) {
240
+                                // iOS风格:确定在右边,取消在左边
241
+                                return AlertDialog(
242
+                                  title: const Text('确认退出'),
243
+                                  content: const Text('确定要退出登录吗?'),
244
+                                  actionsAlignment: MainAxisAlignment.spaceBetween,
245
+                                  actions: [
246
+                                    TextButton(
247
+                                      onPressed: () => Navigator.of(context).pop(true),
248
+                                      child: const Text('确定'),
249
+                                    ),
250
+                                    TextButton(
251
+                                      onPressed: () => Navigator.of(context).pop(false),
252
+                                      child: const Text('取消'),
253
+                                    ),
254
+                                  ],
255
+                                );
256
+                              } else {
257
+                                // Android/Material风格:取消在左边,确定在右边
258
+                                return AlertDialog(
259
+                                  title: const Text('确认退出'),
260
+                                  content: const Text('确定要退出登录吗?'),
261
+                                  actionsAlignment: MainAxisAlignment.spaceBetween,
262
+                                  actions: [
263
+                                    TextButton(
264
+                                      onPressed: () => Navigator.of(context).pop(false),
265
+                                      child: const Text('取消'),
266
+                                    ),
267
+                                    TextButton(
268
+                                      onPressed: () => Navigator.of(context).pop(true),
269
+                                      child: const Text('确定'),
270
+                                    ),
271
+                                  ],
272
+                                );
273
+                              }
274
+                            },
275
+                          );
276
+                          
277
+                          if (shouldLogout == true) {
278
+                            await authProvider.logout();
279
+
280
+                            // 登出后,确保在组件仍然存在时进行导航
281
+                            if (mounted) {
282
+                              // 使用pushReplacementNamed清空导航栈
283
+                              Navigator.of(context).pushReplacementNamed(RouteConstants.home);
284
+                            }
237 285
                           }
238 286
                         },
239 287
                       ),

+ 3
- 0
lib/presentation/widgets/custom/protected_widget.dart Dosyayı Görüntüle

@@ -18,10 +18,12 @@ class ProtectedWidget extends StatelessWidget {
18 18
   Widget build(BuildContext context) {
19 19
     final authProvider = Provider.of<AuthProvider>(context);
20 20
     
21
+    // 如果正在加载,显示loading
21 22
     if (authProvider.isLoading) {
22 23
       return loadingWidget ?? const LoadingIndicator();
23 24
     }
24 25
     
26
+    // 如果未认证,显示登录页
25 27
     if (!authProvider.isAuthenticated) {
26 28
       return LoginScreen(
27 29
         onSuccess: () {
@@ -30,6 +32,7 @@ class ProtectedWidget extends StatelessWidget {
30 32
       );
31 33
     }
32 34
     
35
+    // 已认证,显示子组件
33 36
     return child;
34 37
   }
35 38
 }