feat(order): add all-orders APIs and query workflow
All checks were successful
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Successful in 1m51s
All checks were successful
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Successful in 1m51s
This commit is contained in:
331
src/Api/TakeoutSaaS.TenantApi/Contracts/Order/OrderContracts.cs
Normal file
331
src/Api/TakeoutSaaS.TenantApi/Contracts/Order/OrderContracts.cs
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Order;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单筛选请求。
|
||||||
|
/// </summary>
|
||||||
|
public class OrderAllFilterRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string? StartDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结束日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string? EndDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态筛选。
|
||||||
|
/// </summary>
|
||||||
|
public string? Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道筛选(delivery/pickup/dine_in)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Channel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式筛选(wechat/alipay/balance/cash/card)。
|
||||||
|
/// </summary>
|
||||||
|
public string? PaymentMethod { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键词(订单号/手机号)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单列表请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllListRequest : OrderAllFilterRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单详情请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllDetailRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单列表结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllListResultResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<OrderAllListItemResponse> Items { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总数。
|
||||||
|
/// </summary>
|
||||||
|
public int Total { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单行。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllListItemResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下单时间。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderedAt { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道。
|
||||||
|
/// </summary>
|
||||||
|
public string Channel { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客。
|
||||||
|
/// </summary>
|
||||||
|
public string Customer { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品摘要。
|
||||||
|
/// </summary>
|
||||||
|
public string ItemsSummary { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否弱化展示。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDimmed { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单统计。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllStatsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalOrders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 平均客单价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal AverageAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退款单数。
|
||||||
|
/// </summary>
|
||||||
|
public int RefundCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单详情。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllDetailResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道。
|
||||||
|
/// </summary>
|
||||||
|
public string Channel { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式。
|
||||||
|
/// </summary>
|
||||||
|
public string PaymentMethod { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下单时间。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderedAt { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间。
|
||||||
|
/// </summary>
|
||||||
|
public string? PaidAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 完成时间。
|
||||||
|
/// </summary>
|
||||||
|
public string? FinishedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客手机号。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerPhone { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 收货地址。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerAddress { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ItemsAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送费。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DeliveryFee { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优惠减免。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DiscountAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实付金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal PaidAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string Remark { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品明细。
|
||||||
|
/// </summary>
|
||||||
|
public List<OrderAllDetailItemResponse> Items { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态时间线。
|
||||||
|
/// </summary>
|
||||||
|
public List<OrderAllTimelineResponse> Timeline { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单商品明细行。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllDetailItemResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规格。
|
||||||
|
/// </summary>
|
||||||
|
public string Spec { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal UnitPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小计。
|
||||||
|
/// </summary>
|
||||||
|
public decimal SubTotal { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单时间线节点。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllTimelineResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 节点文案。
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 时间。
|
||||||
|
/// </summary>
|
||||||
|
public string Time { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单导出回执。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllExportResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件名。
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件 Base64。
|
||||||
|
/// </summary>
|
||||||
|
public string FileContentBase64 { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出记录数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
}
|
||||||
373
src/Api/TakeoutSaaS.TenantApi/Controllers/OrderController.cs
Normal file
373
src/Api/TakeoutSaaS.TenantApi/Controllers/OrderController.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户端订单管理。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/tenant/v{version:apiVersion}/order")]
|
||||||
|
public sealed class OrderController(
|
||||||
|
IMediator mediator,
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
StoreContextService storeContextService) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("all/list")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<OrderAllListResultResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<OrderAllListResultResponse>> 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<OrderAllListResultResponse>.Ok(new OrderAllListResultResponse
|
||||||
|
{
|
||||||
|
Items = result.Items.Select(MapListItem).ToList(),
|
||||||
|
Total = result.TotalCount,
|
||||||
|
Page = result.Page,
|
||||||
|
PageSize = result.PageSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单统计。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("all/stats")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<OrderAllStatsResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<OrderAllStatsResponse>> 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<OrderAllStatsResponse>.Ok(new OrderAllStatsResponse
|
||||||
|
{
|
||||||
|
TotalOrders = result.TotalOrders,
|
||||||
|
TotalAmount = result.TotalAmount,
|
||||||
|
AverageAmount = result.AverageAmount,
|
||||||
|
RefundCount = result.RefundCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单详情。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("all/detail")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<OrderAllDetailResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<OrderAllDetailResponse>> 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<OrderAllDetailResponse>.Error(ErrorCodes.BadRequest, "orderNo 非法");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await mediator.Send(new GetOrderAllDetailQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
OrderNo = orderNo
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
return ApiResponse<OrderAllDetailResponse>.Error(ErrorCodes.NotFound, "订单不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse<OrderAllDetailResponse>.Ok(MapDetail(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单导出。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("all/export")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<OrderAllExportResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<OrderAllExportResponse>> 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<OrderAllExportResponse>.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 => "待取餐",
|
||||||
|
_ => "待处理"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
using TakeoutSaaS.Domain.Orders.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Orders.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单列表行 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllListItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下单时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime OrderedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 履约方式。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryType DeliveryType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客名称。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品摘要。
|
||||||
|
/// </summary>
|
||||||
|
public string ItemsSummary { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Amount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否已退款。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRefunded { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否弱化显示。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDimmed { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单统计 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllStatsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 总订单数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalOrders { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 平均客单价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal AverageAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退款单数。
|
||||||
|
/// </summary>
|
||||||
|
public int RefundCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单详情 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllDetailDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 履约方式。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryType DeliveryType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod PaymentMethod { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客手机号。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerPhone { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客地址。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerAddress { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ItemsAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送费。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DeliveryFee { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优惠金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DiscountAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实付金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal PaidAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应付金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal PayableAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下单时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime OrderedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PaidAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 完成时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FinishedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? CancelledAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string Remark { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品明细。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<OrderAllDetailItemDto> Items { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 时间线。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<OrderAllTimelineNodeDto> Timeline { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单详情商品行 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllDetailItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品名。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规格。
|
||||||
|
/// </summary>
|
||||||
|
public string Spec { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Quantity { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal UnitPrice { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小计。
|
||||||
|
/// </summary>
|
||||||
|
public decimal SubTotal { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单时间线节点 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllTimelineNodeDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 节点名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发生时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime OccurredAt { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单导出 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderAllExportDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件名。
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件 Base64。
|
||||||
|
/// </summary>
|
||||||
|
public string FileContentBase64 { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总记录数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单 CSV 导出处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportOrderAllCsvQueryHandler(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<ExportOrderAllCsvQuery, OrderAllExportDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<OrderAllExportDto> 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<HashSet<long>> LoadRefundedOrderIdsAsync(
|
||||||
|
IReadOnlyCollection<Order> 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<Order> orders,
|
||||||
|
IReadOnlySet<long> refundedIds,
|
||||||
|
IReadOnlyDictionary<long, IReadOnlyList<OrderItem>> 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<long, IReadOnlyList<OrderItem>> 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("\"", "\"\"")}\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单详情查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetOrderAllDetailQueryHandler(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetOrderAllDetailQuery, OrderAllDetailDto?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<OrderAllDetailDto?> 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<OrderAllTimelineNodeDto> BuildTimeline(
|
||||||
|
Order order,
|
||||||
|
IReadOnlyList<OrderStatusHistory> 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<OrderAllTimelineNodeDto>
|
||||||
|
{
|
||||||
|
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 => "待取餐",
|
||||||
|
_ => "待处理"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单统计查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetOrderAllStatsQueryHandler(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetOrderAllStatsQuery, OrderAllStatsDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<OrderAllStatsDto> 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<HashSet<long>> LoadRefundedOrderIdsAsync(
|
||||||
|
IReadOnlyCollection<Order> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchOrderAllListQueryHandler(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SearchOrderAllListQuery, PagedResult<OrderAllListItemDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PagedResult<OrderAllListItemDto>> 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<OrderAllListItemDto>(rows, page, pageSize, filteredOrders.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HashSet<long>> LoadRefundedOrderIdsAsync(
|
||||||
|
IReadOnlyCollection<Order> 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<long, IReadOnlyList<OrderItem>> 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<Order> ApplySorting(
|
||||||
|
IReadOnlyCollection<Order> 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单 CSV 导出查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportOrderAllCsvQuery : IRequest<OrderAllExportDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long? StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始时间(含)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结束时间(不含)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus? Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否仅退款。
|
||||||
|
/// </summary>
|
||||||
|
public bool RefundedOnly { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 履约方式。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryType? DeliveryType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod? PaymentMethod { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键词。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Orders.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Orders.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单详情查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetOrderAllDetailQuery : IRequest<OrderAllDetailDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long? StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单统计查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetOrderAllStatsQuery : IRequest<OrderAllStatsDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long? StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始时间(含)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结束时间(不含)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus? Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否仅退款。
|
||||||
|
/// </summary>
|
||||||
|
public bool RefundedOnly { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 履约方式。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryType? DeliveryType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod? PaymentMethod { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键词。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单列表查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchOrderAllListQuery : IRequest<PagedResult<OrderAllListItemDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long? StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始时间(含)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结束时间(不含)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus? Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否仅退款。
|
||||||
|
/// </summary>
|
||||||
|
public bool RefundedOnly { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 履约方式。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryType? DeliveryType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod? PaymentMethod { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键词(订单号/手机号)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; } = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序字段。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortBy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否倒序。
|
||||||
|
/// </summary>
|
||||||
|
public bool SortDescending { get; init; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Orders.Queries;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Orders.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单导出查询验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportOrderAllCsvQueryValidator : AbstractValidator<ExportOrderAllCsvQuery>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
public ExportOrderAllCsvQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Keyword).MaximumLength(64);
|
||||||
|
RuleFor(x => x)
|
||||||
|
.Must(x => !x.StartAt.HasValue || !x.EndAt.HasValue || x.StartAt < x.EndAt)
|
||||||
|
.WithMessage("开始时间必须早于结束时间");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Orders.Queries;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Orders.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单详情查询验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetOrderAllDetailQueryValidator : AbstractValidator<GetOrderAllDetailQuery>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
public GetOrderAllDetailQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.OrderNo).NotEmpty().MaximumLength(32);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Orders.Queries;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Orders.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单统计查询验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetOrderAllStatsQueryValidator : AbstractValidator<GetOrderAllStatsQuery>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
public GetOrderAllStatsQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Keyword).MaximumLength(64);
|
||||||
|
RuleFor(x => x)
|
||||||
|
.Must(x => !x.StartAt.HasValue || !x.EndAt.HasValue || x.StartAt < x.EndAt)
|
||||||
|
.WithMessage("开始时间必须早于结束时间");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Orders.Queries;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Orders.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单列表查询验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchOrderAllListQueryValidator : AbstractValidator<SearchOrderAllListQuery>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
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("开始时间必须早于结束时间");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using TakeoutSaaS.Domain.Orders.Entities;
|
using TakeoutSaaS.Domain.Orders.Entities;
|
||||||
using TakeoutSaaS.Domain.Orders.Enums;
|
using TakeoutSaaS.Domain.Orders.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Deliveries.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Payments.Entities;
|
||||||
using TakeoutSaaS.Domain.Payments.Enums;
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Domain.Orders.Repositories;
|
namespace TakeoutSaaS.Domain.Orders.Repositories;
|
||||||
@@ -37,6 +39,30 @@ public interface IOrderRepository
|
|||||||
/// <returns>订单集合。</returns>
|
/// <returns>订单集合。</returns>
|
||||||
Task<IReadOnlyList<Order>> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<Order>> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全部订单列表筛选查询。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tenantId">租户 ID。</param>
|
||||||
|
/// <param name="storeId">门店 ID。</param>
|
||||||
|
/// <param name="startAt">开始时间(含)。</param>
|
||||||
|
/// <param name="endAt">结束时间(不含)。</param>
|
||||||
|
/// <param name="status">订单状态。</param>
|
||||||
|
/// <param name="deliveryType">履约方式。</param>
|
||||||
|
/// <param name="paymentMethod">支付方式。</param>
|
||||||
|
/// <param name="keyword">关键词(订单号/手机号)。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>订单集合。</returns>
|
||||||
|
Task<IReadOnlyList<Order>> SearchAllOrdersAsync(
|
||||||
|
long tenantId,
|
||||||
|
long? storeId,
|
||||||
|
DateTime? startAt,
|
||||||
|
DateTime? endAt,
|
||||||
|
OrderStatus? status,
|
||||||
|
DeliveryType? deliveryType,
|
||||||
|
PaymentMethod? paymentMethod,
|
||||||
|
string? keyword,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取订单明细行。
|
/// 获取订单明细行。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -46,6 +72,18 @@ public interface IOrderRepository
|
|||||||
/// <returns>订单明细集合。</returns>
|
/// <returns>订单明细集合。</returns>
|
||||||
Task<IReadOnlyList<OrderItem>> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<OrderItem>> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按订单集合读取明细。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orderIds">订单 ID 集合。</param>
|
||||||
|
/// <param name="tenantId">租户 ID。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>订单明细字典。</returns>
|
||||||
|
Task<IReadOnlyDictionary<long, IReadOnlyList<OrderItem>>> GetItemsByOrderIdsAsync(
|
||||||
|
IReadOnlyCollection<long> orderIds,
|
||||||
|
long tenantId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取订单状态流转记录。
|
/// 获取订单状态流转记录。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -64,6 +102,36 @@ public interface IOrderRepository
|
|||||||
/// <returns>退款申请列表。</returns>
|
/// <returns>退款申请列表。</returns>
|
||||||
Task<IReadOnlyList<RefundRequest>> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<RefundRequest>> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按订单集合查询已退款订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orderIds">订单 ID 集合。</param>
|
||||||
|
/// <param name="tenantId">租户 ID。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>已退款订单 ID 集合。</returns>
|
||||||
|
Task<IReadOnlySet<long>> GetRefundedOrderIdsAsync(
|
||||||
|
IReadOnlyCollection<long> orderIds,
|
||||||
|
long tenantId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取订单最近支付记录。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orderId">订单 ID。</param>
|
||||||
|
/// <param name="tenantId">租户 ID。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>支付记录或 null。</returns>
|
||||||
|
Task<PaymentRecord?> GetLatestPaymentRecordAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取订单最近配送单。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orderId">订单 ID。</param>
|
||||||
|
/// <param name="tenantId">租户 ID。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>配送单或 null。</returns>
|
||||||
|
Task<DeliveryOrder?> GetLatestDeliveryOrderAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 新增订单。
|
/// 新增订单。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Domain.Deliveries.Entities;
|
||||||
using TakeoutSaaS.Domain.Orders.Entities;
|
using TakeoutSaaS.Domain.Orders.Entities;
|
||||||
using TakeoutSaaS.Domain.Orders.Enums;
|
using TakeoutSaaS.Domain.Orders.Enums;
|
||||||
using TakeoutSaaS.Domain.Orders.Repositories;
|
using TakeoutSaaS.Domain.Orders.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Payments.Entities;
|
||||||
using TakeoutSaaS.Domain.Payments.Enums;
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
|
||||||
@@ -57,6 +59,70 @@ public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepos
|
|||||||
return orders;
|
return orders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<Order>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<OrderItem>> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<OrderItem>> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -69,6 +135,29 @@ public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepos
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyDictionary<long, IReadOnlyList<OrderItem>>> GetItemsByOrderIdsAsync(
|
||||||
|
IReadOnlyCollection<long> orderIds,
|
||||||
|
long tenantId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (orderIds.Count == 0)
|
||||||
|
{
|
||||||
|
return new Dictionary<long, IReadOnlyList<OrderItem>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<OrderItem>)group.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<OrderStatusHistory>> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<OrderStatusHistory>> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -93,6 +182,55 @@ public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepos
|
|||||||
return refunds;
|
return refunds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlySet<long>> GetRefundedOrderIdsAsync(
|
||||||
|
IReadOnlyCollection<long> orderIds,
|
||||||
|
long tenantId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (orderIds.Count == 0)
|
||||||
|
{
|
||||||
|
return new HashSet<long>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<PaymentRecord?> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<DeliveryOrder?> 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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task AddOrderAsync(Order order, CancellationToken cancellationToken = default)
|
public Task AddOrderAsync(Order order, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user