fix(auth): resetPassword 解除锁定 + 登录错误提示优化
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 <noreply@anthropic.com>
This commit is contained in:
parent
ff82bddbc6
commit
761dcb1115
|
|
@ -63,8 +63,9 @@ export class PasswordService {
|
|||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
// 修改密码,同时解除锁定(短信验证身份已通过,清除失败计数)
|
||||
await user.changePassword(dto.newPassword);
|
||||
user.unlock();
|
||||
await this.userRepository.save(user);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
|||
);
|
||||
return AuthResult.fromJson(response.data as Map<String, dynamic>);
|
||||
} catch (e) {
|
||||
if (e is UnauthorizedException || e is ForbiddenException || e is NetworkException) rethrow;
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<LoginPage> {
|
|||
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<void>(
|
||||
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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue