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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user