From 5a6da9be0c2c73880084c4c9b9b5ac36e3454d1e Mon Sep 17 00:00:00 2001
From: MSuMshk <2039814060@qq.com>
Date: Sat, 28 Feb 2026 15:46:21 +0800
Subject: [PATCH] feat(marketing): add full reduction campaign api module
---
.../Marketing/FullReductionContracts.cs | 685 ++++++++++++++++++
.../MarketingFullReductionController.cs | 477 ++++++++++++
...hangeFullReductionCampaignStatusCommand.cs | 25 +
.../DeleteFullReductionCampaignCommand.cs | 19 +
.../SaveFullReductionCampaignCommand.cs | 90 +++
.../Dto/FullReductionDetailDto.cs | 47 ++
.../Dto/FullReductionGiftRuleDto.cs | 32 +
.../Dto/FullReductionListItemDto.cs | 47 ++
.../Dto/FullReductionListResultDto.cs | 32 +
.../Dto/FullReductionMetricsDto.cs | 47 ++
.../Dto/FullReductionRulesDto.cs | 62 ++
.../Dto/FullReductionScopeRuleDto.cs | 22 +
.../Dto/FullReductionSecondHalfRuleDto.cs | 17 +
.../Dto/FullReductionStatsDto.cs | 27 +
.../Dto/FullReductionTierRuleDto.cs | 17 +
.../FullReduction/FullReductionDtoFactory.cs | 108 +++
.../FullReduction/FullReductionMapping.cs | 606 ++++++++++++++++
...llReductionCampaignStatusCommandHandler.cs | 54 ++
...leteFullReductionCampaignCommandHandler.cs | 41 ++
...FullReductionCampaignDetailQueryHandler.cs | 43 ++
...etFullReductionCampaignListQueryHandler.cs | 122 ++++
...SaveFullReductionCampaignCommandHandler.cs | 122 ++++
.../GetFullReductionCampaignDetailQuery.cs | 20 +
.../GetFullReductionCampaignListQuery.cs | 40 +
.../IPromotionCampaignRepository.cs | 47 ++
.../AppServiceCollectionExtensions.cs | 2 +
.../EfPromotionCampaignRepository.cs | 65 ++
27 files changed, 2916 insertions(+)
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/FullReductionContracts.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingFullReductionController.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Commands/ChangeFullReductionCampaignStatusCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Commands/DeleteFullReductionCampaignCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Commands/SaveFullReductionCampaignCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Dto/FullReductionDetailDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Dto/FullReductionGiftRuleDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Dto/FullReductionListItemDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Dto/FullReductionListResultDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Dto/FullReductionMetricsDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Dto/FullReductionRulesDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Dto/FullReductionScopeRuleDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Dto/FullReductionSecondHalfRuleDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Dto/FullReductionStatsDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Dto/FullReductionTierRuleDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/FullReductionDtoFactory.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/FullReductionMapping.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Handlers/ChangeFullReductionCampaignStatusCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Handlers/DeleteFullReductionCampaignCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Handlers/GetFullReductionCampaignDetailQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Handlers/GetFullReductionCampaignListQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Handlers/SaveFullReductionCampaignCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Queries/GetFullReductionCampaignDetailQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FullReduction/Queries/GetFullReductionCampaignListQuery.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Repositories/IPromotionCampaignRepository.cs
create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPromotionCampaignRepository.cs
diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/FullReductionContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/FullReductionContracts.cs
new file mode 100644
index 0000000..3fe3c1f
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/FullReductionContracts.cs
@@ -0,0 +1,685 @@
+namespace TakeoutSaaS.TenantApi.Contracts.Marketing;
+
+///
+/// 满减活动列表请求。
+///
+public sealed class FullReductionListRequest
+{
+ ///
+ /// 门店 ID(可空;空表示全部门店)。
+ ///
+ public string? StoreId { get; set; }
+
+ ///
+ /// 关键字。
+ ///
+ public string? Keyword { get; set; }
+
+ ///
+ /// 活动类型筛选(reduce/gift/second_half)。
+ ///
+ public string? ActivityType { get; set; }
+
+ ///
+ /// 状态筛选(ongoing/upcoming/ended)。
+ ///
+ public string? Status { get; set; }
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; } = 1;
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; } = 4;
+}
+
+///
+/// 满减活动详情请求。
+///
+public sealed class FullReductionDetailRequest
+{
+ ///
+ /// 操作上下文门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 活动 ID。
+ ///
+ public string ActivityId { get; set; } = string.Empty;
+}
+
+///
+/// 保存满减活动请求。
+///
+public sealed class SaveFullReductionRequest
+{
+ ///
+ /// 操作上下文门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 活动 ID(编辑时传)。
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// 活动名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 活动类型(reduce/gift/second_half)。
+ ///
+ public string ActivityType { get; set; } = "reduce";
+
+ ///
+ /// 满减阶梯规则。
+ ///
+ public List ReduceTiers { get; set; } = [];
+
+ ///
+ /// 满赠规则。
+ ///
+ public FullReductionGiftRuleRequest? GiftRule { get; set; }
+
+ ///
+ /// 第二份半价规则。
+ ///
+ public FullReductionSecondHalfRuleRequest? SecondHalfRule { get; set; }
+
+ ///
+ /// 活动开始日期(yyyy-MM-dd)。
+ ///
+ public string StartDate { get; set; } = string.Empty;
+
+ ///
+ /// 活动结束日期(yyyy-MM-dd)。
+ ///
+ public string EndDate { get; set; } = string.Empty;
+
+ ///
+ /// 适用渠道(delivery/pickup/dine_in)。
+ ///
+ public List? Channels { get; set; }
+
+ ///
+ /// 门店范围模式(all/stores)。
+ ///
+ public string StoreScopeMode { get; set; } = "all";
+
+ ///
+ /// 门店范围 ID 集合(stores 模式必传)。
+ ///
+ public List? StoreIds { get; set; }
+
+ ///
+ /// 选品基准门店 ID。
+ ///
+ public string? ScopeStoreId { get; set; }
+
+ ///
+ /// 是否可叠加优惠券。
+ ///
+ public bool StackWithCoupon { get; set; }
+
+ ///
+ /// 活动说明。
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// 活动指标(用于列表展示)。
+ ///
+ public FullReductionMetricsRequest? Metrics { get; set; }
+}
+
+///
+/// 修改活动状态请求。
+///
+public sealed class ChangeFullReductionStatusRequest
+{
+ ///
+ /// 操作上下文门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 活动 ID。
+ ///
+ public string ActivityId { get; set; } = string.Empty;
+
+ ///
+ /// 状态(active/completed)。
+ ///
+ public string Status { get; set; } = "completed";
+}
+
+///
+/// 删除满减活动请求。
+///
+public sealed class DeleteFullReductionRequest
+{
+ ///
+ /// 操作上下文门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 活动 ID。
+ ///
+ public string ActivityId { get; set; } = string.Empty;
+}
+
+///
+/// 满减活动列表结果。
+///
+public sealed class FullReductionListResultResponse
+{
+ ///
+ /// 列表。
+ ///
+ public List Items { get; set; } = [];
+
+ ///
+ /// 总条数。
+ ///
+ public int Total { get; set; }
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; }
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; }
+
+ ///
+ /// 统计信息。
+ ///
+ public FullReductionStatsResponse Stats { get; set; } = new();
+}
+
+///
+/// 满减活动列表项。
+///
+public sealed class FullReductionListItemResponse
+{
+ ///
+ /// 活动 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 活动名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 活动类型(reduce/gift/second_half)。
+ ///
+ public string ActivityType { get; set; } = "reduce";
+
+ ///
+ /// 活动开始日期(yyyy-MM-dd)。
+ ///
+ public string StartDate { get; set; } = string.Empty;
+
+ ///
+ /// 活动结束日期(yyyy-MM-dd)。
+ ///
+ public string EndDate { get; set; } = string.Empty;
+
+ ///
+ /// 展示状态(ongoing/upcoming/ended)。
+ ///
+ public string DisplayStatus { get; set; } = "ongoing";
+
+ ///
+ /// 是否弱化展示。
+ ///
+ public bool IsDimmed { get; set; }
+
+ ///
+ /// 满减阶梯规则。
+ ///
+ public List ReduceTiers { get; set; } = [];
+
+ ///
+ /// 满赠规则。
+ ///
+ public FullReductionGiftRuleResponse? GiftRule { get; set; }
+
+ ///
+ /// 第二份半价规则。
+ ///
+ public FullReductionSecondHalfRuleResponse? SecondHalfRule { get; set; }
+
+ ///
+ /// 适用渠道。
+ ///
+ public List Channels { get; set; } = [];
+
+ ///
+ /// 门店范围模式(all/stores)。
+ ///
+ public string StoreScopeMode { get; set; } = "all";
+
+ ///
+ /// 门店范围 ID。
+ ///
+ public List StoreIds { get; set; } = [];
+
+ ///
+ /// 选品基准门店 ID。
+ ///
+ public string ScopeStoreId { get; set; } = string.Empty;
+
+ ///
+ /// 是否可叠加优惠券。
+ ///
+ public bool StackWithCoupon { get; set; }
+
+ ///
+ /// 活动说明。
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// 活动指标。
+ ///
+ public FullReductionMetricsResponse Metrics { get; set; } = new();
+
+ ///
+ /// 更新时间(yyyy-MM-dd HH:mm:ss)。
+ ///
+ public string UpdatedAt { get; set; } = string.Empty;
+}
+
+///
+/// 满减活动详情。
+///
+public sealed class FullReductionDetailResponse
+{
+ ///
+ /// 活动 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 活动名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 活动类型(reduce/gift/second_half)。
+ ///
+ public string ActivityType { get; set; } = "reduce";
+
+ ///
+ /// 满减阶梯规则。
+ ///
+ public List ReduceTiers { get; set; } = [];
+
+ ///
+ /// 满赠规则。
+ ///
+ public FullReductionGiftRuleResponse? GiftRule { get; set; }
+
+ ///
+ /// 第二份半价规则。
+ ///
+ public FullReductionSecondHalfRuleResponse? SecondHalfRule { get; set; }
+
+ ///
+ /// 活动开始日期(yyyy-MM-dd)。
+ ///
+ public string StartDate { get; set; } = string.Empty;
+
+ ///
+ /// 活动结束日期(yyyy-MM-dd)。
+ ///
+ public string EndDate { get; set; } = string.Empty;
+
+ ///
+ /// 展示状态(ongoing/upcoming/ended)。
+ ///
+ public string DisplayStatus { get; set; } = "ongoing";
+
+ ///
+ /// 编辑状态(active/completed)。
+ ///
+ public string Status { get; set; } = "active";
+
+ ///
+ /// 适用渠道。
+ ///
+ public List Channels { get; set; } = [];
+
+ ///
+ /// 门店范围模式(all/stores)。
+ ///
+ public string StoreScopeMode { get; set; } = "all";
+
+ ///
+ /// 门店范围 ID。
+ ///
+ public List StoreIds { get; set; } = [];
+
+ ///
+ /// 选品基准门店 ID。
+ ///
+ public string ScopeStoreId { get; set; } = string.Empty;
+
+ ///
+ /// 是否可叠加优惠券。
+ ///
+ public bool StackWithCoupon { get; set; }
+
+ ///
+ /// 活动说明。
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// 活动指标。
+ ///
+ public FullReductionMetricsResponse Metrics { get; set; } = new();
+
+ ///
+ /// 更新时间(yyyy-MM-dd HH:mm:ss)。
+ ///
+ public string UpdatedAt { get; set; } = string.Empty;
+}
+
+///
+/// 满减活动统计。
+///
+public sealed class FullReductionStatsResponse
+{
+ ///
+ /// 活动总数。
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// 进行中数量。
+ ///
+ public int OngoingCount { get; set; }
+
+ ///
+ /// 本月带动销售额。
+ ///
+ public decimal MonthlyDrivenSalesAmount { get; set; }
+
+ ///
+ /// 平均客单价提升。
+ ///
+ public decimal AverageTicketIncrease { get; set; }
+}
+
+///
+/// 满减阶梯规则请求。
+///
+public sealed class FullReductionTierRuleRequest
+{
+ ///
+ /// 满足金额。
+ ///
+ public decimal MeetAmount { get; set; }
+
+ ///
+ /// 减免金额。
+ ///
+ public decimal ReduceAmount { get; set; }
+}
+
+///
+/// 满减阶梯规则响应。
+///
+public sealed class FullReductionTierRuleResponse
+{
+ ///
+ /// 满足金额。
+ ///
+ public decimal MeetAmount { get; set; }
+
+ ///
+ /// 减免金额。
+ ///
+ public decimal ReduceAmount { get; set; }
+}
+
+///
+/// 满赠规则请求。
+///
+public sealed class FullReductionGiftRuleRequest
+{
+ ///
+ /// 购买数量门槛。
+ ///
+ public int BuyQuantity { get; set; }
+
+ ///
+ /// 赠送数量。
+ ///
+ public int GiftQuantity { get; set; }
+
+ ///
+ /// 赠品范围类型(same_lowest/specified)。
+ ///
+ public string GiftScopeType { get; set; } = "same_lowest";
+
+ ///
+ /// 适用商品范围。
+ ///
+ public FullReductionScopeRuleRequest ApplicableScope { get; set; } = new();
+
+ ///
+ /// 指定赠品范围。
+ ///
+ public FullReductionScopeRuleRequest GiftScope { get; set; } = new();
+}
+
+///
+/// 满赠规则响应。
+///
+public sealed class FullReductionGiftRuleResponse
+{
+ ///
+ /// 购买数量门槛。
+ ///
+ public int BuyQuantity { get; set; }
+
+ ///
+ /// 赠送数量。
+ ///
+ public int GiftQuantity { get; set; }
+
+ ///
+ /// 赠品范围类型(same_lowest/specified)。
+ ///
+ public string GiftScopeType { get; set; } = "same_lowest";
+
+ ///
+ /// 适用商品范围。
+ ///
+ public FullReductionScopeRuleResponse ApplicableScope { get; set; } = new();
+
+ ///
+ /// 指定赠品范围。
+ ///
+ public FullReductionScopeRuleResponse GiftScope { get; set; } = new();
+}
+
+///
+/// 第二份半价规则请求。
+///
+public sealed class FullReductionSecondHalfRuleRequest
+{
+ ///
+ /// 折扣类型(half/sixty/seventy/free)。
+ ///
+ public string DiscountType { get; set; } = "half";
+
+ ///
+ /// 适用商品范围。
+ ///
+ public FullReductionScopeRuleRequest ApplicableScope { get; set; } = new();
+}
+
+///
+/// 第二份半价规则响应。
+///
+public sealed class FullReductionSecondHalfRuleResponse
+{
+ ///
+ /// 折扣类型(half/sixty/seventy/free)。
+ ///
+ public string DiscountType { get; set; } = "half";
+
+ ///
+ /// 适用商品范围。
+ ///
+ public FullReductionScopeRuleResponse ApplicableScope { get; set; } = new();
+}
+
+///
+/// 商品范围请求。
+///
+public sealed class FullReductionScopeRuleRequest
+{
+ ///
+ /// 范围类型(all/category/product)。
+ ///
+ public string ScopeType { get; set; } = "all";
+
+ ///
+ /// 分类 ID。
+ ///
+ public List CategoryIds { get; set; } = [];
+
+ ///
+ /// 商品 ID。
+ ///
+ public List ProductIds { get; set; } = [];
+}
+
+///
+/// 商品范围响应。
+///
+public sealed class FullReductionScopeRuleResponse
+{
+ ///
+ /// 范围类型(all/category/product)。
+ ///
+ public string ScopeType { get; set; } = "all";
+
+ ///
+ /// 分类 ID。
+ ///
+ public List CategoryIds { get; set; } = [];
+
+ ///
+ /// 商品 ID。
+ ///
+ public List ProductIds { get; set; } = [];
+}
+
+///
+/// 指标请求。
+///
+public sealed class FullReductionMetricsRequest
+{
+ ///
+ /// 参与订单数。
+ ///
+ public int ParticipatingOrderCount { get; set; }
+
+ ///
+ /// 优惠总额。
+ ///
+ public decimal DiscountTotalAmount { get; set; }
+
+ ///
+ /// 客单价提升。
+ ///
+ public decimal TicketIncreaseAmount { get; set; }
+
+ ///
+ /// 赠出商品数量。
+ ///
+ public int GiftedCount { get; set; }
+
+ ///
+ /// 带动销售额。
+ ///
+ public decimal DrivenSalesAmount { get; set; }
+
+ ///
+ /// 连带率提升百分比。
+ ///
+ public decimal AttachRateIncreasePercent { get; set; }
+
+ ///
+ /// 本月带动销售额。
+ ///
+ public decimal MonthlyDrivenSalesAmount { get; set; }
+
+ ///
+ /// 平均客单价提升。
+ ///
+ public decimal AverageTicketIncrease { get; set; }
+}
+
+///
+/// 指标响应。
+///
+public sealed class FullReductionMetricsResponse
+{
+ ///
+ /// 参与订单数。
+ ///
+ public int ParticipatingOrderCount { get; set; }
+
+ ///
+ /// 优惠总额。
+ ///
+ public decimal DiscountTotalAmount { get; set; }
+
+ ///
+ /// 客单价提升。
+ ///
+ public decimal TicketIncreaseAmount { get; set; }
+
+ ///
+ /// 赠出商品数量。
+ ///
+ public int GiftedCount { get; set; }
+
+ ///
+ /// 带动销售额。
+ ///
+ public decimal DrivenSalesAmount { get; set; }
+
+ ///
+ /// 连带率提升百分比。
+ ///
+ public decimal AttachRateIncreasePercent { get; set; }
+
+ ///
+ /// 本月带动销售额。
+ ///
+ public decimal MonthlyDrivenSalesAmount { get; set; }
+
+ ///
+ /// 平均客单价提升。
+ ///
+ public decimal AverageTicketIncrease { get; set; }
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingFullReductionController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingFullReductionController.cs
new file mode 100644
index 0000000..8ec1310
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingFullReductionController.cs
@@ -0,0 +1,477 @@
+using System.Globalization;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using TakeoutSaaS.Application.App.Coupons.FullReduction.Commands;
+using TakeoutSaaS.Application.App.Coupons.FullReduction.Dto;
+using TakeoutSaaS.Application.App.Coupons.FullReduction.Queries;
+using TakeoutSaaS.Application.App.Stores.Services;
+using TakeoutSaaS.Infrastructure.App.Persistence;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Web.Api;
+using TakeoutSaaS.TenantApi.Contracts.Marketing;
+
+namespace TakeoutSaaS.TenantApi.Controllers;
+
+///
+/// 营销中心满减活动管理。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/tenant/v{version:apiVersion}/marketing/full-reduction")]
+public sealed class MarketingFullReductionController(
+ IMediator mediator,
+ TakeoutAppDbContext dbContext,
+ StoreContextService storeContextService) : BaseApiController
+{
+ ///
+ /// 获取满减活动列表。
+ ///
+ [HttpGet("list")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> List(
+ [FromQuery] FullReductionListRequest request,
+ CancellationToken cancellationToken)
+ {
+ // 1. 解析可见门店范围(支持全部门店)
+ var visibleStoreIds = await ResolveVisibleStoreIdsAsync(request.StoreId, cancellationToken);
+
+ // 2. 查询应用层列表
+ var result = await mediator.Send(new GetFullReductionCampaignListQuery
+ {
+ VisibleStoreIds = visibleStoreIds,
+ Keyword = request.Keyword,
+ ActivityType = request.ActivityType,
+ Status = request.Status,
+ Page = request.Page,
+ PageSize = request.PageSize
+ }, cancellationToken);
+
+ // 3. 映射响应
+ return ApiResponse.Ok(new FullReductionListResultResponse
+ {
+ Items = result.Items.Select(MapListItem).ToList(),
+ Total = result.TotalCount,
+ Page = result.Page,
+ PageSize = result.PageSize,
+ Stats = new FullReductionStatsResponse
+ {
+ TotalCount = result.Stats.TotalCount,
+ OngoingCount = result.Stats.OngoingCount,
+ MonthlyDrivenSalesAmount = result.Stats.MonthlyDrivenSalesAmount,
+ AverageTicketIncrease = result.Stats.AverageTicketIncrease
+ }
+ });
+ }
+
+ ///
+ /// 获取满减活动详情。
+ ///
+ [HttpGet("detail")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Detail(
+ [FromQuery] FullReductionDetailRequest request,
+ CancellationToken cancellationToken)
+ {
+ // 1. 解析并校验操作门店
+ var operationStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(operationStoreId, cancellationToken);
+
+ // 2. 查询详情
+ var detail = await mediator.Send(new GetFullReductionCampaignDetailQuery
+ {
+ OperationStoreId = operationStoreId,
+ CampaignId = StoreApiHelpers.ParseRequiredSnowflake(request.ActivityId, nameof(request.ActivityId))
+ }, cancellationToken);
+
+ // 3. 处理不存在场景
+ if (detail is null)
+ {
+ return ApiResponse.Error(ErrorCodes.NotFound, "活动不存在");
+ }
+
+ return ApiResponse.Ok(MapDetail(detail));
+ }
+
+ ///
+ /// 保存满减活动(新增/编辑)。
+ ///
+ [HttpPost("save")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Save(
+ [FromBody] SaveFullReductionRequest request,
+ CancellationToken cancellationToken)
+ {
+ // 1. 校验操作门店
+ var operationStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(operationStoreId, cancellationToken);
+
+ // 2. 展开活动门店范围
+ var resolvedStoreIds = await ResolveStoreScopeStoreIdsAsync(
+ request.StoreScopeMode,
+ request.StoreIds,
+ cancellationToken);
+
+ // 3. 解析选品基准门店
+ var scopeStoreId = string.IsNullOrWhiteSpace(request.ScopeStoreId)
+ ? operationStoreId
+ : StoreApiHelpers.ParseRequiredSnowflake(request.ScopeStoreId, nameof(request.ScopeStoreId));
+
+ if (!resolvedStoreIds.Contains(scopeStoreId))
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "scopeStoreId 必须在活动门店范围内");
+ }
+
+ // 4. 保存活动
+ var result = await mediator.Send(new SaveFullReductionCampaignCommand
+ {
+ OperationStoreId = operationStoreId,
+ CampaignId = StoreApiHelpers.ParseSnowflakeOrNull(request.Id),
+ Name = request.Name,
+ ActivityType = request.ActivityType,
+ ReduceTiers = request.ReduceTiers.Select(item => new FullReductionTierRuleDto
+ {
+ MeetAmount = item.MeetAmount,
+ ReduceAmount = item.ReduceAmount
+ }).ToList(),
+ GiftRule = MapGiftRuleRequest(request.GiftRule),
+ SecondHalfRule = MapSecondHalfRuleRequest(request.SecondHalfRule),
+ StartAt = StoreApiHelpers.ParseDateOnly(request.StartDate, nameof(request.StartDate)),
+ EndAt = StoreApiHelpers.ParseDateOnly(request.EndDate, nameof(request.EndDate)),
+ Channels = request.Channels ?? [],
+ StoreScopeMode = request.StoreScopeMode,
+ StoreScopeStoreIds = resolvedStoreIds,
+ ScopeStoreId = scopeStoreId,
+ StackWithCoupon = request.StackWithCoupon,
+ Description = request.Description,
+ Metrics = MapMetricsRequest(request.Metrics)
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapDetail(result));
+ }
+
+ ///
+ /// 修改满减活动状态。
+ ///
+ [HttpPost("status")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> ChangeStatus(
+ [FromBody] ChangeFullReductionStatusRequest request,
+ CancellationToken cancellationToken)
+ {
+ // 1. 校验操作门店
+ var operationStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(operationStoreId, cancellationToken);
+
+ // 2. 调用应用层修改状态
+ var result = await mediator.Send(new ChangeFullReductionCampaignStatusCommand
+ {
+ OperationStoreId = operationStoreId,
+ CampaignId = StoreApiHelpers.ParseRequiredSnowflake(request.ActivityId, nameof(request.ActivityId)),
+ Status = request.Status
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapDetail(result));
+ }
+
+ ///
+ /// 删除满减活动。
+ ///
+ [HttpPost("delete")]
+ [ProducesResponseType(typeof(ApiResponse