feat: 菜品菜单查询与子资源替换完善
This commit is contained in:
@@ -171,4 +171,89 @@ public sealed class ProductsController(IMediator mediator) : BaseApiController
|
|||||||
? ApiResponse<ProductDto>.Error(ErrorCodes.NotFound, "商品不存在")
|
? ApiResponse<ProductDto>.Error(ErrorCodes.NotFound, "商品不存在")
|
||||||
: ApiResponse<ProductDto>.Ok(result);
|
: ApiResponse<ProductDto>.Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换商品 SKU。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{productId:long}/skus")]
|
||||||
|
[PermissionAuthorize("product-sku:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductSkuDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<ProductSkuDto>>> ReplaceSkus(long productId, [FromBody] ReplaceProductSkusCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (command.ProductId == 0)
|
||||||
|
{
|
||||||
|
command = command with { ProductId = productId };
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<ProductSkuDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换商品规格。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{productId:long}/attributes")]
|
||||||
|
[PermissionAuthorize("product-attr:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductAttributeGroupDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<ProductAttributeGroupDto>>> ReplaceAttributes(long productId, [FromBody] ReplaceProductAttributesCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (command.ProductId == 0)
|
||||||
|
{
|
||||||
|
command = command with { ProductId = productId };
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<ProductAttributeGroupDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换商品加料。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{productId:long}/addons")]
|
||||||
|
[PermissionAuthorize("product-addon:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductAddonGroupDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<ProductAddonGroupDto>>> ReplaceAddons(long productId, [FromBody] ReplaceProductAddonsCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (command.ProductId == 0)
|
||||||
|
{
|
||||||
|
command = command with { ProductId = productId };
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<ProductAddonGroupDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换商品媒资。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{productId:long}/media")]
|
||||||
|
[PermissionAuthorize("product-media:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductMediaAssetDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<ProductMediaAssetDto>>> ReplaceMedia(long productId, [FromBody] ReplaceProductMediaCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (command.ProductId == 0)
|
||||||
|
{
|
||||||
|
command = command with { ProductId = productId };
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<ProductMediaAssetDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换商品价格策略。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{productId:long}/pricing-rules")]
|
||||||
|
[PermissionAuthorize("product-pricing:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductPricingRuleDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<ProductPricingRuleDto>>> ReplacePricingRules(long productId, [FromBody] ReplaceProductPricingRulesCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (command.ProductId == 0)
|
||||||
|
{
|
||||||
|
command = command with { ProductId = productId };
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<ProductPricingRuleDto>>.Ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,16 @@
|
|||||||
"product:update",
|
"product:update",
|
||||||
"product:delete",
|
"product:delete",
|
||||||
"product:publish",
|
"product:publish",
|
||||||
|
"product-sku:read",
|
||||||
|
"product-sku:update",
|
||||||
|
"product-attr:read",
|
||||||
|
"product-attr:update",
|
||||||
|
"product-addon:read",
|
||||||
|
"product-addon:update",
|
||||||
|
"product-media:read",
|
||||||
|
"product-media:update",
|
||||||
|
"product-pricing:read",
|
||||||
|
"product-pricing:update",
|
||||||
"order:create",
|
"order:create",
|
||||||
"order:read",
|
"order:read",
|
||||||
"order:update",
|
"order:update",
|
||||||
@@ -197,6 +207,17 @@
|
|||||||
"product:read",
|
"product:read",
|
||||||
"product:update",
|
"product:update",
|
||||||
"product:delete",
|
"product:delete",
|
||||||
|
"product:publish",
|
||||||
|
"product-sku:read",
|
||||||
|
"product-sku:update",
|
||||||
|
"product-attr:read",
|
||||||
|
"product-attr:update",
|
||||||
|
"product-addon:read",
|
||||||
|
"product-addon:update",
|
||||||
|
"product-media:read",
|
||||||
|
"product-media:update",
|
||||||
|
"product-pricing:read",
|
||||||
|
"product-pricing:update",
|
||||||
"order:create",
|
"order:create",
|
||||||
"order:read",
|
"order:read",
|
||||||
"order:update",
|
"order:update",
|
||||||
@@ -242,6 +263,17 @@
|
|||||||
"product:create",
|
"product:create",
|
||||||
"product:read",
|
"product:read",
|
||||||
"product:update",
|
"product:update",
|
||||||
|
"product:publish",
|
||||||
|
"product-sku:read",
|
||||||
|
"product-sku:update",
|
||||||
|
"product-attr:read",
|
||||||
|
"product-attr:update",
|
||||||
|
"product-addon:read",
|
||||||
|
"product-addon:update",
|
||||||
|
"product-media:read",
|
||||||
|
"product-media:update",
|
||||||
|
"product-pricing:read",
|
||||||
|
"product-pricing:update",
|
||||||
"order:create",
|
"order:create",
|
||||||
"order:read",
|
"order:read",
|
||||||
"order:update",
|
"order:update",
|
||||||
@@ -331,6 +363,17 @@
|
|||||||
"product:read",
|
"product:read",
|
||||||
"product:update",
|
"product:update",
|
||||||
"product:delete",
|
"product:delete",
|
||||||
|
"product:publish",
|
||||||
|
"product-sku:read",
|
||||||
|
"product-sku:update",
|
||||||
|
"product-attr:read",
|
||||||
|
"product-attr:update",
|
||||||
|
"product-addon:read",
|
||||||
|
"product-addon:update",
|
||||||
|
"product-media:read",
|
||||||
|
"product-media:update",
|
||||||
|
"product-pricing:read",
|
||||||
|
"product-pricing:update",
|
||||||
"order:create",
|
"order:create",
|
||||||
"order:read",
|
"order:read",
|
||||||
"order:update",
|
"order:update",
|
||||||
|
|||||||
38
src/Api/TakeoutSaaS.MiniApi/Controllers/MenusController.cs
Normal file
38
src/Api/TakeoutSaaS.MiniApi/Controllers/MenusController.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Queries;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.MiniApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序端菜单查询。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/mini/v{version:apiVersion}/stores/{storeId:long}/menu")]
|
||||||
|
public sealed class MenusController(IMediator mediator) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店菜单(含分类与商品详情)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreMenuDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreMenuDto>> GetMenu(long storeId, [FromQuery] DateTime? updatedAfter, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 组装查询
|
||||||
|
var query = new GetStoreMenuQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
UpdatedAfter = updatedAfter
|
||||||
|
};
|
||||||
|
// 2. 拉取菜单
|
||||||
|
var result = await mediator.Send(query, cancellationToken);
|
||||||
|
return ApiResponse<StoreMenuDto>.Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换商品加料命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ReplaceProductAddonsCommand : IRequest<IReadOnlyList<ProductAddonGroupDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long ProductId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加料组。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ProductAddonGroupDto> AddonGroups { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换商品规格命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ReplaceProductAttributesCommand : IRequest<IReadOnlyList<ProductAttributeGroupDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long ProductId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规格组。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ProductAttributeGroupDto> AttributeGroups { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换商品媒资命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ReplaceProductMediaCommand : IRequest<IReadOnlyList<ProductMediaAssetDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long ProductId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 媒资列表。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ProductMediaAssetDto> MediaAssets { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换商品价格策略命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ReplaceProductPricingRulesCommand : IRequest<IReadOnlyList<ProductPricingRuleDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long ProductId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 价格策略。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ProductPricingRuleDto> PricingRules { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换商品 SKU 命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ReplaceProductSkusCommand : IRequest<IReadOnlyList<ProductSkuDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long ProductId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SKU 列表。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ProductSkuDto> Skus { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店菜单分类 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ProductCategoryMenuDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 分类 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序。
|
||||||
|
/// </summary>
|
||||||
|
public int SortOrder { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEnabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类下商品列表。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ProductDetailDto> Products { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店菜单数据传输对象。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record StoreMenuDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类与商品集合。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ProductCategoryMenuDto> Categories { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单生成时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime GeneratedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 客户端请求的增量时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? UpdatedAfter { get; init; }
|
||||||
|
}
|
||||||
@@ -37,21 +37,25 @@ public sealed class GetProductDetailQueryHandler(
|
|||||||
await Task.WhenAll(skusTask, attrGroupsTask, attrOptionsTask, addonGroupsTask, addonOptionsTask, mediaTask, pricingTask);
|
await Task.WhenAll(skusTask, attrGroupsTask, attrOptionsTask, addonGroupsTask, addonOptionsTask, mediaTask, pricingTask);
|
||||||
|
|
||||||
// 3. 组装 DTO
|
// 3. 组装 DTO
|
||||||
var attrOptions = attrOptionsTask.Result.ToLookup(x => x.AttributeGroupId);
|
var skus = await skusTask;
|
||||||
var addonOptions = addonOptionsTask.Result.ToLookup(x => x.AddonGroupId);
|
var attrGroups = await attrGroupsTask;
|
||||||
|
var attrOptions = (await attrOptionsTask).ToLookup(x => x.AttributeGroupId);
|
||||||
|
var addonGroups = await addonGroupsTask;
|
||||||
|
var addonOptions = (await addonOptionsTask).ToLookup(x => x.AddonGroupId);
|
||||||
|
var mediaAssets = await mediaTask;
|
||||||
|
var pricingRules = await pricingTask;
|
||||||
var detail = new ProductDetailDto
|
var detail = new ProductDetailDto
|
||||||
{
|
{
|
||||||
Product = ProductMapping.ToDto(product),
|
Product = ProductMapping.ToDto(product),
|
||||||
Skus = skusTask.Result.Select(ProductMapping.ToDto).ToList(),
|
Skus = skus.Select(ProductMapping.ToDto).ToList(),
|
||||||
AttributeGroups = attrGroupsTask.Result
|
AttributeGroups = attrGroups
|
||||||
.Select(g => ProductMapping.ToDto(g, attrOptions[g.Id].ToList()))
|
.Select(g => ProductMapping.ToDto(g, attrOptions[g.Id].ToList()))
|
||||||
.ToList(),
|
.ToList(),
|
||||||
AddonGroups = addonGroupsTask.Result
|
AddonGroups = addonGroups
|
||||||
.Select(g => ProductMapping.ToDto(g, addonOptions[g.Id].ToList()))
|
.Select(g => ProductMapping.ToDto(g, addonOptions[g.Id].ToList()))
|
||||||
.ToList(),
|
.ToList(),
|
||||||
MediaAssets = mediaTask.Result.Select(ProductMapping.ToDto).ToList(),
|
MediaAssets = mediaAssets.Select(ProductMapping.ToDto).ToList(),
|
||||||
PricingRules = pricingTask.Result.Select(ProductMapping.ToDto).ToList()
|
PricingRules = pricingRules.Select(ProductMapping.ToDto).ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
return detail;
|
return detail;
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Products.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Products.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Products.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店菜单查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetStoreMenuQueryHandler(
|
||||||
|
IProductRepository productRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<GetStoreMenuQueryHandler> logger)
|
||||||
|
: IRequestHandler<GetStoreMenuQuery, StoreMenuDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<StoreMenuDto> Handle(GetStoreMenuQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 准备上下文
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var updatedAfterUtc = request.UpdatedAfter?.ToUniversalTime();
|
||||||
|
// 2. 获取分类
|
||||||
|
var categories = await productRepository.GetCategoriesByStoreAsync(tenantId, request.StoreId, true, cancellationToken);
|
||||||
|
// 3. 读取上架商品(支持增量)
|
||||||
|
var products = await productRepository.SearchAsync(tenantId, request.StoreId, null, ProductStatus.OnSale, cancellationToken, updatedAfterUtc);
|
||||||
|
if (products.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation("门店 {StoreId} 没有上架商品,返回空菜单", request.StoreId);
|
||||||
|
return new StoreMenuDto
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId,
|
||||||
|
GeneratedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAfter = updatedAfterUtc,
|
||||||
|
Categories = categories
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.Select(category => new ProductCategoryMenuDto
|
||||||
|
{
|
||||||
|
Id = category.Id,
|
||||||
|
StoreId = category.StoreId,
|
||||||
|
Name = category.Name,
|
||||||
|
Description = category.Description,
|
||||||
|
SortOrder = category.SortOrder,
|
||||||
|
IsEnabled = category.IsEnabled,
|
||||||
|
Products = []
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 并发加载子表数据
|
||||||
|
var productIds = products.Select(x => x.Id).ToList();
|
||||||
|
var skusTask = productRepository.GetSkusByProductIdsAsync(productIds, tenantId, cancellationToken);
|
||||||
|
var attributeGroupsTask = productRepository.GetAttributeGroupsByProductIdsAsync(productIds, tenantId, cancellationToken);
|
||||||
|
var addonGroupsTask = productRepository.GetAddonGroupsByProductIdsAsync(productIds, tenantId, cancellationToken);
|
||||||
|
var mediaTask = productRepository.GetMediaAssetsByProductIdsAsync(productIds, tenantId, cancellationToken);
|
||||||
|
var pricingTask = productRepository.GetPricingRulesByProductIdsAsync(productIds, tenantId, cancellationToken);
|
||||||
|
await Task.WhenAll(skusTask, attributeGroupsTask, addonGroupsTask, mediaTask, pricingTask);
|
||||||
|
var attributeGroups = await attributeGroupsTask;
|
||||||
|
var addonGroups = await addonGroupsTask;
|
||||||
|
// 批量读取规格与加料选项
|
||||||
|
var attributeOptionsTask = attributeGroups.Count == 0
|
||||||
|
? Task.FromResult<IReadOnlyList<ProductAttributeOption>>(Array.Empty<ProductAttributeOption>())
|
||||||
|
: productRepository.GetAttributeOptionsByGroupIdsAsync(attributeGroups.Select(x => x.Id).ToList(), tenantId, cancellationToken);
|
||||||
|
var addonOptionsTask = addonGroups.Count == 0
|
||||||
|
? Task.FromResult<IReadOnlyList<ProductAddonOption>>(Array.Empty<ProductAddonOption>())
|
||||||
|
: productRepository.GetAddonOptionsByGroupIdsAsync(addonGroups.Select(x => x.Id).ToList(), tenantId, cancellationToken);
|
||||||
|
await Task.WhenAll(attributeOptionsTask, addonOptionsTask);
|
||||||
|
|
||||||
|
// 5. 建立查找表
|
||||||
|
var skuLookup = (await skusTask).ToLookup(x => x.ProductId);
|
||||||
|
var attrGroupLookup = attributeGroups.ToLookup(x => x.ProductId);
|
||||||
|
var attrOptionLookup = (await attributeOptionsTask).ToLookup(x => x.AttributeGroupId);
|
||||||
|
var addonGroupLookup = addonGroups.ToLookup(x => x.ProductId);
|
||||||
|
var addonOptionLookup = (await addonOptionsTask).ToLookup(x => x.AddonGroupId);
|
||||||
|
var mediaLookup = (await mediaTask).ToLookup(x => x.ProductId);
|
||||||
|
var pricingLookup = (await pricingTask).ToLookup(x => x.ProductId);
|
||||||
|
// 6. 组装商品详情
|
||||||
|
var productDetails = products.ToDictionary(
|
||||||
|
product => product.Id,
|
||||||
|
product =>
|
||||||
|
{
|
||||||
|
var attributeDtos = attrGroupLookup[product.Id]
|
||||||
|
.Select(group => ProductMapping.ToDto(group, attrOptionLookup[group.Id].ToList()))
|
||||||
|
.ToList();
|
||||||
|
var addonDtos = addonGroupLookup[product.Id]
|
||||||
|
.Select(group => ProductMapping.ToDto(group, addonOptionLookup[group.Id].ToList()))
|
||||||
|
.ToList();
|
||||||
|
return new ProductDetailDto
|
||||||
|
{
|
||||||
|
Product = ProductMapping.ToDto(product),
|
||||||
|
Skus = skuLookup[product.Id].Select(ProductMapping.ToDto).ToList(),
|
||||||
|
AttributeGroups = attributeDtos,
|
||||||
|
AddonGroups = addonDtos,
|
||||||
|
MediaAssets = mediaLookup[product.Id].Select(ProductMapping.ToDto).ToList(),
|
||||||
|
PricingRules = pricingLookup[product.Id].Select(ProductMapping.ToDto).ToList()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// 7. 组装分类菜单
|
||||||
|
var productsByCategory = products.ToLookup(x => x.CategoryId);
|
||||||
|
var categoryMenu = categories
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.Select(category =>
|
||||||
|
{
|
||||||
|
var categoryProducts = productsByCategory[category.Id]
|
||||||
|
.Select(p => productDetails[p.Id])
|
||||||
|
.ToList();
|
||||||
|
return new ProductCategoryMenuDto
|
||||||
|
{
|
||||||
|
Id = category.Id,
|
||||||
|
StoreId = category.StoreId,
|
||||||
|
Name = category.Name,
|
||||||
|
Description = category.Description,
|
||||||
|
SortOrder = category.SortOrder,
|
||||||
|
IsEnabled = category.IsEnabled,
|
||||||
|
Products = categoryProducts
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
return new StoreMenuDto
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId,
|
||||||
|
GeneratedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAfter = updatedAfterUtc,
|
||||||
|
Categories = categoryMenu
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Products.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Products.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换加料处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReplaceProductAddonsCommandHandler(
|
||||||
|
IProductRepository productRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<ReplaceProductAddonsCommandHandler> logger)
|
||||||
|
: IRequestHandler<ReplaceProductAddonsCommand, IReadOnlyList<ProductAddonGroupDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductAddonGroupDto>> Handle(ReplaceProductAddonsCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 校验商品
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var product = await productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken);
|
||||||
|
if (product is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "商品不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验组名唯一
|
||||||
|
var names = request.AddonGroups.Select(x => x.Name.Trim()).ToList();
|
||||||
|
if (names.Count != names.Distinct(StringComparer.OrdinalIgnoreCase).Count())
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, "加料组名称重复");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 替换
|
||||||
|
await productRepository.RemoveAddonGroupsAsync(request.ProductId, tenantId, cancellationToken);
|
||||||
|
// 重新插入组
|
||||||
|
var groupEntities = request.AddonGroups.Select(g => new ProductAddonGroup
|
||||||
|
{
|
||||||
|
ProductId = request.ProductId,
|
||||||
|
Name = g.Name.Trim(),
|
||||||
|
MinSelect = g.MinSelect,
|
||||||
|
MaxSelect = g.MaxSelect,
|
||||||
|
SortOrder = g.SortOrder
|
||||||
|
}).ToList();
|
||||||
|
await productRepository.AddAddonGroupsAsync(groupEntities, [], cancellationToken);
|
||||||
|
await productRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
// 重新建立组与请求的映射
|
||||||
|
var groupIdLookup = groupEntities.Zip(request.AddonGroups, (entity, dto) => (entity, dto))
|
||||||
|
.ToDictionary(x => x.dto, x => x.entity.Id);
|
||||||
|
// 构建选项实体
|
||||||
|
var optionEntities = request.AddonGroups
|
||||||
|
.SelectMany(dto => dto.Options.Select(o => new ProductAddonOption
|
||||||
|
{
|
||||||
|
AddonGroupId = groupIdLookup[dto],
|
||||||
|
Name = o.Name.Trim(),
|
||||||
|
ExtraPrice = o.ExtraPrice,
|
||||||
|
SortOrder = o.SortOrder
|
||||||
|
}))
|
||||||
|
.ToList();
|
||||||
|
await productRepository.AddAddonGroupsAsync([], optionEntities, cancellationToken);
|
||||||
|
await productRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
logger.LogInformation("替换商品 {ProductId} 加料组 {Count} 个", request.ProductId, groupEntities.Count);
|
||||||
|
|
||||||
|
return groupEntities
|
||||||
|
.Select(g => ProductMapping.ToDto(g, optionEntities.Where(o => o.AddonGroupId == g.Id).ToList()))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Products.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Products.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换规格处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReplaceProductAttributesCommandHandler(
|
||||||
|
IProductRepository productRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<ReplaceProductAttributesCommandHandler> logger)
|
||||||
|
: IRequestHandler<ReplaceProductAttributesCommand, IReadOnlyList<ProductAttributeGroupDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductAttributeGroupDto>> Handle(ReplaceProductAttributesCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 校验商品
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var product = await productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken);
|
||||||
|
if (product is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "商品不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 组名唯一
|
||||||
|
var groupNames = request.AttributeGroups.Select(x => x.Name.Trim()).ToList();
|
||||||
|
if (groupNames.Count != groupNames.Distinct(StringComparer.OrdinalIgnoreCase).Count())
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, "规格组名称重复");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 替换
|
||||||
|
await productRepository.RemoveAttributeGroupsAsync(request.ProductId, tenantId, cancellationToken);
|
||||||
|
|
||||||
|
var groupEntities = request.AttributeGroups.Select(g => new ProductAttributeGroup
|
||||||
|
{
|
||||||
|
ProductId = request.ProductId,
|
||||||
|
Name = g.Name.Trim(),
|
||||||
|
SelectionType = (Domain.Products.Enums.AttributeSelectionType)g.SelectionType,
|
||||||
|
SortOrder = g.SortOrder
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
// 4. 持久化(分批保障 FK 正确)
|
||||||
|
await productRepository.AddAttributeGroupsAsync(groupEntities, [], cancellationToken);
|
||||||
|
await productRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 重新建立选项的 GroupId 映射
|
||||||
|
var groupIdLookup = groupEntities.Zip(request.AttributeGroups, (entity, dto) => (entity, dto))
|
||||||
|
.ToDictionary(x => x.dto, x => x.entity.Id);
|
||||||
|
|
||||||
|
var optionEntities = request.AttributeGroups
|
||||||
|
.SelectMany(dto => dto.Options.Select(o => new ProductAttributeOption
|
||||||
|
{
|
||||||
|
AttributeGroupId = groupIdLookup[dto],
|
||||||
|
Name = o.Name.Trim(),
|
||||||
|
SortOrder = o.SortOrder
|
||||||
|
}))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await productRepository.AddAttributeGroupsAsync([], optionEntities, cancellationToken);
|
||||||
|
await productRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
logger.LogInformation("替换商品 {ProductId} 规格组 {GroupCount} 个", request.ProductId, groupEntities.Count);
|
||||||
|
|
||||||
|
// 5. 返回 DTO
|
||||||
|
return groupEntities
|
||||||
|
.Select(g => ProductMapping.ToDto(g, optionEntities.Where(o => o.AttributeGroupId == g.Id).ToList()))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Products.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Products.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换媒资处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReplaceProductMediaCommandHandler(
|
||||||
|
IProductRepository productRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<ReplaceProductMediaCommandHandler> logger)
|
||||||
|
: IRequestHandler<ReplaceProductMediaCommand, IReadOnlyList<ProductMediaAssetDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductMediaAssetDto>> Handle(ReplaceProductMediaCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 校验商品
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var product = await productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken);
|
||||||
|
if (product is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "商品不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 替换
|
||||||
|
await productRepository.RemoveMediaAssetsAsync(request.ProductId, tenantId, cancellationToken);
|
||||||
|
|
||||||
|
var assets = request.MediaAssets.Select(a => new ProductMediaAsset
|
||||||
|
{
|
||||||
|
ProductId = request.ProductId,
|
||||||
|
MediaType = a.MediaType,
|
||||||
|
Url = a.Url.Trim(),
|
||||||
|
Caption = a.Caption?.Trim(),
|
||||||
|
SortOrder = a.SortOrder
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
await productRepository.AddMediaAssetsAsync(assets, cancellationToken);
|
||||||
|
await productRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
logger.LogInformation("替换商品 {ProductId} 媒资 {Count} 条", request.ProductId, assets.Count);
|
||||||
|
|
||||||
|
return assets.Select(ProductMapping.ToDto).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Products.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Products.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换价格策略处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReplaceProductPricingRulesCommandHandler(
|
||||||
|
IProductRepository productRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<ReplaceProductPricingRulesCommandHandler> logger)
|
||||||
|
: IRequestHandler<ReplaceProductPricingRulesCommand, IReadOnlyList<ProductPricingRuleDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductPricingRuleDto>> Handle(ReplaceProductPricingRulesCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 校验商品
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var product = await productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken);
|
||||||
|
if (product is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "商品不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 替换
|
||||||
|
await productRepository.RemovePricingRulesAsync(request.ProductId, tenantId, cancellationToken);
|
||||||
|
|
||||||
|
var rules = request.PricingRules.Select(r => new ProductPricingRule
|
||||||
|
{
|
||||||
|
ProductId = request.ProductId,
|
||||||
|
RuleType = r.RuleType,
|
||||||
|
ConditionsJson = r.ConditionsJson.Trim(),
|
||||||
|
Price = r.Price,
|
||||||
|
WeekdaysJson = r.WeekdaysJson,
|
||||||
|
SortOrder = 0
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
await productRepository.AddPricingRulesAsync(rules, cancellationToken);
|
||||||
|
await productRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
logger.LogInformation("替换商品 {ProductId} 价格策略 {Count} 条", request.ProductId, rules.Count);
|
||||||
|
|
||||||
|
return rules.Select(ProductMapping.ToDto).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Products.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Products.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换 SKU 处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReplaceProductSkusCommandHandler(
|
||||||
|
IProductRepository productRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<ReplaceProductSkusCommandHandler> logger)
|
||||||
|
: IRequestHandler<ReplaceProductSkusCommand, IReadOnlyList<ProductSkuDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductSkuDto>> Handle(ReplaceProductSkusCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 校验商品存在
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var product = await productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken);
|
||||||
|
if (product is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "商品不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验 SKU 唯一性
|
||||||
|
var codes = request.Skus.Select(x => x.SkuCode.Trim()).ToList();
|
||||||
|
if (codes.Count != codes.Distinct(StringComparer.OrdinalIgnoreCase).Count())
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, "SKU 编码重复");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 替换
|
||||||
|
await productRepository.RemoveSkusAsync(request.ProductId, tenantId, cancellationToken);
|
||||||
|
var entities = request.Skus.Select(x => new ProductSku
|
||||||
|
{
|
||||||
|
ProductId = request.ProductId,
|
||||||
|
SkuCode = x.SkuCode.Trim(),
|
||||||
|
Barcode = x.Barcode?.Trim(),
|
||||||
|
Price = x.Price,
|
||||||
|
OriginalPrice = x.OriginalPrice,
|
||||||
|
StockQuantity = x.StockQuantity,
|
||||||
|
Weight = x.Weight,
|
||||||
|
AttributesJson = x.AttributesJson ?? string.Empty,
|
||||||
|
SortOrder = x.SortOrder
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
await productRepository.AddSkusAsync(entities, cancellationToken);
|
||||||
|
await productRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
logger.LogInformation("替换商品 {ProductId} 的 SKU 数量 {Count}", request.ProductId, entities.Count);
|
||||||
|
|
||||||
|
return entities.Select(ProductMapping.ToDto).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店菜单查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record GetStoreMenuQuery : IRequest<StoreMenuDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 增量时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? UpdatedAfter { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换加料验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReplaceProductAddonsCommandValidator : AbstractValidator<ReplaceProductAddonsCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
public ReplaceProductAddonsCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.ProductId).GreaterThan(0);
|
||||||
|
RuleForEach(x => x.AddonGroups).ChildRules(group =>
|
||||||
|
{
|
||||||
|
group.RuleFor(x => x.Name).NotEmpty().MaximumLength(64);
|
||||||
|
group.RuleFor(x => x.MinSelect).GreaterThanOrEqualTo(0);
|
||||||
|
group.RuleFor(x => x.MaxSelect).GreaterThanOrEqualTo(x => x.MinSelect);
|
||||||
|
group.RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
|
||||||
|
group.RuleForEach(x => x.Options).ChildRules(opt =>
|
||||||
|
{
|
||||||
|
opt.RuleFor(x => x.Name).NotEmpty().MaximumLength(64);
|
||||||
|
opt.RuleFor(x => x.ExtraPrice).GreaterThanOrEqualTo(0).When(x => x.ExtraPrice.HasValue);
|
||||||
|
opt.RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换规格验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReplaceProductAttributesCommandValidator : AbstractValidator<ReplaceProductAttributesCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
public ReplaceProductAttributesCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.ProductId).GreaterThan(0);
|
||||||
|
RuleForEach(x => x.AttributeGroups).ChildRules(group =>
|
||||||
|
{
|
||||||
|
group.RuleFor(x => x.Name).NotEmpty().MaximumLength(64);
|
||||||
|
group.RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
|
||||||
|
group.RuleForEach(x => x.Options).ChildRules(opt =>
|
||||||
|
{
|
||||||
|
opt.RuleFor(x => x.Name).NotEmpty().MaximumLength(64);
|
||||||
|
opt.RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换媒资验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReplaceProductMediaCommandValidator : AbstractValidator<ReplaceProductMediaCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
public ReplaceProductMediaCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.ProductId).GreaterThan(0);
|
||||||
|
RuleForEach(x => x.MediaAssets).ChildRules(asset =>
|
||||||
|
{
|
||||||
|
asset.RuleFor(x => x.Url).NotEmpty().MaximumLength(512);
|
||||||
|
asset.RuleFor(x => x.Caption).MaximumLength(256);
|
||||||
|
asset.RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换价格策略验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReplaceProductPricingRulesCommandValidator : AbstractValidator<ReplaceProductPricingRulesCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
public ReplaceProductPricingRulesCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.ProductId).GreaterThan(0);
|
||||||
|
RuleForEach(x => x.PricingRules).ChildRules(rule =>
|
||||||
|
{
|
||||||
|
rule.RuleFor(x => x.Price).GreaterThan(0);
|
||||||
|
rule.RuleFor(x => x.ConditionsJson).NotEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换 SKU 验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReplaceProductSkusCommandValidator : AbstractValidator<ReplaceProductSkusCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
public ReplaceProductSkusCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.ProductId).GreaterThan(0);
|
||||||
|
RuleForEach(x => x.Skus).ChildRules(sku =>
|
||||||
|
{
|
||||||
|
sku.RuleFor(x => x.SkuCode).NotEmpty().MaximumLength(64);
|
||||||
|
sku.RuleFor(x => x.Price).GreaterThan(0);
|
||||||
|
sku.RuleFor(x => x.OriginalPrice).GreaterThan(0).When(x => x.OriginalPrice.HasValue);
|
||||||
|
sku.RuleFor(x => x.StockQuantity).GreaterThanOrEqualTo(0).When(x => x.StockQuantity.HasValue);
|
||||||
|
sku.RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -19,48 +20,88 @@ public interface IProductRepository
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按分类与状态筛选商品列表。
|
/// 按分类与状态筛选商品列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<Product>> SearchAsync(long tenantId, long? storeId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<Product>> SearchAsync(long tenantId, long? storeId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default, DateTime? updatedAfter = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取租户下的商品分类。
|
/// 获取租户下的商品分类。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<ProductCategory>> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<ProductCategory>> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店商品分类。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<ProductCategory>> GetCategoriesByStoreAsync(long tenantId, long storeId, bool onlyEnabled = true, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取商品 SKU。
|
/// 获取商品 SKU。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<ProductSku>> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<ProductSku>> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量获取商品 SKU。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<ProductSku>> GetSkusByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取商品加料组与选项。
|
/// 获取商品加料组与选项。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量获取商品加料组。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取商品加料选项。
|
/// 获取商品加料选项。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量获取商品加料选项。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsByGroupIdsAsync(IReadOnlyCollection<long> addonGroupIds, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取商品规格组与选项。
|
/// 获取商品规格组与选项。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量获取商品规格组。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取商品规格选项。
|
/// 获取商品规格选项。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量获取商品规格选项。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsByGroupIdsAsync(IReadOnlyCollection<long> attributeGroupIds, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取商品媒资。
|
/// 获取商品媒资。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量获取商品媒资。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取商品定价规则。
|
/// 获取商品定价规则。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量获取商品定价规则。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 新增分类。
|
/// 新增分类。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using TakeoutSaaS.Domain.Products.Entities;
|
using TakeoutSaaS.Domain.Products.Entities;
|
||||||
@@ -27,7 +28,7 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<Product>> SearchAsync(long tenantId, long? storeId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<Product>> SearchAsync(long tenantId, long? storeId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default, DateTime? updatedAfter = null)
|
||||||
{
|
{
|
||||||
var query = context.Products
|
var query = context.Products
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -48,6 +49,11 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
query = query.Where(x => x.Status == status.Value);
|
query = query.Where(x => x.Status == status.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updatedAfter.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => (x.UpdatedAt ?? x.CreatedAt) >= updatedAfter.Value);
|
||||||
|
}
|
||||||
|
|
||||||
var products = await query
|
var products = await query
|
||||||
.OrderBy(x => x.Name)
|
.OrderBy(x => x.Name)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
@@ -67,6 +73,22 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
return categories;
|
return categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductCategory>> GetCategoriesByStoreAsync(long tenantId, long storeId, bool onlyEnabled = true, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var query = context.ProductCategories
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == storeId);
|
||||||
|
if (onlyEnabled)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.IsEnabled);
|
||||||
|
}
|
||||||
|
var categories = await query
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<ProductSku>> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<ProductSku>> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -79,6 +101,21 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
return skus;
|
return skus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductSku>> GetSkusByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (productIds.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<ProductSku>();
|
||||||
|
}
|
||||||
|
var skus = await context.ProductSkus
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return skus;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -91,6 +128,21 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (productIds.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<ProductAddonGroup>();
|
||||||
|
}
|
||||||
|
var groups = await context.ProductAddonGroups
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -114,6 +166,21 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsByGroupIdsAsync(IReadOnlyCollection<long> addonGroupIds, long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (addonGroupIds.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<ProductAddonOption>();
|
||||||
|
}
|
||||||
|
var options = await context.ProductAddonOptions
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && addonGroupIds.Contains(x.AddonGroupId))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -126,6 +193,21 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (productIds.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<ProductAttributeGroup>();
|
||||||
|
}
|
||||||
|
var groups = await context.ProductAttributeGroups
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -149,6 +231,21 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsByGroupIdsAsync(IReadOnlyCollection<long> attributeGroupIds, long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (attributeGroupIds.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<ProductAttributeOption>();
|
||||||
|
}
|
||||||
|
var options = await context.ProductAttributeOptions
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && attributeGroupIds.Contains(x.AttributeGroupId))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -161,6 +258,21 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
return assets;
|
return assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (productIds.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<ProductMediaAsset>();
|
||||||
|
}
|
||||||
|
var assets = await context.ProductMediaAssets
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -173,6 +285,21 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
return rules;
|
return rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (productIds.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<ProductPricingRule>();
|
||||||
|
}
|
||||||
|
var rules = await context.ProductPricingRules
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task AddCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default)
|
public Task AddCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user