Refactor and enhance product and order handling

- Updated `DiscountActionDialogBoxViewModel` and `FastProductCreateDialogBoxViewModel` to create command objects directly from properties and improved error handling.
- Added meta tag management UI and logic in `ProductActionDialogBox.razor` and `ProductActionDialogBoxViewModel`.
- Increased max file read stream size to 8 MB in `StorageDialogBoxViewModel`.
- Incremented `AssemblyVersion` and `FileVersion` to `1.7.20.34` in `Netina.AdminPanel.PWA.csproj`.
- Updated `BrandsPage.razor` and `BrandsPageViewModel` for pagination and service injection.
- Updated `CategoriesPageViewModel` to create command objects directly from properties.
- Updated `ProductsPage.razor` for service injection and added a button for product details.
- Updated `ICrudApiRest` and `ICrudDtoApiRest` interfaces to use generic `Create` methods.
- Updated `appsettings.Development.json` for `StorageBaseUrl` and commented out `IsShop`.
- Added new project `AppHost.csproj` targeting .NET 8.0 with Aspire hosting.
- Added new `appsettings.Development.json` and `appsettings.json` for logging.
- Added new `Program.cs` to create and run a distributed application.
- Added new `launchSettings.json` for application launch settings.
- Added `Extensions.cs` for common .NET Aspire services.
- Added new project `ServiceDefaults.csproj` for shared service defaults.
- Introduced `ProductMetaTag` class and related migration for meta tag handling.
- Updated `OrderController.cs` for additional authorization requirements.
- Updated target frameworks to `net8.0` in various projects.
- Enhanced `SiteMapService.cs` to include brand site maps.
- Added new properties to DTOs for customer and meta tag handling.
- Enhanced `Product` class with meta tag management methods.
- Refactored `OrderMapper.g.cs` and `ProductMapper.g.cs` for improved mapping logic.
- Enhanced command handlers to manage meta tags.
- Added `ICurrentUserService` for user permissions in query handlers.
- Refactored `StorageService.cs` for paginated storage file fetching.
subProduct
Amir Hossein Khademi 2024-12-06 17:37:41 +03:30
parent 82f9d604da
commit 53dc2eac0c
26 changed files with 3401 additions and 890 deletions

View File

@ -1 +1 @@
1.6.18.28
1.7.20.34

View File

@ -16,7 +16,10 @@ public class OrderController : ICarterModule
group.MapGet("{id}", GetAsync)
.WithDisplayName("Get Order")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ViewAllOrders, ApplicationPermission.ManageOrders))
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission,
ApplicationPermission.ViewAllOrders,
ApplicationPermission.ManageOrders,
ApplicationPermission.ViewMineOrders))
.HasApiVersion(1.0);
group.MapPost("{id}/confirm", ConfirmOrderStepAsync)

View File

@ -6,8 +6,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<AssemblyVersion>1.6.18.28</AssemblyVersion>
<FileVersion>1.6.18.28</FileVersion>
<AssemblyVersion>1.7.20.34</AssemblyVersion>
<FileVersion>1.7.20.34</FileVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--<PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -12,9 +12,9 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
</ItemGroup>-->
</ItemGroup>
<PropertyGroup>
<!--<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<LangVersion>10</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
@ -25,7 +25,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
</ItemGroup>
</ItemGroup>-->
<ItemGroup>
<Using Include="MD.PersianDateTime.Standard" />

View File

@ -99,7 +99,7 @@ public class SiteMapService(
await CreateCategoriesSiteMapsAsync();
await CreateProductsSiteMapsAsync();
await CreateBlogsSiteMapsAsync();
//await CreateBrandsSiteMapsAsync();
await CreateBrandsSiteMapsAsync();
await CreateBlogCategoriesSiteMapsAsync();
await CreatePagesSiteMapsAsync();

View File

@ -19,6 +19,7 @@ public class OrderLDto : BaseDto<OrderLDto,Order>
public string DiscountCode { get; set; } = string.Empty;
public long TotalPriceWithoutDiscount => TotalPrice + DiscountPrice;
public Guid CustomerId { get; set; }
public string CustomerFullName { get; set; } = string.Empty;
public string CustomerPhoneNumber { get; set; } = string.Empty;

View File

@ -30,6 +30,7 @@ public class ProductLDto : BaseDto<ProductLDto,Product>
public List<SpecificationSDto> Specifications { get; set; } = new();
public List<StorageFileSDto> Files { get; set; } = new();
public List<MetaTagSDto> MetaTags { get; set; } = new();
public DiscountSDto? SpecialOffer { get; set; }
public Guid AuthorId { get; set; }

View File

@ -21,4 +21,5 @@ public class OrderSDto : BaseDto<OrderSDto, Order>
public string CustomerFullName { get; set; } = string.Empty;
public string CustomerPhoneNumber { get; set; } = string.Empty;
public Guid CustomerId { get; set; }
public string DeliveryTrackingCode { get; set; } = string.Empty;
}

View File

@ -6,6 +6,7 @@ public partial class ProductCategoryMetaTag : MetaTag
{
}
public ProductCategoryMetaTag(string type, string value, Guid productCategoryId) : base(type, value)
{
ProductCategoryId = productCategoryId;

View File

@ -60,6 +60,9 @@ public partial class Product
Specifications.Add(ent);
return ent;
}
public void AddMetaTag(string key, string value)
=> MetaTags.Add(ProductMetaTag.Create(key, value, Id));
}
public partial class ProductComment
@ -67,6 +70,11 @@ public partial class ProductComment
public static ProductComment Create(string title, string content, float rate, bool isBuyer, bool isAdmin, Guid userId, Guid productId)
=> new(title, content, rate, isBuyer, isAdmin, userId, productId);
}
public partial class ProductMetaTag
{
public static ProductMetaTag Create(string key, string value, Guid productId)
=> new ProductMetaTag(key, value, productId);
}
public partial class ProductStorageFile
{

View File

@ -85,5 +85,7 @@ public partial class Product : ApiEntity
public List<ProductComment> Comments { get; internal set; } = new();
public List<ProductStorageFile> Files { get; internal set; } = new();
public List<ProductMetaTag> MetaTags { get; internal set; } = new();
public List<OrderProduct> OrderProducts { get; internal set; } = new();
}

View File

@ -0,0 +1,17 @@
namespace Netina.Domain.Entities.Products;
public partial class ProductMetaTag : MetaTag
{
public ProductMetaTag()
{
}
public ProductMetaTag(string type, string value, Guid productId) : base(type, value)
{
ProductId = productId;
}
public Guid ProductId { get; set; }
public Product? Product { get; set; }
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
namespace Netina.Domain.Mappers
{
public static partial class ProductMetaTagMapper
{
}
}

View File

@ -100,6 +100,7 @@ public class MapsterRegister : IRegister
config.NewConfig<Order, OrderSDto>()
.Map("CustomerFullName", o => o.Customer != null && o.Customer.User != null ? o.Customer.User.FirstName + " " + o.Customer.User.LastName : string.Empty)
.Map("CustomerPhoneNumber", o => o.Customer != null && o.Customer.User != null ? o.Customer.User.PhoneNumber : string.Empty)
.Map(d=>d.DeliveryTrackingCode,o=>o.OrderDelivery!=null ? o.OrderDelivery.TrackingCode : string.Empty)
.IgnoreNullValues(false)
.TwoWays();

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--<PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -15,9 +15,9 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" />
</ItemGroup>-->
</ItemGroup>
<PropertyGroup>
<!--<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<LangVersion>10</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
@ -33,7 +33,7 @@
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" />
</ItemGroup>
</ItemGroup>-->
<ItemGroup>

View File

@ -81,7 +81,7 @@ public class StorageService(IOptionsSnapshot<SiteSettings> snapshot) : IStorageS
public async Task<List<StorageFileSDto>> GetStorageFiles(StorageFileType fileType)
{
var client = GetClientAsync();
ListObjectsRequest request = new() { BucketName = _bucketName};
ListObjectsV2Request request = new() { BucketName = _bucketName, MaxKeys = 3000 };
switch (fileType)
{
case StorageFileType.Image:
@ -93,33 +93,22 @@ public class StorageService(IOptionsSnapshot<SiteSettings> snapshot) : IStorageS
default:
break;
}
var files = new List<StorageFileSDto>();
do
var pagination = client.Paginators.ListObjectsV2(request);
var s3Files = new List<S3Object>();
await foreach (var listObjectsV2Response in pagination.Responses)
{
ListObjectsResponse response = await client.ListObjectsAsync(request);
// Process the response.
foreach (var s3Object in response.S3Objects.Where(s=>s.Size>0))
{
files.Add(new StorageFileSDto
{
FileLocation = s3Object.Key,
FileName = s3Object.Key.Split('/').Last()
});
s3Files.AddRange(listObjectsV2Response.S3Objects);
}
// If the response is truncated, set the marker to get the next
// set of keys.
if (response.IsTruncated)
var files = s3Files
.OrderByDescending(s => s.LastModified)
.Take(100)
.Select(s => new StorageFileSDto
{
request.Marker = response.NextMarker;
}
else
{
request = null;
}
} while (request != null);
FileLocation = s.Key,
FileName = s.Key.Split('/').Last()
}).ToList();
return files.OrderByDescending(o=>o.CreatedAt).ToList();
return files.ToList();
}
}

View File

@ -1,19 +1,35 @@
using Netina.Domain.Entities.Orders;
using Netina.Repository.Abstracts;
namespace Netina.Repository.Handlers.Orders;
public class GetOrderLDtoQueryHandler(IRepositoryWrapper repositoryWrapper)
public class GetOrderLDtoQueryHandler(IRepositoryWrapper repositoryWrapper,ICurrentUserService currentUserService)
: IRequestHandler<GetOrderLDtoQuery, OrderLDto>
{
public async Task<OrderLDto> Handle(GetOrderLDtoQuery request, CancellationToken cancellationToken)
{
if (currentUserService.Permissions == null)
throw new BaseApiException(ApiResultStatusCode.UnAuthorized);
if (request.Id == default)
throw new AppException("Order id is null");
var order = await repositoryWrapper.SetRepository<Order>()
.TableNoTracking
.Where(o => o.Id == request.Id)
.Select(OrderMapper.ProjectToLDto)
.FirstOrDefaultAsync(cancellationToken);
if (currentUserService.Permissions.Contains(ApplicationPermission.ViewMineOrders) && !currentUserService.Permissions.Contains(ApplicationPermission.ViewAllOrders))
{
if (currentUserService.UserId.IsNullOrEmpty() || !Guid.TryParse(currentUserService.UserId, out Guid userId))
throw new BaseApiException(ApiResultStatusCode.UnAuthorized);
var customer = await repositoryWrapper.SetRepository<Customer>()
.TableNoTracking
.FirstOrDefaultAsync(c => c.UserId == userId, cancellationToken);
if (customer == null || order.CustomerId != customer.Id)
throw new BaseApiException(ApiResultStatusCode.UnAuthorized);
}
if (order == null)
throw new AppException("Order not found", ApiResultStatusCode.NotFound);
return order;

View File

@ -2,17 +2,17 @@
namespace Netina.Repository.Handlers.Orders;
public class GetOrderQueryHandler(IRepositoryWrapper repositoryWrapper) : IRequestHandler<GetOrderQuery, Order>
public class GetOrderQueryHandler(IRepositoryWrapper repositoryWrapper, ICurrentUserService currentUserService) : IRequestHandler<GetOrderQuery, Order>
{
public async Task<Order> Handle(GetOrderQuery request, CancellationToken cancellationToken)
{
var order = await repositoryWrapper.SetRepository<Order>()
.TableNoTracking
.FirstOrDefaultAsync(o => o.Id == request.Id, cancellationToken);
if (order == null)
throw new AppException("Order not found", ApiResultStatusCode.NotFound);
var orderProducts = await repositoryWrapper.SetRepository<OrderProduct>()
.TableNoTracking
.Where(op => op.OrderId == order.Id)
@ -20,7 +20,7 @@ public class GetOrderQueryHandler(IRepositoryWrapper repositoryWrapper) : IReque
orderProducts.ForEach(op => order.AddOrderProduct(op));
var orderDelivery= await repositoryWrapper.SetRepository<OrderDelivery>()
var orderDelivery = await repositoryWrapper.SetRepository<OrderDelivery>()
.TableNoTracking
.FirstOrDefaultAsync(od => od.OrderId == request.Id, cancellationToken);
if (orderDelivery != null)

View File

@ -56,6 +56,9 @@ public class CreateProductCommandHandler(IRepositoryWrapper repositoryWrapper,IM
await UpdateFaqAsync(ent, request.Faqs, cancellationToken);
foreach (var (key, value) in request.MetaTags)
ent.AddMetaTag(key, value);
return ent.AdaptToLDto();
}

View File

@ -29,6 +29,7 @@ public class UpdateProductCommandHandler(IRepositoryWrapper repositoryWrapper,IM
newEnt.CreatedAt = ent.CreatedAt;
newEnt.CreatedBy = ent.CreatedBy;
//Check Specifications
var dbSpecifications = await repositoryWrapper.SetRepository<Specification>().TableNoTracking
.Where(s => s.ProductId == ent.Id).ToListAsync(cancellationToken);
foreach (var dbSpecification in dbSpecifications)
@ -44,8 +45,22 @@ public class UpdateProductCommandHandler(IRepositoryWrapper repositoryWrapper,IM
newEnt.AddSpecification(specification.Title, specification.Detail, specification.Value, specification.IsFeature, specification.ParentId);
}
//Check MetaTags
var dbMetaTags = await repositoryWrapper.SetRepository<ProductMetaTag>()
.TableNoTracking
.Where(f => f.ProductId == ent.Id)
.ToListAsync(cancellationToken);
foreach (var feature in dbMetaTags.Where(feature => request.MetaTags.Any(f => f.Key == feature.Type) == false))
{
repositoryWrapper.SetRepository<ProductMetaTag>()
.Delete(feature);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
}
foreach (var (key, value) in request.MetaTags.Where(f => dbMetaTags.Any(dbf => dbf.Type == f.Key) == false))
newEnt.AddMetaTag(key, value);
//Check Files
var dbFiles = await repositoryWrapper.SetRepository<ProductStorageFile>().TableNoTracking
.Where(s => s.ProductId == ent.Id).ToListAsync(cancellationToken);
foreach (var dbFile in dbFiles)

View File

@ -17,7 +17,7 @@ namespace NetinaShop.Repository.Migrations
table: "Products",
type: "uuid",
nullable: false,
defaultValue: new Guid("8723f1d2-e091-4812-9110-5161c9e23586"));
defaultValue: new Guid("11c47231-4f8b-4a73-b848-d2edf3c2d9ab"));
migrationBuilder.AddColumn<Guid>(
name: "AuthorId",
@ -25,7 +25,7 @@ namespace NetinaShop.Repository.Migrations
table: "Blogs",
type: "uuid",
nullable: false,
defaultValue: new Guid("8723f1d2-e091-4812-9110-5161c9e23586"));
defaultValue: new Guid("11c47231-4f8b-4a73-b848-d2edf3c2d9ab"));
migrationBuilder.CreateIndex(
name: "IX_Products_AuthorId",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NetinaShop.Repository.Migrations
{
/// <inheritdoc />
public partial class AddProductMetaTag : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "ProductId",
schema: "public",
table: "MetaTags",
type: "uuid",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_MetaTags_ProductId",
schema: "public",
table: "MetaTags",
column: "ProductId");
migrationBuilder.AddForeignKey(
name: "FK_MetaTags_Products_ProductId",
schema: "public",
table: "MetaTags",
column: "ProductId",
principalSchema: "public",
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MetaTags_Products_ProductId",
schema: "public",
table: "MetaTags");
migrationBuilder.DropIndex(
name: "IX_MetaTags_ProductId",
schema: "public",
table: "MetaTags");
migrationBuilder.DropColumn(
name: "ProductId",
schema: "public",
table: "MetaTags");
}
}
}

View File

@ -1698,6 +1698,18 @@ namespace NetinaShop.Repository.Migrations
b.HasDiscriminator().HasValue("ProductCategoryMetaTag");
});
modelBuilder.Entity("Netina.Domain.Entities.Products.ProductMetaTag", b =>
{
b.HasBaseType("Netina.Domain.Entities.Seo.MetaTag");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.HasIndex("ProductId");
b.HasDiscriminator().HasValue("ProductMetaTag");
});
modelBuilder.Entity("Netina.Domain.Entities.Blogs.BlogStorageFile", b =>
{
b.HasBaseType("Netina.Domain.Entities.StorageFiles.StorageFile");
@ -2070,6 +2082,16 @@ namespace NetinaShop.Repository.Migrations
b.Navigation("ProductCategory");
});
modelBuilder.Entity("Netina.Domain.Entities.Products.ProductMetaTag", b =>
{
b.HasOne("Netina.Domain.Entities.Products.Product", "Product")
.WithMany("MetaTags")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Product");
});
modelBuilder.Entity("Netina.Domain.Entities.Blogs.BlogStorageFile", b =>
{
b.HasOne("Netina.Domain.Entities.Blogs.Blog", "Blog")
@ -2161,6 +2183,8 @@ namespace NetinaShop.Repository.Migrations
b.Navigation("Files");
b.Navigation("MetaTags");
b.Navigation("OrderProducts");
b.Navigation("Specifications");