mnemo_cards/mnemo_cards_backend/lib/user/user_manager.dart

275 lines
8.9 KiB
Dart
Raw Normal View History

2025-12-13 14:48:00 +00:00
import 'dart:convert';
2025-11-16 11:25:27 +00:00
import 'dart:developer';
2025-12-13 14:48:00 +00:00
import 'dart:math' as math;
2025-11-16 11:25:27 +00:00
2025-12-13 23:35:14 +00:00
import 'package:drift_postgres/drift_postgres.dart';
2025-11-16 11:25:27 +00:00
import 'package:injectable/injectable.dart';
2025-12-13 13:27:05 +00:00
import 'package:drift/drift.dart' as drift;
import 'package:mnemo_cards_backend/database/database.dart';
2025-11-16 11:25:27 +00:00
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import '../packs/free_packs_distributor.dart';
import '../statistics/session_tracker.dart';
import '../statistics/statistics_calculator.dart';
import '../statistics/achievement_manager.dart';
import 'secure.dart';
2025-12-13 13:27:05 +00:00
import 'user_drift_extension.dart';
2025-11-16 11:25:27 +00:00
2025-12-13 20:55:50 +00:00
Map<String?, DateTime> _onlineUsers = {};
2025-11-16 11:25:27 +00:00
@lazySingleton
class UserManager {
2025-12-13 13:27:05 +00:00
final AppDatabase _db;
2025-11-16 11:25:27 +00:00
final FreePacksDistributor _freePacksDistributor;
2025-12-13 14:48:00 +00:00
final SessionTracker _sessionTracker;
final StatisticsCalculator _statisticsCalculator;
final AchievementManager _achievementManager;
2025-11-16 11:25:27 +00:00
UserManager(
2025-12-13 13:27:05 +00:00
this._db,
2025-11-16 11:25:27 +00:00
this._freePacksDistributor,
2025-12-13 14:48:00 +00:00
this._sessionTracker,
this._statisticsCalculator,
this._achievementManager,
2025-11-16 11:25:27 +00:00
);
2025-12-13 20:55:50 +00:00
Future<UserModel?> fetchUser(String id) async {
2025-12-13 13:27:05 +00:00
final user = await _db.userDao.getUserById(id);
if (user == null) return null;
return await user.toUserModel();
2025-11-16 11:25:27 +00:00
}
2025-12-13 20:55:50 +00:00
DateTime? lastOnline(String id) => _onlineUsers[id];
2025-11-16 11:25:27 +00:00
Future<String> createOrGetAuthToken(UserModel user, String externalId) async {
if (user.id == null) {
throw Exception('Cant create token for empty id');
}
final now = DateTime.now();
2025-12-13 13:27:05 +00:00
final token = await _db.userDao.getTokenByUserId(user.id!);
if (token != null) {
2025-12-13 23:35:14 +00:00
if (token.expires.dateTime.isAfter(now)) {
2025-12-13 13:27:05 +00:00
return token.token;
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
await _db.userDao.deleteToken(token.id);
2025-11-16 11:25:27 +00:00
}
final userToken = Secure.token();
2025-12-13 13:27:05 +00:00
await _db.userDao.createToken(
TokensCompanion.insert(
2025-11-16 11:25:27 +00:00
token: userToken,
externalUserId: externalId,
userId: user.id!,
2025-12-13 23:35:14 +00:00
expires: PgDateTime(now.add(const Duration(days: 360))),
2025-11-16 11:25:27 +00:00
),
);
return userToken;
}
Future<UserModel?> getUserByToken(String authToken) async {
2025-12-13 13:27:05 +00:00
final token = await _db.userDao.getTokenByValue(authToken);
if (token == null) {
2025-11-16 11:25:27 +00:00
return null;
}
final now = DateTime.now();
2025-12-13 23:35:14 +00:00
if (token.expires.dateTime.isBefore(now)) {
2025-12-13 13:27:05 +00:00
await _db.userDao.deleteToken(token.id);
2025-11-16 11:25:27 +00:00
return null;
}
2025-12-13 13:27:05 +00:00
final user = await fetchUser(token.userId);
if (user != null) {
_onlineUsers[token.userId] = now;
2025-11-16 11:25:27 +00:00
if (_onlineUsers.length > 300) {
updateOnlineUsers();
}
}
return user;
}
Future<void> updateOnlineUsers() async {
if (_onlineUsers.isEmpty) {
return;
}
2025-12-13 13:27:05 +00:00
final userIds = _onlineUsers.keys.whereNotNull().toList();
for (final userId in userIds) {
final lastOnline = _onlineUsers[userId];
if (lastOnline != null) {
await _db.userDao.updateUserDataPartial(
UserDatasCompanion(
userId: drift.Value(userId),
2025-12-13 23:35:14 +00:00
lastTimeOnline: drift.Value(PgDateTime(lastOnline)),
updatedAt: drift.Value(PgDateTime(DateTime.now())),
2025-12-13 13:27:05 +00:00
),
);
}
}
_onlineUsers.clear();
2025-11-16 11:25:27 +00:00
}
Future<(UserModel, String)> createOrGetUser({
required String externalId,
required String email,
String? name,
}) async {
2025-12-13 13:27:05 +00:00
// Check if user exists by externalId
final existingUser = await _db.userDao.getUserByExternalId(externalId);
if (existingUser != null) {
print('User found $name $email');
final userModel = await existingUser.toUserModel();
final token = await createOrGetAuthToken(userModel, externalId);
return (userModel, token);
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
print('Creating new user $name $email');
2025-11-16 11:25:27 +00:00
2025-12-14 00:13:49 +00:00
// Create new user with user data in transaction
final now = DateTime.now();
2025-12-13 13:27:05 +00:00
final userCompanion = UsersCompanion.insert(
externalUserId: externalId,
name: drift.Value(name),
email: drift.Value(email),
2025-12-14 00:13:49 +00:00
// Explicitly set fields with default values to ensure they're returned by insertReturning
admin: drift.Value(false),
purchases: drift.Value([]),
createdAt: drift.Value(PgDateTime(now)),
updatedAt: drift.Value(PgDateTime(now)),
isDeleted: drift.Value(false),
2025-11-16 11:25:27 +00:00
);
2025-12-14 00:13:49 +00:00
final userDataCompanion = UserDatasCompanion.insert(
userId: '', // Will be set by createUserWithData via copyWith
registrationDate: drift.Value(PgDateTime(now)),
);
2025-11-16 11:25:27 +00:00
2025-12-14 00:13:49 +00:00
final userId = await _db.userDao.createUserWithData(
user: userCompanion,
userData: userDataCompanion,
2025-12-13 13:27:05 +00:00
);
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
final user = await _db.userDao.getUserById(userId);
if (user == null) {
throw Exception('Failed to create user');
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
final userModel = await user.toUserModel();
final token = await createOrGetAuthToken(userModel, externalId);
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
// Give free packs to new user
await _freePacksDistributor.giveFreePacksToUser(userModel);
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
print('User $name $email created successfully');
return (userModel, token);
2025-11-16 11:25:27 +00:00
}
2025-12-13 14:48:00 +00:00
/// Обновить настройки пользователя
Future<void> updateUserSettings(UserModel user, UserSettingsDto settings) async {
if (user.id == null) {
throw Exception('User ID is required');
}
await _db.userDao.updateUserPartial(
UsersCompanion(
id: drift.Value(user.id!),
userSettings: drift.Value(jsonEncode(settings.toJson())),
2025-12-13 23:35:14 +00:00
updatedAt: drift.Value(PgDateTime(DateTime.now())),
2025-12-13 14:48:00 +00:00
),
);
}
/// Добавить статистику теста
Future<void> addTestStatistics(UserModel user, TestStatisticsDto testStat) async {
if (user.id == null) {
throw Exception('User ID is required');
}
// Получить или создать UserData
var userData = await _db.userDao.getUserData(user.id!);
if (userData == null) {
await _db.userDao.createUserData(
UserDatasCompanion.insert(userId: user.id!),
);
userData = await _db.userDao.getUserData(user.id!);
if (userData == null) {
throw Exception('Failed to create user data');
}
}
// Проверить, не был ли этот sessionToken уже обработан
if (userData.lastTestSessionToken == testStat.sessionToken) {
log('Old session token, skipping');
return;
}
// Получить или создать TestStatistic
final existingStat = await _db.testDao.getTestStatistics(user.id!, testStat.testId);
// Обновить результаты теста
// results хранится как Map<String, dynamic>, где ключ 'attempts' содержит список попыток
final currentResults = existingStat?.results ?? <String, dynamic>{};
final attemptsKey = 'attempts';
final currentAttempts = (currentResults[attemptsKey] as List<dynamic>?) ?? [];
final newAttempt = <String, dynamic>{
'sessionToken': testStat.sessionToken,
'words': testStat.words.words.map((w) => w.toJson()).toList(),
};
final updatedAttempts = <dynamic>[...currentAttempts, newAttempt];
final updatedResults = <String, dynamic>{
...currentResults,
attemptsKey: updatedAttempts,
};
if (existingStat != null) {
// Обновить существующую статистику
final updatedStat = existingStat.copyWith(
results: drift.Value(updatedResults),
2025-12-13 23:35:14 +00:00
updatedAt: PgDateTime(DateTime.now()),
2025-12-13 14:48:00 +00:00
);
await _db.testDao.updateTestStatistics(updatedStat);
} else {
// Создать новую статистику
await _db.testDao.createTestStatistics(
TestStatisticsCompanion.insert(
userId: user.id!,
testId: testStat.testId,
results: drift.Value(updatedResults),
2025-12-13 23:35:14 +00:00
completedAt: drift.Value(PgDateTime(DateTime.now())),
2025-12-13 14:48:00 +00:00
),
);
}
// Обновить статистику пользователя
final now = DateTime.now();
final todayNormalized = DateTime(now.year, now.month, now.day);
final studyDates = List<DateTime>.from(userData.studyDates ?? []);
// Добавить сегодняшнюю дату если еще нет
if (!studyDates.any((date) =>
date.year == todayNormalized.year &&
date.month == todayNormalized.month &&
date.day == todayNormalized.day)) {
studyDates.add(todayNormalized);
}
// Пересчитать streak
final currentStreak = _statisticsCalculator.calculateStreak(studyDates);
final longestStreak = math.max(userData.longestStreak, currentStreak);
// Обновить UserData
await _db.userDao.updateUserDataPartial(
UserDatasCompanion(
userId: drift.Value(user.id!),
lastTestSessionToken: drift.Value(testStat.sessionToken),
studyDates: drift.Value(studyDates),
currentStreak: drift.Value(currentStreak),
longestStreak: drift.Value(longestStreak),
2025-12-13 23:35:14 +00:00
lastTimeOnline: drift.Value(PgDateTime(now)),
updatedAt: drift.Value(PgDateTime(now)),
2025-12-13 14:48:00 +00:00
),
);
}
2025-12-13 13:27:05 +00:00
}