Initial project

This commit is contained in:
Cody
2026-03-06 14:56:17 +08:00
parent 977b627b15
commit bf9e099747
1180 changed files with 50973 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../app/di/network_provider.dart';
import '../../../app/di/db_provider.dart';
import '../../../data/repositories/auth_repository_impl.dart';
import '../../../domain/repositories/auth_repository.dart';
import '../usecases/login_usecase.dart';
/// ## DI 装配Auth Feature 层
///
/// di/ 目录只放**需要手动装配的 Provider**(构造注入、回调组合等)。
/// ViewModel Provider 由 `@riverpod` 注解自动生成,不在此文件中。
///
/// Auth 模块的 DI 链路Repository → UseCase按需
/// app/di/ 只提供 SDK 基础设施apiConfig / apiClient / socketManager / storageApi
/// 业务模块的 Provider 内聚在 features/{模块}/di/ 下。
///
/// ```
/// LoginViewModel ← @riverpod 自动生成
/// → ref.read(loginUseCaseProvider) ← di/ 手动装配
/// → ref.read(authRepositoryProvider) ← di/ 手动装配
/// → ref.read(socketManagerProvider) ← app/di/ 手动装配
/// → ref.read(apiConfigProvider) ← app/di/ 手动装配
/// → ref.read(apiClientProvider) ← app/di/ 手动装配
/// → ref.read(storageSdkProvider) ← app/di/ 手动装配
/// ```
// ── Repository ────────────────────────────────────────────────────────────────
/// 认证 Repository Provider
///
/// 注入 domain 接口类型 [AuthRepository]
/// ViewModel 通过此 Provider 获取依赖,不感知具体实现。
///
/// [onTokenUpdate] 是复合回调:
/// 1. 写入 ApiConfig 内存 → 后续请求自动携带 token
/// 2. TODO: 持久化到安全存储crypto_sdk→ App 重启后恢复
/// 两个 SDK 互不依赖,由 App 层在此组合。
final authRepositoryProvider = Provider<AuthRepository>((ref) {
final apiConfig = ref.read(apiConfigProvider);
// TODO: final secureStorage = ref.read(secureStorageProvider);
return AuthRepositoryImpl(
client: ref.read(networkSdkApiProvider), // 直接注入 ApiClient
onTokenUpdate: (token) {
apiConfig.updateToken(token); // 内存network_sdk
// TODO: secureStorage.saveToken(token); // 持久化crypto_sdk
},
);
});
// ── UseCase ───────────────────────────────────────────────────────────────────
/// 登录用例 Provider
///
/// 多步编排:格式校验 → 调接口 → 写 Token → 连接 WebSocket → 打开数据库
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
return LoginUseCase(
authRepository: ref.read(authRepositoryProvider),
socketManager: ref.read(socketManagerProvider),
apiConfig: ref.read(apiConfigProvider),
storageApi: ref.read(storageSdkProvider),
);
});

View File

@@ -0,0 +1,33 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../domain/entities/user.dart';
part 'login_state.freezed.dart';
/// 登录页面状态(@freezed 自动生成 copyWith / == / toString
///
/// ViewModel 通过 `state = state.copyWith(...)` 更新状态,
/// View 通过 `ref.watch(loginViewModelProvider)` 自动响应变化。
///
/// ## 状态流转
///
/// ```
/// 初始 → LoginState() isLoading: false, user: null, error: null
/// 点击登录 → state.copyWith(isLoading: true) isLoading: true
/// 登录成功 → state.copyWith(user: user) isLoading: false, user: User
/// 格式错误 → state.copyWith(error: '邮箱格式不正确') isLoading: false, error: String
/// 网络错误 → state.copyWith(error: '网络错误') isLoading: false, error: String
/// ```
@freezed
sealed class LoginState with _$LoginState {
const factory LoginState({
/// 登录成功后的用户信息null = 未登录)
User? user,
/// 是否正在请求中(控制 loading 状态 / 按钮禁用)
@Default(false) bool isLoading,
/// 错误信息null = 无错误)
String? error,
}) = _LoginState;
}

View File

@@ -0,0 +1,96 @@
import 'package:flutter/foundation.dart';
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/app/di/db_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:storage_sdk/storage_sdk.dart';
import '../../../app/di/app_providers.dart';
import '../di/auth_providers.dart';
import 'login_state.dart';
part 'login_view_model.g.dart';
/// 登录 ViewModel@riverpod 自动生成 `loginViewModelProvider`
///
/// `@riverpod` 注解 → build_runner 自动生成 `login_view_model.g.dart`
/// 其中包含 `loginViewModelProvider`。View 层直接使用:
///
/// ```dart
/// // View 层读取状态
/// final state = ref.watch(loginViewModelProvider);
///
/// // View 层调用方法
/// ref.read(loginViewModelProvider.notifier).login(email, password);
/// ```
///
/// ## 手动 vs 自动 Provider 对比
///
/// ```
/// loginViewModelProvider ← @riverpod 自动生成(本文件)
/// → ref.read(loginUseCaseProvider) ← di/ 手动装配
/// → ref.read(authRepositoryProvider) ← di/ 手动装配
/// → ref.read(apiClientProvider) ← app/di/ 手动装配
/// ```
///
/// ## 数据流位置
///
/// ```
/// View: ref.read(loginViewModelProvider.notifier).login(email, password)
/// → ★ LoginViewModel.login() ★ ← 你在这里
/// → LoginUseCase.execute() ← 格式校验 + 调 Repository
/// → AuthRepository.login()
/// → _client.executeRequest(LoginRequest)
/// ← LoginData → User
/// ← User
/// → state = state.copyWith(user: user) ← 更新状态
/// View: ref.watch → 自动 rebuild ← UI 刷新
/// ```
@riverpod
class LoginViewModel extends _$LoginViewModel {
@override
LoginState build() => const LoginState();
/// Demo 登录(跳过 API直接设置登录状态
///
/// 仅用于演示路由守卫行为,正式 UI 就绪后删除此方法,改用 [login]。
/// 正式 [login] 成功后同样需要调用 [AuthNotifier.login] 更新守卫状态。
Future<void> demoLogin() async {
final storageApi = ref.read(storageSdkProvider);
///TODO: StorageSDKLifeCycle 需要只在主项目暴露
final storageLifeCycle = storageApi as StorageSdkLifecycle;
ref.read(authNotifierProvider).login();
await storageLifeCycle.openDatabase(1234567);
final rows = await storageApi.rawQuery("PRAGMA table_info('user')");
for (final row in rows) {
debugPrint('Schema: ${row.data}');
}
}
/// 执行登录
///
/// 1. 设置 loading 状态UI 显示加载指示器、禁用按钮)
/// 2. 调 UseCase格式校验 → 登录 → 返回 User
/// 3. 成功:写入 user失败写入 error
Future<void> login(String email, String password) async {
state = state.copyWith(isLoading: true, error: null);
try {
final user = await ref.read(loginUseCaseProvider).execute(
email: email,
password: password,
);
state = state.copyWith(user: user, isLoading: false);
} on FormatException catch (e) {
// 格式校验失败UseCase 层抛出)
state = state.copyWith(error: e.message, isLoading: false);
} on ApiError catch (e) {
// 网络 / 服务端错误Repository → SDK 透传)
state = state.copyWith(error: e.displayMessage, isLoading: false);
} catch (e) {
// 兜底:防止未预期的异常导致 isLoading 死锁
state = state.copyWith(error: e.toString(), isLoading: false);
}
}
}

View File

@@ -0,0 +1,117 @@
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';
/// 登录用例
///
/// 封装登录的完整业务流程:
/// 格式校验 → 调 Repository 登录 → 初始化 WebSocket → 打开本地数据库 → 返回 User
///
/// ## 为什么需要 UseCase
///
/// ViewModel 直接调 Repository 也能跑通,但登录有明确的多步业务规则:
/// - 格式校验(不发无效请求,省流量、减少服务端压力)
/// - 登录后初始化 WebSocket 连接
/// - 登录后按 user id 打开对应的本地数据库
///
/// 把这些规则封装在 UseCase 里ViewModel 只需一行调用。
///
/// ## 数据流位置
///
/// ```
/// LoginViewModel.login(email, password)
/// → ★ LoginUseCase.execute() ★ ← 你在这里
/// → 格式校验(邮箱 + 密码)
/// → AuthRepository.login()
/// → AuthRepositoryImpl.login()
/// → _client.executeRequest(LoginRequest)
/// ← LoginDataDTO
/// → _onTokenUpdate(token) ← 回调写入 Token内存 + 持久化,由 Provider 层组合)
/// ← LoginData.toEntity() → User
/// → SocketManager.connect(token) ← 登录后连接 WebSocket
/// → StorageSdkApi.openDatabase(user.id) ← 按用户 id 打开本地库
/// ← 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]
/// 2. 调 Repository 登录 → 拿到 Usertoken 写入由 Repository 处理)
/// 3. 用已存入 ApiConfig 的 token 连接 WebSocket
/// 4. 按 user id 打开本地数据库
///
/// 抛出:
/// - [FormatException] — 邮箱或密码格式不合法
/// - [ApiError] — 网络/服务端错误(由 Repository 透传)
Future<User> execute({
required String email,
required String password,
}) async {
// ── 1. 格式校验 ──
_validateEmail(email);
_validatePassword(password);
// ── 2. 登录 ──
final user = await _authRepository.login(
email: email,
password: password,
);
// ── 3. 连接 WebSocket ──
// token 在 Repository 的 _onTokenUpdate 回调中已写入 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);
// TODO: 后续扩展点
// - 同步联系人列表
// - 注册推送 token
return user;
}
void _validateEmail(String email) {
if (email.trim().isEmpty) {
throw const FormatException('邮箱不能为空'); // TODO: 接入国际化
}
final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
if (!emailRegex.hasMatch(email.trim())) {
throw const FormatException('邮箱格式不正确'); // TODO: 接入国际化
}
}
void _validatePassword(String password) {
if (password.isEmpty) {
throw const FormatException('密码不能为空'); // TODO: 接入国际化
}
if (password.length < 6) {
throw const FormatException('密码长度不能少于 6 位'); // TODO: 接入国际化
}
}
}

View File

@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/ui/base/context_theme_ext.dart';
import '../presentation/login_view_model.dart';
/// 登录页Demo
///
/// 演示 go_router 登录守卫:点击「登录」后经由 [LoginViewModel.demoLogin]
/// 触发 [GoRouter.refreshListenable],守卫重新执行并重定向到 /chat。
///
/// 正式实现时替换为完整登录流程email/password 输入 → LoginViewModel.login
class LoginPage extends ConsumerWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final s = context.styles;
return Scaffold(
appBar: AppBar(
title: const Text('登录'),
automaticallyImplyLeading: false,
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('IM_Demo', style: s.titleMedium),
const SizedBox(height: 8),
Text(
'未登录时任意路由均被重定向到此页 \n 主要是为了展示路由守卫的功能 \n 后续路由守卫专门处理各种跳转前的逻辑判断',
style: s.bodySmall,
),
const SizedBox(height: 32),
FilledButton(
onPressed: () => ref.read(loginViewModelProvider.notifier).demoLogin(),
child: const Text('登录'),
),
],
),
),
);
}
}