feat: Implement file management and authorization features
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m9s
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m9s
- Add RequiresPermissionAttribute for gRPC method access control. - Create IFileManagementService interface for file upload and management. - Implement AddProductImageCommand and handler for adding product images. - Implement CreateNewProductsCommand and handler for creating new products with image uploads. - Implement DeleteProductsCommand and handler for deleting products and their associations. - Implement RemoveProductImageCommand and handler for removing product images from galleries. - Implement UpdateProductsCommand and handler for updating product details and images. - Create GetProductGalleryQuery and handler for retrieving product galleries. - Implement PermissionService for role-based access control using JWT claims. - Implement FileManagementService for handling file uploads and image optimization. - Define gRPC service and messages for file management in fms.proto. - Add FluentValidation for request validation in various commands. - Create PermissionInterceptor for enforcing permissions on gRPC methods.
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
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;
|
||||
@@ -13,29 +21,142 @@ namespace CMSMicroservice.WebApi.Services;
|
||||
public class ProductsService : ProductsContract.ProductsContractBase
|
||||
{
|
||||
private readonly ISender _sender;
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public ProductsService(ISender sender)
|
||||
public ProductsService(ISender sender, IApplicationDbContext context)
|
||||
{
|
||||
_sender = sender;
|
||||
_context = context;
|
||||
}
|
||||
public override async Task<CreateNewProductsResponse> CreateNewProducts(CreateNewProductsRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
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)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
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)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
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)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
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)
|
||||
@@ -102,22 +223,256 @@ public class ProductsService : ProductsContract.ProductsContractBase
|
||||
|
||||
public override async Task<BulkUpdateProductPricesResponse> BulkUpdateProductPrices(BulkUpdateProductPricesRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
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)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
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)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
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)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
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 =============
|
||||
@@ -279,4 +634,56 @@ public class ProductsService : ProductsContract.ProductsContractBase
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user