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
@@ -0,0 +1,164 @@
using CMSMicroservice.Application.Common.FileManager;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using System.Collections;
using System.Collections.Generic;
namespace CMSMicroservice.WebApi.Interceptors;
/// <summary>
/// gRPC Interceptor — بعد از اجرای هر سرویس، فیلدهای تصویری response را
/// از مسیر نسبی دیسک به base64 data-URI تبدیل می‌کند
/// </summary>
public class ImagePathResolverInterceptor : Interceptor
{
private readonly IFileManager _fileManager;
private readonly ILogger<ImagePathResolverInterceptor> _logger;
// نام فیلدهایی که مسیر تصویر هستند
private static readonly HashSet<string> ImageFieldNames = new(StringComparer.OrdinalIgnoreCase)
{
"image_path",
"thumbnail_path",
"image_thumbnail_path",
"featured_image_path",
"featured_image_thumbnail_path",
"hero_image_path",
"product_thumbnail_path",
"avatar_path",
"avatar_url",
"avatar"
};
public ImagePathResolverInterceptor(IFileManager fileManager, ILogger<ImagePathResolverInterceptor> logger)
{
_fileManager = fileManager;
_logger = logger;
}
// ── Unary call (اکثر gRPC‌ها) ──
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var response = await continuation(request, context);
if (response is IMessage message)
{
ResolveImagePaths(message);
}
return response;
}
// ── Server streaming ──
public override async Task ServerStreamingServerHandler<TRequest, TResponse>(
TRequest request,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
ServerStreamingServerMethod<TRequest, TResponse> continuation)
{
var wrappedStream = new ImageResolvingStreamWriter<TResponse>(responseStream, this);
await continuation(request, wrappedStream, context);
}
/// <summary>
/// بازگشتی: تمام فیلدهای string با نام تصویری را resolve می‌کند
/// شامل فیلدهای تکراری (repeated) و زیر-پیام‌ها (sub-messages)
/// </summary>
internal void ResolveImagePaths(IMessage message)
{
var descriptor = message.Descriptor;
foreach (var field in descriptor.Fields.InFieldNumberOrder())
{
try
{
if (field.FieldType == FieldType.String && ImageFieldNames.Contains(field.Name))
{
// فیلد string ساده
var accessor = field.Accessor;
var value = accessor.GetValue(message) as string;
if (!string.IsNullOrEmpty(value))
{
var resolved = _fileManager.ResolveImageUrl(value);
accessor.SetValue(message, resolved);
}
}
else if (field.FieldType == FieldType.Message)
{
if (field.IsRepeated)
{
// repeated sub-message
var list = field.Accessor.GetValue(message) as System.Collections.IList;
if (list != null)
{
foreach (var item in list)
{
if (item is IMessage subMsg)
ResolveImagePaths(subMsg);
}
}
}
else
{
// فیلد oneof یا فیلد optional message
var subMessage = field.Accessor.GetValue(message) as IMessage;
if (subMessage != null)
{
// Google.Protobuf.WellKnownTypes.StringValue wrapper
if (subMessage is Google.Protobuf.WellKnownTypes.StringValue sv
&& ImageFieldNames.Contains(field.Name))
{
if (!string.IsNullOrEmpty(sv.Value))
{
sv.Value = _fileManager.ResolveImageUrl(sv.Value);
}
}
else
{
ResolveImagePaths(subMessage);
}
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error resolving image path for field {Field}", field.Name);
}
}
}
/// <summary>
/// Wrapper برای Server Streaming — هر پیام قبل از ارسال resolve می‌شود
/// </summary>
private class ImageResolvingStreamWriter<T> : IServerStreamWriter<T> where T : class
{
private readonly IServerStreamWriter<T> _inner;
private readonly ImagePathResolverInterceptor _interceptor;
public ImageResolvingStreamWriter(IServerStreamWriter<T> inner, ImagePathResolverInterceptor interceptor)
{
_inner = inner;
_interceptor = interceptor;
}
public WriteOptions? WriteOptions
{
get => _inner.WriteOptions;
set => _inner.WriteOptions = value;
}
public Task WriteAsync(T message)
{
if (message is IMessage msg)
_interceptor.ResolveImagePaths(msg);
return _inner.WriteAsync(message);
}
}
}