1)修改数据库命名,不和业务使用重合。
2)修改user 表,uid为unique, 聊天室文件夹name unique移除
This commit is contained in:
31
apps/im_app/assets/loginData.json
Normal file
31
apps/im_app/assets/loginData.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"code": 0,
|
||||
"message": "OK",
|
||||
"data": {
|
||||
"account_id": "1713925030yFMUBu",
|
||||
"profile": {
|
||||
"uid": 2137067,
|
||||
"uuid": "1713925030yFMUBu",
|
||||
"last_online": 1772819822,
|
||||
"profile_pic": "Image/7e/f5/7ef5b60dd83a34a74c164a21fbd1f098/7ef5b60dd83a34a74c164a21fbd1f098.jpg",
|
||||
"profile_pic_gaussian": "",
|
||||
"nickname": "Happi(哈比)",
|
||||
"contact": "86552205",
|
||||
"country_code": "+65",
|
||||
"email": "happi@winwayinfo.com",
|
||||
"recovery_email": "",
|
||||
"username": "happi",
|
||||
"bio": "",
|
||||
"relationship": 2,
|
||||
"user_alias": null,
|
||||
"channel_id": 1,
|
||||
"channel_group_id": 1,
|
||||
"hint": "1111"
|
||||
},
|
||||
"nonce": "",
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjIxMzcwNjcsInVkaWQiOjI2NjI2MzcsInNpZCI6IjE3NzI4NjU0NDc0Z1c2VWc9PSIsImlzcyI6ImFiYy5jb20iLCJhdWQiOlsidXNlciJdLCJleHAiOjE3NzQxNjE0NDcsIm5iZiI6MTc3Mjg2NTQ0NywiaWF0IjoxNzcyODY1NDQ3fQ.gUL6hyKgyPP8Tw9y7kRSq-ndNKfV9uGFhU4YKiQzg0I",
|
||||
"refresh_token": "ps0FF3XayvnJB_P8Cnfu7w-uD781b1-vfmUjbrONZxI=",
|
||||
"device_id": "SP1A.210812",
|
||||
"login_data": ""
|
||||
}
|
||||
}
|
||||
@@ -29,25 +29,25 @@ class AppDatabase extends _$AppDatabase {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
Favourite: database.favourites,
|
||||
Sound: database.sounds,
|
||||
Tag: database.tags,
|
||||
PendingFriendRequestHistory: database.pendingFriendRequestHistories,
|
||||
Message: database.messages,
|
||||
RecentMiniApp: database.recentMiniApps,
|
||||
Retry: database.retries,
|
||||
Group: database.groups,
|
||||
FavoriteMiniApp: database.favoriteMiniApps,
|
||||
DiscoverMiniApp: database.discoverMiniApps,
|
||||
ChatCategory: database.chatCategories,
|
||||
ChatBot: database.chatBots,
|
||||
FavouriteDetail: database.favouriteDetails,
|
||||
UserRequestHistory: database.userRequestHistories,
|
||||
Workspace: database.workspaces,
|
||||
User: database.users,
|
||||
ExploreMiniApp: database.exploreMiniApps,
|
||||
CallLog: database.callLogs,
|
||||
Chat: database.chats,
|
||||
DriftFavourite: database.favourites,
|
||||
DriftSound: database.sounds,
|
||||
DriftTag: database.tags,
|
||||
DriftPendingFriendRequestHistory: database.pendingFriendRequestHistories,
|
||||
DriftMessage: database.messages,
|
||||
DriftRecentMiniApp: database.recentMiniApps,
|
||||
DriftRetry: database.retries,
|
||||
DriftGroup: database.groups,
|
||||
DriftFavoriteMiniApp: database.favoriteMiniApps,
|
||||
DriftDiscoverMiniApp: database.discoverMiniApps,
|
||||
DriftChatCategory: database.chatCategories,
|
||||
DriftChatBot: database.chatBots,
|
||||
DriftFavouriteDetail: database.favouriteDetails,
|
||||
DriftUserRequestHistory: database.userRequestHistories,
|
||||
DriftWorkspace: database.workspaces,
|
||||
DriftUser: database.users,
|
||||
DriftExploreMiniApp: database.exploreMiniApps,
|
||||
DriftCallLog: database.callLogs,
|
||||
DriftChat: database.chats,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('CallLog')
|
||||
@DataClassName('DriftCallLog')
|
||||
class CallLogs extends Table {
|
||||
TextColumn get id => text()();
|
||||
IntColumn get callerId => integer().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('ChatBot')
|
||||
@DataClassName('DriftChatBot')
|
||||
class ChatBots extends Table {
|
||||
IntColumn get id => integer()();
|
||||
TextColumn get name => text().nullable()();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('ChatCategory')
|
||||
@DataClassName('DriftChatCategory')
|
||||
class ChatCategories extends Table {
|
||||
IntColumn get id => integer()();
|
||||
TextColumn get name => text().nullable().unique()();
|
||||
TextColumn get name => text().nullable()();
|
||||
TextColumn get includedChatIds => text().nullable()();
|
||||
TextColumn get excludedChatIds => text().nullable()();
|
||||
IntColumn get seq => integer().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('Chat')
|
||||
@DataClassName('DriftChat')
|
||||
class Chats extends Table {
|
||||
IntColumn get id => integer()();
|
||||
IntColumn get typ => integer().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('DiscoverMiniApp')
|
||||
@DataClassName('DriftDiscoverMiniApp')
|
||||
class DiscoverMiniApps extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('ExploreMiniApp')
|
||||
@DataClassName('DriftExploreMiniApp')
|
||||
class ExploreMiniApps extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('FavoriteMiniApp')
|
||||
@DataClassName('DriftFavoriteMiniApp')
|
||||
class FavoriteMiniApps extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('FavouriteDetail')
|
||||
@DataClassName('DriftFavouriteDetail')
|
||||
class FavouriteDetails extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get relatedId => text().withDefault(const Constant(''))();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('Favourite')
|
||||
@DataClassName('DriftFavourite')
|
||||
class Favourites extends Table {
|
||||
IntColumn get id => integer()();
|
||||
TextColumn get parentId => text().withDefault(const Constant(''))();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('Group')
|
||||
@DataClassName('DriftGroup')
|
||||
class Groups extends Table {
|
||||
IntColumn get id => integer()();
|
||||
IntColumn get userJoinDate => integer().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('Message')
|
||||
@DataClassName('DriftMessage')
|
||||
class Messages extends Table {
|
||||
IntColumn get id => integer()();
|
||||
IntColumn get messageId => integer().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('PendingFriendRequestHistory')
|
||||
@DataClassName('DriftPendingFriendRequestHistory')
|
||||
class PendingFriendRequestHistories extends Table {
|
||||
IntColumn get id => integer()();
|
||||
IntColumn get uid => integer()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('RecentMiniApp')
|
||||
@DataClassName('DriftRecentMiniApp')
|
||||
class RecentMiniApps extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('Retry')
|
||||
@DataClassName('DriftRetry')
|
||||
class Retries extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get uid => integer().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('Sound')
|
||||
@DataClassName('DriftSound')
|
||||
class Sounds extends Table {
|
||||
IntColumn get id => integer()();
|
||||
TextColumn get filePath => text().withDefault(const Constant(''))();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('Tag')
|
||||
@DataClassName('DriftTag')
|
||||
class Tags extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get uid => integer().nullable()();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('UserRequestHistory')
|
||||
@DataClassName('DriftUserRequestHistory')
|
||||
class UserRequestHistories extends Table {
|
||||
IntColumn get id => integer()();
|
||||
IntColumn get status => integer().nullable()();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('User')
|
||||
@DataClassName('DriftUser')
|
||||
class Users extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get uid => integer().nullable()();
|
||||
IntColumn get uid => integer().unique()();
|
||||
TextColumn get uuid => text().nullable()();
|
||||
IntColumn get lastOnline => integer().nullable()();
|
||||
TextColumn get profilePic => text().nullable()();
|
||||
@@ -36,6 +36,7 @@ class Users extends Table {
|
||||
TextColumn get publicKey => text().nullable()();
|
||||
IntColumn get configBits => integer().withDefault(const Constant(0))();
|
||||
TextColumn get hint => text().nullable()();
|
||||
|
||||
@override
|
||||
String get tableName => 'user';
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('Workspace')
|
||||
@DataClassName('DriftWorkspace')
|
||||
class Workspaces extends Table {
|
||||
IntColumn get id => integer()();
|
||||
TextColumn get name => text().nullable()();
|
||||
|
||||
@@ -1,68 +1,148 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../domain/entities/user.dart';
|
||||
|
||||
part 'user_dto.g.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:im_app/data/local/drift/app_database.dart';
|
||||
import 'package:im_app/domain/entities/user.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 int uid;
|
||||
final String? uuid;
|
||||
final int? lastOnline;
|
||||
final String? profilePic;
|
||||
final String? profilePicGaussian;
|
||||
final String? nickname;
|
||||
final String? avatar;
|
||||
final String? contact;
|
||||
final String? countryCode;
|
||||
final String? email;
|
||||
final String? recoveryEmail;
|
||||
final String? username;
|
||||
final String? bio;
|
||||
final int? relationship;
|
||||
final String? userAlias;
|
||||
final int? channelId;
|
||||
final int? channelGroupId;
|
||||
final String? hint;
|
||||
|
||||
const UserDto({
|
||||
required this.userId,
|
||||
required this.email,
|
||||
required this.uid,
|
||||
this.uuid,
|
||||
this.lastOnline,
|
||||
this.profilePic,
|
||||
this.profilePicGaussian,
|
||||
this.nickname,
|
||||
this.avatar,
|
||||
this.contact,
|
||||
this.countryCode,
|
||||
this.email,
|
||||
this.recoveryEmail,
|
||||
this.username,
|
||||
this.bio,
|
||||
this.relationship,
|
||||
this.userAlias,
|
||||
this.channelId,
|
||||
this.channelGroupId,
|
||||
this.hint,
|
||||
});
|
||||
|
||||
factory UserDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserDtoFromJson(json);
|
||||
factory UserDto.fromJson(Map<String, dynamic> json) => UserDto(
|
||||
uid: json['uid'] as int,
|
||||
uuid: json['uuid'],
|
||||
lastOnline: json['last_online'],
|
||||
profilePic: json['profile_pic'],
|
||||
profilePicGaussian: json['profile_pic_gaussian'] ?? '',
|
||||
nickname: json['nickname'],
|
||||
contact: json['contact'],
|
||||
countryCode: json['country_code'],
|
||||
email: json['email'],
|
||||
recoveryEmail: json['recovery_email'] ?? '',
|
||||
username: json['username'],
|
||||
bio: json['bio'] ?? '',
|
||||
relationship: json['relationship'],
|
||||
userAlias: json['user_alias'],
|
||||
channelId: json['channel_id'],
|
||||
channelGroupId: json['channel_group_id'],
|
||||
hint: json['hint'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => _$UserDtoToJson(this);
|
||||
Map<String, dynamic> toJson() => {
|
||||
'uid': uid,
|
||||
'uuid': uuid,
|
||||
'last_online': lastOnline,
|
||||
'profile_pic': profilePic,
|
||||
'profile_pic_gaussian': profilePicGaussian,
|
||||
'nickname': nickname,
|
||||
'contact': contact,
|
||||
'country_code': countryCode,
|
||||
'email': email,
|
||||
'recovery_email': recoveryEmail,
|
||||
'username': username,
|
||||
'bio': bio,
|
||||
'relationship': relationship,
|
||||
'user_alias': userAlias,
|
||||
'channel_id': channelId,
|
||||
'channel_group_id': channelGroupId,
|
||||
'hint': hint,
|
||||
};
|
||||
|
||||
/// DTO → Domain Entity
|
||||
User toEntity() {
|
||||
return User(
|
||||
id: userId,
|
||||
email: email,
|
||||
nickname: nickname,
|
||||
avatar: avatar,
|
||||
);
|
||||
}
|
||||
User toEntity() => User(
|
||||
uid: uid,
|
||||
uuid: uuid,
|
||||
lastOnline: lastOnline,
|
||||
profilePic: profilePic,
|
||||
profilePicGaussian: profilePicGaussian,
|
||||
nickname: nickname,
|
||||
contact: contact,
|
||||
countryCode: countryCode,
|
||||
email: email,
|
||||
recoveryEmail: recoveryEmail,
|
||||
username: username,
|
||||
bio: bio,
|
||||
relationship: relationship,
|
||||
userAlias: userAlias,
|
||||
channelId: channelId,
|
||||
channelGroupId: channelGroupId,
|
||||
hint: hint,
|
||||
);
|
||||
|
||||
/// Domain Entity → DTO
|
||||
factory UserDto.fromEntity(User user) {
|
||||
return UserDto(
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
);
|
||||
}
|
||||
factory UserDto.fromEntity(User user) => UserDto(
|
||||
uid: user.uid,
|
||||
uuid: user.uuid,
|
||||
lastOnline: user.lastOnline,
|
||||
profilePic: user.profilePic,
|
||||
profilePicGaussian: user.profilePicGaussian,
|
||||
nickname: user.nickname,
|
||||
contact: user.contact,
|
||||
countryCode: user.countryCode,
|
||||
email: user.email,
|
||||
recoveryEmail: user.recoveryEmail,
|
||||
username: user.username,
|
||||
bio: user.bio,
|
||||
relationship: user.relationship,
|
||||
userAlias: user.userAlias,
|
||||
channelId: user.channelId,
|
||||
channelGroupId: user.channelGroupId,
|
||||
hint: user.hint,
|
||||
);
|
||||
|
||||
/// DTO → Drift Companion (for DB insert/update)
|
||||
UsersCompanion toCompanion() => UsersCompanion(
|
||||
uid: Value(uid),
|
||||
uuid: Value(uuid),
|
||||
lastOnline: Value(lastOnline),
|
||||
profilePic: Value(profilePic),
|
||||
profilePicGaussian: Value(profilePicGaussian ?? ''),
|
||||
nickname: Value(nickname),
|
||||
contact: Value(contact),
|
||||
countryCode: Value(countryCode),
|
||||
email: Value(email),
|
||||
recoveryEmail: Value(recoveryEmail),
|
||||
username: Value(username),
|
||||
bio: Value(bio),
|
||||
relationship: Value(relationship),
|
||||
userAlias: Value(userAlias),
|
||||
hint: Value(hint),
|
||||
);
|
||||
}
|
||||
@@ -28,31 +28,75 @@ part 'get_profile_request.g.dart';
|
||||
/// 用户资料响应 DTO(只需反序列化,禁止生成无用的 toJson)
|
||||
@JsonSerializable(createToJson: false)
|
||||
class ProfileData {
|
||||
@JsonKey(name: 'user_id')
|
||||
final String userId;
|
||||
final int uid;
|
||||
final String uuid;
|
||||
@JsonKey(name: 'last_online')
|
||||
final int lastOnline;
|
||||
@JsonKey(name: 'profile_pic')
|
||||
final String profilePic;
|
||||
@JsonKey(name: 'profile_pic_gaussian')
|
||||
final String profilePicGaussian;
|
||||
final String nickname;
|
||||
final String contact;
|
||||
@JsonKey(name: 'country_code')
|
||||
final String countryCode;
|
||||
final String email;
|
||||
final String? nickname;
|
||||
final String? avatar;
|
||||
@JsonKey(name: 'recovery_email')
|
||||
final String recoveryEmail;
|
||||
final String username;
|
||||
final String bio;
|
||||
final int relationship;
|
||||
@JsonKey(name: 'user_alias')
|
||||
final String? userAlias;
|
||||
@JsonKey(name: 'channel_id')
|
||||
final int channelId;
|
||||
@JsonKey(name: 'channel_group_id')
|
||||
final int channelGroupId;
|
||||
final String hint;
|
||||
|
||||
const ProfileData({
|
||||
required this.userId,
|
||||
required this.uid,
|
||||
required this.uuid,
|
||||
required this.lastOnline,
|
||||
required this.profilePic,
|
||||
required this.profilePicGaussian,
|
||||
required this.nickname,
|
||||
required this.contact,
|
||||
required this.countryCode,
|
||||
required this.email,
|
||||
this.nickname,
|
||||
this.avatar,
|
||||
required this.recoveryEmail,
|
||||
required this.username,
|
||||
required this.bio,
|
||||
required this.relationship,
|
||||
this.userAlias,
|
||||
required this.channelId,
|
||||
required this.channelGroupId,
|
||||
required this.hint,
|
||||
});
|
||||
|
||||
factory ProfileData.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProfileDataFromJson(json);
|
||||
|
||||
/// DTO → Domain Entity
|
||||
User toEntity() {
|
||||
return User(
|
||||
id: userId,
|
||||
email: email,
|
||||
nickname: nickname,
|
||||
avatar: avatar,
|
||||
);
|
||||
}
|
||||
User toEntity() => User(
|
||||
uid: uid,
|
||||
uuid: uuid,
|
||||
lastOnline: lastOnline,
|
||||
profilePic: profilePic,
|
||||
profilePicGaussian: profilePicGaussian,
|
||||
nickname: nickname,
|
||||
contact: contact,
|
||||
countryCode: countryCode,
|
||||
email: email,
|
||||
recoveryEmail: recoveryEmail,
|
||||
username: username,
|
||||
bio: bio,
|
||||
relationship: relationship,
|
||||
userAlias: userAlias,
|
||||
channelId: channelId,
|
||||
channelGroupId: channelGroupId,
|
||||
hint: hint,
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
@@ -8,75 +8,155 @@ 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
|
||||
/// → 响应 JSON → ★ LoginResponse ★ ← 也在这里
|
||||
/// → LoginResponse.toEntity() → User
|
||||
/// ```
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Response DTO
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// 登录响应 DTO
|
||||
///
|
||||
/// 服务端返回的登录数据,包含 token 和用户信息。
|
||||
/// 通过 [toEntity] 转换为 Domain Entity [User]。
|
||||
@JsonSerializable()
|
||||
class LoginData {
|
||||
final String token;
|
||||
@JsonKey(name: 'user_id')
|
||||
final String userId;
|
||||
@JsonSerializable(createToJson: false)
|
||||
class LoginProfile {
|
||||
final int uid;
|
||||
final String uuid;
|
||||
@JsonKey(name: 'last_online')
|
||||
final int lastOnline;
|
||||
@JsonKey(name: 'profile_pic')
|
||||
final String profilePic;
|
||||
@JsonKey(name: 'profile_pic_gaussian')
|
||||
final String profilePicGaussian;
|
||||
final String nickname;
|
||||
final String contact;
|
||||
@JsonKey(name: 'country_code')
|
||||
final String countryCode;
|
||||
final String email;
|
||||
final String? nickname;
|
||||
final String? avatar;
|
||||
@JsonKey(name: 'recovery_email')
|
||||
final String recoveryEmail;
|
||||
final String username;
|
||||
final String bio;
|
||||
final int relationship;
|
||||
@JsonKey(name: 'user_alias')
|
||||
final String? userAlias;
|
||||
@JsonKey(name: 'channel_id')
|
||||
final int channelId;
|
||||
@JsonKey(name: 'channel_group_id')
|
||||
final int channelGroupId;
|
||||
final String hint;
|
||||
|
||||
const LoginProfile({
|
||||
required this.uid,
|
||||
required this.uuid,
|
||||
required this.lastOnline,
|
||||
required this.profilePic,
|
||||
required this.profilePicGaussian,
|
||||
required this.nickname,
|
||||
required this.contact,
|
||||
required this.countryCode,
|
||||
required this.email,
|
||||
required this.recoveryEmail,
|
||||
required this.username,
|
||||
required this.bio,
|
||||
required this.relationship,
|
||||
this.userAlias,
|
||||
required this.channelId,
|
||||
required this.channelGroupId,
|
||||
required this.hint,
|
||||
});
|
||||
|
||||
factory LoginProfile.fromJson(Map<String, dynamic> json) =>
|
||||
_$LoginProfileFromJson(json);
|
||||
|
||||
User toEntity() => User(
|
||||
uid: uid,
|
||||
uuid: uuid,
|
||||
lastOnline: lastOnline,
|
||||
profilePic: profilePic,
|
||||
profilePicGaussian: profilePicGaussian,
|
||||
nickname: nickname,
|
||||
contact: contact,
|
||||
countryCode: countryCode,
|
||||
email: email,
|
||||
recoveryEmail: recoveryEmail,
|
||||
username: username,
|
||||
bio: bio,
|
||||
relationship: relationship,
|
||||
userAlias: userAlias,
|
||||
channelId: channelId,
|
||||
channelGroupId: channelGroupId,
|
||||
hint: hint,
|
||||
);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false, explicitToJson: true)
|
||||
class LoginData {
|
||||
@JsonKey(name: 'account_id')
|
||||
final String accountId;
|
||||
final LoginProfile profile;
|
||||
final String nonce;
|
||||
@JsonKey(name: 'access_token')
|
||||
final String accessToken;
|
||||
@JsonKey(name: 'refresh_token')
|
||||
final String refreshToken;
|
||||
@JsonKey(name: 'device_id')
|
||||
final String deviceId;
|
||||
@JsonKey(name: 'login_data')
|
||||
final String loginData;
|
||||
|
||||
const LoginData({
|
||||
required this.token,
|
||||
required this.userId,
|
||||
required this.email,
|
||||
this.nickname,
|
||||
this.avatar,
|
||||
required this.accountId,
|
||||
required this.profile,
|
||||
required this.nonce,
|
||||
required this.accessToken,
|
||||
required this.refreshToken,
|
||||
required this.deviceId,
|
||||
required this.loginData,
|
||||
});
|
||||
|
||||
factory LoginData.fromJson(Map<String, dynamic> json) =>
|
||||
_$LoginDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
|
||||
User toEntity() => profile.toEntity();
|
||||
}
|
||||
|
||||
/// DTO → Domain Entity
|
||||
User toEntity() {
|
||||
return User(
|
||||
id: userId,
|
||||
email: email,
|
||||
nickname: nickname,
|
||||
avatar: avatar,
|
||||
);
|
||||
}
|
||||
/// Top-level envelope: { "code": 0, "message": "OK", "data": { ... } }
|
||||
@JsonSerializable(createToJson: false, explicitToJson: true)
|
||||
class LoginResponse {
|
||||
final int code;
|
||||
final String message;
|
||||
final LoginData data;
|
||||
|
||||
const LoginResponse({
|
||||
required this.code,
|
||||
required this.message,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
factory LoginResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$LoginResponseFromJson(json);
|
||||
|
||||
User toEntity() => data.toEntity();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Request
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// 登录请求
|
||||
///
|
||||
/// `@ApiRequest` 自动生成 `_$LoginRequestApi` mixin,
|
||||
/// 提供 path / method / requestType / includeToken / fromJson 自动注册。
|
||||
@ApiRequest(
|
||||
path: ApiPaths.authLogin,
|
||||
method: HttpMethod.post,
|
||||
responseType: LoginData,
|
||||
responseType: LoginResponse,
|
||||
requestType: ApiRequestType.login,
|
||||
)
|
||||
@JsonSerializable()
|
||||
class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
|
||||
class LoginRequest extends ApiRequestable<LoginResponse>
|
||||
with _$LoginRequestApi {
|
||||
final String email;
|
||||
final String password;
|
||||
|
||||
|
||||
@@ -30,18 +30,18 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
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),);
|
||||
Future<User> login({required String email, required String password}) async {
|
||||
final LoginResponse? loginResponse = await _client.executeRequest(
|
||||
LoginRequest(email: email, password: password),
|
||||
);
|
||||
|
||||
if (loginData == null) {
|
||||
throw Exception('Login failed: empty response'); // TODO: 接入国际化
|
||||
if (loginResponse == null) {
|
||||
throw Exception('Login failed: empty response');
|
||||
}
|
||||
|
||||
// 回调写入 Token(内存 + 持久化由 Provider 层组合)
|
||||
_onTokenUpdate(loginData.token);
|
||||
_onTokenUpdate(loginResponse.data.accessToken);
|
||||
|
||||
return loginData.toEntity(); // DTO → Domain Entity
|
||||
return loginResponse.toEntity();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -14,15 +14,81 @@
|
||||
/// → View 渲染
|
||||
/// ```
|
||||
class User {
|
||||
final String id;
|
||||
final String email;
|
||||
final int uid;
|
||||
final String? uuid;
|
||||
final int? lastOnline;
|
||||
final String? profilePic;
|
||||
final String? profilePicGaussian;
|
||||
final String? nickname;
|
||||
final String? avatar;
|
||||
final String? contact;
|
||||
final String? countryCode;
|
||||
final String? email;
|
||||
final String? recoveryEmail;
|
||||
final String? username;
|
||||
final String? bio;
|
||||
final int? relationship;
|
||||
final String? userAlias;
|
||||
final int? channelId;
|
||||
final int? channelGroupId;
|
||||
final String? hint;
|
||||
|
||||
const User({
|
||||
required this.id,
|
||||
required this.email,
|
||||
required this.uid,
|
||||
this.uuid,
|
||||
this.lastOnline,
|
||||
this.profilePic,
|
||||
this.profilePicGaussian,
|
||||
this.nickname,
|
||||
this.avatar,
|
||||
this.contact,
|
||||
this.countryCode,
|
||||
this.email,
|
||||
this.recoveryEmail,
|
||||
this.username,
|
||||
this.bio,
|
||||
this.relationship,
|
||||
this.userAlias,
|
||||
this.channelId,
|
||||
this.channelGroupId,
|
||||
this.hint,
|
||||
});
|
||||
|
||||
User copyWith({
|
||||
int? uid,
|
||||
String? uuid,
|
||||
int? lastOnline,
|
||||
String? profilePic,
|
||||
String? profilePicGaussian,
|
||||
String? nickname,
|
||||
String? contact,
|
||||
String? countryCode,
|
||||
String? email,
|
||||
String? recoveryEmail,
|
||||
String? username,
|
||||
String? bio,
|
||||
int? relationship,
|
||||
String? userAlias,
|
||||
int? channelId,
|
||||
int? channelGroupId,
|
||||
String? hint,
|
||||
}) {
|
||||
return User(
|
||||
uid: uid ?? this.uid,
|
||||
uuid: uuid ?? this.uuid,
|
||||
lastOnline: lastOnline ?? this.lastOnline,
|
||||
profilePic: profilePic ?? this.profilePic,
|
||||
profilePicGaussian: profilePicGaussian ?? this.profilePicGaussian,
|
||||
nickname: nickname ?? this.nickname,
|
||||
contact: contact ?? this.contact,
|
||||
countryCode: countryCode ?? this.countryCode,
|
||||
email: email ?? this.email,
|
||||
recoveryEmail: recoveryEmail ?? this.recoveryEmail,
|
||||
username: username ?? this.username,
|
||||
bio: bio ?? this.bio,
|
||||
relationship: relationship ?? this.relationship,
|
||||
userAlias: userAlias ?? this.userAlias,
|
||||
channelId: channelId ?? this.channelId,
|
||||
channelGroupId: channelGroupId ?? this.channelGroupId,
|
||||
hint: hint ?? this.hint,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:im_app/app/di/db_provider.dart';
|
||||
import 'package:im_app/data/local/drift/app_database.dart';
|
||||
@@ -87,12 +86,12 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
||||
final chunk = List.generate(
|
||||
chunkSize.clamp(0, count - i),
|
||||
(j) => UsersCompanion.insert(
|
||||
uid: Value(i + j),
|
||||
uid: i + j,
|
||||
nickname: Value('User ${i + j}'),
|
||||
),
|
||||
);
|
||||
|
||||
await db.batchInsertOrReplace<User>(chunk);
|
||||
await db.batchInsertOrReplace<DriftUser>(chunk);
|
||||
completed += chunk.length;
|
||||
|
||||
// 让出主线程
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:im_app/data/models/user_dto.dart';
|
||||
import 'package:im_app/data/remote/login_request.dart';
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
import 'package:im_app/app/di/db_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -56,10 +60,22 @@ class LoginViewModel extends _$LoginViewModel {
|
||||
/// 正式 [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 provider = ref.read(authNotifierProvider);
|
||||
|
||||
// Read mock response from assets
|
||||
final String raw = await rootBundle.loadString('assets/loginData.json');
|
||||
final Map<String, dynamic> json = jsonDecode(raw);
|
||||
|
||||
// Parse into LoginData (nested under 'data' key)
|
||||
final loginResponse = LoginResponse.fromJson(json);
|
||||
final user = loginResponse.data.toEntity();
|
||||
|
||||
provider.login();
|
||||
// Open database for the user
|
||||
await storageLifeCycle.openDatabase(user.uid);
|
||||
final userCompanion = UserDto.fromEntity(user).toCompanion();
|
||||
storageApi.insert(userCompanion);
|
||||
}
|
||||
|
||||
/// 执行登录
|
||||
|
||||
@@ -62,3 +62,5 @@ dev_dependencies:
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/
|
||||
|
||||
@@ -18,19 +18,19 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
||||
|
||||
@override
|
||||
Future<void> insertOrReplace<T extends Table, D>(
|
||||
TableInfo<T, D> table,
|
||||
Insertable<D> companion,
|
||||
) async {
|
||||
TableInfo<T, D> table,
|
||||
Insertable<D> companion,
|
||||
) async {
|
||||
final db = _db;
|
||||
if (db == null) return;
|
||||
await db.into(table).insertOnConflictUpdate(companion);
|
||||
await db.into(table).insert(companion, mode: InsertMode.insertOrReplace);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> insert<T extends Table, D>(
|
||||
TableInfo<T, D> table,
|
||||
Insertable<D> companion,
|
||||
) async {
|
||||
TableInfo<T, D> table,
|
||||
Insertable<D> companion,
|
||||
) async {
|
||||
final db = _db;
|
||||
if (db == null) return;
|
||||
await db.into(table).insert(companion, mode: InsertMode.insertOrIgnore);
|
||||
@@ -38,13 +38,17 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
||||
|
||||
@override
|
||||
Future<void> batchInsertOrReplace<T extends Table, D>(
|
||||
TableInfo<T, D> table,
|
||||
List<Insertable<D>> companions,
|
||||
) async {
|
||||
TableInfo<T, D> table,
|
||||
List<Insertable<D>> companions,
|
||||
) async {
|
||||
final db = _db;
|
||||
if (db == null) return;
|
||||
await db.batch(
|
||||
(batch) => batch.insertAllOnConflictUpdate(table, companions),
|
||||
(batch) => batch.insertAll(
|
||||
table,
|
||||
companions,
|
||||
mode: InsertMode.insertOrReplace,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,10 +56,10 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
||||
|
||||
@override
|
||||
Future<void> updateWhere<T extends Table, D>(
|
||||
TableInfo<T, D> table,
|
||||
Insertable<D> companion,
|
||||
Expression<bool> Function(T) filter,
|
||||
) async {
|
||||
TableInfo<T, D> table,
|
||||
Insertable<D> companion,
|
||||
Expression<bool> Function(T) filter,
|
||||
) async {
|
||||
final db = _db;
|
||||
if (db == null) return;
|
||||
await (db.update(table)..where(filter)).write(companion);
|
||||
@@ -65,9 +69,9 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
||||
|
||||
@override
|
||||
Future<void> deleteWhere<T extends Table, D>(
|
||||
TableInfo<T, D> table,
|
||||
Expression<bool> Function(T) filter,
|
||||
) async {
|
||||
TableInfo<T, D> table,
|
||||
Expression<bool> Function(T) filter,
|
||||
) async {
|
||||
final db = _db;
|
||||
if (db == null) return;
|
||||
await (db.delete(table)..where(filter)).go();
|
||||
@@ -91,9 +95,9 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
||||
|
||||
@override
|
||||
Future<List<D>> selectWhere<T extends Table, D>(
|
||||
TableInfo<T, D> table,
|
||||
Expression<bool> Function(T) filter,
|
||||
) async {
|
||||
TableInfo<T, D> table,
|
||||
Expression<bool> Function(T) filter,
|
||||
) async {
|
||||
final db = _db;
|
||||
if (db == null) return [];
|
||||
return (db.select(table)..where(filter)).get();
|
||||
@@ -101,9 +105,9 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
||||
|
||||
@override
|
||||
Future<D?> selectFirst<T extends Table, D>(
|
||||
TableInfo<T, D> table,
|
||||
Expression<bool> Function(T) filter,
|
||||
) async {
|
||||
TableInfo<T, D> table,
|
||||
Expression<bool> Function(T) filter,
|
||||
) async {
|
||||
final db = _db;
|
||||
if (db == null) return null;
|
||||
return (db.select(table)..where(filter)..limit(1)).getSingleOrNull();
|
||||
@@ -120,9 +124,9 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
||||
|
||||
@override
|
||||
Stream<List<D>> watchWhere<T extends Table, D>(
|
||||
TableInfo<T, D> table,
|
||||
Expression<bool> Function(T) filter,
|
||||
) {
|
||||
TableInfo<T, D> table,
|
||||
Expression<bool> Function(T) filter,
|
||||
) {
|
||||
final db = _db;
|
||||
if (db == null) return const Stream.empty();
|
||||
return (db.select(table)..where(filter)).watch();
|
||||
@@ -130,9 +134,9 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
||||
|
||||
@override
|
||||
Stream<D?> watchFirst<T extends Table, D>(
|
||||
TableInfo<T, D> table,
|
||||
Expression<bool> Function(T) filter,
|
||||
) {
|
||||
TableInfo<T, D> table,
|
||||
Expression<bool> Function(T) filter,
|
||||
) {
|
||||
final db = _db;
|
||||
if (db == null) return const Stream.empty();
|
||||
return (db.select(table)..where(filter)..limit(1)).watchSingleOrNull();
|
||||
@@ -142,24 +146,24 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
||||
|
||||
@override
|
||||
Future<List<QueryRow>> rawQuery(
|
||||
String sql, [
|
||||
List<Object?> args = const [],
|
||||
]) async {
|
||||
String sql, [
|
||||
List<Object?> args = const [],
|
||||
]) async {
|
||||
final db = _db;
|
||||
if (db == null) return [];
|
||||
return db
|
||||
.customSelect(
|
||||
sql,
|
||||
variables: args.map((e) => Variable(e)).toList(),
|
||||
)
|
||||
sql,
|
||||
variables: args.map((e) => Variable(e)).toList(),
|
||||
)
|
||||
.get();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rawExecute(
|
||||
String sql, [
|
||||
List<Object?> args = const [],
|
||||
]) async {
|
||||
String sql, [
|
||||
List<Object?> args = const [],
|
||||
]) async {
|
||||
final db = _db;
|
||||
if (db == null) return;
|
||||
await db.customStatement(sql, args);
|
||||
@@ -169,9 +173,9 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
||||
|
||||
@override
|
||||
Future<int> count<T extends Table, D>(
|
||||
TableInfo<T, D> table, {
|
||||
Expression<bool> Function(T)? filter,
|
||||
}) async {
|
||||
TableInfo<T, D> table, {
|
||||
Expression<bool> Function(T)? filter,
|
||||
}) async {
|
||||
final db = _db;
|
||||
if (db == null) return 0;
|
||||
final countExpr = table.$columns.first.count();
|
||||
|
||||
Reference in New Issue
Block a user