import 'dart:convert'; import 'package:crypto/crypto.dart'; 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'; import 'package:im_app/domain/repositories/user_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) /// → UserRepository.insertOrReplaceUser(user) /// ← User /// ``` class LoginUseCase { final AuthRepository _authRepository; final SocketManager _socketManager; final ApiConfig _apiConfig; final StorageSdkApi _storageApi; final UserRepository _userRepository; StorageSdkLifecycle get _storageLifeCycle => _storageApi as StorageSdkLifecycle; LoginUseCase({ required AuthRepository authRepository, required SocketManager socketManager, required ApiConfig apiConfig, required StorageSdkApi storageApi, required UserRepository userRepository, }) : _authRepository = authRepository, _socketManager = socketManager, _apiConfig = apiConfig, _storageApi = storageApi, _userRepository = userRepository; /// 步骤 1:发送手机验证码 /// /// 抛出: /// - [FormatException] — 手机号格式不合法 /// - [ApiError] — 网络/服务端错误 Future sendOtp({ required String countryCode, required String contact, }) async { _validatePhone(contact); await _authRepository.sendOtp(countryCode: countryCode, contact: contact); } /// 步骤 2+3:校验验证码并完成登录,返回 [User] /// /// 内部串行:verifyOtp → login → connectWebSocket → openDatabase → saveUser /// /// 抛出: /// - [FormatException] — 验证码格式不合法 /// - [ApiError] — 网络/服务端错误 Future 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, ); // 连接 WebSocket(token 已由 Repository 写入 ApiConfig,直接读取) final token = _apiConfig.token; if (token != null && token.isNotEmpty) { await _socketManager.connect(token: token); } // 按用户 uid 打开本地数据库 await _storageLifeCycle.openDatabase(user.uid); // 持久化登录用户信息 await _userRepository.insertOrReplaceUser(user); // TODO: 扩展点 — 同步联系人列表、注册推送 token return user; } /// 步骤 2b:用户输入二级密码后完成登录 /// /// 在 [verifyAndLogin] 抛出 [SecondaryPasscodeRequiredException] 后调用: /// - MD5 哈希用户输入的密码 /// - 调 [AuthRepository.loginWithPasscode] /// - 初始化 WebSocket / 数据库(与 [verifyAndLogin] 后半段一致) /// /// 抛出: /// - [FormatException] — 密码为空 /// - [ApiError] — 网络/服务端错误(密码错误 → 服务端返回错误码) Future loginWithSecondaryPasscode({ required String countryCode, required String contact, required String vcodeToken, required String passcode, }) async { if (passcode.trim().isEmpty) { throw const FormatException('密码不能为空'); } // MD5 哈希(对齐旧版 makeMD5 逻辑) final bytes = utf8.encode(passcode); final passwordMd5 = md5.convert(bytes).toString(); final user = await _authRepository.loginWithPasscode( countryCode: countryCode, contact: contact, vcodeToken: vcodeToken, passwordMd5: passwordMd5, ); final token = _apiConfig.token; if (token != null && token.isNotEmpty) { await _socketManager.connect(token: token); } await _storageLifeCycle.openDatabase(user.uid); await _userRepository.insertOrReplaceUser(user); 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 } } }