feat: 管理端核心实体CRUD补齐

This commit is contained in:
2025-12-02 10:19:35 +08:00
parent 1a01454266
commit 93141fbf0c
75 changed files with 4513 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
using MediatR;
using TakeoutSaaS.Application.App.Orders.Dto;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Domain.Payments.Enums;
namespace TakeoutSaaS.Application.App.Orders.Commands;
/// <summary>
/// 创建订单命令。
/// </summary>
public sealed class CreateOrderCommand : IRequest<OrderDto>
{
/// <summary>
/// 订单号。
/// </summary>
public string OrderNo { get; set; } = string.Empty;
/// <summary>
/// 门店 ID。
/// </summary>
public long StoreId { get; set; }
/// <summary>
/// 渠道。
/// </summary>
public OrderChannel Channel { get; set; } = OrderChannel.MiniProgram;
/// <summary>
/// 履约方式。
/// </summary>
public DeliveryType DeliveryType { get; set; } = DeliveryType.DineIn;
/// <summary>
/// 状态。
/// </summary>
public OrderStatus Status { get; set; } = OrderStatus.PendingPayment;
/// <summary>
/// 支付状态。
/// </summary>
public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid;
/// <summary>
/// 顾客姓名。
/// </summary>
public string? CustomerName { get; set; }
/// <summary>
/// 顾客手机号。
/// </summary>
public string? CustomerPhone { get; set; }
/// <summary>
/// 桌号。
/// </summary>
public string? TableNo { get; set; }
/// <summary>
/// 排队号。
/// </summary>
public string? QueueNumber { get; set; }
/// <summary>
/// 预约 ID。
/// </summary>
public long? ReservationId { get; set; }
/// <summary>
/// 商品金额。
/// </summary>
public decimal ItemsAmount { get; set; }
/// <summary>
/// 优惠金额。
/// </summary>
public decimal DiscountAmount { get; set; }
/// <summary>
/// 应付金额。
/// </summary>
public decimal PayableAmount { get; set; }
/// <summary>
/// 实付金额。
/// </summary>
public decimal PaidAmount { get; set; }
/// <summary>
/// 支付时间。
/// </summary>
public DateTime? PaidAt { get; set; }
/// <summary>
/// 完成时间。
/// </summary>
public DateTime? FinishedAt { get; set; }
/// <summary>
/// 取消时间。
/// </summary>
public DateTime? CancelledAt { get; set; }
/// <summary>
/// 取消原因。
/// </summary>
public string? CancelReason { get; set; }
/// <summary>
/// 备注。
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 明细。
/// </summary>
public IReadOnlyList<OrderItemRequest> Items { get; set; } = Array.Empty<OrderItemRequest>();
}

View File

@@ -0,0 +1,14 @@
using MediatR;
namespace TakeoutSaaS.Application.App.Orders.Commands;
/// <summary>
/// 删除订单命令。
/// </summary>
public sealed class DeleteOrderCommand : IRequest<bool>
{
/// <summary>
/// 订单 ID。
/// </summary>
public long OrderId { get; set; }
}

View File

@@ -0,0 +1,52 @@
namespace TakeoutSaaS.Application.App.Orders.Commands;
/// <summary>
/// 订单明细请求。
/// </summary>
public sealed class OrderItemRequest
{
/// <summary>
/// 商品 ID。
/// </summary>
public long ProductId { get; set; }
/// <summary>
/// 商品名称。
/// </summary>
public string ProductName { get; set; } = string.Empty;
/// <summary>
/// SKU 描述。
/// </summary>
public string? SkuName { get; set; }
/// <summary>
/// 单位。
/// </summary>
public string? Unit { get; set; }
/// <summary>
/// 数量。
/// </summary>
public int Quantity { get; set; }
/// <summary>
/// 单价。
/// </summary>
public decimal UnitPrice { get; set; }
/// <summary>
/// 折扣金额。
/// </summary>
public decimal DiscountAmount { get; set; }
/// <summary>
/// 小计。
/// </summary>
public decimal SubTotal { get; set; }
/// <summary>
/// 属性 JSON。
/// </summary>
public string? AttributesJson { get; set; }
}

View File

@@ -0,0 +1,117 @@
using MediatR;
using TakeoutSaaS.Application.App.Orders.Dto;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Domain.Payments.Enums;
namespace TakeoutSaaS.Application.App.Orders.Commands;
/// <summary>
/// 更新订单命令。
/// </summary>
public sealed class UpdateOrderCommand : IRequest<OrderDto?>
{
/// <summary>
/// 订单 ID。
/// </summary>
public long OrderId { get; set; }
/// <summary>
/// 订单号。
/// </summary>
public string OrderNo { get; set; } = string.Empty;
/// <summary>
/// 门店 ID。
/// </summary>
public long StoreId { get; set; }
/// <summary>
/// 渠道。
/// </summary>
public OrderChannel Channel { get; set; } = OrderChannel.MiniProgram;
/// <summary>
/// 履约方式。
/// </summary>
public DeliveryType DeliveryType { get; set; } = DeliveryType.DineIn;
/// <summary>
/// 状态。
/// </summary>
public OrderStatus Status { get; set; } = OrderStatus.PendingPayment;
/// <summary>
/// 支付状态。
/// </summary>
public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid;
/// <summary>
/// 顾客姓名。
/// </summary>
public string? CustomerName { get; set; }
/// <summary>
/// 顾客手机号。
/// </summary>
public string? CustomerPhone { get; set; }
/// <summary>
/// 桌号。
/// </summary>
public string? TableNo { get; set; }
/// <summary>
/// 排队号。
/// </summary>
public string? QueueNumber { get; set; }
/// <summary>
/// 预约 ID。
/// </summary>
public long? ReservationId { get; set; }
/// <summary>
/// 商品金额。
/// </summary>
public decimal ItemsAmount { get; set; }
/// <summary>
/// 优惠金额。
/// </summary>
public decimal DiscountAmount { get; set; }
/// <summary>
/// 应付金额。
/// </summary>
public decimal PayableAmount { get; set; }
/// <summary>
/// 实付金额。
/// </summary>
public decimal PaidAmount { get; set; }
/// <summary>
/// 支付时间。
/// </summary>
public DateTime? PaidAt { get; set; }
/// <summary>
/// 完成时间。
/// </summary>
public DateTime? FinishedAt { get; set; }
/// <summary>
/// 取消时间。
/// </summary>
public DateTime? CancelledAt { get; set; }
/// <summary>
/// 取消原因。
/// </summary>
public string? CancelReason { get; set; }
/// <summary>
/// 备注。
/// </summary>
public string? Remark { get; set; }
}

View File

@@ -0,0 +1,141 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Domain.Payments.Enums;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.App.Orders.Dto;
/// <summary>
/// 订单 DTO。
/// </summary>
public sealed class OrderDto
{
/// <summary>
/// 订单 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 租户 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
/// <summary>
/// 订单号。
/// </summary>
public string OrderNo { get; init; } = string.Empty;
/// <summary>
/// 门店 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long StoreId { get; init; }
/// <summary>
/// 渠道。
/// </summary>
public OrderChannel Channel { get; init; }
/// <summary>
/// 履约方式。
/// </summary>
public DeliveryType DeliveryType { get; init; }
/// <summary>
/// 状态。
/// </summary>
public OrderStatus Status { get; init; }
/// <summary>
/// 支付状态。
/// </summary>
public PaymentStatus PaymentStatus { get; init; }
/// <summary>
/// 顾客姓名。
/// </summary>
public string? CustomerName { get; init; }
/// <summary>
/// 顾客手机号。
/// </summary>
public string? CustomerPhone { get; init; }
/// <summary>
/// 桌号。
/// </summary>
public string? TableNo { get; init; }
/// <summary>
/// 排队号。
/// </summary>
public string? QueueNumber { get; init; }
/// <summary>
/// 预约 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long? ReservationId { get; init; }
/// <summary>
/// 商品金额。
/// </summary>
public decimal ItemsAmount { get; init; }
/// <summary>
/// 优惠金额。
/// </summary>
public decimal DiscountAmount { get; init; }
/// <summary>
/// 应付金额。
/// </summary>
public decimal PayableAmount { get; init; }
/// <summary>
/// 实付金额。
/// </summary>
public decimal PaidAmount { 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? CancelReason { get; init; }
/// <summary>
/// 备注。
/// </summary>
public string? Remark { get; init; }
/// <summary>
/// 明细。
/// </summary>
public IReadOnlyList<OrderItemDto> Items { get; init; } = Array.Empty<OrderItemDto>();
/// <summary>
/// 状态流转。
/// </summary>
public IReadOnlyList<OrderStatusHistoryDto> StatusHistory { get; init; } = Array.Empty<OrderStatusHistoryDto>();
/// <summary>
/// 退款申请。
/// </summary>
public IReadOnlyList<RefundRequestDto> Refunds { get; init; } = Array.Empty<RefundRequestDto>();
}

View File

@@ -0,0 +1,68 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.App.Orders.Dto;
/// <summary>
/// 订单明细 DTO。
/// </summary>
public sealed class OrderItemDto
{
/// <summary>
/// 明细 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 订单 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long OrderId { get; init; }
/// <summary>
/// 商品 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long ProductId { get; init; }
/// <summary>
/// 商品名称。
/// </summary>
public string ProductName { get; init; } = string.Empty;
/// <summary>
/// SKU 描述。
/// </summary>
public string? SkuName { get; init; }
/// <summary>
/// 单位。
/// </summary>
public string? Unit { get; init; }
/// <summary>
/// 数量。
/// </summary>
public int Quantity { get; init; }
/// <summary>
/// 单价。
/// </summary>
public decimal UnitPrice { get; init; }
/// <summary>
/// 折扣金额。
/// </summary>
public decimal DiscountAmount { get; init; }
/// <summary>
/// 小计。
/// </summary>
public decimal SubTotal { get; init; }
/// <summary>
/// 属性 JSON。
/// </summary>
public string? AttributesJson { get; init; }
}

View File

@@ -0,0 +1,44 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.App.Orders.Dto;
/// <summary>
/// 订单状态流转 DTO。
/// </summary>
public sealed class OrderStatusHistoryDto
{
/// <summary>
/// 记录 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 订单 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long OrderId { get; init; }
/// <summary>
/// 状态。
/// </summary>
public OrderStatus Status { get; init; }
/// <summary>
/// 操作人。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long? OperatorId { get; init; }
/// <summary>
/// 备注。
/// </summary>
public string? Notes { get; init; }
/// <summary>
/// 时间。
/// </summary>
public DateTime OccurredAt { get; init; }
}

View File

@@ -0,0 +1,58 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.App.Orders.Dto;
/// <summary>
/// 退款申请 DTO。
/// </summary>
public sealed class RefundRequestDto
{
/// <summary>
/// 退款 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 订单 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long OrderId { get; init; }
/// <summary>
/// 退款单号。
/// </summary>
public string RefundNo { get; init; } = string.Empty;
/// <summary>
/// 金额。
/// </summary>
public decimal Amount { get; init; }
/// <summary>
/// 原因。
/// </summary>
public string Reason { get; init; } = string.Empty;
/// <summary>
/// 状态。
/// </summary>
public RefundStatus Status { get; init; }
/// <summary>
/// 申请时间。
/// </summary>
public DateTime RequestedAt { get; init; }
/// <summary>
/// 处理时间。
/// </summary>
public DateTime? ProcessedAt { get; init; }
/// <summary>
/// 审核备注。
/// </summary>
public string? ReviewNotes { get; init; }
}

View File

@@ -0,0 +1,156 @@
using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.Orders.Commands;
using TakeoutSaaS.Application.App.Orders.Dto;
using TakeoutSaaS.Domain.Orders.Entities;
using TakeoutSaaS.Domain.Orders.Repositories;
using TakeoutSaaS.Shared.Abstractions.Ids;
namespace TakeoutSaaS.Application.App.Orders.Handlers;
/// <summary>
/// 创建订单命令处理器。
/// </summary>
public sealed class CreateOrderCommandHandler(
IOrderRepository orderRepository,
IIdGenerator idGenerator,
ILogger<CreateOrderCommandHandler> logger)
: IRequestHandler<CreateOrderCommand, OrderDto>
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly IIdGenerator _idGenerator = idGenerator;
private readonly ILogger<CreateOrderCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<OrderDto> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
// 1. 构建订单
var order = new Order
{
Id = _idGenerator.NextId(),
OrderNo = request.OrderNo.Trim(),
StoreId = request.StoreId,
Channel = request.Channel,
DeliveryType = request.DeliveryType,
Status = request.Status,
PaymentStatus = request.PaymentStatus,
CustomerName = request.CustomerName?.Trim(),
CustomerPhone = request.CustomerPhone?.Trim(),
TableNo = request.TableNo?.Trim(),
QueueNumber = request.QueueNumber?.Trim(),
ReservationId = request.ReservationId,
ItemsAmount = request.ItemsAmount,
DiscountAmount = request.DiscountAmount,
PayableAmount = request.PayableAmount,
PaidAmount = request.PaidAmount,
PaidAt = request.PaidAt,
FinishedAt = request.FinishedAt,
CancelledAt = request.CancelledAt,
CancelReason = request.CancelReason?.Trim(),
Remark = request.Remark?.Trim()
};
// 2. 构建明细
var items = request.Items.Select(item => new OrderItem
{
OrderId = order.Id,
ProductId = item.ProductId,
ProductName = item.ProductName.Trim(),
SkuName = item.SkuName?.Trim(),
Unit = item.Unit?.Trim(),
Quantity = item.Quantity,
UnitPrice = item.UnitPrice,
DiscountAmount = item.DiscountAmount,
SubTotal = item.SubTotal,
AttributesJson = item.AttributesJson?.Trim()
}).ToList();
// 3. 补充金额字段
if (items.Count > 0)
{
var itemsAmount = items.Sum(x => x.SubTotal);
order.ItemsAmount = itemsAmount;
if (order.PayableAmount <= 0)
{
order.PayableAmount = itemsAmount - order.DiscountAmount;
}
}
// 4. 持久化
await _orderRepository.AddOrderAsync(order, cancellationToken);
if (items.Count > 0)
{
await _orderRepository.AddItemsAsync(items, cancellationToken);
}
await _orderRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("创建订单 {OrderNo} ({OrderId})", order.OrderNo, order.Id);
// 5. 返回 DTO
return MapToDto(order, items, [], []);
}
private static OrderDto MapToDto(
Order order,
IReadOnlyList<OrderItem> items,
IReadOnlyList<OrderStatusHistory> histories,
IReadOnlyList<RefundRequest> refunds) => new()
{
Id = order.Id,
TenantId = order.TenantId,
OrderNo = order.OrderNo,
StoreId = order.StoreId,
Channel = order.Channel,
DeliveryType = order.DeliveryType,
Status = order.Status,
PaymentStatus = order.PaymentStatus,
CustomerName = order.CustomerName,
CustomerPhone = order.CustomerPhone,
TableNo = order.TableNo,
QueueNumber = order.QueueNumber,
ReservationId = order.ReservationId,
ItemsAmount = order.ItemsAmount,
DiscountAmount = order.DiscountAmount,
PayableAmount = order.PayableAmount,
PaidAmount = order.PaidAmount,
PaidAt = order.PaidAt,
FinishedAt = order.FinishedAt,
CancelledAt = order.CancelledAt,
CancelReason = order.CancelReason,
Remark = order.Remark,
Items = items.Select(x => new OrderItemDto
{
Id = x.Id,
OrderId = x.OrderId,
ProductId = x.ProductId,
ProductName = x.ProductName,
SkuName = x.SkuName,
Unit = x.Unit,
Quantity = x.Quantity,
UnitPrice = x.UnitPrice,
DiscountAmount = x.DiscountAmount,
SubTotal = x.SubTotal,
AttributesJson = x.AttributesJson
}).ToList(),
StatusHistory = histories.Select(x => new OrderStatusHistoryDto
{
Id = x.Id,
OrderId = x.OrderId,
Status = x.Status,
OperatorId = x.OperatorId,
Notes = x.Notes,
OccurredAt = x.OccurredAt
}).ToList(),
Refunds = refunds.Select(x => new RefundRequestDto
{
Id = x.Id,
OrderId = x.OrderId,
RefundNo = x.RefundNo,
Amount = x.Amount,
Reason = x.Reason,
Status = x.Status,
RequestedAt = x.RequestedAt,
ProcessedAt = x.ProcessedAt,
ReviewNotes = x.ReviewNotes
}).ToList()
};
}

View File

@@ -0,0 +1,40 @@
using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.Orders.Commands;
using TakeoutSaaS.Domain.Orders.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Orders.Handlers;
/// <summary>
/// 删除订单命令处理器。
/// </summary>
public sealed class DeleteOrderCommandHandler(
IOrderRepository orderRepository,
ITenantProvider tenantProvider,
ILogger<DeleteOrderCommandHandler> logger)
: IRequestHandler<DeleteOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ILogger<DeleteOrderCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<bool> Handle(DeleteOrderCommand request, CancellationToken cancellationToken)
{
// 1. 校验存在性
var tenantId = _tenantProvider.GetCurrentTenantId();
var existing = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken);
if (existing == null)
{
return false;
}
// 2. 删除
await _orderRepository.DeleteOrderAsync(request.OrderId, tenantId, cancellationToken);
await _orderRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("删除订单 {OrderId}", request.OrderId);
return true;
}
}

View File

@@ -0,0 +1,102 @@
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 GetOrderByIdQueryHandler(
IOrderRepository orderRepository,
ITenantProvider tenantProvider)
: IRequestHandler<GetOrderByIdQuery, OrderDto?>
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<OrderDto?> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var order = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken);
if (order == null)
{
return null;
}
var items = await _orderRepository.GetItemsAsync(order.Id, tenantId, cancellationToken);
var histories = await _orderRepository.GetStatusHistoryAsync(order.Id, tenantId, cancellationToken);
var refunds = await _orderRepository.GetRefundsAsync(order.Id, tenantId, cancellationToken);
return MapToDto(order, items, histories, refunds);
}
private static OrderDto MapToDto(
Order order,
IReadOnlyList<OrderItem> items,
IReadOnlyList<OrderStatusHistory> histories,
IReadOnlyList<RefundRequest> refunds) => new()
{
Id = order.Id,
TenantId = order.TenantId,
OrderNo = order.OrderNo,
StoreId = order.StoreId,
Channel = order.Channel,
DeliveryType = order.DeliveryType,
Status = order.Status,
PaymentStatus = order.PaymentStatus,
CustomerName = order.CustomerName,
CustomerPhone = order.CustomerPhone,
TableNo = order.TableNo,
QueueNumber = order.QueueNumber,
ReservationId = order.ReservationId,
ItemsAmount = order.ItemsAmount,
DiscountAmount = order.DiscountAmount,
PayableAmount = order.PayableAmount,
PaidAmount = order.PaidAmount,
PaidAt = order.PaidAt,
FinishedAt = order.FinishedAt,
CancelledAt = order.CancelledAt,
CancelReason = order.CancelReason,
Remark = order.Remark,
Items = items.Select(x => new OrderItemDto
{
Id = x.Id,
OrderId = x.OrderId,
ProductId = x.ProductId,
ProductName = x.ProductName,
SkuName = x.SkuName,
Unit = x.Unit,
Quantity = x.Quantity,
UnitPrice = x.UnitPrice,
DiscountAmount = x.DiscountAmount,
SubTotal = x.SubTotal,
AttributesJson = x.AttributesJson
}).ToList(),
StatusHistory = histories.Select(x => new OrderStatusHistoryDto
{
Id = x.Id,
OrderId = x.OrderId,
Status = x.Status,
OperatorId = x.OperatorId,
Notes = x.Notes,
OccurredAt = x.OccurredAt
}).ToList(),
Refunds = refunds.Select(x => new RefundRequestDto
{
Id = x.Id,
OrderId = x.OrderId,
RefundNo = x.RefundNo,
Amount = x.Amount,
Reason = x.Reason,
Status = x.Status,
RequestedAt = x.RequestedAt,
ProcessedAt = x.ProcessedAt,
ReviewNotes = x.ReviewNotes
}).ToList()
};
}

View File

@@ -0,0 +1,65 @@
using MediatR;
using TakeoutSaaS.Application.App.Orders.Dto;
using TakeoutSaaS.Application.App.Orders.Queries;
using TakeoutSaaS.Domain.Orders.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Orders.Handlers;
/// <summary>
/// 订单列表查询处理器。
/// </summary>
public sealed class SearchOrdersQueryHandler(
IOrderRepository orderRepository,
ITenantProvider tenantProvider)
: IRequestHandler<SearchOrdersQuery, IReadOnlyList<OrderDto>>
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<IReadOnlyList<OrderDto>> Handle(SearchOrdersQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var orders = await _orderRepository.SearchAsync(tenantId, request.Status, request.PaymentStatus, cancellationToken);
if (request.StoreId.HasValue)
{
orders = orders.Where(x => x.StoreId == request.StoreId.Value).ToList();
}
if (!string.IsNullOrWhiteSpace(request.OrderNo))
{
var orderNo = request.OrderNo.Trim();
orders = orders
.Where(x => x.OrderNo.Contains(orderNo, StringComparison.OrdinalIgnoreCase))
.ToList();
}
return orders.Select(order => new OrderDto
{
Id = order.Id,
TenantId = order.TenantId,
OrderNo = order.OrderNo,
StoreId = order.StoreId,
Channel = order.Channel,
DeliveryType = order.DeliveryType,
Status = order.Status,
PaymentStatus = order.PaymentStatus,
CustomerName = order.CustomerName,
CustomerPhone = order.CustomerPhone,
TableNo = order.TableNo,
QueueNumber = order.QueueNumber,
ReservationId = order.ReservationId,
ItemsAmount = order.ItemsAmount,
DiscountAmount = order.DiscountAmount,
PayableAmount = order.PayableAmount,
PaidAmount = order.PaidAmount,
PaidAt = order.PaidAt,
FinishedAt = order.FinishedAt,
CancelledAt = order.CancelledAt,
CancelReason = order.CancelReason,
Remark = order.Remark
}).ToList();
}
}

View File

@@ -0,0 +1,134 @@
using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.Orders.Commands;
using TakeoutSaaS.Application.App.Orders.Dto;
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 UpdateOrderCommandHandler(
IOrderRepository orderRepository,
ITenantProvider tenantProvider,
ILogger<UpdateOrderCommandHandler> logger)
: IRequestHandler<UpdateOrderCommand, OrderDto?>
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ILogger<UpdateOrderCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<OrderDto?> Handle(UpdateOrderCommand request, CancellationToken cancellationToken)
{
// 1. 读取订单
var tenantId = _tenantProvider.GetCurrentTenantId();
var existing = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken);
if (existing == null)
{
return null;
}
// 2. 更新字段
existing.OrderNo = request.OrderNo.Trim();
existing.StoreId = request.StoreId;
existing.Channel = request.Channel;
existing.DeliveryType = request.DeliveryType;
existing.Status = request.Status;
existing.PaymentStatus = request.PaymentStatus;
existing.CustomerName = request.CustomerName?.Trim();
existing.CustomerPhone = request.CustomerPhone?.Trim();
existing.TableNo = request.TableNo?.Trim();
existing.QueueNumber = request.QueueNumber?.Trim();
existing.ReservationId = request.ReservationId;
existing.ItemsAmount = request.ItemsAmount;
existing.DiscountAmount = request.DiscountAmount;
existing.PayableAmount = request.PayableAmount;
existing.PaidAmount = request.PaidAmount;
existing.PaidAt = request.PaidAt;
existing.FinishedAt = request.FinishedAt;
existing.CancelledAt = request.CancelledAt;
existing.CancelReason = request.CancelReason?.Trim();
existing.Remark = request.Remark?.Trim();
// 3. 持久化
await _orderRepository.UpdateOrderAsync(existing, cancellationToken);
await _orderRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("更新订单 {OrderNo} ({OrderId})", existing.OrderNo, existing.Id);
// 4. 读取关联数据并返回
var items = await _orderRepository.GetItemsAsync(existing.Id, tenantId, cancellationToken);
var histories = await _orderRepository.GetStatusHistoryAsync(existing.Id, tenantId, cancellationToken);
var refunds = await _orderRepository.GetRefundsAsync(existing.Id, tenantId, cancellationToken);
return MapToDto(existing, items, histories, refunds);
}
private static OrderDto MapToDto(
Order order,
IReadOnlyList<OrderItem> items,
IReadOnlyList<OrderStatusHistory> histories,
IReadOnlyList<RefundRequest> refunds) => new()
{
Id = order.Id,
TenantId = order.TenantId,
OrderNo = order.OrderNo,
StoreId = order.StoreId,
Channel = order.Channel,
DeliveryType = order.DeliveryType,
Status = order.Status,
PaymentStatus = order.PaymentStatus,
CustomerName = order.CustomerName,
CustomerPhone = order.CustomerPhone,
TableNo = order.TableNo,
QueueNumber = order.QueueNumber,
ReservationId = order.ReservationId,
ItemsAmount = order.ItemsAmount,
DiscountAmount = order.DiscountAmount,
PayableAmount = order.PayableAmount,
PaidAmount = order.PaidAmount,
PaidAt = order.PaidAt,
FinishedAt = order.FinishedAt,
CancelledAt = order.CancelledAt,
CancelReason = order.CancelReason,
Remark = order.Remark,
Items = items.Select(x => new OrderItemDto
{
Id = x.Id,
OrderId = x.OrderId,
ProductId = x.ProductId,
ProductName = x.ProductName,
SkuName = x.SkuName,
Unit = x.Unit,
Quantity = x.Quantity,
UnitPrice = x.UnitPrice,
DiscountAmount = x.DiscountAmount,
SubTotal = x.SubTotal,
AttributesJson = x.AttributesJson
}).ToList(),
StatusHistory = histories.Select(x => new OrderStatusHistoryDto
{
Id = x.Id,
OrderId = x.OrderId,
Status = x.Status,
OperatorId = x.OperatorId,
Notes = x.Notes,
OccurredAt = x.OccurredAt
}).ToList(),
Refunds = refunds.Select(x => new RefundRequestDto
{
Id = x.Id,
OrderId = x.OrderId,
RefundNo = x.RefundNo,
Amount = x.Amount,
Reason = x.Reason,
Status = x.Status,
RequestedAt = x.RequestedAt,
ProcessedAt = x.ProcessedAt,
ReviewNotes = x.ReviewNotes
}).ToList()
};
}

View File

@@ -0,0 +1,15 @@
using MediatR;
using TakeoutSaaS.Application.App.Orders.Dto;
namespace TakeoutSaaS.Application.App.Orders.Queries;
/// <summary>
/// 获取订单详情查询。
/// </summary>
public sealed class GetOrderByIdQuery : IRequest<OrderDto?>
{
/// <summary>
/// 订单 ID。
/// </summary>
public long OrderId { get; init; }
}

View File

@@ -0,0 +1,32 @@
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 SearchOrdersQuery : IRequest<IReadOnlyList<OrderDto>>
{
/// <summary>
/// 门店 ID可选
/// </summary>
public long? StoreId { get; init; }
/// <summary>
/// 订单状态。
/// </summary>
public OrderStatus? Status { get; init; }
/// <summary>
/// 支付状态。
/// </summary>
public PaymentStatus? PaymentStatus { get; init; }
/// <summary>
/// 订单号(模糊或精确,由调用方控制)。
/// </summary>
public string? OrderNo { get; init; }
}