feat: 新增加料管理接口与模板能力
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 43s

This commit is contained in:
2026-02-21 08:44:26 +08:00
parent 848778b8b5
commit 93bc072b8d
27 changed files with 1605 additions and 6 deletions

View File

@@ -1185,12 +1185,15 @@ public sealed class TakeoutAppDbContext(
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.Description).HasMaxLength(256).IsRequired();
builder.Property(x => x.TemplateType).HasConversion<int>();
builder.Property(x => x.SelectionType).HasConversion<int>();
builder.Property(x => x.MinSelect).IsRequired();
builder.Property(x => x.MaxSelect).IsRequired();
builder.Property(x => x.SortOrder).IsRequired();
builder.Property(x => x.IsEnabled).IsRequired();
builder.Property(x => x.IsRequired).IsRequired();
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name }).IsUnique();
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.TemplateType, x.Name }).IsUnique();
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.TemplateType, x.IsEnabled });
}
@@ -1201,6 +1204,8 @@ public sealed class TakeoutAppDbContext(
builder.Property(x => x.TemplateId).IsRequired();
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.ExtraPrice).HasPrecision(18, 2);
builder.Property(x => x.Stock).IsRequired();
builder.Property(x => x.IsEnabled).IsRequired();
builder.Property(x => x.SortOrder).IsRequired();
builder.HasIndex(x => new { x.TenantId, x.TemplateId, x.Name }).IsUnique();
}

View File

@@ -138,7 +138,24 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
{
return await context.ProductSpecTemplates
.AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
.Where(x =>
x.TenantId == tenantId &&
x.StoreId == storeId &&
x.TemplateType != ProductSpecTemplateType.Addon)
.OrderBy(x => x.SortOrder)
.ThenBy(x => x.Id)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<ProductSpecTemplate>> GetAddonTemplatesByStoreAsync(long tenantId, long storeId, CancellationToken cancellationToken = default)
{
return await context.ProductSpecTemplates
.AsNoTracking()
.Where(x =>
x.TenantId == tenantId &&
x.StoreId == storeId &&
x.TemplateType == ProductSpecTemplateType.Addon)
.OrderBy(x => x.SortOrder)
.ThenBy(x => x.Id)
.ToListAsync(cancellationToken);
@@ -148,7 +165,21 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
public Task<ProductSpecTemplate?> FindSpecTemplateByIdAsync(long templateId, long tenantId, CancellationToken cancellationToken = default)
{
return context.ProductSpecTemplates
.Where(x => x.TenantId == tenantId && x.Id == templateId)
.Where(x =>
x.TenantId == tenantId &&
x.Id == templateId &&
x.TemplateType != ProductSpecTemplateType.Addon)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task<ProductSpecTemplate?> FindAddonTemplateByIdAsync(long templateId, long tenantId, CancellationToken cancellationToken = default)
{
return context.ProductSpecTemplates
.Where(x =>
x.TenantId == tenantId &&
x.Id == templateId &&
x.TemplateType == ProductSpecTemplateType.Addon)
.FirstOrDefaultAsync(cancellationToken);
}
@@ -162,6 +193,28 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
.Where(x =>
x.TenantId == tenantId &&
x.StoreId == storeId &&
x.TemplateType != ProductSpecTemplateType.Addon &&
x.Name.ToLower() == normalizedLower);
if (excludeTemplateId.HasValue)
{
query = query.Where(x => x.Id != excludeTemplateId.Value);
}
return query.AnyAsync(cancellationToken);
}
/// <inheritdoc />
public Task<bool> ExistsAddonTemplateNameAsync(long tenantId, long storeId, string name, long? excludeTemplateId = null, CancellationToken cancellationToken = default)
{
var normalizedName = (name ?? string.Empty).Trim();
var normalizedLower = normalizedName.ToLowerInvariant();
var query = context.ProductSpecTemplates
.AsNoTracking()
.Where(x =>
x.TenantId == tenantId &&
x.StoreId == storeId &&
x.TemplateType == ProductSpecTemplateType.Addon &&
x.Name.ToLower() == normalizedLower);
if (excludeTemplateId.HasValue)

View File

@@ -0,0 +1,99 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TakeoutSaaS.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class ExtendProductSpecTemplateForAddonGroups : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_product_spec_templates_TenantId_StoreId_Name",
table: "product_spec_templates");
migrationBuilder.AddColumn<string>(
name: "Description",
table: "product_spec_templates",
type: "character varying(256)",
maxLength: 256,
nullable: false,
defaultValue: "",
comment: "模板描述。");
migrationBuilder.AddColumn<int>(
name: "MaxSelect",
table: "product_spec_templates",
type: "integer",
nullable: false,
defaultValue: 1,
comment: "最大可选数。");
migrationBuilder.AddColumn<int>(
name: "MinSelect",
table: "product_spec_templates",
type: "integer",
nullable: false,
defaultValue: 0,
comment: "最小可选数。");
migrationBuilder.AddColumn<bool>(
name: "IsEnabled",
table: "product_spec_template_options",
type: "boolean",
nullable: false,
defaultValue: true,
comment: "是否启用。");
migrationBuilder.AddColumn<int>(
name: "Stock",
table: "product_spec_template_options",
type: "integer",
nullable: false,
defaultValue: 999,
comment: "库存数量。");
migrationBuilder.CreateIndex(
name: "IX_product_spec_templates_TenantId_StoreId_TemplateType_Name",
table: "product_spec_templates",
columns: new[] { "TenantId", "StoreId", "TemplateType", "Name" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_product_spec_templates_TenantId_StoreId_TemplateType_Name",
table: "product_spec_templates");
migrationBuilder.DropColumn(
name: "Description",
table: "product_spec_templates");
migrationBuilder.DropColumn(
name: "MaxSelect",
table: "product_spec_templates");
migrationBuilder.DropColumn(
name: "MinSelect",
table: "product_spec_templates");
migrationBuilder.DropColumn(
name: "IsEnabled",
table: "product_spec_template_options");
migrationBuilder.DropColumn(
name: "Stock",
table: "product_spec_template_options");
migrationBuilder.CreateIndex(
name: "IX_product_spec_templates_TenantId_StoreId_Name",
table: "product_spec_templates",
columns: new[] { "TenantId", "StoreId", "Name" },
unique: true);
}
}
}

View File

@@ -4689,6 +4689,12 @@ namespace TakeoutSaaS.Infrastructure.Migrations
.HasColumnType("bigint")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("模板描述。");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasComment("是否启用。");
@@ -4697,12 +4703,20 @@ namespace TakeoutSaaS.Infrastructure.Migrations
.HasColumnType("boolean")
.HasComment("是否必选。");
b.Property<int>("MaxSelect")
.HasColumnType("integer")
.HasComment("最大可选数。");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("模板名称。");
b.Property<int>("MinSelect")
.HasColumnType("integer")
.HasComment("最小可选数。");
b.Property<int>("SelectionType")
.HasColumnType("integer")
.HasComment("选择方式。");
@@ -4733,7 +4747,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
b.HasKey("Id");
b.HasIndex("TenantId", "StoreId", "Name")
b.HasIndex("TenantId", "StoreId", "TemplateType", "Name")
.IsUnique();
b.HasIndex("TenantId", "StoreId", "TemplateType", "IsEnabled");
@@ -4774,6 +4788,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
.HasColumnType("numeric(18,2)")
.HasComment("附加价格。");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasComment("是否启用。");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
@@ -4784,6 +4802,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
.HasColumnType("integer")
.HasComment("排序值。");
b.Property<int>("Stock")
.HasColumnType("integer")
.HasComment("库存数量。");
b.Property<long>("TemplateId")
.HasColumnType("bigint")
.HasComment("模板 ID。");