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
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:
@@ -18,7 +18,10 @@ public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest> where T
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
var userId = _currentUserService.UserId ?? string.Empty;
|
||||
_logger.LogInformation("Request: {Name} {@UserId} {@Request}",
|
||||
requestName, userId, request);
|
||||
var safeLog = request?.ToString() ?? "";
|
||||
if (safeLog.Length > 2000)
|
||||
safeLog = safeLog[..2000] + "... [TRUNCATED]";
|
||||
_logger.LogInformation("Request: {Name} {UserId} {Request}",
|
||||
requestName, userId, safeLog);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Behaviours;
|
||||
|
||||
public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
public partial class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
private readonly Stopwatch _timer;
|
||||
private readonly ILogger<TRequest> _logger;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
[GeneratedRegex(@"ImageFile(?:Bytes|Mime|FileName)|ImageFile|File", RegexOptions.None)]
|
||||
private static partial Regex BinaryPropertyPattern();
|
||||
|
||||
public PerformanceBehaviour(ILogger<TRequest> logger, ICurrentUserService currentUserService)
|
||||
{
|
||||
_timer = new Stopwatch();
|
||||
@@ -33,11 +37,20 @@ public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequ
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
var userId = _currentUserService.UserId ?? string.Empty;
|
||||
var safeLog = SanitizeForLog(request);
|
||||
|
||||
_logger.LogWarning("Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@Request}",
|
||||
requestName, elapsedMilliseconds, userId, request);
|
||||
_logger.LogWarning("Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {UserId} {Request}",
|
||||
requestName, elapsedMilliseconds, userId, safeLog);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static string SanitizeForLog(TRequest request)
|
||||
{
|
||||
var text = request?.ToString() ?? "";
|
||||
if (text.Length > 2000)
|
||||
return text[..2000] + "... [TRUNCATED]";
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,11 @@ public class UnhandledExceptionBehaviour<TRequest, TResponse> : IPipelineBehavio
|
||||
catch (Exception ex)
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
var safeLog = request?.ToString() ?? "";
|
||||
if (safeLog.Length > 2000)
|
||||
safeLog = safeLog[..2000] + "... [TRUNCATED]";
|
||||
|
||||
_logger.LogError(ex, "Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
|
||||
_logger.LogError(ex, "Request: Unhandled Exception for Request {Name} {Request}", requestName, safeLog);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace CMSMicroservice.Application.Common.FileManager;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس مدیریت فایل — ذخیره روی دیسک، مسیر نسبی در دیتابیس
|
||||
/// موقع واکشی: خواندن از دیسک و تبدیل به base64 data-URI
|
||||
/// </summary>
|
||||
public interface IFileManager
|
||||
{
|
||||
/// <summary>
|
||||
/// آپلود یک فایل خام به دیسک
|
||||
/// </summary>
|
||||
/// <returns>نتیجه آپلود شامل مسیر نسبی فایل</returns>
|
||||
/// <exception cref="FileUploadException">در صورت خطای آپلود</exception>
|
||||
Task<UploadedFile> UploadAsync(
|
||||
string directory,
|
||||
byte[] fileBytes,
|
||||
string mime,
|
||||
string? fileName = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// آپلود تصویر با بهینهسازی خودکار + ساخت بندانگشتی
|
||||
/// </summary>
|
||||
/// <returns>نتیجه آپلود شامل تصویر اصلی و بندانگشتی (مسیرهای نسبی)</returns>
|
||||
/// <exception cref="FileUploadException">در صورت خطای آپلود</exception>
|
||||
Task<UploadedImage> UploadImageAsync(
|
||||
string directory,
|
||||
byte[] fileBytes,
|
||||
string mime,
|
||||
string? fileName = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// حذف فایل از دیسک
|
||||
/// </summary>
|
||||
Task DeleteAsync(long fileId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// خواندن فایل از دیسک و تبدیل به base64 data-URI
|
||||
/// اگر مسیر از قبل data: باشد، همان را برمیگرداند
|
||||
/// اگر فایل وجود نداشته باشد، رشته خالی برمیگرداند
|
||||
/// </summary>
|
||||
string ResolveImageUrl(string? path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// نتیجه آپلود فایل
|
||||
/// </summary>
|
||||
/// <param name="Id">شناسه فایل در FMS</param>
|
||||
/// <param name="Path">مسیر فایل ذخیرهشده (مثلاً /Images/Products/abc.jpg)</param>
|
||||
public sealed record UploadedFile(long Id, string Path);
|
||||
|
||||
/// <summary>
|
||||
/// نتیجه آپلود تصویر — شامل تصویر اصلی و بندانگشتی
|
||||
/// </summary>
|
||||
/// <param name="Main">تصویر اصلی بهینهشده</param>
|
||||
/// <param name="Thumbnail">تصویر بندانگشتی</param>
|
||||
public sealed record UploadedImage(UploadedFile Main, UploadedFile Thumbnail);
|
||||
|
||||
/// <summary>
|
||||
/// خطای آپلود فایل
|
||||
/// </summary>
|
||||
public class FileUploadException : Exception
|
||||
{
|
||||
public FileUploadException(string message) : base(message) { }
|
||||
public FileUploadException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using CMSMicroservice.Domain.Entities.Content;
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
using CMSMicroservice.Domain.Entities.Order;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
@@ -66,6 +68,17 @@ public interface IApplicationDbContext
|
||||
DbSet<InventoryItem> InventoryItems { get; }
|
||||
DbSet<StockMovement> StockMovements { get; }
|
||||
|
||||
// ============= Blog =============
|
||||
DbSet<BlogPost> BlogPosts { get; }
|
||||
DbSet<BlogCategory> BlogCategories { get; }
|
||||
DbSet<BlogPostCategory> BlogPostCategories { get; }
|
||||
DbSet<BlogPostTag> BlogPostTags { get; }
|
||||
DbSet<BlogPostImage> BlogPostImages { get; }
|
||||
|
||||
// ============= Content Management =============
|
||||
DbSet<SitePage> SitePages { get; }
|
||||
DbSet<SitePageSection> SitePageSections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
|
||||
/// </summary>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for uploading files to FMS (File Management Service)
|
||||
/// </summary>
|
||||
public interface IFileManagementService
|
||||
{
|
||||
/// <summary>
|
||||
/// Uploads a file to FMS and returns the stored file path
|
||||
/// </summary>
|
||||
/// <param name="directory">Target directory path (e.g. "Images/Products")</param>
|
||||
/// <param name="fileBytes">Raw file bytes</param>
|
||||
/// <param name="mime">MIME type (e.g. "image/jpeg")</param>
|
||||
/// <param name="fileName">Original file name</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The stored file path returned by FMS, or null if upload failed</returns>
|
||||
Task<string?> UploadFileAsync(string directory, byte[] fileBytes, string mime, string? fileName, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Uploads an image to FMS with optimization (resize + compress)
|
||||
/// Returns both main image path and thumbnail path
|
||||
/// </summary>
|
||||
/// <param name="directory">Target directory path (e.g. "Images/Products")</param>
|
||||
/// <param name="fileBytes">Raw image bytes</param>
|
||||
/// <param name="mime">MIME type (e.g. "image/jpeg")</param>
|
||||
/// <param name="fileName">Original file name</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Tuple of (mainImagePath, thumbnailPath), either can be null if upload failed</returns>
|
||||
Task<(string? MainImagePath, string? ThumbnailPath)> UploadImageWithThumbnailAsync(
|
||||
string directory, byte[] fileBytes, string mime, string? fileName,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a file from FMS by its ID
|
||||
/// </summary>
|
||||
Task<bool> DeleteFileAsync(long fileId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -27,6 +27,24 @@ public interface IPaymentGatewayService
|
||||
string verificationToken,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// تأیید پرداخت با مبلغ — برای درگاههایی مثل زرینپال که مبلغ را در Verify نیاز دارند
|
||||
/// </summary>
|
||||
/// <param name="refId">شماره مرجع تراکنش (Authority در زرینپال)</param>
|
||||
/// <param name="verificationToken">توکن تأیید از درگاه (Status در زرینپال)</param>
|
||||
/// <param name="amountInToman">مبلغ تراکنش به تومان</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>وضعیت نهایی تراکنش</returns>
|
||||
Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||
string refId,
|
||||
string verificationToken,
|
||||
decimal amountInToman,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// پیشفرض: درگاههایی که Amount نمیخواهند، از overload بدون amount استفاده کنند
|
||||
return VerifyPaymentAsync(refId, verificationToken, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// واریز مبلغ به حساب کاربر (برداشت از کیف پول)
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user