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
@@ -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)