cc687ae0c3
- auth_state: 增加 refreshToken 字段 + flutter_secure_storage 持久化 - api_client: 增加 401 自动 refresh + retry 拦截器 - auth_repository: 封装 POST /api/v1/auth/login 调用,错误信息本地化 - login_screen: 增加门店编号字段,调真实 API,行内错误展示 - main: 启动时 restore 持久化登录态,避免每次重启都要重新登录 - 修复 app_shell: hotelName → hotelNo - 修复 widget_test: 移除失效的占位测试 测试账号:门店编号 H001 / 用户名 admin / 密码 password123 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
2.9 KiB
Dart
91 lines
2.9 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../auth/auth_state.dart';
|
|
|
|
const _baseUrl = 'http://localhost:8080/api/v1';
|
|
|
|
/// Public Dio instance for unauthenticated calls (login / refresh)
|
|
final _publicDio = Dio(BaseOptions(
|
|
baseUrl: _baseUrl,
|
|
connectTimeout: const Duration(seconds: 10),
|
|
receiveTimeout: const Duration(seconds: 30),
|
|
headers: {'Content-Type': 'application/json'},
|
|
));
|
|
|
|
final apiClientProvider = Provider<ApiClient>((ref) {
|
|
final authState = ref.watch(authStateProvider);
|
|
return ApiClient(
|
|
token: authState.user?.accessToken,
|
|
refreshToken: authState.user?.refreshToken,
|
|
onTokenRefreshed: (newToken) =>
|
|
ref.read(authStateProvider.notifier).updateAccessToken(newToken),
|
|
onAuthFailed: () => ref.read(authStateProvider.notifier).logout(),
|
|
);
|
|
});
|
|
|
|
class ApiClient {
|
|
late final Dio _dio;
|
|
|
|
ApiClient({
|
|
String? token,
|
|
String? refreshToken,
|
|
void Function(String newToken)? onTokenRefreshed,
|
|
void Function()? onAuthFailed,
|
|
}) {
|
|
_dio = Dio(BaseOptions(
|
|
baseUrl: _baseUrl,
|
|
connectTimeout: const Duration(seconds: 10),
|
|
receiveTimeout: const Duration(seconds: 30),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
if (token != null) 'Authorization': 'Bearer $token',
|
|
},
|
|
));
|
|
|
|
// 401 auto-refresh interceptor
|
|
if (refreshToken != null) {
|
|
_dio.interceptors.add(
|
|
InterceptorsWrapper(
|
|
onError: (DioException e, ErrorInterceptorHandler handler) async {
|
|
if (e.response?.statusCode == 401 && refreshToken.isNotEmpty) {
|
|
try {
|
|
final resp = await _publicDio.post('/auth/refresh', data: {
|
|
'refresh_token': refreshToken,
|
|
});
|
|
final newToken =
|
|
resp.data['data']['access_token'] as String;
|
|
onTokenRefreshed?.call(newToken);
|
|
// Retry original request with new token
|
|
final opts = e.requestOptions;
|
|
opts.headers['Authorization'] = 'Bearer $newToken';
|
|
final retryResp = await _dio.fetch(opts);
|
|
return handler.resolve(retryResp);
|
|
} catch (_) {
|
|
onAuthFailed?.call();
|
|
}
|
|
}
|
|
return handler.next(e);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<Response> get(String path, {Map<String, dynamic>? params}) =>
|
|
_dio.get(path, queryParameters: params);
|
|
|
|
Future<Response> post(String path, {dynamic data}) =>
|
|
_dio.post(path, data: data);
|
|
|
|
Future<Response> put(String path, {dynamic data}) =>
|
|
_dio.put(path, data: data);
|
|
|
|
Future<Response> delete(String path) => _dio.delete(path);
|
|
}
|
|
|
|
/// Unauthenticated client for login/refresh
|
|
class PublicApiClient {
|
|
static Future<Response> post(String path, {dynamic data}) =>
|
|
_publicDio.post(path, data: data);
|
|
}
|