vack tasks
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
This commit is contained in:
parent
336bafc600
commit
99100deb20
20 changed files with 2219 additions and 0 deletions
489
mnemo_cards_backend/lib/api/v2/admin_tasks_api_v2.dart
Normal file
489
mnemo_cards_backend/lib/api/v2/admin_tasks_api_v2.dart
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/api/authorize/acl_types.dart';
|
||||
import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
|
||||
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
|
||||
import 'package:mnemo_cards_backend/repository/export.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
part 'admin_tasks_api_v2.g.dart';
|
||||
|
||||
/// Admin endpoints for managing user tasks in API v2.
|
||||
@injectable
|
||||
class AdminTasksApiV2 {
|
||||
final AppDatabase _db;
|
||||
final TaskRepository _taskRepository;
|
||||
|
||||
AdminTasksApiV2(this._db, this._taskRepository);
|
||||
|
||||
Response _json(
|
||||
Object? data, {
|
||||
int statusCode = 200,
|
||||
Map<String, String> headers = const {},
|
||||
}) {
|
||||
return Response(
|
||||
statusCode,
|
||||
body: data == null ? null : jsonEncode(data),
|
||||
headers: {'Content-Type': 'application/json', ...headers},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> _ensureAdmin(Request request) async {
|
||||
try {
|
||||
await request.access!.requireAdmin(
|
||||
AdminAction.access,
|
||||
user: request.user,
|
||||
);
|
||||
return Response.ok(null);
|
||||
} on AccessDenied catch (e) {
|
||||
return Response(e.status, body: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Drift UserTask to UserTaskModel for JSON serialization
|
||||
UserTaskModel _userTaskToModel(UserTask task) {
|
||||
// Parse rewards from JSON
|
||||
final rewardsJson = task.rewards as List<dynamic>? ?? [];
|
||||
final rewards = rewardsJson
|
||||
.map(
|
||||
(r) => TaskRewardModel.fromJson(Map<String, dynamic>.from(r as Map)),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return UserTaskModel(
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
type: task.type,
|
||||
difficulty: task.difficulty,
|
||||
status: task.status,
|
||||
rewards: rewards,
|
||||
createdAt: task.createdAt.dateTime,
|
||||
expiresAt: task.expiresAt.dateTime,
|
||||
completedAt: task.completedAt?.dateTime,
|
||||
proofUrl: task.proofUrl,
|
||||
instructions: task.instructions,
|
||||
tags: task.tags,
|
||||
imageUrl: task.imageUrl,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert UserTaskModel to UserTasksCompanion for database operations
|
||||
UserTasksCompanion _modelToCompanion(
|
||||
UserTaskModel model, {
|
||||
bool isUpdate = false,
|
||||
}) {
|
||||
final rewardsJson = model.rewards.map((r) => r.toJson()).toList();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
|
||||
return UserTasksCompanion(
|
||||
id: isUpdate && model.id != null
|
||||
? drift.Value(model.id!)
|
||||
: const drift.Value.absent(),
|
||||
title: drift.Value(model.title),
|
||||
description: drift.Value(model.description),
|
||||
type: drift.Value(model.type),
|
||||
difficulty: drift.Value(model.difficulty),
|
||||
status: drift.Value(model.status),
|
||||
rewards: drift.Value(rewardsJson),
|
||||
createdAt: isUpdate
|
||||
? const drift.Value.absent()
|
||||
: drift.Value(PgDateTime(model.createdAt)),
|
||||
expiresAt: drift.Value(PgDateTime(model.expiresAt)),
|
||||
completedAt: model.completedAt != null
|
||||
? drift.Value(PgDateTime(model.completedAt!))
|
||||
: const drift.Value.absent(),
|
||||
proofUrl: model.proofUrl != null
|
||||
? drift.Value(model.proofUrl!)
|
||||
: const drift.Value.absent(),
|
||||
instructions: model.instructions != null
|
||||
? drift.Value(model.instructions!)
|
||||
: const drift.Value.absent(),
|
||||
tags: drift.Value(model.tags),
|
||||
imageUrl: model.imageUrl != null
|
||||
? drift.Value(model.imageUrl!)
|
||||
: const drift.Value.absent(),
|
||||
updatedAt: drift.Value(now),
|
||||
);
|
||||
}
|
||||
|
||||
/// GET /api/v2/admin/tasks
|
||||
/// Get all tasks with pagination and filters
|
||||
@Route.get('/admin/tasks')
|
||||
Future<Response> getTasks(Request request) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
final queryParams = request.url.queryParameters;
|
||||
|
||||
// Parse pagination parameters
|
||||
final page = int.tryParse(queryParams['page'] ?? '1') ?? 1;
|
||||
final limit = int.tryParse(queryParams['limit'] ?? '20') ?? 20;
|
||||
final type = queryParams['type'];
|
||||
final difficulty = queryParams['difficulty'];
|
||||
final status = queryParams['status'];
|
||||
final search = queryParams['search'] ?? '';
|
||||
|
||||
// Validate pagination
|
||||
if (page < 1) {
|
||||
return _json({
|
||||
'error': 'Invalid page parameter',
|
||||
'message': 'Page must be greater than 0. Received: $page',
|
||||
'field': 'page',
|
||||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
// Limit validation: cap at 100
|
||||
final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit);
|
||||
|
||||
// Get all tasks
|
||||
List<UserTask> allTasks = await _taskRepository.getAllUserTasks();
|
||||
|
||||
// Apply filters
|
||||
if (type != null && type.isNotEmpty) {
|
||||
allTasks = allTasks.where((t) => t.type == type).toList();
|
||||
}
|
||||
if (difficulty != null && difficulty.isNotEmpty) {
|
||||
allTasks = allTasks.where((t) => t.difficulty == difficulty).toList();
|
||||
}
|
||||
if (status != null && status.isNotEmpty) {
|
||||
allTasks = allTasks.where((t) => t.status == status).toList();
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if (search.isNotEmpty) {
|
||||
final searchTerm = search.toLowerCase();
|
||||
allTasks = allTasks.where((task) {
|
||||
return task.title.toLowerCase().contains(searchTerm) ||
|
||||
task.description.toLowerCase().contains(searchTerm) ||
|
||||
task.id.toLowerCase().contains(searchTerm);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Convert to models
|
||||
final taskModels = allTasks.map(_userTaskToModel).toList();
|
||||
|
||||
// Calculate pagination
|
||||
final total = taskModels.length;
|
||||
final totalPages = (total / validatedLimit).ceil();
|
||||
final offset = (page - 1) * validatedLimit;
|
||||
final paginatedTasks = taskModels
|
||||
.skip(offset)
|
||||
.take(validatedLimit)
|
||||
.toList();
|
||||
|
||||
return _json({
|
||||
'items': paginatedTasks.map((t) => t.toJson()).toList(),
|
||||
'total': total,
|
||||
'page': page,
|
||||
'limit': validatedLimit,
|
||||
'totalPages': totalPages,
|
||||
});
|
||||
} catch (e, s) {
|
||||
log('Error in getTasks: $e\n$s');
|
||||
return _json({
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to fetch tasks',
|
||||
}, statusCode: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/v2/admin/tasks/{taskId}
|
||||
/// Get task details by ID
|
||||
@Route.get('/admin/tasks/<taskId>')
|
||||
Future<Response> getTask(Request request, String taskId) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
if (taskId.isEmpty) {
|
||||
return _json({
|
||||
'error': 'Invalid task ID',
|
||||
'message': 'Task ID cannot be empty',
|
||||
'field': 'taskId',
|
||||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
final task = await _taskRepository.getUserTaskById(taskId);
|
||||
if (task == null) {
|
||||
return _json({
|
||||
'error': 'Task not found',
|
||||
'message': 'The requested task does not exist or has been deleted',
|
||||
}, statusCode: 404);
|
||||
}
|
||||
|
||||
final model = _userTaskToModel(task);
|
||||
return _json(model.toJson());
|
||||
} catch (e, s) {
|
||||
log('Error in getTask: $e\n$s');
|
||||
return _json({
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to fetch task',
|
||||
}, statusCode: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /api/v2/admin/tasks
|
||||
/// Create a new task
|
||||
@Route.post('/admin/tasks')
|
||||
Future<Response> createTask(Request request) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
final body = await request.readAsString();
|
||||
if (body.isEmpty) {
|
||||
return _json({
|
||||
'error': 'bad_request',
|
||||
'message': 'Task payload is required',
|
||||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
late final UserTaskModel taskModel;
|
||||
try {
|
||||
taskModel = UserTaskModel.fromJson(
|
||||
jsonDecode(body) as Map<String, dynamic>,
|
||||
);
|
||||
} catch (e) {
|
||||
return _json({
|
||||
'error': 'bad_request',
|
||||
'message': 'Invalid task payload: $e',
|
||||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (taskModel.title.trim().isEmpty) {
|
||||
return _json({
|
||||
'error': 'Validation error',
|
||||
'message': 'Title is required',
|
||||
'field': 'title',
|
||||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
if (taskModel.description.trim().isEmpty) {
|
||||
return _json({
|
||||
'error': 'Validation error',
|
||||
'message': 'Description is required',
|
||||
'field': 'description',
|
||||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
// Create task
|
||||
final companion = _modelToCompanion(taskModel, isUpdate: false);
|
||||
final taskId = await _taskRepository.createUserTask(companion);
|
||||
|
||||
// Fetch created task
|
||||
final createdTask = await _taskRepository.getUserTaskById(taskId);
|
||||
if (createdTask == null) {
|
||||
return _json({
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to retrieve created task',
|
||||
}, statusCode: 500);
|
||||
}
|
||||
|
||||
final model = _userTaskToModel(createdTask);
|
||||
return _json({'success': true, 'task': model.toJson()});
|
||||
} catch (e, s) {
|
||||
log('Error in createTask: $e\n$s');
|
||||
return _json({
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to create task',
|
||||
}, statusCode: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// PUT /api/v2/admin/tasks/{taskId}
|
||||
/// Update an existing task
|
||||
@Route.put('/admin/tasks/<taskId>')
|
||||
Future<Response> updateTask(Request request, String taskId) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
if (taskId.isEmpty) {
|
||||
return _json({
|
||||
'error': 'Invalid task ID',
|
||||
'message': 'Task ID cannot be empty',
|
||||
'field': 'taskId',
|
||||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
// Check if task exists
|
||||
final existing = await _taskRepository.getUserTaskById(taskId);
|
||||
if (existing == null) {
|
||||
return _json({
|
||||
'error': 'Task not found',
|
||||
'message': 'The task you are trying to update does not exist',
|
||||
}, statusCode: 404);
|
||||
}
|
||||
|
||||
final body = await request.readAsString();
|
||||
if (body.isEmpty) {
|
||||
return _json({
|
||||
'error': 'bad_request',
|
||||
'message': 'Task payload is required',
|
||||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
late final Map<String, dynamic> updateData;
|
||||
try {
|
||||
updateData = jsonDecode(body) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
return _json({
|
||||
'error': 'bad_request',
|
||||
'message': 'Invalid task payload: $e',
|
||||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
// Convert existing task to model
|
||||
final existingModel = _userTaskToModel(existing);
|
||||
|
||||
// Apply updates (partial update)
|
||||
final updatedModel = existingModel.copyWith(
|
||||
title: updateData['title'] as String? ?? existingModel.title,
|
||||
description:
|
||||
updateData['description'] as String? ?? existingModel.description,
|
||||
type: updateData['type'] as String? ?? existingModel.type,
|
||||
difficulty:
|
||||
updateData['difficulty'] as String? ?? existingModel.difficulty,
|
||||
status: updateData['status'] as String? ?? existingModel.status,
|
||||
rewards: updateData['rewards'] != null
|
||||
? (updateData['rewards'] as List)
|
||||
.map(
|
||||
(r) => TaskRewardModel.fromJson(
|
||||
Map<String, dynamic>.from(r as Map),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
: existingModel.rewards,
|
||||
expiresAt: updateData['expiresAt'] != null
|
||||
? DateTime.parse(updateData['expiresAt'] as String)
|
||||
: existingModel.expiresAt,
|
||||
completedAt: updateData['completedAt'] != null
|
||||
? DateTime.parse(updateData['completedAt'] as String)
|
||||
: existingModel.completedAt,
|
||||
proofUrl: updateData['proofUrl'] as String? ?? existingModel.proofUrl,
|
||||
instructions:
|
||||
updateData['instructions'] as String? ?? existingModel.instructions,
|
||||
tags: updateData['tags'] != null
|
||||
? List<String>.from(updateData['tags'] as List)
|
||||
: existingModel.tags,
|
||||
imageUrl: updateData['imageUrl'] as String? ?? existingModel.imageUrl,
|
||||
);
|
||||
|
||||
// Convert to Drift UserTask for update using update().write() for better null handling
|
||||
final rewardsJson = updatedModel.rewards.map((r) => r.toJson()).toList();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
|
||||
await (_db.update(
|
||||
_db.userTasks,
|
||||
)..where((ut) => ut.id.equals(taskId))).write(
|
||||
UserTasksCompanion(
|
||||
title: drift.Value(updatedModel.title),
|
||||
description: drift.Value(updatedModel.description),
|
||||
type: drift.Value(updatedModel.type),
|
||||
difficulty: drift.Value(updatedModel.difficulty),
|
||||
status: drift.Value(updatedModel.status),
|
||||
rewards: drift.Value(rewardsJson),
|
||||
expiresAt: drift.Value(PgDateTime(updatedModel.expiresAt)),
|
||||
completedAt: updatedModel.completedAt != null
|
||||
? drift.Value(PgDateTime(updatedModel.completedAt!))
|
||||
: const drift.Value.absent(),
|
||||
proofUrl: updatedModel.proofUrl != null
|
||||
? drift.Value(updatedModel.proofUrl!)
|
||||
: const drift.Value.absent(),
|
||||
instructions: updatedModel.instructions != null
|
||||
? drift.Value(updatedModel.instructions!)
|
||||
: const drift.Value.absent(),
|
||||
tags: drift.Value(updatedModel.tags),
|
||||
imageUrl: updatedModel.imageUrl != null
|
||||
? drift.Value(updatedModel.imageUrl!)
|
||||
: const drift.Value.absent(),
|
||||
updatedAt: drift.Value(now),
|
||||
),
|
||||
);
|
||||
|
||||
// Fetch updated task
|
||||
final updatedTask = await _taskRepository.getUserTaskById(taskId);
|
||||
if (updatedTask == null) {
|
||||
return _json({
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to retrieve updated task',
|
||||
}, statusCode: 500);
|
||||
}
|
||||
|
||||
final model = _userTaskToModel(updatedTask);
|
||||
return _json({'success': true, 'task': model.toJson()});
|
||||
} catch (e, s) {
|
||||
log('Error in updateTask: $e\n$s');
|
||||
return _json({
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to update task',
|
||||
}, statusCode: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// DELETE /api/v2/admin/tasks/{taskId}
|
||||
/// Delete a task
|
||||
@Route.delete('/admin/tasks/<taskId>')
|
||||
Future<Response> deleteTask(Request request, String taskId) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
if (taskId.isEmpty) {
|
||||
return _json({
|
||||
'error': 'Invalid task ID',
|
||||
'message': 'Task ID cannot be empty',
|
||||
'field': 'taskId',
|
||||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
// Check if task exists
|
||||
final task = await _taskRepository.getUserTaskById(taskId);
|
||||
if (task == null) {
|
||||
return _json({
|
||||
'error': 'Task not found',
|
||||
'message': 'The task you are trying to delete does not exist',
|
||||
}, statusCode: 404);
|
||||
}
|
||||
|
||||
// Delete task
|
||||
final deleted = await _taskRepository.deleteUserTask(taskId);
|
||||
if (!deleted) {
|
||||
return _json({
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to delete task',
|
||||
}, statusCode: 500);
|
||||
}
|
||||
|
||||
return _json({'success': true, 'message': 'Task deleted successfully'});
|
||||
} catch (e, s) {
|
||||
log('Error in deleteTask: $e\n$s');
|
||||
return _json({
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to delete task',
|
||||
}, statusCode: 500);
|
||||
}
|
||||
}
|
||||
|
||||
Router get router => _$AdminTasksApiV2Router(this);
|
||||
}
|
||||
17
mnemo_cards_backend/lib/api/v2/admin_tasks_api_v2.g.dart
Normal file
17
mnemo_cards_backend/lib/api/v2/admin_tasks_api_v2.g.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'admin_tasks_api_v2.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ShelfRouterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Router _$AdminTasksApiV2Router(AdminTasksApiV2 service) {
|
||||
final router = Router();
|
||||
router.add('GET', r'/admin/tasks', service.getTasks);
|
||||
router.add('GET', r'/admin/tasks/<taskId>', service.getTask);
|
||||
router.add('POST', r'/admin/tasks', service.createTask);
|
||||
router.add('PUT', r'/admin/tasks/<taskId>', service.updateTask);
|
||||
router.add('DELETE', r'/admin/tasks/<taskId>', service.deleteTask);
|
||||
return router;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
||||
/// Repository для работы с достижениями
|
||||
/// Работает с Drift моделями (UserAchievement), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class AchievementRepository {
|
||||
final AppDatabase _db;
|
||||
|
||||
AchievementRepository(this._db);
|
||||
|
||||
/// Получить все достижения пользователя
|
||||
Future<List<UserAchievement>> getUserAchievements(String userId) async {
|
||||
return await _db.achievementDao.getUserAchievements(userId);
|
||||
}
|
||||
|
||||
/// Получить достижение пользователя
|
||||
Future<UserAchievement?> getUserAchievement(
|
||||
String userId,
|
||||
String achievementId,
|
||||
) async {
|
||||
return await _db.achievementDao.getUserAchievement(userId, achievementId);
|
||||
}
|
||||
|
||||
/// Проверить, есть ли у пользователя достижение
|
||||
Future<bool> hasAchievement(String userId, String achievementId) async {
|
||||
return await _db.achievementDao.hasAchievement(userId, achievementId);
|
||||
}
|
||||
|
||||
/// Создать достижение пользователя
|
||||
Future<String> unlockAchievement(UserAchievementsCompanion companion) async {
|
||||
return await _db.achievementDao.unlockAchievement(companion);
|
||||
}
|
||||
|
||||
/// Обновить прогресс достижения
|
||||
Future<bool> updateAchievementProgress(
|
||||
String userId,
|
||||
String achievementId,
|
||||
double progress,
|
||||
) async {
|
||||
return await _db.achievementDao.updateAchievementProgress(
|
||||
userId,
|
||||
achievementId,
|
||||
progress,
|
||||
);
|
||||
}
|
||||
|
||||
/// Получить прогресс по всем достижениям пользователя
|
||||
Future<Map<String, double>> getAchievementProgress(String userId) async {
|
||||
return await _db.achievementDao.getAchievementProgress(userId);
|
||||
}
|
||||
|
||||
/// Удалить достижение пользователя (для сброса)
|
||||
Future<void> removeAchievement(String userId, String achievementId) async {
|
||||
await _db.achievementDao.removeAchievement(userId, achievementId);
|
||||
}
|
||||
}
|
||||
138
mnemo_cards_backend/lib/repository/discount_repository.dart
Normal file
138
mnemo_cards_backend/lib/repository/discount_repository.dart
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
||||
/// Repository для работы со скидками
|
||||
/// Работает с Drift моделями (DiscountCampaign, Discount), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class DiscountRepository {
|
||||
final AppDatabase _db;
|
||||
|
||||
DiscountRepository(this._db);
|
||||
|
||||
// ==================== DiscountCampaigns ====================
|
||||
|
||||
/// Получить кампанию по ID
|
||||
Future<DiscountCampaign?> getCampaignById(String id) async {
|
||||
return await _db.discountDao.getCampaignById(id);
|
||||
}
|
||||
|
||||
/// Получить все активные кампании
|
||||
Future<List<DiscountCampaign>> getActiveCampaigns() async {
|
||||
return await _db.discountDao.getActiveCampaigns();
|
||||
}
|
||||
|
||||
/// Получить все кампании
|
||||
Future<List<DiscountCampaign>> getAllCampaigns() async {
|
||||
return await _db.discountDao.getAllCampaigns();
|
||||
}
|
||||
|
||||
/// Получить кампании по статусу
|
||||
Future<List<DiscountCampaign>> getCampaignsByStatus(String status) async {
|
||||
return await _db.discountDao.getCampaignsByStatus(status);
|
||||
}
|
||||
|
||||
/// Получить кампании по статусам
|
||||
Future<List<DiscountCampaign>> getCampaignsByStatuses(
|
||||
List<String> statuses,
|
||||
) async {
|
||||
return await _db.discountDao.getCampaignsByStatuses(statuses);
|
||||
}
|
||||
|
||||
/// Получить активные кампании для пользователя с учетом тегов и продуктов
|
||||
Future<List<DiscountCampaign>> getActiveCampaignsForUser({
|
||||
required List<String> userTags,
|
||||
String? productType,
|
||||
String? productId,
|
||||
}) async {
|
||||
return await _db.discountDao.getActiveCampaignsForUser(
|
||||
userTags: userTags,
|
||||
productType: productType,
|
||||
productId: productId,
|
||||
);
|
||||
}
|
||||
|
||||
/// Создать кампанию
|
||||
Future<String> createCampaign(DiscountCampaignsCompanion campaign) async {
|
||||
return await _db.discountDao.createCampaign(campaign);
|
||||
}
|
||||
|
||||
/// Обновить кампанию
|
||||
Future<bool> updateCampaign(DiscountCampaign campaign) async {
|
||||
return await _db.discountDao.updateCampaign(campaign);
|
||||
}
|
||||
|
||||
/// Обновить статус кампании
|
||||
Future<void> updateCampaignStatus(String campaignId, String status) async {
|
||||
await _db.discountDao.updateCampaignStatus(campaignId, status);
|
||||
}
|
||||
|
||||
/// Удалить кампанию (soft delete)
|
||||
Future<void> deleteCampaign(String campaignId) async {
|
||||
await _db.discountDao.deleteCampaign(campaignId);
|
||||
}
|
||||
|
||||
// ==================== Discounts ====================
|
||||
|
||||
/// Получить скидку по ID
|
||||
Future<Discount?> getDiscountById(String id) async {
|
||||
return await _db.discountDao.getDiscountById(id);
|
||||
}
|
||||
|
||||
/// Получить скидки кампании
|
||||
Future<List<Discount>> getDiscountsByCampaignId(String campaignId) async {
|
||||
return await _db.discountDao.getDiscountsByCampaignId(campaignId);
|
||||
}
|
||||
|
||||
/// Создать скидку
|
||||
Future<String> createDiscount(DiscountsCompanion discount) async {
|
||||
return await _db.discountDao.createDiscount(discount);
|
||||
}
|
||||
|
||||
/// Обновить скидку
|
||||
Future<bool> updateDiscount(Discount discount) async {
|
||||
return await _db.discountDao.updateDiscount(discount);
|
||||
}
|
||||
|
||||
/// Удалить скидку (soft delete)
|
||||
Future<void> softDeleteDiscount(String discountId) async {
|
||||
await _db.discountDao.softDeleteDiscount(discountId);
|
||||
}
|
||||
|
||||
// ==================== DiscountUserDatas ====================
|
||||
|
||||
/// Получить скидки пользователя
|
||||
Future<List<Discount>> getUserDiscounts(String userId) async {
|
||||
return await _db.discountDao.getUserDiscounts(userId);
|
||||
}
|
||||
|
||||
/// Дать пользователю доступ к скидке
|
||||
Future<void> grantDiscountToUser(String userId, String discountId) async {
|
||||
await _db.discountDao.grantDiscountToUser(userId, discountId);
|
||||
}
|
||||
|
||||
/// Дать пользователю доступ к нескольким скидкам
|
||||
Future<void> grantDiscountsToUser(
|
||||
String userId,
|
||||
List<String> discountIds,
|
||||
) async {
|
||||
await _db.discountDao.grantDiscountsToUser(userId, discountIds);
|
||||
}
|
||||
|
||||
/// Отозвать скидку у пользователя
|
||||
Future<void> revokeDiscountFromUser(String userId, String discountId) async {
|
||||
await _db.discountDao.revokeDiscountFromUser(userId, discountId);
|
||||
}
|
||||
|
||||
/// Отозвать несколько скидок у пользователя
|
||||
Future<void> revokeDiscountsFromUser(
|
||||
String userId,
|
||||
List<String> discountIds,
|
||||
) async {
|
||||
await _db.discountDao.revokeDiscountsFromUser(userId, discountIds);
|
||||
}
|
||||
|
||||
/// Проверить, есть ли у пользователя доступ к скидке
|
||||
Future<bool> hasDiscountAccess(String userId, String discountId) async {
|
||||
return await _db.discountDao.hasDiscountAccess(userId, discountId);
|
||||
}
|
||||
}
|
||||
13
mnemo_cards_backend/lib/repository/export.dart
Normal file
13
mnemo_cards_backend/lib/repository/export.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export 'user_repository.dart';
|
||||
export 'pack_repository.dart';
|
||||
export 'iap_repository.dart';
|
||||
export 'payment_repository.dart';
|
||||
export 'subscription_repository.dart';
|
||||
export 'test_repository.dart';
|
||||
export 'task_repository.dart';
|
||||
export 'promo_code_repository.dart';
|
||||
export 'discount_repository.dart';
|
||||
export 'statistics_repository.dart';
|
||||
export 'word_statistics_repository.dart';
|
||||
export 'achievement_repository.dart';
|
||||
export 'сonverters/export.dart';
|
||||
219
mnemo_cards_backend/lib/repository/iap_repository.dart
Normal file
219
mnemo_cards_backend/lib/repository/iap_repository.dart
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import 'package:googleapis/firestore/v1.dart';
|
||||
|
||||
import '../api/purchase/products.dart';
|
||||
|
||||
enum IAPSource { googleplay, appstore }
|
||||
|
||||
abstract class Purchase {
|
||||
final IAPSource iapSource;
|
||||
final String orderId;
|
||||
final String productId;
|
||||
final String? userId;
|
||||
final DateTime purchaseDate;
|
||||
final ProductType type;
|
||||
|
||||
const Purchase({
|
||||
required this.iapSource,
|
||||
required this.orderId,
|
||||
required this.productId,
|
||||
required this.userId,
|
||||
required this.purchaseDate,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
Map<String, Value> toDocument() {
|
||||
return {
|
||||
'iapSource': Value(stringValue: iapSource.name),
|
||||
'orderId': Value(stringValue: orderId),
|
||||
'productId': Value(stringValue: productId),
|
||||
'userId': Value(stringValue: userId),
|
||||
'purchaseDate': Value(
|
||||
timestampValue: purchaseDate.toUtc().toIso8601String(),
|
||||
),
|
||||
'type': Value(stringValue: type.name),
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, Value> updateDocument();
|
||||
|
||||
static Purchase fromDocument(Document e) {
|
||||
final type = ProductType.values.firstWhere(
|
||||
(element) => element.name == e.fields!['type']!.stringValue,
|
||||
);
|
||||
switch (type) {
|
||||
case ProductType.subscription:
|
||||
return SubscriptionPurchase(
|
||||
iapSource: e.fields!['iapSource']!.stringValue == 'googleplay'
|
||||
? IAPSource.googleplay
|
||||
: IAPSource.appstore,
|
||||
orderId: e.fields!['orderId']!.stringValue!,
|
||||
productId: e.fields!['productId']!.stringValue!,
|
||||
userId: e.fields!['userId']?.stringValue,
|
||||
purchaseDate: DateTime.parse(
|
||||
e.fields!['purchaseDate']!.timestampValue!,
|
||||
),
|
||||
status: SubscriptionStatus.values.firstWhere(
|
||||
(element) => element.name == e.fields!['status']!.stringValue,
|
||||
),
|
||||
expiryDate:
|
||||
DateTime.tryParse(
|
||||
e.fields!['expiryDate']?.timestampValue ?? '',
|
||||
) ??
|
||||
DateTime.now(),
|
||||
);
|
||||
case ProductType.nonSubscription:
|
||||
return NonSubscriptionPurchase(
|
||||
iapSource: e.fields!['iapSource']!.stringValue == 'googleplay'
|
||||
? IAPSource.googleplay
|
||||
: IAPSource.appstore,
|
||||
orderId: e.fields!['orderId']!.stringValue!,
|
||||
productId: e.fields!['productId']!.stringValue!,
|
||||
userId: e.fields!['userId']?.stringValue,
|
||||
purchaseDate: DateTime.parse(
|
||||
e.fields!['purchaseDate']!.timestampValue!,
|
||||
),
|
||||
status: NonSubscriptionStatus.values.firstWhere(
|
||||
(element) => element.name == e.fields!['status']!.stringValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NonSubscriptionStatus { pending, completed, cancelled }
|
||||
|
||||
enum SubscriptionStatus { pending, active, expired }
|
||||
|
||||
class NonSubscriptionPurchase extends Purchase {
|
||||
final NonSubscriptionStatus status;
|
||||
|
||||
NonSubscriptionPurchase({
|
||||
required super.iapSource,
|
||||
required super.orderId,
|
||||
required super.productId,
|
||||
required super.userId,
|
||||
required super.purchaseDate,
|
||||
required this.status,
|
||||
super.type = ProductType.nonSubscription,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, Value> toDocument() {
|
||||
final doc = super.toDocument();
|
||||
doc.addAll({'status': Value(stringValue: status.name)});
|
||||
return doc;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Value> updateDocument() {
|
||||
return {'status': Value(stringValue: status.name)};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NonSubscriptionPurchase { '
|
||||
'iapSource: $iapSource, '
|
||||
'orderId: $orderId, '
|
||||
'productId: $productId, '
|
||||
'userId: $userId, '
|
||||
'purchaseDate: $purchaseDate, '
|
||||
'status: $status, '
|
||||
'type: $type '
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
class SubscriptionPurchase extends Purchase {
|
||||
final SubscriptionStatus status;
|
||||
final DateTime expiryDate;
|
||||
|
||||
SubscriptionPurchase({
|
||||
required super.iapSource,
|
||||
required super.orderId,
|
||||
required super.productId,
|
||||
required super.userId,
|
||||
required super.purchaseDate,
|
||||
required this.status,
|
||||
required this.expiryDate,
|
||||
super.type = ProductType.subscription,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, Value> toDocument() {
|
||||
final doc = super.toDocument();
|
||||
doc.addAll({
|
||||
'expiryDate': Value(timestampValue: expiryDate.toUtc().toIso8601String()),
|
||||
'status': Value(stringValue: status.name),
|
||||
});
|
||||
return doc;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Value> updateDocument() {
|
||||
return {'status': Value(stringValue: status.name)};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SubscriptionPurchase { '
|
||||
'iapSource: $iapSource, '
|
||||
'orderId: $orderId, '
|
||||
'productId: $productId, '
|
||||
'userId: $userId, '
|
||||
'purchaseDate: $purchaseDate, '
|
||||
'status: $status, '
|
||||
'expiryDate: $expiryDate, '
|
||||
'type: $type '
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
class IapRepository {
|
||||
final FirestoreApi api;
|
||||
final String projectId;
|
||||
|
||||
IapRepository(this.api, this.projectId);
|
||||
|
||||
Future<void> createOrUpdatePurchase(Purchase purchaseData) async {
|
||||
print('Updating $purchaseData');
|
||||
final purchaseId = _purchaseId(purchaseData);
|
||||
await api.projects.databases.documents.commit(
|
||||
CommitRequest(
|
||||
writes: [
|
||||
Write(
|
||||
update: Document(
|
||||
fields: purchaseData.toDocument(),
|
||||
name:
|
||||
'projects/$projectId/databases/(default)/documents/purchases/$purchaseId',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
'projects/$projectId/databases/(default)',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updatePurchase(Purchase purchaseData) async {
|
||||
print('Updating $purchaseData');
|
||||
final purchaseId = _purchaseId(purchaseData);
|
||||
await api.projects.databases.documents.commit(
|
||||
CommitRequest(
|
||||
writes: [
|
||||
Write(
|
||||
update: Document(
|
||||
fields: purchaseData.updateDocument(),
|
||||
name:
|
||||
'projects/$projectId/databases/(default)/documents/purchases/$purchaseId',
|
||||
),
|
||||
updateMask: DocumentMask(fieldPaths: ['status']),
|
||||
),
|
||||
],
|
||||
),
|
||||
'projects/$projectId/databases/(default)',
|
||||
);
|
||||
}
|
||||
|
||||
String _purchaseId(Purchase purchaseData) {
|
||||
return '${purchaseData.iapSource.name}_${purchaseData.orderId}';
|
||||
}
|
||||
}
|
||||
224
mnemo_cards_backend/lib/repository/pack_repository.dart
Normal file
224
mnemo_cards_backend/lib/repository/pack_repository.dart
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/repository/сonverters/export.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'
|
||||
hide VoiceModel;
|
||||
|
||||
/// Repository для работы с паками
|
||||
/// Работает с доменными моделями (CardPackModel), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class PackRepository {
|
||||
final AppDatabase _db;
|
||||
final CardPackConverter _cardPackConverter;
|
||||
final GameCardConverter _gameCardConverter;
|
||||
|
||||
PackRepository(this._db, this._cardPackConverter, this._gameCardConverter);
|
||||
|
||||
/// Получить пак по ID
|
||||
/// [includeCards] - загружать ли связанные карточки
|
||||
/// [includePreviewCards] - загружать ли превью карточки
|
||||
Future<CardPackModel?> getPackById(
|
||||
String id, {
|
||||
bool includeCards = false,
|
||||
bool includePreviewCards = false,
|
||||
}) async {
|
||||
final pack = await _db.packDao.getPackById(id);
|
||||
if (pack == null) return null;
|
||||
|
||||
final packModel = await _cardPackConverter.toModel(pack);
|
||||
|
||||
if (includeCards) {
|
||||
final cards = await _db.packDao.getPackCards(id);
|
||||
packModel.cards.addAll(cards.map((c) => _gameCardConverter.toModel(c)));
|
||||
}
|
||||
|
||||
if (includePreviewCards) {
|
||||
final previewCards = await _db.packDao.getPreviewCards(id);
|
||||
packModel.previewCards.addAll(
|
||||
previewCards.map((c) => _gameCardConverter.toModel(c)),
|
||||
);
|
||||
}
|
||||
|
||||
return packModel;
|
||||
}
|
||||
|
||||
/// Получить все паки
|
||||
Future<List<CardPackModel>> getAllPacks({
|
||||
bool enabledOnly = false,
|
||||
String? orderByField,
|
||||
bool orderDesc = false,
|
||||
bool includeCards = false,
|
||||
bool includePreviewCards = false,
|
||||
}) async {
|
||||
final packs = await _db.packDao.getAllPacks(
|
||||
enabledOnly: enabledOnly,
|
||||
orderByField: orderByField,
|
||||
orderDesc: orderDesc,
|
||||
);
|
||||
|
||||
if (includeCards || includePreviewCards) {
|
||||
final packModels = <CardPackModel>[];
|
||||
for (final pack in packs) {
|
||||
final packModel = await _cardPackConverter.toModel(pack);
|
||||
|
||||
if (includeCards) {
|
||||
final cards = await _db.packDao.getPackCards(pack.id);
|
||||
packModel.cards.addAll(
|
||||
cards.map((c) => _gameCardConverter.toModel(c)),
|
||||
);
|
||||
}
|
||||
|
||||
if (includePreviewCards) {
|
||||
final previewCards = await _db.packDao.getPreviewCards(pack.id);
|
||||
packModel.previewCards.addAll(
|
||||
previewCards.map((c) => _gameCardConverter.toModel(c)),
|
||||
);
|
||||
}
|
||||
|
||||
packModels.add(packModel);
|
||||
}
|
||||
return packModels;
|
||||
}
|
||||
|
||||
return await Future.wait(packs.map((p) => _cardPackConverter.toModel(p)));
|
||||
}
|
||||
|
||||
/// Создать пак
|
||||
Future<String> createPack(CardPackModel packModel) async {
|
||||
final companion = CardPacksCompanion.insert(
|
||||
title: packModel.title,
|
||||
subtitle: packModel.subtitle,
|
||||
size: packModel.size,
|
||||
color: Value(packModel.color),
|
||||
version: Value(packModel.version),
|
||||
cover: Value(packModel.cover),
|
||||
description: Value(packModel.description),
|
||||
googlePlayId: Value(packModel.googlePlayId),
|
||||
rustoreId: Value(packModel.rustoreId),
|
||||
appStoreId: Value(packModel.appStoreId),
|
||||
price: Value(packModel.price),
|
||||
currency: packModel.currency != null
|
||||
? Value(packModel.currency!)
|
||||
: const Value.absent(),
|
||||
enabled: Value(packModel.enabled),
|
||||
order: Value(packModel.order),
|
||||
cardsOrder: Value(packModel.cardsOrder),
|
||||
);
|
||||
|
||||
return await _db.packDao.createPack(companion);
|
||||
}
|
||||
|
||||
/// Обновить пак частично
|
||||
Future<void> updatePackPartial(CardPacksCompanion updates) async {
|
||||
await _db.packDao.updatePackPartial(updates);
|
||||
}
|
||||
|
||||
/// Удалить пак (soft delete)
|
||||
Future<void> softDeletePack(String packId) async {
|
||||
await _db.packDao.softDeletePack(packId);
|
||||
}
|
||||
|
||||
/// Получить карточки пака
|
||||
Future<List<GameCardModel>> getPackCards(String packId) async {
|
||||
final cards = await _db.packDao.getPackCards(packId);
|
||||
return cards.map((c) => _gameCardConverter.toModel(c)).toList();
|
||||
}
|
||||
|
||||
/// Получить превью карточки пака
|
||||
Future<List<GameCardModel>> getPreviewCards(String packId) async {
|
||||
final cards = await _db.packDao.getPreviewCards(packId);
|
||||
return cards.map((c) => _gameCardConverter.toModel(c)).toList();
|
||||
}
|
||||
|
||||
/// Получить карточку по ID
|
||||
Future<GameCardModel?> getCardById(String id) async {
|
||||
final card = await _db.packDao.getCardById(id);
|
||||
return card != null ? _gameCardConverter.toModel(card) : null;
|
||||
}
|
||||
|
||||
/// Добавить карточку в пак
|
||||
Future<void> addCardToPack({
|
||||
required String packId,
|
||||
required String cardId,
|
||||
int order = 0,
|
||||
}) async {
|
||||
await _db.packDao.addCardToPack(
|
||||
packId: packId,
|
||||
cardId: cardId,
|
||||
order: order,
|
||||
);
|
||||
}
|
||||
|
||||
/// Удалить карточку из пака
|
||||
Future<void> removeCardFromPack(String packId, String cardId) async {
|
||||
await _db.packDao.removeCardFromPack(packId, cardId);
|
||||
}
|
||||
|
||||
/// Обновить порядок карточек в паке
|
||||
Future<void> updatePackCardsOrder(String packId, List<String> cardIds) async {
|
||||
await _db.packDao.updatePackCardsOrder(packId, cardIds);
|
||||
}
|
||||
|
||||
/// Установить preview карточки для пака
|
||||
Future<void> setPreviewCards(String packId, List<String> cardIds) async {
|
||||
await _db.packDao.setPreviewCards(packId, cardIds);
|
||||
}
|
||||
|
||||
// ==================== VoiceModels ====================
|
||||
|
||||
/// Получить голосовые модели карточки
|
||||
Future<List<VoiceModel>> getCardVoices(String cardId) async {
|
||||
return await _db.packDao.getCardVoices(cardId);
|
||||
}
|
||||
|
||||
/// Получить голосовую модель по ID
|
||||
Future<VoiceModel?> getVoiceById(String id) async {
|
||||
return await _db.packDao.getVoiceById(id);
|
||||
}
|
||||
|
||||
/// Обновить путь/URL аудиофайла голосовой модели
|
||||
Future<void> updateVoiceUrl(String voiceId, String voiceUrl) async {
|
||||
await _db.packDao.updateVoiceUrl(voiceId, voiceUrl);
|
||||
}
|
||||
|
||||
// ==================== Cards (дополнительные методы) ====================
|
||||
|
||||
/// Создать карточку
|
||||
Future<String> createCard(GameCardsCompanion companion) async {
|
||||
return await _db.packDao.createCard(companion);
|
||||
}
|
||||
|
||||
/// Обновить карточку
|
||||
Future<bool> updateCard(GameCard card) async {
|
||||
return await _db.packDao.updateCard(card);
|
||||
}
|
||||
|
||||
/// Получить все карточки
|
||||
Future<List<GameCardModel>> getAllCards({int? limit, int? offset}) async {
|
||||
final cards = await _db.packDao.getAllCards(limit: limit, offset: offset);
|
||||
return cards.map((c) => _gameCardConverter.toModel(c)).toList();
|
||||
}
|
||||
|
||||
/// Получить паки для карточки
|
||||
Future<List<CardPackModel>> getPacksForCard(String cardId) async {
|
||||
final packs = await _db.packDao.getPacksForCard(cardId);
|
||||
return await Future.wait(packs.map((p) => _cardPackConverter.toModel(p)));
|
||||
}
|
||||
|
||||
/// Поиск карточек по оригинальному тексту
|
||||
Future<List<GameCardModel>> searchCardsByOriginal(String original) async {
|
||||
final cards = await _db.packDao.searchCardsByOriginal(original);
|
||||
return cards.map((c) => _gameCardConverter.toModel(c)).toList();
|
||||
}
|
||||
|
||||
/// Подсчитать карточки
|
||||
Future<int> countCards() async {
|
||||
return await _db.packDao.countCards();
|
||||
}
|
||||
|
||||
/// Подсчитать паки
|
||||
Future<int> countPacks({bool enabledOnly = false}) async {
|
||||
return await _db.packDao.countPacks(enabledOnly: enabledOnly);
|
||||
}
|
||||
}
|
||||
112
mnemo_cards_backend/lib/repository/payment_repository.dart
Normal file
112
mnemo_cards_backend/lib/repository/payment_repository.dart
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/api/purchase/payment_drift_extension.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
|
||||
/// Repository для работы с платежами
|
||||
/// Работает с доменными моделями (PaymentDto), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class PaymentRepository {
|
||||
final AppDatabase _db;
|
||||
|
||||
PaymentRepository(this._db);
|
||||
|
||||
/// Создать платеж
|
||||
Future<PaymentDto> createPayment(PaymentDto paymentDto, String userId) async {
|
||||
final companion = paymentDto.toCompanion(userId);
|
||||
final paymentId = await _db.paymentDao.createPayment(companion);
|
||||
final payment = await _db.paymentDao.getPaymentById(paymentId);
|
||||
if (payment == null) {
|
||||
throw Exception('Failed to create payment');
|
||||
}
|
||||
return payment.toDto();
|
||||
}
|
||||
|
||||
/// Получить платеж по ID
|
||||
Future<PaymentDto?> getPaymentById(String id) async {
|
||||
final payment = await _db.paymentDao.getPaymentById(id);
|
||||
return payment?.toDto();
|
||||
}
|
||||
|
||||
/// Получить платеж по externalToken
|
||||
Future<PaymentDto?> getPaymentByExternalToken(String token) async {
|
||||
final payment = await _db.paymentDao.getPaymentByExternalToken(token);
|
||||
return payment?.toDto();
|
||||
}
|
||||
|
||||
/// Получить последний платеж для пользователя и пака
|
||||
Future<PaymentDto?> getLatestPaymentForUserAndPack({
|
||||
required String userId,
|
||||
required String packId,
|
||||
}) async {
|
||||
final payment = await _db.paymentDao.getLatestPaymentForUserAndPack(
|
||||
userId: userId,
|
||||
packId: packId,
|
||||
);
|
||||
return payment?.toDto();
|
||||
}
|
||||
|
||||
/// Получить платежи пользователя
|
||||
Future<List<PaymentDto>> getPaymentsByUserId(
|
||||
String userId, {
|
||||
int? limit,
|
||||
int? offset,
|
||||
}) async {
|
||||
final payments = await _db.paymentDao.getPaymentsByUserId(
|
||||
userId,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
);
|
||||
return payments.map((p) => p.toDto()).toList();
|
||||
}
|
||||
|
||||
/// Получить платежи по продукту
|
||||
Future<List<PaymentDto>> getPaymentsByProduct(String productId) async {
|
||||
final payments = await _db.paymentDao.getPaymentsByProduct(productId);
|
||||
return payments.map((p) => p.toDto()).toList();
|
||||
}
|
||||
|
||||
/// Получить платежи по статусу
|
||||
Future<List<PaymentDto>> getPaymentsByStatus(String status) async {
|
||||
final payments = await _db.paymentDao.getPaymentsByStatus(status);
|
||||
return payments.map((p) => p.toDto()).toList();
|
||||
}
|
||||
|
||||
/// Получить все платежи (для админки)
|
||||
Future<List<PaymentDto>> getAllPayments({int? limit, int? offset}) async {
|
||||
final payments = await _db.paymentDao.getAllPayments(
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
);
|
||||
return payments.map((p) => p.toDto()).toList();
|
||||
}
|
||||
|
||||
/// Обновить платеж
|
||||
Future<void> updatePayment(String paymentId, PaymentDto paymentDto) async {
|
||||
final companion = paymentDto.toUpdateCompanion();
|
||||
await _db.paymentDao.updatePaymentCompanion(paymentId, companion);
|
||||
}
|
||||
|
||||
/// Обновить статус платежа
|
||||
Future<void> updatePaymentStatus(String externalToken, String status) async {
|
||||
await _db.paymentDao.updatePaymentStatus(externalToken, status);
|
||||
}
|
||||
|
||||
/// Обновить платеж частично
|
||||
Future<void> updatePaymentCompanion(
|
||||
String paymentId,
|
||||
PaymentsCompanion companion,
|
||||
) async {
|
||||
await _db.paymentDao.updatePaymentCompanion(paymentId, companion);
|
||||
}
|
||||
|
||||
/// Подсчитать все платежи
|
||||
Future<int> countPayments() async {
|
||||
return await _db.paymentDao.countAllPayments();
|
||||
}
|
||||
|
||||
/// Подсчитать платежи пользователя
|
||||
Future<int> countPaymentsByUserId(String userId) async {
|
||||
return await _db.paymentDao.countPaymentsByUserId(userId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
||||
/// Repository для работы с промокодами
|
||||
/// Работает с Drift моделями (PromoCodesCampaign, PromoCode), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class PromoCodeRepository {
|
||||
final AppDatabase _db;
|
||||
|
||||
PromoCodeRepository(this._db);
|
||||
|
||||
// ==================== PromoCodesCampaigns ====================
|
||||
|
||||
/// Получить кампанию по ID
|
||||
Future<PromoCodesCampaign?> getCampaignById(String id) async {
|
||||
return await _db.promoCodeDao.getCampaignById(id);
|
||||
}
|
||||
|
||||
/// Получить все активные кампании
|
||||
Future<List<PromoCodesCampaign>> getActiveCampaigns() async {
|
||||
return await _db.promoCodeDao.getActiveCampaigns();
|
||||
}
|
||||
|
||||
/// Получить кампании по статусам
|
||||
Future<List<PromoCodesCampaign>> getCampaignsByStatuses(
|
||||
List<String> statuses,
|
||||
) async {
|
||||
return await _db.promoCodeDao.getCampaignsByStatuses(statuses);
|
||||
}
|
||||
|
||||
/// Создать кампанию
|
||||
Future<String> createCampaign(PromoCodesCampaignsCompanion campaign) async {
|
||||
return await _db.promoCodeDao.createCampaign(campaign);
|
||||
}
|
||||
|
||||
/// Обновить кампанию
|
||||
Future<bool> updateCampaign(PromoCodesCampaign campaign) async {
|
||||
return await _db.promoCodeDao.updateCampaign(campaign);
|
||||
}
|
||||
|
||||
/// Обновить статус кампании
|
||||
Future<void> updateCampaignStatus(String campaignId, String status) async {
|
||||
await _db.promoCodeDao.updateCampaignStatus(campaignId, status);
|
||||
}
|
||||
|
||||
/// Удалить кампанию (soft delete)
|
||||
Future<void> softDeleteCampaign(String campaignId) async {
|
||||
await _db.promoCodeDao.softDeleteCampaign(campaignId);
|
||||
}
|
||||
|
||||
// ==================== PromoCodes ====================
|
||||
|
||||
/// Получить промокод по коду
|
||||
Future<PromoCode?> getPromoCodeByCode(String code) async {
|
||||
return await _db.promoCodeDao.getPromoCodeByCode(code);
|
||||
}
|
||||
|
||||
/// Получить промокоды кампании
|
||||
Future<List<PromoCode>> getPromoCodesByCampaignId(String campaignId) async {
|
||||
return await _db.promoCodeDao.getPromoCodesByCampaignId(campaignId);
|
||||
}
|
||||
|
||||
/// Получить промокоды пользователя
|
||||
Future<List<PromoCode>> getUserPromoCodes(String userId) async {
|
||||
return await _db.promoCodeDao.getUserPromoCodes(userId);
|
||||
}
|
||||
|
||||
/// Создать промокод
|
||||
Future<String> createPromoCode(PromoCodesCompanion promoCode) async {
|
||||
return await _db.promoCodeDao.createPromoCode(promoCode);
|
||||
}
|
||||
|
||||
/// Создать несколько промокодов
|
||||
Future<List<String>> createPromoCodes(
|
||||
List<PromoCodesCompanion> promoCodes,
|
||||
) async {
|
||||
return await _db.promoCodeDao.createPromoCodes(promoCodes);
|
||||
}
|
||||
|
||||
/// Обновить промокод
|
||||
Future<bool> updatePromoCode(PromoCode promoCode) async {
|
||||
return await _db.promoCodeDao.updatePromoCode(promoCode);
|
||||
}
|
||||
|
||||
/// Увеличить счетчик активаций
|
||||
Future<void> incrementActivations(String promoCodeId) async {
|
||||
await _db.promoCodeDao.incrementActivations(promoCodeId);
|
||||
}
|
||||
|
||||
/// Удалить промокод (soft delete)
|
||||
Future<void> softDeletePromoCode(String promoCodeId) async {
|
||||
await _db.promoCodeDao.softDeletePromoCode(promoCodeId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
||||
/// Repository для работы со статистикой сессий
|
||||
/// Работает с Drift моделями (StudySession), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class StatisticsRepository {
|
||||
final AppDatabase _db;
|
||||
|
||||
StatisticsRepository(this._db);
|
||||
|
||||
/// Получить сессию по ID
|
||||
Future<StudySession?> getSessionById(String id) async {
|
||||
return await _db.statisticsDao.getSessionById(id);
|
||||
}
|
||||
|
||||
/// Получить сессию по sessionId
|
||||
Future<StudySession?> getSessionBySessionId(String sessionId) async {
|
||||
return await _db.statisticsDao.getSessionBySessionId(sessionId);
|
||||
}
|
||||
|
||||
/// Получить активные сессии пользователя
|
||||
Future<List<StudySession>> getActiveSessions(String userId) async {
|
||||
return await _db.statisticsDao.getActiveSessions(userId);
|
||||
}
|
||||
|
||||
/// Получить сессии пользователя
|
||||
Future<List<StudySession>> getSessionsByUserId(
|
||||
String userId, {
|
||||
int? limit,
|
||||
int? offset,
|
||||
DateTime? fromDate,
|
||||
DateTime? toDate,
|
||||
}) async {
|
||||
return await _db.statisticsDao.getSessionsByUserId(
|
||||
userId,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
fromDate: fromDate,
|
||||
toDate: toDate,
|
||||
);
|
||||
}
|
||||
|
||||
/// Создать сессию
|
||||
Future<String> createSession(StudySessionsCompanion session) async {
|
||||
return await _db.statisticsDao.createSession(session);
|
||||
}
|
||||
|
||||
/// Обновить сессию
|
||||
Future<bool> updateSession(StudySession session) async {
|
||||
return await _db.statisticsDao.updateSession(session);
|
||||
}
|
||||
|
||||
/// Завершить сессию
|
||||
Future<void> endSession(
|
||||
String sessionId, {
|
||||
int? wordsLearned,
|
||||
int? testsCompleted,
|
||||
double? accuracy,
|
||||
}) async {
|
||||
await _db.statisticsDao.endSession(
|
||||
sessionId,
|
||||
wordsLearned: wordsLearned,
|
||||
testsCompleted: testsCompleted,
|
||||
accuracy: accuracy,
|
||||
);
|
||||
}
|
||||
|
||||
/// Подсчитать сессии пользователя
|
||||
Future<int> countSessionsByUserId(String userId) async {
|
||||
return await _db.statisticsDao.countSessionsByUserId(userId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/repository/сonverters/subscription_converter.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
||||
/// Repository для работы с подписками
|
||||
/// Работает с доменными моделями (SubscriptionPlanModel, UserSubscriptionModel), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class SubscriptionRepository {
|
||||
final AppDatabase _db;
|
||||
final SubscriptionConverter _subscriptionConverter;
|
||||
|
||||
SubscriptionRepository(this._db, this._subscriptionConverter);
|
||||
|
||||
// ==================== SubscriptionPlans ====================
|
||||
|
||||
/// Получить план подписки по ID
|
||||
Future<SubscriptionPlanModel?> getPlanById(String id) async {
|
||||
final plan = await _db.subscriptionDao.getPlanById(id);
|
||||
if (plan == null) return null;
|
||||
return _subscriptionConverter.planToModel(plan);
|
||||
}
|
||||
|
||||
/// Получить все планы подписки
|
||||
Future<List<SubscriptionPlanModel>> getAllPlans() async {
|
||||
final plans = await _db.subscriptionDao.getAllPlans();
|
||||
return plans.map((p) => _subscriptionConverter.planToModel(p)).toList();
|
||||
}
|
||||
|
||||
/// Создать план подписки
|
||||
Future<String> createPlan(SubscriptionPlansCompanion companion) async {
|
||||
return await _db.subscriptionDao.createPlan(companion);
|
||||
}
|
||||
|
||||
/// Обновить план подписки
|
||||
Future<bool> updatePlan(SubscriptionPlan plan) async {
|
||||
return await _db.subscriptionDao.updatePlan(plan);
|
||||
}
|
||||
|
||||
// ==================== UserSubscriptions ====================
|
||||
|
||||
/// Получить активную подписку пользователя
|
||||
Future<UserSubscriptionModel?> getActiveSubscription(String userId) async {
|
||||
final subscription = await _db.subscriptionDao.getActiveUserSubscription(
|
||||
userId,
|
||||
);
|
||||
if (subscription == null) return null;
|
||||
return _subscriptionConverter.toModel(subscription);
|
||||
}
|
||||
|
||||
/// Получить подписку пользователя (последнюю)
|
||||
Future<UserSubscriptionModel?> getUserSubscription(String userId) async {
|
||||
final subscription = await _db.subscriptionDao.getUserSubscription(userId);
|
||||
if (subscription == null) return null;
|
||||
return _subscriptionConverter.toModel(subscription);
|
||||
}
|
||||
|
||||
/// Проверить, есть ли у пользователя активная подписка
|
||||
Future<bool> hasActiveSubscription(String userId) async {
|
||||
return await _db.subscriptionDao.hasActiveSubscription(userId);
|
||||
}
|
||||
|
||||
/// Создать подписку пользователя
|
||||
Future<String> createSubscription(
|
||||
UserSubscriptionsCompanion companion,
|
||||
) async {
|
||||
return await _db.subscriptionDao.createUserSubscription(companion);
|
||||
}
|
||||
|
||||
/// Обновить подписку пользователя
|
||||
Future<bool> updateSubscription(UserSubscription subscription) async {
|
||||
return await _db.subscriptionDao.updateUserSubscription(subscription);
|
||||
}
|
||||
|
||||
/// Отменить подписку пользователя
|
||||
Future<void> cancelSubscription(String userId) async {
|
||||
await _db.subscriptionDao.cancelUserSubscription(userId);
|
||||
}
|
||||
|
||||
/// Получить всех пользователей с активными подписками
|
||||
Future<List<UserSubscriptionModel>> getActiveSubscriptions() async {
|
||||
final subscriptions = await _db.subscriptionDao.getActiveSubscriptions();
|
||||
return subscriptions.map((s) => _subscriptionConverter.toModel(s)).toList();
|
||||
}
|
||||
}
|
||||
127
mnemo_cards_backend/lib/repository/task_repository.dart
Normal file
127
mnemo_cards_backend/lib/repository/task_repository.dart
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
||||
/// Repository для работы с задачами
|
||||
/// Работает с Drift моделями (Task, UserTask), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class TaskRepository {
|
||||
final AppDatabase _db;
|
||||
|
||||
TaskRepository(this._db);
|
||||
|
||||
// ==================== Tasks ====================
|
||||
|
||||
/// Получить задачу по ID
|
||||
Future<Task?> getTaskById(String id) async {
|
||||
return await _db.taskDao.getTaskById(id);
|
||||
}
|
||||
|
||||
/// Получить все задачи
|
||||
Future<List<Task>> getAllTasks() async {
|
||||
return await _db.taskDao.getAllTasks();
|
||||
}
|
||||
|
||||
/// Создать задачу
|
||||
Future<String> createTask(TasksCompanion task) async {
|
||||
return await _db.taskDao.createTask(task);
|
||||
}
|
||||
|
||||
/// Обновить задачу
|
||||
Future<bool> updateTask(Task task) async {
|
||||
return await _db.taskDao.updateTask(task);
|
||||
}
|
||||
|
||||
/// Обновить время последнего выполнения
|
||||
Future<void> updateLastExecution(String taskId) async {
|
||||
await _db.taskDao.updateLastExecution(taskId);
|
||||
}
|
||||
|
||||
// ==================== UserTasks ====================
|
||||
|
||||
/// Получить задачу пользователя по ID
|
||||
Future<UserTask?> getUserTaskById(String id) async {
|
||||
return await _db.taskDao.getUserTaskById(id);
|
||||
}
|
||||
|
||||
/// Получить все задачи пользователей
|
||||
Future<List<UserTask>> getAllUserTasks() async {
|
||||
return await _db.taskDao.getAllUserTasks();
|
||||
}
|
||||
|
||||
/// Получить задачи пользователя
|
||||
Future<List<UserTask>> getUserTasks(
|
||||
String userId, {
|
||||
String? status,
|
||||
bool activeOnly = false,
|
||||
}) async {
|
||||
return await _db.taskDao.getUserTasks(
|
||||
userId,
|
||||
status: status,
|
||||
activeOnly: activeOnly,
|
||||
);
|
||||
}
|
||||
|
||||
/// Создать задачу пользователя
|
||||
Future<String> createUserTask(UserTasksCompanion task) async {
|
||||
return await _db.taskDao.createUserTask(task);
|
||||
}
|
||||
|
||||
/// Создать задачу пользователя или игнорировать, если уже существует
|
||||
Future<bool> createUserTaskOrIgnore(UserTasksCompanion task) async {
|
||||
return await _db.taskDao.createUserTaskOrIgnore(task);
|
||||
}
|
||||
|
||||
/// Обновить задачу пользователя
|
||||
Future<bool> updateUserTask(UserTask task) async {
|
||||
return await _db.taskDao.updateUserTask(task);
|
||||
}
|
||||
|
||||
/// Завершить задачу пользователя
|
||||
Future<void> completeUserTask(String taskId) async {
|
||||
await _db.taskDao.completeUserTask(taskId);
|
||||
}
|
||||
|
||||
/// Подсчитать задачи пользователя
|
||||
Future<int> countUserTasks(String userId, {String? status}) async {
|
||||
return await _db.taskDao.countUserTasks(userId, status: status);
|
||||
}
|
||||
|
||||
/// Удалить задачу пользователя
|
||||
Future<bool> deleteUserTask(String taskId) async {
|
||||
return await _db.taskDao.deleteUserTask(taskId);
|
||||
}
|
||||
|
||||
// ==================== UserTaskProgresses ====================
|
||||
|
||||
/// Получить прогресс задачи пользователя
|
||||
Future<UserTaskProgressesData?> getTaskProgress(
|
||||
String userId,
|
||||
String taskId,
|
||||
) async {
|
||||
return await _db.taskDao.getTaskProgress(userId, taskId);
|
||||
}
|
||||
|
||||
/// Создать прогресс задачи
|
||||
Future<String> createTaskProgress(
|
||||
UserTaskProgressesCompanion progress,
|
||||
) async {
|
||||
return await _db.taskDao.createTaskProgress(progress);
|
||||
}
|
||||
|
||||
/// Обновить прогресс задачи
|
||||
Future<bool> updateTaskProgress(UserTaskProgressesData progress) async {
|
||||
return await _db.taskDao.updateTaskProgress(progress);
|
||||
}
|
||||
|
||||
// ==================== UserTaskResults ====================
|
||||
|
||||
/// Получить результаты задач пользователя
|
||||
Future<List<UserTaskResult>> getTaskResults(String userId) async {
|
||||
return await _db.taskDao.getTaskResults(userId);
|
||||
}
|
||||
|
||||
/// Создать результат задачи
|
||||
Future<String> createTaskResult(UserTaskResultsCompanion result) async {
|
||||
return await _db.taskDao.createTaskResult(result);
|
||||
}
|
||||
}
|
||||
122
mnemo_cards_backend/lib/repository/test_repository.dart
Normal file
122
mnemo_cards_backend/lib/repository/test_repository.dart
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
||||
/// Repository для работы с тестами
|
||||
/// Работает с Drift моделями (Test, TestQuestion, TestStatistic), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class TestRepository {
|
||||
final AppDatabase _db;
|
||||
|
||||
TestRepository(this._db);
|
||||
|
||||
// ==================== Tests ====================
|
||||
|
||||
/// Получить тест по ID
|
||||
Future<Test?> getTestById(String id) async {
|
||||
return await _db.testDao.getTestById(id);
|
||||
}
|
||||
|
||||
/// Получить все тесты
|
||||
Future<List<Test>> getAllTests() async {
|
||||
return await _db.testDao.getAllTests();
|
||||
}
|
||||
|
||||
/// Получить тесты пака
|
||||
Future<List<Test>> getTestsByPackId(String packId) async {
|
||||
return await _db.testDao.getTestsByPackId(packId);
|
||||
}
|
||||
|
||||
/// Создать тест
|
||||
Future<String> createTest(TestsCompanion test) async {
|
||||
return await _db.testDao.createTest(test);
|
||||
}
|
||||
|
||||
/// Обновить тест
|
||||
Future<bool> updateTest(Test test) async {
|
||||
return await _db.testDao.updateTest(test);
|
||||
}
|
||||
|
||||
/// Удалить тест (soft delete)
|
||||
Future<void> softDeleteTest(String testId) async {
|
||||
await _db.testDao.softDeleteTest(testId);
|
||||
}
|
||||
|
||||
/// Hard delete старые сгенерированные тесты, которые были soft-deleted
|
||||
Future<int> hardDeleteOldSoftDeletedGeneratedTests({
|
||||
required Duration olderThan,
|
||||
}) async {
|
||||
return await _db.testDao.hardDeleteOldSoftDeletedGeneratedTests(
|
||||
olderThan: olderThan,
|
||||
);
|
||||
}
|
||||
|
||||
/// Hard delete сгенерированные тесты, которые не связаны ни с одним паком
|
||||
Future<int> hardDeleteOrphanGeneratedTests({
|
||||
required Duration olderThan,
|
||||
}) async {
|
||||
return await _db.testDao.hardDeleteOrphanGeneratedTests(
|
||||
olderThan: olderThan,
|
||||
);
|
||||
}
|
||||
|
||||
/// Связать тест с паком
|
||||
Future<void> linkTestToPack(String testId, String packId) async {
|
||||
await _db.testDao.linkTestToPack(testId, packId);
|
||||
}
|
||||
|
||||
/// Удалить связь теста с паком
|
||||
Future<void> unlinkTestFromPack(String testId, String packId) async {
|
||||
await _db.testDao.unlinkTestFromPack(testId, packId);
|
||||
}
|
||||
|
||||
/// Получить packId для теста
|
||||
Future<String?> getPackIdForTest(String testId) async {
|
||||
return await _db.testDao.getPackIdForTest(testId);
|
||||
}
|
||||
|
||||
/// Получить все packId для теста
|
||||
Future<List<String>> getPackIdsForTest(String testId) async {
|
||||
return await _db.testDao.getPackIdsForTest(testId);
|
||||
}
|
||||
|
||||
// ==================== TestQuestions ====================
|
||||
|
||||
/// Получить вопросы теста
|
||||
Future<List<TestQuestion>> getTestQuestions(String testId) async {
|
||||
return await _db.testDao.getTestQuestions(testId);
|
||||
}
|
||||
|
||||
/// Создать вопрос теста
|
||||
Future<String> createTestQuestion(TestQuestionsCompanion question) async {
|
||||
return await _db.testDao.createTestQuestion(question);
|
||||
}
|
||||
|
||||
/// Обновить вопрос теста
|
||||
Future<bool> updateTestQuestion(TestQuestion question) async {
|
||||
return await _db.testDao.updateTestQuestion(question);
|
||||
}
|
||||
|
||||
/// Удалить вопрос теста (soft delete)
|
||||
Future<void> softDeleteTestQuestion(String questionId) async {
|
||||
await _db.testDao.softDeleteTestQuestion(questionId);
|
||||
}
|
||||
|
||||
// ==================== TestStatistics ====================
|
||||
|
||||
/// Получить статистику теста пользователя
|
||||
Future<TestStatistic?> getTestStatistics(String userId, String testId) async {
|
||||
return await _db.testDao.getTestStatistics(userId, testId);
|
||||
}
|
||||
|
||||
/// Создать статистику теста
|
||||
Future<String> createTestStatistics(
|
||||
TestStatisticsCompanion statistics,
|
||||
) async {
|
||||
return await _db.testDao.createTestStatistics(statistics);
|
||||
}
|
||||
|
||||
/// Обновить статистику теста
|
||||
Future<bool> updateTestStatistics(TestStatistic statistics) async {
|
||||
return await _db.testDao.updateTestStatistics(statistics);
|
||||
}
|
||||
}
|
||||
217
mnemo_cards_backend/lib/repository/user_repository.dart
Normal file
217
mnemo_cards_backend/lib/repository/user_repository.dart
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/repository/сonverters/export.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
||||
/// Repository для работы с пользователями
|
||||
/// Работает с доменными моделями (UserModel), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class UserRepository {
|
||||
final AppDatabase _db;
|
||||
final UserConverter _userConverter;
|
||||
final CardPackConverter _cardPackConverter;
|
||||
final SubscriptionConverter _subscriptionConverter;
|
||||
|
||||
UserRepository(
|
||||
this._db,
|
||||
this._userConverter,
|
||||
this._cardPackConverter,
|
||||
this._subscriptionConverter,
|
||||
);
|
||||
|
||||
/// Получить пользователя по ID
|
||||
/// [includePacks] - загружать ли связанные паки пользователя
|
||||
/// [withData] - загружать ли UserData пользователя
|
||||
Future<UserModel?> getUserById(String id, {bool includePacks = false}) async {
|
||||
final user = await _db.userDao.getUserById(id);
|
||||
if (user == null) return null;
|
||||
|
||||
if (includePacks) {
|
||||
final packs = await _db.userDao.getUserPacks(id);
|
||||
return await _userConverter.toModel(user, packs);
|
||||
}
|
||||
|
||||
return await _userConverter.toModel(user);
|
||||
}
|
||||
|
||||
Future<(UserModel, UserData?)?> getUserWithDataById(
|
||||
String id, {
|
||||
bool includePacks = false,
|
||||
}) async {
|
||||
final userWithData = await _db.userDao.getUserWithDataById(id);
|
||||
if (userWithData == null) return null;
|
||||
|
||||
if (includePacks) {
|
||||
final packs = await _db.userDao.getUserPacks(id);
|
||||
final userModel = await _userConverter.toModel(userWithData.user, packs);
|
||||
return (userModel, userWithData.userData);
|
||||
}
|
||||
|
||||
final userModel = await _userConverter.toModel(userWithData.user);
|
||||
return (userModel, userWithData.userData);
|
||||
}
|
||||
|
||||
/// Получить пользователя по externalUserId
|
||||
Future<UserModel?> getUserByExternalId(String externalId) async {
|
||||
final user = await _db.userDao.getUserByExternalId(externalId);
|
||||
|
||||
if (user == null) return null;
|
||||
|
||||
return await _userConverter.toModel(user);
|
||||
}
|
||||
|
||||
/// Создать пользователя с UserData
|
||||
Future<String> createUserWithData({
|
||||
required UserModel userModel,
|
||||
required UserDatasCompanion userData,
|
||||
}) async {
|
||||
final userCompanion = _userConverter.toCompanion(userModel);
|
||||
return await _db.userDao.createUserWithData(
|
||||
user: userCompanion,
|
||||
userData: userData,
|
||||
);
|
||||
}
|
||||
|
||||
/// Обновить пользователя
|
||||
Future<void> updateUser(UserModel userModel) async {
|
||||
if (userModel.id == null) {
|
||||
throw ArgumentError('User ID is required');
|
||||
}
|
||||
|
||||
await _db.userDao.updateUserPartial(
|
||||
_userConverter
|
||||
.toCompanion(userModel)
|
||||
.copyWith(updatedAt: Value(PgDateTime(DateTime.now()))),
|
||||
);
|
||||
}
|
||||
|
||||
/// Обновить пользователя частично
|
||||
Future<void> updateUserPartial(UsersCompanion updates) async {
|
||||
await _db.userDao.updateUserPartial(updates);
|
||||
}
|
||||
|
||||
/// Удалить пользователя (soft delete)
|
||||
Future<void> softDeleteUser(String userId) async {
|
||||
await _db.userDao.softDeleteUser(userId);
|
||||
}
|
||||
|
||||
/// Получить всех пользователей (для админки)
|
||||
Future<List<UserModel>> getAllUsers({
|
||||
int? limit,
|
||||
int? offset,
|
||||
bool includeDeleted = false,
|
||||
bool includePacks = false,
|
||||
}) async {
|
||||
final users = await _db.userDao.getAllUsers(
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
includeDeleted: includeDeleted,
|
||||
);
|
||||
|
||||
if (includePacks) {
|
||||
final userModels = <UserModel>[];
|
||||
for (final user in users) {
|
||||
final packs = await _db.userDao.getUserPacks(user.id);
|
||||
final userModel = await _userConverter.toModel(user, packs);
|
||||
userModels.add(userModel);
|
||||
}
|
||||
return userModels;
|
||||
}
|
||||
|
||||
return await Future.wait(users.map((u) => _userConverter.toModel(u)));
|
||||
}
|
||||
|
||||
/// Подсчитать пользователей
|
||||
Future<int> countUsers({bool includeDeleted = false}) async {
|
||||
return await _db.userDao.countUsers(includeDeleted: includeDeleted);
|
||||
}
|
||||
|
||||
/// Получить паки пользователя
|
||||
Future<List<CardPackModel>> getUserPacks(String userId) async {
|
||||
final packs = await _db.userDao.getUserPacks(userId);
|
||||
return await Future.wait(packs.map((p) => _cardPackConverter.toModel(p)));
|
||||
}
|
||||
|
||||
/// Получить UserData пользователя
|
||||
Future<UserData?> getUserData(String userId) async {
|
||||
return await _db.userDao.getUserData(userId);
|
||||
}
|
||||
|
||||
/// Получить UserData пользователя по ID (алиас для getUserData)
|
||||
Future<UserData?> getUserDataById(String userId) async {
|
||||
return await getUserData(userId);
|
||||
}
|
||||
|
||||
/// Создать UserData
|
||||
Future<void> createUserData(UserDatasCompanion userData) async {
|
||||
await _db.userDao.createUserData(userData);
|
||||
}
|
||||
|
||||
/// Получить активную подписку пользователя
|
||||
Future<UserSubscriptionModel?> getUserSubscription(String userId) async {
|
||||
final subscription = await _db.subscriptionDao.getActiveSubscription(
|
||||
userId,
|
||||
);
|
||||
if (subscription == null) return null;
|
||||
return _subscriptionConverter.toModel(subscription);
|
||||
}
|
||||
|
||||
/// Обновить UserData частично
|
||||
Future<void> updateUserDataPartial(UserDatasCompanion updates) async {
|
||||
await _db.userDao.updateUserDataPartial(updates);
|
||||
}
|
||||
|
||||
// ==================== Tokens ====================
|
||||
|
||||
/// Получить токен по значению
|
||||
Future<Token?> getTokenByValue(String tokenValue) async {
|
||||
return await _db.userDao.getTokenByValue(tokenValue);
|
||||
}
|
||||
|
||||
/// Получить токен пользователя
|
||||
Future<Token?> getTokenByUserId(String userId) async {
|
||||
return await _db.userDao.getTokenByUserId(userId);
|
||||
}
|
||||
|
||||
/// Создать токен
|
||||
Future<String> createToken(TokensCompanion token) async {
|
||||
return await _db.userDao.createToken(token);
|
||||
}
|
||||
|
||||
/// Удалить токен (hard delete)
|
||||
Future<void> deleteToken(String token) async {
|
||||
await _db.userDao.deleteToken(token);
|
||||
}
|
||||
|
||||
/// Удалить токен (soft delete)
|
||||
Future<int> softDeleteToken(String tokenId) async {
|
||||
return await _db.userDao.softDeleteToken(tokenId);
|
||||
}
|
||||
|
||||
// ==================== User Packs ====================
|
||||
|
||||
/// Проверить, есть ли у пользователя доступ к паку
|
||||
Future<bool> hasPackAccess(String userId, String packId) async {
|
||||
return await _db.userDao.hasPackAccess(userId, packId);
|
||||
}
|
||||
|
||||
/// Дать пользователю доступ к паку
|
||||
Future<void> grantPackAccess({
|
||||
required String userId,
|
||||
required String packId,
|
||||
String grantType = 'purchase',
|
||||
}) async {
|
||||
await _db.userDao.grantPackAccess(
|
||||
userId: userId,
|
||||
packId: packId,
|
||||
grantType: grantType,
|
||||
);
|
||||
}
|
||||
|
||||
/// Отозвать доступ к паку
|
||||
Future<void> revokePackAccess(String userId, String packId) async {
|
||||
await _db.userDao.revokePackAccess(userId, packId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
||||
/// Repository для работы со статистикой слов
|
||||
/// Работает с Drift моделями (WordStatistic), использует DAO для доступа к БД
|
||||
@lazySingleton
|
||||
class WordStatisticsRepository {
|
||||
final AppDatabase _db;
|
||||
|
||||
WordStatisticsRepository(this._db);
|
||||
|
||||
/// Получить статистику для пользователя и карточки
|
||||
Future<WordStatistic?> getByUserAndCard(String userId, String cardId) async {
|
||||
return await _db.wordStatisticsDao.getByUserAndCard(userId, cardId);
|
||||
}
|
||||
|
||||
/// Создать новую запись статистики
|
||||
Future<WordStatistic> create({
|
||||
required String userId,
|
||||
required String cardId,
|
||||
required int correctAnswers,
|
||||
required int incorrectAnswers,
|
||||
}) async {
|
||||
return await _db.wordStatisticsDao.create(
|
||||
userId: userId,
|
||||
cardId: cardId,
|
||||
correctAnswers: correctAnswers,
|
||||
incorrectAnswers: incorrectAnswers,
|
||||
);
|
||||
}
|
||||
|
||||
/// Обновить статистику
|
||||
Future<void> updateStatistics({
|
||||
required String id,
|
||||
required int correctAnswers,
|
||||
required int incorrectAnswers,
|
||||
required DateTime lastReviewed,
|
||||
}) async {
|
||||
await _db.wordStatisticsDao.updateStatistics(
|
||||
id: id,
|
||||
correctAnswers: correctAnswers,
|
||||
incorrectAnswers: incorrectAnswers,
|
||||
lastReviewed: lastReviewed,
|
||||
);
|
||||
}
|
||||
|
||||
/// Получить статистику по всем карточкам пака для пользователя
|
||||
Future<List<WordStatistic>> getPackStatistics(
|
||||
String userId,
|
||||
String packId,
|
||||
) async {
|
||||
return await _db.wordStatisticsDao.getPackStatistics(userId, packId);
|
||||
}
|
||||
|
||||
/// Получить все статистики пользователя
|
||||
Future<List<WordStatistic>> getUserStatistics(String userId) async {
|
||||
return await _db.wordStatisticsDao.getUserStatistics(userId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
||||
/// Конвертер для преобразования CardPack (Drift) в CardPackModel
|
||||
@lazySingleton
|
||||
class CardPackConverter {
|
||||
/// Конвертировать CardPack в CardPackModel
|
||||
Future<CardPackModel> toModel(CardPack pack) async {
|
||||
return CardPackModel(
|
||||
id: pack.id,
|
||||
title: pack.title,
|
||||
subtitle: pack.subtitle,
|
||||
size: pack.size,
|
||||
color: pack.color,
|
||||
version: pack.version,
|
||||
cover: pack.cover,
|
||||
description: pack.description,
|
||||
googlePlayId: pack.googlePlayId,
|
||||
rustoreId: pack.rustoreId,
|
||||
appStoreId: pack.appStoreId,
|
||||
price: pack.price,
|
||||
currency: pack.currency,
|
||||
enabled: pack.enabled,
|
||||
order: pack.order,
|
||||
cardsOrder: pack.cardsOrder,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export 'card_pack_converter.dart';
|
||||
export 'game_card_converter.dart';
|
||||
export 'subscription_converter.dart';
|
||||
export 'user_converter.dart';
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
||||
/// Конвертер для преобразования GameCard (Drift) в GameCardModel
|
||||
@lazySingleton
|
||||
class GameCardConverter {
|
||||
/// Конвертировать GameCard в GameCardModel
|
||||
GameCardModel toModel(GameCard card) {
|
||||
return GameCardModel(
|
||||
id: card.id,
|
||||
image: card.image,
|
||||
mnemo: card.mnemo ?? '',
|
||||
original: card.original,
|
||||
translation: card.translation,
|
||||
transcription: card.transcription,
|
||||
transcriptionMnemo: card.transcriptionMnemo,
|
||||
imageBack: card.imageBack,
|
||||
back: card.back,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
||||
/// Конвертер для преобразования UserSubscription (Drift) в UserSubscriptionModel
|
||||
@lazySingleton
|
||||
class SubscriptionConverter {
|
||||
/// Конвертировать UserSubscription в UserSubscriptionModel
|
||||
UserSubscriptionModel toModel(UserSubscription subscription) {
|
||||
return UserSubscriptionModel(
|
||||
id: subscription.id,
|
||||
start: subscription.start.toDateTime(),
|
||||
finish: subscription.finish.toDateTime(),
|
||||
features: subscription.features
|
||||
.map(
|
||||
(f) => SubscriptionFeatureEnum.values.firstWhere(
|
||||
(e) => e.name == f,
|
||||
orElse: () => SubscriptionFeatureEnum.unknown,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Конвертировать SubscriptionPlan в SubscriptionPlanModel
|
||||
SubscriptionPlanModel planToModel(SubscriptionPlan plan) {
|
||||
final uiMap = plan.ui;
|
||||
final ui = uiMap != null ? SubscriptionPlanUI.fromJson(uiMap) : null;
|
||||
|
||||
return SubscriptionPlanModel(
|
||||
id: plan.id,
|
||||
ui: ui,
|
||||
price: plan.price,
|
||||
currency: plan.currency,
|
||||
durationDays: plan.durationDays,
|
||||
features: [],
|
||||
paymentSystem: PaymentSystem.values.firstWhere(
|
||||
(ps) => ps.name == plan.paymentSystem,
|
||||
orElse: () => PaymentSystem.unknown,
|
||||
),
|
||||
paymentId: plan.paymentId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/daos/user_dao.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
||||
import 'card_pack_converter.dart';
|
||||
|
||||
/// Конвертер для преобразования User (Drift) в UserModel
|
||||
@lazySingleton
|
||||
class UserConverter {
|
||||
final CardPackConverter _cardPackConverter;
|
||||
|
||||
UserConverter(this._cardPackConverter);
|
||||
|
||||
/// Конвертировать User в UserModel
|
||||
/// [packs] - опциональный список связанных паков пользователя
|
||||
Future<UserModel> toModel(User user, [List<CardPack>? packs]) async {
|
||||
final packModels = packs != null
|
||||
? await Future.wait(packs.map((p) => _cardPackConverter.toModel(p)))
|
||||
: <CardPackModel>[];
|
||||
|
||||
return UserModel(
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
telegram: user.telegram,
|
||||
admin: user.admin,
|
||||
purchases: user.purchases,
|
||||
userSettings: user.userSettings,
|
||||
packs: packModels,
|
||||
);
|
||||
}
|
||||
|
||||
/// Конвертировать UserModel в UsersCompanion
|
||||
UsersCompanion toCompanion(UserModel userModel) {
|
||||
return UsersCompanion(
|
||||
id: userModel.id != null
|
||||
? drift.Value(userModel.id!)
|
||||
: const drift.Value.absent(),
|
||||
externalUserId: const drift.Value.absent(),
|
||||
name: drift.Value(userModel.name),
|
||||
email: drift.Value(userModel.email),
|
||||
telegram: drift.Value(userModel.telegram),
|
||||
admin: drift.Value(userModel.admin),
|
||||
purchases: drift.Value(userModel.purchases),
|
||||
userSettings: drift.Value(userModel.userSettings),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Конвертер для преобразования UserWithData в UserDataModel
|
||||
@lazySingleton
|
||||
class UserWithDataConverter {
|
||||
/// Конвертировать UserWithData в UserDataModel
|
||||
Future<UserDataModel> toUserDataModel(UserWithData userWithData) async {
|
||||
if (userWithData.userData == null) {
|
||||
return UserDataModel();
|
||||
}
|
||||
|
||||
return UserDataModel(
|
||||
id: userWithData.userData!.id,
|
||||
lastTestSessionToken: userWithData.userData!.lastTestSessionToken,
|
||||
lastTimeOnline: userWithData.userData!.lastTimeOnline?.toDateTime(),
|
||||
tags: userWithData.userData!.tags,
|
||||
totalStudyTimeMinutes: userWithData.userData!.totalStudyTimeMinutes,
|
||||
currentStreak: userWithData.userData!.currentStreak,
|
||||
longestStreak: userWithData.userData!.longestStreak,
|
||||
// words, packProgress, studyDates, categoryMinutes, achievements
|
||||
// are loaded separately from database when needed
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue