Merge branch 'dev' into happi/dev/database-update

# Conflicts:
#	apps/im_app/lib/features/login/presentation/login_view_model.dart
This commit is contained in:
Happi (哈比)
2026-03-09 20:17:03 +08:00
66 changed files with 1749 additions and 556 deletions

View File

@@ -1,38 +1,38 @@
import 'package:networks_sdk/networks_sdk.dart';
import 'package:storage_sdk/storage_sdk.dart';
import '../../../core/services/socket_manager.dart';
import '../../../domain/entities/user.dart';
import '../../../domain/repositories/auth_repository.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';
/// 登录用例
///
/// 封装登录的完整业务流程:
/// 格式校验 → 调 Repository 登录 → 初始化 WebSocket → 打开本地数据库 → 返回 User
/// - sendOtp格式校验 → 发短信
/// - verifyAndLogin格式校验 → 校验验证码 → 登录 → 初始化 WebSocket → 打开本地数据库
///
/// ## 为什么需要 UseCase
///
/// ViewModel 直接调 Repository 也能跑通,但登录有明确的多步业务规则:
/// - 格式校验(不发无效请求,省流量、减少服务端压力)
/// - 登录后初始化 WebSocket 连接
/// - 登录后按 user id 打开对应的本地数据库
///
/// 把这些规则封装在 UseCase 里ViewModel 只需一行调用。
/// 登录有明确的多步业务规则UseCase 把这些规则集中封装,
/// ViewModel 只需一行调用。
///
/// ## 数据流位置
///
/// ```
/// LoginViewModel.login(email, password)
/// → ★ LoginUseCase.execute() ★ ← 你在这里
/// → 格式校验(邮箱 + 密码
/// → AuthRepository.login()
/// → AuthRepositoryImpl.login()
/// → _client.executeRequest(LoginRequest)
/// LoginResponseSDK 已拆包 envelope
/// → _onTokenUpdate(accessToken) ← 回调写入 Token内存 + 持久化,由 Provider 层组合
/// ← LoginResponse.toEntity() → User
/// → SocketManager.connect(token) ← 登录后连接 WebSocket
/// → StorageSdkApi.openDatabase(user.id) ← 按用户 id 打开本地库
/// 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 {
@@ -40,6 +40,7 @@ class LoginUseCase {
final SocketManager _socketManager;
final ApiConfig _apiConfig;
final StorageSdkApi _storageApi;
final UserRepository _userRepository;
StorageSdkLifecycle get _storageLifeCycle =>
_storageApi as StorageSdkLifecycle;
@@ -49,67 +50,91 @@ class LoginUseCase {
required SocketManager socketManager,
required ApiConfig apiConfig,
required StorageSdkApi storageApi,
required UserRepository userRepository,
}) : _authRepository = authRepository,
_socketManager = socketManager,
_apiConfig = apiConfig,
_storageApi = storageApi;
_storageApi = storageApi,
_userRepository = userRepository;
/// 执行登录
///
/// 1. 格式校验 → 不合法直接抛 [FormatException]
/// 2. 调 Repository 登录 → 拿到 Usertoken 写入由 Repository 处理)
/// 3. 用已存入 ApiConfig 的 token 连接 WebSocket
/// 4. 按 user id 打开本地数据库
/// 步骤 1发送手机验证码
///
/// 抛出:
/// - [FormatException] — 邮箱或密码格式不合法
/// - [ApiError] — 网络/服务端错误(由 Repository 透传)
Future<User> execute({
required String email,
required String password,
/// - [FormatException] — 手机号格式不合法
/// - [ApiError] — 网络/服务端错误
Future<void> sendOtp({
required String countryCode,
required String contact,
}) async {
// ── 1. 格式校验 ──
_validateEmail(email);
_validatePassword(password);
_validatePhone(contact);
await _authRepository.sendOtp(countryCode: countryCode, contact: contact);
}
// ── 2. 登录 ──
final user = await _authRepository.login(email: email, password: password);
/// 步骤 2+3校验验证码并完成登录返回 [User]
///
/// 内部串行verifyOtp → login → connectWebSocket → openDatabase → saveUser
///
/// 抛出:
/// - [FormatException] — 验证码格式不合法
/// - [ApiError] — 网络/服务端错误
Future<User> verifyAndLogin({
required String countryCode,
required String contact,
required String code,
}) async {
_validateCode(code);
// ── 3. 连接 WebSocket ──
// token 在 Repository 的 _onTokenUpdate 回调中已写入 ApiConfig
// 此处直接读取,避免改动现有接口。
// 校验验证码,换取 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);
}
// ── 4. 打开数据库 ──
// TODO: 当服务端返回整型 uid 时,换成 user.uid目前用 hashCode 作为临时标识。
await _storageLifeCycle.openDatabase(user.hashCode);
// 按用户 uid 打开本地数据库
await _storageLifeCycle.openDatabase(user.uid);
// TODO: 后续扩展点
// - 同步联系人列表
// - 注册推送 token
// 持久化登录用户信息
await _userRepository.insertOrReplaceUser(user);
// TODO: 扩展点 — 同步联系人列表、注册推送 token
return user;
}
void _validateEmail(String email) {
if (email.trim().isEmpty) {
throw const FormatException('邮箱不能为空'); // TODO: 接入国际化
void _validatePhone(String contact) {
final trimmed = contact.trim();
if (trimmed.isEmpty) {
throw const FormatException('手机号不能为空'); // TODO: 接入国际化
}
final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
if (!emailRegex.hasMatch(email.trim())) {
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 _validatePassword(String password) {
if (password.isEmpty) {
throw const FormatException('密码不能为空'); // TODO: 接入国际化
void _validateCode(String code) {
final trimmed = code.trim();
if (trimmed.isEmpty) {
throw const FormatException('验证码不能为空'); // TODO: 接入国际化
}
if (password.length < 6) {
throw const FormatException('密码长度不能少于 6 位'); // TODO: 接入国际化
if (!RegExp(r'^\d+$').hasMatch(trimmed)) {
throw const FormatException('验证码只能包含数字'); // TODO: l10n
}
}
}