From 761dcb1115f746865f44e590fdbec4a2b5f25eb7 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 10 Mar 2026 19:46:06 -0700 Subject: [PATCH] =?UTF-8?q?fix(auth):=20resetPassword=20=E8=A7=A3=E9=99=A4?= =?UTF-8?q?=E9=94=81=E5=AE=9A=20+=20=E7=99=BB=E5=BD=95=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - password.service.ts: resetPassword 成功后调用 user.unlock(), 清除 loginFailCount 和 lockedUntil,避免用户改密后仍无法登录 Frontend: - api_client.dart: 401 响应提取后端真实错误消息,不再丢弃 - auth_remote_datasource.dart: loginWithPassword 直接 rethrow 已知异常类型,避免二次包装导致消息格式混乱 - login_page.dart: 登录失败按错误类型分类提示: · 账户锁定 → AlertDialog + "找回密码"按钮 · 还有尝试机会 → SnackBar(橙色) + "找回密码"Action · 其他错误 → 普通 SnackBar Co-Authored-By: Claude Sonnet 4.6 --- .../application/services/password.service.ts | 3 +- .../lib/core/network/api_client.dart | 10 +++- .../remote/auth_remote_datasource.dart | 1 + .../presentation/pages/auth/login_page.dart | 51 ++++++++++++++++++- 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/backend/services/auth-service/src/application/services/password.service.ts b/backend/services/auth-service/src/application/services/password.service.ts index a3f07012..c0bf6bb5 100644 --- a/backend/services/auth-service/src/application/services/password.service.ts +++ b/backend/services/auth-service/src/application/services/password.service.ts @@ -63,8 +63,9 @@ export class PasswordService { throw new NotFoundException('用户不存在'); } - // 修改密码 + // 修改密码,同时解除锁定(短信验证身份已通过,清除失败计数) await user.changePassword(dto.newPassword); + user.unlock(); await this.userRepository.save(user); } diff --git a/frontend/mining-app/lib/core/network/api_client.dart b/frontend/mining-app/lib/core/network/api_client.dart index ffd61a8e..e25b0535 100644 --- a/frontend/mining-app/lib/core/network/api_client.dart +++ b/frontend/mining-app/lib/core/network/api_client.dart @@ -146,7 +146,15 @@ class ApiClient { case DioExceptionType.badResponse: final statusCode = e.response?.statusCode; if (statusCode == 401) { - return UnauthorizedException(); + final data = e.response?.data; + final error = data is Map ? data['error'] : null; + final rawMsg = error is Map ? error['message'] : null; + final msg = rawMsg is String + ? rawMsg + : (rawMsg is List && rawMsg.isNotEmpty) + ? rawMsg[0].toString() + : null; + return UnauthorizedException(msg ?? '未授权,请重新登录'); } if (statusCode == 403) { final data = e.response?.data; diff --git a/frontend/mining-app/lib/data/datasources/remote/auth_remote_datasource.dart b/frontend/mining-app/lib/data/datasources/remote/auth_remote_datasource.dart index d1d5f452..b1a113d9 100644 --- a/frontend/mining-app/lib/data/datasources/remote/auth_remote_datasource.dart +++ b/frontend/mining-app/lib/data/datasources/remote/auth_remote_datasource.dart @@ -138,6 +138,7 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { ); return AuthResult.fromJson(response.data as Map); } catch (e) { + if (e is UnauthorizedException || e is ForbiddenException || e is NetworkException) rethrow; throw ServerException(e.toString()); } } diff --git a/frontend/mining-app/lib/presentation/pages/auth/login_page.dart b/frontend/mining-app/lib/presentation/pages/auth/login_page.dart index a47ca749..df066d29 100644 --- a/frontend/mining-app/lib/presentation/pages/auth/login_page.dart +++ b/frontend/mining-app/lib/presentation/pages/auth/login_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/router/routes.dart'; import '../../../core/constants/app_colors.dart'; +import '../../../core/error/exceptions.dart'; import '../../providers/user_providers.dart'; class LoginPage extends ConsumerStatefulWidget { @@ -45,9 +46,55 @@ class _LoginPageState extends ConsumerState { context.go(Routes.contribution); } } catch (e) { - if (mounted) { + if (!mounted) return; + final message = e is UnauthorizedException + ? e.message + : e is ForbiddenException + ? e.message + : e is NetworkException + ? e.message + : e.toString().replaceFirst(RegExp(r'^[A-Za-z]+Exception:\s*'), ''); + + if (message.contains('锁定')) { + // 账户已锁定 → 弹窗 + 找回密码 + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('账户已锁定'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('知道了', style: TextStyle(color: AppColors.textSecondaryOf(context))), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(ctx); + context.push(Routes.forgotPassword); + }, + style: ElevatedButton.styleFrom(backgroundColor: _orange), + child: const Text('找回密码', style: TextStyle(color: Colors.white)), + ), + ], + ), + ); + } else if (message.contains('还剩')) { + // 密码错误但还有机会 → SnackBar 提醒 + 找回密码入口 ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('登录失败: $e')), + SnackBar( + content: Text(message), + backgroundColor: Colors.orange.shade800, + duration: const Duration(seconds: 5), + action: SnackBarAction( + label: '找回密码', + textColor: Colors.white, + onPressed: () => context.push(Routes.forgotPassword), + ), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), ); } }