From 3c423f87d412699fa81444f88e94cb4bfec8998e Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Fri, 27 Feb 2026 13:10:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=A4=A7=E5=8E=85=E7=9C=8B=E6=9D=BF=20API=EF=BC=88=E5=9B=9B?= =?UTF-8?q?=E5=88=97=E6=95=B0=E6=8D=AE=20+=20=E7=BB=9F=E8=AE=A1=20+=20?= =?UTF-8?q?=E9=87=8D=E8=BF=9E=E8=A1=A5=E5=81=BF=20+=20=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E7=AB=AF=E7=82=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 OrderBoardCardDto、OrderBoardResultDto、OrderBoardStatsDto - 新增看板查询 Query + Handler(board/stats/pending-since) - IOrderRepository 扩展 GetActiveOrdersAsync、GetOrdersChangedSinceAsync - EfOrderRepository 实现看板查询方法 - 新增 OrderBoardController(GET board/stats/pending-since + POST accept/reject/complete/confirm) - 新增 RejectOrderRequest 契约 --- .../OrderBoard/RejectOrderRequest.cs | 12 + .../Controllers/OrderBoardController.cs | 212 ++++++++++++++++++ .../App/Orders/Dto/OrderBoardCardDto.cs | 98 ++++++++ .../App/Orders/Dto/OrderBoardResultDto.cs | 27 +++ .../App/Orders/Dto/OrderBoardStatsDto.cs | 32 +++ .../Handlers/GetOrderBoardQueryHandler.cs | 89 ++++++++ .../GetOrderBoardStatsQueryHandler.cs | 39 ++++ .../GetPendingOrdersSinceQueryHandler.cs | 56 +++++ .../App/Orders/Queries/GetOrderBoardQuery.cs | 26 +++ .../Orders/Queries/GetOrderBoardStatsQuery.cs | 20 ++ .../Queries/GetPendingOrdersSinceQuery.cs | 25 +++ .../Orders/Repositories/IOrderRepository.cs | 19 ++ .../App/Repositories/EfOrderRepository.cs | 29 +++ 13 files changed, 684 insertions(+) create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/OrderBoard/RejectOrderRequest.cs create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/OrderBoardController.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardCardDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardResultDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderBoardStatsDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderBoardQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderBoardStatsQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetPendingOrdersSinceQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderBoardQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderBoardStatsQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetPendingOrdersSinceQuery.cs 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,