From 93bc072b8d57aa4984232e534bbdf303a745a209 Mon Sep 17 00:00:00 2001
From: MSuMshk <2039814060@qq.com>
Date: Sat, 21 Feb 2026 08:44:26 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=8A=A0=E6=96=99?=
=?UTF-8?q?=E7=AE=A1=E7=90=86=E6=8E=A5=E5=8F=A3=E4=B8=8E=E6=A8=A1=E6=9D=BF?=
=?UTF-8?q?=E8=83=BD=E5=8A=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Product/ProductAddonContracts.cs | 279 ++++++++++++++++++
.../Controllers/ProductAddonController.cs | 204 +++++++++++++
.../BindProductAddonGroupProductsCommand.cs | 25 ++
.../ChangeProductAddonGroupStatusCommand.cs | 25 ++
.../DeleteProductAddonGroupCommand.cs | 19 ++
.../Commands/SaveProductAddonGroupCommand.cs | 65 ++++
.../Commands/SaveProductAddonItemCommand.cs | 37 +++
.../Dto/ProductAddonTemplateItemDto.cs | 67 +++++
.../Dto/ProductAddonTemplateOptionDto.cs | 37 +++
...ProductAddonGroupProductsCommandHandler.cs | 71 +++++
...geProductAddonGroupStatusCommandHandler.cs | 52 ++++
.../DeleteProductAddonGroupCommandHandler.cs | 35 +++
.../GetProductAddonGroupListQueryHandler.cs | 75 +++++
.../SaveProductAddonGroupCommandHandler.cs | 241 +++++++++++++++
.../SaveProductSpecTemplateCommandHandler.cs | 13 +
.../ProductAddonTemplateDtoFactory.cs | 55 ++++
.../Products/ProductAddonTemplateMapping.cs | 45 +++
.../Products/ProductSpecTemplateMapping.cs | 7 +-
.../Queries/GetProductAddonGroupListQuery.cs | 25 ++
.../Products/Entities/ProductSpecTemplate.cs | 15 +
.../Entities/ProductSpecTemplateOption.cs | 10 +
.../Products/Enums/ProductSpecTemplateType.cs | 7 +-
.../Repositories/IProductRepository.cs | 15 +
.../App/Persistence/TakeoutAppDbContext.cs | 7 +-
.../App/Repositories/EfProductRepository.cs | 57 +++-
...ExtendProductSpecTemplateForAddonGroups.cs | 99 +++++++
.../TakeoutAppDbContextModelSnapshot.cs | 24 +-
27 files changed, 1605 insertions(+), 6 deletions(-)
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Product/ProductAddonContracts.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/ProductAddonController.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/BindProductAddonGroupProductsCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/ChangeProductAddonGroupStatusCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/DeleteProductAddonGroupCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/SaveProductAddonGroupCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/SaveProductAddonItemCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductAddonTemplateItemDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductAddonTemplateOptionDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/BindProductAddonGroupProductsCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/ChangeProductAddonGroupStatusCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/DeleteProductAddonGroupCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductAddonGroupListQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/SaveProductAddonGroupCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/ProductAddonTemplateDtoFactory.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/ProductAddonTemplateMapping.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Queries/GetProductAddonGroupListQuery.cs
create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260221090000_ExtendProductSpecTemplateForAddonGroups.cs
diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Product/ProductAddonContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Product/ProductAddonContracts.cs
new file mode 100644
index 0000000..29f574c
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Product/ProductAddonContracts.cs
@@ -0,0 +1,279 @@
+namespace TakeoutSaaS.TenantApi.Contracts.Product;
+
+///
+/// 加料组列表查询请求。
+///
+public sealed class ProductAddonGroupListRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 关键字。
+ ///
+ public string? Keyword { get; set; }
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string? Status { get; set; }
+}
+
+///
+/// 保存加料组请求。
+///
+public sealed class SaveProductAddonGroupRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 加料组 ID(编辑时传)。
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// 加料组名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 加料组描述。
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// 是否必选。
+ ///
+ public bool Required { get; set; }
+
+ ///
+ /// 最小可选数。
+ ///
+ public int MinSelect { get; set; }
+
+ ///
+ /// 最大可选数。
+ ///
+ public int MaxSelect { get; set; }
+
+ ///
+ /// 排序值。
+ ///
+ public int Sort { get; set; }
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string Status { get; set; } = "enabled";
+
+ ///
+ /// 关联商品 ID 列表。
+ ///
+ public List ProductIds { get; set; } = [];
+
+ ///
+ /// 加料项列表。
+ ///
+ public List Items { get; set; } = [];
+}
+
+///
+/// 保存加料项请求。
+///
+public sealed class SaveProductAddonItemRequest
+{
+ ///
+ /// 加料项 ID(编辑时传)。
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// 加料项名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 加价金额。
+ ///
+ public decimal Price { get; set; }
+
+ ///
+ /// 库存数量。
+ ///
+ public int Stock { get; set; } = 999;
+
+ ///
+ /// 排序值。
+ ///
+ public int Sort { get; set; }
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string Status { get; set; } = "enabled";
+}
+
+///
+/// 删除加料组请求。
+///
+public sealed class DeleteProductAddonGroupRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 加料组 ID。
+ ///
+ public string GroupId { get; set; } = string.Empty;
+}
+
+///
+/// 修改加料组状态请求。
+///
+public sealed class ChangeProductAddonGroupStatusRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 加料组 ID。
+ ///
+ public string GroupId { get; set; } = string.Empty;
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string Status { get; set; } = "enabled";
+}
+
+///
+/// 绑定加料组商品请求。
+///
+public sealed class BindProductAddonGroupProductsRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 加料组 ID。
+ ///
+ public string GroupId { get; set; } = string.Empty;
+
+ ///
+ /// 商品 ID 列表。
+ ///
+ public List ProductIds { get; set; } = [];
+}
+
+///
+/// 加料项响应。
+///
+public sealed class ProductAddonItemResponse
+{
+ ///
+ /// 加料项 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 加料项名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 加价金额。
+ ///
+ public decimal Price { get; set; }
+
+ ///
+ /// 库存数量。
+ ///
+ public int Stock { get; set; }
+
+ ///
+ /// 排序值。
+ ///
+ public int Sort { get; set; }
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string Status { get; set; } = "enabled";
+}
+
+///
+/// 加料组响应。
+///
+public sealed class ProductAddonGroupItemResponse
+{
+ ///
+ /// 加料组 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 加料组名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 描述。
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// 是否必选。
+ ///
+ public bool Required { get; set; }
+
+ ///
+ /// 最小可选数。
+ ///
+ public int MinSelect { get; set; }
+
+ ///
+ /// 最大可选数。
+ ///
+ public int MaxSelect { get; set; }
+
+ ///
+ /// 排序值。
+ ///
+ public int Sort { get; set; }
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string Status { get; set; } = "enabled";
+
+ ///
+ /// 关联商品数量。
+ ///
+ public int ProductCount { get; set; }
+
+ ///
+ /// 关联商品 ID 列表。
+ ///
+ public List ProductIds { get; set; } = [];
+
+ ///
+ /// 加料项列表。
+ ///
+ public List Items { get; set; } = [];
+
+ ///
+ /// 更新时间。
+ ///
+ public string UpdatedAt { get; set; } = string.Empty;
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/ProductAddonController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/ProductAddonController.cs
new file mode 100644
index 0000000..156cfb3
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/ProductAddonController.cs
@@ -0,0 +1,204 @@
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using TakeoutSaaS.Application.App.Products.Commands;
+using TakeoutSaaS.Application.App.Products.Dto;
+using TakeoutSaaS.Application.App.Products.Queries;
+using TakeoutSaaS.Application.App.Stores.Services;
+using TakeoutSaaS.Infrastructure.App.Persistence;
+using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Web.Api;
+using TakeoutSaaS.TenantApi.Contracts.Product;
+
+namespace TakeoutSaaS.TenantApi.Controllers;
+
+///
+/// 租户端加料管理。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/tenant/v{version:apiVersion}/product")]
+public sealed class ProductAddonController(
+ IMediator mediator,
+ TakeoutAppDbContext dbContext,
+ StoreContextService storeContextService) : BaseApiController
+{
+ ///
+ /// 加料组列表。
+ ///
+ [HttpGet("addon/group/list")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> GetAddonGroupList(
+ [FromQuery] ProductAddonGroupListRequest request,
+ CancellationToken cancellationToken)
+ {
+ // 1. 校验门店访问权限。
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ // 2. 查询并返回列表。
+ var result = await mediator.Send(new GetProductAddonGroupListQuery
+ {
+ StoreId = storeId,
+ Keyword = request.Keyword,
+ Status = request.Status
+ }, cancellationToken);
+
+ return ApiResponse>.Ok(result.Select(MapAddonGroupItem).ToList());
+ }
+
+ ///
+ /// 保存加料组。
+ ///
+ [HttpPost("addon/group/save")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> SaveAddonGroup(
+ [FromBody] SaveProductAddonGroupRequest request,
+ CancellationToken cancellationToken)
+ {
+ // 1. 校验门店访问权限。
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ // 2. 提交保存命令。
+ var result = await mediator.Send(new SaveProductAddonGroupCommand
+ {
+ StoreId = storeId,
+ GroupId = StoreApiHelpers.ParseSnowflakeOrNull(request.Id),
+ Name = request.Name,
+ Description = request.Description,
+ Required = request.Required,
+ MinSelect = request.MinSelect,
+ MaxSelect = request.MaxSelect,
+ Sort = request.Sort,
+ Status = request.Status,
+ ProductIds = StoreApiHelpers.ParseSnowflakeList(request.ProductIds),
+ Items = (request.Items ?? [])
+ .Select(item => new SaveProductAddonItemCommand
+ {
+ Id = StoreApiHelpers.ParseSnowflakeOrNull(item.Id),
+ Name = item.Name,
+ Price = item.Price,
+ Stock = item.Stock,
+ Sort = item.Sort,
+ Status = item.Status
+ })
+ .ToList()
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapAddonGroupItem(result));
+ }
+
+ ///
+ /// 删除加料组。
+ ///
+ [HttpPost("addon/group/delete")]
+ [ProducesResponseType(typeof(ApiResponse