feat: add file management entities and services for chat message handling
This commit is contained in:
@@ -90,7 +90,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundInstitutionContra
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProgramManager", "ProgramManager", "{67AFF7B6-4C4F-464C-A90D-9BDB644D83A9}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProgramManager", "ProgramManager", "{67AFF7B6-4C4F-464C-A90D-9BDB644D83A9}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
ProgramManager\TASKCHAT_ARCHITECTURE.md = ProgramManager\TASKCHAT_ARCHITECTURE.md
|
ProgramManager\appsettings.FileStorage.json = ProgramManager\appsettings.FileStorage.json
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{48F6F6A5-7340-42F8-9216-BEB7A4B7D5A1}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{48F6F6A5-7340-42F8-9216-BEB7A4B7D5A1}"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||||
<PackageReference Include="MediatR" Version="14.0.0" />
|
<PackageReference Include="MediatR" Version="14.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -18,4 +19,10 @@
|
|||||||
<ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Http.Features">
|
||||||
|
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.AspNetCore.Http.Features.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using GozareshgirProgramManager.Application._Common.Models;
|
||||||
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.DeleteMessage;
|
||||||
|
|
||||||
|
public record DeleteMessageCommand(Guid MessageId) : IBaseCommand;
|
||||||
|
|
||||||
|
public class DeleteMessageCommandHandler : IBaseCommandHandler<DeleteMessageCommand>
|
||||||
|
{
|
||||||
|
private readonly ITaskChatMessageRepository _repository;
|
||||||
|
|
||||||
|
public DeleteMessageCommandHandler(ITaskChatMessageRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OperationResult> Handle(DeleteMessageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// TODO: Get current user
|
||||||
|
var currentUserId = 1L;
|
||||||
|
|
||||||
|
var message = await _repository.GetByIdAsync(request.MessageId);
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
return OperationResult.NotFound("پیام یافت نشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
message.DeleteMessage(currentUserId);
|
||||||
|
await _repository.UpdateAsync(message);
|
||||||
|
await _repository.SaveChangesAsync();
|
||||||
|
|
||||||
|
// TODO: SignalR notification
|
||||||
|
|
||||||
|
return OperationResult.Success();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return OperationResult.ValidationError(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
|
using GozareshgirProgramManager.Application._Common.Models;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.EditMessage;
|
||||||
|
|
||||||
|
public record EditMessageCommand(
|
||||||
|
Guid MessageId,
|
||||||
|
string NewTextContent
|
||||||
|
) : IBaseCommand;
|
||||||
|
|
||||||
|
public class EditMessageCommandHandler : IBaseCommandHandler<EditMessageCommand>
|
||||||
|
{
|
||||||
|
private readonly ITaskChatMessageRepository _repository;
|
||||||
|
|
||||||
|
public EditMessageCommandHandler(ITaskChatMessageRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OperationResult> Handle(EditMessageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// TODO: Get current user
|
||||||
|
var currentUserId = 1L;
|
||||||
|
|
||||||
|
var message = await _repository.GetByIdAsync(request.MessageId);
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
return OperationResult.NotFound("پیام یافت نشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
message.EditMessage(request.NewTextContent, currentUserId);
|
||||||
|
await _repository.UpdateAsync(message);
|
||||||
|
await _repository.SaveChangesAsync();
|
||||||
|
|
||||||
|
// TODO: SignalR notification
|
||||||
|
|
||||||
|
return OperationResult.Success();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return OperationResult.ValidationError(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
|
using GozareshgirProgramManager.Application._Common.Models;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.PinMessage;
|
||||||
|
|
||||||
|
public record PinMessageCommand(Guid MessageId) : IBaseCommand;
|
||||||
|
|
||||||
|
public class PinMessageCommandHandler : IBaseCommandHandler<PinMessageCommand>
|
||||||
|
{
|
||||||
|
private readonly ITaskChatMessageRepository _repository;
|
||||||
|
|
||||||
|
public PinMessageCommandHandler(ITaskChatMessageRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OperationResult> Handle(PinMessageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// TODO: Get current user
|
||||||
|
var currentUserId = 1L;
|
||||||
|
|
||||||
|
var message = await _repository.GetByIdAsync(request.MessageId);
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
return OperationResult.NotFound("پیام یافت نشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
message.PinMessage(currentUserId);
|
||||||
|
await _repository.UpdateAsync(message);
|
||||||
|
await _repository.SaveChangesAsync();
|
||||||
|
|
||||||
|
return OperationResult.Success();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return OperationResult.ValidationError(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
|
using GozareshgirProgramManager.Application._Common.Models;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
|
||||||
|
using GozareshgirProgramManager.Application.Services.FileManagement;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||||
|
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.SendMessage;
|
||||||
|
|
||||||
|
public record SendMessageCommand(
|
||||||
|
Guid TaskId,
|
||||||
|
MessageType MessageType,
|
||||||
|
string? TextContent,
|
||||||
|
IFormFile? File,
|
||||||
|
Guid? ReplyToMessageId
|
||||||
|
) : IBaseCommand<MessageDto>;
|
||||||
|
|
||||||
|
public class SendMessageCommandHandler : IBaseCommandHandler<SendMessageCommand, MessageDto>
|
||||||
|
{
|
||||||
|
private readonly ITaskChatMessageRepository _messageRepository;
|
||||||
|
private readonly IUploadedFileRepository _fileRepository;
|
||||||
|
private readonly IProjectTaskRepository _taskRepository;
|
||||||
|
private readonly IFileStorageService _fileStorageService;
|
||||||
|
private readonly IThumbnailGeneratorService _thumbnailService;
|
||||||
|
|
||||||
|
public SendMessageCommandHandler(
|
||||||
|
ITaskChatMessageRepository messageRepository,
|
||||||
|
IUploadedFileRepository fileRepository,
|
||||||
|
IProjectTaskRepository taskRepository,
|
||||||
|
IFileStorageService fileStorageService,
|
||||||
|
IThumbnailGeneratorService thumbnailService)
|
||||||
|
{
|
||||||
|
_messageRepository = messageRepository;
|
||||||
|
_fileRepository = fileRepository;
|
||||||
|
_taskRepository = taskRepository;
|
||||||
|
_fileStorageService = fileStorageService;
|
||||||
|
_thumbnailService = thumbnailService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OperationResult<MessageDto>> Handle(SendMessageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var currentUserId = 1L;
|
||||||
|
|
||||||
|
var task = await _taskRepository.GetByIdAsync(request.TaskId, cancellationToken);
|
||||||
|
if (task == null)
|
||||||
|
{
|
||||||
|
return OperationResult<MessageDto>.NotFound("تسک یافت نشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid? uploadedFileId = null;
|
||||||
|
if (request.File != null)
|
||||||
|
{
|
||||||
|
if (request.File.Length == 0)
|
||||||
|
{
|
||||||
|
return OperationResult<MessageDto>.ValidationError("فایل خالی است");
|
||||||
|
}
|
||||||
|
|
||||||
|
const long maxFileSize = 100 * 1024 * 1024;
|
||||||
|
if (request.File.Length > maxFileSize)
|
||||||
|
{
|
||||||
|
return OperationResult<MessageDto>.ValidationError("حجم فایل بیش از حد مجاز است (حداکثر 100MB)");
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileType = DetectFileType(request.File.ContentType, Path.GetExtension(request.File.FileName));
|
||||||
|
|
||||||
|
var uploadedFile = new UploadedFile(
|
||||||
|
originalFileName: request.File.FileName,
|
||||||
|
fileSizeBytes: request.File.Length,
|
||||||
|
mimeType: request.File.ContentType,
|
||||||
|
fileType: fileType,
|
||||||
|
category: FileCategory.TaskChatMessage,
|
||||||
|
uploadedByUserId: currentUserId,
|
||||||
|
storageProvider: StorageProvider.LocalFileSystem
|
||||||
|
);
|
||||||
|
|
||||||
|
await _fileRepository.AddAsync(uploadedFile);
|
||||||
|
await _fileRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = request.File.OpenReadStream();
|
||||||
|
var uploadResult = await _fileStorageService.UploadAsync(
|
||||||
|
stream,
|
||||||
|
uploadedFile.UniqueFileName,
|
||||||
|
"TaskChatMessage"
|
||||||
|
);
|
||||||
|
|
||||||
|
uploadedFile.CompleteUpload(uploadResult.StoragePath, uploadResult.StorageUrl);
|
||||||
|
|
||||||
|
if (fileType == FileType.Image)
|
||||||
|
{
|
||||||
|
var dimensions = await _thumbnailService.GetImageDimensionsAsync(uploadResult.StoragePath);
|
||||||
|
if (dimensions.HasValue)
|
||||||
|
{
|
||||||
|
uploadedFile.SetImageDimensions(dimensions.Value.Width, dimensions.Value.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
var thumbnail = await _thumbnailService.GenerateImageThumbnailAsync(uploadResult.StoragePath);
|
||||||
|
if (thumbnail.HasValue)
|
||||||
|
{
|
||||||
|
uploadedFile.SetThumbnail(thumbnail.Value.ThumbnailUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _fileRepository.UpdateAsync(uploadedFile);
|
||||||
|
await _fileRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
uploadedFileId = uploadedFile.Id;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await _fileRepository.DeleteAsync(uploadedFile);
|
||||||
|
await _fileRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
return OperationResult<MessageDto>.ValidationError($"خطا در آپلود فایل: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = new TaskChatMessage(
|
||||||
|
taskId: request.TaskId,
|
||||||
|
senderUserId: currentUserId,
|
||||||
|
messageType: request.MessageType,
|
||||||
|
textContent: request.TextContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uploadedFileId.HasValue)
|
||||||
|
{
|
||||||
|
message.SetFile(uploadedFileId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.ReplyToMessageId.HasValue)
|
||||||
|
{
|
||||||
|
message.SetReplyTo(request.ReplyToMessageId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _messageRepository.AddAsync(message);
|
||||||
|
await _messageRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (uploadedFileId.HasValue)
|
||||||
|
{
|
||||||
|
var file = await _fileRepository.GetByIdAsync(uploadedFileId.Value);
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
file.SetReference("TaskChatMessage", message.Id.ToString());
|
||||||
|
await _fileRepository.UpdateAsync(file);
|
||||||
|
await _fileRepository.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dto = new MessageDto
|
||||||
|
{
|
||||||
|
Id = message.Id,
|
||||||
|
TaskId = message.TaskId,
|
||||||
|
SenderUserId = message.SenderUserId,
|
||||||
|
SenderName = "کاربر",
|
||||||
|
MessageType = message.MessageType.ToString(),
|
||||||
|
TextContent = message.TextContent,
|
||||||
|
ReplyToMessageId = message.ReplyToMessageId,
|
||||||
|
IsEdited = message.IsEdited,
|
||||||
|
IsPinned = message.IsPinned,
|
||||||
|
CreationDate = message.CreationDate,
|
||||||
|
IsMine = true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (uploadedFileId.HasValue)
|
||||||
|
{
|
||||||
|
var file = await _fileRepository.GetByIdAsync(uploadedFileId.Value);
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
dto.File = new MessageFileDto
|
||||||
|
{
|
||||||
|
Id = file.Id,
|
||||||
|
FileName = file.OriginalFileName,
|
||||||
|
FileUrl = file.StorageUrl ?? "",
|
||||||
|
FileSizeBytes = file.FileSizeBytes,
|
||||||
|
FileType = file.FileType.ToString(),
|
||||||
|
ThumbnailUrl = file.ThumbnailUrl,
|
||||||
|
ImageWidth = file.ImageWidth,
|
||||||
|
ImageHeight = file.ImageHeight,
|
||||||
|
DurationSeconds = file.DurationSeconds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return OperationResult<MessageDto>.Success(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileType DetectFileType(string mimeType, string extension)
|
||||||
|
{
|
||||||
|
if (mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return FileType.Image;
|
||||||
|
|
||||||
|
if (mimeType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return FileType.Video;
|
||||||
|
|
||||||
|
if (mimeType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return FileType.Audio;
|
||||||
|
|
||||||
|
if (new[] { ".zip", ".rar", ".7z", ".tar", ".gz" }.Contains(extension.ToLower()))
|
||||||
|
return FileType.Archive;
|
||||||
|
|
||||||
|
return FileType.Document;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
|
using GozareshgirProgramManager.Application._Common.Models;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.UnpinMessage;
|
||||||
|
|
||||||
|
public record UnpinMessageCommand(Guid MessageId) : IBaseCommand;
|
||||||
|
|
||||||
|
public class UnpinMessageCommandHandler : IBaseCommandHandler<UnpinMessageCommand>
|
||||||
|
{
|
||||||
|
private readonly ITaskChatMessageRepository _repository;
|
||||||
|
|
||||||
|
public UnpinMessageCommandHandler(ITaskChatMessageRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OperationResult> Handle(UnpinMessageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// TODO: Get current user
|
||||||
|
var currentUserId = 1L;
|
||||||
|
|
||||||
|
var message = await _repository.GetByIdAsync(request.MessageId);
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
return OperationResult.NotFound("پیام یافت نشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
message.UnpinMessage(currentUserId);
|
||||||
|
await _repository.UpdateAsync(message);
|
||||||
|
await _repository.SaveChangesAsync();
|
||||||
|
|
||||||
|
return OperationResult.Success();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return OperationResult.ValidationError(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
namespace GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
|
||||||
|
|
||||||
|
public class SendMessageDto
|
||||||
|
{
|
||||||
|
public Guid TaskId { get; set; }
|
||||||
|
public string MessageType { get; set; } = string.Empty; // "Text", "File", "Image", "Voice", "Video"
|
||||||
|
public string? TextContent { get; set; }
|
||||||
|
public Guid? FileId { get; set; }
|
||||||
|
public Guid? ReplyToMessageId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessageDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TaskId { get; set; }
|
||||||
|
public long SenderUserId { get; set; }
|
||||||
|
public string SenderName { get; set; } = string.Empty;
|
||||||
|
public string MessageType { get; set; } = string.Empty;
|
||||||
|
public string? TextContent { get; set; }
|
||||||
|
public MessageFileDto? File { get; set; }
|
||||||
|
public Guid? ReplyToMessageId { get; set; }
|
||||||
|
public MessageDto? ReplyToMessage { get; set; }
|
||||||
|
public bool IsEdited { get; set; }
|
||||||
|
public DateTime? EditedDate { get; set; }
|
||||||
|
public bool IsPinned { get; set; }
|
||||||
|
public DateTime? PinnedDate { get; set; }
|
||||||
|
public long? PinnedByUserId { get; set; }
|
||||||
|
public DateTime CreationDate { get; set; }
|
||||||
|
public bool IsMine { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessageFileDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
public string FileUrl { get; set; } = string.Empty;
|
||||||
|
public long FileSizeBytes { get; set; }
|
||||||
|
public string FileType { get; set; } = string.Empty;
|
||||||
|
public string? ThumbnailUrl { get; set; }
|
||||||
|
public int? ImageWidth { get; set; }
|
||||||
|
public int? ImageHeight { get; set; }
|
||||||
|
public int? DurationSeconds { get; set; }
|
||||||
|
|
||||||
|
public string FileSizeFormatted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
const long kb = 1024;
|
||||||
|
const long mb = kb * 1024;
|
||||||
|
const long gb = mb * 1024;
|
||||||
|
|
||||||
|
if (FileSizeBytes >= gb)
|
||||||
|
return $"{FileSizeBytes / (double)gb:F2} GB";
|
||||||
|
if (FileSizeBytes >= mb)
|
||||||
|
return $"{FileSizeBytes / (double)mb:F2} MB";
|
||||||
|
if (FileSizeBytes >= kb)
|
||||||
|
return $"{FileSizeBytes / (double)kb:F2} KB";
|
||||||
|
|
||||||
|
return $"{FileSizeBytes} Bytes";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
|
using GozareshgirProgramManager.Application._Common.Models;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetMessages;
|
||||||
|
|
||||||
|
public record GetMessagesQuery(
|
||||||
|
Guid TaskId,
|
||||||
|
int Page = 1,
|
||||||
|
int PageSize = 50
|
||||||
|
) : IBaseQuery<PaginationResult<MessageDto>>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, PaginationResult<MessageDto>>
|
||||||
|
{
|
||||||
|
private readonly IProgramManagerDbContext _context;
|
||||||
|
private readonly IAuthHelper _authHelper;
|
||||||
|
|
||||||
|
public GetMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_authHelper = authHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OperationResult<PaginationResult<MessageDto>>> Handle(GetMessagesQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var currentUserId = _authHelper.GetCurrentUserId();
|
||||||
|
|
||||||
|
var skip = (request.Page - 1) * request.PageSize;
|
||||||
|
|
||||||
|
var query = _context.TaskChatMessages
|
||||||
|
.Where(m => m.TaskId == request.TaskId && !m.IsDeleted)
|
||||||
|
.Include(m => m.ReplyToMessage)
|
||||||
|
.OrderBy(m => m.CreationDate);
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var messages = await query
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(request.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var messageDtos = new List<MessageDto>();
|
||||||
|
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
var dto = new MessageDto
|
||||||
|
{
|
||||||
|
Id = message.Id,
|
||||||
|
TaskId = message.TaskId,
|
||||||
|
SenderUserId = message.SenderUserId,
|
||||||
|
SenderName = "کاربر", // TODO: از User service
|
||||||
|
MessageType = message.MessageType.ToString(),
|
||||||
|
TextContent = message.TextContent,
|
||||||
|
ReplyToMessageId = message.ReplyToMessageId,
|
||||||
|
IsEdited = message.IsEdited,
|
||||||
|
EditedDate = message.EditedDate,
|
||||||
|
IsPinned = message.IsPinned,
|
||||||
|
PinnedDate = message.PinnedDate,
|
||||||
|
PinnedByUserId = message.PinnedByUserId,
|
||||||
|
CreationDate = message.CreationDate,
|
||||||
|
IsMine = message.SenderUserId == currentUserId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (message.ReplyToMessage != null)
|
||||||
|
{
|
||||||
|
dto.ReplyToMessage = new MessageDto
|
||||||
|
{
|
||||||
|
Id = message.ReplyToMessage.Id,
|
||||||
|
SenderUserId = message.ReplyToMessage.SenderUserId,
|
||||||
|
SenderName = "کاربر",
|
||||||
|
TextContent = message.ReplyToMessage.TextContent,
|
||||||
|
CreationDate = message.ReplyToMessage.CreationDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.FileId.HasValue)
|
||||||
|
{
|
||||||
|
var file = await _context.UploadedFiles.FirstOrDefaultAsync(f => f.Id == message.FileId.Value, cancellationToken);
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
dto.File = new MessageFileDto
|
||||||
|
{
|
||||||
|
Id = file.Id,
|
||||||
|
FileName = file.OriginalFileName,
|
||||||
|
FileUrl = file.StorageUrl ?? "",
|
||||||
|
FileSizeBytes = file.FileSizeBytes,
|
||||||
|
FileType = file.FileType.ToString(),
|
||||||
|
ThumbnailUrl = file.ThumbnailUrl,
|
||||||
|
ImageWidth = file.ImageWidth,
|
||||||
|
ImageHeight = file.ImageHeight,
|
||||||
|
DurationSeconds = file.DurationSeconds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messageDtos.Add(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = new PaginationResult<MessageDto>()
|
||||||
|
{
|
||||||
|
List = messageDtos,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
return OperationResult<PaginationResult<MessageDto>>.Success(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
|
using GozareshgirProgramManager.Application._Common.Models;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetPinnedMessages;
|
||||||
|
|
||||||
|
public record GetPinnedMessagesQuery(Guid TaskId) : IBaseQuery<List<MessageDto>>;
|
||||||
|
|
||||||
|
public class GetPinnedMessagesQueryHandler : IBaseQueryHandler<GetPinnedMessagesQuery, List<MessageDto>>
|
||||||
|
{
|
||||||
|
private readonly IProgramManagerDbContext _context;
|
||||||
|
private readonly IAuthHelper _authHelper;
|
||||||
|
|
||||||
|
public GetPinnedMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_authHelper = authHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OperationResult<List<MessageDto>>> Handle(GetPinnedMessagesQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var currentUserId = _authHelper.GetCurrentUserId();
|
||||||
|
|
||||||
|
var messages = await _context.TaskChatMessages
|
||||||
|
.Where(m => m.TaskId == request.TaskId && m.IsPinned && !m.IsDeleted)
|
||||||
|
.Include(m => m.ReplyToMessage)
|
||||||
|
.OrderByDescending(m => m.PinnedDate)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var messageDtos = new List<MessageDto>();
|
||||||
|
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
var dto = new MessageDto
|
||||||
|
{
|
||||||
|
Id = message.Id,
|
||||||
|
TaskId = message.TaskId,
|
||||||
|
SenderUserId = message.SenderUserId,
|
||||||
|
SenderName = "کاربر",
|
||||||
|
MessageType = message.MessageType.ToString(),
|
||||||
|
TextContent = message.TextContent,
|
||||||
|
IsPinned = message.IsPinned,
|
||||||
|
PinnedDate = message.PinnedDate,
|
||||||
|
PinnedByUserId = message.PinnedByUserId,
|
||||||
|
CreationDate = message.CreationDate,
|
||||||
|
IsMine = message.SenderUserId == currentUserId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (message.FileId.HasValue)
|
||||||
|
{
|
||||||
|
var file = await _context.UploadedFiles.FirstOrDefaultAsync(f => f.Id == message.FileId.Value, cancellationToken);
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
dto.File = new MessageFileDto
|
||||||
|
{
|
||||||
|
Id = file.Id,
|
||||||
|
FileName = file.OriginalFileName,
|
||||||
|
FileUrl = file.StorageUrl ?? "",
|
||||||
|
FileSizeBytes = file.FileSizeBytes,
|
||||||
|
FileType = file.FileType.ToString(),
|
||||||
|
ThumbnailUrl = file.ThumbnailUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messageDtos.Add(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OperationResult<List<MessageDto>>.Success(messageDtos);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
|
using GozareshgirProgramManager.Application._Common.Models;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.SearchMessages;
|
||||||
|
|
||||||
|
public record SearchMessagesQuery(
|
||||||
|
Guid TaskId,
|
||||||
|
string SearchText,
|
||||||
|
int Page = 1,
|
||||||
|
int PageSize = 20
|
||||||
|
) : IBaseQuery<List<MessageDto>>;
|
||||||
|
|
||||||
|
public class SearchMessagesQueryHandler : IBaseQueryHandler<SearchMessagesQuery, List<MessageDto>>
|
||||||
|
{
|
||||||
|
private readonly IProgramManagerDbContext _context;
|
||||||
|
private readonly IAuthHelper _authHelper;
|
||||||
|
|
||||||
|
public SearchMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_authHelper = authHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OperationResult<List<MessageDto>>> Handle(SearchMessagesQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var currentUserId = _authHelper.GetCurrentUserId();
|
||||||
|
var skip = (request.Page - 1) * request.PageSize;
|
||||||
|
|
||||||
|
var messages = await _context.TaskChatMessages
|
||||||
|
.Where(m => m.TaskId == request.TaskId &&
|
||||||
|
m.TextContent != null &&
|
||||||
|
m.TextContent.Contains(request.SearchText) &&
|
||||||
|
!m.IsDeleted)
|
||||||
|
.Include(m => m.ReplyToMessage)
|
||||||
|
.OrderByDescending(m => m.CreationDate)
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(request.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var messageDtos = new List<MessageDto>();
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
var dto = new MessageDto
|
||||||
|
{
|
||||||
|
Id = message.Id,
|
||||||
|
TaskId = message.TaskId,
|
||||||
|
SenderUserId = message.SenderUserId,
|
||||||
|
SenderName = "کاربر",
|
||||||
|
MessageType = message.MessageType.ToString(),
|
||||||
|
TextContent = message.TextContent,
|
||||||
|
CreationDate = message.CreationDate,
|
||||||
|
IsMine = message.SenderUserId == currentUserId
|
||||||
|
};
|
||||||
|
|
||||||
|
messageDtos.Add(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OperationResult<List<MessageDto>>.Success(messageDtos);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Application.Services.FileManagement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس ذخیرهسازی فایل
|
||||||
|
/// </summary>
|
||||||
|
public interface IFileStorageService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// آپلود فایل
|
||||||
|
/// </summary>
|
||||||
|
Task<(string StoragePath, string StorageUrl)> UploadAsync(
|
||||||
|
Stream fileStream,
|
||||||
|
string uniqueFileName,
|
||||||
|
string category);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حذف فایل
|
||||||
|
/// </summary>
|
||||||
|
Task DeleteAsync(string storagePath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت فایل
|
||||||
|
/// </summary>
|
||||||
|
Task<Stream?> GetFileStreamAsync(string storagePath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بررسی وجود فایل
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> ExistsAsync(string storagePath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت URL فایل
|
||||||
|
/// </summary>
|
||||||
|
string GetFileUrl(string storagePath);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace GozareshgirProgramManager.Application.Services.FileManagement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس تولید thumbnail برای تصاویر و ویدیوها
|
||||||
|
/// </summary>
|
||||||
|
public interface IThumbnailGeneratorService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// تولید thumbnail برای تصویر
|
||||||
|
/// </summary>
|
||||||
|
Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync(
|
||||||
|
string imagePath,
|
||||||
|
int width = 200,
|
||||||
|
int height = 200);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تولید thumbnail برای ویدیو
|
||||||
|
/// </summary>
|
||||||
|
Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(
|
||||||
|
string videoPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حذف thumbnail
|
||||||
|
/// </summary>
|
||||||
|
Task DeleteThumbnailAsync(string thumbnailPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت ابعاد تصویر
|
||||||
|
/// </summary>
|
||||||
|
Task<(int Width, int Height)?> GetImageDimensionsAsync(string imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,8 @@ using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities;
|
|||||||
using GozareshgirProgramManager.Domain.SkillAgg.Entities;
|
using GozareshgirProgramManager.Domain.SkillAgg.Entities;
|
||||||
using GozareshgirProgramManager.Domain.UserAgg.Entities;
|
using GozareshgirProgramManager.Domain.UserAgg.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
|
||||||
|
|
||||||
namespace GozareshgirProgramManager.Application._Common.Interfaces;
|
namespace GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
|
|
||||||
@@ -26,6 +28,9 @@ public interface IProgramManagerDbContext
|
|||||||
|
|
||||||
DbSet<ProjectTask> ProjectTasks { get; set; }
|
DbSet<ProjectTask> ProjectTasks { get; set; }
|
||||||
|
|
||||||
|
DbSet<TaskChatMessage> TaskChatMessages { get; set; }
|
||||||
|
DbSet<UploadedFile> UploadedFiles { get; set; }
|
||||||
|
|
||||||
DbSet<Skill> Skills { get; set; }
|
DbSet<Skill> Skills { get; set; }
|
||||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
using GozareshgirProgramManager.Domain._Common;
|
||||||
|
using GozareshgirProgramManager.Domain._Common.Exceptions;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Events;
|
||||||
|
using FileType = GozareshgirProgramManager.Domain.FileManagementAgg.Enums.FileType;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فایل آپلود شده - Aggregate Root
|
||||||
|
/// مدیریت مرکزی تمام فایلهای سیستم
|
||||||
|
/// </summary>
|
||||||
|
public class UploadedFile : EntityBase<Guid>
|
||||||
|
{
|
||||||
|
private UploadedFile()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UploadedFile(
|
||||||
|
string originalFileName,
|
||||||
|
long fileSizeBytes,
|
||||||
|
string mimeType,
|
||||||
|
FileType fileType,
|
||||||
|
FileCategory category,
|
||||||
|
long uploadedByUserId,
|
||||||
|
StorageProvider storageProvider = StorageProvider.LocalFileSystem)
|
||||||
|
{
|
||||||
|
OriginalFileName = originalFileName;
|
||||||
|
FileSizeBytes = fileSizeBytes;
|
||||||
|
MimeType = mimeType;
|
||||||
|
FileType = fileType;
|
||||||
|
Category = category;
|
||||||
|
UploadedByUserId = uploadedByUserId;
|
||||||
|
UploadDate = DateTime.Now;
|
||||||
|
StorageProvider = storageProvider;
|
||||||
|
Status = FileStatus.Uploading;
|
||||||
|
|
||||||
|
// Generate unique file name
|
||||||
|
FileExtension = Path.GetExtension(originalFileName);
|
||||||
|
UniqueFileName = $"{Guid.NewGuid()}{FileExtension}";
|
||||||
|
|
||||||
|
ValidateFile();
|
||||||
|
AddDomainEvent(new FileUploadStartedEvent(Id, originalFileName, uploadedByUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// اطلاعات فایل
|
||||||
|
public string OriginalFileName { get; private set; } = string.Empty;
|
||||||
|
public string UniqueFileName { get; private set; } = string.Empty;
|
||||||
|
public string FileExtension { get; private set; } = string.Empty;
|
||||||
|
public long FileSizeBytes { get; private set; }
|
||||||
|
public string MimeType { get; private set; } = string.Empty;
|
||||||
|
public FileType FileType { get; private set; }
|
||||||
|
public FileCategory Category { get; private set; }
|
||||||
|
|
||||||
|
// ذخیرهسازی
|
||||||
|
public StorageProvider StorageProvider { get; private set; }
|
||||||
|
public string? StoragePath { get; private set; }
|
||||||
|
public string? StorageUrl { get; private set; }
|
||||||
|
public string? ThumbnailUrl { get; private set; }
|
||||||
|
|
||||||
|
// متادیتا
|
||||||
|
public long UploadedByUserId { get; private set; }
|
||||||
|
public DateTime UploadDate { get; private set; }
|
||||||
|
public FileStatus Status { get; private set; }
|
||||||
|
|
||||||
|
// اطلاعات تصویر (اختیاری - برای Image)
|
||||||
|
public int? ImageWidth { get; private set; }
|
||||||
|
public int? ImageHeight { get; private set; }
|
||||||
|
|
||||||
|
// اطلاعات صوت/ویدیو (اختیاری)
|
||||||
|
public int? DurationSeconds { get; private set; }
|
||||||
|
|
||||||
|
// امنیت
|
||||||
|
public DateTime? VirusScanDate { get; private set; }
|
||||||
|
public bool? IsVirusScanPassed { get; private set; }
|
||||||
|
public string? VirusScanResult { get; private set; }
|
||||||
|
|
||||||
|
// Soft Delete
|
||||||
|
public bool IsDeleted { get; private set; }
|
||||||
|
public DateTime? DeletedDate { get; private set; }
|
||||||
|
public long? DeletedByUserId { get; private set; }
|
||||||
|
|
||||||
|
// Reference tracking (چه entityهایی از این فایل استفاده میکنند)
|
||||||
|
public string? ReferenceEntityType { get; private set; }
|
||||||
|
public string? ReferenceEntityId { get; private set; }
|
||||||
|
|
||||||
|
private void ValidateFile()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(OriginalFileName))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("نام فایل نمیتواند خالی باشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FileSizeBytes <= 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("حجم فایل باید بیشتر از صفر باشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(MimeType))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("نوع MIME فایل باید مشخص شود");
|
||||||
|
}
|
||||||
|
|
||||||
|
// محدودیت حجم (مثلاً 100MB)
|
||||||
|
const long maxSizeBytes = 100 * 1024 * 1024; // 100MB
|
||||||
|
if (FileSizeBytes > maxSizeBytes)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"حجم فایل نباید بیشتر از {maxSizeBytes / (1024 * 1024)} مگابایت باشد");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompleteUpload(string storagePath, string storageUrl)
|
||||||
|
{
|
||||||
|
if (Status != FileStatus.Uploading)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("فایل قبلاً آپلود شده است");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(storagePath))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("مسیر ذخیرهسازی نمیتواند خالی باشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(storageUrl))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("URL فایل نمیتواند خالی باشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
StoragePath = storagePath;
|
||||||
|
StorageUrl = storageUrl;
|
||||||
|
Status = FileStatus.Active;
|
||||||
|
|
||||||
|
AddDomainEvent(new FileUploadCompletedEvent(Id, OriginalFileName, StorageUrl, UploadedByUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetThumbnail(string thumbnailUrl)
|
||||||
|
{
|
||||||
|
if (FileType != FileType.Image && FileType != FileType.Video)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("فقط میتوان برای تصاویر و ویدیوها thumbnail تنظیم کرد");
|
||||||
|
}
|
||||||
|
|
||||||
|
ThumbnailUrl = thumbnailUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetImageDimensions(int width, int height)
|
||||||
|
{
|
||||||
|
if (FileType != FileType.Image)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("فقط میتوان برای تصاویر ابعاد تنظیم کرد");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("ابعاد تصویر باید بیشتر از صفر باشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageWidth = width;
|
||||||
|
ImageHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDuration(int durationSeconds)
|
||||||
|
{
|
||||||
|
if (FileType != FileType.Audio && FileType != FileType.Video)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("فقط میتوان برای فایلهای صوتی و تصویری مدت زمان تنظیم کرد");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (durationSeconds <= 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("مدت زمان باید بیشتر از صفر باشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
DurationSeconds = durationSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkAsDeleted(long deletedByUserId)
|
||||||
|
{
|
||||||
|
if (IsDeleted)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("فایل قبلاً حذف شده است");
|
||||||
|
}
|
||||||
|
|
||||||
|
IsDeleted = true;
|
||||||
|
DeletedDate = DateTime.Now;
|
||||||
|
DeletedByUserId = deletedByUserId;
|
||||||
|
Status = FileStatus.Deleted;
|
||||||
|
|
||||||
|
AddDomainEvent(new FileDeletedEvent(Id, OriginalFileName, deletedByUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetReference(string entityType, string entityId)
|
||||||
|
{
|
||||||
|
ReferenceEntityType = entityType;
|
||||||
|
ReferenceEntityId = entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsImage()
|
||||||
|
{
|
||||||
|
return FileType == FileType.Image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsVideo()
|
||||||
|
{
|
||||||
|
return FileType == FileType.Video;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAudio()
|
||||||
|
{
|
||||||
|
return FileType == FileType.Audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDocument()
|
||||||
|
{
|
||||||
|
return FileType == FileType.Document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsUploadedBy(long userId)
|
||||||
|
{
|
||||||
|
return UploadedByUserId == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsActive()
|
||||||
|
{
|
||||||
|
return Status == FileStatus.Active && !IsDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetFileSizeFormatted()
|
||||||
|
{
|
||||||
|
const long kb = 1024;
|
||||||
|
const long mb = kb * 1024;
|
||||||
|
const long gb = mb * 1024;
|
||||||
|
|
||||||
|
if (FileSizeBytes >= gb)
|
||||||
|
return $"{FileSizeBytes / (double)gb:F2} GB";
|
||||||
|
if (FileSizeBytes >= mb)
|
||||||
|
return $"{FileSizeBytes / (double)mb:F2} MB";
|
||||||
|
if (FileSizeBytes >= kb)
|
||||||
|
return $"{FileSizeBytes / (double)kb:F2} KB";
|
||||||
|
|
||||||
|
return $"{FileSizeBytes} Bytes";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دستهبندی فایل - مشخص میکند فایل در کجا استفاده شده
|
||||||
|
/// </summary>
|
||||||
|
public enum FileCategory
|
||||||
|
{
|
||||||
|
TaskChatMessage = 1, // پیام چت تسک
|
||||||
|
TaskAttachment = 2, // ضمیمه تسک
|
||||||
|
ProjectDocument = 3, // مستندات پروژه
|
||||||
|
UserProfilePhoto = 4, // عکس پروفایل کاربر
|
||||||
|
Report = 5, // گزارش
|
||||||
|
Other = 6 // سایر
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// وضعیت فایل
|
||||||
|
/// </summary>
|
||||||
|
public enum FileStatus
|
||||||
|
{
|
||||||
|
Uploading = 1, // در حال آپلود
|
||||||
|
Active = 2, // فعال و قابل استفاده
|
||||||
|
Deleted = 5, // حذف شده (Soft Delete)
|
||||||
|
Archived = 6 // آرشیو شده
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نوع فایل
|
||||||
|
/// </summary>
|
||||||
|
public enum FileType
|
||||||
|
{
|
||||||
|
Document = 1, // اسناد (PDF, Word, Excel, etc.)
|
||||||
|
Image = 2, // تصویر
|
||||||
|
Video = 3, // ویدیو
|
||||||
|
Audio = 4, // صوت
|
||||||
|
Archive = 5, // فایل فشرده (ZIP, RAR)
|
||||||
|
Other = 6 // سایر
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نوع ذخیرهساز فایل
|
||||||
|
/// </summary>
|
||||||
|
public enum StorageProvider
|
||||||
|
{
|
||||||
|
LocalFileSystem = 1, // دیسک محلی سرور
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using GozareshgirProgramManager.Domain._Common;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Events;
|
||||||
|
|
||||||
|
// File Upload Events
|
||||||
|
public record FileUploadStartedEvent(Guid FileId, string FileName, long UploadedByUserId) : IDomainEvent
|
||||||
|
{
|
||||||
|
public DateTime OccurredOn { get; init; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record FileUploadCompletedEvent(Guid FileId, string FileName, string StorageUrl, long UploadedByUserId) : IDomainEvent
|
||||||
|
{
|
||||||
|
public DateTime OccurredOn { get; init; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record FileDeletedEvent(Guid FileId, string FileName, long DeletedByUserId) : IDomainEvent
|
||||||
|
{
|
||||||
|
public DateTime OccurredOn { get; init; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Virus Scan Events
|
||||||
|
public record FileQuarantinedEvent(Guid FileId, string FileName) : IDomainEvent
|
||||||
|
{
|
||||||
|
public DateTime OccurredOn { get; init; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record FileVirusScanPassedEvent(Guid FileId, string FileName) : IDomainEvent
|
||||||
|
{
|
||||||
|
public DateTime OccurredOn { get; init; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record FileInfectedEvent(Guid FileId, string FileName, string ScanResult) : IDomainEvent
|
||||||
|
{
|
||||||
|
public DateTime OccurredOn { get; init; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repository برای مدیریت فایلهای آپلود شده
|
||||||
|
/// </summary>
|
||||||
|
public interface IUploadedFileRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت فایل بر اساس شناسه
|
||||||
|
/// </summary>
|
||||||
|
Task<UploadedFile?> GetByIdAsync(Guid fileId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت فایل بر اساس نام یکتا
|
||||||
|
/// </summary>
|
||||||
|
Task<UploadedFile?> GetByUniqueFileNameAsync(string uniqueFileName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت لیست فایلهای یک کاربر
|
||||||
|
/// </summary>
|
||||||
|
Task<List<UploadedFile>> GetUserFilesAsync(long userId, int pageNumber, int pageSize);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت فایلهای یک دسته خاص
|
||||||
|
/// </summary>
|
||||||
|
Task<List<UploadedFile>> GetByCategoryAsync(FileCategory category, int pageNumber, int pageSize);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت فایلهای با وضعیت خاص
|
||||||
|
/// </summary>
|
||||||
|
Task<List<UploadedFile>> GetByStatusAsync(FileStatus status, int pageNumber, int pageSize);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت فایلهای یک Reference خاص
|
||||||
|
/// </summary>
|
||||||
|
Task<List<UploadedFile>> GetByReferenceAsync(string entityType, string entityId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جستجو در فایلها بر اساس نام
|
||||||
|
/// </summary>
|
||||||
|
Task<List<UploadedFile>> SearchByNameAsync(string searchTerm, int pageNumber, int pageSize);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت تعداد کل فایلهای یک کاربر
|
||||||
|
/// </summary>
|
||||||
|
Task<int> GetUserFilesCountAsync(long userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت مجموع حجم فایلهای یک کاربر (به بایت)
|
||||||
|
/// </summary>
|
||||||
|
Task<long> GetUserTotalFileSizeAsync(long userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت فایلهای منقضی شده برای پاکسازی
|
||||||
|
/// </summary>
|
||||||
|
Task<List<UploadedFile>> GetExpiredFilesAsync(DateTime olderThan);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// اضافه کردن فایل جدید
|
||||||
|
/// </summary>
|
||||||
|
Task<UploadedFile> AddAsync(UploadedFile file);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بهروزرسانی فایل
|
||||||
|
/// </summary>
|
||||||
|
Task UpdateAsync(UploadedFile file);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حذف فیزیکی فایل (فقط برای cleanup)
|
||||||
|
/// </summary>
|
||||||
|
Task DeleteAsync(UploadedFile file);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ذخیره تغییرات
|
||||||
|
/// </summary>
|
||||||
|
Task<int> SaveChangesAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بررسی وجود فایل
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> ExistsAsync(Guid fileId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بررسی وجود فایل با نام یکتا
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> ExistsByUniqueFileNameAsync(string uniqueFileName);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -40,14 +40,8 @@ public class TaskChatMessage : EntityBase<Guid>
|
|||||||
// محتوای متنی (برای پیامهای Text و Caption برای فایل/تصویر)
|
// محتوای متنی (برای پیامهای Text و Caption برای فایل/تصویر)
|
||||||
public string? TextContent { get; private set; }
|
public string? TextContent { get; private set; }
|
||||||
|
|
||||||
// اطلاعات فایل (برای پیامهای File, Voice, Image, Video)
|
// ارجاع به فایل (برای پیامهای File, Voice, Image, Video)
|
||||||
public string? FileUrl { get; private set; }
|
public Guid? FileId { 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
|
// پیام Reply
|
||||||
public Guid? ReplyToMessageId { get; private set; }
|
public Guid? ReplyToMessageId { get; private set; }
|
||||||
@@ -71,44 +65,21 @@ public class TaskChatMessage : EntityBase<Guid>
|
|||||||
|
|
||||||
if ((MessageType == MessageType.File || MessageType == MessageType.Voice ||
|
if ((MessageType == MessageType.File || MessageType == MessageType.Voice ||
|
||||||
MessageType == MessageType.Image || MessageType == MessageType.Video)
|
MessageType == MessageType.Image || MessageType == MessageType.Video)
|
||||||
&& string.IsNullOrWhiteSpace(FileUrl))
|
&& FileId == null)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("آدرس فایل نمیتواند خالی باشد");
|
throw new BadRequestException("پیامهای فایلی باید شناسه فایل داشته باشند");
|
||||||
}
|
|
||||||
|
|
||||||
if (MessageType == MessageType.Voice && VoiceDurationSeconds == null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("مدت زمان صدا باید مشخص شود");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetFileInfo(string fileUrl, string fileName, long fileSizeBytes, string mimeType)
|
public void SetFile(Guid fileId)
|
||||||
{
|
{
|
||||||
if (MessageType != MessageType.File && MessageType != MessageType.Image &&
|
if (MessageType != MessageType.File && MessageType != MessageType.Image &&
|
||||||
MessageType != MessageType.Video && MessageType != MessageType.Voice)
|
MessageType != MessageType.Video && MessageType != MessageType.Voice)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("فقط میتوان برای پیامهای فایل، تصویر، ویدیو و صدا اطلاعات فایل تنظیم کرد");
|
throw new BadRequestException("فقط میتوان برای پیامهای فایل، تصویر، ویدیو و صدا شناسه فایل تنظیم کرد");
|
||||||
}
|
}
|
||||||
|
|
||||||
FileUrl = fileUrl;
|
FileId = fileId;
|
||||||
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)
|
public void EditMessage(string newTextContent, long editorUserId)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public interface ITaskChatMessageRepository
|
|||||||
Task<List<TaskChatMessage>> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize);
|
Task<List<TaskChatMessage>> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// دریافت پیامهای با فایل (تصویر، ویدیو، فایل و...)
|
/// دریافت پیامهای با فایل (تصویر، ویدیو، فایل و...) - پیامهایی که FileId دارند
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<List<TaskChatMessage>> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize);
|
Task<List<TaskChatMessage>> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize);
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using GozareshgirProgramManager.Application._Common.Behaviors;
|
using GozareshgirProgramManager.Application._Common.Behaviors;
|
||||||
using GozareshgirProgramManager.Application._Common.Interfaces;
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
|
using GozareshgirProgramManager.Application.Services.FileManagement;
|
||||||
using GozareshgirProgramManager.Domain._Common;
|
using GozareshgirProgramManager.Domain._Common;
|
||||||
using GozareshgirProgramManager.Domain.CheckoutAgg.Repositories;
|
using GozareshgirProgramManager.Domain.CheckoutAgg.Repositories;
|
||||||
using GozareshgirProgramManager.Domain.CustomerAgg.Repositories;
|
using GozareshgirProgramManager.Domain.CustomerAgg.Repositories;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
|
||||||
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
|
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
|
||||||
using GozareshgirProgramManager.Domain.RoleAgg.Repositories;
|
using GozareshgirProgramManager.Domain.RoleAgg.Repositories;
|
||||||
using GozareshgirProgramManager.Domain.RoleAgg.Repositories;
|
using GozareshgirProgramManager.Domain.RoleAgg.Repositories;
|
||||||
@@ -14,6 +16,7 @@ using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Repositories;
|
|||||||
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Repositories;
|
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Repositories;
|
||||||
using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
|
using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
|
||||||
using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
|
using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
|
||||||
using GozareshgirProgramManager.Domain.UserAgg.Repositories;
|
using GozareshgirProgramManager.Domain.UserAgg.Repositories;
|
||||||
using GozareshgirProgramManager.Infrastructure.Persistence;
|
using GozareshgirProgramManager.Infrastructure.Persistence;
|
||||||
using GozareshgirProgramManager.Infrastructure.Persistence.Context;
|
using GozareshgirProgramManager.Infrastructure.Persistence.Context;
|
||||||
@@ -82,6 +85,14 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
services.AddScoped<IUserRefreshTokenRepository, UserRefreshTokenRepository>();
|
services.AddScoped<IUserRefreshTokenRepository, UserRefreshTokenRepository>();
|
||||||
|
|
||||||
|
// File Management & Task Chat
|
||||||
|
services.AddScoped<IUploadedFileRepository, Persistence.Repositories.FileManagement.UploadedFileRepository>();
|
||||||
|
services.AddScoped<ITaskChatMessageRepository, Persistence.Repositories.TaskChat.TaskChatMessageRepository>();
|
||||||
|
|
||||||
|
// File Storage Services
|
||||||
|
services.AddScoped<IFileStorageService, Services.FileManagement.LocalFileStorageService>();
|
||||||
|
services.AddScoped<IThumbnailGeneratorService, Services.FileManagement.ThumbnailGeneratorService>();
|
||||||
|
|
||||||
// JWT Settings
|
// JWT Settings
|
||||||
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));
|
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,19 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
<!--<PackageReference Include="System.Text.Encodings.Web" Version="10.0.0" />-->
|
<!--<PackageReference Include="System.Text.Encodings.Web" Version="10.0.0" />-->
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Application\GozareshgirProgramManager.Application\GozareshgirProgramManager.Application.csproj" />
|
<ProjectReference Include="..\..\Application\GozareshgirProgramManager.Application\GozareshgirProgramManager.Application.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions">
|
||||||
|
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.AspNetCore.Hosting.Abstractions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Microsoft.Extensions.Hosting.Abstractions">
|
||||||
|
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.Extensions.Hosting.Abstractions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,158 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class addtaskchatuploadedfile : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TaskChatMessages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
TaskId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
SenderUserId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
MessageType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||||
|
TextContent = table.Column<string>(type: "nvarchar(4000)", maxLength: 4000, nullable: true),
|
||||||
|
FileId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
ReplyToMessageId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
IsEdited = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||||
|
EditedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||||
|
DeletedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
IsPinned = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||||
|
PinnedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
PinnedByUserId = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TaskChatMessages", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TaskChatMessages_TaskChatMessages_ReplyToMessageId",
|
||||||
|
column: x => x.ReplyToMessageId,
|
||||||
|
principalTable: "TaskChatMessages",
|
||||||
|
principalColumn: "Id");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UploadedFiles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
OriginalFileName = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||||
|
UniqueFileName = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||||
|
FileExtension = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||||
|
FileSizeBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
MimeType = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||||
|
FileType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||||
|
Category = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
StorageProvider = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||||
|
StoragePath = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||||
|
StorageUrl = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||||
|
ThumbnailUrl = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||||
|
UploadedByUserId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
UploadDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||||
|
ImageWidth = table.Column<int>(type: "int", nullable: true),
|
||||||
|
ImageHeight = table.Column<int>(type: "int", nullable: true),
|
||||||
|
DurationSeconds = table.Column<int>(type: "int", nullable: true),
|
||||||
|
VirusScanDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
IsVirusScanPassed = table.Column<bool>(type: "bit", nullable: true),
|
||||||
|
VirusScanResult = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||||
|
DeletedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedByUserId = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
ReferenceEntityType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
ReferenceEntityId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UploadedFiles", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TaskChatMessages_CreationDate",
|
||||||
|
table: "TaskChatMessages",
|
||||||
|
column: "CreationDate");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TaskChatMessages_FileId",
|
||||||
|
table: "TaskChatMessages",
|
||||||
|
column: "FileId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TaskChatMessages_IsDeleted",
|
||||||
|
table: "TaskChatMessages",
|
||||||
|
column: "IsDeleted");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TaskChatMessages_ReplyToMessageId",
|
||||||
|
table: "TaskChatMessages",
|
||||||
|
column: "ReplyToMessageId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TaskChatMessages_SenderUserId",
|
||||||
|
table: "TaskChatMessages",
|
||||||
|
column: "SenderUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TaskChatMessages_TaskId",
|
||||||
|
table: "TaskChatMessages",
|
||||||
|
column: "TaskId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TaskChatMessages_TaskId_IsPinned",
|
||||||
|
table: "TaskChatMessages",
|
||||||
|
columns: new[] { "TaskId", "IsPinned" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UploadedFiles_Category",
|
||||||
|
table: "UploadedFiles",
|
||||||
|
column: "Category");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UploadedFiles_IsDeleted",
|
||||||
|
table: "UploadedFiles",
|
||||||
|
column: "IsDeleted");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UploadedFiles_ReferenceEntityType_ReferenceEntityId",
|
||||||
|
table: "UploadedFiles",
|
||||||
|
columns: new[] { "ReferenceEntityType", "ReferenceEntityId" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UploadedFiles_Status",
|
||||||
|
table: "UploadedFiles",
|
||||||
|
column: "Status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UploadedFiles_UniqueFileName",
|
||||||
|
table: "UploadedFiles",
|
||||||
|
column: "UniqueFileName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UploadedFiles_UploadedByUserId",
|
||||||
|
table: "UploadedFiles",
|
||||||
|
column: "UploadedByUserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TaskChatMessages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UploadedFiles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,6 +102,131 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
|
|||||||
b.ToTable("Customers", (string)null);
|
b.ToTable("Customers", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GozareshgirProgramManager.Domain.FileManagementAgg.Entities.UploadedFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedByUserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int?>("DurationSeconds")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("FileExtension")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<long>("FileSizeBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("FileType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<int?>("ImageHeight")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("ImageWidth")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool?>("IsVirusScanPassed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("MimeType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("ReferenceEntityId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("ReferenceEntityType")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("StoragePath")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("StorageProvider")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("StorageUrl")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("ThumbnailUrl")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("UniqueFileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UploadDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<long>("UploadedByUserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("VirusScanDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("VirusScanResult")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Category");
|
||||||
|
|
||||||
|
b.HasIndex("IsDeleted");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("UniqueFileName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UploadedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("ReferenceEntityType", "ReferenceEntityId");
|
||||||
|
|
||||||
|
b.ToTable("UploadedFiles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.PhaseSection", b =>
|
modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.PhaseSection", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -495,6 +620,81 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
|
|||||||
b.ToTable("Skills", (string)null);
|
b.ToTable("Skills", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EditedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FileId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("IsEdited")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("IsPinned")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<string>("MessageType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<long?>("PinnedByUserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PinnedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ReplyToMessageId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<long>("SenderUserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<Guid>("TaskId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TextContent")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("nvarchar(4000)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreationDate");
|
||||||
|
|
||||||
|
b.HasIndex("FileId");
|
||||||
|
|
||||||
|
b.HasIndex("IsDeleted");
|
||||||
|
|
||||||
|
b.HasIndex("ReplyToMessageId");
|
||||||
|
|
||||||
|
b.HasIndex("SenderUserId");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId", "IsPinned");
|
||||||
|
|
||||||
|
b.ToTable("TaskChatMessages", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b =>
|
modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -779,6 +979,16 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
|
|||||||
b.Navigation("WorkingHoursList");
|
b.Navigation("WorkingHoursList");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", "ReplyToMessage")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ReplyToMessageId")
|
||||||
|
.OnDelete(DeleteBehavior.NoAction);
|
||||||
|
|
||||||
|
b.Navigation("ReplyToMessage");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b =>
|
modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b =>
|
||||||
{
|
{
|
||||||
b.OwnsMany("GozareshgirProgramManager.Domain.RoleUserAgg.RoleUser", "RoleUser", b1 =>
|
b.OwnsMany("GozareshgirProgramManager.Domain.RoleUserAgg.RoleUser", "RoleUser", b1 =>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
using GozareshgirProgramManager.Application._Common.Interfaces;
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
using GozareshgirProgramManager.Domain.CheckoutAgg.Entities;
|
using GozareshgirProgramManager.Domain.CheckoutAgg.Entities;
|
||||||
using GozareshgirProgramManager.Domain.CustomerAgg;
|
using GozareshgirProgramManager.Domain.CustomerAgg;
|
||||||
using GozareshgirProgramManager.Application._Common.Interfaces;
|
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||||
using GozareshgirProgramManager.Domain.CustomerAgg;
|
using GozareshgirProgramManager.Domain.CustomerAgg;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
|
||||||
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
|
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
|
||||||
using GozareshgirProgramManager.Domain.RoleAgg.Entities;
|
using GozareshgirProgramManager.Domain.RoleAgg.Entities;
|
||||||
using GozareshgirProgramManager.Domain.RoleUserAgg;
|
using GozareshgirProgramManager.Domain.RoleUserAgg;
|
||||||
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities;
|
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities;
|
||||||
using GozareshgirProgramManager.Domain.UserAgg.Entities;
|
using GozareshgirProgramManager.Domain.UserAgg.Entities;
|
||||||
using GozareshgirProgramManager.Domain.SkillAgg.Entities;
|
using GozareshgirProgramManager.Domain.SkillAgg.Entities;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace GozareshgirProgramManager.Infrastructure.Persistence.Context;
|
namespace GozareshgirProgramManager.Infrastructure.Persistence.Context;
|
||||||
@@ -40,6 +42,13 @@ public class ProgramManagerDbContext : DbContext, IProgramManagerDbContext
|
|||||||
public DbSet<Role> Roles { get; set; } = null!;
|
public DbSet<Role> Roles { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<Skill> Skills { get; set; } = null!;
|
public DbSet<Skill> Skills { get; set; } = null!;
|
||||||
|
|
||||||
|
// File Management
|
||||||
|
public DbSet<UploadedFile> UploadedFiles { get; set; } = null!;
|
||||||
|
|
||||||
|
// Task Chat
|
||||||
|
public DbSet<TaskChatMessage> TaskChatMessages { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ProgramManagerDbContext).Assembly);
|
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ProgramManagerDbContext).Assembly);
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Infrastructure.Persistence.Mappings;
|
||||||
|
|
||||||
|
public class TaskChatMessageMapping : IEntityTypeConfiguration<TaskChatMessage>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<TaskChatMessage> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("TaskChatMessages");
|
||||||
|
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
|
builder.Property(x => x.Id)
|
||||||
|
.ValueGeneratedNever();
|
||||||
|
|
||||||
|
// Task Reference
|
||||||
|
builder.Property(x => x.TaskId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.HasIndex(x => x.TaskId);
|
||||||
|
|
||||||
|
// Sender
|
||||||
|
builder.Property(x => x.SenderUserId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.HasIndex(x => x.SenderUserId);
|
||||||
|
|
||||||
|
// Message Type
|
||||||
|
builder.Property(x => x.MessageType)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConversion<string>()
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
// Content
|
||||||
|
builder.Property(x => x.TextContent)
|
||||||
|
.HasMaxLength(4000);
|
||||||
|
|
||||||
|
// File Reference
|
||||||
|
builder.Property(x => x.FileId);
|
||||||
|
|
||||||
|
builder.HasIndex(x => x.FileId);
|
||||||
|
|
||||||
|
// Reply
|
||||||
|
builder.Property(x => x.ReplyToMessageId);
|
||||||
|
|
||||||
|
builder.HasOne(x => x.ReplyToMessage)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ReplyToMessageId)
|
||||||
|
.OnDelete(DeleteBehavior.NoAction);
|
||||||
|
|
||||||
|
// Status
|
||||||
|
builder.Property(x => x.IsEdited)
|
||||||
|
.IsRequired()
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
builder.Property(x => x.EditedDate);
|
||||||
|
|
||||||
|
builder.Property(x => x.IsDeleted)
|
||||||
|
.IsRequired()
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
builder.Property(x => x.DeletedDate);
|
||||||
|
|
||||||
|
builder.HasIndex(x => x.IsDeleted);
|
||||||
|
|
||||||
|
// Pin
|
||||||
|
builder.Property(x => x.IsPinned)
|
||||||
|
.IsRequired()
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
builder.Property(x => x.PinnedDate);
|
||||||
|
builder.Property(x => x.PinnedByUserId);
|
||||||
|
|
||||||
|
builder.HasIndex(x => new { x.TaskId, x.IsPinned });
|
||||||
|
|
||||||
|
// Audit
|
||||||
|
builder.Property(x => x.CreationDate)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.HasIndex(x => x.CreationDate);
|
||||||
|
|
||||||
|
// Query Filter - پیامهای حذف نشده
|
||||||
|
builder.HasQueryFilter(x => !x.IsDeleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Infrastructure.Persistence.Mappings;
|
||||||
|
|
||||||
|
public class UploadedFileMapping : IEntityTypeConfiguration<UploadedFile>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UploadedFile> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("UploadedFiles");
|
||||||
|
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
|
builder.Property(x => x.Id)
|
||||||
|
.ValueGeneratedNever();
|
||||||
|
|
||||||
|
// اطلاعات فایل
|
||||||
|
builder.Property(x => x.OriginalFileName)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500);
|
||||||
|
|
||||||
|
builder.Property(x => x.UniqueFileName)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500);
|
||||||
|
|
||||||
|
builder.HasIndex(x => x.UniqueFileName)
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
builder.Property(x => x.FileExtension)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
builder.Property(x => x.FileSizeBytes)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(x => x.MimeType)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
builder.Property(x => x.FileType)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConversion<string>()
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
builder.Property(x => x.Category)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConversion<string>()
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
// ذخیرهسازی
|
||||||
|
builder.Property(x => x.StorageProvider)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConversion<string>()
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
builder.Property(x => x.StoragePath)
|
||||||
|
.HasMaxLength(1000);
|
||||||
|
|
||||||
|
builder.Property(x => x.StorageUrl)
|
||||||
|
.HasMaxLength(1000);
|
||||||
|
|
||||||
|
builder.Property(x => x.ThumbnailUrl)
|
||||||
|
.HasMaxLength(1000);
|
||||||
|
|
||||||
|
// متادیتا
|
||||||
|
builder.Property(x => x.UploadedByUserId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(x => x.UploadDate)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(x => x.Status)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConversion<string>()
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
builder.HasIndex(x => x.Status);
|
||||||
|
builder.HasIndex(x => x.UploadedByUserId);
|
||||||
|
builder.HasIndex(x => x.Category);
|
||||||
|
|
||||||
|
// اطلاعات تصویر
|
||||||
|
builder.Property(x => x.ImageWidth);
|
||||||
|
builder.Property(x => x.ImageHeight);
|
||||||
|
|
||||||
|
// اطلاعات صوت/ویدیو
|
||||||
|
builder.Property(x => x.DurationSeconds);
|
||||||
|
|
||||||
|
// امنیت
|
||||||
|
builder.Property(x => x.VirusScanDate);
|
||||||
|
builder.Property(x => x.IsVirusScanPassed);
|
||||||
|
builder.Property(x => x.VirusScanResult)
|
||||||
|
.HasMaxLength(500);
|
||||||
|
|
||||||
|
// Soft Delete
|
||||||
|
builder.Property(x => x.IsDeleted)
|
||||||
|
.IsRequired()
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
builder.Property(x => x.DeletedDate);
|
||||||
|
builder.Property(x => x.DeletedByUserId);
|
||||||
|
|
||||||
|
builder.HasIndex(x => x.IsDeleted);
|
||||||
|
|
||||||
|
// Reference Tracking
|
||||||
|
builder.Property(x => x.ReferenceEntityType)
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
builder.Property(x => x.ReferenceEntityId)
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
builder.HasIndex(x => new { x.ReferenceEntityType, x.ReferenceEntityId });
|
||||||
|
|
||||||
|
// Audit
|
||||||
|
builder.Property(x => x.CreationDate)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// Query Filter - فایلهای حذف نشده
|
||||||
|
builder.HasQueryFilter(x => !x.IsDeleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||||
|
using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
|
||||||
|
using GozareshgirProgramManager.Infrastructure.Persistence.Context;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Infrastructure.Persistence.Repositories.FileManagement;
|
||||||
|
|
||||||
|
public class UploadedFileRepository : IUploadedFileRepository
|
||||||
|
{
|
||||||
|
private readonly ProgramManagerDbContext _context;
|
||||||
|
|
||||||
|
public UploadedFileRepository(ProgramManagerDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UploadedFile?> GetByIdAsync(Guid fileId)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UploadedFile?> GetByUniqueFileNameAsync(string uniqueFileName)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.FirstOrDefaultAsync(x => x.UniqueFileName == uniqueFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UploadedFile>> GetUserFilesAsync(long userId, int pageNumber, int pageSize)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.Where(x => x.UploadedByUserId == userId)
|
||||||
|
.OrderByDescending(x => x.UploadDate)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UploadedFile>> GetByCategoryAsync(FileCategory category, int pageNumber, int pageSize)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.Where(x => x.Category == category)
|
||||||
|
.OrderByDescending(x => x.UploadDate)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UploadedFile>> GetByStatusAsync(FileStatus status, int pageNumber, int pageSize)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.Where(x => x.Status == status)
|
||||||
|
.OrderByDescending(x => x.UploadDate)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UploadedFile>> GetByReferenceAsync(string entityType, string entityId)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.Where(x => x.ReferenceEntityType == entityType && x.ReferenceEntityId == entityId)
|
||||||
|
.OrderByDescending(x => x.UploadDate)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UploadedFile>> SearchByNameAsync(string searchTerm, int pageNumber, int pageSize)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.Where(x => x.OriginalFileName.Contains(searchTerm))
|
||||||
|
.OrderByDescending(x => x.UploadDate)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetUserFilesCountAsync(long userId)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.CountAsync(x => x.UploadedByUserId == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> GetUserTotalFileSizeAsync(long userId)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.Where(x => x.UploadedByUserId == userId)
|
||||||
|
.SumAsync(x => x.FileSizeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UploadedFile>> GetExpiredFilesAsync(DateTime olderThan)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.IgnoreQueryFilters() // Include deleted files
|
||||||
|
.Where(x => x.IsDeleted && x.DeletedDate < olderThan)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UploadedFile> AddAsync(UploadedFile file)
|
||||||
|
{
|
||||||
|
await _context.UploadedFiles.AddAsync(file);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateAsync(UploadedFile file)
|
||||||
|
{
|
||||||
|
_context.UploadedFiles.Update(file);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteAsync(UploadedFile file)
|
||||||
|
{
|
||||||
|
_context.UploadedFiles.Remove(file);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> SaveChangesAsync()
|
||||||
|
{
|
||||||
|
return await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExistsAsync(Guid fileId)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.AnyAsync(x => x.Id == fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExistsByUniqueFileNameAsync(string uniqueFileName)
|
||||||
|
{
|
||||||
|
return await _context.UploadedFiles
|
||||||
|
.AnyAsync(x => x.UniqueFileName == uniqueFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
|
||||||
|
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
|
||||||
|
using GozareshgirProgramManager.Infrastructure.Persistence.Context;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Infrastructure.Persistence.Repositories.TaskChat;
|
||||||
|
|
||||||
|
public class TaskChatMessageRepository : ITaskChatMessageRepository
|
||||||
|
{
|
||||||
|
private readonly ProgramManagerDbContext _context;
|
||||||
|
|
||||||
|
public TaskChatMessageRepository(ProgramManagerDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TaskChatMessage?> GetByIdAsync(Guid messageId)
|
||||||
|
{
|
||||||
|
return await _context.TaskChatMessages
|
||||||
|
.Include(x => x.ReplyToMessage)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<TaskChatMessage>> GetTaskMessagesAsync(Guid taskId, int pageNumber, int pageSize)
|
||||||
|
{
|
||||||
|
return await _context.TaskChatMessages
|
||||||
|
.Where(x => x.TaskId == taskId)
|
||||||
|
.Include(x => x.ReplyToMessage)
|
||||||
|
.OrderBy(x => x.CreationDate)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTaskMessageCountAsync(Guid taskId)
|
||||||
|
{
|
||||||
|
return await _context.TaskChatMessages
|
||||||
|
.CountAsync(x => x.TaskId == taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<TaskChatMessage>> GetPinnedMessagesAsync(Guid taskId)
|
||||||
|
{
|
||||||
|
return await _context.TaskChatMessages
|
||||||
|
.Where(x => x.TaskId == taskId && x.IsPinned)
|
||||||
|
.Include(x => x.ReplyToMessage)
|
||||||
|
.OrderByDescending(x => x.PinnedDate)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TaskChatMessage?> GetLastMessageAsync(Guid taskId)
|
||||||
|
{
|
||||||
|
return await _context.TaskChatMessages
|
||||||
|
.Where(x => x.TaskId == taskId)
|
||||||
|
.OrderByDescending(x => x.CreationDate)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<TaskChatMessage>> SearchMessagesAsync(Guid taskId, string searchText, int pageNumber, int pageSize)
|
||||||
|
{
|
||||||
|
return await _context.TaskChatMessages
|
||||||
|
.Where(x => x.TaskId == taskId &&
|
||||||
|
x.TextContent != null &&
|
||||||
|
x.TextContent.Contains(searchText))
|
||||||
|
.Include(x => x.ReplyToMessage)
|
||||||
|
.OrderByDescending(x => x.CreationDate)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<TaskChatMessage>> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize)
|
||||||
|
{
|
||||||
|
return await _context.TaskChatMessages
|
||||||
|
.Where(x => x.TaskId == taskId && x.SenderUserId == userId)
|
||||||
|
.Include(x => x.ReplyToMessage)
|
||||||
|
.OrderByDescending(x => x.CreationDate)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<TaskChatMessage>> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize)
|
||||||
|
{
|
||||||
|
return await _context.TaskChatMessages
|
||||||
|
.Where(x => x.TaskId == taskId && x.FileId != null)
|
||||||
|
.Include(x => x.ReplyToMessage)
|
||||||
|
.OrderByDescending(x => x.CreationDate)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TaskChatMessage> AddAsync(TaskChatMessage message)
|
||||||
|
{
|
||||||
|
await _context.TaskChatMessages.AddAsync(message);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateAsync(TaskChatMessage message)
|
||||||
|
{
|
||||||
|
_context.TaskChatMessages.Update(message);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteAsync(TaskChatMessage message)
|
||||||
|
{
|
||||||
|
_context.TaskChatMessages.Remove(message);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> SaveChangesAsync()
|
||||||
|
{
|
||||||
|
return await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExistsAsync(Guid messageId)
|
||||||
|
{
|
||||||
|
return await _context.TaskChatMessages
|
||||||
|
.AnyAsync(x => x.Id == messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using GozareshgirProgramManager.Application.Services.FileManagement;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس ذخیرهسازی فایل در سیستم فایل محلی
|
||||||
|
/// </summary>
|
||||||
|
public class LocalFileStorageService : IFileStorageService
|
||||||
|
{
|
||||||
|
private readonly string _uploadBasePath;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
public LocalFileStorageService(IConfiguration configuration,
|
||||||
|
IHttpContextAccessor httpContextAccessor, IHostEnvironment env)
|
||||||
|
{
|
||||||
|
// محاسبه مسیر پایه: اگر env نبود، از مسیر فعلی استفاده کن
|
||||||
|
var contentRoot = env.ContentRootPath;
|
||||||
|
_uploadBasePath = Path.Combine(contentRoot, "Storage");
|
||||||
|
// Base URL برای دسترسی به فایلها
|
||||||
|
var request = httpContextAccessor.HttpContext?.Request;
|
||||||
|
if (request != null)
|
||||||
|
{
|
||||||
|
_baseUrl = $"{request.Scheme}://{request.Host}/uploads";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/uploads";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ایجاد پوشه اگر وجود نداشت
|
||||||
|
if (!Directory.Exists(_uploadBasePath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_uploadBasePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string StoragePath, string StorageUrl)> UploadAsync(
|
||||||
|
Stream fileStream,
|
||||||
|
string uniqueFileName,
|
||||||
|
string category)
|
||||||
|
{
|
||||||
|
// ایجاد پوشه دستهبندی (مثلاً: Uploads/TaskChatMessage)
|
||||||
|
var categoryPath = Path.Combine(_uploadBasePath, category);
|
||||||
|
if (!Directory.Exists(categoryPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(categoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ایجاد زیرپوشه بر اساس تاریخ (مثلاً: Uploads/TaskChatMessage/2026/01)
|
||||||
|
var datePath = Path.Combine(categoryPath, DateTime.Now.Year.ToString(),
|
||||||
|
DateTime.Now.Month.ToString("00"));
|
||||||
|
if (!Directory.Exists(datePath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(datePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// مسیر کامل فایل
|
||||||
|
var storagePath = Path.Combine(datePath, uniqueFileName);
|
||||||
|
|
||||||
|
// ذخیره فایل
|
||||||
|
await using var fileStreamOutput = new FileStream(storagePath, FileMode.Create, FileAccess.Write);
|
||||||
|
await fileStream.CopyToAsync(fileStreamOutput);
|
||||||
|
|
||||||
|
// URL فایل
|
||||||
|
var relativePath = Path.GetRelativePath(_uploadBasePath, storagePath)
|
||||||
|
.Replace("\\", "/");
|
||||||
|
var storageUrl = $"{_baseUrl}/{relativePath}";
|
||||||
|
|
||||||
|
return (storagePath, storageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteAsync(string storagePath)
|
||||||
|
{
|
||||||
|
if (File.Exists(storagePath))
|
||||||
|
{
|
||||||
|
File.Delete(storagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Stream?> GetFileStreamAsync(string storagePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(storagePath))
|
||||||
|
{
|
||||||
|
return Task.FromResult<Stream?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = new FileStream(storagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
return Task.FromResult<Stream?>(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> ExistsAsync(string storagePath)
|
||||||
|
{
|
||||||
|
return Task.FromResult(File.Exists(storagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetFileUrl(string storagePath)
|
||||||
|
{
|
||||||
|
var relativePath = Path.GetRelativePath(_uploadBasePath, storagePath)
|
||||||
|
.Replace("\\", "/");
|
||||||
|
return $"{_baseUrl}/{relativePath}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using GozareshgirProgramManager.Application.Services.FileManagement;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
|
|
||||||
|
namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس تولید thumbnail با استفاده از ImageSharp
|
||||||
|
/// </summary>
|
||||||
|
public class ThumbnailGeneratorService : IThumbnailGeneratorService
|
||||||
|
{
|
||||||
|
private readonly string _thumbnailBasePath;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
public ThumbnailGeneratorService(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_thumbnailBasePath = configuration["FileStorage:ThumbnailPath"]
|
||||||
|
?? Path.Combine(Directory.GetCurrentDirectory(), "Uploads", "Thumbnails");
|
||||||
|
|
||||||
|
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/uploads";
|
||||||
|
|
||||||
|
if (!Directory.Exists(_thumbnailBasePath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_thumbnailBasePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync(
|
||||||
|
string imagePath,
|
||||||
|
int width = 200,
|
||||||
|
int height = 200)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(imagePath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// بارگذاری تصویر
|
||||||
|
using var image = await Image.LoadAsync(imagePath);
|
||||||
|
|
||||||
|
// Resize با حفظ نسبت
|
||||||
|
image.Mutate(x => x.Resize(new ResizeOptions
|
||||||
|
{
|
||||||
|
Size = new Size(width, height),
|
||||||
|
Mode = ResizeMode.Max
|
||||||
|
}));
|
||||||
|
|
||||||
|
// نام فایل thumbnail
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(imagePath);
|
||||||
|
var extension = Path.GetExtension(imagePath);
|
||||||
|
var thumbnailFileName = $"{fileName}_thumb{extension}";
|
||||||
|
|
||||||
|
// مسیر ذخیره thumbnail
|
||||||
|
var thumbnailPath = Path.Combine(_thumbnailBasePath, thumbnailFileName);
|
||||||
|
|
||||||
|
// ذخیره thumbnail با کیفیت 80
|
||||||
|
await image.SaveAsync(thumbnailPath, new JpegEncoder { Quality = 80 });
|
||||||
|
|
||||||
|
// URL thumbnail
|
||||||
|
var thumbnailUrl = $"{_baseUrl}/thumbnails/{thumbnailFileName}";
|
||||||
|
|
||||||
|
return (thumbnailPath, thumbnailUrl);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// در صورت خطا null برمیگردانیم
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(string videoPath)
|
||||||
|
{
|
||||||
|
// TODO: برای Video thumbnail باید از FFmpeg استفاده کنیم
|
||||||
|
// فعلاً یک placeholder image برمیگردانیم
|
||||||
|
await Task.CompletedTask;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteThumbnailAsync(string thumbnailPath)
|
||||||
|
{
|
||||||
|
if (File.Exists(thumbnailPath))
|
||||||
|
{
|
||||||
|
File.Delete(thumbnailPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(int Width, int Height)?> GetImageDimensionsAsync(string imagePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(imagePath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageInfo = await Image.IdentifyAsync(imagePath);
|
||||||
|
return (imageInfo.Width, imageInfo.Height);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
using GozareshgirProgramManager.Application._Common.Models;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.DeleteMessage;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.EditMessage;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.PinMessage;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.SendMessage;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.UnpinMessage;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetMessages;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetPinnedMessages;
|
||||||
|
using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.SearchMessages;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using ServiceHost.BaseControllers;
|
||||||
|
|
||||||
|
namespace ServiceHost.Areas.Admin.Controllers.ProgramManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// کنترلر مدیریت چت تسک
|
||||||
|
/// </summary>
|
||||||
|
public class TaskChatController : ProgramManagerBaseController
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public TaskChatController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت لیست پیامهای یک تسک
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId">شناسه تسک</param>
|
||||||
|
/// <param name="page">صفحه (پیشفرض: 1)</param>
|
||||||
|
/// <param name="pageSize">تعداد در هر صفحه (پیشفرض: 50)</param>
|
||||||
|
[HttpGet("{taskId:guid}/messages")]
|
||||||
|
public async Task<ActionResult<OperationResult<PaginationResult<MessageDto>>>> GetMessages(
|
||||||
|
Guid taskId,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 50)
|
||||||
|
{
|
||||||
|
var query = new GetMessagesQuery(taskId, page, pageSize);
|
||||||
|
var result = await _mediator.Send(query);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت پیامهای پین شده یک تسک
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId">شناسه تسک</param>
|
||||||
|
[HttpGet("{taskId:guid}/messages/pinned")]
|
||||||
|
public async Task<ActionResult<OperationResult<List<MessageDto>>>> GetPinnedMessages(Guid taskId)
|
||||||
|
{
|
||||||
|
var query = new GetPinnedMessagesQuery(taskId);
|
||||||
|
var result = await _mediator.Send(query);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جستجو در پیامهای یک تسک
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId">شناسه تسک</param>
|
||||||
|
/// <param name="search">متن جستجو</param>
|
||||||
|
/// <param name="page">صفحه</param>
|
||||||
|
/// <param name="pageSize">تعداد در هر صفحه</param>
|
||||||
|
[HttpGet("{taskId:guid}/messages/search")]
|
||||||
|
public async Task<ActionResult<OperationResult<List<MessageDto>>>> SearchMessages(
|
||||||
|
Guid taskId,
|
||||||
|
[FromQuery] string search,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20)
|
||||||
|
{
|
||||||
|
var query = new SearchMessagesQuery(taskId, search, page, pageSize);
|
||||||
|
var result = await _mediator.Send(query);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال پیام جدید (با یا بدون فایل)
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("messages")]
|
||||||
|
public async Task<ActionResult<OperationResult<MessageDto>>> SendMessage(
|
||||||
|
[FromForm] SendMessageCommand command)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ویرایش پیام (فقط متن)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageId">شناسه پیام</param>
|
||||||
|
/// <param name="request">محتوای جدید</param>
|
||||||
|
[HttpPut("messages/{messageId:guid}")]
|
||||||
|
public async Task<ActionResult<OperationResult>> EditMessage(
|
||||||
|
Guid messageId,
|
||||||
|
[FromBody] EditMessageRequest request)
|
||||||
|
{
|
||||||
|
var command = new EditMessageCommand(messageId, request.NewTextContent);
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حذف پیام
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageId">شناسه پیام</param>
|
||||||
|
[HttpDelete("messages/{messageId:guid}")]
|
||||||
|
public async Task<ActionResult<OperationResult>> DeleteMessage(Guid messageId)
|
||||||
|
{
|
||||||
|
var command = new DeleteMessageCommand(messageId);
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پین کردن پیام
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageId">شناسه پیام</param>
|
||||||
|
[HttpPost("messages/{messageId:guid}/pin")]
|
||||||
|
public async Task<ActionResult<OperationResult>> PinMessage(Guid messageId)
|
||||||
|
{
|
||||||
|
var command = new PinMessageCommand(messageId);
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// برداشتن پین پیام
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageId">شناسه پیام</param>
|
||||||
|
[HttpPost("messages/{messageId:guid}/unpin")]
|
||||||
|
public async Task<ActionResult<OperationResult>> UnpinMessage(Guid messageId)
|
||||||
|
{
|
||||||
|
var command = new UnpinMessageCommand(messageId);
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EditMessageRequest
|
||||||
|
{
|
||||||
|
public string NewTextContent { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -492,6 +492,24 @@ app.UseHttpsRedirection();
|
|||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
// Static files برای فایلهای آپلود شده
|
||||||
|
var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
|
||||||
|
if (!Directory.Exists(uploadsPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(uploadsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
|
{
|
||||||
|
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadsPath),
|
||||||
|
RequestPath = "/uploads",
|
||||||
|
OnPrepareResponse = ctx =>
|
||||||
|
{
|
||||||
|
// Cache برای فایلها (30 روز)
|
||||||
|
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.UseCookiePolicy();
|
app.UseCookiePolicy();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,14 @@
|
|||||||
"Issuer": "GozareshgirApp",
|
"Issuer": "GozareshgirApp",
|
||||||
"Audience": "GozareshgirUsers",
|
"Audience": "GozareshgirUsers",
|
||||||
"ExpirationMinutes": 30
|
"ExpirationMinutes": 30
|
||||||
|
},
|
||||||
|
"FileStorage": {
|
||||||
|
"BaseUrl": "https://localhost:7032/uploads",
|
||||||
|
"MaxFileSizeBytes": 104857600,
|
||||||
|
"AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".gif", ".webp" ],
|
||||||
|
"AllowedVideoExtensions": [ ".mp4", ".avi", ".mov", ".wmv" ],
|
||||||
|
"AllowedAudioExtensions": [ ".mp3", ".wav", ".ogg", ".m4a" ],
|
||||||
|
"AllowedDocumentExtensions": [ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,5 +50,13 @@
|
|||||||
"Issuer": "GozareshgirApp",
|
"Issuer": "GozareshgirApp",
|
||||||
"Audience": "GozareshgirUsers",
|
"Audience": "GozareshgirUsers",
|
||||||
"ExpirationMinutes": 30
|
"ExpirationMinutes": 30
|
||||||
|
},
|
||||||
|
"FileStorage": {
|
||||||
|
"BaseUrl": "https://api.pm.gozareshgir.ir/uploads",
|
||||||
|
"MaxFileSizeBytes": 104857600,
|
||||||
|
"AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".gif", ".webp" ],
|
||||||
|
"AllowedVideoExtensions": [ ".mp4", ".avi", ".mov", ".wmv" ],
|
||||||
|
"AllowedAudioExtensions": [ ".mp3", ".wav", ".ogg", ".m4a" ],
|
||||||
|
"AllowedDocumentExtensions": [ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user