diff --git a/src/Api/TakeoutSaaS.TenantApi/Consumers/OrderCreatedConsumer.cs b/src/Api/TakeoutSaaS.TenantApi/Consumers/OrderCreatedConsumer.cs
new file mode 100644
index 0000000..d771bcf
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Consumers/OrderCreatedConsumer.cs
@@ -0,0 +1,35 @@
+using MassTransit;
+using Microsoft.AspNetCore.SignalR;
+using TakeoutSaaS.Application.Messaging.Events;
+using TakeoutSaaS.TenantApi.Hubs;
+
+namespace TakeoutSaaS.TenantApi.Consumers;
+
+///
+/// 订单创建事件消费者 — 推送新订单到看板。
+///
+public sealed class OrderCreatedConsumer(IHubContext hubContext)
+ : IConsumer
+{
+ ///
+ public async Task Consume(ConsumeContext context)
+ {
+ var e = context.Message;
+ var group = $"store:{e.TenantId}:{e.StoreId}";
+
+ // 1. 推送新订单到对应门店 Group
+ await hubContext.Clients.Group(group).SendAsync("NewOrder", new
+ {
+ e.OrderId,
+ e.OrderNo,
+ e.Amount,
+ e.StoreId,
+ e.Channel,
+ e.DeliveryType,
+ e.CustomerName,
+ e.ItemsSummary,
+ e.TableNo,
+ e.CreatedAt
+ }, context.CancellationToken);
+ }
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Consumers/OrderStatusChangedConsumer.cs b/src/Api/TakeoutSaaS.TenantApi/Consumers/OrderStatusChangedConsumer.cs
new file mode 100644
index 0000000..500e11b
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Consumers/OrderStatusChangedConsumer.cs
@@ -0,0 +1,35 @@
+using MassTransit;
+using Microsoft.AspNetCore.SignalR;
+using TakeoutSaaS.Application.Messaging.Events;
+using TakeoutSaaS.TenantApi.Hubs;
+
+namespace TakeoutSaaS.TenantApi.Consumers;
+
+///
+/// 订单状态变更事件消费者 — 推送状态更新到看板。
+///
+public sealed class OrderStatusChangedConsumer(IHubContext hubContext)
+ : IConsumer
+{
+ ///
+ public async Task Consume(ConsumeContext context)
+ {
+ var e = context.Message;
+ var group = $"store:{e.TenantId}:{e.StoreId}";
+
+ // 1. 推送状态变更到对应门店 Group
+ await hubContext.Clients.Group(group).SendAsync("OrderStatusChanged", new
+ {
+ e.OrderId,
+ e.OrderNo,
+ e.OldStatus,
+ e.NewStatus,
+ e.Channel,
+ e.DeliveryType,
+ e.CustomerName,
+ e.ItemsSummary,
+ e.PaidAmount,
+ e.OccurredAt
+ }, context.CancellationToken);
+ }
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Consumers/OrderUrgeConsumer.cs b/src/Api/TakeoutSaaS.TenantApi/Consumers/OrderUrgeConsumer.cs
new file mode 100644
index 0000000..03c67f6
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Consumers/OrderUrgeConsumer.cs
@@ -0,0 +1,29 @@
+using MassTransit;
+using Microsoft.AspNetCore.SignalR;
+using TakeoutSaaS.Application.Messaging.Events;
+using TakeoutSaaS.TenantApi.Hubs;
+
+namespace TakeoutSaaS.TenantApi.Consumers;
+
+///
+/// 订单催单事件消费者 — 推送催单通知到看板。
+///
+public sealed class OrderUrgeConsumer(IHubContext hubContext)
+ : IConsumer
+{
+ ///
+ public async Task Consume(ConsumeContext context)
+ {
+ var e = context.Message;
+ var group = $"store:{e.TenantId}:{e.StoreId}";
+
+ // 1. 推送催单通知到对应门店 Group
+ await hubContext.Clients.Group(group).SendAsync("OrderUrged", new
+ {
+ e.OrderId,
+ e.OrderNo,
+ e.UrgeCount,
+ e.OccurredAt
+ }, context.CancellationToken);
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/AcceptOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/AcceptOrderCommand.cs
new file mode 100644
index 0000000..0a24590
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/AcceptOrderCommand.cs
@@ -0,0 +1,25 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+
+namespace TakeoutSaaS.Application.App.Orders.Commands;
+
+///
+/// 接单命令。
+///
+public sealed record AcceptOrderCommand : IRequest
+{
+ ///
+ /// 订单标识。
+ ///
+ public required long OrderId { get; init; }
+
+ ///
+ /// 租户标识。
+ ///
+ public required long TenantId { get; init; }
+
+ ///
+ /// 操作人标识。
+ ///
+ public long? OperatorId { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/CompletePreparationCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/CompletePreparationCommand.cs
new file mode 100644
index 0000000..67ec80f
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/CompletePreparationCommand.cs
@@ -0,0 +1,25 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+
+namespace TakeoutSaaS.Application.App.Orders.Commands;
+
+///
+/// 出餐完成命令。
+///
+public sealed record CompletePreparationCommand : IRequest
+{
+ ///
+ /// 订单标识。
+ ///
+ public required long OrderId { get; init; }
+
+ ///
+ /// 租户标识。
+ ///
+ public required long TenantId { get; init; }
+
+ ///
+ /// 操作人标识。
+ ///
+ public long? OperatorId { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/ConfirmDeliveryCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/ConfirmDeliveryCommand.cs
new file mode 100644
index 0000000..ca34232
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/ConfirmDeliveryCommand.cs
@@ -0,0 +1,25 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+
+namespace TakeoutSaaS.Application.App.Orders.Commands;
+
+///
+/// 确认送达/取餐命令。
+///
+public sealed record ConfirmDeliveryCommand : IRequest
+{
+ ///
+ /// 订单标识。
+ ///
+ public required long OrderId { get; init; }
+
+ ///
+ /// 租户标识。
+ ///
+ public required long TenantId { get; init; }
+
+ ///
+ /// 操作人标识。
+ ///
+ public long? OperatorId { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/RejectOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/RejectOrderCommand.cs
new file mode 100644
index 0000000..8116c73
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/RejectOrderCommand.cs
@@ -0,0 +1,30 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Orders.Dto;
+
+namespace TakeoutSaaS.Application.App.Orders.Commands;
+
+///
+/// 拒单命令。
+///
+public sealed record RejectOrderCommand : IRequest
+{
+ ///
+ /// 订单标识。
+ ///
+ public required long OrderId { get; init; }
+
+ ///
+ /// 租户标识。
+ ///
+ public required long TenantId { get; init; }
+
+ ///
+ /// 拒单原因。
+ ///
+ public required string Reason { get; init; }
+
+ ///
+ /// 操作人标识。
+ ///
+ public long? OperatorId { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/AcceptOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/AcceptOrderCommandHandler.cs
new file mode 100644
index 0000000..602caf9
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/AcceptOrderCommandHandler.cs
@@ -0,0 +1,98 @@
+using MediatR;
+using Microsoft.Extensions.Logging;
+using TakeoutSaaS.Application.App.Orders.Commands;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Application.Messaging;
+using TakeoutSaaS.Application.Messaging.Abstractions;
+using TakeoutSaaS.Application.Messaging.Events;
+using TakeoutSaaS.Domain.Orders.Entities;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Orders.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Ids;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 接单命令处理器。
+///
+public sealed class AcceptOrderCommandHandler(
+ IOrderRepository orderRepository,
+ IEventPublisher eventPublisher,
+ IIdGenerator idGenerator,
+ ILogger logger)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(AcceptOrderCommand request, CancellationToken cancellationToken)
+ {
+ // 1. 查找订单
+ var order = await orderRepository.FindByIdAsync(request.OrderId, request.TenantId, cancellationToken)
+ ?? throw new BusinessException(ErrorCodes.NotFound, "订单不存在");
+
+ // 2. 校验状态
+ if (order.Status != OrderStatus.AwaitingPreparation)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, $"当前状态 {order.Status} 不允许接单");
+ }
+
+ // 3. 更新状态
+ var oldStatus = order.Status;
+ order.Status = OrderStatus.InProgress;
+
+ // 4. 写入状态流转记录
+ var history = new OrderStatusHistory
+ {
+ Id = idGenerator.NextId(),
+ OrderId = order.Id,
+ TenantId = order.TenantId,
+ Status = OrderStatus.InProgress,
+ OperatorId = request.OperatorId,
+ Notes = "商户接单",
+ OccurredAt = DateTime.UtcNow
+ };
+
+ await orderRepository.UpdateOrderAsync(order, cancellationToken);
+ await orderRepository.AddStatusHistoryAsync(history, cancellationToken);
+ await orderRepository.SaveChangesAsync(cancellationToken);
+
+ // 5. 发布状态变更事件
+ await eventPublisher.PublishAsync(EventRoutingKeys.OrderStatusChanged, new OrderStatusChangedEvent
+ {
+ OrderId = order.Id,
+ OrderNo = order.OrderNo,
+ TenantId = order.TenantId,
+ StoreId = order.StoreId,
+ OldStatus = (int)oldStatus,
+ NewStatus = (int)order.Status,
+ Channel = (int)order.Channel,
+ DeliveryType = (int)order.DeliveryType,
+ CustomerName = order.CustomerName,
+ PaidAmount = order.PaidAmount,
+ OccurredAt = history.OccurredAt
+ }, cancellationToken);
+
+ // 6. 记录日志
+ logger.LogInformation("接单 {OrderNo} ({OrderId})", order.OrderNo, order.Id);
+
+ // 7. 返回看板卡片 DTO
+ return MapToCardDto(order);
+ }
+
+ private static OrderBoardCardDto MapToCardDto(Order order) => new()
+ {
+ Id = order.Id,
+ OrderNo = order.OrderNo,
+ StoreId = order.StoreId,
+ Channel = order.Channel,
+ DeliveryType = order.DeliveryType,
+ Status = order.Status,
+ CustomerName = order.CustomerName,
+ CustomerPhone = order.CustomerPhone,
+ TableNo = order.TableNo,
+ QueueNumber = order.QueueNumber,
+ PaidAmount = order.PaidAmount,
+ CreatedAt = order.CreatedAt
+ };
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CompletePreparationCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CompletePreparationCommandHandler.cs
new file mode 100644
index 0000000..292ad80
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CompletePreparationCommandHandler.cs
@@ -0,0 +1,98 @@
+using MediatR;
+using Microsoft.Extensions.Logging;
+using TakeoutSaaS.Application.App.Orders.Commands;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Application.Messaging;
+using TakeoutSaaS.Application.Messaging.Abstractions;
+using TakeoutSaaS.Application.Messaging.Events;
+using TakeoutSaaS.Domain.Orders.Entities;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Orders.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Ids;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 出餐完成命令处理器。
+///
+public sealed class CompletePreparationCommandHandler(
+ IOrderRepository orderRepository,
+ IEventPublisher eventPublisher,
+ IIdGenerator idGenerator,
+ ILogger logger)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(CompletePreparationCommand request, CancellationToken cancellationToken)
+ {
+ // 1. 查找订单
+ var order = await orderRepository.FindByIdAsync(request.OrderId, request.TenantId, cancellationToken)
+ ?? throw new BusinessException(ErrorCodes.NotFound, "订单不存在");
+
+ // 2. 校验状态
+ if (order.Status != OrderStatus.InProgress)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, $"当前状态 {order.Status} 不允许标记出餐完成");
+ }
+
+ // 3. 更新状态
+ var oldStatus = order.Status;
+ order.Status = OrderStatus.Ready;
+
+ // 4. 写入状态流转记录
+ var history = new OrderStatusHistory
+ {
+ Id = idGenerator.NextId(),
+ OrderId = order.Id,
+ TenantId = order.TenantId,
+ Status = OrderStatus.Ready,
+ OperatorId = request.OperatorId,
+ Notes = "出餐完成",
+ OccurredAt = DateTime.UtcNow
+ };
+
+ await orderRepository.UpdateOrderAsync(order, cancellationToken);
+ await orderRepository.AddStatusHistoryAsync(history, cancellationToken);
+ await orderRepository.SaveChangesAsync(cancellationToken);
+
+ // 5. 发布状态变更事件
+ await eventPublisher.PublishAsync(EventRoutingKeys.OrderStatusChanged, new OrderStatusChangedEvent
+ {
+ OrderId = order.Id,
+ OrderNo = order.OrderNo,
+ TenantId = order.TenantId,
+ StoreId = order.StoreId,
+ OldStatus = (int)oldStatus,
+ NewStatus = (int)order.Status,
+ Channel = (int)order.Channel,
+ DeliveryType = (int)order.DeliveryType,
+ CustomerName = order.CustomerName,
+ PaidAmount = order.PaidAmount,
+ OccurredAt = history.OccurredAt
+ }, cancellationToken);
+
+ // 6. 记录日志
+ logger.LogInformation("出餐完成 {OrderNo} ({OrderId})", order.OrderNo, order.Id);
+
+ // 7. 返回看板卡片 DTO
+ return MapToCardDto(order);
+ }
+
+ private static OrderBoardCardDto MapToCardDto(Order order) => new()
+ {
+ Id = order.Id,
+ OrderNo = order.OrderNo,
+ StoreId = order.StoreId,
+ Channel = order.Channel,
+ DeliveryType = order.DeliveryType,
+ Status = order.Status,
+ CustomerName = order.CustomerName,
+ CustomerPhone = order.CustomerPhone,
+ TableNo = order.TableNo,
+ QueueNumber = order.QueueNumber,
+ PaidAmount = order.PaidAmount,
+ CreatedAt = order.CreatedAt
+ };
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/ConfirmDeliveryCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/ConfirmDeliveryCommandHandler.cs
new file mode 100644
index 0000000..eb602a0
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/ConfirmDeliveryCommandHandler.cs
@@ -0,0 +1,99 @@
+using MediatR;
+using Microsoft.Extensions.Logging;
+using TakeoutSaaS.Application.App.Orders.Commands;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Application.Messaging;
+using TakeoutSaaS.Application.Messaging.Abstractions;
+using TakeoutSaaS.Application.Messaging.Events;
+using TakeoutSaaS.Domain.Orders.Entities;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Orders.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Ids;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 确认送达/取餐命令处理器。
+///
+public sealed class ConfirmDeliveryCommandHandler(
+ IOrderRepository orderRepository,
+ IEventPublisher eventPublisher,
+ IIdGenerator idGenerator,
+ ILogger logger)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(ConfirmDeliveryCommand request, CancellationToken cancellationToken)
+ {
+ // 1. 查找订单
+ var order = await orderRepository.FindByIdAsync(request.OrderId, request.TenantId, cancellationToken)
+ ?? throw new BusinessException(ErrorCodes.NotFound, "订单不存在");
+
+ // 2. 校验状态
+ if (order.Status != OrderStatus.Ready)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, $"当前状态 {order.Status} 不允许确认送达");
+ }
+
+ // 3. 更新状态
+ var oldStatus = order.Status;
+ order.Status = OrderStatus.Completed;
+ order.FinishedAt = DateTime.UtcNow;
+
+ // 4. 写入状态流转记录
+ var history = new OrderStatusHistory
+ {
+ Id = idGenerator.NextId(),
+ OrderId = order.Id,
+ TenantId = order.TenantId,
+ Status = OrderStatus.Completed,
+ OperatorId = request.OperatorId,
+ Notes = "确认送达/取餐",
+ OccurredAt = DateTime.UtcNow
+ };
+
+ await orderRepository.UpdateOrderAsync(order, cancellationToken);
+ await orderRepository.AddStatusHistoryAsync(history, cancellationToken);
+ await orderRepository.SaveChangesAsync(cancellationToken);
+
+ // 5. 发布状态变更事件
+ await eventPublisher.PublishAsync(EventRoutingKeys.OrderStatusChanged, new OrderStatusChangedEvent
+ {
+ OrderId = order.Id,
+ OrderNo = order.OrderNo,
+ TenantId = order.TenantId,
+ StoreId = order.StoreId,
+ OldStatus = (int)oldStatus,
+ NewStatus = (int)order.Status,
+ Channel = (int)order.Channel,
+ DeliveryType = (int)order.DeliveryType,
+ CustomerName = order.CustomerName,
+ PaidAmount = order.PaidAmount,
+ OccurredAt = history.OccurredAt
+ }, cancellationToken);
+
+ // 6. 记录日志
+ logger.LogInformation("确认送达 {OrderNo} ({OrderId})", order.OrderNo, order.Id);
+
+ // 7. 返回看板卡片 DTO
+ return MapToCardDto(order);
+ }
+
+ private static OrderBoardCardDto MapToCardDto(Order order) => new()
+ {
+ Id = order.Id,
+ OrderNo = order.OrderNo,
+ StoreId = order.StoreId,
+ Channel = order.Channel,
+ DeliveryType = order.DeliveryType,
+ Status = order.Status,
+ CustomerName = order.CustomerName,
+ CustomerPhone = order.CustomerPhone,
+ TableNo = order.TableNo,
+ QueueNumber = order.QueueNumber,
+ PaidAmount = order.PaidAmount,
+ CreatedAt = order.CreatedAt
+ };
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs
index 9b3cc0f..4119738 100644
--- a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs
@@ -2,6 +2,9 @@ using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.Orders.Commands;
using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Application.Messaging;
+using TakeoutSaaS.Application.Messaging.Abstractions;
+using TakeoutSaaS.Application.Messaging.Events;
using TakeoutSaaS.Domain.Orders.Entities;
using TakeoutSaaS.Domain.Orders.Repositories;
using TakeoutSaaS.Shared.Abstractions.Ids;
@@ -14,6 +17,7 @@ namespace TakeoutSaaS.Application.App.Orders.Handlers;
public sealed class CreateOrderCommandHandler(
IOrderRepository orderRepository,
IIdGenerator idGenerator,
+ IEventPublisher eventPublisher,
ILogger logger)
: IRequestHandler
{
@@ -81,10 +85,31 @@ public sealed class CreateOrderCommandHandler(
await orderRepository.SaveChangesAsync(cancellationToken);
- // 5. 记录日志
+ // 5. 发布订单创建事件
+ var itemsSummary = items.Count > 0
+ ? string.Join("、", items.Take(3).Select(x => x.ProductName))
+ + (items.Count > 3 ? $" 等{items.Count}件" : string.Empty)
+ : string.Empty;
+
+ await eventPublisher.PublishAsync(EventRoutingKeys.OrderCreated, new OrderCreatedEvent
+ {
+ OrderId = order.Id,
+ OrderNo = order.OrderNo,
+ Amount = order.PaidAmount,
+ TenantId = order.TenantId,
+ StoreId = order.StoreId,
+ Channel = (int)order.Channel,
+ DeliveryType = (int)order.DeliveryType,
+ CustomerName = order.CustomerName,
+ ItemsSummary = itemsSummary,
+ TableNo = order.TableNo,
+ CreatedAt = order.CreatedAt
+ }, cancellationToken);
+
+ // 6. 记录日志
logger.LogInformation("创建订单 {OrderNo} ({OrderId})", order.OrderNo, order.Id);
- // 6. 返回 DTO
+ // 7. 返回 DTO
return MapToDto(order, items, [], []);
}
diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/RejectOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/RejectOrderCommandHandler.cs
new file mode 100644
index 0000000..0590cf4
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/RejectOrderCommandHandler.cs
@@ -0,0 +1,100 @@
+using MediatR;
+using Microsoft.Extensions.Logging;
+using TakeoutSaaS.Application.App.Orders.Commands;
+using TakeoutSaaS.Application.App.Orders.Dto;
+using TakeoutSaaS.Application.Messaging;
+using TakeoutSaaS.Application.Messaging.Abstractions;
+using TakeoutSaaS.Application.Messaging.Events;
+using TakeoutSaaS.Domain.Orders.Entities;
+using TakeoutSaaS.Domain.Orders.Enums;
+using TakeoutSaaS.Domain.Orders.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Ids;
+
+namespace TakeoutSaaS.Application.App.Orders.Handlers;
+
+///
+/// 拒单命令处理器。
+///
+public sealed class RejectOrderCommandHandler(
+ IOrderRepository orderRepository,
+ IEventPublisher eventPublisher,
+ IIdGenerator idGenerator,
+ ILogger logger)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(RejectOrderCommand request, CancellationToken cancellationToken)
+ {
+ // 1. 查找订单
+ var order = await orderRepository.FindByIdAsync(request.OrderId, request.TenantId, cancellationToken)
+ ?? throw new BusinessException(ErrorCodes.NotFound, "订单不存在");
+
+ // 2. 校验状态
+ if (order.Status != OrderStatus.AwaitingPreparation)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, $"当前状态 {order.Status} 不允许拒单");
+ }
+
+ // 3. 更新状态
+ var oldStatus = order.Status;
+ order.Status = OrderStatus.Cancelled;
+ order.CancelledAt = DateTime.UtcNow;
+ order.CancelReason = request.Reason;
+
+ // 4. 写入状态流转记录
+ var history = new OrderStatusHistory
+ {
+ Id = idGenerator.NextId(),
+ OrderId = order.Id,
+ TenantId = order.TenantId,
+ Status = OrderStatus.Cancelled,
+ OperatorId = request.OperatorId,
+ Notes = $"商户拒单:{request.Reason}",
+ OccurredAt = DateTime.UtcNow
+ };
+
+ await orderRepository.UpdateOrderAsync(order, cancellationToken);
+ await orderRepository.AddStatusHistoryAsync(history, cancellationToken);
+ await orderRepository.SaveChangesAsync(cancellationToken);
+
+ // 5. 发布状态变更事件
+ await eventPublisher.PublishAsync(EventRoutingKeys.OrderStatusChanged, new OrderStatusChangedEvent
+ {
+ OrderId = order.Id,
+ OrderNo = order.OrderNo,
+ TenantId = order.TenantId,
+ StoreId = order.StoreId,
+ OldStatus = (int)oldStatus,
+ NewStatus = (int)order.Status,
+ Channel = (int)order.Channel,
+ DeliveryType = (int)order.DeliveryType,
+ CustomerName = order.CustomerName,
+ PaidAmount = order.PaidAmount,
+ OccurredAt = history.OccurredAt
+ }, cancellationToken);
+
+ // 6. 记录日志
+ logger.LogInformation("拒单 {OrderNo} ({OrderId}),原因:{Reason}", order.OrderNo, order.Id, request.Reason);
+
+ // 7. 返回看板卡片 DTO
+ return MapToCardDto(order);
+ }
+
+ private static OrderBoardCardDto MapToCardDto(Order order) => new()
+ {
+ Id = order.Id,
+ OrderNo = order.OrderNo,
+ StoreId = order.StoreId,
+ Channel = order.Channel,
+ DeliveryType = order.DeliveryType,
+ Status = order.Status,
+ CustomerName = order.CustomerName,
+ CustomerPhone = order.CustomerPhone,
+ TableNo = order.TableNo,
+ QueueNumber = order.QueueNumber,
+ PaidAmount = order.PaidAmount,
+ CreatedAt = order.CreatedAt
+ };
+}
diff --git a/src/Application/TakeoutSaaS.Application/Messaging/EventRoutingKeys.cs b/src/Application/TakeoutSaaS.Application/Messaging/EventRoutingKeys.cs
index c161ef3..bbfe2d1 100644
--- a/src/Application/TakeoutSaaS.Application/Messaging/EventRoutingKeys.cs
+++ b/src/Application/TakeoutSaaS.Application/Messaging/EventRoutingKeys.cs
@@ -10,6 +10,16 @@ public static class EventRoutingKeys
///
public const string OrderCreated = "orders.created";
+ ///
+ /// 订单状态变更事件路由键。
+ ///
+ public const string OrderStatusChanged = "orders.status-changed";
+
+ ///
+ /// 订单催单事件路由键。
+ ///
+ public const string OrderUrged = "orders.urged";
+
///
/// 支付成功事件路由键。
///
diff --git a/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs b/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs
index 2a84f05..4605b6f 100644
--- a/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs
+++ b/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs
@@ -25,6 +25,36 @@ public sealed class OrderCreatedEvent
///
public long TenantId { get; init; }
+ ///
+ /// 门店标识。
+ ///
+ public long StoreId { get; init; }
+
+ ///
+ /// 下单渠道。
+ ///
+ public int Channel { get; init; }
+
+ ///
+ /// 履约类型。
+ ///
+ public int DeliveryType { get; init; }
+
+ ///
+ /// 顾客姓名。
+ ///
+ public string? CustomerName { get; init; }
+
+ ///
+ /// 商品摘要。
+ ///
+ public string? ItemsSummary { get; init; }
+
+ ///
+ /// 桌号。
+ ///
+ public string? TableNo { get; init; }
+
///
/// 创建时间(UTC)。
///
diff --git a/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderStatusChangedEvent.cs b/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderStatusChangedEvent.cs
new file mode 100644
index 0000000..5d3877c
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderStatusChangedEvent.cs
@@ -0,0 +1,67 @@
+namespace TakeoutSaaS.Application.Messaging.Events;
+
+///
+/// 订单状态变更事件。
+///
+public sealed class OrderStatusChangedEvent
+{
+ ///
+ /// 订单标识。
+ ///
+ public long OrderId { get; init; }
+
+ ///
+ /// 订单编号。
+ ///
+ public string OrderNo { get; init; } = string.Empty;
+
+ ///
+ /// 所属租户。
+ ///
+ public long TenantId { get; init; }
+
+ ///
+ /// 门店标识。
+ ///
+ public long StoreId { get; init; }
+
+ ///
+ /// 变更前状态。
+ ///
+ public int OldStatus { get; init; }
+
+ ///
+ /// 变更后状态。
+ ///
+ public int NewStatus { get; init; }
+
+ ///
+ /// 下单渠道。
+ ///
+ public int Channel { get; init; }
+
+ ///
+ /// 履约类型。
+ ///
+ public int DeliveryType { get; init; }
+
+ ///
+ /// 顾客姓名。
+ ///
+ public string? CustomerName { get; init; }
+
+ ///
+ /// 商品摘要。
+ ///
+ public string? ItemsSummary { get; init; }
+
+ ///
+ /// 实付金额。
+ ///
+ public decimal PaidAmount { get; init; }
+
+ ///
+ /// 发生时间(UTC)。
+ ///
+ public DateTime OccurredAt { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderUrgeEvent.cs b/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderUrgeEvent.cs
new file mode 100644
index 0000000..7406187
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderUrgeEvent.cs
@@ -0,0 +1,37 @@
+namespace TakeoutSaaS.Application.Messaging.Events;
+
+///
+/// 订单催单事件。
+///
+public sealed class OrderUrgeEvent
+{
+ ///
+ /// 订单标识。
+ ///
+ public long OrderId { get; init; }
+
+ ///
+ /// 订单编号。
+ ///
+ public string OrderNo { get; init; } = string.Empty;
+
+ ///
+ /// 所属租户。
+ ///
+ public long TenantId { get; init; }
+
+ ///
+ /// 门店标识。
+ ///
+ public long StoreId { get; init; }
+
+ ///
+ /// 催单次数。
+ ///
+ public int UrgeCount { get; init; }
+
+ ///
+ /// 发生时间(UTC)。
+ ///
+ public DateTime OccurredAt { get; init; }
+}