feat: добавил каталог достижений и автоначисление
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 10s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 2m53s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 28s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 7s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 10s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 2m53s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 28s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 7s
Реализовал автосоздание и обновление каталога достижений на бэке и синхронизацию на фронте.
This commit is contained in:
@@ -14,7 +14,7 @@ public class AchievementService : IAchievementService
|
||||
public AchievementService(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<List<AchievementDto>> GetAllAsync() =>
|
||||
await _db.Achievements.OrderBy(a => a.Name).Select(a => a.ToDto()).ToListAsync();
|
||||
await _db.Achievements.OrderBy(a => a.Id).Select(a => a.ToDto()).ToListAsync();
|
||||
|
||||
public async Task<AchievementDto> GetByIdAsync(int id)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Globalization;
|
||||
using UniVerse.Application.DTOs.Achievements;
|
||||
using UniVerse.Application.DTOs.Common;
|
||||
using UniVerse.Application.DTOs.Gamification;
|
||||
@@ -41,32 +42,109 @@ public class GamificationService : IGamificationService
|
||||
|
||||
public async Task CheckAndAwardAchievementsAsync(int userId)
|
||||
{
|
||||
var achievements = await _db.Achievements.ToListAsync();
|
||||
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)))
|
||||
{
|
||||
var earned = achievement.Condition switch
|
||||
if (!TryParseCondition(achievement.Condition, out var type, out var value)) continue;
|
||||
|
||||
var earned = type switch
|
||||
{
|
||||
"reviews_1" => reviews >= 1,
|
||||
"reviews_5" => reviews >= 5,
|
||||
"reviews_10" => reviews >= 10,
|
||||
"attended_5" => attended >= 5,
|
||||
"attended_10" => attended >= 10,
|
||||
"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" => CalculateLevel(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 _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
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 int CalculateLevel(int xp)
|
||||
{
|
||||
var thresholds = _config.GetSection("Gamification:XpThresholds").Get<int[]>()
|
||||
|
||||
@@ -12,7 +12,13 @@ namespace UniVerse.Infrastructure.Services;
|
||||
public class LectureService : ILectureService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public LectureService(AppDbContext db) => _db = db;
|
||||
private readonly IGamificationService _gamification;
|
||||
|
||||
public LectureService(AppDbContext db, IGamificationService gamification)
|
||||
{
|
||||
_db = db;
|
||||
_gamification = gamification;
|
||||
}
|
||||
|
||||
private IQueryable<Lecture> BaseQuery() => _db.Lectures
|
||||
.Include(l => l.Course).Include(l => l.Teacher)
|
||||
@@ -96,6 +102,7 @@ public class LectureService : ILectureService
|
||||
throw new ConflictException("Already enrolled.");
|
||||
_db.LectureEnrollments.Add(new LectureEnrollment { LectureId = lectureId, UserId = userId });
|
||||
await _db.SaveChangesAsync();
|
||||
await _gamification.CheckAndAwardAchievementsAsync(userId);
|
||||
}
|
||||
|
||||
public async Task UnenrollAsync(int lectureId, int userId)
|
||||
@@ -114,6 +121,8 @@ public class LectureService : ILectureService
|
||||
?? throw new NotFoundException("Enrollment not found.");
|
||||
enrollment.Attended = attended;
|
||||
await _db.SaveChangesAsync();
|
||||
if (attended)
|
||||
await _gamification.CheckAndAwardAchievementsAsync(userId);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<EnrollmentDto>> GetEnrollmentsAsync(int lectureId, PaginationRequest pagination)
|
||||
|
||||
@@ -13,7 +13,13 @@ namespace UniVerse.Infrastructure.Services;
|
||||
public class ReviewService : IReviewService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public ReviewService(AppDbContext db) => _db = db;
|
||||
private readonly IGamificationService _gamification;
|
||||
|
||||
public ReviewService(AppDbContext db, IGamificationService gamification)
|
||||
{
|
||||
_db = db;
|
||||
_gamification = gamification;
|
||||
}
|
||||
|
||||
private IQueryable<Review> BaseQuery() => _db.Reviews
|
||||
.Include(r => r.Lecture).Include(r => r.User);
|
||||
@@ -31,6 +37,7 @@ public class ReviewService : IReviewService
|
||||
};
|
||||
_db.Reviews.Add(review);
|
||||
await _db.SaveChangesAsync();
|
||||
await _gamification.CheckAndAwardAchievementsAsync(userId);
|
||||
var full = await BaseQuery().FirstAsync(r => r.Id == review.Id);
|
||||
return full.ToDto();
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ public class UserService : IUserService
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await _gamification.CheckAndAwardAchievementsAsync(id);
|
||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user