From 392d9f03a146f9950763090f03593a314c0c9408 Mon Sep 17 00:00:00 2001
From: MSuMshk <2039814060@qq.com>
Date: Fri, 20 Feb 2026 20:08:34 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=A7=84=E6=A0=BC?=
=?UTF-8?q?=E5=81=9A=E6=B3=95=E6=A8=A1=E6=9D=BF=E7=AE=A1=E7=90=86=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3=E4=B8=8E=E6=95=B0=E6=8D=AE=E6=A8=A1=E5=9E=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Contracts/Product/ProductSpecContracts.cs | 254 +
.../Controllers/ProductSpecController.cs | 182 +
.../ChangeProductSpecTemplateStatusCommand.cs | 25 +
.../CopyProductSpecTemplateCommand.cs | 25 +
.../DeleteProductSpecTemplateCommand.cs | 19 +
.../SaveProductSpecTemplateCommand.cs | 60 +
.../SaveProductSpecTemplateValueCommand.cs | 27 +
.../Dto/ProductSpecTemplateItemDto.cs | 62 +
.../Dto/ProductSpecTemplateOptionDto.cs | 27 +
...ProductSpecTemplateStatusCommandHandler.cs | 52 +
.../CopyProductSpecTemplateCommandHandler.cs | 145 +
...DeleteProductSpecTemplateCommandHandler.cs | 35 +
.../GetProductSpecTemplateListQueryHandler.cs | 74 +
.../SaveProductSpecTemplateCommandHandler.cs | 197 +
.../Products/ProductSpecTemplateDtoFactory.cs | 52 +
.../Products/ProductSpecTemplateMapping.cs | 95 +
.../GetProductSpecTemplateListQuery.cs | 30 +
.../Products/Entities/ProductSpecTemplate.cs | 45 +
.../Entities/ProductSpecTemplateOption.cs | 29 +
.../Entities/ProductSpecTemplateProduct.cs | 24 +
.../Products/Enums/ProductSpecTemplateType.cs | 17 +
.../Repositories/IProductRepository.cs | 65 +
.../App/Persistence/TakeoutAppDbContext.cs | 52 +
.../App/Repositories/EfProductRepository.cs | 153 +
...112948_AddProductSpecTemplates.Designer.cs | 8075 +++++++++++++++++
.../20260220112948_AddProductSpecTemplates.cs | 131 +
.../TakeoutAppDbContextModelSnapshot.cs | 209 +
27 files changed, 10161 insertions(+)
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Product/ProductSpecContracts.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/ProductSpecController.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/ChangeProductSpecTemplateStatusCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/CopyProductSpecTemplateCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/DeleteProductSpecTemplateCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/SaveProductSpecTemplateCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/SaveProductSpecTemplateValueCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductSpecTemplateItemDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductSpecTemplateOptionDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/ChangeProductSpecTemplateStatusCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/CopyProductSpecTemplateCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/DeleteProductSpecTemplateCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductSpecTemplateListQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/SaveProductSpecTemplateCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/ProductSpecTemplateDtoFactory.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/ProductSpecTemplateMapping.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Queries/GetProductSpecTemplateListQuery.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSpecTemplate.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSpecTemplateOption.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSpecTemplateProduct.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Enums/ProductSpecTemplateType.cs
create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260220112948_AddProductSpecTemplates.Designer.cs
create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260220112948_AddProductSpecTemplates.cs
diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Product/ProductSpecContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Product/ProductSpecContracts.cs
new file mode 100644
index 0000000..3e40830
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Product/ProductSpecContracts.cs
@@ -0,0 +1,254 @@
+namespace TakeoutSaaS.TenantApi.Contracts.Product;
+
+///
+/// 规格做法列表查询请求。
+///
+public sealed class ProductSpecListRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 关键字。
+ ///
+ public string? Keyword { get; set; }
+
+ ///
+ /// 模板类型(spec/method)。
+ ///
+ public string? Type { get; set; }
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string? Status { get; set; }
+}
+
+///
+/// 保存规格做法模板请求。
+///
+public sealed class SaveProductSpecRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 模板 ID(编辑时传)。
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// 模板名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 模板类型(spec/method)。
+ ///
+ public string Type { get; set; } = "spec";
+
+ ///
+ /// 选择方式(single/multi)。
+ ///
+ public string SelectionType { get; set; } = "single";
+
+ ///
+ /// 是否必选。
+ ///
+ public bool IsRequired { get; set; } = true;
+
+ ///
+ /// 排序值。
+ ///
+ public int Sort { get; set; }
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string Status { get; set; } = "enabled";
+
+ ///
+ /// 关联商品 ID 列表。
+ ///
+ public List ProductIds { get; set; } = [];
+
+ ///
+ /// 模板选项。
+ ///
+ public List Values { get; set; } = [];
+}
+
+///
+/// 保存规格做法模板选项请求。
+///
+public sealed class SaveProductSpecValueRequest
+{
+ ///
+ /// 选项 ID(编辑时传)。
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// 选项名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 附加价格。
+ ///
+ public decimal ExtraPrice { get; set; }
+
+ ///
+ /// 排序值。
+ ///
+ public int Sort { get; set; }
+}
+
+///
+/// 删除规格做法模板请求。
+///
+public sealed class DeleteProductSpecRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 模板 ID。
+ ///
+ public string SpecId { get; set; } = string.Empty;
+}
+
+///
+/// 规格做法模板状态变更请求。
+///
+public sealed class ChangeProductSpecStatusRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 模板 ID。
+ ///
+ public string SpecId { get; set; } = string.Empty;
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string Status { get; set; } = "enabled";
+}
+
+///
+/// 复制规格做法模板请求。
+///
+public sealed class CopyProductSpecRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 模板 ID。
+ ///
+ public string SpecId { get; set; } = string.Empty;
+
+ ///
+ /// 新模板名称(可选)。
+ ///
+ public string? NewName { get; set; }
+}
+
+///
+/// 规格做法模板选项响应。
+///
+public sealed class ProductSpecValueResponse
+{
+ ///
+ /// 选项 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 选项名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 附加价格。
+ ///
+ public decimal ExtraPrice { get; set; }
+
+ ///
+ /// 排序值。
+ ///
+ public int Sort { get; set; }
+}
+
+///
+/// 规格做法模板列表项响应。
+///
+public sealed class ProductSpecItemResponse
+{
+ ///
+ /// 模板 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 模板名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 模板类型(spec/method)。
+ ///
+ public string Type { get; set; } = "spec";
+
+ ///
+ /// 选择方式(single/multi)。
+ ///
+ public string SelectionType { get; set; } = "single";
+
+ ///
+ /// 是否必选。
+ ///
+ public bool IsRequired { 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 Values { get; set; } = [];
+
+ ///
+ /// 更新时间。
+ ///
+ public string UpdatedAt { get; set; } = string.Empty;
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/ProductSpecController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/ProductSpecController.cs
new file mode 100644
index 0000000..ae7d669
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/ProductSpecController.cs
@@ -0,0 +1,182 @@
+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 ProductSpecController(
+ IMediator mediator,
+ TakeoutAppDbContext dbContext,
+ StoreContextService storeContextService) : BaseApiController
+{
+ ///
+ /// 规格做法模板列表。
+ ///
+ [HttpGet("spec/list")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> GetSpecList(
+ [FromQuery] ProductSpecListRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var result = await mediator.Send(new GetProductSpecTemplateListQuery
+ {
+ StoreId = storeId,
+ Keyword = request.Keyword,
+ Type = request.Type,
+ Status = request.Status
+ }, cancellationToken);
+
+ return ApiResponse>.Ok(result.Select(MapSpecItem).ToList());
+ }
+
+ ///
+ /// 保存规格做法模板。
+ ///
+ [HttpPost("spec/save")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> SaveSpec(
+ [FromBody] SaveProductSpecRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var result = await mediator.Send(new SaveProductSpecTemplateCommand
+ {
+ StoreId = storeId,
+ SpecId = StoreApiHelpers.ParseSnowflakeOrNull(request.Id),
+ Name = request.Name,
+ Type = request.Type,
+ SelectionType = request.SelectionType,
+ IsRequired = request.IsRequired,
+ Sort = request.Sort,
+ Status = request.Status,
+ ProductIds = StoreApiHelpers.ParseSnowflakeList(request.ProductIds),
+ Values = (request.Values ?? [])
+ .Select(item => new SaveProductSpecTemplateValueCommand
+ {
+ Id = StoreApiHelpers.ParseSnowflakeOrNull(item.Id),
+ Name = item.Name,
+ ExtraPrice = item.ExtraPrice,
+ Sort = item.Sort
+ })
+ .ToList()
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapSpecItem(result));
+ }
+
+ ///
+ /// 删除规格做法模板。
+ ///
+ [HttpPost("spec/delete")]
+ [ProducesResponseType(typeof(ApiResponse