diff --git a/DadmehrGostar.sln b/DadmehrGostar.sln index 342cf0ef..04c70593 100644 --- a/DadmehrGostar.sln +++ b/DadmehrGostar.sln @@ -89,6 +89,9 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundInstitutionContract.Task", "BackgroundInstitutionContract\BackgroundInstitutionContract.Task\BackgroundInstitutionContract.Task.csproj", "{F78FBB92-294B-88BA-168D-F0C578B0D7D6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProgramManager", "ProgramManager", "{67AFF7B6-4C4F-464C-A90D-9BDB644D83A9}" + ProjectSection(SolutionItems) = preProject + ProgramManager\TASKCHAT_ARCHITECTURE.md = ProgramManager\TASKCHAT_ARCHITECTURE.md + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{48F6F6A5-7340-42F8-9216-BEB7A4B7D5A1}" EndProject diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs new file mode 100644 index 00000000..b132ac62 --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs @@ -0,0 +1,200 @@ +using GozareshgirProgramManager.Domain._Common; +using GozareshgirProgramManager.Domain._Common.Exceptions; +using GozareshgirProgramManager.Domain.TaskChatAgg.Events; +using MessageType = GozareshgirProgramManager.Domain.TaskChatAgg.Enums.MessageType; + +namespace GozareshgirProgramManager.Domain.TaskChatAgg.Entities; + +/// +/// پیام چت تسک - Aggregate Root +/// هر کسی که به تسک دسترسی داشته باشد می‌تواند پیام‌ها را ببیند و ارسال کند +/// نیازی به مدیریت گروه و ممبر نیست چون دسترسی از طریق خود تسک کنترل می‌شود +/// +public class TaskChatMessage : EntityBase +{ + private TaskChatMessage() + { + } + + public TaskChatMessage(Guid taskId, long senderUserId, MessageType messageType, string? textContent = null) + { + TaskId = taskId; + SenderUserId = senderUserId; + MessageType = messageType; + TextContent = textContent; + IsEdited = false; + IsDeleted = false; + IsPinned = false; + + ValidateMessage(); + AddDomainEvent(new TaskChatMessageSentEvent(Id, taskId, senderUserId, messageType)); + } + + // Reference به Task (Foreign Key فقط - بدون Navigation Property برای جلوگیری از coupling) + public Guid TaskId { get; private set; } + + public long SenderUserId { get; private set; } + + public MessageType MessageType { get; private set; } + + // محتوای متنی (برای پیام‌های Text و Caption برای فایل/تصویر) + public string? TextContent { get; private set; } + + // اطلاعات فایل (برای پیام‌های File, Voice, Image, Video) + public string? FileUrl { get; private set; } + public string? FileName { get; private set; } + public long? FileSizeBytes { get; private set; } + public string? FileMimeType { get; private set; } + + // اطلاعات صوت (برای Voice) + public int? VoiceDurationSeconds { get; private set; } + + // پیام Reply + public Guid? ReplyToMessageId { get; private set; } + public TaskChatMessage? ReplyToMessage { get; private set; } + + // وضعیت پیام + public bool IsEdited { get; private set; } + public DateTime? EditedDate { get; private set; } + public bool IsDeleted { get; private set; } + public DateTime? DeletedDate { get; private set; } + public bool IsPinned { get; private set; } + public DateTime? PinnedDate { get; private set; } + public long? PinnedByUserId { get; private set; } + + private void ValidateMessage() + { + if (MessageType == MessageType.Text && string.IsNullOrWhiteSpace(TextContent)) + { + throw new BadRequestException("پیام متنی نمی‌تواند خالی باشد"); + } + + if ((MessageType == MessageType.File || MessageType == MessageType.Voice || + MessageType == MessageType.Image || MessageType == MessageType.Video) + && string.IsNullOrWhiteSpace(FileUrl)) + { + throw new BadRequestException("آدرس فایل نمی‌تواند خالی باشد"); + } + + if (MessageType == MessageType.Voice && VoiceDurationSeconds == null) + { + throw new BadRequestException("مدت زمان صدا باید مشخص شود"); + } + } + + public void SetFileInfo(string fileUrl, string fileName, long fileSizeBytes, string mimeType) + { + if (MessageType != MessageType.File && MessageType != MessageType.Image && + MessageType != MessageType.Video && MessageType != MessageType.Voice) + { + throw new BadRequestException("فقط می‌توان برای پیام‌های فایل، تصویر، ویدیو و صدا اطلاعات فایل تنظیم کرد"); + } + + FileUrl = fileUrl; + FileName = fileName; + FileSizeBytes = fileSizeBytes; + FileMimeType = mimeType; + } + + public void SetVoiceDuration(int durationSeconds) + { + if (MessageType != MessageType.Voice) + { + throw new BadRequestException("فقط می‌توان برای پیام‌های صوتی مدت زمان تنظیم کرد"); + } + + if (durationSeconds <= 0) + { + throw new BadRequestException("مدت زمان صدا باید بیشتر از صفر باشد"); + } + + VoiceDurationSeconds = durationSeconds; + } + + public void EditMessage(string newTextContent, long editorUserId) + { + if (IsDeleted) + { + throw new BadRequestException("نمی‌توان پیام حذف شده را ویرایش کرد"); + } + + if (editorUserId != SenderUserId) + { + throw new BadRequestException("فقط فرستنده می‌تواند پیام را ویرایش کند"); + } + + if (MessageType != MessageType.Text) + { + throw new BadRequestException("فقط پیام‌های متنی قابل ویرایش هستند"); + } + + if (string.IsNullOrWhiteSpace(newTextContent)) + { + throw new BadRequestException("محتوای پیام نمی‌تواند خالی باشد"); + } + + TextContent = newTextContent; + IsEdited = true; + EditedDate = DateTime.Now; + AddDomainEvent(new TaskChatMessageEditedEvent(Id, TaskId, editorUserId)); + } + + public void DeleteMessage(long deleterUserId) + { + if (IsDeleted) + { + throw new BadRequestException("پیام قبلاً حذف شده است"); + } + + if (deleterUserId != SenderUserId) + { + throw new BadRequestException("فقط فرستنده می‌تواند پیام را حذف کند"); + } + + IsDeleted = true; + DeletedDate = DateTime.Now; + AddDomainEvent(new TaskChatMessageDeletedEvent(Id, TaskId, deleterUserId)); + } + + public void PinMessage(long pinnerUserId) + { + if (IsDeleted) + { + throw new BadRequestException("نمی‌توان پیام حذف شده را پین کرد"); + } + + if (IsPinned) + { + throw new BadRequestException("این پیام قبلاً پین شده است"); + } + + IsPinned = true; + PinnedDate = DateTime.Now; + PinnedByUserId = pinnerUserId; + AddDomainEvent(new TaskChatMessagePinnedEvent(Id, TaskId, pinnerUserId)); + } + + public void UnpinMessage(long unpinnerUserId) + { + if (!IsPinned) + { + throw new BadRequestException("این پیام پین نشده است"); + } + + IsPinned = false; + PinnedDate = null; + PinnedByUserId = null; + AddDomainEvent(new TaskChatMessageUnpinnedEvent(Id, TaskId, unpinnerUserId)); + } + + public void SetReplyTo(Guid replyToMessageId) + { + ReplyToMessageId = replyToMessageId; + } + + public bool IsSentBy(long userId) + { + return SenderUserId == userId; + } +} + diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs new file mode 100644 index 00000000..4b30dcd4 --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs @@ -0,0 +1,14 @@ +namespace GozareshgirProgramManager.Domain.TaskChatAgg.Enums; + +/// +/// نوع پیام در چت تسک +/// +public enum MessageType +{ + Text = 1, // پیام متنی + File = 2, // فایل (اسناد، PDF، و غیره) + Image = 3, // تصویر + Voice = 4, // پیام صوتی + Video = 5, // ویدیو +} + diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs new file mode 100644 index 00000000..9493fa55 --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs @@ -0,0 +1,31 @@ +using GozareshgirProgramManager.Domain._Common; +using GozareshgirProgramManager.Domain.TaskChatAgg.Enums; + +namespace GozareshgirProgramManager.Domain.TaskChatAgg.Events; + +// Message Events +public record TaskChatMessageSentEvent(Guid MessageId, Guid TaskId, long SenderUserId, MessageType MessageType) : IDomainEvent +{ + public DateTime OccurredOn { get; init; } = DateTime.Now; +} + +public record TaskChatMessageEditedEvent(Guid MessageId, Guid TaskId, long EditorUserId) : IDomainEvent +{ + public DateTime OccurredOn { get; init; } = DateTime.Now; +} + +public record TaskChatMessageDeletedEvent(Guid MessageId, Guid TaskId, long DeleterUserId) : IDomainEvent +{ + public DateTime OccurredOn { get; init; } = DateTime.Now; +} + +public record TaskChatMessagePinnedEvent(Guid MessageId, Guid TaskId, long PinnerUserId) : IDomainEvent +{ + public DateTime OccurredOn { get; init; } = DateTime.Now; +} + +public record TaskChatMessageUnpinnedEvent(Guid MessageId, Guid TaskId, long UnpinnerUserId) : IDomainEvent +{ + public DateTime OccurredOn { get; init; } = DateTime.Now; +} + diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs new file mode 100644 index 00000000..8ac9b3b8 --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs @@ -0,0 +1,75 @@ +using GozareshgirProgramManager.Domain.TaskChatAgg.Entities; + +namespace GozareshgirProgramManager.Domain.TaskChatAgg.Repositories; + +/// +/// Repository برای مدیریت پیام‌های چت تسک +/// +public interface ITaskChatMessageRepository +{ + /// + /// دریافت پیام بر اساس شناسه + /// + Task GetByIdAsync(Guid messageId); + + /// + /// دریافت لیست پیام‌های یک تسک (با صفحه‌بندی) + /// + Task> GetTaskMessagesAsync(Guid taskId, int pageNumber, int pageSize); + + /// + /// دریافت تعداد کل پیام‌های یک تسک + /// + Task GetTaskMessageCountAsync(Guid taskId); + + /// + /// دریافت پیام‌های پین شده یک تسک + /// + Task> GetPinnedMessagesAsync(Guid taskId); + + /// + /// دریافت آخرین پیام یک تسک + /// + Task GetLastMessageAsync(Guid taskId); + + /// + /// جستجو در پیام‌های یک تسک + /// + Task> SearchMessagesAsync(Guid taskId, string searchText, int pageNumber, int pageSize); + + /// + /// دریافت پیام‌های یک کاربر خاص در یک تسک + /// + Task> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize); + + /// + /// دریافت پیام‌های با فایل (تصویر، ویدیو، فایل و...) + /// + Task> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize); + + /// + /// اضافه کردن پیام جدید + /// + Task AddAsync(TaskChatMessage message); + + /// + /// به‌روزرسانی پیام + /// + Task UpdateAsync(TaskChatMessage message); + + /// + /// حذف فیزیکی پیام (در صورت نیاز - معمولاً استفاده نمی‌شود) + /// + Task DeleteAsync(TaskChatMessage message); + + /// + /// ذخیره تغییرات + /// + Task SaveChangesAsync(); + + /// + /// بررسی وجود پیام + /// + Task ExistsAsync(Guid messageId); +} +