feat: 完成营销中心优惠券后端模块
All checks were successful
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Successful in 1m54s

This commit is contained in:
2026-02-28 11:14:55 +08:00
parent 04e76cd519
commit dda3f96d28
26 changed files with 11107 additions and 0 deletions

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using TakeoutSaaS.Application.App.Stores.Services;
using TakeoutSaaS.Domain.Coupons.Repositories;
using TakeoutSaaS.Domain.Deliveries.Repositories;
using TakeoutSaaS.Domain.Inventory.Repositories;
using TakeoutSaaS.Domain.Merchants.Repositories;
@@ -44,6 +45,7 @@ public static class AppServiceCollectionExtensions
services.AddScoped<IMerchantCategoryRepository, EfMerchantCategoryRepository>();
services.AddScoped<IStoreRepository, EfStoreRepository>();
services.AddScoped<IProductRepository, EfProductRepository>();
services.AddScoped<ICouponRepository, EfCouponRepository>();
services.AddScoped<IOrderRepository, EfOrderRepository>();
services.AddScoped<IPaymentRepository, EfPaymentRepository>();
services.AddScoped<IDeliveryRepository, EfDeliveryRepository>();

View File

@@ -1590,6 +1590,7 @@ public sealed class TakeoutAppDbContext(
builder.Property(x => x.CouponType).HasConversion<int>();
builder.Property(x => x.Description).HasMaxLength(512);
builder.Property(x => x.TotalQuantity);
builder.Property(x => x.PerUserLimit);
builder.Property(x => x.StoreScopeJson).HasColumnType("text");
builder.Property(x => x.ProductScopeJson).HasColumnType("text");
builder.Property(x => x.ChannelsJson).HasColumnType("text");

View File

@@ -0,0 +1,89 @@
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Coupons.Entities;
using TakeoutSaaS.Domain.Coupons.Enums;
using TakeoutSaaS.Domain.Coupons.Repositories;
using TakeoutSaaS.Infrastructure.App.Persistence;
namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary>
/// 优惠券聚合的 EF Core 仓储实现。
/// </summary>
public sealed class EfCouponRepository(TakeoutAppDbContext context) : ICouponRepository
{
/// <inheritdoc />
public async Task<IReadOnlyList<CouponTemplate>> GetTemplatesAsync(long tenantId, CancellationToken cancellationToken = default)
{
return await context.CouponTemplates
.AsNoTracking()
.Where(x => x.TenantId == tenantId)
.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt)
.ThenByDescending(x => x.Id)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public Task<CouponTemplate?> FindTemplateByIdAsync(long templateId, long tenantId, CancellationToken cancellationToken = default)
{
return context.CouponTemplates
.Where(x => x.TenantId == tenantId && x.Id == templateId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task AddTemplateAsync(CouponTemplate template, CancellationToken cancellationToken = default)
{
return context.CouponTemplates.AddAsync(template, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateTemplateAsync(CouponTemplate template, CancellationToken cancellationToken = default)
{
context.CouponTemplates.Update(template);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task DeleteTemplateAsync(CouponTemplate template, CancellationToken cancellationToken = default)
{
context.CouponTemplates.Remove(template);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<int> CountIssuedCouponsByTemplateIdAsync(long tenantId, long templateId, CancellationToken cancellationToken = default)
{
return context.Coupons
.AsNoTracking()
.Where(x => x.TenantId == tenantId && x.CouponTemplateId == templateId)
.CountAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<Dictionary<long, int>> CountRedeemedCouponsByTemplateIdsAsync(
long tenantId,
IReadOnlyCollection<long> templateIds,
CancellationToken cancellationToken = default)
{
if (templateIds.Count == 0)
{
return [];
}
return await context.Coupons
.AsNoTracking()
.Where(x =>
x.TenantId == tenantId &&
templateIds.Contains(x.CouponTemplateId) &&
x.Status == CouponStatus.Redeemed)
.GroupBy(x => x.CouponTemplateId)
.Select(group => new { CouponTemplateId = group.Key, Count = group.Count() })
.ToDictionaryAsync(item => item.CouponTemplateId, item => item.Count, cancellationToken);
}
/// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return context.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TakeoutSaaS.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddCouponTemplatePerUserLimit : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PerUserLimit",
table: "coupon_templates",
type: "integer",
nullable: true,
comment: "每位用户可领取上限。");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PerUserLimit",
table: "coupon_templates");
}
}
}

View File

@@ -548,6 +548,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
.HasColumnType("character varying(128)")
.HasComment("模板名称。");
b.Property<int?>("PerUserLimit")
.HasColumnType("integer")
.HasComment("每位用户可领取上限。");
b.Property<string>("ProductScopeJson")
.HasColumnType("text")
.HasComment("适用品类或商品范围JSON。");