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