Initial commit

This commit is contained in:
admin1
2026-07-14 11:11:36 +08:00
commit 656499cf94
604 changed files with 119518 additions and 0 deletions
@@ -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 => '无法连接服务器,请检查服务器地址',
_ => '网络异常,请稍后重试',
};
}