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,13 +1,20 @@
using Google.Protobuf;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using CMSMicroservice.Application.Common.Interfaces;
using System.Text.RegularExpressions;
namespace CMSMicroservice.WebApi.Common.Behaviours;
public class LoggingBehaviour : Interceptor
public partial class LoggingBehaviour : Interceptor
{
private readonly ILogger _logger;
private readonly ICurrentUserService _currentUserService;
// فیلدهایی که نباید لاگ شوند (بایت‌های تصویر / فایل)
[GeneratedRegex(@"""(File|ImageFile|image_file|file)"":\s*\{[^}]*\}", RegexOptions.Singleline)]
private static partial Regex BinaryFieldPattern();
public LoggingBehaviour(ILogger<LoggingBehaviour> logger, ICurrentUserService currentUserService)
{
_logger = logger;
@@ -21,8 +28,11 @@ public class LoggingBehaviour : Interceptor
{
var requestName = typeof(TRequest).Name;
var userId = _currentUserService.UserId ?? string.Empty;
_logger.LogInformation("gRPC Starting receiving call. Type/Method: {Type} / {Method} Request: {Name} {@UserId} {@Request}",
MethodType.Unary, context.Method , requestName, userId, request);
// لاگ بدون بایت‌های فایل
var safeLog = SanitizeForLog(request);
_logger.LogInformation("gRPC Starting receiving call. Type/Method: {Type} / {Method} Request: {Name} {UserId} {Request}",
MethodType.Unary, context.Method, requestName, userId, safeLog);
try
{
@@ -30,9 +40,26 @@ public class LoggingBehaviour : Interceptor
}
catch (Exception ex)
{
_logger.LogError(ex, "gRPC Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
_logger.LogError(ex, "gRPC Request: Unhandled Exception for Request {Name} {Request}", requestName, safeLog);
throw;
}
}
/// <summary>
/// حذف بایت‌های فایل از لاگ — جایگزینی با [BINARY DATA]
/// </summary>
private static string SanitizeForLog<T>(T request)
{
if (request is IMessage protoMessage)
{
var json = JsonFormatter.Default.Format(protoMessage);
// حذف محتوای فیلدهای باینری
json = BinaryFieldPattern().Replace(json, "\"$1\": \"[BINARY DATA]\"");
// اگر هنوز رشته‌های base64 طولانی هست، خلاصه کن
if (json.Length > 2000)
return json[..2000] + "... [TRUNCATED]";
return json;
}
return request?.ToString() ?? "";
}
}
@@ -1,15 +1,21 @@
using Google.Protobuf;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Text.RegularExpressions;
using CMSMicroservice.Application.Common.Interfaces;
namespace CMSMicroservice.WebApi.Common.Behaviours;
public class PerformanceBehaviour : Interceptor
public partial class PerformanceBehaviour : Interceptor
{
private readonly Stopwatch _timer;
private readonly ILogger _logger;
private readonly ICurrentUserService _currentUserService;
[GeneratedRegex(@"""(File|ImageFile|image_file|file)"":\s*\{[^}]*\}", RegexOptions.Singleline)]
private static partial Regex BinaryFieldPattern();
public PerformanceBehaviour(ILogger<PerformanceBehaviour> logger, ICurrentUserService currentUserService)
{
_timer = new Stopwatch();
@@ -34,11 +40,25 @@ public class PerformanceBehaviour : Interceptor
{
var requestName = typeof(TRequest).Name;
var userId = _currentUserService.UserId ?? string.Empty;
var safeLog = SanitizeForLog(request);
_logger.LogWarning("gRPC Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@Request}",
requestName, elapsedMilliseconds, userId, request);
_logger.LogWarning("gRPC Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {UserId} {Request}",
requestName, elapsedMilliseconds, userId, safeLog);
}
return response;
}
private static string SanitizeForLog<T>(T request)
{
if (request is IMessage protoMessage)
{
var json = JsonFormatter.Default.Format(protoMessage);
json = BinaryFieldPattern().Replace(json, "\"$1\": \"[BINARY DATA]\"");
if (json.Length > 2000)
return json[..2000] + "... [TRUNCATED]";
return json;
}
return request?.ToString() ?? "";
}
}
@@ -0,0 +1,14 @@
using Mapster;
using ProtoBlogCategory = CMSMicroservice.Protobuf.Protos.BlogCategory;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class BlogCategoryProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// CreateBlogCategory: long → CreateBlogCategoryResponse
config.NewConfig<long, ProtoBlogCategory.CreateBlogCategoryResponse>()
.Map(dest => dest.Id, src => src);
}
}
@@ -0,0 +1,26 @@
using CMSMicroservice.Application.BlogPostCQ.Commands.PublishBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.ArchiveBlogPost;
using Google.Protobuf.WellKnownTypes;
using Mapster;
using ProtoBlogPost = CMSMicroservice.Protobuf.Protos.BlogPost;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class BlogPostProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// PublishBlogPost: Command result → Proto response
config.NewConfig<PublishBlogPostResult, ProtoBlogPost.PublishBlogPostResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.PublishedAt, src => src.PublishedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.PublishedAt.Value, DateTimeKind.Utc))
: null);
// ArchiveBlogPost: Command result → Proto response
config.NewConfig<ArchiveBlogPostResult, ProtoBlogPost.ArchiveBlogPostResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message);
}
}
@@ -0,0 +1,132 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Controllers;
/// <summary>
/// Callback endpoint for payment gateways (ZarinPal, etc.)
/// درگاه پرداخت بعد از پرداخت (یا لغو) کاربر را به اینجا redirect می‌کند
/// </summary>
[ApiController]
[AllowAnonymous] // کاربر از درگاه بانک برمی‌گردد — JWT ندارد
[ApiExplorerSettings(GroupName = "cms")]
public class PaymentCallbackController : ControllerBase
{
private readonly ISender _sender;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IApplicationDbContext _context;
private readonly IConfiguration _configuration;
private readonly ILogger<PaymentCallbackController> _logger;
public PaymentCallbackController(
ISender sender,
IPaymentGatewayService paymentGateway,
IApplicationDbContext context,
IConfiguration configuration,
ILogger<PaymentCallbackController> logger)
{
_sender = sender;
_paymentGateway = paymentGateway;
_context = context;
_configuration = configuration;
_logger = logger;
}
/// <summary>
/// Callback برای پرداخت سفارش فروشگاه تخفیفی
/// زرین‌پال کاربر را با Authority و Status به این endpoint برمی‌گرداند
/// </summary>
[HttpGet("/api/payment/discount-order/callback")]
public async Task<IActionResult> DiscountOrderCallback(
[FromQuery] long orderId,
[FromQuery(Name = "Authority")] string? authority,
[FromQuery(Name = "Status")] string? status,
CancellationToken cancellationToken)
{
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
_logger.LogInformation(
"Payment callback received: OrderId={OrderId}, Authority={Authority}, Status={Status}",
orderId, authority, status);
try
{
// پیدا کردن سفارش و تراکنش
var order = await _context.DiscountOrders
.Include(o => o.OrderDetails)
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
if (order == null)
{
_logger.LogError("Payment callback: Order #{OrderId} not found", orderId);
return Redirect($"{frontOfficeBaseUrl}/discount-store/orders?error=order-not-found");
}
var transaction = order.TransactionId.HasValue
? await _context.Transactions.FirstOrDefaultAsync(
t => t.Id == order.TransactionId.Value, cancellationToken)
: null;
// تأیید پرداخت از درگاه
bool paymentSuccess = false;
string? refId = null;
if (string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(authority))
{
// Verify با مبلغ از دیتابیس (تومان)
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
authority,
status!,
order.GatewayAmountPaid, // مبلغ به تومان
cancellationToken);
paymentSuccess = verifyResult.IsSuccess;
refId = verifyResult.TrackingCode ?? verifyResult.RefId;
_logger.LogInformation(
"Payment verification for Order #{OrderId}: Success={Success}, RefId={RefId}, Message={Message}",
orderId, paymentSuccess, refId, verifyResult.Message);
}
else
{
_logger.LogWarning("Payment cancelled by user for Order #{OrderId}", orderId);
}
// تکمیل سفارش از طریق CQRS
var completeResult = await _sender.Send(new CompleteOrderPaymentCommand
{
OrderId = orderId,
TransactionId = transaction?.Id ?? 0,
PaymentSuccess = paymentSuccess,
RefId = refId
}, cancellationToken);
// Redirect به FrontOffice
if (paymentSuccess && completeResult.Success)
{
_logger.LogInformation("Payment completed successfully for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=success");
}
else
{
_logger.LogWarning("Payment failed for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=failed");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment callback error for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=error");
}
}
}
@@ -0,0 +1,150 @@
using System.IO;
using System.Net.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Controllers;
/// <summary>
/// سرویس عمومی سرو تصاویر — فایل‌ها مستقیماً از پوشه Uploads سرو می‌شوند.
/// اگر فایل محلی وجود نداشت، از FMS قدیمی (dl.afrino.co) دانلود و کش می‌شود.
/// </summary>
[ApiController]
[AllowAnonymous]
[ApiExplorerSettings(GroupName = "cms")]
public class UploadsController : ControllerBase
{
private readonly string _uploadRoot;
private readonly string _fmsBaseUrl;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<UploadsController> _logger;
private readonly FileExtensionContentTypeProvider _contentTypeProvider = new();
// حداکثر طول مسیر مجاز (جلوگیری از path traversal)
private const int MaxPathLength = 500;
public UploadsController(
IConfiguration configuration,
IHttpClientFactory httpClientFactory,
ILogger<UploadsController> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_uploadRoot = configuration["FileStorage:UploadPath"]
?? Path.Combine(AppContext.BaseDirectory, "Uploads");
_fmsBaseUrl = configuration["FMS:Address"]?.TrimEnd('/') ?? string.Empty;
}
/// <summary>
/// سرو عمومی فایل از پوشه Uploads.
/// اگر فایل محلی وجود نداشت و FMS تنظیم شده باشد، از FMS دانلود و کش می‌شود.
/// </summary>
/// <param name="path">مسیر نسبی فایل (مثلاً blog/image.jpg)</param>
[HttpGet("uploads/{**path}")]
[ResponseCache(Duration = 86400, Location = ResponseCacheLocation.Any)] // کش مرورگر ۲۴ ساعت
public async Task<IActionResult> GetFile(string path)
{
// ── اعتبارسنجی مسیر ──
if (string.IsNullOrWhiteSpace(path) || path.Length > MaxPathLength)
return BadRequest("مسیر نامعتبر");
// جلوگیری از path traversal
if (path.Contains("..") || path.Contains('\\'))
return BadRequest("مسیر نامعتبر");
var sanitizedPath = path.TrimStart('/');
var fullPath = Path.GetFullPath(Path.Combine(_uploadRoot, sanitizedPath));
// اطمینان از اینکه مسیر درون _uploadRoot باقی می‌ماند
if (!fullPath.StartsWith(Path.GetFullPath(_uploadRoot), StringComparison.OrdinalIgnoreCase))
return BadRequest("مسیر نامعتبر");
// ── سرو فایل محلی ──
if (System.IO.File.Exists(fullPath))
return ServeFile(fullPath);
// ── Fallback: دانلود از FMS قدیمی ──
if (string.IsNullOrWhiteSpace(_fmsBaseUrl))
{
_logger.LogWarning("File not found locally and no FMS configured: {Path}", sanitizedPath);
return NotFound();
}
var downloaded = await TryDownloadFromFmsAsync(sanitizedPath, fullPath);
if (downloaded)
{
_logger.LogInformation("Downloaded and cached from FMS: {Path}", sanitizedPath);
return ServeFile(fullPath);
}
return NotFound();
}
// ────────────────────────────────────────────────────
// سرو فایل با Content-Type مناسب
// ────────────────────────────────────────────────────
private IActionResult ServeFile(string fullPath)
{
if (!_contentTypeProvider.TryGetContentType(fullPath, out var contentType))
contentType = "application/octet-stream";
var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return File(stream, contentType, enableRangeProcessing: true);
}
// ────────────────────────────────────────────────────
// دانلود از FMS قدیمی و ذخیره محلی
// ────────────────────────────────────────────────────
private async Task<bool> TryDownloadFromFmsAsync(string relativePath, string localPath)
{
try
{
var fmsUrl = $"{_fmsBaseUrl}/{relativePath}";
_logger.LogInformation("Attempting FMS download: {Url}", fmsUrl);
using var client = _httpClientFactory.CreateClient("FMS");
using var response = await client.GetAsync(fmsUrl, HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("FMS returned {Status} for {Url}", response.StatusCode, fmsUrl);
return false;
}
// بررسی Content-Type — فقط فایل‌های تصویری/مجاز
var mediaType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
if (!IsAllowedMediaType(mediaType))
{
_logger.LogWarning("FMS returned disallowed content type {Type} for {Url}", mediaType, fmsUrl);
return false;
}
// ذخیره روی دیسک
var directory = Path.GetDirectoryName(localPath)!;
Directory.CreateDirectory(directory);
await using var fileStream = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None);
await response.Content.CopyToAsync(fileStream);
_logger.LogInformation("Cached FMS file locally: {Path} ({Size} bytes)", relativePath, fileStream.Length);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to download from FMS: {Path}", relativePath);
return false;
}
}
private static bool IsAllowedMediaType(string mediaType)
{
return mediaType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)
|| mediaType is "application/pdf"
or "application/octet-stream";
}
}
@@ -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);
}
}
}
+8
View File
@@ -65,6 +65,7 @@ builder.Services.AddGrpc(options =>
options.Interceptors.Add<LoggingBehaviour>();
options.Interceptors.Add<PerformanceBehaviour>();
options.Interceptors.Add<CMSMicroservice.WebApi.Interceptors.PermissionInterceptor>();
options.Interceptors.Add<CMSMicroservice.WebApi.Interceptors.ImagePathResolverInterceptor>();
//options.Interceptors.Add<ExceptionHandlingBehaviour>();
options.EnableDetailedErrors = true;
options.MaxReceiveMessageSize = 1000 * 1024 * 1024; // 1 GB
@@ -92,6 +93,13 @@ builder.Services.AddHealthChecks()
// Add Controllers for REST APIs
builder.Services.AddControllers();
// HttpClient for FMS fallback image download
builder.Services.AddHttpClient("FMS", client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("User-Agent", "FourSat-CMS/1.0");
});
#region Configure Cors
builder.Services.AddCors(options =>
@@ -16,7 +16,6 @@ public class AppVersionService : AppVersionContract.AppVersionContractBase
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
[RequiresPermission(PermissionNames.SettingsView)]
public override async Task<GetAppVersionResponse> GetAppVersion(GetAppVersionRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAppVersionRequest, GetAppVersionQuery, GetAppVersionResponse>(request, context);
@@ -0,0 +1,116 @@
using CMSMicroservice.Protobuf.Protos.BlogCategory;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.BlogCategoryCQ.Commands.CreateBlogCategory;
using CMSMicroservice.Application.BlogCategoryCQ.Commands.UpdateBlogCategory;
using CMSMicroservice.Application.BlogCategoryCQ.Commands.DeleteBlogCategory;
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetAllBlogCategories;
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetActiveBlogCategories;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Mapster;
using MediatR;
using AppModels = CMSMicroservice.Application.Common.Models;
using ProtoMetaData = CMSMicroservice.Protobuf.Protos.MetaData;
namespace CMSMicroservice.WebApi.Services;
public class BlogCategoryService : BlogCategoryContract.BlogCategoryContractBase
{
private readonly ISender _sender;
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public BlogCategoryService(ISender sender, IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_sender = sender;
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<CreateBlogCategoryResponse> CreateBlogCategory(CreateBlogCategoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateBlogCategoryRequest, CreateBlogCategoryCommand, CreateBlogCategoryResponse>(request, context);
}
public override async Task<Empty> UpdateBlogCategory(UpdateBlogCategoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateBlogCategoryRequest, UpdateBlogCategoryCommand>(request, context);
}
public override async Task<Empty> DeleteBlogCategory(DeleteBlogCategoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteBlogCategoryRequest, DeleteBlogCategoryCommand>(request, context);
}
public override async Task<GetBlogCategoryResponse> GetBlogCategory(GetBlogCategoryRequest request, ServerCallContext context)
{
var query = new GetBlogCategoryQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
return MapCategoryToResponse(result);
}
public override async Task<GetAllBlogCategoriesResponse> GetAllBlogCategories(GetAllBlogCategoriesRequest request, ServerCallContext context)
{
var query = new GetAllBlogCategoriesQuery
{
PageNumber = request.PaginationState?.PageNumber ?? 1,
PageSize = request.PaginationState?.PageSize ?? 20,
SearchTerm = request.SortBy
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllBlogCategoriesResponse
{
MetaData = result.MetaData.Adapt<ProtoMetaData>()
};
foreach (var item in result.Models)
response.Models.Add(MapToListItem(item));
return response;
}
public override async Task<GetActiveBlogCategoriesResponse> GetActiveBlogCategories(GetActiveBlogCategoriesRequest request, ServerCallContext context)
{
var query = new GetActiveBlogCategoriesQuery();
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetActiveBlogCategoriesResponse();
foreach (var item in result)
response.Categories.Add(MapToListItem(item));
return response;
}
// ── Private Mapping Helpers ──
private static GetBlogCategoryResponse MapCategoryToResponse(BlogCategoryDto dto)
{
return new GetBlogCategoryResponse
{
Id = dto.Id,
Title = dto.Title ?? string.Empty,
Slug = dto.Slug ?? string.Empty,
Description = dto.Description,
IconName = dto.IconName,
SortOrder = dto.SortOrder,
IsActive = dto.IsActive,
PostCount = dto.PostCount,
Created = dto.Created != default ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.Created, DateTimeKind.Utc)) : null
};
}
private static BlogCategoryListItem MapToListItem(BlogCategoryDto dto)
{
return new BlogCategoryListItem
{
Id = dto.Id,
Title = dto.Title ?? string.Empty,
Slug = dto.Slug ?? string.Empty,
Description = dto.Description,
IconName = dto.IconName,
SortOrder = dto.SortOrder,
IsActive = dto.IsActive,
PostCount = dto.PostCount
};
}
}
@@ -0,0 +1,72 @@
using System.Linq;
using CMSMicroservice.Protobuf.Protos.BlogPostImage;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.BlogPostImageCQ.Commands.AddBlogPostImage;
using CMSMicroservice.Application.BlogPostImageCQ.Commands.DeleteBlogPostImage;
using CMSMicroservice.Application.BlogPostImageCQ.Commands.ReorderBlogPostImages;
using CMSMicroservice.Application.BlogPostImageCQ.Queries.GetBlogPostImages;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using MediatR;
namespace CMSMicroservice.WebApi.Services;
public class BlogPostImageService : BlogPostImageContract.BlogPostImageContractBase
{
private readonly ISender _sender;
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public BlogPostImageService(ISender sender, IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_sender = sender;
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<AddBlogPostImageResponse> AddBlogPostImage(AddBlogPostImageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<AddBlogPostImageRequest, AddBlogPostImageCommand, AddBlogPostImageResponse>(request, context);
}
public override async Task<Empty> DeleteBlogPostImage(DeleteBlogPostImageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteBlogPostImageRequest, DeleteBlogPostImageCommand>(request, context);
}
public override async Task<GetBlogPostImagesResponse> GetBlogPostImages(GetBlogPostImagesRequest request, ServerCallContext context)
{
var query = new GetBlogPostImagesQuery { BlogPostId = request.BlogPostId };
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetBlogPostImagesResponse();
foreach (var item in result)
{
response.Images.Add(new BlogPostImageItem
{
Id = item.Id,
BlogPostId = item.BlogPostId,
ImagePath = item.ImagePath ?? string.Empty,
ThumbnailPath = item.ThumbnailPath ?? string.Empty,
AltText = item.AltText,
Caption = item.Caption,
SortOrder = item.SortOrder
});
}
return response;
}
public override async Task<Empty> ReorderBlogPostImages(ReorderBlogPostImagesRequest request, ServerCallContext context)
{
var command = new ReorderBlogPostImagesCommand
{
Items = request.Items.Select(x => new Application.BlogPostImageCQ.Commands.ReorderBlogPostImages.ImageSortItem
{
Id = x.Id,
SortOrder = x.SortOrder
}).ToList()
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
}
@@ -0,0 +1,231 @@
using System.Collections.Generic;
using System.Linq;
using CMSMicroservice.Protobuf.Protos.BlogPost;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.BlogPostCQ.Commands.CreateBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.UpdateBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.DeleteBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.PublishBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.ArchiveBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.IncrementViewCount;
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPostBySlug;
using CMSMicroservice.Application.BlogPostCQ.Queries.GetAllBlogPosts;
using CMSMicroservice.Application.BlogPostCQ.Queries.GetPublishedBlogPosts;
using CMSMicroservice.Application.BlogPostCQ.Queries.GetFeaturedBlogPosts;
using CMSMicroservice.Domain.Enums;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Mapster;
using MediatR;
using AppModels = CMSMicroservice.Application.Common.Models;
using ProtoMetaData = CMSMicroservice.Protobuf.Protos.MetaData;
namespace CMSMicroservice.WebApi.Services;
public class BlogPostService : BlogPostContract.BlogPostContractBase
{
private readonly ISender _sender;
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public BlogPostService(ISender sender, IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_sender = sender;
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<CreateBlogPostResponse> CreateBlogPost(CreateBlogPostRequest request, ServerCallContext context)
{
var command = new CreateBlogPostCommand
{
Title = request.Title,
Slug = request.Slug,
Summary = request.Summary,
HtmlContent = request.HtmlContent,
FeaturedImagePath = request.FeaturedImagePath,
FeaturedImageThumbnailPath = request.FeaturedImageThumbnailPath,
CategoryIds = request.CategoryIds.ToList(),
TagIds = request.TagIds.ToList(),
IsFeatured = request.IsFeatured,
SortOrder = request.SortOrder,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
var result = await _sender.Send(command, context.CancellationToken);
return new CreateBlogPostResponse { Id = result };
}
public override async Task<Empty> UpdateBlogPost(UpdateBlogPostRequest request, ServerCallContext context)
{
var command = new UpdateBlogPostCommand
{
Id = request.Id,
Title = request.Title,
Slug = request.Slug,
Summary = request.Summary,
HtmlContent = request.HtmlContent,
FeaturedImagePath = request.FeaturedImagePath,
FeaturedImageThumbnailPath = request.FeaturedImageThumbnailPath,
CategoryIds = request.CategoryIds.ToList(),
TagIds = request.TagIds.ToList(),
IsFeatured = request.IsFeatured,
SortOrder = request.SortOrder,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
public override async Task<Empty> DeleteBlogPost(DeleteBlogPostRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteBlogPostRequest, DeleteBlogPostCommand>(request, context);
}
public override async Task<GetBlogPostResponse> GetBlogPost(GetBlogPostRequest request, ServerCallContext context)
{
var query = new GetBlogPostQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
return MapBlogPostDtoToResponse(result);
}
public override async Task<GetBlogPostResponse> GetBlogPostBySlug(GetBlogPostBySlugRequest request, ServerCallContext context)
{
var query = new GetBlogPostBySlugQuery { Slug = request.Slug };
var result = await _sender.Send(query, context.CancellationToken);
return MapBlogPostDtoToResponse(result);
}
public override async Task<GetAllBlogPostsResponse> GetAllBlogPosts(GetAllBlogPostsRequest request, ServerCallContext context)
{
var query = new GetAllBlogPostsQuery
{
PageNumber = request.PaginationState?.PageNumber ?? 1,
PageSize = request.PaginationState?.PageSize ?? 10,
SortBy = request.SortBy,
SearchTerm = request.Filter?.SearchTerm,
Status = request.Filter?.Status.HasValue == true ? (BlogPostStatus?)request.Filter.Status.Value : null,
CategoryId = request.Filter?.CategoryId,
IsFeatured = request.Filter?.IsFeatured
};
var result = await _sender.Send(query, context.CancellationToken);
return MapAllBlogPostsResponse(result);
}
public override async Task<GetAllBlogPostsResponse> GetPublishedBlogPosts(GetPublishedBlogPostsRequest request, ServerCallContext context)
{
var query = new GetPublishedBlogPostsQuery
{
PageNumber = request.PaginationState?.PageNumber ?? 1,
PageSize = request.PaginationState?.PageSize ?? 10,
SearchTerm = request.SearchTerm,
CategoryId = request.CategoryId
};
var result = await _sender.Send(query, context.CancellationToken);
return MapAllBlogPostsResponse(result);
}
public override async Task<GetAllBlogPostsResponse> GetFeaturedBlogPosts(GetFeaturedBlogPostsRequest request, ServerCallContext context)
{
var query = new GetFeaturedBlogPostsQuery { Count = request.Count > 0 ? request.Count : 5 };
var items = await _sender.Send(query, context.CancellationToken);
var response = new GetAllBlogPostsResponse();
foreach (var item in items)
response.Models.Add(MapToListItem(item));
return response;
}
public override async Task<PublishBlogPostResponse> PublishBlogPost(PublishBlogPostRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<PublishBlogPostRequest, PublishBlogPostCommand, PublishBlogPostResponse>(request, context);
}
public override async Task<ArchiveBlogPostResponse> ArchiveBlogPost(ArchiveBlogPostRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ArchiveBlogPostRequest, ArchiveBlogPostCommand, ArchiveBlogPostResponse>(request, context);
}
public override async Task<Empty> IncrementViewCount(IncrementViewCountRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<IncrementViewCountRequest, IncrementViewCountCommand>(request, context);
}
// ── Private Mapping Helpers ──
private static GetBlogPostResponse MapBlogPostDtoToResponse(BlogPostDto dto)
{
var response = new GetBlogPostResponse
{
Id = dto.Id,
Title = dto.Title ?? string.Empty,
Slug = dto.Slug ?? string.Empty,
Summary = dto.Summary,
HtmlContent = dto.HtmlContent ?? string.Empty,
FeaturedImagePath = dto.FeaturedImagePath,
FeaturedImageThumbnailPath = dto.FeaturedImageThumbnailPath,
Status = (int)dto.Status,
StatusName = dto.StatusName ?? string.Empty,
ViewCount = dto.ViewCount,
AuthorUserId = dto.AuthorUserId,
IsFeatured = dto.IsFeatured,
SortOrder = dto.SortOrder,
Created = dto.Created != default ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.Created, DateTimeKind.Utc)) : null,
LastModified = dto.LastModified.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.LastModified.Value, DateTimeKind.Utc)) : null,
PublishedAt = dto.PublishedAt.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.PublishedAt.Value, DateTimeKind.Utc)) : null
};
if (dto.Categories != null)
foreach (var c in dto.Categories)
response.Categories.Add(new BlogPostCategoryInfo { Id = c.Id, Title = c.Title ?? string.Empty, Slug = c.Slug ?? string.Empty });
if (dto.Tags != null)
foreach (var t in dto.Tags)
response.Tags.Add(new BlogPostTagInfo { Id = t.Id, Title = t.Title ?? string.Empty, Name = t.Name ?? string.Empty });
return response;
}
private static GetAllBlogPostsResponse MapAllBlogPostsResponse(GetAllBlogPostsResponseDto dto)
{
var response = new GetAllBlogPostsResponse
{
MetaData = dto.MetaData.Adapt<ProtoMetaData>()
};
foreach (var item in dto.Models)
response.Models.Add(MapToListItem(item));
return response;
}
private static BlogPostListItem MapToListItem(BlogPostListItemDto item)
{
var listItem = new BlogPostListItem
{
Id = item.Id,
Title = item.Title ?? string.Empty,
Slug = item.Slug ?? string.Empty,
Summary = item.Summary,
FeaturedImageThumbnailPath = item.FeaturedImageThumbnailPath,
Status = item.Status,
StatusName = item.StatusName ?? string.Empty,
ViewCount = item.ViewCount,
IsFeatured = item.IsFeatured,
Created = item.Created != default ? Timestamp.FromDateTime(DateTime.SpecifyKind(item.Created, DateTimeKind.Utc)) : null,
PublishedAt = item.PublishedAt.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(item.PublishedAt.Value, DateTimeKind.Utc)) : null
};
if (item.Categories != null)
foreach (var c in item.Categories)
listItem.Categories.Add(new BlogPostCategoryInfo { Id = c.Id, Title = c.Title ?? string.Empty, Slug = c.Slug ?? string.Empty });
return listItem;
}
}
@@ -1,5 +1,6 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DiscountShopCQ.Commands.PlaceOrder;
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
using CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateOrderStatus;
@@ -7,21 +8,57 @@ using CMSMicroservice.Application.DiscountShopCQ.Queries.GetOrderById;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetUserOrders;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetAllDiscountOrders;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountSalesReport;
using Grpc.Core;
using Mapster;
using MediatR;
namespace CMSMicroservice.WebApi.Services;
public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly ICurrentUserService _currentUserService;
public DiscountOrderService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public DiscountOrderService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
ICurrentUserService currentUserService)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_currentUserService = currentUserService;
}
private long GetCurrentUserId()
{
if (long.TryParse(_currentUserService.UserId, out var uid) && uid > 0)
return uid;
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
}
public override async Task<PlaceOrderResponse> PlaceOrder(PlaceOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<PlaceOrderRequest, PlaceOrderCommand, PlaceOrderResponse>(request, context);
var command = new PlaceOrderCommand
{
UserId = GetCurrentUserId(),
UserAddressId = request.UserAddressId,
DiscountBalanceToUse = request.DiscountBalanceToUse
};
var result = await _sender.Send(command);
var response = new PlaceOrderResponse
{
Success = result.Success,
Message = result.Message ?? string.Empty,
OrderId = result.OrderId ?? 0,
GatewayAmount = result.GatewayAmountRequired,
};
if (!string.IsNullOrEmpty(result.PaymentUrl))
response.PaymentUrl = result.PaymentUrl;
return response;
}
public override async Task<CompleteOrderPaymentResponse> CompleteOrderPayment(CompleteOrderPaymentRequest request, ServerCallContext context)
@@ -36,12 +73,23 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
public override async Task<GetOrderByIdResponse> GetOrderById(GetOrderByIdRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetOrderByIdRequest, GetOrderByIdQuery, GetOrderByIdResponse>(request, context);
var query = new GetOrderByIdQuery
{
OrderId = request.OrderId,
UserId = GetCurrentUserId()
};
var result = await _sender.Send(query);
return result.Adapt<GetOrderByIdResponse>();
}
public override async Task<GetUserOrdersResponse> GetUserOrders(GetUserOrdersRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserOrdersRequest, GetUserOrdersQuery, GetUserOrdersResponse>(request, context);
var query = new GetUserOrdersQuery
{
UserId = GetCurrentUserId()
};
var result = await _sender.Send(query);
return result.Adapt<GetUserOrdersResponse>();
}
public override async Task<GetAllDiscountOrdersResponse> GetAllDiscountOrders(GetAllDiscountOrdersRequest request, ServerCallContext context)
@@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.DiscountShopCQ.Commands.CreateDiscountProduct;
@@ -10,26 +12,75 @@ using CMSMicroservice.Application.DiscountShopCQ.Commands.ReorderDiscountProduct
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProductById;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProducts;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProductImages;
using MediatR;
using Mapster;
namespace CMSMicroservice.WebApi.Services;
public class DiscountProductService : DiscountProductContract.DiscountProductContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
public DiscountProductService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public DiscountProductService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
}
public override async Task<CreateDiscountProductResponse> CreateDiscountProduct(CreateDiscountProductRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateDiscountProductRequest, CreateDiscountProductCommand, CreateDiscountProductResponse>(request, context);
var command = new CreateDiscountProductCommand
{
Title = request.Title,
ShortInfomation = request.ShortInfomation,
FullInformation = request.FullInformation,
Price = request.Price,
MaxDiscountPercent = request.MaxDiscountPercent,
ImagePath = request.ImagePath,
ThumbnailPath = request.ThumbnailPath,
SortOrder = request.SortOrder,
IsActive = request.IsActive,
CategoryIds = request.CategoryIds?.ToList() ?? new List<long>(),
// Map binary image data
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName,
ThumbnailFileBytes = request.ThumbnailFile?.File?.ToByteArray(),
ThumbnailFileMime = request.ThumbnailFile?.Mime,
ThumbnailFileName = request.ThumbnailFile?.FileName
};
var productId = await _sender.Send(command, context.CancellationToken);
return new CreateDiscountProductResponse { ProductId = productId };
}
public override async Task<Empty> UpdateDiscountProduct(UpdateDiscountProductRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateDiscountProductRequest, UpdateDiscountProductCommand>(request, context);
var command = new UpdateDiscountProductCommand
{
ProductId = request.ProductId,
Title = request.Title,
ShortInfomation = request.ShortInfomation,
FullInformation = request.FullInformation,
Price = request.Price,
MaxDiscountPercent = request.MaxDiscountPercent,
ImagePath = request.ImagePath,
ThumbnailPath = request.ThumbnailPath,
SortOrder = request.SortOrder,
IsActive = request.IsActive,
CategoryIds = request.CategoryIds?.ToList() ?? new List<long>(),
// Map binary image data
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName,
ThumbnailFileBytes = request.ThumbnailFile?.File?.ToByteArray(),
ThumbnailFileMime = request.ThumbnailFile?.Mime,
ThumbnailFileName = request.ThumbnailFile?.FileName
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
public override async Task<Empty> DeleteDiscountProduct(DeleteDiscountProductRequest request, ServerCallContext context)
@@ -70,6 +121,11 @@ public class DiscountProductService : DiscountProductContract.DiscountProductCon
public override async Task<GetDiscountProductImagesResponse> GetDiscountProductImages(GetDiscountProductImagesRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetDiscountProductImagesRequest, GetDiscountProductImagesQuery, GetDiscountProductImagesResponse>(request, context);
var query = request.Adapt<GetDiscountProductImagesQuery>();
var images = await _sender.Send(query, context.CancellationToken);
var response = new GetDiscountProductImagesResponse();
response.Images.AddRange(images.Select(i => i.Adapt<CMSMicroservice.Protobuf.Protos.DiscountProduct.DiscountProductImageDto>()));
return response;
}
}
@@ -1,44 +1,111 @@
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DiscountShopCQ.Commands.AddToCart;
using CMSMicroservice.Application.DiscountShopCQ.Commands.RemoveFromCart;
using CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateCartItemCount;
using CMSMicroservice.Application.DiscountShopCQ.Commands.ClearCart;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetUserCart;
using Grpc.Core;
using Mapster;
using MediatR;
namespace CMSMicroservice.WebApi.Services;
public class DiscountShoppingCartService : DiscountShoppingCartContract.DiscountShoppingCartContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly ICurrentUserService _currentUserService;
public DiscountShoppingCartService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public DiscountShoppingCartService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
ICurrentUserService currentUserService)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_currentUserService = currentUserService;
}
private long GetCurrentUserId()
{
if (long.TryParse(_currentUserService.UserId, out var uid) && uid > 0)
return uid;
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
}
public override async Task<AddToCartResponse> AddToCart(AddToCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<AddToCartRequest, AddToCartCommand, AddToCartResponse>(request, context);
var command = new AddToCartCommand
{
UserId = GetCurrentUserId(),
ProductId = request.ProductId,
Count = request.Count
};
var result = await _sender.Send(command, context.CancellationToken);
return result.Adapt<AddToCartResponse>();
}
public override async Task<RemoveFromCartResponse> RemoveFromCart(RemoveFromCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<RemoveFromCartRequest, RemoveFromCartCommand, RemoveFromCartResponse>(request, context);
var command = new RemoveFromCartCommand
{
UserId = GetCurrentUserId(),
ProductId = request.ProductId
};
var result = await _sender.Send(command, context.CancellationToken);
return result.Adapt<RemoveFromCartResponse>();
}
public override async Task<UpdateCartItemCountResponse> UpdateCartItemCount(UpdateCartItemCountRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateCartItemCountRequest, UpdateCartItemCountCommand, UpdateCartItemCountResponse>(request, context);
var command = new UpdateCartItemCountCommand
{
UserId = GetCurrentUserId(),
ProductId = request.ProductId,
NewCount = request.NewCount
};
var result = await _sender.Send(command, context.CancellationToken);
return result.Adapt<UpdateCartItemCountResponse>();
}
public override async Task<GetUserCartResponse> GetUserCart(GetUserCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserCartRequest, GetUserCartQuery, GetUserCartResponse>(request, context);
var query = new GetUserCartQuery { UserId = GetCurrentUserId() };
var cart = await _sender.Send(query, context.CancellationToken);
var response = new GetUserCartResponse
{
TotalPrice = cart.TotalAmount,
TotalDiscountAmount = cart.MaxDiscountAmount,
FinalPrice = cart.MinPayableAmount
};
foreach (var item in cart.Items)
{
response.Items.Add(new CMSMicroservice.Protobuf.Protos.DiscountShoppingCart.CartItemDto
{
ProductId = item.ProductId,
ProductTitle = item.ProductTitle ?? string.Empty,
ProductImagePath = item.ProductImagePath ?? string.Empty,
UnitPrice = item.UnitPrice,
MaxDiscountPercent = item.MaxDiscountPercent,
Count = item.Count,
TotalPrice = item.SubTotal,
DiscountAmount = item.MaxDiscountAmount,
FinalPrice = item.MinPayable,
ProductRemainingCount = item.RemainingStock
});
}
return response;
}
public override async Task<Empty> ClearCart(ClearCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ClearCartRequest, ClearCartCommand>(request, context);
var command = new ClearCartCommand { UserId = GetCurrentUserId() };
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
}
@@ -0,0 +1,42 @@
using CMSMicroservice.Application.Common.FileManager;
using CMSMicroservice.Protobuf.Protos.ImageResolver;
using Grpc.Core;
namespace CMSMicroservice.WebApi.Services;
/// <summary>
/// سرویس اختصاصی resolve تصاویر — مسیر نسبی را به base64 data-URI تبدیل می‌کند.
/// FrontOffice از طریق این سرویس تمام تصاویر دینامیک را دریافت می‌کند.
/// </summary>
public class ImageResolverService : ImageResolverContract.ImageResolverContractBase
{
private readonly IFileManager _fileManager;
public ImageResolverService(IFileManager fileManager)
{
_fileManager = fileManager;
}
public override Task<ResolveImagesResponse> ResolveImages(ResolveImagesRequest request, ServerCallContext context)
{
var response = new ResolveImagesResponse();
foreach (var path in request.Paths)
{
var dataUri = string.Empty;
if (!string.IsNullOrWhiteSpace(path))
{
dataUri = _fileManager.ResolveImageUrl(path);
}
response.Images.Add(new ResolvedImage
{
OriginalPath = path ?? string.Empty,
DataUri = dataUri ?? string.Empty
});
}
return Task.FromResult(response);
}
}
@@ -178,6 +178,7 @@ public class ProductsService : ProductsContract.ProductsContractBase
CategoryIds = request.Filter?.CategoryId.HasValue == true
? new List<long> { request.Filter.CategoryId.Value }
: new List<long>(),
IsActive = request.Filter?.IsActive,
SortBy = request.SortBy ?? string.Empty,
PaginationState = request.PaginationState != null
? new AppModels.PaginationState
@@ -216,7 +217,8 @@ public class ProductsService : ProductsContract.ProductsContractBase
SaleCount = m.SaleCount,
ViewCount = m.ViewCount,
RemainingCount = m.RemainingCount,
CategoryIds = { m.Categories?.Select(c => c.CategoryId) ?? Enumerable.Empty<long>() }
CategoryIds = { m.Categories?.Select(c => c.CategoryId) ?? Enumerable.Empty<long>() },
IsActive = m.IsActive
}) }
};
}
@@ -0,0 +1,218 @@
using System.Linq;
using CMSMicroservice.Protobuf.Protos.SitePage;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.SitePageCQ.Commands.CreateSitePage;
using CMSMicroservice.Application.SitePageCQ.Commands.DeleteSitePage;
using CMSMicroservice.Application.SitePageCQ.Commands.UpdateSitePage;
using CMSMicroservice.Application.SitePageCQ.Commands.CreateSitePageSection;
using CMSMicroservice.Application.SitePageCQ.Commands.UpdateSitePageSection;
using CMSMicroservice.Application.SitePageCQ.Commands.DeleteSitePageSection;
using CMSMicroservice.Application.SitePageCQ.Commands.ReorderSitePageSections;
using CMSMicroservice.Application.SitePageCQ.Queries.GetSitePage;
using CMSMicroservice.Application.SitePageCQ.Queries.GetSitePageByKey;
using CMSMicroservice.Application.SitePageCQ.Queries.GetAllSitePages;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using MediatR;
namespace CMSMicroservice.WebApi.Services;
public class SitePageService : SitePageContract.SitePageContractBase
{
private readonly ISender _sender;
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public SitePageService(ISender sender, IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_sender = sender;
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<GetSitePageResponse> GetSitePage(GetSitePageRequest request, ServerCallContext context)
{
var query = new GetSitePageQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
return MapToResponse(result);
}
public override async Task<GetSitePageResponse> GetSitePageByKey(GetSitePageByKeyRequest request, ServerCallContext context)
{
var query = new GetSitePageByKeyQuery { PageKey = request.PageKey };
var result = await _sender.Send(query, context.CancellationToken);
return MapToResponse(result);
}
public override async Task<Empty> UpdateSitePage(UpdateSitePageRequest request, ServerCallContext context)
{
var command = new UpdateSitePageCommand
{
Id = request.Id,
Title = request.Title,
MetaDescription = request.MetaDescription,
HeroTitle = request.HeroTitle,
HeroSubtitle = request.HeroSubtitle,
HeroImagePath = request.HeroImagePath,
IsActive = request.IsActive,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
public override async Task<CreateSitePageResponse> CreateSitePage(CreateSitePageRequest request, ServerCallContext context)
{
var command = new CreateSitePageCommand
{
PageKey = request.PageKey,
Title = request.Title,
MetaDescription = request.MetaDescription,
HeroTitle = request.HeroTitle,
HeroSubtitle = request.HeroSubtitle,
IsActive = request.IsActive,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
var id = await _sender.Send(command, context.CancellationToken);
return new CreateSitePageResponse { Id = id };
}
public override async Task<Empty> DeleteSitePage(DeleteSitePageRequest request, ServerCallContext context)
{
var command = new DeleteSitePageCommand { Id = request.Id };
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
public override async Task<GetAllSitePagesResponse> GetAllSitePages(GetAllSitePagesRequest request, ServerCallContext context)
{
var query = new GetAllSitePagesQuery();
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllSitePagesResponse();
foreach (var item in result)
{
response.Pages.Add(new SitePageSummary
{
Id = item.Id,
PageKey = item.PageKey ?? string.Empty,
Title = item.Title ?? string.Empty,
IsActive = item.IsActive,
SectionCount = item.SectionCount,
LastModified = item.LastModified.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(item.LastModified.Value, DateTimeKind.Utc))
: null
});
}
return response;
}
public override async Task<CreateSitePageSectionResponse> CreateSitePageSection(CreateSitePageSectionRequest request, ServerCallContext context)
{
var command = new CreateSitePageSectionCommand
{
SitePageId = request.SitePageId,
SectionKey = request.SectionKey,
Title = request.Title,
Subtitle = request.Subtitle,
HtmlContent = request.HtmlContent,
IconName = request.IconName,
ImagePath = request.ImagePath,
IsActive = true,
ExtraData = request.ExtraData,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
var result = await _sender.Send(command, context.CancellationToken);
return new CreateSitePageSectionResponse { Id = result };
}
public override async Task<Empty> UpdateSitePageSection(UpdateSitePageSectionRequest request, ServerCallContext context)
{
var command = new UpdateSitePageSectionCommand
{
Id = request.Id,
SectionKey = request.SectionKey,
Title = request.Title,
Subtitle = request.Subtitle,
HtmlContent = request.HtmlContent,
IconName = request.IconName,
ImagePath = request.ImagePath,
SortOrder = request.SortOrder,
IsActive = request.IsActive,
ExtraData = request.ExtraData,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
public override async Task<Empty> DeleteSitePageSection(DeleteSitePageSectionRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteSitePageSectionRequest, DeleteSitePageSectionCommand>(request, context);
}
public override async Task<Empty> ReorderSitePageSections(ReorderSitePageSectionsRequest request, ServerCallContext context)
{
var command = new ReorderSitePageSectionsCommand
{
Items = request.Items.Select(x => new Application.SitePageCQ.Commands.ReorderSitePageSections.SectionSortItem
{
Id = x.Id,
SortOrder = x.SortOrder
}).ToList()
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
// ── Private Mapping Helpers ──
private static GetSitePageResponse MapToResponse(SitePageDto dto)
{
var response = new GetSitePageResponse
{
Id = dto.Id,
PageKey = dto.PageKey ?? string.Empty,
Title = dto.Title ?? string.Empty,
MetaDescription = dto.MetaDescription,
HeroTitle = dto.HeroTitle,
HeroSubtitle = dto.HeroSubtitle,
HeroImagePath = dto.HeroImagePath,
IsActive = dto.IsActive
};
if (dto.Sections != null)
{
foreach (var s in dto.Sections)
{
response.Sections.Add(new SitePageSectionItem
{
Id = s.Id,
SectionKey = s.SectionKey ?? string.Empty,
Title = s.Title ?? string.Empty,
Subtitle = s.Subtitle,
HtmlContent = s.HtmlContent,
IconName = s.IconName,
ImagePath = s.ImagePath,
SortOrder = s.SortOrder,
IsActive = s.IsActive,
ExtraData = s.ExtraData
});
}
}
return response;
}
}
@@ -167,14 +167,14 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
{
UserId = request.Filter?.UserId ?? 0, // 0 means all users (admin view)
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
PaymentStatusFilter = request.Filter?.PaymentStatus != null
PaymentStatusFilter = request.Filter?.HasPaymentStatus == true
? (int?)request.Filter.PaymentStatus
: null,
DeliveryStatusFilter = request.Filter?.DeliveryStatus != null
DeliveryStatusFilter = request.Filter?.HasDeliveryStatus == true
? (int?)request.Filter.DeliveryStatus
: null,
FromDate = request.Filter?.PaymentDate?.ToDateTime(),
ToDate = null
FromDate = request.Filter?.FromDate?.ToDateTime() ?? request.Filter?.PaymentDate?.ToDateTime(),
ToDate = request.Filter?.ToDate?.ToDateTime()
};
var result = await _sender.Send(query, context.CancellationToken);
@@ -261,7 +261,14 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
if (defaultAddress == null)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, "آدرس پیش‌فرض یافت نشد"));
// Check if user has any address at all
var hasAnyAddress = await _context.UserAddresses
.AnyAsync(a => a.UserId == userId && !a.IsDeleted, context.CancellationToken);
throw new RpcException(new Status(StatusCode.FailedPrecondition,
hasAnyAddress
? "لطفاً یک آدرس را به عنوان پیش‌فرض انتخاب کنید."
: "آدرسی ثبت نشده است. لطفاً ابتدا یک آدرس اضافه کنید."));
}
// Calculate amounts
@@ -591,14 +598,14 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
{
UserId = customerUserId,
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
PaymentStatusFilter = request.Filter?.PaymentStatus != null
PaymentStatusFilter = request.Filter?.HasPaymentStatus == true
? (int?)request.Filter.PaymentStatus
: null,
DeliveryStatusFilter = request.Filter?.DeliveryStatus != null
DeliveryStatusFilter = request.Filter?.HasDeliveryStatus == true
? (int?)request.Filter.DeliveryStatus
: null,
FromDate = request.Filter?.PaymentDate?.ToDateTime(),
ToDate = null
FromDate = request.Filter?.FromDate?.ToDateTime() ?? request.Filter?.PaymentDate?.ToDateTime(),
ToDate = request.Filter?.ToDate?.ToDateTime()
};
var result = await _sender.Send(query, context.CancellationToken);
@@ -34,7 +34,7 @@ public class UserService : UserContract.UserContractBase
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly IHashService _hashService;
private readonly IFileManagementService _fileManagementService;
private readonly CMSMicroservice.Application.Common.FileManager.IFileManager _fileManager;
public UserService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
@@ -42,14 +42,14 @@ public class UserService : UserContract.UserContractBase
IApplicationDbContext context,
ICurrentUserService currentUserService,
IHashService hashService,
IFileManagementService fileManagementService)
CMSMicroservice.Application.Common.FileManager.IFileManager fileManager)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_hashService = hashService;
_fileManagementService = fileManagementService;
_fileManager = fileManager;
}
public override async Task<CreateNewUserResponse> CreateNewUser(CreateNewUserRequest request, ServerCallContext context)
{
@@ -65,6 +65,10 @@ public class UserService : UserContract.UserContractBase
}
public override async Task<GetUserResponse> GetUser(GetUserRequest request, ServerCallContext context)
{
// اگر Id ارسال نشده، از JWT بخون (برای کلاینت مشتری)
if (request.Id == 0)
request.Id = GetCurrentUserId();
return await _dispatchRequestToCQRS.Handle<GetUserRequest, GetUserQuery, GetUserResponse>(request, context);
}
public override async Task<GetAllUserByFilterResponse> GetAllUserByFilter(GetAllUserByFilterRequest request, ServerCallContext context)
@@ -335,15 +339,28 @@ public class UserService : UserContract.UserContractBase
if (user == null)
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
// Upload to FMS
// Upload to local file manager
var fileBytes = request.FileData.ToByteArray();
var fileName = $"avatar_{userId}_{DateTime.UtcNow.Ticks}";
var mime = request.FileMimeType ?? "image/jpeg";
var avatarUrl = await _fileManagementService.UploadFileAsync(
"Avatars", fileBytes, mime, fileName, context.CancellationToken);
if (string.IsNullOrEmpty(avatarUrl))
try
{
var result = await _fileManager.UploadImageAsync(
"Avatars", fileBytes, mime, fileName, context.CancellationToken);
// Update user avatar path in DB
user.AvatarPath = result.Main.Path;
await _context.SaveChangesAsync(context.CancellationToken);
return new UploadCustomerAvatarResponse
{
Success = true,
Message = "تصویر پروفایل با موفقیت آپلود شد",
AvatarUrl = result.Main.Path
};
}
catch (Exception ex)
{
return new UploadCustomerAvatarResponse
{
@@ -351,17 +368,6 @@ public class UserService : UserContract.UserContractBase
Message = "خطا در آپلود فایل. لطفاً مجدد تلاش کنید"
};
}
// Update user avatar path in DB
user.AvatarPath = avatarUrl;
await _context.SaveChangesAsync(context.CancellationToken);
return new UploadCustomerAvatarResponse
{
Success = true,
Message = "تصویر پروفایل با موفقیت آپلود شد",
AvatarUrl = avatarUrl
};
}
public override async Task<GetCustomerSettingsResponse> GetCustomerSettings(GetCustomerSettingsRequest request, ServerCallContext context)
@@ -1,5 +1,14 @@
{
"UseRealPaymentGateway": false,
"PaymentProvider": "pyms",
"PYMS": {
"Address": "https://pyms.se.kbs1.ir"
},
"ZarinPal": {
"MerchantId": "00000000-0000-0000-0000-000000000000",
"UseSandbox": true
},
"CmsBaseUrl": "https://localhost:32846",
"FrontOfficeBaseUrl": "https://localhost:5268",
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
+8 -1
View File
@@ -1,5 +1,12 @@
{
"UseRealPaymentGateway": false,
"PaymentProvider": "pyms",
"PYMS": {
"Address": "http://pyms-svc.default.svc.cluster.local:80"
},
"ZarinPal": {
"MerchantId": "6b098fc8-f490-47a1-aac3-1de1a1b84404",
"UseSandbox": true
},
"FMS": {
"Address": "https://dl.afrino.co"
},