Files
CMS/src/CMSMicroservice.WebApi/Services/ProductsService.cs
T
masoodafar-web 2502cbbda2
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m44s
feat: integrate PYMS payment gateway, add blog/sitepage/image services, local file manager
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)
2026-02-15 23:01:16 +03:30

692 lines
27 KiB
C#

using CMSMicroservice.Protobuf.Protos.Products;
using Grpc.Core;
using MediatR;
using CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
using CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
using CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
using CMSMicroservice.Application.ProductsCQ.Commands.AddProductImage;
using CMSMicroservice.Application.ProductsCQ.Commands.RemoveProductImage;
using CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProducts;
using CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProductsByFilter;
using CMSMicroservice.Application.ProductsCQ.Queries.GetProductGallery;
using CMSMicroservice.Application.Common.Interfaces;
using Mapster;
using Microsoft.EntityFrameworkCore;
using AppModels = CMSMicroservice.Application.Common.Models;
using System.Collections.Generic;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
public class ProductsService : ProductsContract.ProductsContractBase
{
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
public ProductsService(ISender sender, IApplicationDbContext context)
{
_sender = sender;
_context = context;
}
public override async Task<CreateNewProductsResponse> CreateNewProducts(CreateNewProductsRequest request, ServerCallContext context)
{
var command = new CreateNewProductsCommand
{
Title = request.Title,
Description = request.Description,
ShortInfomation = request.ShortInfomation,
FullInformation = request.FullInformation,
Price = request.Price,
Discount = request.Discount,
Rate = request.Rate,
ImagePath = request.ImagePath,
ThumbnailPath = request.ThumbnailPath,
SaleCount = request.SaleCount,
ViewCount = request.ViewCount,
RemainingCount = request.RemainingCount,
CategoryIds = request.CategoryIds?.ToList() ?? new List<long>(),
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 result = await _sender.Send(command, context.CancellationToken);
return new CreateNewProductsResponse { Id = result.Id };
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateProducts(UpdateProductsRequest request, ServerCallContext context)
{
var command = new UpdateProductsCommand
{
Id = request.Id,
Title = request.Title,
Description = request.Description,
ShortInfomation = request.ShortInfomation,
FullInformation = request.FullInformation,
Price = request.Price,
Discount = request.Discount,
Rate = request.Rate,
ImagePath = request.ImagePath,
ThumbnailPath = request.ThumbnailPath,
SaleCount = request.SaleCount,
ViewCount = request.ViewCount,
RemainingCount = request.RemainingCount,
CategoryIds = request.CategoryIds?.ToList() ?? new List<long>(),
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 Google.Protobuf.WellKnownTypes.Empty();
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> DeleteProducts(DeleteProductsRequest request, ServerCallContext context)
{
var command = new DeleteProductsCommand { Id = request.Id };
await _sender.Send(command, context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
public override async Task<GetProductsResponse> GetProducts(GetProductsRequest request, ServerCallContext context)
{
var query = new GetCustomerProductsQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetProductsResponse
{
Id = result.Id,
Title = result.Title,
Description = result.Description,
ShortInfomation = result.ShortInfomation,
FullInformation = result.FullInformation,
Price = result.Price,
Discount = result.Discount,
Rate = result.Rate,
ImagePath = result.ImagePath,
ThumbnailPath = result.ThumbnailPath,
SaleCount = result.SaleCount,
ViewCount = result.ViewCount,
RemainingCount = result.RemainingCount
};
if (result.Gallery != null)
{
foreach (var item in result.Gallery)
{
response.Gallery.Add(new ProductGalleryItem
{
ProductGalleryId = item.ProductGalleryId,
ProductImageId = item.ProductImageId,
Title = item.Title,
ImagePath = item.ImagePath,
ImageThumbnailPath = item.ImageThumbnailPath
});
}
}
if (result.Categories != null)
{
foreach (var cat in result.Categories)
{
var categoryPath = new ProductCategoryPath
{
CategoryId = cat.CategoryId,
Title = cat.Title
};
if (cat.Path != null)
{
foreach (var node in cat.Path)
{
categoryPath.Path.Add(new CategoryNode
{
Id = node.Id,
Title = node.Title,
ParentId = node.ParentId
});
}
}
response.Categories.Add(categoryPath);
}
}
return response;
}
public override async Task<GetAllProductsByFilterResponse> GetAllProductsByFilter(GetAllProductsByFilterRequest request, ServerCallContext context)
{
// Map to GetCustomerProductsByFilter query (public products API)
var query = new GetCustomerProductsByFilterQuery
{
Id = request.Filter?.Id,
Title = request.Filter?.Title ?? string.Empty,
Description = request.Filter?.Description ?? string.Empty,
ShortInfomation = request.Filter?.ShortInfomation ?? string.Empty,
FullInformation = request.Filter?.FullInformation ?? string.Empty,
Price = request.Filter?.Price,
Discount = request.Filter?.Discount,
Rate = request.Filter?.Rate,
SaleCount = request.Filter?.SaleCount,
ViewCount = request.Filter?.ViewCount,
RemainingCount = request.Filter?.RemainingCount,
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
{
PageNumber = request.PaginationState.PageNumber,
PageSize = request.PaginationState.PageSize
}
: new AppModels.PaginationState { PageNumber = 1, PageSize = 20 }
};
var result = await _sender.Send(query, context.CancellationToken);
return new GetAllProductsByFilterResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = result.MetaData.CurrentPage,
TotalPage = result.MetaData.TotalPage,
PageSize = result.MetaData.PageSize,
TotalCount = result.MetaData.TotalCount,
HasPrevious = result.MetaData.HasPrevious,
HasNext = result.MetaData.HasNext
},
Models = { result.Models.Select(m => new GetAllProductsByFilterResponseModel
{
Id = m.Id,
Title = m.Title,
Description = m.Description,
ShortInfomation = m.ShortInfomation,
FullInformation = m.FullInformation,
Price = m.Price,
Discount = m.Discount,
Rate = m.Rate,
ImagePath = m.ImagePath,
ThumbnailPath = m.ThumbnailPath,
SaleCount = m.SaleCount,
ViewCount = m.ViewCount,
RemainingCount = m.RemainingCount,
CategoryIds = { m.Categories?.Select(c => c.CategoryId) ?? Enumerable.Empty<long>() },
IsActive = m.IsActive
}) }
};
}
public override async Task<BulkUpdateProductPricesResponse> BulkUpdateProductPrices(BulkUpdateProductPricesRequest request, ServerCallContext context)
{
var response = new BulkUpdateProductPricesResponse { Total = request.Products.Count };
foreach (var item in request.Products)
{
try
{
var product = await _context.Products.FindAsync(new object[] { item.ProductId }, context.CancellationToken);
if (product == null)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = "محصول یافت نشد" });
continue;
}
product.Price = item.NewPrice;
if (item.NewDiscount != null) product.Discount = item.NewDiscount.Value;
if (item.NewClubDiscountPercent != null) product.ClubDiscountPercent = item.NewClubDiscountPercent.Value;
response.Succeeded++;
}
catch (Exception ex)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = ex.Message });
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return response;
}
public override async Task<BulkUpdateProductStockResponse> BulkUpdateProductStock(BulkUpdateProductStockRequest request, ServerCallContext context)
{
var response = new BulkUpdateProductStockResponse { Total = request.Products.Count };
foreach (var item in request.Products)
{
try
{
var product = await _context.Products.FindAsync(new object[] { item.ProductId }, context.CancellationToken);
if (product == null)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = "محصول یافت نشد" });
continue;
}
product.RemainingCount = request.UpdateType switch
{
StockUpdateType.Set => item.Quantity,
StockUpdateType.Add => product.RemainingCount + item.Quantity,
StockUpdateType.Subtract => Math.Max(0, product.RemainingCount - item.Quantity),
_ => item.Quantity
};
response.Succeeded++;
}
catch (Exception ex)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = ex.Message });
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return response;
}
public override async Task<GetLowStockProductsResponse> GetLowStockProducts(GetLowStockProductsRequest request, ServerCallContext context)
{
var threshold = request.Threshold > 0 ? request.Threshold : 10;
var pageIndex = request.PageIndex > 0 ? request.PageIndex : 1;
var pageSize = request.PageSize > 0 ? request.PageSize : 20;
var query = _context.Products
.Where(p => !p.IsDeleted && p.RemainingCount <= threshold);
if (request.IsClubExclusive != null)
query = query.Where(p => p.IsClubExclusive == request.IsClubExclusive.Value);
var totalCount = await query.CountAsync(context.CancellationToken);
var products = await query
.OrderBy(p => p.RemainingCount)
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.Select(p => new LowStockProduct
{
Id = p.Id,
Title = p.Title,
RemainingCount = p.RemainingCount,
Price = p.Price,
IsClubExclusive = p.IsClubExclusive
})
.ToListAsync(context.CancellationToken);
return new GetLowStockProductsResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = pageIndex,
PageSize = pageSize,
TotalCount = totalCount,
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
HasPrevious = pageIndex > 1,
HasNext = pageIndex * pageSize < totalCount
},
Products = { products }
};
}
public override async Task<ToggleProductStatusResponse> ToggleProductStatus(ToggleProductStatusRequest request, ServerCallContext context)
{
var response = new ToggleProductStatusResponse { Total = request.ProductIds.Count };
foreach (var productId in request.ProductIds)
{
try
{
var product = await _context.Products.FindAsync(new object[] { productId }, context.CancellationToken);
if (product == null)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = productId, ErrorMessage = "محصول یافت نشد" });
continue;
}
product.IsDeleted = !request.Enable;
if (request.Enable && request.DefaultStock > 0 && product.RemainingCount == 0)
product.RemainingCount = request.DefaultStock;
response.Succeeded++;
}
catch (Exception ex)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = productId, ErrorMessage = ex.Message });
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return response;
}
// ============= Category-Product DragDrop Methods =============
public override async Task<GetProductsForCategoryResponse> GetProductsForCategory(GetProductsForCategoryRequest request, ServerCallContext context)
{
var assignedProductIds = await _context.ProductCategories
.Where(pc => pc.CategoryId == request.CategoryId && !pc.IsDeleted)
.Select(pc => pc.ProductId)
.ToListAsync(context.CancellationToken);
var allProducts = await _context.Products
.Where(p => !p.IsDeleted)
.OrderBy(p => p.Title)
.Select(p => new CategoryProductItem
{
Id = p.Id,
Title = p.Title,
Selected = assignedProductIds.Contains(p.Id)
})
.ToListAsync(context.CancellationToken);
return new GetProductsForCategoryResponse { Items = { allProducts } };
}
public override async Task<GetCategoriesResponse> GetCategories(GetCategoriesRequest request, ServerCallContext context)
{
var assignedCategoryIds = await _context.ProductCategories
.Where(pc => pc.ProductId == request.ProductId && !pc.IsDeleted)
.Select(pc => pc.CategoryId)
.ToListAsync(context.CancellationToken);
var allCategories = await _context.Categories
.Where(c => !c.IsDeleted && c.IsActive)
.OrderBy(c => c.SortOrder)
.Select(c => new CategoryItem
{
Id = c.Id,
Title = c.Title,
Selected = assignedCategoryIds.Contains(c.Id)
})
.ToListAsync(context.CancellationToken);
return new GetCategoriesResponse { Items = { allCategories } };
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateProductCategories(UpdateProductCategoriesRequest request, ServerCallContext context)
{
var existingLinks = await _context.ProductCategories
.Where(pc => pc.ProductId == request.ProductId)
.ToListAsync(context.CancellationToken);
// حذف لینک‌های قبلی
foreach (var link in existingLinks)
link.IsDeleted = true;
// ایجاد لینک‌های جدید
foreach (var categoryId in request.CategoryIds)
{
var existing = existingLinks.FirstOrDefault(l => l.CategoryId == categoryId);
if (existing != null)
{
existing.IsDeleted = false;
}
else
{
_context.ProductCategories.Add(new Domain.Entities.ProductCategory
{
ProductId = request.ProductId,
CategoryId = categoryId
});
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateCategoryProducts(UpdateCategoryProductsRequest request, ServerCallContext context)
{
var existingLinks = await _context.ProductCategories
.Where(pc => pc.CategoryId == request.CategoryId)
.ToListAsync(context.CancellationToken);
// حذف لینک‌های قبلی
foreach (var link in existingLinks)
link.IsDeleted = true;
// ایجاد لینک‌های جدید
foreach (var productId in request.ProductIds)
{
var existing = existingLinks.FirstOrDefault(l => l.ProductId == productId);
if (existing != null)
{
existing.IsDeleted = false;
}
else
{
_context.ProductCategories.Add(new Domain.Entities.ProductCategory
{
ProductId = productId,
CategoryId = request.CategoryId
});
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
// ============= Customer-specific Methods =============
public override async Task<GetProductsResponse> GetCustomerProducts(GetProductsRequest request, ServerCallContext context)
{
var query = new GetCustomerProductsQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetProductsResponse
{
Id = result.Id,
Title = result.Title,
Description = result.Description,
ShortInfomation = result.ShortInfomation,
FullInformation = result.FullInformation,
Price = result.Price,
Discount = result.Discount,
Rate = result.Rate,
ImagePath = result.ImagePath,
ThumbnailPath = result.ThumbnailPath,
SaleCount = result.SaleCount,
ViewCount = result.ViewCount,
RemainingCount = result.RemainingCount
};
// Add gallery items
if (result.Gallery != null)
{
foreach (var item in result.Gallery)
{
response.Gallery.Add(new ProductGalleryItem
{
ProductGalleryId = item.ProductGalleryId,
ProductImageId = item.ProductImageId,
Title = item.Title,
ImagePath = item.ImagePath,
ImageThumbnailPath = item.ImageThumbnailPath
});
}
}
// Add categories
if (result.Categories != null)
{
foreach (var cat in result.Categories)
{
var categoryPath = new ProductCategoryPath
{
CategoryId = cat.CategoryId,
Title = cat.Title
};
if (cat.Path != null)
{
foreach (var node in cat.Path)
{
categoryPath.Path.Add(new CategoryNode
{
Id = node.Id,
Title = node.Title,
ParentId = node.ParentId
});
}
}
response.Categories.Add(categoryPath);
}
}
return response;
}
public override async Task<GetCustomerProductsByFilterResponse> GetCustomerProductsByFilter(GetAllProductsByFilterRequest request, ServerCallContext context)
{
var query = new GetCustomerProductsByFilterQuery
{
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
SortBy = request.SortBy,
Id = request.Filter?.Id,
Title = request.Filter?.Title,
Description = request.Filter?.Description,
ShortInfomation = request.Filter?.ShortInfomation,
FullInformation = request.Filter?.FullInformation,
Price = request.Filter?.Price,
Discount = request.Filter?.Discount,
Rate = request.Filter?.Rate,
ImagePath = request.Filter?.ImagePath,
ThumbnailPath = request.Filter?.ThumbnailPath,
SaleCount = request.Filter?.SaleCount,
ViewCount = request.Filter?.ViewCount,
RemainingCount = request.Filter?.RemainingCount,
CategoryIds = request.Filter?.CategoryId != null ? new List<long> { request.Filter.CategoryId.Value } : null
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerProductsByFilterResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = result.MetaData.CurrentPage,
TotalPage = result.MetaData.TotalPage,
PageSize = result.MetaData.PageSize,
TotalCount = result.MetaData.TotalCount,
HasPrevious = result.MetaData.HasPrevious,
HasNext = result.MetaData.HasNext
}
};
foreach (var model in result.Models)
{
var productModel = new GetCustomerProductsByFilterResponseModel
{
Id = model.Id,
Title = model.Title,
Description = model.Description,
ShortInfomation = model.ShortInfomation,
FullInformation = model.FullInformation,
Price = model.Price,
Discount = model.Discount,
Rate = model.Rate,
ImagePath = model.ImagePath,
ThumbnailPath = model.ThumbnailPath,
SaleCount = model.SaleCount,
ViewCount = model.ViewCount,
RemainingCount = model.RemainingCount
};
if (model.Categories != null)
{
foreach (var cat in model.Categories)
{
var categoryPath = new ProductCategoryPath
{
CategoryId = cat.CategoryId,
Title = cat.Title
};
if (cat.Path != null)
{
foreach (var node in cat.Path)
{
categoryPath.Path.Add(new CategoryNode
{
Id = node.Id,
Title = node.Title,
ParentId = node.ParentId
});
}
}
productModel.Categories.Add(categoryPath);
}
}
response.Models.Add(productModel);
}
return response;
}
// ============= Product Image Management =============
public override async Task<AddProductImageResponse> AddProductImage(AddProductImageRequest request, ServerCallContext context)
{
var command = new AddProductImageCommand
{
ProductId = request.ProductId,
Title = request.Title,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
var result = await _sender.Send(command, context.CancellationToken);
return new AddProductImageResponse
{
ProductGalleryId = result.ProductGalleryId,
ProductImageId = result.ProductImageId,
Title = result.Title,
ImagePath = result.ImagePath,
ImageThumbnailPath = result.ImageThumbnailPath
};
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> RemoveProductImage(RemoveProductImageRequest request, ServerCallContext context)
{
var command = new RemoveProductImageCommand { ProductGalleryId = request.ProductGalleryId };
await _sender.Send(command, context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
public override async Task<GetProductGalleryResponse> GetProductGallery(GetProductGalleryRequest request, ServerCallContext context)
{
var query = new GetProductGalleryQuery { ProductId = request.ProductId };
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetProductGalleryResponse();
foreach (var item in result.Items)
{
response.Items.Add(new ProductGalleryItem
{
ProductGalleryId = item.ProductGalleryId,
ProductImageId = item.ProductImageId,
Title = item.Title,
ImagePath = item.ImagePath,
ImageThumbnailPath = item.ImageThumbnailPath
});
}
return response;
}
}