网络请求打通,ws 打通
This commit is contained in:
@@ -1,38 +1,36 @@
|
||||
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';
|
||||
|
||||
/// 登录用例
|
||||
///
|
||||
/// 封装登录的完整业务流程:
|
||||
/// 格式校验 → 调 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)
|
||||
/// ← LoginResponse(SDK 已拆包 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)
|
||||
/// ← User
|
||||
/// ```
|
||||
class LoginUseCase {
|
||||
@@ -54,62 +52,84 @@ class LoginUseCase {
|
||||
_apiConfig = apiConfig,
|
||||
_storageApi = storageApi;
|
||||
|
||||
/// 执行登录
|
||||
///
|
||||
/// 1. 格式校验 → 不合法直接抛 [FormatException]
|
||||
/// 2. 调 Repository 登录 → 拿到 User(token 写入由 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
|
||||
///
|
||||
/// 抛出:
|
||||
/// - [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,
|
||||
);
|
||||
|
||||
// 连接 WebSocket(token 已由 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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user