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 Microsoft.Extensions.Logging;
|
||||||
using TakeoutSaaS.Application.App.Orders.Commands;
|
using TakeoutSaaS.Application.App.Orders.Commands;
|
||||||
using TakeoutSaaS.Application.App.Orders.Dto;
|
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.Entities;
|
||||||
using TakeoutSaaS.Domain.Orders.Repositories;
|
using TakeoutSaaS.Domain.Orders.Repositories;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||||
@@ -14,6 +17,7 @@ namespace TakeoutSaaS.Application.App.Orders.Handlers;
|
|||||||
public sealed class CreateOrderCommandHandler(
|
public sealed class CreateOrderCommandHandler(
|
||||||
IOrderRepository orderRepository,
|
IOrderRepository orderRepository,
|
||||||
IIdGenerator idGenerator,
|
IIdGenerator idGenerator,
|
||||||
|
IEventPublisher eventPublisher,
|
||||||
ILogger<CreateOrderCommandHandler> logger)
|
ILogger<CreateOrderCommandHandler> logger)
|
||||||
: IRequestHandler<CreateOrderCommand, OrderDto>
|
: IRequestHandler<CreateOrderCommand, OrderDto>
|
||||||
{
|
{
|
||||||
@@ -81,10 +85,31 @@ public sealed class CreateOrderCommandHandler(
|
|||||||
|
|
||||||
await orderRepository.SaveChangesAsync(cancellationToken);
|
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);
|
logger.LogInformation("创建订单 {OrderNo} ({OrderId})", order.OrderNo, order.Id);
|
||||||
|
|
||||||
// 6. 返回 DTO
|
// 7. 返回 DTO
|
||||||
return MapToDto(order, items, [], []);
|
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>
|
/// </summary>
|
||||||
public const string OrderCreated = "orders.created";
|
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>
|
||||||
/// 支付成功事件路由键。
|
/// 支付成功事件路由键。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -25,6 +25,36 @@ public sealed class OrderCreatedEvent
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public long TenantId { get; init; }
|
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>
|
/// <summary>
|
||||||
/// 创建时间(UTC)。
|
/// 创建时间(UTC)。
|
||||||
/// </summary>
|
/// </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