VirtualClass BackEnd
This commit is contained in:
parent
45b763776d
commit
7d37a07e05
15 changed files with 8467 additions and 0 deletions
|
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
|
||||
namespace Kurs.Platform.VirtualClassrooms;
|
||||
|
||||
public class ChatMessageDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid SenderId { get; set; }
|
||||
public string SenderName { get; set; }
|
||||
public string Message { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public bool IsTeacher { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
|
||||
namespace Kurs.Platform.VirtualClassrooms;
|
||||
|
||||
public class ClassSessionDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public Guid TeacherId { get; set; }
|
||||
public string TeacherName { get; set; }
|
||||
public DateTime ScheduledStartTime { get; set; }
|
||||
public DateTime? ActualStartTime { get; set; }
|
||||
public DateTime? EndTime { get; set; }
|
||||
public int Duration { get; set; }
|
||||
public int MaxParticipants { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsScheduled { get; set; }
|
||||
public int ParticipantCount { get; set; }
|
||||
public bool CanJoin { get; set; }
|
||||
}
|
||||
|
||||
public class CreateClassSessionDto
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public DateTime ScheduledStartTime { get; set; }
|
||||
public int Duration { get; set; } = 60;
|
||||
public int MaxParticipants { get; set; } = 30;
|
||||
}
|
||||
|
||||
public class UpdateClassSessionDto
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public DateTime ScheduledStartTime { get; set; }
|
||||
public int Duration { get; set; }
|
||||
public int MaxParticipants { get; set; }
|
||||
}
|
||||
|
||||
public class GetClassSessionListDto : PagedAndSortedResultRequestDto
|
||||
{
|
||||
public bool? IsActive { get; set; }
|
||||
public Guid? TeacherId { get; set; }
|
||||
}
|
||||
|
||||
public class AttendanceRecordDto : EntityDto<Guid>
|
||||
{
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid StudentId { get; set; }
|
||||
public string StudentName { get; set; }
|
||||
public DateTime JoinTime { get; set; }
|
||||
public DateTime? LeaveTime { get; set; }
|
||||
public int TotalDurationMinutes { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Application.Services;
|
||||
|
||||
namespace Kurs.Platform.VirtualClassrooms;
|
||||
|
||||
public interface IVirtualClassAppService : IApplicationService
|
||||
{
|
||||
Task<ClassSessionDto> CreateAsync(CreateClassSessionDto input);
|
||||
Task<PagedResultDto<ClassSessionDto>> GetListAsync(GetClassSessionListDto input);
|
||||
Task<ClassSessionDto> GetAsync(Guid id);
|
||||
Task<ClassSessionDto> UpdateAsync(Guid id, UpdateClassSessionDto input);
|
||||
Task DeleteAsync(Guid id);
|
||||
Task<ClassSessionDto> StartClassAsync(Guid id);
|
||||
Task EndClassAsync(Guid id);
|
||||
Task<ClassSessionDto> JoinClassAsync(Guid id);
|
||||
Task LeaveClassAsync(Guid id);
|
||||
Task<List<AttendanceRecordDto>> GetAttendanceAsync(Guid sessionId);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
|
||||
namespace Kurs.Platform.VirtualClassrooms;
|
||||
|
||||
public class ParticipantDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string UserEmail { get; set; }
|
||||
public bool IsTeacher { get; set; }
|
||||
public bool IsAudioMuted { get; set; }
|
||||
public bool IsVideoMuted { get; set; }
|
||||
public DateTime JoinTime { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Kurs.Platform.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
|
||||
namespace Kurs.Platform.VirtualClassrooms;
|
||||
|
||||
[Authorize]
|
||||
public class VirtualClassAppService : PlatformAppService, IVirtualClassAppService
|
||||
{
|
||||
private readonly IRepository<ClassSession, Guid> _classSessionRepository;
|
||||
private readonly IRepository<Participant, Guid> _participantRepository;
|
||||
private readonly IRepository<AttendanceRecord, Guid> _attendanceRepository;
|
||||
|
||||
public VirtualClassAppService(
|
||||
IRepository<ClassSession, Guid> classSessionRepository,
|
||||
IRepository<Participant, Guid> participantRepository,
|
||||
IRepository<AttendanceRecord, Guid> attendanceRepository)
|
||||
{
|
||||
_classSessionRepository = classSessionRepository;
|
||||
_participantRepository = participantRepository;
|
||||
_attendanceRepository = attendanceRepository;
|
||||
}
|
||||
|
||||
public async Task<ClassSessionDto> CreateAsync(CreateClassSessionDto input)
|
||||
{
|
||||
var classSession = new ClassSession(
|
||||
GuidGenerator.Create(),
|
||||
input.Name,
|
||||
input.Description,
|
||||
input.Subject,
|
||||
CurrentUser.Id,
|
||||
CurrentUser.Name,
|
||||
input.ScheduledStartTime,
|
||||
input.Duration,
|
||||
input.MaxParticipants
|
||||
);
|
||||
|
||||
await _classSessionRepository.InsertAsync(classSession);
|
||||
await CurrentUnitOfWork.SaveChangesAsync();
|
||||
|
||||
return ObjectMapper.Map<ClassSession, ClassSessionDto>(classSession);
|
||||
}
|
||||
|
||||
public async Task<PagedResultDto<ClassSessionDto>> GetListAsync(GetClassSessionListDto input)
|
||||
{
|
||||
var query = await _classSessionRepository.GetQueryableAsync();
|
||||
|
||||
if (input.IsActive.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.IsActive == input.IsActive.Value);
|
||||
}
|
||||
|
||||
if (input.TeacherId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TeacherId == input.TeacherId.Value);
|
||||
}
|
||||
|
||||
var totalCount = query.Count();
|
||||
var items = query
|
||||
.OrderBy(x => x.ScheduledStartTime)
|
||||
.Skip(input.SkipCount)
|
||||
.Take(input.MaxResultCount)
|
||||
.ToList();
|
||||
|
||||
return new PagedResultDto<ClassSessionDto>(
|
||||
totalCount,
|
||||
ObjectMapper.Map<List<ClassSession>, List<ClassSessionDto>>(items)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<ClassSessionDto> GetAsync(Guid id)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
return ObjectMapper.Map<ClassSession, ClassSessionDto>(classSession);
|
||||
}
|
||||
|
||||
public async Task<ClassSessionDto> UpdateAsync(Guid id, UpdateClassSessionDto input)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
|
||||
if (classSession.TeacherId != CurrentUser.Id)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only the teacher can update this class");
|
||||
}
|
||||
|
||||
if (classSession.IsActive)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot update an active class");
|
||||
}
|
||||
|
||||
classSession.Name = input.Name;
|
||||
classSession.Description = input.Description;
|
||||
classSession.Subject = input.Subject;
|
||||
classSession.ScheduledStartTime = input.ScheduledStartTime;
|
||||
classSession.Duration = input.Duration;
|
||||
classSession.MaxParticipants = input.MaxParticipants;
|
||||
|
||||
await _classSessionRepository.UpdateAsync(classSession);
|
||||
return ObjectMapper.Map<ClassSession, ClassSessionDto>(classSession);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid id)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
|
||||
if (classSession.TeacherId != CurrentUser.Id)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only the teacher can delete this class");
|
||||
}
|
||||
|
||||
if (classSession.IsActive)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot delete an active class");
|
||||
}
|
||||
|
||||
await _classSessionRepository.DeleteAsync(id);
|
||||
}
|
||||
|
||||
public async Task<ClassSessionDto> StartClassAsync(Guid id)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
|
||||
if (classSession.TeacherId != CurrentUser.Id)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only the teacher can start this class");
|
||||
}
|
||||
|
||||
if (!classSession.CanJoin())
|
||||
{
|
||||
throw new InvalidOperationException("Class cannot be started at this time");
|
||||
}
|
||||
|
||||
classSession.StartClass();
|
||||
await _classSessionRepository.UpdateAsync(classSession);
|
||||
|
||||
return ObjectMapper.Map<ClassSession, ClassSessionDto>(classSession);
|
||||
}
|
||||
|
||||
public async Task EndClassAsync(Guid id)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
|
||||
if (classSession.TeacherId != CurrentUser.Id)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only the teacher can end this class");
|
||||
}
|
||||
|
||||
classSession.EndClass();
|
||||
await _classSessionRepository.UpdateAsync(classSession);
|
||||
|
||||
// Update attendance records
|
||||
var activeAttendances = await _attendanceRepository.GetListAsync(
|
||||
x => x.SessionId == id && x.LeaveTime == null
|
||||
);
|
||||
|
||||
foreach (var attendance in activeAttendances)
|
||||
{
|
||||
attendance.LeaveTime = DateTime.UtcNow;
|
||||
attendance.CalculateDuration();
|
||||
await _attendanceRepository.UpdateAsync(attendance);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ClassSessionDto> JoinClassAsync(Guid id)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
|
||||
if (!classSession.CanJoin())
|
||||
{
|
||||
throw new InvalidOperationException("Cannot join this class at this time");
|
||||
}
|
||||
|
||||
if (classSession.ParticipantCount >= classSession.MaxParticipants)
|
||||
{
|
||||
throw new InvalidOperationException("Class is full");
|
||||
}
|
||||
|
||||
// Check if user is already in the class
|
||||
var existingParticipant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == id && x.UserId == CurrentUser.Id
|
||||
);
|
||||
|
||||
if (existingParticipant == null)
|
||||
{
|
||||
// Add participant
|
||||
var participant = new Participant(
|
||||
GuidGenerator.Create(),
|
||||
id,
|
||||
CurrentUser.Id,
|
||||
CurrentUser.Name,
|
||||
CurrentUser.Email,
|
||||
false // isTeacher
|
||||
);
|
||||
|
||||
await _participantRepository.InsertAsync(participant);
|
||||
|
||||
// Create attendance record
|
||||
var attendance = new AttendanceRecord(
|
||||
GuidGenerator.Create(),
|
||||
id,
|
||||
CurrentUser.Id,
|
||||
CurrentUser.Name,
|
||||
DateTime.UtcNow
|
||||
);
|
||||
|
||||
await _attendanceRepository.InsertAsync(attendance);
|
||||
|
||||
// Update participant count
|
||||
classSession.ParticipantCount++;
|
||||
await _classSessionRepository.UpdateAsync(classSession);
|
||||
}
|
||||
|
||||
return ObjectMapper.Map<ClassSession, ClassSessionDto>(classSession);
|
||||
}
|
||||
|
||||
public async Task LeaveClassAsync(Guid id)
|
||||
{
|
||||
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == id && x.UserId == CurrentUser.Id
|
||||
);
|
||||
|
||||
if (participant != null)
|
||||
{
|
||||
await _participantRepository.DeleteAsync(participant);
|
||||
|
||||
// Update attendance record
|
||||
var attendance = await _attendanceRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == id && x.StudentId == CurrentUser.Id && x.LeaveTime == null
|
||||
);
|
||||
|
||||
if (attendance != null)
|
||||
{
|
||||
attendance.LeaveTime = DateTime.UtcNow;
|
||||
attendance.CalculateDuration();
|
||||
await _attendanceRepository.UpdateAsync(attendance);
|
||||
}
|
||||
|
||||
// Update participant count
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
classSession.ParticipantCount = Math.Max(0, classSession.ParticipantCount - 1);
|
||||
await _classSessionRepository.UpdateAsync(classSession);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<AttendanceRecordDto>> GetAttendanceAsync(Guid sessionId)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(sessionId);
|
||||
|
||||
if (classSession.TeacherId != CurrentUser.Id)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only the teacher can view attendance");
|
||||
}
|
||||
|
||||
var attendanceRecords = await _attendanceRepository.GetListAsync(
|
||||
x => x.SessionId == sessionId
|
||||
);
|
||||
|
||||
return ObjectMapper.Map<List<AttendanceRecord>, List<AttendanceRecordDto>>(attendanceRecords);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using AutoMapper;
|
||||
using Kurs.Platform.Entities;
|
||||
|
||||
namespace Kurs.Platform.VirtualClassrooms;
|
||||
|
||||
public class VirtualClassAutoMapperProfile : Profile
|
||||
{
|
||||
public VirtualClassAutoMapperProfile()
|
||||
{
|
||||
CreateMap<ClassSession, ClassSessionDto>()
|
||||
.ForMember(dest => dest.CanJoin, opt => opt.MapFrom(src => src.CanJoin()));
|
||||
|
||||
CreateMap<CreateClassSessionDto, ClassSession>();
|
||||
CreateMap<UpdateClassSessionDto, ClassSession>();
|
||||
|
||||
CreateMap<AttendanceRecord, AttendanceRecordDto>();
|
||||
CreateMap<Participant, ParticipantDto>();
|
||||
CreateMap<ChatMessage, ChatMessageDto>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
|
||||
namespace Kurs.Platform.Entities;
|
||||
|
||||
public class AttendanceRecord : FullAuditedEntity<Guid>
|
||||
{
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid? StudentId { get; set; }
|
||||
public string StudentName { get; set; }
|
||||
public DateTime JoinTime { get; set; }
|
||||
public DateTime? LeaveTime { get; set; }
|
||||
public int TotalDurationMinutes { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual ClassSession Session { get; set; }
|
||||
|
||||
protected AttendanceRecord()
|
||||
{
|
||||
}
|
||||
|
||||
public AttendanceRecord(
|
||||
Guid id,
|
||||
Guid sessionId,
|
||||
Guid? studentId,
|
||||
string studentName,
|
||||
DateTime joinTime
|
||||
) : base(id)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
StudentId = studentId;
|
||||
StudentName = studentName;
|
||||
JoinTime = joinTime;
|
||||
TotalDurationMinutes = 0;
|
||||
}
|
||||
|
||||
public void CalculateDuration()
|
||||
{
|
||||
if (LeaveTime.HasValue)
|
||||
{
|
||||
var duration = LeaveTime.Value - JoinTime;
|
||||
TotalDurationMinutes = (int)duration.TotalMinutes;
|
||||
}
|
||||
else
|
||||
{
|
||||
var duration = DateTime.UtcNow - JoinTime;
|
||||
TotalDurationMinutes = (int)duration.TotalMinutes;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
api/src/Kurs.Platform.Domain/VirtualClass/ChatMessage.cs
Normal file
38
api/src/Kurs.Platform.Domain/VirtualClass/ChatMessage.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
|
||||
namespace Kurs.Platform.Entities;
|
||||
|
||||
public class ChatMessage : FullAuditedEntity<Guid>
|
||||
{
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid SenderId { get; set; }
|
||||
public string SenderName { get; set; }
|
||||
public string Message { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public bool IsTeacher { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual ClassSession Session { get; set; }
|
||||
|
||||
protected ChatMessage()
|
||||
{
|
||||
}
|
||||
|
||||
public ChatMessage(
|
||||
Guid id,
|
||||
Guid sessionId,
|
||||
Guid senderId,
|
||||
string senderName,
|
||||
string message,
|
||||
bool isTeacher
|
||||
) : base(id)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
SenderId = senderId;
|
||||
SenderName = senderName;
|
||||
Message = message;
|
||||
IsTeacher = isTeacher;
|
||||
Timestamp = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
89
api/src/Kurs.Platform.Domain/VirtualClass/ClassSession.cs
Normal file
89
api/src/Kurs.Platform.Domain/VirtualClass/ClassSession.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
|
||||
namespace Kurs.Platform.Entities;
|
||||
|
||||
public class ClassSession : FullAuditedEntity<Guid>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public Guid? TeacherId { get; set; }
|
||||
public string TeacherName { get; set; }
|
||||
public DateTime ScheduledStartTime { get; set; }
|
||||
public DateTime? ActualStartTime { get; set; }
|
||||
public DateTime? EndTime { get; set; }
|
||||
public int Duration { get; set; } // minutes
|
||||
public int MaxParticipants { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsScheduled { get; set; }
|
||||
public int ParticipantCount { get; set; }
|
||||
|
||||
public virtual ICollection<Participant> Participants { get; set; }
|
||||
public virtual ICollection<AttendanceRecord> AttendanceRecords { get; set; }
|
||||
public virtual ICollection<ChatMessage> ChatMessages { get; set; }
|
||||
|
||||
protected ClassSession()
|
||||
{
|
||||
Participants = new HashSet<Participant>();
|
||||
AttendanceRecords = new HashSet<AttendanceRecord>();
|
||||
ChatMessages = new HashSet<ChatMessage>();
|
||||
}
|
||||
|
||||
public ClassSession(
|
||||
Guid id,
|
||||
string name,
|
||||
string description,
|
||||
string subject,
|
||||
Guid? teacherId,
|
||||
string teacherName,
|
||||
DateTime scheduledStartTime,
|
||||
int duration,
|
||||
int maxParticipants
|
||||
) : base(id)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
Subject = subject;
|
||||
TeacherId = teacherId;
|
||||
TeacherName = teacherName;
|
||||
ScheduledStartTime = scheduledStartTime;
|
||||
Duration = duration;
|
||||
MaxParticipants = maxParticipants;
|
||||
IsActive = false;
|
||||
IsScheduled = true;
|
||||
ParticipantCount = 0;
|
||||
|
||||
Participants = new HashSet<Participant>();
|
||||
AttendanceRecords = new HashSet<AttendanceRecord>();
|
||||
ChatMessages = new HashSet<ChatMessage>();
|
||||
}
|
||||
|
||||
public void StartClass()
|
||||
{
|
||||
if (IsActive)
|
||||
throw new InvalidOperationException("Class is already active");
|
||||
|
||||
IsActive = true;
|
||||
ActualStartTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void EndClass()
|
||||
{
|
||||
if (!IsActive)
|
||||
throw new InvalidOperationException("Class is not active");
|
||||
|
||||
IsActive = false;
|
||||
EndTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public bool CanJoin()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var tenMinutesBefore = ScheduledStartTime.AddMinutes(-10);
|
||||
var twoHoursAfter = ScheduledStartTime.AddHours(2);
|
||||
|
||||
return now >= tenMinutesBefore && now <= twoHoursAfter && ParticipantCount < MaxParticipants;
|
||||
}
|
||||
}
|
||||
68
api/src/Kurs.Platform.Domain/VirtualClass/Participant.cs
Normal file
68
api/src/Kurs.Platform.Domain/VirtualClass/Participant.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
|
||||
namespace Kurs.Platform.Entities;
|
||||
|
||||
public class Participant : FullAuditedEntity<Guid>
|
||||
{
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string UserEmail { get; set; }
|
||||
public bool IsTeacher { get; set; }
|
||||
public bool IsAudioMuted { get; set; }
|
||||
public bool IsVideoMuted { get; set; }
|
||||
public DateTime JoinTime { get; set; }
|
||||
public string ConnectionId { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual ClassSession Session { get; set; }
|
||||
|
||||
protected Participant()
|
||||
{
|
||||
}
|
||||
|
||||
public Participant(
|
||||
Guid id,
|
||||
Guid sessionId,
|
||||
Guid? userId,
|
||||
string userName,
|
||||
string userEmail,
|
||||
bool isTeacher
|
||||
) : base(id)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
UserId = userId;
|
||||
UserName = userName;
|
||||
UserEmail = userEmail;
|
||||
IsTeacher = isTeacher;
|
||||
IsAudioMuted = false;
|
||||
IsVideoMuted = false;
|
||||
JoinTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void MuteAudio()
|
||||
{
|
||||
IsAudioMuted = true;
|
||||
}
|
||||
|
||||
public void UnmuteAudio()
|
||||
{
|
||||
IsAudioMuted = false;
|
||||
}
|
||||
|
||||
public void MuteVideo()
|
||||
{
|
||||
IsVideoMuted = true;
|
||||
}
|
||||
|
||||
public void UnmuteVideo()
|
||||
{
|
||||
IsVideoMuted = false;
|
||||
}
|
||||
|
||||
public void UpdateConnectionId(string connectionId)
|
||||
{
|
||||
ConnectionId = connectionId;
|
||||
}
|
||||
}
|
||||
|
|
@ -97,6 +97,11 @@ public class PlatformDbContext :
|
|||
public DbSet<Demo> Demos { get; set; }
|
||||
public DbSet<Service> Services { get; set; }
|
||||
|
||||
public DbSet<ClassSession> ClassSessions { get; set; }
|
||||
public DbSet<Participant> Participants { get; set; }
|
||||
public DbSet<AttendanceRecord> AttendanceRecords { get; set; }
|
||||
public DbSet<ChatMessage> ChatMessages { get; set; }
|
||||
|
||||
#region Entities from the modules
|
||||
|
||||
/* Notice: We only implemented IIdentityDbContext and ITenantManagementDbContext
|
||||
|
|
@ -875,5 +880,79 @@ public class PlatformDbContext :
|
|||
b.Property(x => x.WorkHoursJson).HasColumnType("nvarchar(max)");
|
||||
b.Property(x => x.MapJson).HasColumnType("nvarchar(max)");
|
||||
});
|
||||
|
||||
// ClassSession
|
||||
builder.Entity<ClassSession>(b =>
|
||||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassSession), PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Name).IsRequired().HasMaxLength(200);
|
||||
b.Property(x => x.Description).HasMaxLength(1000);
|
||||
b.Property(x => x.Subject).HasMaxLength(100);
|
||||
b.Property(x => x.TeacherName).IsRequired().HasMaxLength(100);
|
||||
|
||||
b.HasIndex(x => x.TeacherId);
|
||||
b.HasIndex(x => x.ScheduledStartTime);
|
||||
b.HasIndex(x => x.IsActive);
|
||||
|
||||
// Relationships
|
||||
b.HasMany(x => x.Participants)
|
||||
.WithOne(x => x.Session)
|
||||
.HasForeignKey(x => x.SessionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasMany(x => x.AttendanceRecords)
|
||||
.WithOne(x => x.Session)
|
||||
.HasForeignKey(x => x.SessionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasMany(x => x.ChatMessages)
|
||||
.WithOne(x => x.Session)
|
||||
.HasForeignKey(x => x.SessionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// Participant
|
||||
builder.Entity<Participant>(b =>
|
||||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + nameof(Participant), PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.UserName).IsRequired().HasMaxLength(100);
|
||||
b.Property(x => x.UserEmail).HasMaxLength(200);
|
||||
b.Property(x => x.ConnectionId).HasMaxLength(100);
|
||||
|
||||
b.HasIndex(x => x.SessionId);
|
||||
b.HasIndex(x => x.UserId);
|
||||
b.HasIndex(x => new { x.SessionId, x.UserId }).IsUnique();
|
||||
});
|
||||
|
||||
// AttendanceRecord
|
||||
builder.Entity<AttendanceRecord>(b =>
|
||||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + nameof(AttendanceRecord), PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.StudentName).IsRequired().HasMaxLength(100);
|
||||
|
||||
b.HasIndex(x => x.SessionId);
|
||||
b.HasIndex(x => x.StudentId);
|
||||
b.HasIndex(x => x.JoinTime);
|
||||
});
|
||||
|
||||
// ChatMessage
|
||||
builder.Entity<ChatMessage>(b =>
|
||||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ChatMessage), PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.SenderName).IsRequired().HasMaxLength(100);
|
||||
b.Property(x => x.Message).IsRequired().HasMaxLength(2000);
|
||||
|
||||
b.HasIndex(x => x.SessionId);
|
||||
b.HasIndex(x => x.SenderId);
|
||||
b.HasIndex(x => x.Timestamp);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7011
api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250825175413_VirtualClass.Designer.cs
generated
Normal file
7011
api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250825175413_VirtualClass.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,217 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Kurs.Platform.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class VirtualClass : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PClassSession",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
Subject = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
TeacherId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
TeacherName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
ScheduledStartTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ActualStartTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
EndTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
Duration = table.Column<int>(type: "int", nullable: false),
|
||||
MaxParticipants = table.Column<int>(type: "int", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsScheduled = table.Column<bool>(type: "bit", nullable: false),
|
||||
ParticipantCount = table.Column<int>(type: "int", nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PClassSession", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PAttendanceRecord",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SessionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
StudentId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
StudentName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
JoinTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
LeaveTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
TotalDurationMinutes = table.Column<int>(type: "int", nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PAttendanceRecord", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PAttendanceRecord_PClassSession_SessionId",
|
||||
column: x => x.SessionId,
|
||||
principalTable: "PClassSession",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PChatMessage",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SessionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SenderId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SenderName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
Message = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false),
|
||||
Timestamp = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
IsTeacher = table.Column<bool>(type: "bit", nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PChatMessage", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PChatMessage_PClassSession_SessionId",
|
||||
column: x => x.SessionId,
|
||||
principalTable: "PClassSession",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PParticipant",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SessionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
UserName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
UserEmail = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
IsTeacher = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsAudioMuted = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsVideoMuted = table.Column<bool>(type: "bit", nullable: false),
|
||||
JoinTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ConnectionId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PParticipant", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PParticipant_PClassSession_SessionId",
|
||||
column: x => x.SessionId,
|
||||
principalTable: "PClassSession",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PAttendanceRecord_JoinTime",
|
||||
table: "PAttendanceRecord",
|
||||
column: "JoinTime");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PAttendanceRecord_SessionId",
|
||||
table: "PAttendanceRecord",
|
||||
column: "SessionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PAttendanceRecord_StudentId",
|
||||
table: "PAttendanceRecord",
|
||||
column: "StudentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PChatMessage_SenderId",
|
||||
table: "PChatMessage",
|
||||
column: "SenderId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PChatMessage_SessionId",
|
||||
table: "PChatMessage",
|
||||
column: "SessionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PChatMessage_Timestamp",
|
||||
table: "PChatMessage",
|
||||
column: "Timestamp");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PClassSession_IsActive",
|
||||
table: "PClassSession",
|
||||
column: "IsActive");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PClassSession_ScheduledStartTime",
|
||||
table: "PClassSession",
|
||||
column: "ScheduledStartTime");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PClassSession_TeacherId",
|
||||
table: "PClassSession",
|
||||
column: "TeacherId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PParticipant_SessionId",
|
||||
table: "PParticipant",
|
||||
column: "SessionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PParticipant_SessionId_UserId",
|
||||
table: "PParticipant",
|
||||
columns: new[] { "SessionId", "UserId" },
|
||||
unique: true,
|
||||
filter: "[UserId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PParticipant_UserId",
|
||||
table: "PParticipant",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PAttendanceRecord");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PChatMessage");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PParticipant");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PClassSession");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -849,6 +849,72 @@ namespace Kurs.Platform.Migrations
|
|||
b.ToTable("PApiMigration", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.AttendanceRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<DateTime>("JoinTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<DateTime?>("LeaveTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid>("SessionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("StudentId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("StudentName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("TotalDurationMinutes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JoinTime");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("StudentId");
|
||||
|
||||
b.ToTable("PAttendanceRecord", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.BackgroundWorker", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1494,6 +1560,74 @@ namespace Kurs.Platform.Migrations
|
|||
b.ToTable("PChart", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.ChatMessage", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsTeacher")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("SenderName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<Guid>("SessionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SenderId");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.ToTable("PChatMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.City", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1556,6 +1690,97 @@ namespace Kurs.Platform.Migrations
|
|||
b.ToTable("PCity", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.ClassSession", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("ActualStartTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<int>("Duration")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("EndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsScheduled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<int>("MaxParticipants")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("ParticipantCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("ScheduledStartTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<Guid?>("TeacherId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("TeacherName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("ScheduledStartTime");
|
||||
|
||||
b.HasIndex("TeacherId");
|
||||
|
||||
b.ToTable("PClassSession", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.Contact", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -3049,6 +3274,85 @@ namespace Kurs.Platform.Migrations
|
|||
b.ToTable("PMenu", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.Participant", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ConnectionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsAudioMuted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsTeacher")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsVideoMuted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime>("JoinTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<Guid>("SessionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("UserEmail")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("SessionId", "UserId")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("PParticipant", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.PaymentMethod", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
|
@ -6213,6 +6517,17 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("Entity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.AttendanceRecord", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.ClassSession", "Session")
|
||||
.WithMany("AttendanceRecords")
|
||||
.HasForeignKey("SessionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.BankAccount", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
|
||||
|
|
@ -6241,6 +6556,17 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.ChatMessage", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.ClassSession", "Session")
|
||||
.WithMany("ChatMessages")
|
||||
.HasForeignKey("SessionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.City", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.Country", "Country")
|
||||
|
|
@ -6304,6 +6630,17 @@ namespace Kurs.Platform.Migrations
|
|||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.Participant", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.ClassSession", "Session")
|
||||
.WithMany("Participants")
|
||||
.HasForeignKey("SessionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.ReportGenerated", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.ReportTemplate", "Template")
|
||||
|
|
@ -6565,6 +6902,15 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("Districts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.ClassSession", b =>
|
||||
{
|
||||
b.Navigation("AttendanceRecords");
|
||||
|
||||
b.Navigation("ChatMessages");
|
||||
|
||||
b.Navigation("Participants");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.Country", b =>
|
||||
{
|
||||
b.Navigation("Cities");
|
||||
|
|
|
|||
175
api/src/Kurs.Platform.HttpApi.Host/VirtualClass/ClassroomHub.cs
Normal file
175
api/src/Kurs.Platform.HttpApi.Host/VirtualClass/ClassroomHub.cs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
using Kurs.Platform.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Volo.Abp.Guids;
|
||||
|
||||
namespace Kurs.Platform.SignalR.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class ClassroomHub : Hub
|
||||
{
|
||||
private readonly IRepository<ClassSession, Guid> _classSessionRepository;
|
||||
private readonly IRepository<Participant, Guid> _participantRepository;
|
||||
private readonly IRepository<ChatMessage, Guid> _chatMessageRepository;
|
||||
private readonly ILogger<ClassroomHub> _logger;
|
||||
private readonly IGuidGenerator _guidGenerator;
|
||||
|
||||
public ClassroomHub(
|
||||
IRepository<ClassSession, Guid> classSessionRepository,
|
||||
IRepository<Participant, Guid> participantRepository,
|
||||
IRepository<ChatMessage, Guid> chatMessageRepository,
|
||||
ILogger<ClassroomHub> logger,
|
||||
IGuidGenerator guidGenerator)
|
||||
{
|
||||
_classSessionRepository = classSessionRepository;
|
||||
_participantRepository = participantRepository;
|
||||
_chatMessageRepository = chatMessageRepository;
|
||||
_logger = logger;
|
||||
_guidGenerator = guidGenerator;
|
||||
}
|
||||
|
||||
public async Task JoinClassAsync(Guid sessionId, string userName)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(sessionId);
|
||||
|
||||
if (!classSession.CanJoin())
|
||||
{
|
||||
await Clients.Caller.SendAsync("Error", "Cannot join this class at this time");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to SignalR group
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString());
|
||||
|
||||
// Update participant connection
|
||||
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == sessionId && x.UserId == Context.UserIdentifier.To<Guid>()
|
||||
);
|
||||
|
||||
if (participant != null)
|
||||
{
|
||||
participant.UpdateConnectionId(Context.ConnectionId);
|
||||
await _participantRepository.UpdateAsync(participant);
|
||||
}
|
||||
|
||||
// Notify others
|
||||
await Clients.Group(sessionId.ToString())
|
||||
.SendAsync("ParticipantJoined", Context.UserIdentifier, userName);
|
||||
_logger.LogInformation($"User {userName} joined class {sessionId}");
|
||||
}
|
||||
|
||||
public async Task LeaveClassAsync(Guid sessionId)
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, sessionId.ToString());
|
||||
await Clients.Group(sessionId.ToString())
|
||||
.SendAsync("ParticipantLeft", Context.UserIdentifier);
|
||||
_logger.LogInformation($"User {Context.UserIdentifier} left class {sessionId}");
|
||||
}
|
||||
|
||||
public async Task SendSignalingMessageAsync(SignalingMessageDto message)
|
||||
{
|
||||
// Forward WebRTC signaling messages
|
||||
await Clients.User(message.ToUserId)
|
||||
.SendAsync("ReceiveSignalingMessage", message);
|
||||
_logger.LogInformation($"Signaling message sent from {message.FromUserId} to {message.ToUserId}");
|
||||
}
|
||||
|
||||
public async Task SendChatMessageAsync(Guid sessionId, string message)
|
||||
{
|
||||
var userId = Context.UserIdentifier.To<Guid>();
|
||||
var userName = Context.User?.Identity?.Name ?? "Unknown";
|
||||
|
||||
// Check if user is teacher
|
||||
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == sessionId && x.UserId == userId
|
||||
);
|
||||
|
||||
var isTeacher = participant?.IsTeacher ?? false;
|
||||
|
||||
// Save message to database
|
||||
var chatMessage = new ChatMessage(
|
||||
_guidGenerator.Create(),
|
||||
sessionId,
|
||||
userId,
|
||||
userName,
|
||||
message,
|
||||
isTeacher
|
||||
);
|
||||
|
||||
await _chatMessageRepository.InsertAsync(chatMessage);
|
||||
|
||||
// Send to all participants
|
||||
await Clients.Group(sessionId.ToString())
|
||||
.SendAsync("ChatMessage", new
|
||||
{
|
||||
Id = chatMessage.Id,
|
||||
SenderId = chatMessage.SenderId,
|
||||
SenderName = chatMessage.SenderName,
|
||||
Message = chatMessage.Message,
|
||||
Timestamp = chatMessage.Timestamp,
|
||||
IsTeacher = chatMessage.IsTeacher
|
||||
});
|
||||
}
|
||||
|
||||
public async Task MuteParticipantAsync(Guid sessionId, Guid participantId, bool isMuted)
|
||||
{
|
||||
var teacherParticipant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == sessionId && x.UserId == Context.UserIdentifier.To<Guid>()
|
||||
);
|
||||
|
||||
if (teacherParticipant?.IsTeacher != true)
|
||||
{
|
||||
await Clients.Caller.SendAsync("Error", "Only teachers can mute participants");
|
||||
return;
|
||||
}
|
||||
|
||||
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == sessionId && x.UserId == participantId
|
||||
);
|
||||
|
||||
if (participant != null)
|
||||
{
|
||||
if (isMuted)
|
||||
participant.MuteAudio();
|
||||
else
|
||||
participant.UnmuteAudio();
|
||||
|
||||
await _participantRepository.UpdateAsync(participant);
|
||||
|
||||
// Notify the participant and others
|
||||
await Clients.Group(sessionId.ToString())
|
||||
.SendAsync("ParticipantMuted", participantId, isMuted);
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
// Handle cleanup when user disconnects
|
||||
var userId = Context.UserIdentifier?.To<Guid>();
|
||||
if (userId.HasValue)
|
||||
{
|
||||
var participants = await _participantRepository.GetListAsync(
|
||||
x => x.UserId == userId.Value && x.ConnectionId == Context.ConnectionId
|
||||
);
|
||||
foreach (var participant in participants)
|
||||
{
|
||||
await Clients.Group(participant.SessionId.ToString())
|
||||
.SendAsync("ParticipantLeft", userId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public class SignalingMessageDto
|
||||
{
|
||||
public string Type { get; set; } // offer, answer, ice-candidate
|
||||
public string FromUserId { get; set; }
|
||||
public string ToUserId { get; set; }
|
||||
public object Data { get; set; }
|
||||
}
|
||||
Loading…
Reference in a new issue