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; } +}