Initial commit
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../shared/utils/app_logger.dart';
|
||||
import '../../../shared/utils/url_utils.dart';
|
||||
import '../../domain/ports/sm_account_gateway.dart';
|
||||
|
||||
|
||||
class SmAccountClient implements SmAccountGateway {
|
||||
static const _tag = 'SmAccount';
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
SmAccountClient({Dio? dio})
|
||||
: _dio =
|
||||
dio ??
|
||||
Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 8),
|
||||
receiveTimeout: const Duration(seconds: 12),
|
||||
headers: {'Accept': 'application/json'},
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
Future<T> _guard<T>(
|
||||
String op,
|
||||
Future<T> Function() body, {
|
||||
required T Function(String message) failure,
|
||||
required String fallbackMessage,
|
||||
}) async {
|
||||
try {
|
||||
return await body();
|
||||
} on DioException catch (e) {
|
||||
AppLogger.warn(_tag, '$op network error', e);
|
||||
return failure(_networkHint(e));
|
||||
} catch (e) {
|
||||
AppLogger.warn(_tag, '$op unexpected error', e);
|
||||
return failure(fallbackMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SmLoginResult> login({
|
||||
required String baseUrl,
|
||||
required String email,
|
||||
required String password,
|
||||
}) => _guard('login', () async {
|
||||
final url = '${stripTrailingSlash(baseUrl.trim())}/api/user/login';
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
url,
|
||||
data: {'email': email, 'password': password},
|
||||
);
|
||||
return _sessionResultFrom(res, fallbackEmail: email);
|
||||
}, failure: SmLoginResult.failure, fallbackMessage: '登录失败,请稍后重试');
|
||||
|
||||
@override
|
||||
Future<SmActionResult> registerStart({
|
||||
required String baseUrl,
|
||||
required String email,
|
||||
}) => _guard('registerStart', () async {
|
||||
final url = '${stripTrailingSlash(baseUrl.trim())}/api/user/register/start';
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
url,
|
||||
data: {'email': email},
|
||||
);
|
||||
return _actionResultFrom(res, 'registerStart');
|
||||
}, failure: SmActionResult.failure, fallbackMessage: '发送验证码失败,请稍后重试');
|
||||
|
||||
@override
|
||||
Future<SmLoginResult> registerVerify({
|
||||
required String baseUrl,
|
||||
required String email,
|
||||
required String password,
|
||||
required String code,
|
||||
}) => _guard('registerVerify', () async {
|
||||
final url =
|
||||
'${stripTrailingSlash(baseUrl.trim())}/api/user/register/verify';
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
url,
|
||||
data: {'email': email, 'password': password, 'code': code},
|
||||
);
|
||||
return _sessionResultFrom(res, fallbackEmail: email);
|
||||
}, failure: SmLoginResult.failure, fallbackMessage: '验证失败,请稍后重试');
|
||||
|
||||
@override
|
||||
Future<SmActionResult> resendCode({
|
||||
required String baseUrl,
|
||||
required String email,
|
||||
}) => _guard('resendCode', () async {
|
||||
final url = '${stripTrailingSlash(baseUrl.trim())}/api/user/resend-code';
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
url,
|
||||
data: {'email': email, 'purpose': 'register'},
|
||||
);
|
||||
return _actionResultFrom(res, 'resendCode');
|
||||
}, failure: SmActionResult.failure, fallbackMessage: '重发验证码失败,请稍后重试');
|
||||
|
||||
@override
|
||||
Future<SmTokenRefreshResult> refreshAccessToken({
|
||||
required String baseUrl,
|
||||
required String refreshToken,
|
||||
}) => _guard('refreshAccessToken', () async {
|
||||
final url = '${stripTrailingSlash(baseUrl.trim())}/api/user/token/refresh';
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
url,
|
||||
data: {'refreshToken': refreshToken},
|
||||
);
|
||||
final status = res.statusCode ?? 0;
|
||||
final body = res.data ?? const <String, dynamic>{};
|
||||
if (_isSuccess(status) && body['ok'] == true) {
|
||||
return SmTokenRefreshResult(
|
||||
ok: true,
|
||||
accessToken: (body['accessToken'] as String?) ?? '',
|
||||
refreshToken: (body['refreshToken'] as String?) ?? refreshToken,
|
||||
expiresIn: (body['expiresIn'] as num?)?.toInt() ?? 0,
|
||||
isVip: body['isVip'] == true,
|
||||
vipExpiresAt: (body['vipExpiresAt'] as num?)?.toInt(),
|
||||
vipPermanent: body['vipPermanent'] == true,
|
||||
hasVipInfo: body.containsKey('isVip'),
|
||||
);
|
||||
}
|
||||
final message = (body['message'] as String?)?.trim();
|
||||
AppLogger.warn(_tag, 'refreshAccessToken ← HTTP $status ${message ?? ''}');
|
||||
return SmTokenRefreshResult.failure(
|
||||
message ?? _statusHint(status),
|
||||
invalidRefreshToken: status == 401 || status == 403,
|
||||
);
|
||||
}, failure: SmTokenRefreshResult.failure, fallbackMessage: 'Token 刷新失败,请稍后重试');
|
||||
|
||||
|
||||
SmActionResult _actionResultFrom(
|
||||
Response<Map<String, dynamic>> res,
|
||||
String op,
|
||||
) {
|
||||
final body = res.data ?? const <String, dynamic>{};
|
||||
if (_isSuccess(res.statusCode) && body['ok'] == true) {
|
||||
return SmActionResult.success();
|
||||
}
|
||||
final message = (body['message'] as String?)?.trim();
|
||||
AppLogger.warn(_tag, '$op ← HTTP ${res.statusCode} ${message ?? ''}');
|
||||
return SmActionResult.failure(message ?? _statusHint(res.statusCode ?? 0));
|
||||
}
|
||||
|
||||
|
||||
SmLoginResult _sessionResultFrom(
|
||||
Response<Map<String, dynamic>> res, {
|
||||
required String fallbackEmail,
|
||||
}) {
|
||||
final status = res.statusCode ?? 0;
|
||||
final body = res.data ?? const <String, dynamic>{};
|
||||
final ok = body['ok'] == true;
|
||||
|
||||
if (_isSuccess(status) && ok) {
|
||||
final refreshToken = (body['refreshToken'] as String?)?.trim() ?? '';
|
||||
if (refreshToken.isEmpty) {
|
||||
AppLogger.warn(_tag, 'auth ok but missing refresh token');
|
||||
return SmLoginResult.failure('认证成功但未收到登录凭据,请稍后重试');
|
||||
}
|
||||
return SmLoginResult(
|
||||
ok: true,
|
||||
email: (body['email'] as String?)?.trim() ?? fallbackEmail.trim(),
|
||||
accessToken: (body['accessToken'] as String?) ?? '',
|
||||
refreshToken: refreshToken,
|
||||
expiresIn: (body['expiresIn'] as num?)?.toInt() ?? 0,
|
||||
isVip: body['isVip'] == true,
|
||||
vipExpiresAt: (body['vipExpiresAt'] as num?)?.toInt(),
|
||||
vipPermanent: body['vipPermanent'] == true,
|
||||
);
|
||||
}
|
||||
|
||||
final message = (body['message'] as String?)?.trim();
|
||||
final needsVerification = body['needsVerification'] == true;
|
||||
AppLogger.warn(_tag, 'auth ← HTTP $status ${message ?? ''}');
|
||||
return SmLoginResult.failure(
|
||||
message ?? _statusHint(status),
|
||||
needsVerification: needsVerification,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logout({
|
||||
required String baseUrl,
|
||||
required String refreshToken,
|
||||
}) async {
|
||||
final token = refreshToken.trim();
|
||||
if (token.isEmpty) return;
|
||||
final url = '${stripTrailingSlash(baseUrl.trim())}/api/user/token/logout';
|
||||
try {
|
||||
await _dio.post<Map<String, dynamic>>(url, data: {'refreshToken': token});
|
||||
} catch (e) {
|
||||
AppLogger.warn(_tag, 'logout failed (non-fatal)', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool _isSuccess(int? status) =>
|
||||
status != null && status >= 200 && status < 300;
|
||||
|
||||
static String _statusHint(int status) => switch (status) {
|
||||
401 => '邮箱或密码错误',
|
||||
403 => '邮箱尚未验证',
|
||||
429 => '请求过于频繁,请稍后再试',
|
||||
_ => '请求失败(HTTP $status)',
|
||||
};
|
||||
|
||||
static String _networkHint(DioException e) => switch (e.type) {
|
||||
DioExceptionType.connectionTimeout ||
|
||||
DioExceptionType.receiveTimeout ||
|
||||
DioExceptionType.sendTimeout => '连接超时,请检查网络或服务器地址',
|
||||
DioExceptionType.connectionError => '无法连接服务器,请检查服务器地址',
|
||||
_ => '网络异常,请稍后重试',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user