merg from BugSection

This commit is contained in:
gozareshgir
2026-01-27 16:47:33 +03:30
23 changed files with 1947 additions and 87 deletions

3
.gitignore vendored
View File

@@ -369,3 +369,6 @@ MigrationBackup/
# Storage folder - ignore all uploaded files, thumbnails, and temporary files
ServiceHost/Storage
.env
.env.*

View File

@@ -31,8 +31,9 @@ public static class StaticWorkshopAccounts
/// 381 - مهدی قربانی
/// 392 - عمار حسن دوست
/// 20 - سمیرا الهی نیا
/// 322 - ماهان چمنی
/// </summary>
public static List<long> StaticAccountIds = [2, 3, 380, 381, 392, 20, 476];
public static List<long> StaticAccountIds = [2, 3, 380, 381, 392, 20, 476,322];
/// <summary>
/// این تاریخ در جدول اکانت لفت ورک به این معنیست

5
Directory.Build.props Normal file
View File

@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,81 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
using GozareshgirProgramManager.Application.Services.FileManagement;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using Microsoft.AspNetCore.Http;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.CreateBugSection;
public class CreateBugSectionCommandHandler : IBaseCommandHandler<CreateBugSectionCommand>
{
readonly IUnitOfWork _unitOfWork;
readonly IBugSectionRepository _bugSectionRepository;
readonly IFileUploadService _fileUploadService;
private readonly IAuthHelper _authHelper;
public CreateBugSectionCommandHandler(IUnitOfWork unitOfWork, IBugSectionRepository bugSectionRepository,
IAuthHelper authHelper, IFileUploadService fileUploadService)
{
_unitOfWork = unitOfWork;
_bugSectionRepository = bugSectionRepository;
_authHelper = authHelper;
_fileUploadService = fileUploadService;
}
public async Task<OperationResult> Handle(CreateBugSectionCommand request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId()
?? throw new UnAuthorizedException("کاربر احراز هویت نشده است");
#region Validation
if (_bugSectionRepository.Exists(x => x.TaskId == request.TaskId))
return OperationResult.Failure("برای این بخش قبلا تسک باگ ایجاد شده است");
if (string.IsNullOrWhiteSpace(request.InitialDescription))
return OperationResult.Failure("توضیحات باگ خالی است");
if (request.OriginalAssignedUserId == 0)
return OperationResult.Failure("کاربر انتخاب نشده است");
#endregion
var bug = new BugSection(request.TaskId, request.InitialDescription, request.OriginalAssignedUserId,
request.Priority);
await _bugSectionRepository.CreateAsync(bug);
if (request.Files.Any())
{
foreach (var file in request.Files)
{
var uploadedFile = await _fileUploadService.UploadFileAsync
(
file,
FileCategory.BugSection,
currentUserId
);
if (!uploadedFile.IsSuccess)
{
return OperationResult.Failure(uploadedFile.ErrorMessage ?? "خطا در آپلود فایل");
}
bug.AddDocument(new BugDocument(uploadedFile.FileId!.Value));
}
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
}
public record CreateBugSectionCommand(Guid TaskId, string InitialDescription, long OriginalAssignedUserId, ProjectTaskPriority Priority, List<IFormFile> Files) : IBaseCommand;

View File

@@ -0,0 +1,29 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetBugModalDetails;
public class GetBugModalDetailsQueryHandler : IBaseQueryHandler<GetBugModalDetailsQuery, GetBugModalDetailsResponse>
{
private readonly IProgramManagerDbContext _context;
public GetBugModalDetailsQueryHandler(IProgramManagerDbContext context)
{
_context = context;
}
public async Task<OperationResult<GetBugModalDetailsResponse>> Handle(GetBugModalDetailsQuery request, CancellationToken cancellationToken)
{
var projectTask =await _context.ProjectTasks.Include(ph=>ph.Phase).ThenInclude(p=>p.Project).FirstOrDefaultAsync(x=>x.Id == request.TaskId);
if(projectTask == null)
return OperationResult<GetBugModalDetailsResponse>.NotFound("بخش یافت نشد");
var response = new GetBugModalDetailsResponse(projectTask.Name,projectTask.Phase.Name, projectTask.Phase.Project.Name);
return OperationResult<GetBugModalDetailsResponse>.Success(response);
}
}
public record GetBugModalDetailsQuery(Guid TaskId) : IBaseQuery<GetBugModalDetailsResponse>;
public record GetBugModalDetailsResponse(string TaskName, string PhaseName, string ProjectName);

View File

@@ -28,26 +28,25 @@ public class SendMessageCommandHandler : IBaseCommandHandler<SendMessageCommand,
private readonly ITaskChatMessageRepository _messageRepository;
private readonly IUploadedFileRepository _fileRepository;
private readonly IProjectTaskRepository _taskRepository;
private readonly IFileStorageService _fileStorageService;
private readonly IThumbnailGeneratorService _thumbnailService;
private readonly IFileUploadService _fileUploadService;
private readonly IAuthHelper _authHelper;
public SendMessageCommandHandler(
ITaskChatMessageRepository messageRepository,
IUploadedFileRepository fileRepository,
IProjectTaskRepository taskRepository,
IFileStorageService fileStorageService,
IThumbnailGeneratorService thumbnailService, IAuthHelper authHelper)
IProjectTaskRepository taskRepository,
IAuthHelper authHelper,
IFileUploadService fileUploadService)
{
_messageRepository = messageRepository;
_fileRepository = fileRepository;
_taskRepository = taskRepository;
_fileStorageService = fileStorageService;
_thumbnailService = thumbnailService;
_authHelper = authHelper;
_fileUploadService = fileUploadService;
}
public async Task<OperationResult<MessageDto>> Handle(SendMessageCommand request, CancellationToken cancellationToken)
public async Task<OperationResult<MessageDto>> Handle(SendMessageCommand request,
CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId()
?? throw new UnAuthorizedException("کاربر احراز هویت نشده است");
@@ -57,75 +56,21 @@ public class SendMessageCommandHandler : IBaseCommandHandler<SendMessageCommand,
{
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
var uploadedFile = await _fileUploadService.UploadFileAsync
(
request.File,
FileCategory.TaskChatMessage,
currentUserId
);
await _fileRepository.AddAsync(uploadedFile);
await _fileRepository.SaveChangesAsync();
try
if (!uploadedFile.IsSuccess)
{
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, category: "TaskChatMessage");
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}");
return OperationResult<MessageDto>.Failure(uploadedFile.ErrorMessage ?? "خطا در آپلود فایل");
}
uploadedFileId = uploadedFile.FileId!.Value;
}
var message = new TaskChatMessage(
@@ -209,4 +154,4 @@ public class SendMessageCommandHandler : IBaseCommandHandler<SendMessageCommand,
return FileType.Document;
}
}
}

View File

@@ -0,0 +1,69 @@
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
using Microsoft.AspNetCore.Http;
namespace GozareshgirProgramManager.Application.Services.FileManagement;
/// <summary>
/// سرویس آپلود و مدیریت کامل فایل
/// این سرویس تمام مراحل آپلود، ذخیره، تولید thumbnail و... را انجام می‌دهد
/// </summary>
public interface IFileUploadService
{
/// <summary>
/// آپلود فایل با تمام مراحل پردازش
/// </summary>
/// <param name="file">فایل برای آپلود</param>
/// <param name="category">دسته‌بندی فایل</param>
/// <param name="uploadedByUserId">شناسه کاربر آپلودکننده</param>
/// <param name="maxFileSizeBytes">حداکثر حجم مجاز فایل (پیش‌فرض: 100MB)</param>
/// <returns>شناسه فایل آپلود شده یا null در صورت خطا</returns>
Task<FileUploadResult> UploadFileAsync(
IFormFile file,
FileCategory category,
long uploadedByUserId,
long maxFileSizeBytes = 100 * 1024 * 1024);
/// <summary>
/// آپلود فایل با Stream
/// </summary>
Task<FileUploadResult> UploadFileFromStreamAsync(
Stream fileStream,
string fileName,
string contentType,
FileCategory category,
long uploadedByUserId,
long maxFileSizeBytes = 100 * 1024 * 1024);
}
/// <summary>
/// نتیجه عملیات آپلود فایل
/// </summary>
public class FileUploadResult
{
public bool IsSuccess { get; set; }
public Guid? FileId { get; set; }
public string? ErrorMessage { get; set; }
public string? StorageUrl { get; set; }
public string? ThumbnailUrl { get; set; }
public static FileUploadResult Success(Guid fileId, string storageUrl, string? thumbnailUrl = null)
{
return new FileUploadResult
{
IsSuccess = true,
FileId = fileId,
StorageUrl = storageUrl,
ThumbnailUrl = thumbnailUrl
};
}
public static FileUploadResult Failed(string errorMessage)
{
return new FileUploadResult
{
IsSuccess = false,
ErrorMessage = errorMessage
};
}
}

View File

@@ -29,7 +29,7 @@ public interface IProgramManagerDbContext
DbSet<TaskSection> TaskSections { get; set; }
DbSet<ProjectSection> ProjectSections { get; set; }
DbSet<PhaseSection> PhaseSections { get; set; }
DbSet<BugSection> BugSections { get; set; }
DbSet<ProjectTask> ProjectTasks { get; set; }
DbSet<TaskChatMessage> TaskChatMessages { get; set; }

View File

@@ -10,7 +10,7 @@ public enum FileCategory
ProjectDocument = 3, // مستندات پروژه
UserProfilePhoto = 4, // عکس پروفایل کاربر
Report = 5, // گزارش
Other = 6, // سایر
TaskSectionRevision
Other = 6, // سایر
BugSection = 7, // تسک باگ
}

View File

@@ -0,0 +1,14 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
public class BugDocument
{
public BugDocument(Guid fileId)
{
FileId = fileId;
}
private BugDocument() { } // EF
public Guid Id { get; private set; }
public Guid FileId { get; private set; }
public BugSection BugSection { get; private set; }
}

View File

@@ -0,0 +1,65 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
public class BugSection : EntityBase<Guid>
{
public BugSection(Guid taskId, string initialDescription, long originalAssignedUserId, ProjectTaskPriority priority)
{
TaskId = taskId;
InitialDescription = initialDescription;
Status = TaskSectionStatus.ReadyToStart;
OriginalAssignedUserId = originalAssignedUserId;
Priority = priority;
}
// برای EF Core
private BugSection()
{
}
/// <summary>
/// آی دی تسک - بخش فرعی
/// </summary>
public Guid TaskId { get; private set; }
/// <summary>
/// توضیحات مدیر
/// </summary>
public string InitialDescription { get; set; }
/// <summary>
/// وضعیت باگ گزارش شده
/// </summary>
public TaskSectionStatus Status { get; private set; }
// شخصی که برای اولین بار این بخش به او اختصاص داده شده (مالک اصلی)
public long OriginalAssignedUserId { get; private set; }
/// <summary>
/// اولویت رسیدگی
/// </summary>
public ProjectTaskPriority Priority { get; private set; }
// Navigation to ProjectTask (must be Task level)
public ProjectTask ProjectTask { get; private set; } = null!;
/// <summary>
/// لیست مدارک و فایلها
/// </summary>
public List<BugDocument> BugDocuments { get; private set; } = new();
public void AddDocument(BugDocument document)
{
BugDocuments.Add(document);
}
}

View File

@@ -19,7 +19,8 @@ public class ProjectTask : ProjectHierarchyNode
public ProjectTask(string name, Guid phaseId,ProjectTaskPriority priority, string? description = null) : base(name, description)
{
PhaseId = phaseId;
_sections = new List<TaskSection.TaskSection>();
_sections = new List<TaskSection>();
BugSectionList = new List<BugSection>();
Priority = priority;
Priority = ProjectTaskPriority.Low;
AddDomainEvent(new TaskCreatedEvent(Id, phaseId, name));
@@ -27,7 +28,8 @@ public class ProjectTask : ProjectHierarchyNode
public Guid PhaseId { get; private set; }
public ProjectPhase Phase { get; private set; } = null!;
public IReadOnlyList<TaskSection.TaskSection> Sections => _sections.AsReadOnly();
public IReadOnlyList<TaskSection> Sections => _sections.AsReadOnly();
public List<BugSection> BugSectionList { get; set; }
// Task-specific properties
public Enums.TaskStatus Status { get; private set; } = Enums.TaskStatus.NotStarted;

View File

@@ -0,0 +1,9 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
public interface IBugSectionRepository : IRepository<Guid,BugSection>
{
}

View File

@@ -76,7 +76,8 @@ public static class DependencyInjection
services.AddScoped<ITaskSectionActivityRepository, TaskSectionActivityRepository>();
services.AddScoped<IPhaseSectionRepository, PhaseSectionRepository>();
services.AddScoped<IProjectSectionRepository, ProjectSectionRepository>();
services.AddScoped<IBugSectionRepository, BugSectionRepository>();
services.AddScoped<ISkillRepository, SkillRepository>();
services.AddScoped<IUserRefreshTokenRepository, UserRefreshTokenRepository>();
@@ -88,6 +89,7 @@ public static class DependencyInjection
// File Storage Services
services.AddScoped<IFileStorageService, Services.FileManagement.LocalFileStorageService>();
services.AddScoped<IThumbnailGeneratorService, Services.FileManagement.ThumbnailGeneratorService>();
services.AddScoped<IFileUploadService, Services.FileManagement.FileUploadService>();
// JWT Settings
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));

View File

@@ -0,0 +1,77 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GozareshgirProgramManager.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class BugSectioninit : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BugSections",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TaskId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
InitialDescription = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
OriginalAssignedUserId = table.Column<long>(type: "bigint", nullable: false),
Priority = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BugSections", x => x.Id);
table.ForeignKey(
name: "FK_BugSections_ProjectTasks_TaskId",
column: x => x.TaskId,
principalTable: "ProjectTasks",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "BugDocuments",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
FileId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
BugSectionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BugDocuments", x => x.Id);
table.ForeignKey(
name: "FK_BugDocuments_BugSections_BugSectionId",
column: x => x.BugSectionId,
principalTable: "BugSections",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_BugDocuments_BugSectionId",
table: "BugDocuments",
column: "BugSectionId");
migrationBuilder.CreateIndex(
name: "IX_BugSections_TaskId",
table: "BugSections",
column: "TaskId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BugDocuments");
migrationBuilder.DropTable(
name: "BugSections");
}
}
}

View File

@@ -227,7 +227,42 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
b.ToTable("UploadedFiles", (string)null);
});
modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase.PhaseSection", b =>
modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.BugSection", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime2");
b.Property<string>("InitialDescription")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<long>("OriginalAssignedUserId")
.HasColumnType("bigint");
b.Property<string>("Priority")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<Guid>("TaskId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("TaskId");
b.ToTable("BugSections", (string)null);
});
modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.PhaseSection", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -867,7 +902,44 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
b.ToTable("UserRefreshTokens", (string)null);
});
modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase.PhaseSection", b =>
modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.BugSection", b =>
{
b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectTask", "ProjectTask")
.WithMany("BugSectionList")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("GozareshgirProgramManager.Domain.ProjectAgg.Entities.BugDocument", "BugDocuments", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b1.Property<Guid>("BugSectionId")
.HasColumnType("uniqueidentifier");
b1.Property<Guid>("FileId")
.HasColumnType("uniqueidentifier");
b1.HasKey("Id");
b1.HasIndex("BugSectionId");
b1.ToTable("BugDocuments", (string)null);
b1.WithOwner("BugSection")
.HasForeignKey("BugSectionId");
b1.Navigation("BugSection");
});
b.Navigation("BugDocuments");
b.Navigation("ProjectTask");
});
modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.PhaseSection", b =>
{
b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase.ProjectPhase", "Phase")
.WithMany("PhaseSections")
@@ -1170,6 +1242,8 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.ProjectTask", b =>
{
b.Navigation("BugSectionList");
b.Navigation("Sections");
});

View File

@@ -29,6 +29,7 @@ public class ProgramManagerDbContext : DbContext, IProgramManagerDbContext
public DbSet<TaskSection> TaskSections { get; set; } = null!;
public DbSet<ProjectSection> ProjectSections { get; set; } = null!;
public DbSet<PhaseSection> PhaseSections { get; set; } = null!;
public DbSet<BugSection> BugSections { get; set; } = null!;
// New Hierarchy entities
public DbSet<Project> Projects { get; set; } = null!;

View File

@@ -0,0 +1,51 @@
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace GozareshgirProgramManager.Infrastructure.Persistence.Mappings;
public class BugSectionMapping : IEntityTypeConfiguration<BugSection>
{
public void Configure(EntityTypeBuilder<BugSection> builder)
{
builder.ToTable("BugSections");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.ValueGeneratedNever();
builder.Property(x => x.TaskId)
.IsRequired();
builder.Property(x => x.Status)
.HasConversion<string>()
.HasMaxLength(50)
.IsRequired();
builder.Property(x => x.Priority)
.HasConversion<string>()
.HasMaxLength(50)
.IsRequired();
builder.Property(x => x.InitialDescription)
.HasMaxLength(500)
.IsRequired(false);
builder.OwnsMany(x => x.BugDocuments, navigationBuilder =>
{
navigationBuilder.ToTable("BugDocuments");
navigationBuilder.HasKey(x => x.Id);
navigationBuilder.WithOwner(x => x.BugSection);
});
// Navigation to ProjectTask (Task level)
builder.HasOne(x => x.ProjectTask)
.WithMany(t => t.BugSectionList)
.HasForeignKey(x => x.TaskId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@@ -75,6 +75,12 @@ public class ProjectTaskMapping : IEntityTypeConfiguration<ProjectTask>
.WithOne(s => s.Task)
.HasForeignKey(s => s.TaskId)
.OnDelete(DeleteBehavior.Cascade);
// One-to-many relationship with BugSections
builder.HasMany(t => t.BugSectionList)
.WithOne(s => s.ProjectTask)
.HasForeignKey(s => s.TaskId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@@ -0,0 +1,16 @@
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using GozareshgirProgramManager.Infrastructure.Persistence._Common;
using GozareshgirProgramManager.Infrastructure.Persistence.Context;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Infrastructure.Persistence.Repositories;
public class BugSectionRepository : RepositoryBase<Guid,BugSection>, IBugSectionRepository
{
private readonly ProgramManagerDbContext _context;
public BugSectionRepository(ProgramManagerDbContext context) : base(context)
{
_context = context;
}
}

View File

@@ -0,0 +1,232 @@
using GozareshgirProgramManager.Application.Services.FileManagement;
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement;
/// <summary>
/// پیاده‌سازی سرویس آپلود کامل فایل
/// </summary>
public class FileUploadService : IFileUploadService
{
private readonly IUploadedFileRepository _fileRepository;
private readonly IFileStorageService _fileStorageService;
private readonly IThumbnailGeneratorService _thumbnailService;
private readonly ILogger<FileUploadService> _logger;
public FileUploadService(
IUploadedFileRepository fileRepository,
IFileStorageService fileStorageService,
IThumbnailGeneratorService thumbnailService,
ILogger<FileUploadService> logger)
{
_fileRepository = fileRepository;
_fileStorageService = fileStorageService;
_thumbnailService = thumbnailService;
_logger = logger;
}
public async Task<FileUploadResult> UploadFileAsync(
IFormFile file,
FileCategory category,
long uploadedByUserId,
long maxFileSizeBytes = 100 * 1024 * 1024)
{
try
{
// اعتبارسنجی ورودی
if (file.Length == 0)
{
return FileUploadResult.Failed("فایل خالی است");
}
if (file.Length > maxFileSizeBytes)
{
var maxSizeMb = maxFileSizeBytes / (1024 * 1024);
return FileUploadResult.Failed($"حجم فایل بیش از حد مجاز است (حداکثر {maxSizeMb}MB)");
}
using var stream = file.OpenReadStream();
return await UploadFileFromStreamAsync(
stream,
file.FileName,
file.ContentType,
category,
uploadedByUserId,
maxFileSizeBytes);
}
catch (Exception ex)
{
_logger.LogError(ex, "خطا در آپلود فایل {FileName}", file.FileName);
return FileUploadResult.Failed($"خطا در آپلود فایل: {ex.Message}");
}
}
public async Task<FileUploadResult> UploadFileFromStreamAsync(
Stream fileStream,
string fileName,
string contentType,
FileCategory category,
long uploadedByUserId,
long maxFileSizeBytes = 100 * 1024 * 1024)
{
UploadedFile? uploadedFile = null;
try
{
// تشخیص نوع فایل
var fileType = DetectFileType(contentType, Path.GetExtension(fileName));
// ایجاد رکورد فایل در دیتابیس
uploadedFile = new UploadedFile(
originalFileName: fileName,
fileSizeBytes: fileStream.Length,
mimeType: contentType,
fileType: fileType,
category: category,
uploadedByUserId: uploadedByUserId,
storageProvider: StorageProvider.LocalFileSystem
);
await _fileRepository.AddAsync(uploadedFile);
await _fileRepository.SaveChangesAsync();
// آپلود فایل به استوریج
var categoryFolder = category.ToString();
var uploadResult = await _fileStorageService.UploadAsync(
fileStream,
uploadedFile.UniqueFileName,
categoryFolder
);
// به‌روزرسانی اطلاعات آپلود
uploadedFile.CompleteUpload(uploadResult.StoragePath, uploadResult.StorageUrl);
// پردازش‌های خاص بر اساس نوع فایل
string? thumbnailUrl = null;
if (fileType == FileType.Image)
{
thumbnailUrl = await ProcessImageAsync(uploadedFile, uploadResult.StoragePath, categoryFolder);
}
else if (fileType == FileType.Video)
{
thumbnailUrl = await ProcessVideoAsync(uploadedFile, uploadResult.StoragePath, categoryFolder);
}
// ذخیره تغییرات نهایی
await _fileRepository.UpdateAsync(uploadedFile);
await _fileRepository.SaveChangesAsync();
_logger.LogInformation(
"فایل {FileName} با شناسه {FileId} با موفقیت آپلود شد",
fileName,
uploadedFile.Id);
return FileUploadResult.Success(uploadedFile.Id, uploadResult.StorageUrl, thumbnailUrl);
}
catch (Exception ex)
{
_logger.LogError(ex, "خطا در آپلود فایل {FileName}", fileName);
// در صورت خطا، فایل آپلود شده را حذف کنیم
if (uploadedFile != null)
{
try
{
await _fileRepository.DeleteAsync(uploadedFile);
await _fileRepository.SaveChangesAsync();
}
catch (Exception deleteEx)
{
_logger.LogError(deleteEx, "خطا در حذف فایل ناموفق {FileId}", uploadedFile.Id);
}
}
return FileUploadResult.Failed($"خطا در آپلود فایل: {ex.Message}");
}
}
/// <summary>
/// پردازش تصویر (ابعاد و thumbnail)
/// </summary>
private async Task<string?> ProcessImageAsync(UploadedFile file, string storagePath, string categoryFolder)
{
try
{
// دریافت ابعاد تصویر
var dimensions = await _thumbnailService.GetImageDimensionsAsync(storagePath);
if (dimensions.HasValue)
{
file.SetImageDimensions(dimensions.Value.Width, dimensions.Value.Height);
}
// تولید thumbnail
var thumbnail = await _thumbnailService.GenerateImageThumbnailAsync(
storagePath,
category: categoryFolder);
if (thumbnail.HasValue)
{
file.SetThumbnail(thumbnail.Value.ThumbnailUrl);
return thumbnail.Value.ThumbnailUrl;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "خطا در پردازش تصویر {FileId}", file.Id);
}
return null;
}
/// <summary>
/// پردازش ویدیو (thumbnail)
/// </summary>
private async Task<string?> ProcessVideoAsync(UploadedFile file, string storagePath, string categoryFolder)
{
try
{
// تولید thumbnail از ویدیو
var thumbnail = await _thumbnailService.GenerateVideoThumbnailAsync(
storagePath,
category: categoryFolder);
if (thumbnail.HasValue)
{
file.SetThumbnail(thumbnail.Value.ThumbnailUrl);
return thumbnail.Value.ThumbnailUrl;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "خطا در پردازش ویدیو {FileId}", file.Id);
}
return null;
}
/// <summary>
/// تشخیص نوع فایل از روی MIME type و extension
/// </summary>
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;
}
}

View File

@@ -1,29 +1,31 @@
using System.Runtime.InteropServices;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AssignProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoPendingFullTimeTaskSections;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoStopOverTimeTaskSections;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoUpdateDeployStatus;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeDeployStatusProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeStatusSection;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeTaskPriority;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.CreateBugSection;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.CreateProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.DeleteProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.EditProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.TransferSection;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectAssignDetails;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectBoardDetail;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectBoardList;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardDetail;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardList;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ServiceHost.BaseControllers;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeTaskPriority;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoPendingFullTimeTaskSections;
using System.Runtime.InteropServices;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetBugModalDetails;
namespace ServiceHost.Areas.Admin.Controllers.ProgramManager;
@@ -176,5 +178,32 @@ public class ProjectController : ProgramManagerBaseController
var res = await _mediator.Send(command);
return res;
}
/// <summary>
/// دریافت اطلاعات مودال ایجاد تسک باگ
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
[HttpGet("GetCreateBugModalDetails")]
public async Task<ActionResult<OperationResult<GetBugModalDetailsResponse>>> GetCreateBugModalDetails(
[FromQuery] GetBugModalDetailsQuery query)
{
var res = await _mediator.Send(query);
return res;
}
/// <summary>
/// ایجاد تسک باگ
/// </summary>
/// <param name="commnd"></param>
/// <returns></returns>
[HttpPost("CreateBugSection")]
public async Task<ActionResult<OperationResult>> CreateBugSection([FromBody] CreateBugSectionCommand commnd)
{
var res = await _mediator.Send(commnd);
return res;
}
}