feat: 添加订单事件管道与状态流转命令
Some checks failed
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Failing after 12s
Some checks failed
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Failing after 12s
- 新增 OrderStatusChangedEvent、OrderUrgeEvent 事件类型 - 扩展 EventRoutingKeys(orders.status-changed、orders.urged) - 丰富 OrderCreatedEvent(StoreId、Channel、DeliveryType 等字段) - CreateOrderCommandHandler 注入 IEventPublisher 并发布事件 - 新增接单/拒单/出餐完成/确认送达 Command + Handler(4对) - 新增 MassTransit Consumer(OrderCreated/StatusChanged/Urge → SignalR) - Program.cs 注册 3 个 Consumer
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using TakeoutSaaS.Application.Messaging.Events;
|
||||
using TakeoutSaaS.TenantApi.Hubs;
|
||||
|
||||
namespace TakeoutSaaS.TenantApi.Consumers;
|
||||
|
||||
/// <summary>
|
||||
/// 订单创建事件消费者 — 推送新订单到看板。
|
||||
/// </summary>
|
||||
public sealed class OrderCreatedConsumer(IHubContext<OrderBoardHub> hubContext)
|
||||
: IConsumer<OrderCreatedEvent>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task Consume(ConsumeContext<OrderCreatedEvent> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using TakeoutSaaS.Application.Messaging.Events;
|
||||
using TakeoutSaaS.TenantApi.Hubs;
|
||||
|
||||
namespace TakeoutSaaS.TenantApi.Consumers;
|
||||
|
||||
/// <summary>
|
||||
/// 订单状态变更事件消费者 — 推送状态更新到看板。
|
||||
/// </summary>
|
||||
public sealed class OrderStatusChangedConsumer(IHubContext<OrderBoardHub> hubContext)
|
||||
: IConsumer<OrderStatusChangedEvent>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task Consume(ConsumeContext<OrderStatusChangedEvent> 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);
|
||||
}
|
||||
}
|
||||
29
src/Api/TakeoutSaaS.TenantApi/Consumers/OrderUrgeConsumer.cs
Normal file
29
src/Api/TakeoutSaaS.TenantApi/Consumers/OrderUrgeConsumer.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using TakeoutSaaS.Application.Messaging.Events;
|
||||
using TakeoutSaaS.TenantApi.Hubs;
|
||||
|
||||
namespace TakeoutSaaS.TenantApi.Consumers;
|
||||
|
||||
/// <summary>
|
||||
/// 订单催单事件消费者 — 推送催单通知到看板。
|
||||
/// </summary>
|
||||
public sealed class OrderUrgeConsumer(IHubContext<OrderBoardHub> hubContext)
|
||||
: IConsumer<OrderUrgeEvent>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task Consume(ConsumeContext<OrderUrgeEvent> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Orders.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Orders.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 接单命令。
|
||||
/// </summary>
|
||||
public sealed record AcceptOrderCommand : IRequest<OrderBoardCardDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单标识。
|
||||
/// </summary>
|
||||
public required long OrderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户标识。
|
||||
/// </summary>
|
||||
public required long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作人标识。
|
||||
/// </summary>
|
||||
public long? OperatorId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Orders.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Orders.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 出餐完成命令。
|
||||
/// </summary>
|
||||
public sealed record CompletePreparationCommand : IRequest<OrderBoardCardDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单标识。
|
||||
/// </summary>
|
||||
public required long OrderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户标识。
|
||||
/// </summary>
|
||||
public required long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作人标识。
|
||||
/// </summary>
|
||||
public long? OperatorId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Orders.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Orders.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 确认送达/取餐命令。
|
||||
/// </summary>
|
||||
public sealed record ConfirmDeliveryCommand : IRequest<OrderBoardCardDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单标识。
|
||||
/// </summary>
|
||||
public required long OrderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户标识。
|
||||
/// </summary>
|
||||
public required long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作人标识。
|
||||
/// </summary>
|
||||
public long? OperatorId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Orders.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Orders.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 拒单命令。
|
||||
/// </summary>
|
||||
public sealed record RejectOrderCommand : IRequest<OrderBoardCardDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单标识。
|
||||
/// </summary>
|
||||
public required long OrderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户标识。
|
||||
/// </summary>
|
||||
public required long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 拒单原因。
|
||||
/// </summary>
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作人标识。
|
||||
/// </summary>
|
||||
public long? OperatorId { get; init; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 接单命令处理器。
|
||||
/// </summary>
|
||||
public sealed class AcceptOrderCommandHandler(
|
||||
IOrderRepository orderRepository,
|
||||
IEventPublisher eventPublisher,
|
||||
IIdGenerator idGenerator,
|
||||
ILogger<AcceptOrderCommandHandler> logger)
|
||||
: IRequestHandler<AcceptOrderCommand, OrderBoardCardDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<OrderBoardCardDto> 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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 出餐完成命令处理器。
|
||||
/// </summary>
|
||||
public sealed class CompletePreparationCommandHandler(
|
||||
IOrderRepository orderRepository,
|
||||
IEventPublisher eventPublisher,
|
||||
IIdGenerator idGenerator,
|
||||
ILogger<CompletePreparationCommandHandler> logger)
|
||||
: IRequestHandler<CompletePreparationCommand, OrderBoardCardDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<OrderBoardCardDto> 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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 确认送达/取餐命令处理器。
|
||||
/// </summary>
|
||||
public sealed class ConfirmDeliveryCommandHandler(
|
||||
IOrderRepository orderRepository,
|
||||
IEventPublisher eventPublisher,
|
||||
IIdGenerator idGenerator,
|
||||
ILogger<ConfirmDeliveryCommandHandler> logger)
|
||||
: IRequestHandler<ConfirmDeliveryCommand, OrderBoardCardDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<OrderBoardCardDto> 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
|
||||
};
|
||||
}
|
||||
@@ -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<CreateOrderCommandHandler> logger)
|
||||
: IRequestHandler<CreateOrderCommand, OrderDto>
|
||||
{
|
||||
@@ -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, [], []);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 拒单命令处理器。
|
||||
/// </summary>
|
||||
public sealed class RejectOrderCommandHandler(
|
||||
IOrderRepository orderRepository,
|
||||
IEventPublisher eventPublisher,
|
||||
IIdGenerator idGenerator,
|
||||
ILogger<RejectOrderCommandHandler> logger)
|
||||
: IRequestHandler<RejectOrderCommand, OrderBoardCardDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<OrderBoardCardDto> 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
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,16 @@ public static class EventRoutingKeys
|
||||
/// </summary>
|
||||
public const string OrderCreated = "orders.created";
|
||||
|
||||
/// <summary>
|
||||
/// 订单状态变更事件路由键。
|
||||
/// </summary>
|
||||
public const string OrderStatusChanged = "orders.status-changed";
|
||||
|
||||
/// <summary>
|
||||
/// 订单催单事件路由键。
|
||||
/// </summary>
|
||||
public const string OrderUrged = "orders.urged";
|
||||
|
||||
/// <summary>
|
||||
/// 支付成功事件路由键。
|
||||
/// </summary>
|
||||
|
||||
@@ -25,6 +25,36 @@ public sealed class OrderCreatedEvent
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 下单渠道。
|
||||
/// </summary>
|
||||
public int Channel { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 履约类型。
|
||||
/// </summary>
|
||||
public int DeliveryType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 顾客姓名。
|
||||
/// </summary>
|
||||
public string? CustomerName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品摘要。
|
||||
/// </summary>
|
||||
public string? ItemsSummary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 桌号。
|
||||
/// </summary>
|
||||
public string? TableNo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间(UTC)。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace TakeoutSaaS.Application.Messaging.Events;
|
||||
|
||||
/// <summary>
|
||||
/// 订单状态变更事件。
|
||||
/// </summary>
|
||||
public sealed class OrderStatusChangedEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单标识。
|
||||
/// </summary>
|
||||
public long OrderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单编号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 所属租户。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 变更前状态。
|
||||
/// </summary>
|
||||
public int OldStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 变更后状态。
|
||||
/// </summary>
|
||||
public int NewStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 下单渠道。
|
||||
/// </summary>
|
||||
public int Channel { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 履约类型。
|
||||
/// </summary>
|
||||
public int DeliveryType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 顾客姓名。
|
||||
/// </summary>
|
||||
public string? CustomerName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品摘要。
|
||||
/// </summary>
|
||||
public string? ItemsSummary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 实付金额。
|
||||
/// </summary>
|
||||
public decimal PaidAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 发生时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime OccurredAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace TakeoutSaaS.Application.Messaging.Events;
|
||||
|
||||
/// <summary>
|
||||
/// 订单催单事件。
|
||||
/// </summary>
|
||||
public sealed class OrderUrgeEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单标识。
|
||||
/// </summary>
|
||||
public long OrderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单编号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 所属租户。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 催单次数。
|
||||
/// </summary>
|
||||
public int UrgeCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 发生时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime OccurredAt { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user