Files
customer-im-client-dev/apps/im_app/lib/features/login/usecases/login_usecase.dart
2026-03-09 19:05:55 +08:00

136 lines
4.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:networks_sdk/networks_sdk.dart';
import 'package:storage_sdk/storage_sdk.dart';
import 'package:im_app/core/services/socket_manager.dart';
import 'package:im_app/domain/entities/user.dart';
import 'package:im_app/domain/repositories/auth_repository.dart';
/// 登录用例
///
/// 封装登录的完整业务流程:
/// - sendOtp格式校验 → 发短信
/// - verifyAndLogin格式校验 → 校验验证码 → 登录 → 初始化 WebSocket → 打开本地数据库
///
/// ## 为什么需要 UseCase
///
/// 登录有明确的多步业务规则UseCase 把这些规则集中封装,
/// ViewModel 只需一行调用。
///
/// ## 数据流位置
///
/// ```
/// LoginViewModel.sendOtp(countryCode, contact)
/// → ★ LoginUseCase.sendOtp() ★ ← 你在这里(步骤 1
/// → 格式校验(手机号)
/// → AuthRepository.sendOtp()
///
/// LoginViewModel.verifyAndLogin(code)
/// → ★ LoginUseCase.verifyAndLogin() ★ ← 你在这里(步骤 2+3
/// → 格式校验(验证码)
/// → AuthRepository.verifyOtp() → vcode_token
/// → AuthRepository.login() → User + token
/// → SocketManager.connect(token)
/// → StorageSdkApi.openDatabase(uid)
/// ← User
/// ```
class LoginUseCase {
final AuthRepository _authRepository;
final SocketManager _socketManager;
final ApiConfig _apiConfig;
final StorageSdkApi _storageApi;
StorageSdkLifecycle get _storageLifeCycle =>
_storageApi as StorageSdkLifecycle;
LoginUseCase({
required AuthRepository authRepository,
required SocketManager socketManager,
required ApiConfig apiConfig,
required StorageSdkApi storageApi,
}) : _authRepository = authRepository,
_socketManager = socketManager,
_apiConfig = apiConfig,
_storageApi = storageApi;
/// 步骤 1发送手机验证码
///
/// 抛出:
/// - [FormatException] — 手机号格式不合法
/// - [ApiError] — 网络/服务端错误
Future<void> sendOtp({
required String countryCode,
required String contact,
}) async {
_validatePhone(contact);
await _authRepository.sendOtp(
countryCode: countryCode,
contact: contact,
);
}
/// 步骤 2+3校验验证码并完成登录返回 [User]
///
/// 内部串行verifyOtp → login → connectWebSocket → openDatabase
///
/// 抛出:
/// - [FormatException] — 验证码格式不合法
/// - [ApiError] — 网络/服务端错误
Future<User> verifyAndLogin({
required String countryCode,
required String contact,
required String code,
}) async {
_validateCode(code);
// 校验验证码,换取 vcode_token
final vcodeToken = await _authRepository.verifyOtp(
countryCode: countryCode,
contact: contact,
code: code,
);
// 用 vcode_token 登录token 写入由 Repository._onTokenUpdate 回调处理)
final user = await _authRepository.login(
countryCode: countryCode,
contact: contact,
vcodeToken: vcodeToken,
);
// 连接 WebSockettoken 已由 Repository 写入 ApiConfig直接读取
final token = _apiConfig.token;
if (token != null && token.isNotEmpty) {
await _socketManager.connect(token: token);
}
// 按用户 uid 打开本地数据库
await _storageLifeCycle.openDatabase(user.uid);
// TODO: 扩展点 — 同步联系人列表、注册推送 token
return user;
}
void _validatePhone(String contact) {
final trimmed = contact.trim();
if (trimmed.isEmpty) {
throw const FormatException('手机号不能为空'); // TODO: 接入国际化
}
if (trimmed.length < 7 || trimmed.length > 15) {
throw const FormatException('手机号长度不正确'); // TODO: 接入国际化
}
if (!RegExp(r'^\d+$').hasMatch(trimmed)) {
throw const FormatException('手机号只能包含数字'); // TODO: 接入国际化
}
}
void _validateCode(String code) {
final trimmed = code.trim();
if (trimmed.isEmpty) {
throw const FormatException('验证码不能为空'); // TODO: 接入国际化
}
if (!RegExp(r'^\d+$').hasMatch(trimmed)) {
throw const FormatException('验证码只能包含数字'); // TODO: l10n
}
}
}