Initial project
This commit is contained in:
0
apps/im_app/lib/data/cache/.gitkeep
vendored
Normal file
0
apps/im_app/lib/data/cache/.gitkeep
vendored
Normal file
40
apps/im_app/lib/data/local/drift/app_database.dart
Normal file
40
apps/im_app/lib/data/local/drift/app_database.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:im_app/data/local/drift/tables/users.dart';
|
||||
|
||||
part 'app_database.g.dart';
|
||||
|
||||
@DriftDatabase(tables: [Users])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
onUpgrade: (m, from, to) async {
|
||||
// 自动检测并添加缺失列
|
||||
for (final table in allTables) {
|
||||
//取原来的字段
|
||||
final existingColumns = await m.database
|
||||
.customSelect('PRAGMA table_info(${table.actualTableName})')
|
||||
.get();
|
||||
final existingNames = existingColumns
|
||||
.map((r) => r.data['name'] as String)
|
||||
.toSet();
|
||||
|
||||
for (final column in table.$columns) {
|
||||
if (!existingNames.contains(column.name)) {
|
||||
//字段缺失,添加。
|
||||
await m.addColumn(table, column);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
0
apps/im_app/lib/data/local/drift/daos/.gitkeep
Normal file
0
apps/im_app/lib/data/local/drift/daos/.gitkeep
Normal file
41
apps/im_app/lib/data/local/drift/tables/users.dart
Normal file
41
apps/im_app/lib/data/local/drift/tables/users.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('User')
|
||||
class Users extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get uid => integer().nullable()();
|
||||
TextColumn get uuid => text().nullable()();
|
||||
IntColumn get lastOnline => integer().nullable()();
|
||||
TextColumn get profilePic => text().nullable()();
|
||||
TextColumn get profilePicGaussian => text().withDefault(const Constant(''))();
|
||||
TextColumn get nickname => text().nullable()();
|
||||
TextColumn get depositName => text().nullable()();
|
||||
IntColumn get hasSetDepositName => integer().withDefault(const Constant(0))();
|
||||
TextColumn get contact => text().nullable()();
|
||||
TextColumn get countryCode => text().nullable()();
|
||||
TextColumn get username => text().nullable()();
|
||||
IntColumn get role => integer().nullable()();
|
||||
IntColumn get relationship => integer().nullable()();
|
||||
IntColumn get friendStatus => integer().nullable()();
|
||||
TextColumn get bio => text().nullable()();
|
||||
TextColumn get userAlias => text().nullable()();
|
||||
IntColumn get requestAt => integer().nullable()();
|
||||
IntColumn get deletedAt => integer().nullable()();
|
||||
TextColumn get email => text().nullable()();
|
||||
TextColumn get recoveryEmail => text().nullable()();
|
||||
TextColumn get remark => text().nullable()();
|
||||
TextColumn get source => text().nullable()();
|
||||
IntColumn get addIndex => integer().nullable()();
|
||||
IntColumn get incomingSoundId => integer().withDefault(const Constant(0))();
|
||||
IntColumn get outgoingSoundId => integer().withDefault(const Constant(0))();
|
||||
IntColumn get notificationSoundId => integer().withDefault(const Constant(0))();
|
||||
IntColumn get sendMessageSoundId => integer().withDefault(const Constant(0))();
|
||||
IntColumn get groupNotificationSoundId => integer().withDefault(const Constant(0))();
|
||||
TextColumn get groupTags => text().withDefault(const Constant('[]'))();
|
||||
TextColumn get friendTags => text().withDefault(const Constant('[]'))();
|
||||
TextColumn get publicKey => text().nullable()();
|
||||
IntColumn get configBits => integer().withDefault(const Constant(0))();
|
||||
TextColumn get hint => text().nullable()();
|
||||
@override
|
||||
String get tableName => 'user';
|
||||
}
|
||||
0
apps/im_app/lib/data/local/storage/.gitkeep
Normal file
0
apps/im_app/lib/data/local/storage/.gitkeep
Normal file
68
apps/im_app/lib/data/models/user_dto.dart
Normal file
68
apps/im_app/lib/data/models/user_dto.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../domain/entities/user.dart';
|
||||
|
||||
part 'user_dto.g.dart';
|
||||
|
||||
/// 用户 DTO(Data Transfer Object)
|
||||
///
|
||||
/// local / remote 共用的数据传输对象,放在 data/models/。
|
||||
/// 提供与 Domain Entity [User] 之间的双向转换。
|
||||
///
|
||||
/// ## 数据流位置(本地存储场景)
|
||||
///
|
||||
/// ```
|
||||
/// 写入本地:
|
||||
/// LoginData.toEntity() → User
|
||||
/// → UserDto.fromEntity(user) → ★ UserDto ★ ← 你在这里
|
||||
/// → toJson() → SQLite / SharedPreferences
|
||||
///
|
||||
/// 读取本地:
|
||||
/// SQLite / SharedPreferences → JSON
|
||||
/// → ★ UserDto.fromJson() ★ ← 你在这里
|
||||
/// → UserDto.toEntity() → User
|
||||
/// → ViewModel.state → View
|
||||
/// ```
|
||||
///
|
||||
/// 注意:登录接口的 Response DTO 是 [LoginData](含 token),
|
||||
/// 本类用于纯用户信息的本地持久化,不含 token。
|
||||
@JsonSerializable()
|
||||
class UserDto {
|
||||
@JsonKey(name: 'user_id')
|
||||
final String userId;
|
||||
final String email;
|
||||
final String? nickname;
|
||||
final String? avatar;
|
||||
|
||||
const UserDto({
|
||||
required this.userId,
|
||||
required this.email,
|
||||
this.nickname,
|
||||
this.avatar,
|
||||
});
|
||||
|
||||
factory UserDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserDtoFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$UserDtoToJson(this);
|
||||
|
||||
/// DTO → Domain Entity
|
||||
User toEntity() {
|
||||
return User(
|
||||
id: userId,
|
||||
email: email,
|
||||
nickname: nickname,
|
||||
avatar: avatar,
|
||||
);
|
||||
}
|
||||
|
||||
/// Domain Entity → DTO
|
||||
factory UserDto.fromEntity(User user) {
|
||||
return UserDto(
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
);
|
||||
}
|
||||
}
|
||||
82
apps/im_app/lib/data/remote/get_profile_request.dart
Normal file
82
apps/im_app/lib/data/remote/get_profile_request.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
|
||||
import '../../../core/foundation/api_paths.dart';
|
||||
import '../../../domain/entities/user.dart';
|
||||
|
||||
part 'get_profile_request.g.dart';
|
||||
|
||||
/// # /user/profile — 获取用户资料(GET 请求示例)
|
||||
///
|
||||
/// 演示:GET 请求 + 无 body 参数的模式。
|
||||
/// GET 请求的 toJson() 结果会自动作为 URL query parameters 发送。
|
||||
///
|
||||
/// ## 数据流位置
|
||||
///
|
||||
/// ```
|
||||
/// UserRepositoryImpl.getProfile()
|
||||
/// → _client.executeRequest( ★ GetProfileRequest ★ ) ← 你在这里
|
||||
/// → 服务端 GET /user/profile
|
||||
/// → 响应 JSON → ★ ProfileData ★ ← 也在这里
|
||||
/// → ProfileData.toEntity() → User
|
||||
/// ```
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Response DTO
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// 用户资料响应 DTO(只需反序列化,禁止生成无用的 toJson)
|
||||
@JsonSerializable(createToJson: false)
|
||||
class ProfileData {
|
||||
@JsonKey(name: 'user_id')
|
||||
final String userId;
|
||||
final String email;
|
||||
final String? nickname;
|
||||
final String? avatar;
|
||||
|
||||
const ProfileData({
|
||||
required this.userId,
|
||||
required this.email,
|
||||
this.nickname,
|
||||
this.avatar,
|
||||
});
|
||||
|
||||
factory ProfileData.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProfileDataFromJson(json);
|
||||
|
||||
/// DTO → Domain Entity
|
||||
User toEntity() {
|
||||
return User(
|
||||
id: userId,
|
||||
email: email,
|
||||
nickname: nickname,
|
||||
avatar: avatar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Request
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// 获取用户资料请求(GET,无参数)
|
||||
///
|
||||
/// GET 请求无 body,toJson() 返回空 map。
|
||||
/// 如需 query 参数(如分页),添加字段即可,
|
||||
/// toJson() 会自动将字段序列化为 URL query string。
|
||||
@ApiRequest(
|
||||
path: ApiPaths.userProfile,
|
||||
method: HttpMethod.get,
|
||||
responseType: ProfileData,
|
||||
)
|
||||
@JsonSerializable()
|
||||
class GetProfileRequest extends ApiRequestable<ProfileData>
|
||||
with _$GetProfileRequestApi {
|
||||
GetProfileRequest();
|
||||
|
||||
factory GetProfileRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$GetProfileRequestFromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
|
||||
}
|
||||
90
apps/im_app/lib/data/remote/login_request.dart
Normal file
90
apps/im_app/lib/data/remote/login_request.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
|
||||
import '../../../core/foundation/api_paths.dart';
|
||||
import '../../../domain/entities/user.dart';
|
||||
|
||||
part 'login_request.g.dart';
|
||||
|
||||
/// # /auth/login — 登录接口
|
||||
///
|
||||
/// 一个端点 = 一个文件,Response DTO + Request 放在同一文件中。
|
||||
///
|
||||
/// ## 数据流位置
|
||||
///
|
||||
/// ```
|
||||
/// AuthRepositoryImpl.login(email, password)
|
||||
/// → _client.executeRequest( ★ LoginRequest ★ ) ← 你在这里
|
||||
/// → 服务端 POST /auth/login
|
||||
/// → 响应 JSON → ★ LoginData ★ ← 也在这里
|
||||
/// → LoginData.toEntity() → User
|
||||
/// ```
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Response DTO
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// 登录响应 DTO
|
||||
///
|
||||
/// 服务端返回的登录数据,包含 token 和用户信息。
|
||||
/// 通过 [toEntity] 转换为 Domain Entity [User]。
|
||||
@JsonSerializable()
|
||||
class LoginData {
|
||||
final String token;
|
||||
@JsonKey(name: 'user_id')
|
||||
final String userId;
|
||||
final String email;
|
||||
final String? nickname;
|
||||
final String? avatar;
|
||||
|
||||
const LoginData({
|
||||
required this.token,
|
||||
required this.userId,
|
||||
required this.email,
|
||||
this.nickname,
|
||||
this.avatar,
|
||||
});
|
||||
|
||||
factory LoginData.fromJson(Map<String, dynamic> json) =>
|
||||
_$LoginDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
|
||||
|
||||
/// DTO → Domain Entity
|
||||
User toEntity() {
|
||||
return User(
|
||||
id: userId,
|
||||
email: email,
|
||||
nickname: nickname,
|
||||
avatar: avatar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Request
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// 登录请求
|
||||
///
|
||||
/// `@ApiRequest` 自动生成 `_$LoginRequestApi` mixin,
|
||||
/// 提供 path / method / requestType / includeToken / fromJson 自动注册。
|
||||
@ApiRequest(
|
||||
path: ApiPaths.authLogin,
|
||||
method: HttpMethod.post,
|
||||
responseType: LoginData,
|
||||
requestType: ApiRequestType.login,
|
||||
)
|
||||
@JsonSerializable()
|
||||
class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
|
||||
final String email;
|
||||
final String password;
|
||||
|
||||
LoginRequest({required this.email, required this.password});
|
||||
|
||||
factory LoginRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$LoginRequestFromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
||||
}
|
||||
32
apps/im_app/lib/data/remote/logout_request.dart
Normal file
32
apps/im_app/lib/data/remote/logout_request.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
|
||||
import '../../../core/foundation/api_paths.dart';
|
||||
|
||||
/// # /auth/logout — 登出接口(无响应数据示例)
|
||||
///
|
||||
/// 演示:POST 请求 + 无 Response DTO 的模式。
|
||||
/// 服务端返回 `{"code": 0, "message": "ok"}` 无 data 字段,
|
||||
/// `executeRequest` 返回 null,调用方直接 await 即可。
|
||||
///
|
||||
/// 此接口不使用 @ApiRequest 注解,直接实现 ApiRequestable,
|
||||
/// 演示手动实现方式(适用于不需要代码生成器的简单接口)。
|
||||
///
|
||||
/// ## 数据流位置
|
||||
///
|
||||
/// ```
|
||||
/// AuthRepositoryImpl.logout()
|
||||
/// → _client.executeRequest( ★ LogoutRequest ★ ) ← 你在这里
|
||||
/// → 服务端 POST /auth/logout
|
||||
/// → 响应 {"code": 0, "message": "ok"} → null
|
||||
/// ```
|
||||
class LogoutRequest extends ApiRequestable<void> {
|
||||
@override
|
||||
String get path => ApiPaths.authLogout;
|
||||
|
||||
@override
|
||||
HttpMethod get method => HttpMethod.post;
|
||||
|
||||
/// 登出不需要请求体参数
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {};
|
||||
}
|
||||
160
apps/im_app/lib/data/remote/upload_file_request.dart
Normal file
160
apps/im_app/lib/data/remote/upload_file_request.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
|
||||
import '../../../core/foundation/api_paths.dart';
|
||||
|
||||
part 'upload_file_request.g.dart';
|
||||
|
||||
/// # /upload/file — 文件上传(Upload 请求示例)
|
||||
///
|
||||
/// 演示两种上传模式:
|
||||
///
|
||||
/// ## 模式 A: FormData 上传到自有后端
|
||||
/// 适用于后端直接接收文件的场景。
|
||||
/// 使用 [UploadFileRequest] — path 为相对路径,SDK 自动拼 baseURL。
|
||||
///
|
||||
/// ## 模式 B: 二进制上传到 S3 presigned URL
|
||||
/// 适用于先向后端获取 presigned URL,再直接上传到 S3 的场景。
|
||||
/// 使用 [S3UploadRequest] — path 为完整 URL,override decodeResponse。
|
||||
///
|
||||
/// ## Upload 与普通请求的区别
|
||||
///
|
||||
/// | 普通请求 | Upload 请求 |
|
||||
/// |---------|-----------|
|
||||
/// | `toJson()` → JSON body | `uploadData` → FormData / Uint8List |
|
||||
/// | `requestType: request` | `requestType: upload` |
|
||||
/// | `parameters` 有值 | `parameters` 返回 null |
|
||||
/// | 标准 `{ code, msg, data }` 响应 | 可能需要 override `decodeResponse` |
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Response DTO
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// 文件上传响应 DTO(只需反序列化,禁止生成无用的 toJson)
|
||||
@JsonSerializable(createToJson: false)
|
||||
class UploadResult {
|
||||
final String url;
|
||||
|
||||
@JsonKey(name: 'file_id')
|
||||
final String fileId;
|
||||
|
||||
const UploadResult({required this.url, required this.fileId});
|
||||
|
||||
factory UploadResult.fromJson(Map<String, dynamic> json) =>
|
||||
_$UploadResultFromJson(json);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════
|
||||
// 模式 A: FormData 上传到自有后端
|
||||
// ═════════════════════════════════════════════
|
||||
|
||||
/// FormData 上传请求
|
||||
///
|
||||
/// 上传到自有后端 `/upload/file`,响应为标准 `{ code, message, data }` 信封。
|
||||
/// 无需 override `decodeResponse`。
|
||||
@ApiRequest(
|
||||
path: ApiPaths.uploadFile,
|
||||
method: HttpMethod.post,
|
||||
responseType: UploadResult,
|
||||
requestType: ApiRequestType.upload,
|
||||
)
|
||||
class UploadFileRequest extends ApiRequestable<UploadResult>
|
||||
with _$UploadFileRequestApi {
|
||||
final String filePath;
|
||||
final String? fileName;
|
||||
|
||||
UploadFileRequest({required this.filePath, this.fileName});
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {};
|
||||
|
||||
/// FormData — SDK 通过 uploadData 获取上传数据
|
||||
@override
|
||||
Object? get uploadData {
|
||||
return FormData.fromMap({
|
||||
'file': MultipartFile.fromFileSync(filePath, filename: fileName),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════
|
||||
// 模式 B: 二进制上传到 S3 presigned URL
|
||||
// ═════════════════════════════════════════════
|
||||
|
||||
/// S3 presigned URL 上传响应
|
||||
class S3UploadResponse {
|
||||
final bool success;
|
||||
final String? message;
|
||||
|
||||
const S3UploadResponse({this.success = true, this.message});
|
||||
}
|
||||
|
||||
/// S3 presigned URL 上传请求
|
||||
///
|
||||
/// 特点:
|
||||
/// - path 为完整的 presigned URL(SDK 检测到 http 开头不拼 baseURL)
|
||||
/// - uploadData 为 Uint8List 二进制数据
|
||||
/// - 自定义 headers(Content-Type: application/octet-stream)
|
||||
/// - override decodeResponse — S3 返回 204 No Content 或 XML,不是标准信封
|
||||
class S3UploadRequest extends ApiRequestable<S3UploadResponse> {
|
||||
final Uint8List data;
|
||||
final String presignedURL;
|
||||
|
||||
S3UploadRequest({required this.data, required this.presignedURL});
|
||||
|
||||
@override
|
||||
String get path => presignedURL;
|
||||
|
||||
@override
|
||||
HttpMethod get method => HttpMethod.put;
|
||||
|
||||
@override
|
||||
ApiRequestType get requestType => ApiRequestType.upload;
|
||||
|
||||
@override
|
||||
Map<String, String>? get customHeaders => {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
};
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {};
|
||||
|
||||
/// 二进制数据
|
||||
@override
|
||||
Object? get uploadData => data;
|
||||
|
||||
/// S3 响应不走标准 { code, message, data } 信封,需要自定义解码
|
||||
///
|
||||
/// 可能的响应:
|
||||
/// - 204 No Content(空 body)→ 成功
|
||||
/// - 200 + XML body → 成功
|
||||
/// - 200 + JSON body → 尝试解码
|
||||
@override
|
||||
S3UploadResponse? decodeResponse(Response response) {
|
||||
// 空响应或 2xx 状态码 → 成功
|
||||
if (response.data == null ||
|
||||
(response.data is List && (response.data as List).isEmpty)) {
|
||||
return const S3UploadResponse(success: true);
|
||||
}
|
||||
|
||||
// JSON 响应 → 尝试解码
|
||||
if (response.data is Map<String, dynamic>) {
|
||||
final json = response.data as Map<String, dynamic>;
|
||||
return S3UploadResponse(
|
||||
success: true,
|
||||
message: json['message'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
// 2xx 状态码 → 成功
|
||||
if (response.statusCode != null &&
|
||||
response.statusCode! >= 200 &&
|
||||
response.statusCode! < 300) {
|
||||
return const S3UploadResponse(success: true);
|
||||
}
|
||||
|
||||
return const S3UploadResponse(success: true);
|
||||
}
|
||||
}
|
||||
58
apps/im_app/lib/data/repositories/auth_repository_impl.dart
Normal file
58
apps/im_app/lib/data/repositories/auth_repository_impl.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
|
||||
import '../../domain/entities/user.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
import '../remote/login_request.dart';
|
||||
import '../remote/logout_request.dart';
|
||||
|
||||
/// 认证 Repository 实现
|
||||
///
|
||||
/// implements [AuthRepository] 接口(domain/repositories/ 中定义)。
|
||||
/// 直接使用 [ApiClient] 发送请求,将 DTO 转为 Domain Entity。
|
||||
/// 后续可加 Local DataSource 实现离线缓存。
|
||||
///
|
||||
/// ## 数据流位置
|
||||
///
|
||||
/// ```
|
||||
/// LoginUseCase.execute(email, password)
|
||||
/// → ★ AuthRepositoryImpl.login() ★ ← 你在这里
|
||||
/// → ApiClient.executeRequest(LoginRequest)
|
||||
/// → 服务端 POST /auth/login
|
||||
/// ← LoginData(Response DTO)
|
||||
/// → onTokenUpdate(token) ← 回调写入 Token
|
||||
/// ← LoginData.toEntity() → User ← DTO → Entity 转换在这里
|
||||
/// ← User(Domain Entity)
|
||||
/// ```
|
||||
class AuthRepositoryImpl implements AuthRepository {
|
||||
final NetworksSdkApi _client;
|
||||
final void Function(String?) _onTokenUpdate;
|
||||
|
||||
AuthRepositoryImpl({required NetworksSdkApi client, required void Function(String?) onTokenUpdate,}) : _client = client, _onTokenUpdate = onTokenUpdate;
|
||||
|
||||
@override
|
||||
Future<User> login({required String email, required String password,}) async
|
||||
{
|
||||
final LoginData? loginData = await _client.executeRequest(LoginRequest(email: email, password: password),);
|
||||
|
||||
if (loginData == null) {
|
||||
throw Exception('Login failed: empty response'); // TODO: 接入国际化
|
||||
}
|
||||
|
||||
// 回调写入 Token(内存 + 持久化由 Provider 层组合)
|
||||
_onTokenUpdate(loginData.token);
|
||||
|
||||
return loginData.toEntity(); // DTO → Domain Entity
|
||||
}
|
||||
|
||||
@override
|
||||
Future<User?> getCurrentUser() async {
|
||||
// TODO: 从本地存储获取用户信息
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logout() async {
|
||||
await _client.executeRequest(LogoutRequest());
|
||||
_onTokenUpdate(null); // 回调清除 Token(内存 + 持久化由 Provider 层组合)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user