Files
UniVerse/backend/UniVerse.Infrastructure/Services/GamificationService.cs
T
serega404 811b6ef51a
Backend CI / build-and-test (push) Successful in 52s
Frontend CI / build-and-check (push) Failing after 5m15s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 16s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 1m0s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 32s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 13s
fix: перенёс уровни в бд и пофиксид их отображение на фронте
2026-05-18 02:28:05 +03:00

243 lines
9.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Globalization;
using UniVerse.Application.DTOs.Achievements;
using UniVerse.Application.DTOs.Common;
using UniVerse.Application.DTOs.Gamification;
using UniVerse.Application.DTOs.Notifications;
using UniVerse.Application.Interfaces;
using UniVerse.Application.Mappings;
using UniVerse.Domain.Entities;
using UniVerse.Domain.Enums;
using UniVerse.Infrastructure.Data;
namespace UniVerse.Infrastructure.Services;
public class GamificationService : IGamificationService
{
private readonly AppDbContext _db;
private readonly INotificationService _notifications;
private readonly ILogger<GamificationService> _logger;
private List<LevelThreshold>? _levelThresholds;
public GamificationService(
AppDbContext db,
INotificationService notifications,
ILogger<GamificationService> logger)
{
_db = db; _notifications = notifications; _logger = logger;
}
public async Task AwardCoinsAsync(int userId, int amount, CoinTransactionType type,
int? reviewId = null, int? achievementId = null, string? description = null)
{
var user = await _db.Users.FindAsync(userId);
if (user == null) return;
user.Coins += amount;
user.Xp += amount;
_db.CoinTransactions.Add(new CoinTransaction
{
UserId = userId, Amount = amount, Type = type,
ReviewId = reviewId, AchievementId = achievementId, Description = description
});
await _db.SaveChangesAsync();
_logger.LogInformation("Awarded {Amount} coins to user {UserId} ({Type})", amount, userId, type);
}
public async Task CheckAndAwardAchievementsAsync(int userId)
{
var user = await _db.Users
.Include(u => u.StudentProfile)
.FirstOrDefaultAsync(u => u.Id == userId);
if (user == null) return;
var achievements = await _db.Achievements.OrderBy(a => a.Id).ToListAsync();
var existing = await _db.UserAchievements.Where(ua => ua.UserId == userId)
.Select(ua => ua.AchievementId).ToListAsync();
var reviews = await _db.Reviews.CountAsync(r => r.UserId == userId);
var registered = await _db.LectureEnrollments.CountAsync(e => e.UserId == userId);
var attended = await _db.LectureEnrollments.CountAsync(e => e.UserId == userId && e.Attended);
var activeRegistrations = await _db.LectureEnrollments
.Include(e => e.Lecture)
.CountAsync(e => e.UserId == userId && e.Lecture.StartsAt > DateTime.UtcNow);
var earnedCoins = await _db.CoinTransactions
.Where(ct => ct.UserId == userId && ct.Amount > 0)
.SumAsync(ct => (int?)ct.Amount) ?? 0;
var attendanceStreakWeeks = await CalculateAttendanceStreakWeeksAsync(userId);
var profileCompleted = !string.IsNullOrWhiteSpace(user.DisplayName)
&& !string.IsNullOrWhiteSpace(user.AvatarUrl);
var firstActivity = registered > 0 || reviews > 0 || attended > 0;
foreach (var achievement in achievements.Where(a => !existing.Contains(a.Id)))
{
if (!TryParseCondition(achievement.Condition, out var type, out var value)) continue;
var earned = type switch
{
"lectures_attended" => attended >= value,
"reviews_written" => reviews >= value,
"lectures_registered" => registered >= value,
"active_registrations" => activeRegistrations >= value,
"attendance_streak_weeks" => attendanceStreakWeeks >= value,
"attended_registered" => attended >= value,
"coins_earned" => earnedCoins >= value,
"level_reached" => await CalculateLevelAsync(user.Xp) >= value,
"profile_completed" => profileCompleted && value <= 1,
"first_activity" => firstActivity && value <= 1,
_ => false
};
if (!earned) continue;
_db.UserAchievements.Add(new UserAchievement { UserId = userId, AchievementId = achievement.Id });
if (achievement.CoinReward > 0)
{
await AwardCoinsAsync(userId, achievement.CoinReward, CoinTransactionType.AchievementReward,
achievementId: achievement.Id, description: $"Achievement: {achievement.Name}");
earnedCoins += achievement.CoinReward;
}
await TryNotifyAchievementAsync(user, achievement);
}
await _db.SaveChangesAsync();
}
private async Task TryNotifyAchievementAsync(User user, Achievement achievement)
{
try
{
var title = $"Новое достижение: {achievement.Name}";
var rewardText = achievement.CoinReward > 0
? $" Награда: {achievement.CoinReward} монет."
: string.Empty;
var body = $"{achievement.Description ?? "Вы выполнили условие достижения."}{rewardText}";
await _notifications.CreateUserNotificationAsync(user.Id, "achievement", title, body);
await _notifications.SendAsync(new NotificationMessage(
NotificationChannels.Email,
user.Email,
title,
$"Здравствуйте, {user.DisplayName ?? user.Email}!\n\nПоздравляем: вы получили достижение «{achievement.Name}» в UniVerse.\n\n{body}",
user.DisplayName,
new Dictionary<string, string>
{
["event"] = "achievement_earned",
["achievement_id"] = achievement.Id.ToString(),
["achievement_name"] = achievement.Name
}));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to send achievement notification {AchievementId} to user {UserId}", achievement.Id, user.Id);
}
}
private static bool TryParseCondition(string? condition, out string type, out int value)
{
type = string.Empty;
value = 0;
var parts = condition?.Split(':', 2, StringSplitOptions.TrimEntries);
if (parts is not { Length: 2 } || string.IsNullOrWhiteSpace(parts[0])) return false;
if (!int.TryParse(parts[1], out value)) return false;
type = parts[0];
return value >= 0;
}
private async Task<int> CalculateAttendanceStreakWeeksAsync(int userId)
{
var lectureDates = await _db.LectureEnrollments
.Include(e => e.Lecture)
.Where(e => e.UserId == userId && e.Attended)
.Select(e => e.Lecture.StartsAt)
.ToListAsync();
var weekStarts = lectureDates
.Select(GetIsoWeekStart)
.Distinct()
.OrderBy(d => d)
.ToList();
var best = 0;
var current = 0;
DateOnly? previous = null;
foreach (var weekStart in weekStarts)
{
current = previous.HasValue && weekStart.DayNumber - previous.Value.DayNumber == 7
? current + 1
: 1;
best = Math.Max(best, current);
previous = weekStart;
}
return best;
}
private static DateOnly GetIsoWeekStart(DateTime date)
{
var isoYear = ISOWeek.GetYear(date);
var isoWeek = ISOWeek.GetWeekOfYear(date);
return DateOnly.FromDateTime(ISOWeek.ToDateTime(isoYear, isoWeek, DayOfWeek.Monday));
}
public async Task<int> CalculateLevelAsync(int xp)
{
var thresholds = await GetLevelThresholdsAsync();
return thresholds
.Where(t => xp >= t.RequiredXp)
.OrderBy(t => t.RequiredXp)
.ThenBy(t => t.Level)
.LastOrDefault()?.Level ?? thresholds[0].Level;
}
public async Task<LevelProgressDto> GetLevelProgressAsync(int xp)
{
var thresholds = await GetLevelThresholdsAsync();
var current = thresholds
.Where(t => xp >= t.RequiredXp)
.OrderBy(t => t.RequiredXp)
.ThenBy(t => t.Level)
.LastOrDefault() ?? thresholds[0];
var next = thresholds
.Where(t => t.RequiredXp > current.RequiredXp)
.OrderBy(t => t.RequiredXp)
.ThenBy(t => t.Level)
.FirstOrDefault();
return new LevelProgressDto(current.RequiredXp, next?.RequiredXp);
}
private async Task<List<LevelThreshold>> GetLevelThresholdsAsync()
{
if (_levelThresholds is { Count: > 0 }) return _levelThresholds;
_levelThresholds = await _db.LevelThresholds
.AsNoTracking()
.OrderBy(t => t.RequiredXp)
.ThenBy(t => t.Level)
.ToListAsync();
if (_levelThresholds.Count == 0)
_levelThresholds.Add(new LevelThreshold { Level = 1, RequiredXp = 0 });
return _levelThresholds;
}
public async Task<List<UserAchievementDto>> GetUserAchievementsAsync(int userId) =>
await _db.UserAchievements.Include(ua => ua.Achievement)
.Where(ua => ua.UserId == userId).OrderByDescending(ua => ua.AwardedAt)
.Select(ua => ua.ToDto()).ToListAsync();
public async Task<PagedResult<CoinTransactionDto>> GetTransactionsAsync(int userId, PaginationRequest pagination)
{
var query = _db.CoinTransactions.Where(ct => ct.UserId == userId);
var total = await query.CountAsync();
var items = await query.OrderByDescending(ct => ct.CreatedAt)
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize)
.Select(ct => ct.ToDto()).ToListAsync();
return PagedResult<CoinTransactionDto>.Create(items, total, pagination.Page, pagination.PageSize);
}
}