feat: integrate PYMS payment gateway, add blog/sitepage/image services, local file manager
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m44s

Payment Gateway:
- Add PYMSPaymentService: IPaymentGatewayService via gRPC to PYMS microservice
- Add ZarinPalPaymentService: direct ZarinPal integration (backup)
- Register 'pyms' payment provider in DI ConfigureServices
- Add PYMS proto files (pyms_transaction.proto, pyms_public_messages.proto)
- Fix VerifyDiscountWalletCharge: pass 'OK' as status instead of Authority
- Update appsettings: PaymentProvider=pyms, sandbox mode, merchant ID

Blog System:
- Add BlogCategory, BlogPost, BlogPostImage entities and CQRS
- Add proto files and gRPC services for blog management
- Add Mapster profiles for blog responses

Content Management:
- Add SitePage entity and CQRS for static pages
- Add proto and gRPC service for site pages

Image/File Management:
- Add LocalFileManager with disk storage + base64 serving + FMS fallback
- Add ImagePathResolverInterceptor for gRPC responses
- Add ImageResolverService for explicit image resolution
- Add UploadsController for public file serving with FMS fallback
- Add PaymentCallbackController for discount order payment callbacks

Database:
- Add blog and content entity migrations
- Remove ImagePath MaxLength constraints
- Remove old FileManagementService (replaced by LocalFileManager)
This commit is contained in:
masoodafar-web
2026-02-15 23:01:16 +03:30
parent 5a4e4a960d
commit 2502cbbda2
177 changed files with 16632 additions and 487 deletions
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common.FileManager;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using Microsoft.Extensions.Logging;
@@ -7,64 +8,52 @@ namespace CMSMicroservice.Application.ProductsCQ.Commands.AddProductImage;
public class AddProductImageCommandHandler : IRequestHandler<AddProductImageCommand, AddProductImageResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IFileManagementService _fileManagementService;
private readonly IFileManager _fileManager;
private readonly ILogger<AddProductImageCommandHandler> _logger;
public AddProductImageCommandHandler(
IApplicationDbContext context,
IFileManagementService fileManagementService,
IFileManager fileManager,
ILogger<AddProductImageCommandHandler> logger)
{
_context = context;
_fileManagementService = fileManagementService;
_fileManager = fileManager;
_logger = logger;
}
public async Task<AddProductImageResponseDto> Handle(AddProductImageCommand request,
CancellationToken cancellationToken)
{
// Verify product exists
// بررسی وجود محصول
var productExists = await _context.Products
.AnyAsync(p => p.Id == request.ProductId, cancellationToken);
if (!productExists)
throw new NotFoundException(nameof(Product), request.ProductId);
string imagePath = string.Empty;
string thumbnailPath = string.Empty;
// بدون فایل تصویر، کاری انجام نمی‌شود
if (request.ImageFileBytes is not { Length: > 0 })
throw new FileUploadException("فایل تصویر ارسال نشده است");
// Upload image to FMS
if (request.ImageFileBytes is { Length: > 0 })
{
try
{
var (mainPath, thumbPath) = await _fileManagementService.UploadImageWithThumbnailAsync(
"Images/Products/Gallery",
request.ImageFileBytes,
request.ImageFileMime ?? "image/jpeg",
request.ImageFileName,
cancellationToken);
// آپلود تصویر به FMS (اگر خطا بخوره، exception پرتاب می‌شه و entity ذخیره نمیشه)
var uploaded = await _fileManager.UploadImageAsync(
"Images/Products/Gallery",
request.ImageFileBytes,
request.ImageFileMime ?? "image/jpeg",
request.ImageFileName,
cancellationToken);
imagePath = mainPath ?? string.Empty;
thumbnailPath = thumbPath ?? string.Empty;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload gallery image to FMS for product {ProductId}", request.ProductId);
}
}
// Create ProductImage entity
// ساخت رکورد تصویر
var productImage = new ProductImage
{
Title = request.Title,
ImagePath = imagePath,
ImageThumbnailPath = thumbnailPath
ImagePath = uploaded.Main.Path,
ImageThumbnailPath = uploaded.Thumbnail.Path
};
await _context.ProductImages.AddAsync(productImage, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// Create ProductGallery join entity
// اتصال تصویر به محصول
var productGallery = new ProductGallery
{
ProductId = request.ProductId,
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common.FileManager;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using Microsoft.Extensions.Logging;
@@ -7,16 +8,16 @@ namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProductsCommand, CreateNewProductsResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IFileManagementService _fileManagementService;
private readonly IFileManager _fileManager;
private readonly ILogger<CreateNewProductsCommandHandler> _logger;
public CreateNewProductsCommandHandler(
IApplicationDbContext context,
IFileManagementService fileManagementService,
IFileManager fileManager,
ILogger<CreateNewProductsCommandHandler> logger)
{
_context = context;
_fileManagementService = fileManagementService;
_fileManager = fileManager;
_logger = logger;
}
@@ -32,56 +33,38 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
Price = request.Price,
Discount = request.Discount,
Rate = request.Rate,
ImagePath = request.ImagePath ?? string.Empty,
ThumbnailPath = request.ThumbnailPath ?? string.Empty,
ImagePath = string.Empty,
ThumbnailPath = string.Empty,
SaleCount = request.SaleCount,
ViewCount = request.ViewCount,
RemainingCount = request.RemainingCount
};
// Handle image upload to FMS if file bytes provided
// آپلود تصویر اصلی (اگر ارسال شده باشد)
if (request.ImageFileBytes is { Length: > 0 })
{
try
{
var (mainPath, thumbPath) = await _fileManagementService.UploadImageWithThumbnailAsync(
"Images/Products",
request.ImageFileBytes,
request.ImageFileMime ?? "image/jpeg",
request.ImageFileName,
cancellationToken);
var result = await _fileManager.UploadImageAsync(
"Images/Products",
request.ImageFileBytes,
request.ImageFileMime ?? "image/jpeg",
request.ImageFileName,
cancellationToken);
if (!string.IsNullOrWhiteSpace(mainPath))
entity.ImagePath = mainPath;
if (!string.IsNullOrWhiteSpace(thumbPath))
entity.ThumbnailPath = thumbPath;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload product image to FMS");
}
entity.ImagePath = result.Main.Path;
entity.ThumbnailPath = result.Thumbnail.Path;
}
// Handle separate thumbnail upload if provided (and not already set from main image)
if (request.ThumbnailFileBytes is { Length: > 0 } && string.IsNullOrWhiteSpace(entity.ThumbnailPath))
{
try
{
var thumbPath = await _fileManagementService.UploadFileAsync(
"Images/Products/Thumbnails",
request.ThumbnailFileBytes,
request.ThumbnailFileMime ?? "image/jpeg",
request.ThumbnailFileName,
cancellationToken);
if (!string.IsNullOrWhiteSpace(thumbPath))
entity.ThumbnailPath = thumbPath;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload product thumbnail to FMS");
}
// آپلود بندانگشتی جداگانه (اختیاری — جایگزین بندانگشتی خودکار)
if (request.ThumbnailFileBytes is { Length: > 0 })
{
var thumbResult = await _fileManager.UploadAsync(
"Images/Products/Thumbnails",
request.ThumbnailFileBytes,
request.ThumbnailFileMime ?? "image/jpeg",
request.ThumbnailFileName,
cancellationToken);
entity.ThumbnailPath = thumbResult.Path;
}
await _context.Products.AddAsync(entity, cancellationToken);
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common.FileManager;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using Microsoft.Extensions.Logging;
@@ -7,16 +8,16 @@ namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsCommand, Unit>
{
private readonly IApplicationDbContext _context;
private readonly IFileManagementService _fileManagementService;
private readonly IFileManager _fileManager;
private readonly ILogger<UpdateProductsCommandHandler> _logger;
public UpdateProductsCommandHandler(
IApplicationDbContext context,
IFileManagementService fileManagementService,
IFileManager fileManager,
ILogger<UpdateProductsCommandHandler> logger)
{
_context = context;
_fileManagementService = fileManagementService;
_fileManager = fileManager;
_logger = logger;
}
@@ -38,57 +39,31 @@ public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsComman
entity.ViewCount = request.ViewCount;
entity.RemainingCount = request.RemainingCount;
// Handle image upload to FMS if new file bytes provided
// آپلود تصویر اصلی جدید (اگر ارسال شده باشد)
if (request.ImageFileBytes is { Length: > 0 })
{
try
{
var (mainPath, thumbPath) = await _fileManagementService.UploadImageWithThumbnailAsync(
"Images/Products",
request.ImageFileBytes,
request.ImageFileMime ?? "image/jpeg",
request.ImageFileName,
cancellationToken);
var result = await _fileManager.UploadImageAsync(
"Images/Products",
request.ImageFileBytes,
request.ImageFileMime ?? "image/jpeg",
request.ImageFileName,
cancellationToken);
if (!string.IsNullOrWhiteSpace(mainPath))
entity.ImagePath = mainPath;
if (!string.IsNullOrWhiteSpace(thumbPath))
entity.ThumbnailPath = thumbPath;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload updated product image to FMS for product {ProductId}", request.Id);
}
}
else
{
// If no new file uploaded, keep existing paths or update from request
if (!string.IsNullOrWhiteSpace(request.ImagePath))
entity.ImagePath = request.ImagePath;
if (!string.IsNullOrWhiteSpace(request.ThumbnailPath))
entity.ThumbnailPath = request.ThumbnailPath;
entity.ImagePath = result.Main.Path;
entity.ThumbnailPath = result.Thumbnail.Path;
}
// Handle separate thumbnail upload if provided
// آپلود بندانگشتی جداگانه (اختیاری — جایگزین بندانگشتی خودکار)
if (request.ThumbnailFileBytes is { Length: > 0 })
{
try
{
var thumbPath = await _fileManagementService.UploadFileAsync(
"Images/Products/Thumbnails",
request.ThumbnailFileBytes,
request.ThumbnailFileMime ?? "image/jpeg",
request.ThumbnailFileName,
cancellationToken);
var thumbResult = await _fileManager.UploadAsync(
"Images/Products/Thumbnails",
request.ThumbnailFileBytes,
request.ThumbnailFileMime ?? "image/jpeg",
request.ThumbnailFileName,
cancellationToken);
if (!string.IsNullOrWhiteSpace(thumbPath))
entity.ThumbnailPath = thumbPath;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload updated product thumbnail to FMS for product {ProductId}", request.Id);
}
entity.ThumbnailPath = thumbResult.Path;
}
_context.Products.Update(entity);
@@ -21,5 +21,6 @@ public class GetCustomerProductsByFilterQuery : IRequest<GetCustomerProductsByFi
public int? SaleCount { get; set; }
public int? ViewCount { get; set; }
public int? RemainingCount { get; set; }
public bool? IsActive { get; set; }
public List<long>? CategoryIds { get; set; }
}
@@ -59,6 +59,9 @@ public class GetCustomerProductsByFilterQueryHandler : IRequestHandler<GetCustom
if (request.CategoryIds != null && request.CategoryIds.Any())
query = query.Where(x => x.ProductCategories.Any(pc => request.CategoryIds.Contains(pc.CategoryId)));
if (request.IsActive.HasValue)
query = query.Where(x => x.IsDeleted != request.IsActive.Value);
// Apply sorting
if (!string.IsNullOrEmpty(request.SortBy))
query = query.ApplyOrder(request.SortBy);
@@ -99,6 +102,7 @@ public class GetCustomerProductsByFilterQueryHandler : IRequestHandler<GetCustom
SaleCount = p.SaleCount,
ViewCount = p.ViewCount,
RemainingCount = p.RemainingCount,
IsActive = !p.IsDeleted,
Categories = p.ProductCategories?.Select(pc => new ProductCategoryPathModel
{
CategoryId = pc.CategoryId,
@@ -23,6 +23,7 @@ public class CustomerProductModel
public int SaleCount { get; set; }
public int ViewCount { get; set; }
public int RemainingCount { get; set; }
public bool IsActive { get; set; }
public List<ProductCategoryPathModel> Categories { get; set; }
}