diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Order/OrderContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Order/OrderContracts.cs
new file mode 100644
index 0000000..a6503c2
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Order/OrderContracts.cs
@@ -0,0 +1,331 @@
+namespace TakeoutSaaS.TenantApi.Contracts.Order;
+
+///
+/// 全部订单筛选请求。
+///
+public class OrderAllFilterRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 开始日期(yyyy-MM-dd)。
+ ///
+ public string? StartDate { get; set; }
+
+ ///
+ /// 结束日期(yyyy-MM-dd)。
+ ///
+ public string? EndDate { get; set; }
+
+ ///
+ /// 状态筛选。
+ ///
+ public string? Status { get; set; }
+
+ ///
+ /// 渠道筛选(delivery/pickup/dine_in)。
+ ///
+ public string? Channel { get; set; }
+
+ ///
+ /// 支付方式筛选(wechat/alipay/balance/cash/card)。
+ ///
+ public string? PaymentMethod { get; set; }
+
+ ///
+ /// 关键词(订单号/手机号)。
+ ///
+ public string? Keyword { get; set; }
+}
+
+///
+/// 全部订单列表请求。
+///
+public sealed class OrderAllListRequest : OrderAllFilterRequest
+{
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; } = 1;
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; } = 20;
+}
+
+///
+/// 全部订单详情请求。
+///
+public sealed class OrderAllDetailRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 订单号。
+ ///
+ public string OrderNo { get; set; } = string.Empty;
+}
+
+///
+/// 全部订单列表结果。
+///
+public sealed class OrderAllListResultResponse
+{
+ ///
+ /// 列表。
+ ///
+ public List Items { get; set; } = [];
+
+ ///
+ /// 总数。
+ ///
+ public int Total { get; set; }
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; }
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; }
+}
+
+///
+/// 全部订单行。
+///
+public sealed class OrderAllListItemResponse
+{
+ ///
+ /// 订单号。
+ ///
+ public string OrderNo { get; set; } = string.Empty;
+
+ ///
+ /// 下单时间。
+ ///
+ public string OrderedAt { get; set; } = string.Empty;
+
+ ///
+ /// 渠道。
+ ///
+ public string Channel { get; set; } = string.Empty;
+
+ ///
+ /// 顾客。
+ ///
+ public string Customer { get; set; } = string.Empty;
+
+ ///
+ /// 商品摘要。
+ ///
+ public string ItemsSummary { get; set; } = string.Empty;
+
+ ///
+ /// 金额。
+ ///
+ public decimal Amount { get; set; }
+
+ ///
+ /// 状态。
+ ///
+ public string Status { get; set; } = string.Empty;
+
+ ///
+ /// 是否弱化展示。
+ ///
+ public bool IsDimmed { get; set; }
+}
+
+///
+/// 全部订单统计。
+///
+public sealed class OrderAllStatsResponse
+{
+ ///
+ /// 订单数。
+ ///
+ public int TotalOrders { get; set; }
+
+ ///
+ /// 总金额。
+ ///
+ public decimal TotalAmount { get; set; }
+
+ ///
+ /// 平均客单价。
+ ///
+ public decimal AverageAmount { get; set; }
+
+ ///
+ /// 退款单数。
+ ///
+ public int RefundCount { get; set; }
+}
+
+///
+/// 全部订单详情。
+///
+public sealed class OrderAllDetailResponse
+{
+ ///
+ /// 订单号。
+ ///
+ public string OrderNo { get; set; } = string.Empty;
+
+ ///
+ /// 渠道。
+ ///
+ public string Channel { get; set; } = string.Empty;
+
+ ///
+ /// 状态。
+ ///
+ public string Status { get; set; } = string.Empty;
+
+ ///
+ /// 支付方式。
+ ///
+ public string PaymentMethod { get; set; } = string.Empty;
+
+ ///
+ /// 下单时间。
+ ///
+ public string OrderedAt { get; set; } = string.Empty;
+
+ ///
+ /// 支付时间。
+ ///
+ public string? PaidAt { get; set; }
+
+ ///
+ /// 完成时间。
+ ///
+ public string? FinishedAt { get; set; }
+
+ ///
+ /// 顾客姓名。
+ ///
+ public string CustomerName { get; set; } = string.Empty;
+
+ ///
+ /// 顾客手机号。
+ ///
+ public string CustomerPhone { get; set; } = string.Empty;
+
+ ///
+ /// 收货地址。
+ ///
+ public string CustomerAddress { get; set; } = string.Empty;
+
+ ///
+ /// 商品金额。
+ ///
+ public decimal ItemsAmount { get; set; }
+
+ ///
+ /// 配送费。
+ ///
+ public decimal DeliveryFee { get; set; }
+
+ ///
+ /// 优惠减免。
+ ///
+ public decimal DiscountAmount { get; set; }
+
+ ///
+ /// 实付金额。
+ ///
+ public decimal PaidAmount { get; set; }
+
+ ///
+ /// 备注。
+ ///
+ public string Remark { get; set; } = string.Empty;
+
+ ///
+ /// 商品明细。
+ ///
+ public List Items { get; set; } = [];
+
+ ///
+ /// 状态时间线。
+ ///
+ public List Timeline { get; set; } = [];
+}
+
+///
+/// 全部订单商品明细行。
+///
+public sealed class OrderAllDetailItemResponse
+{
+ ///
+ /// 商品名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 规格。
+ ///
+ public string Spec { get; set; } = string.Empty;
+
+ ///
+ /// 数量。
+ ///
+ public int Quantity { get; set; }
+
+ ///
+ /// 单价。
+ ///
+ public decimal UnitPrice { get; set; }
+
+ ///
+ /// 小计。
+ ///
+ public decimal SubTotal { get; set; }
+}
+
+///
+/// 全部订单时间线节点。
+///
+public sealed class OrderAllTimelineResponse
+{
+ ///
+ /// 节点文案。
+ ///
+ public string Label { get; set; } = string.Empty;
+
+ ///
+ /// 时间。
+ ///
+ public string Time { get; set; } = string.Empty;
+}
+
+///
+/// 全部订单导出回执。
+///
+public sealed class OrderAllExportResponse
+{
+ ///
+ /// 文件名。
+ ///
+ public string FileName { get; set; } = string.Empty;
+
+ ///
+ /// 文件 Base64。
+ ///
+ public string FileContentBase64 { get; set; } = string.Empty;
+
+ ///
+ /// 导出记录数。
+ ///
+ public int TotalCount { get; set; }
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/OrderController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/OrderController.cs
new file mode 100644
index 0000000..92abcdc
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/OrderController.cs
@@ -0,0 +1,373 @@
+using System.Globalization;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+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.Domain.Payments.Enums;
+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.Order;
+
+namespace TakeoutSaaS.TenantApi.Controllers;
+
+///
+/// 租户端订单管理。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/tenant/v{version:apiVersion}/order")]
+public sealed class OrderController(
+ IMediator mediator,
+ TakeoutAppDbContext dbContext,
+ StoreContextService storeContextService) : BaseApiController
+{
+ ///
+ /// 全部订单列表。
+ ///
+ [HttpGet("all/list")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> List(
+ [FromQuery] OrderAllListRequest request,
+ CancellationToken cancellationToken)
+ {
+ var (storeId, startAt, endAt, status, refundedOnly, deliveryType, paymentMethod) =
+ await ParseFilterAsync(request, cancellationToken);
+
+ var result = await mediator.Send(new SearchOrderAllListQuery
+ {
+ StoreId = storeId,
+ StartAt = startAt,
+ EndAt = endAt,
+ Status = status,
+ RefundedOnly = refundedOnly,
+ DeliveryType = deliveryType,
+ PaymentMethod = paymentMethod,
+ Keyword = request.Keyword,
+ Page = Math.Max(1, request.Page),
+ PageSize = Math.Clamp(request.PageSize, 1, 200),
+ SortBy = "createdAt",
+ SortDescending = true
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new OrderAllListResultResponse
+ {
+ Items = result.Items.Select(MapListItem).ToList(),
+ Total = result.TotalCount,
+ Page = result.Page,
+ PageSize = result.PageSize
+ });
+ }
+
+ ///
+ /// 全部订单统计。
+ ///
+ [HttpGet("all/stats")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Stats(
+ [FromQuery] OrderAllFilterRequest request,
+ CancellationToken cancellationToken)
+ {
+ var (storeId, startAt, endAt, status, refundedOnly, deliveryType, paymentMethod) =
+ await ParseFilterAsync(request, cancellationToken);
+
+ var result = await mediator.Send(new GetOrderAllStatsQuery
+ {
+ StoreId = storeId,
+ StartAt = startAt,
+ EndAt = endAt,
+ Status = status,
+ RefundedOnly = refundedOnly,
+ DeliveryType = deliveryType,
+ PaymentMethod = paymentMethod,
+ Keyword = request.Keyword
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new OrderAllStatsResponse
+ {
+ TotalOrders = result.TotalOrders,
+ TotalAmount = result.TotalAmount,
+ AverageAmount = result.AverageAmount,
+ RefundCount = result.RefundCount
+ });
+ }
+
+ ///
+ /// 全部订单详情。
+ ///
+ [HttpGet("all/detail")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Detail(
+ [FromQuery] OrderAllDetailRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var orderNo = request.OrderNo?.Trim() ?? string.Empty;
+ if (string.IsNullOrWhiteSpace(orderNo))
+ {
+ return ApiResponse.Error(ErrorCodes.BadRequest, "orderNo 非法");
+ }
+
+ var result = await mediator.Send(new GetOrderAllDetailQuery
+ {
+ StoreId = storeId,
+ OrderNo = orderNo
+ }, cancellationToken);
+
+ if (result is null)
+ {
+ return ApiResponse.Error(ErrorCodes.NotFound, "订单不存在");
+ }
+
+ return ApiResponse.Ok(MapDetail(result));
+ }
+
+ ///
+ /// 全部订单导出。
+ ///
+ [HttpGet("all/export")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Export(
+ [FromQuery] OrderAllFilterRequest request,
+ CancellationToken cancellationToken)
+ {
+ var (storeId, startAt, endAt, status, refundedOnly, deliveryType, paymentMethod) =
+ await ParseFilterAsync(request, cancellationToken);
+
+ var result = await mediator.Send(new ExportOrderAllCsvQuery
+ {
+ StoreId = storeId,
+ StartAt = startAt,
+ EndAt = endAt,
+ Status = status,
+ RefundedOnly = refundedOnly,
+ DeliveryType = deliveryType,
+ PaymentMethod = paymentMethod,
+ Keyword = request.Keyword
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new OrderAllExportResponse
+ {
+ FileName = result.FileName,
+ FileContentBase64 = result.FileContentBase64,
+ TotalCount = result.TotalCount
+ });
+ }
+
+ private async Task<(long StoreId, DateTime? StartAt, DateTime? EndAt, OrderStatus? Status, bool RefundedOnly, DeliveryType? DeliveryType, PaymentMethod? PaymentMethod)> ParseFilterAsync(
+ OrderAllFilterRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var startAt = ParseDateOrNull(request.StartDate);
+ var endAt = ParseDateOrNull(request.EndDate)?.AddDays(1);
+ if (startAt.HasValue && endAt.HasValue && startAt >= endAt)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "开始日期不能晚于结束日期");
+ }
+
+ var (status, refundedOnly) = ParseStatus(request.Status);
+ var deliveryType = ParseDeliveryType(request.Channel);
+ if (deliveryType is null)
+ {
+ var normalizedStatus = (request.Status ?? string.Empty).Trim().ToLowerInvariant();
+ if (normalizedStatus == "pickup")
+ {
+ deliveryType = Domain.Orders.Enums.DeliveryType.Pickup;
+ }
+ else if (normalizedStatus == "delivering")
+ {
+ deliveryType = Domain.Orders.Enums.DeliveryType.Delivery;
+ }
+ }
+ var paymentMethod = ParsePaymentMethod(request.PaymentMethod);
+
+ return (storeId, startAt, endAt, status, refundedOnly, deliveryType, paymentMethod);
+ }
+
+ private async Task EnsureStoreAccessibleAsync(long storeId, CancellationToken cancellationToken)
+ {
+ var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
+ await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, storeId, cancellationToken);
+ }
+
+ private static DateTime? ParseDateOrNull(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return null;
+ }
+
+ if (DateTime.TryParseExact(
+ value,
+ "yyyy-MM-dd",
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.None,
+ out var parsed))
+ {
+ return DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc);
+ }
+
+ throw new BusinessException(ErrorCodes.BadRequest, "日期格式必须为 yyyy-MM-dd");
+ }
+
+ private static (OrderStatus? Status, bool RefundedOnly) ParseStatus(string? value)
+ {
+ var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
+ return normalized switch
+ {
+ "pending" => (OrderStatus.AwaitingPreparation, false),
+ "making" => (OrderStatus.InProgress, false),
+ "delivering" => (OrderStatus.Ready, false),
+ "pickup" => (OrderStatus.Ready, false),
+ "completed" => (OrderStatus.Completed, false),
+ "cancelled" => (OrderStatus.Cancelled, false),
+ "refunded" => (null, true),
+ _ => (null, false)
+ };
+ }
+
+ private static DeliveryType? ParseDeliveryType(string? value)
+ {
+ var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
+ return normalized switch
+ {
+ "delivery" => Domain.Orders.Enums.DeliveryType.Delivery,
+ "pickup" => Domain.Orders.Enums.DeliveryType.Pickup,
+ "dine_in" => Domain.Orders.Enums.DeliveryType.DineIn,
+ _ => null
+ };
+ }
+
+ private static PaymentMethod? ParsePaymentMethod(string? value)
+ {
+ var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
+ return normalized switch
+ {
+ "wechat" => Domain.Payments.Enums.PaymentMethod.WeChatPay,
+ "alipay" => Domain.Payments.Enums.PaymentMethod.Alipay,
+ "balance" => Domain.Payments.Enums.PaymentMethod.Balance,
+ "cash" => Domain.Payments.Enums.PaymentMethod.Cash,
+ "card" => Domain.Payments.Enums.PaymentMethod.Card,
+ _ => null
+ };
+ }
+
+ private static OrderAllListItemResponse MapListItem(OrderAllListItemDto source)
+ {
+ return new OrderAllListItemResponse
+ {
+ OrderNo = source.OrderNo,
+ OrderedAt = source.OrderedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
+ Channel = ToDeliveryTypeText(source.DeliveryType),
+ Customer = source.CustomerName,
+ ItemsSummary = source.ItemsSummary,
+ Amount = source.Amount,
+ Status = ToStatusText(source.Status, source.IsRefunded, source.DeliveryType),
+ IsDimmed = source.IsDimmed
+ };
+ }
+
+ private static OrderAllDetailResponse MapDetail(OrderAllDetailDto source)
+ {
+ return new OrderAllDetailResponse
+ {
+ OrderNo = source.OrderNo,
+ Channel = ToDeliveryTypeText(source.DeliveryType),
+ Status = ToStatusText(source.Status, false, source.DeliveryType),
+ PaymentMethod = ToPaymentMethodText(source.PaymentMethod),
+ OrderedAt = source.OrderedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
+ PaidAt = source.PaidAt?.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
+ FinishedAt = source.FinishedAt?.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
+ CustomerName = source.CustomerName,
+ CustomerPhone = source.CustomerPhone,
+ CustomerAddress = source.CustomerAddress,
+ ItemsAmount = source.ItemsAmount,
+ DeliveryFee = source.DeliveryFee,
+ DiscountAmount = source.DiscountAmount,
+ PaidAmount = source.PaidAmount,
+ Remark = source.Remark,
+ Items = source.Items
+ .Select(item => new OrderAllDetailItemResponse
+ {
+ Name = item.Name,
+ Spec = item.Spec,
+ Quantity = item.Quantity,
+ UnitPrice = item.UnitPrice,
+ SubTotal = item.SubTotal
+ })
+ .ToList(),
+ Timeline = source.Timeline
+ .OrderBy(item => item.OccurredAt)
+ .Select(item => new OrderAllTimelineResponse
+ {
+ Label = item.Label,
+ Time = item.OccurredAt.ToString("HH:mm:ss", CultureInfo.InvariantCulture)
+ })
+ .ToList()
+ };
+ }
+
+ private static string ToDeliveryTypeText(Domain.Orders.Enums.DeliveryType value)
+ {
+ return value switch
+ {
+ Domain.Orders.Enums.DeliveryType.Delivery => "外卖",
+ Domain.Orders.Enums.DeliveryType.Pickup => "自提",
+ Domain.Orders.Enums.DeliveryType.DineIn => "堂食",
+ _ => "未知"
+ };
+ }
+
+ private static string ToPaymentMethodText(Domain.Payments.Enums.PaymentMethod value)
+ {
+ return value switch
+ {
+ Domain.Payments.Enums.PaymentMethod.WeChatPay => "微信支付",
+ Domain.Payments.Enums.PaymentMethod.Alipay => "支付宝",
+ Domain.Payments.Enums.PaymentMethod.Balance => "余额支付",
+ Domain.Payments.Enums.PaymentMethod.Cash => "现金",
+ Domain.Payments.Enums.PaymentMethod.Card => "刷卡",
+ _ => "--"
+ };
+ }
+
+ private static string ToStatusText(OrderStatus status, bool refunded, DeliveryType deliveryType)
+ {
+ if (refunded)
+ {
+ return "已退款";
+ }
+
+ return status switch
+ {
+ OrderStatus.PendingPayment => "待接单",
+ OrderStatus.AwaitingPreparation => "待接单",
+ OrderStatus.InProgress => "制作中",
+ OrderStatus.Ready => ToReadyStatusText(deliveryType),
+ OrderStatus.Completed => "已完成",
+ OrderStatus.Cancelled => "已取消",
+ _ => "未知"
+ };
+ }
+
+ private static string ToReadyStatusText(DeliveryType deliveryType)
+ {
+ return deliveryType switch
+ {
+ DeliveryType.Delivery => "配送中",
+ DeliveryType.Pickup => "待取餐",
+ DeliveryType.DineIn => "待取餐",
+ _ => "待处理"
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderAllDtos.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderAllDtos.cs
new file mode 100644
index 0000000..ba04237
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderAllDtos.cs
@@ -0,0 +1,265 @@
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Payments.Enums;
+
+namespace TakeoutSaaS.Application.App.Orders.Dto;
+
+///
+/// 全部订单列表行 DTO。
+///
+public sealed class OrderAllListItemDto
+{
+ ///
+ /// 订单 ID。
+ ///
+ public long OrderId { get; init; }
+
+ ///
+ /// 订单号。
+ ///
+ public string OrderNo { get; init; } = string.Empty;
+
+ ///
+ /// 下单时间。
+ ///
+ public DateTime OrderedAt { get; init; }
+
+ ///
+ /// 履约方式。
+ ///
+ public DeliveryType DeliveryType { get; init; }
+
+ ///
+ /// 订单状态。
+ ///
+ public OrderStatus Status { get; init; }
+
+ ///
+ /// 顾客名称。
+ ///
+ public string CustomerName { get; init; } = string.Empty;
+
+ ///
+ /// 商品摘要。
+ ///
+ public string ItemsSummary { get; init; } = string.Empty;
+
+ ///
+ /// 金额。
+ ///
+ public decimal Amount { get; init; }
+
+ ///
+ /// 是否已退款。
+ ///
+ public bool IsRefunded { get; init; }
+
+ ///
+ /// 是否弱化显示。
+ ///
+ public bool IsDimmed { get; init; }
+}
+
+///
+/// 全部订单统计 DTO。
+///
+public sealed class OrderAllStatsDto
+{
+ ///
+ /// 总订单数。
+ ///
+ public int TotalOrders { get; init; }
+
+ ///
+ /// 总金额。
+ ///
+ public decimal TotalAmount { get; init; }
+
+ ///
+ /// 平均客单价。
+ ///
+ public decimal AverageAmount { get; init; }
+
+ ///
+ /// 退款单数。
+ ///
+ public int RefundCount { get; init; }
+}
+
+///
+/// 全部订单详情 DTO。
+///
+public sealed class OrderAllDetailDto
+{
+ ///
+ /// 订单 ID。
+ ///
+ public long OrderId { get; init; }
+
+ ///
+ /// 订单号。
+ ///
+ public string OrderNo { get; init; } = string.Empty;
+
+ ///
+ /// 门店 ID。
+ ///
+ public long StoreId { get; init; }
+
+ ///
+ /// 履约方式。
+ ///
+ public DeliveryType DeliveryType { get; init; }
+
+ ///
+ /// 订单状态。
+ ///
+ public OrderStatus Status { get; init; }
+
+ ///
+ /// 支付方式。
+ ///
+ public PaymentMethod PaymentMethod { get; init; }
+
+ ///
+ /// 顾客姓名。
+ ///
+ public string CustomerName { get; init; } = string.Empty;
+
+ ///
+ /// 顾客手机号。
+ ///
+ public string CustomerPhone { get; init; } = string.Empty;
+
+ ///
+ /// 顾客地址。
+ ///
+ public string CustomerAddress { get; init; } = string.Empty;
+
+ ///
+ /// 商品金额。
+ ///
+ public decimal ItemsAmount { get; init; }
+
+ ///
+ /// 配送费。
+ ///
+ public decimal DeliveryFee { get; init; }
+
+ ///
+ /// 优惠金额。
+ ///
+ public decimal DiscountAmount { get; init; }
+
+ ///
+ /// 实付金额。
+ ///
+ public decimal PaidAmount { get; init; }
+
+ ///
+ /// 应付金额。
+ ///
+ public decimal PayableAmount { get; init; }
+
+ ///
+ /// 下单时间。
+ ///
+ public DateTime OrderedAt { get; init; }
+
+ ///
+ /// 支付时间。
+ ///
+ public DateTime? PaidAt { get; init; }
+
+ ///
+ /// 完成时间。
+ ///
+ public DateTime? FinishedAt { get; init; }
+
+ ///
+ /// 取消时间。
+ ///
+ public DateTime? CancelledAt { get; init; }
+
+ ///
+ /// 备注。
+ ///
+ public string Remark { get; init; } = string.Empty;
+
+ ///
+ /// 商品明细。
+ ///
+ public IReadOnlyList Items { get; init; } = [];
+
+ ///
+ /// 时间线。
+ ///
+ public IReadOnlyList Timeline { get; init; } = [];
+}
+
+///
+/// 全部订单详情商品行 DTO。
+///
+public sealed class OrderAllDetailItemDto
+{
+ ///
+ /// 商品名。
+ ///
+ public string Name { get; init; } = string.Empty;
+
+ ///
+ /// 规格。
+ ///
+ public string Spec { get; init; } = string.Empty;
+
+ ///
+ /// 数量。
+ ///
+ public int Quantity { get; init; }
+
+ ///
+ /// 单价。
+ ///
+ public decimal UnitPrice { get; init; }
+
+ ///
+ /// 小计。
+ ///
+ public decimal SubTotal { get; init; }
+}
+
+///
+/// 全部订单时间线节点 DTO。
+///
+public sealed class OrderAllTimelineNodeDto
+{
+ ///
+ /// 节点名称。
+ ///
+ public string Label { get; init; } = string.Empty;
+
+ ///
+ /// 发生时间。
+ ///
+ public DateTime OccurredAt { get; init; }
+}
+
+///
+/// 全部订单导出 DTO。
+///
+public sealed class OrderAllExportDto
+{
+ ///
+ /// 文件名。
+ ///
+ public string FileName { get; init; } = string.Empty;
+
+ ///
+ /// 文件 Base64。
+ ///
+ public string FileContentBase64 { get; init; } = string.Empty;
+
+ ///
+ /// 总记录数。
+ ///
+ public int TotalCount { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/ExportOrderAllCsvQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/ExportOrderAllCsvQueryHandler.cs
new file mode 100644
index 0000000..bfc5b72
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/ExportOrderAllCsvQueryHandler.cs
@@ -0,0 +1,178 @@
+using System.Globalization;
+using System.Text;
+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;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 全部订单 CSV 导出处理器。
+///
+public sealed class ExportOrderAllCsvQueryHandler(
+ IOrderRepository orderRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(ExportOrderAllCsvQuery request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+
+ var orders = (await orderRepository.SearchAllOrdersAsync(
+ tenantId,
+ request.StoreId,
+ request.StartAt,
+ request.EndAt,
+ request.Status,
+ request.DeliveryType,
+ request.PaymentMethod,
+ request.Keyword,
+ cancellationToken))
+ .OrderByDescending(order => order.CreatedAt)
+ .ToList();
+
+ var refundedIds = await LoadRefundedOrderIdsAsync(orders, tenantId, cancellationToken);
+ if (request.RefundedOnly)
+ {
+ orders = orders.Where(order => refundedIds.Contains(order.Id)).ToList();
+ }
+
+ var itemsLookup = await orderRepository.GetItemsByOrderIdsAsync(
+ orders.Select(order => order.Id).ToList(),
+ tenantId,
+ cancellationToken);
+
+ var csv = BuildCsv(orders, refundedIds, itemsLookup);
+ var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(csv)).ToArray();
+
+ return new OrderAllExportDto
+ {
+ FileName = $"全部订单_{DateTime.UtcNow:yyyyMMddHHmmss}.csv",
+ FileContentBase64 = Convert.ToBase64String(bytes),
+ TotalCount = orders.Count
+ };
+ }
+
+ private async Task> LoadRefundedOrderIdsAsync(
+ IReadOnlyCollection orders,
+ long tenantId,
+ CancellationToken cancellationToken)
+ {
+ var orderIds = orders.Select(order => order.Id).ToList();
+ if (orderIds.Count == 0)
+ {
+ return [];
+ }
+
+ var refunded = await orderRepository.GetRefundedOrderIdsAsync(orderIds, tenantId, cancellationToken);
+ return refunded.ToHashSet();
+ }
+
+ private static string BuildCsv(
+ IReadOnlyList orders,
+ IReadOnlySet refundedIds,
+ IReadOnlyDictionary> itemsLookup)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("订单号,下单时间,渠道,顾客,商品,金额,状态");
+
+ foreach (var order in orders)
+ {
+ var isRefunded = refundedIds.Contains(order.Id);
+ var row = new[]
+ {
+ Escape(order.OrderNo),
+ Escape(order.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)),
+ Escape(ToDeliveryTypeText(order.DeliveryType)),
+ Escape(string.IsNullOrWhiteSpace(order.CustomerName) ? "--" : order.CustomerName),
+ Escape(BuildItemSummary(order.Id, itemsLookup)),
+ Escape(ResolveDisplayAmount(order).ToString("0.00", CultureInfo.InvariantCulture)),
+ Escape(ToStatusText(order.Status, isRefunded, order.DeliveryType))
+ };
+
+ sb.AppendLine(string.Join(',', row));
+ }
+
+ return sb.ToString();
+ }
+
+ private static decimal ResolveDisplayAmount(Order order)
+ {
+ return order.PaidAmount > 0 ? order.PaidAmount : order.PayableAmount;
+ }
+
+ private static string BuildItemSummary(
+ long orderId,
+ IReadOnlyDictionary> itemsLookup)
+ {
+ if (!itemsLookup.TryGetValue(orderId, out var items) || items.Count == 0)
+ {
+ return "--";
+ }
+
+ var first = string.IsNullOrWhiteSpace(items[0].ProductName) ? "商品" : items[0].ProductName.Trim();
+ var totalQuantity = items.Sum(item => Math.Max(0, item.Quantity));
+ if (totalQuantity <= 0)
+ {
+ totalQuantity = items.Count;
+ }
+
+ return $"{first}等{totalQuantity}件";
+ }
+
+ private static string ToStatusText(OrderStatus status, bool isRefunded, DeliveryType deliveryType)
+ {
+ if (isRefunded)
+ {
+ return "已退款";
+ }
+
+ return status switch
+ {
+ OrderStatus.PendingPayment => "待接单",
+ OrderStatus.AwaitingPreparation => "待接单",
+ OrderStatus.InProgress => "制作中",
+ OrderStatus.Ready => ToReadyStatusText(deliveryType),
+ OrderStatus.Completed => "已完成",
+ OrderStatus.Cancelled => "已取消",
+ _ => "未知"
+ };
+ }
+
+ private static string ToReadyStatusText(DeliveryType deliveryType)
+ {
+ return deliveryType switch
+ {
+ DeliveryType.Delivery => "配送中",
+ DeliveryType.Pickup => "待取餐",
+ DeliveryType.DineIn => "待取餐",
+ _ => "待处理"
+ };
+ }
+
+ private static string ToDeliveryTypeText(DeliveryType type)
+ {
+ return type switch
+ {
+ DeliveryType.Delivery => "外卖",
+ DeliveryType.Pickup => "自提",
+ DeliveryType.DineIn => "堂食",
+ _ => "未知"
+ };
+ }
+
+ private static string Escape(string input)
+ {
+ if (!input.Contains('"') && !input.Contains(',') && !input.Contains('\n') && !input.Contains('\r'))
+ {
+ return input;
+ }
+
+ return $"\"{input.Replace("\"", "\"\"")}\"";
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderAllDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderAllDetailQueryHandler.cs
new file mode 100644
index 0000000..b8f18c9
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderAllDetailQueryHandler.cs
@@ -0,0 +1,167 @@
+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;
+using TakeoutSaaS.Domain.Payments.Enums;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 全部订单详情查询处理器。
+///
+public sealed class GetOrderAllDetailQueryHandler(
+ IOrderRepository orderRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(GetOrderAllDetailQuery request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var orderNo = request.OrderNo.Trim();
+ if (string.IsNullOrWhiteSpace(orderNo))
+ {
+ return null;
+ }
+
+ var order = await orderRepository.FindByOrderNoAsync(orderNo, tenantId, cancellationToken);
+ if (order is null)
+ {
+ return null;
+ }
+
+ if (request.StoreId.HasValue && order.StoreId != request.StoreId.Value)
+ {
+ return null;
+ }
+
+ var items = await orderRepository.GetItemsAsync(order.Id, tenantId, cancellationToken);
+ var histories = await orderRepository.GetStatusHistoryAsync(order.Id, tenantId, cancellationToken);
+ var payment = await orderRepository.GetLatestPaymentRecordAsync(order.Id, tenantId, cancellationToken);
+ var delivery = await orderRepository.GetLatestDeliveryOrderAsync(order.Id, tenantId, cancellationToken);
+
+ return new OrderAllDetailDto
+ {
+ OrderId = order.Id,
+ OrderNo = order.OrderNo,
+ StoreId = order.StoreId,
+ DeliveryType = order.DeliveryType,
+ Status = order.Status,
+ PaymentMethod = payment?.Method ?? PaymentMethod.Unknown,
+ CustomerName = string.IsNullOrWhiteSpace(order.CustomerName) ? "--" : order.CustomerName,
+ CustomerPhone = string.IsNullOrWhiteSpace(order.CustomerPhone) ? "--" : order.CustomerPhone,
+ CustomerAddress = "--",
+ ItemsAmount = order.ItemsAmount,
+ DeliveryFee = delivery?.DeliveryFee ?? 0,
+ DiscountAmount = order.DiscountAmount,
+ PayableAmount = order.PayableAmount,
+ PaidAmount = ResolveDisplayAmount(order),
+ OrderedAt = order.CreatedAt,
+ PaidAt = order.PaidAt,
+ FinishedAt = order.FinishedAt,
+ CancelledAt = order.CancelledAt,
+ Remark = order.Remark?.Trim() ?? string.Empty,
+ Items = items.Select(item => new OrderAllDetailItemDto
+ {
+ Name = string.IsNullOrWhiteSpace(item.ProductName) ? "--" : item.ProductName,
+ Spec = string.IsNullOrWhiteSpace(item.SkuName) ? "标准" : item.SkuName,
+ Quantity = item.Quantity,
+ UnitPrice = item.UnitPrice,
+ SubTotal = item.SubTotal
+ }).ToList(),
+ Timeline = BuildTimeline(order, histories)
+ };
+ }
+
+ private static decimal ResolveDisplayAmount(Order order)
+ {
+ return order.PaidAmount > 0 ? order.PaidAmount : order.PayableAmount;
+ }
+
+ private static IReadOnlyList BuildTimeline(
+ Order order,
+ IReadOnlyList histories)
+ {
+ var nodes = histories
+ .OrderBy(history => history.OccurredAt)
+ .Select(history => new OrderAllTimelineNodeDto
+ {
+ Label = ToTimelineLabel(history.Status, order.DeliveryType),
+ OccurredAt = history.OccurredAt
+ })
+ .ToList();
+
+ if (nodes.Count > 0)
+ {
+ return nodes;
+ }
+
+ var fallback = new List
+ {
+ new()
+ {
+ Label = "下单",
+ OccurredAt = order.CreatedAt
+ }
+ };
+
+ if (order.PaidAt.HasValue)
+ {
+ fallback.Add(new OrderAllTimelineNodeDto
+ {
+ Label = "支付成功",
+ OccurredAt = order.PaidAt.Value
+ });
+ }
+
+ if (order.FinishedAt.HasValue)
+ {
+ fallback.Add(new OrderAllTimelineNodeDto
+ {
+ Label = "已完成",
+ OccurredAt = order.FinishedAt.Value
+ });
+ }
+
+ if (order.CancelledAt.HasValue)
+ {
+ fallback.Add(new OrderAllTimelineNodeDto
+ {
+ Label = "已取消",
+ OccurredAt = order.CancelledAt.Value
+ });
+ }
+
+ return fallback
+ .OrderBy(item => item.OccurredAt)
+ .ToList();
+ }
+
+ private static string ToTimelineLabel(OrderStatus status, DeliveryType deliveryType)
+ {
+ return status switch
+ {
+ OrderStatus.PendingPayment => "下单",
+ OrderStatus.AwaitingPreparation => "商家接单",
+ OrderStatus.InProgress => "制作中",
+ OrderStatus.Ready => ToReadyStatusText(deliveryType),
+ OrderStatus.Completed => "已完成",
+ OrderStatus.Cancelled => "已取消",
+ _ => "状态变更"
+ };
+ }
+
+ private static string ToReadyStatusText(DeliveryType deliveryType)
+ {
+ return deliveryType switch
+ {
+ DeliveryType.Delivery => "配送中",
+ DeliveryType.Pickup => "待取餐",
+ DeliveryType.DineIn => "待取餐",
+ _ => "待处理"
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderAllStatsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderAllStatsQueryHandler.cs
new file mode 100644
index 0000000..005ffad
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderAllStatsQueryHandler.cs
@@ -0,0 +1,81 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Application.App.Orders.Queries;
+using TakeoutSaaS.Domain.Orders.Entities;
+using TakeoutSaaS.Domain.Orders.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 全部订单统计查询处理器。
+///
+public sealed class GetOrderAllStatsQueryHandler(
+ IOrderRepository orderRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(GetOrderAllStatsQuery request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+
+ var orders = (await orderRepository.SearchAllOrdersAsync(
+ tenantId,
+ request.StoreId,
+ request.StartAt,
+ request.EndAt,
+ request.Status,
+ request.DeliveryType,
+ request.PaymentMethod,
+ request.Keyword,
+ cancellationToken))
+ .ToList();
+
+ var refundedIds = await LoadRefundedOrderIdsAsync(orders, tenantId, cancellationToken);
+ if (request.RefundedOnly)
+ {
+ orders = orders.Where(order => refundedIds.Contains(order.Id)).ToList();
+ }
+
+ var totalOrders = orders.Count;
+ if (totalOrders == 0)
+ {
+ return new OrderAllStatsDto();
+ }
+
+ var totalAmount = orders.Sum(ResolveDisplayAmount);
+ var averageAmount = decimal.Round(totalAmount / totalOrders, 2, MidpointRounding.AwayFromZero);
+ var refundCount = request.RefundedOnly
+ ? totalOrders
+ : orders.Count(order => refundedIds.Contains(order.Id));
+
+ return new OrderAllStatsDto
+ {
+ TotalOrders = totalOrders,
+ TotalAmount = totalAmount,
+ AverageAmount = averageAmount,
+ RefundCount = refundCount
+ };
+ }
+
+ private async Task> LoadRefundedOrderIdsAsync(
+ IReadOnlyCollection orders,
+ long tenantId,
+ CancellationToken cancellationToken)
+ {
+ var orderIds = orders.Select(order => order.Id).ToList();
+ if (orderIds.Count == 0)
+ {
+ return [];
+ }
+
+ var refunded = await orderRepository.GetRefundedOrderIdsAsync(orderIds, tenantId, cancellationToken);
+ return refunded.ToHashSet();
+ }
+
+ private static decimal ResolveDisplayAmount(Order order)
+ {
+ return order.PaidAmount > 0 ? order.PaidAmount : order.PayableAmount;
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrderAllListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrderAllListQueryHandler.cs
new file mode 100644
index 0000000..6eda490
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrderAllListQueryHandler.cs
@@ -0,0 +1,138 @@
+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;
+using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 全部订单列表查询处理器。
+///
+public sealed class SearchOrderAllListQueryHandler(
+ IOrderRepository orderRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler>
+{
+ ///
+ public async Task> Handle(SearchOrderAllListQuery request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+
+ var orders = await orderRepository.SearchAllOrdersAsync(
+ tenantId,
+ request.StoreId,
+ request.StartAt,
+ request.EndAt,
+ request.Status,
+ request.DeliveryType,
+ request.PaymentMethod,
+ request.Keyword,
+ cancellationToken);
+
+ var filteredOrders = orders.ToList();
+ var refundedSet = await LoadRefundedOrderIdsAsync(filteredOrders, tenantId, cancellationToken);
+ if (request.RefundedOnly)
+ {
+ filteredOrders = filteredOrders
+ .Where(order => refundedSet.Contains(order.Id))
+ .ToList();
+ }
+
+ var sorted = ApplySorting(filteredOrders, request.SortBy, request.SortDescending);
+ var page = Math.Max(1, request.Page);
+ var pageSize = Math.Clamp(request.PageSize, 1, 200);
+ var pagedOrders = sorted
+ .Skip((page - 1) * pageSize)
+ .Take(pageSize)
+ .ToList();
+
+ var itemsLookup = await orderRepository.GetItemsByOrderIdsAsync(
+ pagedOrders.Select(order => order.Id).ToList(),
+ tenantId,
+ cancellationToken);
+
+ var rows = pagedOrders
+ .Select(order =>
+ {
+ var isRefunded = refundedSet.Contains(order.Id);
+ return new OrderAllListItemDto
+ {
+ OrderId = order.Id,
+ OrderNo = order.OrderNo,
+ OrderedAt = order.CreatedAt,
+ DeliveryType = order.DeliveryType,
+ Status = order.Status,
+ CustomerName = string.IsNullOrWhiteSpace(order.CustomerName) ? "--" : order.CustomerName,
+ ItemsSummary = BuildItemSummary(order.Id, itemsLookup),
+ Amount = ResolveDisplayAmount(order),
+ IsRefunded = isRefunded,
+ IsDimmed = order.Status == OrderStatus.Cancelled || isRefunded
+ };
+ })
+ .ToList();
+
+ return new PagedResult(rows, page, pageSize, filteredOrders.Count);
+ }
+
+ private async Task> LoadRefundedOrderIdsAsync(
+ IReadOnlyCollection orders,
+ long tenantId,
+ CancellationToken cancellationToken)
+ {
+ var orderIds = orders.Select(order => order.Id).ToList();
+ if (orderIds.Count == 0)
+ {
+ return [];
+ }
+
+ var refunded = await orderRepository.GetRefundedOrderIdsAsync(orderIds, tenantId, cancellationToken);
+ return refunded.ToHashSet();
+ }
+
+ private static decimal ResolveDisplayAmount(Order order)
+ {
+ return order.PaidAmount > 0 ? order.PaidAmount : order.PayableAmount;
+ }
+
+ private static string BuildItemSummary(
+ long orderId,
+ IReadOnlyDictionary> itemsLookup)
+ {
+ if (!itemsLookup.TryGetValue(orderId, out var items) || items.Count == 0)
+ {
+ return "--";
+ }
+
+ var first = string.IsNullOrWhiteSpace(items[0].ProductName) ? "商品" : items[0].ProductName.Trim();
+ var totalQuantity = items.Sum(item => Math.Max(0, item.Quantity));
+ if (totalQuantity <= 0)
+ {
+ totalQuantity = items.Count;
+ }
+
+ return $"{first}等{totalQuantity}件";
+ }
+
+ private static IEnumerable ApplySorting(
+ IReadOnlyCollection orders,
+ string? sortBy,
+ bool sortDescending)
+ {
+ return sortBy?.Trim().ToLowerInvariant() switch
+ {
+ "amount" => sortDescending
+ ? orders.OrderByDescending(ResolveDisplayAmount).ThenByDescending(order => order.CreatedAt)
+ : orders.OrderBy(ResolveDisplayAmount).ThenBy(order => order.CreatedAt),
+ "status" => sortDescending
+ ? orders.OrderByDescending(order => order.Status).ThenByDescending(order => order.CreatedAt)
+ : orders.OrderBy(order => order.Status).ThenBy(order => order.CreatedAt),
+ _ => sortDescending
+ ? orders.OrderByDescending(order => order.CreatedAt)
+ : orders.OrderBy(order => order.CreatedAt)
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/ExportOrderAllCsvQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/ExportOrderAllCsvQuery.cs
new file mode 100644
index 0000000..33ab0dc
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/ExportOrderAllCsvQuery.cs
@@ -0,0 +1,52 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Payments.Enums;
+
+namespace TakeoutSaaS.Application.App.Orders.Queries;
+
+///
+/// 全部订单 CSV 导出查询。
+///
+public sealed class ExportOrderAllCsvQuery : IRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public long? StoreId { get; init; }
+
+ ///
+ /// 开始时间(含)。
+ ///
+ public DateTime? StartAt { get; init; }
+
+ ///
+ /// 结束时间(不含)。
+ ///
+ public DateTime? EndAt { get; init; }
+
+ ///
+ /// 状态。
+ ///
+ public OrderStatus? Status { get; init; }
+
+ ///
+ /// 是否仅退款。
+ ///
+ public bool RefundedOnly { get; init; }
+
+ ///
+ /// 履约方式。
+ ///
+ public DeliveryType? DeliveryType { get; init; }
+
+ ///
+ /// 支付方式。
+ ///
+ public PaymentMethod? PaymentMethod { get; init; }
+
+ ///
+ /// 关键词。
+ ///
+ public string? Keyword { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderAllDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderAllDetailQuery.cs
new file mode 100644
index 0000000..7c24b89
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderAllDetailQuery.cs
@@ -0,0 +1,20 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+
+namespace TakeoutSaaS.Application.App.Orders.Queries;
+
+///
+/// 全部订单详情查询。
+///
+public sealed class GetOrderAllDetailQuery : IRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public long? StoreId { get; init; }
+
+ ///
+ /// 订单号。
+ ///
+ public string OrderNo { get; init; } = string.Empty;
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderAllStatsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderAllStatsQuery.cs
new file mode 100644
index 0000000..ffc3403
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderAllStatsQuery.cs
@@ -0,0 +1,52 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Payments.Enums;
+
+namespace TakeoutSaaS.Application.App.Orders.Queries;
+
+///
+/// 全部订单统计查询。
+///
+public sealed class GetOrderAllStatsQuery : IRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public long? StoreId { get; init; }
+
+ ///
+ /// 开始时间(含)。
+ ///
+ public DateTime? StartAt { get; init; }
+
+ ///
+ /// 结束时间(不含)。
+ ///
+ public DateTime? EndAt { get; init; }
+
+ ///
+ /// 状态。
+ ///
+ public OrderStatus? Status { get; init; }
+
+ ///
+ /// 是否仅退款。
+ ///
+ public bool RefundedOnly { get; init; }
+
+ ///
+ /// 履约方式。
+ ///
+ public DeliveryType? DeliveryType { get; init; }
+
+ ///
+ /// 支付方式。
+ ///
+ public PaymentMethod? PaymentMethod { get; init; }
+
+ ///
+ /// 关键词。
+ ///
+ public string? Keyword { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrderAllListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrderAllListQuery.cs
new file mode 100644
index 0000000..cc982bb
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrderAllListQuery.cs
@@ -0,0 +1,73 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Payments.Enums;
+using TakeoutSaaS.Shared.Abstractions.Results;
+
+namespace TakeoutSaaS.Application.App.Orders.Queries;
+
+///
+/// 全部订单列表查询。
+///
+public sealed class SearchOrderAllListQuery : IRequest>
+{
+ ///
+ /// 门店 ID。
+ ///
+ public long? StoreId { get; init; }
+
+ ///
+ /// 开始时间(含)。
+ ///
+ public DateTime? StartAt { get; init; }
+
+ ///
+ /// 结束时间(不含)。
+ ///
+ public DateTime? EndAt { get; init; }
+
+ ///
+ /// 状态。
+ ///
+ public OrderStatus? Status { get; init; }
+
+ ///
+ /// 是否仅退款。
+ ///
+ public bool RefundedOnly { get; init; }
+
+ ///
+ /// 履约方式。
+ ///
+ public DeliveryType? DeliveryType { get; init; }
+
+ ///
+ /// 支付方式。
+ ///
+ public PaymentMethod? PaymentMethod { get; init; }
+
+ ///
+ /// 关键词(订单号/手机号)。
+ ///
+ public string? Keyword { get; init; }
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; init; } = 1;
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; init; } = 20;
+
+ ///
+ /// 排序字段。
+ ///
+ public string? SortBy { get; init; }
+
+ ///
+ /// 是否倒序。
+ ///
+ public bool SortDescending { get; init; } = true;
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/ExportOrderAllCsvQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/ExportOrderAllCsvQueryValidator.cs
new file mode 100644
index 0000000..e6dcd7e
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/ExportOrderAllCsvQueryValidator.cs
@@ -0,0 +1,21 @@
+using FluentValidation;
+using TakeoutSaaS.Application.App.Orders.Queries;
+
+namespace TakeoutSaaS.Application.App.Orders.Validators;
+
+///
+/// 全部订单导出查询验证器。
+///
+public sealed class ExportOrderAllCsvQueryValidator : AbstractValidator
+{
+ ///
+ /// 初始化验证规则。
+ ///
+ public ExportOrderAllCsvQueryValidator()
+ {
+ RuleFor(x => x.Keyword).MaximumLength(64);
+ RuleFor(x => x)
+ .Must(x => !x.StartAt.HasValue || !x.EndAt.HasValue || x.StartAt < x.EndAt)
+ .WithMessage("开始时间必须早于结束时间");
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/GetOrderAllDetailQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/GetOrderAllDetailQueryValidator.cs
new file mode 100644
index 0000000..c78ec5d
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/GetOrderAllDetailQueryValidator.cs
@@ -0,0 +1,18 @@
+using FluentValidation;
+using TakeoutSaaS.Application.App.Orders.Queries;
+
+namespace TakeoutSaaS.Application.App.Orders.Validators;
+
+///
+/// 全部订单详情查询验证器。
+///
+public sealed class GetOrderAllDetailQueryValidator : AbstractValidator
+{
+ ///
+ /// 初始化验证规则。
+ ///
+ public GetOrderAllDetailQueryValidator()
+ {
+ RuleFor(x => x.OrderNo).NotEmpty().MaximumLength(32);
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/GetOrderAllStatsQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/GetOrderAllStatsQueryValidator.cs
new file mode 100644
index 0000000..e958a67
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/GetOrderAllStatsQueryValidator.cs
@@ -0,0 +1,21 @@
+using FluentValidation;
+using TakeoutSaaS.Application.App.Orders.Queries;
+
+namespace TakeoutSaaS.Application.App.Orders.Validators;
+
+///
+/// 全部订单统计查询验证器。
+///
+public sealed class GetOrderAllStatsQueryValidator : AbstractValidator
+{
+ ///
+ /// 初始化验证规则。
+ ///
+ public GetOrderAllStatsQueryValidator()
+ {
+ RuleFor(x => x.Keyword).MaximumLength(64);
+ RuleFor(x => x)
+ .Must(x => !x.StartAt.HasValue || !x.EndAt.HasValue || x.StartAt < x.EndAt)
+ .WithMessage("开始时间必须早于结束时间");
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/SearchOrderAllListQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/SearchOrderAllListQueryValidator.cs
new file mode 100644
index 0000000..07d45d8
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/SearchOrderAllListQueryValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using TakeoutSaaS.Application.App.Orders.Queries;
+
+namespace TakeoutSaaS.Application.App.Orders.Validators;
+
+///
+/// 全部订单列表查询验证器。
+///
+public sealed class SearchOrderAllListQueryValidator : AbstractValidator
+{
+ ///
+ /// 初始化验证规则。
+ ///
+ public SearchOrderAllListQueryValidator()
+ {
+ RuleFor(x => x.Page).GreaterThan(0);
+ RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
+ RuleFor(x => x.Keyword).MaximumLength(64);
+ RuleFor(x => x.SortBy).MaximumLength(64);
+ RuleFor(x => x)
+ .Must(x => !x.StartAt.HasValue || !x.EndAt.HasValue || x.StartAt < x.EndAt)
+ .WithMessage("开始时间必须早于结束时间");
+ }
+}
diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs
index 96a3bd6..6e6510e 100644
--- a/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs
@@ -1,5 +1,7 @@
using TakeoutSaaS.Domain.Orders.Entities;
using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Deliveries.Entities;
+using TakeoutSaaS.Domain.Payments.Entities;
using TakeoutSaaS.Domain.Payments.Enums;
namespace TakeoutSaaS.Domain.Orders.Repositories;
@@ -37,6 +39,30 @@ public interface IOrderRepository
/// 订单集合。
Task> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default);
+ ///
+ /// 全部订单列表筛选查询。
+ ///
+ /// 租户 ID。
+ /// 门店 ID。
+ /// 开始时间(含)。
+ /// 结束时间(不含)。
+ /// 订单状态。
+ /// 履约方式。
+ /// 支付方式。
+ /// 关键词(订单号/手机号)。
+ /// 取消标记。
+ /// 订单集合。
+ Task> SearchAllOrdersAsync(
+ long tenantId,
+ long? storeId,
+ DateTime? startAt,
+ DateTime? endAt,
+ OrderStatus? status,
+ DeliveryType? deliveryType,
+ PaymentMethod? paymentMethod,
+ string? keyword,
+ CancellationToken cancellationToken = default);
+
///
/// 获取订单明细行。
///
@@ -46,6 +72,18 @@ public interface IOrderRepository
/// 订单明细集合。
Task> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
+ ///
+ /// 按订单集合读取明细。
+ ///
+ /// 订单 ID 集合。
+ /// 租户 ID。
+ /// 取消标记。
+ /// 订单明细字典。
+ Task>> GetItemsByOrderIdsAsync(
+ IReadOnlyCollection orderIds,
+ long tenantId,
+ CancellationToken cancellationToken = default);
+
///
/// 获取订单状态流转记录。
///
@@ -64,6 +102,36 @@ public interface IOrderRepository
/// 退款申请列表。
Task> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
+ ///
+ /// 按订单集合查询已退款订单 ID。
+ ///
+ /// 订单 ID 集合。
+ /// 租户 ID。
+ /// 取消标记。
+ /// 已退款订单 ID 集合。
+ Task> GetRefundedOrderIdsAsync(
+ IReadOnlyCollection orderIds,
+ long tenantId,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// 获取订单最近支付记录。
+ ///
+ /// 订单 ID。
+ /// 租户 ID。
+ /// 取消标记。
+ /// 支付记录或 null。
+ Task GetLatestPaymentRecordAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 获取订单最近配送单。
+ ///
+ /// 订单 ID。
+ /// 租户 ID。
+ /// 取消标记。
+ /// 配送单或 null。
+ Task GetLatestDeliveryOrderAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
+
///
/// 新增订单。
///
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs
index ba663d4..aac5988 100644
--- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs
@@ -1,7 +1,9 @@
using Microsoft.EntityFrameworkCore;
+using TakeoutSaaS.Domain.Deliveries.Entities;
using TakeoutSaaS.Domain.Orders.Entities;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Domain.Orders.Repositories;
+using TakeoutSaaS.Domain.Payments.Entities;
using TakeoutSaaS.Domain.Payments.Enums;
using TakeoutSaaS.Infrastructure.App.Persistence;
@@ -57,6 +59,70 @@ public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepos
return orders;
}
+ ///
+ public async Task> SearchAllOrdersAsync(
+ long tenantId,
+ long? storeId,
+ DateTime? startAt,
+ DateTime? endAt,
+ OrderStatus? status,
+ DeliveryType? deliveryType,
+ PaymentMethod? paymentMethod,
+ string? keyword,
+ CancellationToken cancellationToken = default)
+ {
+ var query = context.Orders
+ .AsNoTracking()
+ .Where(x => x.TenantId == tenantId);
+
+ if (storeId.HasValue)
+ {
+ query = query.Where(x => x.StoreId == storeId.Value);
+ }
+
+ if (startAt.HasValue)
+ {
+ query = query.Where(x => x.CreatedAt >= startAt.Value);
+ }
+
+ if (endAt.HasValue)
+ {
+ query = query.Where(x => x.CreatedAt < endAt.Value);
+ }
+
+ if (status.HasValue)
+ {
+ query = query.Where(x => x.Status == status.Value);
+ }
+
+ if (deliveryType.HasValue)
+ {
+ query = query.Where(x => x.DeliveryType == deliveryType.Value);
+ }
+
+ if (paymentMethod.HasValue)
+ {
+ query = query.Where(x =>
+ context.PaymentRecords.Any(record =>
+ record.TenantId == tenantId &&
+ record.OrderId == x.Id &&
+ record.Method == paymentMethod.Value));
+ }
+
+ if (!string.IsNullOrWhiteSpace(keyword))
+ {
+ var normalized = keyword.Trim();
+ query = query.Where(x =>
+ x.OrderNo.Contains(normalized) ||
+ (x.CustomerPhone != null && x.CustomerPhone.Contains(normalized)));
+ }
+
+ return await query
+ .OrderByDescending(x => x.CreatedAt)
+ .ThenByDescending(x => x.Id)
+ .ToListAsync(cancellationToken);
+ }
+
///
public async Task> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
{
@@ -69,6 +135,29 @@ public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepos
return items;
}
+ ///
+ public async Task>> GetItemsByOrderIdsAsync(
+ IReadOnlyCollection orderIds,
+ long tenantId,
+ CancellationToken cancellationToken = default)
+ {
+ if (orderIds.Count == 0)
+ {
+ return new Dictionary>();
+ }
+
+ var items = await context.OrderItems
+ .AsNoTracking()
+ .Where(x => x.TenantId == tenantId && orderIds.Contains(x.OrderId))
+ .OrderBy(x => x.OrderId)
+ .ThenBy(x => x.Id)
+ .ToListAsync(cancellationToken);
+
+ return items
+ .GroupBy(item => item.OrderId)
+ .ToDictionary(group => group.Key, group => (IReadOnlyList)group.ToList());
+ }
+
///
public async Task> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
{
@@ -93,6 +182,55 @@ public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepos
return refunds;
}
+ ///
+ public async Task> GetRefundedOrderIdsAsync(
+ IReadOnlyCollection orderIds,
+ long tenantId,
+ CancellationToken cancellationToken = default)
+ {
+ if (orderIds.Count == 0)
+ {
+ return new HashSet();
+ }
+
+ var result = await context.RefundRequests
+ .AsNoTracking()
+ .Where(x =>
+ x.TenantId == tenantId &&
+ orderIds.Contains(x.OrderId) &&
+ x.Status == RefundStatus.Refunded)
+ .Select(x => x.OrderId)
+ .Distinct()
+ .ToListAsync(cancellationToken);
+
+ return result.ToHashSet();
+ }
+
+ ///
+ public Task GetLatestPaymentRecordAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
+ {
+ return context.PaymentRecords
+ .AsNoTracking()
+ .Where(x => x.TenantId == tenantId && x.OrderId == orderId)
+ .OrderByDescending(x => x.PaidAt ?? DateTime.MinValue)
+ .ThenByDescending(x => x.CreatedAt)
+ .ThenByDescending(x => x.Id)
+ .FirstOrDefaultAsync(cancellationToken);
+ }
+
+ ///
+ public Task GetLatestDeliveryOrderAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
+ {
+ return context.DeliveryOrders
+ .AsNoTracking()
+ .Where(x => x.TenantId == tenantId && x.OrderId == orderId)
+ .OrderByDescending(x => x.DeliveredAt ?? DateTime.MinValue)
+ .ThenByDescending(x => x.PickedUpAt ?? DateTime.MinValue)
+ .ThenByDescending(x => x.CreatedAt)
+ .ThenByDescending(x => x.Id)
+ .FirstOrDefaultAsync(cancellationToken);
+ }
+
///
public Task AddOrderAsync(Order order, CancellationToken cancellationToken = default)
{