diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/OrderBoard/RejectOrderRequest.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/OrderBoard/RejectOrderRequest.cs
new file mode 100644
index 0000000..ba8e236
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/OrderBoard/RejectOrderRequest.cs
@@ -0,0 +1,12 @@
+namespace TakeoutSaaS.TenantApi.Contracts.OrderBoard;
+
+///
+/// 拒单请求体。
+///
+public sealed record RejectOrderRequest
+{
+ ///
+ /// 拒单原因。
+ ///
+ public required string Reason { get; init; }
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/OrderBoardController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/OrderBoardController.cs
new file mode 100644
index 0000000..dc60e8f
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/OrderBoardController.cs
@@ -0,0 +1,212 @@
+using Asp.Versioning;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using TakeoutSaaS.Application.App.Orders.Commands;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Application.App.Orders.Queries;
+using TakeoutSaaS.Application.App.Stores.Services;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Infrastructure.App.Persistence;
+using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Web.Api;
+using TakeoutSaaS.TenantApi.Contracts.OrderBoard;
+
+namespace TakeoutSaaS.TenantApi.Controllers;
+
+///
+/// 订单大厅(实时看板)接口。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/tenant/v{version:apiVersion}/order-board")]
+public sealed class OrderBoardController(
+ IMediator mediator,
+ TakeoutAppDbContext dbContext,
+ StoreContextService storeContextService) : BaseApiController
+{
+ ///
+ /// 获取完整看板数据(四列)。
+ ///
+ [HttpGet("board")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> GetBoard(
+ [FromQuery] string storeId,
+ [FromQuery] string? channel,
+ CancellationToken cancellationToken)
+ {
+ // 1. 解析并校验门店权限
+ var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, nameof(storeId));
+ await EnsureStoreAccessibleAsync(parsedStoreId, cancellationToken);
+
+ // 2. 解析渠道筛选
+ var channelFilter = ParseChannel(channel);
+
+ // 3. 查询看板数据
+ var (_, tenantId, _) = storeContextService.GetRequiredContext();
+ var result = await mediator.Send(new GetOrderBoardQuery
+ {
+ StoreId = parsedStoreId,
+ TenantId = tenantId,
+ Channel = channelFilter
+ }, cancellationToken);
+
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 获取看板统计数据。
+ ///
+ [HttpGet("stats")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> GetStats(
+ [FromQuery] string storeId,
+ CancellationToken cancellationToken)
+ {
+ var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, nameof(storeId));
+ await EnsureStoreAccessibleAsync(parsedStoreId, cancellationToken);
+
+ var (_, tenantId, _) = storeContextService.GetRequiredContext();
+ var result = await mediator.Send(new GetOrderBoardStatsQuery
+ {
+ StoreId = parsedStoreId,
+ TenantId = tenantId
+ }, cancellationToken);
+
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 重连补偿拉取(获取指定时间后的订单变更)。
+ ///
+ [HttpGet("pending-since")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> GetPendingSince(
+ [FromQuery] string storeId,
+ [FromQuery] DateTime since,
+ CancellationToken cancellationToken)
+ {
+ var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, nameof(storeId));
+ await EnsureStoreAccessibleAsync(parsedStoreId, cancellationToken);
+
+ var (_, tenantId, _) = storeContextService.GetRequiredContext();
+ var result = await mediator.Send(new GetPendingOrdersSinceQuery
+ {
+ StoreId = parsedStoreId,
+ TenantId = tenantId,
+ Since = since
+ }, cancellationToken);
+
+ return ApiResponse>.Ok(result);
+ }
+
+ ///
+ /// 接单。
+ ///
+ [HttpPost("{orderId}/accept")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Accept(
+ string orderId,
+ CancellationToken cancellationToken)
+ {
+ var parsedOrderId = StoreApiHelpers.ParseRequiredSnowflake(orderId, nameof(orderId));
+ var (userId, tenantId, _) = storeContextService.GetRequiredContext();
+
+ var result = await mediator.Send(new AcceptOrderCommand
+ {
+ OrderId = parsedOrderId,
+ TenantId = tenantId,
+ OperatorId = userId
+ }, cancellationToken);
+
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 拒单。
+ ///
+ [HttpPost("{orderId}/reject")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Reject(
+ string orderId,
+ [FromBody] RejectOrderRequest request,
+ CancellationToken cancellationToken)
+ {
+ var parsedOrderId = StoreApiHelpers.ParseRequiredSnowflake(orderId, nameof(orderId));
+ var (userId, tenantId, _) = storeContextService.GetRequiredContext();
+
+ var result = await mediator.Send(new RejectOrderCommand
+ {
+ OrderId = parsedOrderId,
+ TenantId = tenantId,
+ Reason = request.Reason,
+ OperatorId = userId
+ }, cancellationToken);
+
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 出餐完成。
+ ///
+ [HttpPost("{orderId}/complete-preparation")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> CompletePreparation(
+ string orderId,
+ CancellationToken cancellationToken)
+ {
+ var parsedOrderId = StoreApiHelpers.ParseRequiredSnowflake(orderId, nameof(orderId));
+ var (userId, tenantId, _) = storeContextService.GetRequiredContext();
+
+ var result = await mediator.Send(new CompletePreparationCommand
+ {
+ OrderId = parsedOrderId,
+ TenantId = tenantId,
+ OperatorId = userId
+ }, cancellationToken);
+
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 确认送达/取餐。
+ ///
+ [HttpPost("{orderId}/confirm-delivery")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> ConfirmDelivery(
+ string orderId,
+ CancellationToken cancellationToken)
+ {
+ var parsedOrderId = StoreApiHelpers.ParseRequiredSnowflake(orderId, nameof(orderId));
+ var (userId, tenantId, _) = storeContextService.GetRequiredContext();
+
+ var result = await mediator.Send(new ConfirmDeliveryCommand
+ {
+ OrderId = parsedOrderId,
+ TenantId = tenantId,
+ OperatorId = userId
+ }, cancellationToken);
+
+ return ApiResponse.Ok(result);
+ }
+
+ private async Task EnsureStoreAccessibleAsync(long storeId, CancellationToken cancellationToken)
+ {
+ var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
+ await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, storeId, cancellationToken);
+ }
+
+ private static OrderChannel? ParseChannel(string? value)
+ {
+ var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
+ return normalized switch
+ {
+ "miniprogram" => OrderChannel.MiniProgram,
+ "scan" => OrderChannel.ScanToOrder,
+ "staff" => OrderChannel.StaffConsole,
+ "phone" => OrderChannel.PhoneReservation,
+ "thirdparty" => OrderChannel.ThirdPartyDelivery,
+ _ => null
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardCardDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardCardDto.cs
new file mode 100644
index 0000000..b4ffaf2
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardCardDto.cs
@@ -0,0 +1,98 @@
+using System.Text.Json.Serialization;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Shared.Abstractions.Serialization;
+
+namespace TakeoutSaaS.Application.App.Orders.Dto;
+
+///
+/// 订单看板卡片 DTO。
+///
+public sealed class OrderBoardCardDto
+{
+ ///
+ /// 订单标识。
+ ///
+ [JsonConverter(typeof(SnowflakeIdJsonConverter))]
+ public long Id { get; init; }
+
+ ///
+ /// 订单编号。
+ ///
+ public string OrderNo { get; init; } = string.Empty;
+
+ ///
+ /// 门店标识。
+ ///
+ [JsonConverter(typeof(SnowflakeIdJsonConverter))]
+ public long StoreId { get; init; }
+
+ ///
+ /// 下单渠道。
+ ///
+ public OrderChannel Channel { get; init; }
+
+ ///
+ /// 履约类型。
+ ///
+ public DeliveryType DeliveryType { get; init; }
+
+ ///
+ /// 当前状态。
+ ///
+ public OrderStatus Status { get; init; }
+
+ ///
+ /// 顾客姓名。
+ ///
+ public string? CustomerName { get; init; }
+
+ ///
+ /// 顾客手机号。
+ ///
+ public string? CustomerPhone { get; init; }
+
+ ///
+ /// 桌号。
+ ///
+ public string? TableNo { get; init; }
+
+ ///
+ /// 排队号。
+ ///
+ public string? QueueNumber { get; init; }
+
+ ///
+ /// 商品摘要。
+ ///
+ public string? ItemsSummary { get; init; }
+
+ ///
+ /// 实付金额。
+ ///
+ public decimal PaidAmount { get; init; }
+
+ ///
+ /// 创建时间。
+ ///
+ public DateTime CreatedAt { get; init; }
+
+ ///
+ /// 接单时间。
+ ///
+ public DateTime? AcceptedAt { get; init; }
+
+ ///
+ /// 出餐时间。
+ ///
+ public DateTime? ReadyAt { get; init; }
+
+ ///
+ /// 是否被催单。
+ ///
+ public bool IsUrged { get; init; }
+
+ ///
+ /// 催单次数。
+ ///
+ public int UrgeCount { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardResultDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardResultDto.cs
new file mode 100644
index 0000000..1653f27
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardResultDto.cs
@@ -0,0 +1,27 @@
+namespace TakeoutSaaS.Application.App.Orders.Dto;
+
+///
+/// 订单看板结果 DTO(四列数据)。
+///
+public sealed class OrderBoardResultDto
+{
+ ///
+ /// 待接单列表。
+ ///
+ public IReadOnlyList Pending { get; init; } = [];
+
+ ///
+ /// 制作中列表。
+ ///
+ public IReadOnlyList Making { get; init; } = [];
+
+ ///
+ /// 配送/待取餐列表。
+ ///
+ public IReadOnlyList Delivering { get; init; } = [];
+
+ ///
+ /// 已完成列表(今日,限 20 条)。
+ ///
+ public IReadOnlyList Completed { get; init; } = [];
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardStatsDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardStatsDto.cs
new file mode 100644
index 0000000..a4df898
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardStatsDto.cs
@@ -0,0 +1,32 @@
+namespace TakeoutSaaS.Application.App.Orders.Dto;
+
+///
+/// 订单看板统计 DTO。
+///
+public sealed class OrderBoardStatsDto
+{
+ ///
+ /// 今日订单总数。
+ ///
+ public int TodayTotal { get; init; }
+
+ ///
+ /// 待接单数。
+ ///
+ public int PendingCount { get; init; }
+
+ ///
+ /// 制作中数。
+ ///
+ public int MakingCount { get; init; }
+
+ ///
+ /// 配送/待取餐数。
+ ///
+ public int DeliveringCount { get; init; }
+
+ ///
+ /// 已完成数。
+ ///
+ public int CompletedCount { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderBoardQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderBoardQueryHandler.cs
new file mode 100644
index 0000000..a78f306
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderBoardQueryHandler.cs
@@ -0,0 +1,89 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Application.App.Orders.Queries;
+using TakeoutSaaS.Domain.Orders.Entities;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Orders.Repositories;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 获取订单看板数据查询处理器。
+///
+public sealed class GetOrderBoardQueryHandler(IOrderRepository orderRepository)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(GetOrderBoardQuery request, CancellationToken cancellationToken)
+ {
+ // 1. 查询活跃订单
+ var activeOrders = await orderRepository.GetActiveOrdersAsync(
+ request.TenantId, request.StoreId, cancellationToken);
+
+ // 2. 查询今日已完成订单(限 20 条)
+ var todayStart = DateTime.UtcNow.Date;
+ var completedOrders = await orderRepository.GetOrdersChangedSinceAsync(
+ request.TenantId, request.StoreId, todayStart, cancellationToken);
+ var todayCompleted = completedOrders
+ .Where(o => o.Status == OrderStatus.Completed)
+ .OrderByDescending(o => o.FinishedAt)
+ .Take(20)
+ .ToList();
+
+ // 3. 合并并按渠道筛选
+ var allOrders = activeOrders.Concat(todayCompleted).ToList();
+ if (request.Channel.HasValue)
+ {
+ allOrders = allOrders.Where(o => o.Channel == request.Channel.Value).ToList();
+ }
+
+ // 4. 获取商品摘要
+ var orderIds = allOrders.Select(o => o.Id).ToList();
+ var itemsMap = orderIds.Count > 0
+ ? await orderRepository.GetItemsByOrderIdsAsync(orderIds, request.TenantId, cancellationToken)
+ : new Dictionary>();
+
+ // 5. 按状态分组
+ var cards = allOrders.Select(o => MapToCard(o, itemsMap)).ToList();
+
+ return new OrderBoardResultDto
+ {
+ Pending = cards.Where(c => c.Status == OrderStatus.AwaitingPreparation)
+ .OrderBy(c => c.CreatedAt).ToList(),
+ Making = cards.Where(c => c.Status == OrderStatus.InProgress)
+ .OrderBy(c => c.AcceptedAt).ToList(),
+ Delivering = cards.Where(c => c.Status == OrderStatus.Ready)
+ .OrderBy(c => c.ReadyAt).ToList(),
+ Completed = cards.Where(c => c.Status == OrderStatus.Completed)
+ .OrderByDescending(c => c.CreatedAt).ToList()
+ };
+ }
+
+ private static OrderBoardCardDto MapToCard(
+ Order order,
+ IReadOnlyDictionary> itemsMap)
+ {
+ var items = itemsMap.TryGetValue(order.Id, out var list) ? list : [];
+ var summary = items.Count > 0
+ ? string.Join("、", items.Take(3).Select(x => x.ProductName))
+ + (items.Count > 3 ? $" 等{items.Count}件" : string.Empty)
+ : string.Empty;
+
+ return new OrderBoardCardDto
+ {
+ Id = order.Id,
+ OrderNo = order.OrderNo,
+ StoreId = order.StoreId,
+ Channel = order.Channel,
+ DeliveryType = order.DeliveryType,
+ Status = order.Status,
+ CustomerName = order.CustomerName,
+ CustomerPhone = order.CustomerPhone,
+ TableNo = order.TableNo,
+ QueueNumber = order.QueueNumber,
+ ItemsSummary = summary,
+ PaidAmount = order.PaidAmount,
+ CreatedAt = order.CreatedAt
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderBoardStatsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderBoardStatsQueryHandler.cs
new file mode 100644
index 0000000..40a36bb
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderBoardStatsQueryHandler.cs
@@ -0,0 +1,39 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Application.App.Orders.Queries;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Orders.Repositories;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 获取订单看板统计查询处理器。
+///
+public sealed class GetOrderBoardStatsQueryHandler(IOrderRepository orderRepository)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(GetOrderBoardStatsQuery request, CancellationToken cancellationToken)
+ {
+ // 1. 查询活跃订单
+ var activeOrders = await orderRepository.GetActiveOrdersAsync(
+ request.TenantId, request.StoreId, cancellationToken);
+
+ // 2. 查询今日所有订单
+ var todayStart = DateTime.UtcNow.Date;
+ var todayOrders = await orderRepository.GetOrdersChangedSinceAsync(
+ request.TenantId, request.StoreId, todayStart, cancellationToken);
+
+ // 3. 统计各状态数量
+ var allOrders = activeOrders.Concat(todayOrders).DistinctBy(o => o.Id).ToList();
+
+ return new OrderBoardStatsDto
+ {
+ TodayTotal = allOrders.Count,
+ PendingCount = allOrders.Count(o => o.Status == OrderStatus.AwaitingPreparation),
+ MakingCount = allOrders.Count(o => o.Status == OrderStatus.InProgress),
+ DeliveringCount = allOrders.Count(o => o.Status == OrderStatus.Ready),
+ CompletedCount = allOrders.Count(o => o.Status == OrderStatus.Completed)
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetPendingOrdersSinceQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetPendingOrdersSinceQueryHandler.cs
new file mode 100644
index 0000000..9830087
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetPendingOrdersSinceQueryHandler.cs
@@ -0,0 +1,56 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Application.App.Orders.Queries;
+using TakeoutSaaS.Domain.Orders.Entities;
+using TakeoutSaaS.Domain.Orders.Repositories;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 获取指定时间后的待处理订单查询处理器。
+///
+public sealed class GetPendingOrdersSinceQueryHandler(IOrderRepository orderRepository)
+ : IRequestHandler>
+{
+ ///
+ public async Task> Handle(
+ GetPendingOrdersSinceQuery request, CancellationToken cancellationToken)
+ {
+ // 1. 查询指定时间后变更的订单
+ var orders = await orderRepository.GetOrdersChangedSinceAsync(
+ request.TenantId, request.StoreId, request.Since, cancellationToken);
+
+ // 2. 获取商品摘要
+ var orderIds = orders.Select(o => o.Id).ToList();
+ var itemsMap = orderIds.Count > 0
+ ? await orderRepository.GetItemsByOrderIdsAsync(orderIds, request.TenantId, cancellationToken)
+ : new Dictionary>();
+
+ // 3. 映射为卡片 DTO
+ return orders.Select(o =>
+ {
+ var items = itemsMap.TryGetValue(o.Id, out var list) ? list : [];
+ var summary = items.Count > 0
+ ? string.Join("、", items.Take(3).Select(x => x.ProductName))
+ + (items.Count > 3 ? $" 等{items.Count}件" : string.Empty)
+ : string.Empty;
+
+ return new OrderBoardCardDto
+ {
+ Id = o.Id,
+ OrderNo = o.OrderNo,
+ StoreId = o.StoreId,
+ Channel = o.Channel,
+ DeliveryType = o.DeliveryType,
+ Status = o.Status,
+ CustomerName = o.CustomerName,
+ CustomerPhone = o.CustomerPhone,
+ TableNo = o.TableNo,
+ QueueNumber = o.QueueNumber,
+ ItemsSummary = summary,
+ PaidAmount = o.PaidAmount,
+ CreatedAt = o.CreatedAt
+ };
+ }).ToList();
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderBoardQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderBoardQuery.cs
new file mode 100644
index 0000000..970f801
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderBoardQuery.cs
@@ -0,0 +1,26 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Domain.Orders.Enums;
+
+namespace TakeoutSaaS.Application.App.Orders.Queries;
+
+///
+/// 获取订单看板数据查询。
+///
+public sealed record GetOrderBoardQuery : IRequest
+{
+ ///
+ /// 门店标识。
+ ///
+ public required long StoreId { get; init; }
+
+ ///
+ /// 租户标识。
+ ///
+ public required long TenantId { get; init; }
+
+ ///
+ /// 渠道筛选(可选)。
+ ///
+ public OrderChannel? Channel { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderBoardStatsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderBoardStatsQuery.cs
new file mode 100644
index 0000000..4e4f959
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderBoardStatsQuery.cs
@@ -0,0 +1,20 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+
+namespace TakeoutSaaS.Application.App.Orders.Queries;
+
+///
+/// 获取订单看板统计查询。
+///
+public sealed record GetOrderBoardStatsQuery : IRequest
+{
+ ///
+ /// 门店标识。
+ ///
+ public required long StoreId { get; init; }
+
+ ///
+ /// 租户标识。
+ ///
+ public required long TenantId { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetPendingOrdersSinceQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetPendingOrdersSinceQuery.cs
new file mode 100644
index 0000000..b5bf870
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetPendingOrdersSinceQuery.cs
@@ -0,0 +1,25 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+
+namespace TakeoutSaaS.Application.App.Orders.Queries;
+
+///
+/// 获取指定时间后的待处理订单(重连补偿)。
+///
+public sealed record GetPendingOrdersSinceQuery : IRequest>
+{
+ ///
+ /// 门店标识。
+ ///
+ public required long StoreId { get; init; }
+
+ ///
+ /// 租户标识。
+ ///
+ public required long TenantId { get; init; }
+
+ ///
+ /// 起始时间(UTC)。
+ ///
+ public required DateTime Since { get; init; }
+}
diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs
index 8b0c5e8..767b448 100644
--- a/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs
@@ -221,4 +221,23 @@ public interface IOrderRepository
/// 取消标记。
/// 异步任务。
Task DeleteOrderAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 获取门店活跃订单(待接单、制作中、待取餐)。
+ ///
+ /// 租户 ID。
+ /// 门店 ID。
+ /// 取消标记。
+ /// 活跃订单集合。
+ Task> GetActiveOrdersAsync(long tenantId, long storeId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 获取指定时间后变更的订单(重连补偿)。
+ ///
+ /// 租户 ID。
+ /// 门店 ID。
+ /// 起始时间(UTC)。
+ /// 取消标记。
+ /// 变更订单集合。
+ Task> GetOrdersChangedSinceAsync(long tenantId, long storeId, DateTime since, CancellationToken cancellationToken = default);
}
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs
index ad79ac1..d616f21 100644
--- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs
@@ -313,6 +313,35 @@ public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepos
context.Orders.Remove(existing);
}
+ ///
+ public async Task> GetActiveOrdersAsync(long tenantId, long storeId, CancellationToken cancellationToken = default)
+ {
+ // 查询待接单、制作中、待取餐的活跃订单
+ var activeStatuses = new[]
+ {
+ OrderStatus.AwaitingPreparation,
+ OrderStatus.InProgress,
+ OrderStatus.Ready
+ };
+
+ return await context.Orders
+ .AsNoTracking()
+ .Where(x => x.TenantId == tenantId && x.StoreId == storeId && activeStatuses.Contains(x.Status))
+ .OrderBy(x => x.CreatedAt)
+ .ToListAsync(cancellationToken);
+ }
+
+ ///
+ public async Task> GetOrdersChangedSinceAsync(long tenantId, long storeId, DateTime since, CancellationToken cancellationToken = default)
+ {
+ // 查询指定时间后创建或更新的订单
+ return await context.Orders
+ .AsNoTracking()
+ .Where(x => x.TenantId == tenantId && x.StoreId == storeId && x.UpdatedAt >= since)
+ .OrderByDescending(x => x.UpdatedAt)
+ .ToListAsync(cancellationToken);
+ }
+
private IQueryable BuildSearchAllOrdersQuery(
long tenantId,
long? storeId,