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)), ); } }