From c9e2226b4889b2e2f3837c60eec2de9f6a3606eb Mon Sep 17 00:00:00 2001
From: MSuMshk <2039814060@qq.com>
Date: Mon, 2 Mar 2026 13:08:56 +0800
Subject: [PATCH] feat(marketing): implement tenant seckill backend module
---
TakeoutSaaS.Docs | 2 +-
.../Contracts/Marketing/SeckillContracts.cs | 700 ++++++++++++++++++
.../Controllers/MarketingSeckillController.cs | 446 +++++++++++
.../ChangeSeckillCampaignStatusCommand.cs | 27 +
.../Commands/DeleteSeckillCampaignCommand.cs | 21 +
.../Commands/SaveSeckillCampaignCommand.cs | 90 +++
.../Coupons/Seckill/Dto/SeckillDetailDto.cs | 39 +
.../Coupons/Seckill/Dto/SeckillListItemDto.cs | 44 ++
.../Seckill/Dto/SeckillListResultDto.cs | 34 +
.../Coupons/Seckill/Dto/SeckillMetricsDto.cs | 27 +
.../Dto/SeckillPickerCategoryItemDto.cs | 24 +
.../Dto/SeckillPickerProductItemDto.cs | 49 ++
.../Seckill/Dto/SeckillProductRuleDto.cs | 62 ++
.../Coupons/Seckill/Dto/SeckillRulesDto.cs | 72 ++
.../Seckill/Dto/SeckillSaveProductInputDto.cs | 27 +
.../Seckill/Dto/SeckillSessionRuleDto.cs | 17 +
.../Coupons/Seckill/Dto/SeckillStatsDto.cs | 27 +
...angeSeckillCampaignStatusCommandHandler.cs | 45 ++
.../DeleteSeckillCampaignCommandHandler.cs | 42 ++
.../GetSeckillCampaignDetailQueryHandler.cs | 45 ++
.../GetSeckillCampaignListQueryHandler.cs | 113 +++
.../GetSeckillPickerCategoriesQueryHandler.cs | 51 ++
.../GetSeckillPickerProductsQueryHandler.cs | 61 ++
.../SaveSeckillCampaignCommandHandler.cs | 178 +++++
.../Queries/GetSeckillCampaignDetailQuery.cs | 22 +
.../Queries/GetSeckillCampaignListQuery.cs | 37 +
.../GetSeckillPickerCategoriesQuery.cs | 17 +
.../Queries/GetSeckillPickerProductsQuery.cs | 32 +
.../App/Coupons/Seckill/SeckillDtoFactory.cs | 105 +++
.../App/Coupons/Seckill/SeckillMapping.cs | 652 ++++++++++++++++
.../Coupons/Enums/PromotionType.cs | 7 +-
31 files changed, 3113 insertions(+), 2 deletions(-)
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/SeckillContracts.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingSeckillController.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Commands/ChangeSeckillCampaignStatusCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Commands/DeleteSeckillCampaignCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Commands/SaveSeckillCampaignCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillDetailDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillListItemDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillListResultDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillMetricsDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillPickerCategoryItemDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillPickerProductItemDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillProductRuleDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillRulesDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillSaveProductInputDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillSessionRuleDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Dto/SeckillStatsDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Handlers/ChangeSeckillCampaignStatusCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Handlers/DeleteSeckillCampaignCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Handlers/GetSeckillCampaignDetailQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Handlers/GetSeckillCampaignListQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Handlers/GetSeckillPickerCategoriesQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Handlers/GetSeckillPickerProductsQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Handlers/SaveSeckillCampaignCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Queries/GetSeckillCampaignDetailQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Queries/GetSeckillCampaignListQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Queries/GetSeckillPickerCategoriesQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/Queries/GetSeckillPickerProductsQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/SeckillDtoFactory.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/Seckill/SeckillMapping.cs
diff --git a/TakeoutSaaS.Docs b/TakeoutSaaS.Docs
index de7aefd..5da102c 160000
--- a/TakeoutSaaS.Docs
+++ b/TakeoutSaaS.Docs
@@ -1 +1 @@
-Subproject commit de7aefd0ffe5c3ab842207c62aaa736edf7b8dfb
+Subproject commit 5da102c97c2cd7acbd3f20d69f286b3d0eadf0fe
diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/SeckillContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/SeckillContracts.cs
new file mode 100644
index 0000000..6c51785
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/SeckillContracts.cs
@@ -0,0 +1,700 @@
+namespace TakeoutSaaS.TenantApi.Contracts.Marketing;
+
+///
+/// 秒杀活动列表查询请求。
+///
+public sealed class SeckillListRequest
+{
+ ///
+ /// 门店 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 SeckillDetailRequest
+{
+ ///
+ /// 操作门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 活动 ID。
+ ///
+ public string ActivityId { get; set; } = string.Empty;
+}
+
+///
+/// 保存秒杀活动请求。
+///
+public sealed class SaveSeckillRequest
+{
+ ///
+ /// 操作门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 活动 ID(编辑时传)。
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// 活动名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 活动类型(timed/hourly)。
+ ///
+ public string ActivityType { get; set; } = "timed";
+
+ ///
+ /// 活动开始日期(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; }
+
+ ///
+ /// 整点秒杀场次。
+ ///
+ public List? Sessions { get; set; }
+
+ ///
+ /// 适用渠道(delivery/pickup/dine_in)。
+ ///
+ public List? Channels { get; set; }
+
+ ///
+ /// 活动每人限购(空表示不限)。
+ ///
+ public int? PerUserLimit { get; set; }
+
+ ///
+ /// 是否开启预热。
+ ///
+ public bool PreheatEnabled { get; set; }
+
+ ///
+ /// 预热小时数(空表示不启用)。
+ ///
+ public int? PreheatHours { get; set; }
+
+ ///
+ /// 活动门店 ID。
+ ///
+ public List? StoreIds { get; set; }
+
+ ///
+ /// 秒杀商品列表。
+ ///
+ public List Products { get; set; } = [];
+
+ ///
+ /// 活动指标。
+ ///
+ public SeckillMetricsRequest? Metrics { get; set; }
+}
+
+///
+/// 修改秒杀活动状态请求。
+///
+public sealed class ChangeSeckillStatusRequest
+{
+ ///
+ /// 操作门店 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 DeleteSeckillRequest
+{
+ ///
+ /// 操作门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 活动 ID。
+ ///
+ public string ActivityId { get; set; } = string.Empty;
+}
+
+///
+/// 秒杀活动列表响应。
+///
+public sealed class SeckillListResultResponse
+{
+ ///
+ /// 列表项。
+ ///
+ public List Items { get; set; } = [];
+
+ ///
+ /// 总条数。
+ ///
+ public int Total { get; set; }
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; }
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; }
+
+ ///
+ /// 统计数据。
+ ///
+ public SeckillStatsResponse Stats { get; set; } = new();
+}
+
+///
+/// 秒杀活动列表项响应。
+///
+public sealed class SeckillListItemResponse
+{
+ ///
+ /// 活动 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 活动名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 活动类型(timed/hourly)。
+ ///
+ public string ActivityType { get; set; } = "timed";
+
+ ///
+ /// 活动开始日期(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; }
+
+ ///
+ /// 整点秒杀场次。
+ ///
+ public List Sessions { 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 bool PreheatEnabled { get; set; }
+
+ ///
+ /// 预热小时数(空表示不启用)。
+ ///
+ public int? PreheatHours { get; set; }
+
+ ///
+ /// 活动门店。
+ ///
+ public List StoreIds { get; set; } = [];
+
+ ///
+ /// 秒杀商品。
+ ///
+ public List Products { get; set; } = [];
+
+ ///
+ /// 活动指标。
+ ///
+ public SeckillMetricsResponse Metrics { get; set; } = new();
+
+ ///
+ /// 更新时间(yyyy-MM-dd HH:mm:ss)。
+ ///
+ public string UpdatedAt { get; set; } = string.Empty;
+}
+
+///
+/// 秒杀活动详情响应。
+///
+public sealed class SeckillDetailResponse
+{
+ ///
+ /// 活动 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 活动名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 活动类型(timed/hourly)。
+ ///
+ public string ActivityType { get; set; } = "timed";
+
+ ///
+ /// 活动开始日期(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; }
+
+ ///
+ /// 整点秒杀场次。
+ ///
+ public List Sessions { 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 bool PreheatEnabled { get; set; }
+
+ ///
+ /// 预热小时数(空表示不启用)。
+ ///
+ public int? PreheatHours { get; set; }
+
+ ///
+ /// 活动门店。
+ ///
+ public List StoreIds { get; set; } = [];
+
+ ///
+ /// 秒杀商品。
+ ///
+ public List Products { get; set; } = [];
+
+ ///
+ /// 活动指标。
+ ///
+ public SeckillMetricsResponse Metrics { get; set; } = new();
+
+ ///
+ /// 更新时间(yyyy-MM-dd HH:mm:ss)。
+ ///
+ public string UpdatedAt { get; set; } = string.Empty;
+}
+
+///
+/// 秒杀活动统计响应。
+///
+public sealed class SeckillStatsResponse
+{
+ ///
+ /// 活动总数。
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// 进行中数量。
+ ///
+ public int OngoingCount { get; set; }
+
+ ///
+ /// 本月秒杀销量。
+ ///
+ public int MonthlySeckillSalesCount { get; set; }
+
+ ///
+ /// 秒杀转化率。
+ ///
+ public decimal ConversionRate { get; set; }
+}
+
+///
+/// 秒杀场次请求。
+///
+public sealed class SeckillSessionRequest
+{
+ ///
+ /// 场次开始时间(HH:mm)。
+ ///
+ public string StartTime { get; set; } = string.Empty;
+
+ ///
+ /// 场次持续时长(分钟)。
+ ///
+ public int DurationMinutes { get; set; }
+}
+
+///
+/// 秒杀场次响应。
+///
+public sealed class SeckillSessionResponse
+{
+ ///
+ /// 场次开始时间(HH:mm)。
+ ///
+ public string StartTime { get; set; } = string.Empty;
+
+ ///
+ /// 场次持续时长(分钟)。
+ ///
+ public int DurationMinutes { get; set; }
+}
+
+///
+/// 秒杀商品请求。
+///
+public sealed class SeckillSaveProductRequest
+{
+ ///
+ /// 商品 ID。
+ ///
+ public string ProductId { get; set; } = string.Empty;
+
+ ///
+ /// 秒杀价。
+ ///
+ public decimal SeckillPrice { get; set; }
+
+ ///
+ /// 限量库存(份)。
+ ///
+ public int StockLimit { get; set; }
+
+ ///
+ /// 商品每人限购(空表示不限)。
+ ///
+ public int? PerUserLimit { get; set; }
+}
+
+///
+/// 秒杀商品响应。
+///
+public sealed class SeckillProductResponse
+{
+ ///
+ /// 商品 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 SeckillPrice { get; set; }
+
+ ///
+ /// 限量库存(份)。
+ ///
+ public int StockLimit { get; set; }
+
+ ///
+ /// 商品每人限购(空表示不限)。
+ ///
+ public int? PerUserLimit { get; set; }
+
+ ///
+ /// 已售数量。
+ ///
+ public int SoldCount { get; set; }
+}
+
+///
+/// 秒杀指标请求。
+///
+public sealed class SeckillMetricsRequest
+{
+ ///
+ /// 参与人数。
+ ///
+ public int ParticipantCount { get; set; }
+
+ ///
+ /// 成交单数。
+ ///
+ public int DealCount { get; set; }
+
+ ///
+ /// 转化率(百分比)。
+ ///
+ public decimal ConversionRate { get; set; }
+
+ ///
+ /// 本月秒杀销量(单)。
+ ///
+ public int MonthlySeckillSalesCount { get; set; }
+}
+
+///
+/// 秒杀指标响应。
+///
+public sealed class SeckillMetricsResponse
+{
+ ///
+ /// 参与人数。
+ ///
+ public int ParticipantCount { get; set; }
+
+ ///
+ /// 成交单数。
+ ///
+ public int DealCount { get; set; }
+
+ ///
+ /// 转化率(百分比)。
+ ///
+ public decimal ConversionRate { get; set; }
+
+ ///
+ /// 本月秒杀销量(单)。
+ ///
+ public int MonthlySeckillSalesCount { get; set; }
+}
+
+///
+/// 秒杀商品分类选择器请求。
+///
+public sealed class SeckillPickerCategoriesRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+}
+
+///
+/// 秒杀商品分类选择器响应项。
+///
+public sealed class SeckillPickerCategoryItemResponse
+{
+ ///
+ /// 分类 ID。
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 分类名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 商品数量。
+ ///
+ public int ProductCount { get; set; }
+}
+
+///
+/// 秒杀商品选择器请求。
+///
+public sealed class SeckillPickerProductsRequest
+{
+ ///
+ /// 门店 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 SeckillPickerProductItemResponse
+{
+ ///
+ /// 商品 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/MarketingSeckillController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingSeckillController.cs
new file mode 100644
index 0000000..afd663d
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingSeckillController.cs
@@ -0,0 +1,446 @@
+using System.Globalization;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using TakeoutSaaS.Application.App.Coupons.Seckill.Commands;
+using TakeoutSaaS.Application.App.Coupons.Seckill.Dto;
+using TakeoutSaaS.Application.App.Coupons.Seckill.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/seckill")]
+public sealed class MarketingSeckillController(
+ IMediator mediator,
+ TakeoutAppDbContext dbContext,
+ StoreContextService storeContextService) : BaseApiController
+{
+ ///
+ /// 获取秒杀活动列表。
+ ///
+ [HttpGet("list")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> List(
+ [FromQuery] SeckillListRequest request,
+ CancellationToken cancellationToken)
+ {
+ var visibleStoreIds = await ResolveVisibleStoreIdsAsync(request.StoreId, cancellationToken);
+
+ var result = await mediator.Send(new GetSeckillCampaignListQuery
+ {
+ VisibleStoreIds = visibleStoreIds,
+ Keyword = request.Keyword,
+ Status = request.Status,
+ Page = request.Page,
+ PageSize = request.PageSize
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new SeckillListResultResponse
+ {
+ Items = result.Items.Select(MapListItem).ToList(),
+ Total = result.TotalCount,
+ Page = result.Page,
+ PageSize = result.PageSize,
+ Stats = new SeckillStatsResponse
+ {
+ TotalCount = result.Stats.TotalCount,
+ OngoingCount = result.Stats.OngoingCount,
+ MonthlySeckillSalesCount = result.Stats.MonthlySeckillSalesCount,
+ ConversionRate = result.Stats.ConversionRate
+ }
+ });
+ }
+
+ ///
+ /// 获取秒杀活动详情。
+ ///
+ [HttpGet("detail")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Detail(
+ [FromQuery] SeckillDetailRequest request,
+ CancellationToken cancellationToken)
+ {
+ var operationStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(operationStoreId, cancellationToken);
+
+ var result = await mediator.Send(new GetSeckillCampaignDetailQuery
+ {
+ 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] SaveSeckillRequest 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 SaveSeckillCampaignCommand
+ {
+ OperationStoreId = operationStoreId,
+ CampaignId = StoreApiHelpers.ParseSnowflakeOrNull(request.Id),
+ Name = request.Name,
+ ActivityType = request.ActivityType,
+ 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)),
+ Sessions = (request.Sessions ?? [])
+ .Select(item => new SeckillSessionRuleDto
+ {
+ StartTime = StoreApiHelpers.ParseRequiredTime(item.StartTime, "sessions.startTime"),
+ DurationMinutes = item.DurationMinutes
+ })
+ .ToList(),
+ Channels = request.Channels ?? [],
+ PerUserLimit = request.PerUserLimit,
+ PreheatEnabled = request.PreheatEnabled,
+ PreheatHours = request.PreheatHours,
+ StoreIds = resolvedStoreIds,
+ Products = request.Products.Select(item => new SeckillSaveProductInputDto
+ {
+ ProductId = StoreApiHelpers.ParseRequiredSnowflake(item.ProductId, nameof(item.ProductId)),
+ SeckillPrice = item.SeckillPrice,
+ StockLimit = item.StockLimit,
+ PerUserLimit = item.PerUserLimit
+ }).ToList(),
+ Metrics = request.Metrics is null
+ ? null
+ : new SeckillMetricsDto
+ {
+ ParticipantCount = request.Metrics.ParticipantCount,
+ DealCount = request.Metrics.DealCount,
+ ConversionRate = request.Metrics.ConversionRate,
+ MonthlySeckillSalesCount = request.Metrics.MonthlySeckillSalesCount
+ }
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapDetail(result));
+ }
+
+ ///
+ /// 修改秒杀活动状态。
+ ///
+ [HttpPost("status")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> ChangeStatus(
+ [FromBody] ChangeSeckillStatusRequest request,
+ CancellationToken cancellationToken)
+ {
+ var operationStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(operationStoreId, cancellationToken);
+
+ var result = await mediator.Send(new ChangeSeckillCampaignStatusCommand
+ {
+ OperationStoreId = operationStoreId,
+ CampaignId = StoreApiHelpers.ParseRequiredSnowflake(request.ActivityId, nameof(request.ActivityId)),
+ Status = request.Status
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapDetail(result));
+ }
+
+ ///
+ /// 删除秒杀活动。
+ ///
+ [HttpPost("delete")]
+ [ProducesResponseType(typeof(ApiResponse