Добавил слой Infrastructure
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using UniVerse.Application.DTOs.Achievements;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Application.Mappings;
|
||||
using UniVerse.Domain.Entities;
|
||||
using UniVerse.Domain.Exceptions;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Infrastructure.Services;
|
||||
|
||||
public class AchievementService : IAchievementService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public AchievementService(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<List<AchievementDto>> GetAllAsync() =>
|
||||
await _db.Achievements.OrderBy(a => a.Name).Select(a => a.ToDto()).ToListAsync();
|
||||
|
||||
public async Task<AchievementDto> GetByIdAsync(int id)
|
||||
{
|
||||
var a = await _db.Achievements.FindAsync(id) ?? throw new NotFoundException("Achievement", id);
|
||||
return a.ToDto();
|
||||
}
|
||||
|
||||
public async Task<AchievementDto> CreateAsync(CreateAchievementRequest req)
|
||||
{
|
||||
var a = new Achievement
|
||||
{
|
||||
Name = req.Name, Description = req.Description, IconUrl = req.IconUrl,
|
||||
XpReward = req.XpReward, CoinReward = req.CoinReward, Condition = req.Condition
|
||||
};
|
||||
_db.Achievements.Add(a);
|
||||
await _db.SaveChangesAsync();
|
||||
return a.ToDto();
|
||||
}
|
||||
|
||||
public async Task<AchievementDto> UpdateAsync(int id, UpdateAchievementRequest req)
|
||||
{
|
||||
var a = await _db.Achievements.FindAsync(id) ?? throw new NotFoundException("Achievement", id);
|
||||
a.Name = req.Name; a.Description = req.Description; a.IconUrl = req.IconUrl;
|
||||
a.XpReward = req.XpReward; a.CoinReward = req.CoinReward; a.Condition = req.Condition;
|
||||
await _db.SaveChangesAsync();
|
||||
return a.ToDto();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var a = await _db.Achievements.FindAsync(id) ?? throw new NotFoundException("Achievement", id);
|
||||
_db.Achievements.Remove(a);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using UniVerse.Application.DTOs.Auth;
|
||||
using UniVerse.Application.DTOs.Users;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Application.Mappings;
|
||||
using UniVerse.Domain.Entities;
|
||||
using UniVerse.Domain.Enums;
|
||||
using UniVerse.Domain.Exceptions;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Infrastructure.Services;
|
||||
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IGamificationService _gamification;
|
||||
|
||||
public AuthService(AppDbContext db, IConfiguration config, IGamificationService gamification)
|
||||
{
|
||||
_db = db;
|
||||
_config = config;
|
||||
_gamification = gamification;
|
||||
}
|
||||
|
||||
public async Task<AuthResponse> LoginWithMicrosoftAsync(string authorizationCode)
|
||||
{
|
||||
// Stub: in production, exchange authorizationCode with Microsoft Entra ID
|
||||
// For now, create/find a demo user
|
||||
throw new NotImplementedException(
|
||||
"Microsoft Entra ID integration not yet configured. Use /api/v1/auth/login/dev in Development mode.");
|
||||
}
|
||||
|
||||
public async Task<AuthResponse> DevLoginAsync(string email, string? displayName, UserRole role)
|
||||
{
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
Email = email,
|
||||
DisplayName = displayName ?? email.Split('@')[0],
|
||||
Role = role,
|
||||
IsActive = true
|
||||
};
|
||||
_db.Users.Add(user);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// Create profile based on role
|
||||
if (role == UserRole.Student)
|
||||
{
|
||||
_db.StudentProfiles.Add(new StudentProfile { UserId = user.Id });
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
else if (role == UserRole.Teacher)
|
||||
{
|
||||
_db.TeacherProfiles.Add(new TeacherProfile { UserId = user.Id });
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
if (!user.IsActive)
|
||||
throw new ForbiddenException("Account is deactivated.");
|
||||
|
||||
var accessToken = GenerateAccessToken(user);
|
||||
var refreshToken = await GenerateRefreshTokenAsync(user.Id);
|
||||
|
||||
return new AuthResponse(
|
||||
accessToken,
|
||||
DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()),
|
||||
user.ToAuthDto()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<AuthResponse> RefreshTokenAsync(string refreshToken)
|
||||
{
|
||||
var token = await _db.RefreshTokens
|
||||
.Include(rt => rt.User)
|
||||
.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
||||
|
||||
if (token == null || !token.IsActive)
|
||||
throw new ForbiddenException("Invalid or expired refresh token.");
|
||||
|
||||
// Revoke old token
|
||||
token.RevokedAt = DateTime.UtcNow;
|
||||
|
||||
// Generate new tokens
|
||||
var accessToken = GenerateAccessToken(token.User);
|
||||
var newRefreshToken = await GenerateRefreshTokenAsync(token.UserId);
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return new AuthResponse(
|
||||
accessToken,
|
||||
DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()),
|
||||
token.User.ToAuthDto()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task RevokeRefreshTokenAsync(string refreshToken)
|
||||
{
|
||||
var token = await _db.RefreshTokens.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
||||
if (token != null && token.IsActive)
|
||||
{
|
||||
token.RevokedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UserDto> GetCurrentUserAsync(int userId)
|
||||
{
|
||||
var user = await _db.Users.FindAsync(userId)
|
||||
?? throw new NotFoundException("User", userId);
|
||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||
}
|
||||
|
||||
private string GenerateAccessToken(User user)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
||||
new Claim(ClaimTypes.Role, user.Role.ToString()),
|
||||
new Claim("display_name", user.DisplayName ?? "")
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _config["Jwt:Issuer"],
|
||||
audience: _config["Jwt:Audience"],
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()),
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
private async Task<string> GenerateRefreshTokenAsync(int userId)
|
||||
{
|
||||
var randomBytes = RandomNumberGenerator.GetBytes(64);
|
||||
var tokenString = Convert.ToBase64String(randomBytes);
|
||||
|
||||
var refreshToken = new RefreshToken
|
||||
{
|
||||
UserId = userId,
|
||||
Token = tokenString,
|
||||
ExpiresAt = DateTime.UtcNow.AddDays(GetRefreshTokenExpiration()),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_db.RefreshTokens.Add(refreshToken);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return tokenString;
|
||||
}
|
||||
|
||||
private int GetAccessTokenExpiration() =>
|
||||
int.Parse(_config["Jwt:AccessTokenExpirationMinutes"] ?? "30");
|
||||
|
||||
private int GetRefreshTokenExpiration() =>
|
||||
int.Parse(_config["Jwt:RefreshTokenExpirationDays"] ?? "30");
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using UniVerse.Application.DTOs.Common;
|
||||
using UniVerse.Application.DTOs.Courses;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Application.Mappings;
|
||||
using UniVerse.Domain.Entities;
|
||||
using UniVerse.Domain.Exceptions;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Infrastructure.Services;
|
||||
|
||||
public class CourseService : ICourseService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public CourseService(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<PagedResult<CourseDto>> GetAllAsync(CourseFilterRequest filter)
|
||||
{
|
||||
var query = _db.Courses.Include(c => c.CourseTags).ThenInclude(ct => ct.Tag).AsQueryable();
|
||||
if (!string.IsNullOrEmpty(filter.Search))
|
||||
query = query.Where(c => c.Name.ToLower().Contains(filter.Search.ToLower()));
|
||||
if (filter.IsSynced.HasValue)
|
||||
query = query.Where(c => c.IsSynced == filter.IsSynced.Value);
|
||||
if (filter.TagId.HasValue)
|
||||
query = query.Where(c => c.CourseTags.Any(ct => ct.TagId == filter.TagId.Value));
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var courses = await query.OrderByDescending(c => c.CreatedAt)
|
||||
.Skip((filter.Page - 1) * filter.PageSize).Take(filter.PageSize).ToListAsync();
|
||||
return PagedResult<CourseDto>.Create(courses.Select(c => c.ToDto()).ToList(), total, filter.Page, filter.PageSize);
|
||||
}
|
||||
|
||||
public async Task<CourseDto> GetByIdAsync(int id)
|
||||
{
|
||||
var course = await _db.Courses.Include(c => c.CourseTags).ThenInclude(ct => ct.Tag)
|
||||
.FirstOrDefaultAsync(c => c.Id == id) ?? throw new NotFoundException("Course", id);
|
||||
return course.ToDto();
|
||||
}
|
||||
|
||||
public async Task<CourseDto> CreateAsync(CreateCourseRequest request)
|
||||
{
|
||||
var course = new Course { Name = request.Name, Description = request.Description };
|
||||
_db.Courses.Add(course);
|
||||
await _db.SaveChangesAsync();
|
||||
return await GetByIdAsync(course.Id);
|
||||
}
|
||||
|
||||
public async Task<CourseDto> UpdateAsync(int id, UpdateCourseRequest request)
|
||||
{
|
||||
var course = await _db.Courses.FindAsync(id) ?? throw new NotFoundException("Course", id);
|
||||
course.Name = request.Name;
|
||||
course.Description = request.Description;
|
||||
course.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
return await GetByIdAsync(id);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var course = await _db.Courses.FindAsync(id) ?? throw new NotFoundException("Course", id);
|
||||
_db.Courses.Remove(course);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddTagAsync(int courseId, int tagId)
|
||||
{
|
||||
if (await _db.CourseTags.AnyAsync(ct => ct.CourseId == courseId && ct.TagId == tagId))
|
||||
throw new ConflictException("Tag already linked to course.");
|
||||
_ = await _db.Courses.FindAsync(courseId) ?? throw new NotFoundException("Course", courseId);
|
||||
_ = await _db.Tags.FindAsync(tagId) ?? throw new NotFoundException("Tag", tagId);
|
||||
_db.CourseTags.Add(new CourseTag { CourseId = courseId, TagId = tagId });
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task RemoveTagAsync(int courseId, int tagId)
|
||||
{
|
||||
var ct = await _db.CourseTags.FirstOrDefaultAsync(x => x.CourseId == courseId && x.TagId == tagId)
|
||||
?? throw new NotFoundException("CourseTag not found.");
|
||||
_db.CourseTags.Remove(ct);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using UniVerse.Application.DTOs.Achievements;
|
||||
using UniVerse.Application.DTOs.Common;
|
||||
using UniVerse.Application.DTOs.Gamification;
|
||||
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 IConfiguration _config;
|
||||
private readonly ILogger<GamificationService> _logger;
|
||||
|
||||
public GamificationService(AppDbContext db, IConfiguration config, ILogger<GamificationService> logger)
|
||||
{
|
||||
_db = db; _config = config; _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 achievements = await _db.Achievements.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 attended = await _db.LectureEnrollments.CountAsync(e => e.UserId == userId && e.Attended);
|
||||
|
||||
foreach (var achievement in achievements.Where(a => !existing.Contains(a.Id)))
|
||||
{
|
||||
var earned = achievement.Condition switch
|
||||
{
|
||||
"reviews_1" => reviews >= 1,
|
||||
"reviews_5" => reviews >= 5,
|
||||
"reviews_10" => reviews >= 10,
|
||||
"attended_5" => attended >= 5,
|
||||
"attended_10" => attended >= 10,
|
||||
_ => 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}");
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public int CalculateLevel(int xp)
|
||||
{
|
||||
var thresholds = _config.GetSection("Gamification:XpThresholds").Get<int[]>()
|
||||
?? [0, 100, 300, 600, 1000, 1500, 2500, 4000];
|
||||
for (int i = thresholds.Length - 1; i >= 0; i--)
|
||||
if (xp >= thresholds[i]) return i + 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using UniVerse.Application.DTOs.Common;
|
||||
using UniVerse.Application.DTOs.Lectures;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Application.Mappings;
|
||||
using UniVerse.Domain.Entities;
|
||||
using UniVerse.Domain.Exceptions;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Infrastructure.Services;
|
||||
|
||||
public class LectureService : ILectureService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public LectureService(AppDbContext db) => _db = db;
|
||||
|
||||
private IQueryable<Lecture> BaseQuery() => _db.Lectures
|
||||
.Include(l => l.Course).Include(l => l.Teacher)
|
||||
.Include(l => l.Location).Include(l => l.Enrollments);
|
||||
|
||||
public async Task<PagedResult<LectureDto>> GetAllAsync(LectureFilterRequest filter)
|
||||
{
|
||||
var query = BaseQuery();
|
||||
if (filter.CourseId.HasValue) query = query.Where(l => l.CourseId == filter.CourseId);
|
||||
if (filter.TeacherId.HasValue) query = query.Where(l => l.TeacherId == filter.TeacherId);
|
||||
if (filter.Format.HasValue) query = query.Where(l => l.Format == filter.Format);
|
||||
if (filter.IsOpen.HasValue) query = query.Where(l => l.IsOpen == filter.IsOpen);
|
||||
if (filter.DateFrom.HasValue)
|
||||
query = query.Where(l => DateOnly.FromDateTime(l.StartsAt) >= filter.DateFrom);
|
||||
if (filter.DateTo.HasValue)
|
||||
query = query.Where(l => DateOnly.FromDateTime(l.StartsAt) <= filter.DateTo);
|
||||
if (!string.IsNullOrEmpty(filter.Search))
|
||||
query = query.Where(l => l.Title.ToLower().Contains(filter.Search.ToLower()));
|
||||
if (filter.TagId.HasValue)
|
||||
query = query.Where(l => l.Course.CourseTags.Any(ct => ct.TagId == filter.TagId));
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderBy(l => l.StartsAt)
|
||||
.Skip((filter.Page - 1) * filter.PageSize).Take(filter.PageSize).ToListAsync();
|
||||
return PagedResult<LectureDto>.Create(items.Select(l => l.ToDto()).ToList(), total, filter.Page, filter.PageSize);
|
||||
}
|
||||
|
||||
public async Task<LectureDetailDto> GetByIdAsync(int id, int? currentUserId = null)
|
||||
{
|
||||
var lecture = await BaseQuery().FirstOrDefaultAsync(l => l.Id == id)
|
||||
?? throw new NotFoundException("Lecture", id);
|
||||
var isEnrolled = currentUserId.HasValue &&
|
||||
lecture.Enrollments.Any(e => e.UserId == currentUserId.Value);
|
||||
return lecture.ToDetailDto(isEnrolled);
|
||||
}
|
||||
|
||||
public async Task<LectureDto> CreateAsync(CreateLectureRequest req)
|
||||
{
|
||||
_ = await _db.Courses.FindAsync(req.CourseId) ?? throw new NotFoundException("Course", req.CourseId);
|
||||
var lecture = new Lecture
|
||||
{
|
||||
CourseId = req.CourseId, TeacherId = req.TeacherId, LocationId = req.LocationId,
|
||||
Title = req.Title, Description = req.Description, Format = req.Format,
|
||||
StartsAt = req.StartsAt, EndsAt = req.EndsAt, IsOpen = req.IsOpen,
|
||||
MaxEnrollments = req.MaxEnrollments, OnlineUrl = req.OnlineUrl
|
||||
};
|
||||
_db.Lectures.Add(lecture);
|
||||
await _db.SaveChangesAsync();
|
||||
var full = await BaseQuery().FirstAsync(l => l.Id == lecture.Id);
|
||||
return full.ToDto();
|
||||
}
|
||||
|
||||
public async Task<LectureDto> UpdateAsync(int id, UpdateLectureRequest req)
|
||||
{
|
||||
var lecture = await _db.Lectures.FindAsync(id) ?? throw new NotFoundException("Lecture", id);
|
||||
lecture.TeacherId = req.TeacherId; lecture.LocationId = req.LocationId;
|
||||
lecture.Title = req.Title; lecture.Description = req.Description;
|
||||
lecture.Format = req.Format; lecture.StartsAt = req.StartsAt; lecture.EndsAt = req.EndsAt;
|
||||
lecture.IsOpen = req.IsOpen; lecture.MaxEnrollments = req.MaxEnrollments;
|
||||
lecture.OnlineUrl = req.OnlineUrl; lecture.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
var full = await BaseQuery().FirstAsync(l => l.Id == id);
|
||||
return full.ToDto();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var lecture = await _db.Lectures.FindAsync(id) ?? throw new NotFoundException("Lecture", id);
|
||||
_db.Lectures.Remove(lecture);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task EnrollAsync(int lectureId, int userId)
|
||||
{
|
||||
var lecture = await _db.Lectures.Include(l => l.Enrollments)
|
||||
.FirstOrDefaultAsync(l => l.Id == lectureId) ?? throw new NotFoundException("Lecture", lectureId);
|
||||
if (!lecture.IsOpen) throw new ConflictException("Lecture is not open for enrollment.");
|
||||
if (lecture.MaxEnrollments > 0 && lecture.Enrollments.Count >= lecture.MaxEnrollments)
|
||||
throw new ConflictException("Lecture is full.");
|
||||
if (lecture.Enrollments.Any(e => e.UserId == userId))
|
||||
throw new ConflictException("Already enrolled.");
|
||||
_db.LectureEnrollments.Add(new LectureEnrollment { LectureId = lectureId, UserId = userId });
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UnenrollAsync(int lectureId, int userId)
|
||||
{
|
||||
var enrollment = await _db.LectureEnrollments
|
||||
.FirstOrDefaultAsync(e => e.LectureId == lectureId && e.UserId == userId)
|
||||
?? throw new NotFoundException("Enrollment not found.");
|
||||
_db.LectureEnrollments.Remove(enrollment);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task MarkAttendanceAsync(int lectureId, int userId, bool attended)
|
||||
{
|
||||
var enrollment = await _db.LectureEnrollments
|
||||
.FirstOrDefaultAsync(e => e.LectureId == lectureId && e.UserId == userId)
|
||||
?? throw new NotFoundException("Enrollment not found.");
|
||||
enrollment.Attended = attended;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<PagedResult<EnrollmentDto>> GetEnrollmentsAsync(int lectureId, PaginationRequest pagination)
|
||||
{
|
||||
var query = _db.LectureEnrollments.Include(e => e.User)
|
||||
.Where(e => e.LectureId == lectureId);
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderBy(e => e.CreatedAt)
|
||||
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
|
||||
return PagedResult<EnrollmentDto>.Create(items.Select(e => e.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Domain.Enums;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Infrastructure.Services;
|
||||
|
||||
public class LlmAnalysisService : ILlmAnalysisService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly ILlmClient _llm;
|
||||
private readonly IGamificationService _gamification;
|
||||
private readonly ILogger<LlmAnalysisService> _logger;
|
||||
|
||||
public LlmAnalysisService(AppDbContext db, ILlmClient llm,
|
||||
IGamificationService gamification, ILogger<LlmAnalysisService> logger)
|
||||
{
|
||||
_db = db; _llm = llm; _gamification = gamification; _logger = logger;
|
||||
}
|
||||
|
||||
public async Task AnalyzeReviewAsync(int reviewId)
|
||||
{
|
||||
var review = await _db.Reviews.Include(r => r.Lecture)
|
||||
.FirstOrDefaultAsync(r => r.Id == reviewId);
|
||||
if (review == null || review.LlmStatus != ReviewLlmStatus.Pending) return;
|
||||
|
||||
try
|
||||
{
|
||||
var context = $"Lecture: {review.Lecture?.Title}";
|
||||
var result = await _llm.AnalyzeReviewAsync(review.Text ?? "", context);
|
||||
|
||||
review.QualityScore = result.QualityScore;
|
||||
review.Sentiment = Enum.TryParse<ReviewSentiment>(result.Sentiment, true, out var s)
|
||||
? s : ReviewSentiment.Neutral;
|
||||
review.LlmTags = result.Tags;
|
||||
review.IsInformative = result.IsInformative;
|
||||
review.LlmStatus = ReviewLlmStatus.Analyzed;
|
||||
review.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
if (result.IsInformative)
|
||||
await _gamification.AwardCoinsAsync(review.UserId, 10,
|
||||
CoinTransactionType.ReviewReward, reviewId: review.Id,
|
||||
description: "Informative review reward");
|
||||
|
||||
await _gamification.CheckAndAwardAchievementsAsync(review.UserId);
|
||||
_logger.LogInformation("Review {ReviewId} analyzed successfully", reviewId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to analyze review {ReviewId}, will retry later", reviewId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ProcessPendingReviewsAsync()
|
||||
{
|
||||
var pending = await _db.Reviews
|
||||
.Where(r => r.LlmStatus == ReviewLlmStatus.Pending)
|
||||
.OrderBy(r => r.CreatedAt).Take(10)
|
||||
.Select(r => r.Id).ToListAsync();
|
||||
|
||||
foreach (var id in pending)
|
||||
await AnalyzeReviewAsync(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using UniVerse.Application.DTOs.Locations;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Application.Mappings;
|
||||
using UniVerse.Domain.Entities;
|
||||
using UniVerse.Domain.Exceptions;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Infrastructure.Services;
|
||||
|
||||
public class LocationService : ILocationService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public LocationService(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<List<LocationDto>> GetAllAsync() =>
|
||||
await _db.Locations.OrderBy(l => l.Name)
|
||||
.Select(l => new LocationDto(l.Id, l.Name, l.Building, l.Room, l.Address, l.CreatedAt))
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<LocationDto> GetByIdAsync(int id)
|
||||
{
|
||||
var loc = await _db.Locations.FindAsync(id)
|
||||
?? throw new NotFoundException("Location", id);
|
||||
return loc.ToDto();
|
||||
}
|
||||
|
||||
public async Task<LocationDto> CreateAsync(CreateLocationRequest request)
|
||||
{
|
||||
var loc = new Location
|
||||
{
|
||||
Name = request.Name,
|
||||
Building = request.Building,
|
||||
Room = request.Room,
|
||||
Address = request.Address
|
||||
};
|
||||
_db.Locations.Add(loc);
|
||||
await _db.SaveChangesAsync();
|
||||
return loc.ToDto();
|
||||
}
|
||||
|
||||
public async Task<LocationDto> UpdateAsync(int id, UpdateLocationRequest request)
|
||||
{
|
||||
var loc = await _db.Locations.FindAsync(id)
|
||||
?? throw new NotFoundException("Location", id);
|
||||
|
||||
loc.Name = request.Name;
|
||||
loc.Building = request.Building;
|
||||
loc.Room = request.Room;
|
||||
loc.Address = request.Address;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return loc.ToDto();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var loc = await _db.Locations.FindAsync(id)
|
||||
?? throw new NotFoundException("Location", id);
|
||||
_db.Locations.Remove(loc);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using UniVerse.Application.DTOs.Common;
|
||||
using UniVerse.Application.DTOs.Reviews;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Application.Mappings;
|
||||
using UniVerse.Domain.Entities;
|
||||
using UniVerse.Domain.Enums;
|
||||
using UniVerse.Domain.Exceptions;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Infrastructure.Services;
|
||||
|
||||
public class ReviewService : IReviewService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public ReviewService(AppDbContext db) => _db = db;
|
||||
|
||||
private IQueryable<Review> BaseQuery() => _db.Reviews
|
||||
.Include(r => r.Lecture).Include(r => r.User);
|
||||
|
||||
public async Task<ReviewDto> CreateAsync(int userId, CreateReviewRequest req)
|
||||
{
|
||||
_ = await _db.Lectures.FindAsync(req.LectureId) ?? throw new NotFoundException("Lecture", req.LectureId);
|
||||
if (await _db.Reviews.AnyAsync(r => r.LectureId == req.LectureId && r.UserId == userId))
|
||||
throw new ConflictException("You already reviewed this lecture.");
|
||||
var review = new Review
|
||||
{
|
||||
LectureId = req.LectureId, UserId = userId,
|
||||
Rating = req.Rating, Text = req.Text,
|
||||
LlmStatus = ReviewLlmStatus.Pending
|
||||
};
|
||||
_db.Reviews.Add(review);
|
||||
await _db.SaveChangesAsync();
|
||||
var full = await BaseQuery().FirstAsync(r => r.Id == review.Id);
|
||||
return full.ToDto();
|
||||
}
|
||||
|
||||
public async Task<ReviewDto> GetByIdAsync(int id)
|
||||
{
|
||||
var review = await BaseQuery().FirstOrDefaultAsync(r => r.Id == id)
|
||||
?? throw new NotFoundException("Review", id);
|
||||
return review.ToDto();
|
||||
}
|
||||
|
||||
public async Task<ReviewDto> UpdateAsync(int id, int userId, UpdateReviewRequest req)
|
||||
{
|
||||
var review = await _db.Reviews.FindAsync(id) ?? throw new NotFoundException("Review", id);
|
||||
if (review.UserId != userId) throw new ForbiddenException();
|
||||
review.Rating = req.Rating; review.Text = req.Text;
|
||||
review.LlmStatus = ReviewLlmStatus.Pending;
|
||||
review.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
return await GetByIdAsync(id);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, int userId, bool isAdmin = false)
|
||||
{
|
||||
var review = await _db.Reviews.FindAsync(id) ?? throw new NotFoundException("Review", id);
|
||||
if (review.UserId != userId && !isAdmin) throw new ForbiddenException();
|
||||
_db.Reviews.Remove(review);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<PagedResult<ReviewDto>> GetByLectureAsync(int lectureId, PaginationRequest pagination)
|
||||
{
|
||||
var query = BaseQuery().Where(r => r.LectureId == lectureId);
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderByDescending(r => r.CreatedAt)
|
||||
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
|
||||
return PagedResult<ReviewDto>.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<ReviewDto>> GetByUserAsync(int userId, PaginationRequest pagination)
|
||||
{
|
||||
var query = BaseQuery().Where(r => r.UserId == userId);
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderByDescending(r => r.CreatedAt)
|
||||
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
|
||||
return PagedResult<ReviewDto>.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<ReviewDto>> GetPendingAsync(PaginationRequest pagination)
|
||||
{
|
||||
var query = BaseQuery().Where(r => r.LlmStatus == ReviewLlmStatus.Pending);
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderBy(r => r.CreatedAt)
|
||||
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
|
||||
return PagedResult<ReviewDto>.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||
}
|
||||
|
||||
public async Task ReanalyzeAsync(int id)
|
||||
{
|
||||
var review = await _db.Reviews.FindAsync(id) ?? throw new NotFoundException("Review", id);
|
||||
review.LlmStatus = ReviewLlmStatus.Pending;
|
||||
review.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using UniVerse.Application.DTOs.Sync;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Domain.Entities;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Infrastructure.Services;
|
||||
|
||||
public class ScheduleSyncService : IScheduleSyncService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IModeusApiClient _modeus;
|
||||
private readonly ILogger<ScheduleSyncService> _logger;
|
||||
private static SyncStatusDto _lastStatus = new(null, "idle", null);
|
||||
|
||||
public ScheduleSyncService(AppDbContext db, IModeusApiClient modeus, ILogger<ScheduleSyncService> logger)
|
||||
{
|
||||
_db = db; _modeus = modeus; _logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SyncResultDto> SyncScheduleAsync(SyncScheduleRequest request)
|
||||
{
|
||||
int created = 0, updated = 0, skipped = 0;
|
||||
try
|
||||
{
|
||||
var events = await _modeus.SearchEventsAsync(request);
|
||||
foreach (var ev in events.Events)
|
||||
{
|
||||
var existing = await _db.Lectures.FirstOrDefaultAsync(l => l.ExternalId == ev.Id);
|
||||
if (existing != null) { updated++; existing.StartsAt = ev.StartsAt; existing.EndsAt = ev.EndsAt; existing.UpdatedAt = DateTime.UtcNow; }
|
||||
else
|
||||
{
|
||||
var course = await _db.Courses.FirstOrDefaultAsync(c => c.ExternalId == ev.TypeId);
|
||||
if (course == null) { course = new Course { Name = ev.Name, ExternalId = ev.TypeId, IsSynced = true }; _db.Courses.Add(course); await _db.SaveChangesAsync(); }
|
||||
_db.Lectures.Add(new Lecture { CourseId = course.Id, Title = ev.Name, ExternalId = ev.Id, StartsAt = ev.StartsAt, EndsAt = ev.EndsAt });
|
||||
created++;
|
||||
}
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
var result = new SyncResultDto(created, updated, skipped, null);
|
||||
_lastStatus = new SyncStatusDto(DateTime.UtcNow, "completed", result);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Schedule sync failed");
|
||||
var result = new SyncResultDto(created, updated, skipped, ex.Message);
|
||||
_lastStatus = new SyncStatusDto(DateTime.UtcNow, "failed", result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SyncResultDto> SyncRoomsAsync()
|
||||
{
|
||||
int created = 0, updated = 0;
|
||||
var rooms = await _modeus.SearchRoomsAsync();
|
||||
foreach (var room in rooms.Rooms)
|
||||
{
|
||||
var existing = await _db.Locations.FirstOrDefaultAsync(l => l.ExternalId == room.Id);
|
||||
if (existing != null) { existing.Name = room.Name; existing.Building = room.Building; updated++; }
|
||||
else { _db.Locations.Add(new Location { Name = room.Name, Building = room.Building, ExternalId = room.Id }); created++; }
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
return new SyncResultDto(created, updated, 0, null);
|
||||
}
|
||||
|
||||
public async Task<List<EmployeeDto>> SearchEmployeesAsync(string fullname)
|
||||
{
|
||||
var employees = await _modeus.SearchEmployeeAsync(fullname);
|
||||
return employees.Select(e => new EmployeeDto(e.Id, e.FullName, e.Department)).ToList();
|
||||
}
|
||||
|
||||
public Task<SyncStatusDto> GetLastSyncStatusAsync() => Task.FromResult(_lastStatus);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using UniVerse.Application.DTOs.Tags;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Application.Mappings;
|
||||
using UniVerse.Domain.Entities;
|
||||
using UniVerse.Domain.Enums;
|
||||
using UniVerse.Domain.Exceptions;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Infrastructure.Services;
|
||||
|
||||
public class TagService : ITagService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public TagService(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<List<TagDto>> GetAllAsync(TagType? type = null, int? parentId = null)
|
||||
{
|
||||
var query = _db.Tags.AsQueryable();
|
||||
if (type.HasValue) query = query.Where(t => t.Type == type.Value);
|
||||
if (parentId.HasValue) query = query.Where(t => t.ParentId == parentId.Value);
|
||||
|
||||
return await query.OrderBy(t => t.Name)
|
||||
.Select(t => new TagDto(t.Id, t.Name, t.Type, t.ParentId, t.CreatedAt))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<TagDto> GetByIdAsync(int id)
|
||||
{
|
||||
var tag = await _db.Tags.FindAsync(id)
|
||||
?? throw new NotFoundException("Tag", id);
|
||||
return tag.ToDto();
|
||||
}
|
||||
|
||||
public async Task<TagDto> CreateAsync(CreateTagRequest request)
|
||||
{
|
||||
if (request.ParentId.HasValue)
|
||||
{
|
||||
var parent = await _db.Tags.FindAsync(request.ParentId.Value)
|
||||
?? throw new NotFoundException("Parent Tag", request.ParentId.Value);
|
||||
}
|
||||
|
||||
var tag = new Tag
|
||||
{
|
||||
Name = request.Name,
|
||||
Type = request.Type,
|
||||
ParentId = request.ParentId
|
||||
};
|
||||
|
||||
_db.Tags.Add(tag);
|
||||
await _db.SaveChangesAsync();
|
||||
return tag.ToDto();
|
||||
}
|
||||
|
||||
public async Task<TagDto> UpdateAsync(int id, UpdateTagRequest request)
|
||||
{
|
||||
var tag = await _db.Tags.FindAsync(id)
|
||||
?? throw new NotFoundException("Tag", id);
|
||||
|
||||
tag.Name = request.Name;
|
||||
tag.Type = request.Type;
|
||||
tag.ParentId = request.ParentId;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return tag.ToDto();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var tag = await _db.Tags.FindAsync(id)
|
||||
?? throw new NotFoundException("Tag", id);
|
||||
_db.Tags.Remove(tag);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<TagTreeDto>> GetTreeAsync()
|
||||
{
|
||||
var allTags = await _db.Tags
|
||||
.Include(t => t.Children)
|
||||
.ToListAsync();
|
||||
|
||||
var roots = allTags.Where(t => t.ParentId == null).ToList();
|
||||
return roots.Select(t => BuildTree(t, allTags)).ToList();
|
||||
}
|
||||
|
||||
private TagTreeDto BuildTree(Tag tag, List<Tag> allTags)
|
||||
{
|
||||
var children = allTags.Where(t => t.ParentId == tag.Id).ToList();
|
||||
return new TagTreeDto(
|
||||
tag.Id, tag.Name, tag.Type,
|
||||
children.Select(c => BuildTree(c, allTags)).ToList()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using UniVerse.Application.DTOs.Common;
|
||||
using UniVerse.Application.DTOs.Users;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Application.Mappings;
|
||||
using UniVerse.Domain.Enums;
|
||||
using UniVerse.Domain.Exceptions;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Infrastructure.Services;
|
||||
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IGamificationService _gamification;
|
||||
|
||||
public UserService(AppDbContext db, IGamificationService gamification)
|
||||
{
|
||||
_db = db;
|
||||
_gamification = gamification;
|
||||
}
|
||||
|
||||
public async Task<UserDto> GetByIdAsync(int id)
|
||||
{
|
||||
var user = await _db.Users.FindAsync(id)
|
||||
?? throw new NotFoundException("User", id);
|
||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||
}
|
||||
|
||||
public async Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request)
|
||||
{
|
||||
var user = await _db.Users.FindAsync(id)
|
||||
?? throw new NotFoundException("User", id);
|
||||
|
||||
if (request.DisplayName != null) user.DisplayName = request.DisplayName;
|
||||
if (request.AvatarUrl != null) user.AvatarUrl = request.AvatarUrl;
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||
}
|
||||
|
||||
public async Task<UserStatsDto> GetStatsAsync(int id)
|
||||
{
|
||||
var user = await _db.Users.FindAsync(id)
|
||||
?? throw new NotFoundException("User", id);
|
||||
|
||||
var totalLectures = await _db.LectureEnrollments.CountAsync(e => e.UserId == id);
|
||||
var attended = await _db.LectureEnrollments.CountAsync(e => e.UserId == id && e.Attended);
|
||||
var reviews = await _db.Reviews.CountAsync(r => r.UserId == id);
|
||||
var achievements = await _db.UserAchievements.CountAsync(ua => ua.UserId == id);
|
||||
|
||||
return new UserStatsDto(
|
||||
totalLectures, attended, reviews,
|
||||
user.Xp, user.Coins, _gamification.CalculateLevel(user.Xp), achievements
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<UserDto>> GetAllAsync(UserFilterRequest filter)
|
||||
{
|
||||
var query = _db.Users.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(filter.Search))
|
||||
{
|
||||
var search = filter.Search.ToLower();
|
||||
query = query.Where(u =>
|
||||
u.Email.ToLower().Contains(search) ||
|
||||
(u.DisplayName != null && u.DisplayName.ToLower().Contains(search)));
|
||||
}
|
||||
|
||||
if (filter.Role.HasValue)
|
||||
query = query.Where(u => u.Role == filter.Role.Value);
|
||||
|
||||
if (filter.IsActive.HasValue)
|
||||
query = query.Where(u => u.IsActive == filter.IsActive.Value);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
var users = await query
|
||||
.OrderByDescending(u => u.CreatedAt)
|
||||
.Skip((filter.Page - 1) * filter.PageSize)
|
||||
.Take(filter.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var items = users.Select(u => u.ToDto(_gamification.CalculateLevel(u.Xp))).ToList();
|
||||
return PagedResult<UserDto>.Create(items, total, filter.Page, filter.PageSize);
|
||||
}
|
||||
|
||||
public async Task SetRoleAsync(int id, UserRole role)
|
||||
{
|
||||
var user = await _db.Users.FindAsync(id)
|
||||
?? throw new NotFoundException("User", id);
|
||||
user.Role = role;
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task SetActiveAsync(int id, bool isActive)
|
||||
{
|
||||
var user = await _db.Users.FindAsync(id)
|
||||
?? throw new NotFoundException("User", id);
|
||||
user.IsActive = isActive;
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user