From 0f3542f33faad1c8398bdf05c398c9ddc9165112 Mon Sep 17 00:00:00 2001
From: MSuMshk <2039814060@qq.com>
Date: Mon, 2 Mar 2026 11:08:14 +0800
Subject: [PATCH] feat(marketing): implement flash sale module api and app
layer
---
.../Contracts/Marketing/FlashSaleContracts.cs | 643 ++++++++++++++++++
.../MarketingFlashSaleController.cs | 427 ++++++++++++
.../ChangeFlashSaleCampaignStatusCommand.cs | 26 +
.../DeleteFlashSaleCampaignCommand.cs | 20 +
.../Commands/SaveFlashSaleCampaignCommand.cs | 86 +++
.../FlashSale/Dto/FlashSaleDetailDto.cs | 38 ++
.../FlashSale/Dto/FlashSaleListItemDto.cs | 43 ++
.../FlashSale/Dto/FlashSaleListResultDto.cs | 33 +
.../FlashSale/Dto/FlashSaleMetricsDto.cs | 28 +
.../Dto/FlashSalePickerCategoryItemDto.cs | 23 +
.../Dto/FlashSalePickerProductItemDto.cs | 48 ++
.../FlashSale/Dto/FlashSaleProductRuleDto.cs | 58 ++
.../FlashSale/Dto/FlashSaleRulesDto.cs | 68 ++
.../Dto/FlashSaleSaveProductInputDto.cs | 23 +
.../FlashSale/Dto/FlashSaleStatsDto.cs | 28 +
.../Coupons/FlashSale/FlashSaleDtoFactory.cs | 102 +++
.../App/Coupons/FlashSale/FlashSaleMapping.cs | 581 ++++++++++++++++
...geFlashSaleCampaignStatusCommandHandler.cs | 44 ++
.../DeleteFlashSaleCampaignCommandHandler.cs | 41 ++
.../GetFlashSaleCampaignDetailQueryHandler.cs | 44 ++
.../GetFlashSaleCampaignListQueryHandler.cs | 112 +++
...etFlashSalePickerCategoriesQueryHandler.cs | 50 ++
.../GetFlashSalePickerProductsQueryHandler.cs | 60 ++
.../SaveFlashSaleCampaignCommandHandler.cs | 177 +++++
.../GetFlashSaleCampaignDetailQuery.cs | 21 +
.../Queries/GetFlashSaleCampaignListQuery.cs | 36 +
.../GetFlashSalePickerCategoriesQuery.cs | 16 +
.../GetFlashSalePickerProductsQuery.cs | 31 +
.../Repositories/IProductRepository.cs | 9 +
.../App/Repositories/EfProductRepository.cs | 23 +
30 files changed, 2939 insertions(+)
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/FlashSaleContracts.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingFlashSaleController.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Commands/ChangeFlashSaleCampaignStatusCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Commands/DeleteFlashSaleCampaignCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Commands/SaveFlashSaleCampaignCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Dto/FlashSaleDetailDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Dto/FlashSaleListItemDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Dto/FlashSaleListResultDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Dto/FlashSaleMetricsDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Dto/FlashSalePickerCategoryItemDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Dto/FlashSalePickerProductItemDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Dto/FlashSaleProductRuleDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Dto/FlashSaleRulesDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Dto/FlashSaleSaveProductInputDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Dto/FlashSaleStatsDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/FlashSaleDtoFactory.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/FlashSaleMapping.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Handlers/ChangeFlashSaleCampaignStatusCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Handlers/DeleteFlashSaleCampaignCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Handlers/GetFlashSaleCampaignDetailQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Handlers/GetFlashSaleCampaignListQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Handlers/GetFlashSalePickerCategoriesQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Handlers/GetFlashSalePickerProductsQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Handlers/SaveFlashSaleCampaignCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Queries/GetFlashSaleCampaignDetailQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Queries/GetFlashSaleCampaignListQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Queries/GetFlashSalePickerCategoriesQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/FlashSale/Queries/GetFlashSalePickerProductsQuery.cs
diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/FlashSaleContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/FlashSaleContracts.cs
new file mode 100644
index 0000000..b42bf6d
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/FlashSaleContracts.cs
@@ -0,0 +1,643 @@
+namespace TakeoutSaaS.TenantApi.Contracts.Marketing;
+
+///
+/// 限时折扣列表查询请求。
+///
+public sealed class FlashSaleListRequest
+{
+ ///
+ /// 门店 ID(可空,空表示全部门店)。
+ ///
+ public string? StoreId { get; set; }
+
+ ///
+ /// 活动名称关键字。
+ ///
+ public string? Keyword { 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 FlashSaleDetailRequest
+{
+ ///
+ /// 操作门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 活动 ID。
+ ///
+ public string ActivityId { get; set; } = string.Empty;
+}
+
+///
+/// 保存限时折扣请求。
+///
+public sealed class SaveFlashSaleRequest
+{
+ ///
+ /// 操作门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 活动 ID(编辑时传)。
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// 活动名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 活动周期(once/recurring)。
+ ///
+ public string CycleType { get; set; } = "once";
+
+ ///
+ /// 周期日期模式(fixed/long_term)。
+ ///
+ public string RecurringDateMode { get; set; } = "fixed";
+
+ ///
+ /// 活动开始日期(yyyy-MM-dd)。
+ ///
+ public string? StartDate { get; set; }
+
+ ///
+ /// 活动结束日期(yyyy-MM-dd)。
+ ///
+ public string? EndDate { get; set; }
+
+ ///
+ /// 每日开始时间(HH:mm)。
+ ///
+ public string? TimeStart { get; set; }
+
+ ///
+ /// 每日结束时间(HH:mm)。
+ ///
+ public string? TimeEnd { get; set; }
+
+ ///
+ /// 循环星期(1-7,周一到周日)。
+ ///
+ public List? WeekDays { get; set; }
+
+ ///
+ /// 适用渠道(delivery/pickup/dine_in)。
+ ///
+ public List? Channels { get; set; }
+
+ ///
+ /// 活动每人限购(空表示不限)。
+ ///
+ public int? PerUserLimit { get; set; }
+
+ ///
+ /// 活动门店 ID。
+ ///
+ public List? StoreIds { get; set; }
+
+ ///
+ /// 折扣商品列表。
+ ///
+ public List Products { get; set; } = [];
+
+ ///
+ /// 活动指标。
+ ///
+ public FlashSaleMetricsRequest? Metrics { get; set; }
+}
+
+///
+/// 修改限时折扣状态请求。
+///
+public sealed class ChangeFlashSaleStatusRequest
+{
+ ///
+ /// 操作门店 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 DeleteFlashSaleRequest
+{
+ ///
+ /// 操作门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 活动 ID。
+ ///
+ public string ActivityId { get; set; } = string.Empty;
+}
+
+///
+/// 限时折扣列表响应。
+///
+public sealed class FlashSaleListResultResponse
+{
+ ///
+ /// 列表项。
+ ///
+ public List Items { get; set; } = [];
+
+ ///
+ /// 总条数。
+ ///
+ public int Total { get; set; }
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; }
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; }
+
+ ///
+ /// 统计数据。
+ ///
+ public FlashSaleStatsResponse Stats { get; set; } = new();
+}
+
+///
+/// 限时折扣列表项响应。
+///
+public sealed class FlashSaleListItemResponse
+{
+ ///
+ /// 活动 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 活动名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 活动周期(once/recurring)。
+ ///
+ public string CycleType { get; set; } = "once";
+
+ ///
+ /// 周期日期模式(fixed/long_term)。
+ ///
+ public string RecurringDateMode { get; set; } = "fixed";
+
+ ///
+ /// 活动开始日期(yyyy-MM-dd)。
+ ///
+ public string? StartDate { get; set; }
+
+ ///
+ /// 活动结束日期(yyyy-MM-dd)。
+ ///
+ public string? EndDate { get; set; }
+
+ ///
+ /// 每日开始时间(HH:mm)。
+ ///
+ public string? TimeStart { get; set; }
+
+ ///
+ /// 每日结束时间(HH:mm)。
+ ///
+ public string? TimeEnd { get; set; }
+
+ ///
+ /// 循环星期(1-7)。
+ ///
+ public List WeekDays { get; set; } = [];
+
+ ///
+ /// 编辑状态(active/completed)。
+ ///
+ public string Status { get; set; } = "active";
+
+ ///
+ /// 展示状态(ongoing/upcoming/ended)。
+ ///
+ public string DisplayStatus { get; set; } = "ongoing";
+
+ ///
+ /// 是否弱化展示。
+ ///
+ public bool IsDimmed { get; set; }
+
+ ///
+ /// 适用渠道。
+ ///
+ public List Channels { get; set; } = [];
+
+ ///
+ /// 活动每人限购(空表示不限)。
+ ///
+ public int? PerUserLimit { get; set; }
+
+ ///
+ /// 活动门店。
+ ///
+ public List StoreIds { get; set; } = [];
+
+ ///
+ /// 折扣商品。
+ ///
+ public List Products { get; set; } = [];
+
+ ///
+ /// 活动指标。
+ ///
+ public FlashSaleMetricsResponse Metrics { get; set; } = new();
+
+ ///
+ /// 更新时间(yyyy-MM-dd HH:mm:ss)。
+ ///
+ public string UpdatedAt { get; set; } = string.Empty;
+}
+
+///
+/// 限时折扣详情响应。
+///
+public sealed class FlashSaleDetailResponse
+{
+ ///
+ /// 活动 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 活动名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 活动周期(once/recurring)。
+ ///
+ public string CycleType { get; set; } = "once";
+
+ ///
+ /// 周期日期模式(fixed/long_term)。
+ ///
+ public string RecurringDateMode { get; set; } = "fixed";
+
+ ///
+ /// 活动开始日期(yyyy-MM-dd)。
+ ///
+ public string? StartDate { get; set; }
+
+ ///
+ /// 活动结束日期(yyyy-MM-dd)。
+ ///
+ public string? EndDate { get; set; }
+
+ ///
+ /// 每日开始时间(HH:mm)。
+ ///
+ public string? TimeStart { get; set; }
+
+ ///
+ /// 每日结束时间(HH:mm)。
+ ///
+ public string? TimeEnd { get; set; }
+
+ ///
+ /// 循环星期(1-7)。
+ ///
+ public List WeekDays { get; set; } = [];
+
+ ///
+ /// 编辑状态(active/completed)。
+ ///
+ public string Status { get; set; } = "active";
+
+ ///
+ /// 展示状态(ongoing/upcoming/ended)。
+ ///
+ public string DisplayStatus { get; set; } = "ongoing";
+
+ ///
+ /// 适用渠道。
+ ///
+ public List Channels { get; set; } = [];
+
+ ///
+ /// 活动每人限购(空表示不限)。
+ ///
+ public int? PerUserLimit { get; set; }
+
+ ///
+ /// 活动门店。
+ ///
+ public List StoreIds { get; set; } = [];
+
+ ///
+ /// 折扣商品。
+ ///
+ public List Products { get; set; } = [];
+
+ ///
+ /// 活动指标。
+ ///
+ public FlashSaleMetricsResponse Metrics { get; set; } = new();
+
+ ///
+ /// 更新时间(yyyy-MM-dd HH:mm:ss)。
+ ///
+ public string UpdatedAt { get; set; } = string.Empty;
+}
+
+///
+/// 限时折扣统计响应。
+///
+public sealed class FlashSaleStatsResponse
+{
+ ///
+ /// 活动总数。
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// 进行中数量。
+ ///
+ public int OngoingCount { get; set; }
+
+ ///
+ /// 参与商品数。
+ ///
+ public int ParticipatingProductCount { get; set; }
+
+ ///
+ /// 本月折扣销量。
+ ///
+ public int MonthlyDiscountSalesCount { get; set; }
+}
+
+///
+/// 限时折扣商品请求。
+///
+public sealed class FlashSaleSaveProductRequest
+{
+ ///
+ /// 商品 ID。
+ ///
+ public string ProductId { get; set; } = string.Empty;
+
+ ///
+ /// 折扣价。
+ ///
+ public decimal DiscountPrice { get; set; }
+
+ ///
+ /// 商品每人限购(空表示不限)。
+ ///
+ public int? PerUserLimit { get; set; }
+}
+
+///
+/// 限时折扣商品响应。
+///
+public sealed class FlashSaleProductResponse
+{
+ ///
+ /// 商品 ID。
+ ///
+ public string ProductId { get; set; } = string.Empty;
+
+ ///
+ /// 分类 ID。
+ ///
+ public string CategoryId { get; set; } = string.Empty;
+
+ ///
+ /// 分类名称。
+ ///
+ public string CategoryName { get; set; } = string.Empty;
+
+ ///
+ /// 商品名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// SPU 编码。
+ ///
+ public string SpuCode { get; set; } = string.Empty;
+
+ ///
+ /// 商品状态(on_sale/off_shelf/sold_out)。
+ ///
+ public string Status { get; set; } = "off_shelf";
+
+ ///
+ /// 原价。
+ ///
+ public decimal OriginalPrice { get; set; }
+
+ ///
+ /// 折扣价。
+ ///
+ public decimal DiscountPrice { get; set; }
+
+ ///
+ /// 商品每人限购(空表示不限)。
+ ///
+ public int? PerUserLimit { get; set; }
+
+ ///
+ /// 已售数量。
+ ///
+ public int SoldCount { get; set; }
+}
+
+///
+/// 限时折扣指标请求。
+///
+public sealed class FlashSaleMetricsRequest
+{
+ ///
+ /// 活动销量(单)。
+ ///
+ public int ActivitySalesCount { get; set; }
+
+ ///
+ /// 折扣总额。
+ ///
+ public decimal DiscountTotalAmount { get; set; }
+
+ ///
+ /// 已循环周数。
+ ///
+ public int LoopedWeeks { get; set; }
+
+ ///
+ /// 本月折扣销量(单)。
+ ///
+ public int MonthlyDiscountSalesCount { get; set; }
+}
+
+///
+/// 限时折扣指标响应。
+///
+public sealed class FlashSaleMetricsResponse
+{
+ ///
+ /// 活动销量(单)。
+ ///
+ public int ActivitySalesCount { get; set; }
+
+ ///
+ /// 折扣总额。
+ ///
+ public decimal DiscountTotalAmount { get; set; }
+
+ ///
+ /// 已循环周数。
+ ///
+ public int LoopedWeeks { get; set; }
+
+ ///
+ /// 本月折扣销量(单)。
+ ///
+ public int MonthlyDiscountSalesCount { get; set; }
+}
+
+///
+/// 限时折扣商品分类选择器请求。
+///
+public sealed class FlashSalePickerCategoriesRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+}
+
+///
+/// 限时折扣商品分类选择器响应项。
+///
+public sealed class FlashSalePickerCategoryItemResponse
+{
+ ///
+ /// 分类 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 分类名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 商品数量。
+ ///
+ public int ProductCount { get; set; }
+}
+
+///
+/// 限时折扣商品选择器请求。
+///
+public sealed class FlashSalePickerProductsRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 分类 ID(可空)。
+ ///
+ public string? CategoryId { get; set; }
+
+ ///
+ /// 关键字。
+ ///
+ public string? Keyword { get; set; }
+
+ ///
+ /// 数量上限。
+ ///
+ public int? Limit { get; set; }
+}
+
+///
+/// 限时折扣商品选择器响应项。
+///
+public sealed class FlashSalePickerProductItemResponse
+{
+ ///
+ /// 商品 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 分类 ID。
+ ///
+ public string CategoryId { get; set; } = string.Empty;
+
+ ///
+ /// 分类名称。
+ ///
+ public string CategoryName { get; set; } = string.Empty;
+
+ ///
+ /// 商品名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 售价。
+ ///
+ public decimal Price { get; set; }
+
+ ///
+ /// 库存。
+ ///
+ public int Stock { get; set; }
+
+ ///
+ /// SPU 编码。
+ ///
+ public string SpuCode { get; set; } = string.Empty;
+
+ ///
+ /// 状态(on_sale/off_shelf/sold_out)。
+ ///
+ public string Status { get; set; } = "off_shelf";
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingFlashSaleController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingFlashSaleController.cs
new file mode 100644
index 0000000..d7ca13e
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingFlashSaleController.cs
@@ -0,0 +1,427 @@
+using System.Globalization;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using TakeoutSaaS.Application.App.Coupons.FlashSale.Commands;
+using TakeoutSaaS.Application.App.Coupons.FlashSale.Dto;
+using TakeoutSaaS.Application.App.Coupons.FlashSale.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/flash-sale")]
+public sealed class MarketingFlashSaleController(
+ IMediator mediator,
+ TakeoutAppDbContext dbContext,
+ StoreContextService storeContextService) : BaseApiController
+{
+ ///
+ /// 获取限时折扣活动列表。
+ ///
+ [HttpGet("list")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> List(
+ [FromQuery] FlashSaleListRequest request,
+ CancellationToken cancellationToken)
+ {
+ var visibleStoreIds = await ResolveVisibleStoreIdsAsync(request.StoreId, cancellationToken);
+
+ var result = await mediator.Send(new GetFlashSaleCampaignListQuery
+ {
+ VisibleStoreIds = visibleStoreIds,
+ Keyword = request.Keyword,
+ Status = request.Status,
+ Page = request.Page,
+ PageSize = request.PageSize
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new FlashSaleListResultResponse
+ {
+ Items = result.Items.Select(MapListItem).ToList(),
+ Total = result.TotalCount,
+ Page = result.Page,
+ PageSize = result.PageSize,
+ Stats = new FlashSaleStatsResponse
+ {
+ TotalCount = result.Stats.TotalCount,
+ OngoingCount = result.Stats.OngoingCount,
+ ParticipatingProductCount = result.Stats.ParticipatingProductCount,
+ MonthlyDiscountSalesCount = result.Stats.MonthlyDiscountSalesCount
+ }
+ });
+ }
+
+ ///
+ /// 获取限时折扣活动详情。
+ ///
+ [HttpGet("detail")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Detail(
+ [FromQuery] FlashSaleDetailRequest request,
+ CancellationToken cancellationToken)
+ {
+ var operationStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(operationStoreId, cancellationToken);
+
+ var result = await mediator.Send(new GetFlashSaleCampaignDetailQuery
+ {
+ OperationStoreId = operationStoreId,
+ CampaignId = StoreApiHelpers.ParseRequiredSnowflake(request.ActivityId, nameof(request.ActivityId))
+ }, cancellationToken);
+
+ if (result is null)
+ {
+ return ApiResponse.Error(ErrorCodes.NotFound, "活动不存在");
+ }
+
+ return ApiResponse.Ok(MapDetail(result));
+ }
+
+ ///
+ /// 保存限时折扣活动(新增/编辑)。
+ ///
+ [HttpPost("save")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Save(
+ [FromBody] SaveFlashSaleRequest request,
+ CancellationToken cancellationToken)
+ {
+ var operationStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(operationStoreId, cancellationToken);
+
+ var resolvedStoreIds = await ResolveStoreIdsForSaveAsync(
+ request.StoreIds,
+ operationStoreId,
+ cancellationToken);
+
+ var result = await mediator.Send(new SaveFlashSaleCampaignCommand
+ {
+ OperationStoreId = operationStoreId,
+ CampaignId = StoreApiHelpers.ParseSnowflakeOrNull(request.Id),
+ Name = request.Name,
+ CycleType = request.CycleType,
+ RecurringDateMode = request.RecurringDateMode,
+ StartDate = ParseDateOnlyOrNull(request.StartDate, nameof(request.StartDate)),
+ EndDate = ParseDateOnlyOrNull(request.EndDate, nameof(request.EndDate)),
+ TimeStart = ParseTimeOrNull(request.TimeStart, nameof(request.TimeStart)),
+ TimeEnd = ParseTimeOrNull(request.TimeEnd, nameof(request.TimeEnd)),
+ WeekDays = request.WeekDays ?? [],
+ Channels = request.Channels ?? [],
+ PerUserLimit = request.PerUserLimit,
+ StoreIds = resolvedStoreIds,
+ Products = request.Products.Select(item => new FlashSaleSaveProductInputDto
+ {
+ ProductId = StoreApiHelpers.ParseRequiredSnowflake(item.ProductId, nameof(item.ProductId)),
+ DiscountPrice = item.DiscountPrice,
+ PerUserLimit = item.PerUserLimit
+ }).ToList(),
+ Metrics = request.Metrics is null
+ ? null
+ : new FlashSaleMetricsDto
+ {
+ ActivitySalesCount = request.Metrics.ActivitySalesCount,
+ DiscountTotalAmount = request.Metrics.DiscountTotalAmount,
+ LoopedWeeks = request.Metrics.LoopedWeeks,
+ MonthlyDiscountSalesCount = request.Metrics.MonthlyDiscountSalesCount
+ }
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapDetail(result));
+ }
+
+ ///
+ /// 修改限时折扣活动状态。
+ ///
+ [HttpPost("status")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> ChangeStatus(
+ [FromBody] ChangeFlashSaleStatusRequest request,
+ CancellationToken cancellationToken)
+ {
+ var operationStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(operationStoreId, cancellationToken);
+
+ var result = await mediator.Send(new ChangeFlashSaleCampaignStatusCommand
+ {
+ OperationStoreId = operationStoreId,
+ CampaignId = StoreApiHelpers.ParseRequiredSnowflake(request.ActivityId, nameof(request.ActivityId)),
+ Status = request.Status
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapDetail(result));
+ }
+
+ ///
+ /// 删除限时折扣活动。
+ ///
+ [HttpPost("delete")]
+ [ProducesResponseType(typeof(ApiResponse